akanjs 2.4.2-rc.2 → 3.0.0-alpha.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 (170) hide show
  1. package/base/baseEnv.ts +0 -6
  2. package/client/cn.ts +52 -0
  3. package/client/csrTypes.ts +2 -2
  4. package/client/frameConfig.ts +2 -2
  5. package/client/index.ts +1 -0
  6. package/client/types.ts +0 -4
  7. package/common/fileUpload.ts +1 -1
  8. package/common/index.ts +1 -0
  9. package/common/isThenable.ts +12 -0
  10. package/constant/purify.ts +9 -10
  11. package/fetch/client/fetchClient.ts +1 -1
  12. package/fetch/client/httpClient.ts +9 -5
  13. package/fetch/fetchType/sliceFetch.type.ts +1 -1
  14. package/package.json +4 -7
  15. package/server/akanApp.ts +2 -0
  16. package/server/systemPageDocument.tsx +12 -12
  17. package/signal/endpointInfo.ts +2 -1
  18. package/signal/slice.ts +1 -1
  19. package/signal/sliceInfo.ts +2 -1
  20. package/store/action.ts +18 -2
  21. package/store/baseSt.ts +2 -2
  22. package/store/state.ts +9 -1
  23. package/store/storeInstance.ts +2 -0
  24. package/test/sampleOf.ts +3 -3
  25. package/test/signalTestRuntime.ts +0 -2
  26. package/types/base/baseEnv.d.ts +0 -2
  27. package/types/client/cn.d.ts +12 -0
  28. package/types/client/index.d.ts +1 -0
  29. package/types/client/types.d.ts +0 -3
  30. package/types/common/fileUpload.d.ts +1 -1
  31. package/types/common/index.d.ts +1 -0
  32. package/types/common/isThenable.d.ts +9 -0
  33. package/types/constant/purify.d.ts +3 -2
  34. package/types/fetch/fetchType/sliceFetch.type.d.ts +1 -1
  35. package/types/signal/endpointInfo.d.ts +2 -2
  36. package/types/signal/slice.d.ts +1 -1
  37. package/types/signal/sliceInfo.d.ts +2 -2
  38. package/types/store/action.d.ts +1 -1
  39. package/types/store/state.d.ts +4 -2
  40. package/types/test/sampleOf.d.ts +2 -2
  41. package/types/ui/Badge.d.ts +6 -0
  42. package/types/ui/Button.d.ts +26 -6
  43. package/types/ui/Load/Units.d.ts +3 -1
  44. package/types/ui/RecentTime.d.ts +1 -1
  45. package/types/ui/Signal/Collapse.d.ts +12 -0
  46. package/types/ui/Signal/style.d.ts +5 -6
  47. package/types/ui/Switch.d.ts +14 -0
  48. package/types/ui/Tooltip.d.ts +11 -0
  49. package/types/ui/UiOverride/Provider.d.ts +6 -5
  50. package/types/ui/UiOverride/context.d.ts +22 -1
  51. package/types/ui/UiOverride/index.d.ts +2 -1
  52. package/types/ui/UiOverride/override.d.ts +7 -6
  53. package/types/ui/UiOverride/useUiRecipe.d.ts +8 -0
  54. package/types/ui/index.d.ts +5 -1
  55. package/types/ui/recipe/badgeRecipe.d.ts +13 -0
  56. package/types/ui/recipe/buttonRecipe.d.ts +15 -0
  57. package/types/ui/recipe/factory.d.ts +24 -0
  58. package/types/ui/recipe/index.d.ts +18 -0
  59. package/types/ui/recipe/inputRecipe.d.ts +13 -0
  60. package/types/ui/recipe.d.ts +1 -0
  61. package/ui/Badge.tsx +17 -0
  62. package/ui/BottomSheet.tsx +8 -10
  63. package/ui/Button.tsx +158 -48
  64. package/ui/Clipboard.tsx +3 -3
  65. package/ui/Constant/Doc.tsx +108 -65
  66. package/ui/Constant/Mermaid.tsx +9 -9
  67. package/ui/Data/CardList.tsx +3 -3
  68. package/ui/Data/Dashboard.tsx +22 -13
  69. package/ui/Data/Insight.tsx +6 -6
  70. package/ui/Data/Item.tsx +49 -34
  71. package/ui/Data/ListContainer.tsx +11 -7
  72. package/ui/Data/Pagination.tsx +2 -2
  73. package/ui/DatePicker.tsx +5 -5
  74. package/ui/Dialog/Content.tsx +2 -2
  75. package/ui/Dialog/Modal.tsx +12 -8
  76. package/ui/Dialog/Provider.tsx +2 -2
  77. package/ui/DragAction.tsx +2 -2
  78. package/ui/DraggableList.tsx +10 -5
  79. package/ui/Dropdown.tsx +33 -26
  80. package/ui/Empty.tsx +5 -5
  81. package/ui/Field.tsx +82 -70
  82. package/ui/Image.tsx +2 -2
  83. package/ui/Input.tsx +62 -60
  84. package/ui/Layout/BottomInset.tsx +2 -2
  85. package/ui/Layout/BottomTab.tsx +5 -5
  86. package/ui/Layout/Header.tsx +3 -3
  87. package/ui/Layout/LeftSider.tsx +6 -5
  88. package/ui/Layout/Navbar.tsx +2 -12
  89. package/ui/Layout/RightSider.tsx +10 -8
  90. package/ui/Layout/Sider.tsx +5 -4
  91. package/ui/Layout/Template.tsx +2 -2
  92. package/ui/Layout/TopInset.tsx +2 -2
  93. package/ui/Layout/Unit.tsx +2 -2
  94. package/ui/Layout/View.tsx +2 -2
  95. package/ui/Layout/Zone.tsx +2 -2
  96. package/ui/Link/Back.tsx +2 -2
  97. package/ui/Link/Close.tsx +2 -2
  98. package/ui/Link/CsrLink.tsx +6 -4
  99. package/ui/Link/Lang.tsx +2 -2
  100. package/ui/Link/SsrLink.tsx +6 -5
  101. package/ui/Load/Page.tsx +1 -1
  102. package/ui/Load/Pagination.tsx +2 -2
  103. package/ui/Load/Units.tsx +19 -4
  104. package/ui/Load/View.tsx +2 -2
  105. package/ui/Loading/Area.tsx +4 -2
  106. package/ui/Loading/Button.tsx +2 -2
  107. package/ui/Loading/Input.tsx +2 -2
  108. package/ui/Loading/ProgressBar.tsx +2 -2
  109. package/ui/Loading/Skeleton.tsx +6 -6
  110. package/ui/Loading/Spin.tsx +2 -2
  111. package/ui/Menu.tsx +29 -27
  112. package/ui/Model/Edit.tsx +3 -3
  113. package/ui/Model/EditModal.tsx +5 -5
  114. package/ui/Model/EditWrapper.tsx +2 -2
  115. package/ui/Model/New.tsx +3 -3
  116. package/ui/Model/NewWrapper_Client.tsx +2 -2
  117. package/ui/Model/Remove.tsx +5 -4
  118. package/ui/Model/RemoveWrapper.tsx +2 -2
  119. package/ui/Model/SureToRemove.tsx +7 -6
  120. package/ui/Model/View.tsx +2 -2
  121. package/ui/Model/ViewEditModal.tsx +20 -21
  122. package/ui/Model/ViewWrapper.tsx +2 -2
  123. package/ui/More.tsx +2 -2
  124. package/ui/ObjectId.tsx +9 -3
  125. package/ui/Pagination.tsx +14 -11
  126. package/ui/Popconfirm.tsx +20 -11
  127. package/ui/Radio.tsx +4 -4
  128. package/ui/RecentTime.tsx +30 -83
  129. package/ui/Refresh.tsx +1 -1
  130. package/ui/ScreenNavigator.tsx +2 -2
  131. package/ui/Select.tsx +21 -23
  132. package/ui/Signal/Arg.tsx +21 -18
  133. package/ui/Signal/Collapse.tsx +29 -0
  134. package/ui/Signal/Doc.tsx +27 -22
  135. package/ui/Signal/Listener.tsx +3 -3
  136. package/ui/Signal/Message.tsx +31 -17
  137. package/ui/Signal/Object.tsx +13 -8
  138. package/ui/Signal/PubSub.tsx +26 -16
  139. package/ui/Signal/Request.tsx +3 -2
  140. package/ui/Signal/Response.tsx +5 -4
  141. package/ui/Signal/RestApi.tsx +27 -17
  142. package/ui/Signal/style.ts +16 -16
  143. package/ui/Switch.tsx +62 -0
  144. package/ui/System/CSR.tsx +40 -35
  145. package/ui/System/Client.tsx +2 -2
  146. package/ui/System/DevModeToggle.tsx +10 -12
  147. package/ui/System/Messages.tsx +14 -16
  148. package/ui/System/Reconnect.tsx +7 -7
  149. package/ui/System/SSR.tsx +25 -21
  150. package/ui/System/SelectLanguage.tsx +24 -28
  151. package/ui/System/ThemeToggle.tsx +38 -45
  152. package/ui/Tab/Menu.tsx +20 -18
  153. package/ui/Tab/Panel.tsx +3 -3
  154. package/ui/Tab/Provider.tsx +2 -2
  155. package/ui/Table.tsx +11 -8
  156. package/ui/ToggleSelect.tsx +14 -18
  157. package/ui/Tooltip.tsx +55 -0
  158. package/ui/UiOverride/Provider.tsx +10 -6
  159. package/ui/UiOverride/context.ts +22 -2
  160. package/ui/UiOverride/index.ts +8 -1
  161. package/ui/UiOverride/override.ts +7 -6
  162. package/ui/UiOverride/useUiRecipe.ts +15 -0
  163. package/ui/Unauthorized.tsx +5 -5
  164. package/ui/index.ts +16 -0
  165. package/ui/recipe/badgeRecipe.ts +23 -0
  166. package/ui/recipe/buttonRecipe.ts +32 -0
  167. package/ui/recipe/factory.ts +32 -0
  168. package/ui/recipe/index.ts +18 -0
  169. package/ui/recipe/inputRecipe.ts +13 -0
  170. package/ui/styles.css +150 -21
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Whether a value is awaitable. Duck-typed rather than `instanceof Promise` so a thenable produced by a
3
+ * different realm or promise implementation still counts — a same-realm check silently misses those.
4
+ *
5
+ * `<Button>` uses this to decide whether a click handler is asynchronous: a handler that returns a
6
+ * thenable gets the pending/success states, a plain one renders as an ordinary button. That makes the
7
+ * distinction impossible to forget, unlike an explicit flag.
8
+ */
9
+ export declare const isThenable: (value: unknown) => value is PromiseLike<unknown>;
@@ -1,4 +1,4 @@
1
- import { type Dayjs, type GetStateObject, type Upload } from "akanjs/base";
1
+ import { type Dayjs, type GetStateObject } from "akanjs/base";
2
2
  import { type BaseObject, type ConstantModelRef, type DefaultOf, type DefaultOfSchema } from ".";
