@skewedaspect/sleekspace-ui 0.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/sleekspace-ui.css +12844 -0
- package/dist/sleekspace-ui.es.js +19021 -0
- package/dist/sleekspace-ui.umd.js +19040 -0
- package/docs/components/accordion.md +92 -0
- package/docs/components/alert.md +72 -0
- package/docs/components/avatar.md +69 -0
- package/docs/components/breadcrumbs.md +65 -0
- package/docs/components/button/_meta.yaml +12 -0
- package/docs/components/button/accessibility.md +16 -0
- package/docs/components/button/custom-colors.md +18 -0
- package/docs/components/button/icons.md +31 -0
- package/docs/components/button/intro.md +8 -0
- package/docs/components/button/kinds.md +25 -0
- package/docs/components/button/sizes.md +14 -0
- package/docs/components/button/states.md +12 -0
- package/docs/components/button/usage.md +23 -0
- package/docs/components/button/variants.md +14 -0
- package/docs/components/button.md +110 -0
- package/docs/components/card.md +87 -0
- package/docs/components/checkbox.md +77 -0
- package/docs/components/collapsible.md +71 -0
- package/docs/components/divider.md +62 -0
- package/docs/components/dropdown.md +88 -0
- package/docs/components/field.md +80 -0
- package/docs/components/group.md +41 -0
- package/docs/components/input.md +84 -0
- package/docs/components/listbox.md +82 -0
- package/docs/components/modal.md +101 -0
- package/docs/components/navbar.md +64 -0
- package/docs/components/number-input.md +78 -0
- package/docs/components/page.md +77 -0
- package/docs/components/pagination.md +88 -0
- package/docs/components/panel.md +74 -0
- package/docs/components/popover.md +93 -0
- package/docs/components/progress.md +76 -0
- package/docs/components/radio.md +86 -0
- package/docs/components/sidebar.md +74 -0
- package/docs/components/skeleton.md +76 -0
- package/docs/components/slider.md +94 -0
- package/docs/components/spinner.md +59 -0
- package/docs/components/switch.md +97 -0
- package/docs/components/table.md +91 -0
- package/docs/components/tabs.md +108 -0
- package/docs/components/tag.md +75 -0
- package/docs/components/tags-input.md +88 -0
- package/docs/components/textarea.md +80 -0
- package/docs/components/theme.md +65 -0
- package/docs/components/toast.md +95 -0
- package/docs/components/tooltip.md +90 -0
- package/docs/guides/custom-colors.md +84 -0
- package/docs/guides/design-tokens.md +105 -0
- package/docs/guides/getting-started.md +144 -0
- package/docs/guides/installation.md +62 -0
- package/docs/guides/theming.md +101 -0
- package/package.json +76 -0
- package/src/components/Accordion/SkAccordion.vue +133 -0
- package/src/components/Accordion/SkAccordionItem.vue +131 -0
- package/src/components/Accordion/index.ts +3 -0
- package/src/components/Accordion/types.ts +9 -0
- package/src/components/Alert/SkAlert.vue +137 -0
- package/src/components/Alert/types.ts +10 -0
- package/src/components/Avatar/SkAvatar.vue +141 -0
- package/src/components/Avatar/index.ts +8 -0
- package/src/components/Avatar/types.ts +31 -0
- package/src/components/Breadcrumbs/SkBreadcrumbItem.vue +76 -0
- package/src/components/Breadcrumbs/SkBreadcrumbSeparator.vue +38 -0
- package/src/components/Breadcrumbs/SkBreadcrumbs.vue +93 -0
- package/src/components/Breadcrumbs/index.ts +10 -0
- package/src/components/Breadcrumbs/types.ts +36 -0
- package/src/components/Button/SkButton.vue +148 -0
- package/src/components/Button/types.ts +21 -0
- package/src/components/Card/SkCard.vue +144 -0
- package/src/components/Card/types.ts +12 -0
- package/src/components/Checkbox/SkCheckbox.vue +136 -0
- package/src/components/Checkbox/index.ts +8 -0
- package/src/components/Checkbox/types.ts +10 -0
- package/src/components/Collapsible/SkCollapsible.vue +159 -0
- package/src/components/Collapsible/index.ts +2 -0
- package/src/components/Collapsible/types.ts +8 -0
- package/src/components/Divider/SkDivider.vue +63 -0
- package/src/components/Divider/types.ts +15 -0
- package/src/components/Dropdown/SkDropdown.vue +150 -0
- package/src/components/Dropdown/SkDropdownMenuItem.vue +58 -0
- package/src/components/Dropdown/SkDropdownMenuSeparator.vue +26 -0
- package/src/components/Dropdown/SkDropdownSubmenu.vue +107 -0
- package/src/components/Dropdown/index.ts +11 -0
- package/src/components/Dropdown/types.ts +11 -0
- package/src/components/Field/SkField.vue +152 -0
- package/src/components/Field/index.ts +8 -0
- package/src/components/Field/types.ts +7 -0
- package/src/components/Group/SkGroup.vue +52 -0
- package/src/components/Group/types.ts +10 -0
- package/src/components/Input/SkInput.vue +117 -0
- package/src/components/Input/index.ts +8 -0
- package/src/components/Input/types.ts +11 -0
- package/src/components/Listbox/SkListbox.vue +164 -0
- package/src/components/Listbox/SkListboxItem.vue +68 -0
- package/src/components/Listbox/SkListboxSeparator.vue +26 -0
- package/src/components/Listbox/index.ts +10 -0
- package/src/components/Listbox/types.ts +10 -0
- package/src/components/Modal/SkModal.vue +231 -0
- package/src/components/Modal/index.ts +8 -0
- package/src/components/Modal/types.ts +12 -0
- package/src/components/NavBar/SkNavBar.vue +83 -0
- package/src/components/NavBar/index.ts +8 -0
- package/src/components/NavBar/types.ts +15 -0
- package/src/components/NumberInput/SkNumberInput.vue +168 -0
- package/src/components/NumberInput/index.ts +8 -0
- package/src/components/NumberInput/types.ts +10 -0
- package/src/components/Page/SkPage.vue +94 -0
- package/src/components/Page/index.ts +8 -0
- package/src/components/Page/types.ts +21 -0
- package/src/components/Pagination/SkPagination.vue +185 -0
- package/src/components/Pagination/SkPaginationItem.vue +107 -0
- package/src/components/Pagination/index.ts +9 -0
- package/src/components/Pagination/types.ts +40 -0
- package/src/components/Panel/SkPanel.vue +96 -0
- package/src/components/Panel/types.ts +15 -0
- package/src/components/Popover/SkPopover.vue +185 -0
- package/src/components/Popover/index.ts +8 -0
- package/src/components/Popover/types.ts +11 -0
- package/src/components/Progress/SkProgress.vue +144 -0
- package/src/components/Progress/index.ts +8 -0
- package/src/components/Progress/types.ts +34 -0
- package/src/components/Radio/SkRadio.vue +110 -0
- package/src/components/Radio/SkRadioGroup.vue +92 -0
- package/src/components/Radio/index.ts +9 -0
- package/src/components/Radio/types.ts +11 -0
- package/src/components/Sidebar/README.md +405 -0
- package/src/components/Sidebar/SkSidebar.vue +88 -0
- package/src/components/Sidebar/SkSidebarItem.vue +58 -0
- package/src/components/Sidebar/SkSidebarSection.vue +40 -0
- package/src/components/Sidebar/types.ts +3 -0
- package/src/components/Skeleton/SkSkeleton.vue +171 -0
- package/src/components/Skeleton/index.ts +8 -0
- package/src/components/Skeleton/types.ts +31 -0
- package/src/components/Slider/SkSlider.vue +165 -0
- package/src/components/Slider/index.ts +8 -0
- package/src/components/Slider/types.ts +44 -0
- package/src/components/Spinner/SkSpinner.vue +105 -0
- package/src/components/Spinner/index.ts +8 -0
- package/src/components/Spinner/types.ts +28 -0
- package/src/components/Switch/SkSwitch.vue +215 -0
- package/src/components/Switch/index.ts +8 -0
- package/src/components/Switch/types.ts +12 -0
- package/src/components/Table/SkTable.vue +109 -0
- package/src/components/Table/index.ts +2 -0
- package/src/components/Table/types.ts +15 -0
- package/src/components/Tabs/README.md +331 -0
- package/src/components/Tabs/SkTab.vue +84 -0
- package/src/components/Tabs/SkTabList.vue +62 -0
- package/src/components/Tabs/SkTabPanel.vue +47 -0
- package/src/components/Tabs/SkTabPanels.vue +23 -0
- package/src/components/Tabs/SkTabs.vue +124 -0
- package/src/components/Tabs/types.ts +21 -0
- package/src/components/Tag/SkTag.vue +129 -0
- package/src/components/Tag/types.ts +15 -0
- package/src/components/TagsInput/SkTagsInput.vue +184 -0
- package/src/components/TagsInput/index.ts +8 -0
- package/src/components/TagsInput/types.ts +10 -0
- package/src/components/Textarea/SkTextarea.vue +117 -0
- package/src/components/Textarea/index.ts +8 -0
- package/src/components/Textarea/types.ts +10 -0
- package/src/components/Theme/SkTheme.vue +47 -0
- package/src/components/Theme/types.ts +17 -0
- package/src/components/Theme/useTheme.ts +131 -0
- package/src/components/Toast/SkToast.vue +156 -0
- package/src/components/Toast/SkToastProvider.vue +180 -0
- package/src/components/Toast/index.ts +15 -0
- package/src/components/Toast/types.ts +63 -0
- package/src/components/Toast/useToast.ts +78 -0
- package/src/components/Tooltip/SkTooltip.vue +162 -0
- package/src/components/Tooltip/SkTooltipProvider.vue +114 -0
- package/src/components/Tooltip/index.ts +9 -0
- package/src/components/Tooltip/types.ts +13 -0
- package/src/composables/useCustomColors.test.ts +505 -0
- package/src/composables/useCustomColors.ts +124 -0
- package/src/composables/usePortalContext.test.ts +402 -0
- package/src/composables/usePortalContext.ts +95 -0
- package/src/global.d.ts +76 -0
- package/src/index.ts +259 -0
- package/src/styles/_scrollbar.scss +100 -0
- package/src/styles/base/_fonts.scss +105 -0
- package/src/styles/base/_global.scss +47 -0
- package/src/styles/base/_index.scss +24 -0
- package/src/styles/base/_reset.scss +11 -0
- package/src/styles/base/_typography.scss +178 -0
- package/src/styles/components/_accordion.scss +250 -0
- package/src/styles/components/_alert.scss +239 -0
- package/src/styles/components/_avatar.scss +133 -0
- package/src/styles/components/_breadcrumbs.scss +137 -0
- package/src/styles/components/_button.scss +731 -0
- package/src/styles/components/_card.scss +141 -0
- package/src/styles/components/_checkbox.scss +232 -0
- package/src/styles/components/_collapsible.scss +158 -0
- package/src/styles/components/_divider.scss +121 -0
- package/src/styles/components/_field.scss +87 -0
- package/src/styles/components/_group.scss +138 -0
- package/src/styles/components/_index.scss +46 -0
- package/src/styles/components/_input.scss +205 -0
- package/src/styles/components/_listbox.scss +453 -0
- package/src/styles/components/_menu.scss +216 -0
- package/src/styles/components/_modal.scss +329 -0
- package/src/styles/components/_navbar.scss +258 -0
- package/src/styles/components/_number-input.scss +352 -0
- package/src/styles/components/_page.scss +98 -0
- package/src/styles/components/_pagination.scss +411 -0
- package/src/styles/components/_panel.scss +281 -0
- package/src/styles/components/_popover.scss +258 -0
- package/src/styles/components/_progress.scss +280 -0
- package/src/styles/components/_radio.scss +255 -0
- package/src/styles/components/_sidebar.scss +92 -0
- package/src/styles/components/_skeleton.scss +138 -0
- package/src/styles/components/_slider.scss +262 -0
- package/src/styles/components/_spinner.scss +331 -0
- package/src/styles/components/_switch.scss +370 -0
- package/src/styles/components/_table.scss +405 -0
- package/src/styles/components/_tabs.scss +486 -0
- package/src/styles/components/_tag.scss +425 -0
- package/src/styles/components/_tags-input.scss +279 -0
- package/src/styles/components/_textarea.scss +208 -0
- package/src/styles/components/_toast.scss +331 -0
- package/src/styles/components/_tooltip.scss +206 -0
- package/src/styles/fonts/Titillium_Web/OFL.txt +93 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-Black.ttf +0 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-Bold.ttf +0 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-BoldItalic.ttf +0 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-ExtraLight.ttf +0 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-ExtraLightItalic.ttf +0 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-Italic.ttf +0 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-Light.ttf +0 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-LightItalic.ttf +0 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-Regular.ttf +0 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-SemiBold.ttf +0 -0
- package/src/styles/fonts/Titillium_Web/TitilliumWeb-SemiBoldItalic.ttf +0 -0
- package/src/styles/index.scss +17 -0
- package/src/styles/mixins/_cut-border.scss +254 -0
- package/src/styles/mixins/_index.scss +7 -0
- package/src/styles/theme/_variables.scss +42 -0
- package/src/styles/themes/README.md +127 -0
- package/src/styles/themes/_colorful.scss +58 -0
- package/src/styles/themes/_greyscale.scss +58 -0
- package/src/styles/themes/index.scss +9 -0
- package/src/styles/tokens/README.md +268 -0
- package/src/styles/tokens/_foundation-borders.scss +26 -0
- package/src/styles/tokens/_foundation-colors.scss +169 -0
- package/src/styles/tokens/_foundation-glow.scss +36 -0
- package/src/styles/tokens/_foundation-radius.scss +53 -0
- package/src/styles/tokens/_foundation-scrollbar.scss +31 -0
- package/src/styles/tokens/_foundation-shadows.scss +37 -0
- package/src/styles/tokens/_foundation-space.scss +36 -0
- package/src/styles/tokens/_foundation-transitions.scss +37 -0
- package/src/styles/tokens/_foundation-typography.scss +58 -0
- package/src/styles/tokens/_semantic-color-kinds.scss +112 -0
- package/src/styles/tokens/_semantic-colors.scss +10 -0
- package/src/styles/tokens/_semantic-interactive.scss +29 -0
- package/src/styles/tokens/_semantic-scrollbar.scss +48 -0
- package/src/styles/tokens/_semantic-surfaces.scss +36 -0
- package/src/styles/tokens/index.scss +38 -0
- package/src/styles/tokens.scss +268 -0
- package/src/styles/utilities/_index.scss +9 -0
- package/src/styles/utilities/_typography.scss +121 -0
- package/src/types.ts +50 -0
- package/web-types.json +3524 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// Custom Colors Composable
|
|
3
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { type Ref, computed } from 'vue';
|
|
6
|
+
|
|
7
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Composable for handling custom color props in components.
|
|
11
|
+
*
|
|
12
|
+
* This composable provides a consistent way to apply custom colors to components by generating
|
|
13
|
+
* CSS variables that override the component's default color tokens. It supports any CSS color
|
|
14
|
+
* format including hex, rgb, hsl, oklch, named colors, and CSS variables.
|
|
15
|
+
*
|
|
16
|
+
* Works with any component that follows the CSS variable naming convention:
|
|
17
|
+
* - `--sk-{componentName}-color-base` for the base/background color
|
|
18
|
+
* - `--sk-{componentName}-fg` for the foreground/text color
|
|
19
|
+
*
|
|
20
|
+
* @param componentName - The component name used in CSS variable naming
|
|
21
|
+
* (e.g., 'button', 'panel', 'my-custom-component')
|
|
22
|
+
* @param baseColor - The base/background color (any CSS color value, including CSS variables)
|
|
23
|
+
* @param textColor - Optional foreground/text color. If not provided, falls back to `--sk-neutral-text`
|
|
24
|
+
*
|
|
25
|
+
* @returns Computed style object with CSS variables ready to bind to a component's style attribute
|
|
26
|
+
*
|
|
27
|
+
* @example Basic usage with base color only
|
|
28
|
+
* ```vue
|
|
29
|
+
* <script setup>
|
|
30
|
+
* import { useCustomColors } from '@/composables/useCustomColors';
|
|
31
|
+
*
|
|
32
|
+
* const props = defineProps<{ baseColor?: string }>();
|
|
33
|
+
* const customColors = useCustomColors('button', toRef(() => props.baseColor), undefined);
|
|
34
|
+
* </script>
|
|
35
|
+
*
|
|
36
|
+
* <template>
|
|
37
|
+
* <button :style="customColors">Click me</button>
|
|
38
|
+
* </template>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example With both base and text colors
|
|
42
|
+
* ```vue
|
|
43
|
+
* <SkButton base-color="oklch(0.7 0.25 300)" text-color="white">
|
|
44
|
+
* Custom Purple Button
|
|
45
|
+
* </SkButton>
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example Using CSS variables
|
|
49
|
+
* ```vue
|
|
50
|
+
* <SkPanel base-color="var(--my-custom-color)" text-color="var(--my-text-color)">
|
|
51
|
+
* Content
|
|
52
|
+
* </SkPanel>
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example Custom component
|
|
56
|
+
* ```vue
|
|
57
|
+
* <script setup>
|
|
58
|
+
* import { useCustomColors } from '@/composables/useCustomColors';
|
|
59
|
+
*
|
|
60
|
+
* const props = defineProps<{ baseColor?: string; textColor?: string }>();
|
|
61
|
+
* const customColors = useCustomColors('my-widget', toRef(() => props.baseColor), toRef(() => props.textColor));
|
|
62
|
+
* </script>
|
|
63
|
+
*
|
|
64
|
+
* <template>
|
|
65
|
+
* <div class="my-widget" :style="customColors">
|
|
66
|
+
* <!-- Will generate: --sk-my-widget-color-base and --sk-my-widget-fg -->
|
|
67
|
+
* </div>
|
|
68
|
+
* </template>
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* Generated CSS variables:
|
|
72
|
+
* - `--sk-{componentName}-color-base` - The base color for backgrounds and accents
|
|
73
|
+
* - `--sk-{componentName}-fg` - The foreground/text color
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* - If `textColor` is not provided, components will use `--sk-neutral-text` from the active theme
|
|
77
|
+
* - For best contrast, always provide `textColor` when using custom `baseColor`
|
|
78
|
+
* - The generated CSS variables integrate with the component's existing token system
|
|
79
|
+
* - Works with any component name - no need to register components beforehand
|
|
80
|
+
*/
|
|
81
|
+
export function useCustomColors(
|
|
82
|
+
componentName : string,
|
|
83
|
+
baseColor : Ref<string | undefined> | string | undefined,
|
|
84
|
+
textColor : Ref<string | undefined> | string | undefined
|
|
85
|
+
) : Ref<Record<string, string>>
|
|
86
|
+
{
|
|
87
|
+
return computed(() =>
|
|
88
|
+
{
|
|
89
|
+
const styles : Record<string, string> = {};
|
|
90
|
+
|
|
91
|
+
// Resolve refs to values
|
|
92
|
+
const baseColorValue = typeof baseColor === 'string' ? baseColor : baseColor?.value;
|
|
93
|
+
const textColorValue = typeof textColor === 'string' ? textColor : textColor?.value;
|
|
94
|
+
|
|
95
|
+
// Only apply custom colors if baseColor is provided
|
|
96
|
+
if(!baseColorValue)
|
|
97
|
+
{
|
|
98
|
+
return styles;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Set the base color CSS variable
|
|
102
|
+
const baseVarName = `--sk-${ componentName }-color-base`;
|
|
103
|
+
styles[baseVarName] = baseColorValue;
|
|
104
|
+
|
|
105
|
+
// Set or calculate text color
|
|
106
|
+
const fgVarName = `--sk-${ componentName }-fg`;
|
|
107
|
+
|
|
108
|
+
if(textColorValue)
|
|
109
|
+
{
|
|
110
|
+
// Use provided text color
|
|
111
|
+
styles[fgVarName] = textColorValue;
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
{
|
|
115
|
+
// Fallback to the theme's default text color
|
|
116
|
+
// Users should provide textColor for optimal contrast
|
|
117
|
+
styles[fgVarName] = 'var(--sk-neutral-text)';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return styles;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// Portal Context Composable Tests
|
|
3
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { defineComponent, h, inject, provide, ref } from 'vue';
|
|
7
|
+
import { mount } from '@vue/test-utils';
|
|
8
|
+
import { usePortalContext } from './usePortalContext';
|
|
9
|
+
|
|
10
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
describe('usePortalContext', () =>
|
|
13
|
+
{
|
|
14
|
+
describe('theme injection', () =>
|
|
15
|
+
{
|
|
16
|
+
it('should return default greyscale theme when no provider exists', () =>
|
|
17
|
+
{
|
|
18
|
+
const TestComponent = defineComponent({
|
|
19
|
+
setup()
|
|
20
|
+
{
|
|
21
|
+
const { theme } = usePortalContext();
|
|
22
|
+
return { theme };
|
|
23
|
+
},
|
|
24
|
+
render()
|
|
25
|
+
{
|
|
26
|
+
return h('div', { 'data-scheme': this.theme });
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const wrapper = mount(TestComponent);
|
|
31
|
+
expect(wrapper.attributes('data-scheme')).toBe('greyscale');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should inject theme from parent provider', () =>
|
|
35
|
+
{
|
|
36
|
+
const TestComponent = defineComponent({
|
|
37
|
+
setup()
|
|
38
|
+
{
|
|
39
|
+
const { theme } = usePortalContext();
|
|
40
|
+
return { theme };
|
|
41
|
+
},
|
|
42
|
+
render()
|
|
43
|
+
{
|
|
44
|
+
return h('div', { 'data-scheme': this.theme });
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const ParentComponent = defineComponent({
|
|
49
|
+
setup()
|
|
50
|
+
{
|
|
51
|
+
provide('sk-theme', ref('colorful'));
|
|
52
|
+
},
|
|
53
|
+
render()
|
|
54
|
+
{
|
|
55
|
+
return h(TestComponent);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const wrapper = mount(ParentComponent);
|
|
60
|
+
expect(wrapper.find('div').attributes('data-scheme')).toBe('colorful');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should be reactive to theme changes', async () =>
|
|
64
|
+
{
|
|
65
|
+
const themeRef = ref('greyscale');
|
|
66
|
+
|
|
67
|
+
const TestComponent = defineComponent({
|
|
68
|
+
setup()
|
|
69
|
+
{
|
|
70
|
+
const { theme } = usePortalContext();
|
|
71
|
+
return { theme };
|
|
72
|
+
},
|
|
73
|
+
render()
|
|
74
|
+
{
|
|
75
|
+
return h('div', { 'data-scheme': this.theme });
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const ParentComponent = defineComponent({
|
|
80
|
+
setup()
|
|
81
|
+
{
|
|
82
|
+
provide('sk-theme', themeRef);
|
|
83
|
+
},
|
|
84
|
+
render()
|
|
85
|
+
{
|
|
86
|
+
return h(TestComponent);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const wrapper = mount(ParentComponent);
|
|
91
|
+
expect(wrapper.find('div').attributes('data-scheme')).toBe('greyscale');
|
|
92
|
+
|
|
93
|
+
themeRef.value = 'colorful';
|
|
94
|
+
await wrapper.vm.$nextTick();
|
|
95
|
+
|
|
96
|
+
expect(wrapper.find('div').attributes('data-scheme')).toBe('colorful');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('theme re-provision for nested portals', () =>
|
|
101
|
+
{
|
|
102
|
+
it('should re-provide theme for child components', () =>
|
|
103
|
+
{
|
|
104
|
+
// This child component injects from the re-provided context
|
|
105
|
+
const ChildComponent = defineComponent({
|
|
106
|
+
setup()
|
|
107
|
+
{
|
|
108
|
+
const theme = inject('sk-theme', ref('default'));
|
|
109
|
+
return { theme };
|
|
110
|
+
},
|
|
111
|
+
render()
|
|
112
|
+
{
|
|
113
|
+
return h('span', { 'class': 'child', 'data-theme': this.theme });
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// This component uses usePortalContext which re-provides
|
|
118
|
+
const PortalComponent = defineComponent({
|
|
119
|
+
setup()
|
|
120
|
+
{
|
|
121
|
+
const { theme } = usePortalContext();
|
|
122
|
+
return { theme };
|
|
123
|
+
},
|
|
124
|
+
render()
|
|
125
|
+
{
|
|
126
|
+
return h('div', { 'data-scheme': this.theme }, [
|
|
127
|
+
h(ChildComponent),
|
|
128
|
+
]);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Root provides the theme
|
|
133
|
+
const RootComponent = defineComponent({
|
|
134
|
+
setup()
|
|
135
|
+
{
|
|
136
|
+
provide('sk-theme', ref('colorful'));
|
|
137
|
+
},
|
|
138
|
+
render()
|
|
139
|
+
{
|
|
140
|
+
return h(PortalComponent);
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const wrapper = mount(RootComponent);
|
|
145
|
+
expect(wrapper.find('.child').attributes('data-theme')).toBe('colorful');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should support nested portal components', () =>
|
|
149
|
+
{
|
|
150
|
+
// Inner portal component (like Listbox inside Modal)
|
|
151
|
+
const InnerPortalComponent = defineComponent({
|
|
152
|
+
setup()
|
|
153
|
+
{
|
|
154
|
+
const { theme } = usePortalContext();
|
|
155
|
+
return { theme };
|
|
156
|
+
},
|
|
157
|
+
render()
|
|
158
|
+
{
|
|
159
|
+
return h('div', { 'class': 'inner', 'data-scheme': this.theme });
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Outer portal component (like Modal)
|
|
164
|
+
const OuterPortalComponent = defineComponent({
|
|
165
|
+
setup()
|
|
166
|
+
{
|
|
167
|
+
const { theme } = usePortalContext();
|
|
168
|
+
return { theme };
|
|
169
|
+
},
|
|
170
|
+
render()
|
|
171
|
+
{
|
|
172
|
+
return h('div', { 'class': 'outer', 'data-scheme': this.theme }, [
|
|
173
|
+
h(InnerPortalComponent),
|
|
174
|
+
]);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Root provides the theme
|
|
179
|
+
const RootComponent = defineComponent({
|
|
180
|
+
setup()
|
|
181
|
+
{
|
|
182
|
+
provide('sk-theme', ref('colorful'));
|
|
183
|
+
},
|
|
184
|
+
render()
|
|
185
|
+
{
|
|
186
|
+
return h(OuterPortalComponent);
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const wrapper = mount(RootComponent);
|
|
191
|
+
expect(wrapper.find('.outer').attributes('data-scheme')).toBe('colorful');
|
|
192
|
+
expect(wrapper.find('.inner').attributes('data-scheme')).toBe('colorful');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle deeply nested portal components', () =>
|
|
196
|
+
{
|
|
197
|
+
const createPortalComponent = (name : string, children : any[] = []) : ReturnType<typeof defineComponent> =>
|
|
198
|
+
{
|
|
199
|
+
return defineComponent({
|
|
200
|
+
name,
|
|
201
|
+
setup()
|
|
202
|
+
{
|
|
203
|
+
const { theme } = usePortalContext();
|
|
204
|
+
return { theme };
|
|
205
|
+
},
|
|
206
|
+
render()
|
|
207
|
+
{
|
|
208
|
+
return h('div', { 'class': name, 'data-scheme': this.theme }, children);
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Create 5 levels of nested portal components
|
|
214
|
+
const Level5 = createPortalComponent('level-5');
|
|
215
|
+
const Level4 = createPortalComponent('level-4', [ h(Level5) ]);
|
|
216
|
+
const Level3 = createPortalComponent('level-3', [ h(Level4) ]);
|
|
217
|
+
const Level2 = createPortalComponent('level-2', [ h(Level3) ]);
|
|
218
|
+
const Level1 = createPortalComponent('level-1', [ h(Level2) ]);
|
|
219
|
+
|
|
220
|
+
const RootComponent = defineComponent({
|
|
221
|
+
setup()
|
|
222
|
+
{
|
|
223
|
+
provide('sk-theme', ref('colorful'));
|
|
224
|
+
},
|
|
225
|
+
render()
|
|
226
|
+
{
|
|
227
|
+
return h(Level1);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const wrapper = mount(RootComponent);
|
|
232
|
+
|
|
233
|
+
// All levels should receive the theme
|
|
234
|
+
expect(wrapper.find('.level-1').attributes('data-scheme')).toBe('colorful');
|
|
235
|
+
expect(wrapper.find('.level-2').attributes('data-scheme')).toBe('colorful');
|
|
236
|
+
expect(wrapper.find('.level-3').attributes('data-scheme')).toBe('colorful');
|
|
237
|
+
expect(wrapper.find('.level-4').attributes('data-scheme')).toBe('colorful');
|
|
238
|
+
expect(wrapper.find('.level-5').attributes('data-scheme')).toBe('colorful');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should propagate theme changes through all nested levels', async () =>
|
|
242
|
+
{
|
|
243
|
+
const themeRef = ref('greyscale');
|
|
244
|
+
|
|
245
|
+
const InnerComponent = defineComponent({
|
|
246
|
+
setup()
|
|
247
|
+
{
|
|
248
|
+
const { theme } = usePortalContext();
|
|
249
|
+
return { theme };
|
|
250
|
+
},
|
|
251
|
+
render()
|
|
252
|
+
{
|
|
253
|
+
return h('span', { 'class': 'inner', 'data-scheme': this.theme });
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const OuterComponent = defineComponent({
|
|
258
|
+
setup()
|
|
259
|
+
{
|
|
260
|
+
const { theme } = usePortalContext();
|
|
261
|
+
return { theme };
|
|
262
|
+
},
|
|
263
|
+
render()
|
|
264
|
+
{
|
|
265
|
+
return h('div', { 'class': 'outer', 'data-scheme': this.theme }, [
|
|
266
|
+
h(InnerComponent),
|
|
267
|
+
]);
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const RootComponent = defineComponent({
|
|
272
|
+
setup()
|
|
273
|
+
{
|
|
274
|
+
provide('sk-theme', themeRef);
|
|
275
|
+
},
|
|
276
|
+
render()
|
|
277
|
+
{
|
|
278
|
+
return h(OuterComponent);
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const wrapper = mount(RootComponent);
|
|
283
|
+
|
|
284
|
+
expect(wrapper.find('.outer').attributes('data-scheme')).toBe('greyscale');
|
|
285
|
+
expect(wrapper.find('.inner').attributes('data-scheme')).toBe('greyscale');
|
|
286
|
+
|
|
287
|
+
themeRef.value = 'colorful';
|
|
288
|
+
await wrapper.vm.$nextTick();
|
|
289
|
+
|
|
290
|
+
expect(wrapper.find('.outer').attributes('data-scheme')).toBe('colorful');
|
|
291
|
+
expect(wrapper.find('.inner').attributes('data-scheme')).toBe('colorful');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('default behavior without SkTheme', () =>
|
|
296
|
+
{
|
|
297
|
+
it('should work without any parent theme provider', () =>
|
|
298
|
+
{
|
|
299
|
+
const TestComponent = defineComponent({
|
|
300
|
+
setup()
|
|
301
|
+
{
|
|
302
|
+
const { theme } = usePortalContext();
|
|
303
|
+
return { theme };
|
|
304
|
+
},
|
|
305
|
+
render()
|
|
306
|
+
{
|
|
307
|
+
return h('div', { 'data-scheme': this.theme });
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const wrapper = mount(TestComponent);
|
|
312
|
+
// Should default to greyscale
|
|
313
|
+
expect(wrapper.attributes('data-scheme')).toBe('greyscale');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should still re-provide default theme for nested components', () =>
|
|
317
|
+
{
|
|
318
|
+
const ChildComponent = defineComponent({
|
|
319
|
+
setup()
|
|
320
|
+
{
|
|
321
|
+
const theme = inject('sk-theme', ref('fallback'));
|
|
322
|
+
return { theme };
|
|
323
|
+
},
|
|
324
|
+
render()
|
|
325
|
+
{
|
|
326
|
+
return h('span', { 'class': 'child', 'data-theme': this.theme });
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const ParentComponent = defineComponent({
|
|
331
|
+
setup()
|
|
332
|
+
{
|
|
333
|
+
const { theme } = usePortalContext();
|
|
334
|
+
return { theme };
|
|
335
|
+
},
|
|
336
|
+
render()
|
|
337
|
+
{
|
|
338
|
+
return h('div', [
|
|
339
|
+
h(ChildComponent),
|
|
340
|
+
]);
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Mount without any theme provider
|
|
345
|
+
const wrapper = mount(ParentComponent);
|
|
346
|
+
|
|
347
|
+
// Child should receive the default greyscale theme
|
|
348
|
+
expect(wrapper.find('.child').attributes('data-theme')).toBe('greyscale');
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('multiple instances', () =>
|
|
353
|
+
{
|
|
354
|
+
it('should handle multiple portal components at the same level', () =>
|
|
355
|
+
{
|
|
356
|
+
const PortalA = defineComponent({
|
|
357
|
+
setup()
|
|
358
|
+
{
|
|
359
|
+
const { theme } = usePortalContext();
|
|
360
|
+
return { theme };
|
|
361
|
+
},
|
|
362
|
+
render()
|
|
363
|
+
{
|
|
364
|
+
return h('div', { 'class': 'portal-a', 'data-scheme': this.theme });
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const PortalB = defineComponent({
|
|
369
|
+
setup()
|
|
370
|
+
{
|
|
371
|
+
const { theme } = usePortalContext();
|
|
372
|
+
return { theme };
|
|
373
|
+
},
|
|
374
|
+
render()
|
|
375
|
+
{
|
|
376
|
+
return h('div', { 'class': 'portal-b', 'data-scheme': this.theme });
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const RootComponent = defineComponent({
|
|
381
|
+
setup()
|
|
382
|
+
{
|
|
383
|
+
provide('sk-theme', ref('colorful'));
|
|
384
|
+
},
|
|
385
|
+
render()
|
|
386
|
+
{
|
|
387
|
+
return h('div', [
|
|
388
|
+
h(PortalA),
|
|
389
|
+
h(PortalB),
|
|
390
|
+
]);
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const wrapper = mount(RootComponent);
|
|
395
|
+
|
|
396
|
+
expect(wrapper.find('.portal-a').attributes('data-scheme')).toBe('colorful');
|
|
397
|
+
expect(wrapper.find('.portal-b').attributes('data-scheme')).toBe('colorful');
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// Portal Context Composable
|
|
3
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { type Ref, inject, provide, readonly, ref } from 'vue';
|
|
6
|
+
|
|
7
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Composable for handling context propagation through portals.
|
|
11
|
+
*
|
|
12
|
+
* **The Portal Problem:**
|
|
13
|
+
* When components use portals (Modal, Dropdown, Tooltip, Listbox, Popover), the portaled content
|
|
14
|
+
* is teleported to `<body>`, breaking two critical things:
|
|
15
|
+
*
|
|
16
|
+
* 1. **CSS Cascade** - CSS custom properties defined on parent elements don't inherit to portaled
|
|
17
|
+
* content because it's no longer a DOM descendant.
|
|
18
|
+
*
|
|
19
|
+
* 2. **Vue's provide/inject** - The Vue component tree is preserved, but nested portal components
|
|
20
|
+
* (e.g., Listbox inside Modal) need the theme re-provided to reach their own portaled content.
|
|
21
|
+
*
|
|
22
|
+
* **This composable solves both problems by:**
|
|
23
|
+
* - Injecting the current theme (with a safe default)
|
|
24
|
+
* - Re-providing it for any nested portal components
|
|
25
|
+
* - Returning the theme ref for binding to `data-scheme` on portaled elements
|
|
26
|
+
*
|
|
27
|
+
* **IMPORTANT:** All portal components MUST:
|
|
28
|
+
* 1. Use this composable
|
|
29
|
+
* 2. Apply `:data-scheme="theme"` to their portaled content root element(s)
|
|
30
|
+
* 3. Apply kind classes directly on portaled content (CSS can't cascade from parent)
|
|
31
|
+
*
|
|
32
|
+
* @returns Object containing:
|
|
33
|
+
* - `theme`: Readonly ref of current theme name for `:data-scheme` binding
|
|
34
|
+
*
|
|
35
|
+
* @example Basic usage in a portal component
|
|
36
|
+
* ```vue
|
|
37
|
+
* <script setup lang="ts">
|
|
38
|
+
* import { usePortalContext } from '@/composables/usePortalContext';
|
|
39
|
+
*
|
|
40
|
+
* const { theme } = usePortalContext();
|
|
41
|
+
* </script>
|
|
42
|
+
*
|
|
43
|
+
* <template>
|
|
44
|
+
* <SomePortal>
|
|
45
|
+
* <SomeContent :data-scheme="theme" :class="contentClasses">
|
|
46
|
+
* <slot />
|
|
47
|
+
* </SomeContent>
|
|
48
|
+
* </SomePortal>
|
|
49
|
+
* </template>
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @example With multiple portaled elements (Modal with overlay)
|
|
53
|
+
* ```vue
|
|
54
|
+
* <template>
|
|
55
|
+
* <DialogPortal>
|
|
56
|
+
* <DialogOverlay :data-scheme="theme" />
|
|
57
|
+
* <DialogContent :data-scheme="theme" :class="contentClasses">
|
|
58
|
+
* <slot />
|
|
59
|
+
* </DialogContent>
|
|
60
|
+
* </DialogPortal>
|
|
61
|
+
* </template>
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* - The default theme is 'greyscale' if no SkTheme wrapper is present
|
|
66
|
+
* - This composable automatically handles provide/inject - you don't need to do it manually
|
|
67
|
+
* - The returned `theme` ref is readonly to prevent accidental mutation
|
|
68
|
+
* - Nested portals (e.g., Dropdown inside Modal) work automatically because each portal
|
|
69
|
+
* component re-provides the theme
|
|
70
|
+
*
|
|
71
|
+
* **Portal components in this library:**
|
|
72
|
+
* - SkModal (DialogPortal)
|
|
73
|
+
* - SkDropdown (DropdownMenuPortal)
|
|
74
|
+
* - SkDropdownSubmenu (DropdownMenuPortal)
|
|
75
|
+
* - SkListbox (ComboboxPortal)
|
|
76
|
+
* - SkTooltip (TooltipPortal)
|
|
77
|
+
* - SkPopover (PopoverPortal) - future
|
|
78
|
+
* - SkToast (ToastPortal) - future
|
|
79
|
+
*/
|
|
80
|
+
export function usePortalContext() : { theme : Ref<string> }
|
|
81
|
+
{
|
|
82
|
+
// Inject the current theme from SkTheme or use default
|
|
83
|
+
// Always use a ref to ensure reactivity through portals
|
|
84
|
+
const currentTheme = inject('sk-theme', readonly(ref('greyscale')));
|
|
85
|
+
|
|
86
|
+
// Re-provide for nested portal components
|
|
87
|
+
// This is critical for components like Listbox inside Modal
|
|
88
|
+
provide('sk-theme', currentTheme);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
theme: currentTheme as Ref<string>,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//----------------------------------------------------------------------------------------------------------------------
|