@sableui/react 0.3.2 → 0.5.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.
Files changed (59) hide show
  1. package/AGENTS.md +3 -3
  2. package/CHANGELOG.md +87 -0
  3. package/CONTRIBUTING.md +2 -2
  4. package/DESIGN.md +7 -7
  5. package/README.md +2 -2
  6. package/bin/sable-lint.mjs +0 -0
  7. package/bin/sable.mjs +4 -3
  8. package/dist/{_contract-BlKMR4eX.d.ts → _contract-Cn3lUpih.d.ts} +7 -7
  9. package/dist/blocks/index.js +4 -4
  10. package/dist/{chunk-YXBOYWBN.js → chunk-C4Z3CF54.js} +1 -1
  11. package/dist/{chunk-MXJX3NZI.js → chunk-EFLC4NA3.js} +4 -4
  12. package/dist/{chunk-EP55KSYA.js → chunk-EHH4MHO6.js} +6 -6
  13. package/dist/{chunk-FBSL2LZQ.js → chunk-GU3T7SBA.js} +11 -30
  14. package/dist/{chunk-NGBDP3BM.js → chunk-QJ6FWTUG.js} +1 -1
  15. package/dist/{chunk-CCUDFLU3.js → chunk-RCLKXF2N.js} +43 -9
  16. package/dist/{chunk-TH563WLW.js → chunk-XAZGMY73.js} +10 -3
  17. package/dist/{chunk-D2ETQBHV.js → chunk-ZBZMKOO7.js} +4 -4
  18. package/dist/icon/index.js +2 -2
  19. package/dist/icon-sets/brands.d.ts +0 -27
  20. package/dist/icon-sets/brands.js +7 -1
  21. package/dist/icon-sets/reicon.js +1 -1
  22. package/dist/index.d.ts +44 -19
  23. package/dist/index.js +104 -27
  24. package/dist/patterns/index.js +4 -4
  25. package/dist/presets/index.d.ts +1 -1
  26. package/dist/presets/index.js +1 -1
  27. package/dist/styles.css +19 -11
  28. package/dist/{theme-provider-CuYYxF0o.d.ts → theme-provider-CGeiusgu.d.ts} +3 -3
  29. package/dist/themes/atelier/index.d.ts +1 -1
  30. package/dist/themes/atelier/index.js +1 -1
  31. package/dist/themes/ember/ember.css +8 -5
  32. package/dist/themes/ember/index.d.ts +1 -1
  33. package/dist/themes/index.d.ts +3 -3
  34. package/dist/themes/index.js +3 -3
  35. package/dist/themes/meridian/index.d.ts +1 -1
  36. package/dist/themes/poppy/index.d.ts +1 -1
  37. package/dist/themes/poppy/poppy.css +1 -1
  38. package/dist/themes/prism/index.d.ts +1 -1
  39. package/dist/themes/sable/index.d.ts +9 -0
  40. package/dist/themes/sable/index.js +1 -0
  41. package/dist/themes/{base/base.css → sable/sable.css} +6 -6
  42. package/dist/themes/verdant/index.d.ts +1 -1
  43. package/dist/themes.css +3 -3
  44. package/llms/components/tabs.md +1 -1
  45. package/llms/components/theme-selector.md +1 -1
  46. package/llms/components/toast.md +12 -1
  47. package/llms/reference/token-intent.md +1 -1
  48. package/llms/tasks/building-a-page.md +1 -1
  49. package/llms/tasks/creating-a-preset.md +1 -1
  50. package/llms/tasks/designing-a-surface.md +1 -1
  51. package/llms-full.txt +19 -8
  52. package/llms.txt +2 -2
  53. package/package.json +5 -5
  54. package/registry.json +43 -14
  55. package/scripts/design-presets.mjs +1 -1
  56. package/scripts/grade.mjs +18 -5
  57. package/scripts/lint-rules.mjs +20 -1
  58. package/dist/themes/base/index.d.ts +0 -9
  59. package/dist/themes/base/index.js +0 -1
package/AGENTS.md CHANGED
@@ -4,7 +4,7 @@ How to **build with** Sable. This is the whole consumer contract an agent needs;
4
4
 
5
5
  ## What this is
6
6
 
