@leviosa-ai/detail-page-editor 0.1.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 (190) hide show
  1. package/components/cardnews/color-input.tsx +380 -0
  2. package/components/detail-page/ai-generate-panel.tsx +763 -0
  3. package/components/detail-page/annotation-canvas.tsx +860 -0
  4. package/components/detail-page/annotation-dialog.tsx +397 -0
  5. package/components/detail-page/bubble-tail-overlay.tsx +209 -0
  6. package/components/detail-page/canvas-context-menu.tsx +234 -0
  7. package/components/detail-page/canvas-observer.tsx +68 -0
  8. package/components/detail-page/canvas-overlay-host.tsx +44 -0
  9. package/components/detail-page/canvas-store-facade.ts +51 -0
  10. package/components/detail-page/chart-inspector.tsx +531 -0
  11. package/components/detail-page/design-reference-picker.tsx +383 -0
  12. package/components/detail-page/detail-page-basic-shapes.tsx +180 -0
  13. package/components/detail-page/detail-page-brand-gifs-panel.tsx +261 -0
  14. package/components/detail-page/detail-page-brand-kit-panel.tsx +77 -0
  15. package/components/detail-page/detail-page-brand-panel-header.tsx +44 -0
  16. package/components/detail-page/detail-page-charts-panel.tsx +104 -0
  17. package/components/detail-page/detail-page-decorations-panel.tsx +125 -0
  18. package/components/detail-page/detail-page-download-dialog.tsx +524 -0
  19. package/components/detail-page/detail-page-editor.tsx +974 -0
  20. package/components/detail-page/detail-page-elements-panel.tsx +117 -0
  21. package/components/detail-page/detail-page-font-picker.tsx +231 -0
  22. package/components/detail-page/detail-page-history-buttons.tsx +87 -0
  23. package/components/detail-page/detail-page-host-context.tsx +465 -0
  24. package/components/detail-page/detail-page-icons-panel.tsx +251 -0
  25. package/components/detail-page/detail-page-konva-json-preview.tsx +474 -0
  26. package/components/detail-page/detail-page-layers-panel.tsx +618 -0
  27. package/components/detail-page/detail-page-my-images-panel.tsx +179 -0
  28. package/components/detail-page/detail-page-my-shapes-panel.tsx +174 -0
  29. package/components/detail-page/detail-page-page-toolbar.tsx +171 -0
  30. package/components/detail-page/detail-page-pages-panel.tsx +299 -0
  31. package/components/detail-page/detail-page-pages-timeline.tsx +79 -0
  32. package/components/detail-page/detail-page-photos-panel.tsx +257 -0
  33. package/components/detail-page/detail-page-properties-panel.tsx +2680 -0
  34. package/components/detail-page/detail-page-qr-panel.tsx +137 -0
  35. package/components/detail-page/detail-page-references-panel.tsx +166 -0
  36. package/components/detail-page/detail-page-selection.ts +44 -0
  37. package/components/detail-page/detail-page-shapes-panel.tsx +16 -0
  38. package/components/detail-page/detail-page-sidebar-sections.tsx +324 -0
  39. package/components/detail-page/detail-page-tables-panel.tsx +106 -0
  40. package/components/detail-page/detail-page-text-panel.tsx +179 -0
  41. package/components/detail-page/detail-page-thumbnail-bus.ts +42 -0
  42. package/components/detail-page/edit-quota-ui.tsx +150 -0
  43. package/components/detail-page/editor-history.ts +56 -0
  44. package/components/detail-page/editor-hotkeys.tsx +165 -0
  45. package/components/detail-page/element-recents-strip.tsx +105 -0
  46. package/components/detail-page/element-rects.ts +82 -0
  47. package/components/detail-page/fill-control.tsx +261 -0
  48. package/components/detail-page/find-replace-panel.tsx +252 -0
  49. package/components/detail-page/generation-progress-fill.tsx +57 -0
  50. package/components/detail-page/gif-animator.tsx +184 -0
  51. package/components/detail-page/gif-effect-picker.tsx +136 -0
  52. package/components/detail-page/group-drill-in.tsx +219 -0
  53. package/components/detail-page/group-prompt-edit-panel.tsx +154 -0
  54. package/components/detail-page/hover-highlight-overlay.tsx +111 -0
  55. package/components/detail-page/hovered-layer.ts +34 -0
  56. package/components/detail-page/inspector-controls.tsx +175 -0
  57. package/components/detail-page/leviosa-canvas-workspace.tsx +313 -0
  58. package/components/detail-page/nested-selection-overlay.tsx +364 -0
  59. package/components/detail-page/panel-search-input.tsx +49 -0
  60. package/components/detail-page/prompt-edit-panel.tsx +146 -0
  61. package/components/detail-page/section-height-handle.tsx +276 -0
  62. package/components/detail-page/section-reauthor-controller.tsx +210 -0
  63. package/components/detail-page/spec-prompt-edit-panel.tsx +178 -0
  64. package/components/detail-page/spec-resize-absorber.tsx +149 -0
  65. package/components/detail-page/svg-prompt-edit-panel.tsx +139 -0
  66. package/components/detail-page/table-canvas-overlay.tsx +707 -0
  67. package/components/detail-page/table-inspector.tsx +586 -0
  68. package/components/ui/button.tsx +64 -0
  69. package/components/ui/command.tsx +175 -0
  70. package/components/ui/dialog.tsx +167 -0
  71. package/components/ui/label.tsx +24 -0
  72. package/components/ui/select.tsx +190 -0
  73. package/components/ui/slider.tsx +63 -0
  74. package/components/ui/switch.tsx +35 -0
  75. package/components/ui/tooltip.tsx +57 -0
  76. package/config/detail-page-bundle-font-tags.json +39 -0
  77. package/config/detail-page-fonts.json +373 -0
  78. package/index.ts +24 -0
  79. package/lib/cardnews/font-loader.ts +306 -0
  80. package/lib/detail-page/animation-sniff.ts +101 -0
  81. package/lib/detail-page/asset-bytes-url.ts +70 -0
  82. package/lib/detail-page/basic-shapes.ts +603 -0
  83. package/lib/detail-page/canvas-menu.ts +182 -0
  84. package/lib/detail-page/chart/defaults.ts +283 -0
  85. package/lib/detail-page/chart/format.ts +66 -0
  86. package/lib/detail-page/chart/normalize.ts +149 -0
  87. package/lib/detail-page/chart/palette.ts +85 -0
  88. package/lib/detail-page/chart/paste.ts +82 -0
  89. package/lib/detail-page/chart/preview.ts +78 -0
  90. package/lib/detail-page/chart/render.ts +188 -0
  91. package/lib/detail-page/chart/renderers/bar-h.ts +103 -0
  92. package/lib/detail-page/chart/renderers/bar-v.ts +213 -0
  93. package/lib/detail-page/chart/renderers/donut.ts +186 -0
  94. package/lib/detail-page/chart/renderers/gauge.ts +96 -0
  95. package/lib/detail-page/chart/renderers/line.ts +143 -0
  96. package/lib/detail-page/chart/renderers/shared.ts +263 -0
  97. package/lib/detail-page/chart/renderers/stack.ts +121 -0
  98. package/lib/detail-page/chart/sync.ts +92 -0
  99. package/lib/detail-page/chart/types.ts +145 -0
  100. package/lib/detail-page/css-color.ts +34 -0
  101. package/lib/detail-page/data-gif-payload.ts +167 -0
  102. package/lib/detail-page/design-reference.ts +244 -0
  103. package/lib/detail-page/distribute.ts +89 -0
  104. package/lib/detail-page/editor-routes.ts +10 -0
  105. package/lib/detail-page/element-recents.ts +167 -0
  106. package/lib/detail-page/find-replace.ts +120 -0
  107. package/lib/detail-page/format-painter.ts +159 -0
  108. package/lib/detail-page/gif-frames.ts +216 -0
  109. package/lib/detail-page/gif-timeline.ts +65 -0
  110. package/lib/detail-page/group-action.ts +62 -0
  111. package/lib/detail-page/hangul-roman.ts +58 -0
  112. package/lib/detail-page/icon-curated.ts +290 -0
  113. package/lib/detail-page/icon-fuzzy.ts +250 -0
  114. package/lib/detail-page/icon-keywords.generated.ts +3766 -0
  115. package/lib/detail-page/icon-keywords.ko.ts +524 -0
  116. package/lib/detail-page/icon-search.ts +257 -0
  117. package/lib/detail-page/icons.ts +67 -0
  118. package/lib/detail-page/image-credit.ts +121 -0
  119. package/lib/detail-page/image-data-uri.ts +29 -0
  120. package/lib/detail-page/insert-image.ts +75 -0
  121. package/lib/detail-page/insert-shape.ts +49 -0
  122. package/lib/detail-page/korean-dictionary.ts +187 -0
  123. package/lib/detail-page/layer-move.ts +257 -0
  124. package/lib/detail-page/new-product-draft-store.ts +105 -0
  125. package/lib/detail-page/new-product-draft.ts +283 -0
  126. package/lib/detail-page/personal-images-refresh.ts +30 -0
  127. package/lib/detail-page/product-source.ts +587 -0
  128. package/lib/detail-page/qr-code.ts +214 -0
  129. package/lib/detail-page/reference-image.ts +103 -0
  130. package/lib/detail-page/reference-upload.ts +181 -0
  131. package/lib/detail-page/replace-page.ts +35 -0
  132. package/lib/detail-page/replace-with-gif.ts +251 -0
  133. package/lib/detail-page/section-height.ts +131 -0
  134. package/lib/detail-page/section-reauthor-bus.ts +61 -0
  135. package/lib/detail-page/shape-to-image.ts +219 -0
  136. package/lib/detail-page/spec-group/clipboard.ts +35 -0
  137. package/lib/detail-page/spec-group/sync.ts +377 -0
  138. package/lib/detail-page/spec-group/text-metrics.ts +48 -0
  139. package/lib/detail-page/stock-photos.ts +88 -0
  140. package/lib/detail-page/svg-colors.ts +74 -0
  141. package/lib/detail-page/table/defaults.ts +344 -0
  142. package/lib/detail-page/table/edit.ts +312 -0
  143. package/lib/detail-page/table/harvest.ts +103 -0
  144. package/lib/detail-page/table/normalize.ts +125 -0
  145. package/lib/detail-page/table/paste.ts +49 -0
  146. package/lib/detail-page/table/preview.ts +66 -0
  147. package/lib/detail-page/table/render.ts +388 -0
  148. package/lib/detail-page/table/sync.ts +107 -0
  149. package/lib/detail-page/table/types.ts +102 -0
  150. package/lib/detail-page/text-gif-layout.ts +245 -0
  151. package/lib/detail-page/text-gif-payload.ts +67 -0
  152. package/lib/detail-page/text-presets.ts +230 -0
  153. package/lib/detail-page/use-fake-progress.ts +67 -0
  154. package/lib/detail-page/z-order.ts +82 -0
  155. package/lib/detail-page-canvas/bubble-path.ts +190 -0
  156. package/lib/detail-page-canvas/canvas-key.ts +22 -0
  157. package/lib/detail-page-canvas/clip-rect.ts +65 -0
  158. package/lib/detail-page-canvas/custom-props-adapter.ts +219 -0
  159. package/lib/detail-page-canvas/editor-fonts.ts +161 -0
  160. package/lib/detail-page-canvas/export/ai.ts +413 -0
  161. package/lib/detail-page-canvas/export/color.ts +35 -0
  162. package/lib/detail-page-canvas/export/document-model.ts +78 -0
  163. package/lib/detail-page-canvas/export/export-files.ts +290 -0
  164. package/lib/detail-page-canvas/export/font-subset.ts +264 -0
  165. package/lib/detail-page-canvas/export/gif-export.ts +310 -0
  166. package/lib/detail-page-canvas/export/gif-plan.ts +95 -0
  167. package/lib/detail-page-canvas/export/mp4-encode.ts +158 -0
  168. package/lib/detail-page-canvas/export/mp4-support.ts +22 -0
  169. package/lib/detail-page-canvas/export/pdf/geometry.ts +309 -0
  170. package/lib/detail-page-canvas/export/pdf/inline-svg.ts +399 -0
  171. package/lib/detail-page-canvas/export/pdf/resources.ts +443 -0
  172. package/lib/detail-page-canvas/export/pdf/text-ops.ts +81 -0
  173. package/lib/detail-page-canvas/export/pdf/writer.ts +109 -0
  174. package/lib/detail-page-canvas/export/psd.ts +354 -0
  175. package/lib/detail-page-canvas/export/raster.ts +255 -0
  176. package/lib/detail-page-canvas/export/svg.ts +479 -0
  177. package/lib/detail-page-canvas/export/text-layout.ts +142 -0
  178. package/lib/detail-page-canvas/font-catalog.ts +177 -0
  179. package/lib/detail-page-canvas/font-tags.ts +47 -0
  180. package/lib/detail-page-canvas/gif-web-fonts.ts +90 -0
  181. package/lib/detail-page-canvas/konva-fallback.ts +477 -0
  182. package/lib/detail-page-canvas/text-background-plan.ts +124 -0
  183. package/lib/detail-page-canvas/text-highlight-bands.ts +127 -0
  184. package/lib/utils.ts +10 -0
  185. package/package.json +68 -0
  186. package/types/commerce.ts +171 -0
  187. package/types/detail-page-api.ts +200 -0
  188. package/types/detail-page-canvas.ts +20 -0
  189. package/types/fontkit.d.ts +42 -0
  190. package/types/gifenc.d.ts +51 -0
