@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.
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +87 -0
- package/CONTRIBUTING.md +2 -2
- package/DESIGN.md +7 -7
- package/README.md +2 -2
- package/bin/sable-lint.mjs +0 -0
- package/bin/sable.mjs +4 -3
- package/dist/{_contract-BlKMR4eX.d.ts → _contract-Cn3lUpih.d.ts} +7 -7
- package/dist/blocks/index.js +4 -4
- package/dist/{chunk-YXBOYWBN.js → chunk-C4Z3CF54.js} +1 -1
- package/dist/{chunk-MXJX3NZI.js → chunk-EFLC4NA3.js} +4 -4
- package/dist/{chunk-EP55KSYA.js → chunk-EHH4MHO6.js} +6 -6
- package/dist/{chunk-FBSL2LZQ.js → chunk-GU3T7SBA.js} +11 -30
- package/dist/{chunk-NGBDP3BM.js → chunk-QJ6FWTUG.js} +1 -1
- package/dist/{chunk-CCUDFLU3.js → chunk-RCLKXF2N.js} +43 -9
- package/dist/{chunk-TH563WLW.js → chunk-XAZGMY73.js} +10 -3
- package/dist/{chunk-D2ETQBHV.js → chunk-ZBZMKOO7.js} +4 -4
- package/dist/icon/index.js +2 -2
- package/dist/icon-sets/brands.d.ts +0 -27
- package/dist/icon-sets/brands.js +7 -1
- package/dist/icon-sets/reicon.js +1 -1
- package/dist/index.d.ts +44 -19
- package/dist/index.js +104 -27
- package/dist/patterns/index.js +4 -4
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +1 -1
- package/dist/styles.css +19 -11
- package/dist/{theme-provider-CuYYxF0o.d.ts → theme-provider-CGeiusgu.d.ts} +3 -3
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/atelier/index.js +1 -1
- package/dist/themes/ember/ember.css +8 -5
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +3 -3
- package/dist/themes/index.js +3 -3
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +1 -1
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/sable/index.d.ts +9 -0
- package/dist/themes/sable/index.js +1 -0
- package/dist/themes/{base/base.css → sable/sable.css} +6 -6
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes.css +3 -3
- package/llms/components/tabs.md +1 -1
- package/llms/components/theme-selector.md +1 -1
- package/llms/components/toast.md +12 -1
- package/llms/reference/token-intent.md +1 -1
- package/llms/tasks/building-a-page.md +1 -1
- package/llms/tasks/creating-a-preset.md +1 -1
- package/llms/tasks/designing-a-surface.md +1 -1
- package/llms-full.txt +19 -8
- package/llms.txt +2 -2
- package/package.json +5 -5
- package/registry.json +43 -14
- package/scripts/design-presets.mjs +1 -1
- package/scripts/grade.mjs +18 -5
- package/scripts/lint-rules.mjs +20 -1
- package/dist/themes/base/index.d.ts +0 -9
- 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 **
|
|
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 **
|
|
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
|
|
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/
|
|
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.
|
|
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 "
|
|
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.**
|
|
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
|
|
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 (
|
|
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
|
-
**
|
|
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/
|
|
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
|
|
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: **
|
|
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
|
|
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
|
package/bin/sable-lint.mjs
CHANGED
|
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
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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".
|
package/dist/blocks/index.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
6
|
-
import '../chunk-
|
|
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
|
|
|
@@ -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 {
|
|
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-
|
|
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 =
|
|
13
|
+
var DEFAULT_THEME_ID = sableTheme.id;
|
|
14
14
|
var defaultThemeId = DEFAULT_THEME_ID;
|
|
15
15
|
var themeManifests = [
|
|
16
|
-
|
|
16
|
+
sableTheme,
|
|
17
17
|
meridianTheme,
|
|
18
18
|
prismTheme,
|
|
19
19
|
verdantTheme,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/themes/
|
|
2
|
-
var
|
|
3
|
-
id: "
|
|
4
|
-
label: "
|
|
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
|
|
12
|
+
var sable_default = sableTheme;
|
|
13
13
|
|
|
14
|
-
export {
|
|
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-
|
|
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-
|
|
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,
|
|
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
|
|
2656
|
-
const
|
|
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-
|
|
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
|
|
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
|
-
|
|
4232
|
+
syncFade();
|
|
4250
4233
|
onScroll?.(event);
|
|
4251
4234
|
},
|
|
4252
|
-
[
|
|
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:
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import { cn } from './chunk-SRQWQUTW.js';
|
|
2
|
-
import { Icon } from './chunk-
|
|
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
|
-
|
|
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-
|
|
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
|
-
"
|
|
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
|
-
] }) :
|
|
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
|
-
|
|
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 };
|