diff --git a/.changeset/config.json b/.changeset/config.json index 87499f25..001a9858 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,5 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["onu-web"] + "ignore": ["onu-web", "onu-nextjs-fixture"] } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5041de7e..176cc6f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,3 +68,6 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # npm only signs provenance for public repositories. NPM_CONFIG_PROVENANCE: ${{ !github.event.repository.private }} + - name: Check the packages carry provenance + if: ${{ !github.event.repository.private }} + run: node scripts/check-provenance.mjs "${GITHUB_REF_NAME#v}" diff --git a/.github/workflows/ssr.yml b/.github/workflows/ssr.yml new file mode 100644 index 00000000..dde21728 --- /dev/null +++ b/.github/workflows/ssr.yml @@ -0,0 +1,52 @@ +name: Server rendering + +# Builds a small Next.js app against the workspace copy of @javaabu/onu, then checks its +# server-rendered HTML and hydration in English and Dhivehi (R16.1). + +on: + push: + branches: [main] + paths: + - packages/onu/** + - e2e/nextjs/** + - pnpm-lock.yaml + - pnpm-workspace.yaml + - .github/workflows/ssr.yml + pull_request: + paths: + - packages/onu/** + - e2e/nextjs/** + - pnpm-lock.yaml + - pnpm-workspace.yaml + - .github/workflows/ssr.yml + workflow_dispatch: + +concurrency: + group: ssr-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + NEXT_TELEMETRY_DISABLED: 1 + +jobs: + nextjs: + name: Next.js (build, server HTML, hydration) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v5 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install --with-deps chromium + - name: Build the library + run: pnpm build + - name: Build the app + run: pnpm --filter onu-nextjs-fixture build + - name: Check the server HTML and hydration + run: pnpm --filter onu-nextjs-fixture test:ssr diff --git a/.prettierignore b/.prettierignore index 1091ee7c..e325b341 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,6 @@ dist +.next +next-env.d.ts coverage pnpm-lock.yaml packages/onu/src/ui diff --git a/AGENTS.md b/AGENTS.md index e91bac57..dab625a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ Onu (`@javaabu/onu`) is Javaabu's React UI library: shadcn/ui (Radix base) and T | `web` | Docs and demo site: `src/showcase//.demo.tsx`, `src/pages//.page.tsx` | | `docs` | Markdown docs, one page per component (`docs//.md`) | | `scripts/llms.mjs` | Builds `llms.txt`, `llms-full.txt` and `docs/*.md` for AI assistants from `docs/` | -| `e2e`, `figma` | Playwright tests, Figma plugin | +| `e2e`, `figma` | Playwright tests and the Next.js server-rendering fixture (`e2e/nextjs`), Figma plugin | ## Commands diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee12c34d..cc24c14c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,6 +62,8 @@ E2E (Playwright) runs in two tiers: Run either locally with `pnpm e2e:chromium`, adding the two spec files for the smoke tier. +Server rendering has its own check. The **Server rendering** workflow builds the Next.js app in `e2e/nextjs` against the workspace copy of Onu, then checks its server HTML and hydration in English and Dhivehi, when a pull request touches `packages/onu/**` or `e2e/nextjs/**`. Run it locally with `pnpm build && pnpm --filter onu-nextjs-fixture build && pnpm --filter onu-nextjs-fixture test:ssr`. + The Chromium job compares a desktop screenshot of every component demo, in light LTR and dark RTL, with baselines stored as the `visual-baselines` Actions artifact (not in git). After an intended visual change, or when you add a component, run **E2E** by hand with **Update the visual baselines** ticked. Look at the screenshots in that run's `visual-baselines` artifact before relying on them. Baselines are kept for 90 days (about 90 MB). If none are stored (the first run, or 90 days after the last update), the run takes new ones instead of comparing. ```bash @@ -99,4 +101,4 @@ To release: The **Release** workflow runs only for `v*` tags. It checks that the tagged commit is on `main` and that every package's version and the plugin's match the tag, then builds and publishes to npm (versions already on npm are skipped). Tags with a pre-release suffix (`v1.0.0-beta.1`) publish under the `next` dist-tag. -Publishing needs an `NPM_TOKEN` repository secret: an npm granular access token with read and write access to the `@javaabu` scope and "bypass 2FA" allowed, from an account that can publish to `@javaabu`. Write tokens expire after about 90 days. Provenance is signed only once the repository is public. After the first release, you can switch to npm trusted publishing (each package's settings on npmjs.com, pointing at `release.yml`) and remove the token. +Publishing needs an `NPM_TOKEN` repository secret: an npm granular access token with read and write access to the `@javaabu` scope and "bypass 2FA" allowed, from an account that can publish to `@javaabu`. Write tokens expire after about 90 days. Provenance is signed only once the repository is public. Set up npm trusted publishing for each package (its settings on npmjs.com, pointing at `release.yml`): without it, the OIDC exchange fails with only a warning and packages publish with the token and no attestation. The workflow's last step checks for attestations and fails if any are missing; by then the packages are already on npm, so a failure means fixing the setup for the next release, not republishing. Once trusted publishing works, remove the token and the workflow's "Check the npm token" step. diff --git a/docs/display-and-feedback/countdown.md b/docs/display-and-feedback/countdown.md index b7028c59..ede66578 100644 --- a/docs/display-and-feedback/countdown.md +++ b/docs/display-and-feedback/countdown.md @@ -19,7 +19,7 @@ import { Countdown } from "@javaabu/onu" | `to` | `Date \| string \| number` | | Target date and time | | `onComplete` | `() => void` | | Called once at zero (straight away if `to` is already past) | | `variant` | `'boxes' \| 'inline'` | `'boxes'` | Boxes with labels, or `00:00:00` | -| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Number size (boxes) | +| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Number size (boxes) | | `showDays` | `boolean \| 'auto'` | `'auto'` | `auto` hides days when there are none left (hours then count past 24) | | `showSeconds` | `boolean` | `true` | Show seconds | | `announceEvery` | `number` | `60` | Seconds between screen reader announcements | diff --git a/docs/display-and-feedback/icon-text.md b/docs/display-and-feedback/icon-text.md index 65db3c4f..a75459f0 100644 --- a/docs/display-and-feedback/icon-text.md +++ b/docs/display-and-feedback/icon-text.md @@ -21,7 +21,7 @@ import { IconText } from "@javaabu/onu" | `icon` | `string \| ReactNode` | | Semantic icon name or a node | | `iconPosition` | `'start' \| 'end'` | `'start'` | Icon side | | `iconLabel` | `string` | | Makes the icon meaningful (`role="img"`) | -| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Text and icon size | +| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Text and icon size | | `tone` | `'neutral' \| 'muted' \| 'primary' \| 'success' \| 'warning' \| 'danger' \| 'info'` | `'neutral'` | Colour | | `asChild` | `boolean` | `false` | Render your element (e.g. a link) | | `classNames` | `{ icon, text }` | | Classes for each part | diff --git a/docs/display-and-feedback/status-badge.md b/docs/display-and-feedback/status-badge.md index f0c7995b..c9191a73 100644 --- a/docs/display-and-feedback/status-badge.md +++ b/docs/display-and-feedback/status-badge.md @@ -49,7 +49,7 @@ Unknown statuses fall back to the `neutral` tone with the raw value as the label | `statusMap` | `Record` | | `{ tone, label?, icon? }` per status | | `children` | `ReactNode` | | Label override | | `icon` | `string \| ReactNode \| false` | | Icon override; `false` hides it | -| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size | +| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Size | | `asChild` | `boolean` | `false` | Render your element (e.g. a link) with the badge styles | | `classNames` | `{ root, icon }` | | Classes for each part | diff --git a/docs/layout-and-navigation/hero.md b/docs/layout-and-navigation/hero.md index 9b2815c0..45f74a27 100644 --- a/docs/layout-and-navigation/hero.md +++ b/docs/layout-and-navigation/hero.md @@ -52,7 +52,7 @@ import { Button, Hero, SearchBox } from "@javaabu/onu" | `media` | `ReactNode` | | Image or illustration beside the text | | `backgroundImage` | `string \| { src, alt?, srcSet?, sizes?, position? }` | | Photo behind the content, with a token scrim | | `tone` | `"plain" \| "muted" \| "primary"` | `"plain"` | Surface colour, or scrim colour over a background image | -| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Padding and title size (`text-fluid-3xl` / `4xl` / `5xl`) | +| `size` | `"sm" \| "default" \| "lg"` | `"default"` | Padding and title size (`text-fluid-3xl` / `4xl` / `5xl`) | | `align` | `"start" \| "center"` | `"start"` | Text alignment (centred only without `media`) | | `headingLevel` | `1 \| 2 \| 3` | `1` | Heading element | | `maxWidth` | `"3xl" \| "4xl" \| "5xl" \| "6xl" \| "7xl" \| "none"` | `"6xl"` | Width of the centred content container | diff --git a/docs/loading/progress-bar.md b/docs/loading/progress-bar.md index 8eb8f202..398eacb8 100644 --- a/docs/loading/progress-bar.md +++ b/docs/loading/progress-bar.md @@ -26,7 +26,7 @@ import { ProgressBar } from "@javaabu/onu" | `showValue` | `boolean` | with label | Show the value text | | `formatValue` | `(value, max) => string` | percent | Custom value text, also used for `aria-valuetext` | | `color` | `'primary' \| 'success' \| 'warning' \| 'destructive' \| 'info' \| 'muted'` | `'primary'` | Indicator colour | -| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Track height | +| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Track height | | `aria-label` | `string` | "Progress" | Name when there is no `label` | | `className` | `string` | | Classes for the wrapper | | `classNames` | `{ header, label, value, track, indicator }` | | Classes for each part | diff --git a/docs/maps/map-legend-swatch.md b/docs/maps/map-legend-swatch.md index f87c801e..e327d51c 100644 --- a/docs/maps/map-legend-swatch.md +++ b/docs/maps/map-legend-swatch.md @@ -29,7 +29,7 @@ Shapes: `circle`, `square`, `pin`, `area`, `line`, `dashed`, `gradient` (with `c | `tone` | `MapTone` | `"primary"` | Theme colour | | `shape` | `MapLegendShape` | `"circle"` | Sample shape | | `colors` | `string[]` | | Gradient stops (CSS colours or `toneVar(tone)`) | -| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size (`mapLegendSwatchVariants`) | +| `size` | `"sm" \| "default" \| "lg"` | `"default"` | Size (`mapLegendSwatchVariants`) | `MapLegend`: diff --git a/docs/overlays/bottom-sheet.md b/docs/overlays/bottom-sheet.md index fc663676..3d163be1 100644 --- a/docs/overlays/bottom-sheet.md +++ b/docs/overlays/bottom-sheet.md @@ -56,7 +56,7 @@ Snap points are fractions of the screen height (`0.4`) or pixel strings (`"320px | `handleOnly` | `boolean` | `false` | Only the handle starts a drag | | `handle` | `boolean` | `true` | Show the drag handle | | `closeButton` | `boolean` | `true` | Show the × button | -| `size` | `'sm' \| 'md' \| 'lg' \| 'full'` | `'md'` | Max width on wider screens (full width on phones) | +| `size` | `'sm' \| 'default' \| 'lg' \| 'full'` | `'default'` | Max width on wider screens (full width on phones) | | `open`, `defaultOpen`, `onOpenChange`, `modal` | | | Dialog state | | `classNames` | `{ handle, header, title, description, body, footer, close, expand }` | | Classes for each part | diff --git a/docs/overlays/slide-over.md b/docs/overlays/slide-over.md index e9dfdbaa..566a2a1a 100644 --- a/docs/overlays/slide-over.md +++ b/docs/overlays/slide-over.md @@ -30,7 +30,7 @@ import { Button, SlideOver, SlideOverClose } from "@javaabu/onu" ## Sides and sizes - `side="end"` (default) is on the right in LTR and on the left in RTL; `side="start"` is the opposite. -- `size`: `sm`, `md` (default), `lg`, `xl` or `full`. On phones the panel leaves a 2.5rem strip of the page visible (except `full`). +- `size`: `sm`, `default`, `lg`, `xl` or `full`. On phones the panel leaves a 2.5rem strip of the page visible (except `full`). ## Composing @@ -45,7 +45,7 @@ import { Button, SlideOver, SlideOverClose } from "@javaabu/onu" | `footer` | `ReactNode` | | Sticky action bar | | `trigger` | `ReactNode` | | Element that opens the panel | | `side` | `'start' \| 'end'` | `'end'` | Edge it slides from | -| `size` | `'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Width | +| `size` | `'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'` | `'default'` | Width | | `closeButton` | `boolean` | `true` | Show the × button | | `open`, `defaultOpen`, `onOpenChange`, `modal` | | | Dialog state | | `className` | `string` | | Classes for the panel | diff --git a/e2e/nextjs/.gitignore b/e2e/nextjs/.gitignore new file mode 100644 index 00000000..56ccee10 --- /dev/null +++ b/e2e/nextjs/.gitignore @@ -0,0 +1,3 @@ +/.next +next-env.d.ts +*.tsbuildinfo diff --git a/e2e/nextjs/app/globals.css b/e2e/nextjs/app/globals.css new file mode 100644 index 00000000..b9f054fe --- /dev/null +++ b/e2e/nextjs/app/globals.css @@ -0,0 +1,2 @@ +@import "tailwindcss"; +@import "@javaabu/onu/tailwind.css"; diff --git a/e2e/nextjs/app/layout.tsx b/e2e/nextjs/app/layout.tsx new file mode 100644 index 00000000..3dd30110 --- /dev/null +++ b/e2e/nextjs/app/layout.tsx @@ -0,0 +1,19 @@ +import { cookies } from "next/headers" + +import { Providers } from "@/components/providers" + +import "./globals.css" + +export const metadata = { title: "Onu server-rendering fixture" } + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const locale = (await cookies()).get("locale")?.value === "dv" ? "dv" : "en" + + return ( + + + {children} + + + ) +} diff --git a/e2e/nextjs/app/page.tsx b/e2e/nextjs/app/page.tsx new file mode 100644 index 00000000..fa30a9b4 --- /dev/null +++ b/e2e/nextjs/app/page.tsx @@ -0,0 +1,22 @@ +import { Callout, Card, Countdown, PageTitle, PublishBox, RelativeDate, StatusBadge, Timeline } from "@javaabu/onu" + +import { ContactForm } from "@/components/contact-form" + +// A Server Component rendering Onu's client components with serialisable props. The clock-based ones catch hydration +// mismatches; ContactForm is the "use client" boundary for props that are functions. +export default function HomePage() { + return ( +
+ + + + Approved + + + + + + +
+ ) +} diff --git a/e2e/nextjs/components/contact-form.tsx b/e2e/nextjs/components/contact-form.tsx new file mode 100644 index 00000000..2471c07f --- /dev/null +++ b/e2e/nextjs/components/contact-form.tsx @@ -0,0 +1,17 @@ +"use client" + +import { Form, FormField, PasswordInput, SubmitButton, TextInput } from "@javaabu/onu" + +export function ContactForm() { + return ( +
{}}> + + + + + + + Send +
+ ) +} diff --git a/e2e/nextjs/components/providers.tsx b/e2e/nextjs/components/providers.tsx new file mode 100644 index 00000000..d93762e1 --- /dev/null +++ b/e2e/nextjs/components/providers.tsx @@ -0,0 +1,19 @@ +"use client" + +import { OnuProvider, ThemeProvider, TooltipProvider } from "@javaabu/onu" +import { dv } from "@javaabu/onu/locales/dv" +import Link from "next/link" + +/** + * `linkComponent` is a function, which a Server Component can't pass to a client component. `locale={dv}` bundles the + * Dhivehi text so the server renders it; `locale="dv"` would load it on demand, after the first render. + */ +export function Providers({ locale, children }: { locale: "en" | "dv"; children: React.ReactNode }) { + return ( + + + {children} + + + ) +} diff --git a/e2e/nextjs/next.config.ts b/e2e/nextjs/next.config.ts new file mode 100644 index 00000000..f62ca427 --- /dev/null +++ b/e2e/nextjs/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next" + +const nextConfig: NextConfig = {} + +export default nextConfig diff --git a/e2e/nextjs/package.json b/e2e/nextjs/package.json new file mode 100644 index 00000000..e9a93638 --- /dev/null +++ b/e2e/nextjs/package.json @@ -0,0 +1,26 @@ +{ + "name": "onu-nextjs-fixture", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "next build", + "test:ssr": "node scripts/ssr-check.mjs" + }, + "dependencies": { + "@javaabu/onu": "workspace:*", + "@tanstack/react-query": "^5.104.1", + "next": "^16.4.3", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "react-hook-form": "^7.89.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.3.3", + "@types/node": "^26.6.4", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "tailwindcss": "^4.3.3", + "typescript": "~6.0.3" + } +} diff --git a/e2e/nextjs/postcss.config.mjs b/e2e/nextjs/postcss.config.mjs new file mode 100644 index 00000000..e9ec000f --- /dev/null +++ b/e2e/nextjs/postcss.config.mjs @@ -0,0 +1,3 @@ +export default { + plugins: { "@tailwindcss/postcss": {} }, +} diff --git a/e2e/nextjs/scripts/ssr-check.mjs b/e2e/nextjs/scripts/ssr-check.mjs new file mode 100644 index 00000000..7eafa7c9 --- /dev/null +++ b/e2e/nextjs/scripts/ssr-check.mjs @@ -0,0 +1,130 @@ +// Server-rendering check (R16.1), in English (LTR) and Dhivehi (RTL), against the production build: +// the HTML has Onu's markup before any JavaScript runs, and the page hydrates in a browser without errors. +// +// pnpm build && pnpm --filter onu-nextjs-fixture build && pnpm --filter onu-nextjs-fixture test:ssr +// +// Set CHROMIUM_PATH to use your own Chromium instead of Playwright's. + +import { spawn } from "node:child_process" +import { createRequire } from "node:module" +import { createServer } from "node:net" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" + +import { chromium } from "@playwright/test" + +const root = join(dirname(fileURLToPath(import.meta.url)), "..") +const port = await freePort() +const base = `http://127.0.0.1:${port}` +const nextBin = createRequire(join(root, "package.json")).resolve("next/dist/bin/next") + +const markup = [ + 'data-slot="page-title"', + 'data-slot="callout"', + 'data-slot="card"', + 'data-slot="status-badge"', + 'data-slot="form"', + 'data-slot="form-field"', +] + +const checks = [ + { + name: "English (LTR)", + locale: "en", + expect: [' !html.includes(needle)).map((needle) => `HTML is missing: ${needle}`) + if (response.status !== 200) problems.unshift(`HTTP ${response.status}`) + return problems +} + +/** React reports hydration mismatches as uncaught errors or console errors, even in production. */ +async function checkHydration({ locale }) { + const context = await browser.newContext({ baseURL: base }) + await context.addCookies([{ name: "locale", value: locale, url: base }]) + const page = await context.newPage() + const problems = [] + page.on("pageerror", (error) => problems.push(`Page error: ${error.message}`)) + page.on("console", (message) => { + if (message.type() === "error") problems.push(`Console error: ${message.text()}`) + }) + await page.goto("/", { waitUntil: "networkidle" }) + + // An interaction only works once the page has hydrated. + const field = page.locator('[data-slot="form-field"]').nth(1) + const input = field.locator("input") + const before = await input.getAttribute("type") + await field.getByRole("button").click() + if ((await input.getAttribute("type")) === before) + problems.push("The password toggle did nothing, so the page didn't hydrate") + + await context.close() + return problems +} + +async function waitForServer(timeoutMs = 30_000) { + const start = Date.now() + while (Date.now() - start < timeoutMs) { + if (server.exitCode !== null) throw new Error(`next start exited with code ${server.exitCode}`) + try { + await fetch(base, { method: "HEAD" }) + return + } catch { + await new Promise((resolve) => setTimeout(resolve, 250)) + } + } + throw new Error(`next start did not answer on ${base} within ${timeoutMs / 1000}s`) +} + +function freePort() { + return new Promise((resolve, reject) => { + const probe = createServer() + probe.unref() + probe.on("error", reject) + probe.listen(0, "127.0.0.1", () => { + const { port } = probe.address() + probe.close(() => resolve(port)) + }) + }) +} diff --git a/e2e/nextjs/tsconfig.json b/e2e/nextjs/tsconfig.json new file mode 100644 index 00000000..9f397f65 --- /dev/null +++ b/e2e/nextjs/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "allowJs": true, + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.mts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/eslint.config.js b/eslint.config.js index 24fcbad4..a0b19677 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -25,6 +25,8 @@ export default tseslint.config( "**/*.config.*", "docs/**", "**/playwright-report/**", + "**/.next/**", + "**/next-env.d.ts", // Built Figma plugin bundle (generated from figma/plugin/src, committed for designers). "figma/plugin/code.js", ], diff --git a/figma/plugin/code.js b/figma/plugin/code.js index e8f71dd4..4bf6c8ef 100644 --- a/figma/plugin/code.js +++ b/figma/plugin/code.js @@ -2,7 +2,7 @@ (function() { //#region ../packages/onu/package.json - var version = "0.2.0"; + var version = "0.2.1"; //#endregion //#region ../tokens.json @@ -2356,18 +2356,18 @@ variants: { status: Object.keys(STATUS), size: [ - "md", + "default", "sm", "lg" ], dir: DIRS }, - filter: (v) => v.size === "md" || v.dir === "ltr", + filter: (v) => v.size === "default" || v.dir === "ltr", width: 1200, build(root, v, b) { const s = STATUS[v.status]; const look = s.color ? preset(s.color) : TAG_VARIANT.default; - const size = TAG_SIZE[v.size === "md" ? "default" : v.size]; + const size = TAG_SIZE[v.size]; b.style(root, { h: size.h, px: size.px, diff --git a/figma/plugin/src/kit/display.ts b/figma/plugin/src/kit/display.ts index a0c7ea79..8d928036 100644 --- a/figma/plugin/src/kit/display.ts +++ b/figma/plugin/src/kit/display.ts @@ -116,15 +116,14 @@ export const statusBadge: KitComponent = { page: "Display", key: "display-and-feedback/status-badge", description: ' from "@javaabu/onu" (status is a tone or a key of statusMap).', - variants: { status: Object.keys(STATUS), size: ["md", "sm", "lg"], dir: DIRS }, - filter: (v) => v.size === "md" || v.dir === "ltr", + variants: { status: Object.keys(STATUS), size: ["default", "sm", "lg"], dir: DIRS }, + filter: (v) => v.size === "default" || v.dir === "ltr", // No TEXT property: each status shows its own default label (edit the layer to override, like `children`). width: 1200, build(root, v, b) { const s = STATUS[v.status!]! const look = s.color ? preset(s.color) : TAG_VARIANT.default! - // StatusBadge keeps "md" for its middle size; Tag calls the same size "default". - const size = TAG_SIZE[v.size === "md" ? "default" : v.size!]! + const size = TAG_SIZE[v.size!]! b.style(root, { h: size.h, px: size.px, diff --git a/packages/onu-mcp/CHANGELOG.md b/packages/onu-mcp/CHANGELOG.md index d98bd507..342a0a59 100644 --- a/packages/onu-mcp/CHANGELOG.md +++ b/packages/onu-mcp/CHANGELOG.md @@ -1,5 +1,9 @@ # @javaabu/onu-mcp +## 0.2.1 + +No changes in this release. + ## 0.2.0 ### Patch Changes diff --git a/packages/onu-mcp/package.json b/packages/onu-mcp/package.json index 0a57e7f9..fd3cb709 100644 --- a/packages/onu-mcp/package.json +++ b/packages/onu-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@javaabu/onu-mcp", - "version": "0.2.0", + "version": "0.2.1", "description": "MCP server for Onu (@javaabu/onu): gives AI assistants the component docs and the rules for writing Onu code.", "license": "MIT", "author": { diff --git a/packages/onu/CHANGELOG.md b/packages/onu/CHANGELOG.md index 29a2679b..e2015201 100644 --- a/packages/onu/CHANGELOG.md +++ b/packages/onu/CHANGELOG.md @@ -1,5 +1,14 @@ # @javaabu/onu +## 0.2.1 + +### Patch Changes + +- a1a7ad9: `WysiwygEditor` and `BlockEditor` no longer throw when they unmount with a dialog open. +- 15b4633: `KanbanBoard` no longer widens the page when its columns overflow. +- 2771d3e: `NavTabs` no longer scrolls the page to itself when it mounts below the fold; it scrolls only its own list. +- ce06e79: `StatusBadge`, `IconText`, `Countdown`, `BottomSheet`, `SlideOver`, `ProgressBar`, `Hero` and `MapLegendSwatch` rename size `md` to `default`. + ## 0.2.0 ### Minor Changes diff --git a/packages/onu/package.json b/packages/onu/package.json index 09c7aaa4..8c75e51e 100644 --- a/packages/onu/package.json +++ b/packages/onu/package.json @@ -1,6 +1,6 @@ { "name": "@javaabu/onu", - "version": "0.2.0", + "version": "0.2.1", "description": "Javaabu's React UI component library: shadcn/ui based, RTL and Dhivehi first, Thaana keyboard input, one-variable theming.", "license": "MIT", "author": { diff --git a/packages/onu/src/components/display/countdown.tsx b/packages/onu/src/components/display/countdown.tsx index 4990fe9d..d19f88a3 100644 --- a/packages/onu/src/components/display/countdown.tsx +++ b/packages/onu/src/components/display/countdown.tsx @@ -56,14 +56,14 @@ export const countdownVariants = cva("tabular-nums", { }, size: { sm: "", - md: "", + default: "", lg: "", }, }, - defaultVariants: { variant: "boxes", size: "md" }, + defaultVariants: { variant: "boxes", size: "default" }, }) -const VALUE_SIZES = { sm: "text-lg", md: "text-2xl", lg: "text-4xl" } as const +const VALUE_SIZES = { sm: "text-lg", default: "text-2xl", lg: "text-4xl" } as const export interface CountdownProps extends Omit, "children">, VariantProps { @@ -174,7 +174,7 @@ export function Countdown({ classNames?.unit, )} > - + {pad(value(unit.key))} diff --git a/packages/onu/src/components/display/icon-text.tsx b/packages/onu/src/components/display/icon-text.tsx index ede9b524..b52f83ce 100644 --- a/packages/onu/src/components/display/icon-text.tsx +++ b/packages/onu/src/components/display/icon-text.tsx @@ -9,7 +9,7 @@ export const iconTextVariants = cva("inline-flex items-center [&>svg]:shrink-0", variants: { size: { sm: "gap-1 text-xs [&>svg]:size-3.5", - md: "gap-1.5 text-sm [&>svg]:size-4", + default: "gap-1.5 text-sm [&>svg]:size-4", lg: "gap-2 text-base [&>svg]:size-5", }, tone: { @@ -22,7 +22,7 @@ export const iconTextVariants = cva("inline-flex items-center [&>svg]:shrink-0", info: "text-info", }, }, - defaultVariants: { size: "md", tone: "neutral" }, + defaultVariants: { size: "default", tone: "neutral" }, }) export interface IconTextProps extends React.ComponentProps<"span">, VariantProps { diff --git a/packages/onu/src/components/display/status-badge.tsx b/packages/onu/src/components/display/status-badge.tsx index 5aa5a827..029131ab 100644 --- a/packages/onu/src/components/display/status-badge.tsx +++ b/packages/onu/src/components/display/status-badge.tsx @@ -34,11 +34,11 @@ export const statusBadgeVariants = cva( }, size: { sm: "h-5 px-1.5 text-xs [&>svg]:size-3", - md: "h-5.5 px-1.75 text-xs [&>svg]:size-3", + default: "h-5.5 px-1.75 text-xs [&>svg]:size-3", lg: "h-7 px-2.5 text-sm [&>svg]:size-3.5", }, }, - defaultVariants: { tone: "neutral", size: "md" }, + defaultVariants: { tone: "neutral", size: "default" }, }, ) diff --git a/packages/onu/src/components/layout/hero.tsx b/packages/onu/src/components/layout/hero.tsx index 6e2304f3..64e49fe4 100644 --- a/packages/onu/src/components/layout/hero.tsx +++ b/packages/onu/src/components/layout/hero.tsx @@ -16,7 +16,7 @@ export const heroVariants = cva("@container/hero relative isolate overflow-hidde }, size: { sm: "py-10 @3xl/hero:py-14", - md: "py-14 @3xl/hero:py-20", + default: "py-14 @3xl/hero:py-20", lg: "py-16 @3xl/hero:py-28", }, rounded: { @@ -24,7 +24,7 @@ export const heroVariants = cva("@container/hero relative isolate overflow-hidde false: "", }, }, - defaultVariants: { tone: "plain", size: "md", rounded: false }, + defaultVariants: { tone: "plain", size: "default", rounded: false }, }) const OVERLAY: Record, string> = { @@ -33,7 +33,7 @@ const OVERLAY: Record, string> = { primary: "bg-linear-to-t from-primary/95 via-primary/90 to-primary/80", } -const TITLE_SIZE = { sm: "text-fluid-3xl", md: "text-fluid-4xl", lg: "text-fluid-5xl" } as const +const TITLE_SIZE = { sm: "text-fluid-3xl", default: "text-fluid-4xl", lg: "text-fluid-5xl" } as const export interface HeroBackgroundImage { src: string @@ -188,7 +188,11 @@ export function Hero({ {title} diff --git a/packages/onu/src/components/layout/nav-tabs.test.tsx b/packages/onu/src/components/layout/nav-tabs.test.tsx index fea4a15a..555baf94 100644 --- a/packages/onu/src/components/layout/nav-tabs.test.tsx +++ b/packages/onu/src/components/layout/nav-tabs.test.tsx @@ -54,12 +54,19 @@ describe("NavTabs", () => { expect(edit).toHaveClass("opacity-50") }) - it("is scrollable on phones and scrolls the current tab into view", () => { - const spy = vi.spyOn(Element.prototype, "scrollIntoView") + it("is scrollable on phones and scrolls the current tab into view without scrolling the page", () => { + const pageScroll = vi.spyOn(Element.prototype, "scrollIntoView") + const boxes = vi.spyOn(Element.prototype, "getBoundingClientRect").mockImplementation(function (this: Element) { + // The list is 100 px wide, and the current tab ends 50 px past its end. + return (this.tagName === "UL" ? { left: 0, right: 100 } : { left: 100, right: 150 }) as DOMRect + }) renderWithOnu() - expect(screen.getByRole("list")).toHaveClass("overflow-x-auto") - expect(spy).toHaveBeenCalledWith({ block: "nearest", inline: "nearest" }) - spy.mockRestore() + const list = screen.getByRole("list") + expect(list).toHaveClass("overflow-x-auto") + expect(list.scrollLeft).toBe(50) + expect(pageScroll).not.toHaveBeenCalled() + boxes.mockRestore() + pageScroll.mockRestore() }) it("supports prefix matching, forced state, badges, pill variant and keyboard focus", async () => { diff --git a/packages/onu/src/components/layout/nav-tabs.tsx b/packages/onu/src/components/layout/nav-tabs.tsx index 41b1d257..d18e15ab 100644 --- a/packages/onu/src/components/layout/nav-tabs.tsx +++ b/packages/onu/src/components/layout/nav-tabs.tsx @@ -83,8 +83,14 @@ export function NavTabs({ const activeIndex = items.findIndex((item) => !item.disabled && isActive(item)) React.useEffect(() => { if (activeIndex < 0) return - const el = listRef.current?.children[activeIndex] as HTMLElement | undefined - el?.scrollIntoView?.({ block: "nearest", inline: "nearest" }) + const list = listRef.current + const el = list?.children[activeIndex] + if (!list || !el) return + // Scrolls the list only: scrollIntoView would also scroll the page to a NavTabs below the fold. + const listBox = list.getBoundingClientRect() + const box = el.getBoundingClientRect() + if (box.left < listBox.left) list.scrollLeft += box.left - listBox.left + else if (box.right > listBox.right) list.scrollLeft += box.right - listBox.right }, [activeIndex]) return ( diff --git a/packages/onu/src/components/listing/drag-and-drop.test.tsx b/packages/onu/src/components/listing/drag-and-drop.test.tsx index 3dee8b06..d3b3ecd8 100644 --- a/packages/onu/src/components/listing/drag-and-drop.test.tsx +++ b/packages/onu/src/components/listing/drag-and-drop.test.tsx @@ -175,6 +175,11 @@ function Board(props: { canMove?: (m: { to: string }) => boolean; onMove?: (m: u } describe("KanbanBoard", () => { + it("contains the columns' absolutely positioned sr-only counts, so they can't widen the page", () => { + const { container } = renderWithOnu() + expect(container.querySelector("[data-slot=kanban-board]")).toHaveClass("relative", "overflow-x-auto") + }) + it("shows columns with counts and cards", async () => { renderWithOnu() const todo = screen.getByRole("region", { name: "To do" }) diff --git a/packages/onu/src/components/listing/kanban-board.tsx b/packages/onu/src/components/listing/kanban-board.tsx index bee5a9a0..a5da74fa 100644 --- a/packages/onu/src/components/listing/kanban-board.tsx +++ b/packages/onu/src/components/listing/kanban-board.tsx @@ -298,7 +298,8 @@ export function KanbanBoard({ {region}
{columns.map((column) => { const ids = layout[column.id] ?? [] diff --git a/packages/onu/src/components/loading/progress-bar.tsx b/packages/onu/src/components/loading/progress-bar.tsx index cd3b5537..b214d108 100644 --- a/packages/onu/src/components/loading/progress-bar.tsx +++ b/packages/onu/src/components/loading/progress-bar.tsx @@ -14,9 +14,9 @@ registerEnglish({ overlay: en_overlay }) export const progressTrackVariants = cva("relative w-full overflow-hidden rounded-full bg-muted", { variants: { - size: { sm: "h-1", md: "h-2", lg: "h-3" }, + size: { sm: "h-1", default: "h-2", lg: "h-3" }, }, - defaultVariants: { size: "md" }, + defaultVariants: { size: "default" }, }) export const progressIndicatorVariants = cva("ease-out h-full rounded-full transition-[width] duration-300", { diff --git a/packages/onu/src/components/overlays/bottom-sheet.tsx b/packages/onu/src/components/overlays/bottom-sheet.tsx index c3837312..5d6702b0 100644 --- a/packages/onu/src/components/overlays/bottom-sheet.tsx +++ b/packages/onu/src/components/overlays/bottom-sheet.tsx @@ -31,12 +31,12 @@ export const bottomSheetVariants = cva( /** Maximum width on wider screens; on phones the sheet is always full width. */ size: { sm: "sm:max-w-md", - md: "sm:max-w-lg", + default: "sm:max-w-lg", lg: "sm:max-w-2xl", full: "max-w-none", }, }, - defaultVariants: { size: "md" }, + defaultVariants: { size: "default" }, }, ) @@ -77,7 +77,7 @@ export function BottomSheetContent({ { expect(screen.getByRole("dialog")).toHaveAttribute("data-side", dir === "rtl" ? "right" : "left") }) - it.each(["sm", "md", "lg", "xl", "full"] as const)("applies size %s", (size) => { + it.each(["sm", "default", "lg", "xl", "full"] as const)("applies size %s", (size) => { renderWithOnu() expect(screen.getByRole("dialog")).toHaveAttribute("data-size", size) }) diff --git a/packages/onu/src/components/overlays/slide-over.tsx b/packages/onu/src/components/overlays/slide-over.tsx index e78f63f4..a1b2ab9c 100644 --- a/packages/onu/src/components/overlays/slide-over.tsx +++ b/packages/onu/src/components/overlays/slide-over.tsx @@ -20,13 +20,13 @@ export const slideOverVariants = cva( variants: { size: { sm: "data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm", - md: "data-[side=left]:sm:max-w-md data-[side=right]:sm:max-w-md", + default: "data-[side=left]:sm:max-w-md data-[side=right]:sm:max-w-md", lg: "data-[side=left]:sm:max-w-xl data-[side=right]:sm:max-w-xl", xl: "data-[side=left]:sm:max-w-3xl data-[side=right]:sm:max-w-3xl", full: "data-[side=left]:sm:max-w-none data-[side=right]:sm:max-w-none data-[side=left]:w-full data-[side=right]:w-full", }, }, - defaultVariants: { size: "md" }, + defaultVariants: { size: "default" }, }, ) @@ -65,7 +65,7 @@ export function SlideOverContent({ return ( { expect(editor.getHTML()).not.toContain(" { + const onError = vi.fn<(event: ErrorEvent) => void>() + window.addEventListener("error", onError) + const { editor, unmount } = await setup() + const list = await openSlash(editor, "image") + fireEvent.click(within(list).getByRole("option", { name: /Image/ })) + await screen.findByRole("dialog", { name: "Insert image" }) + unmount() + // Radix calls onCloseAutoFocus from a timer after the unmount. + await act(() => new Promise((resolve) => setTimeout(resolve, 50))) + window.removeEventListener("error", onError) + expect(onError).not.toHaveBeenCalled() + }) + it("does not open inside code blocks", async () => { const { editor } = await setup({ defaultValue: "
a
" }) act(() => { diff --git a/packages/onu/src/editor/block-editor.tsx b/packages/onu/src/editor/block-editor.tsx index 90668e48..14700b64 100644 --- a/packages/onu/src/editor/block-editor.tsx +++ b/packages/onu/src/editor/block-editor.tsx @@ -232,7 +232,8 @@ export function BlockEditor({ event.preventDefault() const target = returnFocus.current if (target?.isConnected) target.focus() - else editor?.commands.focus() + // Radix calls this from a timer, which can run after the editor unmounts. + else if (editor && !editor.isDestroyed) editor.commands.focus() } return ( diff --git a/packages/onu/src/editor/wysiwyg-editor.test.tsx b/packages/onu/src/editor/wysiwyg-editor.test.tsx index 82ea3c57..73e6b804 100644 --- a/packages/onu/src/editor/wysiwyg-editor.test.tsx +++ b/packages/onu/src/editor/wysiwyg-editor.test.tsx @@ -298,9 +298,20 @@ describe("WysiwygEditor", () => { const dialog = await screen.findByRole("dialog", { name: "Insert image" }) expect(within(dialog).getByLabelText(/Alternative text/)).toHaveAttribute("data-thaana") expect(within(dialog).getByLabelText(/Image URL/)).not.toHaveAttribute("data-thaana") - // Close it and let focus return, because the dialog would otherwise focus the editor after it unmounts. - await user.keyboard("{Escape}") - await waitFor(() => expect(button("Image")).toHaveFocus()) + }) + + it("unmounts with a dialog open without touching the destroyed editor", async () => { + const onError = vi.fn<(event: ErrorEvent) => void>() + window.addEventListener("error", onError) + const user = userEvent.setup() + const { unmount } = await setup() + await user.click(button("Link")) + await screen.findByRole("dialog", { name: "Insert link" }) + unmount() + // Radix calls onCloseAutoFocus from a timer after the unmount. + await act(() => new Promise((resolve) => setTimeout(resolve, 50))) + window.removeEventListener("error", onError) + expect(onError).not.toHaveBeenCalled() }) it("uploads images through the upload callback and reports failures", async () => { diff --git a/packages/onu/src/editor/wysiwyg-editor.tsx b/packages/onu/src/editor/wysiwyg-editor.tsx index a946cf9d..d8eca730 100644 --- a/packages/onu/src/editor/wysiwyg-editor.tsx +++ b/packages/onu/src/editor/wysiwyg-editor.tsx @@ -169,7 +169,8 @@ export function WysiwygEditor({ event.preventDefault() const target = returnFocus.current if (target?.isConnected) target.focus() - else editor?.commands.focus() + // Radix calls this from a timer, which can run after the editor unmounts. + else if (editor && !editor.isDestroyed) editor.commands.focus() } return ( diff --git a/packages/onu/src/maps/map-legend-swatch.tsx b/packages/onu/src/maps/map-legend-swatch.tsx index d51fd80b..a2c12e09 100644 --- a/packages/onu/src/maps/map-legend-swatch.tsx +++ b/packages/onu/src/maps/map-legend-swatch.tsx @@ -18,11 +18,11 @@ export const mapLegendSwatchVariants = cva("inline-block shrink-0", { variants: { size: { sm: "h-3 w-3", - md: "h-4 w-4", + default: "h-4 w-4", lg: "h-5 w-5", }, }, - defaultVariants: { size: "md" }, + defaultVariants: { size: "default" }, }) export interface MapLegendSwatchProps diff --git a/plugins/onu/.claude-plugin/plugin.json b/plugins/onu/.claude-plugin/plugin.json index 94d6d6ea..9b3e4bd0 100644 --- a/plugins/onu/.claude-plugin/plugin.json +++ b/plugins/onu/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "onu", "description": "Build UI with Onu (@javaabu/onu): the rules for writing Onu code, and the component docs through the Onu MCP server.", - "version": "0.2.0", + "version": "0.2.1", "author": { "name": "Javaabu Pvt. Ltd.", "url": "https://javaabu.com" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13e5ee47..aa8fb793 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -203,6 +203,46 @@ importers: specifier: ^8.71.1 version: 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + e2e/nextjs: + dependencies: + '@javaabu/onu': + specifier: workspace:* + version: link:../../packages/onu + '@tanstack/react-query': + specifier: ^5.104.1 + version: 5.104.1(react@19.3.0) + next: + specifier: ^16.4.3 + version: 16.4.3(@babel/core@7.29.7(supports-color@10.2.2))(@playwright/test@1.63.0)(@types/node@26.6.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react: + specifier: ^19.3.0 + version: 19.3.0 + react-dom: + specifier: ^19.3.0 + version: 19.3.0(react@19.3.0) + react-hook-form: + specifier: ^7.89.0 + version: 7.89.0(@types/react@19.3.0)(react@19.3.0) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4.3.3 + version: 4.3.3 + '@types/node': + specifier: ^26.6.4 + version: 26.6.4 + '@types/react': + specifier: ^19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.3.0 + version: 19.3.0(@types/react@19.3.0) + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + typescript: + specifier: ~6.0.3 + version: 6.0.3 + figma: devDependencies: '@figma/plugin-typings': @@ -715,6 +755,10 @@ packages: '@adobe/css-tools@4.5.0': resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@alloc/quick-lru@5.3.0': + resolution: {integrity: sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==} + engines: {node: '>=10'} + '@asamuzakjp/css-color@7.1.3': resolution: {integrity: sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -1040,6 +1084,9 @@ packages: '@dotenvx/primitives@0.8.0': resolution: {integrity: sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@eslint-community/eslint-utils@4.10.1': resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1220,6 +1267,168 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@internationalized/date@3.12.4': resolution: {integrity: sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==} @@ -1359,6 +1568,61 @@ packages: resolution: {integrity: sha512-rgR9yxSaAJ+bcsi/XroT/S2AfPiIWloXAnrdfYSCms9ivVju0ishu/wW0J21Ho6+ZOR1r7RyeRbjiRuF87ix5Q==} engines: {node: '>=20'} + '@next/env@16.4.3': + resolution: {integrity: sha512-a7tz1QCcWWkwC/rPJcPMJk/UNRu9CFhDd3OHYE5hddbJrs8BqRTalxI6GlKCWEGzRLHN+S/B7a4PAHCtm25aBQ==} + + '@next/swc-darwin-arm64@16.4.3': + resolution: {integrity: sha512-4yq5EXBt+VDtAZwTyfijaFp5D2C5oz3Ikw5a2kiX2K9nw72vrZX2q5UuAlkPtUMO1tQz6JTKpaKWjS7OOefTDQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.4.3': + resolution: {integrity: sha512-1Rvkn/LUhnEF8WxnRkOrqGhbwXui1bkWFxONKIbN+r7C+9KHDSZ+iUoj4CzymVMWUyZIyx9szcb9+kbKhD500A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.4.3': + resolution: {integrity: sha512-dCKpXErGg3abUM6t5JTJV/ottVBTStJB5lRJTbgdUdQaQ+jNHiK56Hsc0YAEg/rinSh42hYG3HWiYmEMdilnxg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.4.3': + resolution: {integrity: sha512-WwYbhWkhYBvBtib1K4W0kL8MW2snBD2A5b3SN4vPpqalZbZs5+bQ1yzAdepVoOqrFfEW3dmREy4Zs1MrWcrFbw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.4.3': + resolution: {integrity: sha512-QymWvfUwDEC5/lpJOsxX5VAbyoR2D13+2Jsjz4be7gT4LPqEgdpkeLIR/HFfCLgKzoMhpJbp6I/JnogyNZHcaA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.4.3': + resolution: {integrity: sha512-Vmjd9D7DpA4Vv3cUlr82QWgJD/BQZmns801vtC5VWHg6jCIA4a5vMKpQnfrM2pyCrp2eV0ENdUg4PlN3bAc2Yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.4.3': + resolution: {integrity: sha512-lG1pPdPLk7gTyGb00n5JIMLdBJgjb+UEhSdZvFbhUMg0DyC6z4B7RnpSKF9VCd4imkLObVbXOFA0kFCpw7yFow==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.4.3': + resolution: {integrity: sha512-NmQC9RkJdnPXRaMxtkCzobB3teVjqF33oSOnwDfi+XYGWWYpt+xlGK3mtktSKLbdjwaWbJu2OkkMiRFGUUncKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2311,24 +2575,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.3': resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.3': resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.3': resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.3': resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} @@ -2358,6 +2626,9 @@ packages: resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} engines: {node: '>= 20'} + '@tailwindcss/postcss@4.3.3': + resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} + '@tailwindcss/vite@4.3.3': resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} peerDependencies: @@ -4684,6 +4955,7 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.33.0: resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} @@ -4696,6 +4968,7 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.33.0: resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} @@ -4708,6 +4981,7 @@ packages: engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.33.0: resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} @@ -4720,6 +4994,7 @@ packages: engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.33.0: resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} @@ -4913,6 +5188,27 @@ packages: resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} engines: {node: '>=18'} + next@16.4.3: + resolution: {integrity: sha512-OqxKTGi9u80FydydsMNYNjnuje9Au+kvHlt2GZ//ftdpDHm1dxKqAaENoOOTX5wKzn+9vaKUNIfZeyZUuLhCnA==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + node-releases@2.0.57: resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} engines: {node: '>=18'} @@ -5106,6 +5402,10 @@ packages: resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} engines: {node: '>=4'} + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.30: resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} engines: {node: ^10 || ^12 || >=14} @@ -5546,6 +5846,15 @@ packages: engines: {node: '>=20.18.1'} hasBin: true + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -5697,6 +6006,19 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + supercluster@8.0.1: resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} @@ -6184,6 +6506,8 @@ snapshots: '@adobe/css-tools@4.5.0': {} + '@alloc/quick-lru@5.3.0': {} + '@asamuzakjp/css-color@7.1.3': dependencies: '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) @@ -6674,6 +6998,11 @@ snapshots: '@dotenvx/primitives@0.8.0': {} + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@eslint-community/eslint-utils@4.10.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))': dependencies: eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) @@ -6830,6 +7159,113 @@ snapshots: '@iconify/types@2.0.0': {} + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + '@internationalized/date@3.12.4': dependencies: '@swc/helpers': 0.5.23 @@ -7019,6 +7455,32 @@ snapshots: '@modelcontextprotocol/core': 2.4.0 zod: 4.6.7 + '@next/env@16.4.3': {} + + '@next/swc-darwin-arm64@16.4.3': + optional: true + + '@next/swc-darwin-x64@16.4.3': + optional: true + + '@next/swc-linux-arm64-gnu@16.4.3': + optional: true + + '@next/swc-linux-arm64-musl@16.4.3': + optional: true + + '@next/swc-linux-x64-gnu@16.4.3': + optional: true + + '@next/swc-linux-x64-musl@16.4.3': + optional: true + + '@next/swc-win32-arm64-msvc@16.4.3': + optional: true + + '@next/swc-win32-x64-msvc@16.4.3': + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8028,6 +8490,14 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + '@tailwindcss/postcss@4.3.3': + dependencies: + '@alloc/quick-lru': 5.3.0 + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + postcss: 8.5.30 + tailwindcss: 4.3.3 + '@tailwindcss/vite@4.3.3(vite@8.3.4(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))': dependencies: '@tailwindcss/node': 4.3.3 @@ -11343,6 +11813,32 @@ snapshots: dependencies: content-type: 2.1.0 + next@16.4.3(@babel/core@7.29.7(supports-color@10.2.2))(@playwright/test@1.63.0)(@types/node@26.6.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + '@next/env': 16.4.3 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.27 + caniuse-lite: 1.0.30001815 + postcss: 8.5.23 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0) + optionalDependencies: + '@next/swc-darwin-arm64': 16.4.3 + '@next/swc-darwin-x64': 16.4.3 + '@next/swc-linux-arm64-gnu': 16.4.3 + '@next/swc-linux-arm64-musl': 16.4.3 + '@next/swc-linux-x64-gnu': 16.4.3 + '@next/swc-linux-x64-musl': 16.4.3 + '@next/swc-win32-arm64-msvc': 16.4.3 + '@next/swc-win32-x64-msvc': 16.4.3 + '@playwright/test': 1.63.0 + sharp: 0.35.5(@types/node@26.6.4) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + node-releases@2.0.57: {} normalize-wheel@1.0.1: {} @@ -11534,6 +12030,12 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss@8.5.23: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + postcss@8.5.30: dependencies: nanoid: 3.3.20 @@ -12133,6 +12635,40 @@ snapshots: - babel-plugin-macros - supports-color + sharp@0.35.5(@types/node@26.6.4): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 26.6.4 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -12293,6 +12829,13 @@ snapshots: dependencies: min-indent: 1.0.1 + styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0): + dependencies: + client-only: 0.0.1 + react: 19.3.0 + optionalDependencies: + '@babel/core': 7.29.7(supports-color@10.2.2) + supercluster@8.0.1: dependencies: kdbush: 4.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a8729ace..527d76c8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - packages/* - web - figma + - e2e/nextjs diff --git a/scripts/check-provenance.mjs b/scripts/check-provenance.mjs new file mode 100644 index 00000000..c323c82e --- /dev/null +++ b/scripts/check-provenance.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +// Fails unless every published package version has an npm provenance attestation. +// pnpm only warns when the OIDC exchange fails, so a successful publish doesn't prove one. +// +// node scripts/check-provenance.mjs [package…] +// +// Packages default to the ones in packages/*. +import { execFileSync } from "node:child_process" +import { readdirSync, readFileSync } from "node:fs" +import { setTimeout as sleep } from "node:timers/promises" + +const ATTEMPTS = 6 +const WAIT_MS = 20_000 // A new version takes a moment to reach the registry's read side. + +const [version, ...named] = process.argv.slice(2) +if (!version) { + console.error("Usage: node scripts/check-provenance.mjs [package…]") + process.exit(2) +} + +const workspace = () => + readdirSync("packages").map((dir) => JSON.parse(readFileSync(`packages/${dir}/package.json`, "utf8")).name) + +function attestationUrl(name) { + try { + return execFileSync("npm", ["view", `${name}@${version}`, "dist.attestations.url"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim() + } catch { + return "" // Not on the registry yet. + } +} + +let missing = named.length ? named : workspace() +for (let attempt = 1; ; attempt++) { + missing = missing.filter((name) => { + const url = attestationUrl(name) + if (url) console.log(`${name}@${version}: ${url}`) + return !url + }) + if (!missing.length || attempt === ATTEMPTS) break + await sleep(WAIT_MS) +} + +for (const name of missing) { + console.error(`::error::${name}@${version} has no npm provenance attestation. See "Releasing" in CONTRIBUTING.md.`) +} +process.exit(missing.length ? 1 : 0) diff --git a/web/src/components/home.tsx b/web/src/components/home.tsx index a4953e51..bf14f8bf 100644 --- a/web/src/components/home.tsx +++ b/web/src/components/home.tsx @@ -244,7 +244,7 @@ export function Home() { - + No icon diff --git a/web/src/showcase/foundations/theme-builder.demo.tsx b/web/src/showcase/foundations/theme-builder.demo.tsx index 5d772930..f9e751c2 100644 --- a/web/src/showcase/foundations/theme-builder.demo.tsx +++ b/web/src/showcase/foundations/theme-builder.demo.tsx @@ -117,7 +117,14 @@ export default function ThemeBuilder() { -
+        
           {css}