@entifix/next-shell 0.5.0 → 0.6.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 (101) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/back-office/group-by-screen-type.d.ts +2 -2
  5. package/dist/lib/back-office/group-by-screen-type.js +2 -2
  6. package/dist/lib/back-office/group-by-screen-type.js.map +1 -1
  7. package/dist/lib/back-office/nav.d.ts +1 -2
  8. package/dist/lib/back-office/nav.d.ts.map +1 -1
  9. package/dist/lib/back-office/nav.js.map +1 -1
  10. package/dist/lib/back-office/sidebar-nav.d.ts +2 -2
  11. package/dist/lib/back-office/sidebar-nav.js +2 -2
  12. package/dist/lib/back-office/sidebar-nav.js.map +1 -1
  13. package/dist/lib/command-palette/command-palette-host.js +1 -1
  14. package/dist/lib/command-palette/command-palette-host.js.map +1 -1
  15. package/dist/lib/command-palette/use-command-recency.d.ts +1 -1
  16. package/dist/lib/command-palette/use-command-recency.js +1 -1
  17. package/dist/lib/command-palette/use-command-recency.js.map +1 -1
  18. package/dist/lib/command-palette/use-nav-source.d.ts +4 -3
  19. package/dist/lib/command-palette/use-nav-source.d.ts.map +1 -1
  20. package/dist/lib/command-palette/use-nav-source.js +4 -3
  21. package/dist/lib/command-palette/use-nav-source.js.map +1 -1
  22. package/dist/lib/command-palette/use-records-source.d.ts +1 -1
  23. package/dist/lib/command-palette/use-records-source.js +2 -2
  24. package/dist/lib/command-palette/use-records-source.js.map +1 -1
  25. package/dist/lib/command-palette/use-tabs-source.d.ts +1 -1
  26. package/dist/lib/command-palette/use-tabs-source.js +1 -1
  27. package/dist/lib/command-palette/use-tabs-source.js.map +1 -1
  28. package/dist/lib/command-palette/use-use-cases-source.js +2 -2
  29. package/dist/lib/command-palette/use-use-cases-source.js.map +1 -1
  30. package/dist/lib/config/service-proxy-route.d.ts +11 -2
  31. package/dist/lib/config/service-proxy-route.d.ts.map +1 -1
  32. package/dist/lib/config/service-proxy-route.js +27 -4
  33. package/dist/lib/config/service-proxy-route.js.map +1 -1
  34. package/dist/lib/crud/entity-crud-form.js +3 -3
  35. package/dist/lib/crud/entity-crud-form.js.map +1 -1
  36. package/dist/lib/crud/hand-off-write.d.ts +1 -1
  37. package/dist/lib/crud/hand-off-write.js +1 -1
  38. package/dist/lib/crud/hand-off-write.js.map +1 -1
  39. package/dist/lib/crud/make-entity-crud.d.ts.map +1 -1
  40. package/dist/lib/crud/make-entity-crud.js +5 -6
  41. package/dist/lib/crud/make-entity-crud.js.map +1 -1
  42. package/dist/lib/crud/make-entity-crud.types.d.ts +8 -10
  43. package/dist/lib/crud/make-entity-crud.types.d.ts.map +1 -1
  44. package/dist/lib/crud/make-entity-crud.types.js.map +1 -1
  45. package/dist/lib/crud/pending-notice.d.ts +2 -2
  46. package/dist/lib/crud/pending-notice.js +2 -2
  47. package/dist/lib/crud/pending-notice.js.map +1 -1
  48. package/dist/lib/crud/use-entity-affordances.d.ts +1 -1
  49. package/dist/lib/crud/use-entity-affordances.js +1 -1
  50. package/dist/lib/crud/use-entity-affordances.js.map +1 -1
  51. package/dist/lib/crud/use-entity-bulk.d.ts +2 -2
  52. package/dist/lib/crud/use-entity-bulk.js +2 -2
  53. package/dist/lib/crud/use-entity-bulk.js.map +1 -1
  54. package/dist/lib/i18n/catalog/en.d.ts.map +1 -1
  55. package/dist/lib/i18n/catalog/en.js +1 -119
  56. package/dist/lib/i18n/catalog/en.js.map +1 -1
  57. package/dist/lib/i18n/catalog/es.d.ts +2 -131
  58. package/dist/lib/i18n/catalog/es.d.ts.map +1 -1
  59. package/dist/lib/i18n/catalog/es.js +2 -136
  60. package/dist/lib/i18n/catalog/es.js.map +1 -1
  61. package/dist/lib/i18n/catalog/index.d.ts +0 -226
  62. package/dist/lib/i18n/catalog/index.d.ts.map +1 -1
  63. package/dist/lib/i18n/spec-catalog.js +4 -4
  64. package/dist/lib/i18n/spec-catalog.js.map +1 -1
  65. package/dist/lib/search/record-search-route.d.ts +1 -1
  66. package/dist/lib/search/record-search-route.js +1 -1
  67. package/dist/lib/search/record-search-route.js.map +1 -1
  68. package/dist/lib/search/record-search-source.d.ts +1 -2
  69. package/dist/lib/search/record-search-source.d.ts.map +1 -1
  70. package/dist/lib/search/record-search-source.js.map +1 -1
  71. package/dist/lib/search/record-search.types.d.ts +1 -1
  72. package/dist/lib/search/record-search.types.js +1 -1
  73. package/dist/lib/search/record-search.types.js.map +1 -1
  74. package/dist/lib/session/account-links.js +1 -1
  75. package/dist/lib/session/account-links.js.map +1 -1
  76. package/dist/lib/session/session-keepalive.d.ts +2 -4
  77. package/dist/lib/session/session-keepalive.d.ts.map +1 -1
  78. package/dist/lib/session/session-keepalive.js +2 -4
  79. package/dist/lib/session/session-keepalive.js.map +1 -1
  80. package/dist/lib/session/use-session-refresh.js +1 -1
  81. package/dist/lib/session/use-session-refresh.js.map +1 -1
  82. package/dist/lib/wizard/wizard-tab-kind.d.ts +1 -2
  83. package/dist/lib/wizard/wizard-tab-kind.d.ts.map +1 -1
  84. package/dist/lib/wizard/wizard-tab-kind.js +1 -2
  85. package/dist/lib/wizard/wizard-tab-kind.js.map +1 -1
  86. package/dist/lib/workspace/drafts-state.d.ts.map +1 -1
  87. package/dist/lib/workspace/drafts-state.js +3 -2
  88. package/dist/lib/workspace/drafts-state.js.map +1 -1
  89. package/dist/lib/workspace/entity-tab-kind.d.ts +1 -2
  90. package/dist/lib/workspace/entity-tab-kind.d.ts.map +1 -1
  91. package/dist/lib/workspace/entity-tab-kind.js +1 -2
  92. package/dist/lib/workspace/entity-tab-kind.js.map +1 -1
  93. package/dist/lib/workspace/pending-state.d.ts +5 -4
  94. package/dist/lib/workspace/pending-state.d.ts.map +1 -1
  95. package/dist/lib/workspace/pending-state.js +1 -1
  96. package/dist/lib/workspace/pending-state.js.map +1 -1
  97. package/dist/lib/workspace/tabs-state.js +1 -1
  98. package/dist/lib/workspace/tabs-state.js.map +1 -1
  99. package/dist/server.js +1 -1
  100. package/dist/server.js.map +1 -1
  101. package/package.json +9 -9
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export * from './lib/workspace/index.js';
13
13
  * autosave it, so it has to be able to name the type — but the type is core's,
14
14
  * and a fourth structural copy of `Record<string, string>` beside
15
15
  * `EntityFormValues`/`EntityFormDraft`/`EntityLinkDraft` is exactly what
16
- * [ADR 0034](../../../../docs/adr/0034-composition-metadata.md) collapsed. One
16
+ * composition metadata collapsed. One
17
17
  * declaration, reachable from the layer its consumers already depend on.
18
18
  */
19
19
  export * from './lib/i18n/catalog/index.js';
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export * from './lib/workspace/index.js';
12
12
  * autosave it, so it has to be able to name the type — but the type is core's,
13
13
  * and a fourth structural copy of `Record<string, string>` beside
14
14
  * `EntityFormValues`/`EntityFormDraft`/`EntityLinkDraft` is exactly what
15
- * [ADR 0034](../../../../docs/adr/0034-composition-metadata.md) collapsed. One
15
+ * composition metadata collapsed. One
16
16
  * declaration, reachable from the layer its consumers already depend on.
17
17
  */ export * from './lib/i18n/catalog/index.js';
18
18
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Use this file to export React client components (e.g. those with 'use client' directive) or other non-server utilities\n\nexport * from './lib/back-office/index.js';\nexport * from './lib/command-palette/index.js';\nexport * from './lib/crud/index.js';\nexport * from './lib/i18n/index.js';\n// The browser half of record search: a typed `fetch` and the shapes it returns.\n// The route handler and the source declarations stay in `/server`, since they\n// read cookies and would be stamped as client references here.\nexport type * from './lib/search/record-search.types.js';\nexport * from './lib/search/search-records.js';\nexport * from './lib/session/index.js';\n// Type-only, so it stays erased: the account-link *values* ship from `/server`\n// (see src/server.ts), but `AccountMenuProps` names these types and a consumer\n// of the client entry has to be able to name them too.\nexport type {\n AccountDestination,\n AccountLabelKey,\n AccountLink,\n} from './lib/session/account-links.js';\nexport * from './lib/wizard/index.js';\nexport * from './lib/workspace/index.js';\n\n/**\n * Re-exported, not redeclared. A workspace host holds an entity form's draft to\n * autosave it, so it has to be able to name the type — but the type is core's,\n * and a fourth structural copy of `Record<string, string>` beside\n * `EntityFormValues`/`EntityFormDraft`/`EntityLinkDraft` is exactly what\n * [ADR 0034](../../../../docs/adr/0034-composition-metadata.md) collapsed. One\n * declaration, reachable from the layer its consumers already depend on.\n */\nexport * from './lib/i18n/catalog/index.js';\nexport type { EntityDraft } from '@entifix/core';\n"],"names":[],"mappings":"AAAA,yHAAyH;AAEzH,cAAc,6BAA6B;AAC3C,cAAc,iCAAiC;AAC/C,cAAc,sBAAsB;AACpC,cAAc,sBAAsB;AAKpC,cAAc,iCAAiC;AAC/C,cAAc,yBAAyB;AASvC,cAAc,wBAAwB;AACtC,cAAc,2BAA2B;AAEzC;;;;;;;CAOC,GACD,cAAc,8BAA8B"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Use this file to export React client components (e.g. those with 'use client' directive) or other non-server utilities\n\nexport * from './lib/back-office/index.js';\nexport * from './lib/command-palette/index.js';\nexport * from './lib/crud/index.js';\nexport * from './lib/i18n/index.js';\n// The browser half of record search: a typed `fetch` and the shapes it returns.\n// The route handler and the source declarations stay in `/server`, since they\n// read cookies and would be stamped as client references here.\nexport type * from './lib/search/record-search.types.js';\nexport * from './lib/search/search-records.js';\nexport * from './lib/session/index.js';\n// Type-only, so it stays erased: the account-link *values* ship from `/server`\n// (see src/server.ts), but `AccountMenuProps` names these types and a consumer\n// of the client entry has to be able to name them too.\nexport type {\n AccountDestination,\n AccountLabelKey,\n AccountLink,\n} from './lib/session/account-links.js';\nexport * from './lib/wizard/index.js';\nexport * from './lib/workspace/index.js';\n\n/**\n * Re-exported, not redeclared. A workspace host holds an entity form's draft to\n * autosave it, so it has to be able to name the type — but the type is core's,\n * and a fourth structural copy of `Record<string, string>` beside\n * `EntityFormValues`/`EntityFormDraft`/`EntityLinkDraft` is exactly what\n * composition metadata collapsed. One\n * declaration, reachable from the layer its consumers already depend on.\n */\nexport * from './lib/i18n/catalog/index.js';\nexport type { EntityDraft } from '@entifix/core';\n"],"names":[],"mappings":"AAAA,yHAAyH;AAEzH,cAAc,6BAA6B;AAC3C,cAAc,iCAAiC;AAC/C,cAAc,sBAAsB;AACpC,cAAc,sBAAsB;AAKpC,cAAc,iCAAiC;AAC/C,cAAc,yBAAyB;AASvC,cAAc,wBAAwB;AACtC,cAAc,2BAA2B;AAEzC;;;;;;;CAOC,GACD,cAAc,8BAA8B"}
@@ -3,7 +3,7 @@ import type { NavSection, NavTier } from './nav.js';
3
3
  * Group sections into the sidebar's top tier — the screen type — preserving the
4
4
  * order sections were contributed in within each group.
5
5
  *
6
- * The tier is the **type** and the domain sits beneath it (ADR 0033). Four fixed
6
+ * The tier is the **type** and the domain sits beneath it. Four fixed
7
7
  * entries against eleven domains, so "where do I go to add a courier" has one
8
8
  * answer whoever owns couriers; the daily operator's speed is the command
9
9
  * palette's job rather than the sidebar's.
@@ -15,7 +15,7 @@ import type { NavSection, NavTier } from './nav.js';
15
15
  *
16
16
  * There is no third level: a `NavItem` has no children, so the depth cap lives
17
17
  * in the type rather than in a convention. An arbitrary tree scans badly and
18
- * rots, and every case ADR 0022's eleven domains present fits three tiers.
18
+ * rots, and every case a back office with a dozen domains presents fits three tiers.
19
19
  */
20
20
  export declare function groupByScreenType(sections: NavSection[]): NavTier[];
21
21
  //# sourceMappingURL=group-by-screen-type.d.ts.map
@@ -3,7 +3,7 @@ import { screenTypeRank } from '@entifix/authz';
3
3
  * Group sections into the sidebar's top tier — the screen type — preserving the
4
4
  * order sections were contributed in within each group.
5
5
  *
6
- * The tier is the **type** and the domain sits beneath it (ADR 0033). Four fixed
6
+ * The tier is the **type** and the domain sits beneath it. Four fixed
7
7
  * entries against eleven domains, so "where do I go to add a courier" has one
8
8
  * answer whoever owns couriers; the daily operator's speed is the command
9
9
  * palette's job rather than the sidebar's.
@@ -15,7 +15,7 @@ import { screenTypeRank } from '@entifix/authz';
15
15
  *
16
16
  * There is no third level: a `NavItem` has no children, so the depth cap lives
