@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.
- package/Component/Component.js +598 -0
- package/Component/Component.scenarios.js +88 -0
- package/Component/Component.test.js +717 -0
- package/Component/README.md +455 -0
- package/Component/index.js +3 -0
- package/Component/observeElementConnection.js +52 -0
- package/Component/observeElementConnection.test.js +121 -0
- package/Elem/Elem.js +304 -0
- package/Elem/Elem.test.js +679 -0
- package/Elem/README.md +373 -0
- package/Elem/index.js +1 -0
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/components/BottomSheet/BottomSheet.js +192 -0
- package/components/BottomSheet/BottomSheet.lld.md +25 -0
- package/components/BottomSheet/README.md +66 -0
- package/components/BottomSheet/index.js +1 -0
- package/components/Button/Button.js +53 -0
- package/components/Button/Button.lld.md +21 -0
- package/components/Button/index.js +1 -0
- package/components/Calendar/Calendar.js +720 -0
- package/components/Calendar/Calendar.lld.md +22 -0
- package/components/Calendar/CalendarEvent.js +102 -0
- package/components/Calendar/Toolbar.js +78 -0
- package/components/Calendar/index.js +2 -0
- package/components/Calendar/utils.js +56 -0
- package/components/Code/Code.js +84 -0
- package/components/Code/Code.lld.md +21 -0
- package/components/Code/index.js +1 -0
- package/components/ColorPicker/ColorPicker.js +445 -0
- package/components/ColorPicker/ColorPicker.lld.md +21 -0
- package/components/ColorPicker/index.js +1 -0
- package/components/ColorPicker/svg.js +5 -0
- package/components/Dialog/Dialog.js +278 -0
- package/components/Dialog/Dialog.lld.md +20 -0
- package/components/Dialog/README.md +96 -0
- package/components/Dialog/index.js +1 -0
- package/components/Form/Form.js +257 -0
- package/components/Form/Form.lld.md +21 -0
- package/components/Form/README.md +87 -0
- package/components/Form/index.js +1 -0
- package/components/Icon/Icon.js +54 -0
- package/components/Icon/Icon.lld.md +21 -0
- package/components/Icon/index.js +1 -0
- package/components/Input/Input.js +173 -0
- package/components/Input/Input.lld.md +28 -0
- package/components/Input/README.md +97 -0
- package/components/Input/index.js +2 -0
- package/components/Input/utils.js +122 -0
- package/components/Keyboard/Key.js +38 -0
- package/components/Keyboard/Keyboard.js +173 -0
- package/components/Keyboard/Keyboard.lld.md +21 -0
- package/components/Keyboard/index.js +1 -0
- package/components/Label/Label.js +214 -0
- package/components/Label/Label.lld.md +20 -0
- package/components/Label/index.js +1 -0
- package/components/Link/Link.js +43 -0
- package/components/Link/Link.lld.md +15 -0
- package/components/Link/index.js +1 -0
- package/components/List/List.js +82 -0
- package/components/List/List.lld.md +19 -0
- package/components/List/index.js +1 -0
- package/components/Menu/Menu.js +93 -0
- package/components/Menu/Menu.lld.md +15 -0
- package/components/Menu/index.js +1 -0
- package/components/Notify/Notify.js +96 -0
- package/components/Notify/Notify.lld.md +20 -0
- package/components/Notify/index.js +1 -0
- package/components/Page/Page.js +67 -0
- package/components/Page/Page.lld.md +20 -0
- package/components/Page/index.js +1 -0
- package/components/Popover/Popover.js +175 -0
- package/components/Popover/Popover.lld.md +19 -0
- package/components/Popover/index.js +1 -0
- package/components/RadioButton/RadioButton.js +108 -0
- package/components/RadioButton/RadioButton.lld.md +15 -0
- package/components/RadioButton/index.js +1 -0
- package/components/Router/README.md +160 -0
- package/components/Router/Router.js +150 -0
- package/components/Router/Router.lld.md +31 -0
- package/components/Router/View.js +15 -0
- package/components/Router/index.js +2 -0
- package/components/Router/utils.js +17 -0
- package/components/Select/README.md +88 -0
- package/components/Select/Select.js +74 -0
- package/components/Select/Select.lld.md +20 -0
- package/components/Select/index.js +1 -0
- package/components/Table/README.md +94 -0
- package/components/Table/Table.js +171 -0
- package/components/Table/Table.lld.md +21 -0
- package/components/Table/index.js +1 -0
- package/components/TagList/Tag.js +84 -0
- package/components/TagList/TagList.js +118 -0
- package/components/TagList/TagList.lld.md +30 -0
- package/components/TagList/design.excalidraw.png +0 -0
- package/components/TagList/index.js +2 -0
- package/components/Tooltip/Tooltip.js +139 -0
- package/components/Tooltip/Tooltip.lld.md +22 -0
- package/components/Tooltip/index.js +1 -0
- package/components/TooltipWrapper/TooltipWrapper.js +89 -0
- package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
- package/components/TooltipWrapper/index.js +1 -0
- package/components/Whiteboard/Whiteboard.js +198 -0
- package/components/Whiteboard/Whiteboard.lld.md +35 -0
- package/components/Whiteboard/index.js +1 -0
- package/components/index.js +27 -0
- package/eslint.config.cjs +118 -0
- package/index.d.ts +635 -0
- package/index.js +19 -0
- package/package.json +123 -0
- package/plugins/asText.js +38 -0
- package/plugins/loadPlugins.js +5 -0
- package/plugins/markdownLoader.js +121 -0
- package/prettier.config.cjs +7 -0
- package/spellcheck.config.cjs +227 -0
- package/styled/README.md +329 -0
- package/styled/appendStyles.js +26 -0
- package/styled/appendStyles.test.js +45 -0
- package/styled/index.js +4 -0
- package/styled/shimCSS.js +31 -0
- package/styled/shimCSS.test.js +103 -0
- package/styled/styled.js +91 -0
- package/styled/styled.test.js +586 -0
- package/styled/themeStyles.js +36 -0
- package/styled/themeStyles.test.js +135 -0
- package/test-setup.js +123 -0
- package/theme/.test.js +69 -0
- package/theme/README.md +607 -0
- package/theme/button.js +100 -0
- package/theme/code.js +123 -0
- package/theme/colors.js +42 -0
- package/theme/fonts.js +42 -0
- package/theme/index.js +33 -0
- package/theme/input.js +64 -0
- package/theme/page.js +208 -0
- package/theme/scrollbar.js +24 -0
- package/theme/table.js +53 -0
- package/utils/README.md +176 -0
- package/utils/browser.js +92 -0
- package/utils/class.js +30 -0
- package/utils/color.js +81 -0
- package/utils/data.js +164 -0
- package/utils/element.js +55 -0
- package/utils/index.js +7 -0
- package/utils/rand.js +12 -0
- package/utils/string.js +72 -0
package/styled/README.md
ADDED
|
@@ -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
|
+
});
|
package/styled/index.js
ADDED
|
@@ -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
|
+
});
|
package/styled/styled.js
ADDED
|
@@ -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
|
+
};
|