@bookklik/senangstart-css 0.2.7 → 0.2.9

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 (49) hide show
  1. package/dist/senangstart-css.js +9052 -2142
  2. package/dist/senangstart-css.min.js +1207 -119
  3. package/dist/senangstart-tw.js +170 -73
  4. package/dist/senangstart-tw.min.js +1 -1
  5. package/docs/guide/configuration.md +2 -2
  6. package/docs/guide/states.md +60 -0
  7. package/docs/ms/guide/configuration.md +2 -2
  8. package/docs/ms/guide/states.md +60 -0
  9. package/docs/ms/reference/colors.md +2 -2
  10. package/docs/ms/reference/space/height.md +10 -10
  11. package/docs/ms/reference/space/width.md +12 -12
  12. package/docs/public/assets/senangstart-css.min.js +1207 -119
  13. package/docs/public/llms.txt +28 -0
  14. package/docs/reference/colors.md +2 -2
  15. package/docs/reference/space/height.md +10 -10
  16. package/docs/reference/space/width.md +12 -12
  17. package/package.json +1 -1
  18. package/public/senangstart.css +1196 -0
  19. package/scripts/convert-tailwind.js +191 -68
  20. package/scripts/generate-llms-txt.js +28 -0
  21. package/src/cdn/senangstart-engine.js +36 -2268
  22. package/src/cdn/tw-conversion-engine.js +203 -74
  23. package/src/compiler/generators/css.js +309 -249
  24. package/src/compiler/parser.js +14 -4
  25. package/src/compiler/tokenizer.js +0 -1
  26. package/src/config/defaults.js +1 -1
  27. package/src/core/constants.js +5 -3
  28. package/src/core/tokenizer-core.js +3 -58
  29. package/src/definitions/index.js +3 -2
  30. package/src/definitions/layout.js +6 -2
  31. package/src/definitions/space.js +45 -19
  32. package/src/index.js +47 -0
  33. package/src/utils/common.js +27 -0
  34. package/templates/senangstart.config.js +1 -1
  35. package/tests/helpers/test-utils.js +1 -1
  36. package/tests/integration/compiler.test.js +12 -1
  37. package/tests/unit/compiler/generators/css.coverage.test.js +833 -0
  38. package/tests/unit/compiler/generators/css.test.js +1418 -1
  39. package/tests/unit/compiler/generators/preflight.test.js +31 -0
  40. package/tests/unit/compiler/parser.test.js +26 -0
  41. package/tests/unit/config/defaults.test.js +2 -2
  42. package/tests/unit/convert-tailwind.cli.test.js +95 -0
  43. package/tests/unit/convert-tailwind.coverage.test.js +225 -0
  44. package/tests/unit/convert-tailwind.test.js +49 -20
  45. package/tests/unit/core/tokenizer-core.test.js +102 -0
  46. package/tests/unit/definitions/index.test.js +108 -0
  47. package/tests/unit/definitions/layout_definitions.test.js +40 -0
  48. package/tests/unit/utils/common.test.js +26 -0
  49. package/scripts/bundle-jit.js +0 -45
@@ -7,7 +7,9 @@
7
7
  const ATTRIBUTE_PATTERNS = {
8
8
  layout: /layout\s*=\s*["']([^"']*)["']/g,
9
9
  space: /space\s*=\s*["']([^"']*)["']/g,
10
- visual: /visual\s*=\s*["']([^"']*)["']/g
10
+ visual: /visual\s*=\s*["']([^"']*)["']/g,
11
+ interact: /interact\s*=\s*["']([^"']*)["']/g,
12
+ listens: /listens\s*=\s*["']([^"']*)["']/g
11
13
  };
12
14
 
