@bookklik/senangstart-css 0.2.10 → 0.2.12

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 (39) hide show
  1. package/.agent/skills/add-utility/SKILL.md +65 -0
  2. package/.agent/workflows/add-utility.md +2 -0
  3. package/.agent/workflows/build.md +2 -0
  4. package/.agent/workflows/dev.md +2 -0
  5. package/AGENTS.md +30 -0
  6. package/dist/senangstart-css.js +362 -151
  7. package/dist/senangstart-css.min.js +175 -174
  8. package/dist/senangstart-tw.js +4 -4
  9. package/dist/senangstart-tw.min.js +1 -1
  10. package/docs/ms/reference/visual/ring-color.md +2 -2
  11. package/docs/ms/reference/visual/ring-offset.md +3 -3
  12. package/docs/ms/reference/visual/ring.md +5 -5
  13. package/docs/public/assets/senangstart-css.min.js +175 -174
  14. package/docs/public/llms.txt +10 -10
  15. package/docs/reference/visual/ring-color.md +2 -2
  16. package/docs/reference/visual/ring-offset.md +3 -3
  17. package/docs/reference/visual/ring.md +5 -5
  18. package/package.json +1 -1
  19. package/src/cdn/tw-conversion-engine.js +4 -4
  20. package/src/cli/commands/build.js +42 -14
  21. package/src/cli/commands/dev.js +157 -93
  22. package/src/compiler/generators/css.js +371 -199
  23. package/src/compiler/tokenizer.js +25 -23
  24. package/src/core/tokenizer-core.js +46 -19
  25. package/src/definitions/visual-borders.js +10 -10
  26. package/src/utils/common.js +456 -39
  27. package/src/utils/node-io.js +82 -0
  28. package/tests/integration/dev-recovery.test.js +231 -0
  29. package/tests/unit/cli/memory-limits.test.js +169 -0
  30. package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
  31. package/tests/unit/compiler/generators/css-errors.test.js +102 -0
  32. package/tests/unit/convert-tailwind.test.js +518 -442
  33. package/tests/unit/utils/common.test.js +376 -26
  34. package/tests/unit/utils/file-timeout.test.js +154 -0
  35. package/tests/unit/utils/theme-validation.test.js +181 -0
  36. package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
  37. package/tests/unit/convert-tailwind.cli.test.js +0 -95
  38. package/tests/unit/security.test.js +0 -206
  39. /package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js} +0 -0
