@spatika/react 1.0.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 +72 -0
- package/dist/composites/ActionBar.d.ts +7 -0
- package/dist/composites/ActionBar.d.ts.map +1 -0
- package/dist/composites/ActionBar.js +5 -0
- package/dist/composites/AmountInput.d.ts +10 -0
- package/dist/composites/AmountInput.d.ts.map +1 -0
- package/dist/composites/AmountInput.js +7 -0
- package/dist/composites/AppHeader.d.ts +25 -0
- package/dist/composites/AppHeader.d.ts.map +1 -0
- package/dist/composites/AppHeader.js +13 -0
- package/dist/composites/AppSidebar.d.ts +30 -0
- package/dist/composites/AppSidebar.d.ts.map +1 -0
- package/dist/composites/AppSidebar.js +15 -0
- package/dist/composites/BottomSheet.d.ts +36 -0
- package/dist/composites/BottomSheet.d.ts.map +1 -0
- package/dist/composites/BottomSheet.js +37 -0
- package/dist/composites/Breadcrumb.d.ts +10 -0
- package/dist/composites/Breadcrumb.d.ts.map +1 -0
- package/dist/composites/Breadcrumb.js +10 -0
- package/dist/composites/Callout.d.ts +8 -0
- package/dist/composites/Callout.d.ts.map +1 -0
- package/dist/composites/Callout.js +5 -0
- package/dist/composites/Card.d.ts +31 -0
- package/dist/composites/Card.d.ts.map +1 -0
- package/dist/composites/Card.js +33 -0
- package/dist/composites/Chip.d.ts +13 -0
- package/dist/composites/Chip.d.ts.map +1 -0
- package/dist/composites/Chip.js +59 -0
- package/dist/composites/ChipGroup.d.ts +23 -0
- package/dist/composites/ChipGroup.d.ts.map +1 -0
- package/dist/composites/ChipGroup.js +27 -0
- package/dist/composites/CommandSearchField.d.ts +27 -0
- package/dist/composites/CommandSearchField.d.ts.map +1 -0
- package/dist/composites/CommandSearchField.js +17 -0
- package/dist/composites/CoverHero.d.ts +51 -0
- package/dist/composites/CoverHero.d.ts.map +1 -0
- package/dist/composites/CoverHero.js +71 -0
- package/dist/composites/CoverPattern.d.ts +12 -0
- package/dist/composites/CoverPattern.d.ts.map +1 -0
- package/dist/composites/CoverPattern.js +14 -0
- package/dist/composites/DataTableToolbar.d.ts +13 -0
- package/dist/composites/DataTableToolbar.d.ts.map +1 -0
- package/dist/composites/DataTableToolbar.js +7 -0
- package/dist/composites/DividerLabel.d.ts +5 -0
- package/dist/composites/DividerLabel.d.ts.map +1 -0
- package/dist/composites/DividerLabel.js +5 -0
- package/dist/composites/EmptyState.d.ts +16 -0
- package/dist/composites/EmptyState.d.ts.map +1 -0
- package/dist/composites/EmptyState.js +10 -0
- package/dist/composites/EntityCard.d.ts +31 -0
- package/dist/composites/EntityCard.d.ts.map +1 -0
- package/dist/composites/EntityCard.js +21 -0
- package/dist/composites/EntityMediaCard.d.ts +25 -0
- package/dist/composites/EntityMediaCard.d.ts.map +1 -0
- package/dist/composites/EntityMediaCard.js +17 -0
- package/dist/composites/FilterSheet.d.ts +34 -0
- package/dist/composites/FilterSheet.d.ts.map +1 -0
- package/dist/composites/FilterSheet.js +36 -0
- package/dist/composites/FloatingPageChrome.d.ts +90 -0
- package/dist/composites/FloatingPageChrome.d.ts.map +1 -0
- package/dist/composites/FloatingPageChrome.js +67 -0
- package/dist/composites/FormField.d.ts +15 -0
- package/dist/composites/FormField.d.ts.map +1 -0
- package/dist/composites/FormField.js +9 -0
- package/dist/composites/GlassCard.d.ts +13 -0
- package/dist/composites/GlassCard.d.ts.map +1 -0
- package/dist/composites/GlassCard.js +32 -0
- package/dist/composites/HeaderIconButton.d.ts +13 -0
- package/dist/composites/HeaderIconButton.d.ts.map +1 -0
- package/dist/composites/HeaderIconButton.js +12 -0
- package/dist/composites/ListRow.d.ts +9 -0
- package/dist/composites/ListRow.d.ts.map +1 -0
- package/dist/composites/ListRow.js +6 -0
- package/dist/composites/MediaEntryCard.d.ts +25 -0
- package/dist/composites/MediaEntryCard.d.ts.map +1 -0
- package/dist/composites/MediaEntryCard.js +10 -0
- package/dist/composites/MobileTabBar.d.ts +33 -0
- package/dist/composites/MobileTabBar.d.ts.map +1 -0
- package/dist/composites/MobileTabBar.js +19 -0
- package/dist/composites/NotificationBell.d.ts +28 -0
- package/dist/composites/NotificationBell.d.ts.map +1 -0
- package/dist/composites/NotificationBell.js +25 -0
- package/dist/composites/PageHeader.d.ts +11 -0
- package/dist/composites/PageHeader.d.ts.map +1 -0
- package/dist/composites/PageHeader.js +6 -0
- package/dist/composites/PageShell.d.ts +12 -0
- package/dist/composites/PageShell.d.ts.map +1 -0
- package/dist/composites/PageShell.js +8 -0
- package/dist/composites/PageStickyHeader.d.ts +33 -0
- package/dist/composites/PageStickyHeader.d.ts.map +1 -0
- package/dist/composites/PageStickyHeader.js +19 -0
- package/dist/composites/Pagination.d.ts +7 -0
- package/dist/composites/Pagination.d.ts.map +1 -0
- package/dist/composites/Pagination.js +6 -0
- package/dist/composites/PhotoViewer.d.ts +18 -0
- package/dist/composites/PhotoViewer.d.ts.map +1 -0
- package/dist/composites/PhotoViewer.js +37 -0
- package/dist/composites/PinPad.d.ts +29 -0
- package/dist/composites/PinPad.d.ts.map +1 -0
- package/dist/composites/PinPad.js +58 -0
- package/dist/composites/ProfileHero.d.ts +34 -0
- package/dist/composites/ProfileHero.d.ts.map +1 -0
- package/dist/composites/ProfileHero.js +21 -0
- package/dist/composites/QuickFilterRail.d.ts +25 -0
- package/dist/composites/QuickFilterRail.d.ts.map +1 -0
- package/dist/composites/QuickFilterRail.js +18 -0
- package/dist/composites/QuickSettings.d.ts +27 -0
- package/dist/composites/QuickSettings.d.ts.map +1 -0
- package/dist/composites/QuickSettings.js +18 -0
- package/dist/composites/RichTextEditor.d.ts +13 -0
- package/dist/composites/RichTextEditor.d.ts.map +1 -0
- package/dist/composites/RichTextEditor.js +23 -0
- package/dist/composites/SearchField.d.ts +8 -0
- package/dist/composites/SearchField.d.ts.map +1 -0
- package/dist/composites/SearchField.js +8 -0
- package/dist/composites/SectionPanel.d.ts +16 -0
- package/dist/composites/SectionPanel.d.ts.map +1 -0
- package/dist/composites/SectionPanel.js +8 -0
- package/dist/composites/SegmentedControl.d.ts +17 -0
- package/dist/composites/SegmentedControl.d.ts.map +1 -0
- package/dist/composites/SegmentedControl.js +11 -0
- package/dist/composites/StatCard.d.ts +11 -0
- package/dist/composites/StatCard.d.ts.map +1 -0
- package/dist/composites/StatCard.js +6 -0
- package/dist/composites/StatusDot.d.ts +11 -0
- package/dist/composites/StatusDot.d.ts.map +1 -0
- package/dist/composites/StatusDot.js +23 -0
- package/dist/composites/SurfaceCard.d.ts +12 -0
- package/dist/composites/SurfaceCard.d.ts.map +1 -0
- package/dist/composites/SurfaceCard.js +24 -0
- package/dist/composites/Timeline.d.ts +19 -0
- package/dist/composites/Timeline.d.ts.map +1 -0
- package/dist/composites/Timeline.js +12 -0
- package/dist/composites/Toast.d.ts +26 -0
- package/dist/composites/Toast.d.ts.map +1 -0
- package/dist/composites/Toast.js +49 -0
- package/dist/composites/Toolbar.d.ts +6 -0
- package/dist/composites/Toolbar.d.ts.map +1 -0
- package/dist/composites/Toolbar.js +5 -0
- package/dist/composites/ViewFiltersPanel.d.ts +21 -0
- package/dist/composites/ViewFiltersPanel.d.ts.map +1 -0
- package/dist/composites/ViewFiltersPanel.js +11 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/lib/animations.d.ts +62 -0
- package/dist/lib/animations.d.ts.map +1 -0
- package/dist/lib/animations.js +37 -0
- package/dist/lib/avatar-utils.d.ts +10 -0
- package/dist/lib/avatar-utils.d.ts.map +1 -0
- package/dist/lib/avatar-utils.js +49 -0
- package/dist/lib/chips.d.ts +10 -0
- package/dist/lib/chips.d.ts.map +1 -0
- package/dist/lib/chips.js +30 -0
- package/dist/lib/cn.d.ts +4 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +6 -0
- package/dist/lib/compose-refs.d.ts +6 -0
- package/dist/lib/compose-refs.d.ts.map +1 -0
- package/dist/lib/compose-refs.js +17 -0
- package/dist/lib/cover-chrome.d.ts +9 -0
- package/dist/lib/cover-chrome.d.ts.map +1 -0
- package/dist/lib/cover-chrome.js +23 -0
- package/dist/lib/cover-pattern.d.ts +75 -0
- package/dist/lib/cover-pattern.d.ts.map +1 -0
- package/dist/lib/cover-pattern.js +135 -0
- package/dist/lib/overlay-stack.d.ts +23 -0
- package/dist/lib/overlay-stack.d.ts.map +1 -0
- package/dist/lib/overlay-stack.js +37 -0
- package/dist/lib/portal.d.ts +12 -0
- package/dist/lib/portal.d.ts.map +1 -0
- package/dist/lib/portal.js +12 -0
- package/dist/lib/slot.d.ts +12 -0
- package/dist/lib/slot.d.ts.map +1 -0
- package/dist/lib/slot.js +55 -0
- package/dist/lib/surfaces.d.ts +14 -0
- package/dist/lib/surfaces.d.ts.map +1 -0
- package/dist/lib/surfaces.js +18 -0
- package/dist/lib/themes.d.ts +16 -0
- package/dist/lib/themes.d.ts.map +1 -0
- package/dist/lib/themes.js +53 -0
- package/dist/lib/typography.d.ts +17 -0
- package/dist/lib/typography.d.ts.map +1 -0
- package/dist/lib/typography.js +16 -0
- package/dist/lib/use-controllable-state.d.ts +12 -0
- package/dist/lib/use-controllable-state.d.ts.map +1 -0
- package/dist/lib/use-controllable-state.js +20 -0
- package/dist/lib/use-cover-chrome-bleed.d.ts +9 -0
- package/dist/lib/use-cover-chrome-bleed.d.ts.map +1 -0
- package/dist/lib/use-cover-chrome-bleed.js +28 -0
- package/dist/lib/use-dismissable.d.ts +13 -0
- package/dist/lib/use-dismissable.d.ts.map +1 -0
- package/dist/lib/use-dismissable.js +33 -0
- package/dist/lib/use-floating-position.d.ts +27 -0
- package/dist/lib/use-floating-position.d.ts.map +1 -0
- package/dist/lib/use-floating-position.js +115 -0
- package/dist/lib/use-focus-trap.d.ts +7 -0
- package/dist/lib/use-focus-trap.d.ts.map +1 -0
- package/dist/lib/use-focus-trap.js +61 -0
- package/dist/lib/use-media-query.d.ts +5 -0
- package/dist/lib/use-media-query.d.ts.map +1 -0
- package/dist/lib/use-media-query.js +19 -0
- package/dist/primitives/Accordion.d.ts +24 -0
- package/dist/primitives/Accordion.d.ts.map +1 -0
- package/dist/primitives/Accordion.js +37 -0
- package/dist/primitives/Alert.d.ts +10 -0
- package/dist/primitives/Alert.d.ts.map +1 -0
- package/dist/primitives/Alert.js +25 -0
- package/dist/primitives/AlertDialog.d.ts +33 -0
- package/dist/primitives/AlertDialog.d.ts.map +1 -0
- package/dist/primitives/AlertDialog.js +96 -0
- package/dist/primitives/Avatar.d.ts +15 -0
- package/dist/primitives/Avatar.d.ts.map +1 -0
- package/dist/primitives/Avatar.js +52 -0
- package/dist/primitives/Badge.d.ts +12 -0
- package/dist/primitives/Badge.d.ts.map +1 -0
- package/dist/primitives/Badge.js +27 -0
- package/dist/primitives/Button.d.ts +14 -0
- package/dist/primitives/Button.d.ts.map +1 -0
- package/dist/primitives/Button.js +37 -0
- package/dist/primitives/Checkbox.d.ts +9 -0
- package/dist/primitives/Checkbox.d.ts.map +1 -0
- package/dist/primitives/Checkbox.js +22 -0
- package/dist/primitives/Command.d.ts +17 -0
- package/dist/primitives/Command.d.ts.map +1 -0
- package/dist/primitives/Command.js +33 -0
- package/dist/primitives/Dialog.d.ts +34 -0
- package/dist/primitives/Dialog.d.ts.map +1 -0
- package/dist/primitives/Dialog.js +96 -0
- package/dist/primitives/DropdownMenu.d.ts +65 -0
- package/dist/primitives/DropdownMenu.d.ts.map +1 -0
- package/dist/primitives/DropdownMenu.js +196 -0
- package/dist/primitives/Input.d.ts +4 -0
- package/dist/primitives/Input.d.ts.map +1 -0
- package/dist/primitives/Input.js +6 -0
- package/dist/primitives/Kbd.d.ts +4 -0
- package/dist/primitives/Kbd.d.ts.map +1 -0
- package/dist/primitives/Kbd.js +6 -0
- package/dist/primitives/Label.d.ts +4 -0
- package/dist/primitives/Label.d.ts.map +1 -0
- package/dist/primitives/Label.js +6 -0
- package/dist/primitives/NativeSelect.d.ts +7 -0
- package/dist/primitives/NativeSelect.d.ts.map +1 -0
- package/dist/primitives/NativeSelect.js +10 -0
- package/dist/primitives/Popover.d.ts +29 -0
- package/dist/primitives/Popover.d.ts.map +1 -0
- package/dist/primitives/Popover.js +95 -0
- package/dist/primitives/Progress.d.ts +8 -0
- package/dist/primitives/Progress.d.ts.map +1 -0
- package/dist/primitives/Progress.js +7 -0
- package/dist/primitives/RadioGroup.d.ts +13 -0
- package/dist/primitives/RadioGroup.d.ts.map +1 -0
- package/dist/primitives/RadioGroup.js +33 -0
- package/dist/primitives/Select.d.ts +33 -0
- package/dist/primitives/Select.d.ts.map +1 -0
- package/dist/primitives/Select.js +178 -0
- package/dist/primitives/Separator.d.ts +7 -0
- package/dist/primitives/Separator.d.ts.map +1 -0
- package/dist/primitives/Separator.js +6 -0
- package/dist/primitives/Sheet.d.ts +28 -0
- package/dist/primitives/Sheet.d.ts.map +1 -0
- package/dist/primitives/Sheet.js +96 -0
- package/dist/primitives/Skeleton.d.ts +4 -0
- package/dist/primitives/Skeleton.d.ts.map +1 -0
- package/dist/primitives/Skeleton.js +6 -0
- package/dist/primitives/Slider.d.ts +12 -0
- package/dist/primitives/Slider.d.ts.map +1 -0
- package/dist/primitives/Slider.js +23 -0
- package/dist/primitives/Spinner.d.ts +6 -0
- package/dist/primitives/Spinner.d.ts.map +1 -0
- package/dist/primitives/Spinner.js +7 -0
- package/dist/primitives/StarRating.d.ts +10 -0
- package/dist/primitives/StarRating.d.ts.map +1 -0
- package/dist/primitives/StarRating.js +11 -0
- package/dist/primitives/Switch.d.ts +8 -0
- package/dist/primitives/Switch.d.ts.map +1 -0
- package/dist/primitives/Switch.js +20 -0
- package/dist/primitives/Table.d.ts +13 -0
- package/dist/primitives/Table.d.ts.map +1 -0
- package/dist/primitives/Table.js +20 -0
- package/dist/primitives/Tabs.d.ts +17 -0
- package/dist/primitives/Tabs.d.ts.map +1 -0
- package/dist/primitives/Tabs.js +30 -0
- package/dist/primitives/Textarea.d.ts +4 -0
- package/dist/primitives/Textarea.d.ts.map +1 -0
- package/dist/primitives/Textarea.js +6 -0
- package/dist/primitives/Tooltip.d.ts +31 -0
- package/dist/primitives/Tooltip.d.ts.map +1 -0
- package/dist/primitives/Tooltip.js +94 -0
- package/dist/theme/OriginThemeProvider.d.ts +31 -0
- package/dist/theme/OriginThemeProvider.d.ts.map +1 -0
- package/dist/theme/OriginThemeProvider.js +37 -0
- package/dist/theme/SpatikaThemeProvider.d.ts +36 -0
- package/dist/theme/SpatikaThemeProvider.d.ts.map +1 -0
- package/dist/theme/SpatikaThemeProvider.js +38 -0
- package/package.json +58 -0
- package/src/composites/ActionBar.tsx +25 -0
- package/src/composites/AmountInput.tsx +40 -0
- package/src/composites/AppHeader.tsx +88 -0
- package/src/composites/AppSidebar.tsx +120 -0
- package/src/composites/BottomSheet.tsx +179 -0
- package/src/composites/Breadcrumb.tsx +42 -0
- package/src/composites/Callout.tsx +22 -0
- package/src/composites/Card.tsx +119 -0
- package/src/composites/Chip.tsx +87 -0
- package/src/composites/ChipGroup.component.test.tsx +31 -0
- package/src/composites/ChipGroup.test.ts +16 -0
- package/src/composites/ChipGroup.tsx +86 -0
- package/src/composites/CommandSearchField.tsx +124 -0
- package/src/composites/CoverHero.tsx +359 -0
- package/src/composites/CoverPattern.tsx +90 -0
- package/src/composites/DataTableToolbar.tsx +48 -0
- package/src/composites/DividerLabel.tsx +22 -0
- package/src/composites/EmptyState.tsx +55 -0
- package/src/composites/EntityCard.tsx +100 -0
- package/src/composites/EntityMediaCard.tsx +118 -0
- package/src/composites/FilterSheet.tsx +178 -0
- package/src/composites/FloatingPageChrome.tsx +294 -0
- package/src/composites/FormField.tsx +44 -0
- package/src/composites/GlassCard.tsx +47 -0
- package/src/composites/HeaderIconButton.tsx +49 -0
- package/src/composites/ListRow.tsx +29 -0
- package/src/composites/MediaEntryCard.tsx +95 -0
- package/src/composites/MobileTabBar.tsx +130 -0
- package/src/composites/NotificationBell.tsx +164 -0
- package/src/composites/PageHeader.tsx +27 -0
- package/src/composites/PageShell.tsx +20 -0
- package/src/composites/PageStickyHeader.tsx +157 -0
- package/src/composites/Pagination.tsx +41 -0
- package/src/composites/PhotoViewer.tsx +142 -0
- package/src/composites/PinPad.tsx +163 -0
- package/src/composites/ProfileHero.tsx +163 -0
- package/src/composites/QuickFilterRail.tsx +92 -0
- package/src/composites/QuickSettings.tsx +120 -0
- package/src/composites/RichTextEditor.tsx +103 -0
- package/src/composites/SearchField.tsx +38 -0
- package/src/composites/SectionPanel.tsx +59 -0
- package/src/composites/SegmentedControl.tsx +62 -0
- package/src/composites/StatCard.tsx +26 -0
- package/src/composites/StatusDot.tsx +40 -0
- package/src/composites/SurfaceCard.tsx +44 -0
- package/src/composites/Timeline.tsx +70 -0
- package/src/composites/Toast.tsx +135 -0
- package/src/composites/Toolbar.tsx +19 -0
- package/src/composites/ViewFiltersPanel.tsx +82 -0
- package/src/composites/chrome.test.tsx +170 -0
- package/src/composites/smoke.test.tsx +49 -0
- package/src/index.ts +355 -0
- package/src/lib/animations.ts +47 -0
- package/src/lib/avatar-utils.ts +52 -0
- package/src/lib/chips.ts +58 -0
- package/src/lib/cn.test.ts +16 -0
- package/src/lib/cn.ts +7 -0
- package/src/lib/compose-refs.ts +21 -0
- package/src/lib/cover-chrome.ts +31 -0
- package/src/lib/cover-pattern.ts +242 -0
- package/src/lib/overlay-stack.ts +44 -0
- package/src/lib/portal.tsx +21 -0
- package/src/lib/slot.tsx +69 -0
- package/src/lib/surfaces.ts +57 -0
- package/src/lib/themes.test.ts +73 -0
- package/src/lib/themes.ts +59 -0
- package/src/lib/tokens.test.ts +52 -0
- package/src/lib/typography.ts +27 -0
- package/src/lib/use-controllable-state.ts +38 -0
- package/src/lib/use-cover-chrome-bleed.ts +33 -0
- package/src/lib/use-dismissable.ts +49 -0
- package/src/lib/use-floating-position.ts +168 -0
- package/src/lib/use-focus-trap.ts +73 -0
- package/src/lib/use-media-query.ts +22 -0
- package/src/primitives/Accordion.tsx +114 -0
- package/src/primitives/Alert.tsx +57 -0
- package/src/primitives/AlertDialog.tsx +270 -0
- package/src/primitives/Avatar.tsx +140 -0
- package/src/primitives/Badge.test.tsx +15 -0
- package/src/primitives/Badge.tsx +43 -0
- package/src/primitives/Button.test.tsx +37 -0
- package/src/primitives/Button.tsx +58 -0
- package/src/primitives/Checkbox.tsx +75 -0
- package/src/primitives/Command.tsx +175 -0
- package/src/primitives/Dialog.tsx +241 -0
- package/src/primitives/DropdownMenu.tsx +552 -0
- package/src/primitives/Input.test.tsx +22 -0
- package/src/primitives/Input.tsx +22 -0
- package/src/primitives/Kbd.tsx +17 -0
- package/src/primitives/Label.tsx +20 -0
- package/src/primitives/NativeSelect.tsx +22 -0
- package/src/primitives/Popover.tsx +209 -0
- package/src/primitives/Progress.tsx +31 -0
- package/src/primitives/RadioGroup.tsx +116 -0
- package/src/primitives/Select.tsx +409 -0
- package/src/primitives/Separator.tsx +30 -0
- package/src/primitives/Sheet.tsx +265 -0
- package/src/primitives/Skeleton.tsx +16 -0
- package/src/primitives/Slider.tsx +97 -0
- package/src/primitives/Spinner.tsx +21 -0
- package/src/primitives/StarRating.tsx +53 -0
- package/src/primitives/Switch.tsx +68 -0
- package/src/primitives/Table.tsx +134 -0
- package/src/primitives/Tabs.tsx +97 -0
- package/src/primitives/Textarea.tsx +20 -0
- package/src/primitives/Tooltip.tsx +193 -0
- package/src/primitives/smoke.test.tsx +34 -0
- package/src/test/setup.ts +1 -0
- package/src/theme/SpatikaThemeProvider.test.tsx +82 -0
- package/src/theme/SpatikaThemeProvider.tsx +112 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeded cover scenes for profile / story heroes.
|
|
3
|
+
* Compact kit set — journalD ships a larger catalog; kinds here keep the same
|
|
4
|
+
* contract (palette, headerInk, chromeInk, layered SVG geometry).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const COVER_PATTERN_KINDS = [
|
|
8
|
+
"mountains",
|
|
9
|
+
"aurora",
|
|
10
|
+
"ocean",
|
|
11
|
+
"meadow",
|
|
12
|
+
"dusk",
|
|
13
|
+
"lake",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type CoverPatternKind = (typeof COVER_PATTERN_KINDS)[number];
|
|
17
|
+
|
|
18
|
+
export type CoverPatternInput = {
|
|
19
|
+
seed: string;
|
|
20
|
+
hue: number;
|
|
21
|
+
secondaryHue?: number;
|
|
22
|
+
tertiaryHue?: number;
|
|
23
|
+
dark?: boolean;
|
|
24
|
+
warm?: boolean;
|
|
25
|
+
kind?: CoverPatternKind | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type CoverNaturePalette = {
|
|
29
|
+
skyTop: string;
|
|
30
|
+
skyBottom: string;
|
|
31
|
+
far: string;
|
|
32
|
+
mid: string;
|
|
33
|
+
near: string;
|
|
34
|
+
accent: string;
|
|
35
|
+
water: string;
|
|
36
|
+
highlight: string;
|
|
37
|
+
star: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type CoverCircle = {
|
|
41
|
+
cx: number;
|
|
42
|
+
cy: number;
|
|
43
|
+
r: number;
|
|
44
|
+
fill: string;
|
|
45
|
+
opacity?: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type CoverEllipse = {
|
|
49
|
+
cx: number;
|
|
50
|
+
cy: number;
|
|
51
|
+
rx: number;
|
|
52
|
+
ry: number;
|
|
53
|
+
fill?: string;
|
|
54
|
+
opacity?: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type CoverPath = {
|
|
58
|
+
d: string;
|
|
59
|
+
fill?: string;
|
|
60
|
+
opacity?: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type CoverPolygon = {
|
|
64
|
+
points: string;
|
|
65
|
+
fill: string;
|
|
66
|
+
opacity?: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** `light` = white-ish text on a dark scene; `dark` = near-black text on a pale scene. */
|
|
70
|
+
export type CoverHeaderInk = "light" | "dark";
|
|
71
|
+
|
|
72
|
+
export type CoverScene = {
|
|
73
|
+
kind: CoverPatternKind;
|
|
74
|
+
palette: CoverNaturePalette;
|
|
75
|
+
uid: string;
|
|
76
|
+
skyStyle: { backgroundColor: string; backgroundImage: string };
|
|
77
|
+
headerInk: CoverHeaderInk;
|
|
78
|
+
chromeInk: CoverHeaderInk;
|
|
79
|
+
polygons: CoverPolygon[];
|
|
80
|
+
paths: CoverPath[];
|
|
81
|
+
ellipses: CoverEllipse[];
|
|
82
|
+
circles: CoverCircle[];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type CoverPattern = CoverScene;
|
|
86
|
+
|
|
87
|
+
const VW = 1600;
|
|
88
|
+
const VH = 420;
|
|
89
|
+
|
|
90
|
+
function hashSeed(value: string): number {
|
|
91
|
+
let hash = 2166136261;
|
|
92
|
+
const source = value || "User";
|
|
93
|
+
for (let i = 0; i < source.length; i++) {
|
|
94
|
+
hash ^= source.charCodeAt(i);
|
|
95
|
+
hash = Math.imul(hash, 16777619);
|
|
96
|
+
}
|
|
97
|
+
return hash >>> 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function wrapHue(hue: number): number {
|
|
101
|
+
return ((hue % 360) + 360) % 360;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pick(hash: number, salt: number, modulo: number): number {
|
|
105
|
+
return (hash + salt * 2654435761) % modulo;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hsl(h: number, s: number, l: number): string {
|
|
109
|
+
return `hsl(${Math.round(wrapHue(h))} ${Math.round(s)}% ${Math.round(l)}%)`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseHslL(color: string): number | null {
|
|
113
|
+
const match = color.match(/hsl\(\s*[\d.]+\s+[\d.]+%\s+([\d.]+)%\s*\)/i);
|
|
114
|
+
return match ? Number(match[1]) : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isCoverPatternKind(value: string | null | undefined): value is CoverPatternKind {
|
|
118
|
+
return Boolean(value && (COVER_PATTERN_KINDS as readonly string[]).includes(value));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildPalette(
|
|
122
|
+
kind: CoverPatternKind,
|
|
123
|
+
hue: number,
|
|
124
|
+
secondary: number,
|
|
125
|
+
dark: boolean,
|
|
126
|
+
): CoverNaturePalette {
|
|
127
|
+
const skyL = dark ? 14 : kind === "dusk" || kind === "aurora" ? 28 : 78;
|
|
128
|
+
const landL = dark ? 18 : 42;
|
|
129
|
+
const shift = kind === "ocean" || kind === "lake" ? 18 : 0;
|
|
130
|
+
return {
|
|
131
|
+
skyTop: hsl(hue + (kind === "aurora" ? 40 : 8), dark ? 38 : 52, skyL + (dark ? 0 : 8)),
|
|
132
|
+
skyBottom: hsl(hue + shift, dark ? 32 : 48, skyL - 8),
|
|
133
|
+
far: hsl(secondary, 28, landL + 12),
|
|
134
|
+
mid: hsl(hue + 12, 34, landL),
|
|
135
|
+
near: hsl(hue - 8, 38, landL - 8),
|
|
136
|
+
accent: hsl(hue + 48, 62, dark ? 58 : 52),
|
|
137
|
+
water: hsl(hue + 18 + shift, 42, dark ? 28 : 48),
|
|
138
|
+
highlight: hsl(hue + 20, 70, dark ? 72 : 88),
|
|
139
|
+
star: hsl(hue, 20, 96),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getCoverHeaderInk(palette: CoverNaturePalette): CoverHeaderInk {
|
|
144
|
+
const samples = [palette.near, palette.mid, palette.skyBottom];
|
|
145
|
+
const luma =
|
|
146
|
+
samples.reduce((sum, color) => sum + (parseHslL(color) ?? 50), 0) / samples.length;
|
|
147
|
+
return luma < 36 ? "light" : "dark";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getCoverChromeInk(palette: CoverNaturePalette): CoverHeaderInk {
|
|
151
|
+
const luma = parseHslL(palette.skyTop);
|
|
152
|
+
return (luma ?? 50) < 42 ? "light" : "dark";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ridge(y: number, amp: number, phase: number, fill: string): CoverPath {
|
|
156
|
+
const steps = 8;
|
|
157
|
+
let d = `M0 ${VH} L0 ${y}`;
|
|
158
|
+
for (let i = 1; i <= steps; i++) {
|
|
159
|
+
const x = (VW / steps) * i;
|
|
160
|
+
const wave = Math.sin((i + phase) * 0.9) * amp;
|
|
161
|
+
d += ` L${x} ${y + wave}`;
|
|
162
|
+
}
|
|
163
|
+
d += ` L${VW} ${VH} Z`;
|
|
164
|
+
return { d, fill };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildScene(
|
|
168
|
+
kind: CoverPatternKind,
|
|
169
|
+
hash: number,
|
|
170
|
+
palette: CoverNaturePalette,
|
|
171
|
+
): Pick<CoverScene, "polygons" | "paths" | "ellipses" | "circles"> {
|
|
172
|
+
const sunX = 220 + pick(hash, 3, 1100);
|
|
173
|
+
const sunY = kind === "dusk" ? 90 : 70;
|
|
174
|
+
const sunR = kind === "aurora" ? 28 : 46;
|
|
175
|
+
const paths: CoverPath[] = [
|
|
176
|
+
ridge(250, 28, pick(hash, 5, 6), palette.far),
|
|
177
|
+
ridge(290, 36, pick(hash, 7, 6), palette.mid),
|
|
178
|
+
ridge(340, 22, pick(hash, 9, 6), palette.near),
|
|
179
|
+
];
|
|
180
|
+
const circles: CoverCircle[] = [
|
|
181
|
+
{ cx: sunX, cy: sunY, r: sunR, fill: palette.highlight, opacity: 0.9 },
|
|
182
|
+
];
|
|
183
|
+
const ellipses: CoverEllipse[] = [];
|
|
184
|
+
|
|
185
|
+
if (kind === "ocean" || kind === "lake") {
|
|
186
|
+
paths.push({
|
|
187
|
+
d: `M0 310 Q400 280 800 320 T${VW} 300 L${VW} ${VH} L0 ${VH} Z`,
|
|
188
|
+
fill: palette.water,
|
|
189
|
+
opacity: 0.85,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (kind === "aurora") {
|
|
193
|
+
ellipses.push(
|
|
194
|
+
{ cx: 480, cy: 90, rx: 340, ry: 48, fill: palette.accent, opacity: 0.35 },
|
|
195
|
+
{ cx: 980, cy: 70, rx: 280, ry: 36, fill: palette.highlight, opacity: 0.28 },
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (kind === "meadow") {
|
|
199
|
+
circles.push(
|
|
200
|
+
{ cx: 180, cy: 360, r: 14, fill: palette.accent, opacity: 0.7 },
|
|
201
|
+
{ cx: 420, cy: 380, r: 10, fill: palette.highlight, opacity: 0.65 },
|
|
202
|
+
{ cx: 1280, cy: 350, r: 12, fill: palette.accent, opacity: 0.6 },
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { polygons: [], paths, ellipses, circles };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function getCoverPattern(input: CoverPatternInput): CoverScene {
|
|
210
|
+
const seed = input.seed || "User";
|
|
211
|
+
const hash = hashSeed(seed);
|
|
212
|
+
const kind = isCoverPatternKind(input.kind)
|
|
213
|
+
? input.kind
|
|
214
|
+
: COVER_PATTERN_KINDS[pick(hash, 1, COVER_PATTERN_KINDS.length)];
|
|
215
|
+
const dark = Boolean(input.dark);
|
|
216
|
+
const warmth = input.warm ? 8 : 0;
|
|
217
|
+
const hue = wrapHue(input.hue + warmth);
|
|
218
|
+
const secondary = wrapHue((input.secondaryHue ?? input.hue + 28) + warmth);
|
|
219
|
+
const palette = buildPalette(kind, hue, secondary, dark);
|
|
220
|
+
const drawn = buildScene(kind, hash, palette);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
kind,
|
|
224
|
+
palette,
|
|
225
|
+
uid: `n${hash.toString(36)}-${kind}`,
|
|
226
|
+
headerInk: getCoverHeaderInk(palette),
|
|
227
|
+
chromeInk: getCoverChromeInk(palette),
|
|
228
|
+
skyStyle: {
|
|
229
|
+
backgroundColor: palette.skyBottom,
|
|
230
|
+
backgroundImage: `linear-gradient(180deg, ${palette.skyTop} 0%, ${palette.skyBottom} 72%)`,
|
|
231
|
+
},
|
|
232
|
+
...drawn,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function getCoverPatternStyle(input: CoverPatternInput): CoverScene["skyStyle"] {
|
|
237
|
+
return getCoverPattern(input).skyStyle;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function getCoverPatternKind(input: CoverPatternInput): CoverPatternKind {
|
|
241
|
+
return getCoverPattern(input).kind;
|
|
242
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stacking layers for portaled UI.
|
|
5
|
+
*
|
|
6
|
+
* App shells (e.g. journalD fullscreen story editor) often use z-index ~9999.
|
|
7
|
+
* Floating menus sit just above that; modal dialogs sit above menus.
|
|
8
|
+
*/
|
|
9
|
+
export const OVERLAY_Z_INDEX = {
|
|
10
|
+
/** Popovers / selects that are not competing with fullscreen editors. */
|
|
11
|
+
popover: 90,
|
|
12
|
+
/** Dropdown menus (including menus opened inside fullscreen editors). */
|
|
13
|
+
menu: 10000,
|
|
14
|
+
/** Modal overlays + dialog/sheet/alert content. */
|
|
15
|
+
modal: 11000,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/** Inline styles that beat consumer `.glass { position: relative }` rules. */
|
|
19
|
+
export function fixedLayerStyle(
|
|
20
|
+
zIndex: number,
|
|
21
|
+
extra?: React.CSSProperties,
|
|
22
|
+
): React.CSSProperties {
|
|
23
|
+
return {
|
|
24
|
+
position: "fixed",
|
|
25
|
+
zIndex,
|
|
26
|
+
...extra,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Radix-compatible synthetic event for menu `onSelect`.
|
|
32
|
+
* Callers use `event.preventDefault()` to keep the menu open.
|
|
33
|
+
*/
|
|
34
|
+
export function createSelectEvent(): Event {
|
|
35
|
+
let prevented = false;
|
|
36
|
+
return {
|
|
37
|
+
preventDefault() {
|
|
38
|
+
prevented = true;
|
|
39
|
+
},
|
|
40
|
+
get defaultPrevented() {
|
|
41
|
+
return prevented;
|
|
42
|
+
},
|
|
43
|
+
} as Event;
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
|
|
4
|
+
export type PortalProps = {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
container?: HTMLElement | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Renders children into `document.body` (or a provided container) via React portal.
|
|
11
|
+
* Mounts synchronously on the client so floating content can be measured in the
|
|
12
|
+
* same layout pass (avoids width=0 positioning flashes).
|
|
13
|
+
*/
|
|
14
|
+
export function Portal({ children, container }: PortalProps) {
|
|
15
|
+
const target =
|
|
16
|
+
container ?? (typeof document !== "undefined" ? document.body : null);
|
|
17
|
+
|
|
18
|
+
if (!target) return null;
|
|
19
|
+
|
|
20
|
+
return createPortal(children, target);
|
|
21
|
+
}
|
package/src/lib/slot.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { composeRefs } from "./compose-refs";
|
|
3
|
+
|
|
4
|
+
type AnyProps = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merges two prop objects, composing event handlers and classNames,
|
|
8
|
+
* and merging style objects. Used by Slot for asChild composition.
|
|
9
|
+
*/
|
|
10
|
+
function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
|
|
11
|
+
const override: AnyProps = { ...childProps };
|
|
12
|
+
|
|
13
|
+
for (const propName of Object.keys(slotProps)) {
|
|
14
|
+
const slotValue = slotProps[propName];
|
|
15
|
+
const childValue = childProps[propName];
|
|
16
|
+
|
|
17
|
+
const isHandler = /^on[A-Z]/.test(propName);
|
|
18
|
+
if (isHandler) {
|
|
19
|
+
if (slotValue && childValue) {
|
|
20
|
+
override[propName] = (...args: unknown[]) => {
|
|
21
|
+
(childValue as (...a: unknown[]) => void)(...args);
|
|
22
|
+
(slotValue as (...a: unknown[]) => void)(...args);
|
|
23
|
+
};
|
|
24
|
+
} else if (slotValue) {
|
|
25
|
+
override[propName] = slotValue;
|
|
26
|
+
}
|
|
27
|
+
} else if (propName === "style") {
|
|
28
|
+
override[propName] = {
|
|
29
|
+
...((slotValue as React.CSSProperties) || {}),
|
|
30
|
+
...((childValue as React.CSSProperties) || {}),
|
|
31
|
+
};
|
|
32
|
+
} else if (propName === "className") {
|
|
33
|
+
override[propName] = [slotValue, childValue].filter(Boolean).join(" ");
|
|
34
|
+
} else if (childValue === undefined) {
|
|
35
|
+
override[propName] = slotValue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return override;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SlotProps = React.HTMLAttributes<HTMLElement> & {
|
|
43
|
+
children?: React.ReactNode;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Renders as its single React element child, merging Slot props onto that child.
|
|
48
|
+
* Used for `asChild` composition on Button, Badge, and overlay triggers.
|
|
49
|
+
*/
|
|
50
|
+
export const Slot = React.forwardRef<HTMLElement, SlotProps>(
|
|
51
|
+
({ children, ...props }, forwardedRef) => {
|
|
52
|
+
if (!React.isValidElement(children)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const child = children as React.ReactElement & {
|
|
57
|
+
ref?: React.Ref<HTMLElement>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const childProps = (child.props ?? {}) as AnyProps;
|
|
61
|
+
const merged = mergeProps(props as AnyProps, childProps);
|
|
62
|
+
|
|
63
|
+
return React.cloneElement(child, {
|
|
64
|
+
...merged,
|
|
65
|
+
ref: composeRefs(forwardedRef, child.ref),
|
|
66
|
+
} as AnyProps);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
Slot.displayName = "Slot";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface / card hierarchy class recipes.
|
|
3
|
+
* Use with GlassCard or SurfaceCard rather than copying Tailwind strings.
|
|
4
|
+
*/
|
|
5
|
+
import { cn } from "./cn";
|
|
6
|
+
|
|
7
|
+
export const surfaceRadiusCard = "rounded-[var(--radius-card)]";
|
|
8
|
+
export const surfaceRadiusControl = "rounded-[var(--radius-control)]";
|
|
9
|
+
export const surfaceRadiusPill = "rounded-[var(--radius-pill)]";
|
|
10
|
+
|
|
11
|
+
/** Soft profile-style card radius (squarish). */
|
|
12
|
+
export const surfaceRadiusProfile = "rounded-lg";
|
|
13
|
+
|
|
14
|
+
/** Primary elevated card (About / hero panels). */
|
|
15
|
+
export const surfacePrimaryClass = cn(
|
|
16
|
+
"relative h-full overflow-hidden",
|
|
17
|
+
surfaceRadiusProfile,
|
|
18
|
+
"border border-border/20",
|
|
19
|
+
"bg-gradient-to-br from-card via-card to-primary/[0.06] dark:from-card/95 dark:via-card/90 dark:to-primary/[0.08]",
|
|
20
|
+
"p-3.5 sm:p-5 lg:p-6",
|
|
21
|
+
"shadow-[0_18px_40px_-24px_rgba(0,0,0,0.35)] dark:shadow-[0_22px_48px_-20px_rgba(0,0,0,0.65)]",
|
|
22
|
+
"ring-1 ring-inset ring-white/25 dark:ring-white/8",
|
|
23
|
+
"backdrop-blur-xl",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/** Quieter secondary card. */
|
|
27
|
+
export const surfaceSecondaryClass = cn(
|
|
28
|
+
"relative h-full overflow-hidden",
|
|
29
|
+
surfaceRadiusProfile,
|
|
30
|
+
"border border-border/20",
|
|
31
|
+
"bg-muted/25 dark:bg-muted/15",
|
|
32
|
+
"p-3 sm:p-3.5 lg:p-4",
|
|
33
|
+
"shadow-none",
|
|
34
|
+
"backdrop-blur-md",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/** Detail glass card with soft elevation. */
|
|
38
|
+
export const surfaceDetailClass = cn(
|
|
39
|
+
"relative h-full overflow-hidden",
|
|
40
|
+
surfaceRadiusProfile,
|
|
41
|
+
"border border-border/25",
|
|
42
|
+
"bg-card/95 dark:bg-card/80",
|
|
43
|
+
"p-3 sm:p-4 lg:p-5",
|
|
44
|
+
"shadow-[0_10px_28px_-18px_rgba(0,0,0,0.28)] dark:shadow-[0_14px_32px_-16px_rgba(0,0,0,0.55)]",
|
|
45
|
+
"ring-1 ring-inset ring-white/40 dark:ring-white/8",
|
|
46
|
+
"backdrop-blur-xl",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/** Minimal dashed supporting strip. */
|
|
50
|
+
export const surfaceTertiaryClass = cn(
|
|
51
|
+
"relative overflow-hidden",
|
|
52
|
+
surfaceRadiusProfile,
|
|
53
|
+
"border border-dashed border-border/30",
|
|
54
|
+
"bg-transparent dark:bg-muted/5",
|
|
55
|
+
"p-3 sm:p-3.5",
|
|
56
|
+
"shadow-none",
|
|
57
|
+
);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
applyTheme,
|
|
4
|
+
DEFAULT_THEME,
|
|
5
|
+
isDarkTheme,
|
|
6
|
+
isThemeId,
|
|
7
|
+
SPATIKA_THEME_STORAGE_KEY,
|
|
8
|
+
readStoredTheme,
|
|
9
|
+
THEME_IDS,
|
|
10
|
+
THEME_LABELS,
|
|
11
|
+
writeStoredTheme,
|
|
12
|
+
} from "./themes";
|
|
13
|
+
|
|
14
|
+
function resetRoot() {
|
|
15
|
+
document.documentElement.className = "";
|
|
16
|
+
document.documentElement.removeAttribute("data-spk-theme");
|
|
17
|
+
document.documentElement.style.colorScheme = "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("themes", () => {
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
resetRoot();
|
|
23
|
+
localStorage.removeItem(SPATIKA_THEME_STORAGE_KEY);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("lists four named theme ids", () => {
|
|
27
|
+
expect(THEME_IDS).toEqual(["mukta", "neelam", "usha", "sandhya"]);
|
|
28
|
+
expect(DEFAULT_THEME).toBe("mukta");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("provides human labels for each theme", () => {
|
|
32
|
+
for (const id of THEME_IDS) {
|
|
33
|
+
expect(THEME_LABELS[id]).toBeTruthy();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("narrows theme ids", () => {
|
|
38
|
+
expect(isThemeId("neelam")).toBe(true);
|
|
39
|
+
expect(isThemeId("dark")).toBe(false);
|
|
40
|
+
expect(isDarkTheme("neelam")).toBe(true);
|
|
41
|
+
expect(isDarkTheme("usha")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("applies neelam theme class and data attribute", () => {
|
|
45
|
+
applyTheme("neelam");
|
|
46
|
+
expect(document.documentElement.classList.contains("neelam")).toBe(true);
|
|
47
|
+
expect(document.documentElement.getAttribute("data-spk-theme")).toBe("neelam");
|
|
48
|
+
expect(document.documentElement.style.colorScheme).toBe("dark");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("clears theme classes for mukta", () => {
|
|
52
|
+
applyTheme("neelam");
|
|
53
|
+
applyTheme("mukta");
|
|
54
|
+
expect(document.documentElement.classList.contains("neelam")).toBe(false);
|
|
55
|
+
expect(document.documentElement.getAttribute("data-spk-theme")).toBe("mukta");
|
|
56
|
+
expect(document.documentElement.style.colorScheme).toBe("light");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("applies usha and sandhya without leaving stale classes", () => {
|
|
60
|
+
applyTheme("usha");
|
|
61
|
+
applyTheme("sandhya");
|
|
62
|
+
expect(document.documentElement.classList.contains("usha")).toBe(false);
|
|
63
|
+
expect(document.documentElement.classList.contains("sandhya")).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("reads and writes stored themes", () => {
|
|
67
|
+
expect(readStoredTheme(SPATIKA_THEME_STORAGE_KEY)).toBeNull();
|
|
68
|
+
writeStoredTheme(SPATIKA_THEME_STORAGE_KEY, "sandhya");
|
|
69
|
+
expect(readStoredTheme(SPATIKA_THEME_STORAGE_KEY)).toBe("sandhya");
|
|
70
|
+
writeStoredTheme(false, "neelam");
|
|
71
|
+
expect(readStoredTheme(SPATIKA_THEME_STORAGE_KEY)).toBe("sandhya");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Supported theme identifiers applied as classes on a root element. */
|
|
2
|
+
export const THEME_IDS = ["mukta", "neelam", "usha", "sandhya"] as const;
|
|
3
|
+
|
|
4
|
+
export type ThemeId = (typeof THEME_IDS)[number];
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_THEME: ThemeId = "mukta";
|
|
7
|
+
|
|
8
|
+
/** Default `localStorage` key used by the docs site and `SpatikaThemeProvider`. */
|
|
9
|
+
export const SPATIKA_THEME_STORAGE_KEY = "spk-theme";
|
|
10
|
+
|
|
11
|
+
export const THEME_LABELS: Record<ThemeId, string> = {
|
|
12
|
+
mukta: "Mukta",
|
|
13
|
+
neelam: "Neelam",
|
|
14
|
+
usha: "Usha",
|
|
15
|
+
sandhya: "Sandhya",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const THEME_CLASS_LIST = ["neelam", "usha", "sandhya"] as const;
|
|
19
|
+
|
|
20
|
+
export function isThemeId(value: unknown): value is ThemeId {
|
|
21
|
+
return typeof value === "string" && (THEME_IDS as readonly string[]).includes(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isDarkTheme(theme: ThemeId): boolean {
|
|
25
|
+
return theme === "neelam" || theme === "sandhya";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Read a persisted theme. Returns `null` when missing or invalid. */
|
|
29
|
+
export function readStoredTheme(storageKey?: string | false): ThemeId | null {
|
|
30
|
+
if (!storageKey || typeof window === "undefined") return null;
|
|
31
|
+
try {
|
|
32
|
+
const value = window.localStorage.getItem(storageKey);
|
|
33
|
+
return isThemeId(value) ? value : null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Persist a theme. No-op when `storageKey` is unset. */
|
|
40
|
+
export function writeStoredTheme(storageKey: string | false | undefined, theme: ThemeId): void {
|
|
41
|
+
if (!storageKey || typeof window === "undefined") return;
|
|
42
|
+
try {
|
|
43
|
+
window.localStorage.setItem(storageKey, theme);
|
|
44
|
+
} catch {
|
|
45
|
+
// Private mode / quota — keep the in-memory theme.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Apply a theme class to an element (defaults to `<html>`). */
|
|
50
|
+
export function applyTheme(theme: ThemeId, target?: HTMLElement | null) {
|
|
51
|
+
if (typeof document === "undefined") return;
|
|
52
|
+
const root = target ?? document.documentElement;
|
|
53
|
+
root.classList.remove(...THEME_CLASS_LIST);
|
|
54
|
+
if (theme !== DEFAULT_THEME) {
|
|
55
|
+
root.classList.add(theme);
|
|
56
|
+
}
|
|
57
|
+
root.setAttribute("data-spk-theme", theme);
|
|
58
|
+
root.style.colorScheme = isDarkTheme(theme) ? "dark" : "light";
|
|
59
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
const tokensRoot = join(
|
|
7
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
8
|
+
"../../../tokens/src",
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
describe("@spatika/tokens", () => {
|
|
12
|
+
it("defines four theme roots", () => {
|
|
13
|
+
const css = readFileSync(join(tokensRoot, "tokens.css"), "utf8");
|
|
14
|
+
expect(css).toContain(":root");
|
|
15
|
+
expect(css).toContain(".neelam");
|
|
16
|
+
expect(css).toContain(".usha");
|
|
17
|
+
expect(css).toContain(".sandhya");
|
|
18
|
+
expect(css).toContain("--glass-bg");
|
|
19
|
+
expect(css).toContain("--primary");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("ships glass utilities and a11y helpers", () => {
|
|
23
|
+
const css = readFileSync(join(tokensRoot, "glass.css"), "utf8");
|
|
24
|
+
expect(css).toContain(".glass");
|
|
25
|
+
expect(css).toContain(".glass-panel");
|
|
26
|
+
expect(css).toContain(".spk-focus-ring");
|
|
27
|
+
expect(css).toContain(".spk-touch-target");
|
|
28
|
+
expect(css).toContain("prefers-reduced-motion");
|
|
29
|
+
expect(css).toContain(".glass-tabbar");
|
|
30
|
+
expect(css).toContain(".glass-page-toolbar");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("ships app chrome, cover bleed, and page-header type", () => {
|
|
34
|
+
const chrome = readFileSync(join(tokensRoot, "chrome.css"), "utf8");
|
|
35
|
+
expect(chrome).toContain(".app-chrome-header");
|
|
36
|
+
expect(chrome).toContain("data-cover-chrome-bleed");
|
|
37
|
+
expect(chrome).toContain(".story-identity");
|
|
38
|
+
expect(chrome).toContain(".profile-identity");
|
|
39
|
+
expect(chrome).toContain(".glass-page-toolbar");
|
|
40
|
+
expect(chrome).toContain(".app-page-chrome");
|
|
41
|
+
const tokens = readFileSync(join(tokensRoot, "tokens.css"), "utf8");
|
|
42
|
+
expect(tokens).toContain("--app-tabbar-height");
|
|
43
|
+
expect(tokens).toContain("--app-chrome-offset");
|
|
44
|
+
const surfaces = readFileSync(join(tokensRoot, "surfaces.css"), "utf8");
|
|
45
|
+
expect(surfaces).toContain(".page-header-title");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("respects reduced motion in base styles", () => {
|
|
49
|
+
const css = readFileSync(join(tokensRoot, "base.css"), "utf8");
|
|
50
|
+
expect(css).toContain("prefers-reduced-motion");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named typography roles for the Spatika design language.
|
|
3
|
+
* Prefer these over ad-hoc text-[10px] font-black uppercase strings.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Uppercase micro-label (section kickers, chip group titles). */
|
|
7
|
+
export const typographyEyebrow =
|
|
8
|
+
"text-[10px] font-black uppercase tracking-widest text-muted-foreground/70";
|
|
9
|
+
|
|
10
|
+
/** Slightly stronger eyebrow for page bands. */
|
|
11
|
+
export const typographyBandEyebrow =
|
|
12
|
+
"text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground/55";
|
|
13
|
+
|
|
14
|
+
/** Compact card / filter section title. */
|
|
15
|
+
export const typographySectionLabel =
|
|
16
|
+
"text-[10px] font-black uppercase tracking-widest text-muted-foreground";
|
|
17
|
+
|
|
18
|
+
/** Primary page title treatment (pairs with .app-title CSS when preferred). */
|
|
19
|
+
export const typographyPageTitle =
|
|
20
|
+
"text-2xl font-black tracking-tight text-foreground sm:text-3xl";
|
|
21
|
+
|
|
22
|
+
/** Supporting copy under titles. */
|
|
23
|
+
export const typographyPageSubtitle =
|
|
24
|
+
"text-sm font-medium leading-relaxed text-muted-foreground";
|
|
25
|
+
|
|
26
|
+
/** Dense body used in admin / settings panels. */
|
|
27
|
+
export const typographyBodyDense = "text-sm font-medium leading-snug text-foreground";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
type UseControllableStateParams<T> = {
|
|
4
|
+
prop?: T;
|
|
5
|
+
defaultProp?: T;
|
|
6
|
+
onChange?: (value: T) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Controlled/uncontrolled state helper (Radix-style).
|
|
11
|
+
* When `prop` is provided the state is controlled; otherwise it uses local state.
|
|
12
|
+
*/
|
|
13
|
+
export function useControllableState<T>({
|
|
14
|
+
prop,
|
|
15
|
+
defaultProp,
|
|
16
|
+
onChange,
|
|
17
|
+
}: UseControllableStateParams<T>): [T | undefined, (value: T | ((prev: T | undefined) => T)) => void] {
|
|
18
|
+
const [uncontrolled, setUncontrolled] = React.useState(defaultProp);
|
|
19
|
+
const isControlled = prop !== undefined;
|
|
20
|
+
const value = isControlled ? prop : uncontrolled;
|
|
21
|
+
|
|
22
|
+
const setValue = React.useCallback(
|
|
23
|
+
(next: T | ((prev: T | undefined) => T)) => {
|
|
24
|
+
const resolved =
|
|
25
|
+
typeof next === "function"
|
|
26
|
+
? (next as (prev: T | undefined) => T)(value)
|
|
27
|
+
: next;
|
|
28
|
+
|
|
29
|
+
if (!isControlled) {
|
|
30
|
+
setUncontrolled(resolved);
|
|
31
|
+
}
|
|
32
|
+
onChange?.(resolved);
|
|
33
|
+
},
|
|
34
|
+
[isControlled, onChange, value],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return [value, setValue];
|
|
38
|
+
}
|