@conscience-technology/ctds 0.3.1 → 0.3.3

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.
@@ -166,6 +166,70 @@
166
166
  color: var(--fg);
167
167
  }
168
168
 
169
+ /* Menu trigger ─── segment 자체가 트리거. Link와 같은 xs · ghost 스펙 + ▾ caret.
170
+ active=true일 때는 Current와 같은 톤(--fg-strong) + hover는 그대로 유지. */
171
+ .menuTrigger {
172
+ display: inline-flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ gap: var(--space-1);
176
+ height: var(--control-h-xs);
177
+ padding-inline: var(--space-1);
178
+ border: 1px solid transparent;
179
+ border-radius: var(--radius-xs);
180
+ background: transparent;
181
+ color: var(--fg-muted);
182
+ font-family: var(--label-font);
183
+ font-weight: var(--label-weight);
184
+ font-size: var(--label-sm-size);
185
+ line-height: var(--lh-tight);
186
+ letter-spacing: var(--label-tracking);
187
+ cursor: pointer;
188
+ user-select: none;
189
+ min-width: 0;
190
+ transition: var(--transition-colors), var(--transition-ring), var(--transition-press);
191
+ }
192
+ .menuTrigger:hover {
193
+ background: var(--hover);
194
+ color: var(--fg);
195
+ }
196
+ .menuTrigger:active {
197
+ transform: translateY(var(--space-0_5));
198
+ }
199
+ .menuTrigger:focus-visible {
200
+ outline: none;
201
+ box-shadow: var(--ring-shadow);
202
+ }
203
+ .menuTrigger[data-state="open"] {
204
+ background: var(--hover);
205
+ color: var(--fg);
206
+ }
207
+ .menuTriggerActive {
208
+ color: var(--fg-strong);
209
+ }
210
+ .menuTrigger:hover .linkIcon,
211
+ .menuTrigger[data-state="open"] .linkIcon {
212
+ opacity: 1;
213
+ }
214
+
215
+ /* ▾ caret ─── 트리거 끝에 붙는 작은 꼬리표. Separator처럼 반응 없음. */
216
+ .menuCaret {
217
+ display: inline-flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ flex-shrink: 0;
221
+ color: var(--fg-muted);
222
+ opacity: 0.5;
223
+ transition: opacity var(--duration-slow) var(--ease-out);
224
+ }
225
+ .menuCaret > svg {
226
+ display: block;
227
+ }
228
+ .menuTrigger:hover .menuCaret,
229
+ .menuTrigger[data-state="open"] .menuCaret {
230
+ opacity: 0.9;
231
+ }
232
+
169
233
  /* Popover surface ─── Select · ContextMenu와 동일한 팝오버 표면.
170
234
  ContextMenu.module.css의 .surface를 composes해 시각 규칙 원본을 재사용. */
