@lablup/ui-common 0.1.0-alpha.1 → 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +9 -2
  2. package/dist/assets/components/BaseCard/BaseCard.css +1 -1
  3. package/dist/assets/components/Button/Button.css +1 -1
  4. package/dist/assets/components/DataTable/DataTable.css +1 -1
  5. package/dist/assets/components/Drawer/Drawer.css +1 -1
  6. package/dist/assets/components/EmptyState/EmptyState.css +1 -1
  7. package/dist/assets/components/ErrorState/ErrorState.css +1 -1
  8. package/dist/assets/components/StatCard/StatCard.css +1 -1
  9. package/dist/assets/components/Tabs/Tabs.css +1 -1
  10. package/dist/assets/components/Tooltip/Tooltip.css +1 -1
  11. package/dist/components/DataTable/DataTable.d.ts +16 -5
  12. package/dist/components/DataTable/DataTable2.js +31 -64
  13. package/dist/components/DataTable/DataTable2.js.map +1 -1
  14. package/dist/components/EmptyState/EmptyState.d.ts +10 -3
  15. package/dist/components/EmptyState/EmptyState2.js +31 -48
  16. package/dist/components/EmptyState/EmptyState2.js.map +1 -1
  17. package/dist/components/EmptyState/index.d.ts +6 -1
  18. package/dist/components/ErrorState/ErrorState.d.ts +17 -4
  19. package/dist/components/ErrorState/ErrorState2.js +20 -23
  20. package/dist/components/ErrorState/ErrorState2.js.map +1 -1
  21. package/dist/components/ErrorState/index.d.ts +1 -1
  22. package/dist/components/Skeleton/SkeletonCard.d.ts +3 -1
  23. package/dist/components/Skeleton/SkeletonCard2.js +12 -3
  24. package/dist/components/Skeleton/SkeletonCard2.js.map +1 -1
  25. package/dist/components/Skeleton/SkeletonRow.d.ts +3 -1
  26. package/dist/components/Skeleton/SkeletonRow2.js +8 -3
  27. package/dist/components/Skeleton/SkeletonRow2.js.map +1 -1
  28. package/dist/components/Skeleton/SkeletonText.d.ts +3 -1
  29. package/dist/components/Skeleton/SkeletonText2.js +6 -5
  30. package/dist/components/Skeleton/SkeletonText2.js.map +1 -1
  31. package/dist/components/Tabs/Tabs.d.ts +10 -23
  32. package/dist/components/Tabs/Tabs2.js +127 -146
  33. package/dist/components/Tabs/Tabs2.js.map +1 -1
  34. package/dist/components/Tabs/index.d.ts +2 -2
  35. package/dist/components/Tabs/index.js +2 -3
  36. package/dist/index.d.ts +5 -3
  37. package/dist/index.js +3 -4
  38. package/dist/styles/base.css +130 -48
  39. package/dist/styles/themes/orange-dark.css +3 -0
  40. package/dist/styles/themes/orange-light.css +6 -1
  41. package/package.json +1 -1
  42. package/dist/components/EmptyState/illustrations.d.ts +0 -51
  43. package/dist/components/EmptyState/illustrations.js +0 -571
  44. package/dist/components/EmptyState/illustrations.js.map +0 -1
  45. package/dist/components/Tabs/tabTags.d.ts +0 -9
  46. package/dist/components/Tabs/tabTags.js +0 -11
  47. package/dist/components/Tabs/tabTags.js.map +0 -1
  48. package/dist/styles/themes/bliss-dark.css +0 -123
  49. package/dist/styles/themes/bliss-light.css +0 -123
  50. package/dist/styles/themes/glass-dark.css +0 -124
  51. package/dist/styles/themes/glass-light.css +0 -125
  52. package/dist/styles/themes/reverie-dark.css +0 -104
  53. package/dist/styles/themes/reverie-light.css +0 -104
  54. package/dist/styles/themes/stained-dark.css +0 -80
  55. package/dist/styles/themes/stained-light.css +0 -80
