diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0459bb5..066b2a44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,12 @@ jobs: - name: Build library run: npm run build + # Compiled file by file, then each relative import is completed against + # what was emitted. An import the second half cannot resolve only fails + # here: the typecheck follows the same specifier without its extension. + - name: Build web package + run: npm run build --workspace=panelui-web + # A real production Metro export catches dependency and asset growth that # source-level package budgets cannot see. Offline mode and a cold cache # keep this reproducible; the current export takes about 15 seconds. diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index e9651141..b7f86434 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,4 +1,4 @@ -# Deploys panelui.dev when a library release is published. +# Deploys panelui.dev when a library release or a web release is published. # # The docs site used to build on every push and every pull request, through # Vercel's Git integration. That is a full Next.js build of a site whose content @@ -24,8 +24,11 @@ # # `release: published` fires for every release in the repository, and the CLI # packages are released under their own tag prefixes. A `cli-v0.4.2` release -# does not change a component page, so it should not spend a build. Only a plain -# `vX.Y.Z` gets through — the same filter publish.yml uses, for the same reason. +# does not change a component page, so it should not spend a build. A plain +# `vX.Y.Z` gets through — the same filter publish.yml uses, for the same reason +# — and so does `web-vX.Y.Z`: the web collection's pages and its registry at +# /r/web are served from this site, so a web release that did not deploy would +# put a new version on npm and leave the old source behind the shadcn CLI. # # ## One-time setup # @@ -53,7 +56,7 @@ permissions: jobs: deploy: runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'v') + if: github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'v') || startsWith(github.ref_name, 'web-v') steps: - name: Trigger the production deployment env: diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml new file mode 100644 index 00000000..8ffc1e43 --- /dev/null +++ b/.github/workflows/publish-web.yml @@ -0,0 +1,106 @@ +# Publishes panelui-web to npm when a GitHub release is published, in the same +# shape as publish.yml does for the library and publish-cli.yml for the CLIs. +# +# The web collection is versioned independently of all of them, so a release is +# for it only when the tag says so: +# +# web-vX.Y.Z -> packages/web (panelui-web) +# +# A release whose tag does not start with `web-v` publishes nothing here and +# says so, rather than failing: every workflow on `release: published` sees +# every release. +# +# Authentication is npm Trusted Publishing: the job asks GitHub for an OIDC +# token and npm exchanges it for a short-lived one, so there is no credential in +# this repository to leak or rotate, and the package carries a provenance +# attestation naming this workflow and commit. +# +# One-time setup on npmjs.com, without which every run fails at the last step. +# A trusted publisher can only be set on a package that exists, so the first +# version has to be published by hand, once, by somebody with the account's +# second factor. After that: +# panelui-web -> Settings -> Trusted Publisher -> GitHub Actions +# Organization or user: panel-ui +# Repository: PanelUI +# Workflow filename: publish-web.yml +name: Publish web + +on: + release: + types: [published] + # A dry run on demand, so the whole path — install, build, test, pack — can + # be exercised without putting anything on the registry. + workflow_dispatch: + +permissions: + contents: read + id-token: write # required for trusted publishing + +jobs: + publish: + # Decided before anything is installed: a library or CLI release has + # nothing to do here, and should not spend a job finding that out. + if: github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'web-v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + registry-url: https://registry.npmjs.org + + # Node 22 ships npm 10, which has no trusted publishing. Without this the + # publish step fails asking for a token that deliberately does not exist. + - name: Use an npm that can publish with OIDC + run: npm install -g npm@latest + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck --workspace=panelui-web + + - name: Build + run: npm run build --workspace=panelui-web + + # The package contract: every component is exported, client modules say + # so, and every compound part has a name a Server Component can import. + - name: Test the package + run: node --test packages/web/test/*.test.mjs + + # The only thing standing between a mistyped tag and a wrong version on + # npm, which cannot be taken back — npm does not allow a version to be + # republished, even after an unpublish. + - name: Check the tag matches the package version + if: github.event_name == 'release' + run: | + tag="${GITHUB_REF_NAME#web-v}" + version="$(node -p "require('./packages/web/package.json').version")" + if [ "$tag" != "$version" ]; then + echo "::error::Release tag ${GITHUB_REF_NAME} does not match packages/web/package.json (${version}). Fix one of them and re-cut the release." + exit 1 + fi + echo "Publishing panelui-web ${version} from ${GITHUB_REF_NAME}." + + # The first version is published by hand (see the top of this file), and + # its release is cut afterwards so that there is a record of it. That + # run finds the version already on the registry and has nothing to do, + # which is not a failure. npm would refuse it anyway: a version cannot be + # published twice. + - name: Publish + if: github.event_name == 'release' + run: | + version="$(node -p "require('./package.json').version")" + if npm view "panelui-web@${version}" version >/dev/null 2>&1; then + echo "::notice::panelui-web ${version} is already on npm. Nothing to publish." + exit 0 + fi + npm publish --provenance --access public + working-directory: packages/web + + - name: Dry run + if: github.event_name == 'workflow_dispatch' + run: npm publish --dry-run --access public + working-directory: packages/web diff --git a/CLAUDE.md b/CLAUDE.md index f2de8768..a9474cdb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -623,6 +623,7 @@ independently of the library and of each other. Which package a release is for c | `vX.Y.Z` | `packages/panelui` → `panelui-native` | | `cli-vX.Y.Z` | `packages/cli` → `panelui-cli` | | `create-vX.Y.Z` | `packages/create-panelui-app` → `create-panelui-app` | +| `web-vX.Y.Z` | `packages/web` → `panelui-web`, by `publish-web.yml` | `create-panelui-app` depends on `panelui-cli`, so a breaking change to the `panelui-cli/init` or `panelui-cli/ui` subpath exports needs both released, cli first. @@ -633,7 +634,7 @@ counts as that go-ahead for the release they just approved, and for that one onl ### What builds when -Four workflows, and nothing else runs on its own. This is the whole picture: +Five workflows, and nothing else runs on its own. This is the whole picture: | Event | What runs | What it produces | | --- | --- | --- | @@ -643,6 +644,8 @@ Four workflows, and nothing else runs on its own. This is the whole picture: | Release published, tag `vX.Y.Z` | `deploy-docs.yml` | panelui.dev, by firing a Vercel deploy hook. | | Release published, tag `cli-v*` | `publish-cli.yml` | `panelui-cli` on npm. | | Release published, tag `create-v*` | `publish-cli.yml` | `create-panelui-app` on npm. | +| Release published, tag `web-v*` | `publish-web.yml` | `panelui-web` on npm. | +| Release published, tag `web-v*` | `deploy-docs.yml` | panelui.dev, because the web docs and the registry at `/r/web` are served from it. | | **Deploy docs** run by hand | `deploy-docs.yml` | panelui.dev, from the current `main`. | Every workflow on `release: published` sees **every** release and filters by tag prefix itself, so diff --git a/README.md b/README.md index e8937b26..8efce1b0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

