@misoto22/design 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/Accordion.md +1 -1
- package/dist/agent/Alert.md +1 -1
- package/dist/agent/AppShell.md +1 -1
- package/dist/agent/ArchitectureFigure.md +1 -1
- package/dist/agent/AreaChart.md +1 -1
- package/dist/agent/Article.md +1 -1
- package/dist/agent/AspectRatio.md +1 -1
- package/dist/agent/Avatar.md +1 -1
- package/dist/agent/Badge.md +1 -1
- package/dist/agent/BarChart.md +1 -1
- package/dist/agent/BarList.md +1 -1
- package/dist/agent/BigNumber.md +1 -1
- package/dist/agent/BoxPlot.md +1 -1
- package/dist/agent/Breadcrumb.md +1 -1
- package/dist/agent/BulletChart.md +1 -1
- package/dist/agent/Button.md +1 -1
- package/dist/agent/Calendar.md +1 -1
- package/dist/agent/Card.md +1 -1
- package/dist/agent/Checkbox.md +1 -1
- package/dist/agent/Code.md +1 -1
- package/dist/agent/CodeBlock.md +1 -1
- package/dist/agent/Collapsible.md +1 -1
- package/dist/agent/ColorPicker.md +1 -1
- package/dist/agent/Combobox.md +1 -1
- package/dist/agent/Command.md +1 -1
- package/dist/agent/ComposedChart.md +1 -1
- package/dist/agent/ContextMenu.md +1 -1
- package/dist/agent/DataflowFigure.md +1 -1
- package/dist/agent/DatePicker.md +1 -1
- package/dist/agent/DescriptionList.md +1 -1
- package/dist/agent/Diagram.md +1 -1
- package/dist/agent/DiagramCanvas.md +1 -1
- package/dist/agent/DiagramExportMenu.md +1 -1
- package/dist/agent/DiagramInspector.md +1 -1
- package/dist/agent/DiagramLegend.md +1 -1
- package/dist/agent/DiagramMinimap.md +1 -1
- package/dist/agent/DiagramToolbar.md +1 -1
- package/dist/agent/Dialog.md +1 -1
- package/dist/agent/DropdownMenu.md +1 -1
- package/dist/agent/EmptyState.md +1 -1
- package/dist/agent/ErrorState.md +1 -1
- package/dist/agent/Facet.md +1 -1
- package/dist/agent/Field.md +1 -1
- package/dist/agent/FigureBand.md +1 -1
- package/dist/agent/FloatingIconButton.md +1 -1
- package/dist/agent/FunnelChart.md +1 -1
- package/dist/agent/Heading.md +1 -1
- package/dist/agent/Heatmap.md +1 -1
- package/dist/agent/Histogram.md +1 -1
- package/dist/agent/Input.md +1 -1
- package/dist/agent/Kbd.md +2 -2
- package/dist/agent/LifecycleFigure.md +1 -1
- package/dist/agent/LineChart.md +1 -1
- package/dist/agent/LinkArrow.md +1 -1
- package/dist/agent/Markdown.md +1 -1
- package/dist/agent/NativeSelect.md +1 -1
- package/dist/agent/NavItem.md +1 -1
- package/dist/agent/NumberField.md +1 -1
- package/dist/agent/Pagination.md +1 -1
- package/dist/agent/PieChart.md +1 -1
- package/dist/agent/Popover.md +1 -1
- package/dist/agent/Progress.md +1 -1
- package/dist/agent/RadarChart.md +1 -1
- package/dist/agent/RadialChart.md +1 -1
- package/dist/agent/RadioGroup.md +1 -1
- package/dist/agent/SankeyChart.md +1 -1
- package/dist/agent/ScatterChart.md +1 -1
- package/dist/agent/ScrollArea.md +1 -1
- package/dist/agent/SearchableMenu.md +1 -1
- package/dist/agent/Select.md +1 -1
- package/dist/agent/Separator.md +1 -1
- package/dist/agent/SequenceFigure.md +1 -1
- package/dist/agent/Sheet.md +1 -1
- package/dist/agent/Sidebar.md +2 -2
- package/dist/agent/Skeleton.md +1 -1
- package/dist/agent/Slider.md +1 -1
- package/dist/agent/Sparkline.md +1 -1
- package/dist/agent/Spinner.md +1 -1
- package/dist/agent/StatusDot.md +1 -1
- package/dist/agent/StatusPill.md +1 -1
- package/dist/agent/Steps.md +1 -1
- package/dist/agent/Switch.md +1 -1
- package/dist/agent/Table.md +1 -1
- package/dist/agent/Tabs.md +1 -1
- package/dist/agent/Tag.md +1 -1
- package/dist/agent/Text.md +1 -1
- package/dist/agent/Textarea.md +1 -1
- package/dist/agent/Timestamp.md +1 -1
- package/dist/agent/Toast.md +1 -1
- package/dist/agent/ToggleGroup.md +1 -1
- package/dist/agent/Toolbar.md +1 -1
- package/dist/agent/Tooltip.md +1 -1
- package/dist/agent/TreemapChart.md +1 -1
- package/dist/agent/WaterfallChart.md +1 -1
- package/dist/agent/WorkflowFigure.md +1 -1
- package/dist/agent/catalog.json +2 -1
- package/dist/agent/index.md +2 -2
- package/dist/charts/lib/chart.js +16 -1
- package/dist/charts/lib/chart.js.map +1 -1
- package/dist/charts/lib/empty.js +12 -1
- package/dist/charts/lib/empty.js.map +1 -1
- package/dist/components/AppShell/AppShell.js +2 -2
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/Button/Button.js +3 -3
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Kbd/Kbd.d.ts +7 -0
- package/dist/components/Kbd/Kbd.js +1 -1
- package/dist/components/Kbd/Kbd.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.d.ts +8 -0
- package/dist/components/Sidebar/Sidebar.js +2 -2
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/semantic.css +4 -2
- package/dist/styles.css +1 -1
- package/dist/themes.css +91 -0
- package/dist/tokens.css +67 -1
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.js +14 -3
- package/dist/tokens.json +14 -3
- package/package.json +1 -1
package/dist/agent/Progress.md
CHANGED
package/dist/agent/RadarChart.md
CHANGED
|
@@ -4,7 +4,7 @@ Values on an arc — a gauge, or a few totals against one scale.
|
|
|
4
4
|
|
|
5
5
|
- Group: Charts
|
|
6
6
|
- Import: `import { RadialChart } from '@misoto22/design/charts'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/radial-chart/
|
|
9
9
|
- Related: pie-chart, bar-chart
|
|
10
10
|
|
package/dist/agent/RadioGroup.md
CHANGED
|
@@ -4,7 +4,7 @@ Two measures against each other, one mark per observation.
|
|
|
4
4
|
|
|
5
5
|
- Group: Charts
|
|
6
6
|
- Import: `import { ScatterChart } from '@misoto22/design/charts'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/scatter-chart/
|
|
9
9
|
- Related: line-chart, heatmap
|
|
10
10
|
|
package/dist/agent/ScrollArea.md
CHANGED
|
@@ -4,7 +4,7 @@ A box that scrolls, with a scrollbar that looks the same everywhere.
|
|
|
4
4
|
|
|
5
5
|
- Group: Surfaces
|
|
6
6
|
- Import: `import { ScrollArea } from '@misoto22/design'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/scroll-area/
|
|
9
9
|
- Related: table
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ A menu of actions you can type into.
|
|
|
4
4
|
|
|
5
5
|
- Group: Overlays
|
|
6
6
|
- Import: `import { SearchableMenu } from '@misoto22/design'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/searchable-menu/
|
|
9
9
|
- Related: dropdown-menu, command, combobox
|
|
10
10
|
|
package/dist/agent/Select.md
CHANGED
package/dist/agent/Separator.md
CHANGED
|
@@ -4,7 +4,7 @@ A rule, in the three weights a monochrome page needs — with words in it when t
|
|
|
4
4
|
|
|
5
5
|
- Group: Display
|
|
6
6
|
- Import: `import { Separator } from '@misoto22/design'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/separator/
|
|
9
9
|
|
|
10
10
|
## When to reach for it
|
|
@@ -4,7 +4,7 @@ A call chain over time: who asks whom, in what order, and what comes back.
|
|
|
4
4
|
|
|
5
5
|
- Group: Diagrams
|
|
6
6
|
- Import: `import { SequenceFigure } from '@misoto22/design/diagrams'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/sequence-figure/
|
|
9
9
|
- Related: workflow-figure, architecture-figure
|
|
10
10
|
|
package/dist/agent/Sheet.md
CHANGED
package/dist/agent/Sidebar.md
CHANGED
|
@@ -4,7 +4,7 @@ A navigation rail down the side of an application.
|
|
|
4
4
|
|
|
5
5
|
- Group: Navigation
|
|
6
6
|
- Import: `import { Sidebar } from '@misoto22/design'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/sidebar/
|
|
9
9
|
- Related: nav-item, collapsible, app-shell
|
|
10
10
|
|
|
@@ -102,7 +102,7 @@ The page beside the rail. Every layout built on this component was writing the s
|
|
|
102
102
|
|
|
103
103
|
## SidebarHeader
|
|
104
104
|
|
|
105
|
-
The block at the top of the rail: a brand, a workspace, a switcher. It is also where `SidebarTrigger` belongs, and the layout assumes one: anything passed as `children` takes the space and the trigger sits at the inline end of the row.
|
|
105
|
+
The block at the top of the rail: a brand, a workspace, a switcher. It is also where `SidebarTrigger` belongs, and the layout assumes one: anything passed as `children` takes the space and the trigger sits at the inline end of the row. `--bar-h` is the floor, not a number of its own. This block and the masthead beside it meet at a corner, and their two bottom rules are read as one line — so a floor typed here is a floor that has to be retyped there, and the two were 56px and 64px for exactly that reason. The token also survives a rail that pins its own density: `--bar-h` is declared on the root, so its `var(--control-h-md)` resolves there and inherits as one answer, which is what keeps a compact rail's head level with a comfortable page's bar.
|
|
106
106
|
|
|
107
107
|
## SidebarContent
|
|
108
108
|
|
package/dist/agent/Skeleton.md
CHANGED
package/dist/agent/Slider.md
CHANGED
package/dist/agent/Sparkline.md
CHANGED
package/dist/agent/Spinner.md
CHANGED
package/dist/agent/StatusDot.md
CHANGED
package/dist/agent/StatusPill.md
CHANGED
|
@@ -4,7 +4,7 @@ A live state, named: a dot plus an uppercase mono label.
|
|
|
4
4
|
|
|
5
5
|
- Group: Display
|
|
6
6
|
- Import: `import { StatusPill } from '@misoto22/design'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/status-pill/
|
|
9
9
|
- Related: status-dot, badge
|
|
10
10
|
|
package/dist/agent/Steps.md
CHANGED
|
@@ -4,7 +4,7 @@ A numbered sequence, as a rail — one thing after another, with a rule through
|
|
|
4
4
|
|
|
5
5
|
- Group: Display
|
|
6
6
|
- Import: `import { Steps } from '@misoto22/design'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/steps/
|
|
9
9
|
- Related: diagram, article
|
|
10
10
|
|
package/dist/agent/Switch.md
CHANGED
package/dist/agent/Table.md
CHANGED
package/dist/agent/Tabs.md
CHANGED
package/dist/agent/Tag.md
CHANGED
|
@@ -4,7 +4,7 @@ A subject label — a topic, a technology, a filter facet — that filters with
|
|
|
4
4
|
|
|
5
5
|
- Group: Display
|
|
6
6
|
- Import: `import { Tag } from '@misoto22/design'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/tag/
|
|
9
9
|
- Related: badge, status-pill
|
|
10
10
|
|
package/dist/agent/Text.md
CHANGED
|
@@ -4,7 +4,7 @@ The system’s paragraph, on the second rung of the ink ladder.
|
|
|
4
4
|
|
|
5
5
|
- Group: Display
|
|
6
6
|
- Import: `import { Text } from '@misoto22/design'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/text/
|
|
9
9
|
- Related: heading, article, markdown
|
|
10
10
|
|
package/dist/agent/Textarea.md
CHANGED
package/dist/agent/Timestamp.md
CHANGED
|
@@ -4,7 +4,7 @@ A date or a time, rendered the one way the system renders them.
|
|
|
4
4
|
|
|
5
5
|
- Group: Display
|
|
6
6
|
- Import: `import { Timestamp } from '@misoto22/design'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/timestamp/
|
|
9
9
|
- Related: text, badge, description-list
|
|
10
10
|
|
package/dist/agent/Toast.md
CHANGED
package/dist/agent/Toolbar.md
CHANGED
package/dist/agent/Tooltip.md
CHANGED
|
@@ -4,7 +4,7 @@ Part of a whole, when the whole has too many parts for a pie — and the parts n
|
|
|
4
4
|
|
|
5
5
|
- Group: Charts
|
|
6
6
|
- Import: `import { TreemapChart } from '@misoto22/design/charts'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/treemap-chart/
|
|
9
9
|
- Related: pie-chart, bar-chart
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ How a total got from one figure to another.
|
|
|
4
4
|
|
|
5
5
|
- Group: Charts
|
|
6
6
|
- Import: `import { WaterfallChart } from '@misoto22/design/charts'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/waterfall-chart/
|
|
9
9
|
- Related: bar-chart, funnel-chart
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ A process in lanes and phases, with the main path drawn heavier than its branche
|
|
|
4
4
|
|
|
5
5
|
- Group: Diagrams
|
|
6
6
|
- Import: `import { WorkflowFigure } from '@misoto22/design/diagrams'`
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.10.1
|
|
8
8
|
- Docs: https://ui.misoto22.com/components/workflow-figure/
|
|
9
9
|
- Related: lifecycle-figure, sequence-figure, steps
|
|
10
10
|
|
package/dist/agent/catalog.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.10.1",
|
|
3
3
|
"groups": [
|
|
4
4
|
"Actions",
|
|
5
5
|
"Forms",
|
|
@@ -7205,6 +7205,7 @@
|
|
|
7205
7205
|
{
|
|
7206
7206
|
"axis": "data-chart-palette",
|
|
7207
7207
|
"values": [
|
|
7208
|
+
"accent",
|
|
7208
7209
|
"chroma"
|
|
7209
7210
|
],
|
|
7210
7211
|
"unset": "the neutral series ramp"
|
package/dist/agent/index.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @misoto22/design 0.
|
|
1
|
+
# @misoto22/design 0.10.1
|
|
2
2
|
|
|
3
3
|
> Monochrome design system: portable CSS tokens and accessible React 19
|
|
4
4
|
> primitives. Components ship compiled — import them, do not copy them.
|
|
@@ -16,7 +16,7 @@ npx misoto22-design docs <Component>
|
|
|
16
16
|
Attributes, independent, and they work on any element — not just the root.
|
|
17
17
|
An unset axis is the default.
|
|
18
18
|
|
|
19
|
-
- `data-chart-palette`: `chroma` (unset = the neutral series ramp)
|
|
19
|
+
- `data-chart-palette`: `accent` `chroma` (unset = the neutral series ramp)
|
|
20
20
|
- `data-density`: `comfortable` `compact` (unset = comfortable)
|
|
21
21
|
- `data-mode`: `dark` `light` (unset = follows the app)
|
|
22
22
|
- `data-motion`: `snappy` `still` (unset = calm)
|
package/dist/charts/lib/chart.js
CHANGED
|
@@ -85,6 +85,11 @@ function ChartContainer({
|
|
|
85
85
|
// 1200px it is a 675px-tall plot with six points in it. The cap is
|
|
86
86
|
// what stops the aspect ratio running away with a wide container,
|
|
87
87
|
// and `min-h` keeps it from collapsing in a narrow one.
|
|
88
|
+
//
|
|
89
|
+
// With a footer the shape moves DOWN one element rather than being
|
|
90
|
+
// dropped — see the container below. Sizing this box 16:9 would hand
|
|
91
|
+
// the footer's height to the footer out of the plot's share, which is
|
|
92
|
+
// a plot that shrinks as its brush grows.
|
|
88
93
|
!footer && "aspect-video max-h-[26rem] min-h-[13rem]",
|
|
89
94
|
// Recharts' own defaults, re-pointed at the token layer.
|
|
90
95
|
"[&_.recharts-cartesian-axis-tick_text]:fill-(--chart-axis)",
|
|
@@ -104,7 +109,17 @@ function ChartContainer({
|
|
|
104
109
|
...rest,
|
|
105
110
|
children: [
|
|
106
111
|
/* @__PURE__ */ jsx(ChartStyle, { id: chartId, config }),
|
|
107
|
-
/* @__PURE__ */ jsx(
|
|
112
|
+
/* @__PURE__ */ jsx(
|
|
113
|
+
ResponsiveContainer,
|
|
114
|
+
{
|
|
115
|
+
className: cn(
|
|
116
|
+
"w-full flex-1",
|
|
117
|
+
footer ? "aspect-video max-h-[26rem] min-h-[13rem]" : "min-h-0"
|
|
118
|
+
),
|
|
119
|
+
initialDimension,
|
|
120
|
+
children
|
|
121
|
+
}
|
|
122
|
+
),
|
|
108
123
|
footer
|
|
109
124
|
]
|
|
110
125
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/charts/lib/chart.tsx"],"sourcesContent":["'use client'\n\nimport {\n createContext,\n useContext,\n useId,\n useMemo,\n type ComponentProps,\n type ComponentType,\n type ReactNode,\n} from 'react'\nimport { ResponsiveContainer } from 'recharts'\nimport { cn } from '../../lib/cn'\n\n/**\n * How many series the system will paint before it refuses.\n *\n * Eight is not a rendering limit — it is the width of the validated ramp in\n * `tokens.css`. A ninth series has no slot, and generating one would mean\n * inventing a colour nobody checked against the eighth. The catalogued answer\n * to \"I have twelve categories\" is to fold the tail into an \"Other\" series or\n * to facet, and this constant is where that decision is enforced rather than\n * left to whoever is looking at the chart.\n */\nexport const SERIES_SLOTS = 8\n\n/** Theme name → the selector its block is written under. */\nconst THEMES = { light: '', dark: \"[data-mode='dark']\" } as const\n\ntype ThemeKey = keyof typeof THEMES\n\nconst THEME_KEYS = Object.keys(THEMES) as ThemeKey[]\n\n/** Colours for one series, per theme. At least one theme must be given. */\ntype ThemeColors = {\n [K in ThemeKey]: Required<Pick<Record<ThemeKey, string[]>, K>> &\n Partial<Omit<Record<ThemeKey, string[]>, K>>\n}[ThemeKey]\n\nexport interface ChartSeries {\n /**\n * What the legend, the tooltip and the hidden data table call this series.\n *\n * Required, unlike the shape this was ported from. Colour is never the only\n * carrier of identity in this system, and a legend entry with no label is a\n * swatch that means nothing — least of all in the monochrome default, where\n * two series differ by a step of grey and a texture.\n */\n label: ReactNode\n /** Drawn in place of the legend/tooltip swatch. Decorative; label still wins. */\n icon?: ComponentType\n /**\n * Explicit colours for this series, overriding its slot on the series ramp.\n *\n * Reach for this only when the data itself carries a colour the reader\n * already knows — a brand, a traffic light, a map key. For \"I want my chart\n * to have colours\", set `data-chart-palette=\"chroma\"` on any ancestor: that\n * swaps the whole ramp for a palette validated against both grounds, which a\n * hand-picked array is not.\n *\n * More than one colour paints the series as a horizontal gradient across\n * those stops. Values are plain CSS colours; anything that could break out of\n * a declaration is rejected at the boundary rather than injected.\n */\n colors?: ThemeColors\n}\n\n/** Series keys → how each is labelled and painted. Declaration order is slot order. */\nexport type ChartConfig = Record<string, ChartSeries>\n\ninterface ChartContextValue {\n config: ChartConfig\n}\n\nconst ChartContext = createContext<ChartContextValue | null>(null)\n\n/** The chart config of the nearest chart root. Throws outside one. */\nexport function useChart(): ChartContextValue {\n const context = useContext(ChartContext)\n if (!context) throw new Error('A chart part must be rendered inside a chart root')\n return context\n}\n\n/**\n * A CSS colour, or something that would escape the declaration it is written\n * into.\n *\n * `ChartStyle` writes consumer-supplied values into a `<style>` element, which\n * makes this a system boundary in the security sense: without the check, a\n * `colors` array carrying `red; } html { display: none } .x {` is not a colour,\n * it is a stylesheet. The allowed set is deliberately shaped around what a\n * colour is made of — identifiers, digits, `#`, brackets, commas, percent — so\n * `var(--x)`, `oklch(…)` and `color-mix(in srgb, …)` all pass and no `;`, `{`,\n * `}`, `<`, `\\` or comment sequence does.\n */\nconst CSS_COLOR = /^[A-Za-z0-9#(),.%\\-\\s_]{1,120}$/\n\nfunction isSafeColor(value: unknown): value is string {\n return typeof value === 'string' && CSS_COLOR.test(value) && !value.includes('/*')\n}\n\n/**\n * Distributes fewer colours than slots, giving the surplus to the last stops.\n * Two colours over four slots read as `[a, a, b, b]`; three over four as\n * `[a, b, c, c]`.\n */\nfunction distribute(colors: string[], slots: number): string[] {\n if (colors.length >= slots) return colors.slice(0, slots)\n\n const base = Math.floor(slots / colors.length)\n const extra = slots % colors.length\n const out: string[] = []\n colors.forEach((color, index) => {\n const count = base + (index >= colors.length - extra ? 1 : 0)\n for (let n = 0; n < count; n += 1) out.push(color)\n })\n return out\n}\n\n/** How many gradient stops a series paints with — one means a flat fill. */\nexport function colorStops(series: ChartSeries | undefined): number {\n if (!series?.colors) return 1\n return Math.max(1, ...THEME_KEYS.map((theme) => series.colors?.[theme]?.length ?? 0))\n}\n\n/**\n * The custom properties one chart instance paints from.\n *\n * Every series resolves to `--color-<key>-<n>`, and every mark, swatch and\n * gradient stop in the package reads only those names. That indirection is what\n * lets the same chart be repainted by an ancestor attribute — a theme, a\n * palette, dark mode — without a component knowing anything about colour.\n *\n * A series with no `colors` takes its slot on the ramp, in declaration order.\n * That is the \"assign in fixed order, never cycle\" rule expressed as code: the\n * fourth series is `--series-4` whether or not the first three are on screen,\n * so hiding a series never repaints the survivors.\n */\nfunction ChartStyle({ id, config }: { id: string; config: ChartConfig }) {\n const css = useMemo(() => {\n const entries = Object.entries(config)\n\n const rules = (theme: ThemeKey) =>\n entries\n .flatMap(([key, series], slot) => {\n const stops = colorStops(series)\n const slotColor = `var(--series-${(slot % SERIES_SLOTS) + 1})`\n const given = series.colors?.[theme] ?? series.colors?.light ?? series.colors?.dark\n const safe = given?.filter(isSafeColor) ?? []\n // A rejected colour falls back to the ramp rather than to nothing: an\n // unpainted series is invisible, which is a worse failure than an\n // ignored value and a harder one to notice.\n const colors = safe.length > 0 ? safe : [slotColor]\n return distribute(colors, stops).map(\n (color, index) => ` --color-${cssName(key)}-${index}: ${color};`,\n )\n })\n .join('\\n')\n\n return THEME_KEYS.map((theme) => {\n const body = rules(theme)\n if (!body) return ''\n return `${THEMES[theme]} [data-chart='${id}'] {\\n${body}\\n}`\n })\n .filter(Boolean)\n .join('\\n')\n }, [id, config])\n\n if (!css) return null\n\n return <style>{css}</style>\n}\n\n/**\n * A series key, reduced to what may appear in a custom property name. Keys come\n * from consumer data, so the same boundary argument as `isSafeColor` applies —\n * here it is the property NAME rather than its value.\n */\nexport function cssName(key: string): string {\n return key.replace(/[^A-Za-z0-9_-]/g, '-')\n}\n\nexport interface ChartContainerProps\n extends Omit<ComponentProps<'div'>, 'children'>,\n Pick<\n ComponentProps<typeof ResponsiveContainer>,\n 'initialDimension' | 'minHeight' | 'minWidth' | 'maxHeight' | 'height' | 'width' | 'children'\n > {\n config: ChartConfig\n /** Rendered under the plot, inside the same measured box — the brush lives here. */\n footer?: ReactNode\n}\n\n/**\n * The measured box every chart is drawn into, and the only place Recharts'\n * hard-coded greys are re-pointed at system tokens.\n *\n * Recharts paints its axes, grid, cursor and radial track with literal `#ccc`\n * and `#fff`. The selectors below catch each of those and hand it a token, so a\n * chart follows light/dark and a surface theme without a consumer configuring\n * anything — and so nothing in the package has to pass a colour down by prop.\n */\nexport function ChartContainer({\n id,\n config,\n initialDimension = { width: 320, height: 200 },\n className,\n children,\n footer,\n ...rest\n}: ChartContainerProps) {\n const generated = useId().replace(/:/g, '')\n const chartId = `chart-${id ?? generated}`\n\n return (\n <ChartContext.Provider value={{ config }}>\n <div\n data-slot=\"chart\"\n data-chart={chartId}\n className={cn(\n 'relative flex min-h-0 w-full flex-1 flex-col justify-center text-xs',\n // 16:9 is right for a card and wrong for a full-width panel: at\n // 1200px it is a 675px-tall plot with six points in it. The cap is\n // what stops the aspect ratio running away with a wide container,\n // and `min-h` keeps it from collapsing in a narrow one.\n !footer && 'aspect-video max-h-[26rem] min-h-[13rem]',\n // Recharts' own defaults, re-pointed at the token layer.\n \"[&_.recharts-cartesian-axis-tick_text]:fill-(--chart-axis)\",\n \"[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-(--chart-grid)\",\n '[&_.recharts-curve.recharts-tooltip-cursor]:stroke-(--chart-cursor)',\n \"[&_.recharts-polar-grid_[stroke='#ccc']]:stroke-(--chart-grid)\",\n '[&_.recharts-radial-bar-background-sector]:fill-(--chart-track)',\n '[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-(--chart-track)',\n \"[&_.recharts-reference-line_[stroke='#ccc']]:stroke-(--chart-grid)\",\n \"[&_.recharts-dot[stroke='#fff']]:stroke-transparent\",\n \"[&_.recharts-sector[stroke='#fff']]:stroke-transparent\",\n // Recharts draws its own focus ring; the system draws one focus ring,\n // in one place, and this is how the duplicate is removed.\n '[&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden',\n className,\n )}\n {...rest}\n >\n <ChartStyle id={chartId} config={config} />\n <ResponsiveContainer className=\"min-h-0 w-full flex-1\" initialDimension={initialDimension}>\n {children}\n </ResponsiveContainer>\n {footer}\n </div>\n </ChartContext.Provider>\n )\n}\n\n/**\n * Reads the config entry behind a Recharts payload item.\n *\n * Recharts hands the same series different names in different chart types — a\n * pie sector's key is on `payload.payload[nameKey]`, a bar's is `dataKey` — so\n * this is the one place that disagreement is resolved.\n */\nexport function seriesFromPayload(\n config: ChartConfig,\n payload: unknown,\n key: string,\n): ChartSeries | undefined {\n if (typeof payload !== 'object' || payload === null) return undefined\n\n const nested =\n 'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null\n ? (payload.payload as Record<string, unknown>)\n : undefined\n\n let name = key\n const direct = (payload as Record<string, unknown>)[key]\n if (typeof direct === 'string') name = direct\n else if (nested && typeof nested[key] === 'string') name = nested[key] as string\n\n return name in config ? config[name] : config[key]\n}\n\n/** Formats an expanded stack's axis, where 0–1 means 0–100%. */\nexport function percentTick(value: number): string {\n return `${Math.round(value * 100)}%`\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/lib/chart.tsx"],"sourcesContent":["'use client'\n\nimport {\n createContext,\n useContext,\n useId,\n useMemo,\n type ComponentProps,\n type ComponentType,\n type ReactNode,\n} from 'react'\nimport { ResponsiveContainer } from 'recharts'\nimport { cn } from '../../lib/cn'\n\n/**\n * How many series the system will paint before it refuses.\n *\n * Eight is not a rendering limit — it is the width of the validated ramp in\n * `tokens.css`. A ninth series has no slot, and generating one would mean\n * inventing a colour nobody checked against the eighth. The catalogued answer\n * to \"I have twelve categories\" is to fold the tail into an \"Other\" series or\n * to facet, and this constant is where that decision is enforced rather than\n * left to whoever is looking at the chart.\n */\nexport const SERIES_SLOTS = 8\n\n/** Theme name → the selector its block is written under. */\nconst THEMES = { light: '', dark: \"[data-mode='dark']\" } as const\n\ntype ThemeKey = keyof typeof THEMES\n\nconst THEME_KEYS = Object.keys(THEMES) as ThemeKey[]\n\n/** Colours for one series, per theme. At least one theme must be given. */\ntype ThemeColors = {\n [K in ThemeKey]: Required<Pick<Record<ThemeKey, string[]>, K>> &\n Partial<Omit<Record<ThemeKey, string[]>, K>>\n}[ThemeKey]\n\nexport interface ChartSeries {\n /**\n * What the legend, the tooltip and the hidden data table call this series.\n *\n * Required, unlike the shape this was ported from. Colour is never the only\n * carrier of identity in this system, and a legend entry with no label is a\n * swatch that means nothing — least of all in the monochrome default, where\n * two series differ by a step of grey and a texture.\n */\n label: ReactNode\n /** Drawn in place of the legend/tooltip swatch. Decorative; label still wins. */\n icon?: ComponentType\n /**\n * Explicit colours for this series, overriding its slot on the series ramp.\n *\n * Reach for this only when the data itself carries a colour the reader\n * already knows — a brand, a traffic light, a map key. For \"I want my chart\n * to have colours\", set `data-chart-palette=\"chroma\"` on any ancestor: that\n * swaps the whole ramp for a palette validated against both grounds, which a\n * hand-picked array is not.\n *\n * More than one colour paints the series as a horizontal gradient across\n * those stops. Values are plain CSS colours; anything that could break out of\n * a declaration is rejected at the boundary rather than injected.\n */\n colors?: ThemeColors\n}\n\n/** Series keys → how each is labelled and painted. Declaration order is slot order. */\nexport type ChartConfig = Record<string, ChartSeries>\n\ninterface ChartContextValue {\n config: ChartConfig\n}\n\nconst ChartContext = createContext<ChartContextValue | null>(null)\n\n/** The chart config of the nearest chart root. Throws outside one. */\nexport function useChart(): ChartContextValue {\n const context = useContext(ChartContext)\n if (!context) throw new Error('A chart part must be rendered inside a chart root')\n return context\n}\n\n/**\n * A CSS colour, or something that would escape the declaration it is written\n * into.\n *\n * `ChartStyle` writes consumer-supplied values into a `<style>` element, which\n * makes this a system boundary in the security sense: without the check, a\n * `colors` array carrying `red; } html { display: none } .x {` is not a colour,\n * it is a stylesheet. The allowed set is deliberately shaped around what a\n * colour is made of — identifiers, digits, `#`, brackets, commas, percent — so\n * `var(--x)`, `oklch(…)` and `color-mix(in srgb, …)` all pass and no `;`, `{`,\n * `}`, `<`, `\\` or comment sequence does.\n */\nconst CSS_COLOR = /^[A-Za-z0-9#(),.%\\-\\s_]{1,120}$/\n\nfunction isSafeColor(value: unknown): value is string {\n return typeof value === 'string' && CSS_COLOR.test(value) && !value.includes('/*')\n}\n\n/**\n * Distributes fewer colours than slots, giving the surplus to the last stops.\n * Two colours over four slots read as `[a, a, b, b]`; three over four as\n * `[a, b, c, c]`.\n */\nfunction distribute(colors: string[], slots: number): string[] {\n if (colors.length >= slots) return colors.slice(0, slots)\n\n const base = Math.floor(slots / colors.length)\n const extra = slots % colors.length\n const out: string[] = []\n colors.forEach((color, index) => {\n const count = base + (index >= colors.length - extra ? 1 : 0)\n for (let n = 0; n < count; n += 1) out.push(color)\n })\n return out\n}\n\n/** How many gradient stops a series paints with — one means a flat fill. */\nexport function colorStops(series: ChartSeries | undefined): number {\n if (!series?.colors) return 1\n return Math.max(1, ...THEME_KEYS.map((theme) => series.colors?.[theme]?.length ?? 0))\n}\n\n/**\n * The custom properties one chart instance paints from.\n *\n * Every series resolves to `--color-<key>-<n>`, and every mark, swatch and\n * gradient stop in the package reads only those names. That indirection is what\n * lets the same chart be repainted by an ancestor attribute — a theme, a\n * palette, dark mode — without a component knowing anything about colour.\n *\n * A series with no `colors` takes its slot on the ramp, in declaration order.\n * That is the \"assign in fixed order, never cycle\" rule expressed as code: the\n * fourth series is `--series-4` whether or not the first three are on screen,\n * so hiding a series never repaints the survivors.\n */\nfunction ChartStyle({ id, config }: { id: string; config: ChartConfig }) {\n const css = useMemo(() => {\n const entries = Object.entries(config)\n\n const rules = (theme: ThemeKey) =>\n entries\n .flatMap(([key, series], slot) => {\n const stops = colorStops(series)\n const slotColor = `var(--series-${(slot % SERIES_SLOTS) + 1})`\n const given = series.colors?.[theme] ?? series.colors?.light ?? series.colors?.dark\n const safe = given?.filter(isSafeColor) ?? []\n // A rejected colour falls back to the ramp rather than to nothing: an\n // unpainted series is invisible, which is a worse failure than an\n // ignored value and a harder one to notice.\n const colors = safe.length > 0 ? safe : [slotColor]\n return distribute(colors, stops).map(\n (color, index) => ` --color-${cssName(key)}-${index}: ${color};`,\n )\n })\n .join('\\n')\n\n return THEME_KEYS.map((theme) => {\n const body = rules(theme)\n if (!body) return ''\n return `${THEMES[theme]} [data-chart='${id}'] {\\n${body}\\n}`\n })\n .filter(Boolean)\n .join('\\n')\n }, [id, config])\n\n if (!css) return null\n\n return <style>{css}</style>\n}\n\n/**\n * A series key, reduced to what may appear in a custom property name. Keys come\n * from consumer data, so the same boundary argument as `isSafeColor` applies —\n * here it is the property NAME rather than its value.\n */\nexport function cssName(key: string): string {\n return key.replace(/[^A-Za-z0-9_-]/g, '-')\n}\n\nexport interface ChartContainerProps\n extends Omit<ComponentProps<'div'>, 'children'>,\n Pick<\n ComponentProps<typeof ResponsiveContainer>,\n 'initialDimension' | 'minHeight' | 'minWidth' | 'maxHeight' | 'height' | 'width' | 'children'\n > {\n config: ChartConfig\n /** Rendered under the plot, inside the same measured box — the brush lives here. */\n footer?: ReactNode\n}\n\n/**\n * The measured box every chart is drawn into, and the only place Recharts'\n * hard-coded greys are re-pointed at system tokens.\n *\n * Recharts paints its axes, grid, cursor and radial track with literal `#ccc`\n * and `#fff`. The selectors below catch each of those and hand it a token, so a\n * chart follows light/dark and a surface theme without a consumer configuring\n * anything — and so nothing in the package has to pass a colour down by prop.\n */\nexport function ChartContainer({\n id,\n config,\n initialDimension = { width: 320, height: 200 },\n className,\n children,\n footer,\n ...rest\n}: ChartContainerProps) {\n const generated = useId().replace(/:/g, '')\n const chartId = `chart-${id ?? generated}`\n\n return (\n <ChartContext.Provider value={{ config }}>\n <div\n data-slot=\"chart\"\n data-chart={chartId}\n className={cn(\n 'relative flex min-h-0 w-full flex-1 flex-col justify-center text-xs',\n // 16:9 is right for a card and wrong for a full-width panel: at\n // 1200px it is a 675px-tall plot with six points in it. The cap is\n // what stops the aspect ratio running away with a wide container,\n // and `min-h` keeps it from collapsing in a narrow one.\n //\n // With a footer the shape moves DOWN one element rather than being\n // dropped — see the container below. Sizing this box 16:9 would hand\n // the footer's height to the footer out of the plot's share, which is\n // a plot that shrinks as its brush grows.\n !footer && 'aspect-video max-h-[26rem] min-h-[13rem]',\n // Recharts' own defaults, re-pointed at the token layer.\n \"[&_.recharts-cartesian-axis-tick_text]:fill-(--chart-axis)\",\n \"[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-(--chart-grid)\",\n '[&_.recharts-curve.recharts-tooltip-cursor]:stroke-(--chart-cursor)',\n \"[&_.recharts-polar-grid_[stroke='#ccc']]:stroke-(--chart-grid)\",\n '[&_.recharts-radial-bar-background-sector]:fill-(--chart-track)',\n '[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-(--chart-track)',\n \"[&_.recharts-reference-line_[stroke='#ccc']]:stroke-(--chart-grid)\",\n \"[&_.recharts-dot[stroke='#fff']]:stroke-transparent\",\n \"[&_.recharts-sector[stroke='#fff']]:stroke-transparent\",\n // Recharts draws its own focus ring; the system draws one focus ring,\n // in one place, and this is how the duplicate is removed.\n '[&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden',\n className,\n )}\n {...rest}\n >\n <ChartStyle id={chartId} config={config} />\n {/* The plot carries the shape when there is a footer under it.\n It used to carry nothing: the outer box dropped `aspect-video` AND\n `min-h` the moment a footer existed, on the reasoning that a fixed\n aspect plus a footer overflows — which is true of the aspect and\n not of the floor. So a chart with a brush had a plot with no\n intrinsic height at all, and in any container that sizes to its\n content the plot measured zero and only the brush's own 56px\n survived. Recharts says so out loud — \"width(-1) and height(-1) of\n chart should be greater than 0\" — into a console nobody reads. */}\n <ResponsiveContainer\n className={cn(\n 'w-full flex-1',\n footer ? 'aspect-video max-h-[26rem] min-h-[13rem]' : 'min-h-0',\n )}\n initialDimension={initialDimension}\n >\n {children}\n </ResponsiveContainer>\n {footer}\n </div>\n </ChartContext.Provider>\n )\n}\n\n/**\n * Reads the config entry behind a Recharts payload item.\n *\n * Recharts hands the same series different names in different chart types — a\n * pie sector's key is on `payload.payload[nameKey]`, a bar's is `dataKey` — so\n * this is the one place that disagreement is resolved.\n */\nexport function seriesFromPayload(\n config: ChartConfig,\n payload: unknown,\n key: string,\n): ChartSeries | undefined {\n if (typeof payload !== 'object' || payload === null) return undefined\n\n const nested =\n 'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null\n ? (payload.payload as Record<string, unknown>)\n : undefined\n\n let name = key\n const direct = (payload as Record<string, unknown>)[key]\n if (typeof direct === 'string') name = direct\n else if (nested && typeof nested[key] === 'string') name = nested[key] as string\n\n return name in config ? config[name] : config[key]\n}\n\n/** Formats an expanded stack's axis, where 0–1 means 0–100%. */\nexport function percentTick(value: number): string {\n return `${Math.round(value * 100)}%`\n}\n"],"mappings":";AA0KS,cA8CH,YA9CG;AAxKT;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,2BAA2B;AACpC,SAAS,UAAU;AAYZ,MAAM,eAAe;AAG5B,MAAM,SAAS,EAAE,OAAO,IAAI,MAAM,qBAAqB;AAIvD,MAAM,aAAa,OAAO,KAAK,MAAM;AA2CrC,MAAM,eAAe,cAAwC,IAAI;AAG1D,SAAS,WAA8B;AAC5C,QAAM,UAAU,WAAW,YAAY;AACvC,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,mDAAmD;AACjF,SAAO;AACT;AAcA,MAAM,YAAY;AAElB,SAAS,YAAY,OAAiC;AACpD,SAAO,OAAO,UAAU,YAAY,UAAU,KAAK,KAAK,KAAK,CAAC,MAAM,SAAS,IAAI;AACnF;AAOA,SAAS,WAAW,QAAkB,OAAyB;AAC7D,MAAI,OAAO,UAAU,MAAO,QAAO,OAAO,MAAM,GAAG,KAAK;AAExD,QAAM,OAAO,KAAK,MAAM,QAAQ,OAAO,MAAM;AAC7C,QAAM,QAAQ,QAAQ,OAAO;AAC7B,QAAM,MAAgB,CAAC;AACvB,SAAO,QAAQ,CAAC,OAAO,UAAU;AAC/B,UAAM,QAAQ,QAAQ,SAAS,OAAO,SAAS,QAAQ,IAAI;AAC3D,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK,EAAG,KAAI,KAAK,KAAK;AAAA,EACnD,CAAC;AACD,SAAO;AACT;AAGO,SAAS,WAAW,QAAyC;AAClE,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,SAAO,KAAK,IAAI,GAAG,GAAG,WAAW,IAAI,CAAC,UAAU,OAAO,SAAS,KAAK,GAAG,UAAU,CAAC,CAAC;AACtF;AAeA,SAAS,WAAW,EAAE,IAAI,OAAO,GAAwC;AACvE,QAAM,MAAM,QAAQ,MAAM;AACxB,UAAM,UAAU,OAAO,QAAQ,MAAM;AAErC,UAAM,QAAQ,CAAC,UACb,QACG,QAAQ,CAAC,CAAC,KAAK,MAAM,GAAG,SAAS;AAChC,YAAM,QAAQ,WAAW,MAAM;AAC/B,YAAM,YAAY,gBAAiB,OAAO,eAAgB,CAAC;AAC3D,YAAM,QAAQ,OAAO,SAAS,KAAK,KAAK,OAAO,QAAQ,SAAS,OAAO,QAAQ;AAC/E,YAAM,OAAO,OAAO,OAAO,WAAW,KAAK,CAAC;AAI5C,YAAM,SAAS,KAAK,SAAS,IAAI,OAAO,CAAC,SAAS;AAClD,aAAO,WAAW,QAAQ,KAAK,EAAE;AAAA,QAC/B,CAAC,OAAO,UAAU,aAAa,QAAQ,GAAG,CAAC,IAAI,KAAK,KAAK,KAAK;AAAA,MAChE;AAAA,IACF,CAAC,EACA,KAAK,IAAI;AAEd,WAAO,WAAW,IAAI,CAAC,UAAU;AAC/B,YAAM,OAAO,MAAM,KAAK;AACxB,UAAI,CAAC,KAAM,QAAO;AAClB,aAAO,GAAG,OAAO,KAAK,CAAC,iBAAiB,EAAE;AAAA,EAAS,IAAI;AAAA;AAAA,IACzD,CAAC,EACE,OAAO,OAAO,EACd,KAAK,IAAI;AAAA,EACd,GAAG,CAAC,IAAI,MAAM,CAAC;AAEf,MAAI,CAAC,IAAK,QAAO;AAEjB,SAAO,oBAAC,WAAO,eAAI;AACrB;AAOO,SAAS,QAAQ,KAAqB;AAC3C,SAAO,IAAI,QAAQ,mBAAmB,GAAG;AAC3C;AAsBO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA,mBAAmB,EAAE,OAAO,KAAK,QAAQ,IAAI;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwB;AACtB,QAAM,YAAY,MAAM,EAAE,QAAQ,MAAM,EAAE;AAC1C,QAAM,UAAU,SAAS,MAAM,SAAS;AAExC,SACE,oBAAC,aAAa,UAAb,EAAsB,OAAO,EAAE,OAAO,GACrC;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,cAAY;AAAA,MACZ,WAAW;AAAA,QACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAUA,CAAC,UAAU;AAAA;AAAA,QAEX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,4BAAC,cAAW,IAAI,SAAS,QAAgB;AAAA,QAUzC;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,SAAS,6CAA6C;AAAA,YACxD;AAAA,YACA;AAAA,YAEC;AAAA;AAAA,QACH;AAAA,QACC;AAAA;AAAA;AAAA,EACH,GACF;AAEJ;AASO,SAAS,kBACd,QACA,SACA,KACyB;AACzB,MAAI,OAAO,YAAY,YAAY,YAAY,KAAM,QAAO;AAE5D,QAAM,SACJ,aAAa,WAAW,OAAO,QAAQ,YAAY,YAAY,QAAQ,YAAY,OAC9E,QAAQ,UACT;AAEN,MAAI,OAAO;AACX,QAAM,SAAU,QAAoC,GAAG;AACvD,MAAI,OAAO,WAAW,SAAU,QAAO;AAAA,WAC9B,UAAU,OAAO,OAAO,GAAG,MAAM,SAAU,QAAO,OAAO,GAAG;AAErE,SAAO,QAAQ,SAAS,OAAO,IAAI,IAAI,OAAO,GAAG;AACnD;AAGO,SAAS,YAAY,OAAuB;AACjD,SAAO,GAAG,KAAK,MAAM,QAAQ,GAAG,CAAC;AACnC;","names":[]}
|
package/dist/charts/lib/empty.js
CHANGED
|
@@ -5,7 +5,18 @@ function ChartEmpty({
|
|
|
5
5
|
description = "Nothing matched the current filters. Widen the range, or clear a filter.",
|
|
6
6
|
action
|
|
7
7
|
}) {
|
|
8
|
-
return
|
|
8
|
+
return (
|
|
9
|
+
// `my-auto` is what keeps this where the plot was. The box sizes itself
|
|
10
|
+
// 16:9 up to 26rem, which is right for a card that grows to its content and
|
|
11
|
+
// short of a figure given a fixed height by the page around it — a
|
|
12
|
+
// dashboard tile, a panel in a grid. Without the auto margin the state pins
|
|
13
|
+
// to the top of that figure while the plot it stands in for filled the
|
|
14
|
+
// whole cell, so the same chart reads at two different heights depending on
|
|
15
|
+
// whether it has data. The margin absorbs the free space when there is any
|
|
16
|
+
// and does nothing when there is none, so the content-sized case is
|
|
17
|
+
// untouched.
|
|
18
|
+
/* @__PURE__ */ jsx("div", { className: "my-auto flex aspect-video max-h-[26rem] min-h-[13rem] w-full items-center justify-center", children: /* @__PURE__ */ jsx(EmptyState, { title, description, action, className: "py-0" }) })
|
|
19
|
+
);
|
|
9
20
|
}
|
|
10
21
|
export {
|
|
11
22
|
ChartEmpty
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/charts/lib/empty.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { EmptyState } from '../../components/EmptyState/EmptyState'\n\nexport interface ChartEmptyProps {\n /** What is missing. Says what happened, not \"no data\". */\n title?: ReactNode\n /** Why, or what to change — a filter to widen, a range to move. */\n description?: ReactNode\n /** The one thing to do next. */\n action?: ReactNode\n}\n\n/**\n * What a chart shows when it has nothing to draw.\n *\n * The state almost every chart library forgets, and the one a real dashboard\n * hits within a week: a filter that matches nothing, a new account, a range\n * before the data starts. Without it a chart hands the reader an empty pair of\n * axes, which is indistinguishable from a chart that failed to load — so the\n * reader reloads the page, and it is still empty.\n *\n * Distinct from the loading skeleton, which says \"not yet\". This says \"there\n * is nothing here\", which is a different sentence and usually has an action\n * attached to it.\n *\n * Reuses the system's own `EmptyState` rather than inventing a chart-shaped\n * one: an empty chart and an empty table are the same event, and a reader\n * should not have to learn two vocabularies for it.\n */\nexport function ChartEmpty({\n title = 'No data in this range',\n description = 'Nothing matched the current filters. Widen the range, or clear a filter.',\n action,\n}: ChartEmptyProps) {\n return (\n <div className=\"flex aspect-video max-h-[26rem] min-h-[13rem] w-full items-center justify-center\">\n <EmptyState title={title} description={description} action={action} className=\"py-0\" />\n </div>\n )\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/lib/empty.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { EmptyState } from '../../components/EmptyState/EmptyState'\n\nexport interface ChartEmptyProps {\n /** What is missing. Says what happened, not \"no data\". */\n title?: ReactNode\n /** Why, or what to change — a filter to widen, a range to move. */\n description?: ReactNode\n /** The one thing to do next. */\n action?: ReactNode\n}\n\n/**\n * What a chart shows when it has nothing to draw.\n *\n * The state almost every chart library forgets, and the one a real dashboard\n * hits within a week: a filter that matches nothing, a new account, a range\n * before the data starts. Without it a chart hands the reader an empty pair of\n * axes, which is indistinguishable from a chart that failed to load — so the\n * reader reloads the page, and it is still empty.\n *\n * Distinct from the loading skeleton, which says \"not yet\". This says \"there\n * is nothing here\", which is a different sentence and usually has an action\n * attached to it.\n *\n * Reuses the system's own `EmptyState` rather than inventing a chart-shaped\n * one: an empty chart and an empty table are the same event, and a reader\n * should not have to learn two vocabularies for it.\n */\nexport function ChartEmpty({\n title = 'No data in this range',\n description = 'Nothing matched the current filters. Widen the range, or clear a filter.',\n action,\n}: ChartEmptyProps) {\n return (\n // `my-auto` is what keeps this where the plot was. The box sizes itself\n // 16:9 up to 26rem, which is right for a card that grows to its content and\n // short of a figure given a fixed height by the page around it — a\n // dashboard tile, a panel in a grid. Without the auto margin the state pins\n // to the top of that figure while the plot it stands in for filled the\n // whole cell, so the same chart reads at two different heights depending on\n // whether it has data. The margin absorbs the free space when there is any\n // and does nothing when there is none, so the content-sized case is\n // untouched.\n <div className=\"my-auto flex aspect-video max-h-[26rem] min-h-[13rem] w-full items-center justify-center\">\n <EmptyState title={title} description={description} action={action} className=\"py-0\" />\n </div>\n )\n}\n"],"mappings":"AA6CM;AA5CN,SAAS,kBAAkB;AA4BpB,SAAS,WAAW;AAAA,EACzB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AACF,GAAoB;AAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUE,oBAAC,SAAI,WAAU,4FACb,8BAAC,cAAW,OAAc,aAA0B,QAAgB,WAAU,QAAO,GACvF;AAAA;AAEJ;","names":[]}
|
|
@@ -83,7 +83,7 @@ function AppShell({
|
|
|
83
83
|
"md:static md:z-auto md:translate-x-0"
|
|
84
84
|
),
|
|
85
85
|
children: [
|
|
86
|
-
brand && /* @__PURE__ */ jsx("div", { className: "flex h-
|
|
86
|
+
brand && /* @__PURE__ */ jsx("div", { className: "flex h-(--bar-h) items-center border-b border-(--rule) px-5", children: brand }),
|
|
87
87
|
/* @__PURE__ */ jsx(
|
|
88
88
|
"nav",
|
|
89
89
|
{
|
|
@@ -96,7 +96,7 @@ function AppShell({
|
|
|
96
96
|
}
|
|
97
97
|
),
|
|
98
98
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
|
|
99
|
-
/* @__PURE__ */ jsxs("header", { className: "sticky top-0 z-(--z-sticky) flex h-
|
|
99
|
+
/* @__PURE__ */ jsxs("header", { className: "sticky top-0 z-(--z-sticky) flex h-(--bar-h) items-center gap-3 border-b border-(--rule) bg-(--paper)/85 px-(--page-pad) backdrop-blur", children: [
|
|
100
100
|
/* @__PURE__ */ jsx(
|
|
101
101
|
"button",
|
|
102
102
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["'use client'\n\nimport { Menu, X } from 'lucide-react'\nimport { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * The width the sidebar stops being a drawer at — `--breakpoint-md`, the same\n * 768px every `md:` variant below is compiled against. Written out rather than\n * read from the token, because a media query string is not a custom property\n * and `matchMedia` cannot resolve one.\n */\nconst DESKTOP = '(min-width: 768px)'\n\n/** True where nothing can answer the question: a server, or a DOM without it. */\nfunction cannotMeasure(): boolean {\n return typeof window === 'undefined' || typeof window.matchMedia !== 'function'\n}\n\nfunction subscribeToWidth(onChange: () => void): () => void {\n if (cannotMeasure()) return () => {}\n const list = window.matchMedia(DESKTOP)\n list.addEventListener('change', onChange)\n return () => list.removeEventListener('change', onChange)\n}\n\n/**\n * Falls back to \"not a drawer\", which is the safe answer rather than the likely\n * one: an environment that cannot measure must not take an entire application's\n * navigation out of the accessibility tree on a guess.\n */\nfunction readIsDrawer(): boolean {\n if (cannotMeasure()) return false\n return !window.matchMedia(DESKTOP).matches\n}\n\nfunction readIsDrawerOnServer(): boolean {\n return false\n}\n\n/**\n * True while the sidebar is a drawer rather than a column.\n *\n * `open` alone cannot answer this. It is false on a desktop too, where the\n * sidebar is a visible grid column — so anything keyed on `open` by itself\n * would hide the navigation of every desktop app in the package.\n *\n * `useSyncExternalStore` rather than an effect: the server has no viewport, so\n * it renders the desktop answer and React reconciles the real one on hydration\n * instead of the two silently disagreeing.\n */\nfunction useIsDrawer(): boolean {\n return useSyncExternalStore(subscribeToWidth, readIsDrawer, readIsDrawerOnServer)\n}\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Navigation content for the sidebar (e.g. a stack of `NavItem`s). */\n sidebar: ReactNode\n /** Optional content for the sticky topbar, laid out after the mobile toggle. */\n topbar?: ReactNode\n /** Optional brand lockup pinned to the top of the sidebar. */\n brand?: ReactNode\n /**\n * Which element the content well renders as.\n *\n * `main` is right for the shell of an application, and is the default. A\n * document may contain exactly one `main` landmark, so an AppShell rendered\n * INSIDE another page — a preview on a documentation site, a screenshot\n * harness — must pass `div`, or the page has two and assistive tech can no\n * longer answer \"where is the content\".\n */\n contentAs?: 'main' | 'div'\n /**\n * Names the sidebar landmark.\n *\n * Two `complementary` landmarks with the same name cannot be told apart, and\n * a shell rendered inside another page — a preview, a screenshot harness —\n * makes exactly that pair. It is also the only way a non-English app gets a\n * landmark name its readers can read.\n */\n sidebarLabel?: string\n /** Names the navigation landmark inside the sidebar. */\n navLabel?: string\n /** The drawer toggle, closed and open. */\n openLabel?: string\n closeLabel?: string\n children: ReactNode\n}\n\n/**\n * Two columns on a desktop, one column and a drawer on a phone.\n *\n * The drawer closes on Escape as well as on the scrim, because a drawer that\n * only closes by tapping outside it strands a keyboard user inside a menu they\n * cannot leave. The scrim is a `<button>` for the same reason — a `<div>` with\n * an `onClick` is not reachable by keyboard and not announced as anything.\n *\n * Below `md` the closed drawer is `inert`. Sliding it off-screen is a visual\n * state and nothing more: without that attribute every link in it stays\n * focusable and stays in the accessibility tree, so Tab from the toggle walks\n * into a menu nobody can see. Closing it also returns focus to the toggle,\n * because the element focus was on is the element that just left.\n *\n * @example\n * <AppShell brand={<Wordmark />} sidebar={<NavItem href=\"/\">Home</NavItem>}>\n * <Card>…</Card>\n * </AppShell>\n */\nexport function AppShell({\n sidebar,\n topbar,\n brand,\n contentAs: Content = 'main',\n sidebarLabel = 'Sidebar',\n navLabel = 'Primary',\n openLabel = 'Open navigation',\n closeLabel = 'Close navigation',\n children,\n className,\n ...rest\n}: AppShellProps) {\n const [open, setOpen] = useState(false)\n const sidebarId = useId()\n const toggle = useRef<HTMLButtonElement>(null)\n const isDrawer = useIsDrawer()\n\n /**\n * Both ways out, so both return focus.\n *\n * Escape used to leave the reader standing on a link that had just slid off\n * screen; the scrim is worse, because the scrim itself is the focused element\n * and it unmounts, dropping focus to `<body>`. Either way the drawer is about\n * to become inert, and focus inside an inert subtree is focus the browser\n * throws away.\n */\n const close = useCallback(() => {\n setOpen(false)\n toggle.current?.focus()\n }, [])\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') close()\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [open, close])\n\n return (\n <div\n className={cn('min-h-svh bg-(--paper) md:grid md:grid-cols-[15rem_1fr]', className)}\n {...rest}\n >\n {open && (\n <button\n type=\"button\"\n aria-label={closeLabel}\n onClick={close}\n className=\"fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden\"\n />\n )}\n\n <aside\n id={sidebarId}\n aria-label={sidebarLabel}\n // Only below `md`, and only while it is shut. Above the breakpoint this\n // element is the page's navigation column, and inert would delete it.\n inert={isDrawer && !open}\n // The transition is motion like any other, and the reduced-motion rule\n // in keyframes.css matches on this attribute.\n data-m22-animated\n className={cn(\n 'fixed inset-y-0 start-0 z-(--z-modal) flex w-60 flex-col border-e border-(--rule) bg-(--paper) transition-transform duration-(--duration-slow) ease-(--ease-out-expo)',\n // The drawer slides in from the edge reading STARTS at, so in an\n // right-to-left document it comes from the right.\n open ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full',\n 'md:static md:z-auto md:translate-x-0',\n )}\n >\n {brand && (\n <div className=\"flex h-14 items-center border-b border-(--rule) px-5\">{brand}</div>\n )}\n <nav\n aria-label={navLabel}\n className=\"flex flex-1 flex-col gap-1 overflow-y-auto p-3 scroll-slim\"\n >\n {sidebar}\n </nav>\n </aside>\n\n <div className=\"flex min-w-0 flex-col\">\n <header className=\"sticky top-0 z-(--z-sticky) flex h-14 items-center gap-3 border-b border-(--rule) bg-(--paper)/85 px-(--page-pad) backdrop-blur\">\n <button\n type=\"button\"\n aria-label={open ? closeLabel : openLabel}\n aria-expanded={open}\n aria-controls={sidebarId}\n ref={toggle}\n onClick={() => setOpen((previous) => !previous)}\n className=\"-ms-2.5 grid size-11 place-items-center rounded-(--radius) text-(--ink-2) transition-colors duration-(--duration-fast) hover:text-(--ink) md:hidden\"\n >\n {open ? <X size={20} strokeWidth={1.5} aria-hidden /> : <Menu size={20} strokeWidth={1.5} aria-hidden />}\n </button>\n {topbar}\n </header>\n\n <Content className=\"mx-auto w-full max-w-(--w-page) px-(--page-pad) py-8\">{children}</Content>\n </div>\n </div>\n )\n}\n\nexport default AppShell\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["'use client'\n\nimport { Menu, X } from 'lucide-react'\nimport { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * The width the sidebar stops being a drawer at — `--breakpoint-md`, the same\n * 768px every `md:` variant below is compiled against. Written out rather than\n * read from the token, because a media query string is not a custom property\n * and `matchMedia` cannot resolve one.\n */\nconst DESKTOP = '(min-width: 768px)'\n\n/** True where nothing can answer the question: a server, or a DOM without it. */\nfunction cannotMeasure(): boolean {\n return typeof window === 'undefined' || typeof window.matchMedia !== 'function'\n}\n\nfunction subscribeToWidth(onChange: () => void): () => void {\n if (cannotMeasure()) return () => {}\n const list = window.matchMedia(DESKTOP)\n list.addEventListener('change', onChange)\n return () => list.removeEventListener('change', onChange)\n}\n\n/**\n * Falls back to \"not a drawer\", which is the safe answer rather than the likely\n * one: an environment that cannot measure must not take an entire application's\n * navigation out of the accessibility tree on a guess.\n */\nfunction readIsDrawer(): boolean {\n if (cannotMeasure()) return false\n return !window.matchMedia(DESKTOP).matches\n}\n\nfunction readIsDrawerOnServer(): boolean {\n return false\n}\n\n/**\n * True while the sidebar is a drawer rather than a column.\n *\n * `open` alone cannot answer this. It is false on a desktop too, where the\n * sidebar is a visible grid column — so anything keyed on `open` by itself\n * would hide the navigation of every desktop app in the package.\n *\n * `useSyncExternalStore` rather than an effect: the server has no viewport, so\n * it renders the desktop answer and React reconciles the real one on hydration\n * instead of the two silently disagreeing.\n */\nfunction useIsDrawer(): boolean {\n return useSyncExternalStore(subscribeToWidth, readIsDrawer, readIsDrawerOnServer)\n}\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Navigation content for the sidebar (e.g. a stack of `NavItem`s). */\n sidebar: ReactNode\n /** Optional content for the sticky topbar, laid out after the mobile toggle. */\n topbar?: ReactNode\n /** Optional brand lockup pinned to the top of the sidebar. */\n brand?: ReactNode\n /**\n * Which element the content well renders as.\n *\n * `main` is right for the shell of an application, and is the default. A\n * document may contain exactly one `main` landmark, so an AppShell rendered\n * INSIDE another page — a preview on a documentation site, a screenshot\n * harness — must pass `div`, or the page has two and assistive tech can no\n * longer answer \"where is the content\".\n */\n contentAs?: 'main' | 'div'\n /**\n * Names the sidebar landmark.\n *\n * Two `complementary` landmarks with the same name cannot be told apart, and\n * a shell rendered inside another page — a preview, a screenshot harness —\n * makes exactly that pair. It is also the only way a non-English app gets a\n * landmark name its readers can read.\n */\n sidebarLabel?: string\n /** Names the navigation landmark inside the sidebar. */\n navLabel?: string\n /** The drawer toggle, closed and open. */\n openLabel?: string\n closeLabel?: string\n children: ReactNode\n}\n\n/**\n * Two columns on a desktop, one column and a drawer on a phone.\n *\n * The drawer closes on Escape as well as on the scrim, because a drawer that\n * only closes by tapping outside it strands a keyboard user inside a menu they\n * cannot leave. The scrim is a `<button>` for the same reason — a `<div>` with\n * an `onClick` is not reachable by keyboard and not announced as anything.\n *\n * Below `md` the closed drawer is `inert`. Sliding it off-screen is a visual\n * state and nothing more: without that attribute every link in it stays\n * focusable and stays in the accessibility tree, so Tab from the toggle walks\n * into a menu nobody can see. Closing it also returns focus to the toggle,\n * because the element focus was on is the element that just left.\n *\n * @example\n * <AppShell brand={<Wordmark />} sidebar={<NavItem href=\"/\">Home</NavItem>}>\n * <Card>…</Card>\n * </AppShell>\n */\nexport function AppShell({\n sidebar,\n topbar,\n brand,\n contentAs: Content = 'main',\n sidebarLabel = 'Sidebar',\n navLabel = 'Primary',\n openLabel = 'Open navigation',\n closeLabel = 'Close navigation',\n children,\n className,\n ...rest\n}: AppShellProps) {\n const [open, setOpen] = useState(false)\n const sidebarId = useId()\n const toggle = useRef<HTMLButtonElement>(null)\n const isDrawer = useIsDrawer()\n\n /**\n * Both ways out, so both return focus.\n *\n * Escape used to leave the reader standing on a link that had just slid off\n * screen; the scrim is worse, because the scrim itself is the focused element\n * and it unmounts, dropping focus to `<body>`. Either way the drawer is about\n * to become inert, and focus inside an inert subtree is focus the browser\n * throws away.\n */\n const close = useCallback(() => {\n setOpen(false)\n toggle.current?.focus()\n }, [])\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') close()\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [open, close])\n\n return (\n <div\n className={cn('min-h-svh bg-(--paper) md:grid md:grid-cols-[15rem_1fr]', className)}\n {...rest}\n >\n {open && (\n <button\n type=\"button\"\n aria-label={closeLabel}\n onClick={close}\n className=\"fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden\"\n />\n )}\n\n <aside\n id={sidebarId}\n aria-label={sidebarLabel}\n // Only below `md`, and only while it is shut. Above the breakpoint this\n // element is the page's navigation column, and inert would delete it.\n inert={isDrawer && !open}\n // The transition is motion like any other, and the reduced-motion rule\n // in keyframes.css matches on this attribute.\n data-m22-animated\n className={cn(\n 'fixed inset-y-0 start-0 z-(--z-modal) flex w-60 flex-col border-e border-(--rule) bg-(--paper) transition-transform duration-(--duration-slow) ease-(--ease-out-expo)',\n // The drawer slides in from the edge reading STARTS at, so in an\n // right-to-left document it comes from the right.\n open ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full',\n 'md:static md:z-auto md:translate-x-0',\n )}\n >\n {brand && (\n <div className=\"flex h-(--bar-h) items-center border-b border-(--rule) px-5\">{brand}</div>\n )}\n <nav\n aria-label={navLabel}\n className=\"flex flex-1 flex-col gap-1 overflow-y-auto p-3 scroll-slim\"\n >\n {sidebar}\n </nav>\n </aside>\n\n <div className=\"flex min-w-0 flex-col\">\n <header className=\"sticky top-0 z-(--z-sticky) flex h-(--bar-h) items-center gap-3 border-b border-(--rule) bg-(--paper)/85 px-(--page-pad) backdrop-blur\">\n <button\n type=\"button\"\n aria-label={open ? closeLabel : openLabel}\n aria-expanded={open}\n aria-controls={sidebarId}\n ref={toggle}\n onClick={() => setOpen((previous) => !previous)}\n className=\"-ms-2.5 grid size-11 place-items-center rounded-(--radius) text-(--ink-2) transition-colors duration-(--duration-fast) hover:text-(--ink) md:hidden\"\n >\n {open ? <X size={20} strokeWidth={1.5} aria-hidden /> : <Menu size={20} strokeWidth={1.5} aria-hidden />}\n </button>\n {topbar}\n </header>\n\n <Content className=\"mx-auto w-full max-w-(--w-page) px-(--page-pad) py-8\">{children}</Content>\n </div>\n </div>\n )\n}\n\nexport default AppShell\n"],"mappings":"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","names":[]}
|
|
@@ -13,9 +13,9 @@ const VARIANT = {
|
|
|
13
13
|
danger: "bg-(--danger) text-(--paper) border border-(--danger) hover:opacity-85"
|
|
14
14
|
};
|
|
15
15
|
const SIZE = {
|
|
16
|
-
sm: "min-h-(--control-h-sm) px-(--control-px-sm) py-(--control-py-sm) text-[13px]",
|
|
17
|
-
md: "min-h-(--control-h-md) px-(--control-px-md) py-(--control-py-md) text-sm",
|
|
18
|
-
lg: "min-h-(--control-h-lg) px-(--control-px-lg) py-(--control-py-lg) text-[15px]"
|
|
16
|
+
sm: "min-h-(--control-h-sm) px-(--control-px-sm) py-(--control-py-sm) text-[13px] leading-(--control-lh)",
|
|
17
|
+
md: "min-h-(--control-h-md) px-(--control-px-md) py-(--control-py-md) text-sm leading-(--control-lh)",
|
|
18
|
+
lg: "min-h-(--control-h-lg) px-(--control-px-lg) py-(--control-py-lg) text-[15px] leading-(--control-lh)"
|
|
19
19
|
};
|
|
20
20
|
const ICON_SIZE = {
|
|
21
21
|
sm: "size-(--control-h-sm) p-0",
|