@shipfox/client-shell 12.0.2 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/components/main-layout.js +2 -2
  2. package/dist/components/main-layout.js.map +1 -1
  3. package/dist/components/settings-nav.d.ts +2 -1
  4. package/dist/components/settings-nav.d.ts.map +1 -1
  5. package/dist/components/settings-nav.js +17 -13
  6. package/dist/components/settings-nav.js.map +1 -1
  7. package/dist/components/workspace-crumb.js +2 -2
  8. package/dist/components/workspace-crumb.js.map +1 -1
  9. package/dist/components/workspace-switcher.d.ts.map +1 -1
  10. package/dist/components/workspace-switcher.js +4 -2
  11. package/dist/components/workspace-switcher.js.map +1 -1
  12. package/dist/compose/compose-routes.d.ts.map +1 -1
  13. package/dist/compose/compose-routes.js +4 -1
  14. package/dist/compose/compose-routes.js.map +1 -1
  15. package/dist/compose/validate-registries.d.ts.map +1 -1
  16. package/dist/compose/validate-registries.js +1 -1
  17. package/dist/compose/validate-registries.js.map +1 -1
  18. package/dist/contract.d.ts +2 -1
  19. package/dist/contract.d.ts.map +1 -1
  20. package/dist/contract.js.map +1 -1
  21. package/dist/core/session.d.ts +1 -0
  22. package/dist/core/session.d.ts.map +1 -1
  23. package/dist/core/session.js.map +1 -1
  24. package/dist/hooks/api/session-auth.d.ts.map +1 -1
  25. package/dist/hooks/api/session-auth.js +1 -0
  26. package/dist/hooks/api/session-auth.js.map +1 -1
  27. package/dist/runtime/active-workspace.js +3 -3
  28. package/dist/runtime/active-workspace.js.map +1 -1
  29. package/dist/runtime/anchor-paths.d.ts +14 -4
  30. package/dist/runtime/anchor-paths.d.ts.map +1 -1
  31. package/dist/runtime/anchor-paths.js +57 -4
  32. package/dist/runtime/anchor-paths.js.map +1 -1
  33. package/dist/runtime/anchors.d.ts +9 -6
  34. package/dist/runtime/anchors.d.ts.map +1 -1
  35. package/dist/runtime/anchors.js +85 -42
  36. package/dist/runtime/anchors.js.map +1 -1
  37. package/dist/runtime/assemble-route-tree.d.ts.map +1 -1
  38. package/dist/runtime/assemble-route-tree.js +7 -2
  39. package/dist/runtime/assemble-route-tree.js.map +1 -1
  40. package/dist/runtime/chrome-context.d.ts +2 -1
  41. package/dist/runtime/chrome-context.d.ts.map +1 -1
  42. package/dist/runtime/chrome-context.js.map +1 -1
  43. package/dist/runtime/compose-client-app.d.ts.map +1 -1
  44. package/dist/runtime/compose-client-app.js +5 -3
  45. package/dist/runtime/compose-client-app.js.map +1 -1
  46. package/dist/runtime/index.d.ts +1 -0
  47. package/dist/runtime/index.d.ts.map +1 -1
  48. package/dist/runtime/index.js +1 -0
  49. package/dist/runtime/index.js.map +1 -1
  50. package/dist/runtime/registries.js +4 -1
  51. package/dist/runtime/registries.js.map +1 -1
  52. package/dist/runtime/route-inputs.d.ts +3 -3
  53. package/dist/runtime/route-inputs.d.ts.map +1 -1
  54. package/dist/runtime/route-inputs.js +9 -9
  55. package/dist/runtime/route-inputs.js.map +1 -1
  56. package/dist/runtime/router-context.d.ts +7 -0
  57. package/dist/runtime/router-context.d.ts.map +1 -1
  58. package/dist/runtime/router-context.js.map +1 -1
  59. package/dist/runtime/search-serialization.d.ts +26 -0
  60. package/dist/runtime/search-serialization.d.ts.map +1 -0
  61. package/dist/runtime/search-serialization.js +78 -0
  62. package/dist/runtime/search-serialization.js.map +1 -0
  63. package/dist/runtime/workspace-setup.d.ts +1 -0
  64. package/dist/runtime/workspace-setup.d.ts.map +1 -1
  65. package/dist/runtime/workspace-setup.js.map +1 -1
  66. package/dist/tsconfig.test.tsbuildinfo +1 -1
  67. package/dist/vite/generate.d.ts.map +1 -1
  68. package/dist/vite/generate.js +12 -3
  69. package/dist/vite/generate.js.map +1 -1
  70. package/package.json +6 -6
@@ -7,7 +7,7 @@ import { NavBar } from './nav-bar.js';
7
7
  import { NavTabs } from './nav-tabs.js';
8
8
  export function MainLayout({ navigation, hideProjectNavigation = false }) {
9
9
  const workspace = useMaybeActiveWorkspace();
10
- const { pid } = useRouteParams(parseWorkspaceProjectParams);
10
+ const { projectSlug } = useRouteParams(parseWorkspaceProjectParams);
11
11
  const matches = useMatches();
12
12
  if (!workspace) return /*#__PURE__*/ _jsx(FullPageLoader, {});
13
13
  const fullBleed = matches.some((match)=>match.staticData.layout === 'full-bleed');
@@ -20,7 +20,7 @@ export function MainLayout({ navigation, hideProjectNavigation = false }) {
20
20
  }),
21
21
  hideProjectNavigation ? undefined : /*#__PURE__*/ _jsx(NavTabs, {
22
22
  entries: navigation,
23
- scope: pid ? 'project' : 'workspace'
23
+ scope: projectSlug ? 'project' : 'workspace'
24
24
  }),
