diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6713304..41b6ba4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # required for git-diff later - # 2 ▸ Set up pnpm - uses: pnpm/action-setup@v3 with: @@ -49,7 +48,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} + key: ${{ runner.os }}-playwright-${{ hashFiles('packages/core/package.json') }} restore-keys: | ${{ runner.os }}-playwright- @@ -57,26 +56,26 @@ jobs: if: steps.pw-cache.outputs.cache-hit != 'true' run: pnpm --filter @react-zero-ui/core exec playwright install --with-deps - - name: Save Playwright cache - if: steps.pw-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: ~/.cache/ms-playwright - key: ${{ steps.pw-cache.outputs.cache-primary-key }} - # 6 ▸ Lint fast, fail fast - name: Lint run: pnpm lint - # 7 ▸ Pack → inject tar-ball → run whole test suite - - name: Pack core tarball - run: pnpm run prepack:core + # 7 ▸ Run Build + - name: Run Build + run: pnpm build + + # 8 ▸ Run Prepack + - name: Run Prepack + run: pnpm prepack:core - - name: Inject tarball into fixtures - run: node scripts/install-local-tarball.js + # 9 ▸ Run Install Tarball + - name: Run Install Tarball + run: pnpm i-tarball + # 10 ▸ Run all tests - name: Run Vite tests run: pnpm test:vite + - name: Run Next.js tests run: pnpm test:next @@ -85,3 +84,6 @@ jobs: - name: Run CLI tests run: pnpm test:cli + + - name: Run integration tests + run: pnpm test:integration diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 881b989..780e2ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,10 +2,6 @@ name: Release on: workflow_dispatch: - workflow_run: - workflows: ['CI'] - types: [completed] - branches: [main] permissions: contents: write @@ -14,13 +10,64 @@ permissions: jobs: release: - if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Check CI Status + run: | + # Get the latest CI workflow run for this branch + CI_STATUS=$(gh run list --workflow=ci.yml --branch=${{ github.ref_name }} --limit=1 --json conclusion --jq '.[0].conclusion') + + if [[ "$CI_STATUS" != "success" ]]; then + echo "❌ CI must pass before releasing. Current status: $CI_STATUS" + exit 1 + fi + + echo "✅ CI passed. Proceeding with release..." + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: googleapis/release-please-action@v4 + id: release with: token: ${{ secrets.GITHUB_TOKEN }} config-file: .release-please-manifest.json manifest-file: .release-please-manifest.json + + # Publish to npm when releases are created + - uses: actions/setup-node@v4 + if: ${{ steps.release.outputs.releases_created }} + with: + node-version: '18' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + if: ${{ steps.release.outputs.releases_created }} + run: | + corepack enable + pnpm install --frozen-lockfile + + - name: Publish packages + if: ${{ steps.release.outputs.releases_created }} + run: | + # Check which packages were released and publish them + echo "Releases created: ${{ steps.release.outputs.releases_created }}" + + # Publish core package if it was released + if [[ '${{ steps.release.outputs.packages_core--release_created }}' == 'true' ]]; then + echo "📦 Publishing @react-zero-ui/core..." + cd packages/core + npm publish --access public + cd ../.. + fi + + # Publish CLI package if it was released + if [[ '${{ steps.release.outputs.packages_cli--release_created }}' == 'true' ]]; then + echo "📦 Publishing create-zero-ui..." + cd packages/cli + npm publish --access public + cd ../.. + fi + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 5d3307b..2338da7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ # package artifacts node_modules/ **/.next/ -dist/ +**/dist/ +# **/build/ coverage/ test-results/ .pnpm-store/ @@ -19,11 +20,8 @@ yarn-error.log* # tarballs produced during local tests *.tgz -# local scratch files -t.py -todo.md # keep these files !next-env.d.ts +!packages/core/src/dist/ - \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 3c03064..295e1be 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -6,7 +6,7 @@ "useTabs": true, "printWidth": 160, "endOfLine": "lf", - "arrowParens": "avoid", + "arrowParens": "always", "bracketSpacing": true, "objectWrap": "collapse", "bracketSameLine": true, diff --git a/README.md b/README.md index 55f70ac..5f4ba09 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # React Zero‑UI (Beta) -**Instant UI state updates. ZERO React re‑renders. <1 KB runtime.** +**Instant UI state updates. ZERO React re‑renders. Near ZERO runtime. <500 bytes** Pre‑render your UI once, flip a `data-*` attribute to update — that's it. @@ -156,18 +156,18 @@ Any `data-{key}="{value}"` pair becomes a variant: `{key}-{value}:`. - **Zero React re‑renders** for UI‑only state. - **Global setters** — call from any component or util. - **Tiny**: < 391 Byte gzipped runtime. -- **TypeScript‑first**. - **SSR‑friendly** (Next.js & Vite SSR). -- **Framework‑agnostic CSS** — generated classes work in plain HTML / Vue / Svelte as well with extra config. +- **Use from anywhere** — Consume with tailwind variants from anywhere. --- ## 🏗 Best Practices -1. **UI state only** → themes, layout toggles, feature flags. +1. **Global UI state only** → themes, layout toggles, feature flags. 2. **Business logic stays in React** → fetching, data mutation, etc. 3. **Kebab‑case keys** → e.g. `sidebar-open`. 4. **Provide defaults** to avoid Flash‑Of‑Unstyled‑Content. +5. **Avoid** for per-component logic or data. --- diff --git a/docs/assets/internal.md b/docs/assets/internal.md new file mode 100644 index 0000000..30b1804 --- /dev/null +++ b/docs/assets/internal.md @@ -0,0 +1,146 @@ +Below is a **“mental model”** of the Zero-UI variant extractor. distilled so that _another_ human (or LLM) can reason about, extend, or safely refactor the code-base. + +--- + +## 1. Top-level goal + +- **Locate every call** to a user-supplied React hook + + ```js + const [value, setterFn] = useUI('stateKey', 'initialValue'); + ``` + +- Statically discover **all possible string values** that flow into + `stateKey`, `initialValue`, and `setterFn()` arguments. + - `stateKey` can resolve to a local static string. + - `initialValue` is the same rule as above. + - `setterFn()` argument is many forms allowed (see table 3.1) but **must be resolvable**; otherwise the value is ignored (silent) _unless_ it looked resolvable but failed inside the helpers, in which case a targeted error is thrown. + - Imported bindings are never allowed - the dev must re-cast them through a local `const`. + +- Report the result as a list of `VariantData` objects. + +```ts +type VariantData = { + key: string; // 'stateKey' + values: string[]; // ['light','dark',…] (unique, sorted) + initialValue: string; // from 2nd arg of useUI() +}; +``` + +--- + +## 2. Two-pass pipeline + +| Pass | File-scope work | Output | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| **Pass 1 - `collectUseUISetters`** | 1. Traverse the AST once.
2. For each `useUI()` destructuring:
• validate shapes & count.
• resolve the **state key** and **initial value** with **`literalFromNode`** (see rules below).
• grab the **binding** of the setter variable. | `SetterMeta[]` = `{ binding, setterName, stateKey, initialValue }[]` | +| **Pass 2 - `harvestSetterValues`** | 1. For every `binding.referencePaths` (i.e. every place the setter is used)
2. Only keep `CallExpression`s: `setX(…)`
3. Examine the first argument:
• direct literal / identifier / template → resolve via `literalFromNode`.
• conditional `cond ? a : b` → resolve both arms.
• logical fallback `a \|\| b`, `a ?? b` → resolve each side.
   arrow / function bodies → collect every returned expression and resolve.
