@ds-mo/ui 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Accordion/Accordion.d.ts +29 -0
- package/dist/components/Accordion/Accordion.d.ts.map +1 -0
- package/dist/components/Accordion/index.d.ts +3 -0
- package/dist/components/Accordion/index.d.ts.map +1 -0
- package/dist/components/Badge/Badge.d.ts +18 -0
- package/dist/components/Badge/Badge.d.ts.map +1 -0
- package/dist/components/Badge/index.d.ts +3 -0
- package/dist/components/Badge/index.d.ts.map +1 -0
- package/dist/components/Banner/Banner.d.ts +16 -0
- package/dist/components/Banner/Banner.d.ts.map +1 -0
- package/dist/components/Banner/index.d.ts +3 -0
- package/dist/components/Banner/index.d.ts.map +1 -0
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +15 -0
- package/dist/components/Breadcrumb/Breadcrumb.d.ts.map +1 -0
- package/dist/components/Breadcrumb/index.d.ts +3 -0
- package/dist/components/Breadcrumb/index.d.ts.map +1 -0
- package/dist/components/Button/Button.d.ts +85 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.d.ts +26 -0
- package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -0
- package/dist/components/ButtonGroup/index.d.ts +3 -0
- package/dist/components/ButtonGroup/index.d.ts.map +1 -0
- package/dist/components/Card/Card.d.ts +20 -0
- package/dist/components/Card/Card.d.ts.map +1 -0
- package/dist/components/Card/index.d.ts +3 -0
- package/dist/components/Card/index.d.ts.map +1 -0
- package/dist/components/Checkbox/Checkbox.d.ts +18 -0
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/dist/components/Checkbox/index.d.ts +3 -0
- package/dist/components/Checkbox/index.d.ts.map +1 -0
- package/dist/components/Divider/Divider.d.ts +9 -0
- package/dist/components/Divider/Divider.d.ts.map +1 -0
- package/dist/components/Divider/index.d.ts +3 -0
- package/dist/components/Divider/index.d.ts.map +1 -0
- package/dist/components/EmptyState/EmptyState.d.ts +9 -0
- package/dist/components/EmptyState/EmptyState.d.ts.map +1 -0
- package/dist/components/EmptyState/index.d.ts +3 -0
- package/dist/components/EmptyState/index.d.ts.map +1 -0
- package/dist/components/ErrorBoundary/ErrorBoundary.d.ts +18 -0
- package/dist/components/ErrorBoundary/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary/index.d.ts +3 -0
- package/dist/components/ErrorBoundary/index.d.ts.map +1 -0
- package/dist/components/Fade/Fade.d.ts +11 -0
- package/dist/components/Fade/Fade.d.ts.map +1 -0
- package/dist/components/Fade/index.d.ts +3 -0
- package/dist/components/Fade/index.d.ts.map +1 -0
- package/dist/components/Field/Field.d.ts +9 -0
- package/dist/components/Field/Field.d.ts.map +1 -0
- package/dist/components/Field/index.d.ts +3 -0
- package/dist/components/Field/index.d.ts.map +1 -0
- package/dist/components/Header/Header.d.ts +12 -0
- package/dist/components/Header/Header.d.ts.map +1 -0
- package/dist/components/Header/index.d.ts +3 -0
- package/dist/components/Header/index.d.ts.map +1 -0
- package/dist/components/Input/Input.d.ts +27 -0
- package/dist/components/Input/Input.d.ts.map +1 -0
- package/dist/components/Input/index.d.ts +3 -0
- package/dist/components/Input/index.d.ts.map +1 -0
- package/dist/components/LabelWrap/LabelWrap.d.ts +27 -0
- package/dist/components/LabelWrap/LabelWrap.d.ts.map +1 -0
- package/dist/components/LabelWrap/index.d.ts +2 -0
- package/dist/components/LabelWrap/index.d.ts.map +1 -0
- package/dist/components/Loader/Loader.d.ts +33 -0
- package/dist/components/Loader/Loader.d.ts.map +1 -0
- package/dist/components/Loader/index.d.ts +3 -0
- package/dist/components/Loader/index.d.ts.map +1 -0
- package/dist/components/Menu/DestructiveMenuItem.d.ts +12 -0
- package/dist/components/Menu/DestructiveMenuItem.d.ts.map +1 -0
- package/dist/components/Menu/Menu.d.ts +55 -0
- package/dist/components/Menu/Menu.d.ts.map +1 -0
- package/dist/components/Menu/MenuItem.d.ts +33 -0
- package/dist/components/Menu/MenuItem.d.ts.map +1 -0
- package/dist/components/Menu/index.d.ts +7 -0
- package/dist/components/Menu/index.d.ts.map +1 -0
- package/dist/components/Modal/Modal.d.ts +15 -0
- package/dist/components/Modal/Modal.d.ts.map +1 -0
- package/dist/components/Modal/index.d.ts +3 -0
- package/dist/components/Modal/index.d.ts.map +1 -0
- package/dist/components/Pagination/Pagination.d.ts +16 -0
- package/dist/components/Pagination/Pagination.d.ts.map +1 -0
- package/dist/components/Pagination/index.d.ts +3 -0
- package/dist/components/Pagination/index.d.ts.map +1 -0
- package/dist/components/Radio/Radio.d.ts +35 -0
- package/dist/components/Radio/Radio.d.ts.map +1 -0
- package/dist/components/Radio/index.d.ts +3 -0
- package/dist/components/Radio/index.d.ts.map +1 -0
- package/dist/components/Scrollbar/Scrollbar.d.ts +13 -0
- package/dist/components/Scrollbar/Scrollbar.d.ts.map +1 -0
- package/dist/components/Scrollbar/index.d.ts +3 -0
- package/dist/components/Scrollbar/index.d.ts.map +1 -0
- package/dist/components/Select/Select.d.ts +22 -0
- package/dist/components/Select/Select.d.ts.map +1 -0
- package/dist/components/Select/index.d.ts +3 -0
- package/dist/components/Select/index.d.ts.map +1 -0
- package/dist/components/Sidebar/Sidebar.d.ts +15 -0
- package/dist/components/Sidebar/Sidebar.d.ts.map +1 -0
- package/dist/components/Sidebar/SidebarItem.d.ts +14 -0
- package/dist/components/Sidebar/SidebarItem.d.ts.map +1 -0
- package/dist/components/Sidebar/SidebarSection.d.ts +10 -0
- package/dist/components/Sidebar/SidebarSection.d.ts.map +1 -0
- package/dist/components/Sidebar/index.d.ts +7 -0
- package/dist/components/Sidebar/index.d.ts.map +1 -0
- package/dist/components/Skeleton/Skeleton.d.ts +17 -0
- package/dist/components/Skeleton/Skeleton.d.ts.map +1 -0
- package/dist/components/Skeleton/index.d.ts +3 -0
- package/dist/components/Skeleton/index.d.ts.map +1 -0
- package/dist/components/Slider/Slider.d.ts +16 -0
- package/dist/components/Slider/Slider.d.ts.map +1 -0
- package/dist/components/Slider/index.d.ts +3 -0
- package/dist/components/Slider/index.d.ts.map +1 -0
- package/dist/components/Surface/Surface.d.ts +59 -0
- package/dist/components/Surface/Surface.d.ts.map +1 -0
- package/dist/components/Surface/index.d.ts +3 -0
- package/dist/components/Surface/index.d.ts.map +1 -0
- package/dist/components/Tab/Tab.d.ts +21 -0
- package/dist/components/Tab/Tab.d.ts.map +1 -0
- package/dist/components/Tab/index.d.ts +3 -0
- package/dist/components/Tab/index.d.ts.map +1 -0
- package/dist/components/TabGroup/TabGroup.d.ts +26 -0
- package/dist/components/TabGroup/TabGroup.d.ts.map +1 -0
- package/dist/components/TabGroup/index.d.ts +3 -0
- package/dist/components/TabGroup/index.d.ts.map +1 -0
- package/dist/components/Table/Table.d.ts +46 -0
- package/dist/components/Table/Table.d.ts.map +1 -0
- package/dist/components/Table/index.d.ts +3 -0
- package/dist/components/Table/index.d.ts.map +1 -0
- package/dist/components/Tag/Tag.d.ts +38 -0
- package/dist/components/Tag/Tag.d.ts.map +1 -0
- package/dist/components/Tag/index.d.ts +3 -0
- package/dist/components/Tag/index.d.ts.map +1 -0
- package/dist/components/Text/Text.d.ts +41 -0
- package/dist/components/Text/Text.d.ts.map +1 -0
- package/dist/components/Text/index.d.ts +3 -0
- package/dist/components/Text/index.d.ts.map +1 -0
- package/dist/components/Toast/Toast.d.ts +33 -0
- package/dist/components/Toast/Toast.d.ts.map +1 -0
- package/dist/components/Toast/index.d.ts +3 -0
- package/dist/components/Toast/index.d.ts.map +1 -0
- package/dist/components/Toggle/Toggle.d.ts +17 -0
- package/dist/components/Toggle/Toggle.d.ts.map +1 -0
- package/dist/components/Toggle/index.d.ts +3 -0
- package/dist/components/Toggle/index.d.ts.map +1 -0
- package/dist/components/ToggleButton/ToggleButton.d.ts +35 -0
- package/dist/components/ToggleButton/ToggleButton.d.ts.map +1 -0
- package/dist/components/ToggleButton/index.d.ts +3 -0
- package/dist/components/ToggleButton/index.d.ts.map +1 -0
- package/dist/components/ToggleButtonGroup/ToggleButtonGroup.d.ts +24 -0
- package/dist/components/ToggleButtonGroup/ToggleButtonGroup.d.ts.map +1 -0
- package/dist/components/ToggleButtonGroup/index.d.ts +3 -0
- package/dist/components/ToggleButtonGroup/index.d.ts.map +1 -0
- package/dist/components/Tooltip/Tooltip.d.ts +17 -0
- package/dist/components/Tooltip/Tooltip.d.ts.map +1 -0
- package/dist/components/Tooltip/index.d.ts +3 -0
- package/dist/components/Tooltip/index.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +82 -969
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1641 -1165
- package/dist/index.js.map +1 -1
- package/dist/r/badge.json +1 -1
- package/dist/r/banner.json +2 -2
- package/dist/r/loader.json +2 -2
- package/dist/r/menu.json +4 -4
- package/dist/r/select.json +1 -1
- package/dist/r/tab-group.json +1 -1
- package/dist/r/tab.json +1 -1
- package/dist/r/toggle.json +1 -1
- package/dist/types/icons.d.ts +14 -0
- package/dist/types/icons.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/cn.d.ts +5 -0
- package/dist/utils/cn.d.ts.map +1 -0
- package/dist/utils/css-tokens.d.ts +11 -0
- package/dist/utils/css-tokens.d.ts.map +1 -0
- package/package.json +17 -12
- package/src/components/Accordion/Accordion.tsx +10 -1
- package/src/components/Badge/Badge.stories.tsx +32 -0
- package/src/components/Badge/Badge.tsx +19 -2
- package/src/components/Banner/Banner.module.css +38 -0
- package/src/components/Banner/Banner.tsx +34 -4
- package/src/components/Button/Button.stories.tsx +1 -1
- package/src/components/Button/Button.tsx +42 -11
- package/src/components/Checkbox/Checkbox.tsx +6 -3
- package/src/components/Field/Field.tsx +20 -7
- package/src/components/Input/Input.module.css +21 -0
- package/src/components/Input/Input.tsx +62 -21
- package/src/components/Loader/Loader.module.css +17 -0
- package/src/components/Loader/Loader.stories.tsx +9 -0
- package/src/components/Loader/Loader.tsx +42 -20
- package/src/components/Menu/DestructiveMenuItem.tsx +59 -6
- package/src/components/Menu/Menu.tsx +160 -11
- package/src/components/Menu/MenuItem.module.css +2 -0
- package/src/components/Menu/MenuItem.tsx +48 -5
- package/src/components/Modal/Modal.tsx +81 -4
- package/src/components/Radio/Radio.tsx +117 -29
- package/src/components/Scrollbar/Scrollbar.module.css +5 -0
- package/src/components/Scrollbar/Scrollbar.tsx +18 -4
- package/src/components/Select/Select.tsx +139 -9
- package/src/components/Sidebar/Sidebar.module.css +7 -0
- package/src/components/Sidebar/Sidebar.tsx +102 -4
- package/src/components/Slider/Slider.module.css +5 -1
- package/src/components/Slider/Slider.stories.tsx +25 -0
- package/src/components/Slider/Slider.tsx +4 -4
- package/src/components/Tab/Tab.tsx +32 -2
- package/src/components/TabGroup/TabGroup.stories.tsx +46 -15
- package/src/components/TabGroup/TabGroup.tsx +77 -10
- package/src/components/Table/Table.module.css +10 -0
- package/src/components/Table/Table.tsx +58 -29
- package/src/components/Tag/Tag.tsx +5 -2
- package/src/components/Toast/Toast.tsx +44 -8
- package/src/components/Toggle/Toggle.tsx +23 -22
- package/src/components/Tooltip/Tooltip.tsx +42 -6
package/dist/r/badge.json
CHANGED
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"path": "src/components/Badge/Badge.tsx",
|
|
65
|
-
"content": "import React from 'react';\nimport { Text } from '@/components/Text';\nimport styles from './Badge.module.css';\n\nexport interface BadgeProps {\n count: number;\n isSelected?: boolean;\n className?: string;\n}\n\nexport const Badge: React.FC<BadgeProps> = ({
|
|
65
|
+
"content": "import React from 'react';\nimport { Text } from '@/components/Text';\nimport styles from './Badge.module.css';\n\nexport interface BadgeProps {\n count: number;\n isSelected?: boolean;\n className?: string;\n /**\n * Accessible label announced by screen readers. Takes precedence over `labelFormat`.\n * Provide this when the badge has a specific meaning (e.g. `\"3 unread messages\"`).\n */\n 'aria-label'?: string;\n /**\n * Formats the accessible label from the count. Used when `aria-label` is not provided.\n * Defaults to returning the count as a string.\n */\n labelFormat?: (count: number) => string;\n}\n\nexport const Badge: React.FC<BadgeProps> = ({\n count,\n isSelected = false,\n className,\n 'aria-label': ariaLabel,\n labelFormat,\n}) => {\n if (count === 0) return null;\n const displayText = count > 9 ? '+' : count.toString();\n const accessibleLabel = ariaLabel ?? labelFormat?.(count) ?? String(count);\n return (\n <span className={`${styles.badge} ${className || ''}`} aria-label={accessibleLabel}>\n <Text variant=\"text-caption-emphasis\" as=\"span\" color={isSelected ? 'primary' : 'secondary'}>\n {displayText}\n </Text>\n </span>\n );\n};\n\nBadge.displayName = 'Badge';\n",
|
|
66
66
|
"type": "registry:ui"
|
|
67
67
|
}
|
|
68
68
|
]
|
package/dist/r/banner.json
CHANGED
|
@@ -78,12 +78,12 @@
|
|
|
78
78
|
"files": [
|
|
79
79
|
{
|
|
80
80
|
"path": "src/components/Banner/Banner.module.css",
|
|
81
|
-
"content": ".bannerWrapper {}\n\n.banner {\n position: relative;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.bannerHeaderSurface {\n width: 100%;\n height: var(--dimension-space-100);\n flex-shrink: 0;\n overflow: hidden;\n border-bottom: var(--dimension-stroke-width-015) solid var(--color-border-primary);\n}\n\n.bannerHeader {\n width: 100%;\n height: 100%;\n background: repeating-linear-gradient(\n -45deg,\n var(--banner-content),\n var(--banner-content) var(--dimension-size-100),\n transparent var(--dimension-size-100),\n transparent var(--dimension-size-200)\n );\n}\n\n.bannerBody {\n padding: var(--dimension-space-075);\n min-height: var(--dimension-size-400);\n}\n\n.message {\n color: var(--banner-content);\n line-height: var(--typography-lineheight-md);\n display: block;\n}\n\n.floatingPosition {\n position: fixed;\n top: var(--dimension-space-200);\n left: 50%;\n transform: translateX(-50%);\n pointer-events: none;\n}\n\n.floatingToast {\n backface-visibility: hidden;\n -webkit-font-smoothing: subpixel-antialiased;\n animation: toastFadeIn var(--effect-motion-short-2) forwards;\n}\n\n.floatingToast.closing {\n animation: toastFadeOut var(--effect-motion-short-3) forwards;\n}\n\n@keyframes toastFadeIn {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@keyframes toastFadeOut {\n from { opacity: 1; transform: translateY(0); }\n to { opacity: 0; transform: translateY(-4px); }\n}\n",
|
|
81
|
+
"content": ".bannerWrapper {}\n\n.banner {\n position: relative;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.bannerHeaderSurface {\n width: 100%;\n height: var(--dimension-space-100);\n flex-shrink: 0;\n overflow: hidden;\n border-bottom: var(--dimension-stroke-width-015) solid var(--color-border-primary);\n}\n\n.bannerHeader {\n width: 100%;\n height: 100%;\n background: repeating-linear-gradient(\n -45deg,\n var(--banner-content),\n var(--banner-content) var(--dimension-size-100),\n transparent var(--dimension-size-100),\n transparent var(--dimension-size-200)\n );\n}\n\n.bannerBody {\n padding: var(--dimension-space-075);\n min-height: var(--dimension-size-400);\n display: flex;\n align-items: flex-start;\n gap: var(--dimension-space-075);\n}\n\n.message {\n color: var(--banner-content);\n line-height: var(--typography-lineheight-md);\n display: block;\n flex: 1;\n min-width: 0;\n}\n\n.dismiss {\n background: none;\n border: none;\n margin: 0;\n padding: var(--dimension-space-025);\n color: var(--banner-content);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n border-radius: var(--dimension-radius-050);\n position: relative;\n}\n\n.dismiss::before {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background-color: transparent;\n transition: var(--effect-transition-interaction-background-short-2);\n pointer-events: none;\n}\n\n.dismiss:hover::before { background-color: var(--color-interaction-hover); }\n.dismiss:active::before { background-color: var(--color-interaction-pressed); }\n\n.dismiss:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-stroke-width-015);\n}\n\n.floatingPosition {\n position: fixed;\n top: var(--dimension-space-200);\n left: 50%;\n transform: translateX(-50%);\n pointer-events: none;\n}\n\n.floatingToast {\n backface-visibility: hidden;\n -webkit-font-smoothing: subpixel-antialiased;\n animation: toastFadeIn var(--effect-motion-short-2) forwards;\n}\n\n.floatingToast.closing {\n animation: toastFadeOut var(--effect-motion-short-3) forwards;\n}\n\n@keyframes toastFadeIn {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@keyframes toastFadeOut {\n from { opacity: 1; transform: translateY(0); }\n to { opacity: 0; transform: translateY(-4px); }\n}\n",
|
|
82
82
|
"type": "registry:ui"
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"path": "src/components/Banner/Banner.tsx",
|
|
86
|
-
"content": "import React, { useState, useEffect, useCallback, useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport { Surface } from '@/components/Surface';\nimport { Text } from '@/components/Text';\nimport { getCssTimeMs } from '@/utils/css-tokens';\nimport styles from './Banner.module.css';\n\nconst TOAST_DURATION_FALLBACK_MS = 4000;\nconst FADE_OUT_DURATION_MS = 200;\n\nexport type BannerIntent =\n | 'brand' | 'positive' | 'negative' | 'warning' | 'caution'\n | 'ai' | 'neutral' | 'walkthrough' | 'guide';\n\nexport type BannerContrast = 'faint' | 'medium' | 'bold' | 'strong';\n\nexport interface BannerProps {\n intent: BannerIntent;\n contrast: BannerContrast;\n message: string;\n header?: boolean;\n className?: string;\n floating?: boolean;\n onDismiss?: () => void;\n}\n\nconst getContentColorVar = (intent: BannerIntent, contrast: BannerContrast): string => {\n if (contrast === 'bold') return 'var(--color-foreground-on-bold-background-primary)';\n const contentContrast = contrast === 'faint' ? 'bold' : contrast === 'medium' ? 'strong' : 'medium';\n return `var(--color-foreground-${contentContrast}-${intent})`;\n};\n\nexport const Banner: React.FC<BannerProps> = ({\n intent,\n contrast,\n message,\n header = false,\n className = '',\n floating = false,\n onDismiss,\n}) => {\n const toastDurationMs = getCssTimeMs('--effect-animation-delay-long-2', TOAST_DURATION_FALLBACK_MS);\n const messageColor = getContentColorVar(intent, contrast);\n const [isClosing, setIsClosing] = useState(false);\n const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const
|
|
86
|
+
"content": "import React, { useState, useEffect, useCallback, useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport { Cross } from '@ds-mo/icons';\nimport { Surface } from '@/components/Surface';\nimport { Text } from '@/components/Text';\nimport { getCssTimeMs } from '@/utils/css-tokens';\nimport styles from './Banner.module.css';\n\nconst TOAST_DURATION_FALLBACK_MS = 4000;\nconst FADE_OUT_DURATION_MS = 200;\n\nexport type BannerIntent =\n | 'brand' | 'positive' | 'negative' | 'warning' | 'caution'\n | 'ai' | 'neutral' | 'walkthrough' | 'guide';\n\nexport type BannerContrast = 'faint' | 'medium' | 'bold' | 'strong';\n\nexport interface BannerProps {\n intent: BannerIntent;\n contrast: BannerContrast;\n message: string;\n header?: boolean;\n className?: string;\n floating?: boolean;\n onDismiss?: () => void;\n /** Accessible label for the dismiss button. Defaults to \"Dismiss\". */\n dismissLabel?: string;\n}\n\nconst getContentColorVar = (intent: BannerIntent, contrast: BannerContrast): string => {\n if (contrast === 'bold') return 'var(--color-foreground-on-bold-background-primary)';\n const contentContrast = contrast === 'faint' ? 'bold' : contrast === 'medium' ? 'strong' : 'medium';\n return `var(--color-foreground-${contentContrast}-${intent})`;\n};\n\nexport const Banner: React.FC<BannerProps> = ({\n intent,\n contrast,\n message,\n header = false,\n className = '',\n floating = false,\n onDismiss,\n dismissLabel = 'Dismiss',\n}) => {\n const toastDurationMs = getCssTimeMs('--effect-animation-delay-long-2', TOAST_DURATION_FALLBACK_MS);\n const messageColor = getContentColorVar(intent, contrast);\n const [isClosing, setIsClosing] = useState(false);\n const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const beginClose = useCallback(() => {\n if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);\n if (closeTimerRef.current) clearTimeout(closeTimerRef.current);\n dismissTimerRef.current = null;\n if (floating) {\n setIsClosing(true);\n closeTimerRef.current = setTimeout(() => {\n closeTimerRef.current = null;\n onDismiss?.();\n }, FADE_OUT_DURATION_MS);\n } else {\n onDismiss?.();\n }\n }, [floating, onDismiss]);\n\n const scheduleDismiss = useCallback(() => {\n if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);\n dismissTimerRef.current = setTimeout(() => {\n dismissTimerRef.current = null;\n beginClose();\n }, toastDurationMs);\n }, [beginClose, toastDurationMs]);\n\n useEffect(() => {\n if (floating && message) scheduleDismiss();\n return () => {\n if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);\n if (closeTimerRef.current) clearTimeout(closeTimerRef.current);\n };\n }, [floating, message, scheduleDismiss]);\n\n const isAssertive = intent === 'negative';\n const role = isAssertive ? 'alert' : 'status';\n const ariaLive = isAssertive ? undefined : 'polite';\n\n const content = (\n <div\n className={`${styles.bannerWrapper} ${className}`.trim()}\n style={{ '--banner-content': messageColor } as React.CSSProperties}\n role={role}\n aria-live={ariaLive}\n >\n <Surface\n elevation={floating ? 'floating' : 'elevated'}\n intent={intent}\n contrast={contrast}\n radius=\"sm\"\n className={styles.banner}\n >\n {header && (\n <div className={styles.bannerHeaderSurface}>\n <div className={styles.bannerHeader} aria-hidden />\n </div>\n )}\n <div className={styles.bannerBody}>\n <Text variant=\"text-body-medium\" as=\"span\" className={styles.message}>\n {message}\n </Text>\n {onDismiss && (\n <button\n type=\"button\"\n className={styles.dismiss}\n onClick={beginClose}\n aria-label={dismissLabel}\n >\n <Cross size={16} />\n </button>\n )}\n </div>\n </Surface>\n </div>\n );\n\n if (floating) {\n return createPortal(\n <div className={styles.floatingPosition} style={{ zIndex: 9998 }}>\n <div className={`${styles.floatingToast} ${isClosing ? styles.closing : ''}`}>\n {content}\n </div>\n </div>,\n document.body\n );\n }\n\n return content;\n};\n\nBanner.displayName = 'Banner';\n",
|
|
87
87
|
"type": "registry:ui"
|
|
88
88
|
}
|
|
89
89
|
]
|
package/dist/r/loader.json
CHANGED
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"files": [
|
|
43
43
|
{
|
|
44
44
|
"path": "src/components/Loader/Loader.module.css",
|
|
45
|
-
"content": ".loader {\n flex-shrink: 0;\n animation: spin 0.75s linear infinite;\n}\n\n@keyframes spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n",
|
|
45
|
+
"content": ".loader {\n flex-shrink: 0;\n animation: spin 0.75s linear infinite;\n}\n\n.status {\n display: inline-flex;\n align-items: center;\n}\n\n.visuallyHidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n@keyframes spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n",
|
|
46
46
|
"type": "registry:ui"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"path": "src/components/Loader/Loader.tsx",
|
|
50
|
-
"content": "import React from 'react';\nimport styles from './Loader.module.css';\n\nexport interface LoaderProps {\n /**\n * Render size in px. Designed at 16px (r=6, strokeWidth=1).\n * Stroke weight scales proportionally with size — same behaviour as icons.\n * Defaults to 20.\n */\n size?: number;\n className?: string;\n}\n\n/**\n * Placeholder spinner component. Will be replaced with a full Loader\n * component once motion/brand specs are finalised.\n *\n * Designed at 16×16 viewBox so it matches the icon grid:\n * - r=5 → 10px circle in a 16px box (3px optical padding each side)\n * - strokeWidth=1 at native size → scales to 1.25px at 20px, 1.5px at 24px\n */\nexport const Loader = ({ size = 20, className }: LoaderProps) => (\n
|
|
50
|
+
"content": "import React from 'react';\nimport styles from './Loader.module.css';\n\nexport interface LoaderProps {\n /**\n * Render size in px. Designed at 16px (r=6, strokeWidth=1).\n * Stroke weight scales proportionally with size — same behaviour as icons.\n * Defaults to 20.\n */\n size?: number;\n className?: string;\n /**\n * Accessible label announced by assistive tech when the Loader is used\n * standalone. When provided, the spinner is wrapped in a live region\n * (`role=\"status\"`, `aria-live=\"polite\"`) and the label is rendered\n * visually-hidden so screen readers announce it on mount.\n *\n * Omit when the Loader is rendered inside a host that already conveys\n * busy state (e.g. a Button with `aria-busy`) — the spinner stays\n * `aria-hidden` to avoid double-announcing.\n */\n label?: string;\n}\n\n/**\n * Placeholder spinner component. Will be replaced with a full Loader\n * component once motion/brand specs are finalised.\n *\n * Designed at 16×16 viewBox so it matches the icon grid:\n * - r=5 → 10px circle in a 16px box (3px optical padding each side)\n * - strokeWidth=1 at native size → scales to 1.25px at 20px, 1.5px at 24px\n */\nexport const Loader = ({ size = 20, className, label }: LoaderProps) => {\n const svg = (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n className={`${styles.loader}${className ? ` ${className}` : ''}`}\n aria-hidden=\"true\"\n >\n <circle\n cx=\"8\"\n cy=\"8\"\n r=\"5\"\n stroke=\"currentColor\"\n strokeWidth=\"1\"\n strokeLinecap=\"round\"\n strokeDasharray=\"23.6 7.9\"\n />\n </svg>\n );\n\n if (label === undefined) return svg;\n\n return (\n <span role=\"status\" aria-live=\"polite\" className={styles.status}>\n {svg}\n <span className={styles.visuallyHidden}>{label}</span>\n </span>\n );\n};\n\nLoader.displayName = 'Loader';\n",
|
|
51
51
|
"type": "registry:ui"
|
|
52
52
|
}
|
|
53
53
|
]
|
package/dist/r/menu.json
CHANGED
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"files": [
|
|
99
99
|
{
|
|
100
100
|
"path": "src/components/Menu/Menu.tsx",
|
|
101
|
-
"content": "import React, { useState, useEffect, useLayoutEffect, useRef, useMemo } from 'react';\nimport { createPortal } from 'react-dom';\nimport { Text } from '@/components/Text';\nimport { Surface } from '@/components/Surface';\nimport { MenuItem as MenuItemComponent } from './MenuItem';\nimport { DestructiveMenuItem } from './DestructiveMenuItem';\nimport { getCssSpacePx } from '@/utils/css-tokens';\nimport type { IconComponent } from '@/types';\nimport type { MenuItemSelectionStyle } from './MenuItem';\nimport styles from './Menu.module.css';\n\nconst CLOSE_ANIMATION_MS = 220;\n\nexport type MenuSide = 'top' | 'right' | 'bottom' | 'left';\nexport type MenuAlign = 'start' | 'center' | 'end';\n\nexport interface MenuItemData {\n label: string;\n onClick: () => void;\n isSelected?: boolean;\n isDestructive?: boolean;\n isInactive?: boolean;\n intent?: 'negative';\n keepOpenOnClick?: boolean;\n holdDuration?: number;\n icon?: IconComponent;\n subtext?: string;\n showToggle?: boolean;\n toggleValue?: boolean;\n selectionStyle?: MenuItemSelectionStyle;\n}\n\nexport interface MenuSection {\n header?: string;\n items: MenuItemData[];\n}\n\nexport interface MenuProps {\n isOpen: boolean;\n onClose: () => void;\n anchorRef: React.RefObject<HTMLElement | null>;\n items?: MenuItemData[];\n sections?: MenuSection[];\n side?: MenuSide;\n align?: MenuAlign;\n sideOffset?: number;\n alignOffset?: number;\n sideOffsetToken?: string;\n alignOffsetToken?: string;\n width?: string;\n minWidth?: string;\n maxWidth?: string;\n matchAnchorWidth?: boolean;\n matchAnchorWidthOffset?: number;\n usePortal?: boolean;\n}\n\nexport const Menu: React.FC<MenuProps> = ({\n isOpen,\n onClose,\n anchorRef,\n items,\n sections,\n side = 'bottom',\n align = 'start',\n sideOffset = 4,\n alignOffset = 0,\n sideOffsetToken,\n alignOffsetToken,\n width,\n minWidth,\n maxWidth,\n matchAnchorWidth = false,\n matchAnchorWidthOffset = 0,\n usePortal = true,\n}) => {\n const [isClosing, setIsClosing] = useState(false);\n const [shouldRender, setShouldRender] = useState(isOpen);\n const [isPositionReady, setIsPositionReady] = useState(false);\n const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });\n const [menuSize, setMenuSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 });\n const [anchorSize, setAnchorSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 });\n const positionCalculatedRef = useRef(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const lastSectionsRef = useRef<MenuSection[]>([]);\n\n const menuSections: MenuSection[] = sections || (items ? [{ items }] : []);\n\n const hasItems = menuSections.some((s) => s.items.length > 0);\n if (!isClosing && menuSections.length > 0 && hasItems) {\n lastSectionsRef.current = menuSections;\n }\n const sectionsToRender = isClosing ? lastSectionsRef.current : menuSections;\n\n useEffect(() => {\n if (isOpen) {\n setShouldRender(true);\n setIsClosing(false);\n setIsPositionReady(false);\n positionCalculatedRef.current = false;\n } else if (shouldRender) {\n setIsClosing(true);\n const timer = setTimeout(() => {\n setShouldRender(false);\n setIsClosing(false);\n positionCalculatedRef.current = false;\n }, CLOSE_ANIMATION_MS);\n return () => clearTimeout(timer);\n }\n }, [isOpen, shouldRender]);\n\n useLayoutEffect(() => {\n if (!isOpen || !shouldRender || !menuRef.current) return;\n const updateSize = () => {\n if (!menuRef.current) return;\n const rect = menuRef.current.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) setMenuSize({ width: rect.width, height: rect.height });\n };\n updateSize();\n requestAnimationFrame(() => requestAnimationFrame(updateSize));\n if (typeof ResizeObserver !== 'undefined') {\n const observer = new ResizeObserver(updateSize);\n observer.observe(menuRef.current);\n return () => observer.disconnect();\n }\n }, [isOpen, shouldRender, menuSections.length]);\n\n useLayoutEffect(() => {\n if (!isOpen || !shouldRender || !anchorRef.current || !menuRef.current) return;\n const calculatePosition = () => {\n const anchor = anchorRef.current;\n const menu = menuRef.current;\n if (!anchor || !menu) return;\n const anchorRect = anchor.getBoundingClientRect();\n const menuRect = menu.getBoundingClientRect();\n const menuWidth = menuRect.width || menuSize.width || 200;\n const menuHeight = menuRect.height || menuSize.height || 160;\n setAnchorSize({ width: anchorRect.width, height: anchorRect.height });\n const gap = sideOffsetToken != null ? getCssSpacePx(sideOffsetToken, 12) : sideOffset;\n const alignOff = alignOffsetToken != null ? getCssSpacePx(alignOffsetToken, 4) : alignOffset;\n let x = 0, y = 0;\n switch (side) {\n case 'top':\n y = anchorRect.top - menuHeight - gap;\n x = align === 'start' ? anchorRect.left + alignOff : align === 'end' ? anchorRect.right - menuWidth + alignOff : anchorRect.left + anchorRect.width / 2 - menuWidth / 2 + alignOff;\n break;\n case 'bottom':\n y = anchorRect.bottom + gap;\n x = align === 'start' ? anchorRect.left + alignOff : align === 'end' ? anchorRect.right - menuWidth + alignOff : anchorRect.left + anchorRect.width / 2 - menuWidth / 2 + alignOff;\n break;\n case 'left':\n x = anchorRect.left - menuWidth - gap;\n y = align === 'start' ? anchorRect.top + alignOff : align === 'end' ? anchorRect.bottom - menuHeight + alignOff : anchorRect.top + anchorRect.height / 2 - menuHeight / 2 + alignOff;\n break;\n case 'right':\n x = anchorRect.right + gap;\n y = align === 'start' ? anchorRect.top + alignOff : align === 'end' ? anchorRect.bottom - menuHeight + alignOff : anchorRect.top + anchorRect.height / 2 - menuHeight / 2 + alignOff;\n break;\n }\n const vp = 4;\n setPosition({\n x: Math.min(Math.max(x, vp), window.innerWidth - menuWidth - vp),\n y: Math.min(Math.max(y, vp), window.innerHeight - menuHeight - vp),\n });\n positionCalculatedRef.current = true;\n setIsPositionReady(true);\n };\n if (!positionCalculatedRef.current) {\n calculatePosition();\n requestAnimationFrame(() => { if (!positionCalculatedRef.current) calculatePosition(); });\n }\n const handleScrollResize = () => calculatePosition();\n window.addEventListener('scroll', handleScrollResize, true);\n window.addEventListener('resize', handleScrollResize);\n return () => {\n window.removeEventListener('scroll', handleScrollResize, true);\n window.removeEventListener('resize', handleScrollResize);\n };\n }, [isOpen, shouldRender, anchorRef, side, align, sideOffset, alignOffset, sideOffsetToken, alignOffsetToken, menuSize.width, menuSize.height, menuSections.length, matchAnchorWidthOffset]);\n\n useEffect(() => {\n if (!isOpen || !shouldRender) return;\n const handleClickOutside = (event: MouseEvent) => {\n const target = event.target as HTMLElement;\n if (menuRef.current?.contains(target)) return;\n if (anchorRef.current?.contains(target)) return;\n onClose();\n };\n document.addEventListener('mousedown', handleClickOutside, true);\n return () => document.removeEventListener('mousedown', handleClickOutside, true);\n }, [isOpen, shouldRender, onClose, anchorRef]);\n\n useEffect(() => {\n if (!isOpen) return;\n const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [isOpen, onClose]);\n\n if (!shouldRender || sectionsToRender.length === 0) return null;\n\n const renderMenuItem = (item: MenuItemData, index: number) => {\n if (item.isDestructive) {\n return (\n <DestructiveMenuItem\n key={index}\n label={item.label}\n subtext={item.subtext}\n holdDuration={item.holdDuration ?? 4000}\n onClick={() => { item.onClick(); if (!item.keepOpenOnClick) onClose(); }}\n />\n );\n }\n return (\n <MenuItemComponent\n key={index}\n icon={item.icon}\n label={item.label}\n subtext={item.subtext}\n onClick={() => { if (!item.isInactive) { item.onClick(); if (!item.keepOpenOnClick) onClose(); } }}\n isSelected={item.isSelected}\n isInactive={item.isInactive}\n intent={item.intent}\n showToggle={item.showToggle}\n toggleValue={item.toggleValue}\n selectionStyle={item.selectionStyle}\n />\n );\n };\n\n const menuStyle: React.CSSProperties = {\n position: 'absolute',\n left: 0,\n top: 0,\n transform: `translate(${Math.round(position.x)}px, ${Math.round(position.y)}px)`,\n willChange: 'transform',\n zIndex: 9998,\n visibility: isPositionReady ? 'visible' : 'hidden',\n ...(matchAnchorWidth && anchorSize.width > 0 ? { width: `${anchorSize.width + matchAnchorWidthOffset}px` } : width ? { width } : {}),\n ...(minWidth ? { minWidth } : {}),\n ...(maxWidth ? { maxWidth } : {}),\n };\n\n const menuContent = (\n <div ref={menuRef} className={`${styles.menuContainer} ${isClosing ? styles.closing : ''}`} style={menuStyle} role=\"menu\" aria-label=\"Menu\">\n {sectionsToRender.map((section, sectionIndex) => (\n <Surface\n key={sectionIndex}\n as=\"div\"\n className={styles.menuSection}\n edge={sectionIndex < sectionsToRender.length - 1 ? 'bottom' : undefined}\n elevation=\"none\"\n >\n {section.header && (\n <div className={styles.sectionHeader}>\n <Text variant=\"text-body-small-emphasis\" as=\"span\" className={styles.sectionLabel}>{section.header}</Text>\n </div>\n )}\n {section.items.map((item, itemIndex) => renderMenuItem(item, itemIndex))}\n </Surface>\n ))}\n </div>\n );\n\n if (usePortal) return createPortal(menuContent, document.body);\n return menuContent;\n};\n\nMenu.displayName = 'Menu';\n",
|
|
101
|
+
"content": "import React, { useState, useEffect, useLayoutEffect, useRef, useMemo, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\nimport { Text } from '@/components/Text';\nimport { Surface } from '@/components/Surface';\nimport { MenuItem as MenuItemComponent } from './MenuItem';\nimport { DestructiveMenuItem } from './DestructiveMenuItem';\nimport { getCssSpacePx } from '@/utils/css-tokens';\nimport type { IconComponent } from '@/types';\nimport type { MenuItemSelectionStyle } from './MenuItem';\nimport styles from './Menu.module.css';\n\nconst CLOSE_ANIMATION_MS = 220;\n\nexport type MenuSide = 'top' | 'right' | 'bottom' | 'left';\nexport type MenuAlign = 'start' | 'center' | 'end';\n\nexport interface MenuItemData {\n label: string;\n onClick: () => void;\n isSelected?: boolean;\n isDestructive?: boolean;\n isInactive?: boolean;\n intent?: 'negative';\n keepOpenOnClick?: boolean;\n holdDuration?: number;\n icon?: IconComponent;\n subtext?: string;\n showToggle?: boolean;\n toggleValue?: boolean;\n selectionStyle?: MenuItemSelectionStyle;\n}\n\nexport interface MenuSection {\n header?: string;\n items: MenuItemData[];\n}\n\nexport interface MenuProps {\n isOpen: boolean;\n onClose: () => void;\n anchorRef: React.RefObject<HTMLElement | null>;\n items?: MenuItemData[];\n sections?: MenuSection[];\n side?: MenuSide;\n align?: MenuAlign;\n sideOffset?: number;\n alignOffset?: number;\n sideOffsetToken?: string;\n alignOffsetToken?: string;\n width?: string;\n minWidth?: string;\n maxWidth?: string;\n matchAnchorWidth?: boolean;\n matchAnchorWidthOffset?: number;\n usePortal?: boolean;\n /** Container role + item role semantics. Defaults to `menu` (items are buttons with menuitem* roles, keyboard nav handled internally). `listbox` makes items render as `option` with `aria-selected`, with keyboard nav handled by the consumer via `activeIndex`. */\n role?: 'menu' | 'listbox';\n /** DOM id for the popup container — required when an external combobox references it via `aria-controls`/`aria-activedescendant`. */\n id?: string;\n /** Accessible label for the popup container. Defaults to `'Menu'` for menu role; omitted for listbox (let consumer provide via aria-labelledby on the combobox). */\n ariaLabel?: string;\n /** Active descendant index for listbox keyboard nav — highlights one option visually without moving DOM focus. */\n activeIndex?: number;\n /** Builds the DOM id for option `index`. Required in listbox mode if `aria-activedescendant` is used externally. */\n getOptionId?: (index: number) => string;\n}\n\nexport const Menu: React.FC<MenuProps> = ({\n isOpen,\n onClose,\n anchorRef,\n items,\n sections,\n side = 'bottom',\n align = 'start',\n sideOffset = 4,\n alignOffset = 0,\n sideOffsetToken,\n alignOffsetToken,\n width,\n minWidth,\n maxWidth,\n matchAnchorWidth = false,\n matchAnchorWidthOffset = 0,\n usePortal = true,\n role = 'menu',\n id,\n ariaLabel,\n activeIndex,\n getOptionId,\n}) => {\n const isListbox = role === 'listbox';\n const [isClosing, setIsClosing] = useState(false);\n const [shouldRender, setShouldRender] = useState(isOpen);\n const [isPositionReady, setIsPositionReady] = useState(false);\n const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });\n const [menuSize, setMenuSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 });\n const [anchorSize, setAnchorSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 });\n const [focusedIndex, setFocusedIndex] = useState(0);\n const positionCalculatedRef = useRef(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const lastSectionsRef = useRef<MenuSection[]>([]);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const focusedOnOpenRef = useRef(false);\n\n const menuSections: MenuSection[] = sections || (items ? [{ items }] : []);\n\n const hasItems = menuSections.some((s) => s.items.length > 0);\n if (!isClosing && menuSections.length > 0 && hasItems) {\n lastSectionsRef.current = menuSections;\n }\n const sectionsToRender = isClosing ? lastSectionsRef.current : menuSections;\n\n const flatItems = useMemo(() => {\n const list: MenuItemData[] = [];\n sectionsToRender.forEach((section) => {\n section.items.forEach((it) => list.push(it));\n });\n return list;\n }, [sectionsToRender]);\n\n useEffect(() => {\n if (isOpen) {\n setShouldRender(true);\n setIsClosing(false);\n setIsPositionReady(false);\n positionCalculatedRef.current = false;\n } else if (shouldRender) {\n setIsClosing(true);\n const timer = setTimeout(() => {\n setShouldRender(false);\n setIsClosing(false);\n positionCalculatedRef.current = false;\n }, CLOSE_ANIMATION_MS);\n return () => clearTimeout(timer);\n }\n }, [isOpen, shouldRender]);\n\n useLayoutEffect(() => {\n if (!isOpen || !shouldRender || !menuRef.current) return;\n const updateSize = () => {\n if (!menuRef.current) return;\n const rect = menuRef.current.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) setMenuSize({ width: rect.width, height: rect.height });\n };\n updateSize();\n requestAnimationFrame(() => requestAnimationFrame(updateSize));\n if (typeof ResizeObserver !== 'undefined') {\n const observer = new ResizeObserver(updateSize);\n observer.observe(menuRef.current);\n return () => observer.disconnect();\n }\n }, [isOpen, shouldRender, menuSections.length]);\n\n useLayoutEffect(() => {\n if (!isOpen || !shouldRender || !anchorRef.current || !menuRef.current) return;\n const calculatePosition = () => {\n const anchor = anchorRef.current;\n const menu = menuRef.current;\n if (!anchor || !menu) return;\n const anchorRect = anchor.getBoundingClientRect();\n const menuRect = menu.getBoundingClientRect();\n const menuWidth = menuRect.width || menuSize.width || 200;\n const menuHeight = menuRect.height || menuSize.height || 160;\n setAnchorSize({ width: anchorRect.width, height: anchorRect.height });\n const gap = sideOffsetToken != null ? getCssSpacePx(sideOffsetToken, 12) : sideOffset;\n const alignOff = alignOffsetToken != null ? getCssSpacePx(alignOffsetToken, 4) : alignOffset;\n let x = 0, y = 0;\n switch (side) {\n case 'top':\n y = anchorRect.top - menuHeight - gap;\n x = align === 'start' ? anchorRect.left + alignOff : align === 'end' ? anchorRect.right - menuWidth + alignOff : anchorRect.left + anchorRect.width / 2 - menuWidth / 2 + alignOff;\n break;\n case 'bottom':\n y = anchorRect.bottom + gap;\n x = align === 'start' ? anchorRect.left + alignOff : align === 'end' ? anchorRect.right - menuWidth + alignOff : anchorRect.left + anchorRect.width / 2 - menuWidth / 2 + alignOff;\n break;\n case 'left':\n x = anchorRect.left - menuWidth - gap;\n y = align === 'start' ? anchorRect.top + alignOff : align === 'end' ? anchorRect.bottom - menuHeight + alignOff : anchorRect.top + anchorRect.height / 2 - menuHeight / 2 + alignOff;\n break;\n case 'right':\n x = anchorRect.right + gap;\n y = align === 'start' ? anchorRect.top + alignOff : align === 'end' ? anchorRect.bottom - menuHeight + alignOff : anchorRect.top + anchorRect.height / 2 - menuHeight / 2 + alignOff;\n break;\n }\n const vp = 4;\n setPosition({\n x: Math.min(Math.max(x, vp), window.innerWidth - menuWidth - vp),\n y: Math.min(Math.max(y, vp), window.innerHeight - menuHeight - vp),\n });\n positionCalculatedRef.current = true;\n setIsPositionReady(true);\n };\n if (!positionCalculatedRef.current) {\n calculatePosition();\n requestAnimationFrame(() => { if (!positionCalculatedRef.current) calculatePosition(); });\n }\n const handleScrollResize = () => calculatePosition();\n window.addEventListener('scroll', handleScrollResize, true);\n window.addEventListener('resize', handleScrollResize);\n return () => {\n window.removeEventListener('scroll', handleScrollResize, true);\n window.removeEventListener('resize', handleScrollResize);\n };\n }, [isOpen, shouldRender, anchorRef, side, align, sideOffset, alignOffset, sideOffsetToken, alignOffsetToken, menuSize.width, menuSize.height, menuSections.length, matchAnchorWidthOffset]);\n\n const closeAndRestoreFocus = useCallback(() => {\n const active = document.activeElement as HTMLElement | null;\n const focusInside = !!(active && menuRef.current?.contains(active));\n if (focusInside && anchorRef.current) {\n anchorRef.current.focus();\n }\n onClose();\n }, [onClose, anchorRef]);\n\n useEffect(() => {\n if (!isOpen || !shouldRender) return;\n const handleClickOutside = (event: MouseEvent) => {\n const target = event.target as HTMLElement;\n if (menuRef.current?.contains(target)) return;\n if (anchorRef.current?.contains(target)) return;\n onClose();\n };\n document.addEventListener('mousedown', handleClickOutside, true);\n return () => document.removeEventListener('mousedown', handleClickOutside, true);\n }, [isOpen, shouldRender, onClose, anchorRef]);\n\n useEffect(() => {\n if (!isOpen) return;\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n if (isListbox) {\n onClose();\n } else {\n e.preventDefault();\n closeAndRestoreFocus();\n }\n }\n };\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [isOpen, isListbox, onClose, closeAndRestoreFocus]);\n\n useEffect(() => {\n if (!isOpen) {\n focusedOnOpenRef.current = false;\n return;\n }\n if (isListbox || !isPositionReady || focusedOnOpenRef.current) return;\n const firstEnabled = flatItems.findIndex((it) => !it.isInactive);\n const targetIdx = firstEnabled >= 0 ? firstEnabled : 0;\n setFocusedIndex(targetIdx);\n itemRefs.current[targetIdx]?.focus();\n focusedOnOpenRef.current = true;\n }, [isOpen, isListbox, isPositionReady, flatItems]);\n\n const moveFocus = useCallback((idx: number) => {\n setFocusedIndex(idx);\n itemRefs.current[idx]?.focus();\n }, []);\n\n const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {\n if (isListbox || !flatItems.length) return;\n const enabled: number[] = [];\n flatItems.forEach((it, i) => { if (!it.isInactive) enabled.push(i); });\n if (!enabled.length) return;\n const currentPos = enabled.indexOf(focusedIndex);\n const safePos = currentPos < 0 ? 0 : currentPos;\n\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault();\n e.stopPropagation();\n moveFocus(enabled[(safePos + 1) % enabled.length]);\n break;\n }\n case 'ArrowUp': {\n e.preventDefault();\n e.stopPropagation();\n moveFocus(enabled[(safePos - 1 + enabled.length) % enabled.length]);\n break;\n }\n case 'Home': {\n e.preventDefault();\n e.stopPropagation();\n moveFocus(enabled[0]);\n break;\n }\n case 'End': {\n e.preventDefault();\n e.stopPropagation();\n moveFocus(enabled[enabled.length - 1]);\n break;\n }\n case 'Tab': {\n e.preventDefault();\n closeAndRestoreFocus();\n break;\n }\n }\n }, [isListbox, flatItems, focusedIndex, moveFocus, closeAndRestoreFocus]);\n\n if (!shouldRender || sectionsToRender.length === 0) return null;\n\n const setItemRef = (flatIndex: number) => (el: HTMLButtonElement | null) => {\n itemRefs.current[flatIndex] = el;\n };\n\n const handleItemFocus = (flatIndex: number) => () => {\n setFocusedIndex(flatIndex);\n };\n\n const handleItemActivate = (item: MenuItemData) => {\n if (item.isInactive) return;\n item.onClick();\n if (item.keepOpenOnClick) return;\n if (isListbox) {\n onClose();\n } else {\n closeAndRestoreFocus();\n }\n };\n\n const renderMenuItem = (item: MenuItemData, key: number, flatIndex: number) => {\n if (item.isDestructive) {\n const tabIndexValue = isListbox ? -1 : (focusedIndex === flatIndex ? 0 : -1);\n return (\n <DestructiveMenuItem\n key={key}\n ref={setItemRef(flatIndex)}\n label={item.label}\n subtext={item.subtext}\n holdDuration={item.holdDuration ?? 4000}\n onClick={() => { item.onClick(); if (!item.keepOpenOnClick) { if (isListbox) onClose(); else closeAndRestoreFocus(); } }}\n tabIndex={tabIndexValue}\n onFocus={handleItemFocus(flatIndex)}\n />\n );\n }\n const optionId = isListbox && getOptionId ? getOptionId(flatIndex) : undefined;\n const tabIndexValue = isListbox ? -1 : (focusedIndex === flatIndex ? 0 : -1);\n return (\n <MenuItemComponent\n key={key}\n ref={setItemRef(flatIndex)}\n icon={item.icon}\n label={item.label}\n subtext={item.subtext}\n onClick={() => handleItemActivate(item)}\n isSelected={item.isSelected}\n isInactive={item.isInactive}\n intent={item.intent}\n showToggle={item.showToggle}\n toggleValue={item.toggleValue}\n selectionStyle={item.selectionStyle}\n role={isListbox ? 'option' : undefined}\n id={optionId}\n ariaSelected={isListbox ? !!item.isSelected : undefined}\n isActive={isListbox && activeIndex === flatIndex}\n tabIndex={tabIndexValue}\n onFocus={handleItemFocus(flatIndex)}\n />\n );\n };\n\n const menuStyle: React.CSSProperties = {\n position: 'absolute',\n left: 0,\n top: 0,\n transform: `translate(${Math.round(position.x)}px, ${Math.round(position.y)}px)`,\n willChange: 'transform',\n zIndex: 9998,\n visibility: isPositionReady ? 'visible' : 'hidden',\n ...(matchAnchorWidth && anchorSize.width > 0 ? { width: `${anchorSize.width + matchAnchorWidthOffset}px` } : width ? { width } : {}),\n ...(minWidth ? { minWidth } : {}),\n ...(maxWidth ? { maxWidth } : {}),\n };\n\n let flatIndexCounter = 0;\n const containerLabel = ariaLabel ?? (isListbox ? undefined : 'Menu');\n const menuContent = (\n <div\n ref={menuRef}\n id={id}\n className={`${styles.menuContainer} ${isClosing ? styles.closing : ''}`}\n style={menuStyle}\n role={role}\n aria-label={containerLabel}\n aria-orientation=\"vertical\"\n onKeyDown={handleKeyDown}\n >\n {sectionsToRender.map((section, sectionIndex) => (\n <Surface\n key={sectionIndex}\n as=\"div\"\n className={styles.menuSection}\n edge={sectionIndex < sectionsToRender.length - 1 ? 'bottom' : undefined}\n elevation=\"none\"\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <div className={styles.sectionHeader} aria-hidden=\"true\">\n <Text variant=\"text-body-small-emphasis\" as=\"span\" className={styles.sectionLabel}>{section.header}</Text>\n </div>\n )}\n {section.items.map((item, itemIndex) => renderMenuItem(item, itemIndex, flatIndexCounter++))}\n </Surface>\n ))}\n </div>\n );\n\n if (usePortal) return createPortal(menuContent, document.body);\n return menuContent;\n};\n\nMenu.displayName = 'Menu';\n",
|
|
102
102
|
"type": "registry:ui"
|
|
103
103
|
},
|
|
104
104
|
{
|
|
@@ -108,17 +108,17 @@
|
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
"path": "src/components/Menu/MenuItem.tsx",
|
|
111
|
-
"content": "import React, { forwardRef } from 'react';\nimport { Text } from '@/components/Text';\nimport { Surface } from '@/components/Surface';\nimport type { IconComponent } from '@/types';\nimport styles from './MenuItem.module.css';\n\nexport type MenuItemSelectionStyle = 'highlight' | 'radio' | 'noOverlay';\n\nexport interface MenuItemProps {\n label: string;\n onClick: () => void;\n isSelected?: boolean;\n isInactive?: boolean;\n icon?: IconComponent;\n labelColor?: 'primary' | 'secondary' | 'tertiary';\n subtext?: string;\n showToggle?: boolean;\n toggleValue?: boolean;\n intent?: 'negative';\n selectionStyle?: MenuItemSelectionStyle;\n showTrailingChevron?: boolean;\n checkIcon?: IconComponent;\n chevronIcon?: IconComponent;\n}\n\nexport const MenuItem = forwardRef<HTMLButtonElement, MenuItemProps>(\n ({\n label,\n onClick,\n isSelected = false,\n isInactive = false,\n icon: Icon,\n labelColor,\n subtext,\n showToggle = false,\n toggleValue = false,\n intent,\n selectionStyle = 'highlight',\n showTrailingChevron = false,\n checkIcon: CheckIcon,\n chevronIcon: ChevronIcon,\n }, ref) => {\n const useRadioStyle = selectionStyle === 'radio';\n const noOverlay = selectionStyle === 'noOverlay' || useRadioStyle;\n const showSelectedOverlay = isSelected && !noOverlay;\n\n return (\n <Surface\n ref={ref as React.Ref<HTMLElement>}\n as=\"button\"\n interactive={!isInactive}\n selected={showSelectedOverlay}\n radius=\"sm\"\n className={`${styles.menuItem} ${isSelected ? styles.menuItemSelected : ''} ${isInactive ? styles.menuItemInactive : ''} ${intent === 'negative' ? styles.menuItemNegative : ''} ${labelColor === 'tertiary' ? styles.menuItemLabelTertiary : ''}`}\n onClick={onClick}\n type=\"button\"\n inactive={isInactive}\n >\n {Icon && (\n <div className={styles.iconPrefix}>\n <Icon size={20} />\n </div>\n )}\n <div className={styles.content}>\n <Text\n variant={isSelected ? 'text-body-medium-emphasis' : 'text-body-medium'}\n color={labelColor}\n as=\"span\"\n className={styles.label}\n >\n {label}\n </Text>\n {subtext && (\n <Text variant=\"text-body-small\" as=\"span\" className={styles.subtext}>\n {subtext}\n </Text>\n )}\n </div>\n {showToggle && (\n <div className={styles.toggleSuffix}>\n <div className={`${styles.toggle} ${toggleValue ? styles.toggleOn : ''}`}>\n <div className={styles.toggleThumb} />\n </div>\n </div>\n )}\n {useRadioStyle && isSelected && CheckIcon && (\n <div className={styles.checkSuffix}>\n <CheckIcon size={20} />\n </div>\n )}\n {showTrailingChevron && ChevronIcon && (\n <div className={styles.chevronSuffix}>\n <ChevronIcon size={20} />\n </div>\n )}\n </Surface>\n );\n }\n);\n\nMenuItem.displayName = 'MenuItem';\n",
|
|
111
|
+
"content": "import React, { forwardRef } from 'react';\nimport { Text } from '@/components/Text';\nimport { Surface } from '@/components/Surface';\nimport type { IconComponent } from '@/types';\nimport styles from './MenuItem.module.css';\n\nexport type MenuItemSelectionStyle = 'highlight' | 'radio' | 'noOverlay';\n\nexport interface MenuItemProps {\n label: string;\n onClick: () => void;\n isSelected?: boolean;\n isInactive?: boolean;\n icon?: IconComponent;\n labelColor?: 'primary' | 'secondary' | 'tertiary';\n subtext?: string;\n showToggle?: boolean;\n toggleValue?: boolean;\n intent?: 'negative';\n selectionStyle?: MenuItemSelectionStyle;\n showTrailingChevron?: boolean;\n checkIcon?: IconComponent;\n chevronIcon?: IconComponent;\n /** Override the implicit role. Defaults derive from menu context: `menuitem`, `menuitemcheckbox` (showToggle), or `menuitemradio` (radio selectionStyle). Pass `'option'` for listbox usage. */\n role?: string;\n /** Element id — required when used as an `option` referenced by `aria-activedescendant`. */\n id?: string;\n /** Maps to `aria-selected`. Used when role is `option`. */\n ariaSelected?: boolean;\n /** Visual highlight for keyboard \"active\" cursor (separate from selection state). */\n isActive?: boolean;\n /** Override default tabIndex (e.g. -1 for listbox options driven by aria-activedescendant). */\n tabIndex?: number;\n onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;\n onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void;\n}\n\nexport const MenuItem = forwardRef<HTMLButtonElement, MenuItemProps>(\n ({\n label,\n onClick,\n isSelected = false,\n isInactive = false,\n icon: Icon,\n labelColor,\n subtext,\n showToggle = false,\n toggleValue = false,\n intent,\n selectionStyle = 'highlight',\n showTrailingChevron = false,\n checkIcon: CheckIcon,\n chevronIcon: ChevronIcon,\n role,\n id,\n ariaSelected,\n isActive = false,\n tabIndex,\n onKeyDown,\n onFocus,\n }, ref) => {\n const useRadioStyle = selectionStyle === 'radio';\n const noOverlay = selectionStyle === 'noOverlay' || useRadioStyle;\n const showSelectedOverlay = isSelected && !noOverlay;\n\n const resolvedRole = role ?? (\n showToggle ? 'menuitemcheckbox'\n : useRadioStyle ? 'menuitemradio'\n : 'menuitem'\n );\n\n const ariaCheckedValue: boolean | undefined = role\n ? undefined\n : resolvedRole === 'menuitemcheckbox'\n ? !!toggleValue\n : resolvedRole === 'menuitemradio'\n ? !!isSelected\n : undefined;\n\n const ariaSelectedValue: boolean | undefined = role && ariaSelected !== undefined ? ariaSelected : undefined;\n\n return (\n <Surface\n ref={ref as React.Ref<HTMLElement>}\n as=\"button\"\n interactive={!isInactive}\n selected={showSelectedOverlay}\n radius=\"sm\"\n className={`${styles.menuItem} ${isSelected ? styles.menuItemSelected : ''} ${isInactive ? styles.menuItemInactive : ''} ${intent === 'negative' ? styles.menuItemNegative : ''} ${labelColor === 'tertiary' ? styles.menuItemLabelTertiary : ''} ${isActive ? styles.menuItemActive : ''}`}\n onClick={onClick}\n type=\"button\"\n inactive={isInactive}\n role={resolvedRole}\n id={id}\n aria-checked={ariaCheckedValue}\n aria-selected={ariaSelectedValue}\n aria-disabled={isInactive || undefined}\n tabIndex={tabIndex}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n >\n {Icon && (\n <div className={styles.iconPrefix} aria-hidden=\"true\">\n <Icon size={20} />\n </div>\n )}\n <div className={styles.content}>\n <Text\n variant={isSelected ? 'text-body-medium-emphasis' : 'text-body-medium'}\n color={labelColor}\n as=\"span\"\n className={styles.label}\n >\n {label}\n </Text>\n {subtext && (\n <Text variant=\"text-body-small\" as=\"span\" className={styles.subtext}>\n {subtext}\n </Text>\n )}\n </div>\n {showToggle && (\n <div className={styles.toggleSuffix} aria-hidden=\"true\">\n <div className={`${styles.toggle} ${toggleValue ? styles.toggleOn : ''}`}>\n <div className={styles.toggleThumb} />\n </div>\n </div>\n )}\n {useRadioStyle && isSelected && CheckIcon && (\n <div className={styles.checkSuffix} aria-hidden=\"true\">\n <CheckIcon size={20} />\n </div>\n )}\n {showTrailingChevron && ChevronIcon && (\n <div className={styles.chevronSuffix} aria-hidden=\"true\">\n <ChevronIcon size={20} />\n </div>\n )}\n </Surface>\n );\n }\n);\n\nMenuItem.displayName = 'MenuItem';\n",
|
|
112
112
|
"type": "registry:ui"
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"path": "src/components/Menu/MenuItem.module.css",
|
|
116
|
-
"content": ".menuItem {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-075);\n border: none;\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n text-align: left;\n width: 100%;\n min-height: var(--dimension-size-400);\n}\n\n.menuItem:has(.subtext) { align-items: flex-start; }\n\n.iconPrefix {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n color: var(--color-foreground-secondary);\n}\n\n.menuItemSelected .iconPrefix { color: var(--color-foreground-primary); }\n\n.content {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-025);\n min-width: 0;\n}\n\n.label { color: var(--color-foreground-secondary); }\n.menuItem:has(.subtext) .label { color: var(--color-foreground-primary); }\n.menuItemSelected .label { color: var(--color-foreground-primary) !important; }\n.menuItemLabelTertiary .label, .menuItemLabelTertiary .iconPrefix { color: var(--color-foreground-tertiary); }\n.menuItemNegative .label { color: var(--color-foreground-bold-negative) !important; }\n\n.subtext { color: var(--color-foreground-secondary); line-height: 1.4; }\n\n.menuItemInactive { opacity: 0.5; cursor: default; pointer-events: none; }\n\n.toggleSuffix { flex-shrink: 0; display: flex; align-items: center; }\n\n.toggle {\n width: calc(var(--dimension-size-400) + var(--dimension-size-050));\n height: var(--dimension-size-250);\n background-color: var(--color-background-translucent);\n border-radius: var(--dimension-radius-125);\n position: relative;\n transition: var(--effect-transition-interaction-background-short-2);\n}\n\n.toggleOn { background-color: var(--color-background-medium-brand); }\n\n.toggleThumb {\n width: var(--dimension-size-200);\n height: var(--dimension-size-200);\n background-color: var(--color-foreground-on-bold-background-primary);\n border-radius: 50%;\n position: absolute;\n top: var(--dimension-space-025);\n left: var(--dimension-space-025);\n transition: transform var(--effect-motion-short-2);\n}\n\n.toggleOn .toggleThumb { transform: translateX(var(--dimension-offset-200)); }\n\n.checkSuffix {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n color: var(--color-foreground-primary);\n}\n\n.chevronSuffix {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n color: var(--color-foreground-secondary);\n}\n\n.menuItemSelected .chevronSuffix { color: var(--color-foreground-primary); }\n",
|
|
116
|
+
"content": ".menuItem {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-075);\n border: none;\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n text-align: left;\n width: 100%;\n min-height: var(--dimension-size-400);\n}\n\n.menuItem:has(.subtext) { align-items: flex-start; }\n\n.iconPrefix {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n color: var(--color-foreground-secondary);\n}\n\n.menuItemSelected .iconPrefix { color: var(--color-foreground-primary); }\n\n.content {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-025);\n min-width: 0;\n}\n\n.label { color: var(--color-foreground-secondary); }\n.menuItem:has(.subtext) .label { color: var(--color-foreground-primary); }\n.menuItemSelected .label { color: var(--color-foreground-primary) !important; }\n.menuItemLabelTertiary .label, .menuItemLabelTertiary .iconPrefix { color: var(--color-foreground-tertiary); }\n.menuItemNegative .label { color: var(--color-foreground-bold-negative) !important; }\n\n.subtext { color: var(--color-foreground-secondary); line-height: 1.4; }\n\n.menuItemInactive { opacity: 0.5; cursor: default; pointer-events: none; }\n\n.menuItemActive { background-color: var(--color-interaction-hover); }\n\n.toggleSuffix { flex-shrink: 0; display: flex; align-items: center; }\n\n.toggle {\n width: calc(var(--dimension-size-400) + var(--dimension-size-050));\n height: var(--dimension-size-250);\n background-color: var(--color-background-translucent);\n border-radius: var(--dimension-radius-125);\n position: relative;\n transition: var(--effect-transition-interaction-background-short-2);\n}\n\n.toggleOn { background-color: var(--color-background-medium-brand); }\n\n.toggleThumb {\n width: var(--dimension-size-200);\n height: var(--dimension-size-200);\n background-color: var(--color-foreground-on-bold-background-primary);\n border-radius: 50%;\n position: absolute;\n top: var(--dimension-space-025);\n left: var(--dimension-space-025);\n transition: transform var(--effect-motion-short-2);\n}\n\n.toggleOn .toggleThumb { transform: translateX(var(--dimension-offset-200)); }\n\n.checkSuffix {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n color: var(--color-foreground-primary);\n}\n\n.chevronSuffix {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n color: var(--color-foreground-secondary);\n}\n\n.menuItemSelected .chevronSuffix { color: var(--color-foreground-primary); }\n",
|
|
117
117
|
"type": "registry:ui"
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"path": "src/components/Menu/DestructiveMenuItem.tsx",
|
|
121
|
-
"content": "import React, { forwardRef, useState, useRef, useEffect } from 'react';\nimport { Text } from '@/components/Text';\nimport styles from './DestructiveMenuItem.module.css';\n\nexport interface DestructiveMenuItemProps {\n label: string;\n onClick: () => void;\n holdDuration?: number;\n subtext?: string;\n}\n\nexport const DestructiveMenuItem = forwardRef<HTMLButtonElement, DestructiveMenuItemProps>(\n ({ label, onClick, holdDuration = 4000, subtext }, ref) => {\n const [progress, setProgress] = useState(0);\n const pressTimerRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const animationFrameRef = useRef<number | null>(null);\n\n const resetState = () => {\n setProgress(0);\n if (pressTimerRef.current) { clearTimeout(pressTimerRef.current); pressTimerRef.current = null; }\n if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; }\n };\n\n const
|
|
121
|
+
"content": "import React, { forwardRef, useState, useRef, useEffect } from 'react';\nimport { Text } from '@/components/Text';\nimport styles from './DestructiveMenuItem.module.css';\n\nexport interface DestructiveMenuItemProps {\n label: string;\n onClick: () => void;\n holdDuration?: number;\n subtext?: string;\n tabIndex?: number;\n onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;\n onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void;\n}\n\nexport const DestructiveMenuItem = forwardRef<HTMLButtonElement, DestructiveMenuItemProps>(\n ({ label, onClick, holdDuration = 4000, subtext, tabIndex, onKeyDown, onFocus }, ref) => {\n const [progress, setProgress] = useState(0);\n const pressTimerRef = useRef<number | null>(null);\n const startTimeRef = useRef<number>(0);\n const animationFrameRef = useRef<number | null>(null);\n const isHoldingRef = useRef(false);\n\n const resetState = () => {\n setProgress(0);\n isHoldingRef.current = false;\n if (pressTimerRef.current) { clearTimeout(pressTimerRef.current); pressTimerRef.current = null; }\n if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; }\n };\n\n const startHold = () => {\n if (isHoldingRef.current) return;\n isHoldingRef.current = true;\n if (holdDuration === 0) { onClick(); resetState(); return; }\n startTimeRef.current = performance.now();\n const animate = () => {\n const elapsed = performance.now() - startTimeRef.current;\n const newProgress = Math.min((elapsed / holdDuration) * 100, 100);\n setProgress(newProgress);\n if (newProgress < 100) animationFrameRef.current = requestAnimationFrame(animate);\n };\n animationFrameRef.current = requestAnimationFrame(animate);\n pressTimerRef.current = window.setTimeout(() => { onClick(); resetState(); }, holdDuration);\n };\n\n const handleMouseDown = (e: React.MouseEvent) => {\n e.stopPropagation();\n e.preventDefault();\n startHold();\n };\n\n const handleMouseUp = (e: React.MouseEvent) => { e.stopPropagation(); resetState(); };\n const handleMouseLeave = (e: React.MouseEvent) => { e.stopPropagation(); resetState(); };\n\n const isSpaceKey = (e: React.KeyboardEvent) => e.key === ' ' || e.key === 'Spacebar';\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n if (isSpaceKey(e)) {\n e.preventDefault();\n if (e.repeat) return;\n e.stopPropagation();\n startHold();\n return;\n }\n if (e.key === 'Enter') {\n e.preventDefault();\n return;\n }\n onKeyDown?.(e);\n };\n\n const handleKeyUp = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n if (isSpaceKey(e)) {\n e.preventDefault();\n e.stopPropagation();\n resetState();\n }\n };\n\n const handleBlur = () => { resetState(); };\n\n useEffect(() => () => { resetState(); }, []);\n\n return (\n <button\n ref={ref}\n className={styles.destructiveMenuItem}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onMouseLeave={handleMouseLeave}\n onKeyDown={handleKeyDown}\n onKeyUp={handleKeyUp}\n onBlur={handleBlur}\n onFocus={onFocus}\n type=\"button\"\n role=\"menuitem\"\n tabIndex={tabIndex ?? -1}\n aria-keyshortcuts=\"Space\"\n >\n <div\n className={styles.progressFill}\n style={{ width: `${progress}%` }}\n role=\"progressbar\"\n aria-valuenow={Math.round(progress)}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={`Hold to confirm ${label}`}\n />\n <div className={styles.content}>\n <Text variant=\"text-body-medium\" as=\"span\" className={styles.label}>{label}</Text>\n {subtext && <Text variant=\"text-body-small\" as=\"span\" className={styles.subtext}>{subtext}</Text>}\n </div>\n </button>\n );\n }\n);\n\nDestructiveMenuItem.displayName = 'DestructiveMenuItem';\n",
|
|
122
122
|
"type": "registry:ui"
|
|
123
123
|
},
|
|
124
124
|
{
|
package/dist/r/select.json
CHANGED
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
"path": "src/components/Select/Select.tsx",
|
|
89
|
-
"content": "import React, { forwardRef, useState, useRef } from 'react';\nimport { Surface } from '@/components/Surface';\nimport { Text } from '@/components/Text';\nimport { Menu } from '@/components/Menu';\nimport type { MenuItemData } from '@/components/Menu';\nimport type { IconComponent } from '@/types';\nimport styles from './Select.module.css';\n\nexport interface SelectOption {\n label: string;\n value: string | number;\n}\n\nexport interface SelectProps {\n value: string | number;\n onChange: (value: string | number) => void;\n options: SelectOption[];\n placeholder?: string;\n className?: string;\n inactive?: boolean;\n id?: string;\n chevronIcon?: IconComponent;\n}\n\nexport const Select = forwardRef<HTMLDivElement, SelectProps>(\n ({
|
|
89
|
+
"content": "import React, { forwardRef, useId, useState, useRef, useEffect, useCallback } from 'react';\nimport { Surface } from '@/components/Surface';\nimport { Text } from '@/components/Text';\nimport { Menu } from '@/components/Menu';\nimport type { MenuItemData } from '@/components/Menu';\nimport type { IconComponent } from '@/types';\nimport styles from './Select.module.css';\n\nexport interface SelectOption {\n label: string;\n value: string | number;\n}\n\nexport interface SelectProps {\n value: string | number;\n onChange: (value: string | number) => void;\n options: SelectOption[];\n placeholder?: string;\n className?: string;\n inactive?: boolean;\n id?: string;\n chevronIcon?: IconComponent;\n /** Accessible name. Required when no associated `<label htmlFor>` element exists. */\n 'aria-label'?: string;\n /** Accessible name reference. Use when a visible label already exists outside the trigger. */\n 'aria-labelledby'?: string;\n}\n\nexport const Select = forwardRef<HTMLDivElement, SelectProps>(\n (\n {\n value,\n onChange,\n options,\n placeholder = 'Select option',\n className,\n inactive = false,\n id,\n chevronIcon: ChevronIcon,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n },\n ref\n ) => {\n const [isMenuOpen, setIsMenuOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState(-1);\n const buttonRef = useRef<HTMLButtonElement>(null);\n\n const reactId = useId();\n const triggerId = id ?? `select-${reactId}`;\n const listboxId = `${triggerId}-listbox`;\n const getOptionId = useCallback((index: number) => `${triggerId}-option-${index}`, [triggerId]);\n\n const selectedIndex = options.findIndex(opt => opt.value === value);\n const selectedOption = selectedIndex >= 0 ? options[selectedIndex] : undefined;\n const selectedLabel = selectedOption?.label || placeholder;\n\n const commitSelection = useCallback(\n (index: number) => {\n const option = options[index];\n if (!option) return;\n onChange(option.value);\n setIsMenuOpen(false);\n },\n [options, onChange]\n );\n\n const openWith = useCallback(\n (initialIndex: number) => {\n if (inactive || options.length === 0) return;\n const clamped = Math.max(0, Math.min(options.length - 1, initialIndex));\n setActiveIndex(clamped);\n setIsMenuOpen(true);\n },\n [inactive, options.length]\n );\n\n useEffect(() => {\n if (isMenuOpen) {\n setActiveIndex(prev => {\n if (prev >= 0 && prev < options.length) return prev;\n return selectedIndex >= 0 ? selectedIndex : 0;\n });\n } else {\n setActiveIndex(-1);\n }\n }, [isMenuOpen, options.length, selectedIndex]);\n\n const menuItems: MenuItemData[] = options.map((option, index) => ({\n label: option.label,\n onClick: () => commitSelection(index),\n isSelected: option.value === value,\n }));\n\n const handleToggle = () => {\n if (inactive) return;\n if (isMenuOpen) {\n setIsMenuOpen(false);\n } else {\n openWith(selectedIndex >= 0 ? selectedIndex : 0);\n }\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n if (inactive || options.length === 0) return;\n const last = options.length - 1;\n\n if (!isMenuOpen) {\n switch (event.key) {\n case 'ArrowDown':\n case 'ArrowUp':\n case 'Enter':\n case ' ':\n event.preventDefault();\n openWith(selectedIndex >= 0 ? selectedIndex : 0);\n return;\n case 'Home':\n event.preventDefault();\n openWith(0);\n return;\n case 'End':\n event.preventDefault();\n openWith(last);\n return;\n default:\n return;\n }\n }\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n setActiveIndex(prev => Math.min(last, (prev < 0 ? -1 : prev) + 1));\n break;\n case 'ArrowUp':\n event.preventDefault();\n setActiveIndex(prev => Math.max(0, (prev < 0 ? options.length : prev) - 1));\n break;\n case 'Home':\n event.preventDefault();\n setActiveIndex(0);\n break;\n case 'End':\n event.preventDefault();\n setActiveIndex(last);\n break;\n case 'Enter':\n case ' ':\n event.preventDefault();\n if (activeIndex >= 0) commitSelection(activeIndex);\n break;\n case 'Escape':\n event.preventDefault();\n setIsMenuOpen(false);\n break;\n case 'Tab':\n setIsMenuOpen(false);\n break;\n }\n };\n\n const activeDescendantId =\n isMenuOpen && activeIndex >= 0 ? getOptionId(activeIndex) : undefined;\n\n return (\n <div ref={ref} className={`${styles.selectWrapper} ${className || ''}`}>\n <Surface\n id={triggerId}\n ref={buttonRef as React.RefObject<HTMLElement>}\n as=\"button\"\n type=\"button\"\n elevation=\"elevated\"\n radius=\"sm\"\n interactive\n selected={isMenuOpen}\n className={styles.selectButton}\n onClick={handleToggle}\n onKeyDown={handleKeyDown}\n inactive={inactive}\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={isMenuOpen}\n aria-controls={isMenuOpen ? listboxId : undefined}\n aria-activedescendant={activeDescendantId}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n <Text\n variant=\"text-body-medium\"\n as=\"span\"\n color={selectedOption ? 'primary' : 'secondary'}\n >\n {selectedLabel}\n </Text>\n {ChevronIcon && (\n <span className={styles.chevron}>\n <ChevronIcon size={16} />\n </span>\n )}\n </Surface>\n <Menu\n isOpen={isMenuOpen}\n onClose={() => setIsMenuOpen(false)}\n anchorRef={buttonRef as React.RefObject<HTMLElement | null>}\n items={menuItems}\n side=\"bottom\"\n align=\"start\"\n sideOffset={4}\n alignOffset={-4}\n matchAnchorWidth\n matchAnchorWidthOffset={8}\n role=\"listbox\"\n id={listboxId}\n activeIndex={activeIndex}\n getOptionId={getOptionId}\n />\n </div>\n );\n }\n);\n\nSelect.displayName = 'Select';\n",
|
|
90
90
|
"type": "registry:ui"
|
|
91
91
|
}
|
|
92
92
|
]
|
package/dist/r/tab-group.json
CHANGED
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
"path": "src/components/TabGroup/TabGroup.tsx",
|
|
68
|
-
"content": "import React, { forwardRef, useRef, useEffect, useState, useCallback } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Tab } from '@/components/Tab';\nimport styles from './TabGroup.module.css';\n\nexport interface TabGroupTab {\n label: string;\n id?: string;\n}\n\nexport interface TabGroupProps {\n /** Tab definitions. */\n tabs: TabGroupTab[];\n /** Index of the active tab. */\n activeIndex?: number;\n /** Called when a tab is
|
|
68
|
+
"content": "import React, { forwardRef, useRef, useEffect, useState, useCallback } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Tab } from '@/components/Tab';\nimport styles from './TabGroup.module.css';\n\nexport interface TabGroupTab {\n label: string;\n /** id for the tab. Pair with `panelId` to wire SR semantics on the consumer's tabpanel. */\n id?: string;\n /** id of the tabpanel this tab controls. Forwarded to the Tab as `aria-controls`. */\n panelId?: string;\n disabled?: boolean;\n}\n\nexport interface TabGroupProps {\n /** Tab definitions. */\n tabs: TabGroupTab[];\n /** Index of the active tab. */\n activeIndex?: number;\n /** Called when a tab is activated (click or keyboard). */\n onTabChange?: (index: number) => void;\n /** Tablist orientation. Determines which arrow keys navigate. */\n orientation?: 'horizontal' | 'vertical';\n /** Accessible label for the tablist. */\n 'aria-label'?: string;\n /** id of an external label for the tablist. */\n 'aria-labelledby'?: string;\n className?: string;\n}\n\nexport const TabGroup = forwardRef<HTMLDivElement, TabGroupProps>(\n (\n {\n tabs,\n activeIndex = 0,\n onTabChange,\n orientation = 'horizontal',\n className,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n },\n ref,\n ) => {\n const tabsRef = useRef<(HTMLButtonElement | null)[]>([]);\n const [indicator, setIndicator] = useState({ left: 0, width: 0 });\n\n const updateIndicator = useCallback(() => {\n const el = tabsRef.current[activeIndex];\n if (el) {\n setIndicator({\n left: el.offsetLeft,\n width: el.offsetWidth,\n });\n }\n }, [activeIndex]);\n\n useEffect(() => {\n updateIndicator();\n }, [updateIndicator]);\n\n useEffect(() => {\n const observer = new ResizeObserver(updateIndicator);\n const container = tabsRef.current[0]?.parentElement;\n if (container) observer.observe(container);\n return () => observer.disconnect();\n }, [updateIndicator]);\n\n const findEnabled = useCallback(\n (from: number, step: number): number => {\n const len = tabs.length;\n if (len === 0) return from;\n let i = from;\n for (let n = 0; n < len; n++) {\n i = (i + step + len) % len;\n if (!tabs[i].disabled) return i;\n }\n return from;\n },\n [tabs],\n );\n\n const activate = useCallback(\n (index: number) => {\n if (index !== activeIndex) onTabChange?.(index);\n tabsRef.current[index]?.focus();\n },\n [activeIndex, onTabChange],\n );\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n const isHorizontal = orientation !== 'vertical';\n const nextKey = isHorizontal ? 'ArrowRight' : 'ArrowDown';\n const prevKey = isHorizontal ? 'ArrowLeft' : 'ArrowUp';\n\n let nextIndex: number | null = null;\n if (e.key === nextKey) {\n nextIndex = findEnabled(activeIndex, 1);\n } else if (e.key === prevKey) {\n nextIndex = findEnabled(activeIndex, -1);\n } else if (e.key === 'Home') {\n nextIndex = findEnabled(-1, 1);\n } else if (e.key === 'End') {\n nextIndex = findEnabled(tabs.length, -1);\n }\n\n if (nextIndex !== null) {\n e.preventDefault();\n activate(nextIndex);\n }\n };\n\n return (\n <div\n ref={ref}\n role=\"tablist\"\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n aria-orientation={orientation === 'vertical' ? 'vertical' : undefined}\n onKeyDown={handleKeyDown}\n className={cn(styles.tabGroup, className)}\n >\n <div className={styles.tabsRow}>\n {tabs.map((tab, i) => (\n <Tab\n key={tab.id ?? i}\n ref={el => { tabsRef.current[i] = el; }}\n id={tab.id}\n controls={tab.panelId}\n label={tab.label}\n isSelected={i === activeIndex}\n disabled={tab.disabled}\n onClick={() => onTabChange?.(i)}\n />\n ))}\n </div>\n <div className={styles.track}>\n <div\n className={styles.indicator}\n style={{\n transform: `translateX(${indicator.left}px)`,\n width: indicator.width,\n }}\n />\n </div>\n </div>\n );\n }\n);\n\nTabGroup.displayName = 'TabGroup';\n",
|
|
69
69
|
"type": "registry:ui"
|
|
70
70
|
}
|
|
71
71
|
]
|
package/dist/r/tab.json
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"path": "src/components/Tab/Tab.tsx",
|
|
67
|
-
"content": "import React, { forwardRef } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Text } from '@/components/Text';\nimport styles from './Tab.module.css';\n\nexport type TabBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';\n\nexport interface TabProps {\n id?: string;\n label: string;\n isSelected?: boolean;\n /** Parent surface context. Adjusts hover tokens and fg for tabs on colored backgrounds. */\n background?: TabBackground;\n onClick?: () => void;\n className?: string;\n}\n\nexport const Tab = forwardRef<HTMLButtonElement, TabProps>(\n ({
|
|
67
|
+
"content": "import React, { forwardRef } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Text } from '@/components/Text';\nimport styles from './Tab.module.css';\n\nexport type TabBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';\n\nexport interface TabProps {\n /** id of this tab. Pair with the tabpanel's `aria-labelledby` to wire SR semantics. */\n id?: string;\n /** id of the tabpanel this tab controls. Sets `aria-controls`. */\n controls?: string;\n label: string;\n isSelected?: boolean;\n disabled?: boolean;\n /** Parent surface context. Adjusts hover tokens and fg for tabs on colored backgrounds. */\n background?: TabBackground;\n onClick?: () => void;\n onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;\n onFocus?: React.FocusEventHandler<HTMLButtonElement>;\n /** Override tab order. Defaults to a roving pattern (0 when selected, -1 otherwise). */\n tabIndex?: number;\n className?: string;\n}\n\nexport const Tab = forwardRef<HTMLButtonElement, TabProps>(\n (\n {\n id,\n controls,\n label,\n isSelected = false,\n disabled = false,\n background,\n onClick,\n onKeyDown,\n onFocus,\n tabIndex,\n className,\n },\n ref,\n ) => {\n const bgClass = background && background !== 'faint'\n ? styles[background === 'always-dark' ? 'onAlwaysDark' : `on${background.charAt(0).toUpperCase() + background.slice(1)}`]\n : undefined;\n const effectiveTabIndex = tabIndex ?? (isSelected ? 0 : -1);\n return (\n <button\n ref={ref}\n id={id}\n className={cn(styles.tab, isSelected && styles.tabSelected, bgClass, className)}\n onClick={onClick}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n type=\"button\"\n role=\"tab\"\n aria-selected={isSelected}\n aria-controls={controls}\n aria-disabled={disabled || undefined}\n disabled={disabled}\n tabIndex={effectiveTabIndex}\n >\n <Text variant={isSelected ? 'text-body-medium-emphasis' : 'text-body-medium'} as=\"span\">\n {label}\n </Text>\n </button>\n );\n }\n);\n\nTab.displayName = 'Tab';\n",
|
|
68
68
|
"type": "registry:ui"
|
|
69
69
|
}
|
|
70
70
|
]
|
package/dist/r/toggle.json
CHANGED
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"path": "src/components/Toggle/Toggle.tsx",
|
|
61
|
-
"content": "import React, { forwardRef
|
|
61
|
+
"content": "import React, { forwardRef } from 'react';\nimport { cn } from '@/utils/cn';\nimport styles from './Toggle.module.css';\n\ninterface ToggleBaseProps {\n checked?: boolean;\n onChange?: (checked: boolean) => void;\n inactive?: boolean;\n className?: string;\n}\n\nexport type ToggleProps = ToggleBaseProps &\n (\n | { 'aria-label': string; 'aria-labelledby'?: never }\n | { 'aria-label'?: never; 'aria-labelledby': string }\n );\n\nexport const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(\n (\n {\n checked = false,\n onChange,\n inactive = false,\n className,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n },\n ref,\n ) => {\n const handleClick = () => {\n if (inactive) return;\n onChange?.(!checked);\n };\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n disabled={inactive}\n className={cn(styles.toggle, checked && styles.checked, inactive && styles.inactive, className)}\n onClick={handleClick}\n >\n <span className={styles.thumb} />\n </button>\n );\n },\n);\n\nToggle.displayName = 'Toggle';\n",
|
|
62
62
|
"type": "registry:ui"
|
|
63
63
|
}
|
|
64
64
|
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ForwardRefExoticComponent, RefAttributes, SVGAttributes } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Props accepted by icon components (compatible with @ds-mo/icons).
|
|
4
|
+
*/
|
|
5
|
+
export interface IconProps extends SVGAttributes<SVGSVGElement> {
|
|
6
|
+
size?: number | string;
|
|
7
|
+
color?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* An icon component type (compatible with @ds-mo/icons IconComponent).
|
|
12
|
+
*/
|
|
13
|
+
export type IconComponent = ForwardRefExoticComponent<IconProps & RefAttributes<SVGSVGElement>>;
|
|
14
|
+
//# sourceMappingURL=icons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../src/types/icons.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAErF;;GAEG;AACH,MAAM,WAAW,SAAU,SAAQ,aAAa,CAAC,aAAa,CAAC;IAC7D,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,yBAAyB,CAAC,SAAS,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cn.d.ts","sourceRoot":"","sources":["../../src/utils/cn.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,KAAK,CAAC,EAAE,GAAG,MAAM,CAE5E"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a CSS custom property value from :root as a pixel number.
|
|
3
|
+
* Returns the fallback if the property is not set or not parseable.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getCssSpacePx(property: string, fallback: number): number;
|
|
6
|
+
/**
|
|
7
|
+
* Reads a CSS custom property value from :root as a millisecond number.
|
|
8
|
+
* Handles both "300ms" and "0.3s" formats.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getCssTimeMs(property: string, fallback: number): number;
|
|
11
|
+
//# sourceMappingURL=css-tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"css-tokens.d.ts","sourceRoot":"","sources":["../../src/utils/css-tokens.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKxE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMvE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ds-mo/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CompoMo — composable React UI components styled with TokoMo design tokens",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -54,22 +54,27 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@ds-mo/icons": "^0.
|
|
58
|
-
"@ds-mo/tokens": "^
|
|
57
|
+
"@ds-mo/icons": "^2.0.0",
|
|
58
|
+
"@ds-mo/tokens": "^2.5.0",
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
60
|
-
"@storybook/addon-docs": "^10.3.
|
|
61
|
-
"@storybook/react": "^10.3.
|
|
62
|
-
"@storybook/react-vite": "^10.3.
|
|
63
|
-
"@types/react": "^19.2.
|
|
60
|
+
"@storybook/addon-docs": "^10.3.6",
|
|
61
|
+
"@storybook/react": "^10.3.6",
|
|
62
|
+
"@storybook/react-vite": "^10.3.6",
|
|
63
|
+
"@types/react": "^19.2.14",
|
|
64
64
|
"@types/react-dom": "^19.2.3",
|
|
65
65
|
"@vitejs/plugin-react": "^6.0.1",
|
|
66
|
-
"
|
|
66
|
+
"eslint": "^9.39.4",
|
|
67
|
+
"eslint-plugin-react": "^7.37.5",
|
|
68
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
69
|
+
"globals": "^17.6.0",
|
|
70
|
+
"react": "^19.2.5",
|
|
67
71
|
"react-dom": "^19.2.5",
|
|
68
|
-
"storybook": "^10.3.
|
|
72
|
+
"storybook": "^10.3.6",
|
|
69
73
|
"typescript": "^6.0.3",
|
|
70
|
-
"
|
|
71
|
-
"vite
|
|
72
|
-
"
|
|
74
|
+
"typescript-eslint": "^8.59.1",
|
|
75
|
+
"vite": "^8.0.10",
|
|
76
|
+
"vite-plugin-dts": "^5.0.0",
|
|
77
|
+
"zod": "^4.4.3"
|
|
73
78
|
},
|
|
74
79
|
"engines": {
|
|
75
80
|
"node": ">=18.0.0"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, useState, useRef, useEffect, useCallback } from 'react';
|
|
1
|
+
import React, { forwardRef, useState, useRef, useEffect, useCallback, useId } from 'react';
|
|
2
2
|
import { cn } from '@/utils/cn';
|
|
3
3
|
import { Text } from '@/components/Text';
|
|
4
4
|
import styles from './Accordion.module.css';
|
|
@@ -73,6 +73,9 @@ export const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
|
|
|
73
73
|
({ label, children, isExpanded = false, onToggle, inactive = false, className }, ref) => {
|
|
74
74
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
75
75
|
const [height, setHeight] = useState<number>(0);
|
|
76
|
+
const reactId = useId();
|
|
77
|
+
const triggerId = `${reactId}-trigger`;
|
|
78
|
+
const panelId = `${reactId}-panel`;
|
|
76
79
|
|
|
77
80
|
const measure = useCallback(() => {
|
|
78
81
|
if (contentRef.current) {
|
|
@@ -93,9 +96,12 @@ export const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
|
|
|
93
96
|
<div ref={ref} className={cn(styles.item, inactive && styles.inactive, className)}>
|
|
94
97
|
<button
|
|
95
98
|
type="button"
|
|
99
|
+
id={triggerId}
|
|
96
100
|
className={styles.trigger}
|
|
97
101
|
onClick={() => !inactive && onToggle?.()}
|
|
98
102
|
aria-expanded={isExpanded}
|
|
103
|
+
aria-controls={panelId}
|
|
104
|
+
aria-disabled={inactive || undefined}
|
|
99
105
|
tabIndex={inactive ? -1 : 0}
|
|
100
106
|
>
|
|
101
107
|
<Text variant="text-body-medium-emphasis" as="span" color="inherit">
|
|
@@ -106,6 +112,9 @@ export const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
|
|
|
106
112
|
</span>
|
|
107
113
|
</button>
|
|
108
114
|
<div
|
|
115
|
+
id={panelId}
|
|
116
|
+
role="region"
|
|
117
|
+
aria-labelledby={triggerId}
|
|
109
118
|
className={styles.body}
|
|
110
119
|
style={{ height: isExpanded ? height : 0 }}
|
|
111
120
|
aria-hidden={!isExpanded}
|
|
@@ -97,3 +97,35 @@ export const ZeroCount: Story = {
|
|
|
97
97
|
</div>
|
|
98
98
|
),
|
|
99
99
|
};
|
|
100
|
+
|
|
101
|
+
export const AccessibleLabel: Story = {
|
|
102
|
+
parameters: {
|
|
103
|
+
docs: {
|
|
104
|
+
description: {
|
|
105
|
+
story:
|
|
106
|
+
'Inspect the rendered HTML to see how the accessible name is composed. ' +
|
|
107
|
+
'Default is just the count. Pass `aria-label` for an explicit string, ' +
|
|
108
|
+
'or `labelFormat` for a count-aware formatter.',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
render: () => (
|
|
113
|
+
<div style={{ ...col, fontFamily: 'sans-serif', fontSize: 12 }}>
|
|
114
|
+
<div style={row}>
|
|
115
|
+
<span style={{ ...labelStyle, minWidth: 220 }}>default (just count)</span>
|
|
116
|
+
<Badge count={3} />
|
|
117
|
+
<code style={{ color: '#888' }}>aria-label="3"</code>
|
|
118
|
+
</div>
|
|
119
|
+
<div style={row}>
|
|
120
|
+
<span style={{ ...labelStyle, minWidth: 220 }}>aria-label override</span>
|
|
121
|
+
<Badge count={3} aria-label="3 new notifications" />
|
|
122
|
+
<code style={{ color: '#888' }}>aria-label="3 new notifications"</code>
|
|
123
|
+
</div>
|
|
124
|
+
<div style={row}>
|
|
125
|
+
<span style={{ ...labelStyle, minWidth: 220 }}>labelFormat callback</span>
|
|
126
|
+
<Badge count={3} labelFormat={n => `${n} unread messages`} />
|
|
127
|
+
<code style={{ color: '#888' }}>aria-label="3 unread messages"</code>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
),
|
|
131
|
+
};
|
|
@@ -6,13 +6,30 @@ export interface BadgeProps {
|
|
|
6
6
|
count: number;
|
|
7
7
|
isSelected?: boolean;
|
|
8
8
|
className?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Accessible label announced by screen readers. Takes precedence over `labelFormat`.
|
|
11
|
+
* Provide this when the badge has a specific meaning (e.g. `"3 unread messages"`).
|
|
12
|
+
*/
|
|
13
|
+
'aria-label'?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Formats the accessible label from the count. Used when `aria-label` is not provided.
|
|
16
|
+
* Defaults to returning the count as a string.
|
|
17
|
+
*/
|
|
18
|
+
labelFormat?: (count: number) => string;
|
|
9
19
|
}
|
|
10
20
|
|
|
11
|
-
export const Badge: React.FC<BadgeProps> = ({
|
|
21
|
+
export const Badge: React.FC<BadgeProps> = ({
|
|
22
|
+
count,
|
|
23
|
+
isSelected = false,
|
|
24
|
+
className,
|
|
25
|
+
'aria-label': ariaLabel,
|
|
26
|
+
labelFormat,
|
|
27
|
+
}) => {
|
|
12
28
|
if (count === 0) return null;
|
|
13
29
|
const displayText = count > 9 ? '+' : count.toString();
|
|
30
|
+
const accessibleLabel = ariaLabel ?? labelFormat?.(count) ?? String(count);
|
|
14
31
|
return (
|
|
15
|
-
<span className={`${styles.badge} ${className || ''}`} aria-label={
|
|
32
|
+
<span className={`${styles.badge} ${className || ''}`} aria-label={accessibleLabel}>
|
|
16
33
|
<Text variant="text-caption-emphasis" as="span" color={isSelected ? 'primary' : 'secondary'}>
|
|
17
34
|
{displayText}
|
|
18
35
|
</Text>
|
|
@@ -30,12 +30,50 @@
|
|
|
30
30
|
.bannerBody {
|
|
31
31
|
padding: var(--dimension-space-075);
|
|
32
32
|
min-height: var(--dimension-size-400);
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: flex-start;
|
|
35
|
+
gap: var(--dimension-space-075);
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
.message {
|
|
36
39
|
color: var(--banner-content);
|
|
37
40
|
line-height: var(--typography-lineheight-md);
|
|
38
41
|
display: block;
|
|
42
|
+
flex: 1;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.dismiss {
|
|
47
|
+
background: none;
|
|
48
|
+
border: none;
|
|
49
|
+
margin: 0;
|
|
50
|
+
padding: var(--dimension-space-025);
|
|
51
|
+
color: var(--banner-content);
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
flex-shrink: 0;
|
|
57
|
+
border-radius: var(--dimension-radius-050);
|
|
58
|
+
position: relative;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dismiss::before {
|
|
62
|
+
content: '';
|
|
63
|
+
position: absolute;
|
|
64
|
+
inset: 0;
|
|
65
|
+
border-radius: inherit;
|
|
66
|
+
background-color: transparent;
|
|
67
|
+
transition: var(--effect-transition-interaction-background-short-2);
|
|
68
|
+
pointer-events: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.dismiss:hover::before { background-color: var(--color-interaction-hover); }
|
|
72
|
+
.dismiss:active::before { background-color: var(--color-interaction-pressed); }
|
|
73
|
+
|
|
74
|
+
.dismiss:focus-visible {
|
|
75
|
+
outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);
|
|
76
|
+
outline-offset: var(--dimension-stroke-width-015);
|
|
39
77
|
}
|
|
40
78
|
|
|
41
79
|
.floatingPosition {
|