@@ -1 +1 @@
1
- {"version":3,"file":"Tabs2.js","names":[],"sources":["../../../src/components/Tabs/Tabs.tsx"],"sourcesContent":["/**\n * Tabs Component\n *\n * Unified tab navigation component with optional grouping, guide tag badges,\n * and responsive overflow handling. Follows WCAG 2.1 ARIA tab pattern.\n *\n * Features:\n * - Scroll arrows for overflow\n * - Mobile dropdown mode (overflowMode=\"dropdown\", < 480px)\n * - Overflow menu mode (overflowMode=\"menu\", < 768px)\n * - Tab grouping with separators and labels\n * - Guide tag badge support (required, recommended, beta, experimental)\n * - Touch swipe navigation (dropdown mode)\n * - Auto-scroll to active tab\n * - Full keyboard navigation (Arrow keys, Home, End)\n */\n\nimport {\n useState,\n useCallback,\n useRef,\n useEffect,\n useMemo,\n type ReactNode,\n type KeyboardEvent,\n type TouchEvent,\n} from \"react\";\nimport { Badge } from \"../Badge\";\nimport { type TabTagType, type TabTagLabels, TAG_CONFIG } from \"./tabTags\";\nimport \"./Tabs.css\";\n\n// Re-export for consumers that import from Tabs.tsx directly\nexport type { TabTagType, TabTagLabels } from \"./tabTags\";\nexport { TAG_CONFIG } from \"./tabTags\";\n\n/** English default for every {@link TabTagType} badge label. */\nconst DEFAULT_TAG_LABELS: TabTagLabels = {\n required: \"Required\",\n recommended: \"Recommended\",\n beta: \"Beta\",\n experimental: \"Experimental\",\n};\n\nexport interface TabItem {\n id: string;\n label: string;\n content: ReactNode;\n /**\n * Show a \"Required\" badge next to the tab label.\n * @deprecated Use `tag=\"required\"` instead. Kept for backward compatibility.\n */\n required?: boolean;\n /**\n * Guide tag displayed after the tab label as a small badge.\n * When set, takes precedence over the `required` boolean.\n */\n tag?: TabTagType;\n /**\n * Optional extra trailing node rendered next to the tab label (e.g. a\n * count badge). Distinct from `tag` (which uses a fixed preset). Use this\n * when migrating segmented controls that rendered their own inline count\n * pill alongside the label.\n */\n labelExtra?: ReactNode;\n /**\n * Optional leading node rendered before the tab label (e.g. an icon).\n * Useful when migrating segmented controls that prefix the label with\n * an SVG glyph (e.g. ImportExportDialog source toggle: File / Paste / URL).\n */\n labelPrefix?: ReactNode;\n /** Assign this tab to a group (used with the `groups` prop) */\n groupId?: string;\n}\n\nexport interface TabGroupMeta {\n id: string;\n label: string;\n}\n\nexport type TabOverflowMode = \"dropdown\" | \"menu\";\n\n/**\n * Visual variant of the tab bar.\n *\n * - `\"underlined\"` (default): pill container with raised active tab and scroll\n * arrows; used for primary page-level navigation that may overflow.\n * - `\"segmented\"`: gray-background segmented control with raised active tab;\n * intended for short, fixed-arity in-page mode toggles.\n * - `\"compact\"`: smaller segmented control with reduced padding; used where\n * the tab bar shares a row with other controls (e.g. StatisticsPage).\n */\nexport type TabVariant = \"underlined\" | \"segmented\" | \"compact\";\n\nexport interface TabsProps {\n tabs: TabItem[];\n /** Group definitions for organizing tabs with separators and labels */\n groups?: TabGroupMeta[];\n defaultTab?: string;\n activeTab?: string;\n onTabChange?: (tabId: string) => void;\n className?: string;\n panelClassName?: string;\n ariaLabel?: string;\n /** Show scroll arrows and overflow controls (default: true) */\n showOverflowControls?: boolean;\n /** Show group labels in the tab bar and overflow menu (default: true) */\n showGroupLabels?: boolean;\n /**\n * Overflow strategy for small screens:\n * - \"dropdown\": mobile dropdown trigger (< 480px) — default when no groups\n * - \"menu\": overflow menu button (< 768px) — default when groups are provided\n */\n overflowMode?: TabOverflowMode;\n /**\n * Visual variant. Defaults to `\"underlined\"` (the original Tabs look).\n * Use `\"segmented\"` for iOS-style pill toggles, `\"compact\"` for a smaller\n * variant suitable for in-header use alongside other controls.\n */\n variant?: TabVariant;\n /**\n * When using `variant=\"segmented\"` or `\"compact\"`, make each tab fill the\n * container evenly (flex: 1 per tab). Useful for short, equal-arity mode\n * toggles like the Models page tab bar. Defaults to false.\n */\n fillContainer?: boolean;\n /**\n * Render the tab panel (the active tab's `content`) below the tab list.\n * Defaults to true. Set to false when the consumer manages its own panel\n * rendering outside the Tabs component (e.g. pages that interleave panels\n * with other in-header elements).\n *\n * When false, each `TabItem.content` is ignored — the consumer is expected\n * to inspect `activeTab` / `onTabChange` and render its own panel.\n */\n renderPanel?: boolean;\n /** Accessible label for the overflow-menu trigger button. Default: \"More tabs\" */\n moreTabsLabel?: string;\n /** Accessible label for the mobile dropdown trigger. Default: \"Select tab\" */\n selectTabLabel?: string;\n /** Accessible label for the left scroll-arrow button. Default: \"Scroll left\" */\n scrollLeftLabel?: string;\n /** Accessible label for the right scroll-arrow button. Default: \"Scroll right\" */\n scrollRightLabel?: string;\n /**\n * English defaults for every guide-tag badge label (required / recommended\n * / beta / experimental). Override individual entries to translate them.\n */\n tagLabels?: TabTagLabels;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function Tabs({\n tabs,\n groups = [],\n defaultTab,\n activeTab: controlledTab,\n onTabChange,\n className = \"\",\n panelClassName = \"\",\n ariaLabel,\n showOverflowControls = true,\n showGroupLabels = true,\n overflowMode: explicitOverflowMode,\n variant = \"underlined\",\n fillContainer = false,\n renderPanel = true,\n moreTabsLabel = \"More tabs\",\n selectTabLabel = \"Select tab\",\n scrollLeftLabel = \"Scroll left\",\n scrollRightLabel = \"Scroll right\",\n tagLabels = DEFAULT_TAG_LABELS,\n}: TabsProps) {\n // Auto-detect overflow mode from groups presence\n const overflowMode =\n explicitOverflowMode ?? (groups.length > 0 ? \"menu\" : \"dropdown\");\n\n // ---- State ----\n const [internalActiveTab, setInternalActiveTab] = useState(\n defaultTab || tabs[0]?.id || \"\",\n );\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n\n // Dropdown mode state (overflowMode === \"dropdown\")\n const [isMobileDropdown, setIsMobileDropdown] = useState(false);\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n\n // Overflow menu state (overflowMode === \"menu\")\n const [showOverflowMenu, setShowOverflowMenu] = useState(false);\n const [focusedMenuIndex, setFocusedMenuIndex] = useState(-1);\n\n // ---- Refs ----\n const tabListRef = useRef<HTMLDivElement>(null);\n const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map());\n const dropdownContainerRef = useRef<HTMLDivElement>(null);\n const overflowMenuRef = useRef<HTMLDivElement>(null);\n const overflowButtonRef = useRef<HTMLButtonElement>(null);\n const menuItemRefs = useRef<HTMLButtonElement[]>([]);\n const scrollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const touchStartX = useRef<number>(0);\n const touchEndX = useRef<number>(0);\n\n // ---- Derived state ----\n const activeTab = controlledTab ?? internalActiveTab;\n\n // Group tabs by groupId\n const groupedTabs = useMemo(() => {\n return groups.length > 0\n ? groups.map((group) => ({\n group,\n tabs: tabs.filter((tab) => tab.groupId === group.id),\n }))\n : [{ group: null as TabGroupMeta | null, tabs }];\n }, [groups, tabs]);\n\n // Flatten menu items for keyboard navigation\n const flattenedMenuTabs = useMemo(() => {\n return groupedTabs.flatMap(({ tabs: groupTabs }) => groupTabs);\n }, [groupedTabs]);\n\n // ---- Handlers ----\n\n const handleTabClick = useCallback(\n (tabId: string) => {\n if (!controlledTab) {\n setInternalActiveTab(tabId);\n }\n onTabChange?.(tabId);\n setShowOverflowMenu(false);\n },\n [controlledTab, onTabChange],\n );\n\n const handleKeyDown = useCallback(\n (\n e: KeyboardEvent<HTMLButtonElement> | KeyboardEvent<HTMLDivElement>,\n currentIndex: number,\n ) => {\n let targetIndex: number;\n\n switch (e.key) {\n case \"ArrowRight\":\n e.preventDefault();\n targetIndex = currentIndex === tabs.length - 1 ? 0 : currentIndex + 1;\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n targetIndex = currentIndex === 0 ? tabs.length - 1 : currentIndex - 1;\n break;\n case \"Home\":\n e.preventDefault();\n targetIndex = 0;\n break;\n case \"End\":\n e.preventDefault();\n targetIndex = tabs.length - 1;\n break;\n default:\n return;\n }\n\n const targetTab = tabs[targetIndex];\n if (targetTab) {\n handleTabClick(targetTab.id);\n tabRefs.current.get(targetTab.id)?.focus();\n }\n },\n [tabs, handleTabClick],\n );\n\n const setTabRef = useCallback((id: string, element: HTMLButtonElement | null) => {\n if (element) {\n tabRefs.current.set(id, element);\n } else {\n tabRefs.current.delete(id);\n }\n }, []);\n\n // ---- Scroll controls ----\n\n const checkScroll = useCallback(() => {\n const container = tabListRef.current;\n if (!container || !showOverflowControls) {\n setCanScrollLeft(false);\n setCanScrollRight(false);\n return;\n }\n\n const { scrollLeft, scrollWidth, clientWidth } = container;\n setCanScrollLeft(scrollLeft > 0);\n setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);\n }, [showOverflowControls]);\n\n const scrollTabs = useCallback(\n (direction: \"left\" | \"right\") => {\n const container = tabListRef.current;\n if (!container) return;\n\n const scrollAmount = container.clientWidth * 0.75;\n container.scrollBy({\n left: direction === \"left\" ? -scrollAmount : scrollAmount,\n behavior: \"smooth\",\n });\n\n if (scrollTimeoutRef.current !== null) {\n clearTimeout(scrollTimeoutRef.current);\n }\n scrollTimeoutRef.current = setTimeout(checkScroll, 300);\n },\n [checkScroll],\n );\n\n // ---- Auto-scroll to active tab ----\n\n const scrollToActiveTab = useCallback(() => {\n if (!tabListRef.current || isMobileDropdown) return;\n\n const activeButton = tabRefs.current.get(activeTab);\n if (!activeButton) return;\n\n const container = tabListRef.current;\n const buttonRect = activeButton.getBoundingClientRect();\n const containerRect = container.getBoundingClientRect();\n\n if (buttonRect.left < containerRect.left) {\n container.scrollLeft -= containerRect.left - buttonRect.left + 8;\n } else if (buttonRect.right > containerRect.right) {\n container.scrollLeft += buttonRect.right - containerRect.right + 8;\n }\n }, [activeTab, isMobileDropdown]);\n\n // ---- Mobile dropdown mode ----\n\n const checkMobileMode = useCallback(() => {\n if (typeof window === \"undefined\" || overflowMode !== \"dropdown\") return;\n setIsMobileDropdown(window.innerWidth < 480);\n }, [overflowMode]);\n\n const toggleDropdown = useCallback(() => {\n setIsDropdownOpen((prev) => !prev);\n }, []);\n\n const handleDropdownKeyDown = useCallback(\n (e: KeyboardEvent<HTMLButtonElement>) => {\n if (e.key === \"Escape\" && isDropdownOpen) {\n e.preventDefault();\n setIsDropdownOpen(false);\n }\n },\n [isDropdownOpen],\n );\n\n // ---- Touch swipe (dropdown mode) ----\n\n const handleTouchStart = useCallback((e: TouchEvent<HTMLDivElement>) => {\n if (e.touches[0]) {\n touchStartX.current = e.touches[0].clientX;\n }\n }, []);\n\n const handleTouchMove = useCallback((e: TouchEvent<HTMLDivElement>) => {\n if (e.touches[0]) {\n touchEndX.current = e.touches[0].clientX;\n }\n }, []);\n\n const handleTouchEnd = useCallback(() => {\n if (!touchStartX.current || !touchEndX.current) return;\n\n const diff = touchStartX.current - touchEndX.current;\n const threshold = 50;\n\n if (Math.abs(diff) > threshold) {\n const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);\n const nextTab = tabs[currentIndex + 1];\n const prevTab = tabs[currentIndex - 1];\n\n if (diff > 0 && nextTab) {\n handleTabClick(nextTab.id);\n } else if (diff < 0 && prevTab) {\n handleTabClick(prevTab.id);\n }\n }\n\n touchStartX.current = 0;\n touchEndX.current = 0;\n }, [activeTab, tabs, handleTabClick]);\n\n // ---- Overflow menu (menu mode) ----\n\n const handleOverflowMenuKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n const menuItemCount = flattenedMenuTabs.length;\n\n switch (e.key) {\n case \"Escape\":\n e.preventDefault();\n setShowOverflowMenu(false);\n setFocusedMenuIndex(-1);\n overflowButtonRef.current?.focus();\n break;\n case \"ArrowDown\":\n e.preventDefault();\n setFocusedMenuIndex((prev) => {\n const next = prev >= menuItemCount - 1 ? 0 : prev + 1;\n menuItemRefs.current[next]?.focus();\n return next;\n });\n break;\n case \"ArrowUp\":\n e.preventDefault();\n setFocusedMenuIndex((prev) => {\n const next = prev <= 0 ? menuItemCount - 1 : prev - 1;\n menuItemRefs.current[next]?.focus();\n return next;\n });\n break;\n case \"Home\":\n e.preventDefault();\n setFocusedMenuIndex(0);\n menuItemRefs.current[0]?.focus();\n break;\n case \"End\":\n e.preventDefault();\n setFocusedMenuIndex(menuItemCount - 1);\n menuItemRefs.current[menuItemCount - 1]?.focus();\n break;\n default:\n break;\n }\n },\n [flattenedMenuTabs.length],\n );\n\n const handleOverflowButtonKeyDown = useCallback(\n (e: KeyboardEvent<HTMLButtonElement>) => {\n if (e.key === \"Escape\" && showOverflowMenu) {\n e.preventDefault();\n setShowOverflowMenu(false);\n setFocusedMenuIndex(-1);\n }\n },\n [showOverflowMenu],\n );\n\n // ---- Effects ----\n\n // Close dropdown when clicking outside\n useEffect(() => {\n if (!isDropdownOpen) return;\n\n const handleClickOutside = (event: MouseEvent) => {\n if (\n dropdownContainerRef.current &&\n !dropdownContainerRef.current.contains(event.target as Node)\n ) {\n setIsDropdownOpen(false);\n }\n };\n\n document.addEventListener(\"mousedown\", handleClickOutside);\n return () => {\n document.removeEventListener(\"mousedown\", handleClickOutside);\n };\n }, [isDropdownOpen]);\n\n // Close overflow menu when clicking outside\n useEffect(() => {\n if (!showOverflowMenu) return undefined;\n\n const handleClickOutside = (event: MouseEvent) => {\n if (\n overflowMenuRef.current &&\n !overflowMenuRef.current.contains(event.target as Node)\n ) {\n setShowOverflowMenu(false);\n setFocusedMenuIndex(-1);\n }\n };\n\n const handleEscapeKey = (event: globalThis.KeyboardEvent) => {\n if (event.key === \"Escape\") {\n setShowOverflowMenu(false);\n setFocusedMenuIndex(-1);\n overflowButtonRef.current?.focus();\n }\n };\n\n document.addEventListener(\"mousedown\", handleClickOutside);\n document.addEventListener(\"keydown\", handleEscapeKey);\n return () => {\n document.removeEventListener(\"mousedown\", handleClickOutside);\n document.removeEventListener(\"keydown\", handleEscapeKey);\n };\n }, [showOverflowMenu]);\n\n // Focus menu item when overflow menu opens\n useEffect(() => {\n if (showOverflowMenu && menuItemRefs.current.length > 0) {\n const activeIndex = flattenedMenuTabs.findIndex((tab) => tab.id === activeTab);\n const indexToFocus = activeIndex >= 0 ? activeIndex : 0;\n setFocusedMenuIndex(indexToFocus);\n requestAnimationFrame(() => {\n menuItemRefs.current[indexToFocus]?.focus();\n });\n }\n }, [showOverflowMenu, flattenedMenuTabs, activeTab]);\n\n // Ensure active tab is valid\n useEffect(() => {\n if (!tabs.find((tab) => tab.id === activeTab)) {\n const firstTab = tabs[0];\n if (firstTab) {\n handleTabClick(firstTab.id);\n }\n }\n }, [tabs, activeTab, handleTabClick]);\n\n // Check mobile mode and scroll on mount/resize\n useEffect(() => {\n checkMobileMode();\n checkScroll();\n\n const handleResize = () => {\n checkMobileMode();\n checkScroll();\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => {\n window.removeEventListener(\"resize\", handleResize);\n };\n }, [checkMobileMode, checkScroll]);\n\n // Scroll to active tab when it changes\n useEffect(() => {\n scrollToActiveTab();\n }, [activeTab, scrollToActiveTab]);\n\n // Check scroll when tabs change\n useEffect(() => {\n checkScroll();\n }, [tabs, checkScroll]);\n\n // Cleanup scroll timeout on unmount\n useEffect(() => {\n return () => {\n if (scrollTimeoutRef.current !== null) {\n clearTimeout(scrollTimeoutRef.current);\n }\n };\n }, []);\n\n // ---- Render helpers ----\n\n const activeTabContent = tabs.find((tab) => tab.id === activeTab)?.content;\n const activeTabLabel = tabs.find((tab) => tab.id === activeTab)?.label;\n\n const hasGroups = groups.length > 0;\n\n // Reset menu item refs array each render\n menuItemRefs.current = [];\n\n /** Resolve the effective tag type for a tab (prefers `tag`, falls back to legacy `required`) */\n const resolveTabTag = (tab: TabItem): TabTagType | null => {\n if (tab.tag) return tab.tag;\n\n if (tab.required) return \"required\";\n return null;\n };\n\n /** Render a single tab button */\n const renderTabButton = (tab: TabItem, globalIndex: number) => {\n const isActive = tab.id === activeTab;\n const effectiveTag = resolveTabTag(tab);\n const tagConfig = effectiveTag ? TAG_CONFIG[effectiveTag] : null;\n const tagLabel = effectiveTag ? tagLabels[effectiveTag] : null;\n\n return (\n <button\n key={tab.id}\n ref={(el) => {\n setTabRef(tab.id, el);\n }}\n role=\"tab\"\n aria-selected={isActive}\n aria-controls={renderPanel ? `tabpanel-${tab.id}` : undefined}\n id={`tab-${tab.id}`}\n tabIndex={isActive ? 0 : -1}\n className={`tabs__tab${isActive ? \" tabs__tab--active\" : \"\"}`}\n onClick={() => {\n handleTabClick(tab.id);\n }}\n onKeyDown={(e) => {\n handleKeyDown(e, globalIndex);\n // Prevent the tablist's onKeyDown from also firing (it runs the\n // same Arrow/Home/End logic for synthetic tests / unusual focus\n // setups where focus is on the tablist itself).\n if (\n e.key === \"ArrowLeft\" ||\n e.key === \"ArrowRight\" ||\n e.key === \"Home\" ||\n e.key === \"End\"\n ) {\n e.stopPropagation();\n }\n }}\n >\n {tab.labelPrefix}\n <span className=\"tabs__tab-label\">{tab.label}</span>\n {tagConfig && (\n <Badge variant={tagConfig.variant} size=\"small\" className=\"tabs__tag-badge\">\n {tagLabel}\n </Badge>\n )}\n {tab.labelExtra}\n </button>\n );\n };\n\n /** Render the tab list content (flat or grouped) */\n const renderTabListContent = () => {\n if (hasGroups) {\n return groupedTabs.map(({ group, tabs: groupTabs }, groupIndex) => (\n <div key={group?.id || \"default\"} className=\"tabs__group\">\n {group && (\n <>\n {groupIndex > 0 && (\n <div className=\"tabs__separator\" role=\"separator\" aria-hidden=\"true\" />\n )}\n {showGroupLabels && (\n <span className=\"tabs__group-label\">{group.label}</span>\n )}\n </>\n )}\n <div className=\"tabs__group-tabs\">\n {groupTabs.map((tab) => {\n const globalIndex = tabs.findIndex((t) => t.id === tab.id);\n return renderTabButton(tab, globalIndex);\n })}\n </div>\n </div>\n ));\n }\n\n return tabs.map((tab, index) => renderTabButton(tab, index));\n };\n\n /** Render the overflow menu (menu mode) */\n const renderOverflowMenu = () => {\n if (overflowMode !== \"menu\" || !showOverflowControls) return null;\n\n return (\n <div className=\"tabs__overflow\" ref={overflowMenuRef}>\n <button\n ref={overflowButtonRef}\n type=\"button\"\n className=\"tabs__overflow-btn\"\n onClick={() => {\n setShowOverflowMenu(!showOverflowMenu);\n }}\n onKeyDown={handleOverflowButtonKeyDown}\n aria-label={moreTabsLabel}\n aria-expanded={showOverflowMenu}\n aria-haspopup=\"menu\"\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"1\" />\n <circle cx=\"12\" cy=\"5\" r=\"1\" />\n <circle cx=\"12\" cy=\"19\" r=\"1\" />\n </svg>\n </button>\n\n {showOverflowMenu && (\n <div\n className=\"tabs__overflow-menu\"\n role=\"menu\"\n onKeyDown={handleOverflowMenuKeyDown}\n >\n {groupedTabs.map(({ group, tabs: groupTabs }) => (\n <div key={group?.id || \"default\"} className=\"tabs__overflow-group\">\n {group && showGroupLabels && (\n <div className=\"tabs__overflow-group-label\">{group.label}</div>\n )}\n {groupTabs.map((tab) => {\n const isActive = tab.id === activeTab;\n const flatIndex = flattenedMenuTabs.findIndex((t) => t.id === tab.id);\n const effectiveTag = resolveTabTag(tab);\n const tagConfig = effectiveTag ? TAG_CONFIG[effectiveTag] : null;\n const tagLabel = effectiveTag ? tagLabels[effectiveTag] : null;\n return (\n <button\n key={tab.id}\n ref={(el) => {\n if (el) {\n menuItemRefs.current[flatIndex] = el;\n }\n }}\n role=\"menuitem\"\n tabIndex={focusedMenuIndex === flatIndex ? 0 : -1}\n className={`tabs__overflow-item ${isActive ? \"tabs__overflow-item--active\" : \"\"}`}\n onClick={() => {\n handleTabClick(tab.id);\n }}\n >\n {tab.labelPrefix}\n <span>{tab.label}</span>\n {tagConfig && (\n <Badge variant={tagConfig.variant} size=\"small\">\n {tagLabel}\n </Badge>\n )}\n {tab.labelExtra}\n </button>\n );\n })}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n };\n\n // ---- Variant class helpers ----\n\n // For segmented/compact variants we always want a single horizontal pill —\n // there is no scrollable arrow affordance and the mobile dropdown trigger\n // would visually conflict with the segmented look. Underlined remains the\n // only variant that follows the historical \"scroll + dropdown\" behavior.\n const isSegmentedFamily = variant === \"segmented\" || variant === \"compact\";\n const variantClass =\n variant === \"segmented\"\n ? \"tabs--variant-segmented\"\n : variant === \"compact\"\n ? \"tabs--variant-compact\"\n : \"\";\n const fillContainerClass =\n isSegmentedFamily && fillContainer ? \"tabs--fill-container\" : \"\";\n\n // ---- Mobile dropdown render (overflowMode === \"dropdown\") ----\n\n // Segmented/compact variants always render as a single inline tab bar — we\n // skip the mobile-dropdown branch even when the viewport is narrow so the\n // visual matches the existing per-page \"segmented control\" markup the\n // migration replaces.\n if (overflowMode === \"dropdown\" && isMobileDropdown && !isSegmentedFamily) {\n return (\n <div className={`tabs tabs--mobile-dropdown ${className}`}>\n <div className=\"tabs__dropdown-container\" ref={dropdownContainerRef}>\n <button\n className=\"tabs__dropdown-trigger\"\n onClick={toggleDropdown}\n onKeyDown={handleDropdownKeyDown}\n aria-expanded={isDropdownOpen}\n aria-haspopup=\"listbox\"\n aria-label={ariaLabel || selectTabLabel}\n >\n <span className=\"tabs__dropdown-label\">{activeTabLabel}</span>\n <svg\n className={`tabs__dropdown-arrow ${isDropdownOpen ? \"tabs__dropdown-arrow--open\" : \"\"}`}\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 6L8 10L12 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n\n {isDropdownOpen && (\n <div className=\"tabs__dropdown-menu\" role=\"listbox\">\n {tabs.map((tab) => (\n <button\n key={tab.id}\n role=\"option\"\n aria-selected={tab.id === activeTab}\n className={`tabs__dropdown-item ${tab.id === activeTab ? \"tabs__dropdown-item--active\" : \"\"}`}\n onClick={() => {\n handleTabClick(tab.id);\n setIsDropdownOpen(false);\n }}\n >\n {tab.label}\n </button>\n ))}\n </div>\n )}\n </div>\n\n {renderPanel && (\n <div\n id={`tabpanel-${activeTab}`}\n role=\"tabpanel\"\n aria-labelledby={`tab-${activeTab}`}\n className={`tabs__panel ${panelClassName}`.trim()}\n tabIndex={0}\n onTouchStart={handleTouchStart}\n onTouchMove={handleTouchMove}\n onTouchEnd={handleTouchEnd}\n >\n {activeTabContent}\n </div>\n )}\n </div>\n );\n }\n\n // ---- Desktop/tablet render ----\n\n const modeClass = overflowMode === \"menu\" ? \"tabs--overflow-menu\" : \"\";\n\n return (\n <div\n className={`tabs ${modeClass} ${variantClass} ${fillContainerClass} ${className}`\n .replace(/\\s+/g, \" \")\n .trim()}\n >\n <div className=\"tabs__container\">\n {/* Scroll left arrow */}\n {showOverflowControls && canScrollLeft && (\n <button\n className=\"tabs__scroll-arrow tabs__scroll-arrow--left\"\n onClick={() => {\n scrollTabs(\"left\");\n }}\n aria-label={scrollLeftLabel}\n tabIndex={-1}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 15L7 10L12 5\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n )}\n\n <div\n ref={tabListRef}\n className=\"tabs__list\"\n role=\"tablist\"\n aria-label={ariaLabel}\n onScroll={checkScroll}\n onKeyDown={(e) => {\n // Mirror the per-button handler at the tablist level so synthetic\n // tests (and unusual focus setups where the tablist itself owns\n // the keyboard focus) get the same Arrow/Home/End behavior. Tab\n // buttons are still the primary handler; this only fires when\n // the event reaches the tablist without being handled by a tab.\n const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);\n if (currentIndex < 0) return;\n handleKeyDown(e, currentIndex);\n }}\n onTouchStart={overflowMode === \"dropdown\" ? handleTouchStart : undefined}\n onTouchMove={overflowMode === \"dropdown\" ? handleTouchMove : undefined}\n onTouchEnd={overflowMode === \"dropdown\" ? handleTouchEnd : undefined}\n >\n {renderTabListContent()}\n </div>\n\n {/* Scroll right arrow */}\n {showOverflowControls && canScrollRight && (\n <button\n className=\"tabs__scroll-arrow tabs__scroll-arrow--right\"\n onClick={() => {\n scrollTabs(\"right\");\n }}\n aria-label={scrollRightLabel}\n tabIndex={-1}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M8 5L13 10L8 15\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n )}\n\n {/* Overflow menu (menu mode) */}\n {renderOverflowMenu()}\n </div>\n\n {renderPanel && (\n <div\n id={`tabpanel-${activeTab}`}\n role=\"tabpanel\"\n aria-labelledby={`tab-${activeTab}`}\n className={`tabs__panel ${panelClassName}`.trim()}\n tabIndex={0}\n >\n {activeTabContent}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAoCA,IAAM,KAAmC;CACvC,UAAU;CACV,aAAa;CACb,MAAM;CACN,cAAc;AAChB;AAiHA,SAAgB,EAAK,EACnB,SACA,YAAS,CAAC,GACV,gBACA,WAAW,GACX,gBACA,eAAY,IACZ,oBAAiB,IACjB,eACA,0BAAuB,IACvB,qBAAkB,IAClB,cAAc,IACd,aAAU,cACV,oBAAgB,IAChB,iBAAc,IACd,oBAAgB,aAChB,qBAAiB,cACjB,sBAAkB,eAClB,uBAAmB,gBACnB,eAAY,MACA;CAEZ,IAAM,IACJ,OAAyB,EAAO,SAAS,IAAI,SAAS,aAGlD,CAAC,IAAmB,MAAwB,EAChD,MAAc,EAAK,EAAE,EAAE,MAAM,EAC/B,GACM,CAAC,IAAe,KAAoB,EAAS,EAAK,GAClD,CAAC,IAAgB,KAAqB,EAAS,EAAK,GAGpD,CAAC,GAAkB,MAAuB,EAAS,EAAK,GACxD,CAAC,GAAgB,KAAqB,EAAS,EAAK,GAGpD,CAAC,GAAkB,KAAuB,EAAS,EAAK,GACxD,CAAC,IAAkB,KAAuB,EAAS,EAAE,GAGrD,IAAa,EAAuB,IAAI,GACxC,IAAU,kBAAuC,IAAI,IAAI,CAAC,GAC1D,IAAuB,EAAuB,IAAI,GAClD,IAAkB,EAAuB,IAAI,GAC7C,IAAoB,EAA0B,IAAI,GAClD,IAAe,EAA4B,CAAC,CAAC,GAC7C,IAAmB,EAA6C,IAAI,GACpE,IAAc,EAAe,CAAC,GAC9B,IAAY,EAAe,CAAC,GAG5B,IAAY,KAAiB,IAG7B,IAAc,QACX,EAAO,SAAS,IACnB,EAAO,KAAK,OAAW;EACrB;EACA,MAAM,EAAK,QAAQ,MAAQ,EAAI,YAAY,EAAM,EAAE;CACrD,EAAE,IACF,CAAC;EAAE,OAAO;EAA6B;CAAK,CAAC,GAChD,CAAC,GAAQ,CAAI,CAAC,GAGX,IAAoB,QACjB,EAAY,SAAS,EAAE,MAAM,QAAgB,CAAS,GAC5D,CAAC,CAAW,CAAC,GAIV,IAAiB,GACpB,MAAkB;EAKjB,AAJK,KACH,GAAqB,CAAK,GAE5B,IAAc,CAAK,GACnB,EAAoB,EAAK;CAC3B,GACA,CAAC,GAAe,CAAW,CAC7B,GAEM,IAAgB,GAElB,GACA,MACG;EACH,IAAI;EAEJ,QAAQ,EAAE,KAAV;GACE,KAAK;IAEH,AADA,EAAE,eAAe,GACjB,IAAc,MAAiB,EAAK,SAAS,IAAI,IAAI,IAAe;IACpE;GACF,KAAK;IAEH,AADA,EAAE,eAAe,GACjB,IAAc,MAAiB,IAAI,EAAK,SAAS,IAAI,IAAe;IACpE;GACF,KAAK;IAEH,AADA,EAAE,eAAe,GACjB,IAAc;IACd;GACF,KAAK;IAEH,AADA,EAAE,eAAe,GACjB,IAAc,EAAK,SAAS;IAC5B;GACF,SACE;EACJ;EAEA,IAAM,IAAY,EAAK;EACvB,AAAI,MACF,EAAe,EAAU,EAAE,GAC3B,EAAQ,QAAQ,IAAI,EAAU,EAAE,CAAC,EAAE,MAAM;CAE7C,GACA,CAAC,GAAM,CAAc,CACvB,GAEM,KAAY,GAAa,GAAY,MAAsC;EAC/E,AAAI,IACF,EAAQ,QAAQ,IAAI,GAAI,CAAO,IAE/B,EAAQ,QAAQ,OAAO,CAAE;CAE7B,GAAG,CAAC,CAAC,GAIC,IAAc,QAAkB;EACpC,IAAM,IAAY,EAAW;EAC7B,IAAI,CAAC,KAAa,CAAC,GAAsB;GAEvC,AADA,EAAiB,EAAK,GACtB,EAAkB,EAAK;GACvB;EACF;EAEA,IAAM,EAAE,eAAY,gBAAa,mBAAgB;EAEjD,AADA,EAAiB,IAAa,CAAC,GAC/B,EAAkB,IAAa,IAAc,IAAc,CAAC;CAC9D,GAAG,CAAC,CAAoB,CAAC,GAEnB,IAAa,GAChB,MAAgC;EAC/B,IAAM,IAAY,EAAW;EAC7B,IAAI,CAAC,GAAW;EAEhB,IAAM,IAAe,EAAU,cAAc;EAS7C,AARA,EAAU,SAAS;GACjB,MAAM,MAAc,SAAS,CAAC,IAAe;GAC7C,UAAU;EACZ,CAAC,GAEG,EAAiB,YAAY,QAC/B,aAAa,EAAiB,OAAO,GAEvC,EAAiB,UAAU,WAAW,GAAa,GAAG;CACxD,GACA,CAAC,CAAW,CACd,GAIM,IAAoB,QAAkB;EAC1C,IAAI,CAAC,EAAW,WAAW,GAAkB;EAE7C,IAAM,IAAe,EAAQ,QAAQ,IAAI,CAAS;EAClD,IAAI,CAAC,GAAc;EAEnB,IAAM,IAAY,EAAW,SACvB,IAAa,EAAa,sBAAsB,GAChD,IAAgB,EAAU,sBAAsB;EAEtD,AAAI,EAAW,OAAO,EAAc,OAClC,EAAU,cAAc,EAAc,OAAO,EAAW,OAAO,IACtD,EAAW,QAAQ,EAAc,UAC1C,EAAU,cAAc,EAAW,QAAQ,EAAc,QAAQ;CAErE,GAAG,CAAC,GAAW,CAAgB,CAAC,GAI1B,IAAkB,QAAkB;EACpC,OAAO,SAAW,OAAe,MAAiB,cACtD,GAAoB,OAAO,aAAa,GAAG;CAC7C,GAAG,CAAC,CAAY,CAAC,GAEX,KAAiB,QAAkB;EACvC,GAAmB,MAAS,CAAC,CAAI;CACnC,GAAG,CAAC,CAAC,GAEC,KAAwB,GAC3B,MAAwC;EACvC,AAAI,EAAE,QAAQ,YAAY,MACxB,EAAE,eAAe,GACjB,EAAkB,EAAK;CAE3B,GACA,CAAC,CAAc,CACjB,GAIM,IAAmB,GAAa,MAAkC;EACtE,AAAI,EAAE,QAAQ,OACZ,EAAY,UAAU,EAAE,QAAQ,EAAE,CAAC;CAEvC,GAAG,CAAC,CAAC,GAEC,IAAkB,GAAa,MAAkC;EACrE,AAAI,EAAE,QAAQ,OACZ,EAAU,UAAU,EAAE,QAAQ,EAAE,CAAC;CAErC,GAAG,CAAC,CAAC,GAEC,IAAiB,QAAkB;EACvC,IAAI,CAAC,EAAY,WAAW,CAAC,EAAU,SAAS;EAEhD,IAAM,IAAO,EAAY,UAAU,EAAU;EAG7C,IAAI,KAAK,IAAI,CAAI,IAAI,IAAW;GAC9B,IAAM,IAAe,EAAK,WAAW,MAAQ,EAAI,OAAO,CAAS,GAC3D,IAAU,EAAK,IAAe,IAC9B,IAAU,EAAK,IAAe;GAEpC,AAAI,IAAO,KAAK,IACd,EAAe,EAAQ,EAAE,IAChB,IAAO,KAAK,KACrB,EAAe,EAAQ,EAAE;EAE7B;EAGA,AADA,EAAY,UAAU,GACtB,EAAU,UAAU;CACtB,GAAG;EAAC;EAAW;EAAM;CAAc,CAAC,GAI9B,KAA4B,GAC/B,MAAqC;EACpC,IAAM,IAAgB,EAAkB;EAExC,QAAQ,EAAE,KAAV;GACE,KAAK;IAIH,AAHA,EAAE,eAAe,GACjB,EAAoB,EAAK,GACzB,EAAoB,EAAE,GACtB,EAAkB,SAAS,MAAM;IACjC;GACF,KAAK;IAEH,AADA,EAAE,eAAe,GACjB,GAAqB,MAAS;KAC5B,IAAM,IAAO,KAAQ,IAAgB,IAAI,IAAI,IAAO;KAEpD,OADA,EAAa,QAAQ,EAAK,EAAE,MAAM,GAC3B;IACT,CAAC;IACD;GACF,KAAK;IAEH,AADA,EAAE,eAAe,GACjB,GAAqB,MAAS;KAC5B,IAAM,IAAO,KAAQ,IAAI,IAAgB,IAAI,IAAO;KAEpD,OADA,EAAa,QAAQ,EAAK,EAAE,MAAM,GAC3B;IACT,CAAC;IACD;GACF,KAAK;IAGH,AAFA,EAAE,eAAe,GACjB,EAAoB,CAAC,GACrB,EAAa,QAAQ,EAAE,EAAE,MAAM;IAC/B;GACF,KAAK,OAGH,AAFA,EAAE,eAAe,GACjB,EAAoB,IAAgB,CAAC,GACrC,EAAa,QAAQ,IAAgB,EAAE,EAAE,MAAM;EAInD;CACF,GACA,CAAC,EAAkB,MAAM,CAC3B,GAEM,KAA8B,GACjC,MAAwC;EACvC,AAAI,EAAE,QAAQ,YAAY,MACxB,EAAE,eAAe,GACjB,EAAoB,EAAK,GACzB,EAAoB,EAAE;CAE1B,GACA,CAAC,CAAgB,CACnB;CAsGA,AAjGA,QAAgB;EACd,IAAI,CAAC,GAAgB;EAErB,IAAM,KAAsB,MAAsB;GAChD,AACE,EAAqB,WACrB,CAAC,EAAqB,QAAQ,SAAS,EAAM,MAAc,KAE3D,EAAkB,EAAK;EAE3B;EAGA,OADA,SAAS,iBAAiB,aAAa,CAAkB,SAC5C;GACX,SAAS,oBAAoB,aAAa,CAAkB;EAC9D;CACF,GAAG,CAAC,CAAc,CAAC,GAGnB,QAAgB;EACd,IAAI,CAAC,GAAkB;EAEvB,IAAM,KAAsB,MAAsB;GAChD,AACE,EAAgB,WAChB,CAAC,EAAgB,QAAQ,SAAS,EAAM,MAAc,MAEtD,EAAoB,EAAK,GACzB,EAAoB,EAAE;EAE1B,GAEM,KAAmB,MAAoC;GAC3D,AAAI,EAAM,QAAQ,aAChB,EAAoB,EAAK,GACzB,EAAoB,EAAE,GACtB,EAAkB,SAAS,MAAM;EAErC;EAIA,OAFA,SAAS,iBAAiB,aAAa,CAAkB,GACzD,SAAS,iBAAiB,WAAW,CAAe,SACvC;GAEX,AADA,SAAS,oBAAoB,aAAa,CAAkB,GAC5D,SAAS,oBAAoB,WAAW,CAAe;EACzD;CACF,GAAG,CAAC,CAAgB,CAAC,GAGrB,QAAgB;EACd,IAAI,KAAoB,EAAa,QAAQ,SAAS,GAAG;GACvD,IAAM,IAAc,EAAkB,WAAW,MAAQ,EAAI,OAAO,CAAS,GACvE,IAAe,KAAe,IAAI,IAAc;GAEtD,AADA,EAAoB,CAAY,GAChC,4BAA4B;IAC1B,EAAa,QAAQ,EAAa,EAAE,MAAM;GAC5C,CAAC;EACH;CACF,GAAG;EAAC;EAAkB;EAAmB;CAAS,CAAC,GAGnD,QAAgB;EACd,IAAI,CAAC,EAAK,MAAM,MAAQ,EAAI,OAAO,CAAS,GAAG;GAC7C,IAAM,IAAW,EAAK;GACtB,AAAI,KACF,EAAe,EAAS,EAAE;EAE9B;CACF,GAAG;EAAC;EAAM;EAAW;CAAc,CAAC,GAGpC,QAAgB;EAEd,AADA,EAAgB,GAChB,EAAY;EAEZ,IAAM,UAAqB;GAEzB,AADA,EAAgB,GAChB,EAAY;EACd;EAGA,OADA,OAAO,iBAAiB,UAAU,CAAY,SACjC;GACX,OAAO,oBAAoB,UAAU,CAAY;EACnD;CACF,GAAG,CAAC,GAAiB,CAAW,CAAC,GAGjC,QAAgB;EACd,EAAkB;CACpB,GAAG,CAAC,GAAW,CAAiB,CAAC,GAGjC,QAAgB;EACd,EAAY;CACd,GAAG,CAAC,GAAM,CAAW,CAAC,GAGtB,cACe;EACX,AAAI,EAAiB,YAAY,QAC/B,aAAa,EAAiB,OAAO;CAEzC,GACC,CAAC,CAAC;CAIL,IAAM,IAAmB,EAAK,MAAM,MAAQ,EAAI,OAAO,CAAS,CAAC,EAAE,SAC7D,KAAiB,EAAK,MAAM,MAAQ,EAAI,OAAO,CAAS,CAAC,EAAE,OAE3D,KAAY,EAAO,SAAS;CAGlC,EAAa,UAAU,CAAC;CAGxB,IAAM,KAAiB,MACjB,EAAI,MAAY,EAAI,MAEpB,EAAI,WAAiB,aAClB,MAIH,MAAmB,GAAc,MAAwB;EAC7D,IAAM,IAAW,EAAI,OAAO,GACtB,IAAe,EAAc,CAAG,GAChC,IAAY,IAAe,EAAW,KAAgB,MACtD,IAAW,IAAe,EAAU,KAAgB;EAE1D,OACE,kBAAC,UAAD;GAEE,MAAM,MAAO;IACX,GAAU,EAAI,IAAI,CAAE;GACtB;GACA,MAAK;GACL,iBAAe;GACf,iBAAe,IAAc,YAAY,EAAI,OAAO,KAAA;GACpD,IAAI,OAAO,EAAI;GACf,UAAU,IAAW,IAAI;GACzB,WAAW,YAAY,IAAW,uBAAuB;GACzD,eAAe;IACb,EAAe,EAAI,EAAE;GACvB;GACA,YAAY,MAAM;IAKhB,AAJA,EAAc,GAAG,CAAW,IAK1B,EAAE,QAAQ,eACV,EAAE,QAAQ,gBACV,EAAE,QAAQ,UACV,EAAE,QAAQ,UAEV,EAAE,gBAAgB;GAEtB;GA3BF,UAAA;IA6BG,EAAI;IACL,kBAAC,QAAD;KAAM,WAAU;KAAmB,UAAA,EAAI;IAAY,CAAA;IAClD,KACC,kBAAC,GAAD;KAAO,SAAS,EAAU;KAAS,MAAK;KAAQ,WAAU;KACvD,UAAA;IACI,CAAA;IAER,EAAI;GACC;EApCD,GAAA,EAAI,EAoCH;CAEZ,GAGM,WACA,KACK,EAAY,KAAK,EAAE,UAAO,MAAM,KAAa,MAClD,kBAAC,OAAD;EAAkC,WAAU;EAA5C,UAAA,CACG,KACC,kBAAA,GAAA,EAAA,UAAA,CACG,IAAa,KACZ,kBAAC,OAAD;GAAK,WAAU;GAAkB,MAAK;GAAY,eAAY;EAAQ,CAAA,GAEvE,KACC,kBAAC,QAAD;GAAM,WAAU;GAAqB,UAAA,EAAM;EAAY,CAAA,CAEzD,EAAA,CAAA,GAEJ,kBAAC,OAAD;GAAK,WAAU;GACZ,UAAA,EAAU,KAAK,MAAQ;IACtB,IAAM,IAAc,EAAK,WAAW,MAAM,EAAE,OAAO,EAAI,EAAE;IACzD,OAAO,GAAgB,GAAK,CAAW;GACzC,CAAC;EACE,CAAA,CACF;CAjBK,GAAA,GAAO,MAAM,SAiBlB,CACN,IAGI,EAAK,KAAK,GAAK,MAAU,GAAgB,GAAK,CAAK,CAAC,GAIvD,WACA,MAAiB,UAAU,CAAC,IAA6B,OAG3D,kBAAC,OAAD;EAAK,WAAU;EAAiB,KAAK;EAArC,UAAA,CACE,kBAAC,UAAD;GACE,KAAK;GACL,MAAK;GACL,WAAU;GACV,eAAe;IACb,EAAoB,CAAC,CAAgB;GACvC;GACA,WAAW;GACX,cAAY;GACZ,iBAAe;GACf,iBAAc;GAEd,UAAA,kBAAC,OAAD;IACE,OAAM;IACN,QAAO;IACP,SAAQ;IACR,MAAK;IACL,QAAO;IACP,aAAY;IACZ,eAAc;IACd,gBAAe;IARjB,UAAA;KAUE,kBAAC,UAAD;MAAQ,IAAG;MAAK,IAAG;MAAK,GAAE;KAAK,CAAA;KAC/B,kBAAC,UAAD;MAAQ,IAAG;MAAK,IAAG;MAAI,GAAE;KAAK,CAAA;KAC9B,kBAAC,UAAD;MAAQ,IAAG;MAAK,IAAG;MAAK,GAAE;KAAK,CAAA;IAC5B;;EACC,CAAA,GAEP,KACC,kBAAC,OAAD;GACE,WAAU;GACV,MAAK;GACL,WAAW;GAEV,UAAA,EAAY,KAAK,EAAE,UAAO,MAAM,QAC/B,kBAAC,OAAD;IAAkC,WAAU;IAA5C,UAAA,CACG,KAAS,KACR,kBAAC,OAAD;KAAK,WAAU;KAA8B,UAAA,EAAM;IAAW,CAAA,GAE/D,EAAU,KAAK,MAAQ;KACtB,IAAM,IAAW,EAAI,OAAO,GACtB,IAAY,EAAkB,WAAW,MAAM,EAAE,OAAO,EAAI,EAAE,GAC9D,IAAe,EAAc,CAAG,GAChC,IAAY,IAAe,EAAW,KAAgB,MACtD,IAAW,IAAe,EAAU,KAAgB;KAC1D,OACE,kBAAC,UAAD;MAEE,MAAM,MAAO;OACX,AAAI,MACF,EAAa,QAAQ,KAAa;MAEtC;MACA,MAAK;MACL,UAAU,OAAqB,IAAY,IAAI;MAC/C,WAAW,uBAAuB,IAAW,gCAAgC;MAC7E,eAAe;OACb,EAAe,EAAI,EAAE;MACvB;MAZF,UAAA;OAcG,EAAI;OACL,kBAAC,QAAD,EAAA,UAAO,EAAI,MAAY,CAAA;OACtB,KACC,kBAAC,GAAD;QAAO,SAAS,EAAU;QAAS,MAAK;QACrC,UAAA;OACI,CAAA;OAER,EAAI;MACC;KArBD,GAAA,EAAI,EAqBH;IAEZ,CAAC,CACE;GApCK,GAAA,GAAO,MAAM,SAoClB,CACN;EACE,CAAA,CAEJ;KAUH,IAAoB,MAAY,eAAe,MAAY;CA0FjE,OA1EI,MAAiB,cAAc,KAAoB,CAAC,IAEpD,kBAAC,OAAD;EAAK,WAAW,8BAA8B;EAA9C,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;GAA2B,KAAK;GAA/C,UAAA,CACE,kBAAC,UAAD;IACE,WAAU;IACV,SAAS;IACT,WAAW;IACX,iBAAe;IACf,iBAAc;IACd,cAAY,MAAa;IAN3B,UAAA,CAQE,kBAAC,QAAD;KAAM,WAAU;KAAwB,UAAA;IAAqB,CAAA,GAC7D,kBAAC,OAAD;KACE,WAAW,wBAAwB,IAAiB,+BAA+B;KACnF,OAAM;KACN,QAAO;KACP,SAAQ;KACR,MAAK;KACL,OAAM;KACN,eAAY;KAEZ,UAAA,kBAAC,QAAD;MACE,GAAE;MACF,QAAO;MACP,aAAY;MACZ,eAAc;MACd,gBAAe;KAChB,CAAA;IACE,CAAA,CACC;GAEP,CAAA,GAAA,KACC,kBAAC,OAAD;IAAK,WAAU;IAAsB,MAAK;IACvC,UAAA,EAAK,KAAK,MACT,kBAAC,UAAD;KAEE,MAAK;KACL,iBAAe,EAAI,OAAO;KAC1B,WAAW,uBAAuB,EAAI,OAAO,IAAY,gCAAgC;KACzF,eAAe;MAEb,AADA,EAAe,EAAI,EAAE,GACrB,EAAkB,EAAK;KACzB;KAEC,UAAA,EAAI;IACC,GAVD,EAAI,EAUH,CACT;GACE,CAAA,CAEJ;EAEJ,CAAA,GAAA,KACC,kBAAC,OAAD;GACE,IAAI,YAAY;GAChB,MAAK;GACL,mBAAiB,OAAO;GACxB,WAAW,eAAe,IAAiB,KAAK;GAChD,UAAU;GACV,cAAc;GACd,aAAa;GACb,YAAY;GAEX,UAAA;EACE,CAAA,CAEJ;MASP,kBAAC,OAAD;EACE,WAAW,QAJG,MAAiB,SAAS,wBAAwB,GAInC,GA1F/B,MAAY,cACR,4BACA,MAAY,YACV,0BACA,GAsFyC,GApF/C,KAAqB,KAAgB,yBAAyB,GAoFO,GAAG,IACnE,QAAQ,QAAQ,GAAG,CAAC,CACpB,KAAK;EAHV,UAAA,CAKE,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IAEG,KAAwB,MACvB,kBAAC,UAAD;KACE,WAAU;KACV,eAAe;MACb,EAAW,MAAM;KACnB;KACA,cAAY;KACZ,UAAU;KAEV,UAAA,kBAAC,OAAD;MACE,OAAM;MACN,QAAO;MACP,SAAQ;MACR,MAAK;MACL,OAAM;MACN,eAAY;MAEZ,UAAA,kBAAC,QAAD;OACE,GAAE;OACF,QAAO;OACP,aAAY;OACZ,eAAc;OACd,gBAAe;MAChB,CAAA;KACE,CAAA;IACC,CAAA;IAGV,kBAAC,OAAD;KACE,KAAK;KACL,WAAU;KACV,MAAK;KACL,cAAY;KACZ,UAAU;KACV,YAAY,MAAM;MAMhB,IAAM,IAAe,EAAK,WAAW,MAAQ,EAAI,OAAO,CAAS;MAC7D,IAAe,KACnB,EAAc,GAAG,CAAY;KAC/B;KACA,cAAc,MAAiB,aAAa,IAAmB,KAAA;KAC/D,aAAa,MAAiB,aAAa,IAAkB,KAAA;KAC7D,YAAY,MAAiB,aAAa,IAAiB,KAAA;KAE1D,UAAA,GAAqB;IACnB,CAAA;IAGJ,KAAwB,MACvB,kBAAC,UAAD;KACE,WAAU;KACV,eAAe;MACb,EAAW,OAAO;KACpB;KACA,cAAY;KACZ,UAAU;KAEV,UAAA,kBAAC,OAAD;MACE,OAAM;MACN,QAAO;MACP,SAAQ;MACR,MAAK;MACL,OAAM;MACN,eAAY;MAEZ,UAAA,kBAAC,QAAD;OACE,GAAE;OACF,QAAO;OACP,aAAY;OACZ,eAAc;OACd,gBAAe;MAChB,CAAA;KACE,CAAA;IACC,CAAA;IAIT,GAAmB;GACjB;EAEJ,CAAA,GAAA,KACC,kBAAC,OAAD;GACE,IAAI,YAAY;GAChB,MAAK;GACL,mBAAiB,OAAO;GACxB,WAAW,eAAe,IAAiB,KAAK;GAChD,UAAU;GAET,UAAA;EACE,CAAA,CAEJ;;AAET"}
1
+ {"version":3,"file":"Tabs2.js","names":[],"sources":["../../../src/components/Tabs/Tabs.tsx"],"sourcesContent":["/**\n * Tabs Component\n *\n * Unified tab navigation component with optional grouping, guide tag badges,\n * and responsive overflow handling. Follows WCAG 2.1 ARIA tab pattern.\n *\n * Features:\n * - Scroll arrows for overflow\n * - Mobile dropdown mode (overflowMode=\"dropdown\", < 480px)\n * - Overflow menu mode (overflowMode=\"menu\", < 768px)\n * - Tab grouping with separators and labels\n * - Guide tag badge support (required, recommended, beta, experimental)\n * - Touch swipe navigation (dropdown mode)\n * - Auto-scroll to active tab\n * - Full keyboard navigation (Arrow keys, Home, End)\n */\n\nimport {\n useState,\n useCallback,\n useRef,\n useEffect,\n useMemo,\n type ReactNode,\n type KeyboardEvent,\n type TouchEvent,\n} from \"react\";\nimport \"./Tabs.css\";\n\nexport interface TabItem {\n id: string;\n label: string;\n content: ReactNode;\n /**\n * Trailing node rendered next to the tab label: a count pill, a status\n * badge, whatever the consumer's vocabulary calls for.\n *\n * This used to sit beside a `tag` prop that took one of four fixed names,\n * required / recommended / beta / experimental, and rendered a Badge for\n * it. Those are one product's guide vocabulary, and the arrangement had\n * already split across the boundary: the badge variant lived here while the\n * label text had to be passed in from the consumer's locale bundle. A\n * consumer that wants that badge renders it here, and owns both halves.\n */\n labelExtra?: ReactNode;\n /**\n * Optional leading node rendered before the tab label (e.g. an icon).\n * Useful when migrating segmented controls that prefix the label with\n * an SVG glyph (e.g. ImportExportDialog source toggle: File / Paste / URL).\n */\n labelPrefix?: ReactNode;\n /** Assign this tab to a group (used with the `groups` prop) */\n groupId?: string;\n}\n\nexport interface TabGroupMeta {\n id: string;\n label: string;\n}\n\nexport type TabOverflowMode = \"dropdown\" | \"menu\";\n\n/**\n * Visual variant of the tab bar.\n *\n * - `\"underlined\"` (default): pill container with raised active tab and scroll\n * arrows; used for primary page-level navigation that may overflow.\n * - `\"segmented\"`: gray-background segmented control with raised active tab;\n * intended for short, fixed-arity in-page mode toggles.\n * - `\"compact\"`: smaller segmented control with reduced padding; used where\n * the tab bar shares a row with other controls (e.g. StatisticsPage).\n */\nexport type TabVariant = \"underlined\" | \"segmented\" | \"compact\";\n\nexport interface TabsProps {\n tabs: TabItem[];\n /** Group definitions for organizing tabs with separators and labels */\n groups?: TabGroupMeta[];\n defaultTab?: string;\n activeTab?: string;\n onTabChange?: (tabId: string) => void;\n className?: string;\n panelClassName?: string;\n ariaLabel?: string;\n /** Show scroll arrows and overflow controls (default: true) */\n showOverflowControls?: boolean;\n /** Show group labels in the tab bar and overflow menu (default: true) */\n showGroupLabels?: boolean;\n /**\n * Overflow strategy for small screens:\n * - \"dropdown\": mobile dropdown trigger (< 480px) — default when no groups\n * - \"menu\": overflow menu button (< 768px) — default when groups are provided\n */\n overflowMode?: TabOverflowMode;\n /**\n * Visual variant. Defaults to `\"underlined\"` (the original Tabs look).\n * Use `\"segmented\"` for iOS-style pill toggles, `\"compact\"` for a smaller\n * variant suitable for in-header use alongside other controls.\n */\n variant?: TabVariant;\n /**\n * When using `variant=\"segmented\"` or `\"compact\"`, make each tab fill the\n * container evenly (flex: 1 per tab). Useful for short, equal-arity mode\n * toggles like the Models page tab bar. Defaults to false.\n */\n fillContainer?: boolean;\n /**\n * Render the tab panel (the active tab's `content`) below the tab list.\n * Defaults to true. Set to false when the consumer manages its own panel\n * rendering outside the Tabs component (e.g. pages that interleave panels\n * with other in-header elements).\n *\n * When false, each `TabItem.content` is ignored — the consumer is expected\n * to inspect `activeTab` / `onTabChange` and render its own panel.\n */\n renderPanel?: boolean;\n /** Accessible label for the overflow-menu trigger button. Default: \"More tabs\" */\n moreTabsLabel?: string;\n /** Accessible label for the mobile dropdown trigger. Default: \"Select tab\" */\n selectTabLabel?: string;\n /** Accessible label for the left scroll-arrow button. Default: \"Scroll left\" */\n scrollLeftLabel?: string;\n /** Accessible label for the right scroll-arrow button. Default: \"Scroll right\" */\n scrollRightLabel?: string;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function Tabs({\n tabs,\n groups = [],\n defaultTab,\n activeTab: controlledTab,\n onTabChange,\n className = \"\",\n panelClassName = \"\",\n ariaLabel,\n showOverflowControls = true,\n showGroupLabels = true,\n overflowMode: explicitOverflowMode,\n variant = \"underlined\",\n fillContainer = false,\n renderPanel = true,\n moreTabsLabel = \"More tabs\",\n selectTabLabel = \"Select tab\",\n scrollLeftLabel = \"Scroll left\",\n scrollRightLabel = \"Scroll right\",\n}: TabsProps) {\n // Auto-detect overflow mode from groups presence\n const overflowMode =\n explicitOverflowMode ?? (groups.length > 0 ? \"menu\" : \"dropdown\");\n\n // ---- State ----\n const [internalActiveTab, setInternalActiveTab] = useState(\n defaultTab || tabs[0]?.id || \"\",\n );\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n\n // Dropdown mode state (overflowMode === \"dropdown\")\n const [isMobileDropdown, setIsMobileDropdown] = useState(false);\n const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n\n // Overflow menu state (overflowMode === \"menu\")\n const [showOverflowMenu, setShowOverflowMenu] = useState(false);\n const [focusedMenuIndex, setFocusedMenuIndex] = useState(-1);\n\n // ---- Refs ----\n const tabListRef = useRef<HTMLDivElement>(null);\n const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map());\n const dropdownContainerRef = useRef<HTMLDivElement>(null);\n const overflowMenuRef = useRef<HTMLDivElement>(null);\n const overflowButtonRef = useRef<HTMLButtonElement>(null);\n const menuItemRefs = useRef<HTMLButtonElement[]>([]);\n const scrollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const touchStartX = useRef<number>(0);\n const touchEndX = useRef<number>(0);\n\n // ---- Derived state ----\n const activeTab = controlledTab ?? internalActiveTab;\n\n // Group tabs by groupId\n const groupedTabs = useMemo(() => {\n return groups.length > 0\n ? groups.map((group) => ({\n group,\n tabs: tabs.filter((tab) => tab.groupId === group.id),\n }))\n : [{ group: null as TabGroupMeta | null, tabs }];\n }, [groups, tabs]);\n\n // Flatten menu items for keyboard navigation\n const flattenedMenuTabs = useMemo(() => {\n return groupedTabs.flatMap(({ tabs: groupTabs }) => groupTabs);\n }, [groupedTabs]);\n\n // ---- Handlers ----\n\n const handleTabClick = useCallback(\n (tabId: string) => {\n if (!controlledTab) {\n setInternalActiveTab(tabId);\n }\n onTabChange?.(tabId);\n setShowOverflowMenu(false);\n },\n [controlledTab, onTabChange],\n );\n\n const handleKeyDown = useCallback(\n (\n e: KeyboardEvent<HTMLButtonElement> | KeyboardEvent<HTMLDivElement>,\n currentIndex: number,\n ) => {\n let targetIndex: number;\n\n switch (e.key) {\n case \"ArrowRight\":\n e.preventDefault();\n targetIndex = currentIndex === tabs.length - 1 ? 0 : currentIndex + 1;\n break;\n case \"ArrowLeft\":\n e.preventDefault();\n targetIndex = currentIndex === 0 ? tabs.length - 1 : currentIndex - 1;\n break;\n case \"Home\":\n e.preventDefault();\n targetIndex = 0;\n break;\n case \"End\":\n e.preventDefault();\n targetIndex = tabs.length - 1;\n break;\n default:\n return;\n }\n\n const targetTab = tabs[targetIndex];\n if (targetTab) {\n handleTabClick(targetTab.id);\n tabRefs.current.get(targetTab.id)?.focus();\n }\n },\n [tabs, handleTabClick],\n );\n\n const setTabRef = useCallback((id: string, element: HTMLButtonElement | null) => {\n if (element) {\n tabRefs.current.set(id, element);\n } else {\n tabRefs.current.delete(id);\n }\n }, []);\n\n // ---- Scroll controls ----\n\n const checkScroll = useCallback(() => {\n const container = tabListRef.current;\n if (!container || !showOverflowControls) {\n setCanScrollLeft(false);\n setCanScrollRight(false);\n return;\n }\n\n const { scrollLeft, scrollWidth, clientWidth } = container;\n setCanScrollLeft(scrollLeft > 0);\n setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);\n }, [showOverflowControls]);\n\n const scrollTabs = useCallback(\n (direction: \"left\" | \"right\") => {\n const container = tabListRef.current;\n if (!container) return;\n\n const scrollAmount = container.clientWidth * 0.75;\n container.scrollBy({\n left: direction === \"left\" ? -scrollAmount : scrollAmount,\n behavior: \"smooth\",\n });\n\n if (scrollTimeoutRef.current !== null) {\n clearTimeout(scrollTimeoutRef.current);\n }\n scrollTimeoutRef.current = setTimeout(checkScroll, 300);\n },\n [checkScroll],\n );\n\n // ---- Auto-scroll to active tab ----\n\n const scrollToActiveTab = useCallback(() => {\n if (!tabListRef.current || isMobileDropdown) return;\n\n const activeButton = tabRefs.current.get(activeTab);\n if (!activeButton) return;\n\n const container = tabListRef.current;\n const buttonRect = activeButton.getBoundingClientRect();\n const containerRect = container.getBoundingClientRect();\n\n if (buttonRect.left < containerRect.left) {\n container.scrollLeft -= containerRect.left - buttonRect.left + 8;\n } else if (buttonRect.right > containerRect.right) {\n container.scrollLeft += buttonRect.right - containerRect.right + 8;\n }\n }, [activeTab, isMobileDropdown]);\n\n // ---- Mobile dropdown mode ----\n\n const checkMobileMode = useCallback(() => {\n if (typeof window === \"undefined\" || overflowMode !== \"dropdown\") return;\n setIsMobileDropdown(window.innerWidth < 480);\n }, [overflowMode]);\n\n const toggleDropdown = useCallback(() => {\n setIsDropdownOpen((prev) => !prev);\n }, []);\n\n const handleDropdownKeyDown = useCallback(\n (e: KeyboardEvent<HTMLButtonElement>) => {\n if (e.key === \"Escape\" && isDropdownOpen) {\n e.preventDefault();\n setIsDropdownOpen(false);\n }\n },\n [isDropdownOpen],\n );\n\n // ---- Touch swipe (dropdown mode) ----\n\n const handleTouchStart = useCallback((e: TouchEvent<HTMLDivElement>) => {\n if (e.touches[0]) {\n touchStartX.current = e.touches[0].clientX;\n }\n }, []);\n\n const handleTouchMove = useCallback((e: TouchEvent<HTMLDivElement>) => {\n if (e.touches[0]) {\n touchEndX.current = e.touches[0].clientX;\n }\n }, []);\n\n const handleTouchEnd = useCallback(() => {\n if (!touchStartX.current || !touchEndX.current) return;\n\n const diff = touchStartX.current - touchEndX.current;\n const threshold = 50;\n\n if (Math.abs(diff) > threshold) {\n const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);\n const nextTab = tabs[currentIndex + 1];\n const prevTab = tabs[currentIndex - 1];\n\n if (diff > 0 && nextTab) {\n handleTabClick(nextTab.id);\n } else if (diff < 0 && prevTab) {\n handleTabClick(prevTab.id);\n }\n }\n\n touchStartX.current = 0;\n touchEndX.current = 0;\n }, [activeTab, tabs, handleTabClick]);\n\n // ---- Overflow menu (menu mode) ----\n\n const handleOverflowMenuKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n const menuItemCount = flattenedMenuTabs.length;\n\n switch (e.key) {\n case \"Escape\":\n e.preventDefault();\n setShowOverflowMenu(false);\n setFocusedMenuIndex(-1);\n overflowButtonRef.current?.focus();\n break;\n case \"ArrowDown\":\n e.preventDefault();\n setFocusedMenuIndex((prev) => {\n const next = prev >= menuItemCount - 1 ? 0 : prev + 1;\n menuItemRefs.current[next]?.focus();\n return next;\n });\n break;\n case \"ArrowUp\":\n e.preventDefault();\n setFocusedMenuIndex((prev) => {\n const next = prev <= 0 ? menuItemCount - 1 : prev - 1;\n menuItemRefs.current[next]?.focus();\n return next;\n });\n break;\n case \"Home\":\n e.preventDefault();\n setFocusedMenuIndex(0);\n menuItemRefs.current[0]?.focus();\n break;\n case \"End\":\n e.preventDefault();\n setFocusedMenuIndex(menuItemCount - 1);\n menuItemRefs.current[menuItemCount - 1]?.focus();\n break;\n default:\n break;\n }\n },\n [flattenedMenuTabs.length],\n );\n\n const handleOverflowButtonKeyDown = useCallback(\n (e: KeyboardEvent<HTMLButtonElement>) => {\n if (e.key === \"Escape\" && showOverflowMenu) {\n e.preventDefault();\n setShowOverflowMenu(false);\n setFocusedMenuIndex(-1);\n }\n },\n [showOverflowMenu],\n );\n\n // ---- Effects ----\n\n // Close dropdown when clicking outside\n useEffect(() => {\n if (!isDropdownOpen) return;\n\n const handleClickOutside = (event: MouseEvent) => {\n if (\n dropdownContainerRef.current &&\n !dropdownContainerRef.current.contains(event.target as Node)\n ) {\n setIsDropdownOpen(false);\n }\n };\n\n document.addEventListener(\"mousedown\", handleClickOutside);\n return () => {\n document.removeEventListener(\"mousedown\", handleClickOutside);\n };\n }, [isDropdownOpen]);\n\n // Close overflow menu when clicking outside\n useEffect(() => {\n if (!showOverflowMenu) return undefined;\n\n const handleClickOutside = (event: MouseEvent) => {\n if (\n overflowMenuRef.current &&\n !overflowMenuRef.current.contains(event.target as Node)\n ) {\n setShowOverflowMenu(false);\n setFocusedMenuIndex(-1);\n }\n };\n\n const handleEscapeKey = (event: globalThis.KeyboardEvent) => {\n if (event.key === \"Escape\") {\n setShowOverflowMenu(false);\n setFocusedMenuIndex(-1);\n overflowButtonRef.current?.focus();\n }\n };\n\n document.addEventListener(\"mousedown\", handleClickOutside);\n document.addEventListener(\"keydown\", handleEscapeKey);\n return () => {\n document.removeEventListener(\"mousedown\", handleClickOutside);\n document.removeEventListener(\"keydown\", handleEscapeKey);\n };\n }, [showOverflowMenu]);\n\n // Focus menu item when overflow menu opens\n useEffect(() => {\n if (showOverflowMenu && menuItemRefs.current.length > 0) {\n const activeIndex = flattenedMenuTabs.findIndex((tab) => tab.id === activeTab);\n const indexToFocus = activeIndex >= 0 ? activeIndex : 0;\n setFocusedMenuIndex(indexToFocus);\n requestAnimationFrame(() => {\n menuItemRefs.current[indexToFocus]?.focus();\n });\n }\n }, [showOverflowMenu, flattenedMenuTabs, activeTab]);\n\n // Ensure active tab is valid\n useEffect(() => {\n if (!tabs.find((tab) => tab.id === activeTab)) {\n const firstTab = tabs[0];\n if (firstTab) {\n handleTabClick(firstTab.id);\n }\n }\n }, [tabs, activeTab, handleTabClick]);\n\n // Check mobile mode and scroll on mount/resize\n useEffect(() => {\n checkMobileMode();\n checkScroll();\n\n const handleResize = () => {\n checkMobileMode();\n checkScroll();\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => {\n window.removeEventListener(\"resize\", handleResize);\n };\n }, [checkMobileMode, checkScroll]);\n\n // Scroll to active tab when it changes\n useEffect(() => {\n scrollToActiveTab();\n }, [activeTab, scrollToActiveTab]);\n\n // Check scroll when tabs change\n useEffect(() => {\n checkScroll();\n }, [tabs, checkScroll]);\n\n // Cleanup scroll timeout on unmount\n useEffect(() => {\n return () => {\n if (scrollTimeoutRef.current !== null) {\n clearTimeout(scrollTimeoutRef.current);\n }\n };\n }, []);\n\n // ---- Render helpers ----\n\n const activeTabContent = tabs.find((tab) => tab.id === activeTab)?.content;\n const activeTabLabel = tabs.find((tab) => tab.id === activeTab)?.label;\n\n const hasGroups = groups.length > 0;\n\n // Reset menu item refs array each render\n menuItemRefs.current = [];\n\n /** Render a single tab button */\n const renderTabButton = (tab: TabItem, globalIndex: number) => {\n const isActive = tab.id === activeTab;\n\n return (\n <button\n key={tab.id}\n ref={(el) => {\n setTabRef(tab.id, el);\n }}\n role=\"tab\"\n aria-selected={isActive}\n aria-controls={renderPanel ? `tabpanel-${tab.id}` : undefined}\n id={`tab-${tab.id}`}\n tabIndex={isActive ? 0 : -1}\n className={`tabs__tab${isActive ? \" tabs__tab--active\" : \"\"}`}\n onClick={() => {\n handleTabClick(tab.id);\n }}\n onKeyDown={(e) => {\n handleKeyDown(e, globalIndex);\n // Prevent the tablist's onKeyDown from also firing (it runs the\n // same Arrow/Home/End logic for synthetic tests / unusual focus\n // setups where focus is on the tablist itself).\n if (\n e.key === \"ArrowLeft\" ||\n e.key === \"ArrowRight\" ||\n e.key === \"Home\" ||\n e.key === \"End\"\n ) {\n e.stopPropagation();\n }\n }}\n >\n {tab.labelPrefix}\n <span className=\"tabs__tab-label\">{tab.label}</span>\n {tab.labelExtra}\n </button>\n );\n };\n\n /** Render the tab list content (flat or grouped) */\n const renderTabListContent = () => {\n if (hasGroups) {\n return groupedTabs.map(({ group, tabs: groupTabs }, groupIndex) => (\n <div key={group?.id || \"default\"} className=\"tabs__group\">\n {group && (\n <>\n {groupIndex > 0 && (\n <div className=\"tabs__separator\" role=\"separator\" aria-hidden=\"true\" />\n )}\n {showGroupLabels && (\n <span className=\"tabs__group-label\">{group.label}</span>\n )}\n </>\n )}\n <div className=\"tabs__group-tabs\">\n {groupTabs.map((tab) => {\n const globalIndex = tabs.findIndex((t) => t.id === tab.id);\n return renderTabButton(tab, globalIndex);\n })}\n </div>\n </div>\n ));\n }\n\n return tabs.map((tab, index) => renderTabButton(tab, index));\n };\n\n /** Render the overflow menu (menu mode) */\n const renderOverflowMenu = () => {\n if (overflowMode !== \"menu\" || !showOverflowControls) return null;\n\n return (\n <div className=\"tabs__overflow\" ref={overflowMenuRef}>\n <button\n ref={overflowButtonRef}\n type=\"button\"\n className=\"tabs__overflow-btn\"\n onClick={() => {\n setShowOverflowMenu(!showOverflowMenu);\n }}\n onKeyDown={handleOverflowButtonKeyDown}\n aria-label={moreTabsLabel}\n aria-expanded={showOverflowMenu}\n aria-haspopup=\"menu\"\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"1\" />\n <circle cx=\"12\" cy=\"5\" r=\"1\" />\n <circle cx=\"12\" cy=\"19\" r=\"1\" />\n </svg>\n </button>\n\n {showOverflowMenu && (\n <div\n className=\"tabs__overflow-menu\"\n role=\"menu\"\n onKeyDown={handleOverflowMenuKeyDown}\n >\n {groupedTabs.map(({ group, tabs: groupTabs }) => (\n <div key={group?.id || \"default\"} className=\"tabs__overflow-group\">\n {group && showGroupLabels && (\n <div className=\"tabs__overflow-group-label\">{group.label}</div>\n )}\n {groupTabs.map((tab) => {\n const isActive = tab.id === activeTab;\n const flatIndex = flattenedMenuTabs.findIndex((t) => t.id === tab.id);\n return (\n <button\n key={tab.id}\n ref={(el) => {\n if (el) {\n menuItemRefs.current[flatIndex] = el;\n }\n }}\n role=\"menuitem\"\n tabIndex={focusedMenuIndex === flatIndex ? 0 : -1}\n className={`tabs__overflow-item ${isActive ? \"tabs__overflow-item--active\" : \"\"}`}\n onClick={() => {\n handleTabClick(tab.id);\n }}\n >\n {tab.labelPrefix}\n <span>{tab.label}</span>\n {tab.labelExtra}\n </button>\n );\n })}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n };\n\n // ---- Variant class helpers ----\n\n // For segmented/compact variants we always want a single horizontal pill —\n // there is no scrollable arrow affordance and the mobile dropdown trigger\n // would visually conflict with the segmented look. Underlined remains the\n // only variant that follows the historical \"scroll + dropdown\" behavior.\n const isSegmentedFamily = variant === \"segmented\" || variant === \"compact\";\n const variantClass =\n variant === \"segmented\"\n ? \"tabs--variant-segmented\"\n : variant === \"compact\"\n ? \"tabs--variant-compact\"\n : \"\";\n const fillContainerClass =\n isSegmentedFamily && fillContainer ? \"tabs--fill-container\" : \"\";\n\n // ---- Mobile dropdown render (overflowMode === \"dropdown\") ----\n\n // Segmented/compact variants always render as a single inline tab bar — we\n // skip the mobile-dropdown branch even when the viewport is narrow so the\n // visual matches the existing per-page \"segmented control\" markup the\n // migration replaces.\n if (overflowMode === \"dropdown\" && isMobileDropdown && !isSegmentedFamily) {\n return (\n <div className={`tabs tabs--mobile-dropdown ${className}`}>\n <div className=\"tabs__dropdown-container\" ref={dropdownContainerRef}>\n <button\n className=\"tabs__dropdown-trigger\"\n onClick={toggleDropdown}\n onKeyDown={handleDropdownKeyDown}\n aria-expanded={isDropdownOpen}\n aria-haspopup=\"listbox\"\n aria-label={ariaLabel || selectTabLabel}\n >\n <span className=\"tabs__dropdown-label\">{activeTabLabel}</span>\n <svg\n className={`tabs__dropdown-arrow ${isDropdownOpen ? \"tabs__dropdown-arrow--open\" : \"\"}`}\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 6L8 10L12 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n\n {isDropdownOpen && (\n <div className=\"tabs__dropdown-menu\" role=\"listbox\">\n {tabs.map((tab) => (\n <button\n key={tab.id}\n role=\"option\"\n aria-selected={tab.id === activeTab}\n className={`tabs__dropdown-item ${tab.id === activeTab ? \"tabs__dropdown-item--active\" : \"\"}`}\n onClick={() => {\n handleTabClick(tab.id);\n setIsDropdownOpen(false);\n }}\n >\n {tab.label}\n </button>\n ))}\n </div>\n )}\n </div>\n\n {renderPanel && (\n <div\n id={`tabpanel-${activeTab}`}\n role=\"tabpanel\"\n aria-labelledby={`tab-${activeTab}`}\n className={`tabs__panel ${panelClassName}`.trim()}\n tabIndex={0}\n onTouchStart={handleTouchStart}\n onTouchMove={handleTouchMove}\n onTouchEnd={handleTouchEnd}\n >\n {activeTabContent}\n </div>\n )}\n </div>\n );\n }\n\n // ---- Desktop/tablet render ----\n\n const modeClass = overflowMode === \"menu\" ? \"tabs--overflow-menu\" : \"\";\n\n return (\n <div\n className={`tabs ${modeClass} ${variantClass} ${fillContainerClass} ${className}`\n .replace(/\\s+/g, \" \")\n .trim()}\n >\n <div className=\"tabs__container\">\n {/* Scroll left arrow */}\n {showOverflowControls && canScrollLeft && (\n <button\n className=\"tabs__scroll-arrow tabs__scroll-arrow--left\"\n onClick={() => {\n scrollTabs(\"left\");\n }}\n aria-label={scrollLeftLabel}\n tabIndex={-1}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 15L7 10L12 5\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n )}\n\n <div\n ref={tabListRef}\n className=\"tabs__list\"\n role=\"tablist\"\n aria-label={ariaLabel}\n onScroll={checkScroll}\n onKeyDown={(e) => {\n // Mirror the per-button handler at the tablist level so synthetic\n // tests (and unusual focus setups where the tablist itself owns\n // the keyboard focus) get the same Arrow/Home/End behavior. Tab\n // buttons are still the primary handler; this only fires when\n // the event reaches the tablist without being handled by a tab.\n const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);\n if (currentIndex < 0) return;\n handleKeyDown(e, currentIndex);\n }}\n onTouchStart={overflowMode === \"dropdown\" ? handleTouchStart : undefined}\n onTouchMove={overflowMode === \"dropdown\" ? handleTouchMove : undefined}\n onTouchEnd={overflowMode === \"dropdown\" ? handleTouchEnd : undefined}\n >\n {renderTabListContent()}\n </div>\n\n {/* Scroll right arrow */}\n {showOverflowControls && canScrollRight && (\n <button\n className=\"tabs__scroll-arrow tabs__scroll-arrow--right\"\n onClick={() => {\n scrollTabs(\"right\");\n }}\n aria-label={scrollRightLabel}\n tabIndex={-1}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M8 5L13 10L8 15\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n )}\n\n {/* Overflow menu (menu mode) */}\n {renderOverflowMenu()}\n </div>\n\n {renderPanel && (\n <div\n id={`tabpanel-${activeTab}`}\n role=\"tabpanel\"\n aria-labelledby={`tab-${activeTab}`}\n className={`tabs__panel ${panelClassName}`.trim()}\n tabIndex={0}\n >\n {activeTabContent}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- export { Tabs, TAG_CONFIG } from './Tabs';
2
- export type { TabsProps, TabItem, TabGroupMeta, TabOverflowMode, TabTagType, TabTagLabels, TabVariant, } from './Tabs';
1
+ export { Tabs } from './Tabs';
2
+ export type { TabsProps, TabItem, TabGroupMeta, TabOverflowMode, TabVariant, } from './Tabs';
@@ -1,3 +1,2 @@
1
- import { TAG_CONFIG as e } from "./tabTags.js";
2
- import { Tabs as t } from "./Tabs2.js";
3
- export { e as TAG_CONFIG, t as Tabs };
1
+ import { Tabs as e } from "./Tabs2.js";
2
+ export { e as Tabs };
package/dist/index.d.ts CHANGED
@@ -6,9 +6,11 @@
6
6
  * `@lablup/ui-common/components/<Name>` when you want the smallest
7
7
  * possible graph (see the component subpath exports in package.json).
8
8
  *
9
- * Styling is opt-in and lives in a separate entry point:
10
- * `@lablup/ui-common/styles/base.css` (required) plus at most one
11
- * `@lablup/ui-common/styles/themes/<theme>.css` (optional).
9
+ * Component CSS travels with the component. What stays opt-in is the palette:
10
+ * `@lablup/ui-common/styles/base.css` (required, and the default theme on its
11
+ * own) plus `styles/themes/orange-dark.css` if you switch through
12
+ * `[data-theme]`. A product with its own identity ships its own themes over
13
+ * the same token names.
12
14
  */
13
15
  export * from './components/BaseCard';
14
16
  export * from './components/Badge';
package/dist/index.js CHANGED
@@ -18,7 +18,6 @@ import { usePrefersReducedMotion as g } from "./hooks/usePrefersReducedMotion.js
18
18
  import { StatCard as _ } from "./components/StatCard/StatCard2.js";
19
19
  import { formatCompactNumber as v } from "./components/StatCard/formatters.js";
20
20
  import { StatusTag as y } from "./components/StatusTag/StatusTag2.js";
21
- import { TAG_CONFIG as b } from "./components/Tabs/tabTags.js";
22
- import { Tabs as x } from "./components/Tabs/Tabs2.js";
23
- import { Tooltip as S } from "./components/Tooltip/Tooltip2.js";
24
- export { t as Badge, e as BaseCard, n as Button, r as DataTable, i as Drawer, a as EmptyState, o as ErrorState, s as PageHeader, c as PageLayout, l as ProgressBar, u as Skeleton, d as SkeletonCard, p as SkeletonChart, m as SkeletonRow, f as SkeletonText, h as SmoothHeight, _ as StatCard, y as StatusTag, b as TAG_CONFIG, x as Tabs, S as Tooltip, v as formatCompactNumber, g as usePrefersReducedMotion };
21
+ import { Tabs as b } from "./components/Tabs/Tabs2.js";
22
+ import { Tooltip as x } from "./components/Tooltip/Tooltip2.js";
23
+ export { t as Badge, e as BaseCard, n as Button, r as DataTable, i as Drawer, a as EmptyState, o as ErrorState, s as PageHeader, c as PageLayout, l as ProgressBar, u as Skeleton, d as SkeletonCard, p as SkeletonChart, m as SkeletonRow, f as SkeletonText, h as SmoothHeight, _ as StatCard, y as StatusTag, b as Tabs, x as Tooltip, v as formatCompactNumber, g as usePrefersReducedMotion };
@@ -1,34 +1,59 @@
1
1
  /**
2
2
  * @lablup/ui-common design tokens
3
3
  *
4
- * The 113 `--token-*` custom properties every component in this package
4
+ * The 119 `--token-*` custom properties every component in this package
5
5
  * reads. This file is the token contract: renaming or removing an entry is
6
6
  * a breaking change, exactly like renaming a prop (see CONTRIBUTING.md).
7
7
  *
8
- * Values are the resolved defaults of the source product's default theme
9
- * (theme family "stained", color scheme "light"), taken from that product's
10
- * `src/themes/base.css` (structural tokens: spacing, radius, motion, type
11
- * scale, focus ring, z-index) and `src/themes/tokens/stained-light.css`
12
- * (color and button tokens). A handful of tokens that no source theme
13
- * ever defines (e.g. `--token-colorTextQuaternary`, `--token-marginXXS`)
14
- * are given the same literal fallback value the component CSS already
15
- * carries at its call site.
8
+ * It also carries one block of element rules, at the end: the scrollbar. A
9
+ * scrollbar is painted by the browser on a box this package does not own, so
10
+ * there is no component to hang the rules on and a consumer that wants a
11
+ * themed bar has nowhere to put one that every scrollable surface inherits.
12
+ * The rest of the file is custom properties only.
13
+ *
14
+ * Colour and button values are the Lablup brand default, theme family
15
+ * "orange", colour scheme "light", which is also the one theme this package
16
+ * ships as a stylesheet (`styles/themes/orange-light.css` and its dark
17
+ * counterpart). Structural tokens (spacing, radius, motion, type scale, focus
18
+ * ring, z-index) are theme-independent and were taken from the source
19
+ * product's `src/themes/base.css`. A handful of tokens that no theme ever
20
+ * defines (e.g. `--token-colorTextQuaternary`, `--token-marginXXS`) are given
21
+ * the same literal fallback value the component CSS already carries at its
22
+ * call site.
23
+ *
24
+ * A product with its own visual identity defines its own `[data-theme]`
25
+ * blocks over these 119 names and ships them itself, the way the source
26
+ * product does with its five families. This package carries the mechanism and
27
+ * one default, not a catalogue of somebody else's palettes.
16
28
  *
17
29
  * Every component also gives every token reference here its own inline
18
30
  * fallback, so a consumer that imports a component without this stylesheet
19
- * still gets a rendered (unthemed) result.
31
+ * still gets a rendered (unthemed) result. Focus indicators are the one
32
+ * exception, and deliberately so: see below.
20
33
  *
21
- * Those inline fallbacks are NOT all equal to the values above. Roughly 143
22
- * of them were inherited verbatim from the source product, where they predate
23
- * current theme and were already dead code (the token is always defined, so
24
- * the fallback never applies). The most visible divergence is
25
- * `--token-colorPrimary`, whose call-site fallback is the older orange
26
- * `#ff7a00` rather than the current `#8b5cf6`.
34
+ * Those inline fallbacks are NOT all equal to the values above, and no single
35
+ * theme would make them agree. The largest group is `--token-colorPrimary`,
36
+ * written as `#8b5cf6` at 28 call sites, which is a `stained-light` accent
37
+ * this package does not ship and does not agree with the `#ff7a00` declared
38
+ * below. They were inherited verbatim from the source product, where they were
39
+ * already dead code, since the token is always defined and the fallback never
40
+ * applies.
27
41
  *
28
42
  * Consequence: importing a component WITHOUT this stylesheet renders, but not
29
43
  * in the documented default palette. Import `styles/base.css`. Aligning the
30
- * fallbacks is a deliberate visual change and was left out of the extraction,
31
- * which was required to preserve current rendering exactly.
44
+ * fallbacks is a deliberate visual change and is still out of scope.
45
+ *
46
+ * The exception, added for issue #7: a declaration that paints a focus
47
+ * indicator carries NO colour literal. It reads
48
+ * `var(--token-focusRingColor, var(--token-colorPrimary))`, or mixes the
49
+ * accent with the text colour inline, and stops there. A fallback literal
50
+ * cannot serve a focus ring, because WCAG 2.2 SC 1.4.11 requires 3:1 against
51
+ * the surface the indicator lands on and a fixed value cannot know that
52
+ * surface: the `#8b5cf6` these sites used to carry measures 3.85:1 in one
53
+ * palette and the `#ff7a00` below measures 2.37:1. "Renders unthemed" and
54
+ * "meets the contrast floor" are not both achievable from a constant, and for
55
+ * an accessibility affordance the second one wins. Everywhere else the rule in
56
+ * CONTRIBUTING.md stands unchanged: give the token a fallback.
32
57
  */
33
58
 
34
59
  :root {
@@ -36,6 +61,16 @@
36
61
  --token-borderRadiusLG: 0.5rem;
37
62
  --token-borderRadiusSM: 0.25rem;
38
63
  --token-borderRadiusXL: 0.75rem;
64
+ /* One height ladder for every form control, so a text field, a picker and a
65
+ button standing next to each other in a filter row land on the same top
66
+ and bottom edge. Only `Button` reads it today, at SM and LG; the middle
67
+ rung is declared with them because a ladder missing its default is not a
68
+ ladder, and the first control that needs a 40px box would otherwise
69
+ invent a literal, which is the defect this replaces. rem-based like the
70
+ rest of the scale, so controls grow with browser zoom (WCAG 1.4.4). */
71
+ --token-controlHeightSM: 2rem; /* 32px */
72
+ --token-controlHeight: 2.5rem; /* 40px */
73
+ --token-controlHeightLG: 2.75rem; /* 44px, the WCAG 2.1 touch target */
39
74
  --token-boxShadow:
40
75
  0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02),
41
76
  0 2px 4px 0 rgba(0, 0, 0, 0.02);
@@ -55,37 +90,29 @@
55
90
  --token-buttonGhostBackdrop: none;
56
91
  --token-buttonGhostBg: transparent;
57
92
  --token-buttonGhostBgActive: #f5f5f5;
58
- --token-buttonGhostBgHover: rgba(139, 92, 246, 0.06);
93
+ --token-buttonGhostBgHover: rgba(255, 122, 0, 0.06);
59
94
  --token-buttonGhostBorder: 1px solid transparent;
60
95
  --token-buttonGhostBorderHover: 1px solid transparent;
61
96
  --token-buttonGhostShadow: none;
62
97
  --token-buttonGhostShadowHover: none;
63
98
  --token-buttonPrimaryBackdrop: none;
64
- --token-buttonPrimaryBg:
65
- linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, transparent 50%), #8b5cf6;
66
- --token-buttonPrimaryBgHover:
67
- linear-gradient(180deg, rgba(255, 255, 255, 0.25) 0%, transparent 50%), #7c3aed;
99
+ --token-buttonPrimaryBg: var(--token-colorPrimary);
100
+ --token-buttonPrimaryBgHover: #e86e00;
68
101
  --token-buttonPrimaryBorder: none;
69
- --token-buttonPrimaryShadow:
70
- 0 2px 6px rgba(139, 92, 246, 0.25), inset 0 1px 1px rgba(255, 255, 255, 0.2);
71
- --token-buttonPrimaryShadowHover:
72
- 0 4px 10px rgba(139, 92, 246, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.25);
102
+ --token-buttonPrimaryShadow: 0 2px 4px rgba(255, 122, 0, 0.2);
103
+ --token-buttonPrimaryShadowHover: 0 4px 8px rgba(255, 122, 0, 0.25);
73
104
  --token-buttonSecondaryBackdrop: none;
74
- --token-buttonSecondaryBg: rgba(139, 92, 246, 0.04);
105
+ --token-buttonSecondaryBg: transparent;
75
106
  --token-buttonSecondaryBgActive: #f5f5f5;
76
- --token-buttonSecondaryBgHover: rgba(139, 92, 246, 0.08);
77
- --token-buttonSecondaryBorder: 1px solid rgba(139, 92, 246, 0.2);
78
- --token-buttonSecondaryBorderHover: 1px solid rgba(139, 92, 246, 0.3);
79
- --token-buttonSecondaryShadow:
80
- 0 1px 3px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.8);
107
+ --token-buttonSecondaryBgHover: rgba(255, 122, 0, 0.06);
108
+ --token-buttonSecondaryBorder: 1px solid var(--token-colorBorder);
109
+ --token-buttonSecondaryBorderHover: 1px solid rgba(255, 122, 0, 0.3);
110
+ --token-buttonSecondaryShadow: none;
81
111
  --token-buttonSuccessBackdrop: none;
82
- --token-buttonSuccessBg:
83
- linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, transparent 50%), #007a63;
84
- --token-buttonSuccessBgHover:
85
- linear-gradient(180deg, rgba(255, 255, 255, 0.25) 0%, transparent 50%), #006654;
112
+ --token-buttonSuccessBg: var(--token-colorSuccess);
113
+ --token-buttonSuccessBgHover: #006654;
86
114
  --token-buttonSuccessBorder: none;
