akanjs 3.0.0-alpha.2 → 3.0.0-alpha.4

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 (76) hide show
  1. package/base/symbols.ts +0 -1
  2. package/client/cn.ts +7 -0
  3. package/client/csrTypes.ts +2 -0
  4. package/client/frameDebug.ts +3 -2
  5. package/constant/cascadePaths.ts +8 -79
  6. package/document/database.ts +1 -2
  7. package/document/databaseRegistry.ts +2 -2
  8. package/document/into.ts +6 -10
  9. package/package.json +1 -1
  10. package/server/SSR_MEMORY_DIAGNOSIS.md +2 -19
  11. package/server/cachePolicy.ts +12 -114
  12. package/server/di/diLifecycle.ts +10 -8
  13. package/server/processMetricsCollector.ts +0 -4
  14. package/server/resolver/database.resolver.ts +3 -23
  15. package/server/resolver/index.ts +0 -1
  16. package/server/resolver/service.resolver.ts +25 -9
  17. package/server/rscWorker.tsx +4 -32
  18. package/server/rscWorkerHost.ts +12 -61
  19. package/server/ssrFromRscRenderer.tsx +2 -2
  20. package/server/webRouter.ts +96 -9
  21. package/service/ipcTypes.ts +0 -25
  22. package/service/predefinedAdaptor/database.adaptor.ts +3 -3
  23. package/service/serve.ts +1 -5
  24. package/service/types.ts +0 -2
  25. package/types/base/symbols.d.ts +0 -1
  26. package/types/client/cn.d.ts +5 -0
  27. package/types/client/csrTypes.d.ts +2 -0
  28. package/types/constant/cascadePaths.d.ts +4 -22
  29. package/types/document/database.d.ts +1 -2
  30. package/types/document/databaseRegistry.d.ts +2 -2
  31. package/types/document/into.d.ts +5 -6
  32. package/types/server/cachePolicy.d.ts +2 -33
  33. package/types/server/resolver/database.resolver.d.ts +2 -6
  34. package/types/server/resolver/index.d.ts +0 -1
  35. package/types/server/resolver/service.resolver.d.ts +3 -3
  36. package/types/server/rscWorkerHost.d.ts +0 -13
  37. package/types/service/ipcTypes.d.ts +0 -24
  38. package/types/service/predefinedAdaptor/database.adaptor.d.ts +2 -2
  39. package/types/service/types.d.ts +1 -2
  40. package/types/ui/Layout/BottomInset.d.ts +2 -1
  41. package/types/ui/Layout/index.d.ts +1 -1
  42. package/types/ui/Signal/style.d.ts +1 -1
  43. package/types/ui/UiOverride/context.d.ts +12 -2
  44. package/types/ui/recipe/badgeRecipe.d.ts +7 -3
  45. package/types/ui/recipe/buttonRecipe.d.ts +7 -3
  46. package/types/ui/recipe/inputRecipe.d.ts +5 -1
  47. package/types/webkit/useCsrValues.d.ts +3 -3
  48. package/types/webkit/useFrameRuntime.d.ts +6 -1
  49. package/ui/Badge.tsx +2 -2
  50. package/ui/Button.tsx +3 -1
  51. package/ui/Constant/Doc.tsx +1 -1
  52. package/ui/Data/ListContainer.tsx +1 -1
  53. package/ui/DatePicker.tsx +4 -3
  54. package/ui/DraggableList.tsx +3 -1
  55. package/ui/Dropdown.tsx +4 -2
  56. package/ui/Field.tsx +17 -9
  57. package/ui/Input.tsx +7 -5
  58. package/ui/Layout/BottomInset.tsx +6 -1
  59. package/ui/Loading/ProgressBar.tsx +8 -1
  60. package/ui/Menu.tsx +7 -8
  61. package/ui/Model/SureToRemove.tsx +2 -1
  62. package/ui/ObjectId.tsx +3 -4
  63. package/ui/Pagination.tsx +3 -4
  64. package/ui/Popconfirm.tsx +5 -7
  65. package/ui/Select.tsx +1 -1
  66. package/ui/System/CSR.tsx +6 -2
  67. package/ui/ToggleSelect.tsx +4 -3
  68. package/ui/UiOverride/context.ts +12 -2
  69. package/ui/recipe/badgeRecipe.ts +22 -3
  70. package/ui/recipe/buttonRecipe.ts +51 -2
  71. package/ui/recipe/factory.ts +2 -2
  72. package/ui/recipe/inputRecipe.ts +18 -4
  73. package/webkit/useCsrValues.ts +121 -4
  74. package/webkit/useFrameRuntime.ts +42 -12
  75. package/server/resolver/CascadeRunner.ts +0 -201
  76. package/types/server/resolver/CascadeRunner.d.ts +0 -13
@@ -78,45 +78,14 @@ export declare function shouldStoreRouteCache(input: {
78
78
  }): RouteCacheRenderState;
79
79
  export declare function hasRouteCacheInvalidationScope(invalidation?: RouteCacheInvalidation): boolean;
80
80
  export declare function shouldInvalidateRouteCacheEntry(metadata: RouteCacheMetadata, invalidation: RouteCacheInvalidation): boolean;