4. Add every successfully-resolved string to a `Set` bucket **per stateKey**. | `Map< stateKey, Set >` | + +`normalizeVariants` just converts that map back into the +`VariantData[]` shape (keeping initial values, sorting, etc.). + +--- + +## 3. The **literal-resolution micro-framework** + +Everything funnels through **`literalFromNode`**. +Think of it as a deterministic _static evaluator_ restricted to a +_very_ small grammar. + +### 3.1 Supported input forms (setterFn()) + +``` +┌────────────────────────────────┬─────────────────────────┐ +│ Expression │ Accepted? → Returns │ +├────────────────────────────────┼─────────────────────────┤ +│ dark │ ✔ string literal │ +│ `th-${COLOR}` │ ✔ if every `${}`resolves│ +│ const DARK │ ✔ if IDENT → const str │ +│ THEMES.dark/[idx]/?. │ ✔ if the whole chain is │ +│ │ top-level `const` │ +│ "a" + "b" │ ✔ "ab" │ +│ a ?? b or a || b │ ✔ Attempts to resolve │ +│ │ both sides │ +│prev =>(prev=== 'a' ? 'b' : 'a')│ "a" ,"b" │ +│ Anything imported │ ❌ Hard error │ +│ Anything dynamic at runtime │ ❌ Returns null / error │ +└────────────────────────────────┴─────────────────────────┘ +``` + +### 3.2 Helpers + +| Helper | Job | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`resolveTemplateLiteral`** | Ensures every `${expr}` itself resolves to a string via `literalFromNode`. | +| **`resolveLocalConstIdentifier`** | Maps an `Identifier` → its `const` initializer _if_ that initializer is an accepted string/ template. Rejects imported bindings with a _single_ descriptive error. | +| **`resolveMemberExpression`** | Static walk of `obj.prop`, `obj['prop']`, `obj?.prop`, etc. Works through `as const`, optional-chaining, arrays, numbers, nested chains. Throws if any hop can't be resolved. | +| **`literalFromNode`** | Router that calls the above; memoised (`WeakMap`) so each AST node is evaluated once. | + +All helpers accept `opts:{ throwOnFail, source, hook }` so _contextual_ +error messages can be emitted with **`throwCodeFrame`** +(using `@babel/code-frame` to show a coloured snippet). + +--- + +## 4. Validation rules (why errors occur) + +| Position in `useUI` | Allowed value | Example error | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | +| **stateKey (arg 0)** | _Local_ static string | `State key cannot be resolved at build-time.` | +| **initialValue (arg 1)** | Same rule as above. | `Initial value cannot be resolved …` | +| **setter argument** | Many forms allowed (see table 3.1) but **must be resolvable**; otherwise the value is ignored (silent) _unless_ it looked resolvable but failed inside the helpers, in which case a targeted error is thrown. | | + +Imported bindings are never allowed - the dev must re-cast them +through a local `const`. + +--- + +## 5. Optional-chain & optional-member details + +- The updated `resolveMemberExpression` loop iterates while + `isMemberExpression || isOptionalMemberExpression`. +- Inside array/obj traversal it throws a clear error if a link is + missing instead of silently returning `null`. +- `props` collects mixed `string | number` keys in **reverse** (deep → shallow) order, so they can be replayed from the root identifier outward. + +--- + +## 6. Performance enhancements + +- **Memoisation** (`WeakMap`) across _both_ passes. +- **Quick literals** - string & number keys handled without extra calls. +- `throwCodeFrame` (and thus `generate(node).code`) runs **only** on + failing branches. +- A small **LRU file-cache** (<5k entries) avoids re-parsing unchanged + files (mtime + size signature, with hash fallback). + +--- + +## 7. What is **not** supported + +- Runtime-only constructs (`import.meta`, env checks, dynamic imports …). +- Cross-file constant propagation - the extractor is intentionally + single-file to keep the build independent of user bundler config. +- Non-string variants (numbers, booleans) - strings only. +- Private class fields in member chains. +- Setter arguments that are **imported functions**. + +--- + +## 8. How to extend + +- **Add more expression kinds**: extend `literalFromNode` with new + cases _and_ unit-test them. +- **Cross-file constants**: in `resolveLocalConstIdentifier`, detect + `ImportSpecifier`, read & parse the target file, then recurse - but + beware performance. +- **Boolean / number variants**: relax `literalToString` and adjust + variant schema. + +--- + +> **In one sentence**: +> The extractor turns _purely static, in-file JavaScript_ around `useUI` +> into a deterministic list of variant strings, throwing early and with +> helpful frames whenever something would otherwise need runtime +> evaluation. diff --git a/eslint.config.js b/eslint.config.js index 495cbe8..a1a8396 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,6 +1,7 @@ // eslint.config.js - ESLint 9 flat-config, JS + CJS import js from '@eslint/js'; import nodePlugin from 'eslint-plugin-node'; +import importPlugin from 'eslint-plugin-import'; const nodeGlobals = { require: 'readonly', @@ -32,7 +33,7 @@ const browserGlobals = { export default [ /* 1 - never lint generated / vendor files */ - { ignores: ['**/node_modules/**', '**/dist/**', '**/.next/**', 'eslint.config.js'] }, + { ignores: ['**/node_modules/**', '**/dist/**', '**/.next/**', 'eslint.config.js', './packages/core/__tests__/unit/fixtures'] }, /* 2 - baseline rules */ js.configs.recommended, @@ -40,16 +41,47 @@ export default [ /* 3 - CommonJS (*.cjs) */ { files: ['**/*.cjs'], - plugins: { node: nodePlugin }, + plugins: { node: nodePlugin, import: importPlugin }, languageOptions: { ecmaVersion: 'latest', sourceType: 'script', globals: nodeGlobals }, - rules: { 'node/no-unsupported-features/es-syntax': 'off' }, + rules: { + 'node/no-unsupported-features/es-syntax': 'off', + 'import/no-unresolved': 'error', // Catch unresolved imports + 'import/named': 'error', // Catch missing named exports + 'import/default': 'error', // Catch missing default exports + 'import/no-absolute-path': 'error', // Prevent absolute paths + }, }, /* 4 - ES-module / browser (*.js) */ { files: ['**/*.js'], - plugins: { node: nodePlugin }, + plugins: { node: nodePlugin, import: importPlugin }, languageOptions: { ecmaVersion: 'latest', sourceType: 'module', globals: { ...nodeGlobals, ...browserGlobals } }, - rules: { 'node/no-unsupported-features/es-syntax': 'off' }, + rules: { + 'node/no-unsupported-features/es-syntax': 'off', + 'import/no-unresolved': 'error', + 'import/named': 'error', + 'import/default': 'error', + 'import/no-absolute-path': 'error', + }, + }, + + /* 5 - JSX files */ + { + files: ['**/*.jsx'], + plugins: { node: nodePlugin, import: importPlugin }, + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { ...nodeGlobals, ...browserGlobals }, + parserOptions: { ecmaFeatures: { jsx: true } }, + }, + rules: { + 'node/no-unsupported-features/es-syntax': 'off', + 'import/no-unresolved': 'error', + 'import/named': 'error', + 'import/default': 'error', + 'import/no-absolute-path': 'error', + }, }, ]; diff --git a/examples/demo/package.json b/examples/demo/package.json index 4616b02..40bfdc0 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -19,7 +19,7 @@ "@vercel/analytics": "^1.5.0", "clsx": "^2.1.1", "motion": "12.18.1", - "next": "^15.3.4", + "next": "^15.3.5", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/demo/src/app/(test)/ReactTracker.tsx b/examples/demo/src/app/(test)/ReactTracker.tsx index bc106e3..ddbca98 100644 --- a/examples/demo/src/app/(test)/ReactTracker.tsx +++ b/examples/demo/src/app/(test)/ReactTracker.tsx @@ -29,7 +29,7 @@ class RenderStore { this.data.set(componentName, { count: existing.count + 1, lastRenderTime: renderTime, totalRenderTime: existing.totalRenderTime + renderTime }); - this.listeners.forEach(listener => listener()); + this.listeners.forEach((listener) => listener()); } } @@ -41,7 +41,7 @@ const trackerListeners = new Set<() => void>(); const setTrackerEnabled = (enabled: boolean) => { isTrackerEnabled = enabled; - trackerListeners.forEach(listener => listener()); + trackerListeners.forEach((listener) => listener()); }; // Flash animations store @@ -71,11 +71,11 @@ const addFlash = (element: HTMLElement, componentName: string, renderTime: numbe }; flashStore = [...flashStore, flash]; - flashListeners.forEach(listener => listener()); + flashListeners.forEach((listener) => listener()); setTimeout(() => { - flashStore = flashStore.filter(f => f.id !== flash.id); - flashListeners.forEach(listener => listener()); + flashStore = flashStore.filter((f) => f.id !== flash.id); + flashListeners.forEach((listener) => listener()); }, 1500); }; @@ -84,7 +84,7 @@ export function RenderTracker() { const renderMetrics = useSyncExternalStore(renderStore.subscribe, renderStore.getSnapshot, renderStore.getSnapshot); const showTracker = useSyncExternalStore( - listener => { + (listener) => { trackerListeners.add(listener); return () => trackerListeners.delete(listener); }, @@ -93,7 +93,7 @@ export function RenderTracker() { ); const flashes = useSyncExternalStore( - listener => { + (listener) => { flashListeners.add(listener); return () => flashListeners.delete(listener); }, @@ -157,7 +157,7 @@ export function RenderTracker() {
{showTracker && - flashes.map(flash => ( + flashes.map((flash) => ( - setMenuOpen(prev => !prev)} /> + setMenuOpen((prev) => !prev)} />
); diff --git a/examples/demo/src/app/components/MobileMenu.tsx b/examples/demo/src/app/components/MobileMenu.tsx index 3034bbd..79cb31b 100644 --- a/examples/demo/src/app/components/MobileMenu.tsx +++ b/examples/demo/src/app/components/MobileMenu.tsx @@ -14,7 +14,7 @@ export const MobileMenu: React.FC<{ navItems: { name: string; href: string }[] } style={{ '--index': index } as React.CSSProperties}> setMenuOpen(prev => (prev === 'closed' ? 'open' : 'closed'))} + onClick={() => setMenuOpen((prev) => (prev === 'closed' ? 'open' : 'closed'))} className="block pt-4 font-medium"> {item.name} @@ -23,7 +23,7 @@ export const MobileMenu: React.FC<{ navItems: { name: string; href: string }[] }
  • setMenuOpen(prev => (prev === 'closed' ? 'open' : 'closed'))} + onClick={() => setMenuOpen((prev) => (prev === 'closed' ? 'open' : 'closed'))} className="bubble-hover block rounded-full border border-gray-200 bg-white px-3 py-2 text-center font-medium shadow-lg duration-300 hover:border-white"> Contact diff --git a/examples/demo/src/app/components/MobileMenuButton.tsx b/examples/demo/src/app/components/MobileMenuButton.tsx index 99d99ac..48bcb9c 100644 --- a/examples/demo/src/app/components/MobileMenuButton.tsx +++ b/examples/demo/src/app/components/MobileMenuButton.tsx @@ -16,7 +16,7 @@ export const MobileMenuButton: React.FC = () => { setMobileMenu('closed'); }); - useMotionValueEvent(scrollY, 'change', current => { + useMotionValueEvent(scrollY, 'change', (current) => { if (!isDesktop) return; const previous = scrollY.getPrevious() ?? current; @@ -35,7 +35,7 @@ export const MobileMenuButton: React.FC = () => { if (isDesktop) setScrolled('up'); }} onClick={() => { - if (!isDesktop) setMobileMenu(prev => (prev === 'closed' ? 'open' : 'closed')); + if (!isDesktop) setMobileMenu((prev) => (prev === 'closed' ? 'open' : 'closed')); }} className={clsx( 'md:scrolled-up:opacity-0 md:scrolled-up:pointer-events-none group right-3 h-6 w-6 text-sm transition-all duration-300 ease-in-out hover:cursor-pointer md:absolute' diff --git a/examples/demo/src/app/components/TopBar.tsx b/examples/demo/src/app/components/TopBar.tsx index d1ccfa5..cff3d48 100644 --- a/examples/demo/src/app/components/TopBar.tsx +++ b/examples/demo/src/app/components/TopBar.tsx @@ -32,7 +32,7 @@ export const TopBarV2: React.FC = () => { {/* Desktop Navigation */}
      - {navItems.map(item => ( + {navItems.map((item) => (
    • diff --git a/examples/demo/src/app/react/Dashboard.tsx b/examples/demo/src/app/react/Dashboard.tsx index fa62058..9883f5a 100644 --- a/examples/demo/src/app/react/Dashboard.tsx +++ b/examples/demo/src/app/react/Dashboard.tsx @@ -17,7 +17,7 @@ export const Dashboard: React.FC = () => {
      diff --git a/examples/demo/src/app/style/page.tsx b/examples/demo/src/app/style/page.tsx new file mode 100644 index 0000000..b6cb424 --- /dev/null +++ b/examples/demo/src/app/style/page.tsx @@ -0,0 +1,902 @@ +'use client'; + +import { useUI } from '@react-zero-ui/core'; + +const variants = [ + // Basic theme variants - background colors + 'theme-light:bg-gray-100', + 'theme-light:bg-gray-200', + 'theme-light:bg-gray-300', + 'theme-light:bg-gray-400', + 'theme-light:bg-gray-500', + 'theme-light:bg-gray-600', + 'theme-light:bg-gray-700', + 'theme-light:bg-gray-800', + 'theme-light:bg-gray-900', + 'theme-dark:bg-gray-100', + 'theme-dark:bg-gray-200', + 'theme-dark:bg-gray-300', + 'theme-dark:bg-gray-400', + 'theme-dark:bg-gray-500', + 'theme-dark:bg-gray-600', + 'theme-dark:bg-gray-700', + 'theme-dark:bg-gray-800', + 'theme-dark:bg-gray-900', + + // More background colors + 'theme-light:bg-red-100', + 'theme-light:bg-red-200', + 'theme-light:bg-red-300', + 'theme-light:bg-red-400', + 'theme-light:bg-red-500', + 'theme-light:bg-red-600', + 'theme-light:bg-red-700', + 'theme-light:bg-red-800', + 'theme-light:bg-red-900', + 'theme-light:bg-blue-100', + 'theme-light:bg-blue-200', + 'theme-light:bg-blue-300', + 'theme-light:bg-blue-400', + 'theme-light:bg-blue-500', + 'theme-light:bg-blue-600', + 'theme-light:bg-blue-700', + 'theme-light:bg-blue-800', + 'theme-light:bg-blue-900', + 'theme-light:bg-green-100', + 'theme-light:bg-green-200', + 'theme-light:bg-green-300', + 'theme-light:bg-green-400', + 'theme-light:bg-green-500', + 'theme-light:bg-green-600', + 'theme-light:bg-green-700', + 'theme-light:bg-green-800', + 'theme-light:bg-green-900', + 'theme-light:bg-yellow-100', + 'theme-light:bg-yellow-200', + 'theme-light:bg-yellow-300', + 'theme-light:bg-yellow-400', + 'theme-light:bg-yellow-500', + 'theme-light:bg-yellow-600', + 'theme-light:bg-yellow-700', + 'theme-light:bg-yellow-800', + 'theme-light:bg-yellow-900', + 'theme-light:bg-purple-100', + 'theme-light:bg-purple-200', + 'theme-light:bg-purple-300', + 'theme-light:bg-purple-400', + 'theme-light:bg-purple-500', + 'theme-light:bg-purple-600', + 'theme-light:bg-purple-700', + 'theme-light:bg-purple-800', + 'theme-light:bg-purple-900', + 'theme-light:bg-pink-100', + 'theme-light:bg-pink-200', + 'theme-light:bg-pink-300', + 'theme-light:bg-pink-400', + 'theme-light:bg-pink-500', + 'theme-light:bg-pink-600', + 'theme-light:bg-pink-700', + 'theme-light:bg-pink-800', + 'theme-light:bg-pink-900', + 'theme-light:bg-indigo-100', + 'theme-light:bg-indigo-200', + 'theme-light:bg-indigo-300', + 'theme-light:bg-indigo-400', + 'theme-light:bg-indigo-500', + 'theme-light:bg-indigo-600', + 'theme-light:bg-indigo-700', + 'theme-light:bg-indigo-800', + 'theme-light:bg-indigo-900', + + // Dark theme background colors + 'theme-dark:bg-red-100', + 'theme-dark:bg-red-200', + 'theme-dark:bg-red-300', + 'theme-dark:bg-red-400', + 'theme-dark:bg-red-500', + 'theme-dark:bg-red-600', + 'theme-dark:bg-red-700', + 'theme-dark:bg-red-800', + 'theme-dark:bg-red-900', + 'theme-dark:bg-blue-100', + 'theme-dark:bg-blue-200', + 'theme-dark:bg-blue-300', + 'theme-dark:bg-blue-400', + 'theme-dark:bg-blue-500', + 'theme-dark:bg-blue-600', + 'theme-dark:bg-blue-700', + 'theme-dark:bg-blue-800', + 'theme-dark:bg-blue-900', + 'theme-dark:bg-green-100', + 'theme-dark:bg-green-200', + 'theme-dark:bg-green-300', + 'theme-dark:bg-green-400', + 'theme-dark:bg-green-500', + 'theme-dark:bg-green-600', + 'theme-dark:bg-green-700', + 'theme-dark:bg-green-800', + 'theme-dark:bg-green-900', + + // Text colors + 'theme-light:text-gray-100', + 'theme-light:text-gray-200', + 'theme-light:text-gray-300', + 'theme-light:text-gray-400', + 'theme-light:text-gray-500', + 'theme-light:text-gray-600', + 'theme-light:text-gray-700', + 'theme-light:text-gray-800', + 'theme-light:text-gray-900', + 'theme-light:text-red-100', + 'theme-light:text-red-200', + 'theme-light:text-red-300', + 'theme-light:text-red-400', + 'theme-light:text-red-500', + 'theme-light:text-red-600', + 'theme-light:text-red-700', + 'theme-light:text-red-800', + 'theme-light:text-red-900', + 'theme-light:text-blue-100', + 'theme-light:text-blue-200', + 'theme-light:text-blue-300', + 'theme-light:text-blue-400', + 'theme-light:text-blue-500', + 'theme-light:text-blue-600', + 'theme-light:text-blue-700', + 'theme-light:text-blue-800', + 'theme-light:text-blue-900', + 'theme-dark:text-gray-100', + 'theme-dark:text-gray-200', + 'theme-dark:text-gray-300', + 'theme-dark:text-gray-400', + 'theme-dark:text-gray-500', + 'theme-dark:text-red-100', + 'theme-dark:text-red-200', + 'theme-dark:text-red-300', + 'theme-dark:text-red-400', + 'theme-dark:text-red-500', + 'theme-dark:text-blue-100', + 'theme-dark:text-blue-200', + 'theme-dark:text-blue-300', + 'theme-dark:text-blue-400', + 'theme-dark:text-blue-500', + + // Border colors + 'theme-light:border-gray-100', + 'theme-light:border-gray-200', + 'theme-light:border-gray-300', + 'theme-light:border-gray-400', + 'theme-light:border-gray-500', + 'theme-light:border-red-100', + 'theme-light:border-red-200', + 'theme-light:border-red-300', + 'theme-light:border-red-400', + 'theme-light:border-red-500', + 'theme-light:border-blue-100', + 'theme-light:border-blue-200', + 'theme-light:border-blue-300', + 'theme-light:border-blue-400', + 'theme-light:border-blue-500', + 'theme-dark:border-gray-100', + 'theme-dark:border-gray-200', + 'theme-dark:border-gray-300', + 'theme-dark:border-gray-400', + 'theme-dark:border-gray-500', + 'theme-dark:border-red-100', + 'theme-dark:border-red-200', + 'theme-dark:border-red-300', + 'theme-dark:border-red-400', + 'theme-dark:border-red-500', + + // Responsive variants with theme + 'sm:theme-light:bg-gray-100', + 'sm:theme-light:bg-gray-200', + 'sm:theme-light:bg-gray-300', + 'sm:theme-light:bg-gray-400', + 'sm:theme-light:bg-gray-500', + 'sm:theme-light:bg-red-100', + 'sm:theme-light:bg-red-200', + 'sm:theme-light:bg-red-300', + 'sm:theme-light:bg-blue-100', + 'sm:theme-light:bg-blue-200', + 'sm:theme-light:bg-blue-300', + 'sm:theme-dark:bg-gray-100', + 'sm:theme-dark:bg-gray-200', + 'sm:theme-dark:bg-gray-300', + 'sm:theme-dark:bg-red-100', + 'sm:theme-dark:bg-red-200', + 'sm:theme-dark:bg-blue-100', + 'sm:theme-dark:bg-blue-200', + + 'md:theme-light:bg-gray-100', + 'md:theme-light:bg-gray-200', + 'md:theme-light:bg-gray-300', + 'md:theme-light:bg-gray-400', + 'md:theme-light:bg-gray-500', + 'md:theme-light:bg-red-100', + 'md:theme-light:bg-red-200', + 'md:theme-light:bg-red-300', + 'md:theme-light:bg-blue-100', + 'md:theme-light:bg-blue-200', + 'md:theme-light:bg-blue-300', + 'md:theme-dark:bg-gray-100', + 'md:theme-dark:bg-gray-200', + 'md:theme-dark:bg-gray-300', + 'md:theme-dark:bg-red-100', + 'md:theme-dark:bg-red-200', + 'md:theme-dark:bg-blue-100', + + 'lg:theme-light:bg-gray-100', + 'lg:theme-light:bg-gray-200', + 'lg:theme-light:bg-gray-300', + 'lg:theme-light:bg-gray-400', + 'lg:theme-light:bg-gray-500', + 'lg:theme-light:bg-red-100', + 'lg:theme-light:bg-red-200', + 'lg:theme-light:bg-red-300', + 'lg:theme-light:bg-blue-100', + 'lg:theme-light:bg-blue-200', + 'lg:theme-dark:bg-gray-100', + 'lg:theme-dark:bg-gray-200', + 'lg:theme-dark:bg-gray-300', + 'lg:theme-dark:bg-red-100', + 'lg:theme-dark:bg-red-200', + + 'xl:theme-light:bg-gray-100', + 'xl:theme-light:bg-gray-200', + 'xl:theme-light:bg-gray-300', + 'xl:theme-light:bg-gray-400', + 'xl:theme-light:bg-gray-500', + 'xl:theme-light:bg-red-100', + 'xl:theme-light:bg-red-200', + 'xl:theme-light:bg-blue-100', + 'xl:theme-light:bg-blue-200', + 'xl:theme-dark:bg-gray-100', + 'xl:theme-dark:bg-gray-200', + 'xl:theme-dark:bg-red-100', + 'xl:theme-dark:bg-red-200', + + '2xl:theme-light:bg-gray-100', + '2xl:theme-light:bg-gray-200', + '2xl:theme-light:bg-gray-300', + '2xl:theme-light:bg-gray-400', + '2xl:theme-light:bg-gray-500', + '2xl:theme-light:bg-red-100', + '2xl:theme-light:bg-red-200', + '2xl:theme-dark:bg-gray-100', + '2xl:theme-dark:bg-gray-200', + + // Accent color variants + 'accent-violet:bg-violet-100', + 'accent-violet:bg-violet-200', + 'accent-violet:bg-violet-300', + 'accent-violet:bg-violet-400', + 'accent-violet:bg-violet-500', + 'accent-violet:bg-violet-600', + 'accent-violet:bg-violet-700', + 'accent-violet:bg-violet-800', + 'accent-violet:bg-violet-900', + 'accent-violet:text-violet-100', + 'accent-violet:text-violet-200', + 'accent-violet:text-violet-300', + 'accent-violet:text-violet-400', + 'accent-violet:text-violet-500', + 'accent-violet:text-violet-600', + 'accent-violet:text-violet-700', + 'accent-violet:text-violet-800', + 'accent-violet:text-violet-900', + 'accent-violet:border-violet-100', + 'accent-violet:border-violet-200', + 'accent-violet:border-violet-300', + 'accent-violet:border-violet-400', + 'accent-violet:border-violet-500', + 'accent-violet:border-violet-600', + 'accent-violet:border-violet-700', + 'accent-violet:border-violet-800', + 'accent-violet:border-violet-900', + + 'accent-emerald:bg-emerald-100', + 'accent-emerald:bg-emerald-200', + 'accent-emerald:bg-emerald-300', + 'accent-emerald:bg-emerald-400', + 'accent-emerald:bg-emerald-500', + 'accent-emerald:bg-emerald-600', + 'accent-emerald:bg-emerald-700', + 'accent-emerald:bg-emerald-800', + 'accent-emerald:bg-emerald-900', + 'accent-emerald:text-emerald-100', + 'accent-emerald:text-emerald-200', + 'accent-emerald:text-emerald-300', + 'accent-emerald:text-emerald-400', + 'accent-emerald:text-emerald-500', + 'accent-emerald:text-emerald-600', + 'accent-emerald:text-emerald-700', + 'accent-emerald:text-emerald-800', + 'accent-emerald:text-emerald-900', + 'accent-emerald:border-emerald-100', + 'accent-emerald:border-emerald-200', + 'accent-emerald:border-emerald-300', + 'accent-emerald:border-emerald-400', + 'accent-emerald:border-emerald-500', + 'accent-emerald:border-emerald-600', + 'accent-emerald:border-emerald-700', + 'accent-emerald:border-emerald-800', + 'accent-emerald:border-emerald-900', + + 'accent-amber:bg-amber-100', + 'accent-amber:bg-amber-200', + 'accent-amber:bg-amber-300', + 'accent-amber:bg-amber-400', + 'accent-amber:bg-amber-500', + 'accent-amber:bg-amber-600', + 'accent-amber:bg-amber-700', + 'accent-amber:bg-amber-800', + 'accent-amber:bg-amber-900', + 'accent-amber:text-amber-100', + 'accent-amber:text-amber-200', + 'accent-amber:text-amber-300', + 'accent-amber:text-amber-400', + 'accent-amber:text-amber-500', + 'accent-amber:text-amber-600', + 'accent-amber:text-amber-700', + 'accent-amber:text-amber-800', + 'accent-amber:text-amber-900', + 'accent-amber:border-amber-100', + 'accent-amber:border-amber-200', + 'accent-amber:border-amber-300', + 'accent-amber:border-amber-400', + 'accent-amber:border-amber-500', + 'accent-amber:border-amber-600', + 'accent-amber:border-amber-700', + 'accent-amber:border-amber-800', + 'accent-amber:border-amber-900', + + // Combined theme and accent variants + 'theme-light:accent-violet:bg-gray-100', + 'theme-light:accent-violet:bg-gray-200', + 'theme-light:accent-violet:bg-gray-300', + 'theme-light:accent-violet:bg-violet-100', + 'theme-light:accent-violet:bg-violet-200', + 'theme-light:accent-violet:bg-violet-300', + 'theme-light:accent-violet:text-gray-100', + 'theme-light:accent-violet:text-gray-200', + 'theme-light:accent-violet:text-violet-100', + 'theme-light:accent-violet:text-violet-200', + 'theme-light:accent-violet:border-gray-100', + 'theme-light:accent-violet:border-violet-100', + 'theme-light:accent-emerald:bg-gray-100', + 'theme-light:accent-emerald:bg-gray-200', + 'theme-light:accent-emerald:bg-emerald-100', + 'theme-light:accent-emerald:bg-emerald-200', + 'theme-light:accent-emerald:text-gray-100', + 'theme-light:accent-emerald:text-emerald-100', + 'theme-light:accent-emerald:border-gray-100', + 'theme-light:accent-emerald:border-emerald-100', + 'theme-light:accent-amber:bg-gray-100', + 'theme-light:accent-amber:bg-gray-200', + 'theme-light:accent-amber:bg-amber-100', + 'theme-light:accent-amber:bg-amber-200', + 'theme-light:accent-amber:text-gray-100', + 'theme-light:accent-amber:text-amber-100', + 'theme-light:accent-amber:border-gray-100', + 'theme-light:accent-amber:border-amber-100', + + 'theme-dark:accent-violet:bg-gray-100', + 'theme-dark:accent-violet:bg-gray-200', + 'theme-dark:accent-violet:bg-violet-100', + 'theme-dark:accent-violet:bg-violet-200', + 'theme-dark:accent-violet:text-gray-100', + 'theme-dark:accent-violet:text-violet-100', + 'theme-dark:accent-violet:border-gray-100', + 'theme-dark:accent-violet:border-violet-100', + 'theme-dark:accent-emerald:bg-gray-100', + 'theme-dark:accent-emerald:bg-emerald-100', + 'theme-dark:accent-emerald:text-gray-100', + 'theme-dark:accent-emerald:text-emerald-100', + 'theme-dark:accent-emerald:border-gray-100', + 'theme-dark:accent-emerald:border-emerald-100', + 'theme-dark:accent-amber:bg-gray-100', + 'theme-dark:accent-amber:bg-amber-100', + 'theme-dark:accent-amber:text-gray-100', + 'theme-dark:accent-amber:text-amber-100', + 'theme-dark:accent-amber:border-gray-100', + 'theme-dark:accent-amber:border-amber-100', + + // Responsive + theme + accent combinations + 'sm:theme-light:accent-violet:bg-gray-100', + 'sm:theme-light:accent-violet:bg-violet-100', + 'sm:theme-light:accent-violet:text-gray-100', + 'sm:theme-light:accent-violet:text-violet-100', + 'sm:theme-light:accent-violet:border-gray-100', + 'sm:theme-light:accent-violet:border-violet-100', + 'sm:theme-light:accent-emerald:bg-gray-100', + 'sm:theme-light:accent-emerald:bg-emerald-100', + 'sm:theme-light:accent-emerald:text-gray-100', + 'sm:theme-light:accent-emerald:text-emerald-100', + 'sm:theme-light:accent-emerald:border-gray-100', + 'sm:theme-light:accent-emerald:border-emerald-100', + 'sm:theme-light:accent-amber:bg-gray-100', + 'sm:theme-light:accent-amber:bg-amber-100', + 'sm:theme-light:accent-amber:text-gray-100', + 'sm:theme-light:accent-amber:text-amber-100', + 'sm:theme-light:accent-amber:border-gray-100', + 'sm:theme-light:accent-amber:border-amber-100', + 'sm:theme-dark:accent-violet:bg-gray-100', + 'sm:theme-dark:accent-violet:bg-violet-100', + 'sm:theme-dark:accent-emerald:bg-gray-100', + 'sm:theme-dark:accent-emerald:bg-emerald-100', + 'sm:theme-dark:accent-amber:bg-gray-100', + 'sm:theme-dark:accent-amber:bg-amber-100', + + 'md:theme-light:accent-violet:bg-gray-100', + 'md:theme-light:accent-violet:bg-violet-100', + 'md:theme-light:accent-emerald:bg-gray-100', + 'md:theme-light:accent-emerald:bg-emerald-100', + 'md:theme-light:accent-amber:bg-gray-100', + 'md:theme-light:accent-amber:bg-amber-100', + 'md:theme-dark:accent-violet:bg-gray-100', + 'md:theme-dark:accent-violet:bg-violet-100', + 'md:theme-dark:accent-emerald:bg-gray-100', + 'md:theme-dark:accent-emerald:bg-emerald-100', + 'md:theme-dark:accent-amber:bg-gray-100', + 'md:theme-dark:accent-amber:bg-amber-100', + + 'lg:theme-light:accent-violet:bg-gray-100', + 'lg:theme-light:accent-violet:bg-violet-100', + 'lg:theme-light:accent-emerald:bg-gray-100', + 'lg:theme-light:accent-emerald:bg-emerald-100', + 'lg:theme-light:accent-amber:bg-gray-100', + 'lg:theme-light:accent-amber:bg-amber-100', + 'lg:theme-dark:accent-violet:bg-gray-100', + 'lg:theme-dark:accent-violet:bg-violet-100', + 'lg:theme-dark:accent-emerald:bg-gray-100', + 'lg:theme-dark:accent-emerald:bg-emerald-100', + 'lg:theme-dark:accent-amber:bg-gray-100', + 'lg:theme-dark:accent-amber:bg-amber-100', + + 'xl:theme-light:accent-violet:bg-gray-100', + 'xl:theme-light:accent-violet:bg-violet-100', + 'xl:theme-light:accent-emerald:bg-gray-100', + 'xl:theme-light:accent-emerald:bg-emerald-100', + 'xl:theme-light:accent-amber:bg-gray-100', + 'xl:theme-light:accent-amber:bg-amber-100', + 'xl:theme-dark:accent-violet:bg-gray-100', + 'xl:theme-dark:accent-violet:bg-violet-100', + 'xl:theme-dark:accent-emerald:bg-gray-100', + 'xl:theme-dark:accent-emerald:bg-emerald-100', + 'xl:theme-dark:accent-amber:bg-gray-100', + 'xl:theme-dark:accent-amber:bg-amber-100', + + '2xl:theme-light:accent-violet:bg-gray-100', + '2xl:theme-light:accent-violet:bg-violet-100', + '2xl:theme-light:accent-emerald:bg-gray-100', + '2xl:theme-light:accent-emerald:bg-emerald-100', + '2xl:theme-light:accent-amber:bg-gray-100', + '2xl:theme-light:accent-amber:bg-amber-100', + '2xl:theme-dark:accent-violet:bg-gray-100', + '2xl:theme-dark:accent-violet:bg-violet-100', + '2xl:theme-dark:accent-emerald:bg-gray-100', + '2xl:theme-dark:accent-emerald:bg-emerald-100', + '2xl:theme-dark:accent-amber:bg-gray-100', + '2xl:theme-dark:accent-amber:bg-amber-100', + + // State variants with hover, focus, active + 'hover:theme-light:bg-gray-100', + 'hover:theme-light:bg-gray-200', + 'hover:theme-light:bg-gray-300', + 'hover:theme-light:bg-red-100', + 'hover:theme-light:bg-red-200', + 'hover:theme-light:bg-blue-100', + 'hover:theme-light:bg-blue-200', + 'hover:theme-dark:bg-gray-100', + 'hover:theme-dark:bg-gray-200', + 'hover:theme-dark:bg-red-100', + 'hover:theme-dark:bg-blue-100', + 'focus:theme-light:bg-gray-100', + 'focus:theme-light:bg-gray-200', + 'focus:theme-light:bg-red-100', + 'focus:theme-light:bg-blue-100', + 'focus:theme-dark:bg-gray-100', + 'focus:theme-dark:bg-red-100', + 'active:theme-light:bg-gray-100', + 'active:theme-light:bg-gray-200', + 'active:theme-light:bg-red-100', + 'active:theme-dark:bg-gray-100', + + // Responsive + hover combinations + 'sm:hover:theme-light:bg-gray-100', + 'sm:hover:theme-light:bg-gray-200', + 'sm:hover:theme-light:bg-red-100', + 'sm:hover:theme-dark:bg-gray-100', + 'md:hover:theme-light:bg-gray-100', + 'md:hover:theme-light:bg-red-100', + 'lg:hover:theme-light:bg-gray-100', + 'xl:hover:theme-light:bg-gray-100', + + // Spacing variants + 'theme-light:p-1', + 'theme-light:p-2', + 'theme-light:p-3', + 'theme-light:p-4', + 'theme-light:p-5', + 'theme-light:p-6', + 'theme-light:p-8', + 'theme-light:p-10', + 'theme-light:p-12', + 'theme-light:p-16', + 'theme-light:p-20', + 'theme-light:p-24', + 'theme-dark:p-1', + 'theme-dark:p-2', + 'theme-dark:p-3', + 'theme-dark:p-4', + 'theme-dark:p-5', + 'theme-dark:p-6', + 'theme-dark:p-8', + 'theme-dark:p-10', + 'theme-dark:p-12', + 'theme-dark:p-16', + 'theme-dark:p-20', + 'theme-dark:p-24', + + 'theme-light:m-1', + 'theme-light:m-2', + 'theme-light:m-3', + 'theme-light:m-4', + 'theme-light:m-5', + 'theme-light:m-6', + 'theme-light:m-8', + 'theme-light:m-10', + 'theme-light:m-12', + 'theme-light:m-16', + 'theme-light:m-20', + 'theme-light:m-24', + 'theme-dark:m-1', + 'theme-dark:m-2', + 'theme-dark:m-3', + 'theme-dark:m-4', + 'theme-dark:m-5', + 'theme-dark:m-6', + 'theme-dark:m-8', + 'theme-dark:m-10', + 'theme-dark:m-12', + 'theme-dark:m-16', + 'theme-dark:m-20', + 'theme-dark:m-24', + + // Width and height variants + 'theme-light:w-1', + 'theme-light:w-2', + 'theme-light:w-3', + 'theme-light:w-4', + 'theme-light:w-5', + 'theme-light:w-6', + 'theme-light:w-8', + 'theme-light:w-10', + 'theme-light:w-12', + 'theme-light:w-16', + 'theme-light:w-20', + 'theme-light:w-24', + 'theme-light:w-32', + 'theme-light:w-40', + 'theme-light:w-48', + 'theme-light:w-56', + 'theme-light:w-64', + 'theme-light:w-72', + 'theme-light:w-80', + 'theme-light:w-96', + 'theme-light:w-auto', + 'theme-light:w-full', + 'theme-light:w-screen', + 'theme-dark:w-1', + 'theme-dark:w-2', + 'theme-dark:w-3', + 'theme-dark:w-4', + 'theme-dark:w-5', + 'theme-dark:w-6', + 'theme-dark:w-8', + 'theme-dark:w-10', + 'theme-dark:w-12', + 'theme-dark:w-16', + 'theme-dark:w-20', + 'theme-dark:w-24', + 'theme-dark:w-32', + 'theme-dark:w-40', + 'theme-dark:w-48', + 'theme-dark:w-56', + 'theme-dark:w-64', + 'theme-dark:w-72', + 'theme-dark:w-80', + 'theme-dark:w-96', + + 'theme-light:h-1', + 'theme-light:h-2', + 'theme-light:h-3', + 'theme-light:h-4', + 'theme-light:h-5', + 'theme-light:h-6', + 'theme-light:h-8', + 'theme-light:h-10', + 'theme-light:h-12', + 'theme-light:h-16', + 'theme-light:h-20', + 'theme-light:h-24', + 'theme-light:h-32', + 'theme-light:h-40', + 'theme-light:h-48', + 'theme-light:h-56', + 'theme-light:h-64', + 'theme-light:h-72', + 'theme-light:h-80', + 'theme-light:h-96', + 'theme-light:h-auto', + 'theme-light:h-full', + 'theme-light:h-screen', + 'theme-dark:h-1', + 'theme-dark:h-2', + 'theme-dark:h-3', + 'theme-dark:h-4', + 'theme-dark:h-5', + 'theme-dark:h-6', + 'theme-dark:h-8', + 'theme-dark:h-10', + 'theme-dark:h-12', + 'theme-dark:h-16', + 'theme-dark:h-20', + 'theme-dark:h-24', + 'theme-dark:h-32', + 'theme-dark:h-40', + 'theme-dark:h-48', + 'theme-dark:h-56', + 'theme-dark:h-64', + 'theme-dark:h-72', + 'theme-dark:h-80', + 'theme-dark:h-96', + + // Typography variants + 'theme-light:text-xs', + 'theme-light:text-sm', + 'theme-light:text-base', + 'theme-light:text-lg', + 'theme-light:text-xl', + 'theme-light:text-2xl', + 'theme-light:text-3xl', + 'theme-light:text-4xl', + 'theme-light:text-5xl', + 'theme-light:text-6xl', + 'theme-light:text-7xl', + 'theme-light:text-8xl', + 'theme-light:text-9xl', + 'theme-dark:text-xs', + 'theme-dark:text-sm', + 'theme-dark:text-base', + 'theme-dark:text-lg', + 'theme-dark:text-xl', + 'theme-dark:text-2xl', + 'theme-dark:text-3xl', + 'theme-dark:text-4xl', + 'theme-dark:text-5xl', + 'theme-dark:text-6xl', + + 'theme-light:font-thin', + 'theme-light:font-extralight', + 'theme-light:font-light', + 'theme-light:font-normal', + 'theme-light:font-medium', + 'theme-light:font-semibold', + 'theme-light:font-bold', + 'theme-light:font-extrabold', + 'theme-light:font-black', + 'theme-dark:font-thin', + 'theme-dark:font-extralight', + 'theme-dark:font-light', + 'theme-dark:font-normal', + 'theme-dark:font-medium', + 'theme-dark:font-semibold', + 'theme-dark:font-bold', + 'theme-dark:font-extrabold', + 'theme-dark:font-black', + + // Border variants + 'theme-light:border-0', + 'theme-light:border', + 'theme-light:border-2', + 'theme-light:border-4', + 'theme-light:border-8', + 'theme-light:border-t', + 'theme-light:border-r', + 'theme-light:border-b', + 'theme-light:border-l', + 'theme-light:border-t-0', + 'theme-light:border-r-0', + 'theme-light:border-b-0', + 'theme-light:border-l-0', + 'theme-dark:border-0', + 'theme-dark:border', + 'theme-dark:border-2', + 'theme-dark:border-4', + 'theme-dark:border-8', + 'theme-dark:border-t', + 'theme-dark:border-r', + 'theme-dark:border-b', + 'theme-dark:border-l', + + // Rounded variants + 'theme-light:rounded-none', + 'theme-light:rounded-sm', + 'theme-light:rounded', + 'theme-light:rounded-md', + 'theme-light:rounded-lg', + 'theme-light:rounded-xl', + 'theme-light:rounded-2xl', + 'theme-light:rounded-3xl', + 'theme-light:rounded-full', + 'theme-dark:rounded-none', + 'theme-dark:rounded-sm', + 'theme-dark:rounded', + 'theme-dark:rounded-md', + 'theme-dark:rounded-lg', + 'theme-dark:rounded-xl', + 'theme-dark:rounded-2xl', + 'theme-dark:rounded-3xl', + 'theme-dark:rounded-full', + + // Shadow variants + 'theme-light:shadow-sm', + 'theme-light:shadow', + 'theme-light:shadow-md', + 'theme-light:shadow-lg', + 'theme-light:shadow-xl', + 'theme-light:shadow-2xl', + 'theme-light:shadow-inner', + 'theme-light:shadow-none', + 'theme-dark:shadow-sm', + 'theme-dark:shadow', + 'theme-dark:shadow-md', + 'theme-dark:shadow-lg', + 'theme-dark:shadow-xl', + 'theme-dark:shadow-2xl', + 'theme-dark:shadow-inner', + 'theme-dark:shadow-none', + + // Complex combinations for maximum CSS generation + 'sm:hover:focus:theme-light:accent-violet:bg-red-500', + 'md:hover:focus:theme-light:accent-emerald:bg-blue-500', + 'lg:hover:focus:theme-dark:accent-amber:bg-green-500', + 'xl:hover:active:theme-light:accent-violet:text-purple-500', + '2xl:focus:active:theme-dark:accent-emerald:border-yellow-500', + 'sm:md:theme-light:accent-violet:bg-gradient-to-r', + 'lg:xl:theme-dark:accent-amber:from-red-500', + 'hover:focus:active:theme-light:accent-emerald:to-blue-500', + + // Absolutely insane complex combinations + 'sm:md:lg:hover:focus:active:theme-light:theme-dark:accent-violet:accent-emerald:bg-red-500', + 'md:lg:xl:hover:active:focus:theme-dark:theme-light:accent-amber:accent-violet:text-blue-900', + 'lg:xl:2xl:focus:hover:active:theme-light:theme-dark:accent-emerald:accent-amber:border-purple-700', + 'sm:lg:2xl:active:hover:focus:theme-dark:theme-light:accent-violet:accent-emerald:bg-gradient-to-br', + 'md:xl:hover:focus:active:disabled:theme-light:theme-dark:accent-amber:accent-violet:from-red-500', + 'lg:2xl:focus:active:hover:checked:theme-dark:theme-light:accent-emerald:accent-amber:to-blue-800', + 'sm:md:xl:hover:active:focus:visited:theme-light:theme-dark:accent-violet:accent-emerald:via-green-600', + 'md:lg:2xl:active:focus:hover:invalid:theme-dark:theme-light:accent-amber:accent-violet:shadow-2xl', + 'sm:xl:2xl:hover:focus:active:required:theme-light:theme-dark:accent-emerald:accent-amber:rounded-full', + 'lg:xl:focus:hover:active:optional:theme-dark:theme-light:accent-violet:accent-emerald:border-dashed', + + // Multiple breakpoint chaos + 'sm:md:lg:xl:2xl:theme-light:accent-violet:bg-red-100', + 'md:lg:xl:2xl:theme-dark:accent-emerald:bg-blue-200', + 'sm:lg:xl:2xl:theme-light:accent-amber:bg-green-300', + 'sm:md:xl:2xl:theme-dark:accent-violet:bg-yellow-400', + 'sm:md:lg:2xl:theme-light:accent-emerald:bg-purple-500', + 'sm:md:lg:xl:theme-dark:accent-amber:bg-pink-600', + + // State combination madness + 'hover:focus:active:disabled:checked:theme-light:accent-violet:bg-red-500', + 'focus:active:hover:invalid:required:theme-dark:accent-emerald:text-blue-600', + 'active:hover:focus:visited:optional:theme-light:accent-amber:border-green-700', + 'hover:active:focus:checked:disabled:theme-dark:accent-violet:shadow-lg', + 'focus:hover:active:required:invalid:theme-light:accent-emerald:rounded-xl', + 'active:focus:hover:optional:visited:theme-dark:accent-amber:p-8', + + // Impossible but fun combinations + 'sm:md:lg:xl:2xl:hover:focus:active:disabled:checked:invalid:required:optional:visited:theme-light:theme-dark:accent-violet:accent-emerald:accent-amber:bg-red-500', + 'md:lg:xl:2xl:focus:active:hover:checked:disabled:required:invalid:visited:optional:theme-dark:theme-light:accent-emerald:accent-amber:accent-violet:text-blue-600', + 'lg:xl:2xl:active:hover:focus:invalid:required:checked:disabled:visited:optional:theme-light:theme-dark:accent-amber:accent-violet:accent-emerald:border-green-700', + + // Theme conflicts (testing edge cases) + 'theme-light:theme-dark:bg-red-500', + 'theme-dark:theme-light:text-blue-600', + 'theme-light:theme-dark:theme-light:border-green-700', + 'theme-dark:theme-light:theme-dark:shadow-xl', + + // Accent conflicts (testing edge cases) + 'accent-violet:accent-emerald:bg-red-500', + 'accent-emerald:accent-amber:text-blue-600', + 'accent-amber:accent-violet:border-green-700', + 'accent-violet:accent-emerald:accent-amber:shadow-lg', + + // Responsive with conflicting themes and accents + 'sm:theme-light:md:theme-dark:lg:theme-light:accent-violet:bg-red-500', + 'md:theme-dark:lg:theme-light:xl:theme-dark:accent-emerald:text-blue-600', + 'lg:theme-light:xl:theme-dark:2xl:theme-light:accent-amber:border-green-700', + 'sm:accent-violet:md:accent-emerald:lg:accent-amber:xl:accent-violet:bg-purple-500', + + // Ultra-nested responsive and state combinations + 'sm:hover:md:focus:lg:active:xl:disabled:2xl:checked:theme-light:accent-violet:bg-red-500', + 'md:focus:lg:hover:xl:active:2xl:invalid:sm:required:theme-dark:accent-emerald:text-blue-600', + 'lg:active:xl:hover:2xl:focus:sm:visited:md:optional:theme-light:accent-amber:border-green-700', + + // Gradient combinations with multiple breakpoints and states + 'sm:hover:md:focus:lg:active:theme-light:accent-violet:bg-gradient-to-r', + 'md:focus:lg:hover:xl:active:theme-dark:accent-emerald:from-red-500', + 'lg:active:xl:hover:2xl:focus:theme-light:accent-amber:via-blue-600', + 'xl:hover:2xl:focus:sm:active:theme-dark:accent-violet:to-green-700', + '2xl:focus:sm:hover:md:active:theme-light:accent-emerald:bg-gradient-to-br', + 'sm:active:md:hover:lg:focus:theme-dark:accent-amber:from-purple-500', + 'md:hover:lg:active:xl:focus:theme-light:accent-violet:via-pink-600', + 'lg:focus:xl:active:2xl:hover:theme-dark:accent-emerald:to-yellow-700', + + // Animation and transform combinations + 'sm:hover:focus:theme-light:accent-violet:transform:scale-105', + 'md:active:hover:theme-dark:accent-emerald:transition-all:duration-300', + 'lg:focus:active:theme-light:accent-amber:rotate-45:translate-x-4', + 'xl:hover:focus:theme-dark:accent-violet:animate-pulse:delay-150', + '2xl:active:hover:theme-light:accent-emerald:animate-bounce:duration-500', + + // Typography with complex combinations + 'sm:hover:md:focus:lg:active:theme-light:accent-violet:text-6xl:font-black:italic', + 'md:focus:lg:hover:xl:active:theme-dark:accent-emerald:text-xs:font-thin:underline', + 'lg:active:xl:hover:2xl:focus:theme-light:accent-amber:text-2xl:font-bold:line-through', + + // Spacing with complex combinations + 'sm:hover:md:focus:lg:active:theme-light:accent-violet:p-20:m-16:space-x-8', + 'md:focus:lg:hover:xl:active:theme-dark:accent-emerald:px-12:py-8:mx-6:my-4', + 'lg:active:xl:hover:2xl:focus:theme-light:accent-amber:pt-10:pb-6:pl-8:pr-4', + + // Flexbox and Grid combinations + 'sm:hover:md:focus:theme-light:accent-violet:flex:items-center:justify-between', + 'md:focus:lg:hover:theme-dark:accent-emerald:grid:grid-cols-12:gap-6', + 'lg:active:xl:hover:theme-light:accent-amber:flex:flex-col:items-stretch', + + // Positioning with complex combinations + 'sm:hover:md:focus:theme-light:accent-violet:absolute:top-4:right-6:z-50', + 'md:focus:lg:hover:theme-dark:accent-emerald:relative:inset-x-4:bottom-8', + 'lg:active:xl:hover:theme-light:accent-amber:fixed:left-0:top-0:w-full', + + // Overflow and display combinations + 'sm:hover:md:focus:theme-light:accent-violet:overflow-hidden:whitespace-nowrap', + 'md:focus:lg:hover:theme-dark:accent-emerald:overflow-scroll:block', + 'lg:active:xl:hover:theme-light:accent-amber:overflow-auto:inline-block', +]; + +export default function HeavyVariantsTest() { + const [_, setAccent] = useUI<'violet' | 'emerald' | 'amber'>('accent', 'violet'); + const [__, setTheme] = useUI<'light' | 'dark'>('theme', 'light'); + + return ( +
      +

      Heavy Variants Test

      +

      Total variants: {variants.length}

      +
      + + +
      +
      +

      This component should generate a massive amount of CSS to test your useUI hook's performance.

      +

      Check the generated CSS output to see how many rules are created!

      +
      +
      + ); +} diff --git a/examples/demo/src/app/zero-ui/Dashboard.tsx b/examples/demo/src/app/zero-ui/Dashboard.tsx index 69ae890..189795c 100644 --- a/examples/demo/src/app/zero-ui/Dashboard.tsx +++ b/examples/demo/src/app/zero-ui/Dashboard.tsx @@ -10,7 +10,7 @@ export const Dashboard: React.FC = () => {
      +
      + {answer} +
      +
      + ); +} + +export default FAQ; diff --git a/packages/core/__tests__/fixtures/next/app/UseEffectComponent.tsx b/packages/core/__tests__/fixtures/next/app/UseEffectComponent.tsx new file mode 100644 index 0000000..2df0e9a --- /dev/null +++ b/packages/core/__tests__/fixtures/next/app/UseEffectComponent.tsx @@ -0,0 +1,29 @@ +'use client'; +import { useEffect, useState } from 'react'; +import { useUI } from '@react-zero-ui/core'; + +// Component that automatically cycles through themes using useEffect +export default function UseEffectComponent() { + const [, setAutoTheme] = useUI<'light' | 'dark'>('use-effect-theme', 'light'); + const [state, setState] = useState(false); + + useEffect(() => { + setAutoTheme(state ? 'dark' : 'light'); + }, [state]); + + return ( +
      + +
      + Theme: Dark Light +
      +
      + ); +} diff --git a/packages/core/__tests__/fixtures/next/app/layout.tsx b/packages/core/__tests__/fixtures/next/app/layout.tsx index e9b8664..5456a59 100644 --- a/packages/core/__tests__/fixtures/next/app/layout.tsx +++ b/packages/core/__tests__/fixtures/next/app/layout.tsx @@ -1,14 +1,11 @@ -import { bodyAttributes } from '@zero-ui/attributes'; +import { bodyAttributes } from "@zero-ui/attributes"; import './globals.css'; -export default function RootLayout({ children }) { - return ( - - +export default function RootLayout({ + children +}) { + return + {children} - - ); -} + ; +} \ No newline at end of file diff --git a/packages/core/__tests__/fixtures/next/app/page.tsx b/packages/core/__tests__/fixtures/next/app/page.tsx index 64f0332..ac22b5c 100644 --- a/packages/core/__tests__/fixtures/next/app/page.tsx +++ b/packages/core/__tests__/fixtures/next/app/page.tsx @@ -1,132 +1,212 @@ 'use client'; +import { useUI } from '@react-zero-ui/core'; +import UseEffectComponent from './UseEffectComponent'; +import FAQ from './FAQ'; -import useUI from '@react-zero-ui/core'; -import { useEffect, useState } from 'react'; - -// Component that automatically cycles through themes using useEffect -function AutoThemeComponent() { - const [, setAutoTheme] = useUI<'light' | 'dark'>('auto-theme', 'light'); - const [isRunning, setIsRunning] = useState(false); - const [cycleCount, setCycleCount] = useState(0); - - useEffect(() => { - if (!isRunning) return; +export default function Page() { + const [, setTheme] = useUI<'light' | 'dark'>('theme', 'light'); + const [, setTheme2] = useUI<'light' | 'dark'>('theme-2', 'light'); + const [, setThemeThree] = useUI<'light' | 'dark'>('theme-three', 'light'); + const [, setToggle] = useUI<'true' | 'false'>('toggle-boolean', 'true'); + const [, setNumber] = useUI<'1' | '2'>('number', '1'); + const [, setOpen] = useUI<'open' | 'closed'>('faq', 'closed'); // Same key everywhere! + const [, setScope] = useUI<'off' | 'on'>('scope', 'off'); + const [, setMobile] = useUI<'true' | 'false'>('mobile', 'false'); - const interval = setInterval(() => { - setAutoTheme(prev => { - const newTheme = prev === 'light' ? 'dark' : 'light'; - setCycleCount(count => count + 1); - return newTheme; - }); - }, 2000); // Switch every 2 seconds + const [, setToggleFunction] = useUI<'white' | 'black'>('toggle-function', 'white'); - return () => clearInterval(interval); - }, [isRunning, setAutoTheme]); + const toggleFunction = () => { + setToggleFunction((prev) => (prev === 'white' ? 'black' : 'white')); + }; return (
      -

      Auto Theme Switcher (useEffect Test)

      - -
      - - -
      -
      -
      - Current Theme: - Light Mode - Dark Mode -
      - -
      Status: {isRunning ? 'Auto-switching...' : 'Stopped'}
      - -
      Cycle Count: {cycleCount}
      + className="p-8 theme-light:bg-white theme-dark:bg-white bg-black" + data-testid="page-container"> +

      Global State

      +
      +
      + {/* Auto Theme Component */} + + +
      + +
      + +
      + Theme: Dark Light
      -
      -
      -
      ☀️ Light Theme Active
      -
      🌙 Dark Theme Active
      +
      + +
      + +
      + Theme: Dark Light
      +
      -
      -
      Reactive UI
      -
      No Re-renders!
      +
      + +
      + +
      + Theme: Dark Light
      -
      -
      - ); -} - -export default function Page() { - const [, setTheme] = useUI<'light' | 'dark'>('theme', 'light'); - const [, setTheme2] = useUI<'light' | 'dark'>('theme-2', 'light'); - const [, setThemeThree] = useUI<'light' | 'dark'>('themeThree', 'light'); - return ( - <> - {/* Auto Theme Component - NEW */} - - -
      -
      - -
      - Theme: Dark Light +
      + +
      + +
      + Boolean: True False +
      +
      +
      + +
      + +
      + Number: 1 2 +
      +
      +
      + +
      + +
      + Function: White Black +
      - +
      +

      Scoped Style Tests


      -
      - -
      - Theme: Dark Light +
      +
      + +
      + Scope: False + True +
      +
      +
      + +
      + +
      + Mobile: False + True +
      -
      - -
      +
      -
      - Theme: Dark Light -
      +
      answer
      - + + + + +
      ); } diff --git a/packages/core/__tests__/fixtures/next/app/test/page.jsx b/packages/core/__tests__/fixtures/next/app/test/page.jsx new file mode 100644 index 0000000..509b696 --- /dev/null +++ b/packages/core/__tests__/fixtures/next/app/test/page.jsx @@ -0,0 +1,209 @@ +// 'use client'; + +// import useUI from '@react-zero-ui/core'; +// import UseEffectComponent from '../UseEffectComponent'; +// import FAQ from '../FAQ'; + +// export default function Page() { +// const [, setTheme] = useUI('theme', 'light'); +// const [, setTheme2] = useUI('theme-2', 'light'); +// const [, setThemeThree] = useUI('theme-three', 'light'); +// const [, setToggle] = useUI('toggle-boolean', 'true'); +// const [, setNumber] = useUI('number', '1'); +// const [, setOpen] = useUI('faq', 'closed'); // Same key everywhere! +// const [, setScope] = useUI('scope', 'off'); +// const [, setMobile] = useUI('mobile', 'false'); + +// const [, setToggleFunction] = useUI('toggle-function', 'white'); + +// const toggleFunction = () => { +// setToggleFunction((prev) => (prev === 'white' ? 'black' : 'white')); +// }; + +// return ( +//
      +//

      Global State

      +//
      +//
      +// {/* Auto Theme Component */} +// + +//
      + +//
      +// +//
      +// Theme: Dark Light +//
      +//
      + +//
      + +//
      +// +//
      +// Theme: Dark Light +//
      +//
      + +//
      + +//
      +// +//
      +// Theme: Dark Light +//
      +//
      + +//
      + +//
      +// +//
      +// Boolean: True False +//
      +//
      +//
      + +//
      +// +//
      +// Number: 1 2 +//
      +//
      +//
      + +//
      +// +//
      +// Function: White Black +//
      +//
      +//
      +//
      +//

      Scoped Style Tests

      +//
      + +//
      +//
      +// +//
      +// Scope: False +// True +//
      +//
      +//
      + +//
      +// +//
      +// Mobile: False +// True +//
      +//
      +//
      + +//
      +// +//
      answer
      +//
      + +// +// +// +//
      +// ); +// } diff --git a/packages/core/__tests__/fixtures/next/app/variables.ts b/packages/core/__tests__/fixtures/next/app/variables.ts new file mode 100644 index 0000000..e4b0dd7 --- /dev/null +++ b/packages/core/__tests__/fixtures/next/app/variables.ts @@ -0,0 +1,4 @@ +export const THEME_BLUE = 'blue'; +export const THEME_RED = 'red'; + +export const THEMES: Record<'blueee' | 'dark', string> = { blueee: 'blue', dark: 'dark' }; diff --git a/packages/core/__tests__/fixtures/next/package.json b/packages/core/__tests__/fixtures/next/package.json index 72a02c0..433ac84 100644 --- a/packages/core/__tests__/fixtures/next/package.json +++ b/packages/core/__tests__/fixtures/next/package.json @@ -2,7 +2,7 @@ "name": "test-nextjs-app", "version": "1.0.0", "scripts": { - "dev": "next dev", + "dev": "rm -rf .next && next dev", "build": "next build", "start": "next start", "clean": "rm -rf .next node_modules package-lock.json" diff --git a/packages/core/__tests__/fixtures/next/tsconfig.json b/packages/core/__tests__/fixtures/next/tsconfig.json index 2da9377..d73e90d 100644 --- a/packages/core/__tests__/fixtures/next/tsconfig.json +++ b/packages/core/__tests__/fixtures/next/tsconfig.json @@ -30,12 +30,12 @@ } }, "include": [ - "next-env.d.ts", - ".next/types/**/*.ts", "**/*.ts", "**/*.tsx", ".next/**/*.d.ts", - ".zero-ui/**/*.d.ts" + ".next/types/**/*.ts", + ".zero-ui/**/*.d.ts", + "next-env.d.ts" ], "exclude": [ "node_modules" diff --git a/packages/core/__tests__/fixtures/vite/.zero-ui/attributes.d.ts b/packages/core/__tests__/fixtures/vite/.zero-ui/attributes.d.ts index f925b09..f0f376e 100644 --- a/packages/core/__tests__/fixtures/vite/.zero-ui/attributes.d.ts +++ b/packages/core/__tests__/fixtures/vite/.zero-ui/attributes.d.ts @@ -1,6 +1,13 @@ /* AUTO-GENERATED - DO NOT EDIT */ export declare const bodyAttributes: { + "data-faq": "closed" | "open"; + "data-mobile": "false" | "true"; + "data-number": "1" | "2"; + "data-scope": "off" | "on"; "data-theme": "dark" | "light"; "data-theme-2": "dark" | "light"; "data-theme-three": "dark" | "light"; + "data-toggle-boolean": "false" | "true"; + "data-toggle-function": "black" | "blue" | "green" | "red" | "white"; + "data-use-effect-theme": "dark" | "light"; }; diff --git a/packages/core/__tests__/fixtures/vite/.zero-ui/attributes.js b/packages/core/__tests__/fixtures/vite/.zero-ui/attributes.js index 9378fb2..2bbbf57 100644 --- a/packages/core/__tests__/fixtures/vite/.zero-ui/attributes.js +++ b/packages/core/__tests__/fixtures/vite/.zero-ui/attributes.js @@ -1,6 +1,13 @@ /* AUTO-GENERATED - DO NOT EDIT */ export const bodyAttributes = { + "data-faq": "closed", + "data-mobile": "false", + "data-number": "1", + "data-scope": "off", "data-theme": "light", "data-theme-2": "light", - "data-theme-three": "light" + "data-theme-three": "light", + "data-toggle-boolean": "true", + "data-toggle-function": "white", + "data-use-effect-theme": "light" }; diff --git a/packages/core/__tests__/fixtures/vite/src/App.css b/packages/core/__tests__/fixtures/vite/src/App.css index c462dde..4a3a51c 100644 --- a/packages/core/__tests__/fixtures/vite/src/App.css +++ b/packages/core/__tests__/fixtures/vite/src/App.css @@ -4,3 +4,7 @@ --color-primary: #000000; --color-secondary: #ffffff; } + +body { + @apply bg-primary text-secondary; +} diff --git a/packages/core/__tests__/fixtures/vite/src/App.tsx b/packages/core/__tests__/fixtures/vite/src/App.tsx index 94491ee..180fc03 100644 --- a/packages/core/__tests__/fixtures/vite/src/App.tsx +++ b/packages/core/__tests__/fixtures/vite/src/App.tsx @@ -1,64 +1,213 @@ 'use client'; -import useUI from '@react-zero-ui/core'; +import { useUI } from '@react-zero-ui/core'; +import UseEffectComponent from './UseEffectComponent'; +import FAQ from './FAQ'; import './App.css'; -const App: React.FC = () => { +export default function App() { const [, setTheme] = useUI<'light' | 'dark'>('theme', 'light'); const [, setTheme2] = useUI<'light' | 'dark'>('theme-2', 'light'); - const [, setThemeThree] = useUI<'light' | 'dark'>('themeThree', 'light'); + const [, setThemeThree] = useUI<'light' | 'dark'>('theme-three', 'light'); + const [, setToggle] = useUI<'true' | 'false'>('toggle-boolean', 'true'); + const [, setNumber] = useUI<'1' | '2'>('number', '1'); + const [, setOpen] = useUI<'open' | 'closed'>('faq', 'closed'); // Same key everywhere! + const [, setScope] = useUI<'off' | 'on'>('scope', 'off'); + const [, setMobile] = useUI<'true' | 'false'>('mobile', 'false'); + + const [, setToggleFunction] = useUI<'white' | 'black'>('toggle-function', 'white'); + + const toggleFunction = () => { + setToggleFunction((prev) => (prev === 'white' ? 'black' : 'white')); + }; return ( - <> -
      - -
      - Theme: Dark Light +
      +

      Global State

      +
      +
      + {/* Auto Theme Component */} + + +
      + +
      + +
      + Theme: Dark Light +
      -
      -
      +
      -
      - -
      - Theme: Dark Light +
      + +
      + Theme: Dark Light +
      -
      +
      + +
      + +
      + Theme: Dark Light +
      +
      + +
      + +
      + +
      + Boolean: True False +
      +
      +
      + +
      + +
      + Number: 1 2 +
      +
      +
      + +
      + +
      + Function: White Black +
      +
      +
      +
      +

      Scoped Style Tests


      -
      +
      +
      + +
      + Scope: False + True +
      +
      +
      + +
      + +
      + Mobile: False + True +
      +
      +
      + +
      -
      - Theme: Dark Light -
      +
      answer
      - - ); -}; -export default App; + + + +
      + ); +} diff --git a/packages/core/__tests__/fixtures/vite/src/FAQ.tsx b/packages/core/__tests__/fixtures/vite/src/FAQ.tsx new file mode 100644 index 0000000..c14fb9e --- /dev/null +++ b/packages/core/__tests__/fixtures/vite/src/FAQ.tsx @@ -0,0 +1,25 @@ +import { useUI } from '@react-zero-ui/core'; + +function FAQ({ question, answer, index }: { question: string; answer: string; index: number }) { + const [, setOpen] = useUI<'open' | 'closed'>('faq', 'closed'); // Same key everywhere! + + return ( +
      + +
      + {answer} +
      +
      + ); +} + +export default FAQ; diff --git a/packages/core/__tests__/fixtures/vite/src/UseEffectComponent.tsx b/packages/core/__tests__/fixtures/vite/src/UseEffectComponent.tsx new file mode 100644 index 0000000..2df0e9a --- /dev/null +++ b/packages/core/__tests__/fixtures/vite/src/UseEffectComponent.tsx @@ -0,0 +1,29 @@ +'use client'; +import { useEffect, useState } from 'react'; +import { useUI } from '@react-zero-ui/core'; + +// Component that automatically cycles through themes using useEffect +export default function UseEffectComponent() { + const [, setAutoTheme] = useUI<'light' | 'dark'>('use-effect-theme', 'light'); + const [state, setState] = useState(false); + + useEffect(() => { + setAutoTheme(state ? 'dark' : 'light'); + }, [state]); + + return ( +
      + +
      + Theme: Dark Light +
      +
      + ); +} diff --git a/packages/core/__tests__/fixtures/vite/tsconfig.app.json b/packages/core/__tests__/fixtures/vite/tsconfig.app.json index 5b7f80a..62e17a1 100644 --- a/packages/core/__tests__/fixtures/vite/tsconfig.app.json +++ b/packages/core/__tests__/fixtures/vite/tsconfig.app.json @@ -17,7 +17,6 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, diff --git a/packages/core/__tests__/fixtures/vite/tsconfig.node.json b/packages/core/__tests__/fixtures/vite/tsconfig.node.json index 042221e..524789d 100644 --- a/packages/core/__tests__/fixtures/vite/tsconfig.node.json +++ b/packages/core/__tests__/fixtures/vite/tsconfig.node.json @@ -15,7 +15,6 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, diff --git a/packages/core/__tests__/fixtures/vite/vite.config.ts b/packages/core/__tests__/fixtures/vite/vite.config.ts index 3b300ec..1d610da 100644 --- a/packages/core/__tests__/fixtures/vite/vite.config.ts +++ b/packages/core/__tests__/fixtures/vite/vite.config.ts @@ -4,5 +4,5 @@ import react from '@vitejs/plugin-react'; // https://vite.dev/config/ export default defineConfig({ - plugins: [react(), zeroUI()] + plugins: [zeroUI(), react()] }); \ No newline at end of file diff --git a/packages/core/__tests__/helpers/loadCli.js b/packages/core/__tests__/helpers/loadCli.js index 6bc3db6..6e130ad 100644 --- a/packages/core/__tests__/helpers/loadCli.js +++ b/packages/core/__tests__/helpers/loadCli.js @@ -4,7 +4,7 @@ import path from 'node:path'; export async function loadCliFromFixture(fixtureDir) { const r = createRequire(path.join(fixtureDir, 'package.json')); - const modulePath = r.resolve('../../../src/cli/init.cjs'); // get the path + const modulePath = r.resolve('../../../dist/cli/init.cjs'); // get the path const mod = r(modulePath); // actually require the module console.log('[Global Setup] Loaded CLI from fixture:', modulePath); // Return a wrapper function that changes directory before running CLI diff --git a/packages/core/__tests__/helpers/overwriteFile.js b/packages/core/__tests__/helpers/overwriteFile.js index 73bbfff..152fd4c 100644 --- a/packages/core/__tests__/helpers/overwriteFile.js +++ b/packages/core/__tests__/helpers/overwriteFile.js @@ -15,6 +15,6 @@ export async function overwriteFile(filePath, content) { fs.writeFileSync(filePath, content); console.log(`[Reset] ✅ Overwrote: ${filePath}`); - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); console.log(`[Reset] ✅ Wait complete, continuing...`); } diff --git a/packages/core/__tests__/unit/ast.test.cjs b/packages/core/__tests__/unit/ast.test.cjs new file mode 100644 index 0000000..8cde437 --- /dev/null +++ b/packages/core/__tests__/unit/ast.test.cjs @@ -0,0 +1,243 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { performance } = require('node:perf_hooks'); +const { findAllSourceFiles } = require('../../dist/postcss/helpers.cjs'); +const { collectUseUISetters, extractVariants } = require('../../dist/postcss/ast-parsing.cjs'); + +const ComponentImports = readFile(path.join(__dirname, './fixtures/test-components.jsx')); +const AllPatternsComponent = readFile(path.join(__dirname, './fixtures/ts-test-components.tsx')); + +const { parse } = require('@babel/parser'); + +// a helper to read a file and return the content +function readFile(path) { + return fs.readFileSync(path, 'utf-8'); +} +// Helper to create temp directory and run test +async function runTest(files, callback) { + const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-ui-test-ast')); + const originalCwd = process.cwd(); + + try { + process.chdir(testDir); + + // Create test files + for (const [filePath, content] of Object.entries(files)) { + const dir = path.dirname(filePath); + if (dir !== '.') { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(filePath, content); + } + + // Run assertions + await callback(); + } finally { + process.chdir(originalCwd); + + // Clean up any generated files in the package directory + fs.rmSync(testDir, { recursive: true, force: true }); + } +} + +test('findAllSourceFiles', async () => { + await runTest( + { + 'src/components/Button.tsx': ` + import { Button } from '@zero-ui/core'; + export default Button; + `, + 'src/components/Button.jsx': ` + import { Button } from '@zero-ui/core'; + export default Button; + `, + }, + async () => { + const sourceFiles = findAllSourceFiles(); + assert.ok(sourceFiles.length > 0); + } + ); +}); + +test('collectUseUISetters - basic functionality', async () => { + const sourceCode = ` + import { useUI } from '@react-zero-ui/core'; + + const Component = () => { + const [theme, setTheme] = useUI('theme', 'light'); + const [size, setSize] = useUI('size', 'medium'); + return <> + + +
      + ); +} +`, + }, + + async () => { + const variants = extractVariants('src/app/Component.jsx'); + assert.strictEqual(variants.length, 2); + assert.strictEqual(variants.length, 2); + + assert.ok(variants.some((v) => v.key === 'theme' && ['light', 'dark', 'blue', 'purple'].every((c) => v.values.includes(c)))); + + assert.ok(variants.some((v) => v.key === 'size' && ['medium', 'large'].every((c) => v.values.includes(c)))); + } + ); +}); + +test('Extract Variant with imports and throw error', async () => { + await runTest({ 'src/app/Component.jsx': ComponentImports }, async () => { + assert.throws(() => { + collectUseUISetters(parse(ComponentImports, { sourceType: 'module', plugins: ['jsx', 'typescript'] }), ComponentImports); + // tests that the error message contains the correct text + }, /const VARSLocal = VARS/); + }); +}); + +test('testKeyInitialValue', async () => { + await runTest({ 'src/app/Component.jsx': AllPatternsComponent }, async () => { + const setters = collectUseUISetters(parse(AllPatternsComponent, { sourceType: 'module', plugins: ['jsx', 'typescript'] }), AllPatternsComponent); + assert.strictEqual(setters[0].stateKey, 'theme'); + assert.strictEqual(setters[0].initialValue, 'light'); + assert.strictEqual(setters[1].stateKey, 'altTheme'); + assert.strictEqual(setters[1].initialValue, 'dark'); + assert.strictEqual(setters[2].stateKey, 'variant'); + assert.strictEqual(setters[2].initialValue, 'th-dark'); + assert.strictEqual(setters[3].stateKey, 'size'); + assert.strictEqual(setters[3].initialValue, 'lg'); + assert.strictEqual(setters[4].stateKey, 'mode'); + assert.strictEqual(setters[4].initialValue, 'auto'); + assert.strictEqual(setters[5].stateKey, 'color'); + assert.strictEqual(setters[5].initialValue, 'bg-blue'); + assert.strictEqual(setters[6].initialValue, 'th-dark'); + assert.strictEqual(setters[7].initialValue, 'th-blue'); + assert.strictEqual(setters[8].initialValue, 'th-blue-inverse'); + assert.strictEqual(setters[9].initialValue, 'blue-inverse'); + assert.strictEqual(setters[10].initialValue, 'blue-th-dark'); + assert.strictEqual(setters[11].initialValue, 'th-blue-th-dark'); + assert.strictEqual(setters[12].initialValue, 'th-light'); + assert.strictEqual(setters[13].initialValue, 'blue'); + }); +}); + +test('conditional setterFn value', async () => { + await runTest( + { + 'src/app/Component.jsx': ` +const COLORS = { primary: 'blue', secondary: 'green' } as const; +const VARIANTS = { dark: \`th-\${DARK}\`, light: COLORS.primary } as const; + +const isMobile = false; + +function TestComponent() { + const [theme, setTheme] = useUI('theme', 'light'); + const [variant, setVariant] = useUI('variant', 'th-light'); + const [variant2, setVariant2] = useUI('variant2', 'th-light'); + + + setTheme(prev => (prev === 'light' ? 'dark' : 'light')); + setVariant(isMobile ? VARIANTS.light : 'th-light'); + setVariant2(isMobile ? VARIANTS?.light : 'th-light'); +} +`, + }, + async () => { + const variants = extractVariants('src/app/Component.jsx'); + assert.strictEqual(variants.length, 3); + } + ); +}); + +test('cache performance', async () => { + // Simple inline component with a few useUI hooks, no conflicting constants + const simpleComponent = ` + import { useUI } from '@zero-ui/core'; + const DARK = 'dark' as const; + const PREFIX = \`th-\${DARK}\` as const; + const SIZES = { small: 'sm', large: 'lg' } as const; + const MODES = ['auto', 'manual'] as const; + + const COLORS = { primary: 'blue', secondary: 'green' } as const; + const VARIANTS = { dark: \`th-\${DARK}\`, light: COLORS.primary } as const; + function TestComponent() { + /* ① literal */ + const [theme, setTheme] = useUI('theme', 'light'); + /* ② identifier */ + const [altTheme, setAltTheme] = useUI('altTheme', DARK); + /* ③ static template literal */ + const [variant, setVariant] = useUI('variant', PREFIX); + /* ④ object-member */ + const [size, setSize] = useUI('size', SIZES.large); + /* ⑤ array-index */ + const [mode, setMode] = useUI('mode', MODES[0]); + /* ⑥ nested template + member */ + const [color, setColor] = useUI('color', \`bg-\${COLORS.primary}\`); + /* ⑦ object-member */ + const [variant2, setVariant2] = useUI('variant', VARIANTS.dark); + + + + return
      setTheme('dark')}>Test
      ; + } + `; + + await runTest({ 'src/Component.jsx': simpleComponent }, async () => { + const filePath = 'src/Component.jsx'; + + console.log('=== FIRST CALL ==='); + const start1 = performance.now(); + const result1 = extractVariants(filePath); + const firstCall = performance.now() - start1; + + console.log('=== SECOND CALL ==='); + const start2 = performance.now(); + const result2 = extractVariants(filePath); + const secondCall = performance.now() - start2; + + console.log('=== THIRD CALL ==='); + const start3 = performance.now(); + const result3 = extractVariants(filePath); + const thirdCall = performance.now() - start3; + + console.log(`First call: ${firstCall.toFixed(2)}ms`); + console.log(`Second call: ${secondCall.toFixed(2)}ms`); + console.log(`Third call: ${thirdCall.toFixed(2)}ms`); + + if (secondCall < firstCall) { + console.log(`Speedup: ${(firstCall / secondCall).toFixed(1)}x faster`); + } + + assert.strictEqual(result1, result2); + assert.strictEqual(result2, result3); + }); +}); diff --git a/packages/core/__tests__/unit/cli.test.cjs b/packages/core/__tests__/unit/cli.test.cjs index 4b3e142..24d7912 100644 --- a/packages/core/__tests__/unit/cli.test.cjs +++ b/packages/core/__tests__/unit/cli.test.cjs @@ -21,7 +21,7 @@ function cleanupTestDir(testDir) { console.warn(`Warning: Could not clean up test directory ${testDir}: ${error.message}`); try { // Try to remove files individually - const cleanup = dir => { + const cleanup = (dir) => { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); @@ -63,11 +63,11 @@ function runCLIScript(targetDir, timeout = 30000) { let stdout = ''; let stderr = ''; - child.stdout.on('data', data => { + child.stdout.on('data', (data) => { stdout += data.toString(); }); - child.stderr.on('data', data => { + child.stderr.on('data', (data) => { stderr += data.toString(); }); @@ -76,12 +76,12 @@ function runCLIScript(targetDir, timeout = 30000) { reject(new Error(`CLI script timed out after ${timeout}ms`)); }, timeout); - child.on('close', code => { + child.on('close', (code) => { clearTimeout(timer); resolve({ code, stdout, stderr, success: code === 0 }); }); - child.on('error', error => { + child.on('error', (error) => { clearTimeout(timer); reject(error); }); @@ -127,7 +127,7 @@ test('CLI script uses existing package.json if it exists', async () => { fs.writeFileSync(packageJsonPath, JSON.stringify(customPackageJson, null, 2)); // Run CLI (this will timeout on npm install, but that's ok for this test) - await runCLIScript(testDir, 5000).catch(err => { + await runCLIScript(testDir, 5000).catch((err) => { // We expect this to timeout/fail during npm install console.log('CLI run resulted in:', err.message); return { timedOut: true }; @@ -192,7 +192,7 @@ test('CLI script installs correct dependencies', async () => { try { // Run CLI script - await runCLIScript(testDir, 10000).catch(err => { + await runCLIScript(testDir, 10000).catch((err) => { console.log('CLI run resulted in:', err.message); return { error: err.message }; }); @@ -247,12 +247,12 @@ test('CLI script handles different target directories', async () => { resolve({ timedOut: true }); }, 5000); - child.on('close', code => { + child.on('close', (code) => { clearTimeout(timer); resolve({ code }); }); - child.on('error', error => { + child.on('error', (error) => { clearTimeout(timer); reject(error); }); @@ -274,7 +274,7 @@ test('Library CLI initializes project correctly', async () => { try { // Create a test React component with useUI hook - const componentDir = path.join(testDir, 'src', 'components'); + const componentDir = path.join(testDir, 'dist', 'components'); fs.mkdirSync(componentDir, { recursive: true }); const testComponent = ` @@ -296,7 +296,7 @@ export function TestComponent() { fs.writeFileSync(path.join(componentDir, 'TestComponent.jsx'), testComponent); // Import and run the library CLI directly - const { runZeroUiInit } = require('../../src/cli/postInstall.cjs'); + const { runZeroUiInit } = require('../../dist/cli/postInstall.cjs'); // Mock console to capture output const originalConsoleLog = console.log; @@ -348,13 +348,13 @@ test('Library CLI handles errors gracefully', async () => { // Mock process.exit to prevent actual exit const originalExit = process.exit; let exitCalled = false; - process.exit = code => { + process.exit = (code) => { exitCalled = true; throw new Error(`Process exit called with code ${code}`); }; try { - const { runZeroUiInit } = require('../../src/cli/postInstall.cjs'); + const { runZeroUiInit } = require('../../dist/cli/postInstall.cjs'); // This should complete without errors in most cases await runZeroUiInit(); @@ -413,12 +413,12 @@ fi resolve({ timedOut: true }); }, 5000); - child.on('close', code => { + child.on('close', (code) => { clearTimeout(timer); resolve({ code }); }); - child.on('error', error => { + child.on('error', (error) => { clearTimeout(timer); reject(error); }); @@ -445,11 +445,11 @@ test('CLI script handles invalid target directory', async () => { // Try to run CLI with a non-existent directory const binScript = path.resolve(__dirname, '../../../cli/bin.js'); - const result = await new Promise(resolve => { + const result = await new Promise((resolve) => { const child = spawn('node', [binScript, 'non-existent-dir'], { cwd: testDir, stdio: ['pipe', 'pipe', 'pipe'] }); let stderr = ''; - child.stderr.on('data', data => { + child.stderr.on('data', (data) => { stderr += data.toString(); }); @@ -458,12 +458,12 @@ test('CLI script handles invalid target directory', async () => { resolve({ timedOut: true, stderr }); }, 5000); - child.on('close', code => { + child.on('close', (code) => { clearTimeout(timer); resolve({ code, stderr }); }); - child.on('error', error => { + child.on('error', (error) => { clearTimeout(timer); resolve({ error: error.message }); }); @@ -484,7 +484,7 @@ test('Library CLI processes useUI hooks correctly', async () => { try { // Create multiple test components with useUI hooks - const componentsDir = path.join(testDir, 'src', 'components'); + const componentsDir = path.join(testDir, 'dist', 'components'); fs.mkdirSync(componentsDir, { recursive: true }); const component1 = ` @@ -523,7 +523,7 @@ export function Toggle() { fs.writeFileSync(path.join(componentsDir, 'Toggle.jsx'), component2); // Import and run the library CLI - const { runZeroUiInit } = require('../../src/cli/postInstall.cjs'); + const { runZeroUiInit } = require('../../dist/cli/postInstall.cjs'); const originalConsoleLog = console.log; const logMessages = []; diff --git a/packages/core/__tests__/unit/fixtures/test-components.jsx b/packages/core/__tests__/unit/fixtures/test-components.jsx new file mode 100644 index 0000000..eb812e1 --- /dev/null +++ b/packages/core/__tests__/unit/fixtures/test-components.jsx @@ -0,0 +1,14 @@ +/* eslint-disable import/no-unresolved */ +import { THEME, MENU_SIZES, VARS } from './variables'; + +export function ComponentImports() { + const [, setTheme] = useUI(VARS, THEME); + const [, setSize] = useUI('size', MENU_SIZES.medium); + + return ( +
      + + +
      + ); +} diff --git a/packages/core/__tests__/unit/fixtures/ts-test-components.tsx b/packages/core/__tests__/unit/fixtures/ts-test-components.tsx new file mode 100644 index 0000000..290116e --- /dev/null +++ b/packages/core/__tests__/unit/fixtures/ts-test-components.tsx @@ -0,0 +1,88 @@ +/* eslint-disable import/no-unresolved */ + +// @ts-ignore +import { useUI } from '@zero-ui/core'; + +/*───────────────────────────────────────────┐ +│ Top-level local constants (legal sources) │ +└───────────────────────────────────────────*/ +const DARK = 'dark' as const; +const PREFIX = `th-${DARK}` as const; +const SIZES = { small: 'sm', large: 'lg' } as const; +const MODES = ['auto', 'manual'] as const; + +const COLORS = { primary: 'blue', secondary: 'green' } as const; +const VARIANTS = { dark: `th-${DARK}`, light: COLORS.primary } as const; + +const isMobile = false; + +/*───────────────────────────────────────────┐ +│ Component covering every legal pattern │ +└───────────────────────────────────────────*/ +export function AllPatternsComponent() { + /* ① literal */ + const [theme, setTheme] = useUI('theme', 'light'); + /* ② identifier */ + const [altTheme, setAltTheme] = useUI('altTheme', DARK); + /* ③ static template literal */ + const [variant, setVariant] = useUI('variant', PREFIX); + /* ④ object-member */ + const [size, setSize] = useUI('size', SIZES.large); + /* ⑤ array-index */ + const [mode, setMode] = useUI('mode', MODES[0]); + /* ⑥ nested template + member */ + const [color, setColor] = useUI('color', `bg-${COLORS.primary}`); + /* ⑦ object-member */ + const [variant2, setVariant2] = useUI('variant', VARIANTS.dark); + /* ⑧ nested template + member */ + const [variant3, setVariant3] = useUI('variant', `th-${VARIANTS.light}`); + /* ⑨ BinaryExpression (template + member) */ + const [variant4, setVariant4] = useUI('variant', `th-${VARIANTS.light + '-inverse'}`); + /* ⑩ BinaryExpression (member + template) */ + const [variant5, setVariant5] = useUI('variant', `${VARIANTS.light}-inverse`); + /* ⑪ BinaryExpression (member + member) */ + const [variant6, setVariant6] = useUI('variant', `${VARIANTS.light}-${VARIANTS.dark}`); + /* ⑫ BinaryExpression (template + member) */ + const [variant7, setVariant7] = useUI('variant', `th-${VARIANTS.light}-${VARIANTS.dark}`); + /* ⑬ Optional-chaining w/ unresolvable member should pick th-light */ + // @ts-ignore + const [variant8, setVariant8] = useUI('variant', VARIANTS?.light.d ?? 'th-light'); + /* ⑭ nullish-coalesce */ + const [variant9, setVariant9] = useUI('variant', VARIANTS.light ?? 'th-light'); + + /* ── setters exercised in every allowed style ── */ + const clickHandler = () => { + /* direct literals */ + setTheme('dark'); + + /* identifier */ + setSize(SIZES.small); + + /* template literal */ + setVariant(`th-${DARK}-inverse`); + + /* member expression */ + setColor(COLORS.secondary); + + /* array index */ + setMode(MODES[1]); + + setVariant9(isMobile ? VARIANTS.light : 'th-light'); + }; + + /* conditional toggle with prev-state */ + const toggleAlt = () => setAltTheme((prev: string) => (prev === 'dark' ? 'light' : 'dark')); + + /* logical expression setter */ + useEffect(() => { + mode === 'auto' && setMode(MODES[1]); + }, [mode]); + + return ( +
      + + +
      {JSON.stringify({ theme, altTheme, variant, size, mode, color }, null, 2)}
      +
      + ); +} diff --git a/packages/core/__tests__/unit/fixtures/variables.js b/packages/core/__tests__/unit/fixtures/variables.js new file mode 100644 index 0000000..b87369d --- /dev/null +++ b/packages/core/__tests__/unit/fixtures/variables.js @@ -0,0 +1,5 @@ +export const THEME = 'dark'; + +export const MENU_SIZES = { small: 'sm', medium: 'md', large: 'lg' }; + +export const VARS = 'theme'; diff --git a/packages/core/__tests__/unit/index.test.cjs b/packages/core/__tests__/unit/index.test.cjs index efeb47c..945819a 100644 --- a/packages/core/__tests__/unit/index.test.cjs +++ b/packages/core/__tests__/unit/index.test.cjs @@ -4,8 +4,10 @@ const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); -const plugin = require('../../src/postcss/index.cjs'); -const { patchConfigAlias, toKebabCase, patchPostcssConfig, patchViteConfig } = require('../../src/postcss/helpers.cjs'); +// This file is the entry point for the react-zero-ui library, that uses postcss to trigger the build process +const plugin = require('../../dist/postcss/index.cjs'); + +const { patchTsConfig, toKebabCase, patchPostcssConfig, patchViteConfig } = require('../../dist/postcss/helpers.cjs'); function getAttrFile() { return path.join(process.cwd(), '.zero-ui', 'attributes.js'); @@ -50,7 +52,7 @@ test('generates body attributes file correctly', async () => { await runTest( { 'app/test.jsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Component() { const [theme, setTheme] = useUI('theme', 'light'); @@ -59,14 +61,13 @@ test('generates body attributes file correctly', async () => { } `, }, - result => { + (result) => { // Check attributes file exists assert(fs.existsSync(getAttrFile()), 'Attributes file should exist'); // Read and parse attributes const content = fs.readFileSync(getAttrFile(), 'utf-8'); console.log('\n📄 Generated attributes file:'); - console.log(content); // Verify content assert(content.includes('export const bodyAttributes'), 'Should export bodyAttributes'); @@ -74,8 +75,8 @@ test('generates body attributes file correctly', async () => { assert(content.includes('"data-sidebar": "expanded"'), 'Should have sidebar attribute'); // Verify CSS variants - assert(result.css.includes('@variant theme-light'), 'Should have theme-light variant'); - assert(result.css.includes('@variant sidebar-expanded'), 'Should have sidebar-expanded variant'); + assert(result.css.includes('@custom-variant theme-light'), 'Should have theme-light variant'); + assert(result.css.includes('@custom-variant sidebar-expanded'), 'Should have sidebar-expanded variant'); } ); }); @@ -84,7 +85,7 @@ test('generates body attributes file correctly when kebab-case is used', async ( await runTest( { 'app/test.jsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Component() { const [theme, setTheme] = useUI('theme-secondary', 'light'); @@ -97,12 +98,11 @@ test('generates body attributes file correctly when kebab-case is used', async ( } `, }, - result => { + (result) => { // Check attributes file exists assert(fs.existsSync(getAttrFile()), 'Attributes file should exist'); const content = fs.readFileSync(getAttrFile(), 'utf-8'); - console.log('result.css: ', result.css); // Verify content assert(content.includes('export const bodyAttributes'), 'Should export bodyAttributes'); @@ -110,182 +110,8 @@ test('generates body attributes file correctly when kebab-case is used', async ( assert(content.includes('"data-sidebar-new": "expanded"'), 'Should have sidebar-new attribute'); // Verify CSS variants - assert(result.css.includes('@variant theme-secondary-light'), 'Should have theme-secondary-light variant'); - assert(result.css.includes('@variant sidebar-new-expanded'), 'Should have sidebar-new-expanded variant'); - } - ); -}); - -test('handles TypeScript generic types', async () => { - await runTest( - { - 'src/component.tsx': ` - import { useUI } from 'react-zero-ui'; - - function Component() { - const [status, setStatus] = useUI<'idle' | 'loading' | 'success' | 'error'>('status', 'idle'); - return
      Status: {status}
      ; - } - `, - }, - result => { - console.log('\n🔍 TypeScript Generic Test:'); - - // Check all variants were generated - const variants = ['idle', 'loading', 'success', 'error']; - variants.forEach(variant => { - assert(result.css.includes(`@variant status-${variant}`), `Should have status-${variant} variant`); - }); - - // Check attributes file - const content = fs.readFileSync(getAttrFile(), 'utf-8'); - console.log('Attributes:', content); - assert(content.includes('"data-status": "idle"'), 'Should use initial value'); - } - ); -}); - -test('detects JavaScript setValue calls', async () => { - await runTest( - { - 'src/modal.js': ` - import { useUI } from 'react-zero-ui'; - - function Modal() { - const [modal, setModal] = useUI('modal', 'closed'); - - return ( -
      - - - -
      - ); - } - `, - }, - result => { - console.log('\n🔍 JavaScript Detection Test:'); - - const states = ['closed', 'open', 'minimized', 'fullscreen']; - states.forEach(state => { - assert(result.css.includes(`@variant modal-${state}`), `Should detect modal-${state}`); - }); - - const content = fs.readFileSync(getAttrFile(), 'utf-8'); - console.log('Initial value:', content.match(/"data-modal": "[^"]+"/)[0]); - } - ); -}); - -test('handles boolean values', async () => { - await runTest( - { - 'app/toggle.tsx': ` - import { useUI } from 'react-zero-ui'; - - function Toggle() { - const [isOpen, setIsOpen] = useUI('drawer', false); - const [checked, setChecked] = useUI('checkbox', true); - - return ( - - ); - } - `, - }, - result => { - console.log('\n🔍 Boolean Values Test:'); - - assert(result.css.includes('@variant drawer-true'), 'Should have drawer-true'); - assert(result.css.includes('@variant drawer-false'), 'Should have drawer-false'); - assert(result.css.includes('@variant checkbox-true'), 'Should have checkbox-true'); - assert(result.css.includes('@variant checkbox-false'), 'Should have checkbox-false'); - - const content = fs.readFileSync(getAttrFile(), 'utf-8'); - console.log('Boolean attributes:', content); - } - ); -}); - -test('handles kebab-case conversion', async () => { - await runTest( - { - 'src/styles.jsx': ` - import { useUI } from 'react-zero-ui'; - - function StyledComponent() { - const [primaryColor, setPrimaryColor] = useUI('primaryColor', 'deepBlue'); - const [bgStyle, setBgStyle] = useUI('backgroundColor', 'lightGray'); - - return ( -
      - - -
      - ); - } - `, - }, - result => { - console.log('\n🔍 Kebab-case Test:'); - - // Check CSS has kebab-case - assert(result.css.includes('@variant primary-color-deep-blue'), 'Should convert to kebab-case'); - assert(result.css.includes('@variant primary-color-dark-red'), 'Should convert to kebab-case'); - assert(result.css.includes('@variant background-color-light-gray'), 'Should convert to kebab-case'); - assert(result.css.includes('@variant background-color-pale-yellow'), 'Should convert to kebab-case'); - - // Check attributes use kebab-case keys - const content = fs.readFileSync(getAttrFile(), 'utf-8'); - assert(content.includes('"data-primary-color"'), 'Attribute key should be kebab-case'); - assert(content.includes('"data-background-color"'), 'Attribute key should be kebab-case'); - console.log('Kebab-case attributes:', content); - } - ); -}); - -test('handles conditional expressions', async () => { - await runTest( - { - 'app/conditional.jsx': ` - import { useUI } from 'react-zero-ui'; - - function ConditionalComponent({ isActive, mode }) { - const [state, setState] = useUI('state', 'default'); - - return ( -
      - - - -
      - ); - } - `, - }, - result => { - console.log('\n🔍 Conditional Expressions Test:'); - - const expectedStates = ['default', 'active', 'inactive', 'night', 'day', 'fallback']; - expectedStates.forEach(state => { - assert(result.css.includes(`@variant state-${state}`), `Should detect state-${state}`); - }); + assert(result.css.includes('@custom-variant theme-secondary-light'), 'Should have theme-secondary-light variant'); + assert(result.css.includes('@custom-variant sidebar-new-expanded'), 'Should have sidebar-new-expanded variant'); } ); }); @@ -294,98 +120,73 @@ test('handles multiple files and deduplication', async () => { await runTest( { 'src/header.jsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Header() { const [theme, setTheme] = useUI('theme', 'light'); - return ; + return ; } `, 'src/footer.jsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Footer() { const [theme, setTheme] = useUI('theme', 'light'); - return ; + return
      + + + Footer +
      ; } `, 'app/sidebar.tsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Sidebar() { const [theme, setTheme] = useUI<'light' | 'dark' | 'auto'>('theme', 'light'); - return
      Sidebar
      ; + return
      + + Sidebar +
      ; } `, }, - result => { - console.log('\n🔍 Multiple Files Test: ', result.css); - + (result) => { // Should combine all theme values from all files const themeVariants = ['light', 'dark', 'blue', 'auto']; - themeVariants.forEach(variant => { - assert(result.css.includes(`@variant theme-${variant}`), `Should have theme-${variant}`); + themeVariants.forEach((variant) => { + assert(result.css.includes(`@custom-variant theme-${variant}`), `Should have theme-${variant}`); }); // Count occurrences - should be deduplicated - const lightCount = (result.css.match(/@variant theme-light/g) || []).length; + const lightCount = (result.css.match(/@custom-variant theme-light/g) || []).length; assert.equal(lightCount, 1, 'Should deduplicate variants'); } ); }); -test('handles parsing errors gracefully', async () => { - await runTest( - { +test('throws on invalid syntax', async () => { + await assert.rejects(async () => { + await runTest({ 'src/valid.jsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Valid() { const [state, setState] = useUI('valid', 'working'); return
      Valid
      ; } `, 'src/invalid.js': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Invalid() { const [state, setState] = useUI('test' 'missing-comma'); {{{ invalid syntax } `, - }, - result => { - console.log('\n🔍 Parse Error Test:'); - console.log('result: ', result.css); - // Should still process valid files - assert(result.css.includes('@variant valid-working'), 'Should process valid files'); - - // Should not crash on invalid files - assert(result.css.includes('AUTO-GENERATED'), 'Should complete processing'); - } - ); + }); + }, /Unexpected token, expected ","/); }); test('throws on empty string initial value', () => { assert.throws(() => toKebabCase('')); }); -test('valid edge cases: underscores + missing initial', async () => { - await runTest( - { - 'src/edge.jsx': ` - import { useUI } from 'react-zero-ui'; - function EdgeCases() { - const [noInitial] = useUI('noInitial_value'); - const [, setOnlySetter] = useUI('only_setter_key', 'yes'); - setOnlySetter('set_later'); - return
      Edge cases
      ; - } - `, - }, - result => { - console.log('result: ', result.css); - assert(result.css.includes('@variant only-setter-key-set-later')); - assert(!result.css.includes('@variant no-initial-value')); - } - ); -}); - test('watches for file changes', async () => { if (process.env.NODE_ENV === 'production') { console.log('Skipping watch test in production'); @@ -395,36 +196,36 @@ test('watches for file changes', async () => { await runTest( { 'src/initial.jsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Initial() { - const [state, setState] = useUI('watchTest', 'initial'); - return
      Initial
      ;e + const [state, setState] = useUI('watch-test', 'initial'); + return
      Initial
      ;e } `, }, - async result => { + async (result) => { // Initial state - assert(result.css.includes('@variant watch-test-initial')); + assert(result.css.includes('@custom-variant watch-test-initial')); // Add a new file fs.writeFileSync( 'src/new.jsx', ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function New() { - const [state, setState] = useUI('watchTest', 'initial'); - return ; + const [state, setState] = useUI('watch-test', 'initial'); + return ; } ` ); // Wait for file watcher to process - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); // Re-process to check if watcher picked up changes const result2 = await postcss([plugin()]).process('', { from: undefined }); - assert(result2.css.includes('@variant watch-test-updated'), 'Should detect new state'); + assert(result2.css.includes('@custom-variant watch-test-updated'), 'Should detect new state'); } ); }); @@ -433,99 +234,44 @@ test('ignores node_modules and hidden directories', async () => { await runTest( { 'src/valid.jsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Valid() { - const [state] = useUI('valid', 'yes'); + const [state, setState] = useUI('valid', 'yes'); return
      Valid
      ; } `, 'node_modules/package/file.jsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Ignored() { const [state] = useUI('ignored', 'shouldNotAppear'); return
      Should be ignored
      ; } `, '.next/file.jsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Hidden() { const [state] = useUI('hidden', 'shouldNotAppear'); return
      Should be ignored
      ; } `, }, - result => { - console.log('result: ', result.css); - assert(result.css.includes('@variant valid-yes'), 'Should process valid files'); + (result) => { + assert(result.css.includes('@custom-variant valid-yes'), 'Should process valid files'); assert(!result.css.includes('ignored'), 'Should ignore node_modules'); assert(!result.css.includes('hidden'), 'Should ignore hidden directories'); } ); }); -test('handles deeply nested file structures', async () => { - await runTest( - { - 'src/features/auth/components/login/LoginForm.jsx': ` - import { useUI } from 'react-zero-ui'; - function LoginForm() { - const [authState, setAuthState] = useUI('authState', 'loggedOut'); - return ; - } - `, - }, - result => { - assert(result.css.includes('@variant auth-state-logged-out')); - assert(result.css.includes('@variant auth-state-logged-in')); - } - ); -}); - -test('handles complex TypeScript scenarios', async () => { - await runTest( - { - 'src/complex.tsx': ` - import { useUI } from 'react-zero-ui'; - - type Status = 'idle' | 'loading' | 'success' | 'error'; - - function Complex() { - // Type reference - const [status] = useUI('status', 'idle'); - - // Inline boolean - const [open] = useUI('modal', false); - - // String literal union with many values - const [size] = useUI<'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'>('size', 'md'); - - return
      Complex types
      ; - } - `, - }, - result => { - // Should extract all size variants - ['xs', 'sm', 'md', 'lg', 'xl', '2xl'].forEach(size => { - assert(result.css.includes(`@variant size-${size}`), `Should have size-${size}`); - }); - // Check attributes file - const content = fs.readFileSync(getAttrFile(), 'utf-8'); - assert(content.includes('"data-size": "md"'), 'Should have size-md'); - assert(content.includes('"data-status": "idle"'), 'Should have status-idle'); - assert(content.includes('"data-modal": "false"'), 'Should have modal-false'); - } - ); -}); - -test('handles large projects efficiently', async function () { +test('handles large projects efficiently - 500 files', async function () { const files = {}; // Generate 50 files - for (let i = 0; i < 50; i++) { + for (let i = 0; i < 500; i++) { files[`src/component${i}.jsx`] = ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Component${i}() { - const [state${i}] = useUI('state${i}', 'value${i}'); + const [state${i}, setState${i}] = useUI('state${i}', 'value${i}'); return
      Component ${i}
      ; } `; @@ -533,53 +279,28 @@ test('handles large projects efficiently', async function () { const startTime = Date.now(); - await runTest(files, result => { + await runTest(files, (result) => { const endTime = Date.now(); const duration = endTime - startTime; - console.log(`\n⚡ Performance: Processed 50 files in ${duration}ms`); + console.log(`\n⚡ Performance: Processed 500 files in ${duration}ms`); // Should process all files - assert(result.css.includes('@variant state49-value49'), 'Should process all files'); + assert(result.css.includes('@custom-variant state49-value49'), 'Should process all files'); // Should complete in reasonable time - assert(duration < 300, 'Should process 50 files in under 300ms'); + assert(duration < 500, 'Should process 500 files in under 500ms'); }); }); -test('handles special characters in values', async () => { - await runTest( - { - 'src/special.jsx': ` - import { useUI } from 'react-zero-ui'; - function Special() { - const [state, setState] = useUI('special', 'default'); - return ( -
      - - - -
      - ); - } - `, - }, - result => { - assert(result.css.includes('@variant special-with-dash')); - assert(result.css.includes('@variant special-with-underscore')); - assert(result.css.includes('@variant special-123numeric')); - } - ); -}); - test('handles concurrent file modifications', async () => { // Test that rapid changes don't cause issues await runTest( { 'src/rapid.jsx': ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Rapid() { - const [count] = useUI('count', 'zero'); + const [count,setCount] = useUI('count', 'zero'); return
      Initial
      ; } `, @@ -590,7 +311,7 @@ test('handles concurrent file modifications', async () => { fs.writeFileSync( 'src/rapid.jsx', ` - import { useUI } from 'react-zero-ui'; + import { useUI } from '@react-zero-ui/core'; function Rapid() { const [count, setCount] = useUI('count', 'zero'); return ; @@ -599,7 +320,7 @@ test('handles concurrent file modifications', async () => { ); // Small delay to simulate real editing - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)); } // Final processing should work correctly @@ -610,7 +331,7 @@ test('handles concurrent file modifications', async () => { ); }); -test('patchConfigAlias - config file patching', async t => { +test('patchTsConfig - config file patching', async (t) => { await t.test('patches tsconfig.json when it exists', async () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-ui-config-test')); const originalCwd = process.cwd(); @@ -622,8 +343,8 @@ test('patchConfigAlias - config file patching', async t => { const tsconfigContent = { compilerOptions: { target: 'ES2015', module: 'ESNext' } }; fs.writeFileSync('tsconfig.json', JSON.stringify(tsconfigContent, null, 2)); - // Run patchConfigAlias - patchConfigAlias(); + // Run patchTsConfig + patchTsConfig(); // Read the updated config const updatedConfig = JSON.parse(fs.readFileSync('tsconfig.json', 'utf-8')); @@ -651,8 +372,8 @@ test('patchConfigAlias - config file patching', async t => { const jsconfigContent = { compilerOptions: { target: 'ES2015' } }; fs.writeFileSync('jsconfig.json', JSON.stringify(jsconfigContent, null, 2)); - // Run patchConfigAlias - patchConfigAlias(); + // Run patchTsConfig + patchTsConfig(); // Read the updated config const updatedConfig = JSON.parse(fs.readFileSync('jsconfig.json', 'utf-8')); @@ -677,8 +398,8 @@ test('patchConfigAlias - config file patching', async t => { try { process.chdir(testDir); - // Run patchConfigAlias (should not throw) - patchConfigAlias(); + // Run patchTsConfig (should not throw) + patchTsConfig(); // Verify no files were created assert(!fs.existsSync('tsconfig.json'), 'Should not create tsconfig.json'); @@ -707,8 +428,8 @@ test('patchConfigAlias - config file patching', async t => { fs.writeFileSync('tsconfig.json', JSON.stringify(tsconfigContent, null, 2)); const originalContent = fs.readFileSync('tsconfig.json', 'utf-8'); - // Run patchConfigAlias - patchConfigAlias(); + // Run patchTsConfig + patchTsConfig(); // Verify the config was not modified const updatedContent = fs.readFileSync('tsconfig.json', 'utf-8'); @@ -730,8 +451,8 @@ test('patchConfigAlias - config file patching', async t => { const tsconfigContent = { include: ['src/**/*'] }; fs.writeFileSync('tsconfig.json', JSON.stringify(tsconfigContent, null, 2)); - // Run patchConfigAlias - patchConfigAlias(); + // Run patchTsConfig + patchTsConfig(); // Read the updated config const updatedConfig = JSON.parse(fs.readFileSync('tsconfig.json', 'utf-8')); @@ -768,8 +489,8 @@ test('patchConfigAlias - config file patching', async t => { }`; fs.writeFileSync('tsconfig.json', tsconfigContent); - // Run patchConfigAlias - patchConfigAlias(); + // Run patchTsConfig + patchTsConfig(); // Verify file was updated (should parse despite comments) const updatedConfig = JSON.parse(fs.readFileSync('tsconfig.json', 'utf-8')); @@ -784,7 +505,7 @@ test('patchConfigAlias - config file patching', async t => { } }); - await t.test('patchConfigAlias prefers tsconfig.json over jsconfig.json', async () => { + await t.test('patchTsConfig prefers tsconfig.json over jsconfig.json', async () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-ui-config-test')); const originalCwd = process.cwd(); @@ -795,8 +516,8 @@ test('patchConfigAlias - config file patching', async t => { fs.writeFileSync('tsconfig.json', JSON.stringify({ compilerOptions: {} }, null, 2)); fs.writeFileSync('jsconfig.json', JSON.stringify({ compilerOptions: {} }, null, 2)); - // Run patchConfigAlias - patchConfigAlias(); + // Run patchTsConfig + patchTsConfig(); // Verify tsconfig.json was modified const tsconfigContent = JSON.parse(fs.readFileSync('tsconfig.json', 'utf-8')); @@ -1388,37 +1109,6 @@ export default config`; } }); -test('Vite config - handles parse errors gracefully', async () => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-ui-vite-test')); - const originalCwd = process.cwd(); - - try { - process.chdir(testDir); - - // Create invalid Vite config with syntax errors - const invalidConfig = `import { defineConfig } from 'vite' -import tailwindcss from '@tailwindcss/vite' - -export default defineConfig({ - plugins: [ - tailwindcss( // missing closing parenthesis - ] - // missing closing brace`; - fs.writeFileSync('vite.config.ts', invalidConfig); - const originalContent = fs.readFileSync('vite.config.ts', 'utf-8'); - - // Run patchViteConfig (should not throw) - patchViteConfig(); - - // Verify config was not modified due to parse error - const updatedContent = fs.readFileSync('vite.config.ts', 'utf-8'); - assert.equal(originalContent, updatedContent, 'Should not modify config with parse errors'); - } finally { - process.chdir(originalCwd); - fs.rmSync(testDir, { recursive: true, force: true }); - } -}); - test('Vite config - handles config with no plugins array', async () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-ui-vite-test')); const originalCwd = process.cwd(); @@ -1484,3 +1174,164 @@ export default defineConfig({ fs.rmSync(testDir, { recursive: true, force: true }); } }); + +test('generated variants for initial value without setterFn', async () => { + await runTest( + { + 'app/initial-value.jsx': ` + import { useUI } from '@react-zero-ui/core'; + function Component() { + const [theme,setTheme] = useUI('theme', 'light'); + return
      Test
      ; + } + `, + }, + (result) => { + console.log('\n📄 Initial value without setterFn:'); + + assert(result.css.includes('@custom-variant theme-light')); + } + ); +}); +/* +The following tests are for advanced edge cases +-------------------------------------------------------------------------------------------- +---------------------------------------------- +---------------------------------------------- +---------------------------------------------- +---------------------------------------------- +---------------------------------------------- +-------------------------------------------------------------------------------------------- +---------------------------------------------- +---------------------------------------------- +---------------------------------------------- +---------------------------------------------- +---------------------------------------------- +---------------------------------------------- +*/ + +test.skip('handles all common setter patterns - full coverage sanity check - COMPLEX', async () => { + await runTest( + { + 'app/component.tsx': ` + import { useUI } from '@react-zero-ui/core'; + + const DARK = 'dark'; + const LIGHT = 'light'; + + function Component() { + const [theme, setTheme] = useUI<'light' | 'dark' | 'contrast' | 'neon' | 'retro'>('theme', 'light'); + const [size, setSize] = useUI<'sm' | 'lg'>('size', 'sm'); + + setTheme('dark'); // direct + setTheme(DARK); // identifier + setTheme(() => 'light'); // arrow fn + setTheme(prev => prev === 'light' ? 'dark' : 'light'); // updater + setTheme(prev => { if (a) return 'neon'; return 'retro'; }); // block + setTheme(userPref || 'contrast'); // logical fallback + setSize(SIZES.SMALL); // object constant + + return ( + <> + + + + ); + } + + const SIZES = { + SMALL: 'sm', + LARGE: 'lg' + }; + `, + }, + (result) => { + console.log('\n📄 Full coverage test:'); + + // ✅ things that MUST be included + assert(result.css.includes('@custom-variant theme-dark')); + assert(result.css.includes('@custom-variant theme-light')); + assert(result.css.includes('@custom-variant theme-contrast')); + assert(result.css.includes('@custom-variant theme-neon')); + assert(result.css.includes('@custom-variant theme-retro')); + assert(result.css.includes('@custom-variant size-sm')); + assert(result.css.includes('@custom-variant size-lg')); + + // ❌ known misses: test exposes what won't work without resolution + // assert(result.css.includes('@custom-variant theme-dynamic-from-e-target')); + } + ); +}); + +test('performance with large files and many variants', async () => { + // Generate a large file with many useUI calls + const generateLargeFile = () => { + let content = `import { useUI } from '@react-zero-ui/core';\n\n`; + + // Create many components with different state keys + for (let i = 0; i < 50; i++) { + const toggleInitial = i % 2 === 0 ? "'true'" : "'false'"; + content += ` + function Component${i}() { + const [state${i}, setState${i}] = useUI('state-${i}', 'initial-${i}'); + const [toggle${i}, setToggle${i}] = useUI('toggle-${i}', ${toggleInitial}); + + + + return
      Component ${i}
      ; + } + `; + } + + return content; + }; + + const startTime = Date.now(); + + await runTest({ 'app/large-file.jsx': generateLargeFile() }, (result) => { + const endTime = Date.now(); + const duration = endTime - startTime; + + console.log(`\n⏱️ Large file processing took: ${duration}ms`); + + // Should handle large files reasonably quickly (< 5 seconds) + assert(duration < 5000, `Processing took too long: ${duration}ms`); + + // Should still extract all variants correctly + assert(result.css.includes('@custom-variant state-0-initial-0')); + assert(result.css.includes('@custom-variant state-49-initial-49')); + assert(result.css.includes('@custom-variant toggle-0-true')); + assert(result.css.includes('@custom-variant toggle-0-false')); + }); +}); + +test('caching works correctly', async () => { + const testFiles = { + 'app/cached.jsx': ` + import { useUI } from '@react-zero-ui/core'; + + function Component() { + const [theme, setTheme] = useUI('theme', 'light'); + return
      Test
      ; + } + `, + }; + + // First run + const start1 = Date.now(); + await runTest(testFiles, () => {}); + const duration1 = Date.now() - start1; + + // Second run with same files (should be faster due to caching) + const start2 = Date.now(); + await runTest(testFiles, () => {}); + const duration2 = Date.now() - start2; + + console.log(`\n📊 First run: ${duration1}ms, Second run: ${duration2}ms`); + + // Note: This test might be flaky in CI, but useful for development + // Second run should generally be faster, but timing can vary +}); diff --git a/packages/core/package.json b/packages/core/package.json index b1ea194..1e770c4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -4,41 +4,45 @@ "description": "Zero re-render, global UI state management for React", "private": false, "type": "module", - "main": "./src/index.js", - "module": "./src/index.js", - "types": "./src/index.d.ts", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", "sideEffects": false, "files": [ - "src/**/*", + "dist/**/*", "README.md", - "LICENSE" + "LICENSE", + "!dist/postcss/coming-soon/**/*" ], "exports": { ".": { - "types": "./src/index.d.ts", - "import": "./src/index.js" + "types": "./dist/index.d.ts", + "import": "./dist/index.js" }, "./postcss": { - "types": "./src/postcss/index.d.ts", - "require": "./src/postcss/index.cjs" + "types": "./dist/postcss/index.d.ts", + "require": "./dist/postcss/index.cjs" }, "./vite": { - "types": "./src/postcss/vite.d.ts", - "import": "./src/postcss/vite.js" + "types": "./dist/postcss/vite.d.ts", + "import": "./dist/postcss/vite.js" }, "./cli": { - "types": "./src/cli/init.d.ts", - "require": "./src/cli/init.cjs", - "import": "./src/cli/init.cjs" + "types": "./dist/cli/init.d.ts", + "require": "./dist/cli/init.cjs", + "import": "./dist/cli/init.cjs" } }, "scripts": { + "prepack": "pnpm run build", + "build": "tsc -p tsconfig.build.json", + "dev": "tsc -p tsconfig.json --watch", "test:next": "playwright test -c __tests__/config/playwright.next.config.js", "test:vite": "playwright test -c __tests__/config/playwright.vite.config.js", - "test:unit": "node --test __tests__/unit/index.test.cjs", + "test:integration": "node --test __tests__/unit/index.test.cjs", "test:cli": "node --test __tests__/unit/cli.test.cjs", - "test:all": "pnpm run test:vite && pnpm run test:next && pnpm run test:unit && pnpm run test:cli", - "test": "pnpm run test:all" + "test": "pnpm run test:vite && pnpm run test:next && pnpm run test:unit && pnpm run test:cli && pnpm run test:integration", + "test:unit": "tsx --test src/**/*.test.ts" }, "keywords": [ "react", @@ -69,18 +73,26 @@ "node": ">=18.0.0" }, "peerDependencies": { + "@tailwindcss/postcss": "^4.1.10", "postcss": "^8.5.5", "react": ">=16.8.0", - "tailwindcss": "^4.1.10", - "@tailwindcss/postcss": "^4.1.10" + "tailwindcss": "^4.1.10" }, "dependencies": { "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.4" + "@babel/types": "^7.27.4", + "fast-glob": "^3.3.3" }, "devDependencies": { - "@playwright/test": "^1.53.0" + "@babel/code-frame": "^7.27.1", + "@playwright/test": "^1.53.0", + "@types/babel__code-frame": "^7.0.6", + "@types/babel__generator": "^7.27.0", + "@types/babel__traverse": "^7.20.7", + "@types/react": "^19.1.8", + "lru-cache": "^10.4.3", + "tsx": "^4.20.3" } -} +} \ No newline at end of file diff --git a/packages/core/src/cli/init.cjs b/packages/core/src/cli/init.cjs deleted file mode 100755 index c30cd23..0000000 --- a/packages/core/src/cli/init.cjs +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env node - -// src/cli/init.cjs - single source of truth - -//import the actual implementation from postInstall.cjs -const { runZeroUiInit } = require('./postInstall.cjs'); - -// Take command line arguments (defaulting to process.argv.slice(2) which are the args after node ) and pass them to runZeroUiInit -async function cli(argv = process.argv.slice(2)) { - return await runZeroUiInit(argv); -} - -/* -------- CL I -------- */ -if (require.main === module) { - cli().catch(error => { - console.error('CLI failed:', error); - process.exit(1); - }); -} - -/* -------- CJS -------- */ -module.exports = cli; // `require('@…/cli')()` - -/* -------- ESM -------- */ -module.exports.default = cli; // `import('@…/cli').then(m => m.default())` diff --git a/packages/core/src/cli/init.cts b/packages/core/src/cli/init.cts new file mode 100755 index 0000000..6acd8a0 --- /dev/null +++ b/packages/core/src/cli/init.cts @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +// src/cli/init.ts - single source of truth + +import { runZeroUiInit } from './postInstall.cjs'; + +async function cli() { + return await runZeroUiInit(); +} + +/* -------- CLI -------- */ +if (require.main === module) { + cli().catch((error) => { + console.error('CLI failed:', error); + process.exit(1); + }); +} + +/* -------- ES6 Export -------- */ +export default cli; diff --git a/packages/core/src/cli/init.d.ts b/packages/core/src/cli/init.d.ts deleted file mode 100644 index 1c835a2..0000000 --- a/packages/core/src/cli/init.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function cli(argv?: string[]): void; diff --git a/packages/core/src/cli/postInstall.cjs b/packages/core/src/cli/postInstall.cts similarity index 74% rename from packages/core/src/cli/postInstall.cjs rename to packages/core/src/cli/postInstall.cts index 29939a9..6e3b36e 100644 --- a/packages/core/src/cli/postInstall.cjs +++ b/packages/core/src/cli/postInstall.cts @@ -1,8 +1,8 @@ -// scripts/postInstall.cjs -const { processVariants, generateAttributesFile, patchConfigAlias, patchPostcssConfig, patchViteConfig, hasViteConfig } = require('../postcss/helpers.cjs'); -const { patchNextBodyTag } = require('../postcss/ast.cjs'); +// src/cli/postInstall.cts +import { patchNextBodyTag } from '../postcss/ast-generating.cjs'; +import { processVariants, generateAttributesFile, patchTsConfig, patchPostcssConfig, patchViteConfig, hasViteConfig } from '../postcss/helpers.cjs'; -async function runZeroUiInit() { +export async function runZeroUiInit() { try { console.log('[Zero-UI] Initializing...'); @@ -13,7 +13,7 @@ async function runZeroUiInit() { if (!hasViteConfig()) { // Patch config for module resolution - await patchConfigAlias(); + await patchTsConfig(); // Patch PostCSS config for Next.js projects await patchPostcssConfig(); } @@ -38,5 +38,3 @@ async function runZeroUiInit() { process.exit(1); } } - -module.exports = { runZeroUiInit }; diff --git a/packages/core/src/config.cjs b/packages/core/src/config.cjs deleted file mode 100644 index 656a05f..0000000 --- a/packages/core/src/config.cjs +++ /dev/null @@ -1,11 +0,0 @@ -const CONFIG = { - SUPPORTED_EXTENSIONS: { TYPESCRIPT: ['.ts', '.tsx'], JAVASCRIPT: ['.js', '.jsx'] }, - HOOK_NAME: 'useUI', - MIN_HOOK_ARGUMENTS: 2, - MAX_HOOK_ARGUMENTS: 2, - HEADER: '/* AUTO-GENERATED - DO NOT EDIT */', - ZERO_UI_DIR: '.zero-ui', -}; - -const IGNORE_DIRS = new Set(['node_modules', '.next', '.turbo', '.vercel', '.git', 'coverage', 'out', 'public', 'dist', 'build']); -module.exports = { CONFIG, IGNORE_DIRS }; diff --git a/packages/core/src/config.cts b/packages/core/src/config.cts new file mode 100644 index 0000000..8db7e6e --- /dev/null +++ b/packages/core/src/config.cts @@ -0,0 +1,25 @@ +export const CONFIG = { + SUPPORTED_EXTENSIONS: { TYPESCRIPT: ['.ts', '.tsx'], JAVASCRIPT: ['.js', '.jsx'] }, + HOOK_NAME: 'useUI', + IMPORT_NAME: '@react-zero-ui/core', + MIN_HOOK_ARGUMENTS: 2, + MAX_HOOK_ARGUMENTS: 2, + HEADER: '/* AUTO-GENERATED - DO NOT EDIT */', + ZERO_UI_DIR: '.zero-ui', + CONTENT: ['src/**/*.{ts,tsx,js,jsx}', 'app/**/*.{ts,tsx,js,jsx}', 'pages/**/*.{ts,tsx,js,jsx}'], + POSTCSS_PLUGIN: '@react-zero-ui/core/postcss', + VITE_PLUGIN: '@react-zero-ui/core/vite', +}; + +export const IGNORE_DIRS = [ + '**/node_modules/**', + '**/.next/**', + '**/.turbo/**', + '**/.vercel/**', + '**/.git/**', + '**/coverage/**', + '**/out/**', + '**/public/**', + '**/dist/**', + '**/build/**', +]; diff --git a/packages/core/src/index.d.ts b/packages/core/src/index.d.ts deleted file mode 100644 index 1884489..0000000 --- a/packages/core/src/index.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Returns [staleValue, setState] - destructure as `const [, setState] = useUI(...)` - * The first value is intentionally stale/static, use only the setter. - */ -declare function useUI( - key: string, - initialValue: T -): readonly [T, (v: T | ((currentValue: T) => T)) => void]; - -export { useUI }; -export default useUI; diff --git a/packages/core/src/index.js b/packages/core/src/index.js deleted file mode 100644 index 1e6bac0..0000000 --- a/packages/core/src/index.js +++ /dev/null @@ -1,38 +0,0 @@ -import { useCallback } from 'react'; - -function useUI(key, initialValue) { - //setValue(valueOrUpdater) - const setValue = useCallback( - valueOrUpdater => { - if (typeof window === 'undefined') return; - // Convert kebab-case to camelCase for dataset API - // "theme-secondary" -> "themeSecondary" - const camelKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); - let newValue; - if (typeof valueOrUpdater === 'function') { - const parse = v => { - if (!v) return initialValue; - switch (typeof initialValue) { - case 'boolean': - return v === 'true'; - case 'number': { - const n = Number(v); - return isNaN(n) ? initialValue : n; - } - default: - return v; - } - }; - newValue = valueOrUpdater(parse(document.body.dataset[camelKey])); - } else { - newValue = valueOrUpdater; - } - document.body.dataset[camelKey] = String(newValue); - }, - [key] - ); - return [initialValue, setValue]; -} - -export { useUI }; -export default useUI; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..2a66a8d --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,134 @@ +import { useCallback, useRef, type RefObject } from 'react'; + +interface GlobalThis { + __useUIRegistry?: Map; +} + +export interface UISetterFn { + (valueOrUpdater: T | ((currentValue: T) => T)): void; + ref?: RefObject | ((node: HTMLElement | null) => void); +} + +function useUI(key: string, initialValue: T): [T, UISetterFn] { + /* ─ DEV-ONLY COLLISION GUARD (removed in production by modern bundlers) ─ */ + if (process.env.NODE_ENV !== 'production') { + //validate key and initialValue make sure there are no spaces + if (key.includes(' ') || initialValue.includes(' ')) { + throw new Error(`[Zero-UI] useUI(key, initialValue); key and initialValue must not contain spaces, got "${key}" and "${initialValue}"`); + } + + // enforce kebab-case for the key: lowercase letters, digits and single dashes + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(key)) { + throw new Error( + `[Zero-UI] useUI(key, …); key must be kebab-case (e.g. "theme-dark"), got "${key}". ` + `Avoid camelCase or uppercase — they break variant generation.` + ); + } + + // Validate inputs with helpful error messages + if (!key || typeof key !== 'string' || key.trim() === '') { + throw new Error(`useUI(key, initialValue); key must be a non-empty string, got "${key}"`); + } + // force string type for initialValue + if (typeof initialValue !== 'string') { + throw new Error(`useUI(key, initialValue); initialValue must be or resolve to a string, got "${typeof initialValue}"`); + } + + if (initialValue === '' || initialValue == null) { + throw new Error(`useUI(key, initialValue); initialValue cannot be empty string, null, or undefined, got "${initialValue}"`); + } + // One shared registry per window / Node context + const registry = typeof globalThis !== 'undefined' ? ((globalThis as GlobalThis).__useUIRegistry ||= new Map()) : new Map(); + + const prev = registry.get(key); + // TODO try to add per page error boundaries + if (prev !== undefined && prev !== initialValue) { + console.error( + `[useUI] Inconsistent initial values for key "${key}": ` + + `expected "${prev}", got "${initialValue}". ` + + `Use the same initial value everywhere or namespace your keys.` + ); + } else if (prev === undefined) { + registry.set(key, initialValue); + } + } + /* ───────────────────────────────────────────────────────────── */ + + // Create a ref to hold the DOM element that will receive the data-* attributes + // This allows scoping UI state to specific elements instead of always using document.body + const scopeRef = useRef(null); + + /* ─ DEV-ONLY MULTIPLE REF GUARD (removed in production by modern bundlers) ─ */ + const refAttachCount = process.env.NODE_ENV !== 'production' ? useRef(0) : null; + /* ─────────────────────────────────────────────────────────────────────── */ + + // Convert kebab-case key to camelCase for dataset property access + // e.g., "my-key" becomes "myKey" since dataset auto-lowercases after dashes + const camelKey = key.replace(/-([a-z])/g, (_, l) => l.toUpperCase()); + + // Memoized setter function that updates data-* attributes on the target element + // useCallback prevents recreation on every render, essential for useEffect dependencies + const setValue: UISetterFn = useCallback( + (valueOrUpdater: T | ((currentValue: T) => T)) => { + // SSR safety: bail out if running on server where window is undefined + if (typeof window === 'undefined') return; + + // Use the scoped element if ref is attached, otherwise fall back to document.body + // This enables both scoped UI state (with ref) and global UI state (without ref) + const target = scopeRef.current ?? document.body; + + let newValue: T; + // Check if caller passed an updater function (like React's setState(prev => prev === 'true' ? 'false' : 'true') pattern) + if (typeof valueOrUpdater === 'function') { + const value = target.dataset[camelKey] as T; + + // Call the updater function with the parsed current value + newValue = valueOrUpdater(value); + } else { + // Direct value assignment (no updater function) + newValue = valueOrUpdater; + } + + // Write the new value to the data-* attribute + target.dataset[camelKey] = newValue; + }, + // Recreate callback only when the key changes + // Note: initialValue intentionally excluded since it should be stable + [key] // camelKey depends on key, so no need to include it in the dependency array + ); + + // -- DEV-ONLY MULTIPLE REF GUARD (removed in production by modern bundlers) -- + // Attach the ref to the setter function so users can write:
      + // This creates a clean API where the ref and setter are bundled together + if (process.env.NODE_ENV !== 'production') { + // DEV: Wrap scopeRef to detect multiple attachments + (setValue as UISetterFn).ref = useCallback( + (node: HTMLElement | null) => { + if (node) { + refAttachCount!.current++; + if (refAttachCount!.current > 1) { + // TODO add documentation link + throw new Error( + `[useUI] Multiple ref attachments detected for key "${key}". ` + + `Each useUI hook supports only one ref attachment per component. ` + + `Solution: Create separate component. and reuse.\n` + + `Example: instead of multiple refs in one component.` + ); + } + } else { + // Handle cleanup when ref is detached + refAttachCount!.current = Math.max(0, refAttachCount!.current - 1); + } + scopeRef.current = node; + }, + [key] + ); + } else { + // PROD: Direct ref assignment for zero overhead + setValue.ref = scopeRef; + } + + // Return tuple matching React's useState pattern: [initialValue, setter] + return [initialValue, setValue]; +} + +export { useUI }; diff --git a/packages/core/src/postcss/ast-generating.cts b/packages/core/src/postcss/ast-generating.cts new file mode 100644 index 0000000..78169e5 --- /dev/null +++ b/packages/core/src/postcss/ast-generating.cts @@ -0,0 +1,355 @@ +import { parse, parseExpression } from '@babel/parser'; +import * as babelTraverse from '@babel/traverse'; +import { NodePath } from '@babel/traverse'; +import * as t from '@babel/types'; +import { generate } from '@babel/generator'; +const traverse = (babelTraverse as any).default; +import * as fs from 'fs'; +import path from 'path'; + +/** + * Parse PostCSS config JavaScript file and add Zero-UI plugin if not present + * Uses Babel AST for robust parsing and modification + * Supports both CommonJS (.js) and ES Modules (.mjs) formats + * @param {string} source - The PostCSS config source code + * @param {string} zeroUiPlugin - The Zero-UI plugin name + * @param {boolean} isESModule - Whether the config is an ES module + * @returns {string | null} The modified config code or null if no changes were made + */ +export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string, isESModule: boolean = false): string | null { + try { + const ast = parse(source, { sourceType: 'module', plugins: ['jsonStrings'] }); + + let modified = false; + + // Check if Zero-UI plugin already exists + if (source.includes(zeroUiPlugin)) { + return source; // Already configured + } + + traverse(ast, { + // Handle CommonJS: module.exports = { ... } and exports = { ... } + AssignmentExpression(path: NodePath) { + const { left, right } = path.node; + + // Check for module.exports or exports assignment + const isModuleExports = + t.isMemberExpression(left) && t.isIdentifier(left.object, { name: 'module' }) && t.isIdentifier(left.property, { name: 'exports' }); + const isExportsAssignment = t.isIdentifier(left, { name: 'exports' }); + + if ((isModuleExports || isExportsAssignment) && t.isObjectExpression(right)) { + const pluginsProperty = right.properties.find( + (prop): prop is t.ObjectProperty => t.isObjectProperty(prop) && t.isIdentifier(prop.key, { name: 'plugins' }) + ); + + if (pluginsProperty && t.isExpression(pluginsProperty.value)) { + modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); + } + } + }, + + // Handle ES Modules: export default { ... } + ExportDefaultDeclaration(path: NodePath) { + if (isESModule && t.isObjectExpression(path.node.declaration)) { + const pluginsProperty = path.node.declaration.properties.find( + (prop): prop is t.ObjectProperty => t.isObjectProperty(prop) && t.isIdentifier(prop.key, { name: 'plugins' }) + ); + + if (pluginsProperty && t.isExpression(pluginsProperty.value)) { + modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); + } + } + }, + + // Handle: const config = { plugins: ... }; export default config + VariableDeclarator(path: NodePath) { + if (isESModule && path.node.init && t.isObjectExpression(path.node.init)) { + const pluginsProperty = path.node.init.properties.find( + (prop): prop is t.ObjectProperty => t.isObjectProperty(prop) && t.isIdentifier(prop.key, { name: 'plugins' }) + ); + + if (pluginsProperty && t.isExpression(pluginsProperty.value)) { + modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); + } + } + }, + }); + + if (modified) { + return generate(ast).code; + } else { + console.warn(`[Zero-UI] Failed to automatically modify PostCSS config: ${source}`); + return null; // Could not automatically modify + } + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : String(err); + console.warn(`[Zero-UI] Failed to parse PostCSS config: ${errorMessage}`); + return null; + } +} + +/** + * Helper function to add Zero-UI plugin to plugins configuration + * Handles both object format {plugin: {}} and array format [plugin] + */ +function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): boolean { + if (t.isObjectExpression(pluginsNode)) { + // Object format: { 'plugin': {} } + pluginsNode.properties.unshift(t.objectProperty(t.stringLiteral(zeroUiPlugin), t.objectExpression([]))); + return true; + } else if (t.isArrayExpression(pluginsNode)) { + // Array format: ['plugin'] + pluginsNode.elements.unshift(t.stringLiteral(zeroUiPlugin)); + return true; + } + return false; +} + +/** + * Helper to create a zeroUI() call AST node + */ +function createZeroUICallNode(): t.CallExpression { + return t.callExpression(t.identifier('zeroUI'), []); +} + +/** + * Helper to create a zeroUI import AST node + */ +function createZeroUIImportNode(importPath: string): t.ImportDeclaration { + return t.importDeclaration([t.importDefaultSpecifier(t.identifier('zeroUI'))], t.stringLiteral(importPath)); +} +/** + * Ensure the plugins array contains `zeroUI()` and **no** Tailwind entry. + * @returns true if the array was modified + */ +function processPluginsArray(elements: (t.Expression | null)[]): boolean { + let modified = false; + + // 1 remove "@tailwindcss/vite" string or tailwindcss() call + for (let i = elements.length - 1; i >= 0; i--) { + const el = elements[i]; + if ((t.isStringLiteral(el) && el.value === '@tailwindcss/vite') || (t.isCallExpression(el) && t.isIdentifier(el.callee, { name: 'tailwindcss' }))) { + elements.splice(i, 1); + modified = true; + } + } + + // 2 inject zeroUI() if absent + const hasZero = elements.some( + (el) => (t.isCallExpression(el) && t.isIdentifier(el.callee, { name: 'zeroUI' })) || (t.isStringLiteral(el) && el.value === 'zeroUI()') + ); + + if (!hasZero) { + elements.push(createZeroUICallNode()); + modified = true; + } + + return modified; +} + +/** + * Parse Vite config TypeScript/JavaScript file and add Zero-UI plugin + * Removes tailwindcss plugin if present, and adds zeroUI plugin if missing + * @param {string} source - The Vite config source code + * @param {string} zeroUiImportPath - The Zero-UI plugin import path + * @returns {string | null} The modified config code or null if no changes were made + */ +export function parseAndUpdateViteConfig(source: string, zeroUiImportPath: string): string | null { + if (source.includes(zeroUiImportPath) && source.includes('zeroUI()') && !source.includes('@tailwindcss/')) { + return source; + } + + let ast: t.File; + try { + ast = parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx', 'importMeta', 'decorators-legacy'] }); + } catch (e) { + throw new Error(`[Zero-UI] Failed to parse vite.config: ${e instanceof Error ? e.message : String(e)}`); + } + + let modified = false; + + traverse(ast, { + Program(p: NodePath) { + // inject import once + if (!source.includes(zeroUiImportPath)) { + p.node.body.unshift(t.importDeclaration([t.importDefaultSpecifier(t.identifier('zeroUI'))], t.stringLiteral(zeroUiImportPath))); + modified = true; + } + }, + + CallExpression(p: NodePath) { + if (t.isIdentifier(p.node.callee, { name: 'defineConfig' }) && t.isObjectExpression(p.node.arguments[0])) { + if (processConfigObject(p.node.arguments[0])) modified = true; + } + }, + + ExportDefaultDeclaration(p: NodePath) { + const decl = p.node.declaration; + let obj: t.ObjectExpression | null = null; + + if (t.isObjectExpression(decl)) obj = decl; + else if (t.isIdentifier(decl)) { + const b = p.scope.getBinding(decl.name); + if (b?.path.isVariableDeclarator() && t.isObjectExpression(b.path.node.init)) obj = b.path.node.init; + } else if ((t.isFunctionDeclaration(decl) || t.isArrowFunctionExpression(decl) || t.isFunctionExpression(decl)) && t.isObjectExpression(decl.body)) { + obj = decl.body; + } + + if (obj && processConfigObject(obj)) modified = true; + }, + + ReturnStatement(p: NodePath) { + const arg = p.node.argument; + if (t.isObjectExpression(arg) && processConfigObject(arg)) modified = true; + }, + + ObjectExpression(p: NodePath) { + if (processConfigObject(p.node)) modified = true; + }, + + ImportDeclaration(p: NodePath) { + if (p.node.source.value.startsWith('@tailwindcss/')) { + p.remove(); + modified = true; + } + }, + }); + + return modified ? generate(ast).code : null; +} + +/* ------------------------------------------------------------------ * + * processConfigObject - mutate a { plugins: [...] } + * ------------------------------------------------------------------ */ +function processConfigObject(obj: t.ObjectExpression): boolean { + let touched = false; + + // locate/create plugins array + let prop = obj.properties.find((p): p is t.ObjectProperty => t.isObjectProperty(p) && t.isIdentifier(p.key, { name: 'plugins' })); + + if (!prop) { + prop = t.objectProperty(t.identifier('plugins'), t.arrayExpression([])); + obj.properties.push(prop); + touched = true; + } + if (!t.isArrayExpression(prop.value)) return touched; + const arr = prop.value; + + // strip Tailwind + arr.elements = arr.elements.filter( + (el) => + !( + (t.isStringLiteral(el) && el.value.startsWith('@tailwindcss/')) || // ← ❸ filter all Tailwind strings + (t.isCallExpression(el) && t.isIdentifier(el.callee, { name: 'tailwindcss' })) + ) + ); + // prepend zeroUI() if missing + if (!arr.elements.some((el) => t.isCallExpression(el) && t.isIdentifier(el.callee, { name: 'zeroUI' }))) { + arr.elements.unshift(t.callExpression(t.identifier('zeroUI'), [])); + touched = true; + } + + return touched; +} + +function findLayoutWithBody(root = process.cwd()): string[] { + const matches: string[] = []; + function walk(dir: string): void { + for (const file of fs.readdirSync(dir)) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + walk(fullPath); + } else if (/^layout\.(tsx|jsx|js|ts)$/.test(file)) { + const source = fs.readFileSync(fullPath, 'utf8'); + if (source.includes(' { + const matches = findLayoutWithBody(); + + if (matches.length !== 1) { + console.warn(`[Zero-UI] ⚠️ Found ${matches.length} layout files with tags. ` + `Expected exactly one. Skipping automatic injection.`); + return; + } + + const filePath = matches[0]; + const code = fs.readFileSync(filePath, 'utf8'); + + // Parse the file into an AST + const ast = parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] }); + + let hasImport = false; + traverse(ast, { + ImportDeclaration(path: NodePath) { + const specifiers = path.node.specifiers; + const source = path.node.source.value; + if (source === '@zero-ui/attributes') { + for (const spec of specifiers) { + if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported) && spec.imported.name === 'bodyAttributes') { + hasImport = true; + } + } + } + }, + }); + + traverse(ast, { + Program(path: NodePath) { + if (!hasImport) { + const importDecl = t.importDeclaration( + [t.importSpecifier(t.identifier('bodyAttributes'), t.identifier('bodyAttributes'))], + t.stringLiteral('@zero-ui/attributes') + ); + path.node.body.unshift(importDecl); + } + }, + }); + + // Inject JSX spread into + let injected = false; + traverse(ast, { + JSXOpeningElement(path: NodePath) { + if (!injected && t.isJSXIdentifier(path.node.name, { name: 'body' })) { + // Prevent duplicate injection + const hasSpread = path.node.attributes.some((attr) => t.isJSXSpreadAttribute(attr) && t.isIdentifier(attr.argument, { name: 'bodyAttributes' })); + if (!hasSpread) { + path.node.attributes.unshift(t.jsxSpreadAttribute(t.identifier('bodyAttributes'))); + injected = true; + } + } + }, + }); + + const output = generate( + ast, + { + /* retain lines, formatting */ + }, + code + ).code; + fs.writeFileSync(filePath, output, 'utf8'); + console.log(`[Zero-UI] ✅ Patched in ${filePath} with {...bodyAttributes}`); +} + +/** + * Parse a tsconfig/jsconfig JSON file using Babel (handles comments, trailing commas) + */ +export function parseJsonWithBabel(source: string): any { + try { + const ast = parseExpression(source, { sourceType: 'module', plugins: ['jsonStrings'] }); + // Convert Babel AST back to plain JS object + return eval(`(${generate(ast).code})`); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : String(err); + console.warn(`[Zero-UI] Failed to parse ${source}: ${errorMessage}`); + return null; + } +} diff --git a/packages/core/src/postcss/ast-generating.test.ts b/packages/core/src/postcss/ast-generating.test.ts new file mode 100644 index 0000000..9a5cbf7 --- /dev/null +++ b/packages/core/src/postcss/ast-generating.test.ts @@ -0,0 +1,90 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { readFile, runTest } from '../utilities.ts'; +import { parseAndUpdatePostcssConfig, parseAndUpdateViteConfig, parseJsonWithBabel, patchNextBodyTag } from './ast-generating.cts'; + +const zeroUiPlugin = '@react-zero-ui/core/postcss'; +const zeroUiVitePlugin = '@react-zero-ui/core/vite'; + +test('parseAndUpdatePostcssConfig should parse and update PostCSS config', async () => { + await runTest( + { + 'postcss.config.js': ` + module.exports = { + plugins: [require('tailwindcss'), require('autoprefixer')], + }`, + 'postcss.config2.mjs': ` + const config = { plugins: [ '@tailwindcss/postcss'] }; +export default config; +`, + }, + async () => { + const config = parseAndUpdatePostcssConfig(readFile('postcss.config.js'), zeroUiPlugin, false); + assert(config?.includes(zeroUiPlugin)); + const config2 = parseAndUpdatePostcssConfig(readFile('postcss.config2.mjs'), zeroUiPlugin, true); + assert(config2?.includes(zeroUiPlugin)); + } + ); +}); + +const FIXTURES = { + // 1. ESM object + 'vite.config.js': ` + + export default { plugins: [react(),'@tailwindcss/vite'] }; + `, + + // 2. defineConfig helper + 'vite.config.ts': ` + import { defineConfig } from 'vite'; + export default defineConfig({ plugins: [react(),'@tailwindcss/vite'] }); + `, + + // 3. sync factory + 'vite.config.mjs': ` + export default ({ mode }) => ({ plugins: [react(),'@tailwindcss/vite'] }); + `, + + // 4. async factory + indirection + 'vite.config.mts': ` + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + const cfg = defineConfig({ plugins: [react(),'@tailwindcss/vite'] }); + export default async () => cfg; + `, +}; + +test('parseAndUpdateViteConfig inserts Zero-UI and removes Tailwind', () => { + for (const [name, src] of Object.entries(FIXTURES)) { + const out = parseAndUpdateViteConfig(src, zeroUiVitePlugin); + assert(out && out.includes(zeroUiVitePlugin), `${name} missing zeroUI`); + assert(out.includes('zeroUI()'), `${name} missing zeroUI`); + assert(out.includes('react()'), `${name} missing react`); + assert(!out.includes('@tailwindcss/vite'), `${name} still contains tailwind`); + } +}); + +test('patchNextBodyTag should patch the body tag with the {...bodyAttributes}', async () => { + await runTest( + { + 'src/app/layout.tsx': ` + + +
      Hello
      + + + `, + }, + async () => { + await patchNextBodyTag(); + const result = readFile('src/app/layout.tsx'); + assert(result.includes('{...bodyAttributes}'), 'body tag not patched'); + assert(result.includes('import { bodyAttributes } from "@zero-ui/attributes";'), 'import not found'); + } + ); +}); + +test('parseJsonWithBabel should parse a JSON file', () => { + const result = parseJsonWithBabel(readFile('package.json')); + assert(result, 'package.json not parsed'); +}); diff --git a/packages/core/src/postcss/ast-parsing.cts b/packages/core/src/postcss/ast-parsing.cts new file mode 100644 index 0000000..0b7b896 --- /dev/null +++ b/packages/core/src/postcss/ast-parsing.cts @@ -0,0 +1,236 @@ +// src/core/postcss/ast-parsing.cts + +import { parse } from '@babel/parser'; +import * as babelTraverse from '@babel/traverse'; +import { Binding, NodePath, Node } from '@babel/traverse'; +import * as t from '@babel/types'; +import { CONFIG } from '../config.cjs'; +import { createHash } from 'crypto'; +import * as fs from 'fs'; +import { literalFromNode, ResolveOpts } from './resolvers.cjs'; +import { codeFrameColumns } from '@babel/code-frame'; +const traverse = (babelTraverse as any).default; +import { LRUCache as LRU } from 'lru-cache'; +import { scanVariantTokens } from './scanner.cjs'; + +export interface SetterMeta { + /** Babel binding object — use `binding.referencePaths` in Pass 2 */ + binding: Binding; + /** Variable name (`setTheme`) */ + setterName: string; + /** State key passed to `useUI` (`'theme'`) */ + stateKey: string; + /** Literal initial value as string, or `null` if non-literal */ + initialValue: string | null; +} + +/** + * Collect every `[ value, setterFn ] = useUI('key', 'initial')` in a file. + * Re-uses `literalFromNode` so **initialArg** can be: + * • literal `'dark'` + * • local const `DARK` + * • static template `` `da${'rk'}` `` + * Throws if the key is dynamic or if the initial value cannot be + * reduced to a space-free string. + */ +export function collectUseUISetters(ast: t.File, sourceCode: string): SetterMeta[] { + const setters: SetterMeta[] = []; + /* ---------- cache resolved literals per AST node ---------- */ + const memo = new WeakMap(); + const optsBase = { throwOnFail: false, source: sourceCode } as ResolveOpts; + + function lit(node: t.Expression, p: NodePath, hook: ResolveOpts['hook']): string | null { + if (memo.has(node)) return memo.get(node)!; + + // clone instead of mutate + const localOpts: ResolveOpts = { ...optsBase, hook }; + + const ev = (p as NodePath).evaluate?.(); + const value = ev?.confident && typeof ev.value === 'string' ? ev.value : literalFromNode(node, p, localOpts); + + memo.set(node, value); + return value; + } + + traverse(ast, { + VariableDeclarator(path: NodePath) { + const { id, init } = path.node; + + // match: const [ , setX ] = useUI(...) + if (!t.isArrayPattern(id) || !t.isCallExpression(init) || !t.isIdentifier(init.callee, { name: CONFIG.HOOK_NAME })) return; + + if (id.elements.length !== 2) { + throwCodeFrame(path, path.opts?.filename, sourceCode, `[Zero-UI] useUI() must destructure two values: [value, setterFn].`); + } + + const [, setterEl] = id.elements; + + if (!setterEl || !t.isIdentifier(setterEl)) { + throwCodeFrame(path, path.opts?.filename, sourceCode, `[Zero-UI] useUI() setterFn must be a variable name.`); + } + + const [keyArg, initialArg] = init.arguments; + + // resolve state key with new helpers + const stateKey = lit(keyArg as t.Expression, path as NodePath, 'stateKey'); + + if (stateKey === null) { + throwCodeFrame( + keyArg, + path.opts?.filename, + sourceCode, + // TODO add link to docs + `[Zero-UI] State key cannot be resolved at build-time.\n` + `Only local, fully-static strings are supported. - collectUseUISetters-stateKey` + ); + } + + // resolve initial value with new helpers + const initialValue = lit(initialArg as t.Expression, path as NodePath, 'initialValue'); + + if (initialValue === null) { + throwCodeFrame( + initialArg, + path.opts?.filename, + sourceCode, + // TODO add link to docs + `[Zero-UI] initial value cannot be resolved at build-time.\n` + + `Only local, fully-static objects/arrays are supported. - collectUseUISetters-initialValue` + ); + } + + const binding = path.scope.getBinding(setterEl.name); + if (!binding) { + throwCodeFrame(path, path.opts?.filename, sourceCode, `[Zero-UI] Could not resolve binding for setter "${setterEl.name}".`); + } + + setters.push({ binding, setterName: setterEl.name, stateKey, initialValue }); + }, + }); + + return setters; +} + +export interface VariantData { + /** The state key (e.g., 'theme') */ + key: string; + /** Array of unique values discovered in the file */ + values: string[]; + /** Literal initial value as string, or `null` if non-literal */ + initialValue: string | null; +} +/** + * Convert the harvested variants map to the final output format + */ +export function normalizeVariants(scanned: Map>, setters: SetterMeta[], sort = true): VariantData[] { + // key → initialValue (may be null) + const initialMap = new Map(setters.map((s) => [s.stateKey, s.initialValue ?? null])); + + const out: VariantData[] = []; + + // 1️⃣ merge scan-results with initial values + for (const [key, init] of initialMap) { + // ensure we have a bucket + if (!scanned.has(key)) scanned.set(key, new Set()); + + if (init) scanned.get(key)!.add(init); + } + + // 2️⃣ convert to the public shape + for (const [key, set] of scanned) { + const values = Array.from(set); + if (sort) values.sort(); + + out.push({ key, values, initialValue: initialMap.get(key) ?? null }); + } + + if (sort) out.sort((a, b) => a.key.localeCompare(b.key)); + return out; +} + +// File cache to avoid re-parsing unchanged files +export interface CacheEntry { + hash: string; + variants: VariantData[]; +} + +const fileCache = new LRU({ max: 5000 }); + +function keysFrom(setters: SetterMeta[]): Set { + return new Set(setters.map((s) => s.stateKey)); // already kebab-cased by collectUseUISetters +} + +export function extractVariants(filePath: string): VariantData[] { + // console.log(`[CACHE] Checking: ${filePath}`); + + try { + const { mtimeMs, size } = fs.statSync(filePath); + const sig = `${mtimeMs}:${size}`; + + const cached = fileCache.get(filePath); + if (cached && cached.hash === sig) { + // console.log(`[CACHE] HIT (sig): ${filePath}`); + return cached.variants; // Fast path: file unchanged + } + + const source = fs.readFileSync(filePath, 'utf8'); + const hash = createHash('md5').update(source).digest('hex'); + + // Fallback: content unchanged despite mtime/size change + if (cached && cached.hash === hash) { + // console.log(`[CACHE] HIT (hash): ${filePath}`); + // Update cache with new sig for next time + const entry = { hash: sig, variants: cached.variants }; + fileCache.set(filePath, entry); + return cached.variants; + } + + // console.log(`[CACHE] MISS: ${filePath} (parsing...)`); + // Parse the file + const ast = parse(source, { sourceType: 'module', plugins: ['jsx', 'typescript', 'decorators-legacy'], sourceFilename: filePath }); + + // Collect useUI setters + const setters = collectUseUISetters(ast, source); + if (!setters.length) return []; + + // Normalize variants + const variants = normalizeVariants(scanVariantTokens(source, keysFrom(setters)), setters); + + // Store with signature for fast future lookups + fileCache.set(filePath, { hash: sig, variants }); + return variants; + } catch (error) { + // Fallback for virtual/non-existent files - use content hash only + const source = fs.readFileSync(filePath, 'utf8'); + const hash = createHash('md5').update(source).digest('hex'); + + const cached = fileCache.get(filePath); + if (cached && cached.hash === hash) { + return cached.variants; + } + + // Parse and cache... + const ast = parse(source, { sourceType: 'module', plugins: ['jsx', 'typescript', 'decorators-legacy'], sourceFilename: filePath }); + const setters = collectUseUISetters(ast, source); + if (!setters.length) return []; + + // final normalization of variants + const variants = normalizeVariants(scanVariantTokens(source, keysFrom(setters)), setters); + console.log('variants extractVariants: ', variants); + + fileCache.set(filePath, { hash, variants }); + return variants; + } +} + +export function throwCodeFrame(nodeOrPath: Node | NodePath, filename: string, source: string, msg: string): never { + // Accept either a raw node or a NodePath + const node = (nodeOrPath as NodePath).node ?? (nodeOrPath as Node); + + if (!node.loc) throw new Error(msg); // defensive + + const { start, end } = node.loc; + const header = `${filename ?? ''}:${start.line}:${start.column}\n`; + const frame = codeFrameColumns(source, { start, end }, { highlightCode: true, linesAbove: 1, linesBelow: 1 }); + + throw new Error(`${header}${msg}\n${frame}`); +} diff --git a/packages/core/src/postcss/ast-parsing.test.ts b/packages/core/src/postcss/ast-parsing.test.ts new file mode 100644 index 0000000..d31e6a6 --- /dev/null +++ b/packages/core/src/postcss/ast-parsing.test.ts @@ -0,0 +1,64 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import fs from 'fs'; +import { collectUseUISetters, extractVariants, normalizeVariants } from './ast-parsing.cts'; +import { parse } from '@babel/parser'; +import { runTest } from '../utilities.ts'; + +test('collectUseUISetters should collect setters from a component', async () => { + await runTest( + { + 'app/boolean-edge-cases.tsx': ` + import { useUI } from '@react-zero-ui/core'; + + const featureEnabled = 'feature-enabled'; + const bool = false; + + const theme = ['true']; + +export function Component() { + const [isVisible, setIsVisible] = useUI('modal-visible', 'false'); + const [isEnabled, setIsEnabled] = useUI(bool ?? featureEnabled, theme[0]); +return ( +
      +
      + Open Modal +
      +
      +); }`, + }, + + async () => { + const src = fs.readFileSync('app/boolean-edge-cases.tsx', 'utf8'); + const ast = parse(src, { sourceType: 'module', plugins: ['jsx', 'typescript'] }); + const setters = collectUseUISetters(ast, src); + assert(setters[0].binding !== null); + assert(setters[0].initialValue === 'false'); + assert(setters[0].stateKey === 'modal-visible'); + assert(setters[0].setterName === 'setIsVisible'); + assert(setters[1].binding !== null); + assert(setters[1].initialValue === 'true'); + assert(setters[1].stateKey === 'feature-enabled'); + assert(setters[1].setterName === 'setIsEnabled'); + + const variants = normalizeVariants(new Map(), setters, false); + // variants: [ + // { key: 'modal-visible', values: [ 'false' ], initialValue: 'false' }, + // { key: 'feature-enabled', values: [ 'true' ], initialValue: 'true' } + // ] + assert(variants.length === 2); + assert(variants[0].key === 'modal-visible'); + assert(variants[0].values[0] === 'false'); + assert(variants[1].key === 'feature-enabled'); + assert(variants[1].values[0] === 'true'); + + const finalVariants = extractVariants('app/boolean-edge-cases.tsx'); + + assert(finalVariants[0].key === 'feature-enabled'); + assert(finalVariants[0].values.includes('true')); + assert(finalVariants[1].key === 'modal-visible'); + assert(finalVariants[1].values.includes('false')); + } + ); +}); diff --git a/packages/core/src/postcss/ast.cjs b/packages/core/src/postcss/ast.cjs deleted file mode 100644 index f732248..0000000 --- a/packages/core/src/postcss/ast.cjs +++ /dev/null @@ -1,603 +0,0 @@ -// src/postcss/ast.cjs - -// TODO update to esbuild or SWC + run in parallel w/ per changed file only -const parser = require('@babel/parser'); -const generate = require('@babel/generator').default; -const traverse = require('@babel/traverse').default; -const t = require('@babel/types'); -const crypto = require('crypto'); -const path = require('path'); -const fs = require('fs'); -const { CONFIG } = require('../config.cjs'); - -// Cache for parsed files -const fileCache = new Map(); - -function extractVariants(filePath) { - try { - const code = fs.readFileSync(filePath, 'utf-8'); - if (!code.includes(CONFIG.HOOK_NAME)) return []; - // Check cache - const hash = crypto.createHash('md5').update(code).digest('hex'); - const cached = fileCache.get(filePath); - if (cached && cached.hash === hash) { - return cached.variants; - } - let ast; - ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] }); - const variants = CONFIG.SUPPORTED_EXTENSIONS.TYPESCRIPT.includes(path.extname(filePath).toLowerCase()) - ? extractTypeScriptVariants(ast) - : extractJavaScriptVariants(ast); - - // Update cache - fileCache.set(filePath, { hash, variants }); - - return variants; - } catch (error) { - console.error(`Error processing ${filePath}:`, error.message); - return []; - } -} - -function extractTypeScriptVariants(ast) { - const extractedUIStates = []; - - traverse(ast, { - CallExpression(path) { - const callee = path.get('callee'); - - if (!callee.isIdentifier() || callee.node.name !== CONFIG.HOOK_NAME) return; - - // useUI('theme', 'light') -> hookArguments = ['theme', 'light'] - const hookArguments = path.node.arguments; - const typeScriptGenericTypes = path.node.typeParameters ? path.node.typeParameters.params : undefined; - - // First arg: the state key (e.g., 'theme', 'modal', 'sidebar') - const stateKeyArgument = hookArguments[0]; - if (stateKeyArgument.type !== 'StringLiteral') return; - const stateKey = stateKeyArgument.value; - - let possibleStateValues = []; - let initialStateValue = null; - - // First, try to get values from TypeScript generic type - if (typeScriptGenericTypes && typeScriptGenericTypes[0]) { - const genericType = typeScriptGenericTypes[0]; - - if (genericType.type === 'TSBooleanKeyword') { - possibleStateValues = ['true', 'false']; - } else if (genericType.type === 'TSUnionType') { - // useUI<'light' | 'dark' | 'auto'> - possibleStateValues = genericType.types - .filter(unionMember => unionMember.type === 'TSLiteralType') - .map(unionMember => unionMember.literal.value) - .filter(Boolean) - .map(String); - } - } - - // If no TypeScript types found, infer from initial value - if (possibleStateValues.length === 0) { - // Second arg: the initial value (e.g., 'light', false, 'closed') - const initialValueArgument = hookArguments[1]; - - if (initialValueArgument.type === 'BooleanLiteral') { - possibleStateValues = ['true', 'false']; - initialStateValue = String(initialValueArgument.value); - } else if (initialValueArgument.type === 'StringLiteral') { - possibleStateValues = [initialValueArgument.value]; - initialStateValue = initialValueArgument.value; - } - } else { - // We do have TypeScript types, and still need to extract initial value - const initialValueArgument = hookArguments[1]; - if (initialValueArgument.type === 'StringLiteral') { - initialStateValue = initialValueArgument.value; - } else if (initialValueArgument.type === 'BooleanLiteral') { - initialStateValue = String(initialValueArgument.value); - } - } - - if (possibleStateValues.length > 0) { - extractedUIStates.push({ - key: stateKey, - values: possibleStateValues, - initialValue: initialStateValue, // Track initial value explicitly - }); - } - }, - }); - - return extractedUIStates; -} - -function extractJavaScriptVariants(ast) { - const stateKeyToPossibleValues = new Map(); - const setterFunctionNameToStateKey = new Map(); - const stateKeyToInitialValue = new Map(); // Track initial values - - // First pass: Find all useUI calls and extract initial setup - traverse(ast, { - CallExpression(path) { - if (path.node.callee.name !== CONFIG.HOOK_NAME) return; - - const hookArguments = path.node.arguments; - if (hookArguments.length < CONFIG.MIN_HOOK_ARGUMENTS) return; - - // useUI('theme', 'light') -> stateKey = 'theme', initialValue = 'light' - const stateKeyArgument = hookArguments[0]; - const initialValueArgument = hookArguments[1]; - - if (stateKeyArgument.type !== 'StringLiteral') return; - const stateKey = stateKeyArgument.value; - - // Initialize the set for this state key - if (!stateKeyToPossibleValues.has(stateKey)) { - stateKeyToPossibleValues.set(stateKey, new Set()); - } - - // Store the initial value - if (initialValueArgument.type === 'StringLiteral') { - const initialValue = initialValueArgument.value; - stateKeyToPossibleValues.get(stateKey).add(initialValue); - stateKeyToInitialValue.set(stateKey, initialValue); - } else if (initialValueArgument.type === 'BooleanLiteral') { - stateKeyToPossibleValues.set(stateKey, new Set(['true', 'false'])); - stateKeyToInitialValue.set(stateKey, String(initialValueArgument.value)); - } - - // Track the setter function name - // const [theme, setTheme] = useUI(...) -> setterName = 'setTheme' - const parentDeclaration = path.parent; - if (parentDeclaration.type === 'VariableDeclarator' && parentDeclaration.id.type === 'ArrayPattern' && parentDeclaration.id.elements[1]) { - const setterElement = parentDeclaration.id.elements[1]; - if (setterElement.type === 'Identifier') { - const setterFunctionName = setterElement.name; - setterFunctionNameToStateKey.set(setterFunctionName, stateKey); - } - } - }, - }); - - // Second pass: Find all setter function calls to discover possible values - traverse(ast, { - CallExpression(path) { - const { callee, arguments: callArguments } = path.node; - - // Direct setter call: setTheme('dark') - if (callee.type === 'Identifier' && setterFunctionNameToStateKey.has(callee.name)) { - const stateKey = setterFunctionNameToStateKey.get(callee.name); - const setterArgumentValues = extractArgumentValues(callArguments[0], path); - setterArgumentValues.forEach(value => stateKeyToPossibleValues.get(stateKey).add(value)); - } - }, - - // Check event handlers: onClick={() => setTheme('dark')} - JSXAttribute(path) { - if (path.node.name.name.startsWith('on')) { - const jsxAttributeValue = path.node.value; - if (jsxAttributeValue.type === 'JSXExpressionContainer') { - checkExpressionForSetters(jsxAttributeValue.expression, setterFunctionNameToStateKey, stateKeyToPossibleValues, path); - } - } - }, - }); - - // Convert to final format with initial values - return Array.from(stateKeyToPossibleValues.entries()).map(([stateKey, possibleValuesSet]) => ({ - key: stateKey, - values: Array.from(possibleValuesSet), - initialValue: stateKeyToInitialValue.get(stateKey) || null, - })); -} - -function checkExpressionForSetters(node, setterToKey, variants, path) { - if (node.type === 'ArrowFunctionExpression') { - const body = node.body; - - const expressions = body.type === 'BlockStatement' ? findCallExpressionsInBlock(body) : [body]; - - expressions.forEach(expr => { - if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier' && setterToKey.has(expr.callee.name)) { - const key = setterToKey.get(expr.callee.name); - const values = extractArgumentValues(expr.arguments[0], path); - values.forEach(v => variants.get(key).add(v)); - } - }); - } -} - -function findCallExpressionsInBlock(block) { - const calls = []; - block.body.forEach(statement => { - if (statement.type === 'ExpressionStatement' && statement.expression.type === 'CallExpression') { - calls.push(statement.expression); - } - }); - return calls; -} - -function extractArgumentValues(node, path) { - const values = new Set(); - - if (!node) return values; - - switch (node.type) { - case 'StringLiteral': - values.add(node.value); - break; - case 'BooleanLiteral': - values.add(String(node.value)); - break; - - case 'NumericLiteral': - values.add(String(node.value)); - break; - - case 'ConditionalExpression': - // isActive ? 'react' : 'zero' - extractArgumentValues(node.consequent, path).forEach(v => values.add(v)); - extractArgumentValues(node.alternate, path).forEach(v => values.add(v)); - break; - - case 'LogicalExpression': - if (node.operator === '||' || node.operator === '??') { - extractArgumentValues(node.left, path).forEach(v => values.add(v)); - extractArgumentValues(node.right, path).forEach(v => values.add(v)); - } - break; - - case 'Identifier': - { - const binding = path.scope.getBinding(node.name); - if (binding && binding.path.isVariableDeclarator()) { - const init = binding.path.node.init; - if (init.type === 'StringLiteral') { - values.add(init.value); - } - } - } - break; - - case 'MemberExpression': - if (node.property.type === 'Identifier') { - values.add(node.property.name.toLowerCase()); - } - break; - } - - return values; -} - -/** - * Parse a tsconfig/jsconfig JSON file using Babel (handles comments, trailing commas) - */ -function parseJsonWithBabel(source) { - try { - const ast = parser.parseExpression(source, { sourceType: 'module', plugins: ['json'] }); - // Convert Babel AST back to plain JS object - return eval(`(${generate(ast).code})`); - } catch (err) { - console.warn(`[Zero-UI] Failed to parse ${source}: ${err.message}`); - return null; - } -} - -/** - * Parse PostCSS config JavaScript file and add Zero-UI plugin if not present - * Uses Babel AST for robust parsing and modification - * Supports both CommonJS (.js) and ES Modules (.mjs) formats - * @param {string} source - The PostCSS config source code - * @param {string} zeroUiPlugin - The Zero-UI plugin name - * @param {boolean} isESModule - Whether the config is an ES module - * @returns {string | null} The modified config code or null if no changes were made - */ -function parseAndUpdatePostcssConfig(source, zeroUiPlugin, isESModule = false) { - try { - const ast = parser.parse(source, { sourceType: 'module', plugins: ['commonjs', 'importMeta'] }); - - let modified = false; - - // Check if Zero-UI plugin already exists - if (source.includes(zeroUiPlugin)) { - return source; // Already configured - } - - traverse(ast, { - // Handle CommonJS: module.exports = { ... } and exports = { ... } - AssignmentExpression(path) { - const { left, right } = path.node; - - // Check for module.exports or exports assignment - const isModuleExports = left.type === 'MemberExpression' && left.object.name === 'module' && left.property.name === 'exports'; - const isExportsAssignment = left.type === 'Identifier' && left.name === 'exports'; - - if ((isModuleExports || isExportsAssignment) && right.type === 'ObjectExpression') { - const pluginsProperty = right.properties.find(prop => prop.key && prop.key.name === 'plugins'); - - if (pluginsProperty) { - modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); - } - } - }, - - // Handle ES Modules: export default { ... } - ExportDefaultDeclaration(path) { - if (isESModule && path.node.declaration.type === 'ObjectExpression') { - const pluginsProperty = path.node.declaration.properties.find(prop => prop.key && prop.key.name === 'plugins'); - - if (pluginsProperty) { - modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); - } - } - }, - - // Handle: const config = { plugins: ... }; export default config - VariableDeclarator(path) { - if (isESModule && path.node.init && path.node.init.type === 'ObjectExpression') { - const pluginsProperty = path.node.init.properties.find(prop => prop.key && prop.key.name === 'plugins'); - - if (pluginsProperty) { - modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); - } - } - }, - }); - - if (modified) { - return generate(ast).code; - } else { - return null; // Could not automatically modify - } - } catch (err) { - console.warn(`[Zero-UI] Failed to parse PostCSS config: ${err.message}`); - return null; - } -} - -/** - * Helper function to add Zero-UI plugin to plugins configuration - * Handles both object format {plugin: {}} and array format [plugin] - */ -function addZeroUiToPlugins(pluginsNode, zeroUiPlugin) { - if (pluginsNode.type === 'ObjectExpression') { - // Object format: { 'plugin': {} } - pluginsNode.properties.unshift({ - type: 'ObjectProperty', - key: { type: 'StringLiteral', value: zeroUiPlugin }, - value: { type: 'ObjectExpression', properties: [] }, - }); - return true; - } else if (pluginsNode.type === 'ArrayExpression') { - // Array format: ['plugin'] - pluginsNode.elements.unshift({ type: 'StringLiteral', value: zeroUiPlugin }); - return true; - } - return false; -} - -/** - * Helper to create a zeroUI() call AST node - */ -function createZeroUICallNode() { - return { type: 'CallExpression', callee: { type: 'Identifier', name: 'zeroUI' }, arguments: [] }; -} - -/** - * Helper to create a zeroUI import AST node - */ -function createZeroUIImportNode(importPath) { - return { - type: 'ImportDeclaration', - specifiers: [{ type: 'ImportDefaultSpecifier', local: { type: 'Identifier', name: 'zeroUI' } }], - source: { type: 'StringLiteral', value: importPath }, - }; -} - -/** - * Helper to process a plugins array - replaces Tailwind with zeroUI or adds zeroUI - */ -function processPluginsArray(pluginsArray) { - let tailwindIndex = -1; - let zeroUIIndex = -1; - - // Find existing plugins - pluginsArray.forEach((element, index) => { - if (element && element.type === 'CallExpression') { - if (element.callee.name === 'tailwindcss') { - tailwindIndex = index; - } else if (element.callee.name === 'zeroUI') { - zeroUIIndex = index; - } - } - }); - - // Replace Tailwind with Zero-UI - if (tailwindIndex >= 0) { - pluginsArray[tailwindIndex] = createZeroUICallNode(); - return true; - } - // Add Zero-UI if not present - else if (zeroUIIndex === -1) { - pluginsArray.push(createZeroUICallNode()); - return true; - } - - return false; -} - -/** - * Helper to handle config object (creates plugins array if needed) - */ -function processConfigObject(configObject) { - const pluginsProperty = configObject.properties.find(prop => prop.key && prop.key.name === 'plugins'); - - if (pluginsProperty && pluginsProperty.value.type === 'ArrayExpression') { - // Process existing plugins array - return processPluginsArray(pluginsProperty.value.elements); - } else if (!pluginsProperty) { - // Create new plugins array with zeroUI - configObject.properties.push({ - type: 'ObjectProperty', - key: { type: 'Identifier', name: 'plugins' }, - value: { type: 'ArrayExpression', elements: [createZeroUICallNode()] }, - }); - return true; - } - - return false; -} - -/** - * Helper to add zeroUI import to program - * Uses the FAANG approach: add at the beginning and let tooling handle organization - */ -function addZeroUIImport(programPath, zeroUiPlugin) { - const zeroUIImport = createZeroUIImportNode(zeroUiPlugin); - // Simple approach: add at the beginning - programPath.node.body.unshift(zeroUIImport); -} - -/** - * Parse Vite config TypeScript/JavaScript file and add Zero-UI plugin - * Replaces @tailwindcss/vite plugin if present, otherwise adds zeroUI plugin - * @param {string} source - The Vite config source code - * @param {string} zeroUiPlugin - The Zero-UI plugin import path - * @returns {string | null} The modified config code or null if no changes were made - */ -function parseAndUpdateViteConfig(source, zeroUiPlugin) { - try { - // Quick check - if already configured correctly, return original - const hasZeroUIImport = source.includes(zeroUiPlugin); - const hasZeroUIPlugin = source.includes('zeroUI()'); - const hasTailwindPlugin = source.includes('@tailwindcss/vite'); - - if (hasZeroUIImport && hasZeroUIPlugin && !hasTailwindPlugin) { - return source; - } - - const ast = parser.parse(source, { sourceType: 'module', plugins: ['typescript', 'importMeta'] }); - - let modified = false; - - traverse(ast, { - Program(path) { - if (!hasZeroUIImport) { - addZeroUIImport(path, zeroUiPlugin); - modified = true; - } - }, - - // Handle both direct export and variable assignment patterns - CallExpression(path) { - if (path.node.callee.name === 'defineConfig' && path.node.arguments.length > 0 && path.node.arguments[0].type === 'ObjectExpression') { - if (processConfigObject(path.node.arguments[0])) { - modified = true; - } - } - }, - - // Remove Tailwind import if we're replacing it - ImportDeclaration(path) { - if (path.node.source.value === '@tailwindcss/vite' && hasTailwindPlugin) { - path.remove(); - modified = true; - } - }, - }); - - return modified ? generate(ast).code : null; - } catch (err) { - console.warn(`[Zero-UI] Failed to parse Vite config: ${err.message}`); - return null; - } -} - -function findLayoutWithBody(root = process.cwd()) { - const matches = []; - function walk(dir) { - for (const file of fs.readdirSync(dir)) { - const fullPath = path.join(dir, file); - const stat = fs.statSync(fullPath); - if (stat.isDirectory()) { - walk(fullPath); - } else if (/^layout\.(tsx|jsx|js|ts)$/.test(file)) { - const source = fs.readFileSync(fullPath, 'utf8'); - if (source.includes(' tags. ` + `Expected exactly one. Skipping automatic injection.`); - return; - } - - const filePath = matches[0]; - const code = fs.readFileSync(filePath, 'utf8'); - - // Parse the file into an AST - const ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] }); - - let hasImport = false; - traverse(ast, { - ImportDeclaration(path) { - const specifiers = path.node.specifiers; - const source = path.node.source.value; - if (source === '@zero-ui/attributes') { - for (const spec of specifiers) { - if (t.isImportSpecifier(spec) && spec.imported.name === 'bodyAttributes') { - hasImport = true; - } - } - } - }, - }); - - traverse(ast, { - Program(path) { - if (!hasImport) { - const importDecl = t.importDeclaration( - [t.importSpecifier(t.identifier('bodyAttributes'), t.identifier('bodyAttributes'))], - t.stringLiteral('@zero-ui/attributes') - ); - path.node.body.unshift(importDecl); - } - }, - }); - - // Inject JSX spread into - let injected = false; - traverse(ast, { - JSXOpeningElement(path) { - if (!injected && t.isJSXIdentifier(path.node.name, { name: 'body' })) { - // Prevent duplicate injection - const hasSpread = path.node.attributes.some(attr => t.isJSXSpreadAttribute(attr) && t.isIdentifier(attr.argument, { name: 'bodyAttributes' })); - if (!hasSpread) { - path.node.attributes.unshift(t.jsxSpreadAttribute(t.identifier('bodyAttributes'))); - injected = true; - } - } - }, - }); - - const output = generate( - ast, - { - /* retain lines, formatting */ - }, - code - ).code; - fs.writeFileSync(filePath, output, 'utf8'); - console.log(`[Zero-UI] ✅ Patched in ${filePath} with {...bodyAttributes}`); -} - -module.exports = { extractVariants, parseJsonWithBabel, parseAndUpdatePostcssConfig, parseAndUpdateViteConfig, patchNextBodyTag }; diff --git a/packages/core/src/postcss/coming-soon/build-tool.ts b/packages/core/src/postcss/coming-soon/build-tool.ts new file mode 100644 index 0000000..1cf9aa7 --- /dev/null +++ b/packages/core/src/postcss/coming-soon/build-tool.ts @@ -0,0 +1,182 @@ +import { readFileSync, writeFileSync } from 'fs'; +import { glob } from 'glob'; +import { extractVariants } from '../ast-parsing.cjs'; +import { batchInjectDataAttributes, SEMANTIC_CONFIG } from './inject-attributes.js'; +import { RefLocationTracker } from './collect-refs.cjs'; + +const globalRefTracker = new RefLocationTracker(); + +/** + * Complete build process example + */ +export async function buildWithDataAttributes() { + console.log('🔍 Scanning for useUI usage...'); + + // 1. Find all relevant files + const files = await glob('src/**/*.{ts,tsx,js,jsx}', { ignore: ['**/*.d.ts', '**/node_modules/**'] }); + + // 2. Extract variants and populate ref tracker + console.log(`📋 Processing ${files.length} files...`); + const allVariants = extractVariants(files); + + console.log( + '✅ Found variants:', + allVariants.map((v) => `${v.key}: [${v.values.join(', ')}]`) + ); + + // 3. Read all file contents + const fileContents = new Map(); + for (const filePath of files) { + const content = readFileSync(filePath, 'utf-8'); + fileContents.set(filePath, content); + } + + // 4. Inject data attributes + console.log('💉 Injecting data attributes...'); + const modifiedFiles = batchInjectDataAttributes(fileContents, globalRefTracker, allVariants, SEMANTIC_CONFIG); + + // 5. Write modified files (or handle according to your build system) + let modifiedCount = 0; + for (const [filePath, originalContent] of fileContents) { + const modifiedContent = modifiedFiles.get(filePath)!; + + if (modifiedContent !== originalContent) { + // In a real build system, you might return the modified content + // instead of writing directly to disk + writeFileSync(filePath, modifiedContent); + modifiedCount++; + } + } + + console.log(`✨ Modified ${modifiedCount} files with data attributes`); + + // 6. Generate CSS or other assets based on variants + generateCSS(allVariants); + + return { variants: allVariants, modifiedFiles: modifiedCount, refLocations: globalRefTracker.getAllRefs() }; +} + +/** + * Vite plugin example + */ +export function createViteUIPlugin() { + let refTracker: any; + let variants: any[]; + + return { + name: 'vite-ui-data-attributes', + buildStart() { + // Initialize on build start + console.log('🚀 Initializing UI data attribute injection...'); + }, + async buildEnd() { + // Extract variants from all processed files + const files = await glob('src/**/*.{ts,tsx,js,jsx}'); + variants = extractVariants(files); + refTracker = globalRefTracker; + + console.log('📊 Extracted variants:', variants); + }, + transform(code: string, id: string) { + // Only process relevant files + if (!/\.(tsx?|jsx?)$/.test(id)) return null; + + // Check if this file has any ref locations + const refLocations = refTracker?.getRefsByFile(id) || []; + if (refLocations.length === 0) return null; + + // Inject data attributes + try { + const { injectDataAttributes } = require('./data_attribute_injector'); + const transformedCode = injectDataAttributes(code, refLocations, variants || [], SEMANTIC_CONFIG); + + return { + code: transformedCode, + map: null, // You might want to generate source maps + }; + } catch (error) { + console.error('Error injecting data attributes:', error); + return null; + } + }, + }; +} + +/** + * Webpack loader example +// */ +// export function createWebpackUILoader() { +// return function uiDataAttributeLoader(source: string) { +// const callback = this.async(); +// const resourcePath = this.resourcePath; + +// // Only process relevant files +// if (!/\.(tsx?|jsx?)$/.test(resourcePath)) { +// return callback(null, source); +// } + +// try { +// // Get ref locations for this file +// const refLocations = globalRefTracker.getRefsByFile(resourcePath); + +// if (refLocations.length === 0) { +// return callback(null, source); +// } + +// // Get all variants (you'd need to make this available globally) +// const variants = []; // This would come from your build context + +// const { injectDataAttributes } = require('./data_attribute_injector'); +// const transformedSource = injectDataAttributes(source, refLocations, variants, SEMANTIC_CONFIG); + +// callback(null, transformedSource); +// } catch (error) { +// callback(error); +// } +// }; +// } + +/** + * Generate CSS based on variants + * TODO MAKE ONE GLOBAL GENERATE CSS FILE Function + * TODO MAKE ONE LOCAL GENERATE CSS FILE Function + */ +function generateCSS(variants: any[]) { + const cssRules: string[] = []; + + for (const variant of variants) { + for (const value of variant.values) { + // Generate CSS for each variant value + cssRules.push(`[data-ui-${variant.key}="${value}"] { + /* Styles for ${variant.key}=${value} */ +}`); + } + } + + const cssContent = cssRules.join('\n\n'); + writeFileSync('dist/ui-variants.css', cssContent); + + console.log('📝 Generated CSS with variant selectors'); +} + +/** + * Example of the transformation result + */ +const EXAMPLE_TRANSFORMATION = ` +// BEFORE: + + +// AFTER: + +`; + +console.log('Example transformation:', EXAMPLE_TRANSFORMATION); + +// CLI usage example +if (require.main === module) { + buildWithDataAttributes().catch(console.error); +} diff --git a/packages/core/src/postcss/coming-soon/collect-refs.cts b/packages/core/src/postcss/coming-soon/collect-refs.cts new file mode 100644 index 0000000..4650e04 --- /dev/null +++ b/packages/core/src/postcss/coming-soon/collect-refs.cts @@ -0,0 +1,189 @@ +import * as t from '@babel/types'; +import { NodePath } from '@babel/traverse'; +import * as babelTraverse from '@babel/traverse'; +import type { SetterMeta } from '../ast-parsing.cjs'; +const traverse = (babelTraverse as any).default; + +export interface RefLocation { + filePath: string; //'./src/components/ThemeToggle.tsx', + elementName: string; // 'button', + line: number; // 15 + column: number; // 8 + stateKey: string; // 'theme', + refProperty: string; // 'ref', +} + +/** + * Find all JSX attributes that reference setter.ref patterns + * @param ast - Parsed AST + * @param filePath - Current file path for location tracking + * @param setters - SetterMeta array from Pass 1 + * @returns RefLocation[] + */ +export function collectRefLocations(ast: t.File, filePath: string, setters: SetterMeta[]): RefLocation[] { + const refLocations: RefLocation[] = []; + + // Create a map of setter variable names to their state keys + const setterNameToStateKey = new Map(); + for (const setter of setters) { + setterNameToStateKey.set(setter.setterName, setter.stateKey); + } + + traverse(ast, { + JSXAttribute(path: NodePath) { + const { name, value } = path.node; + + // Must be a regular attribute (not spread) + if (!t.isJSXIdentifier(name)) return; + + // Must be exactly "ref" attribute + if (name.name !== 'ref') return; + + // Must be an expression container + if (!t.isJSXExpressionContainer(value)) return; + + // Must be exactly: setterFn.ref (no other variations allowed) + const expression = value.expression; + if (t.isMemberExpression(expression) && t.isIdentifier(expression.object) && t.isIdentifier(expression.property, { name: 'ref' })) { + const setterName = expression.object.name; + const stateKey = setterNameToStateKey.get(setterName); + + if (stateKey) { + // Find the JSX element name + const elementName = getJSXElementName(path); + const loc = path.node.loc; + + refLocations.push({ + filePath, + elementName, + line: loc?.start.line || 0, + column: loc?.start.column || 0, + stateKey, + refProperty: 'ref', // Always 'ref' now + }); + } + } + }, + }); + + return refLocations; +} + +/** + * Get the JSX element name from a JSX attribute path + */ +function getJSXElementName(attributePath: NodePath): string { + const jsxElement = attributePath.findParent((path) => path.isJSXElement()); + + if (jsxElement && jsxElement.isJSXElement()) { + const openingElement = jsxElement.node.openingElement; + const name = openingElement.name; + + if (t.isJSXIdentifier(name)) { + return name.name; + } else if (t.isJSXMemberExpression(name)) { + // Handle cases like + return getJSXMemberExpressionName(name); + } else if (t.isJSXNamespacedName(name)) { + // Handle cases like + return `${name.namespace.name}:${name.name.name}`; + } + } + + return 'unknown'; +} + +/** + * Convert JSX member expression to string (e.g., Component.SubComponent) + */ +function getJSXMemberExpressionName(expr: t.JSXMemberExpression): string { + const parts: string[] = []; + + let current: t.JSXMemberExpression | t.JSXIdentifier = expr; + while (t.isJSXMemberExpression(current)) { + if (t.isJSXIdentifier(current.property)) { + parts.unshift(current.property.name); + } + current = current.object; + } + + if (t.isJSXIdentifier(current)) { + parts.unshift(current.name); + } + + return parts.join('.'); +} + +/** + * Global ref location tracker for the entire app + */ +export class RefLocationTracker { + private refMap = new Map(); + + /** + * Add ref locations from a file + */ + addFile(filePath: string, refLocations: RefLocation[]): void { + this.refMap.set(filePath, refLocations); + } + + /** + * Get all ref locations for a specific state key + */ + getRefsByStateKey(stateKey: string): RefLocation[] { + const results: RefLocation[] = []; + + for (const locations of this.refMap.values()) { + results.push(...locations.filter((loc) => loc.stateKey === stateKey)); + } + + return results; + } + + /** + * Get all ref locations in a specific file + */ + getRefsByFile(filePath: string): RefLocation[] { + return this.refMap.get(filePath) || []; + } + + /** + * Get all ref locations for a specific element type + */ + getRefsByElement(elementName: string): RefLocation[] { + const results: RefLocation[] = []; + + for (const locations of this.refMap.values()) { + results.push(...locations.filter((loc) => loc.elementName === elementName)); + } + + return results; + } + + /** + * Get all ref locations in the entire app + */ + getAllRefs(): RefLocation[] { + const results: RefLocation[] = []; + + for (const locations of this.refMap.values()) { + results.push(...locations); + } + + return results; + } + + /** + * Clear all stored locations + */ + clear(): void { + this.refMap.clear(); + } + + /** + * Remove a specific file from tracking + */ + removeFile(filePath: string): void { + this.refMap.delete(filePath); + } +} diff --git a/packages/core/src/postcss/coming-soon/inject-attributes.ts b/packages/core/src/postcss/coming-soon/inject-attributes.ts new file mode 100644 index 0000000..b80df85 --- /dev/null +++ b/packages/core/src/postcss/coming-soon/inject-attributes.ts @@ -0,0 +1,207 @@ +import { parse } from '@babel/parser'; +import * as babelTraverse from '@babel/traverse'; +import { NodePath } from '@babel/traverse'; +import * as t from '@babel/types'; +import { generate } from '@babel/generator'; +import type { RefLocation, RefLocationTracker } from './collect-refs.cjs'; +import type { VariantData } from '../ast-parsing.cjs'; + +const traverse = (babelTraverse as any).default; + +export interface DataAttributeConfig { + /** Attribute name pattern. Use {stateKey} for substitution */ + attributeName: string; + /** Attribute value pattern. Use {stateKey}, {elementName}, {initialValue} for substitution */ + attributeValue?: string; + /** Whether to include the initial value in the attribute */ + includeInitialValue?: boolean; +} + +/** + * Default configuration for data attributes + */ +export const DEFAULT_DATA_ATTRIBUTE_CONFIG: DataAttributeConfig = { + attributeName: 'data-ui-{stateKey}', + attributeValue: '{initialValue}', // e.g., data-ui-theme="light" + includeInitialValue: true, +}; + +/** + * Inject data attributes into JSX elements based on ref locations + * @param sourceCode - Original source code + * @param refLocations - Array of ref locations from the ref tracker + * @param variantData - Variant data to get initial values + * @param config - Configuration for attribute generation + * @returns Modified source code with injected data attributes + */ +export function injectDataAttributes( + sourceCode: string, + refLocations: RefLocation[], + variantData: VariantData[], + config: DataAttributeConfig = DEFAULT_DATA_ATTRIBUTE_CONFIG +): string { + // Create a map of stateKey -> initial value for quick lookup + const initialValueMap = new Map(); + for (const variant of variantData) { + if (variant.initialValue) { + initialValueMap.set(variant.key, variant.initialValue); + } + } + + // Parse the source code + const ast = parse(sourceCode, { + sourceType: 'module', + plugins: ['jsx', 'typescript', 'decorators-legacy'], + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + }); + + // Create a map of line:column -> RefLocation for quick lookup + const locationMap = new Map(); + for (const refLoc of refLocations) { + const key = `${refLoc.line}:${refLoc.column}`; + locationMap.set(key, refLoc); + } + + // Traverse and inject data attributes + traverse(ast, { + JSXOpeningElement(path: NodePath) { + const loc = path.node.loc; + if (!loc) return; + + const key = `${loc.start.line}:${loc.start.column}`; + const refLocation = locationMap.get(key); + + if (refLocation) { + // Check if the element already has a ref={setterFn.ref} attribute + const hasTargetRef = path.node.attributes.some((attr) => { + if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: 'ref' }) && t.isJSXExpressionContainer(attr.value)) { + const expr = attr.value.expression; + return t.isMemberExpression(expr) && t.isIdentifier(expr.property, { name: 'ref' }); + } + return false; + }); + + if (hasTargetRef) { + // Generate the data attribute + const dataAttr = createDataAttribute(refLocation, initialValueMap, config); + if (dataAttr) { + // Check if this data attribute already exists + const existingAttr = path.node.attributes.find( + (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === dataAttr.name.name + ); + + if (!existingAttr) { + path.node.attributes.push(dataAttr); + } + } + } + } + }, + }); + + // Generate the modified code + const result = generate(ast, { retainLines: true, compact: false }); + + return result.code; +} + +/** + * Create a JSX data attribute based on ref location and config + */ +function createDataAttribute(refLocation: RefLocation, initialValueMap: Map, config: DataAttributeConfig): t.JSXAttribute | null { + const { stateKey, elementName } = refLocation; + const initialValue = initialValueMap.get(stateKey) || ''; + + // Generate attribute name + const attributeName = config.attributeName.replace('{stateKey}', stateKey).replace('{elementName}', elementName); + + // Generate attribute value + let attributeValue = ''; + if (config.attributeValue) { + attributeValue = config.attributeValue.replace('{stateKey}', stateKey).replace('{elementName}', elementName).replace('{initialValue}', initialValue); + } else if (config.includeInitialValue) { + attributeValue = initialValue; + } + + // Create the JSX attribute + const name = t.jsxIdentifier(attributeName); + const value = attributeValue ? t.stringLiteral(attributeValue) : null; + + return t.jsxAttribute(name, value); +} + +/** + * Batch process multiple files + * @param fileContents - Map of filePath -> source code + * @param refTracker - Global ref tracker with all locations + * @param variantData - All variant data + * @param config - Data attribute configuration + * @returns Map of filePath -> modified source code + */ +export function batchInjectDataAttributes( + fileContents: Map, + refTracker: RefLocationTracker, + variantData: VariantData[], + config: DataAttributeConfig = DEFAULT_DATA_ATTRIBUTE_CONFIG +): Map { + const results = new Map(); + + for (const [filePath, sourceCode] of fileContents) { + const refLocations = refTracker.getRefsByFile(filePath); + + if (refLocations.length > 0) { + const modifiedCode = injectDataAttributes(sourceCode, refLocations, variantData, config); + results.set(filePath, modifiedCode); + } else { + // No refs in this file, return unchanged + results.set(filePath, sourceCode); + } + } + + return results; +} + +/** + * Webpack/Vite plugin helper + * Transform source code during build + */ +export function createDataAttributeTransformer( + refTracker: RefLocationTracker, + variantData: VariantData[], + config: DataAttributeConfig = DEFAULT_DATA_ATTRIBUTE_CONFIG +) { + return function transformCode(sourceCode: string, filePath: string): string { + const refLocations = refTracker.getRefsByFile(filePath); + + if (refLocations.length === 0) { + return sourceCode; // No changes needed + } + + return injectDataAttributes(sourceCode, refLocations, variantData, config); + }; +} + +// Example usage configurations: + +/** + * Configuration for semantic data attributes + */ +export const SEMANTIC_CONFIG: DataAttributeConfig = { attributeName: 'data-ui-{stateKey}', attributeValue: '{initialValue}', includeInitialValue: true }; +// Result: +
      + Theme: Dark Light +
      +
      + `; + // should return theme-three-light, theme-three-dark + const result = scanVariantTokens(src, new Set(['theme-three'])); + assert.deepStrictEqual(result.get('theme-three'), new Set(['light', 'dark'])); +}); diff --git a/packages/core/src/postcss/vite.d.ts b/packages/core/src/postcss/vite.d.ts deleted file mode 100644 index 8a0e475..0000000 --- a/packages/core/src/postcss/vite.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Plugin } from 'vite'; - -export interface ZeroUIOptions { - /** reserved for future options */ -} - -declare function zeroUI(options?: ZeroUIOptions): Plugin; - -export default zeroUI; diff --git a/packages/core/src/postcss/vite.js b/packages/core/src/postcss/vite.ts similarity index 72% rename from packages/core/src/postcss/vite.js rename to packages/core/src/postcss/vite.ts index 21f1602..5d4562a 100644 --- a/packages/core/src/postcss/vite.js +++ b/packages/core/src/postcss/vite.ts @@ -1,17 +1,17 @@ -//src/postcss/vite.js +//src/postcss/vite.ts // vite-plugin-zero-ui.ts (ESM wrapper) import tailwindcss from '@tailwindcss/postcss'; +import zeroUiPostcss from './index.cjs'; import path from 'path'; export default function zeroUI() { return { name: 'vite-zero-ui', enforce: 'pre', // run before other Vite plugins async config() { - const { default: zeroUiPostcss } = await import('./index.cjs'); - return { css: { postcss: { plugins: [zeroUiPostcss(), tailwindcss] } } }; + return { css: { postcss: { plugins: [zeroUiPostcss, tailwindcss()] } } }; }, - async transformIndexHtml(html) { + async transformIndexHtml(html: string): Promise { const { bodyAttributes } = await import(path.join(process.cwd(), './.zero-ui/attributes.js')); const attrs = Object.entries(bodyAttributes) .map(([k, v]) => `${k}="${v}"`) diff --git a/packages/core/src/utilities.ts b/packages/core/src/utilities.ts new file mode 100644 index 0000000..dfb4c68 --- /dev/null +++ b/packages/core/src/utilities.ts @@ -0,0 +1,31 @@ +import path from 'path'; +import fs from 'fs'; +import os from 'os'; + +// Helper to create temp directory and run test +export async function runTest(files: Record, callback: () => Promise) { + const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-ui-test-ast')); + const originalCwd = process.cwd(); + try { + process.chdir(testDir); + // Create test files + for (const [filePath, content] of Object.entries(files)) { + const dir = path.dirname(filePath); + if (dir !== '.') { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(filePath, content); + } + + // Run assertions + await callback(); + } finally { + process.chdir(originalCwd); + // Clean up any generated files in the package directory + fs.rmSync(testDir, { recursive: true, force: true }); + } +} + +export function readFile(path: string) { + return fs.readFileSync(path, 'utf-8'); +} diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json new file mode 100644 index 0000000..08de3ef --- /dev/null +++ b/packages/core/tsconfig.build.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + /* — compile exactly one file — */ + "include": ["src/**/*"], + "exclude": ["src/postcss/coming-soon", "src/**/*.test.ts", "./utilities.ts"], + /* — compiler output — */ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "rootDir": "./src", // keeps relative paths clean + "outDir": "./dist", // compiled JS → dist/ + "composite": false, // flip to true when we add references + "incremental": true, // speeds up "one-file" rebuilds + "strict": true, // enable all strict type-checking options + "skipLibCheck": true // Hides all errors coming from node_modules + } +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..bb2e510 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.base.json", + /* — compile exactly one file — */ + "include": [ + "src/**/*" + ], + "exclude": [ + "src/postcss/coming-soon", + "node_modules", + ], + /* — compiler output — */ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "rootDir": "./src", // keeps relative paths clean + "outDir": "./dist", // compiled JS → dist/ + "composite": false, // flip to true when we add references + "incremental": true, // speeds up "one-file" rebuilds + "strict": true, // enable all strict type-checking options + "skipLibCheck": true // Hides all errors coming from node_modules + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e21544b..1e22a56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,18 @@ importers: '@eslint/js': specifier: ^9.0.0 version: 9.29.0 + '@types/node': + specifier: ^24.0.7 + version: 24.0.7 + esbuild: + specifier: ^0.25.5 + version: 0.25.5 eslint: specifier: ^9.0.0 version: 9.29.0(jiti@2.4.2) + eslint-plugin-import: + specifier: ^2.32.0 + version: 2.32.0(eslint@9.29.0(jiti@2.4.2)) eslint-plugin-node: specifier: ^11.1.0 version: 11.1.0(eslint@9.29.0(jiti@2.4.2)) @@ -23,6 +32,12 @@ importers: release-please: specifier: ^17.0.0 version: 17.1.0 + tsx: + specifier: ^4.20.3 + version: 4.20.3 + typescript: + specifier: ^5.8.3 + version: 5.8.3 examples/demo: dependencies: @@ -31,7 +46,7 @@ importers: version: 0.1.0(@tailwindcss/postcss@4.1.10)(postcss@8.5.6)(react@19.1.0)(tailwindcss@4.1.10) '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(next@15.3.3(@playwright/test@1.53.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + version: 1.5.0(next@15.3.5(@playwright/test@1.53.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -39,8 +54,8 @@ importers: specifier: 12.18.1 version: 12.18.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: - specifier: 15.3.3 - version: 15.3.3(@playwright/test@1.53.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^15.3.5 + version: 15.3.5(@playwright/test@1.53.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 @@ -102,6 +117,9 @@ importers: '@tailwindcss/postcss': specifier: ^4.1.10 version: 4.1.10 + fast-glob: + specifier: ^3.3.3 + version: 3.3.3 postcss: specifier: ^8.5.5 version: 8.5.6 @@ -112,9 +130,30 @@ importers: specifier: ^4.1.10 version: 4.1.10 devDependencies: + '@babel/code-frame': + specifier: ^7.27.1 + version: 7.27.1 '@playwright/test': specifier: ^1.53.0 version: 1.53.1 + '@types/babel__code-frame': + specifier: ^7.0.6 + version: 7.0.6 + '@types/babel__generator': + specifier: ^7.27.0 + version: 7.27.0 + '@types/babel__traverse': + specifier: ^7.20.7 + version: 7.20.7 + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 + lru-cache: + specifier: ^10.4.3 + version: 10.4.3 + tsx: + specifier: ^4.20.3 + version: 4.20.3 packages: @@ -165,6 +204,156 @@ packages: '@emnapi/runtime@1.4.3': resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -384,57 +573,69 @@ packages: peerDependencies: jsep: ^0.4.0||^1.0.0 - '@next/env@15.3.3': - resolution: {integrity: sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==} + '@next/env@15.3.5': + resolution: {integrity: sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g==} - '@next/swc-darwin-arm64@15.3.3': - resolution: {integrity: sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==} + '@next/swc-darwin-arm64@15.3.5': + resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.3.3': - resolution: {integrity: sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==} + '@next/swc-darwin-x64@15.3.5': + resolution: {integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.3.3': - resolution: {integrity: sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==} + '@next/swc-linux-arm64-gnu@15.3.5': + resolution: {integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.3.3': - resolution: {integrity: sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==} + '@next/swc-linux-arm64-musl@15.3.5': + resolution: {integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.3.3': - resolution: {integrity: sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==} + '@next/swc-linux-x64-gnu@15.3.5': + resolution: {integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.3.3': - resolution: {integrity: sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==} + '@next/swc-linux-x64-musl@15.3.5': + resolution: {integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.3.3': - resolution: {integrity: sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==} + '@next/swc-win32-arm64-msvc@15.3.5': + resolution: {integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.3.3': - resolution: {integrity: sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==} + '@next/swc-win32-x64-msvc@15.3.5': + resolution: {integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@octokit/auth-token@4.0.0': resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} engines: {node: '>= 18'} @@ -501,6 +702,9 @@ packages: react: '>=16.8.0' tailwindcss: ^4.1.10 + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -595,18 +799,33 @@ packages: '@tailwindcss/postcss@4.1.10': resolution: {integrity: sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ==} + '@types/babel__code-frame@7.0.6': + resolution: {integrity: sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} '@types/node@20.19.2': resolution: {integrity: sha512-9pLGGwdzOUBDYi0GNjM97FIA+f92fqSke6joWeBjWXllfNxZBs7qeMF7tvtOIsbY45xkWkxrdwUfUf3MnQa9gA==} + '@types/node@24.0.7': + resolution: {integrity: sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -688,16 +907,48 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + arrify@1.0.1: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -713,10 +964,26 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -806,9 +1073,29 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + dateformat@3.0.3: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -829,6 +1116,14 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} @@ -848,6 +1143,10 @@ packages: resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} engines: {node: '>=0.3.1'} + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -865,6 +1164,10 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -879,6 +1182,39 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -891,12 +1227,46 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + eslint-plugin-es@3.0.1: resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} engines: {node: '>=8.10.0'} peerDependencies: eslint: '>=4.19.1' + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint-plugin-node@11.1.0: resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} engines: {node: '>=8.10.0'} @@ -956,12 +1326,19 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -970,6 +1347,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -985,6 +1366,10 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + framer-motion@12.19.2: resolution: {integrity: sha512-0cWMLkYr+i0emeXC4hkLF+5aYpzo32nRdQ0D/5DI460B3O7biQ3l2BpDzIGsAHYuZ0fpBP0DC8XBkVf6RPAlZw==} peerDependencies: @@ -1007,13 +1392,44 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -1030,6 +1446,14 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1042,10 +1466,29 @@ packages: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -1092,28 +1535,84 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-obj@2.0.0: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} @@ -1122,6 +1621,45 @@ packages: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1160,6 +1698,10 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + jsonpath-plus@10.3.0: resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==} engines: {node: '>=18.0.0'} @@ -1257,6 +1799,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -1272,10 +1817,22 @@ packages: resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} engines: {node: '>=8'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + meow@8.1.2: resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} engines: {node: '>=10'} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -1345,8 +1902,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@15.3.3: - resolution: {integrity: sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==} + next@15.3.5: + resolution: {integrity: sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -1379,6 +1936,30 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1386,6 +1967,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -1438,6 +2023,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + playwright-core@1.53.1: resolution: {integrity: sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==} engines: {node: '>=18'} @@ -1448,6 +2037,10 @@ packages: engines: {node: '>=18'} hasBin: true + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -1469,6 +2062,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-lru@4.0.1: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} @@ -1494,6 +2090,14 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + regexpp@3.2.0: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} engines: {node: '>=8'} @@ -1511,6 +2115,9 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -1520,6 +2127,25 @@ packages: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -1536,6 +2162,18 @@ packages: engines: {node: '>=10'} hasBin: true + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + sharp@0.34.2: resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1548,6 +2186,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -1574,6 +2228,10 @@ packages: split@1.0.1: resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -1582,10 +2240,26 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -1629,13 +2303,25 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1656,6 +2342,22 @@ packages: resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} engines: {node: '>=14.16'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + typescript@4.9.5: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} @@ -1671,9 +2373,16 @@ packages: engines: {node: '>=0.8.0'} hasBin: true + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + unist-util-is@4.1.0: resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} @@ -1692,6 +2401,22 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1810,9 +2535,84 @@ snapshots: unist-util-visit: 2.0.3 unist-util-visit-parents: 3.1.1 - '@emnapi/runtime@1.4.3': - dependencies: - tslib: 2.8.1 + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': optional: true '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0(jiti@2.4.2))': @@ -1994,32 +2794,44 @@ snapshots: dependencies: jsep: 1.4.0 - '@next/env@15.3.3': {} + '@next/env@15.3.5': {} - '@next/swc-darwin-arm64@15.3.3': + '@next/swc-darwin-arm64@15.3.5': optional: true - '@next/swc-darwin-x64@15.3.3': + '@next/swc-darwin-x64@15.3.5': optional: true - '@next/swc-linux-arm64-gnu@15.3.3': + '@next/swc-linux-arm64-gnu@15.3.5': optional: true - '@next/swc-linux-arm64-musl@15.3.3': + '@next/swc-linux-arm64-musl@15.3.5': optional: true - '@next/swc-linux-x64-gnu@15.3.3': + '@next/swc-linux-x64-gnu@15.3.5': optional: true - '@next/swc-linux-x64-musl@15.3.3': + '@next/swc-linux-x64-musl@15.3.5': optional: true - '@next/swc-win32-arm64-msvc@15.3.3': + '@next/swc-win32-arm64-msvc@15.3.5': optional: true - '@next/swc-win32-x64-msvc@15.3.3': + '@next/swc-win32-x64-msvc@15.3.5': optional: true + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + '@octokit/auth-token@4.0.0': {} '@octokit/core@5.2.1': @@ -2100,6 +2912,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@rtsao/scc@1.1.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -2178,16 +2992,32 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.10 + '@types/babel__code-frame@7.0.6': {} + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.27.6 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.27.6 + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} + '@types/json5@0.0.29': {} + '@types/minimist@1.2.5': {} '@types/node@20.19.2': dependencies: undici-types: 6.21.0 + '@types/node@24.0.7': + dependencies: + undici-types: 7.8.0 + '@types/normalize-package-data@2.4.4': {} '@types/npm-package-arg@6.1.4': {} @@ -2208,9 +3038,9 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@vercel/analytics@1.5.0(next@15.3.3(@playwright/test@1.53.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + '@vercel/analytics@1.5.0(next@15.3.5(@playwright/test@1.53.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': optionalDependencies: - next: 15.3.3(@playwright/test@1.53.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.5(@playwright/test@1.53.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 '@xmldom/xmldom@0.8.10': {} @@ -2238,14 +3068,70 @@ snapshots: argparse@2.0.1: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + array-ify@1.0.0: {} + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + arrify@1.0.1: {} + async-function@1.0.0: {} + async-retry@1.3.3: dependencies: retry: 0.13.1 + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + balanced-match@1.0.2: {} before-after-hook@2.2.3: {} @@ -2261,10 +3147,31 @@ snapshots: dependencies: balanced-match: 1.0.2 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} camelcase-keys@6.2.2: @@ -2372,8 +3279,30 @@ snapshots: csstype@3.1.3: {} + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + dateformat@3.0.3: {} + debug@3.2.7: + dependencies: + ms: 2.1.3 + debug@4.4.1: dependencies: ms: 2.1.3 @@ -2387,6 +3316,18 @@ snapshots: deep-is@0.1.4: {} + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + deprecation@2.3.1: {} detect-indent@6.1.0: {} @@ -2397,6 +3338,10 @@ snapshots: diff@7.0.0: {} + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -2419,6 +3364,12 @@ snapshots: dependencies: is-obj: 2.0.0 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + emoji-regex@8.0.0: {} enhanced-resolve@5.18.2: @@ -2432,18 +3383,172 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.29.0(jiti@2.4.2)): + dependencies: + debug: 3.2.7 + optionalDependencies: + eslint: 9.29.0(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + eslint-plugin-es@3.0.1(eslint@9.29.0(jiti@2.4.2)): dependencies: eslint: 9.29.0(jiti@2.4.2) eslint-utils: 2.1.0 regexpp: 3.2.0 + eslint-plugin-import@2.32.0(eslint@9.29.0(jiti@2.4.2)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.29.0(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.29.0(jiti@2.4.2)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-node@11.1.0(eslint@9.29.0(jiti@2.4.2)): dependencies: eslint: 9.29.0(jiti@2.4.2) @@ -2531,10 +3636,22 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -2543,6 +3660,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -2560,6 +3681,10 @@ snapshots: flatted@3.3.3: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + framer-motion@12.19.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: motion-dom: 12.19.0 @@ -2574,10 +3699,56 @@ snapshots: fsevents@2.3.2: optional: true + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -2595,6 +3766,13 @@ snapshots: globals@14.0.0: {} + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + graceful-fs@4.2.11: {} handlebars@4.7.8: @@ -2608,8 +3786,24 @@ snapshots: hard-rejection@2.1.0: {} + has-bigints@1.1.0: {} + has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -2654,27 +3848,132 @@ snapshots: inherits@2.0.4: {} + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} is-arrayish@0.3.2: optional: true + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + is-core-module@2.16.1: dependencies: hasown: 2.0.2 + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-extglob@2.1.1: {} + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + is-obj@2.0.0: {} is-plain-obj@1.1.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + isexe@2.0.0: {} jiti@2.4.2: {} @@ -2699,6 +3998,10 @@ snapshots: json-stringify-safe@5.0.1: {} + json5@1.0.2: + dependencies: + minimist: 1.2.8 + jsonpath-plus@10.3.0: dependencies: '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) @@ -2775,6 +4078,8 @@ snapshots: lodash.merge@4.6.2: {} + lru-cache@10.4.3: {} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -2787,6 +4092,8 @@ snapshots: map-obj@4.3.0: {} + math-intrinsics@1.1.0: {} + meow@8.1.2: dependencies: '@types/minimist': 1.2.5 @@ -2801,6 +4108,13 @@ snapshots: type-fest: 0.18.1 yargs-parser: 20.2.9 + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + min-indent@1.0.1: {} minimatch@3.1.2: @@ -2851,9 +4165,9 @@ snapshots: neo-async@2.6.2: {} - next@15.3.3(@playwright/test@1.53.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.3.5(@playwright/test@1.53.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@next/env': 15.3.3 + '@next/env': 15.3.5 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 @@ -2863,14 +4177,14 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.3.3 - '@next/swc-darwin-x64': 15.3.3 - '@next/swc-linux-arm64-gnu': 15.3.3 - '@next/swc-linux-arm64-musl': 15.3.3 - '@next/swc-linux-x64-gnu': 15.3.3 - '@next/swc-linux-x64-musl': 15.3.3 - '@next/swc-win32-arm64-msvc': 15.3.3 - '@next/swc-win32-x64-msvc': 15.3.3 + '@next/swc-darwin-arm64': 15.3.5 + '@next/swc-darwin-x64': 15.3.5 + '@next/swc-linux-arm64-gnu': 15.3.5 + '@next/swc-linux-arm64-musl': 15.3.5 + '@next/swc-linux-x64-gnu': 15.3.5 + '@next/swc-linux-x64-musl': 15.3.5 + '@next/swc-win32-arm64-msvc': 15.3.5 + '@next/swc-win32-x64-msvc': 15.3.5 '@playwright/test': 1.53.1 sharp: 0.34.2 transitivePeerDependencies: @@ -2900,6 +4214,39 @@ snapshots: dependencies: boolbase: 1.0.0 + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -2913,6 +4260,12 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -2956,6 +4309,8 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.1: {} + playwright-core@1.53.1: {} playwright@1.53.1: @@ -2964,6 +4319,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} + postcss@8.4.31: dependencies: nanoid: 3.3.11 @@ -2982,6 +4339,8 @@ snapshots: punycode@2.3.1: {} + queue-microtask@1.2.3: {} + quick-lru@4.0.1: {} react-dom@19.1.0(react@19.1.0): @@ -3009,6 +4368,26 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + regexpp@3.2.0: {} release-please@17.1.0: @@ -3051,6 +4430,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -3059,6 +4440,31 @@ snapshots: retry@0.13.1: {} + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + scheduler@0.26.0: {} semver@5.7.2: {} @@ -3067,6 +4473,28 @@ snapshots: semver@7.7.2: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + sharp@0.34.2: dependencies: color: 4.2.3 @@ -3102,6 +4530,34 @@ snapshots: shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 @@ -3129,6 +4585,11 @@ snapshots: dependencies: through: 2.3.8 + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + streamsearch@1.1.0: {} string-width@4.2.3: @@ -3137,10 +4598,35 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 + strip-bom@3.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -3173,10 +4659,28 @@ snapshots: through@2.3.8: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + trim-newlines@3.0.1: {} + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@2.8.1: {} + tsx@4.20.3: + dependencies: + esbuild: 0.25.5 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -3189,6 +4693,39 @@ snapshots: type-fest@3.13.1: {} + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + typescript@4.9.5: {} typescript@5.8.3: {} @@ -3196,8 +4733,17 @@ snapshots: uglify-js@3.19.3: optional: true + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + undici-types@6.21.0: {} + undici-types@7.8.0: {} + unist-util-is@4.1.0: {} unist-util-visit-parents@3.1.1: @@ -3222,6 +4768,47 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/scripts/install-local-tarball.js b/scripts/install-local-tarball.js index d545fda..ecebb46 100644 --- a/scripts/install-local-tarball.js +++ b/scripts/install-local-tarball.js @@ -4,7 +4,7 @@ import { execSync } from 'node:child_process'; const dist = join(process.cwd(), 'dist'); const pkg = readdirSync(dist) - .filter(f => f.endsWith('.tgz')) + .filter((f) => f.endsWith('.tgz')) .sort((a, b) => statSync(join(dist, b)).mtimeMs - statSync(join(dist, a)).mtimeMs)[0]; const fixtures = ['packages/core/__tests__/fixtures/next', 'packages/core/__tests__/fixtures/vite']; diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..53f743b --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,32 @@ +{ + // Shared defaults + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + // "verbatimModuleSyntax": true, + /* Gradual-migration flags */ + "allowJs": true, // <- JS still builds + "checkJs": false, // <- flip to true later for type-checking JS + "strict": true, // <- tighten as we go + "declaration": true, // <- emit declaration files + "emitDeclarationOnly": false, // <- emit declaration files only + "outDir": "dist", // <- output directory + "skipLibCheck": true, // <- skip type checking of declaration files in node_modules + /* Maps for monorepo imports like "@react-zero-ui/core" */ + "composite": true, + "rootDir": "./", + "forceConsistentCasingInFileNames": true + }, + "exclude": [ + "dist", + "**/*.tsx", + "**/*.jsx", + "**/*.test.ts", + "pnpm-lock.yaml", + "pnpm-workspace.yaml", + "examples/**", + "**/fixtures/**", + "**/coming-soon/**" + ] +} \ No newline at end of file