@solidrt/cli 0.0.20 → 0.0.22
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/package.json +5 -5
- package/scaffold/AGENTS.md +8 -0
- package/scaffold/package.json +4 -4
- package/scaffold/templates/gallery/icon.png +0 -0
- package/scaffold/templates/gallery/index.tsx +485 -0
- package/src/args.ts +4 -3
- package/src/commands/init.ts +71 -12
- package/src/prompt.ts +76 -0
- /package/scaffold/{src → templates/default}/index.tsx +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"qrcode-generator": "^2.0.4"
|
|
23
23
|
},
|
|
24
24
|
"optionalDependencies": {
|
|
25
|
-
"@solidrt/darwin-arm64": "0.0.
|
|
26
|
-
"@solidrt/linux-x64-gnu": "0.0.
|
|
27
|
-
"@solidrt/win32-x64-msvc": "0.0.
|
|
25
|
+
"@solidrt/darwin-arm64": "0.0.22",
|
|
26
|
+
"@solidrt/linux-x64-gnu": "0.0.22",
|
|
27
|
+
"@solidrt/win32-x64-msvc": "0.0.22"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@solidrt/core": "0.0.
|
|
30
|
+
"@solidrt/core": "0.0.22",
|
|
31
31
|
"typescript": "^6"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
package/scaffold/AGENTS.md
CHANGED
|
@@ -8,6 +8,7 @@ Authoritative references ship inside the installed packages - read them:
|
|
|
8
8
|
- node_modules/solid-js/CHEATSHEET.md - SolidJS 2.0 reactivity/control-flow model
|
|
9
9
|
- node_modules/@solidrt/components/AGENTS.md - the component vocabulary; build UI from these
|
|
10
10
|
- node_modules/@solidrt/components/README.md - full prop tables for every component
|
|
11
|
+
- node_modules/@solidrt/components/examples/ - single-concept usage patterns to copy (see its README.md index)
|
|
11
12
|
- node_modules/@solidrt/core/AGENTS.md - the underlying element/prop/reactivity model
|
|
12
13
|
- node_modules/@solidrt/core/examples/ - single-concept usage patterns to copy (see its README.md index)
|
|
13
14
|
- node_modules/@solidrt/cli/AGENTS.md - running, bundling, headless verify
|
|
@@ -68,6 +69,13 @@ Authoritative references ship inside the installed packages - read them:
|
|
|
68
69
|
em-dashes (use a hyphen), no smart/curly quotes, no unicode symbols.
|
|
69
70
|
11. Prefer let over const. Use const only for real constants - a single fixed
|
|
70
71
|
string or number value - and name those in ALL_CAPS.
|
|
72
|
+
12. Reading a signal/prop/store at the top level of a component body (not
|
|
73
|
+
inside JSX, a `createMemo`, or an effect's compute phase) reads it
|
|
74
|
+
untracked - it silently freezes at the initial value instead of updating
|
|
75
|
+
on change. `createEffect` takes two arguments now: `(compute, apply)`.
|
|
76
|
+
`compute` is the tracked read phase; `apply(value, prev)` runs untracked
|
|
77
|
+
and is where side effects/DOM-equivalent writes belong. The old
|
|
78
|
+
single-arg `createEffect(fn)` form is gone - using it is an error.
|
|
71
79
|
|
|
72
80
|
## Run / verify
|
|
73
81
|
|
package/scaffold/package.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"android": "srt client --android"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@solidrt/core": "0.0.
|
|
13
|
-
"@solidrt/components": "0.0.
|
|
12
|
+
"@solidrt/core": "0.0.22",
|
|
13
|
+
"@solidrt/components": "0.0.22"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@solidrt/cli": "0.0.
|
|
17
|
-
"@solidrt/flux-types": "0.0.
|
|
16
|
+
"@solidrt/cli": "0.0.22",
|
|
17
|
+
"@solidrt/flux-types": "0.0.22",
|
|
18
18
|
"typescript": "^6"
|
|
19
19
|
}
|
|
20
20
|
}
|
|
Binary file
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
createSignal,
|
|
4
|
+
createEffect,
|
|
5
|
+
untrack,
|
|
6
|
+
env,
|
|
7
|
+
capabilities,
|
|
8
|
+
} from "@solidrt/core"
|
|
9
|
+
import {
|
|
10
|
+
policy,
|
|
11
|
+
setPolicy,
|
|
12
|
+
type DensityPolicy,
|
|
13
|
+
type MotionPolicy,
|
|
14
|
+
Window,
|
|
15
|
+
View,
|
|
16
|
+
Text,
|
|
17
|
+
Image,
|
|
18
|
+
Button,
|
|
19
|
+
TextInput,
|
|
20
|
+
Switch,
|
|
21
|
+
Checkbox,
|
|
22
|
+
RadioGroup,
|
|
23
|
+
Radio,
|
|
24
|
+
Slider,
|
|
25
|
+
QrCode,
|
|
26
|
+
Card,
|
|
27
|
+
Divider,
|
|
28
|
+
Badge,
|
|
29
|
+
Spinner,
|
|
30
|
+
ProgressBar,
|
|
31
|
+
Tooltip,
|
|
32
|
+
Select,
|
|
33
|
+
ContextMenu,
|
|
34
|
+
Icon,
|
|
35
|
+
ScrollView,
|
|
36
|
+
theme,
|
|
37
|
+
space,
|
|
38
|
+
typeWeight,
|
|
39
|
+
setTheme,
|
|
40
|
+
darkTheme,
|
|
41
|
+
lightTheme,
|
|
42
|
+
SafeArea,
|
|
43
|
+
} from "@solidrt/components"
|
|
44
|
+
|
|
45
|
+
import icon from "./icon.png" with { type: "binary" }
|
|
46
|
+
|
|
47
|
+
// A growing gallery of @solidrt/components. The whole thing recolors live when
|
|
48
|
+
// the theme switches: every component reads theme.* reactively, so pressing
|
|
49
|
+
// "Toggle theme" reflows the palette with no remount. The "Environment and
|
|
50
|
+
// policies" card shows the environment -> capabilities -> policies cascade and
|
|
51
|
+
// lets you force density/motion overrides to watch every control adapt.
|
|
52
|
+
|
|
53
|
+
// Lucide icons are SVG documents (strings). In a real app you import them as
|
|
54
|
+
// assets (`import House from "lucide-static/icons/house.svg"`) or from a string
|
|
55
|
+
// export; we inline a few here so the example stays self-contained. Each strokes
|
|
56
|
+
// with currentColor, so Icon recolors them from the theme.
|
|
57
|
+
const LUCIDE = (body: string) =>
|
|
58
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"` +
|
|
59
|
+
` stroke="currentColor" stroke-width="2" stroke-linecap="round"` +
|
|
60
|
+
` stroke-linejoin="round">${body}</svg>`
|
|
61
|
+
const HOUSE = LUCIDE(
|
|
62
|
+
`<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/>` +
|
|
63
|
+
`<path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>`,
|
|
64
|
+
)
|
|
65
|
+
const HEART = LUCIDE(
|
|
66
|
+
`<path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"/>`,
|
|
67
|
+
)
|
|
68
|
+
const STAR = LUCIDE(
|
|
69
|
+
`<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>`,
|
|
70
|
+
)
|
|
71
|
+
const SETTINGS = LUCIDE(
|
|
72
|
+
`<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/>` +
|
|
73
|
+
`<circle cx="12" cy="12" r="3"/>`,
|
|
74
|
+
)
|
|
75
|
+
const BELL = LUCIDE(
|
|
76
|
+
`<path d="M10.268 21a2 2 0 0 0 3.464 0"/>` +
|
|
77
|
+
`<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/>`,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
function Row(props: { label: string; children?: any }) {
|
|
81
|
+
return (
|
|
82
|
+
<View
|
|
83
|
+
layout={{
|
|
84
|
+
flexDirection: "row",
|
|
85
|
+
alignItems: "center",
|
|
86
|
+
justifyContent: "space-between",
|
|
87
|
+
gap: space("lg"),
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<Text muted>{props.label}</Text>
|
|
91
|
+
{props.children}
|
|
92
|
+
</View>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function Value(props: { children?: any }) {
|
|
97
|
+
return <Text>{props.children}</Text>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Joins the names whose flag is true, e.g. list(["mouse", env.mouseSeen], ...).
|
|
101
|
+
function list(...entries: [string, boolean][]) {
|
|
102
|
+
let names = entries.filter(([, on]) => on).map(([name]) => name)
|
|
103
|
+
return names.length > 0 ? names.join(", ") : "none yet"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function App() {
|
|
107
|
+
let [dark, setDark] = createSignal(true)
|
|
108
|
+
let [name, setName] = createSignal("")
|
|
109
|
+
let [notify, setNotify] = createSignal(true)
|
|
110
|
+
let [agree, setAgree] = createSignal(false)
|
|
111
|
+
let [plan, setPlan] = createSignal<unknown>("pro")
|
|
112
|
+
let [volume, setVolume] = createSignal(40)
|
|
113
|
+
let [fruit, setFruit] = createSignal<unknown>(undefined)
|
|
114
|
+
|
|
115
|
+
// The QR code follows the name field debounced, not per keystroke: each data
|
|
116
|
+
// change regenerates the whole module grid, which is too heavy to run at
|
|
117
|
+
// typing frequency.
|
|
118
|
+
// untrack: seed with the current name once; the debounced effect below owns
|
|
119
|
+
// keeping it in sync, and a bare name() read here would warn (strict mode
|
|
120
|
+
// flags top-level reactive reads in component bodies as one-shot).
|
|
121
|
+
let [qrData, setQrData] = createSignal(untrack(name))
|
|
122
|
+
createEffect(
|
|
123
|
+
() => name(),
|
|
124
|
+
(v) => {
|
|
125
|
+
let timer = setTimeout(() => setQrData(v), 250)
|
|
126
|
+
return () => clearTimeout(timer)
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
// Follow the OS dark/light preference until the user toggles manually; the
|
|
131
|
+
// manual toggle then owns the choice for the rest of the session.
|
|
132
|
+
let userToggledTheme = false
|
|
133
|
+
let toggleTheme = () => {
|
|
134
|
+
userToggledTheme = true
|
|
135
|
+
let next = !dark()
|
|
136
|
+
setDark(next)
|
|
137
|
+
setTheme(next ? darkTheme : lightTheme)
|
|
138
|
+
}
|
|
139
|
+
createEffect(
|
|
140
|
+
() => env.systemTheme,
|
|
141
|
+
(t) => {
|
|
142
|
+
if (userToggledTheme || t === "unknown") return
|
|
143
|
+
let isDark = t === "dark"
|
|
144
|
+
setDark(isDark)
|
|
145
|
+
setTheme(isDark ? darkTheme : lightTheme)
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
// Policy override choices; "auto" hands the policy back to the resolver.
|
|
150
|
+
let [densityChoice, setDensityChoice] = createSignal<unknown>("auto")
|
|
151
|
+
let [motionChoice, setMotionChoice] = createSignal<unknown>("auto")
|
|
152
|
+
let chooseDensity = (v: unknown) => {
|
|
153
|
+
setDensityChoice(v)
|
|
154
|
+
setPolicy({ density: v === "auto" ? undefined : (v as DensityPolicy) })
|
|
155
|
+
}
|
|
156
|
+
let chooseMotion = (v: unknown) => {
|
|
157
|
+
setMotionChoice(v)
|
|
158
|
+
setPolicy({ motion: v === "auto" ? undefined : (v as MotionPolicy) })
|
|
159
|
+
}
|
|
160
|
+
let [textScaleChoice, setTextScaleChoice] = createSignal<unknown>("auto")
|
|
161
|
+
let chooseTextScale = (v: unknown) => {
|
|
162
|
+
setTextScaleChoice(v)
|
|
163
|
+
setPolicy({ textScale: v === "auto" ? undefined : (v as number) })
|
|
164
|
+
}
|
|
165
|
+
let [weightDeltaChoice, setWeightDeltaChoice] = createSignal<unknown>("auto")
|
|
166
|
+
let chooseWeightDelta = (v: unknown) => {
|
|
167
|
+
setWeightDeltaChoice(v)
|
|
168
|
+
setPolicy({ textWeightDelta: v === "auto" ? undefined : (v as number) })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Window
|
|
173
|
+
title="Components gallery"
|
|
174
|
+
layout={{ flexDirection: "column" }}
|
|
175
|
+
style={{ backgroundColor: theme.color.background }}
|
|
176
|
+
>
|
|
177
|
+
<SafeArea>
|
|
178
|
+
<ScrollView horizontal layout={{ flex: 1 }}>
|
|
179
|
+
<View
|
|
180
|
+
layout={{
|
|
181
|
+
height: "100%",
|
|
182
|
+
flexDirection: "column",
|
|
183
|
+
flexWrap: "wrap",
|
|
184
|
+
alignContent: "flex-start",
|
|
185
|
+
justifyContent: "flex-start",
|
|
186
|
+
gap: space("xl"),
|
|
187
|
+
padding: space("xl"),
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
<Card title={dark() ? "Dark theme" : "Light theme"} layout={{ width: 360 }}>
|
|
191
|
+
<Text muted>Toggle to recolor every control below.</Text>
|
|
192
|
+
<Button onPress={toggleTheme}>Toggle theme</Button>
|
|
193
|
+
</Card>
|
|
194
|
+
|
|
195
|
+
<Card title="Environment" layout={{ width: 360 }}>
|
|
196
|
+
<Row label="Window">
|
|
197
|
+
<Value>
|
|
198
|
+
{`${env.windowSize.width} x ${env.windowSize.height} @ ${env.displayScale}x`}
|
|
199
|
+
</Value>
|
|
200
|
+
</Row>
|
|
201
|
+
<Row label="System">
|
|
202
|
+
<Value>{`${env.systemTheme} theme, ${env.orientation}`}</Value>
|
|
203
|
+
</Row>
|
|
204
|
+
<Row label="Devices">
|
|
205
|
+
<Value>
|
|
206
|
+
{env.inputDevices
|
|
207
|
+
? list(
|
|
208
|
+
["mouse", env.inputDevices.mouse],
|
|
209
|
+
["touch", env.inputDevices.touch],
|
|
210
|
+
["keyboard", env.inputDevices.keyboard],
|
|
211
|
+
)
|
|
212
|
+
: "not reported"}
|
|
213
|
+
</Value>
|
|
214
|
+
</Row>
|
|
215
|
+
<Row label="Inputs seen">
|
|
216
|
+
<Value>
|
|
217
|
+
{list(
|
|
218
|
+
["mouse", env.mouseSeen],
|
|
219
|
+
["touch", env.touchSeen],
|
|
220
|
+
["keyboard", env.keyboardSeen],
|
|
221
|
+
)}
|
|
222
|
+
</Value>
|
|
223
|
+
</Row>
|
|
224
|
+
<Row label="Capabilities">
|
|
225
|
+
<Value>
|
|
226
|
+
{list(
|
|
227
|
+
["hover", capabilities.hover],
|
|
228
|
+
["touch", capabilities.touch],
|
|
229
|
+
["keyboard nav", capabilities.keyboardNav],
|
|
230
|
+
)}
|
|
231
|
+
</Value>
|
|
232
|
+
</Row>
|
|
233
|
+
<Row label="Size class">
|
|
234
|
+
<Value>{capabilities.windowSizeClass}</Value>
|
|
235
|
+
</Row>
|
|
236
|
+
</Card>
|
|
237
|
+
|
|
238
|
+
<Card title="Policies" layout={{ width: 360 }}>
|
|
239
|
+
<Row label="Interaction policy">
|
|
240
|
+
<Value>{policy.interaction}</Value>
|
|
241
|
+
</Row>
|
|
242
|
+
<Row label="Focus ring">
|
|
243
|
+
<Value>{policy.focusRing ? "visible" : "hidden"}</Value>
|
|
244
|
+
</Row>
|
|
245
|
+
<Row label="App policies">
|
|
246
|
+
<Value>{`${policy.navigation}, ${policy.layout}`}</Value>
|
|
247
|
+
</Row>
|
|
248
|
+
<Divider />
|
|
249
|
+
<Row label="Density policy">
|
|
250
|
+
<Value>{policy.density}</Value>
|
|
251
|
+
</Row>
|
|
252
|
+
<RadioGroup
|
|
253
|
+
value={densityChoice()}
|
|
254
|
+
onChange={chooseDensity}
|
|
255
|
+
layout={{ flexDirection: "row", flexWrap: "wrap", gap: space("md") }}
|
|
256
|
+
>
|
|
257
|
+
<Radio value="auto">Auto</Radio>
|
|
258
|
+
<Radio value="comfortable">Comfortable</Radio>
|
|
259
|
+
<Radio value="compact">Compact</Radio>
|
|
260
|
+
<Radio value="dense">Dense</Radio>
|
|
261
|
+
</RadioGroup>
|
|
262
|
+
<Divider />
|
|
263
|
+
<Row label="Motion policy">
|
|
264
|
+
<Value>{policy.motion}</Value>
|
|
265
|
+
</Row>
|
|
266
|
+
<RadioGroup
|
|
267
|
+
value={motionChoice()}
|
|
268
|
+
onChange={chooseMotion}
|
|
269
|
+
layout={{ flexDirection: "row", flexWrap: "wrap", gap: space("md") }}
|
|
270
|
+
>
|
|
271
|
+
<Radio value="auto">Auto</Radio>
|
|
272
|
+
<Radio value="normal">Normal</Radio>
|
|
273
|
+
<Radio value="reduced">Reduced</Radio>
|
|
274
|
+
<Radio value="none">None</Radio>
|
|
275
|
+
</RadioGroup>
|
|
276
|
+
<Divider />
|
|
277
|
+
<Row label="Text scale">
|
|
278
|
+
<Value>{`${policy.textScale}x`}</Value>
|
|
279
|
+
</Row>
|
|
280
|
+
<RadioGroup
|
|
281
|
+
value={textScaleChoice()}
|
|
282
|
+
onChange={chooseTextScale}
|
|
283
|
+
layout={{ flexDirection: "row", flexWrap: "wrap", gap: space("md") }}
|
|
284
|
+
>
|
|
285
|
+
<Radio value="auto">Auto</Radio>
|
|
286
|
+
<Radio value={0.9}>0.9</Radio>
|
|
287
|
+
<Radio value={1.0}>1.0</Radio>
|
|
288
|
+
<Radio value={1.1}>1.1</Radio>
|
|
289
|
+
</RadioGroup>
|
|
290
|
+
<Divider />
|
|
291
|
+
<Row label="Text weight delta">
|
|
292
|
+
<Value>
|
|
293
|
+
{`base +${policy.textWeightDelta}, body ${typeWeight(
|
|
294
|
+
theme.text.body.weight,
|
|
295
|
+
theme.text.body.size * policy.textScale,
|
|
296
|
+
)}, title ${typeWeight(theme.text.title.weight, theme.text.title.size * policy.textScale)}`}
|
|
297
|
+
</Value>
|
|
298
|
+
</Row>
|
|
299
|
+
<RadioGroup
|
|
300
|
+
value={weightDeltaChoice()}
|
|
301
|
+
onChange={chooseWeightDelta}
|
|
302
|
+
layout={{ flexDirection: "row", flexWrap: "wrap", gap: space("md") }}
|
|
303
|
+
>
|
|
304
|
+
<Radio value="auto">Auto</Radio>
|
|
305
|
+
<Radio value={0}>0</Radio>
|
|
306
|
+
<Radio value={100}>+100</Radio>
|
|
307
|
+
<Radio value={200}>+200</Radio>
|
|
308
|
+
</RadioGroup>
|
|
309
|
+
</Card>
|
|
310
|
+
|
|
311
|
+
<Card title="Buttons" layout={{ width: 360 }}>
|
|
312
|
+
<View
|
|
313
|
+
layout={{
|
|
314
|
+
flexDirection: "row",
|
|
315
|
+
flexWrap: "wrap",
|
|
316
|
+
gap: space("md"),
|
|
317
|
+
alignItems: "center",
|
|
318
|
+
}}
|
|
319
|
+
>
|
|
320
|
+
<Button>Primary</Button>
|
|
321
|
+
<Button variant="secondary">Secondary</Button>
|
|
322
|
+
<Button variant="ghost">Ghost</Button>
|
|
323
|
+
<Button variant="danger">Danger</Button>
|
|
324
|
+
<Button disabled>Disabled</Button>
|
|
325
|
+
</View>
|
|
326
|
+
</Card>
|
|
327
|
+
|
|
328
|
+
<Card title="Text input" layout={{ width: 360 }}>
|
|
329
|
+
<TextInput
|
|
330
|
+
value={name()}
|
|
331
|
+
onInput={setName}
|
|
332
|
+
placeholder="Your name"
|
|
333
|
+
layout={{ width: 320 }}
|
|
334
|
+
/>
|
|
335
|
+
</Card>
|
|
336
|
+
|
|
337
|
+
<Card title="Switch and checkbox" layout={{ width: 360 }}>
|
|
338
|
+
<Row label="Notifications">
|
|
339
|
+
<Switch value={notify()} onChange={setNotify} />
|
|
340
|
+
</Row>
|
|
341
|
+
<Row label="I agree to the terms">
|
|
342
|
+
<Checkbox checked={agree()} onChange={setAgree} />
|
|
343
|
+
</Row>
|
|
344
|
+
</Card>
|
|
345
|
+
|
|
346
|
+
<Card title="Radio group" layout={{ width: 360 }}>
|
|
347
|
+
<RadioGroup value={plan()} onChange={setPlan}>
|
|
348
|
+
<Radio value="free">Free</Radio>
|
|
349
|
+
<Radio value="pro">Pro</Radio>
|
|
350
|
+
<Radio value="team">Team</Radio>
|
|
351
|
+
</RadioGroup>
|
|
352
|
+
</Card>
|
|
353
|
+
|
|
354
|
+
<Card title="Context menu" layout={{ width: 360 }}>
|
|
355
|
+
<Text muted>
|
|
356
|
+
Right-click (mouse) or long-press (touch) the box below. Touch policy presents a
|
|
357
|
+
bottom sheet, desktop and hybrid a menu at the pointer.
|
|
358
|
+
</Text>
|
|
359
|
+
<ContextMenu
|
|
360
|
+
items={[
|
|
361
|
+
{ label: "Copy", onSelect: () => console.log("copy") },
|
|
362
|
+
{ label: "Paste", onSelect: () => console.log("paste") },
|
|
363
|
+
{ label: "Rename", disabled: true },
|
|
364
|
+
{ label: "Delete", onSelect: () => console.log("delete") },
|
|
365
|
+
]}
|
|
366
|
+
>
|
|
367
|
+
<View
|
|
368
|
+
layout={{ padding: space("lg"), alignItems: "center" }}
|
|
369
|
+
style={{
|
|
370
|
+
backgroundColor: theme.color.surfaceAlt,
|
|
371
|
+
borderRadius: theme.radius.sm,
|
|
372
|
+
}}
|
|
373
|
+
>
|
|
374
|
+
<Text>Secondary actions live here</Text>
|
|
375
|
+
</View>
|
|
376
|
+
</ContextMenu>
|
|
377
|
+
</Card>
|
|
378
|
+
|
|
379
|
+
<Card title="Select" layout={{ width: 360 }}>
|
|
380
|
+
<Text muted>
|
|
381
|
+
Forks on the interaction policy: desktop and hybrid anchor a dropdown, touch opens a
|
|
382
|
+
bottom sheet.
|
|
383
|
+
</Text>
|
|
384
|
+
<Select
|
|
385
|
+
value={fruit()}
|
|
386
|
+
onChange={setFruit}
|
|
387
|
+
placeholder="Pick a fruit"
|
|
388
|
+
options={[
|
|
389
|
+
{ value: "apple", label: "Apple" },
|
|
390
|
+
{ value: "banana", label: "Banana" },
|
|
391
|
+
{ value: "cherry", label: "Cherry" },
|
|
392
|
+
{ value: "dragonfruit", label: "Dragonfruit" },
|
|
393
|
+
]}
|
|
394
|
+
layout={{ width: 200 }}
|
|
395
|
+
/>
|
|
396
|
+
</Card>
|
|
397
|
+
|
|
398
|
+
<Card title="Slider" layout={{ width: 360 }}>
|
|
399
|
+
<Row label={`Volume: ${Math.round(volume())}`}>
|
|
400
|
+
<Slider
|
|
401
|
+
value={volume()}
|
|
402
|
+
onChange={setVolume}
|
|
403
|
+
min={0}
|
|
404
|
+
max={100}
|
|
405
|
+
step={1}
|
|
406
|
+
layout={{ width: 180 }}
|
|
407
|
+
/>
|
|
408
|
+
</Row>
|
|
409
|
+
</Card>
|
|
410
|
+
|
|
411
|
+
<Card title="Progress and spinner" layout={{ width: 360 }}>
|
|
412
|
+
<Row label="Loading">
|
|
413
|
+
<Spinner />
|
|
414
|
+
</Row>
|
|
415
|
+
<Text muted>Determinate, tracking the volume slider:</Text>
|
|
416
|
+
<ProgressBar value={volume() / 100} />
|
|
417
|
+
<Text muted>Indeterminate:</Text>
|
|
418
|
+
<ProgressBar />
|
|
419
|
+
</Card>
|
|
420
|
+
|
|
421
|
+
<Card title="Badges and divider" layout={{ width: 360 }}>
|
|
422
|
+
<Row label="Status">
|
|
423
|
+
<View layout={{ flexDirection: "row", gap: space("md"), alignItems: "center" }}>
|
|
424
|
+
<Badge>New</Badge>
|
|
425
|
+
<Badge variant="neutral">3</Badge>
|
|
426
|
+
<Badge variant="danger">Error</Badge>
|
|
427
|
+
</View>
|
|
428
|
+
</Row>
|
|
429
|
+
<Divider />
|
|
430
|
+
<Text muted>A divider separates content within a card.</Text>
|
|
431
|
+
</Card>
|
|
432
|
+
|
|
433
|
+
<Card title="Tooltip" layout={{ width: 360 }}>
|
|
434
|
+
<Text muted>
|
|
435
|
+
Rest the mouse on a button. Shows under desktop and hybrid interaction policies,
|
|
436
|
+
never under touch.
|
|
437
|
+
</Text>
|
|
438
|
+
<View layout={{ flexDirection: "row", gap: space("md") }}>
|
|
439
|
+
<Tooltip content="Saves your changes">
|
|
440
|
+
<Button>Hover me</Button>
|
|
441
|
+
</Tooltip>
|
|
442
|
+
<Tooltip content="Appears below the anchor" placement="bottom">
|
|
443
|
+
<Button>Below</Button>
|
|
444
|
+
</Tooltip>
|
|
445
|
+
</View>
|
|
446
|
+
</Card>
|
|
447
|
+
|
|
448
|
+
<Card title="Icons" layout={{ width: 360 }}>
|
|
449
|
+
<Text muted>
|
|
450
|
+
Lucide SVGs drawn through the core svg primitive. currentColor follows the theme;
|
|
451
|
+
the last two are recolored explicitly.
|
|
452
|
+
</Text>
|
|
453
|
+
<View layout={{ flexDirection: "row", gap: space("lg"), alignItems: "center" }}>
|
|
454
|
+
<Icon src={HOUSE} />
|
|
455
|
+
<Icon src={SETTINGS} />
|
|
456
|
+
<Icon src={BELL} />
|
|
457
|
+
<Icon src={HEART} color={theme.color.danger} />
|
|
458
|
+
<Icon src={STAR} color={theme.color.primary} />
|
|
459
|
+
</View>
|
|
460
|
+
</Card>
|
|
461
|
+
|
|
462
|
+
<Card title="QR code" layout={{ width: 360 }}>
|
|
463
|
+
<Text muted>Encodes your name, or a link if the field is empty.</Text>
|
|
464
|
+
<View layout={{ alignItems: "center" }}>
|
|
465
|
+
<QrCode data={qrData() || "https://solidjs.com"} />
|
|
466
|
+
</View>
|
|
467
|
+
</Card>
|
|
468
|
+
|
|
469
|
+
<Card title="Image" layout={{ width: 360 }}>
|
|
470
|
+
<Text muted>
|
|
471
|
+
Fetches, decodes, and uploads an image, then shows it as a GPU texture. Rounded via
|
|
472
|
+
a clipping border radius.
|
|
473
|
+
</Text>
|
|
474
|
+
<View layout={{ alignItems: "center" }}>
|
|
475
|
+
<Image src={icon} style={{ borderRadius: theme.radius.md }} />
|
|
476
|
+
</View>
|
|
477
|
+
</Card>
|
|
478
|
+
</View>
|
|
479
|
+
</ScrollView>
|
|
480
|
+
</SafeArea>
|
|
481
|
+
</Window>
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
render(() => <App />)
|
package/src/args.ts
CHANGED
|
@@ -16,6 +16,7 @@ export let { values, positionals } = parseArgs({
|
|
|
16
16
|
stats: { type: "boolean", default: false },
|
|
17
17
|
android: { type: "boolean", default: false },
|
|
18
18
|
device: { type: "string" },
|
|
19
|
+
template: { type: "string", short: "t" },
|
|
19
20
|
},
|
|
20
21
|
allowPositionals: true,
|
|
21
22
|
})
|
|
@@ -35,9 +36,6 @@ function usage(line: string): never {
|
|
|
35
36
|
// Per-command argument requirements. Called once before dispatch.
|
|
36
37
|
export function validateArgs() {
|
|
37
38
|
switch (command) {
|
|
38
|
-
case "init":
|
|
39
|
-
if (!source) usage("srt init <dir> (the target folder is required)")
|
|
40
|
-
break
|
|
41
39
|
case "bundle":
|
|
42
40
|
if (values.flux) {
|
|
43
41
|
if (!source || !isTs) usage("srt bundle --flux [options] <entry.[ts|js]>")
|
|
@@ -76,6 +74,9 @@ Commands:
|
|
|
76
74
|
record <file.tsx|jsx> Capture frames for video generation
|
|
77
75
|
pack <file> Bundle + compile to a standalone executable (experimental)
|
|
78
76
|
|
|
77
|
+
init options:
|
|
78
|
+
-t, --template <name> Start from a named template (skips the interactive picker)
|
|
79
|
+
|
|
79
80
|
run/server options:
|
|
80
81
|
--proxy-files Route file/dir access through the dev server
|
|
81
82
|
--proxy-http Route fetch calls through the dev server (HTTP cache enabled)
|
package/src/commands/init.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
|
|
1
|
+
import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises"
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path"
|
|
3
|
-
import { source } from "../args"
|
|
3
|
+
import { source, values } from "../args"
|
|
4
|
+
import { select, text } from "../prompt"
|
|
5
|
+
|
|
6
|
+
const DEFAULT_NAME = "solidrt-app"
|
|
4
7
|
|
|
5
8
|
const SCAFFOLD_DIR = join(import.meta.dir, "../../scaffold")
|
|
9
|
+
const TEMPLATES_DIR = join(SCAFFOLD_DIR, "templates")
|
|
6
10
|
|
|
7
|
-
//
|
|
8
|
-
// .gitignore is stored there as `gitignore` because npm
|
|
9
|
-
// named `.gitignore` from published packages, so it is
|
|
11
|
+
// Shared project files written for every template. Sources live in
|
|
12
|
+
// cli/scaffold/. The .gitignore is stored there as `gitignore` because npm
|
|
13
|
+
// strips files literally named `.gitignore` from published packages, so it is
|
|
14
|
+
// renamed on the way out. The per-template src/ comes from scaffold/templates/.
|
|
10
15
|
const TEMPLATE_FILES: Array<{ from: string; to: string }> = [
|
|
11
16
|
{ from: "package.json", to: "package.json" },
|
|
12
17
|
{ from: "tsconfig.json", to: "tsconfig.json" },
|
|
13
18
|
{ from: "gitignore", to: ".gitignore" },
|
|
14
19
|
{ from: "AGENTS.md", to: "AGENTS.md" },
|
|
15
|
-
{ from: "src/index.tsx", to: "src/index.tsx" },
|
|
16
20
|
]
|
|
17
21
|
|
|
18
22
|
// A valid npm package name derived from the target directory.
|
|
@@ -23,17 +27,63 @@ function packageName(dir: string): string {
|
|
|
23
27
|
return name || "solidrt-app"
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
const DEFAULT_TEMPLATE = "default"
|
|
31
|
+
|
|
32
|
+
// Templates are the directories under scaffold/templates/; each holds the files
|
|
33
|
+
// that become the new project's src/. `default` sorts first as the starting
|
|
34
|
+
// point, the rest alphabetically.
|
|
35
|
+
async function listTemplates(): Promise<string[]> {
|
|
36
|
+
let entries = await readdir(TEMPLATES_DIR, { withFileTypes: true })
|
|
37
|
+
return entries
|
|
38
|
+
.filter((e) => e.isDirectory())
|
|
39
|
+
.map((e) => e.name)
|
|
40
|
+
.sort((a, b) =>
|
|
41
|
+
a === DEFAULT_TEMPLATE ? -1 : b === DEFAULT_TEMPLATE ? 1 : a.localeCompare(b),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Resolve which template to scaffold from: an explicit --template if valid, an
|
|
46
|
+
// interactive picker on a TTY, else `default` (or the first available).
|
|
47
|
+
async function resolveTemplate(): Promise<string> {
|
|
48
|
+
let templates = await listTemplates()
|
|
49
|
+
if (templates.length === 0) {
|
|
50
|
+
console.error(`!! No templates found in ${TEMPLATES_DIR}`)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
let chosen = values.template
|
|
54
|
+
if (chosen) {
|
|
55
|
+
if (!templates.includes(chosen)) {
|
|
56
|
+
console.error(`!! Unknown template "${chosen}"; choose from: ${templates.join(", ")}`)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
return chosen
|
|
60
|
+
}
|
|
61
|
+
if (process.stdin.isTTY) return select("Select a template", templates)
|
|
62
|
+
return templates.includes(DEFAULT_TEMPLATE) ? DEFAULT_TEMPLATE : templates[0]!
|
|
63
|
+
}
|
|
64
|
+
|
|
26
65
|
export async function runInitCommand() {
|
|
27
|
-
// The target folder
|
|
28
|
-
//
|
|
29
|
-
let dir = source
|
|
66
|
+
// The target folder comes from the positional arg, or an interactive prompt
|
|
67
|
+
// (defaulting to a suggested name) when omitted.
|
|
68
|
+
let dir = source
|
|
69
|
+
if (!dir) {
|
|
70
|
+
dir = await text("Project name", DEFAULT_NAME)
|
|
71
|
+
if (!dir) {
|
|
72
|
+
console.error("!! A project name is required")
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The folder must not exist yet, so init can never touch an existing project.
|
|
30
78
|
let existing = await readdir(dir).catch(() => null)
|
|
31
|
-
if (existing
|
|
32
|
-
console.error(`!! ${resolve(dir)} already exists
|
|
79
|
+
if (existing) {
|
|
80
|
+
console.error(`!! ${resolve(dir)} already exists; choose a new folder name`)
|
|
33
81
|
process.exit(1)
|
|
34
82
|
}
|
|
35
83
|
|
|
36
|
-
|
|
84
|
+
let template = await resolveTemplate()
|
|
85
|
+
|
|
86
|
+
console.log(`>> Scaffolding SolidRT project in ${resolve(dir)} (${template})`)
|
|
37
87
|
for (let { from, to } of TEMPLATE_FILES) {
|
|
38
88
|
let dest = join(dir, to)
|
|
39
89
|
await mkdir(dirname(dest), { recursive: true })
|
|
@@ -41,6 +91,15 @@ export async function runInitCommand() {
|
|
|
41
91
|
console.log(` Write ${to}`)
|
|
42
92
|
}
|
|
43
93
|
|
|
94
|
+
// The chosen template's files become the project's src/. Entries may be
|
|
95
|
+
// nested directories (e.g. an asset folder), so copy recursively.
|
|
96
|
+
let templateDir = join(TEMPLATES_DIR, template)
|
|
97
|
+
await mkdir(join(dir, "src"), { recursive: true })
|
|
98
|
+
for (let file of await readdir(templateDir)) {
|
|
99
|
+
await cp(join(templateDir, file), join(dir, "src", file), { recursive: true })
|
|
100
|
+
console.log(` Write src/${file}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
44
103
|
// The scaffold package.json carries a placeholder name; set it from the
|
|
45
104
|
// target folder.
|
|
46
105
|
let pkgPath = join(dir, "package.json")
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createInterface, emitKeypressEvents } from "node:readline"
|
|
2
|
+
|
|
3
|
+
// Single-line text prompt with an optional default (shown in parentheses, used
|
|
4
|
+
// when the answer is blank). Non-TTY stdin resolves the default rather than
|
|
5
|
+
// blocking on input that will never arrive.
|
|
6
|
+
export function text(message: string, def = ""): Promise<string> {
|
|
7
|
+
return new Promise<string>((resolve) => {
|
|
8
|
+
if (!process.stdin.isTTY) return resolve(def)
|
|
9
|
+
let rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
10
|
+
let suffix = def ? ` (${def})` : ""
|
|
11
|
+
rl.question(`? ${message}${suffix}: `, (answer) => {
|
|
12
|
+
rl.close()
|
|
13
|
+
resolve(answer.trim() || def)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Minimal arrow-key single-select prompt, built on node:readline (same
|
|
19
|
+
// dependency-free approach as repl.ts). Renders the option list, moves the
|
|
20
|
+
// highlight on up/down, resolves the chosen value on enter. Callers guard on
|
|
21
|
+
// process.stdin.isTTY; a non-TTY stdin here resolves the first option rather
|
|
22
|
+
// than hanging on input that will never arrive.
|
|
23
|
+
export function select(message: string, options: string[]): Promise<string> {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let input = process.stdin
|
|
26
|
+
let output = process.stdout
|
|
27
|
+
if (!input.isTTY) return resolve(options[0]!)
|
|
28
|
+
|
|
29
|
+
let selected = 0
|
|
30
|
+
emitKeypressEvents(input)
|
|
31
|
+
let wasRaw = input.isRaw
|
|
32
|
+
input.setRawMode(true)
|
|
33
|
+
|
|
34
|
+
let render = (first = false) => {
|
|
35
|
+
// After the first paint the cursor sits below the block; move it back up
|
|
36
|
+
// to the message line so the list redraws in place.
|
|
37
|
+
if (!first) output.write(`\x1b[${options.length + 1}A`)
|
|
38
|
+
output.write(`\x1b[K? ${message}\n`)
|
|
39
|
+
for (let i = 0; i < options.length; i++) {
|
|
40
|
+
let active = i === selected
|
|
41
|
+
let pointer = active ? "\x1b[36m> " : " "
|
|
42
|
+
let reset = active ? "\x1b[0m" : ""
|
|
43
|
+
output.write(`\x1b[K${pointer}${options[i]}${reset}\n`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let cleanup = () => {
|
|
48
|
+
input.off("keypress", onKey)
|
|
49
|
+
input.setRawMode(wasRaw)
|
|
50
|
+
input.pause()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let onKey = (_str: string, key: { name: string; ctrl: boolean } | undefined) => {
|
|
54
|
+
if (!key) return
|
|
55
|
+
if (key.name === "up") {
|
|
56
|
+
selected = (selected - 1 + options.length) % options.length
|
|
57
|
+
render()
|
|
58
|
+
} else if (key.name === "down") {
|
|
59
|
+
selected = (selected + 1) % options.length
|
|
60
|
+
render()
|
|
61
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
62
|
+
cleanup()
|
|
63
|
+
output.write("\n")
|
|
64
|
+
resolve(options[selected]!)
|
|
65
|
+
} else if (key.ctrl && (key.name === "c" || key.name === "d")) {
|
|
66
|
+
cleanup()
|
|
67
|
+
output.write("\n")
|
|
68
|
+
process.exit(130)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
input.on("keypress", onKey)
|
|
73
|
+
input.resume()
|
|
74
|
+
render(true)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
File without changes
|