87
- --token-buttonSuccessShadow:
88
- 0 2px 6px rgba(0, 189, 155, 0.25), inset 0 1px 1px rgba(255, 255, 255, 0.2);
115
+ --token-buttonSuccessShadow: 0 2px 4px rgba(0, 189, 155, 0.2);
89
116
  --token-colorBgContainer: #ffffff;
90
117
  --token-colorBgElevated: #ffffff;
91
118
  --token-colorBgLayout: #f5f5f5;
@@ -99,12 +126,12 @@
99
126
  --token-colorFillTertiary: #f9fafb;
100
127
  --token-colorError: #c82333;
101
128
  --token-colorInfo: #0066cc;
102
- --token-colorLink: #8b5cf6;
129
+ --token-colorLink: #ff7a00;
103
130
  --token-colorLinkHover: #7c3aed;
104
- --token-colorPrimary: #8b5cf6;
105
- --token-colorPrimaryActive: #6d28d9;
106
- --token-colorPrimaryBg: rgba(139, 92, 246, 0.1);
107
- --token-colorPrimaryBorder: rgba(139, 92, 246, 0.2);
131
+ --token-colorPrimary: #ff7a00;
132
+ --token-colorPrimaryActive: #cc6200;
133
+ --token-colorPrimaryBg: rgba(255, 122, 0, 0.1);
134
+ --token-colorPrimaryBorder: rgba(255, 122, 0, 0.2);
108
135
  --token-colorSuccess: #007a63;