7
- **Sable** is an owned, themeable React component system: about 100 components (`npx sable manifest` has the live count), 13 layout primitives, 8 page shells, on [Base UI](https://base-ui.com) behavior with Tailwind v4 + semantic CSS-variable tokens. The default theme is **Base**; the other featured presets are Meridian, Prism, Verdant, Ember, and Poppy.
7
+ **Sable** is an owned, themeable React component system: about 100 components (`npx sable manifest` has the live count), 13 layout primitives, 8 page shells, on [Base UI](https://base-ui.com) behavior with Tailwind v4 + semantic CSS-variable tokens. The default theme is **Sable**; the other featured presets are Meridian, Prism, Verdant, Ember, and Poppy.
8
8
 
9
9
  **Where things are**
10
10
 
@@ -59,12 +59,12 @@ The machine-readable version of this section is the `consume` block in `registry
59
59
  2. **Dedupe React** in the bundler (`resolve.dedupe: ["react","react-dom"]`) — Base UI must not resolve a second React, or you get an "invalid hook call" blank page.
60
60
  3. **CSS, in order:** `@import "tailwindcss";` → `@import "@sableui/react/styles.css";` → `@source "<node_modules>/@sableui/react/dist/**/*.{js,mjs}";` so Tailwind scans the package's classes.
61
61
 
62
- `styles.css` carries **Base only** — the default theme, and the layout floor every other theme inherits (`--page-inset`, `--region-*`, `--rhythm-unit` on a bare `:root`). The bundled **alternates are opt-in**, because they are `[data-sable-theme]`-scoped and an app that never sets the attribute ships CSS that can never match: 5.5 KB gzip of permanently-dead weight, which is exactly how a real app's build audit found this. Using one? `@import "@sableui/react/theme/prism.css";`. Offering the whole set through a `<ThemeSelector>`? `@import "@sableui/react/themes.css";`. **Symptom of forgetting:** `data-sable-theme` is set, nothing matches, and the page silently renders as Base — no error, just the wrong theme.
62
+ `styles.css` carries **Sable only** — the default theme, and the layout floor every other theme inherits (`--page-inset`, `--region-*`, `--rhythm-unit` on a bare `:root`). The bundled **alternates are opt-in**, because they are `[data-sable-theme]`-scoped and an app that never sets the attribute ships CSS that can never match: 5.5 KB gzip of permanently-dead weight, which is exactly how a real app's build audit found this. Using one? `@import "@sableui/react/theme/prism.css";`. Offering the whole set through a `<ThemeSelector>`? `@import "@sableui/react/themes.css";`. **Symptom of forgetting:** `data-sable-theme` is set, nothing matches, and the page silently renders as the Sable theme — no error, just the wrong theme.
63
63
  4. **Fonts:** load Figtree (sans) + DM Mono (mono) — the theme declares the stacks, the host provides the files. Apply `antialiased` (`-webkit-font-smoothing: antialiased` / `-moz-osx-font-smoothing: grayscale`) **once on the app root**: Sable's type scale was drawn against smoothed text, and without it macOS renders every weight heavier than designed. The package deliberately doesn't set it — a library has no business styling the host's whole page — so it has to happen here.
64
64
  5. **Providers (all optional):** components render with **Reicon** by default, so `<IconProvider set={…}>` is only needed to replace it with another icon package or extend it using `composeIconSets(appExtras, reiconIconSet)` (list the added names in `.sable/icons.json` as `{ "names": [...] }` so `sable grade` accepts them); `<MotionProvider>` is optional too. Use `<SableThemeProvider>` + `useSableTheme()` when the app should expose user-selectable themes; pair `themeId` / `setThemeId` with `<ThemeSelector>` and `preference` / `setPreference` with `<ThemeSwitcher>`. The provider stores `light` / `dark` / `system`, toggles the `dark` class on `<html>`, stores the active design theme, and writes `data-sable-theme` for non-default themes. Apps with their own state layer can call `applySableColorMode()` / `applySableTheme()` instead. You only need a Tailwind `@custom-variant dark` if **your** app code uses `dark:` utilities (Sable ships none).
65
65
  6. Import from the root barrel: `import { Button } from "@sableui/react"`. Page shells live on the `patterns` subpath (`import { SettingsPage } from "@sableui/react/patterns"`) — preview one, then copy its source from `registry.json → patterns` when the route needs to diverge. Blocks — section-sized compositions such as a sign-in card or a pricing card — live on the `blocks` subpath (`import { SignInBlock } from "@sableui/react/blocks"`) and ship the same way (`registry.json → blocks`); copy one and edit its content constants. A block's props are only `className`, `framed` and its primary action's callback, never content.
66
66
 
67
- 7. **Check the wiring:** `npx sable doctor`. Every check it runs corresponds to a way Sable fails *silently* — a second React (blank page, "invalid hook call"), a missing `@source` (Tailwind emits none of the package's classes, everything renders unstyled), or `data-sable-theme` set with no theme CSS imported (every theme renders as Base and the switcher looks broken). Exits 1 on any failure; `--json` for the structured report.
67
+ 7. **Check the wiring:** `npx sable doctor`. Every check it runs corresponds to a way Sable fails *silently* — a second React (blank page, "invalid hook call"), a missing `@source` (Tailwind emits none of the package's classes, everything renders unstyled), or `data-sable-theme` set with no theme CSS imported (every theme renders as the Sable theme and the switcher looks broken). Exits 1 on any failure; `--json` for the structured report.
68
68
 
69
69
  8. **Read the catalog through the CLI, not the file.** `registry.json` is about 1 MB and `llms-full.txt` about 800 KB — complete and unreadable in a working context. `npx sable` serves the same generated contract in pieces, offline, at your installed version:
70
70
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # What's new
2
2
 
3
+ ## 0.5.0
4
+
5
+ - **Stacked toasts.** `<ToastProvider stacked>` stacks toasts like a deck of
6
+ cards: the newest in front, older ones peeking out behind it a step smaller,
7
+ so a burst of toasts no longer takes over the corner of the page. Hovering
8
+ or focusing the stack fans it out into the list. It works at every
9
+ `position`. `ToastContent` is the new part that holds a toast's row; in a
10
+ stacked viewport it hides the cards behind the front one. Collapsed, the
11
+ deck casts one shadow rather than one per card, so a theme with hard offset
12
+ shadows (Poppy) stays clean.
13
+ - **Depth in Customize.** On sableui.com, Customize has a Depth control (Flat ↔
14
+ Lifted) and a Shadow style (Preset, Soft, Crisp, Offset). They write the five
15
+ `--theme-shadow-*` tokens for light and dark, so the CSS export and preset
16
+ download carry them. Depth scales the reach and strength of every step
17
+ together; lifting a flat preset such as Meridian brings in soft shadows. The
18
+ shadow takes the preset's neutral hue. Advanced replaces the slider with
19
+ exact numbers set on the largest step, which the others follow: light angle
20
+ (90° is overhead), distance, blur and opacity. Blur takes one
21
+ blur-to-distance ratio, so an Offset shadow can be softened; opacity is set
22
+ in light mode and dark mode darkens in proportion.
23
+ - **Knob shadows stay on their track.** The Switch thumb, Slider thumb and
24
+ ColorField handle used `shadow-sm`, the elevation step for raised surfaces.
25
+ Where a theme or a Depth setting made that a long offset (Poppy's hard
26
+ drops), the shadow fell outside the switch's track. Knobs now take
27
+ `--knob-shadow`, one small contact shadow that themes can set through
28
+ `--theme-knob-shadow`. In Sable it looks the same as before.
29
+ - **Pill tabs take the theme's button shape.** `TabsList variant="pill"`
30
+ triggers used the small radius, so in Sable they were rounded rectangles
31
+ beside pill buttons. They now read `--button-radius`
32
+ as `Button` does: pills in Sable, 12px in Poppy, square in Meridian, and a
33
+ theme's own button shape wherever it sets one.
34
+ - **CodeBlock no longer scrolls code under its copy button.** Without a
35
+ filename header, the copy button floated over the code, so on a narrow
36
+ screen a long line ran underneath it. It now has its own column beside the
37
+ code. The code's edges fade while there is more to scroll, like `Scroller`'s.
38
+ - **Scroller's edge fades follow content and size changes.** They were measured
39
+ on mount and on scroll only, so a list that grew, shrank or was resized kept
40
+ the fade it started with until it was scrolled.
41
+ - **Icons no longer redraw on every render.** The default Reicon set and the
42
+ brand set rewrote their SVG each time a parent re-rendered (React 19
43
+ re-applies `dangerouslySetInnerHTML` when the object changes). Inside a
44
+ toast that never settled and froze the page.
45
+ - **A card that is only a header keeps its bottom padding.** `CardHeader` leaves
46
+ the bottom padding to the content that follows it; with nothing after it (a
47
+ stat card: label, number, sparkline) the number sat on the card's edge.
48
+ - **Line and area sparklines span their own range.** They used to include zero,
49
+ so a run from 12 to 26 used only the top half of the box: a flat squiggle over
50
+ a solid block. Bar sparklines keep the zero baseline, since a bar's length is
51
+ the value.
52
+ - **Combobox chips are balanced.** The remove button was a full 28px icon
53
+ button, which swelled each chip into a 34px pill and left more room after the
54
+ label than before it. It is now drawn to the chip (20px, with a 14px X) and
55
+ keeps a 28px pointer target, so the label and the X sit evenly in a 23px chip.
56
+
57
+ ## 0.4.0
58
+
59
+ ### The default theme is now called Sable
60
+
61
+ The default theme was called Base, which read too close to Base UI, the
62
+ library Sable's components are built on. It is now **Sable**. It looks the same.
63
+
64
+ - Theme id `base` → `sable` (`data-sable-theme="sable"`, `setThemeId("sable")`).
65
+ - `@sableui/react/theme/base` and `theme/base.css` → `theme/sable` and
66
+ `theme/sable.css`. `styles.css` still includes it, so most apps import nothing new.
67
+ - `baseTheme` → `sableTheme`.
68
+ - A stored `base` choice is no longer a known id, so `SableThemeProvider`
69
+ falls back to the default, which is Sable.
70
+
71
+ ### Themes
72
+
73
+ - **Panel fields read in dark mode.** `panel-field` was the canvas tone, 3% ink
74
+ off the panel, which measured 1.04:1 on a dark panel. It is now mixed from the
75
+ panel at 9% ink (1.15:1 dark, 1.25:1 light), with hover a step further at 12%.
76
+ - **Ember's dark-mode primary button has white text.** The fill is now Ember's
77
+ light-mode orange-red. On the old, brighter fill the dark label passed WCAG
78
+ but read muddy (APCA Lc 42); white now clears both (4.8:1, Lc 76). The
79
+ sidebar's active item follows.
80
+
81
+ ### Agent tools
82
+
83
+ - `sable-lint` no longer reports `hand-derived-surface` on the value of a
84
+ `--theme-*` hook, such as `--theme-panel-field: color-mix(…)`. Tuning a
85
+ derived token is what the hooks are for.
86
+ - `sable grade --ignore <glob>` skips the same paths `sable-lint --ignore`
87
+ does, so a data layer of document colours is not counted as contract
88
+ violations. Ignored files are still fingerprinted.
89
+
3
90
  ## 0.3.2
4
91
 
5
92
  ### Workspace panels
package/CONTRIBUTING.md CHANGED
@@ -117,7 +117,7 @@ deliberately reverses: `DESIGN.md → Registers`. What a contributor needs to kn
117
117
 
118
118
  ## Theme contract
119
119
 
120
- A theme is a CSS file that defines every token in `themes/_contract.ts` for `:root` (light) and `.dark` (dark), plus an optional TS manifest. Components depend on the **semantic** layer; each theme supplies its own palette underneath. Alternate themes activate through the root `data-sable-theme` attribute; consuming apps should usually let `<SableThemeProvider>` own that wiring and read/update it through `useSableTheme()`. To make a new theme: copy `themes/base/`, swap the palette + semantic values, keep the token names identical, add the manifest to `themes/registry.ts`, import its CSS from `src/themes.css`, add `./theme/<id>` + `./theme/<id>.css` to both `exports` and `publishConfig.exports`, and cover it in `themes.test.ts`. `build-registry` fails on a theme missing from any of those, and generates `registry.json → themes.available` from the manifests — never hand-edit that list. Re-skinning never touches component code.
120
+ A theme is a CSS file that defines every token in `themes/_contract.ts` for `:root` (light) and `.dark` (dark), plus an optional TS manifest. Components depend on the **semantic** layer; each theme supplies its own palette underneath. Alternate themes activate through the root `data-sable-theme` attribute; consuming apps should usually let `<SableThemeProvider>` own that wiring and read/update it through `useSableTheme()`. To make a new theme: copy `themes/sable/`, swap the palette + semantic values, keep the token names identical, add the manifest to `themes/registry.ts`, import its CSS from `src/themes.css`, add `./theme/<id>` + `./theme/<id>.css` to both `exports` and `publishConfig.exports`, and cover it in `themes.test.ts`. `build-registry` fails on a theme missing from any of those, and generates `registry.json → themes.available` from the manifests — never hand-edit that list. Re-skinning never touches component code.
121
121
 
122
122
  **Themes are presets, not the unit of customization.** Every token a theme sets can also be set by an app (or by the docs customizer) on `:root` after the theme loads. A theme is the starting point a consumer picks; the customizer exports the deltas on top of it. Keep that working: a theme must not set a token with `!important` or under a selector more specific than `[data-sable-theme="<id>"]`, or a consumer override cannot win.
123
123
 
@@ -130,7 +130,7 @@ while allowing a consumer to tune the whole type system, rather than making only
130
130
  headings reflect its art direction. Existing defaults remain the fallback; themes
131
131
  do not need to declare every optional hook.
132
132
 
133
- `SEMANTIC_TOKENS` includes `border-strong` alongside `border` — a visually-stronger structural divider (grid lines, table rules, section separators) distinct from `border`, which stays tuned for subtle chrome (badge/input outlines). Both need light + dark values, derived from the same neutral ramp as `border` but pulled from a higher-contrast step (e.g. Base: `border` = `--zinc-200` light / low-alpha `--zinc-700` dark → `border-strong` = `--zinc-300` light / higher-alpha `--zinc-500` dark). `styles.css`'s `@theme inline` block maps it to `--color-border-strong`, so components/consumers use it via the `border-strong` Tailwind color utility, same pattern as `border`.
133
+ `SEMANTIC_TOKENS` includes `border-strong` alongside `border` — a visually-stronger structural divider (grid lines, table rules, section separators) distinct from `border`, which stays tuned for subtle chrome (badge/input outlines). Both need light + dark values, derived from the same neutral ramp as `border` but pulled from a higher-contrast step (e.g. the Sable theme: `border` = `--zinc-200` light / low-alpha `--zinc-700` dark → `border-strong` = `--zinc-300` light / higher-alpha `--zinc-500` dark). `styles.css`'s `@theme inline` block maps it to `--color-border-strong`, so components/consumers use it via the `border-strong` Tailwind color utility, same pattern as `border`.
134
134
 
135
135
  `TYPE_OVERLINE_TOKENS` (`type-overline-size/weight/leading/tracking/transform`) back the `text-overline` utility — the eyebrow/badge-label/stat-caption text role with no home in `TYPE_SCALE_TOKENS`'s display/heading/title ramp. `type-overline-transform` is themeable rather than a hardcoded `uppercase`, so a quieter reference (e.g. Cove) can opt into sentence case. Like the other type tiers, every built-in theme declares explicit values rather than relying on styles.css's fallback defaults (`0.75rem` / weight `600` / leading `1.2` / tracking `0.08em` / `uppercase`).
136
136
 
package/DESIGN.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sable — Design System Spec
2
2
 
3
- The system-level design spec for **Sable**, an owned, themeable component system. This document is **theme-agnostic**: it describes the structure, tokens, and rules that every theme inherits. The **default theme is "Base"** (a dark-by-default, content-first, fluid product aesthetic); its concrete values live in `src/themes/base/`. Other themes supply their own values for the same token contract.
3
+ The system-level design spec for **Sable**, an owned, themeable component system. This document is **theme-agnostic**: it describes the structure, tokens, and rules that every theme inherits. The **default theme is "Sable"** (a dark-by-default, content-first, fluid product aesthetic); its concrete values live in `src/themes/sable/`. Other themes supply their own values for the same token contract.
4
4
 
5
5
  For building with Sable see `AGENTS.md`; for changing the package see `CONTRIBUTING.md`. For the machine-readable catalog see `registry.json` / `llms.txt`.
6
6
 
@@ -9,7 +9,7 @@ For building with Sable see `AGENTS.md`; for changing the package see `CONTRIBUT
9
9
  1. **Own the surface, theme the values.** Components reference semantic roles, not colors. Swapping a theme re-skins everything without touching a component.
10
10
  2. **Content over chrome.** The UI recedes so content advances. Restraint over decoration.
11
11
  3. **Constraint is generative.** A small set of tokens, tiers, and primitives yields a large space of good UIs. Reach for what exists before inventing.
12
- 4. **A deliberate visual world.** Base starts dark. A consuming product chooses its modes, typography, shape, and art direction from its brief; the token contract travels across those choices.
12
+ 4. **A deliberate visual world.** The Sable theme starts dark. A consuming product chooses its modes, typography, shape, and art direction from its brief; the token contract travels across those choices.
13
13
  5. **Accessible by construction.** Base UI carries behavior/ARIA; semantic token pairs are pre-checked to WCAG AA.
14
14
  6. **AI-native.** The system is documented for machines as well as people (`registry.json`, per-component `*.meta.ts`).
15
15
 
@@ -26,7 +26,7 @@ evidence contract. A reviewer still has to see and operate the result. Read
26
26
 
27
27
  The semantic contract protects meaning, behavior, and consistency. It does not
28
28
  choose the story, visual hierarchy, imagery, or composition for every product.
29
- Statements about Base's accent, shape, and dark-first identity describe the
29
+ Statements about the Sable theme's accent, shape, and dark-first identity describe the
30
30
  default theme; consumers can choose a different art direction while preserving
31
31
  semantic roles and accessibility. A palette change alone is not a new design.
32
32
 
@@ -57,7 +57,7 @@ Three layers, outermost is what components use:
57
57
  | **Semantic** (the contract) | `--primary`, `--card`, `--muted-foreground`, `--tone-edition`, `--ring` | **Components** (via `bg-*`/`text-*` utilities). |
58
58
  | **Component** (local) | a cva variant string | Inside one component. |
59
59
 
60
- `themes/_contract.ts` enumerates the semantic tokens every theme must define. The default (Base) palette is authored in **OKLCH** for perceptually-uniform ramps, locked hue, gamut-safe chroma, and dark-mode vibrancy.
60
+ `themes/_contract.ts` enumerates the semantic tokens every theme must define. The default (Sable) palette is authored in **OKLCH** for perceptually-uniform ramps, locked hue, gamut-safe chroma, and dark-mode vibrancy.
61
61
 
62
62
  ### Semantic tokens (the contract)
63
63
 
@@ -68,7 +68,7 @@ Tone ink (legible text/icon on a tone fill, theme-adaptive): `tone-standard-fore
68
68
  Overlay (fixed dark surface over content — scrims, media letterbox, pills on imagery): `overlay`, `overlay-foreground`.
69
69
  Sidebar set: `sidebar`, `sidebar-foreground`, `sidebar-primary(-foreground)`, `sidebar-accent(-foreground)`, `sidebar-border`, `sidebar-ring`.
70
70
 
71
- **Base inversion rule:** in dark mode `primary` is light (a near-white pill); in light mode it is near-black. Other themes may use a different action palette. Never call `primary` "black" or `primary-foreground` "white" — use the semantic pair, whose values belong to the theme.
71
+ **Sable theme inversion rule:** in dark mode `primary` is light (a near-white pill); in light mode it is near-black. Other themes may use a different action palette. Never call `primary` "black" or `primary-foreground` "white" — use the semantic pair, whose values belong to the theme.
72
72
 
73
73
  ### Tone system
74
74
 
@@ -442,7 +442,7 @@ Motion is a first-class token layer, not an afterthought. **Subtle by default, w
442
442
 
443
443
  **Three parts:**
444
444
 
445
- 1. **Tokens** (`themes/base/base.css` → Motion tokens; contract in `themes/_contract.ts` → `MOTION_TOKENS`).
445
+ 1. **Tokens** (`themes/sable/sable.css` → Motion tokens; contract in `themes/_contract.ts` → `MOTION_TOKENS`).
446
446
  - **Durations:** `instant` 75 · `fast` 150 · `base` 200 · `slow` 300 · `slower` 500, plus semantic `enter`/`exit`. Every duration is multiplied by `--motion-scale`.
447
447
  - **Easings:** `standard` (≈ease-in-out, default) · `decelerate` (≈ease-out, entrances) · `emphasized` (large surfaces) · `linear` (indeterminate loaders) · **`spring`** (gentle ~+4% single overshoot — the default for delightful pops) · **`bounce`** (~+10%, opt-in playful accents). Springs apply on **enter only**; exits stay overshoot-free.
448
448
  - Consume easings via the `ease-*` utilities; durations via `duration-(--motion-duration-*)` (so the kill-switch always applies).
@@ -526,7 +526,7 @@ Not every removal is a regression; check for an **equivalent replacement** befor
526
526
  - Inline `oklch()`/hex or raw palette steps in components. *(CI-enforced: `inline-color`, `palette-ref`.)*
527
527
  - Decomposing paired typography utilities. *(CI-enforced: `decomposed-type`.)*
528
528
  - Pure `#000`/`#fff` (use `zinc-950`/`zinc-50`, or `overlay`/`overlay-foreground` for scrims). *(CI-enforced: `raw-black-white`.)*
529
- - In Base, purple/brand for primary CTAs (brand = editions + selection only). A different theme chooses its action palette through the semantic pair.
529
+ - In the Sable theme, purple/brand for primary CTAs (brand = editions + selection only). A different theme chooses its action palette through the semantic pair.
530
530
  - Gradient text, side-stripe borders, nested cards, mixed radii.
531
531
  - Concrete icon-package imports scattered through components. Reicon supplies the default through the shared adapter; IconProvider allows replacement.
532
532
  - A global page max-width (use opt-in region caps).
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sable
2
2
 
3
- **Sable** — a themeable React component system with layout patterns, semantic tokens and tools for coding agents. Default theme: **Base**. On npm as `@sableui/react`; docs and the live catalog at [sableui.com](https://www.sableui.com).
3
+ **Sable** — a themeable React component system with layout patterns, semantic tokens and tools for coding agents. Default theme: **Sable**. On npm as `@sableui/react`; docs and the live catalog at [sableui.com](https://www.sableui.com).
4
4
 
5
5
  For a clickable iOS prototype (demos and presentations, not production), ask the
6
6
  Sable skill to build with the iOS target. Every screen is Sable components in your
@@ -13,7 +13,7 @@ Frame it with `PhoneFrame` from `@sableui/react/ios/preview`. See
13
13
 
14
14
  Original Sable code is [MIT licensed](./LICENSE).
15
15
 
16
- The featured presets are Base (neutral), Meridian (square), Prism (compact technical),
16
+ The featured presets are Sable (neutral), Meridian (square), Prism (compact technical),
17
17
  Verdant (serif), Ember (warm), and Poppy (playful). Every preset supports light and
18
18
  dark modes. Load its declared fonts and use `ThemeSelector` to offer preset choices.
19
19
  [Third-party notices](./THIRD_PARTY_NOTICES.md) list the software and fonts used
Binary file
package/bin/sable.mjs CHANGED
@@ -83,7 +83,7 @@ const COMMANDS = [
83
83
  summary: "Create, validate, or compile a portable Sable preset used by Canvas and builds.",
84
84
  arg: null,
85
85
  flags: [
86
- { name: "--template", value: null, describe: "Print a complete editable Base preset JSON record." },
86
+ { name: "--template", value: null, describe: "Print a complete editable Sable preset JSON record." },
87
87
  { name: "--check", value: "file.json", describe: "Validate the schema and Sable token contract." },
88
88
  { name: "--css", value: "file.json", describe: "Generate scoped theme CSS from a validated preset." },
89
89
  ],
@@ -210,6 +210,7 @@ const COMMANDS = [
210
210
  { name: "--strict", value: null, describe: "Exit 1 for contract findings or missing/incomplete/unresolved review evidence." },
211
211
  { name: "--cli-log", value: "file.jsonl", describe: "Include retrieval usage from a SABLE_CLI_LOG file." },
212
212
  { name: "--icons", value: "icons.json", describe: 'Icon names the app adds with composeIconSets, as { "names": [...] }. Default: .sable/icons.json when present.' },
213
+ { name: "--ignore", value: "glob", describe: "Skip matching paths in the checks, with the same globs as sable-lint --ignore. Repeatable." },
213
214
  ],
214
215
  returns: ["grade.score", "grade.template"],
215
216
  },
@@ -1078,7 +1079,7 @@ function cmdDoctor() {
1078
1079
  /* The theme trap: opting into theme switching without importing theme CSS. */
1079
1080
  const src = collectSources(/\.(tsx?|jsx?)$/)
1080
1081
  const srcText = src.map((f) => f.text).join("\n")
1081
- // A provider used only for light/dark mode needs Base, not alternate themes.
1082
+ // A provider used only for light/dark mode needs only the Sable theme, not alternate themes.
1082
1083
  const usesThemes = /ThemeSelector|data-sable-theme|applySableTheme|setThemeId|defaultThemeId/.test(srcText)
1083
1084
  if (usesThemes) {
1084
1085
  const themeCss = /@import\s+["'](?:@sableui\/react\/|[^"']*packages\/sable\/(?:src|dist)\/)(themes\.css|theme\/[a-z-]+\.css|themes\/[a-z-]+\/[a-z-]+\.css)["']/.test(cssText)
@@ -1088,7 +1089,7 @@ function cmdDoctor() {
1088
1089
  add(
1089
1090
  "fail",
1090
1091
  "themes",
1091
- "This app selects a design theme but has no alternate or app-owned scoped theme CSS — the selected theme may silently render as Base.",
1092
+ "This app selects a design theme but has no alternate or app-owned scoped theme CSS — the selected theme may silently render as the Sable theme.",
1092
1093
  'Import themes.css, one theme/<id>.css, or the scoped CSS generated by sable preset --css.',
1093
1094
  )
1094
1095
  }
@@ -4,14 +4,14 @@
4
4
  * never a theme's internal palette steps — which is what makes the system
5
5
  * re-themeable without touching component code.
6
6
  *
7
- * To author a new theme: copy `themes/base/`, keep these token names, and
7
+ * To author a new theme: copy `themes/sable/`, keep these token names, and
8
8
  * supply your own palette + values. See CONTRIBUTING.md → "Theme contract".
9
9
  */
10
10
  declare const SEMANTIC_TOKENS: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "muted", "muted-foreground", "border", "border-strong", "input", "control-border", "ring", "primary", "primary-foreground", "secondary", "secondary-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "success", "success-foreground", "tone-standard", "tone-collectible", "tone-edition", "tone-info", "tone-warning", "highlight", "tone-standard-foreground", "tone-collectible-foreground", "tone-edition-foreground", "tone-info-foreground", "tone-warning-foreground", "highlight-foreground", "overlay", "overlay-foreground", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring"];
11
11
  type SemanticToken = (typeof SEMANTIC_TOKENS)[number];
12
12
  /**
13
13
  * Motion tokens (mode-independent). A theme MAY override these to retune its
14
- * motion feel; if omitted, the Base defaults apply. Components reference
14
+ * motion feel; if omitted, the Sable theme's defaults apply. Components reference
15
15
  * them via `duration-(--motion-duration-*)` + the `ease-*` utilities, or — far
16
16
  * more often — via the named motion recipes (`motion-pop`, `motion-press`, …)
17
17
  * defined in styles.css. Every duration is multiplied by `--motion-scale`, the
@@ -23,7 +23,7 @@ type MotionToken = (typeof MOTION_TOKENS)[number];
23
23
  /**
24
24
  * Radius tokens (mode-independent). A theme MAY override these to retune its
25
25
  * shape language (soft-square, rectilinear, full-pill, …); if omitted, the
26
- * Base defaults apply (see styles.css → `@theme inline`, which bridges
26
+ * Sable defaults apply (see styles.css → `@theme inline`, which bridges
27
27
  * each `--theme-radius-*` var to the `radius-*` Tailwind scale). Components
28
28
  * never reference these directly — they use the derived `rounded-*`
29
29
  * utilities.
@@ -33,7 +33,7 @@ type RadiusToken = (typeof RADIUS_TOKENS)[number];
33
33
  /**
34
34
  * Type-scale tokens (mode-independent). A theme MAY override these to retune
35
35
  * its typographic voice (weight, leading, tracking, and — for heading/display
36
- * tiers — size); if omitted, the Base defaults baked into styles.css's
36
+ * tiers — size); if omitted, the Sable theme's defaults baked into styles.css's
37
37
  * paired `text-*` utilities apply. Additional body/label/caption/mono and size
38
38
  * hooks are advertised separately in TYPE_REFINEMENT_TOKENS: they are optional,
39
39
  * so a theme can retune its reading voice without duplicating every default.
@@ -97,9 +97,9 @@ type DensityToken = (typeof DENSITY_TOKENS)[number];
97
97
  * tuned for the opposite job (lifting content off a page).
98
98
  * - `panel-field` / `panel-field-hover` are the inset fill of a compact control
99
99
  * docked in a panel (the `size="xs"` field rung, a field-style button) and its
100
- * hover step. They default to `canvas` / `panel-raised`, but name the role so
101
- * a consumer's own panel control can wear it without borrowing the work
102
- * surface's token or a fixed hover the surface-hover rule rightly rejects.
100
+ * hover step, mixed from the panel they sit in. Named so a consumer's own panel
101
+ * control can wear them without borrowing the work surface's token or a fixed
102
+ * hover the surface-hover rule rightly rejects.
103
103
  * - `hairline` / `hairline-strong` are that structural edge. In this register
104
104
  * hairlines carry structure INSTEAD of tonal steps — a deliberate reversal of
105
105
  * the page register's "prefer a tonal step or space over a divider line".
@@ -1,9 +1,9 @@
1
- import { Kbd, CommandMenuPanel, ToggleGroup, Toggle, BarChart, DateTile, Accordion, AccordionItem, AccordionTrigger, AccordionPanel, RadialChart, Sparkline, Slider, Stepper, StepperItem, SegmentedBar, Checkbox, PieChart, AreaChart, OtpField } from '../chunk-FBSL2LZQ.js';
2
- import { Card, CardHeader, CardTitle, CardDescription, CardAction, Button, CardList, Entity, OverflowMenu, MenuItem, MenuSeparator, Code, CardContent, Message, Bubble, CardFooter, DestructiveActionModal, Badge, AvatarGroup, Avatar, Textarea, CodeBlock, Input, Table, TableHeader, TableRow, TableHead, TableBody, TableCell, StatusDot, Switch, Form, Field, Select, SelectTrigger, SelectContent, SelectItem, Separator, Label, Attachment } from '../chunk-CCUDFLU3.js';
1
+ import { Kbd, CommandMenuPanel, ToggleGroup, Toggle, BarChart, DateTile, Accordion, AccordionItem, AccordionTrigger, AccordionPanel, RadialChart, Sparkline, Slider, Stepper, StepperItem, SegmentedBar, Checkbox, PieChart, AreaChart, OtpField } from '../chunk-GU3T7SBA.js';
2
+ import { Card, CardHeader, CardTitle, CardDescription, CardAction, Button, CardList, Entity, OverflowMenu, MenuItem, MenuSeparator, Code, CardContent, Message, Bubble, CardFooter, DestructiveActionModal, Badge, AvatarGroup, Avatar, Textarea, CodeBlock, Input, Table, TableHeader, TableRow, TableHead, TableBody, TableCell, StatusDot, Switch, Form, Field, Select, SelectTrigger, SelectContent, SelectItem, Separator, Label, Attachment } from '../chunk-RCLKXF2N.js';
3
3
  import { Region, Stack, Row, Grid } from '../chunk-3IWZYBGH.js';
4
4
  import { cn } from '../chunk-SRQWQUTW.js';
5
- import { Icon } from '../chunk-NGBDP3BM.js';
6
- import '../chunk-TH563WLW.js';
5
+ import { Icon } from '../chunk-QJ6FWTUG.js';
6
+ import '../chunk-XAZGMY73.js';
7
7
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
8
  import { useId, useState } from 'react';
9
9
 
@@ -2,7 +2,7 @@
2
2
  var atelierTheme = {
3
3
  id: "atelier",
4
4
  label: "Atelier",
5
- deprecated: { replacement: "base" },
5
+ deprecated: { replacement: "sable" },
6
6
  modes: ["light", "dark"],
7
7
  defaultMode: "light",
8
8
  fonts: {
@@ -1,19 +1,19 @@
1
1
  import { prismTheme } from './chunk-7MG4SHYX.js';
2
2
  import { verdantTheme } from './chunk-KO3NAEBF.js';
3
3
  import { emberTheme } from './chunk-L4IEIQQA.js';
4
- import { baseTheme } from './chunk-EP55KSYA.js';
4
+ import { sableTheme } from './chunk-EHH4MHO6.js';
5
5
  import { poppyTheme } from './chunk-ADLLDWLW.js';
6
6
  import { meridianTheme } from './chunk-K7RUHUGC.js';
7
- import { atelierTheme } from './chunk-YXBOYWBN.js';
7
+ import { atelierTheme } from './chunk-C4Z3CF54.js';
8
8
  import { createContext, useMemo, useState, useEffect, useCallback, useContext } from 'react';
9
9
  import { jsx } from 'react/jsx-runtime';
10
10
 
11
11
  // src/themes/registry.ts
12
12
  var SABLE_THEME_ATTRIBUTE = "data-sable-theme";
13
- var DEFAULT_THEME_ID = baseTheme.id;
13
+ var DEFAULT_THEME_ID = sableTheme.id;
14
14
  var defaultThemeId = DEFAULT_THEME_ID;
15
15
  var themeManifests = [
16
- baseTheme,
16
+ sableTheme,
17
17
  meridianTheme,
18
18
  prismTheme,
19
19
  verdantTheme,
@@ -1,7 +1,7 @@
1
- // src/themes/base/index.ts
2
- var baseTheme = {
3
- id: "base",
4
- label: "Base",
1
+ // src/themes/sable/index.ts
2
+ var sableTheme = {
3
+ id: "sable",
4
+ label: "Sable",
5
5
  modes: ["dark", "light"],
6
6
  defaultMode: "dark",
7
7
  fonts: {
@@ -9,6 +9,6 @@ var baseTheme = {
9
9
  mono: '"DM Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Courier New", monospace'
10
10
  }
11
11
  };
12
- var base_default = baseTheme;
12
+ var sable_default = sableTheme;
13
13
 
14
- export { baseTheme, base_default };
14
+ export { sableTheme, sable_default };
@@ -1,8 +1,8 @@
1
- import { FIELD_SHELL, FIELD_INVALID, useMotion, useTypeTier, useScrollActivity, setRef, useAnchoredDensity, Dialog, DialogContent, DialogTitle, DialogDescription, useIsomorphicLayoutEffect } from './chunk-CCUDFLU3.js';
1
+ import { FIELD_SHELL, FIELD_INVALID, useMotion, useTypeTier, useScrollActivity, setRef, useEdgeFade, useAnchoredDensity, Dialog, DialogContent, DialogTitle, DialogDescription, useIsomorphicLayoutEffect } from './chunk-RCLKXF2N.js';
2
2
  import { cn } from './chunk-SRQWQUTW.js';
3
- import { Icon } from './chunk-NGBDP3BM.js';
3
+ import { Icon } from './chunk-QJ6FWTUG.js';
4
4
  import * as OtpParts from '@base-ui/react/otp-field';
5
- import { createContext, Fragment, useRef, useState, useId, useEffect, Children, isValidElement, useContext, useCallback, useLayoutEffect, useSyncExternalStore, useMemo } from 'react';
5
+ import { createContext, Fragment, useRef, useState, useId, useEffect, Children, isValidElement, useContext, useCallback, useSyncExternalStore, useMemo } from 'react';
6
6
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
7
7
  import { Checkbox as Checkbox$1 } from '@base-ui/react/checkbox';
8
8
  import { cva } from 'class-variance-authority';
@@ -2652,8 +2652,9 @@ function Sparkline({ data, label, variant = "line", tone = "primary", animate =
2652
2652
  const [ref, inView] = useArrival();
2653
2653
  const shown = !animate || !enabled || inView;
2654
2654
  const color = toneColor(tone);
2655
- const lo = Math.min(0, ...data);
2656
- const hi = Math.max(...data, lo + 1);
2655
+ const min = data.length ? Math.min(...data) : 0;
2656
+ const max = data.length ? Math.max(...data) : 1;
2657
+ const [lo, hi] = variant === "bar" ? [Math.min(0, min), Math.max(max, Math.min(0, min) + 1)] : min === max ? [min - 1, max + 1] : [min, max];
2657
2658
  const y = (v) => H - 2 - (v - lo) / (hi - lo) * (H - 4);
2658
2659
  const nf = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });
2659
2660
  const summary = data.length ? `${label}: from ${nf.format(data[0])} to ${nf.format(data[data.length - 1])}, low ${nf.format(Math.min(...data))}, high ${nf.format(Math.max(...data))}` : label;
@@ -3775,7 +3776,7 @@ function SliderThumb({ className, children, ...props }) {
3775
3776
  {
3776
3777
  "aria-hidden": true,
3777
3778
  className: cn(
3778
- "pointer-events-none absolute inset-0 rounded-full border-2 border-primary bg-background shadow-sm",
3779
+ "pointer-events-none absolute inset-0 rounded-full border-2 border-primary bg-background shadow-(--knob-shadow)",
3779
3780
  // The only thing that resizes. Scale (not width/height) so the box
3780
3781
  // metrics — and the row height around them — never move.
3781
3782
  "scale-75 motion-interactive ease-spring",
@@ -4225,35 +4226,15 @@ function Scroller({
4225
4226
  },
4226
4227
  [onKeyDown, orientation]
4227
4228
  );
4228
- const [showStart, setShowStart] = useState(false);
4229
- const [showEnd, setShowEnd] = useState(false);
4230
- const sync = useCallback(
4231
- (el) => {
4232
- if (orientation === "horizontal") {
4233
- const { scrollLeft, scrollWidth, clientWidth } = el;
4234
- setShowStart(scrollLeft > 0);
4235
- setShowEnd(scrollLeft < scrollWidth - clientWidth - 1);
4236
- } else {
4237
- const { scrollTop, scrollHeight, clientHeight } = el;
4238
- setShowStart(scrollTop > 0);
4239
- setShowEnd(scrollTop < scrollHeight - clientHeight - 1);
4240
- }
4241
- },
4242
- [orientation]
4243
- );
4244
- useLayoutEffect(() => {
4245
- if (regionRef.current) sync(regionRef.current);
4246
- }, [sync]);
4229
+ const { sync: syncFade, style: fadeStyle } = useEdgeFade(regionRef, orientation);
4247
4230
  const handleScroll = useCallback(
4248
4231
  (event) => {
4249
- sync(event.currentTarget);
4232
+ syncFade();
4250
4233
  onScroll?.(event);
4251
4234
  },
4252
- [sync, onScroll]
4235
+ [syncFade, onScroll]
4253
4236
  );
4254
4237
  const isVertical = orientation === "vertical";
4255
- const fade = "calc(var(--spacing) * 7)";
4256
- const mask = showStart || showEnd ? `linear-gradient(to ${isVertical ? "bottom" : "right"}, ${showStart ? "transparent" : "#000"}, #000 ${showStart ? fade : "0"}, #000 ${showEnd ? `calc(100% - ${fade})` : "100%"}, ${showEnd ? "transparent" : "#000"})` : void 0;
4257
4238
  return (
4258
4239
  // The chrome box. Carries the consumer's border/background/radius and the
4259
4240
  // sizing, and is NOT masked — which is the entire point of the split. A
@@ -4269,7 +4250,7 @@ function Scroller({
4269
4250
  role: "group",
4270
4251
  "aria-label": label,
4271
4252
  "data-orientation": orientation,
4272
- style: mask ? { maskImage: mask, WebkitMaskImage: mask } : void 0,
4253
+ style: fadeStyle,
4273
4254
  className: cn(
4274
4255
  // Fill the chrome box without a percentage height. `min-h-0` /
4275
4256
  // `min-w-0` because a flex item's default `min-size: auto` refuses to
@@ -1,4 +1,4 @@
1
- import { reiconIconSet } from './chunk-TH563WLW.js';
1
+ import { reiconIconSet } from './chunk-XAZGMY73.js';
2
2
  import { createContext, useMemo, useContext } from 'react';
3
3
  import { jsx, Fragment } from 'react/jsx-runtime';
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { cn } from './chunk-SRQWQUTW.js';
2
- import { Icon } from './chunk-NGBDP3BM.js';
2
+ import { Icon } from './chunk-QJ6FWTUG.js';
3
3
  import { createContext, useMemo, useState, useRef, useLayoutEffect, useEffect, useSyncExternalStore, useCallback, useContext, isValidElement, Children, useId, cloneElement } from 'react';
4
4
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
5
5
  import { cva } from 'class-variance-authority';
@@ -837,8 +837,11 @@ function CardHeader({
837
837
  // one it collapses to a single stacked column (same as before).
838
838
  // Zeroes its own bottom padding so it owns the CardHeader/CardContent
839
839
  // seam — CardContent always has full padding on its own (see below),
840
- // so a standalone CardContent never loses its top padding.
841
- "grid auto-rows-min grid-cols-1 items-start gap-1.5 p-[var(--card-p,--spacing(6))] pb-0 has-data-[slot=card-action]:grid-cols-[1fr_auto]",
840
+ // so a standalone CardContent never loses its top padding. As the
841
+ // card's last child (a stat card: label, number, sparkline) nothing
842
+ // follows to pad the bottom, so it keeps its own; zeroed, the number
843
+ // sat on the card's bottom edge.
844
+ "grid auto-rows-min grid-cols-1 items-start gap-1.5 p-[var(--card-p,--spacing(6))] pb-0 last:pb-[var(--card-p,--spacing(6))] has-data-[slot=card-action]:grid-cols-[1fr_auto]",
842
845
  className
843
846
  ),
844
847
  ...props
@@ -2888,7 +2891,7 @@ function Switch({ className, ...props }) {
2888
2891
  className
2889
2892
  ),
2890
2893
  ...props,
2891
- children: /* @__PURE__ */ jsx(Switch$1.Thumb, { className: "block h-5 w-5 rounded-full bg-(--switch-thumb) shadow-sm motion-interactive ease-spring data-[checked]:translate-x-4 rtl:data-[checked]:-translate-x-4 data-[checked]:bg-primary-foreground" })
2894
+ children: /* @__PURE__ */ jsx(Switch$1.Thumb, { className: "block h-5 w-5 rounded-full bg-(--switch-thumb) shadow-(--knob-shadow) motion-interactive ease-spring data-[checked]:translate-x-4 rtl:data-[checked]:-translate-x-4 data-[checked]:bg-primary-foreground" })
2892
2895
  }
2893
2896
  );
2894
2897
  }
@@ -4819,6 +4822,29 @@ function useCopy(text, { timeout = 1500 } = {}) {
4819
4822
  }, [text, timeout]);
4820
4823
  return { copy, copied };
4821
4824
  }
