@pylerai/design-system 2.4.0 → 2.5.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 (43) hide show
  1. package/README.md +2 -1
  2. package/dist/components/ui/badge.d.ts +3 -0
  3. package/dist/components/ui/badge.js +28 -20
  4. package/dist/components/ui/button.d.ts +1 -1
  5. package/dist/components/ui/button.js +38 -25
  6. package/dist/components/ui/dropdown.js +89 -86
  7. package/dist/components/ui/input.js +30 -30
  8. package/dist/components/ui/label.d.ts +1 -1
  9. package/dist/components/ui/label.js +11 -10
  10. package/dist/components/ui/media-slider.d.ts +20 -0
  11. package/dist/components/ui/media-slider.js +90 -0
  12. package/dist/components/ui/progress-bar.d.ts +21 -0
  13. package/dist/components/ui/progress-bar.js +45 -0
  14. package/dist/components/ui/search-field.js +45 -43
  15. package/dist/components/ui/table.js +33 -33
  16. package/dist/components/ui/tabs.js +24 -24
  17. package/dist/components/ui/timeline-tracks.d.ts +66 -0
  18. package/dist/components/ui/timeline-tracks.js +80 -0
  19. package/dist/components/ui/timeline.d.ts +31 -0
  20. package/dist/components/ui/timeline.js +119 -0
  21. package/dist/components/ui/toast.js +5 -5
  22. package/dist/components/ui/video-player.d.ts +29 -0
  23. package/dist/components/ui/video-player.js +174 -0
  24. package/dist/index.d.ts +7 -1
  25. package/dist/index.js +38 -32
  26. package/dist/lib/field-box.d.ts +13 -0
  27. package/dist/lib/field-box.js +11 -0
  28. package/dist/lib/format-time.d.ts +5 -0
  29. package/dist/lib/format-time.js +8 -0
  30. package/dist/lib/icons.d.ts +6 -0
  31. package/dist/lib/icons.js +68 -44
  32. package/dist/lib/utils.d.ts +1 -0
  33. package/dist/lib/utils.js +14 -7
  34. package/dist/style.css +1 -1
  35. package/dist/tokens/pyler-light.d.ts +12 -0
  36. package/dist/tokens/pyler-light.js +27 -23
  37. package/dist/tokens/shadow-radius.d.ts +3 -3
  38. package/dist/tokens/shadow-radius.js +3 -3
  39. package/dist/tokens/typography.d.ts +8 -2
  40. package/dist/tokens/typography.js +6 -0
  41. package/package.json +1 -1
  42. package/pyler-tailwind-colors.cjs +4 -0
  43. package/pyler-typography.cjs +7 -0
