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
package/ui/Button.tsx CHANGED
@@ -1,80 +1,190 @@
1
1
  "use client";
2
- import { clsx, usePage } from "akanjs/client";
2
+ import { cn, usePage } from "akanjs/client";
3
+ import { isThenable } from "akanjs/common";
3
4
  import type React from "react";
4
5
  import { type ButtonHTMLAttributes, type ComponentType, createElement, useState } from "react";
5
- import { AiOutlineCheckCircle } from "react-icons/ai";
6
+ import { AiFillCheckCircle, AiOutlineLoading3Quarters } from "react-icons/ai";
7
+ import { type ButtonVariants, buttonRecipe } from "./recipe";
8
+ import { useUiOverride, useUiRecipe } from "./UiOverride";
6
9
 
7
- import { useUiOverride } from "./UiOverride";
10
+ export { buttonRecipe, type ButtonVariants };
8
11
 
9
- export type ButtonProps<Result> = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> & {
10
- /** Async-aware click handler. Call onError to show the localized error state without throwing. */
11
- onClick: (
12
- e: React.MouseEvent<HTMLButtonElement>,
13
- { onError }: { onError: (error: string) => void },
14
- ) => Promise<Result> | Result;
15
- /** Called after the button briefly enters success state. */
16
- onSuccess?: (result: Result) => void;
17
- };
12
+ export type ButtonProps<Result> = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> &
13
+ ButtonVariants & {
14
+ /**
15
+ * Click handler. Returning a promise is what opts this button into the async states — a plain
16
+ * synchronous handler renders as an ordinary button with no spinner. Call onError to show the
17
+ * localized error state without throwing.
18
+ */
19
+ onClick?: (
20
+ e: React.MouseEvent<HTMLButtonElement>,
21
+ { onError }: { onError: (error: string) => void },
22
+ ) => Promise<Result> | Result;
23
+ /** Called after the button briefly enters success state. */
24
+ onSuccess?: (result: Result) => void;
25
+ /**
26
+ * How the pending/success indicator is drawn. Both modes keep the button's box fixed — CSS cannot
27
+ * animate an auto width, so a resizing button can only ever snap.
28
+ *
29
+ * `hold` (default) fades a bare indicator over the children, so the box stays exactly the children's
30
+ * size. `replace` cross-fades to a labelled indicator ("Processing…"); both labels stay stacked in the
31
+ * DOM, so the box is the wider of the two from the start and still never moves — at the cost of an idle
32
+ * button that is as wide as its longest state.
33
+ */
34
+ loadingMode?: "hold" | "replace";
35
+ /** Whether a failed action renders its message under the button. Off leaves it to the framework toast. */
36
+ showError?: boolean;
37
+ };
38
+
39
+ /**
40
+ * Success mark, entering with `checkIn` (akanjs/ui/styles.css — a 340ms fade plus a 6% overshoot; `pop`'s 1.5x
41
+ * overshoot from zero reads as cartoonish on a glyph this small).
42
+ *
43
+ * The colour is derived from the button, never named — any fixed token fails somewhere: `text-success` vanishes
44
+ * on a `success` button, and a `bg-success` disc lands an unrelated hue on every other fill.
45
+ *
46
+ * `AiFillCheckCircle` is a solid disc with the tick punched out, which is why it is used instead of an outline
47
+ * glyph plus a tinted halo. The disc is `currentColor` (the button's foreground) and the tick is the fill
48
+ * showing through, so the mark reuses the variant's own foreground/background pair — already contrast-checked
49
+ * by themeValidator — at full strength. An earlier attempt put an outline tick on a low-alpha current-colour
50
+ * halo, which did the opposite: tinting the local background toward the glyph's own colour *lowered* the
51
+ * contrast that made the tick readable. (Do not name the class here — Tailwind's source scanner reads comments
52
+ * too, so a class literal in prose compiles to a dead rule.)
53
+ *
54
+ * Sizing is in `em`, so one class covers `xs` through `lg`.
55
+ */
56
+ const successIconClass = "animate-checkIn text-[1.15em]";
18
57
 
