@fgv/ts-app-shell 5.1.0-4 → 5.1.0-40

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 (225) hide show
  1. package/README.md +182 -0
  2. package/dist/index.browser.js.map +1 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/packlets/ai-assist/index.js.map +1 -0
  5. package/dist/packlets/ai-assist/useAiAssist.js +225 -18
  6. package/dist/packlets/ai-assist/useAiAssist.js.map +1 -0
  7. package/dist/packlets/badge/index.js +23 -0
  8. package/dist/packlets/badge/index.js.map +1 -0
  9. package/dist/packlets/badge/renderStatusBadge.js +44 -0
  10. package/dist/packlets/badge/renderStatusBadge.js.map +1 -0
  11. package/dist/packlets/cascade/CascadeContainer.js.map +1 -0
  12. package/dist/packlets/cascade/ComparisonView.js.map +1 -0
  13. package/dist/packlets/cascade/EntityTabLayout.js.map +1 -0
  14. package/dist/packlets/cascade/MobileCascadeStack.js.map +1 -0
  15. package/dist/packlets/cascade/index.js.map +1 -0
  16. package/dist/packlets/cascade/model.js.map +1 -0
  17. package/dist/packlets/cascade/useCascadeOps.js.map +1 -0
  18. package/dist/packlets/cascade/useCascadeTransitions.js.map +1 -0
  19. package/dist/packlets/detail/DetailHelpers.js.map +1 -0
  20. package/dist/packlets/detail/index.js.map +1 -0
  21. package/dist/packlets/drop-zone/JsonDropZone.js.map +1 -0
  22. package/dist/packlets/drop-zone/index.js.map +1 -0
  23. package/dist/packlets/editing/EditFieldHelpers.js +4 -4
  24. package/dist/packlets/editing/EditFieldHelpers.js.map +1 -0
  25. package/dist/packlets/editing/MultiActionButton.js.map +1 -0
  26. package/dist/packlets/editing/NumericInput.js.map +1 -0
  27. package/dist/packlets/editing/TypeaheadInput.js.map +1 -0
  28. package/dist/packlets/editing/index.js.map +1 -0
  29. package/dist/packlets/editing/useTypeaheadMatch.js.map +1 -0
  30. package/dist/packlets/keyboard/index.js.map +1 -0
  31. package/dist/packlets/keyboard/registry.js.map +1 -0
  32. package/dist/packlets/keyboard/useKeyboardShortcuts.js.map +1 -0
  33. package/dist/packlets/messages/MessagesContext.js +79 -3
  34. package/dist/packlets/messages/MessagesContext.js.map +1 -0
  35. package/dist/packlets/messages/MessagesLogger.js +10 -31
  36. package/dist/packlets/messages/MessagesLogger.js.map +1 -0
  37. package/dist/packlets/messages/StatusBar.js +53 -28
  38. package/dist/packlets/messages/StatusBar.js.map +1 -0
  39. package/dist/packlets/messages/Toast.js +7 -4
  40. package/dist/packlets/messages/Toast.js.map +1 -0
  41. package/dist/packlets/messages/index.js +2 -2
  42. package/dist/packlets/messages/index.js.map +1 -0
  43. package/dist/packlets/messages/model.js +31 -4
  44. package/dist/packlets/messages/model.js.map +1 -0
  45. package/dist/packlets/messages/useLogReporter.js.map +1 -0
  46. package/dist/packlets/modal/ConfirmDialog.js.map +1 -0
  47. package/dist/packlets/modal/Modal.js.map +1 -0
  48. package/dist/packlets/modal/index.js.map +1 -0
  49. package/dist/packlets/print/PrintEnclosure.js.map +1 -0
  50. package/dist/packlets/print/index.js.map +1 -0
  51. package/dist/packlets/print/openPrintWindow.js.map +1 -0
  52. package/dist/packlets/responsive/ResponsiveProvider.js.map +1 -0
  53. package/dist/packlets/responsive/index.js.map +1 -0
  54. package/dist/packlets/responsive/useResponsiveLayout.js.map +1 -0
  55. package/dist/packlets/selectors/EntityRow.js.map +1 -0
  56. package/dist/packlets/selectors/PreferredSelector.js.map +1 -0
  57. package/dist/packlets/selectors/index.js.map +1 -0
  58. package/dist/packlets/sidebar/CollectionSection.js +158 -43
  59. package/dist/packlets/sidebar/CollectionSection.js.map +1 -0
  60. package/dist/packlets/sidebar/EntityList.js.map +1 -0
  61. package/dist/packlets/sidebar/FilterBar.js.map +1 -0
  62. package/dist/packlets/sidebar/FilterRow.js.map +1 -0
  63. package/dist/packlets/sidebar/GroupedEntityList.js.map +1 -0
  64. package/dist/packlets/sidebar/SearchBar.js.map +1 -0
  65. package/dist/packlets/sidebar/SidebarLayout.js.map +1 -0
  66. package/dist/packlets/sidebar/index.js.map +1 -0
  67. package/dist/packlets/theme/ThemeProvider.js.map +1 -0
  68. package/dist/packlets/theme/index.js.map +1 -0
  69. package/dist/packlets/top-bar/ModeSelector.js.map +1 -0
  70. package/dist/packlets/top-bar/TabBar.js +4 -1
  71. package/dist/packlets/top-bar/TabBar.js.map +1 -0
  72. package/dist/packlets/top-bar/index.js.map +1 -0
  73. package/dist/packlets/url-sync/index.js.map +1 -0
  74. package/dist/packlets/url-sync/useUrlSync.js.map +1 -0
  75. package/dist/styles/theme.css +194 -0
  76. package/lib/index.browser.d.ts.map +1 -0
  77. package/lib/index.browser.js.map +1 -0
  78. package/lib/index.d.ts.map +1 -0
  79. package/lib/index.js.map +1 -0
  80. package/lib/packlets/ai-assist/index.d.ts.map +1 -0
  81. package/lib/packlets/ai-assist/index.js.map +1 -0
  82. package/lib/packlets/ai-assist/useAiAssist.d.ts +26 -0
  83. package/lib/packlets/ai-assist/useAiAssist.d.ts.map +1 -0
  84. package/lib/packlets/ai-assist/useAiAssist.js +225 -18
  85. package/lib/packlets/ai-assist/useAiAssist.js.map +1 -0
  86. package/lib/packlets/badge/index.d.ts +2 -0
  87. package/lib/packlets/badge/index.d.ts.map +1 -0
  88. package/lib/packlets/badge/index.js +27 -0
  89. package/lib/packlets/badge/index.js.map +1 -0
  90. package/lib/packlets/badge/renderStatusBadge.d.ts +11 -0
  91. package/lib/packlets/badge/renderStatusBadge.d.ts.map +1 -0
  92. package/lib/packlets/badge/renderStatusBadge.js +50 -0
  93. package/lib/packlets/badge/renderStatusBadge.js.map +1 -0
  94. package/lib/packlets/cascade/CascadeContainer.d.ts.map +1 -0
  95. package/lib/packlets/cascade/CascadeContainer.js.map +1 -0
  96. package/lib/packlets/cascade/ComparisonView.d.ts.map +1 -0
  97. package/lib/packlets/cascade/ComparisonView.js.map +1 -0
  98. package/lib/packlets/cascade/EntityTabLayout.d.ts.map +1 -0
  99. package/lib/packlets/cascade/EntityTabLayout.js.map +1 -0
  100. package/lib/packlets/cascade/MobileCascadeStack.d.ts.map +1 -0
  101. package/lib/packlets/cascade/MobileCascadeStack.js.map +1 -0
  102. package/lib/packlets/cascade/index.d.ts.map +1 -0
  103. package/lib/packlets/cascade/index.js.map +1 -0
  104. package/lib/packlets/cascade/model.d.ts.map +1 -0
  105. package/lib/packlets/cascade/model.js.map +1 -0
  106. package/lib/packlets/cascade/useCascadeOps.d.ts.map +1 -0
  107. package/lib/packlets/cascade/useCascadeOps.js.map +1 -0
  108. package/lib/packlets/cascade/useCascadeTransitions.d.ts.map +1 -0
  109. package/lib/packlets/cascade/useCascadeTransitions.js.map +1 -0
  110. package/lib/packlets/detail/DetailHelpers.d.ts.map +1 -0
  111. package/lib/packlets/detail/DetailHelpers.js.map +1 -0
  112. package/lib/packlets/detail/index.d.ts.map +1 -0
  113. package/lib/packlets/detail/index.js.map +1 -0
  114. package/lib/packlets/drop-zone/JsonDropZone.d.ts.map +1 -0
  115. package/lib/packlets/drop-zone/JsonDropZone.js.map +1 -0
  116. package/lib/packlets/drop-zone/index.d.ts.map +1 -0
  117. package/lib/packlets/drop-zone/index.js.map +1 -0
  118. package/lib/packlets/editing/EditFieldHelpers.d.ts +6 -2
  119. package/lib/packlets/editing/EditFieldHelpers.d.ts.map +1 -0
  120. package/lib/packlets/editing/EditFieldHelpers.js +4 -4
  121. package/lib/packlets/editing/EditFieldHelpers.js.map +1 -0
  122. package/lib/packlets/editing/MultiActionButton.d.ts.map +1 -0
  123. package/lib/packlets/editing/MultiActionButton.js.map +1 -0
  124. package/lib/packlets/editing/NumericInput.d.ts.map +1 -0
  125. package/lib/packlets/editing/NumericInput.js.map +1 -0
  126. package/lib/packlets/editing/TypeaheadInput.d.ts.map +1 -0
  127. package/lib/packlets/editing/TypeaheadInput.js.map +1 -0
  128. package/lib/packlets/editing/index.d.ts.map +1 -0
  129. package/lib/packlets/editing/index.js.map +1 -0
  130. package/lib/packlets/editing/useTypeaheadMatch.d.ts.map +1 -0
  131. package/lib/packlets/editing/useTypeaheadMatch.js.map +1 -0
  132. package/lib/packlets/keyboard/index.d.ts.map +1 -0
  133. package/lib/packlets/keyboard/index.js.map +1 -0
  134. package/lib/packlets/keyboard/registry.d.ts.map +1 -0
  135. package/lib/packlets/keyboard/registry.js.map +1 -0
  136. package/lib/packlets/keyboard/useKeyboardShortcuts.d.ts.map +1 -0
  137. package/lib/packlets/keyboard/useKeyboardShortcuts.js.map +1 -0
  138. package/lib/packlets/messages/MessagesContext.d.ts +47 -3
  139. package/lib/packlets/messages/MessagesContext.d.ts.map +1 -0
  140. package/lib/packlets/messages/MessagesContext.js +79 -2
  141. package/lib/packlets/messages/MessagesContext.js.map +1 -0
  142. package/lib/packlets/messages/MessagesLogger.d.ts +8 -3
  143. package/lib/packlets/messages/MessagesLogger.d.ts.map +1 -0
  144. package/lib/packlets/messages/MessagesLogger.js +10 -31
  145. package/lib/packlets/messages/MessagesLogger.js.map +1 -0
  146. package/lib/packlets/messages/StatusBar.d.ts +10 -1
  147. package/lib/packlets/messages/StatusBar.d.ts.map +1 -0
  148. package/lib/packlets/messages/StatusBar.js +52 -27
  149. package/lib/packlets/messages/StatusBar.js.map +1 -0
  150. package/lib/packlets/messages/Toast.d.ts.map +1 -0
  151. package/lib/packlets/messages/Toast.js +6 -3
  152. package/lib/packlets/messages/Toast.js.map +1 -0
  153. package/lib/packlets/messages/index.d.ts +2 -2
  154. package/lib/packlets/messages/index.d.ts.map +1 -0
  155. package/lib/packlets/messages/index.js +6 -1
  156. package/lib/packlets/messages/index.js.map +1 -0
  157. package/lib/packlets/messages/model.d.ts +74 -8
  158. package/lib/packlets/messages/model.d.ts.map +1 -0
  159. package/lib/packlets/messages/model.js +32 -4
  160. package/lib/packlets/messages/model.js.map +1 -0
  161. package/lib/packlets/messages/useLogReporter.d.ts.map +1 -0
  162. package/lib/packlets/messages/useLogReporter.js.map +1 -0
  163. package/lib/packlets/modal/ConfirmDialog.d.ts.map +1 -0
  164. package/lib/packlets/modal/ConfirmDialog.js.map +1 -0
  165. package/lib/packlets/modal/Modal.d.ts.map +1 -0
  166. package/lib/packlets/modal/Modal.js.map +1 -0
  167. package/lib/packlets/modal/index.d.ts.map +1 -0
  168. package/lib/packlets/modal/index.js.map +1 -0
  169. package/lib/packlets/print/PrintEnclosure.d.ts.map +1 -0
  170. package/lib/packlets/print/PrintEnclosure.js.map +1 -0
  171. package/lib/packlets/print/index.d.ts.map +1 -0
  172. package/lib/packlets/print/index.js.map +1 -0
  173. package/lib/packlets/print/openPrintWindow.d.ts.map +1 -0
  174. package/lib/packlets/print/openPrintWindow.js.map +1 -0
  175. package/lib/packlets/responsive/ResponsiveProvider.d.ts.map +1 -0
  176. package/lib/packlets/responsive/ResponsiveProvider.js.map +1 -0
  177. package/lib/packlets/responsive/index.d.ts.map +1 -0
  178. package/lib/packlets/responsive/index.js.map +1 -0
  179. package/lib/packlets/responsive/useResponsiveLayout.d.ts.map +1 -0
  180. package/lib/packlets/responsive/useResponsiveLayout.js.map +1 -0
  181. package/lib/packlets/selectors/EntityRow.d.ts.map +1 -0
  182. package/lib/packlets/selectors/EntityRow.js.map +1 -0
  183. package/lib/packlets/selectors/PreferredSelector.d.ts.map +1 -0
  184. package/lib/packlets/selectors/PreferredSelector.js.map +1 -0
  185. package/lib/packlets/selectors/index.d.ts.map +1 -0
  186. package/lib/packlets/selectors/index.js.map +1 -0
  187. package/lib/packlets/sidebar/CollectionSection.d.ts +40 -2
  188. package/lib/packlets/sidebar/CollectionSection.d.ts.map +1 -0
  189. package/lib/packlets/sidebar/CollectionSection.js +157 -42
  190. package/lib/packlets/sidebar/CollectionSection.js.map +1 -0
  191. package/lib/packlets/sidebar/EntityList.d.ts.map +1 -0
  192. package/lib/packlets/sidebar/EntityList.js.map +1 -0
  193. package/lib/packlets/sidebar/FilterBar.d.ts.map +1 -0
  194. package/lib/packlets/sidebar/FilterBar.js.map +1 -0
  195. package/lib/packlets/sidebar/FilterRow.d.ts.map +1 -0
  196. package/lib/packlets/sidebar/FilterRow.js.map +1 -0
  197. package/lib/packlets/sidebar/GroupedEntityList.d.ts.map +1 -0
  198. package/lib/packlets/sidebar/GroupedEntityList.js.map +1 -0
  199. package/lib/packlets/sidebar/SearchBar.d.ts.map +1 -0
  200. package/lib/packlets/sidebar/SearchBar.js.map +1 -0
  201. package/lib/packlets/sidebar/SidebarLayout.d.ts.map +1 -0
  202. package/lib/packlets/sidebar/SidebarLayout.js.map +1 -0
  203. package/lib/packlets/sidebar/index.d.ts +1 -1
  204. package/lib/packlets/sidebar/index.d.ts.map +1 -0
  205. package/lib/packlets/sidebar/index.js.map +1 -0
  206. package/lib/packlets/theme/ThemeProvider.d.ts.map +1 -0
  207. package/lib/packlets/theme/ThemeProvider.js.map +1 -0
  208. package/lib/packlets/theme/index.d.ts.map +1 -0
  209. package/lib/packlets/theme/index.js.map +1 -0
  210. package/lib/packlets/top-bar/ModeSelector.d.ts.map +1 -0
  211. package/lib/packlets/top-bar/ModeSelector.js.map +1 -0
  212. package/lib/packlets/top-bar/TabBar.d.ts +12 -0
  213. package/lib/packlets/top-bar/TabBar.d.ts.map +1 -0
  214. package/lib/packlets/top-bar/TabBar.js +4 -1
  215. package/lib/packlets/top-bar/TabBar.js.map +1 -0
  216. package/lib/packlets/top-bar/index.d.ts +1 -1
  217. package/lib/packlets/top-bar/index.d.ts.map +1 -0
  218. package/lib/packlets/top-bar/index.js.map +1 -0
  219. package/lib/packlets/url-sync/index.d.ts.map +1 -0
  220. package/lib/packlets/url-sync/index.js.map +1 -0
  221. package/lib/packlets/url-sync/useUrlSync.d.ts.map +1 -0
  222. package/lib/packlets/url-sync/useUrlSync.js.map +1 -0
  223. package/lib/styles/theme.css +194 -0
  224. package/package.json +11 -7
  225. package/tailwind-preset.cjs +102 -0
