@brrritscold/core-react 0.0.0-stage → 1.1.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/LICENSE +21 -0
- package/README.md +318 -2
- package/dist/components/accordion.d.ts +6 -0
- package/dist/components/accordion.js +26 -0
- package/dist/components/alert-dialog.d.ts +18 -0
- package/dist/components/alert-dialog.js +48 -0
- package/dist/components/alert.d.ts +10 -0
- package/dist/components/alert.js +33 -0
- package/dist/components/avatar.d.ts +11 -0
- package/dist/components/avatar.js +25 -0
- package/dist/components/badge.d.ts +7 -0
- package/dist/components/badge.js +36 -0
- package/dist/components/breadcrumb.d.ts +10 -0
- package/dist/components/breadcrumb.js +41 -0
- package/dist/components/button.d.ts +8 -0
- package/dist/components/button.js +38 -0
- package/dist/components/calendar.d.ts +10 -0
- package/dist/components/calendar.js +113 -0
- package/dist/components/card.d.ts +11 -0
- package/dist/components/card.js +36 -0
- package/dist/components/checkbox.d.ts +3 -0
- package/dist/components/checkbox.js +12 -0
- package/dist/components/combobox.d.ts +24 -0
- package/dist/components/combobox.js +68 -0
- package/dist/components/data-table-column-header.d.ts +13 -0
- package/dist/components/data-table-column-header.js +27 -0
- package/dist/components/data-table-pagination.d.ts +15 -0
- package/dist/components/data-table-pagination.js +53 -0
- package/dist/components/data-table-toolbar.d.ts +9 -0
- package/dist/components/data-table-toolbar.js +17 -0
- package/dist/components/data-table-view-options.d.ts +14 -0
- package/dist/components/data-table-view-options.js +16 -0
- package/dist/components/date-picker.d.ts +18 -0
- package/dist/components/date-picker.js +31 -0
- package/dist/components/dialog.d.ts +17 -0
- package/dist/components/dialog.js +43 -0
- package/dist/components/dropdown-menu.d.ts +29 -0
- package/dist/components/dropdown-menu.js +56 -0
- package/dist/components/field.d.ts +24 -0
- package/dist/components/field.js +70 -0
- package/dist/components/input-group.d.ts +18 -0
- package/dist/components/input-group.js +63 -0
- package/dist/components/input.d.ts +3 -0
- package/dist/components/input.js +10 -0
- package/dist/components/label.d.ts +3 -0
- package/dist/components/label.js +8 -0
- package/dist/components/pagination.d.ts +17 -0
- package/dist/components/pagination.js +31 -0
- package/dist/components/popover.d.ts +9 -0
- package/dist/components/popover.js +26 -0
- package/dist/components/radio-group.d.ts +5 -0
- package/dist/components/radio-group.js +15 -0
- package/dist/components/select.d.ts +15 -0
- package/dist/components/select.js +38 -0
- package/dist/components/separator.d.ts +3 -0
- package/dist/components/separator.js +12 -0
- package/dist/components/sheet.d.ts +14 -0
- package/dist/components/sheet.js +43 -0
- package/dist/components/skeleton.d.ts +2 -0
- package/dist/components/skeleton.js +7 -0
- package/dist/components/switch.d.ts +5 -0
- package/dist/components/switch.js +12 -0
- package/dist/components/table.d.ts +10 -0
- package/dist/components/table.js +33 -0
- package/dist/components/tabs.d.ts +10 -0
- package/dist/components/tabs.js +39 -0
- package/dist/components/textarea.d.ts +3 -0
- package/dist/components/textarea.js +9 -0
- package/dist/components/toast.d.ts +16 -0
- package/dist/components/toast.js +96 -0
- package/dist/components/toggle-group.d.ts +16 -0
- package/dist/components/toggle-group.js +32 -0
- package/dist/components/toggle.d.ts +8 -0
- package/dist/components/toggle.js +29 -0
- package/dist/components/tooltip.d.ts +6 -0
- package/dist/components/tooltip.js +23 -0
- package/dist/data-table.d.ts +126 -0
- package/dist/data-table.js +118 -0
- package/dist/icons.d.ts +1 -0
- package/dist/icons.js +2023 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +39 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +6 -0
- package/dist/library.snapshot.json +21410 -0
- package/dist/styles.css +255 -0
- package/dist/testing.d.ts +358 -0
- package/dist/testing.js +664 -0
- package/dist/tokens.json +609 -0
- package/package.json +115 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Blaine Richardson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,319 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @brrritscold/core-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The component library: shadcn (Base UI) React components on Tailwind v4, themed from one design token file, and the code side of every component link to the design system library in Penpot. **Code wins**: the library mirrors this package.
|
|
4
|
+
|
|
5
|
+
**Storybook (the docs): https://brrritscold.github.io/ts-packages/core-react/** · run it locally with `pnpm storybook`. Spec: [`docs/design-system/SPEC.md` § D](https://github.com/bRRRITSCOLD/ts-packages/blob/main/docs/design-system/SPEC.md); terms: [`GLOSSARY.md`](https://github.com/bRRRITSCOLD/ts-packages/blob/main/docs/design-system/GLOSSARY.md).
|
|
6
|
+
|
|
7
|
+
**Every PR touching `src/` (stories aside), `tokens/tokens.json` or the `styles.css` build (`scripts/`) needs a changeset naming this package; CI fails without one.** Bumps: **minor** = new component, icon, variant value, optional prop or semantic token; **major** = renamed or removed prop, variant value, component, icon or token, or a changed default; **patch** = a token _value_ change, or a visual or behaviour fix with the same API. A major changeset has a `### Migration` section with exact before → after pairs and a pointer to the matching token migrations. Library-only changes with no published file (an annotation, a snapshot-only refresh) get none. There is no library version: the snapshot at tag `@brrritscold/core-react@x.y.z`, shipped in that release as `./library.snapshot.json`, is the library state that release matches. A snapshot-only refresh rides the next release.
|
|
8
|
+
|
|
9
|
+
## Using it
|
|
10
|
+
|
|
11
|
+
```css
|
|
12
|
+
/* app/globals.css: Tailwind, the theme and a @source for this package's files, in one import */
|
|
13
|
+
@import "@brrritscold/core-react/styles.css";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { Button } from '@brrritscold/core-react'
|
|
18
|
+
import { PlusIcon } from '@brrritscold/core-react/icons'
|
|
19
|
+
|
|
20
|
+
;<Button variant="outline" size="sm">
|
|
21
|
+
<PlusIcon /> Add
|
|
22
|
+
</Button>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Dark mode is the `.dark` class on an ancestor; there is no theme provider.
|
|
26
|
+
|
|
27
|
+
| Subpath | Holds |
|
|
28
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `.` | named component and part exports plus `cn`, from one pure barrel (`src/index.ts`) |
|
|
30
|
+
| `./icons` | every `lucide-react` export, one re-export per icon module; use the `<Name>Icon` aliases |
|
|
31
|
+
| `./data-table` | `useDataTable` (and its types): the one importer of `@tanstack/react-table`; see [Data table](#data-table) |
|
|
32
|
+
| `./styles.css` | the built theme: `@import "tailwindcss"`, its own `@source "./**/*.js"`, tokens, `@theme inline` |
|
|
33
|
+
| `./tokens.json` | the raw token file |
|
|
34
|
+
| `./library.snapshot.json` | the design system library snapshot this release matches (`design/library.snapshot.json`), for other renderers' drift checks |
|
|
35
|
+
| `./testing` | component objects for Playwright specs (`dialog(root)`, `toast(page)`, `toggleGroup(root)`, `accordion(root)`, `tabs(root)`, `dataTable(root)`, `calendar(root)`, `datePicker(trigger)`, …); `@playwright/test` is an optional peer |
|
|
36
|
+
|
|
37
|
+
## Where this package departs from the repo's package layout
|
|
38
|
+
|
|
39
|
+
Everything not listed here follows the shared layout (script names, strict pnpm dependencies, `.js` on relative imports).
|
|
40
|
+
|
|
41
|
+
| The other packages | core-react | Why |
|
|
42
|
+
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| tsup, dual ESM/CJS | **ESM only, unbundled**: `tsc -p tsconfig.build.json` emits one `.js` + `.d.ts` per source file; no `require` condition | each file keeps its own `"use client"` |
|
|
44
|
+
| `build` is `tsup` | `build` is Style Dictionary (`tokens/tokens.json` → `dist/styles.css`, `scripts/build-styles.ts`) → copy `tokens.json` and the library snapshot to `dist/` → `tsc` | the CSS and JSON exports are build output too |
|
|
45
|
+
| no `.` entry, no `index.ts` | `src/index.ts` is the `.` entry: named re-exports only, no logic, no `export *`. `scripts/check-entries.mjs` allowlists it and checks that | ESM and unbundled, so bundlers tree-shake it per component and Next's `optimizePackageImports` rewrites named imports from it |
|
|
46
|
+
| every export is JavaScript | `./styles.css`, `./tokens.json` and `./library.snapshot.json` are plain files; `"sideEffects": ["*.css"]` | apps import the theme once and read raw tokens |
|
|
47
|
+
| typedoc site and generated README `## API` | **Storybook is the docs**; the root `typedoc.json` excludes this package and this README has no API section | a component's docs are its rendered variants |
|
|
48
|
+
| tiers `unit`, `integration`, `e2e` on `vitest.shared.ts` | own `vitest.config.ts`: `stories` (beside components in `src/`, excluded from the build) and `unit` on jsdom taking `.tsx`; `integration` is Playwright against a static Storybook build; no `e2e` | the story is the test; browser flows need real Playwright `Locator`s |
|
|
49
|
+
| siblings as `workspace:^` | devDeps `@brrritscold/design-core-js` and `design-core-node` as `workspace:*` | they are private, for the drift check, never published |
|
|
50
|
+
| starts at `0.1.0`, releases with its first changeset | `"private": true`, version `0.0.0` until the day-one set is done; first publish `1.0.0` | pre-release churn never reaches npm |
|
|
51
|
+
| `tsconfig.json` with `lib: ["esnext"]` | adds `dom` and `jsx: "react-jsx"`; typechecks `src`, `tests`, `scripts`, `.storybook` | React components and their stories |
|
|
52
|
+
| one `turbo.json` at the root | `turbo.json` here extends it so `build` also hashes `tokens/` and `scripts/` | the token build is part of `build` |
|
|
53
|
+
|
|
54
|
+
Scripts beyond the contract: `build:styles` (the token half of `build`), `drift:manifest` / `drift:check` (see [Drift check](#drift-check)), `test:stories` / `test:stories:watch`, `storybook`, `storybook:build`. Each Storybook and stories script runs `build:styles` first, since the preview imports `dist/styles.css`. `test:integration` builds Storybook itself and has no containers, so `test:integration:setup` / `:teardown` run empty scripts; they are kept so `test:integration:ci` has the same setup → run → teardown shape as every other package.
|
|
55
|
+
|
|
56
|
+
## Tokens
|
|
57
|
+
|
|
58
|
+
`tokens/tokens.json` is the **token file**, one W3C DTCG tree with the top-level sets `primitive`, `light` and `dark` (no `$themes` or `$metadata`).
|
|
59
|
+
|
|
60
|
+
- **Primitives** hold raw values: `color.neutral.500`, `space.4`, `size.md`, `font-size.sm`, … A half step is written with a dash, since a token name can't hold a dot: `px-2.5` is `space.2-5`. Each class has one token: `h-9` and `size-9` read `space.9`; `size.*` holds only the container widths `max-w-*` reads.
|
|
61
|
+
- **Semantics** are named exactly as shadcn's CSS variables (`background`, `primary-foreground`, `border`, `ring`, `radius`, …), point at primitives, and are defined in both `light` and `dark`. Colours are only ever semantic in components.
|
|
62
|
+
- **`$type`s** follow Penpot's token types, so token sync writes them as they are: `color`, `spacing`, `sizing`, `borderRadius`, `fontFamilies`, `fontSizes`, `fontWeights`, `number` (line height), `letterSpacing`, `shadow`, `opacity`.
|
|
63
|
+
- **`styles.css`**: primitives and light semantics on `:root`, dark semantics on `.dark`, then the `@theme inline` block. It clears Tailwind's default scale for every token type and maps the tokens in their place, so a utility reads its token's variable (`p-4` → `var(--space-4)`) and changing a token changes every class that uses it:
|
|
64
|
+
|
|
65
|
+
| Token | Utilities |
|
|
66
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| colour semantics | `bg-primary`, `text-muted-foreground`, `border-input`, … |
|
|
68
|
+
| `radius-*` | `rounded-xs` … `rounded-xl`, one semantic per step aliasing its `border-radius.*` primitive; `rounded-full` reads `border-radius.full`. `radius` itself stays for shadcn's `--radius` |
|
|
69
|
+
| `space.*` | spacing and sizing: `p-4`, `gap-1.5`, `h-9`, `size-4`, `inset-x-4` |
|
|
70
|
+
| `size.*` | `max-w-xs` … `max-w-2xl` |
|
|
71
|
+
| `font-family.*` | `font-sans`, `font-mono`. `sans` is Inter first: `styles.css` imports `@fontsource/inter` (pinned exactly) at 400, 500, 600 and 700, so apps get the font with no setup. An Inter bump is deliberate and matched by the other renderers |
|
|
72
|
+
| `font-size.*` | `text-xs` … `text-2xl` (font size only; line height comes from `leading-*`, inherited from `body`'s `leading-normal`) |
|
|
73
|
+
| `font-weight.*` | `font-normal` … `font-bold` |
|
|
74
|
+
| `line-height.*` | `leading-tight`, `leading-normal`, `leading-relaxed` |
|
|
75
|
+
| `letter-spacing.*` | `tracking-tight`, `tracking-normal`, `tracking-wide` |
|
|
76
|
+
| `shadow.*` | `shadow-xs`, `shadow-sm` |
|
|
77
|
+
| `opacity.*` | `opacity-0`, `opacity-10`, `opacity-50`, `opacity-100`, and a colour's `/10` or `/50` modifier |
|
|
78
|
+
|
|
79
|
+
A token already named as its theme variable (`--font-weight-medium`, `--shadow-xs`, `--opacity-50`) sits in a second `@theme inline reference` block, so Tailwind doesn't emit it again as a variable pointing at itself. Breakpoints, motion and z-index keep Tailwind's defaults. Font sizes are stored in px and emitted as rem; opacities are stored as fractions and emitted as percentages, the form a colour modifier's `color-mix()` takes. Style Dictionary fails the build on a broken reference.
|
|
80
|
+
|
|
81
|
+
- Components use tokens only through those utilities. **Arbitrary values (`bg-[#…]`, `p-[13px]`) and default-palette utilities (`bg-slate-500`, `text-white`) are rejected** by the unit tier's source scan (every file in `src/` but `testing.ts`, whose strings are Playwright selectors such as `td[data-day="…"]`), and the token checks fail a component class that sets a spacing, sizing, typography, shadow, opacity, radius or colour property without reading a token.
|
|
82
|
+
|
|
83
|
+
## Drift check
|
|
84
|
+
|
|
85
|
+
The CI job `drift` keeps this package and the design system library in Penpot honest, offline and without Penpot credentials:
|
|
86
|
+
|
|
87
|
+
1. `pnpm drift:manifest` writes `code-manifest.json` (gitignored; CI uploads it as the `code-manifest` artifact) from the built `.d.ts`: every export, each prop's literal union, the icon names. Run `build` first.
|
|
88
|
+
2. `pnpm drift:check` checks `design/library.snapshot.json` against the manifest and `tokens/tokens.json` (drift rules 1–9 in `@brrritscold/design-core-js`). Library → code mismatches fail; code → library gaps are a warning, except the exports on the allowlist.
|
|
89
|
+
3. Every token the PR's base `tokens.json` has and this one doesn't needs a `design/token-migrations.json` entry, or the job fails (`design-drift migrations`).
|
|
90
|
+
|
|
91
|
+
| File | Holds |
|
|
92
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
93
|
+
| `design/library.snapshot.json` | the normalized library file; written only by the snapshot refresh, never by hand |
|
|
94
|
+
| `design/drift.config.json` | `allowlist`: exports that never get a library twin (`cn`, cva configs like `buttonVariants`); `valueAllowlist`: prop values no variant shows (`aria-invalid` `grammar` / `spelling` render like `false`, since `aria-invalid:` matches only `"true"`; `TooltipContent` `side` `inline-start` / `inline-end` match `left` / `right` in LTR) |
|
|
95
|
+
| `design/token-migrations.json` | append-only `[{ "version", "from", "to" }]`; `to: null` when nothing replaces the token. Token sync reads it |
|
|
96
|
+
|
|
97
|
+
Fixing a red check, lockstep PRs and promotion: the personal-ai `design-system` skill.
|
|
98
|
+
|
|
99
|
+
### Where a library twin can't match the code
|
|
100
|
+
|
|
101
|
+
Penpot can't say everything a class list says. The library twin keeps every checked attribute on a token and accepts these known differences:
|
|
102
|
+
|
|
103
|
+
- **Some corners still carry `radius`, not their step.** The `rounded-*` scale is the `radius-*` semantics in `tokens.json`, but library components built before those existed carry `radius` (10px) where the code uses `rounded-md` (8px) or another step. They move to the matching `radius-<step>` in [#116](https://github.com/bRRRITSCOLD/ts-packages/issues/116).
|
|
104
|
+
- **A colour at an opacity is a tint layer.** Applying a colour token resets the fill's opacity to 1, so Button `destructive`'s `bg-destructive/10` is an absolute `Tint` rectangle behind the label: fill `destructive`, layer opacity `opacity.10`. Penpot can't change an opacity by theme, so dark's `/20` stays at 10%.
|
|
105
|
+
- **`dark:` overrides that pick another semantic are dropped.** Button `outline`'s `dark:border-input dark:bg-input/30` stay `border` and `background`.
|
|
106
|
+
- **Hover and focus states have no variants**, and neither do the focus and `aria-invalid` rings (`ring-3 ring-*/20`). A state backed by a prop is a variant property named after it, with values `true` / `false`: Checkbox `checked` / `disabled` / `aria-invalid`, Switch `checked` / `disabled`, Input and Textarea `disabled` / `aria-invalid`. Button has none: it was built before the form controls.
|
|
107
|
+
- **A disabled control is the whole twin at `opacity.50`**, as `data-disabled:opacity-50` fades the whole element.
|
|
108
|
+
- **RadioGroup's item has no `checked` variant**, because `RadioGroupItem` has no `checked` prop (the group's `value` picks it). `Components/RadioGroup/Item` holds a hidden `Checked` layer, which a copy shows to draw the selected item.
|
|
109
|
+
- **Input and Textarea fill with `background`** where code is `bg-transparent`. A Penpot board with no fill casts its shadow from its children, so `shadow.xs` smeared the placeholder text; on a page the fill looks the same.
|
|
110
|
+
- **Checkbox's corners are `border-radius.sm`** (6px, the `rounded-sm` it is in code), not `radius`, which would make a 16px box nearly round.
|
|
111
|
+
- **Field `responsive` looks like `vertical`.** It goes horizontal in a field group 28rem or wider, a container query Penpot can't express.
|
|
112
|
+
- **Toast is the resting toast only.** Stacking, expand-on-hover and swipe have no twin; `Toast` is one toast at the viewport's `max-w-sm` (384px). Its `type` icon is a hidden `Icon` layer (`CircleCheck`) a copy shows and swaps, since `type` is toast data, not a prop. Its corners are `border-radius.xl` (14px, the `rounded-xl` it is in code), and its text lines are 21px tall against code's 20px, so it stands 2px taller.
|
|
113
|
+
- **Badge is 2px narrower.** Its `border border-transparent` adds 1px a side in code; the twin draws no transparent stroke. `destructive` is a `Background` tint (`destructive` at `opacity.10`), dark's `/20` included.
|
|
114
|
+
- **Toggle and ToggleGroup use exact radius tokens and text labels.** `rounded-md` is `border-radius.md` (8px), as Checkbox does; the items show a `Label` text where stories show an icon. `outline` fills with `background`, as Input does. A joined (`spacing` 0) outline group keeps each item's full border, so its dividers are 2px where code's `border-l-0` makes them 1px.
|
|
115
|
+
- **Alert's action is a hidden `Action` layer** at `top-2.5 right-3`. A copy that shows it applies `space.18` to the right padding, as `has-data-[slot=alert-action]:pr-18` does. `destructive`'s description is `destructive` at full strength, not `/90`: no `opacity.90` token.
|
|
116
|
+
- **Avatar's border ring is a `Ring` ellipse** on top (`after:` overlay, `mix-blend-darken` dropped). `Image` and `Badge` are hidden layers a copy shows; `AvatarGroup` is layout-only (`-space-x-2`) and on the drift allowlist.
|
|
117
|
+
- **Open/active states are hidden layers.** Base UI holds them, not a prop. `Accordion/Item` has a hidden `Content` and its Trigger a hidden `Open` chevron (`ChevronUp`) beside `Closed` (`ChevronDown`). `Tabs/Trigger` has a hidden `Active` pill (`background`, `shadow.sm`) and a hidden `Line` bar for the `line` list. A copy shows them. The bottom border of an accordion item is a 1px `Border` layer, hidden on the last item.
|
|
118
|
+
- **An inactive tab's text is `muted-foreground`,** not `text-foreground/60` (there is no `opacity.60` token). Dark's active `border-input bg-input/30` stays `background`. Vertical `Tabs` turns its `List` copy into a column.
|
|
119
|
+
- **Card's `ring-1 ring-foreground/10` is a `Ring` layer** on top (`foreground` stroke at `opacity.10`). The `sm` variant overrides its parts' padding to `space.4` and the title to `font-size.sm`. `Action` is hidden in `Header`; `Action`, `Content`, `Footer` and `Tabs/Content`, `Accordion/Content` hold `_internal/Slot` placeholders. `BreadcrumbItem` is layout-only and on the drift allowlist. `BreadcrumbList`'s gap is `space.2-5` (`sm:gap-2.5`, the desktop width).
|
|
120
|
+
- **Table's columns are fixed-width cells.** `Table/Head` and `Table/Cell` are 120px boards (a table lays out its columns from content; Penpot can't), so the twin's columns don't stretch to the container's `w-full`. `TableHeader` and `TableBody` are layout-only (`Columns` and `Rows` wrappers) and on the drift allowlist. A row's `border-b` is a 1px `Border` layer, hidden on the last body row and on the footer's row; `data-[state=selected]:bg-muted` is a hidden `Selected` layer a copy shows (no prop). `Table/Footer`'s `bg-muted/50` is a `Tint` layer (`muted` at `opacity.50`); its text is `font-weight.medium` on each cell's copy. Hover (`hover:bg-muted/50`) has no twin.
|
|
121
|
+
- **Pagination's buttons are their own boards, not Button copies.** `Button`'s main holds a `Label` only, and a copy can't add an icon, so `Pagination/Link` (`isActive` `true` → outline, `false` → ghost, `icon` size), `Previous`, `Next`, `Ellipsis`, `DataTableColumnHeader`'s trigger, `DataTableViewOptions` and the toolbar's `Reset` are boards carrying Button's tokens for that variant and size. `PaginationItem` is a bare `<li>`, on the drift allowlist. `Pagination` hugs its content where code is `w-full justify-center`; `DataTablePagination` hugs too, so its pagination sits next to the rows-per-page select instead of pushed right, and Previous / Next show no `aria-disabled` fade (no prop to vary).
|
|
122
|
+
- **The data-table primitives twin their resting look.** `DataTableColumnHeader` has a `sort` variant (`none` / `asc` / `desc`) and draws the sortable trigger; the plain-text header (no `onSortChange`) and the trigger's `-ml-3` have no twin, nor does its menu. `DataTableViewOptions` is the trigger only; its menu is a `DropdownMenu` of checkbox items. `DataTableToolbar`'s `Reset` and `DataTablePagination`'s `Rows per page` group are hidden layers a copy shows; the toolbar's `Actions` is an `_internal/Slot`. The filter input is an `Input` copy at 32px tall with `size.xs` on its width.
|
|
123
|
+
- **Calendar is one month, November 2026, and its day states are copy overrides.** react-day-picker holds today, selected, outside and disabled as modifiers, not props, so `CalendarDayButton` is the resting 32×32 ghost day (`radius`, `font-size.sm`) and `Calendar`'s main draws today (9, `muted` fill), the selected day (12, `primary` fill, `primary-foreground` text) and the outside days (December, `muted-foreground` text) as overridden copies. Range, dropdown captions, week numbers and multiple months have no twin. The nav buttons are boards with Button `ghost`'s tokens at `icon` size, as Pagination's are; the absolute nav over a centred caption is a `space-between` header row. Text lines are 18px against code's 16px for `text-xs` weekdays.
|
|
124
|
+
- **DatePicker is the trigger only,** empty (`Pick a date`, icon and label `muted-foreground`), with a `disabled` variant at `opacity.50`. Its `w-72` width stays raw (no sizing token binds it). The popup is a `Popover/Content` holding a `Calendar` copy on a page design.
|
|
125
|
+
- **The dark-mode switch thumb is wrong.** Code's thumb is `dark:bg-foreground` unchecked and `dark:bg-primary-foreground` checked. The twin keeps `background`, so in Dark an unchecked thumb is near-black on the track.
|
|
126
|
+
- **An icon's stroke stays 2px when resized.** lucide scales the stroke with the icon's size; Penpot scales the paths (constraints `scale`) but not the stroke width. A twin that resizes an icon copy sets the copy's stroke width by hand (Checkbox's 14px check: 2 × 14 / 24 ≈ 1.167).
|
|
127
|
+
- **An icon's dot is a filled circle.** lucide draws a dot (`Info`, `CircleExclamationPoint`) as a 0.01px path with round caps, which Penpot doesn't render; the twin uses a 2×2 ellipse with fill `foreground`, the same shape.
|
|
128
|
+
- **Library icons use lucide's canonical names, never aliases** (`CircleExclamationPoint`, not `CircleAlert`), so a lucide major that drops an alias doesn't break drift rule 4. Icon twins are built from `lucide-react`'s own icon data over MCP, so their geometry matches the installed version.
|
|
129
|
+
- **`lucide-react` is pinned exactly, and every Lucide bump is its own core-react release.** A Lucide release that only adds icons is a core-react minor; one that renames or removes an icon is a major. Other renderers (the GPUI library) vendor Lucide at the version this `package.json` names, so a range would let apps resolve a different icon set than the one released and checked against drift.
|
|
130
|
+
|
|
131
|
+
## Data table
|
|
132
|
+
|
|
133
|
+
The data table is two halves that meet only through plain props:
|
|
134
|
+
|
|
135
|
+
- **The primitives, in `.`**: `DataTableColumnHeader` (`title`, `sort: 'none' | 'asc' | 'desc'`, `onSortChange`, `onHide`), `DataTableToolbar` (`filter`, `onFilterChange`, `filterLabel`, children at the end), `DataTableViewOptions` (`columns: { id, label, visible }[]`, `onVisibilityChange`) and `DataTablePagination` (`pageIndex`, `pageCount`, `onPageChange`, and a rows-per-page select with `pageSize` / `pageSizeOptions` / `onPageSizeChange`), built on `Table`, `Pagination`, `DropdownMenu`, `Input`, `Select` and `Button`. They **never import TanStack**: a hand-rolled or server-driven table feeds them its own state. Each has a library twin.
|
|
136
|
+
- **`useDataTable`, in `./data-table`**: wraps TanStack Table v9's `useTable` with sorting, a global filter, pagination, row selection and column visibility, client-side, and returns the `table` plus a prop getter per primitive. It is the only file that imports `@tanstack/react-table`; the unit tier's TanStack boundary check walks the `.` entry's import graph and fails if it ever reaches TanStack. Code only: no library twin, on the drift allowlist.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import {
|
|
140
|
+
Checkbox,
|
|
141
|
+
DataTableColumnHeader,
|
|
142
|
+
DataTablePagination,
|
|
143
|
+
DataTableToolbar,
|
|
144
|
+
DataTableViewOptions,
|
|
145
|
+
Table,
|
|
146
|
+
TableBody,
|
|
147
|
+
TableCell,
|
|
148
|
+
TableHead,
|
|
149
|
+
TableHeader,
|
|
150
|
+
TableRow,
|
|
151
|
+
} from '@brrritscold/core-react'
|
|
152
|
+
import { type DataTableColumnDef, useDataTable } from '@brrritscold/core-react/data-table'
|
|
153
|
+
|
|
154
|
+
const columns: DataTableColumnDef<Payment>[] = [
|
|
155
|
+
{ id: 'select', enableSorting: false, enableHiding: false },
|
|
156
|
+
{ accessorKey: 'email', header: 'Email' },
|
|
157
|
+
{ accessorKey: 'status', header: 'Status', enableSorting: false },
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
function Payments({ data }: { data: Payment[] }) {
|
|
161
|
+
const dt = useDataTable({ data, columns, getRowId: (row) => row.id })
|
|
162
|
+
return (
|
|
163
|
+
<div data-testid="payments">
|
|
164
|
+
<DataTableToolbar {...dt.getToolbarProps()}>
|
|
165
|
+
<DataTableViewOptions {...dt.getViewOptionsProps()} />
|
|
166
|
+
</DataTableToolbar>
|
|
167
|
+
<Table>
|
|
168
|
+
<TableHeader>
|
|
169
|
+
{dt.table.getHeaderGroups().map((group) => (
|
|
170
|
+
<TableRow key={group.id}>
|
|
171
|
+
{group.headers.map((header) => (
|
|
172
|
+
<TableHead key={header.id} {...dt.getHeadProps(header)}>
|
|
173
|
+
{header.column.id === 'select' ? (
|
|
174
|
+
<Checkbox {...dt.getSelectAllProps()} />
|
|
175
|
+
) : (
|
|
176
|
+
<DataTableColumnHeader
|
|
177
|
+
{...dt.getColumnHeaderProps(header.column)}
|
|
178
|
+
/>
|
|
179
|
+
)}
|
|
180
|
+
</TableHead>
|
|
181
|
+
))}
|
|
182
|
+
</TableRow>
|
|
183
|
+
))}
|
|
184
|
+
</TableHeader>
|
|
185
|
+
<TableBody>
|
|
186
|
+
{dt.table.getRowModel().rows.map((row) => (
|
|
187
|
+
<TableRow key={row.id} {...dt.getRowProps(row)}>
|
|
188
|
+
{row.getVisibleCells().map((cell) => (
|
|
189
|
+
<TableCell key={cell.id}>
|
|
190
|
+
{cell.column.id === 'select' ? (
|
|
191
|
+
<Checkbox {...dt.getRowSelectProps(row)} />
|
|
192
|
+
) : (
|
|
193
|
+
dt.renderCell(cell)
|
|
194
|
+
)}
|
|
195
|
+
</TableCell>
|
|
196
|
+
))}
|
|
197
|
+
</TableRow>
|
|
198
|
+
))}
|
|
199
|
+
</TableBody>
|
|
200
|
+
</Table>
|
|
201
|
+
<DataTablePagination {...dt.getPaginationProps()} />
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
| Getter | Feeds | Gives |
|
|
208
|
+
| ------------------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
209
|
+
| `getHeadProps(header)` | `TableHead` | `colSpan`, and `aria-sort` (`ascending` / `descending`) on the sorted column's cell only |
|
|
210
|
+
| `getColumnHeaderProps(column)` | `DataTableColumnHeader` | `title` (the column's text `header`, else its id), `sort`, `onSortChange` if it can sort, `onHide` if it can hide |
|
|
211
|
+
| `getRowProps(row)` | `TableRow` | `data-state="selected"` on a selected row |
|
|
212
|
+
| `getSelectAllProps()` / `getRowSelectProps(row)` | `Checkbox` | `checked`, `indeterminate` (select-all), `disabled` (a row), `onCheckedChange`, `aria-label` `Select all` / `Select row` |
|
|
213
|
+
| `getToolbarProps()` | `DataTableToolbar` | `filter`, `onFilterChange` (the global filter; a new filter goes back to the first page) |
|
|
214
|
+
| `getViewOptionsProps()` | `DataTableViewOptions` | `columns` that can hide, by label, and `onVisibilityChange` |
|
|
215
|
+
| `getPaginationProps()` | `DataTablePagination` | `pageIndex`, `pageCount` (at least 1), `onPageChange`, `pageSize`, `onPageSizeChange` |
|
|
216
|
+
| `renderHeader(header)` / `renderCell(cell)` | — | a column's own `header` / `cell`, through TanStack's `flexRender` |
|
|
217
|
+
|
|
218
|
+
Keep `data` and `columns` stable between renders (state, a memo or module scope): TanStack recomputes its row models on a new array. `table` is the TanStack table for anything the getters don't cover. `useDataTable` follows TanStack Table v9 (`useTable` and a feature registry), not v8's `useReactTable`; `DataTableColumnDef<TData>` is a column definition over its features, so an app types its columns without depending on TanStack itself. `dataTable(root)` in `./testing` drives the result.
|
|
219
|
+
|
|
220
|
+
## Vendoring a shadcn component
|
|
221
|
+
|
|
222
|
+
Components come from shadcn's `base-vega` registry (Base UI), one file per component in `src/components/<kebab>.tsx` with its part exports and stories beside it. `components.json` is kept so the shadcn CLI can vendor more. Keep a file close to upstream so a re-pull diffs cleanly: pull, run `pnpm lint` (Biome reformats quotes and indentation), then diff. Upstream `"use client"` stays verbatim.
|
|
223
|
+
|
|
224
|
+
Where upstream steps off the tokens, the file says so in a header comment and fixes it:
|
|
225
|
+
|
|
226
|
+
- `button.tsx`: secondary's `hover:bg-[color-mix(…)]` is `hover:bg-secondary/80`, and the small sizes' `rounded-[min(var(--radius-md),…)]` corners are dropped for the base `rounded-md` (the same 8px at today's `radius`).
|
|
227
|
+
- `dialog.tsx`, `alert-dialog.tsx`: the overlay's `bg-black/10` is `bg-foreground/10 dark:bg-background/10`, near-black at 10% in both modes. Dialog's popup centres with `inset-x-4 mx-auto` instead of `left-1/2 w-full max-w-[calc(100%-2rem)] -translate-x-1/2` (the same 1rem gutter). Alert Dialog's header drops its three `grid-rows-[…]` templates; implicit rows lay out the same.
|
|
228
|
+
- `checkbox.tsx`: the box's `rounded-[4px]` is `rounded-sm` (6px at today's `radius`, 2px rounder).
|
|
229
|
+
- `checkbox.tsx`, `radio-group.tsx`: `disabled:` is `data-disabled:`. The box and the item are `<span>`s Base UI marks `data-disabled`, so `:disabled` never matched and a disabled control didn't fade.
|
|
230
|
+
- `separator.tsx`: `data-horizontal:` / `data-vertical:` are `data-[orientation=horizontal]:` / `data-[orientation=vertical]:`, the attribute Base UI 1.9 sets; upstream's never matched, so the line was 0px tall (and Field's separator a 10px bar).
|
|
231
|
+
- `input.tsx`, `textarea.tsx`: `transition-[color,box-shadow]` is `transition-all`.
|
|
232
|
+
- `switch.tsx`: the track's `h-[18.4px] w-[32px]` and `h-[14px] w-[24px]` are `h-4.5 w-8` and `h-3.5 w-6` (the default track 0.4px shorter; `space.4-5` was added for it), and the thumb's `translate-x-[calc(100%-2px)]` is the step it works out to, `translate-x-3.5` / `translate-x-2.5`.
|
|
233
|
+
- `sheet.tsx`: the overlay's `bg-black/10` is `bg-foreground/10 dark:bg-background/10`, as in Dialog; the slide offsets `translate-x-[2.5rem]` (each side) are `translate-x-10`.
|
|
234
|
+
- `tooltip.tsx`: the arrow's `translate-y-[calc(-50%-2px)]` is `-translate-y-1/2`, its 2px moved into the top/bottom offsets (`top-0.5`, `-bottom-2`); its `rounded-[2px]` is `rounded-xs` (`border-radius.xs`, added for it). The `data-[state=delayed-open]:` animations are dropped: Base UI 1.9 sets no `data-state`.
|
|
235
|
+
- `dropdown-menu.tsx`: the submenu's `min-w-[96px]` is `min-w-24`.
|
|
236
|
+
- Popover, Dropdown Menu, Sheet and Tooltip read primitives added for them: `space.32`, `space.72`, `shadow.md`, `shadow.lg`, `letter-spacing.widest`, `border-radius.xs`. `shadow.md` and `shadow.lg` are Tailwind's with each layer's negative spread set to 0: Penpot rejects a negative spread. Base UI's runtime positioning variables (`origin-(--transform-origin)`, `w-(--anchor-width)`, `max-h-(--available-height)`) are layout, not design, and the source scan allows them.
|
|
237
|
+
- `select.tsx`, `combobox.tsx`, `input-group.tsx`: `transition-[color,box-shadow]` is `transition-all`. Select's `min-w-36` reads `space.36`, added for it.
|
|
238
|
+
- `combobox.tsx`: the popup's `min-w-[calc(var(--anchor-width)+--spacing(7))]` and the list's `max-h-[min(…)]` read the theme variables (`var(--spacing-7)`), since the theme clears `--spacing`; the source scan allows arithmetic on Base UI's runtime variables and spacing variables only. The list drops `no-scrollbar`, a utility upstream defines in its own CSS. The chip's `h-[calc(--spacing(5.5))]` is `h-5.5` (`space.5-5`, added for it). The icon-only trigger, clear and chip-remove buttons are named `Show options`, `Clear` and `Remove` (upstream's fail a11y's button-name rule), and the trigger is out of the tab order: an open popup marks everything but the input `aria-hidden`, and a tabbable button in there fails aria-hidden-focus. The input is the keyboard stop; ArrowDown opens the list. `ComboboxList` has no name of its own: give it an `aria-label`.
|
|
239
|
+
- `input-group.tsx` is vendored as Combobox's input and **not exported**: Input Group isn't in the day-one set, so it has no library twin; exporting it is a promotion. `rounded-[calc(var(--radius)-5px)]` is `rounded-sm` (1px rounder) and the addon's kbd nudges `-0.15rem` are `-0.5`.
|
|
240
|
+
- `toast.tsx` (Base UI Toast, not Sonner): upstream's stacking and swipe geometry is kept, without its element-local variables (`[--gap:0.75rem]`, `[--peek:…]`, `[--offset-y:…]`, `[--scale:…]`, `[--shrink:…]`, `[--height:…]`). Each `transform` reads Base UI's toast variables (`--toast-index`, `--toast-offset-y`, `--toast-swipe-movement-x/y`, `--toast-frontmost-height`) directly, with the 0.75rem gap and peek as `var(--spacing-3)`, written `transform-[…]` instead of `[transform:…]`. It stays `transform` rather than `translate-*` / `scale-*`: Base UI reads the computed `transform` back when a swipe starts and sets an inline one while dragging. The source scan allows such arithmetic on Base UI's variables: spacing variables, unitless numbers and percentages beside them, never a unit. The default and down-swipe exits are one rule, upstream's four `data-expanded:data-ending-style:` copies are dropped (the swipe variants already out-rank `data-expanded:`), and a sideways swipe keeps the toast's own `translateY`. `[transition:transform_500ms_cubic-bezier(…),…,height_150ms]` is `transition-all duration-500 ease-out`, the content's `ease-[cubic-bezier(…)]` is `ease-out`, `rounded-2xl` is `rounded-xl`, `ring-[3px]` is `ring-3`, the hover bridge's `after:h-[calc(var(--gap)+1px)]` is `after:h-3.5`, and `after:content-['']` is dropped (Tailwind v4's `after:` sets it). The viewport is `z-60`, not `z-50`: Base UI leaves it out of a modal dialog's `aria-hidden` and outside-press handling, but at the dialog's own `z-50` the dialog's later portal painted its backdrop over the toast and took every click. Icons are lucide's canonical names (`LoaderCircleIcon`, `TriangleExclamationPointIcon`).
|
|
241
|
+
- `field.tsx`: `responsive`'s `@md/field-group:` is `@min-[28rem]/field-group:`. A container query can't read a token through `var()`, so the width is written out: 28rem is `size.md`'s 448px. Field's `leading-snug` reads `line-height.snug`, added for it.
|
|
242
|
+
- `alert.tsx`: the icon layout's `has-[>svg]:grid-cols-[auto_1fr]` is `has-[>svg]:justify-start`: the columns stay implicit and `auto`, and not stretching them keeps the icon's column at the icon's width and gives the text the rest. The action's room, `pr-18`, reads `space.18` (72px), added for it. `alertVariants` is exported.
|
|
243
|
+
- `badge.tsx`: `rounded-4xl` is `rounded-full` (the same pill on a 20px badge) and `ring-[3px]` is `ring-3`.
|
|
244
|
+
- `toggle.tsx`: `transition-[color,box-shadow]` is `transition-all` and `ring-[3px]` is `ring-3`.
|
|
245
|
+
- `toggle-group.tsx`: `spacing` is the literal union `0 | 1 | 2`, mapped to `gap-0` / `gap-1` / `gap-2`; upstream took any number into `gap-[--spacing(var(--gap))]`, a value no token holds. `orientation` is passed to Base UI as well as to the `data-` attribute, so the arrow keys follow it. `data-horizontal:` / `data-vertical:` are `data-[orientation=…]:`, and the item's `data-[state=on]:bg-muted` is `data-pressed:bg-muted`: Base UI 1.9 sets neither upstream attribute, so a vertical group didn't stack.
|
|
246
|
+
- `avatar.tsx` and `skeleton.tsx` are upstream unchanged. The avatar fallback's `text-muted-foreground` on `bg-muted` (neutral.500 on neutral.100) is 4.36:1, under AA's 4.5:1 for its `text-sm` initials; the pair is the token file's, so the Fallback story turns off only axe's `color-contrast` rule and says so.
|
|
247
|
+
- `accordion.tsx` (Base UI Accordion): the trigger's `aria-disabled:` is `disabled:`. Base UI 1.9 disables the trigger as a native button with the `disabled` attribute, so a disabled item never dimmed. The height animation moves onto the panel: upstream put `h-(--accordion-panel-height)` and `data-starting-style:h-0` / `data-ending-style:h-0` on the inner div, which carries none of Base UI's attributes, and animated the panel with tw-animate-css's `animate-accordion-down/up`, whose keyframes read Radix's `--radix-accordion-content-height`, so the height jumped. The panel transitions its own height (`transition-all duration-200 ease-out`) between 0 and Base UI's `--accordion-panel-height`, which joins the source scan's Base UI variables. Upstream has no `"use client"`, and neither does the file.
|
|
248
|
+
- `breadcrumb.tsx` is upstream on tokens; the ellipsis icon is lucide's canonical `EllipsisIcon`, not the `MoreHorizontalIcon` alias.
|
|
249
|
+
- `card.tsx`: upstream spaced every part with an element-local `--card-spacing` (`[--card-spacing:--spacing(6)]`, `gap-(--card-spacing)`, `px-(--card-spacing)`), and `--spacing(…)` reads Tailwind's `--spacing`, which the theme clears, so the card had no padding or gaps. Each use is the step it means, `6` (24px) and `4` (16px) at `size` sm: `gap-6 py-6` / `data-[size=sm]:gap-4 data-[size=sm]:py-4`, the parts' `px-6` / `group-data-[size=sm]/card:px-4`, and the ruled header's `pb` and footer's `pt` likewise. The header drops `has-data-[slot=card-action]:grid-cols-[1fr_auto]` and `has-data-[slot=card-description]:grid-rows-[auto_auto]`: the action's `col-start-2 row-span-2 justify-self-end` makes the implicit tracks, and its column holds no more than the action, so it still ends at the header's right edge.
|
|
250
|
+
- `table.tsx` is upstream unchanged. A row's `data-[state=selected]:bg-muted` reads an attribute the app sets (`useDataTable`'s `getRowProps`), not one Base UI sets, and the story asserts it.
|
|
251
|
+
- `pagination.tsx` (on Button): the ellipsis icon is lucide's canonical `EllipsisIcon`, not the `MoreHorizontalIcon` alias. A link is Base UI's Button rendering an `<a>` (`nativeButton={false}`), and Base UI gives it `role="button"`, with or without an `href`: find pages as buttons, not links. A link has no `disabled`; `DataTablePagination` marks Previous / Next `aria-disabled` and fades them with `aria-disabled:pointer-events-none aria-disabled:opacity-50`.
|
|
252
|
+
- `data-table-column-header.tsx`, `data-table-toolbar.tsx`, `data-table-view-options.tsx`, `data-table-pagination.tsx` have no registry item: they follow shadcn's data-table guide on our components, with plain props instead of a TanStack `column` / `table`. The column header's sort is a `sort` prop with a `none` value, and its menu is a radio group (Asc, Desc, None) that shows the current order and closes on a pick, where the guide had two plain items; the guide's `data-[state=open]:bg-accent` is dropped (Base UI sets no `data-state`; Button's ghost variant already styles `aria-expanded`) and the trigger's `h-8` is Button size `sm`'s own. The toolbar has one text filter (a `search` input, so a `searchbox`, its browser clear button hidden as Reset clears it) where the guide filtered one column, and the input's `w-[150px] lg:w-[250px]` is `w-full max-w-xs`. View options shows the trigger at every width (the guide's `hidden lg:flex` is an app's call), its menu's `w-[150px]` is `w-36`, items show the column's label rather than its capitalised id, and the label sits in a group, as Base UI's group label needs. Pagination is Pagination's numbered pages (first, last, the current one and its neighbours, an ellipsis per gap) with Previous and Next, not the guide's first/previous/next/last icon buttons and "Page x of y"; the rows-per-page select shows only with `onPageSizeChange`, and the selected-row count is the app's.
|
|
253
|
+
- `calendar.tsx` (react-day-picker 10, with `date-fns`, on Button): upstream's element-local `[--cell-size:--spacing(8)]` and `[--cell-radius:var(--radius-md)]` are the steps they mean, `size-8` / `h-8` / `px-8` / `min-w-8` / `w-8` (32px, `space.8`) and `rounded-md` with its `-l` / `-r` sides; `--spacing(…)` reads Tailwind's `--spacing`, which the theme clears, so upstream's cells had no size. An app that wants other cells passes `classNames`, not a variable. base-vega's `cn-calendar-*` style hooks are inlined as the classes the style gives them (the dropdown root's `border border-input shadow-xs` and focus ring, the dropdown caption's `h-8 pr-1 pl-2`) and `cn-rtl-flip` is dropped (the root's `rtl:` rules turn the chevrons). The weekday and week-number `text-[0.8rem]` is `text-xs` (12px, upstream's 12.8px), `ring-[3px]` is `ring-3`, and a day's sub-label `[&>span]:opacity-70` is `opacity-50` (no `opacity.70` token). Three fixes: the day button takes the `ref` upstream makes and never passes, so its focus effect runs and the arrow keys move focus; the custom parts (root, chevron, week number, day button) are components declared once rather than arrow functions in the `components` prop, which DayPicker renders as given, so upstream remounted the whole calendar on every pick and focus move; and a day button's name is the date in the locale's full style (`Thursday, November 12, 2026`, plus react-day-picker's `Today, ` / `, selected`), since its default `November 12th` fails axe's label-content-name-mismatch (WCAG 2.5.3) on a button showing `12`. A `labels.labelDayButton` from the app or the locale wins. `CalendarDayButton` is exported for a custom `DayButton`, as upstream does. The selection states read react-day-picker's own attributes (`data-selected`, `data-today`, `data-outside` on the cell; `data-selected-single`, `data-range-*` on the button), and the stories assert each.
|
|
254
|
+
- `date-picker.tsx` has no registry item: shadcn's date picker is a composition in its docs (Popover, Button, Calendar), packaged here with plain props so it is one export with one library twin: `value` (`Date | null`, or leave it out for `defaultValue`), `onValueChange`, `placeholder` (`Pick a date`), `disabled`, `disabledDays` (react-day-picker's `Matcher`s) and `formatValue` (date-fns `PPP`, `November 9th, 2026`), with every other prop, `id` and `data-testid` included, on the trigger. The trigger is base-vega's example, an outline Button with a calendar icon before the label and `font-normal`, at `w-72` (`space.72`, the example field's width, where the docs demo had `w-[212px]`), with the demo's `data-[empty=true]:text-muted-foreground` placeholder. The popup is `PopoverContent` at `w-auto p-0`, named by the placeholder; a pick closes it, and focus lands on the picked day (else today) through DayPicker's `autoFocus`, not Base UI's initial focus. Single date only: a range is the app's composition of Popover and `Calendar mode="range"`. Name the trigger with a `FieldLabel` (`htmlFor` its `id`), not `aria-label`: an `aria-label` replaces the date the button shows, and axe's label-content-name-mismatch fails it.
|
|
255
|
+
- `tabs.tsx` (Base UI Tabs): `data-horizontal:` / `data-vertical:` (and the `group-data-*/tabs:` forms) are `data-[orientation=…]:`. Base UI 1.9 sets only `data-orientation`, so the list never sat above the panel and a vertical list never stacked. `orientation` is passed to Base UI as well as to the `data-` attribute (upstream set only the attribute), so the arrow keys follow it. The list's `p-[3px]` is `p-1` and the trigger's `h-[calc(100%-1px)]` is `h-full` (a 28px tab in the 36px list, upstream's was 29px), the line underline's `bottom-[-5px]` is `-bottom-1`, on the list's foot as the vertical bar's `-right-1` already was, and `ring-[3px]` is `ring-3`. `tabsListVariants` is exported.
|
|
256
|
+
|
|
257
|
+
The shadcn CLI resolves `components.json`'s `utils` alias to the npm package `cn` and writes `src/components/…` imports: after a pull, point them back at `../lib/utils.js` and `./<name>.js`, and drop any `cn` dependency it added.
|
|
258
|
+
|
|
259
|
+
Upstream's open and close animations (`animate-in`, `fade-in-0`, `zoom-in-95`, …) come from `tw-animate-css`, which `styles.css` imports after Tailwind.
|
|
260
|
+
|
|
261
|
+
Naming is the component link: export name = library component name, cva variant keys = the Penpot variant properties with literal-union values (`variant`, `size`), and the cva config is exported (`buttonVariants`).
|
|
262
|
+
|
|
263
|
+
## Tests
|
|
264
|
+
|
|
265
|
+
| Tier | Files | Runner |
|
|
266
|
+
| ------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
267
|
+
| `stories` | `src/components/*.stories.tsx` | Vitest browser mode (Chromium) via `@storybook/addon-vitest`; `@storybook/addon-a11y` fails on violations |
|
|
268
|
+
| `unit` | `tests/unit/**/*.unit.test.ts(x)` | Vitest on jsdom, `fetch` throwing: `cn`, `useDataTable` (through `renderHook`), the token build, the token checks, the source scan, the TanStack boundary, the story-title and tier-placement checks |
|
|
269
|
+
| `integration` | `tests/integration/**/*.integration.spec.ts` | Playwright against `storybook build`, served by `vite preview`; proves the component objects and the Base UI traps |
|
|
270
|
+
|
|
271
|
+
A story's `title` is its library component's Penpot path (`Components/Button`), one story per variant value, each with a `play` function. The component name in that path is the export, so a two-word component is `Components/AlertDialog`, never `Alert Dialog`; the unit tier's story-title check fails a title whose last segment isn't a `.` export. The `design` parameter holds the library component's Penpot URL; no story sets it yet, because the design system library and its component URLs arrive with [ts-packages#51](https://github.com/bRRRITSCOLD/ts-packages/issues/51). A story that opts out of a11y (`parameters: { a11y: { test: 'off' } }`) says why in a comment beside it. The token checks: light and dark define the same semantic tokens, every class a component applies compiles to a utility of the built theme, and each of those utilities reads a token for every token-typed property it sets.
|
|
272
|
+
|
|
273
|
+
An integration spec opens a story by id (`openStory(page, 'components-dialog--nested')`, from `tests/integration/__utils__/story.ts`), which waits until Storybook has run the story's `play` function: the spec starts from the state `play` leaves. `retries` is 0. The tier-placement check fails a file whose suffix doesn't match its tier directory, and any skipped test or suite in a tier or a stories file (`test.fixme` is allowed).
|
|
274
|
+
|
|
275
|
+
`pnpm test:stories` and `pnpm test:integration` need Chromium once: `pnpm exec playwright install chromium`.
|
|
276
|
+
|
|
277
|
+
## Component objects (`./testing`)
|
|
278
|
+
|
|
279
|
+
A component object drives one component the way a person would, so an app's specs never meet a Base UI trap. Each is a plain factory over a root `Locator`, with no Playwright fixtures; an app registers the ones it uses through its own `test.extend`:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
// an app — tests/integration/__utils__/test.ts
|
|
283
|
+
import { test as base } from '@playwright/test'
|
|
284
|
+
import { dialog } from '@brrritscold/core-react/testing'
|
|
285
|
+
|
|
286
|
+
export const test = base.extend<{ profileDialog: ReturnType<typeof dialog> }>({
|
|
287
|
+
profileDialog: async ({ page }, use) =>
|
|
288
|
+
use(dialog(page.getByRole('dialog', { name: 'Edit profile' }))),
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`dialog(root)` drives a Dialog or an Alert Dialog: `root` is the popup (role `dialog` or `alertdialog`), `button(name)` and `field(label)` look inside it, and `dismiss()` presses Escape. Its methods wait; specs assert. The Base UI traps it carries, numbered as in the spec and each proven in `tests/integration/dialog.integration.spec.ts` (trap 3 belongs to Toast, 4 to Combobox and 6 to Select):
|
|
293
|
+
|
|
294
|
+
- **Trap 1: Escape closes only the top layer.** `dismiss()` waits for its dialog to be on top: a dialog under a nested one is `aria-hidden`, so a root found by role is hidden until the layers above close.
|
|
295
|
+
- **Trap 2: the page under a dialog is `aria-hidden`, not `inert`**, so an unscoped `getByLabel` or `getByText` still hits a twin on the page. Every lookup goes through `root`.
|
|
296
|
+
- **Trap 5: an exit animation keeps a closing popup mounted** (`data-ending-style`). `dismiss()` returns once the dialog is hidden; a spec asserts closed with auto-waiting `toBeHidden()`, never an immediate count.
|
|
297
|
+
- **Trap 7: `data-testid` reaches parts, not Root.** `Dialog` renders no DOM, so a testid on it reaches nothing; put it on `DialogContent` or `DialogTrigger`.
|
|
298
|
+
|
|
299
|
+
`popover(root)` drives a Popover the same way, from its popup. In a dialog, Escape or a click elsewhere in the dialog closes only the popover (trap 1).
|
|
300
|
+
|
|
301
|
+
`select(trigger)` drives a Select from its trigger (role `combobox`): `options()` reads the option labels, `select(label)` opens the popup and picks one, `value()` reads what the trigger shows, `dismiss()` presses Escape. The portalled popup is found through the trigger's `aria-controls`, so lookups stay scoped. **Trap 6:** the value lives in a hidden input; never `fill()` it. **Trap 1:** the popup puts a backdrop over the page, so in a dialog Escape or a click outside closes only the select. Proven in `select.integration.spec.ts`.
|
|
302
|
+
|
|
303
|
+
`combobox(input)` drives a Combobox from its input (role `combobox`): `filter(text)`, `options()`, `select(...labels)`, `value()`, `chips()` in `multiple`, `dismiss()`. **Trap 4:** options carry no value attribute, so everything goes by label; in `multiple` a pick clears the filter and closes the popup (Base UI's default, kept), so `select(...labels)` filters to and picks each label in turn, reopening the popup. **Trap 1:** in a dialog, Escape or a click elsewhere in the dialog closes only the popup. Proven in `combobox.integration.spec.ts`.
|
|
304
|
+
|
|
305
|
+
`toast(page)` drives the toasts `Toaster` shows, from the page: they render in the `region` named "Notifications". `get(text)` finds a toast by its title or description, `read(text)` returns `{ title, description, priority }`, `titles()` lists the toasts newest first, `button(text, name)` finds its action, `announcer(text)` the high-priority announcer, `close(text)` hovers the toast, clicks its close button and waits until it is hidden, and `waitForHidden(text)` waits out a timeout or swipe. **Trap 3:** a toast is a `dialog`, so an unscoped `getByRole('dialog')` also matches it; a high-priority toast is an `alertdialog`, `aria-hidden` until the viewport gets focus (F6 or Tab), and a visually hidden `role="alert"` outside the region announces its title and description meanwhile. The close button is `aria-hidden` too until the stack expands on hover or the button gets focus, so every lookup includes hidden elements. **Trap 2:** the region is exempt from an open dialog's `aria-hidden`, so a toast shown from a dialog is found, read and closed the same way. **Trap 5:** `close()` returns once the exit animation has unmounted the toast. Proven in `toast.integration.spec.ts`.
|
|
306
|
+
|
|
307
|
+
`Toaster` renders the provider, portal, viewport and one `Toast` per toast the `toast` manager adds (`toast.add({ title, description, type, priority, actionProps })`); `type` (`success`, `info`, `warning`, `error`, `loading`) only picks the icon. `ToastProvider`, `ToastPortal`, `ToastViewport` and `ToastContent` are exported for a custom toaster; they, `Toaster`, `toast`, `createToastManager` and `useToastManager` are code only, on the drift allowlist.
|
|
308
|
+
|
|
309
|
+
`toggleGroup(root)` drives a Toggle Group from its root (role `group`; give the group an `aria-label`): `item(label)`, `pressed()` reads the pressed items' labels in order, `press(label)` and `release(label)` click an item unless it is already in that state and wait for it. Items are toggle buttons (`aria-pressed`) even in a single-value group: there is no `radio` to find and no `aria-checked` to read, and pressing an item of a single-value group releases the one before. The group is one tab stop; the arrow keys move between items, along its `orientation`. Proven in `toggle-group.integration.spec.ts`.
|
|
310
|
+
|
|
311
|
+
`accordion(root)` drives an Accordion from its root, which has no role: put a testid on `Accordion`. `trigger(label)`, `panel(label)` (role `region`, named by its trigger), `expanded()` reads the open items' labels in order, and `open(label)` / `close(label)` click a trigger unless the item is already in that state. **Trap 5:** a closed panel is unmounted, not hidden, once its height animation ends; `close()` returns after that, so a closed item has no panel to read. Opening an item of a single-value accordion closes the one before. A disabled trigger is a native disabled button that ignores the pointer, so `open()` / `close()` throw rather than wait on it. Each trigger is its own tab stop: Base UI 1.9 dropped the accordion's roving focus (following the APG), so the arrow keys do nothing and Tab moves between triggers, skipping a disabled one. Proven in `accordion.integration.spec.ts`.
|
|
312
|
+
|
|
313
|
+
`tabs(root)` drives Tabs from its root, which holds the `tablist` and the panels and has no role: put a testid on `Tabs`. `tab(label)`, `selected()` reads the selected tab's label, `panel(label)` finds a tab's panel by the tab that names it, and `select(label)` clicks a tab and waits until it is selected. Only the selected tab's panel is mounted, but the panel being left stays a moment beside the new one, so never look a panel up as the only `tabpanel`. The arrow keys move focus between tabs without selecting (Base UI's `activateOnFocus` is off); Enter or Space selects. A disabled tab stays focusable (`aria-disabled`) but ignores the pointer, so `select()` throws rather than wait on it. Proven in `tabs.integration.spec.ts`.
|
|
314
|
+
|
|
315
|
+
`dataTable(root)` drives a data table built from `Table` and the four primitives, as `useDataTable` wires them; the parts share no role, so put a testid on the element holding them. `columns()` and `rows()` read the header labels and the page's cell text, `row(text)`, `header(label)` and `cell(text, column)` find parts, `sort(column)` reads a column's order and `sortBy(column, 'asc' | 'desc' | 'none')` sets it through the header menu, `filter(text)` types into the toolbar, `toggleColumn(label, visible)` goes through the View menu, `selectRow(text, selected?)`, `selectAll(selected?)` and `selectedRows()` drive the checkboxes, and `page()`, `pages()`, `nextPage()`, `previousPage()` and `goToPage(n)` the pagination. What it carries: **`aria-sort` is on the header cell** (`columnheader`), not on its sort button, and only on the sorted column, so `sort()` reads its absence as `none`. **The header and View menus are portalled** out of `root` and found through their trigger's `aria-controls`, which Base UI sets only once the popup has mounted; a sort pick closes its menu, but a View checkbox item keeps it open, so `toggleColumn()` closes it with Escape, and both wait out the exit animation (**trap 5**). **Pages are buttons, not links** (Base UI's `<a role="button">`), the current one `aria-current="page"`; Previous on the first page and Next on the last are `aria-disabled` and ignore the pointer, so `previousPage()` / `nextPage()` throw rather than wait. A checkbox is Base UI's `role="checkbox"` span with a hidden input beside it (**trap 6**): click the role; select-all reads `mixed` while some rows are picked. A new filter goes back to the first page. Proven in `data-table.integration.spec.ts`.
|
|
316
|
+
|
|
317
|
+
`calendar(root)` drives a `Calendar` from its root, which has no role: put a testid on `Calendar` (its root is a DOM element, unlike a Base UI Root, so the testid reaches it). Days go by ISO date (`'2026-11-09'`, the `data-day` react-day-picker writes on each cell), not by name, as a day's name is the full date in the app's locale: `day(iso)` finds its button, `selected()` reads the picked days, `months()` the months on show (each a `grid` named by its month), `next()` / `previous()` step a month and `show(iso)` steps to a date's month. `select(iso)` steps there and picks the day unless it is picked already (a click on the picked day clears it in single mode). An outside day (one of the next or previous month at a grid's edge) is skipped, so a date is found in its own month. The nav buttons are found by react-day-picker's `rdp-button_previous` / `rdp-button_next` classes, as their names follow the locale; past `startMonth` / `endMonth` they are `aria-disabled`, and a disabled day is a native disabled button (`aria-disabled` while focused), so `previous()`, `next()` and `select()` throw rather than wait. The grid is one tab stop on the picked day, else today; the arrow keys move a day or a week, Page Up / Down a month, Enter picks. Proven in `calendar.integration.spec.ts`.
|
|
318
|
+
|
|
319
|
+
`datePicker(trigger)` drives a `DatePicker` from its trigger (a testid on `DatePicker` reaches it; with a `FieldLabel` it is the button named by the label). `value()` reads what the trigger shows, `open()` opens the popup and returns a `calendar()` over the calendar in it, `select(iso)` steps to the date's month, picks it and waits for the popup to close, and `dismiss()` presses Escape. The popup is portalled and found through the trigger's `aria-controls`, which Base UI sets once it has mounted. **Trap 5:** `select()` and `dismiss()` return once the exit animation has unmounted the popup. Picking the picked date again would clear it, so `select()` closes the popup with Escape instead. Focus returns to the trigger after a pick. Proven in `date-picker.integration.spec.ts`.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion';
|
|
2
|
+
declare function Accordion({ className, ...props }: AccordionPrimitive.Root.Props): import("react").JSX.Element;
|
|
3
|
+
declare function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props): import("react").JSX.Element;
|
|
4
|
+
declare function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props): import("react").JSX.Element;
|
|
5
|
+
declare function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props): import("react").JSX.Element;
|
|
6
|
+
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Vendored from shadcn's base-vega registry (Base UI Accordion). Kept close to upstream so a
|
|
3
|
+
// re-pull diffs cleanly (see README → Vendoring). Upstream has no "use client" and neither does
|
|
4
|
+
// this file. The trigger's `aria-disabled:` is `disabled:`: Base UI 1.9 renders the trigger as a
|
|
5
|
+
// native button and disables it with the `disabled` attribute, so a disabled item never dimmed. The
|
|
6
|
+
// panel's height animation moves onto the panel: upstream put `h-(--accordion-panel-height)` and
|
|
7
|
+
// `data-starting-style:h-0` / `data-ending-style:h-0` on the inner div, which carries none of Base
|
|
8
|
+
// UI's attributes, and animated the panel with tw-animate-css's `animate-accordion-down/up`, whose
|
|
9
|
+
// keyframes read Radix's `--radix-accordion-content-height`, not Base UI's, so the height jumped.
|
|
10
|
+
// The panel now transitions its own height between 0 and `--accordion-panel-height`.
|
|
11
|
+
import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion';
|
|
12
|
+
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
|
13
|
+
import { cn } from '../lib/utils.js';
|
|
14
|
+
function Accordion({ className, ...props }) {
|
|
15
|
+
return (_jsx(AccordionPrimitive.Root, { "data-slot": "accordion", className: cn('flex w-full flex-col', className), ...props }));
|
|
16
|
+
}
|
|
17
|
+
function AccordionItem({ className, ...props }) {
|
|
18
|
+
return (_jsx(AccordionPrimitive.Item, { "data-slot": "accordion-item", className: cn('not-last:border-b', className), ...props }));
|
|
19
|
+
}
|
|
20
|
+
function AccordionTrigger({ className, children, ...props }) {
|
|
21
|
+
return (_jsx(AccordionPrimitive.Header, { className: "flex", children: _jsxs(AccordionPrimitive.Trigger, { "data-slot": "accordion-trigger", className: cn('group/accordion-trigger relative flex flex-1 items-start justify-between rounded-md border border-transparent py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground', className), ...props, children: [children, _jsx(ChevronDownIcon, { "data-slot": "accordion-trigger-icon", className: "pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" }), _jsx(ChevronUpIcon, { "data-slot": "accordion-trigger-icon", className: "pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" })] }) }));
|
|
22
|
+
}
|
|
23
|
+
function AccordionContent({ className, children, ...props }) {
|
|
24
|
+
return (_jsx(AccordionPrimitive.Panel, { "data-slot": "accordion-content", className: "h-(--accordion-panel-height) overflow-hidden text-sm transition-all duration-200 ease-out data-ending-style:h-0 data-starting-style:h-0", ...props, children: _jsx("div", { className: cn('pt-0 pb-4 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4', className), children: children }) }));
|
|
25
|
+
}
|
|
26
|
+
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Button } from './button.js';
|
|
4
|
+
declare function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props): React.JSX.Element;
|
|
5
|
+
declare function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props): React.JSX.Element;
|
|
6
|
+
declare function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props): React.JSX.Element;
|
|
7
|
+
declare function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props): React.JSX.Element;
|
|
8
|
+
declare function AlertDialogContent({ className, size, ...props }: AlertDialogPrimitive.Popup.Props & {
|
|
9
|
+
size?: 'default' | 'sm';
|
|
10
|
+
}): React.JSX.Element;
|
|
11
|
+
declare function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
|
|
12
|
+
declare function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
|
|
13
|
+
declare function AlertDialogMedia({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
|
|
14
|
+
declare function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Title>): React.JSX.Element;
|
|
15
|
+
declare function AlertDialogDescription({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Description>): React.JSX.Element;
|
|
16
|
+
declare function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof Button>): React.JSX.Element;
|
|
17
|
+
declare function AlertDialogCancel({ className, variant, size, ...props }: AlertDialogPrimitive.Close.Props & Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>): React.JSX.Element;
|
|
18
|
+
export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// Vendored from shadcn's base-vega registry (Base UI). Kept close to upstream so a re-pull diffs
|
|
4
|
+
// cleanly; the only edits put arbitrary values back on tokens (see README → Vendoring): the
|
|
5
|
+
// overlay's `bg-black/10` is `bg-foreground/10 dark:bg-background/10` (near-black at 10% in both
|
|
6
|
+
// modes), and the header's three `grid-rows-[…]` templates are dropped: implicit auto rows
|
|
7
|
+
// stretch to the same layout, with and without media.
|
|
8
|
+
import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog';
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import { cn } from '../lib/utils.js';
|
|
11
|
+
import { Button } from './button.js';
|
|
12
|
+
function AlertDialog({ ...props }) {
|
|
13
|
+
return _jsx(AlertDialogPrimitive.Root, { "data-slot": "alert-dialog", ...props });
|
|
14
|
+
}
|
|
15
|
+
function AlertDialogTrigger({ ...props }) {
|
|
16
|
+
return _jsx(AlertDialogPrimitive.Trigger, { "data-slot": "alert-dialog-trigger", ...props });
|
|
17
|
+
}
|
|
18
|
+
function AlertDialogPortal({ ...props }) {
|
|
19
|
+
return _jsx(AlertDialogPrimitive.Portal, { "data-slot": "alert-dialog-portal", ...props });
|
|
20
|
+
}
|
|
21
|
+
function AlertDialogOverlay({ className, ...props }) {
|
|
22
|
+
return (_jsx(AlertDialogPrimitive.Backdrop, { "data-slot": "alert-dialog-overlay", className: cn('fixed inset-0 isolate z-50 bg-foreground/10 dark:bg-background/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0', className), ...props }));
|
|
23
|
+
}
|
|
24
|
+
function AlertDialogContent({ className, size = 'default', ...props }) {
|
|
25
|
+
return (_jsxs(AlertDialogPortal, { children: [_jsx(AlertDialogOverlay, {}), _jsx(AlertDialogPrimitive.Popup, { "data-slot": "alert-dialog-content", "data-size": size, className: cn('group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95', className), ...props })] }));
|
|
26
|
+
}
|
|
27
|
+
function AlertDialogHeader({ className, ...props }) {
|
|
28
|
+
return (_jsx("div", { "data-slot": "alert-dialog-header", className: cn('grid place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left', className), ...props }));
|
|
29
|
+
}
|
|
30
|
+
function AlertDialogFooter({ className, ...props }) {
|
|
31
|
+
return (_jsx("div", { "data-slot": "alert-dialog-footer", className: cn('flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end', className), ...props }));
|
|
32
|
+
}
|
|
33
|
+
function AlertDialogMedia({ className, ...props }) {
|
|
34
|
+
return (_jsx("div", { "data-slot": "alert-dialog-media", className: cn("mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8", className), ...props }));
|
|
35
|
+
}
|
|
36
|
+
function AlertDialogTitle({ className, ...props }) {
|
|
37
|
+
return (_jsx(AlertDialogPrimitive.Title, { "data-slot": "alert-dialog-title", className: cn('text-lg font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2', className), ...props }));
|
|
38
|
+
}
|
|
39
|
+
function AlertDialogDescription({ className, ...props }) {
|
|
40
|
+
return (_jsx(AlertDialogPrimitive.Description, { "data-slot": "alert-dialog-description", className: cn('text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground', className), ...props }));
|
|
41
|
+
}
|
|
42
|
+
function AlertDialogAction({ className, ...props }) {
|
|
43
|
+
return _jsx(Button, { "data-slot": "alert-dialog-action", className: cn(className), ...props });
|
|
44
|
+
}
|
|
45
|
+
function AlertDialogCancel({ className, variant = 'outline', size = 'default', ...props }) {
|
|
46
|
+
return (_jsx(AlertDialogPrimitive.Close, { "data-slot": "alert-dialog-cancel", className: cn(className), render: _jsx(Button, { variant: variant, size: size }), ...props }));
|
|
47
|
+
}
|
|
48
|
+
export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
declare const alertVariants: (props?: ({
|
|
4
|
+
variant?: "default" | "destructive" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
declare function Alert({ className, variant, ...props }: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>): React.JSX.Element;
|
|
7
|
+
declare function AlertTitle({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
|
|
8
|
+
declare function AlertDescription({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
|
|
9
|
+
declare function AlertAction({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
|
|
10
|
+
export { Alert, AlertAction, AlertDescription, AlertTitle, alertVariants };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Vendored from shadcn's base-vega registry. Kept close to upstream so a re-pull diffs cleanly; the
|
|
3
|
+
// only edits put arbitrary values back on tokens (see README → Vendoring): the icon layout's
|
|
4
|
+
// `has-[>svg]:grid-cols-[auto_1fr]` is `has-[>svg]:justify-start`, so the implicit columns hug
|
|
5
|
+
// the icon and the text takes the rest, and the action's `pr-18` reads `space.18`, added for it.
|
|
6
|
+
// `alertVariants` is exported, as every cva config is.
|
|
7
|
+
import { cva } from 'class-variance-authority';
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import { cn } from '../lib/utils.js';
|
|
10
|
+
const alertVariants = cva("group/alert relative grid w-full gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:justify-start has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", {
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: 'bg-card text-card-foreground',
|
|
14
|
+
destructive: 'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: 'default',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
function Alert({ className, variant, ...props }) {
|
|
22
|
+
return (_jsx("div", { "data-slot": "alert", role: "alert", className: cn(alertVariants({ variant }), className), ...props }));
|
|
23
|
+
}
|
|
24
|
+
function AlertTitle({ className, ...props }) {
|
|
25
|
+
return (_jsx("div", { "data-slot": "alert-title", className: cn('font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground', className), ...props }));
|
|
26
|
+
}
|
|
27
|
+
function AlertDescription({ className, ...props }) {
|
|
28
|
+
return (_jsx("div", { "data-slot": "alert-description", className: cn('text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4', className), ...props }));
|
|
29
|
+
}
|
|
30
|
+
function AlertAction({ className, ...props }) {
|
|
31
|
+
return (_jsx("div", { "data-slot": "alert-action", className: cn('absolute top-2.5 right-3', className), ...props }));
|
|
32
|
+
}
|
|
33
|
+
export { Alert, AlertAction, AlertDescription, AlertTitle, alertVariants };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Avatar as AvatarPrimitive } from '@base-ui/react/avatar';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
declare function Avatar({ className, size, ...props }: AvatarPrimitive.Root.Props & {
|
|
4
|
+
size?: 'default' | 'sm' | 'lg';
|
|
5
|
+
}): React.JSX.Element;
|
|
6
|
+
declare function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props): React.JSX.Element;
|
|
7
|
+
declare function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props): React.JSX.Element;
|
|
8
|
+
declare function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>): React.JSX.Element;
|
|
9
|
+
declare function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
|
|
10
|
+
declare function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
|
|
11
|
+
export { Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage };
|