sh-ui-cli 0.48.0 → 0.50.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 (93) hide show
  1. package/data/changelog/versions.json +27 -0
  2. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.css.ts +131 -0
  4. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.css.ts +68 -0
  6. package/data/registry/react/components/badge/index.vanilla-extract.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.css.ts +71 -0
  8. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.css.ts +95 -0
  10. package/data/registry/react/components/button/index.vanilla-extract.tsx +45 -0
  11. package/data/registry/react/components/button/styles.css.ts +120 -0
  12. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +806 -0
  13. package/data/registry/react/components/calendar/styles.css.ts +250 -0
  14. package/data/registry/react/components/card/index.vanilla-extract.tsx +63 -0
  15. package/data/registry/react/components/card/styles.css.ts +88 -0
  16. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +430 -0
  17. package/data/registry/react/components/carousel/styles.css.ts +169 -0
  18. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +96 -0
  19. package/data/registry/react/components/checkbox/styles.css.ts +74 -0
  20. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +230 -0
  21. package/data/registry/react/components/code-editor/styles.css.ts +97 -0
  22. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +191 -0
  23. package/data/registry/react/components/code-panel/styles.css.ts +151 -0
  24. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +467 -0
  25. package/data/registry/react/components/color-picker/styles.css.ts +169 -0
  26. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +165 -0
  27. package/data/registry/react/components/combobox/styles.css.ts +174 -0
  28. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +251 -0
  29. package/data/registry/react/components/context-menu/styles.css.ts +167 -0
  30. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +520 -0
  31. package/data/registry/react/components/date-picker/styles.css.ts +111 -0
  32. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +95 -0
  33. package/data/registry/react/components/dialog/styles.css.ts +140 -0
  34. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +255 -0
  35. package/data/registry/react/components/dropdown-menu/styles.css.ts +175 -0
  36. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +487 -0
  37. package/data/registry/react/components/file-upload/styles.css.ts +193 -0
  38. package/data/registry/react/components/form/index.vanilla-extract.tsx +61 -0
  39. package/data/registry/react/components/form/styles.css.ts +56 -0
  40. package/data/registry/react/components/header/index.vanilla-extract.tsx +805 -0
  41. package/data/registry/react/components/header/styles.css.ts +413 -0
  42. package/data/registry/react/components/input/index.vanilla-extract.tsx +425 -0
  43. package/data/registry/react/components/input/styles.css.ts +202 -0
  44. package/data/registry/react/components/label/index.vanilla-extract.tsx +52 -0
  45. package/data/registry/react/components/label/styles.css.ts +141 -0
  46. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +119 -0
  47. package/data/registry/react/components/markdown-editor/styles.css.ts +231 -0
  48. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +32 -0
  49. package/data/registry/react/components/menubar/styles.css.ts +53 -0
  50. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +148 -0
  51. package/data/registry/react/components/numeric-input/styles.css.ts +65 -0
  52. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +174 -0
  53. package/data/registry/react/components/page-toc/styles.css.ts +97 -0
  54. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +269 -0
  55. package/data/registry/react/components/pagination/styles.css.ts +113 -0
  56. package/data/registry/react/components/popover/index.vanilla-extract.tsx +113 -0
  57. package/data/registry/react/components/popover/styles.css.ts +78 -0
  58. package/data/registry/react/components/progress/index.vanilla-extract.tsx +54 -0
  59. package/data/registry/react/components/progress/styles.css.ts +53 -0
  60. package/data/registry/react/components/radio/index.vanilla-extract.tsx +65 -0
  61. package/data/registry/react/components/radio/styles.css.ts +79 -0
  62. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +348 -0
  63. package/data/registry/react/components/rich-text-editor/styles.css.ts +243 -0
  64. package/data/registry/react/components/select/index.vanilla-extract.tsx +234 -0
  65. package/data/registry/react/components/select/styles.css.ts +225 -0
  66. package/data/registry/react/components/separator/index.vanilla-extract.tsx +46 -0
  67. package/data/registry/react/components/separator/styles.css.ts +24 -0
  68. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +1067 -0
  69. package/data/registry/react/components/sidebar/styles.css.ts +578 -0
  70. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +22 -0
  71. package/data/registry/react/components/skeleton/styles.css.ts +30 -0
  72. package/data/registry/react/components/slider/index.vanilla-extract.tsx +298 -0
  73. package/data/registry/react/components/slider/styles.css.ts +75 -0
  74. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +38 -0
  75. package/data/registry/react/components/spinner/styles.css.ts +60 -0
  76. package/data/registry/react/components/switch/index.vanilla-extract.tsx +39 -0
  77. package/data/registry/react/components/switch/styles.css.ts +87 -0
  78. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +91 -0
  79. package/data/registry/react/components/tabs/styles.css.ts +145 -0
  80. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +23 -0
  81. package/data/registry/react/components/textarea/styles.css.ts +55 -0
  82. package/data/registry/react/components/toast/index.vanilla-extract.tsx +258 -0
  83. package/data/registry/react/components/toast/styles.css.ts +307 -0
  84. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +131 -0
  85. package/data/registry/react/components/toggle/styles.css.ts +109 -0
  86. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +83 -0
  87. package/data/registry/react/components/tooltip/styles.css.ts +59 -0
  88. package/data/registry/react/peer-versions.json +1 -0
  89. package/data/registry/react/registry.json +922 -42
  90. package/data/tokens/build.mjs +3 -0
  91. package/package.json +1 -1
  92. package/src/api.d.ts +4 -3
  93. package/src/constants.js +4 -3
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { byKey, numericInput, numericInputInput, numericInputUnit } from "./styles.css";
5
+
6
+
7
+ import { cn } from "@SH_UI_UTILS@";
8
+ export interface NumericInputProps
9
+ extends Omit<
10
+ React.InputHTMLAttributes<HTMLInputElement>,
11
+ "value" | "defaultValue" | "onChange" | "type" | "min" | "max" | "step"
12
+ > {
13
+ /** 제어 모드 값. */
14
+ value?: number;
15
+ /** 비제어 모드 초기값. */
16
+ defaultValue?: number;
17
+ /** 값 변경 콜백. min/max 범위로 자동 clamp 된 값이 전달된다. */
18
+ onValueChange?: (value: number) => void;
19
+ /** 허용 최솟값. 입력값이 이보다 작으면 자동 clamp. */
20
+ min?: number;
21
+ /** 허용 최댓값. 입력값이 이보다 크면 자동 clamp. */
22
+ max?: number;
23
+ /** 화살표 키 step 폭. 디폴트 1. */
24
+ step?: number;
25
+ /** 값 우측에 부착할 단위 표시 (px / ms / % / ° 등). */
26
+ unit?: React.ReactNode;
27
+ }
28
+
29
+ /**
30
+ * 슬라이더 동반·토큰 편집 등 컴팩트 컨텍스트에 적합한 숫자 입력.
31
+ *
32
+ * 구현 특이점:
33
+ * - `type="text"` + `inputMode="decimal"` — type=number 가 Chrome 에서 select() 와
34
+ * selectionStart/End 를 지원하지 않아 "0 위에 2 타이핑 → 02" 회귀가 발생함.
35
+ * text 로 바꾸고 우리가 직접 숫자 검증/클램프.
36
+ * - 내부 buffer state — "-", "1.", "" 같이 입력 중간 transient 상태 허용. 유효한
37
+ * 숫자가 되는 순간 onValueChange 즉시 호출. 포커스 잃을 때 정규화.
38
+ * - focus 시 setTimeout(0) → select() — mouseup 의 커서 재배치 이후에 selection
39
+ * 적용되도록.
40
+ * - ArrowUp/Down 으로 step 조정, Enter 로 blur(commit).
41
+ *
42
+ * 일반 폼 입력에는 `Input` / `NumberInput` 사용 권장.
43
+ */
44
+ export const NumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
45
+ (
46
+ {
47
+ value,
48
+ defaultValue,
49
+ onValueChange,
50
+ min,
51
+ max,
52
+ step = 1,
53
+ unit,
54
+ className,
55
+ onFocus,
56
+ onBlur,
57
+ onKeyDown,
58
+ ...props
59
+ },
60
+ ref,
61
+ ) => {
62
+ const isControlled = value !== undefined;
63
+ const [internal, setInternal] = React.useState<number>(defaultValue ?? 0);
64
+ const current = isControlled ? value! : internal;
65
+
66
+ const [buffer, setBuffer] = React.useState<string>(() => String(current));
67
+ const focusedRef = React.useRef(false);
68
+
69
+ // 포커스 잡지 않은 동안 외부 value 변경되면 buffer 동기화.
70
+ React.useEffect(() => {
71
+ if (!focusedRef.current) setBuffer(String(current));
72
+ }, [current]);
73
+
74
+ const clamp = (n: number) => {
75
+ let v = n;
76
+ if (min !== undefined && v < min) v = min;
77
+ if (max !== undefined && v > max) v = max;
78
+ return v;
79
+ };
80
+
81
+ const commit = (n: number): number => {
82
+ const c = clamp(n);
83
+ if (!isControlled) setInternal(c);
84
+ onValueChange?.(c);
85
+ return c;
86
+ };
87
+
88
+ return (
89
+ <span className={numericInput}>
90
+ <input
91
+ ref={ref}
92
+ type="text"
93
+ inputMode="decimal"
94
+ className={cn(numericInputInput, className)}
95
+ value={buffer}
96
+ onChange={(e) => {
97
+ const raw = e.target.value;
98
+ setBuffer(raw);
99
+ // 입력 중간 상태("", "-", ".", "-.") 는 commit 안 함 — 사용자 타이핑 흐름 유지.
100
+ if (raw === "" || raw === "-" || raw === "." || raw === "-.") return;
101
+ const n = Number(raw);
102
+ if (Number.isFinite(n)) commit(n);
103
+ }}
104
+ onFocus={(e) => {
105
+ focusedRef.current = true;
106
+ const t = e.currentTarget;
107
+ // setTimeout 0 로 미뤄야 mouseup 의 커서 재배치 이후에 select 가 적용됨.
108
+ setTimeout(() => t.select(), 0);
109
+ onFocus?.(e);
110
+ }}
111
+ onBlur={(e) => {
112
+ focusedRef.current = false;
113
+ const n = Number(buffer);
114
+ if (buffer !== "" && Number.isFinite(n)) {
115
+ const c = commit(n);
116
+ setBuffer(String(c));
117
+ } else {
118
+ // 비어있거나 NaN — 마지막 유효 값으로 복원
119
+ setBuffer(String(current));
120
+ }
121
+ onBlur?.(e);
122
+ }}
123
+ onKeyDown={(e) => {
124
+ if (e.key === "ArrowUp") {
125
+ e.preventDefault();
126
+ const next = commit(current + step);
127
+ setBuffer(String(next));
128
+ } else if (e.key === "ArrowDown") {
129
+ e.preventDefault();
130
+ const next = commit(current - step);
131
+ setBuffer(String(next));
132
+ } else if (e.key === "Enter") {
133
+ e.currentTarget.blur();
134
+ }
135
+ onKeyDown?.(e);
136
+ }}
137
+ {...props}
138
+ />
139
+ {unit !== undefined && unit !== "" && (
140
+ <span className={numericInputUnit} aria-hidden>
141
+ {unit}
142
+ </span>
143
+ )}
144
+ </span>
145
+ );
146
+ },
147
+ );
148
+ NumericInput.displayName = "NumericInput";
@@ -0,0 +1,65 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const numericInput = style({
4
+ display: "inline-flex",
5
+ alignItems: "baseline",
6
+ gap: "2px",
7
+ minWidth: "3rem",
8
+ justifyContent: "flex-end",
9
+ });
10
+
11
+ export const numericInputInput = style({
12
+ width: "2.5rem",
13
+ padding: "2px 4px",
14
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
15
+ fontSize: "var(--text-xs)",
16
+ lineHeight: 1.2,
17
+ textAlign: "right",
18
+ border: "1px solid transparent",
19
+ borderRadius: "calc(var(--radius) - 4px)",
20
+ background: "transparent",
21
+ color: "var(--foreground)",
22
+ appearance: "textfield",
23
+ MozAppearance: "textfield",
24
+ transition: "border-color var(--duration-fast) var(--ease-standard),\n background-color var(--duration-fast) var(--ease-standard)",
25
+ selectors: {
26
+ "&::-webkit-inner-spin-button": {
27
+ WebkitAppearance: "none",
28
+ margin: 0,
29
+ },
30
+ "&::-webkit-outer-spin-button": {
31
+ WebkitAppearance: "none",
32
+ margin: 0,
33
+ },
34
+ "&:hover:not(:disabled):not(:focus)": {
35
+ borderColor: "var(--border)",
36
+ },
37
+ "&:focus": {
38
+ outline: "none",
39
+ borderColor: "var(--foreground)",
40
+ background: "var(--background)",
41
+ },
42
+ "&:focus-visible": {
43
+ outline: "none",
44
+ borderColor: "var(--foreground)",
45
+ background: "var(--background)",
46
+ },
47
+ "&:disabled": {
48
+ cursor: "not-allowed",
49
+ opacity: "var(--opacity-disabled)",
50
+ },
51
+ },
52
+ });
53
+
54
+ export const numericInputUnit = style({
55
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
56
+ fontSize: "var(--text-xs)",
57
+ color: "var(--foreground-muted)",
58
+ });
59
+
60
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
61
+ export const byKey: Record<string, string> = {
62
+ "numeric-input": numericInput,
63
+ "numeric-input__input": numericInputInput,
64
+ "numeric-input__unit": numericInputUnit,
65
+ };
@@ -0,0 +1,174 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "@SH_UI_UTILS@";
5
+ import { byKey, pageToc, pageTocLabel, pageTocList, pageTocLink } from "./styles.css";
6
+
7
+ export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
8
+
9
+ export interface PageTOCProps {
10
+ /**
11
+ * 스캔할 컨테이너 selector. 기본 `"main"`.
12
+ */
13
+ containerSelector?: string;
14
+ /**
15
+ * 외부 신호로 TOC 재스캔. Next.js 사용 시 `usePathname()` 결과를 그대로 전달하면
16
+ * 라우트 변경 때마다 자동 갱신. 같은 페이지 안에서 헤딩이 동적으로 바뀌면 이 값을 갱신.
17
+ */
18
+ routeKey?: string;
19
+ /**
20
+ * sticky 헤더 아래로 헤딩이 가려지지 않도록 띄울 거리(rem). `scroll-margin-top` 으로 적용.
21
+ * @default 5
22
+ */
23
+ headerOffsetRem?: number;
24
+ /**
25
+ * 라벨 텍스트.
26
+ * @default "On this page"
27
+ */
28
+ label?: React.ReactNode;
29
+ /**
30
+ * 수집할 헤딩 레벨.
31
+ * @default ["h2", "h3"]
32
+ */
33
+ levels?: HeadingLevel[];
34
+ /**
35
+ * 제외할 헤딩 selector. 컨테이너 안에서 이 selector 의 자손인 헤딩은 무시.
36
+ * 데모 미리보기·중첩 위젯 등을 TOC 에서 빼고 싶을 때 사용.
37
+ */
38
+ excludeSelector?: string;
39
+ /** 추가 클래스. */
40
+ className?: string;
41
+ }
42
+
43
+ const slugify = (text: string): string =>
44
+ text
45
+ .trim()
46
+ .toLowerCase()
47
+ .replace(/[^\w\s가-힣-]/g, "")
48
+ .replace(/\s+/g, "-");
49
+
50
+ interface TocItem {
51
+ id: string;
52
+ text: string;
53
+ level: HeadingLevel;
54
+ }
55
+
56
+
57
+ /**
58
+ * 페이지 내 자동 목차 (On this page).
59
+ *
60
+ * 컨테이너 안의 지정한 헤딩 레벨을 스캔해 자동 slugify · id 부여 · `IntersectionObserver` 로
61
+ * 현재 보이는 섹션을 active 표시 · 클릭 시 smooth scroll. 라우터 비종속 — `routeKey` 를
62
+ * 외부에서 갱신하면 재스캔된다.
63
+ */
64
+ export function PageTOC({
65
+ containerSelector = "main",
66
+ routeKey,
67
+ headerOffsetRem = 5,
68
+ label = "On this page",
69
+ levels = ["h2", "h3"],
70
+ excludeSelector,
71
+ className,
72
+ }: PageTOCProps) {
73
+ const [items, setItems] = React.useState<TocItem[]>([]);
74
+ const [activeId, setActiveId] = React.useState<string | null>(null);
75
+
76
+ // levels 가 inline 배열(["h2", "h3"]) 로 전달되면 매 렌더마다 새 참조라 useEffect 가
77
+ // 무한 루프에 빠짐. 내용 기반 안정 키로 비교하고, 효과 안에서는 ref 로 최신 값 사용.
78
+ const levelsKey = levels.join(",");
79
+ const levelsRef = React.useRef(levels);
80
+ levelsRef.current = levels;
81
+
82
+ React.useEffect(() => {
83
+ const container = document.querySelector(containerSelector);
84
+ if (!container) {
85
+ setItems([]);
86
+ return;
87
+ }
88
+
89
+ const headingSelector = levelsRef.current.join(", ");
90
+ let headings = Array.from(
91
+ container.querySelectorAll<HTMLHeadingElement>(headingSelector),
92
+ );
93
+ if (excludeSelector) {
94
+ headings = headings.filter((h) => !h.closest(excludeSelector));
95
+ }
96
+
97
+ const usedIds = new Set<string>();
98
+ const collected: TocItem[] = headings.map((h) => {
99
+ const text = h.textContent?.trim() ?? "";
100
+ let id = h.id || slugify(text);
101
+ let suffix = 2;
102
+ const base = id;
103
+ while (!id || usedIds.has(id)) {
104
+ id = `${base}-${suffix++}`;
105
+ }
106
+ usedIds.add(id);
107
+ if (!h.id) h.id = id;
108
+ h.style.scrollMarginTop = `${headerOffsetRem}rem`;
109
+ const level = h.tagName.toLowerCase() as HeadingLevel;
110
+ return { id, text, level };
111
+ });
112
+
113
+ setItems(collected);
114
+
115
+ if (collected.length === 0) return;
116
+
117
+ const remInPx =
118
+ parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
119
+ const topOffsetPx = Math.round(headerOffsetRem * remInPx);
120
+
121
+ const observer = new IntersectionObserver(
122
+ (entries) => {
123
+ const visible = entries
124
+ .filter((e) => e.isIntersecting)
125
+ .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
126
+ if (visible.length > 0) {
127
+ setActiveId(visible[0].target.id);
128
+ }
129
+ },
130
+ {
131
+ rootMargin: `-${topOffsetPx}px 0px -70% 0px`,
132
+ threshold: 0,
133
+ },
134
+ );
135
+
136
+ headings.forEach((h) => observer.observe(h));
137
+ return () => observer.disconnect();
138
+ }, [containerSelector, headerOffsetRem, levelsKey, excludeSelector, routeKey]);
139
+
140
+ const handleClick = (event: React.MouseEvent<HTMLAnchorElement>, id: string) => {
141
+ event.preventDefault();
142
+ const el = document.getElementById(id);
143
+ if (!el) return;
144
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
145
+ history.replaceState(null, "", `#${id}`);
146
+ setActiveId(id);
147
+ };
148
+
149
+ if (items.length === 0) return null;
150
+
151
+ return (
152
+ <nav
153
+ className={cn(pageToc, className)}
154
+ aria-label={typeof label === "string" ? label : "목차"}
155
+ >
156
+ <div className={pageTocLabel}>{label}</div>
157
+ <ul className={pageTocList}>
158
+ {items.map((item) => (
159
+ <li key={item.id} data-level={item.level.replace("h", "")}>
160
+ <a
161
+ href={`#${item.id}`}
162
+ onClick={(e) => handleClick(e, item.id)}
163
+ className={pageTocLink}
164
+ data-active={activeId === item.id ? "true" : undefined}
165
+ aria-current={activeId === item.id ? "true" : undefined}
166
+ >
167
+ {item.text}
168
+ </a>
169
+ </li>
170
+ ))}
171
+ </ul>
172
+ </nav>
173
+ );
174
+ }
@@ -0,0 +1,97 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const pageToc = style({
4
+ position: "fixed",
5
+ top: "5rem",
6
+ right: "1.5rem",
7
+ width: "14rem",
8
+ maxHeight: "calc(100vh - 7rem)",
9
+ overflowY: "auto",
10
+ padding: "0.75rem 0.5rem 0.75rem 1rem",
11
+ borderLeft: "1px solid var(--border)",
12
+ fontSize: "0.8125rem",
13
+ zIndex: 5,
14
+ "@media": {
15
+ "(max-width: 80rem)": {
16
+ display: "none",
17
+ },
18
+ },
19
+ });
20
+
21
+ export const pageTocLabel = style({
22
+ fontWeight: 600,
23
+ fontSize: "0.75rem",
24
+ color: "var(--foreground-muted)",
25
+ textTransform: "uppercase",
26
+ letterSpacing: "0.04em",
27
+ marginBottom: "0.5rem",
28
+ });
29
+
30
+ export const pageTocList = style({
31
+ listStyle: "none",
32
+ margin: 0,
33
+ padding: 0,
34
+ display: "flex",
35
+ flexDirection: "column",
36
+ gap: "0.125rem",
37
+ selectors: {
38
+ [`& > li[data-level="3"] ${pageTocLink}`]: {
39
+ paddingLeft: "1.25rem",
40
+ fontSize: "0.8125em",
41
+ color: "var(--foreground-subtle, var(--foreground-muted))",
42
+ },
43
+ [`& > li[data-level="4"] ${pageTocLink}`]: {
44
+ paddingLeft: "1.25rem",
45
+ fontSize: "0.8125em",
46
+ color: "var(--foreground-subtle, var(--foreground-muted))",
47
+ },
48
+ [`& > li[data-level="5"] ${pageTocLink}`]: {
49
+ paddingLeft: "2rem",
50
+ fontSize: "0.75em",
51
+ color: "var(--foreground-subtle, var(--foreground-muted))",
52
+ },
53
+ [`& > li[data-level="6"] ${pageTocLink}`]: {
54
+ paddingLeft: "2rem",
55
+ fontSize: "0.75em",
56
+ color: "var(--foreground-subtle, var(--foreground-muted))",
57
+ },
58
+ },
59
+ });
60
+
61
+ export const pageTocLink = style({
62
+ display: "block",
63
+ padding: "0.25rem 0.5rem",
64
+ borderRadius: "calc(var(--radius) - 4px)",
65
+ color: "var(--foreground-muted)",
66
+ textDecoration: "none",
67
+ lineHeight: 1.4,
68
+ transition: "color var(--duration-fast), background-color var(--duration-fast)",
69
+ selectors: {
70
+ "&:hover": {
71
+ color: "var(--foreground)",
72
+ background: "var(--background-subtle)",
73
+ },
74
+ "&:focus-visible": {
75
+ outline: "var(--border-width-strong) solid var(--foreground)",
76
+ outlineOffset: "2px",
77
+ },
78
+ "&[data-active="true"]": {
79
+ color: "var(--foreground)",
80
+ fontWeight: 600,
81
+ background: "var(--background-subtle)",
82
+ },
83
+ },
84
+ "@media": {
85
+ "(prefers-reduced-motion: reduce)": {
86
+ transition: "none",
87
+ },
88
+ },
89
+ });
90
+
91
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
92
+ export const byKey: Record<string, string> = {
93
+ "page-toc": pageToc,
94
+ "page-toc__label": pageTocLabel,
95
+ "page-toc__list": pageTocList,
96
+ "page-toc__link": pageTocLink,
97
+ };