@@ -0,0 +1,44 @@
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
+ const BADGE_COUNT_BASE_CLASSES = 'ml-1.5 min-w-4 px-1.5 py-0.5 text-[0.6875rem] inline-flex shrink-0 items-center justify-center rounded-full font-medium leading-none';
24
+ const BADGE_DOT_BASE_CLASSES = 'ml-1.5 h-2 w-2 inline-block shrink-0 rounded-full';
25
+ const BADGE_COUNT_TONE_CLASSES = {
26
+ info: 'bg-status-info-bg text-status-info-text',
27
+ warning: 'bg-status-warning-bg text-status-warning-text',
28
+ danger: 'bg-status-error-bg text-status-error-text'
29
+ };
30
+ const BADGE_DOT_TONE_CLASSES = {
31
+ info: 'bg-status-info-icon',
32
+ warning: 'bg-status-warning-icon',
33
+ danger: 'bg-status-error-icon'
34
+ };
35
+ export function renderStatusBadge(badge) {
36
+ var _a, _b;
37
+ const tone = (_a = badge.tone) !== null && _a !== void 0 ? _a : 'info';
38
+ const ariaLabel = badge.ariaLabel;
39
+ if (badge.kind === 'dot') {
40
+ return (React.createElement("span", { className: `${BADGE_DOT_BASE_CLASSES} ${BADGE_DOT_TONE_CLASSES[tone]}`, "aria-label": ariaLabel }));
41
+ }
42
+ return (React.createElement("span", { className: `${BADGE_COUNT_BASE_CLASSES} ${BADGE_COUNT_TONE_CLASSES[tone]}`, "aria-label": ariaLabel }, (_b = badge.count) !== null && _b !== void 0 ? _b : 0));
43
+ }
44
+ //# sourceMappingURL=renderStatusBadge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderStatusBadge.js","sourceRoot":"","sources":["../../../src/packlets/badge/renderStatusBadge.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAW1B,MAAM,wBAAwB,GAC5B,sIAAsI,CAAC;AAEzI,MAAM,sBAAsB,GAAW,mDAAmD,CAAC;AAE3F,MAAM,wBAAwB,GAA8B;IAC1D,IAAI,EAAE,yCAAyC;IAC/C,OAAO,EAAE,+CAA+C;IACxD,MAAM,EAAE,2CAA2C;CACpD,CAAC;AAEF,MAAM,sBAAsB,GAA8B;IACxD,IAAI,EAAE,qBAAqB;IAC3B,OAAO,EAAE,wBAAwB;IACjC,MAAM,EAAE,sBAAsB;CAC/B,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,KAAiB;;IACjD,MAAM,IAAI,GAAG,MAAA,KAAK,CAAC,IAAI,mCAAI,MAAM,CAAC;IAClC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IAElC,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QACzB,OAAO,CACL,8BAAM,SAAS,EAAE,GAAG,sBAAsB,IAAI,sBAAsB,CAAC,IAAI,CAAC,EAAE,gBAAc,SAAS,GAAI,CACxG,CAAC;IACJ,CAAC;IAED,OAAO,CACL,8BAAM,SAAS,EAAE,GAAG,wBAAwB,IAAI,wBAAwB,CAAC,IAAI,CAAC,EAAE,gBAAc,SAAS,IACpG,MAAA,KAAK,CAAC,KAAK,mCAAI,CAAC,CACZ,CACR,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React from 'react';\n\ntype BadgeTone = 'info' | 'warning' | 'danger';\n\ninterface IBadgeLike {\n readonly kind: 'dot' | 'count';\n readonly tone?: BadgeTone;\n readonly count?: number;\n readonly ariaLabel?: string;\n}\n\nconst BADGE_COUNT_BASE_CLASSES: string =\n 'ml-1.5 min-w-4 px-1.5 py-0.5 text-[0.6875rem] inline-flex shrink-0 items-center justify-center rounded-full font-medium leading-none';\n\nconst BADGE_DOT_BASE_CLASSES: string = 'ml-1.5 h-2 w-2 inline-block shrink-0 rounded-full';\n\nconst BADGE_COUNT_TONE_CLASSES: Record<BadgeTone, string> = {\n info: 'bg-status-info-bg text-status-info-text',\n warning: 'bg-status-warning-bg text-status-warning-text',\n danger: 'bg-status-error-bg text-status-error-text'\n};\n\nconst BADGE_DOT_TONE_CLASSES: Record<BadgeTone, string> = {\n info: 'bg-status-info-icon',\n warning: 'bg-status-warning-icon',\n danger: 'bg-status-error-icon'\n};\n\nexport function renderStatusBadge(badge: IBadgeLike): React.ReactElement {\n const tone = badge.tone ?? 'info';\n const ariaLabel = badge.ariaLabel;\n\n if (badge.kind === 'dot') {\n return (\n <span className={`${BADGE_DOT_BASE_CLASSES} ${BADGE_DOT_TONE_CLASSES[tone]}`} aria-label={ariaLabel} />\n );\n }\n\n return (\n <span className={`${BADGE_COUNT_BASE_CLASSES} ${BADGE_COUNT_TONE_CLASSES[tone]}`} aria-label={ariaLabel}>\n {badge.count ?? 0}\n </span>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CascadeContainer.js","sourceRoot":"","sources":["../../../src/packlets/cascade/CascadeContainer.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Column cascade container — horizontal scroll of detail columns.\n * @packageDocumentation\n */\n\nimport React, { useCallback, useEffect, useRef } from 'react';\nimport { useResponsive } from '../responsive';\nimport { MobileCascadeStack } from './MobileCascadeStack';\n\n// ============================================================================\n// Cascade Column\n// ============================================================================\n\n/**\n * Describes a single column in the cascade.\n * @public\n */\nexport interface ICascadeColumn {\n /** Unique key for React reconciliation */\n readonly key: string;\n /** Breadcrumb label for this column */\n readonly label: string;\n /** Column content */\n readonly content: React.ReactNode;\n}\n\n// ============================================================================\n// CascadeContainer Props\n// ============================================================================\n\n/**\n * Props for the CascadeContainer component.\n * @public\n */\nexport interface ICascadeContainerProps {\n /** Ordered columns (left-to-right) */\n readonly columns: ReadonlyArray<ICascadeColumn>;\n /** Callback to pop the cascade back to a specific depth (0 = clear all) */\n readonly onPopTo: (depth: number) => void;\n /** Minimum column width in CSS units (default: '400px') */\n readonly minColumnWidth?: string;\n /** Callback when the user clicks inside a cascade column (signals list should collapse) */\n readonly onFocus?: () => void;\n /** Label for the breadcrumb root link (default: 'List') */\n readonly rootLabel?: string;\n}\n\n// ============================================================================\n// CascadeContainer Component\n// ============================================================================\n\n/**\n * Horizontal scroll container for the column cascade.\n *\n * Renders a breadcrumb trail at the top and horizontally-scrollable\n * detail columns below. Auto-scrolls to the rightmost column when\n * a new column is pushed.\n *\n * @public\n */\nexport function CascadeContainer(props: ICascadeContainerProps): React.ReactElement | null {\n const { columns, onPopTo, minColumnWidth = '400px', onFocus, rootLabel = 'List' } = props;\n const { layoutMode } = useResponsive();\n const scrollRef = useRef<HTMLDivElement>(null);\n\n const handleMouseDown = useCallback((): void => {\n onFocus?.();\n }, [onFocus]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent): void => {\n if (e.key === 'Escape' || e.key === 'ArrowLeft') {\n e.preventDefault();\n e.stopPropagation();\n if (columns.length > 1) {\n onPopTo(columns.length - 1);\n } else {\n onPopTo(0);\n }\n }\n },\n [columns.length, onPopTo]\n );\n\n // Auto-scroll to rightmost column when columns change\n useEffect(() => {\n if (scrollRef.current) {\n scrollRef.current.scrollTo({\n left: scrollRef.current.scrollWidth,\n behavior: 'smooth'\n });\n }\n }, [columns.length]);\n\n if (layoutMode === 'mobile') {\n return <MobileCascadeStack {...props} />;\n }\n\n if (columns.length === 0) {\n return null;\n }\n\n return (\n <div\n className=\"flex flex-col flex-1 overflow-hidden outline-none\"\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n >\n {/* Breadcrumb trail */}\n <div className=\"flex items-center gap-1 px-3 py-1.5 bg-surface-alt border-b border-border text-xs shrink-0 overflow-x-auto\">\n <button\n onClick={(): void => onPopTo(0)}\n className=\"text-brand-accent hover:text-brand-primary hover:underline shrink-0\"\n >\n {rootLabel}\n </button>\n {columns.map((col, idx) => (\n <React.Fragment key={col.key}>\n <span className=\"text-muted shrink-0\">/</span>\n {idx < columns.length - 1 ? (\n <button\n onClick={(): void => onPopTo(idx + 1)}\n className=\"text-brand-accent hover:text-brand-primary hover:underline truncate max-w-[200px] shrink-0\"\n >\n {col.label}\n </button>\n ) : (\n <span className=\"text-secondary font-medium truncate max-w-[200px] shrink-0\">{col.label}</span>\n )}\n </React.Fragment>\n ))}\n </div>\n\n {/* Columns */}\n <div\n ref={scrollRef}\n className=\"flex flex-1 overflow-x-auto overflow-y-hidden\"\n onMouseDown={handleMouseDown}\n >\n {columns.map((col) => (\n <div\n key={col.key}\n className=\"flex flex-col shrink-0 border-r border-border overflow-y-auto\"\n style={{ minWidth: minColumnWidth, width: minColumnWidth }}\n >\n {col.content}\n </div>\n ))}\n </div>\n </div>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ComparisonView.js","sourceRoot":"","sources":["../../../src/packlets/cascade/ComparisonView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;GAGG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AA4B1B,+EAA+E;AAC/E,2BAA2B;AAC3B,+EAA+E;AAE/E;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,KAA2B;IACxD,MAAM,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC;IAE1B,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,CACL,6BAAK,SAAS,EAAC,4DAA4D,0CAErE,CACP,CAAC;IACJ,CAAC;IAED,OAAO,CACL,6BAAK,SAAS,EAAC,6BAA6B,IACzC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACpB,6BACE,GAAG,EAAE,GAAG,CAAC,GAAG,EACZ,SAAS,EAAC,qFAAqF;QAG/F,6BAAK,SAAS,EAAC,4DAA4D;YACzE,8BAAM,SAAS,EAAC,mDAAmD,IAAE,GAAG,CAAC,KAAK,CAAQ,CAClF;QAEN,6BAAK,SAAS,EAAC,wBAAwB,IAAE,GAAG,CAAC,OAAO,CAAO,CACvD,CACP,CAAC,CACE,CACP,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * ComparisonView — side-by-side read-only comparison of 2–4 entities.\n * @packageDocumentation\n */\n\nimport React from 'react';\n\n// ============================================================================\n// ComparisonView Props\n// ============================================================================\n\n/**\n * A single column in the comparison view.\n * @public\n */\nexport interface IComparisonColumn {\n /** Unique key for React reconciliation */\n readonly key: string;\n /** Column header label */\n readonly label: string;\n /** Column content (typically a detail component) */\n readonly content: React.ReactNode;\n}\n\n/**\n * Props for the ComparisonView component.\n * @public\n */\nexport interface IComparisonViewProps {\n /** Columns to compare (2–4) */\n readonly columns: ReadonlyArray<IComparisonColumn>;\n}\n\n// ============================================================================\n// ComparisonView Component\n// ============================================================================\n\n/**\n * Side-by-side comparison view for entities.\n *\n * Renders 2–4 entity detail views in equal-width columns with\n * synchronized scrolling (future) and column headers.\n *\n * @public\n */\nexport function ComparisonView(props: IComparisonViewProps): React.ReactElement {\n const { columns } = props;\n\n if (columns.length < 2) {\n return (\n <div className=\"flex flex-1 items-center justify-center text-muted text-sm\">\n Select at least 2 items to compare.\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-1 overflow-hidden\">\n {columns.map((col) => (\n <div\n key={col.key}\n className=\"flex flex-col flex-1 min-w-0 border-r border-border last:border-r-0 overflow-hidden\"\n >\n {/* Column header */}\n <div className=\"px-3 py-1.5 bg-surface-alt border-b border-border shrink-0\">\n <span className=\"text-xs font-medium text-secondary truncate block\">{col.label}</span>\n </div>\n {/* Column content */}\n <div className=\"flex-1 overflow-y-auto\">{col.content}</div>\n </div>\n ))}\n </div>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityTabLayout.js","sourceRoot":"","sources":["../../../src/packlets/cascade/EntityTabLayout.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * EntityTabLayout — shared layout for entity tab content with list + cascade + collapse-on-focus.\n * @packageDocumentation\n */\n\nimport React from 'react';\n\nimport { CascadeContainer, type ICascadeColumn } from './CascadeContainer';\nimport { useResponsive } from '../responsive';\nimport { ComparisonView, type IComparisonColumn } from './ComparisonView';\n\n// ============================================================================\n// EntityTabLayout Props\n// ============================================================================\n\n/**\n * Props for the EntityTabLayout component.\n * @public\n */\nexport interface IEntityTabLayoutProps {\n /** The entity list content (rendered in the collapsible left panel) */\n readonly list: React.ReactNode;\n /** Cascade columns to display (empty array = no cascade) */\n readonly cascadeColumns: ReadonlyArray<ICascadeColumn>;\n /** Callback to pop the cascade back to a specific depth (0 = clear all) */\n readonly onPopTo: (depth: number) => void;\n /** Whether the entity list is currently collapsed */\n readonly listCollapsed: boolean;\n /** Callback to collapse the entity list (fired when user clicks inside cascade) */\n readonly onListCollapse: () => void;\n /** Whether compare mode is active */\n readonly compareMode?: boolean;\n /** Columns for the comparison view (when compare mode is active with 2+ selections) */\n readonly comparisonColumns?: ReadonlyArray<IComparisonColumn>;\n /** Whether the comparison view is actively showing (user explicitly triggered) */\n readonly showingComparison?: boolean;\n /** Callback to exit the comparison view (back to selection list) */\n readonly onExitComparison?: () => void;\n /** Columns for variation comparison (when comparing variations of a single recipe) */\n readonly variationCompareColumns?: ReadonlyArray<IComparisonColumn>;\n /** Callback to exit variation comparison mode */\n readonly onExitVariationCompare?: () => void;\n}\n\n// ============================================================================\n// EntityTabLayout Component\n// ============================================================================\n\n/**\n * Shared layout for entity tab content.\n *\n * Renders an entity list on the left and a cascade container on the right.\n * The list stays expanded while browsing (selecting items in the list).\n * It collapses when the user clicks inside the cascade detail pane,\n * signaling they are focused on the detail rather than browsing.\n *\n * @public\n */\nexport function EntityTabLayout(props: IEntityTabLayoutProps): React.ReactElement {\n const { list, cascadeColumns, onPopTo, listCollapsed, onListCollapse, compareMode, comparisonColumns } =\n props;\n\n const variationCompareColumns = props.variationCompareColumns;\n const onExitVariationCompare = props.onExitVariationCompare;\n const showingComparison = props.showingComparison ?? false;\n const onExitComparison = props.onExitComparison;\n const { layoutMode } = useResponsive();\n\n const isVariationCompare = variationCompareColumns !== undefined && variationCompareColumns.length >= 2;\n const showComparison =\n !isVariationCompare &&\n showingComparison &&\n comparisonColumns !== undefined &&\n comparisonColumns.length >= 2;\n const showCascade = !compareMode && !isVariationCompare && !showComparison && cascadeColumns.length > 0;\n const hasCascadeOrCompare = cascadeColumns.length > 0 || showComparison;\n\n // Variation compare banner — shared between mobile and desktop\n const variationCompareBanner = isVariationCompare && onExitVariationCompare && (\n <div className=\"flex items-center gap-2 px-3 py-1.5 bg-status-warning-bg border-b border-status-warning-border shrink-0\">\n <span className=\"text-xs text-status-warning-text\">\n Comparing {variationCompareColumns.length} variations\n </span>\n <button\n onClick={onExitVariationCompare}\n className=\"px-2 py-0.5 text-xs rounded border border-status-warning-border text-status-warning-text hover:bg-status-warning-surface transition-colors\"\n >\n Exit\n </button>\n </div>\n );\n\n // Mobile: show one pane at a time — list or cascade/comparison full-screen\n if (layoutMode === 'mobile') {\n const hasCascadeContent = showCascade || showComparison || isVariationCompare;\n\n return (\n <div className=\"flex flex-col flex-1 overflow-hidden\">\n {variationCompareBanner}\n {hasCascadeContent ? (\n <>\n {showCascade && <CascadeContainer columns={cascadeColumns} onPopTo={onPopTo} />}\n {showComparison && (\n <div className=\"flex flex-col flex-1 overflow-hidden\">\n <div className=\"flex items-center gap-2 px-3 py-1.5 bg-status-info-bg border-b border-status-info-border shrink-0\">\n <span className=\"text-xs text-status-info-text\">\n Comparing {comparisonColumns.length} items\n </span>\n {onExitComparison && (\n <button\n onClick={onExitComparison}\n className=\"px-2 py-0.5 text-xs rounded border border-status-info-border text-status-info-text hover:bg-status-info-surface transition-colors\"\n >\n ← Back to list\n </button>\n )}\n </div>\n <ComparisonView columns={comparisonColumns} />\n </div>\n )}\n {isVariationCompare && <ComparisonView columns={variationCompareColumns} />}\n </>\n ) : (\n <div className=\"flex flex-col flex-1 overflow-hidden\">{list}</div>\n )}\n </div>\n );\n }\n\n // Desktop/compact: side-by-side list and cascade\n return (\n <div className=\"flex flex-col flex-1 overflow-hidden\">\n {variationCompareBanner}\n\n {/* Main content area */}\n <div className=\"flex flex-1 overflow-hidden\">\n {/* Entity list — proportional width when cascade is showing, full width when browsing */}\n <div\n className={`flex flex-col overflow-hidden transition-all ${\n isVariationCompare || showComparison\n ? 'w-0 min-w-0'\n : listCollapsed\n ? 'w-0 min-w-0'\n : hasCascadeOrCompare\n ? 'w-1/4 max-w-xs shrink-0 border-r border-border'\n : 'w-full max-w-sm shrink-0 border-r border-border'\n }`}\n >\n {list}\n </div>\n\n {/* Cascade columns (normal mode) */}\n {showCascade && (\n <CascadeContainer columns={cascadeColumns} onPopTo={onPopTo} onFocus={onListCollapse} />\n )}\n\n {/* Entity comparison view (compare mode — explicitly triggered) */}\n {showComparison && (\n <div className=\"flex flex-col flex-1 overflow-hidden\">\n <div className=\"flex items-center gap-2 px-3 py-1.5 bg-status-info-bg border-b border-status-info-border shrink-0\">\n <span className=\"text-xs text-status-info-text\">\n Comparing {comparisonColumns.length} items\n </span>\n {onExitComparison && (\n <button\n onClick={onExitComparison}\n className=\"px-2 py-0.5 text-xs rounded border border-status-info-border text-status-info-text hover:bg-status-info-surface transition-colors\"\n >\n ← Back to list\n </button>\n )}\n </div>\n <ComparisonView columns={comparisonColumns} />\n </div>\n )}\n\n {/* Variation comparison view */}\n {isVariationCompare && <ComparisonView columns={variationCompareColumns} />}\n </div>\n </div>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MobileCascadeStack.js","sourceRoot":"","sources":["../../../src/packlets/cascade/MobileCascadeStack.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;GAGG;AAEH,OAAO,KAAK,EAAE,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAG3C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAA6B;IAC9D,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC;IAEvD,MAAM,UAAU,GAAG,WAAW,CAAC,GAAS,EAAE;QACxC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,CAAC,CAAC,CAAC;QACb,CAAC;IACH,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAE9B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAErF,OAAO,CACL,6BAAK,SAAS,EAAC,sCAAsC;QAEnD,6BAAK,SAAS,EAAC,kFAAkF;YAC/F,gCACE,OAAO,EAAE,UAAU,EACnB,SAAS,EAAC,4EAA4E,gBAC1E,WAAW,SAAS,EAAE;gBAElC,6BACE,SAAS,EAAC,kBAAkB,EAC5B,IAAI,EAAC,MAAM,EACX,OAAO,EAAC,WAAW,EACnB,WAAW,EAAE,CAAC,EACd,MAAM,EAAC,cAAc;oBAErB,8BAAM,aAAa,EAAC,OAAO,EAAC,cAAc,EAAC,OAAO,EAAC,CAAC,EAAC,6BAA6B,GAAG,CACjF;gBACL,SAAS,CACH;YACR,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CACrB,8BAAM,SAAS,EAAC,qCAAqC,IAAE,aAAa,CAAC,KAAK,CAAQ,CACnF,CACG;QAGN,6BAAK,SAAS,EAAC,sCAAsC,IAAE,aAAa,CAAC,OAAO,CAAO,CAC/E,CACP,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Mobile view-stack cascade — one full-screen column at a time with back navigation.\n * @packageDocumentation\n */\n\nimport React, { useCallback } from 'react';\nimport { type ICascadeContainerProps } from './CascadeContainer';\n\n/**\n * Mobile replacement for {@link CascadeContainer}.\n *\n * Shows the rightmost (deepest) cascade column full-screen with a back button\n * that pops one level at a time. At the first column, back returns to the list\n * by calling `onPopTo(0)`.\n *\n * Accepts the same props as {@link CascadeContainer} so `CascadeContainer` can\n * delegate to it transparently on mobile.\n *\n * @public\n */\nexport function MobileCascadeStack(props: ICascadeContainerProps): React.ReactElement | null {\n const { columns, onPopTo, rootLabel = 'List' } = props;\n\n const handleBack = useCallback((): void => {\n if (columns.length > 1) {\n onPopTo(columns.length - 1);\n } else {\n onPopTo(0);\n }\n }, [columns.length, onPopTo]);\n\n if (columns.length === 0) {\n return null;\n }\n\n const currentColumn = columns[columns.length - 1];\n const backLabel = columns.length > 1 ? columns[columns.length - 2].label : rootLabel;\n\n return (\n <div className=\"flex flex-col flex-1 overflow-hidden\">\n {/* Back navigation header */}\n <div className=\"flex items-center gap-2 px-3 py-2 bg-surface-alt border-b border-border shrink-0\">\n <button\n onClick={handleBack}\n className=\"flex items-center gap-1 text-sm text-brand-accent hover:text-brand-primary\"\n aria-label={`Back to ${backLabel}`}\n >\n <svg\n className=\"w-4 h-4 shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n strokeWidth={2}\n stroke=\"currentColor\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M15.75 19.5L8.25 12l7.5-7.5\" />\n </svg>\n {backLabel}\n </button>\n {columns.length > 1 && (\n <span className=\"ml-auto text-xs text-muted truncate\">{currentColumn.label}</span>\n )}\n </div>\n\n {/* Current column — full screen, scrollable */}\n <div className=\"flex flex-col flex-1 overflow-y-auto\">{currentColumn.content}</div>\n </div>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/cascade/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;GAGG;AAEH,gBAAgB;AAChB,OAAO,EAAE,gBAAgB,EAAoD,MAAM,oBAAoB,CAAC;AACxG,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAA8B,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAAE,cAAc,EAAqD,MAAM,kBAAkB,CAAC;AAErG,sBAAsB;AACtB,OAAO,EAIL,qBAAqB,EACtB,MAAM,SAAS,CAAC;AAEjB,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE3E,8BAA8B;AAC9B,OAAO,EAKL,aAAa,EACd,MAAM,iBAAiB,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Cascade packlet - column cascade container, layout, and semantic operations.\n * @packageDocumentation\n */\n\n// UI components\nexport { CascadeContainer, type ICascadeContainerProps, type ICascadeColumn } from './CascadeContainer';\nexport { MobileCascadeStack } from './MobileCascadeStack';\nexport { EntityTabLayout, type IEntityTabLayoutProps } from './EntityTabLayout';\nexport { ComparisonView, type IComparisonViewProps, type IComparisonColumn } from './ComparisonView';\n\n// Cascade model types\nexport {\n type CascadeColumnMode,\n type CascadeEntryOrigin,\n type ICascadeEntryBase,\n CASCADE_NEW_ENTITY_ID\n} from './model';\n\n// Cascade transition hooks\nexport { useSquashAt, useCascadeDrillDown } from './useCascadeTransitions';\n\n// Semantic cascade operations\nexport {\n type ICascadeConflict,\n type ICascadeFind,\n type CascadeEntrySpec,\n type ICascadeOps,\n useCascadeOps\n} from './useCascadeOps';\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../../src/packlets/cascade/model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAkCH,+EAA+E;AAC/E,qBAAqB;AACrB,+EAA+E;AAE/E;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAc,SAAS,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Base cascade model types for column cascade navigation.\n *\n * These types are domain-agnostic — they define the structure and semantics\n * of cascade navigation without knowledge of specific entity types.\n * Domain-specific applications extend {@link ICascadeEntryBase} with their\n * own entity types and additional fields.\n *\n * @packageDocumentation\n */\n\n// ============================================================================\n// Cascade Column Modes\n// ============================================================================\n\n/**\n * Mode for a cascade column (view-only, editing, or creating a new entity).\n * @public\n */\nexport type CascadeColumnMode = 'view' | 'edit' | 'create' | 'preview';\n\n/**\n * How a cascade entry was opened — determines save/cancel behavior.\n *\n * - `'primary'` — selected from the entity list. Save/cancel transitions to view mode in-place.\n * - `'nested'` — reached via drill-down, typeahead-create, or sub-entity editing. Save/cancel pops (removes entry).\n *\n * Entries without an explicit origin are treated as `'primary'` for backwards compatibility.\n * @public\n */\nexport type CascadeEntryOrigin = 'primary' | 'nested';\n\n// ============================================================================\n// Cascade Entry Base\n// ============================================================================\n\n/**\n * Sentinel value used as entityId when creating a new entity.\n * @public\n */\nexport const CASCADE_NEW_ENTITY_ID: '__new__' = '__new__';\n\n/**\n * Base interface for a single entry in the column cascade stack.\n *\n * This interface contains only the fields needed by the generic cascade\n * operations ({@link useCascadeOps}, {@link useSquashAt}, {@link useCascadeDrillDown}).\n * Domain-specific applications should extend this interface with additional\n * fields (e.g., target weights, source references, session context).\n *\n * @public\n */\nexport interface ICascadeEntryBase {\n /** The type of entity displayed in this column (domain-specific string). */\n readonly entityType: string;\n /** The ID of the entity (qualified ID, e.g. 'collection.base-id'). */\n readonly entityId: string;\n /** Whether the column is in view, edit, create, or preview mode. */\n readonly mode: CascadeColumnMode;\n /** How this entry was opened. Determines save/cancel behavior. @see CascadeEntryOrigin */\n readonly origin?: CascadeEntryOrigin;\n /** Optional pre-resolved materialized entity. */\n readonly entity?: unknown;\n /** Whether this cascade entry has unsaved changes (set by the owning tab component). */\n readonly hasChanges?: boolean;\n /** Pre-fill name for entity creation (set when on-blur typeahead doesn't match). */\n readonly prefillName?: string;\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCascadeOps.js","sourceRoot":"","sources":["../../../src/packlets/cascade/useCascadeOps.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Semantic cascade operations hook.\n *\n * Encodes cascade navigation rules so tabs declare intent (select, drillDown,\n * openEditor, pop) rather than manually computing cascade stacks.\n *\n * The hook is generic over the entry type `TEntry extends ICascadeEntryBase`.\n * Domain-specific applications pass their extended entry type to preserve\n * full type information through the cascade operations.\n *\n * @packageDocumentation\n */\n\nimport { useCallback, useMemo } from 'react';\n\nimport { type Result, fail, succeed } from '@fgv/ts-utils';\n\nimport type { CascadeColumnMode, ICascadeEntryBase } from './model';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Describes a located entry in the cascade stack.\n * @public\n */\nexport interface ICascadeFind<TEntry extends ICascadeEntryBase = ICascadeEntryBase> {\n readonly depth: number;\n readonly entry: TEntry;\n}\n\n/**\n * Describes editors that would be affected by a cascade operation.\n * Returned by {@link ICascadeOps.openEditor} when the operation is blocked.\n * @public\n */\nexport interface ICascadeConflict<TEntry extends ICascadeEntryBase = ICascadeEntryBase> {\n readonly conflictingEditors: ReadonlyArray<ICascadeFind<TEntry>>;\n}\n\n/**\n * Partial cascade entry for operations that set mode and origin automatically.\n * @public\n */\nexport type CascadeEntrySpec<TEntry extends ICascadeEntryBase = ICascadeEntryBase> = Omit<\n TEntry,\n 'mode' | 'origin'\n> & { readonly mode?: CascadeColumnMode };\n\n/**\n * Semantic cascade operations.\n *\n * These operations encode the cascade navigation rules:\n * 1. Views stack on views\n * 2. Editor replaces its view and trims views above (but not below)\n * 3. Editors never squash editors — blocked if editors exist above target\n * 4. Editors can stack on editors (child above parent is fine)\n * 5. Save/cancel on nested panels pops; on primary panels returns to view\n * 6. Preview behaves like view for all stacking/trimming rules\n * 7. Save/cancel blocked when child editors exist above\n *\n * @typeParam TEntry - The cascade entry type, defaults to {@link ICascadeEntryBase}.\n * @public\n */\nexport interface ICascadeOps<TEntry extends ICascadeEntryBase = ICascadeEntryBase> {\n /**\n * Replace entire stack with a single view entry (from list selection).\n * @returns The created entry at depth 0.\n */\n readonly select: (entry: CascadeEntrySpec<TEntry>) => Result<ICascadeFind<TEntry>>;\n\n /**\n * Push a view entry after `fromDepth`, trimming anything beyond.\n * Toggle: if the same entity is already at `fromDepth + 1`, collapse instead.\n * @returns The pushed entry, or the collapsed entry on toggle.\n */\n readonly drillDown: (fromDepth: number, entry: CascadeEntrySpec<TEntry>) => Result<ICascadeFind<TEntry>>;\n\n /**\n * Switch panel at `depth` to edit mode.\n * Trims view/preview panels above `depth`. Blocked if editors/creates exist above.\n * @returns The entry switched to edit mode, or failure if blocked by conflicting editors.\n */\n readonly openEditor: (depth: number) => Result<ICascadeFind<TEntry>>;\n\n /**\n * Push a nested panel (create or edit) on top of the current stack.\n * Used for typeahead-on-blur creation and sub-entity editing.\n * @returns The pushed nested entry.\n */\n readonly openNested: (fromDepth: number, entry: Omit<TEntry, 'origin'>) => Result<ICascadeFind<TEntry>>;\n\n /**\n * Pop the topmost entry (for nested save/cancel). Always safe.\n * @returns The removed entry, or failure if the stack was empty.\n */\n readonly pop: () => Result<ICascadeFind<TEntry>>;\n\n /**\n * Transition entry at `depth` from edit/create to view mode.\n * Used for primary entity save/cancel.\n * @returns The entry transitioned to view mode.\n */\n readonly popToView: (depth: number, refreshedEntity?: unknown) => Result<ICascadeFind<TEntry>>;\n\n /**\n * Trim the stack to keep only entries below `depth` (exclusive).\n * Removes the entry at `depth` and everything above it.\n * @returns The last remaining entry after trimming, or failure if stack becomes empty.\n */\n readonly trimTo: (depth: number) => Result<ICascadeFind<TEntry>>;\n\n /** Return editors/creates above a given depth. */\n readonly editorsAbove: (depth: number) => ReadonlyArray<ICascadeFind<TEntry>>;\n\n /** Whether any editors/creates exist anywhere in the stack. */\n readonly hasUnsavedEditors: () => boolean;\n\n /** Whether save/cancel is allowed at `depth` (false if child editors exist above). */\n readonly canSaveOrCancel: (depth: number) => boolean;\n\n /** Clear the entire cascade stack. */\n readonly clear: () => void;\n\n /** Clear the cascade if any entry has the given entity ID. */\n readonly clearById: (entityId: string) => void;\n\n /** Clear the cascade if any entry matches the predicate. */\n readonly clearIf: (predicate: (entry: TEntry) => boolean) => void;\n\n /** Find the first entry matching a predicate. */\n readonly find: (predicate: (entry: TEntry) => boolean) => Result<ICascadeFind<TEntry>>;\n\n /** The current cascade stack (for rendering). */\n readonly stack: ReadonlyArray<TEntry>;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isEditOrCreate(mode: CascadeColumnMode): boolean {\n return mode === 'edit' || mode === 'create';\n}\n\nfunction findEditorsAbove<TEntry extends ICascadeEntryBase>(\n stack: ReadonlyArray<TEntry>,\n depth: number\n): ReadonlyArray<ICascadeFind<TEntry>> {\n const result: Array<ICascadeFind<TEntry>> = [];\n for (let i = depth + 1; i < stack.length; i++) {\n if (isEditOrCreate(stack[i].mode)) {\n result.push({ depth: i, entry: stack[i] });\n }\n }\n return result;\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/**\n * Hook providing semantic cascade operations.\n *\n * Takes the cascade stack and a squash function as parameters, making it\n * independent of any specific state management solution. Domain-specific\n * applications typically provide a convenience wrapper that reads these\n * values from their own store.\n *\n * @typeParam TEntry - The cascade entry type. Inferred from `cascadeStack`.\n * @public\n */\nexport function useCascadeOps<TEntry extends ICascadeEntryBase>(\n cascadeStack: ReadonlyArray<TEntry>,\n squashCascade: (entries: ReadonlyArray<TEntry>) => void\n): ICascadeOps<TEntry> {\n // Internal helper: spread-override mode/origin on a stack entry.\n // The spread preserves all TEntry fields, but TypeScript can't prove that\n // Omit<TEntry, K> & { K: V } is assignable to TEntry. The cast is safe\n // because we only override properties defined on ICascadeEntryBase.\n type AsTEntry = (entry: ICascadeEntryBase) => TEntry;\n const asTEntry: AsTEntry = (entry) => entry as unknown as TEntry;\n\n const select = useCallback(\n (entry: CascadeEntrySpec<TEntry>): Result<ICascadeFind<TEntry>> => {\n const created = asTEntry({ ...entry, mode: entry.mode ?? 'view', origin: 'primary' });\n squashCascade([created]);\n return succeed({ depth: 0, entry: created });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [squashCascade]\n );\n\n const drillDown = useCallback(\n (fromDepth: number, entry: CascadeEntrySpec<TEntry>): Result<ICascadeFind<TEntry>> => {\n const nextEntry = cascadeStack[fromDepth + 1];\n if (nextEntry?.entityType === entry.entityType && nextEntry.entityId === entry.entityId) {\n // Toggle: collapse if same entity is already at fromDepth + 1\n squashCascade(cascadeStack.slice(0, fromDepth + 1));\n return succeed({ depth: fromDepth + 1, entry: nextEntry });\n }\n const created = asTEntry({ ...entry, mode: entry.mode ?? 'view', origin: 'nested' });\n squashCascade([...cascadeStack.slice(0, fromDepth + 1), created]);\n return succeed({ depth: fromDepth + 1, entry: created });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [cascadeStack, squashCascade]\n );\n\n const editorsAbove = useCallback(\n (depth: number): ReadonlyArray<ICascadeFind<TEntry>> => {\n return findEditorsAbove(cascadeStack, depth);\n },\n [cascadeStack]\n );\n\n const openEditor = useCallback(\n (depth: number): Result<ICascadeFind<TEntry>> => {\n const entry = cascadeStack[depth];\n if (!entry) {\n return fail(`depth ${depth} out of bounds for cascade stack of length ${cascadeStack.length}`);\n }\n\n // Check for editors above the target depth\n const conflicts = findEditorsAbove(cascadeStack, depth);\n if (conflicts.length > 0) {\n const names = conflicts.map((c) => `${c.entry.entityType}(${c.entry.mode}) at depth ${c.depth}`);\n return fail(`blocked by editors above: ${names.join(', ')}`);\n }\n\n // Trim everything above depth (downstream views become incoherent),\n // switch target to edit mode, preserving origin\n const edited = asTEntry({ ...entry, mode: 'edit' });\n squashCascade([...cascadeStack.slice(0, depth), edited]);\n return succeed({ depth, entry: edited });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [cascadeStack, squashCascade]\n );\n\n const openNested = useCallback(\n (fromDepth: number, entry: Omit<TEntry, 'origin'>): Result<ICascadeFind<TEntry>> => {\n const created = asTEntry({ ...entry, origin: 'nested' });\n const newDepth = fromDepth + 1;\n squashCascade([...cascadeStack.slice(0, newDepth), created]);\n return succeed({ depth: newDepth, entry: created });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [cascadeStack, squashCascade]\n );\n\n const pop = useCallback((): Result<ICascadeFind<TEntry>> => {\n if (cascadeStack.length === 0) {\n return fail('cannot pop from empty cascade stack');\n }\n const removed = cascadeStack[cascadeStack.length - 1];\n squashCascade(cascadeStack.slice(0, -1));\n return succeed({ depth: cascadeStack.length - 1, entry: removed });\n }, [cascadeStack, squashCascade]);\n\n const popToView = useCallback(\n (depth: number, refreshedEntity?: unknown): Result<ICascadeFind<TEntry>> => {\n const entry = cascadeStack[depth];\n if (!entry) {\n return fail(`depth ${depth} out of bounds for cascade stack of length ${cascadeStack.length}`);\n }\n const viewed = asTEntry({\n ...entry,\n mode: 'view' as const,\n ...(refreshedEntity !== undefined ? { entity: refreshedEntity } : {})\n });\n squashCascade([...cascadeStack.slice(0, depth), viewed]);\n return succeed({ depth, entry: viewed });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [cascadeStack, squashCascade]\n );\n\n const trimTo = useCallback(\n (depth: number): Result<ICascadeFind<TEntry>> => {\n const trimmed = Math.max(0, depth);\n const newStack = cascadeStack.slice(0, trimmed);\n squashCascade(newStack);\n if (newStack.length === 0) {\n return fail('cascade stack is empty after trim');\n }\n const lastIdx = newStack.length - 1;\n return succeed({ depth: lastIdx, entry: newStack[lastIdx] });\n },\n [cascadeStack, squashCascade]\n );\n\n const hasUnsavedEditors = useCallback((): boolean => {\n return cascadeStack.some((e) => isEditOrCreate(e.mode));\n }, [cascadeStack]);\n\n const canSaveOrCancel = useCallback(\n (depth: number): boolean => {\n return findEditorsAbove(cascadeStack, depth).length === 0;\n },\n [cascadeStack]\n );\n\n const clear = useCallback((): void => {\n squashCascade([] as unknown as ReadonlyArray<TEntry>);\n }, [squashCascade]);\n\n const clearIf = useCallback(\n (predicate: (entry: TEntry) => boolean): void => {\n if (cascadeStack.some(predicate)) {\n squashCascade([] as unknown as ReadonlyArray<TEntry>);\n }\n },\n [cascadeStack, squashCascade]\n );\n\n const clearById = useCallback(\n (entityId: string): void => {\n clearIf((e) => e.entityId === entityId);\n },\n [clearIf]\n );\n\n const find = useCallback(\n (predicate: (entry: TEntry) => boolean): Result<ICascadeFind<TEntry>> => {\n const depth = cascadeStack.findIndex(predicate);\n if (depth < 0) {\n return fail('no matching entry in cascade stack');\n }\n return succeed({ depth, entry: cascadeStack[depth] });\n },\n [cascadeStack]\n );\n\n return useMemo(\n () => ({\n select,\n drillDown,\n openEditor,\n openNested,\n pop,\n popToView,\n trimTo,\n editorsAbove,\n hasUnsavedEditors,\n canSaveOrCancel,\n clear,\n clearById,\n clearIf,\n find,\n stack: cascadeStack\n }),\n [\n select,\n drillDown,\n openEditor,\n openNested,\n pop,\n popToView,\n trimTo,\n editorsAbove,\n hasUnsavedEditors,\n canSaveOrCancel,\n clear,\n clearById,\n clearIf,\n find,\n cascadeStack\n ]\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCascadeTransitions.js","sourceRoot":"","sources":["../../../src/packlets/cascade/useCascadeTransitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAIpC;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,YAA8C,EAC9C,aAAkE;IAElE,OAAO,WAAW,CAChB,CAAC,KAAa,EAAE,KAAwB,EAAQ,EAAE;QAChD,aAAa,CAAC,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAC9D,CAAC,EACD,CAAC,YAAY,EAAE,aAAa,CAAC,CAC9B,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CACjC,YAA8C,EAC9C,aAAkE,EAClE,QAA2D;IAE3D,OAAO,WAAW,CAChB,CAAC,KAAa,EAAE,UAAkB,EAAE,QAAgB,EAAE,KAAkC,EAAQ,EAAE;QAChG,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAC1C,IAAI,CAAA,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,UAAU,MAAK,UAAU,IAAI,SAAS,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC5E,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;YAChD,OAAO;QACT,CAAC;QACD,QAAQ,CAAC,KAAK,kBAAI,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,IAAK,KAAK,EAAG,CAAC;IACpE,CAAC,EACD,CAAC,YAAY,EAAE,QAAQ,EAAE,aAAa,CAAC,CACxC,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Shared cascade transition hooks for entity tabs.\n *\n * @packageDocumentation\n */\n\nimport { useCallback } from 'react';\n\nimport type { ICascadeEntryBase } from './model';\n\n/**\n * Returns a depth-aware squash helper used by cascade views.\n *\n * Keeps stack entries up to and including `depth`, then appends `entry`.\n *\n * @public\n */\nexport function useSquashAt(\n cascadeStack: ReadonlyArray<ICascadeEntryBase>,\n squashCascade: (entries: ReadonlyArray<ICascadeEntryBase>) => void\n): (depth: number, entry: ICascadeEntryBase) => void {\n return useCallback(\n (depth: number, entry: ICascadeEntryBase): void => {\n squashCascade([...cascadeStack.slice(0, depth + 1), entry]);\n },\n [cascadeStack, squashCascade]\n );\n}\n\n/**\n * Returns a shared drill-down toggle helper for cascade columns.\n *\n * If the target entry is already immediately to the right of `depth`, it collapses\n * back to `depth`. Otherwise, it appends a new view entry at `depth + 1`.\n *\n * @public\n */\nexport function useCascadeDrillDown(\n cascadeStack: ReadonlyArray<ICascadeEntryBase>,\n squashCascade: (entries: ReadonlyArray<ICascadeEntryBase>) => void,\n squashAt: (depth: number, entry: ICascadeEntryBase) => void\n): (depth: number, entityType: string, entityId: string, extra?: Partial<ICascadeEntryBase>) => void {\n return useCallback(\n (depth: number, entityType: string, entityId: string, extra?: Partial<ICascadeEntryBase>): void => {\n const nextEntry = cascadeStack[depth + 1];\n if (nextEntry?.entityType === entityType && nextEntry.entityId === entityId) {\n squashCascade(cascadeStack.slice(0, depth + 1));\n return;\n }\n squashAt(depth, { entityType, entityId, mode: 'view', ...extra });\n },\n [cascadeStack, squashAt, squashCascade]\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DetailHelpers.js","sourceRoot":"","sources":["../../../src/packlets/detail/DetailHelpers.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAWtD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAuB;IACpE,OAAO,CACL,6BAAK,SAAS,EAAC,MAAM;QACnB,4BAAI,SAAS,EAAC,kEAAkE,IAAE,KAAK,CAAM;QAC5F,QAAQ,CACL,CACP,CAAC;AACJ,CAAC;AAiBD;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAmB;IAC5E,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,OAAO,CACL,6BAAK,SAAS,EAAC,kCAAkC;YAC/C,8BAAM,SAAS,EAAC,kCAAkC,IAAE,KAAK,CAAQ;YACjE,8BAAM,SAAS,EAAC,sBAAsB,IAAE,KAAK,CAAQ,CACjD,CACP,CAAC;IACJ,CAAC;IACD,OAAO,CACL,6BAAK,SAAS,EAAC,oDAAoD;QACjE,8BAAM,SAAS,EAAC,0BAA0B,IAAE,KAAK,CAAQ;QACzD,8BAAM,SAAS,EAAC,yBAAyB,IAAE,KAAK,CAAQ,CACpD,CACP,CAAC;AACJ,CAAC;AAUD;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,EAAE,IAAI,EAAiB;IAC7C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,CACL,6BAAK,SAAS,EAAC,sBAAsB,IAClC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACjB,8BAAM,GAAG,EAAE,GAAG,EAAE,SAAS,EAAC,mEAAmE,IAC1F,GAAG,CACC,CACR,CAAC,CACE,CACP,CAAC;AACJ,CAAC;AAYD;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,EAAE,KAAK,EAAE,UAAU,EAAqB;IAClE,OAAO,CACL,8BAAM,SAAS,EAAE,iEAAiE,UAAU,EAAE,IAC3F,KAAK,CACD,CACR,CAAC;AACJ,CAAC;AAqBD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,WAAW,EACX,UAAU,EACV,OAAO,EACP,OAAO,EACY;IACnB,OAAO,CACL,6BAAK,SAAS,EAAC,eAAe;QAC5B,6BAAK,SAAS,EAAC,kBAAkB;YAC/B,4BAAI,SAAS,EAAC,mDAAmD,IAAE,KAAK,CAAM;YAC7E,OAAO,IAAI,CACV,gCACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,OAAO,EAChB,KAAK,EAAC,OAAO,EACb,SAAS,EAAC,8GAA8G;gBAExH,oBAAC,SAAS,IAAC,SAAS,EAAC,SAAS,GAAG,CAC1B,CACV,CACG;QACL,QAAQ,IAAI,2BAAG,SAAS,EAAC,0CAA0C,IAAE,QAAQ,CAAK;QAClF,CAAC,UAAU,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS,CAAC,IAAI,CACtD,6BAAK,SAAS,EAAC,8CAA8C;YAC3D,6BAAK,SAAS,EAAC,yBAAyB,IAAE,UAAU,CAAO;YAC1D,OAAO,KAAK,SAAS,IAAI,6BAAK,SAAS,EAAC,kCAAkC,IAAE,OAAO,CAAO,CACvF,CACP;QACA,WAAW,IAAI,2BAAG,SAAS,EAAC,6BAA6B,IAAE,WAAW,CAAK,CACxE,CACP,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Generic detail-view primitive components.\n *\n * Domain-agnostic building blocks for entity detail panels:\n * - `DetailSection` — labeled section wrapper\n * - `DetailRow` — key/value row with spread or inline layout\n * - `TagList` — pill tag row\n * - `StatusBadge` — colored pill badge (caller supplies color classes)\n * - `DetailHeader` — two-line header with indicators and action slots\n *\n * @packageDocumentation\n */\n\nimport React from 'react';\nimport { XMarkIcon } from '@heroicons/react/20/solid';\n\n// ============================================================================\n// DetailSection\n// ============================================================================\n\nexport interface IDetailSectionProps {\n readonly title: string;\n readonly children: React.ReactNode;\n}\n\n/**\n * Labeled section wrapper with uppercase tracking header.\n * @public\n */\nexport function DetailSection({ title, children }: IDetailSectionProps): React.ReactElement {\n return (\n <div className=\"mb-4\">\n <h3 className=\"text-xs font-semibold text-muted uppercase tracking-wider mb-1.5\">{title}</h3>\n {children}\n </div>\n );\n}\n\n// ============================================================================\n// DetailRow\n// ============================================================================\n\nexport interface IDetailRowProps {\n readonly label: string;\n readonly value: React.ReactNode;\n /**\n * Layout variant:\n * - `'spread'` (default) — label left, value right, justify-between\n * - `'inline'` — label fixed-width, value follows immediately\n */\n readonly layout?: 'spread' | 'inline';\n}\n\n/**\n * Key/value row for detail panels.\n * Returns `null` if `value` is `null` or `undefined`.\n * @public\n */\nexport function DetailRow({ label, value, layout = 'spread' }: IDetailRowProps): React.ReactElement | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (layout === 'inline') {\n return (\n <div className=\"flex items-baseline gap-2 py-0.5\">\n <span className=\"text-xs text-muted w-32 shrink-0\">{label}</span>\n <span className=\"text-sm text-primary\">{value}</span>\n </div>\n );\n }\n return (\n <div className=\"flex items-baseline justify-between py-0.5 text-sm\">\n <span className=\"text-muted shrink-0 mr-2\">{label}</span>\n <span className=\"text-primary text-right\">{value}</span>\n </div>\n );\n}\n\n// ============================================================================\n// TagList\n// ============================================================================\n\nexport interface ITagListProps {\n readonly tags: ReadonlyArray<string>;\n}\n\n/**\n * Horizontal pill row for string tags. Returns `null` if tags is empty.\n * @public\n */\nexport function TagList({ tags }: ITagListProps): React.ReactElement | null {\n if (tags.length === 0) {\n return null;\n }\n return (\n <div className=\"flex flex-wrap gap-1\">\n {tags.map((tag) => (\n <span key={tag} className=\"px-2 py-0.5 text-xs rounded-full bg-surface-raised text-secondary\">\n {tag}\n </span>\n ))}\n </div>\n );\n}\n\n// ============================================================================\n// StatusBadge\n// ============================================================================\n\nexport interface IStatusBadgeProps {\n readonly label: string;\n /** Tailwind color classes, e.g. `'bg-amber-100 text-amber-800'` */\n readonly colorClass: string;\n}\n\n/**\n * Generic pill badge. Caller supplies the Tailwind color classes.\n * @public\n */\nexport function StatusBadge({ label, colorClass }: IStatusBadgeProps): React.ReactElement {\n return (\n <span className={`inline-block px-2 py-0.5 text-[11px] font-medium rounded-full ${colorClass}`}>\n {label}\n </span>\n );\n}\n\n// ============================================================================\n// DetailHeader\n// ============================================================================\n\nexport interface IDetailHeaderProps {\n /** Primary entity name — rendered full-width on its own line */\n readonly title: string;\n /** Optional de-emphasized subtitle (e.g. entity ID) rendered below the title */\n readonly subtitle?: string;\n /** Optional description rendered below the status bar */\n readonly description?: string;\n /** Left slot of the status bar — badges, icons, etc. */\n readonly indicators?: React.ReactNode;\n /** Right slot of the status bar — action buttons */\n readonly actions?: React.ReactNode;\n /** If provided, renders a close button in the upper-right corner, inline with the title */\n readonly onClose?: () => void;\n}\n\n/**\n * Three-line entity detail header.\n *\n * Line 1: full-width `title` (bold headline)\n * Line 2: optional `subtitle` (de-emphasized, monospace — e.g. entity ID)\n * Line 3: `indicators` left-justified, `actions` right-justified (status bar)\n * Below: optional `description`\n *\n * Both `indicators` and `actions` are `React.ReactNode` — callers own the content.\n * @public\n */\nexport function DetailHeader({\n title,\n subtitle,\n description,\n indicators,\n actions,\n onClose\n}: IDetailHeaderProps): React.ReactElement {\n return (\n <div className=\"mb-4 relative\">\n <div className=\"flex items-start\">\n <h2 className=\"text-lg font-semibold text-primary flex-1 min-w-0\">{title}</h2>\n {onClose && (\n <button\n type=\"button\"\n onClick={onClose}\n title=\"Close\"\n className=\"shrink-0 ml-2 mt-0.5 p-0.5 text-muted hover:text-secondary hover:bg-surface-raised rounded transition-colors\"\n >\n <XMarkIcon className=\"w-4 h-4\" />\n </button>\n )}\n </div>\n {subtitle && <p className=\"text-xs text-muted font-mono mt-0.5 mb-1\">{subtitle}</p>}\n {(indicators !== undefined || actions !== undefined) && (\n <div className=\"flex items-center justify-between gap-2 mt-1\">\n <div className=\"flex items-center gap-2\">{indicators}</div>\n {actions !== undefined && <div className=\"flex items-center gap-1 shrink-0\">{actions}</div>}\n </div>\n )}\n {description && <p className=\"text-sm text-secondary mt-1\">{description}</p>}\n </div>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/detail/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,aAAa,EAEb,SAAS,EAET,OAAO,EAEP,WAAW,EAEX,YAAY,EAEb,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * Detail packlet - generic detail-view primitive components.\n * @packageDocumentation\n */\n\nexport {\n DetailSection,\n type IDetailSectionProps,\n DetailRow,\n type IDetailRowProps,\n TagList,\n type ITagListProps,\n StatusBadge,\n type IStatusBadgeProps,\n DetailHeader,\n type IDetailHeaderProps\n} from './DetailHelpers';\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JsonDropZone.js","sourceRoot":"","sources":["../../../src/packlets/drop-zone/JsonDropZone.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useCallback, useState } from 'react';\nimport { Converter } from '@fgv/ts-utils';\n\n/**\n * Strips markdown code fences from text.\n * AI agents often wrap JSON in code fences.\n * @param text - Raw text that may contain code fences\n * @returns Text with outer code fences removed\n * @internal\n */\nexport function stripCodeFences(text: string): string {\n const trimmed = text.trim();\n const fencePattern = /^```(?:\\w+)?\\s*\\n?([\\s\\S]*?)\\n?\\s*```$/;\n const match = fencePattern.exec(trimmed);\n return match ? match[1].trim() : trimmed;\n}\n\n/**\n * Props for the JsonDropZone component.\n * @typeParam T - The expected validated type\n * @public\n */\nexport interface IJsonDropZoneProps<T> {\n /** Converter to validate parsed JSON against */\n readonly converter: Converter<T>;\n /** Called with the validated value on successful drop/paste */\n readonly onValueReceived: (value: T) => void;\n /** Called with an error message on parse/validation failure */\n readonly onError?: (message: string) => void;\n /** Externally-controlled error message to display */\n readonly error?: string;\n /** Placeholder hint text */\n readonly hint?: string;\n /** Additional CSS class names */\n readonly className?: string;\n}\n\n/**\n * Generic JSON drop/paste target with converter-based validation.\n * Accepts text via drag-and-drop or paste, strips markdown code fences,\n * parses as JSON, validates through the provided converter, and calls\n * the appropriate callback.\n *\n * @typeParam T - The expected validated type\n * @public\n */\nexport function JsonDropZone<T>(props: IJsonDropZoneProps<T>): React.ReactElement {\n const { converter, onValueReceived, onError, error, hint, className } = props;\n const [isDragOver, setIsDragOver] = useState(false);\n const [internalError, setInternalError] = useState<string | undefined>(undefined);\n\n const displayError = error ?? internalError;\n\n const processText = useCallback(\n (rawText: string): void => {\n setInternalError(undefined);\n\n const stripped = stripCodeFences(rawText);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(stripped);\n } catch {\n const message = 'Invalid JSON: could not parse the dropped text.';\n setInternalError(message);\n onError?.(message);\n return;\n }\n\n const result = converter.convert(parsed);\n if (result.isSuccess()) {\n setInternalError(undefined);\n onValueReceived(result.value);\n } else {\n const message = `Validation failed: ${result.message}`;\n setInternalError(message);\n onError?.(message);\n }\n },\n [converter, onValueReceived, onError]\n );\n\n const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>): void => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragOver(true);\n }, []);\n\n const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>): void => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragOver(false);\n }, []);\n\n const handleDrop = useCallback(\n (e: React.DragEvent<HTMLDivElement>): void => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragOver(false);\n\n const text = e.dataTransfer.getData('text/plain') || e.dataTransfer.getData('text');\n if (text) {\n processText(text);\n } else {\n const message = 'No text data found in the dropped content.';\n setInternalError(message);\n onError?.(message);\n }\n },\n [processText, onError]\n );\n\n const handlePaste = useCallback(\n (e: React.ClipboardEvent<HTMLDivElement>): void => {\n e.preventDefault();\n const text = e.clipboardData.getData('text/plain') || e.clipboardData.getData('text');\n if (text) {\n processText(text);\n }\n },\n [processText]\n );\n\n const borderClass = displayError\n ? 'border-status-error-border bg-status-error-bg'\n : isDragOver\n ? 'border-status-info-border bg-status-info-bg'\n : 'border-border bg-surface-alt';\n\n return (\n <div\n className={`border-2 border-dashed rounded-md p-3 text-center text-sm transition-colors cursor-default ${borderClass} ${\n className ?? ''\n }`}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n onPaste={handlePaste}\n tabIndex={0}\n role=\"region\"\n aria-label={hint ?? 'Drop or paste JSON here'}\n >\n {displayError ? (\n <span className=\"text-status-error-text text-xs\">{displayError}</span>\n ) : (\n <span className=\"text-muted\">{hint ?? 'Drop or paste JSON here'}</span>\n )}\n </div>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/drop-zone/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAA2B,MAAM,gBAAgB,CAAC","sourcesContent":["/**\n * Drop zone packlet - generic JSON drop/paste target with converter-based validation.\n * @packageDocumentation\n */\n\nexport { JsonDropZone, type IJsonDropZoneProps } from './JsonDropZone';\n"]}
@@ -33,18 +33,18 @@ import React from 'react';
33
33
  * Horizontal label + field layout for a single edit field.