@@ -1247,28 +1247,28 @@ export default {
1247
1247
  **Syntax:** `visual="ring:[size]"`
1248
1248
 
1249
1249
  * `none`: No ring (`box-shadow: 0 0 0 0 transparent;`)
1250
- * `thin`: Thin ring (1px) (`box-shadow: 0 0 0 1px var(--ring-color);`)
1251
- * `regular`: Regular ring (2px) (`box-shadow: 0 0 0 2px var(--ring-color);`)
1252
- * `small`: Small ring (4px) (`box-shadow: 0 0 0 4px var(--ring-color);`)
1253
- * `medium`: Medium ring (6px) (`box-shadow: 0 0 0 6px var(--ring-color);`)
1254
- * `big`: Big ring (8px) (`box-shadow: 0 0 0 8px var(--ring-color);`)
1250
+ * `thin`: Thin ring (1px)
1251
+ * `regular`: Regular ring (2px)
1252
+ * `small`: Small ring (4px)
1253
+ * `medium`: Medium ring (6px)
1254
+ * `big`: Big ring (8px)
1255
1255
 
1256
1256
  ## ring-color
1257
1257
  > Set ring color
1258
1258
 
1259
1259
  **Syntax:** `visual="ring-color:[color]"`
1260
1260
 
1261
- * `primary`: Primary ring color (`--ring-color: var(--c-primary);`)
1262
- * `blue-500`: Blue ring color (`--ring-color: var(--c-blue-500);`)
1261
+ * `primary`: Primary ring color (`--ss-ring-color: var(--c-primary);`)
1262
+ * `blue-500`: Blue ring color (`--ss-ring-color: var(--c-blue-500);`)
1263
1263
 
1264
1264
  ## ring-offset
1265
1265
  > Add gap between ring and element
1266
1266
 
1267
1267
  **Syntax:** `visual="ring-offset:[size]"`
1268
1268
 
1269
- * `0`: No offset (`--ring-offset: 0px;`)
1270
- * `2`: 2px offset (`--ring-offset: 2px;`)
1271
- * `4`: 4px offset (`--ring-offset: 4px;`)
1269
+ * `0`: No offset (`--ss-ring-offset-width: 0px;`)
1270
+ * `2`: 2px offset (`--ss-ring-offset-width: 2px;`)
1271
+ * `4`: 4px offset (`--ss-ring-offset-width: 4px;`)
1272
1272
 
1273
1273
  ## scroll-behavior
1274
1274
  > Set scroll behavior
@@ -11,8 +11,8 @@ visual="ring-color:[color]"
11
11
 
12
12
  | Value | CSS Output | Description |
13
13
  |-------|------------|-------------|
14
- | `primary` | `--ring-color: var(--c-primary)` | Primary ring color |
15
- | `blue-500` | `--ring-color: var(--c-blue-500)` | Blue ring color |
14
+ | `primary` | `--ss-ring-color: var(--c-primary)` | Primary ring color |
15
+ | `blue-500` | `--ss-ring-color: var(--c-blue-500)` | Blue ring color |
16
16
 
17
17
  ## Examples
18
18
 
@@ -11,9 +11,9 @@ visual="ring-offset:[size]"
11
11
 
12
12
  | Value | CSS Output | Description |
13
13
  |-------|------------|-------------|
14
- | `0` | `--ring-offset: 0px` | No offset |
15
- | `2` | `--ring-offset: 2px` | 2px offset |
16
- | `4` | `--ring-offset: 4px` | 4px offset |
14
+ | `0` | `--ss-ring-offset-width: 0px` | No offset |
15
+ | `2` | `--ss-ring-offset-width: 2px` | 2px offset |
16
+ | `4` | `--ss-ring-offset-width: 4px` | 4px offset |
17
17
 
18
18
  ## Examples
19
19
 
@@ -12,11 +12,11 @@ visual="ring:[size]"
12
12
  | Value | CSS Output | Description |
13
13
  |-------|------------|-------------|
14
14
  | `none` | `box-shadow: 0 0 0 0 transparent` | No ring |
15
- | `thin` | `box-shadow: 0 0 0 1px var(--ring-color)` | Thin ring (1px) |
16
- | `regular` | `box-shadow: 0 0 0 2px var(--ring-color)` | Regular ring (2px) |
17
- | `small` | `box-shadow: 0 0 0 4px var(--ring-color)` | Small ring (4px) |
18
- | `medium` | `box-shadow: 0 0 0 6px var(--ring-color)` | Medium ring (6px) |
19
- | `big` | `box-shadow: 0 0 0 8px var(--ring-color)` | Big ring (8px) |
15
+ | `thin` | `box-shadow: var(--ring-inset) 0 0 0 1px var(--ss-ring-color)` | Thin ring (1px) |
16
+ | `regular` | `box-shadow: var(--ring-inset) 0 0 0 2px var(--ss-ring-color)` | Regular ring (2px) |
17
+ | `small` | `box-shadow: var(--ring-inset) 0 0 0 4px var(--ss-ring-color)` | Small ring (4px) |
18
+ | `medium` | `box-shadow: var(--ring-inset) 0 0 0 6px var(--ss-ring-color)` | Medium ring (6px) |
19
+ | `big` | `box-shadow: var(--ring-inset) 0 0 0 8px var(--ss-ring-color)` | Big ring (8px) |
20
20
 
21
21
  ## Examples
22
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookklik/senangstart-css",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Fluent Style Utilities for Humans and AI",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -330,7 +330,7 @@ const visualKeywords = {
330
330
 
331
331
  function getSpacing(value, exact) {
332
332
  // Check if it's already an arbitrary value with brackets
333
- if (value.startsWith('[') && value.endsWith(']')) {
333
+ if (value && value.startsWith('[') && value.endsWith(']')) {
334
334
  return value; // Return as-is, don't double-wrap
335
335
  }
336
336
  if (exact) {
@@ -548,7 +548,7 @@ function convertClass(twClass, exact) {
548
548
  const side = marginMatch[1] ? "-" + marginMatch[1] : "";
549
549
  let val = getSpacing(marginMatch[2], exact);
550
550
 
551
- if (isNeg) {
551
+ if (isNeg && val) {
552
552
  if (val.startsWith('[') && val.endsWith(']')) {
553
553
  // Handle arbitrary value: [10px] -> [-10px]
554
554
  const inner = val.slice(1, -1);
@@ -646,7 +646,7 @@ function convertClass(twClass, exact) {
646
646
  if (positionMatch) {
647
647
  const prop = positionMatch[1];
648
648
  let val = positionMatch[2];
649
- if (val.startsWith('[') && val.endsWith(']')) {
649
+ if (val && val.startsWith('[') && val.endsWith(']')) {
650
650
  // Keep arbitrary values as-is
651
651
  } else if (fractionScale[val]) {
652
652
  // Map fractions to semantic names (1/2 → half, etc.)
@@ -668,7 +668,7 @@ function convertClass(twClass, exact) {
668
668
  let val = translateMatch[3];
669
669
 
670
670
  // Map fractions and values
671
- if (val.startsWith('[') && val.endsWith(']')) {
671
+ if (val && val.startsWith('[') && val.endsWith(']')) {
672
672
  // Keep arbitrary values as-is, but handle negative
673
673
  if (isNeg) {
674
674
  const inner = val.slice(1, -1);
@@ -3,16 +3,17 @@
3
3
  * One-time compilation of CSS from source files
4
4
  */
5
5
 
6
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
6
+ import { writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
7
7
  import { join, dirname, resolve } from 'path';
8
8
  import { defaultConfig, mergeConfig } from '../../config/defaults.js';
9
9
  import { parseSource } from '../../compiler/parser.js';
10
- import { tokenizeAll } from '../../compiler/tokenizer.js';
10
+ import { tokenizeAll, tokenizeAllWithBatching } from '../../compiler/tokenizer.js';
11
11
  import { generateCSS, minifyCSS } from '../../compiler/generators/css.js';
12
12
  import { generateAIContext } from '../../compiler/generators/ai-context.js';
13
13
  import { generateTypeScript } from '../../compiler/generators/typescript.js';
14
14
  import logger from '../../utils/logger.js';
15
- import { validateThemeValue } from '../../utils/common.js';
15
+ import { validateThemeValue, getMemoryUsage } from '../../utils/common.js';
16
+ import { readMultipleFilesWithTimeout } from '../../utils/node-io.js';
16
17
 
17
18
  /**
18
19
  * Find files matching content patterns
@@ -119,31 +120,58 @@ export async function build(options = {}) {
119
120
  }
120
121
 
121
122
  logger.info(`Found ${files.length} source files`);
122
-
123
- // Parse all files
123
+
124
+ // Parse all files with timeout protection
124
125
  const allTokens = {
125
126
  layout: new Set(),
126
127
  space: new Set(),
127
128
  visual: new Set()
128
129
  };
129
-
130
- for (const filePath of files) {
130
+
131
+ let failedFiles = 0;
132
+ const fileReadResults = await readMultipleFilesWithTimeout(files, 5000);
133
+
134
+ for (const { path: filePath, content, error } of fileReadResults) {
135
+ if (error) {
136
+ logger.warn(`Skipping ${filePath}: ${error.message}`);
137
+ failedFiles++;
138
+ continue;
139
+ }
140
+
131
141
  try {
132
- const content = readFileSync(filePath, 'utf-8');
133
142
  const parsed = parseSource(content);
134
-
143
+
135
144
  parsed.layout.forEach(t => allTokens.layout.add(t));
136
145
  parsed.space.forEach(t => allTokens.space.add(t));
137
146
  parsed.visual.forEach(t => allTokens.visual.add(t));
138
147
  } catch (e) {
139
- logger.warn(`Could not parse ${filePath}`);
148
+ logger.warn(`Could not parse ${filePath}: ${e.message}`);
149
+ failedFiles++;
140
150
  }
141
151
  }
142
-
152
+
153
+ if (failedFiles > 0) {
154
+ logger.warn(`${failedFiles} file(s) failed to process`);
155
+ }
156
+
157
+ // Calculate total token count
158
+ const totalTokens = allTokens.layout.size + allTokens.space.size + allTokens.visual.size;
159
+ logger.info(`Found ${totalTokens} unique token values`);
160
+
161
+ // Check memory usage and decide whether to use batch processing
162
+ const currentMemory = getMemoryUsage();
163
+ const useBatching = totalTokens > 10000 || currentMemory > 200;
164
+
143
165
  // Tokenize
144
- const tokens = tokenizeAll(allTokens);
145
-
146
- logger.info(`Extracted ${tokens.length} unique tokens`);
166
+ let tokens;
167
+ if (useBatching) {
168
+ logger.info('Using batch processing for memory protection');
169
+ tokens = await tokenizeAllWithBatching(allTokens, 1000);
170
+ } else {
171
+ tokens = tokenizeAll(allTokens);
172
+ }
173
+
174
+ logger.info(`Generated ${tokens.length} tokens`);
147
175
 
148
176
  // Check for invalid tokens
149
177
  const invalidTokens = tokens.filter(token => token.error);
@@ -1,93 +1,157 @@
1
- /**
2
- * SenangStart CSS - Dev Command
3
- * Watch mode with live compilation
4
- */
5
-
6
- import chokidar from 'chokidar';
7
- import { build } from './build.js';
8
- import logger from '../../utils/logger.js';
9
-
10
- /**
11
- * Dev command handler - watches files and rebuilds on changes
12
- */
13
- export async function dev(options = {}) {
14
- logger.watch('Starting development mode...');
15
-
16
- // Build lock to prevent overlapping builds
17
- let buildInProgress = false;
18
- let pendingBuild = false;
19
-
20
- // Initial build
21
- await build(options);
22
-
23
- // Watch patterns
24
- const watchPatterns = [
25
- './**/*.html',
26
- './**/*.htm',
27
- './src/**/*.{html,jsx,tsx,vue,svelte}',
28
- './pages/**/*.{html,jsx,tsx}',
29
- './components/**/*.{html,jsx,tsx,vue,svelte}'
30
- ];
31
-
32
- // Ignore patterns
33
- const ignorePatterns = [
34
- '**/node_modules/**',
35
- '**/dist/**',
36
- '**/.git/**',
37
- '**/public/**'
38
- ];
39
-
40
- // Create watcher
41
- const watcher = chokidar.watch(watchPatterns, {
42
- ignored: ignorePatterns,
43
- persistent: true,
44
- ignoreInitial: true
45
- });
46
-
47
- // Debounce timer
48
- let debounceTimer = null;
49
-
50
- async function debouncedBuild() {
51
- if (buildInProgress) {
52
- pendingBuild = true;
53
- return;
54
- }
55
-
56
- buildInProgress = true;
57
-
58
- if (debounceTimer) {
59
- clearTimeout(debounceTimer);
60
- }
61
-
62
- debounceTimer = setTimeout(async () => {
63
- logger.watch('Change detected, rebuilding...');
64
- await build(options);
65
- buildInProgress = false;
66
-
67
- // Handle pending build
68
- if (pendingBuild) {
69
- pendingBuild = false;
70
- debouncedBuild();
71
- }
72
- }, 100);
73
- }
74
-
75
- // Watch events
76
- watcher
77
- .on('change', (path) => {
78
- logger.info(`Changed: ${path}`);
79
- debouncedBuild();
80
- })
81
- .on('add', (path) => {
82
- logger.info(`Added: ${path}`);
83
- debouncedBuild();
84
- })
85
- .on('unlink', (path) => {
86
- logger.info(`Removed: ${path}`);
87
- debouncedBuild();
88
- });
89
-
90
- logger.watch('Watching for changes... (Ctrl+C to stop)');
91
- }
92
-
93
- export default dev;
1
+ /**
2
+ * SenangStart CSS - Dev Command
3
+ * Watch mode with live compilation
4
+ */
5
+
6
+ import chokidar from 'chokidar';
7
+ import { build } from './build.js';
8
+ import logger from '../../utils/logger.js';
9
+
10
+ /**
11
+ * Dev command handler - watches files and rebuilds on changes
12
+ */
13
+ export async function dev(options = {}) {
14
+ logger.watch('Starting development mode...');
15
+
16
+ // Error tracking for cooldown mechanism
17
+ let consecutiveErrors = 0;
18
+ let lastErrorTime = 0;
19
+ const MAX_CONSECUTIVE_ERRORS = 5;
20
+ const COOLDOWN_DURATION = 30000; // 30 seconds
21
+
22
+ // Build lock to prevent overlapping builds
23
+ let buildInProgress = false;
24
+ let pendingBuild = false;
25
+
26
+ async function runBuild() {
27
+ // Check if we're in cooldown
28
+ if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
29
+ const timeSinceLastError = Date.now() - lastErrorTime;
30
+ if (timeSinceLastError < COOLDOWN_DURATION) {
31
+ const cooldownRemaining = Math.ceil((COOLDOWN_DURATION - timeSinceLastError) / 1000);
32
+ logger.warn(`Cooldown active: ${cooldownRemaining}s remaining. Skipping build.`);
33
+ return;
34
+ }
35
+ // Cooldown expired, reset error count
36
+ consecutiveErrors = 0;
37
+ }
38
+
39
+ try {
40
+ await build(options);
41
+ // Reset error count on successful build
42
+ consecutiveErrors = 0;
43
+ } catch (error) {
44
+ consecutiveErrors++;
45
+ lastErrorTime = Date.now();
46
+ logger.error(`Build failed (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}): ${error.message}`);
47
+
48
+ // Enter cooldown if max errors reached
49
+ if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
50
+ logger.warn(`Maximum consecutive errors (${MAX_CONSECUTIVE_ERRORS}) reached.`);
51
+ logger.warn(`Entering ${COOLDOWN_DURATION / 1000}s cooldown to prevent resource exhaustion.`);
52
+ }
53
+ }
54
+ }
55
+
56
+ // Initial build
57
+ await runBuild();
58
+
59
+ function createWatcher() {
60
+ // Watch patterns
61
+ const watchPatterns = [
62
+ './**/*.html',
63
+ './**/*.htm',
64
+ './src/**/*.{html,jsx,tsx,vue,svelte}',
65
+ './pages/**/*.{html,jsx,tsx}',
66
+ './components/**/*.{html,jsx,tsx,vue,svelte}'
67
+ ];
68
+
69
+ // Ignore patterns
70
+ const ignorePatterns = [
71
+ '**/node_modules/**',
72
+ '**/dist/**',
73
+ '**/.git/**',
74
+ '**/public/**'
75
+ ];
76
+
77
+ // Create watcher
78
+ const watcher = chokidar.watch(watchPatterns, {
79
+ ignored: ignorePatterns,
80
+ persistent: true,
81
+ ignoreInitial: true
82
+ });
83
+
84
+ // Handle watcher errors
85
+ watcher.on('error', (error) => {
86
+ logger.error(`Watcher error: ${error.message}`);
87
+ consecutiveErrors++;
88
+ lastErrorTime = Date.now();
89
+
90
+ // Try to restart watcher after error
91
+ logger.info('Attempting to restart watcher...');
92
+ try {
93
+ watcher.close();
94
+ setTimeout(() => {
95
+ const newWatcher = createWatcher();
96
+ Object.assign(watcher, newWatcher);
97
+ logger.success('Watcher restarted successfully');
98
+ }, 1000);
99
+ } catch (restartError) {
100
+ logger.error(`Failed to restart watcher: ${restartError.message}`);
101
+ }
102
+ });
103
+
104
+ return watcher;
105
+ }
106
+
107
+ // Initialize watcher
108
+ const watcher = createWatcher();
109
+
110
+ // Debounce timer
111
+ let debounceTimer = null;
112
+
113
+ async function debouncedBuild() {
114
+ if (buildInProgress) {
115
+ pendingBuild = true;
116
+ return;
117
+ }
118
+
119
+ buildInProgress = true;
120
+
121
+ if (debounceTimer) {
122
+ clearTimeout(debounceTimer);
123
+ }
124
+
125
+ debounceTimer = setTimeout(async () => {
126
+ logger.watch('Change detected, rebuilding...');
127
+ await runBuild();
128
+ buildInProgress = false;
129
+
130
+ // Handle pending build
131
+ if (pendingBuild) {
132
+ pendingBuild = false;
133
+ debouncedBuild();
134
+ }
135
+ }, 100);
136
+ }
137
+
138
+ // Watch events
139
+ watcher
140
+ .on('change', (path) => {
141
+ logger.info(`Changed: ${path}`);
142
+ debouncedBuild();
143
+ })
144
+ .on('add', (path) => {
145
+ logger.info(`Added: ${path}`);
146
+ debouncedBuild();
147
+ })
148
+ .on('unlink', (path) => {
149
+ logger.info(`Removed: ${path}`);
150
+ debouncedBuild();
151
+ });
152
+
153
+ logger.watch('Watching for changes... (Ctrl+C to stop)');
154
+ }
155
+
156
+ export default dev;
157
+