@timber-js/app 0.1.10 → 0.1.12

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/_chunks/request-context-BzES06i1.js.map +1 -1
  2. package/dist/_chunks/ssr-data-BgSwMbN9.js +38 -0
  3. package/dist/_chunks/ssr-data-BgSwMbN9.js.map +1 -0
  4. package/dist/_chunks/{use-cookie-HcvNlW4L.js → use-cookie-D2cZu0jK.js} +3 -37
  5. package/dist/_chunks/use-cookie-D2cZu0jK.js.map +1 -0
  6. package/dist/_chunks/use-query-states-wEXY2JQB.js +109 -0
  7. package/dist/_chunks/use-query-states-wEXY2JQB.js.map +1 -0
  8. package/dist/client/error-boundary.d.ts.map +1 -1
  9. package/dist/client/error-boundary.js +8 -0
  10. package/dist/client/error-boundary.js.map +1 -1
  11. package/dist/client/index.js +3 -84
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/client/ssr-data.d.ts +9 -0
  14. package/dist/client/ssr-data.d.ts.map +1 -1
  15. package/dist/client/use-query-states.d.ts.map +1 -1
  16. package/dist/cookies/index.js +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +10 -12
  19. package/dist/index.js.map +1 -1
  20. package/dist/plugins/entries.d.ts.map +1 -1
  21. package/dist/plugins/routing.d.ts.map +1 -1
  22. package/dist/plugins/server-bundle.d.ts.map +1 -1
  23. package/dist/plugins/shims.d.ts.map +1 -1
  24. package/dist/routing/status-file-lint.d.ts.map +1 -1
  25. package/dist/search-params/create.d.ts.map +1 -1
  26. package/dist/search-params/index.js +13 -4
  27. package/dist/search-params/index.js.map +1 -1
  28. package/dist/server/fallback-error.d.ts +28 -0
  29. package/dist/server/fallback-error.d.ts.map +1 -0
  30. package/dist/server/html-injectors.d.ts.map +1 -1
  31. package/dist/server/index.js +13 -10
  32. package/dist/server/index.js.map +1 -1
  33. package/dist/server/pipeline.d.ts +12 -0
  34. package/dist/server/pipeline.d.ts.map +1 -1
  35. package/dist/server/request-context.d.ts.map +1 -1
  36. package/dist/server/route-matcher.d.ts.map +1 -1
  37. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  38. package/dist/server/slot-resolver.d.ts +1 -1
  39. package/dist/server/slot-resolver.d.ts.map +1 -1
  40. package/dist/server/ssr-entry.d.ts +7 -0
  41. package/dist/server/ssr-entry.d.ts.map +1 -1
  42. package/dist/server/tree-builder.d.ts +10 -0
  43. package/dist/server/tree-builder.d.ts.map +1 -1
  44. package/package.json +23 -23
  45. package/src/client/browser-entry.ts +1 -1
  46. package/src/client/error-boundary.tsx +22 -0
  47. package/src/client/ssr-data.ts +7 -0
  48. package/src/client/use-query-states.ts +13 -1
  49. package/src/index.ts +16 -16
  50. package/src/plugins/dev-server.ts +3 -1
  51. package/src/plugins/entries.ts +2 -1
  52. package/src/plugins/routing.ts +5 -4
  53. package/src/plugins/server-bundle.ts +15 -6
  54. package/src/plugins/shims.ts +8 -14
  55. package/src/routing/status-file-lint.ts +1 -3
  56. package/src/search-params/create.ts +15 -8
  57. package/src/server/error-formatter.ts +12 -0
  58. package/src/server/fallback-error.ts +159 -0
  59. package/src/server/html-injectors.ts +9 -4
  60. package/src/server/pipeline.ts +24 -0
  61. package/src/server/request-context.ts +0 -1
  62. package/src/server/route-matcher.ts +1 -4
  63. package/src/server/rsc-entry/index.ts +98 -39
  64. package/src/server/slot-resolver.ts +38 -5
  65. package/src/server/ssr-entry.ts +12 -1
  66. package/src/server/tree-builder.ts +39 -11
  67. package/src/shims/server-only-noop.js +1 -0
  68. package/dist/_chunks/registry-BfPM41ri.js +0 -20
  69. package/dist/_chunks/registry-BfPM41ri.js.map +0 -1
  70. package/dist/_chunks/use-cookie-HcvNlW4L.js.map +0 -1
