@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.
Files changed (266) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/dist/sleekspace-ui.css +12844 -0
  4. package/dist/sleekspace-ui.es.js +19021 -0
  5. package/dist/sleekspace-ui.umd.js +19040 -0
  6. package/docs/components/accordion.md +92 -0
  7. package/docs/components/alert.md +72 -0
  8. package/docs/components/avatar.md +69 -0
  9. package/docs/components/breadcrumbs.md +65 -0
  10. package/docs/components/button/_meta.yaml +12 -0
  11. package/docs/components/button/accessibility.md +16 -0
  12. package/docs/components/button/custom-colors.md +18 -0
  13. package/docs/components/button/icons.md +31 -0
  14. package/docs/components/button/intro.md +8 -0
  15. package/docs/components/button/kinds.md +25 -0
  16. package/docs/components/button/sizes.md +14 -0
  17. package/docs/components/button/states.md +12 -0
  18. package/docs/components/button/usage.md +23 -0
  19. package/docs/components/button/variants.md +14 -0
  20. package/docs/components/button.md +110 -0
  21. package/docs/components/card.md +87 -0
  22. package/docs/components/checkbox.md +77 -0
  23. package/docs/components/collapsible.md +71 -0
  24. package/docs/components/divider.md +62 -0
  25. package/docs/components/dropdown.md +88 -0
  26. package/docs/components/field.md +80 -0
  27. package/docs/components/group.md +41 -0
  28. package/docs/components/input.md +84 -0
  29. package/docs/components/listbox.md +82 -0
  30. package/docs/components/modal.md +101 -0
  31. package/docs/components/navbar.md +64 -0
  32. package/docs/components/number-input.md +78 -0
  33. package/docs/components/page.md +77 -0
  34. package/docs/components/pagination.md +88 -0
  35. package/docs/components/panel.md +74 -0
  36. package/docs/components/popover.md +93 -0
  37. package/docs/components/progress.md +76 -0
  38. package/docs/components/radio.md +86 -0
  39. package/docs/components/sidebar.md +74 -0
  40. package/docs/components/skeleton.md +76 -0
  41. package/docs/components/slider.md +94 -0
  42. package/docs/components/spinner.md +59 -0
  43. package/docs/components/switch.md +97 -0
  44. package/docs/components/table.md +91 -0
  45. package/docs/components/tabs.md +108 -0
  46. package/docs/components/tag.md +75 -0
  47. package/docs/components/tags-input.md +88 -0
  48. package/docs/components/textarea.md +80 -0
  49. package/docs/components/theme.md +65 -0
  50. package/docs/components/toast.md +95 -0
  51. package/docs/components/tooltip.md +90 -0
  52. package/docs/guides/custom-colors.md +84 -0
  53. package/docs/guides/design-tokens.md +105 -0
  54. package/docs/guides/getting-started.md +144 -0
  55. package/docs/guides/installation.md +62 -0
  56. package/docs/guides/theming.md +101 -0
  57. package/package.json +76 -0
  58. package/src/components/Accordion/SkAccordion.vue +133 -0
  59. package/src/components/Accordion/SkAccordionItem.vue +131 -0
  60. package/src/components/Accordion/index.ts +3 -0
  61. package/src/components/Accordion/types.ts +9 -0
  62. package/src/components/Alert/SkAlert.vue +137 -0
  63. package/src/components/Alert/types.ts +10 -0
  64. package/src/components/Avatar/SkAvatar.vue +141 -0
  65. package/src/components/Avatar/index.ts +8 -0
  66. package/src/components/Avatar/types.ts +31 -0
  67. package/src/components/Breadcrumbs/SkBreadcrumbItem.vue +76 -0
  68. package/src/components/Breadcrumbs/SkBreadcrumbSeparator.vue +38 -0
  69. package/src/components/Breadcrumbs/SkBreadcrumbs.vue +93 -0
  70. package/src/components/Breadcrumbs/index.ts +10 -0
  71. package/src/components/Breadcrumbs/types.ts +36 -0
  72. package/src/components/Button/SkButton.vue +148 -0
  73. package/src/components/Button/types.ts +21 -0
  74. package/src/components/Card/SkCard.vue +144 -0
  75. package/src/components/Card/types.ts +12 -0
  76. package/src/components/Checkbox/SkCheckbox.vue +136 -0
  77. package/src/components/Checkbox/index.ts +8 -0
  78. package/src/components/Checkbox/types.ts +10 -0
  79. package/src/components/Collapsible/SkCollapsible.vue +159 -0
  80. package/src/components/Collapsible/index.ts +2 -0
  81. package/src/components/Collapsible/types.ts +8 -0
  82. package/src/components/Divider/SkDivider.vue +63 -0
  83. package/src/components/Divider/types.ts +15 -0
  84. package/src/components/Dropdown/SkDropdown.vue +150 -0
  85. package/src/components/Dropdown/SkDropdownMenuItem.vue +58 -0
  86. package/src/components/Dropdown/SkDropdownMenuSeparator.vue +26 -0
  87. package/src/components/Dropdown/SkDropdownSubmenu.vue +107 -0
  88. package/src/components/Dropdown/index.ts +11 -0
  89. package/src/components/Dropdown/types.ts +11 -0
  90. package/src/components/Field/SkField.vue +152 -0
  91. package/src/components/Field/index.ts +8 -0
  92. package/src/components/Field/types.ts +7 -0
  93. package/src/components/Group/SkGroup.vue +52 -0
  94. package/src/components/Group/types.ts +10 -0
  95. package/src/components/Input/SkInput.vue +117 -0
  96. package/src/components/Input/index.ts +8 -0
  97. package/src/components/Input/types.ts +11 -0
  98. package/src/components/Listbox/SkListbox.vue +164 -0
  99. package/src/components/Listbox/SkListboxItem.vue +68 -0
  100. package/src/components/Listbox/SkListboxSeparator.vue +26 -0
  101. package/src/components/Listbox/index.ts +10 -0
  102. package/src/components/Listbox/types.ts +10 -0
  103. package/src/components/Modal/SkModal.vue +231 -0
  104. package/src/components/Modal/index.ts +8 -0
  105. package/src/components/Modal/types.ts +12 -0
  106. package/src/components/NavBar/SkNavBar.vue +83 -0
  107. package/src/components/NavBar/index.ts +8 -0
  108. package/src/components/NavBar/types.ts +15 -0
  109. package/src/components/NumberInput/SkNumberInput.vue +168 -0
  110. package/src/components/NumberInput/index.ts +8 -0
  111. package/src/components/NumberInput/types.ts +10 -0
  112. package/src/components/Page/SkPage.vue +94 -0
  113. package/src/components/Page/index.ts +8 -0
  114. package/src/components/Page/types.ts +21 -0
  115. package/src/components/Pagination/SkPagination.vue +185 -0
  116. package/src/components/Pagination/SkPaginationItem.vue +107 -0
  117. package/src/components/Pagination/index.ts +9 -0
  118. package/src/components/Pagination/types.ts +40 -0
  119. package/src/components/Panel/SkPanel.vue +96 -0
  120. package/src/components/Panel/types.ts +15 -0
  121. package/src/components/Popover/SkPopover.vue +185 -0
  122. package/src/components/Popover/index.ts +8 -0
  123. package/src/components/Popover/types.ts +11 -0
  124. package/src/components/Progress/SkProgress.vue +144 -0
  125. package/src/components/Progress/index.ts +8 -0
  126. package/src/components/Progress/types.ts +34 -0
  127. package/src/components/Radio/SkRadio.vue +110 -0
  128. package/src/components/Radio/SkRadioGroup.vue +92 -0
  129. package/src/components/Radio/index.ts +9 -0
  130. package/src/components/Radio/types.ts +11 -0
  131. package/src/components/Sidebar/README.md +405 -0
  132. package/src/components/Sidebar/SkSidebar.vue +88 -0
  133. package/src/components/Sidebar/SkSidebarItem.vue +58 -0
  134. package/src/components/Sidebar/SkSidebarSection.vue +40 -0
  135. package/src/components/Sidebar/types.ts +3 -0
  136. package/src/components/Skeleton/SkSkeleton.vue +171 -0
  137. package/src/components/Skeleton/index.ts +8 -0
  138. package/src/components/Skeleton/types.ts +31 -0
  139. package/src/components/Slider/SkSlider.vue +165 -0
  140. package/src/components/Slider/index.ts +8 -0
  141. package/src/components/Slider/types.ts +44 -0
  142. package/src/components/Spinner/SkSpinner.vue +105 -0
  143. package/src/components/Spinner/index.ts +8 -0
  144. package/src/components/Spinner/types.ts +28 -0
  145. package/src/components/Switch/SkSwitch.vue +215 -0
  146. package/src/components/Switch/index.ts +8 -0
  147. package/src/components/Switch/types.ts +12 -0
  148. package/src/components/Table/SkTable.vue +109 -0
  149. package/src/components/Table/index.ts +2 -0
  150. package/src/components/Table/types.ts +15 -0
  151. package/src/components/Tabs/README.md +331 -0
  152. package/src/components/Tabs/SkTab.vue +84 -0
  153. package/src/components/Tabs/SkTabList.vue +62 -0
  154. package/src/components/Tabs/SkTabPanel.vue +47 -0
  155. package/src/components/Tabs/SkTabPanels.vue +23 -0
  156. package/src/components/Tabs/SkTabs.vue +124 -0
  157. package/src/components/Tabs/types.ts +21 -0
  158. package/src/components/Tag/SkTag.vue +129 -0
  159. package/src/components/Tag/types.ts +15 -0
  160. package/src/components/TagsInput/SkTagsInput.vue +184 -0
  161. package/src/components/TagsInput/index.ts +8 -0
  162. package/src/components/TagsInput/types.ts +10 -0
  163. package/src/components/Textarea/SkTextarea.vue +117 -0
  164. package/src/components/Textarea/index.ts +8 -0
  165. package/src/components/Textarea/types.ts +10 -0
  166. package/src/components/Theme/SkTheme.vue +47 -0
  167. package/src/components/Theme/types.ts +17 -0
  168. package/src/components/Theme/useTheme.ts +131 -0
  169. package/src/components/Toast/SkToast.vue +156 -0
  170. package/src/components/Toast/SkToastProvider.vue +180 -0
  171. package/src/components/Toast/index.ts +15 -0
  172. package/src/components/Toast/types.ts +63 -0
  173. package/src/components/Toast/useToast.ts +78 -0
  174. package/src/components/Tooltip/SkTooltip.vue +162 -0
  175. package/src/components/Tooltip/SkTooltipProvider.vue +114 -0
  176. package/src/components/Tooltip/index.ts +9 -0
  177. package/src/components/Tooltip/types.ts +13 -0
  178. package/src/composables/useCustomColors.test.ts +505 -0
  179. package/src/composables/useCustomColors.ts +124 -0
  180. package/src/composables/usePortalContext.test.ts +402 -0
  181. package/src/composables/usePortalContext.ts +95 -0
  182. package/src/global.d.ts +76 -0
  183. package/src/index.ts +259 -0
  184. package/src/styles/_scrollbar.scss +100 -0
  185. package/src/styles/base/_fonts.scss +105 -0
  186. package/src/styles/base/_global.scss +47 -0
  187. package/src/styles/base/_index.scss +24 -0
  188. package/src/styles/base/_reset.scss +11 -0
  189. package/src/styles/base/_typography.scss +178 -0
  190. package/src/styles/components/_accordion.scss +250 -0
  191. package/src/styles/components/_alert.scss +239 -0
  192. package/src/styles/components/_avatar.scss +133 -0
  193. package/src/styles/components/_breadcrumbs.scss +137 -0
  194. package/src/styles/components/_button.scss +731 -0
  195. package/src/styles/components/_card.scss +141 -0
  196. package/src/styles/components/_checkbox.scss +232 -0
  197. package/src/styles/components/_collapsible.scss +158 -0
  198. package/src/styles/components/_divider.scss +121 -0
  199. package/src/styles/components/_field.scss +87 -0
  200. package/src/styles/components/_group.scss +138 -0
  201. package/src/styles/components/_index.scss +46 -0
  202. package/src/styles/components/_input.scss +205 -0
  203. package/src/styles/components/_listbox.scss +453 -0
  204. package/src/styles/components/_menu.scss +216 -0
  205. package/src/styles/components/_modal.scss +329 -0
  206. package/src/styles/components/_navbar.scss +258 -0
  207. package/src/styles/components/_number-input.scss +352 -0
  208. package/src/styles/components/_page.scss +98 -0
  209. package/src/styles/components/_pagination.scss +411 -0
  210. package/src/styles/components/_panel.scss +281 -0
  211. package/src/styles/components/_popover.scss +258 -0
  212. package/src/styles/components/_progress.scss +280 -0
  213. package/src/styles/components/_radio.scss +255 -0
  214. package/src/styles/components/_sidebar.scss +92 -0
  215. package/src/styles/components/_skeleton.scss +138 -0
  216. package/src/styles/components/_slider.scss +262 -0
  217. package/src/styles/components/_spinner.scss +331 -0
  218. package/src/styles/components/_switch.scss +370 -0
  219. package/src/styles/components/_table.scss +405 -0
  220. package/src/styles/components/_tabs.scss +486 -0
  221. package/src/styles/components/_tag.scss +425 -0
  222. package/src/styles/components/_tags-input.scss +279 -0
  223. package/src/styles/components/_textarea.scss +208 -0
  224. package/src/styles/components/_toast.scss +331 -0
  225. package/src/styles/components/_tooltip.scss +206 -0
  226. package/src/styles/fonts/Titillium_Web/OFL.txt +93 -0
  227. package/src/styles/fonts/Titillium_Web/TitilliumWeb-Black.ttf +0 -0
  228. package/src/styles/fonts/Titillium_Web/TitilliumWeb-Bold.ttf +0 -0
  229. package/src/styles/fonts/Titillium_Web/TitilliumWeb-BoldItalic.ttf +0 -0
  230. package/src/styles/fonts/Titillium_Web/TitilliumWeb-ExtraLight.ttf +0 -0
  231. package/src/styles/fonts/Titillium_Web/TitilliumWeb-ExtraLightItalic.ttf +0 -0
  232. package/src/styles/fonts/Titillium_Web/TitilliumWeb-Italic.ttf +0 -0
  233. package/src/styles/fonts/Titillium_Web/TitilliumWeb-Light.ttf +0 -0
  234. package/src/styles/fonts/Titillium_Web/TitilliumWeb-LightItalic.ttf +0 -0
  235. package/src/styles/fonts/Titillium_Web/TitilliumWeb-Regular.ttf +0 -0
  236. package/src/styles/fonts/Titillium_Web/TitilliumWeb-SemiBold.ttf +0 -0
  237. package/src/styles/fonts/Titillium_Web/TitilliumWeb-SemiBoldItalic.ttf +0 -0
  238. package/src/styles/index.scss +17 -0
  239. package/src/styles/mixins/_cut-border.scss +254 -0
  240. package/src/styles/mixins/_index.scss +7 -0
  241. package/src/styles/theme/_variables.scss +42 -0
  242. package/src/styles/themes/README.md +127 -0
  243. package/src/styles/themes/_colorful.scss +58 -0
  244. package/src/styles/themes/_greyscale.scss +58 -0
  245. package/src/styles/themes/index.scss +9 -0
  246. package/src/styles/tokens/README.md +268 -0
  247. package/src/styles/tokens/_foundation-borders.scss +26 -0
  248. package/src/styles/tokens/_foundation-colors.scss +169 -0
  249. package/src/styles/tokens/_foundation-glow.scss +36 -0
  250. package/src/styles/tokens/_foundation-radius.scss +53 -0
  251. package/src/styles/tokens/_foundation-scrollbar.scss +31 -0
  252. package/src/styles/tokens/_foundation-shadows.scss +37 -0
  253. package/src/styles/tokens/_foundation-space.scss +36 -0
  254. package/src/styles/tokens/_foundation-transitions.scss +37 -0
  255. package/src/styles/tokens/_foundation-typography.scss +58 -0
  256. package/src/styles/tokens/_semantic-color-kinds.scss +112 -0
  257. package/src/styles/tokens/_semantic-colors.scss +10 -0
  258. package/src/styles/tokens/_semantic-interactive.scss +29 -0
  259. package/src/styles/tokens/_semantic-scrollbar.scss +48 -0
  260. package/src/styles/tokens/_semantic-surfaces.scss +36 -0
  261. package/src/styles/tokens/index.scss +38 -0
  262. package/src/styles/tokens.scss +268 -0
  263. package/src/styles/utilities/_index.scss +9 -0
  264. package/src/styles/utilities/_typography.scss +121 -0
  265. package/src/types.ts +50 -0
  266. 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
+ //----------------------------------------------------------------------------------------------------------------------