34
34
  * @public
35
35
  */
36
- export function EditField({ label, children }) {
36
+ export function EditField({ label, tooltip, children }) {
37
37
  return (React.createElement("div", { className: "flex items-baseline gap-2 py-1" },
38
- React.createElement("label", { className: "text-xs text-muted w-32 shrink-0" }, label),
38
+ React.createElement("label", { className: "text-xs text-muted w-32 shrink-0", title: tooltip }, label),
39
39
  React.createElement("div", { className: "flex-1" }, children)));
40
40
  }
41
41
  /**
42
42
  * Titled section wrapper for grouping related edit fields.
43
43
  * @public
44
44
  */
45
- export function EditSection({ title, children }) {
45
+ export function EditSection({ title, tooltip, children }) {
46
46
  return (React.createElement("div", { className: "mb-4" },
47
- React.createElement("h4", { className: "text-xs font-medium text-muted uppercase tracking-wider mb-1.5" }, title),
47
+ React.createElement("h4", { className: "text-xs font-medium text-muted uppercase tracking-wider mb-1.5", title: tooltip }, title),
48
48
  children));
49
49
  }
50
50
  /**
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EditFieldHelpers.js","sourceRoot":"","sources":["../../../src/packlets/editing/EditFieldHelpers.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Generic edit field helper components for entity editors.\n *\n * Provides reusable form field primitives: text inputs, number inputs,\n * select dropdowns, tag editors, checkboxes, and layout wrappers.\n * These components are domain-agnostic and can be used by any entity editor.\n *\n * @packageDocumentation\n */\n\nimport React from 'react';\n\n// ============================================================================\n// Layout Helpers\n// ============================================================================\n\n/**\n * Props for the EditField component.\n * @public\n */\nexport interface IEditFieldProps {\n /** Label text displayed to the left of the field */\n readonly label: string;\n /** Optional tooltip shown on hover over the label */\n readonly tooltip?: string;\n /** The input control(s) to render */\n readonly children: React.ReactNode;\n}\n\n/**\n * Horizontal label + field layout for a single edit field.\n * @public\n */\nexport function EditField({ label, tooltip, children }: IEditFieldProps): React.ReactElement {\n return (\n <div className=\"flex items-baseline gap-2 py-1\">\n <label className=\"text-xs text-muted w-32 shrink-0\" title={tooltip}>\n {label}\n </label>\n <div className=\"flex-1\">{children}</div>\n </div>\n );\n}\n\n/**\n * Props for the EditSection component.\n * @public\n */\nexport interface IEditSectionProps {\n /** Section heading text */\n readonly title: string;\n /** Optional tooltip shown on hover over the section heading */\n readonly tooltip?: string;\n /** Section content (typically EditField components) */\n readonly children: React.ReactNode;\n}\n\n/**\n * Titled section wrapper for grouping related edit fields.\n * @public\n */\nexport function EditSection({ title, tooltip, children }: IEditSectionProps): React.ReactElement {\n return (\n <div className=\"mb-4\">\n <h4 className=\"text-xs font-medium text-muted uppercase tracking-wider mb-1.5\" title={tooltip}>\n {title}\n </h4>\n {children}\n </div>\n );\n}\n\n// ============================================================================\n// Text Inputs\n// ============================================================================\n\n/**\n * Props for the TextInput component.\n * @public\n */\nexport interface ITextInputProps {\n /** Current value */\n readonly value: string;\n /** Called when value changes */\n readonly onChange: (value: string) => void;\n /** Placeholder text */\n readonly placeholder?: string;\n}\n\n/**\n * Single-line text input for required string fields.\n * @public\n */\nexport function TextInput({ value, onChange, placeholder }: ITextInputProps): React.ReactElement {\n return (\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder={placeholder}\n className=\"w-full px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring\"\n />\n );\n}\n\n/**\n * Props for the OptionalTextInput component.\n * @public\n */\nexport interface IOptionalTextInputProps {\n /** Current value (undefined means empty) */\n readonly value: string | undefined;\n /** Called when value changes (empty string → undefined) */\n readonly onChange: (value: string | undefined) => void;\n /** Placeholder text */\n readonly placeholder?: string;\n}\n\n/**\n * Single-line text input for optional string fields.\n * Empty input produces `undefined`.\n * @public\n */\nexport function OptionalTextInput({\n value,\n onChange,\n placeholder\n}: IOptionalTextInputProps): React.ReactElement {\n return (\n <input\n type=\"text\"\n value={value ?? ''}\n onChange={(e) => onChange(e.target.value || undefined)}\n placeholder={placeholder}\n className=\"w-full px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring\"\n />\n );\n}\n\n/**\n * Props for the TextAreaInput component.\n * @public\n */\nexport interface ITextAreaInputProps {\n /** Current value (undefined means empty) */\n readonly value: string | undefined;\n /** Called when value changes (empty string → undefined) */\n readonly onChange: (value: string | undefined) => void;\n /** Placeholder text */\n readonly placeholder?: string;\n}\n\n/**\n * Multi-line text input for optional string fields.\n * Empty input produces `undefined`.\n * @public\n */\nexport function TextAreaInput({ value, onChange, placeholder }: ITextAreaInputProps): React.ReactElement {\n return (\n <textarea\n value={value ?? ''}\n onChange={(e) => onChange(e.target.value || undefined)}\n placeholder={placeholder}\n rows={3}\n className=\"w-full px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring resize-y\"\n />\n );\n}\n\n// ============================================================================\n// Number Input\n// ============================================================================\n\n/**\n * Props for the NumberInput component.\n * @public\n */\nexport interface INumberInputProps {\n /** Current value (undefined means empty) */\n readonly value: number | undefined;\n /** Called when value changes (empty input → undefined) */\n readonly onChange: (value: number | undefined) => void;\n /** Accessible label for the input */\n readonly label: string;\n /** Minimum allowed value */\n readonly min?: number;\n /** Maximum allowed value */\n readonly max?: number;\n /** Step increment */\n readonly step?: number;\n}\n\n/**\n * Numeric input for optional number fields.\n * Empty input produces `undefined`.\n * @public\n */\nexport function NumberInput({\n value,\n onChange,\n label,\n min,\n max,\n step\n}: INumberInputProps): React.ReactElement {\n return (\n <input\n type=\"number\"\n value={value ?? ''}\n onChange={(e) => {\n const raw = e.target.value;\n if (!raw.trim()) {\n onChange(undefined);\n } else {\n const num = parseFloat(raw);\n if (!isNaN(num)) {\n onChange(num);\n }\n }\n }}\n min={min}\n max={max}\n step={step ?? 0.1}\n className=\"w-24 px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring text-right\"\n aria-label={label}\n />\n );\n}\n\n// ============================================================================\n// Select Input\n// ============================================================================\n\n/**\n * Props for the SelectInput component.\n * @public\n */\nexport interface ISelectInputProps<T extends string> {\n /** Currently selected value */\n readonly value: T;\n /** Available options */\n readonly options: ReadonlyArray<T>;\n /** Called when selection changes */\n readonly onChange: (value: T) => void;\n}\n\n/**\n * Dropdown select for enumerated string values.\n * @public\n */\nexport function SelectInput<T extends string>({\n value,\n options,\n onChange\n}: ISelectInputProps<T>): React.ReactElement {\n return (\n <select\n value={value}\n onChange={(e) => onChange(e.target.value as T)}\n className=\"px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring\"\n >\n {options.map((opt) => (\n <option key={opt} value={opt}>\n {opt}\n </option>\n ))}\n </select>\n );\n}\n\n// ============================================================================\n// Tags Input\n// ============================================================================\n\n/**\n * Props for the TagsInput component.\n * @public\n */\nexport interface ITagsInputProps {\n /** Current tags (undefined means no tags) */\n readonly value: ReadonlyArray<string> | undefined;\n /** Called when tags change (empty → undefined) */\n readonly onChange: (value: ReadonlyArray<string> | undefined) => void;\n /** Placeholder text */\n readonly placeholder?: string;\n}\n\n/**\n * Comma-separated tag editor for string arrays.\n * Empty input produces `undefined`.\n * @public\n */\nexport function TagsInput({ value, onChange, placeholder }: ITagsInputProps): React.ReactElement {\n const text = value?.join(', ') ?? '';\n return (\n <input\n type=\"text\"\n value={text}\n onChange={(e) => {\n const raw = e.target.value;\n if (!raw.trim()) {\n onChange(undefined);\n } else {\n onChange(\n raw\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n );\n }\n }}\n placeholder={placeholder ?? 'comma-separated values'}\n className=\"w-full px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring\"\n />\n );\n}\n\n// ============================================================================\n// Checkbox Input\n// ============================================================================\n\n/**\n * Props for the CheckboxInput component.\n * @public\n */\nexport interface ICheckboxInputProps {\n /** Current value (undefined treated as false) */\n readonly value: boolean | undefined;\n /** Called when value changes (false → undefined) */\n readonly onChange: (value: boolean | undefined) => void;\n /** Label text displayed next to the checkbox */\n readonly label: string;\n}\n\n/**\n * Checkbox input for optional boolean fields.\n * Unchecked produces `undefined`.\n * @public\n */\nexport function CheckboxInput({ value, onChange, label }: ICheckboxInputProps): React.ReactElement {\n return (\n <label className=\"flex items-center gap-2 text-sm text-secondary cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={value ?? false}\n onChange={(e) => onChange(e.target.checked || undefined)}\n className=\"rounded border-border text-brand-primary focus:ring-focus-ring\"\n />\n {label}\n </label>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultiActionButton.js","sourceRoot":"","sources":["../../../src/packlets/editing/MultiActionButton.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Multi-action button component with dropdown for alternative actions.\n * @packageDocumentation\n */\n\nimport React, { useState, useRef, useEffect } from 'react';\nimport { ChevronDownIcon } from '@heroicons/react/20/solid';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Action definition for multi-action button.\n * @public\n */\nexport interface IMultiActionButtonAction {\n /** Unique identifier for the action. */\n readonly id: string;\n /** Display label for the action. */\n readonly label: string;\n /** Optional icon element to display before the label. */\n readonly icon?: React.ReactNode;\n /** Callback when this action is selected. */\n readonly onSelect: () => void;\n}\n\n/**\n * Props for MultiActionButton component.\n * @public\n */\nexport interface IMultiActionButtonProps {\n /** Primary action that appears on the main button. */\n readonly primaryAction: IMultiActionButtonAction;\n /** Alternative actions shown in the dropdown menu. */\n readonly alternativeActions: readonly IMultiActionButtonAction[];\n /** Optional variant for button styling. */\n readonly variant?: 'primary' | 'default' | 'danger';\n /** If true, the primary action button is disabled. */\n readonly disabled?: boolean;\n /** If true, the dropdown toggle is disabled. Defaults to `disabled`. */\n readonly dropdownDisabled?: boolean;\n /** Optional CSS class name. */\n readonly className?: string;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * Multi-action button with dropdown for alternative actions.\n * Similar to Git commit button with \"Commit\", \"Commit & Push\", etc.\n *\n * @public\n */\nexport function MultiActionButton(props: IMultiActionButtonProps): React.ReactElement {\n const {\n primaryAction,\n alternativeActions,\n variant = 'primary',\n disabled = false,\n dropdownDisabled,\n className = ''\n } = props;\n const isDropdownDisabled = dropdownDisabled ?? disabled;\n const [isOpen, setIsOpen] = useState(false);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n // Close dropdown when clicking outside\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent): void => {\n if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {\n setIsOpen(false);\n }\n };\n\n if (isOpen) {\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }\n }, [isOpen]);\n\n const variantClasses: Record<string, string> = {\n primary: 'bg-brand-primary hover:bg-brand-primary/90 text-white',\n default: 'bg-surface-raised hover:bg-surface-raised text-secondary',\n danger: 'bg-status-error-btn hover:bg-status-error-btn-hover text-white'\n };\n\n const baseClasses =\n 'inline-flex items-center text-xs font-medium rounded transition-colors disabled:opacity-40 disabled:cursor-not-allowed';\n\n return (\n <div className={`relative inline-flex ${className}`} ref={dropdownRef}>\n {/* Primary action button */}\n <button\n type=\"button\"\n onClick={primaryAction.onSelect}\n disabled={disabled}\n className={`${baseClasses} ${variantClasses[variant]} px-2.5 py-1 ${\n alternativeActions.length > 0 ? 'rounded-r-none' : 'rounded'\n }`}\n >\n {primaryAction.icon}\n <span>{primaryAction.label}</span>\n </button>\n\n {/* Dropdown toggle button — only shown when there are alternative actions */}\n {alternativeActions.length > 0 && (\n <button\n type=\"button\"\n onClick={(): void => setIsOpen(!isOpen)}\n disabled={isDropdownDisabled}\n className={`${baseClasses} ${variantClasses[variant]} px-1 py-1 rounded-l-none border-l border-white/20`}\n >\n <ChevronDownIcon className=\"h-3.5 w-3.5\" />\n </button>\n )}\n\n {/* Dropdown menu */}\n {isOpen && alternativeActions.length > 0 && (\n <div className=\"absolute top-full right-0 mt-1 w-48 bg-surface rounded-md shadow-lg border border-border py-1 z-50\">\n {alternativeActions.map((action) => (\n <button\n key={action.id}\n type=\"button\"\n onClick={(): void => {\n action.onSelect();\n setIsOpen(false);\n }}\n className=\"w-full text-left px-3 py-2 text-xs text-secondary hover:bg-surface-raised flex items-center gap-2\"\n >\n {action.icon}\n <span>{action.label}</span>\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NumericInput.js","sourceRoot":"","sources":["../../../src/packlets/editing/NumericInput.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Numeric input with select-all-on-focus and empty-string support.\n *\n * Uses `type=\"text\"` with `inputMode=\"decimal\"` to avoid native number-input\n * quirks (spinner buttons, empty-string rejection). Manages a string internally\n * but exposes `number | undefined` to callers.\n *\n * @packageDocumentation\n */\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst NUMERIC_PATTERN: RegExp = /^-?\\d*\\.?\\d*$/;\n\nconst DEFAULT_CLASS: string =\n 'text-sm border border-border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-focus-ring focus:border-focus-ring';\n\n// ============================================================================\n// Props\n// ============================================================================\n\n/**\n * Props for the {@link NumericInput} component.\n * @public\n */\nexport interface INumericInputProps {\n /** Current numeric value (`undefined` = empty field). */\n readonly value: number | undefined;\n /** Called with the parsed number on blur, or `undefined` if the field is empty. */\n readonly onChange: (value: number | undefined) => void;\n /** Accessible label (maps to `aria-label`). */\n readonly label?: string;\n /** Minimum allowed value (clamped on blur). */\n readonly min?: number;\n /** Maximum allowed value (clamped on blur). */\n readonly max?: number;\n /** Step for arrow-key increment/decrement. Defaults to 1. */\n readonly step?: number;\n /** Additional or replacement CSS classes. */\n readonly className?: string;\n /** Placeholder text shown when the field is empty. */\n readonly placeholder?: string;\n /** Auto-focus the input on mount. */\n readonly autoFocus?: boolean;\n /** Disabled state. */\n readonly disabled?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number | undefined): string {\n return value !== undefined ? String(value) : '';\n}\n\nfunction clamp(value: number, min: number | undefined, max: number | undefined): number {\n let result = value;\n if (min !== undefined && result < min) {\n result = min;\n }\n if (max !== undefined && result > max) {\n result = max;\n }\n return result;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * Numeric input that selects all on focus and allows empty values.\n *\n * Internally manages a string so the user can freely type, backspace to empty,\n * and replace the entire value. On blur, the string is parsed to a number\n * (clamped to min/max) and reported to the parent via `onChange`.\n *\n * @public\n */\nexport function NumericInput(props: INumericInputProps): React.ReactElement {\n const { value, onChange, label, min, max, step, className, placeholder, autoFocus, disabled } = props;\n\n const [displayValue, setDisplayValue] = useState(() => formatValue(value));\n const focusedRef = useRef(false);\n\n // Sync display when value prop changes externally (but not while focused)\n useEffect(() => {\n if (!focusedRef.current) {\n setDisplayValue(formatValue(value));\n }\n }, [value]);\n\n const handleFocus = useCallback((e: React.FocusEvent<HTMLInputElement>): void => {\n focusedRef.current = true;\n e.target.select();\n }, []);\n\n const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>): void => {\n const raw = e.target.value;\n if (raw === '' || NUMERIC_PATTERN.test(raw)) {\n setDisplayValue(raw);\n }\n }, []);\n\n const handleBlur = useCallback((): void => {\n focusedRef.current = false;\n const trimmed = displayValue.trim();\n if (trimmed === '' || trimmed === '-' || trimmed === '.') {\n setDisplayValue('');\n onChange(undefined);\n return;\n }\n const parsed = parseFloat(trimmed);\n if (isNaN(parsed)) {\n setDisplayValue('');\n onChange(undefined);\n return;\n }\n const clamped = clamp(parsed, min, max);\n setDisplayValue(String(clamped));\n onChange(clamped);\n }, [displayValue, onChange, min, max]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLInputElement>): void => {\n if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {\n return;\n }\n e.preventDefault();\n const effectiveStep = step ?? 1;\n const current = parseFloat(displayValue) || 0;\n const next = e.key === 'ArrowUp' ? current + effectiveStep : current - effectiveStep;\n const clamped = clamp(next, min, max);\n // Round to avoid floating-point drift\n const rounded = parseFloat(clamped.toFixed(10));\n setDisplayValue(String(rounded));\n onChange(rounded);\n },\n [displayValue, onChange, min, max, step]\n );\n\n return (\n <input\n type=\"text\"\n inputMode=\"decimal\"\n value={displayValue}\n onChange={handleChange}\n onFocus={handleFocus}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n className={className ?? DEFAULT_CLASS}\n aria-label={label}\n placeholder={placeholder}\n autoFocus={autoFocus}\n disabled={disabled}\n />\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TypeaheadInput.js","sourceRoot":"","sources":["../../../src/packlets/editing/TypeaheadInput.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * TypeaheadInput — text input with custom autocomplete dropdown supporting\n * tiered suggestions and built-in blur resolution.\n * @packageDocumentation\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { type ITypeaheadSuggestion, useTypeaheadMatch } from './useTypeaheadMatch';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Props for the TypeaheadInput component.\n * @public\n */\nexport interface ITypeaheadInputProps<TId extends string = string> {\n /** Current text value (controlled). */\n readonly value: string;\n /** Called on every keystroke. */\n readonly onChange: (value: string) => void;\n /** Full catalog of suggestions (lower priority). */\n readonly suggestions: ReadonlyArray<ITypeaheadSuggestion<TId>>;\n /** Optional priority suggestions shown first (e.g. recipe alternates). */\n readonly prioritySuggestions?: ReadonlyArray<ITypeaheadSuggestion<TId>>;\n /** Called when a suggestion is definitively selected (click, Enter, blur auto-resolve). */\n readonly onSelect: (suggestion: ITypeaheadSuggestion<TId>) => void;\n /** Called on blur/Enter/Tab when input cannot be resolved to a single suggestion. */\n readonly onUnresolved?: (text: string) => void;\n /** Placeholder text for the input. */\n readonly placeholder?: string;\n /** Additional CSS class for the input element. */\n readonly className?: string;\n /** If true, the input is disabled. */\n readonly disabled?: boolean;\n /** Maximum dropdown height in pixels. Default: 240. */\n readonly maxHeight?: number;\n /** If true, the input is focused on mount. */\n readonly autoFocus?: boolean;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/**\n * Text input with a custom autocomplete dropdown supporting tiered suggestions.\n *\n * Priority suggestions (e.g. recipe alternates) appear first, visually separated\n * from the full catalog. On blur, applies resolution logic: exact match → auto-select,\n * single partial match → auto-select, else fires `onUnresolved`.\n *\n * @public\n */\nexport function TypeaheadInput<TId extends string = string>(\n props: ITypeaheadInputProps<TId>\n): React.ReactElement {\n const {\n value,\n onChange,\n suggestions,\n prioritySuggestions,\n onSelect,\n onUnresolved,\n placeholder,\n className,\n disabled,\n maxHeight = 240,\n autoFocus\n } = props;\n\n const [isOpen, setIsOpen] = useState(false);\n const [focusIndex, setFocusIndex] = useState(-1);\n const inputRef = useRef<HTMLInputElement>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const mouseDownOnDropdownRef = useRef(false);\n\n const matcher = useTypeaheadMatch(suggestions, prioritySuggestions);\n\n // ---- Filtered suggestions ----\n\n const filtered = useMemo(() => matcher.filterSuggestions(value), [matcher, value]);\n\n const flatItems = useMemo(\n () => [...filtered.priority, ...filtered.catalog],\n [filtered.priority, filtered.catalog]\n );\n\n const hasPriorityItems = filtered.priority.length > 0;\n const hasCatalogItems = filtered.catalog.length > 0;\n const hasItems = flatItems.length > 0;\n\n // Reset focus index when suggestions change\n useEffect(() => {\n setFocusIndex(-1);\n }, [value]);\n\n // Scroll focused item into view\n useEffect(() => {\n if (isOpen && focusIndex >= 0 && itemRefs.current[focusIndex]) {\n itemRefs.current[focusIndex]?.scrollIntoView({ block: 'nearest' });\n }\n }, [isOpen, focusIndex]);\n\n // Close dropdown on outside click\n useEffect(() => {\n if (!isOpen) return;\n const handler = (e: MouseEvent): void => {\n if (\n dropdownRef.current &&\n !dropdownRef.current.contains(e.target as Node) &&\n inputRef.current &&\n !inputRef.current.contains(e.target as Node)\n ) {\n setIsOpen(false);\n }\n };\n document.addEventListener('mousedown', handler);\n return (): void => {\n document.removeEventListener('mousedown', handler);\n };\n }, [isOpen]);\n\n // ---- Selection ----\n\n const handleSelect = useCallback(\n (item: ITypeaheadSuggestion<TId>): void => {\n onSelect(item);\n setIsOpen(false);\n setFocusIndex(-1);\n },\n [onSelect]\n );\n\n const handleResolve = useCallback(\n (text: string): void => {\n const trimmed = text.trim();\n if (!trimmed) {\n setIsOpen(false);\n return;\n }\n const match = matcher.resolveOnBlur(trimmed);\n if (match) {\n handleSelect(match);\n } else if (onUnresolved) {\n onUnresolved(trimmed);\n setIsOpen(false);\n } else {\n setIsOpen(false);\n }\n },\n [matcher, handleSelect, onUnresolved]\n );\n\n // ---- Input handlers ----\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>): void => {\n onChange(e.target.value);\n if (!isOpen) {\n setIsOpen(true);\n }\n },\n [onChange, isOpen]\n );\n\n const handleFocus = useCallback((): void => {\n inputRef.current?.select();\n if (hasItems) {\n setIsOpen(true);\n }\n }, [hasItems]);\n\n const handleBlur = useCallback((): void => {\n // Guard: don't resolve if user is clicking an item in the dropdown\n if (mouseDownOnDropdownRef.current) {\n mouseDownOnDropdownRef.current = false;\n return;\n }\n handleResolve(value);\n }, [value, handleResolve]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent): void => {\n if (!isOpen) {\n if (e.key === 'ArrowDown' && hasItems) {\n e.preventDefault();\n setIsOpen(true);\n setFocusIndex(0);\n }\n return;\n }\n\n switch (e.key) {\n case 'Escape': {\n e.preventDefault();\n e.stopPropagation();\n setIsOpen(false);\n setFocusIndex(-1);\n break;\n }\n case 'ArrowDown': {\n e.preventDefault();\n setFocusIndex((prev) => (prev < flatItems.length - 1 ? prev + 1 : 0));\n break;\n }\n case 'ArrowUp': {\n e.preventDefault();\n setFocusIndex((prev) => (prev > 0 ? prev - 1 : flatItems.length - 1));\n break;\n }\n case 'Enter': {\n e.preventDefault();\n if (focusIndex >= 0 && focusIndex < flatItems.length) {\n handleSelect(flatItems[focusIndex]);\n } else {\n handleResolve(value);\n }\n break;\n }\n case 'Tab': {\n // Allow default tab behavior but resolve first\n handleResolve(value);\n break;\n }\n default:\n break;\n }\n },\n [isOpen, hasItems, flatItems, focusIndex, handleSelect, handleResolve, value]\n );\n\n // ---- Dropdown mouse guard ----\n\n const handleDropdownMouseDown = useCallback((): void => {\n mouseDownOnDropdownRef.current = true;\n }, []);\n\n // ---- Render ----\n\n const priorityEndIndex = filtered.priority.length;\n\n return (\n <div className=\"relative\">\n <input\n ref={inputRef}\n type=\"text\"\n value={value}\n onChange={handleChange}\n onFocus={handleFocus}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled}\n autoFocus={autoFocus}\n className={\n className ??\n 'w-full px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring'\n }\n role=\"combobox\"\n aria-expanded={isOpen}\n aria-autocomplete=\"list\"\n autoComplete=\"off\"\n />\n\n {isOpen && hasItems && (\n <div\n ref={dropdownRef}\n className=\"absolute z-50 mt-1 w-full bg-surface border border-border rounded-lg shadow-lg overflow-hidden\"\n style={{ maxHeight }}\n role=\"listbox\"\n onMouseDown={handleDropdownMouseDown}\n >\n <div className=\"overflow-y-auto\" style={{ maxHeight }}>\n {/* Priority items */}\n {hasPriorityItems &&\n filtered.priority.map((item, index) => {\n const isFocused = index === focusIndex;\n return (\n <button\n key={item.id}\n ref={(el): void => {\n itemRefs.current[index] = el;\n }}\n role=\"option\"\n aria-selected={isFocused}\n onClick={(): void => handleSelect(item)}\n className={`flex items-center w-full px-2.5 py-1.5 text-left text-sm transition-colors border-l-2 ${\n isFocused\n ? 'bg-surface-alt text-primary border-brand-primary/50'\n : 'text-secondary hover:bg-hover border-brand-primary/20'\n }`}\n >\n <span className=\"flex-1 min-w-0 truncate\">{item.name}</span>\n </button>\n );\n })}\n\n {/* Separator */}\n {hasPriorityItems && hasCatalogItems && <div className=\"border-t border-border my-0.5\" />}\n\n {/* Catalog items */}\n {hasCatalogItems &&\n filtered.catalog.map((item, catalogIndex) => {\n const flatIndex = priorityEndIndex + catalogIndex;\n const isFocused = flatIndex === focusIndex;\n return (\n <button\n key={item.id}\n ref={(el): void => {\n itemRefs.current[flatIndex] = el;\n }}\n role=\"option\"\n aria-selected={isFocused}\n onClick={(): void => handleSelect(item)}\n className={`flex items-center w-full px-2.5 py-1.5 text-left text-sm transition-colors ${\n isFocused ? 'bg-surface-alt text-primary' : 'text-secondary hover:bg-hover'\n }`}\n >\n <span className=\"flex-1 min-w-0 truncate\">{item.name}</span>\n </button>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/editing/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,SAAS,EAET,WAAW,EAEX,SAAS,EAET,iBAAiB,EAEjB,aAAa,EAEb,WAAW,EAEX,WAAW,EAEX,SAAS,EAET,aAAa,EAEd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,iBAAiB,EAGlB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,YAAY,EAA2B,MAAM,gBAAgB,CAAC;AAEvE,OAAO,EAAE,cAAc,EAA6B,MAAM,kBAAkB,CAAC;AAE7E,OAAO,EACL,iBAAiB,EAIlB,MAAM,qBAAqB,CAAC","sourcesContent":["/**\n * Editing packlet - generic form field primitives for entity editors.\n * @packageDocumentation\n */\n\nexport {\n EditField,\n type IEditFieldProps,\n EditSection,\n type IEditSectionProps,\n TextInput,\n type ITextInputProps,\n OptionalTextInput,\n type IOptionalTextInputProps,\n TextAreaInput,\n type ITextAreaInputProps,\n NumberInput,\n type INumberInputProps,\n SelectInput,\n type ISelectInputProps,\n TagsInput,\n type ITagsInputProps,\n CheckboxInput,\n type ICheckboxInputProps\n} from './EditFieldHelpers';\n\nexport {\n MultiActionButton,\n type IMultiActionButtonProps,\n type IMultiActionButtonAction\n} from './MultiActionButton';\n\nexport { NumericInput, type INumericInputProps } from './NumericInput';\n\nexport { TypeaheadInput, type ITypeaheadInputProps } from './TypeaheadInput';\n\nexport {\n useTypeaheadMatch,\n type ITypeaheadSuggestion,\n type ITypeaheadMatchResult,\n type IFilteredSuggestions\n} from './useTypeaheadMatch';\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useTypeaheadMatch.js","sourceRoot":"","sources":["../../../src/packlets/editing/useTypeaheadMatch.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Reusable typeahead matching hook with tiered priority support.\n * @packageDocumentation\n */\n\nimport { useCallback, useMemo } from 'react';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * A suggestion entry for typeahead matching.\n * @public\n */\nexport interface ITypeaheadSuggestion<TId extends string = string> {\n readonly id: TId;\n readonly name: string;\n}\n\n/**\n * Filtered suggestions split by tier.\n * @public\n */\nexport interface IFilteredSuggestions<TId extends string> {\n readonly priority: ReadonlyArray<ITypeaheadSuggestion<TId>>;\n readonly catalog: ReadonlyArray<ITypeaheadSuggestion<TId>>;\n}\n\n/**\n * Result of the useTypeaheadMatch hook.\n * @public\n */\nexport interface ITypeaheadMatchResult<TId extends string> {\n /** Find an exact match by id or case-insensitive name. Checks priority first. */\n findExactMatch: (input: string) => ITypeaheadSuggestion<TId> | undefined;\n /** Resolve on blur: exact match → use it; single partial → use it; else undefined. */\n resolveOnBlur: (input: string) => ITypeaheadSuggestion<TId> | undefined;\n /** Filter suggestions by input text, returning priority and catalog tiers separately. */\n filterSuggestions: (input: string) => IFilteredSuggestions<TId>;\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/**\n * Hook that provides typeahead matching and filtering with tiered priority support.\n *\n * `findExactMatch` matches by exact id or case-insensitive name (checks priority first).\n * `resolveOnBlur` tries exact match, then single partial match (priority first).\n * `filterSuggestions` returns suggestions split by tier, filtered by substring match.\n *\n * @param suggestions - The full catalog of suggestions\n * @param prioritySuggestions - Optional priority suggestions shown first (e.g. recipe alternates)\n * @returns Match and filter functions\n * @public\n */\nexport function useTypeaheadMatch<TId extends string>(\n suggestions: ReadonlyArray<ITypeaheadSuggestion<TId>>,\n prioritySuggestions?: ReadonlyArray<ITypeaheadSuggestion<TId>>\n): ITypeaheadMatchResult<TId> {\n // Pre-compute priority IDs for efficient catalog deduplication\n const priorityIds = useMemo(\n () => new Set(prioritySuggestions?.map((s) => s.id) ?? []),\n [prioritySuggestions]\n );\n\n const findExactMatch = useCallback(\n (input: string): ITypeaheadSuggestion<TId> | undefined => {\n const trimmed = input.trim();\n if (!trimmed) return undefined;\n const lower = trimmed.toLowerCase();\n if (prioritySuggestions) {\n const match = prioritySuggestions.find((s) => s.id === trimmed || s.name.toLowerCase() === lower);\n if (match) return match;\n }\n return suggestions.find((s) => s.id === trimmed || s.name.toLowerCase() === lower);\n },\n [suggestions, prioritySuggestions]\n );\n\n const resolveOnBlur = useCallback(\n (input: string): ITypeaheadSuggestion<TId> | undefined => {\n const trimmed = input.trim();\n if (!trimmed) return undefined;\n const lower = trimmed.toLowerCase();\n\n // Try exact match (priority first, then all)\n if (prioritySuggestions) {\n const priorityExact = prioritySuggestions.find(\n (s) => s.id === trimmed || s.name.toLowerCase() === lower\n );\n if (priorityExact) return priorityExact;\n }\n\n const exact = suggestions.find((s) => s.id === trimmed || s.name.toLowerCase() === lower);\n if (exact) return exact;\n\n // Try partial match in priority suggestions first\n if (prioritySuggestions) {\n const priorityPartials = prioritySuggestions.filter(\n (s) => s.name.toLowerCase().includes(lower) || s.id.toLowerCase().includes(lower)\n );\n if (priorityPartials.length === 1) return priorityPartials[0];\n }\n\n // Fall back to partial match in all suggestions\n const partials = suggestions.filter(\n (s) => s.name.toLowerCase().includes(lower) || s.id.toLowerCase().includes(lower)\n );\n if (partials.length === 1) return partials[0];\n\n return undefined;\n },\n [suggestions, prioritySuggestions]\n );\n\n const filterSuggestions = useCallback(\n (input: string): IFilteredSuggestions<TId> => {\n const trimmed = input.trim();\n const lower = trimmed.toLowerCase();\n\n // No input — return all suggestions split by tier\n if (!trimmed) {\n return {\n priority: prioritySuggestions ?? [],\n catalog: suggestions.filter((s) => !priorityIds.has(s.id))\n };\n }\n\n // Filter by substring match\n const matchesSuggestion = (s: ITypeaheadSuggestion<TId>): boolean =>\n s.name.toLowerCase().includes(lower) || s.id.toLowerCase().includes(lower);\n\n const priority = prioritySuggestions?.filter(matchesSuggestion) ?? [];\n const catalog = suggestions.filter((s) => !priorityIds.has(s.id) && matchesSuggestion(s));\n\n return { priority, catalog };\n },\n [suggestions, prioritySuggestions, priorityIds]\n );\n\n return { findExactMatch, resolveOnBlur, filterSuggestions };\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/keyboard/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAIL,wBAAwB,EACxB,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EAEL,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC","sourcesContent":["/**\n * Keyboard shortcut packlet - registry, context, and hooks.\n * @packageDocumentation\n */\n\nexport {\n type IKeyBinding,\n type IShortcut,\n type IShortcutRegistration,\n KeyboardShortcutRegistry,\n matchesBinding\n} from './registry';\n\nexport {\n type IKeyboardShortcutProviderProps,\n KeyboardShortcutProvider,\n useKeyboardRegistry,\n useKeyboardShortcuts\n} from './useKeyboardShortcuts';\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/packlets/keyboard/registry.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Keyboard shortcut registry infrastructure.\n *\n * Provides a centralized registry for keyboard shortcuts with:\n * - Modifier-aware key matching (Cmd/Ctrl, Shift, Alt)\n * - Priority-based handler resolution (higher priority wins)\n * - React hook for component-scoped shortcut registration\n * - Global listener that dispatches to registered handlers\n *\n * @packageDocumentation\n */\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Describes a keyboard shortcut binding.\n * @public\n */\nexport interface IKeyBinding {\n /** The key (e.g., 'k', 'z', 'Escape'). Case-insensitive. */\n readonly key: string;\n /** Require Cmd (Mac) / Ctrl (Windows/Linux) */\n readonly meta?: boolean;\n /** Require Shift */\n readonly shift?: boolean;\n /** Require Alt/Option */\n readonly alt?: boolean;\n}\n\n/**\n * A registered keyboard shortcut.\n * @public\n */\nexport interface IShortcut {\n /** The key binding */\n readonly binding: IKeyBinding;\n /** Human-readable description (for command palette / help) */\n readonly description: string;\n /** Handler function. Return true to indicate the event was handled. */\n readonly handler: () => boolean | void;\n /** Priority (higher wins when multiple shortcuts match). Default: 0 */\n readonly priority?: number;\n}\n\n/**\n * A registration handle returned when a shortcut is registered.\n * Call `unregister()` to remove the shortcut.\n * @public\n */\nexport interface IShortcutRegistration {\n readonly unregister: () => void;\n}\n\n// ============================================================================\n// Key Matching\n// ============================================================================\n\n/**\n * Tests whether a keyboard event matches a key binding.\n * @param event - The keyboard event\n * @param binding - The binding to match against\n * @returns true if the event matches\n * @public\n */\nexport function matchesBinding(event: KeyboardEvent, binding: IKeyBinding): boolean {\n const keyMatch = event.key.toLowerCase() === binding.key.toLowerCase();\n const metaMatch = (binding.meta ?? false) === (event.metaKey || event.ctrlKey);\n const shiftMatch = (binding.shift ?? false) === event.shiftKey;\n const altMatch = (binding.alt ?? false) === event.altKey;\n return keyMatch && metaMatch && shiftMatch && altMatch;\n}\n\n// ============================================================================\n// Registry\n// ============================================================================\n\n/**\n * Centralized keyboard shortcut registry.\n *\n * Components register shortcuts via `register()` and receive a handle\n * to unregister when they unmount. The registry attaches a single global\n * keydown listener that dispatches to the highest-priority matching handler.\n *\n * @public\n */\nexport class KeyboardShortcutRegistry {\n private readonly _shortcuts: Map<string, IShortcut> = new Map();\n private _listener: ((e: KeyboardEvent) => void) | undefined;\n private _nextId: number = 0;\n\n /**\n * Registers a keyboard shortcut.\n * @param shortcut - The shortcut to register\n * @returns A registration handle with an `unregister()` method\n */\n public register(shortcut: IShortcut): IShortcutRegistration {\n const id = `ks-${++this._nextId}`;\n this._shortcuts.set(id, shortcut);\n this._ensureListener();\n\n return {\n unregister: (): void => {\n this._shortcuts.delete(id);\n if (this._shortcuts.size === 0) {\n this._removeListener();\n }\n }\n };\n }\n\n /**\n * Returns all currently registered shortcuts (for command palette / help display).\n */\n public getAll(): ReadonlyArray<IShortcut> {\n return Array.from(this._shortcuts.values());\n }\n\n /**\n * Dispatches a keyboard event to the highest-priority matching handler.\n * @param event - The keyboard event\n * @returns true if a handler consumed the event\n */\n public dispatch(event: KeyboardEvent): boolean {\n // Skip if the event target is an input/textarea/contenteditable\n const target = event.target as HTMLElement | null;\n if (target) {\n const tagName = target.tagName?.toLowerCase();\n if (tagName === 'input' || tagName === 'textarea' || target.isContentEditable) {\n return false;\n }\n }\n\n // Find all matching shortcuts, sorted by priority (highest first)\n const matches = Array.from(this._shortcuts.values())\n .filter((s) => matchesBinding(event, s.binding))\n .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));\n\n for (const shortcut of matches) {\n const result = shortcut.handler();\n if (result !== false) {\n event.preventDefault();\n event.stopPropagation();\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Removes all shortcuts and the global listener.\n */\n public dispose(): void {\n this._shortcuts.clear();\n this._removeListener();\n }\n\n private _ensureListener(): void {\n if (this._listener) {\n return;\n }\n this._listener = (e: KeyboardEvent): void => {\n this.dispatch(e);\n };\n document.addEventListener('keydown', this._listener);\n }\n\n private _removeListener(): void {\n if (this._listener) {\n document.removeEventListener('keydown', this._listener);\n this._listener = undefined;\n }\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useKeyboardShortcuts.js","sourceRoot":"","sources":["../../../src/packlets/keyboard/useKeyboardShortcuts.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;GAGG;AAEH,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAEtE,OAAO,EAAE,wBAAwB,EAAkB,MAAM,YAAY,CAAC;AAEtE,+EAA+E;AAC/E,UAAU;AACV,+EAA+E;AAE/E,MAAM,eAAe,GAAwD,KAAK,CAAC,aAAa,CAE9F,SAAS,CAAC,CAAC;AAcb;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAqC;IAC5E,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,wBAAwB,EAAE,EAAE,EAAE,CAAC,CAAC;IAEnE,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,GAAS,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IACxC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,OAAO,oBAAC,eAAe,CAAC,QAAQ,IAAC,KAAK,EAAE,QAAQ,IAAG,KAAK,CAAC,QAAQ,CAA4B,CAAC;AAChG,CAAC;AAED,+EAA+E;AAC/E,QAAQ;AACR,+EAA+E;AAE/E;;;;GAIG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,GAAG,GAAG,UAAU,CAAC,eAAe,CAAC,CAAC;IACxC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAmC;IACtE,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAC;IACvC,MAAM,YAAY,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IACvC,YAAY,CAAC,OAAO,GAAG,SAAS,CAAC;IAEjC,SAAS,CAAC,GAAG,EAAE;QACb,8EAA8E;QAC9E,6EAA6E;QAC7E,2EAA2E;QAC3E,MAAM,aAAa,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAC/D,QAAQ,CAAC,QAAQ,CAAC;YAChB,IAAI,OAAO;;gBACT,OAAO,MAAA,MAAA,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,0CAAE,OAAO,mCAAI,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;YAC7D,CAAC;YACD,IAAI,WAAW;;gBACb,OAAO,MAAA,MAAA,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,0CAAE,WAAW,mCAAI,EAAE,CAAC;YACxD,CAAC;YACD,IAAI,QAAQ;;gBACV,OAAO,MAAA,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,0CAAE,QAAQ,CAAC;YAC/C,CAAC;YACD,OAAO,EAAE,GAAmB,EAAE;;gBAC5B,OAAO,MAAA,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,0CAAE,OAAO,EAAE,CAAC;YAChD,CAAC;SACF,CAAC,CACH,CAAC;QACF,OAAO,GAAS,EAAE;YAChB,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;gBAChC,GAAG,CAAC,UAAU,EAAE,CAAC;YACnB,CAAC;QACH,CAAC,CAAC;QACF,kFAAkF;QAClF,6EAA6E;IAC/E,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;AACnC,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * React context and hooks for the keyboard shortcut registry.\n * @packageDocumentation\n */\n\nimport React, { useContext, useEffect, useMemo, useRef } from 'react';\n\nimport { KeyboardShortcutRegistry, type IShortcut } from './registry';\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst KeyboardContext: React.Context<KeyboardShortcutRegistry | undefined> = React.createContext<\n KeyboardShortcutRegistry | undefined\n>(undefined);\n\n// ============================================================================\n// Provider\n// ============================================================================\n\n/**\n * Props for the KeyboardShortcutProvider.\n * @public\n */\nexport interface IKeyboardShortcutProviderProps {\n readonly children: React.ReactNode;\n}\n\n/**\n * Provides a KeyboardShortcutRegistry to the component tree.\n * Should be mounted once at the app root.\n * @public\n */\nexport function KeyboardShortcutProvider(props: IKeyboardShortcutProviderProps): React.ReactElement {\n const registry = useMemo(() => new KeyboardShortcutRegistry(), []);\n\n useEffect(() => {\n return (): void => registry.dispose();\n }, [registry]);\n\n return <KeyboardContext.Provider value={registry}>{props.children}</KeyboardContext.Provider>;\n}\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\n/**\n * Returns the keyboard shortcut registry.\n * Must be used within a KeyboardShortcutProvider.\n * @public\n */\nexport function useKeyboardRegistry(): KeyboardShortcutRegistry {\n const ctx = useContext(KeyboardContext);\n if (ctx === undefined) {\n throw new Error('useKeyboardRegistry must be used within a KeyboardShortcutProvider');\n }\n return ctx;\n}\n\n/**\n * Registers one or more keyboard shortcuts for the lifetime of the component.\n * Shortcuts are automatically unregistered on unmount.\n *\n * @example\n * ```tsx\n * useKeyboardShortcuts([\n * {\n * binding: { key: 'k', meta: true },\n * description: 'Open command palette',\n * handler: () => { setCommandPaletteOpen(true); }\n * },\n * {\n * binding: { key: 'z', meta: true },\n * description: 'Undo',\n * handler: () => { workspace.undo(); }\n * }\n * ]);\n * ```\n *\n * @param shortcuts - Array of shortcuts to register\n * @public\n */\nexport function useKeyboardShortcuts(shortcuts: ReadonlyArray<IShortcut>): void {\n const registry = useKeyboardRegistry();\n const shortcutsRef = useRef(shortcuts);\n shortcutsRef.current = shortcuts;\n\n useEffect(() => {\n // Register stable wrapper shortcuts that delegate to the current ref entries.\n // This means the effect only runs when the registry changes or the number of\n // shortcuts changes — not on every render when the array identity changes.\n const registrations = shortcutsRef.current.map((__item, index) =>\n registry.register({\n get binding() {\n return shortcutsRef.current[index]?.binding ?? { key: '' };\n },\n get description() {\n return shortcutsRef.current[index]?.description ?? '';\n },\n get priority() {\n return shortcutsRef.current[index]?.priority;\n },\n handler: (): boolean | void => {\n return shortcutsRef.current[index]?.handler();\n }\n })\n );\n return (): void => {\n for (const reg of registrations) {\n reg.unregister();\n }\n };\n // Re-register only when the registry instance or the number of shortcuts changes.\n // Handler/binding updates are picked up via the ref without re-registration.\n }, [registry, shortcuts.length]);\n}\n"]}