@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,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ArrowLeft } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
import { EmptyState } from "./empty-state.js";
|
|
5
|
+
/**
|
|
6
|
+
* Index on the left, the open item on the right.
|
|
7
|
+
*
|
|
8
|
+
* Two panes above `lg`, one below it. On a narrow screen the detail *replaces*
|
|
9
|
+
* the list rather than sitting under it: a phone-width column showing both
|
|
10
|
+
* means the user opens something and sees no change without scrolling.
|
|
11
|
+
*
|
|
12
|
+
* That swap is why `onCloseDetail` is required rather than optional. Once the
|
|
13
|
+
* list is hidden, the back control is the only way out — a caller who omitted
|
|
14
|
+
* it would ship a dead end on mobile, so the type does not let them.
|
|
15
|
+
*
|
|
16
|
+
* Both panes scroll independently, so reading a long detail does not scroll the
|
|
17
|
+
* list out from under the selection.
|
|
18
|
+
*/ export function ListDetail({ list, detail, onCloseDetail, placeholder, detailLabel = "Details", className }) {
|
|
19
|
+
const open = detail !== null;
|
|
20
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
21
|
+
className: cn("flex min-h-0 flex-1 gap-6", className),
|
|
22
|
+
children: [
|
|
23
|
+
/*#__PURE__*/ _jsx("div", {
|
|
24
|
+
className: cn("min-h-0 flex-1 overflow-y-auto lg:max-w-md lg:shrink-0 lg:border-r lg:border-border lg:pr-6", // Hidden, not merely visually stacked: leaving it in the tree on
|
|
25
|
+
// mobile would keep every row in the tab order behind the detail.
|
|
26
|
+
open && "hidden lg:block"),
|
|
27
|
+
children: list
|
|
28
|
+
}),
|
|
29
|
+
/*#__PURE__*/ _jsx("section", {
|
|
30
|
+
"aria-label": detailLabel,
|
|
31
|
+
className: cn("min-h-0 min-w-0 flex-1 overflow-y-auto", !open && "hidden lg:block"),
|
|
32
|
+
children: open ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
33
|
+
children: [
|
|
34
|
+
/*#__PURE__*/ _jsxs("button", {
|
|
35
|
+
type: "button",
|
|
36
|
+
onClick: onCloseDetail,
|
|
37
|
+
className: "mb-3 inline-flex items-center gap-1.5 rounded-md text-sm text-muted-foreground transition-colors hover:text-foreground lg:hidden",
|
|
38
|
+
children: [
|
|
39
|
+
/*#__PURE__*/ _jsx(ArrowLeft, {
|
|
40
|
+
className: "size-4",
|
|
41
|
+
"aria-hidden": true
|
|
42
|
+
}),
|
|
43
|
+
"Back to list"
|
|
44
|
+
]
|
|
45
|
+
}),
|
|
46
|
+
detail
|
|
47
|
+
]
|
|
48
|
+
}) : placeholder ?? /*#__PURE__*/ _jsx(EmptyState, {
|
|
49
|
+
title: "Nothing selected",
|
|
50
|
+
description: "Pick something from the list."
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type LiveRunBannerProps = {
|
|
2
|
+
readonly awaiting?: boolean;
|
|
3
|
+
readonly message?: string;
|
|
4
|
+
readonly elapsed?: string;
|
|
5
|
+
readonly className?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The one-line status strip above a live run's steps: awaiting gets the
|
|
9
|
+
* accent tone that marks "this needs you" everywhere else in the registry,
|
|
10
|
+
* running gets the info tone. Standalone so a consumer building its own run
|
|
11
|
+
* surface can reuse just this strip.
|
|
12
|
+
*/
|
|
13
|
+
export declare function LiveRunBanner({ awaiting, message, elapsed, className }: LiveRunBannerProps): 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
|
+
* The one-line status strip above a live run's steps: awaiting gets the
|
|
5
|
+
* accent tone that marks "this needs you" everywhere else in the registry,
|
|
6
|
+
* running gets the info tone. Standalone so a consumer building its own run
|
|
7
|
+
* surface can reuse just this strip.
|
|
8
|
+
*/ export function LiveRunBanner({ awaiting = false, message, elapsed, className }) {
|
|
9
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
10
|
+
role: "status",
|
|
11
|
+
className: cn("mb-3 flex flex-wrap items-center justify-between gap-2 rounded-lg border px-3 py-2 text-xs", awaiting ? "border-primary-emphasis/35 bg-primary/10 text-primary-emphasis" : "border-accent bg-accent text-accent-foreground", className),
|
|
12
|
+
children: [
|
|
13
|
+
/*#__PURE__*/ _jsx("span", {
|
|
14
|
+
className: "font-semibold",
|
|
15
|
+
children: message ?? (awaiting ? "Paused for your input" : "Running")
|
|
16
|
+
}),
|
|
17
|
+
elapsed === undefined ? null : /*#__PURE__*/ _jsx("span", {
|
|
18
|
+
className: "font-mono text-[11.5px] tabular-nums opacity-90",
|
|
19
|
+
children: elapsed
|
|
20
|
+
})
|
|
21
|
+
]
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type LiveStatusLineProps = {
|
|
2
|
+
readonly label: string;
|
|
3
|
+
readonly className?: string;
|
|
4
|
+
};
|
|
5
|
+
/** The single "what's happening now" line under a stepper. A spinning ring
|
|
6
|
+
* plus text, not colour alone — `motion-reduce:animate-none` stills the
|
|
7
|
+
* spinner without hiding the line, since the words already carry the
|
|
8
|
+
* meaning. */
|
|
9
|
+
export declare function LiveStatusLine({ label, className }: LiveStatusLineProps): import("react").JSX.Element;
|
|
10
|
+
export type LiveStatusSlotProps = {
|
|
11
|
+
/** `null` animates the line out and renders nothing. */
|
|
12
|
+
readonly label: string | null;
|
|
13
|
+
readonly className?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Layout-stable wrapper for `LiveStatusLine`: a `null` label collapses the
|
|
17
|
+
* slot's height instead of the line disappearing instantly, so the body
|
|
18
|
+
* below it doesn't jump at every gate-to-processing boundary. The transition
|
|
19
|
+
* is a plain CSS grid-rows trick — no animation library — and
|
|
20
|
+
* `prefers-reduced-motion` disables it globally via the theme's base layer.
|
|
21
|
+
*/
|
|
22
|
+
export declare function LiveStatusSlot({ label, className }: LiveStatusSlotProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
/** The single "what's happening now" line under a stepper. A spinning ring
|
|
4
|
+
* plus text, not colour alone — `motion-reduce:animate-none` stills the
|
|
5
|
+
* spinner without hiding the line, since the words already carry the
|
|
6
|
+
* meaning. */ export function LiveStatusLine({ label, className }) {
|
|
7
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
8
|
+
role: "status",
|
|
9
|
+
"aria-live": "polite",
|
|
10
|
+
className: cn("flex items-center gap-2 border-b border-border bg-card px-6 py-2.5 text-sm text-muted-foreground", className),
|
|
11
|
+
children: [
|
|
12
|
+
/*#__PURE__*/ _jsx("span", {
|
|
13
|
+
"aria-hidden": true,
|
|
14
|
+
className: "size-3 animate-spin rounded-full border-2 border-border border-t-primary-emphasis motion-reduce:animate-none"
|
|
15
|
+
}),
|
|
16
|
+
label,
|
|
17
|
+
"…"
|
|
18
|
+
]
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Layout-stable wrapper for `LiveStatusLine`: a `null` label collapses the
|
|
23
|
+
* slot's height instead of the line disappearing instantly, so the body
|
|
24
|
+
* below it doesn't jump at every gate-to-processing boundary. The transition
|
|
25
|
+
* is a plain CSS grid-rows trick — no animation library — and
|
|
26
|
+
* `prefers-reduced-motion` disables it globally via the theme's base layer.
|
|
27
|
+
*/ export function LiveStatusSlot({ label, className }) {
|
|
28
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
29
|
+
className: cn("grid transition-[grid-template-rows] duration-200 ease-out", label === null ? "grid-rows-[0fr]" : "grid-rows-[1fr]", className),
|
|
30
|
+
children: /*#__PURE__*/ _jsx("div", {
|
|
31
|
+
className: "overflow-hidden",
|
|
32
|
+
children: label === null ? null : /*#__PURE__*/ _jsx(LiveStatusLine, {
|
|
33
|
+
label: label
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* The popover menu used by toolbars and selectors.
|
|
5
|
+
*
|
|
6
|
+
* Radix owns anchored positioning, focus management, keyboard navigation and
|
|
7
|
+
* dismissal; this layer only paints. Every menu gets the same fully opaque
|
|
8
|
+
* popover surface — a see-through menu over a busy page is the bug this
|
|
9
|
+
* primitive exists to prevent — and the same item hover, so a menu opened from
|
|
10
|
+
* a toolbar and one opened from a table row read as the same control.
|
|
11
|
+
*/
|
|
12
|
+
export declare const Menu: React.FC<DropdownMenuPrimitive.DropdownMenuProps>;
|
|
13
|
+
export declare const MenuTrigger: React.ForwardRefExoticComponent<DropdownMenuPrimitive.DropdownMenuTriggerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
14
|
+
export declare const MenuGroup: React.ForwardRefExoticComponent<DropdownMenuPrimitive.DropdownMenuGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
15
|
+
export declare function MenuContent({ className, sideOffset, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>): React.JSX.Element;
|
|
16
|
+
export declare function MenuItem({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Item>): React.JSX.Element;
|
|
17
|
+
export declare function MenuLabel({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Label>): React.JSX.Element;
|
|
18
|
+
export declare function MenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>): React.JSX.Element;
|
package/dist/ui/menu.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* The popover menu used by toolbars and selectors.
|
|
6
|
+
*
|
|
7
|
+
* Radix owns anchored positioning, focus management, keyboard navigation and
|
|
8
|
+
* dismissal; this layer only paints. Every menu gets the same fully opaque
|
|
9
|
+
* popover surface — a see-through menu over a busy page is the bug this
|
|
10
|
+
* primitive exists to prevent — and the same item hover, so a menu opened from
|
|
11
|
+
* a toolbar and one opened from a table row read as the same control.
|
|
12
|
+
*/ export const Menu = DropdownMenuPrimitive.Root;
|
|
13
|
+
export const MenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
14
|
+
export const MenuGroup = DropdownMenuPrimitive.Group;
|
|
15
|
+
export function MenuContent({ className, sideOffset = 4, ...props }) {
|
|
16
|
+
return /*#__PURE__*/ _jsx(DropdownMenuPrimitive.Portal, {
|
|
17
|
+
children: /*#__PURE__*/ _jsx(DropdownMenuPrimitive.Content, {
|
|
18
|
+
"data-slot": "menu-content",
|
|
19
|
+
sideOffset: sideOffset,
|
|
20
|
+
className: cn("z-50 min-w-32 overflow-hidden rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg", className),
|
|
21
|
+
...props
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function MenuItem({ className, ...props }) {
|
|
26
|
+
return /*#__PURE__*/ _jsx(DropdownMenuPrimitive.Item, {
|
|
27
|
+
"data-slot": "menu-item",
|
|
28
|
+
className: cn("flex w-full cursor-pointer items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm outline-none select-none transition-colors data-[highlighted]:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className),
|
|
29
|
+
...props
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function MenuLabel({ className, ...props }) {
|
|
33
|
+
return /*#__PURE__*/ _jsx(DropdownMenuPrimitive.Label, {
|
|
34
|
+
"data-slot": "menu-label",
|
|
35
|
+
className: cn("px-2.5 py-1.5 text-xs font-semibold text-muted-foreground", className),
|
|
36
|
+
...props
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function MenuSeparator({ className, ...props }) {
|
|
40
|
+
return /*#__PURE__*/ _jsx(DropdownMenuPrimitive.Separator, {
|
|
41
|
+
"data-slot": "menu-separator",
|
|
42
|
+
className: cn("my-1 h-px bg-border", className),
|
|
43
|
+
...props
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ChatMessage } from "../lib/chat-message.js";
|
|
3
|
+
export type MessageBubbleProps = {
|
|
4
|
+
readonly message: ChatMessage;
|
|
5
|
+
/** Retry affordance for a failed send. Omit and a failure is only reported. */
|
|
6
|
+
readonly onRetry?: () => void;
|
|
7
|
+
readonly now?: number;
|
|
8
|
+
/** Rich body — a markdown renderer. Defaults to the message's plain text. */
|
|
9
|
+
readonly children?: ReactNode;
|
|
10
|
+
readonly className?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* One message, as said by whoever said it.
|
|
14
|
+
*
|
|
15
|
+
* Only the user's messages get a filled bubble. An agent reply is set on the
|
|
16
|
+
* page ground with no container: agent turns are long, and a 600-word tinted
|
|
17
|
+
* block is harder to read than plain text, while the alternating alignment that
|
|
18
|
+
* makes chat apps legible comes from the user side alone.
|
|
19
|
+
*
|
|
20
|
+
* Text is rendered as text. Passing agent output through a markdown or HTML
|
|
21
|
+
* renderer is the host's decision to make, with the host's sanitiser — pass one
|
|
22
|
+
* as `children`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function MessageBubble({ message, onRetry, now, children, className }: MessageBubbleProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { messageText } from "../lib/chat-message.js";
|
|
3
|
+
import { formatRelativeTime } from "../lib/relative-time.js";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
/**
|
|
6
|
+
* One message, as said by whoever said it.
|
|
7
|
+
*
|
|
8
|
+
* Only the user's messages get a filled bubble. An agent reply is set on the
|
|
9
|
+
* page ground with no container: agent turns are long, and a 600-word tinted
|
|
10
|
+
* block is harder to read than plain text, while the alternating alignment that
|
|
11
|
+
* makes chat apps legible comes from the user side alone.
|
|
12
|
+
*
|
|
13
|
+
* Text is rendered as text. Passing agent output through a markdown or HTML
|
|
14
|
+
* renderer is the host's decision to make, with the host's sanitiser — pass one
|
|
15
|
+
* as `children`.
|
|
16
|
+
*/ export function MessageBubble({ message, onRetry, now, children, className }) {
|
|
17
|
+
const isUser = message.role === "user";
|
|
18
|
+
const isSystem = message.role === "system";
|
|
19
|
+
const failed = message.status === "failed";
|
|
20
|
+
if (isSystem) {
|
|
21
|
+
return /*#__PURE__*/ _jsx("p", {
|
|
22
|
+
"data-slot": "message-bubble",
|
|
23
|
+
className: cn("px-2 py-1 text-center text-xs text-muted-foreground", className),
|
|
24
|
+
children: children ?? messageText(message)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
28
|
+
"data-slot": "message-bubble",
|
|
29
|
+
className: cn("flex flex-col gap-1", isUser ? "items-end" : "items-start", className),
|
|
30
|
+
children: [
|
|
31
|
+
/*#__PURE__*/ _jsx("div", {
|
|
32
|
+
className: cn("max-w-[46rem] text-sm leading-relaxed break-words whitespace-pre-wrap", isUser && "rounded-lg bg-muted px-3 py-2", failed && "opacity-60"),
|
|
33
|
+
children: children ?? messageText(message)
|
|
34
|
+
}),
|
|
35
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
36
|
+
className: "flex items-center gap-2 text-[11px] text-muted-foreground",
|
|
37
|
+
children: [
|
|
38
|
+
/*#__PURE__*/ _jsx("time", {
|
|
39
|
+
dateTime: message.createdAt,
|
|
40
|
+
children: formatRelativeTime(message.createdAt, now)
|
|
41
|
+
}),
|
|
42
|
+
message.status === "sending" ? /*#__PURE__*/ _jsx("span", {
|
|
43
|
+
children: "Sending…"
|
|
44
|
+
}) : null,
|
|
45
|
+
failed ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
46
|
+
children: [
|
|
47
|
+
/*#__PURE__*/ _jsx("span", {
|
|
48
|
+
role: "alert",
|
|
49
|
+
className: "text-destructive",
|
|
50
|
+
children: "Not sent"
|
|
51
|
+
}),
|
|
52
|
+
onRetry === undefined ? null : /*#__PURE__*/ _jsx("button", {
|
|
53
|
+
type: "button",
|
|
54
|
+
onClick: onRetry,
|
|
55
|
+
className: "text-primary-emphasis hover:underline",
|
|
56
|
+
children: "Retry"
|
|
57
|
+
})
|
|
58
|
+
]
|
|
59
|
+
}) : null
|
|
60
|
+
]
|
|
61
|
+
})
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type DictationState = "idle" | "starting" | "listening" | "denied" | "unsupported";
|
|
2
|
+
export type MicButtonProps = {
|
|
3
|
+
readonly state: DictationState;
|
|
4
|
+
readonly onToggle: () => void;
|
|
5
|
+
readonly disabled?: boolean;
|
|
6
|
+
readonly className?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A microphone toggle for dictation, presentational only.
|
|
10
|
+
*
|
|
11
|
+
* This component and its siblings (`VoiceWaveform`, `DictationStatusLine`,
|
|
12
|
+
* `MicPermissionDialog`) carry no `SpeechRecognition`, Tauri, or media-device
|
|
13
|
+
* code — react-ui never imports a browser or native capture API. A consumer
|
|
14
|
+
* wires them to a backend through a hook shaped roughly like:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* type UseDictation = (value: string, onValueChange: (value: string) => void) => {
|
|
18
|
+
* supported: boolean;
|
|
19
|
+
* state: DictationState;
|
|
20
|
+
* levels: number[]; // recent RMS samples, 0..1, oldest first
|
|
21
|
+
* errorMessage: string | null; // set when state is "denied"
|
|
22
|
+
* permissionHelp: { title: string; detail: string; action: string } | null;
|
|
23
|
+
* start: () => void;
|
|
24
|
+
* stop: () => void;
|
|
25
|
+
* dismiss: () => void;
|
|
26
|
+
* };
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* `state` drives the pressed look, the glyph, and the label. Listening is
|
|
30
|
+
* green with a stop square so colour is never the only channel; starting
|
|
31
|
+
* stays the idle mic so it cannot be mistaken for a live capture.
|
|
32
|
+
* `unsupported` is disabled. `denied` stays clickable so the host can open
|
|
33
|
+
* `MicPermissionDialog` from `onToggle`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function MicButton({ state, onToggle, disabled, className }: MicButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Mic, MicOff, Square } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
function labelFor(state) {
|
|
5
|
+
if (state === "denied") return "Microphone blocked";
|
|
6
|
+
if (state === "unsupported") return "Dictation unavailable";
|
|
7
|
+
if (state === "listening") return "Stop dictating";
|
|
8
|
+
if (state === "starting") return "Starting microphone";
|
|
9
|
+
return "Dictate instead of typing";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A microphone toggle for dictation, presentational only.
|
|
13
|
+
*
|
|
14
|
+
* This component and its siblings (`VoiceWaveform`, `DictationStatusLine`,
|
|
15
|
+
* `MicPermissionDialog`) carry no `SpeechRecognition`, Tauri, or media-device
|
|
16
|
+
* code — react-ui never imports a browser or native capture API. A consumer
|
|
17
|
+
* wires them to a backend through a hook shaped roughly like:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* type UseDictation = (value: string, onValueChange: (value: string) => void) => {
|
|
21
|
+
* supported: boolean;
|
|
22
|
+
* state: DictationState;
|
|
23
|
+
* levels: number[]; // recent RMS samples, 0..1, oldest first
|
|
24
|
+
* errorMessage: string | null; // set when state is "denied"
|
|
25
|
+
* permissionHelp: { title: string; detail: string; action: string } | null;
|
|
26
|
+
* start: () => void;
|
|
27
|
+
* stop: () => void;
|
|
28
|
+
* dismiss: () => void;
|
|
29
|
+
* };
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* `state` drives the pressed look, the glyph, and the label. Listening is
|
|
33
|
+
* green with a stop square so colour is never the only channel; starting
|
|
34
|
+
* stays the idle mic so it cannot be mistaken for a live capture.
|
|
35
|
+
* `unsupported` is disabled. `denied` stays clickable so the host can open
|
|
36
|
+
* `MicPermissionDialog` from `onToggle`.
|
|
37
|
+
*/ export function MicButton({ state, onToggle, disabled = false, className }) {
|
|
38
|
+
const listening = state === "listening";
|
|
39
|
+
const denied = state === "denied";
|
|
40
|
+
const unusable = disabled || state === "unsupported";
|
|
41
|
+
const label = labelFor(state);
|
|
42
|
+
const Glyph = listening ? Square : denied ? MicOff : Mic;
|
|
43
|
+
return /*#__PURE__*/ _jsx("button", {
|
|
44
|
+
type: "button",
|
|
45
|
+
// Denied opens the permission dialog rather than toggling — declare the
|
|
46
|
+
// popup and drop the toggle semantic.
|
|
47
|
+
"aria-pressed": denied ? undefined : listening,
|
|
48
|
+
"aria-haspopup": denied ? "dialog" : undefined,
|
|
49
|
+
"aria-label": label,
|
|
50
|
+
disabled: unusable,
|
|
51
|
+
// Tapping the mic must not blur the field being dictated into — a field
|
|
52
|
+
// that commits on blur would lose the edit to its own microphone tap.
|
|
53
|
+
onMouseDown: (event)=>event.preventDefault(),
|
|
54
|
+
onClick: onToggle,
|
|
55
|
+
className: cn("grid size-9 shrink-0 place-items-center rounded-md transition-colors disabled:opacity-50", !unusable && "active:brightness-95 motion-safe:active:scale-[0.97]", listening ? "bg-success text-success-foreground hover:bg-success/90" : "text-muted-foreground hover:bg-muted hover:text-foreground", className),
|
|
56
|
+
children: /*#__PURE__*/ _jsx(Glyph, {
|
|
57
|
+
className: listening ? "size-3.5 fill-current" : "size-4",
|
|
58
|
+
"aria-hidden": true
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type MicPermissionDialogProps = {
|
|
2
|
+
readonly open: boolean;
|
|
3
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly detail: string;
|
|
6
|
+
/** Label for the action button, e.g. "Open Sound settings". */
|
|
7
|
+
readonly actionLabel: string;
|
|
8
|
+
readonly onAction: () => void;
|
|
9
|
+
/** Shown when opening the settings pane itself failed. */
|
|
10
|
+
readonly failure?: string | null;
|
|
11
|
+
readonly dismissLabel?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A refusal that is a setting, put in front of the person with the button
|
|
15
|
+
* that opens the pane. Presentational: it does not know what "open settings"
|
|
16
|
+
* means for a given host (a native shell invoking an OS pane, a browser
|
|
17
|
+
* opening a help link) — the consumer's dictation hook supplies `onAction`
|
|
18
|
+
* and, if that action can itself fail, `failure`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function MicPermissionDialog({ open, onOpenChange, title, detail, actionLabel, onAction, failure, dismissLabel, }: MicPermissionDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "./button.js";
|
|
3
|
+
import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./dialog.js";
|
|
4
|
+
/**
|
|
5
|
+
* A refusal that is a setting, put in front of the person with the button
|
|
6
|
+
* that opens the pane. Presentational: it does not know what "open settings"
|
|
7
|
+
* means for a given host (a native shell invoking an OS pane, a browser
|
|
8
|
+
* opening a help link) — the consumer's dictation hook supplies `onAction`
|
|
9
|
+
* and, if that action can itself fail, `failure`.
|
|
10
|
+
*/ export function MicPermissionDialog({ open, onOpenChange, title, detail, actionLabel, onAction, failure = null, dismissLabel = "Not now" }) {
|
|
11
|
+
return /*#__PURE__*/ _jsx(Dialog, {
|
|
12
|
+
open: open,
|
|
13
|
+
onOpenChange: onOpenChange,
|
|
14
|
+
children: /*#__PURE__*/ _jsxs(DialogContent, {
|
|
15
|
+
children: [
|
|
16
|
+
/*#__PURE__*/ _jsxs(DialogHeader, {
|
|
17
|
+
children: [
|
|
18
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
19
|
+
children: title
|
|
20
|
+
}),
|
|
21
|
+
/*#__PURE__*/ _jsx(DialogDescription, {
|
|
22
|
+
children: detail
|
|
23
|
+
})
|
|
24
|
+
]
|
|
25
|
+
}),
|
|
26
|
+
failure ? /*#__PURE__*/ _jsx(DialogBody, {
|
|
27
|
+
children: /*#__PURE__*/ _jsx("p", {
|
|
28
|
+
role: "alert",
|
|
29
|
+
className: "text-sm text-destructive",
|
|
30
|
+
children: failure
|
|
31
|
+
})
|
|
32
|
+
}) : null,
|
|
33
|
+
/*#__PURE__*/ _jsxs(DialogFooter, {
|
|
34
|
+
children: [
|
|
35
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
36
|
+
variant: "ghost",
|
|
37
|
+
onClick: ()=>onOpenChange(false),
|
|
38
|
+
children: dismissLabel
|
|
39
|
+
}),
|
|
40
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
41
|
+
variant: "primary",
|
|
42
|
+
onClick: onAction,
|
|
43
|
+
children: actionLabel
|
|
44
|
+
})
|
|
45
|
+
]
|
|
46
|
+
})
|
|
47
|
+
]
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type NotificationsBellProps = {
|
|
3
|
+
/** Unread count. `0` renders the bell with no marker. */
|
|
4
|
+
readonly count: number;
|
|
5
|
+
/** Panel contents — a notification list, an empty state, whatever the host has. */
|
|
6
|
+
readonly children: ReactNode;
|
|
7
|
+
/** Counts above this render as "N+" so the marker cannot grow unbounded. */
|
|
8
|
+
readonly maxCount?: number;
|
|
9
|
+
/** Controlled open state. Pair with `onOpenChange` to lift it to a parent. */
|
|
10
|
+
readonly open?: boolean;
|
|
11
|
+
readonly onOpenChange?: (open: boolean) => void;
|
|
12
|
+
readonly className?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Bell + count, with the panel body as a slot. It renders a count and opens a
|
|
16
|
+
* panel; it does not know what a notification is, does not fetch, and does not
|
|
17
|
+
* mark anything read — the host puts a list in `children` and handles the rest.
|
|
18
|
+
*
|
|
19
|
+
* The panel is a `dialog`-role popup rather than a menu: its contents are
|
|
20
|
+
* arbitrary (links, buttons, an empty state), and a menu role would promise
|
|
21
|
+
* arrow-key semantics that a free-form list does not have. Escape closes and
|
|
22
|
+
* returns focus to the bell; a pointer-down outside closes without stealing
|
|
23
|
+
* focus back, so clicking straight into another control still works.
|
|
24
|
+
*
|
|
25
|
+
* Uncontrolled by default. Pass `open`/`onOpenChange` to drive it from a
|
|
26
|
+
* parent — closing every other panel when this one opens, say.
|
|
27
|
+
*/
|
|
28
|
+
export declare function NotificationsBell({ count, children, maxCount, open: openProp, onOpenChange, className, }: NotificationsBellProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Bell } from "lucide-react";
|
|
3
|
+
import { useEffect, useId, useRef } from "react";
|
|
4
|
+
import { useDismissablePopover } from "../hooks/use-dismissable-popover.js";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
/**
|
|
7
|
+
* Bell + count, with the panel body as a slot. It renders a count and opens a
|
|
8
|
+
* panel; it does not know what a notification is, does not fetch, and does not
|
|
9
|
+
* mark anything read — the host puts a list in `children` and handles the rest.
|
|
10
|
+
*
|
|
11
|
+
* The panel is a `dialog`-role popup rather than a menu: its contents are
|
|
12
|
+
* arbitrary (links, buttons, an empty state), and a menu role would promise
|
|
13
|
+
* arrow-key semantics that a free-form list does not have. Escape closes and
|
|
14
|
+
* returns focus to the bell; a pointer-down outside closes without stealing
|
|
15
|
+
* focus back, so clicking straight into another control still works.
|
|
16
|
+
*
|
|
17
|
+
* Uncontrolled by default. Pass `open`/`onOpenChange` to drive it from a
|
|
18
|
+
* parent — closing every other panel when this one opens, say.
|
|
19
|
+
*/ export function NotificationsBell({ count, children, maxCount = 99, open: openProp, onOpenChange, className }) {
|
|
20
|
+
const { open, setOpen, rootRef, triggerRef } = useDismissablePopover({
|
|
21
|
+
open: openProp,
|
|
22
|
+
onOpenChange
|
|
23
|
+
});
|
|
24
|
+
const panelRef = useRef(null);
|
|
25
|
+
const panelId = useId();
|
|
26
|
+
useEffect(()=>{
|
|
27
|
+
if (!open) return;
|
|
28
|
+
panelRef.current?.focus();
|
|
29
|
+
}, [
|
|
30
|
+
open
|
|
31
|
+
]);
|
|
32
|
+
const label = count === 0 ? "Notifications" : `Notifications, ${count} unread`;
|
|
33
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
34
|
+
ref: rootRef,
|
|
35
|
+
className: cn("relative", className),
|
|
36
|
+
children: [
|
|
37
|
+
/*#__PURE__*/ _jsxs("button", {
|
|
38
|
+
ref: triggerRef,
|
|
39
|
+
type: "button",
|
|
40
|
+
"aria-haspopup": "dialog",
|
|
41
|
+
"aria-expanded": open,
|
|
42
|
+
"aria-controls": open ? panelId : undefined,
|
|
43
|
+
"aria-label": label,
|
|
44
|
+
onClick: ()=>setOpen((value)=>!value),
|
|
45
|
+
className: "relative grid size-9 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
46
|
+
children: [
|
|
47
|
+
/*#__PURE__*/ _jsx(Bell, {
|
|
48
|
+
className: "size-4",
|
|
49
|
+
"aria-hidden": true
|
|
50
|
+
}),
|
|
51
|
+
count > 0 ? /*#__PURE__*/ _jsx("span", {
|
|
52
|
+
"aria-hidden": true,
|
|
53
|
+
className: "absolute top-1 right-1 min-w-4 rounded-full bg-primary px-1 text-center font-mono text-[10px] leading-4 text-primary-foreground",
|
|
54
|
+
children: count > maxCount ? `${maxCount}+` : count
|
|
55
|
+
}) : null
|
|
56
|
+
]
|
|
57
|
+
}),
|
|
58
|
+
open ? /*#__PURE__*/ _jsx("div", {
|
|
59
|
+
ref: panelRef,
|
|
60
|
+
id: panelId,
|
|
61
|
+
role: "dialog",
|
|
62
|
+
"aria-label": label,
|
|
63
|
+
tabIndex: -1,
|
|
64
|
+
className: "absolute top-full right-0 z-50 mt-1 max-h-96 w-80 overflow-y-auto rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-lg",
|
|
65
|
+
children: children
|
|
66
|
+
}) : null
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type NowItem } from "../lib/now-item.js";
|
|
3
|
+
export type NowSectionProps = {
|
|
4
|
+
/** How many things are asking for attention — drives the count sentence. */
|
|
5
|
+
readonly count: number;
|
|
6
|
+
/**
|
|
7
|
+
* Tucks the body away and leaves a one-line summary. For when a detail pane
|
|
8
|
+
* is open and the vertical space is worth more to it than to the strip.
|
|
9
|
+
*/
|
|
10
|
+
readonly collapsed?: boolean;
|
|
11
|
+
readonly children: ReactNode;
|
|
12
|
+
readonly className?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The attention band at the top of the inbox. Its own container rather than the
|
|
16
|
+
* generic `Section` because it says something `Section` cannot: an eyebrow that
|
|
17
|
+
* marks urgency, a count in words, and a collapsed mode.
|
|
18
|
+
*
|
|
19
|
+
* Collapsing keeps the body mounted (so it can animate, and so anchors into it
|
|
20
|
+
* still resolve) but marks it `inert` — `aria-hidden` alone would hide it from
|
|
21
|
+
* a screen reader while leaving every card in the tab order.
|
|
22
|
+
*/
|
|
23
|
+
export declare function NowSection({ count, collapsed, children, className }: NowSectionProps): import("react").JSX.Element;
|
|
24
|
+
export type NowCardsProps = {
|
|
25
|
+
readonly items: readonly NowItem[];
|
|
26
|
+
/** Cap on cards shown. The rest belong in the queue below, not here. */
|
|
27
|
+
readonly limit?: number;
|
|
28
|
+
readonly selectedId?: string | null;
|
|
29
|
+
readonly now?: number;
|
|
30
|
+
readonly className?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The highest-attention items as cards. Ordering is not the caller's problem —
|
|
34
|
+
* the list is sorted here by priority then recency, so two surfaces fed the
|
|
35
|
+
* same items always agree on what is most urgent.
|
|
36
|
+
*/
|
|
37
|
+
export declare function NowCards({ items, limit, selectedId, now, className }: NowCardsProps): import("react").JSX.Element | null;
|