@lattice-php/lattice 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/README.md +10 -0
  2. package/dist/action/components/action-form.d.ts +6 -1
  3. package/dist/action/components/action-form.js +48 -58
  4. package/dist/action/components/action-form.js.map +1 -1
  5. package/dist/action/components/action-trigger-provider.d.ts +1 -1
  6. package/dist/action/components/action-trigger-provider.js.map +1 -1
  7. package/dist/action/components/action.js +2 -1
  8. package/dist/action/components/action.js.map +1 -1
  9. package/dist/action/hooks/use-action.d.ts +1 -1
  10. package/dist/action/hooks/use-action.js +42 -33
  11. package/dist/action/hooks/use-action.js.map +1 -1
  12. package/dist/action/index.d.ts +0 -2
  13. package/dist/action/index.js +1 -2
  14. package/dist/action/lib/action-label.d.ts +2 -0
  15. package/dist/action/lib/action-label.js +9 -0
  16. package/dist/action/lib/action-label.js.map +1 -0
  17. package/dist/action/lib/run-action.d.ts +7 -7
  18. package/dist/action/lib/run-action.js +8 -7
  19. package/dist/action/lib/run-action.js.map +1 -1
  20. package/dist/chat/plugin.d.ts +0 -1
  21. package/dist/chat/plugin.js.map +1 -1
  22. package/dist/clipboard/index.d.ts +0 -4
  23. package/dist/clipboard/index.js +1 -15
  24. package/dist/{events → core}/event-names.js +1 -1
  25. package/dist/core/event-names.js.map +1 -0
  26. package/dist/core/hooks/use-window-event.d.ts +8 -0
  27. package/dist/core/hooks/use-window-event.js +22 -0
  28. package/dist/core/hooks/use-window-event.js.map +1 -0
  29. package/dist/core/index.js +3 -3
  30. package/dist/core/types.d.ts +8 -73
  31. package/dist/create-app.js +1 -1
  32. package/dist/effects/dispatch.js +1 -1
  33. package/dist/effects/dispatch.js.map +1 -1
  34. package/dist/effects/registry.d.ts +2 -18
  35. package/dist/effects/registry.js +4 -8
  36. package/dist/effects/registry.js.map +1 -1
  37. package/dist/event-bridge.js +10 -23
  38. package/dist/event-bridge.js.map +1 -1
  39. package/dist/form/components/fields/date-picker-field.js +1 -1
  40. package/dist/form/components/fields/file-upload.js +4 -6
  41. package/dist/form/components/fields/file-upload.js.map +1 -1
  42. package/dist/form/components/fields/select.js +2 -2
  43. package/dist/form/components/fields/select.js.map +1 -1
  44. package/dist/form/components/fields/table-rows.js +3 -15
  45. package/dist/form/components/fields/table-rows.js.map +1 -1
  46. package/dist/form/components/form.js +11 -24
  47. package/dist/form/components/form.js.map +1 -1
  48. package/dist/form/embed.d.ts +4 -0
  49. package/dist/form/embed.js +3 -1
  50. package/dist/form/lib/collect-fields.d.ts +7 -0
  51. package/dist/form/lib/collect-fields.js +19 -0
  52. package/dist/form/lib/collect-fields.js.map +1 -0
  53. package/dist/form/lib/field-errors.d.ts +3 -0
  54. package/dist/form/lib/field-errors.js +11 -0
  55. package/dist/form/lib/field-errors.js.map +1 -0
  56. package/dist/format/format-context.d.ts +6 -0
  57. package/dist/format/format-context.js +17 -0
  58. package/dist/format/format-context.js.map +1 -0
  59. package/dist/format/number.d.ts +1 -1
  60. package/dist/format/number.js.map +1 -1
  61. package/dist/format/value.d.ts +1 -1
  62. package/dist/format/value.js.map +1 -1
  63. package/dist/i18n/backend.js +1 -1
  64. package/dist/i18n/index.js +2 -2
  65. package/dist/i18n/instance.js +1 -1
  66. package/dist/i18n/locale-reload.d.ts +1 -1
  67. package/dist/i18n/locale-reload.js +7 -6
  68. package/dist/i18n/locale-reload.js.map +1 -1
  69. package/dist/i18n/locale.js +1 -1
  70. package/dist/i18n/locale.js.map +1 -1
  71. package/dist/i18n/page-props.js +1 -1
  72. package/dist/i18n/shared-props.js +1 -3
  73. package/dist/i18n/shared-props.js.map +1 -1
  74. package/dist/i18n/timezone.js +1 -1
  75. package/dist/i18n/timezone.js.map +1 -1
  76. package/dist/index.d.ts +6 -7
  77. package/dist/index.js +7 -8
  78. package/dist/lattice.css +105 -0
  79. package/dist/layout/components/callouts.js +1 -1
  80. package/dist/layout/components/sidebar.js +15 -48
  81. package/dist/layout/components/sidebar.js.map +1 -1
  82. package/dist/layout/plugin.d.ts +0 -1
  83. package/dist/layout/plugin.js.map +1 -1
  84. package/dist/lib/is-truthy.d.ts +2 -0
  85. package/dist/lib/is-truthy.js +9 -0
  86. package/dist/lib/is-truthy.js.map +1 -0
  87. package/dist/lib/use-debounced-callback.d.ts +10 -0
  88. package/dist/lib/use-debounced-callback.js +34 -0
  89. package/dist/lib/use-debounced-callback.js.map +1 -0
  90. package/dist/lib/use-media-query.d.ts +5 -0
  91. package/dist/lib/use-media-query.js +26 -0
  92. package/dist/lib/use-media-query.js.map +1 -0
  93. package/dist/lib/use-persistent-state.d.ts +13 -0
  94. package/dist/lib/use-persistent-state.js +62 -0
  95. package/dist/lib/use-persistent-state.js.map +1 -0
  96. package/dist/notifications/components/notifications.js +4 -1
  97. package/dist/notifications/components/notifications.js.map +1 -1
  98. package/dist/notifications/plugin.d.ts +0 -1
  99. package/dist/notifications/plugin.js.map +1 -1
  100. package/dist/provider-base.js +1 -1
  101. package/dist/realtime/listeners.d.ts +1 -1
  102. package/dist/realtime/listeners.js +1 -1
  103. package/dist/realtime/listeners.js.map +1 -1
  104. package/dist/registry.d.ts +5 -0
  105. package/dist/registry.js.map +1 -1
  106. package/dist/remote/components/data-list.js +1 -1
  107. package/dist/remote/plugin.d.ts +0 -1
  108. package/dist/remote/plugin.js.map +1 -1
  109. package/dist/table/components/bulk-bar.js +18 -15
  110. package/dist/table/components/bulk-bar.js.map +1 -1
  111. package/dist/table/components/cells/boolean-cell.js +1 -3
  112. package/dist/table/components/cells/boolean-cell.js.map +1 -1
  113. package/dist/table/components/cells/copyable-cell.d.ts +9 -0
  114. package/dist/table/components/cells/copyable-cell.js +17 -0
  115. package/dist/table/components/cells/copyable-cell.js.map +1 -0
  116. package/dist/table/components/cells/money-cell.js +7 -18
  117. package/dist/table/components/cells/money-cell.js.map +1 -1
  118. package/dist/table/components/cells/number-cell.js +14 -27
  119. package/dist/table/components/cells/number-cell.js.map +1 -1
  120. package/dist/table/components/cells/numeric-cell.d.ts +10 -0
  121. package/dist/table/components/cells/numeric-cell.js +25 -0
  122. package/dist/table/components/cells/numeric-cell.js.map +1 -0
  123. package/dist/table/components/cells/stack-cell.js +1 -1
  124. package/dist/table/components/cells/text-cell.js +4 -5
  125. package/dist/table/components/cells/text-cell.js.map +1 -1
  126. package/dist/table/components/filter-controls.js +1 -3
  127. package/dist/table/components/filter-controls.js.map +1 -1
  128. package/dist/table/components/table-cell.js.map +1 -1
  129. package/dist/table/components/table-search.js +4 -7
  130. package/dist/table/components/table-search.js.map +1 -1
  131. package/dist/table/components/table.js +2 -3
  132. package/dist/table/components/table.js.map +1 -1
  133. package/dist/table/hooks/use-column-visibility.js +29 -58
  134. package/dist/table/hooks/use-column-visibility.js.map +1 -1
  135. package/dist/table/hooks/use-table.js +5 -13
  136. package/dist/table/hooks/use-table.js.map +1 -1
  137. package/dist/table/lib/bulk.d.ts +2 -0
  138. package/dist/table/lib/bulk.js +5 -2
  139. package/dist/table/lib/bulk.js.map +1 -1
  140. package/dist/table/lib/query.js +1 -1
  141. package/dist/table/lib/query.js.map +1 -1
  142. package/dist/table/plugin.d.ts +0 -1
  143. package/dist/table/plugin.js.map +1 -1
  144. package/dist/table/types.d.ts +4 -57
  145. package/dist/toast/callout.js +1 -1
  146. package/dist/toast/callout.js.map +1 -1
  147. package/dist/toast/index.d.ts +1 -1
  148. package/dist/toast/index.js +2 -2
  149. package/dist/toast/toast.d.ts +0 -2
  150. package/dist/toast/toast.js +3 -10
  151. package/dist/toast/toast.js.map +1 -1
  152. package/dist/toast/toaster.js +2 -2
  153. package/dist/types/generated.d.ts +224 -80
  154. package/dist/types/index.d.ts +1 -1
  155. package/dist/ui/button.d.ts +2 -2
  156. package/dist/ui/chart-view.js +7 -30
  157. package/dist/ui/chart-view.js.map +1 -1
  158. package/dist/ui/click-behavior.d.ts +4 -4
  159. package/dist/ui/click-behavior.js.map +1 -1
  160. package/dist/ui/collapsible.js +3 -15
  161. package/dist/ui/collapsible.js.map +1 -1
  162. package/dist/ui/combobox.js +6 -3
  163. package/dist/ui/combobox.js.map +1 -1
  164. package/dist/ui/confirm-dialog.js +1 -1
  165. package/dist/ui/confirm-dialog.js.map +1 -1
  166. package/dist/ui/control.d.ts +1 -1
  167. package/dist/ui/dialog.d.ts +6 -2
  168. package/dist/ui/dialog.js +29 -5
  169. package/dist/ui/dialog.js.map +1 -1
  170. package/dist/ui/fragment.js +1 -1
  171. package/dist/ui/fragment.js.map +1 -1
  172. package/dist/ui/icon-button.d.ts +1 -1
  173. package/dist/ui/image-preview.js +1 -1
  174. package/dist/ui/image-preview.js.map +1 -1
  175. package/dist/ui/modal.js +6 -3
  176. package/dist/ui/modal.js.map +1 -1
  177. package/dist/ui/pill.d.ts +2 -0
  178. package/dist/ui/pill.js +10 -0
  179. package/dist/ui/pill.js.map +1 -0
  180. package/dist/ui/plugin.js.map +1 -1
  181. package/dist/ui/progress.js +1 -1
  182. package/dist/ui/section.js +2 -15
  183. package/dist/ui/section.js.map +1 -1
  184. package/dist/ui/segmented-pills.js +2 -1
  185. package/dist/ui/segmented-pills.js.map +1 -1
  186. package/dist/ui/tabs.js +12 -10
  187. package/dist/ui/tabs.js.map +1 -1
  188. package/dist/ui/tree-context.js +11 -13
  189. package/dist/ui/tree-context.js.map +1 -1
  190. package/dist/ui/tree.js +1 -1
  191. package/dist/ui/tree.js.map +1 -1
  192. package/dist/ui/use-collapsible-state.d.ts +7 -0
  193. package/dist/ui/use-collapsible-state.js +21 -0
  194. package/dist/ui/use-collapsible-state.js.map +1 -0
  195. package/dist-standalone/chunks/chart-view-C6R5iPra.js +36 -0
  196. package/dist-standalone/chunks/{date-picker-field-VbH9xrQO.js → date-picker-field-DhhEB-Mh.js} +1 -1
  197. package/dist-standalone/chunks/{page-props-AIa5UR6n.js → page-props-DOfg30l5.js} +1 -1
  198. package/dist-standalone/chunks/{rich-editor-field-DLQTyt36.js → rich-editor-field-DORXTHPw.js} +2 -2
  199. package/dist-standalone/chunks/{subscriptions-DY2tj9Im.js → subscriptions-DK_s_F2g.js} +1 -1
  200. package/dist-standalone/chunks/use-effect-dispatcher-EVZxmfoo.js +97 -0
  201. package/dist-standalone/lattice.css +1 -1
  202. package/dist-standalone/lattice.js +15 -15
  203. package/dist-standalone/manifest.json +8 -8
  204. package/package.json +3 -3
  205. package/dist/clipboard/index.js.map +0 -1
  206. package/dist/events/event-names.js.map +0 -1
  207. package/dist/table/components/table-action-node.d.ts +0 -4
  208. package/dist/table/components/table-action-node.js +0 -24
  209. package/dist/table/components/table-action-node.js.map +0 -1
  210. package/dist-standalone/chunks/chart-view-wJmnVu7L.js +0 -36
  211. package/dist-standalone/chunks/use-effect-dispatcher-feEs0GCP.js +0 -97
  212. /package/dist/{events → core}/event-names.d.ts +0 -0
package/dist/ui/modal.js CHANGED
@@ -1,4 +1,5 @@
1
- import { LATTICE_EVENT } from "../events/event-names.js";
1
+ import { LATTICE_EVENT } from "../core/event-names.js";
2
+ import { useT } from "../i18n/instance.js";
2
3
  import { Dialog, DialogContent, DialogHeader } from "./dialog.js";
3
4
  import { useEffect, useRef, useState } from "react";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -8,7 +9,8 @@ function matchesModal(event, modal) {
8
9
  return target == null || target === modal;
9
10
  }
