akanjs 2.3.11-rc.9 → 2.3.12-rc.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 (65) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/client/csrTypes.ts +2 -0
  3. package/common/routeConvention.ts +8 -5
  4. package/fetch/client/fetchClient.ts +29 -14
  5. package/fetch/serializer/fetch.serializer.ts +10 -0
  6. package/package.json +1 -1
  7. package/server/resolver/signal.resolver.ts +3 -3
  8. package/server/routeTreeBuilder.ts +37 -4
  9. package/signal/serializer/fetch.serializer.ts +10 -0
  10. package/signal/signalContext.ts +5 -0
  11. package/signal/slice.ts +17 -15
  12. package/signal/types.ts +3 -0
  13. package/types/client/csrTypes.d.ts +2 -0
  14. package/types/common/routeConvention.d.ts +1 -1
  15. package/types/signal/signalContext.d.ts +1 -0
  16. package/types/signal/slice.d.ts +6 -0
  17. package/types/signal/types.d.ts +3 -0
  18. package/types/ui/Button.d.ts +6 -1
  19. package/types/ui/DatePicker.d.ts +12 -9
  20. package/types/ui/Dropdown.d.ts +6 -1
  21. package/types/ui/Empty.d.ts +7 -1
  22. package/types/ui/Input.d.ts +12 -8
  23. package/types/ui/Loading/index.d.ts +11 -6
  24. package/types/ui/Menu.d.ts +9 -4
  25. package/types/ui/Modal.d.ts +11 -1
  26. package/types/ui/Pagination.d.ts +6 -1
  27. package/types/ui/Popconfirm.d.ts +7 -1
  28. package/types/ui/Radio.d.ts +7 -4
  29. package/types/ui/Select.d.ts +6 -1
  30. package/types/ui/Table.d.ts +6 -1
  31. package/types/ui/ToggleSelect.d.ts +11 -7
  32. package/types/ui/UiOverride/Provider.d.ts +14 -0
  33. package/types/ui/UiOverride/context.d.ts +72 -0
  34. package/types/ui/UiOverride/createOverridable.d.ts +9 -0
  35. package/types/ui/UiOverride/index.d.ts +5 -0
  36. package/types/ui/UiOverride/override.d.ts +18 -0
  37. package/types/ui/UiOverride/useUiOverride.d.ts +7 -0
  38. package/types/ui/UiOverride.d.ts +1 -0
  39. package/types/ui/Unauthorized.d.ts +8 -3
  40. package/types/ui/index.d.ts +1 -0
  41. package/types/webkit/usePushNotification.d.ts +10 -1
  42. package/ui/Button.tsx +15 -2
  43. package/ui/DatePicker.tsx +19 -9
  44. package/ui/Dropdown.tsx +9 -1
  45. package/ui/Empty.tsx +11 -1
  46. package/ui/Input.tsx +22 -14
  47. package/ui/Loading/index.tsx +15 -1
  48. package/ui/Menu.tsx +12 -3
  49. package/ui/Modal.tsx +13 -1
  50. package/ui/Pagination.tsx +9 -1
  51. package/ui/Popconfirm.tsx +9 -1
  52. package/ui/Radio.tsx +14 -3
  53. package/ui/Select.tsx +22 -2
  54. package/ui/Table.tsx +8 -1
  55. package/ui/ToggleSelect.tsx +31 -5
  56. package/ui/UiOverride/Provider.tsx +22 -0
  57. package/ui/UiOverride/context.ts +85 -0
  58. package/ui/UiOverride/createOverridable.tsx +25 -0
  59. package/ui/UiOverride/index.ts +5 -0
  60. package/ui/UiOverride/override.ts +19 -0
  61. package/ui/UiOverride/useUiOverride.ts +15 -0
  62. package/ui/Unauthorized.tsx +12 -3
  63. package/ui/index.ts +10 -0
  64. package/webkit/bootCsr.tsx +28 -5
  65. package/webkit/usePushNotification.tsx +21 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # akanjs
2
2
 
3
+ ## 2.3.11
4
+
5
+ ### Minor Changes
6
+
7
+ - 595390a: feat: UiOverride 시스템 및 \_overrides.tsx 지원 추가
8
+
9
+ - `akanjs/ui/UiOverride` 추가: `Provider`, `createOverridable`, `useUiOverride`, `override` API로 UI 컴포넌트 커스터마이징 지원
10
+ - 모든 akanjs UI 컴포넌트(Button, Modal, Select, Table 등)에 `useUiOverride()` 통합
11
+ - 라우트 시스템에 `_overrides.tsx` 지원 추가 (routeConvention, routeTreeBuilder)
12
+ - qualityScanner에 `_overrides.tsx` 파일 검증 로직 추가
13
+ - 앱 예제: `apps/minimal`에 `_overrides.tsx`, `BrandModal`, `OverrideDemo` 추가
14
+ - `apps/akan` 문서에 UI 커스터마이징 가이드 페이지 추가
15
+ - devkit에 `no-throw-raw-error.grit` lint rule 추가
16
+ - `PushNotificationServer.ts` 리팩토링
17
+ - biome.json 업데이트 및 패키지 의존성 정리
18
+
19
+ ### Patch Changes
20
+
21
+ - 5ce752a: enhance: add host option for staging server tests
22
+ - 5ce752a: add host option for staging server tests
23
+
3
24
  ## 2.3.10
