@versini/ui-system 4.0.6 → 4.1.1
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 +279 -1
- package/dist/cli/flexgrid-to-tw.js +495 -0
- package/dist/components/ThemeProvider/ThemeProvider.js +14 -15
- package/dist/index.js +3 -3
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -1,3 +1,281 @@
|
|
|
1
1
|
# @versini/ui-system
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](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
|
|
2
|
-
import
|
|
3
|
-
import { useRef as
|
|
4
|
-
import { THEMEPROVIDER_CLASSNAME as
|
|
5
|
-
const
|
|
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:
|
|
7
|
+
children: t,
|
|
8
8
|
global: e,
|
|
9
|
-
className:
|
|
9
|
+
className: s
|
|
10
10
|
}) => {
|
|
11
|
-
const
|
|
12
|
-
return
|
|
13
|
-
|
|
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 [
|
|
17
|
-
n.setProperty(
|
|
18
|
-
}, [r, e]), e && !r ?
|
|
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
|
-
|
|
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.
|
|
5
|
+
@versini/ui-system v4.1.1
|
|
6
6
|
© 2025 gizmette.com
|
|
7
7
|
*/
|
|
8
8
|
try {
|
|
9
9
|
window.__VERSINI_UI_SYSTEM__ || (window.__VERSINI_UI_SYSTEM__ = {
|
|
10
|
-
version: "4.
|
|
11
|
-
buildTime: "07/
|
|
10
|
+
version: "4.1.1",
|
|
11
|
+
buildTime: "08/07/2025 04:12 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.
|
|
3
|
+
"version": "4.1.1",
|
|
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,8 +44,8 @@
|
|
|
39
44
|
"react-dom": "^18.3.1 || ^19.0.0"
|
|
40
45
|
},
|
|
41
46
|
"devDependencies": {
|
|
42
|
-
"@testing-library/jest-dom": "6.6.
|
|
43
|
-
"@versini/ui-types": "5.0.
|
|
47
|
+
"@testing-library/jest-dom": "6.6.4",
|
|
48
|
+
"@versini/ui-types": "5.0.6"
|
|
44
49
|
},
|
|
45
50
|
"dependencies": {
|
|
46
51
|
"clsx": "2.1.1",
|
|
@@ -49,5 +54,5 @@
|
|
|
49
54
|
"sideEffects": [
|
|
50
55
|
"**/*.css"
|
|
51
56
|
],
|
|
52
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "42daab1fb88c366495abbc1db78a9987de05e59b"
|
|
53
58
|
}
|