@entifix/next-shell 0.1.0-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 (271) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +21 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +19 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/back-office/account-menu.d.ts +29 -0
  8. package/dist/lib/back-office/account-menu.d.ts.map +1 -0
  9. package/dist/lib/back-office/account-menu.js +70 -0
  10. package/dist/lib/back-office/account-menu.js.map +1 -0
  11. package/dist/lib/back-office/back-office-shell.d.ts +50 -0
  12. package/dist/lib/back-office/back-office-shell.d.ts.map +1 -0
  13. package/dist/lib/back-office/back-office-shell.js +128 -0
  14. package/dist/lib/back-office/back-office-shell.js.map +1 -0
  15. package/dist/lib/back-office/breadcrumbs.d.ts +16 -0
  16. package/dist/lib/back-office/breadcrumbs.d.ts.map +1 -0
  17. package/dist/lib/back-office/breadcrumbs.js +53 -0
  18. package/dist/lib/back-office/breadcrumbs.js.map +1 -0
  19. package/dist/lib/back-office/group-by-screen-type.d.ts +21 -0
  20. package/dist/lib/back-office/group-by-screen-type.d.ts.map +1 -0
  21. package/dist/lib/back-office/group-by-screen-type.js +39 -0
  22. package/dist/lib/back-office/group-by-screen-type.js.map +1 -0
  23. package/dist/lib/back-office/index.d.ts +6 -0
  24. package/dist/lib/back-office/index.d.ts.map +1 -0
  25. package/dist/lib/back-office/index.js +7 -0
  26. package/dist/lib/back-office/index.js.map +1 -0
  27. package/dist/lib/back-office/nav.d.ts +39 -0
  28. package/dist/lib/back-office/nav.d.ts.map +1 -0
  29. package/dist/lib/back-office/nav.js +3 -0
  30. package/dist/lib/back-office/nav.js.map +1 -0
  31. package/dist/lib/back-office/sidebar-nav.d.ts +26 -0
  32. package/dist/lib/back-office/sidebar-nav.d.ts.map +1 -0
  33. package/dist/lib/back-office/sidebar-nav.js +181 -0
  34. package/dist/lib/back-office/sidebar-nav.js.map +1 -0
  35. package/dist/lib/command-palette/command-palette-host.d.ts +41 -0
  36. package/dist/lib/command-palette/command-palette-host.d.ts.map +1 -0
  37. package/dist/lib/command-palette/command-palette-host.js +196 -0
  38. package/dist/lib/command-palette/command-palette-host.js.map +1 -0
  39. package/dist/lib/command-palette/index.d.ts +9 -0
  40. package/dist/lib/command-palette/index.d.ts.map +1 -0
  41. package/dist/lib/command-palette/index.js +10 -0
  42. package/dist/lib/command-palette/index.js.map +1 -0
  43. package/dist/lib/command-palette/palette-command.d.ts +32 -0
  44. package/dist/lib/command-palette/palette-command.d.ts.map +1 -0
  45. package/dist/lib/command-palette/palette-command.js +19 -0
  46. package/dist/lib/command-palette/palette-command.js.map +1 -0
  47. package/dist/lib/command-palette/use-command-recency.d.ts +28 -0
  48. package/dist/lib/command-palette/use-command-recency.d.ts.map +1 -0
  49. package/dist/lib/command-palette/use-command-recency.js +67 -0
  50. package/dist/lib/command-palette/use-command-recency.js.map +1 -0
  51. package/dist/lib/command-palette/use-command-source.d.ts +17 -0
  52. package/dist/lib/command-palette/use-command-source.d.ts.map +1 -0
  53. package/dist/lib/command-palette/use-command-source.js +70 -0
  54. package/dist/lib/command-palette/use-command-source.js.map +1 -0
  55. package/dist/lib/command-palette/use-nav-source.d.ts +21 -0
  56. package/dist/lib/command-palette/use-nav-source.d.ts.map +1 -0
  57. package/dist/lib/command-palette/use-nav-source.js +78 -0
  58. package/dist/lib/command-palette/use-nav-source.js.map +1 -0
  59. package/dist/lib/command-palette/use-records-source.d.ts +23 -0
  60. package/dist/lib/command-palette/use-records-source.d.ts.map +1 -0
  61. package/dist/lib/command-palette/use-records-source.js +199 -0
  62. package/dist/lib/command-palette/use-records-source.js.map +1 -0
  63. package/dist/lib/command-palette/use-tabs-source.d.ts +20 -0
  64. package/dist/lib/command-palette/use-tabs-source.d.ts.map +1 -0
  65. package/dist/lib/command-palette/use-tabs-source.js +52 -0
  66. package/dist/lib/command-palette/use-tabs-source.js.map +1 -0
  67. package/dist/lib/command-palette/use-use-cases-source.d.ts +22 -0
  68. package/dist/lib/command-palette/use-use-cases-source.d.ts.map +1 -0
  69. package/dist/lib/command-palette/use-use-cases-source.js +93 -0
  70. package/dist/lib/command-palette/use-use-cases-source.js.map +1 -0
  71. package/dist/lib/config/config-route.d.ts +51 -0
  72. package/dist/lib/config/config-route.d.ts.map +1 -0
  73. package/dist/lib/config/config-route.js +65 -0
  74. package/dist/lib/config/config-route.js.map +1 -0
  75. package/dist/lib/config/service-proxy-route.d.ts +28 -0
  76. package/dist/lib/config/service-proxy-route.d.ts.map +1 -0
  77. package/dist/lib/config/service-proxy-route.js +125 -0
  78. package/dist/lib/config/service-proxy-route.js.map +1 -0
  79. package/dist/lib/config/service-token.d.ts +5 -0
  80. package/dist/lib/config/service-token.d.ts.map +1 -0
  81. package/dist/lib/config/service-token.js +18 -0
  82. package/dist/lib/config/service-token.js.map +1 -0
  83. package/dist/lib/crud/entity-crud-form.d.ts +49 -0
  84. package/dist/lib/crud/entity-crud-form.d.ts.map +1 -0
  85. package/dist/lib/crud/entity-crud-form.js +133 -0
  86. package/dist/lib/crud/entity-crud-form.js.map +1 -0
  87. package/dist/lib/crud/hand-off-write.d.ts +35 -0
  88. package/dist/lib/crud/hand-off-write.d.ts.map +1 -0
  89. package/dist/lib/crud/hand-off-write.js +27 -0
  90. package/dist/lib/crud/hand-off-write.js.map +1 -0
  91. package/dist/lib/crud/index.d.ts +9 -0
  92. package/dist/lib/crud/index.d.ts.map +1 -0
  93. package/dist/lib/crud/index.js +10 -0
  94. package/dist/lib/crud/index.js.map +1 -0
  95. package/dist/lib/crud/make-entity-crud.d.ts +28 -0
  96. package/dist/lib/crud/make-entity-crud.d.ts.map +1 -0
  97. package/dist/lib/crud/make-entity-crud.js +318 -0
  98. package/dist/lib/crud/make-entity-crud.js.map +1 -0
  99. package/dist/lib/crud/make-entity-crud.types.d.ts +237 -0
  100. package/dist/lib/crud/make-entity-crud.types.d.ts.map +1 -0
  101. package/dist/lib/crud/make-entity-crud.types.js +6 -0
  102. package/dist/lib/crud/make-entity-crud.types.js.map +1 -0
  103. package/dist/lib/crud/pending-notice.d.ts +31 -0
  104. package/dist/lib/crud/pending-notice.d.ts.map +1 -0
  105. package/dist/lib/crud/pending-notice.js +72 -0
  106. package/dist/lib/crud/pending-notice.js.map +1 -0
  107. package/dist/lib/crud/slug.d.ts +13 -0
  108. package/dist/lib/crud/slug.d.ts.map +1 -0
  109. package/dist/lib/crud/slug.js +13 -0
  110. package/dist/lib/crud/slug.js.map +1 -0
  111. package/dist/lib/crud/use-entity-affordances.d.ts +20 -0
  112. package/dist/lib/crud/use-entity-affordances.d.ts.map +1 -0
  113. package/dist/lib/crud/use-entity-affordances.js +22 -0
  114. package/dist/lib/crud/use-entity-affordances.js.map +1 -0
  115. package/dist/lib/crud/use-entity-bulk.d.ts +43 -0
  116. package/dist/lib/crud/use-entity-bulk.d.ts.map +1 -0
  117. package/dist/lib/crud/use-entity-bulk.js +106 -0
  118. package/dist/lib/crud/use-entity-bulk.js.map +1 -0
  119. package/dist/lib/crud/use-entity-link-sources.d.ts +25 -0
  120. package/dist/lib/crud/use-entity-link-sources.d.ts.map +1 -0
  121. package/dist/lib/crud/use-entity-link-sources.js +37 -0
  122. package/dist/lib/crud/use-entity-link-sources.js.map +1 -0
  123. package/dist/lib/health/health-routes.d.ts +37 -0
  124. package/dist/lib/health/health-routes.d.ts.map +1 -0
  125. package/dist/lib/health/health-routes.js +90 -0
  126. package/dist/lib/health/health-routes.js.map +1 -0
  127. package/dist/lib/i18n/catalog/en.d.ts +4 -0
  128. package/dist/lib/i18n/catalog/en.d.ts.map +1 -0
  129. package/dist/lib/i18n/catalog/en.js +347 -0
  130. package/dist/lib/i18n/catalog/en.js.map +1 -0
  131. package/dist/lib/i18n/catalog/es.d.ts +397 -0
  132. package/dist/lib/i18n/catalog/es.d.ts.map +1 -0
  133. package/dist/lib/i18n/catalog/es.js +407 -0
  134. package/dist/lib/i18n/catalog/es.js.map +1 -0
  135. package/dist/lib/i18n/catalog/index.d.ts +671 -0
  136. package/dist/lib/i18n/catalog/index.d.ts.map +1 -0
  137. package/dist/lib/i18n/catalog/index.js +28 -0
  138. package/dist/lib/i18n/catalog/index.js.map +1 -0
  139. package/dist/lib/i18n/index.d.ts +2 -0
  140. package/dist/lib/i18n/index.d.ts.map +1 -0
  141. package/dist/lib/i18n/index.js +3 -0
  142. package/dist/lib/i18n/index.js.map +1 -0
  143. package/dist/lib/i18n/locale-link.d.ts +23 -0
  144. package/dist/lib/i18n/locale-link.d.ts.map +1 -0
  145. package/dist/lib/i18n/locale-link.js +43 -0
  146. package/dist/lib/i18n/locale-link.js.map +1 -0
  147. package/dist/lib/i18n/spec-catalog.d.ts +2 -0
  148. package/dist/lib/i18n/spec-catalog.d.ts.map +1 -0
  149. package/dist/lib/i18n/spec-catalog.js +84 -0
  150. package/dist/lib/i18n/spec-catalog.js.map +1 -0
  151. package/dist/lib/search/record-search-route.d.ts +44 -0
  152. package/dist/lib/search/record-search-route.d.ts.map +1 -0
  153. package/dist/lib/search/record-search-route.js +158 -0
  154. package/dist/lib/search/record-search-route.js.map +1 -0
  155. package/dist/lib/search/record-search-source.d.ts +59 -0
  156. package/dist/lib/search/record-search-source.d.ts.map +1 -0
  157. package/dist/lib/search/record-search-source.js +118 -0
  158. package/dist/lib/search/record-search-source.js.map +1 -0
  159. package/dist/lib/search/record-search.types.d.ts +85 -0
  160. package/dist/lib/search/record-search.types.d.ts.map +1 -0
  161. package/dist/lib/search/record-search.types.js +29 -0
  162. package/dist/lib/search/record-search.types.js.map +1 -0
  163. package/dist/lib/search/search-records.d.ts +25 -0
  164. package/dist/lib/search/search-records.d.ts.map +1 -0
  165. package/dist/lib/search/search-records.js +36 -0
  166. package/dist/lib/search/search-records.js.map +1 -0
  167. package/dist/lib/session/account-links.d.ts +46 -0
  168. package/dist/lib/session/account-links.d.ts.map +1 -0
  169. package/dist/lib/session/account-links.js +41 -0
  170. package/dist/lib/session/account-links.js.map +1 -0
  171. package/dist/lib/session/bearer.d.ts +25 -0
  172. package/dist/lib/session/bearer.d.ts.map +1 -0
  173. package/dist/lib/session/bearer.js +31 -0
  174. package/dist/lib/session/bearer.js.map +1 -0
  175. package/dist/lib/session/cookies.d.ts +14 -0
  176. package/dist/lib/session/cookies.d.ts.map +1 -0
  177. package/dist/lib/session/cookies.js +33 -0
  178. package/dist/lib/session/cookies.js.map +1 -0
  179. package/dist/lib/session/device.d.ts +31 -0
  180. package/dist/lib/session/device.d.ts.map +1 -0
  181. package/dist/lib/session/device.js +80 -0
  182. package/dist/lib/session/device.js.map +1 -0
  183. package/dist/lib/session/index.d.ts +3 -0
  184. package/dist/lib/session/index.d.ts.map +1 -0
  185. package/dist/lib/session/index.js +12 -0
  186. package/dist/lib/session/index.js.map +1 -0
  187. package/dist/lib/session/refresh-route.d.ts +19 -0
  188. package/dist/lib/session/refresh-route.d.ts.map +1 -0
  189. package/dist/lib/session/refresh-route.js +85 -0
  190. package/dist/lib/session/refresh-route.js.map +1 -0
  191. package/dist/lib/session/session-keepalive.d.ts +52 -0
  192. package/dist/lib/session/session-keepalive.d.ts.map +1 -0
  193. package/dist/lib/session/session-keepalive.js +72 -0
  194. package/dist/lib/session/session-keepalive.js.map +1 -0
  195. package/dist/lib/session/use-session-refresh.d.ts +43 -0
  196. package/dist/lib/session/use-session-refresh.d.ts.map +1 -0
  197. package/dist/lib/session/use-session-refresh.js +120 -0
  198. package/dist/lib/session/use-session-refresh.js.map +1 -0
  199. package/dist/lib/wizard/index.d.ts +4 -0
  200. package/dist/lib/wizard/index.d.ts.map +1 -0
  201. package/dist/lib/wizard/index.js +5 -0
  202. package/dist/lib/wizard/index.js.map +1 -0
  203. package/dist/lib/wizard/use-wizard-draft.d.ts +22 -0
  204. package/dist/lib/wizard/use-wizard-draft.d.ts.map +1 -0
  205. package/dist/lib/wizard/use-wizard-draft.js +39 -0
  206. package/dist/lib/wizard/use-wizard-draft.js.map +1 -0
  207. package/dist/lib/wizard/use-wizard-step-param.d.ts +60 -0
  208. package/dist/lib/wizard/use-wizard-step-param.d.ts.map +1 -0
  209. package/dist/lib/wizard/use-wizard-step-param.js +94 -0
  210. package/dist/lib/wizard/use-wizard-step-param.js.map +1 -0
  211. package/dist/lib/wizard/wizard-tab-kind.d.ts +31 -0
  212. package/dist/lib/wizard/wizard-tab-kind.d.ts.map +1 -0
  213. package/dist/lib/wizard/wizard-tab-kind.js +33 -0
  214. package/dist/lib/wizard/wizard-tab-kind.js.map +1 -0
  215. package/dist/lib/workspace/drafts-state.d.ts +88 -0
  216. package/dist/lib/workspace/drafts-state.d.ts.map +1 -0
  217. package/dist/lib/workspace/drafts-state.js +128 -0
  218. package/dist/lib/workspace/drafts-state.js.map +1 -0
  219. package/dist/lib/workspace/entity-nav.d.ts +26 -0
  220. package/dist/lib/workspace/entity-nav.d.ts.map +1 -0
  221. package/dist/lib/workspace/entity-nav.js +56 -0
  222. package/dist/lib/workspace/entity-nav.js.map +1 -0
  223. package/dist/lib/workspace/entity-tab-kind.d.ts +51 -0
  224. package/dist/lib/workspace/entity-tab-kind.d.ts.map +1 -0
  225. package/dist/lib/workspace/entity-tab-kind.js +47 -0
  226. package/dist/lib/workspace/entity-tab-kind.js.map +1 -0
  227. package/dist/lib/workspace/idb-state-storage.d.ts +9 -0
  228. package/dist/lib/workspace/idb-state-storage.d.ts.map +1 -0
  229. package/dist/lib/workspace/idb-state-storage.js +32 -0
  230. package/dist/lib/workspace/idb-state-storage.js.map +1 -0
  231. package/dist/lib/workspace/index.d.ts +13 -0
  232. package/dist/lib/workspace/index.d.ts.map +1 -0
  233. package/dist/lib/workspace/index.js +14 -0
  234. package/dist/lib/workspace/index.js.map +1 -0
  235. package/dist/lib/workspace/pending-state.d.ts +84 -0
  236. package/dist/lib/workspace/pending-state.d.ts.map +1 -0
  237. package/dist/lib/workspace/pending-state.js +155 -0
  238. package/dist/lib/workspace/pending-state.js.map +1 -0
  239. package/dist/lib/workspace/pending-transactions.d.ts +44 -0
  240. package/dist/lib/workspace/pending-transactions.d.ts.map +1 -0
  241. package/dist/lib/workspace/pending-transactions.js +114 -0
  242. package/dist/lib/workspace/pending-transactions.js.map +1 -0
  243. package/dist/lib/workspace/tab-kind.d.ts +48 -0
  244. package/dist/lib/workspace/tab-kind.d.ts.map +1 -0
  245. package/dist/lib/workspace/tab-kind.js +47 -0
  246. package/dist/lib/workspace/tab-kind.js.map +1 -0
  247. package/dist/lib/workspace/tab-state.d.ts +36 -0
  248. package/dist/lib/workspace/tab-state.d.ts.map +1 -0
  249. package/dist/lib/workspace/tab-state.js +81 -0
  250. package/dist/lib/workspace/tab-state.js.map +1 -0
  251. package/dist/lib/workspace/tabs-state.d.ts +46 -0
  252. package/dist/lib/workspace/tabs-state.d.ts.map +1 -0
  253. package/dist/lib/workspace/tabs-state.js +56 -0
  254. package/dist/lib/workspace/tabs-state.js.map +1 -0
  255. package/dist/lib/workspace/use-entity-draft.d.ts +25 -0
  256. package/dist/lib/workspace/use-entity-draft.d.ts.map +1 -0
  257. package/dist/lib/workspace/use-entity-draft.js +38 -0
  258. package/dist/lib/workspace/use-entity-draft.js.map +1 -0
  259. package/dist/lib/workspace/workspace-scope.d.ts +51 -0
  260. package/dist/lib/workspace/workspace-scope.d.ts.map +1 -0
  261. package/dist/lib/workspace/workspace-scope.js +42 -0
  262. package/dist/lib/workspace/workspace-scope.js.map +1 -0
  263. package/dist/lib/workspace/workspace-shell.d.ts +33 -0
  264. package/dist/lib/workspace/workspace-shell.d.ts.map +1 -0
  265. package/dist/lib/workspace/workspace-shell.js +206 -0
  266. package/dist/lib/workspace/workspace-shell.js.map +1 -0
  267. package/dist/server.d.ts +13 -0
  268. package/dist/server.d.ts.map +1 -0
  269. package/dist/server.js +41 -0
  270. package/dist/server.js.map +1 -0
  271. package/package.json +95 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/workspace/entity-tab-kind.tsx"],"sourcesContent":["'use client';\n\nimport { parseScreenPayload, type ScreenType } from '@entifix/authz';\nimport type { ReactNode } from 'react';\n\nimport type { TabKind } from './tab-kind.js';\n\n/** One list a workspace may open — the collection half of a screen. */\nexport interface EntityTabList {\n /** The tab caption's catalog key. `@entity({ pluralKey })` for a generated screen. */\n readonly titleKey: string;\n render(): ReactNode;\n}\n\n/** One record a workspace may open — the same screen, with an id. */\nexport interface EntityTabRecord {\n /** The tab caption's catalog key. `@entity({ labelKey })` for a generated screen. */\n readonly labelKey: string;\n render(id: string): ReactNode;\n}\n\nexport interface EntityTabScreens {\n readonly lists: Record<string, EntityTabList>;\n readonly records: Record<string, EntityTabRecord>;\n}\n\n/**\n * An entity tab kind: `<type>:<key>` for a list, `<type>:<key>:<id>` for one\n * record.\n *\n * **The list and the record are not different kinds.** They are the same screen\n * with and without a record, which is exactly what the optional id in the\n * payload says — three kinds (`catalog:`, `entity:`, `system:`) collapsed into\n * one for that reason, and the prefix names the *taxonomy* now\n * ([ADR 0042](../../../../../../../docs/adr/0042-the-workspace-address-is-the-taxonomy-serialized.md)).\n *\n * A factory over the {@link ScreenType} rather than a constant, and that is the\n * whole of what makes an Operaciones screen addressable. It was written inline\n * in back-office-app as a `masterKind`, so `master:` was the only entity\n * address any host could resolve and `operation:` matched nothing at all — a\n * second copy of this parser beside it is exactly the drift the registry was\n * consolidated to stop, and the two would have had to agree about a grammar\n * neither owns.\n *\n * Which screens a host offers stays the host's decision — a second host\n * mounting the same shells may want a different set — so the screens arrive as\n * an argument. What no longer varies is how one is addressed.\n *\n * `render` takes the id rather than a component, so the caller decides what\n * hosting a record means: in a workspace that is an editor wired to an\n * address-keyed autosaved draft, and the address it keys on has to carry this\n * same `type`.\n */\nexport function entityTabKind(\n type: ScreenType,\n screens: EntityTabScreens,\n): TabKind<{ key: string; id?: string }> {\n return {\n kind: type,\n match: payload => {\n const parsed = parseScreenPayload(payload);\n if (parsed === null) return null;\n // A key is valid for the half it names: a record address for a screen\n // with a list and no record editor resolves to nothing, and the workspace\n // answers with its fallback rather than rendering an empty editor.\n const known =\n parsed.id === undefined\n ? parsed.key in screens.lists\n : parsed.key in screens.records;\n return known ? parsed : null;\n },\n toParam: addr =>\n addr.id === undefined ? addr.key : `${addr.key}:${addr.id}`,\n title: (addr, translate) =>\n addr.id === undefined\n ? translate(screens.lists[addr.key].titleKey)\n : `${translate(screens.records[addr.key].labelKey)} #${addr.id}`,\n render: addr =>\n addr.id === undefined\n ? screens.lists[addr.key].render()\n : screens.records[addr.key].render(addr.id),\n };\n}\n"],"names":["parseScreenPayload","entityTabKind","type","screens","kind","match","payload","parsed","known","id","undefined","key","lists","records","toParam","addr","title","translate","titleKey","labelKey","render"],"mappings":"AAAA;AAEA,SAASA,kBAAkB,QAAyB,iBAAiB;AAwBrE;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,SAASC,cACdC,IAAgB,EAChBC,OAAyB;IAEzB,OAAO;QACLC,MAAMF;QACNG,OAAOC,CAAAA;YACL,MAAMC,SAASP,mBAAmBM;YAClC,IAAIC,WAAW,MAAM,OAAO;YAC5B,sEAAsE;YACtE,0EAA0E;YAC1E,mEAAmE;YACnE,MAAMC,QACJD,OAAOE,EAAE,KAAKC,YACVH,OAAOI,GAAG,IAAIR,QAAQS,KAAK,GAC3BL,OAAOI,GAAG,IAAIR,QAAQU,OAAO;YACnC,OAAOL,QAAQD,SAAS;QAC1B;QACAO,SAASC,CAAAA,OACPA,KAAKN,EAAE,KAAKC,YAAYK,KAAKJ,GAAG,GAAG,GAAGI,KAAKJ,GAAG,CAAC,CAAC,EAAEI,KAAKN,EAAE,EAAE;QAC7DO,OAAO,CAACD,MAAME,YACZF,KAAKN,EAAE,KAAKC,YACRO,UAAUd,QAAQS,KAAK,CAACG,KAAKJ,GAAG,CAAC,CAACO,QAAQ,IAC1C,GAAGD,UAAUd,QAAQU,OAAO,CAACE,KAAKJ,GAAG,CAAC,CAACQ,QAAQ,EAAE,EAAE,EAAEJ,KAAKN,EAAE,EAAE;QACpEW,QAAQL,CAAAA,OACNA,KAAKN,EAAE,KAAKC,YACRP,QAAQS,KAAK,CAACG,KAAKJ,GAAG,CAAC,CAACS,MAAM,KAC9BjB,QAAQU,OAAO,CAACE,KAAKJ,GAAG,CAAC,CAACS,MAAM,CAACL,KAAKN,EAAE;IAChD;AACF"}
@@ -0,0 +1,9 @@
1
+ import type { StateStorage } from 'zustand/middleware';
2
+ /**
3
+ * A Zustand {@link StateStorage} backed by IndexedDB — the persistence seam for
4
+ * the workspace stores, so open tabs and drafts survive a refresh. SSR-safe:
5
+ * with no `indexedDB` (the server) reads resolve `null` and writes are dropped,
6
+ * so the store simply starts empty until the client hydrates.
7
+ */
8
+ export declare function makeIndexedDbStateStorage(dbName: string, storeName: string): StateStorage;
9
+ //# sourceMappingURL=idb-state-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idb-state-storage.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/idb-state-storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,YAAY,CA2Bd"}
@@ -0,0 +1,32 @@
1
+ import { openDB } from 'idb';
2
+ /**
3
+ * A Zustand {@link StateStorage} backed by IndexedDB — the persistence seam for
4
+ * the workspace stores, so open tabs and drafts survive a refresh. SSR-safe:
5
+ * with no `indexedDB` (the server) reads resolve `null` and writes are dropped,
6
+ * so the store simply starts empty until the client hydrates.
7
+ */ export function makeIndexedDbStateStorage(dbName, storeName) {
8
+ let database;
9
+ const hasIndexedDb = ()=>typeof indexedDB !== 'undefined';
10
+ const open = ()=>database != null ? database : database = openDB(dbName, 1, {
11
+ upgrade (db) {
12
+ db.createObjectStore(storeName);
13
+ }
14
+ });
15
+ return {
16
+ getItem: async (name)=>{
17
+ var _ref;
18
+ if (!hasIndexedDb()) return null;
19
+ return (_ref = await (await open()).get(storeName, name)) != null ? _ref : null;
20
+ },
21
+ setItem: async (name, value)=>{
22
+ if (!hasIndexedDb()) return;
23
+ await (await open()).put(storeName, value, name);
24
+ },
25
+ removeItem: async (name)=>{
26
+ if (!hasIndexedDb()) return;
27
+ await (await open()).delete(storeName, name);
28
+ }
29
+ };
30
+ }
31
+
32
+ //# sourceMappingURL=idb-state-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/workspace/idb-state-storage.ts"],"sourcesContent":["import { type IDBPDatabase, openDB } from 'idb';\nimport type { StateStorage } from 'zustand/middleware';\n\n/**\n * A Zustand {@link StateStorage} backed by IndexedDB — the persistence seam for\n * the workspace stores, so open tabs and drafts survive a refresh. SSR-safe:\n * with no `indexedDB` (the server) reads resolve `null` and writes are dropped,\n * so the store simply starts empty until the client hydrates.\n */\nexport function makeIndexedDbStateStorage(\n dbName: string,\n storeName: string,\n): StateStorage {\n let database: Promise<IDBPDatabase> | undefined;\n const hasIndexedDb = () => typeof indexedDB !== 'undefined';\n const open = () =>\n (database ??= openDB(dbName, 1, {\n upgrade(db) {\n db.createObjectStore(storeName);\n },\n }));\n\n return {\n getItem: async name => {\n if (!hasIndexedDb()) return null;\n return (\n ((await (await open()).get(storeName, name)) as string | undefined) ??\n null\n );\n },\n setItem: async (name, value) => {\n if (!hasIndexedDb()) return;\n await (await open()).put(storeName, value, name);\n },\n removeItem: async name => {\n if (!hasIndexedDb()) return;\n await (await open()).delete(storeName, name);\n },\n };\n}\n"],"names":["openDB","makeIndexedDbStateStorage","dbName","storeName","database","hasIndexedDb","indexedDB","open","upgrade","db","createObjectStore","getItem","name","get","setItem","value","put","removeItem","delete"],"mappings":"AAAA,SAA4BA,MAAM,QAAQ,MAAM;AAGhD;;;;;CAKC,GACD,OAAO,SAASC,0BACdC,MAAc,EACdC,SAAiB;IAEjB,IAAIC;IACJ,MAAMC,eAAe,IAAM,OAAOC,cAAc;IAChD,MAAMC,OAAO,IACVH,mBAAAA,WAAAA,WAAaJ,OAAOE,QAAQ,GAAG;YAC9BM,SAAQC,EAAE;gBACRA,GAAGC,iBAAiB,CAACP;YACvB;QACF;IAEF,OAAO;QACLQ,SAAS,OAAMC;gBAGT;YAFJ,IAAI,CAACP,gBAAgB,OAAO;YAC5B,QACI,OAAA,MAAM,AAAC,CAAA,MAAME,MAAK,EAAGM,GAAG,CAACV,WAAWS,iBAApC,OACF;QAEJ;QACAE,SAAS,OAAOF,MAAMG;YACpB,IAAI,CAACV,gBAAgB;YACrB,MAAM,AAAC,CAAA,MAAME,MAAK,EAAGS,GAAG,CAACb,WAAWY,OAAOH;QAC7C;QACAK,YAAY,OAAML;YAChB,IAAI,CAACP,gBAAgB;YACrB,MAAM,AAAC,CAAA,MAAME,MAAK,EAAGW,MAAM,CAACf,WAAWS;QACzC;IACF;AACF"}
@@ -0,0 +1,13 @@
1
+ export * from './drafts-state.js';
2
+ export * from './entity-nav.js';
3
+ export * from './entity-tab-kind.js';
4
+ export * from './idb-state-storage.js';
5
+ export * from './pending-state.js';
6
+ export * from './pending-transactions.js';
7
+ export * from './tab-kind.js';
8
+ export * from './tab-state.js';
9
+ export * from './tabs-state.js';
10
+ export * from './use-entity-draft.js';
11
+ export * from './workspace-scope.js';
12
+ export * from './workspace-shell.js';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
@@ -0,0 +1,14 @@
1
+ export * from './drafts-state.js';
2
+ export * from './entity-nav.js';
3
+ export * from './entity-tab-kind.js';
4
+ export * from './idb-state-storage.js';
5
+ export * from './pending-state.js';
6
+ export * from './pending-transactions.js';
7
+ export * from './tab-kind.js';
8
+ export * from './tab-state.js';
9
+ export * from './tabs-state.js';
10
+ export * from './use-entity-draft.js';
11
+ export * from './workspace-scope.js';
12
+ export * from './workspace-shell.js';
13
+
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/workspace/index.ts"],"sourcesContent":["export * from './drafts-state.js';\nexport * from './entity-nav.js';\nexport * from './entity-tab-kind.js';\nexport * from './idb-state-storage.js';\nexport * from './pending-state.js';\nexport * from './pending-transactions.js';\nexport * from './tab-kind.js';\nexport * from './tab-state.js';\nexport * from './tabs-state.js';\nexport * from './use-entity-draft.js';\nexport * from './workspace-scope.js';\nexport * from './workspace-shell.js';\n"],"names":[],"mappings":"AAAA,cAAc,oBAAoB;AAClC,cAAc,kBAAkB;AAChC,cAAc,uBAAuB;AACrC,cAAc,yBAAyB;AACvC,cAAc,qBAAqB;AACnC,cAAc,4BAA4B;AAC1C,cAAc,gBAAgB;AAC9B,cAAc,iBAAiB;AAC/B,cAAc,kBAAkB;AAChC,cAAc,wBAAwB;AACtC,cAAc,uBAAuB;AACrC,cAAc,uBAAuB"}
@@ -0,0 +1,84 @@
1
+ import type { PendingEntry, PendingTransaction } from '@entifix/transactions';
2
+ /**
3
+ * Writes the browser started and the server has not finished, keyed by
4
+ * transaction id.
5
+ *
6
+ * ⚠️ **This is not workspace state, and the distinction is load-bearing.** Tabs
7
+ * and drafts belong to the workspace and are scoped by `WorkspaceShell`; a
8
+ * pending write is not, because a create happens at `/<basePath>/new` on the
9
+ * plain route, outside any `WorkspaceShell` (a generated list still uses plain
10
+ * `href`s — ADR 0042's own residual). A store mounted inside the workspace would
11
+ * be written to by nothing, or — worse — written to *before* `persist.setOptions`
12
+ * had scoped it, which is the unscoped cross-account restore ADR 0032 exists to
13
+ * prevent. So `PendingTransactionsProvider` sits at the authenticated layout
14
+ * instead, above both.
15
+ *
16
+ * It shares the workspace's IndexedDB database because it is the same browser
17
+ * profile and the same scope key, not because it is the same concern.
18
+ */
19
+ export interface PendingState {
20
+ pending: Record<string, PendingEntry>;
21
+ began(transaction: PendingTransaction): void;
22
+ attach(transactionId: string, record: unknown): boolean;
23
+ settle(transactionId: string): void;
24
+ fail(transactionId: string, reason?: string): void;
25
+ dismiss(transactionId: string): void;
26
+ }
27
+ /**
28
+ * ⚠️ **`record` is stripped before persisting.** It is a class instance, and a
29
+ * draft-style JSON round trip does not preserve one — it returns as something
30
+ * else, silently (ADR 0032). So a refresh keeps the *watch* and loses the row,
31
+ * which is the documented behaviour: past a refresh, server truth plus an honest
32
+ * notice beats a record rebuilt from a blob.
33
+ */
34
+ export declare function persistedPending(store: PendingState): Pick<PendingState, 'pending'>;
35
+ /**
36
+ * How many pending writes are watched at once, oldest evicted first.
37
+ *
38
+ * The same reasoning as {@link MAX_TABS}: an unbounded set persisted to
39
+ * IndexedDB grows without anything ever pruning it, and reconciliation asks the
40
+ * tracker once per entry on every reconnect — so the cap is also what keeps that
41
+ * fan-out a handful of requests rather than an unbounded one.
42
+ *
43
+ * An evicted entry is dropped silently. The record is still on the server and
44
+ * the list's own query is the fallback; what is lost is only the watching.
45
+ */
46
+ export declare const MAX_PENDING = 12;
47
+ /**
48
+ * Bump when the *shape* of an entry changes, and every persisted pending write
49
+ * is discarded on the next load.
50
+ *
51
+ * Discarding is right here for a reason the other two stores do not share: an
52
+ * entry is a claim that something is still in flight, and it stops being true on
53
+ * its own. A stale one restored from a previous build watches a transaction that
54
+ * settled hours ago, and the reconcile that would correct it is keyed on a shape
55
+ * this build may no longer understand.
56
+ */
57
+ export declare const PENDING_VERSION = 1;
58
+ /**
59
+ * Discard, never guess — written out rather than left to zustand's default,
60
+ * which discards too but logs an error for a decision that was made on purpose.
61
+ * Same call as `migrateDrafts` and `migrateTabs`.
62
+ */
63
+ export declare function migratePending(): Pick<PendingState, 'pending'>;
64
+ /**
65
+ * Restores only the entries that are actually entries — per entry, so one bad
66
+ * record does not take the rest with it. The `mergeDrafts` rule, for the same
67
+ * reason: this is what happens when something else was written by a build that
68
+ * predates the shape.
69
+ */
70
+ export declare function mergePending(persistedState: unknown, currentState: PendingState): PendingState;
71
+ export declare const usePendingState: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<PendingState>, "setState" | "persist"> & {
72
+ setState(partial: PendingState | Partial<PendingState> | ((state: PendingState) => PendingState | Partial<PendingState>), replace?: false | undefined): unknown;
73
+ setState(state: PendingState | ((state: PendingState) => PendingState), replace: true): unknown;
74
+ persist: {
75
+ setOptions: (options: Partial<import("zustand/middleware").PersistOptions<PendingState, unknown, unknown>>) => void;
76
+ clearStorage: () => void;
77
+ rehydrate: () => Promise<void> | void;
78
+ hasHydrated: () => boolean;
79
+ onHydrate: (fn: (state: PendingState) => void) => () => void;
80
+ onFinishHydration: (fn: (state: PendingState) => void) => () => void;
81
+ getOptions: () => Partial<import("zustand/middleware").PersistOptions<PendingState, unknown, unknown>>;
82
+ };
83
+ }>;
84
+ //# sourceMappingURL=pending-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-state.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/pending-state.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAO9E;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACtC,KAAK,CAAC,WAAW,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC7C,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;IACxD,MAAM,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,IAAI,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnD,OAAO,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,YAAY,GAClB,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAQ/B;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,KAAK,CAAC;AAE9B;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,IAAI,CAAC;AAIjC;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAE9D;AAcD;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,cAAc,EAAE,OAAO,EACvB,YAAY,EAAE,YAAY,GACzB,YAAY,CAUd;AAgBD,eAAO,MAAM,eAAe;;;;;;;;;;;;EAgE3B,CAAC"}
@@ -0,0 +1,155 @@
1
+ 'use client';
2
+ import { _ as _extends } from "@swc/helpers/_/_extends";
3
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
4
+ import { _ as _to_property_key } from "@swc/helpers/_/_to_property_key";
5
+ import { create } from 'zustand';
6
+ import { createJSONStorage, persist } from 'zustand/middleware';
7
+ import { makeIndexedDbStateStorage } from './idb-state-storage.js';
8
+ import { WORKSPACE_DB } from './tabs-state.js';
9
+ /**
10
+ * ⚠️ **`record` is stripped before persisting.** It is a class instance, and a
11
+ * draft-style JSON round trip does not preserve one — it returns as something
12
+ * else, silently (ADR 0032). So a refresh keeps the *watch* and loses the row,
13
+ * which is the documented behaviour: past a refresh, server truth plus an honest
14
+ * notice beats a record rebuilt from a blob.
15
+ */ export function persistedPending(store) {
16
+ const pending = {};
17
+ for (const _ref of Object.entries(store.pending)){
18
+ let [transactionId, _ref1] = _ref, { record: _dropped } = _ref1, rest = _object_without_properties_loose(_ref1, [
19
+ "record"
20
+ ]);
21
+ pending[transactionId] = rest;
22
+ }
23
+ return {
24
+ pending
25
+ };
26
+ }
27
+ const PENDING_STORE = 'stores';
28
+ /**
29
+ * How many pending writes are watched at once, oldest evicted first.
30
+ *
31
+ * The same reasoning as {@link MAX_TABS}: an unbounded set persisted to
32
+ * IndexedDB grows without anything ever pruning it, and reconciliation asks the
33
+ * tracker once per entry on every reconnect — so the cap is also what keeps that
34
+ * fan-out a handful of requests rather than an unbounded one.
35
+ *
36
+ * An evicted entry is dropped silently. The record is still on the server and
37
+ * the list's own query is the fallback; what is lost is only the watching.
38
+ */ export const MAX_PENDING = 12;
39
+ /**
40
+ * Bump when the *shape* of an entry changes, and every persisted pending write
41
+ * is discarded on the next load.
42
+ *
43
+ * Discarding is right here for a reason the other two stores do not share: an
44
+ * entry is a claim that something is still in flight, and it stops being true on
45
+ * its own. A stale one restored from a previous build watches a transaction that
46
+ * settled hours ago, and the reconcile that would correct it is keyed on a shape
47
+ * this build may no longer understand.
48
+ */ export const PENDING_VERSION = 1;
49
+ const emptyPending = {
50
+ pending: {}
51
+ };
52
+ /**
53
+ * Discard, never guess — written out rather than left to zustand's default,
54
+ * which discards too but logs an error for a decision that was made on purpose.
55
+ * Same call as `migrateDrafts` and `migrateTabs`.
56
+ */ export function migratePending() {
57
+ return emptyPending;
58
+ }
59
+ /** Whether a restored value is an entry this build can act on. */ function isPendingEntry(value) {
60
+ if (value === null || typeof value !== 'object') return false;
61
+ const entry = value;
62
+ return typeof entry.transactionId === 'string' && typeof entry.entity === 'string' && typeof entry.at === 'string' && (entry.state === 'pending' || entry.state === 'failed');
63
+ }
64
+ /**
65
+ * Restores only the entries that are actually entries — per entry, so one bad
66
+ * record does not take the rest with it. The `mergeDrafts` rule, for the same
67
+ * reason: this is what happens when something else was written by a build that
68
+ * predates the shape.
69
+ */ export function mergePending(persistedState, currentState) {
70
+ const restored = persistedState == null ? void 0 : persistedState.pending;
71
+ if (restored === null || typeof restored !== 'object') return currentState;
72
+ const pending = {};
73
+ for (const [transactionId, value] of Object.entries(restored)){
74
+ if (isPendingEntry(value)) pending[transactionId] = value;
75
+ }
76
+ return _extends({}, currentState, {
77
+ pending
78
+ });
79
+ }
80
+ /** Drops the oldest entries until the set fits, by announcement time. */ function capPending(pending) {
81
+ const entries = Object.entries(pending);
82
+ if (entries.length <= MAX_PENDING) return pending;
83
+ return Object.fromEntries(entries.sort(([, a], [, b])=>a.at.localeCompare(b.at)).slice(entries.length - MAX_PENDING));
84
+ }
85
+ export const usePendingState = create()(persist((set, get)=>({
86
+ pending: {},
87
+ began: (transaction)=>set((state)=>({
88
+ pending: capPending(_extends({}, state.pending, {
89
+ [transaction.transactionId]: _extends({}, transaction, {
90
+ state: 'pending'
91
+ })
92
+ }))
93
+ })),
94
+ // Reads current state rather than taking it from the caller, so a save
95
+ // handler cannot ask about a store snapshot older than its own `await`.
96
+ attach: (transactionId, record)=>{
97
+ const entry = get().pending[transactionId];
98
+ if (entry === undefined) return false;
99
+ set((state)=>({
100
+ pending: _extends({}, state.pending, {
101
+ [transactionId]: _extends({}, entry, {
102
+ record
103
+ })
104
+ })
105
+ }));
106
+ return true;
107
+ },
108
+ // Settling drops the entry rather than marking it done: the server's copy
109
+ // is the truth now, and the list query that just refetched is showing it.
110
+ settle: (transactionId)=>set((state)=>{
111
+ if (!(transactionId in state.pending)) return state;
112
+ const _state_pending = state.pending, { [transactionId]: _settled } = _state_pending, rest = _object_without_properties_loose(_state_pending, [
113
+ _to_property_key(transactionId)
114
+ ]);
115
+ return {
116
+ pending: rest
117
+ };
118
+ }),
119
+ // A failure is *kept*, marked, until someone reads it. Dropping it here
120
+ // would un-render the optimistic row and say nothing about why.
121
+ fail: (transactionId, reason)=>set((state)=>{
122
+ const entry = state.pending[transactionId];
123
+ if (entry === undefined) return state;
124
+ return {
125
+ pending: _extends({}, state.pending, {
126
+ [transactionId]: _extends({}, entry, {
127
+ state: 'failed',
128
+ reason
129
+ })
130
+ })
131
+ };
132
+ }),
133
+ dismiss: (transactionId)=>set((state)=>{
134
+ if (!(transactionId in state.pending)) return state;
135
+ const _state_pending = state.pending, { [transactionId]: _dismissed } = _state_pending, rest = _object_without_properties_loose(_state_pending, [
136
+ _to_property_key(transactionId)
137
+ ]);
138
+ return {
139
+ pending: rest
140
+ };
141
+ })
142
+ }), {
143
+ // Replaced with a scoped name before rehydrating, exactly as the tab and
144
+ // draft stores are. This literal is only what an unscoped store would
145
+ // write, and nothing rehydrates without passing through that call.
146
+ name: 'pending',
147
+ version: PENDING_VERSION,
148
+ migrate: migratePending,
149
+ merge: mergePending,
150
+ storage: createJSONStorage(()=>makeIndexedDbStateStorage(WORKSPACE_DB, PENDING_STORE)),
151
+ partialize: persistedPending,
152
+ skipHydration: true
153
+ }));
154
+
155
+ //# sourceMappingURL=pending-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/workspace/pending-state.ts"],"sourcesContent":["'use client';\n\nimport type { PendingEntry, PendingTransaction } from '@entifix/transactions';\nimport { create } from 'zustand';\nimport { createJSONStorage, persist } from 'zustand/middleware';\n\nimport { makeIndexedDbStateStorage } from './idb-state-storage.js';\nimport { WORKSPACE_DB } from './tabs-state.js';\n\n/**\n * Writes the browser started and the server has not finished, keyed by\n * transaction id.\n *\n * ⚠️ **This is not workspace state, and the distinction is load-bearing.** Tabs\n * and drafts belong to the workspace and are scoped by `WorkspaceShell`; a\n * pending write is not, because a create happens at `/<basePath>/new` on the\n * plain route, outside any `WorkspaceShell` (a generated list still uses plain\n * `href`s — ADR 0042's own residual). A store mounted inside the workspace would\n * be written to by nothing, or — worse — written to *before* `persist.setOptions`\n * had scoped it, which is the unscoped cross-account restore ADR 0032 exists to\n * prevent. So `PendingTransactionsProvider` sits at the authenticated layout\n * instead, above both.\n *\n * It shares the workspace's IndexedDB database because it is the same browser\n * profile and the same scope key, not because it is the same concern.\n */\nexport interface PendingState {\n pending: Record<string, PendingEntry>;\n began(transaction: PendingTransaction): void;\n attach(transactionId: string, record: unknown): boolean;\n settle(transactionId: string): void;\n fail(transactionId: string, reason?: string): void;\n dismiss(transactionId: string): void;\n}\n\n/**\n * ⚠️ **`record` is stripped before persisting.** It is a class instance, and a\n * draft-style JSON round trip does not preserve one — it returns as something\n * else, silently (ADR 0032). So a refresh keeps the *watch* and loses the row,\n * which is the documented behaviour: past a refresh, server truth plus an honest\n * notice beats a record rebuilt from a blob.\n */\nexport function persistedPending(\n store: PendingState,\n): Pick<PendingState, 'pending'> {\n const pending: Record<string, PendingEntry> = {};\n for (const [transactionId, { record: _dropped, ...rest }] of Object.entries(\n store.pending,\n )) {\n pending[transactionId] = rest;\n }\n return { pending };\n}\n\nconst PENDING_STORE = 'stores';\n\n/**\n * How many pending writes are watched at once, oldest evicted first.\n *\n * The same reasoning as {@link MAX_TABS}: an unbounded set persisted to\n * IndexedDB grows without anything ever pruning it, and reconciliation asks the\n * tracker once per entry on every reconnect — so the cap is also what keeps that\n * fan-out a handful of requests rather than an unbounded one.\n *\n * An evicted entry is dropped silently. The record is still on the server and\n * the list's own query is the fallback; what is lost is only the watching.\n */\nexport const MAX_PENDING = 12;\n\n/**\n * Bump when the *shape* of an entry changes, and every persisted pending write\n * is discarded on the next load.\n *\n * Discarding is right here for a reason the other two stores do not share: an\n * entry is a claim that something is still in flight, and it stops being true on\n * its own. A stale one restored from a previous build watches a transaction that\n * settled hours ago, and the reconcile that would correct it is keyed on a shape\n * this build may no longer understand.\n */\nexport const PENDING_VERSION = 1;\n\nconst emptyPending: Pick<PendingState, 'pending'> = { pending: {} };\n\n/**\n * Discard, never guess — written out rather than left to zustand's default,\n * which discards too but logs an error for a decision that was made on purpose.\n * Same call as `migrateDrafts` and `migrateTabs`.\n */\nexport function migratePending(): Pick<PendingState, 'pending'> {\n return emptyPending;\n}\n\n/** Whether a restored value is an entry this build can act on. */\nfunction isPendingEntry(value: unknown): value is PendingEntry {\n if (value === null || typeof value !== 'object') return false;\n const entry = value as Partial<PendingEntry>;\n return (\n typeof entry.transactionId === 'string' &&\n typeof entry.entity === 'string' &&\n typeof entry.at === 'string' &&\n (entry.state === 'pending' || entry.state === 'failed')\n );\n}\n\n/**\n * Restores only the entries that are actually entries — per entry, so one bad\n * record does not take the rest with it. The `mergeDrafts` rule, for the same\n * reason: this is what happens when something else was written by a build that\n * predates the shape.\n */\nexport function mergePending(\n persistedState: unknown,\n currentState: PendingState,\n): PendingState {\n const restored = (persistedState as Partial<PendingState> | undefined)\n ?.pending;\n if (restored === null || typeof restored !== 'object') return currentState;\n\n const pending: Record<string, PendingEntry> = {};\n for (const [transactionId, value] of Object.entries(restored)) {\n if (isPendingEntry(value)) pending[transactionId] = value;\n }\n return { ...currentState, pending };\n}\n\n/** Drops the oldest entries until the set fits, by announcement time. */\nfunction capPending(\n pending: Record<string, PendingEntry>,\n): Record<string, PendingEntry> {\n const entries = Object.entries(pending);\n if (entries.length <= MAX_PENDING) return pending;\n\n return Object.fromEntries(\n entries\n .sort(([, a], [, b]) => a.at.localeCompare(b.at))\n .slice(entries.length - MAX_PENDING),\n );\n}\n\nexport const usePendingState = create<PendingState>()(\n persist(\n (set, get) => ({\n pending: {},\n began: transaction =>\n set(state => ({\n pending: capPending({\n ...state.pending,\n [transaction.transactionId]: { ...transaction, state: 'pending' },\n }),\n })),\n // Reads current state rather than taking it from the caller, so a save\n // handler cannot ask about a store snapshot older than its own `await`.\n attach: (transactionId, record) => {\n const entry = get().pending[transactionId];\n if (entry === undefined) return false;\n set(state => ({\n pending: { ...state.pending, [transactionId]: { ...entry, record } },\n }));\n return true;\n },\n // Settling drops the entry rather than marking it done: the server's copy\n // is the truth now, and the list query that just refetched is showing it.\n settle: transactionId =>\n set(state => {\n if (!(transactionId in state.pending)) return state;\n const { [transactionId]: _settled, ...rest } = state.pending;\n return { pending: rest };\n }),\n // A failure is *kept*, marked, until someone reads it. Dropping it here\n // would un-render the optimistic row and say nothing about why.\n fail: (transactionId, reason) =>\n set(state => {\n const entry = state.pending[transactionId];\n if (entry === undefined) return state;\n return {\n pending: {\n ...state.pending,\n [transactionId]: { ...entry, state: 'failed', reason },\n },\n };\n }),\n dismiss: transactionId =>\n set(state => {\n if (!(transactionId in state.pending)) return state;\n const { [transactionId]: _dismissed, ...rest } = state.pending;\n return { pending: rest };\n }),\n }),\n {\n // Replaced with a scoped name before rehydrating, exactly as the tab and\n // draft stores are. This literal is only what an unscoped store would\n // write, and nothing rehydrates without passing through that call.\n name: 'pending',\n version: PENDING_VERSION,\n migrate: migratePending,\n merge: mergePending,\n storage: createJSONStorage(() =>\n makeIndexedDbStateStorage(WORKSPACE_DB, PENDING_STORE),\n ),\n partialize: persistedPending,\n skipHydration: true,\n },\n ),\n);\n"],"names":["create","createJSONStorage","persist","makeIndexedDbStateStorage","WORKSPACE_DB","persistedPending","store","pending","Object","entries","transactionId","record","_dropped","rest","PENDING_STORE","MAX_PENDING","PENDING_VERSION","emptyPending","migratePending","isPendingEntry","value","entry","entity","at","state","mergePending","persistedState","currentState","restored","capPending","length","fromEntries","sort","a","b","localeCompare","slice","usePendingState","set","get","began","transaction","attach","undefined","settle","_settled","fail","reason","dismiss","_dismissed","name","version","migrate","merge","storage","partialize","skipHydration"],"mappings":"AAAA;;;;AAGA,SAASA,MAAM,QAAQ,UAAU;AACjC,SAASC,iBAAiB,EAAEC,OAAO,QAAQ,qBAAqB;AAEhE,SAASC,yBAAyB,QAAQ,yBAAyB;AACnE,SAASC,YAAY,QAAQ,kBAAkB;AA4B/C;;;;;;CAMC,GACD,OAAO,SAASC,iBACdC,KAAmB;IAEnB,MAAMC,UAAwC,CAAC;IAC/C,KAAK,MAAM,QAAkDC,OAAOC,OAAO,CACzEH,MAAMC,OAAO,EACZ;YAFQ,CAACG,qBAA6C,SAA9B,EAAEC,QAAQC,QAAQ,EAAW,UAANC;;;QAGhDN,OAAO,CAACG,cAAc,GAAGG;IAC3B;IACA,OAAO;QAAEN;IAAQ;AACnB;AAEA,MAAMO,gBAAgB;AAEtB;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,cAAc,GAAG;AAE9B;;;;;;;;;CASC,GACD,OAAO,MAAMC,kBAAkB,EAAE;AAEjC,MAAMC,eAA8C;IAAEV,SAAS,CAAC;AAAE;AAElE;;;;CAIC,GACD,OAAO,SAASW;IACd,OAAOD;AACT;AAEA,gEAAgE,GAChE,SAASE,eAAeC,KAAc;IACpC,IAAIA,UAAU,QAAQ,OAAOA,UAAU,UAAU,OAAO;IACxD,MAAMC,QAAQD;IACd,OACE,OAAOC,MAAMX,aAAa,KAAK,YAC/B,OAAOW,MAAMC,MAAM,KAAK,YACxB,OAAOD,MAAME,EAAE,KAAK,YACnBF,CAAAA,MAAMG,KAAK,KAAK,aAAaH,MAAMG,KAAK,KAAK,QAAO;AAEzD;AAEA;;;;;CAKC,GACD,OAAO,SAASC,aACdC,cAAuB,EACvBC,YAA0B;IAE1B,MAAMC,WAAYF,kCAAD,AAACA,eACdnB,OAAO;IACX,IAAIqB,aAAa,QAAQ,OAAOA,aAAa,UAAU,OAAOD;IAE9D,MAAMpB,UAAwC,CAAC;IAC/C,KAAK,MAAM,CAACG,eAAeU,MAAM,IAAIZ,OAAOC,OAAO,CAACmB,UAAW;QAC7D,IAAIT,eAAeC,QAAQb,OAAO,CAACG,cAAc,GAAGU;IACtD;IACA,OAAO,aAAKO;QAAcpB;;AAC5B;AAEA,uEAAuE,GACvE,SAASsB,WACPtB,OAAqC;IAErC,MAAME,UAAUD,OAAOC,OAAO,CAACF;IAC/B,IAAIE,QAAQqB,MAAM,IAAIf,aAAa,OAAOR;IAE1C,OAAOC,OAAOuB,WAAW,CACvBtB,QACGuB,IAAI,CAAC,CAAC,GAAGC,EAAE,EAAE,GAAGC,EAAE,GAAKD,EAAEV,EAAE,CAACY,aAAa,CAACD,EAAEX,EAAE,GAC9Ca,KAAK,CAAC3B,QAAQqB,MAAM,GAAGf;AAE9B;AAEA,OAAO,MAAMsB,kBAAkBrC,SAC7BE,QACE,CAACoC,KAAKC,MAAS,CAAA;QACbhC,SAAS,CAAC;QACViC,OAAOC,CAAAA,cACLH,IAAId,CAAAA,QAAU,CAAA;oBACZjB,SAASsB,WAAW,aACfL,MAAMjB,OAAO;wBAChB,CAACkC,YAAY/B,aAAa,CAAC,EAAE,aAAK+B;4BAAajB,OAAO;;;gBAE1D,CAAA;QACF,uEAAuE;QACvE,wEAAwE;QACxEkB,QAAQ,CAAChC,eAAeC;YACtB,MAAMU,QAAQkB,MAAMhC,OAAO,CAACG,cAAc;YAC1C,IAAIW,UAAUsB,WAAW,OAAO;YAChCL,IAAId,CAAAA,QAAU,CAAA;oBACZjB,SAAS,aAAKiB,MAAMjB,OAAO;wBAAE,CAACG,cAAc,EAAE,aAAKW;4BAAOV;;;gBAC5D,CAAA;YACA,OAAO;QACT;QACA,0EAA0E;QAC1E,0EAA0E;QAC1EiC,QAAQlC,CAAAA,gBACN4B,IAAId,CAAAA;gBACF,IAAI,CAAEd,CAAAA,iBAAiBc,MAAMjB,OAAO,AAAD,GAAI,OAAOiB;gBAC9C,MAA+CA,iBAAAA,MAAMjB,OAAO,EAAtD,EAAE,CAACG,cAAc,EAAEmC,QAAQ,EAAW,GAAGrB,gBAATX,wCAASW;qCAAtCd;;gBACT,OAAO;oBAAEH,SAASM;gBAAK;YACzB;QACF,wEAAwE;QACxE,gEAAgE;QAChEiC,MAAM,CAACpC,eAAeqC,SACpBT,IAAId,CAAAA;gBACF,MAAMH,QAAQG,MAAMjB,OAAO,CAACG,cAAc;gBAC1C,IAAIW,UAAUsB,WAAW,OAAOnB;gBAChC,OAAO;oBACLjB,SAAS,aACJiB,MAAMjB,OAAO;wBAChB,CAACG,cAAc,EAAE,aAAKW;4BAAOG,OAAO;4BAAUuB;;;gBAElD;YACF;QACFC,SAAStC,CAAAA,gBACP4B,IAAId,CAAAA;gBACF,IAAI,CAAEd,CAAAA,iBAAiBc,MAAMjB,OAAO,AAAD,GAAI,OAAOiB;gBAC9C,MAAiDA,iBAAAA,MAAMjB,OAAO,EAAxD,EAAE,CAACG,cAAc,EAAEuC,UAAU,EAAW,GAAGzB,gBAATX,wCAASW;qCAAxCd;;gBACT,OAAO;oBAAEH,SAASM;gBAAK;YACzB;IACJ,CAAA,GACA;IACE,yEAAyE;IACzE,sEAAsE;IACtE,mEAAmE;IACnEqC,MAAM;IACNC,SAASnC;IACToC,SAASlC;IACTmC,OAAO5B;IACP6B,SAASrD,kBAAkB,IACzBE,0BAA0BC,cAAcU;IAE1CyC,YAAYlD;IACZmD,eAAe;AACjB,IAEF"}
@@ -0,0 +1,44 @@
1
+ import type { PendingEntry, PendingTransactionStore } from '@entifix/transactions';
2
+ import { type ReactNode } from 'react';
3
+ /** Test seam: forget that any scope was hydrated. */
4
+ export declare function resetPendingHydration(): void;
5
+ export interface PendingTransactionsProviderProps {
6
+ /**
7
+ * Who these pending writes belong to — the same `workspaceScopeKey({ userId,
8
+ * organizationId })` the workspace is scoped with.
9
+ *
10
+ * Required for the reason `WorkspaceShellProps.scope` is: IndexedDB is a
11
+ * property of the browser profile, so a host that forgets one hands the next
12
+ * account to sign in on that machine the previous account's in-flight writes.
13
+ */
14
+ scope: string;
15
+ children: ReactNode;
16
+ }
17
+ /**
18
+ * Makes the pending set available to everything below it.
19
+ *
20
+ * ⚠️ **Mount this above the plain routes, not only the workspace.** A create is
21
+ * `/<basePath>/new` on the route host, so a provider inside `WorkspaceShell`
22
+ * would never see the only kind of write that is transactional today.
23
+ */
24
+ export declare function PendingTransactionsProvider({ scope, children, }: PendingTransactionsProviderProps): import("react").JSX.Element;
25
+ /** The pending set, or a store that watches nothing outside a provider. */
26
+ export declare function usePendingTransactions(): PendingTransactionStore;
27
+ /** The entries for one entity, which is what a list renders a notice from. */
28
+ export declare function pendingFor(store: PendingTransactionStore, entity: string): readonly PendingEntry[];
29
+ /**
30
+ * The records a list should show that its own query cannot yet return.
31
+ *
32
+ * ⚠️ **Why this is not a cache patch.** Writing the row into the TanStack cache
33
+ * looks simpler and does not survive: the list refetches on mount — which is
34
+ * exactly when the operator arrives, having just been navigated here — and the
35
+ * server legitimately does not have the record yet, so the refetch replaces the
36
+ * patched page and the row disappears. The pending set outlives every refetch,
37
+ * so merging at render time is the only version that holds until the write
38
+ * actually settles.
39
+ *
40
+ * A failed entry contributes no row: the write did not happen, and the notice
41
+ * beside the table is what says so.
42
+ */
43
+ export declare function pendingRecordsFor<TRecord>(store: PendingTransactionStore, entity: string): TRecord[];
44
+ //# sourceMappingURL=pending-transactions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-transactions.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/pending-transactions.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EACZ,uBAAuB,EACxB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAEL,KAAK,SAAS,EAIf,MAAM,OAAO,CAAC;AAiCf,qDAAqD;AACrD,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,MAAM,WAAW,gCAAgC;IAC/C;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,EAC1C,KAAK,EACL,QAAQ,GACT,EAAE,gCAAgC,+BAgDlC;AAED,2EAA2E;AAC3E,wBAAgB,sBAAsB,IAAI,uBAAuB,CAEhE;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CACxB,KAAK,EAAE,uBAAuB,EAC9B,MAAM,EAAE,MAAM,GACb,SAAS,YAAY,EAAE,CAEzB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EACvC,KAAK,EAAE,uBAAuB,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,EAAE,CAKX"}
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+ import { _ as _extends } from "@swc/helpers/_/_extends";
3
+ import { jsx as _jsx } from "react/jsx-runtime";
4
+ import { NoopTransactionSink } from '@entifix/transactions';
5
+ import { createContext, useContext, useEffect, useMemo } from 'react';
6
+ import { usePendingState } from './pending-state.js';
7
+ /**
8
+ * What a caller outside a mounted provider gets: a store that watches nothing.
9
+ *
10
+ * A create on a page with no provider still works exactly as it does today — it
11
+ * simply gets no optimistic treatment. That is the honest boundary, and it is
12
+ * what keeps `began` from ever writing into an unscoped store.
13
+ */ const noopStore = _extends({}, NoopTransactionSink, {
14
+ entries: [],
15
+ attach: ()=>false,
16
+ settle: ()=>undefined,
17
+ fail: ()=>undefined,
18
+ dismiss: ()=>undefined
19
+ });
20
+ const PendingTransactionsContext = /*#__PURE__*/ createContext(noopStore);
21
+ /**
22
+ * Which scope the module-level store has already been read for.
23
+ *
24
+ * Module-level because the store it guards is: `usePendingState` is a singleton,
25
+ * so "has this been hydrated" is a fact about the module and not about any one
26
+ * component instance. A ref would reset on remount, which is the case this
27
+ * exists to survive.
28
+ */ let hydratedScope;
29
+ /** Test seam: forget that any scope was hydrated. */ export function resetPendingHydration() {
30
+ hydratedScope = undefined;
31
+ }
32
+ /**
33
+ * Makes the pending set available to everything below it.
34
+ *
35
+ * ⚠️ **Mount this above the plain routes, not only the workspace.** A create is
36
+ * `/<basePath>/new` on the route host, so a provider inside `WorkspaceShell`
37
+ * would never see the only kind of write that is transactional today.
38
+ */ export function PendingTransactionsProvider({ scope, children }) {
39
+ const pending = usePendingState((state)=>state.pending);
40
+ const began = usePendingState((state)=>state.began);
41
+ const attach = usePendingState((state)=>state.attach);
42
+ const settle = usePendingState((state)=>state.settle);
43
+ const fail = usePendingState((state)=>state.fail);
44
+ const dismiss = usePendingState((state)=>state.dismiss);
45
+ // ⚠️ The scope is applied *before* the read, never after: the storage key is
46
+ // what decides whose in-flight writes come back, so setting it late would
47
+ // restore the unscoped set first and only then start writing to the right key.
48
+ //
49
+ // ⚠️ **And the read happens once per scope, not once per mount.** Measured on
50
+ // the live fleet: `rehydrate()` *replaces* in-memory state with what is on
51
+ // disk, and the disk write that follows a `began` is asynchronous — so a
52
+ // second call (a remount, or React's development double-invoke) lands between
53
+ // the two and wipes the very entry that was just recorded. The symptom is a
54
+ // notice that appears and vanishes, with the write still in flight.
55
+ //
56
+ // Unlike `WorkspaceShell` there is no hydration *gate*, deliberately: this
57
+ // provider wraps every authenticated page, so holding render back on an
58
+ // IndexedDB read would blank the whole app on each load. A tab set has to
59
+ // gate; a pending set has nothing to be wrong about while it is still empty.
60
+ useEffect(()=>{
61
+ if (hydratedScope === scope) return;
62
+ hydratedScope = scope;
63
+ usePendingState.persist.setOptions({
64
+ name: `pending:${scope}`
65
+ });
66
+ // `rehydrate()` is typed `void | Promise<void>`, hence the wrap.
67
+ void Promise.resolve(usePendingState.persist.rehydrate());
68
+ }, [
69
+ scope
70
+ ]);
71
+ const store = useMemo(()=>({
72
+ entries: Object.values(pending),
73
+ began,
74
+ attach,
75
+ settle,
76
+ fail,
77
+ dismiss
78
+ }), [
79
+ pending,
80
+ began,
81
+ attach,
82
+ settle,
83
+ fail,
84
+ dismiss
85
+ ]);
86
+ return /*#__PURE__*/ _jsx(PendingTransactionsContext, {
87
+ value: store,
88
+ children: children
89
+ });
90
+ }
91
+ /** The pending set, or a store that watches nothing outside a provider. */ export function usePendingTransactions() {
92
+ return useContext(PendingTransactionsContext);
93
+ }
94
+ /** The entries for one entity, which is what a list renders a notice from. */ export function pendingFor(store, entity) {
95
+ return store.entries.filter((entry)=>entry.entity === entity);
96
+ }
97
+ /**
98
+ * The records a list should show that its own query cannot yet return.
99
+ *
100
+ * ⚠️ **Why this is not a cache patch.** Writing the row into the TanStack cache
101
+ * looks simpler and does not survive: the list refetches on mount — which is
102
+ * exactly when the operator arrives, having just been navigated here — and the
103
+ * server legitimately does not have the record yet, so the refetch replaces the
104
+ * patched page and the row disappears. The pending set outlives every refetch,
105
+ * so merging at render time is the only version that holds until the write
106
+ * actually settles.
107
+ *
108
+ * A failed entry contributes no row: the write did not happen, and the notice
109
+ * beside the table is what says so.
110
+ */ export function pendingRecordsFor(store, entity) {
111
+ return store.entries.filter((entry)=>entry.entity === entity && entry.state === 'pending').map((entry)=>entry.record).filter((record)=>record !== undefined);
112
+ }
113
+
114
+ //# sourceMappingURL=pending-transactions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/workspace/pending-transactions.tsx"],"sourcesContent":["'use client';\n\nimport type {\n PendingEntry,\n PendingTransactionStore,\n} from '@entifix/transactions';\nimport { NoopTransactionSink } from '@entifix/transactions';\nimport {\n createContext,\n type ReactNode,\n useContext,\n useEffect,\n useMemo,\n} from 'react';\n\nimport { usePendingState } from './pending-state.js';\n\n/**\n * What a caller outside a mounted provider gets: a store that watches nothing.\n *\n * A create on a page with no provider still works exactly as it does today — it\n * simply gets no optimistic treatment. That is the honest boundary, and it is\n * what keeps `began` from ever writing into an unscoped store.\n */\nconst noopStore: PendingTransactionStore = {\n ...NoopTransactionSink,\n entries: [],\n attach: () => false,\n settle: () => undefined,\n fail: () => undefined,\n dismiss: () => undefined,\n};\n\nconst PendingTransactionsContext =\n createContext<PendingTransactionStore>(noopStore);\n\n/**\n * Which scope the module-level store has already been read for.\n *\n * Module-level because the store it guards is: `usePendingState` is a singleton,\n * so \"has this been hydrated\" is a fact about the module and not about any one\n * component instance. A ref would reset on remount, which is the case this\n * exists to survive.\n */\nlet hydratedScope: string | undefined;\n\n/** Test seam: forget that any scope was hydrated. */\nexport function resetPendingHydration(): void {\n hydratedScope = undefined;\n}\n\nexport interface PendingTransactionsProviderProps {\n /**\n * Who these pending writes belong to — the same `workspaceScopeKey({ userId,\n * organizationId })` the workspace is scoped with.\n *\n * Required for the reason `WorkspaceShellProps.scope` is: IndexedDB is a\n * property of the browser profile, so a host that forgets one hands the next\n * account to sign in on that machine the previous account's in-flight writes.\n */\n scope: string;\n children: ReactNode;\n}\n\n/**\n * Makes the pending set available to everything below it.\n *\n * ⚠️ **Mount this above the plain routes, not only the workspace.** A create is\n * `/<basePath>/new` on the route host, so a provider inside `WorkspaceShell`\n * would never see the only kind of write that is transactional today.\n */\nexport function PendingTransactionsProvider({\n scope,\n children,\n}: PendingTransactionsProviderProps) {\n const pending = usePendingState(state => state.pending);\n const began = usePendingState(state => state.began);\n const attach = usePendingState(state => state.attach);\n const settle = usePendingState(state => state.settle);\n const fail = usePendingState(state => state.fail);\n const dismiss = usePendingState(state => state.dismiss);\n\n // ⚠️ The scope is applied *before* the read, never after: the storage key is\n // what decides whose in-flight writes come back, so setting it late would\n // restore the unscoped set first and only then start writing to the right key.\n //\n // ⚠️ **And the read happens once per scope, not once per mount.** Measured on\n // the live fleet: `rehydrate()` *replaces* in-memory state with what is on\n // disk, and the disk write that follows a `began` is asynchronous — so a\n // second call (a remount, or React's development double-invoke) lands between\n // the two and wipes the very entry that was just recorded. The symptom is a\n // notice that appears and vanishes, with the write still in flight.\n //\n // Unlike `WorkspaceShell` there is no hydration *gate*, deliberately: this\n // provider wraps every authenticated page, so holding render back on an\n // IndexedDB read would blank the whole app on each load. A tab set has to\n // gate; a pending set has nothing to be wrong about while it is still empty.\n useEffect(() => {\n if (hydratedScope === scope) return;\n hydratedScope = scope;\n usePendingState.persist.setOptions({ name: `pending:${scope}` });\n // `rehydrate()` is typed `void | Promise<void>`, hence the wrap.\n void Promise.resolve(usePendingState.persist.rehydrate());\n }, [scope]);\n\n const store = useMemo<PendingTransactionStore>(\n () => ({\n entries: Object.values(pending),\n began,\n attach,\n settle,\n fail,\n dismiss,\n }),\n [pending, began, attach, settle, fail, dismiss],\n );\n\n return (\n <PendingTransactionsContext value={store}>\n {children}\n </PendingTransactionsContext>\n );\n}\n\n/** The pending set, or a store that watches nothing outside a provider. */\nexport function usePendingTransactions(): PendingTransactionStore {\n return useContext(PendingTransactionsContext);\n}\n\n/** The entries for one entity, which is what a list renders a notice from. */\nexport function pendingFor(\n store: PendingTransactionStore,\n entity: string,\n): readonly PendingEntry[] {\n return store.entries.filter(entry => entry.entity === entity);\n}\n\n/**\n * The records a list should show that its own query cannot yet return.\n *\n * ⚠️ **Why this is not a cache patch.** Writing the row into the TanStack cache\n * looks simpler and does not survive: the list refetches on mount — which is\n * exactly when the operator arrives, having just been navigated here — and the\n * server legitimately does not have the record yet, so the refetch replaces the\n * patched page and the row disappears. The pending set outlives every refetch,\n * so merging at render time is the only version that holds until the write\n * actually settles.\n *\n * A failed entry contributes no row: the write did not happen, and the notice\n * beside the table is what says so.\n */\nexport function pendingRecordsFor<TRecord>(\n store: PendingTransactionStore,\n entity: string,\n): TRecord[] {\n return store.entries\n .filter(entry => entry.entity === entity && entry.state === 'pending')\n .map(entry => entry.record)\n .filter((record): record is TRecord => record !== undefined);\n}\n"],"names":["NoopTransactionSink","createContext","useContext","useEffect","useMemo","usePendingState","noopStore","entries","attach","settle","undefined","fail","dismiss","PendingTransactionsContext","hydratedScope","resetPendingHydration","PendingTransactionsProvider","scope","children","pending","state","began","persist","setOptions","name","Promise","resolve","rehydrate","store","Object","values","value","usePendingTransactions","pendingFor","entity","filter","entry","pendingRecordsFor","map","record"],"mappings":"AAAA;;;AAMA,SAASA,mBAAmB,QAAQ,wBAAwB;AAC5D,SACEC,aAAa,EAEbC,UAAU,EACVC,SAAS,EACTC,OAAO,QACF,QAAQ;AAEf,SAASC,eAAe,QAAQ,qBAAqB;AAErD;;;;;;CAMC,GACD,MAAMC,YAAqC,aACtCN;IACHO,SAAS,EAAE;IACXC,QAAQ,IAAM;IACdC,QAAQ,IAAMC;IACdC,MAAM,IAAMD;IACZE,SAAS,IAAMF;;AAGjB,MAAMG,2CACJZ,cAAuCK;AAEzC;;;;;;;CAOC,GACD,IAAIQ;AAEJ,mDAAmD,GACnD,OAAO,SAASC;IACdD,gBAAgBJ;AAClB;AAeA;;;;;;CAMC,GACD,OAAO,SAASM,4BAA4B,EAC1CC,KAAK,EACLC,QAAQ,EACyB;IACjC,MAAMC,UAAUd,gBAAgBe,CAAAA,QAASA,MAAMD,OAAO;IACtD,MAAME,QAAQhB,gBAAgBe,CAAAA,QAASA,MAAMC,KAAK;IAClD,MAAMb,SAASH,gBAAgBe,CAAAA,QAASA,MAAMZ,MAAM;IACpD,MAAMC,SAASJ,gBAAgBe,CAAAA,QAASA,MAAMX,MAAM;IACpD,MAAME,OAAON,gBAAgBe,CAAAA,QAASA,MAAMT,IAAI;IAChD,MAAMC,UAAUP,gBAAgBe,CAAAA,QAASA,MAAMR,OAAO;IAEtD,6EAA6E;IAC7E,0EAA0E;IAC1E,+EAA+E;IAC/E,EAAE;IACF,8EAA8E;IAC9E,2EAA2E;IAC3E,yEAAyE;IACzE,8EAA8E;IAC9E,4EAA4E;IAC5E,oEAAoE;IACpE,EAAE;IACF,2EAA2E;IAC3E,wEAAwE;IACxE,0EAA0E;IAC1E,6EAA6E;IAC7ET,UAAU;QACR,IAAIW,kBAAkBG,OAAO;QAC7BH,gBAAgBG;QAChBZ,gBAAgBiB,OAAO,CAACC,UAAU,CAAC;YAAEC,MAAM,CAAC,QAAQ,EAAEP,OAAO;QAAC;QAC9D,iEAAiE;QACjE,KAAKQ,QAAQC,OAAO,CAACrB,gBAAgBiB,OAAO,CAACK,SAAS;IACxD,GAAG;QAACV;KAAM;IAEV,MAAMW,QAAQxB,QACZ,IAAO,CAAA;YACLG,SAASsB,OAAOC,MAAM,CAACX;YACvBE;YACAb;YACAC;YACAE;YACAC;QACF,CAAA,GACA;QAACO;QAASE;QAAOb;QAAQC;QAAQE;QAAMC;KAAQ;IAGjD,qBACE,KAACC;QAA2BkB,OAAOH;kBAChCV;;AAGP;AAEA,yEAAyE,GACzE,OAAO,SAASc;IACd,OAAO9B,WAAWW;AACpB;AAEA,4EAA4E,GAC5E,OAAO,SAASoB,WACdL,KAA8B,EAC9BM,MAAc;IAEd,OAAON,MAAMrB,OAAO,CAAC4B,MAAM,CAACC,CAAAA,QAASA,MAAMF,MAAM,KAAKA;AACxD;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASG,kBACdT,KAA8B,EAC9BM,MAAc;IAEd,OAAON,MAAMrB,OAAO,CACjB4B,MAAM,CAACC,CAAAA,QAASA,MAAMF,MAAM,KAAKA,UAAUE,MAAMhB,KAAK,KAAK,WAC3DkB,GAAG,CAACF,CAAAA,QAASA,MAAMG,MAAM,EACzBJ,MAAM,CAAC,CAACI,SAA8BA,WAAW7B;AACtD"}
@@ -0,0 +1,48 @@
1
+ import type { ReactNode } from 'react';
2
+ /** Resolves a catalog key. The widened form: a tab title is data, not authored copy. */
3
+ export type TabTranslate = (key: string, params?: Record<string, unknown>) => string;
4
+ /**
5
+ * One kind of tab (a catalog, an entity editor, later an operation or wizard).
6
+ * A page is addressed by the `?tab=` value `"<kind>:<payload>"`; the kind owns
7
+ * the payload half — parsing it to an address, serializing it back, and
8
+ * rendering it. Registering a kind is the only change adding a new tab type
9
+ * needs; the router and store never change.
10
+ */
11
+ export interface TabKind<TAddr = unknown> {
12
+ /** The leading segment of the `?tab=` value, e.g. `catalog`, `entity`. */
13
+ kind: string;
14
+ /** Parse the part after `<kind>:` into an address, or `null` if invalid. */
15
+ match(payload: string): TAddr | null;
16
+ /** Serialize an address back to the payload (without the `<kind>:` prefix). */
17
+ toParam(addr: TAddr): string;
18
+ /**
19
+ * The tab caption. Handed a translate function rather than reaching for a
20
+ * hook: kinds are registered at module scope, outside any React tree.
21
+ */
22
+ title(addr: TAddr, translate: TabTranslate): string;
23
+ render(addr: TAddr): ReactNode;
24
+ }
25
+ /** A resolved tab: everything the strip and body need, addressed by `param`. */
26
+ export interface ResolvedTab {
27
+ /** The canonical `?tab=` value — the tab's identity and deep link. */
28
+ param: string;
29
+ title: string;
30
+ render: () => ReactNode;
31
+ }
32
+ /** Split a `?tab=` value into its kind and payload halves. */
33
+ export declare function splitParam(param: string): {
34
+ kind: string;
35
+ payload: string;
36
+ };
37
+ /**
38
+ * The registry of tab kinds. `resolve` turns a `?tab=` value into a renderable
39
+ * tab, or `null` when the kind is unknown or the payload is invalid — the
40
+ * caller shows a fallback rather than crashing the workspace.
41
+ */
42
+ export declare class TabRegistry {
43
+ #private;
44
+ register<TAddr>(kind: TabKind<TAddr>): this;
45
+ has(kind: string): boolean;
46
+ resolve(param: string, translate: TabTranslate): ResolvedTab | null;
47
+ }
48
+ //# sourceMappingURL=tab-kind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-kind.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/tab-kind.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,wFAAwF;AACxF,MAAM,MAAM,YAAY,GAAG,CACzB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC;AAEZ;;;;;;GAMG;AACH,MAAM,WAAW,OAAO,CAAC,KAAK,GAAG,OAAO;IACtC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAAC;IACrC,+EAA+E;IAC/E,OAAO,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IAC7B;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,GAAG,MAAM,CAAC;IACpD,MAAM,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,CAAC;CAChC;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,SAAS,CAAC;CACzB;AAED,8DAA8D;AAC9D,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAK3E;AAED;;;;GAIG;AACH,qBAAa,WAAW;;IAGtB,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI;IAK3C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,GAAG,WAAW,GAAG,IAAI;CAcpE"}