@sableui/react 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -3
- package/CHANGELOG.md +132 -0
- package/README.md +11 -4
- package/bin/sable.mjs +160 -18
- package/dist/activity-DT8VK9fx.d.ts +25 -0
- package/dist/adapters/github.d.ts +41 -0
- package/dist/adapters/github.js +24 -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/activity-heatmap/activity-heatmap.js +420 -0
- package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
- package/dist/components/activity-heatmap/index.js +9 -0
- package/dist/components/code-block/code-block.js +5 -3
- package/dist/components/code-block/code-block.meta.js +6 -0
- package/dist/components/dialog/dialog.js +25 -5
- package/dist/components/dialog/dialog.meta.js +26 -5
- package/dist/components/media-image/media-image.js +3 -1
- package/dist/components/media-image/media-image.meta.js +15 -1
- package/dist/components/media-tile/media-tile.js +13 -1
- package/dist/components/media-tile/media-tile.meta.js +10 -0
- package/dist/components/page-header/page-header.js +9 -4
- package/dist/components/page-header/page-header.meta.js +26 -1
- package/dist/components/pill/pill.meta.js +15 -6
- package/dist/components/pill/pill.variants.js +1 -0
- package/dist/components/search-input/search-input.js +30 -5
- package/dist/components/search-input/search-input.meta.js +29 -4
- package/dist/components/section-header/section-header.js +27 -17
- package/dist/components/section-header/section-header.meta.js +27 -5
- 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/components/sheet/sheet.js +15 -3
- package/dist/components/sheet/sheet.meta.js +2 -2
- package/dist/file-hashes.json +53 -39
- package/dist/icon-sets/reicon.js +23 -1
- package/dist/index.d.ts +364 -27
- package/dist/index.js +11 -1
- package/dist/layout/AspectBox.js +2 -5
- package/dist/layout/Bleed.js +10 -6
- package/dist/layout/Center.js +11 -3
- package/dist/layout/Columns.js +6 -3
- package/dist/layout/Grid.js +14 -6
- package/dist/layout/GridOverlay.js +7 -7
- package/dist/layout/Region.js +18 -3
- package/dist/layout/Row.js +27 -20
- package/dist/layout/Spacer.js +13 -2
- package/dist/layout/SplitPane.js +1 -1
- package/dist/layout/Stack.js +25 -18
- package/dist/layout/index.d.ts +57 -6
- package/dist/lib/chart/activity.js +32 -0
- package/dist/lib/chart/calendar.js +67 -0
- package/dist/lib/chart/chart-tooltip.js +2 -2
- package/dist/lib/chart/heat.js +55 -0
- package/dist/lib/focus-return.js +53 -0
- package/dist/lib/taxonomy.js +2 -0
- package/dist/lib/use-active-section.js +59 -0
- package/dist/styles.css +122 -114
- package/llms/blocks/approval-block.md +40 -55
- package/llms/components/activity-heatmap.md +133 -0
- package/llms/components/code-block.md +1 -0
- package/llms/components/dialog.md +34 -3
- package/llms/components/media-image.md +12 -0
- package/llms/components/media-tile.md +21 -0
- package/llms/components/page-header.md +16 -1
- package/llms/components/pill.md +24 -5
- package/llms/components/search-input.md +37 -4
- package/llms/components/section-header.md +28 -4
- package/llms/components/section-rail.md +101 -0
- package/llms/components/sheet.md +2 -2
- package/llms/layout/aspect-box.md +1 -1
- package/llms/layout/bleed.md +1 -1
- package/llms/layout/center.md +1 -1
- package/llms/layout/columns.md +1 -1
- package/llms/layout/grid.md +1 -1
- package/llms/layout/region.md +1 -1
- package/llms/layout/row.md +9 -1
- package/llms/layout/spacer.md +1 -1
- package/llms/layout/split-pane.md +1 -1
- package/llms/layout/stack.md +1 -1
- package/llms/tasks/auditing-an-app.md +31 -0
- package/llms/tasks/getting-started.md +10 -3
- package/llms/tasks/using-presets.md +1 -2
- package/llms-full.txt +516 -90
- package/llms.txt +5 -3
- package/package.json +6 -1
- package/registry.json +745 -46
- package/scripts/adoption.mjs +239 -0
- package/scripts/agent-setup.mjs +110 -18
- package/scripts/grade.mjs +17 -0
- package/scripts/wiring.mjs +2 -1
- package/skills/entry/SKILL.md +1 -1
- package/skills/sable/SKILL.md +33 -4
- package/skills/sable/agents/openai.yaml +1 -1
- package/targets/ios.json +18 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/lib/chart/heat.ts
|
|
2
|
+
function quantile(sorted, p) {
|
|
3
|
+
if (!sorted.length) return 0;
|
|
4
|
+
const i = (sorted.length - 1) * p;
|
|
5
|
+
const lo = Math.floor(i);
|
|
6
|
+
const hi = Math.ceil(i);
|
|
7
|
+
return sorted[lo] + (sorted[hi] - sorted[lo]) * (i - lo);
|
|
8
|
+
}
|
|
9
|
+
function activityLevels(values, { levels = 5, scale = "quantile" } = {}) {
|
|
10
|
+
const steps = Math.max(2, Math.round(levels));
|
|
11
|
+
const top = steps - 1;
|
|
12
|
+
const positive = values.filter((v) => v > 0).sort((a, b) => a - b);
|
|
13
|
+
const stats = { min: positive[0] ?? 0, max: positive.at(-1) ?? 0, levels: steps };
|
|
14
|
+
const clamp = (l) => Math.min(top, Math.max(0, Math.round(l)));
|
|
15
|
+
if (typeof scale === "function") {
|
|
16
|
+
return { thresholds: [], level: (v) => v > 0 ? Math.max(1, clamp(scale(v, stats))) : 0 };
|
|
17
|
+
}
|
|
18
|
+
let thresholds;
|
|
19
|
+
if (Array.isArray(scale)) {
|
|
20
|
+
thresholds = scale.slice(0, top);
|
|
21
|
+
} else if (scale === "linear") {
|
|
22
|
+
thresholds = Array.from({ length: top }, (_, k) => k === 0 ? Number.MIN_VALUE : stats.max * k / top);
|
|
23
|
+
} else {
|
|
24
|
+
thresholds = Array.from({ length: top }, (_, k) => quantile(positive, k / top));
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
thresholds,
|
|
28
|
+
level: (v) => {
|
|
29
|
+
if (!(v > 0)) return 0;
|
|
30
|
+
let l = 1;
|
|
31
|
+
for (let k = 1; k < thresholds.length; k++) if (v >= thresholds[k]) l = k + 1;
|
|
32
|
+
return l;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
var HEAT_EMPTY = "color-mix(in oklch, var(--foreground) 8%, transparent)";
|
|
37
|
+
function heatColor(base, level, levels) {
|
|
38
|
+
if (level <= 0) return HEAT_EMPTY;
|
|
39
|
+
const top = Math.max(1, levels - 1);
|
|
40
|
+
if (level >= top) return base;
|
|
41
|
+
const share = Math.round(35 + 65 * (level - 1) / (top - 1));
|
|
42
|
+
return `color-mix(in oklch, ${base} ${share}%, ${HEAT_EMPTY})`;
|
|
43
|
+
}
|
|
44
|
+
function levelNames(levels) {
|
|
45
|
+
if (levels === 5) return ["None", "Low", "Medium-low", "Medium-high", "High"];
|
|
46
|
+
if (levels === 2) return ["None", "Some"];
|
|
47
|
+
if (levels === 3) return ["None", "Low", "High"];
|
|
48
|
+
return Array.from({ length: levels }, (_, k) => k === 0 ? "None" : `Level ${k} of ${levels - 1}`);
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
HEAT_EMPTY,
|
|
52
|
+
activityLevels,
|
|
53
|
+
heatColor,
|
|
54
|
+
levelNames
|
|
55
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/lib/focus-return.tsx
|
|
2
|
+
import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
|
3
|
+
var FocusReturnContext = createContext(null);
|
|
4
|
+
function focusedElement() {
|
|
5
|
+
if (typeof document === "undefined") return null;
|
|
6
|
+
let element = document.activeElement;
|
|
7
|
+
while (element?.shadowRoot?.activeElement) element = element.shadowRoot.activeElement;
|
|
8
|
+
return element instanceof HTMLElement && element !== document.body ? element : null;
|
|
9
|
+
}
|
|
10
|
+
function useFocusReturn(open, onOpenChange) {
|
|
11
|
+
const [opener, setOpener] = useState(null);
|
|
12
|
+
const popupRef = useRef(null);
|
|
13
|
+
const [wasOpen, setWasOpen] = useState(false);
|
|
14
|
+
if (open !== void 0 && open !== wasOpen) {
|
|
15
|
+
setWasOpen(open);
|
|
16
|
+
if (open) setOpener(focusedElement());
|
|
17
|
+
}
|
|
18
|
+
const handleOpenChange = useCallback(
|
|
19
|
+
(next, ...rest) => {
|
|
20
|
+
if (next && open === void 0) setOpener(focusedElement());
|
|
21
|
+
onOpenChange?.(next, ...rest);
|
|
22
|
+
},
|
|
23
|
+
[open, onOpenChange]
|
|
24
|
+
);
|
|
25
|
+
const value = useMemo(
|
|
26
|
+
() => ({
|
|
27
|
+
popupRef(node) {
|
|
28
|
+
popupRef.current = node;
|
|
29
|
+
},
|
|
30
|
+
// Base UI reads finalFocus when the popup closes, from the latest props,
|
|
31
|
+
// so a new function per opener is picked up.
|
|
32
|
+
finalFocus() {
|
|
33
|
+
const target = opener;
|
|
34
|
+
if (!target?.isConnected) return true;
|
|
35
|
+
const active = focusedElement();
|
|
36
|
+
const popup = popupRef.current;
|
|
37
|
+
if (active && active !== target && popup && !popup.contains(active)) return false;
|
|
38
|
+
return target;
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
[opener]
|
|
42
|
+
);
|
|
43
|
+
return { onOpenChange: handleOpenChange, focusReturn: value };
|
|
44
|
+
}
|
|
45
|
+
var FocusReturnProvider = FocusReturnContext.Provider;
|
|
46
|
+
function useFocusReturnDefault() {
|
|
47
|
+
return useContext(FocusReturnContext);
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
FocusReturnProvider,
|
|
51
|
+
useFocusReturn,
|
|
52
|
+
useFocusReturnDefault
|
|
53
|
+
};
|
package/dist/lib/taxonomy.js
CHANGED
|
@@ -90,6 +90,7 @@ var COMPONENT_TAXONOMY = {
|
|
|
90
90
|
BottomNav: ["Navigation", "page"],
|
|
91
91
|
NavigationMenu: ["Navigation", "page"],
|
|
92
92
|
Pagination: ["Navigation", "either"],
|
|
93
|
+
SectionRail: ["Navigation", "page"],
|
|
93
94
|
ShowMore: ["Navigation", "page"],
|
|
94
95
|
SideNav: ["Navigation", "page"],
|
|
95
96
|
Sidebar: ["Navigation", "page"],
|
|
@@ -130,6 +131,7 @@ var COMPONENT_TAXONOMY = {
|
|
|
130
131
|
RadarChart: ["Charts", "page"],
|
|
131
132
|
RadialChart: ["Charts", "page"],
|
|
132
133
|
Sparkline: ["Charts", "either"],
|
|
134
|
+
ActivityHeatmap: ["Charts", "either"],
|
|
133
135
|
/* ── Data display ─────────────────────────────────────────────────────────
|
|
134
136
|
Showing records and readings. `Meter` (bar or ring) is here: it shows a
|
|
135
137
|
value within a known range (a reading), which is not loading or progress. */
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/lib/use-active-section.ts
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
var END_SLACK = 2;
|
|
6
|
+
function useActiveSection(ids, { offset = 0.35, root } = {}) {
|
|
7
|
+
const [active, setActive] = useState(ids[0] ?? null);
|
|
8
|
+
const key = ids.join("\n");
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const list = key ? key.split("\n") : [];
|
|
11
|
+
if (list.length === 0) {
|
|
12
|
+
setActive(null);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const scroller = root?.current ?? null;
|
|
16
|
+
let frame = 0;
|
|
17
|
+
const measure = () => {
|
|
18
|
+
frame = 0;
|
|
19
|
+
const top = scroller ? scroller.getBoundingClientRect().top : 0;
|
|
20
|
+
const height = scroller ? scroller.clientHeight : window.innerHeight;
|
|
21
|
+
const line = top + height * offset;
|
|
22
|
+
const bottom = top + height;
|
|
23
|
+
const scrolled = scroller ? scroller.scrollTop : window.scrollY;
|
|
24
|
+
const range = scroller ? scroller.scrollHeight - scroller.clientHeight : document.documentElement.scrollHeight - window.innerHeight;
|
|
25
|
+
const atEnd = range > END_SLACK && scrolled >= range - END_SLACK;
|
|
26
|
+
let current = list[0];
|
|
27
|
+
for (const id of list) {
|
|
28
|
+
const section = document.getElementById(id);
|
|
29
|
+
if (!section) continue;
|
|
30
|
+
const sectionTop = section.getBoundingClientRect().top;
|
|
31
|
+
if (sectionTop <= line || atEnd && sectionTop < bottom) current = id;
|
|
32
|
+
}
|
|
33
|
+
setActive(current);
|
|
34
|
+
};
|
|
35
|
+
const schedule = () => {
|
|
36
|
+
if (!frame) frame = requestAnimationFrame(measure);
|
|
37
|
+
};
|
|
38
|
+
measure();
|
|
39
|
+
const target = scroller ?? window;
|
|
40
|
+
target.addEventListener("scroll", schedule, { passive: true });
|
|
41
|
+
window.addEventListener("resize", schedule);
|
|
42
|
+
const resize = new ResizeObserver(schedule);
|
|
43
|
+
resize.observe(scroller ?? document.documentElement);
|
|
44
|
+
for (const id of list) {
|
|
45
|
+
const section = document.getElementById(id);
|
|
46
|
+
if (section) resize.observe(section);
|
|
47
|
+
}
|
|
48
|
+
return () => {
|
|
49
|
+
cancelAnimationFrame(frame);
|
|
50
|
+
target.removeEventListener("scroll", schedule);
|
|
51
|
+
window.removeEventListener("resize", schedule);
|
|
52
|
+
resize.disconnect();
|
|
53
|
+
};
|
|
54
|
+
}, [key, offset, root]);
|
|
55
|
+
return active;
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
useActiveSection
|
|
59
|
+
};
|
package/dist/styles.css
CHANGED
|
@@ -1665,13 +1665,19 @@
|
|
|
1665
1665
|
/* ==========================================================================
|
|
1666
1666
|
Layout — responsive grid spans + split panes.
|
|
1667
1667
|
|
|
1668
|
-
The fluid layout primitives (Region, Grid, Stack…)
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1668
|
+
The fluid layout primitives (Region, Grid, Stack…) put their layout in
|
|
1669
|
+
classes (`flex`, `grid`, `items-*`, `px-(--page-inset)`) and pass dynamic
|
|
1670
|
+
values as inline CSS custom properties that those classes read, so any value
|
|
1671
|
+
works without Tailwind scanning a class. Never an inline `display`, `gap` or
|
|
1672
|
+
padding: inline style beats every utility, and `<Row className="hidden
|
|
1673
|
+
md:flex">` silently did nothing. Breakpoint-aware layout can't be inline (a
|
|
1674
|
+
style attribute has no media queries), so `Col` and `SplitPane` set
|
|
1675
|
+
per-breakpoint CSS custom properties inline and these rules resolve them
|
|
1676
|
+
mobile-first. The rules sit in `@layer components` so a utility in the
|
|
1677
|
+
caller's `className` (`hidden`, `lg:grid-cols-3`, `gap-8`) still wins.
|
|
1678
|
+
Breakpoints match Tailwind's defaults (sm 40 / md 48 / lg 64 / xl 80 /
|
|
1679
|
+
2xl 96 rem) so they line up with the `lg:*` visibility utilities the
|
|
1680
|
+
app-shell components already use.
|
|
1675
1681
|
========================================================================== */
|
|
1676
1682
|
|
|
1677
1683
|
/* Each element owns its breakpoint values. Custom properties inherit, so a
|
|
@@ -1679,18 +1685,20 @@
|
|
|
1679
1685
|
Grid inside `Columns count={{ lg: 12 }}` drew 12 tracks at lg. Resetting
|
|
1680
1686
|
them here makes an unset one invalid, so the fallback chain below runs on the
|
|
1681
1687
|
element itself; the values it was given are inline and win over this. */
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1688
|
+
@layer components {
|
|
1689
|
+
[data-sable-col] {
|
|
1690
|
+
--col-span-base: initial; --col-span-sm: initial; --col-span-md: initial;
|
|
1691
|
+
--col-span-lg: initial; --col-span-xl: initial; --col-span-2xl: initial;
|
|
1692
|
+
--col-start-base: initial; --col-start-sm: initial; --col-start-md: initial;
|
|
1693
|
+
--col-start-lg: initial; --col-start-xl: initial; --col-start-2xl: initial;
|
|
1694
|
+
}
|
|
1695
|
+
[data-sable-grid-cols] {
|
|
1696
|
+
--cols-base: initial; --cols-sm: initial; --cols-md: initial;
|
|
1697
|
+
--cols-lg: initial; --cols-xl: initial; --cols-2xl: initial;
|
|
1698
|
+
}
|
|
1699
|
+
[data-sable-split] {
|
|
1700
|
+
--split-gap: initial;
|
|
1701
|
+
}
|
|
1694
1702
|
}
|
|
1695
1703
|
|
|
1696
1704
|
/* Toasts keep clear of an open Sheet's actions. A right sheet's pinned footer
|
|
@@ -1761,126 +1769,126 @@
|
|
|
1761
1769
|
}
|
|
1762
1770
|
}
|
|
1763
1771
|
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
grid-column: var(--col-start, auto) / span var(--col-span, 1);
|
|
1768
|
-
--col-span: var(--col-span-base, 1);
|
|
1769
|
-
--col-start: var(--col-start-base, auto);
|
|
1770
|
-
}
|
|
1771
|
-
@media (width >= 40rem) {
|
|
1772
|
+
@layer components {
|
|
1773
|
+
/* `Col span={{ base, md, lg }}` / `start={…}` — each breakpoint resolves to the
|
|
1774
|
+
most specific value defined at or below it (mobile-first cascade). */
|
|
1772
1775
|
[data-sable-col] {
|
|
1773
|
-
|
|
1774
|
-
--col-
|
|
1776
|
+
grid-column: var(--col-start, auto) / span var(--col-span, 1);
|
|
1777
|
+
--col-span: var(--col-span-base, 1);
|
|
1778
|
+
--col-start: var(--col-start-base, auto);
|
|
1779
|
+
}
|
|
1780
|
+
@media (width >= 40rem) {
|
|
1781
|
+
[data-sable-col] {
|
|
1782
|
+
--col-span: var(--col-span-sm, var(--col-span-base, 1));
|
|
1783
|
+
--col-start: var(--col-start-sm, var(--col-start-base, auto));
|
|
1784
|
+
}
|
|
1775
1785
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1786
|
+
@media (width >= 48rem) {
|
|
1787
|
+
[data-sable-col] {
|
|
1788
|
+
--col-span: var(--col-span-md, var(--col-span-sm, var(--col-span-base, 1)));
|
|
1789
|
+
--col-start: var(--col-start-md, var(--col-start-sm, var(--col-start-base, auto)));
|
|
1790
|
+
}
|
|
1781
1791
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1792
|
+
@media (width >= 64rem) {
|
|
1793
|
+
[data-sable-col] {
|
|
1794
|
+
--col-span: var(--col-span-lg, var(--col-span-md, var(--col-span-sm, var(--col-span-base, 1))));
|
|
1795
|
+
--col-start: var(--col-start-lg, var(--col-start-md, var(--col-start-sm, var(--col-start-base, auto))));
|
|
1796
|
+
}
|
|
1787
1797
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1798
|
+
@media (width >= 80rem) {
|
|
1799
|
+
[data-sable-col] {
|
|
1800
|
+
--col-span: var(--col-span-xl, var(--col-span-lg, var(--col-span-md, var(--col-span-sm, var(--col-span-base, 1)))));
|
|
1801
|
+
--col-start: var(--col-start-xl, var(--col-start-lg, var(--col-start-md, var(--col-start-sm, var(--col-start-base, auto)))));
|
|
1802
|
+
}
|
|
1793
1803
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1804
|
+
@media (width >= 96rem) {
|
|
1805
|
+
[data-sable-col] {
|
|
1806
|
+
--col-span: var(--col-span-2xl, var(--col-span-xl, var(--col-span-lg, var(--col-span-md, var(--col-span-sm, var(--col-span-base, 1))))));
|
|
1807
|
+
--col-start: var(--col-start-2xl, var(--col-start-xl, var(--col-start-lg, var(--col-start-md, var(--col-start-sm, var(--col-start-base, auto))))));
|
|
1808
|
+
}
|
|
1799
1809
|
}
|
|
1800
|
-
}
|
|
1801
1810
|
|
|
1802
|
-
/* `Columns count` / `Grid cols` as a breakpoint map — set per-breakpoint --cols-*
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
[data-sable-grid-cols] {
|
|
1806
|
-
display: grid;
|
|
1807
|
-
grid-template-columns: repeat(var(--cols, var(--grid-columns)), minmax(0, 1fr));
|
|
1808
|
-
--cols: var(--cols-base, var(--grid-columns));
|
|
1809
|
-
}
|
|
1810
|
-
@media (width >= 40rem) {
|
|
1811
|
+
/* `Columns count` / `Grid cols` as a breakpoint map — set per-breakpoint --cols-*
|
|
1812
|
+
props inline; resolved mobile-first into the track's column count, so a grid
|
|
1813
|
+
thins to fewer columns on small screens (e.g. 2 → 6 → 12). */
|
|
1811
1814
|
[data-sable-grid-cols] {
|
|
1812
|
-
|
|
1815
|
+
grid-template-columns: repeat(var(--cols, var(--grid-columns)), minmax(0, 1fr));
|
|
1816
|
+
--cols: var(--cols-base, var(--grid-columns));
|
|
1813
1817
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
+
@media (width >= 40rem) {
|
|
1819
|
+
[data-sable-grid-cols] {
|
|
1820
|
+
--cols: var(--cols-sm, var(--cols-base, var(--grid-columns)));
|
|
1821
|
+
}
|
|
1818
1822
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
--cols-lg,
|
|
1824
|
-
var(--cols-md, var(--cols-sm, var(--cols-base, var(--grid-columns))))
|
|
1825
|
-
);
|
|
1823
|
+
@media (width >= 48rem) {
|
|
1824
|
+
[data-sable-grid-cols] {
|
|
1825
|
+
--cols: var(--cols-md, var(--cols-sm, var(--cols-base, var(--grid-columns))));
|
|
1826
|
+
}
|
|
1826
1827
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
--cols: var(
|
|
1831
|
-
--cols-xl,
|
|
1832
|
-
var(
|
|
1828
|
+
@media (width >= 64rem) {
|
|
1829
|
+
[data-sable-grid-cols] {
|
|
1830
|
+
--cols: var(
|
|
1833
1831
|
--cols-lg,
|
|
1834
1832
|
var(--cols-md, var(--cols-sm, var(--cols-base, var(--grid-columns))))
|
|
1835
|
-
)
|
|
1836
|
-
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1837
1835
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
--cols: var(
|
|
1842
|
-
--cols-2xl,
|
|
1843
|
-
var(
|
|
1836
|
+
@media (width >= 80rem) {
|
|
1837
|
+
[data-sable-grid-cols] {
|
|
1838
|
+
--cols: var(
|
|
1844
1839
|
--cols-xl,
|
|
1845
1840
|
var(
|
|
1846
1841
|
--cols-lg,
|
|
1847
1842
|
var(--cols-md, var(--cols-sm, var(--cols-base, var(--grid-columns))))
|
|
1848
1843
|
)
|
|
1849
|
-
)
|
|
1850
|
-
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
@media (width >= 96rem) {
|
|
1848
|
+
[data-sable-grid-cols] {
|
|
1849
|
+
--cols: var(
|
|
1850
|
+
--cols-2xl,
|
|
1851
|
+
var(
|
|
1852
|
+
--cols-xl,
|
|
1853
|
+
var(
|
|
1854
|
+
--cols-lg,
|
|
1855
|
+
var(--cols-md, var(--cols-sm, var(--cols-base, var(--grid-columns))))
|
|
1856
|
+
)
|
|
1857
|
+
)
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1851
1860
|
}
|
|
1852
|
-
}
|
|
1853
1861
|
|
|
1854
|
-
/* `SplitPane collapseBelow="lg"` — a single stacked column below the breakpoint;
|
|
1855
|
-
|
|
1856
|
-
[data-sable-split] {
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
grid-template-columns: 1fr;
|
|
1860
|
-
}
|
|
1861
|
-
@media (width >= 40rem) {
|
|
1862
|
-
[data-sable-split][data-collapse="sm"] {
|
|
1863
|
-
grid-template-columns: var(--split-primary, 1fr) var(--split-secondary, 1fr);
|
|
1862
|
+
/* `SplitPane collapseBelow="lg"` — a single stacked column below the breakpoint;
|
|
1863
|
+
a two-column track (primary:secondary fr ratio) at or above it. */
|
|
1864
|
+
[data-sable-split] {
|
|
1865
|
+
gap: var(--split-gap, var(--grid-gutter));
|
|
1866
|
+
grid-template-columns: 1fr;
|
|
1864
1867
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1868
|
+
@media (width >= 40rem) {
|
|
1869
|
+
[data-sable-split][data-collapse="sm"] {
|
|
1870
|
+
grid-template-columns: var(--split-primary, 1fr) var(--split-secondary, 1fr);
|
|
1871
|
+
}
|
|
1869
1872
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1873
|
+
@media (width >= 48rem) {
|
|
1874
|
+
[data-sable-split][data-collapse="md"] {
|
|
1875
|
+
grid-template-columns: var(--split-primary, 1fr) var(--split-secondary, 1fr);
|
|
1876
|
+
}
|
|
1874
1877
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1878
|
+
@media (width >= 64rem) {
|
|
1879
|
+
[data-sable-split][data-collapse="lg"] {
|
|
1880
|
+
grid-template-columns: var(--split-primary, 1fr) var(--split-secondary, 1fr);
|
|
1881
|
+
}
|
|
1879
1882
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1883
|
+
@media (width >= 80rem) {
|
|
1884
|
+
[data-sable-split][data-collapse="xl"] {
|
|
1885
|
+
grid-template-columns: var(--split-primary, 1fr) var(--split-secondary, 1fr);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
@media (width >= 96rem) {
|
|
1889
|
+
[data-sable-split][data-collapse="2xl"] {
|
|
1890
|
+
grid-template-columns: var(--split-primary, 1fr) var(--split-secondary, 1fr);
|
|
1891
|
+
}
|
|
1884
1892
|
}
|
|
1885
1893
|
}
|
|
1886
1894
|
|
|
@@ -1,48 +1,45 @@
|
|
|
1
1
|
# ApprovalBlock
|
|
2
2
|
|
|
3
|
-
An agent asking permission for one tool call: the question, what
|
|
3
|
+
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.
|
|
4
4
|
|
|
5
|
-
**Intent:** Reach for this in a chat or agent run when a tool call waits on the person — sending, paying, deleting, publishing.
|
|
5
|
+
**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.
|
|
6
6
|
|
|
7
7
|
**Block:** AI & agents · card · designed for 18rem–28rem wide (fills its container up to the max)
|
|
8
8
|
|
|
9
9
|
## Notes
|
|
10
10
|
|
|
11
|
-
- The title is the question in plain words ("
|
|
12
|
-
- The
|
|
13
|
-
-
|
|
14
|
-
- After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer.
|
|
11
|
+
- 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.
|
|
12
|
+
- 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.
|
|
13
|
+
- 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.
|
|
14
|
+
- 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.
|
|
15
15
|
|
|
16
16
|
## Accessibility
|
|
17
17
|
|
|
18
18
|
- 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.
|
|
19
|
-
- The
|
|
20
|
-
- The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name.
|
|
19
|
+
- The ToolCall announces its own status change, and its input is a labelled code view.
|
|
21
20
|
|
|
22
21
|
## Source
|
|
23
22
|
|
|
24
23
|
```tsx
|
|
25
|
-
import { useState, useEffect, useRef
|
|
26
|
-
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,
|
|
24
|
+
import { useState, useEffect, useRef } from "react"
|
|
25
|
+
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, InlineStatus, ToolCall } from "@sableui/react"
|
|
27
26
|
|
|
28
27
|
const request = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
allowed: "Allowed. Sending the email.",
|
|
38
|
-
denied: "Denied. The agent won't send it.",
|
|
28
|
+
title: "Run this command?",
|
|
29
|
+
description: "The agent wants to migrate the production database in acme/api. It changes live data.",
|
|
30
|
+
tool: "run_command",
|
|
31
|
+
action: "Run a command",
|
|
32
|
+
// A command reads as the command; a structured call would pass its arguments object.
|
|
33
|
+
input: "pnpm db:migrate --env production",
|
|
34
|
+
allowed: "Allowed. Running the migration.",
|
|
35
|
+
denied: "Denied. The agent won't run it.",
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
export interface ApprovalBlockProps {
|
|
42
39
|
className?: string
|
|
43
40
|
/** false drops the card's surface and border — for a chat column that already frames it. */
|
|
44
41
|
framed?: boolean
|
|
45
|
-
/** The person allowed the call. `remember` is
|
|
42
|
+
/** The person allowed the call. `remember` is true for "Always allow". */
|
|
46
43
|
onApprove?: (options: { remember: boolean }) => void
|
|
47
44
|
/** The person declined the call. */
|
|
48
45
|
onDeny?: () => void
|
|
@@ -50,8 +47,6 @@ export interface ApprovalBlockProps {
|
|
|
50
47
|
|
|
51
48
|
export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: ApprovalBlockProps) {
|
|
52
49
|
const [decision, setDecision] = useState<"allowed" | "denied" | null>(null)
|
|
53
|
-
const [remember, setRemember] = useState(false)
|
|
54
|
-
const id = useId()
|
|
55
50
|
const outcome = useRef<HTMLDivElement>(null)
|
|
56
51
|
// The buttons leave with the decision, so focus moves to the line that
|
|
57
52
|
// replaces them instead of falling back to the top of the page.
|
|
@@ -59,37 +54,30 @@ export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: A
|
|
|
59
54
|
if (decision) outcome.current?.focus()
|
|
60
55
|
}, [decision])
|
|
61
56
|
|
|
57
|
+
const approve = (remember: boolean) => {
|
|
58
|
+
setDecision("allowed")
|
|
59
|
+
onApprove?.({ remember })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
62
|
return (
|
|
63
63
|
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-md", className)}>
|
|
64
64
|
<CardHeader>
|
|
65
65
|
<CardTitle>{request.title}</CardTitle>
|
|
66
66
|
<CardDescription>{request.description}</CardDescription>
|
|
67
67
|
</CardHeader>
|
|
68
|
-
<CardContent
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<Code>{request.tool}</Code>
|
|
80
|
-
</dd>
|
|
81
|
-
</div>
|
|
82
|
-
</dl>
|
|
83
|
-
{decision ? null : (
|
|
84
|
-
<div className="flex items-center gap-2">
|
|
85
|
-
<Checkbox id={`${id}-remember`} checked={remember} onCheckedChange={(v: boolean) => setRemember(v)} />
|
|
86
|
-
<Label htmlFor={`${id}-remember`} className="text-body-sm">
|
|
87
|
-
Allow <Code>{request.tool}</Code> for the rest of this chat
|
|
88
|
-
</Label>
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
68
|
+
<CardContent>
|
|
69
|
+
{/* The call itself, exactly as the agent made it. It answers with the
|
|
70
|
+
person: held, then done or denied. */}
|
|
71
|
+
<ToolCall
|
|
72
|
+
name={request.tool}
|
|
73
|
+
title={request.action}
|
|
74
|
+
status={decision === "allowed" ? "complete" : decision === "denied" ? "denied" : "awaiting"}
|
|
75
|
+
statusLabel={decision === "allowed" ? "Allowed" : undefined}
|
|
76
|
+
input={request.input}
|
|
77
|
+
defaultOpen
|
|
78
|
+
/>
|
|
91
79
|
</CardContent>
|
|
92
|
-
<CardFooter actions
|
|
80
|
+
<CardFooter actions>
|
|
93
81
|
{decision ? (
|
|
94
82
|
<InlineStatus
|
|
95
83
|
ref={outcome}
|
|
@@ -103,7 +91,8 @@ export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: A
|
|
|
103
91
|
) : (
|
|
104
92
|
<>
|
|
105
93
|
<Button
|
|
106
|
-
variant="
|
|
94
|
+
variant="ghost"
|
|
95
|
+
className="me-auto"
|
|
107
96
|
onClick={() => {
|
|
108
97
|
setDecision("denied")
|
|
109
98
|
onDeny?.()
|
|
@@ -111,14 +100,10 @@ export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: A
|
|
|
111
100
|
>
|
|
112
101
|
Deny
|
|
113
102
|
</Button>
|
|
114
|
-
<Button
|
|
115
|
-
|
|
116
|
-
setDecision("allowed")
|
|
117
|
-
onApprove?.({ remember })
|
|
118
|
-
}}
|
|
119
|
-
>
|
|
120
|
-
Allow
|
|
103
|
+
<Button variant="outline" onClick={() => approve(true)}>
|
|
104
|
+
Always allow
|
|
121
105
|
</Button>
|
|
106
|
+
<Button onClick={() => approve(false)}>Allow once</Button>
|
|
122
107
|
</>
|
|
123
108
|
)}
|
|
124
109
|
</CardFooter>
|