@@ -7,27 +7,27 @@ import { Checkbox as C } from "./checkbox.js";
7
7
  const S = {
8
8
  borderRight: "3px solid var(--ds-line-normal)"
9
9
  }, j = f.forwardRef(
10
- ({ className: l, ...r }, n) => /* @__PURE__ */ a(
10
+ ({ className: i, ...r }, n) => /* @__PURE__ */ a(
11
11
  "div",
12
12
  {
13
13
  ref: n,
14
14
  role: "table",
15
- className: g("w-full overflow-x-auto", l),
15
+ className: g("w-full overflow-x-auto", i),
16
16
  ...r
17
17
  }
18
18
  )
19
19
  );
20
20
  j.displayName = "TableRoot";
21
21
  const L = f.forwardRef(
22
- (l, r) => /* @__PURE__ */ a("div", { ref: r, role: "rowgroup", ...l })
22
+ (i, r) => /* @__PURE__ */ a("div", { ref: r, role: "rowgroup", ...i })
23
23
  );
24
24
  L.displayName = "TableHead";
25
25
  const R = f.forwardRef(
26
- (l, r) => /* @__PURE__ */ a("div", { ref: r, role: "rowgroup", ...l })
26
+ (i, r) => /* @__PURE__ */ a("div", { ref: r, role: "rowgroup", ...i })
27
27
  );
28
28
  R.displayName = "TableBody";
29
29
  const N = f.forwardRef(
30
- ({ className: l, hoverable: r = !0, selected: n, ...i }, s) => /* @__PURE__ */ a(
30
+ ({ className: i, hoverable: r = !0, selected: n, ...l }, s) => /* @__PURE__ */ a(
31
31
  "div",
32
32
  {
33
33
  ref: s,
@@ -37,43 +37,43 @@ const N = f.forwardRef(
37
37
  "flex items-stretch w-full",
38
38
  r && "hover:bg-ds-surface-strong",
39
39
  n && "bg-ds-surface-normal",
40
- l
40
+ i
41
41
  ),
42
- ...i
42
+ ...l
43
43
  }
44
44
  )
45
45
  );
46
46
  N.displayName = "TableRow";
47
47
  const k = f.forwardRef(
48
48
  ({
49
- className: l,
49
+ className: i,
50
50
  children: r,
51
51
  sortable: n,
52
- sortDirection: i,
52
+ sortDirection: l,
53
53
  onSort: s,
54
54
  info: d,
55
55
  fixedLine: m,
56
56
  align: h = "left",
57
- width: p,
58
- style: b,
57
+ width: b,
58
+ style: p,
59
59
  ...y
60
60
  }, c) => {
61
- const v = n && i != null;
61
+ const v = n && l != null;
62
62
  return /* @__PURE__ */ x(
63
63
  "div",
64
64
  {
65
65
  ref: c,
66
66
  role: "columnheader",
67
67
  tabIndex: n ? 0 : void 0,
68
- "aria-sort": n ? i === "asc" ? "ascending" : i === "desc" ? "descending" : "none" : void 0,
68
+ "aria-sort": n ? l === "asc" ? "ascending" : l === "desc" ? "descending" : "none" : void 0,
69
69
  className: g(
70
- "flex h-10 items-center gap-1 border-t border-ds-line-normal bg-ds-surface-normal px-3 py-2.5",
71
- p ? "shrink-0" : "flex-1 min-w-0",
70
+ "flex h-10 items-center gap-1 border-b border-ds-line-normal bg-ds-surface-alternative px-3 py-2.5",
71
+ b ? "shrink-0" : "flex-1 min-w-0",
72
72
  h === "right" && "justify-end",
73
73
  n && "cursor-pointer select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ds-primary-blue-normal",
74
- l
74
+ i
75
75
  ),
76
- style: { width: p, ...m ? S : {}, ...b },
76
+ style: { width: b, ...m ? S : {}, ...p },
77
77
  onClick: n ? s : void 0,
78
78
  onKeyDown: n ? (u) => {
79
79
  (u.key === "Enter" || u.key === " ") && (u.preventDefault(), s?.());
@@ -88,7 +88,7 @@ const k = f.forwardRef(
88
88
  className: g(
89
89
  "shrink-0 transition-transform duration-150",
90
90
  v ? "text-ds-text-strong" : "text-ds-text-disabled",
91
- i === "asc" && "rotate-180"
91
+ l === "asc" && "rotate-180"
92
92
  )
93
93
  }
94
94
  )
@@ -99,18 +99,18 @@ const k = f.forwardRef(
99
99
  );
100
100
  k.displayName = "TableHeaderCell";
101
101
  const T = f.forwardRef(
102
- ({ className: l, align: r = "left", fixedLine: n, width: i, style: s, children: d, ...m }, h) => /* @__PURE__ */ a(
102
+ ({ className: i, align: r = "left", fixedLine: n, width: l, style: s, children: d, ...m }, h) => /* @__PURE__ */ a(
103
103
  "div",
104
104
  {
105
105
  ref: h,
106
106
  role: "cell",
107
107
  className: g(
108
108
  "flex items-center border-b border-ds-line-normal px-3 py-2",
109
- i ? "shrink-0" : "flex-1 min-w-0",
109
+ l ? "shrink-0" : "flex-1 min-w-0",
110
110
  r === "right" && "justify-end",
111
- l
111
+ i
112
112
  ),
113
- style: { width: i, ...n ? S : {}, ...s },
113
+ style: { width: l, ...n ? S : {}, ...s },
114
114
  ...m,
115
115
  children: d
116
116
  }
@@ -118,21 +118,21 @@ const T = f.forwardRef(
118
118
  );
119
119
  T.displayName = "TableCell";
120
120
  function Y({
121
- columns: l,
121
+ columns: i,
122
122
  data: r,
123
123
  sortKey: n,
124
- sortDirection: i,
124
+ sortDirection: l,
125
125
  onSort: s,
126
126
  selectedRows: d,
127
127
  onRowSelect: m,
128
128
  onSelectAll: h,
129
- getRowKey: p,
130
- onRowClick: b,
129
+ getRowKey: b,
130
+ onRowClick: p,
131
131
  className: y
132
132
  }) {
133
133
  const c = d !== void 0, v = c && r.length > 0 && d.length === r.length, u = c && d.length > 0 && d.length < r.length, E = (e) => {
134
134
  if (!s) return;
135
- s(e, n === e && i === "asc" ? "desc" : "asc");
135
+ s(e, n === e && l === "asc" ? "desc" : "asc");
136
136
  };
137
137
  return /* @__PURE__ */ x(j, { className: y, children: [
138
138
  /* @__PURE__ */ a(L, { children: /* @__PURE__ */ x(N, { hoverable: !1, children: [
@@ -145,11 +145,11 @@ function Y({
145
145
  onChange: (e) => h?.(e)
146
146
  }
147
147
  ) }),
148
- l.map((e) => /* @__PURE__ */ a(
148
+ i.map((e) => /* @__PURE__ */ a(
149
149
  k,
150
150
  {
151
151
  sortable: e.sortable,
152
- sortDirection: e.sortable && n === e.key ? i ?? null : null,
152
+ sortDirection: e.sortable && n === e.key ? l ?? null : null,
153
153
  onSort: () => E(e.key),
154
154
  info: e.info,
155
155
  fixedLine: e.fixedLine,
@@ -161,13 +161,13 @@ function Y({
161
161
  ))
162
162
  ] }) }),
163
163
  /* @__PURE__ */ a(R, { children: r.map((e, o) => {
164
- const H = p ? p(e, o) : o, w = c && d.includes(o);
164
+ const H = b ? b(e, o) : o, w = c && d.includes(o);
165
165
  return /* @__PURE__ */ x(
166
166
  N,
167
167
  {
168
168
  selected: w,
169
- onClick: b ? () => b(e, o) : void 0,
170
- className: b ? "cursor-pointer" : void 0,
169
+ onClick: p ? () => p(e, o) : void 0,
170
+ className: p ? "cursor-pointer" : void 0,
171
171
  children: [
172
172
  c && /* @__PURE__ */ a(T, { width: 40, className: "justify-center px-1.5", children: /* @__PURE__ */ a(
173
173
  C,
@@ -178,7 +178,7 @@ function Y({
178
178
  onClick: (t) => t.stopPropagation()
179
179
  }
180
180
  ) }),
181
- l.map((t) => /* @__PURE__ */ a(
181
+ i.map((t) => /* @__PURE__ */ a(
182
182
  T,
183
183
  {
184
184
  align: t.align,
@@ -1,14 +1,14 @@
1
1
  "use client";
2
2
  import { jsx as h } from "react/jsx-runtime";
3
3
  import { cn as T } from "../../lib/utils.js";
4
- import * as o from "react";
5
- const $ = o.createContext(null);
4
+ import * as s from "react";
5
+ const $ = s.createContext(null);
6
6
  function C() {
7
- const e = o.useContext($);
7
+ const e = s.useContext($);
8
8
  if (!e) throw new Error("TabsTrigger must be used within Tabs");
9
9
  return e;
10
10
  }
11
- const A = o.forwardRef(
11
+ const A = s.forwardRef(
12
12
  ({
13
13
  value: e,
14
14
  defaultValue: t = "",
@@ -17,7 +17,7 @@ const A = o.forwardRef(
17
17
  children: f,
18
18
  className: b
19
19
  }, x) => {
20
- const [u, p] = o.useState(t), g = e !== void 0, i = g ? e : u, m = o.useId(), [y, r] = o.useState(() => /* @__PURE__ */ new Set()), w = o.useCallback((c) => (r((a) => {
20
+ const [u, p] = s.useState(t), g = e !== void 0, i = g ? e : u, m = s.useId(), [w, r] = s.useState(() => /* @__PURE__ */ new Set()), y = s.useCallback((c) => (r((a) => {
21
21
  if (a.has(c)) return a;
22
22
  const n = new Set(a);
23
23
  return n.add(c), n;
@@ -27,17 +27,17 @@ const A = o.forwardRef(
27
27
  return n.delete(c), n;
28
28
  })), []), k = (c) => {
29
29
  g || p(c), d?.(c);
30
- }, s = o.useMemo(
31
- () => ({ value: i, onValueChange: k, variant: l, baseId: m, panels: y, registerPanel: w }),
30
+ }, o = s.useMemo(
31
+ () => ({ value: i, onValueChange: k, variant: l, baseId: m, panels: w, registerPanel: y }),
32
32
  // handleChange 는 렌더마다 새로 만들어지지만 참조하는 값이 아래 의존성에 모두 들어 있다
33
33
  // eslint-disable-next-line react-hooks/exhaustive-deps
34
- [i, g, d, l, m, y, w]
34
+ [i, g, d, l, m, w, y]
35
35
  );
36
- return /* @__PURE__ */ h($.Provider, { value: s, children: /* @__PURE__ */ h("div", { ref: x, className: T("flex flex-col", b), children: f }) });
36
+ return /* @__PURE__ */ h($.Provider, { value: o, children: /* @__PURE__ */ h("div", { ref: x, className: T("flex flex-col", b), children: f }) });
37
37
  }
38
38
  );
39
39
  A.displayName = "Tabs";
40
- const R = o.forwardRef(
40
+ const R = s.forwardRef(
41
41
  ({ children: e, className: t }, d) => {
42
42
  const { variant: l } = C();
43
43
  return /* @__PURE__ */ h(
@@ -57,16 +57,16 @@ const R = o.forwardRef(
57
57
  }
58
58
  );
59
59
  R.displayName = "TabsList";
60
- const P = o.forwardRef(
60
+ const P = s.forwardRef(
61
61
  ({ value: e, disabled: t, children: d, className: l, onClick: f, onKeyDown: b, ...x }, u) => {
62
- const { value: p, onValueChange: g, variant: i, baseId: m, panels: y } = C(), r = p === e, w = y.has(e), k = (s) => {
63
- if (b?.(s), s.defaultPrevented || !["ArrowRight", "ArrowLeft", "Home", "End"].includes(s.key)) return;
64
- const a = s.currentTarget.closest('[role="tablist"]'), n = Array.from(
62
+ const { value: p, onValueChange: g, variant: i, baseId: m, panels: w } = C(), r = p === e, y = w.has(e), k = (o) => {
63
+ if (b?.(o), o.defaultPrevented || !["ArrowRight", "ArrowLeft", "Home", "End"].includes(o.key)) return;
64
+ const a = o.currentTarget.closest('[role="tablist"]'), n = Array.from(
65
65
  a?.querySelectorAll('[role="tab"]:not([disabled])') ?? []
66
66
  );
67
67
  if (n.length === 0) return;
68
- s.preventDefault();
69
- const I = n.indexOf(s.currentTarget), N = s.key === "Home" ? 0 : s.key === "End" ? n.length - 1 : s.key === "ArrowRight" ? (I + 1 + n.length) % n.length : (I - 1 + n.length) % n.length;
68
+ o.preventDefault();
69
+ const I = n.indexOf(o.currentTarget), N = o.key === "Home" ? 0 : o.key === "End" ? n.length - 1 : o.key === "ArrowRight" ? (I + 1 + n.length) % n.length : (I - 1 + n.length) % n.length;
70
70
  n[N].focus(), n[N].click();
71
71
  };
72
72
  return /* @__PURE__ */ h(
@@ -77,15 +77,15 @@ const P = o.forwardRef(
77
77
  type: "button",
78
78
  id: `${m}-tab-${e}`,
79
79
  "aria-selected": r,
80
- "aria-controls": w ? `${m}-panel-${e}` : void 0,
80
+ "aria-controls": y ? `${m}-panel-${e}` : void 0,
81
81
  tabIndex: r ? 0 : -1,
82
82
  disabled: t,
83
- onClick: (s) => {
84
- t || g(e), f?.(s);
83
+ onClick: (o) => {
84
+ t || g(e), f?.(o);
85
85
  },
86
86
  onKeyDown: k,
87
87
  className: T(
88
- "inline-flex items-center justify-center whitespace-nowrap shrink-0 transition-colors focus-visible:outline-none",
88
+ "inline-flex items-center justify-center whitespace-nowrap shrink-0 transition-[color,background-color,border-color,box-shadow] focus-visible:outline-none",
89
89
  // 선택되지 않은 탭(비활성 포함)은 모든 변형 공통 14 Regular. 선택된 탭의 글씨는 변형별로 아래에서 정한다
90
90
  !r && "text-body-2-r",
91
91
  i === "underline" && [
@@ -113,8 +113,8 @@ const P = o.forwardRef(
113
113
  ],
114
114
  i === "round" && [
115
115
  "h-7 px-2.5 rounded",
116
- r && "text-body-4-r",
117
- r && !t && "bg-ds-surface-alternative text-ds-text-strong",
116
+ r && "text-body-4-m",
117
+ r && !t && "bg-ds-surface-alternative text-ds-text-strong shadow-ds-default",
118
118
  !r && !t && "bg-ds-surface-strong text-ds-text-normal",
119
119
  t && "bg-ds-surface-strong text-ds-text-disabled cursor-not-allowed"
120
120
  ],
@@ -127,10 +127,10 @@ const P = o.forwardRef(
127
127
  }
128
128
  );
129
129
  P.displayName = "TabsTrigger";
130
- const S = o.forwardRef(
130
+ const S = s.forwardRef(
131
131
  ({ value: e, children: t, className: d }, l) => {
132
132
  const { value: f, baseId: b, registerPanel: x } = C(), u = f === e;
133
- return o.useEffect(() => {
133
+ return s.useEffect(() => {
134
134
  if (u)
135
135
  return x(e);
136
136
  }, [u, e, x]), u ? /* @__PURE__ */ h(
@@ -0,0 +1,66 @@
1
+ import * as React from "react";
2
+ /** Timeline 트랙 타입 · 색 팔레트 · 트랙 행 렌더러. Timeline 내부용 — 타입만 index.tsx 로 공개한다. */
3
+ export interface TimelineTrackBase {
4
+ id: string;
5
+ label: React.ReactNode;
6
+ /**
7
+ * 트랙 색 강제 지정 (CSS color · `var(--ds-*)`). 보통은 비워 둔다 — 비우면
8
+ * `TRACK_PALETTE` 에서 서로 잘 구분되는 색이 트랙 순서대로 자동 배정된다.
9
+ */
10
+ color?: string;
11
+ /** 표 아래 범례에 쓰는 설명 (예: `Probability (darker = higher)`). 없으면 범례에서 빠진다. */
12
+ description?: React.ReactNode;
13
+ }
14
+ /** 구간별 강도 — `values` 를 전체 길이에 균등 분할해 진할수록 높게 칠한다. */
15
+ export interface TimelineHeatmapTrack extends TimelineTrackBase {
16
+ type: "heatmap";
17
+ /**
18
+ * 0~1. 값 하나가 구간 하나(div)라 화면 폭보다 많으면 보이지도 않고 DOM 만 늘어난다 —
19
+ * 소비처에서 구간 평균으로 줄여 수백 개 이하로 넘긴다.
20
+ */
21
+ values: number[];
22
+ }
23
+ /** 시간에 따른 점수 — 균등 간격 샘플을 영역 그래프로 그린다. */
24
+ export interface TimelineLineTrack extends TimelineTrackBase {
25
+ type: "line";
26
+ values: number[];
27
+ /** `values` 의 최댓값 기준. 기본 100 */
28
+ max?: number;
29
+ }
30
+ /** 이벤트 구간 — 시작·끝(초)에 막대를 놓는다. */
31
+ export interface TimelineEventsTrack extends TimelineTrackBase {
32
+ type: "events";
33
+ events: {
34
+ start: number;
35
+ end: number;
36
+ label?: string;
37
+ }[];
38
+ }
39
+ export type TimelineTrack = TimelineHeatmapTrack | TimelineLineTrack | TimelineEventsTrack;
40
+ /**
41
+ * 자동 배정 순서 — `category` 토큰 11색을 CIEDE2000 기준 farthest-point 로 정렬했다.
42
+ * 각 색은 앞선 색들과 플레이헤드 빨강(`primary-red-normal`)에서 가장 먼 것을 고른 결과이고,
43
+ * 괄호는 앞선 색들·플레이헤드와의 최소 ΔE00 이다. 앞 6색은 서로 ΔE00 ≥ 21.8 로 한눈에 구분되고,
44
+ * 7번째부터는 차이가 줄어 트랙이 많으면 비슷한 색이 섞일 수 있다.
45
+ * 토큰 값이 바뀌면 순서를 다시 계산해야 한다 — `npm run check:tokens` 가 순서와 최솟값을 대조한다.
46
+ */
47
+ export declare const TRACK_PALETTE: readonly ["var(--ds-category-politics)", "var(--ds-category-talk)", "var(--ds-category-religion)", "var(--ds-category-overseas)", "var(--ds-category-controversy)", "var(--ds-category-custom)", "var(--ds-category-sensitive)", "var(--ds-category-hate)", "var(--ds-category-issue)", "var(--ds-category-violence)", "var(--ds-category-sexual)"];
48
+ export declare const ROW_HEIGHT: Record<TimelineTrack["type"], string>;
49
+ export declare function TrackSwatch({ track, color }: {
50
+ track: TimelineTrack;
51
+ color: string;
52
+ }): import("react/jsx-runtime").JSX.Element;
53
+ export declare const HeatmapRow: React.NamedExoticComponent<{
54
+ track: TimelineHeatmapTrack;
55
+ color: string;
56
+ duration: number;
57
+ }>;
58
+ export declare const LineRow: React.NamedExoticComponent<{
59
+ track: TimelineLineTrack;
60
+ color: string;
61
+ }>;
62
+ export declare const EventsRow: React.NamedExoticComponent<{
63
+ track: TimelineEventsTrack;
64
+ color: string;
65
+ duration: number;
66
+ }>;
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ import { jsx as n, jsxs as g, Fragment as x } from "react/jsx-runtime";
3
+ import { formatTime as c } from "../../lib/format-time.js";
4
+ import { clamp as l } from "../../lib/utils.js";
5
+ import * as u from "react";
6
+ const b = [
7
+ "var(--ds-category-politics)",
8
+ // 초록 (70.6)
9
+ "var(--ds-category-talk)",
10
+ // 파랑 (46.4)
11
+ "var(--ds-category-religion)",
12
+ // 노랑 (36.2)
13
+ "var(--ds-category-overseas)",
14
+ // 분홍 (34.2)
15
+ "var(--ds-category-controversy)",
16
+ // 청록 (24.5)
17
+ "var(--ds-category-custom)",
18
+ // 올리브 (21.9)
19
+ "var(--ds-category-sensitive)",
20
+ // 자주 (21.6)
21
+ "var(--ds-category-hate)",
22
+ // 보라 (20.2)
23
+ "var(--ds-category-issue)",
24
+ // 하늘 (19.4)
25
+ "var(--ds-category-violence)",
26
+ // 살구 (17.4)
27
+ "var(--ds-category-sexual)"
28
+ // 빨강 (5.6)
29
+ ], d = (o, e) => `color-mix(in srgb, ${o} ${Math.round(l(e, 0, 1) * 100)}%, var(--ds-background-default))`, v = (o) => 0.15 + 0.85 * l(o, 0, 1), R = {
30
+ heatmap: "h-10",
31
+ line: "h-[60px]",
32
+ events: "h-10"
33
+ };
34
+ function k({ track: o, color: e }) {
35
+ return o.type === "heatmap" ? /* @__PURE__ */ n("span", { "aria-hidden": !0, className: "flex h-2 w-2 shrink-0", children: [0, 0.5, 1].map((s) => /* @__PURE__ */ n("span", { className: "h-full flex-1", style: { background: d(e, v(s)) } }, s)) }) : /* @__PURE__ */ n("span", { "aria-hidden": !0, className: "size-2 shrink-0", style: { background: e } });
36
+ }
37
+ const N = u.memo(function({ track: e, color: s, duration: r }) {
38
+ const t = e.values.length;
39
+ return /* @__PURE__ */ n("div", { className: "flex h-3 w-full", children: e.values.map((i, a) => /* @__PURE__ */ n(
40
+ "div",
41
+ {
42
+ title: `${c(a / t * r)}–${c((a + 1) / t * r)} · ${Math.round(l(i, 0, 1) * 100)}%`,
43
+ className: "h-full min-w-px flex-1 hover:outline hover:outline-1 hover:-outline-offset-1 hover:outline-ds-primitive-black",
44
+ style: { background: d(s, v(i)) }
45
+ },
46
+ a
47
+ )) });
48
+ }), E = u.memo(function({ track: e, color: s }) {
49
+ const r = e.max && e.max > 0 ? e.max : 100, t = e.values.length;
50
+ if (t === 0) return null;
51
+ const a = `M${e.values.map((m, h) => {
52
+ const p = t === 1 ? 50 : h / (t - 1) * 100, f = 100 - l(m / r, 0, 1) * 100;
53
+ return `${p},${f}`;
54
+ }).join(" L")}`;
55
+ return /* @__PURE__ */ g("svg", { "aria-hidden": !0, viewBox: "0 0 100 100", preserveAspectRatio: "none", className: "absolute inset-x-0 bottom-0 top-3 h-[calc(100%-12px)] w-full overflow-visible", children: [
56
+ /* @__PURE__ */ n("path", { d: `${a} L100,100 L0,100 Z`, fill: s, fillOpacity: 0.12 }),
57
+ /* @__PURE__ */ n("path", { d: a, fill: "none", stroke: s, strokeWidth: 1, vectorEffect: "non-scaling-stroke" })
58
+ ] });
59
+ }), L = u.memo(function({ track: e, color: s, duration: r }) {
60
+ return /* @__PURE__ */ n(x, { children: e.events.map((t, i) => {
61
+ const a = l(t.start / r, 0, 1), m = l(t.end / r, a, 1);
62
+ return /* @__PURE__ */ n(
63
+ "div",
64
+ {
65
+ title: `${t.label ? `${t.label} · ` : ""}${c(t.start)}–${c(t.end)}`,
66
+ className: "absolute top-1/2 h-3 min-w-[2px] -translate-y-1/2",
67
+ style: { left: `${a * 100}%`, width: `${(m - a) * 100}%`, background: s }
68
+ },
69
+ i
70
+ );
71
+ }) });
72
+ });
73
+ export {
74
+ L as EventsRow,
75
+ N as HeatmapRow,
76
+ E as LineRow,
77
+ R as ROW_HEIGHT,
78
+ b as TRACK_PALETTE,
79
+ k as TrackSwatch
80
+ };
@@ -0,0 +1,31 @@
1
+ import { TimelineTrack } from './timeline-tracks';
2
+ import * as React from "react";
3
+ export type { TimelineTrack, TimelineHeatmapTrack, TimelineLineTrack, TimelineEventsTrack, } from './timeline-tracks';
4
+ export interface TimelineProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSeek"> {
5
+ /** 전체 길이(초) */
6
+ duration: number;
7
+ /**
8
+ * 트랙 행은 `React.memo` 라 플레이헤드가 움직여도 다시 그리지 않는다 — 단, 트랙 객체 참조가 같을 때만.
9
+ * `currentTime` 을 가진 컴포넌트에서 렌더마다 `.map` 으로 새로 만들지 말고 `useMemo` 등으로 고정해 넘긴다.
10
+ */
11
+ tracks: TimelineTrack[];
12
+ /** 재생 위치(초) — 주면 빨간 플레이헤드를 그린다. `onSeek` 을 쓰면 키보드 이동 기준이므로 함께 넘긴다 */
13
+ currentTime?: number;
14
+ /** 시간 영역 클릭·키보드(←→)로 위치를 고르면 초 단위로 호출된다 */
15
+ onSeek?: (time: number) => void;
16
+ /**
17
+ * 눈금 간격(초, 1 이상). 비우면 길이·폭에 맞춰 눈금이 최대 8개가 되도록 고른다 (60초 영상 → 10초).
18
+ * 주어도 1초 미만이거나 눈금이 폭에 비해 너무 많아지면(눈금당 64px 미만) 자동 간격을 쓴다.
19
+ */
20
+ tickInterval?: number;
21
+ /** 접근성 라벨 — `onSeek` 이 있으면 시크 슬라이더(기본 "타임라인 재생 위치"), 없으면 루트에 붙는다 */
22
+ "aria-label"?: string;
23
+ /** 트랙 이름 열 폭(px). 기본 240 (Figma). 좁은 화면에서는 줄여서 넘긴다. */
24
+ labelWidth?: number;
25
+ }
26
+ /**
27
+ * Figma TimeLine (Instant Case). 영상 길이를 가로축으로 히트맵·점수 그래프·이벤트 구간 트랙을
28
+ * 쌓고, 재생 위치를 빨간 플레이헤드로 보여준다. 트랙 색은 `TRACK_PALETTE` 에서 자동 배정된다.
29
+ */
30
+ declare const Timeline: React.ForwardRefExoticComponent<TimelineProps & React.RefAttributes<HTMLDivElement>>;
31
+ export { Timeline };
@@ -0,0 +1,119 @@
1
+ "use client";
2
+ import { jsxs as s, jsx as n } from "react/jsx-runtime";
3
+ import { formatTime as T } from "../../lib/format-time.js";
4
+ import { cn as m, clamp as w } from "../../lib/utils.js";
5
+ import * as d from "react";
6
+ import { TRACK_PALETTE as M, TrackSwatch as F, HeatmapRow as W, LineRow as $, EventsRow as j, ROW_HEIGHT as z } from "./timeline-tracks.js";
7
+ const G = 100, U = [1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600], V = 8, Z = 64, q = 32;
8
+ function J(u, c) {
9
+ const f = u / c;
10
+ return U.find((r) => r >= f) ?? Math.ceil(f / 3600) * 3600;
11
+ }
12
+ const A = "w-[var(--timeline-label-w)]", P = d.forwardRef(
13
+ ({ duration: u, tracks: c, currentTime: f, onSeek: r, tickInterval: v, "aria-label": p, labelWidth: E = 240, className: I, ..._ }, C) => {
14
+ const x = d.useRef(null), [h, D] = d.useState(0);
15
+ d.useEffect(() => {
16
+ const e = x.current;
17
+ if (!e || typeof ResizeObserver > "u") return;
18
+ const t = new ResizeObserver(([i]) => D(i.contentRect.width));
19
+ return t.observe(e), () => t.disconnect();
20
+ }, []);
21
+ const a = Number.isFinite(u) && u > 0, l = a ? u : 1, y = h > 0 ? Math.max(2, Math.floor(h / Z)) : G, L = Math.min(V, y), b = v != null && v >= 1 && l / v <= y ? v : J(l, L), K = d.useMemo(() => {
22
+ if (!a) return [];
23
+ const e = [];
24
+ for (let t = 0; t <= l + 1e-6; t += b) e.push(t);
25
+ return e;
26
+ }, [a, l, b]), o = w(f ?? 0, 0, l), g = !!r && a, H = (e) => {
27
+ const t = x.current?.getBoundingClientRect();
28
+ !g || !t || e.clientX < t.left || r?.(w((e.clientX - t.left) / t.width, 0, 1) * l);
29
+ }, O = (e) => {
30
+ if (!g) return;
31
+ const t = e.shiftKey ? b : 1, i = {
32
+ ArrowRight: o + t,
33
+ ArrowUp: o + t,
34
+ ArrowLeft: o - t,
35
+ ArrowDown: o - t,
36
+ Home: 0,
37
+ End: l
38
+ };
39
+ e.key in i && (e.preventDefault(), r?.(w(i[e.key], 0, l)));
40
+ }, B = d.useMemo(() => {
41
+ let e = 0;
42
+ return c.map((t) => t.color ?? M[e++ % M.length]);
43
+ }, [c]), N = c.filter((e) => e.description != null), R = f != null && a ? o / l * 100 : null;
44
+ return /* @__PURE__ */ s(
45
+ "div",
46
+ {
47
+ ref: C,
48
+ role: !r && p ? "group" : void 0,
49
+ "aria-label": r ? void 0 : p,
50
+ className: m("flex w-full flex-col gap-2", I),
51
+ ..._,
52
+ children: [
53
+ /* @__PURE__ */ s(
54
+ "div",
55
+ {
56
+ onClick: H,
57
+ style: { "--timeline-label-w": `${E}px` },
58
+ className: m(
59
+ "relative overflow-hidden rounded-ds-md border border-ds-line-normal bg-ds-surface-alternative",
60
+ g && "cursor-pointer"
61
+ ),
62
+ children: [
63
+ /* @__PURE__ */ s("div", { className: "flex h-6 bg-ds-surface-normal", children: [
64
+ /* @__PURE__ */ n("div", { className: m(A, "shrink-0 border-r border-ds-line-normal") }),
65
+ /* @__PURE__ */ n(
66
+ "div",
67
+ {
68
+ ref: x,
69
+ role: r ? "slider" : void 0,
70
+ tabIndex: r ? 0 : void 0,
71
+ "aria-label": r ? p ?? "타임라인 재생 위치" : void 0,
72
+ "aria-valuemin": r ? 0 : void 0,
73
+ "aria-valuemax": r ? Math.round(l) : void 0,
74
+ "aria-valuenow": r ? Math.round(o) : void 0,
75
+ "aria-valuetext": r ? T(o) : void 0,
76
+ "aria-disabled": r && !a ? !0 : void 0,
77
+ onKeyDown: O,
78
+ className: "relative flex-1 text-label-1-r text-ds-text-strong outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ds-primary-blue-normal",
79
+ children: K.map((e, t) => {
80
+ const i = e / l * 100, X = h > 0 ? (100 - i) / 100 * h < q : i >= 100;
81
+ return /* @__PURE__ */ n("span", { className: m("absolute top-1/2 -translate-y-1/2 whitespace-nowrap", t === 0 ? "translate-x-2" : X ? "-translate-x-[calc(100%+8px)]" : "-translate-x-1/2"), style: { left: `${i}%` }, children: T(e) }, e);
82
+ })
83
+ }
84
+ )
85
+ ] }),
86
+ c.map((e, t) => {
87
+ const i = B[t];
88
+ return /* @__PURE__ */ s("div", { className: m("flex border-t border-ds-line-normal", z[e.type]), children: [
89
+ /* @__PURE__ */ s("div", { className: m(A, "flex shrink-0 items-center gap-1 border-r border-ds-line-normal px-3 text-body-4-r text-ds-text-strong"), children: [
90
+ /* @__PURE__ */ n(F, { track: e, color: i }),
91
+ /* @__PURE__ */ n("span", { className: "truncate", children: e.label })
92
+ ] }),
93
+ /* @__PURE__ */ s("div", { className: "relative flex flex-1 items-center", children: [
94
+ e.type === "heatmap" && /* @__PURE__ */ n(W, { track: e, color: i, duration: l }),
95
+ e.type === "line" && /* @__PURE__ */ n($, { track: e, color: i }),
96
+ e.type === "events" && a && /* @__PURE__ */ n(j, { track: e, color: i, duration: l })
97
+ ] })
98
+ ] }, e.id);
99
+ }),
100
+ R != null && /* @__PURE__ */ n("div", { "aria-hidden": !0, className: "pointer-events-none absolute inset-y-0 left-[var(--timeline-label-w)] right-0", children: /* @__PURE__ */ s("div", { className: "absolute inset-y-0", style: { left: `${R}%` }, children: [
101
+ /* @__PURE__ */ n("div", { className: "absolute inset-y-0 left-1/2 w-0.5 -translate-x-1/2 bg-ds-primary-red-normal" }),
102
+ /* @__PURE__ */ n("svg", { width: "12", height: "12", viewBox: "0 0 12 12", className: "relative -left-1.5 top-1.5 text-ds-primary-red-normal", children: /* @__PURE__ */ n("path", { d: "M0.8 0H11.2L6 9Z", fill: "currentColor" }) })
103
+ ] }) })
104
+ ]
105
+ }
106
+ ),
107
+ N.length > 0 && /* @__PURE__ */ n("dl", { className: "grid grid-cols-[max-content_1fr] gap-x-7 gap-y-1 text-body-4-m text-ds-text-strong", children: N.map((e) => /* @__PURE__ */ s(d.Fragment, { children: [
108
+ /* @__PURE__ */ n("dt", { children: e.label }),
109
+ /* @__PURE__ */ n("dd", { children: e.description })
110
+ ] }, e.id)) })
111
+ ]
112
+ }
113
+ );
114
+ }
115
+ );
116
+ P.displayName = "Timeline";
117
+ export {
118
+ P as Timeline
119
+ };
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsxs as i, jsx as e } from "react/jsx-runtime";
3
- import { ChevronRightIcon as l, CautionIcon as a, NegativeIcon as m, PositiveIcon as d } from "../../lib/icons.js";
3
+ import { ChevronRightIcon as d, CautionIcon as a, NegativeIcon as m, PositiveIcon as l } from "../../lib/icons.js";
4
4
  import { cn as u } from "../../lib/utils.js";
5
5
  import { Toaster as p, toast as s } from "sonner";
6
6
  function c({ icon: t, message: o }) {
@@ -19,7 +19,7 @@ function w({ position: t = "bottom-center" }) {
19
19
  );
20
20
  }
21
21
  const g = {
22
- success: (t, o) => s.custom(() => /* @__PURE__ */ e(c, { icon: /* @__PURE__ */ e(d, {}), message: t }), o),
22
+ success: (t, o) => s.custom(() => /* @__PURE__ */ e(c, { icon: /* @__PURE__ */ e(l, {}), message: t }), o),
23
23
  error: (t, o) => s.custom(() => /* @__PURE__ */ e(c, { icon: /* @__PURE__ */ e(m, {}), message: t }), o),
24
24
  caution: (t, o) => s.custom(() => /* @__PURE__ */ e(c, { icon: /* @__PURE__ */ e(a, {}), message: t }), o)
25
25
  };
@@ -28,7 +28,7 @@ function r({
28
28
  message: o,
29
29
  action: n
30
30
  }) {
31
- return /* @__PURE__ */ i("div", { className: "bg-ds-primitive-black flex items-center gap-1.5 px-3 py-4 rounded-ds-md w-[336px] shadow-ds-default", children: [
31
+ return /* @__PURE__ */ i("div", { className: "bg-ds-primitive-black flex items-center gap-1.5 px-3 py-4 rounded-ds-lg w-[336px] shadow-ds-default", children: [
32
32
  t,
33
33
  /* @__PURE__ */ e("p", { className: "text-white text-body-2-r flex-1 min-w-0", children: o }),
34
34
  n && /* @__PURE__ */ i(
@@ -42,7 +42,7 @@ function r({
42
42
  ),
43
43
  children: [
44
44
  n.label,
45
- n.withIcon && /* @__PURE__ */ e(l, {})
45
+ n.withIcon && /* @__PURE__ */ e(d, {})
46
46
  ]
47
47
  }
48
48
  )
@@ -50,7 +50,7 @@ function r({
50
50
  }
51
51
  const k = {
52
52
  success: (t, o, n) => s.custom(
53
- () => /* @__PURE__ */ e(r, { icon: /* @__PURE__ */ e(d, {}), message: t, action: o }),
53
+ () => /* @__PURE__ */ e(r, { icon: /* @__PURE__ */ e(l, {}), message: t, action: o }),
54
54
  n
55
55
  ),
56
56
  error: (t, o, n) => s.custom(