109
136
  --token-colorSuccessActive: #005244;
110
137
  --token-colorSuccessBg: rgba(0, 189, 155, 0.05);
@@ -116,7 +143,16 @@
116
143
  --token-colorTextQuaternary: #bfbfbf;
117
144
  --token-colorTextSecondary: #595959;
118
145
  --token-colorTextTertiary: #737373;
119
- --token-focusRingColor: #8b5cf6;
146
+ /* Not --token-colorPrimary. That accent is chosen for brand and measures
147
+ * 2.37:1 against --token-colorFillSecondary here, under the 3:1 floor WCAG
148
+ * 2.2 SC 1.4.11 sets for the visual indicator of a component state. This is
149
+ * color-mix(in srgb, #ff7a00 70%, #141414) resolved to a literal, which
150
+ * measures 4.18:1 at its worst. It is written resolved rather than as a
151
+ * color-mix() call because a custom property holds an unparsed token stream:
152
+ * an unsupported color-mix() would substitute successfully and only then
153
+ * invalidate the outline shorthand at computed-value time, leaving
154
+ * outline-style: none and no ring at all, with no var() fallback firing. */
155
+ --token-focusRingColor: #b95b06;
120
156
  --token-focusRingOffset: 2px;
121
157
  --token-focusRingStyle: solid;
