@evinvest/uikit 0.30.0 → 0.32.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/README.md +267 -73
- package/dist/{chunk-3IPB4QQJ.js → chunk-35SPIU4U.js} +10 -6
- package/dist/chunk-35SPIU4U.js.map +1 -0
- package/dist/components/{accordion.js → atomic/accordion.js} +3 -3
- package/dist/components/atomic/accordion.js.map +1 -0
- package/dist/components/{alert-dialog.js → atomic/alert-dialog.js} +11 -10
- package/dist/components/atomic/alert-dialog.js.map +1 -0
- package/dist/components/{alert.js → atomic/alert.js} +2 -2
- package/dist/components/atomic/alert.js.map +1 -0
- package/dist/components/atomic/animated-number-columns.js +105 -0
- package/dist/components/atomic/animated-number-columns.js.map +1 -0
- package/dist/components/{avatar.js → atomic/avatar.js} +2 -2
- package/dist/components/atomic/avatar.js.map +1 -0
- package/dist/components/{badge.js → atomic/badge.js} +4 -4
- package/dist/components/atomic/badge.js.map +1 -0
- package/dist/components/{band.js → atomic/band.js} +2 -2
- package/dist/components/atomic/band.js.map +1 -0
- package/dist/components/{breadcrumb.js → atomic/breadcrumb.js} +3 -3
- package/dist/components/atomic/breadcrumb.js.map +1 -0
- package/dist/components/{button-group.js → atomic/button-group.js} +3 -3
- package/dist/components/atomic/button-group.js.map +1 -0
- package/dist/components/{button.js → atomic/button.js} +7 -15
- package/dist/components/atomic/button.js.map +1 -0
- package/dist/components/{calendar.js → atomic/calendar.js} +2 -2
- package/dist/components/atomic/calendar.js.map +1 -0
- package/dist/components/{card.js → atomic/card.js} +2 -2
- package/dist/components/atomic/card.js.map +1 -0
- package/dist/components/{checkbox.js → atomic/checkbox.js} +4 -4
- package/dist/components/atomic/checkbox.js.map +1 -0
- package/dist/components/{collapsible.js → atomic/collapsible.js} +1 -1
- package/dist/components/atomic/collapsible.js.map +1 -0
- package/dist/components/{command.js → atomic/command.js} +9 -7
- package/dist/components/atomic/command.js.map +1 -0
- package/dist/components/{container.js → atomic/container.js} +2 -2
- package/dist/components/atomic/container.js.map +1 -0
- package/dist/components/{context-menu.js → atomic/context-menu.js} +62 -14
- package/dist/components/atomic/context-menu.js.map +1 -0
- package/dist/components/{dialog.js → atomic/dialog.js} +11 -10
- package/dist/components/atomic/dialog.js.map +1 -0
- package/dist/components/{drawer.js → atomic/drawer.js} +10 -8
- package/dist/components/atomic/drawer.js.map +1 -0
- package/dist/components/{dropdown-menu.js → atomic/dropdown-menu.js} +74 -15
- package/dist/components/atomic/dropdown-menu.js.map +1 -0
- package/dist/components/{empty.js → atomic/empty.js} +2 -2
- package/dist/components/atomic/empty.js.map +1 -0
- package/dist/components/atomic/field-context.js.map +1 -0
- package/dist/components/atomic/form-row.js +51 -0
- package/dist/components/atomic/form-row.js.map +1 -0
- package/dist/components/{hover-card.js → atomic/hover-card.js} +8 -8
- package/dist/components/atomic/hover-card.js.map +1 -0
- package/dist/components/{info-tip.js → atomic/info-tip.js} +10 -10
- package/dist/components/atomic/info-tip.js.map +1 -0
- package/dist/components/{input-otp.js → atomic/input-otp.js} +3 -3
- package/dist/components/atomic/input-otp.js.map +1 -0
- package/dist/components/{input.js → atomic/input.js} +3 -3
- package/dist/components/atomic/input.js.map +1 -0
- package/dist/components/{kbd.js → atomic/kbd.js} +2 -2
- package/dist/components/atomic/kbd.js.map +1 -0
- package/dist/components/atomic/key-value.js +68 -0
- package/dist/components/atomic/key-value.js.map +1 -0
- package/dist/components/{label.js → atomic/label.js} +2 -2
- package/dist/components/atomic/label.js.map +1 -0
- package/dist/components/{menubar.js → atomic/menubar.js} +48 -14
- package/dist/components/atomic/menubar.js.map +1 -0
- package/dist/components/{native-select.js → atomic/native-select.js} +6 -7
- package/dist/components/atomic/native-select.js.map +1 -0
- package/dist/components/{navigation-menu.js → atomic/navigation-menu.js} +11 -12
- package/dist/components/atomic/navigation-menu.js.map +1 -0
- package/dist/components/{pagination.js → atomic/pagination.js} +1 -1
- package/dist/components/atomic/pagination.js.map +1 -0
- package/dist/components/{popover.js → atomic/popover.js} +10 -10
- package/dist/components/atomic/popover.js.map +1 -0
- package/dist/components/{progress.js → atomic/progress.js} +2 -2
- package/dist/components/atomic/progress.js.map +1 -0
- package/dist/components/{radio-group.js → atomic/radio-group.js} +4 -4
- package/dist/components/atomic/radio-group.js.map +1 -0
- package/dist/components/{scroll-area.js → atomic/scroll-area.js} +2 -2
- package/dist/components/atomic/scroll-area.js.map +1 -0
- package/dist/components/{section-descriptor.js → atomic/section-descriptor.js} +2 -2
- package/dist/components/atomic/section-descriptor.js.map +1 -0
- package/dist/components/{select-listbox.js → atomic/select-listbox.js} +1 -1
- package/dist/components/atomic/select-listbox.js.map +1 -0
- package/dist/components/{select.js → atomic/select.js} +26 -16
- package/dist/components/atomic/select.js.map +1 -0
- package/dist/components/{separator.js → atomic/separator.js} +2 -2
- package/dist/components/atomic/separator.js.map +1 -0
- package/dist/components/{sheet.js → atomic/sheet.js} +11 -10
- package/dist/components/atomic/sheet.js.map +1 -0
- package/dist/components/{sidebar.js → atomic/sidebar.js} +5 -5
- package/dist/components/atomic/sidebar.js.map +1 -0
- package/dist/components/{skeleton.js → atomic/skeleton.js} +2 -2
- package/dist/components/atomic/skeleton.js.map +1 -0
- package/dist/components/{slider.js → atomic/slider.js} +3 -3
- package/dist/components/atomic/slider.js.map +1 -0
- package/dist/components/{sonner.js → atomic/sonner.js} +2 -2
- package/dist/components/atomic/sonner.js.map +1 -0
- package/dist/components/{spinner.js → atomic/spinner.js} +1 -1
- package/dist/components/atomic/spinner.js.map +1 -0
- package/dist/components/atomic/status-item.js +42 -0
- package/dist/components/atomic/status-item.js.map +1 -0
- package/dist/components/{switch.js → atomic/switch.js} +4 -4
- package/dist/components/atomic/switch.js.map +1 -0
- package/dist/components/{table.js → atomic/table.js} +2 -2
- package/dist/components/atomic/table.js.map +1 -0
- package/dist/components/{tabs.js → atomic/tabs.js} +4 -4
- package/dist/components/atomic/tabs.js.map +1 -0
- package/dist/components/{textarea.js → atomic/textarea.js} +3 -3
- package/dist/components/atomic/textarea.js.map +1 -0
- package/dist/components/{toggle-group.js → atomic/toggle-group.js} +2 -2
- package/dist/components/atomic/toggle-group.js.map +1 -0
- package/dist/components/{toggle.js → atomic/toggle.js} +3 -3
- package/dist/components/atomic/toggle.js.map +1 -0
- package/dist/components/{tooltip.js → atomic/tooltip.js} +8 -8
- package/dist/components/atomic/tooltip.js.map +1 -0
- package/dist/components/atomic/use-count-tween.js +42 -0
- package/dist/components/atomic/use-count-tween.js.map +1 -0
- package/dist/components/derived/animated-number.js +157 -0
- package/dist/components/derived/animated-number.js.map +1 -0
- package/dist/components/{carousel.js → derived/carousel.js} +4 -4
- package/dist/components/derived/carousel.js.map +1 -0
- package/dist/components/derived/command-palette.js +119 -0
- package/dist/components/derived/command-palette.js.map +1 -0
- package/dist/components/derived/confirm-dialog.js +123 -0
- package/dist/components/derived/confirm-dialog.js.map +1 -0
- package/dist/components/{date-time-picker.js → derived/date-time-picker.js} +8 -8
- package/dist/components/derived/date-time-picker.js.map +1 -0
- package/dist/components/{field.js → derived/field.js} +6 -6
- package/dist/components/derived/field.js.map +1 -0
- package/dist/components/{form.js → derived/form.js} +4 -4
- package/dist/components/derived/form.js.map +1 -0
- package/dist/components/derived/icon-button.js +35 -0
- package/dist/components/derived/icon-button.js.map +1 -0
- package/dist/components/{input-group.js → derived/input-group.js} +5 -5
- package/dist/components/derived/input-group.js.map +1 -0
- package/dist/components/{item.js → derived/item.js} +4 -4
- package/dist/components/derived/item.js.map +1 -0
- package/dist/components/derived/popover-form.js +60 -0
- package/dist/components/derived/popover-form.js.map +1 -0
- package/dist/components/derived/settings.js +94 -0
- package/dist/components/derived/settings.js.map +1 -0
- package/dist/components/shell/account-list.js +128 -0
- package/dist/components/shell/account-list.js.map +1 -0
- package/dist/components/shell/account-menu-profile.js +114 -0
- package/dist/components/shell/account-menu-profile.js.map +1 -0
- package/dist/components/shell/account-menu.js +97 -0
- package/dist/components/shell/account-menu.js.map +1 -0
- package/dist/components/shell/account-settings-dialog.js +84 -0
- package/dist/components/shell/account-settings-dialog.js.map +1 -0
- package/dist/components/{app-shell.js → shell/app-shell.js} +10 -1
- package/dist/components/shell/app-shell.js.map +1 -0
- package/dist/components/shell/aside.js +140 -0
- package/dist/components/shell/aside.js.map +1 -0
- package/dist/components/{auth-wall.js → shell/auth-wall.js} +1 -1
- package/dist/components/shell/auth-wall.js.map +1 -0
- package/dist/components/{bottom-tab-bar.js → shell/bottom-tab-bar.js} +1 -1
- package/dist/components/shell/bottom-tab-bar.js.map +1 -0
- package/dist/components/shell/chat-composer.js +141 -0
- package/dist/components/shell/chat-composer.js.map +1 -0
- package/dist/components/shell/chat-message.js +195 -0
- package/dist/components/shell/chat-message.js.map +1 -0
- package/dist/components/shell/chat-panel.js +137 -0
- package/dist/components/shell/chat-panel.js.map +1 -0
- package/dist/components/shell/chat-stream-text.js +69 -0
- package/dist/components/shell/chat-stream-text.js.map +1 -0
- package/dist/components/shell/chat-typing.js +26 -0
- package/dist/components/shell/chat-typing.js.map +1 -0
- package/dist/components/{footer.js → shell/footer.js} +1 -1
- package/dist/components/shell/footer.js.map +1 -0
- package/dist/components/shell/header-controls.js +149 -0
- package/dist/components/shell/header-controls.js.map +1 -0
- package/dist/components/{list-rows.js → shell/list-rows.js} +1 -1
- package/dist/components/shell/list-rows.js.map +1 -0
- package/dist/components/{logo.js → shell/logo.js} +1 -1
- package/dist/components/shell/logo.js.map +1 -0
- package/dist/components/{mobile-app-bar.js → shell/mobile-app-bar.js} +1 -1
- package/dist/components/shell/mobile-app-bar.js.map +1 -0
- package/dist/components/{nav-badge.js → shell/nav-badge.js} +1 -1
- package/dist/components/shell/nav-badge.js.map +1 -0
- package/dist/components/shell/nav-collapse.js +142 -0
- package/dist/components/shell/nav-collapse.js.map +1 -0
- package/dist/components/shell/nav-link.js.map +1 -0
- package/dist/components/shell/nav-model.js.map +1 -0
- package/dist/components/{page-frame.js → shell/page-frame.js} +3 -6
- package/dist/components/shell/page-frame.js.map +1 -0
- package/dist/components/{resource-error.js → shell/resource-error.js} +4 -4
- package/dist/components/shell/resource-error.js.map +1 -0
- package/dist/components/shell/section-nav.js +139 -0
- package/dist/components/shell/section-nav.js.map +1 -0
- package/dist/components/shell/settled.js.map +1 -0
- package/dist/components/shell/shell-icons.js.map +1 -0
- package/dist/components/{shell-nav.js → shell/shell-nav.js} +81 -39
- package/dist/components/shell/shell-nav.js.map +1 -0
- package/dist/components/shell/smart-header.js +53 -0
- package/dist/components/shell/smart-header.js.map +1 -0
- package/dist/components/{status-screen.js → shell/status-screen.js} +3 -3
- package/dist/components/shell/status-screen.js.map +1 -0
- package/dist/components/{system-banner.js → shell/system-banner.js} +23 -13
- package/dist/components/shell/system-banner.js.map +1 -0
- package/dist/components/{top-bar.js → shell/top-bar.js} +5 -4
- package/dist/components/shell/top-bar.js.map +1 -0
- package/dist/components/shell/use-shell-nav.js.map +1 -0
- package/dist/components/shell/workspace-tab.js +140 -0
- package/dist/components/shell/workspace-tab.js.map +1 -0
- package/dist/generated/accent.js +2 -2
- package/dist/generated/accent.js.map +1 -1
- package/dist/generated/account-list.js +35 -0
- package/dist/generated/account-list.js.map +1 -0
- package/dist/generated/account-settings-dialog.js +27 -0
- package/dist/generated/account-settings-dialog.js.map +1 -0
- package/dist/generated/alert-dialog.js +1 -1
- package/dist/generated/alert-dialog.js.map +1 -1
- package/dist/generated/alert.js +2 -2
- package/dist/generated/alert.js.map +1 -1
- package/dist/generated/aside.js +36 -0
- package/dist/generated/aside.js.map +1 -0
- package/dist/generated/badge.js +7 -6
- package/dist/generated/badge.js.map +1 -1
- package/dist/generated/band.js +1 -1
- package/dist/generated/band.js.map +1 -1
- package/dist/generated/breadcrumb.js +2 -2
- package/dist/generated/breadcrumb.js.map +1 -1
- package/dist/generated/button-group.js +2 -2
- package/dist/generated/button-group.js.map +1 -1
- package/dist/generated/button.js +4 -4
- package/dist/generated/button.js.map +1 -1
- package/dist/generated/card.js +6 -6
- package/dist/generated/card.js.map +1 -1
- package/dist/generated/chat-composer.js +21 -0
- package/dist/generated/chat-composer.js.map +1 -0
- package/dist/generated/chat-message.js +86 -0
- package/dist/generated/chat-message.js.map +1 -0
- package/dist/generated/chat-typing.js +9 -0
- package/dist/generated/chat-typing.js.map +1 -0
- package/dist/generated/command-palette.js +17 -0
- package/dist/generated/command-palette.js.map +1 -0
- package/dist/generated/command.js +2 -2
- package/dist/generated/command.js.map +1 -1
- package/dist/generated/confirm-dialog.js +28 -0
- package/dist/generated/confirm-dialog.js.map +1 -0
- package/dist/generated/context-menu.js +1 -1
- package/dist/generated/context-menu.js.map +1 -1
- package/dist/generated/date-time-picker.js +1 -1
- package/dist/generated/date-time-picker.js.map +1 -1
- package/dist/generated/dialog.js +1 -1
- package/dist/generated/dialog.js.map +1 -1
- package/dist/generated/drawer.js +1 -1
- package/dist/generated/drawer.js.map +1 -1
- package/dist/generated/dropdown-menu.js +1 -1
- package/dist/generated/dropdown-menu.js.map +1 -1
- package/dist/generated/form-row.js +17 -0
- package/dist/generated/form-row.js.map +1 -0
- package/dist/generated/header-controls.js +56 -0
- package/dist/generated/header-controls.js.map +1 -0
- package/dist/generated/icon-button.js +13 -0
- package/dist/generated/icon-button.js.map +1 -0
- package/dist/generated/key-value.js +15 -0
- package/dist/generated/key-value.js.map +1 -0
- package/dist/generated/menubar.js +1 -1
- package/dist/generated/menubar.js.map +1 -1
- package/dist/generated/navigation-menu.js +1 -1
- package/dist/generated/navigation-menu.js.map +1 -1
- package/dist/generated/popover-form.js +13 -0
- package/dist/generated/popover-form.js.map +1 -0
- package/dist/generated/section-label.js +10 -0
- package/dist/generated/section-label.js.map +1 -0
- package/dist/generated/select.js +15 -3
- package/dist/generated/select.js.map +1 -1
- package/dist/generated/settings.js +36 -0
- package/dist/generated/settings.js.map +1 -0
- package/dist/generated/sheet.js +1 -1
- package/dist/generated/sheet.js.map +1 -1
- package/dist/generated/slider.js +1 -1
- package/dist/generated/slider.js.map +1 -1
- package/dist/generated/smart-header.js +11 -0
- package/dist/generated/smart-header.js.map +1 -0
- package/dist/generated/sonner.js +4 -4
- package/dist/generated/sonner.js.map +1 -1
- package/dist/generated/status-item.js +17 -0
- package/dist/generated/status-item.js.map +1 -0
- package/dist/generated/system-banner.js +33 -0
- package/dist/generated/system-banner.js.map +1 -0
- package/dist/generated/top-bar.js +9 -0
- package/dist/generated/top-bar.js.map +1 -0
- package/dist/generated/workspace-tab.js +17 -0
- package/dist/generated/workspace-tab.js.map +1 -0
- package/dist/icons/select-chevron.js.map +1 -0
- package/dist/icons/ui-icons.js +77 -0
- package/dist/icons/ui-icons.js.map +1 -0
- package/dist/index.d.ts +852 -156
- package/dist/index.js +96 -76
- package/dist/lib/cn.js +8 -1
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/number-parts.js +90 -0
- package/dist/lib/number-parts.js.map +1 -0
- package/dist/palette-cli.js +1 -1
- package/dist/palette.d.ts +6 -4
- package/dist/palette.js +3 -1
- package/dist/primitives/dismissable-layer.js +76 -4
- package/dist/primitives/dismissable-layer.js.map +1 -1
- package/dist/primitives/focus-scope.js +20 -5
- package/dist/primitives/focus-scope.js.map +1 -1
- package/dist/primitives/use-floating.js +16 -1
- package/dist/primitives/use-floating.js.map +1 -1
- package/dist/primitives/use-reduced-motion.js +20 -0
- package/dist/primitives/use-reduced-motion.js.map +1 -0
- package/package.json +2 -2
- package/styles/ev.css +281 -49
- package/styles/theme.css +335 -39
- package/styles/tokens.css +643 -103
- package/dist/chunk-3IPB4QQJ.js.map +0 -1
- package/dist/components/accordion.js.map +0 -1
- package/dist/components/account-menu.js +0 -70
- package/dist/components/account-menu.js.map +0 -1
- package/dist/components/alert-dialog.js.map +0 -1
- package/dist/components/alert.js.map +0 -1
- package/dist/components/app-shell.js.map +0 -1
- package/dist/components/auth-wall.js.map +0 -1
- package/dist/components/avatar.js.map +0 -1
- package/dist/components/badge.js.map +0 -1
- package/dist/components/band.js.map +0 -1
- package/dist/components/bottom-tab-bar.js.map +0 -1
- package/dist/components/breadcrumb.js.map +0 -1
- package/dist/components/button-group.js.map +0 -1
- package/dist/components/button.js.map +0 -1
- package/dist/components/calendar.js.map +0 -1
- package/dist/components/card.js.map +0 -1
- package/dist/components/carousel.js.map +0 -1
- package/dist/components/checkbox.js.map +0 -1
- package/dist/components/collapsible.js.map +0 -1
- package/dist/components/command.js.map +0 -1
- package/dist/components/container.js.map +0 -1
- package/dist/components/context-menu.js.map +0 -1
- package/dist/components/date-time-picker.js.map +0 -1
- package/dist/components/dialog.js.map +0 -1
- package/dist/components/drawer.js.map +0 -1
- package/dist/components/dropdown-menu.js.map +0 -1
- package/dist/components/empty.js.map +0 -1
- package/dist/components/field-context.js.map +0 -1
- package/dist/components/field.js.map +0 -1
- package/dist/components/footer.js.map +0 -1
- package/dist/components/form.js.map +0 -1
- package/dist/components/hover-card.js.map +0 -1
- package/dist/components/info-tip.js.map +0 -1
- package/dist/components/input-group.js.map +0 -1
- package/dist/components/input-otp.js.map +0 -1
- package/dist/components/input.js.map +0 -1
- package/dist/components/item.js.map +0 -1
- package/dist/components/kbd.js.map +0 -1
- package/dist/components/label.js.map +0 -1
- package/dist/components/list-rows.js.map +0 -1
- package/dist/components/logo.js.map +0 -1
- package/dist/components/menubar.js.map +0 -1
- package/dist/components/mobile-app-bar.js.map +0 -1
- package/dist/components/native-select.js.map +0 -1
- package/dist/components/nav-badge.js.map +0 -1
- package/dist/components/nav-link.js.map +0 -1
- package/dist/components/nav-model.js.map +0 -1
- package/dist/components/navigation-menu.js.map +0 -1
- package/dist/components/page-frame.js.map +0 -1
- package/dist/components/pagination.js.map +0 -1
- package/dist/components/popover.js.map +0 -1
- package/dist/components/progress.js.map +0 -1
- package/dist/components/radio-group.js.map +0 -1
- package/dist/components/resource-error.js.map +0 -1
- package/dist/components/scroll-area.js.map +0 -1
- package/dist/components/section-descriptor.js.map +0 -1
- package/dist/components/section-nav.js +0 -86
- package/dist/components/section-nav.js.map +0 -1
- package/dist/components/select-chevron.js.map +0 -1
- package/dist/components/select-listbox.js.map +0 -1
- package/dist/components/select.js.map +0 -1
- package/dist/components/separator.js.map +0 -1
- package/dist/components/settled.js.map +0 -1
- package/dist/components/sheet.js.map +0 -1
- package/dist/components/shell-icons.js.map +0 -1
- package/dist/components/shell-nav.js.map +0 -1
- package/dist/components/sidebar.js.map +0 -1
- package/dist/components/skeleton.js.map +0 -1
- package/dist/components/slider.js.map +0 -1
- package/dist/components/sonner.js.map +0 -1
- package/dist/components/spinner.js.map +0 -1
- package/dist/components/status-screen.js.map +0 -1
- package/dist/components/switch.js.map +0 -1
- package/dist/components/system-banner.js.map +0 -1
- package/dist/components/table.js.map +0 -1
- package/dist/components/tabs.js.map +0 -1
- package/dist/components/terminal.js +0 -206
- package/dist/components/terminal.js.map +0 -1
- package/dist/components/textarea.js.map +0 -1
- package/dist/components/toggle-group.js.map +0 -1
- package/dist/components/toggle.js.map +0 -1
- package/dist/components/tooltip.js.map +0 -1
- package/dist/components/top-bar.js.map +0 -1
- package/dist/components/use-shell-nav.js.map +0 -1
- package/dist/generated/terminal.js +0 -70
- package/dist/generated/terminal.js.map +0 -1
- /package/dist/components/{field-context.js → atomic/field-context.js} +0 -0
- /package/dist/components/{nav-link.js → shell/nav-link.js} +0 -0
- /package/dist/components/{nav-model.js → shell/nav-model.js} +0 -0
- /package/dist/components/{settled.js → shell/settled.js} +0 -0
- /package/dist/components/{shell-icons.js → shell/shell-icons.js} +0 -0
- /package/dist/components/{use-shell-nav.js → shell/use-shell-nav.js} +0 -0
- /package/dist/{components → icons}/select-chevron.js +0 -0
|
@@ -16,6 +16,11 @@ function focusable(root) {
|
|
|
16
16
|
(el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
|
+
let claimed = [];
|
|
20
|
+
function claim(root, saved) {
|
|
21
|
+
if (claimed.length === 0) queueMicrotask(() => claimed = []);
|
|
22
|
+
claimed.push({ root, saved });
|
|
23
|
+
}
|
|
19
24
|
function usable(el) {
|
|
20
25
|
return !!el && el !== document.body && el.isConnected;
|
|
21
26
|
}
|
|
@@ -31,10 +36,18 @@ function useFocusScope(enabled, { initialFocus = "first", returnFocusTo } = {})
|
|
|
31
36
|
if (!enabled) return;
|
|
32
37
|
const root = ref.current;
|
|
33
38
|
if (!root) return;
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
const inner = claimed.find((c) => c.root !== root && c.root.contains(document.activeElement));
|
|
40
|
+
const saved = {
|
|
41
|
+
el: inner ? inner.saved.el : document.activeElement,
|
|
42
|
+
behind: false
|
|
43
|
+
};
|
|
44
|
+
if (inner) inner.saved.behind = true;
|
|
45
|
+
else {
|
|
46
|
+
const first = initialFocus === "first" ? focusable(root)[0] : void 0;
|
|
47
|
+
if (first) first.focus();
|
|
48
|
+
else focusRoot(root);
|
|
49
|
+
}
|
|
50
|
+
claim(root, saved);
|
|
38
51
|
function onKeyDown(event) {
|
|
39
52
|
if (event.key !== "Tab" || !root) return;
|
|
40
53
|
const items = focusable(root);
|
|
@@ -59,9 +72,11 @@ function useFocusScope(enabled, { initialFocus = "first", returnFocusTo } = {})
|
|
|
59
72
|
root.addEventListener("keydown", onKeyDown);
|
|
60
73
|
return () => {
|
|
61
74
|
root.removeEventListener("keydown", onKeyDown);
|
|
75
|
+
claimed = claimed.filter((c) => c.root !== root);
|
|
62
76
|
const active = document.activeElement;
|
|
63
77
|
if (active && active !== document.body && !root.contains(active)) return;
|
|
64
|
-
const
|
|
78
|
+
const anchor = fallbackRef.current?.current;
|
|
79
|
+
const target = saved.behind && usable(anchor) ? anchor : usable(saved.el) ? saved.el : anchor;
|
|
65
80
|
if (usable(target)) target.focus({ preventScroll: true });
|
|
66
81
|
};
|
|
67
82
|
}, [enabled, initialFocus]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/primitives/focus-scope.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n// `tabindex=\"-1\"` opts an element out of Tab order whatever its tag, so the\n// exclusion goes on every branch — otherwise a roving-tabindex grid's parked\n// cells would count as tab stops.\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]\",\n]\n .map(sel => `${sel}:not([tabindex=\"-1\"])`)\n .join(\",\");\n\n/**\n * Every enabled, Tab-reachable candidate under `root` in DOM (= Tab) order,\n * before the layout-based visibility filter; `tabindex=\"-1\"` is skipped on any\n * tag. For content that is fully visible while it exists (a popover), this is\n * the tab order itself. Internal to the kit.\n */\nexport function focusCandidates(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE));\n}\n\nfunction focusable(root: HTMLElement): HTMLElement[] {\n return focusCandidates(root).filter(\n (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement,\n );\n}\n\n/**\n * Where focus lands when the scope opens. `first` is the first Tab stop inside\n * (an input, an alert dialog's least destructive action); `container` is the\n * scope root itself, so a reader starts at the top and the first Tab reaches the\n * first control — right for a panel of content, where the first stop may be an\n * action one keypress away from firing.\n */\nexport type InitialFocus = \"first\" | \"container\";\n\nexport interface FocusScopeOptions {\n /** Default `first`; falls back to the container when nothing inside is focusable. */\n initialFocus?: InitialFocus;\n /**\n * Where focus goes back on close when the element focused before opening is\n * no use: `<body>` (Safari does not focus a clicked button), or detached (the\n * opener was re-rendered away). Pass the overlay's trigger.\n */\n returnFocusTo?: React.RefObject<HTMLElement | null>;\n}\n\nfunction usable(el: HTMLElement | null | undefined): el is HTMLElement {\n return !!el && el !== document.body && el.isConnected;\n}\n\n// A `<div>` without a tabindex ignores `.focus()`, so a scope with no Tab stops\n// used to leave focus on the trigger behind the scrim. `-1` makes the root\n// focusable by script (and by a click on its empty area, which keeps focus in\n// the scope) without adding it to the Tab order.\nfunction focusRoot(root: HTMLElement) {\n if (!root.hasAttribute(\"tabindex\")) root.tabIndex = -1;\n root.focus();\n}\n\n/**\n * Traps Tab focus inside the returned ref's subtree and restores focus to the\n * previously-focused element on unmount — the dep-light core of dialogs/menus,\n * replacing `@radix-ui/react-focus-scope`. On mount, focuses per\n * `initialFocus`; the root gets `tabindex=\"-1\"` whenever it is the target.\n *\n * Rust dialogs rely on the browser's native focus order within a fixed overlay;\n * see the README \"Limitations\".\n */\nexport function useFocusScope(\n enabled: boolean,\n { initialFocus = \"first\", returnFocusTo }: FocusScopeOptions = {},\n): React.RefObject<HTMLDivElement | null> {\n const ref = React.useRef<HTMLDivElement | null>(null);\n // Read at close, not captured at open: a trigger re-rendered while the\n // overlay was up is a new node by then.\n const fallbackRef = React.useRef(returnFocusTo);\n fallbackRef.current = returnFocusTo;\n\n React.useEffect(() => {\n if (!enabled) return;\n const root = ref.current;\n if (!root) return;\n\n const
|
|
1
|
+
{"version":3,"sources":["../../src/primitives/focus-scope.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n// `tabindex=\"-1\"` opts an element out of Tab order whatever its tag, so the\n// exclusion goes on every branch — otherwise a roving-tabindex grid's parked\n// cells would count as tab stops.\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]\",\n]\n .map(sel => `${sel}:not([tabindex=\"-1\"])`)\n .join(\",\");\n\n/**\n * Every enabled, Tab-reachable candidate under `root` in DOM (= Tab) order,\n * before the layout-based visibility filter; `tabindex=\"-1\"` is skipped on any\n * tag. For content that is fully visible while it exists (a popover), this is\n * the tab order itself. Internal to the kit.\n */\nexport function focusCandidates(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE));\n}\n\nfunction focusable(root: HTMLElement): HTMLElement[] {\n return focusCandidates(root).filter(\n (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement,\n );\n}\n\n/**\n * Where focus lands when the scope opens. `first` is the first Tab stop inside\n * (an input, an alert dialog's least destructive action); `container` is the\n * scope root itself, so a reader starts at the top and the first Tab reaches the\n * first control — right for a panel of content, where the first stop may be an\n * action one keypress away from firing.\n */\nexport type InitialFocus = \"first\" | \"container\";\n\nexport interface FocusScopeOptions {\n /** Default `first`; falls back to the container when nothing inside is focusable. */\n initialFocus?: InitialFocus;\n /**\n * Where focus goes back on close when the element focused before opening is\n * no use: `<body>` (Safari does not focus a clicked button), or detached (the\n * opener was re-rendered away). Pass the overlay's trigger.\n */\n returnFocusTo?: React.RefObject<HTMLElement | null>;\n}\n\ninterface Claim {\n root: HTMLElement;\n // Boxed so an outer scope opened in the same commit can mark it `behind`.\n saved: { el: HTMLElement | null; behind: boolean };\n}\n\n// Scopes that took focus in the current task. Cleared in a microtask, so only\n// scopes opened in the same commit count, never one that has been open a while.\nlet claimed: Claim[] = [];\n\nfunction claim(root: HTMLElement, saved: Claim[\"saved\"]) {\n if (claimed.length === 0) queueMicrotask(() => (claimed = []));\n claimed.push({ root, saved });\n}\n\nfunction usable(el: HTMLElement | null | undefined): el is HTMLElement {\n return !!el && el !== document.body && el.isConnected;\n}\n\n// A `<div>` without a tabindex ignores `.focus()`, so a scope with no Tab stops\n// used to leave focus on the trigger behind the scrim. `-1` makes the root\n// focusable by script (and by a click on its empty area, which keeps focus in\n// the scope) without adding it to the Tab order.\nfunction focusRoot(root: HTMLElement) {\n if (!root.hasAttribute(\"tabindex\")) root.tabIndex = -1;\n root.focus();\n}\n\n/**\n * Traps Tab focus inside the returned ref's subtree and restores focus to the\n * previously-focused element on unmount — the dep-light core of dialogs/menus,\n * replacing `@radix-ui/react-focus-scope`. On mount, focuses per\n * `initialFocus`; the root gets `tabindex=\"-1\"` whenever it is the target.\n *\n * Rust dialogs rely on the browser's native focus order within a fixed overlay;\n * see the README \"Limitations\".\n */\nexport function useFocusScope(\n enabled: boolean,\n { initialFocus = \"first\", returnFocusTo }: FocusScopeOptions = {},\n): React.RefObject<HTMLDivElement | null> {\n const ref = React.useRef<HTMLDivElement | null>(null);\n // Read at close, not captured at open: a trigger re-rendered while the\n // overlay was up is a new node by then.\n const fallbackRef = React.useRef(returnFocusTo);\n fallbackRef.current = returnFocusTo;\n\n React.useEffect(() => {\n if (!enabled) return;\n const root = ref.current;\n if (!root) return;\n\n // A scope opened in the same commit (a popover inside this dialog) has\n // already run, since children's effects come first, and taken focus. This\n // one must not pull focus out of it. Both saved the element focused before\n // either opened; the inner scope sits inside this one, so on close it goes\n // back to its own trigger first and to that element only as a fallback.\n const inner = claimed.find((c) => c.root !== root && c.root.contains(document.activeElement));\n const saved: Claim[\"saved\"] = {\n el: inner ? inner.saved.el : (document.activeElement as HTMLElement | null),\n behind: false,\n };\n if (inner) inner.saved.behind = true;\n else {\n const first = initialFocus === \"first\" ? focusable(root)[0] : undefined;\n if (first) first.focus();\n else focusRoot(root);\n }\n claim(root, saved);\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key !== \"Tab\" || !root) return;\n const items = focusable(root);\n if (items.length === 0) {\n event.preventDefault();\n return;\n }\n const firstEl = items[0]!;\n const lastEl = items[items.length - 1]!;\n const active = document.activeElement;\n // From the root itself Shift+Tab would leave the scope for whatever\n // precedes the portal, so both directions are steered explicitly.\n if (active === root) {\n event.preventDefault();\n (event.shiftKey ? lastEl : firstEl).focus();\n } else if (event.shiftKey && active === firstEl) {\n event.preventDefault();\n lastEl.focus();\n } else if (!event.shiftKey && active === lastEl) {\n event.preventDefault();\n firstEl.focus();\n }\n }\n\n root.addEventListener(\"keydown\", onKeyDown);\n return () => {\n root.removeEventListener(\"keydown\", onKeyDown);\n claimed = claimed.filter((c) => c.root !== root);\n // Focus that already left the scope (a click outside landed on a field)\n // is the user's; only focus still inside, or dropped to <body>, returns.\n const active = document.activeElement;\n if (active && active !== document.body && !root.contains(active)) return;\n const anchor = fallbackRef.current?.current;\n const target = saved.behind && usable(anchor) ? anchor : usable(saved.el) ? saved.el : anchor;\n if (usable(target)) target.focus({ preventScroll: true });\n };\n }, [enabled, initialFocus]);\n\n return ref;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -21,6 +21,12 @@ function toContainingBlock(floating, x, y) {
|
|
|
21
21
|
function layoutSize(el) {
|
|
22
22
|
return { width: el.offsetWidth, height: el.offsetHeight };
|
|
23
23
|
}
|
|
24
|
+
function hasRunningAnimationAbove(el) {
|
|
25
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
26
|
+
if (node.getAnimations?.().some((a) => a.playState === "running")) return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
24
30
|
function viewport() {
|
|
25
31
|
const el = document.documentElement;
|
|
26
32
|
return {
|
|
@@ -105,7 +111,15 @@ function useFloating(opts) {
|
|
|
105
111
|
const { top, left } = ideal(a, f, placed);
|
|
106
112
|
write(floating, f, top + shiftY, left + shiftX, true);
|
|
107
113
|
}
|
|
108
|
-
|
|
114
|
+
let frame = 0;
|
|
115
|
+
function follow() {
|
|
116
|
+
frame = 0;
|
|
117
|
+
place();
|
|
118
|
+
if (anchorRef.current && hasRunningAnimationAbove(anchorRef.current)) {
|
|
119
|
+
frame = requestAnimationFrame(follow);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
follow();
|
|
109
123
|
window.addEventListener("scroll", sync, true);
|
|
110
124
|
window.addEventListener("resize", place);
|
|
111
125
|
let observer;
|
|
@@ -115,6 +129,7 @@ function useFloating(opts) {
|
|
|
115
129
|
if (floatingRef.current) observer.observe(floatingRef.current);
|
|
116
130
|
}
|
|
117
131
|
return () => {
|
|
132
|
+
if (frame) cancelAnimationFrame(frame);
|
|
118
133
|
window.removeEventListener("scroll", sync, true);
|
|
119
134
|
window.removeEventListener("resize", place);
|
|
120
135
|
observer?.disconnect();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/primitives/use-floating.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nexport type Side = \"top\" | \"right\" | \"bottom\" | \"left\";\nexport type Align = \"start\" | \"center\" | \"end\";\n\nexport interface FloatingResult {\n floatingRef: React.RefObject<HTMLElement | null>;\n style: React.CSSProperties;\n side: Side;\n align: Align;\n}\n\n// The element sits at its containing block's origin and is moved with the\n// `translate` property, never with `top`/`left`: an absolute box's\n// shrink-to-fit width is \"containing block minus `left`\", so a `w-fit` surface\n// pushed past the viewport's right edge (horizontal scroll, a nested scroller)\n// would collapse to one word per line. `translate` composes with `transform`,\n// so the enter animations (which animate `transform`) are unaffected. The\n// offset never goes through React: `place()` / `sync()` write it straight to\n// the element, so this style prop stays one frozen object.\nconst ABSOLUTE: React.CSSProperties = Object.freeze({ position: \"absolute\", top: 0, left: 0 });\n\nconst POSITIONED = new Set([\"relative\", \"absolute\", \"fixed\", \"sticky\"]);\n\n/**\n * Converts a viewport-relative point into the coordinate space of the\n * floating element's containing block, i.e. the numbers `position: absolute`\n * expects. floating-ui's `strategy: \"absolute\"` does the same walk.\n */\nfunction toContainingBlock(\n floating: HTMLElement,\n x: number,\n y: number,\n): { x: number; y: number; root: boolean } {\n const parent = floating.offsetParent;\n const doc = floating.ownerDocument;\n const root = parent === doc.body || parent === doc.documentElement || parent === null;\n // `offsetParent` reports `body` even when nothing is positioned; only a\n // positioned body/html really forms a containing block.\n const positioned =\n parent instanceof HTMLElement &&\n (!root || POSITIONED.has(doc.defaultView?.getComputedStyle(parent).position ?? \"\"));\n if (!positioned) {\n const win = doc.defaultView;\n return { x: x + (win?.scrollX ?? 0), y: y + (win?.scrollY ?? 0), root: true };\n }\n const p = parent.getBoundingClientRect();\n return {\n x: x - p.left - parent.clientLeft + parent.scrollLeft,\n y: y - p.top - parent.clientTop + parent.scrollTop,\n root: false,\n };\n}\n\ninterface Size2D {\n width: number;\n height: number;\n}\n\n// The layout box, not `getBoundingClientRect()`: the latter reports the box\n// mid-animation (`zoom-in-95` scales it to 0.95 on the first frame), which\n// placed a top-side popover 5 % of its height too low, over the anchor.\nfunction layoutSize(el: HTMLElement): Size2D {\n return { width: el.offsetWidth, height: el.offsetHeight };\n}\n\nfunction viewport(): { vw: number; vh: number } {\n // `clientWidth` excludes the scrollbar, which the floating element cannot\n // sit under anyway; jsdom reports 0 there, hence the fallback.\n const el = document.documentElement;\n return {\n vw: el.clientWidth || window.innerWidth,\n vh: el.clientHeight || window.innerHeight,\n };\n}\n\n/**\n * Positions a floating element against an anchor with `position: absolute` in\n * the coordinate space of its containing block (the `Portal` puts it in\n * `document.body`, so normally document coordinates) — the dep-light stand-in\n * for `@radix-ui/react-popper` / floating-ui. Supports a preferred\n * `side`/`align`, an `offset`, and a single collision flip onto the opposite\n * side when the preferred side overflows the viewport.\n *\n * Two update modes:\n * - `place()` — full pass (flip + clamp into the viewport) on open, on window\n * `resize`, and whenever the anchor or the floating element changes size.\n * - `sync()` — re-derives the coordinates from the anchor with the side and\n * the clamp shift already chosen; runs on every `scroll` (captured on\n * `window`, so nested scroll containers count too).\n *\n * Because the element is absolutely positioned in the document, a document\n * scroll moves it natively with the page — `sync()` then computes the same\n * numbers and is a no-op — so the overlay never lags behind the compositor.\n * Only a nested scroll container changes the coordinates, and there a frame\n * of lag is acceptable. The side is never revisited on scroll: no jumping.\n * A page that scrolls `body` instead of the document (`html { overflow:\n * hidden } body { overflow: auto }`) is such a nested scroller: correct, but\n * with the frame of lag the `fixed` strategy had.\n *\n * Rust's overlays use CSS-only placement (`data-side`) and do not measure; see\n * the README \"Limitations\".\n */\nexport function useFloating(opts: {\n anchorRef: React.RefObject<Element | null>;\n open: boolean;\n side?: Side;\n align?: Align;\n offset?: number;\n}): FloatingResult {\n const { anchorRef, open, side = \"bottom\", align = \"center\", offset = 4 } = opts;\n const floatingRef = React.useRef<HTMLElement | null>(null);\n const [placedSide, setPlacedSide] = React.useState<Side>(side);\n // The last full placement: the side that won the flip and how far the\n // viewport clamp pushed the element off its ideal spot. `sync()` reuses both\n // instead of measuring against the viewport again.\n const placement = React.useRef<{ side: Side; shiftX: number; shiftY: number }>({\n side,\n shiftX: 0,\n shiftY: 0,\n });\n\n React.useLayoutEffect(() => {\n if (!open) return;\n\n function ideal(a: DOMRect, f: Size2D, placed: Side): { top: number; left: number } {\n let top = 0;\n let left = 0;\n const vertical = placed === \"top\" || placed === \"bottom\";\n\n if (placed === \"bottom\") top = a.bottom + offset;\n else if (placed === \"top\") top = a.top - offset - f.height;\n else if (placed === \"right\") left = a.right + offset;\n else left = a.left - offset - f.width;\n\n if (vertical) {\n if (align === \"start\") left = a.left;\n else if (align === \"end\") left = a.right - f.width;\n else left = a.left + a.width / 2 - f.width / 2;\n } else {\n if (align === \"start\") top = a.top;\n else if (align === \"end\") top = a.bottom - f.height;\n else top = a.top + a.height / 2 - f.height / 2;\n }\n return { top, left };\n }\n\n // `contain` keeps the box inside the initial containing block: a box past\n // the document's edge grows the page (a scrollbar shows for as long as the\n // overlay is open). `place()` already clamps against the viewport;\n // `sync()` follows a nested scroller's anchor and only needs this bound.\n function write(floating: HTMLElement, f: Size2D, top: number, left: number, contain: boolean) {\n const p = toContainingBlock(floating, left, top);\n let { x, y } = p;\n if (contain && p.root) {\n const icbWidth = floating.ownerDocument.documentElement.clientWidth || window.innerWidth;\n x = Math.max(0, Math.min(x, icbWidth - f.width));\n y = Math.max(0, y);\n }\n floating.style.translate = `${Math.round(x)}px ${Math.round(y)}px`;\n }\n\n function place() {\n const anchor = anchorRef.current;\n const floating = floatingRef.current;\n if (!anchor || !floating) return;\n\n const a = anchor.getBoundingClientRect();\n const f = layoutSize(floating);\n const { vw, vh } = viewport();\n\n let placed: Side = side;\n if (side === \"bottom\" && a.bottom + offset + f.height > vh && a.top - offset - f.height > 0)\n placed = \"top\";\n else if (side === \"top\" && a.top - offset - f.height < 0 && a.bottom + offset + f.height < vh)\n placed = \"bottom\";\n else if (side === \"right\" && a.right + offset + f.width > vw && a.left - offset - f.width > 0)\n placed = \"left\";\n else if (side === \"left\" && a.left - offset - f.width < 0 && a.right + offset + f.width < vw)\n placed = \"right\";\n\n const { top, left } = ideal(a, f, placed);\n const clampedLeft = Math.max(offset, Math.min(left, vw - f.width - offset));\n const clampedTop = Math.max(offset, Math.min(top, vh - f.height - offset));\n\n placement.current = {\n side: placed,\n shiftX: clampedLeft - left,\n shiftY: clampedTop - top,\n };\n write(floating, f, clampedTop, clampedLeft, false);\n // Only a real change may re-render: this runs on every resize tick.\n setPlacedSide(prev => (prev === placed ? prev : placed));\n }\n\n function sync() {\n const anchor = anchorRef.current;\n const floating = floatingRef.current;\n if (!anchor || !floating) return;\n\n const a = anchor.getBoundingClientRect();\n const f = layoutSize(floating);\n const { side: placed, shiftX, shiftY } = placement.current;\n const { top, left } = ideal(a, f, placed);\n write(floating, f, top + shiftY, left + shiftX, true);\n }\n\n place();\n // No rAF: `scroll` already fires at most once per frame, and a synchronous\n // DOM write from the handler lands in that same frame — rAF would cost one.\n window.addEventListener(\"scroll\", sync, true);\n window.addEventListener(\"resize\", place);\n\n let observer: ResizeObserver | undefined;\n if (typeof ResizeObserver !== \"undefined\") {\n observer = new ResizeObserver(place);\n if (anchorRef.current) observer.observe(anchorRef.current);\n if (floatingRef.current) observer.observe(floatingRef.current);\n }\n\n return () => {\n window.removeEventListener(\"scroll\", sync, true);\n window.removeEventListener(\"resize\", place);\n observer?.disconnect();\n };\n }, [open, side, align, offset, anchorRef]);\n\n return { floatingRef, style: ABSOLUTE, side: placedSide, align };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/primitives/use-floating.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nexport type Side = \"top\" | \"right\" | \"bottom\" | \"left\";\nexport type Align = \"start\" | \"center\" | \"end\";\n\nexport interface FloatingResult {\n floatingRef: React.RefObject<HTMLElement | null>;\n style: React.CSSProperties;\n side: Side;\n align: Align;\n}\n\n// The element sits at its containing block's origin and is moved with the\n// `translate` property, never with `top`/`left`: an absolute box's\n// shrink-to-fit width is \"containing block minus `left`\", so a `w-fit` surface\n// pushed past the viewport's right edge (horizontal scroll, a nested scroller)\n// would collapse to one word per line. `translate` composes with `transform`,\n// so the enter animations (which animate `transform`) are unaffected. The\n// offset never goes through React: `place()` / `sync()` write it straight to\n// the element, so this style prop stays one frozen object.\nconst ABSOLUTE: React.CSSProperties = Object.freeze({ position: \"absolute\", top: 0, left: 0 });\n\nconst POSITIONED = new Set([\"relative\", \"absolute\", \"fixed\", \"sticky\"]);\n\n/**\n * Converts a viewport-relative point into the coordinate space of the\n * floating element's containing block, i.e. the numbers `position: absolute`\n * expects. floating-ui's `strategy: \"absolute\"` does the same walk.\n */\nfunction toContainingBlock(\n floating: HTMLElement,\n x: number,\n y: number,\n): { x: number; y: number; root: boolean } {\n const parent = floating.offsetParent;\n const doc = floating.ownerDocument;\n const root = parent === doc.body || parent === doc.documentElement || parent === null;\n // `offsetParent` reports `body` even when nothing is positioned; only a\n // positioned body/html really forms a containing block.\n const positioned =\n parent instanceof HTMLElement &&\n (!root || POSITIONED.has(doc.defaultView?.getComputedStyle(parent).position ?? \"\"));\n if (!positioned) {\n const win = doc.defaultView;\n return { x: x + (win?.scrollX ?? 0), y: y + (win?.scrollY ?? 0), root: true };\n }\n const p = parent.getBoundingClientRect();\n return {\n x: x - p.left - parent.clientLeft + parent.scrollLeft,\n y: y - p.top - parent.clientTop + parent.scrollTop,\n root: false,\n };\n}\n\ninterface Size2D {\n width: number;\n height: number;\n}\n\n// The layout box, not `getBoundingClientRect()`: the latter reports the box\n// mid-animation (`zoom-in-95` scales it to 0.95 on the first frame), which\n// placed a top-side popover 5 % of its height too low, over the anchor.\nfunction layoutSize(el: HTMLElement): Size2D {\n return { width: el.offsetWidth, height: el.offsetHeight };\n}\n\n// jsdom has no `getAnimations`; there nothing animates.\nfunction hasRunningAnimationAbove(el: Element): boolean {\n for (let node = el.parentElement; node; node = node.parentElement) {\n if (node.getAnimations?.().some((a) => a.playState === \"running\")) return true;\n }\n return false;\n}\n\nfunction viewport(): { vw: number; vh: number } {\n // `clientWidth` excludes the scrollbar, which the floating element cannot\n // sit under anyway; jsdom reports 0 there, hence the fallback.\n const el = document.documentElement;\n return {\n vw: el.clientWidth || window.innerWidth,\n vh: el.clientHeight || window.innerHeight,\n };\n}\n\n/**\n * Positions a floating element against an anchor with `position: absolute` in\n * the coordinate space of its containing block (the `Portal` puts it in\n * `document.body`, so normally document coordinates) — the dep-light stand-in\n * for `@radix-ui/react-popper` / floating-ui. Supports a preferred\n * `side`/`align`, an `offset`, and a single collision flip onto the opposite\n * side when the preferred side overflows the viewport.\n *\n * Two update modes:\n * - `place()` — full pass (flip + clamp into the viewport) on open, on window\n * `resize`, and whenever the anchor or the floating element changes size.\n * - `sync()` — re-derives the coordinates from the anchor with the side and\n * the clamp shift already chosen; runs on every `scroll` (captured on\n * `window`, so nested scroll containers count too).\n *\n * Because the element is absolutely positioned in the document, a document\n * scroll moves it natively with the page — `sync()` then computes the same\n * numbers and is a no-op — so the overlay never lags behind the compositor.\n * Only a nested scroll container changes the coordinates, and there a frame\n * of lag is acceptable. The side is never revisited on scroll: no jumping.\n * A page that scrolls `body` instead of the document (`html { overflow:\n * hidden } body { overflow: auto }`) is such a nested scroller: correct, but\n * with the frame of lag the `fixed` strategy had.\n *\n * Rust's overlays use CSS-only placement (`data-side`) and do not measure; see\n * the README \"Limitations\".\n */\nexport function useFloating(opts: {\n anchorRef: React.RefObject<Element | null>;\n open: boolean;\n side?: Side;\n align?: Align;\n offset?: number;\n}): FloatingResult {\n const { anchorRef, open, side = \"bottom\", align = \"center\", offset = 4 } = opts;\n const floatingRef = React.useRef<HTMLElement | null>(null);\n const [placedSide, setPlacedSide] = React.useState<Side>(side);\n // The last full placement: the side that won the flip and how far the\n // viewport clamp pushed the element off its ideal spot. `sync()` reuses both\n // instead of measuring against the viewport again.\n const placement = React.useRef<{ side: Side; shiftX: number; shiftY: number }>({\n side,\n shiftX: 0,\n shiftY: 0,\n });\n\n React.useLayoutEffect(() => {\n if (!open) return;\n\n function ideal(a: DOMRect, f: Size2D, placed: Side): { top: number; left: number } {\n let top = 0;\n let left = 0;\n const vertical = placed === \"top\" || placed === \"bottom\";\n\n if (placed === \"bottom\") top = a.bottom + offset;\n else if (placed === \"top\") top = a.top - offset - f.height;\n else if (placed === \"right\") left = a.right + offset;\n else left = a.left - offset - f.width;\n\n if (vertical) {\n if (align === \"start\") left = a.left;\n else if (align === \"end\") left = a.right - f.width;\n else left = a.left + a.width / 2 - f.width / 2;\n } else {\n if (align === \"start\") top = a.top;\n else if (align === \"end\") top = a.bottom - f.height;\n else top = a.top + a.height / 2 - f.height / 2;\n }\n return { top, left };\n }\n\n // `contain` keeps the box inside the initial containing block: a box past\n // the document's edge grows the page (a scrollbar shows for as long as the\n // overlay is open). `place()` already clamps against the viewport;\n // `sync()` follows a nested scroller's anchor and only needs this bound.\n function write(floating: HTMLElement, f: Size2D, top: number, left: number, contain: boolean) {\n const p = toContainingBlock(floating, left, top);\n let { x, y } = p;\n if (contain && p.root) {\n const icbWidth = floating.ownerDocument.documentElement.clientWidth || window.innerWidth;\n x = Math.max(0, Math.min(x, icbWidth - f.width));\n y = Math.max(0, y);\n }\n floating.style.translate = `${Math.round(x)}px ${Math.round(y)}px`;\n }\n\n function place() {\n const anchor = anchorRef.current;\n const floating = floatingRef.current;\n if (!anchor || !floating) return;\n\n const a = anchor.getBoundingClientRect();\n const f = layoutSize(floating);\n const { vw, vh } = viewport();\n\n let placed: Side = side;\n if (side === \"bottom\" && a.bottom + offset + f.height > vh && a.top - offset - f.height > 0)\n placed = \"top\";\n else if (side === \"top\" && a.top - offset - f.height < 0 && a.bottom + offset + f.height < vh)\n placed = \"bottom\";\n else if (side === \"right\" && a.right + offset + f.width > vw && a.left - offset - f.width > 0)\n placed = \"left\";\n else if (side === \"left\" && a.left - offset - f.width < 0 && a.right + offset + f.width < vw)\n placed = \"right\";\n\n const { top, left } = ideal(a, f, placed);\n const clampedLeft = Math.max(offset, Math.min(left, vw - f.width - offset));\n const clampedTop = Math.max(offset, Math.min(top, vh - f.height - offset));\n\n placement.current = {\n side: placed,\n shiftX: clampedLeft - left,\n shiftY: clampedTop - top,\n };\n write(floating, f, clampedTop, clampedLeft, false);\n // Only a real change may re-render: this runs on every resize tick.\n setPlacedSide(prev => (prev === placed ? prev : placed));\n }\n\n function sync() {\n const anchor = anchorRef.current;\n const floating = floatingRef.current;\n if (!anchor || !floating) return;\n\n const a = anchor.getBoundingClientRect();\n const f = layoutSize(floating);\n const { side: placed, shiftX, shiftY } = placement.current;\n const { top, left } = ideal(a, f, placed);\n write(floating, f, top + shiftY, left + shiftX, true);\n }\n\n // An anchor inside an entering dialog is measured scaled and shifted by the\n // dialog's animation (`zoom-in-95`), which places the floating element off\n // its anchor for good: no resize or scroll follows the animation's end. So\n // keep re-placing, a frame at a time, until no ancestor animates.\n let frame = 0;\n function follow() {\n frame = 0;\n place();\n if (anchorRef.current && hasRunningAnimationAbove(anchorRef.current)) {\n frame = requestAnimationFrame(follow);\n }\n }\n follow();\n // No rAF: `scroll` already fires at most once per frame, and a synchronous\n // DOM write from the handler lands in that same frame — rAF would cost one.\n window.addEventListener(\"scroll\", sync, true);\n window.addEventListener(\"resize\", place);\n\n let observer: ResizeObserver | undefined;\n if (typeof ResizeObserver !== \"undefined\") {\n observer = new ResizeObserver(place);\n if (anchorRef.current) observer.observe(anchorRef.current);\n if (floatingRef.current) observer.observe(floatingRef.current);\n }\n\n return () => {\n if (frame) cancelAnimationFrame(frame);\n window.removeEventListener(\"scroll\", sync, true);\n window.removeEventListener(\"resize\", place);\n observer?.disconnect();\n };\n }, [open, side, align, offset, anchorRef]);\n\n return { floatingRef, style: ABSOLUTE, side: placedSide, align };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
const QUERY = "(prefers-reduced-motion: reduce)";
|
|
4
|
+
function subscribe(notify) {
|
|
5
|
+
if (typeof window.matchMedia !== "function") return () => {
|
|
6
|
+
};
|
|
7
|
+
const mq = window.matchMedia(QUERY);
|
|
8
|
+
mq.addEventListener("change", notify);
|
|
9
|
+
return () => mq.removeEventListener("change", notify);
|
|
10
|
+
}
|
|
11
|
+
function snapshot() {
|
|
12
|
+
return typeof window.matchMedia === "function" && window.matchMedia(QUERY).matches;
|
|
13
|
+
}
|
|
14
|
+
function useReducedMotion() {
|
|
15
|
+
return React.useSyncExternalStore(subscribe, snapshot, () => false);
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
useReducedMotion
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=use-reduced-motion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/primitives/use-reduced-motion.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nconst QUERY = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribe(notify: () => void): () => void {\n if (typeof window.matchMedia !== \"function\") return () => {};\n const mq = window.matchMedia(QUERY);\n mq.addEventListener(\"change\", notify);\n return () => mq.removeEventListener(\"change\", notify);\n}\n\nfunction snapshot(): boolean {\n return typeof window.matchMedia === \"function\" && window.matchMedia(QUERY).matches;\n}\n\n/**\n * True when the user asked for less motion. The server and the hydration pass\n * answer false, so markup matches; a reduced-motion client corrects it on the\n * first commit, before anything has animated.\n */\nexport function useReducedMotion(): boolean {\n return React.useSyncExternalStore(subscribe, snapshot, () => false);\n}\n"],"mappings":";AAEA,YAAY,WAAW;AAEvB,MAAM,QAAQ;AAEd,SAAS,UAAU,QAAgC;AACjD,MAAI,OAAO,OAAO,eAAe,WAAY,QAAO,MAAM;AAAA,EAAC;AAC3D,QAAM,KAAK,OAAO,WAAW,KAAK;AAClC,KAAG,iBAAiB,UAAU,MAAM;AACpC,SAAO,MAAM,GAAG,oBAAoB,UAAU,MAAM;AACtD;AAEA,SAAS,WAAoB;AAC3B,SAAO,OAAO,OAAO,eAAe,cAAc,OAAO,WAAW,KAAK,EAAE;AAC7E;AAOO,SAAS,mBAA4B;AAC1C,SAAO,MAAM,qBAAqB,WAAW,UAAU,MAAM,KAAK;AACpE;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evinvest/uikit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Dep-light React UI kit: shadcn-semantics components with zero @radix-ui / cva, mirroring the ev_lib::uikit Rust feature. Ships the shared design tokens.",
|
|
5
5
|
"license": "BlueOak-1.0.0",
|
|
6
6
|
"publishConfig": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/EV-Main/
|
|
11
|
+
"url": "git+https://github.com/EV-Main/web_std.git",
|
|
12
12
|
"directory": "ts/uikit"
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
package/styles/ev.css
CHANGED
|
@@ -1,75 +1,307 @@
|
|
|
1
1
|
/* AUTO-GENERATED by `nix run .#gen` from the repo-root ev.css — do not edit. */
|
|
2
2
|
/* ═══ PALETTE: EV ════════════════════════════════════════════════
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
4
|
+
* Three themes, each a zero-specificity rule: a brand scope overrides them
|
|
5
|
+
* whatever order the sheets are imported in, and later rules here win over
|
|
6
|
+
* earlier ones on the same element (so the order below is load-bearing).
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* u2 Dark the default: `:root`, `[data-brand="ev"]`, or a `.dark` wrapper
|
|
9
|
+
* u2 Light a `.light` wrapper (on <html> or on any section)
|
|
10
|
+
* Oldschool `[data-theme="oldschool"]`: the navy look the kit shipped before
|
|
11
|
+
* 1.0, single-polarity, rounder, with shadows
|
|
12
|
+
*
|
|
13
|
+
* Each rule pins every EV-specific token, so a wrapper of one theme nested in
|
|
14
|
+
* another replaces all of it and leaks nothing. A brand scope is not EV: the
|
|
15
|
+
* `:not(…)` guard keeps these pins out of a `[data-brand]` subtree (and off the
|
|
16
|
+
* brand's own element), where the brand's palette and the derived formulas in
|
|
17
|
+
* theme.css apply instead. Without it a brand's `.light` would inherit EV
|
|
18
|
+
* Light's hover and borders.
|
|
19
|
+
*
|
|
20
|
+
* Surface ladder — the u2 planes sit 1.04–1.14:1 apart, which is the design
|
|
21
|
+
* (flat, no tinted fills) and under the ~1.2 floor in AGENTS.md. Planes are
|
|
22
|
+
* separated by a `--border` hairline instead: 1.37–1.46:1 on Dark and 1.35:1 on
|
|
23
|
+
* Light, measured with the alpha composited onto the plane. Do not lift a plane
|
|
24
|
+
* to chase the ratio; retune the hairline, and tokens.test.ts will measure it.
|
|
25
|
+
* Oldschool keeps its own, wider ladder: #070d18 → card #16233c (1.24:1) →
|
|
26
|
+
* popover #1d2c49 (1.13:1), with --primary held at 3:1 against the popover.
|
|
27
|
+
*
|
|
28
|
+
* Where a Figma value failed a floor the nearest passing one is pinned
|
|
29
|
+
* instead; the comment beside each says which.
|
|
14
30
|
*/
|
|
15
|
-
|
|
31
|
+
|
|
32
|
+
/* ── u2 Dark ─────────────────────────────────────────────────── */
|
|
33
|
+
:where(:is(:root, [data-brand="ev"], .dark):not([data-brand]:not([data-brand="ev"]), [data-brand]:not([data-brand="ev"]) *)) {
|
|
16
34
|
/* the polarity, for what the platform draws itself (a native <select>'s
|
|
17
35
|
* popup); a custom property so nothing but the kit's opt-ins reads it */
|
|
18
36
|
--scheme: dark;
|
|
19
37
|
|
|
20
|
-
/*
|
|
38
|
+
/* surfaces */
|
|
39
|
+
--background: #0d0d0f;
|
|
40
|
+
--card: #151518;
|
|
41
|
+
--secondary: #1c1c20;
|
|
42
|
+
--on-secondary: var(--ink);
|
|
43
|
+
--popover: #252529;
|
|
44
|
+
--muted: #070709;
|
|
45
|
+
--hover: #e6e1d30f;
|
|
46
|
+
--active: #e6e1d31a;
|
|
47
|
+
--selected: #e6e1d31a;
|
|
48
|
+
--scrim: #000000a8;
|
|
49
|
+
|
|
50
|
+
/* ink: the Figma steps, opaque, so the text floor is measured on the value
|
|
51
|
+
* that paints (tertiary is 4.53:1 on the popover, the tightest plane) */
|
|
52
|
+
--ink: #e6e1d3;
|
|
53
|
+
--ink-mid: #bebab1;
|
|
54
|
+
--ink-soft: #8d8c87;
|
|
55
|
+
--ink-faint: #5e5e5b;
|
|
56
|
+
|
|
57
|
+
/* lines. --input stays the derived 48 % ink: the control edge owes 3:1 and
|
|
58
|
+
* Figma has no variable for it. */
|
|
59
|
+
--border-subtle: #e6e1d314;
|
|
60
|
+
--border: #e6e1d324;
|
|
61
|
+
--border-strong: #e6e1d342;
|
|
62
|
+
--ring: #35a395;
|
|
63
|
+
|
|
64
|
+
/* brand and primary. A fill and an ink are different jobs (docs/spec/accents.md):
|
|
65
|
+
* --primary carries white at 4.62:1 and reads as a shape on every plane (3.30:1
|
|
66
|
+
* on the popover); --primary-ink is the teal that reads as text (4.96:1 there).
|
|
67
|
+
* Figma's hover #18998b is 3.52:1 under white, so #127c71 (5.06:1) is pinned. */
|
|
68
|
+
--brand: #0d1627;
|
|
69
|
+
--primary: #138377;
|
|
70
|
+
--on-primary: #ffffff;
|
|
71
|
+
--primary-ink: #35a395;
|
|
72
|
+
--primary-hover: #127c71;
|
|
73
|
+
--primary-border: #35a3958c;
|
|
74
|
+
|
|
75
|
+
/* status. Fills carry the dark label (white fails 3:1–3.4:1 on these); the
|
|
76
|
+
* *-ink values are the ones to use as text. */
|
|
77
|
+
--positive: #2e9e5c;
|
|
78
|
+
--on-positive: #0d0d0f;
|
|
79
|
+
--positive-ink: #4abf76;
|
|
80
|
+
--accent-trace: #6b7fa0;
|
|
81
|
+
--on-accent-trace: #0d0d0f;
|
|
82
|
+
--accent-debug: #35a395;
|
|
83
|
+
--on-accent-debug: #0d0d0f;
|
|
84
|
+
--accent-info: #4990d8;
|
|
85
|
+
--on-accent-info: #0d0d0f;
|
|
86
|
+
--accent-info-ink: #6ca6e4;
|
|
87
|
+
--accent-warn: #f5c84e;
|
|
88
|
+
--on-accent-warn: #0d0d0f;
|
|
89
|
+
--accent-error: #f05b52;
|
|
90
|
+
--on-accent-error: #0d0d0f;
|
|
91
|
+
|
|
92
|
+
--chart-1: #35a395;
|
|
93
|
+
--chart-2: #4990d8;
|
|
94
|
+
--chart-3: #f5c84e;
|
|
95
|
+
--chart-4: #e68aaf;
|
|
96
|
+
--chart-5: #2e9e5c;
|
|
97
|
+
--chart-6: #988eef;
|
|
98
|
+
|
|
99
|
+
--button-primary-bg: #e6e1d300;
|
|
100
|
+
--button-primary-hover: #e6e1d314;
|
|
101
|
+
--button-primary-border: #e6e1d399;
|
|
102
|
+
--button-primary-ink: #e6e1d3;
|
|
103
|
+
--button-danger-bg: #f05b5200;
|
|
104
|
+
--button-danger-hover: #f05b521a;
|
|
105
|
+
--button-danger-border: #f05b52;
|
|
106
|
+
--button-danger-ink: var(--accent-error);
|
|
107
|
+
|
|
108
|
+
/* chips are outlines: a transparent fill and the label's hue at 50 % */
|
|
109
|
+
--chip-neutral-bg: var(--hover);
|
|
110
|
+
--chip-neutral-border: var(--border);
|
|
111
|
+
--chip-primary-bg: #35a39500;
|
|
112
|
+
--chip-primary-border: #35a3958c;
|
|
113
|
+
--chip-positive-bg: #2e9e5c00;
|
|
114
|
+
--chip-positive-border: #4abf7680;
|
|
115
|
+
--chip-info-bg: #4990d800;
|
|
116
|
+
--chip-info-border: #6ca6e480;
|
|
117
|
+
--chip-warn-bg: #f5c84e00;
|
|
118
|
+
--chip-warn-border: #f5c84e80;
|
|
119
|
+
--chip-error-bg: #f05b5200;
|
|
120
|
+
--chip-error-border: #f05b5280;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ── u2 Light ────────────────────────────────────────────────── */
|
|
124
|
+
:where(.light:not([data-brand]:not([data-brand="ev"]), [data-brand]:not([data-brand="ev"]) *)) {
|
|
125
|
+
--scheme: light;
|
|
126
|
+
|
|
127
|
+
--background: #efebdf;
|
|
128
|
+
--card: #f6f3eb;
|
|
129
|
+
--secondary: #fbfaf6;
|
|
130
|
+
--on-secondary: var(--ink);
|
|
131
|
+
--popover: #ffffff;
|
|
132
|
+
--muted: #e6e1d3;
|
|
133
|
+
--hover: #0d0d0f0d;
|
|
134
|
+
--active: #0d0d0f17;
|
|
135
|
+
--selected: #0d0d0f14;
|
|
136
|
+
--scrim: #00000066;
|
|
137
|
+
|
|
138
|
+
--ink: #0d0d0f;
|
|
139
|
+
--ink-mid: #3d3d41;
|
|
140
|
+
--ink-soft: #67696c;
|
|
141
|
+
--ink-faint: #9a9b9e;
|
|
142
|
+
|
|
143
|
+
--border-subtle: #0d0d0f14;
|
|
144
|
+
--border: #0d0d0f24;
|
|
145
|
+
--border-strong: #0d0d0f47;
|
|
146
|
+
--ring: #138377;
|
|
147
|
+
|
|
148
|
+
--brand: #080e1a;
|
|
149
|
+
--primary: #138377;
|
|
150
|
+
--on-primary: #ffffff;
|
|
151
|
+
--primary-ink: #116f65;
|
|
152
|
+
--primary-hover: #116f65;
|
|
153
|
+
--primary-border: #116f658c;
|
|
154
|
+
|
|
155
|
+
/* Figma's positive #228a4e carries neither label at 4.5:1 (white 4.36, dark
|
|
156
|
+
* 4.45), so the fill is one step darker: white reads at 5.01:1. */
|
|
157
|
+
--positive: #1f7f48;
|
|
158
|
+
--on-positive: #ffffff;
|
|
159
|
+
--positive-ink: #1b7943;
|
|
160
|
+
--accent-trace: #5a6b7c;
|
|
161
|
+
--on-accent-trace: #ffffff;
|
|
162
|
+
--accent-debug: #116f65;
|
|
163
|
+
--on-accent-debug: #ffffff;
|
|
164
|
+
--accent-info: #2d79c2;
|
|
165
|
+
--on-accent-info: #ffffff;
|
|
166
|
+
--accent-info-ink: #25639f;
|
|
167
|
+
/* Figma's #d8a523 is 1.89:1 on the page plane; warn is text in its own
|
|
168
|
+
* colour, so it is darkened toward Figma's warn text #846305 until it clears
|
|
169
|
+
* the non-text 3:1 (3.10:1) */
|
|
170
|
+
--accent-warn: #a77f12;
|
|
171
|
+
--on-accent-warn: #0d0d0f;
|
|
172
|
+
--accent-error: #db3834;
|
|
173
|
+
--on-accent-error: #ffffff;
|
|
174
|
+
|
|
175
|
+
--chart-1: #116f65;
|
|
176
|
+
--chart-2: #2d79c2;
|
|
177
|
+
--chart-3: #d8a523;
|
|
178
|
+
--chart-4: #b74679;
|
|
179
|
+
--chart-5: #228a4e;
|
|
180
|
+
--chart-6: #6d5acc;
|
|
181
|
+
|
|
182
|
+
--button-primary-bg: #0d0d0f00;
|
|
183
|
+
--button-primary-hover: #0d0d0f0f;
|
|
184
|
+
--button-primary-border: #0d0d0fa6;
|
|
185
|
+
--button-primary-ink: #080e1a;
|
|
186
|
+
--button-danger-bg: #db383400;
|
|
187
|
+
--button-danger-hover: #db383414;
|
|
188
|
+
--button-danger-border: #db3834;
|
|
189
|
+
--button-danger-ink: var(--accent-error);
|
|
190
|
+
|
|
191
|
+
/* the solid hue at 50 % (warning 60 %), where Dark uses the ink's */
|
|
192
|
+
--chip-neutral-bg: var(--hover);
|
|
193
|
+
--chip-neutral-border: var(--border);
|
|
194
|
+
--chip-primary-bg: #13837700;
|
|
195
|
+
--chip-primary-border: #116f658c;
|
|
196
|
+
--chip-positive-bg: #228a4e00;
|
|
197
|
+
--chip-positive-border: #228a4e80;
|
|
198
|
+
--chip-info-bg: #2d79c200;
|
|
199
|
+
--chip-info-border: #2d79c280;
|
|
200
|
+
--chip-warn-bg: #a77f1200;
|
|
201
|
+
--chip-warn-border: #a77f1299;
|
|
202
|
+
--chip-error-bg: #db383400;
|
|
203
|
+
--chip-error-border: #db383480;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ── Oldschool ───────────────────────────────────────────────── *
|
|
207
|
+
* Opt-in. Dark-only, so a `.light` or `.dark` inside it takes these values
|
|
208
|
+
* too. The surfaces are the ones the kit shipped (they pass every floor in
|
|
209
|
+
* tokens.test.ts); Figma's raised/overlay #1f2d4b measured 2.96:1 for the fill
|
|
210
|
+
* and 4.06:1 for tertiary text, so the popover stays #1d2c49. Its ink steps and
|
|
211
|
+
* --border are the derived formulas for the same reason: Figma's opaque
|
|
212
|
+
* border #1b2843 is 1.1:1 on the card and would leave the planes unseparated.
|
|
213
|
+
*/
|
|
214
|
+
:where([data-theme="oldschool"], [data-theme="oldschool"] :is(.light, .dark)) {
|
|
215
|
+
--scheme: dark;
|
|
216
|
+
|
|
217
|
+
--brand-font-serif: var(--font-playfair, "Playfair Display");
|
|
218
|
+
--brand-font-mono: var(--font-inter, "Inter");
|
|
219
|
+
|
|
21
220
|
--background: #070d18;
|
|
22
221
|
--card: #16233c;
|
|
222
|
+
--secondary: #0d1526;
|
|
223
|
+
--on-secondary: var(--ink);
|
|
23
224
|
--popover: #1d2c49;
|
|
24
225
|
--muted: #26375c;
|
|
226
|
+
--hover: #e6e1d312;
|
|
227
|
+
--active: #e6e1d31f;
|
|
228
|
+
--selected: #35a3952e;
|
|
229
|
+
--scrim: #02050ea3;
|
|
25
230
|
|
|
26
|
-
/* ── ink ─────────────────────────────────────────────────────── */
|
|
27
231
|
--ink: #e6e1d3;
|
|
232
|
+
--ink-mid: color-mix(in srgb, var(--ink) 82%, transparent);
|
|
233
|
+
--ink-soft: color-mix(in srgb, var(--ink) 60%, transparent);
|
|
234
|
+
--ink-faint: color-mix(in srgb, var(--ink) 38%, transparent);
|
|
28
235
|
|
|
29
|
-
|
|
30
|
-
--
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* values (docs/spec/accents.md). --primary-ink is the teal that reads on the
|
|
37
|
-
* surfaces: 5.85:1 on background, 4.72:1 on card. --primary is the same hue
|
|
38
|
-
* and chroma one step down in lightness — OKLCH L 0.550 C 0.094 H 183°
|
|
39
|
-
* against the ink's L 0.630 C 0.101 H 183° — so the CTA carries white:
|
|
40
|
-
* --on-primary reads on it at 4.63:1, and the fill clears the 3:1 non-text
|
|
41
|
-
* floor against background (4.20:1), secondary (3.94:1), card (3.39:1) and
|
|
42
|
-
* popover (3.01:1), so a checked Switch on a Card still reads as one. Muted
|
|
43
|
-
* is the one surface under the floor (2.54:1); the controls that fill there
|
|
44
|
-
* carry an on-primary mark, see the spec. Darkening the single value instead
|
|
45
|
-
* would have dropped every teal label below AA. */
|
|
46
|
-
--primary: #128377;
|
|
236
|
+
--border-subtle: #e6e1d312;
|
|
237
|
+
--border: color-mix(in srgb, var(--ink) 14%, transparent);
|
|
238
|
+
--border-strong: #2e3e64;
|
|
239
|
+
--ring: #35a395;
|
|
240
|
+
|
|
241
|
+
--brand: #080e1a;
|
|
242
|
+
--primary: #138377;
|
|
47
243
|
--on-primary: #ffffff;
|
|
48
|
-
--primary-ink: #
|
|
49
|
-
--
|
|
50
|
-
--
|
|
51
|
-
|
|
52
|
-
--positive: #
|
|
244
|
+
--primary-ink: #35a395;
|
|
245
|
+
--primary-hover: #127c71;
|
|
246
|
+
--primary-border: #35a39500;
|
|
247
|
+
|
|
248
|
+
--positive: #2e9e5c;
|
|
53
249
|
--on-positive: #070d18;
|
|
250
|
+
--positive-ink: #4abf76;
|
|
54
251
|
--accent-trace: #6b7fa0;
|
|
55
252
|
--on-accent-trace: #070d18;
|
|
56
|
-
--accent-debug: #
|
|
253
|
+
--accent-debug: #35a395;
|
|
57
254
|
--on-accent-debug: #070d18;
|
|
58
|
-
--accent-info: #
|
|
255
|
+
--accent-info: #4990d8;
|
|
59
256
|
--on-accent-info: #070d18;
|
|
60
|
-
--accent-
|
|
257
|
+
--accent-info-ink: #6ca6e4;
|
|
258
|
+
--accent-warn: #f5c84e;
|
|
61
259
|
--on-accent-warn: #070d18;
|
|
62
|
-
|
|
63
|
-
* is the fill and the text on a bare surface; on its own 10 % tint (a
|
|
64
|
-
* destructive menu row) the text is the derived --accent-error-ink, which
|
|
65
|
-
* EV leaves to the contract: #ec8379, 4.83:1 on that tint over popover. The
|
|
66
|
-
* vivid brand red is a gitops constant, not a UI token. */
|
|
67
|
-
--accent-error: #ef5b52;
|
|
260
|
+
--accent-error: #f05b52;
|
|
68
261
|
--on-accent-error: #070d18;
|
|
69
262
|
|
|
70
|
-
--chart-1: #
|
|
71
|
-
--chart-2: #
|
|
72
|
-
--chart-3: #
|
|
73
|
-
--chart-4: #
|
|
74
|
-
--chart-5: #
|
|
263
|
+
--chart-1: #35a395;
|
|
264
|
+
--chart-2: #4990d8;
|
|
265
|
+
--chart-3: #f5c84e;
|
|
266
|
+
--chart-4: #e68aaf;
|
|
267
|
+
--chart-5: #2e9e5c;
|
|
268
|
+
--chart-6: #988eef;
|
|
269
|
+
|
|
270
|
+
/* filled buttons. The danger label stays dark: white on #f05b52 is 3.32:1. */
|
|
271
|
+
--button-primary-bg: #138377;
|
|
272
|
+
--button-primary-hover: #127c71;
|
|
273
|
+
--button-primary-border: #138377;
|
|
274
|
+
--button-primary-ink: #ffffff;
|
|
275
|
+
--button-danger-bg: #f05b52;
|
|
276
|
+
--button-danger-hover: #f06c61;
|
|
277
|
+
--button-danger-border: #f05b52;
|
|
278
|
+
--button-danger-ink: #070d18;
|
|
279
|
+
|
|
280
|
+
/* chips are tinted pills with no edge */
|
|
281
|
+
--chip-neutral-bg: var(--hover);
|
|
282
|
+
--chip-neutral-border: #e6e1d300;
|
|
283
|
+
/* Figma tints the accent chip 16 %, which leaves its teal label at 4.03:1 on
|
|
284
|
+
* the card; 8 % holds 4.56:1 */
|
|
285
|
+
--chip-primary-bg: #35a39514;
|
|
286
|
+
--chip-primary-border: #35a39500;
|
|
287
|
+
--chip-positive-bg: #2e9e5c2e;
|
|
288
|
+
--chip-positive-border: #4abf7600;
|
|
289
|
+
--chip-info-bg: #4990d82e;
|
|
290
|
+
--chip-info-border: #6ca6e400;
|
|
291
|
+
--chip-warn-bg: #f5c84e29;
|
|
292
|
+
--chip-warn-border: #f5c84e00;
|
|
293
|
+
--chip-error-bg: #f05b522e;
|
|
294
|
+
--chip-error-border: #f06c6100;
|
|
295
|
+
|
|
296
|
+
--radius: 0.5rem;
|
|
297
|
+
--control-radius: 0.375rem;
|
|
298
|
+
--chip-radius: 9999px;
|
|
299
|
+
/* the ladder is three tiers, not one drop shadow: on a dark theme the value
|
|
300
|
+
* steps alone are too close to separate a resting card from a menu from a
|
|
301
|
+
* modal, so each tier gets its own spread and opacity */
|
|
302
|
+
--depth-card: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
|
303
|
+
--depth-elevated:
|
|
304
|
+
0 4px 12px -2px rgb(0 0 0 / 0.4),
|
|
305
|
+
0 2px 4px -2px rgb(0 0 0 / 0.3);
|
|
306
|
+
--depth-overlay: 0 16px 48px -12px rgb(0 0 0 / 0.6);
|
|
75
307
|
}
|