3
3
  type Purified<O> = O extends BaseObject ? string : O extends BaseObject[] ? string[] : O extends Dayjs ? Dayjs : O extends object ? PurifiedModel<O> : O;
4
4
  type PurifiedWithObjectToId<T, StateKeys extends keyof GetStateObject<T> = keyof GetStateObject<T>> = {
@@ -6,7 +6,8 @@ type PurifiedWithObjectToId<T, StateKeys extends keyof GetStateObject<T> = keyof
6
6
  } & {
7
7
  [K in StateKeys as null extends T[K] ? K : never]?: Purified<T[K]> | undefined;
8
8
  };
9
- export type PurifiedModel<T> = T extends Upload[] ? FileList : T extends (infer S)[] ? PurifiedModel<S>[] : T extends string | number | boolean | Dayjs | File ? T : T extends Map<infer K, infer V> ? Map<K, PurifiedModel<V>> : PurifiedWithObjectToId<T>;
9
+ export type PurifiedModel<T> = T extends (infer S)[] ? PurifiedModel<S>[] : T extends string | number | boolean | Dayjs | File ? T : T extends Map<infer K, infer V> ? Map<K, PurifiedModel<V>> : PurifiedWithObjectToId<T>;
10
+ export type UploadableClientArg<T> = [T] extends [File[]] ? File[] | FileList : T;
10
11
  export type PurifyFunc<Input, _DefaultInput = DefaultOf<Input>, _PurifiedInput = PurifiedModel<Input>> = (self: _DefaultInput, isChild?: boolean) => _PurifiedInput | null;
11
12
  export type PurifyFuncV2<Input, RelationKey extends string = never, _DefaultInput = DefaultOfSchema<Input, RelationKey>, _PurifiedInput = PurifiedModel<Input>> = (self: _DefaultInput, isChild?: boolean) => _PurifiedInput | null;
