@vanilla-bean/components 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.
Files changed (147) hide show
  1. package/Component/Component.js +598 -0
  2. package/Component/Component.scenarios.js +88 -0
  3. package/Component/Component.test.js +717 -0
  4. package/Component/README.md +455 -0
  5. package/Component/index.js +3 -0
  6. package/Component/observeElementConnection.js +52 -0
  7. package/Component/observeElementConnection.test.js +121 -0
  8. package/Elem/Elem.js +304 -0
  9. package/Elem/Elem.test.js +679 -0
  10. package/Elem/README.md +373 -0
  11. package/Elem/index.js +1 -0
  12. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
  13. package/LICENSE +21 -0
  14. package/README.md +413 -0
  15. package/components/BottomSheet/BottomSheet.js +192 -0
  16. package/components/BottomSheet/BottomSheet.lld.md +25 -0
  17. package/components/BottomSheet/README.md +66 -0
  18. package/components/BottomSheet/index.js +1 -0
  19. package/components/Button/Button.js +53 -0
  20. package/components/Button/Button.lld.md +21 -0
  21. package/components/Button/index.js +1 -0
  22. package/components/Calendar/Calendar.js +720 -0
  23. package/components/Calendar/Calendar.lld.md +22 -0
  24. package/components/Calendar/CalendarEvent.js +102 -0
  25. package/components/Calendar/Toolbar.js +78 -0
  26. package/components/Calendar/index.js +2 -0
  27. package/components/Calendar/utils.js +56 -0
  28. package/components/Code/Code.js +84 -0
  29. package/components/Code/Code.lld.md +21 -0
  30. package/components/Code/index.js +1 -0
  31. package/components/ColorPicker/ColorPicker.js +445 -0
  32. package/components/ColorPicker/ColorPicker.lld.md +21 -0
  33. package/components/ColorPicker/index.js +1 -0
  34. package/components/ColorPicker/svg.js +5 -0
  35. package/components/Dialog/Dialog.js +278 -0
  36. package/components/Dialog/Dialog.lld.md +20 -0
  37. package/components/Dialog/README.md +96 -0
  38. package/components/Dialog/index.js +1 -0
  39. package/components/Form/Form.js +257 -0
  40. package/components/Form/Form.lld.md +21 -0
  41. package/components/Form/README.md +87 -0
  42. package/components/Form/index.js +1 -0
  43. package/components/Icon/Icon.js +54 -0
  44. package/components/Icon/Icon.lld.md +21 -0
  45. package/components/Icon/index.js +1 -0
  46. package/components/Input/Input.js +173 -0
  47. package/components/Input/Input.lld.md +28 -0
  48. package/components/Input/README.md +97 -0
  49. package/components/Input/index.js +2 -0
  50. package/components/Input/utils.js +122 -0
  51. package/components/Keyboard/Key.js +38 -0
  52. package/components/Keyboard/Keyboard.js +173 -0
  53. package/components/Keyboard/Keyboard.lld.md +21 -0
  54. package/components/Keyboard/index.js +1 -0
  55. package/components/Label/Label.js +214 -0
  56. package/components/Label/Label.lld.md +20 -0
  57. package/components/Label/index.js +1 -0
  58. package/components/Link/Link.js +43 -0
  59. package/components/Link/Link.lld.md +15 -0
  60. package/components/Link/index.js +1 -0
  61. package/components/List/List.js +82 -0
  62. package/components/List/List.lld.md +19 -0
  63. package/components/List/index.js +1 -0
  64. package/components/Menu/Menu.js +93 -0
  65. package/components/Menu/Menu.lld.md +15 -0
  66. package/components/Menu/index.js +1 -0
  67. package/components/Notify/Notify.js +96 -0
  68. package/components/Notify/Notify.lld.md +20 -0
  69. package/components/Notify/index.js +1 -0
  70. package/components/Page/Page.js +67 -0
  71. package/components/Page/Page.lld.md +20 -0
  72. package/components/Page/index.js +1 -0
  73. package/components/Popover/Popover.js +175 -0
  74. package/components/Popover/Popover.lld.md +19 -0
  75. package/components/Popover/index.js +1 -0
  76. package/components/RadioButton/RadioButton.js +108 -0
  77. package/components/RadioButton/RadioButton.lld.md +15 -0
  78. package/components/RadioButton/index.js +1 -0
  79. package/components/Router/README.md +160 -0
  80. package/components/Router/Router.js +150 -0
  81. package/components/Router/Router.lld.md +31 -0
  82. package/components/Router/View.js +15 -0
  83. package/components/Router/index.js +2 -0
  84. package/components/Router/utils.js +17 -0
  85. package/components/Select/README.md +88 -0
  86. package/components/Select/Select.js +74 -0
  87. package/components/Select/Select.lld.md +20 -0
  88. package/components/Select/index.js +1 -0
  89. package/components/Table/README.md +94 -0
  90. package/components/Table/Table.js +171 -0
  91. package/components/Table/Table.lld.md +21 -0
  92. package/components/Table/index.js +1 -0
  93. package/components/TagList/Tag.js +84 -0
  94. package/components/TagList/TagList.js +118 -0
  95. package/components/TagList/TagList.lld.md +30 -0
  96. package/components/TagList/design.excalidraw.png +0 -0
  97. package/components/TagList/index.js +2 -0
  98. package/components/Tooltip/Tooltip.js +139 -0
  99. package/components/Tooltip/Tooltip.lld.md +22 -0
  100. package/components/Tooltip/index.js +1 -0
  101. package/components/TooltipWrapper/TooltipWrapper.js +89 -0
  102. package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
  103. package/components/TooltipWrapper/index.js +1 -0
  104. package/components/Whiteboard/Whiteboard.js +198 -0
  105. package/components/Whiteboard/Whiteboard.lld.md +35 -0
  106. package/components/Whiteboard/index.js +1 -0
  107. package/components/index.js +27 -0
  108. package/eslint.config.cjs +118 -0
  109. package/index.d.ts +635 -0
  110. package/index.js +19 -0
  111. package/package.json +123 -0
  112. package/plugins/asText.js +38 -0
  113. package/plugins/loadPlugins.js +5 -0
  114. package/plugins/markdownLoader.js +121 -0
  115. package/prettier.config.cjs +7 -0
  116. package/spellcheck.config.cjs +227 -0
  117. package/styled/README.md +329 -0
  118. package/styled/appendStyles.js +26 -0
  119. package/styled/appendStyles.test.js +45 -0
  120. package/styled/index.js +4 -0
  121. package/styled/shimCSS.js +31 -0
  122. package/styled/shimCSS.test.js +103 -0
  123. package/styled/styled.js +91 -0
  124. package/styled/styled.test.js +586 -0
  125. package/styled/themeStyles.js +36 -0
  126. package/styled/themeStyles.test.js +135 -0
  127. package/test-setup.js +123 -0
  128. package/theme/.test.js +69 -0
  129. package/theme/README.md +607 -0
  130. package/theme/button.js +100 -0
  131. package/theme/code.js +123 -0
  132. package/theme/colors.js +42 -0
  133. package/theme/fonts.js +42 -0
  134. package/theme/index.js +33 -0
  135. package/theme/input.js +64 -0
  136. package/theme/page.js +208 -0
  137. package/theme/scrollbar.js +24 -0
  138. package/theme/table.js +53 -0
  139. package/utils/README.md +176 -0
  140. package/utils/browser.js +92 -0
  141. package/utils/class.js +30 -0
  142. package/utils/color.js +81 -0
  143. package/utils/data.js +164 -0
  144. package/utils/element.js +55 -0
  145. package/utils/index.js +7 -0
  146. package/utils/rand.js +12 -0
  147. package/utils/string.js +72 -0
