@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 CHANGED
@@ -1,5 +1,31 @@
1
1
  # What's new
2
2
 
3
+ ## 0.17.0
4
+
5
+ Knowing where you are: a rail that follows a long read, and an approval
6
+ that shows an agent's call the way agent products do.
7
+
8
+ - **New: `SectionRail`.** A slim vertical rail of dots beside a long read —
9
+ a case study, an essay, a long landing page — one per section. A bar marks
10
+ the section being read and glides to the next as the page scrolls; hovering
11
+ or focusing a dot names it in one tooltip that travels between dots
12
+ (`TooltipGroup`). Picking a dot jumps there with the page's own
13
+ `scroll-behavior` (instant under reduced motion), and the bar goes straight
14
+ to the pick rather than stepping through every section in between. Pass
15
+ `root` when the content scrolls inside an element: only that element moves,
16
+ never the page around it. Links are real `#id` anchors, so it works without
17
+ script. Promoted from carldecaire.com's case-study rail.
18
+ - **New: `useActiveSection(ids, { offset, root })`.** The scroll spy behind it,
19
+ exported for a contents list you draw yourself. The current section is the
20
+ last whose top has crossed the reading line; at the end of the scroll the
21
+ last one in view wins, so a short closing section is still reachable.
22
+ - **`ApprovalBlock` asks the way agent products do.** It wraps the call's own
23
+ `ToolCall` — tool name and exact input, open on arrival — that turns to
24
+ Allowed or Denied once answered, and offers Deny / Always allow / Allow once.
25
+ The details table and the "for the rest of this chat" checkbox are gone;
26
+ `onApprove({ remember })` is unchanged, with `remember` now true for Always
27
+ allow. The example is an agent asking to run a command.
28
+
3
29
  ## 0.16.0
4
30
 
5
31
  Five pieces of the calm, product-chrome look that AI and B2B tools share:
@@ -1,59 +1,48 @@
1
1
  // src/blocks/approval-block.tsx
2
- import { useEffect, useId, useRef, useState } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
3
  import { Button } from "../components/button/index.js";
4
4
  import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../components/card/index.js";
5
- import { Checkbox } from "../components/checkbox/index.js";
6
- import { Code } from "../components/code/index.js";
7
5
  import { InlineStatus } from "../components/inline-status/index.js";
8
- import { Label } from "../components/label/index.js";
6
+ import { ToolCall } from "../components/tool-call/index.js";
9
7
  import { cn } from "../lib/cn.js";
10
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
9
  var request = {
12
- tool: "send_email",
13
- title: "Send the invoice to Ada?",
14
- description: "The agent wants to email invoice INV-1042. An email can't be unsent.",
15
- details: [
16
- { label: "To", value: "ada@example.com" },
17
- { label: "Subject", value: "INV-1042 for October" },
18
- { label: "Attached", value: "INV-1042.pdf" }
19
- ],
20
- allowed: "Allowed. Sending the email.",
21
- denied: "Denied. The agent won't send it."
10
+ title: "Run this command?",
11
+ description: "The agent wants to migrate the production database in acme/api. It changes live data.",
12
+ tool: "run_command",
13
+ action: "Run a command",
14
+ // A command reads as the command; a structured call would pass its arguments object.
15
+ input: "pnpm db:migrate --env production",
16
+ allowed: "Allowed. Running the migration.",
17
+ denied: "Denied. The agent won't run it."
22
18
  };