81
- export interface LruTtlCacheOptions<T> {
82
- /**
83
- * Measures one entry's payload. Entry count alone says nothing about a cache whose entries span
84
- * three orders of magnitude, and a byte ceiling needs a running total to enforce. The default
85
- * reports 0 rather than guessing, so `byteSize` stays honest about not knowing.
86
- */
87
- sizeOf?: (value: T) => number;
88
- /** Total payload ceiling. 0 leaves the cache bounded only by `maxEntries`. */
89
- maxBytes?: number;
90
- /** An entry over this is not stored at all, rather than evicting everything else to fit it. */
91
- maxEntryBytes?: number;
92
- /**
93
- * Cadence of the idle sweep. Without one a filled cache never shrinks: an entry is dropped only
94
- * when its own key is fetched after expiry or when a write evicts it, so a pod that stops
95
- * serving holds its peak forever — measured at 100 entries / 21.4 MiB still resident 310s after
96
- * the last request, with a 30s TTL. 0 disables it.
97
- */
98
- sweepIntervalMs?: number;
99
- }
100
81
  export declare class LruTtlCache<T> {
101
82
  #private;
102
83
  readonly maxEntries: number;
103
- constructor(maxEntries?: number, options?: LruTtlCacheOptions<T>);
84
+ constructor(maxEntries?: number);
104
85
  get size(): number;
105
- get byteSize(): number;
106
86
  get(key: string): T | null;
107
- /** Returns whether the entry was stored; a payload over `maxEntryBytes` is rejected. */
108
- set(key: string, value: T, ttlSeconds: number): boolean;
109
- /**
110
- * Drops every expired entry. Deliberately a full scan rather than a walk from the oldest that
111
- * stops at the first live entry: insertion order is *LRU* order because `get` reinserts, and TTLs
112
- * differ per entry, so expiry is not monotonic in map order and an early break would leave
113
- * expired entries behind. The map is bounded by `maxEntries`, so the scan is cheap.
114
- */
115
- sweepExpired(now?: number): number;
116
- /** Stops the idle sweep. The cache stays usable; only the timer goes away. */
117
- dispose(): void;
87
+ set(key: string, value: T, ttlSeconds: number): void;
118
88
  delete(key: string): boolean;
119
89
  invalidate(predicate: (key: string, value: T) => boolean): number;
120
90
  clear(): void;
121
- static parseByteCeiling(value: string | undefined | null, fallback?: number): number;
122
91
  }
@@ -1,10 +1,6 @@
1
1
  import { type ConstantModel } from "akanjs/constant";
2
- import { type DatabaseInstance, type DatabaseModel, DocumentSchema } from "akanjs/document";
2
+ import { type DatabaseInstance, type DatabaseModel } from "akanjs/document";
3
3
  import { type AdaptorCls } from "akanjs/service";
4
4
  export declare class DatabaseResolver {
5
- /** Returns the schema alongside the adaptor: the cascade planner reads its `remove` hooks to pick a strategy. */
6
- static resolveDatabase(constant: ConstantModel, database: DatabaseModel): {
7
- adaptor: AdaptorCls<DatabaseInstance>;
8
- schema: DocumentSchema;
9
- };
5
+ static resolveDatabase(constant: ConstantModel, database: DatabaseModel): AdaptorCls<DatabaseInstance>;
10
6
  }
@@ -1,4 +1,3 @@
1
- export * from "./CascadeRunner.d.ts";
2
1
  export * from "./database.resolver";
3
2
  export * from "./service.resolver";
4
3
  export * from "./signal.resolver";
@@ -1,7 +1,7 @@
1
+ import { type ConstantModel } from "akanjs/constant";
1
2
  import { type DatabaseModel } from "akanjs/document";
2
- import type { ServiceCls } from "akanjs/service";
3
- import type { CascadeRunner } from "./CascadeRunner.d.ts";
3
+ import type { DatabaseService, ServiceCls } from "akanjs/service";
4
4
  export declare class ServiceResolver {
5
5
  #private;
6
- static resolveDatabaseService(database: DatabaseModel, srvRef: ServiceCls, cascade: CascadeRunner): ServiceCls;
6
+ static resolveDatabaseService(constant: ConstantModel, database: DatabaseModel, srvRef: ServiceCls, getService: (refName: string) => DatabaseService): ServiceCls;
7
7
  }
