@fgv/ts-app-shell 5.1.0-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.
Files changed (180) hide show
  1. package/README.md +26 -0
  2. package/dist/index.browser.js +3 -0
  3. package/dist/index.js +43 -0
  4. package/dist/packlets/ai-assist/index.js +6 -0
  5. package/dist/packlets/ai-assist/useAiAssist.js +219 -0
  6. package/dist/packlets/cascade/CascadeContainer.js +83 -0
  7. package/dist/packlets/cascade/ComparisonView.js +48 -0
  8. package/dist/packlets/cascade/EntityTabLayout.js +104 -0
  9. package/dist/packlets/cascade/MobileCascadeStack.js +63 -0
  10. package/dist/packlets/cascade/index.js +37 -0
  11. package/dist/packlets/cascade/model.js +30 -0
  12. package/dist/packlets/cascade/useCascadeOps.js +206 -0
  13. package/dist/packlets/cascade/useCascadeTransitions.js +58 -0
  14. package/dist/packlets/detail/DetailHelpers.js +103 -0
  15. package/dist/packlets/detail/index.js +6 -0
  16. package/dist/packlets/drop-zone/JsonDropZone.js +112 -0
  17. package/dist/packlets/drop-zone/index.js +6 -0
  18. package/dist/packlets/editing/EditFieldHelpers.js +130 -0
  19. package/dist/packlets/editing/MultiActionButton.js +73 -0
  20. package/dist/packlets/editing/NumericInput.js +119 -0
  21. package/dist/packlets/editing/TypeaheadInput.js +207 -0
  22. package/dist/packlets/editing/index.js +10 -0
  23. package/dist/packlets/editing/useTypeaheadMatch.js +102 -0
  24. package/dist/packlets/keyboard/index.js +7 -0
  25. package/dist/packlets/keyboard/registry.js +133 -0
  26. package/dist/packlets/keyboard/useKeyboardShortcuts.js +117 -0
  27. package/dist/packlets/messages/MessagesContext.js +76 -0
  28. package/dist/packlets/messages/MessagesLogger.js +103 -0
  29. package/dist/packlets/messages/StatusBar.js +154 -0
  30. package/dist/packlets/messages/Toast.js +68 -0
  31. package/dist/packlets/messages/index.js +11 -0
  32. package/dist/packlets/messages/model.js +56 -0
  33. package/dist/packlets/messages/useLogReporter.js +66 -0
  34. package/dist/packlets/modal/ConfirmDialog.js +78 -0
  35. package/dist/packlets/modal/Modal.js +55 -0
  36. package/dist/packlets/modal/index.js +7 -0
  37. package/dist/packlets/print/PrintEnclosure.js +60 -0
  38. package/dist/packlets/print/index.js +7 -0
  39. package/dist/packlets/print/openPrintWindow.js +112 -0
  40. package/dist/packlets/responsive/ResponsiveProvider.js +56 -0
  41. package/dist/packlets/responsive/index.js +7 -0
  42. package/dist/packlets/responsive/useResponsiveLayout.js +118 -0
  43. package/dist/packlets/selectors/EntityRow.js +276 -0
  44. package/dist/packlets/selectors/PreferredSelector.js +251 -0
  45. package/dist/packlets/selectors/index.js +24 -0
  46. package/dist/packlets/sidebar/CollectionSection.js +107 -0
  47. package/dist/packlets/sidebar/EntityList.js +164 -0
  48. package/dist/packlets/sidebar/FilterBar.js +42 -0
  49. package/dist/packlets/sidebar/FilterRow.js +182 -0
  50. package/dist/packlets/sidebar/GroupedEntityList.js +183 -0
  51. package/dist/packlets/sidebar/SearchBar.js +34 -0
  52. package/dist/packlets/sidebar/SidebarLayout.js +62 -0
  53. package/dist/packlets/sidebar/index.js +12 -0
  54. package/dist/packlets/theme/ThemeProvider.js +141 -0
  55. package/dist/packlets/theme/index.js +6 -0
  56. package/dist/packlets/top-bar/ModeSelector.js +46 -0
  57. package/dist/packlets/top-bar/TabBar.js +37 -0
  58. package/dist/packlets/top-bar/index.js +7 -0
  59. package/dist/packlets/url-sync/index.js +6 -0
  60. package/dist/packlets/url-sync/useUrlSync.js +157 -0
  61. package/eslint.config.js +22 -0
  62. package/lib/index.browser.d.ts +2 -0
  63. package/lib/index.browser.js +19 -0
  64. package/lib/index.d.ts +28 -0
  65. package/lib/index.js +59 -0
  66. package/lib/packlets/ai-assist/index.d.ts +6 -0
  67. package/lib/packlets/ai-assist/index.js +11 -0
  68. package/lib/packlets/ai-assist/useAiAssist.d.ts +77 -0
  69. package/lib/packlets/ai-assist/useAiAssist.js +223 -0
  70. package/lib/packlets/cascade/CascadeContainer.d.ts +44 -0
  71. package/lib/packlets/cascade/CascadeContainer.js +119 -0
  72. package/lib/packlets/cascade/ComparisonView.d.ts +35 -0
  73. package/lib/packlets/cascade/ComparisonView.js +54 -0
  74. package/lib/packlets/cascade/EntityTabLayout.d.ts +47 -0
  75. package/lib/packlets/cascade/EntityTabLayout.js +110 -0
  76. package/lib/packlets/cascade/MobileCascadeStack.d.ts +20 -0
  77. package/lib/packlets/cascade/MobileCascadeStack.js +99 -0
  78. package/lib/packlets/cascade/index.d.ts +12 -0
  79. package/lib/packlets/cascade/index.js +48 -0
  80. package/lib/packlets/cascade/model.d.ts +57 -0
  81. package/lib/packlets/cascade/model.js +33 -0
  82. package/lib/packlets/cascade/useCascadeOps.d.ts +111 -0
  83. package/lib/packlets/cascade/useCascadeOps.js +209 -0
  84. package/lib/packlets/cascade/useCascadeTransitions.d.ts +19 -0
  85. package/lib/packlets/cascade/useCascadeTransitions.js +62 -0
  86. package/lib/packlets/detail/DetailHelpers.d.ts +83 -0
  87. package/lib/packlets/detail/DetailHelpers.js +113 -0
  88. package/lib/packlets/detail/index.d.ts +6 -0
  89. package/lib/packlets/detail/index.js +14 -0
  90. package/lib/packlets/drop-zone/JsonDropZone.d.ts +40 -0
  91. package/lib/packlets/drop-zone/JsonDropZone.js +149 -0
  92. package/lib/packlets/drop-zone/index.d.ts +6 -0
  93. package/lib/packlets/drop-zone/index.js +10 -0
  94. package/lib/packlets/editing/EditFieldHelpers.d.ts +171 -0
  95. package/lib/packlets/editing/EditFieldHelpers.js +144 -0
  96. package/lib/packlets/editing/MultiActionButton.d.ts +45 -0
  97. package/lib/packlets/editing/MultiActionButton.js +109 -0
  98. package/lib/packlets/editing/NumericInput.d.ts +47 -0
  99. package/lib/packlets/editing/NumericInput.js +155 -0
  100. package/lib/packlets/editing/TypeaheadInput.d.ts +46 -0
  101. package/lib/packlets/editing/TypeaheadInput.js +243 -0
  102. package/lib/packlets/editing/index.d.ts +10 -0
  103. package/lib/packlets/editing/index.js +26 -0
  104. package/lib/packlets/editing/useTypeaheadMatch.d.ts +42 -0
  105. package/lib/packlets/editing/useTypeaheadMatch.js +105 -0
  106. package/lib/packlets/keyboard/index.d.ts +7 -0
  107. package/lib/packlets/keyboard/index.js +15 -0
  108. package/lib/packlets/keyboard/registry.d.ts +92 -0
  109. package/lib/packlets/keyboard/registry.js +138 -0
  110. package/lib/packlets/keyboard/useKeyboardShortcuts.d.ts +50 -0
  111. package/lib/packlets/keyboard/useKeyboardShortcuts.js +155 -0
  112. package/lib/packlets/messages/MessagesContext.d.ts +40 -0
  113. package/lib/packlets/messages/MessagesContext.js +113 -0
  114. package/lib/packlets/messages/MessagesLogger.d.ts +50 -0
  115. package/lib/packlets/messages/MessagesLogger.js +107 -0
  116. package/lib/packlets/messages/StatusBar.d.ts +22 -0
  117. package/lib/packlets/messages/StatusBar.js +190 -0
  118. package/lib/packlets/messages/Toast.d.ts +31 -0
  119. package/lib/packlets/messages/Toast.js +105 -0
  120. package/lib/packlets/messages/index.d.ts +11 -0
  121. package/lib/packlets/messages/index.js +24 -0
  122. package/lib/packlets/messages/model.d.ts +59 -0
  123. package/lib/packlets/messages/model.js +61 -0
  124. package/lib/packlets/messages/useLogReporter.d.ts +22 -0
  125. package/lib/packlets/messages/useLogReporter.js +69 -0
  126. package/lib/packlets/modal/ConfirmDialog.d.ts +39 -0
  127. package/lib/packlets/modal/ConfirmDialog.js +114 -0
  128. package/lib/packlets/modal/Modal.d.ts +22 -0
  129. package/lib/packlets/modal/Modal.js +91 -0
  130. package/lib/packlets/modal/index.d.ts +7 -0
  131. package/lib/packlets/modal/index.js +12 -0
  132. package/lib/packlets/print/PrintEnclosure.d.ts +33 -0
  133. package/lib/packlets/print/PrintEnclosure.js +96 -0
  134. package/lib/packlets/print/index.d.ts +7 -0
  135. package/lib/packlets/print/index.js +12 -0
  136. package/lib/packlets/print/openPrintWindow.d.ts +35 -0
  137. package/lib/packlets/print/openPrintWindow.js +118 -0
  138. package/lib/packlets/responsive/ResponsiveProvider.d.ts +35 -0
  139. package/lib/packlets/responsive/ResponsiveProvider.js +93 -0
  140. package/lib/packlets/responsive/index.d.ts +7 -0
  141. package/lib/packlets/responsive/index.js +13 -0
  142. package/lib/packlets/responsive/useResponsiveLayout.d.ts +48 -0
  143. package/lib/packlets/responsive/useResponsiveLayout.js +121 -0
  144. package/lib/packlets/selectors/EntityRow.d.ts +45 -0
  145. package/lib/packlets/selectors/EntityRow.js +315 -0
  146. package/lib/packlets/selectors/PreferredSelector.d.ts +50 -0
  147. package/lib/packlets/selectors/PreferredSelector.js +287 -0
  148. package/lib/packlets/selectors/index.d.ts +5 -0
  149. package/lib/packlets/selectors/index.js +29 -0
  150. package/lib/packlets/sidebar/CollectionSection.d.ts +82 -0
  151. package/lib/packlets/sidebar/CollectionSection.js +143 -0
  152. package/lib/packlets/sidebar/EntityList.d.ts +105 -0
  153. package/lib/packlets/sidebar/EntityList.js +200 -0
  154. package/lib/packlets/sidebar/FilterBar.d.ts +26 -0
  155. package/lib/packlets/sidebar/FilterBar.js +48 -0
  156. package/lib/packlets/sidebar/FilterRow.d.ts +42 -0
  157. package/lib/packlets/sidebar/FilterRow.js +218 -0
  158. package/lib/packlets/sidebar/GroupedEntityList.d.ts +59 -0
  159. package/lib/packlets/sidebar/GroupedEntityList.js +219 -0
  160. package/lib/packlets/sidebar/SearchBar.d.ts +19 -0
  161. package/lib/packlets/sidebar/SearchBar.js +40 -0
  162. package/lib/packlets/sidebar/SidebarLayout.d.ts +28 -0
  163. package/lib/packlets/sidebar/SidebarLayout.js +98 -0
  164. package/lib/packlets/sidebar/index.d.ts +12 -0
  165. package/lib/packlets/sidebar/index.js +22 -0
  166. package/lib/packlets/theme/ThemeProvider.d.ts +68 -0
  167. package/lib/packlets/theme/ThemeProvider.js +178 -0
  168. package/lib/packlets/theme/index.d.ts +6 -0
  169. package/lib/packlets/theme/index.js +11 -0
  170. package/lib/packlets/top-bar/ModeSelector.d.ts +38 -0
  171. package/lib/packlets/top-bar/ModeSelector.js +52 -0
  172. package/lib/packlets/top-bar/TabBar.d.ts +31 -0
  173. package/lib/packlets/top-bar/TabBar.js +43 -0
  174. package/lib/packlets/top-bar/index.d.ts +7 -0
  175. package/lib/packlets/top-bar/index.js +12 -0
  176. package/lib/packlets/url-sync/index.d.ts +6 -0
  177. package/lib/packlets/url-sync/index.js +12 -0
  178. package/lib/packlets/url-sync/useUrlSync.d.ts +75 -0
  179. package/lib/packlets/url-sync/useUrlSync.js +162 -0
  180. package/package.json +82 -0
