@keyfunc/keybricks-ds 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPLOY.md +204 -0
- package/DESIGN.md +269 -0
- package/INTEGRATION.md +417 -0
- package/SKILL.md +32 -0
- package/_adherence.oxlintrc.json +1469 -0
- package/_scripts/build-index.mjs +80 -0
- package/_scripts/build-tokens.mjs +135 -0
- package/assets/icons/icon-index.js +1 -0
- package/assets/icons/icon-index.json +1 -0
- package/assets/icons/unicons-index.js +1 -0
- package/assets/icons/unicons-index.json +1 -0
- package/assets/keybricks-logo.png +0 -0
- package/components/_cards/accordion.card.html +32 -0
- package/components/_cards/action-chip.card.html +22 -0
- package/components/_cards/activity.card.html +34 -0
- package/components/_cards/alert.card.html +32 -0
- package/components/_cards/app-chrome.card.html +36 -0
- package/components/_cards/banner.card.html +21 -0
- package/components/_cards/brand-visual.card.html +43 -0
- package/components/_cards/breadcrumb.card.html +23 -0
- package/components/_cards/button-1-base.card.html +39 -0
- package/components/_cards/button-2-states.card.html +56 -0
- package/components/_cards/button-3-sizes-icons.card.html +32 -0
- package/components/_cards/button-4-group.card.html +47 -0
- package/components/_cards/canvas-zoom-controls.card.html +19 -0
- package/components/_cards/cards-indicators.card.html +59 -0
- package/components/_cards/charts-1-sparkline.card.html +55 -0
- package/components/_cards/charts-10-gauge.card.html +31 -0
- package/components/_cards/charts-11-funnel.card.html +28 -0
- package/components/_cards/charts-12-treemap.card.html +23 -0
- package/components/_cards/charts-14-radar.card.html +31 -0
- package/components/_cards/charts-15-distribution.card.html +32 -0
- package/components/_cards/charts-16-pareto.card.html +25 -0
- package/components/_cards/charts-17-candlestick.card.html +34 -0
- package/components/_cards/charts-18-sankey.card.html +29 -0
- package/components/_cards/charts-19-chord.card.html +31 -0
- package/components/_cards/charts-2-line.card.html +46 -0
- package/components/_cards/charts-20-sunburst.card.html +38 -0
- package/components/_cards/charts-21-worldmap.card.html +33 -0
- package/components/_cards/charts-22-gantt.card.html +37 -0
- package/components/_cards/charts-23-bar3d.card.html +26 -0
- package/components/_cards/charts-24-pie3d.card.html +32 -0
- package/components/_cards/charts-25-surface3d.card.html +24 -0
- package/components/_cards/charts-26-scatter3d.card.html +24 -0
- package/components/_cards/charts-27-ribbon3d.card.html +23 -0
- package/components/_cards/charts-3-bar.card.html +71 -0
- package/components/_cards/charts-4-barlist.card.html +30 -0
- package/components/_cards/charts-5-donut.card.html +35 -0
- package/components/_cards/charts-6-scatter.card.html +26 -0
- package/components/_cards/charts-7-heatmap.card.html +28 -0
- package/components/_cards/charts-8-waterfall.card.html +25 -0
- package/components/_cards/charts-9-bullet.card.html +31 -0
- package/components/_cards/charts-99-fullpage.card.html +194 -0
- package/components/_cards/checkbox.card.html +34 -0
- package/components/_cards/codeblock.card.html +19 -0
- package/components/_cards/combobox.card.html +25 -0
- package/components/_cards/context-menu.card.html +21 -0
- package/components/_cards/datatable.card.html +48 -0
- package/components/_cards/date-picker.card.html +31 -0
- package/components/_cards/diffvalue.card.html +21 -0
- package/components/_cards/divider.card.html +24 -0
- package/components/_cards/drawer.card.html +30 -0
- package/components/_cards/dropdown-menu.card.html +38 -0
- package/components/_cards/dropzone.card.html +22 -0
- package/components/_cards/empty-state.card.html +29 -0
- package/components/_cards/filter-bar.card.html +34 -0
- package/components/_cards/icon-button.card.html +27 -0
- package/components/_cards/icons-1-core.card.html +49 -0
- package/components/_cards/icons-2-feather.card.html +39 -0
- package/components/_cards/icons-3-unicons.card.html +44 -0
- package/components/_cards/input.card.html +25 -0
- package/components/_cards/kbd.card.html +20 -0
- package/components/_cards/keyvalue.card.html +23 -0
- package/components/_cards/listrow.card.html +30 -0
- package/components/_cards/loading.card.html +33 -0
- package/components/_cards/log-viewer.card.html +23 -0
- package/components/_cards/modal.card.html +35 -0
- package/components/_cards/multi-select.card.html +26 -0
- package/components/_cards/number-stepper.card.html +24 -0
- package/components/_cards/pagination.card.html +25 -0
- package/components/_cards/people.card.html +46 -0
- package/components/_cards/pipeline-node.card.html +76 -0
- package/components/_cards/pipeline-palette.card.html +54 -0
- package/components/_cards/search.card.html +23 -0
- package/components/_cards/select.card.html +23 -0
- package/components/_cards/slider.card.html +25 -0
- package/components/_cards/smart-table.card.html +37 -0
- package/components/_cards/spreadsheet.card.html +31 -0
- package/components/_cards/stepper.card.html +25 -0
- package/components/_cards/tabs.card.html +25 -0
- package/components/_cards/tag.card.html +26 -0
- package/components/_cards/textarea.card.html +27 -0
- package/components/_cards/timeline.card.html +32 -0
- package/components/_cards/toast.card.html +33 -0
- package/components/_cards/toggles.card.html +68 -0
- package/components/_cards/tooltip.card.html +22 -0
- package/components/_cards/tree.card.html +150 -0
- package/components/_shared/frame-theme.js +16 -0
- package/components/_shared/src-brand.js +1 -0
- package/components/_shared/src-buttons.js +1 -0
- package/components/_shared/src-charts.js +1 -0
- package/components/_shared/src-data.js +1 -0
- package/components/_shared/src-display.js +1 -0
- package/components/_shared/src-feedback.js +1 -0
- package/components/_shared/src-forms.js +1 -0
- package/components/_shared/src-icons.js +1 -0
- package/components/_shared/src-navigation.js +1 -0
- package/components/_shared/src-pipeline.js +1 -0
- package/components/_shared/src-tabs.js +1 -0
- package/components/_shared/svg-download.js +19 -0
- package/components/_shared/view-source.js +185 -0
- package/components/_vue/index.mjs +114 -0
- package/components/brand/BrandVisual.d.ts +17 -0
- package/components/brand/BrandVisual.jsx +29 -0
- package/components/brand/BrandVisual.vue +34 -0
- package/components/brand/_dir.card.html +13 -0
- package/components/brand/brandMotifs.js +210 -0
- package/components/buttons/ActionChip.d.ts +10 -0
- package/components/buttons/ActionChip.jsx +20 -0
- package/components/buttons/ActionChip.vue +18 -0
- package/components/buttons/Button.d.ts +40 -0
- package/components/buttons/Button.jsx +101 -0
- package/components/buttons/Button.vue +87 -0
- package/components/buttons/IconButton.d.ts +26 -0
- package/components/buttons/IconButton.jsx +28 -0
- package/components/buttons/IconButton.vue +21 -0
- package/components/buttons/_dir.card.html +13 -0
- package/components/buttons/buttons.prompt.md +19 -0
- package/components/charts/Bar3D.d.ts +15 -0
- package/components/charts/Bar3D.jsx +93 -0
- package/components/charts/Bar3D.vue +79 -0
- package/components/charts/BarChart.d.ts +23 -0
- package/components/charts/BarChart.jsx +135 -0
- package/components/charts/BarChart.vue +139 -0
- package/components/charts/BarList.d.ts +13 -0
- package/components/charts/BarList.jsx +31 -0
- package/components/charts/BarList.vue +25 -0
- package/components/charts/BoxPlot.d.ts +18 -0
- package/components/charts/BoxPlot.jsx +62 -0
- package/components/charts/BoxPlot.vue +58 -0
- package/components/charts/BulletChart.d.ts +12 -0
- package/components/charts/BulletChart.jsx +41 -0
- package/components/charts/BulletChart.vue +33 -0
- package/components/charts/CandlestickChart.d.ts +15 -0
- package/components/charts/CandlestickChart.jsx +63 -0
- package/components/charts/CandlestickChart.vue +62 -0
- package/components/charts/ChartKit.d.ts +8 -0
- package/components/charts/ChartKit.jsx +76 -0
- package/components/charts/ChartKit.vue +47 -0
- package/components/charts/ChordChart.d.ts +13 -0
- package/components/charts/ChordChart.jsx +55 -0
- package/components/charts/ChordChart.vue +54 -0
- package/components/charts/DonutChart.d.ts +15 -0
- package/components/charts/DonutChart.jsx +73 -0
- package/components/charts/DonutChart.vue +61 -0
- package/components/charts/FunnelChart.d.ts +11 -0
- package/components/charts/FunnelChart.jsx +37 -0
- package/components/charts/FunnelChart.vue +29 -0
- package/components/charts/GanttChart.d.ts +20 -0
- package/components/charts/GanttChart.jsx +80 -0
- package/components/charts/GanttChart.vue +60 -0
- package/components/charts/GaugeChart.d.ts +13 -0
- package/components/charts/GaugeChart.jsx +33 -0
- package/components/charts/GaugeChart.vue +29 -0
- package/components/charts/HeatmapChart.d.ts +13 -0
- package/components/charts/HeatmapChart.jsx +62 -0
- package/components/charts/HeatmapChart.vue +38 -0
- package/components/charts/Histogram.d.ts +16 -0
- package/components/charts/Histogram.jsx +56 -0
- package/components/charts/Histogram.vue +53 -0
- package/components/charts/LineChart.d.ts +17 -0
- package/components/charts/LineChart.jsx +84 -0
- package/components/charts/LineChart.vue +86 -0
- package/components/charts/ParetoChart.d.ts +13 -0
- package/components/charts/ParetoChart.jsx +54 -0
- package/components/charts/ParetoChart.vue +55 -0
- package/components/charts/Pie3D.d.ts +17 -0
- package/components/charts/Pie3D.jsx +145 -0
- package/components/charts/Pie3D.vue +115 -0
- package/components/charts/RadarChart.d.ts +14 -0
- package/components/charts/RadarChart.jsx +79 -0
- package/components/charts/RadarChart.vue +73 -0
- package/components/charts/Ribbon3D.d.ts +13 -0
- package/components/charts/Ribbon3D.jsx +89 -0
- package/components/charts/Ribbon3D.vue +79 -0
- package/components/charts/SankeyChart.d.ts +15 -0
- package/components/charts/SankeyChart.jsx +59 -0
- package/components/charts/SankeyChart.vue +53 -0
- package/components/charts/Scatter3D.d.ts +12 -0
- package/components/charts/Scatter3D.jsx +69 -0
- package/components/charts/Scatter3D.vue +59 -0
- package/components/charts/ScatterChart.d.ts +12 -0
- package/components/charts/ScatterChart.jsx +53 -0
- package/components/charts/ScatterChart.vue +57 -0
- package/components/charts/Sparkline.d.ts +19 -0
- package/components/charts/Sparkline.jsx +63 -0
- package/components/charts/Sparkline.vue +54 -0
- package/components/charts/SunburstChart.d.ts +12 -0
- package/components/charts/SunburstChart.jsx +61 -0
- package/components/charts/SunburstChart.vue +60 -0
- package/components/charts/Surface3D.d.ts +13 -0
- package/components/charts/Surface3D.jsx +70 -0
- package/components/charts/Surface3D.vue +63 -0
- package/components/charts/TreemapChart.d.ts +11 -0
- package/components/charts/TreemapChart.jsx +35 -0
- package/components/charts/TreemapChart.vue +27 -0
- package/components/charts/WaterfallChart.d.ts +11 -0
- package/components/charts/WaterfallChart.jsx +59 -0
- package/components/charts/WaterfallChart.vue +46 -0
- package/components/charts/WorldMap.d.ts +21 -0
- package/components/charts/WorldMap.jsx +107 -0
- package/components/charts/WorldMap.vue +73 -0
- package/components/charts/_dir.card.html +7 -0
- package/components/charts/chart-3d.js +84 -0
- package/components/charts/chart-geometry.js +223 -0
- package/components/charts/charts.prompt.md +24 -0
- package/components/data/CodeBlock.d.ts +9 -0
- package/components/data/CodeBlock.jsx +19 -0
- package/components/data/CodeBlock.vue +22 -0
- package/components/data/DataTable.d.ts +28 -0
- package/components/data/DataTable.jsx +32 -0
- package/components/data/DataTable.vue +25 -0
- package/components/data/DiffValue.d.ts +11 -0
- package/components/data/DiffValue.jsx +15 -0
- package/components/data/DiffValue.vue +13 -0
- package/components/data/FilterBar.d.ts +16 -0
- package/components/data/FilterBar.jsx +22 -0
- package/components/data/FilterBar.vue +23 -0
- package/components/data/KeyValue.d.ts +16 -0
- package/components/data/KeyValue.jsx +25 -0
- package/components/data/KeyValue.vue +26 -0
- package/components/data/ListRow.d.ts +14 -0
- package/components/data/ListRow.jsx +25 -0
- package/components/data/ListRow.vue +25 -0
- package/components/data/Pagination.d.ts +12 -0
- package/components/data/Pagination.jsx +27 -0
- package/components/data/Pagination.vue +24 -0
- package/components/data/SmartTable.d.ts +30 -0
- package/components/data/SmartTable.jsx +73 -0
- package/components/data/SmartTable.vue +74 -0
- package/components/data/Spreadsheet.d.ts +33 -0
- package/components/data/Spreadsheet.jsx +110 -0
- package/components/data/Spreadsheet.vue +95 -0
- package/components/data/_dir.card.html +13 -0
- package/components/data/data.prompt.md +33 -0
- package/components/display/Avatar.d.ts +21 -0
- package/components/display/Avatar.jsx +31 -0
- package/components/display/Avatar.vue +34 -0
- package/components/display/Badge.d.ts +27 -0
- package/components/display/Badge.jsx +40 -0
- package/components/display/Badge.vue +47 -0
- package/components/display/Card.d.ts +27 -0
- package/components/display/Card.jsx +27 -0
- package/components/display/Card.vue +28 -0
- package/components/display/Divider.d.ts +8 -0
- package/components/display/Divider.jsx +14 -0
- package/components/display/Divider.vue +13 -0
- package/components/display/Kbd.d.ts +7 -0
- package/components/display/Kbd.jsx +12 -0
- package/components/display/Kbd.vue +9 -0
- package/components/display/ProgressBar.d.ts +42 -0
- package/components/display/ProgressBar.jsx +70 -0
- package/components/display/ProgressBar.vue +69 -0
- package/components/display/Tag.d.ts +9 -0
- package/components/display/Tag.jsx +15 -0
- package/components/display/Tag.vue +15 -0
- package/components/display/_dir.card.html +13 -0
- package/components/display/display.prompt.md +30 -0
- package/components/feedback/Accordion.d.ts +13 -0
- package/components/feedback/Accordion.jsx +21 -0
- package/components/feedback/Accordion.vue +22 -0
- package/components/feedback/ActivityItem.d.ts +25 -0
- package/components/feedback/ActivityItem.jsx +39 -0
- package/components/feedback/ActivityItem.vue +46 -0
- package/components/feedback/Alert.d.ts +36 -0
- package/components/feedback/Alert.jsx +48 -0
- package/components/feedback/Alert.vue +59 -0
- package/components/feedback/Banner.d.ts +15 -0
- package/components/feedback/Banner.jsx +25 -0
- package/components/feedback/Banner.vue +25 -0
- package/components/feedback/ContextMenu.d.ts +8 -0
- package/components/feedback/ContextMenu.jsx +18 -0
- package/components/feedback/ContextMenu.vue +19 -0
- package/components/feedback/Drawer.d.ts +18 -0
- package/components/feedback/Drawer.jsx +26 -0
- package/components/feedback/Drawer.vue +24 -0
- package/components/feedback/Menu.d.ts +10 -0
- package/components/feedback/Menu.jsx +24 -0
- package/components/feedback/Menu.vue +21 -0
- package/components/feedback/Modal.d.ts +38 -0
- package/components/feedback/Modal.jsx +69 -0
- package/components/feedback/Modal.vue +65 -0
- package/components/feedback/Stepper.d.ts +9 -0
- package/components/feedback/Stepper.jsx +24 -0
- package/components/feedback/Stepper.vue +18 -0
- package/components/feedback/Timeline.d.ts +15 -0
- package/components/feedback/Timeline.jsx +27 -0
- package/components/feedback/Timeline.vue +21 -0
- package/components/feedback/_dir.card.html +13 -0
- package/components/feedback/feedback.prompt.md +35 -0
- package/components/forms/Checkbox.d.ts +13 -0
- package/components/forms/Checkbox.jsx +17 -0
- package/components/forms/Checkbox.vue +18 -0
- package/components/forms/Combobox.d.ts +13 -0
- package/components/forms/Combobox.jsx +34 -0
- package/components/forms/Combobox.vue +35 -0
- package/components/forms/DatePicker.d.ts +12 -0
- package/components/forms/DatePicker.jsx +48 -0
- package/components/forms/DatePicker.vue +46 -0
- package/components/forms/Dropzone.d.ts +20 -0
- package/components/forms/Dropzone.jsx +36 -0
- package/components/forms/Dropzone.vue +28 -0
- package/components/forms/Field.d.ts +13 -0
- package/components/forms/Field.jsx +13 -0
- package/components/forms/Field.vue +12 -0
- package/components/forms/Input.d.ts +18 -0
- package/components/forms/Input.jsx +35 -0
- package/components/forms/Input.vue +38 -0
- package/components/forms/MultiSelect.d.ts +14 -0
- package/components/forms/MultiSelect.jsx +33 -0
- package/components/forms/MultiSelect.vue +36 -0
- package/components/forms/NumberStepper.d.ts +13 -0
- package/components/forms/NumberStepper.jsx +15 -0
- package/components/forms/NumberStepper.vue +17 -0
- package/components/forms/Radio.vue +16 -0
- package/components/forms/RadioGroup.vue +11 -0
- package/components/forms/SearchInput.d.ts +12 -0
- package/components/forms/SearchInput.jsx +17 -0
- package/components/forms/SearchInput.vue +14 -0
- package/components/forms/Select.d.ts +14 -0
- package/components/forms/Select.jsx +17 -0
- package/components/forms/Select.vue +16 -0
- package/components/forms/Slider.d.ts +15 -0
- package/components/forms/Slider.jsx +21 -0
- package/components/forms/Slider.vue +23 -0
- package/components/forms/Switch.d.ts +31 -0
- package/components/forms/Switch.jsx +40 -0
- package/components/forms/Switch.vue +16 -0
- package/components/forms/_dir.card.html +13 -0
- package/components/forms/forms.prompt.md +23 -0
- package/components/icons/Icon.d.ts +19 -0
- package/components/icons/Icon.jsx +93 -0
- package/components/icons/Icon.prompt.md +8 -0
- package/components/icons/Icon.vue +12 -0
- package/components/icons/Unicon.d.ts +12 -0
- package/components/icons/Unicon.jsx +16 -0
- package/components/icons/Unicon.vue +15 -0
- package/components/icons/_dir.card.html +13 -0
- package/components/icons/feather-paths.js +2 -0
- package/components/icons/icon-paths.js +73 -0
- package/components/index.d.ts +87 -0
- package/components/index.js +110 -0
- package/components/navigation/Breadcrumb.d.ts +24 -0
- package/components/navigation/Breadcrumb.jsx +120 -0
- package/components/navigation/Breadcrumb.vue +94 -0
- package/components/navigation/DocTree.d.ts +18 -0
- package/components/navigation/DocTree.jsx +37 -0
- package/components/navigation/DocTree.vue +25 -0
- package/components/navigation/Sidebar.d.ts +29 -0
- package/components/navigation/Sidebar.jsx +75 -0
- package/components/navigation/Sidebar.vue +54 -0
- package/components/navigation/TopBar.d.ts +27 -0
- package/components/navigation/TopBar.jsx +44 -0
- package/components/navigation/TopBar.vue +51 -0
- package/components/navigation/Tree.d.ts +83 -0
- package/components/navigation/Tree.jsx +347 -0
- package/components/navigation/Tree.vue +258 -0
- package/components/navigation/_dir.card.html +13 -0
- package/components/navigation/navigation.prompt.md +22 -0
- package/components/pipeline/ConfigPanel.d.ts +18 -0
- package/components/pipeline/ConfigPanel.jsx +30 -0
- package/components/pipeline/ConfigPanel.vue +32 -0
- package/components/pipeline/DataPreview.d.ts +14 -0
- package/components/pipeline/DataPreview.jsx +24 -0
- package/components/pipeline/DataPreview.vue +21 -0
- package/components/pipeline/NodeCard.d.ts +47 -0
- package/components/pipeline/NodeCard.jsx +97 -0
- package/components/pipeline/NodeCard.vue +75 -0
- package/components/pipeline/PaletteItem.vue +15 -0
- package/components/pipeline/PaletteListItem.vue +15 -0
- package/components/pipeline/PalettePanel.d.ts +20 -0
- package/components/pipeline/PalettePanel.jsx +74 -0
- package/components/pipeline/PalettePanel.vue +45 -0
- package/components/pipeline/PipelineCanvas.d.ts +25 -0
- package/components/pipeline/PipelineCanvas.jsx +94 -0
- package/components/pipeline/PipelineCanvas.vue +73 -0
- package/components/pipeline/ZoomControls.d.ts +9 -0
- package/components/pipeline/ZoomControls.jsx +15 -0
- package/components/pipeline/ZoomControls.vue +15 -0
- package/components/pipeline/_dir.card.html +13 -0
- package/components/pipeline/pipeline.prompt.md +22 -0
- package/components/tabs/SegmentedControl.d.ts +11 -0
- package/components/tabs/SegmentedControl.jsx +21 -0
- package/components/tabs/SegmentedControl.vue +15 -0
- package/components/tabs/Tabs.d.ts +13 -0
- package/components/tabs/Tabs.jsx +52 -0
- package/components/tabs/Tabs.vue +39 -0
- package/components/tabs/ViewToggle.d.ts +8 -0
- package/components/tabs/ViewToggle.jsx +12 -0
- package/components/tabs/ViewToggle.vue +12 -0
- package/components/tabs/_dir.card.html +13 -0
- package/components/tabs/tabs.prompt.md +14 -0
- package/package.json +76 -0
- package/readme.md +150 -0
- package/styles.css +10 -0
- package/tokens/alpha.css +27 -0
- package/tokens/charts.css +55 -0
- package/tokens/colors-extended.css +40 -0
- package/tokens/colors.css +39 -0
- package/tokens/dark.css +96 -0
- package/tokens/fonts.css +4 -0
- package/tokens/interaction.css +48 -0
- package/tokens/layout.css +30 -0
- package/tokens/motion.css +22 -0
- package/tokens/tokens.d.ts +680 -0
- package/tokens/tokens.js +1018 -0
- package/tokens/tokens.json +2928 -0
- package/tokens/typography.css +22 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
import { IconButton } from "../buttons/IconButton.jsx";
|
|
4
|
+
import { inputBase } from "./Input.jsx";
|
|
5
|
+
|
|
6
|
+
const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
7
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
8
|
+
const iso = (d) => d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
|
|
9
|
+
|
|
10
|
+
/** Date picker: input with calendar glyph opening a month-grid popover — chevron month nav, today outlined, selected day filled blue. value/onChange use "YYYY-MM-DD". */
|
|
11
|
+
export function DatePicker({ value = "", placeholder = "Select date", disabled, error, width = 180, onChange, style }) {
|
|
12
|
+
const [open, setOpen] = React.useState(false);
|
|
13
|
+
const sel = value ? new Date(value + "T00:00:00") : null;
|
|
14
|
+
const [view, setView] = React.useState(() => { const d = sel || new Date(); return { y: d.getFullYear(), m: d.getMonth() }; });
|
|
15
|
+
const root = React.useRef(null);
|
|
16
|
+
React.useEffect(() => { const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []);
|
|
17
|
+
const first = new Date(view.y, view.m, 1);
|
|
18
|
+
const start = (first.getDay() + 6) % 7; // Monday-first
|
|
19
|
+
const days = new Date(view.y, view.m + 1, 0).getDate();
|
|
20
|
+
const todayIso = iso(new Date());
|
|
21
|
+
const nav = (d) => { let m = view.m + d, y = view.y; if (m < 0) { m = 11; y--; } if (m > 11) { m = 0; y++; } setView({ y, m }); };
|
|
22
|
+
const label = sel ? MONTHS[sel.getMonth()].slice(0, 3) + " " + sel.getDate() + ", " + sel.getFullYear() : "";
|
|
23
|
+
return (
|
|
24
|
+
<div ref={root} style={{ position: "relative", width, ...style }}>
|
|
25
|
+
<div onClick={() => !disabled && setOpen(!open)} style={{ ...inputBase, display: "flex", alignItems: "center", justifyContent: "space-between", cursor: disabled ? "not-allowed" : "pointer", borderColor: error ? "var(--danger)" : open ? "var(--focus-border)" : "var(--border-control)", boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", opacity: disabled ? .5 : 1, color: label ? "var(--text-primary)" : "var(--text-muted)" }}>
|
|
26
|
+
{label || placeholder}<span style={{ color: "var(--text-muted)", display: "inline-flex" }}><Icon name="calendar" size={14} /></span>
|
|
27
|
+
</div>
|
|
28
|
+
{open && <div style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, zIndex: "var(--z-menu)", width: 252, background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-menu)", padding: 12, boxSizing: "border-box" }}>
|
|
29
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", paddingBottom: 8 }}>
|
|
30
|
+
<IconButton icon="chevron-left" size="sm" label="Previous month" onClick={() => nav(-1)} />
|
|
31
|
+
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-sm)", color: "var(--text-heading)" }}>{MONTHS[view.m]} {view.y}</span>
|
|
32
|
+
<IconButton icon="chevron-right" size="sm" label="Next month" onClick={() => nav(1)} />
|
|
33
|
+
</div>
|
|
34
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 2 }}>
|
|
35
|
+
{["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"].map(d => <span key={d} style={{ textAlign: "center", fontFamily: "var(--font-sans)", fontSize: "var(--text-2xs)", letterSpacing: "var(--tracking-2xs)", color: "var(--text-muted)", padding: "2px 0" }}>{d}</span>)}
|
|
36
|
+
{Array.from({ length: start }, (_, i) => <span key={"e" + i}></span>)}
|
|
37
|
+
{Array.from({ length: days }, (_, i) => {
|
|
38
|
+
const dIso = view.y + "-" + pad(view.m + 1) + "-" + pad(i + 1);
|
|
39
|
+
const isSel = value === dIso, isToday = todayIso === dIso;
|
|
40
|
+
return <button key={i} type="button" onClick={() => { onChange && onChange(dIso); setOpen(false); }}
|
|
41
|
+
style={{ height: 28, borderRadius: "var(--radius-sm)", border: isToday && !isSel ? "1px solid var(--action-primary)" : "1px solid transparent", cursor: "pointer", background: isSel ? "var(--action-primary)" : "transparent", color: isSel ? "var(--on-accent)" : "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", padding: 0, boxSizing: "border-box" }}
|
|
42
|
+
onMouseEnter={(e) => { if (!isSel) e.currentTarget.style.background = "var(--bg-hover)"; }} onMouseLeave={(e) => { if (!isSel) e.currentTarget.style.background = "transparent"; }}>{i + 1}</button>;
|
|
43
|
+
})}
|
|
44
|
+
</div>
|
|
45
|
+
</div>}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
3
|
+
import Icon from '../icons/Icon.vue'
|
|
4
|
+
import IconButton from '../buttons/IconButton.vue'
|
|
5
|
+
import { inputBase } from './Input.vue'
|
|
6
|
+
// Date picker: input with calendar glyph opening a month-grid popover — Monday-first, today outlined, selected filled. v-model is "YYYY-MM-DD".
|
|
7
|
+
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']
|
|
8
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
9
|
+
const props = defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select date' }, disabled: Boolean, error: Boolean, width: { type: [Number, String], default: 180 }, style: Object })
|
|
10
|
+
const emit = defineEmits(['update:modelValue'])
|
|
11
|
+
const open = ref(false), root = ref(null)
|
|
12
|
+
const sel = computed(() => props.modelValue ? new Date(props.modelValue + 'T00:00:00') : null)
|
|
13
|
+
const d0 = sel.value || new Date()
|
|
14
|
+
const view = ref({ y: d0.getFullYear(), m: d0.getMonth() })
|
|
15
|
+
const start = computed(() => (new Date(view.value.y, view.value.m, 1).getDay() + 6) % 7)
|
|
16
|
+
const days = computed(() => new Date(view.value.y, view.value.m + 1, 0).getDate())
|
|
17
|
+
const todayIso = (() => { const d = new Date(); return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) })()
|
|
18
|
+
const label = computed(() => sel.value ? MONTHS[sel.value.getMonth()].slice(0, 3) + ' ' + sel.value.getDate() + ', ' + sel.value.getFullYear() : '')
|
|
19
|
+
function nav(d) { let m = view.value.m + d, y = view.value.y; if (m < 0) { m = 11; y-- } if (m > 11) { m = 0; y++ } view.value = { y, m } }
|
|
20
|
+
function dayIso(i) { return view.value.y + '-' + pad(view.value.m + 1) + '-' + pad(i) }
|
|
21
|
+
function pick(i) { emit('update:modelValue', dayIso(i)); open.value = false }
|
|
22
|
+
function onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }
|
|
23
|
+
onMounted(() => document.addEventListener('mousedown', onDoc))
|
|
24
|
+
onUnmounted(() => document.removeEventListener('mousedown', onDoc))
|
|
25
|
+
</script>
|
|
26
|
+
<template>
|
|
27
|
+
<div ref="root" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
|
|
28
|
+
<div @click="!disabled && (open = !open)" :style="{ ...inputBase, display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: disabled ? 'not-allowed' : 'pointer', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--border-control)', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1, color: label ? 'var(--text-primary)' : 'var(--text-muted)' }">
|
|
29
|
+
{{ label || placeholder }}<span style="color:var(--text-muted);display:inline-flex"><Icon name="calendar" :size="14" /></span>
|
|
30
|
+
</div>
|
|
31
|
+
<div v-if="open" style="position:absolute;top:calc(100% + 4px);left:0;z-index:var(--z-menu);width:252px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);padding:12px;box-sizing:border-box">
|
|
32
|
+
<div style="display:flex;align-items:center;justify-content:space-between;padding-bottom:8px">
|
|
33
|
+
<IconButton icon="chevron-left" size="sm" label="Previous month" @click="nav(-1)" />
|
|
34
|
+
<span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);color:var(--text-heading)">{{ MONTHS[view.m] }} {{ view.y }}</span>
|
|
35
|
+
<IconButton icon="chevron-right" size="sm" label="Next month" @click="nav(1)" />
|
|
36
|
+
</div>
|
|
37
|
+
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:2px">
|
|
38
|
+
<span v-for="d in ['Mo','Tu','We','Th','Fr','Sa','Su']" :key="d" style="text-align:center;font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding:2px 0">{{ d }}</span>
|
|
39
|
+
<span v-for="i in start" :key="'e' + i" />
|
|
40
|
+
<button v-for="i in days" :key="i" type="button" @click="pick(i)" class="kb-dp-day"
|
|
41
|
+
:style="{ height: '28px', borderRadius: 'var(--radius-sm)', border: todayIso === dayIso(i) && modelValue !== dayIso(i) ? '1px solid var(--action-primary)' : '1px solid transparent', cursor: 'pointer', background: modelValue === dayIso(i) ? 'var(--action-primary)' : 'transparent', color: modelValue === dayIso(i) ? 'var(--on-accent)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', padding: 0, boxSizing: 'border-box' }">{{ i }}</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
<style scoped>.kb-dp-day:hover{background:var(--bg-hover)!important}</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Dashed 2px r10 file dropzone ("Upload Configuration File"). Real HTML5 drag-and-drop is wired in: dragging a file over shows a blue border + light-blue tint automatically. Pass `file` to render the uploaded state instead of the empty prompt. */
|
|
3
|
+
export interface DropzoneProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
height?: number;
|
|
7
|
+
/** Red error message shown in place of `title`, with a red dashed border */
|
|
8
|
+
error?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Force the blue drag-over look for demos (real drag-and-drop already triggers it automatically) */
|
|
11
|
+
dragOver?: boolean;
|
|
12
|
+
/** File name; when set, renders the filled/uploaded state with a Remove action */
|
|
13
|
+
file?: string;
|
|
14
|
+
onRemove?: () => void;
|
|
15
|
+
onClick?: () => void;
|
|
16
|
+
/** Fires with the dropped FileList when a real drag-and-drop completes */
|
|
17
|
+
onDropFiles?: (files: FileList) => void;
|
|
18
|
+
style?: React.CSSProperties;
|
|
19
|
+
}
|
|
20
|
+
export declare function Dropzone(props: DropzoneProps): JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
|
|
4
|
+
const toneColor = { default: "var(--dropzone-border)", hover: "var(--dropzone-border-hover)", error: "var(--danger)", drag: "var(--focus-border)" };
|
|
5
|
+
|
|
6
|
+
/** Dashed upload dropzone (2px, r10). States: default/hover, real drag-and-drop (blue border + tint while a file is dragged over), error, disabled, and a filled "uploaded" state showing the file name + remove. */
|
|
7
|
+
export function Dropzone({ title = "Click to upload or drag and drop", hint = "JSON files only", height = 185.5, error, disabled, dragOver, file, onRemove, onClick, onDropFiles, style }) {
|
|
8
|
+
const [hover, setHover] = React.useState(false);
|
|
9
|
+
const [dragging, setDragging] = React.useState(false);
|
|
10
|
+
const dragActive = dragOver || dragging;
|
|
11
|
+
if (file) {
|
|
12
|
+
return (
|
|
13
|
+
<div style={{ height, borderRadius: "var(--radius-lg)", border: "1px solid var(--border-control)", background: "var(--bg-inset)", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 8, boxSizing: "border-box", ...style }}>
|
|
14
|
+
<span style={{ color: "var(--success-strong)" }}><Icon name="file-check" size={22} strokeWidth={1.5} /></span>
|
|
15
|
+
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", color: "var(--text-heading)" }}>{file}</span>
|
|
16
|
+
<button type="button" onClick={onRemove} style={{ display: "flex", alignItems: "center", gap: 4, border: "none", background: "transparent", cursor: "pointer", color: "var(--danger)", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", padding: 0 }}>
|
|
17
|
+
<Icon name="trash-2" size={13} />Remove
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const tone = disabled ? "default" : error ? "error" : dragActive ? "drag" : hover ? "hover" : "default";
|
|
23
|
+
const stop = (e) => { e.preventDefault(); e.stopPropagation(); };
|
|
24
|
+
return (
|
|
25
|
+
<div onClick={disabled ? undefined : onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
26
|
+
onDragEnter={disabled ? undefined : (e) => { stop(e); setDragging(true); }}
|
|
27
|
+
onDragOver={disabled ? undefined : stop}
|
|
28
|
+
onDragLeave={disabled ? undefined : (e) => { stop(e); setDragging(false); }}
|
|
29
|
+
onDrop={disabled ? undefined : (e) => { stop(e); setDragging(false); onDropFiles && onDropFiles(e.dataTransfer.files); }}
|
|
30
|
+
style={{ height, borderRadius: "var(--radius-lg)", border: `1px dashed ${toneColor[tone]}`, background: error ? "var(--danger-bg)" : dragActive ? "var(--soft-bg)" : "transparent", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 4, cursor: disabled ? "not-allowed" : "pointer", boxSizing: "border-box", opacity: disabled ? .5 : 1, transition: "var(--t-hover)", ...style }}>
|
|
31
|
+
<span style={{ color: error ? "var(--danger)" : dragActive ? "var(--action-primary)" : "var(--text-muted)", marginBottom: 6 }}><Icon name={error ? "triangle-alert" : "upload"} size={22} strokeWidth={1.5} /></span>
|
|
32
|
+
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", color: error ? "var(--danger-text)" : dragActive ? "var(--action-primary)" : "var(--text-heading)" }}>{error || (dragActive ? "Drop file to upload" : title)}</span>
|
|
33
|
+
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: dragActive ? "var(--action-primary)" : "var(--text-secondary)" }}>{hint}</span>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import Icon from '../icons/Icon.vue'
|
|
4
|
+
const toneColor = { default: 'var(--dropzone-border)', hover: 'var(--dropzone-border-hover)', error: 'var(--danger)', drag: 'var(--focus-border)' }
|
|
5
|
+
// Dashed upload dropzone (2px, r10). States: default/hover, real drag-and-drop (blue border + tint while a file is dragged over), error, disabled, and a filled "uploaded" state.
|
|
6
|
+
const props = defineProps({ title: { type: String, default: 'Click to upload or drag and drop' }, hint: { type: String, default: 'JSON files only' }, height: { type: [Number, String], default: 185.5 }, error: String, disabled: Boolean, dragOver: Boolean, file: String, style: Object })
|
|
7
|
+
const emit = defineEmits(['remove', 'click', 'dropFiles'])
|
|
8
|
+
const hover = ref(false), dragging = ref(false)
|
|
9
|
+
const dragActive = computed(() => props.dragOver || dragging.value)
|
|
10
|
+
const tone = computed(() => props.disabled ? 'default' : props.error ? 'error' : dragActive.value ? 'drag' : hover.value ? 'hover' : 'default')
|
|
11
|
+
function stop(e) { e.preventDefault(); e.stopPropagation() }
|
|
12
|
+
function onDrop(e) { stop(e); dragging.value = false; emit('dropFiles', e.dataTransfer.files) }
|
|
13
|
+
</script>
|
|
14
|
+
<template>
|
|
15
|
+
<div v-if="file" :style="{ height, borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-control)', background: 'var(--bg-inset)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8, boxSizing: 'border-box', ...style }">
|
|
16
|
+
<span style="color:var(--success-strong)"><Icon name="file-check" :size="22" :stroke-width="1.5" /></span>
|
|
17
|
+
<span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);color:var(--text-heading)">{{ file }}</span>
|
|
18
|
+
<button type="button" @click="$emit('remove')" style="display:flex;align-items:center;gap:4px;border:none;background:transparent;cursor:pointer;color:var(--danger);font-family:var(--font-sans);font-size:var(--text-sm);padding:0"><Icon name="trash-2" :size="13" />Remove</button>
|
|
19
|
+
</div>
|
|
20
|
+
<div v-else @click="!disabled && $emit('click')" @mouseenter="hover = true" @mouseleave="hover = false"
|
|
21
|
+
@dragenter="!disabled && ((e) => { stop(e); dragging = true })($event)" @dragover="!disabled && stop($event)"
|
|
22
|
+
@dragleave="!disabled && ((e) => { stop(e); dragging = false })($event)" @drop="!disabled && onDrop($event)"
|
|
23
|
+
:style="{ height, borderRadius: 'var(--radius-lg)', border: `1px dashed ${toneColor[tone]}`, background: error ? 'var(--danger-bg)' : dragActive ? 'var(--soft-bg)' : 'transparent', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4, cursor: disabled ? 'not-allowed' : 'pointer', boxSizing: 'border-box', opacity: disabled ? .5 : 1, transition: 'var(--t-hover)', ...style }">
|
|
24
|
+
<span :style="{ color: error ? 'var(--danger)' : dragActive ? 'var(--action-primary)' : 'var(--text-muted)', marginBottom: 6 }"><Icon :name="error ? 'triangle-alert' : 'upload'" :size="22" :stroke-width="1.5" /></span>
|
|
25
|
+
<span :style="{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: error ? 'var(--danger-text)' : dragActive ? 'var(--action-primary)' : 'var(--text-heading)' }">{{ error || (dragActive ? 'Drop file to upload' : title) }}</span>
|
|
26
|
+
<span :style="{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: dragActive ? 'var(--action-primary)' : 'var(--text-secondary)' }">{{ hint }}</span>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Label + control wrapper. Label 12px/500 #525252; required appends a red " *" after the label. */
|
|
3
|
+
export interface FieldProps {
|
|
4
|
+
label: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
/** Muted helper text below the control; hidden when `error` is set */
|
|
7
|
+
hint?: string;
|
|
8
|
+
/** Red error text below the control; also expected to drive the control's own error look */
|
|
9
|
+
error?: string;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
export declare function Field(props: FieldProps): JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/** Form field wrapper: 12px/500 label (red + * when required) above the control, optional hint/error below. */
|
|
4
|
+
export function Field({ label, required, hint, error, children, style }) {
|
|
5
|
+
return (
|
|
6
|
+
<div style={{ display: "flex", flexDirection: "column", ...style }}>
|
|
7
|
+
<label style={{ paddingBottom: 6, fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-body)" }}>{label}{required ? <span style={{ color: "var(--danger)" }}> *</span> : null}</label>
|
|
8
|
+
{children}
|
|
9
|
+
{error ? <span style={{ paddingTop: 5, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--danger)" }}>{error}</span>
|
|
10
|
+
: hint ? <span style={{ paddingTop: 5, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-muted)" }}>{hint}</span> : null}
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
// Form field wrapper: 12px/500 label (red + * when required) above the control, optional hint/error below.
|
|
3
|
+
defineProps({ label: String, required: Boolean, hint: String, error: String, style: Object })
|
|
4
|
+
</script>
|
|
5
|
+
<template>
|
|
6
|
+
<div :style="{ display: 'flex', flexDirection: 'column', ...style }">
|
|
7
|
+
<label style="padding-bottom:6px;font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-body)">{{ label }}<span v-if="required" style="color:var(--danger)"> *</span></label>
|
|
8
|
+
<slot />
|
|
9
|
+
<span v-if="error" style="padding-top:5px;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--danger)">{{ error }}</span>
|
|
10
|
+
<span v-else-if="hint" style="padding-top:5px;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-muted)">{{ hint }}</span>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** 36px r8 hairline text input; readOnly = #fafafa inset, disabled = 50% opacity, error = red border+ring. Extends native input props. */
|
|
3
|
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
4
|
+
readOnly?: boolean;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
error?: boolean;
|
|
7
|
+
style?: React.CSSProperties;
|
|
8
|
+
}
|
|
9
|
+
export declare function Input(props: InputProps): JSX.Element;
|
|
10
|
+
/** r8 hairline textarea (default 90px). showCount + maxLength renders a live "n/max" character counter that turns red past the limit. */
|
|
11
|
+
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
12
|
+
error?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
showCount?: boolean;
|
|
15
|
+
maxLength?: number;
|
|
16
|
+
style?: React.CSSProperties;
|
|
17
|
+
}
|
|
18
|
+
export declare function Textarea(props: TextareaProps): JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export const inputBase = { height: "var(--h-input)", borderRadius: "var(--radius-md)", border: "1px solid var(--border-control)", background: "var(--bg-surface)", padding: "8px 12px", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-primary)", outline: "none", boxSizing: "border-box", width: "100%" };
|
|
4
|
+
|
|
5
|
+
/** Text input, 36px r8. readOnly = #fafafa inset; disabled = 50% opacity; error = red border + ring. */
|
|
6
|
+
export function Input({ readOnly, disabled, error, style, ...rest }) {
|
|
7
|
+
const [focus, setFocus] = React.useState(false);
|
|
8
|
+
return <input readOnly={readOnly} disabled={disabled} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
|
|
9
|
+
style={{ ...inputBase, ...(readOnly ? { background: "var(--bg-inset)", color: "var(--text-secondary)" } : null),
|
|
10
|
+
...(error ? { borderColor: "var(--danger)" } : null),
|
|
11
|
+
...(focus && !readOnly && !disabled ? { borderColor: error ? "var(--danger)" : "var(--focus-border)", boxShadow: `0 0 0 2px ${error ? "var(--danger-bg)" : "var(--focus-ring)"}` } : null),
|
|
12
|
+
...(disabled ? { opacity: .5, cursor: "not-allowed", background: "var(--bg-inset)" } : null), ...style }} {...rest} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Multiline textarea, r8, default 90px tall. showCount + maxLength renders a bottom-right "n/max" counter (red past the limit). */
|
|
16
|
+
export function Textarea({ style, rows = 4, error, disabled, showCount, maxLength, value, defaultValue, onChange, ...rest }) {
|
|
17
|
+
const [focus, setFocus] = React.useState(false);
|
|
18
|
+
const [inner, setInner] = React.useState(defaultValue || "");
|
|
19
|
+
const val = value != null ? value : inner;
|
|
20
|
+
const len = (val || "").length;
|
|
21
|
+
const over = maxLength != null && len > maxLength;
|
|
22
|
+
return (
|
|
23
|
+
<div style={{ position: "relative" }}>
|
|
24
|
+
<textarea rows={rows} disabled={disabled} maxLength={showCount ? undefined : maxLength}
|
|
25
|
+
value={value} defaultValue={value == null ? defaultValue : undefined}
|
|
26
|
+
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
|
|
27
|
+
onChange={(e) => { if (value == null) setInner(e.target.value); onChange && onChange(e); }}
|
|
28
|
+
style={{ ...inputBase, height: 90, resize: "vertical", paddingBottom: showCount ? 22 : 8,
|
|
29
|
+
...(error || over ? { borderColor: "var(--danger)" } : null),
|
|
30
|
+
...(focus && !disabled ? { borderColor: error || over ? "var(--danger)" : "var(--focus-border)", boxShadow: `0 0 0 2px ${error || over ? "var(--danger-bg)" : "var(--focus-ring)"}` } : null),
|
|
31
|
+
...(disabled ? { opacity: .5, cursor: "not-allowed", background: "var(--bg-inset)" } : null), ...style }} {...rest} />
|
|
32
|
+
{showCount ? <span style={{ position: "absolute", right: 10, bottom: 7, fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: over ? "var(--danger)" : "var(--text-muted)" }}>{len}{maxLength != null ? `/${maxLength}` : ""}</span> : null}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
export const inputBase = { height: 'var(--h-input)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-control)', background: 'var(--bg-surface)', padding: '8px 12px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-primary)', outline: 'none', boxSizing: 'border-box', width: '100%' }
|
|
4
|
+
// Text input, 36px r8. v-model. readonly = inset fill; disabled = 50% opacity; error = red border + ring.
|
|
5
|
+
const props = defineProps({ modelValue: String, readonly: Boolean, disabled: Boolean, error: Boolean, style: Object })
|
|
6
|
+
defineEmits(['update:modelValue'])
|
|
7
|
+
const focus = ref(false)
|
|
8
|
+
const s = computed(() => ({ ...inputBase, ...(props.readonly ? { background: 'var(--bg-inset)', color: 'var(--text-secondary)' } : null),
|
|
9
|
+
...(props.error ? { borderColor: 'var(--danger)' } : null),
|
|
10
|
+
...(focus.value && !props.readonly && !props.disabled ? { borderColor: props.error ? 'var(--danger)' : 'var(--focus-border)', boxShadow: `0 0 0 2px ${props.error ? 'var(--danger-bg)' : 'var(--focus-ring)'}` } : null),
|
|
11
|
+
...(props.disabled ? { opacity: .5, cursor: 'not-allowed', background: 'var(--bg-inset)' } : null), ...props.style }))
|
|
12
|
+
</script>
|
|
13
|
+
<template>
|
|
14
|
+
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" :readonly="readonly" :disabled="disabled" @focus="focus = true" @blur="focus = false" :style="s" />
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<!-- Textarea — multiline textarea, r8, default 90px tall. showCount + maxLength renders a bottom-right "n/max" counter (red past the limit). -->
|
|
18
|
+
<!--
|
|
19
|
+
<script setup name="Textarea">
|
|
20
|
+
import { ref, computed } from 'vue'
|
|
21
|
+
import { inputBase } from './Input.vue'
|
|
22
|
+
const props = defineProps({ modelValue: String, rows: { type: Number, default: 4 }, error: Boolean, disabled: Boolean, showCount: Boolean, maxLength: Number, style: Object })
|
|
23
|
+
defineEmits(['update:modelValue'])
|
|
24
|
+
const focus = ref(false)
|
|
25
|
+
const len = computed(() => (props.modelValue || '').length)
|
|
26
|
+
const over = computed(() => props.maxLength != null && len.value > props.maxLength)
|
|
27
|
+
const s = computed(() => ({ ...inputBase, height: 90, resize: 'vertical', paddingBottom: props.showCount ? 22 : 8,
|
|
28
|
+
...((props.error || over.value) ? { borderColor: 'var(--danger)' } : null),
|
|
29
|
+
...(focus.value && !props.disabled ? { borderColor: (props.error || over.value) ? 'var(--danger)' : 'var(--focus-border)', boxShadow: `0 0 0 2px ${(props.error || over.value) ? 'var(--danger-bg)' : 'var(--focus-ring)'}` } : null),
|
|
30
|
+
...(props.disabled ? { opacity: .5, cursor: 'not-allowed', background: 'var(--bg-inset)' } : null), ...props.style }))
|
|
31
|
+
</script>
|
|
32
|
+
<template>
|
|
33
|
+
<div style="position:relative">
|
|
34
|
+
<textarea :rows="rows" :disabled="disabled" :maxlength="showCount ? undefined : maxLength" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" @focus="focus = true" @blur="focus = false" :style="s" />
|
|
35
|
+
<span v-if="showCount" :style="{ position: 'absolute', right: 10, bottom: 7, fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: over ? 'var(--danger)' : 'var(--text-muted)' }">{{ len }}{{ maxLength != null ? `/${maxLength}` : '' }}</span>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
-->
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
/** Multi-select dropdown with checkbox options; picks render as removable tags with "+n" overflow and a clear-all ×. */
|
|
4
|
+
export interface MultiSelectProps {
|
|
5
|
+
options?: string[];
|
|
6
|
+
value?: string[];
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
/** Tags shown before collapsing to "+n" (default 2) */
|
|
9
|
+
maxTags?: number;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
width?: number | string;
|
|
12
|
+
onChange?: (value: string[]) => void;
|
|
13
|
+
style?: React.CSSProperties;
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
import { Checkbox } from "./Checkbox.jsx";
|
|
4
|
+
import { Tag } from "../display/Tag.jsx";
|
|
5
|
+
|
|
6
|
+
/** Multi-select dropdown: checkbox option list, selected values render as removable tags in the field (overflow collapses to "+n"), clear-all ×. */
|
|
7
|
+
export function MultiSelect({ options = [], value = [], placeholder = "Select...", maxTags = 2, disabled, width, onChange, style }) {
|
|
8
|
+
const [open, setOpen] = React.useState(false);
|
|
9
|
+
const root = React.useRef(null);
|
|
10
|
+
React.useEffect(() => { const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []);
|
|
11
|
+
const toggle = (o) => { const next = value.includes(o) ? value.filter(x => x !== o) : [...value, o]; onChange && onChange(next); };
|
|
12
|
+
const shown = value.slice(0, maxTags), extra = value.length - shown.length;
|
|
13
|
+
return (
|
|
14
|
+
<div ref={root} style={{ position: "relative", width, ...style }}>
|
|
15
|
+
<div onClick={() => !disabled && setOpen(!open)} style={{ minHeight: "var(--h-input)", borderRadius: "var(--radius-md)", border: `1px solid ${open ? "var(--focus-border)" : "var(--border-control)"}`, boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", background: "var(--bg-surface)", padding: "5px 30px 5px 8px", boxSizing: "border-box", display: "flex", alignItems: "center", flexWrap: "wrap", gap: 4, cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, position: "relative" }}>
|
|
16
|
+
{value.length === 0 && <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-muted)", paddingLeft: 4 }}>{placeholder}</span>}
|
|
17
|
+
{shown.map(v => <Tag key={v} removable onRemove={() => toggle(v)}>{v}</Tag>)}
|
|
18
|
+
{extra > 0 && <Tag>+{extra}</Tag>}
|
|
19
|
+
<span style={{ position: "absolute", right: 8, top: "50%", transform: "translateY(-50%)", display: "flex", alignItems: "center", gap: 2, color: "var(--text-muted)" }}>
|
|
20
|
+
{value.length > 0 && <button type="button" onClick={(e) => { e.stopPropagation(); onChange && onChange([]); }} style={{ border: "none", background: "transparent", cursor: "pointer", color: "inherit", display: "flex", padding: 0 }}><Icon name="x" size={13} /></button>}
|
|
21
|
+
<span style={{ display: "inline-flex", transform: open ? "rotate(180deg)" : "none", transition: "transform var(--dur-fast) var(--ease-out)" }}><Icon name="chevron-down" size={14} /></span>
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
{open && <div style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, right: 0, zIndex: "var(--z-menu)", background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-md)", boxShadow: "var(--shadow-menu)", padding: 4, maxHeight: 208, overflowY: "auto" }}>
|
|
25
|
+
{options.map(o => <div key={o} role="option" aria-selected={value.includes(o)} tabIndex={0} onClick={() => toggle(o)} style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 10px", borderRadius: "var(--radius-sm)", cursor: "pointer", background: "transparent" }}
|
|
26
|
+
onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-hover)"} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
|
|
27
|
+
<Checkbox size="sm" checked={value.includes(o)} onChange={() => toggle(o)} />
|
|
28
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-heading)" }}>{o}</span>
|
|
29
|
+
</div>)}
|
|
30
|
+
</div>}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
3
|
+
import Icon from '../icons/Icon.vue'
|
|
4
|
+
import Checkbox from './Checkbox.vue'
|
|
5
|
+
import Tag from '../display/Tag.vue'
|
|
6
|
+
// Multi-select dropdown: checkbox options; picks render as removable tags with "+n" overflow and clear-all ×.
|
|
7
|
+
const props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: Array, default: () => [] }, placeholder: { type: String, default: 'Select...' }, maxTags: { type: Number, default: 2 }, disabled: Boolean, width: [Number, String], style: Object })
|
|
8
|
+
const emit = defineEmits(['update:modelValue'])
|
|
9
|
+
const open = ref(false), root = ref(null)
|
|
10
|
+
function toggle(o) { emit('update:modelValue', props.modelValue.includes(o) ? props.modelValue.filter(x => x !== o) : [...props.modelValue, o]) }
|
|
11
|
+
const shown = computed(() => props.modelValue.slice(0, props.maxTags))
|
|
12
|
+
const extra = computed(() => props.modelValue.length - shown.value.length)
|
|
13
|
+
function onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }
|
|
14
|
+
onMounted(() => document.addEventListener('mousedown', onDoc))
|
|
15
|
+
onUnmounted(() => document.removeEventListener('mousedown', onDoc))
|
|
16
|
+
</script>
|
|
17
|
+
<template>
|
|
18
|
+
<div ref="root" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
|
|
19
|
+
<div @click="!disabled && (open = !open)" :style="{ minHeight: 'var(--h-input)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--border-control)'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: '5px 30px 5px 8px', boxSizing: 'border-box', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, position: 'relative' }">
|
|
20
|
+
<span v-if="modelValue.length === 0" style="font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-muted);padding-left:4px">{{ placeholder }}</span>
|
|
21
|
+
<Tag v-for="v in shown" :key="v" removable @remove="toggle(v)">{{ v }}</Tag>
|
|
22
|
+
<Tag v-if="extra > 0">+{{ extra }}</Tag>
|
|
23
|
+
<span style="position:absolute;right:8px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:2px;color:var(--text-muted)">
|
|
24
|
+
<button v-if="modelValue.length" type="button" @click.stop="emit('update:modelValue', [])" style="border:none;background:transparent;cursor:pointer;color:inherit;display:flex;padding:0"><Icon name="x" :size="13" /></button>
|
|
25
|
+
<span :style="{ display: 'inline-flex', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }"><Icon name="chevron-down" :size="14" /></span>
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div v-if="open" style="position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:var(--z-menu);background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;max-height:208px;overflow-y:auto">
|
|
29
|
+
<div v-for="o in options" :key="o" role="option" :aria-selected="modelValue.includes(o)" tabindex="0" @click="toggle(o)" class="kb-ms-row" style="display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--radius-sm);cursor:pointer">
|
|
30
|
+
<Checkbox size="sm" :checked="modelValue.includes(o)" @change="toggle(o)" />
|
|
31
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-heading)">{{ o }}</span>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
<style scoped>.kb-ms-row:hover{background:var(--bg-hover)}</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Segmented −/value/+ number stepper (25.5px, r4) — "Preview rows" control. */
|
|
3
|
+
export interface NumberStepperProps {
|
|
4
|
+
value?: number;
|
|
5
|
+
onChange?: (next: number) => void;
|
|
6
|
+
step?: number;
|
|
7
|
+
min?: number;
|
|
8
|
+
/** When set, the + button disables at the ceiling */
|
|
9
|
+
max?: number;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
export declare function NumberStepper(props: NumberStepperProps): JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
|
|
4
|
+
/** Segmented − / value / + stepper, 25.5px tall, r4 ("Preview rows"). Buttons disable themselves at min/max. */
|
|
5
|
+
export function NumberStepper({ value = 100, onChange, step = 10, min = 0, max, disabled, style }) {
|
|
6
|
+
const atMin = value <= min, atMax = max != null && value >= max;
|
|
7
|
+
const seg = (dis) => ({ width: 30, display: "flex", alignItems: "center", justifyContent: "center", cursor: dis ? "not-allowed" : "pointer", color: dis ? "var(--text-muted)" : "var(--text-body)", alignSelf: "stretch", background: "transparent", border: "none", padding: 0 });
|
|
8
|
+
return (
|
|
9
|
+
<div style={{ display: "inline-flex", alignItems: "center", height: 25.5, borderRadius: "var(--radius-sm)", border: "1px solid var(--border-control)", overflow: "hidden", background: "var(--bg-surface)", boxSizing: "border-box", opacity: disabled ? .5 : 1, ...style }}>
|
|
10
|
+
<button type="button" disabled={disabled || atMin} style={seg(disabled || atMin)} onClick={() => onChange && onChange(Math.max(min, value - step))}><Icon name="minus" size={12} /></button>
|
|
11
|
+
<span style={{ minWidth: 34, textAlign: "center", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-primary)", borderLeft: "1px solid var(--border-control)", borderRight: "1px solid var(--border-control)", alignSelf: "stretch", display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "0 6px" }}>{value}</span>
|
|
12
|
+
<button type="button" disabled={disabled || atMax} style={seg(disabled || atMax)} onClick={() => onChange && onChange(max != null ? Math.min(max, value + step) : value + step)}><Icon name="plus" size={12} /></button>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import Icon from '../icons/Icon.vue'
|
|
4
|
+
// Segmented − / value / + stepper, 25.5px tall, r4 ("Preview rows"). Buttons disable themselves at min/max.
|
|
5
|
+
const props = defineProps({ modelValue: { type: Number, default: 100 }, step: { type: Number, default: 10 }, min: { type: Number, default: 0 }, max: Number, disabled: Boolean, style: Object })
|
|
6
|
+
const emit = defineEmits(['update:modelValue'])
|
|
7
|
+
const atMin = computed(() => props.modelValue <= props.min)
|
|
8
|
+
const atMax = computed(() => props.max != null && props.modelValue >= props.max)
|
|
9
|
+
function seg(dis) { return { width: 30, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: dis ? 'not-allowed' : 'pointer', color: dis ? 'var(--text-muted)' : 'var(--text-body)', alignSelf: 'stretch', background: 'transparent', border: 'none', padding: 0 } }
|
|
10
|
+
</script>
|
|
11
|
+
<template>
|
|
12
|
+
<div :style="{ display: 'inline-flex', alignItems: 'center', height: 25.5, borderRadius: 'var(--radius-sm)', border: '1px solid var(--border-control)', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }">
|
|
13
|
+
<button type="button" :disabled="disabled || atMin" :style="seg(disabled || atMin)" @click="emit('update:modelValue', Math.max(min, modelValue - step))"><Icon name="minus" :size="12" /></button>
|
|
14
|
+
<span style="min-width:34px;text-align:center;font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-primary);border-left:1px solid var(--border-control);border-right:1px solid var(--border-control);align-self:stretch;display:inline-flex;align-items:center;justify-content:center;padding:0 6px">{{ modelValue }}</span>
|
|
15
|
+
<button type="button" :disabled="disabled || atMax" :style="seg(disabled || atMax)" @click="emit('update:modelValue', max != null ? Math.min(max, modelValue + step) : modelValue + step)"><Icon name="plus" :size="12" /></button>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
// Radio — 16px (sm: 13px) circle, 1.5px border, filled dot when selected.
|
|
4
|
+
const dims = { sm: { box: 13, dot: 6 }, md: { box: 16, dot: 8 } }
|
|
5
|
+
const props = defineProps({ checked: Boolean, label: String, size: { type: String, default: 'md' }, disabled: Boolean, style: Object })
|
|
6
|
+
const emit = defineEmits(['change'])
|
|
7
|
+
const d = computed(() => dims[props.size] || dims.md)
|
|
8
|
+
</script>
|
|
9
|
+
<template>
|
|
10
|
+
<label :style="{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--leading-sm)', gap: 6, cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .5 : 1, ...style }" @click="!disabled && emit('change', true)">
|
|
11
|
+
<span :style="{ width: d.box + 'px', height: d.box + 'px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', border: `1.5px solid ${checked ? 'var(--action-primary)' : 'var(--checkbox-border)'}`, background: 'var(--bg-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, transition: 'var(--t-hover)' }">
|
|
12
|
+
<span v-if="checked" :style="{ width: d.dot + 'px', height: d.dot + 'px', borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }" />
|
|
13
|
+
</span>
|
|
14
|
+
<span v-if="label" :style="{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-body)' }">{{ label }}</span>
|
|
15
|
+
</label>
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import Radio from './Radio.vue'
|
|
3
|
+
// Vertical or horizontal group of Radios.
|
|
4
|
+
defineProps({ options: { type: Array, default: () => [] }, modelValue: String, row: Boolean, size: { type: String, default: 'md' }, style: Object })
|
|
5
|
+
const emit = defineEmits(['update:modelValue'])
|
|
6
|
+
</script>
|
|
7
|
+
<template>
|
|
8
|
+
<div :style="{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: (row ? 16 : 10) + 'px', ...style }">
|
|
9
|
+
<Radio v-for="o in options" :key="o" :label="o" :size="size" :checked="modelValue === o" @change="emit('update:modelValue', o)" />
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Search field. Default: r10 hairline card filter. bare: borderless 13px top-bar flavor. Shows a clear (×) button once there's a value; uncontrolled if `value` is omitted. */
|
|
3
|
+
export interface SearchInputProps {
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
bare?: boolean;
|
|
6
|
+
value?: string;
|
|
7
|
+
/** Receives the new string value (consistent with Select / Combobox / MultiSelect) */
|
|
8
|
+
onChange?: (value: string) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
style?: React.CSSProperties;
|
|
11
|
+
}
|
|
12
|
+
export declare function SearchInput(props: SearchInputProps): JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
|
|
4
|
+
/** Bordered search field (r10) with leading search glyph; bare=true renders the borderless top-bar flavor. Shows a clear (×) button once there's a value. */
|
|
5
|
+
export function SearchInput({ placeholder = "Search...", bare, value, onChange, disabled, style }) {
|
|
6
|
+
const [inner, setInner] = React.useState("");
|
|
7
|
+
const val = value != null ? value : inner;
|
|
8
|
+
const set = (v) => { if (value == null) setInner(v); onChange && onChange(v); };
|
|
9
|
+
return (
|
|
10
|
+
<div style={{ display: "flex", alignItems: "center", gap: bare ? 12 : 8, opacity: disabled ? .5 : 1, ...(bare ? null : { border: "1px solid var(--border-control)", borderRadius: "var(--radius-lg)", padding: "6px 12px", background: "var(--bg-surface)" }), ...style }}>
|
|
11
|
+
<span style={{ color: "var(--text-muted)" }}><Icon name="search" size={bare ? 16 : 14} /></span>
|
|
12
|
+
<input value={val} disabled={disabled} onChange={(e) => set(e.target.value)} placeholder={placeholder}
|
|
13
|
+
style={{ border: "none", outline: "none", background: "transparent", flex: 1, minWidth: 0, fontFamily: "var(--font-sans)", fontSize: bare ? "var(--text-base)" : "var(--text-sm)", letterSpacing: bare ? "var(--tracking-base)" : 0, color: "var(--text-primary)", cursor: disabled ? "not-allowed" : "text" }} />
|
|
14
|
+
{val && !disabled ? <button type="button" onClick={() => set("")} style={{ border: "none", background: "transparent", cursor: "pointer", color: "var(--text-muted)", display: "flex", padding: 0 }}><Icon name="x" size={13} /></button> : null}
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import Icon from '../icons/Icon.vue'
|
|
3
|
+
// Bordered search field (r10) with leading search glyph; bare=true renders the borderless top-bar flavor. Shows a clear (×) button once there's a value.
|
|
4
|
+
defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Search...' }, bare: Boolean, disabled: Boolean, style: Object })
|
|
5
|
+
const emit = defineEmits(['update:modelValue'])
|
|
6
|
+
</script>
|
|
7
|
+
<template>
|
|
8
|
+
<div :style="{ display: 'flex', alignItems: 'center', gap: bare ? 12 : 8, opacity: disabled ? .5 : 1, ...(bare ? null : { border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', padding: '6px 12px', background: 'var(--bg-surface)' }), ...style }">
|
|
9
|
+
<span style="color:var(--text-muted)"><Icon name="search" :size="bare ? 16 : 14" /></span>
|
|
10
|
+
<input :value="modelValue" :disabled="disabled" @input="emit('update:modelValue', $event.target.value)" :placeholder="placeholder"
|
|
11
|
+
:style="{ border: 'none', outline: 'none', background: 'transparent', flex: 1, minWidth: 0, fontFamily: 'var(--font-sans)', fontSize: bare ? 'var(--text-base)' : 'var(--text-sm)', letterSpacing: bare ? 'var(--tracking-base)' : 0, color: 'var(--text-primary)', cursor: disabled ? 'not-allowed' : 'text' }" />
|
|
12
|
+
<button v-if="modelValue && !disabled" type="button" @click="emit('update:modelValue', '')" style="border:none;background:transparent;cursor:pointer;color:var(--text-muted);display:flex;padding:0"><Icon name="x" :size="13" /></button>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Hairline select. md 36/r8 for filters, sm 25/r4 inside table rows. disabled fills #fafafa inset like Input's readOnly. */
|
|
3
|
+
export interface SelectProps {
|
|
4
|
+
options?: string[];
|
|
5
|
+
size?: "md" | "sm";
|
|
6
|
+
value?: string;
|
|
7
|
+
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
|
8
|
+
/** Shown as a disabled, muted-color first option when no value is set */
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
error?: boolean;
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
export declare function Select(props: SelectProps): JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
|
|
4
|
+
/** Native select styled per Keybricks. size md 36/r8 · sm 25/r4 (table rows). disabled = #fafafa inset fill (matches Input readOnly), 50% opacity; error = red border. */
|
|
5
|
+
export function Select({ options = [], size = "md", value, onChange, placeholder, disabled, error, style }) {
|
|
6
|
+
const sm = size === "sm";
|
|
7
|
+
return (
|
|
8
|
+
<div style={{ position: "relative", display: "inline-flex", ...style }}>
|
|
9
|
+
<select value={value} onChange={onChange} disabled={disabled}
|
|
10
|
+
style={{ appearance: "none", WebkitAppearance: "none", height: sm ? 25 : 36, borderRadius: sm ? "var(--radius-sm)" : "var(--radius-md)", border: `1px solid ${error ? "var(--danger)" : "var(--border-control)"}`, background: disabled ? "var(--bg-inset)" : "var(--bg-surface)", padding: sm ? "0 26px 0 10px" : "0 32px 0 12px", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: disabled ? "var(--text-secondary)" : value ? "var(--text-primary)" : "var(--text-muted)", outline: "none", cursor: disabled ? "not-allowed" : "pointer", width: "100%", opacity: disabled ? .5 : 1 }}>
|
|
11
|
+
{placeholder ? <option value="" disabled hidden>{placeholder}</option> : null}
|
|
12
|
+
{options.map((o) => <option key={o} value={o}>{o}</option>)}
|
|
13
|
+
</select>
|
|
14
|
+
<span style={{ position: "absolute", right: sm ? 8 : 10, top: "50%", transform: "translateY(-50%)", pointerEvents: "none", color: "var(--text-muted)" }}><Icon name="chevron-down" size={sm ? 12 : 14} /></span>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|