23
19
  function ApprovalBlock({ className, framed = true, onApprove, onDeny }) {
24
20
  const [decision, setDecision] = useState(null);
25
- const [remember, setRemember] = useState(false);
26
- const id = useId();
27
21
  const outcome = useRef(null);
28
22
  useEffect(() => {
29
23
  if (decision) outcome.current?.focus();
30
24
  }, [decision]);
25
+ const approve = (remember) => {
26
+ setDecision("allowed");
27
+ onApprove?.({ remember });
28
+ };
31
29
  return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-md", className), children: [
32
30
  /* @__PURE__ */ jsxs(CardHeader, { children: [
33
31
  /* @__PURE__ */ jsx(CardTitle, { children: request.title }),
34
32
  /* @__PURE__ */ jsx(CardDescription, { children: request.description })
35
33
  ] }),
36
- /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-4", children: [
37
- /* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm", children: [
38
- request.details.map((d) => /* @__PURE__ */ jsxs("div", { className: "contents", children: [
39
- /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: d.label }),
40
- /* @__PURE__ */ jsx("dd", { className: "truncate text-foreground", children: d.value })
41
- ] }, d.label)),
42
- /* @__PURE__ */ jsxs("div", { className: "contents", children: [
43
- /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Tool" }),
44
- /* @__PURE__ */ jsx("dd", { className: "min-w-0", children: /* @__PURE__ */ jsx(Code, { children: request.tool }) })
45
- ] })
46
- ] }),
47
- decision ? null : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
48
- /* @__PURE__ */ jsx(Checkbox, { id: `${id}-remember`, checked: remember, onCheckedChange: (v) => setRemember(v) }),
49
- /* @__PURE__ */ jsxs(Label, { htmlFor: `${id}-remember`, className: "text-body-sm", children: [
50
- "Allow ",
51
- /* @__PURE__ */ jsx(Code, { children: request.tool }),
52
- " for the rest of this chat"
53
- ] })
54
- ] })
55
- ] }),
56
- /* @__PURE__ */ jsx(CardFooter, { actions: true, className: "justify-end", children: decision ? /* @__PURE__ */ jsx(
34
+ /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx(
35
+ ToolCall,
36
+ {
37
+ name: request.tool,
38
+ title: request.action,
39
+ status: decision === "allowed" ? "complete" : decision === "denied" ? "denied" : "awaiting",
40
+ statusLabel: decision === "allowed" ? "Allowed" : void 0,
41
+ input: request.input,
42
+ defaultOpen: true
43
+ }
44
+ ) }),
45
+ /* @__PURE__ */ jsx(CardFooter, { actions: true, children: decision ? /* @__PURE__ */ jsx(
57
46
  InlineStatus,
58
47
  {
59
48
  ref: outcome,
@@ -67,7 +56,8 @@ function ApprovalBlock({ className, framed = true, onApprove, onDeny }) {
67
56
  /* @__PURE__ */ jsx(
68
57
  Button,
69
58
  {
70
- variant: "outline",
59
+ variant: "ghost",
60
+ className: "me-auto",
71
61
  onClick: () => {
72
62
  setDecision("denied");
73
63
  onDeny?.();
@@ -75,16 +65,8 @@ function ApprovalBlock({ className, framed = true, onApprove, onDeny }) {
75
65
  children: "Deny"
76
66
  }
77
67
  ),
78
- /* @__PURE__ */ jsx(
79
- Button,
80
- {
81
- onClick: () => {
82
- setDecision("allowed");
83
- onApprove?.({ remember });
84
- },
85
- children: "Allow"
86
- }
87
- )
68
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => approve(true), children: "Always allow" }),
69
+ /* @__PURE__ */ jsx(Button, { onClick: () => approve(false), children: "Allow once" })
88
70
  ] }) })
89
71
  ] });
90
72
  }
