@versini/ui-system 4.0.5 → 4.1.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/README.md CHANGED
@@ -1,3 +1,281 @@
1
1
  # @versini/ui-system
2
2
 
3
- System level components such as Flexgrid and ThemeProvider.
3
+ [![npm version](https://img.shields.io/npm/v/@versini/ui-system?style=flat-square)](https://www.npmjs.com/package/@versini/ui-system)
4
+
5
+ > Layout and system components for React applications built with TypeScript and TailwindCSS.
6
+
7
+ The System package provides foundational layout components including flexbox grids, theme providers, and other system-level utilities for building consistent user interfaces.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Features](#features)
12
+ - [Installation](#installation)
13
+ - [Usage](#usage)
14
+ - [Tailwind Alternative](#tailwind-alternative)
15
+
16
+ ## Features
17
+
18
+ - **Flexgrid**: A flexible grid system based on CSS flexbox with 12-column layout support
19
+ - **FlexgridItem**: Grid items with configurable span, responsive breakpoints, and auto-sizing
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @versini/ui-system
25
+ ```
26
+
27
+ > **Note**: This component requires TailwindCSS and the `@versini/ui-styles` plugin for proper styling. See the [root README](../../README.md#tailwindcss-setup) for complete setup instructions.
28
+
29
+ ## Usage
30
+
31
+ ### Basic Grid Layout
32
+
33
+ ```tsx
34
+ import { Flexgrid, FlexgridItem } from "@versini/ui-system";
35
+
36
+ function App() {
37
+ return (
38
+ <Flexgrid>
39
+ <FlexgridItem span={6}>Left column</FlexgridItem>
40
+ <FlexgridItem span={6}>Right column</FlexgridItem>
41
+ </Flexgrid>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ### Grid with Gaps and Alignment
47
+
48
+ ```tsx
49
+ <Flexgrid
50
+ columnGap={2}
51
+ rowGap={1}
52
+ alignHorizontal="center"
53
+ alignVertical="stretch"
54
+ >
55
+ <FlexgridItem span={4}>Item 1</FlexgridItem>
56
+ <FlexgridItem span={4}>Item 2</FlexgridItem>
57
+ <FlexgridItem span={4}>Item 3</FlexgridItem>
58
+ </Flexgrid>
59
+ ```
60
+
61
+ ### Responsive Layout
62
+
63
+ ```tsx
64
+ <Flexgrid columnGap={1}>
65
+ <FlexgridItem span={{ fallback: 12, sm: 6, md: 4, lg: 3 }}>
66
+ Responsive item
67
+ </FlexgridItem>
68
+ <FlexgridItem span={{ fallback: 12, sm: 6, md: 8, lg: 9 }}>
69
+ Another responsive item
70
+ </FlexgridItem>
71
+ </Flexgrid>
72
+ ```
73
+
74
+ ### Auto-sizing and Direction
75
+
76
+ ```tsx
77
+ <Flexgrid direction="column" columnGap={1}>
78
+ <FlexgridItem span="auto">Auto-growing item</FlexgridItem>
79
+ <FlexgridItem span={6}>Fixed width item</FlexgridItem>
80
+ </Flexgrid>
81
+ ```
82
+
83
+ ## Tailwind Alternative
84
+
85
+ While Flexgrid and FlexgridItem provide a convenient abstraction, the same layouts can often be achieved using Tailwind CSS flex utilities directly. In many cases, using Tailwind directly may be a better long-term approach as it reduces component dependencies and leverages standard CSS patterns.
86
+
87
+ ### Basic Grid Layout
88
+
89
+ **Flexgrid/FlexgridItem:**
90
+
91
+ ```tsx
92
+ <Flexgrid>
93
+ <FlexgridItem span={6}>Left column</FlexgridItem>
94
+ <FlexgridItem span={6}>Right column</FlexgridItem>
95
+ </Flexgrid>
96
+ ```
97
+
98
+ **Tailwind Alternative:**
99
+
100
+ ```tsx
101
+ <div className="flex flex-wrap">
102
+ <div className="basis-6/12 max-w-full">Left column</div>
103
+ <div className="basis-6/12 max-w-full">Right column</div>
104
+ </div>
105
+ ```
106
+
107
+ ### Grid with Gaps and Alignment
108
+
109
+ **Flexgrid/FlexgridItem:**
110
+
111
+ ```tsx
112
+ <Flexgrid
113
+ columnGap={2}
114
+ rowGap={1}
115
+ alignHorizontal="center"
116
+ alignVertical="stretch"
117
+ >
118
+ <FlexgridItem span={4}>Item 1</FlexgridItem>
119
+ <FlexgridItem span={4}>Item 2</FlexgridItem>
120
+ <FlexgridItem span={4}>Item 3</FlexgridItem>
121
+ </Flexgrid>
122
+ ```
123
+
124
+ **Tailwind Alternative:**
125
+
126
+ ```tsx
127
+ <div className="flex flex-wrap justify-center items-stretch gap-x-2 gap-y-1">
128
+ <div className="basis-4/12 max-w-full">Item 1</div>
129
+ <div className="basis-4/12 max-w-full">Item 2</div>
130
+ <div className="basis-4/12 max-w-full">Item 3</div>
131
+ </div>
132
+ ```
133
+
134
+ ### Responsive Layout
135
+
136
+ **Flexgrid/FlexgridItem:**
137
+
138
+ ```tsx
139
+ <Flexgrid columnGap={1}>
140
+ <FlexgridItem span={{ fallback: 12, sm: 6, md: 4, lg: 3 }}>
141
+ Responsive item
142
+ </FlexgridItem>
143
+ <FlexgridItem span={{ fallback: 12, sm: 6, md: 8, lg: 9 }}>
144
+ Another responsive item
145
+ </FlexgridItem>
146
+ </Flexgrid>
147
+ ```
148
+
149
+ **Tailwind Alternative:**
150
+
151
+ ```tsx
152
+ <div className="flex flex-wrap gap-x-1">
153
+ <div className="basis-full sm:basis-6/12 md:basis-4/12 lg:basis-3/12 max-w-full">
154
+ Responsive item
155
+ </div>
156
+ <div className="basis-full sm:basis-6/12 md:basis-8/12 lg:basis-9/12 max-w-full">
157
+ Another responsive item
158
+ </div>
159
+ </div>
160
+ ```
161
+
162
+ ### Auto-sizing and Direction
163
+
164
+ **Flexgrid/FlexgridItem:**
165
+
166
+ ```tsx
167
+ <Flexgrid direction="column" columnGap={1}>
168
+ <FlexgridItem span="auto">Auto-growing item</FlexgridItem>
169
+ <FlexgridItem span={6}>Fixed width item</FlexgridItem>
170
+ </Flexgrid>
171
+ ```
172
+
173
+ **Tailwind Alternative:**
174
+
175
+ ```tsx
176
+ <div className="flex flex-col gap-x-1">
177
+ <div className="basis-auto max-w-full grow">Auto-growing item</div>
178
+ <div className="basis-6/12 max-w-full">Fixed width item</div>
179
+ </div>
180
+ ```
181
+
182
+ ### What Flexgrid Provides That's Hard to Replicate
183
+
184
+ While most Flexgrid functionality can be achieved with Tailwind, there are a few areas where Flexgrid provides unique value:
185
+
186
+ 1. **Custom Gap System**: Flexgrid uses a custom gap ratio (0.25rem per unit) with negative margins and padding, which creates a different spacing behavior than Tailwind's standard gap utilities.
187
+
188
+ 2. **Simplified API**: The `span` prop with 12-column abstraction is more intuitive than remembering Tailwind's fractional basis classes.
189
+
190
+ 3. **Responsive Object Syntax**: The object-based responsive configuration (`{ fallback: 12, sm: 6 }`) is more readable than multiple responsive classes.
191
+
192
+ 4. **Context-aware Spacing**: The gap values are passed through React context to child items, ensuring consistent spacing without prop drilling.
193
+
194
+ However, for most use cases, Tailwind's flex utilities provide equivalent functionality with better performance (no JavaScript overhead) and broader ecosystem compatibility.
195
+
196
+ ## Migration Tool
197
+
198
+ ### Flexgrid to Tailwind CLI Converter
199
+
200
+ This package includes a CLI tool to help migrate existing Flexgrid components to native Tailwind CSS classes:
201
+
202
+ ```bash
203
+ # Using npx (recommended)
204
+ npx @versini/ui-system/flexgrid-to-tw --file src/components/MyComponent.tsx
205
+
206
+ # Or after installation
207
+ node ./node_modules/@versini/ui-system/dist/cli/flexgrid-to-tw.js --file src/components/MyComponent.tsx
208
+ ```
209
+
210
+ #### Features
211
+
212
+ - **Conservative approach**: Only converts simple, unambiguous cases
213
+ - **Complex preservation**: Preserves complex scenarios with explanatory comments
214
+ - **Import management**: Automatically handles import statements
215
+ - **Detailed reporting**: Shows conversion statistics and warnings
216
+
217
+ #### What Gets Converted
218
+
219
+ ✅ **Simple cases that are converted automatically:**
220
+
221
+ - Basic `Flexgrid` with standard props (`columnGap`, `rowGap`, `direction`, `alignHorizontal`, `alignVertical`)
222
+ - `FlexgridItem` with numeric spans (1-12) or `"auto"`
223
+ - Static responsive objects with simple breakpoint values
224
+ - Standard className preservation
225
+
226
+ ✅ **Examples of successful conversions:**
227
+
228
+ ```tsx
229
+ // Before
230
+ <Flexgrid columnGap={2} alignHorizontal="center">
231
+ <FlexgridItem span={4}>Content</FlexgridItem>
232
+ <FlexgridItem span={8}>Content</FlexgridItem>
233
+ </Flexgrid>
234
+
235
+ // After
236
+ <div className="flex flex-wrap justify-center gap-x-2">
237
+ <div className="box-border max-w-full basis-4/12">Content</div>
238
+ <div className="box-border max-w-full basis-8/12">Content</div>
239
+ </div>
240
+ ```
241
+
242
+ âš ī¸ **Complex cases that are preserved with comments:**
243
+
244
+ - Nested `Flexgrid` components
245
+ - Dynamic expressions in props (`span={isVisible ? 6 : 12}`)
246
+ - Function calls in props (`span={getSpan()}`)
247
+ - Prop spreading (`{...props}`)
248
+ - Template literals or complex objects
249
+ - Ternary operators, logical operators, or method calls
250
+
251
+ âš ī¸ **Examples of preserved code:**
252
+
253
+ ```tsx
254
+ // Before (preserved due to complexity)
255
+ <FlexgridItem span={isVisible ? 6 : 12}>Dynamic content</FlexgridItem>;
256
+
257
+ // After (preserved with comment)
258
+ {
259
+ /* TODO: Convert manually - Complex FlexgridItem with nested components or complex expressions */
260
+ }
261
+ <FlexgridItem span={isVisible ? 6 : 12}>Dynamic content</FlexgridItem>;
262
+ ```
263
+
264
+ #### Usage Tips
265
+
266
+ 1. **Run on individual files** for better control and review
267
+ 2. **Review preserved components** marked with `TODO: Convert manually` comments
268
+ 3. **Test thoroughly** after conversion to ensure functionality is preserved
269
+ 4. **Handle responsive objects manually** for complex breakpoint logic
270
+ 5. **Remove unused imports** if no other `@versini/ui-system` components remain
271
+
272
+ #### Why This Approach?
273
+
274
+ The CLI takes a **conservative approach** to ensure it never breaks your existing code or changes the intended behavior. Complex scenarios are preserved with clear comments explaining why manual conversion is needed, allowing you to:
275
+
276
+ - **Maintain confidence** that converted code works identically
277
+ - **Focus manual effort** only where truly needed
278
+ - **Understand limitations** through clear documentation
279
+ - **Proceed incrementally** file by file at your own pace
280
+
281
+ This ensures the tool helps accelerate migration while maintaining code quality and preserving the original intent of complex component usage.
@@ -0,0 +1,495 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ class FlexgridConverter {
5
+ convertedCount = 0;
6
+ skippedCount = 0;
7
+ errors = [];
8
+ warnings = [];
9
+ reset() {
10
+ this.convertedCount = 0;
11
+ this.skippedCount = 0;
12
+ this.errors = [];
13
+ this.warnings = [];
14
+ }
15
+ convert(content) {
16
+ this.reset();
17
+ const hasFlexgridComponents = this.hasFlexgridComponents(content);
18
+ content = this.convertFlexgridComponents(content);
19
+ if (hasFlexgridComponents && !this.hasPreservedComponents(content)) {
20
+ content = this.removeFlexgridImports(content);
21
+ } else if (hasFlexgridComponents && this.hasPreservedComponents(content)) {
22
+ this.warnings.push(
23
+ "Some Flexgrid components were preserved - imports kept intact"
24
+ );
25
+ }
26
+ return content;
27
+ }
28
+ hasFlexgridComponents(content) {
29
+ const contentWithoutImports = content.replace(
30
+ /import\s*\{[^}]*\}\s*from\s*["']@versini\/ui-system["'];?\n?/g,
31
+ ""
32
+ );
33
+ return /<Flexgrid|<FlexgridItem/.test(contentWithoutImports);
34
+ }
35
+ hasPreservedComponents(content) {
36
+ const contentWithoutImports = content.replace(
37
+ /import\s*\{[^}]*\}\s*from\s*["']@versini\/ui-system["'];?\n?/g,
38
+ ""
39
+ );
40
+ return /<Flexgrid|<FlexgridItem/.test(contentWithoutImports);
41
+ }
42
+ getResult() {
43
+ return {
44
+ success: this.errors.length === 0,
45
+ convertedCount: this.convertedCount,
46
+ skippedCount: this.skippedCount,
47
+ errors: this.errors,
48
+ warnings: this.warnings
49
+ };
50
+ }
51
+ removeFlexgridImports(content) {
52
+ content = content.replace(
53
+ /import\s*\{\s*([^}]*)\s*\}\s*from\s*["']@versini\/ui-system["'];?/g,
54
+ (_match, imports) => {
55
+ const importList = imports.split(",").map((imp) => imp.trim()).filter((imp) => !["Flexgrid", "FlexgridItem"].includes(imp));
56
+ if (importList.length === 0) {
57
+ return "";
58
+ }
59
+ return `import { ${importList.join(", ")} } from "@versini/ui-system";`;
60
+ }
61
+ );
62
+ content = content.replace(
63
+ /import\s*\{\s*(Flexgrid|FlexgridItem|Flexgrid,\s*FlexgridItem|FlexgridItem,\s*Flexgrid)\s*\}\s*from\s*["']@versini\/ui-system["'];?\n?/g,
64
+ ""
65
+ );
66
+ return content;
67
+ }
68
+ convertFlexgridComponents(content) {
69
+ content = this.convertFlexgridItems(content);
70
+ content = this.convertFlexgrids(content);
71
+ return content;
72
+ }
73
+ isComplexScenario(componentMatch) {
74
+ const isFlexgridItem = componentMatch.startsWith("<FlexgridItem");
75
+ const isFlexgrid = componentMatch.startsWith("<Flexgrid");
76
+ if (isFlexgridItem) {
77
+ const content = componentMatch.match(/<FlexgridItem[^>]*>(.*)<\/FlexgridItem>/s)?.[1] || "";
78
+ if (/<Flexgrid|<FlexgridItem/.test(content)) {
79
+ return true;
80
+ }
81
+ }
82
+ if (isFlexgrid) {
83
+ const content = componentMatch.match(/<Flexgrid[^>]*>(.*)<\/Flexgrid>/s)?.[1] || "";
84
+ if (/<Flexgrid/.test(content)) {
85
+ return true;
86
+ }
87
+ }
88
+ const complexExpressionRegex = /\{[^}]*(?:\?|&&|\|\||\.(?!\d)|\[|\(|=>|\+|\-|\*|\/)/;
89
+ if (complexExpressionRegex.test(componentMatch)) {
90
+ return true;
91
+ }
92
+ const propSpreadingRegex = /\{\.\.\./;
93
+ if (propSpreadingRegex.test(componentMatch)) {
94
+ return true;
95
+ }
96
+ const complexObjectRegex = /span=\{\s*\{[^}]*(?:\?|&&|\|\||\.(?!\d)|\[|\(|=>|`|\$)/;
97
+ if (complexObjectRegex.test(componentMatch)) {
98
+ return true;
99
+ }
100
+ const templateLiteralRegex = /\{[^}]*`[^`]*`[^}]*\}/;
101
+ if (templateLiteralRegex.test(componentMatch)) {
102
+ return true;
103
+ }
104
+ const functionCallRegex = /\{[^}]*\w+\([^)]*\)[^}]*\}/;
105
+ if (functionCallRegex.test(componentMatch)) {
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+ addPreservationComment(componentMatch, reason) {
111
+ return `{/* TODO: Convert manually - ${reason} */}
112
+ ${componentMatch}`;
113
+ }
114
+ convertFlexgridItems(content) {
115
+ const flexgridItemRegex = /<FlexgridItem\s*([^>]*?)>(.*?)<\/FlexgridItem>/gs;
116
+ return content.replace(
117
+ flexgridItemRegex,
118
+ (match, propsString, children) => {
119
+ if (this.isComplexScenario(match)) {
120
+ this.skippedCount++;
121
+ this.warnings.push(
122
+ "Complex FlexgridItem preserved - contains nested components, complex expressions, or prop spreading"
123
+ );
124
+ return this.addPreservationComment(
125
+ match,
126
+ "Complex FlexgridItem with nested components or complex expressions"
127
+ );
128
+ }
129
+ try {
130
+ const props = this.parseProps(propsString);
131
+ const classes = this.convertFlexgridItemToClasses(props);
132
+ this.convertedCount++;
133
+ if (props.className) {
134
+ return `<div className="${classes} ${props.className}">${children}</div>`;
135
+ }
136
+ return `<div className="${classes}">${children}</div>`;
137
+ } catch (error) {
138
+ this.errors.push(`Failed to convert FlexgridItem: ${error}`);
139
+ this.skippedCount++;
140
+ return this.addPreservationComment(
141
+ match,
142
+ `Conversion failed: ${error}`
143
+ );
144
+ }
145
+ }
146
+ );
147
+ }
148
+ convertFlexgrids(content) {
149
+ const flexgridRegex = /<Flexgrid\s*([^>]*?)>(.*?)<\/Flexgrid>/gs;
150
+ return content.replace(flexgridRegex, (match, propsString, children) => {
151
+ if (this.isComplexScenario(match)) {
152
+ this.skippedCount++;
153
+ this.warnings.push(
154
+ "Complex Flexgrid preserved - contains nested components, complex expressions, or prop spreading"
155
+ );
156
+ return this.addPreservationComment(
157
+ match,
158
+ "Complex Flexgrid with nested components or complex expressions"
159
+ );
160
+ }
161
+ try {
162
+ const props = this.parseProps(propsString);
163
+ const classes = this.convertFlexgridToClasses(props);
164
+ const styles = this.convertFlexgridToStyles(props);
165
+ this.convertedCount++;
166
+ const classAttr = props.className ? `${classes} ${props.className}` : classes;
167
+ const styleAttr = styles ? ` style={${styles}}` : "";
168
+ return `<div className="${classAttr}"${styleAttr}>${children}</div>`;
169
+ } catch (error) {
170
+ this.errors.push(`Failed to convert Flexgrid: ${error}`);
171
+ this.skippedCount++;
172
+ return this.addPreservationComment(
173
+ match,
174
+ `Conversion failed: ${error}`
175
+ );
176
+ }
177
+ });
178
+ }
179
+ parseProps(propsString) {
180
+ const props = {};
181
+ if (!propsString.trim()) {
182
+ return props;
183
+ }
184
+ const stringPropRegex = /(\w+)=["']([^"']+)["']/g;
185
+ let match = stringPropRegex.exec(propsString);
186
+ while (match !== null) {
187
+ const [, name, value] = match;
188
+ props[name] = value;
189
+ match = stringPropRegex.exec(propsString);
190
+ }
191
+ const propNames = propsString.match(/(\w+)=\{/g);
192
+ if (propNames) {
193
+ for (const propNameMatch of propNames) {
194
+ const propName = propNameMatch.slice(0, -2);
195
+ const startIndex = propsString.indexOf(`${propName}={`) + propName.length + 2;
196
+ let braceCount = 1;
197
+ let endIndex = startIndex;
198
+ while (braceCount > 0 && endIndex < propsString.length) {
199
+ const char = propsString[endIndex];
200
+ if (char === "{") {
201
+ braceCount++;
202
+ } else if (char === "}") {
203
+ braceCount--;
204
+ }
205
+ endIndex++;
206
+ }
207
+ if (braceCount === 0) {
208
+ const value = propsString.slice(startIndex, endIndex - 1);
209
+ try {
210
+ if (value === "true") {
211
+ props[propName] = true;
212
+ } else if (value === "false") {
213
+ props[propName] = false;
214
+ } else if (/^\d+$/.test(value)) {
215
+ props[propName] = parseInt(value, 10);
216
+ } else if (/^\d+\.\d+$/.test(value)) {
217
+ props[propName] = parseFloat(value);
218
+ } else if (value.startsWith('"') && value.endsWith('"')) {
219
+ props[propName] = value.slice(1, -1);
220
+ } else if (value.startsWith("'") && value.endsWith("'")) {
221
+ props[propName] = value.slice(1, -1);
222
+ } else {
223
+ props[propName] = JSON.parse(value);
224
+ }
225
+ } catch {
226
+ props[propName] = value;
227
+ }
228
+ }
229
+ }
230
+ }
231
+ return props;
232
+ }
233
+ convertFlexgridToClasses(props) {
234
+ const classes = ["flex", "flex-wrap"];
235
+ if (props.direction) {
236
+ switch (props.direction) {
237
+ case "column":
238
+ classes.push("flex-col");
239
+ break;
240
+ case "row-reverse":
241
+ classes.push("flex-row-reverse");
242
+ break;
243
+ case "column-reverse":
244
+ classes.push("flex-col-reverse");
245
+ break;
246
+ }
247
+ }
248
+ if (props.alignHorizontal) {
249
+ switch (props.alignHorizontal) {
250
+ case "flex-start":
251
+ classes.push("justify-start");
252
+ break;
253
+ case "center":
254
+ classes.push("justify-center");
255
+ break;
256
+ case "flex-end":
257
+ classes.push("justify-end");
258
+ break;
259
+ case "space-between":
260
+ classes.push("justify-between");
261
+ break;
262
+ case "space-around":
263
+ classes.push("justify-around");
264
+ break;
265
+ case "space-evenly":
266
+ classes.push("justify-evenly");
267
+ break;
268
+ }
269
+ }
270
+ if (props.alignVertical) {
271
+ switch (props.alignVertical) {
272
+ case "flex-start":
273
+ classes.push("items-start");
274
+ break;
275
+ case "center":
276
+ classes.push("items-center");
277
+ break;
278
+ case "flex-end":
279
+ classes.push("items-end");
280
+ break;
281
+ case "stretch":
282
+ classes.push("items-stretch");
283
+ break;
284
+ case "baseline":
285
+ classes.push("items-baseline");
286
+ break;
287
+ }
288
+ }
289
+ if (props.columnGap || props.rowGap) {
290
+ if (props.columnGap && props.rowGap) {
291
+ if (props.columnGap === props.rowGap) {
292
+ classes.push(`gap-${props.columnGap}`);
293
+ } else {
294
+ classes.push(`gap-x-${props.columnGap}`, `gap-y-${props.rowGap}`);
295
+ }
296
+ } else if (props.columnGap) {
297
+ classes.push(`gap-x-${props.columnGap}`);
298
+ } else if (props.rowGap) {
299
+ classes.push(`gap-y-${props.rowGap}`);
300
+ }
301
+ }
302
+ return classes.join(" ");
303
+ }
304
+ convertFlexgridToStyles(props) {
305
+ const styles = [];
306
+ if (props.height && props.height !== "auto") {
307
+ styles.push(`height: '${props.height}'`);
308
+ }
309
+ if (props.width && props.width !== "auto") {
310
+ styles.push(`width: '${props.width}'`);
311
+ }
312
+ return styles.length > 0 ? `{ ${styles.join(", ")} }` : null;
313
+ }
314
+ convertFlexgridItemToClasses(props) {
315
+ const classes = ["box-border", "max-w-full"];
316
+ if (!props.span) {
317
+ classes.push("basis-auto");
318
+ return classes.join(" ");
319
+ }
320
+ if (typeof props.span === "string" && props.span === "auto") {
321
+ classes.push("basis-auto", "grow");
322
+ return classes.join(" ");
323
+ }
324
+ if (typeof props.span === "number") {
325
+ const basisClass = this.getBasisClass(props.span);
326
+ classes.push(basisClass);
327
+ return classes.join(" ");
328
+ }
329
+ if (typeof props.span === "object") {
330
+ const responsiveClasses = this.convertResponsiveSpan(
331
+ props.span
332
+ );
333
+ classes.push(...responsiveClasses);
334
+ return classes.join(" ");
335
+ }
336
+ this.warnings.push(`Unsupported span type: ${typeof props.span}`);
337
+ classes.push("basis-auto");
338
+ return classes.join(" ");
339
+ }
340
+ getBasisClass(span) {
341
+ if (span === 12) {
342
+ return "basis-full";
343
+ }
344
+ if (span >= 1 && span <= 11) {
345
+ return `basis-${span}/12`;
346
+ }
347
+ this.warnings.push(
348
+ `Invalid span value: ${span}. Using basis-auto instead.`
349
+ );
350
+ return "basis-auto";
351
+ }
352
+ convertResponsiveSpan(spanObj) {
353
+ const classes = [];
354
+ const validBreakpoints = ["fallback", "sm", "md", "lg", "xl", "2xl"];
355
+ const objKeys = Object.keys(spanObj);
356
+ const isSimpleResponsiveObject = objKeys.every(
357
+ (key) => validBreakpoints.includes(key) && typeof spanObj[key] === "number" && Number.isInteger(spanObj[key]) && spanObj[key] >= 1 && spanObj[key] <= 12
358
+ );
359
+ if (!isSimpleResponsiveObject) {
360
+ this.warnings.push(
361
+ "Complex responsive span object detected - preserving original FlexgridItem"
362
+ );
363
+ return ["basis-auto"];
364
+ }
365
+ if (spanObj.fallback) {
366
+ classes.push(this.getBasisClass(spanObj.fallback));
367
+ }
368
+ const breakpoints = ["sm", "md", "lg", "xl", "2xl"];
369
+ for (const bp of breakpoints) {
370
+ if (spanObj[bp]) {
371
+ const basisClass = this.getBasisClass(spanObj[bp]);
372
+ if (basisClass === "basis-full") {
373
+ classes.push(`${bp}:basis-full`);
374
+ } else {
375
+ classes.push(`${bp}:${basisClass}`);
376
+ }
377
+ }
378
+ }
379
+ return classes;
380
+ }
381
+ }
382
+ function showHelp() {
383
+ console.info(`
384
+ Flexgrid to Tailwind CSS Converter
385
+
386
+ Converts Flexgrid and FlexgridItem components to Tailwind CSS equivalents.
387
+
388
+ Usage:
389
+ npx @versini/ui-system/flexgrid-to-tw --file <path>
390
+ node ./node_modules/@versini/ui-system/dist/cli/flexgrid-to-tw.js --file <path>
391
+
392
+ Options:
393
+ -f, --file <path> Path to the file to convert (required)
394
+ -h, --help Show this help message
395
+
396
+ Examples:
397
+ npx @versini/ui-system/flexgrid-to-tw --file src/components/MyComponent.tsx
398
+ npx @versini/ui-system/flexgrid-to-tw -f src/pages/HomePage.jsx
399
+
400
+ Supported conversions:
401
+ - Flexgrid props: columnGap, rowGap, direction, alignHorizontal, alignVertical
402
+ - FlexgridItem span: numbers (1-12), "auto", responsive objects
403
+ - Basic className merging
404
+
405
+ Features:
406
+ - ✅ Conservative conversion: Only converts simple, safe cases
407
+ - ✅ Symlink compatible: Works with npm, pnpm, and symlinked installations
408
+ - ✅ Preserves complex scenarios with explanatory comments
409
+ - ✅ Smart import management: Only removes imports when safe to do so
410
+
411
+ Note: Complex prop expressions and custom gap calculations may not be fully converted.
412
+ The tool will report what was converted and what was skipped.
413
+ `);
414
+ }
415
+ function main() {
416
+ const args = process.argv.slice(2);
417
+ if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
418
+ showHelp();
419
+ return;
420
+ }
421
+ let filePath = "";
422
+ for (let i = 0; i < args.length; i++) {
423
+ if (args[i] === "-f" || args[i] === "--file") {
424
+ filePath = args[i + 1];
425
+ break;
426
+ }
427
+ }
428
+ if (!filePath) {
429
+ console.error("Error: --file parameter is required");
430
+ showHelp();
431
+ process.exit(1);
432
+ }
433
+ if (!fs.existsSync(filePath)) {
434
+ console.error(`Error: File not found: ${filePath}`);
435
+ process.exit(1);
436
+ }
437
+ const ext = path.extname(filePath).toLowerCase();
438
+ if (![".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
439
+ console.error(`Error: Unsupported file type: ${ext}`);
440
+ console.error("Supported types: .ts, .tsx, .js, .jsx");
441
+ process.exit(1);
442
+ }
443
+ try {
444
+ const content = fs.readFileSync(filePath, "utf-8");
445
+ const converter = new FlexgridConverter();
446
+ const convertedContent = converter.convert(content);
447
+ const result = converter.getResult();
448
+ fs.writeFileSync(filePath, convertedContent, "utf-8");
449
+ console.info(`✅ Conversion complete for: ${filePath}`);
450
+ console.info(`📊 Converted: ${result.convertedCount} components`);
451
+ if (result.skippedCount > 0) {
452
+ console.info(`âš ī¸ Skipped: ${result.skippedCount} components`);
453
+ }
454
+ if (result.warnings.length > 0) {
455
+ console.info(`
456
+ âš ī¸ Warnings:`);
457
+ result.warnings.forEach((warning) => console.info(` ${warning}`));
458
+ }
459
+ if (result.errors.length > 0) {
460
+ console.info(`
461
+ ❌ Errors:`);
462
+ result.errors.forEach((error) => console.info(` ${error}`));
463
+ }
464
+ if (result.convertedCount === 0 && result.skippedCount === 0) {
465
+ console.info(`â„šī¸ No Flexgrid components found in the file.`);
466
+ } else {
467
+ console.info(
468
+ `
469
+ 💡 Don't forget to remove any unused @versini/ui-system imports if no other components are used.`
470
+ );
471
+ }
472
+ } catch (error) {
473
+ console.error(`Error processing file: ${error}`);
474
+ process.exit(1);
475
+ }
476
+ }
477
+ function isMainModule() {
478
+ try {
479
+ const currentModulePath = new URL(import.meta.url).pathname;
480
+ const currentModuleRealPath = fs.realpathSync(currentModulePath);
481
+ const executedScriptRealPath = fs.realpathSync(process.argv[1]);
482
+ return currentModuleRealPath === executedScriptRealPath;
483
+ } catch (_error) {
484
+ return import.meta.url === `file://${process.argv[1]}`;
485
+ }
486
+ }
487
+ if (isMainModule()) {
488
+ main();
489
+ }
490
+ export {
491
+ FlexgridConverter,
492
+ isMainModule,
493
+ main,
494
+ showHelp
495
+ };
@@ -1,22 +1,21 @@
1
- import { jsx as m } from "react/jsx-runtime";
2
- import u from "clsx";
3
- import { useRef as E, useEffect as l } from "react";
4
- import { THEMEPROVIDER_CLASSNAME as y } from "../../chunks/constants.Gcd2KXZh.js";
5
- const P = ({
1
+ import { jsx as i } from "react/jsx-runtime";
2
+ import m from "clsx";
3
+ import { useRef as u, useEffect as E } from "react";
4
+ import { THEMEPROVIDER_CLASSNAME as a } from "../../chunks/constants.Gcd2KXZh.js";
5
+ const v = ({
6
6
  customTheme: r,
7
- children: o,
7
+ children: t,
8
8
  global: e,
9
- className: f
9
+ className: s
10
10
  }) => {
11
- const t = E(null), c = u(y, "contents", f);
12
- return l(() => {
13
- var s;
14
- const n = e ? document.documentElement.style : (s = t == null ? void 0 : t.current) == null ? void 0 : s.style;
11
+ const o = u(null), f = m(a, "contents", s);
12
+ return E(() => {
13
+ const n = e ? document.documentElement.style : o?.current?.style;
15
14
  if (!(!n || !r))
16
- for (const [i, p] of Object.entries(r))
17
- n.setProperty(i, p);
18
- }, [r, e]), e && !r ? o : /* @__PURE__ */ m("div", { ref: t, className: c, children: o });
15
+ for (const [p, c] of Object.entries(r))
16
+ n.setProperty(p, c);
17
+ }, [r, e]), e && !r ? t : /* @__PURE__ */ i("div", { ref: o, className: f, children: t });
19
18
  };
20
19
  export {
21
- P as ThemeProvider
20
+ v as ThemeProvider
22
21
  };
package/dist/index.js CHANGED
@@ -2,13 +2,13 @@ import { Flexgrid as i } from "./components/Flexgrid/Flexgrid.js";
2
2
  import { FlexgridItem as t } from "./components/Flexgrid/FlexgridItem.js";
3
3
  import { ThemeProvider as I } from "./components/ThemeProvider/ThemeProvider.js";
4
4
  /*!
5
- @versini/ui-system v4.0.5
5
+ @versini/ui-system v4.1.0
6
6
  Š 2025 gizmette.com
7
7
  */
8
8
  try {
9
9
  window.__VERSINI_UI_SYSTEM__ || (window.__VERSINI_UI_SYSTEM__ = {
10
- version: "4.0.5",
11
- buildTime: "06/28/2025 08:58 PM EDT",
10
+ version: "4.1.0",
11
+ buildTime: "07/20/2025 01:16 PM EDT",
12
12
  homepage: "https://github.com/aversini/ui-components",
13
13
  license: "MIT"
14
14
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-system",
3
- "version": "4.0.5",
3
+ "version": "4.1.0",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -14,19 +14,24 @@
14
14
  "type": "module",
15
15
  "main": "dist/index.js",
16
16
  "types": "dist/index.d.ts",
17
+ "bin": {
18
+ "flexgrid-to-tw": "dist/cli/flexgrid-to-tw.js"
19
+ },
17
20
  "files": [
18
21
  "dist"
19
22
  ],
20
23
  "scripts": {
21
24
  "build:check": "tsc",
25
+ "build:cli": "vite build --config vite.cli.config.ts",
22
26
  "build:js": "vite build",
23
27
  "build:types": "tsup",
24
- "build": "npm-run-all --serial clean build:check build:js build:types",
28
+ "build": "npm-run-all --serial clean build:check build:js build:cli build:types",
25
29
  "clean": "rimraf dist tmp",
26
30
  "dev:js": "vite build --watch --mode development",
27
31
  "dev:types": "tsup --watch src",
28
32
  "dev": "npm-run-all clean --parallel dev:js dev:types",
29
33
  "lint": "biome lint src",
34
+ "lint:fix": "biome check src --write --no-errors-on-unmatched",
30
35
  "prettier": "biome check --write --no-errors-on-unmatched",
31
36
  "start": "static-server dist --port 5173",
32
37
  "test:coverage:ui": "vitest --coverage --ui",
@@ -39,6 +44,7 @@
39
44
  "react-dom": "^18.3.1 || ^19.0.0"
40
45
  },
41
46
  "devDependencies": {
47
+ "@testing-library/jest-dom": "6.6.3",
42
48
  "@versini/ui-types": "5.0.5"
43
49
  },
44
50
  "dependencies": {
@@ -48,5 +54,5 @@
48
54
  "sideEffects": [
49
55
  "**/*.css"
50
56
  ],
51
- "gitHead": "1b9a792d10e1f67fc7af7cfdff718ef1e1c78633"
57
+ "gitHead": "7a28fe534f6a212bbd15412a262cd79975b3537a"
52
58
  }