12
13
  export declare const makePurify: <I>(modelRef: ConstantModelRef<I>) => PurifyFunc<I>;
@@ -60,7 +60,7 @@ type AppliedBaseSliceFetchType<RefName extends string, Input, Full, _Capitalized
60
60
  } & {
61
61
  [K in `get${_CapitalizedRefName}Edit`]: (id: string, option?: FetchPolicy) => Promise<ServerEdit<RefName, Full>>;
62
62
  } & {
63
- [K in `add${_CapitalizedRefName}Files`]: (fileList: FileList, parentId?: string, option?: FetchPolicy) => Promise<ProtoFile[]>;
63
+ [K in `add${_CapitalizedRefName}Files`]: (fileList: FileList | File[], parentId?: string, option?: FetchPolicy) => Promise<ProtoFile[]>;
64
64
  } & {
65
65
  [K in `merge${_CapitalizedRefName}`]: (modelOrId: Full | string, data: Partial<_PurifiedInput>, option?: FetchPolicy) => Promise<Full>;
66
66
  };
@@ -1,5 +1,5 @@
1
1
  import { type Dayjs, type EnumInstance, type PromiseOrObject } from "akanjs/base";
2
- import type { ConstantFieldType, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel } from "akanjs/constant";
2
+ import type { ConstantFieldType, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel, UploadableClientArg } from "akanjs/constant";
3
3
  import type { ServiceModel } from "akanjs/service";
4
4
  import type { InternalArgCls } from "./internalArg.d.ts";
5
5
  import type { ArgType, SignalOption, SrvMap } from "./types.d.ts";
@@ -45,7 +45,7 @@ export declare class EndpointInfo<ReqType extends EndpointType = EndpointType, S
45
45
  static getReturnInfo<Returns extends ConstantFieldTypeInput = ConstantFieldTypeInput, Nullable extends boolean = false>(modelRef: Returns, signalOption?: SignalOption<Returns, Nullable>): ReturnInfo<Returns, Nullable>;
46
46
  constructor(type: ReqType, returnRef: Returns, signalOption?: SignalOption<Returns, Nullable>);
47
47
  param<ArgName extends string, Arg extends ParamFieldType, _ClientArg = FieldToValue<Arg>, _ServerArg = DocumentModel<_ClientArg>>(name: string, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg], Returns, ClientReturns, ServerReturns, Nullable>;
48
- body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)], Returns, ClientReturns, ServerReturns, Nullable>;
48
+ body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = UploadableClientArg<PurifiedModel<_ArgType>>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)], Returns, ClientReturns, ServerReturns, Nullable>;
49
49
  room<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: string, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg], Returns, ClientReturns, ServerReturns, Nullable>;
50
50
  msg<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: string, arg: Arg, option?: EndpointArgProps<Optional>): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)], Returns, ClientReturns, ServerReturns, Nullable>;
51
51
  search<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: string, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], [...Args, arg?: _ClientArg | null], InternalArgs, [...ServerArgs, arg: _ServerArg | undefined], Returns, ClientReturns, ServerReturns, Nullable>;
@@ -51,7 +51,7 @@ export declare function slice<SrvModule extends ServiceModel, BuildSlice extends
51
51
  [""]: SliceInfo<SrvRefName<SrvModule>, _Input, _Full, _Light, _Insight, _Filter, SrvMap<SrvModule>, [
52
52
  "query"
53
53
  ], [
54
- _Query
54
+ query?: _Query | null
55
55
  ], [
56
56
  ], [
57
57
  _Query
@@ -1,5 +1,5 @@
1
1
  import type { Cls, PromiseOrObject } from "akanjs/base";
2
- import type { BaseInsight, BaseObject, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel, QueryOf } from "akanjs/constant";
2
+ import type { BaseInsight, BaseObject, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel, QueryOf, UploadableClientArg } from "akanjs/constant";
3
3
  import type { FilterCls, FilterInstance } from "akanjs/document";
4
4
  import type { ServiceModel } from "akanjs/service";
5
5
  import { type ArgInfo, type EndpointArgProps, type InternalArgInfo, type InternalArgProps } from "./endpointInfo.d.ts";
@@ -23,7 +23,7 @@ export declare class SliceInfo<RefName extends string = string, Input = any, Ful
23
23
  execFn: ((...args: [...ServerArgs, ...InternalArgs]) => QueryOf<DocumentModel<Full>>) | null;
24
24
  constructor(refName: RefName, input: Cls<Input>, full: Cls<Full>, light: Cls<Light>, insight: Cls<Insight>, filter: FilterCls<Filter>, signalOption?: SignalOption);
25
25
  param<ArgName extends string, Arg extends ParamFieldType, _ClientArg = FieldToValue<Arg>, _ServerArg = DocumentModel<_ClientArg>>(name: ArgName, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg]>;
26
- body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)]>;
26
+ body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = UploadableClientArg<PurifiedModel<_ArgType>>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)]>;
27
27
  search<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], [...Args, arg?: _ClientArg | null], InternalArgs, [...ServerArgs, arg: _ServerArg | undefined]>;
28
28
  with<ArgType, Optional extends boolean = false>(argRef: InternalArgCls<ArgType>, option?: InternalArgProps<Optional>): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, ArgNames, Args, [...InternalArgs, arg: NonNullable<ArgType> | (Optional extends true ? null : never)], ServerArgs>;
29
29
  exec(query: (this: {
@@ -111,7 +111,7 @@ type ArrayFieldAddOrSubSetters<RefName extends string, _CapRef extends string, _
111
111
  }) => void;
112
112
  };
113
113
  type FormSetter<Full, RefName extends string, _CapitalizedRefName extends string = Capitalize<RefName>, _DefaultState = DefaultOf<Full>> = FieldFormSetter<_DefaultState, RefName, _CapitalizedRefName> & ArrayFieldAddSetters<RefName, _CapitalizedRefName, _DefaultState> & ArrayFieldSubSetters<RefName, _CapitalizedRefName, _DefaultState> & ArrayFieldAddOrSubSetters<RefName, _CapitalizedRefName, _DefaultState> & {
114
- [K in keyof _DefaultState as _DefaultState[K] extends (ProtoFile | null) | ProtoFile[] ? K extends string ? SetterKey<"upload", K, RefName, _CapitalizedRefName> : never : never]: (fileList: FileList, idx?: number) => Promise<void>;
114
+ [K in keyof _DefaultState as _DefaultState[K] extends (ProtoFile | null) | ProtoFile[] ? K extends string ? SetterKey<"upload", K, RefName, _CapitalizedRefName> : never : never]: (fileList: FileList | File[], idx?: number) => Promise<void>;
115
115
  } & {
116
116
  [K in `writeOn${_CapitalizedRefName}`]: (path: string | (string | number)[], value: any) => void;
117
117
  };