171
235
  .popoverContent {
@@ -139,72 +139,37 @@ const Current = forwardRef<HTMLSpanElement, BreadcrumbCurrentProps>(
139
139
  Current.displayName = "Breadcrumb.Current";
140
140
 
141
141
  // ────────────────────────────────────────────────────────────
142
- // Ellipsis — 항목이 많을 때 중간 축약. 클릭 시 Select와 동일한 팝오버 표면으로
143
- // 숨긴 항목 리스트를 연다. items는 필수 — 클릭 불가한 정적 마커는 지원하지 않는다.
142
+ // Shared — Ellipsis·Menu가 공유하는 팝오버 아이템 모델/렌더.
144
143
  // ────────────────────────────────────────────────────────────
145
144
 
146
- export type BreadcrumbEllipsisItem = {
145
+ export type BreadcrumbMenuItem = {
147
146
  label: ReactNode;
148
147
  href?: string;
149
148
  onSelect?: () => void;
150
149
  leftIcon?: ReactNode;
151
150
  disabled?: boolean;
151
+ /** 현재 선택된 항목. 체크 아이콘이 trailingActions에 자동 삽입. */
152
+ active?: boolean;
152
153
  };
153
154
 
154
- export interface BreadcrumbEllipsisProps
155
- extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onSelect"> {
156
- /** 숨긴 항목 리스트. 최소 1개. */
157
- items: BreadcrumbEllipsisItem[];
158
- /** 팝오버 side offset (px). 기본 6. */
159
- sideOffset?: number;
160
- /** aria-label. 기본 "Show hidden pages". */
161
- "aria-label"?: string;
162
- }
163
-
164
- const Ellipsis = forwardRef<HTMLButtonElement, BreadcrumbEllipsisProps>(
165
- (
166
- {
167
- className,
168
- items,
169
- sideOffset = 6,
170
- "aria-label": ariaLabel = "Show hidden pages",
171
- ...rest
172
- },
173
- ref,
174
- ) => {
175
- return (
176
- <RPopover.Root>
177
- <RPopover.Trigger asChild>
178
- <button
179
- ref={ref}
180
- type="button"
181
- aria-label={ariaLabel}
182
- className={[styles.ellipsisTrigger, className ?? ""].filter(Boolean).join(" ")}
183
- {...rest}
184
- >
185
- &hellip;
186
- </button>
187
- </RPopover.Trigger>
188
- <EllipsisContent items={items} sideOffset={sideOffset} />
189
- </RPopover.Root>
190
- );
191
- },
192
- );
193
- Ellipsis.displayName = "Breadcrumb.Ellipsis";
155
+ /** 하위 호환 — 기존 Ellipsis.items 타입명. */
156
+ export type BreadcrumbEllipsisItem = BreadcrumbMenuItem;
194
157
 
195
158
  /** Portal 하위에서 IconAxis 재주입 — Radix Portal이 트리 컨텍스트를 끊는다. */
196
- function EllipsisContent({
159
+ function PopoverMenuContent({
197
160
  items,
198
161
  sideOffset,
162
+ align = "start",
199
163
  }: {
200
- items: BreadcrumbEllipsisItem[];
164
+ items: BreadcrumbMenuItem[];
201
165
  sideOffset: number;
166
+ align?: "start" | "center" | "end";
202
167
  }) {
203
168
  const iconAxis = useIconAxis();
204
169
  return (
205
170
  <RPopover.Portal>
206
171
  <RPopover.Content
207
- align="start"
172
+ align={align}
208
173
  sideOffset={sideOffset}
209
174
  collisionPadding={8}
210
175
  className={[styles.popoverContent, "m-float"].join(" ")}
@@ -222,8 +187,14 @@ function EllipsisContent({
222
187
  : item.leftIcon,
223
188
  ]
224
189
  : undefined;
190
+ const trailing = item.active ? [<Icon key="check" name="check" size={14} />] : undefined;
225
191
  const inner = (
226
- <MenuItem size="sm" disabled={item.disabled} leadingActions={leading}>
192
+ <MenuItem
193
+ size="sm"
194
+ disabled={item.disabled}
195
+ leadingActions={leading}
196
+ trailingActions={trailing}
197
+ >
227
198
  {item.label}
228
199
  </MenuItem>
229
200
  );
@@ -234,6 +205,7 @@ function EllipsisContent({
234
205
  <a
235
206
  href={item.href}
236
207
  role="menuitem"
208
+ aria-current={item.active ? "page" : undefined}
237
209
  className={styles.popoverItemLink}
238
210
  aria-disabled={item.disabled || undefined}
239
211
  >
@@ -245,6 +217,7 @@ function EllipsisContent({
245
217
  <button
246
218
  type="button"
247
219
  role="menuitem"
220
+ aria-current={item.active ? "page" : undefined}
248
221
  disabled={item.disabled}
249
222
  onClick={() => item.onSelect?.()}
250
223
  className={styles.popoverItemButton}
@@ -263,17 +236,117 @@ function EllipsisContent({
263
236
  );
264
237
  }
265
238
 
239
+ // ────────────────────────────────────────────────────────────
240
+ // Ellipsis — 항목이 많을 때 중간 축약. 클릭 시 팝오버로 숨긴 항목 리스트를 연다.
241
+ // ────────────────────────────────────────────────────────────
242
+
243
+ export interface BreadcrumbEllipsisProps
244
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onSelect"> {
245
+ /** 숨긴 항목 리스트. 최소 1개. */
246
+ items: BreadcrumbMenuItem[];
247
+ /** 팝오버 side offset (px). 기본 6. */
248
+ sideOffset?: number;
249
+ /** aria-label. 기본 "Show hidden pages". */
250
+ "aria-label"?: string;
251
+ }
252
+
253
+ const Ellipsis = forwardRef<HTMLButtonElement, BreadcrumbEllipsisProps>(
254
+ (
255
+ {
256
+ className,
257
+ items,
258
+ sideOffset = 6,
259
+ "aria-label": ariaLabel = "Show hidden pages",
260
+ ...rest
261
+ },
262
+ ref,
263
+ ) => {
264
+ return (
265
+ <RPopover.Root>
266
+ <RPopover.Trigger asChild>
267
+ <button
268
+ ref={ref}
269
+ type="button"
270
+ aria-label={ariaLabel}
271
+ className={[styles.ellipsisTrigger, className ?? ""].filter(Boolean).join(" ")}
272
+ {...rest}
273
+ >
274
+ &hellip;
275
+ </button>
276
+ </RPopover.Trigger>
277
+ <PopoverMenuContent items={items} sideOffset={sideOffset} />
278
+ </RPopover.Root>
279
+ );
280
+ },
281
+ );
282
+ Ellipsis.displayName = "Breadcrumb.Ellipsis";
283
+
284
+ // ────────────────────────────────────────────────────────────
285
+ // Menu — segment 자체가 트리거. ▾ caret으로 형제 전환 UI 노출.
286
+ // active=true면 aria-current="page" 유지 (현재 페이지 자리로 쓸 때).
287
+ // ────────────────────────────────────────────────────────────
288
+
289
+ export interface BreadcrumbMenuProps
290
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onSelect"> {
291
+ /** 트리거에 표시할 라벨. 아이콘 단독 모드 지원 안 함 — 메뉴 트리거는 라벨 필수. */
292
+ children: ReactNode;
293
+ /** 라벨 앞 아이콘. */
294
+ leftIcon?: ReactNode;
295
+ /** 메뉴에 넣을 형제/대체 페이지들. */
296
+ items: BreadcrumbMenuItem[];
297
+ /** 현재 페이지 자리에 쓸 때 true. aria-current="page" + fg-strong 톤. */
298
+ active?: boolean;
299
+ /** 팝오버 side offset (px). 기본 6. */
300
+ sideOffset?: number;
301
+ }
302
+
303
+ const Menu = forwardRef<HTMLButtonElement, BreadcrumbMenuProps>(
304
+ ({ className, children, leftIcon, items, active = false, sideOffset = 6, ...rest }, ref) => {
305
+ const cls = [styles.menuTrigger, active ? styles.menuTriggerActive : "", className ?? ""]
306
+ .filter(Boolean)
307
+ .join(" ");
308
+ return (
309
+ <RPopover.Root>
310
+ <RPopover.Trigger asChild>
311
+ <button
312
+ ref={ref}
313
+ type="button"
314
+ aria-current={active ? "page" : undefined}
315
+ aria-haspopup="menu"
316
+ className={cls}
317
+ {...rest}
318
+ >
319
+ {leftIcon && (
320
+ <span className={styles.linkIcon} aria-hidden>
321
+ {sizedIcon(leftIcon)}
322
+ </span>
323
+ )}
324
+ <span className={styles.linkLabel}>{children}</span>
325
+ <span className={styles.menuCaret} aria-hidden>
326
+ <Icon name="caret-down" size={ICON_SIZE} />
327
+ </span>
328
+ </button>
329
+ </RPopover.Trigger>
330
+ <PopoverMenuContent items={items} sideOffset={sideOffset} />
331
+ </RPopover.Root>
332
+ );
333
+ },
334
+ );
335
+ Menu.displayName = "Breadcrumb.Menu";
336
+
266
337
  // ────────────────────────────────────────────────────────────
267
338
 
268
339
  type BreadcrumbCompound = typeof Root & {
269
340
  Link: typeof Link;
270
341
  Current: typeof Current;
271
342
  Ellipsis: typeof Ellipsis;
343
+ Menu: typeof Menu;
272
344
  };
273
345
 
274
346
  const Breadcrumb = Root as BreadcrumbCompound;
275
347
  Breadcrumb.Link = Link;
276
348
  Breadcrumb.Current = Current;
277
349
  Breadcrumb.Ellipsis = Ellipsis;
350
+ Breadcrumb.Menu = Menu;
278
351
 
279
352
  export { Breadcrumb };