19
- const DefaultButton = <Result = unknown>({ className, children, onClick, onSuccess, ...rest }: ButtonProps<Result>) => {
58
+ const successDwellMs = 700;
59
+
60
+ const DefaultButton = <Result = unknown>({
61
+ className,
62
+ variant,
63
+ size,
64
+ type = "button",
65
+ loadingMode = "hold",
66
+ showError = true,
67
+ children,
68
+ onClick,
69
+ onSuccess,
70
+ ...rest
71
+ }: ButtonProps<Result>) => {
20
72
  const { l } = usePage();
73
+
74
+ const recipe = useUiRecipe("button") ?? buttonRecipe;
75
+
21
76
  const [state, setState] = useState<{
22
77
  mode: "idle" | "loading" | "success" | "error";
23
78
  error: string | null;
24
- times: number;
25
- }>({
26
- mode: "idle",
27
- error: null,
28
- times: 0,
29
- });
79
+ shown: "loading" | "success";
80
+ }>({ mode: "idle", error: null, shown: "loading" });
81
+ const busy = state.mode === "loading" || state.mode === "success";
30
82
  return (
31
83
  <>
32
84
  <button
33
- className={clsx("btn", className)}
85
+ type={type}
86
+ className={recipe(
87
+ { variant, size },
88
+
89
+ cn(loadingMode === "hold" && "relative", busy && !rest.disabled && "disabled:opacity-100", className),
90
+ )}
34
91
  {...rest}
35
- disabled={!!rest.disabled || ["loading", "success"].includes(state.mode)}
92
+ disabled={!!rest.disabled || busy}
36
93
  onClick={(e) => {
37
- setState({ mode: "loading", error: null, times: state.times + 1 });
94
+ if (!onClick) return;
95
+ let errored = false;
96
+ const result = onClick(e, {
97
+ onError: (error) => {
98
+ errored = true;
99
+ setState((s) => ({ ...s, mode: "error", error }));
100
+ },
101
+ });
102
+ if (!isThenable(result)) return;
103
+ if (!errored) setState({ mode: "loading", error: null, shown: "loading" });
38
104
  void (async () => {
39
- const result = await onClick(e, {
40
- onError: (error) => {
41
- setState({ mode: "error", error, times: state.times + 1 });
42
- },
43
- });
44
- setState({ mode: "success", error: null, times: state.times + 1 });
45
- setTimeout(() => {
46
- setState({ mode: "idle", error: null, times: state.times + 1 });
47
- onSuccess?.(result);
48
- }, 300);
105
+ try {
106
+ const awaited = (await result) as Result;
107
+
108
+ if (errored) return;
109
+ setState({ mode: "success", error: null, shown: "success" });
110
+ setTimeout(() => {
111
+
112
+ setState((s) => ({ ...s, mode: "idle", error: null }));
113
+ onSuccess?.(awaited);
114
+ }, successDwellMs);
115
+ } catch {
116
+
117
+ setState((s) => ({ ...s, mode: "idle", error: null }));
118
+ }
49
119
  })();
50
120
  }}
51
121
  >
52
- {state.mode === "loading" ? (
53
- <>
54
- <span className="loading loading-bars loading-md" /> {l("base.processing")}
55
- </>
56
- ) : state.mode === "success" ? (
122
+ {loadingMode === "replace" ? (
123
+
124
+ <span className="grid place-items-center">
125
+ <span
126
+ className={cn(
127
+ "col-start-1 row-start-1 flex items-center gap-2 transition-opacity duration-200",
128
+ busy && "opacity-0",
129
+ )}
130
+ >
131
+ {children}
132
+ </span>
133
+ <span
134
+ aria-hidden={!busy}
135
+ className={cn(
136
+ "col-start-1 row-start-1 flex items-center gap-2 transition-opacity duration-200",
137
+ busy ? "opacity-100" : "opacity-0",
138
+ )}
139
+ >
140
+ {state.shown === "success" ? (
141
+ <>
142
+ <AiFillCheckCircle className={successIconClass} /> {l("base.processed")}
143
+ </>
144
+ ) : (
145
+ <>
146
+ <AiOutlineLoading3Quarters className="animate-spin" /> {l("base.processing")}
147
+ </>
148
+ )}
149
+ </span>
150
+ </span>
151
+ ) : (
57
152
  <>
58
- <AiOutlineCheckCircle /> {l("base.processed")}
153
+ {/* Children keep their box while hidden, so the button's measured size never changes. */}
154
+ <span className={cn("inline-flex items-center gap-2 transition-opacity duration-200", busy && "opacity-0")}>
155
+ {children}
156
+ </span>
157
+ <span
158
+ aria-hidden={!busy}
159
+ className={cn(
160
+ "absolute inset-0 flex items-center justify-center transition-opacity duration-200",
161
+ busy ? "opacity-100" : "pointer-events-none opacity-0",
162
+ )}
163
+ >
164
+ {state.shown === "success" ? (
165
+ <AiFillCheckCircle className={successIconClass} />
166
+ ) : (
167
+ <AiOutlineLoading3Quarters className="animate-spin" />
168
+ )}
169
+ </span>
59
170
  </>
60
- ) : (
61
- children
62
171
  )}
63
172
  </button>
64
- {/* ? 이거 뭔지 */}
65
- {state.error ? (
66
- <div className="h-10 w-full p-2 text-center text-error text-sm">
67
- {state.error ? l(state.error as "base.error") : " "}
68
- </div>
173
+ {showError && state.error ? (
174
+ <span role="alert" className="mt-1 block text-center text-destructive text-xs">
175
+ {l(state.error as "base.error")}
176
+ </span>
69
177
  ) : null}
70
178
  </>
71
179
  );
72
180
  };
73
181
 
74
182
  /**
75
- * Async-aware button. Resolves to a route-scoped override when a `page/**\/_overrides.tsx`
76
- * in the route's ancestry declares one, otherwise renders {@link DefaultButton}. The public
77
- * generic signature is preserved, so `<Button<Todo> onSuccess={(r: Todo) => …} />` still infers.
183
+ * Button whose async states are driven by the handler's return value: return a promise and it shows a
184
+ * pending indicator, then a brief success state; return nothing and it behaves as a plain button. Resolves
185
+ * to a route-scoped override when a `page/**\/_overrides.tsx` in the route's ancestry declares one,
186
+ * otherwise renders {@link DefaultButton}. The public generic signature is preserved, so
187
+ * `<Button<Todo> onSuccess={(r: Todo) => …} />` still infers.
78
188
  */
79
189
  export const Button = <Result = unknown>(props: ButtonProps<Result>) => {
80
190
  const Override = useUiOverride("Button");
package/ui/Clipboard.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { clsx } from "akanjs/client";
2
+ import { cn } from "akanjs/client";
3
3
  import { type ReactElement, useEffect, useState } from "react";
4
4
  import { FaCheck } from "react-icons/fa";
5
5
  import { MdContentCopy } from "react-icons/md";
@@ -30,8 +30,8 @@ export const Clipboard = ({ text, className }: ClipboardProps): ReactElement =>
30
30
  onClick={async () => {
31
31
  await handleCopy();
32
32
  }}
33
- className={clsx(
34
- "flex h-6 w-6 items-center justify-center rounded-md bg-white/30 text-white transition-all duration-300 hover:bg-white/20",
33
+ className={cn(
34
+ "flex h-6 w-6 items-center justify-center rounded-md bg-foreground/10 text-foreground transition-all duration-300 hover:bg-foreground/20",
35
35
  className,
36
36
  )}
37
37
  >