├── docs ├── components │ ├── CodeNpm.mdx │ ├── CodePnpm.mdx │ ├── CodeYarn.mdx │ ├── HeroCode.mdx │ ├── Footer.tsx │ ├── Example.tsx │ └── Hero.tsx ├── public │ ├── og.jpg │ ├── og.png │ ├── favicon.png │ └── favicon.svg ├── .eslintrc.json ├── fonts │ └── inter.woff2 ├── scripts │ ├── dev.sh │ ├── build.sh │ └── start.sh ├── postcss.config.js ├── app │ ├── api │ │ └── search │ │ │ └── route.ts │ ├── (home) │ │ ├── layout.tsx │ │ └── page.tsx │ ├── robots.ts │ ├── docs │ │ ├── layout.tsx │ │ └── [[...slug]] │ │ │ └── page.tsx │ ├── global.css │ ├── layout.config.tsx │ ├── sitemap.ts │ └── layout.tsx ├── source.config.ts ├── lib │ └── source.ts ├── content │ └── docs │ │ ├── meta.json │ │ ├── selectors.mdx │ │ ├── defaults.mdx │ │ ├── presets.mdx │ │ ├── changelog.mdx │ │ ├── scrolltrigger.mdx │ │ ├── install.mdx │ │ ├── index.mdx │ │ ├── breakpoints.mdx │ │ ├── syntax.mdx │ │ └── timelines.mdx ├── .gitignore ├── next.config.mjs ├── tailwind.config.js ├── README.md ├── tsconfig.json └── package.json ├── image.png ├── pnpm-workspace.yaml ├── .gitattributes ├── .gitignore ├── turbo.json ├── packages └── core │ ├── vitest.config.unit.ts │ ├── src │ ├── utils │ │ ├── isObject.ts │ │ ├── getSelectorFromBracket.ts │ │ ├── debounce.ts │ │ ├── parseTimeline.ts │ │ ├── getSelectorOrElement.ts │ │ ├── mergeDeep.ts │ │ ├── parseMediaQueries.ts │ │ ├── castValue.ts │ │ └── parseToObject.ts │ ├── types.ts │ └── index.ts │ ├── playwright.config.ts │ ├── tsconfig.json │ ├── dist │ ├── index.d.ts │ └── index.es.js │ ├── scripts │ └── postbuild.js │ ├── tests │ ├── unit │ │ ├── utils │ │ │ ├── debounce.test.ts │ │ │ ├── isObject.test.ts │ │ │ ├── getSelectorFromBracket.test.ts │ │ │ ├── castValue.test.ts │ │ │ ├── parseMediaQueries.test.ts │ │ │ ├── getSelectorOrElement.test.ts │ │ │ ├── parseTimeline.test.ts │ │ │ ├── parseToObject.test.ts │ │ │ └── mergeDeep.test.ts │ │ └── index.test.ts │ └── e2e │ │ └── index.spec.ts │ ├── vite.config.ts │ ├── package.json │ ├── README.md │ ├── index.ts │ ├── index.html │ └── pnpm-lock.yaml ├── package.json ├── LICENSE ├── CHANGELOG.md └── README.md /docs/components/CodeNpm.mdx: -------------------------------------------------------------------------------- 1 | ```bash 2 | npm i glazejs 3 | ``` 4 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnnsjsk/glaze/HEAD/image.png -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - docs 4 | -------------------------------------------------------------------------------- /docs/components/CodePnpm.mdx: -------------------------------------------------------------------------------- 1 | ```bash 2 | pnpm add glazejs 3 | ``` 4 | -------------------------------------------------------------------------------- /docs/components/CodeYarn.mdx: -------------------------------------------------------------------------------- 1 | ```bash 2 | yarn add glazejs 3 | ``` 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /docs/public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnnsjsk/glaze/HEAD/docs/public/og.jpg -------------------------------------------------------------------------------- /docs/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnnsjsk/glaze/HEAD/docs/public/og.png -------------------------------------------------------------------------------- /docs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/fonts/inter.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnnsjsk/glaze/HEAD/docs/fonts/inter.woff2 -------------------------------------------------------------------------------- /docs/scripts/dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $HOME/.nvm/nvm.sh 3 | nvm use 20 4 | next dev 5 | -------------------------------------------------------------------------------- /docs/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnnsjsk/glaze/HEAD/docs/public/favicon.png -------------------------------------------------------------------------------- /docs/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $HOME/.nvm/nvm.sh 3 | nvm use 20 4 | next build 5 | -------------------------------------------------------------------------------- /docs/scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $HOME/.nvm/nvm.sh 3 | nvm use 20 4 | next start 5 | -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | **/node_modules 4 | 5 | # misc 6 | .DS_Store 7 | 8 | # intellij 9 | .idea/ 10 | 11 | .next 12 | .turbo 13 | test-results 14 | -------------------------------------------------------------------------------- /docs/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { source } from '@/lib/source'; 2 | import { createFromSource } from 'fumadocs-core/search/server'; 3 | 4 | export const { GET } = createFromSource(source); 5 | -------------------------------------------------------------------------------- /docs/source.config.ts: -------------------------------------------------------------------------------- 1 | import { defineDocs, defineConfig } from 'fumadocs-mdx/config'; 2 | 3 | export const { docs, meta } = defineDocs({ 4 | dir: 'content/docs', 5 | }); 6 | 7 | export default defineConfig(); 8 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "dependsOn": [ 5 | "^build" 6 | ], 7 | "outputs": [ 8 | "dist/**" 9 | ] 10 | }, 11 | "dev": { 12 | "cache": false 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/lib/source.ts: -------------------------------------------------------------------------------- 1 | import { docs, meta } from '@/.source'; 2 | import { createMDXSource } from 'fumadocs-mdx'; 3 | import { loader } from 'fumadocs-core/source'; 4 | 5 | export const source = loader({ 6 | baseUrl: '/docs', 7 | source: createMDXSource(docs, meta), 8 | }); 9 | -------------------------------------------------------------------------------- /packages/core/vitest.config.unit.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import config from "./vite.config"; 3 | 4 | export default defineConfig({ 5 | ...config, 6 | test: { 7 | environment: "jsdom", 8 | include: ["tests/unit/**/*.test.ts"], 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/core/src/utils/isObject.ts: -------------------------------------------------------------------------------- 1 | function isObject(item: any): item is Record { 2 | return ( 3 | item !== null && 4 | typeof item === "object" && 5 | !Array.isArray(item) && 6 | !(item instanceof HTMLElement) 7 | ); 8 | } 9 | 10 | export default isObject; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glaze-monorepo", 3 | "private": true, 4 | "scripts": { 5 | "prebuild": "rm -rf docs/.next || true", 6 | "build": "npm run prebuild && npx turbo run build" 7 | }, 8 | "devDependencies": { 9 | "turbo": "latest" 10 | }, 11 | "packageManager": "pnpm@8.15.1" 12 | } 13 | -------------------------------------------------------------------------------- /docs/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { HomeLayout } from 'fumadocs-ui/layouts/home'; 3 | import { baseOptions } from '@/app/layout.config'; 4 | 5 | export default function Layout({ children }: { children: ReactNode }) { 6 | return {children}; 7 | } 8 | -------------------------------------------------------------------------------- /docs/app/robots.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from "next"; 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: "*", 7 | allow: "/", 8 | }, 9 | sitemap: "https://glaze.dev/sitemap.xml", 10 | host: "https://glaze.dev", 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /docs/content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "---Getting Started---", 4 | "index", 5 | "install", 6 | "---Usage---", 7 | "syntax", 8 | "selectors", 9 | "breakpoints", 10 | "timelines", 11 | "scrolltrigger", 12 | "defaults", 13 | "presets", 14 | "changelog" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | export default defineConfig({ 4 | testDir: "./tests/e2e", 5 | projects: [ 6 | { 7 | name: "chromium", 8 | use: { ...devices["Desktop Chrome"] }, 9 | }, 10 | ], 11 | use: { 12 | // headless: false, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/core/src/utils/getSelectorFromBracket.ts: -------------------------------------------------------------------------------- 1 | function getSelectorFromBracket(inputString: string) { 2 | const regex = /^\[([^\]]+)]:(.*)/; 3 | const match = inputString.match(regex); 4 | 5 | if (match) { 6 | return { 7 | content: match[1], 8 | restOfString: match[2], 9 | }; 10 | } 11 | return null; 12 | } 13 | 14 | export default getSelectorFromBracket; 15 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | /node_modules 3 | 4 | # generated content 5 | .contentlayer 6 | .content-collections 7 | .source 8 | 9 | # test & build 10 | /coverage 11 | /.next/ 12 | /out/ 13 | /build 14 | *.tsbuildinfo 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | /.pnp 20 | .pnp.js 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # others 26 | .env*.local 27 | .vercel 28 | next-env.d.ts -------------------------------------------------------------------------------- /docs/components/HeroCode.mdx: -------------------------------------------------------------------------------- 1 | ```jsx {1} 2 |