@@ -3,7 +3,7 @@ import { type DefaultOf } from "akanjs/constant";
3
3
  import type { ExtractSort, FilterInstance } from "akanjs/document";
4
4
  import type { SerializedSlice, SlceCnstCapitalizedRefName, SlceCnstDefault, SlceCnstFull, SlceCnstInsight, SlceCnstLight, SlceCnstRefName, SlceDbFilter, SlceDbSort, SliceCls } from "akanjs/signal";
5
5
  import type { StoreSliceArgs, StoreSliceMap, StoreSliceSuffixCap, Submit } from "./types.d.ts";
6
- export type SliceStateKey = "defaultModel" | "modelInsight" | "modelList" | "modelListLoading" | "modelInitList" | "modelInitAt" | "modelSelection" | "lastPageOfModel" | "pageOfModel" | "limitOfModel" | "queryArgsOfModel" | "sortOfModel";
6
+ export type SliceStateKey = "defaultModel" | "modelInsight" | "modelList" | "modelListLoading" | "modelInitList" | "modelInitAt" | "modelStaleAt" | "modelSelection" | "lastPageOfModel" | "pageOfModel" | "limitOfModel" | "queryArgsOfModel" | "sortOfModel";
7
7
  type _SliceMap<S extends SliceCls> = StoreSliceMap<S>;
8
8
  type _StateRefName<S extends SliceCls> = SlceCnstRefName<S>;
9
9
  type _StateCap<S extends SliceCls> = SlceCnstCapitalizedRefName<S>;
@@ -42,6 +42,8 @@ export type SliceState<RefName extends string, Suffix extends string, Full, Ligh
42
42
  [K in `${RefName}InitList${_CapitalizedSuffix}`]: DataList<Light>;