13
15
  /**
@@ -18,7 +20,9 @@ function createAttributePatterns() {
18
20
  return {
19
21
  layout: /layout\s*=\s*["']([^"']*)["']/g,
20
22
  space: /space\s*=\s*["']([^"']*)["']/g,
21
- visual: /visual\s*=\s*["']([^"']*)["']/g
23
+ visual: /visual\s*=\s*["']([^"']*)["']/g,
24
+ interact: /interact\s*=\s*["']([^"']*)["']/g,
25
+ listens: /listens\s*=\s*["']([^"']*)["']/g
22
26
  };
23
27
  }
24
28
 
@@ -31,7 +35,9 @@ export function parseSource(content) {
31
35
  const results = {
32
36
  layout: new Set(),
33
37
  space: new Set(),
34
- visual: new Set()
38
+ visual: new Set(),
39
+ interact: new Set(),
40
+ listens: new Set()
35
41
  };
36
42
 
37
43
  const patterns = createAttributePatterns();
@@ -63,7 +69,9 @@ export function parseMultipleSources(files) {
63
69
  const combined = {
64
70
  layout: new Set(),
65
71
  space: new Set(),
66
- visual: new Set()
72
+ visual: new Set(),
73
+ interact: new Set(),
74
+ listens: new Set()
67
75
  };
68
76
 
69
77
  for (const file of files) {
@@ -72,6 +80,8 @@ export function parseMultipleSources(files) {
72
80
  parsed.layout.forEach(token => combined.layout.add(token));
73
81
  parsed.space.forEach(token => combined.space.add(token));
74
82
  parsed.visual.forEach(token => combined.visual.add(token));
83
+ parsed.interact.forEach(token => combined.interact.add(token));
84
+ parsed.listens.forEach(token => combined.listens.add(token));
75
85
  }
76
86
 
77
87
  return combined;
@@ -7,7 +7,6 @@
7
7
  export {
8
8
  tokenize,
9
9
  tokenizeAll,
10
- parseToken,
11
10
  sanitizeValue,
12
11
  isValidToken
13
12
  } from '../core/tokenizer-core.js';
@@ -158,7 +158,7 @@ export const defaultConfig = {
158
158
  'dark': '#3E4A5D', // Brand dark
159
159
  'light': '#DBEAFE', // Brand light/secondary
160
160
  'primary': '#2563EB', // Brand primary
161
- 'secondary': '#DBEAFE', // Brand secondary
161
+ 'secondary': '#1E40AF', // Brand secondary
162
162
  'success': '#10B981',
163
163
  'warning': '#F59E0B',
164
164
  'danger': '#EF4444',
@@ -7,7 +7,7 @@
7
7
  export const BREAKPOINTS = ['mob', 'tab', 'lap', 'desk', 'tw-sm', 'tw-md', 'tw-lg', 'tw-xl', 'tw-2xl'];
8
8
 
9
9
  // State prefixes
10
- export const STATES = ['hover', 'focus', 'focus-visible', 'active', 'disabled', 'dark'];
10
+ export const STATES = ['hover', 'focus', 'focus-visible', 'active', 'disabled', 'dark', 'expanded', 'selected'];
11
11
 
12
12
  // Layout keywords (no colon syntax)
13
13
  export const LAYOUT_KEYWORDS = [
@@ -15,7 +15,9 @@ export const LAYOUT_KEYWORDS = [
15
15
  'row', 'col', 'row-reverse', 'col-reverse',
16
16
  'center', 'start', 'end', 'between', 'around', 'evenly',
17
17
  'wrap', 'nowrap',
18
- 'absolute', 'relative', 'fixed', 'sticky'
18
+ 'absolute', 'relative', 'fixed', 'sticky',
19
+ // State Capabilities
20
+ 'hoverable', 'focusable', 'pressable', 'expandable', 'selectable', 'disabled'
19
21
  ];
20
22
 
21
23
  // Layout CSS mappings
@@ -290,7 +292,7 @@ export const DEFAULT_THEME = {
290
292
  'dark': '#3E4A5D',
291
293
  'light': '#DBEAFE',
292
294
  'primary': '#2563EB',
293
- 'secondary': '#DBEAFE',
295
+ 'secondary': '#1E40AF',
294
296
  'success': '#10B981',
295
297
  'warning': '#F59E0B',
296
298
  'danger': '#EF4444',
@@ -4,20 +4,14 @@
4
4
  */
5
5
 
6
6
  import { BREAKPOINTS, STATES, LAYOUT_KEYWORDS } from './constants.js';
