sh-ui-cli 0.52.0 → 0.52.2

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 (89) hide show
  1. package/data/changelog/versions.json +25 -0
  2. package/data/registry/react/components/_smoke/vanilla-extract.test.ts +33 -0
  3. package/data/registry/react/components/input/styles.css.ts +6 -6
  4. package/data/registry/react/registry.json +35 -852
  5. package/package.json +2 -2
  6. package/src/api.d.ts +3 -4
  7. package/src/constants.js +9 -5
  8. package/src/create/plugins/pluginSchema.js +5 -3
  9. package/src/mcp.mjs +4 -3
  10. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +0 -97
  11. package/data/registry/react/components/accordion/styles.css.ts +0 -131
  12. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +0 -73
  13. package/data/registry/react/components/avatar/styles.css.ts +0 -68
  14. package/data/registry/react/components/badge/index.vanilla-extract.tsx +0 -40
  15. package/data/registry/react/components/badge/styles.css.ts +0 -71
  16. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +0 -152
  17. package/data/registry/react/components/breadcrumb/styles.css.ts +0 -95
  18. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +0 -806
  19. package/data/registry/react/components/calendar/styles.css.ts +0 -250
  20. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +0 -430
  21. package/data/registry/react/components/carousel/styles.css.ts +0 -169
  22. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +0 -96
  23. package/data/registry/react/components/checkbox/styles.css.ts +0 -74
  24. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +0 -230
  25. package/data/registry/react/components/code-editor/styles.css.ts +0 -97
  26. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +0 -191
  27. package/data/registry/react/components/code-panel/styles.css.ts +0 -151
  28. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +0 -467
  29. package/data/registry/react/components/color-picker/styles.css.ts +0 -169
  30. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +0 -165
  31. package/data/registry/react/components/combobox/styles.css.ts +0 -174
  32. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +0 -251
  33. package/data/registry/react/components/context-menu/styles.css.ts +0 -167
  34. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +0 -520
  35. package/data/registry/react/components/date-picker/styles.css.ts +0 -111
  36. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +0 -95
  37. package/data/registry/react/components/dialog/styles.css.ts +0 -140
  38. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +0 -255
  39. package/data/registry/react/components/dropdown-menu/styles.css.ts +0 -175
  40. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +0 -487
  41. package/data/registry/react/components/file-upload/styles.css.ts +0 -193
  42. package/data/registry/react/components/form/index.vanilla-extract.tsx +0 -61
  43. package/data/registry/react/components/form/styles.css.ts +0 -56
  44. package/data/registry/react/components/header/index.vanilla-extract.tsx +0 -805
  45. package/data/registry/react/components/header/styles.css.ts +0 -413
  46. package/data/registry/react/components/label/index.vanilla-extract.tsx +0 -52
  47. package/data/registry/react/components/label/styles.css.ts +0 -141
  48. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +0 -119
  49. package/data/registry/react/components/markdown-editor/styles.css.ts +0 -231
  50. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +0 -32
  51. package/data/registry/react/components/menubar/styles.css.ts +0 -53
  52. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +0 -148
  53. package/data/registry/react/components/numeric-input/styles.css.ts +0 -65
  54. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +0 -174
  55. package/data/registry/react/components/page-toc/styles.css.ts +0 -97
  56. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +0 -269
  57. package/data/registry/react/components/pagination/styles.css.ts +0 -113
  58. package/data/registry/react/components/popover/index.vanilla-extract.tsx +0 -113
  59. package/data/registry/react/components/popover/styles.css.ts +0 -78
  60. package/data/registry/react/components/progress/index.vanilla-extract.tsx +0 -54
  61. package/data/registry/react/components/progress/styles.css.ts +0 -53
  62. package/data/registry/react/components/radio/index.vanilla-extract.tsx +0 -65
  63. package/data/registry/react/components/radio/styles.css.ts +0 -79
  64. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +0 -348
  65. package/data/registry/react/components/rich-text-editor/styles.css.ts +0 -243
  66. package/data/registry/react/components/select/index.vanilla-extract.tsx +0 -234
  67. package/data/registry/react/components/select/styles.css.ts +0 -225
  68. package/data/registry/react/components/separator/index.vanilla-extract.tsx +0 -46
  69. package/data/registry/react/components/separator/styles.css.ts +0 -24
  70. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +0 -1067
  71. package/data/registry/react/components/sidebar/styles.css.ts +0 -578
  72. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +0 -22
  73. package/data/registry/react/components/skeleton/styles.css.ts +0 -30
  74. package/data/registry/react/components/slider/index.vanilla-extract.tsx +0 -298
  75. package/data/registry/react/components/slider/styles.css.ts +0 -75
  76. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +0 -38
  77. package/data/registry/react/components/spinner/styles.css.ts +0 -60
  78. package/data/registry/react/components/switch/index.vanilla-extract.tsx +0 -39
  79. package/data/registry/react/components/switch/styles.css.ts +0 -87
  80. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +0 -91
  81. package/data/registry/react/components/tabs/styles.css.ts +0 -145
  82. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +0 -23
  83. package/data/registry/react/components/textarea/styles.css.ts +0 -55
  84. package/data/registry/react/components/toast/index.vanilla-extract.tsx +0 -258
  85. package/data/registry/react/components/toast/styles.css.ts +0 -307
  86. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +0 -131
  87. package/data/registry/react/components/toggle/styles.css.ts +0 -109
  88. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +0 -83
  89. package/data/registry/react/components/tooltip/styles.css.ts +0 -59
