@visulima/tui-kit 0.0.0 → 1.0.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/CHANGELOG.md +13 -0
- package/LICENSE.md +2095 -0
- package/README.md +115 -29
- package/dist/accordion.d.ts +52 -0
- package/dist/accordion.js +1 -0
- package/dist/alert.d.ts +22 -0
- package/dist/alert.js +1 -0
- package/dist/animate-presence.d.ts +16 -0
- package/dist/animate-presence.js +1 -0
- package/dist/approval-prompt.d.ts +55 -0
- package/dist/approval-prompt.js +1 -0
- package/dist/area-chart.d.ts +56 -0
- package/dist/area-chart.js +1 -0
- package/dist/badge.d.ts +19 -0
- package/dist/badge.js +1 -0
- package/dist/bar-chart.d.ts +58 -0
- package/dist/bar-chart.js +1 -0
- package/dist/big-text.d.ts +71 -0
- package/dist/big-text.js +1 -0
- package/dist/blink-dot.d.ts +31 -0
- package/dist/blink-dot.js +1 -0
- package/dist/breadcrumb.d.ts +29 -0
- package/dist/breadcrumb.js +1 -0
- package/dist/button.d.ts +38 -0
- package/dist/button.js +1 -0
- package/dist/calendar.d.ts +60 -0
- package/dist/calendar.js +1 -0
- package/dist/card.d.ts +47 -0
- package/dist/card.js +1 -0
- package/dist/checkbox-group.d.ts +50 -0
- package/dist/checkbox-group.js +1 -0
- package/dist/checkbox.d.ts +40 -0
- package/dist/checkbox.js +1 -0
- package/dist/code.d.ts +41 -0
- package/dist/code.js +2 -0
- package/dist/collapsible.d.ts +44 -0
- package/dist/collapsible.js +1 -0
- package/dist/color-picker.d.ts +59 -0
- package/dist/color-picker.js +1 -0
- package/dist/command-block.d.ts +39 -0
- package/dist/command-block.js +4 -0
- package/dist/command-palette.d.ts +73 -0
- package/dist/command-palette.js +1 -0
- package/dist/confirm-dialog.d.ts +63 -0
- package/dist/confirm-dialog.js +1 -0
- package/dist/confirm-input.d.ts +32 -0
- package/dist/confirm-input.js +1 -0
- package/dist/console-overlay.d.ts +47 -0
- package/dist/console-overlay.js +1 -0
- package/dist/content-switcher.d.ts +49 -0
- package/dist/content-switcher.js +1 -0
- package/dist/controlled-scroll-view.d.ts +3 -0
- package/dist/controlled-scroll-view.js +1 -0
- package/dist/data-grid.d.ts +43 -0
- package/dist/data-grid.js +1 -0
- package/dist/date-picker.d.ts +54 -0
- package/dist/date-picker.js +1 -0
- package/dist/definition-list.d.ts +38 -0
- package/dist/definition-list.js +1 -0
- package/dist/dialog.d.ts +94 -0
- package/dist/dialog.js +1 -0
- package/dist/diff-view.d.ts +72 -0
- package/dist/diff-view.js +1 -0
- package/dist/digits.d.ts +26 -0
- package/dist/digits.js +1 -0
- package/dist/directory-tree.d.ts +44 -0
- package/dist/directory-tree.js +1 -0
- package/dist/divider.d.ts +41 -0
- package/dist/divider.js +1 -0
- package/dist/drawer.d.ts +45 -0
- package/dist/drawer.js +1 -0
- package/dist/file-change.d.ts +34 -0
- package/dist/file-change.js +1 -0
- package/dist/file-picker-entry.d.ts +2 -0
- package/dist/file-picker-entry.js +1 -0
- package/dist/form.d.ts +74 -0
- package/dist/form.js +1 -0
- package/dist/gauge.d.ts +76 -0
- package/dist/gauge.js +1 -0
- package/dist/gradient.d.ts +38 -0
- package/dist/gradient.js +1 -0
- package/dist/grid.d.ts +34 -0
- package/dist/grid.js +1 -0
- package/dist/heading.d.ts +28 -0
- package/dist/heading.js +1 -0
- package/dist/heatmap.d.ts +52 -0
- package/dist/heatmap.js +1 -0
- package/dist/help.d.ts +45 -0
- package/dist/help.js +1 -0
- package/dist/histogram.d.ts +52 -0
- package/dist/histogram.js +1 -0
- package/dist/image.d.ts +23 -0
- package/dist/image.js +1 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +1 -0
- package/dist/json.d.ts +51 -0
- package/dist/json.js +1 -0
- package/dist/kbd.d.ts +27 -0
- package/dist/kbd.js +1 -0
- package/dist/line-chart.d.ts +100 -0
- package/dist/line-chart.js +1 -0
- package/dist/link.d.ts +34 -0
- package/dist/link.js +1 -0
- package/dist/loading-indicator.d.ts +25 -0
- package/dist/loading-indicator.js +1 -0
- package/dist/log.d.ts +33 -0
- package/dist/log.js +1 -0
- package/dist/markdown.d.ts +35 -0
- package/dist/markdown.js +1 -0
- package/dist/masked-input.d.ts +42 -0
- package/dist/masked-input.js +1 -0
- package/dist/menu.d.ts +70 -0
- package/dist/menu.js +1 -0
- package/dist/message-bubble.d.ts +36 -0
- package/dist/message-bubble.js +1 -0
- package/dist/model-badge.d.ts +33 -0
- package/dist/model-badge.js +1 -0
- package/dist/model-selector.d.ts +45 -0
- package/dist/model-selector.js +1 -0
- package/dist/multi-progress.d.ts +48 -0
- package/dist/multi-progress.js +1 -0
- package/dist/multi-select.d.ts +55 -0
- package/dist/multi-select.js +1 -0
- package/dist/notification-badge.d.ts +40 -0
- package/dist/notification-badge.js +1 -0
- package/dist/number-input.d.ts +71 -0
- package/dist/number-input.js +1 -0
- package/dist/operation-tree.d.ts +46 -0
- package/dist/operation-tree.js +1 -0
- package/dist/option-list.d.ts +41 -0
- package/dist/option-list.js +1 -0
- package/dist/ordered-list.d.ts +22 -0
- package/dist/ordered-list.js +1 -0
- package/dist/packem_shared/ControlledScrollView-BzlbIUph.js +1 -0
- package/dist/packem_shared/FilePicker-V5H2fb-v.js +1 -0
- package/dist/packem_shared/ScrollBar-C9i3z3Ft.js +1 -0
- package/dist/packem_shared/ScrollBarBox-DZEC8YnV.js +1 -0
- package/dist/packem_shared/ScrollList-Djtb2vVy.js +1 -0
- package/dist/packem_shared/ScrollView-ESxpl6Hy.js +1 -0
- package/dist/packem_shared/TreeNodeMap-cJlYqckB.js +1 -0
- package/dist/packem_shared/TreeView-q0l446NV.js +1 -0
- package/dist/packem_shared/controlled-scroll-view-DerVKKv6.js +1 -0
- package/dist/packem_shared/controlled-scroll-view.d-D3kZZzjI.d.ts +43 -0
- package/dist/packem_shared/file-picker.d-CH7pccC7.d.ts +92 -0
- package/dist/packem_shared/line-chart-BbhMhcZg.js +1 -0
- package/dist/packem_shared/scroll-bar-box.d-CpTYEcTZ.d.ts +13 -0
- package/dist/packem_shared/scroll-bar.d-B4sD1hBR.d.ts +18 -0
- package/dist/packem_shared/scroll-list.d-CPCF0RwC.d.ts +12 -0
- package/dist/packem_shared/scroll-view.d-GZcgAHl4.d.ts +98 -0
- package/dist/packem_shared/treeViewTheme-Dq3ekRAC.js +1 -0
- package/dist/packem_shared/use-tree-view.d-DeMQ6G2r.d.ts +314 -0
- package/dist/packem_shared/useTreeView-D0JEqT6W.js +1 -0
- package/dist/packem_shared/useTreeViewState-BiTJakkh.js +1 -0
- package/dist/packem_shared/variant-config-BJqpNo7H.js +1 -0
- package/dist/packem_shared/variant-config.d-D5NnrrQK.d.ts +2 -0
- package/dist/paginator.d.ts +59 -0
- package/dist/paginator.js +1 -0
- package/dist/paragraph.d.ts +32 -0
- package/dist/paragraph.js +1 -0
- package/dist/path-input.d.ts +56 -0
- package/dist/path-input.js +1 -0
- package/dist/pie-chart.d.ts +44 -0
- package/dist/pie-chart.js +1 -0
- package/dist/placeholder.d.ts +47 -0
- package/dist/placeholder.js +1 -0
- package/dist/popover.d.ts +39 -0
- package/dist/popover.js +1 -0
- package/dist/progress-bar.d.ts +43 -0
- package/dist/progress-bar.js +1 -0
- package/dist/progress-circle.d.ts +50 -0
- package/dist/progress-circle.js +1 -0
- package/dist/qr-code.d.ts +39 -0
- package/dist/qr-code.js +1 -0
- package/dist/r/accordion.json +24 -0
- package/dist/r/alert.json +24 -0
- package/dist/r/animate-presence.json +22 -0
- package/dist/r/approval-prompt.json +24 -0
- package/dist/r/area-chart.json +27 -0
- package/dist/r/badge.json +24 -0
- package/dist/r/bar-chart.json +24 -0
- package/dist/r/big-text.json +23 -0
- package/dist/r/blink-dot.json +24 -0
- package/dist/r/breadcrumb.json +24 -0
- package/dist/r/button.json +24 -0
- package/dist/r/calendar.json +24 -0
- package/dist/r/card.json +26 -0
- package/dist/r/chart-utils.json +24 -0
- package/dist/r/checkbox-group.json +24 -0
- package/dist/r/checkbox.json +24 -0
- package/dist/r/code.json +23 -0
- package/dist/r/collapsible.json +24 -0
- package/dist/r/color-picker.json +24 -0
- package/dist/r/command-block.json +26 -0
- package/dist/r/command-palette.json +24 -0
- package/dist/r/confirm-dialog.json +24 -0
- package/dist/r/confirm-input.json +22 -0
- package/dist/r/console-overlay.json +22 -0
- package/dist/r/content-switcher.json +24 -0
- package/dist/r/controlled-scroll-view.json +24 -0
- package/dist/r/data-grid.json +24 -0
- package/dist/r/date-picker.json +26 -0
- package/dist/r/definition-list.json +24 -0
- package/dist/r/dialog.json +24 -0
- package/dist/r/diff-view.json +24 -0
- package/dist/r/digits.json +24 -0
- package/dist/r/directory-tree.json +24 -0
- package/dist/r/divider.json +24 -0
- package/dist/r/drawer.json +24 -0
- package/dist/r/file-change.json +24 -0
- package/dist/r/file-picker.json +44 -0
- package/dist/r/form.json +24 -0
- package/dist/r/gauge.json +24 -0
- package/dist/r/gradient.json +24 -0
- package/dist/r/grid.json +22 -0
- package/dist/r/heading.json +24 -0
- package/dist/r/heatmap.json +24 -0
- package/dist/r/help.json +22 -0
- package/dist/r/histogram.json +26 -0
- package/dist/r/image.json +24 -0
- package/dist/r/json.json +24 -0
- package/dist/r/kbd.json +24 -0
- package/dist/r/line-chart.json +26 -0
- package/dist/r/link.json +23 -0
- package/dist/r/loading-indicator.json +27 -0
- package/dist/r/log.json +24 -0
- package/dist/r/markdown.json +29 -0
- package/dist/r/masked-input.json +22 -0
- package/dist/r/menu.json +24 -0
- package/dist/r/message-bubble.json +24 -0
- package/dist/r/model-badge.json +24 -0
- package/dist/r/model-selector.json +24 -0
- package/dist/r/multi-progress.json +24 -0
- package/dist/r/multi-select.json +24 -0
- package/dist/r/notification-badge.json +24 -0
- package/dist/r/number-input.json +24 -0
- package/dist/r/operation-tree.json +26 -0
- package/dist/r/option-list.json +24 -0
- package/dist/r/ordered-list.json +22 -0
- package/dist/r/paginator.json +22 -0
- package/dist/r/paragraph.json +24 -0
- package/dist/r/path-input.json +26 -0
- package/dist/r/pie-chart.json +24 -0
- package/dist/r/placeholder.json +24 -0
- package/dist/r/popover.json +24 -0
- package/dist/r/progress-bar.json +22 -0
- package/dist/r/progress-circle.json +24 -0
- package/dist/r/qr-code.json +24 -0
- package/dist/r/radio-group.json +24 -0
- package/dist/r/registry.json +1051 -0
- package/dist/r/scatter-plot.json +27 -0
- package/dist/r/scroll-bar-box.json +24 -0
- package/dist/r/scroll-bar.json +24 -0
- package/dist/r/scroll-list.json +24 -0
- package/dist/r/scroll-view.json +24 -0
- package/dist/r/scroll.json +58 -0
- package/dist/r/search-input.json +26 -0
- package/dist/r/select-input-indicator.json +24 -0
- package/dist/r/select-input-item.json +24 -0
- package/dist/r/select-input.json +28 -0
- package/dist/r/shimmer-text.json +24 -0
- package/dist/r/sidebar.json +24 -0
- package/dist/r/slider.json +24 -0
- package/dist/r/sparkline.json +24 -0
- package/dist/r/spinner.json +23 -0
- package/dist/r/status-line.json +24 -0
- package/dist/r/status-message.json +24 -0
- package/dist/r/stepper.json +24 -0
- package/dist/r/stopwatch.json +22 -0
- package/dist/r/streaming-text.json +24 -0
- package/dist/r/switch.json +24 -0
- package/dist/r/tab.json +22 -0
- package/dist/r/table.json +23 -0
- package/dist/r/tabs.json +26 -0
- package/dist/r/tag-input.json +24 -0
- package/dist/r/tag.json +24 -0
- package/dist/r/text-input.json +22 -0
- package/dist/r/textarea.json +22 -0
- package/dist/r/thinking-block.json +24 -0
- package/dist/r/time-picker.json +24 -0
- package/dist/r/timer.json +22 -0
- package/dist/r/toast.json +24 -0
- package/dist/r/token-usage.json +24 -0
- package/dist/r/tool-call.json +24 -0
- package/dist/r/tooltip.json +24 -0
- package/dist/r/transition.json +22 -0
- package/dist/r/tree-select.json +24 -0
- package/dist/r/tree-view.json +64 -0
- package/dist/r/unordered-list.json +22 -0
- package/dist/r/use-linked-scroll.json +24 -0
- package/dist/r/variant-config.json +22 -0
- package/dist/radio-group.d.ts +63 -0
- package/dist/radio-group.js +1 -0
- package/dist/scatter-plot.d.ts +24 -0
- package/dist/scatter-plot.js +1 -0
- package/dist/scroll-bar-box.d.ts +3 -0
- package/dist/scroll-bar-box.js +1 -0
- package/dist/scroll-bar.d.ts +3 -0
- package/dist/scroll-bar.js +1 -0
- package/dist/scroll-list.d.ts +4 -0
- package/dist/scroll-list.js +1 -0
- package/dist/scroll-view.d.ts +3 -0
- package/dist/scroll-view.js +1 -0
- package/dist/search-input.d.ts +46 -0
- package/dist/search-input.js +1 -0
- package/dist/select-input-indicator.d.ts +28 -0
- package/dist/select-input-indicator.js +1 -0
- package/dist/select-input-item.d.ts +35 -0
- package/dist/select-input-item.js +1 -0
- package/dist/select-input.d.ts +97 -0
- package/dist/select-input.js +1 -0
- package/dist/shimmer-text.d.ts +41 -0
- package/dist/shimmer-text.js +1 -0
- package/dist/sidebar.d.ts +40 -0
- package/dist/sidebar.js +1 -0
- package/dist/slider.d.ts +87 -0
- package/dist/slider.js +1 -0
- package/dist/sparkline.d.ts +36 -0
- package/dist/sparkline.js +1 -0
- package/dist/spinner.d.ts +15 -0
- package/dist/spinner.js +1 -0
- package/dist/status-line.d.ts +31 -0
- package/dist/status-line.js +1 -0
- package/dist/status-message.d.ts +18 -0
- package/dist/status-message.js +1 -0
- package/dist/stepper.d.ts +42 -0
- package/dist/stepper.js +1 -0
- package/dist/stopwatch.d.ts +42 -0
- package/dist/stopwatch.js +1 -0
- package/dist/streaming-text.d.ts +39 -0
- package/dist/streaming-text.js +1 -0
- package/dist/switch.d.ts +46 -0
- package/dist/switch.js +1 -0
- package/dist/tab.d.ts +17 -0
- package/dist/tab.js +1 -0
- package/dist/table.d.ts +118 -0
- package/dist/table.js +1 -0
- package/dist/tabs.d.ts +96 -0
- package/dist/tabs.js +1 -0
- package/dist/tag-input.d.ts +59 -0
- package/dist/tag-input.js +1 -0
- package/dist/tag.d.ts +30 -0
- package/dist/tag.js +1 -0
- package/dist/text-input.d.ts +40 -0
- package/dist/text-input.js +1 -0
- package/dist/textarea.d.ts +57 -0
- package/dist/textarea.js +2 -0
- package/dist/thinking-block.d.ts +48 -0
- package/dist/thinking-block.js +1 -0
- package/dist/time-picker.d.ts +57 -0
- package/dist/time-picker.js +1 -0
- package/dist/timer.d.ts +50 -0
- package/dist/timer.js +1 -0
- package/dist/toast.d.ts +41 -0
- package/dist/toast.js +1 -0
- package/dist/token-usage.d.ts +43 -0
- package/dist/token-usage.js +1 -0
- package/dist/tool-call.d.ts +28 -0
- package/dist/tool-call.js +1 -0
- package/dist/tooltip.d.ts +37 -0
- package/dist/tooltip.js +1 -0
- package/dist/transition.d.ts +66 -0
- package/dist/transition.js +1 -0
- package/dist/tree-select.d.ts +55 -0
- package/dist/tree-select.js +1 -0
- package/dist/tree-view-entry.d.ts +4 -0
- package/dist/tree-view-entry.js +1 -0
- package/dist/unordered-list.d.ts +27 -0
- package/dist/unordered-list.js +1 -0
- package/dist/use-linked-scroll.d.ts +42 -0
- package/dist/use-linked-scroll.js +1 -0
- package/package.json +553 -7
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "bar-chart",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Bar Chart",
|
|
6
|
+
"description": "Bar Chart component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport type { CanvasContext } from \"@visulima/tui/canvas\";\nimport Box from \"@visulima/tui/components/box\";\nimport Canvas from \"@visulima/tui/components/canvas\";\nimport Text from \"@visulima/tui/components/text\";\nimport type { ReactElement } from \"react\";\nimport { useMemo } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type BarDatum = {\n /**\n * Per-bar color override. Falls back to the chart-level palette.\n */\n readonly color?: LiteralUnion<AnsiColors, string>;\n readonly label?: string;\n readonly value: number;\n};\n\nexport type Props = {\n /**\n * Bars to render.\n */\n readonly data: ReadonlyArray<BarDatum>;\n\n /**\n * Total height of the plot area in character rows.\n * @default 10\n */\n readonly height?: number;\n\n /**\n * Optional upper bound for the axis. When omitted the chart uses the\n * largest value in `data`.\n */\n readonly max?: number;\n\n /**\n * Orientation of the bars.\n * @default \"vertical\"\n */\n readonly orientation?: \"horizontal\" | \"vertical\";\n\n /**\n * Fallback palette cycled through when a bar does not provide its own color.\n * @default [\"cyan\", \"magenta\", \"yellow\", \"green\", \"blue\", \"red\"]\n */\n readonly palette?: ReadonlyArray<LiteralUnion<AnsiColors, string>>;\n\n /**\n * Render labels underneath vertical bars (or to the left of horizontal bars).\n * @default true\n */\n readonly showLabels?: boolean;\n\n /**\n * Render numeric values next to each bar.\n * @default false\n */\n readonly showValues?: boolean;\n\n /**\n * Total width of the plot area in character columns. Defaults to\n * `data.length * 3` for vertical charts, or `24` for horizontal.\n */\n readonly width?: number;\n};\n\nconst DEFAULT_PALETTE: ReadonlyArray<LiteralUnion<AnsiColors, string>> = [\"cyan\", \"magenta\", \"yellow\", \"green\", \"blue\", \"red\"];\n\n/**\n * Walk `data` once to find the maximum value. Avoids `Math.max(...values)`\n * which blows the call-stack on large arrays (same rule as Sparkline).\n */\nconst findMax = (data: ReadonlyArray<BarDatum>, userMax: number | undefined): number => {\n if (userMax !== undefined) {\n return userMax;\n }\n\n if (data.length === 0) {\n return 0;\n }\n\n // Seed from the first sample so all-negative datasets still yield a\n // sensible upper bound (otherwise initializing `max = 0` would wipe out\n // every datum < 0).\n let max = data[0]!.value;\n\n for (let index = 1; index < data.length; index += 1) {\n const { value } = data[index]!;\n\n if (value > max) {\n max = value;\n }\n }\n\n return max;\n};\n\nconst colorFor = (index: number, palette: ReadonlyArray<LiteralUnion<AnsiColors, string>>, datum: BarDatum): LiteralUnion<AnsiColors, string> =>\n datum.color ?? palette[index % palette.length] ?? \"cyan\";\n\n/**\n * Bar chart rendered on top of `Canvas`. Uses fractional block glyphs for\n * sub-cell precision (▁▂▃▄▅▆▇█ vertical, ▏▎▍▌▋▊▉█ horizontal).\n */\nexport default function BarChart({\n data,\n height = 10,\n max: maxOverride,\n orientation = \"vertical\",\n palette = DEFAULT_PALETTE,\n showLabels = true,\n showValues = false,\n width: widthOverride,\n}: Props): ReactElement {\n const computedMax = useMemo(() => findMax(data, maxOverride), [data, maxOverride]);\n\n if (orientation === \"horizontal\") {\n const plotWidth = widthOverride ?? 24;\n\n return (\n <Box flexDirection=\"column\">\n {data.map((datum, index) => {\n const ratio = computedMax === 0 ? 0 : datum.value / computedMax;\n const color = colorFor(index, palette, datum);\n\n return (\n // eslint-disable-next-line react-x/no-array-index-key -- chart bar index is stable for the render\n <Box gap={1} key={index}>\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {showLabels && datum.label !== undefined ? (\n <Box flexShrink={0} minWidth={8}>\n <Text dimColor wrap=\"truncate-end\">\n {datum.label}\n </Text>\n </Box>\n ) : undefined}\n <Box flexGrow={1} flexShrink={1}>\n <Canvas\n // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop -- canvas re-renders on `version` change, not draw identity\n draw={(context: CanvasContext) => {\n context.clear();\n context.drawHBar(0, 0, plotWidth, ratio, { color });\n }}\n height={1}\n // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop -- version array is the canvas redraw key\n version={[datum.value, computedMax, color, plotWidth]}\n width={plotWidth}\n />\n </Box>\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {showValues ? (\n <Box flexShrink={0}>\n <Text>{String(datum.value)}</Text>\n </Box>\n ) : undefined}\n </Box>\n );\n })}\n </Box>\n );\n }\n\n const barWidth = 2;\n const gap = 1;\n const plotWidth = widthOverride ?? Math.max(data.length * (barWidth + gap), 1);\n\n return (\n <Box flexDirection=\"column\">\n <Canvas\n // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop -- canvas re-renders on `version` change, not draw identity\n draw={(context: CanvasContext) => {\n context.clear();\n\n data.forEach((datum, index) => {\n const ratio = computedMax === 0 ? 0 : datum.value / computedMax;\n const color = colorFor(index, palette, datum);\n const x = index * (barWidth + gap);\n\n for (let column = 0; column < barWidth; column += 1) {\n context.drawVBar(x + column, 0, height, ratio, { color });\n }\n });\n }}\n height={height}\n // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop -- version array is the canvas redraw key\n version={[data, computedMax, palette, barWidth, gap, height]}\n width={plotWidth}\n />\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {showValues ? (\n <Box>\n {data.map((datum, index) => (\n // eslint-disable-next-line react-x/no-array-index-key -- chart column index is stable for the render\n <Box key={index} width={barWidth + gap}>\n <Text dimColor>{String(datum.value).padEnd(barWidth + gap, \" \")}</Text>\n </Box>\n ))}\n </Box>\n ) : undefined}\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {showLabels ? (\n <Box>\n {data.map((datum, index) => (\n // eslint-disable-next-line react-x/no-array-index-key -- chart column index is stable for the render\n <Box key={index} width={barWidth + gap}>\n <Text dimColor wrap=\"truncate-end\">\n {(datum.label ?? \"\").padEnd(barWidth + gap, \" \")}\n </Text>\n </Box>\n ))}\n </Box>\n ) : undefined}\n </Box>\n );\n}\n\nexport { BarChart };\nexport type { Props as BarChartProps };\n",
|
|
19
|
+
"path": "registry/ui/bar-chart.tsx",
|
|
20
|
+
"target": "components/ui/bar-chart.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "big-text",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Big Text",
|
|
6
|
+
"description": "Big Text component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/tui",
|
|
12
|
+
"cfonts"
|
|
13
|
+
],
|
|
14
|
+
"registryDependencies": [],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport Text from \"@visulima/tui/components/text\";\nimport CFonts from \"cfonts\";\nimport type { ReactElement } from \"react\";\nimport { useMemo } from \"react\";\n\n/**\n * Available CFonts font faces.\n */\ntype Font = \"3d\" | \"block\" | \"chrome\" | \"grid\" | \"huge\" | \"pallet\" | \"shade\" | \"simple\" | \"simple3d\" | \"simpleBlock\" | \"slick\" | \"tiny\";\n\n/**\n * Horizontal text alignment.\n */\ntype Align = \"center\" | \"left\" | \"right\";\n\n/**\n * Terminal background color names supported by CFonts.\n */\ntype BackgroundColor = \"black\" | \"blue\" | \"cyan\" | \"green\" | \"magenta\" | \"red\" | \"transparent\" | \"white\" | \"yellow\";\n\ntype Props = {\n /**\n * Horizontal alignment of the text.\n * @default \"left\"\n */\n readonly align?: Align;\n\n /**\n * Background color.\n * @default \"transparent\"\n */\n readonly backgroundColor?: BackgroundColor;\n\n /**\n * Colors to use for the font. Accepts color names, hex values, or `\"system\"` for terminal default.\n * @default [\"system\"]\n */\n readonly colors?: string[];\n\n /**\n * Font face to use.\n * @default \"block\"\n * @see https://github.com/dominikwilkowski/cfonts#supported-fonts\n */\n readonly font?: Font;\n\n /**\n * Letter spacing.\n * @default 1\n */\n readonly letterSpacing?: number;\n\n /**\n * Line height.\n * @default 1\n */\n readonly lineHeight?: number;\n\n /**\n * Maximum character length per line. `0` means no limit.\n * @default 0\n */\n readonly maxLength?: number;\n\n /**\n * Whether to add an empty line on top and at the bottom of the output.\n * @default true\n */\n readonly space?: boolean;\n\n /**\n * The text to render in a large font.\n */\n readonly text: string;\n};\n\nconst defaults = {\n align: \"left\" as const,\n backgroundColor: \"transparent\" as const,\n colors: [\"system\"],\n font: \"block\" as const,\n letterSpacing: 1,\n lineHeight: 1,\n maxLength: 0,\n space: true,\n};\n\n/**\n * Render large terminal text using CFonts.\n * @example\n * ```tsx\n * import { BigText } from \"@visulima/tui/ink\";\n *\n * <BigText text=\"Hello\" font=\"block\" />\n * ```\n */\nexport default function BigText({ align, backgroundColor, colors, font, letterSpacing, lineHeight, maxLength, space, text }: Props): ReactElement {\n const rendered = useMemo(\n () => CFonts.render(text, { ...defaults, align, backgroundColor, colors, font, letterSpacing, lineHeight, maxLength, space }),\n [text, align, backgroundColor, colors, font, letterSpacing, lineHeight, maxLength, space],\n );\n\n return <Text>{rendered && rendered.string}</Text>;\n}\n\nexport { BigText };\nexport type { Align, BackgroundColor, Font, Props };\nexport type { Align as BigTextAlign, BackgroundColor as BigTextBackgroundColor, Font as BigTextFont, Props as BigTextProps };\n",
|
|
18
|
+
"path": "registry/ui/big-text.tsx",
|
|
19
|
+
"target": "components/ui/big-text.tsx",
|
|
20
|
+
"type": "registry:ui"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "blink-dot",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Blink Dot",
|
|
6
|
+
"description": "Blink Dot component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Text from \"@visulima/tui/components/text\";\nimport useAnimation from \"@visulima/tui/hooks/use-animation\";\nimport type { ReactElement } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type Props = {\n /**\n * Glyph rendered as the blinking dot.\n * @default \"●\" (filled bullet)\n */\n readonly character?: string;\n\n /**\n * Dot color.\n * @default \"red\"\n */\n readonly color?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Milliseconds per blink cycle. The dot is visible for half the cycle.\n * @default 800\n */\n readonly interval?: number;\n\n /**\n * When false, the dot stays solid (no animation).\n * @default true\n */\n readonly isActive?: boolean;\n};\n\n/**\n * Steady or blinking indicator dot. Pair with `StatusLine` or next to a\n * label to show a \"live\" / \"recording\" status.\n */\nexport default function BlinkDot({ character = \"●\", color = \"red\", interval = 800, isActive = true }: Props): ReactElement {\n const { frame } = useAnimation({ interval: Math.max(1, Math.floor(interval / 2)), isActive });\n // When the animation is paused, useAnimation freezes `frame` at 0 — the\n // dot stays solid. When running, even frames show the character and odd\n // frames show a space, producing the blink.\n const visible = !isActive || frame % 2 === 0;\n\n return <Text color={color}>{visible ? character : \" \"}</Text>;\n}\n\nexport { BlinkDot };\nexport type { Props as BlinkDotProps };\n",
|
|
19
|
+
"path": "registry/ui/blink-dot.tsx",
|
|
20
|
+
"target": "components/ui/blink-dot.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "breadcrumb",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Breadcrumb",
|
|
6
|
+
"description": "Breadcrumb component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Fragment } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type BreadcrumbItem = {\n readonly icon?: ReactNode;\n readonly key?: string;\n readonly label: string;\n};\n\nexport type Props = {\n /**\n * Color applied to the trailing (current) item.\n * @default \"blue\"\n */\n readonly currentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Items to display from root to current page.\n */\n readonly items: ReadonlyArray<BreadcrumbItem>;\n\n /**\n * Glyph rendered between adjacent items.\n * @default \"›\" (right single angle quote)\n */\n readonly separator?: string;\n};\n\n/**\n * Render a breadcrumb trail (last item is highlighted).\n */\nexport default function Breadcrumb({ currentColor = \"blue\", items, separator = \"›\" }: Props): ReactElement {\n return (\n <Box>\n {items.map((item, index) => {\n const isLast = index === items.length - 1;\n\n return (\n <Fragment key={item.key ?? item.label}>\n {index > 0\n ? (\n<Text dimColor>\n{\" \"}\n{separator}\n{\" \"}\n</Text>\n )\n : undefined}\n <Text bold={isLast} color={isLast ? currentColor : undefined} dimColor={!isLast}>\n {item.icon === undefined\n ? undefined\n : (\n<>\n{item.icon}\n{\" \"}\n</>\n )}\n {item.label}\n </Text>\n </Fragment>\n );\n })}\n </Box>\n );\n}\n\nexport { Breadcrumb };\nexport type { Props as BreadcrumbProps };\n",
|
|
19
|
+
"path": "registry/ui/breadcrumb.tsx",
|
|
20
|
+
"target": "components/ui/breadcrumb.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "button",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Button",
|
|
6
|
+
"description": "Button component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useCallback } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type ButtonVariant = \"ghost\" | \"outline\" | \"primary\" | \"secondary\";\n\nexport type Props = {\n /**\n * Accent color used when the button is focused.\n * @default \"blue\"\n */\n readonly accentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Focus this button when mounted if nothing else is focused.\n */\n readonly autoFocus?: boolean;\n\n /**\n * Button label.\n */\n readonly children: ReactNode;\n\n /**\n * When true, the button ignores input and is rendered dim.\n */\n readonly isDisabled?: boolean;\n\n /**\n * Called when the user presses Enter or Space while the button has focus.\n */\n readonly onPress?: () => void;\n\n /**\n * Visual style.\n * @default \"primary\"\n */\n readonly variant?: ButtonVariant;\n};\n\nconst BORDER_STYLE = \"round\" as const;\n\n/**\n * Pick the label color for a Button based on its variant, focus state, and\n * disabled state. Returns `undefined` when the default terminal color should\n * be used (disabled, or an unfocused non-primary button). The focused primary\n * variant uses `\"black\"` so the label is readable against the filled accent\n * background; every other focused variant just echoes `accentColor`.\n */\nconst resolveTextColor = (\n variant: ButtonVariant,\n isFocused: boolean,\n isDisabled: boolean,\n accentColor: LiteralUnion<AnsiColors, string>,\n // eslint-disable-next-line sonarjs/function-return-type -- legitimate union return based on variant/state\n): LiteralUnion<AnsiColors, string> | undefined => {\n if (isDisabled) {\n return undefined;\n }\n\n if (variant === \"primary\" && isFocused) {\n return \"black\";\n }\n\n if (isFocused) {\n return accentColor;\n }\n\n return undefined;\n};\n\n/**\n * Focusable button. Triggers `onPress` on Enter / Space.\n * @returns A `ReactElement` rendering the button (bordered or ghost variant).\n */\nexport default function Button({ accentColor = \"blue\", autoFocus = false, children, isDisabled = false, onPress, variant = \"primary\" }: Props): ReactElement {\n const { isFocused } = useFocus({ autoFocus, isActive: !isDisabled });\n\n useInput(\n useCallback(\n (input, key) => {\n if (!isFocused) {\n return;\n }\n\n if (key.return || input === \" \") {\n onPress?.();\n }\n },\n [isFocused, onPress],\n ),\n { isActive: !isDisabled && isFocused },\n );\n\n const textColor = resolveTextColor(variant, isFocused, isDisabled, accentColor);\n\n if (variant === \"ghost\") {\n return (\n <Box paddingX={1}>\n <Text bold={isFocused} color={textColor} dimColor={isDisabled}>\n {isFocused ? \"▸ \" : \" \"}\n {children}\n </Text>\n </Box>\n );\n }\n\n // Visual tokens per variant:\n //\n // primary → filled accent background + accent border (highlighted when focused)\n // secondary → transparent background + accent border always; bolder label on focus\n // outline → transparent background + neutral border, accent-colored when focused\n // eslint-disable-next-line sonarjs/function-return-type -- legitimate union return based on variant/focus\n const borderColor = (() => {\n if (variant === \"outline\") {\n return isFocused ? accentColor : \"gray\";\n }\n\n if (variant === \"secondary\") {\n // Secondary keeps the accent-colored border always; focus is\n // conveyed by the bold label alone.\n return accentColor;\n }\n\n // primary (and any future variants) — accent border when focused.\n return isFocused ? accentColor : undefined;\n })();\n const fillBackground = variant === \"primary\" && isFocused ? accentColor : undefined;\n\n return (\n <Box backgroundColor={fillBackground} borderColor={borderColor} borderStyle={BORDER_STYLE} paddingX={1}>\n <Text bold={isFocused} color={textColor} dimColor={isDisabled}>\n {children}\n </Text>\n </Box>\n );\n}\n\nexport { Button };\nexport type { Props as ButtonProps };\n",
|
|
19
|
+
"path": "registry/ui/button.tsx",
|
|
20
|
+
"target": "components/ui/button.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "calendar",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Calendar",
|
|
6
|
+
"description": "Calendar component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement } from \"react\";\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type Props = {\n /**\n * Accent color for today / the focused cell.\n * @default \"blue\"\n */\n readonly accentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Auto-focus the grid on mount.\n */\n readonly autoFocus?: boolean;\n\n /**\n * Initial focused day when uncontrolled. Defaults to `value` or today.\n */\n readonly defaultValue?: Date;\n\n /**\n * First day of the week: 0 = Sunday, 1 = Monday.\n * @default 1\n */\n readonly firstDayOfWeek?: 0 | 1;\n\n /**\n * When true, disable input and render the grid dim.\n */\n readonly isDisabled?: boolean;\n\n /**\n * Upper bound; days after `maxDate` cannot be selected.\n */\n readonly maxDate?: Date;\n\n /**\n * Lower bound; days before `minDate` cannot be selected.\n */\n readonly minDate?: Date;\n\n /**\n * Called whenever the user navigates or selects a day.\n */\n readonly onChange?: (date: Date) => void;\n\n /**\n * Called when the user presses Enter.\n */\n readonly onSubmit?: (date: Date) => void;\n\n /**\n * Color used to mark the currently selected day.\n * @default \"green\"\n */\n readonly selectedColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Controlled selected date.\n */\n readonly value?: Date;\n};\n\nconst MONTH_NAMES = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"] as const;\n\nconst WEEKDAYS = [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"] as const;\n\nconst startOfDay = (date: Date): Date => new Date(date.getFullYear(), date.getMonth(), date.getDate());\n\n/**\n * Move `date` by `delta` months while clamping the day so end-of-month\n * navigation does not roll over (e.g., Jan 31 + 1 month becomes Feb 28/29\n * instead of overflowing to March 3).\n */\nconst shiftMonth = (date: Date, delta: number): Date => {\n const targetYear = date.getFullYear();\n const targetMonth = date.getMonth() + delta;\n // `new Date(year, monthIndex + 1, 0)` returns the last day of `monthIndex`.\n const lastDay = new Date(targetYear, targetMonth + 1, 0).getDate();\n const day = Math.min(date.getDate(), lastDay);\n\n return new Date(targetYear, targetMonth, day);\n};\n\nconst isSameDay = (a: Date, b: Date): boolean => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n\nconst isOutOfRange = (date: Date, minDate: Date | undefined, maxDate: Date | undefined): boolean => {\n if (minDate !== undefined && date < startOfDay(minDate)) {\n return true;\n }\n\n if (maxDate !== undefined && date > startOfDay(maxDate)) {\n return true;\n }\n\n return false;\n};\n\nconst buildWeeks = (year: number, month: number, firstDayOfWeek: 0 | 1): ReadonlyArray<ReadonlyArray<Date | undefined>> => {\n const firstOfMonth = new Date(year, month, 1);\n const firstDow = firstOfMonth.getDay();\n const leadingBlanks = (firstDow - firstDayOfWeek + 7) % 7;\n\n const daysInMonth = new Date(year, month + 1, 0).getDate();\n\n const cells: (Date | undefined)[] = [];\n\n for (let index = 0; index < leadingBlanks; index += 1) {\n cells.push(undefined);\n }\n\n for (let day = 1; day <= daysInMonth; day += 1) {\n cells.push(new Date(year, month, day));\n }\n\n while (cells.length % 7 !== 0) {\n cells.push(undefined);\n }\n\n const weeks: ReadonlyArray<Date | undefined>[] = [];\n\n for (let row = 0; row < cells.length; row += 7) {\n weeks.push(cells.slice(row, row + 7));\n }\n\n return weeks;\n};\n\n/**\n * Month-grid date picker. Arrow keys move focus by one day, page up/down move\n * by month. Enter submits; Space or Enter selects.\n * @returns A bordered `ReactElement` containing the month header, weekday\n * labels, and the day grid.\n */\nexport default function Calendar({\n accentColor = \"blue\",\n autoFocus = false,\n defaultValue,\n firstDayOfWeek = 1,\n isDisabled = false,\n maxDate,\n minDate,\n onChange,\n onSubmit,\n selectedColor = \"green\",\n value,\n}: Props): ReactElement {\n const { isFocused } = useFocus({ autoFocus, isActive: !isDisabled });\n // eslint-disable-next-line no-restricted-syntax -- today must be stable across renders; useRef would lose semantic clarity\n const today = useMemo(() => startOfDay(new Date()), []);\n const initial = startOfDay(value ?? defaultValue ?? today);\n const [internalCursor, setInternalCursor] = useState(initial);\n const cursor = value ? startOfDay(value) : internalCursor;\n const cursorRef = useRef(cursor);\n\n cursorRef.current = cursor;\n\n const weekdayLabels = useMemo(() => [...WEEKDAYS.slice(firstDayOfWeek), ...WEEKDAYS.slice(0, firstDayOfWeek)], [firstDayOfWeek]);\n\n const weeks = useMemo(() => buildWeeks(cursor.getFullYear(), cursor.getMonth(), firstDayOfWeek), [cursor, firstDayOfWeek]);\n\n const moveCursor = useCallback(\n (next: Date) => {\n const clamped = startOfDay(next);\n\n if (isOutOfRange(clamped, minDate, maxDate)) {\n return;\n }\n\n if (value === undefined) {\n setInternalCursor(clamped);\n }\n\n onChange?.(clamped);\n },\n [value, onChange, minDate, maxDate],\n );\n\n useInput(\n useCallback(\n (_, key) => {\n if (!isFocused) {\n return;\n }\n\n const { current } = cursorRef;\n\n if (key.leftArrow) {\n moveCursor(new Date(current.getFullYear(), current.getMonth(), current.getDate() - 1));\n } else if (key.rightArrow) {\n moveCursor(new Date(current.getFullYear(), current.getMonth(), current.getDate() + 1));\n } else if (key.upArrow) {\n moveCursor(new Date(current.getFullYear(), current.getMonth(), current.getDate() - 7));\n } else if (key.downArrow) {\n moveCursor(new Date(current.getFullYear(), current.getMonth(), current.getDate() + 7));\n } else if (key.pageUp) {\n moveCursor(shiftMonth(current, -1));\n } else if (key.pageDown) {\n moveCursor(shiftMonth(current, 1));\n } else if (key.return) {\n onSubmit?.(current);\n }\n },\n [isFocused, moveCursor, onSubmit],\n ),\n { isActive: !isDisabled && isFocused },\n );\n\n const monthLabel = `${MONTH_NAMES[cursor.getMonth()]} ${cursor.getFullYear()}`;\n\n return (\n <Box borderColor={isFocused ? accentColor : undefined} borderStyle=\"round\" flexDirection=\"column\" paddingX={1}>\n <Box justifyContent=\"center\">\n <Text bold color={accentColor}>\n {monthLabel}\n </Text>\n </Box>\n <Box marginTop={1}>\n {weekdayLabels.map((name) => (\n <Box key={name} width={3}>\n <Text dimColor>{name.padEnd(2, \" \")}</Text>\n </Box>\n ))}\n </Box>\n {weeks.map((week, weekIndex) => (\n // eslint-disable-next-line react-x/no-array-index-key -- calendar grid is positionally stable\n <Box key={weekIndex}>\n {week.map((day, dayIndex) => {\n if (day === undefined) {\n return (\n // eslint-disable-next-line react-x/no-array-index-key -- calendar grid is positionally stable\n <Box key={dayIndex} width={3}>\n <Text>{\" \"}</Text>\n </Box>\n );\n }\n\n const isToday = isSameDay(day, today);\n const isCursor = isSameDay(day, cursor);\n const outOfRange = isOutOfRange(day, minDate, maxDate);\n const focusedColor = isToday ? selectedColor : undefined;\n const dayColor = isCursor && isFocused ? \"black\" : focusedColor;\n\n return (\n // eslint-disable-next-line react-x/no-array-index-key -- calendar grid is positionally stable\n <Box key={dayIndex} width={3}>\n <Text\n backgroundColor={isCursor && isFocused ? accentColor : undefined}\n bold={isToday}\n color={dayColor}\n dimColor={outOfRange || isDisabled}\n >\n {String(day.getDate()).padStart(2, \" \")}\n{\" \"}\n </Text>\n </Box>\n );\n })}\n </Box>\n ))}\n </Box>\n );\n}\n\nexport { Calendar };\nexport type { Props as CalendarProps };\n",
|
|
19
|
+
"path": "registry/ui/calendar.tsx",
|
|
20
|
+
"target": "components/ui/calendar.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
package/dist/r/card.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "card",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Card",
|
|
6
|
+
"description": "Card component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"https://visulima.com/r/divider.json"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
{
|
|
20
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nimport Divider from \"@/components/ui/divider\";\n\nexport type Props = {\n /**\n * Border color.\n */\n readonly borderColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Border style name, passed through to `Box`.\n * @default \"round\"\n */\n readonly borderStyle?: \"arrow\" | \"bold\" | \"classic\" | \"double\" | \"doubleSingle\" | \"round\" | \"single\" | \"singleDouble\";\n\n /**\n * Card body.\n */\n readonly children: ReactNode;\n\n /**\n * Optional footer rendered below body with a dim divider.\n */\n readonly footer?: ReactNode;\n\n /**\n * Optional content rendered after the title on the same row.\n */\n readonly headerRight?: ReactNode;\n\n /**\n * Optional subtitle rendered below the title.\n */\n readonly subtitle?: string;\n\n /**\n * Optional title rendered in the header area.\n */\n readonly title?: string;\n\n /**\n * Color applied to the title.\n */\n readonly titleColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Fix the card width. When omitted the card grows to fill its parent.\n */\n readonly width?: number | string;\n};\n\n/**\n * Bordered container with optional title, subtitle, and footer.\n */\nexport default function Card({ borderColor, borderStyle = \"round\", children, footer, headerRight, subtitle, title, titleColor, width }: Props): ReactElement {\n const hasHeader = title !== undefined || subtitle !== undefined || headerRight !== undefined;\n\n return (\n <Box borderColor={borderColor} borderStyle={borderStyle} flexDirection=\"column\" paddingX={1} width={width}>\n {hasHeader\n ? (\n <Box flexDirection=\"column\" marginBottom={1}>\n <Box justifyContent=\"space-between\">\n <Box>\n {title === undefined\n ? undefined\n : (\n <Text bold color={titleColor}>\n {title}\n </Text>\n )}\n </Box>\n {headerRight === undefined ? undefined : <Box>{headerRight}</Box>}\n </Box>\n {subtitle === undefined ? undefined : <Text dimColor>{subtitle}</Text>}\n </Box>\n )\n : undefined}\n <Box flexDirection=\"column\">{children}</Box>\n {footer === undefined\n ? undefined\n : (\n <Box flexDirection=\"column\" marginTop={1}>\n <Divider dimColor length={typeof width === \"number\" ? Math.max(1, width - 4) : undefined} />\n <Box marginTop={1}>{footer}</Box>\n </Box>\n )}\n </Box>\n );\n}\n\nexport { Card };\nexport type { Props as CardProps };\n",
|
|
21
|
+
"path": "registry/ui/card.tsx",
|
|
22
|
+
"target": "components/ui/card.tsx",
|
|
23
|
+
"type": "registry:ui"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "chart-utils",
|
|
4
|
+
"type": "registry:lib",
|
|
5
|
+
"title": "Chart Utils",
|
|
6
|
+
"description": "Chart Utils component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "import type { AnsiColors } from \"@visulima/colorize\";\nimport type { LiteralUnion } from \"type-fest\";\n\n/**\n * A single 2D data point.\n */\nexport type Point = { readonly x: number; readonly y: number };\n\n/**\n * Minimal shape every \"series-like\" chart entry conforms to. Used so chart\n * components share utilities without coupling to a specific Props type.\n */\nexport type SeriesLike = {\n readonly color?: LiteralUnion<AnsiColors, string>;\n readonly data: ReadonlyArray<number> | ReadonlyArray<Point>;\n readonly label?: string;\n};\n\n/**\n * Default palette cycled through when a series does not declare its own color.\n */\nexport const DEFAULT_CHART_PALETTE: ReadonlyArray<LiteralUnion<AnsiColors, string>> = [\"cyan\", \"magenta\", \"yellow\", \"green\", \"blue\", \"red\"];\n\n/**\n * Normalize the per-series `data` prop into a uniform `Point[]`. Numeric\n * arrays are treated as y-values with their index as x.\n * @param data Either a numeric array or an array of `{x, y}` points.\n * @returns A uniform `Point[]` — empty when no samples were provided.\n */\nexport const toPoints = (data: SeriesLike[\"data\"]): ReadonlyArray<Point> => {\n if (data.length === 0) {\n return [];\n }\n\n const first = data[0];\n\n if (typeof first === \"number\") {\n return (data as ReadonlyArray<number>).map((y, x) => {\n return { x, y };\n });\n }\n\n return data as ReadonlyArray<Point>;\n};\n\n/**\n * Pick the effective color for a series: per-series override, else palette\n * cycling by index, with a safe `\"cyan\"` fallback.\n * @param series The series whose color we want.\n * @param index The series index for palette cycling.\n * @param palette Fallback palette.\n * @returns The resolved color string.\n */\nexport const pickSeriesColor = (\n series: SeriesLike,\n index: number,\n palette: ReadonlyArray<LiteralUnion<AnsiColors, string>>,\n): LiteralUnion<AnsiColors, string> => series.color ?? palette[index % palette.length] ?? \"cyan\";\n\nexport type Extents = {\n readonly xMax: number;\n readonly xMin: number;\n readonly yMax: number;\n readonly yMin: number;\n};\n\n/**\n * Single-pass min/max scan for an arbitrary collection of series. Returns\n * `Infinity` / `-Infinity` sentinels when no points are provided; callers\n * should replace these with their chosen defaults before rendering.\n * @param seriesList Iterable of series whose projected `points` need extents.\n * @returns The `{xMin, xMax, yMin, yMax}` tuple across all input points.\n */\nexport const computeExtents = (seriesList: Iterable<{ readonly points: ReadonlyArray<Point> }>): Extents => {\n let xMin = Number.POSITIVE_INFINITY;\n let xMax = Number.NEGATIVE_INFINITY;\n let yMin = Number.POSITIVE_INFINITY;\n let yMax = Number.NEGATIVE_INFINITY;\n\n for (const { points } of seriesList) {\n for (const point of points) {\n if (point.x < xMin) {\n xMin = point.x;\n }\n\n if (point.x > xMax) {\n xMax = point.x;\n }\n\n if (point.y < yMin) {\n yMin = point.y;\n }\n\n if (point.y > yMax) {\n yMax = point.y;\n }\n }\n }\n\n return { xMax, xMin, yMax, yMin };\n};\n",
|
|
19
|
+
"path": "registry/lib/chart-utils.ts",
|
|
20
|
+
"target": "lib/chart-utils.ts",
|
|
21
|
+
"type": "registry:lib"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "checkbox-group",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Checkbox Group",
|
|
6
|
+
"description": "Checkbox Group component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement } from \"react\";\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nconst EMPTY_VALUES: ReadonlyArray<string> = [];\n\nexport type CheckboxOption = {\n readonly description?: string;\n readonly isDisabled?: boolean;\n readonly label: string;\n readonly value: string;\n};\n\nexport type Props = {\n /**\n * Accent color for the focused row and checked marks.\n * @default \"blue\"\n */\n readonly accentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Auto-focus on mount.\n */\n readonly autoFocus?: boolean;\n\n /**\n * Checked values when uncontrolled.\n */\n readonly defaultValue?: ReadonlyArray<string>;\n\n /**\n * Disable the whole group.\n */\n readonly isDisabled?: boolean;\n\n /**\n * Fires whenever the set of checked values changes.\n */\n readonly onChange?: (values: ReadonlyArray<string>) => void;\n\n /**\n * Fires on Enter with the current checked values.\n */\n readonly onSubmit?: (values: ReadonlyArray<string>) => void;\n\n /**\n * The options to render.\n */\n readonly options: ReadonlyArray<CheckboxOption>;\n\n /**\n * Controlled checked values. When provided, `defaultValue` is ignored.\n */\n readonly value?: ReadonlyArray<string>;\n};\n\nfunction wrap(value: number, size: number): number {\n return size === 0 ? 0 : ((value % size) + size) % size;\n}\n\n/**\n * A vertical list of checkboxes. ↑/↓ move focus, Space toggles the focused\n * option, `a` toggles all, and Enter submits.\n */\nexport default function CheckboxGroup({\n accentColor = \"blue\",\n autoFocus = false,\n defaultValue = EMPTY_VALUES,\n isDisabled = false,\n onChange,\n onSubmit,\n options,\n value: controlledValue,\n}: Props): ReactElement {\n const { isFocused } = useFocus({ autoFocus, isActive: !isDisabled });\n const isControlled = controlledValue !== undefined;\n\n const [internal, setInternal] = useState<ReadonlyArray<string>>(defaultValue);\n const checked = useMemo(() => new Set(controlledValue ?? internal), [controlledValue, internal]);\n\n const [cursor, setCursor] = useState(0);\n\n const onChangeRef = useRef(onChange);\n\n onChangeRef.current = onChange;\n\n const emit = useCallback(\n (next: Set<string>) => {\n const values = options.map((option) => option.value).filter((value) => next.has(value));\n\n if (!isControlled) {\n setInternal(values);\n }\n\n onChangeRef.current?.(values);\n\n return values;\n },\n [isControlled, options],\n );\n\n const inputHandler = useCallback(\n (input: string, key: { downArrow: boolean; return: boolean; upArrow: boolean }) => {\n if (key.upArrow) {\n setCursor((index) => wrap(index - 1, options.length));\n\n return;\n }\n\n if (key.downArrow) {\n setCursor((index) => wrap(index + 1, options.length));\n\n return;\n }\n\n if (input === \" \") {\n const option = options[cursor];\n\n if (option === undefined || option.isDisabled) {\n return;\n }\n\n const next = new Set(checked);\n\n if (next.has(option.value)) {\n next.delete(option.value);\n } else {\n next.add(option.value);\n }\n\n emit(next);\n\n return;\n }\n\n if (input === \"a\") {\n const enabled = options.filter((option) => !option.isDisabled);\n const allChecked = enabled.every((option) => checked.has(option.value));\n // Disabled rows aren't user-mutable: carry their checked state\n // through toggle-all in both directions.\n const next = new Set(options.filter((option) => option.isDisabled && checked.has(option.value)).map((option) => option.value));\n\n if (!allChecked) {\n for (const option of enabled) {\n next.add(option.value);\n }\n }\n\n emit(next);\n\n return;\n }\n\n if (key.return) {\n onSubmit?.(options.map((option) => option.value).filter((value) => checked.has(value)));\n }\n },\n [checked, cursor, emit, onSubmit, options],\n );\n\n useInput(inputHandler, { isActive: isFocused && !isDisabled });\n\n return (\n <Box flexDirection=\"column\">\n {options.map((option, index) => {\n const isActive = isFocused && index === cursor;\n const isChecked = checked.has(option.value);\n const mark = isChecked ? \"◉\" : \"◯\";\n const isHighlighted = !option.isDisabled && (isChecked || isActive);\n const color = isHighlighted ? accentColor : undefined;\n\n return (\n // eslint-disable-next-line react-x/no-array-index-key -- option index is stable for the render\n <Box key={index}>\n <Text color={isActive ? accentColor : undefined}>{isActive ? \"❯ \" : \" \"}</Text>\n <Text color={color} dimColor={option.isDisabled}>\n {mark}\n {\" \"}\n {option.label}\n </Text>\n {option.description === undefined\n ? undefined\n : (\n <Box marginLeft={1}>\n <Text dimColor>{option.description}</Text>\n </Box>\n )}\n </Box>\n );\n })}\n </Box>\n );\n}\n\nexport { CheckboxGroup };\nexport type { Props as CheckboxGroupProps };\n",
|
|
19
|
+
"path": "registry/ui/checkbox-group.tsx",
|
|
20
|
+
"target": "components/ui/checkbox-group.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "checkbox",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Checkbox",
|
|
6
|
+
"description": "Checkbox component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useCallback, useState } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type Props = {\n /**\n * Accent color when focused.\n * @default \"blue\"\n */\n readonly accentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Auto-focus when mounted if nothing else is focused.\n */\n readonly autoFocus?: boolean;\n\n /**\n * Label rendered next to the checkbox.\n */\n readonly children?: ReactNode;\n\n /**\n * Initial checked state when uncontrolled.\n * @default false\n */\n readonly defaultChecked?: boolean;\n\n /**\n * Controlled checked state. When provided, `onChange` should update the parent.\n */\n readonly isChecked?: boolean;\n\n /**\n * Disable input and dim the label.\n */\n readonly isDisabled?: boolean;\n\n /**\n * Fired when the checkbox toggles.\n */\n readonly onChange?: (checked: boolean) => void;\n};\n\nconst CHECKED_ICON = \"☒\";\nconst UNCHECKED_ICON = \"☐\";\n\n/**\n * Focusable checkbox. Toggles with Space or Enter.\n */\nexport default function Checkbox({\n accentColor = \"blue\",\n autoFocus = false,\n children,\n defaultChecked = false,\n isChecked,\n isDisabled = false,\n onChange,\n}: Props): ReactElement {\n const { isFocused } = useFocus({ autoFocus, isActive: !isDisabled });\n const [internalChecked, setInternalChecked] = useState(defaultChecked);\n const checked = isChecked ?? internalChecked;\n\n useInput(\n useCallback(\n (input, key) => {\n if (!isFocused) {\n return;\n }\n\n if (key.return || input === \" \") {\n const next = !checked;\n\n if (isChecked === undefined) {\n setInternalChecked(next);\n }\n\n onChange?.(next);\n }\n },\n [isFocused, checked, isChecked, onChange],\n ),\n { isActive: !isDisabled && isFocused },\n );\n\n return (\n <Box gap={1}>\n <Text color={isFocused ? accentColor : undefined} dimColor={isDisabled}>\n {isFocused ? \"▸ \" : \" \"}\n {checked ? CHECKED_ICON : UNCHECKED_ICON}\n </Text>\n {children === undefined\n ? undefined\n : (\n <Text color={isFocused ? accentColor : undefined} dimColor={isDisabled}>\n {children}\n </Text>\n )}\n </Box>\n );\n}\n\nexport { Checkbox };\nexport type { Props as CheckboxProps };\n",
|
|
19
|
+
"path": "registry/ui/checkbox.tsx",
|
|
20
|
+
"target": "components/ui/checkbox.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
package/dist/r/code.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "code",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Code",
|
|
6
|
+
"description": "Code component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/tui",
|
|
12
|
+
"shiki"
|
|
13
|
+
],
|
|
14
|
+
"registryDependencies": [],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"content": "/* eslint-disable consistent-return, no-void, react-x/no-array-index-key, react-you-might-not-need-an-effect/no-adjust-state-on-prop-change, react/function-component-definition */\n\n/**\n * Syntax-highlighted code display component for Ink.\n *\n * Uses Shiki for tokenization and maps themed tokens to colored\n * Ink Text elements for terminal rendering.\n */\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport type { TokenRenderOptions } from \"@visulima/tui/highlighter\";\nimport { getCachedTokens, getHighlighter, isLanguageSupported, resolveLanguage, tokenLinesToElements } from \"@visulima/tui/highlighter\";\nimport type { ReactElement } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { ThemedToken } from \"shiki\";\n\nexport type Props = {\n /**\n * The source code string to highlight.\n */\n readonly code: string;\n\n /**\n * Set of line numbers to visually emphasize.\n */\n readonly highlightLines?: ReadonlySet<number>;\n\n /**\n * Programming language for syntax highlighting.\n * Falls back to plain text if the language is unknown.\n */\n readonly language?: string;\n\n /**\n * Whether to show line numbers in the gutter.\n * @default false\n */\n readonly showLineNumbers?: boolean;\n\n /**\n * Starting line number (useful for code excerpts).\n * @default 1\n */\n readonly startLine?: number;\n\n /**\n * Shiki theme name.\n * @default \"github-dark-default\"\n */\n readonly theme?: string;\n};\n\n/**\n * Render source code with syntax highlighting.\n *\n * ```tsx\n * <Code code=\"const x = 42;\" language=\"typescript\" />\n * <Code code={source} language=\"python\" showLineNumbers />\n * ```\n */\nexport default function Code({ code, highlightLines, language, showLineNumbers = false, startLine = 1, theme = \"github-dark-default\" }: Props): ReactElement {\n const [tokens, setTokens] = useState<ThemedToken[][] | null>(null);\n const cancelledRef = useRef(false);\n\n useEffect(() => {\n cancelledRef.current = false;\n\n const lang = language ? resolveLanguage(language) : \"text\";\n\n if (lang !== \"text\" && !isLanguageSupported(lang)) {\n // Unknown language — stay with plain rendering\n setTokens(null); // eslint-disable-line react-x/set-state-in-effect\n\n return;\n }\n\n void (async () => {\n try {\n const highlighter = await getHighlighter(lang === \"text\" ? [] : [lang], theme);\n const result = getCachedTokens(highlighter, code, lang, theme);\n\n if (!cancelledRef.current) {\n setTokens(result.tokens);\n }\n } catch {\n // Highlighting failed — fall back to plain text\n if (!cancelledRef.current) {\n setTokens(null);\n }\n }\n })();\n\n return () => {\n cancelledRef.current = true;\n };\n }, [code, language, theme]);\n\n const renderOptions: TokenRenderOptions = { highlightLines, showLineNumbers, startLine };\n\n // Highlighted rendering\n if (tokens) {\n return tokenLinesToElements(tokens, renderOptions);\n }\n\n // Plain text fallback (before highlighting resolves or for unsupported languages)\n const lines = code.split(\"\\n\");\n const gutterWidth = showLineNumbers ? String(startLine + lines.length - 1).length : 0;\n\n return (\n <Box flexDirection=\"column\">\n {lines.map((line, index) => {\n const lineNumber = startLine + index;\n const isHighlighted = highlightLines?.has(lineNumber) ?? false;\n\n return (\n <Box key={index}>\n {showLineNumbers\n ? (\n <Text color={isHighlighted ? \"yellow\" : undefined} dimColor={!isHighlighted}>\n {String(lineNumber).padStart(gutterWidth)}\n{\" \"}\n </Text>\n )\n : undefined}\n <Text>{line}</Text>\n </Box>\n );\n })}\n </Box>\n );\n}\n\nexport { Code };\nexport type { Props as CodeProps };\n",
|
|
18
|
+
"path": "registry/ui/code.tsx",
|
|
19
|
+
"target": "components/ui/code.tsx",
|
|
20
|
+
"type": "registry:ui"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "collapsible",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Collapsible",
|
|
6
|
+
"description": "Collapsible component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useCallback, useState } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type Props = {\n /**\n * Accent color when the header is focused.\n * @default \"blue\"\n */\n readonly accentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Auto-focus the collapsible on mount.\n */\n readonly autoFocus?: boolean;\n\n /**\n * Child content rendered when open.\n */\n readonly children: ReactNode;\n\n /**\n * Whether the section starts expanded.\n * @default false\n */\n readonly defaultOpen?: boolean;\n\n /**\n * Disable interaction.\n */\n readonly isDisabled?: boolean;\n\n /**\n * Controlled open state.\n */\n readonly isOpen?: boolean;\n\n /**\n * Called when the user toggles the section.\n */\n readonly onToggle?: (isOpen: boolean) => void;\n\n /**\n * Header label.\n */\n readonly title: ReactNode;\n};\n\n/**\n * Focusable section that can be expanded or collapsed.\n */\nexport default function Collapsible({\n accentColor = \"blue\",\n autoFocus = false,\n children,\n defaultOpen = false,\n isDisabled = false,\n isOpen,\n onToggle,\n title,\n}: Props): ReactElement {\n const { isFocused } = useFocus({ autoFocus, isActive: !isDisabled });\n const [internal, setInternal] = useState(defaultOpen);\n const open = isOpen ?? internal;\n\n useInput(\n useCallback(\n (input, key) => {\n if (key.return || input === \" \") {\n const next = !open;\n\n if (isOpen === undefined) {\n setInternal(next);\n }\n\n onToggle?.(next);\n }\n },\n [open, isOpen, onToggle],\n ),\n { isActive: !isDisabled && isFocused },\n );\n\n return (\n <Box flexDirection=\"column\">\n <Box>\n <Text color={isFocused ? accentColor : undefined} dimColor={isDisabled}>\n {open ? \"▼ \" : \"▶ \"}\n </Text>\n <Text bold={isFocused} color={isFocused ? accentColor : undefined} dimColor={isDisabled}>\n {title}\n </Text>\n </Box>\n {open\n ? (\n <Box flexDirection=\"column\" marginLeft={2}>\n {children}\n </Box>\n )\n : undefined}\n </Box>\n );\n}\n\nexport { Collapsible };\nexport type { Props as CollapsibleProps };\n",
|
|
19
|
+
"path": "registry/ui/collapsible.tsx",
|
|
20
|
+
"target": "components/ui/collapsible.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "color-picker",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Color Picker",
|
|
6
|
+
"description": "Color Picker component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement } from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\ntype Color = LiteralUnion<AnsiColors, string>;\n\nconst DEFAULT_PALETTE: ReadonlyArray<Color> = [\n \"black\",\n \"red\",\n \"green\",\n \"yellow\",\n \"blue\",\n \"magenta\",\n \"cyan\",\n \"white\",\n \"gray\",\n \"redBright\",\n \"greenBright\",\n \"yellowBright\",\n \"blueBright\",\n \"magentaBright\",\n \"cyanBright\",\n \"whiteBright\",\n];\n\nexport type Props = {\n /**\n * Accent color for the focused-cell outline.\n * @default \"blue\"\n */\n readonly accentColor?: Color;\n\n /**\n * Auto-focus on mount.\n */\n readonly autoFocus?: boolean;\n\n /**\n * Number of swatches per row.\n * @default 8\n */\n readonly columns?: number;\n\n /**\n * Selected color when uncontrolled.\n */\n readonly defaultValue?: Color;\n\n /**\n * Disable input and dim the grid.\n */\n readonly isDisabled?: boolean;\n\n /**\n * Fires when the highlighted swatch changes.\n */\n readonly onChange?: (color: Color) => void;\n\n /**\n * Fires on Enter with the highlighted swatch.\n */\n readonly onSubmit?: (color: Color) => void;\n\n /**\n * Colors to offer. Defaults to the 16 standard ANSI colors.\n */\n readonly palette?: ReadonlyArray<Color>;\n\n /**\n * Render the highlighted color's name beneath the grid.\n * @default true\n */\n readonly showName?: boolean;\n\n /**\n * The two-cell glyph filled with each palette color (default `\"██\"`).\n */\n readonly swatch?: string;\n\n /**\n * Controlled selected color. When provided, `defaultValue` is ignored.\n */\n readonly value?: Color;\n};\n\nfunction wrap(value: number, size: number): number {\n return size === 0 ? 0 : ((value % size) + size) % size;\n}\n\n/**\n * A grid of color swatches. Arrow keys move the highlight, Enter selects. The\n * highlighted swatch's name is shown below the grid by default.\n */\nexport default function ColorPicker({\n accentColor = \"blue\",\n autoFocus = false,\n columns = 8,\n defaultValue,\n isDisabled = false,\n onChange,\n onSubmit,\n palette = DEFAULT_PALETTE,\n showName = true,\n swatch = \"██\",\n value: controlledValue,\n}: Props): ReactElement {\n const { isFocused } = useFocus({ autoFocus, isActive: !isDisabled });\n const isControlled = controlledValue !== undefined;\n // Guard against columns <= 0, which would never advance the row loop.\n const columnCount = Math.max(1, columns);\n\n const startIndex = Math.max(0, palette.indexOf((controlledValue ?? defaultValue) as Color));\n const [cursor, setCursor] = useState(startIndex);\n\n const effectiveCursor = isControlled ? Math.max(0, palette.indexOf(controlledValue)) : cursor;\n\n const onChangeRef = useRef(onChange);\n\n onChangeRef.current = onChange;\n\n const move = useCallback(\n (delta: number) => {\n if (palette.length === 0) {\n return;\n }\n\n const next = wrap(effectiveCursor + delta, palette.length);\n\n if (!isControlled) {\n setCursor(next);\n }\n\n onChangeRef.current?.(palette[next]!);\n },\n [effectiveCursor, isControlled, palette],\n );\n\n const inputHandler = useCallback(\n (_input: string, key: { downArrow: boolean; leftArrow: boolean; return: boolean; rightArrow: boolean; upArrow: boolean }) => {\n if (key.leftArrow) {\n move(-1);\n } else if (key.rightArrow) {\n move(1);\n } else if (key.upArrow) {\n move(-columnCount);\n } else if (key.downArrow) {\n move(columnCount);\n } else if (key.return) {\n onSubmit?.(palette[effectiveCursor]!);\n }\n },\n [columnCount, effectiveCursor, move, onSubmit, palette],\n );\n\n useInput(inputHandler, { isActive: isFocused && !isDisabled });\n\n const rows: Color[][] = [];\n\n for (let index = 0; index < palette.length; index += columnCount) {\n rows.push(palette.slice(index, index + columnCount));\n }\n\n return (\n <Box flexDirection=\"column\">\n {rows.map((row, rowIndex) => (\n // eslint-disable-next-line react-x/no-array-index-key -- row index is stable for the render\n <Box key={rowIndex}>\n {row.map((color, columnIndex) => {\n const index = rowIndex * columnCount + columnIndex;\n const isActive = isFocused && index === effectiveCursor;\n\n return (\n // eslint-disable-next-line react-x/no-array-index-key -- cell index is stable for the render\n <Box key={columnIndex}>\n <Text color={isActive ? accentColor : undefined}>{isActive ? \"[\" : \" \"}</Text>\n <Text color={color} dimColor={isDisabled}>\n {swatch}\n </Text>\n <Text color={isActive ? accentColor : undefined}>{isActive ? \"]\" : \" \"}</Text>\n </Box>\n );\n })}\n </Box>\n ))}\n {showName\n ? (\n <Box marginTop={1}>\n <Text dimColor>{String(palette[effectiveCursor] ?? \"\")}</Text>\n </Box>\n )\n : undefined}\n </Box>\n );\n}\n\nexport { ColorPicker };\nexport type { Props as ColorPickerProps };\n",
|
|
19
|
+
"path": "registry/ui/color-picker.tsx",
|
|
20
|
+
"target": "components/ui/color-picker.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "command-block",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Command Block",
|
|
6
|
+
"description": "Command Block component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"https://visulima.com/r/spinner.json"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
{
|
|
20
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nimport Spinner from \"@/components/ui/spinner\";\n\nexport type CommandStatus = \"error\" | \"idle\" | \"running\" | \"success\";\n\nexport type Props = {\n /**\n * Shell command rendered in the header.\n */\n readonly command: string;\n\n /**\n * Optional working directory shown after the command name.\n */\n readonly cwd?: string;\n\n /**\n * Duration in ms; shown when finished.\n */\n readonly durationMs?: number;\n\n /**\n * Exit code of the completed command.\n */\n readonly exitCode?: number;\n\n /**\n * Max output rows rendered before truncation. Use `Infinity` to disable.\n */\n readonly maxOutputRows?: number;\n\n /**\n * Command output. Either a string (rendered as-is) or arbitrary children.\n */\n readonly output?: ReactNode;\n\n /**\n * Lifecycle state. Controls the header icon and color.\n * @default \"idle\"\n */\n readonly status?: CommandStatus;\n};\n\nconst STATUS_COLOR: Record<CommandStatus, LiteralUnion<AnsiColors, string>> = {\n error: \"red\",\n idle: \"gray\",\n running: \"cyan\",\n success: \"green\",\n};\n\nconst formatDuration = (ms: number): string => {\n if (ms < 1000) {\n return `${Math.round(ms)}ms`;\n }\n\n return `${(ms / 1000).toFixed(2)}s`;\n};\n\n/**\n * Resolve the header icon for a given status. `running` returns a live\n * Spinner; every other value maps to a static glyph.\n */\n// eslint-disable-next-line sonarjs/function-return-type -- legitimate union return: spinner element vs static glyph string\nconst resolveStatusIcon = (status: CommandStatus): ReactNode => {\n if (status === \"running\") {\n return <Spinner type=\"dots\" />;\n }\n\n const ICONS: Record<Exclude<CommandStatus, \"running\">, string> = {\n error: \"✖\",\n idle: \"$\",\n success: \"✔\",\n };\n\n return ICONS[status];\n};\n\nconst truncateOutput = (output: string, maxRows: number): string => {\n if (!Number.isFinite(maxRows)) {\n return output;\n }\n\n const rows = output.split(\"\\n\");\n\n if (rows.length <= maxRows) {\n return output;\n }\n\n const kept = rows.slice(0, maxRows).join(\"\\n\");\n const hidden = rows.length - maxRows;\n\n return `${kept}\\n… ${hidden} more line${hidden === 1 ? \"\" : \"s\"}`;\n};\n\n/**\n * Renders a shell command with its output and a status header. Designed for\n * coding-agent UIs that stream terminal operations.\n */\nexport default function CommandBlock({ command, cwd, durationMs, exitCode, maxOutputRows = 12, output, status = \"idle\" }: Props): ReactElement {\n const color = STATUS_COLOR[status];\n const statusIcon = resolveStatusIcon(status);\n\n const trailing: ReactNode[] = [];\n\n if (durationMs !== undefined) {\n trailing.push(\n <Text dimColor key=\"duration\">\n {\" \"}\n {formatDuration(durationMs)}\n </Text>,\n );\n }\n\n if (exitCode !== undefined && status !== \"running\") {\n trailing.push(\n <Text color={exitCode === 0 ? \"green\" : \"red\"} key=\"exit\">\n {\" \"}\n exit\n{\" \"}\n{exitCode}\n </Text>,\n );\n }\n\n return (\n <Box borderColor={color} borderStyle=\"round\" flexDirection=\"column\" paddingX={1}>\n <Box>\n <Text color={color}>{statusIcon}</Text>\n <Text> </Text>\n <Box flexGrow={1} flexShrink={1} minWidth={0}>\n <Text bold wrap=\"truncate-end\">\n {command}\n </Text>\n </Box>\n {cwd === undefined\n ? undefined\n : (\n<Text dimColor>\n{\" \"}\n@\n{cwd}\n</Text>\n )}\n {trailing.length === 0 ? undefined : <Box flexShrink={0}>{trailing}</Box>}\n </Box>\n {output === undefined\n ? undefined\n : (\n <Box flexDirection=\"column\" marginTop={1}>\n {typeof output === \"string\" ? <Text>{truncateOutput(output, maxOutputRows)}</Text> : output}\n </Box>\n )}\n </Box>\n );\n}\n\nexport { CommandBlock };\nexport type { Props as CommandBlockProps };\n",
|
|
21
|
+
"path": "registry/ui/command-block.tsx",
|
|
22
|
+
"target": "components/ui/command-block.tsx",
|
|
23
|
+
"type": "registry:ui"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "command-palette",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Command Palette",
|
|
6
|
+
"description": "Command Palette component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport { isInsertableInput } from \"@visulima/tui/input\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type CommandEntry = {\n /**\n * Optional description rendered under the label.\n */\n readonly description?: string;\n\n /**\n * Optional hotkey rendered on the right.\n */\n readonly hotkey?: string;\n\n /**\n * Optional icon rendered before the label.\n */\n readonly icon?: ReactNode;\n\n /**\n * Stable identifier returned by `onSelect`.\n */\n readonly id: string;\n\n /**\n * Optional keywords to boost matching (not displayed).\n */\n readonly keywords?: ReadonlyArray<string>;\n\n /**\n * Label shown in the list (and used as the primary match target).\n */\n readonly label: string;\n};\n\nexport type Props = {\n /**\n * Accent color for the border and focused row.\n * @default \"cyan\"\n */\n readonly accentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Auto-focus the palette on mount.\n */\n readonly autoFocus?: boolean;\n\n /**\n * Available commands to search.\n */\n readonly commands: ReadonlyArray<CommandEntry>;\n\n /**\n * Empty-state text when nothing matches.\n * @default \"No results\"\n */\n readonly emptyText?: string;\n\n /**\n * Max rows rendered in the list.\n * @default 8\n */\n readonly limit?: number;\n\n /**\n * Called when the user presses Escape.\n */\n readonly onCancel?: () => void;\n\n /**\n * Called when the user picks a command with Enter.\n */\n readonly onSelect: (id: string, query: string) => void;\n\n /**\n * Placeholder inside the search field.\n * @default \"Type a command or search...\"\n */\n readonly placeholder?: string;\n};\n\n/**\n * Rank candidates by a simple subsequence scoring algorithm. Higher is better.\n */\nconst scoreCommand = (query: string, command: CommandEntry): number => {\n if (query.length === 0) {\n return 1;\n }\n\n const haystacks = [command.label, command.description ?? \"\", ...command.keywords ?? []]\n .map((text) => text.toLowerCase())\n .filter((text) => text.length > 0);\n\n const needle = query.toLowerCase();\n let best = 0;\n\n for (const haystack of haystacks) {\n if (haystack.includes(needle)) {\n // Direct substring match: the earlier the match, the better.\n const index = haystack.indexOf(needle);\n\n best = Math.max(best, 100 - index);\n\n continue;\n }\n\n // Fallback: subsequence match. Count consecutive chars.\n let haystackIndex = 0;\n let matched = 0;\n\n for (const char of needle) {\n const found = haystack.indexOf(char, haystackIndex);\n\n if (found === -1) {\n matched = 0;\n\n break;\n }\n\n matched += 1;\n haystackIndex = found + 1;\n }\n\n if (matched === needle.length) {\n best = Math.max(best, 20);\n }\n }\n\n return best;\n};\n\n/**\n * Ctrl-P style fuzzy command finder. Features a search input, ranked results,\n * keyboard navigation, and Esc to cancel.\n */\nexport default function CommandPalette({\n accentColor = \"cyan\",\n autoFocus = true,\n commands,\n emptyText = \"No results\",\n limit = 8,\n onCancel,\n onSelect,\n placeholder = \"Type a command or search...\",\n}: Props): ReactElement {\n const { isFocused } = useFocus({ autoFocus });\n const [query, setQuery] = useState(\"\");\n const [focusedIndex, setFocusedIndex] = useState(0);\n const focusedIndexRef = useRef(focusedIndex);\n\n focusedIndexRef.current = focusedIndex;\n\n const filtered = useMemo(() => {\n const scored = commands\n .map((command) => {\n return { command, score: scoreCommand(query, command) };\n })\n .filter(({ score }) => score > 0);\n\n scored.sort((a, b) => b.score - a.score);\n\n return scored.slice(0, limit).map(({ command }) => command);\n }, [commands, query, limit]);\n\n // Keep `focusedIndex` inside the live range. This handles shrinking\n // results (typing narrows the list) and external changes to `commands`\n // that would otherwise leave focus pointing past the last row.\n useEffect(() => {\n const maxIndex = Math.max(0, filtered.length - 1);\n\n if (focusedIndexRef.current > maxIndex) {\n focusedIndexRef.current = maxIndex;\n setFocusedIndex(maxIndex);\n }\n }, [filtered]);\n\n useInput(\n useCallback(\n (input, key) => {\n if (!isFocused) {\n return;\n }\n\n if (key.escape) {\n onCancel?.();\n\n return;\n }\n\n if (key.downArrow) {\n if (filtered.length === 0) {\n return;\n }\n\n setFocusedIndex((previous) => Math.min(filtered.length - 1, previous + 1));\n\n return;\n }\n\n if (key.upArrow) {\n setFocusedIndex((previous) => Math.max(0, previous - 1));\n\n return;\n }\n\n if (key.return) {\n const target = filtered[focusedIndexRef.current];\n\n if (target) {\n onSelect(target.id, query);\n }\n\n return;\n }\n\n if (key.backspace || key.delete) {\n setQuery((previous) => previous.slice(0, -1));\n setFocusedIndex(0);\n\n return;\n }\n\n if (isInsertableInput(input, key)) {\n setQuery((previous) => previous + input);\n setFocusedIndex(0);\n }\n },\n [isFocused, filtered, query, onSelect, onCancel],\n ),\n { isActive: isFocused },\n );\n\n return (\n <Box borderColor={accentColor} borderStyle=\"round\" flexDirection=\"column\" paddingX={1}>\n <Box>\n <Text color={accentColor}>❯</Text>\n <Text>\n{\" \"}\n{query.length === 0 ? <Text dimColor>{placeholder}</Text> : query}\n </Text>\n <Text inverse> </Text>\n </Box>\n <Box marginTop={1}>\n <Text dimColor>\n {filtered.length}\n {\" of \"}\n {commands.length}\n </Text>\n </Box>\n <Box flexDirection=\"column\" marginTop={1}>\n {filtered.length === 0\n ? (\n <Text dimColor>{emptyText}</Text>\n )\n : filtered.map((command, index) => {\n const isRowFocused = index === focusedIndex;\n const color = isRowFocused ? accentColor : undefined;\n\n return (\n <Box flexDirection=\"column\" key={command.id}>\n <Box>\n <Text color={color}>{isRowFocused ? \"▸ \" : \" \"}</Text>\n {command.icon === undefined\n ? undefined\n : (\n<Text color={color}>\n{command.icon}\n{\" \"}\n</Text>\n )}\n <Box flexGrow={1} flexShrink={1} minWidth={0}>\n <Text bold={isRowFocused} color={color} wrap=\"truncate-end\">\n {command.label}\n </Text>\n </Box>\n {command.hotkey === undefined\n ? undefined\n : (\n <Box flexShrink={0}>\n <Text dimColor>\n{\" \"}\n{command.hotkey}\n </Text>\n </Box>\n )}\n </Box>\n {command.description === undefined\n ? undefined\n : (\n <Box marginLeft={2}>\n <Text dimColor wrap=\"truncate-end\">\n {command.description}\n </Text>\n </Box>\n )}\n </Box>\n );\n })}\n </Box>\n </Box>\n );\n}\n\nexport { CommandPalette };\nexport type { Props as CommandPaletteProps };\n",
|
|
19
|
+
"path": "registry/ui/command-palette.tsx",
|
|
20
|
+
"target": "components/ui/command-palette.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "confirm-dialog",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Confirm Dialog",
|
|
6
|
+
"description": "Confirm Dialog component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/colorize",
|
|
12
|
+
"@visulima/tui",
|
|
13
|
+
"type-fest"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useCallback, useState } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type ConfirmTone = \"danger\" | \"info\" | \"warning\";\n\nexport type Props = {\n /**\n * Auto-focus the dialog on mount. Set `false` if a parent already owns\n * focus and routes input here.\n * @default true\n */\n readonly autoFocus?: boolean;\n\n /**\n * Label for the cancel button.\n * @default \"Cancel\"\n */\n readonly cancelLabel?: string;\n\n /**\n * Body content (rendered between title and buttons).\n */\n readonly children?: ReactNode;\n\n /**\n * Label for the confirm button.\n * @default \"Confirm\"\n */\n readonly confirmLabel?: string;\n\n /**\n * Default button when opening the dialog.\n * @default \"confirm\"\n */\n readonly defaultFocus?: \"cancel\" | \"confirm\";\n\n /**\n * Disable input.\n */\n readonly isDisabled?: boolean;\n\n /**\n * Fired when the user declines.\n */\n readonly onCancel: () => void;\n\n /**\n * Fired when the user confirms.\n */\n readonly onConfirm: () => void;\n\n /**\n * Heading rendered at the top of the dialog.\n */\n readonly title?: string;\n\n /**\n * Controls the border color.\n * @default \"info\"\n */\n readonly tone?: ConfirmTone;\n\n /**\n * Dialog width.\n */\n readonly width?: number;\n};\n\nconst TONE_COLOR: Record<ConfirmTone, LiteralUnion<AnsiColors, string>> = {\n danger: \"red\",\n info: \"blue\",\n warning: \"yellow\",\n};\n\ntype ButtonLabelProps = {\n readonly color: LiteralUnion<AnsiColors, string>;\n readonly isFocused: boolean;\n readonly label: string;\n};\n\n/**\n * Pill-shaped button rendered as the dialog's primary / secondary action.\n * Lives at module scope so it isn't recreated on every dialog render.\n */\nconst ButtonLabel = ({ color, isFocused, label }: ButtonLabelProps): ReactElement => (\n <Box backgroundColor={isFocused ? color : undefined} borderColor={isFocused ? color : undefined} borderStyle=\"round\" paddingX={1}>\n <Text bold={isFocused} color={isFocused ? \"black\" : undefined}>\n {label}\n </Text>\n </Box>\n);\n\n/**\n * Full-width modal-style confirm/cancel prompt. Unlike `ConfirmInput`, this\n * renders a visible two-button UI: ← / → to toggle focus, Enter to activate,\n * y/n as shortcuts, Esc to cancel.\n * @returns A bordered `ReactElement` with optional title, body, and two\n * buttons (cancel / confirm).\n */\nexport default function ConfirmDialog({\n autoFocus = true,\n cancelLabel = \"Cancel\",\n children,\n confirmLabel = \"Confirm\",\n defaultFocus = \"confirm\",\n isDisabled = false,\n onCancel,\n onConfirm,\n title,\n tone = \"info\",\n width,\n}: Props): ReactElement {\n const [focus, setFocus] = useState<\"cancel\" | \"confirm\">(defaultFocus);\n const color = TONE_COLOR[tone];\n const { isFocused } = useFocus({ autoFocus, isActive: !isDisabled });\n\n useInput(\n useCallback(\n (input, key) => {\n if (key.leftArrow || key.rightArrow || input === \"h\" || input === \"l\") {\n setFocus(focus === \"confirm\" ? \"cancel\" : \"confirm\");\n\n return;\n }\n\n if (key.return) {\n if (focus === \"confirm\") {\n onConfirm();\n } else {\n onCancel();\n }\n\n return;\n }\n\n if (key.escape || input === \"n\") {\n onCancel();\n\n return;\n }\n\n if (input === \"y\") {\n onConfirm();\n }\n },\n [focus, onConfirm, onCancel],\n ),\n { isActive: !isDisabled && isFocused },\n );\n\n return (\n <Box borderColor={color} borderStyle=\"round\" flexDirection=\"column\" paddingX={1} paddingY={1} width={width}>\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {title === undefined ? undefined : (\n <Box marginBottom={1}>\n <Text bold color={color}>\n {title}\n </Text>\n </Box>\n )}\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {children === undefined ? undefined : (\n <Box flexDirection=\"column\" marginBottom={1}>\n {typeof children === \"string\" ? <Text>{children}</Text> : children}\n </Box>\n )}\n <Box gap={2} justifyContent=\"flex-end\">\n <ButtonLabel color={color} isFocused={focus === \"cancel\"} label={cancelLabel} />\n <ButtonLabel color={color} isFocused={focus === \"confirm\"} label={confirmLabel} />\n </Box>\n </Box>\n );\n}\n\nexport { ConfirmDialog };\nexport type { Props as ConfirmDialogProps };\n",
|
|
19
|
+
"path": "registry/ui/confirm-dialog.tsx",
|
|
20
|
+
"target": "components/ui/confirm-dialog.tsx",
|
|
21
|
+
"type": "registry:ui"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "confirm-input",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Confirm Input",
|
|
6
|
+
"description": "Confirm Input component for @visulima/tui.",
|
|
7
|
+
"categories": [
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@visulima/tui"
|
|
12
|
+
],
|
|
13
|
+
"registryDependencies": [],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"content": "/* eslint-disable react/function-component-definition */\n\n/**\n * Confirm input component for Ink.\n *\n * Inspired by ink-ui ConfirmInput by Vadim Demedes.\n * @see https://github.com/vadimdemedes/ink-ui\n *\n * MIT License\n * Copyright (c) Vadym Demedes (github.com/vadimdemedes)\n */\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement } from \"react\";\nimport { useCallback, useRef } from \"react\";\n\nexport type Props = {\n /**\n * The default action when pressing Enter without typing Y or N.\n * @default \"confirm\"\n */\n readonly defaultChoice?: \"cancel\" | \"confirm\";\n\n /**\n * Whether the input is disabled.\n * @default false\n */\n readonly isDisabled?: boolean;\n\n /**\n * Called when the user cancels (presses N).\n */\n readonly onCancel: () => void;\n\n /**\n * Called when the user confirms (presses Y).\n */\n readonly onConfirm: () => void;\n\n /**\n * Whether pressing Enter submits the default choice.\n * @default true\n */\n readonly submitOnEnter?: boolean;\n};\n\n/**\n * Renders a Y/N confirmation prompt. Shows \"Y/n\" when default is confirm,\n * or \"y/N\" when default is cancel.\n */\nexport default function ConfirmInput({ defaultChoice = \"confirm\", isDisabled = false, onCancel, onConfirm, submitOnEnter = true }: Props): ReactElement {\n const onConfirmRef = useRef(onConfirm);\n\n onConfirmRef.current = onConfirm;\n const onCancelRef = useRef(onCancel);\n\n onCancelRef.current = onCancel;\n\n useInput(\n useCallback(\n (input, key) => {\n if (input.toLowerCase() === \"y\") {\n onConfirmRef.current();\n\n return;\n }\n\n if (input.toLowerCase() === \"n\") {\n onCancelRef.current();\n\n return;\n }\n\n if (key.return && submitOnEnter) {\n if (defaultChoice === \"confirm\") {\n onConfirmRef.current();\n } else {\n onCancelRef.current();\n }\n }\n },\n [submitOnEnter, defaultChoice],\n ),\n { isActive: !isDisabled },\n );\n\n const yesText = defaultChoice === \"confirm\" ? \"Y\" : \"y\";\n const noText = defaultChoice === \"cancel\" ? \"N\" : \"n\";\n\n return (\n <Box>\n <Text dimColor={isDisabled}>\n {/* eslint-disable-next-line @stylistic/jsx-one-expression-per-line */}\n {yesText}/{noText}\n </Text>\n </Box>\n );\n}\n\nexport { ConfirmInput };\nexport type { Props as ConfirmInputProps };\n",
|
|
17
|
+
"path": "registry/ui/confirm-input.tsx",
|
|
18
|
+
"target": "components/ui/confirm-input.tsx",
|
|
19
|
+
"type": "registry:ui"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|