122
158
  --token-focusRingWidth: 2px;
@@ -127,6 +163,11 @@
127
163
  --token-fontSizeLG: 1rem;
128
164
  --token-fontSizeSM: 0.75rem;
129
165
  --token-fontSizeXL: 1.25rem;
166
+ /* The step above XL, for a value that is displayed rather than read as a
167
+ heading. The ladder had XXS but no XXL, so a component with a large
168
+ numeric readout had nothing to reach for and took a heading token
169
+ instead. Both products that define this name already declare 1.5rem. */
170
+ --token-fontSizeXXL: 1.5rem;
130
171
  --token-fontSizeXXS: 0.5rem;
131
172
  --token-marginXXS: 4px;
132
173
  --token-padding: 0.75rem;
@@ -136,14 +177,21 @@
136
177
  --token-paddingXL: 2rem;
137
178
  --token-paddingXS: 0.25rem;
138
179
  --token-paddingXXS: 0.125rem;
180
+ /* One size for every theme. A theme family may restyle the track and the
181
+ thumb, but must not restate the size: `[data-theme="..."]
182
+ ::-webkit-scrollbar` outranks the global rule on specificity, so a wider
183
+ bar declared there cannot be corrected from here, and a sidebar rail is
184
+ the surface that pays for it. `scrollbar.test.ts` keeps it that way. */
185
+ --token-scrollbarSize: 0.5rem; /* 8px */
186
+ --token-scrollbarRadius: 0.25rem; /* 4px */
139
187
  --token-motionDurationFast: 0.1s;