10
11
  var ModalComponent = ({ children, node }) => {
11
- const title = node.props.title ?? "Dialog";
12
+ const { t } = useT("lattice");
13
+ const title = node.props.title ?? t("common.dialog", "Dialog");
12
14
  const description = node.props.description;
13
15
  const closeLabel = node.props.closeLabel;
14
16
  const [isOpen, setIsOpen] = useState(node.props.open === true);
@@ -46,7 +48,8 @@ var ModalComponent = ({ children, node }) => {
46
48
  onOpenChange: setIsOpen,
47
49
  children: /* @__PURE__ */ jsxs(DialogContent, {
48
50
  ...description ? {} : { "aria-describedby": void 0 },
49
- className: "max-h-[min(680px,calc(100vh-2rem))] w-full max-w-lg overflow-y-auto",
51
+ placement: node.props.side ?? "center",
52
+ width: node.props.width,
50
53
  children: [/* @__PURE__ */ jsx(DialogHeader, {
51
54
  closeLabel,
52
55
  description,
@@ -1 +1 @@
1
- {"version":3,"file":"modal.js","names":[],"sources":["../../resources/js/ui/modal.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { Dialog, DialogContent, DialogHeader } from \"@lattice-php/lattice/ui/dialog\";\nimport type { RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { LATTICE_EVENT } from \"@lattice-php/lattice/events/event-names\";\n\ntype ModalEvent = CustomEvent<{\n modal: string | null;\n}>;\n\nfunction matchesModal(event: Event, modal: string): boolean {\n const target = (event as ModalEvent).detail?.modal;\n\n return target == null || target === modal;\n}\n\nconst ModalComponent: RendererComponent<\"modal\"> = ({ children, node }) => {\n const title = node.props.title ?? \"Dialog\";\n const description = node.props.description;\n const closeLabel = node.props.closeLabel;\n const [isOpen, setIsOpen] = useState(node.props.open === true);\n const openerRef = useRef<HTMLElement | null>(null);\n const wasOpenRef = useRef(isOpen);\n\n // Honour server-driven open changes across re-renders, not just on mount. One\n // way on purpose: closing stays user/closeModal-driven so it never fights a\n // manual close, and the dep keeps it from re-firing while open stays true.\n useEffect(() => {\n if (node.props.open === true) {\n setIsOpen(true);\n }\n }, [node.props.open]);\n\n useEffect(() => {\n function open(event: Event): void {\n if (node.id && matchesModal(event, node.id)) {\n openerRef.current =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n setIsOpen(true);\n }\n }\n\n function close(event: Event): void {\n if (!node.id || matchesModal(event, node.id)) {\n setIsOpen(false);\n }\n }\n\n window.addEventListener(LATTICE_EVENT.openModal, open);\n window.addEventListener(LATTICE_EVENT.closeModal, close);\n\n return () => {\n window.removeEventListener(LATTICE_EVENT.openModal, open);\n window.removeEventListener(LATTICE_EVENT.closeModal, close);\n };\n }, [node.id]);\n\n // Radix only restores focus to a `DialogTrigger`-registered element, and this\n // modal is opened imperatively rather than through one. Own the restore\n // ourselves instead: whatever had focus when the modal opened gets it back\n // once the closing dialog has finished unmounting.\n useEffect(() => {\n if (wasOpenRef.current && !isOpen) {\n const opener = openerRef.current;\n\n if (opener) {\n requestAnimationFrame(() => opener.focus());\n }\n }\n\n wasOpenRef.current = isOpen;\n }, [isOpen]);\n\n return (\n <Dialog open={isOpen} onOpenChange={setIsOpen}>\n <DialogContent\n {...(description ? {} : { \"aria-describedby\": undefined })}\n className=\"max-h-[min(680px,calc(100vh-2rem))] w-full max-w-lg overflow-y-auto\"\n >\n <DialogHeader closeLabel={closeLabel} description={description} title={title} />\n <div className=\"mt-6 space-y-6\">{children}</div>\n </DialogContent>\n </Dialog>\n );\n};\n\nexport default ModalComponent;\n"],"mappings":";;;;;AASA,SAAS,aAAa,OAAc,OAAwB;CAC1D,MAAM,SAAU,MAAqB,QAAQ;CAE7C,OAAO,UAAU,QAAQ,WAAW;AACtC;AAEA,IAAM,kBAA8C,EAAE,UAAU,WAAW;CACzE,MAAM,QAAQ,KAAK,MAAM,SAAS;CAClC,MAAM,cAAc,KAAK,MAAM;CAC/B,MAAM,aAAa,KAAK,MAAM;CAC9B,MAAM,CAAC,QAAQ,aAAa,SAAS,KAAK,MAAM,SAAS,IAAI;CAC7D,MAAM,YAAY,OAA2B,IAAI;CACjD,MAAM,aAAa,OAAO,MAAM;CAKhC,gBAAgB;EACd,IAAI,KAAK,MAAM,SAAS,MACtB,UAAU,IAAI;CAElB,GAAG,CAAC,KAAK,MAAM,IAAI,CAAC;CAEpB,gBAAgB;EACd,SAAS,KAAK,OAAoB;GAChC,IAAI,KAAK,MAAM,aAAa,OAAO,KAAK,EAAE,GAAG;IAC3C,UAAU,UACR,SAAS,yBAAyB,cAAc,SAAS,gBAAgB;IAC3E,UAAU,IAAI;GAChB;EACF;EAEA,SAAS,MAAM,OAAoB;GACjC,IAAI,CAAC,KAAK,MAAM,aAAa,OAAO,KAAK,EAAE,GACzC,UAAU,KAAK;EAEnB;EAEA,OAAO,iBAAiB,cAAc,WAAW,IAAI;EACrD,OAAO,iBAAiB,cAAc,YAAY,KAAK;EAEvD,aAAa;GACX,OAAO,oBAAoB,cAAc,WAAW,IAAI;GACxD,OAAO,oBAAoB,cAAc,YAAY,KAAK;EAC5D;CACF,GAAG,CAAC,KAAK,EAAE,CAAC;CAMZ,gBAAgB;EACd,IAAI,WAAW,WAAW,CAAC,QAAQ;GACjC,MAAM,SAAS,UAAU;GAEzB,IAAI,QACF,4BAA4B,OAAO,MAAM,CAAC;EAE9C;EAEA,WAAW,UAAU;CACvB,GAAG,CAAC,MAAM,CAAC;CAEX,OACE,oBAAC,QAAD;EAAQ,MAAM;EAAQ,cAAc;YAClC,qBAAC,eAAD;GACE,GAAK,cAAc,CAAC,IAAI,EAAE,oBAAoB,KAAA,EAAU;GACxD,WAAU;aAFZ,CAIE,oBAAC,cAAD;IAA0B;IAAyB;IAAoB;GAAQ,CAAA,GAC/E,oBAAC,OAAD;IAAK,WAAU;IAAkB;GAAc,CAAA,CAClC;;CACT,CAAA;AAEZ"}
1
+ {"version":3,"file":"modal.js","names":[],"sources":["../../resources/js/ui/modal.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { Dialog, DialogContent, DialogHeader } from \"@lattice-php/lattice/ui/dialog\";\nimport type { RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { LATTICE_EVENT } from \"@lattice-php/lattice/core/event-names\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\n\ntype ModalEvent = CustomEvent<{\n modal: string | null;\n}>;\n\nfunction matchesModal(event: Event, modal: string): boolean {\n const target = (event as ModalEvent).detail?.modal;\n\n return target == null || target === modal;\n}\n\nconst ModalComponent: RendererComponent<\"modal\"> = ({ children, node }) => {\n const { t } = useT(\"lattice\");\n const title = node.props.title ?? t(\"common.dialog\", \"Dialog\");\n const description = node.props.description;\n const closeLabel = node.props.closeLabel;\n const [isOpen, setIsOpen] = useState(node.props.open === true);\n const openerRef = useRef<HTMLElement | null>(null);\n const wasOpenRef = useRef(isOpen);\n\n // Honour server-driven open changes across re-renders, not just on mount. One\n // way on purpose: closing stays user/closeModal-driven so it never fights a\n // manual close, and the dep keeps it from re-firing while open stays true.\n useEffect(() => {\n if (node.props.open === true) {\n setIsOpen(true);\n }\n }, [node.props.open]);\n\n useEffect(() => {\n function open(event: Event): void {\n if (node.id && matchesModal(event, node.id)) {\n openerRef.current =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n setIsOpen(true);\n }\n }\n\n function close(event: Event): void {\n if (!node.id || matchesModal(event, node.id)) {\n setIsOpen(false);\n }\n }\n\n window.addEventListener(LATTICE_EVENT.openModal, open);\n window.addEventListener(LATTICE_EVENT.closeModal, close);\n\n return () => {\n window.removeEventListener(LATTICE_EVENT.openModal, open);\n window.removeEventListener(LATTICE_EVENT.closeModal, close);\n };\n }, [node.id]);\n\n // Radix only restores focus to a `DialogTrigger`-registered element, and this\n // modal is opened imperatively rather than through one. Own the restore\n // ourselves instead: whatever had focus when the modal opened gets it back\n // once the closing dialog has finished unmounting.\n useEffect(() => {\n if (wasOpenRef.current && !isOpen) {\n const opener = openerRef.current;\n\n if (opener) {\n requestAnimationFrame(() => opener.focus());\n }\n }\n\n wasOpenRef.current = isOpen;\n }, [isOpen]);\n\n return (\n <Dialog open={isOpen} onOpenChange={setIsOpen}>\n <DialogContent\n {...(description ? {} : { \"aria-describedby\": undefined })}\n placement={node.props.side ?? \"center\"}\n width={node.props.width}\n >\n <DialogHeader closeLabel={closeLabel} description={description} title={title} />\n <div className=\"mt-6 space-y-6\">{children}</div>\n </DialogContent>\n </Dialog>\n );\n};\n\nexport default ModalComponent;\n"],"mappings":";;;;;;AAUA,SAAS,aAAa,OAAc,OAAwB;CAC1D,MAAM,SAAU,MAAqB,QAAQ;CAE7C,OAAO,UAAU,QAAQ,WAAW;AACtC;AAEA,IAAM,kBAA8C,EAAE,UAAU,WAAW;CACzE,MAAM,EAAE,MAAM,KAAK,SAAS;CAC5B,MAAM,QAAQ,KAAK,MAAM,SAAS,EAAE,iBAAiB,QAAQ;CAC7D,MAAM,cAAc,KAAK,MAAM;CAC/B,MAAM,aAAa,KAAK,MAAM;CAC9B,MAAM,CAAC,QAAQ,aAAa,SAAS,KAAK,MAAM,SAAS,IAAI;CAC7D,MAAM,YAAY,OAA2B,IAAI;CACjD,MAAM,aAAa,OAAO,MAAM;CAKhC,gBAAgB;EACd,IAAI,KAAK,MAAM,SAAS,MACtB,UAAU,IAAI;CAElB,GAAG,CAAC,KAAK,MAAM,IAAI,CAAC;CAEpB,gBAAgB;EACd,SAAS,KAAK,OAAoB;GAChC,IAAI,KAAK,MAAM,aAAa,OAAO,KAAK,EAAE,GAAG;IAC3C,UAAU,UACR,SAAS,yBAAyB,cAAc,SAAS,gBAAgB;IAC3E,UAAU,IAAI;GAChB;EACF;EAEA,SAAS,MAAM,OAAoB;GACjC,IAAI,CAAC,KAAK,MAAM,aAAa,OAAO,KAAK,EAAE,GACzC,UAAU,KAAK;EAEnB;EAEA,OAAO,iBAAiB,cAAc,WAAW,IAAI;EACrD,OAAO,iBAAiB,cAAc,YAAY,KAAK;EAEvD,aAAa;GACX,OAAO,oBAAoB,cAAc,WAAW,IAAI;GACxD,OAAO,oBAAoB,cAAc,YAAY,KAAK;EAC5D;CACF,GAAG,CAAC,KAAK,EAAE,CAAC;CAMZ,gBAAgB;EACd,IAAI,WAAW,WAAW,CAAC,QAAQ;GACjC,MAAM,SAAS,UAAU;GAEzB,IAAI,QACF,4BAA4B,OAAO,MAAM,CAAC;EAE9C;EAEA,WAAW,UAAU;CACvB,GAAG,CAAC,MAAM,CAAC;CAEX,OACE,oBAAC,QAAD;EAAQ,MAAM;EAAQ,cAAc;YAClC,qBAAC,eAAD;GACE,GAAK,cAAc,CAAC,IAAI,EAAE,oBAAoB,KAAA,EAAU;GACxD,WAAW,KAAK,MAAM,QAAQ;GAC9B,OAAO,KAAK,MAAM;aAHpB,CAKE,oBAAC,cAAD;IAA0B;IAAyB;IAAoB;GAAQ,CAAA,GAC/E,oBAAC,OAAD;IAAK,WAAU;IAAkB;GAAc,CAAA,CAClC;;CACT,CAAA;AAEZ"}
@@ -0,0 +1,2 @@
1
+ /** Shared pill styling for the tabs strip and the segmented control. */
2
+ export declare function pillClassName(active: boolean): string;
@@ -0,0 +1,10 @@
1
+ import { cn } from "../lib/utils.js";
2
+ //#region resources/js/ui/pill.ts
3
+ /** Shared pill styling for the tabs strip and the segmented control. */
4
+ function pillClassName(active) {
5
+ return cn("whitespace-nowrap rounded-lt-sm px-3 py-1.5 text-sm font-medium transition-colors", active ? "bg-lt-bg text-lt-fg shadow-lt-xs" : "text-lt-muted-fg hover:bg-lt-bg/60 hover:text-lt-fg");
6
+ }
7
+ //#endregion
8
+ export { pillClassName };
9
+
10
+ //# sourceMappingURL=pill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pill.js","names":[],"sources":["../../resources/js/ui/pill.ts"],"sourcesContent":["import { cn } from \"@lattice-php/lattice/lib/utils\";\n\n/** Shared pill styling for the tabs strip and the segmented control. */\nexport function pillClassName(active: boolean): string {\n return cn(\n \"whitespace-nowrap rounded-lt-sm px-3 py-1.5 text-sm font-medium transition-colors\",\n active\n ? \"bg-lt-bg text-lt-fg shadow-lt-xs\"\n : \"text-lt-muted-fg hover:bg-lt-bg/60 hover:text-lt-fg\",\n );\n}\n"],"mappings":";;;AAGA,SAAgB,cAAc,QAAyB;CACrD,OAAO,GACL,qFACA,SACI,qCACA,qDACN;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":[],"sources":["../../resources/js/ui/plugin.ts"],"sourcesContent":["import {\n createPlugin,\n eagerComponent,\n type ComponentRegistryFor,\n} from \"@lattice-php/lattice/core/registry\";\nimport type { ActionNodeType, FormNodeType, NodeType } from \"@lattice-php/lattice/types/generated\";\nimport type { ChatComponentType } from \"@lattice-php/lattice/chat/plugin\";\nimport type { LayoutComponentType } from \"@lattice-php/lattice/layout/plugin\";\nimport type { NotificationsComponentType } from \"@lattice-php/lattice/notifications/plugin\";\nimport type { RemoteComponentType } from \"@lattice-php/lattice/remote/plugin\";\nimport type { TableComponentType } from \"@lattice-php/lattice/table/plugin\";\nimport AvatarComponent from \"./avatar\";\nimport BadgeComponent from \"./badge\";\nimport ButtonComponent from \"./button\";\nimport CardComponent from \"./card\";\nimport ChartComponent from \"./chart\";\nimport CollapsibleComponent from \"./collapsible\";\nimport FloatingPanelComponent from \"./floating-panel\";\nimport FragmentComponent from \"./fragment\";\nimport GridComponent from \"./grid\";\nimport HeadingComponent from \"./heading\";\nimport IconComponent from \"./icon\";\nimport ImageComponent from \"./image\";\nimport LinkComponent from \"./link\";\nimport ModalComponent from \"./modal\";\nimport ProgressComponent from \"./progress\";\nimport RawBlockComponent from \"./raw-block\";\nimport SectionComponent from \"./section\";\nimport SegmentedControlComponent from \"./segmented-control\";\nimport SeparatorComponent from \"./separator\";\nimport StackComponent from \"./stack\";\nimport TabComponent, { TabsComponent } from \"./tabs\";\nimport TextComponent from \"./text\";\nimport TooltipComponent from \"./tooltip\";\nimport TreeComponent from \"./tree\";\n\ntype NonUiComponentType =\n | ActionNodeType\n | ChatComponentType\n | FormNodeType\n | LayoutComponentType\n | NotificationsComponentType\n | RemoteComponentType\n | TableComponentType;\n\ntype UiComponentType = Exclude<NodeType, NonUiComponentType>;\n\nexport const uiComponents = createPlugin({\n components: {\n avatar: eagerComponent(AvatarComponent),\n badge: eagerComponent(BadgeComponent),\n button: eagerComponent(ButtonComponent),\n card: eagerComponent(CardComponent),\n chart: eagerComponent(ChartComponent),\n collapsible: eagerComponent(CollapsibleComponent),\n \"floating-panel\": eagerComponent(FloatingPanelComponent),\n fragment: eagerComponent(FragmentComponent),\n grid: eagerComponent(GridComponent),\n heading: eagerComponent(HeadingComponent),\n icon: eagerComponent(IconComponent),\n image: eagerComponent(ImageComponent),\n link: eagerComponent(LinkComponent),\n modal: eagerComponent(ModalComponent),\n progress: eagerComponent(ProgressComponent),\n \"raw-block\": eagerComponent(RawBlockComponent),\n section: eagerComponent(SectionComponent),\n \"segmented-control\": eagerComponent(SegmentedControlComponent),\n separator: eagerComponent(SeparatorComponent),\n stack: eagerComponent(StackComponent),\n tab: eagerComponent(TabComponent),\n tabs: eagerComponent(TabsComponent),\n text: eagerComponent(TextComponent),\n tooltip: eagerComponent(TooltipComponent),\n tree: eagerComponent(TreeComponent),\n } satisfies ComponentRegistryFor<UiComponentType>,\n name: \"lattice/ui\",\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,IAAa,eAAe,aAAa;CACvC,YAAY;EACV,QAAQ,eAAe,eAAe;EACtC,OAAO,eAAe,cAAc;EACpC,QAAQ,eAAe,eAAe;EACtC,MAAM,eAAe,aAAa;EAClC,OAAO,eAAe,cAAc;EACpC,aAAa,eAAe,oBAAoB;EAChD,kBAAkB,eAAe,sBAAsB;EACvD,UAAU,eAAe,iBAAiB;EAC1C,MAAM,eAAe,aAAa;EAClC,SAAS,eAAe,gBAAgB;EACxC,MAAM,eAAe,aAAa;EAClC,OAAO,eAAe,cAAc;EACpC,MAAM,eAAe,aAAa;EAClC,OAAO,eAAe,cAAc;EACpC,UAAU,eAAe,iBAAiB;EAC1C,aAAa,eAAe,iBAAiB;EAC7C,SAAS,eAAe,gBAAgB;EACxC,qBAAqB,eAAe,yBAAyB;EAC7D,WAAW,eAAe,kBAAkB;EAC5C,OAAO,eAAe,cAAc;EACpC,KAAK,eAAe,YAAY;EAChC,MAAM,eAAe,aAAa;EAClC,MAAM,eAAe,aAAa;EAClC,SAAS,eAAe,gBAAgB;EACxC,MAAM,eAAe,aAAa;CACpC;CACA,MAAM;AACR,CAAC"}
1
+ {"version":3,"file":"plugin.js","names":[],"sources":["../../resources/js/ui/plugin.ts"],"sourcesContent":["import {\n createPlugin,\n eagerComponent,\n type ComponentRegistryFor,\n} from \"@lattice-php/lattice/core/registry\";\nimport type { FragmentNodeType, UiNodeType } from \"@lattice-php/lattice/types/generated\";\nimport AvatarComponent from \"./avatar\";\nimport BadgeComponent from \"./badge\";\nimport ButtonComponent from \"./button\";\nimport CardComponent from \"./card\";\nimport ChartComponent from \"./chart\";\nimport CollapsibleComponent from \"./collapsible\";\nimport FloatingPanelComponent from \"./floating-panel\";\nimport FragmentComponent from \"./fragment\";\nimport GridComponent from \"./grid\";\nimport HeadingComponent from \"./heading\";\nimport IconComponent from \"./icon\";\nimport ImageComponent from \"./image\";\nimport LinkComponent from \"./link\";\nimport ModalComponent from \"./modal\";\nimport ProgressComponent from \"./progress\";\nimport RawBlockComponent from \"./raw-block\";\nimport SectionComponent from \"./section\";\nimport SegmentedControlComponent from \"./segmented-control\";\nimport SeparatorComponent from \"./separator\";\nimport StackComponent from \"./stack\";\nimport TabComponent, { TabsComponent } from \"./tabs\";\nimport TextComponent from \"./text\";\nimport TooltipComponent from \"./tooltip\";\nimport TreeComponent from \"./tree\";\n\ntype UiComponentType = UiNodeType | FragmentNodeType;\n\nexport const uiComponents = createPlugin({\n components: {\n avatar: eagerComponent(AvatarComponent),\n badge: eagerComponent(BadgeComponent),\n button: eagerComponent(ButtonComponent),\n card: eagerComponent(CardComponent),\n chart: eagerComponent(ChartComponent),\n collapsible: eagerComponent(CollapsibleComponent),\n \"floating-panel\": eagerComponent(FloatingPanelComponent),\n fragment: eagerComponent(FragmentComponent),\n grid: eagerComponent(GridComponent),\n heading: eagerComponent(HeadingComponent),\n icon: eagerComponent(IconComponent),\n image: eagerComponent(ImageComponent),\n link: eagerComponent(LinkComponent),\n modal: eagerComponent(ModalComponent),\n progress: eagerComponent(ProgressComponent),\n \"raw-block\": eagerComponent(RawBlockComponent),\n section: eagerComponent(SectionComponent),\n \"segmented-control\": eagerComponent(SegmentedControlComponent),\n separator: eagerComponent(SeparatorComponent),\n stack: eagerComponent(StackComponent),\n tab: eagerComponent(TabComponent),\n tabs: eagerComponent(TabsComponent),\n text: eagerComponent(TextComponent),\n tooltip: eagerComponent(TooltipComponent),\n tree: eagerComponent(TreeComponent),\n } satisfies ComponentRegistryFor<UiComponentType>,\n name: \"lattice/ui\",\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAa,eAAe,aAAa;CACvC,YAAY;EACV,QAAQ,eAAe,eAAe;EACtC,OAAO,eAAe,cAAc;EACpC,QAAQ,eAAe,eAAe;EACtC,MAAM,eAAe,aAAa;EAClC,OAAO,eAAe,cAAc;EACpC,aAAa,eAAe,oBAAoB;EAChD,kBAAkB,eAAe,sBAAsB;EACvD,UAAU,eAAe,iBAAiB;EAC1C,MAAM,eAAe,aAAa;EAClC,SAAS,eAAe,gBAAgB;EACxC,MAAM,eAAe,aAAa;EAClC,OAAO,eAAe,cAAc;EACpC,MAAM,eAAe,aAAa;EAClC,OAAO,eAAe,cAAc;EACpC,UAAU,eAAe,iBAAiB;EAC1C,aAAa,eAAe,iBAAiB;EAC7C,SAAS,eAAe,gBAAgB;EACxC,qBAAqB,eAAe,yBAAyB;EAC7D,WAAW,eAAe,kBAAkB;EAC5C,OAAO,eAAe,cAAc;EACpC,KAAK,eAAe,YAAY;EAChC,MAAM,eAAe,aAAa;EAClC,MAAM,eAAe,aAAa;EAClC,SAAS,eAAe,gBAAgB;EACxC,MAAM,eAAe,aAAa;CACpC;CACA,MAAM;AACR,CAAC"}
@@ -1,5 +1,5 @@
1
- import { cn } from "../lib/utils.js";
2
1
  import { useLocale } from "../i18n/locale.js";
2
+ import { cn } from "../lib/utils.js";
3
3
  import { coerceColor, colorValue, namedColor } from "../lib/color.js";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region resources/js/ui/progress.tsx
@@ -3,30 +3,17 @@ import { Icon } from "../icons/sprite.js";
3
3
  import { toNodes } from "../core/nodes.js";
4
4
  import { Renderer } from "../core/renderer.js";
5
5
  import { nodeIdentity, prefixedTestId } from "../core/test-id.js";
6
+ import { useCollapsibleState } from "./use-collapsible-state.js";
6
7
  import { InfoTooltip } from "./info-tooltip.js";
7
- import { useState } from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region resources/js/ui/section.tsx
10
- function readStored(key, remember, fallback) {
11
- if (!remember || typeof window === "undefined") return fallback;
12
- const stored = window.localStorage.getItem(key);
13
- return stored === null ? fallback : stored === "true";
14
- }
15
10
  var SectionComponent = ({ children, node }) => {
16
11
  const { title, description, tooltip } = node.props;
17
12
  const collapsible = node.props.collapsible === true;
18
13
  const rememberState = node.props.rememberState !== false;
19
14
  const headerActions = toNodes(node.props.headerActions);
20
15
  const identity = nodeIdentity(node);
21
- const storageKey = `lattice:section:${identity ?? "default"}`;
22
- const [collapsed, setCollapsed] = useState(() => readStored(storageKey, collapsible && rememberState, node.props.collapsed === true));
23
- function toggle() {
24
- setCollapsed((value) => {
25
- const next = !value;
26
- if (rememberState && typeof window !== "undefined") window.localStorage.setItem(storageKey, String(next));
27
- return next;
28
- });
29
- }
16
+ const [collapsed, toggle] = useCollapsibleState(`lattice:section:${identity ?? "default"}`, node.props.collapsed === true, collapsible && rememberState);
30
17
  const isCollapsed = collapsible && collapsed;
31
18
  return /* @__PURE__ */ jsxs("section", {
32
19
  "data-slot": "section",
@@ -1 +1 @@
1
- {"version":3,"file":"section.js","names":[],"sources":["../../resources/js/ui/section.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { Icon } from \"@lattice-php/lattice/icons\";\nimport { Renderer } from \"@lattice-php/lattice/core/renderer\";\nimport { nodeIdentity, prefixedTestId } from \"@lattice-php/lattice/core/test-id\";\nimport { toNodes } from \"@lattice-php/lattice/core/nodes\";\nimport type { RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\nimport { InfoTooltip } from \"./info-tooltip\";\n\nfunction readStored(key: string, remember: boolean, fallback: boolean): boolean {\n if (!remember || typeof window === \"undefined\") {\n return fallback;\n }\n\n const stored = window.localStorage.getItem(key);\n\n return stored === null ? fallback : stored === \"true\";\n}\n\nconst SectionComponent: RendererComponent<\"section\"> = ({ children, node }) => {\n const { title, description, tooltip } = node.props;\n const collapsible = node.props.collapsible === true;\n const rememberState = node.props.rememberState !== false;\n const headerActions = toNodes(node.props.headerActions);\n const identity = nodeIdentity(node);\n const storageKey = `lattice:section:${identity ?? \"default\"}`;\n\n const [collapsed, setCollapsed] = useState(() =>\n readStored(storageKey, collapsible && rememberState, node.props.collapsed === true),\n );\n\n function toggle(): void {\n setCollapsed((value) => {\n const next = !value;\n\n if (rememberState && typeof window !== \"undefined\") {\n window.localStorage.setItem(storageKey, String(next));\n }\n\n return next;\n });\n }\n\n const isCollapsed = collapsible && collapsed;\n const hasHeader = Boolean(title || description || headerActions.length > 0 || collapsible);\n\n return (\n <section\n data-slot=\"section\"\n className=\"flex flex-col gap-6 rounded-lt border border-lt-border bg-lt-surface py-lt-gutter text-lt-surface-fg shadow-lt-sm\"\n data-lattice-component={identity}\n >\n {hasHeader && (\n <div className=\"flex items-start justify-between gap-4 px-lt-gutter\">\n <div className=\"flex min-w-0 items-start gap-2\">\n {collapsible && (\n <button\n aria-expanded={!isCollapsed}\n aria-label={isCollapsed ? \"Expand section\" : \"Collapse section\"}\n data-test={prefixedTestId(\"section-toggle\", identity) ?? \"section-toggle-default\"}\n className=\"mt-0.5 inline-flex shrink-0 items-center rounded-lt-sm p-0.5 text-lt-muted-fg transition-colors hover:bg-lt-muted hover:text-lt-fg\"\n onClick={toggle}\n type=\"button\"\n >\n <Icon\n name=\"chevron-down\"\n className={cn(\n \"size-lt-icon-md transition-transform\",\n isCollapsed && \"-rotate-90\",\n )}\n />\n </button>\n )}\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n {title && (\n <div className=\"flex items-center\">\n <div className=\"font-semibold leading-none\">{title}</div>\n <InfoTooltip content={tooltip} />\n </div>\n )}\n {description && (\n <div className=\"flex items-center\">\n <div className=\"text-sm text-lt-muted-fg\">{description}</div>\n {!title && <InfoTooltip content={tooltip} />}\n </div>\n )}\n </div>\n </div>\n\n {headerActions.length > 0 && (\n <div className=\"flex shrink-0 items-center gap-2\">\n <Renderer nodes={headerActions} />\n </div>\n )}\n </div>\n )}\n\n {!isCollapsed && children && (\n <div className=\"flex flex-col gap-6 px-lt-gutter\">{children}</div>\n )}\n </section>\n );\n};\n\nexport default SectionComponent;\n"],"mappings":";;;;;;;;;AASA,SAAS,WAAW,KAAa,UAAmB,UAA4B;CAC9E,IAAI,CAAC,YAAY,OAAO,WAAW,aACjC,OAAO;CAGT,MAAM,SAAS,OAAO,aAAa,QAAQ,GAAG;CAE9C,OAAO,WAAW,OAAO,WAAW,WAAW;AACjD;AAEA,IAAM,oBAAkD,EAAE,UAAU,WAAW;CAC7E,MAAM,EAAE,OAAO,aAAa,YAAY,KAAK;CAC7C,MAAM,cAAc,KAAK,MAAM,gBAAgB;CAC/C,MAAM,gBAAgB,KAAK,MAAM,kBAAkB;CACnD,MAAM,gBAAgB,QAAQ,KAAK,MAAM,aAAa;CACtD,MAAM,WAAW,aAAa,IAAI;CAClC,MAAM,aAAa,mBAAmB,YAAY;CAElD,MAAM,CAAC,WAAW,gBAAgB,eAChC,WAAW,YAAY,eAAe,eAAe,KAAK,MAAM,cAAc,IAAI,CACpF;CAEA,SAAS,SAAe;EACtB,cAAc,UAAU;GACtB,MAAM,OAAO,CAAC;GAEd,IAAI,iBAAiB,OAAO,WAAW,aACrC,OAAO,aAAa,QAAQ,YAAY,OAAO,IAAI,CAAC;GAGtD,OAAO;EACT,CAAC;CACH;CAEA,MAAM,cAAc,eAAe;CAGnC,OACE,qBAAC,WAAD;EACE,aAAU;EACV,WAAU;EACV,0BAAwB;YAH1B,CAHgB,QAAQ,SAAS,eAAe,cAAc,SAAS,KAAK,WAQzE,KACC,qBAAC,OAAD;GAAK,WAAU;aAAf,CACE,qBAAC,OAAD;IAAK,WAAU;cAAf,CACG,eACC,oBAAC,UAAD;KACE,iBAAe,CAAC;KAChB,cAAY,cAAc,mBAAmB;KAC7C,aAAW,eAAe,kBAAkB,QAAQ,KAAK;KACzD,WAAU;KACV,SAAS;KACT,MAAK;eAEL,oBAAC,MAAD;MACE,MAAK;MACL,WAAW,GACT,wCACA,eAAe,YACjB;KACD,CAAA;IACK,CAAA,GAEV,qBAAC,OAAD;KAAK,WAAU;eAAf,CACG,SACC,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,OAAD;OAAK,WAAU;iBAA8B;MAAW,CAAA,GACxD,oBAAC,aAAD,EAAa,SAAS,QAAU,CAAA,CAC7B;SAEN,eACC,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,OAAD;OAAK,WAAU;iBAA4B;MAAiB,CAAA,GAC3D,CAAC,SAAS,oBAAC,aAAD,EAAa,SAAS,QAAU,CAAA,CACxC;OAEJ;MACF;OAEJ,cAAc,SAAS,KACtB,oBAAC,OAAD;IAAK,WAAU;cACb,oBAAC,UAAD,EAAU,OAAO,cAAgB,CAAA;GAC9B,CAAA,CAEJ;MAGN,CAAC,eAAe,YACf,oBAAC,OAAD;GAAK,WAAU;GAAoC;EAAc,CAAA,CAE5D;;AAEb"}
1
+ {"version":3,"file":"section.js","names":[],"sources":["../../resources/js/ui/section.tsx"],"sourcesContent":["import { Icon } from \"@lattice-php/lattice/icons\";\nimport { Renderer } from \"@lattice-php/lattice/core/renderer\";\nimport { nodeIdentity, prefixedTestId } from \"@lattice-php/lattice/core/test-id\";\nimport { toNodes } from \"@lattice-php/lattice/core/nodes\";\nimport type { RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\nimport { InfoTooltip } from \"./info-tooltip\";\nimport { useCollapsibleState } from \"./use-collapsible-state\";\n\nconst SectionComponent: RendererComponent<\"section\"> = ({ children, node }) => {\n const { title, description, tooltip } = node.props;\n const collapsible = node.props.collapsible === true;\n const rememberState = node.props.rememberState !== false;\n const headerActions = toNodes(node.props.headerActions);\n const identity = nodeIdentity(node);\n const storageKey = `lattice:section:${identity ?? \"default\"}`;\n\n const [collapsed, toggle] = useCollapsibleState(\n storageKey,\n node.props.collapsed === true,\n collapsible && rememberState,\n );\n\n const isCollapsed = collapsible && collapsed;\n const hasHeader = Boolean(title || description || headerActions.length > 0 || collapsible);\n\n return (\n <section\n data-slot=\"section\"\n className=\"flex flex-col gap-6 rounded-lt border border-lt-border bg-lt-surface py-lt-gutter text-lt-surface-fg shadow-lt-sm\"\n data-lattice-component={identity}\n >\n {hasHeader && (\n <div className=\"flex items-start justify-between gap-4 px-lt-gutter\">\n <div className=\"flex min-w-0 items-start gap-2\">\n {collapsible && (\n <button\n aria-expanded={!isCollapsed}\n aria-label={isCollapsed ? \"Expand section\" : \"Collapse section\"}\n data-test={prefixedTestId(\"section-toggle\", identity) ?? \"section-toggle-default\"}\n className=\"mt-0.5 inline-flex shrink-0 items-center rounded-lt-sm p-0.5 text-lt-muted-fg transition-colors hover:bg-lt-muted hover:text-lt-fg\"\n onClick={toggle}\n type=\"button\"\n >\n <Icon\n name=\"chevron-down\"\n className={cn(\n \"size-lt-icon-md transition-transform\",\n isCollapsed && \"-rotate-90\",\n )}\n />\n </button>\n )}\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n {title && (\n <div className=\"flex items-center\">\n <div className=\"font-semibold leading-none\">{title}</div>\n <InfoTooltip content={tooltip} />\n </div>\n )}\n {description && (\n <div className=\"flex items-center\">\n <div className=\"text-sm text-lt-muted-fg\">{description}</div>\n {!title && <InfoTooltip content={tooltip} />}\n </div>\n )}\n </div>\n </div>\n\n {headerActions.length > 0 && (\n <div className=\"flex shrink-0 items-center gap-2\">\n <Renderer nodes={headerActions} />\n </div>\n )}\n </div>\n )}\n\n {!isCollapsed && children && (\n <div className=\"flex flex-col gap-6 px-lt-gutter\">{children}</div>\n )}\n </section>\n );\n};\n\nexport default SectionComponent;\n"],"mappings":";;;;;;;;;AASA,IAAM,oBAAkD,EAAE,UAAU,WAAW;CAC7E,MAAM,EAAE,OAAO,aAAa,YAAY,KAAK;CAC7C,MAAM,cAAc,KAAK,MAAM,gBAAgB;CAC/C,MAAM,gBAAgB,KAAK,MAAM,kBAAkB;CACnD,MAAM,gBAAgB,QAAQ,KAAK,MAAM,aAAa;CACtD,MAAM,WAAW,aAAa,IAAI;CAGlC,MAAM,CAAC,WAAW,UAAU,oBAC1B,mBAHoC,YAAY,aAIhD,KAAK,MAAM,cAAc,MACzB,eAAe,aACjB;CAEA,MAAM,cAAc,eAAe;CAGnC,OACE,qBAAC,WAAD;EACE,aAAU;EACV,WAAU;EACV,0BAAwB;YAH1B,CAHgB,QAAQ,SAAS,eAAe,cAAc,SAAS,KAAK,WAQzE,KACC,qBAAC,OAAD;GAAK,WAAU;aAAf,CACE,qBAAC,OAAD;IAAK,WAAU;cAAf,CACG,eACC,oBAAC,UAAD;KACE,iBAAe,CAAC;KAChB,cAAY,cAAc,mBAAmB;KAC7C,aAAW,eAAe,kBAAkB,QAAQ,KAAK;KACzD,WAAU;KACV,SAAS;KACT,MAAK;eAEL,oBAAC,MAAD;MACE,MAAK;MACL,WAAW,GACT,wCACA,eAAe,YACjB;KACD,CAAA;IACK,CAAA,GAEV,qBAAC,OAAD;KAAK,WAAU;eAAf,CACG,SACC,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,OAAD;OAAK,WAAU;iBAA8B;MAAW,CAAA,GACxD,oBAAC,aAAD,EAAa,SAAS,QAAU,CAAA,CAC7B;SAEN,eACC,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,OAAD;OAAK,WAAU;iBAA4B;MAAiB,CAAA,GAC3D,CAAC,SAAS,oBAAC,aAAD,EAAa,SAAS,QAAU,CAAA,CACxC;OAEJ;MACF;OAEJ,cAAc,SAAS,KACtB,oBAAC,OAAD;IAAK,WAAU;cACb,oBAAC,UAAD,EAAU,OAAO,cAAgB,CAAA;GAC9B,CAAA,CAEJ;MAGN,CAAC,eAAe,YACf,oBAAC,OAAD;GAAK,WAAU;GAAoC;EAAc,CAAA,CAE5D;;AAEb"}
@@ -1,4 +1,5 @@
1
1
  import { cn } from "../lib/utils.js";
2
+ import { pillClassName } from "./pill.js";
2
3
  import { useEffect, useRef } from "react";
3
4
  import { jsx } from "react/jsx-runtime";
4
5
  //#region resources/js/ui/segmented-pills.tsx
@@ -23,7 +24,7 @@ function SegmentedPills({ ariaLabel, autoFocus = false, disabled = false, name,
23
24
  return /* @__PURE__ */ jsx("button", {
24
25
  "aria-checked": isSelected,
25
26
  "data-test": `${name ?? "segment"}-${option.value}`,
26
- className: cn("whitespace-nowrap rounded-lt-sm px-3 py-1.5 text-sm font-medium transition-colors", isSelected ? "bg-lt-bg text-lt-fg shadow-lt-xs" : "text-lt-muted-fg hover:bg-lt-bg/60 hover:text-lt-fg", disabled && "cursor-not-allowed opacity-60"),
27
+ className: cn(pillClassName(isSelected), disabled && "cursor-not-allowed opacity-60"),
27
28
  disabled,
28
29
  onClick: () => onSelect(option.value),
29
30
  role: "radio",
@@ -1 +1 @@
1
- {"version":3,"file":"segmented-pills.js","names":[],"sources":["../../resources/js/ui/segmented-pills.tsx"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport type { Option } from \"@lattice-php/lattice/core/types\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\n\n/**\n * Presentational segmented pill group. Used by the form choice field (bound to a\n * form value) and the core segmented control (standalone, emits an event).\n */\nexport function SegmentedPills({\n ariaLabel,\n autoFocus = false,\n disabled = false,\n name,\n onSelect,\n options,\n tabIndex,\n value,\n}: {\n ariaLabel?: string;\n autoFocus?: boolean;\n disabled?: boolean;\n name?: string;\n onSelect: (value: string) => void;\n options: Option[];\n tabIndex?: number;\n value: string;\n}) {\n const groupRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!autoFocus) {\n return;\n }\n\n const group = groupRef.current;\n const target =\n group?.querySelector<HTMLButtonElement>('button[aria-checked=\"true\"]') ??\n group?.querySelector<HTMLButtonElement>(\"button\");\n\n target?.focus();\n }, [autoFocus]);\n\n return (\n <div\n aria-label={ariaLabel}\n className=\"inline-flex w-fit max-w-full gap-1 overflow-x-auto rounded-lt bg-lt-muted p-1\"\n ref={groupRef}\n role=\"radiogroup\"\n >\n {options.map((option) => {\n const isSelected = value === option.value;\n\n return (\n <button\n aria-checked={isSelected}\n data-test={`${name ?? \"segment\"}-${option.value}`}\n className={cn(\n \"whitespace-nowrap rounded-lt-sm px-3 py-1.5 text-sm font-medium transition-colors\",\n isSelected\n ? \"bg-lt-bg text-lt-fg shadow-lt-xs\"\n : \"text-lt-muted-fg hover:bg-lt-bg/60 hover:text-lt-fg\",\n disabled && \"cursor-not-allowed opacity-60\",\n )}\n disabled={disabled}\n key={option.value}\n onClick={() => onSelect(option.value)}\n role=\"radio\"\n tabIndex={tabIndex}\n type=\"button\"\n >\n {option.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,eAAe,EAC7B,WACA,YAAY,OACZ,WAAW,OACX,MACA,UACA,SACA,UACA,SAUC;CACD,MAAM,WAAW,OAAuB,IAAI;CAE5C,gBAAgB;EACd,IAAI,CAAC,WACH;EAGF,MAAM,QAAQ,SAAS;EAKvB,CAHE,OAAO,cAAiC,+BAA6B,KACrE,OAAO,cAAiC,QAAQ,IAE1C,MAAM;CAChB,GAAG,CAAC,SAAS,CAAC;CAEd,OACE,oBAAC,OAAD;EACE,cAAY;EACZ,WAAU;EACV,KAAK;EACL,MAAK;YAEJ,QAAQ,KAAK,WAAW;GACvB,MAAM,aAAa,UAAU,OAAO;GAEpC,OACE,oBAAC,UAAD;IACE,gBAAc;IACd,aAAW,GAAG,QAAQ,UAAU,GAAG,OAAO;IAC1C,WAAW,GACT,qFACA,aACI,qCACA,uDACJ,YAAY,+BACd;IACU;IAEV,eAAe,SAAS,OAAO,KAAK;IACpC,MAAK;IACK;IACV,MAAK;cAEJ,OAAO;GACF,GAPD,OAAO,KAON;EAEZ,CAAC;CACE,CAAA;AAET"}
1
+ {"version":3,"file":"segmented-pills.js","names":[],"sources":["../../resources/js/ui/segmented-pills.tsx"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport type { Option } from \"@lattice-php/lattice/core/types\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\nimport { pillClassName } from \"./pill\";\n\n/**\n * Presentational segmented pill group. Used by the form choice field (bound to a\n * form value) and the core segmented control (standalone, emits an event).\n */\nexport function SegmentedPills({\n ariaLabel,\n autoFocus = false,\n disabled = false,\n name,\n onSelect,\n options,\n tabIndex,\n value,\n}: {\n ariaLabel?: string;\n autoFocus?: boolean;\n disabled?: boolean;\n name?: string;\n onSelect: (value: string) => void;\n options: Option[];\n tabIndex?: number;\n value: string;\n}) {\n const groupRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!autoFocus) {\n return;\n }\n\n const group = groupRef.current;\n const target =\n group?.querySelector<HTMLButtonElement>('button[aria-checked=\"true\"]') ??\n group?.querySelector<HTMLButtonElement>(\"button\");\n\n target?.focus();\n }, [autoFocus]);\n\n return (\n <div\n aria-label={ariaLabel}\n className=\"inline-flex w-fit max-w-full gap-1 overflow-x-auto rounded-lt bg-lt-muted p-1\"\n ref={groupRef}\n role=\"radiogroup\"\n >\n {options.map((option) => {\n const isSelected = value === option.value;\n\n return (\n <button\n aria-checked={isSelected}\n data-test={`${name ?? \"segment\"}-${option.value}`}\n className={cn(pillClassName(isSelected), disabled && \"cursor-not-allowed opacity-60\")}\n disabled={disabled}\n key={option.value}\n onClick={() => onSelect(option.value)}\n role=\"radio\"\n tabIndex={tabIndex}\n type=\"button\"\n >\n {option.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,eAAe,EAC7B,WACA,YAAY,OACZ,WAAW,OACX,MACA,UACA,SACA,UACA,SAUC;CACD,MAAM,WAAW,OAAuB,IAAI;CAE5C,gBAAgB;EACd,IAAI,CAAC,WACH;EAGF,MAAM,QAAQ,SAAS;EAKvB,CAHE,OAAO,cAAiC,+BAA6B,KACrE,OAAO,cAAiC,QAAQ,IAE1C,MAAM;CAChB,GAAG,CAAC,SAAS,CAAC;CAEd,OACE,oBAAC,OAAD;EACE,cAAY;EACZ,WAAU;EACV,KAAK;EACL,MAAK;YAEJ,QAAQ,KAAK,WAAW;GACvB,MAAM,aAAa,UAAU,OAAO;GAEpC,OACE,oBAAC,UAAD;IACE,gBAAc;IACd,aAAW,GAAG,QAAQ,UAAU,GAAG,OAAO;IAC1C,WAAW,GAAG,cAAc,UAAU,GAAG,YAAY,+BAA+B;IAC1E;IAEV,eAAe,SAAS,OAAO,KAAK;IACpC,MAAK;IACK;IACV,MAAK;cAEJ,OAAO;GACF,GAPD,OAAO,KAON;EAEZ,CAAC;CACE,CAAA;AAET"}
package/dist/ui/tabs.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { cn } from "../lib/utils.js";
2
2
  import { useT } from "../i18n/instance.js";
3
+ import { pillClassName } from "./pill.js";
3
4
  import { router } from "@inertiajs/react";
4
- import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
5
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region resources/js/ui/tabs.tsx
7
8
  var TabsContext = createContext(null);
@@ -54,18 +55,22 @@ var TabsComponent = ({ children, node }) => {
54
55
  const defaultValue = node.props.defaultValue ?? firstValue;
55
56
  const tablistRef = useRef(null);
56
57
  const [activeValue, setActiveTabValue] = useState(() => serverActiveValue || (queryValue(queryKey, tabs) ?? defaultValue) || firstValue);
57
- function selectTab(tab) {
58
+ const selectTab = useCallback((tab) => {
58
59
  if (tab.confirm?.required) {
59
60
  router.visit(queryUrl(queryKey, tab.value), { preserveScroll: true });
60
61
  return;
61
62
  }
62
63
  setActiveTabValue(tab.value);
63
64
  replaceQueryValue(queryKey, tab.value);
64
- }
65
- function selectTabValue(value) {
65
+ }, [queryKey]);
66
+ const selectTabValue = useCallback((value) => {
66
67
  const tab = tabs.find((item) => item.value === value);
67
68
  if (tab) selectTab(tab);
68
- }
69
+ }, [selectTab, tabs]);
70
+ const contextValue = useMemo(() => ({
71
+ activeValue,
72
+ setActiveValue: selectTabValue
73
+ }), [activeValue, selectTabValue]);
69
74
  function onTablistKeyDown(event) {
70
75
  const nextKey = isVertical ? "ArrowDown" : "ArrowRight";
71
76
  const prevKey = isVertical ? "ArrowUp" : "ArrowLeft";
@@ -84,10 +89,7 @@ var TabsComponent = ({ children, node }) => {
84
89
  buttons[index]?.focus();
85
90
  }
86
91
  return /* @__PURE__ */ jsx(TabsContext.Provider, {
87
- value: {
88
- activeValue,
89
- setActiveValue: selectTabValue
90
- },
92
+ value: contextValue,
91
93
  children: /* @__PURE__ */ jsxs("div", {
92
94
  className: cn("gap-6", isVertical ? cn("flex", alignment === "end" && "flex-row-reverse") : "grid"),
93
95
  "data-lattice-tabs": node.key ?? node.id,
@@ -106,7 +108,7 @@ var TabsComponent = ({ children, node }) => {
106
108
  "aria-controls": `${tab.value}-panel`,
107
109
  "aria-selected": isActive,
108
110
  "data-test": `tab-${tab.value}`,
109
- className: cn("whitespace-nowrap rounded-lt-sm px-3 py-1.5 text-sm font-medium transition-colors", isActive ? "bg-lt-bg text-lt-fg shadow-lt-xs" : "text-lt-muted-fg hover:bg-lt-bg/60 hover:text-lt-fg", isVertical && "text-left", isStretched && "flex-1"),
111
+ className: cn(pillClassName(isActive), isVertical && "text-left", isStretched && "flex-1"),
110
112
  id: `${tab.value}-tab`,
111
113
  onClick: () => selectTab(tab),
112
114
  onKeyDown: onTablistKeyDown,
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.js","names":[],"sources":["../../resources/js/ui/tabs.tsx"],"sourcesContent":["import { router } from \"@inertiajs/react\";\nimport { createContext, useContext, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport type { Node, RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport type { Tab } from \"@lattice-php/lattice/types/generated\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\n\ntype TabsContextValue = {\n activeValue: string;\n setActiveValue: (value: string) => void;\n};\n\nconst TabsContext = createContext<TabsContextValue | null>(null);\n\nfunction useTabsContext(): TabsContextValue {\n const context = useContext(TabsContext);\n\n if (!context) {\n return {\n activeValue: \"\",\n setActiveValue: () => {},\n };\n }\n\n return context;\n}\n\ntype TabItem = {\n confirm?: {\n required?: boolean;\n };\n label: string;\n value: string;\n};\n\nfunction getTabs(node: Node): TabItem[] {\n return (node.schema ?? [])\n .filter((child) => child.type === \"tab\")\n .map((child) => {\n const props = child.props as unknown as Tab;\n\n return {\n confirm: props.confirm ? { required: props.confirm.required } : undefined,\n label: props.label,\n value: props.value,\n };\n })\n .filter((tab) => tab.value !== \"\");\n}\n\nfunction queryValue(queryKey: string, tabs: Array<{ value: string }>): string | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n const value = new URLSearchParams(window.location.search).get(queryKey);\n\n if (!value || !tabs.some((tab) => tab.value === value)) {\n return null;\n }\n\n return value;\n}\n\nfunction replaceQueryValue(queryKey: string, value: string): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n const url = new URL(window.location.href);\n url.searchParams.set(queryKey, value);\n window.history.replaceState(window.history.state, \"\", `${url.pathname}${url.search}${url.hash}`);\n}\n\nfunction queryUrl(queryKey: string, value: string): string {\n if (typeof window === \"undefined\") {\n return \"\";\n }\n\n const url = new URL(window.location.href);\n url.searchParams.set(queryKey, value);\n\n return `${url.pathname}${url.search}${url.hash}`;\n}\n\nexport const TabsComponent: RendererComponent<\"tabs\"> = ({ children, node }) => {\n const { t } = useT(\"lattice\");\n const tabs = useMemo(() => getTabs(node), [node]);\n const firstValue = tabs[0]?.value ?? \"\";\n const queryKey = node.props.queryKey;\n const orientation = node.props.orientation;\n const isVertical = orientation === \"vertical\";\n const alignment = node.props.alignment;\n const isStretched = !isVertical && alignment === \"stretch\";\n const serverActiveValue = node.props.activeValue;\n const defaultValue = node.props.defaultValue ?? firstValue;\n const tablistRef = useRef<HTMLDivElement>(null);\n const [activeValue, setActiveTabValue] = useState(\n () => serverActiveValue || (queryValue(queryKey, tabs) ?? defaultValue) || firstValue,\n );\n\n function selectTab(tab: TabItem): void {\n if (tab.confirm?.required) {\n router.visit(queryUrl(queryKey, tab.value), {\n preserveScroll: true,\n });\n\n return;\n }\n\n setActiveTabValue(tab.value);\n replaceQueryValue(queryKey, tab.value);\n }\n\n function selectTabValue(value: string): void {\n const tab = tabs.find((item) => item.value === value);\n\n if (tab) {\n selectTab(tab);\n }\n }\n\n function onTablistKeyDown(event: KeyboardEvent<HTMLButtonElement>): void {\n const nextKey = isVertical ? \"ArrowDown\" : \"ArrowRight\";\n const prevKey = isVertical ? \"ArrowUp\" : \"ArrowLeft\";\n\n if (\n event.key !== nextKey &&\n event.key !== prevKey &&\n event.key !== \"Home\" &&\n event.key !== \"End\"\n ) {\n return;\n }\n\n const buttons = Array.from(\n tablistRef.current?.querySelectorAll<HTMLButtonElement>('[role=\"tab\"]') ?? [],\n );\n\n if (buttons.length === 0) {\n return;\n }\n\n event.preventDefault();\n const current = buttons.indexOf(document.activeElement as HTMLButtonElement);\n\n let index: number;\n if (event.key === \"Home\") {\n index = 0;\n } else if (event.key === \"End\") {\n index = buttons.length - 1;\n } else {\n const delta = event.key === nextKey ? 1 : -1;\n const base = current < 0 ? 0 : current;\n index = (base + delta + buttons.length) % buttons.length;\n }\n\n buttons[index]?.focus();\n }\n\n return (\n <TabsContext.Provider value={{ activeValue, setActiveValue: selectTabValue }}>\n <div\n className={cn(\n \"gap-6\",\n isVertical ? cn(\"flex\", alignment === \"end\" && \"flex-row-reverse\") : \"grid\",\n )}\n data-lattice-tabs={node.key ?? node.id}\n >\n <div\n aria-label={t(\"common.tabs\", \"Tabs\")}\n aria-orientation={orientation}\n className={cn(\n \"gap-1 rounded-lt bg-lt-muted p-1\",\n isVertical && \"flex flex-col\",\n isStretched && \"flex w-full\",\n !isVertical &&\n !isStretched &&\n cn(\"inline-flex w-fit max-w-full overflow-x-auto\", {\n \"justify-self-center\": alignment === \"center\",\n \"justify-self-end\": alignment === \"end\",\n }),\n )}\n ref={tablistRef}\n role=\"tablist\"\n >\n {tabs.map((tab) => {\n const isActive = activeValue === tab.value;\n\n return (\n <button\n aria-controls={`${tab.value}-panel`}\n aria-selected={isActive}\n data-test={`tab-${tab.value}`}\n className={cn(\n \"whitespace-nowrap rounded-lt-sm px-3 py-1.5 text-sm font-medium transition-colors\",\n isActive\n ? \"bg-lt-bg text-lt-fg shadow-lt-xs\"\n : \"text-lt-muted-fg hover:bg-lt-bg/60 hover:text-lt-fg\",\n isVertical && \"text-left\",\n isStretched && \"flex-1\",\n )}\n id={`${tab.value}-tab`}\n key={tab.value}\n onClick={() => selectTab(tab)}\n onKeyDown={onTablistKeyDown}\n role=\"tab\"\n tabIndex={isActive ? 0 : -1}\n type=\"button\"\n >\n {tab.label}\n </button>\n );\n })}\n </div>\n\n <div className={cn(\"min-w-0\", isVertical && \"flex-1\")}>{children}</div>\n </div>\n </TabsContext.Provider>\n );\n};\n\nconst TabComponent: RendererComponent<\"tab\"> = ({ children, node }) => {\n const { activeValue } = useTabsContext();\n const value = node.props.value;\n const isActive = activeValue === value;\n const [hasOpened, setHasOpened] = useState(isActive);\n\n useEffect(() => {\n if (isActive) {\n setHasOpened(true);\n }\n }, [isActive]);\n\n return (\n <section\n aria-labelledby={`${value}-tab`}\n className={cn(\"space-y-8\", !isActive && \"hidden\")}\n hidden={!isActive}\n id={`${value}-panel`}\n role=\"tabpanel\"\n tabIndex={0}\n >\n {hasOpened ? (children as ReactNode) : null}\n </section>\n );\n};\n\nexport default TabComponent;\n"],"mappings":"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"}
1
+ {"version":3,"file":"tabs.js","names":[],"sources":["../../resources/js/ui/tabs.tsx"],"sourcesContent":["import { router } from \"@inertiajs/react\";\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport type { Node, RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport type { Tab } from \"@lattice-php/lattice/types/generated\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\nimport { pillClassName } from \"./pill\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\n\ntype TabsContextValue = {\n activeValue: string;\n setActiveValue: (value: string) => void;\n};\n\nconst TabsContext = createContext<TabsContextValue | null>(null);\n\nfunction useTabsContext(): TabsContextValue {\n const context = useContext(TabsContext);\n\n if (!context) {\n return {\n activeValue: \"\",\n setActiveValue: () => {},\n };\n }\n\n return context;\n}\n\ntype TabItem = {\n confirm?: {\n required?: boolean;\n };\n label: string;\n value: string;\n};\n\nfunction getTabs(node: Node): TabItem[] {\n return (node.schema ?? [])\n .filter((child) => child.type === \"tab\")\n .map((child) => {\n const props = child.props as unknown as Tab;\n\n return {\n confirm: props.confirm ? { required: props.confirm.required } : undefined,\n label: props.label,\n value: props.value,\n };\n })\n .filter((tab) => tab.value !== \"\");\n}\n\nfunction queryValue(queryKey: string, tabs: Array<{ value: string }>): string | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n const value = new URLSearchParams(window.location.search).get(queryKey);\n\n if (!value || !tabs.some((tab) => tab.value === value)) {\n return null;\n }\n\n return value;\n}\n\nfunction replaceQueryValue(queryKey: string, value: string): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n const url = new URL(window.location.href);\n url.searchParams.set(queryKey, value);\n window.history.replaceState(window.history.state, \"\", `${url.pathname}${url.search}${url.hash}`);\n}\n\nfunction queryUrl(queryKey: string, value: string): string {\n if (typeof window === \"undefined\") {\n return \"\";\n }\n\n const url = new URL(window.location.href);\n url.searchParams.set(queryKey, value);\n\n return `${url.pathname}${url.search}${url.hash}`;\n}\n\nexport const TabsComponent: RendererComponent<\"tabs\"> = ({ children, node }) => {\n const { t } = useT(\"lattice\");\n const tabs = useMemo(() => getTabs(node), [node]);\n const firstValue = tabs[0]?.value ?? \"\";\n const queryKey = node.props.queryKey;\n const orientation = node.props.orientation;\n const isVertical = orientation === \"vertical\";\n const alignment = node.props.alignment;\n const isStretched = !isVertical && alignment === \"stretch\";\n const serverActiveValue = node.props.activeValue;\n const defaultValue = node.props.defaultValue ?? firstValue;\n const tablistRef = useRef<HTMLDivElement>(null);\n const [activeValue, setActiveTabValue] = useState(\n () => serverActiveValue || (queryValue(queryKey, tabs) ?? defaultValue) || firstValue,\n );\n\n const selectTab = useCallback(\n (tab: TabItem): void => {\n if (tab.confirm?.required) {\n router.visit(queryUrl(queryKey, tab.value), {\n preserveScroll: true,\n });\n\n return;\n }\n\n setActiveTabValue(tab.value);\n replaceQueryValue(queryKey, tab.value);\n },\n [queryKey],\n );\n\n const selectTabValue = useCallback(\n (value: string): void => {\n const tab = tabs.find((item) => item.value === value);\n\n if (tab) {\n selectTab(tab);\n }\n },\n [selectTab, tabs],\n );\n\n const contextValue = useMemo(\n () => ({ activeValue, setActiveValue: selectTabValue }),\n [activeValue, selectTabValue],\n );\n\n function onTablistKeyDown(event: KeyboardEvent<HTMLButtonElement>): void {\n const nextKey = isVertical ? \"ArrowDown\" : \"ArrowRight\";\n const prevKey = isVertical ? \"ArrowUp\" : \"ArrowLeft\";\n\n if (\n event.key !== nextKey &&\n event.key !== prevKey &&\n event.key !== \"Home\" &&\n event.key !== \"End\"\n ) {\n return;\n }\n\n const buttons = Array.from(\n tablistRef.current?.querySelectorAll<HTMLButtonElement>('[role=\"tab\"]') ?? [],\n );\n\n if (buttons.length === 0) {\n return;\n }\n\n event.preventDefault();\n const current = buttons.indexOf(document.activeElement as HTMLButtonElement);\n\n let index: number;\n if (event.key === \"Home\") {\n index = 0;\n } else if (event.key === \"End\") {\n index = buttons.length - 1;\n } else {\n const delta = event.key === nextKey ? 1 : -1;\n const base = current < 0 ? 0 : current;\n index = (base + delta + buttons.length) % buttons.length;\n }\n\n buttons[index]?.focus();\n }\n\n return (\n <TabsContext.Provider value={contextValue}>\n <div\n className={cn(\n \"gap-6\",\n isVertical ? cn(\"flex\", alignment === \"end\" && \"flex-row-reverse\") : \"grid\",\n )}\n data-lattice-tabs={node.key ?? node.id}\n >\n <div\n aria-label={t(\"common.tabs\", \"Tabs\")}\n aria-orientation={orientation}\n className={cn(\n \"gap-1 rounded-lt bg-lt-muted p-1\",\n isVertical && \"flex flex-col\",\n isStretched && \"flex w-full\",\n !isVertical &&\n !isStretched &&\n cn(\"inline-flex w-fit max-w-full overflow-x-auto\", {\n \"justify-self-center\": alignment === \"center\",\n \"justify-self-end\": alignment === \"end\",\n }),\n )}\n ref={tablistRef}\n role=\"tablist\"\n >\n {tabs.map((tab) => {\n const isActive = activeValue === tab.value;\n\n return (\n <button\n aria-controls={`${tab.value}-panel`}\n aria-selected={isActive}\n data-test={`tab-${tab.value}`}\n className={cn(\n pillClassName(isActive),\n isVertical && \"text-left\",\n isStretched && \"flex-1\",\n )}\n id={`${tab.value}-tab`}\n key={tab.value}\n onClick={() => selectTab(tab)}\n onKeyDown={onTablistKeyDown}\n role=\"tab\"\n tabIndex={isActive ? 0 : -1}\n type=\"button\"\n >\n {tab.label}\n </button>\n );\n })}\n </div>\n\n <div className={cn(\"min-w-0\", isVertical && \"flex-1\")}>{children}</div>\n </div>\n </TabsContext.Provider>\n );\n};\n\nconst TabComponent: RendererComponent<\"tab\"> = ({ children, node }) => {\n const { activeValue } = useTabsContext();\n const value = node.props.value;\n const isActive = activeValue === value;\n const [hasOpened, setHasOpened] = useState(isActive);\n\n useEffect(() => {\n if (isActive) {\n setHasOpened(true);\n }\n }, [isActive]);\n\n return (\n <section\n aria-labelledby={`${value}-tab`}\n className={cn(\"space-y-8\", !isActive && \"hidden\")}\n hidden={!isActive}\n id={`${value}-panel`}\n role=\"tabpanel\"\n tabIndex={0}\n >\n {hasOpened ? (children as ReactNode) : null}\n </section>\n );\n};\n\nexport default TabComponent;\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { usePersistentState } from "../lib/use-persistent-state.js";
1
2
  import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
2
3
  var TreeContext = createContext({
3
4
  activate: () => {},
@@ -14,23 +15,21 @@ var TreeContext = createContext({
14
15
  function useTreeContext() {
15
16
  return useContext(TreeContext);
16
17
  }
17
- function readStoredExpanded(key, remember, fallback) {
18
- if (!remember || typeof window === "undefined") return new Set(fallback);
19
- const stored = window.localStorage.getItem(key);
20
- if (stored === null) return new Set(fallback);
21
- try {
22
- const parsed = JSON.parse(stored);
23
- return Array.isArray(parsed) ? new Set(parsed.filter((id) => typeof id === "string")) : new Set(fallback);
24
- } catch {
25
- return new Set(fallback);
26
- }
18
+ function parseExpanded(raw) {
19
+ const parsed = JSON.parse(raw);
20
+ if (!Array.isArray(parsed)) throw new Error("expected an array of ids");
21
+ return new Set(parsed.filter((id) => typeof id === "string"));
27
22
  }
28
23
  function visibleOrder(registry) {
29
24
  return [...registry.values()].sort((a, b) => a.orderPath.localeCompare(b.orderPath));
30
25
  }
31
26
  var TYPEAHEAD_IDLE_MS = 800;
32
27
  function useTreeState({ activeId: initialActiveId, defaultExpanded, nodes, rememberState, storageKey }) {
33
- const [expanded, setExpanded] = useState(() => readStoredExpanded(storageKey, rememberState, defaultExpanded));
28
+ const [expanded, setExpanded] = usePersistentState(storageKey, () => new Set(defaultExpanded), {
29
+ enabled: rememberState,
30
+ parse: parseExpanded,
31
+ serialize: (value) => JSON.stringify([...value])
32
+ });
34
33
  const [activeId, setActiveId] = useState(initialActiveId);
35
34
  const [focusedId, setFocusedId] = useState(() => nodes[0]?.id ?? null);
36
35
  const registryRef = useRef(/* @__PURE__ */ new Map());
@@ -43,10 +42,9 @@ function useTreeState({ activeId: initialActiveId, defaultExpanded, nodes, remem
43
42
  const next = new Set(current);
44
43
  if (next.has(id)) next.delete(id);
45
44
  else next.add(id);
46
- if (rememberState && typeof window !== "undefined") window.localStorage.setItem(storageKey, JSON.stringify([...next]));
47
45
  return next;
48
46
  });
49
- }, [rememberState, storageKey]);
47
+ }, [setExpanded]);
50
48
  const activate = useCallback((id) => setActiveId(id), []);
51
49
  const focus = useCallback((id) => {
52
50
  setFocusedId(id);
@@ -1 +1 @@
1
- {"version":3,"file":"tree-context.js","names":[],"sources":["../../resources/js/ui/tree-context.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useMemo, useRef, useState } from \"react\";\nimport type { RefObject } from \"react\";\n\nexport type TreeItemRegistration = {\n id: string;\n label: string;\n orderPath: string;\n parentPath: string | null;\n path: string;\n ref: RefObject<HTMLLIElement | null>;\n};\n\nexport type TreeFocusDirection = \"first\" | \"firstChild\" | \"last\" | \"next\" | \"parent\" | \"prev\";\n\nexport type TreeContextValue = {\n activate: (id: string) => void;\n activeId: string | null;\n expanded: Set<string>;\n focus: (id: string) => void;\n focusedId: string | null;\n moveFocus: (fromId: string, direction: TreeFocusDirection) => void;\n register: (entry: TreeItemRegistration) => void;\n toggle: (id: string) => void;\n typeAhead: (fromId: string, character: string) => void;\n unregister: (path: string) => void;\n};\n\nconst defaultTreeContext: TreeContextValue = {\n activate: () => {},\n activeId: null,\n expanded: new Set(),\n focus: () => {},\n focusedId: null,\n moveFocus: () => {},\n register: () => {},\n toggle: () => {},\n typeAhead: () => {},\n unregister: () => {},\n};\n\nexport const TreeContext = createContext<TreeContextValue>(defaultTreeContext);\n\nexport function useTreeContext(): TreeContextValue {\n return useContext(TreeContext);\n}\n\nfunction readStoredExpanded(key: string, remember: boolean, fallback: string[]): Set<string> {\n if (!remember || typeof window === \"undefined\") {\n return new Set(fallback);\n }\n\n const stored = window.localStorage.getItem(key);\n\n if (stored === null) {\n return new Set(fallback);\n }\n\n try {\n const parsed: unknown = JSON.parse(stored);\n\n return Array.isArray(parsed)\n ? new Set(parsed.filter((id): id is string => typeof id === \"string\"))\n : new Set(fallback);\n } catch {\n return new Set(fallback);\n }\n}\n\nfunction visibleOrder(registry: Map<string, TreeItemRegistration>): TreeItemRegistration[] {\n return [...registry.values()].sort((a, b) => a.orderPath.localeCompare(b.orderPath));\n}\n\nconst TYPEAHEAD_IDLE_MS = 800;\n\nexport function useTreeState({\n activeId: initialActiveId,\n defaultExpanded,\n nodes,\n rememberState,\n storageKey,\n}: {\n activeId: string | null;\n defaultExpanded: string[];\n nodes: Array<{ id: string }>;\n rememberState: boolean;\n storageKey: string;\n}): TreeContextValue {\n const [expanded, setExpanded] = useState<Set<string>>(() =>\n readStoredExpanded(storageKey, rememberState, defaultExpanded),\n );\n const [activeId, setActiveId] = useState<string | null>(initialActiveId);\n const [focusedId, setFocusedId] = useState<string | null>(() => nodes[0]?.id ?? null);\n const registryRef = useRef<Map<string, TreeItemRegistration>>(new Map());\n const typeAheadRef = useRef<{ text: string; timestamp: number }>({ text: \"\", timestamp: 0 });\n\n const toggle = useCallback(\n (id: string) => {\n setExpanded((current) => {\n const next = new Set(current);\n\n if (next.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n\n if (rememberState && typeof window !== \"undefined\") {\n window.localStorage.setItem(storageKey, JSON.stringify([...next]));\n }\n\n return next;\n });\n },\n [rememberState, storageKey],\n );\n\n const activate = useCallback((id: string) => setActiveId(id), []);\n\n const focus = useCallback((id: string) => {\n setFocusedId(id);\n const entry = [...registryRef.current.values()].find((candidate) => candidate.id === id);\n entry?.ref.current?.focus();\n }, []);\n\n const register = useCallback((entry: TreeItemRegistration) => {\n registryRef.current.set(entry.path, entry);\n }, []);\n\n const unregister = useCallback((path: string) => {\n registryRef.current.delete(path);\n }, []);\n\n const moveFocus = useCallback(\n (fromId: string, direction: TreeFocusDirection) => {\n const order = visibleOrder(registryRef.current);\n\n if (order.length === 0) {\n return;\n }\n\n const index = order.findIndex((entry) => entry.id === fromId);\n const current = index === -1 ? undefined : order[index];\n\n let target: TreeItemRegistration | undefined;\n\n switch (direction) {\n case \"next\":\n target = index === -1 ? undefined : order[index + 1];\n break;\n case \"prev\":\n target = index === -1 ? undefined : order[index - 1];\n break;\n case \"first\":\n target = order[0];\n break;\n case \"last\":\n target = order[order.length - 1];\n break;\n case \"parent\":\n target = current ? order.find((entry) => entry.path === current.parentPath) : undefined;\n break;\n case \"firstChild\":\n target = current ? order.find((entry) => entry.parentPath === current.path) : undefined;\n break;\n }\n\n if (target) {\n focus(target.id);\n }\n },\n [focus],\n );\n\n const typeAhead = useCallback(\n (fromId: string, character: string) => {\n const order = visibleOrder(registryRef.current);\n\n if (order.length === 0) {\n return;\n }\n\n const now = Date.now();\n const buffer = typeAheadRef.current;\n const text = now - buffer.timestamp > TYPEAHEAD_IDLE_MS ? character : buffer.text + character;\n typeAheadRef.current = { text, timestamp: now };\n\n const needle = text.toLowerCase();\n const startIndex = order.findIndex((entry) => entry.id === fromId);\n const start = startIndex === -1 ? 0 : startIndex;\n\n for (let offset = 1; offset <= order.length; offset++) {\n const candidate = order[(start + offset) % order.length];\n\n if (candidate.label.toLowerCase().startsWith(needle)) {\n focus(candidate.id);\n return;\n }\n }\n },\n [focus],\n );\n\n return useMemo(\n () => ({\n activate,\n activeId,\n expanded,\n focus,\n focusedId,\n moveFocus,\n register,\n toggle,\n typeAhead,\n unregister,\n }),\n [\n activate,\n activeId,\n expanded,\n focus,\n focusedId,\n moveFocus,\n register,\n toggle,\n typeAhead,\n unregister,\n ],\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tree-context.js","names":[],"sources":["../../resources/js/ui/tree-context.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useMemo, useRef, useState } from \"react\";\nimport type { RefObject } from \"react\";\nimport { usePersistentState } from \"@lattice-php/lattice/lib/use-persistent-state\";\n\nexport type TreeItemRegistration = {\n id: string;\n label: string;\n orderPath: string;\n parentPath: string | null;\n path: string;\n ref: RefObject<HTMLLIElement | null>;\n};\n\nexport type TreeFocusDirection = \"first\" | \"firstChild\" | \"last\" | \"next\" | \"parent\" | \"prev\";\n\nexport type TreeContextValue = {\n activate: (id: string) => void;\n activeId: string | null;\n expanded: Set<string>;\n focus: (id: string) => void;\n focusedId: string | null;\n moveFocus: (fromId: string, direction: TreeFocusDirection) => void;\n register: (entry: TreeItemRegistration) => void;\n toggle: (id: string) => void;\n typeAhead: (fromId: string, character: string) => void;\n unregister: (path: string) => void;\n};\n\nconst defaultTreeContext: TreeContextValue = {\n activate: () => {},\n activeId: null,\n expanded: new Set(),\n focus: () => {},\n focusedId: null,\n moveFocus: () => {},\n register: () => {},\n toggle: () => {},\n typeAhead: () => {},\n unregister: () => {},\n};\n\nexport const TreeContext = createContext<TreeContextValue>(defaultTreeContext);\n\nexport function useTreeContext(): TreeContextValue {\n return useContext(TreeContext);\n}\n\nfunction parseExpanded(raw: string): Set<string> {\n const parsed: unknown = JSON.parse(raw);\n\n if (!Array.isArray(parsed)) {\n throw new Error(\"expected an array of ids\");\n }\n\n return new Set(parsed.filter((id): id is string => typeof id === \"string\"));\n}\n\nfunction visibleOrder(registry: Map<string, TreeItemRegistration>): TreeItemRegistration[] {\n return [...registry.values()].sort((a, b) => a.orderPath.localeCompare(b.orderPath));\n}\n\nconst TYPEAHEAD_IDLE_MS = 800;\n\nexport function useTreeState({\n activeId: initialActiveId,\n defaultExpanded,\n nodes,\n rememberState,\n storageKey,\n}: {\n activeId: string | null;\n defaultExpanded: string[];\n nodes: Array<{ id: string }>;\n rememberState: boolean;\n storageKey: string;\n}): TreeContextValue {\n const [expanded, setExpanded] = usePersistentState<Set<string>>(\n storageKey,\n () => new Set(defaultExpanded),\n {\n enabled: rememberState,\n parse: parseExpanded,\n serialize: (value) => JSON.stringify([...value]),\n },\n );\n const [activeId, setActiveId] = useState<string | null>(initialActiveId);\n const [focusedId, setFocusedId] = useState<string | null>(() => nodes[0]?.id ?? null);\n const registryRef = useRef<Map<string, TreeItemRegistration>>(new Map());\n const typeAheadRef = useRef<{ text: string; timestamp: number }>({ text: \"\", timestamp: 0 });\n\n const toggle = useCallback(\n (id: string) => {\n setExpanded((current) => {\n const next = new Set(current);\n\n if (next.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n\n return next;\n });\n },\n [setExpanded],\n );\n\n const activate = useCallback((id: string) => setActiveId(id), []);\n\n const focus = useCallback((id: string) => {\n setFocusedId(id);\n const entry = [...registryRef.current.values()].find((candidate) => candidate.id === id);\n entry?.ref.current?.focus();\n }, []);\n\n const register = useCallback((entry: TreeItemRegistration) => {\n registryRef.current.set(entry.path, entry);\n }, []);\n\n const unregister = useCallback((path: string) => {\n registryRef.current.delete(path);\n }, []);\n\n const moveFocus = useCallback(\n (fromId: string, direction: TreeFocusDirection) => {\n const order = visibleOrder(registryRef.current);\n\n if (order.length === 0) {\n return;\n }\n\n const index = order.findIndex((entry) => entry.id === fromId);\n const current = index === -1 ? undefined : order[index];\n\n let target: TreeItemRegistration | undefined;\n\n switch (direction) {\n case \"next\":\n target = index === -1 ? undefined : order[index + 1];\n break;\n case \"prev\":\n target = index === -1 ? undefined : order[index - 1];\n break;\n case \"first\":\n target = order[0];\n break;\n case \"last\":\n target = order[order.length - 1];\n break;\n case \"parent\":\n target = current ? order.find((entry) => entry.path === current.parentPath) : undefined;\n break;\n case \"firstChild\":\n target = current ? order.find((entry) => entry.parentPath === current.path) : undefined;\n break;\n }\n\n if (target) {\n focus(target.id);\n }\n },\n [focus],\n );\n\n const typeAhead = useCallback(\n (fromId: string, character: string) => {\n const order = visibleOrder(registryRef.current);\n\n if (order.length === 0) {\n return;\n }\n\n const now = Date.now();\n const buffer = typeAheadRef.current;\n const text = now - buffer.timestamp > TYPEAHEAD_IDLE_MS ? character : buffer.text + character;\n typeAheadRef.current = { text, timestamp: now };\n\n const needle = text.toLowerCase();\n const startIndex = order.findIndex((entry) => entry.id === fromId);\n const start = startIndex === -1 ? 0 : startIndex;\n\n for (let offset = 1; offset <= order.length; offset++) {\n const candidate = order[(start + offset) % order.length];\n\n if (candidate.label.toLowerCase().startsWith(needle)) {\n focus(candidate.id);\n return;\n }\n }\n },\n [focus],\n );\n\n return useMemo(\n () => ({\n activate,\n activeId,\n expanded,\n focus,\n focusedId,\n moveFocus,\n register,\n toggle,\n typeAhead,\n unregister,\n }),\n [\n activate,\n activeId,\n expanded,\n focus,\n focusedId,\n moveFocus,\n register,\n toggle,\n typeAhead,\n unregister,\n ],\n );\n}\n"],"mappings":"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"}
package/dist/ui/tree.js CHANGED
@@ -54,7 +54,7 @@ function TreeItem({ depth, node, orderPath, parentPath, siblingCount, siblingInd
54
54
  container.querySelectorAll("button, a[href], [tabindex]").forEach((control) => {
55
55
  control.tabIndex = -1;
56
56
  });
57
- });
57
+ }, [node.actions]);
58
58
  function onKeyDown(event) {
59
59
  if (event.target !== event.currentTarget) return;
60
60
  switch (event.key) {
@@ -1 +1 @@
1
- {"version":3,"file":"tree.js","names":[],"sources":["../../resources/js/ui/tree.tsx"],"sourcesContent":["import { router } from \"@inertiajs/react\";\nimport { useEffect, useRef } from \"react\";\nimport type { KeyboardEvent } from \"react\";\nimport { Renderer } from \"@lattice-php/lattice/core/renderer\";\nimport { nodeIdentity } from \"@lattice-php/lattice/core/test-id\";\nimport type { RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { Icon, IconRenderer } from \"@lattice-php/lattice/icons\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\nimport type {\n Tree as TreeProps,\n TreeNode as GeneratedTreeNode,\n} from \"@lattice-php/lattice/types/generated\";\nimport { Badge } from \"./badge\";\nimport { TextLink } from \"./link\";\nimport { TreeContext, useTreeContext, useTreeState } from \"./tree-context\";\n\n/**\n * The actual sparse wire shape a tree node serializes as (see `TreeNode::jsonSerialize()`):\n * every optional/falsy field is omitted rather than sent as `null`/`false`, unlike the\n * generated `TreeNode`, which reflects every declared PHP property as required.\n */\nexport type TreeNodeData = Pick<GeneratedTreeNode, \"id\" | \"label\"> &\n Partial<Omit<GeneratedTreeNode, \"children\" | \"id\" | \"label\">> & {\n children?: TreeNodeData[];\n };\n\ndeclare module \"@lattice-php/lattice/core/types\" {\n interface ComponentProps {\n tree: TreeProps & { nodes: TreeNodeData[] };\n }\n}\n\nfunction hasExpandableChildren(node: TreeNodeData): boolean {\n return Boolean(node.children?.length) || node.hasChildren === true;\n}\n\nconst ORDER_PATH_SEGMENT_WIDTH = 6;\n\nfunction orderPathSegment(index: number): string {\n return String(index).padStart(ORDER_PATH_SEGMENT_WIDTH, \"0\");\n}\n\nfunction TreeItem({\n depth,\n node,\n orderPath,\n parentPath,\n siblingCount,\n siblingIndex,\n}: {\n depth: number;\n node: TreeNodeData;\n orderPath: string;\n parentPath: string | null;\n siblingCount: number;\n siblingIndex: number;\n}) {\n const {\n activate,\n activeId,\n expanded,\n focusedId,\n moveFocus,\n register,\n toggle,\n typeAhead,\n unregister,\n } = useTreeContext();\n const { t } = useT(\"lattice\");\n const ref = useRef<HTMLLIElement>(null);\n const path = parentPath ? `${parentPath}/${node.id}` : node.id;\n const isExpanded = expanded.has(node.id);\n const isActive = activeId === node.id;\n const isFocused = focusedId === node.id;\n const isDisabled = node.disabled === true;\n const expandable = hasExpandableChildren(node);\n const actionsRef = useRef<HTMLSpanElement>(null);\n\n useEffect(() => {\n register({ id: node.id, label: node.label, orderPath, parentPath, path, ref });\n\n return () => unregister(path);\n }, [node.id, node.label, orderPath, parentPath, path, register, unregister]);\n\n useEffect(() => {\n const container = actionsRef.current;\n\n if (!container) {\n return;\n }\n\n container.querySelectorAll<HTMLElement>(\"button, a[href], [tabindex]\").forEach((control) => {\n control.tabIndex = -1;\n });\n });\n\n function onKeyDown(event: KeyboardEvent<HTMLLIElement>): void {\n if (event.target !== event.currentTarget) {\n return;\n }\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n moveFocus(node.id, \"next\");\n return;\n case \"ArrowUp\":\n event.preventDefault();\n moveFocus(node.id, \"prev\");\n return;\n case \"ArrowRight\":\n event.preventDefault();\n if (expandable && !isExpanded) {\n toggle(node.id);\n } else if (expandable) {\n moveFocus(node.id, \"firstChild\");\n }\n return;\n case \"ArrowLeft\":\n event.preventDefault();\n if (expandable && isExpanded) {\n toggle(node.id);\n } else {\n moveFocus(node.id, \"parent\");\n }\n return;\n case \"Home\":\n event.preventDefault();\n moveFocus(node.id, \"first\");\n return;\n case \"End\":\n event.preventDefault();\n moveFocus(node.id, \"last\");\n return;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n if (isDisabled) {\n return;\n }\n if (node.href) {\n router.visit(node.href);\n } else if (node.actions) {\n actionsRef.current?.querySelector(\"button\")?.click();\n ref.current?.focus();\n } else {\n activate(node.id);\n }\n return;\n default:\n if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {\n typeAhead(node.id, event.key);\n }\n }\n }\n\n return (\n <li\n aria-disabled={isDisabled}\n aria-expanded={expandable ? isExpanded : undefined}\n aria-level={depth}\n aria-posinset={siblingIndex}\n aria-selected={isActive}\n aria-setsize={siblingCount}\n data-test={`tree-node-${node.id}`}\n onKeyDown={onKeyDown}\n ref={ref}\n role=\"treeitem\"\n tabIndex={isFocused ? 0 : -1}\n >\n <div\n className={cn(\n \"flex items-center gap-2 rounded-lt-sm px-2 py-1.5 text-sm text-lt-fg\",\n isActive && \"bg-lt-muted font-medium\",\n isDisabled && \"pointer-events-none opacity-50\",\n )}\n >\n {expandable ? (\n <button\n aria-label={\n isExpanded\n ? t(\"common.tree.collapse\", \"Collapse {{label}}\", { label: node.label })\n : t(\"common.tree.expand\", \"Expand {{label}}\", { label: node.label })\n }\n data-test={`tree-node-${node.id}-toggle`}\n onClick={() => toggle(node.id)}\n tabIndex={-1}\n type=\"button\"\n >\n <Icon\n className={cn(\n \"size-lt-icon-md shrink-0 transition-transform\",\n isExpanded && \"rotate-90\",\n )}\n name=\"chevron-right\"\n />\n </button>\n ) : null}\n {node.icon ? <IconRenderer className=\"size-lt-icon-md shrink-0\" icon={node.icon} /> : null}\n {node.href && !isDisabled ? (\n <TextLink href={node.href} tabIndex={-1}>\n {node.label}\n </TextLink>\n ) : (\n <span>{node.label}</span>\n )}\n {node.badge ? <Badge variant=\"secondary\">{node.badge}</Badge> : null}\n {node.actions ? (\n <span className=\"ml-auto\" ref={actionsRef}>\n <Renderer nodes={[node.actions]} />\n </span>\n ) : null}\n </div>\n {expandable && isExpanded && node.children && node.children.length > 0 ? (\n <ul className=\"pl-6\" role=\"group\">\n {node.children.map((child, index) => (\n <TreeItem\n depth={depth + 1}\n key={child.id}\n node={child}\n orderPath={`${orderPath}.${orderPathSegment(index)}`}\n parentPath={path}\n siblingCount={node.children?.length ?? 1}\n siblingIndex={index + 1}\n />\n ))}\n </ul>\n ) : null}\n </li>\n );\n}\n\nconst TreeComponent: RendererComponent<\"tree\"> = ({ node }) => {\n const identity = nodeIdentity(node);\n const value = useTreeState({\n activeId: node.props.activeId,\n defaultExpanded: node.props.defaultExpanded,\n nodes: node.props.nodes,\n rememberState: node.props.rememberState,\n storageKey: `lattice:tree:${identity ?? \"default\"}`,\n });\n\n return (\n <TreeContext.Provider value={value}>\n <ul data-lattice-component={identity} role=\"tree\">\n {node.props.nodes.map((child, index) => (\n <TreeItem\n depth={1}\n key={child.id}\n node={child}\n orderPath={orderPathSegment(index)}\n parentPath={null}\n siblingCount={node.props.nodes.length}\n siblingIndex={index + 1}\n />\n ))}\n </ul>\n </TreeContext.Provider>\n );\n};\n\nexport default TreeComponent;\n"],"mappings":"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"}
1
+ {"version":3,"file":"tree.js","names":[],"sources":["../../resources/js/ui/tree.tsx"],"sourcesContent":["import { router } from \"@inertiajs/react\";\nimport { useEffect, useRef } from \"react\";\nimport type { KeyboardEvent } from \"react\";\nimport { Renderer } from \"@lattice-php/lattice/core/renderer\";\nimport { nodeIdentity } from \"@lattice-php/lattice/core/test-id\";\nimport type { RendererComponent } from \"@lattice-php/lattice/core/types\";\nimport { Icon, IconRenderer } from \"@lattice-php/lattice/icons\";\nimport { cn } from \"@lattice-php/lattice/lib/utils\";\nimport { useT } from \"@lattice-php/lattice/i18n\";\nimport type {\n Tree as TreeProps,\n TreeNode as GeneratedTreeNode,\n} from \"@lattice-php/lattice/types/generated\";\nimport { Badge } from \"./badge\";\nimport { TextLink } from \"./link\";\nimport { TreeContext, useTreeContext, useTreeState } from \"./tree-context\";\n\n/**\n * The actual sparse wire shape a tree node serializes as (see `TreeNode::jsonSerialize()`):\n * every optional/falsy field is omitted rather than sent as `null`/`false`, unlike the\n * generated `TreeNode`, which reflects every declared PHP property as required.\n */\nexport type TreeNodeData = Pick<GeneratedTreeNode, \"id\" | \"label\"> &\n Partial<Omit<GeneratedTreeNode, \"children\" | \"id\" | \"label\">> & {\n children?: TreeNodeData[];\n };\n\ndeclare module \"@lattice-php/lattice/core/types\" {\n interface ComponentProps {\n tree: TreeProps & { nodes: TreeNodeData[] };\n }\n}\n\nfunction hasExpandableChildren(node: TreeNodeData): boolean {\n return Boolean(node.children?.length) || node.hasChildren === true;\n}\n\nconst ORDER_PATH_SEGMENT_WIDTH = 6;\n\nfunction orderPathSegment(index: number): string {\n return String(index).padStart(ORDER_PATH_SEGMENT_WIDTH, \"0\");\n}\n\nfunction TreeItem({\n depth,\n node,\n orderPath,\n parentPath,\n siblingCount,\n siblingIndex,\n}: {\n depth: number;\n node: TreeNodeData;\n orderPath: string;\n parentPath: string | null;\n siblingCount: number;\n siblingIndex: number;\n}) {\n const {\n activate,\n activeId,\n expanded,\n focusedId,\n moveFocus,\n register,\n toggle,\n typeAhead,\n unregister,\n } = useTreeContext();\n const { t } = useT(\"lattice\");\n const ref = useRef<HTMLLIElement>(null);\n const path = parentPath ? `${parentPath}/${node.id}` : node.id;\n const isExpanded = expanded.has(node.id);\n const isActive = activeId === node.id;\n const isFocused = focusedId === node.id;\n const isDisabled = node.disabled === true;\n const expandable = hasExpandableChildren(node);\n const actionsRef = useRef<HTMLSpanElement>(null);\n\n useEffect(() => {\n register({ id: node.id, label: node.label, orderPath, parentPath, path, ref });\n\n return () => unregister(path);\n }, [node.id, node.label, orderPath, parentPath, path, register, unregister]);\n\n useEffect(() => {\n const container = actionsRef.current;\n\n if (!container) {\n return;\n }\n\n container.querySelectorAll<HTMLElement>(\"button, a[href], [tabindex]\").forEach((control) => {\n control.tabIndex = -1;\n });\n }, [node.actions]);\n\n function onKeyDown(event: KeyboardEvent<HTMLLIElement>): void {\n if (event.target !== event.currentTarget) {\n return;\n }\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n moveFocus(node.id, \"next\");\n return;\n case \"ArrowUp\":\n event.preventDefault();\n moveFocus(node.id, \"prev\");\n return;\n case \"ArrowRight\":\n event.preventDefault();\n if (expandable && !isExpanded) {\n toggle(node.id);\n } else if (expandable) {\n moveFocus(node.id, \"firstChild\");\n }\n return;\n case \"ArrowLeft\":\n event.preventDefault();\n if (expandable && isExpanded) {\n toggle(node.id);\n } else {\n moveFocus(node.id, \"parent\");\n }\n return;\n case \"Home\":\n event.preventDefault();\n moveFocus(node.id, \"first\");\n return;\n case \"End\":\n event.preventDefault();\n moveFocus(node.id, \"last\");\n return;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n if (isDisabled) {\n return;\n }\n if (node.href) {\n router.visit(node.href);\n } else if (node.actions) {\n actionsRef.current?.querySelector(\"button\")?.click();\n ref.current?.focus();\n } else {\n activate(node.id);\n }\n return;\n default:\n if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {\n typeAhead(node.id, event.key);\n }\n }\n }\n\n return (\n <li\n aria-disabled={isDisabled}\n aria-expanded={expandable ? isExpanded : undefined}\n aria-level={depth}\n aria-posinset={siblingIndex}\n aria-selected={isActive}\n aria-setsize={siblingCount}\n data-test={`tree-node-${node.id}`}\n onKeyDown={onKeyDown}\n ref={ref}\n role=\"treeitem\"\n tabIndex={isFocused ? 0 : -1}\n >\n <div\n className={cn(\n \"flex items-center gap-2 rounded-lt-sm px-2 py-1.5 text-sm text-lt-fg\",\n isActive && \"bg-lt-muted font-medium\",\n isDisabled && \"pointer-events-none opacity-50\",\n )}\n >\n {expandable ? (\n <button\n aria-label={\n isExpanded\n ? t(\"common.tree.collapse\", \"Collapse {{label}}\", { label: node.label })\n : t(\"common.tree.expand\", \"Expand {{label}}\", { label: node.label })\n }\n data-test={`tree-node-${node.id}-toggle`}\n onClick={() => toggle(node.id)}\n tabIndex={-1}\n type=\"button\"\n >\n <Icon\n className={cn(\n \"size-lt-icon-md shrink-0 transition-transform\",\n isExpanded && \"rotate-90\",\n )}\n name=\"chevron-right\"\n />\n </button>\n ) : null}\n {node.icon ? <IconRenderer className=\"size-lt-icon-md shrink-0\" icon={node.icon} /> : null}\n {node.href && !isDisabled ? (\n <TextLink href={node.href} tabIndex={-1}>\n {node.label}\n </TextLink>\n ) : (\n <span>{node.label}</span>\n )}\n {node.badge ? <Badge variant=\"secondary\">{node.badge}</Badge> : null}\n {node.actions ? (\n <span className=\"ml-auto\" ref={actionsRef}>\n <Renderer nodes={[node.actions]} />\n </span>\n ) : null}\n </div>\n {expandable && isExpanded && node.children && node.children.length > 0 ? (\n <ul className=\"pl-6\" role=\"group\">\n {node.children.map((child, index) => (\n <TreeItem\n depth={depth + 1}\n key={child.id}\n node={child}\n orderPath={`${orderPath}.${orderPathSegment(index)}`}\n parentPath={path}\n siblingCount={node.children?.length ?? 1}\n siblingIndex={index + 1}\n />\n ))}\n </ul>\n ) : null}\n </li>\n );\n}\n\nconst TreeComponent: RendererComponent<\"tree\"> = ({ node }) => {\n const identity = nodeIdentity(node);\n const value = useTreeState({\n activeId: node.props.activeId,\n defaultExpanded: node.props.defaultExpanded,\n nodes: node.props.nodes,\n rememberState: node.props.rememberState,\n storageKey: `lattice:tree:${identity ?? \"default\"}`,\n });\n\n return (\n <TreeContext.Provider value={value}>\n <ul data-lattice-component={identity} role=\"tree\">\n {node.props.nodes.map((child, index) => (\n <TreeItem\n depth={1}\n key={child.id}\n node={child}\n orderPath={orderPathSegment(index)}\n parentPath={null}\n siblingCount={node.props.nodes.length}\n siblingIndex={index + 1}\n />\n ))}\n </ul>\n </TreeContext.Provider>\n );\n};\n\nexport default TreeComponent;\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Boolean open/collapsed state remembered in `localStorage` as `"true"`/`"false"`.
3
+ * Shared by the section, collapsible, and sidebar chrome. Callers resolve
4
+ * `rememberState` as `props.rememberState !== false`; the wire prop is always a
5
+ * boolean, so the polarity is uniform across all three.
6
+ */
7
+ export declare function useCollapsibleState(storageKey: string, fallback: boolean, rememberState: boolean): [boolean, () => void];
@@ -0,0 +1,21 @@
1
+ import { usePersistentState } from "../lib/use-persistent-state.js";
2
+ import { useCallback } from "react";
3
+ //#region resources/js/ui/use-collapsible-state.ts
4
+ /**
5
+ * Boolean open/collapsed state remembered in `localStorage` as `"true"`/`"false"`.
6
+ * Shared by the section, collapsible, and sidebar chrome. Callers resolve
7
+ * `rememberState` as `props.rememberState !== false`; the wire prop is always a
8
+ * boolean, so the polarity is uniform across all three.
9
+ */
10
+ function useCollapsibleState(storageKey, fallback, rememberState) {
11
+ const [value, setValue] = usePersistentState(storageKey, fallback, {
12
+ enabled: rememberState,
13
+ parse: (raw) => raw === "true",
14
+ serialize: String
15
+ });
16
+ return [value, useCallback(() => setValue((current) => !current), [setValue])];
17
+ }
18
+ //#endregion
19
+ export { useCollapsibleState };
20
+
21
+ //# sourceMappingURL=use-collapsible-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-collapsible-state.js","names":[],"sources":["../../resources/js/ui/use-collapsible-state.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { usePersistentState } from \"@lattice-php/lattice/lib/use-persistent-state\";\n\n/**\n * Boolean open/collapsed state remembered in `localStorage` as `\"true\"`/`\"false\"`.\n * Shared by the section, collapsible, and sidebar chrome. Callers resolve\n * `rememberState` as `props.rememberState !== false`; the wire prop is always a\n * boolean, so the polarity is uniform across all three.\n */\nexport function useCollapsibleState(\n storageKey: string,\n fallback: boolean,\n rememberState: boolean,\n): [boolean, () => void] {\n const [value, setValue] = usePersistentState<boolean>(storageKey, fallback, {\n enabled: rememberState,\n parse: (raw) => raw === \"true\",\n serialize: String,\n });\n\n const toggle = useCallback(() => setValue((current) => !current), [setValue]);\n\n return [value, toggle];\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,oBACd,YACA,UACA,eACuB;CACvB,MAAM,CAAC,OAAO,YAAY,mBAA4B,YAAY,UAAU;EAC1E,SAAS;EACT,QAAQ,QAAQ,QAAQ;EACxB,WAAW;CACb,CAAC;CAID,OAAO,CAAC,OAFO,kBAAkB,UAAU,YAAY,CAAC,OAAO,GAAG,CAAC,QAAQ,CAE5D,CAAM;AACvB"}