@shipfox/client-shell 5.0.0 → 6.0.2

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 (60) hide show
  1. package/dist/components/main-layout.d.ts.map +1 -1
  2. package/dist/components/main-layout.js +3 -4
  3. package/dist/components/main-layout.js.map +1 -1
  4. package/dist/components/nav-tabs.d.ts.map +1 -1
  5. package/dist/components/nav-tabs.js +3 -4
  6. package/dist/components/nav-tabs.js.map +1 -1
  7. package/dist/components/settings-nav.d.ts.map +1 -1
  8. package/dist/components/settings-nav.js +3 -4
  9. package/dist/components/settings-nav.js.map +1 -1
  10. package/dist/components/workspace-switcher.d.ts.map +1 -1
  11. package/dist/components/workspace-switcher.js +3 -2
  12. package/dist/components/workspace-switcher.js.map +1 -1
  13. package/dist/compose/compose-client-features.d.ts +11 -0
  14. package/dist/compose/compose-client-features.d.ts.map +1 -0
  15. package/dist/compose/compose-client-features.js +19 -0
  16. package/dist/compose/compose-client-features.js.map +1 -0
  17. package/dist/compose/compose-routes.d.ts +1 -0
  18. package/dist/compose/compose-routes.d.ts.map +1 -1
  19. package/dist/compose/compose-routes.js +4 -2
  20. package/dist/compose/compose-routes.js.map +1 -1
  21. package/dist/compose/validate-registries.d.ts +9 -2
  22. package/dist/compose/validate-registries.d.ts.map +1 -1
  23. package/dist/compose/validate-registries.js +32 -8
  24. package/dist/compose/validate-registries.js.map +1 -1
  25. package/dist/contract.d.ts +6 -0
  26. package/dist/contract.d.ts.map +1 -1
  27. package/dist/contract.js.map +1 -1
  28. package/dist/hooks/api/session-auth.d.ts +15 -0
  29. package/dist/hooks/api/session-auth.d.ts.map +1 -0
  30. package/dist/hooks/api/session-auth.js +55 -0
  31. package/dist/hooks/api/session-auth.js.map +1 -0
  32. package/dist/runtime/active-workspace.d.ts.map +1 -1
  33. package/dist/runtime/active-workspace.js +2 -4
  34. package/dist/runtime/active-workspace.js.map +1 -1
  35. package/dist/runtime/anchors.js +5 -5
  36. package/dist/runtime/anchors.js.map +1 -1
  37. package/dist/runtime/auth.d.ts +4 -11
  38. package/dist/runtime/auth.d.ts.map +1 -1
  39. package/dist/runtime/auth.js +73 -64
  40. package/dist/runtime/auth.js.map +1 -1
  41. package/dist/runtime/compose-client-app.d.ts.map +1 -1
  42. package/dist/runtime/compose-client-app.js +3 -9
  43. package/dist/runtime/compose-client-app.js.map +1 -1
  44. package/dist/runtime/index.d.ts +2 -0
  45. package/dist/runtime/index.d.ts.map +1 -1
  46. package/dist/runtime/index.js +2 -0
  47. package/dist/runtime/index.js.map +1 -1
  48. package/dist/runtime/last-workspace.d.ts +6 -3
  49. package/dist/runtime/last-workspace.d.ts.map +1 -1
  50. package/dist/runtime/last-workspace.js +12 -21
  51. package/dist/runtime/last-workspace.js.map +1 -1
  52. package/dist/runtime/route-inputs.d.ts +18 -0
  53. package/dist/runtime/route-inputs.d.ts.map +1 -0
  54. package/dist/runtime/route-inputs.js +49 -0
  55. package/dist/runtime/route-inputs.js.map +1 -0
  56. package/dist/tsconfig.test.tsbuildinfo +1 -1
  57. package/dist/vite/plugin.d.ts.map +1 -1
  58. package/dist/vite/plugin.js +6 -14
  59. package/dist/vite/plugin.js.map +1 -1
  60. package/package.json +10 -11