3 | {"Utility-based animations for the web.".split(" ").map((w, i, arr) => ( 4 | 5 | {w} 6 | {w < arr.length - 1 ?   : ""} 7 | 8 | ))} 9 |

10 | ``` 11 | -------------------------------------------------------------------------------- /docs/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DocsLayout } from 'fumadocs-ui/layouts/docs'; 2 | import type { ReactNode } from 'react'; 3 | import { baseOptions } from '@/app/layout.config'; 4 | import { source } from '@/lib/source'; 5 | 6 | export default function Layout({ children }: { children: ReactNode }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | function debounce void>( 2 | func: T, 3 | wait: number, 4 | ): T { 5 | let timeoutId: number | null = null; 6 | 7 | return function (...args: Parameters): void { 8 | if (timeoutId !== null) { 9 | clearTimeout(timeoutId); 10 | } 11 | 12 | timeoutId = window.setTimeout(() => { 13 | func(...args); 14 | }, wait); 15 | } as T; 16 | } 17 | 18 | export default debounce; 19 | -------------------------------------------------------------------------------- /docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createMDX } from 'fumadocs-mdx/next'; 2 | 3 | const withMDX = createMDX(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const config = { 7 | reactStrictMode: true, 8 | trailingSlash: true, 9 | async redirects() { 10 | return [ 11 | { 12 | source: '/documentation/:path*', 13 | destination: '/docs/:path*', 14 | permanent: true, 15 | }, 16 | ]; 17 | }, 18 | }; 19 | 20 | export default withMDX(config); 21 | -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { createPreset } from "fumadocs-ui/tailwind-plugin"; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: [ 7 | "./components/**/*.{ts,tsx}", 8 | "./app/**/*.{ts,tsx}", 9 | "./content/**/*.{md,mdx}", 10 | "./mdx-components.{ts,tsx}", 11 | "./node_modules/fumadocs-ui/dist/**/*.js", 12 | ], 13 | presets: [ 14 | createPreset({ 15 | preset: "neutral", 16 | }), 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # docs2 2 | 3 | This is a Next.js application generated with 4 | [Create Fumadocs](https://github.com/fuma-nama/fumadocs). 5 | 6 | Run development server: 7 | 8 | ```bash 9 | npm run dev 10 | # or 11 | pnpm dev 12 | # or 13 | yarn dev 14 | ``` 15 | 16 | Open http://localhost:3000 with your browser to see the result. 17 | 18 | ## Learn More 19 | 20 | To learn more about Next.js and Fumadocs, take a look at the following 21 | resources: 22 | 23 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js 24 | features and API. 25 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 26 | - [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs 27 | -------------------------------------------------------------------------------- /docs/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | export const Footer = () => { 2 | return ( 3 |
4 |
5 |

6 | MIT License © {new Date().getFullYear()}{" "} 7 | 12 | dennn.is 13 | 14 |