140
188
  --token-motionDurationMid: 0.2s;
141
189
  --token-motionDurationSlow: 0.3s;
142
190
  --token-themeTransitionDuration: 0.25s;
143
191
  --token-themeTransitionTiming: ease-in-out;
144
- --token-tabActiveBg: rgba(139, 92, 246, 0.1);
145
- --token-tabFocusShadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
146
- --token-tabHoverBg: #f3f4f6;
192
+ --token-tabActiveBg: rgba(255, 122, 0, 0.1);
193
+ --token-tabFocusShadow: 0 0 0 3px rgba(255, 122, 0, 0.15);
194
+ --token-tabHoverBg: var(--token-colorFillSecondary);
147
195
  --token-zIndexDrawer: 1100;
148
196
  --token-zIndexDrawerContent: 1101;
149
197
  --token-zIndexPopover: 1150;
@@ -167,3 +215,37 @@
167
215
  --token-buttonSecondaryShadowHover:
168
216
  0 2px 6px rgba(0, 0, 0, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.8);
169
217
  }
218
+
219
+ /* ============================================
220
+ Scrollbar
221
+
222
+ The only element rules in this file. Both halves are needed because the two
223
+ engines expose different surfaces, and neither one alone themes a bar.
224
+ ============================================ */
225
+
226
+ /* Firefox has no ::-webkit pseudo-elements. Both properties are inherited, so
227
+ declaring them once on the root themes every scrollable descendant, and a
228
+ surface that hides its own bar (see `Tabs.css`) still overrides them
229
+ locally. */
230
+ :root {
231
+ scrollbar-width: thin;
232
+ scrollbar-color: var(--token-colorBorder, #d9d9d9) transparent;
233
+ }
234
+
235
+ ::-webkit-scrollbar {
236
+ width: var(--token-scrollbarSize, 0.5rem);
237
+ height: var(--token-scrollbarSize, 0.5rem);
238
+ }
239
+
240
+ ::-webkit-scrollbar-track {
241
+ background: transparent;
242
+ }
243
+
244
+ ::-webkit-scrollbar-thumb {
245
+ background-color: var(--token-colorBorder, #d9d9d9);
246
+ border-radius: var(--token-scrollbarRadius, 0.25rem);
247
+ }
248
+
249
+ ::-webkit-scrollbar-thumb:hover {
250
+ background-color: var(--token-colorTextTertiary, #737373);
251
+ }
@@ -60,6 +60,9 @@
60
60
  --token-colorTextSecondary: #b3b3b3;
61
61
  --token-colorTextTertiary: #8c8c8c;
62
62
  --token-colorWarning: #d89614;
63
+ /* Left as the brand accent lightened for dark: it already measures 7.00:1 at
64
+ * its worst against this theme's six surfaces, so the 70% text mix its light
65
+ * counterpart needs would lower it to 6.42:1 for no benefit. */
63
66
  --token-focusRingColor: #ff9729;
64
67
  --token-focusRingOffset: 2px;
65
68
  --token-focusRingStyle: solid;
@@ -60,7 +60,12 @@
60
60
  --token-colorTextSecondary: #595959;
61
61
  --token-colorTextTertiary: #737373;
62
62
  --token-colorWarning: #9a5d00;
63
- --token-focusRingColor: #ff7a00;
63
+ /* color-mix(in srgb, #ff7a00 70%, #141414) resolved to a literal. The bare
64
+ * accent measures 2.37:1 against --token-colorFillSecondary, under the 3:1
65
+ * floor of WCAG 2.2 SC 1.4.11; this measures 4.18:1 at its worst. See the
66
+ * note on the same token in ../base.css for why it is resolved here rather
67
+ * than left as a color-mix() call. */
68
+ --token-focusRingColor: #b95b06;
64
69
  --token-focusRingOffset: 2px;
65
70
  --token-focusRingStyle: solid;
66
71
  --token-focusRingWidth: 2px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lablup/ui-common",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.10",
4
4
  "description": "Shared, product-neutral UI components and design tokens for Lablup products",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Lablup Inc.",
@@ -1,51 +0,0 @@
1
- /**
2
- * EmptyState Illustrations
3
- *
4
- * SVG illustration components for different empty state contexts.
5
- * All illustrations are inline React components for optimal performance.
6
- * Each component is memoized to prevent unnecessary re-renders.
7
- */
8
- interface IllustrationProps {
9
- className?: string;
10
- }
11
- /**
12
- * Chat illustration - Speech bubbles with sparkles
13
- */
14
- export declare const ChatIllustration: import('react').NamedExoticComponent<IllustrationProps>;
15
- /**
16
- * Models illustration - Package/download box
17
- */
18
- export declare const ModelsIllustration: import('react').NamedExoticComponent<IllustrationProps>;
19
- /**
20
- * Creations illustration - Gallery frame
21
- */
22
- export declare const CreationsIllustration: import('react').NamedExoticComponent<IllustrationProps>;
23
- /**
24
- * Benchmark illustration - Speedometer
25
- */
26
- export declare const BenchmarkIllustration: import('react').NamedExoticComponent<IllustrationProps>;
27
- /**
28
- * Logs illustration - Document stack
29
- */
30
- export declare const LogsIllustration: import('react').NamedExoticComponent<IllustrationProps>;
31
- /**
32
- * Statistics illustration - Chart placeholder
33
- */
34
- export declare const StatisticsIllustration: import('react').NamedExoticComponent<IllustrationProps>;
35
- /**
36
- * Error illustration - Warning/error state
37
- */
38
- export declare const ErrorIllustration: import('react').NamedExoticComponent<IllustrationProps>;
39
- /**
40
- * Text illustration - Document with bookmark
41
- */
42
- export declare const TextIllustration: import('react').NamedExoticComponent<IllustrationProps>;
43
- /**
44
- * Schedule illustration - Calendar with clock
45
- */
46
- export declare const ScheduleIllustration: import('react').NamedExoticComponent<IllustrationProps>;
47
- /**
48
- * Generic illustration - Placeholder icon
49
- */
50
- export declare const GenericIllustration: import('react').NamedExoticComponent<IllustrationProps>;
51
- export {};