@misoto22/design 0.8.0 → 0.9.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.
Files changed (157) hide show
  1. package/dist/agent/Accordion.md +1 -1
  2. package/dist/agent/Alert.md +1 -1
  3. package/dist/agent/AppShell.md +1 -1
  4. package/dist/agent/ArchitectureFigure.md +1 -1
  5. package/dist/agent/AreaChart.md +1 -1
  6. package/dist/agent/Article.md +1 -1
  7. package/dist/agent/AspectRatio.md +1 -1
  8. package/dist/agent/Avatar.md +1 -1
  9. package/dist/agent/Badge.md +1 -1
  10. package/dist/agent/BarChart.md +1 -1
  11. package/dist/agent/BarList.md +1 -1
  12. package/dist/agent/BigNumber.md +1 -1
  13. package/dist/agent/BoxPlot.md +1 -1
  14. package/dist/agent/Breadcrumb.md +1 -1
  15. package/dist/agent/BulletChart.md +1 -1
  16. package/dist/agent/Button.md +1 -1
  17. package/dist/agent/Calendar.md +1 -1
  18. package/dist/agent/Card.md +1 -1
  19. package/dist/agent/Checkbox.md +1 -1
  20. package/dist/agent/Code.md +1 -1
  21. package/dist/agent/CodeBlock.md +1 -1
  22. package/dist/agent/Collapsible.md +1 -1
  23. package/dist/agent/ColorPicker.md +75 -0
  24. package/dist/agent/Combobox.md +1 -1
  25. package/dist/agent/Command.md +1 -1
  26. package/dist/agent/ComposedChart.md +1 -1
  27. package/dist/agent/ContextMenu.md +1 -1
  28. package/dist/agent/DataflowFigure.md +1 -1
  29. package/dist/agent/DatePicker.md +1 -1
  30. package/dist/agent/DescriptionList.md +1 -1
  31. package/dist/agent/Diagram.md +1 -1
  32. package/dist/agent/DiagramCanvas.md +1 -1
  33. package/dist/agent/DiagramExportMenu.md +1 -1
  34. package/dist/agent/DiagramInspector.md +1 -1
  35. package/dist/agent/DiagramLegend.md +1 -1
  36. package/dist/agent/DiagramMinimap.md +1 -1
  37. package/dist/agent/DiagramToolbar.md +1 -1
  38. package/dist/agent/Dialog.md +1 -1
  39. package/dist/agent/DropdownMenu.md +1 -1
  40. package/dist/agent/EmptyState.md +1 -1
  41. package/dist/agent/ErrorState.md +1 -1
  42. package/dist/agent/Facet.md +1 -1
  43. package/dist/agent/Field.md +1 -1
  44. package/dist/agent/FigureBand.md +1 -1
  45. package/dist/agent/FloatingIconButton.md +1 -1
  46. package/dist/agent/FunnelChart.md +1 -1
  47. package/dist/agent/Heading.md +1 -1
  48. package/dist/agent/Heatmap.md +1 -1
  49. package/dist/agent/Histogram.md +1 -1
  50. package/dist/agent/Input.md +1 -1
  51. package/dist/agent/Kbd.md +1 -1
  52. package/dist/agent/LifecycleFigure.md +1 -1
  53. package/dist/agent/LineChart.md +1 -1
  54. package/dist/agent/LinkArrow.md +1 -1
  55. package/dist/agent/Markdown.md +1 -1
  56. package/dist/agent/NativeSelect.md +1 -1
  57. package/dist/agent/NavItem.md +1 -1
  58. package/dist/agent/NumberField.md +72 -0
  59. package/dist/agent/Pagination.md +1 -1
  60. package/dist/agent/PieChart.md +1 -1
  61. package/dist/agent/Popover.md +1 -1
  62. package/dist/agent/Progress.md +1 -1
  63. package/dist/agent/RadarChart.md +1 -1
  64. package/dist/agent/RadialChart.md +1 -1
  65. package/dist/agent/RadioGroup.md +1 -1
  66. package/dist/agent/SankeyChart.md +1 -1
  67. package/dist/agent/ScatterChart.md +1 -1
  68. package/dist/agent/ScrollArea.md +1 -1
  69. package/dist/agent/SearchableMenu.md +1 -1
  70. package/dist/agent/Select.md +1 -1
  71. package/dist/agent/Separator.md +1 -1
  72. package/dist/agent/SequenceFigure.md +1 -1
  73. package/dist/agent/Sheet.md +1 -1
  74. package/dist/agent/Sidebar.md +193 -0
  75. package/dist/agent/Skeleton.md +1 -1
  76. package/dist/agent/Slider.md +7 -3
  77. package/dist/agent/Sparkline.md +1 -1
  78. package/dist/agent/Spinner.md +1 -1
  79. package/dist/agent/StatusDot.md +1 -1
  80. package/dist/agent/StatusPill.md +1 -1
  81. package/dist/agent/Steps.md +1 -1
  82. package/dist/agent/Switch.md +1 -1
  83. package/dist/agent/Table.md +3 -1
  84. package/dist/agent/Tabs.md +1 -1
  85. package/dist/agent/Tag.md +1 -1
  86. package/dist/agent/Text.md +1 -1
  87. package/dist/agent/Textarea.md +1 -1
  88. package/dist/agent/Timestamp.md +1 -1
  89. package/dist/agent/Toast.md +1 -1
  90. package/dist/agent/ToggleGroup.md +1 -1
  91. package/dist/agent/Toolbar.md +1 -1
  92. package/dist/agent/Tooltip.md +1 -1
  93. package/dist/agent/TreemapChart.md +1 -1
  94. package/dist/agent/WaterfallChart.md +1 -1
  95. package/dist/agent/WorkflowFigure.md +1 -1
  96. package/dist/agent/catalog.json +361 -2
  97. package/dist/agent/index.md +8 -5
  98. package/dist/charts/BulletChart/BulletChart.js +16 -2
  99. package/dist/charts/BulletChart/BulletChart.js.map +1 -1
  100. package/dist/charts/RadarChart/RadarChart.js +4 -2
  101. package/dist/charts/RadarChart/RadarChart.js.map +1 -1
  102. package/dist/components/ColorPicker/ColorPicker.d.ts +67 -0
  103. package/dist/components/ColorPicker/ColorPicker.js +334 -0
  104. package/dist/components/ColorPicker/ColorPicker.js.map +1 -0
  105. package/dist/components/Command/Command.js +1 -1
  106. package/dist/components/Command/Command.js.map +1 -1
  107. package/dist/components/ContextMenu/ContextMenu.js +1 -1
  108. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  109. package/dist/components/Dialog/Dialog.js +1 -1
  110. package/dist/components/Dialog/Dialog.js.map +1 -1
  111. package/dist/components/DropdownMenu/DropdownMenu.js +1 -1
  112. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  113. package/dist/components/ErrorState/ErrorState.js +1 -1
  114. package/dist/components/ErrorState/ErrorState.js.map +1 -1
  115. package/dist/components/NavItem/NavItem.js +1 -1
  116. package/dist/components/NavItem/NavItem.js.map +1 -1
  117. package/dist/components/NumberField/NumberField.d.ts +69 -0
  118. package/dist/components/NumberField/NumberField.js +162 -0
  119. package/dist/components/NumberField/NumberField.js.map +1 -0
  120. package/dist/components/Popover/Popover.js +1 -1
  121. package/dist/components/Popover/Popover.js.map +1 -1
  122. package/dist/components/Select/Select.js +1 -1
  123. package/dist/components/Select/Select.js.map +1 -1
  124. package/dist/components/Sheet/Sheet.js +1 -1
  125. package/dist/components/Sheet/Sheet.js.map +1 -1
  126. package/dist/components/Sidebar/Sidebar.d.ts +349 -0
  127. package/dist/components/Sidebar/Sidebar.js +455 -0
  128. package/dist/components/Sidebar/Sidebar.js.map +1 -0
  129. package/dist/components/Slider/Slider.d.ts +22 -1
  130. package/dist/components/Slider/Slider.js +72 -8
  131. package/dist/components/Slider/Slider.js.map +1 -1
  132. package/dist/components/Table/Table.d.ts +14 -0
  133. package/dist/components/Table/Table.js.map +1 -1
  134. package/dist/components/Tabs/Tabs.js +7 -1
  135. package/dist/components/Tabs/Tabs.js.map +1 -1
  136. package/dist/diagrams/lib/route.d.ts +22 -1
  137. package/dist/diagrams/lib/route.js +24 -1
  138. package/dist/diagrams/lib/route.js.map +1 -1
  139. package/dist/diagrams/lib/wires.js +9 -5
  140. package/dist/diagrams/lib/wires.js.map +1 -1
  141. package/dist/index.d.ts +3 -0
  142. package/dist/index.js +3 -0
  143. package/dist/index.js.map +1 -1
  144. package/dist/lib/color.d.ts +80 -0
  145. package/dist/lib/color.js +189 -0
  146. package/dist/lib/color.js.map +1 -0
  147. package/dist/lib/numeric.d.ts +35 -0
  148. package/dist/lib/numeric.js +29 -0
  149. package/dist/lib/numeric.js.map +1 -0
  150. package/dist/styles.css +1 -1
  151. package/dist/themes.css +8 -0
  152. package/dist/tokens.css +66 -3
  153. package/dist/tokens.d.ts +3 -0
  154. package/dist/tokens.js +17 -0
  155. package/dist/tokens.json +17 -0
  156. package/package.json +1 -1
  157. package/skills/misoto22-design/rules/forms.md +3 -1
@@ -1,15 +1,22 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as SliderPrimitive from "@radix-ui/react-slider";
4
- import { useState } from "react";
4
+ import { Fragment, useRef, useState } from "react";
5
5
  import { cn } from "../../lib/cn.js";
6
+ import { clampToRange, parseNumber, snapToStep } from "../../lib/numeric.js";
6
7
  import { warnBlankName } from "../../lib/warn.js";
