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
package/ui/Input.tsx CHANGED
@@ -14,10 +14,7 @@ import React, {
14
14
  import { AiOutlineEye, AiOutlineEyeInvisible } from "react-icons/ai";
15
15
 
16
16
  import { inputRecipe } from "./recipe";
17
- import { createOverridable } from "./UiOverride";
18
-
19
- const inputBase = inputRecipe();
20
- const textareaBase = inputRecipe({ kind: "area" });
17
+ import { createOverridable, useUiRecipe } from "./UiOverride";
21
18
 
22
19
  export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "onChange"> & {
23
20
  /** Visual input style. */
@@ -64,6 +61,7 @@ const DefaultInput = ({
64
61
  const { l } = usePage();
65
62
  const [firstFocus, setFirstFocus] = useState(true);
66
63
  const validateResult = validate ? validate(value) : undefined;
64
+ const inputBase = (useUiRecipe("input") ?? inputRecipe)();
67
65
  const status: "error" | "warning" | "success" | null =
68
66
  !nullable && !value ? null : !value.length ? "warning" : validateResult === true ? "success" : "error";
69
67
  const invalidMessage =
@@ -169,6 +167,7 @@ const DefaultTextArea = ({
169
167
  const { l } = usePage();
170
168
  const inputRef = useRef<HTMLTextAreaElement>(null);
171
169
  const validateResult = validate(value);
170
+ const textareaBase = (useUiRecipe("input") ?? inputRecipe)({ kind: "area" });
172
171
  const [firstFocus, setFirstFocus] = useState(true);
173
172
  const status: "error" | "warning" | "success" =
174
173
  !nullable && !value.length ? "warning" : validateResult === true ? "success" : "error";
@@ -263,6 +262,7 @@ const DefaultPassword = ({
263
262
  const validateResult = validate(value);
264
263
  const [firstFocus, setFirstFocus] = useState(true);
265
264
  const [showPassword, setShowPassword] = useState(false);
265
+ const inputBase = (useUiRecipe("input") ?? inputRecipe)();
266
266
  const status: "error" | "warning" | "success" =
267
267
  !nullable && !value.length ? "warning" : validateResult === true ? "success" : "error";
268
268
  const invalidMessage =
@@ -373,6 +373,7 @@ const DefaultEmail = ({
373
373
  const { l } = usePage();
374
374
  const inputRef = useRef<HTMLInputElement>(null);
375
375
  const isValidEmail = isEmail(value);
376
+ const inputBase = (useUiRecipe("input") ?? inputRecipe)();
376
377
  const [firstFocus, setFirstFocus] = useState(true);
377
378
  const validateResult = !isValidEmail ? l("base.emailInvalidError") : validate(value);
378
379
  const status: "error" | "warning" | "success" =
@@ -484,6 +485,7 @@ const DefaultNumber = ({
484
485
  const { l } = usePage();
485
486
  const inputRef = useRef<HTMLInputElement>(null);
486
487
  const validateResult = validate ? validate(value) : undefined;
488
+ const inputBase = (useUiRecipe("input") ?? inputRecipe)();
487
489
  const generateFormat = () => {
488
490
  return isNaN(value ?? 0) ? "" : formatter ? formatter(value?.toString() ?? "") : (value?.toString() ?? "");
489
491
  };
@@ -623,7 +625,7 @@ const DefaultCheckbox = ({ checked, onChange, className, ...rest }: CheckboxProp
623
625
  {...rest}
624
626
  type="checkbox"
625
627
  checked={checked}
626
- className={cn("checkbox", className)}
628
+ className={cn("size-5 accent-primary", className)}
627
629
  onChange={(e) => {
628
630
  onChange(e.target.checked, e);
629
631
  }}
@@ -9,6 +9,7 @@ export interface BottomInsetProps {
9
9
  className?: string;
10
10
  children: ReactNode;
11
11
  keyboardSticky?: boolean;
12
+ contentAnchor?: "bottom";
12
13
  role?: "bottomChrome" | "keyboardAccessory";
13
14
  estimatedHeight?: number;
14
15
  frameScope?: FrameSlotRegistration["scope"];
@@ -20,6 +21,7 @@ export const BottomInset = ({
20
21
  className,
21
22
  children,
22
23
  keyboardSticky,
24
+ contentAnchor,
23
25
  role,
24
26
  estimatedHeight,
25
27
  frameScope = "page",
@@ -49,12 +51,13 @@ export const BottomInset = ({
49
51
  debugFrame("bottomInset.mount", {
50
52
  path,
51
53
  keyboardSticky,
54
+ contentAnchor,
52
55
  role: frameRole,
53
56
  frameScope,
54
57
  frameSource,
55
58
  });
56
59
  return () => debugFrame("bottomInset.unmount", { path, frameSource });
57
- }, [path, keyboardSticky, frameRole, frameScope, frameSource]);
60
+ }, [path, keyboardSticky, contentAnchor, frameRole, frameScope, frameSource]);
58
61
  useLayoutEffect(() => {
59
62
  if (!path) return;
60
63
  const height = useDeclaredHeight ? resolvedEstimatedHeight : (measuredHeight ?? readContentHeight());
@@ -63,6 +66,7 @@ export const BottomInset = ({
63
66
  role: frameRole,
64
67
  scope: frameScope,
65
68
  source: frameSource,
69
+ contentAnchor,
66
70
  estimatedHeight: resolvedEstimatedHeight,
67
71
  height,
68
72
  cache: frameCache,
@@ -72,6 +76,7 @@ export const BottomInset = ({
72
76
  frameRole,
73
77
  frameScope,
74
78
  frameSource,
79
+ contentAnchor,
75
80
  resolvedEstimatedHeight,
76
81
  measuredHeight,
77
82
  frameCache,
@@ -10,5 +10,12 @@ export interface ProgressBarProps {
10
10
  }
11
11
  export const ProgressBar = ({ className, value, max }: ProgressBarProps) => {
12
12
  const progress = useSpring({ value: 0, to: { value: value } });
13
- return <animated.progress className={cn("progress w-full", className)} value={progress.value} max={max} />;
13
+
14
+ return (
15
+ <animated.progress
16
+ className={cn("h-2 w-full rounded-box accent-primary", className)}
17
+ value={progress.value}
18
+ max={max}
19
+ />
20
+ );
14
21
  };
package/ui/Menu.tsx CHANGED
@@ -5,7 +5,7 @@ import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useSta
5
5
  import { AiFillCaretDown, AiOutlineEllipsis } from "react-icons/ai";
6
6
  import { buttonRecipe } from "./Button";
7
7
 
8
- import { createOverridable } from "./UiOverride";
8
+ import { createOverridable, useUiRecipe } from "./UiOverride";
9
9
 
10
10
  export interface MenuItem {
11
11
  label: ReactNode;
@@ -62,13 +62,11 @@ export const DefaultMenu = ({
62
62
  ? `fixed bottom-0 flex translate-y-[98%] flex-col rounded-xs border-0 bg-background shadow-lg hover:bg-background`
63
63
  : "flex flex-col gap-0 p-0 bg-primary/10 hover:bg-primary/10 overflow-hidden";
64
64
 
65
+ const recipe = useUiRecipe("button") ?? buttonRecipe;
65
66
  const subMenuItemClassName =
66
67
  mode === "inline"
67
- ? buttonRecipe({ variant: "ghost" }, "m-0 h-full w-full px-2 font-normal text-primary hover:bg-primary/20")
68
- : buttonRecipe(
69
- { variant: "ghost" },
70
- "w-full whitespace-nowrap text-center font-normal text-primary duration-300",
71
- );
68
+ ? recipe({ variant: "ghost" }, "m-0 h-full w-full px-2 font-normal text-primary hover:bg-primary/20")
69
+ : recipe({ variant: "ghost" }, "w-full whitespace-nowrap text-center font-normal text-primary duration-300");
72
70
 
73
71
  const [overflowMenuItems, setOverflowMenuItems] = useState<MenuItem[]>([]);
74
72
 
@@ -222,6 +220,7 @@ interface OverflowMenuProps {
222
220
  const OverflowMenu = ({ overflowItems, onClick }: OverflowMenuProps) => {
223
221
  const [isOpen, setIsOpen] = useState(false);
224
222
  const [expandedKey, setExpandedKey] = useState<string>();
223
+ const recipe = useUiRecipe("button") ?? buttonRecipe;
225
224
  const handleMouseEnter = () => {
226
225
  setIsOpen(true);
227
226
  };
@@ -239,7 +238,7 @@ const OverflowMenu = ({ overflowItems, onClick }: OverflowMenuProps) => {
239
238
  <div
240
239
  key={item.key}
241
240
  onClick={() => onClick?.(item)}
242
- className={buttonRecipe({ variant: "ghost" }, "relative whitespace-nowrap font-normal text-primary")}
241
+ className={recipe({ variant: "ghost" }, "relative whitespace-nowrap font-normal text-primary")}
243
242
  onMouseEnter={() => {
244
243
  if (item.children && expandedKey !== item.key) setExpandedKey(item.key);
245
244
  }}
@@ -258,7 +257,7 @@ const OverflowMenu = ({ overflowItems, onClick }: OverflowMenuProps) => {
258
257
  setExpandedKey(undefined);
259
258
  }}
260
259
  key={child.key}
261
- className={buttonRecipe(
260
+ className={recipe(
262
261
  { variant: "ghost" },
263
262
  "flex items-center justify-center text-center font-normal text-primary",
264
263
  )}
@@ -8,6 +8,7 @@ import { AiOutlineDelete } from "react-icons/ai";
8
8
 
9
9
  import { buttonRecipe } from "../Button";
10
10
  import { Modal } from "../Modal";
11
+ import { inputRecipe } from "../recipe";
11
12
 
12
13
  interface SureToRemoveProps {
13
14
  className?: string;
@@ -95,7 +96,7 @@ export default function SureToRemove({
95
96
  </div>
96
97
  {typeNameToRemove ? (
97
98
  <input
98
- className="input w-full text-center"
99
+ className={inputRecipe({}, "text-center")}
99
100
  placeholder={`${l(`${modelName}.modelName` as "base.new")} name`}
100
101
  value={repeatName}
101
102
  onChange={(e) => {
package/ui/ObjectId.tsx CHANGED
@@ -4,6 +4,7 @@ import { AiOutlineCheck, AiOutlineCopy } from "react-icons/ai";
4
4
 
5
5
  import { buttonRecipe } from "./Button";
6
6
  import { Tooltip } from "./Tooltip";
7
+ import { useUiRecipe } from "./UiOverride";
7
8
 
8
9
  interface ObjectIdProps {
9
10
  id: string;
@@ -11,6 +12,7 @@ interface ObjectIdProps {
11
12
 
12
13
  export const ObjectId = ({ id }: ObjectIdProps) => {
13
14
  const [isCopied, setIsCopied] = useState(false);
15
+ const recipe = useUiRecipe("button") ?? buttonRecipe;
14
16
  const shortenedId = `${id.substring(0, 4)}...${id.substring(id.length - 5)}`;
15
17
  const handleCopyClick = () => {
16
18
  void navigator.clipboard.writeText(id);
@@ -24,10 +26,7 @@ export const ObjectId = ({ id }: ObjectIdProps) => {
24
26
  <Tooltip content={id} variant="primary">
25
27
  <div className="font-semibold text-xs">{shortenedId}</div>
26
28
  </Tooltip>
27
- <button
28
- className={buttonRecipe({ variant: "ghost", size: "icon" }, "size-6 rounded-full")}
29
- onClick={handleCopyClick}
30
- >
29
+ <button className={recipe({ variant: "ghost", size: "icon" }, "size-6 rounded-full")} onClick={handleCopyClick}>
31
30
  {isCopied ? <AiOutlineCheck /> : <AiOutlineCopy />}
32
31
  </button>
33
32
  </div>
package/ui/Pagination.tsx CHANGED
@@ -4,10 +4,7 @@ import type { FC, ReactNode } from "react";
4
4
  import { BiChevronLeft, BiChevronRight, BiDotsHorizontalRounded } from "react-icons/bi";
5
5
 
6
6
  import { buttonRecipe } from "./Button";
7
- import { createOverridable } from "./UiOverride";
8
-
9
- /** Square ghost button style shared by every pager control (canonical buttonRecipe). */
10
- const pageBtn = buttonRecipe({ variant: "ghost", size: "icon" });
7
+ import { createOverridable, useUiRecipe } from "./UiOverride";
11
8
 
12
9
  export interface PaginationProps {
13
10
  /** Current 1-based page number. */
@@ -36,6 +33,8 @@ export const DefaultPagination: FC<PaginationProps> = ({
36
33
  renderEmpty,
37
34
  classNames,
38
35
  }) => {
36
+
37
+ const pageBtn = (useUiRecipe("button") ?? buttonRecipe)({ variant: "ghost", size: "icon" });
39
38
  const totalPages = Math.ceil(total / (itemsPerPage || 1));
40
39
  const handleLeftClick = () => {
41
40
  if (currentPage <= 1) return;
package/ui/Popconfirm.tsx CHANGED
@@ -6,7 +6,7 @@ import { type ButtonHTMLAttributes, type ReactNode, useEffect, useState } from "
6
6
  import { BiMessageRoundedError } from "react-icons/bi";
7
7
  import { animated } from "./animated";
8
8
  import { buttonRecipe } from "./Button";
9
- import { createOverridable } from "./UiOverride";
9
+ import { createOverridable, useUiRecipe } from "./UiOverride";
10
10
 
11
11
  type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
12
12
  loading?: boolean;
@@ -48,6 +48,8 @@ export const DefaultPopconfirm = ({
48
48
  decoClassName,
49
49
  }: PopconfirmProps) => {
50
50
  const { l } = usePage();
51
+
52
+ const recipe = useUiRecipe("button") ?? buttonRecipe;
51
53
  const [isConfirming, setIsConfirming] = useState(false);
52
54
 
53
55
  const popconfirmProps = useSpring({
@@ -115,17 +117,13 @@ export const DefaultPopconfirm = ({
115
117
  </div>
116
118
  <div className="flex justify-end gap-2">
117
119
  <button
118
- className={buttonRecipe({ variant: "outline", size: "xs" })}
120
+ className={recipe({ variant: "outline", size: "xs" })}
119
121
  onClick={handleCancel}
120
122
  {...cancelButtonProps}
121
123
  >
122
124
  {cancelText ?? l("base.cancel")}
123
125
  </button>
124
- <button
125
- className={buttonRecipe({ variant: "primary", size: "xs" })}
126
- onClick={handleConfirm}
127
- {...okButtonProps}
128
- >
126
+ <button className={recipe({ variant: "primary", size: "xs" })} onClick={handleConfirm} {...okButtonProps}>
129
127
  {okText ?? l("base.ok")}
130
128
  </button>
131
129
  </div>
package/ui/Select.tsx CHANGED
@@ -217,7 +217,7 @@ const DefaultSelect = <
217
217
  {searchable ? (
218
218
  <input
219
219
  type="text"
220
- className="input w-full flex-1 border-none bg-transparent shadow-none outline-none focus:border-none focus:shadow-none focus:outline-none"
220
+ className="w-full flex-1 border-none bg-transparent shadow-none outline-none focus:border-none focus:shadow-none focus:outline-none"
221
221
  placeholder={selectedValues.length > 0 ? "" : placeholder}
222
222
  value={searchText}
223
223
  onChange={(e) => {
package/ui/System/CSR.tsx CHANGED
@@ -226,6 +226,7 @@ const CSRWrapper = ({
226
226
  : bottomInset?.containerStyle
227
227
  }
228
228
  animationDuration={frameLayout.keyboard.animationDuration}
229
+ animationEasing={frameLayout.keyboard.animationEasing}
229
230
  >
230
231
  <CSRFrameSlotTargets slot="keyboardInset" />
231
232
  </KeyboardLayer>
@@ -281,14 +282,17 @@ const KeyboardLayer = ({
281
282
  className,
282
283
  style,
283
284
  animationDuration,
285
+ animationEasing,
284
286
  children,
285
- }: FrameLayerProps & { animationDuration?: number }) => (
287
+ }: FrameLayerProps & { animationDuration?: number; animationEasing?: string }) => (
286
288
  <animated.div
287
289
  id={id}
288
290
  className={className}
289
291
  style={{
290
292
  ...(style ?? {}),
291
- transition: `top ${animationDuration ?? 285}ms ease-out, height ${animationDuration ?? 285}ms ease-out`,
293
+ transition: `top ${animationDuration ?? 420}ms ${animationEasing ?? "cubic-bezier(0.16, 1, 0.3, 1)"}, height ${
294
+ animationDuration ?? 420
295
+ }ms ${animationEasing ?? "cubic-bezier(0.16, 1, 0.3, 1)"}`,
292
296
  willChange: "top, height",
293
297
  }}
294
298
  >
@@ -3,10 +3,9 @@ import { cn, usePage } from "akanjs/client";
3
3
  import { type ComponentType, createElement } from "react";
4
4
 
5
5
  import { buttonRecipe } from "./Button";
6
- import { createOverridable, useUiOverride } from "./UiOverride";
6
+ import { createOverridable, useUiOverride, useUiRecipe } from "./UiOverride";
7
7
 
8
- /** Toggle-select cell: outline button, filled success when selected. */
9
- const toggleBtn = buttonRecipe({ variant: "outline", size: "sm" });
8
+ /** Toggle-select cell: outline button (recipe slot), filled success when selected. */
10
9
  const selectedCls = "border-transparent bg-success/70 text-success-foreground hover:bg-success/70";
11
10
 
12
11
  export interface ToggleSelectProps<I extends string | number | boolean | null> {
@@ -30,6 +29,7 @@ const DefaultToggleSelect = <I extends string | number | boolean | null>({
30
29
  disabled,
31
30
  }: ToggleSelectProps<I>) => {
32
31
  const { l } = usePage();
32
+ const toggleBtn = (useUiRecipe("button") ?? buttonRecipe)({ variant: "outline", size: "sm" });
33
33
  const validateResult = value !== null ? validate(value) : false;
34
34
 
35
35
  const invalidMessage =
@@ -97,6 +97,7 @@ const DefaultMulti = ({
97
97
  disabled,
98
98
  }: MultiProps) => {
99
99
  const { l } = usePage();
100
+ const toggleBtn = (useUiRecipe("button") ?? buttonRecipe)({ variant: "outline", size: "sm" });
100
101
  const validateResult = validate(value);
101
102
 
102
103
  const invalidMessage =
@@ -17,7 +17,7 @@ import type { ModalProps } from "../Modal";
17
17
  import type { PaginationProps } from "../Pagination";
18
18
  import type { PopconfirmProps } from "../Popconfirm";
19
19
  import type { ItemProps as RadioItemProps, RadioProps } from "../Radio";
20
- import type { BadgeVariants, ButtonVariants } from "../recipe";
20
+ import type { BadgeVariants, ButtonVariants, InputSurfaceVariants } from "../recipe";
21
21
  import type { SelectProps } from "../Select";
22
22
  import type { TableProps } from "../Table";
23
23
  import type { MultiProps as ToggleSelectMultiProps, ToggleSelectProps } from "../ToggleSelect";
@@ -87,11 +87,21 @@ export type AkanModalComponent = AkanUiOverrides["Modal"];
87
87
  * manifest: `override({ recipes: { button: neonButtonRecipe } })`. A recipe swap changes
88
88
  * only the className factory — the component's structure/behavior (async states, focus,
89
89
  * a11y) is untouched. Each replacement must accept the framework recipe's full variant
90
- * contract, so every call site keeps working.
90
+ * contract, so every call site keeps working (a replacement with *extra* optional axes is
91
+ * assignable — contravariance — but those axes are only reachable from code that knows the
92
+ * replacement's own type).
93
+ *
94
+ * Scope: a recipe slot is a **client-side, route-scoped restyle**. It reaches framework
95
+ * client components, which resolve through `useUiRecipe(...)`. It never reaches a raw
96
+ * `xRecipe(...)` call in app JSX (statically imported — no context), nor server components
97
+ * (`Unit`/`View`), which intentionally render the canonical framework recipe. Extending the
98
+ * *vocabulary* is not this slot's job: add the axis to the framework recipe, or author an
99
+ * app recipe in `apps/<app>/ui/Recipe/`.
91
100
  */
92
101
  export interface AkanUiRecipes {
93
102
  button: (variants?: ButtonVariants, className?: ClassValue) => string;
94
103
  badge: (variants?: BadgeVariants, className?: ClassValue) => string;
104
+ input: (variants?: InputSurfaceVariants, className?: ClassValue) => string;
95
105
  }
96
106
 
97
107
  /** Shape of an `_overrides.tsx` manifest: component slots plus an optional recipe-slot map. */
@@ -1,23 +1,42 @@
1
1
  import { recipe, tv } from "./factory";
2
2
 
3
- /** 뱃지 look — 시맨틱 variant. `<Badge>` 가 소비하며, recipes.badge 슬롯으로 교체 가능. */
3
+ /** 뱃지 look — 시맨틱 variant × size, outline 플래그는 색을 유지한 외곽선 스타일. `<Badge>` 가 소비하며, recipes.badge 슬롯으로 교체 가능. */
4
4
  export const badgeRecipe = recipe(
5
5
  tv({
6
- base: "inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 font-medium text-xs",
6
+ base: "inline-flex items-center gap-1 rounded-full border font-medium",
7
7
  variants: {
8
8
  variant: {
9
9
  default: "border-transparent bg-muted text-foreground",
10
10
  primary: "border-transparent bg-primary text-primary-foreground",
11
11
  secondary: "border-transparent bg-secondary text-secondary-foreground",
12
12
  accent: "border-transparent bg-accent text-accent-foreground",
13
+ neutral: "border-transparent bg-neutral text-neutral-foreground",
13
14
  success: "border-transparent bg-success text-success-foreground",
14
15
  warning: "border-transparent bg-warning text-warning-foreground",
15
16
  info: "border-transparent bg-info text-info-foreground",
16
17
  error: "border-transparent bg-destructive text-destructive-foreground",
17
18
  outline: "border-border text-foreground",
18
19
  },
20
+ size: {
21
+ xs: "px-1.5 py-0 text-[0.625rem]",
22
+ sm: "px-2 py-0.5 text-xs",
23
+ md: "px-2.5 py-0.5 text-xs",
24
+ lg: "px-3 py-1 text-sm",
25
+ },
26
+
27
+ outline: { true: "border-current bg-transparent" },
19
28
  },
20
- defaultVariants: { variant: "default" },
29
+ compoundVariants: [
30
+ { variant: "primary", outline: true, class: "text-primary" },
31
+ { variant: "secondary", outline: true, class: "text-secondary" },
32
+ { variant: "accent", outline: true, class: "text-accent" },
33
+ { variant: "neutral", outline: true, class: "text-neutral" },
34
+ { variant: "success", outline: true, class: "text-success" },
35
+ { variant: "warning", outline: true, class: "text-warning" },
36
+ { variant: "info", outline: true, class: "text-info" },
37
+ { variant: "error", outline: true, class: "text-destructive" },
38
+ ],
39
+ defaultVariants: { variant: "default", size: "md" },
21
40
  }),
22
41
  );
23
42
  export type BadgeVariants = NonNullable<Parameters<typeof badgeRecipe>[0]>;
@@ -1,15 +1,18 @@
1
1
  import { recipe, tv } from "./factory";
2
2
 
3
- /** 버튼 look — 시맨틱 variant × size. `<Button>` 이 소비하며, `_overrides.tsx` 의 recipes.button 슬롯으로 교체 가능. */
3
+ /** 버튼 look — 시맨틱 variant × size × shape, outline 플래그는 색을 유지한 외곽선 스타일. `<Button>` 이 소비하며, `_overrides.tsx` 의 recipes.button 슬롯으로 교체 가능. */
4
4
  export const buttonRecipe = recipe(
5
5
  tv({
6
6
 
7
7
  base: "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-field font-medium transition active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
8
8
  variants: {
9
9
  variant: {
10
+
11
+ default: "bg-muted text-foreground hover:bg-muted/80",
10
12
  primary: "bg-primary text-primary-foreground hover:bg-primary/90",
11
13
  secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
12
14
  accent: "bg-accent text-accent-foreground hover:bg-accent/90",
15
+ neutral: "bg-neutral text-neutral-foreground hover:bg-neutral/90",
13
16
  outline: "border border-input bg-background hover:bg-muted hover:text-foreground",
14
17
  ghost: "hover:bg-muted hover:text-foreground",
15
18
  destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
@@ -25,8 +28,54 @@ export const buttonRecipe = recipe(
25
28
  lg: "h-12 px-6 text-lg",
26
29
  icon: "h-10 w-10",
27
30
  },
31
+
32
+ shape: {
33
+ default: "",
34
+ square: "aspect-square px-0",
35
+ circle: "aspect-square rounded-full px-0",
36
+ },
37
+
38
+ outline: { true: "border bg-transparent" },
28
39
  },
29
- defaultVariants: { variant: "primary", size: "md" },
40
+ compoundVariants: [
41
+ {
42
+ variant: "primary",
43
+ outline: true,
44
+ class: "border-primary text-primary hover:bg-primary hover:text-primary-foreground",
45
+ },
46
+ {
47
+ variant: "secondary",
48
+ outline: true,
49
+ class: "border-secondary text-secondary hover:bg-secondary hover:text-secondary-foreground",
50
+ },
51
+ {
52
+ variant: "accent",
53
+ outline: true,
54
+ class: "border-accent text-accent hover:bg-accent hover:text-accent-foreground",
55
+ },
56
+ {
57
+ variant: "neutral",
58
+ outline: true,
59
+ class: "border-neutral text-neutral hover:bg-neutral hover:text-neutral-foreground",
60
+ },
61
+ {
62
+ variant: "destructive",
63
+ outline: true,
64
+ class: "border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground",
65
+ },
66
+ {
67
+ variant: "success",
68
+ outline: true,
69
+ class: "border-success text-success hover:bg-success hover:text-success-foreground",
70
+ },
71
+ {
72
+ variant: "warning",
73
+ outline: true,
74
+ class: "border-warning text-warning hover:bg-warning hover:text-warning-foreground",
75
+ },
76
+ { variant: "info", outline: true, class: "border-info text-info hover:bg-info hover:text-info-foreground" },
77
+ ],
78
+ defaultVariants: { variant: "primary", size: "md", shape: "default" },
30
79
  }),
31
80
  );
32
81
  export type ButtonVariants = NonNullable<Parameters<typeof buttonRecipe>[0]>;
@@ -1,6 +1,6 @@
1
1
  import type { ClassNameValue as ClassValue } from "tailwind-merge";
2
2
  import { createTV } from "tailwind-variants";
3
- import { colorTokens } from "../../client/cn";
3
+ import { colorTokens, radiusTokens } from "../../client/cn";
4
4
 
5
5
  /**
6
6
  * recipe 팩토리 모듈 — 변형 팩토리를 만드는 **인프라**. **서버-안전** (절대 `"use client"` 금지).
@@ -8,7 +8,7 @@ import { colorTokens } from "../../client/cn";
8
8
  * 각 앱은 `apps/<app>/ui/Recipe/<name>.ts`.
9
9
  */
10
10
 
11
- export const tv = createTV({ twMergeConfig: { extend: { theme: { color: colorTokens } } } });
11
+ export const tv = createTV({ twMergeConfig: { extend: { theme: { color: colorTokens, radius: radiusTokens } } } });
12
12
 
13
13
  /**
14
14
  * recipe 팩토리 — `tv({...})` 인스턴스를 받아 겉을 `(변형, className?)` 2-인자로 노출하고,
@@ -1,13 +1,27 @@
1
1
  import { recipe, tv } from "./factory";
2
2
 
3
- /** 입력 표면 look — Input/TextArea 가 공유하는 필드 셸. kind 로 한 줄 필드(field)/멀티라인(area)을 고른다. */
3
+ /** 입력 표면 look — Input/TextArea/Select 가 공유하는 필드 셸. kind 로 한 줄 필드(field)/멀티라인(area), tone 으로 강조/오류 상태를 고른다. */
4
4
  export const inputRecipe = recipe(
5
5
  tv({
6
- base: "w-full rounded-field border border-input bg-background text-foreground text-sm focus:border-primary focus:outline-none",
6
+ base: "w-full rounded-field border border-input bg-background text-foreground focus:border-primary focus:outline-none",
7
7
  variants: {
8
- kind: { field: "h-10 px-3", area: "p-3" },
8
+ kind: { field: "px-3", area: "p-3" },
9
+ size: { xs: "text-xs", sm: "text-sm", md: "text-sm", lg: "text-base", xl: "text-lg" },
10
+ tone: {
11
+ default: "",
12
+ primary: "border-primary",
13
+ error: "border-destructive focus:border-destructive",
14
+ },
9
15
  },
10
- defaultVariants: { kind: "field" },
16
+
17
+ compoundVariants: [
18
+ { kind: "field", size: "xs", class: "h-6" },
19
+ { kind: "field", size: "sm", class: "h-8" },
20
+ { kind: "field", size: "md", class: "h-10" },
21
+ { kind: "field", size: "lg", class: "h-12" },
22
+ { kind: "field", size: "xl", class: "h-14" },
23
+ ],
24
+ defaultVariants: { kind: "field", size: "md", tone: "default" },
11
25
  }),
12
26
  );
13
27
  export type InputSurfaceVariants = NonNullable<Parameters<typeof inputRecipe>[0]>;