@miragon/mcp-toolkit-ui 0.10.1
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/README.md +36 -0
- package/dist/app/adapt-data-widget.d.ts +33 -0
- package/dist/app/adapt-data-widget.d.ts.map +1 -0
- package/dist/app/adapt-data-widget.js +35 -0
- package/dist/app/adapt-data-widget.js.map +1 -0
- package/dist/app/builder/CatalogueSheet.d.ts +13 -0
- package/dist/app/builder/CatalogueSheet.d.ts.map +1 -0
- package/dist/app/builder/CatalogueSheet.js +33 -0
- package/dist/app/builder/CatalogueSheet.js.map +1 -0
- package/dist/app/builder/PipelineStrip.d.ts +22 -0
- package/dist/app/builder/PipelineStrip.d.ts.map +1 -0
- package/dist/app/builder/PipelineStrip.js +31 -0
- package/dist/app/builder/PipelineStrip.js.map +1 -0
- package/dist/app/builder/PreviewPane.d.ts +13 -0
- package/dist/app/builder/PreviewPane.d.ts.map +1 -0
- package/dist/app/builder/PreviewPane.js +21 -0
- package/dist/app/builder/PreviewPane.js.map +1 -0
- package/dist/app/builder/SaveDialog.d.ts +14 -0
- package/dist/app/builder/SaveDialog.d.ts.map +1 -0
- package/dist/app/builder/SaveDialog.js +14 -0
- package/dist/app/builder/SaveDialog.js.map +1 -0
- package/dist/app/builder/Toolbar.d.ts +17 -0
- package/dist/app/builder/Toolbar.d.ts.map +1 -0
- package/dist/app/builder/Toolbar.js +15 -0
- package/dist/app/builder/Toolbar.js.map +1 -0
- package/dist/app/builder/Workspace.d.ts +28 -0
- package/dist/app/builder/Workspace.d.ts.map +1 -0
- package/dist/app/builder/Workspace.js +117 -0
- package/dist/app/builder/Workspace.js.map +1 -0
- package/dist/app/builder/builder-model.d.ts +86 -0
- package/dist/app/builder/builder-model.d.ts.map +1 -0
- package/dist/app/builder/builder-model.js +174 -0
- package/dist/app/builder/builder-model.js.map +1 -0
- package/dist/app/builder/builder-reducer.d.ts +71 -0
- package/dist/app/builder/builder-reducer.d.ts.map +1 -0
- package/dist/app/builder/builder-reducer.js +201 -0
- package/dist/app/builder/builder-reducer.js.map +1 -0
- package/dist/app/builder/labels.d.ts +78 -0
- package/dist/app/builder/labels.d.ts.map +1 -0
- package/dist/app/builder/labels.js +71 -0
- package/dist/app/builder/labels.js.map +1 -0
- package/dist/app/builder/status-chip.d.ts +10 -0
- package/dist/app/builder/status-chip.d.ts.map +1 -0
- package/dist/app/builder/status-chip.js +16 -0
- package/dist/app/builder/status-chip.js.map +1 -0
- package/dist/app/host-bridge.d.ts +211 -0
- package/dist/app/host-bridge.d.ts.map +1 -0
- package/dist/app/host-bridge.js +247 -0
- package/dist/app/host-bridge.js.map +1 -0
- package/dist/app/index.d.ts +16 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +18 -0
- package/dist/app/index.js.map +1 -0
- package/dist/app/layout-builder.d.ts +48 -0
- package/dist/app/layout-builder.d.ts.map +1 -0
- package/dist/app/layout-builder.js +418 -0
- package/dist/app/layout-builder.js.map +1 -0
- package/dist/app/mcp-app-view.d.ts +43 -0
- package/dist/app/mcp-app-view.d.ts.map +1 -0
- package/dist/app/mcp-app-view.js +169 -0
- package/dist/app/mcp-app-view.js.map +1 -0
- package/dist/app/mcp-toolkit-app.d.ts +18 -0
- package/dist/app/mcp-toolkit-app.d.ts.map +1 -0
- package/dist/app/mcp-toolkit-app.js +22 -0
- package/dist/app/mcp-toolkit-app.js.map +1 -0
- package/dist/app/use-host-actions.d.ts +57 -0
- package/dist/app/use-host-actions.d.ts.map +1 -0
- package/dist/app/use-host-actions.js +42 -0
- package/dist/app/use-host-actions.js.map +1 -0
- package/dist/app/widget-fixture.d.ts +180 -0
- package/dist/app/widget-fixture.d.ts.map +1 -0
- package/dist/app/widget-fixture.js +273 -0
- package/dist/app/widget-fixture.js.map +1 -0
- package/dist/app/widget-props-sheet.d.ts +20 -0
- package/dist/app/widget-props-sheet.d.ts.map +1 -0
- package/dist/app/widget-props-sheet.js +87 -0
- package/dist/app/widget-props-sheet.js.map +1 -0
- package/dist/app/widget-renderer.d.ts +87 -0
- package/dist/app/widget-renderer.d.ts.map +1 -0
- package/dist/app/widget-renderer.js +105 -0
- package/dist/app/widget-renderer.js.map +1 -0
- package/dist/components/DrillButton.d.ts +22 -0
- package/dist/components/DrillButton.d.ts.map +1 -0
- package/dist/components/DrillButton.js +13 -0
- package/dist/components/DrillButton.js.map +1 -0
- package/dist/components/FilterBar.d.ts +28 -0
- package/dist/components/FilterBar.d.ts.map +1 -0
- package/dist/components/FilterBar.js +12 -0
- package/dist/components/FilterBar.js.map +1 -0
- package/dist/components/GridLayout.d.ts +26 -0
- package/dist/components/GridLayout.d.ts.map +1 -0
- package/dist/components/GridLayout.js +31 -0
- package/dist/components/GridLayout.js.map +1 -0
- package/dist/components/GroupCard.d.ts +21 -0
- package/dist/components/GroupCard.d.ts.map +1 -0
- package/dist/components/GroupCard.js +24 -0
- package/dist/components/GroupCard.js.map +1 -0
- package/dist/components/KpiGrid.d.ts +43 -0
- package/dist/components/KpiGrid.d.ts.map +1 -0
- package/dist/components/KpiGrid.js +35 -0
- package/dist/components/KpiGrid.js.map +1 -0
- package/dist/components/ListFooter.d.ts +22 -0
- package/dist/components/ListFooter.d.ts.map +1 -0
- package/dist/components/ListFooter.js +13 -0
- package/dist/components/ListFooter.js.map +1 -0
- package/dist/components/Pills.d.ts +25 -0
- package/dist/components/Pills.d.ts.map +1 -0
- package/dist/components/Pills.js +17 -0
- package/dist/components/Pills.js.map +1 -0
- package/dist/components/SectionHeading.d.ts +16 -0
- package/dist/components/SectionHeading.d.ts.map +1 -0
- package/dist/components/SectionHeading.js +10 -0
- package/dist/components/SectionHeading.js.map +1 -0
- package/dist/components/WidgetHeader.d.ts +21 -0
- package/dist/components/WidgetHeader.d.ts.map +1 -0
- package/dist/components/WidgetHeader.js +11 -0
- package/dist/components/WidgetHeader.js.map +1 -0
- package/dist/hooks/index.d.ts +11 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-debounced-value.d.ts +7 -0
- package/dist/hooks/use-debounced-value.d.ts.map +1 -0
- package/dist/hooks/use-debounced-value.js +15 -0
- package/dist/hooks/use-debounced-value.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +6 -0
- package/dist/hooks/use-mobile.d.ts.map +1 -0
- package/dist/hooks/use-mobile.js +19 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/hooks/use-tool-query.d.ts +21 -0
- package/dist/hooks/use-tool-query.d.ts.map +1 -0
- package/dist/hooks/use-tool-query.js +47 -0
- package/dist/hooks/use-tool-query.js.map +1 -0
- package/dist/hooks/use-tool-result-recovery.d.ts +54 -0
- package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -0
- package/dist/hooks/use-tool-result-recovery.js +93 -0
- package/dist/hooks/use-tool-result-recovery.js.map +1 -0
- package/dist/hooks/use-view-data.d.ts +19 -0
- package/dist/hooks/use-view-data.d.ts.map +1 -0
- package/dist/hooks/use-view-data.js +18 -0
- package/dist/hooks/use-view-data.js.map +1 -0
- package/dist/hooks/use-view-tool-query.d.ts +18 -0
- package/dist/hooks/use-view-tool-query.d.ts.map +1 -0
- package/dist/hooks/use-view-tool-query.js +29 -0
- package/dist/hooks/use-view-tool-query.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/parse-tool-result.d.ts +39 -0
- package/dist/lib/parse-tool-result.d.ts.map +1 -0
- package/dist/lib/parse-tool-result.js +66 -0
- package/dist/lib/parse-tool-result.js.map +1 -0
- package/dist/lib/tone-utils.d.ts +18 -0
- package/dist/lib/tone-utils.d.ts.map +1 -0
- package/dist/lib/tone-utils.js +25 -0
- package/dist/lib/tone-utils.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/locale/index.d.ts +3 -0
- package/dist/locale/index.d.ts.map +1 -0
- package/dist/locale/index.js +7 -0
- package/dist/locale/index.js.map +1 -0
- package/dist/locale/locale-provider.d.ts +50 -0
- package/dist/locale/locale-provider.d.ts.map +1 -0
- package/dist/locale/locale-provider.js +47 -0
- package/dist/locale/locale-provider.js.map +1 -0
- package/dist/primitives/alert.d.ts +10 -0
- package/dist/primitives/alert.d.ts.map +1 -0
- package/dist/primitives/alert.js +25 -0
- package/dist/primitives/alert.js.map +1 -0
- package/dist/primitives/badge.d.ts +8 -0
- package/dist/primitives/badge.d.ts.map +1 -0
- package/dist/primitives/badge.js +34 -0
- package/dist/primitives/badge.js.map +1 -0
- package/dist/primitives/button.d.ts +11 -0
- package/dist/primitives/button.d.ts.map +1 -0
- package/dist/primitives/button.js +36 -0
- package/dist/primitives/button.js.map +1 -0
- package/dist/primitives/card.d.ts +12 -0
- package/dist/primitives/card.d.ts.map +1 -0
- package/dist/primitives/card.js +25 -0
- package/dist/primitives/card.js.map +1 -0
- package/dist/primitives/dialog.d.ts +18 -0
- package/dist/primitives/dialog.d.ts.map +1 -0
- package/dist/primitives/dialog.js +38 -0
- package/dist/primitives/dialog.js.map +1 -0
- package/dist/primitives/input.d.ts +4 -0
- package/dist/primitives/input.d.ts.map +1 -0
- package/dist/primitives/input.js +7 -0
- package/dist/primitives/input.js.map +1 -0
- package/dist/primitives/scroll-area.d.ts +6 -0
- package/dist/primitives/scroll-area.d.ts.map +1 -0
- package/dist/primitives/scroll-area.js +11 -0
- package/dist/primitives/scroll-area.js.map +1 -0
- package/dist/primitives/select.d.ts +16 -0
- package/dist/primitives/select.d.ts.map +1 -0
- package/dist/primitives/select.js +38 -0
- package/dist/primitives/select.js.map +1 -0
- package/dist/primitives/separator.d.ts +4 -0
- package/dist/primitives/separator.d.ts.map +1 -0
- package/dist/primitives/separator.js +9 -0
- package/dist/primitives/separator.js.map +1 -0
- package/dist/primitives/sheet.d.ts +15 -0
- package/dist/primitives/sheet.d.ts.map +1 -0
- package/dist/primitives/sheet.js +41 -0
- package/dist/primitives/sheet.js.map +1 -0
- package/dist/primitives/skeleton.d.ts +3 -0
- package/dist/primitives/skeleton.d.ts.map +1 -0
- package/dist/primitives/skeleton.js +7 -0
- package/dist/primitives/skeleton.js.map +1 -0
- package/dist/primitives/switch.d.ts +5 -0
- package/dist/primitives/switch.d.ts.map +1 -0
- package/dist/primitives/switch.js +8 -0
- package/dist/primitives/switch.js.map +1 -0
- package/dist/primitives/table.d.ts +11 -0
- package/dist/primitives/table.d.ts.map +1 -0
- package/dist/primitives/table.js +28 -0
- package/dist/primitives/table.js.map +1 -0
- package/dist/primitives/tabs.d.ts +12 -0
- package/dist/primitives/tabs.d.ts.map +1 -0
- package/dist/primitives/tabs.js +29 -0
- package/dist/primitives/tabs.js.map +1 -0
- package/dist/providers/query-provider.d.ts +11 -0
- package/dist/providers/query-provider.d.ts.map +1 -0
- package/dist/providers/query-provider.js +20 -0
- package/dist/providers/query-provider.js.map +1 -0
- package/dist/theme/create-theme.d.ts +130 -0
- package/dist/theme/create-theme.d.ts.map +1 -0
- package/dist/theme/create-theme.js +65 -0
- package/dist/theme/create-theme.js.map +1 -0
- package/dist/theme/index.d.ts +7 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +7 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/presets.d.ts +25 -0
- package/dist/theme/presets.d.ts.map +1 -0
- package/dist/theme/presets.js +68 -0
- package/dist/theme/presets.js.map +1 -0
- package/dist/theme/theme-provider.d.ts +69 -0
- package/dist/theme/theme-provider.d.ts.map +1 -0
- package/dist/theme/theme-provider.js +79 -0
- package/dist/theme/theme-provider.js.map +1 -0
- package/package.json +71 -0
- package/src/app/adapt-data-widget.tsx +51 -0
- package/src/app/builder/CatalogueSheet.tsx +230 -0
- package/src/app/builder/PipelineStrip.tsx +260 -0
- package/src/app/builder/PreviewPane.tsx +72 -0
- package/src/app/builder/SaveDialog.tsx +95 -0
- package/src/app/builder/Toolbar.tsx +95 -0
- package/src/app/builder/Workspace.tsx +483 -0
- package/src/app/builder/builder-model.test.ts +351 -0
- package/src/app/builder/builder-model.ts +213 -0
- package/src/app/builder/builder-reducer.test.ts +441 -0
- package/src/app/builder/builder-reducer.ts +289 -0
- package/src/app/builder/labels.ts +139 -0
- package/src/app/builder/status-chip.tsx +37 -0
- package/src/app/host-bridge.test.ts +206 -0
- package/src/app/host-bridge.tsx +371 -0
- package/src/app/index.ts +51 -0
- package/src/app/layout-builder.tsx +872 -0
- package/src/app/mcp-app-view.tsx +367 -0
- package/src/app/mcp-toolkit-app.tsx +25 -0
- package/src/app/use-host-actions.test.ts +16 -0
- package/src/app/use-host-actions.tsx +88 -0
- package/src/app/widget-fixture.test.ts +108 -0
- package/src/app/widget-fixture.tsx +441 -0
- package/src/app/widget-props-sheet.tsx +285 -0
- package/src/app/widget-renderer.test.ts +42 -0
- package/src/app/widget-renderer.tsx +200 -0
- package/src/components/DrillButton.tsx +37 -0
- package/src/components/FilterBar.tsx +68 -0
- package/src/components/GridLayout.tsx +49 -0
- package/src/components/GroupCard.tsx +52 -0
- package/src/components/KpiGrid.tsx +134 -0
- package/src/components/ListFooter.tsx +48 -0
- package/src/components/Pills.tsx +45 -0
- package/src/components/SectionHeading.tsx +26 -0
- package/src/components/WidgetHeader.tsx +51 -0
- package/src/globals.css +168 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/use-debounced-value.ts +15 -0
- package/src/hooks/use-mobile.ts +24 -0
- package/src/hooks/use-tool-query.ts +63 -0
- package/src/hooks/use-tool-result-recovery.test.ts +65 -0
- package/src/hooks/use-tool-result-recovery.ts +144 -0
- package/src/hooks/use-view-data.ts +31 -0
- package/src/hooks/use-view-tool-query.ts +38 -0
- package/src/index.ts +134 -0
- package/src/lib/parse-tool-result.test.ts +139 -0
- package/src/lib/parse-tool-result.ts +89 -0
- package/src/lib/tone-utils.test.ts +35 -0
- package/src/lib/tone-utils.ts +38 -0
- package/src/lib/utils.ts +6 -0
- package/src/locale/index.ts +7 -0
- package/src/locale/locale-provider.tsx +81 -0
- package/src/primitives/alert.tsx +60 -0
- package/src/primitives/badge.tsx +49 -0
- package/src/primitives/button.tsx +62 -0
- package/src/primitives/card.tsx +92 -0
- package/src/primitives/dialog.tsx +144 -0
- package/src/primitives/input.tsx +21 -0
- package/src/primitives/scroll-area.tsx +54 -0
- package/src/primitives/select.tsx +173 -0
- package/src/primitives/separator.tsx +21 -0
- package/src/primitives/sheet.tsx +134 -0
- package/src/primitives/skeleton.tsx +13 -0
- package/src/primitives/switch.tsx +24 -0
- package/src/primitives/table.tsx +87 -0
- package/src/primitives/tabs.tsx +79 -0
- package/src/providers/query-provider.tsx +38 -0
- package/src/theme/create-theme.test.ts +108 -0
- package/src/theme/create-theme.ts +158 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/presets.ts +82 -0
- package/src/theme/theme-provider.tsx +152 -0
- package/src/ui-catalog.test.ts +112 -0
- package/ui-catalog.json +1280 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { KeyboardEvent, MouseEvent, ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
export interface GroupCardProps {
|
|
4
|
+
/** Whether the body is expanded. */
|
|
5
|
+
expanded: boolean
|
|
6
|
+
/** Toggle handler for the summary bar. */
|
|
7
|
+
onToggle: () => void
|
|
8
|
+
/** Always-visible summary bar (may contain its own buttons/links). */
|
|
9
|
+
summary: ReactNode
|
|
10
|
+
/** Collapsible body, rendered only when `expanded`. */
|
|
11
|
+
children?: ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bordered card with a clickable summary bar and a collapsible body. Use to
|
|
16
|
+
* group related rows behind an expandable header (e.g. items grouped by a key).
|
|
17
|
+
*
|
|
18
|
+
* The summary uses `role="button"` rather than a `<button>` element so it can
|
|
19
|
+
* contain its own controls (drill buttons, links) without producing invalid
|
|
20
|
+
* nested-interactive HTML; clicks landing on those controls do not toggle.
|
|
21
|
+
*/
|
|
22
|
+
export function GroupCard({ expanded, onToggle, summary, children }: GroupCardProps) {
|
|
23
|
+
function handleKeyDown(e: KeyboardEvent<HTMLDivElement>) {
|
|
24
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
onToggle()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function handleClick(e: MouseEvent<HTMLDivElement>) {
|
|
31
|
+
if ((e.target as HTMLElement).closest("button, a, input, select, textarea, label")) return
|
|
32
|
+
onToggle()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="border-border mb-2 overflow-hidden rounded-lg border">
|
|
37
|
+
<div
|
|
38
|
+
role="button"
|
|
39
|
+
tabIndex={0}
|
|
40
|
+
aria-expanded={expanded}
|
|
41
|
+
onClick={handleClick}
|
|
42
|
+
onKeyDown={handleKeyDown}
|
|
43
|
+
className={`bg-card hover:bg-muted focus-visible:ring-ring/50 cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 ${
|
|
44
|
+
expanded ? "border-border border-b" : ""
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
{summary}
|
|
48
|
+
</div>
|
|
49
|
+
{expanded && children && <div>{children}</div>}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import { TONE_TEXT, type ToneVariant } from "../lib/tone-utils.js"
|
|
3
|
+
|
|
4
|
+
/** Trend direction → tone for the small trend line under a value. */
|
|
5
|
+
const TREND_TONE: Record<"up" | "down" | "flat", string> = {
|
|
6
|
+
up: "text-danger",
|
|
7
|
+
down: "text-success",
|
|
8
|
+
flat: "text-muted-foreground",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface KpiCell {
|
|
12
|
+
/** Caption shown above the value. */
|
|
13
|
+
label: ReactNode
|
|
14
|
+
/** The headline figure. Pre-format numbers for locale/currency. */
|
|
15
|
+
value: ReactNode
|
|
16
|
+
/** Small fraction shown next to the value, e.g. ` /14`. */
|
|
17
|
+
fraction?: ReactNode
|
|
18
|
+
/** Optional trend line under the value (e.g. `"+12% vs last week"`). */
|
|
19
|
+
trend?: ReactNode
|
|
20
|
+
/** Colours the trend line. `up` reads as bad (danger), `down` as good (success). */
|
|
21
|
+
trendDirection?: "up" | "down" | "flat"
|
|
22
|
+
/** Tone applied to the value text (e.g. `"danger"` for a breached metric). */
|
|
23
|
+
tone?: ToneVariant
|
|
24
|
+
/** When set, the cell renders as a button — turns a metric into a nav entry point. */
|
|
25
|
+
onClick?: () => void
|
|
26
|
+
/** Accessible label for the clickable cell (recommended when `onClick` is set). */
|
|
27
|
+
ariaLabel?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const COL_CLASS: Record<number, string> = {
|
|
31
|
+
1: "grid-cols-1",
|
|
32
|
+
2: "grid-cols-2",
|
|
33
|
+
3: "grid-cols-3",
|
|
34
|
+
4: "grid-cols-4",
|
|
35
|
+
5: "grid-cols-5",
|
|
36
|
+
6: "grid-cols-6",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface KpiGridHeader {
|
|
40
|
+
/** Group label, rendered uppercase in the strip header. */
|
|
41
|
+
label: ReactNode
|
|
42
|
+
/** Optional muted badge to the right of the label. */
|
|
43
|
+
badge?: ReactNode
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface KpiGridProps {
|
|
47
|
+
/** The metric cells, flowed as columns (1–6, clamped). */
|
|
48
|
+
cells: KpiCell[]
|
|
49
|
+
/** Wrap the strip in a bordered card with an optional group `header`. Default `false`. */
|
|
50
|
+
boxed?: boolean
|
|
51
|
+
/** Group header shown above the strip when `boxed`. */
|
|
52
|
+
header?: KpiGridHeader
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Bordered strip of KPI cells — the canonical way to show a row of headline
|
|
57
|
+
* metrics in a widget (replaces the single-metric KPICard). Cells flow as
|
|
58
|
+
* columns (1–6) and can be made clickable to drill into a detail view. Use
|
|
59
|
+
* `boxed` + `header` for a labelled metric block; unboxed (default) renders the
|
|
60
|
+
* lighter `border-y` strip.
|
|
61
|
+
*/
|
|
62
|
+
export function KpiGrid({ cells, boxed = false, header }: KpiGridProps) {
|
|
63
|
+
const cols = Math.min(Math.max(cells.length, 1), 6)
|
|
64
|
+
const colClass = COL_CLASS[cols] ?? "grid-cols-4"
|
|
65
|
+
const wrapperClass = boxed ? "border-border overflow-hidden rounded-lg border" : ""
|
|
66
|
+
const stripClass = boxed ? "" : "border-border border-y"
|
|
67
|
+
return (
|
|
68
|
+
<div className={wrapperClass}>
|
|
69
|
+
{header && (
|
|
70
|
+
<div className="border-border bg-muted text-muted-foreground flex items-center gap-2 border-b px-4 py-2 text-[11px] font-semibold tracking-wide uppercase">
|
|
71
|
+
<span>{header.label}</span>
|
|
72
|
+
{header.badge && (
|
|
73
|
+
<span className="border-border bg-card text-muted-foreground rounded border px-1.5 py-0 text-[10px] font-medium normal-case">
|
|
74
|
+
{header.badge}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
<div className={`grid ${colClass} ${stripClass}`}>
|
|
80
|
+
{cells.map((cell, idx) => {
|
|
81
|
+
const borderClass = idx < cells.length - 1 ? "border-border border-r" : ""
|
|
82
|
+
const content = (
|
|
83
|
+
<>
|
|
84
|
+
<div className="text-muted-foreground flex items-center justify-between gap-2 text-xs font-medium">
|
|
85
|
+
<span>{cell.label}</span>
|
|
86
|
+
{cell.onClick && (
|
|
87
|
+
<span aria-hidden="true" className="text-sm leading-none">
|
|
88
|
+
›
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
<div
|
|
93
|
+
className={`mt-1.5 text-2xl leading-none font-bold tracking-tight tabular-nums ${
|
|
94
|
+
(cell.tone && TONE_TEXT[cell.tone]) || "text-foreground"
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
{cell.value}
|
|
98
|
+
{cell.fraction && (
|
|
99
|
+
<span className="text-muted-foreground ml-0.5 text-sm font-normal">
|
|
100
|
+
{cell.fraction}
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
{cell.trend && (
|
|
105
|
+
<div
|
|
106
|
+
className={`mt-1.5 text-xs ${
|
|
107
|
+
cell.trendDirection ? TREND_TONE[cell.trendDirection] : "text-muted-foreground"
|
|
108
|
+
}`}
|
|
109
|
+
>
|
|
110
|
+
{cell.trend}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</>
|
|
114
|
+
)
|
|
115
|
+
return cell.onClick ? (
|
|
116
|
+
<button
|
|
117
|
+
key={idx}
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={cell.onClick}
|
|
120
|
+
aria-label={cell.ariaLabel}
|
|
121
|
+
className={`hover:bg-muted focus-visible:ring-ring cursor-pointer px-5 py-4 text-left transition-colors outline-none focus-visible:ring-2 focus-visible:ring-inset ${borderClass}`}
|
|
122
|
+
>
|
|
123
|
+
{content}
|
|
124
|
+
</button>
|
|
125
|
+
) : (
|
|
126
|
+
<div key={idx} className={`px-5 py-4 ${borderClass}`}>
|
|
127
|
+
{content}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
})}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface ListFooterProps {
|
|
2
|
+
/** How many rows are currently rendered. */
|
|
3
|
+
shown: number
|
|
4
|
+
/** The total number of rows available. */
|
|
5
|
+
total: number
|
|
6
|
+
/** Whether more rows can be loaded. */
|
|
7
|
+
hasMore: boolean
|
|
8
|
+
/** Disables the button and shows a loading label while a fetch is in flight. */
|
|
9
|
+
loadingMore?: boolean
|
|
10
|
+
/** Called when the user asks for the next page. */
|
|
11
|
+
onLoadMore: () => void
|
|
12
|
+
/** Plural noun for the count, e.g. `"instances"`. Default `"rows"`. */
|
|
13
|
+
noun?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Footer for a paginated list: an honest "Showing X of Y" total plus an
|
|
18
|
+
* explicit "Load more" control. Deliberately not infinite scroll — the user
|
|
19
|
+
* gets the total, a reachable footer, and control over fetching. Renders
|
|
20
|
+
* nothing when `total` is 0.
|
|
21
|
+
*/
|
|
22
|
+
export function ListFooter({
|
|
23
|
+
shown,
|
|
24
|
+
total,
|
|
25
|
+
hasMore,
|
|
26
|
+
loadingMore = false,
|
|
27
|
+
onLoadMore,
|
|
28
|
+
noun = "rows",
|
|
29
|
+
}: ListFooterProps) {
|
|
30
|
+
if (total <= 0) return null
|
|
31
|
+
return (
|
|
32
|
+
<div className="text-muted-foreground flex items-center justify-between gap-3 px-1 py-2 text-xs">
|
|
33
|
+
<span>
|
|
34
|
+
Showing {shown.toLocaleString()} of {total.toLocaleString()} {noun}
|
|
35
|
+
</span>
|
|
36
|
+
{hasMore && (
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={onLoadMore}
|
|
40
|
+
disabled={loadingMore}
|
|
41
|
+
className="border-border text-foreground hover:bg-muted focus-visible:ring-ring inline-flex items-center gap-1 rounded-md border px-3 py-1.5 font-medium outline-none focus-visible:ring-2 disabled:opacity-50"
|
|
42
|
+
>
|
|
43
|
+
{loadingMore ? "Loading…" : "Load more"}
|
|
44
|
+
</button>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import { TONE_DOT, TONE_SOFT, type ToneVariant } from "../lib/tone-utils.js"
|
|
3
|
+
|
|
4
|
+
export interface LivePillProps {
|
|
5
|
+
/** Status tone driving the dot + tint. Default `"info"`. */
|
|
6
|
+
tone?: ToneVariant
|
|
7
|
+
/** Pill label. Defaults to `"Live"`. */
|
|
8
|
+
children?: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Small tinted pill with a pulsing dot. Use in a widget header to signal that
|
|
13
|
+
* the data is live/real-time. Tone-based, so it inherits the active theme.
|
|
14
|
+
*/
|
|
15
|
+
export function LivePill({ tone = "info", children }: LivePillProps) {
|
|
16
|
+
return (
|
|
17
|
+
<span
|
|
18
|
+
className={`inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-semibold ${TONE_SOFT[tone]}`}
|
|
19
|
+
>
|
|
20
|
+
<span className={`size-1.5 animate-pulse rounded-full ${TONE_DOT[tone]}`} />
|
|
21
|
+
{children ?? "Live"}
|
|
22
|
+
</span>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CountPillProps {
|
|
27
|
+
/** Status tone driving the tint. Default `"neutral"`. */
|
|
28
|
+
tone?: ToneVariant
|
|
29
|
+
/** The count/value to display (tabular numbers). */
|
|
30
|
+
children: ReactNode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compact count badge — tabular numbers on a tinted background. Use as the
|
|
35
|
+
* right-aligned indicator on a list/group row (e.g. a per-group item count).
|
|
36
|
+
*/
|
|
37
|
+
export function CountPill({ tone = "neutral", children }: CountPillProps) {
|
|
38
|
+
return (
|
|
39
|
+
<span
|
|
40
|
+
className={`inline-flex min-w-[2.5rem] items-center justify-center rounded-md px-2.5 py-0.5 text-sm font-semibold tabular-nums ${TONE_SOFT[tone]}`}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</span>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
export interface SectionHeadingProps {
|
|
4
|
+
/** The section title. */
|
|
5
|
+
title: ReactNode
|
|
6
|
+
/** Optional muted hint shown on the right (e.g. a count or last-updated note). */
|
|
7
|
+
hint?: ReactNode
|
|
8
|
+
/** Right-aligned slot that overrides `hint` when set (e.g. a control). */
|
|
9
|
+
trailing?: ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Section title row — a small semibold heading with an optional muted hint or
|
|
14
|
+
* control on the right. Use to label a block inside a widget body (e.g. a list
|
|
15
|
+
* or sub-table) without the visual weight of a full card header.
|
|
16
|
+
*/
|
|
17
|
+
export function SectionHeading({ title, hint, trailing }: SectionHeadingProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="text-muted-foreground mb-3 flex items-center justify-between text-sm font-semibold">
|
|
20
|
+
<span>{title}</span>
|
|
21
|
+
{(hint || trailing) && (
|
|
22
|
+
<span className="text-muted-foreground text-xs font-medium">{trailing ?? hint}</span>
|
|
23
|
+
)}
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import { TONE_SOFT, type ToneVariant } from "../lib/tone-utils.js"
|
|
3
|
+
|
|
4
|
+
export interface WidgetHeaderProps {
|
|
5
|
+
/** Optional icon/glyph rendered in a tinted tile above the title. */
|
|
6
|
+
icon?: ReactNode
|
|
7
|
+
/** Tone for the icon tile. Default `"neutral"`. */
|
|
8
|
+
iconTone?: ToneVariant
|
|
9
|
+
/** The widget title. */
|
|
10
|
+
title: ReactNode
|
|
11
|
+
/** Subtitle/meta row under the title (e.g. a live pill + counts). */
|
|
12
|
+
sub?: ReactNode
|
|
13
|
+
/** Right-aligned actions slot (buttons, drill controls). */
|
|
14
|
+
actions?: ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Page-level header for the top of a dashboard widget: an optional tinted icon
|
|
19
|
+
* tile, a large title, a subtitle/meta row, and a right-aligned actions slot.
|
|
20
|
+
* Use once per widget surface to anchor it.
|
|
21
|
+
*/
|
|
22
|
+
export function WidgetHeader({
|
|
23
|
+
icon,
|
|
24
|
+
iconTone = "neutral",
|
|
25
|
+
title,
|
|
26
|
+
sub,
|
|
27
|
+
actions,
|
|
28
|
+
}: WidgetHeaderProps) {
|
|
29
|
+
return (
|
|
30
|
+
<header className="flex flex-wrap items-start justify-between gap-4">
|
|
31
|
+
<div className="min-w-0">
|
|
32
|
+
{icon && (
|
|
33
|
+
<div
|
|
34
|
+
className={`mb-3.5 grid size-11 place-items-center rounded-xl text-xl ${TONE_SOFT[iconTone]}`}
|
|
35
|
+
>
|
|
36
|
+
{icon}
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
<h1 className="text-foreground mb-1.5 text-3xl leading-tight font-bold tracking-tight">
|
|
40
|
+
{title}
|
|
41
|
+
</h1>
|
|
42
|
+
{sub && (
|
|
43
|
+
<div className="text-muted-foreground flex flex-wrap items-center gap-2 text-sm">
|
|
44
|
+
{sub}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
{actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
|
|
49
|
+
</header>
|
|
50
|
+
)
|
|
51
|
+
}
|
package/src/globals.css
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "@fontsource-variable/geist";
|
|
4
|
+
|
|
5
|
+
@source "../**/*.{ts,tsx}";
|
|
6
|
+
|
|
7
|
+
@custom-variant dark (&:is(.dark *));
|
|
8
|
+
|
|
9
|
+
@theme inline {
|
|
10
|
+
--font-heading: var(--font-sans);
|
|
11
|
+
--font-sans: "Geist Variable", sans-serif;
|
|
12
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
13
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
14
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
15
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
16
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
17
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
18
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
19
|
+
--color-sidebar: var(--sidebar);
|
|
20
|
+
--color-chart-5: var(--chart-5);
|
|
21
|
+
--color-chart-4: var(--chart-4);
|
|
22
|
+
--color-chart-3: var(--chart-3);
|
|
23
|
+
--color-chart-2: var(--chart-2);
|
|
24
|
+
--color-chart-1: var(--chart-1);
|
|
25
|
+
--color-ring: var(--ring);
|
|
26
|
+
--color-input: var(--input);
|
|
27
|
+
--color-border: var(--border);
|
|
28
|
+
--color-destructive: var(--destructive);
|
|
29
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
30
|
+
--color-accent: var(--accent);
|
|
31
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
32
|
+
--color-muted: var(--muted);
|
|
33
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
34
|
+
--color-secondary: var(--secondary);
|
|
35
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
36
|
+
--color-primary: var(--primary);
|
|
37
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
38
|
+
--color-popover: var(--popover);
|
|
39
|
+
--color-card-foreground: var(--card-foreground);
|
|
40
|
+
--color-card: var(--card);
|
|
41
|
+
--color-foreground: var(--foreground);
|
|
42
|
+
--color-background: var(--background);
|
|
43
|
+
/* Semantic status tones — the token surface behind ToneVariant/TONE_* maps.
|
|
44
|
+
Each tone exposes a readable foreground (`text-danger`) and a tinted
|
|
45
|
+
surface (`bg-danger-soft`); both carry light + dark values so tone-based
|
|
46
|
+
components (Pills, KpiGrid, WidgetHeader, FilterBar) re-theme automatically. */
|
|
47
|
+
--color-danger: var(--danger);
|
|
48
|
+
--color-danger-soft: var(--danger-soft);
|
|
49
|
+
--color-warning: var(--warning);
|
|
50
|
+
--color-warning-soft: var(--warning-soft);
|
|
51
|
+
--color-success: var(--success);
|
|
52
|
+
--color-success-soft: var(--success-soft);
|
|
53
|
+
--color-info: var(--info);
|
|
54
|
+
--color-info-soft: var(--info-soft);
|
|
55
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
56
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
57
|
+
--radius-lg: var(--radius);
|
|
58
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
59
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
60
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
61
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:root {
|
|
65
|
+
--background: oklch(1 0 0);
|
|
66
|
+
--foreground: oklch(0.145 0 0);
|
|
67
|
+
--card: oklch(1 0 0);
|
|
68
|
+
--card-foreground: oklch(0.145 0 0);
|
|
69
|
+
--popover: oklch(1 0 0);
|
|
70
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
71
|
+
--primary: oklch(0.205 0 0);
|
|
72
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
73
|
+
--secondary: oklch(0.97 0 0);
|
|
74
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
75
|
+
--muted: oklch(0.97 0 0);
|
|
76
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
77
|
+
--accent: oklch(0.97 0 0);
|
|
78
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
79
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
80
|
+
--border: oklch(0.922 0 0);
|
|
81
|
+
--input: oklch(0.922 0 0);
|
|
82
|
+
--ring: oklch(0.708 0 0);
|
|
83
|
+
--chart-1: oklch(0.87 0 0);
|
|
84
|
+
--chart-2: oklch(0.556 0 0);
|
|
85
|
+
--chart-3: oklch(0.439 0 0);
|
|
86
|
+
--chart-4: oklch(0.371 0 0);
|
|
87
|
+
--chart-5: oklch(0.269 0 0);
|
|
88
|
+
--radius: 0.625rem;
|
|
89
|
+
--sidebar: oklch(0.985 0 0);
|
|
90
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
91
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
92
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
93
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
94
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
95
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
96
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
97
|
+
/* Semantic status tones (light). Foreground = readable on a card; *-soft =
|
|
98
|
+
a tinted surface for badges/dots/icon tiles. Neutral tone reuses the
|
|
99
|
+
muted ramp and needs no dedicated token. */
|
|
100
|
+
--danger: oklch(0.55 0.21 27.3);
|
|
101
|
+
--danger-soft: oklch(0.95 0.04 27.3);
|
|
102
|
+
--warning: oklch(0.62 0.16 70);
|
|
103
|
+
--warning-soft: oklch(0.95 0.05 80);
|
|
104
|
+
--success: oklch(0.58 0.15 152);
|
|
105
|
+
--success-soft: oklch(0.95 0.05 152);
|
|
106
|
+
--info: oklch(0.55 0.2 255);
|
|
107
|
+
--info-soft: oklch(0.95 0.04 255);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.dark {
|
|
111
|
+
--background: oklch(0.145 0 0);
|
|
112
|
+
--foreground: oklch(0.985 0 0);
|
|
113
|
+
--card: oklch(0.205 0 0);
|
|
114
|
+
--card-foreground: oklch(0.985 0 0);
|
|
115
|
+
--popover: oklch(0.205 0 0);
|
|
116
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
117
|
+
--primary: oklch(0.922 0 0);
|
|
118
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
119
|
+
--secondary: oklch(0.269 0 0);
|
|
120
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
121
|
+
--muted: oklch(0.269 0 0);
|
|
122
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
123
|
+
--accent: oklch(0.269 0 0);
|
|
124
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
125
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
126
|
+
--border: oklch(1 0 0 / 10%);
|
|
127
|
+
--input: oklch(1 0 0 / 15%);
|
|
128
|
+
--ring: oklch(0.556 0 0);
|
|
129
|
+
--chart-1: oklch(0.87 0 0);
|
|
130
|
+
--chart-2: oklch(0.556 0 0);
|
|
131
|
+
--chart-3: oklch(0.439 0 0);
|
|
132
|
+
--chart-4: oklch(0.371 0 0);
|
|
133
|
+
--chart-5: oklch(0.269 0 0);
|
|
134
|
+
--sidebar: oklch(0.205 0 0);
|
|
135
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
136
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
137
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
138
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
139
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
140
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
141
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
142
|
+
/* Semantic status tones (dark). Brighter foregrounds for contrast on the
|
|
143
|
+
dark surface; *-soft is a low-alpha tint of the same hue. */
|
|
144
|
+
--danger: oklch(0.7 0.19 22.2);
|
|
145
|
+
--danger-soft: oklch(0.7 0.19 22.2 / 18%);
|
|
146
|
+
--warning: oklch(0.78 0.15 75);
|
|
147
|
+
--warning-soft: oklch(0.78 0.15 75 / 18%);
|
|
148
|
+
--success: oklch(0.72 0.17 152);
|
|
149
|
+
--success-soft: oklch(0.72 0.17 152 / 18%);
|
|
150
|
+
--info: oklch(0.7 0.16 255);
|
|
151
|
+
--info-soft: oklch(0.7 0.16 255 / 18%);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
* {
|
|
155
|
+
font-family: "Geist Variable", ui-sans-serif, system-ui, sans-serif !important;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@layer base {
|
|
159
|
+
*,
|
|
160
|
+
*::before,
|
|
161
|
+
*::after {
|
|
162
|
+
@apply border-border outline-ring/50;
|
|
163
|
+
}
|
|
164
|
+
html,
|
|
165
|
+
body {
|
|
166
|
+
@apply bg-background text-foreground;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { useIsMobile } from "./use-mobile.js"
|
|
2
|
+
export { useDebouncedValue } from "./use-debounced-value.js"
|
|
3
|
+
export { useToolQuery, useToolMutation } from "./use-tool-query.js"
|
|
4
|
+
export type { UseToolQueryOptions } from "./use-tool-query.js"
|
|
5
|
+
export { useViewToolQuery } from "./use-view-tool-query.js"
|
|
6
|
+
export type { UseViewToolQueryOptions } from "./use-view-tool-query.js"
|
|
7
|
+
export { useViewData } from "./use-view-data.js"
|
|
8
|
+
export type { ViewDataResult } from "./use-view-data.js"
|
|
9
|
+
export {
|
|
10
|
+
useToolResultRecovery,
|
|
11
|
+
resolveRecoveryToolName,
|
|
12
|
+
decodeRecoveredResult,
|
|
13
|
+
} from "./use-tool-result-recovery.js"
|
|
14
|
+
export type {
|
|
15
|
+
ToolResultRecoveryOptions,
|
|
16
|
+
ToolResultRecoveryState,
|
|
17
|
+
} from "./use-tool-result-recovery.js"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns `value` delayed by `delayMs`, resetting the timer on every change.
|
|
5
|
+
* Use to debounce a search box so a server-side filter only re-queries once the
|
|
6
|
+
* user pauses typing (not on every keystroke). Pure React — no host dependency.
|
|
7
|
+
*/
|
|
8
|
+
export function useDebouncedValue<T>(value: T, delayMs = 300): T {
|
|
9
|
+
const [debounced, setDebounced] = useState(value)
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const id = setTimeout(() => setDebounced(value), delayMs)
|
|
12
|
+
return () => clearTimeout(id)
|
|
13
|
+
}, [value, delayMs])
|
|
14
|
+
return debounced
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tracks whether the viewport is below the mobile breakpoint (768px), updating
|
|
7
|
+
* on resize. Use to switch a widget between a compact and a wide layout.
|
|
8
|
+
*/
|
|
9
|
+
export function useIsMobile() {
|
|
10
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(() =>
|
|
11
|
+
typeof window === "undefined" ? undefined : window.innerWidth < MOBILE_BREAKPOINT,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
16
|
+
const onChange = () => {
|
|
17
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
18
|
+
}
|
|
19
|
+
mql.addEventListener("change", onChange)
|
|
20
|
+
return () => mql.removeEventListener("change", onChange)
|
|
21
|
+
}, [])
|
|
22
|
+
|
|
23
|
+
return !!isMobile
|
|
24
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
|
2
|
+
import { useCallTool } from "../providers/query-provider.js"
|
|
3
|
+
import { parseToolResult } from "../lib/parse-tool-result.js"
|
|
4
|
+
|
|
5
|
+
export interface UseToolQueryOptions<TData = unknown, TSelected = TData> {
|
|
6
|
+
enabled?: boolean
|
|
7
|
+
select?: (data: TData) => TSelected
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Loads data from a widget tool via TanStack Query.
|
|
12
|
+
*
|
|
13
|
+
* `args` is appended to the effective query key so two callers that share
|
|
14
|
+
* the supplied `queryKey` but pass different `args` get separate cache
|
|
15
|
+
* entries (otherwise the second caller silently reads stale data from the
|
|
16
|
+
* first — the classic queryKey-vs-args trap). `select` transforms data on
|
|
17
|
+
* each read without mutating the cache.
|
|
18
|
+
*/
|
|
19
|
+
export function useToolQuery<TData = unknown, TSelected = TData>(
|
|
20
|
+
queryKey: unknown[],
|
|
21
|
+
toolName: string,
|
|
22
|
+
args: object,
|
|
23
|
+
opts?: UseToolQueryOptions<TData, TSelected>,
|
|
24
|
+
) {
|
|
25
|
+
const callTool = useCallTool()
|
|
26
|
+
|
|
27
|
+
return useQuery<TData, Error, TSelected>({
|
|
28
|
+
queryKey: [...queryKey, args],
|
|
29
|
+
queryFn: async () => {
|
|
30
|
+
if (!callTool) throw new Error("callTool not available")
|
|
31
|
+
const result = await callTool(toolName, args)
|
|
32
|
+
return parseToolResult<TData>(result)
|
|
33
|
+
},
|
|
34
|
+
select: opts?.select,
|
|
35
|
+
enabled: !!callTool && (opts?.enabled ?? true),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Executes a mutation via a widget tool and invalidates configured query keys.
|
|
41
|
+
*/
|
|
42
|
+
export function useToolMutation<TData = unknown>(
|
|
43
|
+
toolName: string,
|
|
44
|
+
opts?: { invalidateKeys?: unknown[][] },
|
|
45
|
+
) {
|
|
46
|
+
const callTool = useCallTool()
|
|
47
|
+
const queryClient = useQueryClient()
|
|
48
|
+
|
|
49
|
+
return useMutation<TData, Error, Record<string, unknown>>({
|
|
50
|
+
mutationFn: async (args: Record<string, unknown>) => {
|
|
51
|
+
if (!callTool) throw new Error("callTool not available")
|
|
52
|
+
const result = await callTool(toolName, args)
|
|
53
|
+
return parseToolResult<TData>(result)
|
|
54
|
+
},
|
|
55
|
+
onSuccess: async () => {
|
|
56
|
+
if (opts?.invalidateKeys) {
|
|
57
|
+
await Promise.all(
|
|
58
|
+
opts.invalidateKeys.map((key) => queryClient.refetchQueries({ queryKey: key })),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
}
|