15 |
16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | }, 7 | "target": "ES2020", 8 | "useDefineForClassFields": true, 9 | "lib": [ 10 | "ES2020", 11 | "DOM", 12 | "DOM.Iterable", 13 | "ES2021" 14 | ], 15 | "module": "ESNext", 16 | "skipLibCheck": true, 17 | "moduleResolution": "bundler", 18 | "allowImportingTsExtensions": true, 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "esModuleInterop": true 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/utils/parseTimeline.ts: -------------------------------------------------------------------------------- 1 | import { GlazeConfig } from "@/types.ts"; 2 | 3 | function parseTimeline( 4 | input: string, 5 | breakpoints: GlazeConfig["breakpoints"], 6 | defaultBp: string, 7 | ) { 8 | const regex = /(?:@(\w+):)?tl(?:\/(\w+))?/; 9 | const match = input.match(regex); 10 | 11 | if (match) { 12 | const breakpoint = match[1]; 13 | const id = match[2] ?? ""; 14 | const result = { id, breakpoint: defaultBp }; 15 | 16 | if (breakpoint) { 17 | if (!breakpoints?.[breakpoint]) return null; 18 | result.breakpoint = breakpoints[breakpoint] ?? defaultBp; 19 | } 20 | 21 | return result; 22 | } else { 23 | return null; 24 | } 25 | } 26 | 27 | export default parseTimeline; 28 | -------------------------------------------------------------------------------- /docs/app/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --fd-primary: 36 90% 56%; 7 | } 8 | 9 | html { 10 | font-feature-settings: "calt", "cv02", "cv03", "cv04", "cv11" !important; 11 | font-variation-settings: normal !important; 12 | 13 | &.dark { 14 | body { 15 | --fd-primary: 40 90% 56%; 16 | } 17 | } 18 | } 19 | 20 | #nd-home-layout { 21 | @apply min-h-screen; 22 | } 23 | 24 | #nd-nav { 25 | @apply max-w-[1266px] !important; 26 | } 27 | 28 | #nd-sidebar { 29 | [data-search-full] { 30 | @apply mt-1 31 | } 32 | 33 | p { 34 | @apply mt-4 md:-mx-1.5; 35 | 36 | &:first-of-type { 37 | @apply mt-1; 38 | } 39 | } 40 | 41 | a[data-active] { 42 | @apply md:-mx-1.5; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/*": ["./*"] 20 | }, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /docs/app/layout.config.tsx: -------------------------------------------------------------------------------- 1 | import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; 2 | 3 | /** 4 | * Shared layout configurations 5 | * 6 | * you can configure layouts individually from: 7 | * Home Layout: app/(home)/layout.tsx 8 | * Docs Layout: app/docs/layout.tsx 9 | */ 10 | export const baseOptions: BaseLayoutProps = { 11 | nav: { 12 | title: ( 13 | 14 | 🍩  Glaze 15 | 16 | ), 17 | }, 18 | links: [ 19 | { 20 | text: "Documentation", 21 | url: "/docs", 22 | active: "nested-url", 23 | }, 24 | { 25 | text: "GitHub", 26 | url: "https://github.com/dnnsjsk/glaze", 27 | external: true, 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /packages/core/src/utils/getSelectorOrElement.ts: -------------------------------------------------------------------------------- 1 | import { GlazeAnimationObject } from "@/types.ts"; 2 | 3 | function getSelectorOrElement( 4 | element: Element, 5 | data: GlazeAnimationObject | string, 6 | single = false, 7 | ) { 8 | let selector = typeof data !== "string" ? data?.selector?.value : ""; 9 | 10 | if (selector) { 11 | selector = selector.replaceAll("_", " "); 12 | const startsWithAnd = selector.startsWith("&"); 13 | if (startsWithAnd) { 14 | const string = selector?.replace("&", ":scope"); 15 | if (string === ":scope") return element; 16 | if (single) return element.querySelector(string); 17 | return element.querySelectorAll(string); 18 | } 19 | return selector; 20 | } 21 | return element; 22 | } 23 | 24 | export default getSelectorOrElement; 25 | -------------------------------------------------------------------------------- /packages/core/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { GlazeAnimationCollection, GlazeAnimationObject, GlazeConfig, GlazeTimeline, PlainObject } from './types.ts'; 2 | 3 | declare function glaze(config: GlazeConfig): { 4 | breakpoints: { 5 | [key: string]: string | undefined; 6 | default?: string | undefined; 7 | } & { 8 | [key: string]: string; 9 | default: string; 10 | }; 11 | state: Omit & { 12 | dataAttribute: string; 13 | element: Document | Element; 14 | breakpoints: { 15 | default: string; 16 | [key: string]: string; 17 | }; 18 | }; 19 | timelines: GlazeTimeline[]; 20 | }; 21 | export default glaze; 22 | export type { GlazeAnimationCollection, GlazeAnimationObject, GlazeConfig, GlazeTimeline, PlainObject, }; 23 | -------------------------------------------------------------------------------- /docs/content/docs/selectors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Selectors 3 | --- 4 | 5 | By default, Glaze will search for elements with the `data-animate` attribute and 6 | build the animation based on the value of the attribute. It is possible to 7 | change the attribute during the initialization of the library. 8 | 9 | ```js copy 10 | glaze({ 11 | dataAttribute: "data-move", 12 | }); 13 | ``` 14 | 15 | ## Classname 16 | 17 | Alternatively, it is also possible to search for elements with a specific class 18 | prefix. This is disabled by default. 19 | 20 | ```js copy 21 | glaze({ 22 | className: "animate", 23 | }); 24 | ``` 25 | 26 | Now Glaze will search for elements with the `animate-` class and build the 27 | animation based on the value of those classes. 28 | 29 | ```html copy 30 |
31 | ``` 32 | -------------------------------------------------------------------------------- /docs/content/docs/defaults.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Defaults 3 | --- 4 | 5 | It is possible to set default values for timelines and elements using the 6 | `defaults` option. 7 | 8 | ```js copy 9 | glaze({ 10 | defaults: { 11 | tl: "defaults:ease-power2.inOut scrollTrigger.trigger-[&]", 12 | element: "to:x-500", 13 | }, 14 | }); 15 | ``` 16 | 17 | Every timeline will use the `ease-power2.inOut` easing function and every 18 | element will move to the left by 500 pixels. On top of that, every element will 19 | be scrolled into view when it is being scrolled into view. 20 | 21 | ## Note on timelines 22 | 23 | Glaze creates a new timeline for each element by default, even if not marked 24 | with `tl`. This means that the `tl` defaults option is perfect for applying 25 | global defaults to all timelines and elements. (ScrollTrigger, ease etc.) 26 | -------------------------------------------------------------------------------- /docs/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import * as docs from "@/content/docs/meta.json"; 2 | 3 | export default function sitemap() { 4 | const entries: { 5 | url: string; 6 | lastModified: Date; 7 | }[] = []; 8 | 9 | const d = { 10 | lastModified: new Date(), 11 | }; 12 | 13 | entries.push({ 14 | url: "https://glaze.dev", 15 | ...d, 16 | }); 17 | 18 | ( 19 | docs as unknown as { 20 | default: { 21 | pages: string[]; 22 | }; 23 | } 24 | ).default.pages.forEach((page) => { 25 | if (page.startsWith("---")) return; 26 | 27 | if (page.startsWith("index")) { 28 | entries.push({ 29 | url: `https://glaze.dev/docs/`, 30 | ...d, 31 | }); 32 | return; 33 | } 34 | 35 | entries.push({ 36 | url: `https://glaze.dev/docs/${page}/`, 37 | ...d, 38 | }); 39 | }); 40 | 41 | return entries; 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/src/utils/mergeDeep.ts: -------------------------------------------------------------------------------- 1 | import { PlainObject } from "@/types.ts"; 2 | import isObject from "./isObject.ts"; 3 | 4 | function mergeDeep( 5 | target: PlainObject, 6 | ...sources: PlainObject[] 7 | ): PlainObject { 8 | if (!sources.length) return target; 9 | const source = sources.shift()!; 10 | 11 | if (isObject(target) && isObject(source)) { 12 | for (const key in source) { 13 | if (key === "__proto__" || key === "constructor" || key === "prototype") { 14 | continue; 15 | } 16 | 17 | if (isObject(source[key])) { 18 | if (!target[key]) Object.assign(target, { [key]: {} }); 19 | mergeDeep(target[key], source[key]); 20 | } else { 21 | Object.assign(target, { [key]: source[key] }); 22 | } 23 | } 24 | } 25 | 26 | return sources.length ? mergeDeep(target, ...sources) : target; 27 | } 28 | 29 | export default mergeDeep; 30 | -------------------------------------------------------------------------------- /docs/content/docs/presets.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Presets 3 | --- 4 | 5 | import { Example } from "../../components/Example"; 6 | 7 | You can create presets for your animations by using the `defaults` object. This 8 | is useful when you want to apply the same settings to multiple animations. 9 | 10 | ## Setup 11 | 12 | ```js copy 13 | glaze({ 14 | presets: { 15 | helicopter: "from:rotate-2160|duration-5", 16 | }, 17 | }); 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```html copy 23 |
27 | ``` 28 | 29 | 30 | {` 31 |
35 | `} 36 |
37 | 38 | Or when using classes: 39 | 40 | ```html copy 41 |
42 | ``` 43 | -------------------------------------------------------------------------------- /packages/core/src/utils/parseMediaQueries.ts: -------------------------------------------------------------------------------- 1 | import { GlazeConfig } from "@/types.ts"; 2 | 3 | function parseMediaQueries( 4 | input: string, 5 | breakpoints: GlazeConfig["breakpoints"], 6 | defaultBp: string, 7 | ) { 8 | const results: { 9 | [key: string]: string[]; 10 | } = {}; 11 | const segments = input.split(" "); 12 | 13 | segments.forEach((segment) => { 14 | const match = segment.match(/@(\w+):/); 15 | if (!match) { 16 | if (!results[defaultBp]) results[defaultBp] = []; 17 | results[defaultBp].push(segment); 18 | return; 19 | } 20 | 21 | const breakpoint = match[1]; 22 | const bpValue = breakpoints?.[breakpoint]; 23 | if (!bpValue) return; 24 | 25 | if (!results[bpValue]) results[bpValue] = []; 26 | results[bpValue].push(segment.replace(match[0], "")); 27 | }); 28 | 29 | return results; 30 | } 31 | 32 | export default parseMediaQueries; 33 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs2", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "build:local": "scripts/build.sh", 8 | "dev": "scripts/dev.sh", 9 | "start": "scripts/start.sh", 10 | "postinstall": "fumadocs-mdx" 11 | }, 12 | "dependencies": { 13 | "glazejs": "workspace:*", 14 | "gsap": "^3.12.5", 15 | "next": "15.1.4", 16 | "fumadocs-ui": "14.7.3", 17 | "fumadocs-core": "14.7.3", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "fumadocs-mdx": "11.3.0" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "22.10.5", 24 | "@types/react": "^19.0.4", 25 | "@types/react-dom": "^19.0.2", 26 | "typescript": "^5.7.3", 27 | "@types/mdx": "^2.0.13", 28 | "autoprefixer": "^10.4.20", 29 | "postcss": "^8.4.49", 30 | "tailwindcss": "^3.4.17", 31 | "eslint": "^8", 32 | "eslint-config-next": "15.1.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/utils/castValue.ts: -------------------------------------------------------------------------------- 1 | import getSelectorOrElement from "./getSelectorOrElement.ts"; 2 | 3 | function castValue( 4 | value: string | null, 5 | element: Element, 6 | key: string, 7 | ): string | number | boolean | Element | null { 8 | if (typeof value !== "string") return value; 9 | const cleanedValue = value.replace(/^\[|]$/g, "").replaceAll("_", " "); 10 | 11 | if (cleanedValue.startsWith("&")) { 12 | return getSelectorOrElement( 13 | element, 14 | { selector: { value: cleanedValue } }, 15 | ["scrollTrigger", "trigger"].includes(key), 16 | ); 17 | } 18 | 19 | if (cleanedValue === "true" || cleanedValue === "false") { 20 | return cleanedValue === "true"; 21 | } else if (!isNaN(Number(cleanedValue))) { 22 | return cleanedValue.includes(".") 23 | ? parseFloat(cleanedValue) 24 | : parseInt(cleanedValue, 10); 25 | } 26 | 27 | return cleanedValue; 28 | } 29 | 30 | export default castValue; 31 | -------------------------------------------------------------------------------- /packages/core/scripts/postbuild.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const terser = require("terser"); 3 | const path = require("path"); 4 | 5 | async function minifyFile(filePath) { 6 | const originalCode = fs.readFileSync(filePath, "utf8"); 7 | const minified = await terser.minify(originalCode, { 8 | compress: true, 9 | mangle: {}, 10 | output: { 11 | comments: false, 12 | }, 13 | }); 14 | fs.writeFileSync(filePath, minified.code, "utf8"); 15 | } 16 | 17 | minifyFile("./dist/index.es.js"); 18 | 19 | [ 20 | { 21 | from: path.join(__dirname, "../../../README.md"), 22 | to: path.join(__dirname, "../README.md"), 23 | }, 24 | { 25 | from: path.join(__dirname, "../../../CHANGELOG.md"), 26 | to: path.join(__dirname, "../../../docs/pages/documentation/changelog.mdx"), 27 | }, 28 | ].forEach(({ from, to }) => { 29 | fs.copyFile(from, to, (err) => { 30 | if (err) { 31 | console.error("Error occurred:", err); 32 | return; 33 | } 34 | console.log(`${from} was copied successfully.`); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/core/tests/unit/utils/debounce.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, afterEach } from "vitest"; 2 | import debounce from "../../../src/utils/debounce"; 3 | 4 | vi.useFakeTimers(); 5 | 6 | describe("debounce", () => { 7 | it("delays the function execution by the specified wait time", async () => { 8 | const mockFn = vi.fn(); 9 | const debouncedFn = debounce(mockFn, 1000); 10 | 11 | debouncedFn(); 12 | expect(mockFn).not.toHaveBeenCalled(); 13 | 14 | vi.advanceTimersByTime(500); 15 | expect(mockFn).not.toHaveBeenCalled(); 16 | 17 | vi.advanceTimersByTime(500); 18 | expect(mockFn).toHaveBeenCalledOnce(); 19 | }); 20 | 21 | it("calls the debounced function only once for multiple rapid invocations", () => { 22 | const mockFn = vi.fn(); 23 | const debouncedFn = debounce(mockFn, 1000); 24 | 25 | debouncedFn(); 26 | debouncedFn(); 27 | debouncedFn(); 28 | 29 | vi.advanceTimersByTime(1000); 30 | 31 | expect(mockFn).toHaveBeenCalledOnce(); 32 | }); 33 | 34 | afterEach(() => { 35 | vi.restoreAllMocks(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/core/tests/unit/utils/isObject.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import isObject from "../../../src/utils/isObject"; 3 | 4 | describe("isObject", () => { 5 | it("returns true for plain objects", () => { 6 | expect(isObject({})).toBe(true); 7 | expect(isObject({ key: "value" })).toBe(true); 8 | }); 9 | 10 | it("returns false for arrays", () => { 11 | expect(isObject([])).toBe(false); 12 | expect(isObject([1, 2, 3])).toBe(false); 13 | }); 14 | 15 | it("returns false for null", () => { 16 | expect(isObject(null)).toBe(false); 17 | }); 18 | 19 | it("returns false for undefined", () => { 20 | expect(isObject(undefined)).toBe(false); 21 | }); 22 | 23 | it("returns false for HTMLElements", () => { 24 | const element = document.createElement("div"); 25 | expect(isObject(element)).toBe(false); 26 | }); 27 | 28 | it("returns false for other non-object types", () => { 29 | expect(isObject(123)).toBe(false); 30 | expect(isObject("string")).toBe(false); 31 | expect(isObject(true)).toBe(false); 32 | expect(isObject(Symbol("symbol"))).toBe(false); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /docs/components/Example.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef } from "react"; 4 | import glaze from "glazejs"; 5 | import gsap from "gsap"; 6 | import { ScrollTrigger } from "gsap/ScrollTrigger"; 7 | gsap.registerPlugin(ScrollTrigger); 8 | 9 | export const Example = ({ children }: { children: string }) => { 10 | const ref = useRef(null); 11 | const refInnerRef = useRef(null); 12 | 13 | useEffect(() => { 14 | if (!refInnerRef.current) return; 15 | refInnerRef.current.innerHTML = children; 16 | glaze({ 17 | lib: { gsap: { core: gsap } }, 18 | element: ref.current, 19 | breakpoints: { 20 | sm: "(min-width: 640px)", 21 | lg: "(min-width: 1024px)", 22 | }, 23 | presets: { 24 | helicopter: "from:rotate-2160|duration-5", 25 | }, 26 | }); 27 | }, [children]); 28 | 29 | return ( 30 |
31 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/core/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, ViteDevServer } from "vite"; 2 | import dts from "vite-plugin-dts"; 3 | import { resolve } from "path"; 4 | 5 | const removeConsolePlugin = () => { 6 | return { 7 | name: "remove-console", // name of the plugin 8 | renderChunk(code: string) { 9 | return code.replace(/console\.log\(.*\);?/g, ""); 10 | }, 11 | }; 12 | }; 13 | 14 | const forceFullReloadPlugin = () => { 15 | return { 16 | name: "force-full-reload", 17 | handleHotUpdate({ file, server }: { file: string; server: ViteDevServer }) { 18 | if (file.endsWith(".ts")) { 19 | server.ws.send({ 20 | type: "full-reload", 21 | path: "*", 22 | }); 23 | } 24 | }, 25 | }; 26 | }; 27 | 28 | export default defineConfig({ 29 | resolve: { 30 | alias: { 31 | "@": resolve(__dirname, "./src"), 32 | }, 33 | }, 34 | build: { 35 | lib: { 36 | entry: "./src/index.ts", 37 | name: "Glaze", 38 | fileName: (format) => `index.${format}.js`, 39 | formats: ["es"], 40 | }, 41 | rollupOptions: { 42 | external: ["gsap"], 43 | }, 44 | minify: "esbuild", 45 | }, 46 | plugins: [ 47 | removeConsolePlugin(), 48 | forceFullReloadPlugin(), 49 | dts({ 50 | include: ["./src/index.ts"], 51 | }), 52 | ], 53 | }); 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 dnnsjsk 4 | 5 | Glaze is an open-source animation library that works alongside GSAP. 6 | To use Glaze, you must also include GSAP in your project. 7 | GSAP is subject to its own licensing terms, which can be found here: 8 | [GSAP Standard License](https://gsap.com/community/standard-license/). 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- 1 | type GlazeAnimationCollection = { 2 | data: GlazeAnimationObject; 3 | element: Element; 4 | breakpoint: string; 5 | }; 6 | 7 | type GlazeAnimationObject = { 8 | [key: string]: { 9 | [key: string]: any; 10 | }; 11 | }; 12 | 13 | type GlazeConfig = { 14 | breakpoints?: { 15 | default?: string; 16 | [key: string]: string | undefined; 17 | }; 18 | dataAttribute?: string; 19 | defaults?: { 20 | tl?: string; 21 | element?: string; 22 | elementSettings?: string; 23 | }; 24 | className?: string; 25 | element?: Document | Element; 26 | lib: { 27 | gsap: { 28 | core: typeof import("gsap").gsap; 29 | }; 30 | }; 31 | presets?: { 32 | [key: string]: string; 33 | }; 34 | watch?: 35 | | boolean 36 | | { 37 | debounceTime?: number; 38 | }; 39 | }; 40 | 41 | type GlazeObjectSettings = { 42 | [key: string]: string | number | GlazeObjectSettings; 43 | }; 44 | 45 | type GlazeTimeline = { 46 | breakpoint: string; 47 | data: GlazeObjectSettings; 48 | elements: Map; 49 | id: string; 50 | timeline: gsap.core.Timeline; 51 | timelineElement: Element; 52 | }; 53 | 54 | type PlainObject = { [key: string]: any }; 55 | 56 | export type { 57 | GlazeAnimationCollection, 58 | GlazeAnimationObject, 59 | GlazeConfig, 60 | GlazeObjectSettings, 61 | GlazeTimeline, 62 | PlainObject, 63 | }; 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.1 4 | 5 | 2024-06-05 6 | 7 | - Fix: build package 8 | 9 | ## 2.0.0 10 | 11 | 2024-06-05 12 | 13 | - Breaking: timeline defaults are not applied directly to `defaults` key anymore 14 | - New: timeline defaults will also be applied to single elements (Glaze creates a new timeline for each element) 15 | 16 | ## 1.2.0 17 | 18 | 2024-04-23 19 | 20 | - New: `presets` option to define presets for animations 21 | - New: `defaults` option to define default values for timelines and animations 22 | 23 | ## 1.1.3 24 | 25 | 2024-03-03 26 | 27 | - Fix: `from` animations flash of initial state #2 28 | 29 | ## 1.1.2 30 | 31 | 2024-03-03 32 | 33 | - Fix: `from` animations flash of initial state 34 | 35 | ## 1.1.1 36 | 37 | 2024-02-29 38 | 39 | - Fix: `watch` option not resetting properly when using `from` animations 40 | 41 | ## 1.1.0 42 | 43 | 2024-02-28 44 | 45 | - New: `watch` option to watch changes to animation attributes in DOM, updating animations accordingly 46 | - New: `className` option to add class prefixes and create animations from classes, e.g. `animate-to:x-500` 47 | - Fix: element selector returning null when just using `&` 48 | - Fix: `from` animations not restarting when using breakpoints 49 | 50 | ## 1.0.2 51 | 52 | 2024-02-10 53 | 54 | - Enhancement: possibility to use dashes (spaces) in selector 55 | 56 | ## 1.0.1 57 | 58 | 2024-02-08 59 | 60 | - Update licensing 61 | 62 | ## 1.0.0 63 | 64 | 2024-02-08 65 | 66 | - Initial release 67 | -------------------------------------------------------------------------------- /packages/core/tests/unit/utils/getSelectorFromBracket.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import getSelectorFromBracket from "../../../src/utils/getSelectorFromBracket"; 3 | 4 | describe("getSelectorFromBracket", () => { 5 | it("extracts content and restOfString for matching inputs", () => { 6 | const input = "[button]:hover"; 7 | const expected = { 8 | content: "button", 9 | restOfString: "hover", 10 | }; 11 | const result = getSelectorFromBracket(input); 12 | expect(result).toEqual(expected); 13 | }); 14 | 15 | it("returns null for inputs that do not match the pattern", () => { 16 | const input = "button:hover"; 17 | const result = getSelectorFromBracket(input); 18 | expect(result).toBeNull(); 19 | }); 20 | 21 | it("handles inputs with multiple colons correctly", () => { 22 | const input = "[icon]:before:hover"; 23 | const expected = { 24 | content: "icon", 25 | restOfString: "before:hover", 26 | }; 27 | const result = getSelectorFromBracket(input); 28 | expect(result).toEqual(expected); 29 | }); 30 | 31 | it("returns null for empty strings", () => { 32 | const input = ""; 33 | const result = getSelectorFromBracket(input); 34 | expect(result).toBeNull(); 35 | }); 36 | 37 | it("returns null for strings without a colon", () => { 38 | const input = "[button]"; 39 | const result = getSelectorFromBracket(input); 40 | expect(result).toBeNull(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glazejs", 3 | "version": "2.0.1", 4 | "description": "Utility-based animations for the web.", 5 | "author": "Dennis Josek (https://dennn.is)", 6 | "homepage": "https://glaze.dev", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/dnnsjsk/glaze" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/dnnsjsk/glaze/issues" 14 | }, 15 | "module": "dist/index.es.js", 16 | "types": "dist/index.d.ts", 17 | "files": [ 18 | "dist" 19 | ], 20 | "keywords": [ 21 | "glaze", 22 | "glazejs", 23 | "animation", 24 | "web", 25 | "utility", 26 | "gsap" 27 | ], 28 | "scripts": { 29 | "build": "pnpm run test:unit && tsc && vite build && node scripts/postbuild.js", 30 | "dev": "vite", 31 | "release": "npm run build && npm publish", 32 | "test": "npm run test:unit && npm run test:e2e", 33 | "test:e2e": "npx playwright test", 34 | "test:unit": "vitest --watch=false --config vitest.config.unit.ts", 35 | "test:unit-watch": "vitest --watch=true --config vitest.config.unit.ts" 36 | }, 37 | "devDependencies": { 38 | "@playwright/test": "^1.43.1", 39 | "@types/node": "^20.12.7", 40 | "gsap": "^3.12.5", 41 | "jsdom": "^24.0.0", 42 | "prettier": "^3.2.5", 43 | "terser": "^5.30.4", 44 | "typescript": "^5.4.5", 45 | "vite": "^5.2.10", 46 | "vite-plugin-dts": "^3.8.3", 47 | "vitest": "^1.5.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/content/docs/changelog.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelog 3 | --- 4 | 5 | ## 2.0.1 6 | 7 | 2024-06-05 8 | 9 | - Fix: build package 10 | 11 | ## 2.0.0 12 | 13 | 2024-06-05 14 | 15 | - Breaking: timeline defaults are not applied directly to `defaults` key anymore 16 | - New: timeline defaults will also be applied to single elements (Glaze creates a new timeline for each element) 17 | 18 | ## 1.2.0 19 | 20 | 2024-04-23 21 | 22 | - New: `presets` option to define presets for animations 23 | - New: `defaults` option to define default values for timelines and animations 24 | 25 | ## 1.1.3 26 | 27 | 2024-03-03 28 | 29 | - Fix: `from` animations flash of initial state #2 30 | 31 | ## 1.1.2 32 | 33 | 2024-03-03 34 | 35 | - Fix: `from` animations flash of initial state 36 | 37 | ## 1.1.1 38 | 39 | 2024-02-29 40 | 41 | - Fix: `watch` option not resetting properly when using `from` animations 42 | 43 | ## 1.1.0 44 | 45 | 2024-02-28 46 | 47 | - New: `watch` option to watch changes to animation attributes in DOM, updating animations accordingly 48 | - New: `className` option to add class prefixes and create animations from classes, e.g. `animate-to:x-500` 49 | - Fix: element selector returning null when just using `&` 50 | - Fix: `from` animations not restarting when using breakpoints 51 | 52 | ## 1.0.2 53 | 54 | 2024-02-10 55 | 56 | - Enhancement: possibility to use dashes (spaces) in selector 57 | 58 | ## 1.0.1 59 | 60 | 2024-02-08 61 | 62 | - Update licensing 63 | 64 | ## 1.0.0 65 | 66 | 2024-02-08 67 | 68 | - Initial release 69 | -------------------------------------------------------------------------------- /docs/content/docs/scrolltrigger.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: ScrollTrigger 3 | --- 4 | 5 | import { Example } from "../../components/Example"; 6 | 7 | ScrollTrigger needs to be registered with GSAP before it can be used. This can 8 | be done by importing it from the `gsap` package and calling `registerPlugin` 9 | with it. 10 | 11 | ```js 12 | import gsap from "gsap"; 13 | import ScrollTrigger from "gsap/ScrollTrigger"; 14 | gsap.registerPlugin(ScrollTrigger); 15 | ``` 16 | 17 | There is no special syntax needed to use 18 | [ScrollTrigger](https://gsap.com/docs/v3/Plugins/ScrollTrigger/) with Glaze. 19 | Since it is defined inside the `scrollTrigger` property of the animation object, 20 | dot notation can be used to adjust its settings. 21 | 22 | ```html copy 23 |
24 |
28 |
29 | ``` 30 | 31 | 32 | {` 33 |
34 |
38 |
39 | `} 40 |
41 | 42 |
43 | -------------------------------------------------------------------------------- /docs/app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { source } from "@/lib/source"; 2 | import { 3 | DocsPage, 4 | DocsBody, 5 | DocsDescription, 6 | DocsTitle, 7 | } from "fumadocs-ui/page"; 8 | import { notFound } from "next/navigation"; 9 | import defaultMdxComponents from "fumadocs-ui/mdx"; 10 | 11 | export default async function Page(props: { 12 | params: Promise<{ slug?: string[] }>; 13 | }) { 14 | const params = await props.params; 15 | const page = source.getPage(params.slug); 16 | if (!page) notFound(); 17 | 18 | const MDX = page.data.body; 19 | 20 | return ( 21 | 34 | {page.data.title} 35 | {page.data.description} 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | export async function generateStaticParams() { 44 | return source.generateParams(); 45 | } 46 | 47 | export async function generateMetadata(props: { 48 | params: Promise<{ slug?: string[] }>; 49 | }) { 50 | const params = await props.params; 51 | const page = source.getPage(params.slug); 52 | if (!page) notFound(); 53 | 54 | return { 55 | title: page.data.title + " - Glaze", 56 | description: page.data.description, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /packages/core/tests/unit/utils/castValue.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import castValue from "../../../src/utils/castValue.ts"; 3 | import getSelectorOrElement from "../../../src/utils/getSelectorOrElement.ts"; 4 | 5 | vi.mock("../../../src/utils/getSelectorOrElement", () => ({ 6 | default: vi.fn((_element, selector) => { 7 | return `Mocked: ${selector}`; 8 | }), 9 | })); 10 | 11 | describe("castValue", () => { 12 | it('returns boolean true for "true" string', () => { 13 | expect(castValue("true", document.createElement("div"), "")).toBe(true); 14 | }); 15 | 16 | it('returns boolean false for "false" string', () => { 17 | expect(castValue("false", document.createElement("div"), "")).toBe(false); 18 | }); 19 | 20 | it("returns a number for numeric string", () => { 21 | expect(castValue("123", document.createElement("div"), "")).toBe(123); 22 | }); 23 | 24 | it("returns a float for floating point string", () => { 25 | expect(castValue("123.45", document.createElement("div"), "")).toBe(123.45); 26 | }); 27 | 28 | it("cleans up and returns the original string without brackets and underscores", () => { 29 | expect(castValue("[hello_world]", document.createElement("div"), "")).toBe( 30 | "hello world", 31 | ); 32 | }); 33 | 34 | it('returns the result of getSelectorOrElement for strings starting with "&"', () => { 35 | const element = document.createElement("div"); 36 | castValue("&", element, "trigger"); 37 | expect(getSelectorOrElement).toHaveBeenCalledWith( 38 | element, 39 | { 40 | selector: { value: "&" }, 41 | }, 42 | true, 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/core/tests/unit/utils/parseMediaQueries.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import parseMediaQueries from "../../../src/utils/parseMediaQueries"; 3 | 4 | describe("parseMediaQueries", () => { 5 | const breakpoints = { 6 | default: "(min-width: 1px)", 7 | sm: "(min-width: 640px)", 8 | md: "(min-width: 768px)", 9 | lg: "(min-width: 1024px)", 10 | }; 11 | const defaultBp = breakpoints.default; 12 | 13 | it("parses segments without breakpoints into the default group", () => { 14 | const input = "opacity:1 scale:0.5"; 15 | const expected = { 16 | [defaultBp]: ["opacity:1", "scale:0.5"], 17 | }; 18 | expect(parseMediaQueries(input, breakpoints, defaultBp)).toEqual(expected); 19 | }); 20 | 21 | it("groups segments by specified breakpoints", () => { 22 | const input = "@sm:opacity:1 @lg:scale:0.5"; 23 | const expected = { 24 | [breakpoints.sm]: ["opacity:1"], 25 | [breakpoints.lg]: ["scale:0.5"], 26 | }; 27 | expect(parseMediaQueries(input, breakpoints, defaultBp)).toEqual(expected); 28 | }); 29 | 30 | it("ignores segments with non-existent breakpoints", () => { 31 | const input = "@sm:opacity:1 @xl:scale:0.5"; 32 | const expected = { 33 | [breakpoints.sm]: ["opacity:1"], 34 | }; 35 | expect(parseMediaQueries(input, breakpoints, defaultBp)).toEqual(expected); 36 | }); 37 | 38 | it("combines segments with and without breakpoints correctly", () => { 39 | const input = "opacity:1 @sm:scale:0.5 transform:rotate(45deg)"; 40 | const expected = { 41 | [defaultBp]: ["opacity:1", "transform:rotate(45deg)"], 42 | [breakpoints.sm]: ["scale:0.5"], 43 | }; 44 | expect(parseMediaQueries(input, breakpoints, defaultBp)).toEqual(expected); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/core/tests/unit/utils/getSelectorOrElement.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach } from "vitest"; 2 | import getSelectorOrElement from "../../../src/utils/getSelectorOrElement"; 3 | import { GlazeAnimationObject } from "@/types.ts"; 4 | 5 | describe("getSelectorOrElement", () => { 6 | let container: HTMLElement; 7 | 8 | beforeEach(() => { 9 | document.body.innerHTML = ` 10 |
11 |
12 |
13 |
14 | `; 15 | container = document.getElementById("test-container") as HTMLElement; 16 | }); 17 | 18 | it("returns the element if no valid selector is provided", () => { 19 | const result = getSelectorOrElement(container, ""); 20 | expect(result).toBe(container); 21 | }); 22 | 23 | it("returns the element if a string is provided", () => { 24 | const selector = ".child"; 25 | const result = getSelectorOrElement(container, selector); 26 | expect(result).toBe(container); 27 | }); 28 | 29 | it('queries all elements within the container if the selector starts with "&" and replace spaces', () => { 30 | const data: GlazeAnimationObject = { selector: { value: "&_.child" } }; 31 | const result = getSelectorOrElement(container, data); 32 | expect(result).toBeInstanceOf(NodeList); 33 | expect(result).toHaveLength(2); 34 | }); 35 | 36 | it('queries a single element within the container if the selector starts with "&" and single is true', () => { 37 | const data: GlazeAnimationObject = { selector: { value: "&>.child" } }; 38 | const result = getSelectorOrElement(container, data, true); 39 | expect(result).toBeInstanceOf(Element); 40 | expect(result).toHaveProperty("id", "first-child"); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![glaze image](https://raw.githubusercontent.com/dnnsjsk/glaze/main/image.png) 2 | 3 | Glaze is an animation framework that combines the power of 4 | [GSAP](https://greensock.com/gsap/) and utility-based document authoring à la 5 | [Tailwind](https://tailwindcss.com) to create a simple, yet powerful, way to 6 | compose declarative animations for the web. 7 | 8 | ```html copy 9 | 13 | ``` 14 | 15 | ## Features 16 | 17 | - **Breakpoints** Define custom breakpoints and animate elements at different 18 | screen sizes using `@{size}` syntax. Uses GSAPs 19 | [matchMedia](). 20 | - **Timelines** Compose timelines using `@tl`. 21 | - **Dot notation** Use dot notation to control nested properties inside the 22 | animation object, e.g. `to:scale-1.5|scrollTrigger.trigger-[&]`. 23 | - **Lightweight** ~3kb minified and gzipped. 24 | - **Library agnostic** Can be extended to work with other animation libraries. 25 | (coming soon) 26 | 27 | ## Credits 28 | 29 | The API and syntax of Glaze is heavily inspired by [Tailwind](https://tailwindcss.com) 30 | and [MasterCSS](https://css.master.co/). 31 | 32 | ## Licensing and Requirements 33 | 34 | Glaze is designed to work seamlessly with [GSAP](https://greensock.com/gsap/). To use 35 | Glaze, you must include GSAP in your project. 36 | 37 | ### GSAP Licensing 38 | 39 | GSAP is subject to its own licensing terms. Before incorporating GSAP with Glaze, ensure you review and comply with 40 | the [GSAP Standard License](https://gsap.com/community/standard-license/). 41 | 42 | Glaze itself is licensed under the MIT License. For more details, see the LICENSE file in the repository. 43 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | ![glaze image](https://raw.githubusercontent.com/dnnsjsk/glaze/main/image.png) 2 | 3 | Glaze is an animation framework that combines the power of 4 | [GSAP](https://greensock.com/gsap/) and utility-based document authoring à la 5 | [Tailwind](https://tailwindcss.com) to create a simple, yet powerful, way to 6 | compose declarative animations for the web. 7 | 8 | ```html copy 9 | 13 | ``` 14 | 15 | ## Features 16 | 17 | - **Breakpoints** Define custom breakpoints and animate elements at different 18 | screen sizes using `@{size}` syntax. Uses GSAPs 19 | [matchMedia](). 20 | - **Timelines** Compose timelines using `@tl`. 21 | - **Dot notation** Use dot notation to control nested properties inside the 22 | animation object, e.g. `to:scale-1.5|scrollTrigger.trigger-[&]`. 23 | - **Lightweight** ~3kb minified and gzipped. 24 | - **Library agnostic** Can be extended to work with other animation libraries. 25 | (coming soon) 26 | 27 | ## Credits 28 | 29 | The API and syntax of Glaze is heavily inspired by [Tailwind](https://tailwindcss.com) 30 | and [MasterCSS](https://css.master.co/). 31 | 32 | ## Licensing and Requirements 33 | 34 | Glaze is designed to work seamlessly with [GSAP](https://greensock.com/gsap/). To use 35 | Glaze, you must include GSAP in your project. 36 | 37 | ### GSAP Licensing 38 | 39 | GSAP is subject to its own licensing terms. Before incorporating GSAP with Glaze, ensure you review and comply with 40 | the [GSAP Standard License](https://gsap.com/community/standard-license/). 41 | 42 | Glaze itself is licensed under the MIT License. For more details, see the LICENSE file in the repository. 43 | -------------------------------------------------------------------------------- /docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./global.css"; 2 | import { RootProvider } from "fumadocs-ui/provider"; 3 | import localFont from "next/font/local"; 4 | import type { ReactNode } from "react"; 5 | import { Metadata } from "next"; 6 | 7 | const inter = localFont({ 8 | src: "../fonts/inter.woff2", 9 | }); 10 | 11 | const socialCard = "https://glaze.dev/og.jpg"; 12 | 13 | export const metadata: Metadata = { 14 | title: "Glaze", 15 | }; 16 | 17 | export default function Layout({ children }: { children: ReactNode }) { 18 | return ( 19 | 20 | 21 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {process.env.NODE_ENV === "production" ? ( 38 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import glaze from "glazejs"; 3 | import gsap from "gsap"; 4 | import { Tab, Tabs } from "fumadocs-ui/components/tabs"; 5 | import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock"; 6 | import CodeNpm from "@/components/CodeNpm.mdx"; 7 | import CodePnpm from "@/components/CodePnpm.mdx"; 8 | import CodeYarn from "@/components/CodeYarn.mdx"; 9 | import Link from "next/link"; 10 | 11 | const LetterSplitter = ({ text }: { text: string }) => { 12 | return text 13 | .split(" ") 14 | .map((word: string, wordIndex: number, arr: string | string[]) => ( 15 | 19 | 20 | {word} 21 | 22 | {wordIndex < arr.length - 1 ?   : ""} 23 | 24 | )); 25 | }; 26 | 27 | const Hero = () => { 28 | const ref = useRef(null); 29 | 30 | useEffect(() => { 31 | glaze({ 32 | lib: { gsap: { core: gsap } }, 33 | element: ref.current, 34 | }); 35 | }, []); 36 | 37 | return ( 38 |
39 |

43 | 44 |

45 |
46 | 51 | 52 | 53 |
54 |                 
55 |               
56 |
57 |
58 | 59 | 60 |
61 |                 
62 |               
63 |
64 |
65 | 66 | 67 |
68 |                 
69 |               
70 |
71 |
72 |
73 | 77 | Go to docs 78 | 79 |
80 |
81 | ); 82 | }; 83 | 84 | export default Hero; 85 | -------------------------------------------------------------------------------- /packages/core/tests/unit/utils/mergeDeep.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import mergeDeep from "../../../src/utils/mergeDeep"; 3 | 4 | describe("mergeDeep", () => { 5 | it("merges two simple objects", () => { 6 | const obj1 = { a: 1 }; 7 | const obj2 = { b: 2 }; 8 | const expected = { a: 1, b: 2 }; 9 | expect(mergeDeep({}, obj1, obj2)).toEqual(expected); 10 | }); 11 | 12 | it("overwrites properties in the target with those from the source", () => { 13 | const target = { a: 1, b: { c: 1 } }; 14 | const source = { b: { d: 2 }, e: 3 }; 15 | const expected = { a: 1, b: { c: 1, d: 2 }, e: 3 }; 16 | expect(mergeDeep(target, source)).toEqual(expected); 17 | }); 18 | 19 | it("performs deep merging", () => { 20 | const target = { a: { b: 1 } }; 21 | const source = { a: { c: 2 }, d: 3 }; 22 | const expected = { a: { b: 1, c: 2 }, d: 3 }; 23 | expect(mergeDeep(target, source)).toEqual(expected); 24 | }); 25 | 26 | it("does not modify source objects", () => { 27 | const target = { a: 1 }; 28 | const source = { b: 2 }; 29 | mergeDeep(target, source); 30 | expect(source).toEqual({ b: 2 }); 31 | }); 32 | 33 | it("handles non-object values gracefully", () => { 34 | const target = { a: 1 }; 35 | const source = "not an object"; 36 | const expected = { a: 1 }; 37 | expect(mergeDeep(target, source as any)).toEqual(expected); 38 | }); 39 | 40 | it("skips merging HTML elements", () => { 41 | const target = { a: 1 }; 42 | const element = document.createElement("div"); 43 | const source = { b: element }; 44 | const result = mergeDeep({}, target, source); 45 | expect(result).toEqual({ a: 1, b: element }); 46 | expect(result.b).toBeInstanceOf(HTMLElement); 47 | }); 48 | 49 | it("skips merging functions", () => { 50 | const target = { a: 1 }; 51 | const source = { b: () => console.log("test") }; 52 | const result = mergeDeep({}, target, source); 53 | expect(result).toEqual({ a: 1, b: source.b }); 54 | expect(typeof result.b).toBe("function"); 55 | }); 56 | 57 | it("correctly merges objects with array properties", () => { 58 | const target = { a: [1, 2] }; 59 | const source = { a: [3, 4], b: 2 }; 60 | const expected = { a: [3, 4], b: 2 }; 61 | expect(mergeDeep(target, source)).toEqual(expected); 62 | }); 63 | 64 | it("ignores null and undefined sources", () => { 65 | const target = { a: 1 }; 66 | const resultWithNull = mergeDeep({}, target, null as any); 67 | const resultWithUndefined = mergeDeep({}, target, undefined as any); 68 | expect(resultWithNull).toEqual({ a: 1 }); 69 | expect(resultWithUndefined).toEqual({ a: 1 }); 70 | }); 71 | 72 | it("does not merge __proto__ properties", () => { 73 | const target = {}; 74 | const source = JSON.parse('{"__proto__": {"polluted": "yes"}}'); 75 | mergeDeep(target, source); 76 | expect(({} as any).polluted).toBeUndefined(); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /docs/content/docs/syntax.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Syntax 3 | --- 4 | 5 | The syntax of Glaze is straightforward and easy to understand. If you're 6 | familiar with Tailwind, you'll find Glaze's approach very similar. 7 | 8 | Animation strings are defined in the following order: 9 | 10 | 1. **Breakpoint** (optional) 11 | 2. **Selector** (optional) 12 | 3. **State** (required) 13 | 4. **Animation object** (required) 14 | 15 | ## Breakpoints 16 | 17 | Breakpoints allow you to specify when an animation should run, based on the 18 | breakpoints defined in the configuration. The breakpoint is defined using the 19 | `@` symbol, followed by the breakpoint name. 20 | 21 | ```html copy 22 |
23 | ``` 24 | 25 | ## Selectors 26 | 27 | By default, animations are applied directly to the element itself. However, you 28 | can target other elements using selectors enclosed in brackets (`[]`) before the 29 | state. 30 | 31 | ```html copy 32 |
33 |