25
25
  fullBleed ? /*#__PURE__*/ _jsx("main", {
26
26
  className: "flex-1 min-h-0 flex flex-col overflow-hidden",
@@ -1 +1 @@
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 const appContentHeight = hideProjectNavigation\n ? '[--app-content-h:calc(100dvh_-_56px)]'\n : '[--app-content-h:calc(100dvh_-_96px)]';\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 ${appContentHeight}`}>\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","appContentHeight","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,MAAMC,mBAAmBT,wBACrB,0CACA;IACJ,qBACE,MAACU;QAAIC,WAAU;;0BACb,KAACf;gBAAOI,uBAAuBA;;YAC9BA,wBAAwBY,0BACvB,KAACf;gBAAQgB,SAASd;gBAAYe,OAAOZ,MAAM,YAAY;;YAExDE,0BACC,KAACW;gBAAKJ,WAAU;0BACd,cAAA,KAACpB;+BAGH,KAACwB;gBAAKJ,WAAW,CAAC,qBAAqB,EAAEF,kBAAkB;0BACzD,cAAA,KAACC;oBAAIC,WAAU;8BACb,cAAA,KAACpB;;;;;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 {projectSlug} = useRouteParams(parseWorkspaceProjectParams);\n const matches = useMatches();\n if (!workspace) return <FullPageLoader />;\n const fullBleed = matches.some((match) => match.staticData.layout === 'full-bleed');\n const appContentHeight = hideProjectNavigation\n ? '[--app-content-h:calc(100dvh_-_56px)]'\n : '[--app-content-h:calc(100dvh_-_96px)]';\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={projectSlug ? '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 ${appContentHeight}`}>\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","projectSlug","matches","fullBleed","some","match","staticData","layout","appContentHeight","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,WAAW,EAAC,GAAGP,eAAeD;IACrC,MAAMS,UAAUX;IAChB,IAAI,CAACS,WAAW,qBAAO,KAACX;IACxB,MAAMc,YAAYD,QAAQE,IAAI,CAAC,CAACC,QAAUA,MAAMC,UAAU,CAACC,MAAM,KAAK;IACtE,MAAMC,mBAAmBT,wBACrB,0CACA;IACJ,qBACE,MAACU;QAAIC,WAAU;;0BACb,KAACf;gBAAOI,uBAAuBA;;YAC9BA,wBAAwBY,0BACvB,KAACf;gBAAQgB,SAASd;gBAAYe,OAAOZ,cAAc,YAAY;;YAEhEE,0BACC,KAACW;gBAAKJ,WAAU;0BACd,cAAA,KAACpB;+BAGH,KAACwB;gBAAKJ,WAAW,CAAC,qBAAqB,EAAEF,kBAAkB;0BACzD,cAAA,KAACC;oBAAIC,WAAU;8BACb,cAAA,KAACpB;;;;;AAMb"}
@@ -1,5 +1,6 @@
1
1
  import type { SettingsSectionEntry } from '#contract.js';
2
- export declare function SettingsNav({ entries }: {
2
+ export declare function SettingsNav({ entries, scope, }: {
3
3
  entries: readonly SettingsSectionEntry[];
4
+ scope: 'workspace' | 'project';
4
5
  }): import("react").JSX.Element | null;
5
6
  //# sourceMappingURL=settings-nav.d.ts.map
@@ -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;AAGvD,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;AAOvD,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,KAAK,GACN,EAAE;IACD,OAAO,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACzC,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;CAChC,sCA4CA"}
@@ -2,21 +2,27 @@ 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
4
  import { Link, useMatchRoute } from '@tanstack/react-router';
5
- import { parseWorkspaceParams, useRouteParams } from '#runtime/route-inputs.js';
6
- export function SettingsNav({ entries }) {
7
- const params = useRouteParams(parseWorkspaceParams);
5
+ import { parseWorkspaceParams, parseWorkspaceProjectParams, useRouteParams } from '#runtime/route-inputs.js';
6
+ export function SettingsNav({ entries, scope }) {
7
+ const params = useRouteParams((input)=>scope === 'workspace' ? parseWorkspaceParams(input) : parseWorkspaceProjectParams(input));
8
8
  const matchRoute = useMatchRoute();
9
- if (!params.wid) return null;
9
+ if (!params.workspaceSlug || scope === 'project' && !params.projectSlug) return null;
10
+ const scopedEntries = entries.filter((entry)=>(entry.scope ?? 'workspace') === scope);
11
+ const settingsPath = scope === 'workspace' ? '/w/$workspaceSlug/settings' : '/w/$workspaceSlug/p/$projectSlug/settings';
12
+ const paramsForLink = scope === 'workspace' ? {
13
+ workspaceSlug: params.workspaceSlug
14
+ } : {
15
+ workspaceSlug: params.workspaceSlug,
16
+ projectSlug: params.projectSlug
17
+ };
10
18
  return /*#__PURE__*/ _jsx("nav", {
11
- "aria-label": "Workspace settings",
19
+ "aria-label": `${scope === 'workspace' ? 'Workspace' : 'Project'} settings`,
12
20
  className: "flex flex-col gap-4",
13
- children: entries.map((entry)=>{
14
- const to = `/workspaces/$wid/settings/${entry.pathSegment}`;
21
+ children: scopedEntries.map((entry)=>{
22
+ const to = `${settingsPath}/${entry.pathSegment}`;
15
23
  const active = Boolean(matchRoute({
16
24
  to: to,
17
- params: {
18
- wid: params.wid
19
- }
25
+ params: paramsForLink
20
26
  }));
21
27
  return /*#__PURE__*/ _jsx(Button, {
22
28
  asChild: true,
@@ -24,9 +30,7 @@ export function SettingsNav({ entries }) {
24
30
  className: "w-full justify-start",
25
31
  children: /*#__PURE__*/ _jsxs(Link, {
26
32
  to: to,
27
- params: {
28
- wid: params.wid
29
- },
33
+ params: paramsForLink,
30
34
  "aria-current": active ? 'page' : undefined,
31
35
  children: [
32
36
  /*#__PURE__*/ _jsx(Icon, {
@@ -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} 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
+ {"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 {\n parseWorkspaceParams,\n parseWorkspaceProjectParams,\n useRouteParams,\n} from '#runtime/route-inputs.js';\n\nexport function SettingsNav({\n entries,\n scope,\n}: {\n entries: readonly SettingsSectionEntry[];\n scope: 'workspace' | 'project';\n}) {\n const params = useRouteParams((input): {workspaceSlug?: string; projectSlug?: string} =>\n scope === 'workspace' ? parseWorkspaceParams(input) : parseWorkspaceProjectParams(input),\n );\n const matchRoute = useMatchRoute();\n if (!params.workspaceSlug || (scope === 'project' && !params.projectSlug)) return null;\n const scopedEntries = entries.filter((entry) => (entry.scope ?? 'workspace') === scope);\n const settingsPath =\n scope === 'workspace'\n ? '/w/$workspaceSlug/settings'\n : '/w/$workspaceSlug/p/$projectSlug/settings';\n const paramsForLink =\n scope === 'workspace'\n ? {workspaceSlug: params.workspaceSlug}\n : {workspaceSlug: params.workspaceSlug, projectSlug: params.projectSlug};\n\n return (\n <nav\n aria-label={`${scope === 'workspace' ? 'Workspace' : 'Project'} settings`}\n className=\"flex flex-col gap-4\"\n >\n {scopedEntries.map((entry) => {\n const to = `${settingsPath}/${entry.pathSegment}`;\n const active = Boolean(matchRoute({to: to as never, params: paramsForLink 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={paramsForLink 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","parseWorkspaceProjectParams","useRouteParams","SettingsNav","entries","scope","params","input","matchRoute","workspaceSlug","projectSlug","scopedEntries","filter","entry","settingsPath","paramsForLink","nav","aria-label","className","map","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,SACEC,oBAAoB,EACpBC,2BAA2B,EAC3BC,cAAc,QACT,2BAA2B;AAElC,OAAO,SAASC,YAAY,EAC1BC,OAAO,EACPC,KAAK,EAIN;IACC,MAAMC,SAASJ,eAAe,CAACK,QAC7BF,UAAU,cAAcL,qBAAqBO,SAASN,4BAA4BM;IAEpF,MAAMC,aAAaT;IACnB,IAAI,CAACO,OAAOG,aAAa,IAAKJ,UAAU,aAAa,CAACC,OAAOI,WAAW,EAAG,OAAO;IAClF,MAAMC,gBAAgBP,QAAQQ,MAAM,CAAC,CAACC,QAAU,AAACA,CAAAA,MAAMR,KAAK,IAAI,WAAU,MAAOA;IACjF,MAAMS,eACJT,UAAU,cACN,+BACA;IACN,MAAMU,gBACJV,UAAU,cACN;QAACI,eAAeH,OAAOG,aAAa;IAAA,IACpC;QAACA,eAAeH,OAAOG,aAAa;QAAEC,aAAaJ,OAAOI,WAAW;IAAA;IAE3E,qBACE,KAACM;QACCC,cAAY,GAAGZ,UAAU,cAAc,cAAc,UAAU,SAAS,CAAC;QACzEa,WAAU;kBAETP,cAAcQ,GAAG,CAAC,CAACN;YAClB,MAAMO,KAAK,GAAGN,aAAa,CAAC,EAAED,MAAMQ,WAAW,EAAE;YACjD,MAAMC,SAASC,QAAQf,WAAW;gBAACY,IAAIA;gBAAad,QAAQS;YAAsB;YAClF,qBACE,KAACnB;gBAEC4B,OAAO;gBACPC,SAASH,SAAS,cAAc;gBAChCJ,WAAU;0BAEV,cAAA,MAACpB;oBACCsB,IAAIA;oBACJd,QAAQS;oBACRW,gBAAcJ,SAAS,SAASK;;sCAEhC,KAAC9B;4BAAK+B,MAAMf,MAAMgB,IAAI;4BAAEX,WAAU;;wBACjCL,MAAMiB,KAAK;;;eAXTjB,MAAMkB,EAAE;QAenB;;AAGN"}
@@ -10,9 +10,9 @@ export function WorkspaceCrumb({ workspace, compact = false }) {
10
10
  className: "flex items-center",
11
11
  children: [
12
12
  /*#__PURE__*/ _jsx(Link, {
13
- to: "/workspaces/$wid",
13
+ to: "/w/$workspaceSlug",
14
14
  params: {
15
- wid: workspace.id
15
+ workspaceSlug: workspace.slug
16
16
  },
17
17
  "aria-current": "page",
18
18
  className: `inline-block text-md font-medium text-foreground-neutral-base px-6 py-4 rounded-6 hover:bg-background-components-hover transition-colors truncate ${compact ? 'max-w-[120px] sm:max-w-[200px]' : 'max-w-[200px]'}`,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/workspace-crumb.tsx"],"sourcesContent":["import {Icon} from '@shipfox/react-ui/icon';\nimport {Popover, PopoverContent, PopoverTrigger} from '@shipfox/react-ui/popover';\nimport {Link} from '@tanstack/react-router';\nimport {useState} from 'react';\nimport type {Workspace} from '#runtime/auth.js';\nimport {WorkspaceSwitcher} from './workspace-switcher.js';\n\nexport interface WorkspaceCrumbProps {\n workspace: Workspace;\n compact?: boolean;\n}\n\nexport function WorkspaceCrumb({workspace, compact = false}: WorkspaceCrumbProps) {\n const [open, setOpen] = useState(false);\n return (\n <div className=\"flex items-center\">\n <Link\n to=\"/workspaces/$wid\"\n params={{wid: workspace.id}}\n aria-current=\"page\"\n className={`inline-block text-md font-medium text-foreground-neutral-base px-6 py-4 rounded-6 hover:bg-background-components-hover transition-colors truncate ${compact ? 'max-w-[120px] sm:max-w-[200px]' : 'max-w-[200px]'}`}\n >\n {workspace.name}\n </Link>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"Switch workspace\"\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n className=\"ml-2 grid place-items-center size-24 rounded-4 text-foreground-neutral-muted hover:bg-background-components-hover hover:text-foreground-neutral-base transition-colors\"\n >\n <Icon name=\"arrowDownSLine\" className=\"size-16\" />\n </button>\n </PopoverTrigger>\n <PopoverContent className=\"w-[280px] p-0\" align=\"start\" sideOffset={8}>\n <WorkspaceSwitcher activeWorkspaceId={workspace.id} onSelect={() => setOpen(false)} />\n </PopoverContent>\n </Popover>\n </div>\n );\n}\n"],"names":["Icon","Popover","PopoverContent","PopoverTrigger","Link","useState","WorkspaceSwitcher","WorkspaceCrumb","workspace","compact","open","setOpen","div","className","to","params","wid","id","aria-current","name","onOpenChange","asChild","button","type","aria-label","aria-haspopup","aria-expanded","align","sideOffset","activeWorkspaceId","onSelect"],"mappings":";AAAA,SAAQA,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,OAAO,EAAEC,cAAc,EAAEC,cAAc,QAAO,4BAA4B;AAClF,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,QAAQ,QAAO,QAAQ;AAE/B,SAAQC,iBAAiB,QAAO,0BAA0B;AAO1D,OAAO,SAASC,eAAe,EAACC,SAAS,EAAEC,UAAU,KAAK,EAAsB;IAC9E,MAAM,CAACC,MAAMC,QAAQ,GAAGN,SAAS;IACjC,qBACE,MAACO;QAAIC,WAAU;;0BACb,KAACT;gBACCU,IAAG;gBACHC,QAAQ;oBAACC,KAAKR,UAAUS,EAAE;gBAAA;gBAC1BC,gBAAa;gBACbL,WAAW,CAAC,kJAAkJ,EAAEJ,UAAU,mCAAmC,iBAAiB;0BAE7ND,UAAUW,IAAI;;0BAEjB,MAAClB;gBAAQS,MAAMA;gBAAMU,cAAcT;;kCACjC,KAACR;wBAAekB,OAAO;kCACrB,cAAA,KAACC;4BACCC,MAAK;4BACLC,cAAW;4BACXC,iBAAc;4BACdC,iBAAehB;4BACfG,WAAU;sCAEV,cAAA,KAACb;gCAAKmB,MAAK;gCAAiBN,WAAU;;;;kCAG1C,KAACX;wBAAeW,WAAU;wBAAgBc,OAAM;wBAAQC,YAAY;kCAClE,cAAA,KAACtB;4BAAkBuB,mBAAmBrB,UAAUS,EAAE;4BAAEa,UAAU,IAAMnB,QAAQ;;;;;;;AAKtF"}
1
+ {"version":3,"sources":["../../src/components/workspace-crumb.tsx"],"sourcesContent":["import {Icon} from '@shipfox/react-ui/icon';\nimport {Popover, PopoverContent, PopoverTrigger} from '@shipfox/react-ui/popover';\nimport {Link} from '@tanstack/react-router';\nimport {useState} from 'react';\nimport type {Workspace} from '#runtime/auth.js';\nimport {WorkspaceSwitcher} from './workspace-switcher.js';\n\nexport interface WorkspaceCrumbProps {\n workspace: Workspace;\n compact?: boolean;\n}\n\nexport function WorkspaceCrumb({workspace, compact = false}: WorkspaceCrumbProps) {\n const [open, setOpen] = useState(false);\n return (\n <div className=\"flex items-center\">\n <Link\n to=\"/w/$workspaceSlug\"\n params={{workspaceSlug: workspace.slug}}\n aria-current=\"page\"\n className={`inline-block text-md font-medium text-foreground-neutral-base px-6 py-4 rounded-6 hover:bg-background-components-hover transition-colors truncate ${compact ? 'max-w-[120px] sm:max-w-[200px]' : 'max-w-[200px]'}`}\n >\n {workspace.name}\n </Link>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"Switch workspace\"\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n className=\"ml-2 grid place-items-center size-24 rounded-4 text-foreground-neutral-muted hover:bg-background-components-hover hover:text-foreground-neutral-base transition-colors\"\n >\n <Icon name=\"arrowDownSLine\" className=\"size-16\" />\n </button>\n </PopoverTrigger>\n <PopoverContent className=\"w-[280px] p-0\" align=\"start\" sideOffset={8}>\n <WorkspaceSwitcher activeWorkspaceId={workspace.id} onSelect={() => setOpen(false)} />\n </PopoverContent>\n </Popover>\n </div>\n );\n}\n"],"names":["Icon","Popover","PopoverContent","PopoverTrigger","Link","useState","WorkspaceSwitcher","WorkspaceCrumb","workspace","compact","open","setOpen","div","className","to","params","workspaceSlug","slug","aria-current","name","onOpenChange","asChild","button","type","aria-label","aria-haspopup","aria-expanded","align","sideOffset","activeWorkspaceId","id","onSelect"],"mappings":";AAAA,SAAQA,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,OAAO,EAAEC,cAAc,EAAEC,cAAc,QAAO,4BAA4B;AAClF,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,QAAQ,QAAO,QAAQ;AAE/B,SAAQC,iBAAiB,QAAO,0BAA0B;AAO1D,OAAO,SAASC,eAAe,EAACC,SAAS,EAAEC,UAAU,KAAK,EAAsB;IAC9E,MAAM,CAACC,MAAMC,QAAQ,GAAGN,SAAS;IACjC,qBACE,MAACO;QAAIC,WAAU;;0BACb,KAACT;gBACCU,IAAG;gBACHC,QAAQ;oBAACC,eAAeR,UAAUS,IAAI;gBAAA;gBACtCC,gBAAa;gBACbL,WAAW,CAAC,kJAAkJ,EAAEJ,UAAU,mCAAmC,iBAAiB;0BAE7ND,UAAUW,IAAI;;0BAEjB,MAAClB;gBAAQS,MAAMA;gBAAMU,cAAcT;;kCACjC,KAACR;wBAAekB,OAAO;kCACrB,cAAA,KAACC;4BACCC,MAAK;4BACLC,cAAW;4BACXC,iBAAc;4BACdC,iBAAehB;4BACfG,WAAU;sCAEV,cAAA,KAACb;gCAAKmB,MAAK;gCAAiBN,WAAU;;;;kCAG1C,KAACX;wBAAeW,WAAU;wBAAgBc,OAAM;wBAAQC,YAAY;kCAClE,cAAA,KAACtB;4BAAkBuB,mBAAmBrB,UAAUsB,EAAE;4BAAEC,UAAU,IAAMpB,QAAQ;;;;;;;AAKtF"}
@@ -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,+BAsDA"}
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,+BAwDA"}
@@ -10,6 +10,8 @@ export function WorkspaceSwitcher({ activeWorkspaceId, onSelect }) {
10
10
  const navigate = useNavigate();
11
11
  const setLastWorkspaceId = useSetAtom(lastWorkspaceIdAtom);
12
12
  const selectWorkspace = (wid)=>{
13
+ const workspace = workspaces.find((candidate)=>candidate.id === wid);
14
+ if (!workspace) return;
13
15
  try {
14
16
  setLastWorkspaceId(wid);
15
17
  if (user?.id) rememberLastWorkspaceId(user.id, wid);
@@ -17,9 +19,9 @@ export function WorkspaceSwitcher({ activeWorkspaceId, onSelect }) {
17
19
  // Local storage is best effort.
18
20
  }
19
21
  navigate({
20
- to: '/workspaces/$wid',
22
+ to: '/w/$workspaceSlug',
21
23
  params: {
22
- wid
24
+ workspaceSlug: workspace.slug
23
25
  }
24
26
  });
25
27
  onSelect?.();
@@ -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, 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"}
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 const workspace = workspaces.find((candidate) => candidate.id === wid);\n if (!workspace) return;\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: '/w/$workspaceSlug', params: {workspaceSlug: workspace.slug}});\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","workspace","find","candidate","id","to","params","workspaceSlug","slug","placeholder","className","div","heading","map","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,MAAMC,YAAYL,WAAWM,IAAI,CAAC,CAACC,YAAcA,UAAUC,EAAE,KAAKJ;QAClE,IAAI,CAACC,WAAW;QAChB,IAAI;YACFH,mBAAmBE;YACnB,IAAIL,MAAMS,IAAIb,wBAAwBI,KAAKS,EAAE,EAAEJ;QACjD,EAAE,OAAM;QACN,gCAAgC;QAClC;QACAH,SAAS;YAACQ,IAAI;YAAqBC,QAAQ;gBAACC,eAAeN,UAAUO,IAAI;YAAA;QAAC;QAC1Ed;IACF;IACA,qBACE,MAACf;;0BACC,KAACG;gBAAa2B,aAAY;;0BAC1B,MAACzB;gBAAY0B,WAAU;;kCACrB,MAACC;wBAAID,WAAU;;0CACb,KAAC9B;0CAAa;;0CACd,KAACC;gCAAa+B,SAAQ;0CACnBhB,WAAWiB,GAAG,CAAC,CAACZ,0BACf,MAAClB;wCAEC+B,OAAOb,UAAUG,EAAE;wCACnBW,UAAU;4CAACd,UAAUe,IAAI;yCAAC;wCAC1BtB,UAAU,IAAMK,gBAAgBE,UAAUG,EAAE;;0DAE5C,KAAClB;gDACC8B,MAAK;gDACLN,WAAW,CAAC,aAAa,EAAEjB,sBAAsBQ,UAAUG,EAAE,GAAG,gBAAgB,aAAa;;4CAE9FH,UAAUe,IAAI;;uCATVf,UAAUG,EAAE;;;;kCAczB,KAACnB;wBAAiBgC,YAAY;wBAACP,WAAU;;kCACzC,KAAC7B;wBAAaqC,UAAU;wBAACR,WAAU;kCACjC,cAAA,MAAC3B;4BACC+B,OAAM;4BACNpB,UAAU;gCACRG,SAAS;oCAACQ,IAAI;gCAAgC;gCAC9CX;4BACF;4BACAwB,UAAU;;8CAEV,KAAChC;oCAAK8B,MAAK;oCAAUN,WAAU;;gCAAY;;;;;;;;AAOvD"}
@@ -1 +1 @@
1
- {"version":3,"file":"compose-routes.d.ts","sourceRoot":"","sources":["../../src/compose/compose-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EAElB,MAAM,cAAc,CAAC;AAKtB,MAAM,WAAW,cAAe,SAAQ,kBAAkB;IACxD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AA0ID,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,cAAc,EAAE,CAanF;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,GAC1C,aAAa,EAAE,CAqFjB"}
1
+ {"version":3,"file":"compose-routes.d.ts","sourceRoot":"","sources":["../../src/compose/compose-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EAElB,MAAM,cAAc,CAAC;AAKtB,MAAM,WAAW,cAAe,SAAQ,kBAAkB;IACxD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AA0ID,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,cAAc,EAAE,CAcnF;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,GAC1C,aAAa,EAAE,CAuFjB"}
@@ -1,4 +1,4 @@
1
- import { anchorPaths } from '#runtime/anchor-paths.js';
1
+ import { anchorPaths, validateRoutePathInvariants } from '#runtime/anchor-paths.js';
2
2
  import { LayoutCompositionError, RouteCompositionError } from './errors.js';
3
3
  import { normalizeRoutePath } from './normalize-route-path.js';
4
4
  function isShellAnchor(parent) {
@@ -102,6 +102,7 @@ export function composeLayouts(features) {
102
102
  })));
103
103
  const layoutById = validateLayoutParents(layouts);
104
104
  for (const layout of layouts){
105
+ validateRoutePathInvariants(layout.path);
105
106
  validateNestedRoute(layout.path, layout.parent, layoutById, layout.featureId);
106
107
  }
107
108
  return layouts;
@@ -113,6 +114,7 @@ export function composeRoutes(features, providedLayouts) {
113
114
  const layoutById = validateLayoutParents(layouts);
114
115
  const layoutPaths = new Map();
115
116
  for (const layout of layouts){
117
+ validateRoutePathInvariants(layout.path);
116
118
  const existing = layoutPaths.get(layout.path);
117
119
  if (existing) {
118
120
  throw new RouteCompositionError(layout.path, `Route "${layout.path}" is contributed by both features "${existing.featureId}" and "${layout.featureId}".`, [
@@ -180,6 +182,7 @@ export function composeRoutes(features, providedLayouts) {
180
182
  validateNestedRoute(layout.path, layout.parent, layoutById, layout.featureId);
181
183
  }
182
184
  for (const route of routes.values()){
185
+ validateRoutePathInvariants(route.path);
183
186
  validateNestedRoute(route.path, route.parent, layoutById, route.featureId);
184
187
  }
185
188
  return [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/compose/compose-routes.ts"],"sourcesContent":["import type {\n AnchorId,\n ClientFeature,\n LayoutContribution,\n RouteContribution,\n RouteParentId,\n} from '#contract.js';\nimport {anchorPaths} from '#runtime/anchor-paths.js';\nimport {LayoutCompositionError, RouteCompositionError} from './errors.js';\nimport {normalizeRoutePath} from './normalize-route-path.js';\n\nexport interface ComposedLayout extends LayoutContribution {\n featureId: string;\n}\n\nexport interface ComposedRoute extends RouteContribution {\n featureId: string;\n ownerFeatureId: string;\n}\n\nfunction isShellAnchor(parent: RouteParentId): parent is AnchorId {\n return Object.hasOwn(anchorPaths, parent);\n}\n\nfunction layoutParentPath(\n parent: RouteParentId,\n layouts: ReadonlyMap<string, ComposedLayout>,\n): string | undefined {\n if (isShellAnchor(parent)) return anchorPaths[parent];\n return layouts.get(parent)?.path;\n}\n\nfunction resolvedShellAnchor(\n parent: RouteParentId,\n layouts: ReadonlyMap<string, ComposedLayout>,\n): AnchorId | undefined {\n let current = parent;\n const visited = new Set<string>();\n while (!isShellAnchor(current)) {\n if (visited.has(current)) return undefined;\n visited.add(current);\n const next = layouts.get(current)?.parent;\n if (next === undefined) return undefined;\n current = next;\n }\n return current;\n}\n\nfunction validateNestedRoute(\n path: string,\n parent: RouteParentId,\n layouts: ReadonlyMap<string, ComposedLayout>,\n featureId: string,\n): void {\n if (resolvedShellAnchor(parent, layouts) === 'root') {\n const protectedAnchor = protectedAnchorForRootPath(path);\n if (protectedAnchor) {\n const [anchor, anchorPath] = protectedAnchor;\n throw new RouteCompositionError(\n path,\n `Route \"${path}\" in feature \"${featureId}\" cannot use root parent inside reserved anchor \"${anchor}\" (${anchorPath}). Use parent \"${anchor}\".`,\n [featureId],\n );\n }\n }\n const parentPath = layoutParentPath(parent, layouts);\n if (!parentPath) {\n if (isShellAnchor(parent)) return;\n throw new RouteCompositionError(\n path,\n `Route \"${path}\" in feature \"${featureId}\" targets missing layout parent \"${parent}\".`,\n [featureId],\n );\n }\n if (parentPath !== '/' && path !== parentPath && !path.startsWith(`${parentPath}/`)) {\n const parentKind = isShellAnchor(parent) ? 'anchor' : 'layout';\n throw new RouteCompositionError(\n path,\n `Route \"${path}\" must be nested under ${parentKind} \"${parent}\" (${parentPath}).`,\n [featureId],\n );\n }\n}\n\nconst anchorPathValues: ReadonlySet<string> = new Set(Object.values(anchorPaths));\nconst protectedAnchorPaths = Object.entries(anchorPaths)\n .filter(([anchor]) => anchor !== 'root')\n .sort(([, leftPath], [, rightPath]) => rightPath.length - leftPath.length) as Array<\n [Exclude<AnchorId, 'root'>, string]\n>;\n\nfunction protectedAnchorForRootPath(\n path: string,\n): readonly [Exclude<AnchorId, 'root'>, string] | undefined {\n return protectedAnchorPaths.find(\n ([, anchorPath]) => path === anchorPath || path.startsWith(`${anchorPath}/`),\n );\n}\n\nfunction validateLayoutParents(layouts: readonly ComposedLayout[]): Map<string, ComposedLayout> {\n const byId = new Map<string, ComposedLayout>();\n for (const layout of layouts) {\n if (isShellAnchor(layout.id)) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout id \"${layout.id}\" in feature \"${layout.featureId}\" is reserved by the shell.`,\n [layout.featureId],\n );\n }\n if (anchorPathValues.has(layout.path)) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout \"${layout.id}\" in feature \"${layout.featureId}\" targets path \"${layout.path}\" which is reserved by a shell anchor.`,\n [layout.featureId],\n );\n }\n const existing = byId.get(layout.id);\n if (existing) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout id \"${layout.id}\" is contributed by both features \"${existing.featureId}\" and \"${layout.featureId}\".`,\n [existing.featureId, layout.featureId],\n );\n }\n byId.set(layout.id, layout);\n }\n\n const visiting = new Set<string>();\n const visited = new Set<string>();\n function visit(layout: ComposedLayout): void {\n if (visited.has(layout.id) || isShellAnchor(layout.parent)) return;\n if (visiting.has(layout.id)) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout \"${layout.id}\" in feature \"${layout.featureId}\" has a cyclic parent reference.`,\n [layout.featureId],\n );\n }\n const parent = byId.get(layout.parent);\n if (!parent) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout \"${layout.id}\" in feature \"${layout.featureId}\" targets missing layout parent \"${layout.parent}\".`,\n [layout.featureId],\n );\n }\n visiting.add(layout.id);\n visit(parent);\n visiting.delete(layout.id);\n visited.add(layout.id);\n }\n\n for (const layout of layouts) visit(layout);\n return byId;\n}\n\nexport function composeLayouts(features: readonly ClientFeature[]): ComposedLayout[] {\n const layouts = features.flatMap((feature) =>\n (feature.layouts ?? []).map((layout) => ({\n ...layout,\n path: normalizeRoutePath(layout.path),\n featureId: feature.id,\n })),\n );\n const layoutById = validateLayoutParents(layouts);\n for (const layout of layouts) {\n validateNestedRoute(layout.path, layout.parent, layoutById, layout.featureId);\n }\n return layouts;\n}\n\nexport function composeRoutes(\n features: readonly ClientFeature[],\n providedLayouts?: readonly ComposedLayout[],\n): ComposedRoute[] {\n const layouts = providedLayouts ? [...providedLayouts] : composeLayouts(features);\n const layoutById = validateLayoutParents(layouts);\n const layoutPaths = new Map<string, ComposedLayout>();\n for (const layout of layouts) {\n const existing = layoutPaths.get(layout.path);\n if (existing) {\n throw new RouteCompositionError(\n layout.path,\n `Route \"${layout.path}\" is contributed by both features \"${existing.featureId}\" and \"${layout.featureId}\".`,\n [existing.featureId, layout.featureId],\n );\n }\n layoutPaths.set(layout.path, layout);\n }\n\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 layoutAtPath = layoutPaths.get(normalizedContribution.path);\n if (layoutAtPath) {\n const message = normalizedContribution.override\n ? `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" cannot replace layout \"${layoutAtPath.id}\" contributed by feature \"${layoutAtPath.featureId}\".`\n : `Route \"${normalizedContribution.path}\" from feature \"${feature.id}\" conflicts with layout \"${layoutAtPath.id}\" contributed by feature \"${layoutAtPath.featureId}\". Routes cannot replace layouts.`;\n throw new RouteCompositionError(normalizedContribution.path, message, [\n layoutAtPath.featureId,\n feature.id,\n ]);\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\n for (const layout of layouts) {\n validateNestedRoute(layout.path, layout.parent, layoutById, layout.featureId);\n }\n for (const route of routes.values()) {\n validateNestedRoute(route.path, route.parent, layoutById, route.featureId);\n }\n return [...routes.values()];\n}\n"],"names":["anchorPaths","LayoutCompositionError","RouteCompositionError","normalizeRoutePath","isShellAnchor","parent","Object","hasOwn","layoutParentPath","layouts","get","path","resolvedShellAnchor","current","visited","Set","has","undefined","add","next","validateNestedRoute","featureId","protectedAnchor","protectedAnchorForRootPath","anchor","anchorPath","parentPath","startsWith","parentKind","anchorPathValues","values","protectedAnchorPaths","entries","filter","sort","leftPath","rightPath","length","find","validateLayoutParents","byId","Map","layout","id","existing","set","visiting","visit","delete","composeLayouts","features","flatMap","feature","map","layoutById","composeRoutes","providedLayouts","layoutPaths","routes","contribution","normalizedContribution","layoutAtPath","message","override","ownerFeatureId","route"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/compose/compose-routes.ts"],"sourcesContent":["import type {\n AnchorId,\n ClientFeature,\n LayoutContribution,\n RouteContribution,\n RouteParentId,\n} from '#contract.js';\nimport {anchorPaths, validateRoutePathInvariants} from '#runtime/anchor-paths.js';\nimport {LayoutCompositionError, RouteCompositionError} from './errors.js';\nimport {normalizeRoutePath} from './normalize-route-path.js';\n\nexport interface ComposedLayout extends LayoutContribution {\n featureId: string;\n}\n\nexport interface ComposedRoute extends RouteContribution {\n featureId: string;\n ownerFeatureId: string;\n}\n\nfunction isShellAnchor(parent: RouteParentId): parent is AnchorId {\n return Object.hasOwn(anchorPaths, parent);\n}\n\nfunction layoutParentPath(\n parent: RouteParentId,\n layouts: ReadonlyMap<string, ComposedLayout>,\n): string | undefined {\n if (isShellAnchor(parent)) return anchorPaths[parent];\n return layouts.get(parent)?.path;\n}\n\nfunction resolvedShellAnchor(\n parent: RouteParentId,\n layouts: ReadonlyMap<string, ComposedLayout>,\n): AnchorId | undefined {\n let current = parent;\n const visited = new Set<string>();\n while (!isShellAnchor(current)) {\n if (visited.has(current)) return undefined;\n visited.add(current);\n const next = layouts.get(current)?.parent;\n if (next === undefined) return undefined;\n current = next;\n }\n return current;\n}\n\nfunction validateNestedRoute(\n path: string,\n parent: RouteParentId,\n layouts: ReadonlyMap<string, ComposedLayout>,\n featureId: string,\n): void {\n if (resolvedShellAnchor(parent, layouts) === 'root') {\n const protectedAnchor = protectedAnchorForRootPath(path);\n if (protectedAnchor) {\n const [anchor, anchorPath] = protectedAnchor;\n throw new RouteCompositionError(\n path,\n `Route \"${path}\" in feature \"${featureId}\" cannot use root parent inside reserved anchor \"${anchor}\" (${anchorPath}). Use parent \"${anchor}\".`,\n [featureId],\n );\n }\n }\n const parentPath = layoutParentPath(parent, layouts);\n if (!parentPath) {\n if (isShellAnchor(parent)) return;\n throw new RouteCompositionError(\n path,\n `Route \"${path}\" in feature \"${featureId}\" targets missing layout parent \"${parent}\".`,\n [featureId],\n );\n }\n if (parentPath !== '/' && path !== parentPath && !path.startsWith(`${parentPath}/`)) {\n const parentKind = isShellAnchor(parent) ? 'anchor' : 'layout';\n throw new RouteCompositionError(\n path,\n `Route \"${path}\" must be nested under ${parentKind} \"${parent}\" (${parentPath}).`,\n [featureId],\n );\n }\n}\n\nconst anchorPathValues: ReadonlySet<string> = new Set(Object.values(anchorPaths));\nconst protectedAnchorPaths = Object.entries(anchorPaths)\n .filter(([anchor]) => anchor !== 'root')\n .sort(([, leftPath], [, rightPath]) => rightPath.length - leftPath.length) as Array<\n [Exclude<AnchorId, 'root'>, string]\n>;\n\nfunction protectedAnchorForRootPath(\n path: string,\n): readonly [Exclude<AnchorId, 'root'>, string] | undefined {\n return protectedAnchorPaths.find(\n ([, anchorPath]) => path === anchorPath || path.startsWith(`${anchorPath}/`),\n );\n}\n\nfunction validateLayoutParents(layouts: readonly ComposedLayout[]): Map<string, ComposedLayout> {\n const byId = new Map<string, ComposedLayout>();\n for (const layout of layouts) {\n if (isShellAnchor(layout.id)) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout id \"${layout.id}\" in feature \"${layout.featureId}\" is reserved by the shell.`,\n [layout.featureId],\n );\n }\n if (anchorPathValues.has(layout.path)) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout \"${layout.id}\" in feature \"${layout.featureId}\" targets path \"${layout.path}\" which is reserved by a shell anchor.`,\n [layout.featureId],\n );\n }\n const existing = byId.get(layout.id);\n if (existing) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout id \"${layout.id}\" is contributed by both features \"${existing.featureId}\" and \"${layout.featureId}\".`,\n [existing.featureId, layout.featureId],\n );\n }\n byId.set(layout.id, layout);\n }\n\n const visiting = new Set<string>();\n const visited = new Set<string>();\n function visit(layout: ComposedLayout): void {\n if (visited.has(layout.id) || isShellAnchor(layout.parent)) return;\n if (visiting.has(layout.id)) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout \"${layout.id}\" in feature \"${layout.featureId}\" has a cyclic parent reference.`,\n [layout.featureId],\n );\n }\n const parent = byId.get(layout.parent);\n if (!parent) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout \"${layout.id}\" in feature \"${layout.featureId}\" targets missing layout parent \"${layout.parent}\".`,\n [layout.featureId],\n );\n }\n visiting.add(layout.id);\n visit(parent);\n visiting.delete(layout.id);\n visited.add(layout.id);\n }\n\n for (const layout of layouts) visit(layout);\n return byId;\n}\n\nexport function composeLayouts(features: readonly ClientFeature[]): ComposedLayout[] {\n const layouts = features.flatMap((feature) =>\n (feature.layouts ?? []).map((layout) => ({\n ...layout,\n path: normalizeRoutePath(layout.path),\n featureId: feature.id,\n })),\n );\n const layoutById = validateLayoutParents(layouts);\n for (const layout of layouts) {\n validateRoutePathInvariants(layout.path);\n validateNestedRoute(layout.path, layout.parent, layoutById, layout.featureId);\n }\n return layouts;\n}\n\nexport function composeRoutes(\n features: readonly ClientFeature[],\n providedLayouts?: readonly ComposedLayout[],\n): ComposedRoute[] {\n const layouts = providedLayouts ? [...providedLayouts] : composeLayouts(features);\n const layoutById = validateLayoutParents(layouts);\n const layoutPaths = new Map<string, ComposedLayout>();\n for (const layout of layouts) {\n validateRoutePathInvariants(layout.path);\n const existing = layoutPaths.get(layout.path);\n if (existing) {\n throw new RouteCompositionError(\n layout.path,\n `Route \"${layout.path}\" is contributed by both features \"${existing.featureId}\" and \"${layout.featureId}\".`,\n [existing.featureId, layout.featureId],\n );\n }\n layoutPaths.set(layout.path, layout);\n }\n\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 layoutAtPath = layoutPaths.get(normalizedContribution.path);\n if (layoutAtPath) {\n const message = normalizedContribution.override\n ? `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" cannot replace layout \"${layoutAtPath.id}\" contributed by feature \"${layoutAtPath.featureId}\".`\n : `Route \"${normalizedContribution.path}\" from feature \"${feature.id}\" conflicts with layout \"${layoutAtPath.id}\" contributed by feature \"${layoutAtPath.featureId}\". Routes cannot replace layouts.`;\n throw new RouteCompositionError(normalizedContribution.path, message, [\n layoutAtPath.featureId,\n feature.id,\n ]);\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\n for (const layout of layouts) {\n validateNestedRoute(layout.path, layout.parent, layoutById, layout.featureId);\n }\n for (const route of routes.values()) {\n validateRoutePathInvariants(route.path);\n validateNestedRoute(route.path, route.parent, layoutById, route.featureId);\n }\n return [...routes.values()];\n}\n"],"names":["anchorPaths","validateRoutePathInvariants","LayoutCompositionError","RouteCompositionError","normalizeRoutePath","isShellAnchor","parent","Object","hasOwn","layoutParentPath","layouts","get","path","resolvedShellAnchor","current","visited","Set","has","undefined","add","next","validateNestedRoute","featureId","protectedAnchor","protectedAnchorForRootPath","anchor","anchorPath","parentPath","startsWith","parentKind","anchorPathValues","values","protectedAnchorPaths","entries","filter","sort","leftPath","rightPath","length","find","validateLayoutParents","byId","Map","layout","id","existing","set","visiting","visit","delete","composeLayouts","features","flatMap","feature","map","layoutById","composeRoutes","providedLayouts","layoutPaths","routes","contribution","normalizedContribution","layoutAtPath","message","override","ownerFeatureId","route"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"validate-registries.d.ts","sourceRoot":"","sources":["../../src/compose/validate-registries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAe,aAAa,EAAC,MAAM,cAAc,CAAC;AAI5E,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,KAAK,eAAe,GAAG,QAAQ,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;AAEzD,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AA0CD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,EAAE,eAAe,EAChC,gBAAgB,GAAE,SAAS,eAAe,EAAO,GAChD,IAAI,CAuEN;AA4BD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,EAAE,eAAe,GAC/B,IAAI,CAiCN"}
1
+ {"version":3,"file":"validate-registries.d.ts","sourceRoot":"","sources":["../../src/compose/validate-registries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAe,aAAa,EAAC,MAAM,cAAc,CAAC;AAI5E,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,KAAK,eAAe,GAAG,QAAQ,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;AAEzD,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AA0CD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,EAAE,eAAe,EAChC,gBAAgB,GAAE,SAAS,eAAe,EAAO,GAChD,IAAI,CAuEN;AA4BD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,EAAE,eAAe,GAC/B,IAAI,CAoCN"}
@@ -133,7 +133,7 @@ export function validateSettingsSections(features, routeReferences) {
133
133
  feature.id
134
134
  ]);
135
135
  }
136
- const path = `/workspaces/$wid/settings/${section.pathSegment}`;
136
+ const path = section.scope === 'project' ? `/w/$workspaceSlug/p/$projectSlug/settings/${section.pathSegment}` : `/w/$workspaceSlug/settings/${section.pathSegment}`;
137
137
  const normalizedPath = normalizeRoutePath(path);
138
138
  const routeOwner = routes.get(normalizedPath);
139
139
  if (routeOwner === undefined && !routes.has(normalizedPath)) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/compose/validate-registries.ts"],"sourcesContent":["import type {ClientFeature, NavTabEntry, RouteParentId} 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 parent?: RouteParentId;\n}\n\ntype RouteReferences = Iterable<string | RouteReference>;\n\ninterface LayoutReference {\n id: string;\n path: string;\n featureId?: string;\n parent?: RouteParentId;\n}\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\nfunction validateRoleMetadata(entry: NavTabEntry, feature: ClientFeature): void {\n const rawEntry = entry as NavTabEntry & {minimumRole?: unknown};\n const hasMinimumRole = Object.hasOwn(rawEntry, 'minimumRole');\n if (entry.scope !== 'layout') {\n if (hasMinimumRole) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" has minimum role metadata but is not layout-scoped.`,\n [feature.id],\n );\n }\n return;\n }\n if (hasMinimumRole) {\n if (typeof rawEntry.minimumRole !== 'string' || rawEntry.minimumRole.trim() === '') {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" has invalid minimum role metadata. Expected a non-empty string.`,\n [feature.id],\n );\n }\n }\n}\n\nexport function validateNavigation(\n features: readonly ClientFeature[],\n routeReferences: RouteReferences,\n layoutReferences: readonly LayoutReference[] = [],\n): void {\n const routeReferencesWithLayouts: Array<string | RouteReference> = [...routeReferences];\n for (const layout of layoutReferences) {\n routeReferencesWithLayouts.push({\n path: layout.path,\n ...(layout.featureId ? {featureId: layout.featureId, ownerFeatureId: layout.featureId} : {}),\n ...(layout.parent ? {parent: layout.parent} : {}),\n });\n }\n const routes = routeOwners(routeReferencesWithLayouts);\n const layouts = new Map(layoutReferences.map((layout) => [layout.id, layout]));\n const entries = new Map<string, string>();\n for (const feature of features) {\n for (const entry of feature.navigation ?? []) {\n validateRoleMetadata(entry, feature);\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 (entry.scope === 'layout') {\n if (typeof entry.layout !== 'string' || entry.layout.trim() === '') {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" has invalid layout metadata. Expected a non-empty layout id.`,\n [feature.id],\n );\n }\n const layout = layouts.get(entry.layout);\n if (!layout) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets missing layout \"${entry.layout}\".`,\n [feature.id],\n );\n }\n const route = routeReferencesForPath(routeReferencesWithLayouts, target);\n const isLayoutRoot = route?.path === layout.path;\n const isLayoutDescendant = routeDescendsFromLayout(route, entry.layout, layouts);\n if (!isLayoutRoot && !isLayoutDescendant) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets route \"${target}\" outside layout \"${entry.layout}\".`,\n [feature.id],\n );\n }\n entries.set(entry.id, feature.id);\n continue;\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\nfunction routeReferencesForPath(\n routeReferences: RouteReferences,\n path: string,\n): RouteReference | undefined {\n for (const route of routeReferences) {\n const reference = typeof route === 'string' ? {path: route} : route;\n if (normalizeRoutePath(reference.path) === path) return reference;\n }\n return undefined;\n}\n\nfunction routeDescendsFromLayout(\n route: RouteReference | undefined,\n layoutId: string,\n layouts: ReadonlyMap<string, LayoutReference>,\n): boolean {\n const visited = new Set<string>();\n let parent = route?.parent;\n while (parent && !visited.has(parent)) {\n if (parent === layoutId) return true;\n visited.add(parent);\n parent = layouts.get(parent)?.parent;\n }\n return false;\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","validateRoleMetadata","entry","rawEntry","hasMinimumRole","Object","hasOwn","scope","minimumRole","trim","validateNavigation","features","layoutReferences","routeReferencesWithLayouts","layout","push","parent","routes","layouts","map","entries","navigation","existingFeatureId","get","target","to","routeOwner","has","routeReferencesForPath","isLayoutRoot","isLayoutDescendant","routeDescendsFromLayout","reference","layoutId","visited","Set","add","validateSettingsSections","sections","section","settingsSections","pathSegment","normalizedPath"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/compose/validate-registries.ts"],"sourcesContent":["import type {ClientFeature, NavTabEntry, RouteParentId} 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 parent?: RouteParentId;\n}\n\ntype RouteReferences = Iterable<string | RouteReference>;\n\ninterface LayoutReference {\n id: string;\n path: string;\n featureId?: string;\n parent?: RouteParentId;\n}\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\nfunction validateRoleMetadata(entry: NavTabEntry, feature: ClientFeature): void {\n const rawEntry = entry as NavTabEntry & {minimumRole?: unknown};\n const hasMinimumRole = Object.hasOwn(rawEntry, 'minimumRole');\n if (entry.scope !== 'layout') {\n if (hasMinimumRole) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" has minimum role metadata but is not layout-scoped.`,\n [feature.id],\n );\n }\n return;\n }\n if (hasMinimumRole) {\n if (typeof rawEntry.minimumRole !== 'string' || rawEntry.minimumRole.trim() === '') {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" has invalid minimum role metadata. Expected a non-empty string.`,\n [feature.id],\n );\n }\n }\n}\n\nexport function validateNavigation(\n features: readonly ClientFeature[],\n routeReferences: RouteReferences,\n layoutReferences: readonly LayoutReference[] = [],\n): void {\n const routeReferencesWithLayouts: Array<string | RouteReference> = [...routeReferences];\n for (const layout of layoutReferences) {\n routeReferencesWithLayouts.push({\n path: layout.path,\n ...(layout.featureId ? {featureId: layout.featureId, ownerFeatureId: layout.featureId} : {}),\n ...(layout.parent ? {parent: layout.parent} : {}),\n });\n }\n const routes = routeOwners(routeReferencesWithLayouts);\n const layouts = new Map(layoutReferences.map((layout) => [layout.id, layout]));\n const entries = new Map<string, string>();\n for (const feature of features) {\n for (const entry of feature.navigation ?? []) {\n validateRoleMetadata(entry, feature);\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 (entry.scope === 'layout') {\n if (typeof entry.layout !== 'string' || entry.layout.trim() === '') {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" has invalid layout metadata. Expected a non-empty layout id.`,\n [feature.id],\n );\n }\n const layout = layouts.get(entry.layout);\n if (!layout) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets missing layout \"${entry.layout}\".`,\n [feature.id],\n );\n }\n const route = routeReferencesForPath(routeReferencesWithLayouts, target);\n const isLayoutRoot = route?.path === layout.path;\n const isLayoutDescendant = routeDescendsFromLayout(route, entry.layout, layouts);\n if (!isLayoutRoot && !isLayoutDescendant) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets route \"${target}\" outside layout \"${entry.layout}\".`,\n [feature.id],\n );\n }\n entries.set(entry.id, feature.id);\n continue;\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\nfunction routeReferencesForPath(\n routeReferences: RouteReferences,\n path: string,\n): RouteReference | undefined {\n for (const route of routeReferences) {\n const reference = typeof route === 'string' ? {path: route} : route;\n if (normalizeRoutePath(reference.path) === path) return reference;\n }\n return undefined;\n}\n\nfunction routeDescendsFromLayout(\n route: RouteReference | undefined,\n layoutId: string,\n layouts: ReadonlyMap<string, LayoutReference>,\n): boolean {\n const visited = new Set<string>();\n let parent = route?.parent;\n while (parent && !visited.has(parent)) {\n if (parent === layoutId) return true;\n visited.add(parent);\n parent = layouts.get(parent)?.parent;\n }\n return false;\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 =\n section.scope === 'project'\n ? `/w/$workspaceSlug/p/$projectSlug/settings/${section.pathSegment}`\n : `/w/$workspaceSlug/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","validateRoleMetadata","entry","rawEntry","hasMinimumRole","Object","hasOwn","scope","minimumRole","trim","validateNavigation","features","layoutReferences","routeReferencesWithLayouts","layout","push","parent","routes","layouts","map","entries","navigation","existingFeatureId","get","target","to","routeOwner","has","routeReferencesForPath","isLayoutRoot","isLayoutDescendant","routeDescendsFromLayout","reference","layoutId","visited","Set","add","validateSettingsSections","sections","section","settingsSections","pathSegment","normalizedPath"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import type { IconName } from '@shipfox/react-ui/icon';
2
2
  import type { ComponentType, PropsWithChildren } from 'react';
3
3
  import type { z } from 'zod';
4
- export type AnchorId = 'root' | 'workspaceLayout' | 'projectLayout' | 'workspaceSettings';
4
+ export type AnchorId = 'root' | 'workspaceLayout' | 'projectLayout' | 'workspaceSettings' | 'projectSettings';
5
5
  /** A fixed shell anchor or the stable id of a feature-owned layout. */
6
6
  export type RouteParentId = AnchorId | (string & {});
7
7
  export interface RouteContribution {
@@ -43,6 +43,7 @@ export interface SettingsSectionEntry {
43
43
  pathSegment: string;
44
44
  label: string;
45
45
  icon: IconName;
46
+ scope?: 'workspace' | 'project';
46
47
  order?: number;
47
48
  }
48
49
  export interface ClientFeature<S extends z.ZodRawShape = z.ZodRawShape> {
@@ -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,uEAAuE;AACvE,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAErD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CAC7C;AAED,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,WAAW,GACnB,CAAC,eAAe,GAAG;IAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,CAAA;CAAC,CAAC,GACpE,CAAC,eAAe,GAAG;IAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAC,CAAC,CAAC;AAEhF,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,WAAW,EAAE;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAC,CAAC,CAAC;AAE5E,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,OAAO,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACxC,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,GAChB,MAAM,GACN,iBAAiB,GACjB,eAAe,GACf,mBAAmB,GACnB,iBAAiB,CAAC;AAEtB,uEAAuE;AACvE,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAErD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CAC7C;AAED,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,WAAW,GACnB,CAAC,eAAe,GAAG;IAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,CAAA;CAAC,CAAC,GACpE,CAAC,eAAe,GAAG;IAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAC,CAAC,CAAC;AAEhF,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,WAAW,EAAE;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAC,CAAC,CAAC;AAE5E,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,WAAW,GAAG,SAAS,CAAC;IAChC,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,OAAO,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACxC,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\n/** A fixed shell anchor or the stable id of a feature-owned layout. */\nexport type RouteParentId = AnchorId | (string & {});\n\nexport interface RouteContribution {\n path: string;\n parent: RouteParentId;\n override?: boolean;\n impl: string;\n}\n\nexport interface LayoutContribution {\n id: string;\n path: string;\n parent: RouteParentId;\n impl: string;\n}\n\nexport interface FeatureProvider {\n id: string;\n Component: ComponentType<PropsWithChildren>;\n}\n\ninterface NavTabEntryBase {\n id: string;\n label: string;\n to: string;\n exact?: boolean;\n order?: number;\n}\n\nexport type NavTabEntry =\n | (NavTabEntryBase & {scope: 'workspace' | 'project'; layout?: never})\n | (NavTabEntryBase & {scope: 'layout'; layout: string; minimumRole?: string});\n\nexport type LayoutNavigationEntry = Extract<NavTabEntry, {scope: 'layout'}>;\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 layouts?: readonly LayoutContribution[];\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":"AAkEA,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 =\n | 'root'\n | 'workspaceLayout'\n | 'projectLayout'\n | 'workspaceSettings'\n | 'projectSettings';\n\n/** A fixed shell anchor or the stable id of a feature-owned layout. */\nexport type RouteParentId = AnchorId | (string & {});\n\nexport interface RouteContribution {\n path: string;\n parent: RouteParentId;\n override?: boolean;\n impl: string;\n}\n\nexport interface LayoutContribution {\n id: string;\n path: string;\n parent: RouteParentId;\n impl: string;\n}\n\nexport interface FeatureProvider {\n id: string;\n Component: ComponentType<PropsWithChildren>;\n}\n\ninterface NavTabEntryBase {\n id: string;\n label: string;\n to: string;\n exact?: boolean;\n order?: number;\n}\n\nexport type NavTabEntry =\n | (NavTabEntryBase & {scope: 'workspace' | 'project'; layout?: never})\n | (NavTabEntryBase & {scope: 'layout'; layout: string; minimumRole?: string});\n\nexport type LayoutNavigationEntry = Extract<NavTabEntry, {scope: 'layout'}>;\n\nexport interface SettingsSectionEntry {\n id: string;\n pathSegment: string;\n label: string;\n icon: IconName;\n scope?: 'workspace' | 'project';\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 layouts?: readonly LayoutContribution[];\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":"AAwEA,OAAO,SAASA,oBAAmDC,OAAU;IAC3E,OAAOA;AACT"}
@@ -17,6 +17,7 @@ export interface WorkspaceMembership {
17
17
  export interface WorkspaceSummary {
18
18
  id: string;
19
19
  name: string;
20
+ slug: string;
20
21
  membershipId: string;
21
22
  status?: 'active' | 'suspended' | 'deleted';
22
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/core/session.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,aAAa,CAAC;AAE5E,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;CAC7C"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/core/session.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,aAAa,CAAC;AAE5E,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;CAC7C"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/session.ts"],"sourcesContent":["export type AdminRole = 'admin-observer' | 'admin-operator' | 'admin-owner';\n\nexport interface UserIdentity {\n id: string;\n email: string;\n name?: string;\n emailVerifiedAt?: string;\n adminRole?: AdminRole;\n}\n\nexport interface AuthenticatedSession {\n accessToken: string;\n user: UserIdentity;\n}\n\nexport interface WorkspaceMembership {\n id: string;\n workspaceId: string;\n}\n\nexport interface WorkspaceSummary {\n id: string;\n name: string;\n membershipId: string;\n status?: 'active' | 'suspended' | 'deleted';\n}\n"],"names":[],"mappings":"AAoBA,WAKC"}
1
+ {"version":3,"sources":["../../src/core/session.ts"],"sourcesContent":["export type AdminRole = 'admin-observer' | 'admin-operator' | 'admin-owner';\n\nexport interface UserIdentity {\n id: string;\n email: string;\n name?: string;\n emailVerifiedAt?: string;\n adminRole?: AdminRole;\n}\n\nexport interface AuthenticatedSession {\n accessToken: string;\n user: UserIdentity;\n}\n\nexport interface WorkspaceMembership {\n id: string;\n workspaceId: string;\n}\n\nexport interface WorkspaceSummary {\n id: string;\n name: string;\n slug: string;\n membershipId: string;\n status?: 'active' | 'suspended' | 'deleted';\n}\n"],"names":[],"mappings":"AAoBA,WAMC"}
@@ -1 +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,CAczB;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"}
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,CAezB;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"}
@@ -24,6 +24,7 @@ export async function listUserWorkspaces(token, signal) {
24
24
  memberships: response.memberships.map((membership)=>({
25
25
  id: membership.workspace_id,
26
26
  name: membership.workspace_name,
27
+ slug: membership.workspace_slug,
27
28
  membershipId: membership.id,
28
29
  status: membership.workspace_status
29
30
  }))
@@ -1 +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 status: membership.workspace_status,\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","status","workspace_status","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;gBAC3BK,QAAQN,WAAWO,gBAAgB;YACrC,CAAA;IACF;AACF;AAEA,OAAO,SAASC;IACd,OAAOpB,aAAa;QAClBqB,UAAUnB;QACVoB,SAAS,CAAC,EAAChB,MAAM,EAAC,GAAKiB,4BAA4BjB;QACnDkB,OAAO;QACPC,WAAW;IACb;AACF;AAEA,OAAO,SAASC,2BAA2BrB,KAAc;IACvD,OAAOL,aAAa;QAClBqB,UAAUlB;QACVmB,SAAS,CAAC,EAAChB,MAAM,EAAC,GAAKF,mBAAmBC,OAAOC;QACjDkB,OAAO;QACPC,WAAW;IACb;AACF;AAEA,OAAO,eAAeF,4BACpBjB,MAAoB;IAEpB,MAAMC,WAAW,MAAMR,kBAAkBF,qBAAqB,iBAAiB;QAC7E8B,QAAQ;QACRrB;IACF;IACA,OAAOL,uBAAuBM;AAChC"}
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 slug: membership.workspace_slug,\n membershipId: membership.id,\n status: membership.workspace_status,\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","slug","workspace_slug","membershipId","status","workspace_status","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,MAAML,WAAWM,cAAc;gBAC/BC,cAAcP,WAAWC,EAAE;gBAC3BO,QAAQR,WAAWS,gBAAgB;YACrC,CAAA;IACF;AACF;AAEA,OAAO,SAASC;IACd,OAAOtB,aAAa;QAClBuB,UAAUrB;QACVsB,SAAS,CAAC,EAAClB,MAAM,EAAC,GAAKmB,4BAA4BnB;QACnDoB,OAAO;QACPC,WAAW;IACb;AACF;AAEA,OAAO,SAASC,2BAA2BvB,KAAc;IACvD,OAAOL,aAAa;QAClBuB,UAAUpB;QACVqB,SAAS,CAAC,EAAClB,MAAM,EAAC,GAAKF,mBAAmBC,OAAOC;QACjDoB,OAAO;QACPC,WAAW;IACb;AACF;AAEA,OAAO,eAAeF,4BACpBnB,MAAoB;IAEpB,MAAMC,WAAW,MAAMR,kBAAkBF,qBAAqB,iBAAiB;QAC7EgC,QAAQ;QACRvB;IACF;IACA,OAAOL,uBAAuBM;AAChC"}
@@ -2,12 +2,12 @@ import { useAuthState } from './auth.js';
2
2
  import { parseWorkspaceParams, useRouteParams } from './route-inputs.js';
3
3
  export function useActiveWorkspace() {
4
4
  const workspace = useMaybeActiveWorkspace();
5
- if (!workspace) throw new Error('useActiveWorkspace called outside a /workspaces/$wid route');
5
+ if (!workspace) throw new Error('useActiveWorkspace called outside a /w/$workspaceSlug route');
6
6
  return workspace;
7
7
  }
8
8
  export function useMaybeActiveWorkspace() {
9
- const { wid } = useRouteParams(parseWorkspaceParams);
10
- return useAuthState().workspaces.find((workspace)=>workspace.id === wid);
9
+ const { workspaceSlug } = useRouteParams(parseWorkspaceParams);
10
+ return useAuthState().workspaces.find((workspace)=>workspace.slug === workspaceSlug);
11
11
  }
12
12
 
13
13
  //# sourceMappingURL=active-workspace.js.map
@@ -1 +1 @@
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"}
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 /w/$workspaceSlug route');\n return workspace;\n}\n\nexport function useMaybeActiveWorkspace(): Workspace | undefined {\n const {workspaceSlug} = useRouteParams(parseWorkspaceParams);\n return useAuthState().workspaces.find((workspace) => workspace.slug === workspaceSlug);\n}\n"],"names":["useAuthState","parseWorkspaceParams","useRouteParams","useActiveWorkspace","workspace","useMaybeActiveWorkspace","Error","workspaceSlug","workspaces","find","slug"],"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,aAAa,EAAC,GAAGL,eAAeD;IACvC,OAAOD,eAAeQ,UAAU,CAACC,IAAI,CAAC,CAACL,YAAcA,UAAUM,IAAI,KAAKH;AAC1E"}