@@ -11,7 +11,6 @@
11
11
  */
12
12
 
13
13
  import type { SegmentNode, RouteFile } from '#/routing/types.js';
14
- import { TimberErrorBoundary } from '#/client/error-boundary.js';
15
14
 
16
15
  // ─── Types ───────────────────────────────────────────────────────────────────
17
16
 
@@ -55,6 +54,16 @@ export interface TreeBuilderConfig {
55
54
  loadModule: ModuleLoader;
56
55
  /** React.createElement or equivalent. */
57
56
  createElement: CreateElement;
57
+ /**
58
+ * Error boundary component for wrapping segments.
59
+ *
60
+ * This is injected by the caller rather than imported directly to avoid
61
+ * pulling 'use client' code into the server barrel (@timber-js/app/server).
62
+ * In the RSC environment, the RSC plugin transforms this import to a
63
+ * client reference proxy — the caller handles the import so the server
64
+ * barrel stays free of client dependencies.
65
+ */
66
+ errorBoundaryComponent?: unknown;
58
67
  }
59
68
 
60
69
  // ─── Component wrappers ──────────────────────────────────────────────────────
@@ -79,7 +88,10 @@ export interface AccessGateProps {
79
88
  * - 'pass': render children
80
89
  * - DenySignal/RedirectSignal: throw synchronously
81
90
  */
82
- verdict?: 'pass' | import('./primitives.js').DenySignal | import('./primitives.js').RedirectSignal;
91
+ verdict?:
92
+ | 'pass'
93
+ | import('./primitives.js').DenySignal
94
+ | import('./primitives.js').RedirectSignal;
83
95
  children: ReactElement;
84
96
  }
85
97
 
