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.
- package/base/symbols.ts +0 -1
- package/client/cn.ts +7 -0
- package/client/csrTypes.ts +2 -0
- package/client/frameDebug.ts +3 -2
- package/constant/cascadePaths.ts +8 -79
- package/document/database.ts +1 -2
- package/document/databaseRegistry.ts +2 -2
- package/document/into.ts +6 -10
- package/package.json +1 -1
- package/server/SSR_MEMORY_DIAGNOSIS.md +2 -19
- package/server/cachePolicy.ts +12 -114
- package/server/di/diLifecycle.ts +10 -8
- package/server/processMetricsCollector.ts +0 -4
- package/server/resolver/database.resolver.ts +3 -23
- package/server/resolver/index.ts +0 -1
- package/server/resolver/service.resolver.ts +25 -9
- package/server/rscWorker.tsx +4 -32
- package/server/rscWorkerHost.ts +12 -61
- package/server/ssrFromRscRenderer.tsx +2 -2
- package/server/webRouter.ts +96 -9
- package/service/ipcTypes.ts +0 -25
- package/service/predefinedAdaptor/database.adaptor.ts +3 -3
- package/service/serve.ts +1 -5
- package/service/types.ts +0 -2
- package/types/base/symbols.d.ts +0 -1
- package/types/client/cn.d.ts +5 -0
- package/types/client/csrTypes.d.ts +2 -0
- package/types/constant/cascadePaths.d.ts +4 -22
- package/types/document/database.d.ts +1 -2
- package/types/document/databaseRegistry.d.ts +2 -2
- package/types/document/into.d.ts +5 -6
- package/types/server/cachePolicy.d.ts +2 -33
- package/types/server/resolver/database.resolver.d.ts +2 -6
- package/types/server/resolver/index.d.ts +0 -1
- package/types/server/resolver/service.resolver.d.ts +3 -3
- package/types/server/rscWorkerHost.d.ts +0 -13
- package/types/service/ipcTypes.d.ts +0 -24
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +2 -2
- package/types/service/types.d.ts +1 -2
- package/types/ui/Layout/BottomInset.d.ts +2 -1
- package/types/ui/Layout/index.d.ts +1 -1
- package/types/ui/Signal/style.d.ts +1 -1
- package/types/ui/UiOverride/context.d.ts +12 -2
- package/types/ui/recipe/badgeRecipe.d.ts +7 -3
- package/types/ui/recipe/buttonRecipe.d.ts +7 -3
- package/types/ui/recipe/inputRecipe.d.ts +5 -1
- package/types/webkit/useCsrValues.d.ts +3 -3
- package/types/webkit/useFrameRuntime.d.ts +6 -1
- package/ui/Badge.tsx +2 -2
- package/ui/Button.tsx +3 -1
- package/ui/Constant/Doc.tsx +1 -1
- package/ui/Data/ListContainer.tsx +1 -1
- package/ui/DatePicker.tsx +4 -3
- package/ui/DraggableList.tsx +3 -1
- package/ui/Dropdown.tsx +4 -2
- package/ui/Field.tsx +17 -9
- package/ui/Input.tsx +7 -5
- package/ui/Layout/BottomInset.tsx +6 -1
- package/ui/Loading/ProgressBar.tsx +8 -1
- package/ui/Menu.tsx +7 -8
- package/ui/Model/SureToRemove.tsx +2 -1
- package/ui/ObjectId.tsx +3 -4
- package/ui/Pagination.tsx +3 -4
- package/ui/Popconfirm.tsx +5 -7
- package/ui/Select.tsx +1 -1
- package/ui/System/CSR.tsx +6 -2
- package/ui/ToggleSelect.tsx +4 -3
- package/ui/UiOverride/context.ts +12 -2
- package/ui/recipe/badgeRecipe.ts +22 -3
- package/ui/recipe/buttonRecipe.ts +51 -2
- package/ui/recipe/factory.ts +2 -2
- package/ui/recipe/inputRecipe.ts +18 -4
- package/webkit/useCsrValues.ts +121 -4
- package/webkit/useFrameRuntime.ts +42 -12
- package/server/resolver/CascadeRunner.ts +0 -201
- 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("
|
|
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
|
-
|
|
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
|
-
?
|
|
68
|
-
:
|
|
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={
|
|
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={
|
|
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="
|
|
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={
|
|
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="
|
|
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 ??
|
|
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
|
>
|
package/ui/ToggleSelect.tsx
CHANGED
|
@@ -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 =
|
package/ui/UiOverride/context.ts
CHANGED
|
@@ -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. */
|
package/ui/recipe/badgeRecipe.ts
CHANGED
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
import { recipe, tv } from "./factory";
|
|
2
2
|
|
|
3
|
-
/** 뱃지 look — 시맨틱 variant
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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]>;
|
package/ui/recipe/factory.ts
CHANGED
|
@@ -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-인자로 노출하고,
|
package/ui/recipe/inputRecipe.ts
CHANGED
|
@@ -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
|
|
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: "
|
|
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
|
-
|
|
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]>;
|