@@ -4,18 +4,17 @@ var approvalBlockMeta = {
4
4
  family: "AI & agents",
5
5
  kind: "card",
6
6
  width: { min: "18rem", max: "28rem" },
7
- 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.`,
8
- intent: 'Reach for this in a chat or agent run when a tool call waits on the person \u2014 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.',
7
+ 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.",
8
+ intent: `Reach for this in a chat or agent run when a tool call waits on the person \u2014 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.`,
9
9
  notes: [
10
- `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.`,
11
- "The description says what can't be undone. That sentence is what the person is deciding on.",
12
- "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.",
13
- "After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer."
10
+ '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.',
11
+ "The call is shown as the agent made it \u2014 tool name and raw input, open on arrival \u2014 because the exact arguments are what a careful person checks. Do not paraphrase them into a form.",
12
+ "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.",
13
+ "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."
14
14
  ],
15
15
  a11y: [
16
16
  "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.",
17
- "The details are a dl, so each value is read with its label.",
18
- "The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name."
17
+ "The ToolCall announces its own status change, and its input is a labelled code view."
19
18
  ]
20
19
  };
21
20
  export {
@@ -17,7 +17,7 @@ interface ApprovalBlockProps {
17
17
  className?: string;
18
18
  /** false drops the card's surface and border — for a chat column that already frames it. */
19
19
  framed?: boolean;
20
- /** The person allowed the call. `remember` is the "for the rest of this chat" choice. */
20
+ /** The person allowed the call. `remember` is true for "Always allow". */
21
21
  onApprove?: (options: {
22
22
  remember: boolean;
23
23
  }) => void;
@@ -0,0 +1,7 @@
1
+ // src/components/section-rail/index.ts
2
+ import { SectionRail } from "./section-rail.js";
3
+ import { sectionRailMeta } from "./section-rail.meta.js";
4
+ export {
5
+ SectionRail,
6
+ sectionRailMeta
7
+ };
@@ -0,0 +1,153 @@
1
+ "use client";
2
+
3
+ // src/components/section-rail/section-rail.tsx
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useRef,
8
+ useState
9
+ } from "react";
10
+ import { cn } from "../../lib/cn.js";
11
+ import { HighlightGlide } from "../../lib/highlight-glide.js";
12
+ import { setRef } from "../../lib/refs.js";
13
+ import { useActiveSection } from "../../lib/use-active-section.js";
14
+ import { useMotionLevel } from "../../motion/MotionProvider.js";
15
+ import { SimpleTooltip, TooltipGroup } from "../tooltip/index.js";
16
+ import { jsx, jsxs } from "react/jsx-runtime";
17
+ var SCROLL_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "]);
18
+ var GLIDE_HANDOFF = "[&[data-glide]_[data-active]>[data-section-rail-mark]]:opacity-0";
19
+ var GLIDE_MARK = "flex items-center justify-center motion-glide after:h-5 after:w-1 after:rounded-full after:bg-primary";
20
+ function SectionRail({
21
+ sections,
22
+ value,
23
+ onValueChange,
24
+ offset,
25
+ root,
26
+ side = "left",
27
+ motion,
28
+ className,
29
+ "aria-label": ariaLabel = "Sections",
30
+ ref,
31
+ ...props
32
+ }) {
33
+ const rail = useRef(null);
34
+ const setRail = useCallback(
35
+ (node) => {
36
+ rail.current = node;
37
+ setRef(ref, node);
38
+ },
39
+ [ref]
40
+ );
41
+ const { enabled, attribute } = useMotionLevel(motion);
42
+ const spied = useActiveSection(
43
+ sections.map((section) => section.id),
44
+ { offset, root }
45
+ );
46
+ const [picked, setPicked] = useState(null);
47
+ const internal = picked ?? spied;
48
+ const active = value ?? internal;
49
+ const report = useRef(onValueChange);
50
+ report.current = onValueChange;
51
+ const reported = useRef(internal);
52
+ useEffect(() => {
53
+ if (internal === null || internal === reported.current) return;
54
+ reported.current = internal;
55
+ report.current?.(internal);
56
+ }, [internal]);
57
+ useEffect(() => {
58
+ if (picked === null) return;
59
+ const release = () => setPicked(null);
60
+ const releaseOnKey = (event) => {
61
+ if (SCROLL_KEYS.has(event.key)) release();
62
+ };
63
+ const listeners = new AbortController();
64
+ const options = { passive: true, signal: listeners.signal };
65
+ window.addEventListener("wheel", release, options);
66
+ window.addEventListener("touchstart", release, options);
67
+ window.addEventListener("keydown", releaseOnKey, options);
68
+ window.addEventListener(
69
+ "pointerdown",
70
+ (event) => {
71
+ if (!(event.target instanceof Node && rail.current?.contains(event.target))) release();
72
+ },
73
+ options
74
+ );
75
+ return () => listeners.abort();
76
+ }, [picked]);
77
+ const jump = (event, id) => {
78
+ if (event.defaultPrevented || event.button !== 0) return;
79
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
80
+ const section = document.getElementById(id);
81
+ if (!section) return;
82
+ event.preventDefault();
83
+ setPicked(id);
84
+ const behavior = enabled ? "auto" : "instant";
85
+ const scroller = root?.current;
86
+ if (scroller) {
87
+ const margin = Number.parseFloat(getComputedStyle(section).scrollMarginTop) || 0;
88
+ const top = section.getBoundingClientRect().top - scroller.getBoundingClientRect().top - scroller.clientTop;
89
+ scroller.scrollTo({ top: scroller.scrollTop + top - margin, behavior });
90
+ } else {
91
+ section.scrollIntoView({ behavior, block: "start" });
92
+ window.history.replaceState(window.history.state, "", `#${id}`);
93
+ }
94
+ if (event.detail === 0) {
95
+ if (!section.hasAttribute("tabindex")) {
96
+ section.setAttribute("tabindex", "-1");
97
+ section.addEventListener("blur", () => section.removeAttribute("tabindex"), { once: true });
98
+ }
99
+ section.focus({ preventScroll: true });
100
+ }
101
+ };
102
+ return /* @__PURE__ */ jsxs(
103
+ "nav",
104
+ {
105
+ ref: setRail,
106
+ "aria-label": ariaLabel,
107
+ "data-motion": attribute,
108
+ className: cn(
109
+ "relative flex w-fit flex-col rounded-full bg-muted py-2",
110
+ GLIDE_HANDOFF,
111
+ className
112
+ ),
113
+ ...props,
114
+ children: [
115
+ /* @__PURE__ */ jsx(HighlightGlide, { follow: "data-active", timing: "selection", className: GLIDE_MARK }),
116
+ /* @__PURE__ */ jsx(TooltipGroup, { motion, children: sections.map((section) => {
117
+ const current = section.id === active;
118
+ return /* @__PURE__ */ jsx(
119
+ SimpleTooltip,
120
+ {
121
+ content: section.label,
122
+ side: side === "left" ? "right" : "left",
123
+ children: /* @__PURE__ */ jsx(
124
+ "a",
125
+ {
126
+ href: `#${section.id}`,
127
+ "aria-label": section.label,
128
+ "aria-current": current ? "location" : void 0,
129
+ "data-active": current ? "" : void 0,
130
+ onClick: (event) => jump(event, section.id),
131
+ className: "group/rail-link relative flex h-(--control-sm) w-11 items-center justify-center rounded-full text-muted-foreground outline-none motion-interactive hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring",
132
+ children: /* @__PURE__ */ jsx(
133
+ "span",
134
+ {
135
+ "aria-hidden": true,
136
+ "data-section-rail-mark": "",
137
+ className: "size-1 rounded-full bg-current motion-interactive group-hover/rail-link:scale-150 group-focus-visible/rail-link:scale-150 group-data-[active]/rail-link:h-5 group-data-[active]/rail-link:bg-primary"
138
+ }
139
+ )
140
+ }
141
+ )
142
+ },
143
+ section.id
144
+ );
145
+ }) })
146
+ ]
147
+ }
148
+ );
149
+ }
150
+ SectionRail.displayName = "SectionRail";
151
+ export {
152
+ SectionRail
153
+ };
@@ -0,0 +1,189 @@
1
+ // src/components/section-rail/section-rail.meta.ts
2
+ var sectionRailMeta = {
3
+ name: "SectionRail",
4
+ 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.",
5
+ keywords: [
6
+ "section rail",
7
+ "scroll spy",
8
+ "scrollspy",
9
+ "dot navigation",
10
+ "page navigation",
11
+ "table of contents",
12
+ "on this page",
13
+ "chapter nav",
14
+ "scroll progress",
15
+ "case study nav"
16
+ ],
17
+ usage: {
18
+ description: "Use SectionRail beside a long, single-column read \u2014 a case study, an essay, a long landing page \u2014 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.",
19
+ bestPractices: [
20
+ {
21
+ guidance: "do",
22
+ description: "Give every section an id that matches its entry, and keep the list to the page's top-level sections \u2014 about ten at most, so the rail stays shorter than the viewport."
23
+ },
24
+ {
25
+ guidance: "do",
26
+ description: "Place it with layout classes \u2014 usually `fixed top-1/2 left-6 z-(--z-nav) -translate-y-1/2` \u2014 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."
27
+ },
28
+ {
29
+ guidance: "do",
30
+ 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."
31
+ },
32
+ {
33
+ guidance: "do",
34
+ description: 'Set `side="right"` when the rail sits on the right edge, so labels open toward the content.'
35
+ },
36
+ {
37
+ guidance: "dont",
38
+ description: "Use it for app navigation between pages; that is SideNav or Sidebar. A SectionRail only moves within one page.",
39
+ instead: ["SideNav", "Sidebar"]
40
+ },
41
+ {
42
+ guidance: "dont",
43
+ 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.",
44
+ instead: ["Stepper"]
45
+ },
46
+ {
47
+ guidance: "dont",
48
+ description: "Use it for a handful of switchable views in one place; that is Tabs.",
49
+ instead: ["Tabs"]
50
+ }
51
+ ],
52
+ anatomy: [
53
+ {
54
+ name: "Rail",
55
+ required: true,
56
+ description: "The `<nav>` landmark: a rounded muted track holding one link per section."
57
+ },
58
+ {
59
+ name: "Dot",
60
+ required: true,
61
+ description: "A link to `#id`, drawn as a small dot that grows under the pointer or focus."
62
+ },
63
+ {
64
+ name: "Marker",
65
+ required: true,
66
+ description: "One primary bar that sits on the current section's dot and glides to the next."
67
+ },
68
+ {
69
+ name: "Label",
70
+ required: true,
71
+ description: "A shared tooltip that names the hovered or focused dot and travels between dots."
72
+ }
73
+ ]
74
+ },
75
+ related: {
76
+ components: ["SideNav", "Tooltip", "Stepper", "Tabs", "GlidingHighlight"]
77
+ },
78
+ status: "stable",
79
+ props: [
80
+ {
81
+ name: "sections",
82
+ type: "readonly { id: string; label: string }[]",
83
+ 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."
84
+ },
85
+ {
86
+ name: "value",
87
+ type: "string",
88
+ description: "The current section's id, when the parent decides it. Leave it unset and the rail follows the scroll itself."
89
+ },
90
+ {
91
+ name: "onValueChange",
92
+ type: "(id: string) => void",
93
+ description: "Called with the new id when the reader scrolls into a section or picks one."
94
+ },
95
+ {
96
+ name: "offset",
97
+ type: "number",
98
+ default: "0.35",
99
+ description: "Where the reading line sits, as a fraction of the viewport from its top. A section becomes current once its top crosses it."
100
+ },
101
+ {
102
+ name: "root",
103
+ type: "RefObject<HTMLElement | null>",
104
+ description: "The element that scrolls, when it is not the window."
105
+ },
106
+ {
107
+ name: "side",
108
+ type: '"left" | "right"',
109
+ default: '"left"',
110
+ description: "Which edge of the page the rail sits on. The labels open on the other side, toward the content."
111
+ },
112
+ {
113
+ name: "motion",
114
+ type: '"full" | "subtle" | "off"',
115
+ default: '"full"',
116
+ 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 \u2014 it never overrides a reader's reduce-motion setting."
117
+ },
118
+ {
119
+ name: "aria-label",
120
+ type: "string",
121
+ default: '"Sections"',
122
+ description: `The nav landmark's name. Name it for the page ("Case study sections") when it has other navs.`
123
+ }
124
+ ],
125
+ motion: [
126
+ {
127
+ slot: "arrival",
128
+ recipe: "none",
129
+ part: "Root",
130
+ 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."
131
+ },
132
+ {
133
+ slot: "response",
134
+ recipe: "motion-interactive",
135
+ part: "Dot",
136
+ 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."
137
+ },
138
+ {
139
+ slot: "transition",
140
+ recipe: "motion-glide",
141
+ part: "Marker",
142
+ 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."
143
+ },
144
+ {
145
+ slot: "activity",
146
+ recipe: "none",
147
+ part: "Root",
148
+ why: "Navigation chrome has no working state."
149
+ }
150
+ ],
151
+ a11y: [
152
+ 'Renders a <nav> landmark (default name "Sections") of real links to `#id`, so it works with no script, middle-click and copy-link.',
153
+ `Each link is named by its section's label; the current one carries aria-current="location".`,
154
+ "Activating a link from the keyboard moves focus into the section, as following an anchor does, so the next Tab continues from there.",
155
+ "The tooltip opens on focus as well as hover; the dots are 44\xD732 targets.",
156
+ "Under reduced motion the jump is instant and the marker moves without travel."
157
+ ],
158
+ tokensUsed: ["muted", "muted-foreground", "foreground", "primary", "ring", "popover", "popover-foreground", "border-floating"],
159
+ examples: [
160
+ {
161
+ label: "Fixed beside a long read",
162
+ code: `<SectionRail
163
+ aria-label="Case study sections"
164
+ sections={[
165
+ { id: "overview", label: "Overview" },
166
+ { id: "problem", label: "The problem" },
167
+ { id: "process", label: "Process" },
168
+ { id: "outcome", label: "Outcome" },
169
+ ]}
170
+ className="fixed top-1/2 left-6 z-(--z-nav) -translate-y-1/2 max-lg:hidden"
171
+ />`
172
+ },
173
+ {
174
+ label: "On the right edge",
175
+ code: `<SectionRail
176
+ side="right"
177
+ sections={[
178
+ { id: "intro", label: "Introduction" },
179
+ { id: "usage", label: "Usage" },
180
+ { id: "api", label: "API" },
181
+ ]}
182
+ className="fixed top-1/2 right-6 z-(--z-nav) -translate-y-1/2 max-lg:hidden"
183
+ />`
184
+ }
185
+ ]
186
+ };
187
+ export {
188
+ sectionRailMeta
189
+ };
@@ -2,8 +2,8 @@
2
2
  "dist/_contract-CDa9-NdB.d.ts": "062fb08b461ea845",