7
+ import { sanitizeValue } from '../utils/common.js';
7
8
 
8
9
  /**
9
10
  * Sanitize token value to prevent CSS injection
10
11
  * @param {string} value - Value to sanitize
11
12
  * @returns {string} - Sanitized value
12
13
  */
13
- export function sanitizeValue(value) {
14
- if (typeof value !== 'string') {
15
- return '';
16
- }
17
- // Only remove semicolons which can terminate CSS rules
18
- // Allow braces for legitimate use cases like content:["{icon}"]
19
- return value.replace(/;/g, '_');
20
- }
14
+ export { sanitizeValue };
21
15
 
22
16
  /**
23
17
  * Validate token structure
@@ -46,56 +40,7 @@ export function isValidToken(token) {
46
40
  return true;
47
41
  }
48
42
 
49
- /**
50
- * Lightweight token parser (no validation, used for internal processing)
51
- * @param {string} raw - Raw token string
52
- * @returns {Object} - Parsed token object
53
- */
54
- export function parseToken(raw) {
55
- const token = {
56
- raw,
57
- breakpoint: null,
58
- state: null,
59
- property: null,
60
- value: null,
61
- isArbitrary: false
62
- };
63
-
64
- const parts = raw.split(':');
65
- let idx = 0;
66
43
 
67
- // Check for breakpoint
68
- if (BREAKPOINTS.includes(parts[0])) {
69
- token.breakpoint = parts[0];
70
- idx++;
71
- }
72
-
73
- // Check for state
74
- if (STATES.includes(parts[idx])) {
75
- token.state = parts[idx];
76
- idx++;
77
- }
78
-
79
- // Property
80
- if (idx < parts.length) {
81
- token.property = parts[idx];
82
- idx++;
83
- }
84
-
85
- // Value
86
- if (idx < parts.length) {
87
- let value = parts.slice(idx).join(':');
88
- const arbitraryMatch = value.match(/^\[(.+)\]$/);
89
- if (arbitraryMatch) {
90
- token.value = arbitraryMatch[1].replace(/_/g, ' ');
91
- token.isArbitrary = true;
92
- } else {
93
- token.value = value;
94
- }
95
- }
96
-
97
- return token;
98
- }
99
44
 
100
45
  /**
101
46
  * Tokenize a single attribute value string
@@ -230,4 +175,4 @@ export function tokenizeAll(parsed) {
230
175
  return tokens;
231
176
  }
232
177
 
233
- export default { tokenize, tokenizeAll, parseToken, sanitizeValue, isValidToken };
178
+ export default { tokenize, tokenizeAll, sanitizeValue, isValidToken };
@@ -108,12 +108,13 @@ export function getDefinition(name) {
108
108
  /**
109
109
  * Validate that all definitions have required fields
110
110
  * Used by tests to ensure definitions are complete
111
+ * @param {Array} definitions - Optional array of definitions to validate (defaults to all)
111
112
  */
