@shipfox/client-shell 2.0.0 → 4.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 (111) hide show
  1. package/assets/apple-touch-icon.png +0 -0
  2. package/assets/favicon-96x96.png +0 -0
  3. package/assets/favicon.ico +0 -0
  4. package/assets/favicon.svg +1 -0
  5. package/assets/site.webmanifest +22 -0
  6. package/assets/web-app-manifest-192x192.png +0 -0
  7. package/assets/web-app-manifest-512x512.png +0 -0
  8. package/dist/runtime/auth.d.ts +4 -0
  9. package/dist/runtime/auth.d.ts.map +1 -1
  10. package/dist/runtime/auth.js +64 -19
  11. package/dist/runtime/auth.js.map +1 -1
  12. package/dist/runtime/last-workspace.d.ts +1 -1
  13. package/dist/runtime/last-workspace.d.ts.map +1 -1
  14. package/dist/runtime/last-workspace.js +33 -14
  15. package/dist/runtime/last-workspace.js.map +1 -1
  16. package/dist/tsconfig.test.tsbuildinfo +1 -1
  17. package/dist/vite/index.d.ts +1 -0
  18. package/dist/vite/index.d.ts.map +1 -1
  19. package/dist/vite/index.js +1 -0
  20. package/dist/vite/index.js.map +1 -1
  21. package/dist/vite/manifest.d.ts +3 -0
  22. package/dist/vite/manifest.d.ts.map +1 -0
  23. package/dist/vite/manifest.js +140 -0
  24. package/dist/vite/manifest.js.map +1 -0
  25. package/package.json +11 -27
  26. package/.turbo/turbo-build.log +0 -2
  27. package/.turbo/turbo-type$colon$emit.log +0 -1
  28. package/.turbo/turbo-type.log +0 -1
  29. package/CHANGELOG.md +0 -59
  30. package/src/components/main-layout.tsx +0 -45
  31. package/src/components/nav-bar.tsx +0 -41
  32. package/src/components/nav-tabs.test.tsx +0 -60
  33. package/src/components/nav-tabs.tsx +0 -47
  34. package/src/components/not-found-page.tsx +0 -14
  35. package/src/components/settings-nav.tsx +0 -35
  36. package/src/components/user-menu.tsx +0 -59
  37. package/src/components/workspace-crumb.tsx +0 -43
  38. package/src/components/workspace-switcher.tsx +0 -75
  39. package/src/compose/compose-routes.test.ts +0 -112
  40. package/src/compose/compose-routes.ts +0 -60
  41. package/src/compose/errors.ts +0 -54
  42. package/src/compose/merge-config.test.ts +0 -26
  43. package/src/compose/merge-config.ts +0 -27
  44. package/src/compose/normalize-route-path.ts +0 -6
  45. package/src/compose/validate-providers.test.ts +0 -31
  46. package/src/compose/validate-providers.ts +0 -36
  47. package/src/compose/validate-registries.test.ts +0 -92
  48. package/src/compose/validate-registries.ts +0 -61
  49. package/src/config/config-screen.test.tsx +0 -37
  50. package/src/config/shell-config.ts +0 -10
  51. package/src/contract.ts +0 -47
  52. package/src/index.ts +0 -1
  53. package/src/runtime/active-workspace.ts +0 -13
  54. package/src/runtime/anchor-paths.ts +0 -18
  55. package/src/runtime/anchors.tsx +0 -106
  56. package/src/runtime/assemble-route-tree.ts +0 -42
  57. package/src/runtime/auth.tsx +0 -213
  58. package/src/runtime/chrome-context.tsx +0 -22
  59. package/src/runtime/compose-client-app.tsx +0 -122
  60. package/src/runtime/define-route.test.ts +0 -15
  61. package/src/runtime/define-route.ts +0 -33
  62. package/src/runtime/index.ts +0 -19
  63. package/src/runtime/last-workspace.ts +0 -22
  64. package/src/runtime/nav-order.ts +0 -1
  65. package/src/runtime/provider-stack.tsx +0 -39
  66. package/src/runtime/providers.test.tsx +0 -51
  67. package/src/runtime/registries.test.tsx +0 -100
  68. package/src/runtime/registries.ts +0 -42
  69. package/src/runtime/router-context.ts +0 -9
  70. package/src/runtime/routes.test.tsx +0 -48
  71. package/src/runtime/workspace-setup.tsx +0 -61
  72. package/src/testing/index.test.tsx +0 -57
  73. package/src/testing/index.tsx +0 -51
  74. package/src/vite/evaluate-features.test.ts +0 -89
  75. package/src/vite/evaluate-features.ts +0 -114
  76. package/src/vite/generate.test.ts +0 -22
  77. package/src/vite/generate.ts +0 -99
  78. package/src/vite/generated-router.test.tsx +0 -58
  79. package/src/vite/index.ts +0 -1
  80. package/src/vite/plugin.test.ts +0 -231
  81. package/src/vite/plugin.ts +0 -101
  82. package/src/vite/validate.test.ts +0 -166
  83. package/test/default-route-impl.tsx +0 -3
  84. package/test/external/FINDINGS.md +0 -41
  85. package/test/external/README.md +0 -37
  86. package/test/external/fixture/index.html +0 -12
  87. package/test/external/fixture/package.json +0 -31
  88. package/test/external/fixture/src/app.fixture.tsx +0 -104
  89. package/test/external/fixture/src/features/external-settings.tsx +0 -19
  90. package/test/external/fixture/src/features/login-override.tsx +0 -9
  91. package/test/external/fixture/src/features.collision.ts +0 -15
  92. package/test/external/fixture/src/features.ts +0 -50
  93. package/test/external/fixture/src/link-typecheck.tsx +0 -10
  94. package/test/external/fixture/src/main.tsx +0 -86
  95. package/test/external/fixture/src/provider.ts +0 -45
  96. package/test/external/fixture/tsconfig.json +0 -15
  97. package/test/external/fixture/vite.config.ts +0 -21
  98. package/test/external/fixture/vitest.config.ts +0 -17
  99. package/test/external/verify.mjs +0 -488
  100. package/test/fixtures/features.ts +0 -56
  101. package/test/named-route-impl.tsx +0 -3
  102. package/test/render.tsx +0 -67
  103. package/test/search-route-impl.tsx +0 -7
  104. package/test/setup.ts +0 -3
  105. package/test/typecheck/shipfox-app.gen.ts +0 -85
  106. package/test/typecheck/types.tsx +0 -23
  107. package/tsconfig.build.json +0 -10
  108. package/tsconfig.build.tsbuildinfo +0 -1
  109. package/tsconfig.json +0 -1
  110. package/tsconfig.test.json +0 -12
  111. package/vitest.config.ts +0 -22
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none"><title>Shipfox</title><path fill-rule="evenodd" clip-rule="evenodd" d="M35.4766 71.1779L87.8471 174.764C92.9421 184.842 107.058 184.842 112.153 174.764L164.523 71.1779L192.438 85.4265C200.787 89.6883 202.593 101.048 195.992 107.787L109.671 195.911C104.33 201.363 95.6704 201.363 90.3295 195.911L4.0079 107.787C-2.59278 101.048 -0.787007 89.6883 7.56226 85.4265L35.4766 71.1779ZM100 38.2425L171.913 1.53536C183.748 -4.50572 196.251 8.42284 190.182 20.4265L164.523 71.1779L100 38.2425ZM100 38.2425L28.0872 1.53536C16.2522 -4.50571 3.74943 8.42284 9.81817 20.4265L35.4766 71.1779L100 38.2425Z" fill="#FF4B00" style="fill:#FF4B00;fill:color(display-p3 1.0000 0.2941 0.0000);fill-opacity:1;"></path></svg>
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "Shipfox",
3
+ "short_name": "Shipfox",
4
+ "icons": [
5
+ {
6
+ "src": "/web-app-manifest-192x192.png",
7
+ "sizes": "192x192",
8
+ "type": "image/png",
9
+ "purpose": "maskable"
10
+ },
11
+ {
12
+ "src": "/web-app-manifest-512x512.png",
13
+ "sizes": "512x512",
14
+ "type": "image/png",
15
+ "purpose": "maskable"
16
+ }
17
+ ],
18
+ "theme_color": "#ff4b00",
19
+ "background_color": "#ff4b00",
20
+ "display": "standalone",
21
+ "start_url": "/"
22
+ }
@@ -31,6 +31,10 @@ export declare function listUserWorkspaces(token?: string): Promise<{
31
31
  memberships: MembershipWithWorkspaceDto[];
32
32
  }>;