4825
+ function useEdgeFade(ref, orientation) {
4826
+ const [showStart, setShowStart] = useState(false);
4827
+ const [showEnd, setShowEnd] = useState(false);
4828
+ const sync = useCallback(() => {
4829
+ const el = ref.current;
4830
+ if (!el) return;
4831
+ if (orientation === "horizontal") {
4832
+ const { scrollLeft, scrollWidth, clientWidth } = el;
4833
+ setShowStart(scrollLeft > 0);
4834
+ setShowEnd(scrollLeft < scrollWidth - clientWidth - 1);
4835
+ } else {
4836
+ const { scrollTop, scrollHeight, clientHeight } = el;
4837
+ setShowStart(scrollTop > 0);
4838
+ setShowEnd(scrollTop < scrollHeight - clientHeight - 1);
4839
+ }
4840
+ }, [ref, orientation]);
4841
+ useIsomorphicLayoutEffect(sync);
4842
+ useResizeObserver(sync, [ref]);
4843
+ const fade = "calc(var(--spacing) * 7)";
4844
+ const direction = orientation === "vertical" ? "bottom" : "right";
4845
+ const mask = showStart || showEnd ? `linear-gradient(to ${direction}, ${showStart ? "transparent" : "#000"}, #000 ${showStart ? fade : "0"}, #000 ${showEnd ? `calc(100% - ${fade})` : "100%"}, ${showEnd ? "transparent" : "#000"})` : void 0;
4846
+ return { sync, style: mask ? { maskImage: mask, WebkitMaskImage: mask } : void 0 };
4847
+ }
4822
4848
  var CodeHighlightContext = createContext(null);