3
3
  "dist/blocks/api-keys-block.js": "0c6d54126392e4fb",
4
4
  "dist/blocks/api-keys-block.meta.js": "41903f3a7743b068",
5
- "dist/blocks/approval-block.js": "252c908185759a46",
6
- "dist/blocks/approval-block.meta.js": "8a356ebd1f534a54",
5
+ "dist/blocks/approval-block.js": "b84f4d310ce30664",
6
+ "dist/blocks/approval-block.meta.js": "e7c947fca94d9d42",
7
7
  "dist/blocks/chat-thread-block.js": "e0981afdbc8e9b84",
8
8
  "dist/blocks/chat-thread-block.meta.js": "e91fdb9ab2277c27",
9
9
  "dist/blocks/command-palette-block.js": "498ad5d4787f9336",
@@ -26,7 +26,7 @@
26
26
  "dist/blocks/goal-progress-block.meta.js": "3e5b376bf58bdb4b",
27
27
  "dist/blocks/hero-block.js": "ac74e80d4d7d2ad9",
28
28
  "dist/blocks/hero-block.meta.js": "2dd2e5be2472a563",
29
- "dist/blocks/index.d.ts": "940238f0164f6d03",
29
+ "dist/blocks/index.d.ts": "5f4a38dda1460f56",
30
30
  "dist/blocks/index.js": "690417c222dc4d2a",