33
33
  export declare function getAuthRefreshDelayMs(token: string, nowMs?: number): number | undefined;
34
+ export declare function useAuthTransition(): {
35
+ enterAuthenticated: (result: LoginResponseDto) => Promise<void>;
36
+ enterGuest: () => Promise<void>;
37
+ };
34
38
  export declare function useRefreshAuth(): () => Promise<{
35
39
  token: string;
36
40
  user: {
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/runtime/auth.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,gBAAgB,EAAE,OAAO,EAAC,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAC,0BAA0B,EAAC,MAAM,6BAA6B,CAAC;AAI5E,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAO7C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,eAAe,GAAG,OAAO,CAAC;AAE/D,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,eAAO,MAAM,gBAAgB,EAAE,SAA+B,CAAC;AAC/D,eAAO,MAAM,aAAa;;CAAoC,CAAC;AAC/D,eAAO,MAAM,mBAAmB,8BAA+B,CAAC;AAChE,eAAO,MAAM,sBAAsB,iCAAkC,CAAC;AAEtE,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,gBAAgB,EACxB,WAAW,GAAE,0BAA0B,EAAO,GAC7C,SAAS,CAWX;AAED,wBAAgB,YAAY,IAAI,cAAc,CAY7C;AAED,wBAAsB,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM;iBACf,0BAA0B,EAAE;GAInE;AAwBD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAa,GAAG,MAAM,GAAG,SAAS,CAG3F;AAED,wBAAgB,cAAc;;;;;;;;;;;GA8B7B;AAED,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACzD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,WAAW,CAAC,EAAC,QAAQ,EAAE,OAAc,EAAC,EAAE,gBAAgB,6BAyEvE"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/runtime/auth.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,gBAAgB,EAAE,OAAO,EAAC,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAC,0BAA0B,EAAC,MAAM,6BAA6B,CAAC;AAI5E,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAO7C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,eAAe,GAAG,OAAO,CAAC;AAE/D,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,eAAO,MAAM,gBAAgB,EAAE,SAA+B,CAAC;AAC/D,eAAO,MAAM,aAAa;;CAAoC,CAAC;AAE/D,eAAO,MAAM,mBAAmB,8BAA+B,CAAC;AAChE,eAAO,MAAM,sBAAsB,iCAAkC,CAAC;AAEtE,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,gBAAgB,EACxB,WAAW,GAAE,0BAA0B,EAAO,GAC7C,SAAS,CAWX;AAED,wBAAgB,YAAY,IAAI,cAAc,CAY7C;AAED,wBAAsB,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM;iBACf,0BAA0B,EAAE;GAInE;AAwBD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAa,GAAG,MAAM,GAAG,SAAS,CAG3F;AAED,wBAAgB,iBAAiB;iCAmBd,gBAAgB;;EAgClC;AAED,wBAAgB,cAAc;;;;;;;;;;;GAkB7B;AAED,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACzD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,WAAW,CAAC,EAAC,QAAQ,EAAE,OAAc,EAAC,EAAE,gBAAgB,6BAyEvE"}
@@ -12,6 +12,7 @@ export const initialAuthState = {
12
12
  status: 'loading'
13
13
  };
14
14
  export const authStateAtom = atom(initialAuthState);
15
+ const authTransitionEpochAtom = atom(0);
15
16
  export const authRefreshQueryKey = [
16
17
  'auth',
17
18
  'refresh'
@@ -74,9 +75,66 @@ export function getAuthRefreshDelayMs(token, nowMs = Date.now()) {
74
75
  const exp = readJwtExp(token);
75
76
  return exp === undefined ? undefined : exp * 1000 - nowMs - REFRESH_EARLY_MS;
76
77
  }
77
- export function useRefreshAuth() {
78
+ export function useAuthTransition() {
78
79
  const queryClient = useQueryClient();
80
+ const store = useStore();
79
81
  const setState = useSetAtom(authStateAtom);
82
+ const clearPrivateState = useCallback(async ()=>{
83
+ await queryClient.cancelQueries();
84
+ queryClient.clear();
85
+ }, [
86
+ queryClient
87
+ ]);
88
+ const enterGuest = useCallback(async ()=>{
89
+ const transitionEpoch = store.get(authTransitionEpochAtom) + 1;
90
+ store.set(authTransitionEpochAtom, transitionEpoch);
91
+ await clearPrivateState();
92
+ if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;
93
+ setState({
94
+ status: 'guest'
95
+ });
96
+ }, [
97
+ clearPrivateState,
98
+ setState,
99
+ store
100
+ ]);
101
+ const enterAuthenticated = useCallback(async (result)=>{
102
+ const transitionEpoch = store.get(authTransitionEpochAtom) + 1;
103
+ store.set(authTransitionEpochAtom, transitionEpoch);
104
+ const previousState = store.get(authStateAtom);
105
+ const principalChanged = previousState.status === 'authenticated' && previousState.user?.id !== result.user.id;
106
+ if (principalChanged) await clearPrivateState();
107
+ if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;
108
+ queryClient.setQueryData(authRefreshQueryKey, result);
109
+ let memberships = [];
110
+ try {
111
+ const workspaces = await queryClient.fetchQuery({
112
+ queryKey: userWorkspacesQueryKey,
113
+ queryFn: ()=>listUserWorkspaces(result.token),
114
+ retry: false,
115
+ staleTime: 0
116
+ });
117
+ memberships = workspaces.memberships;
118
+ queryClient.setQueryData(userWorkspacesQueryKey, workspaces);
119
+ } catch {
120
+ // The authenticated session remains usable while workspace hydration retries on the next route load.
121
+ }
122
+ if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;
123
+ setState(toAuthenticatedState(result, memberships));
124
+ }, [
125
+ clearPrivateState,
126
+ queryClient,
127
+ setState,
128
+ store
129
+ ]);
130
+ return {
131
+ enterAuthenticated,
132
+ enterGuest
133
+ };
134
+ }
135
+ export function useRefreshAuth() {
136
+ const queryClient = useQueryClient();
137
+ const { enterAuthenticated, enterGuest } = useAuthTransition();
80
138
  return useCallback(async ()=>{
81
139
  try {
82
140
  const result = await queryClient.fetchQuery({
@@ -85,29 +143,16 @@ export function useRefreshAuth() {
85
143
  retry: false,
86
144
  staleTime: 0
87
145
  });
88
- let memberships = [];
89
- try {
90
- const workspaces = await queryClient.fetchQuery({
91
- queryKey: userWorkspacesQueryKey,
92
- queryFn: ()=>listUserWorkspaces(result.token),
93
- retry: false,
94
- staleTime: 0
95
- });
96
- memberships = workspaces.memberships;
97
- } catch {
98
- // A valid session remains usable while workspace hydration retries on the next route load.
99
- }
100
- setState(toAuthenticatedState(result, memberships));
146
+ await enterAuthenticated(result);
101
147
  return result;
102
148
  } catch (error) {
103
- if (error instanceof ApiError && error.status === 401) setState({
104
- status: 'guest'
105
- });
149
+ if (error instanceof ApiError && error.status === 401) await enterGuest();
106
150
  throw error;
107
151
  }
108
152
  }, [
109
- queryClient,
110
- setState
153
+ enterAuthenticated,
154
+ enterGuest,
155
+ queryClient
111
156
  ]);
112
157
  }
113
158
  export function AuthRuntime({ children, effects = true }) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runtime/auth.tsx"],"sourcesContent":["import type {LoginResponseDto, UserDto} from '@shipfox/api-auth-dto';\nimport type {MembershipWithWorkspaceDto} from '@shipfox/api-workspaces-dto';\nimport {ApiError, apiRequest, configureApiClient} from '@shipfox/client-api';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {atom, useAtomValue, useSetAtom, useStore} from 'jotai';\nimport type {PropsWithChildren} from 'react';\nimport {useCallback, useEffect, useMemo} from 'react';\n\nconst REFRESH_EARLY_MS = 5 * 60 * 1000;\nconst REFRESH_RETRY_DELAY_MS = 60_000;\nconst BASE64_URL_REPLACEMENTS = {dash: /-/g, underscore: /_/g} as const;\n\nexport type AuthStatus = 'loading' | 'authenticated' | 'guest';\n\nexport interface Workspace {\n id: string;\n name: string;\n membershipId: string;\n}\n\nexport interface AuthState {\n status: AuthStatus;\n token?: string;\n user?: UserDto;\n workspaces?: Workspace[];\n}\n\nexport interface AuthStateValue extends AuthState {\n isLoading: boolean;\n isAuthenticated: boolean;\n workspaces: Workspace[];\n hasWorkspace: boolean;\n}\n\nexport const initialAuthState: AuthState = {status: 'loading'};\nexport const authStateAtom = atom<AuthState>(initialAuthState);\nexport const authRefreshQueryKey = ['auth', 'refresh'] as const;\nexport const userWorkspacesQueryKey = ['workspaces', 'mine'] as const;\n\nexport function toAuthenticatedState(\n result: LoginResponseDto,\n memberships: MembershipWithWorkspaceDto[] = [],\n): AuthState {\n return {\n status: 'authenticated',\n token: result.token,\n user: result.user,\n workspaces: memberships.map((membership) => ({\n id: membership.workspace_id,\n name: membership.workspace_name,\n membershipId: membership.id,\n })),\n };\n}\n\nexport function useAuthState(): AuthStateValue {\n const state = useAtomValue(authStateAtom);\n return useMemo(\n () => ({\n ...state,\n workspaces: state.workspaces ?? [],\n isLoading: state.status === 'loading',\n isAuthenticated: state.status === 'authenticated',\n hasWorkspace: (state.workspaces ?? []).length > 0,\n }),\n [state],\n );\n}\n\nexport async function listUserWorkspaces(token?: string) {\n return await apiRequest<{memberships: MembershipWithWorkspaceDto[]}>(\n '/workspaces',\n token ? {headers: {authorization: `Bearer ${token}`}} : {},\n );\n}\n\nasync function refreshAuthRequest() {\n return await apiRequest<LoginResponseDto>('/auth/refresh', {method: 'POST'});\n}\n\nfunction decodeBase64Url(value: string): string {\n const base64 = value\n .replace(BASE64_URL_REPLACEMENTS.dash, '+')\n .replace(BASE64_URL_REPLACEMENTS.underscore, '/');\n return atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, '='));\n}\n\nfunction readJwtExp(token: string): number | undefined {\n const [, payload] = token.split('.');\n if (!payload) return undefined;\n try {\n const parsed = JSON.parse(decodeBase64Url(payload)) as {exp?: unknown};\n return typeof parsed.exp === 'number' && Number.isFinite(parsed.exp) ? parsed.exp : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function getAuthRefreshDelayMs(token: string, nowMs = Date.now()): number | undefined {\n const exp = readJwtExp(token);\n return exp === undefined ? undefined : exp * 1000 - nowMs - REFRESH_EARLY_MS;\n}\n\nexport function useRefreshAuth() {\n const queryClient = useQueryClient();\n const setState = useSetAtom(authStateAtom);\n return useCallback(async () => {\n try {\n const result = await queryClient.fetchQuery({\n queryKey: authRefreshQueryKey,\n queryFn: refreshAuthRequest,\n retry: false,\n staleTime: 0,\n });\n let memberships: MembershipWithWorkspaceDto[] = [];\n try {\n const workspaces = await queryClient.fetchQuery({\n queryKey: userWorkspacesQueryKey,\n queryFn: () => listUserWorkspaces(result.token),\n retry: false,\n staleTime: 0,\n });\n memberships = workspaces.memberships;\n } catch {\n // A valid session remains usable while workspace hydration retries on the next route load.\n }\n setState(toAuthenticatedState(result, memberships));\n return result;\n } catch (error) {\n if (error instanceof ApiError && error.status === 401) setState({status: 'guest'});\n throw error;\n }\n }, [queryClient, setState]);\n}\n\nexport interface AuthRuntimeProps extends PropsWithChildren {\n effects?: boolean;\n}\n\nexport function AuthRuntime({children, effects = true}: AuthRuntimeProps) {\n const store = useStore();\n const authState = useAtomValue(authStateAtom);\n const refreshAuth = useRefreshAuth();\n\n useEffect(() => {\n if (!effects) return;\n configureApiClient({\n getAccessToken: () => store.get(authStateAtom).token,\n refreshAccessToken: async () => (await refreshAuth()).token,\n });\n }, [effects, refreshAuth, store]);\n\n useEffect(() => {\n if (!effects) return;\n refreshAuth().catch(() => undefined);\n }, [effects, refreshAuth]);\n\n useEffect(() => {\n if (!effects || authState.status !== 'authenticated' || !authState.token) return;\n\n let timeout: ReturnType<typeof setTimeout> | undefined;\n let disposed = false;\n let refreshing = false;\n const clearRefreshTimer = () => {\n if (timeout !== undefined) clearTimeout(timeout);\n timeout = undefined;\n };\n const scheduleRefresh = (delayMs: number) => {\n clearRefreshTimer();\n timeout = setTimeout(runRefresh, Math.max(0, delayMs));\n };\n const retryIfStillDue = () => {\n const current = store.get(authStateAtom);\n if (current.status !== 'authenticated' || !current.token) return;\n const delay = getAuthRefreshDelayMs(current.token);\n if (delay !== undefined && delay <= 0) scheduleRefresh(REFRESH_RETRY_DELAY_MS);\n };\n function runRefresh() {\n if (disposed || refreshing) return;\n refreshing = true;\n clearRefreshTimer();\n refreshAuth()\n .catch(() => undefined)\n .finally(() => {\n refreshing = false;\n if (!disposed) retryIfStillDue();\n });\n }\n const refreshIfDue = () => {\n const current = store.get(authStateAtom);\n if (current.status !== 'authenticated' || !current.token) return;\n const delay = getAuthRefreshDelayMs(current.token);\n if (delay !== undefined && delay <= 0) runRefresh();\n };\n const refreshIfVisible = () => {\n if (document.visibilityState === 'visible') refreshIfDue();\n };\n const delay = getAuthRefreshDelayMs(authState.token);\n if (delay !== undefined) scheduleRefresh(delay);\n window.addEventListener('focus', refreshIfDue);\n window.addEventListener('online', refreshIfDue);\n document.addEventListener('visibilitychange', refreshIfVisible);\n return () => {\n disposed = true;\n clearRefreshTimer();\n window.removeEventListener('focus', refreshIfDue);\n window.removeEventListener('online', refreshIfDue);\n document.removeEventListener('visibilitychange', refreshIfVisible);\n };\n }, [authState.status, authState.token, effects, refreshAuth, store]);\n\n return children;\n}\n"],"names":["ApiError","apiRequest","configureApiClient","useQueryClient","atom","useAtomValue","useSetAtom","useStore","useCallback","useEffect","useMemo","REFRESH_EARLY_MS","REFRESH_RETRY_DELAY_MS","BASE64_URL_REPLACEMENTS","dash","underscore","initialAuthState","status","authStateAtom","authRefreshQueryKey","userWorkspacesQueryKey","toAuthenticatedState","result","memberships","token","user","workspaces","map","membership","id","workspace_id","name","workspace_name","membershipId","useAuthState","state","isLoading","isAuthenticated","hasWorkspace","length","listUserWorkspaces","headers","authorization","refreshAuthRequest","method","decodeBase64Url","value","base64","replace","atob","padEnd","Math","ceil","readJwtExp","payload","split","undefined","parsed","JSON","parse","exp","Number","isFinite","getAuthRefreshDelayMs","nowMs","Date","now","useRefreshAuth","queryClient","setState","fetchQuery","queryKey","queryFn","retry","staleTime","error","AuthRuntime","children","effects","store","authState","refreshAuth","getAccessToken","get","refreshAccessToken","catch","timeout","disposed","refreshing","clearRefreshTimer","clearTimeout","scheduleRefresh","delayMs","setTimeout","runRefresh","max","retryIfStillDue","current","delay","finally","refreshIfDue","refreshIfVisible","document","visibilityState","window","addEventListener","removeEventListener"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/runtime/auth.tsx"],"sourcesContent":["import type {LoginResponseDto, UserDto} from '@shipfox/api-auth-dto';\nimport type {MembershipWithWorkspaceDto} from '@shipfox/api-workspaces-dto';\nimport {ApiError, apiRequest, configureApiClient} from '@shipfox/client-api';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {atom, useAtomValue, useSetAtom, useStore} from 'jotai';\nimport type {PropsWithChildren} from 'react';\nimport {useCallback, useEffect, useMemo} from 'react';\n\nconst REFRESH_EARLY_MS = 5 * 60 * 1000;\nconst REFRESH_RETRY_DELAY_MS = 60_000;\nconst BASE64_URL_REPLACEMENTS = {dash: /-/g, underscore: /_/g} as const;\n\nexport type AuthStatus = 'loading' | 'authenticated' | 'guest';\n\nexport interface Workspace {\n id: string;\n name: string;\n membershipId: string;\n}\n\nexport interface AuthState {\n status: AuthStatus;\n token?: string;\n user?: UserDto;\n workspaces?: Workspace[];\n}\n\nexport interface AuthStateValue extends AuthState {\n isLoading: boolean;\n isAuthenticated: boolean;\n workspaces: Workspace[];\n hasWorkspace: boolean;\n}\n\nexport const initialAuthState: AuthState = {status: 'loading'};\nexport const authStateAtom = atom<AuthState>(initialAuthState);\nconst authTransitionEpochAtom = atom(0);\nexport const authRefreshQueryKey = ['auth', 'refresh'] as const;\nexport const userWorkspacesQueryKey = ['workspaces', 'mine'] as const;\n\nexport function toAuthenticatedState(\n result: LoginResponseDto,\n memberships: MembershipWithWorkspaceDto[] = [],\n): AuthState {\n return {\n status: 'authenticated',\n token: result.token,\n user: result.user,\n workspaces: memberships.map((membership) => ({\n id: membership.workspace_id,\n name: membership.workspace_name,\n membershipId: membership.id,\n })),\n };\n}\n\nexport function useAuthState(): AuthStateValue {\n const state = useAtomValue(authStateAtom);\n return useMemo(\n () => ({\n ...state,\n workspaces: state.workspaces ?? [],\n isLoading: state.status === 'loading',\n isAuthenticated: state.status === 'authenticated',\n hasWorkspace: (state.workspaces ?? []).length > 0,\n }),\n [state],\n );\n}\n\nexport async function listUserWorkspaces(token?: string) {\n return await apiRequest<{memberships: MembershipWithWorkspaceDto[]}>(\n '/workspaces',\n token ? {headers: {authorization: `Bearer ${token}`}} : {},\n );\n}\n\nasync function refreshAuthRequest() {\n return await apiRequest<LoginResponseDto>('/auth/refresh', {method: 'POST'});\n}\n\nfunction decodeBase64Url(value: string): string {\n const base64 = value\n .replace(BASE64_URL_REPLACEMENTS.dash, '+')\n .replace(BASE64_URL_REPLACEMENTS.underscore, '/');\n return atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, '='));\n}\n\nfunction readJwtExp(token: string): number | undefined {\n const [, payload] = token.split('.');\n if (!payload) return undefined;\n try {\n const parsed = JSON.parse(decodeBase64Url(payload)) as {exp?: unknown};\n return typeof parsed.exp === 'number' && Number.isFinite(parsed.exp) ? parsed.exp : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function getAuthRefreshDelayMs(token: string, nowMs = Date.now()): number | undefined {\n const exp = readJwtExp(token);\n return exp === undefined ? undefined : exp * 1000 - nowMs - REFRESH_EARLY_MS;\n}\n\nexport function useAuthTransition() {\n const queryClient = useQueryClient();\n const store = useStore();\n const setState = useSetAtom(authStateAtom);\n\n const clearPrivateState = useCallback(async () => {\n await queryClient.cancelQueries();\n queryClient.clear();\n }, [queryClient]);\n\n const enterGuest = useCallback(async () => {\n const transitionEpoch = store.get(authTransitionEpochAtom) + 1;\n store.set(authTransitionEpochAtom, transitionEpoch);\n await clearPrivateState();\n if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;\n setState({status: 'guest'});\n }, [clearPrivateState, setState, store]);\n\n const enterAuthenticated = useCallback(\n async (result: LoginResponseDto) => {\n const transitionEpoch = store.get(authTransitionEpochAtom) + 1;\n store.set(authTransitionEpochAtom, transitionEpoch);\n const previousState = store.get(authStateAtom);\n const principalChanged =\n previousState.status === 'authenticated' && previousState.user?.id !== result.user.id;\n\n if (principalChanged) await clearPrivateState();\n if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;\n\n queryClient.setQueryData(authRefreshQueryKey, result);\n let memberships: MembershipWithWorkspaceDto[] = [];\n try {\n const workspaces = await queryClient.fetchQuery({\n queryKey: userWorkspacesQueryKey,\n queryFn: () => listUserWorkspaces(result.token),\n retry: false,\n staleTime: 0,\n });\n memberships = workspaces.memberships;\n queryClient.setQueryData(userWorkspacesQueryKey, workspaces);\n } catch {\n // The authenticated session remains usable while workspace hydration retries on the next route load.\n }\n if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;\n\n setState(toAuthenticatedState(result, memberships));\n },\n [clearPrivateState, queryClient, setState, store],\n );\n\n return {enterAuthenticated, enterGuest};\n}\n\nexport function useRefreshAuth() {\n const queryClient = useQueryClient();\n const {enterAuthenticated, enterGuest} = useAuthTransition();\n return useCallback(async () => {\n try {\n const result = await queryClient.fetchQuery({\n queryKey: authRefreshQueryKey,\n queryFn: refreshAuthRequest,\n retry: false,\n staleTime: 0,\n });\n await enterAuthenticated(result);\n return result;\n } catch (error) {\n if (error instanceof ApiError && error.status === 401) await enterGuest();\n throw error;\n }\n }, [enterAuthenticated, enterGuest, queryClient]);\n}\n\nexport interface AuthRuntimeProps extends PropsWithChildren {\n effects?: boolean;\n}\n\nexport function AuthRuntime({children, effects = true}: AuthRuntimeProps) {\n const store = useStore();\n const authState = useAtomValue(authStateAtom);\n const refreshAuth = useRefreshAuth();\n\n useEffect(() => {\n if (!effects) return;\n configureApiClient({\n getAccessToken: () => store.get(authStateAtom).token,\n refreshAccessToken: async () => (await refreshAuth()).token,\n });\n }, [effects, refreshAuth, store]);\n\n useEffect(() => {\n if (!effects) return;\n refreshAuth().catch(() => undefined);\n }, [effects, refreshAuth]);\n\n useEffect(() => {\n if (!effects || authState.status !== 'authenticated' || !authState.token) return;\n\n let timeout: ReturnType<typeof setTimeout> | undefined;\n let disposed = false;\n let refreshing = false;\n const clearRefreshTimer = () => {\n if (timeout !== undefined) clearTimeout(timeout);\n timeout = undefined;\n };\n const scheduleRefresh = (delayMs: number) => {\n clearRefreshTimer();\n timeout = setTimeout(runRefresh, Math.max(0, delayMs));\n };\n const retryIfStillDue = () => {\n const current = store.get(authStateAtom);\n if (current.status !== 'authenticated' || !current.token) return;\n const delay = getAuthRefreshDelayMs(current.token);\n if (delay !== undefined && delay <= 0) scheduleRefresh(REFRESH_RETRY_DELAY_MS);\n };\n function runRefresh() {\n if (disposed || refreshing) return;\n refreshing = true;\n clearRefreshTimer();\n refreshAuth()\n .catch(() => undefined)\n .finally(() => {\n refreshing = false;\n if (!disposed) retryIfStillDue();\n });\n }\n const refreshIfDue = () => {\n const current = store.get(authStateAtom);\n if (current.status !== 'authenticated' || !current.token) return;\n const delay = getAuthRefreshDelayMs(current.token);\n if (delay !== undefined && delay <= 0) runRefresh();\n };\n const refreshIfVisible = () => {\n if (document.visibilityState === 'visible') refreshIfDue();\n };\n const delay = getAuthRefreshDelayMs(authState.token);\n if (delay !== undefined) scheduleRefresh(delay);\n window.addEventListener('focus', refreshIfDue);\n window.addEventListener('online', refreshIfDue);\n document.addEventListener('visibilitychange', refreshIfVisible);\n return () => {\n disposed = true;\n clearRefreshTimer();\n window.removeEventListener('focus', refreshIfDue);\n window.removeEventListener('online', refreshIfDue);\n document.removeEventListener('visibilitychange', refreshIfVisible);\n };\n }, [authState.status, authState.token, effects, refreshAuth, store]);\n\n return children;\n}\n"],"names":["ApiError","apiRequest","configureApiClient","useQueryClient","atom","useAtomValue","useSetAtom","useStore","useCallback","useEffect","useMemo","REFRESH_EARLY_MS","REFRESH_RETRY_DELAY_MS","BASE64_URL_REPLACEMENTS","dash","underscore","initialAuthState","status","authStateAtom","authTransitionEpochAtom","authRefreshQueryKey","userWorkspacesQueryKey","toAuthenticatedState","result","memberships","token","user","workspaces","map","membership","id","workspace_id","name","workspace_name","membershipId","useAuthState","state","isLoading","isAuthenticated","hasWorkspace","length","listUserWorkspaces","headers","authorization","refreshAuthRequest","method","decodeBase64Url","value","base64","replace","atob","padEnd","Math","ceil","readJwtExp","payload","split","undefined","parsed","JSON","parse","exp","Number","isFinite","getAuthRefreshDelayMs","nowMs","Date","now","useAuthTransition","queryClient","store","setState","clearPrivateState","cancelQueries","clear","enterGuest","transitionEpoch","get","set","enterAuthenticated","previousState","principalChanged","setQueryData","fetchQuery","queryKey","queryFn","retry","staleTime","useRefreshAuth","error","AuthRuntime","children","effects","authState","refreshAuth","getAccessToken","refreshAccessToken","catch","timeout","disposed","refreshing","clearRefreshTimer","clearTimeout","scheduleRefresh","delayMs","setTimeout","runRefresh","max","retryIfStillDue","current","delay","finally","refreshIfDue","refreshIfVisible","document","visibilityState","window","addEventListener","removeEventListener"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- export declare const lastWorkspaceIdAtom: import("jotai").WritableAtom<string | undefined, [string | typeof import("jotai/utils").RESET | ((prev: string | undefined) => string | typeof import("jotai/utils").RESET | undefined) | undefined], void>;
1
+ export declare const lastWorkspaceIdAtom: import("jotai").WritableAtom<string | undefined, [workspaceId: string | undefined], void>;
2
2
  export declare function getLastWorkspaceId(): string | undefined;
3
3
  export declare function rememberLastWorkspaceId(workspaceId: string): void;
4
4
  //# sourceMappingURL=last-workspace.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"last-workspace.d.ts","sourceRoot":"","sources":["../../src/runtime/last-workspace.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,mBAAmB,6MAA6D,CAAC;AAE9F,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAUvD;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAGjE"}
1
+ {"version":3,"file":"last-workspace.d.ts","sourceRoot":"","sources":["../../src/runtime/last-workspace.ts"],"names":[],"mappings":"AAgCA,eAAO,MAAM,mBAAmB,2FAO/B,CAAC;AAEF,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAEjE"}
@@ -1,20 +1,39 @@
1
- import { atomWithStorage } from 'jotai/utils';
2
- const storageKey = 'shipfox.lastWorkspaceId';
3
- export const lastWorkspaceIdAtom = atomWithStorage(storageKey, undefined);
4
- export function getLastWorkspaceId() {
5
- if (typeof window === 'undefined') return undefined;
6
- try {
7
- const raw = window.localStorage.getItem(storageKey);
8
- if (!raw) return undefined;
9
- const parsed = JSON.parse(raw);
10
- return typeof parsed === 'string' ? parsed : undefined;
11
- } catch {
12
- return undefined;
1
+ import { createTypedBrowserStorage, localStorageOrUndefined } from '@shipfox/client-ui';
2
+ import { atom } from 'jotai';
3
+ const lastWorkspaceStorageKey = 'shipfox.lastWorkspaceId';
4
+ const lastWorkspaceStorage = createTypedBrowserStorage(localStorageOrUndefined, {
5
+ key: lastWorkspaceStorageKey,
6
+ lifetime: 'persistent',
7
+ principalScope: 'principal',
8
+ serialize: (workspaceId)=>JSON.stringify(workspaceId),
9
+ parse: (raw)=>{
10
+ try {
11
+ const parsed = JSON.parse(raw);
12
+ return typeof parsed === 'string' ? parsed : undefined;
13
+ } catch {
14
+ return undefined;
15
+ }
13
16
  }
17
+ });
18
+ const storedLastWorkspaceIdAtom = atom(getLastWorkspaceId());
19
+ storedLastWorkspaceIdAtom.onMount = (setLastWorkspaceId)=>{
20
+ if (typeof window === 'undefined') return undefined;
21
+ const syncFromStorage = (event)=>{
22
+ if (event.key === lastWorkspaceStorageKey) setLastWorkspaceId(getLastWorkspaceId());
23
+ };
24
+ window.addEventListener('storage', syncFromStorage);
25
+ return ()=>window.removeEventListener('storage', syncFromStorage);
26
+ };
27
+ export const lastWorkspaceIdAtom = atom((get)=>get(storedLastWorkspaceIdAtom), (_get, set, workspaceId)=>{
28
+ set(storedLastWorkspaceIdAtom, workspaceId);
29
+ if (workspaceId === undefined) lastWorkspaceStorage.remove();
30
+ else lastWorkspaceStorage.write(workspaceId);
31
+ });
32
+ export function getLastWorkspaceId() {
33
+ return lastWorkspaceStorage.read();
14
34
  }
15
35
  export function rememberLastWorkspaceId(workspaceId) {
16
- if (typeof window === 'undefined') return;
17
- window.localStorage.setItem(storageKey, JSON.stringify(workspaceId));
36
+ lastWorkspaceStorage.write(workspaceId);
18
37
  }
19
38
 
20
39
  //# sourceMappingURL=last-workspace.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runtime/last-workspace.ts"],"sourcesContent":["import {atomWithStorage} from 'jotai/utils';\n\nconst storageKey = 'shipfox.lastWorkspaceId';\n\nexport const lastWorkspaceIdAtom = atomWithStorage<string | undefined>(storageKey, undefined);\n\nexport function getLastWorkspaceId(): string | undefined {\n if (typeof window === 'undefined') return undefined;\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) return undefined;\n const parsed: unknown = JSON.parse(raw);\n return typeof parsed === 'string' ? parsed : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function rememberLastWorkspaceId(workspaceId: string): void {\n if (typeof window === 'undefined') return;\n window.localStorage.setItem(storageKey, JSON.stringify(workspaceId));\n}\n"],"names":["atomWithStorage","storageKey","lastWorkspaceIdAtom","undefined","getLastWorkspaceId","window","raw","localStorage","getItem","parsed","JSON","parse","rememberLastWorkspaceId","workspaceId","setItem","stringify"],"mappings":"AAAA,SAAQA,eAAe,QAAO,cAAc;AAE5C,MAAMC,aAAa;AAEnB,OAAO,MAAMC,sBAAsBF,gBAAoCC,YAAYE,WAAW;AAE9F,OAAO,SAASC;IACd,IAAI,OAAOC,WAAW,aAAa,OAAOF;IAC1C,IAAI;QACF,MAAMG,MAAMD,OAAOE,YAAY,CAACC,OAAO,CAACP;QACxC,IAAI,CAACK,KAAK,OAAOH;QACjB,MAAMM,SAAkBC,KAAKC,KAAK,CAACL;QACnC,OAAO,OAAOG,WAAW,WAAWA,SAASN;IAC/C,EAAE,OAAM;QACN,OAAOA;IACT;AACF;AAEA,OAAO,SAASS,wBAAwBC,WAAmB;IACzD,IAAI,OAAOR,WAAW,aAAa;IACnCA,OAAOE,YAAY,CAACO,OAAO,CAACb,YAAYS,KAAKK,SAAS,CAACF;AACzD"}
1
+ {"version":3,"sources":["../../src/runtime/last-workspace.ts"],"sourcesContent":["import {createTypedBrowserStorage, localStorageOrUndefined} from '@shipfox/client-ui';\nimport {atom} from 'jotai';\n\nconst lastWorkspaceStorageKey = 'shipfox.lastWorkspaceId';\n\nconst lastWorkspaceStorage = createTypedBrowserStorage(localStorageOrUndefined, {\n key: lastWorkspaceStorageKey,\n lifetime: 'persistent',\n principalScope: 'principal',\n serialize: (workspaceId: string) => JSON.stringify(workspaceId),\n parse: (raw) => {\n try {\n const parsed: unknown = JSON.parse(raw);\n return typeof parsed === 'string' ? parsed : undefined;\n } catch {\n return undefined;\n }\n },\n});\n\nconst storedLastWorkspaceIdAtom = atom<string | undefined>(getLastWorkspaceId());\n\nstoredLastWorkspaceIdAtom.onMount = (setLastWorkspaceId) => {\n if (typeof window === 'undefined') return undefined;\n\n const syncFromStorage = (event: StorageEvent) => {\n if (event.key === lastWorkspaceStorageKey) setLastWorkspaceId(getLastWorkspaceId());\n };\n window.addEventListener('storage', syncFromStorage);\n return () => window.removeEventListener('storage', syncFromStorage);\n};\n\nexport const lastWorkspaceIdAtom = atom(\n (get) => get(storedLastWorkspaceIdAtom),\n (_get, set, workspaceId: string | undefined) => {\n set(storedLastWorkspaceIdAtom, workspaceId);\n if (workspaceId === undefined) lastWorkspaceStorage.remove();\n else lastWorkspaceStorage.write(workspaceId);\n },\n);\n\nexport function getLastWorkspaceId(): string | undefined {\n return lastWorkspaceStorage.read();\n}\n\nexport function rememberLastWorkspaceId(workspaceId: string): void {\n lastWorkspaceStorage.write(workspaceId);\n}\n"],"names":["createTypedBrowserStorage","localStorageOrUndefined","atom","lastWorkspaceStorageKey","lastWorkspaceStorage","key","lifetime","principalScope","serialize","workspaceId","JSON","stringify","parse","raw","parsed","undefined","storedLastWorkspaceIdAtom","getLastWorkspaceId","onMount","setLastWorkspaceId","window","syncFromStorage","event","addEventListener","removeEventListener","lastWorkspaceIdAtom","get","_get","set","remove","write","read","rememberLastWorkspaceId"],"mappings":"AAAA,SAAQA,yBAAyB,EAAEC,uBAAuB,QAAO,qBAAqB;AACtF,SAAQC,IAAI,QAAO,QAAQ;AAE3B,MAAMC,0BAA0B;AAEhC,MAAMC,uBAAuBJ,0BAA0BC,yBAAyB;IAC9EI,KAAKF;IACLG,UAAU;IACVC,gBAAgB;IAChBC,WAAW,CAACC,cAAwBC,KAAKC,SAAS,CAACF;IACnDG,OAAO,CAACC;QACN,IAAI;YACF,MAAMC,SAAkBJ,KAAKE,KAAK,CAACC;YACnC,OAAO,OAAOC,WAAW,WAAWA,SAASC;QAC/C,EAAE,OAAM;YACN,OAAOA;QACT;IACF;AACF;AAEA,MAAMC,4BAA4Bd,KAAyBe;AAE3DD,0BAA0BE,OAAO,GAAG,CAACC;IACnC,IAAI,OAAOC,WAAW,aAAa,OAAOL;IAE1C,MAAMM,kBAAkB,CAACC;QACvB,IAAIA,MAAMjB,GAAG,KAAKF,yBAAyBgB,mBAAmBF;IAChE;IACAG,OAAOG,gBAAgB,CAAC,WAAWF;IACnC,OAAO,IAAMD,OAAOI,mBAAmB,CAAC,WAAWH;AACrD;AAEA,OAAO,MAAMI,sBAAsBvB,KACjC,CAACwB,MAAQA,IAAIV,4BACb,CAACW,MAAMC,KAAKnB;IACVmB,IAAIZ,2BAA2BP;IAC/B,IAAIA,gBAAgBM,WAAWX,qBAAqByB,MAAM;SACrDzB,qBAAqB0B,KAAK,CAACrB;AAClC,GACA;AAEF,OAAO,SAASQ;IACd,OAAOb,qBAAqB2B,IAAI;AAClC;AAEA,OAAO,SAASC,wBAAwBvB,WAAmB;IACzDL,qBAAqB0B,KAAK,CAACrB;AAC7B"}