@corbits/react-ui 0.1.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/LICENSE +501 -0
- package/README.md +125 -0
- package/dist/blocks/login/auth-layout.d.ts +38 -0
- package/dist/blocks/login/auth-layout.js +52 -0
- package/dist/blocks/login/login-form.d.ts +60 -0
- package/dist/blocks/login/login-form.js +151 -0
- package/dist/hooks/use-command-palette-navigation.d.ts +25 -0
- package/dist/hooks/use-command-palette-navigation.js +52 -0
- package/dist/hooks/use-controllable-state.d.ts +28 -0
- package/dist/hooks/use-controllable-state.js +44 -0
- package/dist/hooks/use-delayed-autofocus.d.ts +9 -0
- package/dist/hooks/use-delayed-autofocus.js +20 -0
- package/dist/hooks/use-dismissable-popover.d.ts +37 -0
- package/dist/hooks/use-dismissable-popover.js +64 -0
- package/dist/hooks/use-list-selection.d.ts +66 -0
- package/dist/hooks/use-list-selection.js +147 -0
- package/dist/hooks/use-prefers-reduced-motion.d.ts +13 -0
- package/dist/hooks/use-prefers-reduced-motion.js +28 -0
- package/dist/hooks/use-scroll-current-into-view.d.ts +19 -0
- package/dist/hooks/use-scroll-current-into-view.js +33 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +131 -0
- package/dist/lib/artifact.d.ts +72 -0
- package/dist/lib/artifact.js +67 -0
- package/dist/lib/chart-geometry.d.ts +62 -0
- package/dist/lib/chart-geometry.js +86 -0
- package/dist/lib/chart-palette.d.ts +76 -0
- package/dist/lib/chart-palette.js +92 -0
- package/dist/lib/chat-message.d.ts +115 -0
- package/dist/lib/chat-message.js +41 -0
- package/dist/lib/chat-parts.d.ts +64 -0
- package/dist/lib/chat-parts.js +16 -0
- package/dist/lib/csv.d.ts +60 -0
- package/dist/lib/csv.js +108 -0
- package/dist/lib/metrics.d.ts +70 -0
- package/dist/lib/metrics.js +101 -0
- package/dist/lib/now-item.d.ts +65 -0
- package/dist/lib/now-item.js +59 -0
- package/dist/lib/relative-time.d.ts +9 -0
- package/dist/lib/relative-time.js +47 -0
- package/dist/lib/theme.d.ts +30 -0
- package/dist/lib/theme.js +77 -0
- package/dist/lib/url.d.ts +26 -0
- package/dist/lib/url.js +37 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/lib/workflow-display-flow.d.ts +83 -0
- package/dist/lib/workflow-display-flow.js +104 -0
- package/dist/lib/workflow-registry.d.ts +90 -0
- package/dist/lib/workflow-registry.js +56 -0
- package/dist/lib/workflow-run-error.d.ts +31 -0
- package/dist/lib/workflow-run-error.js +105 -0
- package/dist/lib/workflow-run-progress.d.ts +97 -0
- package/dist/lib/workflow-run-progress.js +135 -0
- package/dist/lib/workflow-run.d.ts +43 -0
- package/dist/lib/workflow-run.js +56 -0
- package/dist/styles.css +3080 -0
- package/dist/test/render-hook.d.ts +14 -0
- package/dist/test/render-hook.js +38 -0
- package/dist/theme.css +628 -0
- package/dist/ui/agent-turn.d.ts +26 -0
- package/dist/ui/agent-turn.js +60 -0
- package/dist/ui/animated-number.d.ts +25 -0
- package/dist/ui/animated-number.js +63 -0
- package/dist/ui/approval-card.d.ts +46 -0
- package/dist/ui/approval-card.js +116 -0
- package/dist/ui/artifact-body.d.ts +26 -0
- package/dist/ui/artifact-body.js +170 -0
- package/dist/ui/artifact-notice.d.ts +21 -0
- package/dist/ui/artifact-notice.js +36 -0
- package/dist/ui/avatar.d.ts +37 -0
- package/dist/ui/avatar.js +73 -0
- package/dist/ui/badge.d.ts +10 -0
- package/dist/ui/badge.js +35 -0
- package/dist/ui/bar-chart.d.ts +38 -0
- package/dist/ui/bar-chart.js +118 -0
- package/dist/ui/block-card.d.ts +29 -0
- package/dist/ui/block-card.js +66 -0
- package/dist/ui/boot-screen.d.ts +23 -0
- package/dist/ui/boot-screen.js +49 -0
- package/dist/ui/bulk-action-bar.d.ts +37 -0
- package/dist/ui/bulk-action-bar.js +64 -0
- package/dist/ui/button.d.ts +12 -0
- package/dist/ui/button.js +73 -0
- package/dist/ui/card.d.ts +6 -0
- package/dist/ui/card.js +37 -0
- package/dist/ui/category-bars.d.ts +20 -0
- package/dist/ui/category-bars.js +69 -0
- package/dist/ui/chart-frame.d.ts +54 -0
- package/dist/ui/chart-frame.js +108 -0
- package/dist/ui/chat-composer.d.ts +24 -0
- package/dist/ui/chat-composer.js +32 -0
- package/dist/ui/chat-dock-timing.d.ts +15 -0
- package/dist/ui/chat-dock-timing.js +11 -0
- package/dist/ui/chat-input.d.ts +38 -0
- package/dist/ui/chat-input.js +138 -0
- package/dist/ui/chat-panel.d.ts +20 -0
- package/dist/ui/chat-panel.js +56 -0
- package/dist/ui/chat-thread.d.ts +28 -0
- package/dist/ui/chat-thread.js +61 -0
- package/dist/ui/checkbox.d.ts +25 -0
- package/dist/ui/checkbox.js +63 -0
- package/dist/ui/command-palette.d.ts +100 -0
- package/dist/ui/command-palette.js +341 -0
- package/dist/ui/compare-body.d.ts +52 -0
- package/dist/ui/compare-body.js +140 -0
- package/dist/ui/confirm-button.d.ts +29 -0
- package/dist/ui/confirm-button.js +41 -0
- package/dist/ui/corbits-mark.d.ts +29 -0
- package/dist/ui/corbits-mark.js +33 -0
- package/dist/ui/csv-table.d.ts +22 -0
- package/dist/ui/csv-table.js +85 -0
- package/dist/ui/dialog.d.ts +37 -0
- package/dist/ui/dialog.js +105 -0
- package/dist/ui/dictation-status-line.d.ts +20 -0
- package/dist/ui/dictation-status-line.js +38 -0
- package/dist/ui/dither-canvas.d.ts +31 -0
- package/dist/ui/dither-canvas.js +169 -0
- package/dist/ui/embed-body.d.ts +30 -0
- package/dist/ui/embed-body.js +92 -0
- package/dist/ui/empty-state.d.ts +26 -0
- package/dist/ui/empty-state.js +42 -0
- package/dist/ui/file-input.d.ts +25 -0
- package/dist/ui/file-input.js +66 -0
- package/dist/ui/filter-bar.d.ts +42 -0
- package/dist/ui/filter-bar.js +95 -0
- package/dist/ui/filter-chip.d.ts +14 -0
- package/dist/ui/filter-chip.js +23 -0
- package/dist/ui/gate-block.d.ts +16 -0
- package/dist/ui/gate-block.js +37 -0
- package/dist/ui/horizontal-stepper.d.ts +13 -0
- package/dist/ui/horizontal-stepper.js +65 -0
- package/dist/ui/input.d.ts +2 -0
- package/dist/ui/input.js +14 -0
- package/dist/ui/inspector-shell.d.ts +47 -0
- package/dist/ui/inspector-shell.js +98 -0
- package/dist/ui/intake-form.d.ts +39 -0
- package/dist/ui/intake-form.js +119 -0
- package/dist/ui/kind-card-grid.d.ts +23 -0
- package/dist/ui/kind-card-grid.js +43 -0
- package/dist/ui/library-page-header.d.ts +20 -0
- package/dist/ui/library-page-header.js +43 -0
- package/dist/ui/list-detail.d.ts +28 -0
- package/dist/ui/list-detail.js +55 -0
- package/dist/ui/live-run-banner.d.ts +13 -0
- package/dist/ui/live-run-banner.js +23 -0
- package/dist/ui/live-status-line.d.ts +22 -0
- package/dist/ui/live-status-line.js +37 -0
- package/dist/ui/menu.d.ts +18 -0
- package/dist/ui/menu.js +45 -0
- package/dist/ui/message-bubble.d.ts +24 -0
- package/dist/ui/message-bubble.js +64 -0
- package/dist/ui/mic-button.d.ts +35 -0
- package/dist/ui/mic-button.js +61 -0
- package/dist/ui/mic-permission-dialog.d.ts +20 -0
- package/dist/ui/mic-permission-dialog.js +50 -0
- package/dist/ui/notifications-bell.d.ts +28 -0
- package/dist/ui/notifications-bell.js +69 -0
- package/dist/ui/now-cards.d.ts +37 -0
- package/dist/ui/now-cards.js +116 -0
- package/dist/ui/page-panel.d.ts +37 -0
- package/dist/ui/page-panel.js +23 -0
- package/dist/ui/page-shell.d.ts +21 -0
- package/dist/ui/page-shell.js +26 -0
- package/dist/ui/parts-renderer.d.ts +22 -0
- package/dist/ui/parts-renderer.js +234 -0
- package/dist/ui/profile-card.d.ts +39 -0
- package/dist/ui/profile-card.js +124 -0
- package/dist/ui/progress-checklist.d.ts +25 -0
- package/dist/ui/progress-checklist.js +70 -0
- package/dist/ui/provider-mark.d.ts +32 -0
- package/dist/ui/provider-mark.js +31 -0
- package/dist/ui/quick-reply-chips.d.ts +14 -0
- package/dist/ui/quick-reply-chips.js +24 -0
- package/dist/ui/quote-card.d.ts +39 -0
- package/dist/ui/quote-card.js +66 -0
- package/dist/ui/reasoning-block.d.ts +35 -0
- package/dist/ui/reasoning-block.js +60 -0
- package/dist/ui/research-body.d.ts +70 -0
- package/dist/ui/research-body.js +270 -0
- package/dist/ui/rich-empty-state.d.ts +35 -0
- package/dist/ui/rich-empty-state.js +65 -0
- package/dist/ui/run-now-button.d.ts +23 -0
- package/dist/ui/run-now-button.js +42 -0
- package/dist/ui/section.d.ts +20 -0
- package/dist/ui/section.js +48 -0
- package/dist/ui/select.d.ts +2 -0
- package/dist/ui/select.js +22 -0
- package/dist/ui/selection-checkbox.d.ts +42 -0
- package/dist/ui/selection-checkbox.js +54 -0
- package/dist/ui/settings-panel.d.ts +35 -0
- package/dist/ui/settings-panel.js +68 -0
- package/dist/ui/shimmer-text.d.ts +19 -0
- package/dist/ui/shimmer-text.js +20 -0
- package/dist/ui/sidebar-item-row.d.ts +40 -0
- package/dist/ui/sidebar-item-row.js +64 -0
- package/dist/ui/sidebar-panel.d.ts +25 -0
- package/dist/ui/sidebar-panel.js +56 -0
- package/dist/ui/sidebar.d.ts +46 -0
- package/dist/ui/sidebar.js +111 -0
- package/dist/ui/skeleton.d.ts +13 -0
- package/dist/ui/skeleton.js +20 -0
- package/dist/ui/sortable-table.d.ts +43 -0
- package/dist/ui/sortable-table.js +103 -0
- package/dist/ui/sparkline.d.ts +27 -0
- package/dist/ui/sparkline.js +68 -0
- package/dist/ui/stat-grid.d.ts +55 -0
- package/dist/ui/stat-grid.js +87 -0
- package/dist/ui/status-dot.d.ts +13 -0
- package/dist/ui/status-dot.js +51 -0
- package/dist/ui/step-list.d.ts +21 -0
- package/dist/ui/step-list.js +77 -0
- package/dist/ui/switch.d.ts +23 -0
- package/dist/ui/switch.js +29 -0
- package/dist/ui/table.d.ts +8 -0
- package/dist/ui/table.js +48 -0
- package/dist/ui/tabs.d.ts +44 -0
- package/dist/ui/tabs.js +101 -0
- package/dist/ui/textarea.d.ts +8 -0
- package/dist/ui/textarea.js +32 -0
- package/dist/ui/theme-provider.d.ts +36 -0
- package/dist/ui/theme-provider.js +126 -0
- package/dist/ui/theme-toggle.d.ts +11 -0
- package/dist/ui/theme-toggle.js +32 -0
- package/dist/ui/thinking-indicator.d.ts +7 -0
- package/dist/ui/thinking-indicator.js +19 -0
- package/dist/ui/thinking-label.d.ts +19 -0
- package/dist/ui/thinking-label.js +54 -0
- package/dist/ui/thinking-mark.d.ts +10 -0
- package/dist/ui/thinking-mark.js +153 -0
- package/dist/ui/time-series-chart.d.ts +55 -0
- package/dist/ui/time-series-chart.js +249 -0
- package/dist/ui/toast.d.ts +16 -0
- package/dist/ui/toast.js +41 -0
- package/dist/ui/token-mosaic.d.ts +22 -0
- package/dist/ui/token-mosaic.js +65 -0
- package/dist/ui/tool-block.d.ts +54 -0
- package/dist/ui/tool-block.js +170 -0
- package/dist/ui/tool-narrative.d.ts +22 -0
- package/dist/ui/tool-narrative.js +77 -0
- package/dist/ui/tool-picker.d.ts +44 -0
- package/dist/ui/tool-picker.js +200 -0
- package/dist/ui/tooltip.d.ts +21 -0
- package/dist/ui/tooltip.js +48 -0
- package/dist/ui/top-bar.d.ts +53 -0
- package/dist/ui/top-bar.js +98 -0
- package/dist/ui/trace-waterfall.d.ts +38 -0
- package/dist/ui/trace-waterfall.js +121 -0
- package/dist/ui/typing-indicator.d.ts +9 -0
- package/dist/ui/typing-indicator.js +25 -0
- package/dist/ui/view-toggle.d.ts +17 -0
- package/dist/ui/view-toggle.js +46 -0
- package/dist/ui/voice-waveform.d.ts +16 -0
- package/dist/ui/voice-waveform.js +23 -0
- package/package.json +583 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
/**
|
|
6
|
+
* 4×4 Bayer matrix, normalised to 0–1.
|
|
7
|
+
*
|
|
8
|
+
* Ordered dithering, not error diffusion. Error diffusion (Floyd–Steinberg and
|
|
9
|
+
* friends) is serial — each pixel's error feeds the next — so it cannot be
|
|
10
|
+
* recomputed cheaply every frame, and the pattern crawls when anything moves.
|
|
11
|
+
* An ordered matrix is a pure function of position, so a frame costs one pass
|
|
12
|
+
* and the texture stays locked to the surface instead of swimming.
|
|
13
|
+
*/ const BAYER = [
|
|
14
|
+
[
|
|
15
|
+
0,
|
|
16
|
+
8,
|
|
17
|
+
2,
|
|
18
|
+
10
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
12,
|
|
22
|
+
4,
|
|
23
|
+
14,
|
|
24
|
+
6
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
3,
|
|
28
|
+
11,
|
|
29
|
+
1,
|
|
30
|
+
9
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
15,
|
|
34
|
+
7,
|
|
35
|
+
13,
|
|
36
|
+
5
|
|
37
|
+
]
|
|
38
|
+
].map((row)=>row.map((value)=>(value + 0.5) / 16));
|
|
39
|
+
/**
|
|
40
|
+
* A dithered field that reacts to the pointer — decoration for a full-bleed
|
|
41
|
+
* panel, and nothing else.
|
|
42
|
+
*
|
|
43
|
+
* Drawn on a canvas rather than as a repeating background image because the
|
|
44
|
+
* point is that it *moves*: a radial field drifts slowly, the pointer pushes a
|
|
45
|
+
* bright spot through it, and both need per-frame recomputation. A CSS pattern
|
|
46
|
+
* can be one of those things but not the other.
|
|
47
|
+
*
|
|
48
|
+
* `aria-hidden` and unfocusable, unconditionally. It carries no information;
|
|
49
|
+
* anything a reader needs is in the content laid over it.
|
|
50
|
+
*
|
|
51
|
+
* Reduced motion is honoured by drawing exactly one frame and stopping — not by
|
|
52
|
+
* slowing down and not by hiding it. The texture is the design; the animation is
|
|
53
|
+
* the part that makes some people ill. The theme's global CSS override cannot
|
|
54
|
+
* reach a `requestAnimationFrame` loop, so the check lives here.
|
|
55
|
+
*
|
|
56
|
+
* The canvas is sized in device pixels and scaled by `devicePixelRatio`. Without
|
|
57
|
+
* that the grid is resampled by the compositor on a retina display and the
|
|
58
|
+
* crisp dot pattern — the entire effect — turns to mush.
|
|
59
|
+
*/ export function DitherCanvas({ cell = 4, color, warpRadius = 160, className }) {
|
|
60
|
+
const ref = useRef(null);
|
|
61
|
+
const reduce = usePrefersReducedMotion();
|
|
62
|
+
useEffect(()=>{
|
|
63
|
+
const canvas = ref.current;
|
|
64
|
+
if (canvas === null) return;
|
|
65
|
+
const context = canvas.getContext("2d");
|
|
66
|
+
if (context === null) return;
|
|
67
|
+
// Resolved once per mount: reading a custom property costs a style
|
|
68
|
+
// recalculation, which is not something to do sixty times a second. The
|
|
69
|
+
// fallback reads the canvas's own computed `color` rather than a literal
|
|
70
|
+
// hex — the theme's base layer already ties that to `--foreground`, so
|
|
71
|
+
// the fallback tracks the active theme instead of only being correct in
|
|
72
|
+
// whichever one it was copied from.
|
|
73
|
+
const computed = getComputedStyle(canvas);
|
|
74
|
+
const themeInk = computed.getPropertyValue("--primary").trim();
|
|
75
|
+
const ink = color ?? (themeInk === "" ? computed.color : themeInk);
|
|
76
|
+
const pointer = {
|
|
77
|
+
x: -1e6,
|
|
78
|
+
y: -1e6
|
|
79
|
+
};
|
|
80
|
+
let frame = 0;
|
|
81
|
+
let width = 0;
|
|
82
|
+
let height = 0;
|
|
83
|
+
const resize = ()=>{
|
|
84
|
+
const ratio = window.devicePixelRatio || 1;
|
|
85
|
+
const box = canvas.getBoundingClientRect();
|
|
86
|
+
width = box.width;
|
|
87
|
+
height = box.height;
|
|
88
|
+
canvas.width = Math.max(1, Math.round(width * ratio));
|
|
89
|
+
canvas.height = Math.max(1, Math.round(height * ratio));
|
|
90
|
+
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
91
|
+
};
|
|
92
|
+
const draw = (time)=>{
|
|
93
|
+
context.clearRect(0, 0, width, height);
|
|
94
|
+
context.fillStyle = ink;
|
|
95
|
+
const drift = reduce ? 0 : time / 3000;
|
|
96
|
+
const columns = Math.ceil(width / cell);
|
|
97
|
+
const rows = Math.ceil(height / cell);
|
|
98
|
+
for(let row = 0; row < rows; row += 1){
|
|
99
|
+
for(let column = 0; column < columns; column += 1){
|
|
100
|
+
const x = column * cell;
|
|
101
|
+
const y = row * cell;
|
|
102
|
+
// Base field: a soft diagonal gradient with a slow sine drift, so the
|
|
103
|
+
// panel is darkest at one corner and the grain has somewhere to go.
|
|
104
|
+
const across = (x / Math.max(width, 1) + y / Math.max(height, 1)) / 2;
|
|
105
|
+
let intensity = 0.55 - across * 0.5 + Math.sin(across * 6 + drift) * 0.08;
|
|
106
|
+
if (warpRadius > 0) {
|
|
107
|
+
const dx = x - pointer.x;
|
|
108
|
+
const dy = y - pointer.y;
|
|
109
|
+
const distance = Math.hypot(dx, dy);
|
|
110
|
+
if (distance < warpRadius) {
|
|
111
|
+
// Squared falloff: a linear one leaves a visible disc edge.
|
|
112
|
+
const falloff = 1 - distance / warpRadius;
|
|
113
|
+
intensity += falloff * falloff * 0.55;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// The threshold comparison *is* the dither: a cell is either on or
|
|
117
|
+
// off, and the matrix decides which, so the average over a 4×4 block
|
|
118
|
+
// approximates the continuous intensity.
|
|
119
|
+
const threshold = BAYER[row % 4]?.[column % 4] ?? 0.5;
|
|
120
|
+
if (intensity > threshold) {
|
|
121
|
+
context.fillRect(x, y, cell - 1, cell - 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const loop = (time)=>{
|
|
127
|
+
draw(time);
|
|
128
|
+
frame = requestAnimationFrame(loop);
|
|
129
|
+
};
|
|
130
|
+
const onPointerMove = (event)=>{
|
|
131
|
+
const box = canvas.getBoundingClientRect();
|
|
132
|
+
pointer.x = event.clientX - box.left;
|
|
133
|
+
pointer.y = event.clientY - box.top;
|
|
134
|
+
};
|
|
135
|
+
const onPointerLeave = ()=>{
|
|
136
|
+
pointer.x = -1e6;
|
|
137
|
+
pointer.y = -1e6;
|
|
138
|
+
};
|
|
139
|
+
resize();
|
|
140
|
+
const observer = new ResizeObserver(()=>{
|
|
141
|
+
resize();
|
|
142
|
+
if (reduce) draw(0);
|
|
143
|
+
});
|
|
144
|
+
observer.observe(canvas);
|
|
145
|
+
if (reduce) {
|
|
146
|
+
draw(0);
|
|
147
|
+
} else {
|
|
148
|
+
frame = requestAnimationFrame(loop);
|
|
149
|
+
canvas.addEventListener("pointermove", onPointerMove);
|
|
150
|
+
canvas.addEventListener("pointerleave", onPointerLeave);
|
|
151
|
+
}
|
|
152
|
+
return ()=>{
|
|
153
|
+
cancelAnimationFrame(frame);
|
|
154
|
+
observer.disconnect();
|
|
155
|
+
canvas.removeEventListener("pointermove", onPointerMove);
|
|
156
|
+
canvas.removeEventListener("pointerleave", onPointerLeave);
|
|
157
|
+
};
|
|
158
|
+
}, [
|
|
159
|
+
cell,
|
|
160
|
+
color,
|
|
161
|
+
warpRadius,
|
|
162
|
+
reduce
|
|
163
|
+
]);
|
|
164
|
+
return /*#__PURE__*/ _jsx("canvas", {
|
|
165
|
+
ref: ref,
|
|
166
|
+
"aria-hidden": true,
|
|
167
|
+
className: cn("block size-full", className)
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type EmbedBodyProps = {
|
|
2
|
+
/** Must be `https:`. Anything else renders the fallback notice. */
|
|
3
|
+
readonly url: string;
|
|
4
|
+
/** Names the frame for assistive tech. Required — an unnamed frame is unnavigable. */
|
|
5
|
+
readonly title: string;
|
|
6
|
+
readonly description?: string;
|
|
7
|
+
/** Offers the original file alongside the embed, when the host serves one. */
|
|
8
|
+
readonly downloadUrl?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Frame shape. `wide` is 16:9 for slides; `tall` suits paged documents, where
|
|
11
|
+
* a 16:9 box shows a third of a page and forces scrolling inside a scroller.
|
|
12
|
+
*/
|
|
13
|
+
readonly aspect?: "wide" | "tall";
|
|
14
|
+
readonly className?: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A third-party URL, framed.
|
|
18
|
+
*
|
|
19
|
+
* One component covers every embedded-viewer kind — slide decks, hosted
|
|
20
|
+
* documents, rendered previews. They differ only in the URL and the frame's
|
|
21
|
+
* proportions, and three near-identical files is how three near-identical
|
|
22
|
+
* sandboxes drift apart until one of them is wrong.
|
|
23
|
+
*
|
|
24
|
+
* The escape hatch below the frame is not decoration. Embedding is the thing
|
|
25
|
+
* most likely to be blocked by a frame-ancestors policy, an extension or a
|
|
26
|
+
* login wall, and an iframe that renders blank fires no error we can catch — so
|
|
27
|
+
* the direct link is always present rather than being a fallback we would have
|
|
28
|
+
* to detect our way into.
|
|
29
|
+
*/
|
|
30
|
+
export declare function EmbedBody({ url, title, description, downloadUrl, aspect, className }: EmbedBodyProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ExternalLink } from "lucide-react";
|
|
3
|
+
import { isSafeUrl, toSafeHref } from "../lib/url.js";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { ArtifactNotice } from "./artifact-notice.js";
|
|
6
|
+
import { buttonVariants } from "./button.js";
|
|
7
|
+
/**
|
|
8
|
+
* Everything embedded here is third-party: a deck on someone's hosting, a
|
|
9
|
+
* rendered document, a preview a model produced. None of it is trusted, so the
|
|
10
|
+
* sandbox is opened one capability at a time.
|
|
11
|
+
*
|
|
12
|
+
* `allow-scripts` and `allow-same-origin` together are required for essentially
|
|
13
|
+
* every real embed (slide players and PDF viewers both need them) and *together*
|
|
14
|
+
* they are what lets framed content reach its own origin's storage — which is
|
|
15
|
+
* correct and expected for a third-party player on its own domain. What is
|
|
16
|
+
* withheld is the set that lets the frame act on the *user*:
|
|
17
|
+
* `allow-top-navigation` (redirect the tab out from under them),
|
|
18
|
+
* `allow-popups` (open a tab that outlives the frame), and `allow-forms` (POST
|
|
19
|
+
* credentials somewhere). A viewer needs none of the three.
|
|
20
|
+
*/ const EMBED_SANDBOX = "allow-scripts allow-same-origin allow-presentation";
|
|
21
|
+
const EMBED_ALLOWED_PROTOCOLS = [
|
|
22
|
+
"https:"
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* A third-party URL, framed.
|
|
26
|
+
*
|
|
27
|
+
* One component covers every embedded-viewer kind — slide decks, hosted
|
|
28
|
+
* documents, rendered previews. They differ only in the URL and the frame's
|
|
29
|
+
* proportions, and three near-identical files is how three near-identical
|
|
30
|
+
* sandboxes drift apart until one of them is wrong.
|
|
31
|
+
*
|
|
32
|
+
* The escape hatch below the frame is not decoration. Embedding is the thing
|
|
33
|
+
* most likely to be blocked by a frame-ancestors policy, an extension or a
|
|
34
|
+
* login wall, and an iframe that renders blank fires no error we can catch — so
|
|
35
|
+
* the direct link is always present rather than being a fallback we would have
|
|
36
|
+
* to detect our way into.
|
|
37
|
+
*/ export function EmbedBody({ url, title, description, downloadUrl, aspect = "wide", className }) {
|
|
38
|
+
if (!isSafeUrl(url, EMBED_ALLOWED_PROTOCOLS)) {
|
|
39
|
+
return /*#__PURE__*/ _jsx(ArtifactNotice, {
|
|
40
|
+
message: "This link is missing or is not a secure (https) address, so it cannot be shown."
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const safeDownloadUrl = toSafeHref(downloadUrl);
|
|
44
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
45
|
+
className: cn("flex w-full flex-col gap-3", className),
|
|
46
|
+
children: [
|
|
47
|
+
description === undefined ? null : /*#__PURE__*/ _jsx("p", {
|
|
48
|
+
className: "max-w-prose text-sm text-muted-foreground",
|
|
49
|
+
children: description
|
|
50
|
+
}),
|
|
51
|
+
/*#__PURE__*/ _jsx("div", {
|
|
52
|
+
className: cn("w-full overflow-hidden rounded-lg border border-border bg-card", aspect === "wide" ? "aspect-video" : "aspect-[3/4] max-h-[75vh]"),
|
|
53
|
+
children: /*#__PURE__*/ _jsx("iframe", {
|
|
54
|
+
src: url,
|
|
55
|
+
title: title,
|
|
56
|
+
sandbox: EMBED_SANDBOX,
|
|
57
|
+
allow: "fullscreen",
|
|
58
|
+
className: "size-full border-0"
|
|
59
|
+
})
|
|
60
|
+
}),
|
|
61
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
62
|
+
className: "flex items-center gap-2",
|
|
63
|
+
children: [
|
|
64
|
+
/*#__PURE__*/ _jsxs("a", {
|
|
65
|
+
href: url,
|
|
66
|
+
target: "_blank",
|
|
67
|
+
rel: "noopener noreferrer",
|
|
68
|
+
className: buttonVariants({
|
|
69
|
+
variant: "outline",
|
|
70
|
+
size: "sm"
|
|
71
|
+
}),
|
|
72
|
+
children: [
|
|
73
|
+
/*#__PURE__*/ _jsx(ExternalLink, {
|
|
74
|
+
"aria-hidden": true
|
|
75
|
+
}),
|
|
76
|
+
"Open in a new tab"
|
|
77
|
+
]
|
|
78
|
+
}),
|
|
79
|
+
safeDownloadUrl === undefined ? null : /*#__PURE__*/ _jsx("a", {
|
|
80
|
+
href: safeDownloadUrl,
|
|
81
|
+
download: true,
|
|
82
|
+
className: buttonVariants({
|
|
83
|
+
variant: "ghost",
|
|
84
|
+
size: "sm"
|
|
85
|
+
}),
|
|
86
|
+
children: "Download"
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
})
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type EmptyStateProps = {
|
|
3
|
+
/** What is not here, as a statement: "No workflows yet". */
|
|
4
|
+
readonly title: string;
|
|
5
|
+
/** Why, or what to do about it. One or two sentences. */
|
|
6
|
+
readonly description?: ReactNode;
|
|
7
|
+
readonly icon?: ReactNode;
|
|
8
|
+
/** The way out — a create button, a "clear filters" control. */
|
|
9
|
+
readonly action?: ReactNode;
|
|
10
|
+
readonly className?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Nothing to show, said properly.
|
|
14
|
+
*
|
|
15
|
+
* A shared component because every list otherwise grows its own centred stack
|
|
16
|
+
* of icon-title-body, and they end up differently spaced and differently
|
|
17
|
+
* worded. Three surfaces had already written this inline before it existed.
|
|
18
|
+
*
|
|
19
|
+
* The icon is decorative and hidden from assistive tech: it never carries
|
|
20
|
+
* meaning the title does not already carry, and a screen reader announcing
|
|
21
|
+
* "inbox icon" before "You're all caught up" is noise.
|
|
22
|
+
*
|
|
23
|
+
* Not a `role="status"`. An empty state is the rendered result of a finished
|
|
24
|
+
* load, not an announcement — the surface that was loading owns that.
|
|
25
|
+
*/
|
|
26
|
+
export declare function EmptyState({ title, description, icon, action, className }: EmptyStateProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Nothing to show, said properly.
|
|
5
|
+
*
|
|
6
|
+
* A shared component because every list otherwise grows its own centred stack
|
|
7
|
+
* of icon-title-body, and they end up differently spaced and differently
|
|
8
|
+
* worded. Three surfaces had already written this inline before it existed.
|
|
9
|
+
*
|
|
10
|
+
* The icon is decorative and hidden from assistive tech: it never carries
|
|
11
|
+
* meaning the title does not already carry, and a screen reader announcing
|
|
12
|
+
* "inbox icon" before "You're all caught up" is noise.
|
|
13
|
+
*
|
|
14
|
+
* Not a `role="status"`. An empty state is the rendered result of a finished
|
|
15
|
+
* load, not an announcement — the surface that was loading owns that.
|
|
16
|
+
*/ export function EmptyState({ title, description, icon, action, className }) {
|
|
17
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
18
|
+
"data-slot": "empty-state",
|
|
19
|
+
className: cn("flex flex-col items-center gap-3 px-6 py-12 text-center", className),
|
|
20
|
+
children: [
|
|
21
|
+
icon === undefined ? null : /*#__PURE__*/ _jsx("span", {
|
|
22
|
+
"aria-hidden": true,
|
|
23
|
+
className: "grid size-11 place-items-center rounded-full bg-muted text-muted-foreground [&_svg]:size-5",
|
|
24
|
+
children: icon
|
|
25
|
+
}),
|
|
26
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
27
|
+
className: "flex flex-col gap-1",
|
|
28
|
+
children: [
|
|
29
|
+
/*#__PURE__*/ _jsx("p", {
|
|
30
|
+
className: "text-sm font-medium",
|
|
31
|
+
children: title
|
|
32
|
+
}),
|
|
33
|
+
description === undefined ? null : /*#__PURE__*/ _jsx("p", {
|
|
34
|
+
className: "max-w-prose text-xs leading-relaxed text-muted-foreground",
|
|
35
|
+
children: description
|
|
36
|
+
})
|
|
37
|
+
]
|
|
38
|
+
}),
|
|
39
|
+
action
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
export type FileInputProps = Omit<React.ComponentProps<"input">, "type" | "onChange" | "className"> & {
|
|
3
|
+
readonly onFiles: (files: FileList) => void;
|
|
4
|
+
readonly label?: string;
|
|
5
|
+
readonly hint?: string;
|
|
6
|
+
readonly className?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A drop zone that is also a button that is also a real file input.
|
|
10
|
+
*
|
|
11
|
+
* The `<input type="file">` is the control — visually hidden with `sr-only`,
|
|
12
|
+
* never `display: none`, because a hidden-by-display input is unreachable by
|
|
13
|
+
* keyboard and invisible to assistive tech. The `<label>` wrapping it is what
|
|
14
|
+
* the user sees and clicks, which is also what makes the whole zone activate
|
|
15
|
+
* the picker with no click handler at all.
|
|
16
|
+
*
|
|
17
|
+
* Drag-and-drop is added on top rather than replacing that. A drop-only zone
|
|
18
|
+
* excludes anyone not using a mouse, and every real upload control needs the
|
|
19
|
+
* picker anyway.
|
|
20
|
+
*
|
|
21
|
+
* The input is cleared after each selection so choosing the same file twice
|
|
22
|
+
* still fires — the browser's change event will not repeat for an identical
|
|
23
|
+
* value.
|
|
24
|
+
*/
|
|
25
|
+
export declare function FileInput({ onFiles, label, hint, className, disabled, ...props }: FileInputProps): React.JSX.Element;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Upload } from "lucide-react";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
/**
|
|
6
|
+
* A drop zone that is also a button that is also a real file input.
|
|
7
|
+
*
|
|
8
|
+
* The `<input type="file">` is the control — visually hidden with `sr-only`,
|
|
9
|
+
* never `display: none`, because a hidden-by-display input is unreachable by
|
|
10
|
+
* keyboard and invisible to assistive tech. The `<label>` wrapping it is what
|
|
11
|
+
* the user sees and clicks, which is also what makes the whole zone activate
|
|
12
|
+
* the picker with no click handler at all.
|
|
13
|
+
*
|
|
14
|
+
* Drag-and-drop is added on top rather than replacing that. A drop-only zone
|
|
15
|
+
* excludes anyone not using a mouse, and every real upload control needs the
|
|
16
|
+
* picker anyway.
|
|
17
|
+
*
|
|
18
|
+
* The input is cleared after each selection so choosing the same file twice
|
|
19
|
+
* still fires — the browser's change event will not repeat for an identical
|
|
20
|
+
* value.
|
|
21
|
+
*/ export function FileInput({ onFiles, label = "Choose files", hint, className, disabled, ...props }) {
|
|
22
|
+
const [dragging, setDragging] = useState(false);
|
|
23
|
+
const inputRef = useRef(null);
|
|
24
|
+
return /*#__PURE__*/ _jsxs("label", {
|
|
25
|
+
onDragOver: (event)=>{
|
|
26
|
+
if (disabled === true) return;
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
setDragging(true);
|
|
29
|
+
},
|
|
30
|
+
onDragLeave: ()=>setDragging(false),
|
|
31
|
+
onDrop: (event)=>{
|
|
32
|
+
if (disabled === true) return;
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
setDragging(false);
|
|
35
|
+
if (event.dataTransfer.files.length > 0) onFiles(event.dataTransfer.files);
|
|
36
|
+
},
|
|
37
|
+
className: cn("flex cursor-pointer flex-col items-center gap-1.5 rounded-lg border border-dashed border-input px-6 py-8 text-center transition-colors", dragging && "border-primary-emphasis bg-primary/10", disabled === true ? "cursor-not-allowed opacity-50" : "hover:bg-muted", // The focus ring lives on the hidden input; project it onto the zone,
|
|
38
|
+
// which is the thing the user can actually see.
|
|
39
|
+
"has-[input:focus-visible]:outline has-[input:focus-visible]:outline-2 has-[input:focus-visible]:outline-offset-2 has-[input:focus-visible]:outline-[var(--ring)]", className),
|
|
40
|
+
children: [
|
|
41
|
+
/*#__PURE__*/ _jsx("input", {
|
|
42
|
+
ref: inputRef,
|
|
43
|
+
type: "file",
|
|
44
|
+
disabled: disabled,
|
|
45
|
+
className: "sr-only",
|
|
46
|
+
onChange: (event)=>{
|
|
47
|
+
if (event.target.files !== null && event.target.files.length > 0) onFiles(event.target.files);
|
|
48
|
+
event.target.value = "";
|
|
49
|
+
},
|
|
50
|
+
...props
|
|
51
|
+
}),
|
|
52
|
+
/*#__PURE__*/ _jsx(Upload, {
|
|
53
|
+
className: "size-5 text-muted-foreground",
|
|
54
|
+
"aria-hidden": true
|
|
55
|
+
}),
|
|
56
|
+
/*#__PURE__*/ _jsx("span", {
|
|
57
|
+
className: "text-sm font-medium",
|
|
58
|
+
children: label
|
|
59
|
+
}),
|
|
60
|
+
/*#__PURE__*/ _jsx("span", {
|
|
61
|
+
className: "text-xs text-muted-foreground",
|
|
62
|
+
children: hint ?? "or drag them here"
|
|
63
|
+
})
|
|
64
|
+
]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type FilterOption = {
|
|
3
|
+
readonly value: string;
|
|
4
|
+
readonly label: string;
|
|
5
|
+
};
|
|
6
|
+
export type FilterSpec = {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly label: string;
|
|
9
|
+
readonly options: readonly FilterOption[];
|
|
10
|
+
/** The selected option's value, or `null` for "no filter". */
|
|
11
|
+
readonly value: string | null;
|
|
12
|
+
/** The wording for the unfiltered choice — "All kinds", "Everyone". */
|
|
13
|
+
readonly anyLabel: string;
|
|
14
|
+
};
|
|
15
|
+
export type FilterBarProps = {
|
|
16
|
+
readonly filters: readonly FilterSpec[];
|
|
17
|
+
readonly onChange: (id: string, value: string | null) => void;
|
|
18
|
+
/** Rendered at the end — a time range, an export button. */
|
|
19
|
+
readonly children?: ReactNode;
|
|
20
|
+
readonly className?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* A row of filters, and — the part that is usually missing — a readable summary
|
|
24
|
+
* of which ones are on.
|
|
25
|
+
*
|
|
26
|
+
* The chips are not decoration. A `<select>` collapsed to its chosen value is
|
|
27
|
+
* easy to scroll past, and "why is this dashboard empty" is almost always a
|
|
28
|
+
* filter someone forgot they set three tabs ago. The chips say what is applied
|
|
29
|
+
* in a sentence-shaped row, each one removable, with a single "Clear all"
|
|
30
|
+
* beside them.
|
|
31
|
+
*
|
|
32
|
+
* Native `<select>`, not a custom listbox. A hand-built one has to re-implement
|
|
33
|
+
* type-ahead, arrow keys, page-up, the mobile wheel and the platform's own
|
|
34
|
+
* rendering, and the usual result is a control that looks better and works
|
|
35
|
+
* worse. When a filter genuinely needs search or multi-select, that is a
|
|
36
|
+
* different control — reach for the command palette, not a bigger select.
|
|
37
|
+
*
|
|
38
|
+
* Filter state lives with the caller because it almost always also lives in the
|
|
39
|
+
* URL. Owning it here would mean every consumer fights the component to make
|
|
40
|
+
* their filters linkable.
|
|
41
|
+
*/
|
|
42
|
+
export declare function FilterBar({ filters, onChange, children, className }: FilterBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
import { Button } from "./button.js";
|
|
5
|
+
/**
|
|
6
|
+
* A row of filters, and — the part that is usually missing — a readable summary
|
|
7
|
+
* of which ones are on.
|
|
8
|
+
*
|
|
9
|
+
* The chips are not decoration. A `<select>` collapsed to its chosen value is
|
|
10
|
+
* easy to scroll past, and "why is this dashboard empty" is almost always a
|
|
11
|
+
* filter someone forgot they set three tabs ago. The chips say what is applied
|
|
12
|
+
* in a sentence-shaped row, each one removable, with a single "Clear all"
|
|
13
|
+
* beside them.
|
|
14
|
+
*
|
|
15
|
+
* Native `<select>`, not a custom listbox. A hand-built one has to re-implement
|
|
16
|
+
* type-ahead, arrow keys, page-up, the mobile wheel and the platform's own
|
|
17
|
+
* rendering, and the usual result is a control that looks better and works
|
|
18
|
+
* worse. When a filter genuinely needs search or multi-select, that is a
|
|
19
|
+
* different control — reach for the command palette, not a bigger select.
|
|
20
|
+
*
|
|
21
|
+
* Filter state lives with the caller because it almost always also lives in the
|
|
22
|
+
* URL. Owning it here would mean every consumer fights the component to make
|
|
23
|
+
* their filters linkable.
|
|
24
|
+
*/ export function FilterBar({ filters, onChange, children, className }) {
|
|
25
|
+
const active = filters.filter((filter)=>filter.value !== null);
|
|
26
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
27
|
+
className: cn("flex flex-col gap-2", className),
|
|
28
|
+
children: [
|
|
29
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
30
|
+
className: "flex flex-wrap items-center gap-3",
|
|
31
|
+
children: [
|
|
32
|
+
filters.map((filter)=>/*#__PURE__*/ _jsxs("label", {
|
|
33
|
+
className: "flex items-center gap-1.5 text-xs text-muted-foreground",
|
|
34
|
+
children: [
|
|
35
|
+
filter.label,
|
|
36
|
+
/*#__PURE__*/ _jsxs("select", {
|
|
37
|
+
value: filter.value ?? "",
|
|
38
|
+
onChange: (event)=>onChange(filter.id, event.target.value === "" ? null : event.target.value),
|
|
39
|
+
className: "h-8 rounded-md border border-input bg-card px-2 text-xs text-foreground",
|
|
40
|
+
children: [
|
|
41
|
+
/*#__PURE__*/ _jsx("option", {
|
|
42
|
+
value: "",
|
|
43
|
+
children: filter.anyLabel
|
|
44
|
+
}),
|
|
45
|
+
filter.options.map((option)=>/*#__PURE__*/ _jsx("option", {
|
|
46
|
+
value: option.value,
|
|
47
|
+
children: option.label
|
|
48
|
+
}, option.value))
|
|
49
|
+
]
|
|
50
|
+
})
|
|
51
|
+
]
|
|
52
|
+
}, filter.id)),
|
|
53
|
+
children
|
|
54
|
+
]
|
|
55
|
+
}),
|
|
56
|
+
active.length === 0 ? null : /*#__PURE__*/ _jsxs("div", {
|
|
57
|
+
className: "flex flex-wrap items-center gap-2",
|
|
58
|
+
children: [
|
|
59
|
+
active.map((filter)=>{
|
|
60
|
+
const option = filter.options.find((candidate)=>candidate.value === filter.value);
|
|
61
|
+
return /*#__PURE__*/ _jsxs("button", {
|
|
62
|
+
type: "button",
|
|
63
|
+
onClick: ()=>onChange(filter.id, null),
|
|
64
|
+
// The whole chip is the remove control, and its accessible name
|
|
65
|
+
// says so — an "×" alone is announced as "times".
|
|
66
|
+
"aria-label": `Remove filter ${filter.label}: ${option?.label ?? filter.value}`,
|
|
67
|
+
className: "inline-flex items-center gap-1 rounded-sm bg-muted px-2 py-0.5 text-xs text-muted-foreground transition-colors hover:text-foreground",
|
|
68
|
+
children: [
|
|
69
|
+
/*#__PURE__*/ _jsx("span", {
|
|
70
|
+
className: "font-medium text-foreground",
|
|
71
|
+
children: filter.label
|
|
72
|
+
}),
|
|
73
|
+
option?.label ?? filter.value,
|
|
74
|
+
/*#__PURE__*/ _jsx(X, {
|
|
75
|
+
className: "size-3",
|
|
76
|
+
"aria-hidden": true
|
|
77
|
+
})
|
|
78
|
+
]
|
|
79
|
+
}, filter.id);
|
|
80
|
+
}),
|
|
81
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
82
|
+
type: "button",
|
|
83
|
+
variant: "ghost",
|
|
84
|
+
size: "sm",
|
|
85
|
+
className: "h-6 px-2 text-xs",
|
|
86
|
+
onClick: ()=>{
|
|
87
|
+
for (const filter of active)onChange(filter.id, null);
|
|
88
|
+
},
|
|
89
|
+
children: "Clear all"
|
|
90
|
+
})
|
|
91
|
+
]
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type FilterChipProps = {
|
|
3
|
+
readonly selected?: boolean;
|
|
4
|
+
readonly onClick?: () => void;
|
|
5
|
+
readonly children: ReactNode;
|
|
6
|
+
readonly count?: number;
|
|
7
|
+
readonly className?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A toggleable filter pill — a category, a tag, a facet value. A real
|
|
11
|
+
* `<button>` with `aria-pressed`, not a styled label, so the selection state
|
|
12
|
+
* is announced and the control is reachable with Space/Enter alone.
|
|
13
|
+
*/
|
|
14
|
+
export declare function FilterChip({ selected, onClick, children, count, className }: FilterChipProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* A toggleable filter pill — a category, a tag, a facet value. A real
|
|
5
|
+
* `<button>` with `aria-pressed`, not a styled label, so the selection state
|
|
6
|
+
* is announced and the control is reachable with Space/Enter alone.
|
|
7
|
+
*/ export function FilterChip({ selected = false, onClick, children, count, className }) {
|
|
8
|
+
return /*#__PURE__*/ _jsxs("button", {
|
|
9
|
+
type: "button",
|
|
10
|
+
"aria-pressed": selected,
|
|
11
|
+
onClick: onClick,
|
|
12
|
+
className: cn(// active:brightness-95 mirrors Button's press state: it lands on
|
|
13
|
+
// pointer-down, so it sits outside transition-colors.
|
|
14
|
+
"inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs font-semibold transition-colors ease-out active:brightness-95", selected ? "border-border bg-card text-foreground shadow-sm" : "border-border bg-transparent text-muted-foreground hover:bg-muted hover:text-foreground", className),
|
|
15
|
+
children: [
|
|
16
|
+
children,
|
|
17
|
+
count === undefined ? null : /*#__PURE__*/ _jsx("span", {
|
|
18
|
+
className: cn("text-[10.5px] font-bold", selected ? "text-primary-emphasis" : "text-muted-foreground"),
|
|
19
|
+
children: count
|
|
20
|
+
})
|
|
21
|
+
]
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type GateShellModel } from "../lib/workflow-registry.js";
|
|
3
|
+
export type GateBlockProps = {
|
|
4
|
+
readonly gate: GateShellModel;
|
|
5
|
+
/** The interactive payload — a review list, choice buttons, a form. */
|
|
6
|
+
readonly children?: ReactNode;
|
|
7
|
+
readonly footer?: ReactNode;
|
|
8
|
+
readonly className?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Presentational chrome for a pending gate: a run parked on a human
|
|
12
|
+
* decision. The interactive payload is entirely the caller's — this
|
|
13
|
+
* component only frames it, so the resume-payload contract stays wherever
|
|
14
|
+
* the host's signal-sending logic already lives.
|
|
15
|
+
*/
|
|
16
|
+
export declare function GateBlock({ gate, children, footer, className }: GateBlockProps): import("react").JSX.Element;
|