31
31
  "dist/blocks/invite-team-block.js": "be4df15518252535",
32
32
  "dist/blocks/invite-team-block.meta.js": "fd57b632f0f32276",
@@ -386,6 +386,9 @@
386
386
  "dist/components/section-header/index.js": "11f5658b3805ce6e",
387
387
  "dist/components/section-header/section-header.js": "eafb346c6efc6d84",
388
388
  "dist/components/section-header/section-header.meta.js": "011c13a1916b1d04",
389
+ "dist/components/section-rail/index.js": "30fd32b042f1721e",
390
+ "dist/components/section-rail/section-rail.js": "0eb588ecae9c68ce",
391
+ "dist/components/section-rail/section-rail.meta.js": "fa1993dcde9735db",
389
392
  "dist/components/segmented-bar/index.js": "3f544d3233ab578f",
390
393
  "dist/components/segmented-bar/segmented-bar.js": "26d87e5c14fddc87",
391
394
  "dist/components/segmented-bar/segmented-bar.meta.js": "490381c6d0692291",
@@ -509,8 +512,8 @@
509
512
  "dist/icon-sets/brands.js": "9a37d8739f14f2a9",
510
513
  "dist/icon-sets/reicon.d.ts": "52f6e41dc8137b4e",
511
514
  "dist/icon-sets/reicon.js": "0f73074baac9b866",