4823
4849
  function CodeHighlightProvider({ highlight, children }) {
4824
4850
  return /* @__PURE__ */ jsx(CodeHighlightContext.Provider, { value: highlight, children });
@@ -4836,6 +4862,8 @@ function CodeBlock({
4836
4862
  const highlighter = highlight === false ? null : highlight ?? contextHighlight;
4837
4863
  const hasHeader = Boolean(filename);
4838
4864
  const { copy, copied } = useCopy(code);
4865
+ const preRef = useRef(null);
4866
+ const fade = useEdgeFade(preRef, "horizontal");
4839
4867
  if (prompt != null) {
4840
4868
  return /* @__PURE__ */ jsxs(
4841
4869
  "div",
@@ -4855,11 +4883,13 @@ function CodeBlock({
4855
4883
  }
4856
4884
  );
4857
4885
  }
4886
+ const copyColumn = !hasHeader && showCopy;
4858
4887
  return /* @__PURE__ */ jsxs(
4859
4888
  "div",
4860
4889
  {
4861
4890
  className: cn(
4862
- "relative overflow-hidden rounded-md border border-border bg-muted/40",
4891
+ "overflow-hidden rounded-md border border-border bg-muted/40",
4892
+ copyColumn && "flex items-start",
4863
4893
  className
4864
4894
  ),
4865
4895
  ...props,
@@ -4867,17 +4897,21 @@ function CodeBlock({
4867
4897
  hasHeader ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2", children: [
4868
4898
  /* @__PURE__ */ jsx("span", { className: "text-mono-caption text-muted-foreground", children: filename }),
4869
4899
  showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { name: copied ? "check" : "copy" }) }) : null
4870
- ] }) : showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "absolute end-2 top-2 z-1 text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { name: copied ? "check" : "copy" }) }) : null,
4900
+ ] }) : null,
4871
4901
  /* @__PURE__ */ jsx(
4872
4902
  "pre",
4873
4903
  {
4904
+ ref: preRef,
4905
+ onScroll: fade.sync,
4906
+ style: fade.style,
4874
4907
  className: cn(
4875
4908
  "overflow-x-auto overscroll-x-contain p-4 text-mono-md text-foreground scrollbar-quiet",
4876
- !hasHeader && showCopy ? "pe-12" : ""
4909
+ copyColumn && "min-w-0 flex-1 pe-2"
4877
4910
  ),
4878
4911
  children: /* @__PURE__ */ jsx("code", { children: highlighter ? highlighter(code) : code })
4879
4912
  }
4880
- )
4913
+ ),
4914
+ copyColumn ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "m-2 ms-0 shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { name: copied ? "check" : "copy" }) }) : null
4881
4915
  ]
