@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/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. 121 components, 13 layout primitives, and a **patterns** tier of canonical page shells. The full, per-component contract is the generated `registry.json`.
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 will happen and what can't be undone, the call's details, an "allow for the rest of this chat" Checkbox, and Deny / Allow.
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. The call itself, before and after the decision, is a ToolCall with status="awaiting", then "complete" or "denied". A destructive action outside an agent is DestructiveActionModal.
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 ("Send the invoice to Ada?"), not the tool's name; the tool's name is one row of the details, for the person who wants to know exactly what runs.
7105
- - The description says what can't be undone. That sentence is what the person is deciding on.
7106
- - Deny comes first and Allow is the primary on the end, the same order as every Sable confirmation. 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, 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 details are a dl, so each value is read with its label.
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, useId } from "react"
7119
- import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, cn, Code, InlineStatus, Label } from "@sableui/react"
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
- tool: "send_email",
7123
- title: "Send the invoice to Ada?",
7124
- description: "The agent wants to email invoice INV-1042. An email can't be unsent.",
7125
- details: [
7126
- { label: "To", value: "ada@example.com" },
7127
- { label: "Subject", value: "INV-1042 for October" },
7128
- { label: "Attached", value: "INV-1042.pdf" },
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 the "for the rest of this chat" choice. */
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 className="flex flex-col gap-4">
7162
- <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm">
7163
- {request.details.map((d) => (
7164
- <div key={d.label} className="contents">
7165
- <dt className="text-muted-foreground">{d.label}</dt>
7166
- <dd className="truncate text-foreground">{d.value}</dd>
7167
- </div>
7168
- ))}
7169
- <div className="contents">
7170
- <dt className="text-muted-foreground">Tool</dt>
7171
- <dd className="min-w-0">
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 className="justify-end">
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="outline"
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
- onClick={() => {
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. 121 components, 13 layout primitives, and a **patterns** tier of canonical page shells. The full, per-component contract is the generated `registry.json`.
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 will happen and what can't be undone, the call's details, an "allow for the rest of this chat" Checkbox, and Deny / Allow. → [`llms/blocks/approval-block.md`](./llms/blocks/approval-block.md)
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 (121) — full props/subcomponents/variants/guidance/a11y/examples in registry.json → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sableui/react",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Sable: themeable React components, layout patterns, and tools for coding agents.",
5
5
  "type": "module",
6
6
  "sideEffects": [
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.16.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 will happen and what can't be undone, the call's details, an \"allow for the rest of this chat\" Checkbox, and Deny / Allow.",
24509
- "intent": "Reach for this in a chat or agent run when a tool call waits on the person — sending, paying, deleting, publishing. The call itself, before and after the decision, is a ToolCall with status=\"awaiting\", then \"complete\" or \"denied\". A destructive action outside an agent is DestructiveActionModal.",
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 (\"Send the invoice to Ada?\"), not the tool's name; the tool's name is one row of the details, for the person who wants to know exactly what runs.",
24518
- "The description says what can't be undone. That sentence is what the person is deciding on.",
24519
- "Deny comes first and Allow is the primary on the end, the same order as every Sable confirmation. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.",
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 details are a dl, so each value is read with its label.",
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, useId } from \"react\"\nimport { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, cn, Code, InlineStatus, Label } from \"@sableui/react\"",
24528
- "code": "const request = {\n\ttool: \"send_email\",\n\ttitle: \"Send the invoice to Ada?\",\n\tdescription: \"The agent wants to email invoice INV-1042. An email can't be unsent.\",\n\tdetails: [\n\t\t{ label: \"To\", value: \"ada@example.com\" },\n\t\t{ label: \"Subject\", value: \"INV-1042 for October\" },\n\t\t{ label: \"Attached\", value: \"INV-1042.pdf\" },\n\t],\n\tallowed: \"Allowed. Sending the email.\",\n\tdenied: \"Denied. The agent won't send 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 the \"for the rest of this chat\" choice. */\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 [remember, setRemember] = useState(false)\n\tconst id = useId()\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\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 className=\"flex flex-col gap-4\">\n\t\t\t\t<dl className=\"grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm\">\n\t\t\t\t\t{request.details.map((d) => (\n\t\t\t\t\t\t<div key={d.label} className=\"contents\">\n\t\t\t\t\t\t\t<dt className=\"text-muted-foreground\">{d.label}</dt>\n\t\t\t\t\t\t\t<dd className=\"truncate text-foreground\">{d.value}</dd>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t))}\n\t\t\t\t\t<div className=\"contents\">\n\t\t\t\t\t\t<dt className=\"text-muted-foreground\">Tool</dt>\n\t\t\t\t\t\t<dd className=\"min-w-0\">\n\t\t\t\t\t\t\t<Code>{request.tool}</Code>\n\t\t\t\t\t\t</dd>\n\t\t\t\t\t</div>\n\t\t\t\t</dl>\n\t\t\t\t{decision ? null : (\n\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t<Checkbox id={`${id}-remember`} checked={remember} onCheckedChange={(v: boolean) => setRemember(v)} />\n\t\t\t\t\t\t<Label htmlFor={`${id}-remember`} className=\"text-body-sm\">\n\t\t\t\t\t\t\tAllow <Code>{request.tool}</Code> for the rest of this chat\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</CardContent>\n\t\t\t<CardFooter actions className=\"justify-end\">\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=\"outline\"\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\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tsetDecision(\"allowed\")\n\t\t\t\t\t\t\t\tonApprove?.({ remember })\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tAllow\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</>\n\t\t\t\t)}\n\t\t\t</CardFooter>\n\t\t</Card>\n\t)\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",