@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,25 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import Icon from '../icons/Icon.vue'
|
|
4
|
+
import { IconTile } from '../display/Badge.vue'
|
|
5
|
+
// Expandable workflow list row: chevron + bordered 32px icon tile + title/meta + right count.
|
|
6
|
+
defineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, trailing: [String, Number], expanded: Boolean, style: Object })
|
|
7
|
+
defineEmits(['click'])
|
|
8
|
+
const hover = ref(false)
|
|
9
|
+
</script>
|
|
10
|
+
<template>
|
|
11
|
+
<div :style="{ borderBottom: '1px solid var(--border-card)', ...style }">
|
|
12
|
+
<div @click="$emit('click')" @mouseenter="hover = true" @mouseleave="hover = false" :style="{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', cursor: 'pointer', background: hover ? 'var(--bg-inset)' : 'transparent', transition: 'var(--t-hover)' }">
|
|
13
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
14
|
+
<span :style="{ color: 'var(--text-muted)', transform: expanded ? 'rotate(90deg)' : 'none', transition: 'transform .15s ease', display: 'inline-flex' }"><Icon name="chevron-right" :size="16" /></span>
|
|
15
|
+
<IconTile :icon="icon" kind="row" />
|
|
16
|
+
<div style="display:flex;flex-direction:column">
|
|
17
|
+
<span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ title }}</span>
|
|
18
|
+
<span v-if="meta" style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)">{{ meta }}</span>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<span v-if="trailing" style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)">{{ trailing }}</span>
|
|
22
|
+
</div>
|
|
23
|
+
<div v-if="expanded" style="padding:0 16px 12px 72px"><slot /></div>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Table pagination (26px controls, r4). Addition — not in the source file. */
|
|
3
|
+
export interface PaginationProps {
|
|
4
|
+
page?: number;
|
|
5
|
+
pageCount?: number;
|
|
6
|
+
/** Total rows; shows an "1–20 of 128" range label when given */
|
|
7
|
+
total?: number;
|
|
8
|
+
pageSize?: number;
|
|
9
|
+
onChange?: (page: number) => void;
|
|
10
|
+
style?: React.CSSProperties;
|
|
11
|
+
}
|
|
12
|
+
export declare function Pagination(props: PaginationProps): JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
|
|
4
|
+
/** Table pagination — 12px range label + prev/next + numbered pages. */
|
|
5
|
+
export function Pagination({ page = 1, pageCount = 1, total, pageSize = 20, onChange, style }) {
|
|
6
|
+
const btn = (content, disabled, onClick, active) => (
|
|
7
|
+
<button type="button" disabled={disabled} onClick={onClick}
|
|
8
|
+
style={{ minWidth: 26, height: 26, padding: "0 6px", borderRadius: "var(--radius-sm)", border: `1px solid ${active ? "var(--action-primary)" : "var(--border-control)"}`, background: active ? "var(--action-primary)" : "var(--bg-surface)", color: active ? "var(--on-accent)" : "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", cursor: disabled ? "default" : "pointer", opacity: disabled ? .45 : 1, display: "inline-flex", alignItems: "center", justifyContent: "center", transition: "var(--t-hover)" }}>
|
|
9
|
+
{content}
|
|
10
|
+
</button>
|
|
11
|
+
);
|
|
12
|
+
const pages = [];
|
|
13
|
+
const from = Math.max(1, Math.min(page - 1, pageCount - 2));
|
|
14
|
+
for (let i = from; i < from + 3 && i <= pageCount; i++) pages.push(i);
|
|
15
|
+
return (
|
|
16
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, ...style }}>
|
|
17
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", lineHeight: "var(--leading-sm)", color: "var(--text-secondary)" }}>
|
|
18
|
+
{total != null ? `${(page - 1) * pageSize + 1}–${Math.min(page * pageSize, total)} of ${total}` : `Page ${page} of ${pageCount}`}
|
|
19
|
+
</span>
|
|
20
|
+
<div style={{ display: "flex", gap: 4 }}>
|
|
21
|
+
{btn(<Icon name="chevron-left" size={13} />, page <= 1, () => onChange && onChange(page - 1))}
|
|
22
|
+
{pages.map((p) => <React.Fragment key={p}>{btn(p, false, () => onChange && onChange(p), p === page)}</React.Fragment>)}
|
|
23
|
+
{btn(<Icon name="chevron-right" size={13} />, page >= pageCount, () => onChange && onChange(page + 1))}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import Icon from '../icons/Icon.vue'
|
|
4
|
+
// Table pagination — 12px range label + prev/next + numbered pages.
|
|
5
|
+
const props = defineProps({ page: { type: Number, default: 1 }, pageCount: { type: Number, default: 1 }, total: Number, pageSize: { type: Number, default: 20 }, style: Object })
|
|
6
|
+
const emit = defineEmits(['change'])
|
|
7
|
+
const pages = computed(() => {
|
|
8
|
+
const from = Math.max(1, Math.min(props.page - 1, props.pageCount - 2)), out = []
|
|
9
|
+
for (let i = from; i < from + 3 && i <= props.pageCount; i++) out.push(i)
|
|
10
|
+
return out
|
|
11
|
+
})
|
|
12
|
+
const label = computed(() => props.total != null ? `${(props.page - 1) * props.pageSize + 1}–${Math.min(props.page * props.pageSize, props.total)} of ${props.total}` : `Page ${props.page} of ${props.pageCount}`)
|
|
13
|
+
function btnStyle(active, disabled) { return { minWidth: 26, height: 26, padding: '0 6px', borderRadius: 'var(--radius-sm)', border: `1px solid ${active ? 'var(--action-primary)' : 'var(--border-control)'}`, background: active ? 'var(--action-primary)' : 'var(--bg-surface)', color: active ? 'var(--on-accent)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .45 : 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', transition: 'var(--t-hover)' } }
|
|
14
|
+
</script>
|
|
15
|
+
<template>
|
|
16
|
+
<div :style="{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, ...style }">
|
|
17
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)">{{ label }}</span>
|
|
18
|
+
<div style="display:flex;gap:4px">
|
|
19
|
+
<button type="button" :disabled="page <= 1" @click="emit('change', page - 1)" :style="btnStyle(false, page <= 1)"><Icon name="chevron-left" :size="13" /></button>
|
|
20
|
+
<button v-for="p in pages" :key="p" type="button" @click="emit('change', p)" :style="btnStyle(p === page, false)">{{ p }}</button>
|
|
21
|
+
<button type="button" :disabled="page >= pageCount" @click="emit('change', page + 1)" :style="btnStyle(false, page >= pageCount)"><Icon name="chevron-right" :size="13" /></button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export interface SmartTableField {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Cell type: text | longText | number | select | multiSelect | date | person | checkbox | url | progress */
|
|
7
|
+
type?: string;
|
|
8
|
+
/** Column width px (default 140) */
|
|
9
|
+
width?: number;
|
|
10
|
+
/** select/multiSelect: option label → hue (gray|blue|green|orange|amber|teal|cyan|purple|pink|red) */
|
|
11
|
+
options?: Record<string, string>;
|
|
12
|
+
/** Footer aggregate: count | sum | avg */
|
|
13
|
+
aggregate?: "count" | "sum" | "avg";
|
|
14
|
+
/** Primary field renders medium-weight */
|
|
15
|
+
primary?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Notion/Airtable-style database grid: field-type header icons, hue pills, person/date/checkbox/url/progress cells, row hover + expand, inline editing, + New row, footer aggregates, view toolbar. */
|
|
19
|
+
export interface SmartTableProps {
|
|
20
|
+
fields?: SmartTableField[];
|
|
21
|
+
rows?: Record<string, any>[];
|
|
22
|
+
/** View name in the toolbar (default "Grid view") */
|
|
23
|
+
title?: string;
|
|
24
|
+
toolbar?: boolean;
|
|
25
|
+
/** Scroll-area max height */
|
|
26
|
+
height?: number | string;
|
|
27
|
+
rowHeight?: number;
|
|
28
|
+
onChange?: (rows: object[]) => void;
|
|
29
|
+
style?: React.CSSProperties;
|
|
30
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
import { IconButton } from "../buttons/IconButton.jsx";
|
|
4
|
+
import { Checkbox } from "../forms/Checkbox.jsx";
|
|
5
|
+
import { Avatar } from "../display/Avatar.jsx";
|
|
6
|
+
import { SearchInput } from "../forms/SearchInput.jsx";
|
|
7
|
+
|
|
8
|
+
const typeIcon = { text: "type", longText: "align-left", number: "hash", select: "chevron-down", multiSelect: "tag", date: "calendar", person: "user", checkbox: "check-square", url: "link", progress: "bar-chart-2" };
|
|
9
|
+
const Pill = ({ hue = "gray", children }) => <span style={{ display: "inline-flex", alignItems: "center", height: 20, padding: "0 8px", borderRadius: "var(--radius-full)", background: `var(--hue-${hue}-bg)`, color: `var(--hue-${hue}-fg)`, fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-xs)", lineHeight: 1, whiteSpace: "nowrap" }}>{children}</span>;
|
|
10
|
+
|
|
11
|
+
/** Notion/Airtable-style database grid ("smart table"): field-type icons in headers, colored select pills, person/date/checkbox/url/progress cells, row hover + expand, inline text/number editing, + New row, add-field column, per-column footer aggregates, and a view toolbar (Hide fields · Filter · Group · Sort · search). */
|
|
12
|
+
export function SmartTable({ fields = [], rows = [], title = "Grid view", toolbar = true, height, rowHeight = 32, onChange, style }) {
|
|
13
|
+
const [data, setData] = React.useState(rows);
|
|
14
|
+
const [hover, setHover] = React.useState(-1);
|
|
15
|
+
const [editing, setEditing] = React.useState(null);
|
|
16
|
+
const [draft, setDraft] = React.useState("");
|
|
17
|
+
const setCell = (r, key, v) => { const next = data.map((row, i) => i === r ? { ...row, [key]: v } : row); setData(next); onChange && onChange(next); };
|
|
18
|
+
const widths = ["40px", ...fields.map(f => (f.width || 140) + "px"), "40px"];
|
|
19
|
+
const grid = { display: "grid", gridTemplateColumns: widths.join(" "), width: "fit-content", minWidth: "100%" };
|
|
20
|
+
const cellBase = { display: "flex", alignItems: "center", gap: 5, padding: "0 8px", borderRight: "1px solid var(--border-subtle)", borderBottom: "1px solid var(--border-subtle)", height: rowHeight, boxSizing: "border-box", overflow: "hidden", whiteSpace: "nowrap", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-primary)" };
|
|
21
|
+
const agg = (f) => {
|
|
22
|
+
if (!f.aggregate) return "";
|
|
23
|
+
const vals = data.map(r => r[f.key]);
|
|
24
|
+
if (f.aggregate === "count") return "COUNT " + vals.filter(v => v === true || (v !== "" && v != null && v !== false)).length;
|
|
25
|
+
const nums = vals.map(Number).filter(n => !isNaN(n));
|
|
26
|
+
if (f.aggregate === "sum") return "SUM " + nums.reduce((a, b) => a + b, 0);
|
|
27
|
+
if (f.aggregate === "avg") return "AVG " + (nums.length ? Math.round(nums.reduce((a, b) => a + b, 0) / nums.length * 10) / 10 : 0);
|
|
28
|
+
return "";
|
|
29
|
+
};
|
|
30
|
+
const renderCell = (f, row, r) => {
|
|
31
|
+
const v = row[f.key];
|
|
32
|
+
const isEdit = editing && editing.r === r && editing.key === f.key;
|
|
33
|
+
if (isEdit) return <input autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onBlur={() => { setCell(r, f.key, f.type === "number" && draft !== "" && !isNaN(Number(draft)) ? Number(draft) : draft); setEditing(null); }} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") setEditing(null); }} style={{ position: "absolute", inset: 0, border: "2px solid var(--action-primary)", borderRadius: 2, outline: "none", padding: "0 6px", font: "inherit", background: "var(--bg-surface)", color: "var(--text-primary)", boxSizing: "border-box" }} />;
|
|
34
|
+
switch (f.type) {
|
|
35
|
+
case "select": { const hue = (f.options || {})[v] || "gray"; return v ? <Pill hue={hue}>{v}</Pill> : null; }
|
|
36
|
+
case "multiSelect": return <span style={{ display: "flex", gap: 4, overflow: "hidden" }}>{(v || []).map(t => <Pill key={t} hue={(f.options || {})[t] || "gray"}>{t}</Pill>)}</span>;
|
|
37
|
+
case "person": return v ? <span style={{ display: "inline-flex", alignItems: "center", gap: 6, minWidth: 0 }}><Avatar name={v} size={20} /><span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{v}</span></span> : null;
|
|
38
|
+
case "checkbox": return <Checkbox size="sm" checked={!!v} onChange={(nv) => setCell(r, f.key, nv)} />;
|
|
39
|
+
case "url": return v ? <a href={"https://" + v} onClick={(e) => e.preventDefault()} style={{ color: "var(--link)", overflow: "hidden", textOverflow: "ellipsis" }}>{v}</a> : null;
|
|
40
|
+
case "progress": return <span style={{ display: "flex", alignItems: "center", gap: 6, width: "100%" }}><span style={{ flex: 1, height: 4, borderRadius: "var(--radius-full)", background: "var(--track)", overflow: "hidden" }}><span style={{ display: "block", width: (v || 0) + "%", height: "100%", background: "var(--chart-blue)" }}></span></span><span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", color: "var(--text-secondary)" }}>{v || 0}%</span></span>;
|
|
41
|
+
case "number": return <span style={{ marginLeft: "auto", fontFamily: "var(--font-mono)", fontSize: 11.5 }}>{v}</span>;
|
|
42
|
+
default: return <span style={{ overflow: "hidden", textOverflow: "ellipsis", fontWeight: f.primary ? "var(--weight-medium)" : "var(--weight-regular)", color: f.primary ? "var(--text-heading)" : "var(--text-primary)" }}>{v}</span>;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const tbBtn = (icon, label) => <button key={label} type="button" style={{ display: "inline-flex", alignItems: "center", gap: 5, height: 26, padding: "0 8px", border: "none", borderRadius: "var(--radius-sm)", background: "transparent", cursor: "pointer", color: "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)" }} onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-hover)"} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}><Icon name={icon} size={13} />{label}</button>;
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ border: "1px solid var(--border-control)", borderRadius: "var(--radius-lg)", background: "var(--bg-surface)", overflow: "hidden", boxSizing: "border-box", ...style }}>
|
|
48
|
+
{toolbar && <div style={{ display: "flex", alignItems: "center", gap: 4, padding: "6px 10px", borderBottom: "1px solid var(--border-subtle)" }}>
|
|
49
|
+
<button type="button" style={{ display: "inline-flex", alignItems: "center", gap: 6, height: 26, padding: "0 8px", border: "none", borderRadius: "var(--radius-sm)", background: "var(--bg-active)", cursor: "pointer", color: "var(--text-heading)", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-sm)" }}><Icon name="layout-grid" size={13} />{title}<Icon name="chevron-down" size={12} /></button>
|
|
50
|
+
<span style={{ width: 1, height: 16, background: "var(--border-control)", margin: "0 4px" }}></span>
|
|
51
|
+
{tbBtn("eye-off", "Hide fields")}{tbBtn("funnel", "Filter")}{tbBtn("layers", "Group")}{tbBtn("arrow-down", "Sort")}
|
|
52
|
+
<span style={{ flex: 1 }}></span>
|
|
53
|
+
<SearchInput bare placeholder="Search..." style={{ width: 140, gap: 6 }} />
|
|
54
|
+
</div>}
|
|
55
|
+
<div style={{ overflow: "auto", maxHeight: height }}>
|
|
56
|
+
<div style={grid}>
|
|
57
|
+
<span style={{ ...cellBase, background: "var(--bg-inset)", justifyContent: "center" }}></span>
|
|
58
|
+
{fields.map(f => <span key={f.key} style={{ ...cellBase, background: "var(--bg-inset)", color: "var(--text-secondary)", fontSize: "var(--text-xs)", fontWeight: "var(--weight-medium)", letterSpacing: "var(--tracking-xs)" }}><Icon name={typeIcon[f.type] || "type"} size={12} style={{ color: "var(--text-muted)" }} />{f.label}</span>)}
|
|
59
|
+
<span style={{ ...cellBase, background: "var(--bg-inset)", justifyContent: "center" }}><IconButton icon="plus" size="xs" muted label="Add field" /></span>
|
|
60
|
+
{data.map((row, r) => [
|
|
61
|
+
<span key={"n" + r} onMouseEnter={() => setHover(r)} onMouseLeave={() => setHover(-1)} style={{ ...cellBase, justifyContent: "center", color: "var(--text-muted)", fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", background: hover === r ? "var(--bg-inset)" : "transparent", cursor: "pointer" }}>{hover === r ? <Icon name="maximize-2" size={12} style={{ color: "var(--action-primary)" }} /> : r + 1}</span>,
|
|
62
|
+
...fields.map(f => <span key={r + f.key} onMouseEnter={() => setHover(r)} onMouseLeave={() => setHover(-1)} onDoubleClick={() => { if (f.type === "text" || f.type === "number" || f.type === "date" || f.type === "url") { setEditing({ r, key: f.key }); setDraft(String(row[f.key] == null ? "" : row[f.key])); } }} style={{ ...cellBase, position: "relative", background: hover === r ? "var(--bg-inset)" : "transparent" }}>{renderCell(f, row, r)}</span>),
|
|
63
|
+
<span key={"a" + r} style={{ ...cellBase, background: hover === r ? "var(--bg-inset)" : "transparent" }}></span>,
|
|
64
|
+
])}
|
|
65
|
+
<button type="button" onClick={() => { const empty = {}; fields.forEach(f => empty[f.key] = f.type === "multiSelect" ? [] : f.type === "checkbox" ? false : ""); const next = [...data, empty]; setData(next); onChange && onChange(next); }} style={{ gridColumn: "1 / -1", display: "flex", alignItems: "center", gap: 6, height: rowHeight, padding: "0 10px", border: "none", borderBottom: "1px solid var(--border-subtle)", background: "transparent", cursor: "pointer", color: "var(--text-secondary)", fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", textAlign: "left" }} onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-inset)"} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}><Icon name="plus" size={13} />New</button>
|
|
66
|
+
<span style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)" }}></span>
|
|
67
|
+
{fields.map(f => <span key={"g" + f.key} style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)", justifyContent: "flex-end", fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", color: "var(--text-secondary)" }}>{agg(f)}</span>)}
|
|
68
|
+
<span style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)" }}></span>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import Icon from '../icons/Icon.vue'
|
|
4
|
+
import IconButton from '../buttons/IconButton.vue'
|
|
5
|
+
import Checkbox from '../forms/Checkbox.vue'
|
|
6
|
+
import Avatar from '../display/Avatar.vue'
|
|
7
|
+
import SearchInput from '../forms/SearchInput.vue'
|
|
8
|
+
// Notion/Airtable-style database grid: field-type header icons, hue pills, person/date/checkbox/url/progress cells, row hover + expand, inline editing, + New row, footer aggregates, view toolbar.
|
|
9
|
+
const typeIcon = { text: 'type', longText: 'align-left', number: 'hash', select: 'chevron-down', multiSelect: 'tag', date: 'calendar', person: 'user', checkbox: 'check-square', url: 'link', progress: 'bar-chart-2' }
|
|
10
|
+
const props = defineProps({ fields: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, title: { type: String, default: 'Grid view' }, toolbar: { type: Boolean, default: true }, height: [Number, String], rowHeight: { type: Number, default: 32 }, style: Object })
|
|
11
|
+
const emit = defineEmits(['change'])
|
|
12
|
+
const data = ref(props.rows.map(r => ({ ...r })))
|
|
13
|
+
const hover = ref(-1), editing = ref(null), draft = ref('')
|
|
14
|
+
const gridCols = computed(() => ['40px', ...props.fields.map(f => (f.width || 140) + 'px'), '40px'].join(' '))
|
|
15
|
+
const cellBase = { display: 'flex', alignItems: 'center', gap: '5px', padding: '0 8px', borderRight: '1px solid var(--border-subtle)', borderBottom: '1px solid var(--border-subtle)', boxSizing: 'border-box', overflow: 'hidden', whiteSpace: 'nowrap', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', color: 'var(--text-primary)' }
|
|
16
|
+
function setCell(r, key, v) { data.value[r][key] = v; emit('change', data.value) }
|
|
17
|
+
function commitEdit(r, f) { setCell(r, f.key, f.type === 'number' && draft.value !== '' && !isNaN(Number(draft.value)) ? Number(draft.value) : draft.value); editing.value = null }
|
|
18
|
+
function startEdit(r, f, row) { if (['text', 'number', 'date', 'url'].includes(f.type || 'text')) { editing.value = { r, key: f.key }; draft.value = String(row[f.key] == null ? '' : row[f.key]) } }
|
|
19
|
+
function addRow() { const empty = {}; props.fields.forEach(f => empty[f.key] = f.type === 'multiSelect' ? [] : f.type === 'checkbox' ? false : ''); data.value.push(empty); emit('change', data.value) }
|
|
20
|
+
function agg(f) {
|
|
21
|
+
if (!f.aggregate) return ''
|
|
22
|
+
const vals = data.value.map(r => r[f.key])
|
|
23
|
+
if (f.aggregate === 'count') return 'COUNT ' + vals.filter(v => v === true || (v !== '' && v != null && v !== false)).length
|
|
24
|
+
const nums = vals.map(Number).filter(n => !isNaN(n))
|
|
25
|
+
if (f.aggregate === 'sum') return 'SUM ' + nums.reduce((a, b) => a + b, 0)
|
|
26
|
+
return 'AVG ' + (nums.length ? Math.round(nums.reduce((a, b) => a + b, 0) / nums.length * 10) / 10 : 0)
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
<template>
|
|
30
|
+
<div :style="{ border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', background: 'var(--bg-surface)', overflow: 'hidden', boxSizing: 'border-box', ...style }">
|
|
31
|
+
<div v-if="toolbar" style="display:flex;align-items:center;gap:4px;padding:6px 10px;border-bottom:1px solid var(--border-subtle)">
|
|
32
|
+
<button type="button" style="display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 8px;border:none;border-radius:var(--radius-sm);background:var(--bg-active);cursor:pointer;color:var(--text-heading);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm)"><Icon name="layout-grid" :size="13" />{{ title }}<Icon name="chevron-down" :size="12" /></button>
|
|
33
|
+
<span style="width:1px;height:16px;background:var(--border-control);margin:0 4px" />
|
|
34
|
+
<button v-for="b in [['eye-off','Hide fields'],['funnel','Filter'],['layers','Group'],['arrow-down','Sort']]" :key="b[1]" type="button" class="kb-st-btn"><Icon :name="b[0]" :size="13" />{{ b[1] }}</button>
|
|
35
|
+
<span style="flex:1" />
|
|
36
|
+
<SearchInput bare placeholder="Search..." style="width:140px;gap:6px" />
|
|
37
|
+
</div>
|
|
38
|
+
<div :style="{ overflow: 'auto', maxHeight: typeof height === 'number' ? height + 'px' : height }">
|
|
39
|
+
<div :style="{ display: 'grid', gridTemplateColumns: gridCols, width: 'fit-content', minWidth: '100%' }">
|
|
40
|
+
<span :style="{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)' }" />
|
|
41
|
+
<span v-for="f in fields" :key="f.key" :style="{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)', color: 'var(--text-secondary)', fontSize: 'var(--text-xs)', fontWeight: 'var(--weight-medium)' }"><Icon :name="typeIcon[f.type] || 'type'" :size="12" style="color:var(--text-muted)" />{{ f.label }}</span>
|
|
42
|
+
<span :style="{ ...cellBase, height: rowHeight + 'px', background: 'var(--bg-inset)', justifyContent: 'center' }"><IconButton icon="plus" size="xs" muted label="Add field" /></span>
|
|
43
|
+
<template v-for="(row, r) in data" :key="r">
|
|
44
|
+
<span @mouseenter="hover = r" @mouseleave="hover = -1" :style="{ ...cellBase, height: rowHeight + 'px', justifyContent: 'center', color: 'var(--text-muted)', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-2xs)', background: hover === r ? 'var(--bg-inset)' : 'transparent', cursor: 'pointer' }">
|
|
45
|
+
<Icon v-if="hover === r" name="maximize-2" :size="12" style="color:var(--action-primary)" /><template v-else>{{ r + 1 }}</template>
|
|
46
|
+
</span>
|
|
47
|
+
<span v-for="f in fields" :key="r + f.key" @mouseenter="hover = r" @mouseleave="hover = -1" @dblclick="startEdit(r, f, row)" :style="{ ...cellBase, height: rowHeight + 'px', position: 'relative', background: hover === r ? 'var(--bg-inset)' : 'transparent' }">
|
|
48
|
+
<input v-if="editing && editing.r === r && editing.key === f.key" autofocus :value="draft" @input="draft = $event.target.value" @blur="commitEdit(r, f)" @keydown.enter="$event.target.blur()" @keydown.esc="editing = null" style="position:absolute;inset:0;border:2px solid var(--action-primary);border-radius:2px;outline:none;padding:0 6px;font:inherit;background:var(--bg-surface);color:var(--text-primary);box-sizing:border-box" />
|
|
49
|
+
<template v-else-if="f.type === 'select'"><span v-if="row[f.key]" class="kb-pill" :style="{ background: `var(--hue-${(f.options || {})[row[f.key]] || 'gray'}-bg)`, color: `var(--hue-${(f.options || {})[row[f.key]] || 'gray'}-fg)` }">{{ row[f.key] }}</span></template>
|
|
50
|
+
<template v-else-if="f.type === 'multiSelect'"><span style="display:flex;gap:4px;overflow:hidden"><span v-for="t in row[f.key] || []" :key="t" class="kb-pill" :style="{ background: `var(--hue-${(f.options || {})[t] || 'gray'}-bg)`, color: `var(--hue-${(f.options || {})[t] || 'gray'}-fg)` }">{{ t }}</span></span></template>
|
|
51
|
+
<template v-else-if="f.type === 'person'"><span v-if="row[f.key]" style="display:inline-flex;align-items:center;gap:6px;min-width:0"><Avatar :name="row[f.key]" :size="20" /><span style="overflow:hidden;text-overflow:ellipsis">{{ row[f.key] }}</span></span></template>
|
|
52
|
+
<template v-else-if="f.type === 'checkbox'"><Checkbox size="sm" :checked="!!row[f.key]" @change="v => setCell(r, f.key, v)" /></template>
|
|
53
|
+
<template v-else-if="f.type === 'url'"><a v-if="row[f.key]" href="#" @click.prevent style="overflow:hidden;text-overflow:ellipsis">{{ row[f.key] }}</a></template>
|
|
54
|
+
<template v-else-if="f.type === 'progress'"><span style="display:flex;align-items:center;gap:6px;width:100%"><span style="flex:1;height:4px;border-radius:var(--radius-full);background:var(--track);overflow:hidden"><span :style="{ display: 'block', width: (row[f.key] || 0) + '%', height: '100%', background: 'var(--chart-blue)' }" /></span><span style="font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-secondary)">{{ row[f.key] || 0 }}%</span></span></template>
|
|
55
|
+
<template v-else-if="f.type === 'number'"><span style="margin-left:auto;font-family:var(--font-mono);font-size:11.5px">{{ row[f.key] }}</span></template>
|
|
56
|
+
<template v-else><span :style="{ overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: f.primary ? 'var(--weight-medium)' : 'var(--weight-regular)', color: f.primary ? 'var(--text-heading)' : 'var(--text-primary)' }">{{ row[f.key] }}</span></template>
|
|
57
|
+
</span>
|
|
58
|
+
<span :style="{ ...cellBase, height: rowHeight + 'px', background: hover === r ? 'var(--bg-inset)' : 'transparent' }" />
|
|
59
|
+
</template>
|
|
60
|
+
<button type="button" @click="addRow" class="kb-st-new" :style="{ gridColumn: '1 / -1', height: rowHeight + 'px' }"><Icon name="plus" :size="13" />New</button>
|
|
61
|
+
<span :style="{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)' }" />
|
|
62
|
+
<span v-for="f in fields" :key="'g' + f.key" :style="{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)', justifyContent: 'flex-end', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-2xs)', color: 'var(--text-secondary)' }">{{ agg(f) }}</span>
|
|
63
|
+
<span :style="{ ...cellBase, height: rowHeight + 'px', borderBottom: 'none', background: 'var(--bg-inset)' }" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
<style scoped>
|
|
69
|
+
.kb-pill{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:var(--radius-full);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-xs);line-height:1;white-space:nowrap}
|
|
70
|
+
.kb-st-btn{display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 8px;border:none;border-radius:var(--radius-sm);background:transparent;cursor:pointer;color:var(--text-body);font-family:var(--font-sans);font-size:var(--text-sm)}
|
|
71
|
+
.kb-st-btn:hover{background:var(--bg-hover)}
|
|
72
|
+
.kb-st-new{display:flex;align-items:center;gap:6px;padding:0 10px;border:none;border-bottom:1px solid var(--border-subtle);background:transparent;cursor:pointer;color:var(--text-secondary);font-family:var(--font-sans);font-size:var(--text-sm);text-align:left}
|
|
73
|
+
.kb-st-new:hover{background:var(--bg-inset)}
|
|
74
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export type SpreadsheetCell = string | number | {
|
|
4
|
+
/** Literal value */
|
|
5
|
+
v?: string | number;
|
|
6
|
+
/** Formula string, e.g. "=SUM(B2:E2)" — supports SUM/AVERAGE/COUNT/MIN/MAX over a range plus + - * / with cell refs */
|
|
7
|
+
f?: string;
|
|
8
|
+
/** Display format */
|
|
9
|
+
t?: "number" | "currency" | "percent";
|
|
10
|
+
/** Bold / italic / underline */
|
|
11
|
+
b?: boolean; i?: boolean; u?: boolean;
|
|
12
|
+
align?: "flex-start" | "center" | "flex-end";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Excel-style spreadsheet: toolbar (undo/redo, B/I/U, format), formula bar with name box + fx, frozen lettered/numbered headers, click + shift-click range selection, double-click cell editing, live formula evaluation, sheet tabs, Sum/Avg/Count status bar. */
|
|
16
|
+
export interface SpreadsheetProps {
|
|
17
|
+
/** 2D cell array (row-major). Primitives or cell objects. */
|
|
18
|
+
data?: SpreadsheetCell[][];
|
|
19
|
+
/** Minimum grid size (padded with empty cells) */
|
|
20
|
+
rows?: number; cols?: number;
|
|
21
|
+
/** Per-column pixel widths; default 92 */
|
|
22
|
+
colWidths?: number[]; defaultColWidth?: number;
|
|
23
|
+
/** Currency symbol for t:"currency" cells */
|
|
24
|
+
currency?: string;
|
|
25
|
+
/** Initially selected cell ref, e.g. "F4" */
|
|
26
|
+
defaultActive?: string;
|
|
27
|
+
sheets?: string[]; activeSheet?: string;
|
|
28
|
+
toolbar?: boolean; formulaBar?: boolean; statusBar?: boolean;
|
|
29
|
+
/** Scroll-area max height */
|
|
30
|
+
height?: number | string;
|
|
31
|
+
onChange?: (cells: object[][]) => void;
|
|
32
|
+
style?: React.CSSProperties;
|
|
33
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
import { IconButton } from "../buttons/IconButton.jsx";
|
|
4
|
+
import { Select } from "../forms/Select.jsx";
|
|
5
|
+
|
|
6
|
+
export const colName = (i) => { let s = ""; i++; while (i > 0) { s = String.fromCharCode(64 + ((i - 1) % 26 + 1)) + s; i = Math.floor((i - 1) / 26); } return s; };
|
|
7
|
+
const norm = (cell) => cell == null ? { v: "" } : (typeof cell === "object" ? { ...cell } : { v: cell });
|
|
8
|
+
const refRC = (ref) => { const m = /^([A-Z]+)(\d+)$/.exec(String(ref).trim().toUpperCase()); if (!m) return null; let c = 0; for (const ch of m[1]) c = c * 26 + (ch.charCodeAt(0) - 64); return { r: +m[2] - 1, c: c - 1 }; };
|
|
9
|
+
|
|
10
|
+
function evalCell(cells, cell, depth = 0) {
|
|
11
|
+
if (!cell || depth > 12) return "";
|
|
12
|
+
const raw = cell.f != null ? cell.f : cell.v;
|
|
13
|
+
if (typeof raw === "string" && raw.startsWith("=")) return evalFormula(cells, raw, depth);
|
|
14
|
+
return raw;
|
|
15
|
+
}
|
|
16
|
+
function numAt(cells, r, c, depth) { const v = evalCell(cells, cells[r] && cells[r][c], depth + 1); const n = typeof v === "number" ? v : parseFloat(String(v).replace(/[^0-9.eE+-]/g, "")); return isNaN(n) ? null : n; }
|
|
17
|
+
function evalFormula(cells, f, depth = 0) {
|
|
18
|
+
try {
|
|
19
|
+
let expr = f.slice(1).toUpperCase().replace(/\s+/g, "");
|
|
20
|
+
expr = expr.replace(/(SUM|AVERAGE|AVG|COUNT|MIN|MAX)\(([A-Z]+\d+):([A-Z]+\d+)\)/g, (m, fn, a, b) => {
|
|
21
|
+
const A = refRC(a), B = refRC(b), vals = [];
|
|
22
|
+
for (let r = Math.min(A.r, B.r); r <= Math.max(A.r, B.r); r++) for (let c = Math.min(A.c, B.c); c <= Math.max(A.c, B.c); c++) { const n = numAt(cells, r, c, depth); if (n != null) vals.push(n); }
|
|
23
|
+
if (fn === "COUNT") return String(vals.length);
|
|
24
|
+
if (!vals.length) return "0";
|
|
25
|
+
if (fn === "SUM") return String(vals.reduce((x, y) => x + y, 0));
|
|
26
|
+
if (fn === "MIN") return String(Math.min(...vals));
|
|
27
|
+
if (fn === "MAX") return String(Math.max(...vals));
|
|
28
|
+
return String(vals.reduce((x, y) => x + y, 0) / vals.length);
|
|
29
|
+
});
|
|
30
|
+
expr = expr.replace(/[A-Z]+\d+/g, (ref) => { const p = refRC(ref); const n = p ? numAt(cells, p.r, p.c, depth) : null; return String(n == null ? 0 : n); });
|
|
31
|
+
if (!/^[-+*/().\d\seE]*$/.test(expr)) return "#ERROR";
|
|
32
|
+
const out = Function('"use strict";return (' + expr + ")")();
|
|
33
|
+
return typeof out === "number" && isFinite(out) ? out : "#ERROR";
|
|
34
|
+
} catch (e) { return "#ERROR"; }
|
|
35
|
+
}
|
|
36
|
+
function fmt(val, t, currency) {
|
|
37
|
+
if (val === "" || val == null) return "";
|
|
38
|
+
if (typeof val !== "number") return String(val);
|
|
39
|
+
const r2 = Math.round(val * 100) / 100;
|
|
40
|
+
if (t === "percent") return (Math.round(val * 1000) / 10) + "%";
|
|
41
|
+
if (t === "currency") return currency + r2.toLocaleString();
|
|
42
|
+
return r2.toLocaleString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Excel-style spreadsheet: toolbar, formula bar (name box + fx), lettered/numbered frozen headers, click + shift-click range selection, double-click editing, =SUM()/AVERAGE()-style formulas, sheet tabs and a Sum/Avg/Count status bar. */
|
|
46
|
+
export function Spreadsheet({ data = [], rows, cols, colWidths = [], defaultColWidth = 92, currency = "$", defaultActive = "A1", sheets = ["Sheet1"], activeSheet, toolbar = true, formulaBar = true, statusBar = true, height, onChange, style }) {
|
|
47
|
+
const nR = Math.max(rows || 0, data.length, 8), nC = Math.max(cols || 0, ...data.map(r => r.length), 6);
|
|
48
|
+
const [cells, setCells] = React.useState(() => Array.from({ length: nR }, (_, r) => Array.from({ length: nC }, (_, c) => norm(data[r] && data[r][c]))));
|
|
49
|
+
const a0 = refRC(defaultActive) || { r: 0, c: 0 };
|
|
50
|
+
const [active, setActive] = React.useState(a0);
|
|
51
|
+
const [anchor, setAnchor] = React.useState(a0);
|
|
52
|
+
const [editing, setEditing] = React.useState(null);
|
|
53
|
+
const [draft, setDraft] = React.useState("");
|
|
54
|
+
const [sheet, setSheet] = React.useState(activeSheet || sheets[0]);
|
|
55
|
+
const cur = cells[active.r][active.c];
|
|
56
|
+
const rawOf = (cell) => cell.f != null ? cell.f : (cell.v == null ? "" : String(cell.v));
|
|
57
|
+
const commit = (r, c, text) => { const next = cells.map(row => row.map(cell => ({ ...cell }))); const cell = next[r][c]; delete cell.f; if (String(text).startsWith("=")) { cell.f = text; cell.v = ""; } else { const n = text !== "" && !isNaN(Number(text)) ? Number(text) : text; cell.v = n; } setCells(next); onChange && onChange(next); };
|
|
58
|
+
const lo = { r: Math.min(active.r, anchor.r), c: Math.min(active.c, anchor.c) }, hi = { r: Math.max(active.r, anchor.r), c: Math.max(active.c, anchor.c) };
|
|
59
|
+
const multi = lo.r !== hi.r || lo.c !== hi.c;
|
|
60
|
+
const inRange = (r, c) => r >= lo.r && r <= hi.r && c >= lo.c && c <= hi.c;
|
|
61
|
+
const stats = React.useMemo(() => { if (!multi) return null; const vals = []; for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) { const v = evalCell(cells, cells[r][c]); const n = typeof v === "number" ? v : NaN; if (!isNaN(n)) vals.push(n); } if (!vals.length) return null; const sum = vals.reduce((x, y) => x + y, 0); return { sum: Math.round(sum * 100) / 100, avg: Math.round(sum / vals.length * 100) / 100, count: vals.length }; }, [cells, active, anchor]);
|
|
62
|
+
const pick = (r, c, e) => { setActive({ r, c }); if (!(e && e.shiftKey)) setAnchor({ r, c }); setEditing(null); };
|
|
63
|
+
const setFlag = (k) => { const next = cells.map(row => row.map(cell => ({ ...cell }))); for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) next[r][c][k] = !cur[k]; setCells(next); };
|
|
64
|
+
const setType = (label) => { const t = { Automatic: undefined, Number: "number", Currency: "currency", Percent: "percent" }[label]; const next = cells.map(row => row.map(cell => ({ ...cell }))); for (let r = lo.r; r <= hi.r; r++) for (let c = lo.c; c <= hi.c; c++) { if (t) next[r][c].t = t; else delete next[r][c].t; } setCells(next); };
|
|
65
|
+
const widths = [46]; for (let c = 0; c < nC; c++) widths.push(colWidths[c] || defaultColWidth);
|
|
66
|
+
const hdr = { position: "sticky", top: 0, zIndex: 3, background: "var(--bg-inset)", borderRight: "1px solid var(--border-control)", borderBottom: "1px solid var(--border-control)", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-xs)", letterSpacing: "var(--tracking-xs)", color: "var(--text-secondary)", height: 26, boxSizing: "border-box", userSelect: "none" };
|
|
67
|
+
const tbBtn = (label, k, st) => <button key={label} type="button" onClick={() => setFlag(k)} style={{ width: 24, height: 24, border: "none", borderRadius: "var(--radius-sm)", cursor: "pointer", background: cur[k] ? "var(--bg-active)" : "transparent", color: "var(--text-body)", fontFamily: "var(--font-sans)", fontSize: 12.5, ...st }}>{label}</button>;
|
|
68
|
+
return (
|
|
69
|
+
<div style={{ border: "1px solid var(--border-control)", borderRadius: "var(--radius-lg)", overflow: "hidden", background: "var(--bg-surface)", boxSizing: "border-box", ...style }}>
|
|
70
|
+
{toolbar && <div style={{ height: 36, display: "flex", alignItems: "center", gap: 2, padding: "0 8px", borderBottom: "1px solid var(--border-subtle)" }}>
|
|
71
|
+
<IconButton icon="undo-2" size="sm" muted label="Undo" /><IconButton icon="redo-2" size="sm" muted label="Redo" />
|
|
72
|
+
<span style={{ width: 1, height: 16, background: "var(--border-control)", margin: "0 6px" }}></span>
|
|
73
|
+
{tbBtn("B", "b", { fontWeight: 700 })}{tbBtn("I", "i", { fontStyle: "italic" })}{tbBtn("U", "u", { textDecoration: "underline" })}
|
|
74
|
+
<span style={{ width: 1, height: 16, background: "var(--border-control)", margin: "0 6px" }}></span>
|
|
75
|
+
<Select size="sm" options={["Automatic", "Number", "Currency", "Percent"]} value={{ number: "Number", currency: "Currency", percent: "Percent" }[cur.t] || "Automatic"} onChange={setType} style={{ width: 110 }} />
|
|
76
|
+
</div>}
|
|
77
|
+
{formulaBar && <div style={{ height: 32, display: "flex", alignItems: "center", borderBottom: "1px solid var(--border-control)" }}>
|
|
78
|
+
<span style={{ width: 72, alignSelf: "stretch", display: "flex", alignItems: "center", justifyContent: "center", borderRight: "1px solid var(--border-control)", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-heading)" }}>{multi ? colName(lo.c) + (lo.r + 1) + ":" + colName(hi.c) + (hi.r + 1) : colName(active.c) + (active.r + 1)}</span>
|
|
79
|
+
<span style={{ padding: "0 10px", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", fontStyle: "italic", color: "var(--text-muted)", borderRight: "1px solid var(--border-subtle)", alignSelf: "stretch", display: "flex", alignItems: "center" }}>fx</span>
|
|
80
|
+
<input value={editing === "fx" ? draft : rawOf(cur)} onFocus={() => { setEditing("fx"); setDraft(rawOf(cur)); }} onChange={(e) => setDraft(e.target.value)} onBlur={() => { if (editing === "fx") { commit(active.r, active.c, draft); setEditing(null); } }} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") { setEditing(null); e.target.blur(); } }} style={{ flex: 1, border: "none", outline: "none", background: "transparent", padding: "0 10px", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-primary)" }} />
|
|
81
|
+
</div>}
|
|
82
|
+
<div style={{ overflow: "auto", maxHeight: height }}>
|
|
83
|
+
<div style={{ display: "grid", gridTemplateColumns: widths.map(w => w + "px").join(" "), width: "fit-content" }}>
|
|
84
|
+
<span style={{ ...hdr, zIndex: 5, left: 0 }}><span style={{ width: 0, height: 0, borderStyle: "solid", borderWidth: "0 0 10px 10px", borderColor: "transparent transparent var(--border-control) transparent", alignSelf: "flex-end", marginLeft: "auto", marginRight: 3, marginBottom: 3 }}></span></span>
|
|
85
|
+
{Array.from({ length: nC }, (_, c) => <span key={c} style={{ ...hdr, ...(c >= lo.c && c <= hi.c ? { background: "var(--soft-bg)", color: "var(--action-primary)" } : null) }}>{colName(c)}</span>)}
|
|
86
|
+
{cells.map((row, r) => [
|
|
87
|
+
<span key={"h" + r} style={{ ...hdr, position: "sticky", left: 0, top: "auto", zIndex: 2, height: 28, ...(r >= lo.r && r <= hi.r ? { background: "var(--soft-bg)", color: "var(--action-primary)" } : null) }}>{r + 1}</span>,
|
|
88
|
+
...row.map((cell, c) => {
|
|
89
|
+
const v = evalCell(cells, cell);
|
|
90
|
+
const isNum = typeof v === "number";
|
|
91
|
+
const isActive = active.r === r && active.c === c;
|
|
92
|
+
const isEdit = editing && editing !== "fx" && editing.r === r && editing.c === c;
|
|
93
|
+
return <span key={r + "," + c} onMouseDown={(e) => pick(r, c, e)} onDoubleClick={() => { setEditing({ r, c }); setDraft(rawOf(cell)); }}
|
|
94
|
+
style={{ position: "relative", height: 28, boxSizing: "border-box", borderRight: "1px solid var(--border-subtle)", borderBottom: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", justifyContent: cell.align || (isNum ? "flex-end" : "flex-start"), padding: "0 8px", cursor: "cell", background: isActive ? "var(--bg-surface)" : inRange(r, c) ? "var(--soft-bg)" : "var(--bg-surface)", boxShadow: isActive ? "inset 0 0 0 2px var(--action-primary)" : "none", zIndex: isActive ? 1 : 0, fontFamily: isNum ? "var(--font-mono)" : "var(--font-sans)", fontSize: isNum ? 11.5 : "var(--text-sm)", color: v === "#ERROR" ? "var(--danger)" : "var(--text-primary)", fontWeight: cell.b ? 600 : "var(--weight-regular)", fontStyle: cell.i ? "italic" : "normal", textDecoration: cell.u ? "underline" : "none", whiteSpace: "nowrap", overflow: "hidden", userSelect: "none" }}>
|
|
95
|
+
{isEdit ? <input autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onBlur={() => { commit(r, c, draft); setEditing(null); }} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") setEditing(null); }} style={{ position: "absolute", inset: 0, border: "none", outline: "none", padding: "0 8px", fontFamily: "inherit", fontSize: "inherit", background: "var(--bg-surface)", color: "var(--text-primary)" }} /> : fmt(v, cell.t, currency)}
|
|
96
|
+
{isActive && !isEdit && <span style={{ position: "absolute", right: -3, bottom: -3, width: 6, height: 6, background: "var(--action-primary)", border: "1px solid var(--bg-surface)", cursor: "crosshair", zIndex: 2 }}></span>}
|
|
97
|
+
</span>;
|
|
98
|
+
})])}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
{statusBar && <div style={{ height: 32, display: "flex", alignItems: "center", justifyContent: "space-between", borderTop: "1px solid var(--border-control)", background: "var(--bg-inset)", paddingRight: 12 }}>
|
|
102
|
+
<div style={{ display: "flex", alignSelf: "stretch", alignItems: "stretch" }}>
|
|
103
|
+
{sheets.map(s => <button key={s} type="button" onClick={() => setSheet(s)} style={{ border: "none", borderRight: "1px solid var(--border-subtle)", padding: "0 16px", cursor: "pointer", background: sheet === s ? "var(--bg-surface)" : "transparent", color: sheet === s ? "var(--text-heading)" : "var(--text-secondary)", fontFamily: "var(--font-sans)", fontWeight: sheet === s ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--text-sm)", boxShadow: sheet === s ? "inset 0 2px 0 var(--action-primary)" : "none" }}>{s}</button>)}
|
|
104
|
+
<span style={{ display: "flex", alignItems: "center", padding: "0 6px" }}><IconButton icon="plus" size="xs" muted label="Add sheet" /></span>
|
|
105
|
+
</div>
|
|
106
|
+
<span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{stats ? "Sum " + fmt(stats.sum, cur.t, currency) + " Avg " + fmt(stats.avg, cur.t, currency) + " Count " + stats.count : "100%"}</span>
|
|
107
|
+
</div>}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import Icon from '../icons/Icon.vue'
|
|
4
|
+
import IconButton from '../buttons/IconButton.vue'
|
|
5
|
+
import KSelect from '../forms/Select.vue'
|
|
6
|
+
// Excel-style spreadsheet: toolbar, formula bar (name box + fx), frozen lettered/numbered headers, click + shift-click range selection, double-click editing, =SUM()/AVERAGE() formulas, sheet tabs, Sum/Avg/Count status bar.
|
|
7
|
+
const props = defineProps({ data: { type: Array, default: () => [] }, rows: Number, cols: Number, colWidths: { type: Array, default: () => [] }, defaultColWidth: { type: Number, default: 92 }, currency: { type: String, default: '$' }, defaultActive: { type: String, default: 'A1' }, sheets: { type: Array, default: () => ['Sheet1'] }, toolbar: { type: Boolean, default: true }, formulaBar: { type: Boolean, default: true }, statusBar: { type: Boolean, default: true }, height: [Number, String], style: Object })
|
|
8
|
+
const emit = defineEmits(['change'])
|
|
9
|
+
|
|
10
|
+
const colName = (i) => { let s = ''; i++; while (i > 0) { s = String.fromCharCode(64 + ((i - 1) % 26 + 1)) + s; i = Math.floor((i - 1) / 26) } return s }
|
|
11
|
+
const refRC = (ref) => { const m = /^([A-Z]+)(\d+)$/.exec(String(ref).trim().toUpperCase()); if (!m) return null; let c = 0; for (const ch of m[1]) c = c * 26 + (ch.charCodeAt(0) - 64); return { r: +m[2] - 1, c: c - 1 } }
|
|
12
|
+
const norm = (cell) => cell == null ? { v: '' } : (typeof cell === 'object' ? { ...cell } : { v: cell })
|
|
13
|
+
function evalCell(cells, cell, depth = 0) { if (!cell || depth > 12) return ''; const raw = cell.f != null ? cell.f : cell.v; if (typeof raw === 'string' && raw.startsWith('=')) return evalFormula(cells, raw, depth); return raw }
|
|
14
|
+
function numAt(cells, r, c, depth) { const v = evalCell(cells, cells[r] && cells[r][c], depth + 1); const n = typeof v === 'number' ? v : parseFloat(String(v).replace(/[^0-9.eE+-]/g, '')); return isNaN(n) ? null : n }
|
|
15
|
+
function evalFormula(cells, f, depth = 0) {
|
|
16
|
+
try {
|
|
17
|
+
let expr = f.slice(1).toUpperCase().replace(/\s+/g, '')
|
|
18
|
+
expr = expr.replace(/(SUM|AVERAGE|AVG|COUNT|MIN|MAX)\(([A-Z]+\d+):([A-Z]+\d+)\)/g, (m, fn, a, b) => {
|
|
19
|
+
const A = refRC(a), B = refRC(b), vals = []
|
|
20
|
+
for (let r = Math.min(A.r, B.r); r <= Math.max(A.r, B.r); r++) for (let c = Math.min(A.c, B.c); c <= Math.max(A.c, B.c); c++) { const n = numAt(cells, r, c, depth); if (n != null) vals.push(n) }
|
|
21
|
+
if (fn === 'COUNT') return String(vals.length)
|
|
22
|
+
if (!vals.length) return '0'
|
|
23
|
+
if (fn === 'SUM') return String(vals.reduce((x, y) => x + y, 0))
|
|
24
|
+
if (fn === 'MIN') return String(Math.min(...vals))
|
|
25
|
+
if (fn === 'MAX') return String(Math.max(...vals))
|
|
26
|
+
return String(vals.reduce((x, y) => x + y, 0) / vals.length)
|
|
27
|
+
})
|
|
28
|
+
expr = expr.replace(/[A-Z]+\d+/g, (ref) => { const p = refRC(ref); const n = p ? numAt(cells, p.r, p.c, depth) : null; return String(n == null ? 0 : n) })
|
|
29
|
+
if (!/^[-+*/().\d\seE]*$/.test(expr)) return '#ERROR'
|
|
30
|
+
const out = Function('"use strict";return (' + expr + ')')()
|
|
31
|
+
return typeof out === 'number' && isFinite(out) ? out : '#ERROR'
|
|
32
|
+
} catch (e) { return '#ERROR' }
|
|
33
|
+
}
|
|
34
|
+
function fmt(val, t, currency) { if (val === '' || val == null) return ''; if (typeof val !== 'number') return String(val); const r2 = Math.round(val * 100) / 100; if (t === 'percent') return (Math.round(val * 1000) / 10) + '%'; if (t === 'currency') return currency + r2.toLocaleString(); return r2.toLocaleString() }
|
|
35
|
+
|
|
36
|
+
const nR = Math.max(props.rows || 0, props.data.length, 8)
|
|
37
|
+
const nC = Math.max(props.cols || 0, ...props.data.map(r => r.length), 6)
|
|
38
|
+
const cells = ref(Array.from({ length: nR }, (_, r) => Array.from({ length: nC }, (_, c) => norm(props.data[r] && props.data[r][c]))))
|
|
39
|
+
const a0 = refRC(props.defaultActive) || { r: 0, c: 0 }
|
|
40
|
+
const active = ref(a0), anchor = ref(a0), editing = ref(null), draft = ref(''), sheet = ref(props.sheets[0])
|
|
41
|
+
const cur = computed(() => cells.value[active.value.r][active.value.c])
|
|
42
|
+
const rawOf = (cell) => cell.f != null ? cell.f : (cell.v == null ? '' : String(cell.v))
|
|
43
|
+
const lo = computed(() => ({ r: Math.min(active.value.r, anchor.value.r), c: Math.min(active.value.c, anchor.value.c) }))
|
|
44
|
+
const hi = computed(() => ({ r: Math.max(active.value.r, anchor.value.r), c: Math.max(active.value.c, anchor.value.c) }))
|
|
45
|
+
const multi = computed(() => lo.value.r !== hi.value.r || lo.value.c !== hi.value.c)
|
|
46
|
+
const inRange = (r, c) => r >= lo.value.r && r <= hi.value.r && c >= lo.value.c && c <= hi.value.c
|
|
47
|
+
const stats = computed(() => { if (!multi.value) return null; const vals = []; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) { const v = evalCell(cells.value, cells.value[r][c]); if (typeof v === 'number') vals.push(v) } if (!vals.length) return null; const sum = vals.reduce((x, y) => x + y, 0); return { sum: Math.round(sum * 100) / 100, avg: Math.round(sum / vals.length * 100) / 100, count: vals.length } })
|
|
48
|
+
function pick(r, c, e) { active.value = { r, c }; if (!(e && e.shiftKey)) anchor.value = { r, c }; editing.value = null }
|
|
49
|
+
function commit(r, c, text) { const cell = cells.value[r][c]; delete cell.f; if (String(text).startsWith('=')) { cell.f = text; cell.v = '' } else cell.v = text !== '' && !isNaN(Number(text)) ? Number(text) : text; emit('change', cells.value) }
|
|
50
|
+
function setFlag(k) { const on = !cur.value[k]; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) cells.value[r][c][k] = on }
|
|
51
|
+
function setType(label) { const t = { Automatic: undefined, Number: 'number', Currency: 'currency', Percent: 'percent' }[label]; for (let r = lo.value.r; r <= hi.value.r; r++) for (let c = lo.value.c; c <= hi.value.c; c++) { if (t) cells.value[r][c].t = t; else delete cells.value[r][c].t } }
|
|
52
|
+
const gridCols = computed(() => ['46px', ...Array.from({ length: nC }, (_, c) => (props.colWidths[c] || props.defaultColWidth) + 'px')].join(' '))
|
|
53
|
+
const hdr = { position: 'sticky', top: 0, zIndex: 3, background: 'var(--bg-inset)', borderRight: '1px solid var(--border-control)', borderBottom: '1px solid var(--border-control)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-xs)', color: 'var(--text-secondary)', height: '26px', boxSizing: 'border-box', userSelect: 'none' }
|
|
54
|
+
const hl = { background: 'var(--soft-bg)', color: 'var(--action-primary)' }
|
|
55
|
+
</script>
|
|
56
|
+
<template>
|
|
57
|
+
<div :style="{ border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', ...style }">
|
|
58
|
+
<div v-if="toolbar" style="height:36px;display:flex;align-items:center;gap:2px;padding:0 8px;border-bottom:1px solid var(--border-subtle)">
|
|
59
|
+
<IconButton icon="undo-2" size="sm" muted label="Undo" /><IconButton icon="redo-2" size="sm" muted label="Redo" />
|
|
60
|
+
<span style="width:1px;height:16px;background:var(--border-control);margin:0 6px" />
|
|
61
|
+
<button type="button" @click="setFlag('b')" :style="{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.b ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', fontWeight: 700 }">B</button>
|
|
62
|
+
<button type="button" @click="setFlag('i')" :style="{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.i ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', fontStyle: 'italic' }">I</button>
|
|
63
|
+
<button type="button" @click="setFlag('u')" :style="{ width: '24px', height: '24px', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: cur.u ? 'var(--bg-active)' : 'transparent', color: 'var(--text-body)', textDecoration: 'underline' }">U</button>
|
|
64
|
+
<span style="width:1px;height:16px;background:var(--border-control);margin:0 6px" />
|
|
65
|
+
<KSelect size="sm" :options="['Automatic','Number','Currency','Percent']" :model-value="{ number: 'Number', currency: 'Currency', percent: 'Percent' }[cur.t] || 'Automatic'" @update:modelValue="setType" style="width:110px" />
|
|
66
|
+
</div>
|
|
67
|
+
<div v-if="formulaBar" style="height:32px;display:flex;align-items:center;border-bottom:1px solid var(--border-control)">
|
|
68
|
+
<span style="width:72px;align-self:stretch;display:flex;align-items:center;justify-content:center;border-right:1px solid var(--border-control);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ multi ? colName(lo.c) + (lo.r + 1) + ':' + colName(hi.c) + (hi.r + 1) : colName(active.c) + (active.r + 1) }}</span>
|
|
69
|
+
<span style="padding:0 10px;font-family:var(--font-mono);font-size:var(--text-xs);font-style:italic;color:var(--text-muted);border-right:1px solid var(--border-subtle);align-self:stretch;display:flex;align-items:center">fx</span>
|
|
70
|
+
<input :value="editing === 'fx' ? draft : rawOf(cur)" @focus="editing = 'fx'; draft = rawOf(cur)" @input="draft = $event.target.value" @blur="editing === 'fx' && (commit(active.r, active.c, draft), editing = null)" @keydown.enter="$event.target.blur()" @keydown.esc="editing = null; $event.target.blur()" style="flex:1;border:none;outline:none;background:transparent;padding:0 10px;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-primary)" />
|
|
71
|
+
</div>
|
|
72
|
+
<div :style="{ overflow: 'auto', maxHeight: typeof height === 'number' ? height + 'px' : height }">
|
|
73
|
+
<div :style="{ display: 'grid', gridTemplateColumns: gridCols, width: 'fit-content' }">
|
|
74
|
+
<span :style="{ ...hdr, zIndex: 5, left: 0 }" />
|
|
75
|
+
<span v-for="c in nC" :key="'c' + c" :style="{ ...hdr, ...((c - 1) >= lo.c && (c - 1) <= hi.c ? hl : null) }">{{ colName(c - 1) }}</span>
|
|
76
|
+
<template v-for="(row, r) in cells" :key="r">
|
|
77
|
+
<span :style="{ ...hdr, position: 'sticky', left: 0, top: 'auto', zIndex: 2, height: '28px', ...(r >= lo.r && r <= hi.r ? hl : null) }">{{ r + 1 }}</span>
|
|
78
|
+
<span v-for="(cell, c) in row" :key="r + ',' + c" @mousedown="pick(r, c, $event)" @dblclick="editing = { r, c }; draft = rawOf(cell)"
|
|
79
|
+
:style="{ position: 'relative', height: '28px', boxSizing: 'border-box', borderRight: '1px solid var(--border-subtle)', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', justifyContent: cell.align || (typeof evalCell(cells, cell) === 'number' ? 'flex-end' : 'flex-start'), padding: '0 8px', cursor: 'cell', background: inRange(r, c) && !(active.r === r && active.c === c) ? 'var(--soft-bg)' : 'var(--bg-surface)', boxShadow: active.r === r && active.c === c ? 'inset 0 0 0 2px var(--action-primary)' : 'none', fontFamily: typeof evalCell(cells, cell) === 'number' ? 'var(--font-mono)' : 'var(--font-sans)', fontSize: typeof evalCell(cells, cell) === 'number' ? '11.5px' : 'var(--text-sm)', color: evalCell(cells, cell) === '#ERROR' ? 'var(--danger)' : 'var(--text-primary)', fontWeight: cell.b ? 600 : 'var(--weight-regular)', fontStyle: cell.i ? 'italic' : 'normal', textDecoration: cell.u ? 'underline' : 'none', whiteSpace: 'nowrap', overflow: 'hidden', userSelect: 'none' }">
|
|
80
|
+
<input v-if="editing && editing !== 'fx' && editing.r === r && editing.c === c" autofocus :value="draft" @input="draft = $event.target.value" @blur="commit(r, c, draft); editing = null" @keydown.enter="$event.target.blur()" @keydown.esc="editing = null" style="position:absolute;inset:0;border:none;outline:none;padding:0 8px;font:inherit;background:var(--bg-surface);color:var(--text-primary)" />
|
|
81
|
+
<template v-else>{{ fmt(evalCell(cells, cell), cell.t, currency) }}</template>
|
|
82
|
+
<span v-if="active.r === r && active.c === c && !editing" style="position:absolute;right:-3px;bottom:-3px;width:6px;height:6px;background:var(--action-primary);border:1px solid var(--bg-surface);cursor:crosshair;z-index:2" />
|
|
83
|
+
</span>
|
|
84
|
+
</template>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<div v-if="statusBar" style="height:32px;display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--border-control);background:var(--bg-inset);padding-right:12px">
|
|
88
|
+
<div style="display:flex;align-self:stretch;align-items:stretch">
|
|
89
|
+
<button v-for="s in sheets" :key="s" type="button" @click="sheet = s" :style="{ border: 'none', borderRight: '1px solid var(--border-subtle)', padding: '0 16px', cursor: 'pointer', background: sheet === s ? 'var(--bg-surface)' : 'transparent', color: sheet === s ? 'var(--text-heading)' : 'var(--text-secondary)', fontFamily: 'var(--font-sans)', fontWeight: sheet === s ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--text-sm)', boxShadow: sheet === s ? 'inset 0 2px 0 var(--action-primary)' : 'none' }">{{ s }}</button>
|
|
90
|
+
<span style="display:flex;align-items:center;padding:0 6px"><IconButton icon="plus" size="xs" muted label="Add sheet" /></span>
|
|
91
|
+
</div>
|
|
92
|
+
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-secondary)">{{ stats ? 'Sum ' + fmt(stats.sum, cur.t, currency) + ' Avg ' + fmt(stats.avg, cur.t, currency) + ' Count ' + stats.count : '100%' }}</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!-- @dsCard group="Directory Index" viewport="700x120" name="Data dir" subtitle="Full cards: Components → Data Table / List Row / etc." -->
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html><head><meta charset="utf-8"><link rel="stylesheet" href="../../styles.css">
|
|
4
|
+
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
5
|
+
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
|
6
|
+
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
|
7
|
+
<script src="../../_ds_bundle.js"></script>
|
|
8
|
+
<style>body{margin:0;background:var(--bg-surface);padding:16px;font-family:var(--font-sans)}</style></head>
|
|
9
|
+
<body><div id="root"></div>
|
|
10
|
+
<script type="text/babel">
|
|
11
|
+
const { DataTable } = window.KeybricksDesignSystem_71231a;
|
|
12
|
+
ReactDOM.createRoot(document.getElementById("root")).render(<DataTable dense columns={[{key:"id",label:"ID",mono:true},{key:"entity",label:"Entity",mono:true}]} rows={[{id:"528563",entity:"HK"}]}/>);
|
|
13
|
+
</script></body></html>
|