esm-styles 0.3.7 → 0.3.8

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/dist/lib/build.js CHANGED
@@ -257,7 +257,8 @@ export async function build(configPath = 'esm-styles.config.js') {
257
257
  '\n';
258
258
  await fs.writeFile(mainCssPath, mainCss, 'utf8');
259
259
  // 6. Create timestamp file
260
- const timestampPath = path.join(config.basePath || '.', config.timestampOutputPath || '', 'timestamp.mjs');
260
+ const { outputPath: timestampOutputPath, extension: timestampExtension } = config.timestamp || { outputPath: '', extension: 'mjs' };
261
+ const timestampPath = path.join(config.basePath || '.', timestampOutputPath, 'timestamp.' + timestampExtension);
261
262
  await fs.writeFile(timestampPath, `export default ${Date.now()}`, 'utf8');
262
263
  }
263
264
  // Helper for file URL import
@@ -1,4 +1,5 @@
1
1
  import * as utils from './cartesian.js';
2
+ import kebabCase from 'lodash/kebabCase.js';
2
3
  const svgTags = [
3
4
  'circle',
4
5
  'ellipse',
@@ -179,6 +180,17 @@ export const isClassSelector = (key) => {
179
180
  // _foo = class, __foo = descendant class
180
181
  return key.startsWith('_');
181
182
  };
183
+ const processClass = (cls) => {
184
+ if (/^[A-Z]/.test(cls)) {
185
+ return cls;
186
+ }
187
+ return kebabCase(cls);
188
+ };
189
+ const transformExplicitClasses = (selector) => {
190
+ return selector.replace(/\.([a-zA-Z0-9_-]+)/g, (_, cls) => {
191
+ return '.' + processClass(cls);
192
+ });
193
+ };
182
194
  export const joinSelectorPath = (path) => {
183
195
  // Compute cartesian product of all segments
184
196
  const combos = utils.cartesianProduct(path);
@@ -187,64 +199,63 @@ export const joinSelectorPath = (path) => {
187
199
  // Check if previous part is a root selector
188
200
  const prev = idx > 0 ? parts[idx - 1] : null;
189
201
  const isPrevRoot = prev && (prev === ':root' || prev.startsWith(':root.'));
190
- if (part === '*') {
191
- // Universal selector
192
- return acc + (acc ? ' ' : '') + '*';
193
- }
194
- else if (part.startsWith('__')) {
195
- return acc + (acc ? ' ' : '') + '.' + part.slice(2);
196
- }
197
- else if (part.startsWith('_')) {
198
- // Attach class directly to previous part unless prev is combinator or root
199
- const combinators = ['>', '+', '~'];
200
- const isPrevCombinator = prev && combinators.some((c) => prev.startsWith(c));
201
- if (isPrevRoot || isPrevCombinator || !acc) {
202
- return acc + (acc ? ' ' : '') + '.' + part.slice(1);
203
- }
204
- else {
202
+ switch (true) {
203
+ case part === '*':
204
+ // Universal selector
205
+ return acc + (acc ? ' ' : '') + '*';
206
+ case part.startsWith('__'):
207
+ return acc + (acc ? ' ' : '') + '.' + processClass(part.slice(2));
208
+ case part.startsWith('_'): {
209
+ // Attach class directly to previous part unless prev is combinator or root
210
+ const combinators = ['>', '+', '~'];
211
+ const isPrevCombinator = prev && combinators.some((c) => prev.startsWith(c));
212
+ if (isPrevRoot || isPrevCombinator || !acc) {
213
+ return acc + (acc ? ' ' : '') + '.' + processClass(part.slice(1));
214
+ }
205
215
  // Attach directly (no space)
206
- return acc + '.' + part.slice(1);
216
+ return acc + '.' + processClass(part.slice(1));
207
217
  }
218
+ case part.startsWith('>') ||
219
+ part.startsWith('+') ||
220
+ part.startsWith('~'):
221
+ // Combinators: always join with a space
222
+ return acc + ' ' + part;
223
+ case part.startsWith(':') ||
224
+ part.startsWith('::') ||
225
+ part.startsWith('#') ||
226
+ part.startsWith('[') ||
227
+ part.startsWith('.'):
228
+ return acc + transformExplicitClasses(part);
229
+ case isHtmlTag(part):
230
+ return acc + (acc ? ' ' : '') + part;
231
+ case startsWithHtmlTag(part):
232
+ // Handle compound selectors that start with HTML tags (e.g., 'div > *')
233
+ return acc + (acc ? ' ' : '') + transformExplicitClasses(part);
234
+ case /^[a-z][a-z0-9]*\.(.+)/.test(part) &&
235
+ isHtmlTag(part.split('.')[0]):
236
+ // If part matches 'tag.class...' and tag is an HTML tag
237
+ return acc + (acc ? ' ' : '') + transformExplicitClasses(part);
238
+ case /^[a-z][a-z0-9]*#[\w-]+$/.test(part) &&
239
+ isHtmlTag(part.split('#')[0]):
240
+ // If part matches 'tag#id' and tag is an HTML tag
241
+ // ID should technically not be kebab-cased, and regex ensures no classes.
242
+ return acc + (acc ? ' ' : '') + part;
243
+ default:
244
+ // Not a tag, not a special selector: treat as class or custom element
245
+ let processedPart = part;
246
+ const match = part.match(/^([a-zA-Z0-9_-]+)(.*)$/);
247
+ if (match) {
248
+ processedPart =
249
+ processClass(match[1]) + transformExplicitClasses(match[2]);
250
+ }
251
+ else {
252
+ processedPart = transformExplicitClasses(part);
253
+ }
254
+ // If previous part is a root selector, insert a space
255
+ if (isPrevRoot) {
256
+ return acc + ' ' + '.' + processedPart;
257
+ }
258
+ return acc + '.' + processedPart;
208
259
  }
209
- else if (part.startsWith('>') ||
210
- part.startsWith('+') ||
211
- part.startsWith('~')) {
212
- // Combinators: always join with a space
213
- return acc + ' ' + part;
214
- }
215
- else if (part.startsWith(':') ||
216
- part.startsWith('::') ||
217
- part.startsWith('#') ||
218
- part.startsWith('[') ||
219
- part.startsWith('.')) {
220
- return acc + part;
221
- }
222
- else if (isHtmlTag(part)) {
223
- return acc + (acc ? ' ' : '') + part;
224
- }
225
- else if (startsWithHtmlTag(part)) {
226
- // Handle compound selectors that start with HTML tags (e.g., 'div > *')
227
- return acc + (acc ? ' ' : '') + part;
228
- }
229
- else if (/^([a-z][a-z0-9]*)\.(.+)/.test(part)) {
230
- // If part matches 'tag.class...' and tag is an HTML tag
231
- const match = part.match(/^([a-z][a-z0-9]*)\.(.+)/);
232
- if (match && isHtmlTag(match[1])) {
233
- return acc + (acc ? ' ' : '') + match[1] + '.' + match[2];
234
- }
235
- }
236
- else if (/^([a-z][a-z0-9]*)#([\w-]+)$/.test(part)) {
237
- // If part matches 'tag#id' and tag is an HTML tag
238
- const match = part.match(/^([a-z][a-z0-9]*)#([\w-]+)$/);
239
- if (match && isHtmlTag(match[1])) {
240
- return acc + (acc ? ' ' : '') + match[1] + '#' + match[2];
241
- }
242
- }
243
- // Not a tag, not a special selector: treat as class or custom element
244
- // If previous part is a root selector, insert a space
245
- if (isPrevRoot) {
246
- return acc + ' ' + '.' + part;
247
- }
248
- return acc + (acc ? '' : '') + '.' + part;
249
260
  }, ''));
250
261
  };
@@ -144,21 +144,21 @@ export default {
144
144
 
145
145
  ### Configuration Properties
146
146
 
147
- | Property | Description |
148
- | --------------------- | ------------------------------------------------------------------------- |
149
- | `basePath` | Base directory for all styles, relative to where the build command is run |
150
- | `sourcePath` | Directory inside `basePath` containing source style files |
151
- | `outputPath` | Directory inside `basePath` where output CSS files will be written |
152
- | `sourceFilesSuffix` | Suffix for source style files (default: `.styles.mjs`) |
153
- | `floors` | Array of floor configurations, defining sources, layers, and output paths |
154
- | `importFloors` | Array of floor names to include in the main CSS file |
155
- | `mainCssFile` | Name of the output CSS file that imports all layer and variable files |
156
- | `globalVariables` | Name of the file containing global CSS variables |
157
- | `globalRootSelector` | Root selector for CSS variables (default: `:root`) |
158
- | `media` | Object defining media types and their variable sets |
159
- | `mediaSelectors` | Configuration for applying media types with selectors/queries |
160
- | `mediaQueries` | Object defining shorthand names for media queries |
161
- | `timestampOutputPath` | Path where timestamp.mjs file will be written (default: `basePath`) |
147
+ | Property | Description |
148
+ | -------------------- | ------------------------------------------------------------------------------- |
149
+ | `basePath` | Base directory for all styles, relative to where the build command is run |
150
+ | `sourcePath` | Directory inside `basePath` containing source style files |
151
+ | `outputPath` | Directory inside `basePath` where output CSS files will be written |
152
+ | `sourceFilesSuffix` | Suffix for source style files (default: `.styles.mjs`) |
153
+ | `floors` | Array of floor configurations, defining sources, layers, and output paths |
154
+ | `importFloors` | Array of floor names to include in the main CSS file |
155
+ | `mainCssFile` | Name of the output CSS file that imports all layer and variable files |
156
+ | `globalVariables` | Name of the file containing global CSS variables |
157
+ | `globalRootSelector` | Root selector for CSS variables (default: `:root`) |
158
+ | `media` | Object defining media types and their variable sets |
159
+ | `mediaSelectors` | Configuration for applying media types with selectors/queries |
160
+ | `mediaQueries` | Object defining shorthand names for media queries |
161
+ | `timestamp` | Configuration for timestamp.mjs file (default: `basePath` and `.mjs` extension) |
162
162
 
163
163
  ## JS to CSS Translation Rules
164
164
 
@@ -319,6 +319,24 @@ Use commas to target multiple selectors:
319
319
  }
320
320
  ```
321
321
 
322
+ ### T9: Automatic Kebab-case Conversion
323
+
324
+ Class names in selectors are automatically converted from camelCase to kebab-case, unless they are PascalCase (starting with an uppercase letter), in which case they are preserved as-is.
325
+
326
+ ```js
327
+ {
328
+ myClass: { // Becomes .my-class
329
+ color: 'red'
330
+ },
331
+ 'myClass:hover': { // Becomes .my-class:hover
332
+ color: 'blue'
333
+ },
334
+ MyComponent: { // Becomes .MyComponent (preserved)
335
+ color: 'green'
336
+ }
337
+ }
338
+ ```
339
+
322
340
  ## Floors and Layers
323
341
 
324
342
  ESM Styles supports @layer directives through its floors configuration system.
@@ -652,3 +670,21 @@ The supporting modules provide:
652
670
 
653
671
  - Autocomplete for available variables in most code editors
654
672
  - The ability to see the actual values for each theme or device
673
+
674
+ ## Timestamp file
675
+
676
+ ESM Styles generate a timestamp file to track the last build time. This can be useful for caching and versioning, or to trigger HMR when in development mode.
677
+
678
+ ```js
679
+ // timestamp.mjs (generated)
680
+ export default 1767867956228
681
+ ```
682
+
683
+ ### Configuration in esm-styles.config.mjs
684
+
685
+ By default, the timestamp file is put to the root of the `basePath` directory with `.mjs` extension.
686
+
687
+ ```js
688
+ // put timestamp file to [basePath]/source folder with .ts extension
689
+ timestamp: { outputPath: 'source', extension: 'ts' },
690
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esm-styles",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "A library for working with ESM styles",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",