- - PanelUI — high-performance React Native components for Expo + + PanelUI — high-performance UI components for Expo and the web

@@ -12,13 +12,15 @@

- Semantic design tokens · Tailwind v4 via Uniwind · Reanimated on the UI thread. + Semantic design tokens · Tailwind v4 · Reanimated on the UI thread.
+ React Native for Expo, and now React on the web.

Documentation · Installation · Components · + Web · Theming · CLI

@@ -52,6 +54,10 @@ in one coherent visual language, with light and dark themes out of the box. Pure TypeScript with no native modules, so it runs in **Expo Go** with no `prebuild`, no Xcode and no Android Studio. +**New: PanelUI for the web.** [`panelui-web`](#panelui-for-the-web) is a collection of eleven +components for React on the web, in the same visual language and drawn in your project's own +theme: a board of cards you drag, a canvas of nodes and edges, a globe, KPI cards and more. + ## Why PanelUI - ⚡ **Fast styling** — [Uniwind](https://uniwind.dev) brings Tailwind v4 to React @@ -173,6 +179,49 @@ Every component takes `className`, so anything can be restyled with Tailwind cla Plus the primitives: `PanelUIProvider`, `Portal`, `AnimatedPressable`, `useTheme`, `useThemeMode`, `useToast`, `cn`. +## PanelUI for the web + +`panelui-web` is a separate collection for React on the web: Next.js, Vite, React Router, or +anything else that runs React 18 or newer with Tailwind CSS v4. It is not a port of the components +above. It is eleven of its own, styled with Tailwind on the tokens a shadcn project already +defines, so they take your colours and radius in light and dark. + +Copy a component's source into your project with the shadcn CLI. It lands in +`components/panelui/` and is yours to edit: + +```bash +npx shadcn@latest add https://panelui.dev/r/web/lane-board.json +``` + +Or install the package, and let Tailwind see the classes it uses: + +```bash +npm install panelui-web +``` + +```css +/* app/globals.css */ +@import "tailwindcss"; +@source "../node_modules/panelui-web/dist"; +``` + +```tsx +import { RollingNumber } from 'panelui-web/rolling-number'; + + +``` + +- **Lane Board** — lanes of cards, dragged with a pointer or carried with the keyboard +- **Flow** — a canvas of nodes joined by edges, to pan, zoom and rearrange +- **Planet** — the world as dots on a globe you can turn, with markers pinned to it +- **Day Grid**, **Bar Picker**, **Range Meter**, **KPI**, **Rolling Number** — figures, to read and + to pick from +- **Command Dock**, **Feedback**, **Tilt Card** — asking, answering and showing + +KPI and Flow have the same parts and props as the app library's, so a screen written for one reads +the same in the other. Every component runs live in the docs, with its source under it, at +**[panelui.dev/docs/web](https://panelui.dev/docs/web)**. + ## Theming Six themes ship in [`theme.css`](packages/panelui/theme.css) — `light`, `dark`, `moon`, @@ -225,6 +274,17 @@ The skill activates on its own once a project has `panelui-native` in its depend `panelui.json` beside it. Full details, and the URLs everything is served from, at **[panelui.dev/docs/skills](https://panelui.dev/docs/skills)**. +The web collection has a skill of its own, generated the same way from its source: + +```sh +npx skills add panel-ui/PanelUI --skill panelui-web +``` + +Its components are shadcn registry items, so the shadcn MCP server can search and add them once +the registry is named in `components.json`. Both are covered at +**[panelui.dev/docs/web/skills](https://panelui.dev/docs/web/skills)** and +**[panelui.dev/docs/web/mcp](https://panelui.dev/docs/web/mcp)**. + ## Example app An Expo Router showcase with a live demo for every component and a theme picker — used to diff --git a/apps/docs/app/(home)/page.tsx b/apps/docs/app/(home)/page.tsx index 1c8b8112..d5e83ae1 100644 --- a/apps/docs/app/(home)/page.tsx +++ b/apps/docs/app/(home)/page.tsx @@ -48,6 +48,29 @@ export const metadata: Metadata = { */ const INSTALL = 'npx create-panelui-app@latest'; +/** + * What the web collection holds itself to. The first is the wide card: two of + * its claims are one act, adding a component and having it come out in your + * colours, and it gets the room to show that as one thing. + */ +const WEB_FEATURES: { figure: IsoFigureKind; title: string; body: string }[] = [ + { + figure: 'workbench', + title: 'Copied in, and drawn in your theme', + body: 'A component is a file the shadcn CLI adds to your project, yours to read and to change. Its colour is one of five roles your theme defines, so it matches the rest of your interface in light and dark.', + }, + { + figure: 'arrows', + title: 'Keyboard first, motion optional', + body: 'Grids, boards and canvases take the arrow keys. Every animation stops for anyone who has asked for reduced motion.', + }, + { + figure: 'twins', + title: 'The same parts as the app', + body: 'KPI and Flow have the parts and props of the React Native library. KPI runs on the server and sends no JavaScript.', + }, +]; + const FEATURES: { figure: IsoFigureKind; title: string; body: string }[] = [ { figure: 'tailwind', @@ -156,6 +179,10 @@ const FAQ = [ q: 'How many components are there?', a: `${COMPONENT_COUNT}, covering overlays, forms, feedback, data and layout — from bottom sheets and popovers to chat transcripts, animated line charts, file attachments, timelines and toasts.`, }, + { + q: 'Does PanelUI work on the web?', + a: 'The React Native library is for iOS and Android. For the web there is a separate collection of React components built on shadcn/ui, installed one at a time from a registry or from npm. Components that exist in both, such as KPI and Flow, have the same parts and props.', + }, ]; export default function HomePage() { @@ -197,13 +224,15 @@ export default function HomePage() { {/* Hero */}
{COMPONENT_COUNT} components · MIT · Expo SDK 57+ -

- React Native UI components for Expo, styled with Tailwind CSS +

+ Native-feeling UI. Zero native code.

-

- Accessible, high-performance components — bottom sheets, dialogs, selects, toasts, - forms — animated on the UI thread with Reanimated. Zero native code, so it runs in - Expo Go. +

+ Tailwind-styled components for Expo that run in Expo Go, and now{' '} + + on the web + {' '} + too.

@@ -221,6 +250,66 @@ export default function HomePage() { + {/* The web collection */} +
+
+
+

+ And built for the web you already have +

+

+ A collection of components for React, built on shadcn/ui. Each one is copied into + your project, drawn in your theme, and usable from the keyboard. +

+
+ + {/* + One card the width of the section over two that share it, where + the section above is a grid of six equals. The wide card is the + figure with two machines on it, so its panel is the shape of a + bench, and its words sit beside the bench and not under it. + */} +
+ {WEB_FEATURES.map(({ figure, title, body }, index) => + index === 0 ? ( + + + {title} + {body} + +
+ +
+
+ ) : ( + +
+ +
+ + {title} + {body} + +
+ ) + )} +
+ +
+ + +
+
+
+ {/* Features */}
@@ -425,7 +514,9 @@ export default function HomePage() {

-