@solidrt/components 0.0.51 → 0.0.52
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/AGENTS.md +34 -7
- package/README.md +47 -10
- package/demos/README.md +24 -0
- package/demos/assets/icon.png +0 -0
- package/demos/assets/icon.svg +23 -0
- package/demos/package.json +9 -0
- package/demos/src/gallery.tsx +866 -0
- package/demos/tsconfig.json +15 -0
- package/docs/button.md +1 -1
- package/docs/focus-nav.md +1 -1
- package/docs/icon.md +1 -1
- package/docs/item.md +1 -1
- package/docs/policy.md +1 -1
- package/docs/scroll-view.md +22 -1
- package/docs/split-view.md +1 -1
- package/docs/theme.md +14 -2
- package/docs/types.md +4 -0
- package/package.json +4 -3
- package/src/badge.tsx +21 -14
- package/src/button.tsx +13 -7
- package/src/card.tsx +10 -3
- package/src/checkbox.tsx +8 -2
- package/src/context-menu.tsx +9 -6
- package/src/divider.tsx +8 -3
- package/src/editor-field.tsx +18 -11
- package/src/field.tsx +4 -2
- package/src/icon.tsx +8 -4
- package/src/image.tsx +10 -3
- package/src/index.ts +10 -1
- package/src/item.tsx +12 -7
- package/src/nav-shell.tsx +6 -16
- package/src/policy.ts +9 -4
- package/src/pressable.tsx +11 -2
- package/src/progress-bar.tsx +4 -3
- package/src/qrcode.tsx +7 -5
- package/src/radio.tsx +12 -4
- package/src/rich-text-editor.tsx +5 -3
- package/src/scroll-view.tsx +76 -7
- package/src/segmented-control.tsx +14 -5
- package/src/select.tsx +23 -12
- package/src/slider.tsx +31 -5
- package/src/spinner.tsx +5 -2
- package/src/split-view.tsx +3 -4
- package/src/switch.tsx +8 -2
- package/src/text-input.tsx +4 -2
- package/src/text.tsx +22 -2
- package/src/theme.ts +72 -20
- package/src/tooltip.tsx +16 -5
- package/src/types.ts +119 -1
- package/src/view.tsx +11 -2
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
createSignal,
|
|
4
|
+
createEffect,
|
|
5
|
+
untrack,
|
|
6
|
+
env,
|
|
7
|
+
capabilities,
|
|
8
|
+
For,
|
|
9
|
+
Show,
|
|
10
|
+
pct,
|
|
11
|
+
} from "@solidrt/core"
|
|
12
|
+
import {
|
|
13
|
+
policy,
|
|
14
|
+
setPolicy,
|
|
15
|
+
type DensityPolicy,
|
|
16
|
+
type MotionPolicy,
|
|
17
|
+
Window,
|
|
18
|
+
View,
|
|
19
|
+
Text,
|
|
20
|
+
Image,
|
|
21
|
+
Button,
|
|
22
|
+
TextInput,
|
|
23
|
+
Switch,
|
|
24
|
+
Checkbox,
|
|
25
|
+
RadioGroup,
|
|
26
|
+
Radio,
|
|
27
|
+
Slider,
|
|
28
|
+
QrCode,
|
|
29
|
+
Card,
|
|
30
|
+
Divider,
|
|
31
|
+
Badge,
|
|
32
|
+
Spinner,
|
|
33
|
+
ProgressBar,
|
|
34
|
+
Tooltip,
|
|
35
|
+
Select,
|
|
36
|
+
ContextMenu,
|
|
37
|
+
Icon,
|
|
38
|
+
ScrollView,
|
|
39
|
+
SplitView,
|
|
40
|
+
Item,
|
|
41
|
+
Pressable,
|
|
42
|
+
type PressState,
|
|
43
|
+
Modal,
|
|
44
|
+
SegmentedControl,
|
|
45
|
+
Field,
|
|
46
|
+
Density,
|
|
47
|
+
theme,
|
|
48
|
+
space,
|
|
49
|
+
typeWeight,
|
|
50
|
+
setTheme,
|
|
51
|
+
darkTheme,
|
|
52
|
+
lightTheme,
|
|
53
|
+
SafeArea,
|
|
54
|
+
} from "@solidrt/components"
|
|
55
|
+
|
|
56
|
+
import icon from "../assets/icon.png" with { type: "binary" }
|
|
57
|
+
// The puzzle mark, the demos project's app icon: a multi-color SVG keeps its
|
|
58
|
+
// own fills through Icon.
|
|
59
|
+
import mark from "../assets/icon.svg" with { type: "text" }
|
|
60
|
+
|
|
61
|
+
// A gallery of @solidrt/components, itself built from them: a SplitView whose
|
|
62
|
+
// list pane picks a group of components and whose detail pane shows that
|
|
63
|
+
// group's cards. Two-pane on a wide window, one pane at a time (with a Back
|
|
64
|
+
// button) on a narrow one - resize to watch policy.layout flip the shell.
|
|
65
|
+
// The whole thing recolors live when the theme switches: every component
|
|
66
|
+
// reads theme.* reactively, so the "Dark theme" switch reflows the palette
|
|
67
|
+
// with no remount. The "Environment" group shows the environment ->
|
|
68
|
+
// capabilities -> policies cascade and lets you force density/motion
|
|
69
|
+
// overrides to watch every control adapt.
|
|
70
|
+
|
|
71
|
+
// Lucide icons are SVG documents (strings). In a real app you import them as
|
|
72
|
+
// assets (`import House from "lucide-static/icons/house.svg"`) or from a string
|
|
73
|
+
// export; we inline a few here so the example stays self-contained. Each strokes
|
|
74
|
+
// with currentColor, so Icon recolors them from the theme.
|
|
75
|
+
const LUCIDE = (body: string) =>
|
|
76
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"` +
|
|
77
|
+
` stroke="currentColor" stroke-width="2" stroke-linecap="round"` +
|
|
78
|
+
` stroke-linejoin="round">${body}</svg>`
|
|
79
|
+
const HOUSE = LUCIDE(
|
|
80
|
+
`<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/>` +
|
|
81
|
+
`<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"/>`,
|
|
82
|
+
)
|
|
83
|
+
const HEART = LUCIDE(
|
|
84
|
+
`<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"/>`,
|
|
85
|
+
)
|
|
86
|
+
const STAR = LUCIDE(
|
|
87
|
+
`<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"/>`,
|
|
88
|
+
)
|
|
89
|
+
const SETTINGS = LUCIDE(
|
|
90
|
+
`<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"/>` +
|
|
91
|
+
`<circle cx="12" cy="12" r="3"/>`,
|
|
92
|
+
)
|
|
93
|
+
const SUN = LUCIDE(
|
|
94
|
+
`<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/>` +
|
|
95
|
+
`<path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/>` +
|
|
96
|
+
`<path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>`,
|
|
97
|
+
)
|
|
98
|
+
const MOON = LUCIDE(`<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>`)
|
|
99
|
+
const ARROW_LEFT = LUCIDE(`<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>`)
|
|
100
|
+
const BELL = LUCIDE(
|
|
101
|
+
`<path d="M10.268 21a2 2 0 0 0 3.464 0"/>` +
|
|
102
|
+
`<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"/>`,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const TAP_TARGET = 44
|
|
106
|
+
// Breathing room between the list's cards and the scroller's clip edge, so a
|
|
107
|
+
// focus ring on the outermost card is not cut off.
|
|
108
|
+
const LIST_GUTTER = 2
|
|
109
|
+
// Reading widths, as the launcher sets them: single-pane runs up to the
|
|
110
|
+
// expanded breakpoint (840), so past these the column is centered rather than
|
|
111
|
+
// stretched. The detail column is wider than the list's because it holds
|
|
112
|
+
// label-and-value rows and controls, not prose.
|
|
113
|
+
const COLUMN_MAX_WIDTH = 440
|
|
114
|
+
const DETAIL_MAX_WIDTH = 640
|
|
115
|
+
|
|
116
|
+
// An icon-only press target, as the launcher's back arrow: a square tap
|
|
117
|
+
// target with a hover tint and no fill, no label.
|
|
118
|
+
function IconButton(props: { src: string; onPress: () => void }) {
|
|
119
|
+
return (
|
|
120
|
+
<Pressable
|
|
121
|
+
focusable
|
|
122
|
+
onPress={props.onPress}
|
|
123
|
+
layout={{
|
|
124
|
+
width: TAP_TARGET,
|
|
125
|
+
height: TAP_TARGET,
|
|
126
|
+
alignItems: "center",
|
|
127
|
+
justifyContent: "center",
|
|
128
|
+
}}
|
|
129
|
+
style={(s: PressState) => ({
|
|
130
|
+
backgroundColor: s.hovered ? theme.color.overlayHover : "transparent",
|
|
131
|
+
borderRadius: theme.radius.md,
|
|
132
|
+
})}
|
|
133
|
+
>
|
|
134
|
+
<Icon src={props.src} size={22} />
|
|
135
|
+
</Pressable>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A group in the list, shaped like the launcher's app rows: a pressable Card
|
|
140
|
+
// with a title and a muted line, filled while selected or hovered. Only
|
|
141
|
+
// two-pane shows the selection - single-pane shows the list without the
|
|
142
|
+
// detail, and a highlighted row with nothing selected on screen reads as a
|
|
143
|
+
// stray highlight.
|
|
144
|
+
function GroupCard(props: {
|
|
145
|
+
label: string
|
|
146
|
+
description: string
|
|
147
|
+
active: boolean
|
|
148
|
+
onPress: () => void
|
|
149
|
+
}) {
|
|
150
|
+
return (
|
|
151
|
+
<Pressable focusable onPress={props.onPress}>
|
|
152
|
+
{(s: PressState) => (
|
|
153
|
+
<Card
|
|
154
|
+
layout={{ flexDirection: "row", alignItems: "center", gap: space("lg") }}
|
|
155
|
+
style={{
|
|
156
|
+
backgroundColor:
|
|
157
|
+
props.active || s.hovered ? theme.color.surfaceAlt : theme.color.surface,
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
<View layout={{ flexDirection: "column", flexGrow: 1, gap: 2 }}>
|
|
161
|
+
<Text variant="title">{props.label}</Text>
|
|
162
|
+
<Text variant="body" muted>
|
|
163
|
+
{props.description}
|
|
164
|
+
</Text>
|
|
165
|
+
</View>
|
|
166
|
+
</Card>
|
|
167
|
+
)}
|
|
168
|
+
</Pressable>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function Row(props: { label: string; children?: any }) {
|
|
173
|
+
return (
|
|
174
|
+
<View
|
|
175
|
+
layout={{
|
|
176
|
+
flexDirection: "row",
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
justifyContent: "space-between",
|
|
179
|
+
gap: space("lg"),
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<Text muted>{props.label}</Text>
|
|
183
|
+
{props.children}
|
|
184
|
+
</View>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function Value(props: { children?: any }) {
|
|
189
|
+
return <Text>{props.children}</Text>
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Joins the names whose flag is true, e.g. list(["mouse", env.mouseSeen], ...).
|
|
193
|
+
function list(...entries: [string, boolean][]) {
|
|
194
|
+
let names = entries.filter(([, on]) => on).map(([name]) => name)
|
|
195
|
+
return names.length > 0 ? names.join(", ") : "none yet"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function App() {
|
|
199
|
+
let [dark, setDark] = createSignal(true)
|
|
200
|
+
let [name, setName] = createSignal("")
|
|
201
|
+
let [notify, setNotify] = createSignal(true)
|
|
202
|
+
let [agree, setAgree] = createSignal(false)
|
|
203
|
+
let [plan, setPlan] = createSignal<unknown>("pro")
|
|
204
|
+
let [volume, setVolume] = createSignal(40)
|
|
205
|
+
let [fruit, setFruit] = createSignal<unknown>(undefined)
|
|
206
|
+
let [notes, setNotes] = createSignal("")
|
|
207
|
+
let [fixedNotes, setFixedNotes] = createSignal("")
|
|
208
|
+
let [range, setRange] = createSignal<unknown>("week")
|
|
209
|
+
// Starts false: a Modal is a portal, and portals cannot mount during the
|
|
210
|
+
// initial render.
|
|
211
|
+
let [modalOpen, setModalOpen] = createSignal(false)
|
|
212
|
+
let [rowDensity, setRowDensity] = createSignal<unknown>("comfortable")
|
|
213
|
+
let [currentIssue, setCurrentIssue] = createSignal("SRT-12")
|
|
214
|
+
let issues = [
|
|
215
|
+
{ id: "SRT-12", title: "Tooltip lingers after the anchor unmounts", assignee: "Antoine" },
|
|
216
|
+
{ id: "SRT-15", title: "Select sheet ignores safe area on Android", assignee: "unassigned" },
|
|
217
|
+
{ id: "SRT-19", title: "Slider thumb misses the first pixel", assignee: "Antoine" },
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
// The QR code follows the name field debounced, not per keystroke: each data
|
|
221
|
+
// change regenerates the whole module grid, which is too heavy to run at
|
|
222
|
+
// typing frequency.
|
|
223
|
+
// untrack: seed with the current name once; the debounced effect below owns
|
|
224
|
+
// keeping it in sync, and a bare name() read here would warn (strict mode
|
|
225
|
+
// flags top-level reactive reads in component bodies as one-shot).
|
|
226
|
+
let [qrData, setQrData] = createSignal(untrack(name))
|
|
227
|
+
createEffect(
|
|
228
|
+
() => name(),
|
|
229
|
+
(v) => {
|
|
230
|
+
let timer = setTimeout(() => setQrData(v), 250)
|
|
231
|
+
return () => clearTimeout(timer)
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
// Follow the OS dark/light preference until the user toggles manually; the
|
|
236
|
+
// manual toggle then owns the choice for the rest of the session.
|
|
237
|
+
let userToggledTheme = false
|
|
238
|
+
let toggleTheme = () => {
|
|
239
|
+
userToggledTheme = true
|
|
240
|
+
let on = !dark()
|
|
241
|
+
setDark(on)
|
|
242
|
+
setTheme(on ? darkTheme : lightTheme)
|
|
243
|
+
}
|
|
244
|
+
createEffect(
|
|
245
|
+
() => env.systemTheme,
|
|
246
|
+
(t) => {
|
|
247
|
+
if (userToggledTheme || t === "unknown") return
|
|
248
|
+
let isDark = t === "dark"
|
|
249
|
+
setDark(isDark)
|
|
250
|
+
setTheme(isDark ? darkTheme : lightTheme)
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
// Policy override choices; "auto" hands the policy back to the resolver.
|
|
255
|
+
let [densityChoice, setDensityChoice] = createSignal<unknown>("auto")
|
|
256
|
+
let [motionChoice, setMotionChoice] = createSignal<unknown>("auto")
|
|
257
|
+
let chooseDensity = (v: unknown) => {
|
|
258
|
+
setDensityChoice(v)
|
|
259
|
+
setPolicy({ density: v === "auto" ? undefined : (v as DensityPolicy) })
|
|
260
|
+
}
|
|
261
|
+
let chooseMotion = (v: unknown) => {
|
|
262
|
+
setMotionChoice(v)
|
|
263
|
+
setPolicy({ motion: v === "auto" ? undefined : (v as MotionPolicy) })
|
|
264
|
+
}
|
|
265
|
+
let [textScaleChoice, setTextScaleChoice] = createSignal<unknown>("auto")
|
|
266
|
+
let chooseTextScale = (v: unknown) => {
|
|
267
|
+
setTextScaleChoice(v)
|
|
268
|
+
setPolicy({ textScale: v === "auto" ? undefined : (v as number) })
|
|
269
|
+
}
|
|
270
|
+
let [weightDeltaChoice, setWeightDeltaChoice] = createSignal<unknown>("auto")
|
|
271
|
+
let chooseWeightDelta = (v: unknown) => {
|
|
272
|
+
setWeightDeltaChoice(v)
|
|
273
|
+
setPolicy({ textWeightDelta: v === "auto" ? undefined : (v as number) })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The gallery's groups: one list row each, one set of cards each. The
|
|
277
|
+
// cards read the signals above, so a group's content is built fresh each
|
|
278
|
+
// time it is shown (the detail reads content() once per selection) and
|
|
279
|
+
// picks up the current state.
|
|
280
|
+
let sections = [
|
|
281
|
+
{
|
|
282
|
+
value: "environment",
|
|
283
|
+
label: "Environment",
|
|
284
|
+
description: "Facts, capabilities and policies",
|
|
285
|
+
content: () => (
|
|
286
|
+
<>
|
|
287
|
+
<Card title="Environment">
|
|
288
|
+
<Row label="Window">
|
|
289
|
+
<Value>
|
|
290
|
+
{`${env.windowSize.width} x ${env.windowSize.height} @ ${env.displayScale}x`}
|
|
291
|
+
</Value>
|
|
292
|
+
</Row>
|
|
293
|
+
<Row label="System">
|
|
294
|
+
<Value>{`${env.systemTheme} theme, ${env.orientation}`}</Value>
|
|
295
|
+
</Row>
|
|
296
|
+
<Row label="Devices">
|
|
297
|
+
<Value>
|
|
298
|
+
{env.inputDevices
|
|
299
|
+
? list(
|
|
300
|
+
["mouse", env.inputDevices.mouse],
|
|
301
|
+
["touch", env.inputDevices.touch],
|
|
302
|
+
["keyboard", env.inputDevices.keyboard],
|
|
303
|
+
)
|
|
304
|
+
: "not reported"}
|
|
305
|
+
</Value>
|
|
306
|
+
</Row>
|
|
307
|
+
<Row label="Inputs seen">
|
|
308
|
+
<Value>
|
|
309
|
+
{list(
|
|
310
|
+
["mouse", env.mouseSeen],
|
|
311
|
+
["touch", env.touchSeen],
|
|
312
|
+
["keyboard", env.keyboardSeen],
|
|
313
|
+
)}
|
|
314
|
+
</Value>
|
|
315
|
+
</Row>
|
|
316
|
+
<Row label="Capabilities">
|
|
317
|
+
<Value>
|
|
318
|
+
{list(
|
|
319
|
+
["hover", capabilities.hover],
|
|
320
|
+
["touch", capabilities.touch],
|
|
321
|
+
["keyboard nav", capabilities.keyboardNav],
|
|
322
|
+
)}
|
|
323
|
+
</Value>
|
|
324
|
+
</Row>
|
|
325
|
+
<Row label="Size class">
|
|
326
|
+
<Value>{capabilities.windowSizeClass}</Value>
|
|
327
|
+
</Row>
|
|
328
|
+
</Card>
|
|
329
|
+
<Card title="Policies">
|
|
330
|
+
<Row label="Interaction policy">
|
|
331
|
+
<Value>{policy.interaction}</Value>
|
|
332
|
+
</Row>
|
|
333
|
+
<Row label="Focus ring">
|
|
334
|
+
<Value>{policy.focusRing ? "visible" : "hidden"}</Value>
|
|
335
|
+
</Row>
|
|
336
|
+
<Row label="App policies">
|
|
337
|
+
<Value>{`${policy.navigation}, ${policy.layout}`}</Value>
|
|
338
|
+
</Row>
|
|
339
|
+
<Divider />
|
|
340
|
+
<Row label="Density policy">
|
|
341
|
+
<Value>{policy.density}</Value>
|
|
342
|
+
</Row>
|
|
343
|
+
<RadioGroup
|
|
344
|
+
value={densityChoice()}
|
|
345
|
+
onChange={chooseDensity}
|
|
346
|
+
layout={{ flexDirection: "row", flexWrap: "wrap", gap: space("md") }}
|
|
347
|
+
>
|
|
348
|
+
<Radio value="auto">Auto</Radio>
|
|
349
|
+
<Radio value="comfortable">Comfortable</Radio>
|
|
350
|
+
<Radio value="compact">Compact</Radio>
|
|
351
|
+
<Radio value="dense">Dense</Radio>
|
|
352
|
+
</RadioGroup>
|
|
353
|
+
<Divider />
|
|
354
|
+
<Row label="Motion policy">
|
|
355
|
+
<Value>{policy.motion}</Value>
|
|
356
|
+
</Row>
|
|
357
|
+
<RadioGroup
|
|
358
|
+
value={motionChoice()}
|
|
359
|
+
onChange={chooseMotion}
|
|
360
|
+
layout={{ flexDirection: "row", flexWrap: "wrap", gap: space("md") }}
|
|
361
|
+
>
|
|
362
|
+
<Radio value="auto">Auto</Radio>
|
|
363
|
+
<Radio value="normal">Normal</Radio>
|
|
364
|
+
<Radio value="reduced">Reduced</Radio>
|
|
365
|
+
<Radio value="none">None</Radio>
|
|
366
|
+
</RadioGroup>
|
|
367
|
+
<Divider />
|
|
368
|
+
<Row label="Text scale">
|
|
369
|
+
<Value>{`${policy.textScale}x`}</Value>
|
|
370
|
+
</Row>
|
|
371
|
+
<RadioGroup
|
|
372
|
+
value={textScaleChoice()}
|
|
373
|
+
onChange={chooseTextScale}
|
|
374
|
+
layout={{ flexDirection: "row", flexWrap: "wrap", gap: space("md") }}
|
|
375
|
+
>
|
|
376
|
+
<Radio value="auto">Auto</Radio>
|
|
377
|
+
<Radio value={0.9}>0.9</Radio>
|
|
378
|
+
<Radio value={1.0}>1.0</Radio>
|
|
379
|
+
<Radio value={1.1}>1.1</Radio>
|
|
380
|
+
</RadioGroup>
|
|
381
|
+
<Divider />
|
|
382
|
+
<Row label="Text weight delta">
|
|
383
|
+
<Value>
|
|
384
|
+
{`base +${policy.textWeightDelta}, body ${typeWeight(
|
|
385
|
+
theme.text.body.weight,
|
|
386
|
+
theme.text.body.size * policy.textScale,
|
|
387
|
+
)}, title ${typeWeight(theme.text.title.weight, theme.text.title.size * policy.textScale)}`}
|
|
388
|
+
</Value>
|
|
389
|
+
</Row>
|
|
390
|
+
<RadioGroup
|
|
391
|
+
value={weightDeltaChoice()}
|
|
392
|
+
onChange={chooseWeightDelta}
|
|
393
|
+
layout={{ flexDirection: "row", flexWrap: "wrap", gap: space("md") }}
|
|
394
|
+
>
|
|
395
|
+
<Radio value="auto">Auto</Radio>
|
|
396
|
+
<Radio value={0}>0</Radio>
|
|
397
|
+
<Radio value={100}>+100</Radio>
|
|
398
|
+
<Radio value={200}>+200</Radio>
|
|
399
|
+
</RadioGroup>
|
|
400
|
+
</Card>
|
|
401
|
+
</>
|
|
402
|
+
),
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
value: "buttons",
|
|
406
|
+
label: "Buttons and badges",
|
|
407
|
+
description: "Buttons, badges, dividers, icons",
|
|
408
|
+
content: () => (
|
|
409
|
+
<>
|
|
410
|
+
<Card title="Buttons">
|
|
411
|
+
<View
|
|
412
|
+
layout={{
|
|
413
|
+
flexDirection: "row",
|
|
414
|
+
flexWrap: "wrap",
|
|
415
|
+
gap: space("md"),
|
|
416
|
+
alignItems: "center",
|
|
417
|
+
}}
|
|
418
|
+
>
|
|
419
|
+
<Button>Primary</Button>
|
|
420
|
+
<Button variant="secondary">Secondary</Button>
|
|
421
|
+
<Button variant="ghost">Ghost</Button>
|
|
422
|
+
<Button variant="danger">Danger</Button>
|
|
423
|
+
<Button disabled>Disabled</Button>
|
|
424
|
+
</View>
|
|
425
|
+
</Card>
|
|
426
|
+
<Card title="Badges and divider">
|
|
427
|
+
<Row label="Status">
|
|
428
|
+
<View layout={{ flexDirection: "row", gap: space("md"), alignItems: "center" }}>
|
|
429
|
+
<Badge>New</Badge>
|
|
430
|
+
<Badge variant="neutral">3</Badge>
|
|
431
|
+
<Badge variant="danger">Error</Badge>
|
|
432
|
+
</View>
|
|
433
|
+
</Row>
|
|
434
|
+
<Divider />
|
|
435
|
+
<Text muted>A divider separates content within a card.</Text>
|
|
436
|
+
</Card>
|
|
437
|
+
<Card title="Pressable">
|
|
438
|
+
<Text muted>
|
|
439
|
+
The primitive under every button: style and children can read the live press state.
|
|
440
|
+
</Text>
|
|
441
|
+
<Pressable
|
|
442
|
+
onPress={() => console.log("pressed")}
|
|
443
|
+
layout={{ padding: space("lg"), alignItems: "center" }}
|
|
444
|
+
style={(s: PressState) => ({
|
|
445
|
+
backgroundColor: s.pressed
|
|
446
|
+
? theme.color.primary
|
|
447
|
+
: s.hovered
|
|
448
|
+
? theme.color.surfaceAlt
|
|
449
|
+
: theme.color.surface,
|
|
450
|
+
borderRadius: theme.radius.md,
|
|
451
|
+
})}
|
|
452
|
+
>
|
|
453
|
+
{(s: PressState) => (
|
|
454
|
+
<Text color={s.pressed ? "onPrimary" : "text"}>
|
|
455
|
+
{s.pressed ? "Pressed" : s.hovered ? "Hovered" : "Idle"}
|
|
456
|
+
</Text>
|
|
457
|
+
)}
|
|
458
|
+
</Pressable>
|
|
459
|
+
</Card>
|
|
460
|
+
<Card title="Icons">
|
|
461
|
+
<Text muted>
|
|
462
|
+
Lucide SVGs parsed by the core parseSvg primitive into d-path draws. currentColor
|
|
463
|
+
follows the theme; the last two are recolored explicitly.
|
|
464
|
+
</Text>
|
|
465
|
+
<View layout={{ flexDirection: "row", gap: space("lg"), alignItems: "center" }}>
|
|
466
|
+
<Icon src={HOUSE} />
|
|
467
|
+
<Icon src={SETTINGS} />
|
|
468
|
+
<Icon src={BELL} />
|
|
469
|
+
<Icon src={HEART} color={theme.color.danger} />
|
|
470
|
+
<Icon src={STAR} color={theme.color.primary} />
|
|
471
|
+
</View>
|
|
472
|
+
</Card>
|
|
473
|
+
</>
|
|
474
|
+
),
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
value: "selection",
|
|
478
|
+
label: "Selection",
|
|
479
|
+
description: "Toggles, radio, segments, select",
|
|
480
|
+
content: () => (
|
|
481
|
+
<>
|
|
482
|
+
<Card title="Switch and checkbox">
|
|
483
|
+
<Row label="Notifications">
|
|
484
|
+
<Switch value={notify()} onChange={setNotify} />
|
|
485
|
+
</Row>
|
|
486
|
+
<Row label="I agree to the terms">
|
|
487
|
+
<Checkbox checked={agree()} onChange={setAgree} />
|
|
488
|
+
</Row>
|
|
489
|
+
</Card>
|
|
490
|
+
<Card title="Radio group">
|
|
491
|
+
<RadioGroup value={plan()} onChange={setPlan}>
|
|
492
|
+
<Radio value="free">Free</Radio>
|
|
493
|
+
<Radio value="pro">Pro</Radio>
|
|
494
|
+
<Radio value="team">Team</Radio>
|
|
495
|
+
</RadioGroup>
|
|
496
|
+
</Card>
|
|
497
|
+
<Card title="Segmented control">
|
|
498
|
+
<SegmentedControl
|
|
499
|
+
options={[
|
|
500
|
+
{ value: "day", label: "Day" },
|
|
501
|
+
{ value: "week", label: "Week" },
|
|
502
|
+
{ value: "month", label: "Month" },
|
|
503
|
+
]}
|
|
504
|
+
value={range()}
|
|
505
|
+
onChange={setRange}
|
|
506
|
+
/>
|
|
507
|
+
</Card>
|
|
508
|
+
<Card title="Select">
|
|
509
|
+
<Text muted>
|
|
510
|
+
Forks on the interaction policy: desktop and hybrid anchor a dropdown, touch opens a
|
|
511
|
+
bottom sheet.
|
|
512
|
+
</Text>
|
|
513
|
+
<Select
|
|
514
|
+
value={fruit()}
|
|
515
|
+
onChange={setFruit}
|
|
516
|
+
placeholder="Pick a fruit"
|
|
517
|
+
options={[
|
|
518
|
+
{ value: "apple", label: "Apple" },
|
|
519
|
+
{ value: "banana", label: "Banana" },
|
|
520
|
+
{ value: "cherry", label: "Cherry" },
|
|
521
|
+
{ value: "dragonfruit", label: "Dragonfruit" },
|
|
522
|
+
]}
|
|
523
|
+
layout={{ width: 200 }}
|
|
524
|
+
/>
|
|
525
|
+
</Card>
|
|
526
|
+
</>
|
|
527
|
+
),
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
value: "text",
|
|
531
|
+
label: "Text",
|
|
532
|
+
description: "A formatted paragraph and text fields",
|
|
533
|
+
content: () => (
|
|
534
|
+
<>
|
|
535
|
+
<Card title="Paragraph">
|
|
536
|
+
<Text>
|
|
537
|
+
Lorem ipsum dolor sit amet,{" "}
|
|
538
|
+
<span textDecoration="underline">consectetur adipiscing elit</span>, sed do eiusmod
|
|
539
|
+
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
|
|
540
|
+
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis
|
|
541
|
+
aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
|
542
|
+
nulla pariatur. Excepteur sint occaecat cupidatat non proident, <span fontStyle="italic">sunt in culpa qui
|
|
543
|
+
officia deserunt mollit anim id est laborum</span>.
|
|
544
|
+
</Text>
|
|
545
|
+
</Card>
|
|
546
|
+
<Card title="Text input">
|
|
547
|
+
<Field label="Single line" description="Enter submits.">
|
|
548
|
+
<TextInput
|
|
549
|
+
value={name()}
|
|
550
|
+
onInput={setName}
|
|
551
|
+
onSubmit={(v) => console.log("submitted", v)}
|
|
552
|
+
placeholder="Your name"
|
|
553
|
+
layout={{ width: pct(100) }}
|
|
554
|
+
/>
|
|
555
|
+
</Field>
|
|
556
|
+
<Field
|
|
557
|
+
label="Multiline"
|
|
558
|
+
description="A layout.height pins the box; it scrolls to the caret."
|
|
559
|
+
>
|
|
560
|
+
<TextInput
|
|
561
|
+
multiline
|
|
562
|
+
value={fixedNotes()}
|
|
563
|
+
onInput={setFixedNotes}
|
|
564
|
+
placeholder="Notes in a fixed box"
|
|
565
|
+
layout={{ width: pct(100), height: 72 }}
|
|
566
|
+
/>
|
|
567
|
+
</Field>
|
|
568
|
+
<Field
|
|
569
|
+
label="Multiline, auto-grow"
|
|
570
|
+
description="Grows with the text up to 4 rows, then scrolls."
|
|
571
|
+
>
|
|
572
|
+
<TextInput
|
|
573
|
+
multiline
|
|
574
|
+
maxRows={4}
|
|
575
|
+
value={notes()}
|
|
576
|
+
onInput={setNotes}
|
|
577
|
+
placeholder="Notes"
|
|
578
|
+
layout={{ width: pct(100) }}
|
|
579
|
+
/>
|
|
580
|
+
</Field>
|
|
581
|
+
</Card>
|
|
582
|
+
</>
|
|
583
|
+
),
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
value: "slider",
|
|
587
|
+
label: "Slider and progress",
|
|
588
|
+
description: "Slider, progress bar, spinner",
|
|
589
|
+
content: () => (
|
|
590
|
+
<>
|
|
591
|
+
<Card title="Slider">
|
|
592
|
+
<Row label={`Volume: ${Math.round(volume())}`}>
|
|
593
|
+
<Slider
|
|
594
|
+
value={volume()}
|
|
595
|
+
onChange={setVolume}
|
|
596
|
+
min={0}
|
|
597
|
+
max={100}
|
|
598
|
+
step={1}
|
|
599
|
+
layout={{ width: 180 }}
|
|
600
|
+
/>
|
|
601
|
+
</Row>
|
|
602
|
+
</Card>
|
|
603
|
+
<Card title="Progress and spinner">
|
|
604
|
+
<Row label="Loading">
|
|
605
|
+
<Spinner />
|
|
606
|
+
</Row>
|
|
607
|
+
<Text muted>Determinate, tracking the volume slider:</Text>
|
|
608
|
+
<ProgressBar value={volume() / 100} />
|
|
609
|
+
<Text muted>Indeterminate:</Text>
|
|
610
|
+
<ProgressBar />
|
|
611
|
+
</Card>
|
|
612
|
+
</>
|
|
613
|
+
),
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
value: "overlays",
|
|
617
|
+
label: "Overlays",
|
|
618
|
+
description: "Context menu, tooltip and modal",
|
|
619
|
+
content: () => (
|
|
620
|
+
<>
|
|
621
|
+
<Card title="Context menu">
|
|
622
|
+
<Text muted>
|
|
623
|
+
Right-click (mouse) or long-press (touch) the box below. Touch policy presents a
|
|
624
|
+
bottom sheet, desktop and hybrid a menu at the pointer.
|
|
625
|
+
</Text>
|
|
626
|
+
<ContextMenu
|
|
627
|
+
items={[
|
|
628
|
+
{ label: "Copy", onSelect: () => console.log("copy") },
|
|
629
|
+
{ label: "Paste", onSelect: () => console.log("paste") },
|
|
630
|
+
{ label: "Rename", disabled: true },
|
|
631
|
+
{ label: "Delete", onSelect: () => console.log("delete") },
|
|
632
|
+
]}
|
|
633
|
+
>
|
|
634
|
+
<View
|
|
635
|
+
layout={{ padding: space("lg"), alignItems: "center" }}
|
|
636
|
+
style={{
|
|
637
|
+
backgroundColor: theme.color.surfaceAlt,
|
|
638
|
+
borderRadius: theme.radius.sm,
|
|
639
|
+
}}
|
|
640
|
+
>
|
|
641
|
+
<Text>Secondary actions live here</Text>
|
|
642
|
+
</View>
|
|
643
|
+
</ContextMenu>
|
|
644
|
+
</Card>
|
|
645
|
+
<Card title="Modal">
|
|
646
|
+
<Text muted>
|
|
647
|
+
A centered card over a scrim, portaled to the window root. The backdrop closes it.
|
|
648
|
+
</Text>
|
|
649
|
+
<Button onPress={() => setModalOpen(true)}>Open modal</Button>
|
|
650
|
+
<Show when={modalOpen()}>
|
|
651
|
+
<Modal onClose={() => setModalOpen(false)}>
|
|
652
|
+
<Card title="Modal" layout={{ width: 360 }}>
|
|
653
|
+
<Text muted>Focus navigation stays inside while this is open.</Text>
|
|
654
|
+
<Button onPress={() => setModalOpen(false)}>Close</Button>
|
|
655
|
+
</Card>
|
|
656
|
+
</Modal>
|
|
657
|
+
</Show>
|
|
658
|
+
</Card>
|
|
659
|
+
<Card title="Tooltip">
|
|
660
|
+
<Text muted>
|
|
661
|
+
Rest the mouse on a button. Shows under desktop and hybrid interaction policies, never
|
|
662
|
+
under touch.
|
|
663
|
+
</Text>
|
|
664
|
+
<View layout={{ flexDirection: "row", gap: space("md") }}>
|
|
665
|
+
<Tooltip content="Saves your changes">
|
|
666
|
+
<Button>Hover me</Button>
|
|
667
|
+
</Tooltip>
|
|
668
|
+
<Tooltip content="Appears below the anchor" placement="bottom">
|
|
669
|
+
<Button>Below</Button>
|
|
670
|
+
</Tooltip>
|
|
671
|
+
</View>
|
|
672
|
+
</Card>
|
|
673
|
+
</>
|
|
674
|
+
),
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
value: "lists",
|
|
678
|
+
label: "Lists",
|
|
679
|
+
description: "Item rows under a Density region",
|
|
680
|
+
content: () => (
|
|
681
|
+
<>
|
|
682
|
+
<Card title="Items">
|
|
683
|
+
<Text muted>
|
|
684
|
+
A column of Item rows is the list; the segments set the Density of the region around
|
|
685
|
+
them.
|
|
686
|
+
</Text>
|
|
687
|
+
<SegmentedControl
|
|
688
|
+
options={[
|
|
689
|
+
{ value: "comfortable", label: "Comfortable" },
|
|
690
|
+
{ value: "compact", label: "Compact" },
|
|
691
|
+
{ value: "dense", label: "Dense" },
|
|
692
|
+
]}
|
|
693
|
+
value={rowDensity()}
|
|
694
|
+
onChange={setRowDensity}
|
|
695
|
+
/>
|
|
696
|
+
<Density value={rowDensity() as DensityPolicy}>
|
|
697
|
+
<View layout={{ flexDirection: "column" }}>
|
|
698
|
+
<For each={issues}>
|
|
699
|
+
{(issue) => (
|
|
700
|
+
<Item
|
|
701
|
+
startContent={<Icon src={BELL} />}
|
|
702
|
+
label={issue.title}
|
|
703
|
+
description={issue.assignee}
|
|
704
|
+
endContent={<Badge variant="neutral">{issue.id}</Badge>}
|
|
705
|
+
selected={issue.id === currentIssue()}
|
|
706
|
+
onPress={() => setCurrentIssue(issue.id)}
|
|
707
|
+
/>
|
|
708
|
+
)}
|
|
709
|
+
</For>
|
|
710
|
+
<Item
|
|
711
|
+
label="Disabled row"
|
|
712
|
+
description="Takes no pointer events"
|
|
713
|
+
disabled
|
|
714
|
+
onPress={() => {}}
|
|
715
|
+
/>
|
|
716
|
+
</View>
|
|
717
|
+
</Density>
|
|
718
|
+
</Card>
|
|
719
|
+
</>
|
|
720
|
+
),
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
value: "misc",
|
|
724
|
+
label: "Misc",
|
|
725
|
+
description: "QR code and image",
|
|
726
|
+
content: () => (
|
|
727
|
+
<>
|
|
728
|
+
<Card title="QR code">
|
|
729
|
+
<View layout={{ alignItems: "center" }}>
|
|
730
|
+
<QrCode data="https://solidjs.com" />
|
|
731
|
+
</View>
|
|
732
|
+
</Card>
|
|
733
|
+
<Card title="Image">
|
|
734
|
+
<Text muted>
|
|
735
|
+
Fetches, decodes, and uploads an image, then shows it as a GPU texture. Rounded via a
|
|
736
|
+
clipping border radius.
|
|
737
|
+
</Text>
|
|
738
|
+
<View layout={{ alignItems: "center" }}>
|
|
739
|
+
<Image src={icon} style={{ borderRadius: theme.radius.md }} />
|
|
740
|
+
</View>
|
|
741
|
+
</Card>
|
|
742
|
+
</>
|
|
743
|
+
),
|
|
744
|
+
},
|
|
745
|
+
]
|
|
746
|
+
|
|
747
|
+
// Selection lives here, not in the panes: SplitView remounts them when the
|
|
748
|
+
// layout policy crosses a breakpoint. showDetail only matters single-pane.
|
|
749
|
+
// Nothing starts selected: a preselected row with no visible detail reads
|
|
750
|
+
// as a stray highlight in single-pane.
|
|
751
|
+
let [current, setCurrent] = createSignal<string | undefined>(undefined)
|
|
752
|
+
let [showDetail, setShowDetail] = createSignal(false)
|
|
753
|
+
let open = (value: string) => {
|
|
754
|
+
setCurrent(value)
|
|
755
|
+
setShowDetail(true)
|
|
756
|
+
}
|
|
757
|
+
let section = () => sections.find((s) => s.value === current())
|
|
758
|
+
let twoPane = () => policy.layout === "twoPane"
|
|
759
|
+
|
|
760
|
+
return (
|
|
761
|
+
<Window
|
|
762
|
+
title="Components gallery"
|
|
763
|
+
layout={{ flexDirection: "column" }}
|
|
764
|
+
style={{ backgroundColor: theme.color.background }}
|
|
765
|
+
>
|
|
766
|
+
<SafeArea>
|
|
767
|
+
<SplitView
|
|
768
|
+
layout={{ flex: 1 }}
|
|
769
|
+
listWidth={380}
|
|
770
|
+
showDetail={showDetail()}
|
|
771
|
+
list={
|
|
772
|
+
<View layout={{ flexGrow: 1, flexDirection: "column", alignItems: "center" }}>
|
|
773
|
+
<View
|
|
774
|
+
layout={{
|
|
775
|
+
flexDirection: "column",
|
|
776
|
+
flexGrow: 1,
|
|
777
|
+
width: pct(100),
|
|
778
|
+
maxWidth: twoPane() ? undefined : COLUMN_MAX_WIDTH,
|
|
779
|
+
padding: space("xl"),
|
|
780
|
+
gap: space("xl"),
|
|
781
|
+
}}
|
|
782
|
+
>
|
|
783
|
+
<View
|
|
784
|
+
layout={{
|
|
785
|
+
flexDirection: "row",
|
|
786
|
+
justifyContent: "space-between",
|
|
787
|
+
alignItems: "center",
|
|
788
|
+
}}
|
|
789
|
+
>
|
|
790
|
+
<View layout={{ flexDirection: "row", alignItems: "center", gap: space("md") }}>
|
|
791
|
+
<Icon src={mark} size={40} />
|
|
792
|
+
<Text variant="heading">Gallery</Text>
|
|
793
|
+
</View>
|
|
794
|
+
<Tooltip content={dark() ? "Light theme" : "Dark theme"}>
|
|
795
|
+
<IconButton src={dark() ? SUN : MOON} onPress={toggleTheme} />
|
|
796
|
+
</Tooltip>
|
|
797
|
+
</View>
|
|
798
|
+
<ScrollView layout={{ flexGrow: 1 }}>
|
|
799
|
+
<View
|
|
800
|
+
layout={{ flexDirection: "column", gap: space("md"), padding: LIST_GUTTER }}
|
|
801
|
+
>
|
|
802
|
+
<For each={sections}>
|
|
803
|
+
{(s) => (
|
|
804
|
+
<GroupCard
|
|
805
|
+
label={s.label}
|
|
806
|
+
description={s.description}
|
|
807
|
+
active={twoPane() && s.value === current()}
|
|
808
|
+
onPress={() => open(s.value)}
|
|
809
|
+
/>
|
|
810
|
+
)}
|
|
811
|
+
</For>
|
|
812
|
+
</View>
|
|
813
|
+
</ScrollView>
|
|
814
|
+
</View>
|
|
815
|
+
</View>
|
|
816
|
+
}
|
|
817
|
+
detail={
|
|
818
|
+
<Show
|
|
819
|
+
when={section()}
|
|
820
|
+
fallback={
|
|
821
|
+
<View layout={{ flexGrow: 1, alignItems: "center", justifyContent: "center" }}>
|
|
822
|
+
<Text variant="heading" muted>
|
|
823
|
+
Pick a group
|
|
824
|
+
</Text>
|
|
825
|
+
</View>
|
|
826
|
+
}
|
|
827
|
+
>
|
|
828
|
+
{(s) => (
|
|
829
|
+
<ScrollView layout={{ flexGrow: 1 }}>
|
|
830
|
+
<View layout={{ flexGrow: 1, alignItems: twoPane() ? "flex-start" : "center" }}>
|
|
831
|
+
<View
|
|
832
|
+
layout={{
|
|
833
|
+
flexDirection: "column",
|
|
834
|
+
gap: space("lg"),
|
|
835
|
+
width: pct(100),
|
|
836
|
+
maxWidth: DETAIL_MAX_WIDTH,
|
|
837
|
+
padding: space("xl"),
|
|
838
|
+
}}
|
|
839
|
+
>
|
|
840
|
+
<View
|
|
841
|
+
layout={{ flexDirection: "row", alignItems: "center", gap: space("md") }}
|
|
842
|
+
>
|
|
843
|
+
<Show when={!twoPane()}>
|
|
844
|
+
<IconButton src={ARROW_LEFT} onPress={() => setShowDetail(false)} />
|
|
845
|
+
</Show>
|
|
846
|
+
<View layout={{ flexDirection: "column", flexGrow: 1, gap: 2 }}>
|
|
847
|
+
<Text variant="heading">{s().label}</Text>
|
|
848
|
+
<Text variant="body" muted>
|
|
849
|
+
{s().description}
|
|
850
|
+
</Text>
|
|
851
|
+
</View>
|
|
852
|
+
</View>
|
|
853
|
+
{s().content()}
|
|
854
|
+
</View>
|
|
855
|
+
</View>
|
|
856
|
+
</ScrollView>
|
|
857
|
+
)}
|
|
858
|
+
</Show>
|
|
859
|
+
}
|
|
860
|
+
/>
|
|
861
|
+
</SafeArea>
|
|
862
|
+
</Window>
|
|
863
|
+
)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
render(() => <App />)
|