@toolpath/dfm 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  parseRuleSetFile,
24
24
  peakHeight,
25
25
  quantityUnit,
26
+ readReachCurve,
26
27
  runStart,
27
28
  serializeRuleSet,
28
29
  subjectLabel,
@@ -36,7 +37,7 @@ import {
36
37
  } from "./chunk-5MPHT7BC.js";
37
38
 
38
39
  // src/rule-list/rule-list.tsx
39
- import { useMemo as useMemo2, useState as useState7 } from "react";
40
+ import { useMemo as useMemo2, useState as useState8 } from "react";
40
41
  import { ScrollArea } from "@toolpath/ui";
41
42
 
42
43
  // src/rule-list/list-keys.ts
@@ -69,48 +70,31 @@ var currentIndex = (items, active) => {
69
70
  var isField = (target) => target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement && target.type !== "color" && target.type !== "button" || target instanceof HTMLElement && target.isContentEditable;
70
71
 
71
72
  // src/rule-list/new-rule.tsx
72
- import { useState } from "react";
73
- import { cn as cn2 } from "@toolpath/ui";
73
+ import { useState as useState2 } from "react";
74
74
 
75
- // src/rule-list/options.tsx
76
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
77
- var MORE = "__more";
78
- var showMore = (select, setAll) => {
79
- setAll(true);
80
- requestAnimationFrame(() => {
81
- try {
82
- select.showPicker();
83
- } catch {
84
- select.focus();
85
- }
86
- });
87
- };
88
- var option = (value, label) => /* @__PURE__ */ jsx("option", { value, children: label }, value);
89
- var SubjectOptions = ({
90
- current,
91
- all
92
- }) => {
75
+ // src/rule-list/options.ts
76
+ var subjectChoices = (current, all) => {
93
77
  const quick = [...QUICK_SUBJECTS, ...QUICK_SUBJECTS.includes(current) ? [] : [current]];
78
+ const first = { choices: quick.map((value) => ({ value, label: subjectLabel(value) })) };
79
+ if (!all) return { groups: [first], more: "More feature types\u2026" };
94
80
  const rest = FEATURE_SUBJECTS.filter((each) => !quick.includes(each.value));
95
- return /* @__PURE__ */ jsxs(Fragment, { children: [
96
- quick.map((value) => option(value, subjectLabel(value))),
97
- all ? /* @__PURE__ */ jsx("optgroup", { label: "More feature types", children: rest.map((each) => option(each.value, each.label)) }) : option(MORE, "More feature types\u2026")
98
- ] });
81
+ return { groups: [first, { heading: "More feature types", choices: rest }] };
99
82
  };
100
- var MetricOptions = ({
101
- subject,
102
- current,
103
- all
104
- }) => {
83
+ var metricChoices = (subject, current, all) => {
105
84
  const offered = metricsForSubject(subject);
106
85
  const quick = offered.filter((each) => QUICK_METRICS.includes(each.key) || each.key === current);
107
86
  quick.sort((a, b) => rank(a.key) - rank(b.key));
108
87
  const rest = offered.filter((each) => !quick.includes(each));
109
- return /* @__PURE__ */ jsxs(Fragment, { children: [
110
- quick.map((each) => option(each.key, each.label)),
111
- all ? grouped(rest).map(([group, metrics]) => /* @__PURE__ */ jsx("optgroup", { label: group, children: metrics.map((each) => option(each.key, each.label)) }, group)) : rest.length > 0 ? option(MORE, "More measures\u2026") : null
112
- ] });
88
+ const first = { choices: quick.map(choice) };
89
+ if (!all) return { groups: [first], more: rest.length > 0 ? "More measures\u2026" : void 0 };
90
+ return {
91
+ groups: [
92
+ first,
93
+ ...grouped(rest).map(([heading, metrics]) => ({ heading, choices: metrics.map(choice) }))
94
+ ]
95
+ };
113
96
  };
97
+ var choice = ({ key, label }) => ({ value: key, label });
114
98
  var rank = (key) => {
115
99
  const at = QUICK_METRICS.indexOf(key);
116
100
  return at === -1 ? QUICK_METRICS.length : at;
@@ -121,6 +105,17 @@ var grouped = (items) => {
121
105
  return [...groups];
122
106
  };
123
107
 
108
+ // src/rule-list/picker.tsx
109
+ import {
110
+ Fragment,
111
+ useEffect,
112
+ useId,
113
+ useRef,
114
+ useState
115
+ } from "react";
116
+ import { CheckIcon } from "@phosphor-icons/react";
117
+ import { Menu, cn as cn2 } from "@toolpath/ui";
118
+
124
119
  // src/rule-list/styles.ts
125
120
  import { cn } from "@toolpath/ui";
126
121
  var FOCUS_RING = "outline-none focus-visible:ring-2 focus-visible:ring-info/75";
@@ -131,10 +126,157 @@ var WORD = cn(
131
126
  "outline-none focus-visible:bg-gray-100 focus-visible:ring-2 focus-visible:ring-info/75 dark:focus-visible:bg-zinc-700"
132
127
  );
133
128
 
129
+ // src/rule-list/picker.tsx
130
+ import { jsx, jsxs } from "react/jsx-runtime";
131
+ var HEADING = "px-2 pt-1.5 pb-1 text-[10px] font-semibold tracking-wider text-gray-300 uppercase font-body dark:text-zinc-500";
132
+ var Picker = ({
133
+ label,
134
+ value,
135
+ choices,
136
+ onChange,
137
+ onMore,
138
+ placeholder,
139
+ title: title2,
140
+ className,
141
+ autoFocus
142
+ }) => {
143
+ const id = useId();
144
+ const held = useRef(null);
145
+ const [holding, setHolding] = useState(0);
146
+ const hold = (next) => {
147
+ held.current = next;
148
+ if (next) setHolding((count) => count + 1);
149
+ };
150
+ const flat = choices.groups.flatMap((group) => group.choices);
151
+ const at = flat.findIndex((choice2) => choice2.value === value);
152
+ const shown = flat[at]?.label ?? placeholder ?? value;
153
+ const starts = choices.groups.map(
154
+ (_, g) => choices.groups.slice(0, g).reduce((count, group) => count + group.choices.length, 0)
155
+ );
156
+ useEffect(() => {
157
+ if (holding === 0) return;
158
+ const frame = requestAnimationFrame(() => focusHeld(id, held.current));
159
+ return () => cancelAnimationFrame(frame);
160
+ }, [holding, id]);
161
+ return (
162
+ // A click on the word, in its menu or on the menu's backdrop is the picker's: React would carry
163
+ // it up through the portal to the row, which opens on a click.
164
+ /* @__PURE__ */ jsx("span", { onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsxs(Menu, { onOpenChange: (open) => hold(open && at !== -1 ? { index: at, reveal: false } : null), children: [
165
+ /* @__PURE__ */ jsx(
166
+ Menu.Trigger,
167
+ {
168
+ nativeButton: true,
169
+ render: /* @__PURE__ */ jsx(
170
+ "button",
171
+ {
172
+ type: "button",
173
+ "aria-label": at === -1 ? label : `${label}: ${shown}`,
174
+ title: title2,
175
+ className: cn2(
176
+ WORD,
177
+ "data-[popup-open]:bg-gray-100 dark:data-[popup-open]:bg-zinc-700",
178
+ className
179
+ ),
180
+ autoFocus
181
+ }
182
+ ),
183
+ children: shown
184
+ }
185
+ ),
186
+ /* @__PURE__ */ jsxs(
187
+ Menu.Popover,
188
+ {
189
+ align: "start",
190
+ sideOffset: 4,
191
+ collisionPadding: 8,
192
+ className: "max-h-[min(24rem,var(--available-height))] min-w-[var(--anchor-width)] overflow-y-auto py-1 shadow-lg",
193
+ onFocus: (event) => {
194
+ const item = held.current && document.getElementById(itemId(id, held.current.index));
195
+ if (item && event.target !== item) focusHeld(id, held.current);
196
+ },
197
+ onPointerMove: () => hold(null),
198
+ onKeyDownCapture: () => hold(null),
199
+ onKeyDown: keepKeys,
200
+ children: [
201
+ choices.groups.map((group, g) => /* @__PURE__ */ jsxs(Fragment, { children: [
202
+ g > 0 ? /* @__PURE__ */ jsx(Menu.Divider, { className: "my-1" }) : null,
203
+ /* @__PURE__ */ jsxs(
204
+ "div",
205
+ {
206
+ role: "group",
207
+ "aria-labelledby": group.heading ? `${id}-group-${g}` : void 0,
208
+ className: "flex flex-col",
209
+ children: [
210
+ group.heading ? /* @__PURE__ */ jsx("div", { id: `${id}-group-${g}`, className: HEADING, children: group.heading }) : null,
211
+ group.choices.map((choice2, c) => {
212
+ const chosen = choice2.value === value;
213
+ return /* @__PURE__ */ jsxs(
214
+ Menu.Item,
215
+ {
216
+ id: itemId(id, (starts[g] ?? 0) + c),
217
+ label: choice2.label,
218
+ onClick: () => {
219
+ if (!chosen) onChange(choice2.value);
220
+ },
221
+ className: "gap-3",
222
+ children: [
223
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: choice2.label }),
224
+ /* @__PURE__ */ jsx(
225
+ CheckIcon,
226
+ {
227
+ "aria-hidden": true,
228
+ weight: "bold",
229
+ className: cn2(
230
+ "size-3.5 text-gray-400 dark:text-zinc-300",
231
+ !chosen && "invisible"
232
+ )
233
+ }
234
+ )
235
+ ]
236
+ },
237
+ choice2.value
238
+ );
239
+ })
240
+ ]
241
+ }
242
+ )
243
+ ] }, group.heading ?? g)),
244
+ choices.more && onMore ? /* @__PURE__ */ jsx(
245
+ Menu.Item,
246
+ {
247
+ closeOnClick: false,
248
+ onClick: () => {
249
+ hold({ index: flat.length, reveal: true });
250
+ onMore();
251
+ },
252
+ className: "text-gray-400 dark:text-zinc-400",
253
+ children: choices.more
254
+ }
255
+ ) : null
256
+ ]
257
+ }
258
+ )
259
+ ] }) })
260
+ );
261
+ };
262
+ var focusHeld = (picker, held) => {
263
+ const item = held && document.getElementById(itemId(picker, held.index));
264
+ if (!item) return;
265
+ item.focus({ preventScroll: held.reveal });
266
+ const menu = item.closest('[role="menu"]');
267
+ const group = item.closest('[role="group"]');
268
+ if (held.reveal && menu && group)
269
+ menu.scrollTop += group.getBoundingClientRect().top - menu.getBoundingClientRect().top - 8;
270
+ };
271
+ var itemId = (picker, index) => `${picker}-${index}`;
272
+ var keepKeys = (event) => {
273
+ if (event.key !== "Tab") event.stopPropagation();
274
+ };
275
+
134
276
  // src/rule-list/new-rule.tsx
135
277
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
136
278
  var NewRule = ({ onAdd }) => {
137
- const [all, setAll] = useState(false);
279
+ const [all, setAll] = useState2(false);
138
280
  return /* @__PURE__ */ jsxs2("li", { className: "flex items-start gap-1.5 py-1 pr-1.5", children: [
139
281
  /* @__PURE__ */ jsx2("span", { "aria-hidden": true, className: "size-7 shrink-0" }),
140
282
  /* @__PURE__ */ jsx2(
@@ -144,27 +286,23 @@ var NewRule = ({ onAdd }) => {
144
286
  className: "mt-1.5 size-4 shrink-0 rounded-full border border-dashed border-gray-300 dark:border-zinc-600"
145
287
  }
146
288
  ),
147
- /* @__PURE__ */ jsx2("p", { className: "min-w-0 flex-1 text-sm leading-7", children: /* @__PURE__ */ jsxs2(
148
- "select",
289
+ /* @__PURE__ */ jsx2("p", { className: "min-w-0 flex-1 text-sm leading-7", children: /* @__PURE__ */ jsx2(
290
+ Picker,
149
291
  {
150
- "aria-label": "New rule",
151
- className: cn2(WORD, "font-normal text-gray-400 dark:text-zinc-500"),
292
+ label: "New rule",
293
+ placeholder: "+ New Rule",
152
294
  value: "",
153
- onChange: (event) => {
154
- if (event.target.value === MORE) showMore(event.currentTarget, setAll);
155
- else if (event.target.value) onAdd(event.target.value);
156
- },
157
- children: [
158
- /* @__PURE__ */ jsx2("option", { value: "", disabled: true, children: "+ New Rule" }),
159
- /* @__PURE__ */ jsx2(SubjectOptions, { current: "any", all })
160
- ]
295
+ choices: subjectChoices("any", all),
296
+ onChange: onAdd,
297
+ onMore: () => setAll(true),
298
+ className: "font-normal text-gray-400 dark:text-zinc-500"
161
299
  }
162
300
  ) })
163
301
  ] });
164
302
  };
165
303
 
166
304
  // src/rule-list/rule-row.tsx
167
- import { useRef as useRef2, useState as useState5 } from "react";
305
+ import { useRef as useRef3, useState as useState6 } from "react";
168
306
  import { CaretRightIcon as CaretRightIcon2, TrashIcon } from "@phosphor-icons/react";
169
307
  import { IconButton, cn as cn7 } from "@toolpath/ui";
170
308
 
@@ -246,7 +384,7 @@ var FeatureButton = ({
246
384
  };
247
385
 
248
386
  // src/rule-list/hole-group-row.tsx
249
- import { useRef, useState as useState2 } from "react";
387
+ import { useRef as useRef2, useState as useState3 } from "react";
250
388
  import { CaretRightIcon } from "@phosphor-icons/react";
251
389
  import { cn as cn4 } from "@toolpath/ui";
252
390
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -255,8 +393,8 @@ var HoleGroupRow = ({
255
393
  direction,
256
394
  ...context
257
395
  }) => {
258
- const [open, setOpen] = useState2(false);
259
- const groupRef = useRef(null);
396
+ const [open, setOpen] = useState3(false);
397
+ const groupRef = useRef2(null);
260
398
  const { rule, check, units, selectedGroup, featureOf, onInspect, onHover, onBack } = context;
261
399
  const [first] = row.tags;
262
400
  const chosen = selectedGroup?.[0] === first;
@@ -373,10 +511,9 @@ var FeatureRows = ({
373
511
  var directionOf = (feature) => feature ? directionLabel(feature.machiningDirection) : "";
374
512
 
375
513
  // src/rule-list/rule-sentence.tsx
376
- import { useState as useState4 } from "react";
514
+ import { useState as useState5 } from "react";
377
515
 
378
- // src/rule-list/ops.tsx
379
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
516
+ // src/rule-list/ops.ts
380
517
  var OP_TITLES = {
381
518
  gte: "at least",
382
519
  gt: "more than",
@@ -388,44 +525,50 @@ var OP_TITLES = {
388
525
  };
389
526
  var LOW_SYMBOLS = { gte: "\u2264", gt: "<" };
390
527
  var ONE_SIDED = "one:";
391
- var OneSidedOptions = () => /* @__PURE__ */ jsx6("optgroup", { label: "One-sided", children: OPS.filter((op) => op !== "between").map((op) => /* @__PURE__ */ jsxs6("option", { value: `${ONE_SIDED}${op}`, children: [
392
- opSymbol(op),
393
- " ",
394
- OP_TITLES[op]
395
- ] }, op)) });
528
+ var ONE_SIDED_CHOICES = {
529
+ heading: "One-sided",
530
+ choices: OPS.filter((op) => op !== "between").map((op) => ({
531
+ value: `${ONE_SIDED}${op}`,
532
+ label: `${opSymbol(op)} ${OP_TITLES[op]}`
533
+ }))
534
+ };
396
535
  var rangeEnd = (rule, chosen, hold) => chosen.startsWith(ONE_SIDED) ? withOp(rule, chosen.slice(ONE_SIDED.length)) : { ...rule, ...hold(chosen) };
397
536
 
398
537
  // src/rule-list/comparison.tsx
399
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
400
- var Comparison = ({ rule, ranged, onChange }) => ranged ? /* @__PURE__ */ jsxs7(
401
- "select",
538
+ import { jsx as jsx6 } from "react/jsx-runtime";
539
+ var COMPARISONS = {
540
+ groups: [{ choices: OPS.map((op) => ({ value: op, label: opSymbol(op) })) }]
541
+ };
542
+ var RANGE_HIGH = {
543
+ groups: [
544
+ { heading: "Range", choices: RANGE_HIGHS.map((op) => ({ value: op, label: opSymbol(op) })) },
545
+ ONE_SIDED_CHOICES
546
+ ]
547
+ };
548
+ var Comparison = ({ rule, ranged, onChange }) => ranged ? /* @__PURE__ */ jsx6(
549
+ Picker,
402
550
  {
403
- "aria-label": "High end",
551
+ label: "High end",
404
552
  title: OP_TITLES[highOf(rule)],
405
- className: WORD,
406
553
  value: highOf(rule),
407
- onChange: (event) => onChange(rangeEnd(rule, event.target.value, (op) => ({ highOp: op }))),
408
- children: [
409
- /* @__PURE__ */ jsx7("optgroup", { label: "Range", children: RANGE_HIGHS.map((op) => /* @__PURE__ */ jsx7("option", { value: op, children: opSymbol(op) }, op)) }),
410
- /* @__PURE__ */ jsx7(OneSidedOptions, {})
411
- ]
554
+ choices: RANGE_HIGH,
555
+ onChange: (value) => onChange(rangeEnd(rule, value, (op) => ({ highOp: op })))
412
556
  }
413
- ) : /* @__PURE__ */ jsx7(
414
- "select",
557
+ ) : /* @__PURE__ */ jsx6(
558
+ Picker,
415
559
  {
416
- "aria-label": "Comparison",
560
+ label: "Comparison",
417
561
  title: OP_TITLES[rule.op],
418
- className: WORD,
419
562
  value: rule.op,
420
- onChange: (event) => onChange(withOp(rule, event.target.value)),
421
- children: OPS.map((op) => /* @__PURE__ */ jsx7("option", { value: op, children: opSymbol(op) }, op))
563
+ choices: COMPARISONS,
564
+ onChange: (value) => onChange(withOp(rule, value))
422
565
  }
423
566
  );
424
567
 
425
568
  // src/rule-list/value-field.tsx
426
- import { useEffect, useState as useState3 } from "react";
569
+ import { useEffect as useEffect2, useState as useState4 } from "react";
427
570
  import { cn as cn5 } from "@toolpath/ui";
428
- import { jsx as jsx8 } from "react/jsx-runtime";
571
+ import { jsx as jsx7 } from "react/jsx-runtime";
429
572
  var ValueField = ({
430
573
  label,
431
574
  info,
@@ -434,13 +577,13 @@ var ValueField = ({
434
577
  onChange
435
578
  }) => {
436
579
  const shown = toField(info.quantity, value, units);
437
- const [text, setText] = useState3(shown);
438
- const [typing, setTyping] = useState3(false);
439
- useEffect(() => {
580
+ const [text, setText] = useState4(shown);
581
+ const [typing, setTyping] = useState4(false);
582
+ useEffect2(() => {
440
583
  if (!typing) setText(shown);
441
584
  }, [shown, typing]);
442
585
  const invalid = fromField(info, text, units) === null;
443
- return /* @__PURE__ */ jsx8(
586
+ return /* @__PURE__ */ jsx7(
444
587
  "input",
445
588
  {
446
589
  "aria-label": label,
@@ -467,7 +610,13 @@ var ValueField = ({
467
610
  };
468
611
 
469
612
  // src/rule-list/rule-sentence.tsx
470
- import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
613
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
614
+ var RANGE_LOW = {
615
+ groups: [
616
+ { heading: "Range", choices: RANGE_LOWS.map((op) => ({ value: op, label: LOW_SYMBOLS[op] })) },
617
+ ONE_SIDED_CHOICES
618
+ ]
619
+ };
471
620
  var RuleSentence = ({
472
621
  rule,
473
622
  units,
@@ -479,28 +628,25 @@ var RuleSentence = ({
479
628
  const presence = info?.quantity === "presence";
480
629
  const ranged = info !== void 0 && !flag && rule.op === "between";
481
630
  const unit = info ? quantityUnit(info.quantity, units) : "";
482
- const [allSubjects, setAllSubjects] = useState4(false);
483
- const [allMetrics, setAllMetrics] = useState4(false);
484
- const unitSpan = unit ? /* @__PURE__ */ jsx9("span", { className: unit === "\xB0" || unit === "%" ? "" : "pl-1", children: unit }) : null;
485
- return /* @__PURE__ */ jsxs8("p", { className: "min-w-0 flex-1 text-sm leading-7 text-gray-500 dark:text-zinc-400", children: [
486
- /* @__PURE__ */ jsx9(
487
- "select",
631
+ const [allSubjects, setAllSubjects] = useState5(false);
632
+ const [allMetrics, setAllMetrics] = useState5(false);
633
+ const unitSpan = unit ? /* @__PURE__ */ jsx8("span", { className: unit === "\xB0" || unit === "%" ? "" : "pl-1", children: unit }) : null;
634
+ return /* @__PURE__ */ jsxs6("p", { className: "min-w-0 flex-1 text-sm leading-7 text-gray-500 dark:text-zinc-400", children: [
635
+ /* @__PURE__ */ jsx8(
636
+ Picker,
488
637
  {
489
- "aria-label": "Features",
490
- className: WORD,
638
+ label: "Features",
491
639
  value: rule.subject,
492
- onChange: (event) => {
493
- if (event.target.value === MORE) showMore(event.currentTarget, setAllSubjects);
494
- else onChange(withSubject(rule, event.target.value, units));
495
- },
496
- children: /* @__PURE__ */ jsx9(SubjectOptions, { current: rule.subject, all: allSubjects })
640
+ choices: subjectChoices(rule.subject, allSubjects),
641
+ onChange: (value) => onChange(withSubject(rule, value, units)),
642
+ onMore: () => setAllSubjects(true)
497
643
  }
498
644
  ),
499
645
  presence ? ", " : " with ",
500
- /* @__PURE__ */ jsxs8("span", { className: ranged ? "whitespace-nowrap" : void 0, children: [
501
- ranged ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
502
- /* @__PURE__ */ jsxs8("span", { className: "whitespace-nowrap", children: [
503
- /* @__PURE__ */ jsx9(
646
+ /* @__PURE__ */ jsxs6("span", { className: ranged ? "whitespace-nowrap" : void 0, children: [
647
+ ranged ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
648
+ /* @__PURE__ */ jsxs6("span", { className: "whitespace-nowrap", children: [
649
+ /* @__PURE__ */ jsx8(
504
650
  ValueField,
505
651
  {
506
652
  label: "From",
@@ -513,43 +659,36 @@ var RuleSentence = ({
513
659
  unitSpan
514
660
  ] }),
515
661
  " ",
516
- /* @__PURE__ */ jsxs8(
517
- "select",
662
+ /* @__PURE__ */ jsx8(
663
+ Picker,
518
664
  {
519
- "aria-label": "Low end",
665
+ label: "Low end",
520
666
  title: OP_TITLES[lowOf(rule)],
521
- className: WORD,
522
667
  value: lowOf(rule),
523
- onChange: (event) => onChange(rangeEnd(rule, event.target.value, (op) => ({ lowOp: op }))),
524
- children: [
525
- /* @__PURE__ */ jsx9("optgroup", { label: "Range", children: RANGE_LOWS.map((op) => /* @__PURE__ */ jsx9("option", { value: op, children: LOW_SYMBOLS[op] }, op)) }),
526
- /* @__PURE__ */ jsx9(OneSidedOptions, {})
527
- ]
668
+ choices: RANGE_LOW,
669
+ onChange: (value) => onChange(rangeEnd(rule, value, (op) => ({ lowOp: op })))
528
670
  }
529
671
  ),
530
672
  " "
531
673
  ] }) : null,
532
- /* @__PURE__ */ jsx9(
533
- "select",
674
+ /* @__PURE__ */ jsx8(
675
+ Picker,
534
676
  {
535
- "aria-label": "Measure",
677
+ label: "Measure",
536
678
  title: info?.hint,
537
- className: WORD,
538
679
  value: rule.metric,
539
- onChange: (event) => {
540
- if (event.target.value === MORE) showMore(event.currentTarget, setAllMetrics);
541
- else onChange(withMetric(rule, event.target.value, units));
542
- },
543
- autoFocus,
544
- children: /* @__PURE__ */ jsx9(MetricOptions, { subject: rule.subject, current: rule.metric, all: allMetrics })
680
+ choices: metricChoices(rule.subject, rule.metric, allMetrics),
681
+ onChange: (value) => onChange(withMetric(rule, value, units)),
682
+ onMore: () => setAllMetrics(true),
683
+ autoFocus
545
684
  }
546
685
  ),
547
- info && !flag ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
686
+ info && !flag ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
548
687
  " ",
549
- /* @__PURE__ */ jsx9(Comparison, { rule, ranged, onChange }),
688
+ /* @__PURE__ */ jsx8(Comparison, { rule, ranged, onChange }),
550
689
  " ",
551
- /* @__PURE__ */ jsxs8("span", { className: "whitespace-nowrap", children: [
552
- /* @__PURE__ */ jsx9(
690
+ /* @__PURE__ */ jsxs6("span", { className: "whitespace-nowrap", children: [
691
+ /* @__PURE__ */ jsx8(
553
692
  ValueField,
554
693
  {
555
694
  label: ranged ? "Up to" : "Value",
@@ -568,7 +707,7 @@ var RuleSentence = ({
568
707
 
569
708
  // src/rule-list/swatch.tsx
570
709
  import { cn as cn6 } from "@toolpath/ui";
571
- import { jsx as jsx10 } from "react/jsx-runtime";
710
+ import { jsx as jsx9 } from "react/jsx-runtime";
572
711
  var swatchKeys = listKeys("swatches", "horizontal");
573
712
  var Swatch = ({
574
713
  color,
@@ -580,7 +719,7 @@ var Swatch = ({
580
719
  ref
581
720
  }) => {
582
721
  const dot = cn6("size-4 shrink-0 rounded-full ring-1 ring-black/10 dark:ring-white/15", className);
583
- return onClick ? /* @__PURE__ */ jsx10(
722
+ return onClick ? /* @__PURE__ */ jsx9(
584
723
  "button",
585
724
  {
586
725
  ref,
@@ -596,12 +735,12 @@ var Swatch = ({
596
735
  ),
597
736
  style: { backgroundColor: color }
598
737
  }
599
- ) : /* @__PURE__ */ jsx10("span", { "aria-hidden": true, className: dot, style: { backgroundColor: color } });
738
+ ) : /* @__PURE__ */ jsx9("span", { "aria-hidden": true, className: dot, style: { backgroundColor: color } });
600
739
  };
601
740
  var ColorPicker = ({
602
741
  value,
603
742
  onChange
604
- }) => /* @__PURE__ */ jsx10("div", { className: "flex flex-wrap items-center gap-1.5 py-1", onKeyDown: swatchKeys, children: RULE_COLORS.map((each) => /* @__PURE__ */ jsx10(
743
+ }) => /* @__PURE__ */ jsx9("div", { className: "flex flex-wrap items-center gap-1.5 py-1", onKeyDown: swatchKeys, children: RULE_COLORS.map((each) => /* @__PURE__ */ jsx9(
605
744
  "button",
606
745
  {
607
746
  type: "button",
@@ -621,7 +760,7 @@ var ColorPicker = ({
621
760
  )) });
622
761
 
623
762
  // src/rule-list/rule-row.tsx
624
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
763
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
625
764
  var isControl = (target) => target instanceof Element && target.closest("button, select, input, a") !== null;
626
765
  var RuleRow = ({
627
766
  rule,
@@ -638,8 +777,8 @@ var RuleRow = ({
638
777
  units,
639
778
  ...list
640
779
  }) => {
641
- const [picking, setPicking] = useState5(false);
642
- const swatchRef = useRef2(null);
780
+ const [picking, setPicking] = useState6(false);
781
+ const swatchRef = useRef3(null);
643
782
  const count = result?.tags.length ?? 0;
644
783
  const others = result?.otherTags.length ?? 0;
645
784
  const canOpen = count + others > 0;
@@ -654,7 +793,7 @@ var RuleRow = ({
654
793
  onToggle();
655
794
  }
656
795
  };
657
- return /* @__PURE__ */ jsxs9(
796
+ return /* @__PURE__ */ jsxs7(
658
797
  "li",
659
798
  {
660
799
  className: cn7(
@@ -663,7 +802,7 @@ var RuleRow = ({
663
802
  !checking && count === 0 && "opacity-55 transition-opacity focus-within:opacity-100 hover:opacity-100"
664
803
  ),
665
804
  children: [
666
- /* @__PURE__ */ jsxs9(
805
+ /* @__PURE__ */ jsxs7(
667
806
  "div",
668
807
  {
669
808
  onClick: (event) => {
@@ -674,7 +813,7 @@ var RuleRow = ({
674
813
  canOpen && "cursor-pointer"
675
814
  ),
676
815
  children: [
677
- /* @__PURE__ */ jsx11(
816
+ /* @__PURE__ */ jsx10(
678
817
  "button",
679
818
  {
680
819
  type: "button",
@@ -686,7 +825,7 @@ var RuleRow = ({
686
825
  "flex size-7 shrink-0 items-center justify-center rounded text-gray-400 dark:text-zinc-400",
687
826
  canOpen ? "cursor-pointer hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-zinc-700 dark:hover:text-zinc-100" : "invisible"
688
827
  ),
689
- children: /* @__PURE__ */ jsx11(
828
+ children: /* @__PURE__ */ jsx10(
690
829
  CaretRightIcon2,
691
830
  {
692
831
  weight: "bold",
@@ -695,7 +834,7 @@ var RuleRow = ({
695
834
  )
696
835
  }
697
836
  ),
698
- /* @__PURE__ */ jsx11(
837
+ /* @__PURE__ */ jsx10(
699
838
  Swatch,
700
839
  {
701
840
  ref: swatchRef,
@@ -707,9 +846,9 @@ var RuleRow = ({
707
846
  className: "mt-1.5"
708
847
  }
709
848
  ),
710
- /* @__PURE__ */ jsx11(RuleSentence, { rule, units, onChange, autoFocus: focusOnMount }),
711
- /* @__PURE__ */ jsxs9("div", { className: "relative mt-1 shrink-0", children: [
712
- /* @__PURE__ */ jsx11(
849
+ /* @__PURE__ */ jsx10(RuleSentence, { rule, units, onChange, autoFocus: focusOnMount }),
850
+ /* @__PURE__ */ jsxs7("div", { className: "relative mt-1 shrink-0", children: [
851
+ /* @__PURE__ */ jsx10(
713
852
  IconButton,
714
853
  {
715
854
  "aria-label": "Remove rule",
@@ -722,10 +861,10 @@ var RuleRow = ({
722
861
  "group-focus-within/rule:pointer-events-auto group-focus-within/rule:opacity-100",
723
862
  "group-hover/rule:pointer-events-auto group-hover/rule:opacity-100"
724
863
  ),
725
- children: /* @__PURE__ */ jsx11(TrashIcon, { weight: "bold" })
864
+ children: /* @__PURE__ */ jsx10(TrashIcon, { weight: "bold" })
726
865
  }
727
866
  ),
728
- /* @__PURE__ */ jsx11(
867
+ /* @__PURE__ */ jsx10(
729
868
  CountPill,
730
869
  {
731
870
  count,
@@ -740,7 +879,7 @@ var RuleRow = ({
740
879
  ]
741
880
  }
742
881
  ),
743
- picking ? /* @__PURE__ */ jsx11("div", { className: "pb-1 pl-[34px]", children: /* @__PURE__ */ jsx11(
882
+ picking ? /* @__PURE__ */ jsx10("div", { className: "pb-1 pl-[34px]", children: /* @__PURE__ */ jsx10(
744
883
  ColorPicker,
745
884
  {
746
885
  value: rule.color,
@@ -751,7 +890,7 @@ var RuleRow = ({
751
890
  }
752
891
  }
753
892
  ) }) : null,
754
- open && result && check && features && sheets ? /* @__PURE__ */ jsx11(
893
+ open && result && check && features && sheets ? /* @__PURE__ */ jsx10(
755
894
  FeatureRows,
756
895
  {
757
896
  rule,
@@ -776,7 +915,7 @@ var CountPill = ({
776
915
  open,
777
916
  canOpen,
778
917
  onToggle
779
- }) => /* @__PURE__ */ jsx11(
918
+ }) => /* @__PURE__ */ jsx10(
780
919
  "button",
781
920
  {
782
921
  type: "button",
@@ -800,22 +939,32 @@ var countLabel = (count, others) => {
800
939
  };
801
940
 
802
941
  // src/rule-list/rule-set-actions.tsx
803
- import {
804
- useCallback,
805
- useEffect as useEffect2,
806
- useRef as useRef3,
807
- useState as useState6
808
- } from "react";
809
- import { CheckIcon, CopyIcon, DownloadSimpleIcon, UploadSimpleIcon } from "@phosphor-icons/react";
942
+ import { useRef as useRef4 } from "react";
943
+ import { CheckIcon as CheckIcon2, CopyIcon, DownloadSimpleIcon, UploadSimpleIcon } from "@phosphor-icons/react";
810
944
  import { Button, cn as cn8 } from "@toolpath/ui";
811
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
945
+
946
+ // src/rule-list/use-timed-value.ts
947
+ import { useCallback, useEffect as useEffect3, useState as useState7 } from "react";
948
+ var useTimedValue = (ms) => {
949
+ const [box, setBox] = useState7(null);
950
+ useEffect3(() => {
951
+ if (box === null) return;
952
+ const timer = setTimeout(() => setBox(null), ms);
953
+ return () => clearTimeout(timer);
954
+ }, [box, ms]);
955
+ const set = useCallback((value) => setBox({ value }), []);
956
+ return [box?.value ?? null, set];
957
+ };
958
+
959
+ // src/rule-list/rule-set-actions.tsx
960
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
812
961
  var NOTE_MS = 4e3;
813
962
  var COPIED_MS = 2e3;
814
963
  var EXPORT_FILE_NAME = "dfm-rules.json";
815
964
  var RuleSetActions = ({ rules }) => {
816
965
  const [note, setNote] = useTimedValue(NOTE_MS);
817
966
  const [copied, setCopied] = useTimedValue(COPIED_MS);
818
- const fileInput = useRef3(null);
967
+ const fileInput = useRef4(null);
819
968
  const importRules = async (file) => {
820
969
  if (!file) return;
821
970
  let text;
@@ -843,23 +992,23 @@ var RuleSetActions = ({ rules }) => {
843
992
  setCopied(true);
844
993
  setNote("Paste the prompt into an LLM, then import the JSON it writes.");
845
994
  };
846
- return /* @__PURE__ */ jsxs10("div", { className: "flex flex-col gap-2 px-4 pt-2 pb-3", children: [
847
- /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap items-center gap-2", children: [
848
- /* @__PURE__ */ jsx12(Button, { variant: "secondary", onClick: () => fileInput.current?.click(), children: /* @__PURE__ */ jsx12(ActionLabel, { icon: /* @__PURE__ */ jsx12(UploadSimpleIcon, { weight: "bold" }), children: "Import\u2026" }) }),
849
- /* @__PURE__ */ jsx12(Button, { variant: "secondary", onClick: () => downloadRuleSet(rules), children: /* @__PURE__ */ jsx12(ActionLabel, { icon: /* @__PURE__ */ jsx12(DownloadSimpleIcon, { weight: "bold" }), children: "Export" }) }),
850
- /* @__PURE__ */ jsx12(
995
+ return /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-2 px-4 pt-2 pb-3", children: [
996
+ /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-center gap-2", children: [
997
+ /* @__PURE__ */ jsx11(Button, { variant: "secondary", onClick: () => fileInput.current?.click(), children: /* @__PURE__ */ jsx11(ActionLabel, { icon: /* @__PURE__ */ jsx11(UploadSimpleIcon, { weight: "bold" }), children: "Import\u2026" }) }),
998
+ /* @__PURE__ */ jsx11(Button, { variant: "secondary", onClick: () => downloadRuleSet(rules), children: /* @__PURE__ */ jsx11(ActionLabel, { icon: /* @__PURE__ */ jsx11(DownloadSimpleIcon, { weight: "bold" }), children: "Export" }) }),
999
+ /* @__PURE__ */ jsx11(
851
1000
  Button,
852
1001
  {
853
1002
  variant: "secondary",
854
1003
  title: "Copies a prompt that walks an LLM through writing a rule set for you",
855
1004
  onClick: () => void copyPrompt(),
856
- children: /* @__PURE__ */ jsxs10("span", { className: "grid justify-items-center *:col-start-1 *:row-start-1", children: [
857
- /* @__PURE__ */ jsx12(ActionLabel, { hidden: copied !== null, icon: /* @__PURE__ */ jsx12(CopyIcon, { weight: "bold" }), children: "Copy LLM prompt" }),
858
- /* @__PURE__ */ jsx12(ActionLabel, { hidden: copied === null, icon: /* @__PURE__ */ jsx12(CheckIcon, { weight: "bold" }), children: "Copied" })
1005
+ children: /* @__PURE__ */ jsxs8("span", { className: "grid justify-items-center *:col-start-1 *:row-start-1", children: [
1006
+ /* @__PURE__ */ jsx11(ActionLabel, { hidden: copied !== null, icon: /* @__PURE__ */ jsx11(CopyIcon, { weight: "bold" }), children: "Copy LLM prompt" }),
1007
+ /* @__PURE__ */ jsx11(ActionLabel, { hidden: copied === null, icon: /* @__PURE__ */ jsx11(CheckIcon2, { weight: "bold" }), children: "Copied" })
859
1008
  ] })
860
1009
  }
861
1010
  ),
862
- /* @__PURE__ */ jsx12(
1011
+ /* @__PURE__ */ jsx11(
863
1012
  "input",
864
1013
  {
865
1014
  ref: fileInput,
@@ -874,10 +1023,10 @@ var RuleSetActions = ({ rules }) => {
874
1023
  }
875
1024
  )
876
1025
  ] }),
877
- note ? /* @__PURE__ */ jsx12("p", { className: "text-xs text-gray-500 dark:text-zinc-400", role: "status", children: note }) : null
1026
+ note ? /* @__PURE__ */ jsx11("p", { className: "text-xs text-gray-500 dark:text-zinc-400", role: "status", children: note }) : null
878
1027
  ] });
879
1028
  };
880
- var ActionLabel = ({ icon, children, hidden = false }) => /* @__PURE__ */ jsxs10(
1029
+ var ActionLabel = ({ icon, children, hidden = false }) => /* @__PURE__ */ jsxs8(
881
1030
  "span",
882
1031
  {
883
1032
  "aria-hidden": hidden || void 0,
@@ -888,16 +1037,6 @@ var ActionLabel = ({ icon, children, hidden = false }) => /* @__PURE__ */ jsxs10
888
1037
  ]
889
1038
  }
890
1039
  );
891
- var useTimedValue = (ms) => {
892
- const [box, setBox] = useState6(null);
893
- useEffect2(() => {
894
- if (box === null) return;
895
- const timer = setTimeout(() => setBox(null), ms);
896
- return () => clearTimeout(timer);
897
- }, [box, ms]);
898
- const set = useCallback((value) => setBox({ value }), []);
899
- return [box?.value ?? null, set];
900
- };
901
1040
  var downloadRuleSet = (rules) => {
902
1041
  const url = URL.createObjectURL(
903
1042
  new Blob([serializeRuleSet(rules.rules)], { type: "application/json" })
@@ -916,7 +1055,7 @@ var importedNote = (count, skipped, name) => {
916
1055
  };
917
1056
 
918
1057
  // src/rule-list/rule-list.tsx
919
- import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1058
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
920
1059
  var ruleListKeys = listKeys("rules");
921
1060
  var RuleList = ({
922
1061
  rules,
@@ -926,8 +1065,8 @@ var RuleList = ({
926
1065
  units,
927
1066
  ...list
928
1067
  }) => {
929
- const [openIds, setOpenIds] = useState7(/* @__PURE__ */ new Set());
930
- const [added, setAdded] = useState7(null);
1068
+ const [openIds, setOpenIds] = useState8(/* @__PURE__ */ new Set());
1069
+ const [added, setAdded] = useState8(null);
931
1070
  const toggleOpen = (id) => setOpenIds((was) => {
932
1071
  const next = new Set(was);
933
1072
  if (!next.delete(id)) next.add(id);
@@ -943,7 +1082,7 @@ var RuleList = ({
943
1082
  };
944
1083
  const active = rules.rules.filter(found);
945
1084
  const none = rules.rules.filter((rule) => !found(rule));
946
- const row = (rule) => /* @__PURE__ */ jsx13(
1085
+ const row = (rule) => /* @__PURE__ */ jsx12(
947
1086
  RuleRow,
948
1087
  {
949
1088
  rule,
@@ -964,11 +1103,11 @@ var RuleList = ({
964
1103
  );
965
1104
  return (
966
1105
  // Grows with its rules, and scrolls once the panel can grow no more.
967
- /* @__PURE__ */ jsxs11(ScrollArea, { className: "min-h-0 flex-auto", children: [
968
- /* @__PURE__ */ jsxs11("ul", { className: "flex flex-col gap-0.5 pt-2 pr-4 pl-2", children: [
1106
+ /* @__PURE__ */ jsxs9(ScrollArea, { className: "min-h-0 flex-auto", children: [
1107
+ /* @__PURE__ */ jsxs9("ul", { className: "flex flex-col gap-0.5 pt-2 pr-4 pl-2", children: [
969
1108
  active.map(row),
970
1109
  none.map(row),
971
- /* @__PURE__ */ jsx13(
1110
+ /* @__PURE__ */ jsx12(
972
1111
  NewRule,
973
1112
  {
974
1113
  onAdd: (subject) => {
@@ -979,7 +1118,7 @@ var RuleList = ({
979
1118
  }
980
1119
  )
981
1120
  ] }),
982
- /* @__PURE__ */ jsx13(RuleSetActions, { rules })
1121
+ /* @__PURE__ */ jsx12(RuleSetActions, { rules })
983
1122
  ] })
984
1123
  );
985
1124
  };
@@ -988,7 +1127,7 @@ var RuleList = ({
988
1127
  import { useMemo as useMemo4 } from "react";
989
1128
 
990
1129
  // src/reach/reach-section.tsx
991
- import { useId, useMemo as useMemo3, useState as useState8 } from "react";
1130
+ import { useId as useId2, useMemo as useMemo3, useState as useState9 } from "react";
992
1131
 
993
1132
  // src/reach/reading.tsx
994
1133
  import { heightAt } from "@toolpath/tool-support";
@@ -1063,7 +1202,7 @@ var READ = "stroke-sky-600 dark:stroke-sky-400";
1063
1202
  var READ_DOT = "fill-sky-600 dark:fill-sky-400";
1064
1203
 
1065
1204
  // src/reach/reading.tsx
1066
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1205
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1067
1206
  var readingAt = ({ offsets, heights }, offset) => ({
1068
1207
  offset,
1069
1208
  height: heightAt({ horizontalOffset: offsets, verticalOffset: heights }, offset),
@@ -1086,8 +1225,8 @@ var ReadingMark = ({
1086
1225
  const pointerX = x(reading.offset);
1087
1226
  const onRight = pointerX + READ_INSET + textWidth <= width - READ_INSET;
1088
1227
  const labelX = onRight ? pointerX + READ_INSET : Math.min(Math.max(pointerX - READ_INSET, READ_INSET + textWidth), width - READ_INSET);
1089
- return /* @__PURE__ */ jsxs12("g", { pointerEvents: "none", children: [
1090
- /* @__PURE__ */ jsx14(
1228
+ return /* @__PURE__ */ jsxs10("g", { pointerEvents: "none", children: [
1229
+ /* @__PURE__ */ jsx13(
1091
1230
  "line",
1092
1231
  {
1093
1232
  x1: pointerX,
@@ -1099,8 +1238,8 @@ var ReadingMark = ({
1099
1238
  className: READ
1100
1239
  }
1101
1240
  ),
1102
- /* @__PURE__ */ jsx14("circle", { cx: pointerX, cy: y(reading.height), r: 4, className: READ_DOT }),
1103
- /* @__PURE__ */ jsx14(
1241
+ /* @__PURE__ */ jsx13("circle", { cx: pointerX, cy: y(reading.height), r: 4, className: READ_DOT }),
1242
+ /* @__PURE__ */ jsx13(
1104
1243
  "text",
1105
1244
  {
1106
1245
  x: labelX,
@@ -1199,8 +1338,8 @@ var sectionGeometry = (whole, feature, width) => {
1199
1338
  };
1200
1339
 
1201
1340
  // src/reach/section-parts.tsx
1202
- import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1203
- var Arrows = ({ id }) => /* @__PURE__ */ jsx15("defs", { children: /* @__PURE__ */ jsx15(
1341
+ import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1342
+ var Arrows = ({ id }) => /* @__PURE__ */ jsx14("defs", { children: /* @__PURE__ */ jsx14(
1204
1343
  "marker",
1205
1344
  {
1206
1345
  id,
@@ -1210,14 +1349,14 @@ var Arrows = ({ id }) => /* @__PURE__ */ jsx15("defs", { children: /* @__PURE__
1210
1349
  markerWidth: "7",
1211
1350
  markerHeight: "7",
1212
1351
  orient: "auto-start-reverse",
1213
- children: /* @__PURE__ */ jsx15("path", { d: "M 0 1.5 L 10 5 L 0 8.5 z", className: ARROW })
1352
+ children: /* @__PURE__ */ jsx14("path", { d: "M 0 1.5 L 10 5 L 0 8.5 z", className: ARROW })
1214
1353
  }
1215
1354
  ) });
1216
1355
  var SectionShapes = ({ geometry }) => {
1217
1356
  const { x, floorY, right, featureTopY, featureBottomY, wallTop, feature } = geometry;
1218
1357
  const floorLeft = feature.through ? x(0) : SECTION_PAD.left;
1219
- return /* @__PURE__ */ jsxs13(Fragment4, { children: [
1220
- /* @__PURE__ */ jsx15(
1358
+ return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1359
+ /* @__PURE__ */ jsx14(
1221
1360
  "rect",
1222
1361
  {
1223
1362
  x: SECTION_PAD.left,
@@ -1227,7 +1366,7 @@ var SectionShapes = ({ geometry }) => {
1227
1366
  className: FEATURE_FILL
1228
1367
  }
1229
1368
  ),
1230
- /* @__PURE__ */ jsx15(
1369
+ /* @__PURE__ */ jsx14(
1231
1370
  "rect",
1232
1371
  {
1233
1372
  x: floorLeft,
@@ -1237,9 +1376,9 @@ var SectionShapes = ({ geometry }) => {
1237
1376
  className: FLOOR
1238
1377
  }
1239
1378
  ),
1240
- /* @__PURE__ */ jsx15("path", { d: geometry.materialPath, strokeWidth: 1, className: MATERIAL }),
1241
- /* @__PURE__ */ jsx15("line", { x1: floorLeft, x2: right, y1: floorY, y2: floorY, strokeWidth: 1, className: LINE }),
1242
- feature.through ? null : /* @__PURE__ */ jsx15(
1379
+ /* @__PURE__ */ jsx14("path", { d: geometry.materialPath, strokeWidth: 1, className: MATERIAL }),
1380
+ /* @__PURE__ */ jsx14("line", { x1: floorLeft, x2: right, y1: floorY, y2: floorY, strokeWidth: 1, className: LINE }),
1381
+ feature.through ? null : /* @__PURE__ */ jsx14(
1243
1382
  "line",
1244
1383
  {
1245
1384
  x1: SECTION_PAD.left,
@@ -1250,7 +1389,7 @@ var SectionShapes = ({ geometry }) => {
1250
1389
  className: FEATURE_LINE
1251
1390
  }
1252
1391
  ),
1253
- /* @__PURE__ */ jsx15(
1392
+ /* @__PURE__ */ jsx14(
1254
1393
  "line",
1255
1394
  {
1256
1395
  x1: x(0),
@@ -1261,7 +1400,7 @@ var SectionShapes = ({ geometry }) => {
1261
1400
  className: FEATURE_LINE
1262
1401
  }
1263
1402
  ),
1264
- /* @__PURE__ */ jsx15(
1403
+ /* @__PURE__ */ jsx14(
1265
1404
  "line",
1266
1405
  {
1267
1406
  x1: x(0),
@@ -1277,8 +1416,8 @@ var SectionShapes = ({ geometry }) => {
1277
1416
  };
1278
1417
  var SectionHeadings = ({ geometry }) => {
1279
1418
  const { right } = geometry;
1280
- return /* @__PURE__ */ jsxs13(Fragment4, { children: [
1281
- /* @__PURE__ */ jsx15(
1419
+ return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1420
+ /* @__PURE__ */ jsx14(
1282
1421
  "text",
1283
1422
  {
1284
1423
  x: geometry.featureHeadingX,
@@ -1290,7 +1429,7 @@ var SectionHeadings = ({ geometry }) => {
1290
1429
  children: geometry.featureHeading
1291
1430
  }
1292
1431
  ),
1293
- /* @__PURE__ */ jsx15(
1432
+ /* @__PURE__ */ jsx14(
1294
1433
  "text",
1295
1434
  {
1296
1435
  x: right,
@@ -1304,7 +1443,7 @@ var SectionHeadings = ({ geometry }) => {
1304
1443
  )
1305
1444
  ] });
1306
1445
  };
1307
- var FeatureBreak = ({ geometry }) => geometry.featureCut ? /* @__PURE__ */ jsx15(
1446
+ var FeatureBreak = ({ geometry }) => geometry.featureCut ? /* @__PURE__ */ jsx14(
1308
1447
  "path",
1309
1448
  {
1310
1449
  d: featureBreak(SECTION_PAD.left, geometry.featureTopY, geometry.featureBottomY),
@@ -1317,9 +1456,9 @@ var FeatureDepth = ({ geometry, units }) => {
1317
1456
  const { x, featureTopY, datumX } = geometry;
1318
1457
  const { depth } = geometry.feature;
1319
1458
  if (depth === null) return null;
1320
- return /* @__PURE__ */ jsxs13("g", { children: [
1321
- /* @__PURE__ */ jsx15("title", { children: "Feature depth" }),
1322
- /* @__PURE__ */ jsx15(
1459
+ return /* @__PURE__ */ jsxs11("g", { children: [
1460
+ /* @__PURE__ */ jsx14("title", { children: "Feature depth" }),
1461
+ /* @__PURE__ */ jsx14(
1323
1462
  "line",
1324
1463
  {
1325
1464
  x1: SECTION_PAD.left,
@@ -1330,7 +1469,7 @@ var FeatureDepth = ({ geometry, units }) => {
1330
1469
  className: FEATURE_LINE
1331
1470
  }
1332
1471
  ),
1333
- /* @__PURE__ */ jsx15(
1472
+ /* @__PURE__ */ jsx14(
1334
1473
  "line",
1335
1474
  {
1336
1475
  x1: x(0),
@@ -1342,7 +1481,7 @@ var FeatureDepth = ({ geometry, units }) => {
1342
1481
  className: FEATURE_LINE
1343
1482
  }
1344
1483
  ),
1345
- x(0) - SECTION_PAD.left >= DEPTH_WORD_PX ? /* @__PURE__ */ jsx15(
1484
+ x(0) - SECTION_PAD.left >= DEPTH_WORD_PX ? /* @__PURE__ */ jsx14(
1346
1485
  "text",
1347
1486
  {
1348
1487
  x: SECTION_PAD.left + 4,
@@ -1353,7 +1492,7 @@ var FeatureDepth = ({ geometry, units }) => {
1353
1492
  children: "depth"
1354
1493
  }
1355
1494
  ) : null,
1356
- /* @__PURE__ */ jsx15(
1495
+ /* @__PURE__ */ jsx14(
1357
1496
  "text",
1358
1497
  {
1359
1498
  x: datumX + 4,
@@ -1369,8 +1508,8 @@ var FeatureDepth = ({ geometry, units }) => {
1369
1508
  var ReadRun = ({ geometry, units, arrowId }) => {
1370
1509
  const { x, last, cut, floorY, ordinateY } = geometry;
1371
1510
  if (last <= 0 || cut) return null;
1372
- return /* @__PURE__ */ jsxs13("g", { children: [
1373
- /* @__PURE__ */ jsx15(
1511
+ return /* @__PURE__ */ jsxs11("g", { children: [
1512
+ /* @__PURE__ */ jsx14(
1374
1513
  "line",
1375
1514
  {
1376
1515
  x1: x(0),
@@ -1383,7 +1522,7 @@ var ReadRun = ({ geometry, units, arrowId }) => {
1383
1522
  className: LINE
1384
1523
  }
1385
1524
  ),
1386
- /* @__PURE__ */ jsx15(
1525
+ /* @__PURE__ */ jsx14(
1387
1526
  "line",
1388
1527
  {
1389
1528
  x1: x(last),
@@ -1394,7 +1533,7 @@ var ReadRun = ({ geometry, units, arrowId }) => {
1394
1533
  className: LINE
1395
1534
  }
1396
1535
  ),
1397
- /* @__PURE__ */ jsxs13(
1536
+ /* @__PURE__ */ jsxs11(
1398
1537
  "text",
1399
1538
  {
1400
1539
  x: (x(0) + x(last)) / 2,
@@ -1413,8 +1552,8 @@ var ReadRun = ({ geometry, units, arrowId }) => {
1413
1552
  var OffsetOrdinates = (props) => {
1414
1553
  const { geometry, units } = props;
1415
1554
  const { x, y, right, width, floorY, ordinateY } = geometry;
1416
- return /* @__PURE__ */ jsxs13(Fragment4, { children: [
1417
- /* @__PURE__ */ jsx15(
1555
+ return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1556
+ /* @__PURE__ */ jsx14(
1418
1557
  "text",
1419
1558
  {
1420
1559
  x: Math.min(
@@ -1429,7 +1568,7 @@ var OffsetOrdinates = (props) => {
1429
1568
  children: "XY PLANE DISTANCE FROM FEATURE"
1430
1569
  }
1431
1570
  ),
1432
- /* @__PURE__ */ jsx15(
1571
+ /* @__PURE__ */ jsx14(
1433
1572
  "line",
1434
1573
  {
1435
1574
  x1: x(0),
@@ -1440,9 +1579,9 @@ var OffsetOrdinates = (props) => {
1440
1579
  className: LINE
1441
1580
  }
1442
1581
  ),
1443
- /* @__PURE__ */ jsx15("text", { x: x(0), y: ordinateY + 15, textAnchor: "middle", fontSize: 10, className: TEXT, children: "0" }),
1444
- geometry.across.map((riser) => /* @__PURE__ */ jsxs13("g", { children: [
1445
- /* @__PURE__ */ jsx15(
1582
+ /* @__PURE__ */ jsx14("text", { x: x(0), y: ordinateY + 15, textAnchor: "middle", fontSize: 10, className: TEXT, children: "0" }),
1583
+ geometry.across.map((riser) => /* @__PURE__ */ jsxs11("g", { children: [
1584
+ /* @__PURE__ */ jsx14(
1446
1585
  "line",
1447
1586
  {
1448
1587
  x1: x(riser.at),
@@ -1454,7 +1593,7 @@ var OffsetOrdinates = (props) => {
1454
1593
  className: LINE
1455
1594
  }
1456
1595
  ),
1457
- /* @__PURE__ */ jsx15(
1596
+ /* @__PURE__ */ jsx14(
1458
1597
  "text",
1459
1598
  {
1460
1599
  x: x(riser.at),
@@ -1466,15 +1605,15 @@ var OffsetOrdinates = (props) => {
1466
1605
  }
1467
1606
  )
1468
1607
  ] }, `x:${riser.at}`)),
1469
- /* @__PURE__ */ jsx15(ReadRun, { ...props })
1608
+ /* @__PURE__ */ jsx14(ReadRun, { ...props })
1470
1609
  ] });
1471
1610
  };
1472
1611
  var HeightOrdinates = ({ geometry, units }) => {
1473
1612
  const { y, right, floorY, datumX } = geometry;
1474
- return /* @__PURE__ */ jsxs13(Fragment4, { children: [
1475
- /* @__PURE__ */ jsx15("text", { x: datumX + 4, y: floorY + 3.5, fontSize: 10, className: TEXT, children: "0" }),
1476
- geometry.up.map((h) => /* @__PURE__ */ jsxs13("g", { children: [
1477
- /* @__PURE__ */ jsx15(
1613
+ return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1614
+ /* @__PURE__ */ jsx14("text", { x: datumX + 4, y: floorY + 3.5, fontSize: 10, className: TEXT, children: "0" }),
1615
+ geometry.up.map((h) => /* @__PURE__ */ jsxs11("g", { children: [
1616
+ /* @__PURE__ */ jsx14(
1478
1617
  "line",
1479
1618
  {
1480
1619
  x1: right - 2,
@@ -1486,37 +1625,37 @@ var HeightOrdinates = ({ geometry, units }) => {
1486
1625
  className: LINE
1487
1626
  }
1488
1627
  ),
1489
- /* @__PURE__ */ jsx15("text", { x: datumX + 4, y: y(h) + 3.5, fontSize: 10, className: TEXT, children: lengthValue(h, units) })
1628
+ /* @__PURE__ */ jsx14("text", { x: datumX + 4, y: y(h) + 3.5, fontSize: 10, className: TEXT, children: lengthValue(h, units) })
1490
1629
  ] }, `y:${h}`))
1491
1630
  ] });
1492
1631
  };
1493
1632
 
1494
1633
  // src/reach/reach-section.tsx
1495
- import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
1634
+ import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1496
1635
  var ReachSection = ({
1497
1636
  curve: whole,
1498
1637
  units,
1499
1638
  feature,
1500
1639
  width
1501
1640
  }) => {
1502
- const [hover, setHover] = useState8(null);
1641
+ const [hover, setHover] = useState9(null);
1503
1642
  const geometry = useMemo3(() => sectionGeometry(whole, feature, width), [whole, feature, width]);
1504
- const reactId = useId();
1643
+ const reactId = useId2();
1505
1644
  const arrowId = `reach-dim-arrow-${reactId.replace(/[^\w-]/g, "")}`;
1506
1645
  const reading = hover === null ? null : readingAt(whole, hover);
1507
1646
  const drawing = useMemo3(
1508
- () => /* @__PURE__ */ jsxs14(Fragment5, { children: [
1509
- /* @__PURE__ */ jsx16(Arrows, { id: arrowId }),
1510
- /* @__PURE__ */ jsx16(SectionShapes, { geometry }),
1511
- /* @__PURE__ */ jsx16(SectionHeadings, { geometry }),
1512
- /* @__PURE__ */ jsx16(FeatureBreak, { geometry }),
1513
- /* @__PURE__ */ jsx16(FeatureDepth, { geometry, units }),
1514
- /* @__PURE__ */ jsx16(OffsetOrdinates, { geometry, units, arrowId }),
1515
- /* @__PURE__ */ jsx16(HeightOrdinates, { geometry, units })
1647
+ () => /* @__PURE__ */ jsxs12(Fragment5, { children: [
1648
+ /* @__PURE__ */ jsx15(Arrows, { id: arrowId }),
1649
+ /* @__PURE__ */ jsx15(SectionShapes, { geometry }),
1650
+ /* @__PURE__ */ jsx15(SectionHeadings, { geometry }),
1651
+ /* @__PURE__ */ jsx15(FeatureBreak, { geometry }),
1652
+ /* @__PURE__ */ jsx15(FeatureDepth, { geometry, units }),
1653
+ /* @__PURE__ */ jsx15(OffsetOrdinates, { geometry, units, arrowId }),
1654
+ /* @__PURE__ */ jsx15(HeightOrdinates, { geometry, units })
1516
1655
  ] }),
1517
1656
  [arrowId, geometry, units]
1518
1657
  );
1519
- return /* @__PURE__ */ jsxs14(
1658
+ return /* @__PURE__ */ jsxs12(
1520
1659
  "svg",
1521
1660
  {
1522
1661
  viewBox: `0 0 ${width} ${geometry.height}`,
@@ -1529,17 +1668,17 @@ var ReachSection = ({
1529
1668
  onPointerLeave: () => setHover(null),
1530
1669
  children: [
1531
1670
  drawing,
1532
- reading ? /* @__PURE__ */ jsx16(ReadingMark, { geometry, reading, units }) : null
1671
+ reading ? /* @__PURE__ */ jsx15(ReadingMark, { geometry, reading, units }) : null
1533
1672
  ]
1534
1673
  }
1535
1674
  );
1536
1675
  };
1537
1676
 
1538
1677
  // src/reach/use-element-size.ts
1539
- import { useLayoutEffect, useRef as useRef4, useState as useState9 } from "react";
1678
+ import { useLayoutEffect, useRef as useRef5, useState as useState10 } from "react";
1540
1679
  var useElementSize = () => {
1541
- const ref = useRef4(null);
1542
- const [size, setSize] = useState9({ width: 0, height: 0 });
1680
+ const ref = useRef5(null);
1681
+ const [size, setSize] = useState10({ width: 0, height: 0 });
1543
1682
  useLayoutEffect(() => {
1544
1683
  const element = ref.current;
1545
1684
  if (!element) return;
@@ -1557,12 +1696,12 @@ var useElementSize = () => {
1557
1696
  };
1558
1697
 
1559
1698
  // src/reach/reach-chart.tsx
1560
- import { Fragment as Fragment6, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
1699
+ import { Fragment as Fragment6, jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
1561
1700
  var CHART_MIN_W = 200;
1562
1701
  var DRAWING_MIN_W = 240;
1563
1702
  var ReachChart = ({ curve, units, feature }) => {
1564
1703
  const [ref, size] = useElementSize();
1565
- return /* @__PURE__ */ jsx17("div", { ref, className: "w-full", children: /* @__PURE__ */ jsx17(
1704
+ return /* @__PURE__ */ jsx16("div", { ref, className: "w-full", children: /* @__PURE__ */ jsx16(
1566
1705
  ReachSection,
1567
1706
  {
1568
1707
  curve,
@@ -1580,7 +1719,7 @@ var CutNote = ({
1580
1719
  if (!trimmed.cut) return null;
1581
1720
  const drawnTo = trimmed.curve.offsets[trimmed.curve.offsets.length - 1] ?? 0;
1582
1721
  const readTo = curve.offsets[curve.offsets.length - 1] ?? 0;
1583
- return /* @__PURE__ */ jsxs15(Fragment6, { children: [
1722
+ return /* @__PURE__ */ jsxs13(Fragment6, { children: [
1584
1723
  " ",
1585
1724
  "It is at full height from ",
1586
1725
  lengthValue(drawnTo, units),
@@ -1595,13 +1734,13 @@ var CutNote = ({
1595
1734
  };
1596
1735
  var ReachDrawing = ({ curve, units, feature }) => {
1597
1736
  const [ref, size] = useElementSize();
1598
- return /* @__PURE__ */ jsxs15("div", { ref, className: "flex flex-col gap-3 p-4", children: [
1599
- /* @__PURE__ */ jsxs15("figure", { className: "flex flex-col gap-1", children: [
1600
- /* @__PURE__ */ jsxs15("figcaption", { className: "text-2xs font-semibold text-gray-400 uppercase dark:text-zinc-500", children: [
1737
+ return /* @__PURE__ */ jsxs13("div", { ref, className: "flex flex-col gap-3 p-4", children: [
1738
+ /* @__PURE__ */ jsxs13("figure", { className: "flex flex-col gap-1", children: [
1739
+ /* @__PURE__ */ jsxs13("figcaption", { className: "text-2xs font-semibold text-gray-400 uppercase dark:text-zinc-500", children: [
1601
1740
  "Section at the wall \xB7 to scale \xB7 ",
1602
1741
  unitLabel(units)
1603
1742
  ] }),
1604
- /* @__PURE__ */ jsx17(
1743
+ /* @__PURE__ */ jsx16(
1605
1744
  ReachSection,
1606
1745
  {
1607
1746
  curve,
@@ -1611,18 +1750,875 @@ var ReachDrawing = ({ curve, units, feature }) => {
1611
1750
  }
1612
1751
  )
1613
1752
  ] }),
1614
- /* @__PURE__ */ jsxs15("p", { className: "text-2xs text-gray-400 dark:text-zinc-500", children: [
1753
+ /* @__PURE__ */ jsxs13("p", { className: "text-2xs text-gray-400 dark:text-zinc-500", children: [
1615
1754
  "Point at the walls to read them: the distance from the feature, then the reach a tool needs there.",
1616
- /* @__PURE__ */ jsx17(CutNote, { curve, units })
1755
+ /* @__PURE__ */ jsx16(CutNote, { curve, units })
1617
1756
  ] })
1618
1757
  ] });
1619
1758
  };
1759
+
1760
+ // src/feature-panel/fold-store.ts
1761
+ var storageFolds = (storage, prefix) => ({
1762
+ read: (id) => {
1763
+ try {
1764
+ const stored = storage?.getItem(`${prefix}${id}`);
1765
+ return stored === "open" ? true : stored === "shut" ? false : void 0;
1766
+ } catch {
1767
+ return void 0;
1768
+ }
1769
+ },
1770
+ write: (id, open) => {
1771
+ try {
1772
+ storage?.setItem(`${prefix}${id}`, open ? "open" : "shut");
1773
+ } catch {
1774
+ }
1775
+ }
1776
+ });
1777
+
1778
+ // src/feature-panel/feature-details.tsx
1779
+ import { useCallback as useCallback2, useMemo as useMemo7, useState as useState13 } from "react";
1780
+ import { CrosshairIcon, InfoIcon } from "@phosphor-icons/react";
1781
+ import { Button as Button3, Tooltip, cn as cn12 } from "@toolpath/ui";
1782
+
1783
+ // src/feature-panel/feature-name.tsx
1784
+ import { cn as cn9 } from "@toolpath/ui";
1785
+
1786
+ // src/feature-panel/look.ts
1787
+ var resolveLook = (look) => ({
1788
+ rules: look?.rules ?? "sentence",
1789
+ selected: look?.selected ?? "grey",
1790
+ muted: look?.muted ?? "pale"
1791
+ });
1792
+ var secondaryText = (look) => look.muted === "strong" ? "text-gray dark:text-zinc-300" : "text-gray-400 dark:text-zinc-400";
1793
+ var faintText = (look) => look.muted === "strong" ? "text-gray dark:text-zinc-300" : "text-gray-300 dark:text-zinc-500";
1794
+
1795
+ // src/feature-panel/feature-name.tsx
1796
+ import { Fragment as Fragment7, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
1797
+ var FeatureName = ({ feature, label, look }) => /* @__PURE__ */ jsxs14(Fragment7, { children: [
1798
+ label ?? featureTypeLabel(feature.featureType),
1799
+ " ",
1800
+ /* @__PURE__ */ jsxs14("span", { className: cn9("font-normal", faintText(look)), children: [
1801
+ "from ",
1802
+ directionLabel(feature.machiningDirection)
1803
+ ] })
1804
+ ] });
1805
+ var RequiredBadge = () => /* @__PURE__ */ jsx17("span", { className: "rounded-full bg-gray-100 px-1.5 text-2xs font-semibold text-gray-500 uppercase dark:bg-zinc-800 dark:text-zinc-300", children: "Required" });
1806
+
1807
+ // src/feature-panel/fold-section.tsx
1808
+ import { useState as useState11 } from "react";
1809
+ import { CaretRightIcon as CaretRightIcon3 } from "@phosphor-icons/react";
1810
+ import { cn as cn10 } from "@toolpath/ui";
1811
+ import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
1812
+ var FoldSection = ({
1813
+ id,
1814
+ title: title2,
1815
+ action,
1816
+ defaultOpen = true,
1817
+ folds,
1818
+ className,
1819
+ children
1820
+ }) => {
1821
+ const [open, setOpen] = useState11(() => folds?.read(id) ?? defaultOpen);
1822
+ return /* @__PURE__ */ jsxs15("section", { className: cn10("flex flex-col gap-1", className), children: [
1823
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", children: [
1824
+ /* @__PURE__ */ jsxs15(
1825
+ "button",
1826
+ {
1827
+ type: "button",
1828
+ "aria-expanded": open,
1829
+ onClick: () => {
1830
+ setOpen(!open);
1831
+ folds?.write(id, !open);
1832
+ },
1833
+ className: cn10(
1834
+ "flex min-w-0 flex-1 cursor-pointer items-center gap-1 rounded text-left text-2xs font-semibold text-gray-400 uppercase",
1835
+ "hover:text-gray-600 dark:text-zinc-500 dark:hover:text-zinc-300",
1836
+ "outline-none focus-visible:ring-2 focus-visible:ring-info/75"
1837
+ ),
1838
+ children: [
1839
+ /* @__PURE__ */ jsx18(
1840
+ CaretRightIcon3,
1841
+ {
1842
+ weight: "bold",
1843
+ className: cn10("size-3 shrink-0 transition-transform duration-150", open && "rotate-90")
1844
+ }
1845
+ ),
1846
+ /* @__PURE__ */ jsx18("span", { className: "truncate", children: title2 })
1847
+ ]
1848
+ }
1849
+ ),
1850
+ action
1851
+ ] }),
1852
+ open ? children : null
1853
+ ] });
1854
+ };
1855
+
1856
+ // src/feature-panel/read-state.tsx
1857
+ import { useEffect as useEffect4, useState as useState12 } from "react";
1858
+ import { Button as Button2 } from "@toolpath/ui";
1859
+ import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
1860
+ var Reading = ({ children }) => /* @__PURE__ */ jsx19("p", { className: "text-sm text-gray-400 dark:text-zinc-400", role: "status", children });
1861
+ var ReadFailure = ({
1862
+ failure
1863
+ }) => {
1864
+ const waiting = useWaiting(failure.retryAt);
1865
+ return /* @__PURE__ */ jsxs16("div", { className: "flex flex-col items-start gap-2 text-sm", children: [
1866
+ /* @__PURE__ */ jsx19("p", { className: "text-danger", role: "alert", children: failure.message }),
1867
+ failure.retry ? /* @__PURE__ */ jsx19(Button2, { variant: "secondary", size: "sm", disabled: waiting, onClick: failure.retry, children: "Retry" }) : null
1868
+ ] });
1869
+ };
1870
+ var useWaiting = (until) => {
1871
+ const [passed, setPassed] = useState12(void 0);
1872
+ useEffect4(() => {
1873
+ if (until === void 0) return;
1874
+ const timer = setTimeout(() => setPassed(until), Math.max(until - Date.now(), 0));
1875
+ return () => clearTimeout(timer);
1876
+ }, [until]);
1877
+ return until !== void 0 && passed !== until;
1878
+ };
1879
+
1880
+ // src/feature-panel/pop-outs.tsx
1881
+ import { useMemo as useMemo6 } from "react";
1882
+ import {
1883
+ ArrowSquareOutIcon,
1884
+ BracketsCurlyIcon,
1885
+ ChartLineIcon,
1886
+ TableIcon
1887
+ } from "@phosphor-icons/react";
1888
+ import { IconButton as IconButton3 } from "@toolpath/ui";
1889
+
1890
+ // src/feature-panel/record-sections.tsx
1891
+ import { useMemo as useMemo5 } from "react";
1892
+ import { CheckIcon as CheckIcon3, CopyIcon as CopyIcon2 } from "@phosphor-icons/react";
1893
+ import { IconButton as IconButton2, cn as cn11 } from "@toolpath/ui";
1894
+
1895
+ // src/model/feature-record.ts
1896
+ var MAX_DATASHEET_FIELDS = 500;
1897
+ var formatLeaf = (value) => typeof value === "number" ? String(Number(value.toFixed(4))) : typeof value === "string" ? value : value === null ? "null" : String(value);
1898
+ var datasheetFields = (datasheet) => {
1899
+ const fields = [];
1900
+ let more = false;
1901
+ const walk = (value, path) => {
1902
+ if (fields.length >= MAX_DATASHEET_FIELDS) {
1903
+ more = true;
1904
+ return;
1905
+ }
1906
+ if (Array.isArray(value)) {
1907
+ if (value.every((each) => typeof each !== "object" || each === null)) {
1908
+ fields.push({ path, value: value.length === 0 ? "[]" : value.map(formatLeaf).join(", ") });
1909
+ } else {
1910
+ value.forEach((each, index) => walk(each, `${path}[${index}]`));
1911
+ }
1912
+ } else if (typeof value === "object" && value !== null) {
1913
+ const entries = Object.entries(value);
1914
+ if (entries.length === 0) fields.push({ path, value: "{}" });
1915
+ for (const [key, each] of entries) walk(each, path ? `${path}.${key}` : key);
1916
+ } else {
1917
+ fields.push({ path, value: formatLeaf(value) });
1918
+ }
1919
+ };
1920
+ if (datasheet !== null && datasheet !== void 0) walk(datasheet, "");
1921
+ return { fields, more };
1922
+ };
1923
+
1924
+ // src/feature-panel/record-sections.tsx
1925
+ import { Fragment as Fragment8, jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
1926
+ var COPIED_MS2 = 1500;
1927
+ var RecordSections = ({
1928
+ record,
1929
+ look,
1930
+ folds,
1931
+ popOut
1932
+ }) => {
1933
+ const value = record.status === "ready" ? record.value : null;
1934
+ const { fields, more } = useMemo5(() => datasheetFields(value?.datasheet), [value]);
1935
+ const json = useMemo5(() => value ? JSON.stringify(value, null, 2) : "", [value]);
1936
+ if (record.status === "ready" && !value) return null;
1937
+ const waiting = record.status === "loading" ? /* @__PURE__ */ jsx20(Reading, { children: "Reading the feature\u2019s record\u2026" }) : record.status === "error" ? /* @__PURE__ */ jsx20(ReadFailure, { failure: record }) : null;
1938
+ const count = more ? `${MAX_DATASHEET_FIELDS}+` : String(fields.length);
1939
+ return /* @__PURE__ */ jsxs17(Fragment8, { children: [
1940
+ /* @__PURE__ */ jsx20(
1941
+ FoldSection,
1942
+ {
1943
+ id: "datasheet",
1944
+ defaultOpen: false,
1945
+ title: value ? `All datasheet fields (${count})` : "All datasheet fields",
1946
+ action: value ? popOut?.("datasheet") : null,
1947
+ folds,
1948
+ children: waiting ?? /* @__PURE__ */ jsx20(DatasheetFields, { record: value, look })
1949
+ }
1950
+ ),
1951
+ /* @__PURE__ */ jsx20(
1952
+ FoldSection,
1953
+ {
1954
+ id: "raw",
1955
+ defaultOpen: false,
1956
+ title: "Raw API record",
1957
+ action: value ? /* @__PURE__ */ jsxs17(Fragment8, { children: [
1958
+ /* @__PURE__ */ jsx20(CopyButton, { text: json }),
1959
+ popOut?.("raw")
1960
+ ] }) : null,
1961
+ folds,
1962
+ children: waiting ?? /* @__PURE__ */ jsx20(RawRecord, { json })
1963
+ }
1964
+ )
1965
+ ] });
1966
+ };
1967
+ var DatasheetFields = ({ record, look, wide = false }) => {
1968
+ const { fields } = useMemo5(() => datasheetFields(record?.datasheet), [record]);
1969
+ return record?.datasheet ? /* @__PURE__ */ jsx20(
1970
+ "dl",
1971
+ {
1972
+ className: cn11(
1973
+ "grid grid-cols-[minmax(0,1fr)_auto] gap-y-0.5 text-xs",
1974
+ wide ? "gap-x-4 p-4" : "gap-x-3"
1975
+ ),
1976
+ children: fields.map((field) => /* @__PURE__ */ jsxs17("div", { className: "contents", children: [
1977
+ /* @__PURE__ */ jsx20("dt", { className: cn11("truncate font-mono", secondaryText(look)), title: field.path, children: field.path }),
1978
+ /* @__PURE__ */ jsx20(
1979
+ "dd",
1980
+ {
1981
+ className: cn11(
1982
+ "truncate text-right font-mono text-gray-600 tabular-nums select-text dark:text-zinc-100",
1983
+ wide ? "max-w-60" : "max-w-40"
1984
+ ),
1985
+ title: field.value,
1986
+ children: field.value
1987
+ }
1988
+ )
1989
+ ] }, field.path))
1990
+ }
1991
+ ) : /* @__PURE__ */ jsx20("p", { className: cn11("text-sm", secondaryText(look), wide && "p-4"), children: "The API sent no datasheet for this feature." });
1992
+ };
1993
+ var RawRecord = ({
1994
+ json,
1995
+ wide = false
1996
+ }) => /* @__PURE__ */ jsx20(
1997
+ "pre",
1998
+ {
1999
+ className: cn11(
2000
+ "font-mono text-2xs leading-relaxed text-gray-600 select-text dark:text-zinc-200",
2001
+ wide ? "p-4" : "max-h-80 overflow-auto rounded bg-gray-50 p-2 dark:bg-zinc-800/60"
2002
+ ),
2003
+ children: json
2004
+ }
2005
+ );
2006
+ var CopyButton = ({ text }) => {
2007
+ const [copied, setCopied] = useTimedValue(COPIED_MS2);
2008
+ return /* @__PURE__ */ jsx20(
2009
+ IconButton2,
2010
+ {
2011
+ "aria-label": copied ? "Copied" : "Copy raw record",
2012
+ title: "Copy",
2013
+ variant: "muted",
2014
+ size: "sm",
2015
+ onClick: () => {
2016
+ void navigator.clipboard?.writeText(text).then(() => setCopied(true)).catch(() => void 0);
2017
+ },
2018
+ children: copied ? /* @__PURE__ */ jsx20(CheckIcon3, { weight: "bold" }) : /* @__PURE__ */ jsx20(CopyIcon2, { weight: "bold" })
2019
+ }
2020
+ );
2021
+ };
2022
+
2023
+ // src/feature-panel/pop-outs.tsx
2024
+ import { Fragment as Fragment9, jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
2025
+ var TITLES = {
2026
+ reach: "Reach",
2027
+ datasheet: "All datasheet fields",
2028
+ raw: "Raw API record"
2029
+ };
2030
+ var LABELS = {
2031
+ reach: "Pop out the reach drawing",
2032
+ datasheet: "Pop out the datasheet fields",
2033
+ raw: "Pop out the raw record"
2034
+ };
2035
+ var PopOutButton = ({
2036
+ id,
2037
+ onOpen
2038
+ }) => /* @__PURE__ */ jsx21(
2039
+ IconButton3,
2040
+ {
2041
+ "aria-label": LABELS[id],
2042
+ title: "Pop out, larger",
2043
+ variant: "muted",
2044
+ size: "sm",
2045
+ onClick: () => onOpen(id),
2046
+ children: /* @__PURE__ */ jsx21(ArrowSquareOutIcon, { weight: "bold" })
2047
+ }
2048
+ );
2049
+ var PopOuts = ({
2050
+ open,
2051
+ onClose,
2052
+ Window,
2053
+ subtitle,
2054
+ record,
2055
+ units,
2056
+ profile,
2057
+ look
2058
+ }) => {
2059
+ const curve = useMemo6(() => readReachCurve(record?.datasheet), [record]);
2060
+ const json = useMemo6(() => record ? JSON.stringify(record, null, 2) : "", [record]);
2061
+ if (!record) return null;
2062
+ return /* @__PURE__ */ jsxs18(Fragment9, { children: [
2063
+ open.has("reach") && curve ? /* @__PURE__ */ jsx21(
2064
+ Window,
2065
+ {
2066
+ id: "reach",
2067
+ title: TITLES.reach,
2068
+ subtitle,
2069
+ icon: /* @__PURE__ */ jsx21(ChartLineIcon, { weight: "bold" }),
2070
+ onClose: () => onClose("reach"),
2071
+ children: /* @__PURE__ */ jsx21(ReachDrawing, { curve, units, feature: profile })
2072
+ }
2073
+ ) : null,
2074
+ open.has("datasheet") ? /* @__PURE__ */ jsx21(
2075
+ Window,
2076
+ {
2077
+ id: "datasheet",
2078
+ title: TITLES.datasheet,
2079
+ subtitle,
2080
+ icon: /* @__PURE__ */ jsx21(TableIcon, { weight: "bold" }),
2081
+ onClose: () => onClose("datasheet"),
2082
+ children: /* @__PURE__ */ jsx21(DatasheetFields, { record, look, wide: true })
2083
+ }
2084
+ ) : null,
2085
+ open.has("raw") ? /* @__PURE__ */ jsx21(
2086
+ Window,
2087
+ {
2088
+ id: "raw",
2089
+ title: TITLES.raw,
2090
+ subtitle,
2091
+ icon: /* @__PURE__ */ jsx21(BracketsCurlyIcon, { weight: "bold" }),
2092
+ action: /* @__PURE__ */ jsx21(CopyButton, { text: json }),
2093
+ onClose: () => onClose("raw"),
2094
+ children: /* @__PURE__ */ jsx21(RawRecord, { json, wide: true })
2095
+ }
2096
+ ) : null
2097
+ ] });
2098
+ };
2099
+
2100
+ // src/feature-panel/feature-details.tsx
2101
+ import { Fragment as Fragment10, jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
2102
+ var FeatureDetails = ({
2103
+ feature,
2104
+ units,
2105
+ directionColor,
2106
+ profile,
2107
+ label,
2108
+ required,
2109
+ onFrame,
2110
+ actions,
2111
+ status,
2112
+ children,
2113
+ rules,
2114
+ measurements,
2115
+ record,
2116
+ PopOut,
2117
+ look: lookOption,
2118
+ folds
2119
+ }) => {
2120
+ const look = resolveLook(lookOption);
2121
+ const name = /* @__PURE__ */ jsx22(FeatureName, { feature, label, look });
2122
+ const [out, setOut] = useState13(/* @__PURE__ */ new Set());
2123
+ const openOut = useCallback2((id) => setOut((was) => new Set(was).add(id)), []);
2124
+ const closeOut = useCallback2(
2125
+ (id) => setOut((was) => {
2126
+ const next = new Set(was);
2127
+ next.delete(id);
2128
+ return next;
2129
+ }),
2130
+ []
2131
+ );
2132
+ const popOut = PopOut ? (id) => /* @__PURE__ */ jsx22(PopOutButton, { id, onOpen: openOut }) : void 0;
2133
+ return /* @__PURE__ */ jsxs19("div", { className: "flex flex-col gap-4 p-4", children: [
2134
+ /* @__PURE__ */ jsxs19("div", { className: "flex flex-col gap-2", children: [
2135
+ /* @__PURE__ */ jsxs19("div", { className: "flex items-start gap-2", children: [
2136
+ /* @__PURE__ */ jsxs19("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
2137
+ /* @__PURE__ */ jsx22("h3", { className: "truncate text-sm font-semibold text-gray-600 dark:text-zinc-100", children: name }),
2138
+ /* @__PURE__ */ jsxs19("p", { className: cn12("flex flex-wrap items-center gap-1.5 text-xs", secondaryText(look)), children: [
2139
+ /* @__PURE__ */ jsx22(Swatch, { color: directionColor, className: "size-2.5" }),
2140
+ "Machined from ",
2141
+ directionLabel(feature.machiningDirection),
2142
+ required ? /* @__PURE__ */ jsx22(RequiredBadge, {}) : null,
2143
+ status
2144
+ ] })
2145
+ ] }),
2146
+ actions,
2147
+ onFrame ? /* @__PURE__ */ jsx22(Button3, { variant: "secondary", size: "sm", onClick: onFrame, className: "shrink-0", children: /* @__PURE__ */ jsxs19("span", { className: "flex items-center gap-1 whitespace-nowrap", children: [
2148
+ /* @__PURE__ */ jsx22(CrosshairIcon, { weight: "bold", className: "size-3.5" }),
2149
+ "Show this feature"
2150
+ ] }) }) : null
2151
+ ] }),
2152
+ children
2153
+ ] }),
2154
+ /* @__PURE__ */ jsx22(FoldSection, { id: "rules", title: "Rules broken", folds, children: /* @__PURE__ */ jsx22(BrokenRuleList, { rules, look }) }),
2155
+ /* @__PURE__ */ jsx22(MeasurementSections, { measurements, look, folds }),
2156
+ /* @__PURE__ */ jsx22(
2157
+ ReachSection2,
2158
+ {
2159
+ record,
2160
+ units,
2161
+ profile,
2162
+ folds,
2163
+ action: popOut?.("reach")
2164
+ }
2165
+ ),
2166
+ /* @__PURE__ */ jsx22(RecordSections, { record, look, folds, popOut }),
2167
+ PopOut ? /* @__PURE__ */ jsx22(
2168
+ PopOuts,
2169
+ {
2170
+ open: out,
2171
+ onClose: closeOut,
2172
+ Window: PopOut,
2173
+ subtitle: name,
2174
+ record: record.status === "ready" ? record.value : null,
2175
+ units,
2176
+ profile,
2177
+ look
2178
+ }
2179
+ ) : null
2180
+ ] });
2181
+ };
2182
+ var BrokenRuleList = ({
2183
+ rules,
2184
+ look
2185
+ }) => rules.length > 0 ? /* @__PURE__ */ jsx22("ul", { className: "flex flex-col gap-1", children: rules.map((rule) => /* @__PURE__ */ jsxs19(
2186
+ "li",
2187
+ {
2188
+ className: "flex items-start gap-2 text-sm text-gray-600 dark:text-zinc-100",
2189
+ children: [
2190
+ /* @__PURE__ */ jsx22(Swatch, { color: rule.color, className: "mt-0.5" }),
2191
+ /* @__PURE__ */ jsx22(Explained, { note: rule.note, children: /* @__PURE__ */ jsx22("span", { className: "min-w-0 flex-1", children: rule.text }) }),
2192
+ rule.figure ? /* @__PURE__ */ jsxs19(
2193
+ "span",
2194
+ {
2195
+ className: cn12(
2196
+ "shrink-0 tabular-nums",
2197
+ look.rules === "result" ? "font-semibold" : secondaryText(look)
2198
+ ),
2199
+ style: look.rules === "result" ? { color: rule.color } : void 0,
2200
+ children: [
2201
+ rule.figure,
2202
+ look.rules === "result" && rule.limit ? /* @__PURE__ */ jsx22("span", { className: "ml-1 font-normal opacity-60", children: rule.limit }) : null
2203
+ ]
2204
+ }
2205
+ ) : null
2206
+ ]
2207
+ },
2208
+ rule.key
2209
+ )) }) : /* @__PURE__ */ jsx22(None, { look });
2210
+ var Explained = ({ note, children }) => note ? /* @__PURE__ */ jsx22(Tooltip, { tip: note, side: "left", children: /* @__PURE__ */ jsx22(
2211
+ "span",
2212
+ {
2213
+ tabIndex: 0,
2214
+ className: "min-w-0 flex-1 rounded outline-none focus-visible:ring-2 focus-visible:ring-info/75",
2215
+ children
2216
+ }
2217
+ ) }) : children;
2218
+ var None = ({ look }) => /* @__PURE__ */ jsx22("p", { className: cn12("text-sm", secondaryText(look)), children: "None" });
2219
+ var MeasurementSections = ({ measurements, look, folds }) => {
2220
+ if (measurements.status !== "ready") {
2221
+ return /* @__PURE__ */ jsx22(Fragment10, { children: /* @__PURE__ */ jsx22(FoldSection, { id: "measurements", title: "Measurements", folds, children: measurements.status === "loading" ? /* @__PURE__ */ jsx22(Reading, { children: "Reading the feature\u2019s datasheet\u2026" }) : /* @__PURE__ */ jsx22(ReadFailure, { failure: measurements }) }) });
2222
+ }
2223
+ const measured = measurements.value.filter((row) => !row.milling);
2224
+ const milling = measurements.value.filter((row) => row.milling);
2225
+ return /* @__PURE__ */ jsxs19(Fragment10, { children: [
2226
+ /* @__PURE__ */ jsx22(FoldSection, { id: "measurements", title: "Measurements", folds, children: /* @__PURE__ */ jsx22(MeasurementList, { rows: measured, look }) }),
2227
+ milling.length > 0 ? /* @__PURE__ */ jsx22(FoldSection, { id: "milling", title: "Milling considerations", folds, children: /* @__PURE__ */ jsx22(MeasurementList, { rows: milling, look }) }) : null
2228
+ ] });
2229
+ };
2230
+ var MeasurementList = ({ rows, look }) => rows.length > 0 ? /* @__PURE__ */ jsx22("dl", { className: "grid grid-cols-[1fr_auto] items-center gap-x-3 gap-y-1 text-sm", children: rows.map((row) => /* @__PURE__ */ jsxs19("div", { className: "contents", children: [
2231
+ /* @__PURE__ */ jsxs19("dt", { className: cn12("flex items-center gap-1", secondaryText(look)), children: [
2232
+ row.label,
2233
+ /* @__PURE__ */ jsx22(Derivation, { lines: row.derivation, look })
2234
+ ] }),
2235
+ /* @__PURE__ */ jsx22("dd", { className: "text-right text-gray-600 tabular-nums dark:text-zinc-100", title: row.alt, children: row.value })
2236
+ ] }, row.key)) }) : /* @__PURE__ */ jsx22(None, { look });
2237
+ var Derivation = ({
2238
+ lines,
2239
+ look
2240
+ }) => lines.length > 0 ? /* @__PURE__ */ jsx22(
2241
+ Tooltip,
2242
+ {
2243
+ side: "left",
2244
+ hoverable: true,
2245
+ tip: /* @__PURE__ */ jsx22("div", { className: "flex max-w-sm flex-col gap-0.5 font-mono text-2xs leading-snug whitespace-pre-wrap", children: lines.map((line, index) => /* @__PURE__ */ jsx22("span", { children: line }, index)) }),
2246
+ children: /* @__PURE__ */ jsx22(
2247
+ "button",
2248
+ {
2249
+ type: "button",
2250
+ "aria-label": "How this was measured",
2251
+ className: cn12(
2252
+ "shrink-0 rounded outline-none hover:text-gray-500 focus-visible:ring-2 focus-visible:ring-info/75 dark:hover:text-zinc-300",
2253
+ faintText(look)
2254
+ ),
2255
+ children: /* @__PURE__ */ jsx22(InfoIcon, { weight: "bold", className: "size-3" })
2256
+ }
2257
+ )
2258
+ }
2259
+ ) : null;
2260
+ var ReachSection2 = ({ record, units, profile, folds, action }) => {
2261
+ const curve = useMemo7(
2262
+ () => record.status === "ready" ? readReachCurve(record.value?.datasheet) : null,
2263
+ [record]
2264
+ );
2265
+ if (record.status === "ready" && !curve) return null;
2266
+ return /* @__PURE__ */ jsx22(FoldSection, { id: "reach", title: "Reach", folds, action: curve ? action : null, children: record.status === "loading" ? /* @__PURE__ */ jsx22(Reading, { children: "Reading the reach curve\u2026" }) : record.status === "error" ? /* @__PURE__ */ jsx22(ReadFailure, { failure: record }) : curve ? /* @__PURE__ */ jsx22(ReachChart, { curve, units, feature: profile }) : null });
2267
+ };
2268
+
2269
+ // src/feature-panel/candidate-list.tsx
2270
+ import { XIcon } from "@phosphor-icons/react";
2271
+ import { IconButton as IconButton4, cn as cn13 } from "@toolpath/ui";
2272
+ import { jsx as jsx23, jsxs as jsxs20 } from "react/jsx-runtime";
2273
+ var candidateListKeys = listKeys("candidates");
2274
+ var faceLetter = (index) => String.fromCharCode(65 + index % 26);
2275
+ var FaceLetter = ({
2276
+ index,
2277
+ active
2278
+ }) => /* @__PURE__ */ jsx23(
2279
+ "span",
2280
+ {
2281
+ className: cn13(
2282
+ "inline-flex size-4 shrink-0 items-center justify-center rounded-sm text-2xs font-bold",
2283
+ active ? "bg-info text-white" : "bg-gray-100 text-gray-500 dark:bg-zinc-800 dark:text-zinc-300"
2284
+ ),
2285
+ children: faceLetter(index)
2286
+ }
2287
+ );
2288
+ var CandidateList = ({
2289
+ rows,
2290
+ selected,
2291
+ onSelect,
2292
+ onHover,
2293
+ face,
2294
+ look: lookOption,
2295
+ folds
2296
+ }) => {
2297
+ const look = resolveLook(lookOption);
2298
+ const count = `Candidate features (${rows.length})`;
2299
+ return /* @__PURE__ */ jsx23("div", { className: "border-b border-gray-100 p-2 dark:border-zinc-800", children: /* @__PURE__ */ jsx23(
2300
+ FoldSection,
2301
+ {
2302
+ id: face ? `candidates-${faceLetter(face.index)}` : "candidates",
2303
+ title: face ? /* @__PURE__ */ jsxs20("span", { className: "flex items-center gap-1.5", children: [
2304
+ /* @__PURE__ */ jsx23(FaceLetter, { index: face.index, active: face.active }),
2305
+ count
2306
+ ] }) : count,
2307
+ action: face?.onStop ? /* @__PURE__ */ jsx23(
2308
+ IconButton4,
2309
+ {
2310
+ "aria-label": `Stop comparing face ${faceLetter(face.index)}`,
2311
+ variant: "muted",
2312
+ size: "sm",
2313
+ onClick: face.onStop,
2314
+ children: /* @__PURE__ */ jsx23(XIcon, { weight: "bold" })
2315
+ }
2316
+ ) : null,
2317
+ folds,
2318
+ className: "px-2",
2319
+ children: /* @__PURE__ */ jsx23("ul", { className: "-mx-2 flex flex-col gap-0.5", children: rows.map((row) => /* @__PURE__ */ jsx23(
2320
+ Row,
2321
+ {
2322
+ row,
2323
+ chosen: row.feature.tag === selected,
2324
+ onSelect,
2325
+ onHover,
2326
+ look
2327
+ },
2328
+ row.feature.tag
2329
+ )) })
2330
+ }
2331
+ ) });
2332
+ };
2333
+ var Row = ({ row, chosen, onSelect, onHover, look }) => {
2334
+ const { feature } = row;
2335
+ const info = look.selected === "info";
2336
+ return /* @__PURE__ */ jsxs20(
2337
+ "li",
2338
+ {
2339
+ className: cn13(
2340
+ "flex items-center gap-1",
2341
+ info && "rounded border pr-1",
2342
+ info && (chosen ? "border-info/60 bg-info/15" : "border-transparent")
2343
+ ),
2344
+ children: [
2345
+ /* @__PURE__ */ jsxs20(
2346
+ "button",
2347
+ {
2348
+ type: "button",
2349
+ "data-list-item": "candidates",
2350
+ "data-follow": true,
2351
+ "aria-pressed": chosen,
2352
+ onClick: () => onSelect(feature.tag),
2353
+ onPointerEnter: onHover ? () => onHover(feature.tag) : void 0,
2354
+ onPointerLeave: onHover ? () => onHover(null) : void 0,
2355
+ className: cn13(
2356
+ "flex min-w-0 flex-1 cursor-pointer flex-col rounded px-2 py-1.5 text-left text-sm",
2357
+ "outline-none focus-visible:ring-2 focus-visible:ring-info/75",
2358
+ "text-gray-600 dark:text-zinc-200",
2359
+ info ? "hover:bg-gray-50 dark:hover:bg-zinc-800/60" : "hover:bg-gray-100 dark:hover:bg-zinc-800",
2360
+ chosen && (info ? "font-medium" : "bg-gray-100 font-semibold dark:bg-zinc-800")
2361
+ ),
2362
+ children: [
2363
+ /* @__PURE__ */ jsxs20("span", { className: "flex w-full items-center gap-2", children: [
2364
+ row.color ? /* @__PURE__ */ jsx23(Swatch, { color: row.color }) : /* @__PURE__ */ jsx23("span", { "aria-hidden": true, className: "size-4 shrink-0" }),
2365
+ /* @__PURE__ */ jsx23("span", { className: "min-w-0 flex-1 truncate", children: /* @__PURE__ */ jsx23(FeatureName, { feature, label: row.label, look }) }),
2366
+ row.ruleCount ? /* @__PURE__ */ jsxs20("span", { className: cn13("text-2xs tabular-nums", secondaryText(look)), children: [
2367
+ row.ruleCount,
2368
+ " ",
2369
+ row.ruleCount === 1 ? "rule" : "rules"
2370
+ ] }) : null,
2371
+ row.required ? /* @__PURE__ */ jsx23(RequiredBadge, {}) : null,
2372
+ row.detail ? /* @__PURE__ */ jsx23("span", { className: cn13("max-w-32 shrink-0 truncate text-xs", secondaryText(look)), children: row.detail }) : null
2373
+ ] }),
2374
+ row.note ? /* @__PURE__ */ jsx23("span", { className: "pl-6 text-2xs text-warning-darken", children: row.note }) : null
2375
+ ]
2376
+ }
2377
+ ),
2378
+ row.action
2379
+ ]
2380
+ }
2381
+ );
2382
+ };
2383
+
2384
+ // src/feature-panel/comparison-table.tsx
2385
+ import { useMemo as useMemo8 } from "react";
2386
+ import { cn as cn14 } from "@toolpath/ui";
2387
+ import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
2388
+ var DASH = "\u2014";
2389
+ var ComparisonTable = ({
2390
+ faces,
2391
+ active,
2392
+ onSelect,
2393
+ onHover,
2394
+ look: lookOption,
2395
+ folds
2396
+ }) => {
2397
+ const look = resolveLook(lookOption);
2398
+ const rows = useMemo8(() => {
2399
+ const row = (key, label, read) => {
2400
+ const values = faces.map((face) => face.feature ? read(face, face.feature) : null);
2401
+ const texts = values.map((value) => value?.text ?? DASH);
2402
+ return {
2403
+ key,
2404
+ label,
2405
+ cells: values.map((value) => value?.node ?? value?.text ?? DASH),
2406
+ differs: new Set(texts).size > 1
2407
+ };
2408
+ };
2409
+ const fixed = [
2410
+ row("type", "Type", (_, feature) => ({ text: featureTypeLabel(feature.featureType) })),
2411
+ row("direction", "Machined from", (face, feature) => {
2412
+ const text = directionLabel(feature.machiningDirection);
2413
+ return {
2414
+ text,
2415
+ node: /* @__PURE__ */ jsxs21("span", { className: "inline-flex items-center gap-1", children: [
2416
+ /* @__PURE__ */ jsx24(Swatch, { color: face.directionColor, className: "size-2.5" }),
2417
+ text
2418
+ ] })
2419
+ };
2420
+ }),
2421
+ ...faces.some((face) => face.required !== void 0) ? [row("required", "Required", (face) => ({ text: face.required ? "Yes" : "No" }))] : [],
2422
+ row("rules", "Rules broken", (face) => ({
2423
+ text: face.rules.map((rule) => rule.key).join(",") || "None",
2424
+ node: face.rules.length > 0 ? /* @__PURE__ */ jsxs21(
2425
+ "span",
2426
+ {
2427
+ className: "inline-flex items-center gap-0.5",
2428
+ title: face.rules.map((rule) => rule.text).join("\n"),
2429
+ children: [
2430
+ face.rules.map((rule) => /* @__PURE__ */ jsx24(Swatch, { color: rule.color, className: "size-2.5" }, rule.key)),
2431
+ /* @__PURE__ */ jsx24("span", { className: "ml-0.5", children: face.rules.length })
2432
+ ]
2433
+ }
2434
+ ) : "None"
2435
+ }))
2436
+ ];
2437
+ const labels = /* @__PURE__ */ new Map();
2438
+ for (const face of faces)
2439
+ for (const each of face.measurements) labels.set(each.key, labels.get(each.key) ?? each.label);
2440
+ const measured = [...labels].map(([key, label]) => {
2441
+ const values = faces.map((face) => face.measurements.find((each) => each.key === key));
2442
+ return {
2443
+ key,
2444
+ label,
2445
+ cells: values.map((value) => value ? /* @__PURE__ */ jsx24("span", { title: value.alt, children: value.value }) : DASH),
2446
+ differs: new Set(values.map((value) => value?.value ?? DASH)).size > 1
2447
+ };
2448
+ });
2449
+ return [...fixed, ...measured];
2450
+ }, [faces]);
2451
+ return /* @__PURE__ */ jsx24("div", { className: "border-b border-gray-100 p-4 dark:border-zinc-800", children: /* @__PURE__ */ jsx24(FoldSection, { id: "comparison", title: "Comparison", folds, children: /* @__PURE__ */ jsx24("div", { className: "-mx-1 overflow-x-auto", children: /* @__PURE__ */ jsxs21("table", { className: "w-full border-separate border-spacing-x-1 border-spacing-y-0.5 text-sm", children: [
2452
+ /* @__PURE__ */ jsx24("thead", { children: /* @__PURE__ */ jsxs21("tr", { children: [
2453
+ /* @__PURE__ */ jsx24("th", { className: "w-0" }),
2454
+ faces.map(({ feature }, index) => /* @__PURE__ */ jsx24("th", { className: "text-left align-bottom font-normal", children: /* @__PURE__ */ jsxs21(
2455
+ "button",
2456
+ {
2457
+ type: "button",
2458
+ disabled: !feature,
2459
+ onClick: () => feature && onSelect(feature.tag, index),
2460
+ onPointerEnter: onHover ? () => onHover(feature?.tag ?? null) : void 0,
2461
+ onPointerLeave: onHover ? () => onHover(null) : void 0,
2462
+ className: cn14(
2463
+ "flex w-full min-w-0 cursor-pointer items-center gap-1.5 rounded px-1 py-1 text-left text-xs",
2464
+ "outline-none focus-visible:ring-2 focus-visible:ring-info/75",
2465
+ "text-gray-600 hover:bg-gray-100 dark:text-zinc-200 dark:hover:bg-zinc-800",
2466
+ index === active && "bg-gray-100 font-semibold dark:bg-zinc-800"
2467
+ ),
2468
+ children: [
2469
+ /* @__PURE__ */ jsx24(FaceLetter, { index, active: index === active }),
2470
+ /* @__PURE__ */ jsx24("span", { className: "min-w-0 truncate", children: feature ? /* @__PURE__ */ jsx24(FeatureName, { feature, look }) : "No feature" })
2471
+ ]
2472
+ }
2473
+ ) }, index))
2474
+ ] }) }),
2475
+ /* @__PURE__ */ jsx24("tbody", { children: rows.map((row) => /* @__PURE__ */ jsxs21("tr", { "data-differs": row.differs || void 0, children: [
2476
+ /* @__PURE__ */ jsx24(
2477
+ "th",
2478
+ {
2479
+ scope: "row",
2480
+ className: cn14(
2481
+ "pr-2 text-left align-top font-normal whitespace-nowrap",
2482
+ secondaryText(look)
2483
+ ),
2484
+ children: row.label
2485
+ }
2486
+ ),
2487
+ row.cells.map((cell, index) => /* @__PURE__ */ jsx24(
2488
+ "td",
2489
+ {
2490
+ className: cn14(
2491
+ "px-1 align-top tabular-nums whitespace-nowrap",
2492
+ row.differs ? "text-gray-600 dark:text-zinc-100" : "text-gray-400 dark:text-zinc-500"
2493
+ ),
2494
+ children: cell
2495
+ },
2496
+ index
2497
+ ))
2498
+ ] }, row.key)) })
2499
+ ] }) }) }) });
2500
+ };
2501
+
2502
+ // src/feature-panel/feature-panel.tsx
2503
+ import { CubeIcon, XIcon as XIcon2 } from "@phosphor-icons/react";
2504
+ import { IconButton as IconButton5, ScrollArea as ScrollArea2, cn as cn15 } from "@toolpath/ui";
2505
+ import { Fragment as Fragment11, jsx as jsx25, jsxs as jsxs22 } from "react/jsx-runtime";
2506
+ var title = (faces, active, group) => {
2507
+ if (faces.length > 1) return `Comparing ${faces.length} faces`;
2508
+ if (faces[active]?.region !== null && faces[active]?.region !== void 0)
2509
+ return "Features on this face";
2510
+ if (group !== void 0) return `${group} identical holes`;
2511
+ return "Feature";
2512
+ };
2513
+ var FeaturePanel = ({
2514
+ faces,
2515
+ active,
2516
+ group,
2517
+ comparison,
2518
+ onSelect,
2519
+ onDropFace,
2520
+ onHover,
2521
+ onClose,
2522
+ headerProps,
2523
+ className,
2524
+ look: lookOption,
2525
+ folds,
2526
+ children
2527
+ }) => {
2528
+ const look = resolveLook(lookOption);
2529
+ const comparing = faces.length > 1;
2530
+ const face = faces[active];
2531
+ const heading = title(faces, active, group);
2532
+ return /* @__PURE__ */ jsxs22(
2533
+ "section",
2534
+ {
2535
+ "aria-label": heading,
2536
+ "data-list-root": true,
2537
+ onKeyDown: (event) => {
2538
+ if (event.defaultPrevented || !event.currentTarget.contains(event.target)) return;
2539
+ if (event.key === "Escape") onClose();
2540
+ else candidateListKeys(event);
2541
+ },
2542
+ className: cn15(
2543
+ "flex h-full min-h-0 flex-col overflow-hidden rounded bg-white/90 shadow-lg ring-1 ring-gray-200/70 backdrop-blur-md",
2544
+ "dark:bg-zinc-900/90 dark:ring-zinc-800",
2545
+ className
2546
+ ),
2547
+ children: [
2548
+ /* @__PURE__ */ jsxs22(
2549
+ "header",
2550
+ {
2551
+ ...headerProps,
2552
+ className: cn15(
2553
+ "flex items-center gap-2 border-b border-gray-100 py-2 pr-2 pl-4 select-none dark:border-zinc-800",
2554
+ headerProps?.className
2555
+ ),
2556
+ children: [
2557
+ /* @__PURE__ */ jsx25(CubeIcon, { weight: "bold", className: cn15("size-5 shrink-0", secondaryText(look)) }),
2558
+ /* @__PURE__ */ jsx25("h2", { className: "flex-1 text-sm font-semibold text-gray-600 dark:text-zinc-100", children: heading }),
2559
+ /* @__PURE__ */ jsx25(IconButton5, { "aria-label": "Close", variant: "muted", onClick: onClose, children: /* @__PURE__ */ jsx25(XIcon2, { weight: "bold" }) })
2560
+ ]
2561
+ }
2562
+ ),
2563
+ /* @__PURE__ */ jsxs22(ScrollArea2, { className: "min-h-0 flex-1", children: [
2564
+ comparing ? /* @__PURE__ */ jsxs22(Fragment11, { children: [
2565
+ faces.map((each, index) => /* @__PURE__ */ jsx25(
2566
+ CandidateList,
2567
+ {
2568
+ rows: each.rows,
2569
+ selected: each.selected,
2570
+ onSelect: (tag) => onSelect(tag, index),
2571
+ onHover,
2572
+ face: {
2573
+ index,
2574
+ active: index === active,
2575
+ ...onDropFace ? { onStop: () => onDropFace(index) } : {}
2576
+ },
2577
+ look: lookOption,
2578
+ folds
2579
+ },
2580
+ `${each.region ?? "list"}-${index}`
2581
+ )),
2582
+ comparison ? /* @__PURE__ */ jsx25(
2583
+ ComparisonTable,
2584
+ {
2585
+ faces: comparison,
2586
+ active,
2587
+ onSelect,
2588
+ onHover,
2589
+ look: lookOption,
2590
+ folds
2591
+ }
2592
+ ) : null
2593
+ ] }) : face && face.region !== null ? /* @__PURE__ */ jsx25(
2594
+ CandidateList,
2595
+ {
2596
+ rows: face.rows,
2597
+ selected: face.selected,
2598
+ onSelect: (tag) => onSelect(tag, active),
2599
+ onHover,
2600
+ look: lookOption,
2601
+ folds
2602
+ }
2603
+ ) : null,
2604
+ face?.selected ? children : /* @__PURE__ */ jsx25("p", { className: cn15("px-4 py-3 text-sm", secondaryText(look)), children: face && face.rows.length > 0 ? "Choose a feature to read its details." : "No feature owns this face." })
2605
+ ] })
2606
+ ]
2607
+ }
2608
+ );
2609
+ };
1620
2610
  export {
2611
+ CandidateList,
2612
+ ComparisonTable,
2613
+ FeatureDetails,
2614
+ FeaturePanel,
1621
2615
  ReachChart,
1622
2616
  ReachDrawing,
1623
2617
  RuleList,
1624
2618
  Swatch,
2619
+ candidateListKeys,
1625
2620
  isField,
1626
2621
  listKeys,
1627
- ruleListKeys
2622
+ ruleListKeys,
2623
+ storageFolds
1628
2624
  };