@@ -1 +1 @@
1
- {"version":3,"file":"main-layout.d.ts","sourceRoot":"","sources":["../../src/components/main-layout.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,cAAc,CAAC;AAK9C,OAAO,QAAQ,wBAAwB,CAAC;IACtC,UAAU,qBAAqB;QAC7B,MAAM,CAAC,EAAE,YAAY,CAAC;KACvB;CACF;AAED,wBAAgB,UAAU,CAAC,EACzB,UAAU,EACV,qBAA6B,GAC9B,EAAE;IACD,UAAU,EAAE,SAAS,WAAW,EAAE,CAAC;IACnC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,+BAyBA"}
1
+ {"version":3,"file":"main-layout.d.ts","sourceRoot":"","sources":["../../src/components/main-layout.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,cAAc,CAAC;AAM9C,OAAO,QAAQ,wBAAwB,CAAC;IACtC,UAAU,qBAAqB;QAC7B,MAAM,CAAC,EAAE,YAAY,CAAC;KACvB;CACF;AAED,wBAAgB,UAAU,CAAC,EACzB,UAAU,EACV,qBAA6B,GAC9B,EAAE;IACD,UAAU,EAAE,SAAS,WAAW,EAAE,CAAC;IACnC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,+BAyBA"}
@@ -1,14 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { FullPageLoader } from '@shipfox/react-ui/loader';
3
- import { Outlet, useMatches, useParams } from '@tanstack/react-router';
3
+ import { Outlet, useMatches } from '@tanstack/react-router';
4
4
  import { useMaybeActiveWorkspace } from '#runtime/active-workspace.js';
5
+ import { parseWorkspaceProjectParams, useRouteParams } from '#runtime/route-inputs.js';
5
6
  import { NavBar } from './nav-bar.js';
6
7
  import { NavTabs } from './nav-tabs.js';
7
8
  export function MainLayout({ navigation, hideProjectNavigation = false }) {
8
9
  const workspace = useMaybeActiveWorkspace();
9
- const { pid } = useParams({
10
- strict: false
11
- });
10
+ const { pid } = useRouteParams(parseWorkspaceProjectParams);
12
11
  const matches = useMatches();
13
12
  if (!workspace) return /*#__PURE__*/ _jsx(FullPageLoader, {});
14
13
  const fullBleed = matches.some((match)=>match.staticData.layout === 'full-bleed');
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/main-layout.tsx"],"sourcesContent":["import {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {Outlet, useMatches, useParams} from '@tanstack/react-router';\nimport type {NavTabEntry} from '#contract.js';\nimport {useMaybeActiveWorkspace} from '#runtime/active-workspace.js';\nimport {NavBar} from './nav-bar.js';\nimport {NavTabs} from './nav-tabs.js';\n\ndeclare module '@tanstack/react-router' {\n interface StaticDataRouteOption {\n layout?: 'full-bleed';\n }\n}\n\nexport function MainLayout({\n navigation,\n hideProjectNavigation = false,\n}: {\n navigation: readonly NavTabEntry[];\n hideProjectNavigation?: boolean;\n}) {\n const workspace = useMaybeActiveWorkspace();\n const {pid} = useParams({strict: false}) as {pid?: string};\n const matches = useMatches();\n if (!workspace) return <FullPageLoader />;\n const fullBleed = matches.some((match) => match.staticData.layout === 'full-bleed');\n return (\n <div className=\"h-screen w-full flex flex-col bg-background-subtle-base\">\n <NavBar hideProjectNavigation={hideProjectNavigation} />\n {hideProjectNavigation ? undefined : (\n <NavTabs entries={navigation} scope={pid ? 'project' : 'workspace'} />\n )}\n {fullBleed ? (\n <main className=\"flex-1 min-h-0 flex flex-col overflow-hidden\">\n <Outlet />\n </main>\n ) : (\n <main className=\"flex-1 overflow-auto\">\n <div className=\"max-w-[1120px] mx-auto px-24 py-32\">\n <Outlet />\n </div>\n </main>\n )}\n </div>\n );\n}\n"],"names":["FullPageLoader","Outlet","useMatches","useParams","useMaybeActiveWorkspace","NavBar","NavTabs","MainLayout","navigation","hideProjectNavigation","workspace","pid","strict","matches","fullBleed","some","match","staticData","layout","div","className","undefined","entries","scope","main"],"mappings":";AAAA,SAAQA,cAAc,QAAO,2BAA2B;AACxD,SAAQC,MAAM,EAAEC,UAAU,EAAEC,SAAS,QAAO,yBAAyB;AAErE,SAAQC,uBAAuB,QAAO,+BAA+B;AACrE,SAAQC,MAAM,QAAO,eAAe;AACpC,SAAQC,OAAO,QAAO,gBAAgB;AAQtC,OAAO,SAASC,WAAW,EACzBC,UAAU,EACVC,wBAAwB,KAAK,EAI9B;IACC,MAAMC,YAAYN;IAClB,MAAM,EAACO,GAAG,EAAC,GAAGR,UAAU;QAACS,QAAQ;IAAK;IACtC,MAAMC,UAAUX;IAChB,IAAI,CAACQ,WAAW,qBAAO,KAACV;IACxB,MAAMc,YAAYD,QAAQE,IAAI,CAAC,CAACC,QAAUA,MAAMC,UAAU,CAACC,MAAM,KAAK;IACtE,qBACE,MAACC;QAAIC,WAAU;;0BACb,KAACf;gBAAOI,uBAAuBA;;YAC9BA,wBAAwBY,0BACvB,KAACf;gBAAQgB,SAASd;gBAAYe,OAAOZ,MAAM,YAAY;;YAExDG,0BACC,KAACU;gBAAKJ,WAAU;0BACd,cAAA,KAACnB;+BAGH,KAACuB;gBAAKJ,WAAU;0BACd,cAAA,KAACD;oBAAIC,WAAU;8BACb,cAAA,KAACnB;;;;;AAMb"}
1
+ {"version":3,"sources":["../../src/components/main-layout.tsx"],"sourcesContent":["import {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {Outlet, useMatches} from '@tanstack/react-router';\nimport type {NavTabEntry} from '#contract.js';\nimport {useMaybeActiveWorkspace} from '#runtime/active-workspace.js';\nimport {parseWorkspaceProjectParams, useRouteParams} from '#runtime/route-inputs.js';\nimport {NavBar} from './nav-bar.js';\nimport {NavTabs} from './nav-tabs.js';\n\ndeclare module '@tanstack/react-router' {\n interface StaticDataRouteOption {\n layout?: 'full-bleed';\n }\n}\n\nexport function MainLayout({\n navigation,\n hideProjectNavigation = false,\n}: {\n navigation: readonly NavTabEntry[];\n hideProjectNavigation?: boolean;\n}) {\n const workspace = useMaybeActiveWorkspace();\n const {pid} = useRouteParams(parseWorkspaceProjectParams);\n const matches = useMatches();\n if (!workspace) return <FullPageLoader />;\n const fullBleed = matches.some((match) => match.staticData.layout === 'full-bleed');\n return (\n <div className=\"h-screen w-full flex flex-col bg-background-subtle-base\">\n <NavBar hideProjectNavigation={hideProjectNavigation} />\n {hideProjectNavigation ? undefined : (\n <NavTabs entries={navigation} scope={pid ? 'project' : 'workspace'} />\n )}\n {fullBleed ? (\n <main className=\"flex-1 min-h-0 flex flex-col overflow-hidden\">\n <Outlet />\n </main>\n ) : (\n <main className=\"flex-1 overflow-auto\">\n <div className=\"max-w-[1120px] mx-auto px-24 py-32\">\n <Outlet />\n </div>\n </main>\n )}\n </div>\n );\n}\n"],"names":["FullPageLoader","Outlet","useMatches","useMaybeActiveWorkspace","parseWorkspaceProjectParams","useRouteParams","NavBar","NavTabs","MainLayout","navigation","hideProjectNavigation","workspace","pid","matches","fullBleed","some","match","staticData","layout","div","className","undefined","entries","scope","main"],"mappings":";AAAA,SAAQA,cAAc,QAAO,2BAA2B;AACxD,SAAQC,MAAM,EAAEC,UAAU,QAAO,yBAAyB;AAE1D,SAAQC,uBAAuB,QAAO,+BAA+B;AACrE,SAAQC,2BAA2B,EAAEC,cAAc,QAAO,2BAA2B;AACrF,SAAQC,MAAM,QAAO,eAAe;AACpC,SAAQC,OAAO,QAAO,gBAAgB;AAQtC,OAAO,SAASC,WAAW,EACzBC,UAAU,EACVC,wBAAwB,KAAK,EAI9B;IACC,MAAMC,YAAYR;IAClB,MAAM,EAACS,GAAG,EAAC,GAAGP,eAAeD;IAC7B,MAAMS,UAAUX;IAChB,IAAI,CAACS,WAAW,qBAAO,KAACX;IACxB,MAAMc,YAAYD,QAAQE,IAAI,CAAC,CAACC,QAAUA,MAAMC,UAAU,CAACC,MAAM,KAAK;IACtE,qBACE,MAACC;QAAIC,WAAU;;0BACb,KAACd;gBAAOI,uBAAuBA;;YAC9BA,wBAAwBW,0BACvB,KAACd;gBAAQe,SAASb;gBAAYc,OAAOX,MAAM,YAAY;;YAExDE,0BACC,KAACU;gBAAKJ,WAAU;0BACd,cAAA,KAACnB;+BAGH,KAACuB;gBAAKJ,WAAU;0BACd,cAAA,KAACD;oBAAIC,WAAU;8BACb,cAAA,KAACnB;;;;;AAMb"}
@@ -1 +1 @@
1
- {"version":3,"file":"nav-tabs.d.ts","sourceRoot":"","sources":["../../src/components/nav-tabs.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,cAAc,CAAC;AAE9C,wBAAgB,OAAO,CAAC,EACtB,OAAO,EACP,KAAK,GACN,EAAE;IACD,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IAChC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CAC7B,+BAoCA"}
1
+ {"version":3,"file":"nav-tabs.d.ts","sourceRoot":"","sources":["../../src/components/nav-tabs.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,cAAc,CAAC;AAG9C,wBAAgB,OAAO,CAAC,EACtB,OAAO,EACP,KAAK,GACN,EAAE;IACD,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IAChC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CAC7B,+BAoCA"}
@@ -1,10 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Link, useParams } from '@tanstack/react-router';
2
+ import { Link } from '@tanstack/react-router';
3
3
  import { useReducedMotion } from 'framer-motion';
4
+ import { parseWorkspaceProjectParams, useRouteParams } from '#runtime/route-inputs.js';
4
5
  export function NavTabs({ entries, scope }) {
5
- const params = useParams({
6
- strict: false
7
- });
6
+ const params = useRouteParams(parseWorkspaceProjectParams);
8
7
  const reduced = useReducedMotion();
9
8
  const tabs = entries.filter((entry)=>entry.scope === scope);
10
9
  const tabClassName = `h-40 inline-flex items-center px-4 text-sm font-medium transition-colors ${reduced ? '' : 'transition-[border-color]'}`;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/nav-tabs.tsx"],"sourcesContent":["import {Link, useParams} from '@tanstack/react-router';\nimport {useReducedMotion} from 'framer-motion';\nimport type {NavTabEntry} from '#contract.js';\n\nexport function NavTabs({\n entries,\n scope,\n}: {\n entries: readonly NavTabEntry[];\n scope: NavTabEntry['scope'];\n}) {\n const params = useParams({strict: false}) as {wid?: string; pid?: string};\n const reduced = useReducedMotion();\n const tabs = entries.filter((entry) => entry.scope === scope);\n const tabClassName = `h-40 inline-flex items-center px-4 text-sm font-medium transition-colors ${reduced ? '' : 'transition-[border-color]'}`;\n const activeProps = {\n className: 'border-b-2 border-border-highlights-interactive text-foreground-neutral-base',\n 'aria-selected': 'true' as const,\n };\n const inactiveProps = {\n className: 'border-b-2 border-transparent text-foreground-neutral-muted',\n 'aria-selected': 'false' as const,\n };\n\n return (\n <div\n role=\"tablist\"\n aria-label={`${scope === 'project' ? 'Project' : 'Workspace'} sections`}\n className=\"sticky top-56 z-20 h-40 px-16 flex items-end gap-12 bg-background-subtle-base border-b border-border-neutral-base\"\n >\n {tabs.map((entry) => (\n <Link\n key={entry.id}\n to={entry.to as never}\n params={params as never}\n role=\"tab\"\n activeOptions={{exact: entry.exact ?? false}}\n activeProps={activeProps}\n inactiveProps={inactiveProps}\n className={tabClassName}\n >\n {entry.label}\n </Link>\n ))}\n </div>\n );\n}\n"],"names":["Link","useParams","useReducedMotion","NavTabs","entries","scope","params","strict","reduced","tabs","filter","entry","tabClassName","activeProps","className","inactiveProps","div","role","aria-label","map","to","activeOptions","exact","label","id"],"mappings":";AAAA,SAAQA,IAAI,EAAEC,SAAS,QAAO,yBAAyB;AACvD,SAAQC,gBAAgB,QAAO,gBAAgB;AAG/C,OAAO,SAASC,QAAQ,EACtBC,OAAO,EACPC,KAAK,EAIN;IACC,MAAMC,SAASL,UAAU;QAACM,QAAQ;IAAK;IACvC,MAAMC,UAAUN;IAChB,MAAMO,OAAOL,QAAQM,MAAM,CAAC,CAACC,QAAUA,MAAMN,KAAK,KAAKA;IACvD,MAAMO,eAAe,CAAC,yEAAyE,EAAEJ,UAAU,KAAK,6BAA6B;IAC7I,MAAMK,cAAc;QAClBC,WAAW;QACX,iBAAiB;IACnB;IACA,MAAMC,gBAAgB;QACpBD,WAAW;QACX,iBAAiB;IACnB;IAEA,qBACE,KAACE;QACCC,MAAK;QACLC,cAAY,GAAGb,UAAU,YAAY,YAAY,YAAY,SAAS,CAAC;QACvES,WAAU;kBAETL,KAAKU,GAAG,CAAC,CAACR,sBACT,KAACX;gBAECoB,IAAIT,MAAMS,EAAE;gBACZd,QAAQA;gBACRW,MAAK;gBACLI,eAAe;oBAACC,OAAOX,MAAMW,KAAK,IAAI;gBAAK;gBAC3CT,aAAaA;gBACbE,eAAeA;gBACfD,WAAWF;0BAEVD,MAAMY,KAAK;eATPZ,MAAMa,EAAE;;AAcvB"}
1
+ {"version":3,"sources":["../../src/components/nav-tabs.tsx"],"sourcesContent":["import {Link} from '@tanstack/react-router';\nimport {useReducedMotion} from 'framer-motion';\nimport type {NavTabEntry} from '#contract.js';\nimport {parseWorkspaceProjectParams, useRouteParams} from '#runtime/route-inputs.js';\n\nexport function NavTabs({\n entries,\n scope,\n}: {\n entries: readonly NavTabEntry[];\n scope: NavTabEntry['scope'];\n}) {\n const params = useRouteParams(parseWorkspaceProjectParams);\n const reduced = useReducedMotion();\n const tabs = entries.filter((entry) => entry.scope === scope);\n const tabClassName = `h-40 inline-flex items-center px-4 text-sm font-medium transition-colors ${reduced ? '' : 'transition-[border-color]'}`;\n const activeProps = {\n className: 'border-b-2 border-border-highlights-interactive text-foreground-neutral-base',\n 'aria-selected': 'true' as const,\n };\n const inactiveProps = {\n className: 'border-b-2 border-transparent text-foreground-neutral-muted',\n 'aria-selected': 'false' as const,\n };\n\n return (\n <div\n role=\"tablist\"\n aria-label={`${scope === 'project' ? 'Project' : 'Workspace'} sections`}\n className=\"sticky top-56 z-20 h-40 px-16 flex items-end gap-12 bg-background-subtle-base border-b border-border-neutral-base\"\n >\n {tabs.map((entry) => (\n <Link\n key={entry.id}\n to={entry.to as never}\n params={params as never}\n role=\"tab\"\n activeOptions={{exact: entry.exact ?? false}}\n activeProps={activeProps}\n inactiveProps={inactiveProps}\n className={tabClassName}\n >\n {entry.label}\n </Link>\n ))}\n </div>\n );\n}\n"],"names":["Link","useReducedMotion","parseWorkspaceProjectParams","useRouteParams","NavTabs","entries","scope","params","reduced","tabs","filter","entry","tabClassName","activeProps","className","inactiveProps","div","role","aria-label","map","to","activeOptions","exact","label","id"],"mappings":";AAAA,SAAQA,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,gBAAgB,QAAO,gBAAgB;AAE/C,SAAQC,2BAA2B,EAAEC,cAAc,QAAO,2BAA2B;AAErF,OAAO,SAASC,QAAQ,EACtBC,OAAO,EACPC,KAAK,EAIN;IACC,MAAMC,SAASJ,eAAeD;IAC9B,MAAMM,UAAUP;IAChB,MAAMQ,OAAOJ,QAAQK,MAAM,CAAC,CAACC,QAAUA,MAAML,KAAK,KAAKA;IACvD,MAAMM,eAAe,CAAC,yEAAyE,EAAEJ,UAAU,KAAK,6BAA6B;IAC7I,MAAMK,cAAc;QAClBC,WAAW;QACX,iBAAiB;IACnB;IACA,MAAMC,gBAAgB;QACpBD,WAAW;QACX,iBAAiB;IACnB;IAEA,qBACE,KAACE;QACCC,MAAK;QACLC,cAAY,GAAGZ,UAAU,YAAY,YAAY,YAAY,SAAS,CAAC;QACvEQ,WAAU;kBAETL,KAAKU,GAAG,CAAC,CAACR,sBACT,KAACX;gBAECoB,IAAIT,MAAMS,EAAE;gBACZb,QAAQA;gBACRU,MAAK;gBACLI,eAAe;oBAACC,OAAOX,MAAMW,KAAK,IAAI;gBAAK;gBAC3CT,aAAaA;gBACbE,eAAeA;gBACfD,WAAWF;0BAEVD,MAAMY,KAAK;eATPZ,MAAMa,EAAE;;AAcvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"settings-nav.d.ts","sourceRoot":"","sources":["../../src/components/settings-nav.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,cAAc,CAAC;AAEvD,wBAAgB,WAAW,CAAC,EAAC,OAAO,EAAC,EAAE;IAAC,OAAO,EAAE,SAAS,oBAAoB,EAAE,CAAA;CAAC,sCA6BhF"}
1
+ {"version":3,"file":"settings-nav.d.ts","sourceRoot":"","sources":["../../src/components/settings-nav.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,cAAc,CAAC;AAGvD,wBAAgB,WAAW,CAAC,EAAC,OAAO,EAAC,EAAE;IAAC,OAAO,EAAE,SAAS,oBAAoB,EAAE,CAAA;CAAC,sCA6BhF"}
@@ -1,11 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button } from '@shipfox/react-ui/button';
3
3
  import { Icon } from '@shipfox/react-ui/icon';
4
- import { Link, useMatchRoute, useParams } from '@tanstack/react-router';
4
+ import { Link, useMatchRoute } from '@tanstack/react-router';
5
+ import { parseWorkspaceParams, useRouteParams } from '#runtime/route-inputs.js';
5
6
  export function SettingsNav({ entries }) {
6
- const params = useParams({
7
- strict: false
8
- });
7
+ const params = useRouteParams(parseWorkspaceParams);
9
8
  const matchRoute = useMatchRoute();
10
9
  if (!params.wid) return null;
11
10
  return /*#__PURE__*/ _jsx("nav", {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/settings-nav.tsx"],"sourcesContent":["import {Button} from '@shipfox/react-ui/button';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {Link, useMatchRoute, useParams} from '@tanstack/react-router';\nimport type {SettingsSectionEntry} from '#contract.js';\n\nexport function SettingsNav({entries}: {entries: readonly SettingsSectionEntry[]}) {\n const params = useParams({strict: false}) as {wid?: string};\n const matchRoute = useMatchRoute();\n if (!params.wid) return null;\n return (\n <nav aria-label=\"Workspace settings\" className=\"flex flex-col gap-4\">\n {entries.map((entry) => {\n const to = `/workspaces/$wid/settings/${entry.pathSegment}`;\n const active = Boolean(matchRoute({to: to as never, params: {wid: params.wid} as never}));\n return (\n <Button\n key={entry.id}\n asChild\n variant={active ? 'secondary' : 'transparent'}\n className=\"w-full justify-start\"\n >\n <Link\n to={to as never}\n params={{wid: params.wid} as never}\n aria-current={active ? 'page' : undefined}\n >\n <Icon name={entry.icon} className=\"size-16\" />\n {entry.label}\n </Link>\n </Button>\n );\n })}\n </nav>\n );\n}\n"],"names":["Button","Icon","Link","useMatchRoute","useParams","SettingsNav","entries","params","strict","matchRoute","wid","nav","aria-label","className","map","entry","to","pathSegment","active","Boolean","asChild","variant","aria-current","undefined","name","icon","label","id"],"mappings":";AAAA,SAAQA,MAAM,QAAO,2BAA2B;AAChD,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,IAAI,EAAEC,aAAa,EAAEC,SAAS,QAAO,yBAAyB;AAGtE,OAAO,SAASC,YAAY,EAACC,OAAO,EAA6C;IAC/E,MAAMC,SAASH,UAAU;QAACI,QAAQ;IAAK;IACvC,MAAMC,aAAaN;IACnB,IAAI,CAACI,OAAOG,GAAG,EAAE,OAAO;IACxB,qBACE,KAACC;QAAIC,cAAW;QAAqBC,WAAU;kBAC5CP,QAAQQ,GAAG,CAAC,CAACC;YACZ,MAAMC,KAAK,CAAC,0BAA0B,EAAED,MAAME,WAAW,EAAE;YAC3D,MAAMC,SAASC,QAAQV,WAAW;gBAACO,IAAIA;gBAAaT,QAAQ;oBAACG,KAAKH,OAAOG,GAAG;gBAAA;YAAU;YACtF,qBACE,KAACV;gBAECoB,OAAO;gBACPC,SAASH,SAAS,cAAc;gBAChCL,WAAU;0BAEV,cAAA,MAACX;oBACCc,IAAIA;oBACJT,QAAQ;wBAACG,KAAKH,OAAOG,GAAG;oBAAA;oBACxBY,gBAAcJ,SAAS,SAASK;;sCAEhC,KAACtB;4BAAKuB,MAAMT,MAAMU,IAAI;4BAAEZ,WAAU;;wBACjCE,MAAMW,KAAK;;;eAXTX,MAAMY,EAAE;QAenB;;AAGN"}
1
+ {"version":3,"sources":["../../src/components/settings-nav.tsx"],"sourcesContent":["import {Button} from '@shipfox/react-ui/button';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {Link, useMatchRoute} from '@tanstack/react-router';\nimport type {SettingsSectionEntry} from '#contract.js';\nimport {parseWorkspaceParams, useRouteParams} from '#runtime/route-inputs.js';\n\nexport function SettingsNav({entries}: {entries: readonly SettingsSectionEntry[]}) {\n const params = useRouteParams(parseWorkspaceParams);\n const matchRoute = useMatchRoute();\n if (!params.wid) return null;\n return (\n <nav aria-label=\"Workspace settings\" className=\"flex flex-col gap-4\">\n {entries.map((entry) => {\n const to = `/workspaces/$wid/settings/${entry.pathSegment}`;\n const active = Boolean(matchRoute({to: to as never, params: {wid: params.wid} as never}));\n return (\n <Button\n key={entry.id}\n asChild\n variant={active ? 'secondary' : 'transparent'}\n className=\"w-full justify-start\"\n >\n <Link\n to={to as never}\n params={{wid: params.wid} as never}\n aria-current={active ? 'page' : undefined}\n >\n <Icon name={entry.icon} className=\"size-16\" />\n {entry.label}\n </Link>\n </Button>\n );\n })}\n </nav>\n );\n}\n"],"names":["Button","Icon","Link","useMatchRoute","parseWorkspaceParams","useRouteParams","SettingsNav","entries","params","matchRoute","wid","nav","aria-label","className","map","entry","to","pathSegment","active","Boolean","asChild","variant","aria-current","undefined","name","icon","label","id"],"mappings":";AAAA,SAAQA,MAAM,QAAO,2BAA2B;AAChD,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,IAAI,EAAEC,aAAa,QAAO,yBAAyB;AAE3D,SAAQC,oBAAoB,EAAEC,cAAc,QAAO,2BAA2B;AAE9E,OAAO,SAASC,YAAY,EAACC,OAAO,EAA6C;IAC/E,MAAMC,SAASH,eAAeD;IAC9B,MAAMK,aAAaN;IACnB,IAAI,CAACK,OAAOE,GAAG,EAAE,OAAO;IACxB,qBACE,KAACC;QAAIC,cAAW;QAAqBC,WAAU;kBAC5CN,QAAQO,GAAG,CAAC,CAACC;YACZ,MAAMC,KAAK,CAAC,0BAA0B,EAAED,MAAME,WAAW,EAAE;YAC3D,MAAMC,SAASC,QAAQV,WAAW;gBAACO,IAAIA;gBAAaR,QAAQ;oBAACE,KAAKF,OAAOE,GAAG;gBAAA;YAAU;YACtF,qBACE,KAACV;gBAECoB,OAAO;gBACPC,SAASH,SAAS,cAAc;gBAChCL,WAAU;0BAEV,cAAA,MAACX;oBACCc,IAAIA;oBACJR,QAAQ;wBAACE,KAAKF,OAAOE,GAAG;oBAAA;oBACxBY,gBAAcJ,SAAS,SAASK;;sCAEhC,KAACtB;4BAAKuB,MAAMT,MAAMU,IAAI;4BAAEZ,WAAU;;wBACjCE,MAAMW,KAAK;;;eAXTX,MAAMY,EAAE;QAenB;;AAGN"}
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-switcher.d.ts","sourceRoot":"","sources":["../../src/components/workspace-switcher.tsx"],"names":[],"mappings":"AAeA,wBAAgB,iBAAiB,CAAC,EAChC,iBAAiB,EACjB,QAAQ,GACT,EAAE;IACD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,+BAqDA"}
1
+ {"version":3,"file":"workspace-switcher.d.ts","sourceRoot":"","sources":["../../src/components/workspace-switcher.tsx"],"names":[],"mappings":"AAeA,wBAAgB,iBAAiB,CAAC,EAChC,iBAAiB,EACjB,QAAQ,GACT,EAAE;IACD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,+BAsDA"}
@@ -4,14 +4,15 @@ import { Icon } from '@shipfox/react-ui/icon';
4
4
  import { useNavigate } from '@tanstack/react-router';
5
5
  import { useSetAtom } from 'jotai';
6
6
  import { useAuthState } from '#runtime/auth.js';
7
- import { lastWorkspaceIdAtom } from '#runtime/last-workspace.js';
7
+ import { lastWorkspaceIdAtom, rememberLastWorkspaceId } from '#runtime/last-workspace.js';
8
8
  export function WorkspaceSwitcher({ activeWorkspaceId, onSelect }) {
9
- const { workspaces } = useAuthState();
9
+ const { user, workspaces } = useAuthState();
10
10
  const navigate = useNavigate();
11
11
  const setLastWorkspaceId = useSetAtom(lastWorkspaceIdAtom);
12
12
  const selectWorkspace = (wid)=>{
13
13
  try {
14
14
  setLastWorkspaceId(wid);
15
+ if (user?.id) rememberLastWorkspaceId(user.id, wid);
15
16
  } catch {
16
17
  // Local storage is best effort.
17
18
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/workspace-switcher.tsx"],"sourcesContent":["import {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from '@shipfox/react-ui/command';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {useNavigate} from '@tanstack/react-router';\nimport {useSetAtom} from 'jotai';\nimport {useAuthState} from '#runtime/auth.js';\nimport {lastWorkspaceIdAtom} from '#runtime/last-workspace.js';\n\nexport function WorkspaceSwitcher({\n activeWorkspaceId,\n onSelect,\n}: {\n activeWorkspaceId?: string;\n onSelect?: () => void;\n}) {\n const {workspaces} = useAuthState();\n const navigate = useNavigate();\n const setLastWorkspaceId = useSetAtom(lastWorkspaceIdAtom);\n const selectWorkspace = (wid: string) => {\n try {\n setLastWorkspaceId(wid);\n } catch {\n // Local storage is best effort.\n }\n navigate({to: '/workspaces/$wid', params: {wid}});\n onSelect?.();\n };\n return (\n <Command>\n <CommandInput placeholder=\"Search workspaces...\" />\n <CommandList className=\"max-h-none overflow-visible overflow-y-visible p-0\">\n <div className=\"max-h-300 overflow-y-auto overflow-x-hidden p-4 scrollbar\">\n <CommandEmpty>No workspaces found.</CommandEmpty>\n <CommandGroup heading=\"Workspaces\">\n {workspaces.map((workspace) => (\n <CommandItem\n key={workspace.id}\n value={workspace.id}\n keywords={[workspace.name]}\n onSelect={() => selectWorkspace(workspace.id)}\n >\n <Icon\n name=\"check\"\n className={`size-16 mr-8 ${activeWorkspaceId === workspace.id ? 'opacity-100' : 'opacity-0'}`}\n />\n {workspace.name}\n </CommandItem>\n ))}\n </CommandGroup>\n </div>\n <CommandSeparator alwaysRender className=\"mx-0\" />\n <CommandGroup forceMount className=\"p-4\">\n <CommandItem\n value=\"__create\"\n onSelect={() => {\n navigate({to: '/setup/workspaces/new' as never});\n onSelect?.();\n }}\n forceMount\n >\n <Icon name=\"addLine\" className=\"size-16\" />\n Create workspace\n </CommandItem>\n </CommandGroup>\n </CommandList>\n </Command>\n );\n}\n"],"names":["Command","CommandEmpty","CommandGroup","CommandInput","CommandItem","CommandList","CommandSeparator","Icon","useNavigate","useSetAtom","useAuthState","lastWorkspaceIdAtom","WorkspaceSwitcher","activeWorkspaceId","onSelect","workspaces","navigate","setLastWorkspaceId","selectWorkspace","wid","to","params","placeholder","className","div","heading","map","workspace","value","id","keywords","name","alwaysRender","forceMount"],"mappings":";AAAA,SACEA,OAAO,EACPC,YAAY,EACZC,YAAY,EACZC,YAAY,EACZC,WAAW,EACXC,WAAW,EACXC,gBAAgB,QACX,4BAA4B;AACnC,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,WAAW,QAAO,yBAAyB;AACnD,SAAQC,UAAU,QAAO,QAAQ;AACjC,SAAQC,YAAY,QAAO,mBAAmB;AAC9C,SAAQC,mBAAmB,QAAO,6BAA6B;AAE/D,OAAO,SAASC,kBAAkB,EAChCC,iBAAiB,EACjBC,QAAQ,EAIT;IACC,MAAM,EAACC,UAAU,EAAC,GAAGL;IACrB,MAAMM,WAAWR;IACjB,MAAMS,qBAAqBR,WAAWE;IACtC,MAAMO,kBAAkB,CAACC;QACvB,IAAI;YACFF,mBAAmBE;QACrB,EAAE,OAAM;QACN,gCAAgC;QAClC;QACAH,SAAS;YAACI,IAAI;YAAoBC,QAAQ;gBAACF;YAAG;QAAC;QAC/CL;IACF;IACA,qBACE,MAACd;;0BACC,KAACG;gBAAamB,aAAY;;0BAC1B,MAACjB;gBAAYkB,WAAU;;kCACrB,MAACC;wBAAID,WAAU;;0CACb,KAACtB;0CAAa;;0CACd,KAACC;gCAAauB,SAAQ;0CACnBV,WAAWW,GAAG,CAAC,CAACC,0BACf,MAACvB;wCAECwB,OAAOD,UAAUE,EAAE;wCACnBC,UAAU;4CAACH,UAAUI,IAAI;yCAAC;wCAC1BjB,UAAU,IAAMI,gBAAgBS,UAAUE,EAAE;;0DAE5C,KAACtB;gDACCwB,MAAK;gDACLR,WAAW,CAAC,aAAa,EAAEV,sBAAsBc,UAAUE,EAAE,GAAG,gBAAgB,aAAa;;4CAE9FF,UAAUI,IAAI;;uCATVJ,UAAUE,EAAE;;;;kCAczB,KAACvB;wBAAiB0B,YAAY;wBAACT,WAAU;;kCACzC,KAACrB;wBAAa+B,UAAU;wBAACV,WAAU;kCACjC,cAAA,MAACnB;4BACCwB,OAAM;4BACNd,UAAU;gCACRE,SAAS;oCAACI,IAAI;gCAAgC;gCAC9CN;4BACF;4BACAmB,UAAU;;8CAEV,KAAC1B;oCAAKwB,MAAK;oCAAUR,WAAU;;gCAAY;;;;;;;;AAOvD"}
1
+ {"version":3,"sources":["../../src/components/workspace-switcher.tsx"],"sourcesContent":["import {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from '@shipfox/react-ui/command';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {useNavigate} from '@tanstack/react-router';\nimport {useSetAtom} from 'jotai';\nimport {useAuthState} from '#runtime/auth.js';\nimport {lastWorkspaceIdAtom, rememberLastWorkspaceId} from '#runtime/last-workspace.js';\n\nexport function WorkspaceSwitcher({\n activeWorkspaceId,\n onSelect,\n}: {\n activeWorkspaceId?: string;\n onSelect?: () => void;\n}) {\n const {user, workspaces} = useAuthState();\n const navigate = useNavigate();\n const setLastWorkspaceId = useSetAtom(lastWorkspaceIdAtom);\n const selectWorkspace = (wid: string) => {\n try {\n setLastWorkspaceId(wid);\n if (user?.id) rememberLastWorkspaceId(user.id, wid);\n } catch {\n // Local storage is best effort.\n }\n navigate({to: '/workspaces/$wid', params: {wid}});\n onSelect?.();\n };\n return (\n <Command>\n <CommandInput placeholder=\"Search workspaces...\" />\n <CommandList className=\"max-h-none overflow-visible overflow-y-visible p-0\">\n <div className=\"max-h-300 overflow-y-auto overflow-x-hidden p-4 scrollbar\">\n <CommandEmpty>No workspaces found.</CommandEmpty>\n <CommandGroup heading=\"Workspaces\">\n {workspaces.map((workspace) => (\n <CommandItem\n key={workspace.id}\n value={workspace.id}\n keywords={[workspace.name]}\n onSelect={() => selectWorkspace(workspace.id)}\n >\n <Icon\n name=\"check\"\n className={`size-16 mr-8 ${activeWorkspaceId === workspace.id ? 'opacity-100' : 'opacity-0'}`}\n />\n {workspace.name}\n </CommandItem>\n ))}\n </CommandGroup>\n </div>\n <CommandSeparator alwaysRender className=\"mx-0\" />\n <CommandGroup forceMount className=\"p-4\">\n <CommandItem\n value=\"__create\"\n onSelect={() => {\n navigate({to: '/setup/workspaces/new' as never});\n onSelect?.();\n }}\n forceMount\n >\n <Icon name=\"addLine\" className=\"size-16\" />\n Create workspace\n </CommandItem>\n </CommandGroup>\n </CommandList>\n </Command>\n );\n}\n"],"names":["Command","CommandEmpty","CommandGroup","CommandInput","CommandItem","CommandList","CommandSeparator","Icon","useNavigate","useSetAtom","useAuthState","lastWorkspaceIdAtom","rememberLastWorkspaceId","WorkspaceSwitcher","activeWorkspaceId","onSelect","user","workspaces","navigate","setLastWorkspaceId","selectWorkspace","wid","id","to","params","placeholder","className","div","heading","map","workspace","value","keywords","name","alwaysRender","forceMount"],"mappings":";AAAA,SACEA,OAAO,EACPC,YAAY,EACZC,YAAY,EACZC,YAAY,EACZC,WAAW,EACXC,WAAW,EACXC,gBAAgB,QACX,4BAA4B;AACnC,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,WAAW,QAAO,yBAAyB;AACnD,SAAQC,UAAU,QAAO,QAAQ;AACjC,SAAQC,YAAY,QAAO,mBAAmB;AAC9C,SAAQC,mBAAmB,EAAEC,uBAAuB,QAAO,6BAA6B;AAExF,OAAO,SAASC,kBAAkB,EAChCC,iBAAiB,EACjBC,QAAQ,EAIT;IACC,MAAM,EAACC,IAAI,EAAEC,UAAU,EAAC,GAAGP;IAC3B,MAAMQ,WAAWV;IACjB,MAAMW,qBAAqBV,WAAWE;IACtC,MAAMS,kBAAkB,CAACC;QACvB,IAAI;YACFF,mBAAmBE;YACnB,IAAIL,MAAMM,IAAIV,wBAAwBI,KAAKM,EAAE,EAAED;QACjD,EAAE,OAAM;QACN,gCAAgC;QAClC;QACAH,SAAS;YAACK,IAAI;YAAoBC,QAAQ;gBAACH;YAAG;QAAC;QAC/CN;IACF;IACA,qBACE,MAACf;;0BACC,KAACG;gBAAasB,aAAY;;0BAC1B,MAACpB;gBAAYqB,WAAU;;kCACrB,MAACC;wBAAID,WAAU;;0CACb,KAACzB;0CAAa;;0CACd,KAACC;gCAAa0B,SAAQ;0CACnBX,WAAWY,GAAG,CAAC,CAACC,0BACf,MAAC1B;wCAEC2B,OAAOD,UAAUR,EAAE;wCACnBU,UAAU;4CAACF,UAAUG,IAAI;yCAAC;wCAC1BlB,UAAU,IAAMK,gBAAgBU,UAAUR,EAAE;;0DAE5C,KAACf;gDACC0B,MAAK;gDACLP,WAAW,CAAC,aAAa,EAAEZ,sBAAsBgB,UAAUR,EAAE,GAAG,gBAAgB,aAAa;;4CAE9FQ,UAAUG,IAAI;;uCATVH,UAAUR,EAAE;;;;kCAczB,KAAChB;wBAAiB4B,YAAY;wBAACR,WAAU;;kCACzC,KAACxB;wBAAaiC,UAAU;wBAACT,WAAU;kCACjC,cAAA,MAACtB;4BACC2B,OAAM;4BACNhB,UAAU;gCACRG,SAAS;oCAACK,IAAI;gCAAgC;gCAC9CR;4BACF;4BACAoB,UAAU;;8CAEV,KAAC5B;oCAAK0B,MAAK;oCAAUP,WAAU;;gCAAY;;;;;;;;AAOvD"}
@@ -0,0 +1,11 @@
1
+ import type { ClientFeature, NavTabEntry, SettingsSectionEntry } from '#contract.js';
2
+ import { type ComposedRoute } from './compose-routes.js';
3
+ import { mergeConfigShapes } from './merge-config.js';
4
+ export interface ComposedClientFeatures {
5
+ configShape: ReturnType<typeof mergeConfigShapes>;
6
+ navigation: NavTabEntry[];
7
+ routes: ComposedRoute[];
8
+ settingsSections: SettingsSectionEntry[];
9
+ }
10
+ export declare function composeClientFeatures(features: readonly ClientFeature[]): ComposedClientFeatures;
11
+ //# sourceMappingURL=compose-client-features.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose-client-features.d.ts","sourceRoot":"","sources":["../../src/compose/compose-client-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,WAAW,EAAE,oBAAoB,EAAC,MAAM,cAAc,CAAC;AAEnF,OAAO,EAAC,KAAK,aAAa,EAAgB,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAC,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAIpD,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;IAClD,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;CAC1C;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,sBAAsB,CAYhG"}
@@ -0,0 +1,19 @@
1
+ import { navigationEntries, settingsEntries } from '#runtime/registries.js';
2
+ import { composeRoutes } from './compose-routes.js';
3
+ import { mergeConfigShapes } from './merge-config.js';
4
+ import { validateProviderIds } from './validate-providers.js';
5
+ import { validateNavigation, validateSettingsSections } from './validate-registries.js';
6
+ export function composeClientFeatures(features) {
7
+ const routes = composeRoutes(features);
8
+ validateProviderIds(features);
9
+ validateNavigation(features, routes);
10
+ validateSettingsSections(features, routes);
11
+ return {
12
+ configShape: mergeConfigShapes(features),
13
+ navigation: navigationEntries(features),
14
+ routes,
15
+ settingsSections: settingsEntries(features)
16
+ };
17
+ }
18
+
19
+ //# sourceMappingURL=compose-client-features.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/compose/compose-client-features.ts"],"sourcesContent":["import type {ClientFeature, NavTabEntry, SettingsSectionEntry} from '#contract.js';\nimport {navigationEntries, settingsEntries} from '#runtime/registries.js';\nimport {type ComposedRoute, composeRoutes} from './compose-routes.js';\nimport {mergeConfigShapes} from './merge-config.js';\nimport {validateProviderIds} from './validate-providers.js';\nimport {validateNavigation, validateSettingsSections} from './validate-registries.js';\n\nexport interface ComposedClientFeatures {\n configShape: ReturnType<typeof mergeConfigShapes>;\n navigation: NavTabEntry[];\n routes: ComposedRoute[];\n settingsSections: SettingsSectionEntry[];\n}\n\nexport function composeClientFeatures(features: readonly ClientFeature[]): ComposedClientFeatures {\n const routes = composeRoutes(features);\n validateProviderIds(features);\n validateNavigation(features, routes);\n validateSettingsSections(features, routes);\n\n return {\n configShape: mergeConfigShapes(features),\n navigation: navigationEntries(features),\n routes,\n settingsSections: settingsEntries(features),\n };\n}\n"],"names":["navigationEntries","settingsEntries","composeRoutes","mergeConfigShapes","validateProviderIds","validateNavigation","validateSettingsSections","composeClientFeatures","features","routes","configShape","navigation","settingsSections"],"mappings":"AACA,SAAQA,iBAAiB,EAAEC,eAAe,QAAO,yBAAyB;AAC1E,SAA4BC,aAAa,QAAO,sBAAsB;AACtE,SAAQC,iBAAiB,QAAO,oBAAoB;AACpD,SAAQC,mBAAmB,QAAO,0BAA0B;AAC5D,SAAQC,kBAAkB,EAAEC,wBAAwB,QAAO,2BAA2B;AAStF,OAAO,SAASC,sBAAsBC,QAAkC;IACtE,MAAMC,SAASP,cAAcM;IAC7BJ,oBAAoBI;IACpBH,mBAAmBG,UAAUC;IAC7BH,yBAAyBE,UAAUC;IAEnC,OAAO;QACLC,aAAaP,kBAAkBK;QAC/BG,YAAYX,kBAAkBQ;QAC9BC;QACAG,kBAAkBX,gBAAgBO;IACpC;AACF"}
@@ -1,6 +1,7 @@
1
1
  import type { ClientFeature, RouteContribution } from '#contract.js';
2
2
  export interface ComposedRoute extends RouteContribution {
3
3
  featureId: string;
4
+ ownerFeatureId: string;
4
5
  }
5
6
  export declare function composeRoutes(features: readonly ClientFeature[]): ComposedRoute[];
6
7
  //# sourceMappingURL=compose-routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compose-routes.d.ts","sourceRoot":"","sources":["../../src/compose/compose-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,cAAc,CAAC;AAInE,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,EAAE,CAmDjF"}
1
+ {"version":3,"file":"compose-routes.d.ts","sourceRoot":"","sources":["../../src/compose/compose-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,cAAc,CAAC;AAInE,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,EAAE,CAqDjF"}
@@ -17,7 +17,8 @@ export function composeRoutes(features) {
17
17
  }
18
18
  routes.set(normalizedContribution.path, {
19
19
  ...normalizedContribution,
20
- featureId: feature.id
20
+ featureId: feature.id,
21
+ ownerFeatureId: feature.id
21
22
  });
22
23
  continue;
23
24
  }
@@ -41,7 +42,8 @@ export function composeRoutes(features) {
41
42
  }
42
43
  routes.set(normalizedContribution.path, {
43
44
  ...normalizedContribution,
44
- featureId: feature.id
45
+ featureId: feature.id,
46
+ ownerFeatureId: existing.ownerFeatureId
45
47
  });
46
48
  }
47
49
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/compose/compose-routes.ts"],"sourcesContent":["import type {ClientFeature, RouteContribution} from '#contract.js';\nimport {RouteCompositionError} from './errors.js';\nimport {normalizeRoutePath} from './normalize-route-path.js';\n\nexport interface ComposedRoute extends RouteContribution {\n featureId: string;\n}\n\nexport function composeRoutes(features: readonly ClientFeature[]): ComposedRoute[] {\n const routes = new Map<string, ComposedRoute>();\n for (const feature of features) {\n for (const contribution of feature.routes ?? []) {\n const normalizedContribution = {\n ...contribution,\n path: normalizeRoutePath(contribution.path),\n };\n const existing = routes.get(normalizedContribution.path);\n if (!existing) {\n if (normalizedContribution.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" has no route to replace.`,\n [feature.id],\n );\n }\n routes.set(normalizedContribution.path, {\n ...normalizedContribution,\n featureId: feature.id,\n });\n continue;\n }\n if (!normalizedContribution.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route \"${normalizedContribution.path}\" is contributed by both features \"${existing.featureId}\" and \"${feature.id}\". Set override: true to replace it explicitly.`,\n [existing.featureId, feature.id],\n );\n }\n if (existing.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route \"${normalizedContribution.path}\" has competing overrides from features \"${existing.featureId}\" and \"${feature.id}\".`,\n [existing.featureId, feature.id],\n );\n }\n if (existing.parent !== normalizedContribution.parent) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" cannot change anchor from \"${existing.parent}\" in feature \"${existing.featureId}\" to \"${normalizedContribution.parent}\".`,\n [existing.featureId, feature.id],\n );\n }\n routes.set(normalizedContribution.path, {\n ...normalizedContribution,\n featureId: feature.id,\n });\n }\n }\n return [...routes.values()];\n}\n"],"names":["RouteCompositionError","normalizeRoutePath","composeRoutes","features","routes","Map","feature","contribution","normalizedContribution","path","existing","get","override","id","set","featureId","parent","values"],"mappings":"AACA,SAAQA,qBAAqB,QAAO,cAAc;AAClD,SAAQC,kBAAkB,QAAO,4BAA4B;AAM7D,OAAO,SAASC,cAAcC,QAAkC;IAC9D,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAMC,WAAWH,SAAU;QAC9B,KAAK,MAAMI,gBAAgBD,QAAQF,MAAM,IAAI,EAAE,CAAE;YAC/C,MAAMI,yBAAyB;gBAC7B,GAAGD,YAAY;gBACfE,MAAMR,mBAAmBM,aAAaE,IAAI;YAC5C;YACA,MAAMC,WAAWN,OAAOO,GAAG,CAACH,uBAAuBC,IAAI;YACvD,IAAI,CAACC,UAAU;gBACb,IAAIF,uBAAuBI,QAAQ,EAAE;oBACnC,MAAM,IAAIZ,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,oBAAoB,EAAED,uBAAuBC,IAAI,CAAC,gBAAgB,EAAEH,QAAQO,EAAE,CAAC,0BAA0B,CAAC,EAC3G;wBAACP,QAAQO,EAAE;qBAAC;gBAEhB;gBACAT,OAAOU,GAAG,CAACN,uBAAuBC,IAAI,EAAE;oBACtC,GAAGD,sBAAsB;oBACzBO,WAAWT,QAAQO,EAAE;gBACvB;gBACA;YACF;YACA,IAAI,CAACL,uBAAuBI,QAAQ,EAAE;gBACpC,MAAM,IAAIZ,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,OAAO,EAAED,uBAAuBC,IAAI,CAAC,mCAAmC,EAAEC,SAASK,SAAS,CAAC,OAAO,EAAET,QAAQO,EAAE,CAAC,+CAA+C,CAAC,EAClK;oBAACH,SAASK,SAAS;oBAAET,QAAQO,EAAE;iBAAC;YAEpC;YACA,IAAIH,SAASE,QAAQ,EAAE;gBACrB,MAAM,IAAIZ,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,OAAO,EAAED,uBAAuBC,IAAI,CAAC,yCAAyC,EAAEC,SAASK,SAAS,CAAC,OAAO,EAAET,QAAQO,EAAE,CAAC,EAAE,CAAC,EAC3H;oBAACH,SAASK,SAAS;oBAAET,QAAQO,EAAE;iBAAC;YAEpC;YACA,IAAIH,SAASM,MAAM,KAAKR,uBAAuBQ,MAAM,EAAE;gBACrD,MAAM,IAAIhB,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,oBAAoB,EAAED,uBAAuBC,IAAI,CAAC,gBAAgB,EAAEH,QAAQO,EAAE,CAAC,6BAA6B,EAAEH,SAASM,MAAM,CAAC,cAAc,EAAEN,SAASK,SAAS,CAAC,MAAM,EAAEP,uBAAuBQ,MAAM,CAAC,EAAE,CAAC,EAC3M;oBAACN,SAASK,SAAS;oBAAET,QAAQO,EAAE;iBAAC;YAEpC;YACAT,OAAOU,GAAG,CAACN,uBAAuBC,IAAI,EAAE;gBACtC,GAAGD,sBAAsB;gBACzBO,WAAWT,QAAQO,EAAE;YACvB;QACF;IACF;IACA,OAAO;WAAIT,OAAOa,MAAM;KAAG;AAC7B"}
1
+ {"version":3,"sources":["../../src/compose/compose-routes.ts"],"sourcesContent":["import type {ClientFeature, RouteContribution} from '#contract.js';\nimport {RouteCompositionError} from './errors.js';\nimport {normalizeRoutePath} from './normalize-route-path.js';\n\nexport interface ComposedRoute extends RouteContribution {\n featureId: string;\n ownerFeatureId: string;\n}\n\nexport function composeRoutes(features: readonly ClientFeature[]): ComposedRoute[] {\n const routes = new Map<string, ComposedRoute>();\n for (const feature of features) {\n for (const contribution of feature.routes ?? []) {\n const normalizedContribution = {\n ...contribution,\n path: normalizeRoutePath(contribution.path),\n };\n const existing = routes.get(normalizedContribution.path);\n if (!existing) {\n if (normalizedContribution.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" has no route to replace.`,\n [feature.id],\n );\n }\n routes.set(normalizedContribution.path, {\n ...normalizedContribution,\n featureId: feature.id,\n ownerFeatureId: feature.id,\n });\n continue;\n }\n if (!normalizedContribution.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route \"${normalizedContribution.path}\" is contributed by both features \"${existing.featureId}\" and \"${feature.id}\". Set override: true to replace it explicitly.`,\n [existing.featureId, feature.id],\n );\n }\n if (existing.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route \"${normalizedContribution.path}\" has competing overrides from features \"${existing.featureId}\" and \"${feature.id}\".`,\n [existing.featureId, feature.id],\n );\n }\n if (existing.parent !== normalizedContribution.parent) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" cannot change anchor from \"${existing.parent}\" in feature \"${existing.featureId}\" to \"${normalizedContribution.parent}\".`,\n [existing.featureId, feature.id],\n );\n }\n routes.set(normalizedContribution.path, {\n ...normalizedContribution,\n featureId: feature.id,\n ownerFeatureId: existing.ownerFeatureId,\n });\n }\n }\n return [...routes.values()];\n}\n"],"names":["RouteCompositionError","normalizeRoutePath","composeRoutes","features","routes","Map","feature","contribution","normalizedContribution","path","existing","get","override","id","set","featureId","ownerFeatureId","parent","values"],"mappings":"AACA,SAAQA,qBAAqB,QAAO,cAAc;AAClD,SAAQC,kBAAkB,QAAO,4BAA4B;AAO7D,OAAO,SAASC,cAAcC,QAAkC;IAC9D,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAMC,WAAWH,SAAU;QAC9B,KAAK,MAAMI,gBAAgBD,QAAQF,MAAM,IAAI,EAAE,CAAE;YAC/C,MAAMI,yBAAyB;gBAC7B,GAAGD,YAAY;gBACfE,MAAMR,mBAAmBM,aAAaE,IAAI;YAC5C;YACA,MAAMC,WAAWN,OAAOO,GAAG,CAACH,uBAAuBC,IAAI;YACvD,IAAI,CAACC,UAAU;gBACb,IAAIF,uBAAuBI,QAAQ,EAAE;oBACnC,MAAM,IAAIZ,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,oBAAoB,EAAED,uBAAuBC,IAAI,CAAC,gBAAgB,EAAEH,QAAQO,EAAE,CAAC,0BAA0B,CAAC,EAC3G;wBAACP,QAAQO,EAAE;qBAAC;gBAEhB;gBACAT,OAAOU,GAAG,CAACN,uBAAuBC,IAAI,EAAE;oBACtC,GAAGD,sBAAsB;oBACzBO,WAAWT,QAAQO,EAAE;oBACrBG,gBAAgBV,QAAQO,EAAE;gBAC5B;gBACA;YACF;YACA,IAAI,CAACL,uBAAuBI,QAAQ,EAAE;gBACpC,MAAM,IAAIZ,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,OAAO,EAAED,uBAAuBC,IAAI,CAAC,mCAAmC,EAAEC,SAASK,SAAS,CAAC,OAAO,EAAET,QAAQO,EAAE,CAAC,+CAA+C,CAAC,EAClK;oBAACH,SAASK,SAAS;oBAAET,QAAQO,EAAE;iBAAC;YAEpC;YACA,IAAIH,SAASE,QAAQ,EAAE;gBACrB,MAAM,IAAIZ,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,OAAO,EAAED,uBAAuBC,IAAI,CAAC,yCAAyC,EAAEC,SAASK,SAAS,CAAC,OAAO,EAAET,QAAQO,EAAE,CAAC,EAAE,CAAC,EAC3H;oBAACH,SAASK,SAAS;oBAAET,QAAQO,EAAE;iBAAC;YAEpC;YACA,IAAIH,SAASO,MAAM,KAAKT,uBAAuBS,MAAM,EAAE;gBACrD,MAAM,IAAIjB,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,oBAAoB,EAAED,uBAAuBC,IAAI,CAAC,gBAAgB,EAAEH,QAAQO,EAAE,CAAC,6BAA6B,EAAEH,SAASO,MAAM,CAAC,cAAc,EAAEP,SAASK,SAAS,CAAC,MAAM,EAAEP,uBAAuBS,MAAM,CAAC,EAAE,CAAC,EAC3M;oBAACP,SAASK,SAAS;oBAAET,QAAQO,EAAE;iBAAC;YAEpC;YACAT,OAAOU,GAAG,CAACN,uBAAuBC,IAAI,EAAE;gBACtC,GAAGD,sBAAsB;gBACzBO,WAAWT,QAAQO,EAAE;gBACrBG,gBAAgBN,SAASM,cAAc;YACzC;QACF;IACF;IACA,OAAO;WAAIZ,OAAOc,MAAM;KAAG;AAC7B"}
@@ -1,4 +1,11 @@
1
1
  import type { ClientFeature } from '#contract.js';
2
- export declare function validateNavigation(features: readonly ClientFeature[], routePaths: Iterable<string>): void;
3
- export declare function validateSettingsSections(features: readonly ClientFeature[], routePaths: Iterable<string>): void;
2
+ interface RouteReference {
3
+ path: string;
4
+ featureId?: string;
5
+ ownerFeatureId?: string;
6
+ }
7
+ type RouteReferences = Iterable<string | RouteReference>;
8
+ export declare function validateNavigation(features: readonly ClientFeature[], routeReferences: RouteReferences): void;
9
+ export declare function validateSettingsSections(features: readonly ClientFeature[], routeReferences: RouteReferences): void;
10
+ export {};
4
11
  //# sourceMappingURL=validate-registries.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-registries.d.ts","sourceRoot":"","sources":["../../src/compose/validate-registries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAIhD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,GAC3B,IAAI,CAwBN;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,GAC3B,IAAI,CAwBN"}
1
+ {"version":3,"file":"validate-registries.d.ts","sourceRoot":"","sources":["../../src/compose/validate-registries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAIhD,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,KAAK,eAAe,GAAG,QAAQ,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;AAkBzD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,EAAE,eAAe,GAC/B,IAAI,CAgCN;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,EAAE,eAAe,GAC/B,IAAI,CAiCN"}
@@ -1,9 +1,18 @@
1
1
  import { NavCompositionError, SettingsCompositionError } from './errors.js';
2
2
  import { normalizeRoutePath } from './normalize-route-path.js';
3
- export function validateNavigation(features, routePaths) {
4
- const routes = new Set([
5
- ...routePaths
6
- ].map(normalizeRoutePath));
3
+ function routeOwners(routeReferences) {
4
+ const owners = new Map();
5
+ for (const route of routeReferences){
6
+ const path = typeof route === 'string' ? route : route.path;
7
+ owners.set(normalizeRoutePath(path), typeof route === 'string' ? undefined : route.ownerFeatureId ?? route.featureId);
8
+ }
9
+ return owners;
10
+ }
11
+ function hasExplicitCoordinator(feature) {
12
+ return feature.coordinator === feature.id;
13
+ }
14
+ export function validateNavigation(features, routeReferences) {
15
+ const routes = routeOwners(routeReferences);
7
16
  const entries = new Map();
8
17
  for (const feature of features){
9
18
  for (const entry of feature.navigation ?? []){
@@ -15,17 +24,24 @@ export function validateNavigation(features, routePaths) {
15
24
  ]);
16
25
  }
17
26
  const target = normalizeRoutePath(entry.to);
18
- if (!routes.has(target)) {
27
+ const routeOwner = routes.get(target);
28
+ if (routeOwner === undefined && !routes.has(target)) {
19
29
  throw new NavCompositionError(entry.id, `Navigation entry "${entry.id}" in feature "${feature.id}" targets missing route "${target}".`, [
20
30
  feature.id
21
31
  ]);
22
32
  }
33
+ if (routeOwner && routeOwner !== feature.id && !hasExplicitCoordinator(feature)) {
34
+ throw new NavCompositionError(entry.id, `Navigation entry "${entry.id}" in feature "${feature.id}" targets route "${target}" owned by feature "${routeOwner}". Declare coordinator: "${feature.id}" to own this cross-feature contribution.`, [
35
+ routeOwner,
36
+ feature.id
37
+ ]);
38
+ }
23
39
  entries.set(entry.id, feature.id);
24
40
  }
25
41
  }
26
42
  }
27
- export function validateSettingsSections(features, routePaths) {
28
- const routes = new Set(routePaths);
43
+ export function validateSettingsSections(features, routeReferences) {
44
+ const routes = routeOwners(routeReferences);
29
45
  const sections = new Map();
30
46
  for (const feature of features){
31
47
  for (const section of feature.settingsSections ?? []){
@@ -37,11 +53,19 @@ export function validateSettingsSections(features, routePaths) {
37
53
  ]);
38
54
  }
39
55
  const path = `/workspaces/$wid/settings/${section.pathSegment}`;
40
- if (!routes.has(path)) {
56
+ const normalizedPath = normalizeRoutePath(path);
57
+ const routeOwner = routes.get(normalizedPath);
58
+ if (routeOwner === undefined && !routes.has(normalizedPath)) {
41
59
  throw new SettingsCompositionError(section.id, `Settings section "${section.id}" in feature "${feature.id}" requires route "${path}".`, [
42
60
  feature.id
43
61
  ]);
44
62
  }
63
+ if (routeOwner && routeOwner !== feature.id && !hasExplicitCoordinator(feature)) {
64
+ throw new SettingsCompositionError(section.id, `Settings section "${section.id}" in feature "${feature.id}" targets route "${path}" owned by feature "${routeOwner}". Declare coordinator: "${feature.id}" to own this cross-feature contribution.`, [
65
+ routeOwner,
66
+ feature.id
67
+ ]);
68
+ }
45
69
  sections.set(section.id, feature.id);
46
70
  }
47
71
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/compose/validate-registries.ts"],"sourcesContent":["import type {ClientFeature} from '#contract.js';\nimport {NavCompositionError, SettingsCompositionError} from './errors.js';\nimport {normalizeRoutePath} from './normalize-route-path.js';\n\nexport function validateNavigation(\n features: readonly ClientFeature[],\n routePaths: Iterable<string>,\n): void {\n const routes = new Set([...routePaths].map(normalizeRoutePath));\n const entries = new Map<string, string>();\n for (const feature of features) {\n for (const entry of feature.navigation ?? []) {\n const existingFeatureId = entries.get(entry.id);\n if (existingFeatureId) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" is contributed by both features \"${existingFeatureId}\" and \"${feature.id}\".`,\n [existingFeatureId, feature.id],\n );\n }\n const target = normalizeRoutePath(entry.to);\n if (!routes.has(target)) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets missing route \"${target}\".`,\n [feature.id],\n );\n }\n entries.set(entry.id, feature.id);\n }\n }\n}\n\nexport function validateSettingsSections(\n features: readonly ClientFeature[],\n routePaths: Iterable<string>,\n): void {\n const routes = new Set(routePaths);\n const sections = new Map<string, string>();\n for (const feature of features) {\n for (const section of feature.settingsSections ?? []) {\n const existingFeatureId = sections.get(section.id);\n if (existingFeatureId) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" is contributed by both features \"${existingFeatureId}\" and \"${feature.id}\".`,\n [existingFeatureId, feature.id],\n );\n }\n const path = `/workspaces/$wid/settings/${section.pathSegment}`;\n if (!routes.has(path)) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" in feature \"${feature.id}\" requires route \"${path}\".`,\n [feature.id],\n );\n }\n sections.set(section.id, feature.id);\n }\n }\n}\n"],"names":["NavCompositionError","SettingsCompositionError","normalizeRoutePath","validateNavigation","features","routePaths","routes","Set","map","entries","Map","feature","entry","navigation","existingFeatureId","get","id","target","to","has","set","validateSettingsSections","sections","section","settingsSections","path","pathSegment"],"mappings":"AACA,SAAQA,mBAAmB,EAAEC,wBAAwB,QAAO,cAAc;AAC1E,SAAQC,kBAAkB,QAAO,4BAA4B;AAE7D,OAAO,SAASC,mBACdC,QAAkC,EAClCC,UAA4B;IAE5B,MAAMC,SAAS,IAAIC,IAAI;WAAIF;KAAW,CAACG,GAAG,CAACN;IAC3C,MAAMO,UAAU,IAAIC;IACpB,KAAK,MAAMC,WAAWP,SAAU;QAC9B,KAAK,MAAMQ,SAASD,QAAQE,UAAU,IAAI,EAAE,CAAE;YAC5C,MAAMC,oBAAoBL,QAAQM,GAAG,CAACH,MAAMI,EAAE;YAC9C,IAAIF,mBAAmB;gBACrB,MAAM,IAAId,oBACRY,MAAMI,EAAE,EACR,CAAC,kBAAkB,EAAEJ,MAAMI,EAAE,CAAC,mCAAmC,EAAEF,kBAAkB,OAAO,EAAEH,QAAQK,EAAE,CAAC,EAAE,CAAC,EAC5G;oBAACF;oBAAmBH,QAAQK,EAAE;iBAAC;YAEnC;YACA,MAAMC,SAASf,mBAAmBU,MAAMM,EAAE;YAC1C,IAAI,CAACZ,OAAOa,GAAG,CAACF,SAAS;gBACvB,MAAM,IAAIjB,oBACRY,MAAMI,EAAE,EACR,CAAC,kBAAkB,EAAEJ,MAAMI,EAAE,CAAC,cAAc,EAAEL,QAAQK,EAAE,CAAC,yBAAyB,EAAEC,OAAO,EAAE,CAAC,EAC9F;oBAACN,QAAQK,EAAE;iBAAC;YAEhB;YACAP,QAAQW,GAAG,CAACR,MAAMI,EAAE,EAAEL,QAAQK,EAAE;QAClC;IACF;AACF;AAEA,OAAO,SAASK,yBACdjB,QAAkC,EAClCC,UAA4B;IAE5B,MAAMC,SAAS,IAAIC,IAAIF;IACvB,MAAMiB,WAAW,IAAIZ;IACrB,KAAK,MAAMC,WAAWP,SAAU;QAC9B,KAAK,MAAMmB,WAAWZ,QAAQa,gBAAgB,IAAI,EAAE,CAAE;YACpD,MAAMV,oBAAoBQ,SAASP,GAAG,CAACQ,QAAQP,EAAE;YACjD,IAAIF,mBAAmB;gBACrB,MAAM,IAAIb,yBACRsB,QAAQP,EAAE,EACV,CAAC,kBAAkB,EAAEO,QAAQP,EAAE,CAAC,mCAAmC,EAAEF,kBAAkB,OAAO,EAAEH,QAAQK,EAAE,CAAC,EAAE,CAAC,EAC9G;oBAACF;oBAAmBH,QAAQK,EAAE;iBAAC;YAEnC;YACA,MAAMS,OAAO,CAAC,0BAA0B,EAAEF,QAAQG,WAAW,EAAE;YAC/D,IAAI,CAACpB,OAAOa,GAAG,CAACM,OAAO;gBACrB,MAAM,IAAIxB,yBACRsB,QAAQP,EAAE,EACV,CAAC,kBAAkB,EAAEO,QAAQP,EAAE,CAAC,cAAc,EAAEL,QAAQK,EAAE,CAAC,kBAAkB,EAAES,KAAK,EAAE,CAAC,EACvF;oBAACd,QAAQK,EAAE;iBAAC;YAEhB;YACAM,SAASF,GAAG,CAACG,QAAQP,EAAE,EAAEL,QAAQK,EAAE;QACrC;IACF;AACF"}
1
+ {"version":3,"sources":["../../src/compose/validate-registries.ts"],"sourcesContent":["import type {ClientFeature} from '#contract.js';\nimport {NavCompositionError, SettingsCompositionError} from './errors.js';\nimport {normalizeRoutePath} from './normalize-route-path.js';\n\ninterface RouteReference {\n path: string;\n featureId?: string;\n ownerFeatureId?: string;\n}\n\ntype RouteReferences = Iterable<string | RouteReference>;\n\nfunction routeOwners(routeReferences: RouteReferences): Map<string, string | undefined> {\n const owners = new Map<string, string | undefined>();\n for (const route of routeReferences) {\n const path = typeof route === 'string' ? route : route.path;\n owners.set(\n normalizeRoutePath(path),\n typeof route === 'string' ? undefined : (route.ownerFeatureId ?? route.featureId),\n );\n }\n return owners;\n}\n\nfunction hasExplicitCoordinator(feature: ClientFeature): boolean {\n return feature.coordinator === feature.id;\n}\n\nexport function validateNavigation(\n features: readonly ClientFeature[],\n routeReferences: RouteReferences,\n): void {\n const routes = routeOwners(routeReferences);\n const entries = new Map<string, string>();\n for (const feature of features) {\n for (const entry of feature.navigation ?? []) {\n const existingFeatureId = entries.get(entry.id);\n if (existingFeatureId) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" is contributed by both features \"${existingFeatureId}\" and \"${feature.id}\".`,\n [existingFeatureId, feature.id],\n );\n }\n const target = normalizeRoutePath(entry.to);\n const routeOwner = routes.get(target);\n if (routeOwner === undefined && !routes.has(target)) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets missing route \"${target}\".`,\n [feature.id],\n );\n }\n if (routeOwner && routeOwner !== feature.id && !hasExplicitCoordinator(feature)) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets route \"${target}\" owned by feature \"${routeOwner}\". Declare coordinator: \"${feature.id}\" to own this cross-feature contribution.`,\n [routeOwner, feature.id],\n );\n }\n entries.set(entry.id, feature.id);\n }\n }\n}\n\nexport function validateSettingsSections(\n features: readonly ClientFeature[],\n routeReferences: RouteReferences,\n): void {\n const routes = routeOwners(routeReferences);\n const sections = new Map<string, string>();\n for (const feature of features) {\n for (const section of feature.settingsSections ?? []) {\n const existingFeatureId = sections.get(section.id);\n if (existingFeatureId) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" is contributed by both features \"${existingFeatureId}\" and \"${feature.id}\".`,\n [existingFeatureId, feature.id],\n );\n }\n const path = `/workspaces/$wid/settings/${section.pathSegment}`;\n const normalizedPath = normalizeRoutePath(path);\n const routeOwner = routes.get(normalizedPath);\n if (routeOwner === undefined && !routes.has(normalizedPath)) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" in feature \"${feature.id}\" requires route \"${path}\".`,\n [feature.id],\n );\n }\n if (routeOwner && routeOwner !== feature.id && !hasExplicitCoordinator(feature)) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" in feature \"${feature.id}\" targets route \"${path}\" owned by feature \"${routeOwner}\". Declare coordinator: \"${feature.id}\" to own this cross-feature contribution.`,\n [routeOwner, feature.id],\n );\n }\n sections.set(section.id, feature.id);\n }\n }\n}\n"],"names":["NavCompositionError","SettingsCompositionError","normalizeRoutePath","routeOwners","routeReferences","owners","Map","route","path","set","undefined","ownerFeatureId","featureId","hasExplicitCoordinator","feature","coordinator","id","validateNavigation","features","routes","entries","entry","navigation","existingFeatureId","get","target","to","routeOwner","has","validateSettingsSections","sections","section","settingsSections","pathSegment","normalizedPath"],"mappings":"AACA,SAAQA,mBAAmB,EAAEC,wBAAwB,QAAO,cAAc;AAC1E,SAAQC,kBAAkB,QAAO,4BAA4B;AAU7D,SAASC,YAAYC,eAAgC;IACnD,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAMC,SAASH,gBAAiB;QACnC,MAAMI,OAAO,OAAOD,UAAU,WAAWA,QAAQA,MAAMC,IAAI;QAC3DH,OAAOI,GAAG,CACRP,mBAAmBM,OACnB,OAAOD,UAAU,WAAWG,YAAaH,MAAMI,cAAc,IAAIJ,MAAMK,SAAS;IAEpF;IACA,OAAOP;AACT;AAEA,SAASQ,uBAAuBC,OAAsB;IACpD,OAAOA,QAAQC,WAAW,KAAKD,QAAQE,EAAE;AAC3C;AAEA,OAAO,SAASC,mBACdC,QAAkC,EAClCd,eAAgC;IAEhC,MAAMe,SAAShB,YAAYC;IAC3B,MAAMgB,UAAU,IAAId;IACpB,KAAK,MAAMQ,WAAWI,SAAU;QAC9B,KAAK,MAAMG,SAASP,QAAQQ,UAAU,IAAI,EAAE,CAAE;YAC5C,MAAMC,oBAAoBH,QAAQI,GAAG,CAACH,MAAML,EAAE;YAC9C,IAAIO,mBAAmB;gBACrB,MAAM,IAAIvB,oBACRqB,MAAML,EAAE,EACR,CAAC,kBAAkB,EAAEK,MAAML,EAAE,CAAC,mCAAmC,EAAEO,kBAAkB,OAAO,EAAET,QAAQE,EAAE,CAAC,EAAE,CAAC,EAC5G;oBAACO;oBAAmBT,QAAQE,EAAE;iBAAC;YAEnC;YACA,MAAMS,SAASvB,mBAAmBmB,MAAMK,EAAE;YAC1C,MAAMC,aAAaR,OAAOK,GAAG,CAACC;YAC9B,IAAIE,eAAejB,aAAa,CAACS,OAAOS,GAAG,CAACH,SAAS;gBACnD,MAAM,IAAIzB,oBACRqB,MAAML,EAAE,EACR,CAAC,kBAAkB,EAAEK,MAAML,EAAE,CAAC,cAAc,EAAEF,QAAQE,EAAE,CAAC,yBAAyB,EAAES,OAAO,EAAE,CAAC,EAC9F;oBAACX,QAAQE,EAAE;iBAAC;YAEhB;YACA,IAAIW,cAAcA,eAAeb,QAAQE,EAAE,IAAI,CAACH,uBAAuBC,UAAU;gBAC/E,MAAM,IAAId,oBACRqB,MAAML,EAAE,EACR,CAAC,kBAAkB,EAAEK,MAAML,EAAE,CAAC,cAAc,EAAEF,QAAQE,EAAE,CAAC,iBAAiB,EAAES,OAAO,oBAAoB,EAAEE,WAAW,yBAAyB,EAAEb,QAAQE,EAAE,CAAC,yCAAyC,CAAC,EACpM;oBAACW;oBAAYb,QAAQE,EAAE;iBAAC;YAE5B;YACAI,QAAQX,GAAG,CAACY,MAAML,EAAE,EAAEF,QAAQE,EAAE;QAClC;IACF;AACF;AAEA,OAAO,SAASa,yBACdX,QAAkC,EAClCd,eAAgC;IAEhC,MAAMe,SAAShB,YAAYC;IAC3B,MAAM0B,WAAW,IAAIxB;IACrB,KAAK,MAAMQ,WAAWI,SAAU;QAC9B,KAAK,MAAMa,WAAWjB,QAAQkB,gBAAgB,IAAI,EAAE,CAAE;YACpD,MAAMT,oBAAoBO,SAASN,GAAG,CAACO,QAAQf,EAAE;YACjD,IAAIO,mBAAmB;gBACrB,MAAM,IAAItB,yBACR8B,QAAQf,EAAE,EACV,CAAC,kBAAkB,EAAEe,QAAQf,EAAE,CAAC,mCAAmC,EAAEO,kBAAkB,OAAO,EAAET,QAAQE,EAAE,CAAC,EAAE,CAAC,EAC9G;oBAACO;oBAAmBT,QAAQE,EAAE;iBAAC;YAEnC;YACA,MAAMR,OAAO,CAAC,0BAA0B,EAAEuB,QAAQE,WAAW,EAAE;YAC/D,MAAMC,iBAAiBhC,mBAAmBM;YAC1C,MAAMmB,aAAaR,OAAOK,GAAG,CAACU;YAC9B,IAAIP,eAAejB,aAAa,CAACS,OAAOS,GAAG,CAACM,iBAAiB;gBAC3D,MAAM,IAAIjC,yBACR8B,QAAQf,EAAE,EACV,CAAC,kBAAkB,EAAEe,QAAQf,EAAE,CAAC,cAAc,EAAEF,QAAQE,EAAE,CAAC,kBAAkB,EAAER,KAAK,EAAE,CAAC,EACvF;oBAACM,QAAQE,EAAE;iBAAC;YAEhB;YACA,IAAIW,cAAcA,eAAeb,QAAQE,EAAE,IAAI,CAACH,uBAAuBC,UAAU;gBAC/E,MAAM,IAAIb,yBACR8B,QAAQf,EAAE,EACV,CAAC,kBAAkB,EAAEe,QAAQf,EAAE,CAAC,cAAc,EAAEF,QAAQE,EAAE,CAAC,iBAAiB,EAAER,KAAK,oBAAoB,EAAEmB,WAAW,yBAAyB,EAAEb,QAAQE,EAAE,CAAC,yCAAyC,CAAC,EACpM;oBAACW;oBAAYb,QAAQE,EAAE;iBAAC;YAE5B;YACAc,SAASrB,GAAG,CAACsB,QAAQf,EAAE,EAAEF,QAAQE,EAAE;QACrC;IACF;AACF"}
@@ -29,6 +29,12 @@ export interface SettingsSectionEntry {
29
29
  }
30
30
  export interface ClientFeature<S extends z.ZodRawShape = z.ZodRawShape> {
31
31
  id: string;
32
+ /**
33
+ * Set this to the feature id when the feature intentionally coordinates a
34
+ * navigation or settings contribution whose route belongs to another
35
+ * feature.
36
+ */
37
+ coordinator?: string;
32
38
  routes?: readonly RouteContribution[];
33
39
  providers?: readonly FeatureProvider[];
34
40
  navigation?: readonly NavTabEntry[];
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAE3B,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,mBAAmB,CAAC;AAE1F,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW;IACpE,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACtC,SAAS,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACvC,UAAU,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACpC,gBAAgB,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACnD,WAAW,CAAC,EAAE,CAAC,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,CAAC,SAAS,aAAa,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAEhF"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAE3B,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,mBAAmB,CAAC;AAE1F,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW;IACpE,EAAE,EAAE,MAAM,CAAC;IACX;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACtC,SAAS,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACvC,UAAU,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACpC,gBAAgB,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACnD,WAAW,CAAC,EAAE,CAAC,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,CAAC,SAAS,aAAa,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAEhF"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/contract.ts"],"sourcesContent":["import type {IconName} from '@shipfox/react-ui/icon';\nimport type {ComponentType, PropsWithChildren} from 'react';\nimport type {z} from 'zod';\n\nexport type AnchorId = 'root' | 'workspaceLayout' | 'projectLayout' | 'workspaceSettings';\n\nexport interface RouteContribution {\n path: string;\n parent: AnchorId;\n override?: boolean;\n impl: string;\n}\n\nexport interface FeatureProvider {\n id: string;\n Component: ComponentType<PropsWithChildren>;\n}\n\nexport interface NavTabEntry {\n id: string;\n scope: 'workspace' | 'project';\n label: string;\n to: string;\n exact?: boolean;\n order?: number;\n}\n\nexport interface SettingsSectionEntry {\n id: string;\n pathSegment: string;\n label: string;\n icon: IconName;\n order?: number;\n}\n\nexport interface ClientFeature<S extends z.ZodRawShape = z.ZodRawShape> {\n id: string;\n routes?: readonly RouteContribution[];\n providers?: readonly FeatureProvider[];\n navigation?: readonly NavTabEntry[];\n settingsSections?: readonly SettingsSectionEntry[];\n configShape?: S;\n}\n\nexport function defineClientFeature<const T extends ClientFeature>(feature: T): T {\n return feature;\n}\n"],"names":["defineClientFeature","feature"],"mappings":"AA4CA,OAAO,SAASA,oBAAmDC,OAAU;IAC3E,OAAOA;AACT"}
1
+ {"version":3,"sources":["../src/contract.ts"],"sourcesContent":["import type {IconName} from '@shipfox/react-ui/icon';\nimport type {ComponentType, PropsWithChildren} from 'react';\nimport type {z} from 'zod';\n\nexport type AnchorId = 'root' | 'workspaceLayout' | 'projectLayout' | 'workspaceSettings';\n\nexport interface RouteContribution {\n path: string;\n parent: AnchorId;\n override?: boolean;\n impl: string;\n}\n\nexport interface FeatureProvider {\n id: string;\n Component: ComponentType<PropsWithChildren>;\n}\n\nexport interface NavTabEntry {\n id: string;\n scope: 'workspace' | 'project';\n label: string;\n to: string;\n exact?: boolean;\n order?: number;\n}\n\nexport interface SettingsSectionEntry {\n id: string;\n pathSegment: string;\n label: string;\n icon: IconName;\n order?: number;\n}\n\nexport interface ClientFeature<S extends z.ZodRawShape = z.ZodRawShape> {\n id: string;\n /**\n * Set this to the feature id when the feature intentionally coordinates a\n * navigation or settings contribution whose route belongs to another\n * feature.\n */\n coordinator?: string;\n routes?: readonly RouteContribution[];\n providers?: readonly FeatureProvider[];\n navigation?: readonly NavTabEntry[];\n settingsSections?: readonly SettingsSectionEntry[];\n configShape?: S;\n}\n\nexport function defineClientFeature<const T extends ClientFeature>(feature: T): T {\n return feature;\n}\n"],"names":["defineClientFeature","feature"],"mappings":"AAkDA,OAAO,SAASA,oBAAmDC,OAAU;IAC3E,OAAOA;AACT"}
@@ -0,0 +1,15 @@
1
+ import { type FetchQueryOptions } from '@tanstack/react-query';
2
+ import type { AuthenticatedSession, WorkspaceSummary } from '#core/session.js';
3
+ export interface UserWorkspaces {
4
+ memberships: WorkspaceSummary[];
5
+ }
6
+ export declare const authRefreshQueryKey: readonly ["auth", "refresh"];
7
+ export declare const userWorkspacesQueryKey: readonly ["workspaces", "mine"];
8
+ type AuthRefreshQueryOptions = FetchQueryOptions<AuthenticatedSession, Error, AuthenticatedSession, typeof authRefreshQueryKey>;
9
+ type UserWorkspacesQueryOptions = FetchQueryOptions<UserWorkspaces, Error, UserWorkspaces, typeof userWorkspacesQueryKey>;
10
+ export declare function listUserWorkspaces(token?: string, signal?: AbortSignal): Promise<UserWorkspaces>;
11
+ export declare function authRefreshQueryOptions(): AuthRefreshQueryOptions;
12
+ export declare function userWorkspacesQueryOptions(token?: string): UserWorkspacesQueryOptions;
13
+ export declare function refreshAuthenticatedSession(signal?: AbortSignal): Promise<AuthenticatedSession>;
14
+ export {};
15
+ //# sourceMappingURL=session-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/session-auth.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,KAAK,iBAAiB,EAAe,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAC,oBAAoB,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAG7E,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,gBAAgB,EAAE,CAAC;CACjC;AAED,eAAO,MAAM,mBAAmB,8BAA+B,CAAC;AAChE,eAAO,MAAM,sBAAsB,iCAAkC,CAAC;AAEtE,KAAK,uBAAuB,GAAG,iBAAiB,CAC9C,oBAAoB,EACpB,KAAK,EACL,oBAAoB,EACpB,OAAO,mBAAmB,CAC3B,CAAC;AAEF,KAAK,0BAA0B,GAAG,iBAAiB,CACjD,cAAc,EACd,KAAK,EACL,cAAc,EACd,OAAO,sBAAsB,CAC9B,CAAC;AAEF,wBAAsB,kBAAkB,CACtC,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAazB;AAED,wBAAgB,uBAAuB,IAAI,uBAAuB,CAOjE;AAED,wBAAgB,0BAA0B,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,0BAA0B,CAOrF;AAED,wBAAsB,2BAA2B,CAC/C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAM/B"}
@@ -0,0 +1,55 @@
1
+ import { loginResponseSchema } from '@shipfox/api-auth-dto';
2
+ import { listUserWorkspacesResponseSchema } from '@shipfox/api-workspaces-dto';
3
+ import { checkedApiRequest } from '@shipfox/client-api';
4
+ import { queryOptions } from '@tanstack/react-query';
5
+ import { toAuthenticatedSession } from './session-mapper.js';
6
+ export const authRefreshQueryKey = [
7
+ 'auth',
8
+ 'refresh'
9
+ ];
10
+ export const userWorkspacesQueryKey = [
11
+ 'workspaces',
12
+ 'mine'
13
+ ];
14
+ export async function listUserWorkspaces(token, signal) {
15
+ const response = await checkedApiRequest(listUserWorkspacesResponseSchema, '/workspaces', token ? {
16
+ headers: {
17
+ authorization: `Bearer ${token}`
18
+ },
19
+ signal
20
+ } : {
21
+ signal
22
+ });
23
+ return {
24
+ memberships: response.memberships.map((membership)=>({
25
+ id: membership.workspace_id,
26
+ name: membership.workspace_name,
27
+ membershipId: membership.id
28
+ }))
29
+ };
30
+ }
31
+ export function authRefreshQueryOptions() {
32
+ return queryOptions({
33
+ queryKey: authRefreshQueryKey,
34
+ queryFn: ({ signal })=>refreshAuthenticatedSession(signal),
35
+ retry: false,
36
+ staleTime: 0
37
+ });
38
+ }
39
+ export function userWorkspacesQueryOptions(token) {
40
+ return queryOptions({
41
+ queryKey: userWorkspacesQueryKey,
42
+ queryFn: ({ signal })=>listUserWorkspaces(token, signal),
43
+ retry: false,
44
+ staleTime: 0
45
+ });
46
+ }
47
+ export async function refreshAuthenticatedSession(signal) {
48
+ const response = await checkedApiRequest(loginResponseSchema, '/auth/refresh', {
49
+ method: 'POST',
50
+ signal
51
+ });
52
+ return toAuthenticatedSession(response);
53
+ }
54
+
55
+ //# sourceMappingURL=session-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/hooks/api/session-auth.ts"],"sourcesContent":["import {loginResponseSchema} from '@shipfox/api-auth-dto';\nimport {listUserWorkspacesResponseSchema} from '@shipfox/api-workspaces-dto';\nimport {checkedApiRequest} from '@shipfox/client-api';\nimport {type FetchQueryOptions, queryOptions} from '@tanstack/react-query';\nimport type {AuthenticatedSession, WorkspaceSummary} from '#core/session.js';\nimport {toAuthenticatedSession} from './session-mapper.js';\n\nexport interface UserWorkspaces {\n memberships: WorkspaceSummary[];\n}\n\nexport const authRefreshQueryKey = ['auth', 'refresh'] as const;\nexport const userWorkspacesQueryKey = ['workspaces', 'mine'] as const;\n\ntype AuthRefreshQueryOptions = FetchQueryOptions<\n AuthenticatedSession,\n Error,\n AuthenticatedSession,\n typeof authRefreshQueryKey\n>;\n\ntype UserWorkspacesQueryOptions = FetchQueryOptions<\n UserWorkspaces,\n Error,\n UserWorkspaces,\n typeof userWorkspacesQueryKey\n>;\n\nexport async function listUserWorkspaces(\n token?: string,\n signal?: AbortSignal,\n): Promise<UserWorkspaces> {\n const response = await checkedApiRequest(\n listUserWorkspacesResponseSchema,\n '/workspaces',\n token ? {headers: {authorization: `Bearer ${token}`}, signal} : {signal},\n );\n return {\n memberships: response.memberships.map((membership) => ({\n id: membership.workspace_id,\n name: membership.workspace_name,\n membershipId: membership.id,\n })),\n };\n}\n\nexport function authRefreshQueryOptions(): AuthRefreshQueryOptions {\n return queryOptions({\n queryKey: authRefreshQueryKey,\n queryFn: ({signal}) => refreshAuthenticatedSession(signal),\n retry: false,\n staleTime: 0,\n });\n}\n\nexport function userWorkspacesQueryOptions(token?: string): UserWorkspacesQueryOptions {\n return queryOptions({\n queryKey: userWorkspacesQueryKey,\n queryFn: ({signal}) => listUserWorkspaces(token, signal),\n retry: false,\n staleTime: 0,\n });\n}\n\nexport async function refreshAuthenticatedSession(\n signal?: AbortSignal,\n): Promise<AuthenticatedSession> {\n const response = await checkedApiRequest(loginResponseSchema, '/auth/refresh', {\n method: 'POST',\n signal,\n });\n return toAuthenticatedSession(response);\n}\n"],"names":["loginResponseSchema","listUserWorkspacesResponseSchema","checkedApiRequest","queryOptions","toAuthenticatedSession","authRefreshQueryKey","userWorkspacesQueryKey","listUserWorkspaces","token","signal","response","headers","authorization","memberships","map","membership","id","workspace_id","name","workspace_name","membershipId","authRefreshQueryOptions","queryKey","queryFn","refreshAuthenticatedSession","retry","staleTime","userWorkspacesQueryOptions","method"],"mappings":"AAAA,SAAQA,mBAAmB,QAAO,wBAAwB;AAC1D,SAAQC,gCAAgC,QAAO,8BAA8B;AAC7E,SAAQC,iBAAiB,QAAO,sBAAsB;AACtD,SAAgCC,YAAY,QAAO,wBAAwB;AAE3E,SAAQC,sBAAsB,QAAO,sBAAsB;AAM3D,OAAO,MAAMC,sBAAsB;IAAC;IAAQ;CAAU,CAAU;AAChE,OAAO,MAAMC,yBAAyB;IAAC;IAAc;CAAO,CAAU;AAgBtE,OAAO,eAAeC,mBACpBC,KAAc,EACdC,MAAoB;IAEpB,MAAMC,WAAW,MAAMR,kBACrBD,kCACA,eACAO,QAAQ;QAACG,SAAS;YAACC,eAAe,CAAC,OAAO,EAAEJ,OAAO;QAAA;QAAGC;IAAM,IAAI;QAACA;IAAM;IAEzE,OAAO;QACLI,aAAaH,SAASG,WAAW,CAACC,GAAG,CAAC,CAACC,aAAgB,CAAA;gBACrDC,IAAID,WAAWE,YAAY;gBAC3BC,MAAMH,WAAWI,cAAc;gBAC/BC,cAAcL,WAAWC,EAAE;YAC7B,CAAA;IACF;AACF;AAEA,OAAO,SAASK;IACd,OAAOlB,aAAa;QAClBmB,UAAUjB;QACVkB,SAAS,CAAC,EAACd,MAAM,EAAC,GAAKe,4BAA4Bf;QACnDgB,OAAO;QACPC,WAAW;IACb;AACF;AAEA,OAAO,SAASC,2BAA2BnB,KAAc;IACvD,OAAOL,aAAa;QAClBmB,UAAUhB;QACViB,SAAS,CAAC,EAACd,MAAM,EAAC,GAAKF,mBAAmBC,OAAOC;QACjDgB,OAAO;QACPC,WAAW;IACb;AACF;AAEA,OAAO,eAAeF,4BACpBf,MAAoB;IAEpB,MAAMC,WAAW,MAAMR,kBAAkBF,qBAAqB,iBAAiB;QAC7E4B,QAAQ;QACRnB;IACF;IACA,OAAOL,uBAAuBM;AAChC"}
@@ -1 +1 @@
1
- {"version":3,"file":"active-workspace.d.ts","sourceRoot":"","sources":["../../src/runtime/active-workspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,SAAS,EAAC,MAAM,WAAW,CAAC;AAEvD,wBAAgB,kBAAkB,IAAI,SAAS,CAI9C;AAED,wBAAgB,uBAAuB,IAAI,SAAS,GAAG,SAAS,CAG/D"}
1
+ {"version":3,"file":"active-workspace.d.ts","sourceRoot":"","sources":["../../src/runtime/active-workspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,SAAS,EAAC,MAAM,WAAW,CAAC;AAGvD,wBAAgB,kBAAkB,IAAI,SAAS,CAI9C;AAED,wBAAgB,uBAAuB,IAAI,SAAS,GAAG,SAAS,CAG/D"}
@@ -1,14 +1,12 @@
1
- import { useParams } from '@tanstack/react-router';
2
1
  import { useAuthState } from './auth.js';
2
+ import { parseWorkspaceParams, useRouteParams } from './route-inputs.js';
3
3
  export function useActiveWorkspace() {
4
4
  const workspace = useMaybeActiveWorkspace();
5
5
  if (!workspace) throw new Error('useActiveWorkspace called outside a /workspaces/$wid route');
6
6
  return workspace;
7
7
  }
8
8
  export function useMaybeActiveWorkspace() {
9
- const { wid } = useParams({
10
- strict: false
11
- });
9
+ const { wid } = useRouteParams(parseWorkspaceParams);
12
10
  return useAuthState().workspaces.find((workspace)=>workspace.id === wid);
13
11
  }
14
12
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runtime/active-workspace.ts"],"sourcesContent":["import {useParams} from '@tanstack/react-router';\nimport {useAuthState, type Workspace} from './auth.js';\n\nexport function useActiveWorkspace(): Workspace {\n const workspace = useMaybeActiveWorkspace();\n if (!workspace) throw new Error('useActiveWorkspace called outside a /workspaces/$wid route');\n return workspace;\n}\n\nexport function useMaybeActiveWorkspace(): Workspace | undefined {\n const {wid} = useParams({strict: false}) as {wid?: string};\n return useAuthState().workspaces.find((workspace) => workspace.id === wid);\n}\n"],"names":["useParams","useAuthState","useActiveWorkspace","workspace","useMaybeActiveWorkspace","Error","wid","strict","workspaces","find","id"],"mappings":"AAAA,SAAQA,SAAS,QAAO,yBAAyB;AACjD,SAAQC,YAAY,QAAuB,YAAY;AAEvD,OAAO,SAASC;IACd,MAAMC,YAAYC;IAClB,IAAI,CAACD,WAAW,MAAM,IAAIE,MAAM;IAChC,OAAOF;AACT;AAEA,OAAO,SAASC;IACd,MAAM,EAACE,GAAG,EAAC,GAAGN,UAAU;QAACO,QAAQ;IAAK;IACtC,OAAON,eAAeO,UAAU,CAACC,IAAI,CAAC,CAACN,YAAcA,UAAUO,EAAE,KAAKJ;AACxE"}
1
+ {"version":3,"sources":["../../src/runtime/active-workspace.ts"],"sourcesContent":["import {useAuthState, type Workspace} from './auth.js';\nimport {parseWorkspaceParams, useRouteParams} from './route-inputs.js';\n\nexport function useActiveWorkspace(): Workspace {\n const workspace = useMaybeActiveWorkspace();\n if (!workspace) throw new Error('useActiveWorkspace called outside a /workspaces/$wid route');\n return workspace;\n}\n\nexport function useMaybeActiveWorkspace(): Workspace | undefined {\n const {wid} = useRouteParams(parseWorkspaceParams);\n return useAuthState().workspaces.find((workspace) => workspace.id === wid);\n}\n"],"names":["useAuthState","parseWorkspaceParams","useRouteParams","useActiveWorkspace","workspace","useMaybeActiveWorkspace","Error","wid","workspaces","find","id"],"mappings":"AAAA,SAAQA,YAAY,QAAuB,YAAY;AACvD,SAAQC,oBAAoB,EAAEC,cAAc,QAAO,oBAAoB;AAEvE,OAAO,SAASC;IACd,MAAMC,YAAYC;IAClB,IAAI,CAACD,WAAW,MAAM,IAAIE,MAAM;IAChC,OAAOF;AACT;AAEA,OAAO,SAASC;IACd,MAAM,EAACE,GAAG,EAAC,GAAGL,eAAeD;IAC7B,OAAOD,eAAeQ,UAAU,CAACC,IAAI,CAAC,CAACL,YAAcA,UAAUM,EAAE,KAAKH;AACxE"}
@@ -20,11 +20,6 @@ export function buildAnchorSkeleton({ navigation, settingsSections }) {
20
20
  getParentRoute: ()=>rootRoute,
21
21
  path: anchorPaths.workspaceLayout,
22
22
  beforeLoad: async ({ context, params, location })=>{
23
- try {
24
- rememberLastWorkspaceId(params.wid);
25
- } catch {
26
- // Local storage is best effort.
27
- }
28
23
  const auth = context.auth;
29
24
  if (!auth || auth.isLoading || !context.queryClient) return;
30
25
  if (!auth.isAuthenticated) throw redirect({
@@ -36,6 +31,11 @@ export function buildAnchorSkeleton({ navigation, settingsSections }) {
36
31
  if (!auth.workspaces.some((workspace)=>workspace.id === params.wid)) throw redirect({
37
32
  to: '/'
38
33
  });
34
+ try {
35
+ if (auth.user?.id) rememberLastWorkspaceId(auth.user.id, params.wid);
36
+ } catch {
37
+ // Local storage is best effort.
38
+ }
39
39
  if (!context.workspaceSetup) throw new Error('Client composition includes workspace routes but no workspaceSetup gate.');
40
40
  return await context.workspaceSetup({
41
41
  queryClient: context.queryClient,