@qijenchen/design-system 0.1.0-beta.53 → 0.1.0-beta.54
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.
- package/dist/components/Avatar/avatar.d.ts +2 -1
- package/dist/components/Avatar/avatar.d.ts.map +1 -1
- package/dist/components/Avatar/avatar.js +3 -16
- package/dist/components/Avatar/avatar.js.map +1 -1
- package/dist/components/Calendar/calendar.d.ts +5 -3
- package/dist/components/Calendar/calendar.d.ts.map +1 -1
- package/dist/components/Calendar/calendar.js +3 -16
- package/dist/components/Calendar/calendar.js.map +1 -1
- package/dist/components/Tag/tag.d.ts +26 -14
- package/dist/components/Tag/tag.d.ts.map +1 -1
- package/dist/components/Tag/tag.js +25 -35
- package/dist/components/Tag/tag.js.map +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +3 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/tokens/categorical-color.d.ts +60 -0
- package/dist/tokens/categorical-color.d.ts.map +1 -0
- package/dist/tokens/categorical-color.js +110 -0
- package/dist/tokens/categorical-color.js.map +1 -0
- package/ds-canonical/references/structural-token-retention.md +1 -1
- package/ds-story-manifest.json +1 -1
- package/package.json +1 -1
- package/src/components/Avatar/avatar.anatomy.stories.tsx +23 -22
- package/src/components/Avatar/avatar.spec.md +23 -15
- package/src/components/Avatar/avatar.tsx +11 -22
- package/src/components/Calendar/calendar.anatomy.stories.tsx +9 -9
- package/src/components/Calendar/calendar.principles.stories.tsx +1 -1
- package/src/components/Calendar/calendar.spec.md +1 -1
- package/src/components/Calendar/calendar.tsx +11 -25
- package/src/components/DropdownMenu/dropdown-menu.spec.md +6 -0
- package/src/components/FileViewer/file-viewer.spec.md +6 -0
- package/src/components/Tag/tag.anatomy.stories.tsx +38 -30
- package/src/components/Tag/tag.spec.md +66 -43
- package/src/components/Tag/tag.tsx +36 -45
- package/src/lib/utils.ts +2 -1
- package/src/patterns/horizontal-overflow/horizontal-overflow.spec.md +6 -0
- package/src/patterns/overlay-surface/overlay-surface.spec.md +6 -0
- package/src/tokens/categorical-color.ts +164 -0
- package/src/tokens/color/color.spec.md +22 -19
- package/src/tokens/color/semantic.css +20 -11
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { LucideIcon } from 'lucide-react';
|
|
3
|
+
import { type CategoricalColor } from '../../tokens/categorical-color';
|
|
3
4
|
/**
|
|
4
5
|
* Avatar — 頭像元件
|
|
5
6
|
*
|
|
@@ -17,7 +18,7 @@ import type { LucideIcon } from 'lucide-react';
|
|
|
17
18
|
* circle(預設)→ rounded-full,用於人物
|
|
18
19
|
* square → rounded-md (4px),用於實體(專案、組織、App)
|
|
19
20
|
*/
|
|
20
|
-
type ColorKey =
|
|
21
|
+
type ColorKey = CategoricalColor;
|
|
21
22
|
export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
22
23
|
/** 尺寸:number (px) 或 'fill'(填滿父容器,由父層決定大小)。預設 32 */
|
|
23
24
|
size?: number | 'fill';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/components/Avatar/avatar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/components/Avatar/avatar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,0CAA0C,CAAA;AAMrH;;;;;;;;;;;;;;;;GAgBG;AAQH,KAAK,QAAQ,GAAG,gBAAgB,CAAA;AA6DhC,MAAM,WAAW,WAAY,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IACvE,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,aAAa;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0BAA0B;IAC1B,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,QAAQ,CAAA;IAChB,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC/C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAiMD,MAAM,WAAW,UAAU;IACzB,aAAa;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,2CAA2C;IAC3C,KAAK,CAAC,EAAE,QAAQ,CAAA;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAID,eAAO,MAAM,UAAU;;;;;;;;;;;CAeb,CAAA;AAGV,QAAA,MAAM,MAAM,+GAA0B,CAAA;AAEtC,OAAO,EAAE,MAAM,EAAE,CAAA"}
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { User } from "lucide-react";
|
|
4
4
|
import { cn } from "../../lib/utils.js";
|
|
5
|
+
import { CAT_SOLID_TOKENS, CAT_SUBTLE_TOKENS } from "../../tokens/categorical-color.js";
|
|
5
6
|
import { HoverCard, HoverCardTrigger, HoverCardContent } from "../HoverCard/hover-card.js";
|
|
6
7
|
import { HOVER_DELAY_CLOSE_MS, HOVER_DELAY_RICH_MS } from "../../tokens/motion/motion.js";
|
|
7
8
|
import { Badge } from "../Badge/badge.js";
|
|
@@ -9,25 +10,11 @@ import { useTableIsScrolling, useFieldContext } from "../Field/field-context.js"
|
|
|
9
10
|
const COLOR_MAP = {
|
|
10
11
|
subtle: {
|
|
11
12
|
neutral: { bg: "var(--muted)", text: "var(--foreground)" },
|
|
12
|
-
|
|
13
|
-
red: { bg: "var(--color-deep-orange-1)", text: "var(--color-deep-orange-7)" },
|
|
14
|
-
green: { bg: "var(--color-green-1)", text: "var(--color-green-7)" },
|
|
15
|
-
yellow: { bg: "var(--color-yellow-1)", text: "var(--color-yellow-7)" },
|
|
16
|
-
turquoise: { bg: "var(--color-turquoise-1)", text: "var(--color-turquoise-7)" },
|
|
17
|
-
purple: { bg: "var(--color-purple-1)", text: "var(--color-purple-7)" },
|
|
18
|
-
magenta: { bg: "var(--color-magenta-1)", text: "var(--color-magenta-7)" },
|
|
19
|
-
indigo: { bg: "var(--color-indigo-1)", text: "var(--color-indigo-7)" }
|
|
13
|
+
...CAT_SUBTLE_TOKENS
|
|
20
14
|
},
|
|
21
15
|
solid: {
|
|
22
16
|
neutral: { bg: "var(--color-neutral-9)", text: "var(--inverse-fg)" },
|
|
23
|
-
|
|
24
|
-
red: { bg: "var(--color-deep-orange-6)", text: "var(--on-emphasis)" },
|
|
25
|
-
green: { bg: "var(--color-green-6)", text: "var(--on-emphasis)" },
|
|
26
|
-
yellow: { bg: "var(--color-yellow-6)", text: "var(--warning-foreground)" },
|
|
27
|
-
turquoise: { bg: "var(--color-turquoise-6)", text: "var(--on-emphasis)" },
|
|
28
|
-
purple: { bg: "var(--color-purple-6)", text: "var(--on-emphasis)" },
|
|
29
|
-
magenta: { bg: "var(--color-magenta-6)", text: "var(--on-emphasis)" },
|
|
30
|
-
indigo: { bg: "var(--color-indigo-6)", text: "var(--on-emphasis)" }
|
|
17
|
+
...CAT_SOLID_TOKENS
|
|
31
18
|
}
|
|
32
19
|
};
|
|
33
20
|
function getIconSize(avatarSize) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avatar.js","sources":["../../../src/components/Avatar/avatar.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport { User } from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { HoverCard, HoverCardTrigger, HoverCardContent } from '@/design-system/components/HoverCard/hover-card'\nimport { HOVER_DELAY_RICH_MS, HOVER_DELAY_CLOSE_MS } from '@/design-system/tokens/motion/motion'\nimport { Badge } from '@/design-system/components/Badge/badge'\nimport { useFieldContext, useTableIsScrolling } from '@/design-system/components/Field/field-context'\n\n/**\n * Avatar — 頭像元件\n *\n * 三種內容模式(按優先順序):\n * 1. src → 圖片\n * 2. icon → icon 在底色圓/方形內\n * 3. alt → 取首字作文字 fallback\n * 4. 都沒有 → 預設 User icon\n *\n * ── 尺寸 ──\n * size 接受任意 px 值,icon 自動 = round_even(size × 0.6)\n * 文字 fallback 字體 = size × 0.5\n *\n * ── 形狀 ──\n * circle(預設)→ rounded-full,用於人物\n * square → rounded-md (4px),用於實體(專案、組織、App)\n */\n\n// ── 色彩 ──\n// 直接引用 primitive(bg=step-1, text=step-7),不經過語義層\n// solid:step-6 全色底 + 白色前景(yellow 例外用 --warning-foreground)\n// neutral solid:neutral-9 + --inverse-fg(自動反轉)\ntype ColorKey = 'neutral' | 'blue' | 'red' | 'green' | 'yellow' | 'turquoise' | 'purple' | 'magenta' | 'indigo'\ntype VariantKey = 'subtle' | 'solid'\n\nconst COLOR_MAP: Record<VariantKey, Record<ColorKey, { bg: string; text: string }>> = {\n subtle: {\n neutral: { bg: 'var(--muted)', text: 'var(--foreground)' },\n blue: { bg: 'var(--color-blue-1)', text: 'var(--color-blue-7)' },\n red: { bg: 'var(--color-deep-orange-1)', text: 'var(--color-deep-orange-7)' },\n green: { bg: 'var(--color-green-1)', text: 'var(--color-green-7)' },\n yellow: { bg: 'var(--color-yellow-1)', text: 'var(--color-yellow-7)' },\n turquoise: { bg: 'var(--color-turquoise-1)', text: 'var(--color-turquoise-7)' },\n purple: { bg: 'var(--color-purple-1)', text: 'var(--color-purple-7)' },\n magenta: { bg: 'var(--color-magenta-1)', text: 'var(--color-magenta-7)' },\n indigo: { bg: 'var(--color-indigo-1)', text: 'var(--color-indigo-7)' },\n },\n solid: {\n neutral: { bg: 'var(--color-neutral-9)', text: 'var(--inverse-fg)' },\n blue: { bg: 'var(--color-blue-6)', text: 'var(--on-emphasis)' },\n red: { bg: 'var(--color-deep-orange-6)', text: 'var(--on-emphasis)' },\n green: { bg: 'var(--color-green-6)', text: 'var(--on-emphasis)' },\n yellow: { bg: 'var(--color-yellow-6)', text: 'var(--warning-foreground)' },\n turquoise: { bg: 'var(--color-turquoise-6)', text: 'var(--on-emphasis)' },\n purple: { bg: 'var(--color-purple-6)', text: 'var(--on-emphasis)' },\n magenta: { bg: 'var(--color-magenta-6)', text: 'var(--on-emphasis)' },\n indigo: { bg: 'var(--color-indigo-6)', text: 'var(--on-emphasis)' },\n },\n}\n\n// ── Icon size: round to nearest even, ≈ 60% ──\nfunction getIconSize(avatarSize: number): number {\n return Math.round((avatarSize * 0.6) / 2) * 2\n}\n\n// ── Text fallback: first character ──\nfunction getInitial(text: string): string {\n return text.trim().charAt(0).toUpperCase()\n}\n\n// Semantic presence tokens — 見 color/semantic.css\n// Module-level constant(2026-04-22 D3 perf audit):從 render body 移到 module scope,\n// 避免每次 Avatar render 都 re-declare 此 4-entry object(Low impact 但渲染大量 avatars 時累積可觀)\nconst STATUS_DOT_COLOR: Record<string, string> = {\n online: 'var(--status-online)',\n away: 'var(--status-away)',\n busy: 'var(--status-busy)',\n offline: 'var(--status-offline)',\n}\n\n// ── useDocumentTheme(2026-04-23;M3 Portal 逃脫防線,scope verified 2026-04-25)──\n// 讀 `<html data-theme>` 並 observe mutation。用於 Avatar hoverCard ProfileCard:\n// Portal 後的 HoverCardContent 會繼承 trigger subtree theme(如 OverflowIndicator\n// dark tooltip 內部),造成 ProfileCard 被污染成 dark。顯式 bind app-level theme\n// 確保 ProfileCard 永遠跟 app 本身 theme 一致(light-in-light-app / dark-in-dark-app)。\n//\n// 範圍 audit 2026-04-25:觀察對象是 `document.documentElement` 自有 DOM,非 3rd-party\n// lib 內部(不屬 M2 scope);attributeFilter 限定 `data-theme` 單一 attr,re-render 成本\n// 為每次全站 theme 切換 × Avatar 數量,可接受。\nfunction useDocumentTheme(): string | null {\n const [theme, setTheme] = React.useState<string | null>(() =>\n typeof document !== 'undefined' ? document.documentElement.getAttribute('data-theme') : null,\n )\n React.useEffect(() => {\n if (typeof document === 'undefined') return\n const root = document.documentElement\n const update = () => setTheme(root.getAttribute('data-theme'))\n update()\n const obs = new MutationObserver(update)\n obs.observe(root, { attributes: true, attributeFilter: ['data-theme'] })\n return () => obs.disconnect()\n }, [])\n return theme\n}\n\n// ── Component ──\n\nexport interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {\n /** 尺寸:number (px) 或 'fill'(填滿父容器,由父層決定大小)。預設 32 */\n size?: number | 'fill'\n /** 形狀:circle(人物)或 square(實體),預設 circle */\n shape?: 'circle' | 'square'\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt?: string\n /** Icon 模式(LucideIcon) */\n icon?: LucideIcon\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /** 深底白字模式(step-6 背景 + 白色前景,warning 例外),預設 false */\n solid?: boolean\n /**\n * 在線狀態指示器(presence),顯示在 avatar **右下角**。\n * 世界級對照:Slack / Teams / Discord — `online` 是最廣泛被理解的術語。\n * 位置語義:右下 = \"此人的 presence\"(使用者聚焦於「這個人是誰 + 現在 在不在」)。\n */\n status?: 'online' | 'away' | 'busy' | 'offline'\n /**\n * 未讀 / 通知計數 badge,顯示在 avatar **右上角**。\n * 世界級對照:chat app(iMessage / Slack thread / LINE / WhatsApp)一律右上角。\n * 位置語義:右上 = \"關於此對話的新事件數量\"(使用者聚焦於「有多少未處理」);\n * 與右下的 presence 共存不衝突(不同角、不同語義)。\n * `> 99` 自動顯示 \"99+\"(交給內部 Badge 的 `max` 行為)。\n */\n badgeCount?: number\n /**\n * 傳入 HoverCard 內容(如 ProfileCard),hover avatar 時自動顯示。\n * 只有人員 avatar 需要傳;實體 avatar(專案、組織)不傳。\n */\n hoverCard?: React.ReactNode\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\n// 2026-05-13 (a) perf fix part-2(per codex Layer C Roadmap rich-cell dominant + user 拍 Path (a)):\n// `React.memo` wrap forwardRef Avatar — Roadmap 13 columns 含 person/multiPerson,每 row 多 avatar\n// × HoverCard subtree + useDocumentTheme observer = 重渲染 hotspot。memo shallow-equal props,\n// HoverCard / themeRef stable across scroll 時 skip re-render。對齊 codex Profile Plan step 5\n// (filter Avatar/PeoplePicker/FieldSurfaceProvider remounts)。\n// code-quality-allow: long-function — size × shape × color × solid × status × badgeCount × hoverCard × img-fallback 多軸 prop 組合,拆 sub-fn 會跨 fn 傳 imgError state + isTableScrolling observer 結果\nconst AvatarInner = React.forwardRef<HTMLDivElement, AvatarProps>(\n ({ size = 32, shape = 'circle', src, alt, icon: Icon, color = 'neutral', solid = false, status, badgeCount, hoverCard, className, style, ...props }, ref) => {\n const [imgError, setImgError] = React.useState(false)\n const documentTheme = useDocumentTheme()\n const isTableScrolling = useTableIsScrolling()\n // 2026-05-13 R3.5(per codex Q3 verdict + user 拍「想盡辦法 auto-handle prereq」):\n // Avatar self-dim when in disabled Field wrapper context(取代既有 wrapper opacity-disabled blanket\n // 逃生艙 — color.spec.md:729 specific-disabled-color canonical)。\n // Scope narrowest:`fieldCtx?.mode === 'disabled' && fieldCtx?.hasFieldWrapper === true`,標準 Field\n // 家族 wrapper disabled 時才 dim;**沒包在 Field wrapper 內的 standalone Avatar**(ProfileCard / FileItem /\n // HoverCard / Dialog 等 display 場景)**backward compat 不變**。對齊 avatar.spec.md「Avatar 在 disabled\n // 元件內 host-controlled opacity」canonical — 升級成「Avatar self-managed via fieldCtx」。\n const fieldCtx = useFieldContext()\n const isDisabledInField = fieldCtx?.mode === 'disabled' && fieldCtx?.hasFieldWrapper === true\n const isFill = size === 'fill'\n // Fill 模式下 icon 用 60% 寬高、text 用 50cqi(container query inline-size);\n // 數字模式下用既有 px 計算\n const numSize = isFill ? 32 : (size as number)\n const iconPx = getIconSize(numSize)\n const fontSizePx = Math.round(numSize * 0.5)\n const variantKey: VariantKey = solid ? 'solid' : 'subtle'\n const colors = COLOR_MAP[variantKey]?.[color] ?? COLOR_MAP.subtle.neutral\n const radius = shape === 'circle' ? '9999px' : '4px'\n\n // 決定內容\n const showImage = src && !imgError\n const showIcon = !showImage && (Icon || (!alt))\n const showText = !showImage && !showIcon && alt\n\n const FallbackIcon = Icon ?? User\n\n // Status dot 尺寸:avatar 的 28%(Slack / Teams / Discord 世界級平均),\n // clamp [8, 16] — floor 8 保小 avatar 仍可辨識但不喧賓奪主(10 floor 會讓 24px\n // avatar 的 dot 占 42% 太大);ceiling 16 防大 avatar dot 過度放大\n const dotSize = isFill ? 10 : Math.max(8, Math.min(16, Math.round(numSize * 0.28)))\n // Border ring 在 surface 上分離 dot 與 avatar,dotSize ≥ 12 時升階到 3px 保持視覺比例\n const dotBorder = dotSize >= 12 ? 3 : 2\n\n const avatarEl = (\n <div\n className={cn(\n 'inline-flex items-center justify-center shrink-0 overflow-hidden select-none',\n isFill && 'w-full h-full',\n // 2026-05-13 R3.5 self-dim:Avatar 在 disabled Field wrapper context 內自 dim\n // (取代 field-wrapper.tsx default/bare/naked disabled blanket opacity-disabled 逃生艙)\n isDisabledInField && 'opacity-disabled',\n )}\n style={{\n ...(isFill\n ? { containerType: 'inline-size' as React.CSSProperties['containerType'] }\n : { width: numSize, height: numSize }),\n borderRadius: radius,\n backgroundColor: showImage ? undefined : colors.bg,\n color: showImage ? undefined : colors.text,\n }}\n data-avatar-size={isFill ? 'fill' : numSize}\n role={!showImage && alt && !hoverCard ? 'img' : undefined}\n aria-label={!showImage && alt && !hoverCard ? alt : undefined}\n >\n {showImage && (\n <img\n src={src}\n alt={alt ?? ''}\n className=\"w-full h-full object-cover\"\n onError={() => setImgError(true)}\n />\n )}\n {showIcon && (\n isFill\n ? <FallbackIcon className=\"w-[60%] h-[60%]\" aria-hidden />\n : <FallbackIcon size={iconPx} aria-hidden />\n )}\n {showText && (\n <span\n className=\"font-medium leading-none\"\n style={{ fontSize: isFill ? '50cqi' : fontSizePx }}\n aria-hidden\n >\n {getInitial(alt!)}\n </span>\n )}\n </div>\n )\n\n const hasOverlay = status || typeof badgeCount === 'number'\n // Keyboard access canonical(D4 UX audit 2026-04-22 finding):Avatar with `hoverCard`\n // 需 keyboard 可達 — Radix `HoverCardTrigger asChild` 不自動加 tabIndex,non-focusable\n // `<div>` 會讓 keyboard-only user 無法 reach ProfileCard popover(WCAG 2.1.1 / 4.1.2 違反)。\n // 解:當 `hoverCard` 存在時,wrapper `<div>` 變 focusable(`tabIndex=0` + `role=\"button\"` +\n // `aria-haspopup=\"dialog\"` + focus-visible ring)。若無 hoverCard 則維持純展示 `<div>`。\n const focusableProps = hoverCard\n ? {\n tabIndex: 0,\n role: 'button' as const,\n 'aria-haspopup': 'dialog' as const,\n 'aria-label': alt ?? 'View profile',\n }\n : {}\n // 2026-05-31:focus ring 圓角跟隨 shape(circle→rounded-full / square→rounded-md 對齊 body radius L173),\n // 原寫死 rounded-full 會讓方形 avatar(實體)配 hoverCard 時出現圓形 ring。hoverCard 為通用行為(任意內容),\n // 方形 avatar 合法可配(內容非 ProfileCard 而已),故 ring 必跟形狀。\n const focusableClass = hoverCard\n ? cn('focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1', shape === 'circle' ? 'rounded-full' : 'rounded-md')\n : ''\n const baseEl = !hasOverlay\n ? <div ref={ref} className={cn('inline-flex shrink-0', focusableClass, className)} style={style} {...focusableProps} {...props}>{avatarEl}</div>\n : (\n <div ref={ref} className={cn('relative inline-flex shrink-0', focusableClass, className)} style={style} {...focusableProps} {...props}>\n {avatarEl}\n {/* Status dot:bottom-right(presence — 世界級對照 Slack / Teams / Discord),\n 落在 circle avatar 圓周 45° 位置 / square avatar 右下直角;\n border ring 用 surface 色讓 dot 從 avatar 邊界視覺分離。\n a11y:`aria-hidden` — presence 資訊整合到 parent avatar 的 aria-label\n (world-class Slack 做法),避免多 `role=\"status\"` 造成 screen reader 洪水 */}\n {status && (\n <span\n className=\"absolute block rounded-full\"\n style={{\n width: dotSize,\n height: dotSize,\n bottom: 0,\n right: 0,\n backgroundColor: STATUS_DOT_COLOR[status],\n boxShadow: `0 0 0 ${dotBorder}px var(--surface-raised, var(--canvas))`,\n }}\n aria-hidden\n />\n )}\n {/* Count badge:top-right(chat 未讀 / 通知計數 — 世界級對照 iMessage /\n Slack thread / LINE / WhatsApp)。消費 DS Badge(critical variant),\n 再加 ring 與 avatar 分離 */}\n {typeof badgeCount === 'number' && badgeCount > 0 && (\n <Badge\n variant=\"critical\"\n count={badgeCount}\n max={99}\n className=\"absolute -top-1 -right-1\"\n style={{\n boxShadow: `0 0 0 2px var(--surface-raised, var(--canvas))`,\n }}\n aria-label={`${badgeCount} unread`}\n />\n )}\n </div>\n )\n\n // 2026-05-13 (c) scroll-defer perf(per user 拍 Path (c) + codex Q3 verdict):\n // DataTable scrolling 期間跳 HoverCard wrapper(Portal + useDocumentTheme observer 是\n // Roadmap 重渲 hotspot,per codex Layer C 分析)。scroll 結束 → context flips false →\n // re-render 接回完整 HoverCard tree(ProfileCard 仍可 hover 顯示)。\n // 對齊 AG Grid `deferRender` for slow React cell components / MUI X DataGrid scroll-defer。\n if (!hoverCard || isTableScrolling) return baseEl\n\n return (\n <HoverCard openDelay={HOVER_DELAY_RICH_MS} closeDelay={HOVER_DELAY_CLOSE_MS}>\n <HoverCardTrigger asChild>\n {baseEl}\n </HoverCardTrigger>\n {/* HoverCardContent canonical(2026-04-23):\n - 無 inner padding(consumer ProfileCard 自帶 `px-4 py-3` chrome)\n - `overflow-hidden` + `rounded-lg` → child(ProfileCard)圓角裁切\n - **不設 max-height**:ProfileCard 自己消費 `--radix-hover-card-content-available-height`\n 自約束高度 + 內部 ScrollArea 處理捲動\n - `data-theme={documentTheme}`:ProfileCard 永遠跟隨 **app-level theme**(從 `<html data-theme>`\n 動態讀),不受 trigger subtree theme 污染。範例:Avatar 位於 OverflowIndicator 的 dark\n tooltip 內,其 Portal 會繼承該 subtree dark theme → ProfileCard 變全黑。顯式設回 app theme\n 確保 ProfileCard 永遠 light-in-light-app / dark-in-dark-app。 */}\n <HoverCardContent\n data-theme={documentTheme ?? undefined}\n className=\"bg-surface-raised rounded-lg border border-border overflow-hidden\"\n style={{ boxShadow: 'var(--elevation-200)' }}\n >\n {hoverCard}\n </HoverCardContent>\n </HoverCard>\n )\n }\n)\nAvatarInner.displayName = 'AvatarInner'\n\n// ── AvatarData ─────────────────────────────────────────────────────────────\n// 資料型別,讓 consumer 傳資料而非 ReactNode。\n// 接收端內部用 Avatar 元件渲染,統一控制尺寸與 fallback。\n\nexport interface AvatarData {\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt: string\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /**\n * Person avatar hover ProfileCard(DS-wide canonical,person avatar 預設必有,見 avatar.spec.md)。\n * Entity avatar(專案 / 組織 logo)不帶 → consumer 不傳 hoverCard 即豁免。\n * 所有消費 AvatarData 的 primitive(MenuItem / DropdownMenu / SelectMenu / SelectionItem / ProfileCard)\n * 需 forward 此 prop 到內部 <Avatar hoverCard={avatar.hoverCard} />。\n */\n hoverCard?: React.ReactNode\n}\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const avatarMeta = {\n component: 'Avatar',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-surface-raised'],\n fg: ['--foreground', '--on-emphasis'],\n ring: ['ring-ring'],\n },\n} as const\n\nAvatarInner.displayName = 'Avatar'\nconst Avatar = React.memo(AvatarInner)\n\nexport { Avatar }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"avatar.js","sources":["../../../src/components/Avatar/avatar.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport { User } from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { CAT_SUBTLE_TOKENS, CAT_SOLID_TOKENS, type CategoricalColor } from '@/design-system/tokens/categorical-color'\nimport { HoverCard, HoverCardTrigger, HoverCardContent } from '@/design-system/components/HoverCard/hover-card'\nimport { HOVER_DELAY_RICH_MS, HOVER_DELAY_CLOSE_MS } from '@/design-system/tokens/motion/motion'\nimport { Badge } from '@/design-system/components/Badge/badge'\nimport { useFieldContext, useTableIsScrolling } from '@/design-system/components/Field/field-context'\n\n/**\n * Avatar — 頭像元件\n *\n * 三種內容模式(按優先順序):\n * 1. src → 圖片\n * 2. icon → icon 在底色圓/方形內\n * 3. alt → 取首字作文字 fallback\n * 4. 都沒有 → 預設 User icon\n *\n * ── 尺寸 ──\n * size 接受任意 px 值,icon 自動 = round_even(size × 0.6)\n * 文字 fallback 字體 = size × 0.5\n *\n * ── 形狀 ──\n * circle(預設)→ rounded-full,用於人物\n * square → rounded-md (4px),用於實體(專案、組織、App)\n */\n\n// ── 色彩 ──\n// **消費 categorical-color SSOT**(CAT_SUBTLE_TOKENS / CAT_SOLID_TOKENS,key X 一律對 `--color-X-*`,\n// 1:1 零 offset)。subtle=step-1 底 + step-7 字;solid=step-6 全色底 + on-emphasis 字\n//(亮 hue yellow/amber/orange/lime 用 --on-emphasis-dark 深字;green 白字例外)。neutral 非色相,自處理(subtle=muted、solid=neutral-9)。\n// 2026-06-04 修:原 `red` 誤接 `--color-deep-orange-*`(red=品牌紅 hue-25 ≠ deep-orange);\n// 改消費 SSOT 後 red→`--color-red-*`,並補齊全 12 色相。\ntype ColorKey = CategoricalColor\ntype VariantKey = 'subtle' | 'solid'\n\nconst COLOR_MAP: Record<VariantKey, Record<ColorKey, { bg: string; text: string }>> = {\n subtle: {\n neutral: { bg: 'var(--muted)', text: 'var(--foreground)' },\n ...CAT_SUBTLE_TOKENS,\n },\n solid: {\n neutral: { bg: 'var(--color-neutral-9)', text: 'var(--inverse-fg)' },\n ...CAT_SOLID_TOKENS,\n },\n}\n\n// ── Icon size: round to nearest even, ≈ 60% ──\nfunction getIconSize(avatarSize: number): number {\n return Math.round((avatarSize * 0.6) / 2) * 2\n}\n\n// ── Text fallback: first character ──\nfunction getInitial(text: string): string {\n return text.trim().charAt(0).toUpperCase()\n}\n\n// Semantic presence tokens — 見 color/semantic.css\n// Module-level constant(2026-04-22 D3 perf audit):從 render body 移到 module scope,\n// 避免每次 Avatar render 都 re-declare 此 4-entry object(Low impact 但渲染大量 avatars 時累積可觀)\nconst STATUS_DOT_COLOR: Record<string, string> = {\n online: 'var(--status-online)',\n away: 'var(--status-away)',\n busy: 'var(--status-busy)',\n offline: 'var(--status-offline)',\n}\n\n// ── useDocumentTheme(2026-04-23;M3 Portal 逃脫防線,scope verified 2026-04-25)──\n// 讀 `<html data-theme>` 並 observe mutation。用於 Avatar hoverCard ProfileCard:\n// Portal 後的 HoverCardContent 會繼承 trigger subtree theme(如 OverflowIndicator\n// dark tooltip 內部),造成 ProfileCard 被污染成 dark。顯式 bind app-level theme\n// 確保 ProfileCard 永遠跟 app 本身 theme 一致(light-in-light-app / dark-in-dark-app)。\n//\n// 範圍 audit 2026-04-25:觀察對象是 `document.documentElement` 自有 DOM,非 3rd-party\n// lib 內部(不屬 M2 scope);attributeFilter 限定 `data-theme` 單一 attr,re-render 成本\n// 為每次全站 theme 切換 × Avatar 數量,可接受。\nfunction useDocumentTheme(): string | null {\n const [theme, setTheme] = React.useState<string | null>(() =>\n typeof document !== 'undefined' ? document.documentElement.getAttribute('data-theme') : null,\n )\n React.useEffect(() => {\n if (typeof document === 'undefined') return\n const root = document.documentElement\n const update = () => setTheme(root.getAttribute('data-theme'))\n update()\n const obs = new MutationObserver(update)\n obs.observe(root, { attributes: true, attributeFilter: ['data-theme'] })\n return () => obs.disconnect()\n }, [])\n return theme\n}\n\n// ── Component ──\n\nexport interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {\n /** 尺寸:number (px) 或 'fill'(填滿父容器,由父層決定大小)。預設 32 */\n size?: number | 'fill'\n /** 形狀:circle(人物)或 square(實體),預設 circle */\n shape?: 'circle' | 'square'\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt?: string\n /** Icon 模式(LucideIcon) */\n icon?: LucideIcon\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /** 深底白字模式(step-6 背景 + 白色前景,warning 例外),預設 false */\n solid?: boolean\n /**\n * 在線狀態指示器(presence),顯示在 avatar **右下角**。\n * 世界級對照:Slack / Teams / Discord — `online` 是最廣泛被理解的術語。\n * 位置語義:右下 = \"此人的 presence\"(使用者聚焦於「這個人是誰 + 現在 在不在」)。\n */\n status?: 'online' | 'away' | 'busy' | 'offline'\n /**\n * 未讀 / 通知計數 badge,顯示在 avatar **右上角**。\n * 世界級對照:chat app(iMessage / Slack thread / LINE / WhatsApp)一律右上角。\n * 位置語義:右上 = \"關於此對話的新事件數量\"(使用者聚焦於「有多少未處理」);\n * 與右下的 presence 共存不衝突(不同角、不同語義)。\n * `> 99` 自動顯示 \"99+\"(交給內部 Badge 的 `max` 行為)。\n */\n badgeCount?: number\n /**\n * 傳入 HoverCard 內容(如 ProfileCard),hover avatar 時自動顯示。\n * 只有人員 avatar 需要傳;實體 avatar(專案、組織)不傳。\n */\n hoverCard?: React.ReactNode\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\n// 2026-05-13 (a) perf fix part-2(per codex Layer C Roadmap rich-cell dominant + user 拍 Path (a)):\n// `React.memo` wrap forwardRef Avatar — Roadmap 13 columns 含 person/multiPerson,每 row 多 avatar\n// × HoverCard subtree + useDocumentTheme observer = 重渲染 hotspot。memo shallow-equal props,\n// HoverCard / themeRef stable across scroll 時 skip re-render。對齊 codex Profile Plan step 5\n// (filter Avatar/PeoplePicker/FieldSurfaceProvider remounts)。\n// code-quality-allow: long-function — size × shape × color × solid × status × badgeCount × hoverCard × img-fallback 多軸 prop 組合,拆 sub-fn 會跨 fn 傳 imgError state + isTableScrolling observer 結果\nconst AvatarInner = React.forwardRef<HTMLDivElement, AvatarProps>(\n ({ size = 32, shape = 'circle', src, alt, icon: Icon, color = 'neutral', solid = false, status, badgeCount, hoverCard, className, style, ...props }, ref) => {\n const [imgError, setImgError] = React.useState(false)\n const documentTheme = useDocumentTheme()\n const isTableScrolling = useTableIsScrolling()\n // 2026-05-13 R3.5(per codex Q3 verdict + user 拍「想盡辦法 auto-handle prereq」):\n // Avatar self-dim when in disabled Field wrapper context(取代既有 wrapper opacity-disabled blanket\n // 逃生艙 — color.spec.md:729 specific-disabled-color canonical)。\n // Scope narrowest:`fieldCtx?.mode === 'disabled' && fieldCtx?.hasFieldWrapper === true`,標準 Field\n // 家族 wrapper disabled 時才 dim;**沒包在 Field wrapper 內的 standalone Avatar**(ProfileCard / FileItem /\n // HoverCard / Dialog 等 display 場景)**backward compat 不變**。對齊 avatar.spec.md「Avatar 在 disabled\n // 元件內 host-controlled opacity」canonical — 升級成「Avatar self-managed via fieldCtx」。\n const fieldCtx = useFieldContext()\n const isDisabledInField = fieldCtx?.mode === 'disabled' && fieldCtx?.hasFieldWrapper === true\n const isFill = size === 'fill'\n // Fill 模式下 icon 用 60% 寬高、text 用 50cqi(container query inline-size);\n // 數字模式下用既有 px 計算\n const numSize = isFill ? 32 : (size as number)\n const iconPx = getIconSize(numSize)\n const fontSizePx = Math.round(numSize * 0.5)\n const variantKey: VariantKey = solid ? 'solid' : 'subtle'\n const colors = COLOR_MAP[variantKey]?.[color] ?? COLOR_MAP.subtle.neutral\n const radius = shape === 'circle' ? '9999px' : '4px'\n\n // 決定內容\n const showImage = src && !imgError\n const showIcon = !showImage && (Icon || (!alt))\n const showText = !showImage && !showIcon && alt\n\n const FallbackIcon = Icon ?? User\n\n // Status dot 尺寸:avatar 的 28%(Slack / Teams / Discord 世界級平均),\n // clamp [8, 16] — floor 8 保小 avatar 仍可辨識但不喧賓奪主(10 floor 會讓 24px\n // avatar 的 dot 占 42% 太大);ceiling 16 防大 avatar dot 過度放大\n const dotSize = isFill ? 10 : Math.max(8, Math.min(16, Math.round(numSize * 0.28)))\n // Border ring 在 surface 上分離 dot 與 avatar,dotSize ≥ 12 時升階到 3px 保持視覺比例\n const dotBorder = dotSize >= 12 ? 3 : 2\n\n const avatarEl = (\n <div\n className={cn(\n 'inline-flex items-center justify-center shrink-0 overflow-hidden select-none',\n isFill && 'w-full h-full',\n // 2026-05-13 R3.5 self-dim:Avatar 在 disabled Field wrapper context 內自 dim\n // (取代 field-wrapper.tsx default/bare/naked disabled blanket opacity-disabled 逃生艙)\n isDisabledInField && 'opacity-disabled',\n )}\n style={{\n ...(isFill\n ? { containerType: 'inline-size' as React.CSSProperties['containerType'] }\n : { width: numSize, height: numSize }),\n borderRadius: radius,\n backgroundColor: showImage ? undefined : colors.bg,\n color: showImage ? undefined : colors.text,\n }}\n data-avatar-size={isFill ? 'fill' : numSize}\n role={!showImage && alt && !hoverCard ? 'img' : undefined}\n aria-label={!showImage && alt && !hoverCard ? alt : undefined}\n >\n {showImage && (\n <img\n src={src}\n alt={alt ?? ''}\n className=\"w-full h-full object-cover\"\n onError={() => setImgError(true)}\n />\n )}\n {showIcon && (\n isFill\n ? <FallbackIcon className=\"w-[60%] h-[60%]\" aria-hidden />\n : <FallbackIcon size={iconPx} aria-hidden />\n )}\n {showText && (\n <span\n className=\"font-medium leading-none\"\n style={{ fontSize: isFill ? '50cqi' : fontSizePx }}\n aria-hidden\n >\n {getInitial(alt!)}\n </span>\n )}\n </div>\n )\n\n const hasOverlay = status || typeof badgeCount === 'number'\n // Keyboard access canonical(D4 UX audit 2026-04-22 finding):Avatar with `hoverCard`\n // 需 keyboard 可達 — Radix `HoverCardTrigger asChild` 不自動加 tabIndex,non-focusable\n // `<div>` 會讓 keyboard-only user 無法 reach ProfileCard popover(WCAG 2.1.1 / 4.1.2 違反)。\n // 解:當 `hoverCard` 存在時,wrapper `<div>` 變 focusable(`tabIndex=0` + `role=\"button\"` +\n // `aria-haspopup=\"dialog\"` + focus-visible ring)。若無 hoverCard 則維持純展示 `<div>`。\n const focusableProps = hoverCard\n ? {\n tabIndex: 0,\n role: 'button' as const,\n 'aria-haspopup': 'dialog' as const,\n 'aria-label': alt ?? 'View profile',\n }\n : {}\n // 2026-05-31:focus ring 圓角跟隨 shape(circle→rounded-full / square→rounded-md 對齊 body radius L173),\n // 原寫死 rounded-full 會讓方形 avatar(實體)配 hoverCard 時出現圓形 ring。hoverCard 為通用行為(任意內容),\n // 方形 avatar 合法可配(內容非 ProfileCard 而已),故 ring 必跟形狀。\n const focusableClass = hoverCard\n ? cn('focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1', shape === 'circle' ? 'rounded-full' : 'rounded-md')\n : ''\n const baseEl = !hasOverlay\n ? <div ref={ref} className={cn('inline-flex shrink-0', focusableClass, className)} style={style} {...focusableProps} {...props}>{avatarEl}</div>\n : (\n <div ref={ref} className={cn('relative inline-flex shrink-0', focusableClass, className)} style={style} {...focusableProps} {...props}>\n {avatarEl}\n {/* Status dot:bottom-right(presence — 世界級對照 Slack / Teams / Discord),\n 落在 circle avatar 圓周 45° 位置 / square avatar 右下直角;\n border ring 用 surface 色讓 dot 從 avatar 邊界視覺分離。\n a11y:`aria-hidden` — presence 資訊整合到 parent avatar 的 aria-label\n (world-class Slack 做法),避免多 `role=\"status\"` 造成 screen reader 洪水 */}\n {status && (\n <span\n className=\"absolute block rounded-full\"\n style={{\n width: dotSize,\n height: dotSize,\n bottom: 0,\n right: 0,\n backgroundColor: STATUS_DOT_COLOR[status],\n boxShadow: `0 0 0 ${dotBorder}px var(--surface-raised, var(--canvas))`,\n }}\n aria-hidden\n />\n )}\n {/* Count badge:top-right(chat 未讀 / 通知計數 — 世界級對照 iMessage /\n Slack thread / LINE / WhatsApp)。消費 DS Badge(critical variant),\n 再加 ring 與 avatar 分離 */}\n {typeof badgeCount === 'number' && badgeCount > 0 && (\n <Badge\n variant=\"critical\"\n count={badgeCount}\n max={99}\n className=\"absolute -top-1 -right-1\"\n style={{\n boxShadow: `0 0 0 2px var(--surface-raised, var(--canvas))`,\n }}\n aria-label={`${badgeCount} unread`}\n />\n )}\n </div>\n )\n\n // 2026-05-13 (c) scroll-defer perf(per user 拍 Path (c) + codex Q3 verdict):\n // DataTable scrolling 期間跳 HoverCard wrapper(Portal + useDocumentTheme observer 是\n // Roadmap 重渲 hotspot,per codex Layer C 分析)。scroll 結束 → context flips false →\n // re-render 接回完整 HoverCard tree(ProfileCard 仍可 hover 顯示)。\n // 對齊 AG Grid `deferRender` for slow React cell components / MUI X DataGrid scroll-defer。\n if (!hoverCard || isTableScrolling) return baseEl\n\n return (\n <HoverCard openDelay={HOVER_DELAY_RICH_MS} closeDelay={HOVER_DELAY_CLOSE_MS}>\n <HoverCardTrigger asChild>\n {baseEl}\n </HoverCardTrigger>\n {/* HoverCardContent canonical(2026-04-23):\n - 無 inner padding(consumer ProfileCard 自帶 `px-4 py-3` chrome)\n - `overflow-hidden` + `rounded-lg` → child(ProfileCard)圓角裁切\n - **不設 max-height**:ProfileCard 自己消費 `--radix-hover-card-content-available-height`\n 自約束高度 + 內部 ScrollArea 處理捲動\n - `data-theme={documentTheme}`:ProfileCard 永遠跟隨 **app-level theme**(從 `<html data-theme>`\n 動態讀),不受 trigger subtree theme 污染。範例:Avatar 位於 OverflowIndicator 的 dark\n tooltip 內,其 Portal 會繼承該 subtree dark theme → ProfileCard 變全黑。顯式設回 app theme\n 確保 ProfileCard 永遠 light-in-light-app / dark-in-dark-app。 */}\n <HoverCardContent\n data-theme={documentTheme ?? undefined}\n className=\"bg-surface-raised rounded-lg border border-border overflow-hidden\"\n style={{ boxShadow: 'var(--elevation-200)' }}\n >\n {hoverCard}\n </HoverCardContent>\n </HoverCard>\n )\n }\n)\nAvatarInner.displayName = 'AvatarInner'\n\n// ── AvatarData ─────────────────────────────────────────────────────────────\n// 資料型別,讓 consumer 傳資料而非 ReactNode。\n// 接收端內部用 Avatar 元件渲染,統一控制尺寸與 fallback。\n\nexport interface AvatarData {\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt: string\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /**\n * Person avatar hover ProfileCard(DS-wide canonical,person avatar 預設必有,見 avatar.spec.md)。\n * Entity avatar(專案 / 組織 logo)不帶 → consumer 不傳 hoverCard 即豁免。\n * 所有消費 AvatarData 的 primitive(MenuItem / DropdownMenu / SelectMenu / SelectionItem / ProfileCard)\n * 需 forward 此 prop 到內部 <Avatar hoverCard={avatar.hoverCard} />。\n */\n hoverCard?: React.ReactNode\n}\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const avatarMeta = {\n component: 'Avatar',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-surface-raised'],\n fg: ['--foreground', '--on-emphasis'],\n ring: ['ring-ring'],\n },\n} as const\n\nAvatarInner.displayName = 'Avatar'\nconst Avatar = React.memo(AvatarInner)\n\nexport { Avatar }\n"],"names":[],"mappings":";;;;;;;;;AAsCA,MAAM,YAAgF;AAAA,EACpF,QAAQ;AAAA,IACN,SAAS,EAAE,IAAI,gBAAgB,MAAM,oBAAA;AAAA,IACrC,GAAG;AAAA,EAAA;AAAA,EAEL,OAAO;AAAA,IACL,SAAS,EAAE,IAAI,0BAA0B,MAAM,oBAAA;AAAA,IAC/C,GAAG;AAAA,EAAA;AAEP;AAGA,SAAS,YAAY,YAA4B;AAC/C,SAAO,KAAK,MAAO,aAAa,MAAO,CAAC,IAAI;AAC9C;AAGA,SAAS,WAAW,MAAsB;AACxC,SAAO,KAAK,KAAA,EAAO,OAAO,CAAC,EAAE,YAAA;AAC/B;AAKA,MAAM,mBAA2C;AAAA,EAC/C,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,MAAM;AAAA,EACN,SAAS;AACX;AAWA,SAAS,mBAAkC;AACzC,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM;AAAA,IAAwB,MACtD,OAAO,aAAa,cAAc,SAAS,gBAAgB,aAAa,YAAY,IAAI;AAAA,EAAA;AAE1F,QAAM,UAAU,MAAM;AACpB,QAAI,OAAO,aAAa,YAAa;AACrC,UAAM,OAAO,SAAS;AACtB,UAAM,SAAS,MAAM,SAAS,KAAK,aAAa,YAAY,CAAC;AAC7D,WAAA;AACA,UAAM,MAAM,IAAI,iBAAiB,MAAM;AACvC,QAAI,QAAQ,MAAM,EAAE,YAAY,MAAM,iBAAiB,CAAC,YAAY,GAAG;AACvE,WAAO,MAAM,IAAI,WAAA;AAAA,EACnB,GAAG,CAAA,CAAE;AACL,SAAO;AACT;AA+CA,MAAM,cAAc,MAAM;AAAA,EACxB,CAAC,EAAE,OAAO,IAAI,QAAQ,UAAU,KAAK,KAAK,MAAM,MAAM,QAAQ,WAAW,QAAQ,OAAO,QAAQ,YAAY,WAAW,WAAW,OAAO,GAAG,MAAA,GAAS,QAAQ;;AAC3J,UAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AACpD,UAAM,gBAAgB,iBAAA;AACtB,UAAM,mBAAmB,oBAAA;AAQzB,UAAM,WAAW,gBAAA;AACjB,UAAM,qBAAoB,qCAAU,UAAS,eAAc,qCAAU,qBAAoB;AACzF,UAAM,SAAS,SAAS;AAGxB,UAAM,UAAU,SAAS,KAAM;AAC/B,UAAM,SAAS,YAAY,OAAO;AAClC,UAAM,aAAa,KAAK,MAAM,UAAU,GAAG;AAC3C,UAAM,aAAyB,QAAQ,UAAU;AACjD,UAAM,WAAS,eAAU,UAAU,MAApB,mBAAwB,WAAU,UAAU,OAAO;AAClE,UAAM,SAAS,UAAU,WAAW,WAAW;AAG/C,UAAM,YAAY,OAAO,CAAC;AAC1B,UAAM,WAAW,CAAC,cAAc,QAAS,CAAC;AAC1C,UAAM,WAAW,CAAC,aAAa,CAAC,YAAY;AAE5C,UAAM,eAAe,QAAQ;AAK7B,UAAM,UAAU,SAAS,KAAK,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,MAAM,UAAU,IAAI,CAAC,CAAC;AAElF,UAAM,YAAY,WAAW,KAAK,IAAI;AAEtC,UAAM,WACJ;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,UAAU;AAAA;AAAA;AAAA,UAGV,qBAAqB;AAAA,QAAA;AAAA,QAEvB,OAAO;AAAA,UACL,GAAI,SACA,EAAE,eAAe,cAAA,IACjB,EAAE,OAAO,SAAS,QAAQ,QAAA;AAAA,UAC9B,cAAc;AAAA,UACd,iBAAiB,YAAY,SAAY,OAAO;AAAA,UAChD,OAAO,YAAY,SAAY,OAAO;AAAA,QAAA;AAAA,QAExC,oBAAkB,SAAS,SAAS;AAAA,QACpC,MAAM,CAAC,aAAa,OAAO,CAAC,YAAY,QAAQ;AAAA,QAChD,cAAY,CAAC,aAAa,OAAO,CAAC,YAAY,MAAM;AAAA,QAEnD,UAAA;AAAA,UAAA,aACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC;AAAA,cACA,KAAK,OAAO;AAAA,cACZ,WAAU;AAAA,cACV,SAAS,MAAM,YAAY,IAAI;AAAA,YAAA;AAAA,UAAA;AAAA,UAGlC,aACC,SACI,oBAAC,cAAA,EAAa,WAAU,mBAAkB,eAAW,KAAA,CAAC,IACtD,oBAAC,cAAA,EAAa,MAAM,QAAQ,eAAW,KAAA,CAAC;AAAA,UAE7C,YACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAU;AAAA,cACV,OAAO,EAAE,UAAU,SAAS,UAAU,WAAA;AAAA,cACtC,eAAW;AAAA,cAEV,qBAAW,GAAI;AAAA,YAAA;AAAA,UAAA;AAAA,QAClB;AAAA,MAAA;AAAA,IAAA;AAKN,UAAM,aAAa,UAAU,OAAO,eAAe;AAMnD,UAAM,iBAAiB,YACnB;AAAA,MACE,UAAU;AAAA,MACV,MAAM;AAAA,MACN,iBAAiB;AAAA,MACjB,cAAc,OAAO;AAAA,IAAA,IAEvB,CAAA;AAIJ,UAAM,iBAAiB,YACnB,GAAG,uGAAuG,UAAU,WAAW,iBAAiB,YAAY,IAC5J;AACJ,UAAM,SAAS,CAAC,aACZ,oBAAC,SAAI,KAAU,WAAW,GAAG,wBAAwB,gBAAgB,SAAS,GAAG,OAAe,GAAG,gBAAiB,GAAG,OAAQ,UAAA,SAAA,CAAS,IAExI,qBAAC,OAAA,EAAI,KAAU,WAAW,GAAG,iCAAiC,gBAAgB,SAAS,GAAG,OAAe,GAAG,gBAAiB,GAAG,OAC7H,UAAA;AAAA,MAAA;AAAA,MAMA,UACC;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAU;AAAA,UACV,OAAO;AAAA,YACL,OAAO;AAAA,YACP,QAAQ;AAAA,YACR,QAAQ;AAAA,YACR,OAAO;AAAA,YACP,iBAAiB,iBAAiB,MAAM;AAAA,YACxC,WAAW,SAAS,SAAS;AAAA,UAAA;AAAA,UAE/B,eAAW;AAAA,QAAA;AAAA,MAAA;AAAA,MAMd,OAAO,eAAe,YAAY,aAAa,KAC9C;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,SAAQ;AAAA,UACR,OAAO;AAAA,UACP,KAAK;AAAA,UACL,WAAU;AAAA,UACV,OAAO;AAAA,YACL,WAAW;AAAA,UAAA;AAAA,UAEb,cAAY,GAAG,UAAU;AAAA,QAAA;AAAA,MAAA;AAAA,IAC3B,GAEJ;AAQJ,QAAI,CAAC,aAAa,iBAAkB,QAAO;AAE3C,WACE,qBAAC,WAAA,EAAU,WAAW,qBAAqB,YAAY,sBACrD,UAAA;AAAA,MAAA,oBAAC,kBAAA,EAAiB,SAAO,MACtB,UAAA,QACH;AAAA,MAUA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,cAAY,iBAAiB;AAAA,UAC7B,WAAU;AAAA,UACV,OAAO,EAAE,WAAW,uBAAA;AAAA,UAEnB,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IACH,GACF;AAAA,EAEJ;AACF;AACA,YAAY,cAAc;AAwBnB,MAAM,aAAa;AAAA,EACxB,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,mBAAmB;AAAA,IACxB,IAAI,CAAC,gBAAgB,eAAe;AAAA,IACpC,MAAM,CAAC,WAAW;AAAA,EAAA;AAEtB;AAEA,YAAY,cAAc;AAC1B,MAAM,SAAS,MAAM,KAAK,WAAW;"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { type CategoricalHue } from '../../tokens/categorical-color';
|
|
2
3
|
/**
|
|
3
4
|
* Calendar — 事件檢視 canvas(月 view MVP)
|
|
4
5
|
*
|
|
@@ -25,10 +26,11 @@ export interface CalendarEvent {
|
|
|
25
26
|
end: string | Date;
|
|
26
27
|
allDay?: boolean;
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
29
|
+
* 事件類別色(categorical 色相,1:1 對 `--color-{hue}-*`)。**消費 categorical-color SSOT**,
|
|
30
|
+
* 與 Tag / Avatar 共用同一組 12 色相。2026-06-04 修:原 `orange` 與 `red` 都誤接 deep-orange;
|
|
31
|
+
* 改消費 SSOT 後 orange→`--color-orange-*`、red→`--color-red-*`(品牌紅 hue 25),各自獨立。
|
|
30
32
|
*/
|
|
31
|
-
color?:
|
|
33
|
+
color?: CategoricalHue;
|
|
32
34
|
metadata?: Record<string, unknown>;
|
|
33
35
|
}
|
|
34
36
|
export type CalendarView = 'month' | 'week' | 'day';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/components/Calendar/calendar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/components/Calendar/calendar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAe9B,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,0CAA0C,CAAA;AAIrG;;;;;;;;;;;;;;;;;GAiBG;AAIH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAA;AAEnD,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC;IAC3F,+CAA+C;IAC/C,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB,WAAW,CAAC,EAAE,YAAY,CAAA;IAC1B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;IAE3C,wBAAwB;IACxB,aAAa,CAAC,EAAE,IAAI,CAAA;IACpB,oBAAoB,CAAC,EAAE,IAAI,CAAA;IAC3B,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IAE5C,WAAW;IACX,MAAM,CAAC,EAAE,aAAa,EAAE,CAAA;IAExB,sBAAsB;IACtB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;IAC7C,uBAAuB;IACvB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IAClC,kBAAkB;IAClB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAE1B,2DAA2D;IAC3D,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;IAEpB,uBAAuB;IACvB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAA;IAE3D,oCAAoC;IACpC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB,yBAAyB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAgCD,QAAA,MAAM,QAAQ,sFAwQZ,CAAA;AAKF,eAAO,MAAM,YAAY;;;;;;;;;;;CAef,CAAA;AAEV,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
|
@@ -3,24 +3,11 @@ import * as React from "react";
|
|
|
3
3
|
import { ChevronLeft, ChevronRight, Plus } from "lucide-react";
|
|
4
4
|
import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, isSameMonth, isSameDay, format, subMonths, addMonths } from "date-fns";
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
|
+
import { CAT_ACCENT, CAT_EVENT } from "../../tokens/categorical-color.js";
|
|
6
7
|
import { Button } from "../Button/button.js";
|
|
7
8
|
import { SegmentedControl, SegmentedControlItem } from "../SegmentedControl/segmented-control.js";
|
|
8
|
-
const EVENT_COLOR_CLASSES =
|
|
9
|
-
|
|
10
|
-
green: "bg-[var(--color-green-1)] text-[var(--color-green-7)] hover:bg-[var(--color-green-2)]",
|
|
11
|
-
orange: "bg-[var(--color-deep-orange-1)] text-[var(--color-deep-orange-7)] hover:bg-[var(--color-deep-orange-2)]",
|
|
12
|
-
purple: "bg-[var(--color-purple-1)] text-[var(--color-purple-7)] hover:bg-[var(--color-purple-2)]",
|
|
13
|
-
red: "bg-[var(--color-deep-orange-1)] text-[var(--color-deep-orange-7)] hover:bg-[var(--color-deep-orange-2)]",
|
|
14
|
-
yellow: "bg-[var(--color-yellow-1)] text-[var(--color-yellow-7)] hover:bg-[var(--color-yellow-2)]"
|
|
15
|
-
};
|
|
16
|
-
const EVENT_ALLDAY_ACCENT = {
|
|
17
|
-
blue: "border-l-[3px] border-[var(--color-blue-6)]",
|
|
18
|
-
green: "border-l-[3px] border-[var(--color-green-6)]",
|
|
19
|
-
orange: "border-l-[3px] border-[var(--color-deep-orange-6)]",
|
|
20
|
-
purple: "border-l-[3px] border-[var(--color-purple-6)]",
|
|
21
|
-
red: "border-l-[3px] border-[var(--color-deep-orange-6)]",
|
|
22
|
-
yellow: "border-l-[3px] border-[var(--color-yellow-6)]"
|
|
23
|
-
};
|
|
9
|
+
const EVENT_COLOR_CLASSES = CAT_EVENT;
|
|
10
|
+
const EVENT_ALLDAY_ACCENT = CAT_ACCENT;
|
|
24
11
|
function coerceDate(value) {
|
|
25
12
|
return value instanceof Date ? value : new Date(value);
|
|
26
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar.js","sources":["../../../src/components/Calendar/calendar.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport { ChevronLeft, ChevronRight, Plus } from 'lucide-react'\nimport {\n startOfMonth,\n endOfMonth,\n startOfWeek,\n endOfWeek,\n eachDayOfInterval,\n format,\n isSameMonth,\n isSameDay,\n addMonths,\n subMonths,\n} from 'date-fns'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/design-system/components/Button/button'\nimport { SegmentedControl, SegmentedControlItem } from '@/design-system/components/SegmentedControl/segmented-control'\n\n/**\n * Calendar — 事件檢視 canvas(月 view MVP)\n *\n * 定位:看事件的 page-level canvas,對齊 Notion Calendar / Google Calendar。\n * 完整 spec 見 `event-calendar.spec.md`。\n *\n * ── Layout Family ──\n * 非 4-Family,屬 page-composite(多區塊 Toolbar + Grid + EventTile)。\n *\n * ── MVP scope(本次 session)──\n * - 月 view 完整(toolbar / grid / event tile / today highlight / outside days)\n * - 週 / 日 view 是 tech debt\n * - 拖拉增刪 event 是 tech debt\n *\n * ── 與 DatePicker 的區分 ──\n * DatePicker 是「選日期」form control;Calendar 是「看事件」page canvas。\n * 名字相近但職責完全不同,spec 頂段明示分界。\n */\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport interface CalendarEvent {\n id: string\n title: string\n /** ISO 字串 \"YYYY-MM-DD\"(all-day)或 \"YYYY-MM-DDTHH:mm\"(timed) */\n start: string | Date\n end: string | Date\n allDay?: boolean\n /**\n * 事件類別色。值為 DS primitive 色名(blue / green / orange / purple / red / yellow)。\n * 對照 Badge / Tag 的 primitive color variants。\n */\n color?: 'blue' | 'green' | 'orange' | 'purple' | 'red' | 'yellow'\n metadata?: Record<string, unknown>\n}\n\nexport type CalendarView = 'month' | 'week' | 'day'\n\nexport interface CalendarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {\n /** 當前 view(MVP 只 'month',其餘 view tech debt) */\n view?: CalendarView\n defaultView?: CalendarView\n onViewChange?: (view: CalendarView) => void\n\n /** 聚焦日期(月 view 的那個月) */\n referenceDate?: Date\n defaultReferenceDate?: Date\n onReferenceDateChange?: (date: Date) => void\n\n /** 事件資料 */\n events?: CalendarEvent[]\n\n /** 點 event tile 回調 */\n onEventClick?: (event: CalendarEvent) => void\n /** 點月 cell 回調(用於新增) */\n onDateClick?: (date: Date) => void\n /** 點新事件 CTA 回調 */\n onCreateEvent?: () => void\n\n /** 0 = Sunday, 1 = Monday。預設 0(對齊 Google Calendar 美系預設) */\n weekStartsOn?: 0 | 1\n\n /** 自訂 event tile 渲染 */\n renderEventTile?: (event: CalendarEvent) => React.ReactNode\n\n /** size(MVP 只 md;lg 為 tech debt) */\n size?: 'md' | 'lg'\n className?: string\n\n /** locale(預設 'en-US') */\n locale?: string\n\n /** ARIA labels for chrome controls. Override for i18n. */\n prevAriaLabel?: string\n nextAriaLabel?: string\n viewToggleAriaLabel?: string\n todayLabel?: string\n}\n\n// ── Event tile color tokens ─────────────────────────────────────────────────\n// 對齊 Tag / Badge 的 primitive color system(見 `color.spec.md`)\n\nconst EVENT_COLOR_CLASSES: Record<NonNullable<CalendarEvent['color']>, string> = {\n blue: 'bg-[var(--color-blue-1)] text-[var(--color-blue-7)] hover:bg-[var(--color-blue-2)]',\n green: 'bg-[var(--color-green-1)] text-[var(--color-green-7)] hover:bg-[var(--color-green-2)]',\n orange: 'bg-[var(--color-deep-orange-1)] text-[var(--color-deep-orange-7)] hover:bg-[var(--color-deep-orange-2)]',\n purple: 'bg-[var(--color-purple-1)] text-[var(--color-purple-7)] hover:bg-[var(--color-purple-2)]',\n red: 'bg-[var(--color-deep-orange-1)] text-[var(--color-deep-orange-7)] hover:bg-[var(--color-deep-orange-2)]',\n yellow: 'bg-[var(--color-yellow-1)] text-[var(--color-yellow-7)] hover:bg-[var(--color-yellow-2)]',\n}\n\n// 2026-06-01 allDay 補實作(user 拍板 A):全天事件 = 淡底 tile + 左側實心 accent 條(color-6)+ medium,\n// 視覺上明確區分「全天長條」vs 有時間事件。用 accent border 而非 solid fill 以保文字對比安全(yellow 等淺色不致白字失對比)。\n// 對齊 Google Calendar / Outlook「全天事件以強調條呈現於頂端」慣例。\nconst EVENT_ALLDAY_ACCENT: Record<NonNullable<CalendarEvent['color']>, string> = {\n blue: 'border-l-[3px] border-[var(--color-blue-6)]',\n green: 'border-l-[3px] border-[var(--color-green-6)]',\n orange: 'border-l-[3px] border-[var(--color-deep-orange-6)]',\n purple: 'border-l-[3px] border-[var(--color-purple-6)]',\n red: 'border-l-[3px] border-[var(--color-deep-orange-6)]',\n yellow: 'border-l-[3px] border-[var(--color-yellow-6)]',\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction coerceDate(value: string | Date): Date {\n return value instanceof Date ? value : new Date(value)\n}\n\nfunction eventsOnDate(events: CalendarEvent[], date: Date): CalendarEvent[] {\n return events.filter((e) => {\n const start = coerceDate(e.start)\n const end = coerceDate(e.end)\n // 日期落在 [start, end] 範圍內(日精度)\n const d = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()\n const s = new Date(start.getFullYear(), start.getMonth(), start.getDate()).getTime()\n const eEnd = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime()\n return d >= s && d <= eEnd\n })\n}\n\n// ── Component ──────────────────────────────────────────────────────────────\n\nconst MAX_TILES_PER_CELL = 3\n\nconst Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(function Calendar({\n view: viewProp,\n defaultView = 'month',\n onViewChange,\n referenceDate: referenceDateProp,\n defaultReferenceDate,\n onReferenceDateChange,\n events = [],\n onEventClick,\n onDateClick,\n onCreateEvent,\n weekStartsOn = 0,\n renderEventTile,\n size = 'md',\n className,\n locale = 'en-US',\n prevAriaLabel = '上個月', // i18n-allow: DS default; consumer override via prevAriaLabel prop\n nextAriaLabel = '下個月', // i18n-allow: DS default; consumer override via nextAriaLabel prop\n viewToggleAriaLabel = '檢視切換', // i18n-allow: DS default; consumer override via viewToggleAriaLabel prop\n todayLabel = '今天', // i18n-allow: DS default; consumer override via todayLabel prop\n ...props\n}, ref) {\n // Controlled / uncontrolled refDate\n const [internalRef, setInternalRef] = React.useState<Date>(\n defaultReferenceDate ?? new Date(),\n )\n const refDate = referenceDateProp ?? internalRef\n const setRefDate = React.useCallback(\n (next: Date) => {\n if (referenceDateProp === undefined) setInternalRef(next)\n onReferenceDateChange?.(next)\n },\n [referenceDateProp, onReferenceDateChange],\n )\n\n // View state(MVP 只用 month,其他 tech debt)\n const [internalView, setInternalView] = React.useState<CalendarView>(defaultView)\n const currentView = viewProp ?? internalView\n const setView = React.useCallback(\n (next: CalendarView) => {\n if (viewProp === undefined) setInternalView(next)\n onViewChange?.(next)\n },\n [viewProp, onViewChange],\n )\n\n // Build month grid\n const days = React.useMemo(() => {\n const monthStart = startOfMonth(refDate)\n const monthEnd = endOfMonth(refDate)\n const gridStart = startOfWeek(monthStart, { weekStartsOn })\n const gridEnd = endOfWeek(monthEnd, { weekStartsOn })\n return eachDayOfInterval({ start: gridStart, end: gridEnd })\n }, [refDate, weekStartsOn])\n\n const monthTitle = new Intl.DateTimeFormat(locale, {\n year: 'numeric',\n month: 'long',\n }).format(refDate)\n\n const today = new Date()\n\n const weekdayNames = React.useMemo(() => {\n // 取 `days[0..6]` 的名字(gridStart 開始 7 天,正好一週)\n return days.slice(0, 7).map((d) =>\n new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(d),\n )\n }, [days, locale])\n\n const handleToday = () => setRefDate(new Date())\n const handlePrev = () => setRefDate(subMonths(refDate, 1))\n const handleNext = () => setRefDate(addMonths(refDate, 1))\n\n return (\n <div\n ref={ref}\n className={cn(\n 'flex flex-col w-full h-full bg-surface rounded-md border border-divider overflow-hidden',\n className,\n )}\n data-view={currentView}\n data-size={size}\n {...props}\n >\n {/* Toolbar:[◀] [今天] [▶] title [view tabs] [+ new] */}\n <div\n className={cn(\n 'flex items-center gap-2 shrink-0 border-b border-divider',\n 'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',\n )}\n >\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"text\"\n size=\"sm\"\n iconOnly\n startIcon={ChevronLeft}\n aria-label={prevAriaLabel}\n onClick={handlePrev}\n />\n <Button variant=\"tertiary\" size=\"sm\" onClick={handleToday}>\n {todayLabel}\n </Button>\n <Button\n variant=\"text\"\n size=\"sm\"\n iconOnly\n startIcon={ChevronRight}\n aria-label={nextAriaLabel}\n onClick={handleNext}\n />\n </div>\n\n <h2 className=\"text-body-lg font-medium text-foreground flex-1 min-w-0 truncate ml-2\">\n {monthTitle}\n </h2>\n\n {/* View switcher:用 SegmentedControl(互斥多選一 canonical)——\n 對齊 CLAUDE.md「互斥分類選擇走 SegmentedControl,非 checked Button group」原則。\n Button 的 pressed 是「toggle 持續狀態」語意,不適合「單選 view 切換」 */}\n <SegmentedControl\n size=\"sm\"\n value={currentView}\n onValueChange={(v) => setView(v as CalendarView)}\n aria-label={viewToggleAriaLabel}\n >\n <SegmentedControlItem value=\"day\" disabled>日</SegmentedControlItem>\n <SegmentedControlItem value=\"week\" disabled>週</SegmentedControlItem>\n <SegmentedControlItem value=\"month\">月</SegmentedControlItem>\n </SegmentedControl>\n\n {onCreateEvent && (\n <Button variant=\"primary\" size=\"sm\" startIcon={Plus} onClick={onCreateEvent}>\n 新事件\n </Button>\n )}\n </div>\n\n {/* Weekday header */}\n <div className=\"grid grid-cols-7 border-b border-divider bg-muted\">\n {weekdayNames.map((name, i) => (\n <div\n key={i}\n className=\"px-2 py-1.5 text-caption text-fg-muted font-normal text-center\"\n >\n {name}\n </div>\n ))}\n </div>\n\n {/* Month grid:7 cols, ~5-6 rows。a11y(2026-04-25):WAI-ARIA grid 要求 row > gridcell\n 階層,chunk days 7 一組,wrap 成 role='row'(display:contents 保 CSS grid 佈局)。 */}\n <div\n className=\"grid grid-cols-7 flex-1 min-h-0\"\n role=\"grid\"\n aria-label={`月行事曆,${monthTitle}`}\n >\n {Array.from({ length: Math.ceil(days.length / 7) }, (_, rowIdx) => (\n <div key={rowIdx} role=\"row\" style={{ display: 'contents' }}>\n {days.slice(rowIdx * 7, rowIdx * 7 + 7).map((date) => {\n const inMonth = isSameMonth(date, refDate)\n const isToday = isSameDay(date, today)\n // 2026-06-01 allDay:全天事件排 cell 頂端(對齊 Google Calendar 全天列在上)\n const dayEvents = eventsOnDate(events, date).slice().sort((a, b) => Number(b.allDay ?? false) - Number(a.allDay ?? false))\n const visibleEvents = dayEvents.slice(0, MAX_TILES_PER_CELL)\n const overflowCount = dayEvents.length - visibleEvents.length\n\n return (\n <button\n key={date.toISOString()}\n type=\"button\"\n role=\"gridcell\"\n aria-label={`${format(date, 'yyyy-MM-dd')},${dayEvents.length} 個事件`}\n onClick={() => onDateClick?.(date)}\n className={cn(\n 'flex flex-col gap-1 min-h-28 p-1.5 text-left',\n 'border-r border-b border-divider last:border-r-0',\n '[&:nth-child(7n)]:border-r-0',\n 'hover:bg-neutral-hover transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n !inMonth && 'bg-muted',\n )}\n >\n {/* Date number header */}\n <div className=\"flex items-start justify-end\">\n {isToday ? (\n <span className=\"inline-flex items-center justify-center min-w-6 h-6 px-2 rounded-full bg-primary text-on-emphasis text-body font-medium\">\n {format(date, 'd')}\n </span>\n ) : (\n <span\n className={cn(\n 'text-body font-medium',\n !inMonth && 'text-fg-disabled',\n )}\n >\n {format(date, 'd')}\n </span>\n )}\n </div>\n\n {/* Event tiles */}\n <div className=\"flex flex-col gap-0.5 min-h-0\">\n {visibleEvents.map((event) => {\n const ec = event.color ?? 'blue'\n // 2026-06-01 allDay:淡底 + 左 accent 條 + medium = 「全天長條」視覺(區分有時間事件)\n const colorClass = event.allDay\n ? cn(EVENT_COLOR_CLASSES[ec], EVENT_ALLDAY_ACCENT[ec], 'font-medium')\n : EVENT_COLOR_CLASSES[ec]\n if (renderEventTile) {\n return (\n <div\n key={event.id}\n onClick={(e) => {\n e.stopPropagation()\n onEventClick?.(event)\n }}\n >\n {renderEventTile(event)}\n </div>\n )\n }\n return (\n <div\n key={event.id}\n role=\"button\"\n tabIndex={0}\n onClick={(e) => {\n e.stopPropagation()\n onEventClick?.(event)\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onEventClick?.(event)\n }\n }}\n aria-label={`事件:${event.title}`}\n className={cn(\n 'rounded-md px-1.5 py-0.5 text-caption truncate cursor-pointer transition-colors',\n // 2026-05-31 #22:事件 tile 是 focusable(tabIndex=0 role=button)但原無 focus ring\n // → WCAG 2.4.7 不合規。補 focus-visible ring 對齊日期格按鈕。\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n colorClass,\n )}\n >\n {event.title}\n </div>\n )\n })}\n {overflowCount > 0 && (\n <div className=\"text-caption text-fg-muted px-1.5\">\n +{overflowCount} more\n </div>\n )}\n </div>\n </button>\n )\n })}\n </div>\n ))}\n </div>\n </div>\n )\n})\nCalendar.displayName = \"Calendar\"\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const calendarMeta = {\n component: 'Calendar',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-muted', 'bg-neutral-hover', 'bg-primary', 'bg-surface'],\n fg: ['text-fg-disabled', 'text-fg-muted', 'text-foreground'],\n ring: ['ring-ring'],\n },\n} as const\n\nexport { Calendar }\n"],"names":["Calendar"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"calendar.js","sources":["../../../src/components/Calendar/calendar.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport { ChevronLeft, ChevronRight, Plus } from 'lucide-react'\nimport {\n startOfMonth,\n endOfMonth,\n startOfWeek,\n endOfWeek,\n eachDayOfInterval,\n format,\n isSameMonth,\n isSameDay,\n addMonths,\n subMonths,\n} from 'date-fns'\nimport { cn } from '@/lib/utils'\nimport { CAT_EVENT, CAT_ACCENT, type CategoricalHue } from '@/design-system/tokens/categorical-color'\nimport { Button } from '@/design-system/components/Button/button'\nimport { SegmentedControl, SegmentedControlItem } from '@/design-system/components/SegmentedControl/segmented-control'\n\n/**\n * Calendar — 事件檢視 canvas(月 view MVP)\n *\n * 定位:看事件的 page-level canvas,對齊 Notion Calendar / Google Calendar。\n * 完整 spec 見 `event-calendar.spec.md`。\n *\n * ── Layout Family ──\n * 非 4-Family,屬 page-composite(多區塊 Toolbar + Grid + EventTile)。\n *\n * ── MVP scope(本次 session)──\n * - 月 view 完整(toolbar / grid / event tile / today highlight / outside days)\n * - 週 / 日 view 是 tech debt\n * - 拖拉增刪 event 是 tech debt\n *\n * ── 與 DatePicker 的區分 ──\n * DatePicker 是「選日期」form control;Calendar 是「看事件」page canvas。\n * 名字相近但職責完全不同,spec 頂段明示分界。\n */\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport interface CalendarEvent {\n id: string\n title: string\n /** ISO 字串 \"YYYY-MM-DD\"(all-day)或 \"YYYY-MM-DDTHH:mm\"(timed) */\n start: string | Date\n end: string | Date\n allDay?: boolean\n /**\n * 事件類別色(categorical 色相,1:1 對 `--color-{hue}-*`)。**消費 categorical-color SSOT**,\n * 與 Tag / Avatar 共用同一組 12 色相。2026-06-04 修:原 `orange` 與 `red` 都誤接 deep-orange;\n * 改消費 SSOT 後 orange→`--color-orange-*`、red→`--color-red-*`(品牌紅 hue 25),各自獨立。\n */\n color?: CategoricalHue\n metadata?: Record<string, unknown>\n}\n\nexport type CalendarView = 'month' | 'week' | 'day'\n\nexport interface CalendarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {\n /** 當前 view(MVP 只 'month',其餘 view tech debt) */\n view?: CalendarView\n defaultView?: CalendarView\n onViewChange?: (view: CalendarView) => void\n\n /** 聚焦日期(月 view 的那個月) */\n referenceDate?: Date\n defaultReferenceDate?: Date\n onReferenceDateChange?: (date: Date) => void\n\n /** 事件資料 */\n events?: CalendarEvent[]\n\n /** 點 event tile 回調 */\n onEventClick?: (event: CalendarEvent) => void\n /** 點月 cell 回調(用於新增) */\n onDateClick?: (date: Date) => void\n /** 點新事件 CTA 回調 */\n onCreateEvent?: () => void\n\n /** 0 = Sunday, 1 = Monday。預設 0(對齊 Google Calendar 美系預設) */\n weekStartsOn?: 0 | 1\n\n /** 自訂 event tile 渲染 */\n renderEventTile?: (event: CalendarEvent) => React.ReactNode\n\n /** size(MVP 只 md;lg 為 tech debt) */\n size?: 'md' | 'lg'\n className?: string\n\n /** locale(預設 'en-US') */\n locale?: string\n\n /** ARIA labels for chrome controls. Override for i18n. */\n prevAriaLabel?: string\n nextAriaLabel?: string\n viewToggleAriaLabel?: string\n todayLabel?: string\n}\n\n// ── Event tile color tokens ─────────────────────────────────────────────────\n// **消費 categorical-color SSOT**(CAT_EVENT = subtle 底 + hover step-2;CAT_ACCENT = 左側 step-6\n// 實心條),與 Tag / Avatar 共用 12 色相,key X 一律對 `--color-X-*`(1:1)。\n// 2026-06-01 allDay:全天事件 = 淡底 tile + 左側實心 accent 條 + medium,視覺區分「全天長條」vs\n// 有時間事件;用 accent border 而非 solid fill 保文字對比安全。對齊 Google Calendar / Outlook 慣例。\nconst EVENT_COLOR_CLASSES = CAT_EVENT\nconst EVENT_ALLDAY_ACCENT = CAT_ACCENT\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction coerceDate(value: string | Date): Date {\n return value instanceof Date ? value : new Date(value)\n}\n\nfunction eventsOnDate(events: CalendarEvent[], date: Date): CalendarEvent[] {\n return events.filter((e) => {\n const start = coerceDate(e.start)\n const end = coerceDate(e.end)\n // 日期落在 [start, end] 範圍內(日精度)\n const d = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()\n const s = new Date(start.getFullYear(), start.getMonth(), start.getDate()).getTime()\n const eEnd = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime()\n return d >= s && d <= eEnd\n })\n}\n\n// ── Component ──────────────────────────────────────────────────────────────\n\nconst MAX_TILES_PER_CELL = 3\n\nconst Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(function Calendar({\n view: viewProp,\n defaultView = 'month',\n onViewChange,\n referenceDate: referenceDateProp,\n defaultReferenceDate,\n onReferenceDateChange,\n events = [],\n onEventClick,\n onDateClick,\n onCreateEvent,\n weekStartsOn = 0,\n renderEventTile,\n size = 'md',\n className,\n locale = 'en-US',\n prevAriaLabel = '上個月', // i18n-allow: DS default; consumer override via prevAriaLabel prop\n nextAriaLabel = '下個月', // i18n-allow: DS default; consumer override via nextAriaLabel prop\n viewToggleAriaLabel = '檢視切換', // i18n-allow: DS default; consumer override via viewToggleAriaLabel prop\n todayLabel = '今天', // i18n-allow: DS default; consumer override via todayLabel prop\n ...props\n}, ref) {\n // Controlled / uncontrolled refDate\n const [internalRef, setInternalRef] = React.useState<Date>(\n defaultReferenceDate ?? new Date(),\n )\n const refDate = referenceDateProp ?? internalRef\n const setRefDate = React.useCallback(\n (next: Date) => {\n if (referenceDateProp === undefined) setInternalRef(next)\n onReferenceDateChange?.(next)\n },\n [referenceDateProp, onReferenceDateChange],\n )\n\n // View state(MVP 只用 month,其他 tech debt)\n const [internalView, setInternalView] = React.useState<CalendarView>(defaultView)\n const currentView = viewProp ?? internalView\n const setView = React.useCallback(\n (next: CalendarView) => {\n if (viewProp === undefined) setInternalView(next)\n onViewChange?.(next)\n },\n [viewProp, onViewChange],\n )\n\n // Build month grid\n const days = React.useMemo(() => {\n const monthStart = startOfMonth(refDate)\n const monthEnd = endOfMonth(refDate)\n const gridStart = startOfWeek(monthStart, { weekStartsOn })\n const gridEnd = endOfWeek(monthEnd, { weekStartsOn })\n return eachDayOfInterval({ start: gridStart, end: gridEnd })\n }, [refDate, weekStartsOn])\n\n const monthTitle = new Intl.DateTimeFormat(locale, {\n year: 'numeric',\n month: 'long',\n }).format(refDate)\n\n const today = new Date()\n\n const weekdayNames = React.useMemo(() => {\n // 取 `days[0..6]` 的名字(gridStart 開始 7 天,正好一週)\n return days.slice(0, 7).map((d) =>\n new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(d),\n )\n }, [days, locale])\n\n const handleToday = () => setRefDate(new Date())\n const handlePrev = () => setRefDate(subMonths(refDate, 1))\n const handleNext = () => setRefDate(addMonths(refDate, 1))\n\n return (\n <div\n ref={ref}\n className={cn(\n 'flex flex-col w-full h-full bg-surface rounded-md border border-divider overflow-hidden',\n className,\n )}\n data-view={currentView}\n data-size={size}\n {...props}\n >\n {/* Toolbar:[◀] [今天] [▶] title [view tabs] [+ new] */}\n <div\n className={cn(\n 'flex items-center gap-2 shrink-0 border-b border-divider',\n 'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',\n )}\n >\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"text\"\n size=\"sm\"\n iconOnly\n startIcon={ChevronLeft}\n aria-label={prevAriaLabel}\n onClick={handlePrev}\n />\n <Button variant=\"tertiary\" size=\"sm\" onClick={handleToday}>\n {todayLabel}\n </Button>\n <Button\n variant=\"text\"\n size=\"sm\"\n iconOnly\n startIcon={ChevronRight}\n aria-label={nextAriaLabel}\n onClick={handleNext}\n />\n </div>\n\n <h2 className=\"text-body-lg font-medium text-foreground flex-1 min-w-0 truncate ml-2\">\n {monthTitle}\n </h2>\n\n {/* View switcher:用 SegmentedControl(互斥多選一 canonical)——\n 對齊 CLAUDE.md「互斥分類選擇走 SegmentedControl,非 checked Button group」原則。\n Button 的 pressed 是「toggle 持續狀態」語意,不適合「單選 view 切換」 */}\n <SegmentedControl\n size=\"sm\"\n value={currentView}\n onValueChange={(v) => setView(v as CalendarView)}\n aria-label={viewToggleAriaLabel}\n >\n <SegmentedControlItem value=\"day\" disabled>日</SegmentedControlItem>\n <SegmentedControlItem value=\"week\" disabled>週</SegmentedControlItem>\n <SegmentedControlItem value=\"month\">月</SegmentedControlItem>\n </SegmentedControl>\n\n {onCreateEvent && (\n <Button variant=\"primary\" size=\"sm\" startIcon={Plus} onClick={onCreateEvent}>\n 新事件\n </Button>\n )}\n </div>\n\n {/* Weekday header */}\n <div className=\"grid grid-cols-7 border-b border-divider bg-muted\">\n {weekdayNames.map((name, i) => (\n <div\n key={i}\n className=\"px-2 py-1.5 text-caption text-fg-muted font-normal text-center\"\n >\n {name}\n </div>\n ))}\n </div>\n\n {/* Month grid:7 cols, ~5-6 rows。a11y(2026-04-25):WAI-ARIA grid 要求 row > gridcell\n 階層,chunk days 7 一組,wrap 成 role='row'(display:contents 保 CSS grid 佈局)。 */}\n <div\n className=\"grid grid-cols-7 flex-1 min-h-0\"\n role=\"grid\"\n aria-label={`月行事曆,${monthTitle}`}\n >\n {Array.from({ length: Math.ceil(days.length / 7) }, (_, rowIdx) => (\n <div key={rowIdx} role=\"row\" style={{ display: 'contents' }}>\n {days.slice(rowIdx * 7, rowIdx * 7 + 7).map((date) => {\n const inMonth = isSameMonth(date, refDate)\n const isToday = isSameDay(date, today)\n // 2026-06-01 allDay:全天事件排 cell 頂端(對齊 Google Calendar 全天列在上)\n const dayEvents = eventsOnDate(events, date).slice().sort((a, b) => Number(b.allDay ?? false) - Number(a.allDay ?? false))\n const visibleEvents = dayEvents.slice(0, MAX_TILES_PER_CELL)\n const overflowCount = dayEvents.length - visibleEvents.length\n\n return (\n <button\n key={date.toISOString()}\n type=\"button\"\n role=\"gridcell\"\n aria-label={`${format(date, 'yyyy-MM-dd')},${dayEvents.length} 個事件`}\n onClick={() => onDateClick?.(date)}\n className={cn(\n 'flex flex-col gap-1 min-h-28 p-1.5 text-left',\n 'border-r border-b border-divider last:border-r-0',\n '[&:nth-child(7n)]:border-r-0',\n 'hover:bg-neutral-hover transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n !inMonth && 'bg-muted',\n )}\n >\n {/* Date number header */}\n <div className=\"flex items-start justify-end\">\n {isToday ? (\n <span className=\"inline-flex items-center justify-center min-w-6 h-6 px-2 rounded-full bg-primary text-on-emphasis text-body font-medium\">\n {format(date, 'd')}\n </span>\n ) : (\n <span\n className={cn(\n 'text-body font-medium',\n !inMonth && 'text-fg-disabled',\n )}\n >\n {format(date, 'd')}\n </span>\n )}\n </div>\n\n {/* Event tiles */}\n <div className=\"flex flex-col gap-0.5 min-h-0\">\n {visibleEvents.map((event) => {\n const ec = event.color ?? 'blue'\n // 2026-06-01 allDay:淡底 + 左 accent 條 + medium = 「全天長條」視覺(區分有時間事件)\n const colorClass = event.allDay\n ? cn(EVENT_COLOR_CLASSES[ec], EVENT_ALLDAY_ACCENT[ec], 'font-medium')\n : EVENT_COLOR_CLASSES[ec]\n if (renderEventTile) {\n return (\n <div\n key={event.id}\n onClick={(e) => {\n e.stopPropagation()\n onEventClick?.(event)\n }}\n >\n {renderEventTile(event)}\n </div>\n )\n }\n return (\n <div\n key={event.id}\n role=\"button\"\n tabIndex={0}\n onClick={(e) => {\n e.stopPropagation()\n onEventClick?.(event)\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onEventClick?.(event)\n }\n }}\n aria-label={`事件:${event.title}`}\n className={cn(\n 'rounded-md px-1.5 py-0.5 text-caption truncate cursor-pointer transition-colors',\n // 2026-05-31 #22:事件 tile 是 focusable(tabIndex=0 role=button)但原無 focus ring\n // → WCAG 2.4.7 不合規。補 focus-visible ring 對齊日期格按鈕。\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n colorClass,\n )}\n >\n {event.title}\n </div>\n )\n })}\n {overflowCount > 0 && (\n <div className=\"text-caption text-fg-muted px-1.5\">\n +{overflowCount} more\n </div>\n )}\n </div>\n </button>\n )\n })}\n </div>\n ))}\n </div>\n </div>\n )\n})\nCalendar.displayName = \"Calendar\"\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const calendarMeta = {\n component: 'Calendar',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-muted', 'bg-neutral-hover', 'bg-primary', 'bg-surface'],\n fg: ['text-fg-disabled', 'text-fg-muted', 'text-foreground'],\n ring: ['ring-ring'],\n },\n} as const\n\nexport { Calendar }\n"],"names":["Calendar"],"mappings":";;;;;;;;AAyGA,MAAM,sBAAsB;AAC5B,MAAM,sBAAsB;AAI5B,SAAS,WAAW,OAA4B;AAC9C,SAAO,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;AACvD;AAEA,SAAS,aAAa,QAAyB,MAA6B;AAC1E,SAAO,OAAO,OAAO,CAAC,MAAM;AAC1B,UAAM,QAAQ,WAAW,EAAE,KAAK;AAChC,UAAM,MAAM,WAAW,EAAE,GAAG;AAE5B,UAAM,IAAI,IAAI,KAAK,KAAK,YAAA,GAAe,KAAK,SAAA,GAAY,KAAK,QAAA,CAAS,EAAE,QAAA;AACxE,UAAM,IAAI,IAAI,KAAK,MAAM,YAAA,GAAe,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,EAAE,QAAA;AAC3E,UAAM,OAAO,IAAI,KAAK,IAAI,YAAA,GAAe,IAAI,SAAA,GAAY,IAAI,QAAA,CAAS,EAAE,QAAA;AACxE,WAAO,KAAK,KAAK,KAAK;AAAA,EACxB,CAAC;AACH;AAIA,MAAM,qBAAqB;AAE3B,MAAM,WAAW,MAAM,WAA0C,SAASA,UAAS;AAAA,EACjF,MAAM;AAAA,EACN,cAAc;AAAA,EACd;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA,SAAS,CAAA;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,SAAS;AAAA,EACT,gBAAgB;AAAA;AAAA,EAChB,gBAAgB;AAAA;AAAA,EAChB,sBAAsB;AAAA;AAAA,EACtB,aAAa;AAAA;AAAA,EACb,GAAG;AACL,GAAG,KAAK;AAEN,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM;AAAA,IAC1C,4CAA4B,KAAA;AAAA,EAAK;AAEnC,QAAM,UAAU,qBAAqB;AACrC,QAAM,aAAa,MAAM;AAAA,IACvB,CAAC,SAAe;AACd,UAAI,sBAAsB,OAAW,gBAAe,IAAI;AACxD,qEAAwB;AAAA,IAC1B;AAAA,IACA,CAAC,mBAAmB,qBAAqB;AAAA,EAAA;AAI3C,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAuB,WAAW;AAChF,QAAM,cAAc,YAAY;AAChC,QAAM,UAAU,MAAM;AAAA,IACpB,CAAC,SAAuB;AACtB,UAAI,aAAa,OAAW,iBAAgB,IAAI;AAChD,mDAAe;AAAA,IACjB;AAAA,IACA,CAAC,UAAU,YAAY;AAAA,EAAA;AAIzB,QAAM,OAAO,MAAM,QAAQ,MAAM;AAC/B,UAAM,aAAa,aAAa,OAAO;AACvC,UAAM,WAAW,WAAW,OAAO;AACnC,UAAM,YAAY,YAAY,YAAY,EAAE,cAAc;AAC1D,UAAM,UAAU,UAAU,UAAU,EAAE,cAAc;AACpD,WAAO,kBAAkB,EAAE,OAAO,WAAW,KAAK,SAAS;AAAA,EAC7D,GAAG,CAAC,SAAS,YAAY,CAAC;AAE1B,QAAM,aAAa,IAAI,KAAK,eAAe,QAAQ;AAAA,IACjD,MAAM;AAAA,IACN,OAAO;AAAA,EAAA,CACR,EAAE,OAAO,OAAO;AAEjB,QAAM,4BAAY,KAAA;AAElB,QAAM,eAAe,MAAM,QAAQ,MAAM;AAEvC,WAAO,KAAK,MAAM,GAAG,CAAC,EAAE;AAAA,MAAI,CAAC,MAC3B,IAAI,KAAK,eAAe,QAAQ,EAAE,SAAS,QAAA,CAAS,EAAE,OAAO,CAAC;AAAA,IAAA;AAAA,EAElE,GAAG,CAAC,MAAM,MAAM,CAAC;AAEjB,QAAM,cAAc,MAAM,WAAW,oBAAI,MAAM;AAC/C,QAAM,aAAa,MAAM,WAAW,UAAU,SAAS,CAAC,CAAC;AACzD,QAAM,aAAa,MAAM,WAAW,UAAU,SAAS,CAAC,CAAC;AAEzD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAEF,aAAW;AAAA,MACX,aAAW;AAAA,MACV,GAAG;AAAA,MAGJ,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,YAAA;AAAA,YAGF,UAAA;AAAA,cAAA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,gBAAA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,SAAQ;AAAA,oBACR,MAAK;AAAA,oBACL,UAAQ;AAAA,oBACR,WAAW;AAAA,oBACX,cAAY;AAAA,oBACZ,SAAS;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAEX,oBAAC,UAAO,SAAQ,YAAW,MAAK,MAAK,SAAS,aAC3C,UAAA,WAAA,CACH;AAAA,gBACA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,SAAQ;AAAA,oBACR,MAAK;AAAA,oBACL,UAAQ;AAAA,oBACR,WAAW;AAAA,oBACX,cAAY;AAAA,oBACZ,SAAS;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACX,GACF;AAAA,cAEA,oBAAC,MAAA,EAAG,WAAU,yEACX,UAAA,YACH;AAAA,cAKA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBACL,OAAO;AAAA,kBACP,eAAe,CAAC,MAAM,QAAQ,CAAiB;AAAA,kBAC/C,cAAY;AAAA,kBAEZ,UAAA;AAAA,oBAAA,oBAAC,sBAAA,EAAqB,OAAM,OAAM,UAAQ,MAAC,UAAA,KAAC;AAAA,wCAC3C,sBAAA,EAAqB,OAAM,QAAO,UAAQ,MAAC,UAAA,KAAC;AAAA,oBAC7C,oBAAC,sBAAA,EAAqB,OAAM,SAAQ,UAAA,IAAA,CAAC;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAAA;AAAA,cAGtC,iBACC,oBAAC,QAAA,EAAO,SAAQ,WAAU,MAAK,MAAK,WAAW,MAAM,SAAS,eAAe,UAAA,MAAA,CAE7E;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAKJ,oBAAC,SAAI,WAAU,qDACZ,uBAAa,IAAI,CAAC,MAAM,MACvB;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,WAAU;AAAA,YAET,UAAA;AAAA,UAAA;AAAA,UAHI;AAAA,QAAA,CAKR,GACH;AAAA,QAIA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAU;AAAA,YACV,MAAK;AAAA,YACL,cAAY,QAAQ,UAAU;AAAA,YAE7B,UAAA,MAAM,KAAK,EAAE,QAAQ,KAAK,KAAK,KAAK,SAAS,CAAC,EAAA,GAAK,CAAC,GAAG,WACtD,oBAAC,OAAA,EAAiB,MAAK,OAAM,OAAO,EAAE,SAAS,WAAA,GAC5C,UAAA,KAAK,MAAM,SAAS,GAAG,SAAS,IAAI,CAAC,EAAE,IAAI,CAAC,SAAS;AACpD,oBAAM,UAAU,YAAY,MAAM,OAAO;AACzC,oBAAM,UAAU,UAAU,MAAM,KAAK;AAErC,oBAAM,YAAY,aAAa,QAAQ,IAAI,EAAE,MAAA,EAAQ,KAAK,CAAC,GAAG,MAAM,OAAO,EAAE,UAAU,KAAK,IAAI,OAAO,EAAE,UAAU,KAAK,CAAC;AACzH,oBAAM,gBAAgB,UAAU,MAAM,GAAG,kBAAkB;AAC3D,oBAAM,gBAAgB,UAAU,SAAS,cAAc;AAEvD,qBACE;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBAEC,MAAK;AAAA,kBACL,MAAK;AAAA,kBACL,cAAY,GAAG,OAAO,MAAM,YAAY,CAAC,IAAI,UAAU,MAAM;AAAA,kBACjE,SAAS,MAAM,2CAAc;AAAA,kBAC7B,WAAW;AAAA,oBACT;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA,CAAC,WAAW;AAAA,kBAAA;AAAA,kBAId,UAAA;AAAA,oBAAA,oBAAC,OAAA,EAAI,WAAU,gCACZ,UAAA,UACC,oBAAC,QAAA,EAAK,WAAU,2HACb,UAAA,OAAO,MAAM,GAAG,EAAA,CACnB,IAEA;AAAA,sBAAC;AAAA,sBAAA;AAAA,wBACC,WAAW;AAAA,0BACT;AAAA,0BACA,CAAC,WAAW;AAAA,wBAAA;AAAA,wBAGb,UAAA,OAAO,MAAM,GAAG;AAAA,sBAAA;AAAA,oBAAA,GAGvB;AAAA,oBAGA,qBAAC,OAAA,EAAI,WAAU,iCACZ,UAAA;AAAA,sBAAA,cAAc,IAAI,CAAC,UAAU;AAC5B,8BAAM,KAAK,MAAM,SAAS;AAE1B,8BAAM,aAAa,MAAM,SACrB,GAAG,oBAAoB,EAAE,GAAG,oBAAoB,EAAE,GAAG,aAAa,IAClE,oBAAoB,EAAE;AAC1B,4BAAI,iBAAiB;AACnB,iCACE;AAAA,4BAAC;AAAA,4BAAA;AAAA,8BAEC,SAAS,CAAC,MAAM;AACd,kCAAE,gBAAA;AACF,6EAAe;AAAA,8BACjB;AAAA,8BAEC,0BAAgB,KAAK;AAAA,4BAAA;AAAA,4BANjB,MAAM;AAAA,0BAAA;AAAA,wBASjB;AACA,+BACE;AAAA,0BAAC;AAAA,0BAAA;AAAA,4BAEC,MAAK;AAAA,4BACL,UAAU;AAAA,4BACV,SAAS,CAAC,MAAM;AACd,gCAAE,gBAAA;AACF,2EAAe;AAAA,4BACjB;AAAA,4BACA,WAAW,CAAC,MAAM;AAChB,kCAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,KAAK;AACtC,kCAAE,eAAA;AACF,6EAAe;AAAA,8BACjB;AAAA,4BACF;AAAA,4BACA,cAAY,MAAM,MAAM,KAAK;AAAA,4BAC7B,WAAW;AAAA,8BACT;AAAA;AAAA;AAAA,8BAGA;AAAA,8BACA;AAAA,4BAAA;AAAA,4BAGD,UAAA,MAAM;AAAA,0BAAA;AAAA,0BAtBF,MAAM;AAAA,wBAAA;AAAA,sBAyBjB,CAAC;AAAA,sBACA,gBAAgB,KACf,qBAAC,OAAA,EAAI,WAAU,qCAAoC,UAAA;AAAA,wBAAA;AAAA,wBAC/C;AAAA,wBAAc;AAAA,sBAAA,EAAA,CAClB;AAAA,oBAAA,EAAA,CAEJ;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAtFS,KAAK,YAAA;AAAA,cAAY;AAAA,YAyF5B,CAAC,EAAA,GApGO,MAqGV,CACD;AAAA,UAAA;AAAA,QAAA;AAAA,MACH;AAAA,IAAA;AAAA,EAAA;AAGN,CAAC;AACD,SAAS,cAAc;AAIhB,MAAM,eAAe;AAAA,EAC1B,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,YAAY,oBAAoB,cAAc,YAAY;AAAA,IAC/D,IAAI,CAAC,oBAAoB,iBAAiB,iBAAiB;AAAA,IAC3D,MAAM,CAAC,WAAW;AAAA,EAAA;AAEtB;"}
|
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
import type { LucideIcon } from "lucide-react";
|
|
4
4
|
declare const tagVariants: (props?: ({
|
|
5
|
-
color?: "blue" | "green" | "indigo" | "magenta" | "purple" | "red" | "turquoise" | "yellow" | "neutral" | null | undefined;
|
|
5
|
+
color?: "blue" | "green" | "indigo" | "lime" | "magenta" | "orange" | "purple" | "red" | "turquoise" | "yellow" | "neutral" | "deep-orange" | "amber" | null | undefined;
|
|
6
6
|
size?: "sm" | "md" | "lg" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
8
|
export interface TagProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'prefix' | 'color'>, VariantProps<typeof tagVariants> {
|
|
@@ -29,31 +29,43 @@ export declare const tagMeta: {
|
|
|
29
29
|
readonly family: 3;
|
|
30
30
|
readonly variants: {
|
|
31
31
|
readonly neutral: {
|
|
32
|
-
readonly purpose: "通用分類、草稿、無特定語義";
|
|
32
|
+
readonly purpose: "通用分類、草稿、無特定語義(secondary 底)";
|
|
33
33
|
};
|
|
34
34
|
readonly blue: {
|
|
35
|
-
readonly purpose: "
|
|
36
|
-
};
|
|
37
|
-
readonly red: {
|
|
38
|
-
readonly purpose: "錯誤、已封鎖、危險(對應 --error)";
|
|
35
|
+
readonly purpose: "categorical 色相(--color-blue-*)";
|
|
39
36
|
};
|
|
40
37
|
readonly green: {
|
|
41
|
-
readonly purpose: "
|
|
38
|
+
readonly purpose: "categorical 色相(--color-green-*)";
|
|
39
|
+
};
|
|
40
|
+
readonly 'deep-orange': {
|
|
41
|
+
readonly purpose: "categorical 色相(--color-deep-orange-*,hue 38)";
|
|
42
42
|
};
|
|
43
43
|
readonly yellow: {
|
|
44
|
-
readonly purpose: "
|
|
44
|
+
readonly purpose: "categorical 色相(--color-yellow-*,淺底深字)";
|
|
45
|
+
};
|
|
46
|
+
readonly red: {
|
|
47
|
+
readonly purpose: "categorical 色相(--color-red-*,品牌紅家族 hue 25;≠ 語意 --error)";
|
|
48
|
+
};
|
|
49
|
+
readonly orange: {
|
|
50
|
+
readonly purpose: "categorical 色相(--color-orange-*)";
|
|
51
|
+
};
|
|
52
|
+
readonly amber: {
|
|
53
|
+
readonly purpose: "categorical 色相(--color-amber-*,淺底深字)";
|
|
54
|
+
};
|
|
55
|
+
readonly lime: {
|
|
56
|
+
readonly purpose: "categorical 色相(--color-lime-*)";
|
|
45
57
|
};
|
|
46
58
|
readonly turquoise: {
|
|
47
|
-
readonly purpose: "
|
|
59
|
+
readonly purpose: "categorical 色相(--color-turquoise-*)";
|
|
60
|
+
};
|
|
61
|
+
readonly indigo: {
|
|
62
|
+
readonly purpose: "categorical 色相(--color-indigo-*)";
|
|
48
63
|
};
|
|
49
64
|
readonly purple: {
|
|
50
|
-
readonly purpose: "
|
|
65
|
+
readonly purpose: "categorical 色相(--color-purple-*)";
|
|
51
66
|
};
|
|
52
67
|
readonly magenta: {
|
|
53
|
-
readonly purpose: "
|
|
54
|
-
};
|
|
55
|
-
readonly indigo: {
|
|
56
|
-
readonly purpose: "分類色(無固定語義)";
|
|
68
|
+
readonly purpose: "categorical 色相(--color-magenta-*)";
|
|
57
69
|
};
|
|
58
70
|
};
|
|
59
71
|
readonly sizes: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/components/Tag/tag.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/components/Tag/tag.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAwB9C,QAAA,MAAM,WAAW;;;8EAuBhB,CAAA;AAWD,MAAM,WAAW,QACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,EACpE,YAAY,CAAC,OAAO,WAAW,CAAC;IAClC,qDAAqD;IACrD,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,sCAAsC;IACtC,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAkHD,QAAA,MAAM,GAAG,iFAAuD,CAAA;AAKhE,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCV,CAAA;AAEV,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -5,6 +5,7 @@ import { X } from "lucide-react";
|
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
6
|
import { Tooltip, TooltipTrigger, TooltipContent } from "../Tooltip/tooltip.js";
|
|
7
7
|
import { ItemInlineActionButton } from "../../patterns/element-anatomy/item-anatomy.js";
|
|
8
|
+
import { CAT_SUBTLE, CAT_SOLID, CAT_INTERACT } from "../../tokens/categorical-color.js";
|
|
8
9
|
let _measureCtx = null;
|
|
9
10
|
function getMeasureCtx() {
|
|
10
11
|
if (!_measureCtx) _measureCtx = document.createElement("canvas").getContext("2d");
|
|
@@ -14,18 +15,13 @@ const tagVariants = cva(
|
|
|
14
15
|
"inline-flex items-center rounded-md border border-transparent transition-colors cursor-text",
|
|
15
16
|
{
|
|
16
17
|
variants: {
|
|
18
|
+
// color:categorical 色相(裝飾性分類,非語意狀態)。**消費 categorical-color SSOT**——
|
|
19
|
+
// key X 一律對 `--color-X-*`(1:1,零 offset)。neutral 非色相(無 hue),用 secondary 底自處理。
|
|
20
|
+
// 2026-06-04 修:原 `red` 誤接 `--color-deep-orange-*`(red=品牌紅 hue-25 ≠ deep-orange hue-38
|
|
21
|
+
// ≠ 語意 --error〔= deep-orange〕);改消費 SSOT 後 red→`--color-red-*`,並補齊全 12 色相。
|
|
17
22
|
color: {
|
|
18
|
-
// 直接引用 primitive(bg=step-1, text=step-7),不經過語義層
|
|
19
|
-
// step-1 在 dark mode 用 alpha 公式,step-7 用 lighter 公式——兩個 mode 都正確
|
|
20
23
|
neutral: "bg-secondary text-foreground",
|
|
21
|
-
|
|
22
|
-
red: "bg-[var(--color-deep-orange-1)] text-[var(--color-deep-orange-7)]",
|
|
23
|
-
green: "bg-[var(--color-green-1)] text-[var(--color-green-7)]",
|
|
24
|
-
yellow: "bg-[var(--color-yellow-1)] text-[var(--color-yellow-7)]",
|
|
25
|
-
turquoise: "bg-[var(--color-turquoise-1)] text-[var(--color-turquoise-7)]",
|
|
26
|
-
purple: "bg-[var(--color-purple-1)] text-[var(--color-purple-7)]",
|
|
27
|
-
magenta: "bg-[var(--color-magenta-1)] text-[var(--color-magenta-7)]",
|
|
28
|
-
indigo: "bg-[var(--color-indigo-1)] text-[var(--color-indigo-7)]"
|
|
24
|
+
...CAT_SUBTLE
|
|
29
25
|
},
|
|
30
26
|
size: {
|
|
31
27
|
sm: "h-5 px-1 text-caption font-medium",
|
|
@@ -41,25 +37,11 @@ const tagVariants = cva(
|
|
|
41
37
|
);
|
|
42
38
|
const SOLID_CLASSES = {
|
|
43
39
|
neutral: "bg-[var(--color-neutral-9)] text-inverse-fg",
|
|
44
|
-
|
|
45
|
-
red: "bg-[var(--color-deep-orange-6)] text-on-emphasis",
|
|
46
|
-
green: "bg-[var(--color-green-6)] text-on-emphasis",
|
|
47
|
-
yellow: "bg-[var(--color-yellow-6)] text-[var(--warning-foreground)]",
|
|
48
|
-
turquoise: "bg-[var(--color-turquoise-6)] text-on-emphasis",
|
|
49
|
-
purple: "bg-[var(--color-purple-6)] text-on-emphasis",
|
|
50
|
-
magenta: "bg-[var(--color-magenta-6)] text-on-emphasis",
|
|
51
|
-
indigo: "bg-[var(--color-indigo-6)] text-on-emphasis"
|
|
40
|
+
...CAT_SOLID
|
|
52
41
|
};
|
|
53
42
|
const SOLID_DISMISS_HOVER = {
|
|
54
43
|
neutral: { hover: "var(--inverse-neutral-hover)", active: "var(--inverse-neutral-active)" },
|
|
55
|
-
|
|
56
|
-
red: { hover: "var(--red-hover)", active: "var(--red-active)" },
|
|
57
|
-
green: { hover: "var(--green-hover)", active: "var(--green-active)" },
|
|
58
|
-
yellow: { hover: "var(--yellow-hover)", active: "var(--yellow-active)" },
|
|
59
|
-
turquoise: { hover: "var(--turquoise-hover)", active: "var(--turquoise-active)" },
|
|
60
|
-
purple: { hover: "var(--purple-hover)", active: "var(--purple-active)" },
|
|
61
|
-
magenta: { hover: "var(--magenta-hover)", active: "var(--magenta-active)" },
|
|
62
|
-
indigo: { hover: "var(--indigo-hover)", active: "var(--indigo-active)" }
|
|
44
|
+
...CAT_INTERACT
|
|
63
45
|
};
|
|
64
46
|
function TagDismiss({ onDismiss, label, solid, color }) {
|
|
65
47
|
const solidColors = solid && color ? SOLID_DISMISS_HOVER[color] : void 0;
|
|
@@ -139,16 +121,24 @@ Tag.displayName = "Tag";
|
|
|
139
121
|
const tagMeta = {
|
|
140
122
|
component: "Tag",
|
|
141
123
|
family: 3,
|
|
124
|
+
// categorical 色相(裝飾性分類,非語意狀態)。**1:1 對 `--color-{hue}-*` primitive,零 offset**。
|
|
125
|
+
// 不對應語意 token——語意狀態(error/info/success/warning)走 Notice / Alert 等狀態元件,
|
|
126
|
+
// 不是 Tag 色相。2026-06-04 修:移除原「red 對應 --error / blue 對應 --info ...」誤導框架
|
|
127
|
+
// (red = 品牌紅 hue-25,跟語意 --error〔= deep-orange〕無關)。
|
|
142
128
|
variants: {
|
|
143
|
-
neutral: { purpose: "通用分類、草稿、無特定語義" },
|
|
144
|
-
blue: { purpose: "
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
yellow: { purpose: "
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
129
|
+
neutral: { purpose: "通用分類、草稿、無特定語義(secondary 底)" },
|
|
130
|
+
blue: { purpose: "categorical 色相(--color-blue-*)" },
|
|
131
|
+
green: { purpose: "categorical 色相(--color-green-*)" },
|
|
132
|
+
"deep-orange": { purpose: "categorical 色相(--color-deep-orange-*,hue 38)" },
|
|
133
|
+
yellow: { purpose: "categorical 色相(--color-yellow-*,淺底深字)" },
|
|
134
|
+
red: { purpose: "categorical 色相(--color-red-*,品牌紅家族 hue 25;≠ 語意 --error)" },
|
|
135
|
+
orange: { purpose: "categorical 色相(--color-orange-*)" },
|
|
136
|
+
amber: { purpose: "categorical 色相(--color-amber-*,淺底深字)" },
|
|
137
|
+
lime: { purpose: "categorical 色相(--color-lime-*)" },
|
|
138
|
+
turquoise: { purpose: "categorical 色相(--color-turquoise-*)" },
|
|
139
|
+
indigo: { purpose: "categorical 色相(--color-indigo-*)" },
|
|
140
|
+
purple: { purpose: "categorical 色相(--color-purple-*)" },
|
|
141
|
+
magenta: { purpose: "categorical 色相(--color-magenta-*)" }
|
|
152
142
|
},
|
|
153
143
|
sizes: {
|
|
154
144
|
// Tag 尺寸不引用 field-height token(spec.md:180/241——Tag 與 Field 尺寸獨立)。
|