@@ -0,0 +1,329 @@
1
+ # styled
2
+
3
+ Create Component subclasses with scoped CSS: each `styled()` call generates a unique class, processes the theme, and injects a `<style>` element into `<head>`.
4
+
5
+ ## Basic Usage
6
+
7
+ ### Template Literal Syntax
8
+
9
+ Create styled components using template literals with theme integration:
10
+
11
+ ```js
12
+ const StyledIcon = styled.Icon`
13
+ background-color: ${({ colors }) => colors.black};
14
+ width: 24px;
15
+ height: 24px;
16
+
17
+ &:before {
18
+ ${({ fonts }) => fonts.fontAwesomeSolid}
19
+ content: "\f015";
20
+ }
21
+ `;
22
+ ```
23
+
24
+ ### Function Syntax with Configuration
25
+
26
+ Use function syntax when you need to pass component configuration options:
27
+
28
+ ```js
29
+ const ConfiguredComponent = styled(
30
+ Component,
31
+ ({ colors }) => `
32
+ background-color: ${colors.black};
33
+ color: ${colors.lighter(colors.blue)};
34
+ padding: 12px;
35
+
36
+ &:hover {
37
+ background-color: ${colors.dark(colors.blue)};
38
+ }
39
+ `,
40
+ {
41
+ tag: 'section',
42
+ role: 'banner',
43
+ textContent: 'Default Text',
44
+ },
45
+ );
46
+ ```
47
+
48
+ ### Named Component Shortcuts
49
+
50
+ All top-level components are available as shorthand methods:
51
+
52
+ ```js
53
+ const StyledButton = styled.Button`
54
+ background-color: ${({ colors }) => colors.green};
55
+ border-radius: 8px;
56
+ `;
57
+
58
+ const StyledInput = styled.Input`
59
+ ${({ fonts }) => fonts.kodeMono}
60
+ border: 2px solid ${({ colors }) => colors.blue};
61
+ `;
62
+ ```
63
+
64
+ **Note**: Template literal syntax creates components with empty configuration. Use function syntax to pass component options.
65
+
66
+ ## Theme Integration
67
+
68
+ Style functions receive the complete theme object containing colors, fonts, and component styles:
69
+
70
+ ```js
71
+ const ThemedComponent = styled.Component`
72
+ ${({ button }) => button} /* Apply base button styles */
73
+ ${({ fonts }) => fonts.kodeMono}
74
+ background: ${({ colors }) => colors.darker(colors.blue)};
75
+ color: ${({ colors }) => colors.mostReadable(colors.blue, [colors.white, colors.black])};
76
+ `;
77
+ ```
78
+
79
+ ### Runtime Style Override
80
+
81
+ Override or extend styles when creating component instances:
82
+
83
+ ```js
84
+ const instance = new StyledComponent({
85
+ styles: ({ colors }) => ({
86
+ backgroundColor: colors.red,
87
+ border: `2px solid ${colors.darker(colors.red)}`,
88
+ }),
89
+ });
90
+ ```
91
+
92
+ Object-based styles apply as inline styles. Function-based styles generate scoped CSS.
93
+
94
+ ## Component Inheritance
95
+
96
+ ### Extending Styled Components
97
+
98
+ Build component hierarchies by extending existing styled components:
99
+
100
+ ```js
101
+ const BaseButton = styled.Button`
102
+ padding: 8px 16px;
103
+ border-radius: 4px;
104
+ `;
105
+
106
+ const PrimaryButton = styled(BaseButton)`
107
+ background: ${({ colors }) => colors.blue};
108
+ color: ${({ colors }) => colors.white};
109
+ `;
110
+ ```
111
+
112
+ ### Template Literals with Any Styled Component
113
+
114
+ Template literal syntax works with any styled component, including those created with function syntax:
115
+
116
+ ```js
117
+ const BaseComponent = styled(Component, () => 'color: red;');
118
+
119
+ // Extend any styled component with template literals
120
+ const ExtendedComponent = styled(BaseComponent)`
121
+ background: ${({ colors }) => colors.blue};
122
+ padding: 16px;
123
+ `;
124
+
125
+ // Use in class definitions for custom methods
126
+ class MyComponent extends (styled(BaseComponent)`
127
+ font-weight: bold;
128
+ border-radius: 4px;
129
+ `) {
130
+ // Add custom methods here
131
+ }
132
+ ```
133
+
134
+ ### Component Functionality Inheritance
135
+
136
+ Styled components inherit all functionality from their base component:
137
+
138
+ ```js
139
+ const StyledInput = styled.Input`
140
+ border: 2px solid ${({ colors }) => colors.blue};
141
+ `;
142
+
143
+ const instance = new StyledInput({
144
+ value: 'initial value',
145
+ onChange: event => console.log(event.value),
146
+ placeholder: 'Enter text...',
147
+ });
148
+ ```
149
+
150
+ ## CSS Processing
151
+
152
+ ### Native CSS Nesting
153
+
154
+ Styles are injected as written, no transformation, no runtime compilation. VBC requires Chrome 112+, Firefox 117+, and Safari 16.5+, all of which support the `&` nesting syntax natively.
155
+
156
+ ```js
157
+ const NestedComponent = styled.Component`
158
+ padding: 16px;
159
+
160
+ & .child {
161
+ margin: 8px;
162
+
163
+ &:hover {
164
+ background: ${({ colors }) => colors.blue};
165
+ }
166
+ }
167
+
168
+ @media (max-width: 768px) {
169
+ padding: 8px;
170
+ }
171
+ `;
172
+ ```
173
+
174
+ ### Automatic Scoping
175
+
176
+ Each styled component receives a unique class identifier to prevent CSS conflicts:
177
+
178
+ ```js
179
+ const StyledDiv = styled(Component, () => `color: red;`);
180
+ const instance = new StyledDiv();
181
+
182
+ // Generated CSS: .a1b2c3d4 { color: red; }
183
+ // Component class: "a1b2c3d4"
184
+ ```
185
+
186
+ ### Conditional Styles
187
+
188
+ Apply conditional styles using CSS classes and selectors:
189
+
190
+ ```js
191
+ const ConditionalComponent = styled.Component`
192
+ padding: 12px;
193
+ background: ${({ colors }) => colors.white};
194
+
195
+ &.active {
196
+ background: ${({ colors }) => colors.blue};
197
+ color: ${({ colors }) => colors.white};
198
+ }
199
+
200
+ &.disabled {
201
+ opacity: 0.5;
202
+ pointer-events: none;
203
+ }
204
+ `;
205
+
206
+ const instance = new ConditionalComponent({
207
+ addClass: 'active',
208
+ textContent: 'Active Button',
209
+ });
210
+ ```
211
+
212
+ ## Performance
213
+
214
+ ### Processing Pipeline
215
+
216
+ The styled system processes styles through this pipeline:
217
+
218
+ 1. **Component Creation** - Generates unique class identifier via `classSafeNanoid()`
219
+ 2. **Style Processing** - Converts template literals into theme functions
220
+ 3. **Theme Application** - Injects complete theme object into style functions
221
+ 4. **CSS Processing** - Processes styles through `shimCSS()` pipeline
222
+ 5. **DOM Injection** - Injects final CSS via `appendStyles()`
223
+
224
+ ### Load-Time Optimization
225
+
226
+ Style processing timing depends on document state:
227
+
228
+ | Document State | Behavior |
229
+ | ------------------- | ------------------------------------------ |
230
+ | Complete | Processes and injects immediately |
231
+ | Loading | Queues for batch processing on window load |
232
+ | Multiple Components | Batches together for efficiency |
233
+
234
+ ```js
235
+ // Document loaded - processes immediately
236
+ const StyledComponent = styled(Component, () => 'color: red;');
237
+
238
+ // Document loading - queued for batch processing
239
+ const AnotherStyled = styled(Component, () => 'color: blue;');
240
+ ```
241
+
242
+ ## API Reference
243
+
244
+ ### styled(BaseComponent, styles?, options?)
245
+
246
+ Creates a styled component class.
247
+
248
+ **Parameters:**
249
+
250
+ - `BaseComponent` (Function) - Component class to extend
251
+ - `styles` (Function|String) - Style function or CSS string
252
+ - `options` (Object) - Component configuration options
253
+
254
+ **Returns:** Extended component class with scoped styling
255
+
256
+ ### configured(BaseComponent, options)
257
+
258
+ Creates a component with configuration but no styles:
259
+
260
+ ```js
261
+ const ConfiguredComponent = configured(Component, {
262
+ tag: 'article',
263
+ role: 'main',
264
+ textContent: 'Default Content',
265
+ });
266
+ ```
267
+
268
+ ### Utility Functions
269
+
270
+ #### appendStyles(css, id?)
271
+
272
+ Inject CSS directly into the page:
273
+
274
+ ```js
275
+ appendStyles(
276
+ `
277
+ .my-global-class {
278
+ font-weight: bold;
279
+ color: red;
280
+ }
281
+ `,
282
+ 'my-global-styles',
283
+ );
284
+ ```
285
+
286
+ #### themeStyles({ styles, scope })
287
+
288
+ Generate themed CSS with optional scoping:
289
+
290
+ ```js
291
+ const themedCSS = themeStyles({
292
+ styles: ({ colors }) => `color: ${colors.white}; background: ${colors.black};`,
293
+ scope: '.my-component',
294
+ });
295
+ // Returns: ".my-component { color: hsl(0, 0%, 90%); background: hsl(0, 0%, 10%); }"
296
+ ```
297
+
298
+ #### shimCSS(styleConfig)
299
+
300
+ Complete style processing pipeline:
301
+
302
+ ```js
303
+ shimCSS({
304
+ styles: ({ colors }) => `
305
+ display: flex;
306
+ background: ${colors.blue};
307
+ & .item { padding: 8px; }
308
+ `,
309
+ scope: '.my-scoped-component',
310
+ });
311
+ ```
312
+
313
+ ## Development Features
314
+
315
+ ### Debug Class Names
316
+
317
+ Development mode adds inheritance-based class names for easier debugging:
318
+
319
+ ```js
320
+ // Development classes: "a1b2c3 MyCustomComponent Component Elem"
321
+ class MyCustomComponent extends Component {}
322
+ const StyledCustom = styled(MyCustomComponent, () => 'color: blue;');
323
+ ```
324
+
325
+ ### Memory Management
326
+
327
+ Class-level styles injected by `styled()` persist for the page lifetime. They are scoped to a unique class and do not interfere with other components, but are not removed when instances disconnect.
328
+
329
+ Per-instance styles set via the `styles` option on a component instance are cleaned up when that component disconnects from the DOM.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Inject CSS directly into the document head via a style element
3
+ * @param {string} css - Raw CSS string to inject
4
+ * @param {string} [id] - Optional element ID for the created style tag (enables later removal/updates)
5
+ * @returns {HTMLStyleElement | undefined} The created or updated style element, or undefined if no CSS provided
6
+ */
7
+ export const appendStyles = (css, id) => {
8
+ if (!css) return undefined;
9
+ if (id) {
10
+ const existing = document.getElementById(id);
11
+
12
+ if (existing) {
13
+ existing.textContent = css;
14
+ return existing;
15
+ }
16
+ }
17
+
18
+ const style = document.createElement('style');
19
+
20
+ style.textContent = css;
21
+ if (id) style.id = id;
22
+
23
+ document.head.append(style);
24
+
25
+ return style;
26
+ };
@@ -0,0 +1,45 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import { appendStyles } from './appendStyles';
4
+
5
+ afterEach(() => {
6
+ document.head.querySelectorAll('style').forEach(el => el.remove());
7
+ });
8
+
9
+ describe('appendStyles', () => {
10
+ test('creates a style element with CSS content', () => {
11
+ const css = '.test { margin: 0; }';
12
+ const style = appendStyles(css);
13
+
14
+ expect(style.innerHTML).toContain(css);
15
+ expect(style.parentElement).toBe(document.head);
16
+ });
17
+
18
+ test('returns undefined for falsy CSS', () => {
19
+ expect(appendStyles('')).toBeUndefined();
20
+ expect(appendStyles(null)).toBeUndefined();
21
+ expect(appendStyles(undefined)).toBeUndefined();
22
+ });
23
+
24
+ test('assigns id when provided', () => {
25
+ const style = appendStyles('.a { color: red; }', 'my-style');
26
+
27
+ expect(style.id).toBe('my-style');
28
+ });
29
+
30
+ test('updates existing element when id matches', () => {
31
+ const style1 = appendStyles('.a { color: red; }', 'reusable');
32
+ const style2 = appendStyles('.a { color: blue; }', 'reusable');
33
+
34
+ expect(style1).toBe(style2);
35
+ expect(style2.innerHTML).toContain('color: blue');
36
+ expect(document.querySelectorAll('#reusable').length).toBe(1);
37
+ });
38
+
39
+ test('creates separate elements without id', () => {
40
+ appendStyles('.a { color: red; }');
41
+ appendStyles('.b { color: blue; }');
42
+
43
+ expect(document.head.querySelectorAll('style').length).toBe(2);
44
+ });
45
+ });
@@ -0,0 +1,4 @@
1
+ export * from './appendStyles';
2
+ export * from './shimCSS';
3
+ export * from './styled';
4
+ export * from './themeStyles';
@@ -0,0 +1,31 @@
1
+ import { appendStyles } from './appendStyles';
2
+ import { themeStyles } from './themeStyles';
3
+
4
+ let loadQueue = null;
5
+ let loadListener = null;
6
+
7
+ /**
8
+ * CSS pipeline: theme hydration → DOM injection.
9
+ * Batches configs queued before document load and flushes them on window load.
10
+ * @param {import('./themeStyles').StyleConfig} styleConfig - Configuration object with styles function and optional scope
11
+ */
12
+ export const shimCSS = styleConfig => {
13
+ if (document.readyState === 'complete') {
14
+ appendStyles(themeStyles(styleConfig) || '', styleConfig.scope?.replace(/^\./, ''));
15
+ } else {
16
+ loadQueue = loadQueue || [];
17
+ loadQueue.push(styleConfig);
18
+
19
+ if (!loadListener) {
20
+ loadListener = () => {
21
+ loadQueue.forEach(config => appendStyles(themeStyles(config) || '', config.scope?.replace(/^\./, '')));
22
+
23
+ loadQueue = null;
24
+ window.removeEventListener('load', loadListener);
25
+ loadListener = null;
26
+ };
27
+
28
+ window.addEventListener('load', loadListener);
29
+ }
30
+ }
31
+ };
@@ -0,0 +1,103 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import { appendStyles } from './appendStyles';
4
+ import { themeStyles } from './themeStyles';
5
+
6
+ /**
7
+ * shimCSS is tested via its internal pipeline because styled.test.js uses
8
+ * mock.module('./shimCSS') which leaks process-wide in bun, replacing the
9
+ * real shimCSS with a no-op for all consumers. Rather than fight the
10
+ * tooling, we test the actual behavior by orchestrating the same pipeline
11
+ * shimCSS uses: themeStyles → appendStyles.
12
+ */
13
+
14
+ describe('shimCSS pipeline', () => {
15
+ beforeEach(() => {
16
+ document.head.querySelectorAll('style').forEach(el => el.remove());
17
+ });
18
+
19
+ describe('immediate processing (document complete)', () => {
20
+ test('themeStyles → appendStyles injects CSS into head', () => {
21
+ const config = {
22
+ styles: () => '.child { color: red; }',
23
+ scope: '.pipeline-test',
24
+ };
25
+
26
+ const style = appendStyles(themeStyles(config) || '', config.scope.replace(/^\./, ''));
27
+
28
+ expect(style).toBeTruthy();
29
+ expect(style.innerHTML).toContain('color: red');
30
+ expect(style.parentElement).toBe(document.head);
31
+ });
32
+
33
+ test('scope is used as style element id with dot stripped', () => {
34
+ const config = {
35
+ styles: () => 'color: blue;',
36
+ scope: '.scoped-id',
37
+ };
38
+
39
+ appendStyles(themeStyles(config) || '', config.scope.replace(/^\./, ''));
40
+
41
+ const style = document.getElementById('scoped-id');
42
+
43
+ expect(style).toBeTruthy();
44
+ expect(style.tagName).toBe('STYLE');
45
+ });
46
+
47
+ test('processes theme function styles through themeStyles', () => {
48
+ const config = {
49
+ styles: ({ colors }) => `color: ${colors.red};`,
50
+ scope: '.themed',
51
+ };
52
+
53
+ const result = themeStyles(config);
54
+
55
+ expect(typeof result).toBe('string');
56
+ expect(result).toContain('color:');
57
+ });
58
+ });
59
+
60
+ describe('batch processing', () => {
61
+ test('multiple configs can be processed and injected', () => {
62
+ const configs = [
63
+ { styles: () => '.child { color: red; }', scope: '.batch-1' },
64
+ { styles: () => '.child { color: blue; }', scope: '.batch-2' },
65
+ ];
66
+
67
+ configs.forEach(config => appendStyles(themeStyles(config) || '', config.scope.replace(/^\./, '')));
68
+
69
+ const style1 = document.getElementById('batch-1');
70
+ const style2 = document.getElementById('batch-2');
71
+
72
+ expect(style1).toBeTruthy();
73
+ expect(style1.innerHTML).toContain('color: red');
74
+ expect(style2).toBeTruthy();
75
+ expect(style2.innerHTML).toContain('color: blue');
76
+ });
77
+ });
78
+
79
+ describe('themeStyles edge cases', () => {
80
+ test('returns empty string for whitespace-only styles', () => {
81
+ const result = themeStyles({ styles: () => ' \n\t ' });
82
+
83
+ expect(result).toBe('');
84
+ });
85
+
86
+ test('returns style object directly when styles function returns object', () => {
87
+ const styleObj = { color: 'red', margin: '10px' };
88
+ const result = themeStyles({ styles: () => styleObj });
89
+
90
+ expect(result).toBe(styleObj);
91
+ });
92
+
93
+ test('wraps CSS in scope selector when provided', () => {
94
+ const result = themeStyles({
95
+ styles: () => 'color: red;',
96
+ scope: '.my-scope',
97
+ });
98
+
99
+ expect(result).toContain('.my-scope');
100
+ expect(result).toContain('color: red');
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,91 @@
1
+ import { classSafeNanoid } from '../utils';
2
+ import theme from '../theme';
3
+
4
+ import { shimCSS } from './shimCSS';
5
+
6
+ /**
7
+ * Creates Component class with embedded configuration options (no styling applied).
8
+ * @param {typeof import('../Component').default} BaseComponent - Component class to extend
9
+ * @param {object} options - Default options merged into all component instances
10
+ * @returns {typeof import('../Component').default} Extended component class with embedded options
11
+ */
12
+ export const configured = function (BaseComponent, options = {}) {
13
+ return class ConfiguredComponent extends BaseComponent {
14
+ constructor(overlayOptions = {}, ...children) {
15
+ super({ ...options, ...overlayOptions }, ...children);
16
+
17
+ this.addClass(options.addClass, overlayOptions.addClass);
18
+ }
19
+ };
20
+ };
21
+
22
+ /**
23
+ * Creates Component class with scoped styles and optional configuration.
24
+ *
25
+ * Generates unique class identifier, processes styles through the theme system,
26
+ * and injects as scoped CSS. Supports both function syntax and template literal syntax.
27
+ * @param {typeof import('../Component').default} BaseComponent - Component class to extend
28
+ * @param {Function|TemplateStringsArray} [styles] - Theme function returning CSS string, or template literal strings array
29
+ * @param {object} [options] - Default options embedded in component instances
30
+ * @returns {typeof import('../Component').default|Function} Extended component class with scoped styling, or template literal function
31
+ */
32
+ export const styled = function (BaseComponent, styles = () => '', options = {}) {
33
+ const isTagFunction = Array.isArray(styles) && styles.raw;
34
+
35
+ if (arguments.length === 1 && typeof BaseComponent === 'function') {
36
+ const tagFunction = function (templateLiteral, ...interpolations) {
37
+ if (Array.isArray(templateLiteral) && templateLiteral.raw) {
38
+ const styleText = templateLiteral
39
+ .flatMap((string, index) => {
40
+ if (index === 0) return string;
41
+
42
+ const interpolation = interpolations[index - 1];
43
+
44
+ return [typeof interpolation === 'function' ? interpolation(theme) : interpolation, string];
45
+ })
46
+ .join('');
47
+
48
+ const componentId = classSafeNanoid();
49
+
50
+ shimCSS({ scope: `.${componentId}`, styles: () => styleText });
51
+
52
+ return configured(BaseComponent, { addClass: [componentId] });
53
+ }
54
+
55
+ return styled(BaseComponent, templateLiteral, ...interpolations);
56
+ };
57
+
58
+ const componentId = classSafeNanoid();
59
+
60
+ const StyledComponent = configured(BaseComponent, { addClass: [componentId] });
61
+
62
+ Object.setPrototypeOf(tagFunction, StyledComponent.prototype);
63
+ tagFunction.prototype = StyledComponent.prototype;
64
+
65
+ return new Proxy(tagFunction, {
66
+ construct(target, args) {
67
+ return new StyledComponent(...args);
68
+ },
69
+ });
70
+ }
71
+
72
+ if (isTagFunction) {
73
+ const [, , ...args] = arguments;
74
+ const styleText = styles
75
+ .flatMap((string, index) => {
76
+ if (index === 0) return string;
77
+ return [typeof args[index - 1] === 'function' ? args[index - 1](theme) : args[index - 1], string];
78
+ })
79
+ .join('');
80
+
81
+ styles = () => styleText;
82
+
83
+ options = {};
84
+ }
85
+
86
+ const componentId = classSafeNanoid();
87
+
88
+ shimCSS({ scope: `.${componentId}`, styles });
89
+
90
+ return configured(BaseComponent, { ...options, addClass: [componentId].concat(options.addClass || []) });
91
+ };