43
43
  } & {
44
44
  [K in `${RefName}InitAt${_CapitalizedSuffix}`]: Date;
45
+ } & {
46
+ [K in `${RefName}StaleAt${_CapitalizedSuffix}`]: Date;
45
47
  } & {
46
48
  [K in `${RefName}Selection${_CapitalizedSuffix}`]: DataList<Light>;
47
49
  } & {
@@ -64,7 +66,7 @@ type DefaultSliceStateFields<SlceCls extends SliceCls, _RefName extends string,
64
66
  } & {
65
67
  [Suffix in _Suffixes as `${_RefName}List${StoreSliceSuffixCap<SlceCls, Suffix>}` | `${_RefName}InitList${StoreSliceSuffixCap<SlceCls, Suffix>}` | `${_RefName}Selection${StoreSliceSuffixCap<SlceCls, Suffix>}`]: DataList<_Light>;
66
68
  } & {
67
- [Suffix in _Suffixes as `${_RefName}InitAt${StoreSliceSuffixCap<SlceCls, Suffix>}`]: Date;
69
+ [Suffix in _Suffixes as `${_RefName}InitAt${StoreSliceSuffixCap<SlceCls, Suffix>}` | `${_RefName}StaleAt${StoreSliceSuffixCap<SlceCls, Suffix>}`]: Date;
68
70
  } & {
69
71
  [Suffix in _Suffixes as `${_RefName}ListLoading${StoreSliceSuffixCap<SlceCls, Suffix>}`]: boolean;
70
72
  } & {
@@ -1,5 +1,5 @@
1
- import type { BaseObject, ConstantCls, DocumentModel } from "akanjs/constant";
1
+ import type { BaseObject, ConstantCls, DocumentModel, FieldObject } from "akanjs/constant";
2
2
  export type SampleOf<Model> = DocumentModel<{
3
3
  [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>;
4
4
  }>;
5
- export declare const sampleOf: <Model>(modelRef: ConstantCls<Model>) => DocumentModel<{ [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>; }>;
5
+ export declare const sampleOf: <Model, FieldObj extends FieldObject>(modelRef: ConstantCls<Model, FieldObj>) => DocumentModel<{ [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>; }>;
@@ -0,0 +1,6 @@
1
+ import type { HTMLAttributes } from "react";
2
+ import { type BadgeVariants, badgeRecipe } from "./recipe.d.ts";
3
+ export { badgeRecipe, type BadgeVariants };
4
+ export type BadgeProps = HTMLAttributes<HTMLSpanElement> & BadgeVariants;
5
+ /** Status/label pill. Route-overridable via `page/**\/_overrides.tsx` (slot `Badge`). */
6
+ export declare const Badge: import("react").ComponentType<BadgeProps>;
@@ -1,16 +1,36 @@
1
1
  import type React from "react";
2
2
  import { type ButtonHTMLAttributes } from "react";
3
- export type ButtonProps<Result> = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> & {
4
- /** Async-aware click handler. Call onError to show the localized error state without throwing. */
5
- onClick: (e: React.MouseEvent<HTMLButtonElement>, { onError }: {
3
+ import { type ButtonVariants, buttonRecipe } from "./recipe.d.ts";
4
+ export { buttonRecipe, type ButtonVariants };
5
+ export type ButtonProps<Result> = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> & ButtonVariants & {
6
+ /**
7
+ * Click handler. Returning a promise is what opts this button into the async states — a plain
8
+ * synchronous handler renders as an ordinary button with no spinner. Call onError to show the
9
+ * localized error state without throwing.
10
+ */
11
+ onClick?: (e: React.MouseEvent<HTMLButtonElement>, { onError }: {
6
12
  onError: (error: string) => void;
7
13
  }) => Promise<Result> | Result;
8
14
  /** Called after the button briefly enters success state. */
9
15
  onSuccess?: (result: Result) => void;
16
+ /**
17
+ * How the pending/success indicator is drawn. Both modes keep the button's box fixed — CSS cannot
18
+ * animate an auto width, so a resizing button can only ever snap.
19
+ *
20
+ * `hold` (default) fades a bare indicator over the children, so the box stays exactly the children's
21
+ * size. `replace` cross-fades to a labelled indicator ("Processing…"); both labels stay stacked in the
22
+ * DOM, so the box is the wider of the two from the start and still never moves — at the cost of an idle
23
+ * button that is as wide as its longest state.
24
+ */
25
+ loadingMode?: "hold" | "replace";
26
+ /** Whether a failed action renders its message under the button. Off leaves it to the framework toast. */
27
+ showError?: boolean;
10
28
  };
11
29
  /**
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.
30
+ * Button whose async states are driven by the handler's return value: return a promise and it shows a
31
+ * pending indicator, then a brief success state; return nothing and it behaves as a plain button. Resolves
32
+ * to a route-scoped override when a `page/**\/_overrides.tsx` in the route's ancestry declares one,
33
+ * otherwise renders {@link DefaultButton}. The public generic signature is preserved, so
34
+ * `<Button<Todo> onSuccess={(r: Todo) => …} />` still infers.
15
35
  */
16
36
  export declare const Button: <Result = unknown>(props: ButtonProps<Result>) => React.ReactElement<ButtonProps<Result>, string | React.JSXElementConstructor<any>>;
@@ -18,6 +18,8 @@ interface DefaultProps<L extends {
18
18
  renderList?: (list: DataList<L>) => ReactNode;
19
19
  reverse?: boolean;
20
20
  pagination?: boolean;
21
+ /** Max age in ms of the cached slice data before the client refetches on mount; `0` always refetches. */
22
+ staleTime?: number;
21
23
  }
22
24
  interface UnitsProps<RefName extends string, Light extends {
23
25
  id: string;
@@ -26,5 +28,5 @@ interface UnitsProps<RefName extends string, Light extends {
26
28
  }
27
29
  export default function Units<RefName extends string, Light extends {
28
30
  id: string;
29
- }>({ containerRef, className, init, noDiv, from, to, loading, renderItem, renderList, renderEmpty, filter, sort, reverse, style, pagination, }: UnitsProps<RefName, Light>): import("react/jsx-runtime").JSX.Element;
31
+ }>({ containerRef, className, init, noDiv, from, to, loading, renderItem, renderList, renderEmpty, filter, sort, reverse, style, pagination, staleTime, }: UnitsProps<RefName, Light>): import("react/jsx-runtime").JSX.Element;
30
32
  export {};
@@ -6,7 +6,7 @@ export interface RecentTimeProps {
6
6
  breakUnit?: Intl.RelativeTimeFormatUnit;
7
7
  /** Use compact automatic formatting or always include date and time. */
8
8
  format?: "full" | "auto";
9
- /** Additional classes for the tooltip span. */
9
+ /** Additional classes for the trigger span. */
10
10
  className?: string;
11
11
  }
12
12
  export declare const RecentTime: ({ date, breakUnit, format, className }: RecentTimeProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,12 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Signal 도큐먼트/테스트 카드의 접이식 컨테이너.
4
+ * daisyui `collapse`(checkbox+CSS) → 네이티브 `<details>/<summary>`. 플러그인 의존 없음.
5
+ */
6
+ export declare const SignalCollapse: ({ summary, children, open, className, contentClassName, }: {
7
+ summary: ReactNode;
8
+ children: ReactNode;
9
+ open?: boolean;
10
+ className?: string;
11
+ contentClassName?: string;
12
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -2,14 +2,13 @@ export declare const signalUi: {
2
2
  sectionTitle: string;
3
3
  sectionDescription: string;
4
4
  sectionPanel: string;
5
- endpointCard: string;
6
- endpointContent: string;
7
5
  tablePanel: string;
6
+ tableClass: string;
8
7
  inputRow: string;
9
8
  inputLabel: string;
10
9
  codePanel: string;
11
10
  };
12
- export declare const getEndpointBadgeClassName: (type: string) => "badge badge-primary" | "badge badge-secondary";
13
- export declare const getGuardBadgeClassName: (guard: string) => "badge badge-primary" | "badge badge-secondary" | "badge";
14
- export declare const getStatusBadgeClassName: (status: string) => "badge badge-outline" | "badge badge-primary" | "badge badge-error";
15
- export declare const getStatusTextareaClassName: (status: string) => "" | "border-error text-error" | "border-primary" | "textarea-disabled";
11
+ export declare const getEndpointBadgeClassName: (type: string) => string;
12
+ export declare const getGuardBadgeClassName: (guard: string) => string;
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";
@@ -0,0 +1,14 @@
1
+ export interface SwitchProps {
2
+ checked?: boolean;
3
+ defaultChecked?: boolean;
4
+ disabled?: boolean;
5
+ onChange?: (checked: boolean) => void;
6
+ className?: string;
7
+ /** 켜짐 상태 색. daisyui `toggle-accent/toggle-primary` 대체. */
8
+ variant?: "primary" | "accent" | "success";
9
+ }
10
+ /**
11
+ * 순수 Tailwind 스위치 (`<button role="switch">`). daisyui `toggle`/`swap` 대체.
12
+ * `<button>` 이라 포커스·Space/Enter 토글이 네이티브로 제공된다. controlled/uncontrolled 모두 지원.
13
+ */
14
+ export declare const Switch: ({ checked, defaultChecked, disabled, onChange, className, variant, }: SwitchProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from "react";
2
+ export interface TooltipProps {
3
+ /** 툴팁에 표시할 내용. 비어 있으면 트리거만 그대로 렌더한다. */
4
+ content?: ReactNode;
5
+ children: ReactNode;
6
+ side?: "top" | "right" | "bottom" | "left";
7
+ className?: string;
8
+ /** 색상 변형. daisyui `tooltip-primary/info` 대체. */
9
+ variant?: "default" | "primary" | "info";
10
+ }
11
+ export declare const Tooltip: import("react").ComponentType<TooltipProps>;
@@ -1,14 +1,15 @@
1
1
  import { type ReactNode } from "react";
2
- import { type AkanUiOverrides } from "./context.d.ts";
2
+ import { type AkanUiOverrideManifest } from "./context.d.ts";
3
3
  export interface UiOverrideProviderProps {
4
- /** Override map for this subtree; merged over any ancestor overrides. */
5
- value?: Partial<AkanUiOverrides>;
4
+ /** Override manifest for this subtree; merged over any ancestor overrides. */
5
+ value?: AkanUiOverrideManifest;
6
6
  children?: ReactNode;
7
7
  }
8
8
  /**
9
9
  * Supplies route-scoped UI overrides. Merges its own `value` over the overrides
10
10
  * inherited from ancestors so the closest declaration wins, matching nested
11
- * `_overrides.tsx` resolution. `routeTreeBuilder` mounts one of these per route
12
- * node once the `_overrides.tsx` convention is wired.
11
+ * `_overrides.tsx` resolution. The `recipes` map merges per-slot (a child that
12
+ * swaps only `button` keeps an ancestor's `badge` swap). `routeTreeBuilder`
13
+ * mounts one of these per route node once the `_overrides.tsx` convention is wired.
13
14
  */
14
15
  export declare const UiOverrideProvider: ({ value, children }: UiOverrideProviderProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,6 @@
1
1
  import { type ComponentType } from "react";
2
+ import type { ClassNameValue as ClassValue } from "tailwind-merge";
3
+ import type { BadgeProps } from "../Badge.d.ts";
2
4
  import type { ButtonProps } from "../Button.d.ts";
3
5
  import type { DatePickerProps, RangePickerProps, TimePickerProps } from "../DatePicker.d.ts";
4
6
  import type { DropdownProps } from "../Dropdown.d.ts";
@@ -14,9 +16,11 @@ import type { ModalProps } from "../Modal.d.ts";
14
16
  import type { PaginationProps } from "../Pagination.d.ts";
15
17
  import type { PopconfirmProps } from "../Popconfirm.d.ts";
16
18
  import type { ItemProps as RadioItemProps, RadioProps } from "../Radio.d.ts";
19
+ import type { BadgeVariants, ButtonVariants } from "../recipe.d.ts";
17
20
  import type { SelectProps } from "../Select.d.ts";
18
21
  import type { TableProps } from "../Table.d.ts";
19
22
  import type { MultiProps as ToggleSelectMultiProps, ToggleSelectProps } from "../ToggleSelect.d.ts";
23
+ import type { TooltipProps } from "../Tooltip.d.ts";
20
24
  import type { UnauthorizedProps } from "../Unauthorized.d.ts";
21
25
  /**
22
26
  * Registry of framework UI components an app may replace per-route through a
@@ -31,6 +35,7 @@ import type { UnauthorizedProps } from "../Unauthorized.d.ts";
31
35
  * before they can be listed here without losing call-site typing.
32
36
  */
33
37
  export interface AkanUiOverrides {
38
+ Badge: ComponentType<BadgeProps>;
34
39
  Modal: ComponentType<ModalProps>;
35
40
  Empty: ComponentType<EmptyProps>;
36
41
  Pagination: ComponentType<PaginationProps>;
@@ -38,6 +43,7 @@ export interface AkanUiOverrides {
38
43
  Dropdown: ComponentType<DropdownProps>;
39
44
  Table: ComponentType<TableProps>;
40
45
  Menu: ComponentType<MenuProps>;
46
+ Tooltip: ComponentType<TooltipProps>;
41
47
  Unauthorized: ComponentType<UnauthorizedProps>;
42
48
  Button: ComponentType<ButtonProps<unknown>>;
43
49
  Select: ComponentType<SelectProps<string | number | boolean | null | undefined>>;
@@ -64,9 +70,24 @@ export interface AkanUiOverrides {
64
70
  export type AkanUiOverrideName = keyof AkanUiOverrides;
65
71
  /** Prop contract an app-authored Modal override must satisfy. */
66
72
  export type AkanModalComponent = AkanUiOverrides["Modal"];
73
+ /**
74
+ * Registry of framework recipe slots an app may swap through the same `_overrides.tsx`
75
+ * manifest: `override({ recipes: { button: neonButtonRecipe } })`. A recipe swap changes
76
+ * only the className factory — the component's structure/behavior (async states, focus,
77
+ * a11y) is untouched. Each replacement must accept the framework recipe's full variant
78
+ * contract, so every call site keeps working.
79
+ */
80
+ export interface AkanUiRecipes {
81
+ button: (variants?: ButtonVariants, className?: ClassValue) => string;
82
+ badge: (variants?: BadgeVariants, className?: ClassValue) => string;
83
+ }
84
+ /** Shape of an `_overrides.tsx` manifest: component slots plus an optional recipe-slot map. */
85
+ export type AkanUiOverrideManifest = Partial<AkanUiOverrides> & {
86
+ recipes?: Partial<AkanUiRecipes>;
87
+ };
67
88
  /**
68
89
  * Holds the override map for the current route subtree. Empty at the root, then
69
90
  * merged (child wins) by each nested `UiOverrideProvider`, mirroring how nested
70
91
  * `_layout.tsx` / `_overrides.tsx` stack down the route tree.
71
92
  */
72
- export declare const UiOverrideContext: import("react").Context<Partial<AkanUiOverrides>>;
93
+ export declare const UiOverrideContext: import("react").Context<AkanUiOverrideManifest>;
@@ -1,5 +1,6 @@
1
- export type { AkanModalComponent, AkanUiOverrideName, AkanUiOverrides } from "./context.d.ts";
1
+ export type { AkanModalComponent, AkanUiOverrideManifest, AkanUiOverrideName, AkanUiOverrides, AkanUiRecipes, } from "./context.d.ts";
2
2
  export { createOverridable } from "./createOverridable.d.ts";
3
3
  export { override } from "./override.d.ts";
4
4
  export { UiOverrideProvider, type UiOverrideProviderProps } from "./Provider.d.ts";
5
5
  export { useUiOverride } from "./useUiOverride.d.ts";
6
+ export { useUiRecipe } from "./useUiRecipe.d.ts";
@@ -1,18 +1,19 @@
1
- import type { AkanUiOverrides } from "./context.d.ts";
1
+ import type { AkanUiOverrideManifest } from "./context.d.ts";
2
2
  /**
3
3
  * Typed manifest builder for a `page/_overrides.tsx` file:
4
4
  *
5
5
  * ```tsx
6
- * export default override({ Modal: BrandModal });
6
+ * export default override({ Modal: BrandModal, recipes: { button: neonButtonRecipe } });
7
7
  * ```
8
8
  *
9
- * It type-checks each binding against that slot's public component contract and rejects unknown slot names as
9
+ * It type-checks each binding against that slot's public contract and rejects unknown slot names as
10
10
  * excess properties — so the manifest is validated at compile time and app components need no annotation of
11
- * their own. Keys are the framework component names (`Modal`), matching `useUiOverride` and the `<Modal>` call
12
- * sites.
11
+ * their own. Component keys are the framework component names (`Modal`), matching `useUiOverride` and the
12
+ * `<Modal>` call sites; the optional `recipes` map swaps recipe slots (look only, behavior untouched),
13
+ * matching `useUiRecipe` and the recipe names in `AkanUiRecipes`.
13
14
  *
14
15
  * It is a pure, server-safe identity function (no React, no `createContext`), so the manifest needs no
15
16
  * `"use client"` directive. The framework generates a `"use client"` wrapper layout that reads this map and
16
17
  * mounts the override provider around the route subtree (see `writeGeneratedOverridesLayoutFile`).
17
18
  */
18
- export declare const override: (overrides: Partial<AkanUiOverrides>) => Partial<AkanUiOverrides>;
19
+ export declare const override: (overrides: AkanUiOverrideManifest) => AkanUiOverrideManifest;
@@ -0,0 +1,8 @@
1
+ import { type AkanUiRecipes } from "./context.d.ts";
2
+ /**
3
+ * Resolves the active recipe swap for a framework recipe slot in the current route
4
+ * subtree, or `undefined` when no `_overrides.tsx` in the ancestry declares one.
5
+ * Framework components consume it as `const recipe = useUiRecipe("button") ?? buttonRecipe;`
6
+ * so an app can restyle every instance app/route-wide without touching behavior.
7
+ */
8
+ export declare const useUiRecipe: <K extends keyof AkanUiRecipes>(name: K) => AkanUiRecipes[K] | undefined;
@@ -1,4 +1,5 @@
1
1
  export { animated } from "./animated.d.ts";
2
+ export { Badge } from "./Badge.d.ts";
2
3
  export { BottomSheet, type BottomSheetRef } from "./BottomSheet.d.ts";
3
4
  export { Button } from "./Button.d.ts";
4
5
  export { ClientSide } from "./ClientSide.d.ts";
@@ -34,12 +35,15 @@ export { Portal } from "./Portal.d.ts";
34
35
  export { Radio } from "./Radio.d.ts";
35
36
  export { RecentTime } from "./RecentTime.d.ts";
36
37
  export { Refresh } from "./Refresh.d.ts";
38
+ export { type BadgeVariants, type ButtonVariants, badgeRecipe, buttonRecipe, type InputSurfaceVariants, inputRecipe, recipe, tv, } from "./recipe.d.ts";
37
39
  export { ScreenNavigator } from "./ScreenNavigator.d.ts";
38
40
  export { Select } from "./Select.d.ts";
39
41
  export { Signal } from "./Signal.d.ts";
42
+ export { Switch, type SwitchProps } from "./Switch.d.ts";
40
43
  export { System, type WebAppManifest } from "./System.d.ts";
41
44
  export { Tab } from "./Tab.d.ts";
42
45
  export { Table } from "./Table.d.ts";
43
46
  export { ToggleSelect } from "./ToggleSelect.d.ts";
44
- export { type AkanModalComponent, type AkanUiOverrideName, type AkanUiOverrides, createOverridable, override, UiOverrideProvider, type UiOverrideProviderProps, useUiOverride, } from "./UiOverride.d.ts";
47
+ export { Tooltip, type TooltipProps } from "./Tooltip.d.ts";
48
+ export { type AkanModalComponent, type AkanUiOverrideManifest, type AkanUiOverrideName, type AkanUiOverrides, type AkanUiRecipes, createOverridable, override, UiOverrideProvider, type UiOverrideProviderProps, useUiOverride, useUiRecipe, } from "./UiOverride.d.ts";
45
49
  export { Unauthorized } from "./Unauthorized.d.ts";
@@ -0,0 +1,13 @@
1
+ /** 뱃지 look — 시맨틱 variant. `<Badge>` 가 소비하며, recipes.badge 슬롯으로 교체 가능. */
2
+ export declare const badgeRecipe: (variants?: Omit<({
3
+ variant?: "default" | "error" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "outline" | undefined;
4
+ } & {
5
+ class?: import("tailwind-variants").ClassValue;
6
+ className?: never;
7
+ }) | ({
8
+ variant?: "default" | "error" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "outline" | undefined;
9
+ } & {
10
+ class?: never;
11
+ className?: import("tailwind-variants").ClassValue;
12
+ }), "class" | "className"> | undefined, className?: import("tailwind-merge").ClassNameValue) => string;
13
+ export type BadgeVariants = NonNullable<Parameters<typeof badgeRecipe>[0]>;
@@ -0,0 +1,15 @@
1
+ /** 버튼 look — 시맨틱 variant × size. `<Button>` 이 소비하며, `_overrides.tsx` 의 recipes.button 슬롯으로 교체 가능. */
2
+ export declare const buttonRecipe: (variants?: Omit<({
3
+ size?: "lg" | "md" | "sm" | "xs" | "icon" | undefined;
4
+ variant?: "link" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "destructive" | "outline" | "ghost" | undefined;
5
+ } & {
6
+ class?: import("tailwind-variants").ClassValue;
7
+ className?: never;
8
+ }) | ({
9
+ size?: "lg" | "md" | "sm" | "xs" | "icon" | undefined;
10
+ variant?: "link" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "destructive" | "outline" | "ghost" | undefined;
11
+ } & {
12
+ class?: never;
13
+ className?: import("tailwind-variants").ClassValue;
14
+ }), "class" | "className"> | undefined, className?: import("tailwind-merge").ClassNameValue) => string;
15
+ export type ButtonVariants = NonNullable<Parameters<typeof buttonRecipe>[0]>;
@@ -0,0 +1,24 @@
1
+ import type { ClassNameValue as ClassValue } from "tailwind-merge";
2
+ /**
3
+ * recipe 팩토리 모듈 — 변형 팩토리를 만드는 **인프라**. **서버-안전** (절대 `"use client"` 금지).
4
+ * 실제 recipe 정의는 이 폴더의 형제 파일 하나당 하나씩 둔다 — 프레임워크는 `./<name>Recipe.ts`,
5
+ * 각 앱은 `apps/<app>/ui/Recipe/<name>.ts`.
6
+ */
7
+ export declare const tv: import("tailwind-variants").TV;
8
+ /**
9
+ * recipe 팩토리 — `tv({...})` 인스턴스를 받아 겉을 `(변형, className?)` 2-인자로 노출하고,
10
+ * className 병합(cn)을 안으로 숨긴다. 타입은 넘겨받은 tv 인스턴스에서 그대로 추론되어 변형 타입이
11
+ * 정확히 보존된다. 반환은 순수 className 문자열이라 서버-안전.
12
+ *
13
+ * export const xRecipe = recipe(tv({ base, variants }));
14
+ * xRecipe({ variant: "primary" }, "w-full") // cn 불필요
15
+ *
16
+ * 2번째 인자는 `ClassNameValue` — 문자열 하나만이 아니라 **배열·중첩 배열·falsy 값**을 그대로 받는다.
17
+ * 여러 클래스를 합칠 때 `cn(...)` 으로 감쌀 이유가 없다(모르면 감싸게 되는데, cn 을 몰아내려고 만든
18
+ * 2-인자 계약이 거기서 새어나간다):
19
+ *
20
+ * xRecipe({}, ["h-full", className]) // ✅ cn 불필요
21
+ * xRecipe({}, ["h-full", isWide && "w-full"]) // ✅ falsy 는 무시됨
22
+ * xRecipe({}, cn("h-full", className)) // ❌ 불필요한 래핑
23
+ */
24
+ export declare const recipe: <P extends object>(styles: (props?: P) => string) => (variants?: Omit<P, "class" | "className">, className?: ClassValue) => string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * recipe 레이어 — 프리미티브의 스타일 recipe(변형)를 모으는 **서버-안전** 폴더.
3
+ * **여기엔 절대 `"use client"` 를 붙이지 않는다.** 팩토리(`recipe`/`tv`)는 `./factory` 에 있다.
4
+ *
5
+ * 구조: recipe 하나당 파일 하나(`<name>Recipe.ts`), 이 index 는 재수출만 한다. 앱도 같은 구조를
6
+ * `apps/<app>/ui/Recipe/` 에서 계승한다.
7
+ *
8
+ * 호출 규격: `xRecipe(변형객체, 커스텀클래스?)`. 두 번째 인자 className 은 recipe 안에서 tailwind-merge 로
9
+ * 자동 병합되므로 호출부에서 `cn(...)` 으로 감쌀 필요가 없다. 문자열 하나만이 아니라 배열도 받는다:
10
+ * buttonRecipe({ variant: "primary" }, "w-full rounded-2xl") // ✅ cn 불필요
11
+ * buttonRecipe({ variant: "primary" }, ["h-full", className]) // ✅ 배열 — 여기서도 cn 불필요
12
+ *
13
+ * 레이어: styles.css(토큰) → recipe(변형) → 컴포넌트(동작).
14
+ */
15
+ export { type BadgeVariants, badgeRecipe } from "./badgeRecipe.d.ts";
16
+ export { type ButtonVariants, buttonRecipe } from "./buttonRecipe.d.ts";
17
+ export { recipe, tv } from "./factory.d.ts";
18
+ export { type InputSurfaceVariants, inputRecipe } from "./inputRecipe.d.ts";
@@ -0,0 +1,13 @@
1
+ /** 입력 표면 look — Input/TextArea 가 공유하는 필드 셸. kind 로 한 줄 필드(field)/멀티라인(area)을 고른다. */
2
+ export declare const inputRecipe: (variants?: Omit<({
3
+ kind?: "field" | "area" | undefined;
4
+ } & {
5
+ class?: import("tailwind-variants").ClassValue;
6
+ className?: never;
7
+ }) | ({
8
+ kind?: "field" | "area" | undefined;
9
+ } & {
10
+ class?: never;
11
+ className?: import("tailwind-variants").ClassValue;
12
+ }), "class" | "className"> | undefined, className?: import("tailwind-merge").ClassNameValue) => string;
13
+ export type InputSurfaceVariants = NonNullable<Parameters<typeof inputRecipe>[0]>;
@@ -0,0 +1 @@
1
+ export * from "./recipe/index.d.ts";
package/ui/Badge.tsx ADDED
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import type { HTMLAttributes } from "react";
3
+ import { type BadgeVariants, badgeRecipe } from "./recipe";
4
+ import { createOverridable, useUiRecipe } from "./UiOverride";
5
+
6
+ export { badgeRecipe, type BadgeVariants };
7
+
8
+ export type BadgeProps = HTMLAttributes<HTMLSpanElement> & BadgeVariants;
9
+
10
+ const DefaultBadge = ({ className, variant, ...rest }: BadgeProps) => {
11
+
12
+ const recipe = useUiRecipe("badge") ?? badgeRecipe;
13
+ return <span className={recipe({ variant }, className)} {...rest} />;
14
+ };
15
+
16
+ /** Status/label pill. Route-overridable via `page/**\/_overrides.tsx` (slot `Badge`). */
17
+ export const Badge = createOverridable("Badge", DefaultBadge);
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { useDrag } from "@use-gesture/react";
3
- import { clsx } from "akanjs/client";
3
+ import { cn } from "akanjs/client";
4
4
  import { st } from "akanjs/store";
5
5
  import { animated } from "akanjs/ui";
6
6
  import { forwardRef, type ReactNode, useEffect, useImperativeHandle, useRef } from "react";
@@ -66,23 +66,21 @@ export const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>(
66
66
  opacity,
67
67
  }}
68
68
  onClick={() => void closeModal()}
69
- className={clsx("fixed top-0 left-0 size-full bg-gray-500/50", {
70
- "z-50": open,
71
- "-z-[1]": !open,
72
- })}
69
+ className={cn("fixed top-0 left-0 size-full bg-muted-foreground/50", open && "z-50", !open && "-z-[1]")}
73
70
  />
74
71
  <animated.div
75
72
  ref={ref}
76
73
  style={{ y, paddingTop: type === "full" ? pageState.topSafeArea : 0 }}
77
- className={clsx("fixed bottom-0 left-0 z-[101] w-full bg-base-100", {
78
- "h-[90%] rounded-t-3xl": type === "half",
79
- "h-[100vh]": type === "full",
80
- })}
74
+ className={cn(
75
+ "fixed bottom-0 left-0 z-[101] w-full bg-background",
76
+ type === "half" && "h-[90%] rounded-t-3xl",
77
+ type === "full" && "h-[100vh]",
78
+ )}
81
79
  >
82
80
  <div className="flex h-8 w-full items-start justify-center pt-2">
83
81
  {type === "half" ? (
84
82
  <animated.div {...bind()} className="flex h-8 w-full items-start justify-center pt-2">
85
- <div className="h-2 w-32 rounded-full bg-gray-300"></div>
83
+ <div className="h-2 w-32 rounded-full bg-muted"></div>
86
84
  </animated.div>
87
85
  ) : (
88
86
  <button