@misoto22/design 0.7.0 → 0.8.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 +87 -0
- package/CLAUDE.md +1 -0
- package/README.md +50 -5
- package/bin/misoto22-design.mjs +59 -13
- package/dist/agent/Accordion.md +24 -1
- package/dist/agent/Alert.md +23 -1
- package/dist/agent/AppShell.md +30 -2
- package/dist/agent/ArchitectureFigure.md +25 -2
- package/dist/agent/AreaChart.md +27 -2
- package/dist/agent/Article.md +26 -3
- package/dist/agent/AspectRatio.md +57 -0
- package/dist/agent/Avatar.md +25 -5
- package/dist/agent/Badge.md +22 -1
- package/dist/agent/BarChart.md +29 -2
- package/dist/agent/BarList.md +27 -1
- package/dist/agent/BigNumber.md +26 -2
- package/dist/agent/BoxPlot.md +24 -1
- package/dist/agent/Breadcrumb.md +24 -2
- package/dist/agent/BulletChart.md +23 -1
- package/dist/agent/Button.md +25 -1
- package/dist/agent/Calendar.md +37 -3
- package/dist/agent/Card.md +24 -2
- package/dist/agent/Checkbox.md +23 -2
- package/dist/agent/Code.md +50 -0
- package/dist/agent/CodeBlock.md +74 -0
- package/dist/agent/Collapsible.md +22 -1
- package/dist/agent/Combobox.md +29 -3
- package/dist/agent/Command.md +25 -2
- package/dist/agent/ComposedChart.md +26 -2
- package/dist/agent/ContextMenu.md +37 -4
- package/dist/agent/DataflowFigure.md +22 -1
- package/dist/agent/DatePicker.md +34 -6
- package/dist/agent/DescriptionList.md +68 -0
- package/dist/agent/Diagram.md +27 -3
- package/dist/agent/DiagramCanvas.md +24 -1
- package/dist/agent/DiagramExportMenu.md +29 -3
- package/dist/agent/DiagramInspector.md +23 -1
- package/dist/agent/DiagramLegend.md +22 -1
- package/dist/agent/DiagramMinimap.md +24 -4
- package/dist/agent/DiagramToolbar.md +23 -1
- package/dist/agent/Dialog.md +26 -3
- package/dist/agent/DropdownMenu.md +45 -6
- package/dist/agent/EmptyState.md +26 -1
- package/dist/agent/ErrorState.md +27 -2
- package/dist/agent/Facet.md +25 -1
- package/dist/agent/Field.md +45 -4
- package/dist/agent/FigureBand.md +24 -1
- package/dist/agent/FloatingIconButton.md +23 -1
- package/dist/agent/FunnelChart.md +23 -1
- package/dist/agent/Heading.md +64 -0
- package/dist/agent/Heatmap.md +25 -1
- package/dist/agent/Histogram.md +26 -2
- package/dist/agent/Input.md +22 -1
- package/dist/agent/Kbd.md +19 -1
- package/dist/agent/LifecycleFigure.md +22 -1
- package/dist/agent/LineChart.md +26 -2
- package/dist/agent/LinkArrow.md +20 -1
- package/dist/agent/Markdown.md +68 -0
- package/dist/agent/NativeSelect.md +23 -2
- package/dist/agent/NavItem.md +21 -1
- package/dist/agent/Pagination.md +28 -2
- package/dist/agent/PieChart.md +26 -2
- package/dist/agent/Popover.md +23 -1
- package/dist/agent/Progress.md +27 -3
- package/dist/agent/RadarChart.md +26 -2
- package/dist/agent/RadialChart.md +26 -3
- package/dist/agent/RadioGroup.md +23 -2
- package/dist/agent/SankeyChart.md +25 -2
- package/dist/agent/ScatterChart.md +27 -2
- package/dist/agent/ScrollArea.md +24 -3
- package/dist/agent/SearchableMenu.md +23 -1
- package/dist/agent/Select.md +29 -3
- package/dist/agent/Separator.md +28 -4
- package/dist/agent/SequenceFigure.md +23 -1
- package/dist/agent/Sheet.md +25 -2
- package/dist/agent/Skeleton.md +24 -2
- package/dist/agent/Slider.md +27 -4
- package/dist/agent/Sparkline.md +23 -1
- package/dist/agent/Spinner.md +24 -2
- package/dist/agent/StatusDot.md +20 -1
- package/dist/agent/StatusPill.md +28 -2
- package/dist/agent/Steps.md +24 -1
- package/dist/agent/Switch.md +21 -1
- package/dist/agent/Table.md +26 -3
- package/dist/agent/Tabs.md +24 -2
- package/dist/agent/Tag.md +40 -9
- package/dist/agent/Text.md +65 -0
- package/dist/agent/Textarea.md +20 -1
- package/dist/agent/Timestamp.md +64 -0
- package/dist/agent/Toast.md +26 -2
- package/dist/agent/ToggleGroup.md +24 -2
- package/dist/agent/Toolbar.md +71 -0
- package/dist/agent/Tooltip.md +23 -1
- package/dist/agent/TreemapChart.md +24 -1
- package/dist/agent/WaterfallChart.md +24 -1
- package/dist/agent/WorkflowFigure.md +24 -2
- package/dist/agent/catalog.json +5602 -745
- package/dist/agent/index.md +13 -4
- package/dist/article.css +16 -4
- package/dist/charts/AreaChart/AreaChart.d.ts +9 -2
- package/dist/charts/AreaChart/AreaChart.js +12 -13
- package/dist/charts/AreaChart/AreaChart.js.map +1 -1
- package/dist/charts/BarChart/BarChart.d.ts +14 -3
- package/dist/charts/BarChart/BarChart.js +20 -12
- package/dist/charts/BarChart/BarChart.js.map +1 -1
- package/dist/charts/BarList/BarList.d.ts +9 -1
- package/dist/charts/BarList/BarList.js +9 -1
- package/dist/charts/BarList/BarList.js.map +1 -1
- package/dist/charts/BigNumber/BigNumber.d.ts +19 -3
- package/dist/charts/BigNumber/BigNumber.js +25 -5
- package/dist/charts/BigNumber/BigNumber.js.map +1 -1
- package/dist/charts/BoxPlot/BoxPlot.js +4 -2
- package/dist/charts/BoxPlot/BoxPlot.js.map +1 -1
- package/dist/charts/BulletChart/BulletChart.js +28 -7
- package/dist/charts/BulletChart/BulletChart.js.map +1 -1
- package/dist/charts/ComposedChart/ComposedChart.d.ts +9 -2
- package/dist/charts/ComposedChart/ComposedChart.js +10 -10
- package/dist/charts/ComposedChart/ComposedChart.js.map +1 -1
- package/dist/charts/FunnelChart/FunnelChart.d.ts +7 -2
- package/dist/charts/FunnelChart/FunnelChart.js +6 -16
- package/dist/charts/FunnelChart/FunnelChart.js.map +1 -1
- package/dist/charts/Heatmap/Heatmap.d.ts +17 -3
- package/dist/charts/Heatmap/Heatmap.js +20 -11
- package/dist/charts/Heatmap/Heatmap.js.map +1 -1
- package/dist/charts/Histogram/Histogram.d.ts +24 -1
- package/dist/charts/Histogram/Histogram.js +43 -11
- package/dist/charts/Histogram/Histogram.js.map +1 -1
- package/dist/charts/LineChart/LineChart.d.ts +9 -2
- package/dist/charts/LineChart/LineChart.js +11 -12
- package/dist/charts/LineChart/LineChart.js.map +1 -1
- package/dist/charts/PieChart/PieChart.d.ts +15 -3
- package/dist/charts/PieChart/PieChart.js +14 -6
- package/dist/charts/PieChart/PieChart.js.map +1 -1
- package/dist/charts/RadarChart/RadarChart.d.ts +15 -3
- package/dist/charts/RadarChart/RadarChart.js +11 -9
- package/dist/charts/RadarChart/RadarChart.js.map +1 -1
- package/dist/charts/RadialChart/RadialChart.d.ts +22 -4
- package/dist/charts/RadialChart/RadialChart.js +28 -6
- package/dist/charts/RadialChart/RadialChart.js.map +1 -1
- package/dist/charts/SankeyChart/SankeyChart.d.ts +16 -4
- package/dist/charts/SankeyChart/SankeyChart.js +14 -6
- package/dist/charts/SankeyChart/SankeyChart.js.map +1 -1
- package/dist/charts/ScatterChart/ScatterChart.d.ts +24 -3
- package/dist/charts/ScatterChart/ScatterChart.js +21 -15
- package/dist/charts/ScatterChart/ScatterChart.js.map +1 -1
- package/dist/charts/Sparkline/Sparkline.js +5 -3
- package/dist/charts/Sparkline/Sparkline.js.map +1 -1
- package/dist/charts/TreemapChart/TreemapChart.d.ts +7 -2
- package/dist/charts/TreemapChart/TreemapChart.js +20 -11
- package/dist/charts/TreemapChart/TreemapChart.js.map +1 -1
- package/dist/charts/WaterfallChart/WaterfallChart.js +4 -2
- package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -1
- package/dist/charts/lib/export.d.ts +1 -0
- package/dist/charts/lib/figure.d.ts +21 -1
- package/dist/charts/lib/figure.js +5 -2
- package/dist/charts/lib/figure.js.map +1 -1
- package/dist/charts/lib/scale.d.ts +23 -0
- package/dist/charts/lib/scale.js +13 -0
- package/dist/charts/lib/scale.js.map +1 -0
- package/dist/charts/lib/selection.d.ts +20 -0
- package/dist/charts/lib/selection.js +18 -0
- package/dist/charts/lib/selection.js.map +1 -0
- package/dist/charts/lib/toolbar.d.ts +1 -0
- package/dist/components/Accordion/Accordion.js +8 -1
- package/dist/components/Accordion/Accordion.js.map +1 -1
- package/dist/components/AppShell/AppShell.d.ts +6 -0
- package/dist/components/AppShell/AppShell.js +34 -4
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/Article/Article.d.ts +11 -4
- package/dist/components/Article/Article.js +13 -0
- package/dist/components/Article/Article.js.map +1 -1
- package/dist/components/AspectRatio/AspectRatio.d.ts +46 -0
- package/dist/components/AspectRatio/AspectRatio.js +33 -0
- package/dist/components/AspectRatio/AspectRatio.js.map +1 -0
- package/dist/components/Avatar/Avatar.d.ts +12 -3
- package/dist/components/Avatar/Avatar.js +6 -1
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +6 -0
- package/dist/components/Breadcrumb/Breadcrumb.js +15 -0
- package/dist/components/Breadcrumb/Breadcrumb.js.map +1 -1
- package/dist/components/Button/Button.js +14 -0
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.d.ts +29 -3
- package/dist/components/Calendar/Calendar.js +60 -12
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Card/Card.d.ts +6 -0
- package/dist/components/Card/Card.js +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts +5 -2
- package/dist/components/Checkbox/Checkbox.js +12 -2
- package/dist/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/components/Code/Code.d.ts +30 -0
- package/dist/components/Code/Code.js +21 -0
- package/dist/components/Code/Code.js.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.d.ts +141 -0
- package/dist/components/CodeBlock/CodeBlock.js +123 -0
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -0
- package/dist/components/Collapsible/Collapsible.js +10 -2
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/Combobox/Combobox.d.ts +15 -1
- package/dist/components/Combobox/Combobox.js +25 -5
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.d.ts +4 -2
- package/dist/components/Command/Command.js +13 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.d.ts +30 -5
- package/dist/components/ContextMenu/ContextMenu.js +32 -2
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.d.ts +36 -4
- package/dist/components/DatePicker/DatePicker.js +55 -12
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.d.ts +73 -0
- package/dist/components/DescriptionList/DescriptionList.js +35 -0
- package/dist/components/DescriptionList/DescriptionList.js.map +1 -0
- package/dist/components/Diagram/Diagram.d.ts +8 -3
- package/dist/components/Diagram/Diagram.js +82 -10
- package/dist/components/Diagram/Diagram.js.map +1 -1
- package/dist/components/Dialog/Dialog.d.ts +18 -3
- package/dist/components/Dialog/Dialog.js +20 -2
- package/dist/components/Dialog/Dialog.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.d.ts +49 -11
- package/dist/components/DropdownMenu/DropdownMenu.js +32 -2
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/EmptyState/EmptyState.d.ts +25 -1
- package/dist/components/EmptyState/EmptyState.js +3 -1
- package/dist/components/EmptyState/EmptyState.js.map +1 -1
- package/dist/components/ErrorState/ErrorState.d.ts +22 -1
- package/dist/components/ErrorState/ErrorState.js +11 -2
- package/dist/components/ErrorState/ErrorState.js.map +1 -1
- package/dist/components/Field/Field.d.ts +70 -3
- package/dist/components/Field/Field.js +53 -12
- package/dist/components/Field/Field.js.map +1 -1
- package/dist/components/Field/field-control.d.ts +31 -0
- package/dist/components/Field/field-control.js +12 -0
- package/dist/components/Field/field-control.js.map +1 -0
- package/dist/components/FloatingIconButton/FloatingIconButton.js +2 -0
- package/dist/components/FloatingIconButton/FloatingIconButton.js.map +1 -1
- package/dist/components/Heading/Heading.d.ts +62 -0
- package/dist/components/Heading/Heading.js +44 -0
- package/dist/components/Heading/Heading.js.map +1 -0
- package/dist/components/Markdown/Markdown.d.ts +139 -0
- package/dist/components/Markdown/Markdown.js +135 -0
- package/dist/components/Markdown/Markdown.js.map +1 -0
- package/dist/components/Markdown/inline.d.ts +29 -0
- package/dist/components/Markdown/inline.js +74 -0
- package/dist/components/Markdown/inline.js.map +1 -0
- package/dist/components/Markdown/nodes.d.ts +65 -0
- package/dist/components/Markdown/nodes.js +1 -0
- package/dist/components/Markdown/nodes.js.map +1 -0
- package/dist/components/Markdown/parse.d.ts +24 -0
- package/dist/components/Markdown/parse.js +110 -0
- package/dist/components/Markdown/parse.js.map +1 -0
- package/dist/components/Markdown/slug.d.ts +15 -0
- package/dist/components/Markdown/slug.js +7 -0
- package/dist/components/Markdown/slug.js.map +1 -0
- package/dist/components/NativeSelect/NativeSelect.d.ts +6 -0
- package/dist/components/NativeSelect/NativeSelect.js +2 -3
- package/dist/components/NativeSelect/NativeSelect.js.map +1 -1
- package/dist/components/Pagination/Pagination.d.ts +24 -1
- package/dist/components/Pagination/Pagination.js +6 -3
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/Progress/Progress.d.ts +17 -5
- package/dist/components/Progress/Progress.js +18 -6
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +6 -0
- package/dist/components/RadioGroup/RadioGroup.js +11 -1
- package/dist/components/RadioGroup/RadioGroup.js.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.d.ts +15 -0
- package/dist/components/ScrollArea/ScrollArea.js +2 -2
- package/dist/components/ScrollArea/ScrollArea.js.map +1 -1
- package/dist/components/SearchableMenu/SearchableMenu.d.ts +11 -2
- package/dist/components/SearchableMenu/SearchableMenu.js +24 -2
- package/dist/components/SearchableMenu/SearchableMenu.js.map +1 -1
- package/dist/components/Select/Select.d.ts +18 -2
- package/dist/components/Select/Select.js +20 -3
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Separator/Separator.d.ts +21 -3
- package/dist/components/Separator/Separator.js +9 -1
- package/dist/components/Separator/Separator.js.map +1 -1
- package/dist/components/Sheet/Sheet.d.ts +3 -0
- package/dist/components/Sheet/Sheet.js +11 -2
- package/dist/components/Sheet/Sheet.js.map +1 -1
- package/dist/components/Skeleton/Skeleton.d.ts +8 -1
- package/dist/components/Skeleton/Skeleton.js +1 -1
- package/dist/components/Skeleton/Skeleton.js.map +1 -1
- package/dist/components/Slider/Slider.d.ts +21 -3
- package/dist/components/Slider/Slider.js +54 -27
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Spinner/Spinner.d.ts +8 -0
- package/dist/components/Spinner/Spinner.js +3 -2
- package/dist/components/Spinner/Spinner.js.map +1 -1
- package/dist/components/StatusPill/StatusPill.d.ts +5 -0
- package/dist/components/StatusPill/StatusPill.js +6 -0
- package/dist/components/StatusPill/StatusPill.js.map +1 -1
- package/dist/components/Table/Table.d.ts +6 -0
- package/dist/components/Table/Table.js +3 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tag/Tag.d.ts +68 -9
- package/dist/components/Tag/Tag.js +50 -10
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Text/Text.d.ts +62 -0
- package/dist/components/Text/Text.js +30 -0
- package/dist/components/Text/Text.js.map +1 -0
- package/dist/components/Timestamp/Timestamp.d.ts +59 -0
- package/dist/components/Timestamp/Timestamp.js +56 -0
- package/dist/components/Timestamp/Timestamp.js.map +1 -0
- package/dist/components/Toast/Toast.d.ts +9 -1
- package/dist/components/Toast/Toast.js +27 -4
- package/dist/components/Toast/Toast.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +6 -1
- package/dist/components/ToggleGroup/ToggleGroup.js +11 -1
- package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
- package/dist/components/Toolbar/Toolbar.d.ts +55 -0
- package/dist/components/Toolbar/Toolbar.js +43 -0
- package/dist/components/Toolbar/Toolbar.js.map +1 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.d.ts +6 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js +48 -8
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js.map +1 -1
- package/dist/diagrams/DataflowFigure/DataflowFigure.js +21 -2
- package/dist/diagrams/DataflowFigure/DataflowFigure.js.map +1 -1
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.d.ts +14 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js +14 -7
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js.map +1 -1
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.d.ts +38 -4
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js +14 -16
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js.map +1 -1
- package/dist/diagrams/DiagramInspector/DiagramInspector.js +2 -2
- package/dist/diagrams/DiagramInspector/DiagramInspector.js.map +1 -1
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.d.ts +23 -7
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js +45 -18
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js.map +1 -1
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js +44 -12
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js.map +1 -1
- package/dist/diagrams/SequenceFigure/SequenceFigure.js +35 -5
- package/dist/diagrams/SequenceFigure/SequenceFigure.js.map +1 -1
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.d.ts +3 -1
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js +76 -28
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js.map +1 -1
- package/dist/diagrams/index.d.ts +1 -0
- package/dist/diagrams/index.js +10 -0
- package/dist/diagrams/index.js.map +1 -1
- package/dist/diagrams/lib/dev.d.ts +61 -0
- package/dist/diagrams/lib/dev.js +96 -0
- package/dist/diagrams/lib/dev.js.map +1 -0
- package/dist/diagrams/lib/frame.d.ts +48 -7
- package/dist/diagrams/lib/frame.js +17 -2
- package/dist/diagrams/lib/frame.js.map +1 -1
- package/dist/diagrams/lib/wires.js.map +1 -1
- package/dist/diagrams/spec.d.ts +64 -12
- package/dist/index.d.ts +16 -4
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/keyframes.css +57 -3
- package/dist/lib/overlay-container.d.ts +14 -4
- package/dist/lib/overlay-container.js.map +1 -1
- package/dist/lib/warn.d.ts +73 -0
- package/dist/lib/warn.js +38 -0
- package/dist/lib/warn.js.map +1 -0
- package/dist/semantic.css +17 -6
- package/dist/styles.css +1 -1
- package/dist/tokens.css +20 -2
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +39 -36
- package/dist/tokens.json +39 -36
- package/llms.txt +35 -0
- package/package.json +5 -2
- package/skills/misoto22-design/evals/evals.json +84 -0
- package/skills/misoto22-design/rules/a11y.md +55 -3
- package/skills/misoto22-design/rules/forms.md +5 -0
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Menu, X } from "lucide-react";
|
|
4
|
-
import { useEffect, useId, useState } from "react";
|
|
4
|
+
import { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
|
+
const DESKTOP = "(min-width: 768px)";
|
|
7
|
+
function cannotMeasure() {
|
|
8
|
+
return typeof window === "undefined" || typeof window.matchMedia !== "function";
|
|
9
|
+
}
|
|
10
|
+
function subscribeToWidth(onChange) {
|
|
11
|
+
if (cannotMeasure()) return () => {
|
|
12
|
+
};
|
|
13
|
+
const list = window.matchMedia(DESKTOP);
|
|
14
|
+
list.addEventListener("change", onChange);
|
|
15
|
+
return () => list.removeEventListener("change", onChange);
|
|
16
|
+
}
|
|
17
|
+
function readIsDrawer() {
|
|
18
|
+
if (cannotMeasure()) return false;
|
|
19
|
+
return !window.matchMedia(DESKTOP).matches;
|
|
20
|
+
}
|
|
21
|
+
function readIsDrawerOnServer() {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function useIsDrawer() {
|
|
25
|
+
return useSyncExternalStore(subscribeToWidth, readIsDrawer, readIsDrawerOnServer);
|
|
26
|
+
}
|
|
6
27
|
function AppShell({
|
|
7
28
|
sidebar,
|
|
8
29
|
topbar,
|
|
@@ -18,14 +39,20 @@ function AppShell({
|
|
|
18
39
|
}) {
|
|
19
40
|
const [open, setOpen] = useState(false);
|
|
20
41
|
const sidebarId = useId();
|
|
42
|
+
const toggle = useRef(null);
|
|
43
|
+
const isDrawer = useIsDrawer();
|
|
44
|
+
const close = useCallback(() => {
|
|
45
|
+
setOpen(false);
|
|
46
|
+
toggle.current?.focus();
|
|
47
|
+
}, []);
|
|
21
48
|
useEffect(() => {
|
|
22
49
|
if (!open) return;
|
|
23
50
|
const onKey = (event) => {
|
|
24
|
-
if (event.key === "Escape")
|
|
51
|
+
if (event.key === "Escape") close();
|
|
25
52
|
};
|
|
26
53
|
document.addEventListener("keydown", onKey);
|
|
27
54
|
return () => document.removeEventListener("keydown", onKey);
|
|
28
|
-
}, [open]);
|
|
55
|
+
}, [open, close]);
|
|
29
56
|
return /* @__PURE__ */ jsxs(
|
|
30
57
|
"div",
|
|
31
58
|
{
|
|
@@ -37,7 +64,7 @@ function AppShell({
|
|
|
37
64
|
{
|
|
38
65
|
type: "button",
|
|
39
66
|
"aria-label": closeLabel,
|
|
40
|
-
onClick:
|
|
67
|
+
onClick: close,
|
|
41
68
|
className: "fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden"
|
|
42
69
|
}
|
|
43
70
|
),
|
|
@@ -46,6 +73,8 @@ function AppShell({
|
|
|
46
73
|
{
|
|
47
74
|
id: sidebarId,
|
|
48
75
|
"aria-label": sidebarLabel,
|
|
76
|
+
inert: isDrawer && !open,
|
|
77
|
+
"data-m22-animated": true,
|
|
49
78
|
className: cn(
|
|
50
79
|
"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)",
|
|
51
80
|
// The drawer slides in from the edge reading STARTS at, so in an
|
|
@@ -75,6 +104,7 @@ function AppShell({
|
|
|
75
104
|
"aria-label": open ? closeLabel : openLabel,
|
|
76
105
|
"aria-expanded": open,
|
|
77
106
|
"aria-controls": sidebarId,
|
|
107
|
+
ref: toggle,
|
|
78
108
|
onClick: () => setOpen((previous) => !previous),
|
|
79
109
|
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",
|
|
80
110
|
children: open ? /* @__PURE__ */ jsx(X, { size: 20, strokeWidth: 1.5, "aria-hidden": true }) : /* @__PURE__ */ jsx(Menu, { size: 20, strokeWidth: 1.5, "aria-hidden": true })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["'use client'\n\nimport { Menu, X } from 'lucide-react'\nimport { useEffect, useId, useState } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\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 * @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\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false)\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [open])\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={() => setOpen(false)}\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 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 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":";AAqFQ,cAQF,YARE;AAnFR,SAAS,MAAM,SAAS;AACxB,SAAS,WAAW,OAAO,gBAAgB;AAE3C,SAAS,UAAU;AAiDZ,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,UAAU;AAAA,EACrB,eAAe;AAAA,EACf,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,YAAY,MAAM;AAExB,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,CAAC,UAAyB;AACtC,UAAI,MAAM,QAAQ,SAAU,SAAQ,KAAK;AAAA,IAC3C;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAC,IAAI,CAAC;AAET,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,2DAA2D,SAAS;AAAA,MACjF,GAAG;AAAA,MAEH;AAAA,gBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY;AAAA,YACZ,SAAS,MAAM,QAAQ,KAAK;AAAA,YAC5B,WAAU;AAAA;AAAA,QACZ;AAAA,QAGF;AAAA,UAAC;AAAA;AAAA,YACC,IAAI;AAAA,YACJ,cAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA;AAAA;AAAA,cAGA,OAAO,kBAAkB;AAAA,cACzB;AAAA,YACF;AAAA,YAEC;AAAA,uBACC,oBAAC,SAAI,WAAU,wDAAwD,iBAAM;AAAA,cAE/E;AAAA,gBAAC;AAAA;AAAA,kBACC,cAAY;AAAA,kBACZ,WAAU;AAAA,kBAET;AAAA;AAAA,cACH;AAAA;AAAA;AAAA,QACF;AAAA,QAEA,qBAAC,SAAI,WAAU,yBACb;AAAA,+BAAC,YAAO,WAAU,mIAChB;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO,aAAa;AAAA,gBAChC,iBAAe;AAAA,gBACf,iBAAe;AAAA,gBACf,SAAS,MAAM,QAAQ,CAAC,aAAa,CAAC,QAAQ;AAAA,gBAC9C,WAAU;AAAA,gBAET,iBAAO,oBAAC,KAAE,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC,IAAK,oBAAC,QAAK,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC;AAAA;AAAA,YACxG;AAAA,YACC;AAAA,aACH;AAAA,UAEA,oBAAC,WAAQ,WAAU,wDAAwD,UAAS;AAAA,WACtF;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,mBAAQ;","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-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":";AA4JQ,cAQF,YARE;AA1JR,SAAS,MAAM,SAAS;AACxB,SAAS,aAAa,WAAW,OAAO,QAAQ,UAAU,4BAA4B;AAEtF,SAAS,UAAU;AAQnB,MAAM,UAAU;AAGhB,SAAS,gBAAyB;AAChC,SAAO,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe;AACvE;AAEA,SAAS,iBAAiB,UAAkC;AAC1D,MAAI,cAAc,EAAG,QAAO,MAAM;AAAA,EAAC;AACnC,QAAM,OAAO,OAAO,WAAW,OAAO;AACtC,OAAK,iBAAiB,UAAU,QAAQ;AACxC,SAAO,MAAM,KAAK,oBAAoB,UAAU,QAAQ;AAC1D;AAOA,SAAS,eAAwB;AAC/B,MAAI,cAAc,EAAG,QAAO;AAC5B,SAAO,CAAC,OAAO,WAAW,OAAO,EAAE;AACrC;AAEA,SAAS,uBAAgC;AACvC,SAAO;AACT;AAaA,SAAS,cAAuB;AAC9B,SAAO,qBAAqB,kBAAkB,cAAc,oBAAoB;AAClF;AAuDO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,UAAU;AAAA,EACrB,eAAe;AAAA,EACf,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,YAAY,MAAM;AACxB,QAAM,SAAS,OAA0B,IAAI;AAC7C,QAAM,WAAW,YAAY;AAW7B,QAAM,QAAQ,YAAY,MAAM;AAC9B,YAAQ,KAAK;AACb,WAAO,SAAS,MAAM;AAAA,EACxB,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,CAAC,UAAyB;AACtC,UAAI,MAAM,QAAQ,SAAU,OAAM;AAAA,IACpC;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAC,MAAM,KAAK,CAAC;AAEhB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,2DAA2D,SAAS;AAAA,MACjF,GAAG;AAAA,MAEH;AAAA,gBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY;AAAA,YACZ,SAAS;AAAA,YACT,WAAU;AAAA;AAAA,QACZ;AAAA,QAGF;AAAA,UAAC;AAAA;AAAA,YACC,IAAI;AAAA,YACJ,cAAY;AAAA,YAGZ,OAAO,YAAY,CAAC;AAAA,YAGpB,qBAAiB;AAAA,YACjB,WAAW;AAAA,cACT;AAAA;AAAA;AAAA,cAGA,OAAO,kBAAkB;AAAA,cACzB;AAAA,YACF;AAAA,YAEC;AAAA,uBACC,oBAAC,SAAI,WAAU,wDAAwD,iBAAM;AAAA,cAE/E;AAAA,gBAAC;AAAA;AAAA,kBACC,cAAY;AAAA,kBACZ,WAAU;AAAA,kBAET;AAAA;AAAA,cACH;AAAA;AAAA;AAAA,QACF;AAAA,QAEA,qBAAC,SAAI,WAAU,yBACb;AAAA,+BAAC,YAAO,WAAU,mIAChB;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO,aAAa;AAAA,gBAChC,iBAAe;AAAA,gBACf,iBAAe;AAAA,gBACf,KAAK;AAAA,gBACL,SAAS,MAAM,QAAQ,CAAC,aAAa,CAAC,QAAQ;AAAA,gBAC9C,WAAU;AAAA,gBAET,iBAAO,oBAAC,KAAE,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC,IAAK,oBAAC,QAAK,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC;AAAA;AAAA,YACxG;AAAA,YACC;AAAA,aACH;AAAA,UAEA,oBAAC,WAAQ,WAAU,wDAAwD,UAAS;AAAA,WACtF;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,mBAAQ;","names":[]}
|
|
@@ -32,10 +32,17 @@ interface ArticleProps extends HTMLAttributes<HTMLElement> {
|
|
|
32
32
|
* hand-rolling a prose stylesheet is three prose stylesheets that drift.
|
|
33
33
|
*
|
|
34
34
|
* Composable either way: pass `html` for a rendered string, or children for
|
|
35
|
-
* real elements
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* the
|
|
35
|
+
* real elements — one or the other, never both. `html` wins when both arrive,
|
|
36
|
+
* and says so in development: `html=""` is still `html`, which made losing a
|
|
37
|
+
* page of children a thing that could happen without a word. A post that mixes
|
|
38
|
+
* the two — prose with a `Diagram` dropped into the middle — renders its blocks
|
|
39
|
+
* in order and gives each one an `Article`. Inside one, these rules BEAT a component's utilities:
|
|
40
|
+
* `article.css` is imported unlayered while Tailwind's utilities sit in
|
|
41
|
+
* `@layer utilities`, and an unlayered rule wins over a layered one whatever
|
|
42
|
+
* either one's specificity is. That is the mechanism rather than an accident —
|
|
43
|
+
* it is what lets a `Markdown` paragraph, a `Text` carrying `m-0`, give its
|
|
44
|
+
* margin up to the article's rhythm. A component that has to hold a property
|
|
45
|
+
* inside an article needs an inline style, not a class.
|
|
39
46
|
*
|
|
40
47
|
* @example
|
|
41
48
|
* <Article html={renderedMarkdown} />
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { DEV, warn } from "../../lib/warn.js";
|
|
3
|
+
function rendersSomething(children) {
|
|
4
|
+
return children !== void 0 && children !== null && children !== false && children !== "";
|
|
5
|
+
}
|
|
2
6
|
function Article({ html, as: Comp = "article", className, children, ...rest }) {
|
|
3
7
|
if (html !== void 0) {
|
|
8
|
+
if (DEV && rendersSomething(children)) {
|
|
9
|
+
warn({
|
|
10
|
+
code: "ARTICLE_HTML_AND_CHILDREN",
|
|
11
|
+
problem: "Article was given html and children together, so the children are not rendered at all. html is written with dangerouslySetInnerHTML, and there is no wrapper that could hold both \u2014 the rhythm is a child combinator, so anything put around the rendered string costs every block inside it its spacing.",
|
|
12
|
+
field: "html",
|
|
13
|
+
fix: "Render the two in order as two Articles, or drop whichever of html and children is not the content. An empty html string still counts as html.",
|
|
14
|
+
component: "Article"
|
|
15
|
+
});
|
|
16
|
+
}
|
|
4
17
|
return /* @__PURE__ */ jsx(
|
|
5
18
|
Comp,
|
|
6
19
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Article/Article.tsx"],"sourcesContent":["import type { HTMLAttributes } from 'react'\n\nexport interface ArticleProps extends HTMLAttributes<HTMLElement> {\n /**\n * Rendered HTML from a Markdown or MDX pipeline.\n *\n * Trusted markup only. This sets `dangerouslySetInnerHTML`, so it must come\n * from content you control — a repository's own posts, a CMS you author —\n * and never from a reader. Untrusted Markdown has to be sanitised before it\n * reaches here; that is a pipeline decision, not a component one, and a\n * sanitiser bolted on inside a styling primitive would be the wrong place to\n * make it and the easiest place to get it wrong.\n */\n html?: string\n /** The element to render. `article` by default; pass `div` for a fragment. */\n as?: 'article' | 'div' | 'section'\n}\n\n/**\n * The long-form reading surface.\n *\n * Everything a Markdown pipeline can emit — headings, prose, lists, tables,\n * quotations, code, figures, footnotes, MathML — set in this system's type,\n * colour and rules. The styles live in `article.css` rather than in a class\n * string, because the input is not JSX: there is no component to hang a class\n * on when the markup arrived as a string, so the contract has to be the element\n * names themselves.\n *\n * That file is published on its own as `@misoto22/design/article.css`, so a\n * site with its own Markdown pipeline can take the reading surface without\n * taking the components — which is the case this exists for. Three sites\n * hand-rolling a prose stylesheet is three prose stylesheets that drift.\n *\n * Composable either way: pass `html` for a rendered string, or children for\n * real elements. A post that mixes the two — prose with a `Diagram` dropped
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Article/Article.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from 'react'\nimport { DEV, warn } from '../../lib/warn'\n\nexport interface ArticleProps extends HTMLAttributes<HTMLElement> {\n /**\n * Rendered HTML from a Markdown or MDX pipeline.\n *\n * Trusted markup only. This sets `dangerouslySetInnerHTML`, so it must come\n * from content you control — a repository's own posts, a CMS you author —\n * and never from a reader. Untrusted Markdown has to be sanitised before it\n * reaches here; that is a pipeline decision, not a component one, and a\n * sanitiser bolted on inside a styling primitive would be the wrong place to\n * make it and the easiest place to get it wrong.\n */\n html?: string\n /** The element to render. `article` by default; pass `div` for a fragment. */\n as?: 'article' | 'div' | 'section'\n}\n\n/**\n * Whether `children` would actually put something on the page.\n *\n * `null`, `false` and `''` are what a conditional child evaluates to when it\n * decided not to render, so none of them is a second Article and none of them\n * is worth a warning.\n */\nfunction rendersSomething(children: ReactNode): boolean {\n return children !== undefined && children !== null && children !== false && children !== ''\n}\n\n/**\n * The long-form reading surface.\n *\n * Everything a Markdown pipeline can emit — headings, prose, lists, tables,\n * quotations, code, figures, footnotes, MathML — set in this system's type,\n * colour and rules. The styles live in `article.css` rather than in a class\n * string, because the input is not JSX: there is no component to hang a class\n * on when the markup arrived as a string, so the contract has to be the element\n * names themselves.\n *\n * That file is published on its own as `@misoto22/design/article.css`, so a\n * site with its own Markdown pipeline can take the reading surface without\n * taking the components — which is the case this exists for. Three sites\n * hand-rolling a prose stylesheet is three prose stylesheets that drift.\n *\n * Composable either way: pass `html` for a rendered string, or children for\n * real elements — one or the other, never both. `html` wins when both arrive,\n * and says so in development: `html=\"\"` is still `html`, which made losing a\n * page of children a thing that could happen without a word. A post that mixes\n * the two — prose with a `Diagram` dropped into the middle — renders its blocks\n * in order and gives each one an `Article`. Inside one, these rules BEAT a component's utilities:\n * `article.css` is imported unlayered while Tailwind's utilities sit in\n * `@layer utilities`, and an unlayered rule wins over a layered one whatever\n * either one's specificity is. That is the mechanism rather than an accident —\n * it is what lets a `Markdown` paragraph, a `Text` carrying `m-0`, give its\n * margin up to the article's rhythm. A component that has to hold a property\n * inside an article needs an inline style, not a class.\n *\n * @example\n * <Article html={renderedMarkdown} />\n * @example\n * <Article>\n * <h1>The White Reset</h1>\n * <p className=\"lead\">A monochrome system for software and writing.</p>\n * <Diagram spec={spec} />\n * </Article>\n */\nexport function Article({ html, as: Comp = 'article', className, children, ...rest }: ArticleProps) {\n // One or the other, never both. The rhythm rules are child combinators, so a\n // wrapper around the rendered string — even a `display: contents` one, which\n // removes the box but not the DOM node — would take every heading and\n // paragraph out of the article's direct children and cost them their\n // spacing. A page with both is a page with two Articles, and it says so\n // below rather than dropping half the page without a word.\n if (html !== undefined) {\n if (DEV && rendersSomething(children)) {\n warn({\n code: 'ARTICLE_HTML_AND_CHILDREN',\n problem:\n 'Article was given html and children together, so the children are not rendered at all. html is written with dangerouslySetInnerHTML, and there is no wrapper that could hold both — the rhythm is a child combinator, so anything put around the rendered string costs every block inside it its spacing.',\n field: 'html',\n fix: 'Render the two in order as two Articles, or drop whichever of html and children is not the content. An empty html string still counts as html.',\n component: 'Article',\n })\n }\n\n return (\n <Comp\n data-m22-article\n className={className}\n dangerouslySetInnerHTML={{ __html: html }}\n {...rest}\n />\n )\n }\n\n return (\n <Comp data-m22-article className={className} {...rest}>\n {children}\n </Comp>\n )\n}\n\nexport default Article\n"],"mappings":"AAuFM;AAtFN,SAAS,KAAK,YAAY;AAyB1B,SAAS,iBAAiB,UAA8B;AACtD,SAAO,aAAa,UAAa,aAAa,QAAQ,aAAa,SAAS,aAAa;AAC3F;AAuCO,SAAS,QAAQ,EAAE,MAAM,IAAI,OAAO,WAAW,WAAW,UAAU,GAAG,KAAK,GAAiB;AAOlG,MAAI,SAAS,QAAW;AACtB,QAAI,OAAO,iBAAiB,QAAQ,GAAG;AACrC,WAAK;AAAA,QACH,MAAM;AAAA,QACN,SACE;AAAA,QACF,OAAO;AAAA,QACP,KAAK;AAAA,QACL,WAAW;AAAA,MACb,CAAC;AAAA,IACH;AAEA,WACE;AAAA,MAAC;AAAA;AAAA,QACC,oBAAgB;AAAA,QAChB;AAAA,QACA,yBAAyB,EAAE,QAAQ,KAAK;AAAA,QACvC,GAAG;AAAA;AAAA,IACN;AAAA,EAEJ;AAEA,SACE,oBAAC,QAAK,oBAAgB,MAAC,WAAuB,GAAG,MAC9C,UACH;AAEJ;AAEA,IAAO,kBAAQ;","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface AspectRatioProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/**
|
|
6
|
+
* Width over height. A number (`16 / 9`) or the CSS form (`'16 / 9'`).
|
|
7
|
+
*
|
|
8
|
+
* It is a style rather than a class because Tailwind can only generate what
|
|
9
|
+
* it can read verbatim in the source, and this value arrives at runtime.
|
|
10
|
+
*/
|
|
11
|
+
ratio?: number | string;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A box that keeps its shape whatever is inside it.
|
|
16
|
+
*
|
|
17
|
+
* The one layout primitive that is genuinely hard to hand-roll. The `padding-
|
|
18
|
+
* top: 56.25%` trick everyone reaches for is a percentage of the WIDTH, which
|
|
19
|
+
* is why it works at all and also why it silently breaks the moment the box is
|
|
20
|
+
* a flex or grid child — and it takes the element's own padding with it. The
|
|
21
|
+
* modern `aspect-ratio` property does the same job in one line, and only holds
|
|
22
|
+
* if nothing inside forces a height, which is what the absolute positioning
|
|
23
|
+
* below guarantees.
|
|
24
|
+
*
|
|
25
|
+
* So: the box declares the ratio, and every direct child is stretched to fill
|
|
26
|
+
* it and taken out of flow. That means a child with no intrinsic size at all —
|
|
27
|
+
* an empty `<div>`, a map that measures its container, a skeleton — still gets
|
|
28
|
+
* the full box, and an `<img>` or `<video>` is cropped to cover it rather than
|
|
29
|
+
* letterboxed. Content that must not be cropped should set `object-contain` on
|
|
30
|
+
* itself.
|
|
31
|
+
*
|
|
32
|
+
* Reach for this when the height must be known before the content loads: a
|
|
33
|
+
* media grid that would otherwise reflow every time an image arrives is the
|
|
34
|
+
* usual case, and that reflow is the layout shift a Core Web Vitals score is
|
|
35
|
+
* measuring.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* <AspectRatio ratio={16 / 9}><img src={cover} alt="" /></AspectRatio>
|
|
39
|
+
* @example
|
|
40
|
+
* <AspectRatio ratio="1 / 1" className="rounded-(--radius) border border-(--rule-2)">
|
|
41
|
+
* <Skeleton />
|
|
42
|
+
* </AspectRatio>
|
|
43
|
+
*/
|
|
44
|
+
declare function AspectRatio({ ratio, className, style, children, ...rest }: AspectRatioProps): react.JSX.Element;
|
|
45
|
+
|
|
46
|
+
export { AspectRatio, type AspectRatioProps, AspectRatio as default };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../lib/cn.js";
|
|
3
|
+
function AspectRatio({
|
|
4
|
+
ratio = 16 / 9,
|
|
5
|
+
className,
|
|
6
|
+
style,
|
|
7
|
+
children,
|
|
8
|
+
...rest
|
|
9
|
+
}) {
|
|
10
|
+
const box = { aspectRatio: String(ratio), ...style };
|
|
11
|
+
return /* @__PURE__ */ jsx(
|
|
12
|
+
"div",
|
|
13
|
+
{
|
|
14
|
+
style: box,
|
|
15
|
+
className: cn(
|
|
16
|
+
"relative w-full overflow-hidden",
|
|
17
|
+
// Every direct child fills the box, so nothing inside can push the
|
|
18
|
+
// height around and break the ratio the box just declared.
|
|
19
|
+
"[&>*]:absolute [&>*]:inset-0 [&>*]:size-full",
|
|
20
|
+
"[&>img]:object-cover [&>video]:object-cover",
|
|
21
|
+
className
|
|
22
|
+
),
|
|
23
|
+
...rest,
|
|
24
|
+
children
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
var AspectRatio_default = AspectRatio;
|
|
29
|
+
export {
|
|
30
|
+
AspectRatio,
|
|
31
|
+
AspectRatio_default as default
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=AspectRatio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/AspectRatio/AspectRatio.tsx"],"sourcesContent":["import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface AspectRatioProps extends HTMLAttributes<HTMLDivElement> {\n /**\n * Width over height. A number (`16 / 9`) or the CSS form (`'16 / 9'`).\n *\n * It is a style rather than a class because Tailwind can only generate what\n * it can read verbatim in the source, and this value arrives at runtime.\n */\n ratio?: number | string\n children?: ReactNode\n}\n\n/**\n * A box that keeps its shape whatever is inside it.\n *\n * The one layout primitive that is genuinely hard to hand-roll. The `padding-\n * top: 56.25%` trick everyone reaches for is a percentage of the WIDTH, which\n * is why it works at all and also why it silently breaks the moment the box is\n * a flex or grid child — and it takes the element's own padding with it. The\n * modern `aspect-ratio` property does the same job in one line, and only holds\n * if nothing inside forces a height, which is what the absolute positioning\n * below guarantees.\n *\n * So: the box declares the ratio, and every direct child is stretched to fill\n * it and taken out of flow. That means a child with no intrinsic size at all —\n * an empty `<div>`, a map that measures its container, a skeleton — still gets\n * the full box, and an `<img>` or `<video>` is cropped to cover it rather than\n * letterboxed. Content that must not be cropped should set `object-contain` on\n * itself.\n *\n * Reach for this when the height must be known before the content loads: a\n * media grid that would otherwise reflow every time an image arrives is the\n * usual case, and that reflow is the layout shift a Core Web Vitals score is\n * measuring.\n *\n * @example\n * <AspectRatio ratio={16 / 9}><img src={cover} alt=\"\" /></AspectRatio>\n * @example\n * <AspectRatio ratio=\"1 / 1\" className=\"rounded-(--radius) border border-(--rule-2)\">\n * <Skeleton />\n * </AspectRatio>\n */\nexport function AspectRatio({\n ratio = 16 / 9,\n className,\n style,\n children,\n ...rest\n}: AspectRatioProps) {\n const box: CSSProperties = { aspectRatio: String(ratio), ...style }\n\n return (\n <div\n style={box}\n className={cn(\n 'relative w-full overflow-hidden',\n // Every direct child fills the box, so nothing inside can push the\n // height around and break the ratio the box just declared.\n '[&>*]:absolute [&>*]:inset-0 [&>*]:size-full',\n '[&>img]:object-cover [&>video]:object-cover',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n )\n}\n\nexport default AspectRatio\n"],"mappings":"AAsDI;AArDJ,SAAS,UAAU;AA2CZ,SAAS,YAAY;AAAA,EAC1B,QAAQ,KAAK;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,MAAqB,EAAE,aAAa,OAAO,KAAK,GAAG,GAAG,MAAM;AAElE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,MACP,WAAW;AAAA,QACT;AAAA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,sBAAQ;","names":[]}
|
|
@@ -5,9 +5,13 @@ import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
|
|
5
5
|
interface AvatarProps extends Omit<ComponentProps<typeof AvatarPrimitive.Root>, 'children'> {
|
|
6
6
|
src?: string;
|
|
7
7
|
/**
|
|
8
|
-
* Describes the person, not the picture.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Describes the person, not the picture. It names the circle itself rather
|
|
9
|
+
* than the image inside it, because the image is the optional half and most
|
|
10
|
+
* rows of a real list render as initials.
|
|
11
|
+
*
|
|
12
|
+
* Empty string is correct and deliberate when the name is already printed
|
|
13
|
+
* beside the avatar: the circle then leaves the accessibility tree entirely,
|
|
14
|
+
* rather than repeating a name a screen reader has just read.
|
|
11
15
|
*/
|
|
12
16
|
alt: string;
|
|
13
17
|
/** Shown while the image loads and if it never does. Usually initials. */
|
|
@@ -21,6 +25,11 @@ interface AvatarProps extends Omit<ComponentProps<typeof AvatarPrimitive.Root>,
|
|
|
21
25
|
* or is still loading — a hand-rolled `onError` swap flashes the initials on
|
|
22
26
|
* every render before the cache answers.
|
|
23
27
|
*
|
|
28
|
+
* The ROOT carries the accessible name, not the image. `alt` on the image
|
|
29
|
+
* reaches the DOM only when `src` does, and the initials are aria-hidden, so an
|
|
30
|
+
* avatar with no photograph announced nothing at all however carefully `alt`
|
|
31
|
+
* was written — and a user list where photographs are optional is full of them.
|
|
32
|
+
*
|
|
24
33
|
* @example
|
|
25
34
|
* <Avatar src={user.photo} alt={user.name} fallback="HC" />
|
|
26
35
|
*/
|
|
@@ -8,9 +8,12 @@ const SIZE = {
|
|
|
8
8
|
lg: "size-12 text-[13px]"
|
|
9
9
|
};
|
|
10
10
|
function Avatar({ src, alt, fallback, size = "md", className, ...rest }) {
|
|
11
|
+
const named = alt.trim() !== "";
|
|
11
12
|
return /* @__PURE__ */ jsxs(
|
|
12
13
|
AvatarPrimitive.Root,
|
|
13
14
|
{
|
|
15
|
+
role: named ? "img" : void 0,
|
|
16
|
+
"aria-label": named ? alt : void 0,
|
|
14
17
|
className: cn(
|
|
15
18
|
"relative inline-flex shrink-0 overflow-hidden rounded-full border border-(--rule) bg-(--stone)",
|
|
16
19
|
SIZE[size],
|
|
@@ -18,7 +21,9 @@ function Avatar({ src, alt, fallback, size = "md", className, ...rest }) {
|
|
|
18
21
|
),
|
|
19
22
|
...rest,
|
|
20
23
|
children: [
|
|
21
|
-
src &&
|
|
24
|
+
src && // Empty, deliberately: the root above is the one element that names
|
|
25
|
+
// this person, and a second name here is the same person read twice.
|
|
26
|
+
/* @__PURE__ */ jsx(AvatarPrimitive.Image, { src, alt: "", className: "size-full object-cover" }),
|
|
22
27
|
/* @__PURE__ */ jsx(
|
|
23
28
|
AvatarPrimitive.Fallback,
|
|
24
29
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["'use client'\n\nimport * as AvatarPrimitive from '@radix-ui/react-avatar'\nimport type { ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface AvatarProps extends Omit<ComponentProps<typeof AvatarPrimitive.Root>, 'children'> {\n src?: string\n /**\n * Describes the person, not the picture.
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["'use client'\n\nimport * as AvatarPrimitive from '@radix-ui/react-avatar'\nimport type { ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface AvatarProps extends Omit<ComponentProps<typeof AvatarPrimitive.Root>, 'children'> {\n src?: string\n /**\n * Describes the person, not the picture. It names the circle itself rather\n * than the image inside it, because the image is the optional half and most\n * rows of a real list render as initials.\n *\n * Empty string is correct and deliberate when the name is already printed\n * beside the avatar: the circle then leaves the accessibility tree entirely,\n * rather than repeating a name a screen reader has just read.\n */\n alt: string\n /** Shown while the image loads and if it never does. Usually initials. */\n fallback: string\n size?: 'sm' | 'md' | 'lg'\n}\n\nconst SIZE = {\n sm: 'size-7 text-[10px]',\n md: 'size-9 text-[11px]',\n lg: 'size-12 text-[13px]',\n} as const\n\n/**\n * A person, as a circle.\n *\n * Wraps Radix so the fallback appears only after the image has actually failed\n * or is still loading — a hand-rolled `onError` swap flashes the initials on\n * every render before the cache answers.\n *\n * The ROOT carries the accessible name, not the image. `alt` on the image\n * reaches the DOM only when `src` does, and the initials are aria-hidden, so an\n * avatar with no photograph announced nothing at all however carefully `alt`\n * was written — and a user list where photographs are optional is full of them.\n *\n * @example\n * <Avatar src={user.photo} alt={user.name} fallback=\"HC\" />\n */\nexport function Avatar({ src, alt, fallback, size = 'md', className, ...rest }: AvatarProps) {\n // An empty alt takes no role at all rather than an unnamed one: `role=\"img\"`\n // with nothing to say is a violation, and the empty string exists precisely\n // to say there is nothing to add.\n const named = alt.trim() !== ''\n\n return (\n <AvatarPrimitive.Root\n role={named ? 'img' : undefined}\n aria-label={named ? alt : undefined}\n className={cn(\n 'relative inline-flex shrink-0 overflow-hidden rounded-full border border-(--rule) bg-(--stone)',\n SIZE[size],\n className,\n )}\n {...rest}\n >\n {src && (\n // Empty, deliberately: the root above is the one element that names\n // this person, and a second name here is the same person read twice.\n <AvatarPrimitive.Image src={src} alt=\"\" className=\"size-full object-cover\" />\n )}\n <AvatarPrimitive.Fallback\n // Nothing to announce: the root carries the identity whether or not the\n // photograph ever arrives. Initials read aloud are noise.\n aria-hidden=\"true\"\n className=\"grid size-full place-items-center font-mono uppercase tracking-wider text-(--ink-3-aa)\"\n >\n {fallback}\n </AvatarPrimitive.Fallback>\n </AvatarPrimitive.Root>\n )\n}\n\nexport default Avatar\n"],"mappings":";AAmDI,SAaI,KAbJ;AAjDJ,YAAY,qBAAqB;AAEjC,SAAS,UAAU;AAmBnB,MAAM,OAAO;AAAA,EACX,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAiBO,SAAS,OAAO,EAAE,KAAK,KAAK,UAAU,OAAO,MAAM,WAAW,GAAG,KAAK,GAAgB;AAI3F,QAAM,QAAQ,IAAI,KAAK,MAAM;AAE7B,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,MAAM,QAAQ,QAAQ;AAAA,MACtB,cAAY,QAAQ,MAAM;AAAA,MAC1B,WAAW;AAAA,QACT;AAAA,QACA,KAAK,IAAI;AAAA,QACT;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA;AAAA,QAGC,oBAAC,gBAAgB,OAAhB,EAAsB,KAAU,KAAI,IAAG,WAAU,0BAAyB;AAAA,QAE7E;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YAGC,eAAY;AAAA,YACZ,WAAU;AAAA,YAET;AAAA;AAAA,QACH;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,iBAAQ;","names":[]}
|
|
@@ -23,6 +23,12 @@ interface BreadcrumbProps extends HTMLAttributes<HTMLElement> {
|
|
|
23
23
|
* Separators live in `<li aria-hidden>` so the trail is read as its items and
|
|
24
24
|
* not as "home slash work slash".
|
|
25
25
|
*
|
|
26
|
+
* A middle crumb with no `href` is a defect that is invisible twice over: it
|
|
27
|
+
* renders as plain text in the same `--ink-3-aa` as the links beside it, and it
|
|
28
|
+
* carries no `aria-current` either — so it reads as the page the reader is on
|
|
29
|
+
* when it is not, and neither the browser nor a review says a word. The console
|
|
30
|
+
* does, in development.
|
|
31
|
+
*
|
|
26
32
|
* @example
|
|
27
33
|
* <Breadcrumb items={[{ label: 'Components', href: '/components' }, { label: 'Button' }]} />
|
|
28
34
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment } from "react";
|
|
3
3
|
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { DEV, warn } from "../../lib/warn.js";
|
|
4
5
|
function Breadcrumb({
|
|
5
6
|
items,
|
|
6
7
|
label = "Breadcrumb",
|
|
@@ -8,6 +9,7 @@ function Breadcrumb({
|
|
|
8
9
|
className,
|
|
9
10
|
...rest
|
|
10
11
|
}) {
|
|
12
|
+
if (DEV) warnUnlinked(items);
|
|
11
13
|
return /* @__PURE__ */ jsx("nav", { "aria-label": label, className: cn("mono-meta text-(--ink-3-aa)", className), ...rest, children: /* @__PURE__ */ jsx("ol", { className: "m-0 flex list-none flex-wrap items-center gap-2 p-0", children: items.map((item, index) => {
|
|
12
14
|
const last = index === items.length - 1;
|
|
13
15
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -23,6 +25,19 @@ function Breadcrumb({
|
|
|
23
25
|
] }, index);
|
|
24
26
|
}) }) });
|
|
25
27
|
}
|
|
28
|
+
function warnUnlinked(items) {
|
|
29
|
+
items.forEach((item, index) => {
|
|
30
|
+
if (item.href || index === items.length - 1) return;
|
|
31
|
+
const named = typeof item.label === "string" ? `"${item.label}"` : `at ${index}`;
|
|
32
|
+
warn({
|
|
33
|
+
code: "BREADCRUMB_CRUMB_NOT_LINKED",
|
|
34
|
+
problem: `The crumb ${named} has no href, so it renders as plain text in the same colour as the links beside it and takes no aria-current. It reads as the current page, and it is not.`,
|
|
35
|
+
field: `items[${index}]`,
|
|
36
|
+
fix: "Give every crumb but the last an href, or drop the crumb if there is nothing to link to.",
|
|
37
|
+
component: "Breadcrumb"
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
26
41
|
var Breadcrumb_default = Breadcrumb;
|
|
27
42
|
export {
|
|
28
43
|
Breadcrumb,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Breadcrumb/Breadcrumb.tsx"],"sourcesContent":["import { Fragment } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface Crumb {\n label: ReactNode\n /** Omit on the last crumb — the page you are already on is not a link. */\n href?: string\n}\n\nexport interface BreadcrumbProps extends HTMLAttributes<HTMLElement> {\n items: Crumb[]\n /** Names this trail when a page has more than one. */\n label?: string\n /** Rendered between crumbs. A slash by default; it is decorative either way. */\n separator?: ReactNode\n}\n\n/**\n * Where you are, as a path.\n *\n * The last crumb is plain text with `aria-current=\"page\"` rather than a link to\n * itself — a self-link is the most common breadcrumb bug, and it makes a screen\n * reader offer a navigation that goes nowhere.\n *\n * Separators live in `<li aria-hidden>` so the trail is read as its items and\n * not as \"home slash work slash\".\n *\n * @example\n * <Breadcrumb items={[{ label: 'Components', href: '/components' }, { label: 'Button' }]} />\n */\nexport function Breadcrumb({\n items,\n label = 'Breadcrumb',\n separator = '/',\n className,\n ...rest\n}: BreadcrumbProps) {\n return (\n <nav aria-label={label} className={cn('mono-meta text-(--ink-3-aa)', className)} {...rest}>\n <ol className=\"m-0 flex list-none flex-wrap items-center gap-2 p-0\">\n {items.map((item, index) => {\n const last = index === items.length - 1\n return (\n <Fragment key={index}>\n <li className=\"min-w-0\">\n {item.href && !last ? (\n <a\n href={item.href}\n className=\"text-(--ink-3-aa) underline decoration-transparent underline-offset-4 transition-colors duration-(--duration-fast) hover:text-(--ink) hover:decoration-(--rule-2)\"\n >\n {item.label}\n </a>\n ) : (\n <span aria-current={last ? 'page' : undefined} className={last ? 'text-(--ink)' : undefined}>\n {item.label}\n </span>\n )}\n </li>\n {!last && (\n <li aria-hidden=\"true\" className=\"select-none text-(--rule-2)\">\n {separator}\n </li>\n )}\n </Fragment>\n )\n })}\n </ol>\n </nav>\n )\n}\n\nexport default Breadcrumb\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Breadcrumb/Breadcrumb.tsx"],"sourcesContent":["import { Fragment } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { DEV, warn } from '../../lib/warn'\n\nexport interface Crumb {\n label: ReactNode\n /** Omit on the last crumb — the page you are already on is not a link. */\n href?: string\n}\n\nexport interface BreadcrumbProps extends HTMLAttributes<HTMLElement> {\n items: Crumb[]\n /** Names this trail when a page has more than one. */\n label?: string\n /** Rendered between crumbs. A slash by default; it is decorative either way. */\n separator?: ReactNode\n}\n\n/**\n * Where you are, as a path.\n *\n * The last crumb is plain text with `aria-current=\"page\"` rather than a link to\n * itself — a self-link is the most common breadcrumb bug, and it makes a screen\n * reader offer a navigation that goes nowhere.\n *\n * Separators live in `<li aria-hidden>` so the trail is read as its items and\n * not as \"home slash work slash\".\n *\n * A middle crumb with no `href` is a defect that is invisible twice over: it\n * renders as plain text in the same `--ink-3-aa` as the links beside it, and it\n * carries no `aria-current` either — so it reads as the page the reader is on\n * when it is not, and neither the browser nor a review says a word. The console\n * does, in development.\n *\n * @example\n * <Breadcrumb items={[{ label: 'Components', href: '/components' }, { label: 'Button' }]} />\n */\nexport function Breadcrumb({\n items,\n label = 'Breadcrumb',\n separator = '/',\n className,\n ...rest\n}: BreadcrumbProps) {\n if (DEV) warnUnlinked(items)\n\n return (\n <nav aria-label={label} className={cn('mono-meta text-(--ink-3-aa)', className)} {...rest}>\n <ol className=\"m-0 flex list-none flex-wrap items-center gap-2 p-0\">\n {items.map((item, index) => {\n const last = index === items.length - 1\n return (\n <Fragment key={index}>\n <li className=\"min-w-0\">\n {item.href && !last ? (\n <a\n href={item.href}\n className=\"text-(--ink-3-aa) underline decoration-transparent underline-offset-4 transition-colors duration-(--duration-fast) hover:text-(--ink) hover:decoration-(--rule-2)\"\n >\n {item.label}\n </a>\n ) : (\n <span aria-current={last ? 'page' : undefined} className={last ? 'text-(--ink)' : undefined}>\n {item.label}\n </span>\n )}\n </li>\n {!last && (\n <li aria-hidden=\"true\" className=\"select-none text-(--rule-2)\">\n {separator}\n </li>\n )}\n </Fragment>\n )\n })}\n </ol>\n </nav>\n )\n}\n\n/**\n * Dev only: every crumb but the last owes the reader a destination.\n *\n * The label is a ReactNode, so a crumb whose label is not a string is named by\n * its position instead — the message has to identify the crumb well enough to\n * repair the call without opening anything.\n */\nfunction warnUnlinked(items: Crumb[]): void {\n items.forEach((item, index) => {\n if (item.href || index === items.length - 1) return\n const named = typeof item.label === 'string' ? `\"${item.label}\"` : `at ${index}`\n\n warn({\n code: 'BREADCRUMB_CRUMB_NOT_LINKED',\n problem: `The crumb ${named} has no href, so it renders as plain text in the same colour as the links beside it and takes no aria-current. It reads as the current page, and it is not.`,\n field: `items[${index}]`,\n fix: 'Give every crumb but the last an href, or drop the crumb if there is nothing to link to.',\n component: 'Breadcrumb',\n })\n })\n}\n\nexport default Breadcrumb\n"],"mappings":"AAqDY,SAGM,KAHN;AArDZ,SAAS,gBAAgB;AAEzB,SAAS,UAAU;AACnB,SAAS,KAAK,YAAY;AAmCnB,SAAS,WAAW;AAAA,EACzB;AAAA,EACA,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,MAAI,IAAK,cAAa,KAAK;AAE3B,SACE,oBAAC,SAAI,cAAY,OAAO,WAAW,GAAG,+BAA+B,SAAS,GAAI,GAAG,MACnF,8BAAC,QAAG,WAAU,uDACX,gBAAM,IAAI,CAAC,MAAM,UAAU;AAC1B,UAAM,OAAO,UAAU,MAAM,SAAS;AACtC,WACE,qBAAC,YACC;AAAA,0BAAC,QAAG,WAAU,WACX,eAAK,QAAQ,CAAC,OACb;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,KAAK;AAAA,UACX,WAAU;AAAA,UAET,eAAK;AAAA;AAAA,MACR,IAEA,oBAAC,UAAK,gBAAc,OAAO,SAAS,QAAW,WAAW,OAAO,iBAAiB,QAC/E,eAAK,OACR,GAEJ;AAAA,MACC,CAAC,QACA,oBAAC,QAAG,eAAY,QAAO,WAAU,+BAC9B,qBACH;AAAA,SAlBW,KAoBf;AAAA,EAEJ,CAAC,GACH,GACF;AAEJ;AASA,SAAS,aAAa,OAAsB;AAC1C,QAAM,QAAQ,CAAC,MAAM,UAAU;AAC7B,QAAI,KAAK,QAAQ,UAAU,MAAM,SAAS,EAAG;AAC7C,UAAM,QAAQ,OAAO,KAAK,UAAU,WAAW,IAAI,KAAK,KAAK,MAAM,MAAM,KAAK;AAE9E,SAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS,aAAa,KAAK;AAAA,MAC3B,OAAO,SAAS,KAAK;AAAA,MACrB,KAAK;AAAA,MACL,WAAW;AAAA,IACb,CAAC;AAAA,EACH,CAAC;AACH;AAEA,IAAO,qBAAQ;","names":[]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { DEV, warn } from "../../lib/warn.js";
|
|
4
5
|
import { Spinner } from "../Spinner/Spinner.js";
|
|
5
6
|
const BASE = "inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none";
|
|
6
7
|
const VARIANT = {
|
|
@@ -37,6 +38,19 @@ function Button(props) {
|
|
|
37
38
|
className
|
|
38
39
|
} = props;
|
|
39
40
|
const cls = cn(BASE, VARIANT[variant], iconOnly ? ICON_SIZE[size] : SIZE[size], className);
|
|
41
|
+
if (DEV && iconOnly) {
|
|
42
|
+
const named = typeof props["aria-label"] === "string" && props["aria-label"].trim() !== "";
|
|
43
|
+
const described = typeof props["aria-labelledby"] === "string";
|
|
44
|
+
if (!named && !described) {
|
|
45
|
+
warn({
|
|
46
|
+
code: "BUTTON_ICON_ONLY_UNNAMED",
|
|
47
|
+
problem: 'An iconOnly Button has no text, so without an accessible name a screen reader announces it as "button" and nothing else.',
|
|
48
|
+
field: "aria-label",
|
|
49
|
+
fix: 'Add aria-label with the action the button performs, e.g. aria-label="Delete invoice".',
|
|
50
|
+
component: "Button"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
40
54
|
const spinnerTone = variant === "primary" || variant === "danger" ? "current" : "default";
|
|
41
55
|
const decoration = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
42
56
|
loading && /* @__PURE__ */ jsx(Spinner, { size: SPINNER_SIZE[size], tone: spinnerTone, label: null }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from '@radix-ui/react-slot'\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { Spinner } from '../Spinner/Spinner'\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'\nexport type ButtonSize = 'sm' | 'md' | 'lg'\n\n/**\n * The base every variant shares.\n *\n * It reads `--radius`, the CONTROL step, which is the same corner an `Input`,\n * a `Select` trigger and a `Textarea` draw. It used to be a pill, and a pill\n * beside an 8px field is two different ideas of what a control is — the one\n * inconsistency in the system that every reader noticed and none of the\n * component pages could explain. Everything on the control line now agrees,\n * and the capsule is kept for the things that genuinely are one: a badge, a\n * status pill, a segmented strip, a progress track.\n *\n * It does not move on hover. An earlier base lifted a pixel and asked for a\n * shadow the White Reset does not have — `--shadow` resolves to `none`, so that\n * hover state was invisible on every button in the system while still costing a\n * transition. Opacity carries the filled hover; the border carries the outlined\n * one.\n *\n * The transition names its properties rather than using `all`, so a layout\n * change (a width that grows when the label becomes \"Saving…\") is instant\n * instead of sliding.\n */\nconst BASE =\n 'inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none'\n\nconst VARIANT: Record<ButtonVariant, string> = {\n primary: 'bg-(--accent) text-(--accent-foreground) border border-(--accent) hover:opacity-85',\n secondary: 'border border-(--rule-2) text-(--ink) bg-transparent hover:border-(--ink)',\n ghost: 'border border-transparent text-(--ink-2) hover:text-(--ink) hover:bg-(--stone)',\n /* The one place chroma is allowed on a control, and only because the action\n is destructive — that is a state, not a brand colour. */\n danger: 'bg-(--danger) text-(--paper) border border-(--danger) hover:opacity-85',\n}\n\n/**\n * `min-h` rather than `h`, so a button that wraps to two lines grows instead of\n * clipping.\n *\n * The heights come from the density tokens rather than from a literal, so a\n * subtree marked `data-density=\"compact\"` shrinks its controls without a single\n * call site being told. At the default density `md` is 44px — the pointer\n * target WCAG 2.5.5 asks for.\n */\nconst SIZE: Record<ButtonSize, string> = {\n sm: 'min-h-(--control-h-sm) px-(--control-px-sm) py-(--control-py-sm) text-[13px]',\n md: 'min-h-(--control-h-md) px-(--control-px-md) py-(--control-py-md) text-sm',\n lg: 'min-h-(--control-h-lg) px-(--control-px-lg) py-(--control-py-lg) text-[15px]',\n}\n\n/** Square, because an icon has no label to give the box its width. */\nconst ICON_SIZE: Record<ButtonSize, string> = {\n sm: 'size-(--control-h-sm) p-0',\n md: 'size-(--control-h-md) p-0',\n lg: 'size-(--control-h-lg) p-0',\n}\n\nconst SPINNER_SIZE: Record<ButtonSize, 'sm' | 'md'> = { sm: 'sm', md: 'sm', lg: 'md' }\n\ninterface CommonProps {\n children?: ReactNode\n /**\n * Which action this is. `primary` is the one thing the screen wants you to\n * do, so there is at most one per view; `danger` is reserved for destructive\n * actions and is the only place chroma is allowed on a control.\n */\n variant?: ButtonVariant\n /** 36 / 44 / 48px tall. `md` meets the pointer-target floor on its own. */\n size?: ButtonSize\n /** Optional mono keycap glyph rendered after the label (e.g. \"P\"). */\n keycap?: string\n /**\n * Swaps the leading content for a spinner and blocks interaction. The label\n * stays put — a button that empties out while it works loses its width, and\n * the page reflows under the pointer that just clicked it.\n */\n loading?: boolean\n /**\n * Square control with no label. REQUIRES `aria-label`: an icon-only button\n * with no accessible name is invisible to a screen reader, and this is the\n * single most common way a design system ships an unusable control.\n */\n iconOnly?: boolean\n /**\n * Render the child element instead of a `<button>`, keeping these styles.\n * Use it to hand the styling to a router's `Link` — `asChild` is what keeps\n * this package free of any one router.\n *\n * The decoration slots (`keycap`, `loading`) are not injected into a slotted\n * child; compose them inside it yourself.\n *\n * @example\n * <Button asChild><Link href=\"/work\">Work</Link></Button>\n */\n asChild?: boolean\n className?: string\n}\n\ntype ButtonAsLink = CommonProps &\n Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className' | 'children'> & { href: string }\n\ntype ButtonAsButton = CommonProps &\n Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> & { href?: undefined }\n\nexport type ButtonProps = ButtonAsLink | ButtonAsButton\n\nfunction Keycap({ glyph }: { glyph: string }) {\n return (\n <span className=\"rounded-(--radius-sm) border border-current px-2 py-0.5 font-mono text-[11px] opacity-55\">\n {glyph}\n </span>\n )\n}\n\n/**\n * The system's action.\n *\n * Renders a `<button>`, or an `<a>` when given `href`, or whatever you hand it\n * via `asChild`. No router is imported, so the package stays framework-agnostic\n * and a Next or React Router app wires its own `Link` at the call site.\n *\n * Server-component friendly: there is no client boundary here, so it renders in\n * a static page as well as an interactive one.\n *\n * @example\n * <Button variant=\"secondary\" keycap=\"P\">View projects</Button>\n * @example\n * <Button loading disabled>Saving…</Button>\n * @example\n * <Button iconOnly aria-label=\"Close\"><X size={16} /></Button>\n */\nexport function Button(props: ButtonProps) {\n const {\n children,\n variant = 'primary',\n size = 'md',\n keycap,\n loading = false,\n iconOnly = false,\n asChild = false,\n className,\n } = props\n\n const cls = cn(BASE, VARIANT[variant], iconOnly ? ICON_SIZE[size] : SIZE[size], className)\n\n // A filled ground needs a spinner in the inherited colour; an outlined one\n // reads better against the page's own rule scale.\n const spinnerTone = variant === 'primary' || variant === 'danger' ? 'current' : 'default'\n const decoration = (\n <>\n {loading && <Spinner size={SPINNER_SIZE[size]} tone={spinnerTone} label={null} />}\n {children}\n {keycap && <Keycap glyph={keycap} />}\n </>\n )\n\n if (asChild) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <Slot className={cls} {...rest}>\n {children}\n </Slot>\n )\n }\n\n if (props.href !== undefined) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, href, ...rest } = props\n return (\n <a\n href={href}\n className={cls}\n // A link cannot be `disabled`; `aria-disabled` plus the pointer-events\n // rule in BASE is the accessible equivalent.\n aria-disabled={loading || undefined}\n {...rest}\n >\n {decoration}\n </a>\n )\n }\n\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <button\n type={rest.type ?? 'button'}\n aria-busy={loading || undefined}\n disabled={rest.disabled || loading}\n className={cls}\n {...rest}\n >\n {decoration}\n </button>\n )\n}\n\nexport default Button\n"],"mappings":"AAkHI,SAyCA,UAzCA,KAyCA,YAzCA;AAlHJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,eAAe;AA0BxB,MAAM,OACJ;AAEF,MAAM,UAAyC;AAAA,EAC7C,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA;AAAA;AAAA,EAGP,QAAQ;AACV;AAWA,MAAM,OAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGA,MAAM,YAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAgD,EAAE,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK;AAiDrF,SAAS,OAAO,EAAE,MAAM,GAAsB;AAC5C,SACE,oBAAC,UAAK,WAAU,4FACb,iBACH;AAEJ;AAmBO,SAAS,OAAO,OAAoB;AACzC,QAAM;AAAA,IACJ;AAAA,IACA,UAAU;AAAA,IACV,OAAO;AAAA,IACP;AAAA,IACA,UAAU;AAAA,IACV,WAAW;AAAA,IACX,UAAU;AAAA,IACV;AAAA,EACF,IAAI;AAEJ,QAAM,MAAM,GAAG,MAAM,QAAQ,OAAO,GAAG,WAAW,UAAU,IAAI,IAAI,KAAK,IAAI,GAAG,SAAS;AAIzF,QAAM,cAAc,YAAY,aAAa,YAAY,WAAW,YAAY;AAChF,QAAM,aACJ,iCACG;AAAA,eAAW,oBAAC,WAAQ,MAAM,aAAa,IAAI,GAAG,MAAM,aAAa,OAAO,MAAM;AAAA,IAC9E;AAAA,IACA,UAAU,oBAAC,UAAO,OAAO,QAAQ;AAAA,KACpC;AAGF,MAAI,SAAS;AACX,UAAM,EAAE,UAAUA,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,GAAGC,MAAK,IAAI;AAC7H,WACE,oBAAC,QAAK,WAAW,KAAM,GAAGA,OACvB,UACH;AAAA,EAEJ;AAEA,MAAI,MAAM,SAAS,QAAW;AAC5B,UAAM,EAAE,UAAUR,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,MAAM,GAAGC,MAAK,IAAI;AACnI,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,QAGX,iBAAe,WAAW;AAAA,QACzB,GAAGA;AAAA,QAEH;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,EAAE,UAAU,IAAI,SAAS,IAAI,MAAM,IAAI,QAAQ,IAAI,SAAS,IAAI,UAAU,IAAI,SAAS,IAAI,WAAW,KAAK,GAAG,KAAK,IAAI;AAC7H,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,KAAK,QAAQ;AAAA,MACnB,aAAW,WAAW;AAAA,MACtB,UAAU,KAAK,YAAY;AAAA,MAC3B,WAAW;AAAA,MACV,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,iBAAQ;","names":["_c","_v","_s","_k","_l","_i","_a","_cn","rest"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from '@radix-ui/react-slot'\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { DEV, warn } from '../../lib/warn'\nimport { Spinner } from '../Spinner/Spinner'\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'\nexport type ButtonSize = 'sm' | 'md' | 'lg'\n\n/**\n * The base every variant shares.\n *\n * It reads `--radius`, the CONTROL step, which is the same corner an `Input`,\n * a `Select` trigger and a `Textarea` draw. It used to be a pill, and a pill\n * beside an 8px field is two different ideas of what a control is — the one\n * inconsistency in the system that every reader noticed and none of the\n * component pages could explain. Everything on the control line now agrees,\n * and the capsule is kept for the things that genuinely are one: a badge, a\n * status pill, a segmented strip, a progress track.\n *\n * It does not move on hover. An earlier base lifted a pixel and asked for a\n * shadow the White Reset does not have — `--shadow` resolves to `none`, so that\n * hover state was invisible on every button in the system while still costing a\n * transition. Opacity carries the filled hover; the border carries the outlined\n * one.\n *\n * The transition names its properties rather than using `all`, so a layout\n * change (a width that grows when the label becomes \"Saving…\") is instant\n * instead of sliding.\n */\nconst BASE =\n 'inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none'\n\nconst VARIANT: Record<ButtonVariant, string> = {\n primary: 'bg-(--accent) text-(--accent-foreground) border border-(--accent) hover:opacity-85',\n secondary: 'border border-(--rule-2) text-(--ink) bg-transparent hover:border-(--ink)',\n ghost: 'border border-transparent text-(--ink-2) hover:text-(--ink) hover:bg-(--stone)',\n /* The one place chroma is allowed on a control, and only because the action\n is destructive — that is a state, not a brand colour. */\n danger: 'bg-(--danger) text-(--paper) border border-(--danger) hover:opacity-85',\n}\n\n/**\n * `min-h` rather than `h`, so a button that wraps to two lines grows instead of\n * clipping.\n *\n * The heights come from the density tokens rather than from a literal, so a\n * subtree marked `data-density=\"compact\"` shrinks its controls without a single\n * call site being told. At the default density `md` is 44px — the pointer\n * target WCAG 2.5.5 asks for.\n */\nconst SIZE: Record<ButtonSize, string> = {\n sm: 'min-h-(--control-h-sm) px-(--control-px-sm) py-(--control-py-sm) text-[13px]',\n md: 'min-h-(--control-h-md) px-(--control-px-md) py-(--control-py-md) text-sm',\n lg: 'min-h-(--control-h-lg) px-(--control-px-lg) py-(--control-py-lg) text-[15px]',\n}\n\n/** Square, because an icon has no label to give the box its width. */\nconst ICON_SIZE: Record<ButtonSize, string> = {\n sm: 'size-(--control-h-sm) p-0',\n md: 'size-(--control-h-md) p-0',\n lg: 'size-(--control-h-lg) p-0',\n}\n\nconst SPINNER_SIZE: Record<ButtonSize, 'sm' | 'md'> = { sm: 'sm', md: 'sm', lg: 'md' }\n\ninterface CommonProps {\n children?: ReactNode\n /**\n * Which action this is. `primary` is the one thing the screen wants you to\n * do, so there is at most one per view; `danger` is reserved for destructive\n * actions and is the only place chroma is allowed on a control.\n */\n variant?: ButtonVariant\n /** 36 / 44 / 48px tall. `md` meets the pointer-target floor on its own. */\n size?: ButtonSize\n /** Optional mono keycap glyph rendered after the label (e.g. \"P\"). */\n keycap?: string\n /**\n * Swaps the leading content for a spinner and blocks interaction. The label\n * stays put — a button that empties out while it works loses its width, and\n * the page reflows under the pointer that just clicked it.\n */\n loading?: boolean\n /**\n * Square control with no label. REQUIRES `aria-label`: an icon-only button\n * with no accessible name is invisible to a screen reader, and this is the\n * single most common way a design system ships an unusable control.\n */\n iconOnly?: boolean\n /**\n * Render the child element instead of a `<button>`, keeping these styles.\n * Use it to hand the styling to a router's `Link` — `asChild` is what keeps\n * this package free of any one router.\n *\n * The decoration slots (`keycap`, `loading`) are not injected into a slotted\n * child; compose them inside it yourself.\n *\n * @example\n * <Button asChild><Link href=\"/work\">Work</Link></Button>\n */\n asChild?: boolean\n className?: string\n}\n\ntype ButtonAsLink = CommonProps &\n Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className' | 'children'> & { href: string }\n\ntype ButtonAsButton = CommonProps &\n Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> & { href?: undefined }\n\nexport type ButtonProps = ButtonAsLink | ButtonAsButton\n\nfunction Keycap({ glyph }: { glyph: string }) {\n return (\n <span className=\"rounded-(--radius-sm) border border-current px-2 py-0.5 font-mono text-[11px] opacity-55\">\n {glyph}\n </span>\n )\n}\n\n/**\n * The system's action.\n *\n * Renders a `<button>`, or an `<a>` when given `href`, or whatever you hand it\n * via `asChild`. No router is imported, so the package stays framework-agnostic\n * and a Next or React Router app wires its own `Link` at the call site.\n *\n * Server-component friendly: there is no client boundary here, so it renders in\n * a static page as well as an interactive one.\n *\n * @example\n * <Button variant=\"secondary\" keycap=\"P\">View projects</Button>\n * @example\n * <Button loading disabled>Saving…</Button>\n * @example\n * <Button iconOnly aria-label=\"Close\"><X size={16} /></Button>\n */\nexport function Button(props: ButtonProps) {\n const {\n children,\n variant = 'primary',\n size = 'md',\n keycap,\n loading = false,\n iconOnly = false,\n asChild = false,\n className,\n } = props\n\n const cls = cn(BASE, VARIANT[variant], iconOnly ? ICON_SIZE[size] : SIZE[size], className)\n\n if (DEV && iconOnly) {\n // The control renders correctly and is announced as \"button\" and nothing\n // else. Nothing about the rendered page says so, which is why this is the\n // single most common way a design system ships an unusable control.\n const named =\n typeof props['aria-label'] === 'string' && props['aria-label'].trim() !== ''\n const described = typeof props['aria-labelledby'] === 'string'\n if (!named && !described) {\n warn({\n code: 'BUTTON_ICON_ONLY_UNNAMED',\n problem:\n 'An iconOnly Button has no text, so without an accessible name a screen reader announces it as \"button\" and nothing else.',\n field: 'aria-label',\n fix: 'Add aria-label with the action the button performs, e.g. aria-label=\"Delete invoice\".',\n component: 'Button',\n })\n }\n }\n\n // A filled ground needs a spinner in the inherited colour; an outlined one\n // reads better against the page's own rule scale.\n const spinnerTone = variant === 'primary' || variant === 'danger' ? 'current' : 'default'\n const decoration = (\n <>\n {loading && <Spinner size={SPINNER_SIZE[size]} tone={spinnerTone} label={null} />}\n {children}\n {keycap && <Keycap glyph={keycap} />}\n </>\n )\n\n if (asChild) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <Slot className={cls} {...rest}>\n {children}\n </Slot>\n )\n }\n\n if (props.href !== undefined) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, href, ...rest } = props\n return (\n <a\n href={href}\n className={cls}\n // A link cannot be `disabled`; `aria-disabled` plus the pointer-events\n // rule in BASE is the accessible equivalent.\n aria-disabled={loading || undefined}\n {...rest}\n >\n {decoration}\n </a>\n )\n }\n\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <button\n type={rest.type ?? 'button'}\n aria-busy={loading || undefined}\n disabled={rest.disabled || loading}\n className={cls}\n {...rest}\n >\n {decoration}\n </button>\n )\n}\n\nexport default Button\n"],"mappings":"AAmHI,SA4DA,UA5DA,KA4DA,YA5DA;AAnHJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,KAAK,YAAY;AAC1B,SAAS,eAAe;AA0BxB,MAAM,OACJ;AAEF,MAAM,UAAyC;AAAA,EAC7C,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA;AAAA;AAAA,EAGP,QAAQ;AACV;AAWA,MAAM,OAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGA,MAAM,YAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAgD,EAAE,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK;AAiDrF,SAAS,OAAO,EAAE,MAAM,GAAsB;AAC5C,SACE,oBAAC,UAAK,WAAU,4FACb,iBACH;AAEJ;AAmBO,SAAS,OAAO,OAAoB;AACzC,QAAM;AAAA,IACJ;AAAA,IACA,UAAU;AAAA,IACV,OAAO;AAAA,IACP;AAAA,IACA,UAAU;AAAA,IACV,WAAW;AAAA,IACX,UAAU;AAAA,IACV;AAAA,EACF,IAAI;AAEJ,QAAM,MAAM,GAAG,MAAM,QAAQ,OAAO,GAAG,WAAW,UAAU,IAAI,IAAI,KAAK,IAAI,GAAG,SAAS;AAEzF,MAAI,OAAO,UAAU;AAInB,UAAM,QACJ,OAAO,MAAM,YAAY,MAAM,YAAY,MAAM,YAAY,EAAE,KAAK,MAAM;AAC5E,UAAM,YAAY,OAAO,MAAM,iBAAiB,MAAM;AACtD,QAAI,CAAC,SAAS,CAAC,WAAW;AACxB,WAAK;AAAA,QACH,MAAM;AAAA,QACN,SACE;AAAA,QACF,OAAO;AAAA,QACP,KAAK;AAAA,QACL,WAAW;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AAIA,QAAM,cAAc,YAAY,aAAa,YAAY,WAAW,YAAY;AAChF,QAAM,aACJ,iCACG;AAAA,eAAW,oBAAC,WAAQ,MAAM,aAAa,IAAI,GAAG,MAAM,aAAa,OAAO,MAAM;AAAA,IAC9E;AAAA,IACA,UAAU,oBAAC,UAAO,OAAO,QAAQ;AAAA,KACpC;AAGF,MAAI,SAAS;AACX,UAAM,EAAE,UAAUA,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,GAAGC,MAAK,IAAI;AAC7H,WACE,oBAAC,QAAK,WAAW,KAAM,GAAGA,OACvB,UACH;AAAA,EAEJ;AAEA,MAAI,MAAM,SAAS,QAAW;AAC5B,UAAM,EAAE,UAAUR,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,MAAM,GAAGC,MAAK,IAAI;AACnI,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,QAGX,iBAAe,WAAW;AAAA,QACzB,GAAGA;AAAA,QAEH;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,EAAE,UAAU,IAAI,SAAS,IAAI,MAAM,IAAI,QAAQ,IAAI,SAAS,IAAI,UAAU,IAAI,SAAS,IAAI,WAAW,KAAK,GAAG,KAAK,IAAI;AAC7H,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,KAAK,QAAQ;AAAA,MACnB,aAAW,WAAW;AAAA,MACtB,UAAU,KAAK,YAAY;AAAA,MAC3B,WAAW;AAAA,MACV,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,iBAAQ;","names":["_c","_v","_s","_k","_l","_i","_a","_cn","rest"]}
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { DayPickerProps } from 'react-day-picker';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The strings the month-and-year picker says, none of which react-day-picker
|
|
6
|
+
* knows about — its own `labels` prop covers its own chrome, and this panel is
|
|
7
|
+
* ours. English by default, and exposed for the same reason `AppShell` exposes
|
|
8
|
+
* `openLabel`: every one of these controls is an icon or a bare number, so the
|
|
9
|
+
* string IS the control as far as a screen reader is concerned.
|
|
10
|
+
*
|
|
11
|
+
* The month names already follow `locale`, which is what made the chrome around
|
|
12
|
+
* them read as an oversight rather than as a policy.
|
|
13
|
+
*/
|
|
14
|
+
interface CalendarLabels {
|
|
15
|
+
/** Names the panel showing twelve months. */
|
|
16
|
+
monthPanelLabel?: string;
|
|
17
|
+
/** Names the panel showing a page of years. */
|
|
18
|
+
yearPanelLabel?: string;
|
|
19
|
+
/** The two chevrons that browse a year at a time, inside the month panel. */
|
|
20
|
+
previousYearLabel?: string;
|
|
21
|
+
nextYearLabel?: string;
|
|
22
|
+
/** The two chevrons that page the year grid, twenty-four at a time. */
|
|
23
|
+
earlierYearsLabel?: string;
|
|
24
|
+
laterYearsLabel?: string;
|
|
25
|
+
}
|
|
26
|
+
type CalendarProps = DayPickerProps & CalendarLabels;
|
|
5
27
|
/**
|
|
6
28
|
* A month, as a grid of days.
|
|
7
29
|
*
|
|
@@ -17,12 +39,16 @@ type CalendarProps = DayPickerProps;
|
|
|
17
39
|
* library's OWN class is kept on each slot, because passing a class REPLACES it
|
|
18
40
|
* and every `.rdp-*` selector downstream depends on those.
|
|
19
41
|
*
|
|
42
|
+
* The month names follow `locale`. The picker's own chrome — the two panels and
|
|
43
|
+
* the four chevrons that browse them — follows `CalendarLabels`, because
|
|
44
|
+
* `locale` cannot supply strings this package invented.
|
|
45
|
+
*
|
|
20
46
|
* @example
|
|
21
47
|
* <Calendar mode="single" selected={date} onSelect={setDate} />
|
|
22
48
|
* @example
|
|
23
49
|
* // A birth date: widen the years, since the default span is deliberately short.
|
|
24
50
|
* <Calendar mode="single" startMonth={new Date(1920, 0)} endMonth={new Date()} />
|
|
25
51
|
*/
|
|
26
|
-
declare function Calendar({ className, classNames, components, ...props }: CalendarProps): react.JSX.Element;
|
|
52
|
+
declare function Calendar({ className, classNames, components, monthPanelLabel, yearPanelLabel, previousYearLabel, nextYearLabel, earlierYearsLabel, laterYearsLabel, ...props }: CalendarProps): react.JSX.Element;
|
|
27
53
|
|
|
28
|
-
export { Calendar, type CalendarProps, Calendar as default };
|
|
54
|
+
export { Calendar, type CalendarLabels, type CalendarProps, Calendar as default };
|