7
8
  function Slider({
8
9
  label,
9
10
  showValue = false,
11
+ editable = false,
10
12
  format,
11
13
  className,
12
14
  onValueChange,
15
+ value: controlledValue,
16
+ defaultValue,
17
+ min = 0,
18
+ max = 100,
19
+ step = 1,
13
20
  "aria-describedby": describedBy,
14
21
  "aria-required": ariaRequired,
15
22
  "aria-invalid": ariaInvalid,
@@ -17,16 +24,30 @@ function Slider({
17
24
  }) {
18
25
  const names = Array.isArray(label) ? label : [label];
19
26
  warnBlankName("Slider", "label", names[0], "the thumb is announced with no name and its value describes nothing");
20
- const [uncontrolled, setUncontrolled] = useState(
21
- props.defaultValue ?? [props.min ?? 0]
22
- );
23
- const current = props.value ?? uncontrolled;
27
+ const [uncontrolled, setUncontrolled] = useState(defaultValue ?? [min]);
28
+ const current = controlledValue ?? uncontrolled;
29
+ const [draft, setDraft] = useState(null);
30
+ const draftRef = useRef(null);
31
+ const edit = (next) => {
32
+ draftRef.current = next;
33
+ setDraft(next);
34
+ };
24
35
  const handleChange = (next) => {
25
- if (props.value === void 0) setUncontrolled(next);
36
+ if (controlledValue === void 0) setUncontrolled(next);
26
37
  onValueChange?.(next);
27
38
  };
39
+ const commit = (index, text) => {
40
+ const parsed = parseNumber(text);
41
+ if (parsed === null) return false;
42
+ const lower = index > 0 ? current[index - 1] ?? min : min;
43
+ const upper = index < current.length - 1 ? current[index + 1] ?? max : max;
44
+ const next = clampToRange(snapToStep(parsed, step, min), Math.max(min, lower), Math.min(max, upper));
45
+ if (next !== current[index]) handleChange(current.map((value, i) => i === index ? next : value));
46
+ return true;
47
+ };
28
48
  const print = format ?? String;
29
49
  const heading = names.length > 1 ? current.map((_, index) => names[index] ?? names[0]).join(" \u2013 ") : names[0];
50
+ const readout = showValue || editable;
30
51
  return /* @__PURE__ */ jsxs(
31
52
  "div",
32
53
  {
@@ -40,14 +61,57 @@ function Slider({
40
61
  className
41
62
  ),
42
63
  children: [
43
- showValue && /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between mono-meta text-(--ink-3-aa)", children: [
64
+ readout && /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-4 mono-meta text-(--ink-3-aa)", children: [
44
65
  /* @__PURE__ */ jsx("span", { children: heading }),
45
- /* @__PURE__ */ jsx("span", { className: "tabular-nums text-(--ink)", children: current.map((value) => print(value)).join(" \u2013 ") })
66
+ editable ? /* @__PURE__ */ jsx("span", { className: "flex items-baseline gap-1.5", children: current.map((value, index) => {
67
+ const editing = draft?.index === index;
68
+ const text = editing ? draft.text : print(value);
69
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
70
+ index > 0 && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2013" }),
71
+ /* @__PURE__ */ jsx(
72
+ "input",
73
+ {
74
+ type: "text",
75
+ inputMode: "decimal",
76
+ disabled: props.disabled,
77
+ value: text,
78
+ "aria-label": `${names[index] ?? names[0]}, exact value`,
79
+ "aria-describedby": describedBy,
80
+ "aria-required": ariaRequired,
81
+ "aria-invalid": editing && parseNumber(draft.text) === null || ariaInvalid || void 0,
82
+ onFocus: () => edit({ index, text: String(value) }),
83
+ onChange: (event) => edit({ index, text: event.target.value }),
84
+ onBlur: () => {
85
+ const pending = draftRef.current;
86
+ if (pending?.index === index) commit(index, pending.text);
87
+ edit(null);
88
+ },
89
+ onKeyDown: (event) => {
90
+ const pending = draftRef.current;
91
+ if (event.key === "Enter") {
92
+ event.preventDefault();
93
+ if (pending?.index === index && commit(index, pending.text)) edit(null);
94
+ }
95
+ if (event.key === "Escape") {
96
+ event.preventDefault();
97
+ edit(null);
98
+ event.currentTarget.blur();
99
+ }
100
+ },
101
+ className: "w-20 rounded-(--radius-sm) border border-(--rule-2) bg-(--paper) px-1.5 py-0.5 text-end tabular-nums text-(--ink) transition-colors duration-(--duration-fast) hover:border-(--rule-hard) focus:border-(--ink) aria-invalid:border-(--danger)"
102
+ }
103
+ )
104
+ ] }, index);
105
+ }) }) : /* @__PURE__ */ jsx("span", { className: "tabular-nums text-(--ink)", children: current.map((value) => print(value)).join(" \u2013 ") })
46
106
  ] }),
47
107
  /* @__PURE__ */ jsxs(
48
108
  SliderPrimitive.Root,
49
109
  {
50
110
  className: "group relative flex w-full touch-none select-none items-center py-3",
111
+ value: current,
112
+ min,
113
+ max,
114
+ step,
51
115
  onValueChange: handleChange,
52
116
  ...props,
53
117
  children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/Slider/Slider.tsx"],"sourcesContent":["'use client'\n\nimport * as SliderPrimitive from '@radix-ui/react-slider'\nimport { useState, type ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\nimport { warnBlankName } from '../../lib/warn'\n\nexport interface SliderProps extends ComponentProps<typeof SliderPrimitive.Root> {\n /**\n * Names the control. Required: a slider with no name announces only a number,\n * and a number with no noun is not information.\n *\n * A range slider (two thumbs) needs one name per thumb — pass an array.\n */\n label: string | [string, string]\n /**\n * Prints the current value beside the label.\n *\n * A two-thumb range prints both names and both values, in the same order —\n * the heading used to print the first name over a pair of numbers, which read\n * as \"Minimum\" over \"10 – 90\".\n */\n showValue?: boolean\n /**\n * Renders the value with a unit or a currency, e.g. `(n) => n + '%'`.\n *\n * Reaches assistive tech as well as the readout: it becomes each thumb's\n * `aria-valuetext`, so a thumb showing \"$1,200\" announces that rather than the\n * bare number. Left off, the platform announces the value itself.\n */\n format?: (value: number) => string\n}\n\n/**\n * A value chosen along a range.\n *\n * Radix owns the keyboard contract — arrows step, Page keys jump, Home and End\n * reach the ends — and the ARIA that reports the value. What is here is the\n * look, and the labelling, which is the part a slider most often gets wrong: a\n * thumb that announces \"42\" and nothing else leaves a screen reader user with a\n * number and no idea what it measures.\n *\n * A 44px hit area sits invisibly around the 16px thumb, because a thumb sized\n * for the design is well under any pointer-target guideline.\n *\n * Inside a `Field`, the hint, the error and the requirement land on the THUMB,\n * which is the element carrying `role=\"slider\"` — on the root they would sit on\n * a `<span>` with no role and announce nothing. The NAME still comes from\n * `label` here: a field's label above a slider points at that same roleless\n * root, so it neither names the control nor clicks through to it.\n *\n * @example\n * <Slider label=\"Quality\" defaultValue={[80]} max={100} step={5} showValue format={(n) => `${n}%`} />\n */\nexport function Slider({\n label,\n showValue = false,\n format,\n className,\n onValueChange,\n 'aria-describedby': describedBy,\n 'aria-required': ariaRequired,\n 'aria-invalid': ariaInvalid,\n ...props\n}: SliderProps) {\n const names = Array.isArray(label) ? label : [label]\n\n // The other five guarded naming props are plain strings; this one is a\n // string or a pair, so the first name is what is checked — a range whose\n // lower thumb has no name is the same silence as a single one with none.\n // It catches a blank, not an absent: `label` is required, so a missing one\n // is a type error everywhere except a caller that has already left the\n // types behind.\n warnBlankName('Slider', 'label', names[0], 'the thumb is announced with no name and its value describes nothing')\n\n // Tracked here, not read off the props. An uncontrolled slider's\n // `defaultValue` never changes, so the printed figure sat at its starting\n // number while the thumb moved — the one thing `showValue` exists to avoid.\n //\n // The fallback is the primitive's own — one thumb at the minimum. The thumbs\n // are rendered from THIS array rather than from Radix's, so an empty one is a\n // track with nothing on it to drag, which is what `<Slider label=\"Volume\" />`\n // used to render.\n const [uncontrolled, setUncontrolled] = useState<number[]>(\n props.defaultValue ?? [props.min ?? 0],\n )\n const current = props.value ?? uncontrolled\n\n const handleChange = (next: number[]) => {\n if (props.value === undefined) setUncontrolled(next)\n onValueChange?.(next)\n }\n\n const print = format ?? String\n // One name per thumb, in the thumbs' own order, so the heading lines up with\n // the numbers under it. A single name stays a single name however many thumbs\n // there are — a price filter called \"Price\" is not \"Price – Price\".\n const heading =\n names.length > 1 ? current.map((_, index) => names[index] ?? names[0]).join(' – ') : names[0]\n\n return (\n <div\n className={cn(\n 'flex w-full flex-col gap-3',\n // On the wrapper, and driven by the prop: the thumb is a <span>, so a\n // `disabled:` variant compiles to `&:disabled` and matches nothing —\n // the control was drawn exactly like a live one while refusing to move.\n // The wrapper is also what dims the readout above the track.\n props.disabled && 'opacity-(--disabled-opacity) pointer-events-none',\n className,\n )}\n >\n {showValue && (\n <div className=\"flex items-baseline justify-between mono-meta text-(--ink-3-aa)\">\n <span>{heading}</span>\n <span className=\"tabular-nums text-(--ink)\">\n {current.map((value) => print(value)).join(' – ')}\n </span>\n </div>\n )}\n <SliderPrimitive.Root\n className=\"group relative flex w-full touch-none select-none items-center py-3\"\n onValueChange={handleChange}\n {...props}\n >\n <SliderPrimitive.Track className=\"relative h-1 w-full grow overflow-hidden rounded-(--radius-pill) bg-(--stone) transition-[height] duration-(--duration-fast) group-hover:h-1.5\">\n <SliderPrimitive.Range className=\"absolute h-full bg-(--accent)\" />\n </SliderPrimitive.Track>\n {current.map((value, index) => (\n <SliderPrimitive.Thumb\n key={index}\n aria-label={names[index] ?? names[0]}\n // The thumb is what carries role=\"slider\", so this is where a\n // Field's description and requirement have to land — on the root\n // they sit on a <span> with no role, which announces nothing.\n aria-describedby={describedBy}\n aria-required={ariaRequired}\n aria-invalid={ariaInvalid}\n // Only when a format was given: setting it otherwise would replace\n // the platform's own reading of the value with the same digits.\n aria-valuetext={format ? format(value) : undefined}\n // The visible thumb is 16px; the `before` pseudo-element widens the\n // hit area to 44px without changing what is drawn.\n // `transition-transform` and not `all`: the thumb's own position is\n // set by Radix as a `left` percentage, and transitioning that would\n // make it lag the pointer. Only the grow-on-grab is animated.\n className=\"relative block size-4 rounded-(--radius-pill) border border-(--accent) bg-(--paper) transition-[transform,background-color] duration-(--duration-fast) ease-(--ease-out-expo) before:absolute before:left-1/2 before:top-1/2 before:size-11 before:-translate-x-1/2 before:-translate-y-1/2 before:content-[''] hover:scale-110 hover:bg-(--stone) active:scale-125 data-[state=active]:scale-125 motion-reduce:transition-none\"\n />\n ))}\n </SliderPrimitive.Root>\n </div>\n )\n}\n\nexport default Slider\n"],"mappings":";AAiHQ,SACE,KADF;AA/GR,YAAY,qBAAqB;AACjC,SAAS,gBAAqC;AAC9C,SAAS,UAAU;AACnB,SAAS,qBAAqB;AAiDvB,SAAS,OAAO;AAAA,EACrB;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,GAAG;AACL,GAAgB;AACd,QAAM,QAAQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AAQnD,gBAAc,UAAU,SAAS,MAAM,CAAC,GAAG,qEAAqE;AAUhH,QAAM,CAAC,cAAc,eAAe,IAAI;AAAA,IACtC,MAAM,gBAAgB,CAAC,MAAM,OAAO,CAAC;AAAA,EACvC;AACA,QAAM,UAAU,MAAM,SAAS;AAE/B,QAAM,eAAe,CAAC,SAAmB;AACvC,QAAI,MAAM,UAAU,OAAW,iBAAgB,IAAI;AACnD,oBAAgB,IAAI;AAAA,EACtB;AAEA,QAAM,QAAQ,UAAU;AAIxB,QAAM,UACJ,MAAM,SAAS,IAAI,QAAQ,IAAI,CAAC,GAAG,UAAU,MAAM,KAAK,KAAK,MAAM,CAAC,CAAC,EAAE,KAAK,UAAK,IAAI,MAAM,CAAC;AAE9F,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA,MAAM,YAAY;AAAA,QAClB;AAAA,MACF;AAAA,MAEC;AAAA,qBACC,qBAAC,SAAI,WAAU,mEACb;AAAA,8BAAC,UAAM,mBAAQ;AAAA,UACf,oBAAC,UAAK,WAAU,6BACb,kBAAQ,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAAE,KAAK,UAAK,GAClD;AAAA,WACF;AAAA,QAEF;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,WAAU;AAAA,YACV,eAAe;AAAA,YACd,GAAG;AAAA,YAEJ;AAAA,kCAAC,gBAAgB,OAAhB,EAAsB,WAAU,kJAC/B,8BAAC,gBAAgB,OAAhB,EAAsB,WAAU,iCAAgC,GACnE;AAAA,cACC,QAAQ,IAAI,CAAC,OAAO,UACnB;AAAA,gBAAC,gBAAgB;AAAA,gBAAhB;AAAA,kBAEC,cAAY,MAAM,KAAK,KAAK,MAAM,CAAC;AAAA,kBAInC,oBAAkB;AAAA,kBAClB,iBAAe;AAAA,kBACf,gBAAc;AAAA,kBAGd,kBAAgB,SAAS,OAAO,KAAK,IAAI;AAAA,kBAMzC,WAAU;AAAA;AAAA,gBAhBL;AAAA,cAiBP,CACD;AAAA;AAAA;AAAA,QACH;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,iBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/Slider/Slider.tsx"],"sourcesContent":["'use client'\n\nimport * as SliderPrimitive from '@radix-ui/react-slider'\nimport { Fragment, useRef, useState, type ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\nimport { clampToRange, parseNumber, snapToStep } from '../../lib/numeric'\nimport { warnBlankName } from '../../lib/warn'\n\nexport interface SliderProps extends ComponentProps<typeof SliderPrimitive.Root> {\n /**\n * Names the control. Required: a slider with no name announces only a number,\n * and a number with no noun is not information.\n *\n * A range slider (two thumbs) needs one name per thumb — pass an array.\n */\n label: string | [string, string]\n /**\n * Prints the current value beside the label.\n *\n * A two-thumb range prints both names and both values, in the same order —\n * the heading used to print the first name over a pair of numbers, which read\n * as \"Minimum\" over \"10 – 90\".\n */\n showValue?: boolean\n /**\n * Turns that readout into a box the number can be typed into, and implies\n * `showValue`.\n *\n * A slider is a control for a NEIGHBOURHOOD; someone who needs 1,150 rather\n * than roughly 1,200 is dragging a 16px thumb across a hundred steps to get\n * it. This is the way out, in the place the value already is, rather than a\n * second field beside the track that has to be kept in step by hand.\n *\n * The box shows `format`'s output at rest and the bare number while it has\n * focus, so a reader still sees \"$1,200\" and a typist is never asked to type\n * a currency symbol back.\n */\n editable?: boolean\n /**\n * Renders the value with a unit or a currency, e.g. `(n) => n + '%'`.\n *\n * Reaches assistive tech as well as the readout: it becomes each thumb's\n * `aria-valuetext`, so a thumb showing \"$1,200\" announces that rather than the\n * bare number. Left off, the platform announces the value itself.\n */\n format?: (value: number) => string\n}\n\n/** Which box is being typed into, and what is in it. Only one can have focus. */\ninterface Draft {\n index: number\n text: string\n}\n\n/**\n * A value chosen along a range.\n *\n * Radix owns the keyboard contract — arrows step, Page keys jump, Home and End\n * reach the ends — and the ARIA that reports the value. What is here is the\n * look, and the labelling, which is the part a slider most often gets wrong: a\n * thumb that announces \"42\" and nothing else leaves a screen reader user with a\n * number and no idea what it measures.\n *\n * A 44px hit area sits invisibly around the 16px thumb, because a thumb sized\n * for the design is well under any pointer-target guideline.\n *\n * `editable` is the answer to the thing a slider cannot do. Reach for it\n * whenever the exact figure is the point — a budget, a timeout, a price — and\n * leave it off when the value is genuinely approximate, because a box invites\n * precision the setting may not have.\n *\n * Inside a `Field`, the hint, the error and the requirement land on the THUMB,\n * which is the element carrying `role=\"slider\"` — on the root they would sit on\n * a `<span>` with no role and announce nothing. The NAME still comes from\n * `label` here: a field's label above a slider points at that same roleless\n * root, so it neither names the control nor clicks through to it.\n *\n * @example\n * <Slider label=\"Quality\" defaultValue={[80]} max={100} step={5} showValue format={(n) => `${n}%`} />\n * @example\n * <Slider label=\"Monthly budget\" defaultValue={[1200]} max={5000} step={50} editable format={(n) => `$${n}`} />\n */\nexport function Slider({\n label,\n showValue = false,\n editable = false,\n format,\n className,\n onValueChange,\n value: controlledValue,\n defaultValue,\n min = 0,\n max = 100,\n step = 1,\n 'aria-describedby': describedBy,\n 'aria-required': ariaRequired,\n 'aria-invalid': ariaInvalid,\n ...props\n}: SliderProps) {\n const names = Array.isArray(label) ? label : [label]\n\n // The other five guarded naming props are plain strings; this one is a\n // string or a pair, so the first name is what is checked — a range whose\n // lower thumb has no name is the same silence as a single one with none.\n // It catches a blank, not an absent: `label` is required, so a missing one\n // is a type error everywhere except a caller that has already left the\n // types behind.\n warnBlankName('Slider', 'label', names[0], 'the thumb is announced with no name and its value describes nothing')\n\n // Tracked here, not read off the props. An uncontrolled slider's\n // `defaultValue` never changes, so the printed figure sat at its starting\n // number while the thumb moved — the one thing `showValue` exists to avoid.\n //\n // The fallback is the primitive's own — one thumb at the minimum. The thumbs\n // are rendered from THIS array rather than from Radix's, so an empty one is a\n // track with nothing on it to drag, which is what `<Slider label=\"Volume\" />`\n // used to render.\n const [uncontrolled, setUncontrolled] = useState<number[]>(defaultValue ?? [min])\n const current = controlledValue ?? uncontrolled\n\n // Mirrored into a ref because Escape clears the draft and blurs the box in\n // the SAME event: the state update has not been applied by the time the\n // blur handler runs, so a handler reading the state variable would see the\n // abandoned text and commit exactly what Escape was pressed to discard.\n const [draft, setDraft] = useState<Draft | null>(null)\n const draftRef = useRef<Draft | null>(null)\n const edit = (next: Draft | null) => {\n draftRef.current = next\n setDraft(next)\n }\n\n const handleChange = (next: number[]) => {\n if (controlledValue === undefined) setUncontrolled(next)\n onValueChange?.(next)\n }\n\n /**\n * Commits one typed box, or reports that there was no number in it.\n *\n * A thumb is also bounded by its NEIGHBOURS, which is the bound a typed\n * value can cross and a dragged one cannot: typing 90 into the lower end of a\n * range whose upper end is at 70 would otherwise hand Radix an unsorted array\n * and leave the two thumbs crossed over.\n */\n const commit = (index: number, text: string): boolean => {\n const parsed = parseNumber(text)\n if (parsed === null) return false\n const lower = index > 0 ? (current[index - 1] ?? min) : min\n const upper = index < current.length - 1 ? (current[index + 1] ?? max) : max\n const next = clampToRange(snapToStep(parsed, step, min), Math.max(min, lower), Math.min(max, upper))\n if (next !== current[index]) handleChange(current.map((value, i) => (i === index ? next : value)))\n return true\n }\n\n const print = format ?? String\n // One name per thumb, in the thumbs' own order, so the heading lines up with\n // the numbers under it. A single name stays a single name however many thumbs\n // there are — a price filter called \"Price\" is not \"Price – Price\".\n const heading =\n names.length > 1 ? current.map((_, index) => names[index] ?? names[0]).join(' – ') : names[0]\n\n const readout = showValue || editable\n\n return (\n <div\n className={cn(\n 'flex w-full flex-col gap-3',\n // On the wrapper, and driven by the prop: the thumb is a <span>, so a\n // `disabled:` variant compiles to `&:disabled` and matches nothing —\n // the control was drawn exactly like a live one while refusing to move.\n // The wrapper is also what dims the readout above the track.\n props.disabled && 'opacity-(--disabled-opacity) pointer-events-none',\n className,\n )}\n >\n {readout && (\n <div className=\"flex items-baseline justify-between gap-4 mono-meta text-(--ink-3-aa)\">\n <span>{heading}</span>\n {editable ? (\n <span className=\"flex items-baseline gap-1.5\">\n {current.map((value, index) => {\n const editing = draft?.index === index\n const text = editing ? draft.text : print(value)\n return (\n <Fragment key={index}>\n {index > 0 && <span aria-hidden=\"true\">–</span>}\n <input\n type=\"text\"\n // Not `type=\"number\"`: the box holds `format`'s output at\n // rest — \"$1,200\" — which a number input refuses to\n // display and silently blanks. `inputMode` is what still\n // brings up the numeric keypad on a phone.\n inputMode=\"decimal\"\n // `disabled` on the wrapper is `pointer-events-none`,\n // which a keyboard walks straight past. Without this, the\n // one editable part of a disabled slider stays editable.\n disabled={props.disabled}\n value={text}\n // The name has to differ from the thumb's: both carry the\n // same value, and two controls announcing \"Quality\" is a\n // reader hearing the same control twice.\n aria-label={`${names[index] ?? names[0]}, exact value`}\n aria-describedby={describedBy}\n aria-required={ariaRequired}\n aria-invalid={(editing && parseNumber(draft.text) === null) || ariaInvalid || undefined}\n onFocus={() => edit({ index, text: String(value) })}\n onChange={(event) => edit({ index, text: event.target.value })}\n onBlur={() => {\n const pending = draftRef.current\n if (pending?.index === index) commit(index, pending.text)\n edit(null)\n }}\n onKeyDown={(event) => {\n const pending = draftRef.current\n if (event.key === 'Enter') {\n event.preventDefault()\n if (pending?.index === index && commit(index, pending.text)) edit(null)\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n edit(null)\n event.currentTarget.blur()\n }\n }}\n className=\"w-20 rounded-(--radius-sm) border border-(--rule-2) bg-(--paper) px-1.5 py-0.5 text-end tabular-nums text-(--ink) transition-colors duration-(--duration-fast) hover:border-(--rule-hard) focus:border-(--ink) aria-invalid:border-(--danger)\"\n />\n </Fragment>\n )\n })}\n </span>\n ) : (\n <span className=\"tabular-nums text-(--ink)\">\n {current.map((value) => print(value)).join(' – ')}\n </span>\n )}\n </div>\n )}\n {/* Controlled from THIS component's state even when the caller left it\n uncontrolled, because the thumbs have to answer to more than dragging:\n a number typed into the readout never passes through Radix, so a Root\n holding its own `defaultValue` would keep the thumb where it was and\n leave the figure above it disagreeing with the track. `min`, `max` and\n `step` are passed on rather than spread for the same reason — the\n typed value is reconciled against them here. */}\n <SliderPrimitive.Root\n className=\"group relative flex w-full touch-none select-none items-center py-3\"\n value={current}\n min={min}\n max={max}\n step={step}\n onValueChange={handleChange}\n {...props}\n >\n <SliderPrimitive.Track className=\"relative h-1 w-full grow overflow-hidden rounded-(--radius-pill) bg-(--stone) transition-[height] duration-(--duration-fast) group-hover:h-1.5\">\n <SliderPrimitive.Range className=\"absolute h-full bg-(--accent)\" />\n </SliderPrimitive.Track>\n {current.map((value, index) => (\n <SliderPrimitive.Thumb\n key={index}\n aria-label={names[index] ?? names[0]}\n // The thumb is what carries role=\"slider\", so this is where a\n // Field's description and requirement have to land — on the root\n // they sit on a <span> with no role, which announces nothing.\n aria-describedby={describedBy}\n aria-required={ariaRequired}\n aria-invalid={ariaInvalid}\n // Only when a format was given: setting it otherwise would replace\n // the platform's own reading of the value with the same digits.\n aria-valuetext={format ? format(value) : undefined}\n // The visible thumb is 16px; the `before` pseudo-element widens the\n // hit area to 44px without changing what is drawn.\n // `transition-transform` and not `all`: the thumb's own position is\n // set by Radix as a `left` percentage, and transitioning that would\n // make it lag the pointer. Only the grow-on-grab is animated.\n className=\"relative block size-4 rounded-(--radius-pill) border border-(--accent) bg-(--paper) transition-[transform,background-color] duration-(--duration-fast) ease-(--ease-out-expo) before:absolute before:left-1/2 before:top-1/2 before:size-11 before:-translate-x-1/2 before:-translate-y-1/2 before:content-[''] hover:scale-110 hover:bg-(--stone) active:scale-125 data-[state=active]:scale-125 motion-reduce:transition-none\"\n />\n ))}\n </SliderPrimitive.Root>\n </div>\n )\n}\n\nexport default Slider\n"],"mappings":";AAiLU,cAOQ,YAPR;AA/KV,YAAY,qBAAqB;AACjC,SAAS,UAAU,QAAQ,gBAAqC;AAChE,SAAS,UAAU;AACnB,SAAS,cAAc,aAAa,kBAAkB;AACtD,SAAS,qBAAqB;AA4EvB,SAAS,OAAO;AAAA,EACrB;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,oBAAoB;AAAA,EACpB,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,GAAG;AACL,GAAgB;AACd,QAAM,QAAQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AAQnD,gBAAc,UAAU,SAAS,MAAM,CAAC,GAAG,qEAAqE;AAUhH,QAAM,CAAC,cAAc,eAAe,IAAI,SAAmB,gBAAgB,CAAC,GAAG,CAAC;AAChF,QAAM,UAAU,mBAAmB;AAMnC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,WAAW,OAAqB,IAAI;AAC1C,QAAM,OAAO,CAAC,SAAuB;AACnC,aAAS,UAAU;AACnB,aAAS,IAAI;AAAA,EACf;AAEA,QAAM,eAAe,CAAC,SAAmB;AACvC,QAAI,oBAAoB,OAAW,iBAAgB,IAAI;AACvD,oBAAgB,IAAI;AAAA,EACtB;AAUA,QAAM,SAAS,CAAC,OAAe,SAA0B;AACvD,UAAM,SAAS,YAAY,IAAI;AAC/B,QAAI,WAAW,KAAM,QAAO;AAC5B,UAAM,QAAQ,QAAQ,IAAK,QAAQ,QAAQ,CAAC,KAAK,MAAO;AACxD,UAAM,QAAQ,QAAQ,QAAQ,SAAS,IAAK,QAAQ,QAAQ,CAAC,KAAK,MAAO;AACzE,UAAM,OAAO,aAAa,WAAW,QAAQ,MAAM,GAAG,GAAG,KAAK,IAAI,KAAK,KAAK,GAAG,KAAK,IAAI,KAAK,KAAK,CAAC;AACnG,QAAI,SAAS,QAAQ,KAAK,EAAG,cAAa,QAAQ,IAAI,CAAC,OAAO,MAAO,MAAM,QAAQ,OAAO,KAAM,CAAC;AACjG,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,UAAU;AAIxB,QAAM,UACJ,MAAM,SAAS,IAAI,QAAQ,IAAI,CAAC,GAAG,UAAU,MAAM,KAAK,KAAK,MAAM,CAAC,CAAC,EAAE,KAAK,UAAK,IAAI,MAAM,CAAC;AAE9F,QAAM,UAAU,aAAa;AAE7B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA,MAAM,YAAY;AAAA,QAClB;AAAA,MACF;AAAA,MAEC;AAAA,mBACC,qBAAC,SAAI,WAAU,yEACb;AAAA,8BAAC,UAAM,mBAAQ;AAAA,UACd,WACC,oBAAC,UAAK,WAAU,+BACb,kBAAQ,IAAI,CAAC,OAAO,UAAU;AAC7B,kBAAM,UAAU,OAAO,UAAU;AACjC,kBAAM,OAAO,UAAU,MAAM,OAAO,MAAM,KAAK;AAC/C,mBACE,qBAAC,YACE;AAAA,sBAAQ,KAAK,oBAAC,UAAK,eAAY,QAAO,oBAAC;AAAA,cACxC;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBAKL,WAAU;AAAA,kBAIV,UAAU,MAAM;AAAA,kBAChB,OAAO;AAAA,kBAIP,cAAY,GAAG,MAAM,KAAK,KAAK,MAAM,CAAC,CAAC;AAAA,kBACvC,oBAAkB;AAAA,kBAClB,iBAAe;AAAA,kBACf,gBAAe,WAAW,YAAY,MAAM,IAAI,MAAM,QAAS,eAAe;AAAA,kBAC9E,SAAS,MAAM,KAAK,EAAE,OAAO,MAAM,OAAO,KAAK,EAAE,CAAC;AAAA,kBAClD,UAAU,CAAC,UAAU,KAAK,EAAE,OAAO,MAAM,MAAM,OAAO,MAAM,CAAC;AAAA,kBAC7D,QAAQ,MAAM;AACZ,0BAAM,UAAU,SAAS;AACzB,wBAAI,SAAS,UAAU,MAAO,QAAO,OAAO,QAAQ,IAAI;AACxD,yBAAK,IAAI;AAAA,kBACX;AAAA,kBACA,WAAW,CAAC,UAAU;AACpB,0BAAM,UAAU,SAAS;AACzB,wBAAI,MAAM,QAAQ,SAAS;AACzB,4BAAM,eAAe;AACrB,0BAAI,SAAS,UAAU,SAAS,OAAO,OAAO,QAAQ,IAAI,EAAG,MAAK,IAAI;AAAA,oBACxE;AACA,wBAAI,MAAM,QAAQ,UAAU;AAC1B,4BAAM,eAAe;AACrB,2BAAK,IAAI;AACT,4BAAM,cAAc,KAAK;AAAA,oBAC3B;AAAA,kBACF;AAAA,kBACA,WAAU;AAAA;AAAA,cACZ;AAAA,iBAzCa,KA0Cf;AAAA,UAEJ,CAAC,GACH,IAEA,oBAAC,UAAK,WAAU,6BACb,kBAAQ,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAAE,KAAK,UAAK,GAClD;AAAA,WAEJ;AAAA,QASF;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,WAAU;AAAA,YACV,OAAO;AAAA,YACP;AAAA,YACA;AAAA,YACA;AAAA,YACA,eAAe;AAAA,YACd,GAAG;AAAA,YAEJ;AAAA,kCAAC,gBAAgB,OAAhB,EAAsB,WAAU,kJAC/B,8BAAC,gBAAgB,OAAhB,EAAsB,WAAU,iCAAgC,GACnE;AAAA,cACC,QAAQ,IAAI,CAAC,OAAO,UACnB;AAAA,gBAAC,gBAAgB;AAAA,gBAAhB;AAAA,kBAEC,cAAY,MAAM,KAAK,KAAK,MAAM,CAAC;AAAA,kBAInC,oBAAkB;AAAA,kBAClB,iBAAe;AAAA,kBACf,gBAAc;AAAA,kBAGd,kBAAgB,SAAS,OAAO,KAAK,IAAI;AAAA,kBAMzC,WAAU;AAAA;AAAA,gBAhBL;AAAA,cAiBP,CACD;AAAA;AAAA;AAAA,QACH;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,iBAAQ;","names":[]}
@@ -97,6 +97,20 @@ interface TDProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'>
97
97
  /** Which edge the cell's contents sit against. Match the column's header. */
98
98
  align?: TableAlign;
99
99
  }
100
+ /**
101
+ * A body cell.
102
+ *
103
+ * TOP-aligned, and that is a choice about which kind of table is the common
104
+ * one here: a cell holding a paragraph. Top is what lines the first lines of a
105
+ * row up with each other, and centring a two-word term against a four-line
106
+ * description puts the term in the middle of nothing.
107
+ *
108
+ * It is the wrong default for the other kind — a row of one-line values with a
109
+ * control in it. A 36px button next to 16px of text makes a 52px row, and every
110
+ * other cell then hangs at the top of it with twenty pixels of nothing
111
+ * underneath, which reads as a column that has slipped. Pass
112
+ * `className="align-middle"` on those rows; the data-table template does.
113
+ */
100
114
  declare function TD({ align, className, ...rest }: TDProps): react.JSX.Element;
101
115
 
102
116
  export { type SortDirection, TBody, TD, type TDProps, TH, type THProps, THead, TR, Table, type TableAlign, type TableBorders, type TableProps, Table as default };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react'\nimport type { HTMLAttributes, TableHTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { warnBlankName } from '../../lib/warn'\n\nexport type TableAlign = 'start' | 'center' | 'end'\n\n/**\n * Which rules the table draws.\n *\n * `rows` is the default and the right answer for reading down a column: one\n * hairline between records and nothing else, so the eye tracks a line across\n * without a grid competing for it. `grid` adds the vertical rules a dense\n * numeric table needs to keep columns apart. `bordered` puts an edge around the\n * whole thing, for a table that sits loose on a page rather than inside a card\n * that already has one. `none` is for a table inside something that draws its\n * own structure.\n */\nexport type TableBorders = 'rows' | 'grid' | 'bordered' | 'bordered-grid' | 'none'\n\nconst ALIGN: Record<TableAlign, string> = {\n start: 'text-start',\n center: 'text-center',\n end: 'text-end',\n}\n\n/**\n * Every rule the table draws, expressed from ONE element.\n *\n * Not React context, which was the first attempt: `createContext` makes the\n * module client-only, which turned every table on every page into a hydration\n * boundary and took the static build down with it. Descendant selectors from\n * the wrapper reach the same cells, cost nothing at runtime, and keep the whole\n * component server-renderable.\n *\n * `--table-pad-x` rather than a padding class per mode, because the ruled\n * variants need symmetric padding inside their boxes while the plain one wants\n * a trailing gap and no leading one.\n */\nconst ROWS =\n '[&_thead]:border-b [&_thead]:border-(--rule-hard) [&_tbody_tr]:border-b [&_tbody_tr]:border-(--rule) [&_tbody_tr:last-child]:border-b-0'\nconst SOFT_HEAD = '[&_thead]:border-(--rule-2)'\nconst COLUMNS =\n '[&_th]:border-e [&_td]:border-e [&_th]:border-(--rule) [&_td]:border-(--rule) [&_th:last-child]:border-e-0 [&_td:last-child]:border-e-0'\nconst BOXED = 'rounded-(--radius) border border-(--rule-2) [--table-pad-x:0.75rem]'\n\nconst BORDERS: Record<TableBorders, string> = {\n rows: ROWS,\n grid: cn(ROWS, COLUMNS, '[--table-pad-x:0.75rem]'),\n bordered: cn(ROWS, SOFT_HEAD, BOXED),\n 'bordered-grid': cn(ROWS, SOFT_HEAD, COLUMNS, BOXED),\n none: '',\n}\n\nexport interface TableProps extends TableHTMLAttributes<HTMLTableElement> {\n /**\n * Describes the table for a screen reader, which cannot see the heading above\n * it. Rendered as a visually-hidden `<caption>` — required, because an\n * unnamed table in a page with three tables is unnavigable.\n */\n caption: string\n /** Prints the caption instead of hiding it. */\n showCaption?: boolean\n /**\n * Pins the header row while the body scrolls. Needs a bounded height on the\n * container — otherwise the page scrolls, not the table, and nothing sticks.\n */\n stickyHeader?: boolean\n /** Which rules to draw. See {@link TableBorders}. */\n borders?: TableBorders\n /** Tightens the row padding, for a table that is mostly numbers. */\n density?: 'comfortable' | 'compact'\n}\n\n/**\n * A ruled data table.\n *\n * Wrapped in its own horizontally scrolling container, because a table is the\n * one block that legitimately exceeds the measure — and a page that scrolls\n * sideways as a whole is a layout bug, while a table that does is a table.\n *\n * That container is focusable, and it has to be. A scrollable region whose\n * contents are not themselves focusable is unreachable by keyboard: there is no\n * element to Tab to and therefore no way to press an arrow key at it, so the\n * columns past the fold simply do not exist for anyone not using a mouse.\n *\n * It is also POSITIONED, which is not decoration. `sr-only` is\n * `position: absolute`, so a visually-hidden label in a cell resolves against\n * the nearest positioned ancestor — and with none between it and the document,\n * it escapes the scroll container and every `overflow-hidden` around it, and\n * widens the page by however far the table happens to be scrolled.\n *\n * No zebra striping at any border setting. In a monochrome system a striped row\n * is a second surface competing with the page ground, and the hairline between\n * rows is already enough to track a line across.\n *\n * @example\n * <Table caption=\"Deploy history\">\n * <THead><TR><TH>Commit</TH><TH align=\"end\" sortable sortDirection={dir} onSort={sort}>Duration</TH></TR></THead>\n * <TBody><TR><TD>a1b2c3d</TD><TD align=\"end\">2m 14s</TD></TR></TBody>\n * </Table>\n * @example\n * <Table caption=\"Inventory\" borders=\"bordered-grid\" density=\"compact\">…</Table>\n */\nexport function Table({\n caption,\n showCaption = false,\n stickyHeader = false,\n borders = 'rows',\n density = 'comfortable',\n className,\n children,\n ...rest\n}: TableProps) {\n warnBlankName('Table', 'caption', caption, 'the table is announced with no name and a reader arrives in it with no idea what it lists')\n return (\n <div\n tabIndex={0}\n role=\"region\"\n aria-label={caption}\n data-table-density={density}\n className={cn('relative w-full overflow-x-auto scroll-slim', BORDERS[borders])}\n >\n <table\n className={cn(\n 'w-full border-collapse text-start text-sm',\n stickyHeader &&\n '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-(--z-rule) [&_thead]:bg-(--paper)',\n className,\n )}\n {...rest}\n >\n <caption className={cn(showCaption ? 'pb-3 text-start eyebrow text-(--ink-3-aa)' : 'sr-only')}>\n {caption}\n </caption>\n {children}\n </table>\n </div>\n )\n}\n\n/** Header group. The rule under it is drawn by the table's border setting. */\nexport function THead({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <thead className={className} {...rest} />\n}\n\nexport function TBody({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <tbody className={className} {...rest} />\n}\n\nexport function TR({ className, ...rest }: HTMLAttributes<HTMLTableRowElement>) {\n return <tr className={className} {...rest} />\n}\n\nexport type SortDirection = 'ascending' | 'descending' | 'none'\n\nexport interface THProps extends Omit<ThHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the column's contents sit against. Numbers belong at `end`. */\n align?: TableAlign\n /** Makes the label a button and shows the sort marker. */\n sortable?: boolean\n /**\n * Which way this column is sorted. Also set as `aria-sort`, which is the\n * only way a screen reader learns a table is ordered at all — a caret drawn\n * in the header tells it nothing.\n */\n sortDirection?: SortDirection\n onSort?: () => void\n}\n\n/**\n * Cell padding, in one string both cells share.\n *\n * `--table-pad-x` is 0 by default and set by the ruled variants, so the plain\n * table keeps its trailing gap and no leading one while a boxed table gets\n * symmetric padding inside its border.\n */\nconst CELL =\n 'px-[var(--table-pad-x,0)] [&:not(:last-child)]:pe-[calc(var(--table-pad-x,0px)+1.5rem)]'\n\nconst SORT_ICON = {\n ascending: ArrowUp,\n descending: ArrowDown,\n none: ChevronsUpDown,\n} as const\n\n/**\n * A column label. Mono and uppercase, so it never reads as data.\n *\n * When sortable, the label becomes a `<button>` INSIDE the `<th>` rather than\n * the `<th>` becoming clickable: a cell with a click handler is not focusable\n * and not announced as a control, so the sort exists only for a mouse.\n *\n * Sorting is per column and opt-in. A table where every header is a button\n * invites the reader to try sorting a column the data cannot be ordered by.\n */\nexport function TH({\n align = 'start',\n sortable = false,\n sortDirection = 'none',\n onSort,\n className,\n children,\n ...rest\n}: THProps) {\n const Icon = SORT_ICON[sortDirection]\n\n return (\n <th\n scope=\"col\"\n aria-sort={sortable ? sortDirection : undefined}\n className={cn(CELL, 'py-3 align-bottom eyebrow text-(--ink-3-aa)', ALIGN[align], className)}\n {...rest}\n >\n {sortable ? (\n <button\n type=\"button\"\n onClick={onSort}\n className={cn(\n 'group inline-flex items-center gap-1.5 eyebrow transition-colors duration-(--duration-fast) hover:text-(--ink)',\n sortDirection === 'none' ? 'text-(--ink-3-aa)' : 'text-(--ink)',\n )}\n >\n {children}\n <Icon\n size={12}\n strokeWidth={2}\n aria-hidden\n className={cn(\n 'shrink-0 transition-opacity',\n sortDirection === 'none' && 'opacity-40 group-hover:opacity-100',\n )}\n />\n </button>\n ) : (\n children\n )}\n </th>\n )\n}\n\nexport interface TDProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the cell's contents sit against. Match the column's header. */\n align?: TableAlign\n}\n\nexport function TD({ align = 'start', className, ...rest }: TDProps) {\n return (\n <td\n className={cn(CELL, 'py-[var(--table-pad-y)] align-top text-(--ink-2)', ALIGN[align], className)}\n {...rest}\n />\n )\n}\n\nexport default Table\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react'\nimport type { HTMLAttributes, TableHTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { warnBlankName } from '../../lib/warn'\n\nexport type TableAlign = 'start' | 'center' | 'end'\n\n/**\n * Which rules the table draws.\n *\n * `rows` is the default and the right answer for reading down a column: one\n * hairline between records and nothing else, so the eye tracks a line across\n * without a grid competing for it. `grid` adds the vertical rules a dense\n * numeric table needs to keep columns apart. `bordered` puts an edge around the\n * whole thing, for a table that sits loose on a page rather than inside a card\n * that already has one. `none` is for a table inside something that draws its\n * own structure.\n */\nexport type TableBorders = 'rows' | 'grid' | 'bordered' | 'bordered-grid' | 'none'\n\nconst ALIGN: Record<TableAlign, string> = {\n start: 'text-start',\n center: 'text-center',\n end: 'text-end',\n}\n\n/**\n * Every rule the table draws, expressed from ONE element.\n *\n * Not React context, which was the first attempt: `createContext` makes the\n * module client-only, which turned every table on every page into a hydration\n * boundary and took the static build down with it. Descendant selectors from\n * the wrapper reach the same cells, cost nothing at runtime, and keep the whole\n * component server-renderable.\n *\n * `--table-pad-x` rather than a padding class per mode, because the ruled\n * variants need symmetric padding inside their boxes while the plain one wants\n * a trailing gap and no leading one.\n */\nconst ROWS =\n '[&_thead]:border-b [&_thead]:border-(--rule-hard) [&_tbody_tr]:border-b [&_tbody_tr]:border-(--rule) [&_tbody_tr:last-child]:border-b-0'\nconst SOFT_HEAD = '[&_thead]:border-(--rule-2)'\nconst COLUMNS =\n '[&_th]:border-e [&_td]:border-e [&_th]:border-(--rule) [&_td]:border-(--rule) [&_th:last-child]:border-e-0 [&_td:last-child]:border-e-0'\nconst BOXED = 'rounded-(--radius) border border-(--rule-2) [--table-pad-x:0.75rem]'\n\nconst BORDERS: Record<TableBorders, string> = {\n rows: ROWS,\n grid: cn(ROWS, COLUMNS, '[--table-pad-x:0.75rem]'),\n bordered: cn(ROWS, SOFT_HEAD, BOXED),\n 'bordered-grid': cn(ROWS, SOFT_HEAD, COLUMNS, BOXED),\n none: '',\n}\n\nexport interface TableProps extends TableHTMLAttributes<HTMLTableElement> {\n /**\n * Describes the table for a screen reader, which cannot see the heading above\n * it. Rendered as a visually-hidden `<caption>` — required, because an\n * unnamed table in a page with three tables is unnavigable.\n */\n caption: string\n /** Prints the caption instead of hiding it. */\n showCaption?: boolean\n /**\n * Pins the header row while the body scrolls. Needs a bounded height on the\n * container — otherwise the page scrolls, not the table, and nothing sticks.\n */\n stickyHeader?: boolean\n /** Which rules to draw. See {@link TableBorders}. */\n borders?: TableBorders\n /** Tightens the row padding, for a table that is mostly numbers. */\n density?: 'comfortable' | 'compact'\n}\n\n/**\n * A ruled data table.\n *\n * Wrapped in its own horizontally scrolling container, because a table is the\n * one block that legitimately exceeds the measure — and a page that scrolls\n * sideways as a whole is a layout bug, while a table that does is a table.\n *\n * That container is focusable, and it has to be. A scrollable region whose\n * contents are not themselves focusable is unreachable by keyboard: there is no\n * element to Tab to and therefore no way to press an arrow key at it, so the\n * columns past the fold simply do not exist for anyone not using a mouse.\n *\n * It is also POSITIONED, which is not decoration. `sr-only` is\n * `position: absolute`, so a visually-hidden label in a cell resolves against\n * the nearest positioned ancestor — and with none between it and the document,\n * it escapes the scroll container and every `overflow-hidden` around it, and\n * widens the page by however far the table happens to be scrolled.\n *\n * No zebra striping at any border setting. In a monochrome system a striped row\n * is a second surface competing with the page ground, and the hairline between\n * rows is already enough to track a line across.\n *\n * @example\n * <Table caption=\"Deploy history\">\n * <THead><TR><TH>Commit</TH><TH align=\"end\" sortable sortDirection={dir} onSort={sort}>Duration</TH></TR></THead>\n * <TBody><TR><TD>a1b2c3d</TD><TD align=\"end\">2m 14s</TD></TR></TBody>\n * </Table>\n * @example\n * <Table caption=\"Inventory\" borders=\"bordered-grid\" density=\"compact\">…</Table>\n */\nexport function Table({\n caption,\n showCaption = false,\n stickyHeader = false,\n borders = 'rows',\n density = 'comfortable',\n className,\n children,\n ...rest\n}: TableProps) {\n warnBlankName('Table', 'caption', caption, 'the table is announced with no name and a reader arrives in it with no idea what it lists')\n return (\n <div\n tabIndex={0}\n role=\"region\"\n aria-label={caption}\n data-table-density={density}\n className={cn('relative w-full overflow-x-auto scroll-slim', BORDERS[borders])}\n >\n <table\n className={cn(\n 'w-full border-collapse text-start text-sm',\n stickyHeader &&\n '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-(--z-rule) [&_thead]:bg-(--paper)',\n className,\n )}\n {...rest}\n >\n <caption className={cn(showCaption ? 'pb-3 text-start eyebrow text-(--ink-3-aa)' : 'sr-only')}>\n {caption}\n </caption>\n {children}\n </table>\n </div>\n )\n}\n\n/** Header group. The rule under it is drawn by the table's border setting. */\nexport function THead({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <thead className={className} {...rest} />\n}\n\nexport function TBody({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <tbody className={className} {...rest} />\n}\n\nexport function TR({ className, ...rest }: HTMLAttributes<HTMLTableRowElement>) {\n return <tr className={className} {...rest} />\n}\n\nexport type SortDirection = 'ascending' | 'descending' | 'none'\n\nexport interface THProps extends Omit<ThHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the column's contents sit against. Numbers belong at `end`. */\n align?: TableAlign\n /** Makes the label a button and shows the sort marker. */\n sortable?: boolean\n /**\n * Which way this column is sorted. Also set as `aria-sort`, which is the\n * only way a screen reader learns a table is ordered at all — a caret drawn\n * in the header tells it nothing.\n */\n sortDirection?: SortDirection\n onSort?: () => void\n}\n\n/**\n * Cell padding, in one string both cells share.\n *\n * `--table-pad-x` is 0 by default and set by the ruled variants, so the plain\n * table keeps its trailing gap and no leading one while a boxed table gets\n * symmetric padding inside its border.\n */\nconst CELL =\n 'px-[var(--table-pad-x,0)] [&:not(:last-child)]:pe-[calc(var(--table-pad-x,0px)+1.5rem)]'\n\nconst SORT_ICON = {\n ascending: ArrowUp,\n descending: ArrowDown,\n none: ChevronsUpDown,\n} as const\n\n/**\n * A column label. Mono and uppercase, so it never reads as data.\n *\n * When sortable, the label becomes a `<button>` INSIDE the `<th>` rather than\n * the `<th>` becoming clickable: a cell with a click handler is not focusable\n * and not announced as a control, so the sort exists only for a mouse.\n *\n * Sorting is per column and opt-in. A table where every header is a button\n * invites the reader to try sorting a column the data cannot be ordered by.\n */\nexport function TH({\n align = 'start',\n sortable = false,\n sortDirection = 'none',\n onSort,\n className,\n children,\n ...rest\n}: THProps) {\n const Icon = SORT_ICON[sortDirection]\n\n return (\n <th\n scope=\"col\"\n aria-sort={sortable ? sortDirection : undefined}\n className={cn(CELL, 'py-3 align-bottom eyebrow text-(--ink-3-aa)', ALIGN[align], className)}\n {...rest}\n >\n {sortable ? (\n <button\n type=\"button\"\n onClick={onSort}\n className={cn(\n 'group inline-flex items-center gap-1.5 eyebrow transition-colors duration-(--duration-fast) hover:text-(--ink)',\n sortDirection === 'none' ? 'text-(--ink-3-aa)' : 'text-(--ink)',\n )}\n >\n {children}\n <Icon\n size={12}\n strokeWidth={2}\n aria-hidden\n className={cn(\n 'shrink-0 transition-opacity',\n sortDirection === 'none' && 'opacity-40 group-hover:opacity-100',\n )}\n />\n </button>\n ) : (\n children\n )}\n </th>\n )\n}\n\nexport interface TDProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the cell's contents sit against. Match the column's header. */\n align?: TableAlign\n}\n\n/**\n * A body cell.\n *\n * TOP-aligned, and that is a choice about which kind of table is the common\n * one here: a cell holding a paragraph. Top is what lines the first lines of a\n * row up with each other, and centring a two-word term against a four-line\n * description puts the term in the middle of nothing.\n *\n * It is the wrong default for the other kind — a row of one-line values with a\n * control in it. A 36px button next to 16px of text makes a 52px row, and every\n * other cell then hangs at the top of it with twenty pixels of nothing\n * underneath, which reads as a column that has slipped. Pass\n * `className=\"align-middle\"` on those rows; the data-table template does.\n */\nexport function TD({ align = 'start', className, ...rest }: TDProps) {\n return (\n <td\n className={cn(CELL, 'py-[var(--table-pad-y)] align-top text-(--ink-2)', ALIGN[align], className)}\n {...rest}\n />\n )\n}\n\nexport default Table\n"],"mappings":"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","names":[]}
@@ -8,7 +8,13 @@ function TabsList({ className, ...rest }) {
8
8
  TabsPrimitive.List,
9
9
  {
10
10
  className: cn(
11
- "flex items-center gap-1 overflow-x-auto border-b border-(--rule-2) scroll-slim",
11
+ // `overflow-y-hidden` is not decoration. A strip only ever scrolls
12
+ // sideways, but `overflow-x: auto` on its own computes `overflow-y` to
13
+ // `auto` as well — and the active trigger's `-mb-px` rule leaves the
14
+ // content exactly one pixel taller than the box, which is enough for a
15
+ // vertical scrollbar to appear beside a row of tabs that has nothing
16
+ // to scroll.
17
+ "flex items-center overflow-x-auto overflow-y-hidden gap-1 border-b border-(--rule-2) scroll-slim",
12
18
  className
13
19
  ),
14
20
  ...rest
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/Tabs/Tabs.tsx"],"sourcesContent":["'use client'\n\nimport * as TabsPrimitive from '@radix-ui/react-tabs'\nimport type { ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\n\n/** Radix Tabs root, re-exported as a typed passthrough. */\nexport const Tabs = TabsPrimitive.Root\n\n/**\n * The tab strip.\n *\n * Scrolls horizontally rather than wrapping. A wrapped second row of tabs moves\n * every tab below it when the strip grows, and the reader loses the one they\n * were about to click.\n *\n * @example\n * <Tabs defaultValue=\"preview\">\n * <TabsList>\n * <TabsTrigger value=\"preview\">Preview</TabsTrigger>\n * <TabsTrigger value=\"code\">Code</TabsTrigger>\n * </TabsList>\n * <TabsContent value=\"preview\">…</TabsContent>\n * </Tabs>\n */\nexport function TabsList({ className, ...rest }: ComponentProps<typeof TabsPrimitive.List>) {\n return (\n <TabsPrimitive.List\n className={cn(\n 'flex items-center gap-1 overflow-x-auto border-b border-(--rule-2) scroll-slim',\n className,\n )}\n {...rest}\n />\n )\n}\n\n/**\n * One tab.\n *\n * The active marker is a 2px ink rule pulled onto the strip's own border with\n * `-mb-px`, so the two occupy the same line instead of stacking into a 3px\n * edge. 44px tall, because a tab is a pointer target like any other.\n */\nexport function TabsTrigger({ className, ...rest }: ComponentProps<typeof TabsPrimitive.Trigger>) {\n return (\n <TabsPrimitive.Trigger\n className={cn(\n '-mb-px min-h-(--control-h-md) shrink-0 whitespace-nowrap border-b-2 border-transparent px-3.5 py-2 text-sm text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:text-(--ink) data-[state=active]:border-(--accent) data-[state=active]:text-(--accent-on-muted)',\n className,\n )}\n {...rest}\n />\n )\n}\n\n/** The panel paired to a {@link TabsTrigger} by matching `value`. */\nexport function TabsContent({ className, ...rest }: ComponentProps<typeof TabsPrimitive.Content>) {\n return (\n <TabsPrimitive.Content\n data-m22-animated\n className={cn('pt-5 data-[state=active]:animate-[m22-panel-in_var(--duration-fast)_var(--ease)]', className)}\n {...rest}\n />\n )\n}\n\nexport default Tabs\n"],"mappings":";AA2BI;AAzBJ,YAAY,mBAAmB;AAE/B,SAAS,UAAU;AAGZ,MAAM,OAAO,cAAc;AAkB3B,SAAS,SAAS,EAAE,WAAW,GAAG,KAAK,GAA8C;AAC1F,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AASO,SAAS,YAAY,EAAE,WAAW,GAAG,KAAK,GAAiD;AAChG,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAGO,SAAS,YAAY,EAAE,WAAW,GAAG,KAAK,GAAiD;AAChG,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,qBAAiB;AAAA,MACjB,WAAW,GAAG,oFAAoF,SAAS;AAAA,MAC1G,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,IAAO,eAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/Tabs/Tabs.tsx"],"sourcesContent":["'use client'\n\nimport * as TabsPrimitive from '@radix-ui/react-tabs'\nimport type { ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\n\n/** Radix Tabs root, re-exported as a typed passthrough. */\nexport const Tabs = TabsPrimitive.Root\n\n/**\n * The tab strip.\n *\n * Scrolls horizontally rather than wrapping. A wrapped second row of tabs moves\n * every tab below it when the strip grows, and the reader loses the one they\n * were about to click.\n *\n * @example\n * <Tabs defaultValue=\"preview\">\n * <TabsList>\n * <TabsTrigger value=\"preview\">Preview</TabsTrigger>\n * <TabsTrigger value=\"code\">Code</TabsTrigger>\n * </TabsList>\n * <TabsContent value=\"preview\">…</TabsContent>\n * </Tabs>\n */\nexport function TabsList({ className, ...rest }: ComponentProps<typeof TabsPrimitive.List>) {\n return (\n <TabsPrimitive.List\n className={cn(\n // `overflow-y-hidden` is not decoration. A strip only ever scrolls\n // sideways, but `overflow-x: auto` on its own computes `overflow-y` to\n // `auto` as well — and the active trigger's `-mb-px` rule leaves the\n // content exactly one pixel taller than the box, which is enough for a\n // vertical scrollbar to appear beside a row of tabs that has nothing\n // to scroll.\n 'flex items-center overflow-x-auto overflow-y-hidden gap-1 border-b border-(--rule-2) scroll-slim',\n className,\n )}\n {...rest}\n />\n )\n}\n\n/**\n * One tab.\n *\n * The active marker is a 2px ink rule pulled onto the strip's own border with\n * `-mb-px`, so the two occupy the same line instead of stacking into a 3px\n * edge. 44px tall, because a tab is a pointer target like any other.\n */\nexport function TabsTrigger({ className, ...rest }: ComponentProps<typeof TabsPrimitive.Trigger>) {\n return (\n <TabsPrimitive.Trigger\n className={cn(\n '-mb-px min-h-(--control-h-md) shrink-0 whitespace-nowrap border-b-2 border-transparent px-3.5 py-2 text-sm text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:text-(--ink) data-[state=active]:border-(--accent) data-[state=active]:text-(--accent-on-muted)',\n className,\n )}\n {...rest}\n />\n )\n}\n\n/** The panel paired to a {@link TabsTrigger} by matching `value`. */\nexport function TabsContent({ className, ...rest }: ComponentProps<typeof TabsPrimitive.Content>) {\n return (\n <TabsPrimitive.Content\n data-m22-animated\n className={cn('pt-5 data-[state=active]:animate-[m22-panel-in_var(--duration-fast)_var(--ease)]', className)}\n {...rest}\n />\n )\n}\n\nexport default Tabs\n"],"mappings":";AA2BI;AAzBJ,YAAY,mBAAmB;AAE/B,SAAS,UAAU;AAGZ,MAAM,OAAO,cAAc;AAkB3B,SAAS,SAAS,EAAE,WAAW,GAAG,KAAK,GAA8C;AAC1F,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AASO,SAAS,YAAY,EAAE,WAAW,GAAG,KAAK,GAAiD;AAChG,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAGO,SAAS,YAAY,EAAE,WAAW,GAAG,KAAK,GAAiD;AAChG,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,qBAAiB;AAAA,MACjB,WAAW,GAAG,oFAAoF,SAAS;AAAA,MAC1G,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,IAAO,eAAQ;","names":[]}
@@ -48,6 +48,27 @@ interface RoutedEdge {
48
48
  /** The last segment's direction, for the arrowhead. */
49
49
  angle: number;
50
50
  }
51
+ /**
52
+ * The face a line should use when the author did not say.
53
+ *
54
+ * Whichever axis separates the two boxes by more wins, and the choice is made
55
+ * on the GAP rather than on centre distance. Two boxes side by side with a
56
+ * small vertical offset separate horizontally, which is what a reader expects;
57
+ * comparing centres would flip that pair to top/bottom as soon as the offset
58
+ * exceeded half a box.
59
+ */
60
+ declare function autoSides(from: Box, to: Box): [Side, Side];
61
+ /**
62
+ * The two faces a line will actually use, author's choice or the router's.
63
+ *
64
+ * Exported because the SPREAD has to key on the real face and cannot wait for
65
+ * the router to decide it. Keyed on the word "auto" instead — which is what it
66
+ * used to do — every auto-routed line leaving one node counted as sharing one
67
+ * face, however far apart the faces they resolved to were. A node with one line
68
+ * going right and one going down had both of them nudged off centre to make
69
+ * room for each other on a face neither was using.
70
+ */
71
+ declare function resolveSides(from: Box, to: Box, fromSide?: Side, toSide?: Side): [Side, Side];
51
72
  /**
52
73
  * The polyline as a path, with the corners turned rather than mitred.
53
74
  *
@@ -82,4 +103,4 @@ interface PortUse {
82
103
  */
83
104
  declare function spreadPorts(uses: PortUse[]): Map<string, number>;
84
105
 
85
- export { type PortUse, type RouteRequest, type RoutedEdge, roundedPath, routeEdge, spreadPorts };
106
+ export { type PortUse, type RouteRequest, type RoutedEdge, autoSides, resolveSides, roundedPath, routeEdge, spreadPorts };
@@ -1,6 +1,7 @@
1
1
  import { round } from "./geometry.js";
2
2
  const STUB = 18;
3
3
  const CORNER = 8;
4
+ const STRAIGHT_SNAP = CORNER * 2;
4
5
  function autoSides(from, to) {
5
6
  const gapX = Math.max(to.x - (from.x + from.w), from.x - (to.x + to.w));
6
7
  const gapY = Math.max(to.y - (from.y + from.h), from.y - (to.y + to.h));
@@ -9,6 +10,10 @@ function autoSides(from, to) {
9
10
  }
10
11
  return to.y >= from.y ? ["bottom", "top"] : ["top", "bottom"];
11
12
  }
13
+ function resolveSides(from, to, fromSide, toSide) {
14
+ const [autoFrom, autoTo] = autoSides(from, to);
15
+ return [fromSide ?? autoFrom, toSide ?? autoTo];
16
+ }
12
17
  function port(box, side, offset = 0.5) {
13
18
  const t = Math.min(0.88, Math.max(0.12, offset));
14
19
  switch (side) {
@@ -57,6 +62,20 @@ function connect(a, b, fromSide, toSide, from, to, channelX, channelY) {
57
62
  }
58
63
  return fromH ? [a, [b[0], a[1]], b] : [a, [a[0], b[1]], b];
59
64
  }
65
+ function straighten(start, end, fromSide, toSide, to) {
66
+ const fromH = isHorizontal(fromSide);
67
+ if (fromH !== isHorizontal(toSide)) return end;
68
+ const opposed = fromH ? fromSide === "right" !== (toSide === "right") : fromSide === "bottom" !== (toSide === "bottom");
69
+ if (!opposed) return end;
70
+ if (fromH) {
71
+ if (Math.abs(start[1] - end[1]) > STRAIGHT_SNAP) return end;
72
+ if (start[1] < to.y + to.h * 0.12 || start[1] > to.y + to.h * 0.88) return end;
73
+ return [end[0], start[1]];
74
+ }
75
+ if (Math.abs(start[0] - end[0]) > STRAIGHT_SNAP) return end;
76
+ if (start[0] < to.x + to.w * 0.12 || start[0] > to.x + to.w * 0.88) return end;
77
+ return [start[0], end[1]];
78
+ }
60
79
  function simplify(points) {
61
80
  const out = [];
62
81
  for (const point of points) {
@@ -126,7 +145,9 @@ function routeEdge(request) {
126
145
  const fromSide = request.fromSide ?? autoFrom;
127
146
  const toSide = request.toSide ?? autoTo;
128
147
  const start = port(from, fromSide, request.fromOffset);
129
- const end = port(to, toSide, request.toOffset);
148
+ const rawEnd = port(to, toSide, request.toOffset);
149
+ const spread = request.fromOffset !== void 0 || request.toOffset !== void 0;
150
+ const end = spread ? rawEnd : straighten(start, rawEnd, fromSide, toSide, to);
130
151
  let points;
131
152
  if (mode === "straight") {
132
153
  points = [start, end];
@@ -171,6 +192,8 @@ function spreadPorts(uses) {
171
192
  return offsets;
172
193
  }
173
194
  export {
195
+ autoSides,
196
+ resolveSides,
174
197
  roundedPath,
175
198
  routeEdge,
176
199
  spreadPorts
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/diagrams/lib/route.ts"],"sourcesContent":["/**\n * Getting a line from one box to another.\n *\n * The routing rule this module keeps, and the reason it is worth having at all:\n * A SIDE IS A DIRECTION CONTRACT. A line that says it leaves a box's right face\n * must leave it going right, and a line that says it arrives at a top face must\n * arrive going down. A router that takes the shortest path between two anchor\n * points breaks that contract constantly — it produces a segment that grazes\n * the box it just left, and a reader cannot tell which of two adjacent boxes an\n * arrow belongs to. Every path below therefore begins and ends with a stub\n * perpendicular to its face, and the interesting geometry happens between the\n * two stubs.\n *\n * The second rule is about crowding. When several lines share one face they are\n * spread across it rather than stacked on its midpoint, because two arrowheads\n * landing on the same coordinate is one arrowhead as far as a reader is\n * concerned. Explicit routing controls opt out of the spread: an author who\n * pinned a channel has already decided where the line goes.\n */\n\nimport type { Point, RouteMode, Side } from '../spec'\nimport { round, type Box } from './geometry'\n\n/** How far a line runs straight out of a face before it is allowed to turn. */\nconst STUB = 18\n\n/** The turn radius on an orthogonal corner. */\nconst CORNER = 8\n\nexport interface RouteRequest {\n from: Box\n to: Box\n fromSide?: Side\n toSide?: Side\n mode?: RouteMode | string\n via?: Point[]\n channelX?: number\n channelY?: number\n /** 0–1 along the face, where this line's port sits. 0.5 is the midpoint. */\n fromOffset?: number\n toOffset?: number\n}\n\nexport interface RoutedEdge {\n /** The `d` attribute, corners already rounded. */\n d: string\n /** The polyline before rounding — what the label placer reads. */\n points: [number, number][]\n /** Where a label wants to sit, and how the segment under it runs. */\n label: { x: number; y: number; axis: 'x' | 'y' }\n /** The last segment's direction, for the arrowhead. */\n angle: number\n}\n\n/**\n * The face a line should use when the author did not say.\n *\n * Whichever axis separates the two boxes by more wins, and the choice is made\n * on the GAP rather than on centre distance. Two boxes side by side with a\n * small vertical offset separate horizontally, which is what a reader expects;\n * comparing centres would flip that pair to top/bottom as soon as the offset\n * exceeded half a box.\n */\nfunction autoSides(from: Box, to: Box): [Side, Side] {\n const gapX = Math.max(to.x - (from.x + from.w), from.x - (to.x + to.w))\n const gapY = Math.max(to.y - (from.y + from.h), from.y - (to.y + to.h))\n\n if (gapX >= gapY) {\n return to.x >= from.x ? ['right', 'left'] : ['left', 'right']\n }\n return to.y >= from.y ? ['bottom', 'top'] : ['top', 'bottom']\n}\n\n/** The point on a face where a line attaches, given its share of that face. */\nfunction port(box: Box, side: Side, offset = 0.5): [number, number] {\n const t = Math.min(0.88, Math.max(0.12, offset))\n switch (side) {\n case 'left':\n return [box.x, box.y + box.h * t]\n case 'right':\n return [box.x + box.w, box.y + box.h * t]\n case 'top':\n return [box.x + box.w * t, box.y]\n case 'bottom':\n default:\n return [box.x + box.w * t, box.y + box.h]\n }\n}\n\n/** One step out of a face, perpendicular to it. */\nfunction stubFrom(anchor: [number, number], side: Side, length = STUB): [number, number] {\n switch (side) {\n case 'left':\n return [anchor[0] - length, anchor[1]]\n case 'right':\n return [anchor[0] + length, anchor[1]]\n case 'top':\n return [anchor[0], anchor[1] - length]\n case 'bottom':\n default:\n return [anchor[0], anchor[1] + length]\n }\n}\n\nconst isHorizontal = (side: Side) => side === 'left' || side === 'right'\n\n/**\n * The polyline between two stub ends, kept orthogonal.\n *\n * Three cases, and they are the only three an axis-aligned router has:\n *\n * - **Facing across the same axis** (right→left, bottom→top). One dogleg\n * through a channel halfway between them, which is the shape everybody draws\n * by hand.\n * - **Turning a corner** (right→top). A single elbow, and WHICH elbow is\n * decided by the outgoing axis: leaving a vertical face means running\n * vertically first, or the line re-enters the box it just left.\n * - **Same-side or back-tracking** (right→right, or right→left with the target\n * behind). The channel goes OUTSIDE both boxes rather than between them,\n * because there is no room between them to go through.\n */\nfunction connect(\n a: [number, number],\n b: [number, number],\n fromSide: Side,\n toSide: Side,\n from: Box,\n to: Box,\n channelX?: number,\n channelY?: number,\n): [number, number][] {\n const fromH = isHorizontal(fromSide)\n const toH = isHorizontal(toSide)\n\n if (fromH && toH) {\n const forward =\n (fromSide === 'right' && b[0] >= a[0]) || (fromSide === 'left' && b[0] <= a[0])\n const x =\n channelX ??\n (forward\n ? (a[0] + b[0]) / 2\n : // Behind: run out past the far edge of both boxes and come back.\n fromSide === 'right'\n ? Math.max(a[0], to.x + to.w + STUB, from.x + from.w + STUB)\n : Math.min(a[0], to.x - STUB, from.x - STUB))\n if (Math.abs(a[1] - b[1]) < 0.5) return [a, b]\n return [a, [x, a[1]], [x, b[1]], b]\n }\n\n if (!fromH && !toH) {\n const forward =\n (fromSide === 'bottom' && b[1] >= a[1]) || (fromSide === 'top' && b[1] <= a[1])\n const y =\n channelY ??\n (forward\n ? (a[1] + b[1]) / 2\n : fromSide === 'bottom'\n ? Math.max(a[1], to.y + to.h + STUB, from.y + from.h + STUB)\n : Math.min(a[1], to.y - STUB, from.y - STUB))\n if (Math.abs(a[0] - b[0]) < 0.5) return [a, b]\n return [a, [a[0], y], [b[0], y], b]\n }\n\n // One elbow. The axis of the OUTGOING face decides which corner it is.\n return fromH ? [a, [b[0], a[1]], b] : [a, [a[0], b[1]], b]\n}\n\n/** Drops points that repeat, and collapses three points on one straight run into two. */\nfunction simplify(points: [number, number][]): [number, number][] {\n const out: [number, number][] = []\n for (const point of points) {\n const last = out[out.length - 1]\n if (last && Math.abs(last[0] - point[0]) < 0.5 && Math.abs(last[1] - point[1]) < 0.5) continue\n out.push(point)\n }\n for (let index = 1; index < out.length - 1; ) {\n const [ax, ay] = out[index - 1]!\n const [bx, by] = out[index]!\n const [cx, cy] = out[index + 1]!\n const collinear =\n (Math.abs(ax - bx) < 0.5 && Math.abs(bx - cx) < 0.5) ||\n (Math.abs(ay - by) < 0.5 && Math.abs(by - cy) < 0.5)\n if (collinear) out.splice(index, 1)\n else index += 1\n }\n return out\n}\n\n/**\n * The polyline as a path, with the corners turned rather than mitred.\n *\n * A quadratic through the corner point, pulled back along both legs by the\n * radius. The pull-back is clamped to a third of each leg so a short segment\n * between two turns cannot produce a curve that overshoots its own endpoint —\n * which renders as a hook and is the classic artefact of rounding a polyline\n * without checking the leg lengths.\n */\nexport function roundedPath(points: [number, number][], radius = CORNER): string {\n if (points.length === 0) return ''\n if (points.length === 1) return `M ${round(points[0]![0])} ${round(points[0]![1])}`\n\n let d = `M ${round(points[0]![0])} ${round(points[0]![1])}`\n\n for (let index = 1; index < points.length - 1; index += 1) {\n const previous = points[index - 1]!\n const corner = points[index]!\n const next = points[index + 1]!\n\n const inLength = Math.hypot(corner[0] - previous[0], corner[1] - previous[1])\n const outLength = Math.hypot(next[0] - corner[0], next[1] - corner[1])\n const r = Math.min(radius, inLength / 3, outLength / 3)\n\n if (r < 1) {\n d += ` L ${round(corner[0])} ${round(corner[1])}`\n continue\n }\n\n const enter: [number, number] = [\n corner[0] + ((previous[0] - corner[0]) / inLength) * r,\n corner[1] + ((previous[1] - corner[1]) / inLength) * r,\n ]\n const leave: [number, number] = [\n corner[0] + ((next[0] - corner[0]) / outLength) * r,\n corner[1] + ((next[1] - corner[1]) / outLength) * r,\n ]\n\n d += ` L ${round(enter[0])} ${round(enter[1])}`\n d += ` Q ${round(corner[0])} ${round(corner[1])} ${round(leave[0])} ${round(leave[1])}`\n }\n\n const end = points[points.length - 1]!\n return `${d} L ${round(end[0])} ${round(end[1])}`\n}\n\n/**\n * Where the line's wording goes: the middle of its longest segment.\n *\n * The longest segment is chosen rather than the path's own midpoint because the\n * midpoint of a dogleg often lands on a corner, and a label on a corner sits\n * across both legs and masks the turn — which is the one part of the line that\n * carries information about where it went.\n */\nfunction labelSpot(points: [number, number][]): { x: number; y: number; axis: 'x' | 'y' } {\n let best = { x: points[0]![0], y: points[0]![1], axis: 'x' as 'x' | 'y', length: -1 }\n\n for (let index = 0; index < points.length - 1; index += 1) {\n const a = points[index]!\n const b = points[index + 1]!\n const length = Math.hypot(b[0] - a[0], b[1] - a[1])\n if (length > best.length) {\n best = {\n x: (a[0] + b[0]) / 2,\n y: (a[1] + b[1]) / 2,\n axis: Math.abs(b[0] - a[0]) >= Math.abs(b[1] - a[1]) ? 'x' : 'y',\n length,\n }\n }\n }\n\n return { x: best.x, y: best.y, axis: best.axis }\n}\n\n/** Routes one relationship. */\nexport function routeEdge(request: RouteRequest): RoutedEdge {\n const { from, to, via, channelX, channelY, mode = 'auto' } = request\n\n const [autoFrom, autoTo] = autoSides(from, to)\n const fromSide = request.fromSide ?? autoFrom\n const toSide = request.toSide ?? autoTo\n\n const start = port(from, fromSide, request.fromOffset)\n const end = port(to, toSide, request.toOffset)\n\n let points: [number, number][]\n\n if (mode === 'straight') {\n // Straight means straight: centre to centre, clipped to both faces. No\n // stubs, because a stub would bend the one route whose whole promise is\n // that it does not bend.\n points = [start, end]\n } else {\n const outward = stubFrom(start, fromSide)\n const inward = stubFrom(end, toSide)\n\n if (via && via.length > 0) {\n // Waypoints are honoured literally and joined orthogonally. An author who\n // reached for `via` is overruling the router, and a router that then\n // \"improved\" the result would make the control useless.\n points = [start, outward]\n let cursor: [number, number] = outward\n for (const waypoint of via) {\n const next: [number, number] = [waypoint[0], waypoint[1]]\n points.push([next[0], cursor[1]], next)\n cursor = next\n }\n points.push([inward[0], cursor[1]], inward, end)\n } else {\n const forced =\n mode === 'orthogonal-h' ? ('h' as const) : mode === 'orthogonal-v' ? ('v' as const) : null\n\n const middle =\n forced === 'h'\n ? ([outward, [inward[0], outward[1]], inward] as [number, number][])\n : forced === 'v'\n ? ([outward, [outward[0], inward[1]], inward] as [number, number][])\n : connect(outward, inward, fromSide, toSide, from, to, channelX, channelY)\n\n points = [start, ...middle, end]\n }\n }\n\n points = simplify(points)\n\n const last = points[points.length - 1]!\n const previous = points[points.length - 2] ?? last\n const angle = (Math.atan2(last[1] - previous[1], last[0] - previous[0]) * 180) / Math.PI\n\n return { d: roundedPath(points), points, label: labelSpot(points), angle }\n}\n\n/** One line's use of one face: which face, and which end of which line it is. */\nexport interface PortUse {\n /** The face, as `nodeId:side`. Two lines sharing this string share a face. */\n face: string\n /** This endpoint, as `edgeIndex:from` or `edgeIndex:to`. */\n ref: string\n}\n\n/**\n * Which share of its face each line gets, when several use the same one.\n *\n * A single line keeps the midpoint — spreading one line only moves it off\n * centre for no reason. Lines that carry an explicit routing control are\n * excluded by the caller before they get here, so a pinned channel is never\n * nudged out from under its pin.\n *\n * Endpoints are keyed separately rather than by line, because a line's two ends\n * land on two different faces with two different amounts of company. Keying by\n * line gave both ends the same offset, which on a figure with one crowded face\n * and one empty one visibly skewed every arrow that touched the empty side.\n */\nexport function spreadPorts(uses: PortUse[]): Map<string, number> {\n const byFace = new Map<string, string[]>()\n for (const use of uses) {\n const list = byFace.get(use.face)\n if (list) list.push(use.ref)\n else byFace.set(use.face, [use.ref])\n }\n\n const offsets = new Map<string, number>()\n for (const list of byFace.values()) {\n if (list.length < 2) continue\n list.forEach((ref, position) => {\n offsets.set(ref, (position + 1) / (list.length + 1))\n })\n }\n return offsets\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/diagrams/lib/route.ts"],"sourcesContent":["/**\n * Getting a line from one box to another.\n *\n * The routing rule this module keeps, and the reason it is worth having at all:\n * A SIDE IS A DIRECTION CONTRACT. A line that says it leaves a box's right face\n * must leave it going right, and a line that says it arrives at a top face must\n * arrive going down. A router that takes the shortest path between two anchor\n * points breaks that contract constantly — it produces a segment that grazes\n * the box it just left, and a reader cannot tell which of two adjacent boxes an\n * arrow belongs to. Every path below therefore begins and ends with a stub\n * perpendicular to its face, and the interesting geometry happens between the\n * two stubs.\n *\n * The second rule is about crowding. When several lines share one face they are\n * spread across it rather than stacked on its midpoint, because two arrowheads\n * landing on the same coordinate is one arrowhead as far as a reader is\n * concerned. Explicit routing controls opt out of the spread: an author who\n * pinned a channel has already decided where the line goes.\n */\n\nimport type { Point, RouteMode, Side } from '../spec'\nimport { round, type Box } from './geometry'\n\n/** How far a line runs straight out of a face before it is allowed to turn. */\nconst STUB = 18\n\n/** The turn radius on an orthogonal corner. */\nconst CORNER = 8\n\n/**\n * How far out of true a line may be and still be drawn straight.\n *\n * Two boxes on the same row are rarely on exactly the same centre — a taller\n * label, a different icon, a row that grew by four units — and a router that\n * insists on the arithmetic answers a four-unit difference with a dogleg: two\n * corners, an S, and a line that looks like it is avoiding something. It is\n * not; it is being honest about four units, which is a fact no reader wanted.\n *\n * Two corner radii is the threshold, because that is the shortest dogleg that\n * can draw itself. Below it `roundedPath` clamps both curves to a third of a\n * leg already shorter than the radius, so what renders is not a pair of turns\n * but a kink. Straightening it loses nothing and removes the only thing in the\n * picture that was inviting a reader to wonder why.\n */\nconst STRAIGHT_SNAP = CORNER * 2\n\nexport interface RouteRequest {\n from: Box\n to: Box\n fromSide?: Side\n toSide?: Side\n mode?: RouteMode | string\n via?: Point[]\n channelX?: number\n channelY?: number\n /** 0–1 along the face, where this line's port sits. 0.5 is the midpoint. */\n fromOffset?: number\n toOffset?: number\n}\n\nexport interface RoutedEdge {\n /** The `d` attribute, corners already rounded. */\n d: string\n /** The polyline before rounding — what the label placer reads. */\n points: [number, number][]\n /** Where a label wants to sit, and how the segment under it runs. */\n label: { x: number; y: number; axis: 'x' | 'y' }\n /** The last segment's direction, for the arrowhead. */\n angle: number\n}\n\n/**\n * The face a line should use when the author did not say.\n *\n * Whichever axis separates the two boxes by more wins, and the choice is made\n * on the GAP rather than on centre distance. Two boxes side by side with a\n * small vertical offset separate horizontally, which is what a reader expects;\n * comparing centres would flip that pair to top/bottom as soon as the offset\n * exceeded half a box.\n */\nexport function autoSides(from: Box, to: Box): [Side, Side] {\n const gapX = Math.max(to.x - (from.x + from.w), from.x - (to.x + to.w))\n const gapY = Math.max(to.y - (from.y + from.h), from.y - (to.y + to.h))\n\n if (gapX >= gapY) {\n return to.x >= from.x ? ['right', 'left'] : ['left', 'right']\n }\n return to.y >= from.y ? ['bottom', 'top'] : ['top', 'bottom']\n}\n\n/**\n * The two faces a line will actually use, author's choice or the router's.\n *\n * Exported because the SPREAD has to key on the real face and cannot wait for\n * the router to decide it. Keyed on the word \"auto\" instead — which is what it\n * used to do — every auto-routed line leaving one node counted as sharing one\n * face, however far apart the faces they resolved to were. A node with one line\n * going right and one going down had both of them nudged off centre to make\n * room for each other on a face neither was using.\n */\nexport function resolveSides(\n from: Box,\n to: Box,\n fromSide?: Side,\n toSide?: Side,\n): [Side, Side] {\n const [autoFrom, autoTo] = autoSides(from, to)\n return [fromSide ?? autoFrom, toSide ?? autoTo]\n}\n\n/** The point on a face where a line attaches, given its share of that face. */\nfunction port(box: Box, side: Side, offset = 0.5): [number, number] {\n const t = Math.min(0.88, Math.max(0.12, offset))\n switch (side) {\n case 'left':\n return [box.x, box.y + box.h * t]\n case 'right':\n return [box.x + box.w, box.y + box.h * t]\n case 'top':\n return [box.x + box.w * t, box.y]\n case 'bottom':\n default:\n return [box.x + box.w * t, box.y + box.h]\n }\n}\n\n/** One step out of a face, perpendicular to it. */\nfunction stubFrom(anchor: [number, number], side: Side, length = STUB): [number, number] {\n switch (side) {\n case 'left':\n return [anchor[0] - length, anchor[1]]\n case 'right':\n return [anchor[0] + length, anchor[1]]\n case 'top':\n return [anchor[0], anchor[1] - length]\n case 'bottom':\n default:\n return [anchor[0], anchor[1] + length]\n }\n}\n\nconst isHorizontal = (side: Side) => side === 'left' || side === 'right'\n\n/**\n * The polyline between two stub ends, kept orthogonal.\n *\n * Three cases, and they are the only three an axis-aligned router has:\n *\n * - **Facing across the same axis** (right→left, bottom→top). One dogleg\n * through a channel halfway between them, which is the shape everybody draws\n * by hand.\n * - **Turning a corner** (right→top). A single elbow, and WHICH elbow is\n * decided by the outgoing axis: leaving a vertical face means running\n * vertically first, or the line re-enters the box it just left.\n * - **Same-side or back-tracking** (right→right, or right→left with the target\n * behind). The channel goes OUTSIDE both boxes rather than between them,\n * because there is no room between them to go through.\n */\nfunction connect(\n a: [number, number],\n b: [number, number],\n fromSide: Side,\n toSide: Side,\n from: Box,\n to: Box,\n channelX?: number,\n channelY?: number,\n): [number, number][] {\n const fromH = isHorizontal(fromSide)\n const toH = isHorizontal(toSide)\n\n if (fromH && toH) {\n const forward =\n (fromSide === 'right' && b[0] >= a[0]) || (fromSide === 'left' && b[0] <= a[0])\n const x =\n channelX ??\n (forward\n ? (a[0] + b[0]) / 2\n : // Behind: run out past the far edge of both boxes and come back.\n fromSide === 'right'\n ? Math.max(a[0], to.x + to.w + STUB, from.x + from.w + STUB)\n : Math.min(a[0], to.x - STUB, from.x - STUB))\n if (Math.abs(a[1] - b[1]) < 0.5) return [a, b]\n return [a, [x, a[1]], [x, b[1]], b]\n }\n\n if (!fromH && !toH) {\n const forward =\n (fromSide === 'bottom' && b[1] >= a[1]) || (fromSide === 'top' && b[1] <= a[1])\n const y =\n channelY ??\n (forward\n ? (a[1] + b[1]) / 2\n : fromSide === 'bottom'\n ? Math.max(a[1], to.y + to.h + STUB, from.y + from.h + STUB)\n : Math.min(a[1], to.y - STUB, from.y - STUB))\n if (Math.abs(a[0] - b[0]) < 0.5) return [a, b]\n return [a, [a[0], y], [b[0], y], b]\n }\n\n // One elbow. The axis of the OUTGOING face decides which corner it is.\n return fromH ? [a, [b[0], a[1]], b] : [a, [a[0], b[1]], b]\n}\n\n/**\n * Moves the arriving port onto the leaving one, when they are nearly in line.\n *\n * Only for faces that oppose each other on one axis — a corner turn is a real\n * turn and keeps its elbow — and only within the target face's usable span, so\n * a snap can never slide an arrowhead off the end of the box it points at.\n * Returns the port unchanged when neither holds, and the router then draws the\n * dogleg it would have drawn.\n *\n * The END moves rather than the start: a reader follows a line from its source,\n * and the source is where the emphasis and the label usually are.\n */\nfunction straighten(\n start: [number, number],\n end: [number, number],\n fromSide: Side,\n toSide: Side,\n to: Box,\n): [number, number] {\n const fromH = isHorizontal(fromSide)\n if (fromH !== isHorizontal(toSide)) return end\n // Facing each other, not the same side twice: right→right is a line that has\n // to go around, and there is nothing there to straighten.\n const opposed = fromH\n ? (fromSide === 'right') !== (toSide === 'right')\n : (fromSide === 'bottom') !== (toSide === 'bottom')\n if (!opposed) return end\n\n if (fromH) {\n if (Math.abs(start[1] - end[1]) > STRAIGHT_SNAP) return end\n if (start[1] < to.y + to.h * 0.12 || start[1] > to.y + to.h * 0.88) return end\n return [end[0], start[1]]\n }\n\n if (Math.abs(start[0] - end[0]) > STRAIGHT_SNAP) return end\n if (start[0] < to.x + to.w * 0.12 || start[0] > to.x + to.w * 0.88) return end\n return [start[0], end[1]]\n}\n\n/** Drops points that repeat, and collapses three points on one straight run into two. */\nfunction simplify(points: [number, number][]): [number, number][] {\n const out: [number, number][] = []\n for (const point of points) {\n const last = out[out.length - 1]\n if (last && Math.abs(last[0] - point[0]) < 0.5 && Math.abs(last[1] - point[1]) < 0.5) continue\n out.push(point)\n }\n for (let index = 1; index < out.length - 1; ) {\n const [ax, ay] = out[index - 1]!\n const [bx, by] = out[index]!\n const [cx, cy] = out[index + 1]!\n const collinear =\n (Math.abs(ax - bx) < 0.5 && Math.abs(bx - cx) < 0.5) ||\n (Math.abs(ay - by) < 0.5 && Math.abs(by - cy) < 0.5)\n if (collinear) out.splice(index, 1)\n else index += 1\n }\n return out\n}\n\n/**\n * The polyline as a path, with the corners turned rather than mitred.\n *\n * A quadratic through the corner point, pulled back along both legs by the\n * radius. The pull-back is clamped to a third of each leg so a short segment\n * between two turns cannot produce a curve that overshoots its own endpoint —\n * which renders as a hook and is the classic artefact of rounding a polyline\n * without checking the leg lengths.\n */\nexport function roundedPath(points: [number, number][], radius = CORNER): string {\n if (points.length === 0) return ''\n if (points.length === 1) return `M ${round(points[0]![0])} ${round(points[0]![1])}`\n\n let d = `M ${round(points[0]![0])} ${round(points[0]![1])}`\n\n for (let index = 1; index < points.length - 1; index += 1) {\n const previous = points[index - 1]!\n const corner = points[index]!\n const next = points[index + 1]!\n\n const inLength = Math.hypot(corner[0] - previous[0], corner[1] - previous[1])\n const outLength = Math.hypot(next[0] - corner[0], next[1] - corner[1])\n const r = Math.min(radius, inLength / 3, outLength / 3)\n\n if (r < 1) {\n d += ` L ${round(corner[0])} ${round(corner[1])}`\n continue\n }\n\n const enter: [number, number] = [\n corner[0] + ((previous[0] - corner[0]) / inLength) * r,\n corner[1] + ((previous[1] - corner[1]) / inLength) * r,\n ]\n const leave: [number, number] = [\n corner[0] + ((next[0] - corner[0]) / outLength) * r,\n corner[1] + ((next[1] - corner[1]) / outLength) * r,\n ]\n\n d += ` L ${round(enter[0])} ${round(enter[1])}`\n d += ` Q ${round(corner[0])} ${round(corner[1])} ${round(leave[0])} ${round(leave[1])}`\n }\n\n const end = points[points.length - 1]!\n return `${d} L ${round(end[0])} ${round(end[1])}`\n}\n\n/**\n * Where the line's wording goes: the middle of its longest segment.\n *\n * The longest segment is chosen rather than the path's own midpoint because the\n * midpoint of a dogleg often lands on a corner, and a label on a corner sits\n * across both legs and masks the turn — which is the one part of the line that\n * carries information about where it went.\n */\nfunction labelSpot(points: [number, number][]): { x: number; y: number; axis: 'x' | 'y' } {\n let best = { x: points[0]![0], y: points[0]![1], axis: 'x' as 'x' | 'y', length: -1 }\n\n for (let index = 0; index < points.length - 1; index += 1) {\n const a = points[index]!\n const b = points[index + 1]!\n const length = Math.hypot(b[0] - a[0], b[1] - a[1])\n if (length > best.length) {\n best = {\n x: (a[0] + b[0]) / 2,\n y: (a[1] + b[1]) / 2,\n axis: Math.abs(b[0] - a[0]) >= Math.abs(b[1] - a[1]) ? 'x' : 'y',\n length,\n }\n }\n }\n\n return { x: best.x, y: best.y, axis: best.axis }\n}\n\n/** Routes one relationship. */\nexport function routeEdge(request: RouteRequest): RoutedEdge {\n const { from, to, via, channelX, channelY, mode = 'auto' } = request\n\n const [autoFrom, autoTo] = autoSides(from, to)\n const fromSide = request.fromSide ?? autoFrom\n const toSide = request.toSide ?? autoTo\n\n const start = port(from, fromSide, request.fromOffset)\n const rawEnd = port(to, toSide, request.toOffset)\n // Not when either end was spread. Spreading exists to keep two arrowheads off\n // one coordinate, and a snap is exactly the move that would put them back —\n // so a face with company keeps its arithmetic.\n const spread = request.fromOffset !== undefined || request.toOffset !== undefined\n const end = spread ? rawEnd : straighten(start, rawEnd, fromSide, toSide, to)\n\n let points: [number, number][]\n\n if (mode === 'straight') {\n // Straight means straight: centre to centre, clipped to both faces. No\n // stubs, because a stub would bend the one route whose whole promise is\n // that it does not bend.\n points = [start, end]\n } else {\n const outward = stubFrom(start, fromSide)\n const inward = stubFrom(end, toSide)\n\n if (via && via.length > 0) {\n // Waypoints are honoured literally and joined orthogonally. An author who\n // reached for `via` is overruling the router, and a router that then\n // \"improved\" the result would make the control useless.\n points = [start, outward]\n let cursor: [number, number] = outward\n for (const waypoint of via) {\n const next: [number, number] = [waypoint[0], waypoint[1]]\n points.push([next[0], cursor[1]], next)\n cursor = next\n }\n points.push([inward[0], cursor[1]], inward, end)\n } else {\n const forced =\n mode === 'orthogonal-h' ? ('h' as const) : mode === 'orthogonal-v' ? ('v' as const) : null\n\n const middle =\n forced === 'h'\n ? ([outward, [inward[0], outward[1]], inward] as [number, number][])\n : forced === 'v'\n ? ([outward, [outward[0], inward[1]], inward] as [number, number][])\n : connect(outward, inward, fromSide, toSide, from, to, channelX, channelY)\n\n points = [start, ...middle, end]\n }\n }\n\n points = simplify(points)\n\n const last = points[points.length - 1]!\n const previous = points[points.length - 2] ?? last\n const angle = (Math.atan2(last[1] - previous[1], last[0] - previous[0]) * 180) / Math.PI\n\n return { d: roundedPath(points), points, label: labelSpot(points), angle }\n}\n\n/** One line's use of one face: which face, and which end of which line it is. */\nexport interface PortUse {\n /** The face, as `nodeId:side`. Two lines sharing this string share a face. */\n face: string\n /** This endpoint, as `edgeIndex:from` or `edgeIndex:to`. */\n ref: string\n}\n\n/**\n * Which share of its face each line gets, when several use the same one.\n *\n * A single line keeps the midpoint — spreading one line only moves it off\n * centre for no reason. Lines that carry an explicit routing control are\n * excluded by the caller before they get here, so a pinned channel is never\n * nudged out from under its pin.\n *\n * Endpoints are keyed separately rather than by line, because a line's two ends\n * land on two different faces with two different amounts of company. Keying by\n * line gave both ends the same offset, which on a figure with one crowded face\n * and one empty one visibly skewed every arrow that touched the empty side.\n */\nexport function spreadPorts(uses: PortUse[]): Map<string, number> {\n const byFace = new Map<string, string[]>()\n for (const use of uses) {\n const list = byFace.get(use.face)\n if (list) list.push(use.ref)\n else byFace.set(use.face, [use.ref])\n }\n\n const offsets = new Map<string, number>()\n for (const list of byFace.values()) {\n if (list.length < 2) continue\n list.forEach((ref, position) => {\n offsets.set(ref, (position + 1) / (list.length + 1))\n })\n }\n return offsets\n}\n"],"mappings":"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","names":[]}
@@ -1,16 +1,20 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { edgeClasses, EdgeLabel, edgeMarker, edgeStroke } from "./marks.js";
3
- import { routeEdge, spreadPorts } from "./route.js";
3
+ import { resolveSides, routeEdge, spreadPorts } from "./route.js";
4
4
  function renderWires(wires, uid) {
5
5
  const uses = [];
6
6
  wires.forEach((wire, index) => {
7
7
  const { edge } = wire;
8
8
  const pinned = wire.keepWaypoints === true && Boolean(edge.via?.length || edge.channelX !== void 0 || edge.channelY !== void 0);
9
9
  if (pinned) return;
10
- if (edge.fromSide) uses.push({ face: `${edge.from}:${edge.fromSide}`, ref: `${index}:from` });
11
- if (edge.toSide) uses.push({ face: `${edge.to}:${edge.toSide}`, ref: `${index}:to` });
12
- if (!edge.fromSide) uses.push({ face: `${edge.from}:auto`, ref: `${index}:from` });
13
- if (!edge.toSide) uses.push({ face: `${edge.to}:auto`, ref: `${index}:to` });
10
+ const [fromSide, toSide] = resolveSides(
11
+ wire.from,
12
+ wire.to,
13
+ edge.fromSide,
14
+ edge.toSide
15
+ );
16
+ uses.push({ face: `${edge.from}:${fromSide}`, ref: `${index}:from` });
17
+ uses.push({ face: `${edge.to}:${toSide}`, ref: `${index}:to` });
14
18
  });
15
19
  const offsets = spreadPorts(uses);
16
20
  const routed = wires.map((wire, index) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/diagrams/lib/wires.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport type { EdgeBase, Side, Variant } from '../spec'\nimport type { Box } from './geometry'\nimport { edgeClasses, EdgeLabel, edgeMarker, edgeStroke } from './marks'\nimport { routeEdge, spreadPorts, type PortUse } from './route'\n\n/** One line to draw, once its two boxes are known. */\nexport interface Wire {\n edge: EdgeBase & { variant?: Variant | 'return'; classification?: string; note?: string }\n from: Box\n to: Box\n /** Overrides the variant's own stroke weight — a main path drawn heavier. */\n weight?: number\n /** An open arrowhead rather than a filled one. */\n open?: boolean\n /**\n * Whether this line's `via` / `channelX` / `channelY` mean anything here.\n *\n * A waypoint is an ABSOLUTE coordinate in whatever space the author's nodes\n * were placed in. When a specification pins its nodes with `pos`, that space\n * is this renderer's space too and the waypoints land exactly where they were\n * meant to. When the nodes are placed from a lane and a column, this renderer\n * chose the coordinates — so the author's waypoints refer to a grid that does\n * not exist here, and following them drags lines across unrelated nodes.\n *\n * So the rule is per line and not per figure: honour the waypoints when both\n * of a line's endpoints were placed absolutely, and route automatically\n * otherwise. Defaults to false, which is the safe half.\n */\n keepWaypoints?: boolean\n}\n\n/**\n * Every line in a figure, routed together.\n *\n * Together rather than one at a time, because port spreading is a property of a\n * FACE and not of a line: how far off centre a given arrow sits depends on how\n * many others land on the same face, which no single line can know. Routing\n * them independently is what produces three arrowheads stacked on one point.\n *\n * Lines that pin their own geometry — `via`, a channel, an explicit side — opt\n * out of the spread. An author who placed a line has already answered the\n * question the spread exists to answer, and moving it afterwards makes the\n * control useless.\n *\n * Returns the paths and the labels separately because ORDER IS THE MASK: a\n * label's plate has to be painted over every line in the figure, not merely\n * over its own. Interleaving them puts the third line on top of the first\n * line's label.\n */\nexport function renderWires(\n wires: Wire[],\n uid: string,\n): { paths: ReactNode; labels: ReactNode } {\n const uses: PortUse[] = []\n\n wires.forEach((wire, index) => {\n const { edge } = wire\n const pinned =\n wire.keepWaypoints === true &&\n Boolean(edge.via?.length || edge.channelX !== undefined || edge.channelY !== undefined)\n if (pinned) return\n if (edge.fromSide) uses.push({ face: `${edge.from}:${edge.fromSide}`, ref: `${index}:from` })\n if (edge.toSide) uses.push({ face: `${edge.to}:${edge.toSide}`, ref: `${index}:to` })\n if (!edge.fromSide) uses.push({ face: `${edge.from}:auto`, ref: `${index}:from` })\n if (!edge.toSide) uses.push({ face: `${edge.to}:auto`, ref: `${index}:to` })\n })\n\n const offsets = spreadPorts(uses)\n\n const routed = wires.map((wire, index) => {\n const { edge } = wire\n const keep = wire.keepWaypoints === true\n return {\n wire,\n index,\n route: routeEdge({\n from: wire.from,\n to: wire.to,\n fromSide: edge.fromSide as Side | undefined,\n toSide: edge.toSide as Side | undefined,\n mode: edge.route,\n via: keep ? edge.via : undefined,\n channelX: keep ? edge.channelX : undefined,\n channelY: keep ? edge.channelY : undefined,\n fromOffset: offsets.get(`${index}:from`),\n toOffset: offsets.get(`${index}:to`),\n }),\n }\n })\n\n return {\n paths: (\n <g data-diagram-wires=\"\">\n {routed.map(({ wire, index, route }) => (\n <path\n key={wire.edge.id ?? index}\n d={route.d}\n fill=\"none\"\n data-edge={wire.edge.id}\n markerEnd={`url(#${uid}-${wire.open ? 'arrow-open' : edgeMarker(wire.edge.variant)})`}\n className={edgeClasses(wire.edge.variant)}\n {...edgeStroke(wire.edge.variant, wire.weight)}\n />\n ))}\n </g>\n ),\n labels: (\n <g data-diagram-wire-labels=\"\">\n {routed.map(({ wire, index, route }) =>\n wire.edge.label ? (\n /* `labelDx` / `labelDy` / `labelAt` are deliberately NOT applied.\n They are collision repairs authored against archify's router,\n measured in the positions THAT router produced — and this one\n does not produce the same collisions, so honouring them moves\n labels off clear ground and onto the nodes they were nudged away\n from. Dropping the nudge and keeping the wording is the reading\n that preserves the meaning; the field stays in the type so a\n specification carrying it still typechecks. */\n <EdgeLabel\n key={wire.edge.id ?? index}\n x={route.label.x}\n y={route.label.y}\n axis={route.label.axis}\n text={wire.edge.label}\n chip={wire.edge.classification}\n note={wire.edge.note}\n />\n ) : null,\n )}\n </g>\n ),\n }\n}\n\n/** Resolves an edge's endpoints against a box index, dropping any that dangle. */\nexport function wiresFor<E extends EdgeBase>(\n edges: E[] | undefined,\n boxes: Map<string, Box>,\n decorate?: (edge: E) => Partial<Wire>,\n): Wire[] {\n if (!edges) return []\n const out: Wire[] = []\n for (const edge of edges) {\n const from = boxes.get(edge.from)\n const to = boxes.get(edge.to)\n // A relationship naming a node that is not in the figure is a specification\n // error, and drawing it from nowhere to nowhere would hide that. The\n // figures filter the same edges through `liveEdges` before they get here,\n // so the drop is announced once in development and the summary list agrees\n // with the picture; this guard is what makes that agreement structural\n // rather than a convention every renderer has to remember.\n if (!from || !to) continue\n out.push({ edge, from, to, ...decorate?.(edge) })\n }\n return out\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/diagrams/lib/wires.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport type { EdgeBase, Side, Variant } from '../spec'\nimport type { Box } from './geometry'\nimport { edgeClasses, EdgeLabel, edgeMarker, edgeStroke } from './marks'\nimport { resolveSides, routeEdge, spreadPorts, type PortUse } from './route'\n\n/** One line to draw, once its two boxes are known. */\nexport interface Wire {\n edge: EdgeBase & { variant?: Variant | 'return'; classification?: string; note?: string }\n from: Box\n to: Box\n /** Overrides the variant's own stroke weight — a main path drawn heavier. */\n weight?: number\n /** An open arrowhead rather than a filled one. */\n open?: boolean\n /**\n * Whether this line's `via` / `channelX` / `channelY` mean anything here.\n *\n * A waypoint is an ABSOLUTE coordinate in whatever space the author's nodes\n * were placed in. When a specification pins its nodes with `pos`, that space\n * is this renderer's space too and the waypoints land exactly where they were\n * meant to. When the nodes are placed from a lane and a column, this renderer\n * chose the coordinates — so the author's waypoints refer to a grid that does\n * not exist here, and following them drags lines across unrelated nodes.\n *\n * So the rule is per line and not per figure: honour the waypoints when both\n * of a line's endpoints were placed absolutely, and route automatically\n * otherwise. Defaults to false, which is the safe half.\n */\n keepWaypoints?: boolean\n}\n\n/**\n * Every line in a figure, routed together.\n *\n * Together rather than one at a time, because port spreading is a property of a\n * FACE and not of a line: how far off centre a given arrow sits depends on how\n * many others land on the same face, which no single line can know. Routing\n * them independently is what produces three arrowheads stacked on one point.\n *\n * Lines that pin their own geometry — `via`, a channel, an explicit side — opt\n * out of the spread. An author who placed a line has already answered the\n * question the spread exists to answer, and moving it afterwards makes the\n * control useless.\n *\n * Returns the paths and the labels separately because ORDER IS THE MASK: a\n * label's plate has to be painted over every line in the figure, not merely\n * over its own. Interleaving them puts the third line on top of the first\n * line's label.\n */\nexport function renderWires(\n wires: Wire[],\n uid: string,\n): { paths: ReactNode; labels: ReactNode } {\n const uses: PortUse[] = []\n\n wires.forEach((wire, index) => {\n const { edge } = wire\n const pinned =\n wire.keepWaypoints === true &&\n Boolean(edge.via?.length || edge.channelX !== undefined || edge.channelY !== undefined)\n if (pinned) return\n // The face this line will REALLY use, not the word \"auto\". Keyed on the\n // word, every auto-routed line leaving one node counted as sharing a face\n // with every other — so a node with one line going right and one going down\n // had both nudged off centre to make room for each other on a face neither\n // of them was on, and every one of those lines then arrived at a few units\n // out of true and drew a dogleg to cover the difference.\n const [fromSide, toSide] = resolveSides(\n wire.from,\n wire.to,\n edge.fromSide as Side | undefined,\n edge.toSide as Side | undefined,\n )\n uses.push({ face: `${edge.from}:${fromSide}`, ref: `${index}:from` })\n uses.push({ face: `${edge.to}:${toSide}`, ref: `${index}:to` })\n })\n\n const offsets = spreadPorts(uses)\n\n const routed = wires.map((wire, index) => {\n const { edge } = wire\n const keep = wire.keepWaypoints === true\n return {\n wire,\n index,\n route: routeEdge({\n from: wire.from,\n to: wire.to,\n fromSide: edge.fromSide as Side | undefined,\n toSide: edge.toSide as Side | undefined,\n mode: edge.route,\n via: keep ? edge.via : undefined,\n channelX: keep ? edge.channelX : undefined,\n channelY: keep ? edge.channelY : undefined,\n fromOffset: offsets.get(`${index}:from`),\n toOffset: offsets.get(`${index}:to`),\n }),\n }\n })\n\n return {\n paths: (\n <g data-diagram-wires=\"\">\n {routed.map(({ wire, index, route }) => (\n <path\n key={wire.edge.id ?? index}\n d={route.d}\n fill=\"none\"\n data-edge={wire.edge.id}\n markerEnd={`url(#${uid}-${wire.open ? 'arrow-open' : edgeMarker(wire.edge.variant)})`}\n className={edgeClasses(wire.edge.variant)}\n {...edgeStroke(wire.edge.variant, wire.weight)}\n />\n ))}\n </g>\n ),\n labels: (\n <g data-diagram-wire-labels=\"\">\n {routed.map(({ wire, index, route }) =>\n wire.edge.label ? (\n /* `labelDx` / `labelDy` / `labelAt` are deliberately NOT applied.\n They are collision repairs authored against archify's router,\n measured in the positions THAT router produced — and this one\n does not produce the same collisions, so honouring them moves\n labels off clear ground and onto the nodes they were nudged away\n from. Dropping the nudge and keeping the wording is the reading\n that preserves the meaning; the field stays in the type so a\n specification carrying it still typechecks. */\n <EdgeLabel\n key={wire.edge.id ?? index}\n x={route.label.x}\n y={route.label.y}\n axis={route.label.axis}\n text={wire.edge.label}\n chip={wire.edge.classification}\n note={wire.edge.note}\n />\n ) : null,\n )}\n </g>\n ),\n }\n}\n\n/** Resolves an edge's endpoints against a box index, dropping any that dangle. */\nexport function wiresFor<E extends EdgeBase>(\n edges: E[] | undefined,\n boxes: Map<string, Box>,\n decorate?: (edge: E) => Partial<Wire>,\n): Wire[] {\n if (!edges) return []\n const out: Wire[] = []\n for (const edge of edges) {\n const from = boxes.get(edge.from)\n const to = boxes.get(edge.to)\n // A relationship naming a node that is not in the figure is a specification\n // error, and drawing it from nowhere to nowhere would hide that. The\n // figures filter the same edges through `liveEdges` before they get here,\n // so the drop is announced once in development and the summary list agrees\n // with the picture; this guard is what makes that agreement structural\n // rather than a convention every renderer has to remember.\n if (!from || !to) continue\n out.push({ edge, from, to, ...decorate?.(edge) })\n }\n return out\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -28,8 +28,10 @@ export { EmptyState, EmptyStateProps } from './components/EmptyState/EmptyState.
28
28
  export { ERROR_ACTION_CLASS, ErrorState, ErrorStateProps } from './components/ErrorState/ErrorState.js';
29
29
  export { Toaster } from './components/Toast/Toast.js';
30
30
  export { Field, FieldLayout, FieldProps } from './components/Field/Field.js';
31
+ export { ColorPicker, ColorPickerProps } from './components/ColorPicker/ColorPicker.js';
31
32
  export { Combobox, ComboboxOption, ComboboxProps } from './components/Combobox/Combobox.js';
32
33
  export { DATE_PRESETS, DatePicker, DatePickerProps, DatePreset, DateRangePicker, DateRangePickerProps, RANGE_PRESETS } from './components/DatePicker/DatePicker.js';
34
+ export { NumberField, NumberFieldProps } from './components/NumberField/NumberField.js';
33
35
  export { Slider, SliderProps } from './components/Slider/Slider.js';
34
36
  export { ToggleGroup, ToggleGroupItem, ToggleGroupItemProps, ToggleGroupProps } from './components/ToggleGroup/ToggleGroup.js';
35
37
  export { Input, InputProps } from './components/Input/Input.js';
@@ -53,6 +55,7 @@ export { Collapsible, CollapsibleContent, CollapsibleSection, CollapsibleSection
53
55
  export { Breadcrumb, BreadcrumbProps, Crumb } from './components/Breadcrumb/Breadcrumb.js';
54
56
  export { Pagination, PaginationProps, paginationRange } from './components/Pagination/Pagination.js';
55
57
  export { NavItem, NavItemProps } from './components/NavItem/NavItem.js';
58
+ export { Sidebar, SidebarBranch, SidebarBranchProps, SidebarBreakpoint, SidebarCollapsible, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupProps, SidebarHeader, SidebarInset, SidebarItem, SidebarItemProps, SidebarProps, SidebarProvider, SidebarProviderProps, SidebarSeparator, SidebarSide, SidebarTrigger, SidebarTriggerProps, SidebarVariant, useSidebar } from './components/Sidebar/Sidebar.js';
56
59
  export { Card, CardBody, CardFooter, CardHeader, CardProps, CardTitle } from './components/Card/Card.js';
57
60
  export { SortDirection, TBody, TD, TDProps, TH, THProps, THead, TR, Table, TableAlign, TableBorders, TableProps } from './components/Table/Table.js';
58
61
  export { DescriptionList, DescriptionListItem, DescriptionListLayout, DescriptionListProps } from './components/DescriptionList/DescriptionList.js';