@shadow-js/ui 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/dist/cli.mjs +2293 -0
- package/dist/dev/cli/schema.d.ts +73 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/locales/ar.d.ts +141 -0
- package/dist/dev/locales/ar.js +128 -0
- package/dist/dev/locales/ar.js.map +1 -0
- package/dist/dev/locales/en.d.ts +141 -0
- package/dist/dev/locales/en.js +128 -0
- package/dist/dev/locales/en.js.map +1 -0
- package/dist/dev/registry/utils/utils.d.ts +29 -0
- package/dist/dev/registry/utils/utils.js +27 -0
- package/dist/dev/registry/utils/utils.js.map +1 -0
- package/dist/dev/theme/theme-logic.d.ts +187 -0
- package/dist/dev/theme/theme-logic.js +184 -0
- package/dist/dev/theme/theme-logic.js.map +1 -0
- package/dist/dev/theme/theme-provider.d.ts +90 -0
- package/dist/dev/theme/theme-provider.js +150 -0
- package/dist/dev/theme/theme-provider.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/locales/ar.js +2 -0
- package/dist/prod/locales/ar.js.map +1 -0
- package/dist/prod/locales/en.js +2 -0
- package/dist/prod/locales/en.js.map +1 -0
- package/dist/prod/registry/utils/utils.js +2 -0
- package/dist/prod/registry/utils/utils.js.map +1 -0
- package/dist/prod/theme/theme-logic.js +2 -0
- package/dist/prod/theme/theme-logic.js.map +1 -0
- package/dist/prod/theme/theme-provider.js +2 -0
- package/dist/prod/theme/theme-provider.js.map +1 -0
- package/package.json +102 -4
- package/registry/accordion/accordion.tsx +210 -0
- package/registry/accordion/item.json +4 -0
- package/registry/alert/alert.tsx +200 -0
- package/registry/alert/item.json +4 -0
- package/registry/alert-dialog/alert-dialog.tsx +530 -0
- package/registry/alert-dialog/item.json +4 -0
- package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
- package/registry/aspect-ratio/item.json +4 -0
- package/registry/attachment/attachment.tsx +459 -0
- package/registry/attachment/item.json +4 -0
- package/registry/avatar/avatar.tsx +385 -0
- package/registry/avatar/item.json +4 -0
- package/registry/badge/badge.tsx +108 -0
- package/registry/badge/item.json +4 -0
- package/registry/breadcrumb/breadcrumb.tsx +354 -0
- package/registry/breadcrumb/item.json +4 -0
- package/registry/bubble/bubble.tsx +260 -0
- package/registry/bubble/item.json +4 -0
- package/registry/button/button.tsx +125 -0
- package/registry/button/item.json +4 -0
- package/registry/button-group/button-group.tsx +189 -0
- package/registry/button-group/item.json +4 -0
- package/registry/calendar/calendar.tsx +348 -0
- package/registry/calendar/item.json +4 -0
- package/registry/card/card.tsx +311 -0
- package/registry/card/item.json +4 -0
- package/registry/carousel/carousel.tsx +851 -0
- package/registry/carousel/item.json +4 -0
- package/registry/chart/chart.tsx +1956 -0
- package/registry/chart/item.json +4 -0
- package/registry/chart-scale/chart-scale.ts +670 -0
- package/registry/chart-scale/item.json +5 -0
- package/registry/checkbox/checkbox.tsx +79 -0
- package/registry/checkbox/item.json +4 -0
- package/registry/code-block/code-block.tsx +470 -0
- package/registry/code-block/item.json +4 -0
- package/registry/collapsible/collapsible.tsx +117 -0
- package/registry/collapsible/item.json +4 -0
- package/registry/combobox/combobox.tsx +977 -0
- package/registry/combobox/item.json +4 -0
- package/registry/command/command.tsx +757 -0
- package/registry/command/item.json +4 -0
- package/registry/context-menu/context-menu.tsx +697 -0
- package/registry/context-menu/item.json +4 -0
- package/registry/data-table/data-table.tsx +437 -0
- package/registry/data-table/item.json +4 -0
- package/registry/date-picker/date-picker.tsx +349 -0
- package/registry/date-picker/item.json +4 -0
- package/registry/dialog/dialog.tsx +412 -0
- package/registry/dialog/item.json +4 -0
- package/registry/direction/direction.tsx +88 -0
- package/registry/direction/item.json +4 -0
- package/registry/drawer/drawer.tsx +787 -0
- package/registry/drawer/item.json +4 -0
- package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
- package/registry/dropdown-menu/item.json +4 -0
- package/registry/empty/empty.tsx +240 -0
- package/registry/empty/item.json +4 -0
- package/registry/field/field.tsx +624 -0
- package/registry/field/item.json +4 -0
- package/registry/form/form.tsx +1112 -0
- package/registry/form/item.json +4 -0
- package/registry/hover-card/hover-card.tsx +144 -0
- package/registry/hover-card/item.json +4 -0
- package/registry/icons/icons.tsx +1691 -0
- package/registry/icons/item.json +4 -0
- package/registry/input/input.tsx +43 -0
- package/registry/input/item.json +4 -0
- package/registry/input-group/input-group.tsx +315 -0
- package/registry/input-group/item.json +4 -0
- package/registry/input-otp/input-otp.tsx +329 -0
- package/registry/input-otp/item.json +4 -0
- package/registry/item/item.json +4 -0
- package/registry/item/item.tsx +439 -0
- package/registry/kbd/item.json +4 -0
- package/registry/kbd/kbd.tsx +65 -0
- package/registry/label/item.json +4 -0
- package/registry/label/label.tsx +60 -0
- package/registry/marker/item.json +4 -0
- package/registry/marker/marker.tsx +175 -0
- package/registry/menubar/item.json +4 -0
- package/registry/menubar/menubar.tsx +694 -0
- package/registry/message/item.json +4 -0
- package/registry/message/message.tsx +280 -0
- package/registry/message-scroller/item.json +4 -0
- package/registry/message-scroller/message-scroller.tsx +262 -0
- package/registry/native-select/item.json +4 -0
- package/registry/native-select/native-select.tsx +113 -0
- package/registry/navigation-menu/item.json +4 -0
- package/registry/navigation-menu/navigation-menu.tsx +362 -0
- package/registry/pagination/item.json +4 -0
- package/registry/pagination/pagination.tsx +460 -0
- package/registry/password-input/item.json +5 -0
- package/registry/password-input/password-input.tsx +494 -0
- package/registry/popover/item.json +4 -0
- package/registry/popover/popover.tsx +284 -0
- package/registry/progress/item.json +4 -0
- package/registry/progress/progress.tsx +190 -0
- package/registry/questionnaire/item.json +4 -0
- package/registry/questionnaire/questionnaire.tsx +569 -0
- package/registry/radio-group/item.json +4 -0
- package/registry/radio-group/radio-group.tsx +101 -0
- package/registry/resizable/item.json +4 -0
- package/registry/resizable/resizable.tsx +455 -0
- package/registry/scroll-area/item.json +4 -0
- package/registry/scroll-area/scroll-area.tsx +149 -0
- package/registry/select/item.json +4 -0
- package/registry/select/select.tsx +571 -0
- package/registry/separator/item.json +4 -0
- package/registry/separator/separator.tsx +52 -0
- package/registry/sheet/item.json +4 -0
- package/registry/sheet/sheet.tsx +476 -0
- package/registry/sidebar/item.json +4 -0
- package/registry/sidebar/sidebar.tsx +1870 -0
- package/registry/skeleton/item.json +4 -0
- package/registry/skeleton/skeleton.tsx +47 -0
- package/registry/slider/item.json +4 -0
- package/registry/slider/slider.tsx +149 -0
- package/registry/spinner/item.json +4 -0
- package/registry/spinner/spinner.tsx +51 -0
- package/registry/switch/item.json +4 -0
- package/registry/switch/switch.tsx +93 -0
- package/registry/table/item.json +4 -0
- package/registry/table/table.tsx +373 -0
- package/registry/tabs/item.json +4 -0
- package/registry/tabs/tabs.tsx +209 -0
- package/registry/textarea/item.json +4 -0
- package/registry/textarea/textarea.tsx +51 -0
- package/registry/toast/item.json +4 -0
- package/registry/toast/toast.tsx +949 -0
- package/registry/toggle/item.json +4 -0
- package/registry/toggle/toggle.tsx +110 -0
- package/registry/toggle-group/item.json +4 -0
- package/registry/toggle-group/toggle-group.tsx +189 -0
- package/registry/tooltip/item.json +4 -0
- package/registry/tooltip/tooltip.tsx +169 -0
- package/registry/trace/item.json +4 -0
- package/registry/trace/trace.tsx +356 -0
- package/registry/ui-strings/item.json +5 -0
- package/registry/ui-strings/ui-strings.tsx +454 -0
- package/registry/use-highlighted-code/item.json +5 -0
- package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
- package/registry/use-mobile/item.json +5 -0
- package/registry/use-mobile/use-mobile.ts +46 -0
- package/registry/utils/item.json +5 -0
- package/registry/utils/utils.ts +38 -0
- package/registry.json +3274 -0
- package/styles/animations.css +321 -0
- package/styles/theme-core.css +55 -0
- package/styles/theme.css +12 -0
- package/styles/themes/gray.css +69 -0
- package/styles/themes/neutral.css +69 -0
- package/styles/themes/slate.css +69 -0
- package/styles/themes/stone.css +69 -0
- package/styles/themes/zinc.css +69 -0
- package/styles/typeset.css +251 -0
package/README.md
CHANGED
|
@@ -1,3 +1,599 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center"><img src="https://raw.githubusercontent.com/shadowjs-dev/ui/main/.github/banner.png" alt="ShadowJS" width="100%"></p>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# @shadow-js/ui
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@shadow-js/ui)
|
|
6
|
+
[](https://github.com/shadowjs-dev/ui/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/shadowjs-dev/ui/blob/main/LICENSE)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
|
|
11
|
+
shadcn/ui for ShadowJS 1.0.0. The same model: you do not install components, you **copy them into your
|
|
12
|
+
project** and own them. They are styled with Tailwind v4 and the shadcn token set (`bg-background`,
|
|
13
|
+
`text-muted-foreground`, `ring-ring`, ...), themed with CSS variables, dark mode with a `.dark` class, and
|
|
14
|
+
they get their keyboard, focus and ARIA behavior from [`@shadow-js/primitives`](https://primitives.shadowjs.net).
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
`@shadow-js/ui` is the `shadow-ui` command plus a few shared helpers. Run it with `npx`; nothing needs to be
|
|
18
|
+
installed first. The project needs `@shadow-js/core`, `@shadow-js/web` and (for the components)
|
|
19
|
+
`@shadow-js/primitives`, a bundler (Vite with `@shadow-js/vite`, or Dusk) and Tailwind v4.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @shadow-js/ui init
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`init` is safe to re-run: an existing file is kept unless you pass `--overwrite`. The exception is
|
|
26
|
+
`components.json`: when `--base-color` or `--css` changes its value, the file is rewritten in the standard layout
|
|
27
|
+
(its aliases stay; other keys are dropped).
|
|
28
|
+
|
|
29
|
+
| It does | Where |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| writes `components.json` (framework, stylesheet, base color, aliases) | project root |
|
|
32
|
+
| writes the theme stylesheet (base color variables, the Tailwind v4 token mapping, the animation utilities) | `shadow-ui.css`, next to your stylesheet |
|
|
33
|
+
| adds `@import "./shadow-ui.css";` after your Tailwind import (asks first, or `--yes`) | your stylesheet |
|
|
34
|
+
| writes `cn()` (clsx + tailwind-merge) | `@/lib/utils` |
|
|
35
|
+
| installs `clsx` and `tailwind-merge` (plus `tailwindcss` and `@tailwindcss/vite` when missing) with the package manager your lockfile indicates (the `packageManager` field wins, npm is the default) | terminal |
|
|
36
|
+
| prints what is left to do by hand (tsconfig alias, Vite alias, `ThemeProvider`) | terminal |
|
|
37
|
+
|
|
38
|
+
Options: `--base-color neutral|zinc|slate|stone|gray`, `--css src/styles.css`, `--yes`, `--overwrite`,
|
|
39
|
+
`--dry-run`, `--skip-install`, `--version` (the version of the installed package). `--base-color` and `--css` apply on
|
|
40
|
+
every run: with a `components.json` already there, `init` updates those two keys (your aliases stay) and regenerates the
|
|
41
|
+
theme only while it is still the file `init` wrote; `--overwrite` replaces everything. The Vite alias and Tailwind plugin
|
|
42
|
+
steps (also on Dusk) are printed only while `vite.config.*` lacks them. Without a terminal every question counts as
|
|
43
|
+
"no", so pass `--yes` in scripts.
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx @shadow-js/ui add button dialog
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```tsx title="src/edit-profile.tsx" run mount="EditProfile"
|
|
52
|
+
import { Button } from "@/components/ui/button";
|
|
53
|
+
import {
|
|
54
|
+
Dialog,
|
|
55
|
+
DialogContent,
|
|
56
|
+
DialogDescription,
|
|
57
|
+
DialogHeader,
|
|
58
|
+
DialogTitle,
|
|
59
|
+
DialogTrigger,
|
|
60
|
+
} from "@/components/ui/dialog";
|
|
61
|
+
|
|
62
|
+
export function EditProfile() {
|
|
63
|
+
return (
|
|
64
|
+
<Dialog>
|
|
65
|
+
<DialogTrigger as={Button} variant="outline">
|
|
66
|
+
Edit profile
|
|
67
|
+
</DialogTrigger>
|
|
68
|
+
<DialogContent>
|
|
69
|
+
<DialogHeader>
|
|
70
|
+
<DialogTitle>Edit profile</DialogTitle>
|
|
71
|
+
<DialogDescription>Make changes to your profile here.</DialogDescription>
|
|
72
|
+
</DialogHeader>
|
|
73
|
+
</DialogContent>
|
|
74
|
+
</Dialog>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```tsx title="edit-profile.test.tsx" test
|
|
80
|
+
import { cleanup, render, userEvent } from "@shadow-js/testing/dom";
|
|
81
|
+
import { afterEach, expect, it } from "vitest";
|
|
82
|
+
import { EditProfile } from "./src/edit-profile";
|
|
83
|
+
|
|
84
|
+
afterEach(cleanup);
|
|
85
|
+
|
|
86
|
+
it("opens a dialog named by its title, from a trigger that is a Button", () => {
|
|
87
|
+
const view = render(() => <EditProfile />);
|
|
88
|
+
const trigger = view.getByRole("button", { name: "Edit profile" });
|
|
89
|
+
expect(trigger.getAttribute("data-slot")).toBe("dialog-trigger");
|
|
90
|
+
expect(trigger.className).toContain("border");
|
|
91
|
+
userEvent.click(trigger);
|
|
92
|
+
const dialog = view.getByRole("dialog", { name: "Edit profile" });
|
|
93
|
+
expect(dialog.getAttribute("aria-modal")).toBe("true");
|
|
94
|
+
userEvent.keyboard("{Escape}");
|
|
95
|
+
expect(view.queryByRole("dialog")).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## The CLI
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
shadow-ui init # components.json, lib/utils and the theme (see Install)
|
|
103
|
+
shadow-ui add <name...> # copy components and everything they import (transitively)
|
|
104
|
+
shadow-ui add --all
|
|
105
|
+
shadow-ui list # the registry; * marks components already in your project
|
|
106
|
+
shadow-ui diff [name...] # your copy vs the registry, as a unified diff (exit 1 if it differs)
|
|
107
|
+
shadow-ui remove <name...> [--force]
|
|
108
|
+
shadow-ui completion <bash|zsh|fish> # print a tab-completion script (see below)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`add` resolves `registryDependencies` (`alert-dialog` brings `button`, `dialog` brings `icons`; every component
|
|
112
|
+
brings `utils`), rewrites the imports in the copied files to **your** aliases from `components.json`, writes only
|
|
113
|
+
inside the project (an alias or `--path` that points outside is refused, symbolic links are resolved first), and
|
|
114
|
+
installs the npm packages the files import that your `package.json` lacks: other packages at the exact versions the
|
|
115
|
+
registry pins, and `@shadow-js/core`, `@shadow-js/web` and `@shadow-js/primitives` (for components built on a
|
|
116
|
+
primitive) without a version.
|
|
117
|
+
|
|
118
|
+
| Flag | Meaning |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `--overwrite` / `-o` | replace files that differ from the registry. `--yes` alone never does this |
|
|
121
|
+
| `--dry-run` | print the plan and the diffs, change nothing, install nothing |
|
|
122
|
+
| `--yes` / `-y` | answer yes to "install packages?" and "edit your stylesheet?" |
|
|
123
|
+
| `--skip-install` | print the install command instead of running it |
|
|
124
|
+
| `--path <dir>` | `add`: write `ui` components to `<dir>` instead of the configured alias |
|
|
125
|
+
| `--all` / `-a` | `add`: every component |
|
|
126
|
+
| `--force` | `remove`: even when another component in your project imports it |
|
|
127
|
+
| `--cwd <dir>` | run in another directory |
|
|
128
|
+
| `--json` | every command: print one JSON document on stdout instead of words, and ask nothing (see "Output for scripts") |
|
|
129
|
+
| `--base-color <color>` | `init`: `neutral` (default), `zinc`, `slate`, `stone` or `gray` |
|
|
130
|
+
| `--css <file>` | `init`: the stylesheet that imports Tailwind |
|
|
131
|
+
|
|
132
|
+
`ls` and `rm` are aliases of `list` and `remove`. `shadow-ui help` (or `-h`) prints the usage, `-v` prints the
|
|
133
|
+
version, and `shadow-ui` with no command prints the usage and exits with code 1.
|
|
134
|
+
|
|
135
|
+
`shadow-ui completion` prints a script that completes the commands and the long flags. Install it once:
|
|
136
|
+
`source <(shadow-ui completion bash)` in bash (or save the output as `shadow-ui` in your bash-completion directory),
|
|
137
|
+
save `shadow-ui completion zsh` as `_shadow-ui` in a directory on your `$fpath` (zsh), or run
|
|
138
|
+
`shadow-ui completion fish > ~/.config/fish/completions/shadow-ui.fish` (fish). The script lists the flags, not
|
|
139
|
+
component names.
|
|
140
|
+
|
|
141
|
+
### Output for scripts
|
|
142
|
+
|
|
143
|
+
`--json` works on every command (and before it: `shadow-ui --json list`). It prints one JSON document on stdout, and
|
|
144
|
+
nothing else there: the progress lines are in `data`, warnings and errors also go to stderr, an installer's own output
|
|
145
|
+
goes to stderr, and nothing is asked (a file that differs is kept unless you pass `--overwrite`, as with `--yes`). The
|
|
146
|
+
exit code is the same with and without `--json`.
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"schema": 1,
|
|
151
|
+
"command": "shadow-ui add",
|
|
152
|
+
"ok": true,
|
|
153
|
+
"data": {
|
|
154
|
+
"dryRun": false,
|
|
155
|
+
"requested": ["dialog"],
|
|
156
|
+
"dependencies": ["utils", "button"],
|
|
157
|
+
"files": [
|
|
158
|
+
{ "path": "src/lib/utils.ts", "action": "unchanged" },
|
|
159
|
+
{ "path": "src/components/ui/button.tsx", "action": "create" },
|
|
160
|
+
{ "path": "src/components/ui/dialog.tsx", "action": "create" }
|
|
161
|
+
],
|
|
162
|
+
"installs": [{ "status": "skipped", "manager": "npm", "packages": ["clsx@2.1.1"], "command": "npm install clsx@2.1.1" }]
|
|
163
|
+
},
|
|
164
|
+
"warnings": []
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`ok` is true exactly when the exit code is 0. A failure adds `error: { "code": "SHADOW_UI_...", "message": "..." }`,
|
|
169
|
+
and `data` still holds what is known (a kept file with its diff, the components that differ). Aliases are named by their
|
|
170
|
+
command: `ls` is `shadow-ui list`, `rm` is `shadow-ui remove`.
|
|
171
|
+
|
|
172
|
+
| Command | `data` |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `init` | `dryRun`, `config` (`baseColor`, `css`), `files` (`path`, `action`: `create`, `update`, `unchanged`, `kept` or `skipped`), `installs`, `nextSteps` |
|
|
175
|
+
| `add` | `dryRun`, `requested`, `dependencies` (what came with them), `files` (`path`, `action`: `create`, `unchanged`, `overwrite`, `kept` or `differs` for a dry run, plus the `diff` of a file that differs), `installs` |
|
|
176
|
+
| `list` | `initialized` (is there a `components.json`), `items` (`name`, `type`, `description`, `installed`) |
|
|
177
|
+
| `diff` | `components` (`name`, `path`, `status`: `up-to-date`, `differs` with its `diff`, or `missing`), `checked`, `differing` |
|
|
178
|
+
| `remove` | `dryRun`, `removed` (`name`, `path`), `blocked` (`name`, `usedBy`) |
|
|
179
|
+
| `completion` | `shell`, `script` |
|
|
180
|
+
| `--help`, `--version` | `usage`, `version` |
|
|
181
|
+
|
|
182
|
+
`installs` lists each package-manager call as `status` (`installed`, `skipped`, `dry-run` or `failed`), `manager`, `packages` and
|
|
183
|
+
`command`. The error codes: `SHADOW_UI_UNKNOWN_COMMAND`, `SHADOW_UI_UNKNOWN_OPTION`, `SHADOW_UI_INVALID_OPTION`,
|
|
184
|
+
`SHADOW_UI_MISSING_COMMAND`, `SHADOW_UI_INVALID_SHELL`, `SHADOW_UI_INVALID_BASE_COLOR`, `SHADOW_UI_NO_PROJECT` (no
|
|
185
|
+
`package.json`), `SHADOW_UI_NO_CONFIG` (run `init` first), `SHADOW_UI_NO_COMPONENTS`, `SHADOW_UI_UNKNOWN_COMPONENT`,
|
|
186
|
+
`SHADOW_UI_INVALID_NAME`, `SHADOW_UI_REGISTRY_CONFLICT`, `SHADOW_UI_INVALID_FILE`, `SHADOW_UI_UNSAFE_PATH`,
|
|
187
|
+
`SHADOW_UI_FILES_DIFFER` (`add` kept a file), `SHADOW_UI_DIFFERS` (`diff` found a difference), `SHADOW_UI_IN_USE`
|
|
188
|
+
(`remove` refused), `SHADOW_UI_INSTALL_FAILED`, `SHADOW_UI_FAILED` and `SHADOW_UI_UNEXPECTED`.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# Fail a CI step when a copied component drifted from the registry, and say which files.
|
|
192
|
+
shadow-ui diff --json | jq -r '.data.components[] | select(.status == "differs") | .path'
|
|
193
|
+
# The components you have not added yet.
|
|
194
|
+
shadow-ui list --json | jq -r '.data.items[] | select(.installed | not) | .name'
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
A file that differs is shown as a diff and kept (exit code 1) unless you pass `--overwrite` or answer yes to the
|
|
198
|
+
prompt. The registry ships inside this package: no network is needed except for installing npm packages.
|
|
199
|
+
|
|
200
|
+
## Project setup
|
|
201
|
+
|
|
202
|
+
`init` prints these when they are missing. TypeScript needs the alias and so does the bundler.
|
|
203
|
+
|
|
204
|
+
```jsonc
|
|
205
|
+
// tsconfig.json
|
|
206
|
+
{ "compilerOptions": { "jsx": "preserve", "jsxImportSource": "@shadow-js/web", "paths": { "@/*": ["./src/*"] } } }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
In `vite.config.ts` add `resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } }` and
|
|
210
|
+
`tailwindcss()` from `@tailwindcss/vite` next to the ShadowJS plugin (`shadow()`, or `dusk()` in a Dusk app).
|
|
211
|
+
A Dusk app needs both too: Dusk adds neither the `@` alias nor Tailwind, so `init` prints both steps for it when they
|
|
212
|
+
are missing. For a new stylesheet, Dusk defaults to `src/app.css`. The entry stylesheet:
|
|
213
|
+
|
|
214
|
+
```css
|
|
215
|
+
/* src/index.css */
|
|
216
|
+
@import "tailwindcss";
|
|
217
|
+
@import "./shadow-ui.css";
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Server-rendered pages hydrate. A dialog that is open at render time is server-rendered too: its portal markup goes to
|
|
221
|
+
the end of `<body>`, and hydration adopts it.
|
|
222
|
+
## Theming
|
|
223
|
+
|
|
224
|
+
Colors are CSS variables in `:root` and `.dark`; Tailwind v4 maps them with `@theme inline`, so `bg-primary`
|
|
225
|
+
works and a theme change is a variable change. The generated `shadow-ui.css` contains:
|
|
226
|
+
|
|
227
|
+
- the variables of your base color (`neutral` default; `zinc`, `slate`, `stone`, `gray`; each also ships as
|
|
228
|
+
`@shadow-js/ui/themes/<base>.css` with the same variable set),
|
|
229
|
+
- the radius scale (`--radius` drives `rounded-sm|md|lg|xl`),
|
|
230
|
+
- `@custom-variant dark (&:is(.dark, .dark *))` and the base layer (`border-border`, `bg-background`),
|
|
231
|
+
- the animation utilities (`animate-in`, `fade-in-0`, `zoom-in-95`, `slide-in-from-top-2`, `animate-accordion-down`...),
|
|
232
|
+
a small stand-in for `tw-animate-css`, collapsed under `prefers-reduced-motion`.
|
|
233
|
+
|
|
234
|
+
Prefer imports over copies? `@import "@shadow-js/ui/theme.css"; @import "@shadow-js/ui/animations.css";` after
|
|
235
|
+
Tailwind does the same from the package for the `neutral` base color (import `@shadow-js/ui/themes/<base>.css` after
|
|
236
|
+
`theme.css` for another one). Change the look by editing the variables:
|
|
237
|
+
|
|
238
|
+
```css
|
|
239
|
+
:root { --radius: 0.5rem; --primary: oklch(0.55 0.2 260); --primary-foreground: oklch(0.98 0 0); }
|
|
240
|
+
.dark { --primary: oklch(0.7 0.15 260); }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Chart tokens (`--chart-1` to `--chart-5`) and sidebar tokens (`--sidebar*`) are included.
|
|
244
|
+
|
|
245
|
+
## Dark mode
|
|
246
|
+
|
|
247
|
+
Dark mode is the `dark` class on `<html>` (or a `data-*` attribute). Two pieces, both optional: a provider that
|
|
248
|
+
keeps the choice, and a script in `<head>` that applies it before the first paint. With `attribute="data-theme"`, the
|
|
249
|
+
stylesheet's dark styles (`@custom-variant dark` and the `.dark` variables) must select `[data-theme=dark]` instead.
|
|
250
|
+
|
|
251
|
+
```tsx title="src/mode-toggle.tsx" run mount="App"
|
|
252
|
+
import { ThemeProvider, useTheme } from "@shadow-js/ui";
|
|
253
|
+
import { Button } from "@/components/ui/button";
|
|
254
|
+
|
|
255
|
+
export function ModeToggle() {
|
|
256
|
+
const { theme, resolvedTheme, setTheme } = useTheme();
|
|
257
|
+
return (
|
|
258
|
+
<Button
|
|
259
|
+
variant="outline"
|
|
260
|
+
size="icon"
|
|
261
|
+
aria-label={`Theme: ${theme()}`}
|
|
262
|
+
onClick={() => setTheme(resolvedTheme() === "dark" ? "light" : "dark")}
|
|
263
|
+
>
|
|
264
|
+
{resolvedTheme() === "dark" ? "Dark" : "Light"}
|
|
265
|
+
</Button>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function App() {
|
|
270
|
+
return (
|
|
271
|
+
<ThemeProvider defaultTheme="system" storageKey="my-app-theme">
|
|
272
|
+
<ModeToggle />
|
|
273
|
+
</ThemeProvider>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
```tsx title="mode-toggle.test.tsx" test
|
|
279
|
+
import { fakeColorScheme } from "@docs/fixtures";
|
|
280
|
+
import { cleanup, render, userEvent } from "@shadow-js/testing/dom";
|
|
281
|
+
import { afterEach, beforeEach, expect, it } from "vitest";
|
|
282
|
+
import { App } from "./src/mode-toggle";
|
|
283
|
+
|
|
284
|
+
beforeEach(() => {
|
|
285
|
+
window.localStorage.clear();
|
|
286
|
+
document.documentElement.className = "";
|
|
287
|
+
});
|
|
288
|
+
afterEach(cleanup);
|
|
289
|
+
|
|
290
|
+
it("starts from the system, toggles the html class and remembers the choice", () => {
|
|
291
|
+
const system = fakeColorScheme(false);
|
|
292
|
+
const view = render(() => <App />);
|
|
293
|
+
const button = view.getByRole("button");
|
|
294
|
+
expect(document.documentElement.classList.contains("light")).toBe(true);
|
|
295
|
+
userEvent.click(button);
|
|
296
|
+
expect(button.getAttribute("aria-label")).toBe("Theme: dark");
|
|
297
|
+
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
|
298
|
+
expect(window.localStorage.getItem("my-app-theme")).toBe("dark");
|
|
299
|
+
system.restore();
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`ThemeProvider` persists the choice in `localStorage`, follows `prefers-color-scheme` while the theme is
|
|
304
|
+
`"system"`, syncs between tabs, and is SSR-safe: it touches nothing before it has mounted, so `resolvedTheme()`
|
|
305
|
+
is `undefined` on the server and on the first client render (hydration never mismatches). If the browser blocks
|
|
306
|
+
`localStorage`, the choice still applies for the current page.
|
|
307
|
+
|
|
308
|
+
**No flash**: `themeScript()` returns a dependency-free ES5 script as text; put it in a `<script>` in `<head>` and the
|
|
309
|
+
theme is applied before first paint. `themeScriptTag({ nonce, ...options })` returns it wrapped in a `<script nonce>` tag for servers
|
|
310
|
+
that build their own document. Pass the same `storageKey`, `defaultTheme`, `attribute` (`"class"` or a `data-*` name such as `"data-theme"`)
|
|
311
|
+
and `enableSystem` to the script and the provider.
|
|
312
|
+
|
|
313
|
+
```ts title="src/theme-head.ts" typecheck
|
|
314
|
+
import { themeScript, themeScriptTag } from "@shadow-js/ui";
|
|
315
|
+
|
|
316
|
+
export const inline = themeScript({ defaultTheme: "system" }); // JavaScript text
|
|
317
|
+
export const tag = themeScriptTag({ nonce: "abc123", storageKey: "my-app-theme" }); // a ready <script nonce="abc123">
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
In Dusk, the metadata API does not put scripts in `<head>`. Use `createHandler`'s `document.head` option instead: it can
|
|
321
|
+
be a function that receives the request event, so `document: { head: (event) => themeScriptTag({ nonce: event.nonce }) }`
|
|
322
|
+
puts the nonce of that response on the script.
|
|
323
|
+
## Components
|
|
324
|
+
|
|
325
|
+
Every component below is `shadow-ui add <name>`. Item names are kebab-case; exports keep shadcn's names.
|
|
326
|
+
`as` replaces `asChild`.
|
|
327
|
+
|
|
328
|
+
<!-- COMPONENTS-TABLE:start -->
|
|
329
|
+
| Item | What it is | Exports | Also adds |
|
|
330
|
+
| --- | --- | --- | --- |
|
|
331
|
+
| `accordion` | A vertically stacked set of interactive headings that each reveal a section of content. | Accordion, AccordionContent, AccordionItem, AccordionTrigger | `icons` |
|
|
332
|
+
| `alert` | Displays a callout for user attention. | Alert, AlertAction, AlertDescription, AlertTitle, alertVariants | |
|
|
333
|
+
| `alert-dialog` | A modal dialog that interrupts the user with important content and expects a response. | AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, ... | `button` |
|
|
334
|
+
| `aspect-ratio` | Displays content within a desired ratio. | AspectRatio | |
|
|
335
|
+
| `attachment` | A file or an image with its media, name, details, upload state and actions, for composers, threads and upload lists. | Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, ... | `button` |
|
|
336
|
+
| `avatar` | User images with fallbacks, size variants, status badges and overlapping groups. | Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage, ... | |
|
|
337
|
+
| `badge` | Displays a badge or a component that looks like a badge. | Badge, badgeVariants | |
|
|
338
|
+
| `breadcrumb` | Displays the path to the current resource using a hierarchy of links. | Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, ... | `icons`, `ui-strings` |
|
|
339
|
+
| `bubble` | A framed message bubble for chat text, short structured output, quoted replies, suggestions and reactions. | Bubble, BubbleContent, BubbleGroup, BubbleReactions, bubbleVariants | |
|
|
340
|
+
| `button` | Displays a button or a component that looks like a button. | Button, buttonVariants | |
|
|
341
|
+
| `button-group` | Buttons and inputs that sit together as one control, with the inner corners square and the borders joined. | ButtonGroup, ButtonGroupSeparator, ButtonGroupText | |
|
|
342
|
+
| `calendar` | A month grid to pick one day, several days or a range, with a keyboard grid, locale names and month and year navigation. | Calendar, CalendarDayCell, CalendarLabels, isCalendarDay | `button`, `icons`, `ui-strings` |
|
|
343
|
+
| `card` | Displays a card with header, content, and footer. | Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, ... | |
|
|
344
|
+
| `carousel` | Slides with buttons, arrow keys, indicators, drag and opt-in autoplay, on native scroll-snap; no carousel library. | Carousel, CarouselApi, CarouselContent, CarouselEvent, CarouselIndicators, CarouselItem, ... | `button`, `icons`, `ui-strings` |
|
|
345
|
+
| `chart` | Themeable SVG line, area, bar, pie, donut and radial charts with axes, a keyboard-navigable tooltip and legend, and a table for screen readers. | ChartActivePoint, ChartArea, ChartBar, ChartConfig, ChartContainer, ChartDatum, ... | `chart-scale`, `ui-strings` |
|
|
346
|
+
| `chart-scale` | Pure functions for charts: scales, nice ticks, line, area and arc paths, pie angles, stacking and decimation. | ChartCurve, ChartPoint, PieAngle, PieAngleOptions, arcPath, areaPath, ... | |
|
|
347
|
+
| `checkbox` | A control that allows the user to toggle between checked, unchecked and indeterminate. | Checkbox | `icons` |
|
|
348
|
+
| `code-block` | Code, terminal output, logs and diffs, with an optional header, line numbers, wrapping and a copy button. | CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockFooter, CodeBlockHeader, ... | `button`, `icons`, `ui-strings` |
|
|
349
|
+
| `collapsible` | An interactive component which expands/collapses a panel. | Collapsible, CollapsibleContent, CollapsibleTrigger | |
|
|
350
|
+
| `combobox` | An input with a filtered list of options, with single or multiple (chips) selection. | Combobox, ComboboxChip, ComboboxChipRemove, ComboboxChips, ComboboxChipsInput, ComboboxClear, ... | `icons`, `ui-strings` |
|
|
351
|
+
| `command` | A command palette: a search field and a filtered, keyboard-navigable list of commands, inline or in a dialog. | Command, CommandDialog, CommandEmpty, CommandFilter, CommandGroup, CommandInput, ... | `dialog`, `icons`, `ui-strings` |
|
|
352
|
+
| `context-menu` | Displays a menu to the user, such as a set of actions or functions, triggered by a right click or long press. | ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ... | `icons` |
|
|
353
|
+
| `data-table` | A data table over @shadow-js/table: sortable headers, pagination, column visibility and selection state. | DataTable, DataTableColumnHeader, DataTablePagination, DataTablePaginationLabels, DataTableViewOptions | `button`, `dropdown-menu`, `table`, `ui-strings` |
|
|
354
|
+
| `date-picker` | A date text field with a calendar in a popover; typed dates follow the locale and are checked against the limits. | DatePicker | `button`, `calendar`, `input`, `popover`, `ui-strings` |
|
|
355
|
+
| `dialog` | A window overlaid on the primary window, rendering the content underneath inert. | Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, ... | `icons`, `ui-strings` |
|
|
356
|
+
| `direction` | A provider that sets the reading direction (left to right or right to left) for the components below it, and a hook that reads it. | DirectionProvider, useDirection | |
|
|
357
|
+
| `drawer` | A bottom sheet for mobile: drag the grip to snap it between heights or dismiss it, with the keyboard as well; it is a dialog. | Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerDirection, DrawerFooter, ... | `ui-strings` |
|
|
358
|
+
| `dropdown-menu` | Displays a menu to the user, such as a set of actions or functions, triggered by a button. | DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, ... | `icons` |
|
|
359
|
+
| `empty` | An empty-state block: media, title, description and the next action. | Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle | |
|
|
360
|
+
| `field` | Form field layout: a label, a control, a description and an error, wired to one control with the ids a form needs. | Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, ... | |
|
|
361
|
+
| `form` | Accessible form wiring for any form library: a Form root, field ids, labels, descriptions, error messages and an error summary. | Form, FormApiLike, FormControl, FormDescription, FormErrorSummary, FormField, ... | `label`, `ui-strings` |
|
|
362
|
+
| `hover-card` | For sighted users to preview content available behind a link. | HoverCard, HoverCardContent, HoverCardTrigger | |
|
|
363
|
+
| `icons` | The inline SVG icons the components use (check, chevrons, close, search...). Replace with your icon library if you prefer. | AlignCenterIcon, AlignLeftIcon, AlignRightIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, ... | |
|
|
364
|
+
| `input` | A form input field. | Input | |
|
|
365
|
+
| `input-group` | A text control with addons (text, icons, buttons) on its sides, sharing one border and focus ring. | InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea | |
|
|
366
|
+
| `input-otp` | A one-time code with custom character patterns, digit normalization, paste, autofill and RTL keyboard navigation. | InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, REGEXP_ONLY_CHARS, REGEXP_ONLY_DIGITS, ... | `ui-strings` |
|
|
367
|
+
| `item` | A row of media, content and actions, in a group, that can be a link. | Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemGroup, ... | |
|
|
368
|
+
| `kbd` | Used to display textual user input from keyboard. | Kbd, KbdGroup | |
|
|
369
|
+
| `label` | Renders an accessible label associated with controls. | Label | |
|
|
370
|
+
| `marker` | An inline note in a conversation: a status update, a system message, a bordered row or a labeled separator. | Marker, MarkerContent, MarkerIcon, markerVariants | |
|
|
371
|
+
| `menubar` | A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands. | Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, ... | `icons` |
|
|
372
|
+
| `message` | One message in a conversation: the avatar, the alignment, a header, the content and a footer of actions. | Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader, ... | |
|
|
373
|
+
| `message-scroller` | A chat transcript scroller that keeps the reader's place while a reply streams: anchored turns, following the newest text, history loading and a jump button. | MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, ... | `icons`, `ui-strings` |
|
|
374
|
+
| `native-select` | The browser's own select, styled like an input, with its picker and keyboard support. | NativeSelect, NativeSelectOptGroup, NativeSelectOption | `icons` |
|
|
375
|
+
| `navigation-menu` | Site navigation with flyout content, keyboard arrows between triggers, Escape to close and focus return. | NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, ... | `icons` |
|
|
376
|
+
| `pagination` | Pagination with page navigation, next and previous links. | Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationItems, PaginationLink, ... | `button`, `icons`, `ui-strings` |
|
|
377
|
+
| `password-input` | A password field with a show and hide button, a strength meter slot and a Caps Lock hint, built on the PasswordToggleField primitive. | PasswordInput, PasswordInputState, PasswordStrength, scorePassword, usePasswordInput | `ui-strings` |
|
|
378
|
+
| `popover` | Displays rich content in a portal, triggered by a button. | Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, ... | |
|
|
379
|
+
| `progress` | A compact or composable progress bar with a track, indicator, visible label and value. | Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue | |
|
|
380
|
+
| `questionnaire` | A multi-step form of single-choice, multiple-choice, free-text and skippable questions, one at a time, with progress, validation and keyboard shortcuts. | Questionnaire, QuestionnaireActions, QuestionnaireChoice, QuestionnaireChoices, QuestionnaireDescription, QuestionnaireError, ... | `button`, `ui-strings` |
|
|
381
|
+
| `radio-group` | A set of checkable buttons, known as radio buttons, where no more than one can be checked at a time. | RadioGroup, RadioGroupItem | `icons` |
|
|
382
|
+
| `resizable` | Panels with draggable handles: pointer and keyboard resizing with bounds, an optional saved layout, and right-to-left support. | ResizableHandle, ResizablePanel, ResizablePanelGroup, resizeSizes | `icons`, `ui-strings` |
|
|
383
|
+
| `scroll-area` | A scrollable region with thin custom scrollbars, a draggable thumb and keyboard scrolling. | ScrollArea, ScrollBar | |
|
|
384
|
+
| `select` | Displays a list of options for the user to pick from, triggered by a button. | Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, ... | `icons` |
|
|
385
|
+
| `separator` | Visually or semantically separates content. | Separator | |
|
|
386
|
+
| `sheet` | Extends the Dialog component to display content that complements the main content of the screen. | Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, ... | `icons`, `ui-strings` |
|
|
387
|
+
| `sidebar` | A composable, collapsible sidebar that becomes a sheet on mobile, with menus, groups, tooltips and a Ctrl/Cmd+B shortcut. | SIDEBAR_COOKIE_NAME, Sidebar, SidebarContent, SidebarContextValue, SidebarCookieOptions, SidebarFooter, ... | `button`, `icons`, `input`, `separator`, `sheet`, `skeleton`, `tooltip`, `ui-strings`, `use-mobile` |
|
|
388
|
+
| `skeleton` | Use to show a placeholder while content is loading. | Skeleton | |
|
|
389
|
+
| `slider` | An input where the user selects a value, or a range, from within a given range. | Slider | |
|
|
390
|
+
| `spinner` | An indicator that can be used to show a loading state. | Spinner | `icons`, `ui-strings` |
|
|
391
|
+
| `switch` | A control that allows the user to toggle between checked and not checked. | Switch | |
|
|
392
|
+
| `table` | A responsive table component. | Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, ... | |
|
|
393
|
+
| `tabs` | A set of layered sections of content, known as tab panels, that are displayed one at a time. | Tabs, TabsContent, TabsList, TabsTrigger | |
|
|
394
|
+
| `textarea` | A multi-line text field that grows with its content. | Textarea | |
|
|
395
|
+
| `toast` | Toast stores (useToast in components, createToaster in module code) and the Toaster that shows their notifications, after Sonner. | ExternalToast, ToastAction, ToastAddOptions, ToastApi, ToastPosition, ToastPromiseContent, ... | `button`, `icons`, `ui-strings` |
|
|
396
|
+
| `toggle` | A two-state button that can be either on or off. | Toggle, toggleVariants | |
|
|
397
|
+
| `toggle-group` | A set of two-state buttons that can be toggled on or off. | ToggleGroup, ToggleGroupItem | `toggle` |
|
|
398
|
+
| `tooltip` | A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it. | Tooltip, TooltipContent, TooltipProvider, TooltipTrigger | |
|
|
399
|
+
| `trace` | A list of lines for the steps an agent takes: reasoning, tool calls, sources, plans and tasks, each with an icon, a title and a status, and optionally expandable. | Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceDescription, TraceIcon, ... | `collapsible`, `icons` |
|
|
400
|
+
| `ui-strings` | Translates the built-in words of the kit (Close, Next, Page n of m): a provider and a hook that read a pack such as the Arabic one. | UiStringValues, UiStringsCatalog, UiStringsProvider, UiTranslate, formatUiString, useUiLocale, ... | |
|
|
401
|
+
| `use-highlighted-code` | Syntax highlighting for code blocks with Shiki: a hook for the browser and an async function for the server, one element per line. | HighlightedLine, HighlightedToken, highlightCode, useHighlightedCode | |
|
|
402
|
+
| `use-mobile` | A hook that tells whether the viewport is narrower than 768px. Used by the sidebar. | useIsMobile | |
|
|
403
|
+
| `utils` | The cn() helper: clsx plus tailwind-merge. Every component imports it. | ClassValue, cn | |
|
|
404
|
+
<!-- COMPONENTS-TABLE:end -->
|
|
405
|
+
|
|
406
|
+
## Conventions
|
|
407
|
+
|
|
408
|
+
- **`class`, not `className`.** It merges with the component's classes through `cn()`: your classes win
|
|
409
|
+
(`<Button class="px-8">` replaces `px-4`).
|
|
410
|
+
- **`as` instead of `asChild`.** Parts people render as something else take `as`: a tag name or a component
|
|
411
|
+
(`<DialogTrigger as={Button} variant="outline">`, `<DropdownMenuItem as="a" href="/profile">`,
|
|
412
|
+
`<BreadcrumbLink as={Link} href="/docs">`). Handlers compose (yours runs first and can `preventDefault()`).
|
|
413
|
+
- **`data-slot`** on every element, for styling hooks (`[data-slot=card-title]`).
|
|
414
|
+
- **Controlled and uncontrolled**: `open`/`defaultOpen`/`onOpenChange`, `value`/`defaultValue`/`onValueChange`,
|
|
415
|
+
`checked`/`defaultChecked`/`onCheckedChange`.
|
|
416
|
+
- **Components run once.** Read props where they are used (`local.x` inside JSX or a closure); do not branch on
|
|
417
|
+
props with an early `return`, use `<Show>` or a conditional inside the JSX.
|
|
418
|
+
- **Icons** are inline SVG from the `icons` item (the Lucide shapes); swap in any icon library by editing the
|
|
419
|
+
imports.
|
|
420
|
+
- **Variants** use `class-variance-authority`: `buttonVariants`, `badgeVariants`, `toggleVariants`,
|
|
421
|
+
`alertVariants` and `sidebarMenuButtonVariants` are exported so links and other elements can look the same.
|
|
422
|
+
|
|
423
|
+
## Differences from shadcn/ui
|
|
424
|
+
|
|
425
|
+
The components read shadcn's token names (`--background`, `--primary`, `--radius`, ...), so a `globals.css` that
|
|
426
|
+
defines the same variables keeps working. In your own code: `className` becomes `class`,
|
|
427
|
+
`asChild` becomes `as`, `forwardRef` is gone (`ref` is a plain prop), `useState` becomes `useStore`, `lucide-react`
|
|
428
|
+
becomes the `icons` item, `next-themes` becomes `ThemeProvider`/`useTheme`, `sonner` becomes the `toast` item, and
|
|
429
|
+
the `--radix-*` CSS variables in custom classes become `--primitives-*` (`transform-origin`, `anchor-width`,
|
|
430
|
+
`available-height`, `accordion-content-height`). Item by item:
|
|
431
|
+
|
|
432
|
+
| Component | Difference from shadcn/ui |
|
|
433
|
+
| --- | --- |
|
|
434
|
+
| `button`, `badge` | `as` replaces `asChild`. `button` sizes: `default`, `xs`, `sm`, `lg`, `icon-xs`, `icon-sm`, `icon`, `icon-lg`. `Button` does not set `type`, so inside a form it submits unless you pass `type="button"`. |
|
|
435
|
+
| `input`, `textarea`, `label` | Plain elements; `Label` wraps the Label primitive (`htmlFor`). `aria-invalid` styles the field. |
|
|
436
|
+
| `checkbox`, `switch`, `radio-group`, `toggle`, `toggle-group`, `slider`, `progress` | State hooks are the primitives' `data-state` values (`checked` or `unchecked` for checkbox, switch and radio, plus `indeterminate` for checkbox; `on` or `off` for toggles; `loading`, `complete` or `indeterminate` for progress), and `forced-colors:` variants keep state visible in high contrast. `Slider` takes `value`/`defaultValue` arrays. |
|
|
437
|
+
| `select` | `SelectTrigger` takes `size` (`sm` or `default`); `SelectContent` takes `position`: `"popper"` (the default here; Radix defaults to item-aligned) or `"item-aligned"`, which puts the chosen item over the trigger and costs nothing when unused. Long lists scroll with scroll buttons that a finger stops when it lifts. With a `name` it takes part in native forms. |
|
|
438
|
+
| `combobox` | An item of its own on the Combobox primitive (`ComboboxInput`, `ComboboxContent`, `ComboboxItem`, chips for `multiple`). A `defaultValue` shows its label before the list was ever opened (the closed list's items report their text); `itemToString` is for server rendering and late items. Home and End jump to the first and last option once one is highlighted. |
|
|
439
|
+
| `command` | A command palette on the Combobox primitive in its `inline` mode (the list is plain markup right below the field, never a popup). `CommandItem` takes `onSelect`, `value`, `keywords`; `Command` takes `shouldFilter`, `filter` (a `CommandFilter` score function) and `loop`. Home and End jump to the first and last item. The highlighted item is not exposed as `value`/`onValueChange`; `CommandInput` `value` is the search text. |
|
|
440
|
+
| `form` | Not react-hook-form. `Form` is the provider counterpart: a `form` wired to the object `useForm` from `@shadow-js/forms` returns (any object with `fieldAt` and `handleSubmit` fits), so a `FormField` takes its field by `name`. Without it `FormField` takes any `{ value, error, touched, onInput, onBlur }` object (each member a value or an accessor). `FormControl` merges `id`, `aria-describedby`, `aria-invalid`, `disabled`, `ref` and the field's handlers into the element you pass with `as`, and `render` also gets `setValue` and `field`. `FormErrorSummary` lists the problems of a failed submit with links to the controls and takes focus (shadcn has none). A submit marks every field touched, and the kit shows an error once the field is touched or the form was submitted. |
|
|
441
|
+
| `dialog`, `alert-dialog`, `sheet` | `showCloseButton` on `DialogContent`/`SheetContent`. `DialogTrigger as={Button}` replaces `asChild`. `Sheet` sides: `top`, `bottom`, `left`, `right` (the physical edges) and `start`, `end` (flip in RTL). `alert-dialog` focuses `Cancel` first and ignores outside presses. |
|
|
442
|
+
| `popover`, `hover-card`, `tooltip` | `PopoverClose`, `PopoverHeader/Title/Description` added (a popover needs an accessible name). `Tooltip` wraps itself in a zero-delay provider only when none is above it. `HoverCard` takes `delay` (shadcn's name for `openDelay`; it is a root prop, the delays live on the root), and `HoverCardContent` takes `side` (`bottom` by default). |
|
|
443
|
+
| `field` | `FieldError` takes `errors` (a validator's `{ message }` entries): one message is shown as text, several as a list, a repeated message once. An empty list hides the error. |
|
|
444
|
+
| `input-group` | `InputGroupButton` takes `size` (`xs` by default, `sm`, `icon-xs`, `icon-sm`) and `variant` (`ghost` by default, then the Button item's variants), as shadcn does. Its classes come from this item, not from `button`. |
|
|
445
|
+
| `dropdown-menu`, `context-menu`, `menubar` | Items take `as`, `inset` and `variant="destructive"`. An uncontrolled checkbox or radio item resets when the menu closes (the items unmount): use `checked` / `value` for state you keep. |
|
|
446
|
+
| `tabs`, `accordion`, `collapsible` | `accordion` and `collapsible` heights animate with `animate-accordion-down/up` and `animate-collapsible-down/up` from `animations.css`. `TabsList` takes `variant` (`default`, or `line` for a transparent list with an underline under the selected tab). |
|
|
447
|
+
| `resizable` | `ResizablePanelGroup` takes `direction` or shadcn's `orientation` (`direction` wins when both are given). |
|
|
448
|
+
| `toggle-group` | `ToggleGroup` takes `spacing` (default `0`: the items are connected; any other value puts a gap between them, in spacing units). In a vertical connected group the ends round their top and bottom corners. |
|
|
449
|
+
| `pagination` | Built on the Pagination primitive: `PaginationItems` renders the page list from `pageCount`/`page` (siblings, ellipsis); `PaginationItem` renders the `li` and its link together. |
|
|
450
|
+
| `breadcrumb`, `card`, `alert`, `table`, `separator`, `skeleton`, `aspect-ratio`, `kbd`, `spinner` | `class` replaces `className`. `BreadcrumbLink` takes `as` (use your router's link). `Separator` is decorative by default. |
|
|
451
|
+
| `avatar` | There is no avatar primitive: the item tracks the image's load state itself (fallback on the server, until load, and on error; optional `delayMs`). |
|
|
452
|
+
| `toast` | Sonner-like calls (`toast.success/error/warning/info/loading/promise/dismiss`) from `useToast()` in components under a `<Toaster />`, or from `createToaster()` in module code. Each toaster has its own store unless it is given one. On the Toast primitive. Not sonner: no stacked-card animation. |
|
|
453
|
+
| `sidebar` | `side="start" \| "end"` or shadcn's `"left" \| "right"`. State is remembered only when asked: `cookieName="sidebar_state"` writes shadcn's cookie (`sidebarCookie` and `readSidebarCookie` serve it from a server), `storageKey` uses `localStorage`. `SidebarProvider isMobile` overrides detection (`useIsMobile`, the `use-mobile` item). Mobile shows the sidebar in a `sheet`. The trigger has `aria-expanded`, and an off-canvas sidebar that is collapsed makes its header, content and footer `inert`. |
|
|
454
|
+
| `chart` | No Recharts: SVG line, area, bar, pie, donut and radial charts (`ChartLine`, `ChartArea`, `ChartBar`, `ChartPie`, `ChartRadial`) inside a `ChartContainer`. `ChartTooltip`, `ChartTooltipContent`, `ChartLegend` and `ChartLegendContent` are children of a chart, with `children` instead of a `content` prop. Colors are one CSS color per series (`--chart-1` to `--chart-5` by default), without a `theme` pair. The scales and paths are the `chart-scale` item. |
|
|
455
|
+
| `password-input` | Not in shadcn. A show/hide toggle, a strength meter slot (`PasswordStrength`, `scorePassword`, `usePasswordInput`), a Caps Lock hint and `autocapitalize`, `spellcheck` and `autocorrect` turned off. |
|
|
456
|
+
| `icons`, `utils` | Inline SVG replaces `lucide-react`; `cn()` is shadcn's `lib/utils`. |
|
|
457
|
+
| `ui-strings` | Not in shadcn. `UiStringsProvider` and `useUiStrings` give the built-in words of the components (Close, Next, Page n of m) to a language pack; see "Translating the kit". |
|
|
458
|
+
|
|
459
|
+
## Translating the kit
|
|
460
|
+
|
|
461
|
+
The components carry a few words of their own ("Close", "Next", "Page 2 of 9", "Toggle Sidebar", the names of the
|
|
462
|
+
calendar buttons). Each has an English default in its source. To change the language of the whole app, wrap it in
|
|
463
|
+
`UiStringsProvider` (the `ui-strings` item) with a pack; a prop such as `labels` or `toggleLabel` on a component still
|
|
464
|
+
wins over the provider.
|
|
465
|
+
|
|
466
|
+
| Pack | Import | What it is |
|
|
467
|
+
| --- | --- | --- |
|
|
468
|
+
| English | `@shadow-js/ui/locales/en` | The defaults, as a catalog of messages. |
|
|
469
|
+
| Arabic | `@shadow-js/ui/locales/ar` | The same keys in Arabic, with the six Arabic plural cases. |
|
|
470
|
+
|
|
471
|
+
A pack is plain data (a nested object of strings in the ICU MessageFormat of `@shadow-js/i18n`: `{name}`, `{n, number}`,
|
|
472
|
+
`{n, plural, ...}`), so it also drops into a `createI18n` catalog. A test checks that every key of the English pack exists
|
|
473
|
+
in the Arabic pack with the same arguments and that every plural has the cases of its language.
|
|
474
|
+
|
|
475
|
+
```tsx title="src/arabic-app.tsx" typecheck
|
|
476
|
+
import ar from "@shadow-js/ui/locales/ar";
|
|
477
|
+
import type { JSX } from "@shadow-js/web";
|
|
478
|
+
import { UiStringsProvider } from "@/lib/ui-strings";
|
|
479
|
+
|
|
480
|
+
export function ArabicApp(props: { children: JSX.Element }): JSX.Element {
|
|
481
|
+
return (
|
|
482
|
+
<UiStringsProvider strings={ar} locale="ar">
|
|
483
|
+
<div dir="rtl" lang="ar">
|
|
484
|
+
{props.children}
|
|
485
|
+
</div>
|
|
486
|
+
</UiStringsProvider>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
The Arabic pack was reviewed by a native Arabic speaker. Words differ between regions, so have your own audience check the wording that matters to your product.
|
|
492
|
+
|
|
493
|
+
## Registry format
|
|
494
|
+
|
|
495
|
+
`registry.json` (also importable as `@shadow-js/ui/registry.json`) is the machine-readable index the CLI and the
|
|
496
|
+
documentation read. One entry per item:
|
|
497
|
+
|
|
498
|
+
```jsonc
|
|
499
|
+
{
|
|
500
|
+
"name": "dialog", "type": "ui", "title": "Dialog", "description": "...",
|
|
501
|
+
"files": [{ "path": "registry/dialog/dialog.tsx", "type": "ui" }],
|
|
502
|
+
"registryDependencies": ["icons", "utils"], // other items it imports
|
|
503
|
+
"dependencies": { }, // npm packages (exact versions); button has class-variance-authority
|
|
504
|
+
"exports": ["Dialog", "DialogContent", "..."], // the API, for documentation
|
|
505
|
+
"primitives": ["Dialog", "..."], // what it imports from @shadow-js/primitives
|
|
506
|
+
"tailwind": { "tokens": ["background", "accent", "..."], "animations": true }
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
It is generated from the files (`node scripts/build-registry.mjs`) and a test fails when it is stale. Item types:
|
|
511
|
+
`ui` (goes to `aliases.ui`), `lib` (`aliases.lib`; `utils` goes to `aliases.utils`), `hook` (`aliases.hooks`).
|
|
512
|
+
Sources import each other through the canonical aliases (`@/components/ui/button`, `@/lib/utils`,
|
|
513
|
+
`@/hooks/use-mobile`), which the CLI rewrites to yours.
|
|
514
|
+
|
|
515
|
+
## Accessibility, RTL, forced colors, reduced motion
|
|
516
|
+
|
|
517
|
+
- Roles, names, `aria-*` relationships, focus management, keyboard maps and typeahead come from the primitives
|
|
518
|
+
and follow the WAI-ARIA Authoring Practices; the components add looks and never change the semantics. You provide
|
|
519
|
+
the names: a `Label` or `aria-label` for every control, a title in every dialog.
|
|
520
|
+
- **RTL**: classes are logical (`ms-*`, `pe-*`, `start-*`, `text-start`, `border-s`), directional icons flip with
|
|
521
|
+
`rtl:rotate-180`, and the primitives flip arrow keys and placement with `dir="rtl"`. A test fails on physical
|
|
522
|
+
`left/right` utilities, except for `Sheet` with `side="left"` or `"right"`.
|
|
523
|
+
- **Forced colors**: custom-drawn state (checkbox, radio, switch, slider, progress, toggle, menu highlight)
|
|
524
|
+
has `forced-colors:` variants so state stays visible in Windows high contrast mode.
|
|
525
|
+
- **Reduced motion**: the animation utilities collapse to ~0ms under `prefers-reduced-motion: reduce`.
|
|
526
|
+
|
|
527
|
+
## API
|
|
528
|
+
|
|
529
|
+
The package exports the helpers the components share and the theme provider. Components are not exported; they are
|
|
530
|
+
copied.
|
|
531
|
+
|
|
532
|
+
```ts title="src/ui-api.ts" typecheck
|
|
533
|
+
import {
|
|
534
|
+
applyTheme,
|
|
535
|
+
type ClassValue,
|
|
536
|
+
cn,
|
|
537
|
+
cva,
|
|
538
|
+
DEFAULT_STORAGE_KEY,
|
|
539
|
+
isTheme,
|
|
540
|
+
readStoredTheme,
|
|
541
|
+
resolveTheme,
|
|
542
|
+
ThemeProvider,
|
|
543
|
+
themeScript,
|
|
544
|
+
themeScriptTag,
|
|
545
|
+
useTheme,
|
|
546
|
+
type VariantProps,
|
|
547
|
+
writeStoredTheme,
|
|
548
|
+
} from "@shadow-js/ui";
|
|
549
|
+
|
|
550
|
+
export type Cx = ClassValue;
|
|
551
|
+
export type Variants = VariantProps<ReturnType<typeof cva>>;
|
|
552
|
+
export const helpers = [cn, cva, applyTheme, isTheme, readStoredTheme, resolveTheme, writeStoredTheme];
|
|
553
|
+
export const theme = [ThemeProvider, useTheme, themeScript, themeScriptTag, DEFAULT_STORAGE_KEY];
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
| Export | Signature |
|
|
557
|
+
| --- | --- |
|
|
558
|
+
| `cn(...inputs)` | `(...ClassValue[]) => string`; clsx + tailwind-merge |
|
|
559
|
+
| `cva`, `VariantProps` | re-exported from `class-variance-authority` |
|
|
560
|
+
| `ThemeProvider` | props `defaultTheme`, `storageKey`, `attribute`, `enableSystem`, `disableTransitionOnChange`, `children` |
|
|
561
|
+
| `useTheme()` | `{ theme(), resolvedTheme(), systemTheme(), setTheme(theme \| (prev) => theme) }` |
|
|
562
|
+
| `themeScript(options)` | the no-flash inline script text |
|
|
563
|
+
| `themeScriptTag({ nonce, ...options })` | the same wrapped in a `<script nonce>` tag |
|
|
564
|
+
| `resolveTheme(theme, systemPrefersDark, enableSystem)` / `applyTheme(root, resolved, attribute)` | the pure parts |
|
|
565
|
+
| `isTheme`, `readStoredTheme`, `writeStoredTheme`, `DEFAULT_STORAGE_KEY` | storage helpers and the default key (`"shadow-ui-theme"`) |
|
|
566
|
+
|
|
567
|
+
CSS exports: `@shadow-js/ui/theme.css`, `@shadow-js/ui/animations.css`, `@shadow-js/ui/themes/<base>.css`.
|
|
568
|
+
Locale packs (data only): `@shadow-js/ui/locales/en`, `@shadow-js/ui/locales/ar`.
|
|
569
|
+
Everything is documented with TSDoc in the sources.
|
|
570
|
+
|
|
571
|
+
## Links
|
|
572
|
+
|
|
573
|
+
- [`@shadow-js/primitives`](https://primitives.shadowjs.net): the headless behavior under every component
|
|
574
|
+
- [`@shadow-js/forms`](https://forms.shadowjs.net): fields for the `form` item
|
|
575
|
+
|
|
576
|
+
## Dependencies
|
|
577
|
+
|
|
578
|
+
- Runtime `@shadow-js/table` `1.0.0`: imported only as types by the data-table item (`registry/data-table/data-table.tsx`).
|
|
579
|
+
- Runtime `class-variance-authority` `0.7.1`: `cva` and `VariantProps` (`src/index.ts`).
|
|
580
|
+
- Runtime `clsx` `2.1.1` and `tailwind-merge` `3.7.0`: class merging behind `cn` (`registry/utils/utils.ts`).
|
|
581
|
+
- Peers: `@shadow-js/core`, `@shadow-js/primitives`, and `@shadow-js/web` `^1.0.0`: the behavior and rendering under each component.
|
|
582
|
+
|
|
583
|
+
## Types
|
|
584
|
+
|
|
585
|
+
- Names from the root: `cn`, `cva`, `ClassValue`, `VariantProps`, `RegistryItem`.
|
|
586
|
+
- Type tests in `test-types/` are compile-time checks; `npm run typecheck:types` runs them.
|
|
587
|
+
|
|
588
|
+
## Support
|
|
589
|
+
|
|
590
|
+
- Node.js `>=22.12.0`. ESM only: use `import`; `require` is not supported.
|
|
591
|
+
- Browsers: current evergreen releases.
|
|
592
|
+
- Versioning: every `@shadow-js/*` package releases together at one version. Upgrade them as one set.
|
|
593
|
+
- Documentation: [ShadowJS documentation](https://shadowjs.net).
|
|
594
|
+
|
|
595
|
+
## Using, changing and contributing
|
|
596
|
+
|
|
597
|
+
- **Use the package** (a person or a coding agent that builds with it): this README, and [SKILL.md](SKILL.md), the short page an agent reads first.
|
|
598
|
+
- **Change the package** (fix a bug, add a feature): [AGENTS.md](AGENTS.md) says how the repository is laid out, how to implement and test a change, and how a pull request is reviewed. It is written for people and coding agents alike.
|
|
599
|
+
- **Take part**: [CONTRIBUTING.md](CONTRIBUTING.md) is the short guide; [SECURITY.md](SECURITY.md) is for vulnerabilities.
|