4882
4916
  }
4883
4917
  );
@@ -6804,4 +6838,4 @@ var attachmentMeta = {
6804
6838
  ]
6805
6839
  };
6806
6840
 
6807
- export { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogTitle, AlertDialogTrigger, Attachment, Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage, AvatarRoot, BADGE_APPEARANCES, BADGE_TONES, Badge, Bubble, BubbleReaction, BubbleReactions, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Code, CodeBlock, CodeHighlightProvider, Density, DensityProvider, DestructiveActionModal, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Entity, FIELD_INVALID, FIELD_SHELL, FIELD_SHELL_XS, Field, Form, HighlightGlide, Input, Label, MENU_ITEM_DESTRUCTIVE, MOBILE_FIELD_TIER, Menu, MenuCheckboxItem, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuPortal, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSubmenu, MenuSubmenuTrigger, MenuTrigger, Message, MotionProvider, OverflowMenu, Pill, Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue, Select, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectPortal, SelectSeparator, SelectTrigger, SelectValue, Separator, SimpleTooltip, Spinner, StatusDot, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Textarea, Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger, anchoredListScroll, anchoredScroll, attachmentMeta, avatarGroupVariants, avatarMeta, avatarVariants, badgeMeta, badgeVariants, bubbleMeta, buttonMeta, buttonVariants, cardMeta, codeBlockMeta, codeMeta, destructiveActionModalMeta, dialogMeta, entityMeta, formMeta, inputMeta, labelMeta, menuMeta, mergeRefs, messageMeta, overflowMenuMeta, pillVariants, progressMeta, progressTrackVariants, selectMeta, separatorMeta, setRef, spinnerMeta, statusDotMeta, statusDotVariants, switchMeta, tableMeta, textareaMeta, tooltipMeta, trimLabel, useAnchoredDensity, useCopy, useDensity, useIsomorphicLayoutEffect, useMotion, useReducedMotion, useResizeObserver, useScrollActivity, useTypeTier, withPortalThemeScope };
6841
+ export { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogTitle, AlertDialogTrigger, Attachment, Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage, AvatarRoot, BADGE_APPEARANCES, BADGE_TONES, Badge, Bubble, BubbleReaction, BubbleReactions, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Code, CodeBlock, CodeHighlightProvider, Density, DensityProvider, DestructiveActionModal, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Entity, FIELD_INVALID, FIELD_SHELL, FIELD_SHELL_XS, Field, Form, HighlightGlide, Input, Label, MENU_ITEM_DESTRUCTIVE, MOBILE_FIELD_TIER, Menu, MenuCheckboxItem, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuPortal, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSubmenu, MenuSubmenuTrigger, MenuTrigger, Message, MotionProvider, OverflowMenu, Pill, Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue, Select, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectPortal, SelectSeparator, SelectTrigger, SelectValue, Separator, SimpleTooltip, Spinner, StatusDot, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Textarea, Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger, anchoredListScroll, anchoredScroll, attachmentMeta, avatarGroupVariants, avatarMeta, avatarVariants, badgeMeta, badgeVariants, bubbleMeta, buttonMeta, buttonVariants, cardMeta, codeBlockMeta, codeMeta, destructiveActionModalMeta, dialogMeta, entityMeta, formMeta, inputMeta, labelMeta, menuMeta, mergeRefs, messageMeta, overflowMenuMeta, pillVariants, progressMeta, progressTrackVariants, selectMeta, separatorMeta, setRef, spinnerMeta, statusDotMeta, statusDotVariants, switchMeta, tableMeta, textareaMeta, tooltipMeta, trimLabel, useAnchoredDensity, useCopy, useDensity, useEdgeFade, useIsomorphicLayoutEffect, useMotion, useReducedMotion, useResizeObserver, useScrollActivity, useTypeTier, withPortalThemeScope };