Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["onu-web"]
"ignore": ["onu-web", "onu-nextjs-fixture"]
}
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
52 changes: 52 additions & 0 deletions .github/workflows/ssr.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
dist
.next
next-env.d.ts
coverage
pnpm-lock.yaml
packages/onu/src/ui
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<group>/<name>.demo.tsx`, `src/pages/<group>/<name>.page.tsx` |
| `docs` | Markdown docs, one page per component (`docs/<group>/<name>.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

Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docs/display-and-feedback/countdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion docs/display-and-feedback/icon-text.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion docs/display-and-feedback/status-badge.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Unknown statuses fall back to the `neutral` tone with the raw value as the label
| `statusMap` | `Record<string, StatusDefinition>` | | `{ 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 |

Expand Down
2 changes: 1 addition & 1 deletion docs/layout-and-navigation/hero.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion docs/loading/progress-bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion docs/maps/map-legend-swatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down
2 changes: 1 addition & 1 deletion docs/overlays/bottom-sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
4 changes: 2 additions & 2 deletions docs/overlays/slide-over.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 |
Expand Down
3 changes: 3 additions & 0 deletions e2e/nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.next
next-env.d.ts
*.tsbuildinfo
2 changes: 2 additions & 0 deletions e2e/nextjs/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import "tailwindcss";
@import "@javaabu/onu/tailwind.css";
19 changes: 19 additions & 0 deletions e2e/nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<html lang={locale} dir={locale === "dv" ? "rtl" : "ltr"}>
<body>
<Providers locale={locale}>{children}</Providers>
</body>
</html>
)
}
22 changes: 22 additions & 0 deletions e2e/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main>
<PageTitle title="Applications" />
<Callout tone="info" title="Welcome" />
<Card title="Recent">
<StatusBadge status="success">Approved</StatusBadge>
</Card>
<Countdown to="2030-01-01T00:00:00Z" />
<RelativeDate value="2025-12-31T00:00:00Z" />
<PublishBox status="draft" publishAt="2030-03-01T00:00:00Z" />
<Timeline groupByDay items={[{ id: "1", title: "Opened", time: "2026-01-01T00:00:00Z" }]} />
<ContactForm />
</main>
)
}
17 changes: 17 additions & 0 deletions e2e/nextjs/components/contact-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use client"

import { Form, FormField, PasswordInput, SubmitButton, TextInput } from "@javaabu/onu"

export function ContactForm() {
return (
<Form onSubmit={async () => {}}>
<FormField label="Name" name="name" required>
<TextInput />
</FormField>
<FormField label="Password" name="password">
<PasswordInput />
</FormField>
<SubmitButton>Send</SubmitButton>
</Form>
)
}
19 changes: 19 additions & 0 deletions e2e/nextjs/components/providers.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ThemeProvider>
<OnuProvider locale={locale === "dv" ? dv : "en"} linkComponent={Link}>
<TooltipProvider>{children}</TooltipProvider>
</OnuProvider>
</ThemeProvider>
)
}
5 changes: 5 additions & 0 deletions e2e/nextjs/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NextConfig } from "next"

const nextConfig: NextConfig = {}

export default nextConfig
26 changes: 26 additions & 0 deletions e2e/nextjs/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 3 additions & 0 deletions e2e/nextjs/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
plugins: { "@tailwindcss/postcss": {} },
}
Loading
Loading