@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,183 @@
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
+ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
23
+ function GroupedEntityRowInner(props) {
24
+ var _a, _b;
25
+ const { entity, descriptor, selectedId, compareMode, checkedIds, onSelect, onDrill, onCheckedChange, onDelete, canDelete } = props;
26
+ const id = descriptor.getId(entity);
27
+ const label = descriptor.getLabel(entity);
28
+ const sublabel = (_a = descriptor.getSublabel) === null || _a === void 0 ? void 0 : _a.call(descriptor, entity);
29
+ const status = (_b = descriptor.getStatus) === null || _b === void 0 ? void 0 : _b.call(descriptor, entity);
30
+ const isSelected = id === selectedId;
31
+ const isChecked = compareMode === true && checkedIds !== undefined && checkedIds.has(id);
32
+ return (React.createElement("div", { className: `group flex items-center gap-2 w-full border-b border-border-subtle transition-colors ${isChecked
33
+ ? 'bg-selected border-l-2 border-l-selected-border'
34
+ : isSelected && !compareMode
35
+ ? 'bg-selected border-l-2 border-l-selected-border'
36
+ : 'hover:bg-hover border-l-2 border-l-transparent'}` },
37
+ React.createElement("button", { "data-entity-id": id, onClick: () => {
38
+ if (compareMode && onCheckedChange) {
39
+ onCheckedChange(id);
40
+ }
41
+ else if (isSelected && onDrill) {
42
+ onDrill();
43
+ }
44
+ else {
45
+ onSelect(id);
46
+ }
47
+ }, className: "flex items-center gap-2 flex-1 min-w-0 px-3 py-2 text-left" },
48
+ compareMode && (React.createElement("span", { className: `flex items-center justify-center w-4 h-4 rounded border shrink-0 transition-colors ${isChecked ? 'bg-brand-accent border-brand-accent text-white' : 'border-border bg-surface'}` }, isChecked && (React.createElement("svg", { className: "w-3 h-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3 },
49
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }))))),
50
+ React.createElement("div", { className: "flex-1 min-w-0" },
51
+ React.createElement("div", { className: `text-sm truncate ${(isSelected && !compareMode) || isChecked ? 'font-medium text-brand-primary' : 'text-primary'}` }, label),
52
+ sublabel && React.createElement("div", { className: "text-xs text-muted truncate mt-0.5" }, sublabel)),
53
+ status && (React.createElement("span", { className: "flex items-center gap-1 shrink-0 mt-0.5" },
54
+ React.createElement("span", { className: `w-2 h-2 rounded-full ${status.colorClass}` }),
55
+ React.createElement("span", { className: "text-xs text-muted" }, status.label)))),
56
+ onDelete && !compareMode && (!canDelete || canDelete(id)) && (React.createElement("button", { onClick: (e) => {
57
+ e.stopPropagation();
58
+ onDelete(id);
59
+ }, className: "shrink-0 mr-1 w-6 h-6 flex items-center justify-center text-faint hover:text-status-error-icon opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity rounded", title: `Delete ${label}`, "aria-label": `Delete ${label}` },
60
+ React.createElement("svg", { className: "w-3.5 h-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 },
61
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" }))))));
62
+ }
63
+ const GroupedEntityRow = React.memo(GroupedEntityRowInner);
64
+ // ============================================================================
65
+ // GroupedEntityList Component
66
+ // ============================================================================
67
+ /**
68
+ * Entity list with sticky group headers.
69
+ *
70
+ * Groups entities by a key extracted via the descriptor, renders a sticky
71
+ * header per group, and delegates item rendering to the same visual pattern
72
+ * as {@link EntityList}.
73
+ *
74
+ * @public
75
+ */
76
+ export function GroupedEntityList(props) {
77
+ const { entities, descriptor, selectedId, onSelect, onDrill, compareMode, checkedIds, onCheckedChange, emptyState, onDelete, canDelete } = props;
78
+ const listRef = useRef(null);
79
+ // Build groups and flat entity list for keyboard navigation
80
+ const { groups, flatEntities } = useMemo(() => {
81
+ const groupMap = new Map();
82
+ for (const entity of entities) {
83
+ const key = descriptor.getGroupKey(entity);
84
+ let group = groupMap.get(key);
85
+ if (!group) {
86
+ group = { label: descriptor.getGroupLabel(entity), items: [] };
87
+ groupMap.set(key, group);
88
+ }
89
+ group.items.push(entity);
90
+ }
91
+ const sortedKeys = Array.from(groupMap.keys());
92
+ if (descriptor.compareGroups) {
93
+ const cmp = descriptor.compareGroups;
94
+ sortedKeys.sort((a, b) => cmp(a, b));
95
+ }
96
+ const builtGroups = [];
97
+ const flat = [];
98
+ for (const key of sortedKeys) {
99
+ const group = groupMap.get(key);
100
+ builtGroups.push({ key, label: group.label, items: group.items });
101
+ flat.push(...group.items);
102
+ }
103
+ return { groups: builtGroups, flatEntities: flat };
104
+ }, [entities, descriptor]);
105
+ // Scroll selected item into view
106
+ useEffect(() => {
107
+ if (selectedId && listRef.current) {
108
+ const selectedButton = listRef.current.querySelector(`[data-entity-id="${selectedId}"]`);
109
+ if (selectedButton) {
110
+ selectedButton.scrollIntoView({ block: 'nearest' });
111
+ }
112
+ }
113
+ }, [selectedId]);
114
+ const selectedIndex = flatEntities.findIndex((e) => descriptor.getId(e) === selectedId);
115
+ const handleKeyDown = useCallback((e) => {
116
+ switch (e.key) {
117
+ case 'ArrowDown': {
118
+ e.preventDefault();
119
+ const nextIndex = selectedIndex < flatEntities.length - 1 ? selectedIndex + 1 : 0;
120
+ onSelect(descriptor.getId(flatEntities[nextIndex]));
121
+ break;
122
+ }
123
+ case 'ArrowUp': {
124
+ e.preventDefault();
125
+ const prevIndex = selectedIndex > 0 ? selectedIndex - 1 : flatEntities.length - 1;
126
+ onSelect(descriptor.getId(flatEntities[prevIndex]));
127
+ break;
128
+ }
129
+ case 'Enter':
130
+ case 'ArrowRight': {
131
+ if (selectedId !== undefined && onDrill) {
132
+ e.preventDefault();
133
+ onDrill();
134
+ }
135
+ break;
136
+ }
137
+ default:
138
+ break;
139
+ }
140
+ }, [flatEntities, descriptor, selectedId, selectedIndex, onSelect, onDrill]);
141
+ // Empty state
142
+ if (entities.length === 0 && emptyState) {
143
+ return (React.createElement("div", { className: "flex flex-col items-center justify-center flex-1 p-6 text-center" },
144
+ React.createElement("h3", { className: "text-sm font-medium text-secondary mb-1" }, emptyState.title),
145
+ React.createElement("p", { className: "text-xs text-muted mb-4" }, emptyState.description),
146
+ emptyState.action && (React.createElement("button", { onClick: emptyState.action.onClick, className: "px-3 py-1.5 text-xs font-medium text-white bg-brand-accent rounded-md hover:bg-brand-primary transition-colors" }, emptyState.action.label))));
147
+ }
148
+ return (React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden", onKeyDown: handleKeyDown },
149
+ flatEntities.length > 0 && (React.createElement("div", { className: "flex items-center justify-between px-3 py-1.5 border-b border-border-subtle" },
150
+ React.createElement("span", { className: "text-xs text-muted" },
151
+ flatEntities.length,
152
+ " item",
153
+ flatEntities.length !== 1 ? 's' : '',
154
+ compareMode && props.compareCount !== undefined && props.compareCount > 0 && (React.createElement("span", { className: "ml-1.5 text-brand-accent" },
155
+ "\u00B7 ",
156
+ props.compareCount,
157
+ " selected"))),
158
+ React.createElement("div", { className: "flex items-center gap-1" },
159
+ compareMode &&
160
+ props.onStartComparison &&
161
+ props.compareCount !== undefined &&
162
+ props.compareCount >= 2 && (React.createElement("button", { onClick: (e) => {
163
+ var _a;
164
+ e.stopPropagation();
165
+ (_a = props.onStartComparison) === null || _a === void 0 ? void 0 : _a.call(props);
166
+ }, className: "px-2 py-0.5 text-[11px] rounded border transition-colors bg-brand-primary text-white border-brand-primary hover:bg-brand-primary/90" }, "Compare Now")),
167
+ props.onToggleCompare && (React.createElement("button", { onClick: (e) => {
168
+ var _a;
169
+ e.stopPropagation();
170
+ (_a = props.onToggleCompare) === null || _a === void 0 ? void 0 : _a.call(props);
171
+ }, className: `px-2 py-0.5 text-[11px] rounded border transition-colors ${compareMode
172
+ ? 'bg-brand-accent text-white border-brand-accent'
173
+ : 'bg-surface text-muted border-border hover:border-brand-accent hover:text-brand-accent'}` }, compareMode ? 'Cancel' : 'Compare'))))),
174
+ React.createElement("div", { ref: listRef, className: "flex-1 overflow-y-auto" }, groups.map((group) => (React.createElement("div", { key: group.key },
175
+ React.createElement("div", { className: "px-3 py-1.5 text-xs font-medium text-muted uppercase tracking-wider bg-surface-alt border-b border-border-subtle sticky top-0 z-10" },
176
+ group.label,
177
+ React.createElement("span", { className: "text-muted normal-case font-normal ml-1" },
178
+ "(",
179
+ group.items.length,
180
+ ")")),
181
+ group.items.map((entity) => (React.createElement(GroupedEntityRow, { key: descriptor.getId(entity), entity: entity, descriptor: descriptor, selectedId: selectedId, compareMode: compareMode, checkedIds: checkedIds, onSelect: onSelect, onDrill: onDrill, onCheckedChange: onCheckedChange, onDelete: onDelete, canDelete: canDelete })))))))));
182
+ }
183
+ //# sourceMappingURL=GroupedEntityList.js.map
@@ -0,0 +1,34 @@
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
+ import React from 'react';
23
+ /**
24
+ * Search input bar for the sidebar.
25
+ * @public
26
+ */
27
+ export function SearchBar(props) {
28
+ const { value, onChange, placeholder = 'Search...' } = props;
29
+ return (React.createElement("div", { className: "px-3 py-2" },
30
+ React.createElement("div", { className: "relative" },
31
+ React.createElement("input", { type: "text", value: value, onChange: (e) => onChange(e.target.value), placeholder: placeholder, className: "w-full pl-3 pr-8 py-1.5 text-sm border border-border rounded-md bg-surface text-primary focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring" }),
32
+ value.length > 0 && (React.createElement("button", { onClick: () => onChange(''), className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted hover:text-secondary text-xs", "aria-label": "Clear search" }, "\u00D7")))));
33
+ }
34
+ //# sourceMappingURL=SearchBar.js.map
@@ -0,0 +1,62 @@
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
+ import React, { useEffect } from 'react';
23
+ import { useResponsive } from '../responsive';
24
+ /**
25
+ * Layout component that renders a persistent left sidebar alongside a main content area.
26
+ *
27
+ * On `full` layout mode, the sidebar is a fixed-width panel.
28
+ * On `compact` and `mobile` layout modes, the sidebar becomes a slide-out drawer
29
+ * controlled by {@link ISidebarLayoutProps.isSidebarOpen | isSidebarOpen} and
30
+ * {@link ISidebarLayoutProps.onSidebarClose | onSidebarClose}.
31
+ * @public
32
+ */
33
+ export function SidebarLayout(props) {
34
+ const { sidebar, children, sidebarWidth = '280px', isSidebarOpen = false, onSidebarClose } = props;
35
+ const { layoutMode } = useResponsive();
36
+ const isDrawer = layoutMode !== 'full';
37
+ // Close drawer on Escape key
38
+ useEffect(() => {
39
+ if (!isDrawer || !isSidebarOpen) {
40
+ return;
41
+ }
42
+ const handleKeyDown = (e) => {
43
+ if (e.key === 'Escape') {
44
+ onSidebarClose === null || onSidebarClose === void 0 ? void 0 : onSidebarClose();
45
+ }
46
+ };
47
+ document.addEventListener('keydown', handleKeyDown);
48
+ return () => document.removeEventListener('keydown', handleKeyDown);
49
+ }, [isDrawer, isSidebarOpen, onSidebarClose]);
50
+ // Full mode: static sidebar
51
+ if (!isDrawer) {
52
+ return (React.createElement("div", { className: "flex flex-1 overflow-hidden" },
53
+ React.createElement("aside", { className: "flex flex-col border-r border-border bg-surface overflow-y-auto shrink-0", style: { width: sidebarWidth } }, sidebar),
54
+ React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden" }, children)));
55
+ }
56
+ // Compact/mobile: drawer overlay
57
+ return (React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden relative" },
58
+ React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden" }, children),
59
+ isSidebarOpen && (React.createElement("div", { className: "fixed inset-0 bg-backdrop z-40 transition-opacity", onClick: onSidebarClose, "aria-hidden": "true" })),
60
+ React.createElement("aside", { className: `fixed inset-y-0 left-0 z-50 flex flex-col bg-surface border-r border-border overflow-y-auto shadow-lg transition-transform duration-200 ease-in-out ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`, style: { width: sidebarWidth } }, sidebar)));
61
+ }
62
+ //# sourceMappingURL=SidebarLayout.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Sidebar packlet - layout, search, filter rows, and entity list.
3
+ * @packageDocumentation
4
+ */
5
+ export { SidebarLayout } from './SidebarLayout';
6
+ export { SearchBar } from './SearchBar';
7
+ export { FilterRow } from './FilterRow';
8
+ export { FilterBar } from './FilterBar';
9
+ export { EntityList } from './EntityList';
10
+ export { GroupedEntityList } from './GroupedEntityList';
11
+ export { CollectionSection } from './CollectionSection';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,141 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
21
+ /**
22
+ * Default built-in themes.
23
+ */
24
+ const BUILT_IN_THEMES = [
25
+ { id: 'light', label: 'Light' },
26
+ { id: 'dark', label: 'Dark' },
27
+ { id: 'system', label: 'System' }
28
+ ];
29
+ const ThemeContext = createContext(undefined);
30
+ /**
31
+ * Maps a theme ID to the CSS class applied to the document element.
32
+ * - 'light': no class (uses :root defaults)
33
+ * - 'dark': 'dark' class
34
+ * - 'system': 'system-theme' class (uses prefers-color-scheme media query)
35
+ * - custom: 'theme-\{id\}' class
36
+ */
37
+ function themeIdToCssClass(id) {
38
+ switch (id) {
39
+ case 'light':
40
+ return undefined;
41
+ case 'dark':
42
+ return 'dark';
43
+ case 'system':
44
+ return 'system-theme';
45
+ default:
46
+ return `theme-${id}`;
47
+ }
48
+ }
49
+ /**
50
+ * Determines whether the effective appearance is dark.
51
+ */
52
+ function resolveIsDark(themeId) {
53
+ if (themeId === 'dark') {
54
+ return true;
55
+ }
56
+ if (themeId === 'system' && typeof window !== 'undefined') {
57
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
58
+ }
59
+ return false;
60
+ }
61
+ /**
62
+ * All CSS classes that ThemeProvider may apply.
63
+ * Used to cleanly remove the previous theme before applying a new one.
64
+ */
65
+ const MANAGED_CLASSES = ['dark', 'system-theme'];
66
+ /**
67
+ * Provides theme context to the application and manages the CSS class on `<html>`.
68
+ *
69
+ * Wrap your app (or a subtree) with this provider. Components use {@link useTheme}
70
+ * to read or change the active theme.
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * <ThemeProvider initialTheme={settings.appearance?.theme} onThemeChange={saveTheme}>
75
+ * <App />
76
+ * </ThemeProvider>
77
+ * ```
78
+ * @public
79
+ */
80
+ export function ThemeProvider({ initialTheme = 'light', customThemes, onThemeChange, children }) {
81
+ const [theme, setThemeState] = useState(initialTheme);
82
+ const [isDark, setIsDark] = useState(() => resolveIsDark(initialTheme));
83
+ const availableThemes = useMemo(() => {
84
+ if (!customThemes || customThemes.length === 0) {
85
+ return BUILT_IN_THEMES;
86
+ }
87
+ return [...BUILT_IN_THEMES, ...customThemes];
88
+ }, [customThemes]);
89
+ const setTheme = useCallback((newTheme) => {
90
+ setThemeState(newTheme);
91
+ onThemeChange === null || onThemeChange === void 0 ? void 0 : onThemeChange(newTheme);
92
+ }, [onThemeChange]);
93
+ // Apply CSS class to document element when theme changes
94
+ useEffect(() => {
95
+ const root = document.documentElement;
96
+ // Remove all managed theme classes
97
+ for (const cls of MANAGED_CLASSES) {
98
+ root.classList.remove(cls);
99
+ }
100
+ // Also remove any custom theme classes
101
+ for (const cls of Array.from(root.classList)) {
102
+ if (cls.startsWith('theme-')) {
103
+ root.classList.remove(cls);
104
+ }
105
+ }
106
+ // Apply new theme class
107
+ const cssClass = themeIdToCssClass(theme);
108
+ if (cssClass) {
109
+ root.classList.add(cssClass);
110
+ }
111
+ setIsDark(resolveIsDark(theme));
112
+ }, [theme]);
113
+ // Listen for OS preference changes when using 'system' theme
114
+ useEffect(() => {
115
+ if (theme !== 'system') {
116
+ return;
117
+ }
118
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
119
+ const handler = (e) => {
120
+ setIsDark(e.matches);
121
+ };
122
+ mediaQuery.addEventListener('change', handler);
123
+ return () => mediaQuery.removeEventListener('change', handler);
124
+ }, [theme]);
125
+ const value = useMemo(() => ({ theme, isDark, setTheme, availableThemes }), [theme, isDark, setTheme, availableThemes]);
126
+ return React.createElement(ThemeContext.Provider, { value: value }, children);
127
+ }
128
+ /**
129
+ * Access the current theme and theme-switching controls.
130
+ *
131
+ * Must be called within a {@link ThemeProvider}.
132
+ * @public
133
+ */
134
+ export function useTheme() {
135
+ const context = useContext(ThemeContext);
136
+ if (!context) {
137
+ throw new Error('useTheme must be used within a ThemeProvider');
138
+ }
139
+ return context;
140
+ }
141
+ //# sourceMappingURL=ThemeProvider.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Theme provider and hook for runtime theme switching.
3
+ * @packageDocumentation
4
+ */
5
+ export { ThemeProvider, useTheme } from './ThemeProvider';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,46 @@
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
+ import React from 'react';
23
+ import { useResponsive } from '../responsive';
24
+ /**
25
+ * Top-level mode selector bar.
26
+ * Renders the application title, mode toggle buttons, and optional right-side content.
27
+ * When {@link IModeSelectorProps.onMenuToggle | onMenuToggle} is provided, a hamburger
28
+ * menu button is shown at the left edge (for compact/mobile layouts).
29
+ * @public
30
+ */
31
+ export function ModeSelector(props) {
32
+ const { modes, activeMode, onModeChange, rightContent, title, onMenuToggle } = props;
33
+ const { layoutMode } = useResponsive();
34
+ const isMobile = layoutMode === 'mobile';
35
+ return (React.createElement("div", { className: "flex items-center justify-between px-4 py-2 bg-brand-primary text-white" },
36
+ React.createElement("div", { className: `flex items-center ${isMobile ? 'gap-2' : 'gap-6'}` },
37
+ onMenuToggle && (React.createElement("button", { onClick: onMenuToggle, className: "p-1.5 -ml-1.5 rounded-md text-white/70 hover:text-white hover:bg-white/10 transition-colors", "aria-label": "Open menu" },
38
+ React.createElement("svg", { className: "w-5 h-5", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor" },
39
+ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" })))),
40
+ !isMobile && React.createElement("h1", { className: "text-lg font-semibold whitespace-nowrap" }, title),
41
+ React.createElement("div", { className: "flex gap-1" }, modes.map((mode) => (React.createElement("button", { key: mode.id, onClick: () => onModeChange(mode.id), className: `rounded-md font-medium transition-colors ${isMobile ? 'px-2 py-1 text-xs' : 'px-4 py-1.5 text-sm'} ${activeMode === mode.id
42
+ ? 'bg-white/20 text-white'
43
+ : 'text-white/70 hover:text-white hover:bg-white/10'}`, "aria-current": activeMode === mode.id ? 'true' : undefined }, mode.label))))),
44
+ rightContent && React.createElement("div", { className: "flex items-center gap-2" }, rightContent)));
45
+ }
46
+ //# sourceMappingURL=ModeSelector.js.map
@@ -0,0 +1,37 @@
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
+ import React from 'react';
23
+ /**
24
+ * Second-level tab bar for switching views within a mode.
25
+ * @public
26
+ */
27
+ export function TabBar(props) {
28
+ const { tabs, activeTab, onTabChange, rightContent } = props;
29
+ return (React.createElement("div", { className: "flex items-center gap-1 px-4 py-1 bg-brand-secondary text-white border-t border-white/10 overflow-x-auto" },
30
+ tabs.map((tab) => (React.createElement("button", { key: tab.id, onClick: () => onTabChange(tab.id), className: `px-3 py-1.5 rounded-md text-sm font-medium transition-colors shrink-0 ${activeTab === tab.id
31
+ ? 'bg-white/20 text-white'
32
+ : 'text-white/60 hover:text-white hover:bg-white/10'}`, "aria-current": activeTab === tab.id ? 'page' : undefined }, tab.label))),
33
+ rightContent !== undefined && (React.createElement(React.Fragment, null,
34
+ React.createElement("div", { className: "flex-1" }),
35
+ rightContent))));
36
+ }
37
+ //# sourceMappingURL=TabBar.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Top bar components: mode selector and tab bar.
3
+ * @packageDocumentation
4
+ */
5
+ export { ModeSelector } from './ModeSelector';
6
+ export { TabBar } from './TabBar';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * URL sync packlet - bidirectional URL hash synchronization for mode/tab navigation.
3
+ * @packageDocumentation
4
+ */
5
+ export { encodeUrlHash, parseUrlHash, useUrlSync } from './useUrlSync';
6
+ //# sourceMappingURL=index.js.map