affora 0.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/CHANGELOG.md +16 -0
- package/CHECKS.md +40 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/agent.md +70 -0
- package/checks/cli.mjs +44 -0
- package/checks/substrate.mjs +269 -0
- package/cli/affora.mjs +131 -0
- package/design.md +113 -0
- package/package.json +71 -0
- package/registry.json +83 -0
- package/rules/antd.js +173 -0
- package/rules/generic.js +299 -0
- package/rules/hidden-radio.js +94 -0
- package/rules/magento.js +325 -0
- package/rules/mui.js +230 -0
- package/rules/shadcn-baseui.js +169 -0
- package/rules/shadcn-radix.js +289 -0
- package/src/components/combobox.tsx +279 -0
- package/src/components/datatable.tsx +454 -0
- package/src/components/dialog.tsx +548 -0
- package/src/components/productcard.tsx +551 -0
- package/src/components/settingsform.tsx +583 -0
- package/src/patterns/confirmundo.tsx +134 -0
- package/src/patterns/errremedy.tsx +155 -0
- package/src/patterns/flowform.tsx +179 -0
- package/src/patterns/gatedaction.tsx +157 -0
- package/src/patterns/persistentfeedback.tsx +106 -0
- package/src/primitives/actionbutton.tsx +64 -0
- package/src/primitives/textfield.tsx +57 -0
- package/src/tokens/themes.css +1068 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { useId, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Combobox — a searchable select for choosing a JavaScript framework.
|
|
6
|
+
*
|
|
7
|
+
* Substrate is deliberately plain and agent-legible:
|
|
8
|
+
* <label> + real <input list> + <datalist> of real <option>s.
|
|
9
|
+
* The options live in the DOM, the chosen value is the input's visible value,
|
|
10
|
+
* and an agent can simply type a value or read the listed options. Every
|
|
11
|
+
* stylistic decision is a var(--token) so the ONE authored component expresses
|
|
12
|
+
* all five theme archetypes without baking in a single look.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type Framework = {
|
|
16
|
+
value: string;
|
|
17
|
+
hint: string; // short editorial descriptor shown as inline documentation
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const FRAMEWORKS: Framework[] = [
|
|
21
|
+
{ value: 'React', hint: 'Component UI · virtual DOM' },
|
|
22
|
+
{ value: 'Vue', hint: 'Progressive · template-first' },
|
|
23
|
+
{ value: 'Svelte', hint: 'Compiler · no runtime' },
|
|
24
|
+
{ value: 'Angular', hint: 'Batteries-included platform' },
|
|
25
|
+
{ value: 'Solid', hint: 'Fine-grained reactivity' },
|
|
26
|
+
{ value: 'Qwik', hint: 'Resumable · zero-hydration' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const VALID = new Set(FRAMEWORKS.map((f) => f.value.toLowerCase()));
|
|
30
|
+
|
|
31
|
+
export const Combobox: React.FC<{ onCommit?: (v: string) => void; initialValue?: string }> = ({ onCommit, initialValue = '' }) => {
|
|
32
|
+
const inputId = useId();
|
|
33
|
+
const listId = useId();
|
|
34
|
+
const statusId = useId();
|
|
35
|
+
const [value, setValue] = useState(initialValue);
|
|
36
|
+
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
const matched = trimmed !== '' && VALID.has(trimmed.toLowerCase());
|
|
39
|
+
|
|
40
|
+
const commit = (raw: string) => {
|
|
41
|
+
const v = raw.trim();
|
|
42
|
+
setValue(v);
|
|
43
|
+
if (v !== '') onCommit?.(v);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="cbx-scope">
|
|
48
|
+
<style>{css}</style>
|
|
49
|
+
|
|
50
|
+
<div className="cbx-field">
|
|
51
|
+
<label className="cbx-label" htmlFor={inputId}>
|
|
52
|
+
Framework
|
|
53
|
+
</label>
|
|
54
|
+
<p className="cbx-help" id={`${inputId}-help`}>
|
|
55
|
+
Type or pick the JavaScript framework for this project.
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<div className="cbx-control" data-matched={matched ? 'true' : 'false'}>
|
|
59
|
+
<FrameworkGlyph />
|
|
60
|
+
<input
|
|
61
|
+
id={inputId}
|
|
62
|
+
className="cbx-input"
|
|
63
|
+
type="text"
|
|
64
|
+
list={listId}
|
|
65
|
+
role="combobox"
|
|
66
|
+
aria-expanded="true"
|
|
67
|
+
aria-controls={listId}
|
|
68
|
+
aria-describedby={`${inputId}-help ${statusId}`}
|
|
69
|
+
autoComplete="off"
|
|
70
|
+
spellCheck={false}
|
|
71
|
+
placeholder="Search frameworks…"
|
|
72
|
+
value={value}
|
|
73
|
+
onChange={(e) => setValue(e.target.value)}
|
|
74
|
+
onInput={(e) => {
|
|
75
|
+
// datalist selection fires input with the full option value
|
|
76
|
+
const v = (e.target as HTMLInputElement).value;
|
|
77
|
+
if (VALID.has(v.trim().toLowerCase())) commit(v);
|
|
78
|
+
}}
|
|
79
|
+
onBlur={(e) => {
|
|
80
|
+
if (e.target.value.trim() !== '') commit(e.target.value);
|
|
81
|
+
}}
|
|
82
|
+
onKeyDown={(e) => {
|
|
83
|
+
if (e.key === 'Enter') {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
commit((e.target as HTMLInputElement).value);
|
|
86
|
+
}
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
<ChevronGlyph />
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<datalist id={listId}>
|
|
93
|
+
{FRAMEWORKS.map((f) => (
|
|
94
|
+
<option key={f.value} value={f.value}>
|
|
95
|
+
{f.hint}
|
|
96
|
+
</option>
|
|
97
|
+
))}
|
|
98
|
+
</datalist>
|
|
99
|
+
|
|
100
|
+
<p className="cbx-status" id={statusId} role="status" aria-live="polite">
|
|
101
|
+
{matched ? (
|
|
102
|
+
<>
|
|
103
|
+
<CheckGlyph />
|
|
104
|
+
<span>
|
|
105
|
+
<strong className="cbx-status-value">{trimmed}</strong> selected
|
|
106
|
+
</span>
|
|
107
|
+
</>
|
|
108
|
+
) : (
|
|
109
|
+
<span className="cbx-status-idle">
|
|
110
|
+
{FRAMEWORKS.length} frameworks available
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/* ---- inline SVG glyphs (currentColor only, no external assets) ---- */
|
|
120
|
+
|
|
121
|
+
const FrameworkGlyph: React.FC = () => (
|
|
122
|
+
<svg className="cbx-lead" width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
123
|
+
<path
|
|
124
|
+
d="M12 3 3 7.5 12 12l9-4.5L12 3Z"
|
|
125
|
+
stroke="currentColor"
|
|
126
|
+
strokeWidth="1.6"
|
|
127
|
+
strokeLinejoin="round"
|
|
128
|
+
/>
|
|
129
|
+
<path
|
|
130
|
+
d="M3 12l9 4.5L21 12M3 16.5 12 21l9-4.5"
|
|
131
|
+
stroke="currentColor"
|
|
132
|
+
strokeWidth="1.6"
|
|
133
|
+
strokeLinejoin="round"
|
|
134
|
+
opacity="0.55"
|
|
135
|
+
/>
|
|
136
|
+
</svg>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const ChevronGlyph: React.FC = () => (
|
|
140
|
+
<svg className="cbx-chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
141
|
+
<path d="m6 9 6 6 6-6" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
|
142
|
+
</svg>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const CheckGlyph: React.FC = () => (
|
|
146
|
+
<svg className="cbx-check" width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
147
|
+
<path d="m5 12.5 4.2 4.5L19 7" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
148
|
+
</svg>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
export const css = `
|
|
152
|
+
.cbx-scope {
|
|
153
|
+
color: var(--fg);
|
|
154
|
+
font-family: var(--font);
|
|
155
|
+
font-size: var(--text-base);
|
|
156
|
+
line-height: var(--leading);
|
|
157
|
+
-webkit-font-smoothing: antialiased;
|
|
158
|
+
}
|
|
159
|
+
.cbx-scope * { box-sizing: border-box; }
|
|
160
|
+
|
|
161
|
+
.cbx-field {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
gap: var(--gap);
|
|
165
|
+
width: 100%;
|
|
166
|
+
max-width: 380px;
|
|
167
|
+
padding: var(--pad-loose);
|
|
168
|
+
background: var(--glass-bg, var(--surface));
|
|
169
|
+
backdrop-filter: var(--glass-blur, none);
|
|
170
|
+
-webkit-backdrop-filter: var(--glass-blur, none);
|
|
171
|
+
border: 1px solid var(--glass-border, var(--border));
|
|
172
|
+
border-radius: var(--radius-lg);
|
|
173
|
+
box-shadow: var(--shadow-sm);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.cbx-label {
|
|
177
|
+
font-family: var(--font-display);
|
|
178
|
+
font-size: calc(var(--text-base) * 1.15);
|
|
179
|
+
font-weight: var(--weight-display);
|
|
180
|
+
letter-spacing: var(--tracking-tight);
|
|
181
|
+
line-height: 1.2;
|
|
182
|
+
}
|
|
183
|
+
.cbx-help {
|
|
184
|
+
margin: calc(var(--gap) * -0.5) 0 0;
|
|
185
|
+
color: var(--fg-muted);
|
|
186
|
+
font-size: calc(var(--text-base) * 0.92);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.cbx-control {
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
gap: var(--pad-tight);
|
|
193
|
+
padding: 0 var(--pad);
|
|
194
|
+
min-height: calc(var(--text-base) * 3.2);
|
|
195
|
+
background: var(--bg);
|
|
196
|
+
border: 1px solid var(--border-strong);
|
|
197
|
+
border-radius: var(--radius);
|
|
198
|
+
transition:
|
|
199
|
+
border-color var(--dur) var(--ease),
|
|
200
|
+
box-shadow var(--dur) var(--ease),
|
|
201
|
+
background var(--dur) var(--ease);
|
|
202
|
+
}
|
|
203
|
+
.cbx-control:hover {
|
|
204
|
+
border-color: var(--accent);
|
|
205
|
+
}
|
|
206
|
+
.cbx-control:focus-within {
|
|
207
|
+
border-color: var(--accent);
|
|
208
|
+
box-shadow: 0 0 0 3px var(--ring);
|
|
209
|
+
background: var(--surface);
|
|
210
|
+
}
|
|
211
|
+
.cbx-control[data-matched='true'] {
|
|
212
|
+
border-color: var(--accent);
|
|
213
|
+
background: var(--accent-weak);
|
|
214
|
+
}
|
|
215
|
+
.cbx-control[data-matched='true']:focus-within {
|
|
216
|
+
background: var(--surface);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.cbx-lead {
|
|
220
|
+
flex: none;
|
|
221
|
+
color: var(--accent);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.cbx-input {
|
|
225
|
+
flex: 1 1 auto;
|
|
226
|
+
min-width: 0;
|
|
227
|
+
appearance: none;
|
|
228
|
+
border: none;
|
|
229
|
+
outline: none;
|
|
230
|
+
background: transparent;
|
|
231
|
+
color: var(--fg);
|
|
232
|
+
font-family: var(--font);
|
|
233
|
+
font-size: var(--text-base);
|
|
234
|
+
font-weight: var(--weight-medium);
|
|
235
|
+
letter-spacing: var(--tracking-tight);
|
|
236
|
+
padding: var(--pad-tight) 0;
|
|
237
|
+
}
|
|
238
|
+
.cbx-input::placeholder {
|
|
239
|
+
color: var(--fg-subtle);
|
|
240
|
+
font-weight: var(--weight-normal);
|
|
241
|
+
}
|
|
242
|
+
/* keep the native datalist affordance from doubling our custom chevron */
|
|
243
|
+
.cbx-input::-webkit-calendar-picker-indicator {
|
|
244
|
+
opacity: 0;
|
|
245
|
+
width: 0;
|
|
246
|
+
margin: 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.cbx-chevron {
|
|
250
|
+
flex: none;
|
|
251
|
+
color: var(--fg-subtle);
|
|
252
|
+
transition: color var(--dur) var(--ease), transform var(--dur) var(--ease-emphasis);
|
|
253
|
+
}
|
|
254
|
+
.cbx-control:focus-within .cbx-chevron {
|
|
255
|
+
color: var(--accent);
|
|
256
|
+
transform: rotate(180deg);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.cbx-status {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: var(--pad-tight);
|
|
263
|
+
margin: 0;
|
|
264
|
+
min-height: calc(var(--text-base) * 1.3);
|
|
265
|
+
font-size: calc(var(--text-base) * 0.92);
|
|
266
|
+
color: var(--fg-muted);
|
|
267
|
+
}
|
|
268
|
+
.cbx-status-idle { color: var(--fg-subtle); }
|
|
269
|
+
.cbx-status-value {
|
|
270
|
+
color: var(--fg);
|
|
271
|
+
font-weight: var(--weight-bold);
|
|
272
|
+
}
|
|
273
|
+
.cbx-check {
|
|
274
|
+
flex: none;
|
|
275
|
+
color: var(--success);
|
|
276
|
+
}
|
|
277
|
+
`;
|
|
278
|
+
|
|
279
|
+
export default Combobox;
|