@@ -0,0 +1,251 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { useInfiniteQuery } from "@tanstack/react-query";
5
+ import { ImageOff, Loader2 } from "lucide-react";
6
+ import { useTranslation } from "react-i18next";
7
+
8
+ import { PanelSearchInput } from "./panel-search-input";
9
+ import { encodeSvgDataUri } from "../../lib/detail-page-canvas/export/svg";
10
+ import { insertShape } from "../../lib/detail-page/insert-shape";
11
+ import { rememberElement } from "../../lib/detail-page/element-recents";
12
+ import { applyCurrentColor } from "../../lib/detail-page/svg-colors";
13
+ import { searchIcons, type IconGroup, type IconItem } from "../../lib/detail-page/icons";
14
+ import type { IconStyle } from "../../lib/detail-page/icon-search";
15
+ import { useDetailPageHost } from "./detail-page-host-context";
16
+
17
+ /**
18
+ * "요소 · 아이콘" 그룹 — 오픈 라이선스 아이콘 검색.
19
+ *
20
+ * 사진 패널이 Pexels를 중계하는 것과 같은 자리인데 두 가지가 다르다.
21
+ *
22
+ * 1. **S3로 옮겨 담지 않는다.** 아이콘은 삽입 순간 마크업이 문서에 박혀서 저장된
23
+ * 상세페이지가 제공처 수명에 묶이지 않는다.
24
+ * 2. **세트 이름을 안 보여 준다.** "Tabler에서 찾기"는 사용자에게 아무 의미가 없다.
25
+ * 축은 **선 / 채움** 둘뿐이다 — 상세페이지는 아이콘 스타일이 섞이면 바로 티가 난다.
26
+ *
27
+ * 브랜드 로고(Simple Icons)는 상표라 일반 아이콘과 섞지 않고 그룹을 나눈다.
28
+ */
29
+
30
+ const DEBOUNCE_MS = 400;
31
+
32
+ export function DetailPageIconsPanel({ store }: { store: unknown }) {
33
+ const { brand, queryKeys } = useDetailPageHost();
34
+ const { t } = useTranslation("branding");
35
+ const [query, setQuery] = useState("");
36
+ const [debounced, setDebounced] = useState("");
37
+ const [style, setStyle] = useState<IconStyle>("stroke");
38
+ const [group, setGroup] = useState<IconGroup>("icons");
39
+ const brandColor = brand.useBrandPrimaryColor();
40
+
41
+ useEffect(() => {
42
+ const timer = setTimeout(() => setDebounced(query.trim()), DEBOUNCE_MS);
43
+ return () => clearTimeout(timer);
44
+ }, [query]);
45
+
46
+ // 로고에는 선/채움 축이 없다 — 세트가 하나고 전부 채움이다.
47
+ const effectiveStyle = group === "logos" ? undefined : style;
48
+
49
+ const icons = useInfiniteQuery({
50
+ queryKey: queryKeys.branding.detailPageIcons(debounced, group, effectiveStyle),
51
+ queryFn: ({ pageParam, signal }) =>
52
+ searchIcons({ query: debounced, group, style: effectiveStyle, page: pageParam, signal }),
53
+ initialPageParam: 0,
54
+ getNextPageParam: (last) => (last.hasMore ? last.page + 1 : undefined),
55
+ staleTime: 30 * 60_000,
56
+ retry: false,
57
+ });
58
+
59
+ const pages = useMemo(() => icons.data?.pages ?? [], [icons.data]);
60
+ const items = useMemo(() => pages.flatMap((page) => page.items), [pages]);
61
+ const truncated = pages.length > 0 && pages[pages.length - 1].truncated;
62
+
63
+ // 격자 바닥이 보이면 다음 쪽. 스크롤 컨테이너가 곧 관찰 뿌리다.
64
+ const scrollRef = useRef<HTMLDivElement | null>(null);
65
+ const sentinelRef = useRef<HTMLDivElement | null>(null);
66
+ const { fetchNextPage, hasNextPage, isFetchingNextPage } = icons;
67
+
68
+ useEffect(() => {
69
+ const sentinel = sentinelRef.current;
70
+ if (!sentinel || !hasNextPage || isFetchingNextPage) return;
71
+ // jsdom·구형 브라우저 대비. 관찰이 안 되면 아래 "더 보기" 버튼이 남는다.
72
+ if (typeof IntersectionObserver === "undefined") return;
73
+ const observer = new IntersectionObserver(
74
+ (entries) => {
75
+ if (entries.some((entry) => entry.isIntersecting)) void fetchNextPage();
76
+ },
77
+ { root: scrollRef.current, rootMargin: "200px" },
78
+ );
79
+ observer.observe(sentinel);
80
+ return () => observer.disconnect();
81
+ }, [fetchNextPage, hasNextPage, isFetchingNextPage, items.length]);
82
+
83
+ // 격자 미리보기도 실제로 넣을 색으로 그린다 — 눌러 보고 다른 색이 나오면 안 된다.
84
+ const cells = useMemo(
85
+ () =>
86
+ items.map((item) => ({
87
+ item,
88
+ markup: applyCurrentColor(item.markup, brandColor),
89
+ })),
90
+ [items, brandColor],
91
+ );
92
+
93
+ const insert = useCallback(
94
+ (item: IconItem, markup: string) => {
95
+ insertShape(store, markup, item.viewBox);
96
+ rememberElement({
97
+ key: item.id,
98
+ markup,
99
+ viewBox: item.viewBox,
100
+ label: item.id.split(":")[1] ?? item.id,
101
+ });
102
+ },
103
+ [store],
104
+ );
105
+
106
+ return (
107
+ // 스크롤은 **격자만** 한다. 패널 전체를 스크롤러로 두면 검색창과 토글이 같이
108
+ // 밀려 올라가고, 그 안의 `flex-1 min-h-0` 격자가 남는 높이에 맞춰 줄어들면서
109
+ // 컨테이너가 아예 넘치지 않아 스크롤바 자체가 안 생긴다.
110
+ <div className="flex h-full flex-col">
111
+ <div className="shrink-0 px-3 pt-3">
112
+ <PanelSearchInput
113
+ value={query}
114
+ onChange={setQuery}
115
+ placeholder={t("detailPage.icons.searchPlaceholder")}
116
+ label={t("detailPage.icons.searchLabel")}
117
+ />
118
+ </div>
119
+
120
+ <div className="mt-2 flex shrink-0 gap-1.5 px-3">
121
+ <SegmentedGroup
122
+ label={t("detailPage.icons.groupLabel")}
123
+ value={group}
124
+ onChange={setGroup}
125
+ options={[
126
+ { value: "icons", label: t("detailPage.icons.groupIcons") },
127
+ { value: "logos", label: t("detailPage.icons.groupLogos") },
128
+ ]}
129
+ />
130
+ {group === "icons" ? (
131
+ <SegmentedGroup
132
+ label={t("detailPage.icons.styleLabel")}
133
+ value={style}
134
+ onChange={setStyle}
135
+ options={[
136
+ { value: "stroke", label: t("detailPage.icons.styleStroke") },
137
+ { value: "fill", label: t("detailPage.icons.styleFill") },
138
+ ]}
139
+ />
140
+ ) : null}
141
+ </div>
142
+
143
+ {group === "logos" ? (
144
+ // 파일은 CC0지만 로고 자체는 상표다. 안내를 지우면 이용 조건을 어기는 쪽으로 샌다.
145
+ <p className="mx-3 mt-2 shrink-0 rounded-md bg-amber-50 px-2 py-1.5 text-[11px] leading-relaxed text-amber-800">
146
+ {t("detailPage.icons.logoTrademark")}
147
+ </p>
148
+ ) : null}
149
+
150
+ <div ref={scrollRef} className="mt-3 min-h-0 flex-1 overflow-y-auto px-3">
151
+ {icons.isPending ? (
152
+ <div className="flex justify-center py-6 text-neutral-400">
153
+ <Loader2 aria-hidden="true" className="animate-spin" size={20} />
154
+ </div>
155
+ ) : icons.isError ? (
156
+ <p className="text-xs text-red-600">{t("detailPage.icons.failed")}</p>
157
+ ) : cells.length === 0 ? (
158
+ <div className="flex flex-col items-center gap-2 py-6 text-center text-neutral-400">
159
+ <ImageOff aria-hidden="true" size={20} />
160
+ <p className="text-xs">{t("detailPage.icons.empty")}</p>
161
+ </div>
162
+ ) : (
163
+ <>
164
+ <div className="grid grid-cols-5 gap-1.5">
165
+ {cells.map(({ item, markup }) => (
166
+ <button
167
+ key={item.id}
168
+ type="button"
169
+ onClick={() => insert(item, markup)}
170
+ title={t("detailPage.shapes.insertHint")}
171
+ aria-label={item.id}
172
+ className="flex aspect-square items-center justify-center rounded-lg border border-neutral-200 p-1.5 hover:border-neutral-400"
173
+ >
174
+ {/* eslint-disable-next-line @next/next/no-img-element */}
175
+ <img
176
+ src={encodeSvgDataUri(markup)}
177
+ alt=""
178
+ className="max-h-full max-w-full object-contain"
179
+ />
180
+ </button>
181
+ ))}
182
+ </div>
183
+ <div ref={sentinelRef} aria-hidden="true" className="h-px" />
184
+ {isFetchingNextPage ? (
185
+ <div className="flex justify-center py-3 text-neutral-400">
186
+ <Loader2 aria-hidden="true" className="animate-spin" size={16} />
187
+ </div>
188
+ ) : hasNextPage ? (
189
+ // 관찰이 안 되는 환경(구형 브라우저·테스트)에서도 이어 받을 수 있어야 한다.
190
+ <button
191
+ type="button"
192
+ onClick={() => void fetchNextPage()}
193
+ className="mt-2 w-full rounded-md border border-neutral-200 py-1.5 text-[11px] font-medium text-neutral-500 hover:bg-neutral-50"
194
+ >
195
+ {t("detailPage.icons.loadMore")}
196
+ </button>
197
+ ) : truncated ? (
198
+ // 조용히 자르면 "이게 전부"로 읽힌다.
199
+ <p className="mt-2 px-1 text-[11px] text-neutral-400">
200
+ {t("detailPage.icons.truncated")}
201
+ </p>
202
+ ) : null}
203
+ </>
204
+ )}
205
+ </div>
206
+
207
+ <p className="shrink-0 border-t border-neutral-100 px-3 py-2 text-[11px] leading-relaxed text-neutral-500">
208
+ {t("detailPage.icons.licenseHint")}
209
+ </p>
210
+ </div>
211
+ );
212
+ }
213
+
214
+ function SegmentedGroup<T extends string>({
215
+ label,
216
+ value,
217
+ onChange,
218
+ options,
219
+ }: {
220
+ label: string;
221
+ value: T;
222
+ onChange: (next: T) => void;
223
+ options: ReadonlyArray<{ value: T; label: string }>;
224
+ }) {
225
+ return (
226
+ <div
227
+ role="group"
228
+ aria-label={label}
229
+ className="flex flex-1 rounded-md border border-neutral-200 p-0.5"
230
+ >
231
+ {options.map((option) => {
232
+ const active = option.value === value;
233
+ return (
234
+ <button
235
+ key={option.value}
236
+ type="button"
237
+ aria-pressed={active}
238
+ onClick={() => onChange(option.value)}
239
+ className={`flex-1 rounded px-2 py-1 text-[11px] font-medium transition-colors ${
240
+ active
241
+ ? "bg-neutral-900 text-white"
242
+ : "text-neutral-500 hover:bg-neutral-100"
243
+ }`}
244
+ >
245
+ {option.label}
246
+ </button>
247
+ );
248
+ })}
249
+ </div>
250
+ );
251
+ }