@@ -49,19 +49,6 @@ export declare function nextRscHostPendingChunkCount(currentPendingChunks: numbe
49
49
  export declare function isRscHostPendingChunkOverflow(pendingChunks: number, maxPendingChunks: number): boolean;
50
50
  export declare function createIdempotentRscRenderCancel(onCancel: (reason?: unknown) => void): (reason?: unknown) => void;
51
51
  export declare function createRscWorkerInvalidateCacheMessage(invalidation?: string | RouteCacheInvalidation): RscWorkerInvalidateCacheMessage;
52
- /**
53
- * The RSC worker samples its own process with the same `ProcessMetricsCollector.collect` its host
54
- * replica uses, so its report carries `pid` / `rssBytes` / `role` / … under the identical names.
55
- * `AkanServer` spreads the host's report into its own `collect({ role, ...webRouter.getMetrics() })`
56
- * and `collect` spreads `extra` last (`processMetricsCollector.ts:122`) — so passing these through
57
- * unprefixed silently overwrote the replica's own process fields, and the replica's RSS was reported
58
- * nowhere. Rename them onto `rscWorker*`; the `rsc*` render counters, which only the worker
59
- * produces, pass through untouched.
60
- *
61
- * XXX A new process-level field in `ProcessMetricsCollector.collect` must be added here too, or it
62
- * starts shadowing the replica again.
63
- */
64
- export declare function projectRscWorkerProcessMetrics(metrics: AkanMetricsReport): AkanMetricsReport;
65
52
  export declare function createRscHostRenderStream(input: {
66
53
  setPending: (pending: RscPending) => void;
67
54
  deletePending: () => void;
@@ -59,25 +59,6 @@ export interface AkanMetricsReport {
59
59
  rscWorkerRestartCount?: number;
60
60
  rscWorkerRecycleCount?: number;
61
61
  rscWorkerLastRecycleReason?: string;
62
- rscWorkerReportedAt?: number;
63
- rscWorkerRssBytes?: number;
64
- rscWorkerHeapTotalBytes?: number;
65
- rscWorkerHeapUsedBytes?: number;
66
- rscWorkerExternalBytes?: number;
67
- rscWorkerArrayBuffersBytes?: number;
68
- rscWorkerCpuUserMicros?: number;
69
- rscWorkerCpuSystemMicros?: number;
70
- rscWorkerMaxRssKb?: number;
71
- rscWorkerJscHeapSizeBytes?: number;
72
- rscWorkerJscHeapCapacityBytes?: number;
73
- /** Off-heap bytes JSC attributes to JS objects — typed-array backing stores, i.e. cached Flight chunks. */
74
- rscWorkerJscExtraMemorySizeBytes?: number;
75
- rscWorkerJscObjectCount?: number;
76
- rscWorkerJscProtectedObjectCount?: number;
77
- rscWorkerEventLoopLagMeanMs?: number;
78
- rscWorkerEventLoopLagP99Ms?: number;
79
- rscWorkerEventLoopLagMaxMs?: number;
80
- rscWorkerGcDurationMs?: number;
81
62
  rscPendingRenderCount?: number;
82
63
  rscQueuedSendCount?: number;
83
64
  rscHostPendingChunkOverflowCount?: number;
@@ -113,13 +94,9 @@ export interface AkanMetricsReport {
113
94
  rscLastRenderLoadedModuleDelta?: number;
114
95
  rscLastRenderLoadedModules?: string[];
115
96
  rscResultCacheEntries?: number;
116
- rscResultCacheBytes?: number;
117
- rscPatchResultCacheEntries?: number;
118
- rscPatchResultCacheBytes?: number;
119
97
  rscResultCacheHits?: number;
120
98
  rscResultCacheMisses?: number;
121
99
  rscResultCacheBypass?: number;
122
- /** Keys the SSR chunk registry tracks — NOT a memory bound; the modules stay in Bun's ESM registry. */
123
100
  ssrChunkRegistrySize?: number;
124
101
  ssrChunkLoadCount?: number;
125
102
  ssrChunkCacheHitCount?: number;
@@ -130,7 +107,6 @@ export interface AkanMetricsReport {
130
107
  httpCsrCount?: number;
131
108
  httpImageCount?: number;
132
109
  httpHtmlCacheEntries?: number;
133
- httpHtmlCacheBytes?: number;
134
110
  httpHtmlCacheHits?: number;
135
111
  httpHtmlCacheMisses?: number;
136
112
  httpHtmlCacheBypass?: number;
@@ -56,7 +56,7 @@ export interface DocumentStore {
56
56
  matchedCount: number;
57
57
  modifiedCount: number;
58
58
  }>;
59
- removeManyByQuery(query: DocumentQuery): Promise<{
59
+ deleteManyByQuery(query: DocumentQuery): Promise<{
60
60
  acknowledged: boolean;
61
61
  matchedCount: number;
62
62
  modifiedCount: number;
@@ -309,7 +309,7 @@ export declare class SqlDocumentStore {
309
309
  matchedCount: number;
310
310
  modifiedCount: number;
311
311
  }>;
312
- removeManyByQuery(query: DocumentQuery): Promise<{
312
+ deleteManyByQuery(query: DocumentQuery): Promise<{
313
313
  acknowledged: boolean;
314
314
  matchedCount: number;
315
315
  modifiedCount: number;
@@ -1,7 +1,7 @@
1
1
  import type { Cls, MergeAllTypes, PromiseOrObject } from "akanjs/base";
2
2
  import type { Logger } from "akanjs/common";
3
3
  import type { QueryOf } from "akanjs/constant";
4
- import type { CRUDEventType, DatabaseModel, DataInputOf, FilterInstance, FindQueryOption, GetDocObject, ListQueryOption, QueryMethodPart, SaveEventType, UpdateResult } from "akanjs/document";
4
+ import type { CRUDEventType, DatabaseModel, DataInputOf, FilterInstance, FindQueryOption, GetDocObject, ListQueryOption, QueryMethodPart, SaveEventType } from "akanjs/document";
5
5
  type ServiceMixinOmitKey = "onInit" | "onDestroy" | "_libsOnInit" | "_libsOnDestroy" | "_preCreate" | "_postCreate" | "_preUpdate" | "_postUpdate" | "_preRemove" | "_postRemove" | "_libsPreCreate" | "_libsPostCreate" | "_libsPreUpdate" | "_libsPostUpdate" | "_libsPreRemove" | "_libsPostRemove";
6
6
  type DatabaseQueryMethods<Query, Sort, Obj, Doc, Insight, FindQueryOption, ListQueryOption, DocQuery> = QueryMethodPart<Query, Sort, Obj, Doc, Insight, FindQueryOption, ListQueryOption, DocQuery>;
7
7
  type DocumentLike = {
@@ -31,7 +31,6 @@ export type DatabaseService<T extends string = string, Input = any, Doc = any, O
31
31
  __create: (data: _DataInputOfDoc) => Promise<Doc>;
32
32
  __update: (id: string, data: Partial<Doc>) => Promise<Doc>;
33
33
  __remove: (id: string) => Promise<Doc>;
34
- __removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
35
34
  __list(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
36
35
  __listIds(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
37
36
  __find(query?: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
@@ -4,10 +4,11 @@ export interface BottomInsetProps {
4
4
  className?: string;
5
5
  children: ReactNode;
6
6
  keyboardSticky?: boolean;
7
+ contentAnchor?: "bottom";
7
8
  role?: "bottomChrome" | "keyboardAccessory";
8
9
  estimatedHeight?: number;
9
10
  frameScope?: FrameSlotRegistration["scope"];
10
11
  frameSource?: FrameSlotRegistration["source"];
11
12
  frameCache?: boolean;
12
13
  }
13
- export declare const BottomInset: ({ className, children, keyboardSticky, role, estimatedHeight, frameScope, frameSource, frameCache, }: BottomInsetProps) => import("react/jsx-runtime").JSX.Element;
14
+ export declare const BottomInset: ({ className, children, keyboardSticky, contentAnchor, role, estimatedHeight, frameScope, frameSource, frameCache, }: BottomInsetProps) => import("react/jsx-runtime").JSX.Element;
@@ -4,7 +4,7 @@ export declare const Layout: {
4
4
  Navbar: ({ back, className, height, children, title, left, right }: import("./Navbar.d.ts").NavbarProps) => import("react/jsx-runtime").JSX.Element;
5
5
  TopInset: ({ className, children, estimatedHeight }: import("./TopInset.d.ts").TopInsetProps) => import("react/jsx-runtime").JSX.Element;
6
6
  BottomTab: ({ className, tabs, height }: import("./BottomTab.d.ts").BottomTabProps) => import("react/jsx-runtime").JSX.Element;
7
- BottomInset: ({ className, children, keyboardSticky, role, estimatedHeight, frameScope, frameSource, frameCache, }: import("./BottomInset.d.ts").BottomInsetProps) => import("react/jsx-runtime").JSX.Element;
7
+ BottomInset: ({ className, children, keyboardSticky, contentAnchor, role, estimatedHeight, frameScope, frameSource, frameCache, }: import("./BottomInset.d.ts").BottomInsetProps) => import("react/jsx-runtime").JSX.Element;
8
8
  Template: ({ className, children }: import("./Template.d.ts").TemplateProps) => import("react/jsx-runtime").JSX.Element;
9
9
  Unit: ({ className, children, href }: import("./Unit.d.ts").UnitProps) => import("react/jsx-runtime").JSX.Element;
10
10
  View: ({ className, children }: import("./View.d.ts").ViewProps) => import("react/jsx-runtime").JSX.Element;
@@ -11,4 +11,4 @@ export declare const signalUi: {
11
11
  export declare const getEndpointBadgeClassName: (type: string) => string;
12
12
  export declare const getGuardBadgeClassName: (guard: string) => string;
13
13
  export declare const getStatusBadgeClassName: (status: string) => string;
14
- export declare const getStatusTextareaClassName: (status: string) => "" | "pointer-events-none opacity-50" | "border-destructive text-destructive" | "border-primary";
14
+ export declare const getStatusTextareaClassName: (status: string) => "" | "border-primary" | "pointer-events-none opacity-50" | "border-destructive text-destructive";
@@ -16,7 +16,7 @@ import type { ModalProps } from "../Modal.d.ts";
16
16
  import type { PaginationProps } from "../Pagination.d.ts";
17
17
  import type { PopconfirmProps } from "../Popconfirm.d.ts";
18
18
  import type { ItemProps as RadioItemProps, RadioProps } from "../Radio.d.ts";
19
- import type { BadgeVariants, ButtonVariants } from "../recipe.d.ts";
19
+ import type { BadgeVariants, ButtonVariants, InputSurfaceVariants } from "../recipe.d.ts";
20
20
  import type { SelectProps } from "../Select.d.ts";
21
21
  import type { TableProps } from "../Table.d.ts";
22
22
  import type { MultiProps as ToggleSelectMultiProps, ToggleSelectProps } from "../ToggleSelect.d.ts";
@@ -75,11 +75,21 @@ export type AkanModalComponent = AkanUiOverrides["Modal"];
75
75
  * manifest: `override({ recipes: { button: neonButtonRecipe } })`. A recipe swap changes
76
76
  * only the className factory — the component's structure/behavior (async states, focus,
77
77
  * a11y) is untouched. Each replacement must accept the framework recipe's full variant
78
- * contract, so every call site keeps working.
78
+ * contract, so every call site keeps working (a replacement with *extra* optional axes is
79
+ * assignable — contravariance — but those axes are only reachable from code that knows the
80
+ * replacement's own type).
81
+ *
82
+ * Scope: a recipe slot is a **client-side, route-scoped restyle**. It reaches framework
83
+ * client components, which resolve through `useUiRecipe(...)`. It never reaches a raw
84
+ * `xRecipe(...)` call in app JSX (statically imported — no context), nor server components
85
+ * (`Unit`/`View`), which intentionally render the canonical framework recipe. Extending the
86
+ * *vocabulary* is not this slot's job: add the axis to the framework recipe, or author an
87
+ * app recipe in `apps/<app>/ui/Recipe/`.
79
88
  */
80
89
  export interface AkanUiRecipes {
81
90
  button: (variants?: ButtonVariants, className?: ClassValue) => string;
82
91
  badge: (variants?: BadgeVariants, className?: ClassValue) => string;
92
+ input: (variants?: InputSurfaceVariants, className?: ClassValue) => string;
83
93
  }
84
94
  /** Shape of an `_overrides.tsx` manifest: component slots plus an optional recipe-slot map. */
85
95
  export type AkanUiOverrideManifest = Partial<AkanUiOverrides> & {
@@ -1,11 +1,15 @@
1
- /** 뱃지 look — 시맨틱 variant. `<Badge>` 가 소비하며, recipes.badge 슬롯으로 교체 가능. */
1
+ /** 뱃지 look — 시맨틱 variant × size, outline 플래그는 색을 유지한 외곽선 스타일. `<Badge>` 가 소비하며, recipes.badge 슬롯으로 교체 가능. */
2
2
  export declare const badgeRecipe: (variants?: Omit<({
3
- variant?: "default" | "error" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "outline" | undefined;
3
+ size?: "lg" | "md" | "sm" | "xs" | undefined;
4
+ outline?: boolean | undefined;
5
+ variant?: "default" | "error" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "neutral" | "outline" | undefined;
4
6
  } & {
5
7
  class?: import("tailwind-variants").ClassValue;
6
8
  className?: never;
7
9
  }) | ({
8
- variant?: "default" | "error" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "outline" | undefined;
10
+ size?: "lg" | "md" | "sm" | "xs" | undefined;
11
+ outline?: boolean | undefined;
12
+ variant?: "default" | "error" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "neutral" | "outline" | undefined;
9
13
  } & {
10
14
  class?: never;
11
15
  className?: import("tailwind-variants").ClassValue;
@@ -1,13 +1,17 @@
1
- /** 버튼 look — 시맨틱 variant × size. `<Button>` 이 소비하며, `_overrides.tsx` 의 recipes.button 슬롯으로 교체 가능. */
1
+ /** 버튼 look — 시맨틱 variant × size × shape, outline 플래그는 색을 유지한 외곽선 스타일. `<Button>` 이 소비하며, `_overrides.tsx` 의 recipes.button 슬롯으로 교체 가능. */
2
2
  export declare const buttonRecipe: (variants?: Omit<({
3
3
  size?: "lg" | "md" | "sm" | "xs" | "icon" | undefined;
4
- variant?: "link" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "destructive" | "outline" | "ghost" | undefined;
4
+ outline?: boolean | undefined;
5
+ variant?: "link" | "default" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "destructive" | "neutral" | "outline" | "ghost" | undefined;
6
+ shape?: "default" | "circle" | "square" | undefined;
5
7
  } & {
6
8
  class?: import("tailwind-variants").ClassValue;
7
9
  className?: never;
8
10
  }) | ({
9
11
  size?: "lg" | "md" | "sm" | "xs" | "icon" | undefined;
10
- variant?: "link" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "destructive" | "outline" | "ghost" | undefined;
12
+ outline?: boolean | undefined;
13
+ variant?: "link" | "default" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "destructive" | "neutral" | "outline" | "ghost" | undefined;
14
+ shape?: "default" | "circle" | "square" | undefined;
11
15
  } & {
12
16
  class?: never;
13
17
  className?: import("tailwind-variants").ClassValue;
@@ -1,11 +1,15 @@
1
- /** 입력 표면 look — Input/TextArea 가 공유하는 필드 셸. kind 로 한 줄 필드(field)/멀티라인(area)을 고른다. */
1
+ /** 입력 표면 look — Input/TextArea/Select 가 공유하는 필드 셸. kind 로 한 줄 필드(field)/멀티라인(area), tone 으로 강조/오류 상태를 고른다. */
2
2
  export declare const inputRecipe: (variants?: Omit<({
3
+ size?: "xl" | "lg" | "md" | "sm" | "xs" | undefined;
3
4
  kind?: "field" | "area" | undefined;
5
+ tone?: "default" | "error" | "primary" | undefined;
4
6
  } & {
5
7
  class?: import("tailwind-variants").ClassValue;
6
8
  className?: never;
7
9
  }) | ({
10
+ size?: "xl" | "lg" | "md" | "sm" | "xs" | undefined;
8
11
  kind?: "field" | "area" | undefined;
12
+ tone?: "default" | "error" | "primary" | undefined;
9
13
  } & {
10
14
  class?: never;
11
15
  className?: import("tailwind-variants").ClassValue;
@@ -1,8 +1,8 @@
1
- import { type NavigationIntent, type PathRoute, type RouteGuide, type RouterInstance, type TransitionType } from "akanjs/client";
1
+ import { type NavigationIntent, type PageTransition, type PathRoute, type RouteGuide, type RouterInstance, type TransitionType } from "akanjs/client";
2
2
  export declare const useCsrValues: (rootRouteGuide: RouteGuide, pathRoutes: PathRoute[]) => {
3
+ page: PageTransition | null;
3
4
  topSafeArea: import("akanjs/client").SafeAreaTransition | null;
4
- page: import("akanjs/client").PageTransition | null;
5
- prevPage: import("akanjs/client").PageTransition | null;
5
+ prevPage: PageTransition | null;
6
6
  topInset: import("akanjs/client").ContainerTransition | null;
7
7
  bottomInset: import("akanjs/client").ContainerTransition | null;
8
8
  topLeftAction: import("akanjs/client").ContainerTransition | null;
@@ -4,7 +4,10 @@ export type FrameSlotBucket = "active" | "pending";
4
4
  export type FrameSlotMapByBucket = Record<FrameSlotBucket, FrameSlotMap>;
5
5
  export declare const PENDING_FRAME_READY_TIMEOUT_MS = 80;
6
6
  export declare const PENDING_FRAME_READY_MAX_TIMEOUT_MS = 120;
7
- export declare const KEYBOARD_FALLBACK_ANIMATION_DURATION_MS = 285;
7
+ export declare const KEYBOARD_SHOW_ANIMATION_DURATION_MS = 420;
8
+ export declare const KEYBOARD_HIDE_ANIMATION_DURATION_MS = 90;
9
+ export declare const KEYBOARD_SHOW_ANIMATION_EASING = "cubic-bezier(0.16, 1, 0.3, 1)";
10
+ export declare const KEYBOARD_HIDE_ANIMATION_EASING = "cubic-bezier(0.7, 0, 0.84, 0)";
8
11
  export declare const FRAME_Z_INDEX: {
9
12
  readonly page: 10;
10
13
  readonly previousPage: 0;
@@ -39,6 +42,7 @@ export declare function useFrameViewport(): {
39
42
  export declare function getKeyboardAccessorySlots(path: string, frameSlots: FrameSlotMap): FrameSlotRegistration[];
40
43
  export declare function resolveKeyboardAccessoryHeight(path: string, frameSlots: FrameSlotMap): number;
41
44
  export declare function hasKeyboardStickySlot(path: string, frameSlots: FrameSlotMap): boolean;
45
+ export declare function hasBottomAnchoredKeyboardSlot(path: string, frameSlots: FrameSlotMap): boolean;
42
46
  export declare function resolveKeyboardFrame({ keyboardHeight, bottomSafeArea, visualViewportKeyboardHeight, platformProfile, sticky, freeze, }: {
43
47
  keyboardHeight: number;
44
48
  bottomSafeArea: number;
@@ -57,6 +61,7 @@ export declare function useKeyboardFrame({ bottomSafeArea, sticky, viewport, pla
57
61
  bottomSafeArea: number;
58
62
  sticky: boolean;
59
63
  viewport: {
64
+ height: number;
60
65
  visualHeight: number;
61
66
  visualOffsetTop: number;
62
67
  };
package/ui/Badge.tsx CHANGED
@@ -7,10 +7,10 @@ export { badgeRecipe, type BadgeVariants };
7
7
 
8
8
  export type BadgeProps = HTMLAttributes<HTMLSpanElement> & BadgeVariants;
9
9
 
10
- const DefaultBadge = ({ className, variant, ...rest }: BadgeProps) => {
10
+ const DefaultBadge = ({ className, variant, size, outline, ...rest }: BadgeProps) => {
11
11
 
12
12
  const recipe = useUiRecipe("badge") ?? badgeRecipe;
13
- return <span className={recipe({ variant }, className)} {...rest} />;
13
+ return <span className={recipe({ variant, size, outline }, className)} {...rest} />;
14
14
  };
15
15
 
16
16
  /** Status/label pill. Route-overridable via `page/**\/_overrides.tsx` (slot `Badge`). */
package/ui/Button.tsx CHANGED
@@ -61,6 +61,8 @@ const DefaultButton = <Result = unknown>({
61
61
  className,
62
62
  variant,
63
63
  size,
64
+ shape,
65
+ outline,
64
66
  type = "button",
65
67
  loadingMode = "hold",
66
68
  showError = true,
@@ -84,7 +86,7 @@ const DefaultButton = <Result = unknown>({
84
86
  <button
85
87
  type={type}
86
88
  className={recipe(
87
- { variant, size },
89
+ { variant, size, shape, outline },
88
90
 
89
91
  cn(loadingMode === "hold" && "relative", busy && !rest.disabled && "disabled:opacity-100", className),
90
92
  )}
@@ -238,7 +238,7 @@ const EnumList = ({ enums = getConstantSchemaDoc().enums }: EnumProps) => {
238
238
  const { l } = usePage();
239
239
  return (
240
240
  <div className="overflow-x-auto rounded-xl bg-muted p-3">
241
- <table className="table">
241
+ <table className={tableClass}>
242
242
  <thead>
243
243
  <tr>
244
244
  <th>Key</th>
@@ -268,7 +268,7 @@ export default function ListContainer<
268
268
  <div className={cn("m-4", className)}>
269
269
  <div className="mb-3 flex flex-wrap justify-between">
270
270
  <div className="flex pb-1">
271
- <p className="prose text-lg">
271
+ <p className="text-lg">
272
272
  {title ?? l._(`${sliceName}.modelName`)}({modelInsight.count})
273
273
  </p>
274
274
  <div className="ml-3 flex items-center">
package/ui/DatePicker.tsx CHANGED
@@ -5,6 +5,7 @@ import { lazy } from "akanjs/webkit";
5
5
  import { type FocusEvent, useEffect, useRef } from "react";
6
6
  import { AiOutlineSwapRight } from "react-icons/ai";
7
7
 
8
+ import { inputRecipe } from "./recipe";
8
9
  import { createOverridable } from "./UiOverride";
9
10
 
10
11
  const reactDatePickerPackage = "react-datepicker";
@@ -58,7 +59,7 @@ const DefaultDatePicker = ({
58
59
 
59
60
  return (
60
61
  <ReactDatePicker
61
- className={cn("input text-center", className)}
62
+ className={inputRecipe({}, ["text-center", className])}
62
63
  selected={value ? value.toDate() : new Date()}
63
64
  disabledKeyboardNavigation
64
65
  onFocus={(e: FocusEvent<HTMLInputElement>) => {
@@ -110,9 +111,9 @@ const DefaultRangePicker = ({
110
111
  onChange([value[0] ?? dayjs(), dayjs(date)]);
111
112
  };
112
113
 
113
- const pickerClassName = "m-0 input focus:outline-hidden z-50 p-3 text-center h-full w-full ";
114
+ const pickerClassName = "m-0 h-full w-full border-none bg-transparent p-3 text-center focus:outline-hidden z-50";
114
115
  return (
115
- <div className={cn("input flex h-full w-fit items-center gap-2 p-0", className)}>
116
+ <div className={inputRecipe({}, ["flex h-full w-fit items-center gap-2 p-0", className])}>
116
117
  <ReactDatePicker
117
118
  className={pickerClassName}
118
119
  selected={value[0] ? value[0].toDate() : undefined}
@@ -7,6 +7,7 @@ import { createContext, type ReactElement, type ReactNode, useContext, useRef }
7
7
  import { BiTrash } from "react-icons/bi";
8
8
  import { MdDragIndicator } from "react-icons/md";
9
9
  import { buttonRecipe } from "./Button";
10
+ import { useUiRecipe } from "./UiOverride";
10
11
 
11
12
  const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
12
13
 
@@ -169,6 +170,7 @@ const Item = ({
169
170
  removeClassName,
170
171
  }: ItemProps) => {
171
172
  const { onRemove } = useDragList();
173
+ const recipe = useUiRecipe("button") ?? buttonRecipe;
172
174
  return (
173
175
  <div className={cn("flex w-full items-center gap-2", className)}>
174
176
  {cursor ? (
@@ -179,7 +181,7 @@ const Item = ({
179
181
  {children}
180
182
  {removable && (
181
183
  <button
182
- className={buttonRecipe({ variant: "outline", size: "xs" }, [
184
+ className={recipe({ variant: "outline", size: "xs" }, [
183
185
  "size-6 border-destructive p-0 text-destructive hover:bg-destructive hover:text-destructive-foreground",
184
186
  removeClassName,
185
187
  ])}
package/ui/Dropdown.tsx CHANGED
@@ -3,7 +3,7 @@ import { cn } from "akanjs/client";
3
3
  import { type ReactNode, useEffect, useRef, useState } from "react";
4
4
 
5
5
  import { buttonRecipe } from "./Button";
6
- import { createOverridable } from "./UiOverride";
6
+ import { createOverridable, useUiRecipe } from "./UiOverride";
7
7
 
8
8
  export interface DropdownProps {
9
9
  /** Button/trigger content. */
@@ -21,6 +21,8 @@ export interface DropdownProps {
21
21
  export const DefaultDropdown = ({ value, content, className, buttonClassName, dropdownClassName }: DropdownProps) => {
22
22
  const [opened, setOpened] = useState(false);
23
23
  const ref = useRef<HTMLDivElement>(null);
24
+
25
+ const recipe = useUiRecipe("button") ?? buttonRecipe;
24
26
  useEffect(() => {
25
27
  if (!opened) return;
26
28
  const onClick = (e: MouseEvent) => {
@@ -35,7 +37,7 @@ export const DefaultDropdown = ({ value, content, className, buttonClassName, dr
35
37
  <div ref={ref} className={cn("relative inline-block", className)}>
36
38
  <button
37
39
  type="button"
38
- className={buttonRecipe({ variant: "ghost" }, ["flex", buttonClassName])}
40
+ className={recipe({ variant: "ghost" }, ["flex", buttonClassName])}
39
41
  onClick={() => {
40
42
  setOpened((o) => !o);
41
43
  }}
package/ui/Field.tsx CHANGED
@@ -13,10 +13,12 @@ import { badgeRecipe } from "./Badge";
13
13
  import { buttonRecipe } from "./Button";
14
14
  import { DraggableList } from "./DraggableList";
15
15
  import { Input } from "./Input";
16
+ import { inputRecipe } from "./recipe";
16
17
  import { Select } from "./Select";
17
18
  import { Switch as UiSwitch } from "./Switch";
18
19
  import { ToggleSelect as UtilToggleSelect } from "./ToggleSelect";
19
20
  import { Tooltip } from "./Tooltip";
21
+ import { useUiRecipe } from "./UiOverride";
20
22
 
21
23
  interface LabelProps {
22
24
  className?: string;
@@ -101,6 +103,7 @@ const List = <Item,>({
101
103
  renderItem,
102
104
  }: ListProps<Item>) => {
103
105
  const { l } = usePage();
106
+ const recipe = useUiRecipe("button") ?? buttonRecipe;
104
107
  return (
105
108
  <div className={cn("flex w-full flex-col", className)}>
106
109
  {label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
@@ -111,7 +114,7 @@ const List = <Item,>({
111
114
  {renderItem(item, idx)}
112
115
  <div className="flex gap-2 border-border border-l pl-2">
113
116
  <button
114
- className={buttonRecipe(
117
+ className={recipe(
115
118
  { variant: "outline", size: "icon" },
116
119
  "size-6 border-destructive p-0 text-destructive hover:bg-destructive hover:text-destructive-foreground",
117
120
  )}
@@ -127,7 +130,7 @@ const List = <Item,>({
127
130
  </>
128
131
  ))}
129
132
  <button
130
- className={buttonRecipe({ variant: "outline" })}
133
+ className={recipe({ variant: "outline" })}
131
134
  onClick={() => {
132
135
  onAdd();
133
136
  }}
@@ -533,6 +536,7 @@ const TextList = ({
533
536
  inputClassName,
534
537
  }: TextListProps) => {
535
538
  const { l } = usePage();
539
+ const recipe = useUiRecipe("button") ?? buttonRecipe;
536
540
  return (
537
541
  <div className={cn("flex flex-col", className)}>
538
542
  {label ? <Label className={labelClassName} nullable={!minlength} label={label} desc={desc} /> : null}
@@ -570,7 +574,7 @@ const TextList = ({
570
574
  disabled={disabled}
571
575
  />
572
576
  <button
573
- className={buttonRecipe(
577
+ className={recipe(
574
578
  { variant: "outline", size: "icon" },
575
579
  "size-6 border-destructive p-0 text-destructive hover:bg-destructive hover:text-destructive-foreground",
576
580
  )}
@@ -588,7 +592,7 @@ const TextList = ({
588
592
  <div className="my-5 h-[0.5px] bg-foreground/20" />
589
593
  {value.length <= maxTextlength ? (
590
594
  <button
591
- className={buttonRecipe({ variant: "outline" }, "w-full")}
595
+ className={recipe({ variant: "outline" }, "w-full")}
592
596
  onClick={() => {
593
597
  onChange([...value, ""]);
594
598
  }}
@@ -638,6 +642,7 @@ const Tags = ({
638
642
  inputClassName,
639
643
  }: TagsProps) => {
640
644
  const { l } = usePage();
645
+ const badge = useUiRecipe("badge") ?? badgeRecipe;
641
646
  const [inputVisible, setInputVisible] = useState(false);
642
647
  const [tag, setTag] = useState("");
643
648
  const addTag = () => {
@@ -652,7 +657,7 @@ const Tags = ({
652
657
  {label ? <Label className={labelClassName} nullable={!minlength} label={label} desc={desc} /> : null}
653
658
  <div className="flex w-full flex-wrap items-center gap-1 rounded-md border border-foreground/20 p-2">
654
659
  {value.map((val, idx) => (
655
- <span className={badgeRecipe({ variant: "outline" }, "items-center")} key={idx}>
660
+ <span className={badge({ variant: "outline" }, "items-center")} key={idx}>
656
661
  <div className="text-xs italic">#</div>
657
662
  {val}
658
663
  <BiX
@@ -733,13 +738,16 @@ const Date = <Nullable extends boolean>({
733
738
  return (
734
739
  <div className={cn("flex flex-col", className)}>
735
740
  {label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
736
- {/* //! daysi UI datetime-local 컴포넌트에 max 값 넣으면 오른쪽 끝 짤리는 버그 있음.*/}
741
+ {/* //! datetime-local 에 max 를 주면 오른쪽 끝이 잘리는 렌더 이슈가 있어 max 를 넘기지 않는다.
742
+ 원래 daisyUI 버그로 기록돼 있었으나 daisyUI 는 제거됐다 — 브라우저 네이티브 동작일 수 있어
743
+ 회피책은 유지한다. 실제 렌더 확인 후에만 걷어낼 것. */}
737
744
  <input
738
745
  type={showTime ? "datetime-local" : "date"}
739
- className={cn(
740
- "input validator text-xs outline-none duration-200 focus-within:outline-none focus:outline-none",
746
+ className={inputRecipe({}, [
747
+
748
+ "user-invalid:border-destructive text-xs outline-none duration-200 focus-within:outline-none focus:outline-none",
741
749
  dateClassName,
742
- )}
750
+ ])}
743
751
  min={min ? (showTime ? dayjs(min).format("YYYY-MM-DDTHH:mm") : dayjs(min).format("YYYY-MM-DD")) : undefined}
744
752
  max={max ? (showTime ? dayjs(max).format("YYYY-MM-DDTHH:mm") : dayjs(max).format("YYYY-MM-DD")) : undefined}
745
753
  value={value ? (showTime ? dayjs(value).format("YYYY-MM-DDTHH:mm") : dayjs(value).format("YYYY-MM-DD")) : ""}