@@ -1,174 +0,0 @@
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
- }
@@ -1,97 +0,0 @@
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
- };
@@ -1,269 +0,0 @@
1
- import * as React from "react";
2
- import { byKey, pagination, pagination__content, pagination__item, pagination__link, pagination__nav, pagination__ellipsis, pagination__sr } from "./styles.css";
3
-
4
-
5
- import { cn } from "@SH_UI_UTILS@";
6
- /* ───────── Pagination (nav) ─────────
7
- * 시맨틱: <nav aria-label="Pagination"><ul>...</ul></nav>.
8
- * 현재 페이지 링크에 aria-current="page"를 부여해 스크린리더가 위치를 읽게 한다.
9
- */
10
-
11
- /**
12
- * 페이지 분할 내비게이션의 시맨틱 컨테이너(`<nav aria-label="Pagination">`).
13
- * 자식 구조: PaginationContent > PaginationItem × n > PaginationLink/Previous/Next/Ellipsis.
14
- */
15
- export const Pagination = React.forwardRef<
16
- HTMLElement,
17
- React.HTMLAttributes<HTMLElement>
18
- >(function Pagination({ className, ...props }, ref) {
19
- return (
20
- <nav
21
- ref={ref}
22
- aria-label="Pagination"
23
- className={cn(pagination, className)}
24
- {...props}
25
- />
26
- );
27
- });
28
-
29
- /* ───────── Content (ul) ───────── */
30
-
31
- /** 페이지 항목들을 담는 정렬되지 않은 리스트(`<ul>`). */
32
- export const PaginationContent = React.forwardRef<
33
- HTMLUListElement,
34
- React.HTMLAttributes<HTMLUListElement>
35
- >(function PaginationContent({ className, ...props }, ref) {
36
- return (
37
- <ul
38
- ref={ref}
39
- className={cn(pagination__content, className)}
40
- {...props}
41
- />
42
- );
43
- });
44
-
45
- /* ───────── Item (li) ───────── */
46
-
47
- /** 한 페이지 슬롯(`<li>`). PaginationLink/Previous/Next/Ellipsis를 자식으로 둔다. */
48
- export const PaginationItem = React.forwardRef<
49
- HTMLLIElement,
50
- React.LiHTMLAttributes<HTMLLIElement>
51
- >(function PaginationItem({ className, ...props }, ref) {
52
- return (
53
- <li
54
- ref={ref}
55
- className={cn(pagination__item, className)}
56
- {...props}
57
- />
58
- );
59
- });
60
-
61
- /* ───────── Link ─────────
62
- * 숫자 페이지 링크. isActive일 때 aria-current="page" + 시각 강조.
63
- */
64
-
65
- export interface PaginationLinkProps
66
- extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
67
- /**
68
- * 현재 페이지 표시. `true`면 `aria-current="page"`가 자동 부여되고 시각적으로 강조된다.
69
- * @default false
70
- */
71
- isActive?: boolean;
72
- /**
73
- * 크기.
74
- * - `sm` — 컴팩트
75
- * - `md` — 일반 (기본)
76
- *
77
- * @default "md"
78
- */
79
- size?: "sm" | "md";
80
- }
81
-
82
- /**
83
- * 숫자 페이지 링크. `isActive`이면 `aria-current="page"`가 자동 부여되어
84
- * 스크린리더가 현재 위치를 읽는다.
85
- */
86
- export const PaginationLink = React.forwardRef<
87
- HTMLAnchorElement,
88
- PaginationLinkProps
89
- >(function PaginationLink(
90
- { className, isActive, size = "md", ...props },
91
- ref,
92
- ) {
93
- return (
94
- <a
95
- ref={ref}
96
- aria-current={isActive ? "page" : undefined}
97
- data-active={isActive ? "" : undefined}
98
- data-size={size}
99
- className={cn(pagination__link, className)}
100
- {...props}
101
- />
102
- );
103
- });
104
-
105
- /* ───────── Previous / Next ─────────
106
- * 아이콘 + 레이블. 레이블이 없는 아이콘 버튼에는 aria-label로 의미 전달.
107
- */
108
-
109
- /** "이전 페이지" 링크. 화살표 아이콘과 라벨이 함께 렌더되며 `aria-label`이 자동 부여된다. */
110
- export const PaginationPrevious = React.forwardRef<
111
- HTMLAnchorElement,
112
- PaginationLinkProps
113
- >(function PaginationPrevious({ className, children, ...props }, ref) {
114
- return (
115
- <PaginationLink
116
- ref={ref}
117
- aria-label="이전 페이지"
118
- className={cn(pagination__nav, className)}
119
- {...props}
120
- >
121
- <ChevronLeftIcon />
122
- {children ?? <span>이전</span>}
123
- </PaginationLink>
124
- );
125
- });
126
-
127
- /** "다음 페이지" 링크. */
128
- export const PaginationNext = React.forwardRef<
129
- HTMLAnchorElement,
130
- PaginationLinkProps
131
- >(function PaginationNext({ className, children, ...props }, ref) {
132
- return (
133
- <PaginationLink
134
- ref={ref}
135
- aria-label="다음 페이지"
136
- className={cn(pagination__nav, className)}
137
- {...props}
138
- >
139
- {children ?? <span>다음</span>}
140
- <ChevronRightIcon />
141
- </PaginationLink>
142
- );
143
- });
144
-
145
- /* ───────── Ellipsis — 생략 표시 ───────── */
146
-
147
- /** 페이지 사이 생략을 표현하는 점 3개. 시각만 표현하고 스크린리더에는 무시된다. */
148
- export const PaginationEllipsis = React.forwardRef<
149
- HTMLSpanElement,
150
- React.HTMLAttributes<HTMLSpanElement>
151
- >(function PaginationEllipsis({ className, ...props }, ref) {
152
- return (
153
- <span
154
- ref={ref}
155
- role="presentation"
156
- aria-hidden="true"
157
- className={cn(pagination__ellipsis, className)}
158
- {...props}
159
- >
160
- <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden>
161
- <circle cx="3" cy="8" r="1.25" />
162
- <circle cx="8" cy="8" r="1.25" />
163
- <circle cx="13" cy="8" r="1.25" />
164
- </svg>
165
- <span className={pagination__sr}>더 많은 페이지</span>
166
- </span>
167
- );
168
- });
169
-
170
- /* ───────── 페이지 범위 유틸 ─────────
171
- * 현재 페이지 주변 siblings개 + 양 끝 1개 + 필요한 곳에 "dots"를 넣어
172
- * 렌더할 토큰 배열을 돌려준다.
173
- *
174
- * 예: page=5, totalPages=10, siblings=1 → [1, "dots", 4, 5, 6, "dots", 10]
175
- *
176
- * page·totalPages는 1-based를 가정한다.
177
- */
178
-
179
- export type PaginationToken = number | "dots";
180
-
181
- /**
182
- * 1-based `page`/`totalPages`로부터 렌더할 토큰 배열을 만든다.
183
- * 양 끝과 현재 주변 `siblings`개를 보여주고, 끊긴 구간엔 `"dots"`를 넣어준다.
184
- *
185
- * @param args.page - 1-based 현재 페이지.
186
- * @param args.siblings - 현재 페이지 양옆에 보일 페이지 개수.
187
- * @returns 렌더 토큰 배열. 숫자 또는 문자열 `"dots"`로 구성.
188
- * @example
189
- * getPaginationRange({ page: 5, totalPages: 10, siblings: 1 })
190
- * // [1, "dots", 4, 5, 6, "dots", 10]
191
- */
192
- export function getPaginationRange({
193
- page,
194
- totalPages,
195
- siblings = 1,
196
- }: {
197
- /** 1-based 현재 페이지. */
198
- page: number;
199
- /** 전체 페이지 수. */
200
- totalPages: number;
201
- /**
202
- * 현재 페이지 양옆에 보일 페이지 개수.
203
- * @default 1
204
- */
205
- siblings?: number;
206
- }): PaginationToken[] {
207
- if (totalPages <= 0) return [];
208
-
209
- // 끝점 2개(첫/마지막) + 현재 주변 (2*siblings + 1) + dots 2개가 총 페이지 수보다 크거나 같으면
210
- // 생략 없이 전부 보여준다.
211
- const totalSlots = siblings * 2 + 5;
212
- if (totalPages <= totalSlots) {
213
- return range(1, totalPages);
214
- }
215
-
216
- const leftSibling = Math.max(page - siblings, 1);
217
- const rightSibling = Math.min(page + siblings, totalPages);
218
-
219
- const showLeftDots = leftSibling > 2;
220
- const showRightDots = rightSibling < totalPages - 1;
221
-
222
- // 왼쪽만 닫혀있음 → [1 ... right side]
223
- if (!showLeftDots && showRightDots) {
224
- const leftCount = 3 + 2 * siblings;
225
- return [...range(1, leftCount), "dots", totalPages];
226
- }
227
-
228
- // 오른쪽만 닫혀있음 → [1 ... left side]
229
- if (showLeftDots && !showRightDots) {
230
- const rightCount = 3 + 2 * siblings;
231
- return [1, "dots", ...range(totalPages - rightCount + 1, totalPages)];
232
- }
233
-
234
- // 양쪽 모두 생략
235
- return [1, "dots", ...range(leftSibling, rightSibling), "dots", totalPages];
236
- }
237
-
238
- function range(start: number, end: number): number[] {
239
- const length = end - start + 1;
240
- return Array.from({ length }, (_, i) => start + i);
241
- }
242
-
243
- function ChevronLeftIcon() {
244
- return (
245
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
246
- <path
247
- d="M10 4l-4 4 4 4"
248
- stroke="currentColor"
249
- strokeWidth="1.5"
250
- strokeLinecap="round"
251
- strokeLinejoin="round"
252
- />
253
- </svg>
254
- );
255
- }
256
-
257
- function ChevronRightIcon() {
258
- return (
259
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
260
- <path
261
- d="M6 4l4 4-4 4"
262
- stroke="currentColor"
263
- strokeWidth="1.5"
264
- strokeLinecap="round"
265
- strokeLinejoin="round"
266
- />
267
- </svg>
268
- );
269
- }
@@ -1,113 +0,0 @@
1
- import { style } from "@vanilla-extract/css";
2
-
3
- export const pagination = style({
4
- display: "flex",
5
- justifyContent: "center",
6
- fontSize: "var(--text-sm)",
7
- color: "var(--foreground)",
8
- });
9
-
10
- export const pagination__content = style({
11
- display: "flex",
12
- flexWrap: "wrap",
13
- alignItems: "center",
14
- gap: "0.25rem",
15
- margin: 0,
16
- padding: 0,
17
- listStyle: "none",
18
- });
19
-
20
- export const pagination__item = style({
21
- display: "inline-flex",
22
- alignItems: "center",
23
- });
24
-
25
- export const pagination__link = style({
26
- display: "inline-flex",
27
- alignItems: "center",
28
- justifyContent: "center",
29
- gap: "0.375rem",
30
- minWidth: "2.25rem",
31
- height: "2.25rem",
32
- padding: "0 0.75rem",
33
- borderRadius: "calc(var(--radius) - 2px)",
34
- border: "var(--border-width) solid transparent",
35
- background: "transparent",
36
- color: "var(--foreground)",
37
- textDecoration: "none",
38
- transition: "background-color var(--duration-fast),\n border-color var(--duration-fast),\n color var(--duration-fast)",
39
- cursor: "pointer",
40
- userSelect: "none",
41
- selectors: {
42
- "&[data-size="sm"]": {
43
- minWidth: "2rem",
44
- height: "2rem",
45
- padding: "0 0.5rem",
46
- },
47
- "&:hover": {
48
- background: "var(--background-muted)",
49
- },
50
- "&:focus-visible": {
51
- outline: "var(--border-width-strong) solid var(--foreground)",
52
- outlineOffset: "2px",
53
- },
54
- "&[data-active]": {
55
- background: "var(--foreground)",
56
- color: "var(--background)",
57
- fontWeight: "var(--weight-medium)",
58
- },
59
- "&[data-active]:hover": {
60
- background: "var(--foreground)",
61
- opacity: 0.9,
62
- },
63
- "&[aria-disabled="true"]": {
64
- pointerEvents: "none",
65
- opacity: 0.45,
66
- },
67
- "&[data-disabled]": {
68
- pointerEvents: "none",
69
- opacity: 0.45,
70
- },
71
- },
72
- "@media": {
73
- "(prefers-reduced-motion: reduce)": {
74
- transition: "none",
75
- },
76
- },
77
- });
78
-
79
- export const pagination__nav = style({
80
- padding: "0 0.625rem",
81
- });
82
-
83
- export const pagination__ellipsis = style({
84
- display: "inline-flex",
85
- alignItems: "center",
86
- justifyContent: "center",
87
- width: "2.25rem",
88
- height: "2.25rem",
89
- color: "var(--foreground-muted)",
90
- });
91
-
92
- export const pagination__sr = style({
93
- position: "absolute",
94
- width: "1px",
95
- height: "1px",
96
- padding: 0,
97
- margin: "-1px",
98
- overflow: "hidden",
99
- clip: "rect(0, 0, 0, 0)",
100
- whiteSpace: "nowrap",
101
- border: 0,
102
- });
103
-
104
- /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
105
- export const byKey: Record<string, string> = {
106
- "pagination": pagination,
107
- "pagination__content": pagination__content,
108
- "pagination__item": pagination__item,
109
- "pagination__link": pagination__link,
110
- "pagination__nav": pagination__nav,
111
- "pagination__ellipsis": pagination__ellipsis,
112
- "pagination__sr": pagination__sr,
113
- };