@ds-mo/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +969 -0
- package/dist/index.js +2623 -0
- package/dist/index.js.map +1 -0
- package/dist/r/accordion.json +79 -0
- package/dist/r/badge.json +69 -0
- package/dist/r/banner.json +90 -0
- package/dist/r/breadcrumb.json +70 -0
- package/dist/r/button-group.json +65 -0
- package/dist/r/button.json +137 -0
- package/dist/r/card.json +82 -0
- package/dist/r/checkbox.json +91 -0
- package/dist/r/divider.json +57 -0
- package/dist/r/empty-state.json +69 -0
- package/dist/r/error-boundary.json +66 -0
- package/dist/r/fade.json +65 -0
- package/dist/r/field.json +71 -0
- package/dist/r/header.json +67 -0
- package/dist/r/input.json +77 -0
- package/dist/r/loader.json +54 -0
- package/dist/r/menu.json +130 -0
- package/dist/r/modal.json +93 -0
- package/dist/r/pagination.json +80 -0
- package/dist/r/radio.json +85 -0
- package/dist/r/registry.json +1600 -0
- package/dist/r/scrollbar.json +68 -0
- package/dist/r/select.json +93 -0
- package/dist/r/sidebar.json +105 -0
- package/dist/r/skeleton.json +72 -0
- package/dist/r/slider.json +87 -0
- package/dist/r/surface.json +97 -0
- package/dist/r/tab-group.json +72 -0
- package/dist/r/tab.json +71 -0
- package/dist/r/table.json +110 -0
- package/dist/r/tag.json +110 -0
- package/dist/r/text.json +94 -0
- package/dist/r/toast.json +77 -0
- package/dist/r/toggle-button-group.json +65 -0
- package/dist/r/toggle-button.json +94 -0
- package/dist/r/toggle.json +65 -0
- package/dist/r/tooltip.json +86 -0
- package/package.json +80 -0
- package/src/components/Accordion/Accordion.module.css +66 -0
- package/src/components/Accordion/Accordion.stories.tsx +95 -0
- package/src/components/Accordion/Accordion.tsx +122 -0
- package/src/components/Accordion/index.ts +2 -0
- package/src/components/Badge/Badge.module.css +6 -0
- package/src/components/Badge/Badge.stories.tsx +99 -0
- package/src/components/Badge/Badge.tsx +23 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/Banner/Banner.module.css +67 -0
- package/src/components/Banner/Banner.stories.tsx +81 -0
- package/src/components/Banner/Banner.tsx +109 -0
- package/src/components/Banner/index.ts +2 -0
- package/src/components/Breadcrumb/Breadcrumb.module.css +46 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +72 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +68 -0
- package/src/components/Breadcrumb/index.ts +2 -0
- package/src/components/Button/Button.module.css +343 -0
- package/src/components/Button/Button.stories.tsx +575 -0
- package/src/components/Button/Button.tsx +201 -0
- package/src/components/Button/index.ts +10 -0
- package/src/components/ButtonGroup/ButtonGroup.module.css +157 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +384 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +122 -0
- package/src/components/ButtonGroup/index.ts +2 -0
- package/src/components/Card/Card.module.css +36 -0
- package/src/components/Card/Card.stories.tsx +78 -0
- package/src/components/Card/Card.tsx +53 -0
- package/src/components/Card/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.module.css +47 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +129 -0
- package/src/components/Checkbox/Checkbox.tsx +68 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/Divider/Divider.module.css +19 -0
- package/src/components/Divider/Divider.stories.tsx +55 -0
- package/src/components/Divider/Divider.tsx +28 -0
- package/src/components/Divider/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.module.css +8 -0
- package/src/components/EmptyState/EmptyState.stories.tsx +53 -0
- package/src/components/EmptyState/EmptyState.tsx +31 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/ErrorBoundary/ErrorBoundary.module.css +18 -0
- package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +38 -0
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +50 -0
- package/src/components/ErrorBoundary/index.ts +2 -0
- package/src/components/Fade/Fade.module.css +31 -0
- package/src/components/Fade/Fade.stories.tsx +35 -0
- package/src/components/Fade/Fade.tsx +33 -0
- package/src/components/Fade/index.ts +2 -0
- package/src/components/Field/Field.module.css +5 -0
- package/src/components/Field/Field.stories.tsx +111 -0
- package/src/components/Field/Field.tsx +22 -0
- package/src/components/Field/index.ts +2 -0
- package/src/components/Header/Header.module.css +33 -0
- package/src/components/Header/Header.stories.tsx +52 -0
- package/src/components/Header/Header.tsx +42 -0
- package/src/components/Header/index.ts +2 -0
- package/src/components/Input/Input.module.css +71 -0
- package/src/components/Input/Input.stories.tsx +135 -0
- package/src/components/Input/Input.tsx +76 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/LabelWrap/LabelWrap.module.css +34 -0
- package/src/components/LabelWrap/LabelWrap.tsx +32 -0
- package/src/components/LabelWrap/index.ts +1 -0
- package/src/components/Loader/Loader.module.css +9 -0
- package/src/components/Loader/Loader.stories.tsx +44 -0
- package/src/components/Loader/Loader.tsx +43 -0
- package/src/components/Loader/index.ts +2 -0
- package/src/components/Menu/DestructiveMenuItem.module.css +57 -0
- package/src/components/Menu/DestructiveMenuItem.tsx +64 -0
- package/src/components/Menu/Menu.module.css +49 -0
- package/src/components/Menu/Menu.stories.tsx +129 -0
- package/src/components/Menu/Menu.tsx +269 -0
- package/src/components/Menu/MenuItem.module.css +84 -0
- package/src/components/Menu/MenuItem.tsx +101 -0
- package/src/components/Menu/index.ts +6 -0
- package/src/components/Modal/Modal.module.css +67 -0
- package/src/components/Modal/Modal.stories.tsx +71 -0
- package/src/components/Modal/Modal.tsx +122 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Pagination/Pagination.module.css +64 -0
- package/src/components/Pagination/Pagination.stories.tsx +51 -0
- package/src/components/Pagination/Pagination.tsx +103 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/Radio/Radio.module.css +61 -0
- package/src/components/Radio/Radio.stories.tsx +101 -0
- package/src/components/Radio/Radio.tsx +108 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/Scrollbar/Scrollbar.module.css +81 -0
- package/src/components/Scrollbar/Scrollbar.stories.tsx +46 -0
- package/src/components/Scrollbar/Scrollbar.tsx +187 -0
- package/src/components/Scrollbar/index.ts +2 -0
- package/src/components/Select/Select.module.css +31 -0
- package/src/components/Select/Select.stories.tsx +59 -0
- package/src/components/Select/Select.tsx +92 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.module.css +161 -0
- package/src/components/Sidebar/Sidebar.stories.tsx +125 -0
- package/src/components/Sidebar/Sidebar.tsx +129 -0
- package/src/components/Sidebar/SidebarItem.tsx +57 -0
- package/src/components/Sidebar/SidebarSection.tsx +42 -0
- package/src/components/Sidebar/index.ts +6 -0
- package/src/components/Skeleton/Skeleton.module.css +53 -0
- package/src/components/Skeleton/Skeleton.stories.tsx +89 -0
- package/src/components/Skeleton/Skeleton.tsx +66 -0
- package/src/components/Skeleton/index.ts +2 -0
- package/src/components/Slider/Slider.module.css +62 -0
- package/src/components/Slider/Slider.stories.tsx +120 -0
- package/src/components/Slider/Slider.tsx +74 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Surface/Surface.module.css +222 -0
- package/src/components/Surface/Surface.stories.tsx +213 -0
- package/src/components/Surface/Surface.tsx +219 -0
- package/src/components/Surface/index.ts +12 -0
- package/src/components/Tab/Tab.module.css +66 -0
- package/src/components/Tab/Tab.stories.tsx +103 -0
- package/src/components/Tab/Tab.tsx +41 -0
- package/src/components/Tab/index.ts +2 -0
- package/src/components/TabGroup/TabGroup.module.css +26 -0
- package/src/components/TabGroup/TabGroup.stories.tsx +60 -0
- package/src/components/TabGroup/TabGroup.tsx +83 -0
- package/src/components/TabGroup/index.ts +2 -0
- package/src/components/Table/Table.module.css +130 -0
- package/src/components/Table/Table.stories.tsx +111 -0
- package/src/components/Table/Table.tsx +226 -0
- package/src/components/Table/index.ts +11 -0
- package/src/components/Tag/Tag.module.css +270 -0
- package/src/components/Tag/Tag.stories.tsx +399 -0
- package/src/components/Tag/Tag.tsx +162 -0
- package/src/components/Tag/index.ts +2 -0
- package/src/components/Text/Text.module.css +166 -0
- package/src/components/Text/Text.stories.tsx +212 -0
- package/src/components/Text/Text.tsx +181 -0
- package/src/components/Text/index.ts +13 -0
- package/src/components/Toast/Toast.module.css +91 -0
- package/src/components/Toast/Toast.stories.tsx +66 -0
- package/src/components/Toast/Toast.tsx +182 -0
- package/src/components/Toast/index.ts +8 -0
- package/src/components/Toggle/Toggle.module.css +87 -0
- package/src/components/Toggle/Toggle.stories.tsx +85 -0
- package/src/components/Toggle/Toggle.tsx +52 -0
- package/src/components/Toggle/index.ts +2 -0
- package/src/components/ToggleButton/ToggleButton.module.css +172 -0
- package/src/components/ToggleButton/ToggleButton.stories.tsx +299 -0
- package/src/components/ToggleButton/ToggleButton.tsx +118 -0
- package/src/components/ToggleButton/index.ts +2 -0
- package/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css +163 -0
- package/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +341 -0
- package/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +111 -0
- package/src/components/ToggleButtonGroup/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.module.css +45 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +46 -0
- package/src/components/Tooltip/Tooltip.tsx +224 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/css-modules.d.ts +4 -0
- package/src/docs/ColorUsage.mdx +195 -0
- package/src/docs/ElevationUsage.mdx +95 -0
- package/src/docs/Introduction.mdx +97 -0
- package/src/docs/OpticalSizing.mdx +178 -0
- package/src/docs/TypographyUsage.mdx +121 -0
- package/src/index.ts +154 -0
- package/src/stories/Colors.stories.tsx +528 -0
- package/src/stories/Dimensions.stories.tsx +200 -0
- package/src/stories/Effects.stories.tsx +231 -0
- package/src/stories/Icons.stories.tsx +159 -0
- package/src/stories/Typography.stories.tsx +157 -0
- package/src/types/icons.ts +15 -0
- package/src/types/index.ts +1 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/css-tokens.ts +22 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "toast",
|
|
4
|
+
"title": "Toast",
|
|
5
|
+
"description": "Imperative toast notification system. Use toast.success()/error()/warning()/info() to show toasts. Place <ToastContainer /> once at the app root.",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@compomo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/CompoMo/?path=/story/toast",
|
|
11
|
+
"consumption": {
|
|
12
|
+
"install": "npm install @compomo/ui @ds-mo/tokens",
|
|
13
|
+
"cssSetup": "Import tokens CSS at your app entry point: import '@ds-mo/tokens/css';\nThis provides all var(--color-*), var(--dimension-*), var(--effect-*), and var(--typography-*) custom properties.\nFor dark mode, set data-theme=\"dark\" on <html>.",
|
|
14
|
+
"import": "import { ToastContainer, toast, useToasts } from '@compomo/ui';",
|
|
15
|
+
"cssImport": "import '@compomo/ui/css';",
|
|
16
|
+
"typeImport": "import type { ToastContainerProps, ToastData, ToastOptions, ToastIntent, ToastPosition } from '@compomo/ui';",
|
|
17
|
+
"internalDependencies": [
|
|
18
|
+
{
|
|
19
|
+
"component": "Surface",
|
|
20
|
+
"note": "Used internally by Toast. No action needed — it's bundled in @compomo/ui."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"component": "Text",
|
|
24
|
+
"note": "Used internally by Toast. No action needed — it's bundled in @compomo/ui."
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"required": [
|
|
29
|
+
"react >=17",
|
|
30
|
+
"react-dom >=17",
|
|
31
|
+
"@ds-mo/tokens >=0.1.0"
|
|
32
|
+
],
|
|
33
|
+
"optional": []
|
|
34
|
+
},
|
|
35
|
+
"notes": "Place <ToastContainer /> once at app root. Then call toast.success(\"Done!\") anywhere."
|
|
36
|
+
},
|
|
37
|
+
"props": {
|
|
38
|
+
"position": {
|
|
39
|
+
"type": "'top-center' | 'top-right' | 'bottom-center' | 'bottom-right'",
|
|
40
|
+
"default": "'top-center'",
|
|
41
|
+
"description": "ToastContainer position prop."
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"exports": [
|
|
45
|
+
"ToastContainer",
|
|
46
|
+
"toast",
|
|
47
|
+
"useToasts"
|
|
48
|
+
],
|
|
49
|
+
"types": [
|
|
50
|
+
"ToastContainerProps",
|
|
51
|
+
"ToastData",
|
|
52
|
+
"ToastOptions",
|
|
53
|
+
"ToastIntent",
|
|
54
|
+
"ToastPosition"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"dependencies": [
|
|
58
|
+
"@compomo/ui",
|
|
59
|
+
"@ds-mo/tokens"
|
|
60
|
+
],
|
|
61
|
+
"registryDependencies": [
|
|
62
|
+
"surface",
|
|
63
|
+
"text"
|
|
64
|
+
],
|
|
65
|
+
"files": [
|
|
66
|
+
{
|
|
67
|
+
"path": "src/components/Toast/Toast.module.css",
|
|
68
|
+
"content": "/* ─── Container ─────────────────────────────────────────────────────────────── */\n\n.container {\n position: fixed;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-075);\n pointer-events: none;\n padding: var(--dimension-space-100);\n max-width: 480px;\n width: 100%;\n box-sizing: border-box;\n}\n\n.topCenter {\n top: 0;\n left: 50%;\n transform: translateX(-50%);\n align-items: center;\n}\n\n.topRight {\n top: 0;\n right: 0;\n align-items: flex-end;\n}\n\n.bottomCenter {\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n align-items: center;\n}\n\n.bottomRight {\n bottom: 0;\n right: 0;\n align-items: flex-end;\n}\n\n/* ─── Toast ─────────────────────────────────────────────────────────────────── */\n\n.toast {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-075);\n padding: var(--dimension-space-075) var(--dimension-space-100);\n min-height: var(--dimension-size-400);\n pointer-events: auto;\n animation: slideIn var(--effect-motion-short-3) ease-out;\n}\n\n.toastExit {\n animation: fadeOut 200ms ease-in forwards;\n}\n\n.dismiss {\n background: none;\n border: none;\n color: inherit;\n cursor: pointer;\n padding: var(--dimension-space-025);\n margin-left: auto;\n font-size: 16px;\n line-height: 1;\n opacity: 0.7;\n border-radius: var(--dimension-radius-050);\n transition: opacity var(--effect-motion-short-2);\n flex-shrink: 0;\n}\n\n.dismiss:hover { opacity: 1; }\n\n/* ─── Animations ────────────────────────────────────────────────────────────── */\n\n@keyframes slideIn {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n",
|
|
69
|
+
"type": "registry:ui"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"path": "src/components/Toast/Toast.tsx",
|
|
73
|
+
"content": "import React, { forwardRef, useEffect, useState, useCallback, useRef, useSyncExternalStore } from 'react';\nimport ReactDOM from 'react-dom';\nimport { cn } from '@/utils/cn';\nimport { Text } from '@/components/Text';\nimport { Surface } from '@/components/Surface';\nimport type { SurfaceIntent } from '@/components/Surface';\nimport styles from './Toast.module.css';\n\n/* ─── Toast Types ───────────────────────────────────────────────────────────── */\n\nexport type ToastIntent = 'neutral' | 'brand' | 'positive' | 'negative' | 'warning' | 'caution';\nexport type ToastPosition = 'top-center' | 'top-right' | 'bottom-center' | 'bottom-right';\n\nexport interface ToastData {\n id: string;\n message: string;\n intent?: ToastIntent;\n duration?: number;\n onDismiss?: () => void;\n}\n\nexport interface ToastOptions {\n message: string;\n intent?: ToastIntent;\n /** Auto-dismiss duration in ms. 0 = no auto-dismiss. Default 4000. */\n duration?: number;\n onDismiss?: () => void;\n}\n\n/* ─── Toast Store (external, framework-agnostic) ────────────────────────────── */\n\nlet toasts: ToastData[] = [];\nconst listeners = new Set<() => void>();\n\nfunction emit() {\n listeners.forEach(fn => fn());\n}\n\nfunction getSnapshot(): ToastData[] {\n return toasts;\n}\n\nfunction subscribe(listener: () => void) {\n listeners.add(listener);\n return () => { listeners.delete(listener); };\n}\n\nlet idCounter = 0;\n\nexport const toast = {\n show(options: ToastOptions): string {\n const id = `toast-${++idCounter}`;\n toasts = [...toasts, { id, ...options }];\n emit();\n return id;\n },\n info(message: string, options?: Omit<ToastOptions, 'message'>): string {\n return toast.show({ message, intent: 'neutral', ...options });\n },\n success(message: string, options?: Omit<ToastOptions, 'message'>): string {\n return toast.show({ message, intent: 'positive', ...options });\n },\n error(message: string, options?: Omit<ToastOptions, 'message'>): string {\n return toast.show({ message, intent: 'negative', ...options });\n },\n warning(message: string, options?: Omit<ToastOptions, 'message'>): string {\n return toast.show({ message, intent: 'warning', ...options });\n },\n dismiss(id: string) {\n toasts = toasts.filter(t => t.id !== id);\n emit();\n },\n dismissAll() {\n toasts = [];\n emit();\n },\n};\n\n/* ─── useToasts hook ────────────────────────────────────────────────────────── */\n\nexport function useToasts(): ToastData[] {\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/* ─── Single Toast ──────────────────────────────────────────────────────────── */\n\nconst INTENT_MAP: Record<ToastIntent, SurfaceIntent> = {\n neutral: 'neutral',\n brand: 'brand',\n positive: 'positive',\n negative: 'negative',\n warning: 'warning',\n caution: 'caution',\n};\n\ninterface ToastItemProps {\n data: ToastData;\n}\n\nconst ToastItem: React.FC<ToastItemProps> = ({ data }) => {\n const [exiting, setExiting] = useState(false);\n const timerRef = useRef<ReturnType<typeof setTimeout>>();\n\n const dismiss = useCallback(() => {\n setExiting(true);\n setTimeout(() => {\n toast.dismiss(data.id);\n data.onDismiss?.();\n }, 200);\n }, [data]);\n\n useEffect(() => {\n const duration = data.duration ?? 4000;\n if (duration > 0) {\n timerRef.current = setTimeout(dismiss, duration);\n }\n return () => { if (timerRef.current) clearTimeout(timerRef.current); };\n }, [data.duration, dismiss]);\n\n return (\n <Surface\n intent={INTENT_MAP[data.intent ?? 'neutral']}\n contrast=\"bold\"\n elevation=\"floating\"\n radius=\"lg\"\n className={cn(styles.toast, exiting && styles.toastExit)}\n role=\"status\"\n aria-live=\"polite\"\n >\n <Text variant=\"text-body-medium\" as=\"span\" color=\"inherit\">\n {data.message}\n </Text>\n <button\n type=\"button\"\n className={styles.dismiss}\n onClick={dismiss}\n aria-label=\"Dismiss\"\n >\n ×\n </button>\n </Surface>\n );\n};\n\n/* ─── ToastContainer ────────────────────────────────────────────────────────── */\n\nexport interface ToastContainerProps {\n /** Where to render toasts on screen. */\n position?: ToastPosition;\n}\n\nexport const ToastContainer = forwardRef<HTMLDivElement, ToastContainerProps>(\n ({ position = 'top-center' }, ref) => {\n const items = useToasts();\n\n if (typeof document === 'undefined') return null;\n\n return ReactDOM.createPortal(\n <div\n ref={ref}\n className={cn(styles.container, styles[positionClass(position)])}\n aria-label=\"Notifications\"\n >\n {items.map(item => (\n <ToastItem key={item.id} data={item} />\n ))}\n </div>,\n document.body\n );\n }\n);\n\nToastContainer.displayName = 'ToastContainer';\n\nfunction positionClass(position: ToastPosition): string {\n switch (position) {\n case 'top-center': return 'topCenter';\n case 'top-right': return 'topRight';\n case 'bottom-center': return 'bottomCenter';\n case 'bottom-right': return 'bottomRight';\n }\n}\n",
|
|
74
|
+
"type": "registry:ui"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "toggle-button-group",
|
|
4
|
+
"title": "ToggleButtonGroup",
|
|
5
|
+
"description": "Groups ToggleButton children with dividers and shared chrome.",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@compomo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/CompoMo/?path=/story/toggle-button-group",
|
|
11
|
+
"consumption": {
|
|
12
|
+
"install": "npm install @compomo/ui @ds-mo/tokens",
|
|
13
|
+
"cssSetup": "Import tokens CSS at your app entry point: import '@ds-mo/tokens/css';\nThis provides all var(--color-*), var(--dimension-*), var(--effect-*), and var(--typography-*) custom properties.\nFor dark mode, set data-theme=\"dark\" on <html>.",
|
|
14
|
+
"import": "import { ToggleButtonGroup } from '@compomo/ui';",
|
|
15
|
+
"cssImport": "import '@compomo/ui/css';",
|
|
16
|
+
"typeImport": "import type { ToggleButtonGroupProps } from '@compomo/ui';",
|
|
17
|
+
"internalDependencies": [
|
|
18
|
+
{
|
|
19
|
+
"component": "ToggleButton",
|
|
20
|
+
"note": "Used internally by ToggleButtonGroup. No action needed — it's bundled in @compomo/ui."
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"required": [
|
|
25
|
+
"react >=17",
|
|
26
|
+
"react-dom >=17",
|
|
27
|
+
"@ds-mo/tokens >=0.1.0"
|
|
28
|
+
],
|
|
29
|
+
"optional": []
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"props": {
|
|
33
|
+
"children": {
|
|
34
|
+
"type": "React.ReactNode",
|
|
35
|
+
"required": true,
|
|
36
|
+
"description": "ToggleButton components as children."
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"exports": [
|
|
40
|
+
"ToggleButtonGroup"
|
|
41
|
+
],
|
|
42
|
+
"types": [
|
|
43
|
+
"ToggleButtonGroupProps"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"dependencies": [
|
|
47
|
+
"@compomo/ui",
|
|
48
|
+
"@ds-mo/tokens"
|
|
49
|
+
],
|
|
50
|
+
"registryDependencies": [
|
|
51
|
+
"toggle-button"
|
|
52
|
+
],
|
|
53
|
+
"files": [
|
|
54
|
+
{
|
|
55
|
+
"path": "src/components/ToggleButtonGroup/ToggleButtonGroup.module.css",
|
|
56
|
+
"content": "/* ==========================================================================\n ToggleButtonGroup — flush row of ToggleButton components\n ========================================================================== */\n\n.group {\n display: inline-flex;\n align-items: stretch;\n border-radius: var(--dimension-radius-050);\n position: relative;\n}\n\n/* Inset ring / highlight overlay — sits above button children so it isn't\n covered by their backgrounds. Outset shadow stays on the element itself. */\n.group::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n}\n\n/* ── Group-level elevation ──────────────────────────────────────────────── */\n/* Individual item chrome (shadow/border) is suppressed via .item below.\n The group container carries the elevation so it wraps the whole control. */\n\n/* Outset shadow on element; inset highlight on ::after (children would cover it otherwise) */\n.groupElevated { box-shadow: var(--effect-shadow-elevated-sm); }\n.groupElevated::after { box-shadow: var(--effect-highlight-elevated-sm); }\n\n/* Flat ring lives entirely on ::after — same reasoning as highlight */\n.groupFlat::after { box-shadow: inset 0 0 0 1px var(--color-border-tertiary); }\n\n.groupFloating { box-shadow: var(--effect-shadow-elevated-floating); }\n.groupFloating::after { box-shadow: var(--effect-highlight-elevated-floating); }\n/* groupNone = ghost — no shadow, no border */\n\n.groupRounded { border-radius: var(--dimension-radius-half); }\n\n/* ── Base item ─────────────────────────────────────────────────────────────── */\n\n.item {\n flex-shrink: 0;\n /* Suppress individual elevation so the group reads as one unit */\n box-shadow: none !important;\n /* Suppress noElevation individual border — divider handles separation */\n border: none !important;\n /* Suppress scale-on-press: transform would detach the divider from the\n adjacent button, creating a visible gap. Pressed state is still\n communicated via the ::after overlay colour. */\n}\n.item:active {\n transform: none !important;\n}\n\n/* ── Divider ────────────────────────────────────────────────────────────────\n * Rendered as a separate <div> between items so it is never part of any\n * button's stacking context — disabled (opacity: 0.5) or other state changes\n * on adjacent buttons cannot bleed through to the divider.\n * ─────────────────────────────────────────────────────────────────────────── */\n\n/* Full-height divider (elevated / flat / floating).\n * Rendered as a separate element (independent of adjacent button opacity),\n * then nudged ±0.5px so it overlaps each neighbour and paints on top via\n * z-index — this lets the alpha token (rgb(0 0 0 / 0.1)) composite over\n * whatever fill the buttons carry (white, blue pressed state, etc.). */\n.divider {\n width: 1px;\n flex-shrink: 0;\n background-color: var(--color-border-tertiary);\n position: relative;\n z-index: 1;\n margin: 0 -0.5px;\n pointer-events: none;\n}\n\n/* Ghost (elevation=none): short centred divider at icon height */\n.dividerGhost { height: 20px; align-self: center; } /* md default */\n.dividerGhostXS { height: 12px; }\n.dividerGhostSM { height: 16px; }\n\n/* Ghost hover-fade: hide the divider on either side of a hovered ghost button */\n.item:hover + .dividerGhost,\n.dividerGhost:has(+ .item:hover) { opacity: 0; }\n\n/* Ghost pressed-state: hide the divider on either side of a selected button */\n.itemPressed + .dividerGhost,\n.dividerGhost:has(+ .itemPressed) { opacity: 0; }\n\n\n/* ── Ghost interaction states: rounded chips ──────────────────────────────────\n * Ghost items have no connecting chrome — hover/press overlay and selected\n * state always appear as independent rounded chips (4px on all corners).\n * Rounded groups: outer corners of first/last become pill; inner stay 4px.\n *\n * Uses explicit longhands throughout to avoid shorthand/longhand cascade\n * ordering ambiguity with the !important rules in .first / .last / .middle.\n * ─────────────────────────────────────────────────────────────────────────── */\n\n/* ::after hover/press overlay — all positions: 4px all corners.\n Uses .group as ancestor (2-class specificity) to reliably beat\n .first / .last / .middle (1-class) regardless of load order. */\n.group .itemShort::after {\n border-top-left-radius: var(--dimension-radius-050) !important;\n border-top-right-radius: var(--dimension-radius-050) !important;\n border-bottom-right-radius: var(--dimension-radius-050) !important;\n border-bottom-left-radius: var(--dimension-radius-050) !important;\n}\n\n/* Selected chip — all positions: 4px all corners */\n.group .itemPressed {\n border-top-left-radius: var(--dimension-radius-050) !important;\n border-top-right-radius: var(--dimension-radius-050) !important;\n border-bottom-right-radius: var(--dimension-radius-050) !important;\n border-bottom-left-radius: var(--dimension-radius-050) !important;\n}\n\n/* ── Border radius: flatten only the inner-facing corners ──────────────────── */\n/* Outer corners keep the button's own radius — works for default + rounded */\n\n.first {\n border-top-right-radius: 0 !important;\n border-bottom-right-radius: 0 !important;\n}\n\n.last {\n border-top-left-radius: 0 !important;\n border-bottom-left-radius: 0 !important;\n}\n\n.middle {\n border-radius: 0 !important;\n}\n\n/* Rounded group: first — left corners become pill (3-class specificity) */\n.groupRounded .first.itemShort::after,\n.groupRounded .first.itemPressed {\n border-top-left-radius: var(--dimension-radius-half) !important;\n border-bottom-left-radius: var(--dimension-radius-half) !important;\n}\n\n/* Rounded group: last — right corners become pill (3-class specificity) */\n.groupRounded .last.itemShort::after,\n.groupRounded .last.itemPressed {\n border-top-right-radius: var(--dimension-radius-half) !important;\n border-bottom-right-radius: var(--dimension-radius-half) !important;\n}\n\n/* ── Rounded: inner-edge padding correction ────────────────────────────────── */\n/*\n * Rounded buttons add `space-050` extra padding on each side.\n * In a group the inner-facing edge should use base (non-pill) padding instead.\n * XS rounded has no extra padding so needs no correction.\n */\n\n/* MD — base padding: space-100 */\n.roundedFirstMD { padding-right: var(--dimension-space-100) !important; }\n.roundedLastMD { padding-left: var(--dimension-space-100) !important; }\n.roundedMidMD { padding-left: var(--dimension-space-100) !important; padding-right: var(--dimension-space-100) !important; }\n\n/* SM — base padding: space-075 */\n.roundedFirstSM { padding-right: var(--dimension-space-075) !important; }\n.roundedLastSM { padding-left: var(--dimension-space-075) !important; }\n.roundedMidSM { padding-left: var(--dimension-space-075) !important; padding-right: var(--dimension-space-075) !important; }\n",
|
|
57
|
+
"type": "registry:ui"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"path": "src/components/ToggleButtonGroup/ToggleButtonGroup.tsx",
|
|
61
|
+
"content": "import React from 'react';\nimport { cn } from '@/utils/cn';\nimport styles from './ToggleButtonGroup.module.css';\n\nexport interface ToggleButtonGroupProps {\n children: React.ReactNode;\n className?: string;\n}\n\n/**\n * Renders ToggleButton children flush side-by-side with a 1px divider between them.\n * Outer corners keep the button's own border-radius; inner corners are flattened.\n *\n * Individual button chrome (shadow/border) is suppressed — the group container\n * carries the elevation so it wraps the whole control as one visual unit.\n *\n * Divider behaviour by elevation:\n * elevated / flat / floating — full-height color-border-tertiary line\n * none (ghost) — short centred line (icon/text height)\n *\n * Rounded groups: outer edges keep pill padding; inner edges revert to base padding.\n * Pressed state and onPressedChange are managed by the consumer on each ToggleButton.\n */\nexport const ToggleButtonGroup = ({ children, className }: ToggleButtonGroupProps) => {\n const items = React.Children.toArray(children).filter(React.isValidElement);\n const count = items.length;\n\n // Read elevation + rounded from first child to drive the group container style.\n const firstEl = items[0] as React.ReactElement<Record<string, unknown>> | undefined;\n const firstProps = firstEl?.props as { elevation?: string; rounded?: boolean } | undefined;\n const groupElevation = firstProps?.elevation ?? 'elevated';\n const groupRounded = !!firstProps?.rounded;\n const groupElevKey = groupElevation.charAt(0).toUpperCase() + groupElevation.slice(1);\n\n return (\n <div\n className={cn(\n styles.group,\n (styles as Record<string, string>)[`group${groupElevKey}`],\n groupRounded && styles.groupRounded,\n className,\n )}\n role=\"group\"\n >\n {items.map((child, i) => {\n const el = child as React.ReactElement<Record<string, unknown>>;\n const p = el.props as {\n className?: string;\n elevation?: string;\n size?: string;\n rounded?: boolean;\n pressed?: boolean;\n icon?: unknown;\n label?: unknown;\n };\n\n const isFirst = i === 0;\n const isLast = i === count - 1;\n const isMid = !isFirst && !isLast;\n\n const size = (p.size ?? 'md') as 'xs' | 'sm' | 'md';\n const isRounded = !!p.rounded;\n const isPressed = !!p.pressed;\n const isIconOnly = !!p.icon && !p.label;\n // Ghost: elevation='none' (no chrome) → short centred divider\n const isGhost = (p.elevation ?? 'elevated') === 'none';\n\n // XS rounded has no extra padding in ToggleButton so needs no correction\n const sizeKey = size.toUpperCase() as 'XS' | 'SM' | 'MD';\n const needsRoundedCorrection = isRounded && !isIconOnly && size !== 'xs';\n\n // Divider injected as a separate element so it's never affected by\n // the adjacent button's opacity (e.g. disabled state).\n const dividerClasses = cn(\n styles.divider,\n isGhost && styles.dividerGhost,\n isGhost && size === 'xs' && styles.dividerGhostXS,\n isGhost && size === 'sm' && styles.dividerGhostSM,\n );\n\n return (\n <React.Fragment key={i}>\n {i > 0 && <div className={dividerClasses} aria-hidden=\"true\" />}\n {React.cloneElement(el, {\n className: cn(\n p.className,\n styles.item,\n isFirst && styles.first,\n isLast && styles.last,\n isMid && styles.middle,\n\n // Ghost variant: ::after overlay gets 4px radius treatment\n isGhost && styles.itemShort,\n\n // Ghost + pressed: selected state background chip\n isGhost && isPressed && styles.itemPressed,\n\n // Rounded: revert inner-edge padding to base (non-pill) value\n needsRoundedCorrection && isFirst && (styles as Record<string, string>)[`roundedFirst${sizeKey}`],\n needsRoundedCorrection && isLast && (styles as Record<string, string>)[`roundedLast${sizeKey}`],\n needsRoundedCorrection && isMid && (styles as Record<string, string>)[`roundedMid${sizeKey}`],\n ),\n })}\n </React.Fragment>\n );\n })}\n </div>\n );\n};\n\nToggleButtonGroup.displayName = 'ToggleButtonGroup';\n",
|
|
62
|
+
"type": "registry:ui"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "toggle-button",
|
|
4
|
+
"title": "ToggleButton",
|
|
5
|
+
"description": "Controlled toggle button with pressed state, icon and label support.",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@compomo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/CompoMo/?path=/story/toggle-button",
|
|
11
|
+
"consumption": {
|
|
12
|
+
"install": "npm install @compomo/ui @ds-mo/tokens @ds-mo/icons",
|
|
13
|
+
"cssSetup": "Import tokens CSS at your app entry point: import '@ds-mo/tokens/css';\nThis provides all var(--color-*), var(--dimension-*), var(--effect-*), and var(--typography-*) custom properties.\nFor dark mode, set data-theme=\"dark\" on <html>.",
|
|
14
|
+
"import": "import { ToggleButton } from '@compomo/ui';",
|
|
15
|
+
"cssImport": "import '@compomo/ui/css';",
|
|
16
|
+
"typeImport": "import type { ToggleButtonProps, ToggleButtonElevation, ToggleButtonSize } from '@compomo/ui';",
|
|
17
|
+
"iconImport": "import { IconName } from '@ds-mo/icons'; // Tree-shakeable, import only what you need",
|
|
18
|
+
"internalDependencies": [
|
|
19
|
+
{
|
|
20
|
+
"component": "Text",
|
|
21
|
+
"note": "Used internally by ToggleButton. No action needed — it's bundled in @compomo/ui."
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"required": [
|
|
26
|
+
"react >=17",
|
|
27
|
+
"react-dom >=17",
|
|
28
|
+
"@ds-mo/tokens >=0.1.0"
|
|
29
|
+
],
|
|
30
|
+
"optional": [
|
|
31
|
+
"@ds-mo/icons >=0.1.0 (for icon props)"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"props": {
|
|
36
|
+
"pressed": {
|
|
37
|
+
"type": "boolean",
|
|
38
|
+
"required": true
|
|
39
|
+
},
|
|
40
|
+
"onPressedChange": {
|
|
41
|
+
"type": "(pressed: boolean) => void",
|
|
42
|
+
"required": true
|
|
43
|
+
},
|
|
44
|
+
"label": {
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"icon": {
|
|
48
|
+
"type": "IconComponent"
|
|
49
|
+
},
|
|
50
|
+
"size": {
|
|
51
|
+
"type": "'md' | 'sm' | 'xs'",
|
|
52
|
+
"default": "'md'"
|
|
53
|
+
},
|
|
54
|
+
"elevation": {
|
|
55
|
+
"type": "'none' | 'flat' | 'elevated' | 'floating'",
|
|
56
|
+
"default": "'elevated'"
|
|
57
|
+
},
|
|
58
|
+
"rounded": {
|
|
59
|
+
"type": "boolean"
|
|
60
|
+
},
|
|
61
|
+
"inactive": {
|
|
62
|
+
"type": "boolean"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"exports": [
|
|
66
|
+
"ToggleButton"
|
|
67
|
+
],
|
|
68
|
+
"types": [
|
|
69
|
+
"ToggleButtonProps",
|
|
70
|
+
"ToggleButtonElevation",
|
|
71
|
+
"ToggleButtonSize"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"dependencies": [
|
|
75
|
+
"@compomo/ui",
|
|
76
|
+
"@ds-mo/tokens",
|
|
77
|
+
"@ds-mo/icons"
|
|
78
|
+
],
|
|
79
|
+
"registryDependencies": [
|
|
80
|
+
"text"
|
|
81
|
+
],
|
|
82
|
+
"files": [
|
|
83
|
+
{
|
|
84
|
+
"path": "src/components/ToggleButton/ToggleButton.module.css",
|
|
85
|
+
"content": "/* ==========================================================================\n ToggleButton — hover/pressed via direct background-color + color-mix().\n No pseudo-element overlays.\n Elevation controls the chrome level:\n elevated — bg-primary + shadow (default)\n flat — border only\n none — ghost (transparent, no border, no shadow)\n floating — bg-primary + FAB-strength shadow\n ========================================================================== */\n\n.toggleButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: var(--dimension-radius-050);\n cursor: pointer;\n position: relative;\n transition: transform var(--effect-motion-short-2);\n height: var(--dimension-size-400);\n font-family: inherit;\n background: transparent;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n\n /* Interaction — ghost defaults */\n --_hover-bg: var(--color-interaction-hover);\n --_pressed-bg: var(--color-interaction-pressed);\n --_border: var(--color-border-tertiary);\n}\n\n.toggleButton:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n\n.toggleButton:hover:not(.inactive) {\n background-color: var(--_hover-bg);\n}\n\n.toggleButton:active:not(.inactive) {\n background-color: var(--_pressed-bg);\n transform: scale(var(--dimension-scale-subtle));\n}\n\n/* ==========================================================================\n Background context — parent surface awareness\n ========================================================================== */\n\n.onMedium {\n --_hover-bg: var(--color-interaction-on-medium-background-hover);\n --_pressed-bg: var(--color-interaction-on-medium-background-pressed);\n --_border: var(--color-border-on-medium-background-tertiary);\n}\n\n.onBold {\n --_hover-bg: var(--color-interaction-on-bold-background-hover);\n --_pressed-bg: var(--color-interaction-on-bold-background-pressed);\n --_border: var(--color-border-on-bold-background-tertiary);\n}\n\n.onStrong {\n --_hover-bg: var(--color-interaction-on-strong-background-hover);\n --_pressed-bg: var(--color-interaction-on-strong-background-pressed);\n --_border: var(--color-border-on-strong-background-tertiary);\n}\n\n.onAlwaysDark {\n --_hover-bg: var(--color-always-dark-interaction-hover);\n --_pressed-bg: var(--color-always-dark-interaction-pressed);\n --_border: var(--color-always-dark-border-tertiary);\n}\n\n/* ==========================================================================\n Elevation levels\n ========================================================================== */\n\n/* ── Elevated (default): bg-primary + shadow ───────────────────────────── */\n\n.elevationElevated {\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-sm);\n color: var(--color-foreground-secondary);\n --_hover-bg: color-mix(in oklch, var(--color-background-primary) 90%, var(--color-foreground-primary));\n --_pressed-bg: color-mix(in oklch, var(--color-background-primary) 85%, var(--color-foreground-primary));\n}\n\n/* ── Flat: border only, transparent bg ─────────────────────────────────── */\n\n.elevationFlat {\n box-shadow: inset 0 0 0 1px var(--_border);\n color: var(--color-foreground-secondary);\n}\n\n/* ── None: ghost — transparent, no border, no shadow ───────────────────── */\n\n.elevationNone {\n color: var(--color-foreground-secondary);\n}\n\n/* ── Floating: bg-primary + FAB-strength shadow ─────────────────────────── */\n\n.elevationFloating {\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-floating);\n color: var(--color-foreground-secondary);\n --_hover-bg: color-mix(in oklch, var(--color-background-primary) 90%, var(--color-foreground-primary));\n --_pressed-bg: color-mix(in oklch, var(--color-background-primary) 85%, var(--color-foreground-primary));\n}\n\n/* ── Fg on bold / strong surface ────────────────────────────────────────── */\n\n.onBold.elevationNone, .onBold.elevationFlat,\n.onStrong.elevationNone, .onStrong.elevationFlat {\n color: var(--color-foreground-on-bold-background-secondary);\n}\n\n/* ── Fg on always-dark surface ─────────────────────────────────────────── */\n\n.onAlwaysDark.elevationNone, .onAlwaysDark.elevationFlat {\n color: var(--color-always-dark-foreground-secondary);\n}\n\n/* ==========================================================================\n Pressed state — shared across all elevation levels\n ========================================================================== */\n\n.pressed {\n background-color: var(--color-background-faint-brand);\n color: var(--color-foreground-bold-brand);\n}\n\n/* ==========================================================================\n Layout\n ========================================================================== */\n\n.iconOnly { padding: 0; width: var(--dimension-size-400); min-width: var(--dimension-size-400); }\n.labelOnly { padding: 0 var(--dimension-space-100); }\n.iconAndLabel{ padding: 0 var(--dimension-space-100) 0 var(--dimension-space-075); gap: var(--dimension-space-025); }\n\n/* ==========================================================================\n Sizes\n ========================================================================== */\n\n.sizeSM { height: var(--dimension-size-300); }\n.sizeSM.iconOnly { width: var(--dimension-size-300); min-width: var(--dimension-size-300); }\n.sizeSM.labelOnly { padding: 0 var(--dimension-space-075); }\n.sizeSM.iconAndLabel { padding: 0 var(--dimension-space-075) 0 var(--dimension-space-050); gap: 0; }\n\n.sizeXS { height: var(--dimension-size-200); }\n.sizeXS.iconOnly { width: var(--dimension-size-200); min-width: var(--dimension-size-200); }\n.sizeXS.labelOnly, .sizeXS.iconAndLabel { padding: 0 var(--dimension-space-025); gap: 0; }\n\n/* ==========================================================================\n Rounded (pill shape)\n ========================================================================== */\n\n.rounded { border-radius: var(--dimension-radius-half); }\n.rounded.labelOnly { padding-left: calc(var(--dimension-space-100) + var(--dimension-space-050)); padding-right: calc(var(--dimension-space-100) + var(--dimension-space-050)); }\n.rounded.iconAndLabel { padding-left: calc(var(--dimension-space-075) + var(--dimension-space-050)); padding-right: calc(var(--dimension-space-100) + var(--dimension-space-050)); }\n.rounded.sizeSM.labelOnly { padding-left: calc(var(--dimension-space-075) + var(--dimension-space-050)); padding-right: calc(var(--dimension-space-075) + var(--dimension-space-050)); }\n.rounded.sizeSM.iconAndLabel { padding-left: calc(var(--dimension-space-050) + var(--dimension-space-050)); padding-right: calc(var(--dimension-space-075) + var(--dimension-space-050)); }\n.rounded.iconOnly { width: var(--dimension-size-400); min-width: var(--dimension-size-400); padding: 0; }\n.rounded.sizeSM.iconOnly { width: var(--dimension-size-300); min-width: var(--dimension-size-300); }\n.rounded.sizeXS.iconOnly { width: var(--dimension-size-200); min-width: var(--dimension-size-200); }\n\n/* ==========================================================================\n Disabled\n ========================================================================== */\n\n.inactive { opacity: 0.5; cursor: not-allowed; pointer-events: none; }\n",
|
|
86
|
+
"type": "registry:ui"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"path": "src/components/ToggleButton/ToggleButton.tsx",
|
|
90
|
+
"content": "import React, { forwardRef } from 'react';\nimport { cn } from '@/utils/cn';\nimport { Text } from '@/components/Text';\nimport { LabelWrap } from '@/components/LabelWrap';\nimport type { IconComponent } from '@/types/icons';\nimport styles from './ToggleButton.module.css';\n\nexport type ToggleButtonElevation = 'none' | 'flat' | 'elevated' | 'floating';\nexport type ToggleButtonSize = 'md' | 'sm' | 'xs';\nexport type ToggleButtonBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';\n\nexport interface ToggleButtonProps {\n /**\n * Chrome level.\n * none — ghost (transparent, no border, no shadow)\n * flat — border only, transparent bg\n * elevated — bg-primary + shadow [default]\n * floating — bg-primary + FAB-strength shadow\n */\n elevation?: ToggleButtonElevation;\n /** Label text. */\n label?: string;\n /** Leading icon component. */\n icon?: IconComponent;\n /** Size: md (32px), sm (24px), xs (16px). Defaults to 'md'. */\n size?: ToggleButtonSize;\n /** Pill shape. */\n rounded?: boolean;\n /** Parent surface context. Adjusts hover tokens for toggle buttons on colored backgrounds. */\n background?: ToggleButtonBackground;\n /** Controlled pressed state. */\n pressed: boolean;\n /** Called when pressed state changes. */\n onPressedChange: (pressed: boolean) => void;\n inactive?: boolean;\n className?: string;\n id?: string;\n 'aria-label'?: string;\n}\n\nconst ICON_SIZE_MAP: Record<ToggleButtonSize, number> = { md: 20, sm: 16, xs: 12 };\n\nconst TEXT_STYLE_MAP: Record<ToggleButtonSize, string> = {\n md: 'text-body-medium-emphasis',\n sm: 'text-body-small-emphasis',\n xs: 'text-caption-emphasis',\n};\n\nexport const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(\n (\n {\n elevation = 'elevated',\n label,\n icon: Icon,\n size = 'md',\n rounded = false,\n background,\n pressed,\n onPressedChange,\n inactive = false,\n className,\n id,\n 'aria-label': ariaLabel,\n },\n ref\n ) => {\n const hasIcon = !!Icon;\n const hasLabel = !!label;\n const isIconOnly = hasIcon && !hasLabel;\n const isLabelOnly = hasLabel && !hasIcon;\n const isIconAndLabel = hasIcon && hasLabel;\n const iconSize = ICON_SIZE_MAP[size];\n const textStyle = TEXT_STYLE_MAP[size];\n\n const elevKey = elevation.charAt(0).toUpperCase() + elevation.slice(1);\n\n const classes = cn(\n styles.toggleButton,\n styles[`elevation${elevKey}`],\n size !== 'md' && styles[`size${size.toUpperCase()}`],\n rounded && styles.rounded,\n inactive && styles.inactive,\n isIconOnly && styles.iconOnly,\n isLabelOnly && styles.labelOnly,\n isIconAndLabel && styles.iconAndLabel,\n background && background !== 'faint' && styles[\n background === 'always-dark' ? 'onAlwaysDark'\n : `on${background.charAt(0).toUpperCase() + background.slice(1)}`\n ],\n pressed && styles.pressed,\n className,\n );\n\n return (\n <button\n ref={ref}\n id={id}\n className={classes}\n onClick={inactive ? undefined : () => onPressedChange(!pressed)}\n type=\"button\"\n disabled={inactive}\n aria-label={ariaLabel || label || 'toggle'}\n aria-pressed={pressed}\n >\n {Icon && <Icon size={iconSize} />}\n {label && (\n <LabelWrap>\n <Text variant={textStyle as never} as=\"span\" color=\"inherit\">\n {label}\n </Text>\n </LabelWrap>\n )}\n </button>\n );\n }\n);\n\nToggleButton.displayName = 'ToggleButton';\n",
|
|
91
|
+
"type": "registry:ui"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "toggle",
|
|
4
|
+
"title": "Toggle",
|
|
5
|
+
"description": "Switch control with on/off state and accessible role=\"switch\".",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@compomo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/CompoMo/?path=/story/toggle",
|
|
11
|
+
"consumption": {
|
|
12
|
+
"install": "npm install @compomo/ui @ds-mo/tokens",
|
|
13
|
+
"cssSetup": "Import tokens CSS at your app entry point: import '@ds-mo/tokens/css';\nThis provides all var(--color-*), var(--dimension-*), var(--effect-*), and var(--typography-*) custom properties.\nFor dark mode, set data-theme=\"dark\" on <html>.",
|
|
14
|
+
"import": "import { Toggle } from '@compomo/ui';",
|
|
15
|
+
"cssImport": "import '@compomo/ui/css';",
|
|
16
|
+
"typeImport": "import type { ToggleProps } from '@compomo/ui';",
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"required": [
|
|
19
|
+
"react >=17",
|
|
20
|
+
"react-dom >=17",
|
|
21
|
+
"@ds-mo/tokens >=0.1.0"
|
|
22
|
+
],
|
|
23
|
+
"optional": []
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"props": {
|
|
27
|
+
"checked": {
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"default": "false"
|
|
30
|
+
},
|
|
31
|
+
"onChange": {
|
|
32
|
+
"type": "(checked: boolean) => void"
|
|
33
|
+
},
|
|
34
|
+
"inactive": {
|
|
35
|
+
"type": "boolean"
|
|
36
|
+
},
|
|
37
|
+
"aria-label": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"exports": [
|
|
42
|
+
"Toggle"
|
|
43
|
+
],
|
|
44
|
+
"types": [
|
|
45
|
+
"ToggleProps"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"dependencies": [
|
|
49
|
+
"@compomo/ui",
|
|
50
|
+
"@ds-mo/tokens"
|
|
51
|
+
],
|
|
52
|
+
"registryDependencies": [],
|
|
53
|
+
"files": [
|
|
54
|
+
{
|
|
55
|
+
"path": "src/components/Toggle/Toggle.module.css",
|
|
56
|
+
"content": ".toggle {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: flex-start;\n width: var(--dimension-size-400);\n height: var(--dimension-size-250);\n padding: 0 var(--dimension-space-025);\n border: none;\n border-radius: var(--dimension-radius-half);\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n outline: none;\n flex-shrink: 0;\n background-color: var(--color-background-medium-neutral);\n box-shadow: var(--effect-highlight-depressed-sm);\n transition: background-color var(--effect-motion-short-2);\n}\n\n.toggle::before {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n box-shadow: var(--effect-shadow-depressed-sm);\n pointer-events: none;\n}\n\n.toggle:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n\n.checked {\n background-color: var(--color-foreground-bold-brand) !important;\n}\n\n.thumb {\n position: relative;\n flex-shrink: 0;\n width: var(--dimension-size-200);\n height: var(--dimension-size-200);\n background-color: var(--color-background-primary);\n border-radius: var(--dimension-radius-half);\n box-shadow: var(--effect-elevation-elevated-sm);\n transition: transform var(--effect-motion-short-2);\n transform: translateX(0);\n z-index: 1;\n}\n\n.thumb::after {\n content: '';\n position: absolute;\n inset: 0;\n background-color: transparent;\n border-radius: inherit;\n transition: var(--effect-transition-interaction-background-instant);\n pointer-events: none;\n}\n\n.toggle:hover:not(.inactive) .thumb::after {\n background-color: var(--color-interaction-hover);\n}\n\n.toggle:active:not(.inactive) .thumb::after {\n background-color: var(--color-interaction-pressed);\n}\n\n.checked .thumb {\n transform: translateX(var(--dimension-offset-150));\n}\n\n.toggle:active:not(.inactive) .thumb {\n transform: translateX(0) scale(var(--dimension-scale-subtle)) !important;\n transition: transform var(--effect-motion-instant);\n}\n\n.checked.toggle:active:not(.inactive) .thumb {\n transform: translateX(var(--dimension-offset-150)) scale(var(--dimension-scale-subtle)) !important;\n transition: transform var(--effect-motion-instant);\n}\n\n.inactive {\n cursor: not-allowed;\n opacity: 0.5;\n}\n",
|
|
57
|
+
"type": "registry:ui"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"path": "src/components/Toggle/Toggle.tsx",
|
|
61
|
+
"content": "import React, { forwardRef, useState } from 'react';\nimport { cn } from '@/utils/cn';\nimport styles from './Toggle.module.css';\n\nexport interface ToggleProps {\n checked?: boolean;\n onChange?: (checked: boolean) => void;\n inactive?: boolean;\n className?: string;\n 'aria-label'?: string;\n}\n\nexport const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(\n ({ checked = false, onChange, inactive = false, className, 'aria-label': ariaLabel }, ref) => {\n const [isPressed, setIsPressed] = useState(false);\n\n const handleMouseDown = (e: React.MouseEvent) => {\n if (inactive) return;\n e.preventDefault();\n setIsPressed(true);\n };\n\n const handleMouseUp = () => {\n if (inactive || !isPressed) return;\n setIsPressed(false);\n onChange?.(!checked);\n };\n\n const handleMouseLeave = () => {\n if (isPressed) setIsPressed(false);\n };\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={ariaLabel}\n disabled={inactive}\n className={cn(styles.toggle, checked && styles.checked, inactive && styles.inactive, className)}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onMouseLeave={handleMouseLeave}\n >\n <span className={styles.thumb} />\n </button>\n );\n }\n);\n\nToggle.displayName = 'Toggle';\n",
|
|
62
|
+
"type": "registry:ui"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tooltip",
|
|
4
|
+
"title": "Tooltip",
|
|
5
|
+
"description": "Positioned tooltip with portal rendering, delay, keyboard shortcut display, and viewport clamping.",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@compomo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/CompoMo/?path=/story/tooltip",
|
|
11
|
+
"consumption": {
|
|
12
|
+
"install": "npm install @compomo/ui @ds-mo/tokens",
|
|
13
|
+
"cssSetup": "Import tokens CSS at your app entry point: import '@ds-mo/tokens/css';\nThis provides all var(--color-*), var(--dimension-*), var(--effect-*), and var(--typography-*) custom properties.\nFor dark mode, set data-theme=\"dark\" on <html>.",
|
|
14
|
+
"import": "import { Tooltip } from '@compomo/ui';",
|
|
15
|
+
"cssImport": "import '@compomo/ui/css';",
|
|
16
|
+
"typeImport": "import type { TooltipProps, TooltipSide, TooltipAlign } from '@compomo/ui';",
|
|
17
|
+
"internalDependencies": [
|
|
18
|
+
{
|
|
19
|
+
"component": "Text",
|
|
20
|
+
"note": "Used internally by Tooltip. No action needed — it's bundled in @compomo/ui."
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"required": [
|
|
25
|
+
"react >=17",
|
|
26
|
+
"react-dom >=17",
|
|
27
|
+
"@ds-mo/tokens >=0.1.0"
|
|
28
|
+
],
|
|
29
|
+
"optional": []
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"props": {
|
|
33
|
+
"label": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"required": true
|
|
36
|
+
},
|
|
37
|
+
"children": {
|
|
38
|
+
"type": "React.ReactElement",
|
|
39
|
+
"required": true
|
|
40
|
+
},
|
|
41
|
+
"side": {
|
|
42
|
+
"type": "'top' | 'right' | 'bottom' | 'left'",
|
|
43
|
+
"default": "'top'"
|
|
44
|
+
},
|
|
45
|
+
"align": {
|
|
46
|
+
"type": "'start' | 'center' | 'end'",
|
|
47
|
+
"default": "'center'"
|
|
48
|
+
},
|
|
49
|
+
"delay": {
|
|
50
|
+
"type": "number",
|
|
51
|
+
"description": "Hover delay in ms."
|
|
52
|
+
},
|
|
53
|
+
"shortcutKey": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Displays keyboard shortcut hint."
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"exports": [
|
|
59
|
+
"Tooltip"
|
|
60
|
+
],
|
|
61
|
+
"types": [
|
|
62
|
+
"TooltipProps",
|
|
63
|
+
"TooltipSide",
|
|
64
|
+
"TooltipAlign"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"dependencies": [
|
|
68
|
+
"@compomo/ui",
|
|
69
|
+
"@ds-mo/tokens"
|
|
70
|
+
],
|
|
71
|
+
"registryDependencies": [
|
|
72
|
+
"text"
|
|
73
|
+
],
|
|
74
|
+
"files": [
|
|
75
|
+
{
|
|
76
|
+
"path": "src/components/Tooltip/Tooltip.module.css",
|
|
77
|
+
"content": ".tooltip {\n box-sizing: border-box;\n background-color: var(--color-background-primary);\n border-radius: var(--dimension-radius-075);\n box-shadow: var(--effect-elevation-elevated-floating);\n padding: var(--dimension-space-050);\n line-height: var(--typography-lineheight-sm);\n pointer-events: none;\n backface-visibility: hidden;\n -webkit-font-smoothing: subpixel-antialiased;\n animation: tooltipFadeIn var(--effect-motion-short-2) forwards;\n}\n\n.tooltipInner {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--dimension-space-050);\n}\n\n.keyHint {\n width: var(--dimension-size-200);\n height: var(--dimension-size-200);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border: var(--dimension-stroke-width-015) solid var(--color-foreground-tertiary);\n border-bottom-width: var(--dimension-stroke-width-025);\n border-radius: var(--dimension-radius-025);\n}\n\n.tooltip.closing {\n animation: tooltipFadeOut var(--effect-motion-short-3) forwards;\n}\n\n@keyframes tooltipFadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes tooltipFadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n",
|
|
78
|
+
"type": "registry:ui"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"path": "src/components/Tooltip/Tooltip.tsx",
|
|
82
|
+
"content": "import React, { useState, useRef, useEffect, useLayoutEffect, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\nimport { getCssTimeMs } from '@/utils/css-tokens';\nimport { Text } from '@/components/Text';\nimport styles from './Tooltip.module.css';\n\nconst HOVER_DELAY_FALLBACK_MS = 1000;\nconst INSTANT_REOPEN_MS = 300;\nconst FADE_OUT_DURATION_MS = 200;\n\nlet lastTooltipDismissedAt = 0;\n\nexport type TooltipSide = 'top' | 'right' | 'bottom' | 'left';\nexport type TooltipAlign = 'start' | 'center' | 'end';\n\nexport interface TooltipProps {\n label: string;\n children: React.ReactElement;\n delay?: number;\n side?: TooltipSide;\n align?: TooltipAlign;\n sideOffset?: number;\n alignOffset?: number;\n shortcutKey?: string;\n shortcutKeyPosition?: 'start' | 'end';\n usePortal?: boolean;\n}\n\nexport const Tooltip: React.FC<TooltipProps> = ({\n label,\n children,\n delay,\n side = 'top',\n align = 'center',\n sideOffset = 4,\n alignOffset = 0,\n shortcutKey,\n shortcutKeyPosition = 'end',\n usePortal = true,\n}) => {\n const hoverDelayMs = getCssTimeMs('--effect-animation-delay-medium-3', HOVER_DELAY_FALLBACK_MS);\n const resolvedDelay = delay ?? hoverDelayMs;\n const anchorRef = useRef<HTMLElement | null>(null);\n const tooltipRef = useRef<HTMLDivElement>(null);\n\n const setAnchorRef = useCallback(\n (el: HTMLElement | null) => {\n (anchorRef as React.MutableRefObject<HTMLElement | null>).current = el;\n const childEl = React.Children.only(children) as React.ReactElement<{ ref?: React.Ref<unknown> }>;\n const childRef = 'ref' in childEl ? childEl.ref : undefined;\n if (typeof childRef === 'function') childRef(el);\n else if (childRef && typeof childRef === 'object') (childRef as React.MutableRefObject<unknown>).current = el;\n },\n [children]\n );\n\n const [open, setOpen] = useState(false);\n const [isClosing, setIsClosing] = useState(false);\n const [shouldRender, setShouldRender] = useState(false);\n const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });\n const [tooltipSize, setTooltipSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 });\n const delayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const viewportPadding = 4;\n\n const clearDelayTimer = () => {\n if (delayTimerRef.current) { clearTimeout(delayTimerRef.current); delayTimerRef.current = null; }\n };\n\n const handleOpen = () => {\n clearDelayTimer();\n const showImmediately = Date.now() - lastTooltipDismissedAt < INSTANT_REOPEN_MS;\n if (showImmediately) {\n setOpen(true); setShouldRender(true); setIsClosing(false); return;\n }\n delayTimerRef.current = setTimeout(() => {\n delayTimerRef.current = null;\n setOpen(true); setShouldRender(true); setIsClosing(false);\n }, resolvedDelay);\n };\n\n const handleClose = () => {\n clearDelayTimer();\n if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); closeTimerRef.current = null; }\n if (!open && !shouldRender) return;\n lastTooltipDismissedAt = Date.now();\n setIsClosing(true);\n closeTimerRef.current = setTimeout(() => {\n closeTimerRef.current = null;\n setShouldRender(false); setOpen(false); setIsClosing(false);\n }, FADE_OUT_DURATION_MS);\n };\n\n useEffect(() => () => { clearDelayTimer(); if (closeTimerRef.current) clearTimeout(closeTimerRef.current); }, []);\n\n useLayoutEffect(() => {\n if (!shouldRender || !tooltipRef.current) return;\n const updateSize = () => {\n if (!tooltipRef.current) return;\n const rect = tooltipRef.current.getBoundingClientRect();\n const w = rect.width || tooltipRef.current.offsetWidth;\n const h = rect.height || tooltipRef.current.offsetHeight;\n if (w > 0 && h > 0) setTooltipSize({ width: w, height: h });\n };\n updateSize();\n requestAnimationFrame(updateSize);\n if (typeof ResizeObserver !== 'undefined') {\n const observer = new ResizeObserver(updateSize);\n observer.observe(tooltipRef.current);\n return () => observer.disconnect();\n }\n }, [shouldRender, label, shortcutKey, shortcutKeyPosition]);\n\n useLayoutEffect(() => {\n if (!shouldRender || !anchorRef.current || !tooltipRef.current) return;\n const calculatePosition = () => {\n if (!anchorRef.current || !tooltipRef.current) return;\n const a = anchorRef.current.getBoundingClientRect();\n const tw = tooltipRef.current.getBoundingClientRect().width || tooltipSize.width || 80;\n const th = tooltipRef.current.getBoundingClientRect().height || tooltipSize.height || 24;\n let x = 0, y = 0;\n switch (side) {\n case 'top':\n y = a.top - th - sideOffset;\n x = align === 'start' ? a.left + alignOffset : align === 'end' ? a.right - tw + alignOffset : a.left + a.width / 2 - tw / 2 + alignOffset;\n break;\n case 'bottom':\n y = a.bottom + sideOffset;\n x = align === 'start' ? a.left + alignOffset : align === 'end' ? a.right - tw + alignOffset : a.left + a.width / 2 - tw / 2 + alignOffset;\n break;\n case 'left':\n x = a.left - tw - sideOffset;\n y = align === 'start' ? a.top + alignOffset : align === 'end' ? a.bottom - th + alignOffset : a.top + a.height / 2 - th / 2 + alignOffset;\n break;\n case 'right':\n x = a.right + sideOffset;\n y = align === 'start' ? a.top + alignOffset : align === 'end' ? a.bottom - th + alignOffset : a.top + a.height / 2 - th / 2 + alignOffset;\n break;\n }\n setPosition({\n x: Math.min(Math.max(x, viewportPadding), window.innerWidth - tw - viewportPadding),\n y: Math.min(Math.max(y, viewportPadding), window.innerHeight - th - viewportPadding),\n });\n };\n calculatePosition();\n requestAnimationFrame(() => requestAnimationFrame(calculatePosition));\n }, [shouldRender, side, align, sideOffset, alignOffset, tooltipSize.width, tooltipSize.height, label]);\n\n useEffect(() => {\n if (!shouldRender) return;\n const onScrollResize = () => {\n if (!anchorRef.current || !tooltipRef.current) return;\n const a = anchorRef.current.getBoundingClientRect();\n const tw = tooltipSize.width || 80;\n const th = tooltipSize.height || 24;\n let x = 0, y = 0;\n switch (side) {\n case 'top': y = a.top - th - sideOffset; x = align === 'start' ? a.left + alignOffset : align === 'end' ? a.right - tw + alignOffset : a.left + a.width / 2 - tw / 2 + alignOffset; break;\n case 'bottom': y = a.bottom + sideOffset; x = align === 'start' ? a.left + alignOffset : align === 'end' ? a.right - tw + alignOffset : a.left + a.width / 2 - tw / 2 + alignOffset; break;\n case 'left': x = a.left - tw - sideOffset; y = align === 'start' ? a.top + alignOffset : align === 'end' ? a.bottom - th + alignOffset : a.top + a.height / 2 - th / 2 + alignOffset; break;\n case 'right': x = a.right + sideOffset; y = align === 'start' ? a.top + alignOffset : align === 'end' ? a.bottom - th + alignOffset : a.top + a.height / 2 - th / 2 + alignOffset; break;\n }\n setPosition({\n x: Math.min(Math.max(x, viewportPadding), window.innerWidth - tw - viewportPadding),\n y: Math.min(Math.max(y, viewportPadding), window.innerHeight - th - viewportPadding),\n });\n };\n window.addEventListener('scroll', onScrollResize, true);\n window.addEventListener('resize', onScrollResize);\n return () => { window.removeEventListener('scroll', onScrollResize, true); window.removeEventListener('resize', onScrollResize); };\n }, [shouldRender, side, align, sideOffset, alignOffset, tooltipSize.width, tooltipSize.height]);\n\n const keyHint = shortcutKey ? (\n <div className={styles.keyHint} aria-hidden>\n <Text variant=\"text-caption-emphasis\" as=\"span\" color=\"primary\">{shortcutKey}</Text>\n </div>\n ) : null;\n\n const tooltipContent = shouldRender ? (\n <div\n ref={tooltipRef}\n className={`${styles.tooltip} ${isClosing ? styles.closing : ''}`}\n style={{\n position: 'fixed',\n left: 0,\n top: 0,\n transform: `translate(${Math.round(position.x)}px, ${Math.round(position.y)}px)`,\n willChange: 'transform',\n zIndex: 10000,\n }}\n role=\"tooltip\"\n >\n <div className={styles.tooltipInner}>\n {shortcutKey && shortcutKeyPosition === 'start' && keyHint}\n <Text variant=\"text-body-small\" as=\"span\" color=\"primary\">{label}</Text>\n {shortcutKey && shortcutKeyPosition === 'end' && keyHint}\n </div>\n </div>\n ) : null;\n\n const trigger = React.Children.only(children);\n const triggerWithRefAndHandlers = React.isValidElement(trigger)\n ? React.cloneElement(trigger as React.ReactElement<any>, {\n ref: setAnchorRef,\n onMouseEnter: (e: React.MouseEvent) => {\n handleOpen();\n (trigger as any).props.onMouseEnter?.(e);\n },\n onMouseLeave: (e: React.MouseEvent) => {\n handleClose();\n (trigger as any).props.onMouseLeave?.(e);\n },\n })\n : children;\n\n return (\n <>\n {triggerWithRefAndHandlers}\n {usePortal ? createPortal(tooltipContent, document.body) : tooltipContent}\n </>\n );\n};\n\nTooltip.displayName = 'Tooltip';\n",
|
|
83
|
+
"type": "registry:ui"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ds-mo/ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CompoMo — composable React UI components styled with TokoMo design tokens",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"components",
|
|
8
|
+
"ui",
|
|
9
|
+
"design-system",
|
|
10
|
+
"component-library",
|
|
11
|
+
"css-modules",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/zainadeel/compomo#readme",
|
|
15
|
+
"bugs": "https://github.com/zainadeel/compomo/issues",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/zainadeel/compomo.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"./css": "./dist/index.css"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "vite build",
|
|
38
|
+
"dev": "npm run registry:build && storybook dev -p 6006",
|
|
39
|
+
"storybook:build": "npm run registry:build && storybook build",
|
|
40
|
+
"clean": "rm -rf dist",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"lint": "eslint src/",
|
|
43
|
+
"registry:build": "node scripts/build-registry.mjs",
|
|
44
|
+
"mcp": "node scripts/mcp-server.mjs"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@ds-mo/tokens": ">=0.3.0",
|
|
48
|
+
"react": ">=17.0.0",
|
|
49
|
+
"react-dom": ">=17.0.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@ds-mo/icons": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@ds-mo/icons": "^0.5.0",
|
|
58
|
+
"@ds-mo/tokens": "^0.3.0",
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
60
|
+
"@storybook/addon-docs": "^10.3.5",
|
|
61
|
+
"@storybook/react": "^10.3.5",
|
|
62
|
+
"@storybook/react-vite": "^10.3.5",
|
|
63
|
+
"@types/react": "^18.3.0",
|
|
64
|
+
"@types/react-dom": "^18.3.0",
|
|
65
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
66
|
+
"react": "^18.3.1",
|
|
67
|
+
"react-dom": "^18.3.1",
|
|
68
|
+
"storybook": "^10.3.5",
|
|
69
|
+
"typescript": "^5.7.0",
|
|
70
|
+
"vite": "^6.0.0",
|
|
71
|
+
"vite-plugin-dts": "^4.3.0",
|
|
72
|
+
"zod": "^4.3.6"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=18.0.0"
|
|
76
|
+
},
|
|
77
|
+
"sideEffects": [
|
|
78
|
+
"**/*.css"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
.accordion {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/* ─── Item ──────────────────────────────────────────────────────────────────── */
|
|
7
|
+
|
|
8
|
+
.item {
|
|
9
|
+
border-bottom: 1px solid var(--color-border-secondary);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.item:first-child {
|
|
13
|
+
border-top: 1px solid var(--color-border-secondary);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.inactive {
|
|
17
|
+
opacity: 0.5;
|
|
18
|
+
pointer-events: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* ─── Trigger ───────────────────────────────────────────────────────────────── */
|
|
22
|
+
|
|
23
|
+
.trigger {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: space-between;
|
|
27
|
+
width: 100%;
|
|
28
|
+
padding: var(--dimension-space-100) var(--dimension-space-050);
|
|
29
|
+
background: none;
|
|
30
|
+
border: none;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
color: var(--color-foreground-primary);
|
|
33
|
+
border-radius: var(--dimension-radius-050);
|
|
34
|
+
transition: background-color var(--effect-motion-short-2);
|
|
35
|
+
-webkit-user-select: none;
|
|
36
|
+
user-select: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.trigger:hover { background-color: var(--color-interaction-hover); }
|
|
40
|
+
.trigger:active { background-color: var(--color-interaction-pressed); }
|
|
41
|
+
|
|
42
|
+
/* ─── Chevron ───────────────────────────────────────────────────────────────── */
|
|
43
|
+
|
|
44
|
+
.chevron {
|
|
45
|
+
font-size: 18px;
|
|
46
|
+
line-height: 1;
|
|
47
|
+
transform: rotate(0deg);
|
|
48
|
+
transition: transform var(--effect-motion-short-3);
|
|
49
|
+
color: var(--color-foreground-secondary);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.chevronOpen {
|
|
53
|
+
transform: rotate(90deg);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ─── Body (animated height) ────────────────────────────────────────────────── */
|
|
57
|
+
|
|
58
|
+
.body {
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
transition: height var(--effect-motion-short-3);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.bodyInner {
|
|
64
|
+
padding: var(--dimension-space-050) var(--dimension-space-050) var(--dimension-space-100);
|
|
65
|
+
color: var(--color-foreground-secondary);
|
|
66
|
+
}
|