...

34 |

...

35 |

...

36 |
37 | ``` 38 | 39 | The `&` symbol refers to the parent element, allowing you to specify a child 40 | selector. 41 | 42 | With media queries: 43 | 44 | ```html copy 45 |
46 |

...

47 |

...

48 |

...

49 |
50 | ``` 51 | 52 | ## State 53 | 54 | The animation state indicates the beginning and end points of the animation: 55 | 56 | - **from**: The initial state of the animation. 57 | - **to**: The final state of the animation. 58 | 59 | ```html copy 60 |
61 |
62 | ``` 63 | 64 | If both states are defined, the animation will run from the initial state to the 65 | final state. ([fromTo in GSAP]()) 66 | 67 | ```html copy 68 |
69 | ``` 70 | 71 | ## Animation object 72 | 73 | The animation object specifies the properties to animate, following the state. 74 | 75 | ```html copy 76 |
77 | ``` 78 | 79 | The string is parsed by splitting at the dash (`-`), where the first part is the 80 | property name, and the second part is its value. Values are automatically 81 | converted to their appropriate type (`string`, `number`, or `boolean`). 82 | 83 | ### Chaining properties 84 | 85 | To combine multiple properties in a single animation, separate them with a pipe 86 | (`|`) character. 87 | 88 | ```html copy 89 |
90 | ``` 91 | 92 | ### Nested objects 93 | 94 | Access nested object properties by separating keys with a dot (`.`) character. 95 | 96 | ```html copy 97 |
98 | ``` 99 | 100 | ### Negative values 101 | 102 | For negative values, enclose the value in brackets. (`[]`) 103 | 104 | ```html copy 105 |
106 | ``` 107 | 108 | ### Spaces 109 | 110 | For values with spaces, use an underscore (`_`) character. 111 | 112 | ```html copy 113 |
114 | ``` 115 | -------------------------------------------------------------------------------- /docs/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Hero from "@/components/Hero"; 4 | import HeroCode from "@/components/HeroCode.mdx"; 5 | import { Pre, CodeBlock } from "fumadocs-ui/components/codeblock"; 6 | import { Footer } from "@/components/Footer"; 7 | 8 | export default function HomePage() { 9 | return ( 10 | <> 11 |
12 |
13 | 14 | 15 |
16 |               
17 |             
18 |
19 |
20 |
21 | {[ 22 | { 23 | emoji: "🚀", 24 | title: "Based on GSAP", 25 | description: 26 | "Glaze is built on top of GSAP, a powerful animation library that allows you to create complex animations with little code.", 27 | }, 28 | { 29 | emoji: "🎨", 30 | title: "Utility inspired", 31 | description: 32 | "If you know Tailwind, you'll feel right at home with Glaze. It is inspired by its HTML- and utility-first approach.", 33 | }, 34 | { 35 | emoji: "📱", 36 | title: "Breakpoints", 37 | description: 38 | "Glaze supports responsive design out of the box, allowing you to define different animations for different screen sizes.", 39 | }, 40 | { 41 | emoji: "🔗", 42 | title: "Timelines", 43 | description: 44 | "Full support for timelines, allowing you to sequence multiple animations together, or run them in parallel.", 45 | }, 46 | { 47 | emoji: "🔍", 48 | title: "Dot notation", 49 | description: 50 | "Use dot notation for specifying nested properties in the settings object, granting complete control over the animation.", 51 | }, 52 | { 53 | emoji: "📚", 54 | title: "Library-agnostic syntax", 55 | description: 56 | "Though Glaze presently supports only GSAP, its syntax lays the groundwork for future integration with other libraries.", 57 | }, 58 | ].map((item) => { 59 | return ( 60 |
64 |
65 | {item.emoji} 66 |
67 |

{item.title}

68 |

72 |

73 | ); 74 | })} 75 |
76 |
77 |