17
17
  * in the type rather than in a convention. An arbitrary tree scans badly and
18
- * rots, and every case ADR 0022's eleven domains present fits three tiers.
18
+ * rots, and every case a back office with a dozen domains presents fits three tiers.
19
19
  */ export function groupByScreenType(sections) {
20
20
  const tiers = [];
21
21
  for (const section of sections){
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/back-office/group-by-screen-type.ts"],"sourcesContent":["import { screenTypeRank } from '@entifix/authz';\n\nimport type { NavSection, NavTier } from './nav.js';\n\n/**\n * Group sections into the sidebar's top tier — the screen type — preserving the\n * order sections were contributed in within each group.\n *\n * The tier is the **type** and the domain sits beneath it (ADR 0033). Four fixed\n * entries against eleven domains, so \"where do I go to add a courier\" has one\n * answer whoever owns couriers; the daily operator's speed is the command\n * palette's job rather than the sidebar's.\n *\n * Adjacent sections are **not** merged blindly — a host concatenates fragments\n * from several shells, and two shells can legitimately contribute `master`\n * sections that are not adjacent in the list. Grouping by rank rather than by\n * position is what keeps Definiciones one heading instead of three.\n *\n * There is no third level: a `NavItem` has no children, so the depth cap lives\n * in the type rather than in a convention. An arbitrary tree scans badly and\n * rots, and every case ADR 0022's eleven domains present fits three tiers.\n */\nexport function groupByScreenType(sections: NavSection[]): NavTier[] {\n const tiers: NavTier[] = [];\n\n for (const section of sections) {\n const tier = tiers.find(candidate => candidate.type === section.type);\n if (tier === undefined) {\n tiers.push({ type: section.type, sections: [section] });\n continue;\n }\n tier.sections.push(section);\n }\n\n // An untyped section sorts last — it is not a screen group, so it belongs\n // below the ones that are, never interleaved with them.\n return tiers.sort((a, b) => screenTypeRank(a.type) - screenTypeRank(b.type));\n}\n"],"names":["screenTypeRank","groupByScreenType","sections","tiers","section","tier","find","candidate","type","undefined","push","sort","a","b"],"mappings":"AAAA,SAASA,cAAc,QAAQ,iBAAiB;AAIhD;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC,kBAAkBC,QAAsB;IACtD,MAAMC,QAAmB,EAAE;IAE3B,KAAK,MAAMC,WAAWF,SAAU;QAC9B,MAAMG,OAAOF,MAAMG,IAAI,CAACC,CAAAA,YAAaA,UAAUC,IAAI,KAAKJ,QAAQI,IAAI;QACpE,IAAIH,SAASI,WAAW;YACtBN,MAAMO,IAAI,CAAC;gBAAEF,MAAMJ,QAAQI,IAAI;gBAAEN,UAAU;oBAACE;iBAAQ;YAAC;YACrD;QACF;QACAC,KAAKH,QAAQ,CAACQ,IAAI,CAACN;IACrB;IAEA,0EAA0E;IAC1E,wDAAwD;IACxD,OAAOD,MAAMQ,IAAI,CAAC,CAACC,GAAGC,IAAMb,eAAeY,EAAEJ,IAAI,IAAIR,eAAea,EAAEL,IAAI;AAC5E"}
1
+ {"version":3,"sources":["../../../src/lib/back-office/group-by-screen-type.ts"],"sourcesContent":["import { screenTypeRank } from '@entifix/authz';\n\nimport type { NavSection, NavTier } from './nav.js';\n\n/**\n * Group sections into the sidebar's top tier — the screen type — preserving the\n * order sections were contributed in within each group.\n *\n * The tier is the **type** and the domain sits beneath it. Four fixed\n * entries against eleven domains, so \"where do I go to add a courier\" has one\n * answer whoever owns couriers; the daily operator's speed is the command\n * palette's job rather than the sidebar's.\n *\n * Adjacent sections are **not** merged blindly — a host concatenates fragments\n * from several shells, and two shells can legitimately contribute `master`\n * sections that are not adjacent in the list. Grouping by rank rather than by\n * position is what keeps Definiciones one heading instead of three.\n *\n * There is no third level: a `NavItem` has no children, so the depth cap lives\n * in the type rather than in a convention. An arbitrary tree scans badly and\n * rots, and every case a back office with a dozen domains presents fits three tiers.\n */\nexport function groupByScreenType(sections: NavSection[]): NavTier[] {\n const tiers: NavTier[] = [];\n\n for (const section of sections) {\n const tier = tiers.find(candidate => candidate.type === section.type);\n if (tier === undefined) {\n tiers.push({ type: section.type, sections: [section] });\n continue;\n }\n tier.sections.push(section);\n }\n\n // An untyped section sorts last — it is not a screen group, so it belongs\n // below the ones that are, never interleaved with them.\n return tiers.sort((a, b) => screenTypeRank(a.type) - screenTypeRank(b.type));\n}\n"],"names":["screenTypeRank","groupByScreenType","sections","tiers","section","tier","find","candidate","type","undefined","push","sort","a","b"],"mappings":"AAAA,SAASA,cAAc,QAAQ,iBAAiB;AAIhD;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC,kBAAkBC,QAAsB;IACtD,MAAMC,QAAmB,EAAE;IAE3B,KAAK,MAAMC,WAAWF,SAAU;QAC9B,MAAMG,OAAOF,MAAMG,IAAI,CAACC,CAAAA,YAAaA,UAAUC,IAAI,KAAKJ,QAAQI,IAAI;QACpE,IAAIH,SAASI,WAAW;YACtBN,MAAMO,IAAI,CAAC;gBAAEF,MAAMJ,QAAQI,IAAI;gBAAEN,UAAU;oBAACE;iBAAQ;YAAC;YACrD;QACF;QACAC,KAAKH,QAAQ,CAACQ,IAAI,CAACN;IACrB;IAEA,0EAA0E;IAC1E,wDAAwD;IACxD,OAAOD,MAAMQ,IAAI,CAAC,CAACC,GAAGC,IAAMb,eAAeY,EAAEJ,IAAI,IAAIR,eAAea,EAAEL,IAAI;AAC5E"}
@@ -17,8 +17,7 @@ export interface NavSection {
17
17
  title?: string;
18
18
  items: NavItem[];
19
19
  /**
20
- * What shape these screens are — the sidebar's **top tier**, above the domain
21
- * ([ADR 0033](../../../../../../../docs/adr/0033-the-screen-taxonomy.md)).
20
+ * What shape these screens are — the sidebar's **top tier**, above the domain.
22
21
  *
23
22
  * A `ScreenType` rather than a translated string, unlike `title`: the four
24
23
  * names are one shipped vocabulary that `SCREEN_TYPE_LABEL_KEYS` maps into the
@@ -1 +1 @@
1
- {"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../../../src/lib/back-office/nav.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB"}
1
+ {"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../../../src/lib/back-office/nav.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/back-office/nav.ts"],"sourcesContent":["import type { ScreenType } from '@entifix/authz';\nimport type { ReactNode } from 'react';\n\nexport interface NavItem {\n label: string;\n href: string;\n /** Optional leading glyph, shown in both expanded and collapsed states. */\n icon?: ReactNode;\n /**\n * When set, the item also offers \"Open in workspace\" — a link to\n * `/workspace?tab=<workspace>` that opens this destination as a persisted tab\n * instead of navigating the whole page.\n */\n workspace?: string;\n}\n\nexport interface NavSection {\n /** The domain group's heading, already translated. */\n title?: string;\n items: NavItem[];\n /**\n * What shape these screens are — the sidebar's **top tier**, above the domain\n * ([ADR 0033](../../../../../../../docs/adr/0033-the-screen-taxonomy.md)).\n *\n * A `ScreenType` rather than a translated string, unlike `title`: the four\n * names are one shipped vocabulary that `SCREEN_TYPE_LABEL_KEYS` maps into the\n * `shell:` namespace, so this component resolves them itself. A host that\n * translated them would be translating copy it does not own.\n *\n * **Optional, and narrowly so.** A section with no type is not a fifth\n * category, it is a section that is not a screen group at all — the account\n * surface is the one case. Untyped sections render below every typed one.\n */\n type?: ScreenType;\n}\n\n/** A tier of the sidebar: the sections sharing one {@link ScreenType}. */\nexport interface NavTier {\n type?: ScreenType;\n sections: NavSection[];\n}\n"],"names":[],"mappings":"AAoCA,wEAAwE,GACxE,WAGC"}
1
+ {"version":3,"sources":["../../../src/lib/back-office/nav.ts"],"sourcesContent":["import type { ScreenType } from '@entifix/authz';\nimport type { ReactNode } from 'react';\n\nexport interface NavItem {\n label: string;\n href: string;\n /** Optional leading glyph, shown in both expanded and collapsed states. */\n icon?: ReactNode;\n /**\n * When set, the item also offers \"Open in workspace\" — a link to\n * `/workspace?tab=<workspace>` that opens this destination as a persisted tab\n * instead of navigating the whole page.\n */\n workspace?: string;\n}\n\nexport interface NavSection {\n /** The domain group's heading, already translated. */\n title?: string;\n items: NavItem[];\n /**\n * What shape these screens are — the sidebar's **top tier**, above the domain.\n *\n * A `ScreenType` rather than a translated string, unlike `title`: the four\n * names are one shipped vocabulary that `SCREEN_TYPE_LABEL_KEYS` maps into the\n * `shell:` namespace, so this component resolves them itself. A host that\n * translated them would be translating copy it does not own.\n *\n * **Optional, and narrowly so.** A section with no type is not a fifth\n * category, it is a section that is not a screen group at all — the account\n * surface is the one case. Untyped sections render below every typed one.\n */\n type?: ScreenType;\n}\n\n/** A tier of the sidebar: the sections sharing one {@link ScreenType}. */\nexport interface NavTier {\n type?: ScreenType;\n sections: NavSection[];\n}\n"],"names":[],"mappings":"AAmCA,wEAAwE,GACxE,WAGC"}
@@ -12,8 +12,8 @@ export declare function isActive(pathname: string, href: string): boolean;
12
12
  /**
13
13
  * The back office's primary navigation: screen type › domain › destination.
14
14
  *
15
- * The top tier is the **type**, not the domain (ADR 0033). Four fixed entries
16
- * against the eleven domains ADR 0022 fixes for v1, so a new operator has one
15
+ * The top tier is the **type**, not the domain. Four fixed entries
16
+ * against however many domains a host has, so a new operator has one
17
17
  * place to look; the daily operator's speed is the command palette's job.
18
18
  *
19
19
  * **Collapsed mode deliberately does not nest.** The icon column stays flat and
@@ -79,8 +79,8 @@ function ItemLink({ item, active, collapsed }) {
79
79
  /**
80
80
  * The back office's primary navigation: screen type › domain › destination.
81
81
  *
82
- * The top tier is the **type**, not the domain (ADR 0033). Four fixed entries
83
- * against the eleven domains ADR 0022 fixes for v1, so a new operator has one
82
+ * The top tier is the **type**, not the domain. Four fixed entries
83
+ * against however many domains a host has, so a new operator has one
84
84
  * place to look; the daily operator's speed is the command palette's job.
85
85
  *
86
86
  * **Collapsed mode deliberately does not nest.** The icon column stays flat and
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/back-office/sidebar-nav.tsx"],"sourcesContent":["'use client';\n\nimport { SCREEN_TYPE_LABEL_KEYS } from '@entifix/authz';\nimport { cn, useT, useTranslateKey } from '@entifix/react-controls';\nimport { usePathname } from 'next/navigation';\nimport { useMemo } from 'react';\n\nimport { LocaleLink, useLocaleHref } from '../i18n/index.js';\nimport { groupByScreenType } from './group-by-screen-type.js';\nimport type { NavItem, NavSection } from './nav.js';\n\nexport interface SidebarNavProps {\n sections: NavSection[];\n /** When collapsed, labels and headings hide; only icons remain. */\n collapsed?: boolean;\n /** Which domain groups are collapsed, keyed by heading. */\n collapsedGroups?: Record<string, boolean>;\n onToggleGroup?: (title: string) => void;\n}\n\n/** A path is active when it equals the item href or is nested beneath it. */\nexport function isActive(pathname: string, href: string): boolean {\n return pathname === href || pathname.startsWith(`${href}/`);\n}\n\nconst workspaceHref = (workspace: string) =>\n `/workspace?tab=${encodeURIComponent(workspace)}`;\n\n/**\n * The two \"open where\" choices for one destination.\n *\n * Both used to be either absent or invisible. The workspace link existed but was\n * `opacity-0` until `group-hover/nav` — undiscoverable on touch, where there is\n * no hover — and was dropped entirely while collapsed, which is the mode a power\n * operator lives in. There was no open-in-a-browser-tab control at all; middle\n * click worked by accident, and only with a mouse.\n *\n * They are now always rendered and always focusable. `text-content-muted` plus a\n * hover/focus lift is what keeps a permanently visible control from competing\n * with the destination itself — the affordance is quiet, not hidden.\n */\nfunction ItemActions({ item, label }: { item: NavItem; label: string }) {\n const t = useT('shell');\n const withLocale = useLocaleHref();\n\n return (\n <>\n {item.workspace !== undefined && (\n <LocaleLink\n href={workspaceHref(item.workspace)}\n aria-label={t('nav.openInWorkspace', { label })}\n title={t('nav.openInWorkspaceShort')}\n className=\"rounded shrink-0 p-3xs text-content-muted focus-ring transition-colors hover:bg-surface hover:text-content\"\n >\n <span aria-hidden=\"true\">⧉</span>\n </LocaleLink>\n )}\n <a\n href={withLocale(item.href)}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={t('nav.openInNewTab', { label })}\n title={t('nav.openInNewTabShort')}\n className=\"rounded shrink-0 p-3xs text-content-muted focus-ring transition-colors hover:bg-surface hover:text-content\"\n >\n <span aria-hidden=\"true\">↗</span>\n </a>\n </>\n );\n}\n\nfunction ItemLink({\n item,\n active,\n collapsed,\n}: {\n item: NavItem;\n active: boolean;\n collapsed: boolean;\n}) {\n return (\n <LocaleLink\n href={item.href}\n aria-current={active ? 'page' : undefined}\n title={collapsed ? item.label : undefined}\n className={cn(\n 'flex flex-1 items-center gap-2xs rounded-md px-2xs py-3xs text-step-sm focus-ring transition-colors',\n active\n ? 'bg-primary text-primary-content'\n : 'text-content hover:bg-surface',\n )}\n >\n {item.icon !== undefined && (\n <span aria-hidden=\"true\" className=\"shrink-0\">\n {item.icon}\n </span>\n )}\n {!collapsed && <span className=\"truncate\">{item.label}</span>}\n </LocaleLink>\n );\n}\n\n/**\n * The back office's primary navigation: screen type › domain › destination.\n *\n * The top tier is the **type**, not the domain (ADR 0033). Four fixed entries\n * against the eleven domains ADR 0022 fixes for v1, so a new operator has one\n * place to look; the daily operator's speed is the command palette's job.\n *\n * **Collapsed mode deliberately does not nest.** The icon column stays flat and\n * every group becomes a labelled separator, because a flyout submenu would cost\n * an extra click per navigation to precisely the operator who chose the compact\n * mode. What collapsing hides is labels, and the per-item tooltip already\n * returns those. Group collapse is therefore an expanded-mode affordance.\n */\nexport function SidebarNav({\n sections,\n collapsed = false,\n collapsedGroups = {},\n onToggleGroup,\n}: SidebarNavProps) {\n const t = useT('shell');\n // The four type names are `shell:` copy owned by `@entifix/authz`'s\n // `SCREEN_TYPE_LABEL_KEYS`, so they arrive as fully-qualified keys rather than\n // as names in this namespace — the documented escape hatch, not authored copy.\n const translateKey = useTranslateKey();\n const pathname = usePathname() ?? '';\n const tiers = useMemo(() => groupByScreenType(sections), [sections]);\n\n return (\n <nav aria-label={t('nav.primary')} className=\"flex flex-col gap-m\">\n {tiers.map((tier, tierIndex) => (\n <div\n key={tier.type ?? `tier-${tierIndex}`}\n className=\"flex flex-col gap-2xs\"\n >\n {tier.type !== undefined && !collapsed && (\n <p className=\"tracking-wide px-2xs text-step-xs font-semibold text-content uppercase\">\n {translateKey(SCREEN_TYPE_LABEL_KEYS[tier.type])}\n </p>\n )}\n {/* Collapsed, the heading is gone but the grouping is not: a rule plus\n an accessible name is what keeps the icon column from reading as\n one undifferentiated list. */}\n {collapsed && tierIndex > 0 && (\n <hr className=\"border-border\" aria-hidden=\"true\" />\n )}\n {/* `role=\"group\"` only where there is a name for it. An unnamed group\n is announced as an empty landmark, which is noise rather than\n structure — and the untyped section is not a screen group at all. */}\n <div\n className=\"flex flex-col gap-2xs\"\n {...(tier.type === undefined\n ? {}\n : {\n role: 'group',\n 'aria-label': translateKey(SCREEN_TYPE_LABEL_KEYS[tier.type]),\n })}\n >\n {tier.sections.map((section, sectionIndex) => {\n const title = section.title;\n const groupCollapsed =\n title !== undefined && collapsedGroups[title] === true;\n // A collapsed group holding the active route still shows where you\n // are — otherwise collapsing a group hides the answer.\n const hasActive = section.items.some(item =>\n isActive(pathname, item.href),\n );\n\n return (\n <div\n key={title ?? `section-${sectionIndex}`}\n className=\"flex flex-col gap-3xs\"\n >\n {title !== undefined &&\n !collapsed &&\n (onToggleGroup === undefined ? (\n <p className=\"tracking-wide px-2xs text-step-xs text-content-muted uppercase\">\n {title}\n </p>\n ) : (\n <button\n type=\"button\"\n onClick={() => onToggleGroup(title)}\n aria-expanded={!groupCollapsed}\n aria-label={\n groupCollapsed\n ? t('sidebar.expandGroup', { group: title })\n : t('sidebar.collapseGroup', { group: title })\n }\n className={cn(\n 'rounded tracking-wide flex items-center gap-3xs px-2xs text-step-xs uppercase focus-ring transition-colors hover:text-content',\n groupCollapsed && hasActive\n ? 'text-content'\n : 'text-content-muted',\n )}\n >\n <span aria-hidden=\"true\">\n {groupCollapsed ? '▸' : '▾'}\n </span>\n <span className=\"truncate\">{title}</span>\n </button>\n ))}\n {!(groupCollapsed && !collapsed) &&\n section.items.map(item => (\n <div\n key={item.href}\n className=\"flex items-center gap-3xs\"\n >\n <ItemLink\n item={item}\n active={isActive(pathname, item.href)}\n collapsed={collapsed}\n />\n {!collapsed && (\n <ItemActions item={item} label={item.label} />\n )}\n </div>\n ))}\n </div>\n );\n })}\n </div>\n </div>\n ))}\n </nav>\n );\n}\n"],"names":["SCREEN_TYPE_LABEL_KEYS","cn","useT","useTranslateKey","usePathname","useMemo","LocaleLink","useLocaleHref","groupByScreenType","isActive","pathname","href","startsWith","workspaceHref","workspace","encodeURIComponent","ItemActions","item","label","t","withLocale","undefined","aria-label","title","className","span","aria-hidden","a","target","rel","ItemLink","active","collapsed","aria-current","icon","SidebarNav","sections","collapsedGroups","onToggleGroup","translateKey","tiers","nav","map","tier","tierIndex","div","type","p","hr","role","section","sectionIndex","groupCollapsed","hasActive","items","some","button","onClick","aria-expanded","group"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/lib/back-office/sidebar-nav.tsx"],"sourcesContent":["'use client';\n\nimport { SCREEN_TYPE_LABEL_KEYS } from '@entifix/authz';\nimport { cn, useT, useTranslateKey } from '@entifix/react-controls';\nimport { usePathname } from 'next/navigation';\nimport { useMemo } from 'react';\n\nimport { LocaleLink, useLocaleHref } from '../i18n/index.js';\nimport { groupByScreenType } from './group-by-screen-type.js';\nimport type { NavItem, NavSection } from './nav.js';\n\nexport interface SidebarNavProps {\n sections: NavSection[];\n /** When collapsed, labels and headings hide; only icons remain. */\n collapsed?: boolean;\n /** Which domain groups are collapsed, keyed by heading. */\n collapsedGroups?: Record<string, boolean>;\n onToggleGroup?: (title: string) => void;\n}\n\n/** A path is active when it equals the item href or is nested beneath it. */\nexport function isActive(pathname: string, href: string): boolean {\n return pathname === href || pathname.startsWith(`${href}/`);\n}\n\nconst workspaceHref = (workspace: string) =>\n `/workspace?tab=${encodeURIComponent(workspace)}`;\n\n/**\n * The two \"open where\" choices for one destination.\n *\n * Both used to be either absent or invisible. The workspace link existed but was\n * `opacity-0` until `group-hover/nav` — undiscoverable on touch, where there is\n * no hover — and was dropped entirely while collapsed, which is the mode a power\n * operator lives in. There was no open-in-a-browser-tab control at all; middle\n * click worked by accident, and only with a mouse.\n *\n * They are now always rendered and always focusable. `text-content-muted` plus a\n * hover/focus lift is what keeps a permanently visible control from competing\n * with the destination itself — the affordance is quiet, not hidden.\n */\nfunction ItemActions({ item, label }: { item: NavItem; label: string }) {\n const t = useT('shell');\n const withLocale = useLocaleHref();\n\n return (\n <>\n {item.workspace !== undefined && (\n <LocaleLink\n href={workspaceHref(item.workspace)}\n aria-label={t('nav.openInWorkspace', { label })}\n title={t('nav.openInWorkspaceShort')}\n className=\"rounded shrink-0 p-3xs text-content-muted focus-ring transition-colors hover:bg-surface hover:text-content\"\n >\n <span aria-hidden=\"true\">⧉</span>\n </LocaleLink>\n )}\n <a\n href={withLocale(item.href)}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={t('nav.openInNewTab', { label })}\n title={t('nav.openInNewTabShort')}\n className=\"rounded shrink-0 p-3xs text-content-muted focus-ring transition-colors hover:bg-surface hover:text-content\"\n >\n <span aria-hidden=\"true\">↗</span>\n </a>\n </>\n );\n}\n\nfunction ItemLink({\n item,\n active,\n collapsed,\n}: {\n item: NavItem;\n active: boolean;\n collapsed: boolean;\n}) {\n return (\n <LocaleLink\n href={item.href}\n aria-current={active ? 'page' : undefined}\n title={collapsed ? item.label : undefined}\n className={cn(\n 'flex flex-1 items-center gap-2xs rounded-md px-2xs py-3xs text-step-sm focus-ring transition-colors',\n active\n ? 'bg-primary text-primary-content'\n : 'text-content hover:bg-surface',\n )}\n >\n {item.icon !== undefined && (\n <span aria-hidden=\"true\" className=\"shrink-0\">\n {item.icon}\n </span>\n )}\n {!collapsed && <span className=\"truncate\">{item.label}</span>}\n </LocaleLink>\n );\n}\n\n/**\n * The back office's primary navigation: screen type › domain › destination.\n *\n * The top tier is the **type**, not the domain. Four fixed entries\n * against however many domains a host has, so a new operator has one\n * place to look; the daily operator's speed is the command palette's job.\n *\n * **Collapsed mode deliberately does not nest.** The icon column stays flat and\n * every group becomes a labelled separator, because a flyout submenu would cost\n * an extra click per navigation to precisely the operator who chose the compact\n * mode. What collapsing hides is labels, and the per-item tooltip already\n * returns those. Group collapse is therefore an expanded-mode affordance.\n */\nexport function SidebarNav({\n sections,\n collapsed = false,\n collapsedGroups = {},\n onToggleGroup,\n}: SidebarNavProps) {\n const t = useT('shell');\n // The four type names are `shell:` copy owned by `@entifix/authz`'s\n // `SCREEN_TYPE_LABEL_KEYS`, so they arrive as fully-qualified keys rather than\n // as names in this namespace — the documented escape hatch, not authored copy.\n const translateKey = useTranslateKey();\n const pathname = usePathname() ?? '';\n const tiers = useMemo(() => groupByScreenType(sections), [sections]);\n\n return (\n <nav aria-label={t('nav.primary')} className=\"flex flex-col gap-m\">\n {tiers.map((tier, tierIndex) => (\n <div\n key={tier.type ?? `tier-${tierIndex}`}\n className=\"flex flex-col gap-2xs\"\n >\n {tier.type !== undefined && !collapsed && (\n <p className=\"tracking-wide px-2xs text-step-xs font-semibold text-content uppercase\">\n {translateKey(SCREEN_TYPE_LABEL_KEYS[tier.type])}\n </p>\n )}\n {/* Collapsed, the heading is gone but the grouping is not: a rule plus\n an accessible name is what keeps the icon column from reading as\n one undifferentiated list. */}\n {collapsed && tierIndex > 0 && (\n <hr className=\"border-border\" aria-hidden=\"true\" />\n )}\n {/* `role=\"group\"` only where there is a name for it. An unnamed group\n is announced as an empty landmark, which is noise rather than\n structure — and the untyped section is not a screen group at all. */}\n <div\n className=\"flex flex-col gap-2xs\"\n {...(tier.type === undefined\n ? {}\n : {\n role: 'group',\n 'aria-label': translateKey(SCREEN_TYPE_LABEL_KEYS[tier.type]),\n })}\n >\n {tier.sections.map((section, sectionIndex) => {\n const title = section.title;\n const groupCollapsed =\n title !== undefined && collapsedGroups[title] === true;\n // A collapsed group holding the active route still shows where you\n // are — otherwise collapsing a group hides the answer.\n const hasActive = section.items.some(item =>\n isActive(pathname, item.href),\n );\n\n return (\n <div\n key={title ?? `section-${sectionIndex}`}\n className=\"flex flex-col gap-3xs\"\n >\n {title !== undefined &&\n !collapsed &&\n (onToggleGroup === undefined ? (\n <p className=\"tracking-wide px-2xs text-step-xs text-content-muted uppercase\">\n {title}\n </p>\n ) : (\n <button\n type=\"button\"\n onClick={() => onToggleGroup(title)}\n aria-expanded={!groupCollapsed}\n aria-label={\n groupCollapsed\n ? t('sidebar.expandGroup', { group: title })\n : t('sidebar.collapseGroup', { group: title })\n }\n className={cn(\n 'rounded tracking-wide flex items-center gap-3xs px-2xs text-step-xs uppercase focus-ring transition-colors hover:text-content',\n groupCollapsed && hasActive\n ? 'text-content'\n : 'text-content-muted',\n )}\n >\n <span aria-hidden=\"true\">\n {groupCollapsed ? '▸' : '▾'}\n </span>\n <span className=\"truncate\">{title}</span>\n </button>\n ))}\n {!(groupCollapsed && !collapsed) &&\n section.items.map(item => (\n <div\n key={item.href}\n className=\"flex items-center gap-3xs\"\n >\n <ItemLink\n item={item}\n active={isActive(pathname, item.href)}\n collapsed={collapsed}\n />\n {!collapsed && (\n <ItemActions item={item} label={item.label} />\n )}\n </div>\n ))}\n </div>\n );\n })}\n </div>\n </div>\n ))}\n </nav>\n );\n}\n"],"names":["SCREEN_TYPE_LABEL_KEYS","cn","useT","useTranslateKey","usePathname","useMemo","LocaleLink","useLocaleHref","groupByScreenType","isActive","pathname","href","startsWith","workspaceHref","workspace","encodeURIComponent","ItemActions","item","label","t","withLocale","undefined","aria-label","title","className","span","aria-hidden","a","target","rel","ItemLink","active","collapsed","aria-current","icon","SidebarNav","sections","collapsedGroups","onToggleGroup","translateKey","tiers","nav","map","tier","tierIndex","div","type","p","hr","role","section","sectionIndex","groupCollapsed","hasActive","items","some","button","onClick","aria-expanded","group"],"mappings":"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"}
@@ -15,7 +15,7 @@ import { useUseCaseSources } from './use-use-cases-source.js';
15
15
  /**
16
16
  * ⌘K **and** ⌘⇧P, and neither is load-bearing.
17
17
  *
18
- * #112 argued for ⌘⇧P alone because Chrome and Firefox claim ⌘K for address-bar
18
+ * The case for ⌘⇧P alone was that Chrome and Firefox claim ⌘K for address-bar
19
19
  * search. That is half the picture: ⌘⇧P / Ctrl+Shift+P is Firefox's private
20
20
  * window, so **both** candidates collide somewhere. ⌘K is also what Linear,
21
21
  * GitHub, Slack and Vercel trained people on, so leaving it unbound costs a
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/command-palette/command-palette-host.tsx"],"sourcesContent":["'use client';\n\nimport { NEW_COMMAND_PAGE } from '@entifix/authz';\nimport {\n type CommandOption,\n type CommandPage,\n type CommandSource,\n parseCommandTerm,\n ROOT_COMMAND_PAGE,\n} from '@entifix/core';\nimport {\n Button,\n CommandPalette,\n type CommandPaletteLabels,\n ConfirmDialog,\n useHotkey,\n useT,\n} from '@entifix/react-controls';\nimport { useRouter } from 'next/navigation';\nimport { useState } from 'react';\n\nimport type { NavSection } from '../back-office/nav.js';\nimport type { PaletteCommand } from './palette-command.js';\nimport { useCommandRecency } from './use-command-recency.js';\nimport { useCommandSource } from './use-command-source.js';\nimport { useNavSource } from './use-nav-source.js';\nimport { useRecordsSource } from './use-records-source.js';\nimport { useTabsSource } from './use-tabs-source.js';\nimport {\n type UseCaseCommandEntity,\n useUseCaseSources,\n} from './use-use-cases-source.js';\n\n/**\n * ⌘K **and** ⌘⇧P, and neither is load-bearing.\n *\n * #112 argued for ⌘⇧P alone because Chrome and Firefox claim ⌘K for address-bar\n * search. That is half the picture: ⌘⇧P / Ctrl+Shift+P is Firefox's private\n * window, so **both** candidates collide somewhere. ⌘K is also what Linear,\n * GitHub, Slack and Vercel trained people on, so leaving it unbound costs a\n * reflex.\n *\n * Binding both, plus a permanently visible trigger, is what makes the collision\n * a non-issue: whichever a given browser swallows, two other ways in remain.\n */\nconst HOTKEYS = [\n { key: 'k', mod: true },\n { key: 'p', mod: true, shift: true },\n] as const;\n\nexport interface CommandPaletteHostProps {\n /**\n * Deterministic commands, already permission-filtered and translated by the\n * server layout that has the principal.\n */\n readonly commands: readonly PaletteCommand[];\n /** The same resolved nav tree the sidebar renders. */\n readonly nav: readonly NavSection[];\n /**\n * Entities whose served `$metadata` contributes `unbound` verbs, with the\n * handler for each.\n *\n * ⚠️ Must be a **module-scope constant**: the sources are produced by a hook\n * per entity, so a fresh array each render changes the hook count. See\n * `useUseCaseSources`.\n */\n readonly useCaseEntities?: readonly UseCaseCommandEntity[];\n}\n\n/** A verb waiting on its confirmation. */\ninterface PendingConfirm {\n readonly option: CommandOption;\n readonly message: string;\n readonly tone: 'destructive' | 'neutral';\n}\n\nconst NO_ENTITIES: readonly UseCaseCommandEntity[] = [];\n\n/**\n * The back office's command palette: its trigger, its shortcuts, and everything\n * it can reach.\n *\n * This is the composition point. The control in `@entifix/react-controls` is\n * presentational and the sources are hooks; what lives here is the small set of\n * decisions neither can make — which sources a typed prefix asks for, what\n * selecting an option does, and when to ask first.\n *\n * **The grammar is for entry, the stack is for depth.** `>` narrows to commands\n * and actions, `#` to records, and anything else searches everything. Descending\n * into \"Nuevo…\" is a pushed page rather than a third prefix, because a prefix has\n * to be remembered and a list can be read.\n *\n * The whole thing is mounted in the chrome, so it is reachable from every route\n * the back office serves — including `/workspace`, which is where the open-tabs\n * group has anything in it.\n */\nexport function CommandPaletteHost({\n commands,\n nav,\n useCaseEntities = NO_ENTITIES,\n}: CommandPaletteHostProps) {\n const t = useT('shell');\n const router = useRouter();\n const recency = useCommandRecency();\n\n const [open, setOpen] = useState(false);\n const [rawTerm, setRawTerm] = useState('');\n const [stack, setStack] = useState<readonly string[]>([]);\n const [pending, setPending] = useState<PendingConfirm | null>(null);\n\n const { scope, term } = parseCommandTerm(rawTerm);\n\n // Every source runs on every render — the hook count may not depend on the\n // page or the prefix. What those decide is which sources are *shown*, which is\n // why each returns a whole `CommandSource` instead of being called\n // conditionally. So the root's commands and the create page's are two sources\n // rather than one that reads the stack.\n const commandSource = useCommandSource(commands, term, undefined);\n const createSource = useCommandSource(commands, term, NEW_COMMAND_PAGE);\n const useCaseSources = useUseCaseSources(useCaseEntities, term);\n const navSource = useNavSource(nav, term);\n const tabsSource = useTabsSource(term);\n const recordsSource = useRecordsSource(\n term,\n // A record fan-out is four authenticated requests per keystroke, so it is\n // spent only where its answer is rendered: not while closed, not while the\n // prefix says commands, and not on a pushed page, which shows its own\n // sources and nothing else.\n open && scope !== 'commands' && stack.length === 0,\n );\n\n const rank = (source: CommandSource): CommandSource => ({\n ...source,\n groups: source.groups.map(recency.rank),\n });\n\n const rootSources: CommandSource[] =\n scope === 'records'\n ? [recordsSource]\n : scope === 'commands'\n ? [commandSource, ...useCaseSources]\n : [\n commandSource,\n ...useCaseSources,\n navSource,\n tabsSource,\n recordsSource,\n ];\n\n const pages: CommandPage[] = [\n {\n id: ROOT_COMMAND_PAGE,\n placeholder: t('commandPalette.placeholder'),\n sources: rootSources.map(rank),\n },\n {\n id: NEW_COMMAND_PAGE,\n title: t('commandPalette.newTitle'),\n placeholder: t('commandPalette.newPlaceholder'),\n sources: [rank(createSource)],\n },\n ];\n\n const labels: CommandPaletteLabels = {\n title: t('commandPalette.title'),\n empty: t('commandPalette.empty'),\n loading: t('commandPalette.loading'),\n more: remaining => t('commandPalette.more', { count: remaining }),\n back: t('commandPalette.back'),\n };\n\n const close = () => {\n setOpen(false);\n setPending(null);\n };\n\n // Opening resets the term and the stack in the same handler that opens it,\n // rather than from an effect keyed on `open` — a fresh palette is a fact about\n // this event, not something to react to afterwards.\n const openPalette = () => {\n setRawTerm('');\n setStack([]);\n setPending(null);\n setOpen(true);\n };\n\n useHotkey(HOTKEYS, openPalette);\n\n const perform = (option: CommandOption) => {\n recency.remember(option.id);\n close();\n if (option.href !== undefined) {\n router.push(option.href);\n return;\n }\n // A rejected command is the handler's to report — it owns the request and\n // therefore the only context in which the failure means anything. Letting it\n // reject unhandled here would surface as an unhandled rejection instead.\n void option.run?.();\n };\n\n const select = (option: CommandOption) => {\n if (option.confirm !== undefined) {\n // The palette closes and the question stands alone. Leaving the list\n // behind a modal invites a second selection against a dialog that is\n // already about the first one.\n setOpen(false);\n setPending({\n option,\n message: option.confirm.message,\n tone: option.confirm.tone,\n });\n return;\n }\n perform(option);\n };\n\n return (\n <>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={openPalette}\n data-testid=\"command-palette-trigger\"\n >\n {t('commandPalette.open')}\n </Button>\n\n <CommandPalette\n open={open}\n onClose={close}\n pages={pages}\n stack={stack}\n onStackChange={setStack}\n term={rawTerm}\n onTermChange={setRawTerm}\n onSelect={select}\n labels={labels}\n />\n\n {pending !== null && (\n <ConfirmDialog\n open\n title={pending.option.label}\n message={pending.message}\n tone={pending.tone}\n onConfirm={() => {\n const option = pending.option;\n setPending(null);\n perform(option);\n }}\n onCancel={() => setPending(null)}\n />\n )}\n </>\n );\n}\n"],"names":["NEW_COMMAND_PAGE","parseCommandTerm","ROOT_COMMAND_PAGE","Button","CommandPalette","ConfirmDialog","useHotkey","useT","useRouter","useState","useCommandRecency","useCommandSource","useNavSource","useRecordsSource","useTabsSource","useUseCaseSources","HOTKEYS","key","mod","shift","NO_ENTITIES","CommandPaletteHost","commands","nav","useCaseEntities","t","router","recency","open","setOpen","rawTerm","setRawTerm","stack","setStack","pending","setPending","scope","term","commandSource","undefined","createSource","useCaseSources","navSource","tabsSource","recordsSource","length","rank","source","groups","map","rootSources","pages","id","placeholder","sources","title","labels","empty","loading","more","remaining","count","back","close","openPalette","perform","option","remember","href","push","run","select","confirm","message","tone","type","variant","size","onClick","data-testid","onClose","onStackChange","onTermChange","onSelect","label","onConfirm","onCancel"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/lib/command-palette/command-palette-host.tsx"],"sourcesContent":["'use client';\n\nimport { NEW_COMMAND_PAGE } from '@entifix/authz';\nimport {\n type CommandOption,\n type CommandPage,\n type CommandSource,\n parseCommandTerm,\n ROOT_COMMAND_PAGE,\n} from '@entifix/core';\nimport {\n Button,\n CommandPalette,\n type CommandPaletteLabels,\n ConfirmDialog,\n useHotkey,\n useT,\n} from '@entifix/react-controls';\nimport { useRouter } from 'next/navigation';\nimport { useState } from 'react';\n\nimport type { NavSection } from '../back-office/nav.js';\nimport type { PaletteCommand } from './palette-command.js';\nimport { useCommandRecency } from './use-command-recency.js';\nimport { useCommandSource } from './use-command-source.js';\nimport { useNavSource } from './use-nav-source.js';\nimport { useRecordsSource } from './use-records-source.js';\nimport { useTabsSource } from './use-tabs-source.js';\nimport {\n type UseCaseCommandEntity,\n useUseCaseSources,\n} from './use-use-cases-source.js';\n\n/**\n * ⌘K **and** ⌘⇧P, and neither is load-bearing.\n *\n * The case for ⌘⇧P alone was that Chrome and Firefox claim ⌘K for address-bar\n * search. That is half the picture: ⌘⇧P / Ctrl+Shift+P is Firefox's private\n * window, so **both** candidates collide somewhere. ⌘K is also what Linear,\n * GitHub, Slack and Vercel trained people on, so leaving it unbound costs a\n * reflex.\n *\n * Binding both, plus a permanently visible trigger, is what makes the collision\n * a non-issue: whichever a given browser swallows, two other ways in remain.\n */\nconst HOTKEYS = [\n { key: 'k', mod: true },\n { key: 'p', mod: true, shift: true },\n] as const;\n\nexport interface CommandPaletteHostProps {\n /**\n * Deterministic commands, already permission-filtered and translated by the\n * server layout that has the principal.\n */\n readonly commands: readonly PaletteCommand[];\n /** The same resolved nav tree the sidebar renders. */\n readonly nav: readonly NavSection[];\n /**\n * Entities whose served `$metadata` contributes `unbound` verbs, with the\n * handler for each.\n *\n * ⚠️ Must be a **module-scope constant**: the sources are produced by a hook\n * per entity, so a fresh array each render changes the hook count. See\n * `useUseCaseSources`.\n */\n readonly useCaseEntities?: readonly UseCaseCommandEntity[];\n}\n\n/** A verb waiting on its confirmation. */\ninterface PendingConfirm {\n readonly option: CommandOption;\n readonly message: string;\n readonly tone: 'destructive' | 'neutral';\n}\n\nconst NO_ENTITIES: readonly UseCaseCommandEntity[] = [];\n\n/**\n * The back office's command palette: its trigger, its shortcuts, and everything\n * it can reach.\n *\n * This is the composition point. The control in `@entifix/react-controls` is\n * presentational and the sources are hooks; what lives here is the small set of\n * decisions neither can make — which sources a typed prefix asks for, what\n * selecting an option does, and when to ask first.\n *\n * **The grammar is for entry, the stack is for depth.** `>` narrows to commands\n * and actions, `#` to records, and anything else searches everything. Descending\n * into \"Nuevo…\" is a pushed page rather than a third prefix, because a prefix has\n * to be remembered and a list can be read.\n *\n * The whole thing is mounted in the chrome, so it is reachable from every route\n * the back office serves — including `/workspace`, which is where the open-tabs\n * group has anything in it.\n */\nexport function CommandPaletteHost({\n commands,\n nav,\n useCaseEntities = NO_ENTITIES,\n}: CommandPaletteHostProps) {\n const t = useT('shell');\n const router = useRouter();\n const recency = useCommandRecency();\n\n const [open, setOpen] = useState(false);\n const [rawTerm, setRawTerm] = useState('');\n const [stack, setStack] = useState<readonly string[]>([]);\n const [pending, setPending] = useState<PendingConfirm | null>(null);\n\n const { scope, term } = parseCommandTerm(rawTerm);\n\n // Every source runs on every render — the hook count may not depend on the\n // page or the prefix. What those decide is which sources are *shown*, which is\n // why each returns a whole `CommandSource` instead of being called\n // conditionally. So the root's commands and the create page's are two sources\n // rather than one that reads the stack.\n const commandSource = useCommandSource(commands, term, undefined);\n const createSource = useCommandSource(commands, term, NEW_COMMAND_PAGE);\n const useCaseSources = useUseCaseSources(useCaseEntities, term);\n const navSource = useNavSource(nav, term);\n const tabsSource = useTabsSource(term);\n const recordsSource = useRecordsSource(\n term,\n // A record fan-out is four authenticated requests per keystroke, so it is\n // spent only where its answer is rendered: not while closed, not while the\n // prefix says commands, and not on a pushed page, which shows its own\n // sources and nothing else.\n open && scope !== 'commands' && stack.length === 0,\n );\n\n const rank = (source: CommandSource): CommandSource => ({\n ...source,\n groups: source.groups.map(recency.rank),\n });\n\n const rootSources: CommandSource[] =\n scope === 'records'\n ? [recordsSource]\n : scope === 'commands'\n ? [commandSource, ...useCaseSources]\n : [\n commandSource,\n ...useCaseSources,\n navSource,\n tabsSource,\n recordsSource,\n ];\n\n const pages: CommandPage[] = [\n {\n id: ROOT_COMMAND_PAGE,\n placeholder: t('commandPalette.placeholder'),\n sources: rootSources.map(rank),\n },\n {\n id: NEW_COMMAND_PAGE,\n title: t('commandPalette.newTitle'),\n placeholder: t('commandPalette.newPlaceholder'),\n sources: [rank(createSource)],\n },\n ];\n\n const labels: CommandPaletteLabels = {\n title: t('commandPalette.title'),\n empty: t('commandPalette.empty'),\n loading: t('commandPalette.loading'),\n more: remaining => t('commandPalette.more', { count: remaining }),\n back: t('commandPalette.back'),\n };\n\n const close = () => {\n setOpen(false);\n setPending(null);\n };\n\n // Opening resets the term and the stack in the same handler that opens it,\n // rather than from an effect keyed on `open` — a fresh palette is a fact about\n // this event, not something to react to afterwards.\n const openPalette = () => {\n setRawTerm('');\n setStack([]);\n setPending(null);\n setOpen(true);\n };\n\n useHotkey(HOTKEYS, openPalette);\n\n const perform = (option: CommandOption) => {\n recency.remember(option.id);\n close();\n if (option.href !== undefined) {\n router.push(option.href);\n return;\n }\n // A rejected command is the handler's to report — it owns the request and\n // therefore the only context in which the failure means anything. Letting it\n // reject unhandled here would surface as an unhandled rejection instead.\n void option.run?.();\n };\n\n const select = (option: CommandOption) => {\n if (option.confirm !== undefined) {\n // The palette closes and the question stands alone. Leaving the list\n // behind a modal invites a second selection against a dialog that is\n // already about the first one.\n setOpen(false);\n setPending({\n option,\n message: option.confirm.message,\n tone: option.confirm.tone,\n });\n return;\n }\n perform(option);\n };\n\n return (\n <>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={openPalette}\n data-testid=\"command-palette-trigger\"\n >\n {t('commandPalette.open')}\n </Button>\n\n <CommandPalette\n open={open}\n onClose={close}\n pages={pages}\n stack={stack}\n onStackChange={setStack}\n term={rawTerm}\n onTermChange={setRawTerm}\n onSelect={select}\n labels={labels}\n />\n\n {pending !== null && (\n <ConfirmDialog\n open\n title={pending.option.label}\n message={pending.message}\n tone={pending.tone}\n onConfirm={() => {\n const option = pending.option;\n setPending(null);\n perform(option);\n }}\n onCancel={() => setPending(null)}\n />\n )}\n </>\n );\n}\n"],"names":["NEW_COMMAND_PAGE","parseCommandTerm","ROOT_COMMAND_PAGE","Button","CommandPalette","ConfirmDialog","useHotkey","useT","useRouter","useState","useCommandRecency","useCommandSource","useNavSource","useRecordsSource","useTabsSource","useUseCaseSources","HOTKEYS","key","mod","shift","NO_ENTITIES","CommandPaletteHost","commands","nav","useCaseEntities","t","router","recency","open","setOpen","rawTerm","setRawTerm","stack","setStack","pending","setPending","scope","term","commandSource","undefined","createSource","useCaseSources","navSource","tabsSource","recordsSource","length","rank","source","groups","map","rootSources","pages","id","placeholder","sources","title","labels","empty","loading","more","remaining","count","back","close","openPalette","perform","option","remember","href","push","run","select","confirm","message","tone","type","variant","size","onClick","data-testid","onClose","onStackChange","onTermChange","onSelect","label","onConfirm","onCancel"],"mappings":"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"}
@@ -9,7 +9,7 @@ export interface CommandRecency {
9
9
  * Which commands this person reaches for, and in what order to offer them.
10
10
  *
11
11
  * **Recency reorders within a group and never across groups.** Group order is
12
- * the ranking (ADR 0040), and it is fixed precisely so an operator learns where
12
+ * the ranking, and it is fixed precisely so an operator learns where
13
13
  * a kind of result lands; letting use move a whole group would take that away
14
14
  * for a convenience nobody asked for. Inside a group there is no such contract,
15
15
  * so the last thing you ran comes first.
@@ -15,7 +15,7 @@ import { useCallback, useMemo } from 'react';
15
15
  * Which commands this person reaches for, and in what order to offer them.
16
16
  *
17
17
  * **Recency reorders within a group and never across groups.** Group order is
18
- * the ranking (ADR 0040), and it is fixed precisely so an operator learns where
18
+ * the ranking, and it is fixed precisely so an operator learns where
19
19
  * a kind of result lands; letting use move a whole group would take that away
20
20
  * for a convenience nobody asked for. Inside a group there is no such contract,
21
21
  * so the last thing you ran comes first.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/command-palette/use-command-recency.ts"],"sourcesContent":["'use client';\n\nimport type { CommandGroup } from '@entifix/core';\nimport { useUiPreference } from '@entifix/react-controls';\nimport { useCallback, useMemo } from 'react';\n\n/** `namespace:` is prepended by the store → `entifix-ui:back-office:command-recency`. */\nconst RECENCY_PREF_KEY = 'back-office:command-recency';\n\n/**\n * How many commands are remembered.\n *\n * A cap rather than an unbounded map: the store writes the whole record on every\n * run, and a record that only ever grows is a preference that gets slower for\n * the rest of the person's account life. Twenty is far more than the number of\n * commands anyone reaches for.\n */\nconst RECENCY_LIMIT = 20;\n\nexport interface CommandRecency {\n /** Reorder a group's options, most recently used first. */\n readonly rank: (group: CommandGroup) => CommandGroup;\n /** Remember that this command was just run. */\n readonly remember: (id: string) => void;\n}\n\n/**\n * Which commands this person reaches for, and in what order to offer them.\n *\n * **Recency reorders within a group and never across groups.** Group order is\n * the ranking (ADR 0040), and it is fixed precisely so an operator learns where\n * a kind of result lands; letting use move a whole group would take that away\n * for a convenience nobody asked for. Inside a group there is no such contract,\n * so the last thing you ran comes first.\n *\n * Stored as **one keyed record**, not a key per command — the same reasoning\n * `back-office:nav-collapsed-groups` records: `useUiPreference` resolves after\n * mount, so N keys are N first-paint transitions while one read settles the\n * whole palette at once.\n *\n * A record group is deliberately left alone: its options are records, whose ids\n * are primary keys, so remembering them would build a list of which records this\n * person opened inside a UI preference. Only commands and destinations are\n * ranked.\n */\nexport function useCommandRecency(): CommandRecency {\n const { value, setValue } = useUiPreference<Record<string, number>>(\n RECENCY_PREF_KEY,\n {},\n );\n\n const rank = useCallback(\n (group: CommandGroup): CommandGroup => {\n if (group.key.startsWith('record:')) return group;\n\n const options = [...group.options].sort(\n (left, right) => (value[right.id] ?? 0) - (value[left.id] ?? 0),\n );\n return { ...group, options };\n },\n [value],\n );\n\n const remember = useCallback(\n (id: string) => {\n const next = { ...value, [id]: Date.now() };\n const kept = Object.entries(next)\n .sort(([, left], [, right]) => right - left)\n .slice(0, RECENCY_LIMIT);\n setValue(Object.fromEntries(kept));\n },\n [value, setValue],\n );\n\n return useMemo(() => ({ rank, remember }), [rank, remember]);\n}\n"],"names":["useUiPreference","useCallback","useMemo","RECENCY_PREF_KEY","RECENCY_LIMIT","useCommandRecency","value","setValue","rank","group","key","startsWith","options","sort","left","right","id","remember","next","Date","now","kept","Object","entries","slice","fromEntries"],"mappings":"AAAA;;AAGA,SAASA,eAAe,QAAQ,0BAA0B;AAC1D,SAASC,WAAW,EAAEC,OAAO,QAAQ,QAAQ;AAE7C,uFAAuF,GACvF,MAAMC,mBAAmB;AAEzB;;;;;;;CAOC,GACD,MAAMC,gBAAgB;AAStB;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC;IACd,MAAM,EAAEC,KAAK,EAAEC,QAAQ,EAAE,GAAGP,gBAC1BG,kBACA,CAAC;IAGH,MAAMK,OAAOP,YACX,CAACQ;QACC,IAAIA,MAAMC,GAAG,CAACC,UAAU,CAAC,YAAY,OAAOF;QAE5C,MAAMG,UAAU;eAAIH,MAAMG,OAAO;SAAC,CAACC,IAAI,CACrC,CAACC,MAAMC;gBAAWT,iBAAyBA;mBAA1B,EAACA,kBAAAA,KAAK,CAACS,MAAMC,EAAE,CAAC,YAAfV,kBAAmB,OAAMA,iBAAAA,KAAK,CAACQ,KAAKE,EAAE,CAAC,YAAdV,iBAAkB;;QAE/D,OAAO,aAAKG;YAAOG;;IACrB,GACA;QAACN;KAAM;IAGT,MAAMW,WAAWhB,YACf,CAACe;QACC,MAAME,OAAO,aAAKZ;YAAO,CAACU,GAAG,EAAEG,KAAKC,GAAG;;QACvC,MAAMC,OAAOC,OAAOC,OAAO,CAACL,MACzBL,IAAI,CAAC,CAAC,GAAGC,KAAK,EAAE,GAAGC,MAAM,GAAKA,QAAQD,MACtCU,KAAK,CAAC,GAAGpB;QACZG,SAASe,OAAOG,WAAW,CAACJ;IAC9B,GACA;QAACf;QAAOC;KAAS;IAGnB,OAAOL,QAAQ,IAAO,CAAA;YAAEM;YAAMS;QAAS,CAAA,GAAI;QAACT;QAAMS;KAAS;AAC7D"}
1
+ {"version":3,"sources":["../../../src/lib/command-palette/use-command-recency.ts"],"sourcesContent":["'use client';\n\nimport type { CommandGroup } from '@entifix/core';\nimport { useUiPreference } from '@entifix/react-controls';\nimport { useCallback, useMemo } from 'react';\n\n/** `namespace:` is prepended by the store → `entifix-ui:back-office:command-recency`. */\nconst RECENCY_PREF_KEY = 'back-office:command-recency';\n\n/**\n * How many commands are remembered.\n *\n * A cap rather than an unbounded map: the store writes the whole record on every\n * run, and a record that only ever grows is a preference that gets slower for\n * the rest of the person's account life. Twenty is far more than the number of\n * commands anyone reaches for.\n */\nconst RECENCY_LIMIT = 20;\n\nexport interface CommandRecency {\n /** Reorder a group's options, most recently used first. */\n readonly rank: (group: CommandGroup) => CommandGroup;\n /** Remember that this command was just run. */\n readonly remember: (id: string) => void;\n}\n\n/**\n * Which commands this person reaches for, and in what order to offer them.\n *\n * **Recency reorders within a group and never across groups.** Group order is\n * the ranking, and it is fixed precisely so an operator learns where\n * a kind of result lands; letting use move a whole group would take that away\n * for a convenience nobody asked for. Inside a group there is no such contract,\n * so the last thing you ran comes first.\n *\n * Stored as **one keyed record**, not a key per command — the same reasoning\n * `back-office:nav-collapsed-groups` records: `useUiPreference` resolves after\n * mount, so N keys are N first-paint transitions while one read settles the\n * whole palette at once.\n *\n * A record group is deliberately left alone: its options are records, whose ids\n * are primary keys, so remembering them would build a list of which records this\n * person opened inside a UI preference. Only commands and destinations are\n * ranked.\n */\nexport function useCommandRecency(): CommandRecency {\n const { value, setValue } = useUiPreference<Record<string, number>>(\n RECENCY_PREF_KEY,\n {},\n );\n\n const rank = useCallback(\n (group: CommandGroup): CommandGroup => {\n if (group.key.startsWith('record:')) return group;\n\n const options = [...group.options].sort(\n (left, right) => (value[right.id] ?? 0) - (value[left.id] ?? 0),\n );\n return { ...group, options };\n },\n [value],\n );\n\n const remember = useCallback(\n (id: string) => {\n const next = { ...value, [id]: Date.now() };\n const kept = Object.entries(next)\n .sort(([, left], [, right]) => right - left)\n .slice(0, RECENCY_LIMIT);\n setValue(Object.fromEntries(kept));\n },\n [value, setValue],\n );\n\n return useMemo(() => ({ rank, remember }), [rank, remember]);\n}\n"],"names":["useUiPreference","useCallback","useMemo","RECENCY_PREF_KEY","RECENCY_LIMIT","useCommandRecency","value","setValue","rank","group","key","startsWith","options","sort","left","right","id","remember","next","Date","now","kept","Object","entries","slice","fromEntries"],"mappings":"AAAA;;AAGA,SAASA,eAAe,QAAQ,0BAA0B;AAC1D,SAASC,WAAW,EAAEC,OAAO,QAAQ,QAAQ;AAE7C,uFAAuF,GACvF,MAAMC,mBAAmB;AAEzB;;;;;;;CAOC,GACD,MAAMC,gBAAgB;AAStB;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC;IACd,MAAM,EAAEC,KAAK,EAAEC,QAAQ,EAAE,GAAGP,gBAC1BG,kBACA,CAAC;IAGH,MAAMK,OAAOP,YACX,CAACQ;QACC,IAAIA,MAAMC,GAAG,CAACC,UAAU,CAAC,YAAY,OAAOF;QAE5C,MAAMG,UAAU;eAAIH,MAAMG,OAAO;SAAC,CAACC,IAAI,CACrC,CAACC,MAAMC;gBAAWT,iBAAyBA;mBAA1B,EAACA,kBAAAA,KAAK,CAACS,MAAMC,EAAE,CAAC,YAAfV,kBAAmB,OAAMA,iBAAAA,KAAK,CAACQ,KAAKE,EAAE,CAAC,YAAdV,iBAAkB;;QAE/D,OAAO,aAAKG;YAAOG;;IACrB,GACA;QAACN;KAAM;IAGT,MAAMW,WAAWhB,YACf,CAACe;QACC,MAAME,OAAO,aAAKZ;YAAO,CAACU,GAAG,EAAEG,KAAKC,GAAG;;QACvC,MAAMC,OAAOC,OAAOC,OAAO,CAACL,MACzBL,IAAI,CAAC,CAAC,GAAGC,KAAK,EAAE,GAAGC,MAAM,GAAKA,QAAQD,MACtCU,KAAK,CAAC,GAAGpB;QACZG,SAASe,OAAOG,WAAW,CAACJ;IAC9B,GACA;QAACf;QAAOC;KAAS;IAGnB,OAAOL,QAAQ,IAAO,CAAA;YAAEM;YAAMS;QAAS,CAAA,GAAI;QAACT;QAAMS;KAAS;AAC7D"}
@@ -4,9 +4,10 @@ import type { NavSection } from '../back-office/nav.js';
4
4
  * Every destination this caller may reach, as one group.
5
5
  *
6
6
  * The nav is already the answer to "what may this person see": `visibleNav`
7
- * applies both of ADR 0037's ceilings server-side, and what reaches here is the
7
+ * applies both ceilings — the role and the organization's provisioning —
8
+ * server-side, and what reaches here is the
8
9
  * resolved, filtered tree the sidebar renders. So the palette holds no second
9
- * projection of navigation — the thing ADR 0037 deleted `GET /api/menu` for.
10
+ * projection of navigation, which is what a separate menu endpoint would be.
10
11
  *
11
12
  * A destination that can also be a workspace tab contributes a **second**
12
13
  * option rather than a modifier on the first. The palette runs on Enter and has
@@ -15,7 +16,7 @@ import type { NavSection } from '../back-office/nav.js';
15
16
  *
16
17
  * The section's screen type is the option's hint, so Definiciones and
17
18
  * Operaciones stay distinguishable in a flat list — the sidebar's top tier
18
- * (ADR 0033) surviving the flattening rather than being dropped by it.
19
+ * surviving the flattening rather than being dropped by it.
19
20
  */
20
21
  export declare function useNavSource(sections: readonly NavSection[], term: string): CommandSource;
21
22
  //# sourceMappingURL=use-nav-source.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-nav-source.d.ts","sourceRoot":"","sources":["../../../src/lib/command-palette/use-nav-source.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAOxD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,SAAS,UAAU,EAAE,EAC/B,IAAI,EAAE,MAAM,GACX,aAAa,CAkDf"}
1
+ {"version":3,"file":"use-nav-source.d.ts","sourceRoot":"","sources":["../../../src/lib/command-palette/use-nav-source.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAOxD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,SAAS,UAAU,EAAE,EAC/B,IAAI,EAAE,MAAM,GACX,aAAa,CAkDf"}
@@ -10,9 +10,10 @@ import { useLocaleHref } from '../i18n/index.js';
10
10
  * Every destination this caller may reach, as one group.
11
11
  *
12
12
  * The nav is already the answer to "what may this person see": `visibleNav`
13
- * applies both of ADR 0037's ceilings server-side, and what reaches here is the
13
+ * applies both ceilings — the role and the organization's provisioning —
14
+ * server-side, and what reaches here is the
14
15
  * resolved, filtered tree the sidebar renders. So the palette holds no second
15
- * projection of navigation — the thing ADR 0037 deleted `GET /api/menu` for.
16
+ * projection of navigation, which is what a separate menu endpoint would be.
16
17
  *
17
18
  * A destination that can also be a workspace tab contributes a **second**
18
19
  * option rather than a modifier on the first. The palette runs on Enter and has
@@ -21,7 +22,7 @@ import { useLocaleHref } from '../i18n/index.js';
21
22
  *
22
23
  * The section's screen type is the option's hint, so Definiciones and
23
24
  * Operaciones stay distinguishable in a flat list — the sidebar's top tier
24
- * (ADR 0033) surviving the flattening rather than being dropped by it.
25
+ * surviving the flattening rather than being dropped by it.
25
26
  */ export function useNavSource(sections, term) {
26
27
  const t = useT('shell');
27
28
  const translateKey = useTranslateKey();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/command-palette/use-nav-source.ts"],"sourcesContent":["'use client';\n\nimport { SCREEN_TYPE_LABEL_KEYS } from '@entifix/authz';\nimport {\n type CommandOption,\n type CommandSource,\n matchesCommand,\n} from '@entifix/core';\nimport { useT, useTranslateKey } from '@entifix/react-controls';\nimport { useMemo } from 'react';\n\nimport type { NavSection } from '../back-office/nav.js';\nimport { useLocaleHref } from '../i18n/index.js';\n\n/** The workspace deep link a nav item's `workspace` address resolves to. */\nconst workspaceHref = (workspace: string) =>\n `/workspace?tab=${encodeURIComponent(workspace)}`;\n\n/**\n * Every destination this caller may reach, as one group.\n *\n * The nav is already the answer to \"what may this person see\": `visibleNav`\n * applies both of ADR 0037's ceilings server-side, and what reaches here is the\n * resolved, filtered tree the sidebar renders. So the palette holds no second\n * projection of navigation — the thing ADR 0037 deleted `GET /api/menu` for.\n *\n * A destination that can also be a workspace tab contributes a **second**\n * option rather than a modifier on the first. The palette runs on Enter and has\n * no place to hang \"hold ⌥ to open as a tab\", and a hidden modifier is exactly\n * the undiscoverable affordance the sidebar's own workspace link was fixed for.\n *\n * The section's screen type is the option's hint, so Definiciones and\n * Operaciones stay distinguishable in a flat list — the sidebar's top tier\n * (ADR 0033) surviving the flattening rather than being dropped by it.\n */\nexport function useNavSource(\n sections: readonly NavSection[],\n term: string,\n): CommandSource {\n const t = useT('shell');\n const translateKey = useTranslateKey();\n const withLocale = useLocaleHref();\n\n return useMemo(() => {\n const options: CommandOption[] = [];\n\n for (const section of sections) {\n // Only the screen type. Falling back to the section title here read as\n // \"Perfil · Cuenta · Cuenta\" on the untyped account section, because the\n // title is already the sublabel — one fact, printed twice.\n const hint =\n section.type === undefined\n ? undefined\n : translateKey(SCREEN_TYPE_LABEL_KEYS[section.type]);\n\n for (const item of section.items) {\n options.push({\n id: `nav:${item.href}`,\n label: item.label,\n href: withLocale(item.href),\n ...(hint === undefined ? {} : { hint }),\n ...(section.title === undefined ? {} : { sublabel: section.title }),\n });\n\n if (item.workspace !== undefined) {\n options.push({\n id: `nav:workspace:${item.workspace}`,\n label: item.label,\n href: withLocale(workspaceHref(item.workspace)),\n hint: t('commandPalette.openInWorkspaceHint'),\n ...(section.title === undefined ? {} : { sublabel: section.title }),\n });\n }\n }\n }\n\n return {\n key: 'navigation',\n groups: [\n {\n key: 'navigation',\n label: t('commandPalette.groups.navigation'),\n options: options.filter(option => matchesCommand(term, option)),\n isLoading: false,\n },\n ],\n } satisfies CommandSource;\n }, [sections, term, t, translateKey, withLocale]);\n}\n"],"names":["SCREEN_TYPE_LABEL_KEYS","matchesCommand","useT","useTranslateKey","useMemo","useLocaleHref","workspaceHref","workspace","encodeURIComponent","useNavSource","sections","term","t","translateKey","withLocale","options","section","hint","type","undefined","item","items","push","id","href","label","title","sublabel","key","groups","filter","option","isLoading"],"mappings":"AAAA;;AAEA,SAASA,sBAAsB,QAAQ,iBAAiB;AACxD,SAGEC,cAAc,QACT,gBAAgB;AACvB,SAASC,IAAI,EAAEC,eAAe,QAAQ,0BAA0B;AAChE,SAASC,OAAO,QAAQ,QAAQ;AAGhC,SAASC,aAAa,QAAQ,mBAAmB;AAEjD,0EAA0E,GAC1E,MAAMC,gBAAgB,CAACC,YACrB,CAAC,eAAe,EAAEC,mBAAmBD,YAAY;AAEnD;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASE,aACdC,QAA+B,EAC/BC,IAAY;IAEZ,MAAMC,IAAIV,KAAK;IACf,MAAMW,eAAeV;IACrB,MAAMW,aAAaT;IAEnB,OAAOD,QAAQ;QACb,MAAMW,UAA2B,EAAE;QAEnC,KAAK,MAAMC,WAAWN,SAAU;YAC9B,uEAAuE;YACvE,yEAAyE;YACzE,2DAA2D;YAC3D,MAAMO,OACJD,QAAQE,IAAI,KAAKC,YACbA,YACAN,aAAab,sBAAsB,CAACgB,QAAQE,IAAI,CAAC;YAEvD,KAAK,MAAME,QAAQJ,QAAQK,KAAK,CAAE;gBAChCN,QAAQO,IAAI,CAAC;oBACXC,IAAI,CAAC,IAAI,EAAEH,KAAKI,IAAI,EAAE;oBACtBC,OAAOL,KAAKK,KAAK;oBACjBD,MAAMV,WAAWM,KAAKI,IAAI;mBACtBP,SAASE,YAAY,CAAC,IAAI;oBAAEF;gBAAK,GACjCD,QAAQU,KAAK,KAAKP,YAAY,CAAC,IAAI;oBAAEQ,UAAUX,QAAQU,KAAK;gBAAC;gBAGnE,IAAIN,KAAKb,SAAS,KAAKY,WAAW;oBAChCJ,QAAQO,IAAI,CAAC;wBACXC,IAAI,CAAC,cAAc,EAAEH,KAAKb,SAAS,EAAE;wBACrCkB,OAAOL,KAAKK,KAAK;wBACjBD,MAAMV,WAAWR,cAAcc,KAAKb,SAAS;wBAC7CU,MAAML,EAAE;uBACJI,QAAQU,KAAK,KAAKP,YAAY,CAAC,IAAI;wBAAEQ,UAAUX,QAAQU,KAAK;oBAAC;gBAErE;YACF;QACF;QAEA,OAAO;YACLE,KAAK;YACLC,QAAQ;gBACN;oBACED,KAAK;oBACLH,OAAOb,EAAE;oBACTG,SAASA,QAAQe,MAAM,CAACC,CAAAA,SAAU9B,eAAeU,MAAMoB;oBACvDC,WAAW;gBACb;aACD;QACH;IACF,GAAG;QAACtB;QAAUC;QAAMC;QAAGC;QAAcC;KAAW;AAClD"}
1
+ {"version":3,"sources":["../../../src/lib/command-palette/use-nav-source.ts"],"sourcesContent":["'use client';\n\nimport { SCREEN_TYPE_LABEL_KEYS } from '@entifix/authz';\nimport {\n type CommandOption,\n type CommandSource,\n matchesCommand,\n} from '@entifix/core';\nimport { useT, useTranslateKey } from '@entifix/react-controls';\nimport { useMemo } from 'react';\n\nimport type { NavSection } from '../back-office/nav.js';\nimport { useLocaleHref } from '../i18n/index.js';\n\n/** The workspace deep link a nav item's `workspace` address resolves to. */\nconst workspaceHref = (workspace: string) =>\n `/workspace?tab=${encodeURIComponent(workspace)}`;\n\n/**\n * Every destination this caller may reach, as one group.\n *\n * The nav is already the answer to \"what may this person see\": `visibleNav`\n * applies both ceilings — the role and the organization's provisioning —\n * server-side, and what reaches here is the\n * resolved, filtered tree the sidebar renders. So the palette holds no second\n * projection of navigation, which is what a separate menu endpoint would be.\n *\n * A destination that can also be a workspace tab contributes a **second**\n * option rather than a modifier on the first. The palette runs on Enter and has\n * no place to hang \"hold ⌥ to open as a tab\", and a hidden modifier is exactly\n * the undiscoverable affordance the sidebar's own workspace link was fixed for.\n *\n * The section's screen type is the option's hint, so Definiciones and\n * Operaciones stay distinguishable in a flat list — the sidebar's top tier\n * surviving the flattening rather than being dropped by it.\n */\nexport function useNavSource(\n sections: readonly NavSection[],\n term: string,\n): CommandSource {\n const t = useT('shell');\n const translateKey = useTranslateKey();\n const withLocale = useLocaleHref();\n\n return useMemo(() => {\n const options: CommandOption[] = [];\n\n for (const section of sections) {\n // Only the screen type. Falling back to the section title here read as\n // \"Perfil · Cuenta · Cuenta\" on the untyped account section, because the\n // title is already the sublabel — one fact, printed twice.\n const hint =\n section.type === undefined\n ? undefined\n : translateKey(SCREEN_TYPE_LABEL_KEYS[section.type]);\n\n for (const item of section.items) {\n options.push({\n id: `nav:${item.href}`,\n label: item.label,\n href: withLocale(item.href),\n ...(hint === undefined ? {} : { hint }),\n ...(section.title === undefined ? {} : { sublabel: section.title }),\n });\n\n if (item.workspace !== undefined) {\n options.push({\n id: `nav:workspace:${item.workspace}`,\n label: item.label,\n href: withLocale(workspaceHref(item.workspace)),\n hint: t('commandPalette.openInWorkspaceHint'),\n ...(section.title === undefined ? {} : { sublabel: section.title }),\n });\n }\n }\n }\n\n return {\n key: 'navigation',\n groups: [\n {\n key: 'navigation',\n label: t('commandPalette.groups.navigation'),\n options: options.filter(option => matchesCommand(term, option)),\n isLoading: false,\n },\n ],\n } satisfies CommandSource;\n }, [sections, term, t, translateKey, withLocale]);\n}\n"],"names":["SCREEN_TYPE_LABEL_KEYS","matchesCommand","useT","useTranslateKey","useMemo","useLocaleHref","workspaceHref","workspace","encodeURIComponent","useNavSource","sections","term","t","translateKey","withLocale","options","section","hint","type","undefined","item","items","push","id","href","label","title","sublabel","key","groups","filter","option","isLoading"],"mappings":"AAAA;;AAEA,SAASA,sBAAsB,QAAQ,iBAAiB;AACxD,SAGEC,cAAc,QACT,gBAAgB;AACvB,SAASC,IAAI,EAAEC,eAAe,QAAQ,0BAA0B;AAChE,SAASC,OAAO,QAAQ,QAAQ;AAGhC,SAASC,aAAa,QAAQ,mBAAmB;AAEjD,0EAA0E,GAC1E,MAAMC,gBAAgB,CAACC,YACrB,CAAC,eAAe,EAAEC,mBAAmBD,YAAY;AAEnD;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASE,aACdC,QAA+B,EAC/BC,IAAY;IAEZ,MAAMC,IAAIV,KAAK;IACf,MAAMW,eAAeV;IACrB,MAAMW,aAAaT;IAEnB,OAAOD,QAAQ;QACb,MAAMW,UAA2B,EAAE;QAEnC,KAAK,MAAMC,WAAWN,SAAU;YAC9B,uEAAuE;YACvE,yEAAyE;YACzE,2DAA2D;YAC3D,MAAMO,OACJD,QAAQE,IAAI,KAAKC,YACbA,YACAN,aAAab,sBAAsB,CAACgB,QAAQE,IAAI,CAAC;YAEvD,KAAK,MAAME,QAAQJ,QAAQK,KAAK,CAAE;gBAChCN,QAAQO,IAAI,CAAC;oBACXC,IAAI,CAAC,IAAI,EAAEH,KAAKI,IAAI,EAAE;oBACtBC,OAAOL,KAAKK,KAAK;oBACjBD,MAAMV,WAAWM,KAAKI,IAAI;mBACtBP,SAASE,YAAY,CAAC,IAAI;oBAAEF;gBAAK,GACjCD,QAAQU,KAAK,KAAKP,YAAY,CAAC,IAAI;oBAAEQ,UAAUX,QAAQU,KAAK;gBAAC;gBAGnE,IAAIN,KAAKb,SAAS,KAAKY,WAAW;oBAChCJ,QAAQO,IAAI,CAAC;wBACXC,IAAI,CAAC,cAAc,EAAEH,KAAKb,SAAS,EAAE;wBACrCkB,OAAOL,KAAKK,KAAK;wBACjBD,MAAMV,WAAWR,cAAcc,KAAKb,SAAS;wBAC7CU,MAAML,EAAE;uBACJI,QAAQU,KAAK,KAAKP,YAAY,CAAC,IAAI;wBAAEQ,UAAUX,QAAQU,KAAK;oBAAC;gBAErE;YACF;QACF;QAEA,OAAO;YACLE,KAAK;YACLC,QAAQ;gBACN;oBACED,KAAK;oBACLH,OAAOb,EAAE;oBACTG,SAASA,QAAQe,MAAM,CAACC,CAAAA,SAAU9B,eAAeU,MAAMoB;oBACvDC,WAAW;gBACb;aACD;QACH;IACF,GAAG;QAACtB;QAAUC;QAAMC;QAAGC;QAAcC;KAAW;AAClD"}
@@ -12,7 +12,7 @@ import type { CommandSource } from '@entifix/core';
12
12
  * **No index, ever.** Every option here came back from `/api/search` for this
13
13
  * principal and this organization, on this keystroke. A prefetched client index
14
14
  * is the shape that surfaces another organization's record the first time a
15
- * session's scope moves under it, and ADR 0040 forecloses it permanently.
15
+ * session's scope moves under it, so there is none, ever.
16
16
  *
17
17
  * Below the two-character floor the group is present and **says why** rather
18
18
  * than being empty or absent: empty would claim nothing matched a search that
@@ -8,7 +8,7 @@ import { RECORD_SEARCH_DEBOUNCE_MS, RECORD_SEARCH_MIN_TERM } from './palette-com
8
8
  /**
9
9
  * Which reasons are the caller's ordinary state, and which mean something broke.
10
10
  *
11
- * The split is ADR 0040's, restated as severity because that is what the palette
11
+ * The split is the record search's, restated as severity because that is what the palette
12
12
  * renders. An operator holds no membership, so every tenant-plane source answers
13
13
  * `409` on every keystroke — painting that as an outage teaches them to ignore
14
14
  * the row entirely, and then the `timeout` that does matter goes unread too.
@@ -39,7 +39,7 @@ const NOTHING_YET = {
39
39
  * **No index, ever.** Every option here came back from `/api/search` for this
40
40
  * principal and this organization, on this keystroke. A prefetched client index
41
41
  * is the shape that surfaces another organization's record the first time a
42
- * session's scope moves under it, and ADR 0040 forecloses it permanently.
42
+ * session's scope moves under it, so there is none, ever.
43
43
  *
44
44
  * Below the two-character floor the group is present and **says why** rather
45
45
  * than being empty or absent: empty would claim nothing matched a search that
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/command-palette/use-records-source.ts"],"sourcesContent":["'use client';\n\nimport type {\n CommandGroup,\n CommandSource,\n CommandUnavailableSeverity,\n} from '@entifix/core';\nimport { useT, useTranslateKey } from '@entifix/react-controls';\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { useLocaleHref } from '../i18n/index.js';\nimport type {\n RecordSearchResponse,\n RecordSearchUnavailableReason,\n} from '../search/record-search.types.js';\nimport { searchRecords } from '../search/search-records.js';\nimport {\n RECORD_SEARCH_DEBOUNCE_MS,\n RECORD_SEARCH_MIN_TERM,\n} from './palette-command.js';\n\n/**\n * Which reasons are the caller's ordinary state, and which mean something broke.\n *\n * The split is ADR 0040's, restated as severity because that is what the palette\n * renders. An operator holds no membership, so every tenant-plane source answers\n * `409` on every keystroke — painting that as an outage teaches them to ignore\n * the row entirely, and then the `timeout` that does matter goes unread too.\n */\nconst SEVERITY: Readonly<\n Record<RecordSearchUnavailableReason, CommandUnavailableSeverity>\n> = {\n forbidden: 'scope',\n noActiveOrganization: 'scope',\n unauthenticated: 'scope',\n timeout: 'reachability',\n network: 'reachability',\n invalidQuery: 'reachability',\n notFound: 'reachability',\n unexpected: 'reachability',\n};\n\n/** What the browser is currently showing for records. */\ninterface RecordsState {\n /** The term this answer is for, so a stale response cannot overwrite a newer one. */\n readonly term: string;\n readonly response?: RecordSearchResponse;\n readonly failed: boolean;\n}\n\nconst NOTHING_YET: RecordsState = { term: '', failed: false };\n\n/**\n * Records matching the term, one group per source, in the order the host\n * declared them.\n *\n * The whole of the *keystroke* policy lives here rather than in `searchRecords`,\n * which is deliberately a bare typed `fetch`: the palette is the thing that\n * knows a keystroke is not a query. So this owns the debounce, the abort and the\n * two-character floor, while the route owns the fan-out, the per-source timeout\n * and the authorization.\n *\n * **No index, ever.** Every option here came back from `/api/search` for this\n * principal and this organization, on this keystroke. A prefetched client index\n * is the shape that surfaces another organization's record the first time a\n * session's scope moves under it, and ADR 0040 forecloses it permanently.\n *\n * Below the two-character floor the group is present and **says why** rather\n * than being empty or absent: empty would claim nothing matched a search that\n * never ran, and absent would leave someone wondering whether records are\n * searchable here at all.\n */\nexport function useRecordsSource(\n term: string,\n enabled: boolean,\n): CommandSource {\n const t = useT('shell');\n const translateKey = useTranslateKey();\n const withLocale = useLocaleHref();\n\n const [state, setState] = useState<RecordsState>(NOTHING_YET);\n // The term the request in flight is for. A ref, not state: it changes on every\n // keystroke and nothing renders from it.\n const pending = useRef<string | null>(null);\n\n const active = enabled && term.length >= RECORD_SEARCH_MIN_TERM;\n\n useEffect(() => {\n // Nothing is cleared here on purpose. A `setState` in an effect body is a\n // cascading render, and there is nothing to clear: the memo below returns no\n // groups while inactive, so a previous answer is unreachable rather than\n // stale — and the host empties the term when it opens the palette.\n if (!active) {\n pending.current = null;\n return;\n }\n\n const controller = new AbortController();\n pending.current = term;\n\n const timer = setTimeout(() => {\n void searchRecords(term, { signal: controller.signal })\n .then(response => {\n if (pending.current !== term) return;\n setState({ term, response, failed: false });\n })\n .catch(() => {\n // An abort lands here too and must not be shown: the term moved on,\n // so there is nothing to report about the request that did not finish.\n if (controller.signal.aborted || pending.current !== term) return;\n setState({ term, failed: true });\n });\n }, RECORD_SEARCH_DEBOUNCE_MS);\n\n return () => {\n clearTimeout(timer);\n controller.abort();\n };\n }, [active, term]);\n\n return useMemo(() => {\n const heading = t('commandPalette.groups.records');\n\n if (!active) {\n // Someone who has typed one character is mid-word, not looking at an\n // answer. Saying so is better than both alternatives: an empty group would\n // claim nothing matched a search that never ran, and an absent one would\n // leave them wondering whether records are searchable at all.\n if (!enabled || term.length === 0) return { key: 'records', groups: [] };\n return {\n key: 'records',\n groups: [\n {\n key: 'records',\n label: heading,\n options: [],\n isLoading: false,\n unavailable: {\n message: t('commandPalette.typeMore', {\n count: RECORD_SEARCH_MIN_TERM,\n }),\n severity: 'scope',\n },\n },\n ],\n };\n }\n // A newer term is in flight. The previous answer stays on screen with its\n // groups marked loading rather than blanking: a list that empties between\n // keystrokes flickers, and the rows it drops were correct a moment ago.\n const isLoading = state.term !== term;\n\n if (state.response === undefined) {\n return {\n key: 'records',\n groups: [\n {\n key: 'records',\n label: heading,\n options: [],\n isLoading: !state.failed,\n // The whole endpoint failed — a lapsed session or a `400`, both of\n // which arrive as a rejection rather than inside a `200`. It is\n // named for the same reason a single degraded source is.\n ...(state.failed\n ? {\n unavailable: {\n message: t('commandPalette.unavailableReach', {\n reason: translateKey('errors:unexpected'),\n }),\n severity: 'reachability' as const,\n },\n }\n : {}),\n },\n ],\n };\n }\n\n const groups: CommandGroup[] = state.response.groups.map(group => ({\n key: `record:${group.source}`,\n // The entity's own `pluralKey`, so record search introduces no copy of\n // its own and a group is titled exactly as its screen is.\n label: translateKey(group.labelKey),\n options: group.items.map(item => ({\n id: `record:${group.source}:${item.id}`,\n label: item.label,\n ...(item.sublabel === undefined ? {} : { sublabel: item.sublabel }),\n href: withLocale(item.href),\n })),\n total: group.total,\n isLoading,\n }));\n\n for (const degraded of state.response.unavailable) {\n const severity = SEVERITY[degraded.reason];\n groups.push({\n key: `record:${degraded.source}`,\n // Derived from the entity rather than from the source key, which is an\n // identifier and would render as `product-specification`. Every\n // searchable entity declares a `pluralKey` — `catalogSurface` throws\n // without one — so this always resolves.\n label: translateKey(`entity:${degraded.entity}.plural`),\n options: [],\n isLoading: false,\n unavailable: {\n message: t(\n severity === 'scope'\n ? 'commandPalette.unavailableScope'\n : 'commandPalette.unavailableReach',\n { reason: translateKey(`errors:${degraded.reason}`) },\n ),\n severity,\n },\n });\n }\n\n return { key: 'records', groups };\n }, [active, enabled, state, term, t, translateKey, withLocale]);\n}\n"],"names":["useT","useTranslateKey","useEffect","useMemo","useRef","useState","useLocaleHref","searchRecords","RECORD_SEARCH_DEBOUNCE_MS","RECORD_SEARCH_MIN_TERM","SEVERITY","forbidden","noActiveOrganization","unauthenticated","timeout","network","invalidQuery","notFound","unexpected","NOTHING_YET","term","failed","useRecordsSource","enabled","t","translateKey","withLocale","state","setState","pending","active","length","current","controller","AbortController","timer","setTimeout","signal","then","response","catch","aborted","clearTimeout","abort","heading","key","groups","label","options","isLoading","unavailable","message","count","severity","undefined","reason","map","group","source","labelKey","items","item","id","sublabel","href","total","degraded","push","entity"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/lib/command-palette/use-records-source.ts"],"sourcesContent":["'use client';\n\nimport type {\n CommandGroup,\n CommandSource,\n CommandUnavailableSeverity,\n} from '@entifix/core';\nimport { useT, useTranslateKey } from '@entifix/react-controls';\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { useLocaleHref } from '../i18n/index.js';\nimport type {\n RecordSearchResponse,\n RecordSearchUnavailableReason,\n} from '../search/record-search.types.js';\nimport { searchRecords } from '../search/search-records.js';\nimport {\n RECORD_SEARCH_DEBOUNCE_MS,\n RECORD_SEARCH_MIN_TERM,\n} from './palette-command.js';\n\n/**\n * Which reasons are the caller's ordinary state, and which mean something broke.\n *\n * The split is the record search's, restated as severity because that is what the palette\n * renders. An operator holds no membership, so every tenant-plane source answers\n * `409` on every keystroke — painting that as an outage teaches them to ignore\n * the row entirely, and then the `timeout` that does matter goes unread too.\n */\nconst SEVERITY: Readonly<\n Record<RecordSearchUnavailableReason, CommandUnavailableSeverity>\n> = {\n forbidden: 'scope',\n noActiveOrganization: 'scope',\n unauthenticated: 'scope',\n timeout: 'reachability',\n network: 'reachability',\n invalidQuery: 'reachability',\n notFound: 'reachability',\n unexpected: 'reachability',\n};\n\n/** What the browser is currently showing for records. */\ninterface RecordsState {\n /** The term this answer is for, so a stale response cannot overwrite a newer one. */\n readonly term: string;\n readonly response?: RecordSearchResponse;\n readonly failed: boolean;\n}\n\nconst NOTHING_YET: RecordsState = { term: '', failed: false };\n\n/**\n * Records matching the term, one group per source, in the order the host\n * declared them.\n *\n * The whole of the *keystroke* policy lives here rather than in `searchRecords`,\n * which is deliberately a bare typed `fetch`: the palette is the thing that\n * knows a keystroke is not a query. So this owns the debounce, the abort and the\n * two-character floor, while the route owns the fan-out, the per-source timeout\n * and the authorization.\n *\n * **No index, ever.** Every option here came back from `/api/search` for this\n * principal and this organization, on this keystroke. A prefetched client index\n * is the shape that surfaces another organization's record the first time a\n * session's scope moves under it, so there is none, ever.\n *\n * Below the two-character floor the group is present and **says why** rather\n * than being empty or absent: empty would claim nothing matched a search that\n * never ran, and absent would leave someone wondering whether records are\n * searchable here at all.\n */\nexport function useRecordsSource(\n term: string,\n enabled: boolean,\n): CommandSource {\n const t = useT('shell');\n const translateKey = useTranslateKey();\n const withLocale = useLocaleHref();\n\n const [state, setState] = useState<RecordsState>(NOTHING_YET);\n // The term the request in flight is for. A ref, not state: it changes on every\n // keystroke and nothing renders from it.\n const pending = useRef<string | null>(null);\n\n const active = enabled && term.length >= RECORD_SEARCH_MIN_TERM;\n\n useEffect(() => {\n // Nothing is cleared here on purpose. A `setState` in an effect body is a\n // cascading render, and there is nothing to clear: the memo below returns no\n // groups while inactive, so a previous answer is unreachable rather than\n // stale — and the host empties the term when it opens the palette.\n if (!active) {\n pending.current = null;\n return;\n }\n\n const controller = new AbortController();\n pending.current = term;\n\n const timer = setTimeout(() => {\n void searchRecords(term, { signal: controller.signal })\n .then(response => {\n if (pending.current !== term) return;\n setState({ term, response, failed: false });\n })\n .catch(() => {\n // An abort lands here too and must not be shown: the term moved on,\n // so there is nothing to report about the request that did not finish.\n if (controller.signal.aborted || pending.current !== term) return;\n setState({ term, failed: true });\n });\n }, RECORD_SEARCH_DEBOUNCE_MS);\n\n return () => {\n clearTimeout(timer);\n controller.abort();\n };\n }, [active, term]);\n\n return useMemo(() => {\n const heading = t('commandPalette.groups.records');\n\n if (!active) {\n // Someone who has typed one character is mid-word, not looking at an\n // answer. Saying so is better than both alternatives: an empty group would\n // claim nothing matched a search that never ran, and an absent one would\n // leave them wondering whether records are searchable at all.\n if (!enabled || term.length === 0) return { key: 'records', groups: [] };\n return {\n key: 'records',\n groups: [\n {\n key: 'records',\n label: heading,\n options: [],\n isLoading: false,\n unavailable: {\n message: t('commandPalette.typeMore', {\n count: RECORD_SEARCH_MIN_TERM,\n }),\n severity: 'scope',\n },\n },\n ],\n };\n }\n // A newer term is in flight. The previous answer stays on screen with its\n // groups marked loading rather than blanking: a list that empties between\n // keystrokes flickers, and the rows it drops were correct a moment ago.\n const isLoading = state.term !== term;\n\n if (state.response === undefined) {\n return {\n key: 'records',\n groups: [\n {\n key: 'records',\n label: heading,\n options: [],\n isLoading: !state.failed,\n // The whole endpoint failed — a lapsed session or a `400`, both of\n // which arrive as a rejection rather than inside a `200`. It is\n // named for the same reason a single degraded source is.\n ...(state.failed\n ? {\n unavailable: {\n message: t('commandPalette.unavailableReach', {\n reason: translateKey('errors:unexpected'),\n }),\n severity: 'reachability' as const,\n },\n }\n : {}),\n },\n ],\n };\n }\n\n const groups: CommandGroup[] = state.response.groups.map(group => ({\n key: `record:${group.source}`,\n // The entity's own `pluralKey`, so record search introduces no copy of\n // its own and a group is titled exactly as its screen is.\n label: translateKey(group.labelKey),\n options: group.items.map(item => ({\n id: `record:${group.source}:${item.id}`,\n label: item.label,\n ...(item.sublabel === undefined ? {} : { sublabel: item.sublabel }),\n href: withLocale(item.href),\n })),\n total: group.total,\n isLoading,\n }));\n\n for (const degraded of state.response.unavailable) {\n const severity = SEVERITY[degraded.reason];\n groups.push({\n key: `record:${degraded.source}`,\n // Derived from the entity rather than from the source key, which is an\n // identifier and would render as `product-specification`. Every\n // searchable entity declares a `pluralKey` — `catalogSurface` throws\n // without one — so this always resolves.\n label: translateKey(`entity:${degraded.entity}.plural`),\n options: [],\n isLoading: false,\n unavailable: {\n message: t(\n severity === 'scope'\n ? 'commandPalette.unavailableScope'\n : 'commandPalette.unavailableReach',\n { reason: translateKey(`errors:${degraded.reason}`) },\n ),\n severity,\n },\n });\n }\n\n return { key: 'records', groups };\n }, [active, enabled, state, term, t, translateKey, withLocale]);\n}\n"],"names":["useT","useTranslateKey","useEffect","useMemo","useRef","useState","useLocaleHref","searchRecords","RECORD_SEARCH_DEBOUNCE_MS","RECORD_SEARCH_MIN_TERM","SEVERITY","forbidden","noActiveOrganization","unauthenticated","timeout","network","invalidQuery","notFound","unexpected","NOTHING_YET","term","failed","useRecordsSource","enabled","t","translateKey","withLocale","state","setState","pending","active","length","current","controller","AbortController","timer","setTimeout","signal","then","response","catch","aborted","clearTimeout","abort","heading","key","groups","label","options","isLoading","unavailable","message","count","severity","undefined","reason","map","group","source","labelKey","items","item","id","sublabel","href","total","degraded","push","entity"],"mappings":"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"}
@@ -13,7 +13,7 @@ import { type CommandSource } from '@entifix/core';
13
13
  * a plain route would navigate somewhere the person did not ask to be.
14
14
  *
15
15
  * Selecting one is a **deep link**, not a store call: `?tab=` is the tab's
16
- * address (ADR 0042), so the workspace focuses it on arrival and the palette
16
+ * address, so the workspace focuses it on arrival and the palette
17
17
  * does not have to be inside the workspace tree to reach the store's actions.
18
18
  */
19
19
  export declare function useTabsSource(term: string): CommandSource;
@@ -18,7 +18,7 @@ import { useTabsState } from '../workspace/tabs-state.js';
18
18
  * a plain route would navigate somewhere the person did not ask to be.
19
19
  *
20
20
  * Selecting one is a **deep link**, not a store call: `?tab=` is the tab's
21
- * address (ADR 0042), so the workspace focuses it on arrival and the palette
21
+ * address, so the workspace focuses it on arrival and the palette
22
22
  * does not have to be inside the workspace tree to reach the store's actions.
23
23
  */ export function useTabsSource(term) {
24
24
  const t = useT('shell');
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/command-palette/use-tabs-source.ts"],"sourcesContent":["'use client';\n\nimport {\n type CommandOption,\n type CommandSource,\n matchesCommand,\n} from '@entifix/core';\nimport { useT } from '@entifix/react-controls';\nimport { useMemo } from 'react';\n\nimport { useLocaleHref } from '../i18n/index.js';\nimport { useTabsState } from '../workspace/tabs-state.js';\n\n/**\n * The workspace's open tabs, as one group.\n *\n * Read straight from the store rather than through a prop, because the palette\n * is mounted in the chrome and the tabs are the workspace's — there is no\n * component between them to thread it through.\n *\n * ⚠️ Outside `/workspace` the store is deliberately **unhydrated**\n * (`skipHydration: true`, rehydrated by `WorkspaceShell`), so this group is\n * empty there and the palette drops it. That is right rather than a limitation:\n * a tab is a thing you can return to inside the workspace, and offering one from\n * a plain route would navigate somewhere the person did not ask to be.\n *\n * Selecting one is a **deep link**, not a store call: `?tab=` is the tab's\n * address (ADR 0042), so the workspace focuses it on arrival and the palette\n * does not have to be inside the workspace tree to reach the store's actions.\n */\nexport function useTabsSource(term: string): CommandSource {\n const t = useT('shell');\n const withLocale = useLocaleHref();\n const tabs = useTabsState(state => state.tabs);\n\n return useMemo(() => {\n const options: CommandOption[] = tabs.map(tab => ({\n id: `tab:${tab.param}`,\n label: tab.title,\n href: withLocale(`/workspace?tab=${encodeURIComponent(tab.param)}`),\n }));\n\n return {\n key: 'tabs',\n groups: [\n {\n key: 'tabs',\n label: t('commandPalette.groups.tabs'),\n options: options.filter(option => matchesCommand(term, option)),\n isLoading: false,\n },\n ],\n } satisfies CommandSource;\n }, [tabs, term, t, withLocale]);\n}\n"],"names":["matchesCommand","useT","useMemo","useLocaleHref","useTabsState","useTabsSource","term","t","withLocale","tabs","state","options","map","tab","id","param","label","title","href","encodeURIComponent","key","groups","filter","option","isLoading"],"mappings":"AAAA;AAEA,SAGEA,cAAc,QACT,gBAAgB;AACvB,SAASC,IAAI,QAAQ,0BAA0B;AAC/C,SAASC,OAAO,QAAQ,QAAQ;AAEhC,SAASC,aAAa,QAAQ,mBAAmB;AACjD,SAASC,YAAY,QAAQ,6BAA6B;AAE1D;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,cAAcC,IAAY;IACxC,MAAMC,IAAIN,KAAK;IACf,MAAMO,aAAaL;IACnB,MAAMM,OAAOL,aAAaM,CAAAA,QAASA,MAAMD,IAAI;IAE7C,OAAOP,QAAQ;QACb,MAAMS,UAA2BF,KAAKG,GAAG,CAACC,CAAAA,MAAQ,CAAA;gBAChDC,IAAI,CAAC,IAAI,EAAED,IAAIE,KAAK,EAAE;gBACtBC,OAAOH,IAAII,KAAK;gBAChBC,MAAMV,WAAW,CAAC,eAAe,EAAEW,mBAAmBN,IAAIE,KAAK,GAAG;YACpE,CAAA;QAEA,OAAO;YACLK,KAAK;YACLC,QAAQ;gBACN;oBACED,KAAK;oBACLJ,OAAOT,EAAE;oBACTI,SAASA,QAAQW,MAAM,CAACC,CAAAA,SAAUvB,eAAeM,MAAMiB;oBACvDC,WAAW;gBACb;aACD;QACH;IACF,GAAG;QAACf;QAAMH;QAAMC;QAAGC;KAAW;AAChC"}
1
+ {"version":3,"sources":["../../../src/lib/command-palette/use-tabs-source.ts"],"sourcesContent":["'use client';\n\nimport {\n type CommandOption,\n type CommandSource,\n matchesCommand,\n} from '@entifix/core';\nimport { useT } from '@entifix/react-controls';\nimport { useMemo } from 'react';\n\nimport { useLocaleHref } from '../i18n/index.js';\nimport { useTabsState } from '../workspace/tabs-state.js';\n\n/**\n * The workspace's open tabs, as one group.\n *\n * Read straight from the store rather than through a prop, because the palette\n * is mounted in the chrome and the tabs are the workspace's — there is no\n * component between them to thread it through.\n *\n * ⚠️ Outside `/workspace` the store is deliberately **unhydrated**\n * (`skipHydration: true`, rehydrated by `WorkspaceShell`), so this group is\n * empty there and the palette drops it. That is right rather than a limitation:\n * a tab is a thing you can return to inside the workspace, and offering one from\n * a plain route would navigate somewhere the person did not ask to be.\n *\n * Selecting one is a **deep link**, not a store call: `?tab=` is the tab's\n * address, so the workspace focuses it on arrival and the palette\n * does not have to be inside the workspace tree to reach the store's actions.\n */\nexport function useTabsSource(term: string): CommandSource {\n const t = useT('shell');\n const withLocale = useLocaleHref();\n const tabs = useTabsState(state => state.tabs);\n\n return useMemo(() => {\n const options: CommandOption[] = tabs.map(tab => ({\n id: `tab:${tab.param}`,\n label: tab.title,\n href: withLocale(`/workspace?tab=${encodeURIComponent(tab.param)}`),\n }));\n\n return {\n key: 'tabs',\n groups: [\n {\n key: 'tabs',\n label: t('commandPalette.groups.tabs'),\n options: options.filter(option => matchesCommand(term, option)),\n isLoading: false,\n },\n ],\n } satisfies CommandSource;\n }, [tabs, term, t, withLocale]);\n}\n"],"names":["matchesCommand","useT","useMemo","useLocaleHref","useTabsState","useTabsSource","term","t","withLocale","tabs","state","options","map","tab","id","param","label","title","href","encodeURIComponent","key","groups","filter","option","isLoading"],"mappings":"AAAA;AAEA,SAGEA,cAAc,QACT,gBAAgB;AACvB,SAASC,IAAI,QAAQ,0BAA0B;AAC/C,SAASC,OAAO,QAAQ,QAAQ;AAEhC,SAASC,aAAa,QAAQ,mBAAmB;AACjD,SAASC,YAAY,QAAQ,6BAA6B;AAE1D;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,cAAcC,IAAY;IACxC,MAAMC,IAAIN,KAAK;IACf,MAAMO,aAAaL;IACnB,MAAMM,OAAOL,aAAaM,CAAAA,QAASA,MAAMD,IAAI;IAE7C,OAAOP,QAAQ;QACb,MAAMS,UAA2BF,KAAKG,GAAG,CAACC,CAAAA,MAAQ,CAAA;gBAChDC,IAAI,CAAC,IAAI,EAAED,IAAIE,KAAK,EAAE;gBACtBC,OAAOH,IAAII,KAAK;gBAChBC,MAAMV,WAAW,CAAC,eAAe,EAAEW,mBAAmBN,IAAIE,KAAK,GAAG;YACpE,CAAA;QAEA,OAAO;YACLK,KAAK;YACLC,QAAQ;gBACN;oBACED,KAAK;oBACLJ,OAAOT,EAAE;oBACTI,SAASA,QAAQW,MAAM,CAACC,CAAAA,SAAUvB,eAAeM,MAAMiB;oBACvDC,WAAW;gBACb;aACD;QACH;IACF,GAAG;QAACf;QAAMH;QAAMC;QAAGC;KAAW;AAChC"}
@@ -7,10 +7,10 @@ import { useMemo } from 'react';
7
7
  /**
8
8
  * The `unbound` verbs this caller may invoke, as one group.
9
9
  *
10
- * This is the surface ADR 0035 mapped and nothing rendered: all three
10
+ * This is the surface the action placement map named and nothing rendered: all three
11
11
  * `unbound:*` cells resolve to `'command-palette'`, and until now the palette
12
12
  * did not exist, so a verb declared there was granted, exported, passed every
13
- * `@r10c/slices` invariant and simply never appeared.
13
+ * check the host's register ran and simply never appeared.
14
14
  *
15
15
  * The descriptors are **served, not imported**: `$metadata` filters them through
16
16
  * `PolicyDecisionTag` against the verified principal, which is a stronger check