@almadar/ui 5.128.0 ā 5.130.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/dist/avl/index.cjs +349 -11
- package/dist/avl/index.js +349 -11
- package/dist/components/index.cjs +352 -11
- package/dist/components/index.d.cts +119 -5
- package/dist/components/index.d.ts +119 -5
- package/dist/components/index.js +353 -12
- package/dist/providers/index.cjs +349 -11
- package/dist/providers/index.js +349 -11
- package/dist/runtime/index.cjs +349 -11
- package/dist/runtime/index.js +349 -11
- package/package.json +6 -5
- package/scripts/audit-tailwind-safelist.ts +237 -0
- package/scripts/export-static.js +61 -0
- package/scripts/generate-design-system.ts +344 -0
- package/scripts/generate-theme-from-schema.ts +480 -0
- package/scripts/generate.ts +835 -0
- package/scripts/strip-base-tokens-from-themes.mjs +179 -0
- package/scripts/suggest-components.ts +508 -0
- package/scripts/theme-contrast-audit.mjs +132 -0
- package/scripts/types.ts +282 -0
- package/themes/_contract.md +1 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Remove typography + density tokens from general-purpose themes so they
|
|
4
|
+
* inherit the shared `_base.css` :root defaults. Personality themes
|
|
5
|
+
* (terminal, wireframe, neon, kiosk, gazette, minimalist, trait-wars)
|
|
6
|
+
* and atelier itself are untouched.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node scripts/strip-base-tokens-from-themes.mjs
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { resolve, dirname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const themesDir = resolve(__dirname, '..', 'themes');
|
|
16
|
+
|
|
17
|
+
const GENERAL_THEMES = [
|
|
18
|
+
'almadar.css',
|
|
19
|
+
'almadar-website.css',
|
|
20
|
+
'prism.css',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// CSS variable names to strip from each [data-theme="..."] block.
|
|
24
|
+
const TYPOGRAPHY_VARS = new Set([
|
|
25
|
+
'--font-family',
|
|
26
|
+
'--font-family-display',
|
|
27
|
+
'--font-family-body',
|
|
28
|
+
'--font-family-mono',
|
|
29
|
+
'--font-weight-normal',
|
|
30
|
+
'--font-weight-medium',
|
|
31
|
+
'--font-weight-bold',
|
|
32
|
+
'--letter-spacing',
|
|
33
|
+
'--line-height',
|
|
34
|
+
'--text-xs', '--leading-xs',
|
|
35
|
+
'--text-sm', '--leading-sm',
|
|
36
|
+
'--text-base', '--leading-base',
|
|
37
|
+
'--text-lg', '--leading-lg',
|
|
38
|
+
'--text-xl', '--leading-xl',
|
|
39
|
+
'--text-2xl', '--leading-2xl',
|
|
40
|
+
'--text-3xl', '--leading-3xl',
|
|
41
|
+
'--text-4xl', '--leading-4xl',
|
|
42
|
+
'--text-display-1', '--leading-display-1',
|
|
43
|
+
'--text-display-2', '--leading-display-2',
|
|
44
|
+
'--intent-heading-major-size',
|
|
45
|
+
'--intent-heading-major-weight',
|
|
46
|
+
'--intent-heading-major-leading',
|
|
47
|
+
'--intent-heading-minor-size',
|
|
48
|
+
'--intent-heading-minor-weight',
|
|
49
|
+
'--intent-heading-minor-leading',
|
|
50
|
+
'--intent-body-emphasis-size',
|
|
51
|
+
'--intent-body-emphasis-weight',
|
|
52
|
+
'--intent-body-emphasis-leading',
|
|
53
|
+
'--intent-body-default-size',
|
|
54
|
+
'--intent-body-default-weight',
|
|
55
|
+
'--intent-body-default-leading',
|
|
56
|
+
'--intent-body-quiet-size',
|
|
57
|
+
'--intent-body-quiet-weight',
|
|
58
|
+
'--intent-body-quiet-leading',
|
|
59
|
+
'--intent-caption-size',
|
|
60
|
+
'--intent-caption-weight',
|
|
61
|
+
'--intent-caption-leading',
|
|
62
|
+
'--intent-numeric-size',
|
|
63
|
+
'--intent-numeric-weight',
|
|
64
|
+
'--intent-numeric-leading',
|
|
65
|
+
'--intent-numeric-family',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const DENSITY_VARS = new Set([
|
|
69
|
+
'--space-0', '--space-1', '--space-2', '--space-3', '--space-4',
|
|
70
|
+
'--space-5', '--space-6', '--space-7', '--space-8', '--space-9',
|
|
71
|
+
'--space-10', '--space-11', '--space-12',
|
|
72
|
+
'--button-height-sm', '--button-height-md', '--button-height-lg',
|
|
73
|
+
'--input-height-sm', '--input-height-md', '--input-height-lg',
|
|
74
|
+
'--row-height-compact', '--row-height-normal', '--row-height-spacious',
|
|
75
|
+
'--card-padding-sm', '--card-padding-md', '--card-padding-lg',
|
|
76
|
+
'--dialog-padding',
|
|
77
|
+
'--section-gap',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const SHARED_VARS = new Set([...TYPOGRAPHY_VARS, ...DENSITY_VARS]);
|
|
81
|
+
|
|
82
|
+
// Block-comment lines that introduce sections we're removing whole. When
|
|
83
|
+
// every variable in the section is stripped, the comment line is too.
|
|
84
|
+
const STRIPPABLE_COMMENTS = [
|
|
85
|
+
/^\s*\/\*\s*Typography[^*]*\*\/\s*$/,
|
|
86
|
+
/^\s*\/\*\s*Type scale[^*]*\*\/\s*$/,
|
|
87
|
+
/^\s*\/\*\s*Intent mapping[^*]*\*\/\s*$/,
|
|
88
|
+
/^\s*\/\*\s*Density[^*]*\*\/\s*$/,
|
|
89
|
+
/^\s*\/\*\s*Density axis[^*]*\*\/\s*$/,
|
|
90
|
+
/^\s*\/\*\s*Type axis[^*]*\*\/\s*$/,
|
|
91
|
+
/^\s*\/\*\s*Layer 1[^*]*\*\/\s*$/,
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
function stripBlock(lines) {
|
|
95
|
+
// Walk the file; for any line that declares one of the shared CSS
|
|
96
|
+
// variables, drop it. CSS variable values can span multiple lines
|
|
97
|
+
// (e.g. multi-fallback font-family), so once we start dropping a
|
|
98
|
+
// declaration, keep dropping subsequent lines until we hit a
|
|
99
|
+
// semicolon-terminated line ā that's the end of the declaration.
|
|
100
|
+
// Drop comments whose adjacent (next-non-blank) line is also being
|
|
101
|
+
// dropped, so we don't leave orphan headers.
|
|
102
|
+
const keep = new Array(lines.length).fill(true);
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < lines.length; i++) {
|
|
105
|
+
const line = lines[i];
|
|
106
|
+
const match = line.match(/^\s*(--[a-z0-9-]+)\s*:/);
|
|
107
|
+
if (match && SHARED_VARS.has(match[1])) {
|
|
108
|
+
keep[i] = false;
|
|
109
|
+
// If the declaration didn't end on this line (no `;`), continue
|
|
110
|
+
// dropping until we see one. Bail if we hit a `}` to avoid
|
|
111
|
+
// eating into the next block on malformed CSS.
|
|
112
|
+
if (!/;\s*$/.test(line)) {
|
|
113
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
114
|
+
if (/^\s*\}/.test(lines[j])) break;
|
|
115
|
+
keep[j] = false;
|
|
116
|
+
if (/;\s*$/.test(lines[j])) break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Drop section-header comments whose body is fully stripped: look
|
|
123
|
+
// forward through blank lines + variable declarations; if the next
|
|
124
|
+
// declaration we see is a kept var, the header stays; if it's a
|
|
125
|
+
// dropped var (or we hit the end of the block), the header goes.
|
|
126
|
+
for (let i = 0; i < lines.length; i++) {
|
|
127
|
+
if (!keep[i]) continue;
|
|
128
|
+
if (!STRIPPABLE_COMMENTS.some(rx => rx.test(lines[i]))) continue;
|
|
129
|
+
let next = i + 1;
|
|
130
|
+
while (next < lines.length) {
|
|
131
|
+
const l = lines[next];
|
|
132
|
+
if (/^\s*$/.test(l)) { next++; continue; }
|
|
133
|
+
if (/^\s*\/\*/.test(l)) break;
|
|
134
|
+
if (/^\s*\}/.test(l)) break;
|
|
135
|
+
const m = l.match(/^\s*(--[a-z0-9-]+)\s*:/);
|
|
136
|
+
if (m) {
|
|
137
|
+
if (!keep[next]) {
|
|
138
|
+
keep[i] = false;
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
next++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Collapse adjacent blank lines (>2 in a row) created by the strip.
|
|
147
|
+
const out = [];
|
|
148
|
+
let blankRun = 0;
|
|
149
|
+
for (let i = 0; i < lines.length; i++) {
|
|
150
|
+
if (!keep[i]) continue;
|
|
151
|
+
if (/^\s*$/.test(lines[i])) {
|
|
152
|
+
blankRun++;
|
|
153
|
+
if (blankRun > 1) continue;
|
|
154
|
+
} else {
|
|
155
|
+
blankRun = 0;
|
|
156
|
+
}
|
|
157
|
+
out.push(lines[i]);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let totalRemoved = 0;
|
|
163
|
+
for (const name of GENERAL_THEMES) {
|
|
164
|
+
const path = resolve(themesDir, name);
|
|
165
|
+
const before = readFileSync(path, 'utf8');
|
|
166
|
+
const lines = before.split('\n');
|
|
167
|
+
const after = stripBlock(lines).join('\n');
|
|
168
|
+
const beforeLines = lines.length;
|
|
169
|
+
const afterLines = after.split('\n').length;
|
|
170
|
+
const delta = beforeLines - afterLines;
|
|
171
|
+
if (delta === 0) {
|
|
172
|
+
console.log(` ${name}: no change`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
writeFileSync(path, after);
|
|
176
|
+
totalRemoved += delta;
|
|
177
|
+
console.log(` ${name}: ${beforeLines} -> ${afterLines} (-${delta})`);
|
|
178
|
+
}
|
|
179
|
+
console.log(`\nTotal lines removed: ${totalRemoved}`);
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Suggester
|
|
3
|
+
*
|
|
4
|
+
* Analyzes an .orb schema and suggests domain-specific components
|
|
5
|
+
* that should be added to the design system for this client.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
interface ComponentSuggestion {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
category: 'atoms' | 'molecules' | 'organisms' | 'templates';
|
|
15
|
+
props: Array<{ name: string; type: string; description: string }>;
|
|
16
|
+
reasoning: string;
|
|
17
|
+
priority: 'high' | 'medium' | 'low';
|
|
18
|
+
codeSketch: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DomainComponents {
|
|
22
|
+
domain: string;
|
|
23
|
+
keywords: string[];
|
|
24
|
+
components: ComponentSuggestion[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Domain-specific component suggestions
|
|
28
|
+
const DOMAIN_COMPONENTS: DomainComponents[] = [
|
|
29
|
+
{
|
|
30
|
+
domain: 'garden',
|
|
31
|
+
keywords: ['plant', 'seed', 'garden', 'grow', 'harvest', 'cultivate', 'soil', 'water', 'farm', 'crop', 'tree'],
|
|
32
|
+
components: [
|
|
33
|
+
{
|
|
34
|
+
name: 'GardenView',
|
|
35
|
+
description: 'A visual garden layout showing plants in a grid or organic arrangement',
|
|
36
|
+
category: 'organisms',
|
|
37
|
+
props: [
|
|
38
|
+
{ name: 'plants', type: 'Plant[]', description: 'Array of plants to display' },
|
|
39
|
+
{ name: 'layout', type: "'grid' | 'organic'", description: 'Layout style' },
|
|
40
|
+
{ name: 'onPlantClick', type: '(plant: Plant) => void', description: 'Plant selection handler' },
|
|
41
|
+
],
|
|
42
|
+
reasoning: 'Core visualization for garden/agriculture domains to show plant collections',
|
|
43
|
+
priority: 'high',
|
|
44
|
+
codeSketch: `
|
|
45
|
+
export function GardenView({ plants, layout = 'grid', onPlantClick }: GardenViewProps) {
|
|
46
|
+
return (
|
|
47
|
+
<div className={cn('garden-view', layout === 'grid' ? 'grid grid-cols-4 gap-4' : 'flex flex-wrap')}>
|
|
48
|
+
{plants.map(plant => (
|
|
49
|
+
<PlantCard key={plant.id} plant={plant} onClick={() => onPlantClick?.(plant)} />
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}`,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'PlantCard',
|
|
57
|
+
description: 'Card displaying a single plant with growth status and care indicators',
|
|
58
|
+
category: 'molecules',
|
|
59
|
+
props: [
|
|
60
|
+
{ name: 'plant', type: 'Plant', description: 'Plant data to display' },
|
|
61
|
+
{ name: 'showCareIndicators', type: 'boolean', description: 'Show water/sun needs' },
|
|
62
|
+
{ name: 'onClick', type: '() => void', description: 'Click handler' },
|
|
63
|
+
],
|
|
64
|
+
reasoning: 'Essential card component for displaying individual plant information',
|
|
65
|
+
priority: 'high',
|
|
66
|
+
codeSketch: `
|
|
67
|
+
export function PlantCard({ plant, showCareIndicators = true, onClick }: PlantCardProps) {
|
|
68
|
+
return (
|
|
69
|
+
<Card className="plant-card garden-card cursor-pointer" onClick={onClick}>
|
|
70
|
+
<div className="plant-icon text-4xl">{getPlantEmoji(plant.type)}</div>
|
|
71
|
+
<CardTitle>{plant.name}</CardTitle>
|
|
72
|
+
{showCareIndicators && (
|
|
73
|
+
<div className="flex gap-2 mt-2">
|
|
74
|
+
<CareIndicator type="water" level={plant.waterNeeds} />
|
|
75
|
+
<CareIndicator type="sun" level={plant.sunNeeds} />
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
<GrowthMeter progress={plant.growthProgress} />
|
|
79
|
+
</Card>
|
|
80
|
+
);
|
|
81
|
+
}`,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'GrowthMeter',
|
|
85
|
+
description: 'Progress indicator showing plant growth stage',
|
|
86
|
+
category: 'atoms',
|
|
87
|
+
props: [
|
|
88
|
+
{ name: 'progress', type: 'number', description: 'Growth progress 0-100' },
|
|
89
|
+
{ name: 'stages', type: 'string[]', description: 'Growth stage labels' },
|
|
90
|
+
],
|
|
91
|
+
reasoning: 'Visual feedback for growth/progress - core to garden metaphor',
|
|
92
|
+
priority: 'high',
|
|
93
|
+
codeSketch: `
|
|
94
|
+
export function GrowthMeter({ progress, stages = ['Seed', 'Sprout', 'Growing', 'Mature'] }: GrowthMeterProps) {
|
|
95
|
+
const stageIndex = Math.floor(progress / (100 / stages.length));
|
|
96
|
+
return (
|
|
97
|
+
<div className="growth-meter">
|
|
98
|
+
<ProgressBar value={progress} className="bg-garden-leaf-color" />
|
|
99
|
+
<span className="text-sm text-muted-foreground">{stages[stageIndex]}</span>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}`,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'TrustMeter',
|
|
106
|
+
description: 'Visual indicator for trust/relationship level between entities',
|
|
107
|
+
category: 'atoms',
|
|
108
|
+
props: [
|
|
109
|
+
{ name: 'level', type: "'low' | 'medium' | 'high' | 'verified'", description: 'Trust level' },
|
|
110
|
+
{ name: 'showLabel', type: 'boolean', description: 'Show text label' },
|
|
111
|
+
],
|
|
112
|
+
reasoning: 'For Winning-11 RFP: visualize trust between farmers and buyers',
|
|
113
|
+
priority: 'medium',
|
|
114
|
+
codeSketch: `
|
|
115
|
+
export function TrustMeter({ level, showLabel = true }: TrustMeterProps) {
|
|
116
|
+
return (
|
|
117
|
+
<div className={cn('trust-badge', \`trust-badge--\${level}\`)}>
|
|
118
|
+
<TrustIcon level={level} />
|
|
119
|
+
{showLabel && <span>{level}</span>}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}`,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'CareIndicator',
|
|
126
|
+
description: 'Small icon indicator for plant care needs (water, sun, nutrients)',
|
|
127
|
+
category: 'atoms',
|
|
128
|
+
props: [
|
|
129
|
+
{ name: 'type', type: "'water' | 'sun' | 'nutrients'", description: 'Care type' },
|
|
130
|
+
{ name: 'level', type: "'low' | 'medium' | 'high'", description: 'Need level' },
|
|
131
|
+
],
|
|
132
|
+
reasoning: 'Quick visual cues for plant care requirements',
|
|
133
|
+
priority: 'medium',
|
|
134
|
+
codeSketch: `
|
|
135
|
+
export function CareIndicator({ type, level }: CareIndicatorProps) {
|
|
136
|
+
const icons = { water: 'š§', sun: 'āļø', nutrients: 'š±' };
|
|
137
|
+
const colors = { low: 'text-green-500', medium: 'text-yellow-500', high: 'text-red-500' };
|
|
138
|
+
return <span className={cn('care-indicator', colors[level])}>{icons[type]}</span>;
|
|
139
|
+
}`,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'SeasonalCalendar',
|
|
143
|
+
description: 'Calendar view showing planting/harvest seasons',
|
|
144
|
+
category: 'organisms',
|
|
145
|
+
props: [
|
|
146
|
+
{ name: 'crops', type: 'Crop[]', description: 'Crops with their seasons' },
|
|
147
|
+
{ name: 'currentMonth', type: 'number', description: 'Current month (0-11)' },
|
|
148
|
+
],
|
|
149
|
+
reasoning: 'Planning component for agricultural scheduling',
|
|
150
|
+
priority: 'low',
|
|
151
|
+
codeSketch: `
|
|
152
|
+
export function SeasonalCalendar({ crops, currentMonth }: SeasonalCalendarProps) {
|
|
153
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
154
|
+
return (
|
|
155
|
+
<div className="seasonal-calendar">
|
|
156
|
+
<div className="grid grid-cols-12 gap-1">
|
|
157
|
+
{months.map((month, i) => (
|
|
158
|
+
<div key={month} className={cn('month', i === currentMonth && 'current')}>
|
|
159
|
+
{month}
|
|
160
|
+
</div>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
{crops.map(crop => <CropSeasonRow key={crop.id} crop={crop} />)}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
domain: 'healthcare',
|
|
172
|
+
keywords: ['patient', 'doctor', 'hospital', 'medical', 'health', 'diagnosis', 'treatment', 'appointment', 'prescription'],
|
|
173
|
+
components: [
|
|
174
|
+
{
|
|
175
|
+
name: 'PatientCard',
|
|
176
|
+
description: 'Card displaying patient information with status indicators',
|
|
177
|
+
category: 'molecules',
|
|
178
|
+
props: [
|
|
179
|
+
{ name: 'patient', type: 'Patient', description: 'Patient data' },
|
|
180
|
+
{ name: 'showVitals', type: 'boolean', description: 'Show vital signs' },
|
|
181
|
+
],
|
|
182
|
+
reasoning: 'Core component for patient-centric healthcare interfaces',
|
|
183
|
+
priority: 'high',
|
|
184
|
+
codeSketch: `
|
|
185
|
+
export function PatientCard({ patient, showVitals }: PatientCardProps) {
|
|
186
|
+
return (
|
|
187
|
+
<Card className="patient-card">
|
|
188
|
+
<Avatar name={patient.name} />
|
|
189
|
+
<div className="patient-info">
|
|
190
|
+
<h3>{patient.name}</h3>
|
|
191
|
+
<Badge variant={patient.status}>{patient.status}</Badge>
|
|
192
|
+
</div>
|
|
193
|
+
{showVitals && <VitalsDisplay vitals={patient.vitals} />}
|
|
194
|
+
</Card>
|
|
195
|
+
);
|
|
196
|
+
}`,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'VitalsDisplay',
|
|
200
|
+
description: 'Compact display of patient vital signs',
|
|
201
|
+
category: 'molecules',
|
|
202
|
+
props: [
|
|
203
|
+
{ name: 'vitals', type: 'Vitals', description: 'Vital signs data' },
|
|
204
|
+
{ name: 'compact', type: 'boolean', description: 'Compact view mode' },
|
|
205
|
+
],
|
|
206
|
+
reasoning: 'Critical for at-a-glance patient monitoring',
|
|
207
|
+
priority: 'high',
|
|
208
|
+
codeSketch: `
|
|
209
|
+
export function VitalsDisplay({ vitals, compact }: VitalsDisplayProps) {
|
|
210
|
+
return (
|
|
211
|
+
<div className={cn('vitals-display', compact && 'compact')}>
|
|
212
|
+
<VitalItem label="HR" value={vitals.heartRate} unit="bpm" />
|
|
213
|
+
<VitalItem label="BP" value={vitals.bloodPressure} />
|
|
214
|
+
<VitalItem label="Temp" value={vitals.temperature} unit="°F" />
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}`,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'AppointmentSlot',
|
|
221
|
+
description: 'Time slot component for scheduling appointments',
|
|
222
|
+
category: 'atoms',
|
|
223
|
+
props: [
|
|
224
|
+
{ name: 'time', type: 'Date', description: 'Appointment time' },
|
|
225
|
+
{ name: 'available', type: 'boolean', description: 'Slot availability' },
|
|
226
|
+
{ name: 'onSelect', type: '() => void', description: 'Selection handler' },
|
|
227
|
+
],
|
|
228
|
+
reasoning: 'Essential for appointment scheduling interfaces',
|
|
229
|
+
priority: 'medium',
|
|
230
|
+
codeSketch: `
|
|
231
|
+
export function AppointmentSlot({ time, available, onSelect }: AppointmentSlotProps) {
|
|
232
|
+
return (
|
|
233
|
+
<button
|
|
234
|
+
className={cn('appointment-slot', available ? 'available' : 'unavailable')}
|
|
235
|
+
disabled={!available}
|
|
236
|
+
onClick={onSelect}
|
|
237
|
+
>
|
|
238
|
+
{formatTime(time)}
|
|
239
|
+
</button>
|
|
240
|
+
);
|
|
241
|
+
}`,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
domain: 'ecommerce',
|
|
247
|
+
keywords: ['product', 'cart', 'order', 'checkout', 'inventory', 'shop', 'store', 'price', 'sku'],
|
|
248
|
+
components: [
|
|
249
|
+
{
|
|
250
|
+
name: 'ProductCard',
|
|
251
|
+
description: 'Card displaying product with image, price, and add-to-cart action',
|
|
252
|
+
category: 'molecules',
|
|
253
|
+
props: [
|
|
254
|
+
{ name: 'product', type: 'Product', description: 'Product data' },
|
|
255
|
+
{ name: 'onAddToCart', type: '() => void', description: 'Add to cart handler' },
|
|
256
|
+
],
|
|
257
|
+
reasoning: 'Core component for product listings',
|
|
258
|
+
priority: 'high',
|
|
259
|
+
codeSketch: `
|
|
260
|
+
export function ProductCard({ product, onAddToCart }: ProductCardProps) {
|
|
261
|
+
return (
|
|
262
|
+
<Card className="product-card">
|
|
263
|
+
<img src={product.image} alt={product.name} />
|
|
264
|
+
<CardTitle>{product.name}</CardTitle>
|
|
265
|
+
<div className="price">{formatCurrency(product.price)}</div>
|
|
266
|
+
<Button onClick={onAddToCart}>Add to Cart</Button>
|
|
267
|
+
</Card>
|
|
268
|
+
);
|
|
269
|
+
}`,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'CartSummary',
|
|
273
|
+
description: 'Compact cart summary with item count and total',
|
|
274
|
+
category: 'molecules',
|
|
275
|
+
props: [
|
|
276
|
+
{ name: 'items', type: 'CartItem[]', description: 'Cart items' },
|
|
277
|
+
{ name: 'onCheckout', type: '() => void', description: 'Checkout handler' },
|
|
278
|
+
],
|
|
279
|
+
reasoning: 'Essential for cart visibility throughout shopping experience',
|
|
280
|
+
priority: 'high',
|
|
281
|
+
codeSketch: `
|
|
282
|
+
export function CartSummary({ items, onCheckout }: CartSummaryProps) {
|
|
283
|
+
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
284
|
+
return (
|
|
285
|
+
<div className="cart-summary">
|
|
286
|
+
<Badge>{items.length} items</Badge>
|
|
287
|
+
<span className="total">{formatCurrency(total)}</span>
|
|
288
|
+
<Button onClick={onCheckout}>Checkout</Button>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}`,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'PriceTag',
|
|
295
|
+
description: 'Stylized price display with optional discount',
|
|
296
|
+
category: 'atoms',
|
|
297
|
+
props: [
|
|
298
|
+
{ name: 'price', type: 'number', description: 'Current price' },
|
|
299
|
+
{ name: 'originalPrice', type: 'number', description: 'Original price (for discount)' },
|
|
300
|
+
{ name: 'currency', type: 'string', description: 'Currency code' },
|
|
301
|
+
],
|
|
302
|
+
reasoning: 'Consistent price formatting across the store',
|
|
303
|
+
priority: 'medium',
|
|
304
|
+
codeSketch: `
|
|
305
|
+
export function PriceTag({ price, originalPrice, currency = 'USD' }: PriceTagProps) {
|
|
306
|
+
const hasDiscount = originalPrice && originalPrice > price;
|
|
307
|
+
return (
|
|
308
|
+
<div className="price-tag">
|
|
309
|
+
{hasDiscount && <span className="original line-through">{formatCurrency(originalPrice, currency)}</span>}
|
|
310
|
+
<span className={cn('current', hasDiscount && 'text-accent')}>{formatCurrency(price, currency)}</span>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
}`,
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
domain: 'game',
|
|
319
|
+
keywords: ['player', 'score', 'level', 'enemy', 'character', 'game', 'sprite', 'health', 'inventory', 'quest'],
|
|
320
|
+
components: [
|
|
321
|
+
{
|
|
322
|
+
name: 'CharacterStats',
|
|
323
|
+
description: 'RPG-style character stats display',
|
|
324
|
+
category: 'molecules',
|
|
325
|
+
props: [
|
|
326
|
+
{ name: 'stats', type: 'CharacterStats', description: 'Character statistics' },
|
|
327
|
+
{ name: 'compact', type: 'boolean', description: 'Compact view' },
|
|
328
|
+
],
|
|
329
|
+
reasoning: 'Core component for character-based games',
|
|
330
|
+
priority: 'high',
|
|
331
|
+
codeSketch: `
|
|
332
|
+
export function CharacterStats({ stats, compact }: CharacterStatsProps) {
|
|
333
|
+
return (
|
|
334
|
+
<div className={cn('character-stats', compact && 'compact')}>
|
|
335
|
+
<StatBar label="HP" value={stats.health} max={stats.maxHealth} color="red" />
|
|
336
|
+
<StatBar label="MP" value={stats.mana} max={stats.maxMana} color="blue" />
|
|
337
|
+
<StatBar label="XP" value={stats.experience} max={stats.nextLevel} color="yellow" />
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
}`,
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'QuestTracker',
|
|
344
|
+
description: 'Panel showing active quests and objectives',
|
|
345
|
+
category: 'organisms',
|
|
346
|
+
props: [
|
|
347
|
+
{ name: 'quests', type: 'Quest[]', description: 'Active quests' },
|
|
348
|
+
{ name: 'onQuestSelect', type: '(quest: Quest) => void', description: 'Quest selection' },
|
|
349
|
+
],
|
|
350
|
+
reasoning: 'Essential for quest/mission-based games',
|
|
351
|
+
priority: 'medium',
|
|
352
|
+
codeSketch: `
|
|
353
|
+
export function QuestTracker({ quests, onQuestSelect }: QuestTrackerProps) {
|
|
354
|
+
return (
|
|
355
|
+
<div className="quest-tracker">
|
|
356
|
+
<h3>Active Quests</h3>
|
|
357
|
+
{quests.map(quest => (
|
|
358
|
+
<QuestItem key={quest.id} quest={quest} onClick={() => onQuestSelect(quest)} />
|
|
359
|
+
))}
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}`,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
interface OrbitalSchema {
|
|
369
|
+
name: string;
|
|
370
|
+
version?: string;
|
|
371
|
+
description?: string;
|
|
372
|
+
orbitals?: Array<{
|
|
373
|
+
name: string;
|
|
374
|
+
entity?: {
|
|
375
|
+
name: string;
|
|
376
|
+
fields?: Array<{ name: string; type: string }>;
|
|
377
|
+
};
|
|
378
|
+
}>;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Detect domains from schema
|
|
383
|
+
*/
|
|
384
|
+
function detectDomains(schema: OrbitalSchema): string[] {
|
|
385
|
+
const detectedDomains: string[] = [];
|
|
386
|
+
|
|
387
|
+
const searchText = [
|
|
388
|
+
schema.name,
|
|
389
|
+
schema.description || '',
|
|
390
|
+
...(schema.orbitals || []).map(o => o.name),
|
|
391
|
+
...(schema.orbitals || []).flatMap(o => o.entity?.fields?.map(f => f.name) || []),
|
|
392
|
+
].join(' ').toLowerCase();
|
|
393
|
+
|
|
394
|
+
for (const domainConfig of DOMAIN_COMPONENTS) {
|
|
395
|
+
for (const keyword of domainConfig.keywords) {
|
|
396
|
+
if (searchText.includes(keyword)) {
|
|
397
|
+
if (!detectedDomains.includes(domainConfig.domain)) {
|
|
398
|
+
detectedDomains.push(domainConfig.domain);
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return detectedDomains;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Suggest components based on schema analysis
|
|
410
|
+
*/
|
|
411
|
+
export function suggestComponents(schemaPath: string): { domains: string[]; suggestions: ComponentSuggestion[] } {
|
|
412
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
413
|
+
const schema: OrbitalSchema = JSON.parse(schemaContent);
|
|
414
|
+
|
|
415
|
+
const domains = detectDomains(schema);
|
|
416
|
+
console.log(`Detected domains: ${domains.join(', ') || 'none'}`);
|
|
417
|
+
|
|
418
|
+
const suggestions: ComponentSuggestion[] = [];
|
|
419
|
+
|
|
420
|
+
for (const domain of domains) {
|
|
421
|
+
const domainConfig = DOMAIN_COMPONENTS.find(d => d.domain === domain);
|
|
422
|
+
if (domainConfig) {
|
|
423
|
+
suggestions.push(...domainConfig.components);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Sort by priority
|
|
428
|
+
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
429
|
+
suggestions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
430
|
+
|
|
431
|
+
return { domains, suggestions };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Generate component stubs from suggestions
|
|
436
|
+
*/
|
|
437
|
+
export function generateComponentStubs(
|
|
438
|
+
suggestions: ComponentSuggestion[],
|
|
439
|
+
outputDir: string
|
|
440
|
+
): void {
|
|
441
|
+
for (const suggestion of suggestions) {
|
|
442
|
+
const categoryDir = path.join(outputDir, suggestion.category);
|
|
443
|
+
if (!fs.existsSync(categoryDir)) {
|
|
444
|
+
fs.mkdirSync(categoryDir, { recursive: true });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const componentFile = path.join(categoryDir, `${suggestion.name}.tsx`);
|
|
448
|
+
|
|
449
|
+
// Generate component stub
|
|
450
|
+
const propsInterface = `interface ${suggestion.name}Props {
|
|
451
|
+
${suggestion.props.map(p => ` /** ${p.description} */\n ${p.name}${p.type.includes('?') ? '' : '?'}: ${p.type};`).join('\n')}
|
|
452
|
+
}`;
|
|
453
|
+
|
|
454
|
+
const content = `/**
|
|
455
|
+
* ${suggestion.name}
|
|
456
|
+
*
|
|
457
|
+
* ${suggestion.description}
|
|
458
|
+
*
|
|
459
|
+
* Category: ${suggestion.category}
|
|
460
|
+
* Priority: ${suggestion.priority}
|
|
461
|
+
*
|
|
462
|
+
* Reasoning: ${suggestion.reasoning}
|
|
463
|
+
*/
|
|
464
|
+
|
|
465
|
+
import React from 'react';
|
|
466
|
+
import { cn } from '../../lib/cn';
|
|
467
|
+
|
|
468
|
+
${propsInterface}
|
|
469
|
+
|
|
470
|
+
export function ${suggestion.name}({ ${suggestion.props.map(p => p.name).join(', ')} }: ${suggestion.name}Props) {
|
|
471
|
+
// TODO: Implement component
|
|
472
|
+
${suggestion.codeSketch}
|
|
473
|
+
}
|
|
474
|
+
`;
|
|
475
|
+
|
|
476
|
+
fs.writeFileSync(componentFile, content);
|
|
477
|
+
console.log(`Generated: ${componentFile}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// CLI usage
|
|
482
|
+
if (require.main === module) {
|
|
483
|
+
const args = process.argv.slice(2);
|
|
484
|
+
if (args.length < 1) {
|
|
485
|
+
console.error('Usage: npx tsx suggest-components.ts <schema.orb> [--generate <output-dir>]');
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const schemaPath = args[0];
|
|
490
|
+
const generateIndex = args.indexOf('--generate');
|
|
491
|
+
|
|
492
|
+
const { domains, suggestions } = suggestComponents(schemaPath);
|
|
493
|
+
|
|
494
|
+
console.log('\nš¦ Component Suggestions:\n');
|
|
495
|
+
|
|
496
|
+
for (const suggestion of suggestions) {
|
|
497
|
+
console.log(`[${suggestion.priority.toUpperCase()}] ${suggestion.name} (${suggestion.category})`);
|
|
498
|
+
console.log(` ${suggestion.description}`);
|
|
499
|
+
console.log(` Reasoning: ${suggestion.reasoning}`);
|
|
500
|
+
console.log('');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (generateIndex !== -1 && args[generateIndex + 1]) {
|
|
504
|
+
const outputDir = args[generateIndex + 1];
|
|
505
|
+
console.log(`\nš§ Generating component stubs to: ${outputDir}\n`);
|
|
506
|
+
generateComponentStubs(suggestions, outputDir);
|
|
507
|
+
}
|
|
508
|
+
}
|