4
25
 
5
26
  ### Patch Changes
@@ -191,6 +191,8 @@ export interface Route {
191
191
  path: string;
192
192
  renderPage?: RouteRender;
193
193
  renderLayout?: RouteRender;
194
+ /** Synthetic layout render from a `_overrides.tsx` at this node; wraps the subtree in a UI-override provider. */
195
+ renderOverrides?: RouteRender;
194
196
  pageIncludesOwnLayout?: boolean;
195
197
  isSpecialRoute?: boolean;
196
198
 
@@ -1,11 +1,13 @@
1
1
  const ROUTE_SOURCE_RE = /^\.\/(.+)\.(tsx|ts|jsx|js)$/;
2
2
  const SOURCE_EXT_RE = /\.(tsx|ts|jsx|js)$/;
3
- const RESERVED_ROUTE_FILES = new Set(["_layout", "_index"]);
3
+ const RESERVED_ROUTE_FILES = new Set(["_layout", "_index", "_overrides"]);
4
4
  const INTERNAL_ROOT_LAYOUT_LEAF = "__root_layout";
5
5
  const IMPLICIT_LOCALE_SEGMENT = "[lang]";
6
6
  const SPECIAL_ROUTE_LEAVES = new Set(["robots.txt"]);
7
7
 
8
- export type RouteModuleKind = "page" | "layout";
8
+ const DIRECTORY_SCOPED_LEAVES = new Set(["_layout", "_index", "_overrides"]);
9
+
10
+ export type RouteModuleKind = "page" | "layout" | "overrides";
9
11
 
10
12
  export interface ParsedRouteModuleKey {
11
13
  key: string;
@@ -51,7 +53,7 @@ export function validatePageSourceFile(filePath: string, options: ValidatePageSo
51
53
  if (ext !== "tsx") throw new Error(`[route-convention] route source files under page/ must use .tsx: ${displayPath}`);
52
54
  if (leaf.startsWith("_") && !RESERVED_ROUTE_FILES.has(leaf) && leaf !== INTERNAL_ROOT_LAYOUT_LEAF)
53
55
  throw new Error(
54
- `[route-convention] only _index.tsx and _layout.tsx are allowed as reserved route files under page/: ${displayPath}`,
56
+ `[route-convention] only _index.tsx, _layout.tsx and _overrides.tsx are allowed as reserved route files under page/: ${displayPath}`,
55
57
  );
56
58
  if (/^[A-Z]/.test(leaf))
57
59
  throw new Error(`[route-convention] route page filenames must not start with an uppercase letter: ${displayPath}`);
@@ -97,13 +99,14 @@ export function parseRouteModuleKey(key: string): ParsedRouteModuleKey {
97
99
  }
98
100
 
99
101
  const isInternalRootLayout = leaf === INTERNAL_ROOT_LAYOUT_LEAF;
100
- const kind: RouteModuleKind = leaf === "_layout" || isInternalRootLayout ? "layout" : "page";
102
+ const kind: RouteModuleKind =
103
+ leaf === "_layout" || isInternalRootLayout ? "layout" : leaf === "_overrides" ? "overrides" : "page";
101
104
  if (leaf.startsWith("_") && !RESERVED_ROUTE_FILES.has(leaf) && !isInternalRootLayout) {
102
105
  throw new Error(`[route-convention] unsupported reserved route file "${leaf}" in ${key}`);
103
106
  }
104
107
 
105
108
  const sourceRouteSegments =
106
- leaf === "_layout" || leaf === "_index" || isInternalRootLayout ? moduleSegments.slice(0, -1) : moduleSegments;
109
+ DIRECTORY_SCOPED_LEAVES.has(leaf) || isInternalRootLayout ? moduleSegments.slice(0, -1) : moduleSegments;
107
110
  const isSpecialRoute = kind === "page" && SPECIAL_ROUTE_LEAVES.has(leaf);
108
111
  const routeSegments = isSpecialRoute ? sourceRouteSegments : [IMPLICIT_LOCALE_SEGMENT, ...sourceRouteSegments];
109
112
  for (const segment of routeSegments) validateRouteSegment(segment, key);
@@ -101,6 +101,9 @@ export class FetchClient {
101
101
  : undefined,
102
102
  getGuards: signal.getGuards ?? current.getGuards,
103
103
  cruGuards: signal.cruGuards ?? current.cruGuards,
104
+ createGuards: signal.createGuards ?? current.createGuards,
105
+ updateGuards: signal.updateGuards ?? current.updateGuards,
106
+ removeGuards: signal.removeGuards ?? current.removeGuards,
104
107
  }
105
108
  : signal;
106
109
  }
@@ -353,6 +356,10 @@ export class FetchClient {
353
356
  modelId: `${refName}Id`,
354
357
  lightModel: `light${capRefName}`,
355
358
  };
359
+
360
+ const createGuards = signal.createGuards ?? signal.cruGuards;
361
+ const updateGuards = signal.updateGuards ?? signal.cruGuards;
362
+ const removeGuards = signal.removeGuards ?? signal.cruGuards;
356
363
  const endpoint: { [key: string]: SerializedEndpoint } = {};
357
364
  if (signal.getGuards) {
358
365
  endpoint[names.model] = {
@@ -368,13 +375,15 @@ export class FetchClient {
368
375
  guards: signal.getGuards,
369
376
  };
370
377
  }
371
- if (signal.cruGuards) {
378
+ if (createGuards) {
372
379
  endpoint[names.createModel] = {
373
380
  type: "mutation",
374
381
  args: [{ type: "body", name: "data", refName, modelType: "input" }],
375
382
  returns: { refName, modelType: "full" },
376
- guards: signal.cruGuards,
383
+ guards: createGuards,
377
384
  };
385
+ }
386
+ if (updateGuards) {
378
387
  endpoint[names.updateModel] = {
379
388
  type: "mutation",
380
389
  args: [
@@ -382,13 +391,15 @@ export class FetchClient {
382
391
  { type: "body", name: "data", refName, modelType: "input" },
383
392
  ],
384
393
  returns: { refName, modelType: "full" },
385
- guards: signal.cruGuards,
394
+ guards: updateGuards,
386
395
  };
396
+ }
397
+ if (removeGuards) {
387
398
  endpoint[names.removeModel] = {
388
399
  type: "mutation",
389
400
  args: [{ type: "param", name: names.modelId, refName: "ID" }],
390
401
  returns: { refName, modelType: "full" },
391
- guards: signal.cruGuards,
402
+ guards: removeGuards,
392
403
  };
393
404
  }
394
405
  return endpoint;
@@ -414,7 +425,9 @@ export class FetchClient {
414
425
  this.#setHandlerFactory(key, () => this.#makeHttpFn(key, value, signal.prefix));
415
426
  });
416
427
 
417
- if (signal.cruGuards) {
428
+ const anyCruGuards = signal.cruGuards ?? signal.createGuards ?? signal.updateGuards ?? signal.removeGuards;
429
+ const updateGuards = signal.updateGuards ?? signal.cruGuards;
430
+ if (anyCruGuards) {
418
431
  this.#setHandlerFactory(
419
432
  names.viewModel,
420
433
  () =>
@@ -461,15 +474,17 @@ export class FetchClient {
461
474
  return { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() };
462
475
  }) as FetchHandler,
463
476
  );
464
- this.#setHandlerFactory(
465
- names.mergeModel,
466
- () =>
467
- (async (modelOrId: string | { id: string }, data: UnknownRecord, option?: FetchPolicy) => {
468
- const id = typeof modelOrId === "string" ? modelOrId : modelOrId.id;
469
- const updateFn = this.#requireHandler(names.updateModel, names.mergeModel);
470
- return await updateFn(id, data, option);
471
- }) as FetchHandler,
472
- );
477
+ if (updateGuards) {
478
+ this.#setHandlerFactory(
479
+ names.mergeModel,
480
+ () =>
481
+ (async (modelOrId: string | { id: string }, data: UnknownRecord, option?: FetchPolicy) => {
482
+ const id = typeof modelOrId === "string" ? modelOrId : modelOrId.id;
483
+ const updateFn = this.#requireHandler(names.updateModel, names.mergeModel);
484
+ return await updateFn(id, data, option);
485
+ }) as FetchHandler,
486
+ );
487
+ }
473
488
  }
474
489
 
475
490
  this.#setHandlerFactory(
@@ -102,6 +102,16 @@ export class FetchSerializer {
102
102
  ...(sliceCls.cruGuards.filter((g) => g.name !== "None").length
103
103
  ? { cruGuards: sliceCls.cruGuards.map((g) => g.name) }
104
104
  : {}),
105
+
106
+ ...(sliceCls.createGuards !== sliceCls.cruGuards && sliceCls.createGuards.filter((g) => g.name !== "None").length
107
+ ? { createGuards: sliceCls.createGuards.map((g) => g.name) }
108
+ : {}),
109
+ ...(sliceCls.updateGuards !== sliceCls.cruGuards && sliceCls.updateGuards.filter((g) => g.name !== "None").length
110
+ ? { updateGuards: sliceCls.updateGuards.map((g) => g.name) }
111
+ : {}),
112
+ ...(sliceCls.removeGuards !== sliceCls.cruGuards && sliceCls.removeGuards.filter((g) => g.name !== "None").length
113
+ ? { removeGuards: sliceCls.removeGuards.map((g) => g.name) }
114
+ : {}),
105
115
  endpoint,
106
116
  };
107
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.3.11-rc.9",
3
+ "version": "2.3.12-rc.0",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -232,14 +232,14 @@ export class SignalResolver {
232
232
  });
