basalt-ui 1.25.0 → 1.27.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/AGENTS.md +8 -6
- package/CHANGELOG.md +26 -0
- package/MIGRATING.md +815 -23
- package/README.md +45 -32
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +160 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +118 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1465 -36
- package/configs/oxlint-plugin.test.ts +1190 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +39 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +93 -0
- package/dist/controls/filter-context.d.ts +60 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +20 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +46 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/number-filter.d.ts +26 -0
- package/dist/controls/number-filter.d.ts.map +1 -0
- package/dist/controls/number-filter.js +143 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +43 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +135 -0
- package/dist/controls/range-filter.type-guard.test.d.ts +2 -0
- package/dist/controls/range-filter.type-guard.test.d.ts.map +1 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/index.d.ts +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +3 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +95 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +108 -49
- package/dist/dashboard/stat-card.module.css +153 -0
- package/dist/dashboard/stat-card.module.css.d.ts +15 -0
- package/dist/data/data-table.d.ts +37 -15
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +96 -58
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +78 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +84 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +271 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +565 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +405 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +60 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +123 -0
- package/dist/state/persisted.test.d.ts +2 -0
- package/dist/state/persisted.test.d.ts.map +1 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +156 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +67 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +89 -0
- package/dist/widget-header/widget-header.module.css +350 -0
- package/dist/widget-header/widget-header.module.css.d.ts +25 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +1053 -0
- package/src/controls/enum-filter.tsx +170 -0
- package/src/controls/filter-context.tsx +158 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +49 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/number-filter.tsx +291 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +137 -0
- package/src/controls/range-filter.tsx +237 -0
- package/src/controls/range-filter.type-guard.test.tsx +122 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/index.ts +6 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +153 -0
- package/src/dashboard/stat-card.module.css.d.ts +15 -0
- package/src/dashboard/stat-card.test.tsx +298 -1
- package/src/dashboard/stat-card.tsx +289 -67
- package/src/data/data-table.test.tsx +96 -11
- package/src/data/data-table.tsx +228 -84
- package/src/guard/check-source.test.ts +159 -13
- package/src/guard/grace.test.ts +158 -0
- package/src/guard/index.ts +185 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +51 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +35 -0
- package/src/router-tanstack/index.ts +28 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +882 -0
- package/src/router-tanstack/search-store.test.ts +578 -0
- package/src/router-tanstack/search-store.ts +546 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +512 -0
- package/src/state/fields.ts +1150 -0
- package/src/state/persisted.test.ts +91 -0
- package/src/state/persisted.ts +261 -0
- package/src/state.ts +52 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +204 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +350 -0
- package/src/widget-header/widget-header.module.css.d.ts +25 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +213 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -329
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WidgetHeader — the one heading primitive behind every section/card/table title
|
|
3
|
+
* (docs/CONTROLS-SPEC.md §2.2). Plain elements only — no `@mantine/*` anywhere in this folder —
|
|
4
|
+
* so `ChartCard` (inside the `charts/` boundary) can compose it without tripping
|
|
5
|
+
* `basalt/token-layer-boundary` / `check-dist-layering.mjs`.
|
|
6
|
+
*
|
|
7
|
+
* `--vx-space-section-header-height` / `--vx-space-widget-header-height` / `--vx-space-control-gap`
|
|
8
|
+
* are density-tracking anchors landing alongside this wave (docs/CONTROLS-SPEC.md §5) — referenced
|
|
9
|
+
* by name with rem fallbacks at their level-0 px value (36/28/6), same idiom as ChartCard's
|
|
10
|
+
* `--mantine-spacing-*` fallbacks.
|
|
11
|
+
*
|
|
12
|
+
* Layout: the title row holds title/icon/info/count/actions only. `value` + `delta` render on
|
|
13
|
+
* their own hero-metric row directly beneath it — for BOTH tiers, never inline with the title.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
.root {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.titleRow {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: var(--vx-space-control-gap, 0.375rem);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Below `sm` the title row WRAPS, which is the safety net for an `actions` slot too wide to sit
|
|
29
|
+
* beside the title. `flex-wrap` is the right tool precisely because it is conditional: a 30px kebab
|
|
30
|
+
* fits and stays on the row, while a control whose hypothetical size does not fit takes its own line
|
|
31
|
+
* instead of squeezing the heading to an ellipsis. No breakpoint arithmetic, no max-width fraction,
|
|
32
|
+
* and no prop — the row measures the actual content.
|
|
33
|
+
*
|
|
34
|
+
* `row-gap` is the 8px rhythm step, so a wrapped slot reads as the line under the title rather than
|
|
35
|
+
* as a second header. `.actions` keeps its `margin-inline-start: auto`, which on a wrapped line makes
|
|
36
|
+
* it right-aligned — the same edge it holds when it is on the row.
|
|
37
|
+
*/
|
|
38
|
+
@media (max-width: 47.99375em) {
|
|
39
|
+
.titleRow {
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
row-gap: var(--vx-space-stack-sm);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.root[data-tier='section'] .titleRow {
|
|
46
|
+
min-height: var(--vx-space-section-header-height, 2.25rem);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.root[data-tier='widget'] .titleRow {
|
|
50
|
+
min-height: var(--vx-space-widget-header-height, 1.75rem);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* Section → h2, widget → h3, and the two tiers now read DIFFERENTLY on purpose — they did not, and
|
|
55
|
+
* that is what flattened the page.
|
|
56
|
+
*
|
|
57
|
+
* A `section` heading names a region of the page: 15px, weight 600, INK. A `widget` heading labels a
|
|
58
|
+
* number that is itself the thing being read (a KPI card's hero value, a chart card's total), so it
|
|
59
|
+
* is the quieter of the pair — one step down and MUTED, which is what lets the 24px mono value under
|
|
60
|
+
* it carry the card. Both keep the shared head-font 88% stretch.
|
|
61
|
+
*
|
|
62
|
+
* Rendered identically (15px ink both) the hero value competed with its own label, four cards in a
|
|
63
|
+
* row read as four headings, and the section that grouped them read as one more of the same.
|
|
64
|
+
*/
|
|
65
|
+
.heading {
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 5px;
|
|
69
|
+
margin: 0;
|
|
70
|
+
min-width: 0;
|
|
71
|
+
font-family: var(--basalt-font-head);
|
|
72
|
+
font-stretch: 88%;
|
|
73
|
+
font-weight: 550;
|
|
74
|
+
font-size: var(--vx-text-md);
|
|
75
|
+
color: var(--vx-ink);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.root[data-tier='section'] .heading {
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.root[data-tier='widget'] .heading {
|
|
83
|
+
font-size: var(--vx-text-sm);
|
|
84
|
+
color: var(--vx-muted);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.titleText {
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
text-overflow: ellipsis;
|
|
90
|
+
white-space: nowrap;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* The icon slot PINS its glyph's box. A consumer's icon arrives at whatever size its own component
|
|
95
|
+
* chose (the playground's is 18px), and an 18px mark beside a 13.5px title is a bullet, not a label
|
|
96
|
+
* icon — so the tier states the size here rather than asking every call site to pass one.
|
|
97
|
+
*/
|
|
98
|
+
/*
|
|
99
|
+
* The heading's icon box carries COLOUR ONLY — geometry belongs to `theme/icon-slot.module.css`,
|
|
100
|
+
* which this class is composed onto (`IconSlot className={classes.icon}`). The per-tier SIZE is set
|
|
101
|
+
* as the slot's own box var below rather than as a `> svg` rule, so it survives whatever the caller's
|
|
102
|
+
* glyph declares for itself: a `width`/`height` attribute is a presentation attribute and loses to
|
|
103
|
+
* the slot's CSS, while a `> svg` rule scoped to one component only ever fixed that component.
|
|
104
|
+
*/
|
|
105
|
+
.icon {
|
|
106
|
+
color: var(--vx-faint);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.root[data-tier='widget'] .icon {
|
|
110
|
+
--vx-space-icon-size: 0.875rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.root[data-tier='section'] .icon {
|
|
114
|
+
--vx-space-icon-size: 1rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Mono count tag after the title (C11 — every table/list states its count in its header). Its box is
|
|
118
|
+
* the `tag` tier (20px), so it never grows the header row it sits in. */
|
|
119
|
+
.count {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
align-items: center;
|
|
123
|
+
/* Exactly the `tag` tier (20px), not a floor: the mono `VX.text.xs` line box plus
|
|
124
|
+
* `badgeInset`-y is 23px on its own, so a `min-height` changed nothing and the tag rendered 3px
|
|
125
|
+
* taller than the tier it is named for — enough to grow a 36px section-header row. `line-height: 1`
|
|
126
|
+
* is what lets the box be the stated height instead of the text's. */
|
|
127
|
+
/* `--mantine-scale`-aware, same reconstruction every other ctl/tag-tier box uses (`theme/index.ts`'s
|
|
128
|
+
* `ctlSizeVars`) — read raw, this box stayed fixed while the Badge/Button beside it in the same
|
|
129
|
+
* header scaled with `theme.scale`. */
|
|
130
|
+
height: calc(var(--vx-space-control-height-tag, 1.25rem) * var(--mantine-scale, 1));
|
|
131
|
+
line-height: 1;
|
|
132
|
+
border-radius: var(--vx-radius-ctrl);
|
|
133
|
+
background: var(--vx-surface-subtle);
|
|
134
|
+
padding: var(--vx-space-badge-inset-y) var(--vx-space-badge-inset-x);
|
|
135
|
+
font-family: var(--basalt-font-mono);
|
|
136
|
+
font-size: var(--vx-text-xs);
|
|
137
|
+
font-weight: 500;
|
|
138
|
+
color: var(--vx-faint);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/*
|
|
142
|
+
* The hero-metric row — value + delta, directly under the title row, for both tiers. The delta rides
|
|
143
|
+
* 4px off the value, not 8: it is a suffix ON the number (`$168,920 +47%`), and at 8px it read as a
|
|
144
|
+
* second, unrelated chip in the row. `baseline` alignment is what keeps the mono numeral and the
|
|
145
|
+
* badge's own smaller mono text sitting on one line.
|
|
146
|
+
*/
|
|
147
|
+
/*
|
|
148
|
+
* The hero-metric row: the value, then its `DeltaBadge`.
|
|
149
|
+
*
|
|
150
|
+
* The gap is the 8px vertical-rhythm step (`--vx-space-stack-sm`), not the 4px it shipped as. 4px is
|
|
151
|
+
* the rhythm's SMALLEST step — the distance between a label and the thing it labels — and at that
|
|
152
|
+
* distance the badge read as a superscript hanging off the numeral rather than as a second fact
|
|
153
|
+
* beside it. It is a token and not a literal so it tracks the density knob with every other spacing
|
|
154
|
+
* in the header (`deriveSpacing`, `tokens/palette.ts`); `theme/layout-rhythm-css.test.ts`'s sibling
|
|
155
|
+
* assertion in `widget-header.test.tsx` pins it against exactly the literal it used to be.
|
|
156
|
+
*
|
|
157
|
+
* `align-items: center`, not `baseline`. The badge is a BOX with its own padding and a mono 11.5px
|
|
158
|
+
* label, so aligning its text baseline to a 24px KPI numeral's baseline put the box itself low —
|
|
159
|
+
* its top edge sat below the numeral's x-height and its bottom edge hung under the numeral's
|
|
160
|
+
* baseline. A chip beside a numeral is centred on the numeral, the same law the `count` tag in the
|
|
161
|
+
* title row above already follows.
|
|
162
|
+
*/
|
|
163
|
+
.metrics {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-wrap: nowrap;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: var(--vx-space-stack-sm);
|
|
168
|
+
margin-top: 2px;
|
|
169
|
+
min-width: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* Below `sm` the metric row WRAPS, so the badge drops under the value instead of ellipsizing it.
|
|
174
|
+
* `nowrap` plus the `.value` clamp below is right on desktop — a `+18.9%` cannot ellipsize and read,
|
|
175
|
+
* a numeral can — but a KPI's numeral IS the content, and in a 2-up card grid at 390px
|
|
176
|
+
* `$1294.9k ▲18.9%` rendered as `$129… ▲18.9%`. Truncating the number to protect the delta about the
|
|
177
|
+
* number is the wrong way round. Conditional like the title row above it: a short value keeps the
|
|
178
|
+
* badge beside it, and only a value that would otherwise clip gives up the line.
|
|
179
|
+
*/
|
|
180
|
+
@media (max-width: 47.99375em) {
|
|
181
|
+
.metrics {
|
|
182
|
+
flex-wrap: wrap;
|
|
183
|
+
row-gap: 2px;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* The value is the row's shrinkable half and the delta badge is not: a pre-formatted numeral can
|
|
188
|
+
* ellipsize and still read, a `+18.9%` cannot. Without the clamp the badge grew past the header box
|
|
189
|
+
* and painted over `StatCard`'s 72px sparkline slot beside it. */
|
|
190
|
+
.value {
|
|
191
|
+
min-width: 0;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
text-overflow: ellipsis;
|
|
194
|
+
white-space: nowrap;
|
|
195
|
+
font-family: var(--basalt-font-mono);
|
|
196
|
+
font-size: var(--vx-text-kpi);
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
letter-spacing: -0.02em;
|
|
199
|
+
color: var(--vx-ink);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/*
|
|
203
|
+
* The unit beside the hero value. Mono like the number it qualifies (so `412 TSS` sits on one
|
|
204
|
+
* baseline grid), `--vx-text-sm` and `--vx-muted` so it never competes with the numeral — the whole
|
|
205
|
+
* point is that a unit is the SMALL half of the pair. `flex-shrink: 0`: the value is the row's
|
|
206
|
+
* shrinkable half (see `.value`), and a unit that ellipsized to `TS…` would state nothing.
|
|
207
|
+
*/
|
|
208
|
+
.unit {
|
|
209
|
+
flex-shrink: 0;
|
|
210
|
+
align-self: baseline;
|
|
211
|
+
font-family: var(--basalt-font-mono);
|
|
212
|
+
font-size: var(--vx-text-sm);
|
|
213
|
+
font-weight: 500;
|
|
214
|
+
color: var(--vx-muted);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* The C1/C5 home for header actions — always pinned right, regardless of what else is present. */
|
|
218
|
+
.actions {
|
|
219
|
+
display: inline-flex;
|
|
220
|
+
flex-shrink: 0;
|
|
221
|
+
align-items: center;
|
|
222
|
+
gap: var(--vx-space-control-gap, 0.375rem);
|
|
223
|
+
margin-inline-start: auto;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* Directly under the title row, 4px off it — 1px put it inside the title's own line box, so a
|
|
227
|
+
* section with both read as a two-line heading rather than a heading plus a note. */
|
|
228
|
+
.subtitle {
|
|
229
|
+
margin-top: 4px;
|
|
230
|
+
font-size: var(--vx-text-sm);
|
|
231
|
+
font-weight: 400;
|
|
232
|
+
color: var(--vx-muted);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* `data-placement` is read by wave-3 composers wanting a `bleed` layout (docs/CONTROLS-SPEC.md
|
|
236
|
+
* §2.2's `sparklinePlacement` composer mapping) — this primitive only ever sets `right`. */
|
|
237
|
+
.sparkline {
|
|
238
|
+
margin-top: 4px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ── info glyph ─────────────────────────────────────────────────────────────────────────────── */
|
|
242
|
+
|
|
243
|
+
.info {
|
|
244
|
+
position: relative;
|
|
245
|
+
display: inline-flex;
|
|
246
|
+
flex-shrink: 0;
|
|
247
|
+
align-items: center;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.infoTrigger {
|
|
251
|
+
display: inline-flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
appearance: none;
|
|
254
|
+
margin: 0;
|
|
255
|
+
padding: 0;
|
|
256
|
+
border: none;
|
|
257
|
+
background: none;
|
|
258
|
+
color: var(--vx-faint);
|
|
259
|
+
cursor: help;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.infoTrigger:hover,
|
|
263
|
+
.infoTrigger:focus-visible,
|
|
264
|
+
.infoTrigger[data-open='true'] {
|
|
265
|
+
color: var(--vx-muted);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
* The tooltip bubble reads like every other floating surface — panel bg + `shadow-card` + card
|
|
270
|
+
* radius (docs/DESIGN-SPEC.md §5). It opens DOWNWARD by convention, into whatever sits below the
|
|
271
|
+
* header. `pointer-events: none` so it can never swallow the pointer on its way back to the
|
|
272
|
+
* trigger's `mouseleave`. Mantine-free by design (this whole folder is), so it is a plain
|
|
273
|
+
* state-driven bubble rather than a Mantine Tooltip — which is also what makes it composable from
|
|
274
|
+
* inside the `charts/` boundary.
|
|
275
|
+
*/
|
|
276
|
+
.infoBubble {
|
|
277
|
+
position: absolute;
|
|
278
|
+
top: calc(100% + 7px);
|
|
279
|
+
left: 0;
|
|
280
|
+
z-index: 10;
|
|
281
|
+
width: max-content;
|
|
282
|
+
max-width: 260px;
|
|
283
|
+
padding: 7px 10px;
|
|
284
|
+
border-radius: var(--vx-radius-card);
|
|
285
|
+
background-color: var(--vx-surface-panel);
|
|
286
|
+
box-shadow: var(--vx-shadow-card);
|
|
287
|
+
color: var(--vx-muted);
|
|
288
|
+
font-size: var(--vx-text-sm);
|
|
289
|
+
font-weight: 400;
|
|
290
|
+
line-height: 1.4;
|
|
291
|
+
white-space: normal;
|
|
292
|
+
pointer-events: none;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* ── DeltaBadge ─────────────────────────────────────────────────────────────────────────────── */
|
|
296
|
+
|
|
297
|
+
.deltaBadge {
|
|
298
|
+
display: inline-flex;
|
|
299
|
+
flex: none;
|
|
300
|
+
white-space: nowrap;
|
|
301
|
+
align-items: center;
|
|
302
|
+
gap: 3px;
|
|
303
|
+
border-radius: var(--vx-radius-ctrl);
|
|
304
|
+
padding: var(--vx-space-badge-inset-y) var(--vx-space-badge-inset-x);
|
|
305
|
+
font-family: var(--basalt-font-mono);
|
|
306
|
+
font-size: var(--vx-text-xs);
|
|
307
|
+
font-weight: 600;
|
|
308
|
+
line-height: 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.deltaGlyph {
|
|
312
|
+
font-size: 0.8em;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.deltaPeriod {
|
|
316
|
+
margin-inline-start: 4px;
|
|
317
|
+
font-weight: 500;
|
|
318
|
+
opacity: 0.6;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/*
|
|
322
|
+
* Law C15 — the `actions` slot is the tier-2/tier-3 home, and its controls are 30px (`ctl`) or 24px
|
|
323
|
+
* (`icon`). Below `sm` an `::after` overlay lifts the HIT area to
|
|
324
|
+
* `--vx-space-touch-control-height` (36px at level 0) without moving the visible box, the same
|
|
325
|
+
* overlay `shell/page-bar.module.css` draws for the bar. `overflow: visible` is load-bearing:
|
|
326
|
+
* Mantine's Button/ActionIcon roots are `overflow: hidden`, which clips an overlay that by
|
|
327
|
+
* definition extends past the box — the clipping only exists for their loading overlay, painted
|
|
328
|
+
* under `[data-loading]`, which is excluded here.
|
|
329
|
+
*/
|
|
330
|
+
@media (max-width: 47.99375em) {
|
|
331
|
+
.actions :is(button, a) {
|
|
332
|
+
position: relative;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.actions :is(button, a):not([data-loading]) {
|
|
336
|
+
overflow: visible;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.actions :is(button, a)::after {
|
|
340
|
+
content: '';
|
|
341
|
+
position: absolute;
|
|
342
|
+
top: 50%;
|
|
343
|
+
left: 50%;
|
|
344
|
+
translate: -50% -50%;
|
|
345
|
+
width: 100%;
|
|
346
|
+
height: 100%;
|
|
347
|
+
min-width: var(--vx-space-touch-control-height, 2.25rem);
|
|
348
|
+
min-height: var(--vx-space-touch-control-height, 2.25rem);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed CSS-module declaration (the S4 CSS-modules path). Named string props — not an index
|
|
3
|
+
* signature — so dot access stays verbatim under the package's strict flags.
|
|
4
|
+
*/
|
|
5
|
+
declare const classes: {
|
|
6
|
+
readonly root: string
|
|
7
|
+
readonly titleRow: string
|
|
8
|
+
readonly heading: string
|
|
9
|
+
readonly titleText: string
|
|
10
|
+
readonly icon: string
|
|
11
|
+
readonly count: string
|
|
12
|
+
readonly metrics: string
|
|
13
|
+
readonly value: string
|
|
14
|
+
readonly unit: string
|
|
15
|
+
readonly actions: string
|
|
16
|
+
readonly subtitle: string
|
|
17
|
+
readonly sparkline: string
|
|
18
|
+
readonly info: string
|
|
19
|
+
readonly infoTrigger: string
|
|
20
|
+
readonly infoBubble: string
|
|
21
|
+
readonly deltaBadge: string
|
|
22
|
+
readonly deltaGlyph: string
|
|
23
|
+
readonly deltaPeriod: string
|
|
24
|
+
}
|
|
25
|
+
export default classes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget-header.test.d.ts","sourceRoot":"","sources":["../../src/widget-header/widget-header.test.tsx"],"names":[],"mappings":""}
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# basalt-ui v1.
|
|
1
|
+
# basalt-ui v1.27.0
|
|
2
2
|
# Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
|
|
3
3
|
#
|
|
4
4
|
# basalt-ui is an opinionated framework for Mantine-based React apps.
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
# ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
|
|
20
20
|
|
|
21
21
|
Package: basalt-ui
|
|
22
|
-
Version: 1.
|
|
22
|
+
Version: 1.27.0
|
|
23
23
|
|
|
24
24
|
## basalt-ui
|
|
25
25
|
Import: import { ... } from 'basalt-ui'
|
|
26
|
-
Description: BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone)
|
|
26
|
+
Description: BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone)
|
|
27
27
|
Layer: mantine-coupled
|
|
28
28
|
Rule: basalt-mantine
|
|
29
29
|
OptionalPeers: react-markdown@^10.1.0, remark-gfm@^4.0.1, rehype-sanitize@^6.0.0, remend@1.3.0, shiki@^4.3.0, beautiful-mermaid@^1.1.0
|
|
@@ -60,82 +60,95 @@ Layer: headless
|
|
|
60
60
|
Import: import { ... } from 'basalt-ui/query'
|
|
61
61
|
Description: createBasaltQueryClient, transport-agnostic unwrap, lazy BasaltQueryDevtools
|
|
62
62
|
Layer: headless
|
|
63
|
-
Rule: basalt-
|
|
63
|
+
Rule: basalt-batteries
|
|
64
64
|
OptionalPeers: @tanstack/react-query-devtools@^5.101.0
|
|
65
65
|
|
|
66
66
|
## basalt-ui/router-tanstack
|
|
67
67
|
Import: import { ... } from 'basalt-ui/router-tanstack'
|
|
68
|
-
Description: TanStack Router bridge: defineNav/navGroup/navTarget (one typed nav definition) + useNav (sections + mobileNav, spread onto BasaltShell) + useBasaltNav (active route) + useRouterBreadcrumbs +
|
|
68
|
+
Description: TanStack Router bridge: defineNav/navGroup/navTarget (one typed nav definition) + useNav (sections + mobileNav, spread onto BasaltShell) + useBasaltNav (active route) + useRouterBreadcrumbs + createSearchStore (typed URL > localStorage > fallback store over field.enum/multi/range/number/boolean/string; deprecated createSearchParamStore/createMultiSearchParamStore wrappers until 1.29.0)
|
|
69
69
|
Layer: headless
|
|
70
|
-
Rule: basalt-
|
|
70
|
+
Rule: basalt-state
|
|
71
71
|
OptionalPeers: @tanstack/react-router@^1.170.0
|
|
72
72
|
|
|
73
73
|
## basalt-ui/forms
|
|
74
74
|
Import: import { ... } from 'basalt-ui/forms'
|
|
75
75
|
Description: Mantine form adapter: useBasaltForm, field, FormErrorSummary, useFormDraft (Standard Schema)
|
|
76
76
|
Layer: mantine-coupled
|
|
77
|
-
Rule: basalt-
|
|
77
|
+
Rule: basalt-batteries
|
|
78
78
|
OptionalPeers: @mantine/form@^9.3.0
|
|
79
79
|
|
|
80
80
|
## basalt-ui/notifications
|
|
81
81
|
Import: import { ... } from 'basalt-ui/notifications'
|
|
82
82
|
Description: Mantine notifications: notify helpers, typed registry, persisted history, NotificationBell, NotificationCenter
|
|
83
83
|
Layer: mantine-coupled
|
|
84
|
-
Rule: basalt-
|
|
84
|
+
Rule: basalt-batteries
|
|
85
85
|
OptionalPeers: @mantine/notifications@^9.3.0
|
|
86
86
|
|
|
87
87
|
## basalt-ui/commands
|
|
88
88
|
Import: import { ... } from 'basalt-ui/commands'
|
|
89
89
|
Description: typed command bus + overlay controller, toSpotlightActions, ShortcutsHelp, BasaltOverlays
|
|
90
90
|
Layer: mantine-coupled
|
|
91
|
-
Rule: basalt-
|
|
91
|
+
Rule: basalt-batteries
|
|
92
92
|
OptionalPeers: @mantine/spotlight@^9.3.0, @mantine/modals@^9.3.0, @tanstack/react-hotkeys@0.10.0
|
|
93
93
|
|
|
94
94
|
## basalt-ui/data
|
|
95
95
|
Import: import { ... } from 'basalt-ui/data'
|
|
96
96
|
Description: Convenience barrel pulling both TanStack Table + Virtual peer groups: BasaltDataTable, BasaltVirtualList (Mantine-rendered) — prefer ./data/table or ./data/virtual for per-feature opt-in
|
|
97
97
|
Layer: mantine-coupled
|
|
98
|
-
Rule: basalt-
|
|
98
|
+
Rule: basalt-batteries
|
|
99
99
|
OptionalPeers: @tanstack/react-table@>=8 <9, @tanstack/react-virtual@>=3.13.26 <4
|
|
100
100
|
|
|
101
101
|
## basalt-ui/data/table
|
|
102
102
|
Import: import { ... } from 'basalt-ui/data/table'
|
|
103
103
|
Description: BasaltDataTable: a sortable data table over TanStack Table, rendered with Mantine (Mantine-rendered)
|
|
104
104
|
Layer: mantine-coupled
|
|
105
|
-
Rule: basalt-
|
|
105
|
+
Rule: basalt-batteries
|
|
106
106
|
OptionalPeers: @tanstack/react-table@>=8 <9
|
|
107
107
|
|
|
108
108
|
## basalt-ui/data/virtual
|
|
109
109
|
Import: import { ... } from 'basalt-ui/data/virtual'
|
|
110
110
|
Description: BasaltVirtualList: a windowed virtual list over TanStack Virtual, rendered with Mantine (Mantine-rendered)
|
|
111
111
|
Layer: mantine-coupled
|
|
112
|
-
Rule: basalt-
|
|
112
|
+
Rule: basalt-batteries
|
|
113
113
|
OptionalPeers: @tanstack/react-virtual@>=3.13.26 <4
|
|
114
114
|
|
|
115
115
|
## basalt-ui/agent
|
|
116
116
|
Import: import { ... } from 'basalt-ui/agent'
|
|
117
117
|
Description: Headless streaming-chat layer: useAgentStream, aiSdkTransport (recommended default) + edenTransport, isResumable/ResumableAgentTransport (stream-resumption seam), PartList, coalesceParts, the ForeignPart/definePartRenderers/narrowAgentPart open part-registry seam, plus the multi-thread createThreadsStore + useAgentThreadRuns + outcome-resolver seam (Mantine-free)
|
|
118
118
|
Layer: headless
|
|
119
|
-
Rule: basalt-
|
|
119
|
+
Rule: basalt-batteries
|
|
120
120
|
OptionalPeers: ai@^7.0.15, use-stick-to-bottom@^1.1.6
|
|
121
121
|
|
|
122
122
|
## basalt-ui/agent-chat
|
|
123
123
|
Import: import { ... } from 'basalt-ui/agent-chat'
|
|
124
124
|
Description: Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed (variant/renderRow), ThreadFeedRow (inline-expanding Slack row, lazily mounted + kept mounted), ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript (open part-renderer registry via its renderers/fallbackRenderer props, per-message MessageAffordances, groupConsecutive, and an optional virtualize/height windowing mode whose VirtualizeOptions carry overscan/estimateSize/initialScroll — a virtualized transcript opens scrolled to the newest message unless initialScroll is "start"), threadPartRenderers, ToolChip (Mantine-coupled). motion is required, not optional — ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without it installed even though peerDependenciesMeta marks it optional (npm has no per-subpath optionality). remend is genuinely optional here — ThreadTranscript reaches it only through the lazy dynamic import() inside content/markdown.tsx, and @tanstack/react-virtual the same way through the lazy import() behind ThreadTranscript virtualize (absent peer degrades to an unwindowed, height-bound pane).
|
|
125
125
|
Layer: mantine-coupled
|
|
126
|
-
Rule: basalt-
|
|
126
|
+
Rule: basalt-batteries
|
|
127
127
|
OptionalPeers: ai@^7.0.15, motion@12.42.0, remend@1.3.0, use-stick-to-bottom@^1.1.6, react-markdown@^10.1.0, remark-gfm@^4.0.1, shiki@^4.3.0, @shikijs/langs@^4.3.1, @shikijs/themes@^4.3.1, beautiful-mermaid@^1.1.0, @tanstack/react-virtual@>=3.13.26 <4
|
|
128
128
|
|
|
129
129
|
## basalt-ui/content
|
|
130
130
|
Import: import { ... } from 'basalt-ui/content'
|
|
131
|
-
Description: Prose (article/chat typography), CodeBlock (shiki, optional peer), Callout, TableOfContents, ReadingProgress, Markdown (react-markdown + remark-gfm, optional peers; `streaming` is a rendering mode ONLY — `contentTrust` is the independent security input, and any surface rendering agent/model output must pin `contentTrust="untrusted"`, the sole input to the image-origin allowlist; a `fenceRenderers` registry — settledOnly/FenceRenderer/FenceRenderers/FenceRenderContext; `sanitizeSchema`, an additions-only SanitizeSchemaExtension merged over BASALT_SANITIZE_SCHEMA via mergeSanitizeSchema; the remend streaming-repair pass is now a lazy optional peer), MermaidDiagram (beautiful-mermaid, optional peer), mdxComponents/createMdxComponents, ArticleLayout (docs-page frame), ArticleCard/ArticleGrid (overview cards), Article model (sortArticles/filterArticles/formatArticleDate),
|
|
131
|
+
Description: Prose (article/chat typography), CodeBlock (shiki, optional peer), Callout, TableOfContents, ReadingProgress, Markdown (react-markdown + remark-gfm, optional peers; `streaming` is a rendering mode ONLY — `contentTrust` is the independent security input, and any surface rendering agent/model output must pin `contentTrust="untrusted"`, the sole input to the image-origin allowlist; a `fenceRenderers` registry — settledOnly/FenceRenderer/FenceRenderers/FenceRenderContext; `sanitizeSchema`, an additions-only SanitizeSchemaExtension merged over BASALT_SANITIZE_SCHEMA via mergeSanitizeSchema; the remend streaming-repair pass is now a lazy optional peer), MermaidDiagram (beautiful-mermaid, optional peer), mdxComponents/createMdxComponents, ArticleLayout (docs-page frame), ArticleCard/ArticleGrid (overview cards), Article model (sortArticles/filterArticles/formatArticleDate), FilterSet/ViewTabs/MultiSelectFilter re-exported from ./controls (they replaced the controlled ArticleFilterBar), toArticleActions (Spotlight projector, @mantine/spotlight type-only), GuideLink/GuideDrawer (contextual-help drawer) — the content/prose surface
|
|
132
132
|
Layer: mantine-coupled
|
|
133
|
-
Rule: basalt-
|
|
133
|
+
Rule: basalt-batteries
|
|
134
134
|
OptionalPeers: shiki@^4.3.0, @shikijs/langs@^4.3.1, @shikijs/themes@^4.3.1, beautiful-mermaid@^1.1.0, react-markdown@^10.1.0, remark-gfm@^4.0.1, rehype-sanitize@^6.0.0, remend@1.3.0, @mantine/spotlight@^9.3.0
|
|
135
135
|
|
|
136
|
+
## basalt-ui/controls
|
|
137
|
+
Import: import { ... } from 'basalt-ui/controls'
|
|
138
|
+
Description: The control tier (docs/CONTROLS-SPEC.md §3): FilterSet (nowrap row + measured +N fold + the mobile Filters (n) sheet), RangeFilter/CompareFilter/SelectFilter/MultiSelectFilter/NumberFilter/SearchFilter/ToggleFilter (each bound to a FieldHandle — no value/onChange/size, law C2/C5; NumberFilter is the field.number lane, a radio list over `options` or a stepper without), ViewTabs, and the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction). Every control owns its own desktop/mobile swap in CSS (C9) and renders size="ctl" internally. Resolves and renders with NO @mantine/dates installed — the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.
|
|
139
|
+
Layer: mantine-coupled
|
|
140
|
+
Rule: basalt-controls
|
|
141
|
+
|
|
142
|
+
## basalt-ui/controls-dates
|
|
143
|
+
Import: import { ... } from 'basalt-ui/controls-dates'
|
|
144
|
+
Description: DateRangePicker — the @mantine/dates implementation of RangeFilter's customPicker seam. Its own subpath because @mantine/dates is an optional peer and basaltViteConfig pre-bundles the whole @mantine scope, so a consumer without the peer (linewatch) must never resolve it: nothing under src/controls may import it, statically or lazily (docs/CONTROLS-SPEC.md §3).
|
|
145
|
+
Layer: mantine-coupled
|
|
146
|
+
Rule: basalt-controls
|
|
147
|
+
OptionalPeers: @mantine/dates@^9.3.0
|
|
148
|
+
|
|
136
149
|
## basalt-ui/state
|
|
137
150
|
Import: import { ... } from 'basalt-ui/state'
|
|
138
|
-
Description: createPersistedState (versioned localStorage) +
|
|
151
|
+
Description: createPersistedState (versioned localStorage) + the store field vocabulary (field.enum/multi/range/number/boolean/string, FieldHandle, lanes) + createLocalStore, the router-free store — Mantine-free state primitives
|
|
139
152
|
Layer: headless
|
|
140
153
|
Rule: basalt-state
|
|
141
154
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basalt-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"charts",
|
|
@@ -113,6 +113,14 @@
|
|
|
113
113
|
"types": "./dist/content/index.d.ts",
|
|
114
114
|
"import": "./dist/content/index.js"
|
|
115
115
|
},
|
|
116
|
+
"./controls": {
|
|
117
|
+
"types": "./dist/controls/index.d.ts",
|
|
118
|
+
"import": "./dist/controls/index.js"
|
|
119
|
+
},
|
|
120
|
+
"./controls-dates": {
|
|
121
|
+
"types": "./dist/controls-dates/index.d.ts",
|
|
122
|
+
"import": "./dist/controls-dates/index.js"
|
|
123
|
+
},
|
|
116
124
|
"./state": {
|
|
117
125
|
"types": "./dist/state.d.ts",
|
|
118
126
|
"import": "./dist/state.js"
|
|
@@ -143,6 +151,7 @@
|
|
|
143
151
|
"@fontsource-variable/hubot-sans": "5.2.8",
|
|
144
152
|
"@fontsource-variable/jetbrains-mono": "5.2.8",
|
|
145
153
|
"@fontsource-variable/nunito-sans": "5.2.7",
|
|
154
|
+
"@mantine/dates": "9.3.0",
|
|
146
155
|
"@shikijs/langs": "^4.3.1",
|
|
147
156
|
"@shikijs/themes": "^4.3.1",
|
|
148
157
|
"@tanstack/react-hotkeys": "0.10.0",
|
|
@@ -179,6 +188,7 @@
|
|
|
179
188
|
"@fontsource-variable/jetbrains-mono": "5.2.8",
|
|
180
189
|
"@fontsource-variable/nunito-sans": "5.2.7",
|
|
181
190
|
"@mantine/core": "^9.3.0",
|
|
191
|
+
"@mantine/dates": "^9.3.0",
|
|
182
192
|
"@mantine/form": "^9.3.0",
|
|
183
193
|
"@mantine/hooks": "^9.3.0",
|
|
184
194
|
"@mantine/modals": "^9.3.0",
|
|
@@ -228,6 +238,9 @@
|
|
|
228
238
|
"@mantine/core": {
|
|
229
239
|
"optional": true
|
|
230
240
|
},
|
|
241
|
+
"@mantine/dates": {
|
|
242
|
+
"optional": true
|
|
243
|
+
},
|
|
231
244
|
"@mantine/form": {
|
|
232
245
|
"optional": true
|
|
233
246
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { buildDomainIndex, parseKey, resolveCursorPoint } from './resolve'
|
|
2
|
+
import { buildDomainIndex, classifyDomain, parseKey, resolveCursorPoint } from './resolve'
|
|
3
3
|
|
|
4
4
|
type Point = { date: string }
|
|
5
5
|
|
|
@@ -121,3 +121,31 @@ describe("resolveCursorPoint — 'leading' resolution (bucket-keyed domains)", (
|
|
|
121
121
|
expect(resolveCursorPoint(i, '2026-08-05', 'leading')?.date).toBe('2026-08-01')
|
|
122
122
|
})
|
|
123
123
|
})
|
|
124
|
+
|
|
125
|
+
describe('classifyDomain', () => {
|
|
126
|
+
test('an ISO date domain classifies as time', () => {
|
|
127
|
+
expect(classifyDomain(['2026-08-01', '2026-08-02', '2026-08-03'])).toBe('time')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("a bare-year domain classifies as linear, matching parseKey's numeric-first precedence", () => {
|
|
131
|
+
// "2026" passes the numeric test before it ever reaches the date fallback — same ordering
|
|
132
|
+
// `parseKey` applies to a single key, applied here across the whole domain.
|
|
133
|
+
expect(classifyDomain(['2024', '2025', '2026'])).toBe('linear')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('a numeric bucket domain classifies as linear', () => {
|
|
137
|
+
expect(classifyDomain(['0', '1', '2', '3'])).toBe('linear')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('a category domain classifies as band', () => {
|
|
141
|
+
expect(classifyDomain(['Visited', 'Added to cart', 'Purchased'])).toBe('band')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('a mixed domain falls back to band', () => {
|
|
145
|
+
expect(classifyDomain(['2026-08-01', 'Direct'])).toBe('band')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('an empty domain classifies as band', () => {
|
|
149
|
+
expect(classifyDomain([])).toBe('band')
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -8,18 +8,50 @@
|
|
|
8
8
|
* PARSED domain instead makes that automatic (`docs/CHARTS-SPEC.md` §3).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/** Numeric-string test — shared by `parseKey` and `classifyDomain` so a key can never be numeric
|
|
12
|
+
* to one and not the other. */
|
|
13
|
+
function isNumericString(key: string): boolean {
|
|
14
|
+
return /^[-+]?\d*\.?\d+$/.test(key) && Number.isFinite(Number(key))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** `Date.parse`-able test — shared by `parseKey` and `classifyDomain`, same reason. */
|
|
18
|
+
function isDateParseableString(key: string): boolean {
|
|
19
|
+
return !Number.isNaN(Date.parse(key))
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
/**
|
|
12
23
|
* A key as a comparable number: numeric-looking strings as numbers, everything else as a date.
|
|
13
24
|
* Neither (a plain category like "Direct") → null, which degrades to exact-match only — correct,
|
|
14
25
|
* since "nearest category" is meaningless.
|
|
15
26
|
*/
|
|
16
27
|
export function parseKey(key: string): number | null {
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
if (isNumericString(key)) return Number(key)
|
|
29
|
+
return isDateParseableString(key) ? Date.parse(key) : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The x-domain families the shared cursor partitions on, derived from a chart's own keys — never
|
|
33
|
+
* a prop, never `ChartCursorScope`. A chart's kind is stamped on every broadcast it makes, and a
|
|
34
|
+
* receiving chart ignores any broadcast whose kind differs from its own — so a category chart can
|
|
35
|
+
* never be mistaken for a time chart just because both domains happen to parse. */
|
|
36
|
+
export type DomainKind = 'time' | 'linear' | 'band'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Classify a domain by the SAME parse precedence `parseKey` applies to each key, so a domain can
|
|
40
|
+
* never be classified one way and resolved another:
|
|
41
|
+
*
|
|
42
|
+
* - empty domain → `'band'` (nothing to share).
|
|
43
|
+
* - every key numeric (same test `parseKey` tries first) → `'linear'`.
|
|
44
|
+
* - otherwise every key `Date.parse`-able (same test `parseKey` falls back to) → `'time'`.
|
|
45
|
+
* - otherwise → `'band'` (categories, or a mixed domain).
|
|
46
|
+
*
|
|
47
|
+
* Ordering matters and mirrors `parseKey` exactly: a domain of bare years like `"2026"` is numeric
|
|
48
|
+
* FIRST, so it classifies `'linear'`, not `'time'`, even though every value is also a valid date.
|
|
49
|
+
*/
|
|
50
|
+
export function classifyDomain(keys: readonly string[]): DomainKind {
|
|
51
|
+
if (keys.length === 0) return 'band'
|
|
52
|
+
if (keys.every(isNumericString)) return 'linear'
|
|
53
|
+
if (keys.every(isDateParseableString)) return 'time'
|
|
54
|
+
return 'band'
|
|
23
55
|
}
|
|
24
56
|
|
|
25
57
|
/**
|