512
- "dist/index.d.ts": "59bef7457412bae9",
513
- "dist/index.js": "b6ac1ec603ffaa55",
515
+ "dist/index.d.ts": "2eefd1036ccd7232",
516
+ "dist/index.js": "8d2cf095c3399a97",
514
517
  "dist/ios/chrome.css": "a7ca9e3735d8658e",
515
518
  "dist/ios/chrome.js": "37091fef7b68bd28",
516
519
  "dist/ios/frame.css": "dd2703c944ce575a",
@@ -575,8 +578,9 @@
575
578
  "dist/lib/segmented.js": "43c21955f7f3ae33",
576
579
  "dist/lib/selection-card.js": "43c49c8c51dde54c",
577
580
  "dist/lib/status-tone.js": "61f0a8bb4fb9c7d6",
578
- "dist/lib/taxonomy.js": "4ecb616f502009c0",
581
+ "dist/lib/taxonomy.js": "f1de3ec452d7516f",
579
582
  "dist/lib/trim-label.js": "9998d45e03c95515",
583
+ "dist/lib/use-active-section.js": "9cdda6529e332ac8",
580
584
  "dist/lib/use-caret-trigger.js": "e9dba2b3bc27f18e",
581
585
  "dist/lib/use-copy.js": "5cf22621e4d5e83b",
582
586
  "dist/lib/use-edge-fade.js": "392e8c33f072fd21",