233
233
 
234
234
  endpointObj[`create${capitalizedRefName}`] = (builder as any)
235
- .mutation(cnst.full, { guards: sliceCls.cruGuards })
235
+ .mutation(cnst.full, { guards: sliceCls.createGuards })
236
236
  .body("data", cnst.input)
237
237
  .exec(async function (this: any, data: any) {
238
238
  return await this[serviceName].__create(data);
239
239
  });
240
240
 
241
241
  endpointObj[`update${capitalizedRefName}`] = (builder as any)
242
- .mutation(cnst.full, { guards: sliceCls.cruGuards })
242
+ .mutation(cnst.full, { guards: sliceCls.updateGuards })
243
243
  .param(`${refName}Id`, ID)
244
244
  .body("data", cnst.input)
245
245
  .exec(async function (this: any, id: string, data: any) {
@@ -247,7 +247,7 @@ export class SignalResolver {
247
247
  });
248
248
 
249
249
  endpointObj[`remove${capitalizedRefName}`] = (builder as any)
250
- .mutation(cnst.full, { guards: sliceCls.cruGuards })
250
+ .mutation(cnst.full, { guards: sliceCls.removeGuards })
251
251
  .param(`${refName}Id`, ID)
252
252
  .exec(async function (this: any, id: string) {
253
253
  return await this[serviceName].__remove(id);
@@ -18,9 +18,12 @@ import {
18
18
  parseRouteModuleKey,
19
19
  routeSegmentToTreePath,
20
20
  } from "akanjs/common";
21
+ import { createElement } from "react";
21
22
  import { validatePageConfig } from "../client/frameConfig";
22
23
  import { resolveHeadExport, resolveMetadataHead } from "./metadata";
23
24
 
25
+ type RouteModuleKindWithOverrides = "page" | "layout" | "overrides";
26
+
24
27
  export type PagesContext = Record<string, () => Promise<RouteModule>>;
25
28
 
26
29
  export const defaultPageState: PageState = {
@@ -197,6 +200,14 @@ export class RouteTreeBuilder {
197
200
  const targetPath = pathSegments[pathSegments.length - 1];
198
201
  if (!targetPath) return;
199
202
 
203
+ if (parsed.kind === "overrides") {
204
+ targetRouteMap.set(targetPath, {
205
+ ...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
206
+ renderOverrides: this.#makeOverridesRender(filePath, loader),
207
+ } as Route);
208
+ return;
209
+ }
210
+
200
211
  const routeRender = RouteTreeBuilder.#makeRouteRender(filePath, parsed.kind, loader);
201
212
  targetRouteMap.set(targetPath, {
202
213
  ...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
@@ -224,8 +235,10 @@ export class RouteTreeBuilder {
224
235
  const pathSegments = [...parentPaths, ...(currentPathSegment ? [currentPathSegment] : [])];
225
236
  const currentRootLayout = isRoot && route.renderLayout ? route.renderLayout : null;
226
237
  const currentLayout = !isRoot && route.renderLayout ? route.renderLayout : null;
238
+
239
+ const currentOverrideRenders = route.renderOverrides ? [route.renderOverrides] : [];
227
240
  const renderRootLayouts = [...parentRootLayouts, ...(currentRootLayout ? [currentRootLayout] : [])];
228
- const renderLayouts = [...parentLayouts, ...(currentLayout ? [currentLayout] : [])];
241
+ const renderLayouts = [...parentLayouts, ...currentOverrideRenders, ...(currentLayout ? [currentLayout] : [])];
229
242
  if (route.renderLayout) {
230
243
  this.#fallbackRoutes.push({
231
244
  path: routePath,
@@ -237,7 +250,10 @@ export class RouteTreeBuilder {
237
250
  const routeHead = RouteTreeBuilder.#composeHeadResolvers(route.renderLayout?.resolveHead, parentHead);
238
251
  const pageRenderRootLayouts =
239
252
  route.pageIncludesOwnLayout === false && currentRootLayout ? parentRootLayouts : renderRootLayouts;
240
- const pageRenderLayouts = route.pageIncludesOwnLayout === false && currentLayout ? parentLayouts : renderLayouts;
253
+ const pageRenderLayouts =
254
+ route.pageIncludesOwnLayout === false && currentLayout
255
+ ? [...parentLayouts, ...currentOverrideRenders]
256
+ : renderLayouts;
241
257
  const pageHead = route.pageIncludesOwnLayout === false ? parentHead : routeHead;
242
258
  return [
243
259
  ...(route.renderPage
@@ -262,7 +278,7 @@ export class RouteTreeBuilder {
262
278
  ];
263
279
  }
264
280
 
265
- static #makeLazyModule(key: string, kind: "page" | "layout", loader: () => Promise<RouteModule>) {
281
+ static #makeLazyModule(key: string, kind: RouteModuleKindWithOverrides, loader: () => Promise<RouteModule>) {
266
282
  let cached: RouteModule | null = null;
267
283
  let loaded = false;
268
284
  RouteTreeBuilder.#moduleCacheStats.moduleCount += 1;
@@ -285,7 +301,12 @@ export class RouteTreeBuilder {
285
301
  };
286
302
  }
287
303
 
288
- static #validateRouteModuleExports(key: string, kind: "page" | "layout", mod: RouteModule) {
304
+ static #validateRouteModuleExports(key: string, kind: RouteModuleKindWithOverrides, mod: RouteModule) {
305
+ if (kind === "overrides") {
306
+
307
+ if (!mod.default) throw new Error(`[route-convention] ${key} generated override wrapper has no default export`);
308
+ return;
309
+ }
289
310
  const parsed = parseRouteModuleKey(key);
290
311
  const allowed =
291
312
  kind === "page"
@@ -378,6 +399,18 @@ export class RouteTreeBuilder {
378
399
  return routeRender;
379
400
  }
380
401
 
402
+ #makeOverridesRender(key: string, loader: () => Promise<RouteModule>): RouteRender {
403
+ const loadModule = RouteTreeBuilder.#makeLazyModule(key, "overrides", loader);
404
+ return {
405
+ isAsync: true,
406
+ render: (async ({ children }: LayoutProps) => {
407
+ const mod = await loadModule();
408
+ if (!mod.default) throw new Error(`[route-convention] ${key} generated override wrapper has no default export`);
409
+ return createElement(mod.default as never, { children } as never);
410
+ }) as RouteRender["render"],
411
+ };
412
+ }
413
+
381
414
  static #composeHeadResolvers(...resolvers: (ResolveHead | undefined)[]): ResolveHead | undefined {
382
415
  const chain = resolvers.filter((resolver): resolver is ResolveHead => Boolean(resolver));
383
416
  if (chain.length === 0) return undefined;
@@ -99,6 +99,16 @@ export class FetchSerializer {
99
99
  ...(sliceCls.cruGuards.filter((g) => g.name !== "None").length
100
100
  ? { cruGuards: sliceCls.cruGuards.map((g) => g.name) }
101
101
  : {}),
102
+
103
+ ...(sliceCls.createGuards !== sliceCls.cruGuards && sliceCls.createGuards.filter((g) => g.name !== "None").length
104
+ ? { createGuards: sliceCls.createGuards.map((g) => g.name) }
105
+ : {}),
106
+ ...(sliceCls.updateGuards !== sliceCls.cruGuards && sliceCls.updateGuards.filter((g) => g.name !== "None").length
107
+ ? { updateGuards: sliceCls.updateGuards.map((g) => g.name) }
108
+ : {}),
109
+ ...(sliceCls.removeGuards !== sliceCls.cruGuards && sliceCls.removeGuards.filter((g) => g.name !== "None").length
110
+ ? { removeGuards: sliceCls.removeGuards.map((g) => g.name) }
111
+ : {}),
102
112
  endpoint,
103
113
  };
104
114
  }
@@ -341,6 +341,11 @@ export class SignalContext<
341
341
  getEnv() {
342
342
  return this.#env;
343
343
  }
344
+ getArg<T = unknown>(argName: string): T | undefined {
345
+ const index = this.endpointInfo.args.findIndex((arg) => arg.name === argName);
346
+ if (index === -1) return undefined;
347
+ return this.args[index] as T;
348
+ }
344
349
  }
345
350
 
346
351
  export class HttpExecutionContext<Appended = unknown> {
package/signal/slice.ts CHANGED
@@ -35,6 +35,9 @@ export type SliceCls<
35
35
  [SLICE_META]: SliceInfoObj;
36
36
  getGuards: GuardCls[];
37
37
  cruGuards: GuardCls[];
38
+ createGuards: GuardCls[];
39
+ updateGuards: GuardCls[];
40
+ removeGuards: GuardCls[];
38
41
  };
39
42
 
40
43
  interface RootSliceOption {
@@ -42,6 +45,9 @@ interface RootSliceOption {
42
45
  root?: Cls<Guard> | Cls<Guard>[];
43
46
  get?: Cls<Guard> | Cls<Guard>[];
44
47
  cru?: Cls<Guard> | Cls<Guard>[];
48
+ create?: Cls<Guard> | Cls<Guard>[];
49
+ update?: Cls<Guard> | Cls<Guard>[];
50
+ remove?: Cls<Guard> | Cls<Guard>[];
45
51
  };
46
52
  prefix?: string;
47
53
  }
@@ -123,21 +129,14 @@ export function slice<
123
129
  srv.cnst.insight,
124
130
  srv.db.filter,
125
131
  );
126
- const rootGuards = option.guards?.root
127
- ? Array.isArray(option.guards.root)
128
- ? option.guards.root
129
- : [option.guards.root]
130
- : [];
131
- const getGuards = option.guards?.get
132
- ? Array.isArray(option.guards.get)
133
- ? option.guards.get
134
- : [option.guards.get]
135
- : [];
136
- const cruGuards = option.guards?.cru
137
- ? Array.isArray(option.guards.cru)
138
- ? option.guards.cru
139
- : [option.guards.cru]
140
- : [];
132
+ const toGuards = (guard?: Cls<Guard> | Cls<Guard>[]) => (guard ? (Array.isArray(guard) ? guard : [guard]) : []);
133
+ const rootGuards = toGuards(option.guards?.root);
134
+ const getGuards = toGuards(option.guards?.get);
135
+ const cruGuards = toGuards(option.guards?.cru);
136
+
137
+ const createGuards = option.guards?.create ? toGuards(option.guards.create) : cruGuards;
138
+ const updateGuards = option.guards?.update ? toGuards(option.guards.update) : cruGuards;
139
+ const removeGuards = option.guards?.remove ? toGuards(option.guards.remove) : cruGuards;
141
140
  const srvKeys = [
142
141
  ...new Set([...Object.keys(srv.srvMap), ...libSlices.flatMap((libSlice) => Object.keys(libSlice.srv.srvMap))]),
143
142
  ];
@@ -148,6 +147,9 @@ export function slice<
148
147
  static srv = srv;
149
148
  static getGuards = getGuards;
150
149
  static cruGuards = cruGuards;
150
+ static createGuards = createGuards;
151
+ static updateGuards = updateGuards;
152
+ static removeGuards = removeGuards;
151
153
  static [SLICE_META] = Object.assign(
152
154
  {
153
155
  [""]: init({ guards: rootGuards })
package/signal/types.ts CHANGED
@@ -129,6 +129,9 @@ export interface SerializedSignal {
129
129
  filter?: SerializedFilter;
130
130
  getGuards?: string[];
131
131
  cruGuards?: string[];
132
+ createGuards?: string[];
133
+ updateGuards?: string[];
134
+ removeGuards?: string[];
132
135
  }
133
136
 
134
137
  export type SignalType = "restapi" | "websocket";
@@ -190,6 +190,8 @@ export interface Route {
190
190
  path: string;
191
191
  renderPage?: RouteRender;
192
192
  renderLayout?: RouteRender;
193
+ /** Synthetic layout render from a `_overrides.tsx` at this node; wraps the subtree in a UI-override provider. */
194
+ renderOverrides?: RouteRender;
193
195
  pageIncludesOwnLayout?: boolean;
194
196
  isSpecialRoute?: boolean;
195
197
  loader?: () => unknown;
@@ -1,4 +1,4 @@
1
- export type RouteModuleKind = "page" | "layout";
1
+ export type RouteModuleKind = "page" | "layout" | "overrides";
2
2
  export interface ParsedRouteModuleKey {
3
3
  key: string;
4
4
  kind: RouteModuleKind;
@@ -48,6 +48,7 @@ export declare class SignalContext<Ctx extends HttpExecutionContext | WebSocketE
48
48
  getWebSocketContext<Appended = unknown>(): WebSocketExecutionContext<Appended>;
49
49
  getRoomId(key: string): string;
50
50
  getEnv(): Env;
51
+ getArg<T = unknown>(argName: string): T | undefined;
51
52
  }
52
53
  export declare class HttpExecutionContext<Appended = unknown> {
53
54
  #private;
@@ -27,12 +27,18 @@ export type SliceCls<SrvModule extends ServiceModel = ServiceModel, SliceInfoObj
27
27
  [SLICE_META]: SliceInfoObj;
28
28
  getGuards: GuardCls[];
29
29
  cruGuards: GuardCls[];
30
+ createGuards: GuardCls[];
31
+ updateGuards: GuardCls[];
32
+ removeGuards: GuardCls[];
30
33
  };
31
34
  interface RootSliceOption {
32
35
  guards?: {
33
36
  root?: Cls<Guard> | Cls<Guard>[];
34
37
  get?: Cls<Guard> | Cls<Guard>[];
35
38
  cru?: Cls<Guard> | Cls<Guard>[];
39
+ create?: Cls<Guard> | Cls<Guard>[];
40
+ update?: Cls<Guard> | Cls<Guard>[];
41
+ remove?: Cls<Guard> | Cls<Guard>[];
36
42
  };
37
43
  prefix?: string;
38
44
  }
@@ -122,6 +122,9 @@ export interface SerializedSignal {
122
122
  filter?: SerializedFilter;
123
123
  getGuards?: string[];
124
124
  cruGuards?: string[];
125
+ createGuards?: string[];
126
+ updateGuards?: string[];
127
+ removeGuards?: string[];
125
128
  }
126
129
  export type SignalType = "restapi" | "websocket";
127
130
  export type WebsocketReqData = {
@@ -8,4 +8,9 @@ export type ButtonProps<Result> = Omit<ButtonHTMLAttributes<HTMLButtonElement>,
8
8
  /** Called after the button briefly enters success state. */
9
9
  onSuccess?: (result: Result) => void;
10
10
  };
11
- export declare const Button: <Result = unknown>({ className, children, onClick, onSuccess, ...rest }: ButtonProps<Result>) => import("react/jsx-runtime").JSX.Element;
11
+ /**
12
+ * Async-aware button. Resolves to a route-scoped override when a `page/**\/_overrides.tsx`
13
+ * in the route's ancestry declares one, otherwise renders {@link DefaultButton}. The public
14
+ * generic signature is preserved, so `<Button<Todo> onSuccess={(r: Todo) => …} />` still infers.
15
+ */
16
+ export declare const Button: <Result = unknown>(props: ButtonProps<Result>) => React.ReactElement<ButtonProps<Result>, string | React.JSXElementConstructor<any>>;
@@ -1,5 +1,5 @@
1
1
  import { type Dayjs } from "akanjs/base";
2
- interface DatePickerProps {
2
+ export interface DatePickerProps {
3
3
  value?: Dayjs | null;
4
4
  onChange: (value: Dayjs | null) => void;
5
5
  showTime?: boolean;
@@ -10,12 +10,7 @@ interface DatePickerProps {
10
10
  placement?: "top" | "bottom" | "left" | "right";
11
11
  defaultValue?: Dayjs;
12
12
  }
13
- export declare const DatePicker: {
14
- ({ value, onChange, showTime, format, timeIntervals, disabledDate, placement, className, defaultValue, }: DatePickerProps): import("react/jsx-runtime").JSX.Element;
15
- RangePicker: ({ value, onChange, format, showTime, timeIntervals, disabledDate, className, }: RangePickerProps) => import("react/jsx-runtime").JSX.Element;
16
- TimePicker: ({ disabled, className, value, format, onChange, timeIntervals, }: TimePickerProps) => import("react/jsx-runtime").JSX.Element;
17
- };
18
- interface RangePickerProps {
13
+ export interface RangePickerProps {
19
14
  value: [Dayjs | null, Dayjs | null];
20
15
  onChange: (value: [Dayjs | null, Dayjs | null]) => void;
21
16
  format?: string;
@@ -24,7 +19,7 @@ interface RangePickerProps {
24
19
  disabledDate?: (date: Dayjs) => boolean | null | undefined;
25
20
  className?: string;
26
21
  }
27
- interface TimePickerProps {
22
+ export interface TimePickerProps {
28
23
  value: Dayjs | null;
29
24
  onChange: (value: Dayjs) => void;
30
25
  format?: string;
@@ -33,4 +28,12 @@ interface TimePickerProps {
33
28
  className?: string;
34
29
  disabled?: boolean;
35
30
  }
36
- export {};
31
+ /**
32
+ * Date picker. `DatePicker`, `DatePicker.RangePicker`, and `DatePicker.TimePicker` each resolve to a
33
+ * route-scoped override when a `page/**\/_overrides.tsx` in the route's ancestry declares one (slots
34
+ * `DatePicker`, `DatePickerRangePicker`, `DatePickerTimePicker`).
35
+ */
36
+ export declare const DatePicker: import("react").ComponentType<DatePickerProps> & {
37
+ RangePicker: import("react").ComponentType<RangePickerProps>;
38
+ TimePicker: import("react").ComponentType<TimePickerProps>;
39
+ };
@@ -11,4 +11,9 @@ export interface DropdownProps {
11
11
  /** Additional classes for the dropdown content panel. */
12
12
  dropdownClassName?: string;
13
13
  }
14
- export declare const Dropdown: ({ value, content, className, buttonClassName, dropdownClassName }: DropdownProps) => import("react/jsx-runtime").JSX.Element;
14
+ export declare const DefaultDropdown: ({ value, content, className, buttonClassName, dropdownClassName }: DropdownProps) => import("react/jsx-runtime").JSX.Element;
15
+ /**
16
+ * Dropdown. Resolves to a route-scoped override when a `page/**\/_overrides.tsx`
17
+ * in the route's ancestry declares one, otherwise renders {@link DefaultDropdown}.
18
+ */
19
+ export declare const Dropdown: import("react").ComponentType<DropdownProps>;
@@ -9,4 +9,10 @@ export interface EmptyProps {
9
9
  /** Minimum empty-state height in pixels. */
10
10
  minHeight?: number;
11
11
  }
12
- export declare const Empty: ({ className, description, children, minHeight }: EmptyProps) => import("react/jsx-runtime").JSX.Element;
12
+ export declare const DefaultEmpty: ({ className, description, children, minHeight }: EmptyProps) => import("react/jsx-runtime").JSX.Element;
13
+ /**
14
+ * Empty-state placeholder. Resolves to a route-scoped override when a
15
+ * `page/**\/_overrides.tsx` in the route's ancestry declares one, otherwise
16
+ * renders {@link DefaultEmpty}.
17
+ */
18
+ export declare const Empty: import("react").ComponentType<EmptyProps>;
@@ -24,14 +24,6 @@ export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "
24
24
  /** Called when Escape is pressed after blurring the input. */
25
25
  onPressEscape?: (e: KeyboardEvent<HTMLInputElement>) => void;
26
26
  };
27
- export declare const Input: {
28
- ({ className, nullable, inputRef, value, cacheKey, inputStyleType, icon, iconClassName, inputClassName, inputWrapperClassName, onPressEnter, onPressEscape, onChange, validate, ...rest }: InputProps): import("react/jsx-runtime").JSX.Element;
29
- TextArea: ({ className, nullable, value, inputClassName, inputWrapperClassName, cacheKey, onPressEnter, onPressEscape, onChange, validate, ...rest }: TextAreaProps) => import("react/jsx-runtime").JSX.Element;
30
- Password: ({ className, nullable, value, icon, iconClassName, inputClassName, inputWrapperClassName, cacheKey, onPressEnter, onPressEscape, onChange, validate, ...rest }: PasswordProps) => import("react/jsx-runtime").JSX.Element;
31
- Email: ({ inputStyleType, className, nullable, value, cacheKey, onPressEnter, onPressEscape, onChange, validate, icon, iconClassName, inputClassName, inputWrapperClassName, ...rest }: EmailProps) => import("react/jsx-runtime").JSX.Element;
32
- Number: ({ className, nullable, value, icon, cacheKey, iconClassName, inputClassName, inputWrapperClassName, numberFormat, onPressEnter, onPressEscape, validate, onChange, formatter, parser, ...rest }: NumberProps) => import("react/jsx-runtime").JSX.Element;
33
- Checkbox: ({ checked, onChange, className, ...rest }: CheckboxProps) => import("react/jsx-runtime").JSX.Element;
34
- };
35
27
  export type TextAreaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "value" | "type" | "onChange" | "onPressEnter"> & {
36
28
  inputRef?: RefObject<HTMLTextAreaElement | null>;
37
29
  value: string;
@@ -93,3 +85,15 @@ export type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, "onChang
93
85
  checked: boolean;
94
86
  onChange: (checked: boolean, e: ChangeEvent<HTMLInputElement>) => void;
95
87
  };
88
+ /**
89
+ * Text input plus its field variants. Each leaf resolves to a route-scoped override when a
90
+ * `page/**\/_overrides.tsx` in the route's ancestry declares one (slots `Input`, `InputTextArea`,
91
+ * `InputPassword`, `InputEmail`, `InputNumber`, `InputCheckbox`), otherwise renders the default.
92
+ */
93
+ export declare const Input: React.ComponentType<InputProps> & {
94
+ TextArea: React.ComponentType<TextAreaProps>;
95
+ Password: React.ComponentType<PasswordProps>;
96
+ Email: React.ComponentType<EmailProps>;
97
+ Number: React.ComponentType<NumberProps>;
98
+ Checkbox: React.ComponentType<CheckboxProps>;
99
+ };
@@ -1,8 +1,13 @@
1
+ /**
2
+ * Loading indicators. Each member is an independent override slot (`LoadingSpin`, `LoadingSkeleton`,
3
+ * `LoadingProgressBar`, `LoadingButton`, `LoadingInput`, `LoadingArea`), resolved from the closest
4
+ * `page/**\/_overrides.tsx` in the route's ancestry, otherwise the shipped default.
5
+ */
1
6
  export declare const Loading: {
2
- Area: () => import("react/jsx-runtime").JSX.Element;
3
- Button: ({ className, active, style }: import("./Button.d.ts").LoadingProps) => import("react/jsx-runtime").JSX.Element;
4
- Input: ({ className, active, style }: import("./Input.d.ts").LoadingProps) => import("react/jsx-runtime").JSX.Element;
5
- ProgressBar: ({ className, value, max }: import("./ProgressBar.d.ts").ProgressBarProps) => import("react/jsx-runtime").JSX.Element;
6
- Skeleton: ({ className, active, style }: import("./Skeleton.d.ts").SkeletonProps) => import("react/jsx-runtime").JSX.Element;
7
- Spin: ({ indicator, isCenter, className }: import("./Spin.d.ts").SpinProps) => import("react/jsx-runtime").JSX.Element;
7
+ Area: import("react").ComponentType<Record<string, never>>;
8
+ Button: import("react").ComponentType<import("./Button.d.ts").LoadingProps>;
9
+ Input: import("react").ComponentType<import("./Input.d.ts").LoadingProps>;
10
+ ProgressBar: import("react").ComponentType<import("./ProgressBar.d.ts").ProgressBarProps>;
11
+ Skeleton: import("react").ComponentType<import("./Skeleton.d.ts").SkeletonProps>;
12
+ Spin: import("react").ComponentType<import("./Spin.d.ts").SpinProps>;
8
13
  };
@@ -1,12 +1,12 @@
1
1
  import { type ReactNode } from "react";
2
- interface MenuItem {
2
+ export interface MenuItem {
3
3
  label: ReactNode;
4
4
  key: string;
5
5
  children?: MenuItem[];
6
6
  icon?: ReactNode;
7
7
  type?: string;
8
8
  }
9
- interface MenuProps {
9
+ export interface MenuProps {
10
10
  className?: string;
11
11
  ulClassName?: string;
12
12
  liClassName?: string;
@@ -22,5 +22,10 @@ interface MenuProps {
22
22
  onMouseOver?: () => void;
23
23
  onMouseLeave?: () => void;
24
24
  }
25
- export declare const Menu: ({ items, onClick, selectedKeys, labelClassName, defaultSelectedKeys, className, ulClassName, liClassName, style, mode, activeStyle, inlineCollapsed, onMouseOver, onMouseLeave, }: MenuProps) => import("react/jsx-runtime").JSX.Element;
26
- export {};
25
+ export declare const DefaultMenu: ({ items, onClick, selectedKeys, labelClassName, defaultSelectedKeys, className, ulClassName, liClassName, style, mode, activeStyle, inlineCollapsed, onMouseOver, onMouseLeave, }: MenuProps) => import("react/jsx-runtime").JSX.Element;
26
+ /**
27
+ * Navigation menu. Resolves to a route-scoped override when a
28
+ * `page/**\/_overrides.tsx` in the route's ancestry declares one, otherwise
29
+ * renders {@link DefaultMenu}.
30
+ */
31
+ export declare const Menu: import("react").ComponentType<MenuProps>;