112
- export function validateDefinitions() {
113
+ export function validateDefinitions(definitions = getAllDefinitions()) {
113
114
  const requiredFields = ['name', 'property', 'description', 'descriptionMs', 'category'];
114
115
  const errors = [];
115
116
 
116
- for (const def of getAllDefinitions()) {
117
+ for (const def of definitions) {
117
118
  for (const field of requiredFields) {
118
119
  if (!def[field]) {
119
120
  errors.push(`Missing '${field}' in definition '${def.name || 'unknown'}'`);
@@ -174,13 +174,17 @@ export const layoutDefinitions = {
174
174
  };
175
175
 
176
176
  // Build flat value map for CSS generator
177
- export function buildLayoutMap() {
177
+ export function buildLayoutMap(definitions = layoutDefinitions) {
178
178
  const map = {};
179
179
 
180
180
  // Add all simple keyword values from definitions
181
- for (const def of Object.values(layoutDefinitions)) {
181
+ for (const def of Object.values(definitions)) {
182
182
  if (def.dynamic) continue; // Skip dynamic properties that need special handling
183
183
 
184
+ // Only include definitions that act as global keywords (no prefix in syntax)
185
+ // format: layout="[value]"
186
+ if (!def.syntax || !def.syntax.includes('layout="[')) continue;
187
+
184
188
  for (const v of def.values) {
185
189
  // Skip range values like '1-12' that need special handling
186
190
  if (v.value.match(/^\d+-\d+$/)) continue;
@@ -244,16 +244,29 @@ export const width = {
244
244
  'big', 'big-2x', 'big-3x', 'big-4x',
245
245
  'giant', 'giant-2x', 'giant-3x', 'giant-4x',
246
246
  'vast', 'vast-2x', 'vast-3x', 'vast-4x', 'vast-5x', 'vast-6x', 'vast-7x', 'vast-8x', 'vast-9x', 'vast-10x',
247
- 'min', 'max', 'fit'
247
+ 'min', 'max', 'fit',
248
+ // Percentage adjectives
249
+ 'full', 'half', 'third', 'third-2x', 'quarter', 'quarter-2x', 'quarter-3x',
250
+ // Fractional values (backwards compatibility)
251
+ '1/1', '1/2', '1/3', '2/3', '1/4', '2/4', '3/4'
252
+ ],
253
+ percentageAdjectives: [
254
+ { name: 'full', value: '100%', description: 'Full width (100%)', descriptionMs: 'Lebar penuh (100%)' },
255
+ { name: 'half', value: '50%', description: 'Half width (50%)', descriptionMs: 'Separuh lebar (50%)' },
256
+ { name: 'third', value: '33.333333%', description: 'One third width (33%)', descriptionMs: 'Satu pertiga lebar (33%)' },
257
+ { name: 'third-2x', value: '66.666667%', description: 'Two thirds width (66%)', descriptionMs: 'Dua pertiga lebar (66%)' },
258
+ { name: 'quarter', value: '25%', description: 'One quarter width (25%)', descriptionMs: 'Satu perempat lebar (25%)' },
259
+ { name: 'quarter-2x', value: '50%', description: 'Two quarters width (50%)', descriptionMs: 'Dua perempat lebar (50%)' },
260
+ { name: 'quarter-3x', value: '75%', description: 'Three quarters width (75%)', descriptionMs: 'Tiga perempat lebar (75%)' }
248
261
  ],
249
262
  supportsArbitrary: true,
250
263
  examples: [
251
- { code: '<div space="w:[100%]">Full width</div>', description: 'Full width' },
264
+ { code: '<div space="w:full">Full width</div>', description: 'Full width' },
265
+ { code: '<div space="w:half">Half width</div>', description: 'Half width (50%)' },
266
+ { code: '<div space="w:third">Third width</div>', description: 'One third width (33%)' },
267
+ { code: '<div space="w:quarter-3x">Three quarters</div>', description: 'Three quarters width (75%)' },
252
268
  { code: '<div space="max-w:[1200px]">Max width container</div>', description: 'Max width' },
253
- { code: '<div space="min-w:[300px]">Min width</div>', description: 'Minimum width' },
254
- { code: '<div space="w:max">Content width</div>', description: 'Width based on content (max-content)' },
255
- { code: '<div space="max-w:max">Max content width</div>', description: 'Maximum content width' },
256
- { code: '<div space="min-w:min">Min content width</div>', description: 'Minimum content width' }
269
+ { code: '<div space="w:max">Content width</div>', description: 'Width based on content (max-content)' }
257
270
  ],
258
271
  preview: [
259
272
  {
@@ -262,11 +275,11 @@ export const width = {
262
275
  description: 'Set fixed or percentage widths',
263
276
  descriptionMs: 'Tetapkan lebar tetap atau peratusan',
264
277
  html: `<div layout="flex col" space="g:small p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium">
265
- <div space="w:[100%] p:small" visual="bg:primary text:white rounded:small">w:[100%]</div>
266
- <div space="w:[75%] p:small" visual="bg:primary text:white rounded:small">w:[75%]</div>
267
- <div space="w:[50%] p:small" visual="bg:primary text:white rounded:small">w:[50%]</div>
278
+ <div space="w:full p:small" visual="bg:primary text:white rounded:small">w:full</div>
279
+ <div space="w:quarter-3x p:small" visual="bg:primary text:white rounded:small">w:quarter-3x</div>
280
+ <div space="w:half p:small" visual="bg:primary text:white rounded:small">w:half</div>
268
281
  </div>`,
269
- highlightValue: 'w:[100%]'
282
+ highlightValue: 'w:full'
270
283
  },
271
284
  {
272
285
  title: 'Content-Based Sizing',
@@ -329,15 +342,28 @@ export const height = {
329
342
  'big', 'big-2x', 'big-3x', 'big-4x',
330
343
  'giant', 'giant-2x', 'giant-3x', 'giant-4x',
331
344
  'vast', 'vast-2x', 'vast-3x', 'vast-4x', 'vast-5x', 'vast-6x', 'vast-7x', 'vast-8x', 'vast-9x', 'vast-10x',
332
- 'min', 'max', 'fit'
345
+ 'min', 'max', 'fit',
346
+ // Percentage adjectives
347
+ 'full', 'half', 'third', 'third-2x', 'quarter', 'quarter-2x', 'quarter-3x',
348
+ // Fractional values (backwards compatibility)
349
+ '1/1', '1/2', '1/3', '2/3', '1/4', '2/4', '3/4'
350
+ ],
351
+ percentageAdjectives: [
352
+ { name: 'full', value: '100%', description: 'Full height (100%)', descriptionMs: 'Tinggi penuh (100%)' },
353
+ { name: 'half', value: '50%', description: 'Half height (50%)', descriptionMs: 'Separuh tinggi (50%)' },
354
+ { name: 'third', value: '33.333333%', description: 'One third height (33%)', descriptionMs: 'Satu pertiga tinggi (33%)' },
355
+ { name: 'third-2x', value: '66.666667%', description: 'Two thirds height (66%)', descriptionMs: 'Dua pertiga tinggi (66%)' },
356
+ { name: 'quarter', value: '25%', description: 'One quarter height (25%)', descriptionMs: 'Satu perempat tinggi (25%)' },
357
+ { name: 'quarter-2x', value: '50%', description: 'Two quarters height (50%)', descriptionMs: 'Dua perempat tinggi (50%)' },
358
+ { name: 'quarter-3x', value: '75%', description: 'Three quarters height (75%)', descriptionMs: 'Tiga perempat tinggi (75%)' }
333
359
  ],
334
360
  supportsArbitrary: true,
335
361
  examples: [
336
- { code: '<div space="h:[100vh]">Full viewport height</div>', description: 'Full height' },
362
+ { code: '<div space="h:full">Full height</div>', description: 'Full height' },
363
+ { code: '<div space="h:half">Half height</div>', description: 'Half height (50%)' },
364
+ { code: '<div space="h:[100vh]">Full viewport height</div>', description: 'Full viewport height' },
337
365
  { code: '<div space="min-h:[400px]">Min height</div>', description: 'Minimum height' },
338
- { code: '<div space="h:max">Content height</div>', description: 'Height based on content (max-content)' },
339
- { code: '<div space="max-h:max">Max content height</div>', description: 'Maximum content height' },
340
- { code: '<div space="min-h:min">Min content height</div>', description: 'Minimum content height' }
366
+ { code: '<div space="h:max">Content height</div>', description: 'Height based on content (max-content)' }
341
367
  ],
342
368
  preview: [
343
369
  {
@@ -346,11 +372,11 @@ export const height = {
346
372
  description: 'Set fixed heights',
347
373
  descriptionMs: 'Tetapkan tinggi tetap',
348
374
  html: `<div layout="flex" space="g:small p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium" style="height: 120px;">
349
- <div space="h:[100%] p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:[100%]</div>
350
- <div space="h:[80px] p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:[80px]</div>
351
- <div space="h:[60px] p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:[60px]</div>
375
+ <div space="h:full p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:full</div>
376
+ <div space="h:third-2x p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:third-2x</div>
377
+ <div space="h:half p:small" visual="bg:primary text:white rounded:small" layout="flex center">h:half</div>
352
378
  </div>`,
353
- highlightValue: 'h:[100%]'
379
+ highlightValue: 'h:full'
354
380
  },
355
381
  {
356
382
  title: 'Content-Based Height',
package/src/index.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * SenangStart CSS - Core API
3
+ * Exporting the compiler engine for programmatic usage
4
+ */
5
+
6
+ // Core Tokenizer
7
+ import { tokenize, tokenizeAll } from './core/tokenizer-core.js';
8
+
9
+ // Parsers
10
+ import { parseSource, parseMultipleSources } from './compiler/parser.js';
11
+
12
+ // Generators
13
+ import { generateCSS, generateCSSVariables } from './compiler/generators/css.js';
14
+ import { generatePreflight } from './compiler/generators/preflight.js';
15
+
16
+ // Configuration
17
+ import { defaultConfig, mergeConfig } from './config/defaults.js';
18
+
19
+ // Constants
20
+ import * as constants from './core/constants.js';
21
+
22
+ // High-level Compiler
23
+ import { compileSource, compileMultiple } from './compiler/index.js';
24
+
25
+ export {
26
+ // Core
27
+ tokenize,
28
+ tokenizeAll,
29
+ parseSource,
30
+ parseMultipleSources,
31
+
32
+ // Generators
33
+ generateCSS,
34
+ generateCSSVariables,
35
+ generatePreflight,
36
+
37
+ // High-level
38
+ compileSource,
39
+ compileMultiple,
40
+
41
+ // Configuration
42
+ defaultConfig,
43
+ mergeConfig,
44
+
45
+ // Constants
46
+ constants
47
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * SenangStart CSS - Common Utilities
3
+ * Shared helper functions for compiler and runtime
4
+ */
5
+
6
+ /**
7
+ * Sanitize token value to prevent CSS injection
8
+ * Removes potentially dangerous characters/sequences
9
+ * @param {string} value - Value to sanitize
10
+ * @returns {string} - Sanitized value
11
+ */
12
+ export function sanitizeValue(value) {
13
+ if (typeof value !== 'string') {
14
+ return '';
15
+ }
16
+
17
+ // Remove potentially dangerous characters that could break CSS syntax
18
+ // Note: We used to filter {} but some tests expect them (e.g. content icons), so we only filter ; for now
19
+ const dangerousChars = /[;]/g;
20
+ if (dangerousChars.test(value)) {
21
+ return value.replace(dangerousChars, '_');
22
+ }
23
+
24
+ return value;
25
+ }
26
+
27
+ export default { sanitizeValue };
@@ -28,7 +28,7 @@ export default {
28
28
 
29
29
  // Add custom colors
30
30
  // colors: {
31
- // 'brand': '#8B5CF6',
31
+ // 'brand': '#38BDF8',
32
32
  // 'accent': '#EC4899'
33
33
  // }
34
34
  }
@@ -62,7 +62,7 @@ export function createTestConfig(overrides = {}) {
62
62
  'white': '#FFFFFF',
63
63
  'black': '#000000',
64
64
  'primary': '#2563EB',
65
- 'secondary': '#DBEAFE',
65
+ 'secondary': '#1E40AF',
66
66
  'success': '#10B981',
67
67
  'warning': '#F59E0B',
68
68
  'danger': '#EF4444',
@@ -170,10 +170,21 @@ describe('Compiler Integration', () => {
170
170
 
171
171
  const result = compileMultiple(files, config);
172
172
 
173
- assert.ok(result.css);
174
173
  assert.strictEqual(result.tokens.length, 0);
175
174
  });
176
175
 
176
+ it('minifies output when configured', () => {
177
+ const files = [
178
+ { path: 'test.html', content: '<div layout="flex">Test</div>' }
179
+ ];
180
+ const config = createTestConfig({ output: { minify: true } });
181
+
182
+ const result = compileMultiple(files, config);
183
+
184
+ assert.ok(result.minifiedCSS);
185
+ assert.ok(result.minifiedCSS.length <= result.css.length);
186
+ });
187
+
177
188
  });
178
189
 
179
190
  describe('Real-world Scenarios', () => {