@sableui/react 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/blocks/approval-block.js +30 -48
- package/dist/blocks/approval-block.meta.js +7 -8
- package/dist/blocks/index.d.ts +1 -1
- package/dist/components/section-rail/index.js +7 -0
- package/dist/components/section-rail/section-rail.js +153 -0
- package/dist/components/section-rail/section-rail.meta.js +189 -0
- package/dist/file-hashes.json +10 -6
- package/dist/index.d.ts +97 -3
- package/dist/index.js +4 -1
- package/dist/lib/taxonomy.js +1 -0
- package/dist/lib/use-active-section.js +59 -0
- package/llms/blocks/approval-block.md +40 -55
- package/llms/components/section-rail.md +101 -0
- package/llms-full.txt +145 -56
- package/llms.txt +4 -3
- package/package.json +1 -1
- package/registry.json +214 -11
- package/targets/ios.json +9 -0
package/llms-full.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sable (@sableui/react)
|
|
2
2
|
|
|
3
|
-
> An owned, themeable React component system. Default theme: **Sable** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter.
|
|
3
|
+
> An owned, themeable React component system. Default theme: **Sable** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter. 122 components, 13 layout primitives, and a **patterns** tier of canonical page shells. The full, per-component contract is the generated `registry.json`.
|
|
4
4
|
|
|
5
5
|
## Start here — brief, composition, evidence
|
|
6
6
|
|
|
@@ -7093,49 +7093,46 @@ export function ApiKeysBlock({ className, framed = true, onCreate, onKeyAction }
|
|
|
7093
7093
|
|
|
7094
7094
|
# ApprovalBlock
|
|
7095
7095
|
|
|
7096
|
-
An agent asking permission for one tool call: the question, what
|
|
7096
|
+
An agent asking permission for one tool call: the question, what is at stake, the call itself as a ToolCall with its exact input, and Deny / Always allow / Allow once.
|
|
7097
7097
|
|
|
7098
|
-
**Intent:** Reach for this in a chat or agent run when a tool call waits on the person — sending, paying, deleting, publishing.
|
|
7098
|
+
**Intent:** Reach for this in a chat or agent run when a tool call waits on the person — running a command, sending, paying, deleting, publishing. It wraps the call's own ToolCall, which shows status="awaiting" until the person answers and then "complete" or "denied". A destructive action outside an agent is DestructiveActionModal.
|
|
7099
7099
|
|
|
7100
7100
|
**Block:** AI & agents · card · designed for 18rem–28rem wide (fills its container up to the max)
|
|
7101
7101
|
|
|
7102
7102
|
## Notes
|
|
7103
7103
|
|
|
7104
|
-
- The title is the question in plain words ("
|
|
7105
|
-
- The
|
|
7106
|
-
-
|
|
7107
|
-
- After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer.
|
|
7104
|
+
- The title is the question in plain words ("Run this command?"); the description says what is at stake. That sentence is what the person is deciding on.
|
|
7105
|
+
- The call is shown as the agent made it — tool name and raw input, open on arrival — because the exact arguments are what a careful person checks. Do not paraphrase them into a form.
|
|
7106
|
+
- Three answers, as agent products settle on: Deny on the start edge, then Always allow and Allow once, the primary on the end. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.
|
|
7107
|
+
- After a decision the buttons give way to one InlineStatus line saying what happened, and the ToolCall shows the outcome, so the thread keeps a record of the answer.
|
|
7108
7108
|
|
|
7109
7109
|
## Accessibility
|
|
7110
7110
|
|
|
7111
7111
|
- Focus moves to the outcome line when the buttons leave, instead of dropping to the page; the line is a role=status region, so the decision is announced.
|
|
7112
|
-
- The
|
|
7113
|
-
- The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name.
|
|
7112
|
+
- The ToolCall announces its own status change, and its input is a labelled code view.
|
|
7114
7113
|
|
|
7115
7114
|
## Source
|
|
7116
7115
|
|
|
7117
7116
|
```tsx
|
|
7118
|
-
import { useState, useEffect, useRef
|
|
7119
|
-
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,
|
|
7117
|
+
import { useState, useEffect, useRef } from "react"
|
|
7118
|
+
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, InlineStatus, ToolCall } from "@sableui/react"
|
|
7120
7119
|
|
|
7121
7120
|
const request = {
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
allowed: "Allowed. Sending the email.",
|
|
7131
|
-
denied: "Denied. The agent won't send it.",
|
|
7121
|
+
title: "Run this command?",
|
|
7122
|
+
description: "The agent wants to migrate the production database in acme/api. It changes live data.",
|
|
7123
|
+
tool: "run_command",
|
|
7124
|
+
action: "Run a command",
|
|
7125
|
+
// A command reads as the command; a structured call would pass its arguments object.
|
|
7126
|
+
input: "pnpm db:migrate --env production",
|
|
7127
|
+
allowed: "Allowed. Running the migration.",
|
|
7128
|
+
denied: "Denied. The agent won't run it.",
|
|
7132
7129
|
}
|
|
7133
7130
|
|
|
7134
7131
|
export interface ApprovalBlockProps {
|
|
7135
7132
|
className?: string
|
|
7136
7133
|
/** false drops the card's surface and border — for a chat column that already frames it. */
|
|
7137
7134
|
framed?: boolean
|
|
7138
|
-
/** The person allowed the call. `remember` is
|
|
7135
|
+
/** The person allowed the call. `remember` is true for "Always allow". */
|
|
7139
7136
|
onApprove?: (options: { remember: boolean }) => void
|
|
7140
7137
|
/** The person declined the call. */
|
|
7141
7138
|
onDeny?: () => void
|
|
@@ -7143,8 +7140,6 @@ export interface ApprovalBlockProps {
|
|
|
7143
7140
|
|
|
7144
7141
|
export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: ApprovalBlockProps) {
|
|
7145
7142
|
const [decision, setDecision] = useState<"allowed" | "denied" | null>(null)
|
|
7146
|
-
const [remember, setRemember] = useState(false)
|
|
7147
|
-
const id = useId()
|
|
7148
7143
|
const outcome = useRef<HTMLDivElement>(null)
|
|
7149
7144
|
// The buttons leave with the decision, so focus moves to the line that
|
|
7150
7145
|
// replaces them instead of falling back to the top of the page.
|
|
@@ -7152,37 +7147,30 @@ export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: A
|
|
|
7152
7147
|
if (decision) outcome.current?.focus()
|
|
7153
7148
|
}, [decision])
|
|
7154
7149
|
|
|
7150
|
+
const approve = (remember: boolean) => {
|
|
7151
|
+
setDecision("allowed")
|
|
7152
|
+
onApprove?.({ remember })
|
|
7153
|
+
}
|
|
7154
|
+
|
|
7155
7155
|
return (
|
|
7156
7156
|
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-md", className)}>
|
|
7157
7157
|
<CardHeader>
|
|
7158
7158
|
<CardTitle>{request.title}</CardTitle>
|
|
7159
7159
|
<CardDescription>{request.description}</CardDescription>
|
|
7160
7160
|
</CardHeader>
|
|
7161
|
-
<CardContent
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
<Code>{request.tool}</Code>
|
|
7173
|
-
</dd>
|
|
7174
|
-
</div>
|
|
7175
|
-
</dl>
|
|
7176
|
-
{decision ? null : (
|
|
7177
|
-
<div className="flex items-center gap-2">
|
|
7178
|
-
<Checkbox id={`${id}-remember`} checked={remember} onCheckedChange={(v: boolean) => setRemember(v)} />
|
|
7179
|
-
<Label htmlFor={`${id}-remember`} className="text-body-sm">
|
|
7180
|
-
Allow <Code>{request.tool}</Code> for the rest of this chat
|
|
7181
|
-
</Label>
|
|
7182
|
-
</div>
|
|
7183
|
-
)}
|
|
7161
|
+
<CardContent>
|
|
7162
|
+
{/* The call itself, exactly as the agent made it. It answers with the
|
|
7163
|
+
person: held, then done or denied. */}
|
|
7164
|
+
<ToolCall
|
|
7165
|
+
name={request.tool}
|
|
7166
|
+
title={request.action}
|
|
7167
|
+
status={decision === "allowed" ? "complete" : decision === "denied" ? "denied" : "awaiting"}
|
|
7168
|
+
statusLabel={decision === "allowed" ? "Allowed" : undefined}
|
|
7169
|
+
input={request.input}
|
|
7170
|
+
defaultOpen
|
|
7171
|
+
/>
|
|
7184
7172
|
</CardContent>
|
|
7185
|
-
<CardFooter actions
|
|
7173
|
+
<CardFooter actions>
|
|
7186
7174
|
{decision ? (
|
|
7187
7175
|
<InlineStatus
|
|
7188
7176
|
ref={outcome}
|
|
@@ -7196,7 +7184,8 @@ export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: A
|
|
|
7196
7184
|
) : (
|
|
7197
7185
|
<>
|
|
7198
7186
|
<Button
|
|
7199
|
-
variant="
|
|
7187
|
+
variant="ghost"
|
|
7188
|
+
className="me-auto"
|
|
7200
7189
|
onClick={() => {
|
|
7201
7190
|
setDecision("denied")
|
|
7202
7191
|
onDeny?.()
|
|
@@ -7204,14 +7193,10 @@ export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: A
|
|
|
7204
7193
|
>
|
|
7205
7194
|
Deny
|
|
7206
7195
|
</Button>
|
|
7207
|
-
<Button
|
|
7208
|
-
|
|
7209
|
-
setDecision("allowed")
|
|
7210
|
-
onApprove?.({ remember })
|
|
7211
|
-
}}
|
|
7212
|
-
>
|
|
7213
|
-
Allow
|
|
7196
|
+
<Button variant="outline" onClick={() => approve(true)}>
|
|
7197
|
+
Always allow
|
|
7214
7198
|
</Button>
|
|
7199
|
+
<Button onClick={() => approve(false)}>Allow once</Button>
|
|
7215
7200
|
</>
|
|
7216
7201
|
)}
|
|
7217
7202
|
</CardFooter>
|
|
@@ -20701,6 +20686,110 @@ import { SectionHeader } from "@sableui/react"
|
|
|
20701
20686
|
|
|
20702
20687
|
---
|
|
20703
20688
|
|
|
20689
|
+
# SectionRail
|
|
20690
|
+
|
|
20691
|
+
**Status:** stable
|
|
20692
|
+
|
|
20693
|
+
A slim vertical rail of dots, one per section of a long page, that shows where the reader is and jumps to any section. The current section's bar glides as the page scrolls; a shared tooltip names each dot.
|
|
20694
|
+
|
|
20695
|
+
**Keywords:** section rail, scroll spy, scrollspy, dot navigation, page navigation, table of contents, on this page, chapter nav, scroll progress, case study nav
|
|
20696
|
+
|
|
20697
|
+
## Usage
|
|
20698
|
+
|
|
20699
|
+
Use SectionRail beside a long, single-column read — a case study, an essay, a long landing page — so the reader can see which section they are in and jump to another without leaving the text. It follows the scroll by itself (useActiveSection); pass value when the parent already knows the current section.
|
|
20700
|
+
|
|
20701
|
+
### Best practices
|
|
20702
|
+
|
|
20703
|
+
- Do: Give every section an id that matches its entry, and keep the list to the page's top-level sections — about ten at most, so the rail stays shorter than the viewport.
|
|
20704
|
+
- Do: Place it with layout classes — usually `fixed top-1/2 left-6 z-(--z-nav) -translate-y-1/2` — and hide it below the width where it would overlap the text (`max-lg:hidden`). Offer the same links another way on narrow screens, such as a Collapsible list at the top of the page.
|
|
20705
|
+
- Do: Pass `root` when the content scrolls inside an element (a Scroller, an inset AppShell main) rather than the window, and give sections `scroll-mt-*` when a sticky header would cover their tops after a jump.
|
|
20706
|
+
- Do: Set `side="right"` when the rail sits on the right edge, so labels open toward the content.
|
|
20707
|
+
- Don't: Use it for app navigation between pages; that is SideNav or Sidebar. A SectionRail only moves within one page. → use **SideNav**, **Sidebar**
|
|
20708
|
+
- Don't: Use it to show the steps of a task or a flow; that is Stepper, which shows progress through steps rather than position in a read. → use **Stepper**
|
|
20709
|
+
- Don't: Use it for a handful of switchable views in one place; that is Tabs. → use **Tabs**
|
|
20710
|
+
|
|
20711
|
+
### Anatomy
|
|
20712
|
+
|
|
20713
|
+
| Part | Required | Description |
|
|
20714
|
+
| --- | --- | --- |
|
|
20715
|
+
| Rail | yes | The `<nav>` landmark: a rounded muted track holding one link per section. |
|
|
20716
|
+
| Dot | yes | A link to `#id`, drawn as a small dot that grows under the pointer or focus. |
|
|
20717
|
+
| Marker | yes | One primary bar that sits on the current section's dot and glides to the next. |
|
|
20718
|
+
| Label | yes | A shared tooltip that names the hovered or focused dot and travels between dots. |
|
|
20719
|
+
|
|
20720
|
+
## Props
|
|
20721
|
+
|
|
20722
|
+
| Name | Type | Default | Description |
|
|
20723
|
+
| --- | --- | --- | --- |
|
|
20724
|
+
| sections | readonly { id: string; label: string }[] | — | The sections, in document order. `id` is the section element's id (the link goes to `#id`); `label` is the tooltip and the link's accessible name. |
|
|
20725
|
+
| value | string | — | The current section's id, when the parent decides it. Leave it unset and the rail follows the scroll itself. |
|
|
20726
|
+
| onValueChange | (id: string) => void | — | Called with the new id when the reader scrolls into a section or picks one. |
|
|
20727
|
+
| offset | number | 0.35 | Where the reading line sits, as a fraction of the viewport from its top. A section becomes current once its top crosses it. |
|
|
20728
|
+
| root | RefObject<HTMLElement \| null> | — | The element that scrolls, when it is not the window. |
|
|
20729
|
+
| side | "left" \| "right" | "left" | Which edge of the page the rail sits on. The labels open on the other side, toward the content. |
|
|
20730
|
+
| motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the marker's glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. |
|
|
20731
|
+
| aria-label | string | "Sections" | The nav landmark's name. Name it for the page ("Case study sections") when it has other navs. |
|
|
20732
|
+
|
|
20733
|
+
## Tokens used
|
|
20734
|
+
|
|
20735
|
+
`muted`, `muted-foreground`, `foreground`, `primary`, `ring`, `popover`, `popover-foreground`, `border-floating`
|
|
20736
|
+
|
|
20737
|
+
## Motion
|
|
20738
|
+
|
|
20739
|
+
- **arrival** · Root · _no motion_ — Page chrome arrives with the page; the marker lands on the current section with no transition, because a bar sliding in on load narrates a move nobody made.
|
|
20740
|
+
- **response** · Dot · `motion-interactive` — The dot darkens and grows under the pointer or focus, and the label travels between dots as one tooltip (TooltipGroup) rather than closing and reopening at each.
|
|
20741
|
+
- **transition** · Marker · `motion-glide` — One bar travels to the new section as the page scrolls, so the change reads as movement down the page. A picked section holds the bar until the reader scrolls by hand, so it goes straight there instead of stepping through every section in between, and stays on the pick even when the page ends before that section can reach the reading line. Expressive: `subtle` moves it at once.
|
|
20742
|
+
- **activity** · Root · _no motion_ — Navigation chrome has no working state.
|
|
20743
|
+
|
|
20744
|
+
## Accessibility
|
|
20745
|
+
|
|
20746
|
+
- Renders a <nav> landmark (default name "Sections") of real links to `#id`, so it works with no script, middle-click and copy-link.
|
|
20747
|
+
- Each link is named by its section's label; the current one carries aria-current="location".
|
|
20748
|
+
- Activating a link from the keyboard moves focus into the section, as following an anchor does, so the next Tab continues from there.
|
|
20749
|
+
- The tooltip opens on focus as well as hover; the dots are 44×32 targets.
|
|
20750
|
+
- Under reduced motion the jump is instant and the marker moves without travel.
|
|
20751
|
+
|
|
20752
|
+
## Related
|
|
20753
|
+
|
|
20754
|
+
- Components: SideNav, Tooltip, Stepper, Tabs, GlidingHighlight
|
|
20755
|
+
|
|
20756
|
+
## Examples
|
|
20757
|
+
|
|
20758
|
+
### Fixed beside a long read
|
|
20759
|
+
|
|
20760
|
+
```tsx
|
|
20761
|
+
import { SectionRail } from "@sableui/react"
|
|
20762
|
+
|
|
20763
|
+
<SectionRail
|
|
20764
|
+
aria-label="Case study sections"
|
|
20765
|
+
sections={[
|
|
20766
|
+
{ id: "overview", label: "Overview" },
|
|
20767
|
+
{ id: "problem", label: "The problem" },
|
|
20768
|
+
{ id: "process", label: "Process" },
|
|
20769
|
+
{ id: "outcome", label: "Outcome" },
|
|
20770
|
+
]}
|
|
20771
|
+
className="fixed top-1/2 left-6 z-(--z-nav) -translate-y-1/2 max-lg:hidden"
|
|
20772
|
+
/>
|
|
20773
|
+
```
|
|
20774
|
+
|
|
20775
|
+
### On the right edge
|
|
20776
|
+
|
|
20777
|
+
```tsx
|
|
20778
|
+
import { SectionRail } from "@sableui/react"
|
|
20779
|
+
|
|
20780
|
+
<SectionRail
|
|
20781
|
+
side="right"
|
|
20782
|
+
sections={[
|
|
20783
|
+
{ id: "intro", label: "Introduction" },
|
|
20784
|
+
{ id: "usage", label: "Usage" },
|
|
20785
|
+
{ id: "api", label: "API" },
|
|
20786
|
+
]}
|
|
20787
|
+
className="fixed top-1/2 right-6 z-(--z-nav) -translate-y-1/2 max-lg:hidden"
|
|
20788
|
+
/>
|
|
20789
|
+
```
|
|
20790
|
+
|
|
20791
|
+
---
|
|
20792
|
+
|
|
20704
20793
|
# SegmentedBar
|
|
20705
20794
|
|
|
20706
20795
|
**Status:** stable
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sable (@sableui/react)
|
|
2
2
|
|
|
3
|
-
> An owned, themeable React component system. Default theme: **Sable** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter.
|
|
3
|
+
> An owned, themeable React component system. Default theme: **Sable** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter. 122 components, 13 layout primitives, and a **patterns** tier of canonical page shells. The full, per-component contract is the generated `registry.json`.
|
|
4
4
|
|
|
5
5
|
## Start here — brief, composition, evidence
|
|
6
6
|
|
|
@@ -291,7 +291,7 @@ import { Button, Card, IconProvider, MotionProvider, SableThemeProvider, useSabl
|
|
|
291
291
|
### Blocks (42) — section-sized compositions (cards, forms, lists), copy as starting points (registry.json → blocks; importable from `@sableui/react/blocks`; `sable block <Name>`)
|
|
292
292
|
|
|
293
293
|
- **ApiKeysBlock** (Settings & team) — An API keys card: each key's name with its masked hint in Code, when it was created and last used, and an OverflowMenu to rename, roll or revoke. → [`llms/blocks/api-keys-block.md`](./llms/blocks/api-keys-block.md)
|
|
294
|
-
- **ApprovalBlock** (AI & agents) — An agent asking permission for one tool call: the question, what
|
|
294
|
+
- **ApprovalBlock** (AI & agents) — An agent asking permission for one tool call: the question, what is at stake, the call itself as a ToolCall with its exact input, and Deny / Always allow / Allow once. → [`llms/blocks/approval-block.md`](./llms/blocks/approval-block.md)
|
|
295
295
|
- **ChatThreadBlock** (Social & messaging) — A short message thread on a card: Message rows with incoming muted Bubbles on the start side and your own primary Bubbles on the end. → [`llms/blocks/chat-thread-block.md`](./llms/blocks/chat-thread-block.md)
|
|
296
296
|
- **CommandPaletteBlock** (Navigation) — An inline command palette: CommandMenuPanel with a filter field and grouped actions, each with an icon and its keyboard shortcut. → [`llms/blocks/command-palette-block.md`](./llms/blocks/command-palette-block.md)
|
|
297
297
|
- **CtaBandBlock** (Marketing) — A closing call to action: a centred h2 and one-line lede on a card, with a primary and an outline action. → [`llms/blocks/cta-band-block.md`](./llms/blocks/cta-band-block.md)
|
|
@@ -373,7 +373,7 @@ import { Button, Card, IconProvider, MotionProvider, SableThemeProvider, useSabl
|
|
|
373
373
|
- **ToastProvider** (part of Toast) — Wraps Base UI Toast.Provider and renders the themed Viewport + toast list.
|
|
374
374
|
- **TooltipProvider** (part of Tooltip) — Wraps an app or group of Tooltips to share open/close delays.
|
|
375
375
|
|
|
376
|
-
### Components (
|
|
376
|
+
### Components (122) — full props/subcomponents/variants/guidance/a11y/examples in registry.json → components
|
|
377
377
|
|
|
378
378
|
- **Accordion** — Stacked, collapsible disclosure sections over Base UI Accordion — roving focus, arrow-key navigation, ARIA region/heading/button wiring, and an animated height transition. *Parts:* AccordionItem, AccordionTrigger, AccordionPanel. → [`llms/components/accordion.md`](./llms/components/accordion.md)
|
|
379
379
|
- **AppHeader** — Mobile top bar (sticky, hidden at lg) with leading / title / actions slots on a 3-column grid that keeps the title optically centered. → [`llms/components/app-header.md`](./llms/components/app-header.md)
|
|
@@ -465,6 +465,7 @@ import { Button, Card, IconProvider, MotionProvider, SableThemeProvider, useSabl
|
|
|
465
465
|
- **ScrubField** — The compact numeric field a property grid is built from: a leading glyph inside the field that doubles as a drag-to-scrub handle, the value, and an optional unit. → [`llms/components/scrub-field.md`](./llms/components/scrub-field.md)
|
|
466
466
|
- **SearchInput** — Styled search field with a leading magnifier and a clearable X. → [`llms/components/search-input.md`](./llms/components/search-input.md)
|
|
467
467
|
- **SectionHeader** — The heading block for one section of a page: a title (h2 by default), an optional line of description, and actions on the title row — a "View all" link or a couple of buttons — that drop beneath the title only when the row runs out of room. → [`llms/components/section-header.md`](./llms/components/section-header.md)
|
|
468
|
+
- **SectionRail** — A slim vertical rail of dots, one per section of a long page, that shows where the reader is and jumps to any section. → [`llms/components/section-rail.md`](./llms/components/section-rail.md)
|
|
468
469
|
- **SegmentedBar** — One bar split into the parts of a whole — revenue by type, storage by kind, results by outcome. → [`llms/components/segmented-bar.md`](./llms/components/segmented-bar.md)
|
|
469
470
|
- **Select** — Listbox over Base UI Select — typeahead, roving focus, collision-aware popup, ARIA option wiring, and form integration. *Parts:* SelectTrigger, SelectContent, SelectItem, SelectGroup, SelectGroupLabel, SelectValue, SelectSeparator. → [`llms/components/select.md`](./llms/components/select.md)
|
|
470
471
|
- **Separator** — A hairline divider. → [`llms/components/separator.md`](./llms/components/separator.md)
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://sable.design/registry.schema.json",
|
|
3
3
|
"name": "@sableui/react",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.0",
|
|
5
5
|
"description": "Themeable React components, semantic tokens, layout patterns, presets, and package-owned agent skills. Default theme: Sable.",
|
|
6
6
|
"engine": {
|
|
7
7
|
"behavior": "@base-ui/react",
|
|
@@ -18765,6 +18765,201 @@
|
|
|
18765
18765
|
}
|
|
18766
18766
|
]
|
|
18767
18767
|
},
|
|
18768
|
+
{
|
|
18769
|
+
"name": "SectionRail",
|
|
18770
|
+
"status": "stable",
|
|
18771
|
+
"group": "Navigation",
|
|
18772
|
+
"register": "page",
|
|
18773
|
+
"path": "components/section-rail",
|
|
18774
|
+
"summary": "A slim vertical rail of dots, one per section of a long page, that shows where the reader is and jumps to any section. The current section's bar glides as the page scrolls; a shared tooltip names each dot.",
|
|
18775
|
+
"keywords": [
|
|
18776
|
+
"section rail",
|
|
18777
|
+
"scroll spy",
|
|
18778
|
+
"scrollspy",
|
|
18779
|
+
"dot navigation",
|
|
18780
|
+
"page navigation",
|
|
18781
|
+
"table of contents",
|
|
18782
|
+
"on this page",
|
|
18783
|
+
"chapter nav",
|
|
18784
|
+
"scroll progress",
|
|
18785
|
+
"case study nav"
|
|
18786
|
+
],
|
|
18787
|
+
"usage": {
|
|
18788
|
+
"description": "Use SectionRail beside a long, single-column read — a case study, an essay, a long landing page — so the reader can see which section they are in and jump to another without leaving the text. It follows the scroll by itself (useActiveSection); pass value when the parent already knows the current section.",
|
|
18789
|
+
"bestPractices": [
|
|
18790
|
+
{
|
|
18791
|
+
"guidance": "do",
|
|
18792
|
+
"description": "Give every section an id that matches its entry, and keep the list to the page's top-level sections — about ten at most, so the rail stays shorter than the viewport."
|
|
18793
|
+
},
|
|
18794
|
+
{
|
|
18795
|
+
"guidance": "do",
|
|
18796
|
+
"description": "Place it with layout classes — usually `fixed top-1/2 left-6 z-(--z-nav) -translate-y-1/2` — and hide it below the width where it would overlap the text (`max-lg:hidden`). Offer the same links another way on narrow screens, such as a Collapsible list at the top of the page."
|
|
18797
|
+
},
|
|
18798
|
+
{
|
|
18799
|
+
"guidance": "do",
|
|
18800
|
+
"description": "Pass `root` when the content scrolls inside an element (a Scroller, an inset AppShell main) rather than the window, and give sections `scroll-mt-*` when a sticky header would cover their tops after a jump."
|
|
18801
|
+
},
|
|
18802
|
+
{
|
|
18803
|
+
"guidance": "do",
|
|
18804
|
+
"description": "Set `side=\"right\"` when the rail sits on the right edge, so labels open toward the content."
|
|
18805
|
+
},
|
|
18806
|
+
{
|
|
18807
|
+
"guidance": "dont",
|
|
18808
|
+
"description": "Use it for app navigation between pages; that is SideNav or Sidebar. A SectionRail only moves within one page.",
|
|
18809
|
+
"instead": [
|
|
18810
|
+
"SideNav",
|
|
18811
|
+
"Sidebar"
|
|
18812
|
+
]
|
|
18813
|
+
},
|
|
18814
|
+
{
|
|
18815
|
+
"guidance": "dont",
|
|
18816
|
+
"description": "Use it to show the steps of a task or a flow; that is Stepper, which shows progress through steps rather than position in a read.",
|
|
18817
|
+
"instead": [
|
|
18818
|
+
"Stepper"
|
|
18819
|
+
]
|
|
18820
|
+
},
|
|
18821
|
+
{
|
|
18822
|
+
"guidance": "dont",
|
|
18823
|
+
"description": "Use it for a handful of switchable views in one place; that is Tabs.",
|
|
18824
|
+
"instead": [
|
|
18825
|
+
"Tabs"
|
|
18826
|
+
]
|
|
18827
|
+
}
|
|
18828
|
+
],
|
|
18829
|
+
"anatomy": [
|
|
18830
|
+
{
|
|
18831
|
+
"name": "Rail",
|
|
18832
|
+
"required": true,
|
|
18833
|
+
"description": "The `<nav>` landmark: a rounded muted track holding one link per section."
|
|
18834
|
+
},
|
|
18835
|
+
{
|
|
18836
|
+
"name": "Dot",
|
|
18837
|
+
"required": true,
|
|
18838
|
+
"description": "A link to `#id`, drawn as a small dot that grows under the pointer or focus."
|
|
18839
|
+
},
|
|
18840
|
+
{
|
|
18841
|
+
"name": "Marker",
|
|
18842
|
+
"required": true,
|
|
18843
|
+
"description": "One primary bar that sits on the current section's dot and glides to the next."
|
|
18844
|
+
},
|
|
18845
|
+
{
|
|
18846
|
+
"name": "Label",
|
|
18847
|
+
"required": true,
|
|
18848
|
+
"description": "A shared tooltip that names the hovered or focused dot and travels between dots."
|
|
18849
|
+
}
|
|
18850
|
+
]
|
|
18851
|
+
},
|
|
18852
|
+
"related": {
|
|
18853
|
+
"components": [
|
|
18854
|
+
"SideNav",
|
|
18855
|
+
"Tooltip",
|
|
18856
|
+
"Stepper",
|
|
18857
|
+
"Tabs",
|
|
18858
|
+
"GlidingHighlight"
|
|
18859
|
+
]
|
|
18860
|
+
},
|
|
18861
|
+
"props": [
|
|
18862
|
+
{
|
|
18863
|
+
"name": "sections",
|
|
18864
|
+
"type": "readonly { id: string; label: string }[]",
|
|
18865
|
+
"description": "The sections, in document order. `id` is the section element's id (the link goes to `#id`); `label` is the tooltip and the link's accessible name."
|
|
18866
|
+
},
|
|
18867
|
+
{
|
|
18868
|
+
"name": "value",
|
|
18869
|
+
"type": "string",
|
|
18870
|
+
"description": "The current section's id, when the parent decides it. Leave it unset and the rail follows the scroll itself."
|
|
18871
|
+
},
|
|
18872
|
+
{
|
|
18873
|
+
"name": "onValueChange",
|
|
18874
|
+
"type": "(id: string) => void",
|
|
18875
|
+
"description": "Called with the new id when the reader scrolls into a section or picks one."
|
|
18876
|
+
},
|
|
18877
|
+
{
|
|
18878
|
+
"name": "offset",
|
|
18879
|
+
"type": "number",
|
|
18880
|
+
"default": "0.35",
|
|
18881
|
+
"description": "Where the reading line sits, as a fraction of the viewport from its top. A section becomes current once its top crosses it."
|
|
18882
|
+
},
|
|
18883
|
+
{
|
|
18884
|
+
"name": "root",
|
|
18885
|
+
"type": "RefObject<HTMLElement | null>",
|
|
18886
|
+
"description": "The element that scrolls, when it is not the window."
|
|
18887
|
+
},
|
|
18888
|
+
{
|
|
18889
|
+
"name": "side",
|
|
18890
|
+
"type": "\"left\" | \"right\"",
|
|
18891
|
+
"default": "\"left\"",
|
|
18892
|
+
"description": "Which edge of the page the rail sits on. The labels open on the other side, toward the content."
|
|
18893
|
+
},
|
|
18894
|
+
{
|
|
18895
|
+
"name": "motion",
|
|
18896
|
+
"type": "\"full\" | \"subtle\" | \"off\"",
|
|
18897
|
+
"default": "\"full\"",
|
|
18898
|
+
"description": "How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the marker's glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting."
|
|
18899
|
+
},
|
|
18900
|
+
{
|
|
18901
|
+
"name": "aria-label",
|
|
18902
|
+
"type": "string",
|
|
18903
|
+
"default": "\"Sections\"",
|
|
18904
|
+
"description": "The nav landmark's name. Name it for the page (\"Case study sections\") when it has other navs."
|
|
18905
|
+
}
|
|
18906
|
+
],
|
|
18907
|
+
"tokensUsed": [
|
|
18908
|
+
"muted",
|
|
18909
|
+
"muted-foreground",
|
|
18910
|
+
"foreground",
|
|
18911
|
+
"primary",
|
|
18912
|
+
"ring",
|
|
18913
|
+
"popover",
|
|
18914
|
+
"popover-foreground",
|
|
18915
|
+
"border-floating"
|
|
18916
|
+
],
|
|
18917
|
+
"motion": [
|
|
18918
|
+
{
|
|
18919
|
+
"slot": "arrival",
|
|
18920
|
+
"recipe": "none",
|
|
18921
|
+
"part": "Root",
|
|
18922
|
+
"why": "Page chrome arrives with the page; the marker lands on the current section with no transition, because a bar sliding in on load narrates a move nobody made."
|
|
18923
|
+
},
|
|
18924
|
+
{
|
|
18925
|
+
"slot": "response",
|
|
18926
|
+
"recipe": "motion-interactive",
|
|
18927
|
+
"part": "Dot",
|
|
18928
|
+
"why": "The dot darkens and grows under the pointer or focus, and the label travels between dots as one tooltip (TooltipGroup) rather than closing and reopening at each."
|
|
18929
|
+
},
|
|
18930
|
+
{
|
|
18931
|
+
"slot": "transition",
|
|
18932
|
+
"recipe": "motion-glide",
|
|
18933
|
+
"part": "Marker",
|
|
18934
|
+
"why": "One bar travels to the new section as the page scrolls, so the change reads as movement down the page. A picked section holds the bar until the reader scrolls by hand, so it goes straight there instead of stepping through every section in between, and stays on the pick even when the page ends before that section can reach the reading line. Expressive: `subtle` moves it at once."
|
|
18935
|
+
},
|
|
18936
|
+
{
|
|
18937
|
+
"slot": "activity",
|
|
18938
|
+
"recipe": "none",
|
|
18939
|
+
"part": "Root",
|
|
18940
|
+
"why": "Navigation chrome has no working state."
|
|
18941
|
+
}
|
|
18942
|
+
],
|
|
18943
|
+
"a11y": [
|
|
18944
|
+
"Renders a <nav> landmark (default name \"Sections\") of real links to `#id`, so it works with no script, middle-click and copy-link.",
|
|
18945
|
+
"Each link is named by its section's label; the current one carries aria-current=\"location\".",
|
|
18946
|
+
"Activating a link from the keyboard moves focus into the section, as following an anchor does, so the next Tab continues from there.",
|
|
18947
|
+
"The tooltip opens on focus as well as hover; the dots are 44×32 targets.",
|
|
18948
|
+
"Under reduced motion the jump is instant and the marker moves without travel."
|
|
18949
|
+
],
|
|
18950
|
+
"examples": [
|
|
18951
|
+
{
|
|
18952
|
+
"label": "Fixed beside a long read",
|
|
18953
|
+
"code": "<SectionRail\n aria-label=\"Case study sections\"\n sections={[\n { id: \"overview\", label: \"Overview\" },\n { id: \"problem\", label: \"The problem\" },\n { id: \"process\", label: \"Process\" },\n { id: \"outcome\", label: \"Outcome\" },\n ]}\n className=\"fixed top-1/2 left-6 z-(--z-nav) -translate-y-1/2 max-lg:hidden\"\n/>",
|
|
18954
|
+
"imports": "import { SectionRail } from \"@sableui/react\""
|
|
18955
|
+
},
|
|
18956
|
+
{
|
|
18957
|
+
"label": "On the right edge",
|
|
18958
|
+
"code": "<SectionRail\n side=\"right\"\n sections={[\n { id: \"intro\", label: \"Introduction\" },\n { id: \"usage\", label: \"Usage\" },\n { id: \"api\", label: \"API\" },\n ]}\n className=\"fixed top-1/2 right-6 z-(--z-nav) -translate-y-1/2 max-lg:hidden\"\n/>",
|
|
18959
|
+
"imports": "import { SectionRail } from \"@sableui/react\""
|
|
18960
|
+
}
|
|
18961
|
+
]
|
|
18962
|
+
},
|
|
18768
18963
|
{
|
|
18769
18964
|
"name": "SegmentedBar",
|
|
18770
18965
|
"status": "stable",
|
|
@@ -24505,8 +24700,8 @@
|
|
|
24505
24700
|
{
|
|
24506
24701
|
"name": "ApprovalBlock",
|
|
24507
24702
|
"path": "blocks/approval-block.tsx",
|
|
24508
|
-
"summary": "An agent asking permission for one tool call: the question, what
|
|
24509
|
-
"intent": "Reach for this in a chat or agent run when a tool call waits on the person — sending, paying, deleting, publishing.
|
|
24703
|
+
"summary": "An agent asking permission for one tool call: the question, what is at stake, the call itself as a ToolCall with its exact input, and Deny / Always allow / Allow once.",
|
|
24704
|
+
"intent": "Reach for this in a chat or agent run when a tool call waits on the person — running a command, sending, paying, deleting, publishing. It wraps the call's own ToolCall, which shows status=\"awaiting\" until the person answers and then \"complete\" or \"denied\". A destructive action outside an agent is DestructiveActionModal.",
|
|
24510
24705
|
"family": "AI & agents",
|
|
24511
24706
|
"kind": "card",
|
|
24512
24707
|
"width": {
|
|
@@ -24514,18 +24709,17 @@
|
|
|
24514
24709
|
"max": "28rem"
|
|
24515
24710
|
},
|
|
24516
24711
|
"notes": [
|
|
24517
|
-
"The title is the question in plain words (\"
|
|
24518
|
-
"The
|
|
24519
|
-
"
|
|
24520
|
-
"After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer."
|
|
24712
|
+
"The title is the question in plain words (\"Run this command?\"); the description says what is at stake. That sentence is what the person is deciding on.",
|
|
24713
|
+
"The call is shown as the agent made it — tool name and raw input, open on arrival — because the exact arguments are what a careful person checks. Do not paraphrase them into a form.",
|
|
24714
|
+
"Three answers, as agent products settle on: Deny on the start edge, then Always allow and Allow once, the primary on the end. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.",
|
|
24715
|
+
"After a decision the buttons give way to one InlineStatus line saying what happened, and the ToolCall shows the outcome, so the thread keeps a record of the answer."
|
|
24521
24716
|
],
|
|
24522
24717
|
"a11y": [
|
|
24523
24718
|
"Focus moves to the outcome line when the buttons leave, instead of dropping to the page; the line is a role=status region, so the decision is announced.",
|
|
24524
|
-
"The
|
|
24525
|
-
"The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name."
|
|
24719
|
+
"The ToolCall announces its own status change, and its input is a labelled code view."
|
|
24526
24720
|
],
|
|
24527
|
-
"imports": "import { useState, useEffect, useRef
|
|
24528
|
-
"code": "const request = {\n\
|
|
24721
|
+
"imports": "import { useState, useEffect, useRef } from \"react\"\nimport { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, InlineStatus, ToolCall } from \"@sableui/react\"",
|
|
24722
|
+
"code": "const request = {\n\ttitle: \"Run this command?\",\n\tdescription: \"The agent wants to migrate the production database in acme/api. It changes live data.\",\n\ttool: \"run_command\",\n\taction: \"Run a command\",\n\t// A command reads as the command; a structured call would pass its arguments object.\n\tinput: \"pnpm db:migrate --env production\",\n\tallowed: \"Allowed. Running the migration.\",\n\tdenied: \"Denied. The agent won't run it.\",\n}\n\nexport interface ApprovalBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a chat column that already frames it. */\n\tframed?: boolean\n\t/** The person allowed the call. `remember` is true for \"Always allow\". */\n\tonApprove?: (options: { remember: boolean }) => void\n\t/** The person declined the call. */\n\tonDeny?: () => void\n}\n\nexport function ApprovalBlock({ className, framed = true, onApprove, onDeny }: ApprovalBlockProps) {\n\tconst [decision, setDecision] = useState<\"allowed\" | \"denied\" | null>(null)\n\tconst outcome = useRef<HTMLDivElement>(null)\n\t// The buttons leave with the decision, so focus moves to the line that\n\t// replaces them instead of falling back to the top of the page.\n\tuseEffect(() => {\n\t\tif (decision) outcome.current?.focus()\n\t}, [decision])\n\n\tconst approve = (remember: boolean) => {\n\t\tsetDecision(\"allowed\")\n\t\tonApprove?.({ remember })\n\t}\n\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-md\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle>{request.title}</CardTitle>\n\t\t\t\t<CardDescription>{request.description}</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t<CardContent>\n\t\t\t\t{/* The call itself, exactly as the agent made it. It answers with the\n\t\t\t\t person: held, then done or denied. */}\n\t\t\t\t<ToolCall\n\t\t\t\t\tname={request.tool}\n\t\t\t\t\ttitle={request.action}\n\t\t\t\t\tstatus={decision === \"allowed\" ? \"complete\" : decision === \"denied\" ? \"denied\" : \"awaiting\"}\n\t\t\t\t\tstatusLabel={decision === \"allowed\" ? \"Allowed\" : undefined}\n\t\t\t\t\tinput={request.input}\n\t\t\t\t\tdefaultOpen\n\t\t\t\t/>\n\t\t\t</CardContent>\n\t\t\t<CardFooter actions>\n\t\t\t\t{decision ? (\n\t\t\t\t\t<InlineStatus\n\t\t\t\t\t\tref={outcome}\n\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\ttone={decision === \"allowed\" ? \"success\" : \"neutral\"}\n\t\t\t\t\t\tclassName=\"me-auto outline-none\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{decision === \"allowed\" ? request.allowed : request.denied}\n\t\t\t\t\t</InlineStatus>\n\t\t\t\t) : (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tclassName=\"me-auto\"\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tsetDecision(\"denied\")\n\t\t\t\t\t\t\t\tonDeny?.()\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tDeny\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button variant=\"outline\" onClick={() => approve(true)}>\n\t\t\t\t\t\t\tAlways allow\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button onClick={() => approve(false)}>Allow once</Button>\n\t\t\t\t\t</>\n\t\t\t\t)}\n\t\t\t</CardFooter>\n\t\t</Card>\n\t)\n}"
|
|
24529
24723
|
},
|
|
24530
24724
|
{
|
|
24531
24725
|
"name": "ChatThreadBlock",
|
|
@@ -29994,6 +30188,15 @@
|
|
|
29994
30188
|
"titleAs",
|
|
29995
30189
|
"titleId"
|
|
29996
30190
|
],
|
|
30191
|
+
"SectionRail": [
|
|
30192
|
+
"motion",
|
|
30193
|
+
"offset",
|
|
30194
|
+
"onValueChange",
|
|
30195
|
+
"root",
|
|
30196
|
+
"sections",
|
|
30197
|
+
"side",
|
|
30198
|
+
"value"
|
|
30199
|
+
],
|
|
29997
30200
|
"SegmentedBar": [
|
|
29998
30201
|
"label",
|
|
29999
30202
|
"locale",
|
package/targets/ios.json
CHANGED
|
@@ -913,6 +913,15 @@
|
|
|
913
913
|
],
|
|
914
914
|
"guidance": "Use the Sable component as it is, in the preset. Adapt layout and touch targets to the phone; do not swap in a native lookalike or restyle it."
|
|
915
915
|
},
|
|
916
|
+
"SectionRail": {
|
|
917
|
+
"source": "SectionRail",
|
|
918
|
+
"strategy": "preserve",
|
|
919
|
+
"importFrom": "@sableui/react",
|
|
920
|
+
"components": [
|
|
921
|
+
"SectionRail"
|
|
922
|
+
],
|
|
923
|
+
"guidance": "Use the Sable component as it is, in the preset. Adapt layout and touch targets to the phone; do not swap in a native lookalike or restyle it."
|
|
924
|
+
},
|
|
916
925
|
"SegmentedBar": {
|
|
917
926
|
"source": "SegmentedBar",
|
|
918
927
|
"strategy": "preserve",
|