@@ -0,0 +1,276 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ /**
23
+ * EntityRow — clickable text row with optional discrete swap-icon for alternates.
24
+ *
25
+ * Renders a uniform row regardless of whether alternates exist:
26
+ * - Left slot: swap icon (when alternates exist) or empty spacer
27
+ * - Name text: clickable for drill-down
28
+ * - Optional right-side content (amount, sublabel, etc.)
29
+ * - Drill-down chevron ›
30
+ *
31
+ * @packageDocumentation
32
+ */
33
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
34
+ import ReactDOM from 'react-dom';
35
+ // ============================================================================
36
+ // EntityRow Component
37
+ // ============================================================================
38
+ /**
39
+ * A clickable text row with an optional discrete swap-icon popover for
40
+ * switching between alternates. Clicking the row itself always triggers
41
+ * drill-down navigation. The swap icon opens a small picker to change
42
+ * which alternate is displayed.
43
+ *
44
+ * @public
45
+ */
46
+ export function EntityRow(props) {
47
+ var _a, _b, _c, _d;
48
+ const { items, preferredId, onClick, onSelect, onCompare, rightContent, label } = props;
49
+ const hasAlternates = items.length > 1;
50
+ const [internalId, setInternalId] = useState((_b = preferredId !== null && preferredId !== void 0 ? preferredId : (_a = items[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : '');
51
+ const displayedId = (_c = props.selectedId) !== null && _c !== void 0 ? _c : internalId;
52
+ const [pickerOpen, setPickerOpen] = useState(false);
53
+ const [compareMode, setCompareMode] = useState(false);
54
+ const [checkedIds, setCheckedIds] = useState(new Set());
55
+ const [focusIndex, setFocusIndex] = useState(-1);
56
+ const pickerRef = useRef(null);
57
+ const pickerBtnRef = useRef(null);
58
+ const itemRefs = useRef([]);
59
+ const [pickerPos, setPickerPos] = useState({ top: 0, left: 0 });
60
+ // Reset displayed item when items or preferred change (e.g. variation switch)
61
+ useEffect(() => {
62
+ var _a, _b;
63
+ setInternalId((_b = preferredId !== null && preferredId !== void 0 ? preferredId : (_a = items[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : '');
64
+ }, [preferredId, items]);
65
+ // Reset compare mode when picker closes; set initial focus when it opens
66
+ useEffect(() => {
67
+ if (!pickerOpen) {
68
+ setCompareMode(false);
69
+ setCheckedIds(new Set());
70
+ setFocusIndex(-1);
71
+ }
72
+ else {
73
+ const idx = items.findIndex((i) => i.id === displayedId);
74
+ setFocusIndex(idx >= 0 ? idx : 0);
75
+ }
76
+ }, [pickerOpen, items, displayedId]);
77
+ const displayedItem = useMemo(() => { var _a; return (_a = items.find((i) => i.id === displayedId)) !== null && _a !== void 0 ? _a : items[0]; }, [items, displayedId]);
78
+ const isPreferred = displayedId === preferredId;
79
+ // Position the portal popover relative to the swap button
80
+ useEffect(() => {
81
+ if (pickerOpen && pickerBtnRef.current) {
82
+ const rect = pickerBtnRef.current.getBoundingClientRect();
83
+ setPickerPos({ top: rect.bottom + 2, left: rect.left });
84
+ }
85
+ }, [pickerOpen]);
86
+ // Close picker on outside click or scroll
87
+ useEffect(() => {
88
+ if (!pickerOpen) {
89
+ return;
90
+ }
91
+ const handleClickOutside = (e) => {
92
+ if (pickerRef.current &&
93
+ !pickerRef.current.contains(e.target) &&
94
+ pickerBtnRef.current &&
95
+ !pickerBtnRef.current.contains(e.target)) {
96
+ setPickerOpen(false);
97
+ }
98
+ };
99
+ const handleScroll = () => {
100
+ setPickerOpen(false);
101
+ };
102
+ document.addEventListener('mousedown', handleClickOutside);
103
+ // Close on any scroll so the popover doesn't float away from the button
104
+ document.addEventListener('scroll', handleScroll, true);
105
+ return () => {
106
+ document.removeEventListener('mousedown', handleClickOutside);
107
+ document.removeEventListener('scroll', handleScroll, true);
108
+ };
109
+ }, [pickerOpen]);
110
+ const handleRowClick = useCallback(() => {
111
+ if (onClick) {
112
+ onClick(displayedId);
113
+ }
114
+ }, [onClick, displayedId]);
115
+ const handleRowKeyDown = useCallback((e) => {
116
+ if (onClick && (e.key === 'Enter' || e.key === ' ')) {
117
+ e.preventDefault();
118
+ onClick(displayedId);
119
+ }
120
+ }, [onClick, displayedId]);
121
+ const handlePickerToggle = useCallback((e) => {
122
+ e.stopPropagation();
123
+ setPickerOpen((prev) => !prev);
124
+ }, []);
125
+ const handlePickItem = useCallback((id) => {
126
+ setInternalId(id);
127
+ if (onSelect) {
128
+ onSelect(id);
129
+ }
130
+ setPickerOpen(false);
131
+ }, [onSelect]);
132
+ const handleCompareAll = useCallback(() => {
133
+ if (onCompare) {
134
+ onCompare(items.map((i) => i.id));
135
+ }
136
+ setPickerOpen(false);
137
+ }, [onCompare, items]);
138
+ const toggleChecked = useCallback((id) => {
139
+ setCheckedIds((prev) => {
140
+ const next = new Set(prev);
141
+ if (next.has(id)) {
142
+ next.delete(id);
143
+ }
144
+ else if (next.size < 4) {
145
+ next.add(id);
146
+ }
147
+ return next;
148
+ });
149
+ }, []);
150
+ const handleCompareSelected = useCallback(() => {
151
+ if (onCompare && checkedIds.size >= 2) {
152
+ onCompare(Array.from(checkedIds));
153
+ setPickerOpen(false);
154
+ }
155
+ }, [onCompare, checkedIds]);
156
+ const handlePickerKeyDown = useCallback((e) => {
157
+ switch (e.key) {
158
+ case 'Escape': {
159
+ e.preventDefault();
160
+ e.stopPropagation();
161
+ if (compareMode) {
162
+ setCompareMode(false);
163
+ setCheckedIds(new Set());
164
+ }
165
+ else {
166
+ setPickerOpen(false);
167
+ }
168
+ break;
169
+ }
170
+ case 'ArrowDown': {
171
+ e.preventDefault();
172
+ setFocusIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
173
+ break;
174
+ }
175
+ case 'ArrowUp': {
176
+ e.preventDefault();
177
+ setFocusIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
178
+ break;
179
+ }
180
+ case 'Enter': {
181
+ e.preventDefault();
182
+ if (focusIndex >= 0 && focusIndex < items.length) {
183
+ if (compareMode) {
184
+ toggleChecked(items[focusIndex].id);
185
+ }
186
+ else {
187
+ handlePickItem(items[focusIndex].id);
188
+ }
189
+ }
190
+ break;
191
+ }
192
+ case ' ': {
193
+ if (compareMode && focusIndex >= 0 && focusIndex < items.length) {
194
+ e.preventDefault();
195
+ toggleChecked(items[focusIndex].id);
196
+ }
197
+ break;
198
+ }
199
+ default:
200
+ break;
201
+ }
202
+ }, [items, focusIndex, compareMode, toggleChecked, handlePickItem]);
203
+ // Render the picker popover via portal so it escapes ancestor overflow clipping
204
+ const pickerPopover = pickerOpen
205
+ ? ReactDOM.createPortal(React.createElement("div", { ref: pickerRef, style: { position: 'fixed', top: pickerPos.top, left: pickerPos.left }, className: "z-50 min-w-[180px] max-h-[200px] bg-surface border border-border rounded-lg shadow-lg flex flex-col overflow-hidden", onKeyDown: handlePickerKeyDown },
206
+ React.createElement("div", { className: "flex items-center justify-between px-2.5 py-1 border-b border-border-subtle shrink-0" }, onCompare && items.length >= 2 ? (compareMode ? (React.createElement(React.Fragment, null,
207
+ React.createElement("div", { className: "flex items-center gap-1.5" },
208
+ checkedIds.size >= 2 && (React.createElement("button", { onClick: (e) => {
209
+ e.stopPropagation();
210
+ handleCompareSelected();
211
+ }, className: "text-[10px] text-white bg-brand-primary rounded px-1.5 py-0.5 hover:bg-brand-primary/90 transition-colors font-medium" },
212
+ "Compare ",
213
+ checkedIds.size)),
214
+ React.createElement("button", { onClick: (e) => {
215
+ e.stopPropagation();
216
+ handleCompareAll();
217
+ }, className: "text-[10px] text-brand-accent hover:text-brand-primary transition-colors font-medium" }, "All")),
218
+ React.createElement("button", { onClick: (e) => {
219
+ e.stopPropagation();
220
+ setCompareMode(false);
221
+ setCheckedIds(new Set());
222
+ }, className: "text-muted hover:text-secondary transition-colors", "aria-label": "Exit compare" },
223
+ React.createElement("svg", { className: "w-3.5 h-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 },
224
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }))))) : (React.createElement(React.Fragment, null,
225
+ React.createElement("span", { className: "text-[10px] font-medium text-muted uppercase tracking-wider" }, "Options"),
226
+ React.createElement("button", { onClick: (e) => {
227
+ e.stopPropagation();
228
+ setCompareMode(true);
229
+ }, className: "text-[10px] text-brand-accent hover:text-brand-primary transition-colors font-medium" }, "Compare\u2026")))) : (React.createElement("span", { className: "text-[10px] font-medium text-muted uppercase tracking-wider" }, "Alternates"))),
230
+ React.createElement("div", { className: "overflow-y-auto flex-1" }, items.map((item, index) => {
231
+ const isCurrent = item.id === displayedId;
232
+ const isPref = item.id === preferredId;
233
+ const isFocused = index === focusIndex;
234
+ const isChecked = compareMode && checkedIds.has(item.id);
235
+ return (React.createElement("button", { key: item.id, ref: (el) => {
236
+ itemRefs.current[index] = el;
237
+ }, onClick: (e) => {
238
+ e.stopPropagation();
239
+ if (compareMode) {
240
+ toggleChecked(item.id);
241
+ }
242
+ else {
243
+ handlePickItem(item.id);
244
+ }
245
+ }, className: `flex items-center gap-2 w-full px-2.5 py-1.5 text-left text-sm transition-colors ${isChecked
246
+ ? 'bg-selected text-brand-primary'
247
+ : isCurrent && !compareMode
248
+ ? 'bg-selected text-brand-primary font-medium'
249
+ : isFocused
250
+ ? 'bg-surface-alt text-primary'
251
+ : 'text-secondary hover:bg-hover'}` },
252
+ compareMode ? (React.createElement("span", { className: `flex items-center justify-center w-3.5 h-3.5 rounded border shrink-0 transition-colors ${isChecked
253
+ ? 'bg-brand-accent border-brand-accent text-white'
254
+ : 'border-border bg-surface'}` }, isChecked && (React.createElement("svg", { className: "w-2.5 h-2.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3 },
255
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }))))) : (React.createElement("span", { className: "w-3 shrink-0 text-center text-xs" }, isPref ? React.createElement("span", { className: "text-star" }, "\u2605") : '')),
256
+ React.createElement("span", { className: "flex-1 min-w-0 truncate" },
257
+ item.label,
258
+ item.sublabel && React.createElement("span", { className: "ml-1.5 text-xs text-muted" }, item.sublabel)),
259
+ !compareMode && isCurrent && (React.createElement("svg", { className: "w-3.5 h-3.5 text-brand-accent shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3 },
260
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" })))));
261
+ }))), document.body)
262
+ : null;
263
+ return (React.createElement("div", null,
264
+ label && React.createElement("div", { className: "text-xs font-semibold text-muted uppercase tracking-wider mb-1" }, label),
265
+ React.createElement("div", { className: `flex items-center gap-1.5 py-1.5 pl-0 pr-2 rounded-md ${onClick ? 'cursor-pointer hover:bg-hover transition-colors' : ''}`, onClick: handleRowClick, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: handleRowKeyDown },
266
+ React.createElement("span", { className: "w-4 shrink-0 flex items-center justify-center" }, hasAlternates ? (React.createElement("button", { ref: pickerBtnRef, onClick: handlePickerToggle, className: "text-muted hover:text-brand-accent p-0 transition-colors", "aria-label": "Switch alternate", tabIndex: -1 },
267
+ React.createElement("svg", { className: "w-3.5 h-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 },
268
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" })))) : null),
269
+ React.createElement("span", { className: "text-sm text-primary flex-1 truncate" }, (_d = displayedItem === null || displayedItem === void 0 ? void 0 : displayedItem.label) !== null && _d !== void 0 ? _d : '',
270
+ hasAlternates && isPreferred && React.createElement("span", { className: "ml-1 text-xs text-star" }, "\u2605"),
271
+ (displayedItem === null || displayedItem === void 0 ? void 0 : displayedItem.sublabel) && (React.createElement("span", { className: "ml-1.5 text-xs text-muted" }, displayedItem.sublabel))),
272
+ rightContent,
273
+ onClick && React.createElement("span", { className: "text-faint text-xs shrink-0" }, "\u203A")),
274
+ pickerPopover));
275
+ }
276
+ //# sourceMappingURL=EntityRow.js.map
@@ -0,0 +1,251 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ /**
23
+ * PreferredSelector — compact popover for choosing one item from a set of options.
24
+ * @packageDocumentation
25
+ */
26
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
27
+ // ============================================================================
28
+ // PreferredSelector Component
29
+ // ============================================================================
30
+ /**
31
+ * Compact inline selector with popover for choosing one item from a set.
32
+ *
33
+ * When there is only one item, renders as a static label.
34
+ * When there are multiple items, renders a clickable trigger that opens
35
+ * a popover with all options. If `onCompare` is provided, the popover
36
+ * includes a compare toggle that enables checkboxes for multi-select,
37
+ * with "Compare All" and "Compare Selected" actions.
38
+ *
39
+ * @public
40
+ */
41
+ export function PreferredSelector(props) {
42
+ var _a;
43
+ const { items, selectedId, preferredId, onSelect, onCompare, onDrillDown, label } = props;
44
+ const [open, setOpen] = useState(false);
45
+ const [focusIndex, setFocusIndex] = useState(-1);
46
+ const [compareMode, setCompareMode] = useState(false);
47
+ const [checkedIds, setCheckedIds] = useState(new Set());
48
+ const triggerRef = useRef(null);
49
+ const popoverRef = useRef(null);
50
+ const itemRefs = useRef([]);
51
+ const selectedItem = items.find((item) => item.id === selectedId);
52
+ const hasMultiple = items.length > 1;
53
+ const canCompare = onCompare !== undefined && items.length >= 2;
54
+ // Reset compare mode when popover closes
55
+ useEffect(() => {
56
+ if (!open) {
57
+ setCompareMode(false);
58
+ setCheckedIds(new Set());
59
+ }
60
+ }, [open]);
61
+ // Close popover when clicking outside
62
+ useEffect(() => {
63
+ if (!open) {
64
+ return;
65
+ }
66
+ const handleClickOutside = (e) => {
67
+ if (popoverRef.current &&
68
+ !popoverRef.current.contains(e.target) &&
69
+ triggerRef.current &&
70
+ !triggerRef.current.contains(e.target)) {
71
+ setOpen(false);
72
+ }
73
+ };
74
+ document.addEventListener('mousedown', handleClickOutside);
75
+ return () => {
76
+ document.removeEventListener('mousedown', handleClickOutside);
77
+ };
78
+ }, [open]);
79
+ // Focus first item when popover opens
80
+ useEffect(() => {
81
+ if (open) {
82
+ const selectedIdx = items.findIndex((item) => item.id === selectedId);
83
+ setFocusIndex(selectedIdx >= 0 ? selectedIdx : 0);
84
+ }
85
+ }, [open, items, selectedId]);
86
+ // Scroll focused item into view
87
+ useEffect(() => {
88
+ var _a;
89
+ if (open && focusIndex >= 0 && itemRefs.current[focusIndex]) {
90
+ (_a = itemRefs.current[focusIndex]) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: 'nearest' });
91
+ }
92
+ }, [open, focusIndex]);
93
+ const handleToggle = useCallback(() => {
94
+ if (hasMultiple) {
95
+ setOpen((prev) => !prev);
96
+ }
97
+ }, [hasMultiple]);
98
+ const handleSelect = useCallback((id) => {
99
+ onSelect(id);
100
+ setOpen(false);
101
+ }, [onSelect]);
102
+ const handleDrillDown = useCallback((id, e) => {
103
+ e.stopPropagation();
104
+ onDrillDown === null || onDrillDown === void 0 ? void 0 : onDrillDown(id);
105
+ setOpen(false);
106
+ }, [onDrillDown]);
107
+ const toggleChecked = useCallback((id) => {
108
+ setCheckedIds((prev) => {
109
+ const next = new Set(prev);
110
+ if (next.has(id)) {
111
+ next.delete(id);
112
+ }
113
+ else if (next.size < 4) {
114
+ next.add(id);
115
+ }
116
+ return next;
117
+ });
118
+ }, []);
119
+ const handleCompareAll = useCallback(() => {
120
+ if (onCompare) {
121
+ onCompare(items.map((item) => item.id));
122
+ setOpen(false);
123
+ }
124
+ }, [onCompare, items]);
125
+ const handleCompareSelected = useCallback(() => {
126
+ if (onCompare && checkedIds.size >= 2) {
127
+ onCompare(Array.from(checkedIds));
128
+ setOpen(false);
129
+ }
130
+ }, [onCompare, checkedIds]);
131
+ const handleKeyDown = useCallback((e) => {
132
+ var _a;
133
+ if (!open) {
134
+ if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
135
+ e.preventDefault();
136
+ setOpen(true);
137
+ }
138
+ return;
139
+ }
140
+ switch (e.key) {
141
+ case 'Escape': {
142
+ e.preventDefault();
143
+ e.stopPropagation();
144
+ if (compareMode) {
145
+ setCompareMode(false);
146
+ setCheckedIds(new Set());
147
+ }
148
+ else {
149
+ setOpen(false);
150
+ (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
151
+ }
152
+ break;
153
+ }
154
+ case 'ArrowDown': {
155
+ e.preventDefault();
156
+ setFocusIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
157
+ break;
158
+ }
159
+ case 'ArrowUp': {
160
+ e.preventDefault();
161
+ setFocusIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
162
+ break;
163
+ }
164
+ case 'Enter': {
165
+ e.preventDefault();
166
+ if (focusIndex >= 0 && focusIndex < items.length) {
167
+ if (compareMode) {
168
+ toggleChecked(items[focusIndex].id);
169
+ }
170
+ else {
171
+ handleSelect(items[focusIndex].id);
172
+ }
173
+ }
174
+ break;
175
+ }
176
+ case ' ': {
177
+ if (compareMode && focusIndex >= 0 && focusIndex < items.length) {
178
+ e.preventDefault();
179
+ toggleChecked(items[focusIndex].id);
180
+ }
181
+ break;
182
+ }
183
+ default:
184
+ break;
185
+ }
186
+ }, [open, items, focusIndex, handleSelect, compareMode, toggleChecked]);
187
+ return (React.createElement("div", { className: "relative" },
188
+ label && React.createElement("div", { className: "text-xs font-medium text-muted mb-1" }, label),
189
+ React.createElement("button", { ref: triggerRef, onClick: handleToggle, onKeyDown: handleKeyDown, className: `flex items-center gap-1.5 px-2 py-1 text-sm rounded-md border transition-colors w-full text-left ${hasMultiple
190
+ ? 'border-border hover:border-brand-primary cursor-pointer bg-surface'
191
+ : 'border-transparent cursor-default bg-transparent'} ${open ? 'border-brand-primary ring-1 ring-focus-ring/20' : ''}`, "aria-expanded": open, "aria-haspopup": "listbox" },
192
+ React.createElement("span", { className: "flex-1 min-w-0 truncate text-primary" }, (_a = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.label) !== null && _a !== void 0 ? _a : selectedId,
193
+ preferredId !== undefined && selectedId === preferredId && (React.createElement("span", { className: "ml-1 text-xs text-star" }, "\u2605"))),
194
+ hasMultiple && (React.createElement(React.Fragment, null,
195
+ React.createElement("span", { className: "text-[10px] text-muted shrink-0 tabular-nums" }, items.length),
196
+ React.createElement("svg", { className: `w-3 h-3 text-muted shrink-0 transition-transform ${open ? 'rotate-180' : ''}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 },
197
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }))))),
198
+ open && (React.createElement("div", { ref: popoverRef, className: "absolute z-50 mt-1 w-full min-w-[200px] max-h-[280px] bg-surface border border-border rounded-lg shadow-lg flex flex-col overflow-hidden", role: "listbox", onKeyDown: handleKeyDown },
199
+ canCompare && (React.createElement("div", { className: "flex items-center justify-between px-2.5 py-1.5 border-b border-border-subtle shrink-0" },
200
+ compareMode ? (React.createElement("div", { className: "flex items-center gap-1.5" },
201
+ checkedIds.size >= 2 && (React.createElement("button", { onClick: handleCompareSelected, className: "text-xs text-white bg-brand-primary rounded px-1.5 py-0.5 hover:bg-brand-primary/90 transition-colors font-medium" },
202
+ "Compare ",
203
+ checkedIds.size)),
204
+ React.createElement("button", { onClick: handleCompareAll, className: "text-xs text-brand-accent hover:text-brand-primary transition-colors font-medium" }, "All"))) : (React.createElement("button", { onClick: () => setCompareMode(true), className: "text-xs text-brand-accent hover:text-brand-primary transition-colors font-medium" }, "Compare\u2026")),
205
+ React.createElement("button", { onClick: () => {
206
+ if (compareMode) {
207
+ setCompareMode(false);
208
+ setCheckedIds(new Set());
209
+ }
210
+ else {
211
+ setOpen(false);
212
+ }
213
+ }, className: "text-muted hover:text-secondary transition-colors", "aria-label": compareMode ? 'Exit compare' : 'Close' },
214
+ React.createElement("svg", { className: "w-3.5 h-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 },
215
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }))))),
216
+ React.createElement("div", { className: "overflow-y-auto flex-1" }, items.map((item, index) => {
217
+ const isSelected = item.id === selectedId;
218
+ const isPreferred = preferredId !== undefined && item.id === preferredId;
219
+ const isFocused = index === focusIndex;
220
+ const isChecked = compareMode && checkedIds.has(item.id);
221
+ return (React.createElement("button", { key: item.id, ref: (el) => {
222
+ itemRefs.current[index] = el;
223
+ }, role: "option", "aria-selected": isSelected, onClick: () => {
224
+ if (compareMode) {
225
+ toggleChecked(item.id);
226
+ }
227
+ else {
228
+ handleSelect(item.id);
229
+ }
230
+ }, className: `flex items-center gap-2 w-full px-2.5 py-1.5 text-left text-sm transition-colors ${isChecked
231
+ ? 'bg-selected text-brand-primary'
232
+ : isSelected && !compareMode
233
+ ? 'bg-selected text-brand-primary font-medium'
234
+ : isFocused
235
+ ? 'bg-surface-alt text-primary'
236
+ : 'text-secondary hover:bg-hover'}` },
237
+ compareMode ? (React.createElement("span", { className: `flex items-center justify-center w-3.5 h-3.5 rounded border shrink-0 transition-colors ${isChecked
238
+ ? 'bg-brand-accent border-brand-accent text-white'
239
+ : 'border-border bg-surface'}` }, isChecked && (React.createElement("svg", { className: "w-2.5 h-2.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3 },
240
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }))))) : (React.createElement("span", { className: "w-3 shrink-0 text-center text-xs" }, isPreferred ? React.createElement("span", { className: "text-star" }, "\u2605") : '')),
241
+ React.createElement("span", { className: "flex-1 min-w-0" },
242
+ React.createElement("span", { className: "truncate block" }, item.label),
243
+ item.sublabel && (React.createElement("span", { className: "text-xs text-muted truncate block" }, item.sublabel))),
244
+ !compareMode && isSelected && (React.createElement("svg", { className: "w-3.5 h-3.5 text-brand-accent shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3 },
245
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }))),
246
+ !compareMode && onDrillDown && (React.createElement("button", { onClick: (e) => handleDrillDown(item.id, e), className: "text-faint hover:text-brand-accent shrink-0 p-0.5 -mr-1", "aria-label": `Open ${item.label}` },
247
+ React.createElement("svg", { className: "w-3 h-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 },
248
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }))))));
249
+ }))))));
250
+ }
251
+ //# sourceMappingURL=PreferredSelector.js.map
@@ -0,0 +1,24 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ export { EntityRow } from './EntityRow';
23
+ export { PreferredSelector } from './PreferredSelector';
24
+ //# sourceMappingURL=index.js.map