@@ -131,7 +143,8 @@ export interface TreeBuildResult {
131
143
  * Parallel slots are resolved at each layout level and composed as named props.
132
144
  */
133
145
  export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeBuildResult> {
134
- const { segments, params, searchParams, loadModule, createElement } = config;
146
+ const { segments, params, searchParams, loadModule, createElement, errorBoundaryComponent } =
147
+ config;
135
148
 
136
149
  if (segments.length === 0) {
137
150
  throw new Error('[timber] buildElementTree: empty segment chain');
@@ -163,7 +176,13 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
163
176
  const segment = segments[i];
164
177
 
165
178
  // Wrap in error boundaries (status-code files + error.tsx)
166
- element = await wrapWithErrorBoundaries(segment, element, loadModule, createElement);
179
+ element = await wrapWithErrorBoundaries(
180
+ segment,
181
+ element,
182
+ loadModule,
183
+ createElement,
184
+ errorBoundaryComponent
185
+ );
167
186
 
168
187
  // Wrap in AccessGate if segment has access.ts
169
188
  if (segment.access) {
@@ -195,7 +214,8 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
195
214
  params,
196
215
  searchParams,
197
216
  loadModule,
198
- createElement
217
+ createElement,
218
+ errorBoundaryComponent
199
219
  );
200
220
  }
201
221
  }
@@ -226,7 +246,8 @@ async function buildSlotElement(
226
246
  params: Record<string, string | string[]>,
227
247
  searchParams: unknown,
228
248
  loadModule: ModuleLoader,
229
- createElement: CreateElement
249
+ createElement: CreateElement,
250
+ errorBoundaryComponent: unknown
230
251
  ): Promise<ReactElement> {
231
252
  // Load slot page
232
253
  const pageModule = slotNode.page ? await loadModule(slotNode.page) : null;
@@ -246,7 +267,13 @@ async function buildSlotElement(
246
267
  let element: ReactElement = createElement(PageComponent, { params, searchParams });
247
268
 
248
269
  // Wrap in error boundaries
249
- element = await wrapWithErrorBoundaries(slotNode, element, loadModule, createElement);
270
+ element = await wrapWithErrorBoundaries(
271
+ slotNode,
272
+ element,
273
+ loadModule,
274
+ createElement,
275
+ errorBoundaryComponent
276
+ );
250
277
 
251
278
  // Wrap in SlotAccessGate if slot has access.ts
252
279
  if (slotNode.access) {
@@ -298,7 +325,8 @@ async function wrapWithErrorBoundaries(
298
325
  segment: SegmentNode,
299
326
  element: ReactElement,
300
327
  loadModule: ModuleLoader,
301
- createElement: CreateElement
328
+ createElement: CreateElement,
329
+ errorBoundaryComponent: unknown
302
330
  ): Promise<ReactElement> {
303
331
  // Wrapping is applied inside-out. The last wrap call produces the outermost boundary.
304
332
  // Order: specific status → category → error.tsx (outermost)
@@ -312,7 +340,7 @@ async function wrapWithErrorBoundaries(
312
340
  const mod = await loadModule(file);
313
341
  const Component = mod.default;
314
342
  if (Component) {
315
- element = createElement(TimberErrorBoundary, {
343
+ element = createElement(errorBoundaryComponent, {
316
344
  fallbackComponent: Component,
317
345
  status,
318
346
  children: element,
@@ -328,7 +356,7 @@ async function wrapWithErrorBoundaries(
328
356
  const mod = await loadModule(file);
329
357
  const Component = mod.default;
330
358
  if (Component) {
331
- element = createElement(TimberErrorBoundary, {
359
+ element = createElement(errorBoundaryComponent, {
332
360
  fallbackComponent: Component,
333
361
  status: key === '4xx' ? 400 : 500, // category marker
334
362
  children: element,
@@ -343,7 +371,7 @@ async function wrapWithErrorBoundaries(
343
371
  const errorModule = await loadModule(segment.error);
344
372
  const ErrorComponent = errorModule.default;
345
373
  if (ErrorComponent) {
346
- element = createElement(TimberErrorBoundary, {
374
+ element = createElement(errorBoundaryComponent, {
347
375
  fallbackComponent: ErrorComponent,
348
376
  children: element,
349
377
  } satisfies ErrorBoundaryProps);
@@ -1,3 +1,4 @@
1
+ // oxlint-disable
1
2
  // No-op shim for server-only/client-only packages.
2
3
  // In dev mode, Vite externalizes node_modules and loads them via Node's
3
4
  // require(). Deps like `bright` that import `server-only` would hit the
@@ -1,20 +0,0 @@
1
- //#region src/search-params/registry.ts
2
- var registry = /* @__PURE__ */ new Map();
3
- /**
4
- * Register a route's search params definition.
5
- * Called by the generated route manifest loader when a route's modules load.
6
- */
7
- function registerSearchParams(route, definition) {
8
- registry.set(route, definition);
9
- }
10
- /**
11
- * Look up a route's search params definition.
12
- * Returns undefined if the route hasn't been loaded yet.
13
- */
14
- function getSearchParams(route) {
15
- return registry.get(route);
16
- }
17
- //#endregion
18
- export { registerSearchParams as n, getSearchParams as t };
19
-
20
- //# sourceMappingURL=registry-BfPM41ri.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"registry-BfPM41ri.js","names":[],"sources":["../../src/search-params/registry.ts"],"sourcesContent":["/**\n * Runtime registry for route-scoped search params definitions.\n *\n * When a route's modules load, the framework registers its search-params\n * definition here. useQueryStates('/route') resolves codecs from this map.\n *\n * Design doc: design/23-search-params.md §\"Runtime: Registration at Route Load\"\n */\n\nimport type { SearchParamsDefinition } from './create.js';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst registry = new Map<string, SearchParamsDefinition<any>>();\n\n/**\n * Register a route's search params definition.\n * Called by the generated route manifest loader when a route's modules load.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function registerSearchParams(route: string, definition: SearchParamsDefinition<any>): void {\n registry.set(route, definition);\n}\n\n/**\n * Look up a route's search params definition.\n * Returns undefined if the route hasn't been loaded yet.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function getSearchParams(route: string): SearchParamsDefinition<any> | undefined {\n return registry.get(route);\n}\n"],"mappings":";AAYA,IAAM,2BAAW,IAAI,KAA0C;;;;;AAO/D,SAAgB,qBAAqB,OAAe,YAA+C;AACjG,UAAS,IAAI,OAAO,WAAW;;;;;;AAQjC,SAAgB,gBAAgB,OAAwD;AACtF,QAAO,SAAS,IAAI,MAAM"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-cookie-HcvNlW4L.js","names":[],"sources":["../../src/client/ssr-data.ts","../../src/client/use-cookie.ts"],"sourcesContent":["/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n */\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: Record<string, string | string[]>;\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n\nlet _ssrDataProvider: (() => SsrData | undefined) | undefined;\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _ssrDataProvider = provider;\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\nlet currentSsrData: SsrData | undefined;\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n currentSsrData = data;\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n currentSsrData = undefined;\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (_ssrDataProvider) {\n return _ssrDataProvider();\n }\n return currentSsrData;\n}\n","/**\n * useCookie — reactive client-side cookie hook.\n *\n * Uses useSyncExternalStore for SSR-safe, reactive cookie access.\n * All components reading the same cookie name re-render on change.\n * No cross-tab sync (intentional — see design/29-cookies.md).\n *\n * See design/29-cookies.md §\"useCookie(name) Hook\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nexport interface ClientCookieOptions {\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Domain scope. Default: omitted (current domain). */\n domain?: string;\n /** Max age in seconds. */\n maxAge?: number;\n /** Expiration date. */\n expires?: Date;\n /** Cross-site policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Only send over HTTPS. Default: true in production. */\n secure?: boolean;\n}\n\nexport type CookieSetter = (value: string, options?: ClientCookieOptions) => void;\n\n// ─── Module-Level Cookie Store ────────────────────────────────────────────\n\ntype Listener = () => void;\n\n/** Per-name subscriber sets. */\nconst listeners = new Map<string, Set<Listener>>();\n\n/** Parse a cookie name from document.cookie. */\nfunction getCookieValue(name: string): string | undefined {\n if (typeof document === 'undefined') return undefined;\n const match = document.cookie.match(\n new RegExp('(?:^|;\\\\s*)' + name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\s*=\\\\s*([^;]*)')\n );\n return match ? decodeURIComponent(match[1]) : undefined;\n}\n\n/** Serialize options into a cookie string suffix. */\nfunction serializeOptions(options?: ClientCookieOptions): string {\n if (!options) return '; Path=/; SameSite=Lax';\n const parts: string[] = [];\n parts.push(`Path=${options.path ?? '/'}`);\n if (options.domain) parts.push(`Domain=${options.domain}`);\n if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);\n if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);\n const sameSite = options.sameSite ?? 'lax';\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n if (options.secure) parts.push('Secure');\n return '; ' + parts.join('; ');\n}\n\n/** Notify all subscribers for a given cookie name. */\nfunction notify(name: string): void {\n const subs = listeners.get(name);\n if (subs) {\n for (const fn of subs) fn();\n }\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────────────\n\n/**\n * Reactive hook for reading/writing a client-side cookie.\n *\n * Returns `[value, setCookie, deleteCookie]`:\n * - `value`: current cookie value (string | undefined)\n * - `setCookie`: sets the cookie and triggers re-renders\n * - `deleteCookie`: deletes the cookie and triggers re-renders\n *\n * @param name - Cookie name.\n * @param defaultOptions - Default options for setCookie calls.\n */\nexport function useCookie(\n name: string,\n defaultOptions?: ClientCookieOptions\n): [value: string | undefined, setCookie: CookieSetter, deleteCookie: () => void] {\n const subscribe = (callback: Listener): (() => void) => {\n let subs = listeners.get(name);\n if (!subs) {\n subs = new Set();\n listeners.set(name, subs);\n }\n subs.add(callback);\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) listeners.delete(name);\n };\n };\n\n const getSnapshot = (): string | undefined => getCookieValue(name);\n const getServerSnapshot = (): string | undefined => getSsrData()?.cookies.get(name);\n\n const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n\n const setCookie: CookieSetter = (newValue: string, options?: ClientCookieOptions) => {\n const merged = { ...defaultOptions, ...options };\n document.cookie = `${name}=${encodeURIComponent(newValue)}${serializeOptions(merged)}`;\n notify(name);\n };\n\n const deleteCookie = (): void => {\n const path = defaultOptions?.path ?? '/';\n const domain = defaultOptions?.domain;\n let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;\n if (domain) cookieStr += `; Domain=${domain}`;\n document.cookie = cookieStr;\n notify(name);\n };\n\n return [value, setCookie, deleteCookie];\n}\n"],"mappings":";;AAwCA,IAAI;AAkBJ,IAAI;;;;;;;AAQJ,SAAgB,WAAW,MAAqB;AAC9C,kBAAiB;;;;;;;;AASnB,SAAgB,eAAqB;AACnC,kBAAiB,KAAA;;;;;;;;;;;AAYnB,SAAgB,aAAkC;AAChD,KAAI,iBACF,QAAO,kBAAkB;AAE3B,QAAO;;;;;;;;;;;;;;ACxDT,IAAM,4BAAY,IAAI,KAA4B;;AAGlD,SAAS,eAAe,MAAkC;AACxD,KAAI,OAAO,aAAa,YAAa,QAAO,KAAA;CAC5C,MAAM,QAAQ,SAAS,OAAO,MAC5B,IAAI,OAAO,gBAAgB,KAAK,QAAQ,uBAAuB,OAAO,GAAG,mBAAmB,CAC7F;AACD,QAAO,QAAQ,mBAAmB,MAAM,GAAG,GAAG,KAAA;;;AAIhD,SAAS,iBAAiB,SAAuC;AAC/D,KAAI,CAAC,QAAS,QAAO;CACrB,MAAM,QAAkB,EAAE;AAC1B,OAAM,KAAK,QAAQ,QAAQ,QAAQ,MAAM;AACzC,KAAI,QAAQ,OAAQ,OAAM,KAAK,UAAU,QAAQ,SAAS;AAC1D,KAAI,QAAQ,WAAW,KAAA,EAAW,OAAM,KAAK,WAAW,QAAQ,SAAS;AACzE,KAAI,QAAQ,QAAS,OAAM,KAAK,WAAW,QAAQ,QAAQ,aAAa,GAAG;CAC3E,MAAM,WAAW,QAAQ,YAAY;AACrC,OAAM,KAAK,YAAY,SAAS,OAAO,EAAE,CAAC,aAAa,GAAG,SAAS,MAAM,EAAE,GAAG;AAC9E,KAAI,QAAQ,OAAQ,OAAM,KAAK,SAAS;AACxC,QAAO,OAAO,MAAM,KAAK,KAAK;;;AAIhC,SAAS,OAAO,MAAoB;CAClC,MAAM,OAAO,UAAU,IAAI,KAAK;AAChC,KAAI,KACF,MAAK,MAAM,MAAM,KAAM,KAAI;;;;;;;;;;;;;AAiB/B,SAAgB,UACd,MACA,gBACgF;CAChF,MAAM,aAAa,aAAqC;EACtD,IAAI,OAAO,UAAU,IAAI,KAAK;AAC9B,MAAI,CAAC,MAAM;AACT,0BAAO,IAAI,KAAK;AAChB,aAAU,IAAI,MAAM,KAAK;;AAE3B,OAAK,IAAI,SAAS;AAClB,eAAa;AACX,QAAM,OAAO,SAAS;AACtB,OAAI,KAAM,SAAS,EAAG,WAAU,OAAO,KAAK;;;CAIhD,MAAM,oBAAwC,eAAe,KAAK;CAClE,MAAM,0BAA8C,YAAY,EAAE,QAAQ,IAAI,KAAK;CAEnF,MAAM,QAAQ,qBAAqB,WAAW,aAAa,kBAAkB;CAE7E,MAAM,aAA2B,UAAkB,YAAkC;EACnF,MAAM,SAAS;GAAE,GAAG;GAAgB,GAAG;GAAS;AAChD,WAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,SAAS,GAAG,iBAAiB,OAAO;AACpF,SAAO,KAAK;;CAGd,MAAM,qBAA2B;EAC/B,MAAM,OAAO,gBAAgB,QAAQ;EACrC,MAAM,SAAS,gBAAgB;EAC/B,IAAI,YAAY,GAAG,KAAK,4DAA4D;AACpF,MAAI,OAAQ,cAAa,YAAY;AACrC,WAAS,SAAS;AAClB,SAAO,KAAK;;AAGd,QAAO;EAAC;EAAO;EAAW;EAAa"}