ugly-app 0.1.76 → 0.1.77
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/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.77";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -19,4 +19,5 @@ export const allPages = {
|
|
|
19
19
|
['user/:userId']: lazyPage(() => import('./pages/UserPage')),
|
|
20
20
|
['search']: lazyPage(() => import('./pages/SearchPage')),
|
|
21
21
|
['ai-test']: lazyPage(() => import('./pages/AITestPage')),
|
|
22
|
+
['ui-components']: lazyPage(() => import('./pages/UIComponentsPage')),
|
|
22
23
|
} satisfies PageMap<AppPages>;
|
|
@@ -185,6 +185,11 @@ function HomePageBody({
|
|
|
185
185
|
label: 'AI Test',
|
|
186
186
|
desc: 'Text & image generation models',
|
|
187
187
|
},
|
|
188
|
+
{
|
|
189
|
+
href: '/ui-components',
|
|
190
|
+
label: 'UI Components',
|
|
191
|
+
desc: 'Live demos of all built-in components',
|
|
192
|
+
},
|
|
188
193
|
].map(({ href, label, desc }) => (
|
|
189
194
|
<a key={href} href={href} className="block group">
|
|
190
195
|
<Card className="transition-shadow group-hover:shadow-md">
|
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
// Layout
|
|
4
|
+
PageLayout,
|
|
5
|
+
ScrollView,
|
|
6
|
+
SimpleScrollView,
|
|
7
|
+
Text,
|
|
8
|
+
View,
|
|
9
|
+
Card,
|
|
10
|
+
Panel,
|
|
11
|
+
PopupPanel,
|
|
12
|
+
Image,
|
|
13
|
+
// Inputs
|
|
14
|
+
Button,
|
|
15
|
+
Input,
|
|
16
|
+
ValidatedTextInput,
|
|
17
|
+
AnimatedInputWrapper,
|
|
18
|
+
EnumInput,
|
|
19
|
+
DatePicker,
|
|
20
|
+
DateTimePicker,
|
|
21
|
+
DateRangePicker,
|
|
22
|
+
WheelPicker,
|
|
23
|
+
SelectView,
|
|
24
|
+
Pressable,
|
|
25
|
+
// Feedback
|
|
26
|
+
Modal,
|
|
27
|
+
AlertPopup,
|
|
28
|
+
alertShowMessage,
|
|
29
|
+
Toast,
|
|
30
|
+
Loading,
|
|
31
|
+
AnimatedLoading,
|
|
32
|
+
CelebrationOverlay,
|
|
33
|
+
confettiRealistic,
|
|
34
|
+
confettiFireworks,
|
|
35
|
+
confettiFailure,
|
|
36
|
+
confettiStars,
|
|
37
|
+
confettiSideCannons,
|
|
38
|
+
confettiSparkle,
|
|
39
|
+
confettiFall,
|
|
40
|
+
confettiHearts,
|
|
41
|
+
confettiEmojiBurst,
|
|
42
|
+
confettiSadEmoji,
|
|
43
|
+
// Lists
|
|
44
|
+
FlatList,
|
|
45
|
+
AnimatedList,
|
|
46
|
+
AnimatedListItem,
|
|
47
|
+
AnimatedPresenceList,
|
|
48
|
+
Pager,
|
|
49
|
+
CardStack,
|
|
50
|
+
ResponsiveGrid,
|
|
51
|
+
// Navigation
|
|
52
|
+
TabPicker,
|
|
53
|
+
HeaderTabPicker,
|
|
54
|
+
TabContent,
|
|
55
|
+
useSelectedTab,
|
|
56
|
+
WizardView,
|
|
57
|
+
SettingGroup,
|
|
58
|
+
// Advanced
|
|
59
|
+
DrawingCanvas,
|
|
60
|
+
TransformWrapper,
|
|
61
|
+
TransformComponent,
|
|
62
|
+
ScrollAnimatedView,
|
|
63
|
+
StaggeredAnimationContainer,
|
|
64
|
+
Animated,
|
|
65
|
+
useAnimatedValue,
|
|
66
|
+
FeedbackButton,
|
|
67
|
+
} from 'ugly-app/client';
|
|
68
|
+
import type {
|
|
69
|
+
TabPickerDictionary,
|
|
70
|
+
FlatListHandle,
|
|
71
|
+
CardStackItem,
|
|
72
|
+
WheelColumn,
|
|
73
|
+
DateRangeValue,
|
|
74
|
+
ToastVariant,
|
|
75
|
+
ImageResult,
|
|
76
|
+
} from 'ugly-app/client';
|
|
77
|
+
|
|
78
|
+
// ─── Section helper ────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function Section({
|
|
81
|
+
title,
|
|
82
|
+
description,
|
|
83
|
+
children,
|
|
84
|
+
}: {
|
|
85
|
+
title: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
children: React.ReactNode;
|
|
88
|
+
}): React.ReactElement {
|
|
89
|
+
return (
|
|
90
|
+
<div style={{ marginBottom: 24 }}>
|
|
91
|
+
<div style={{ marginBottom: 6 }}>
|
|
92
|
+
<Text size="sm" weight="semibold">{title}</Text>
|
|
93
|
+
{description && (
|
|
94
|
+
<Text size="xs" style={{ opacity: 0.55, marginTop: 2 }}>{description}</Text>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
<Card>{children}</Card>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Tab: Layout ───────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function LayoutTab(): React.ReactElement {
|
|
105
|
+
return (
|
|
106
|
+
<ScrollView>
|
|
107
|
+
<div style={{ maxWidth: 640, margin: '0 auto', padding: '16px 16px 48px' }}>
|
|
108
|
+
|
|
109
|
+
<Section title="Text" description="Typography scale and weight variants">
|
|
110
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
111
|
+
{(['xs', 'sm', 'base', 'lg', 'xl', '2xl'] as const).map((size) => (
|
|
112
|
+
<div key={size} style={{ display: 'flex', alignItems: 'baseline', gap: 12 }}>
|
|
113
|
+
<Text size="xs" style={{ opacity: 0.4, width: 32, flexShrink: 0 }}>{size}</Text>
|
|
114
|
+
<Text size={size}>The quick brown fox</Text>
|
|
115
|
+
<Text size={size} weight="bold" style={{ opacity: 0.6 }}>bold</Text>
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</Section>
|
|
120
|
+
|
|
121
|
+
<Section title="View" description="Flex container with optional border/background">
|
|
122
|
+
<View style={{ border: '1px solid var(--app-foreground, #111)', borderRadius: 8, padding: 12, opacity: 0.7 }}>
|
|
123
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
124
|
+
<View style={{ flex: 1, background: 'var(--app-primary, #2563eb)', borderRadius: 6, padding: 8 }}>
|
|
125
|
+
<Text size="xs" style={{ color: 'white' }}>flex: 1</Text>
|
|
126
|
+
</View>
|
|
127
|
+
<View style={{ flex: 2, background: 'var(--app-primary, #2563eb)', borderRadius: 6, padding: 8, opacity: 0.7 }}>
|
|
128
|
+
<Text size="xs" style={{ color: 'white' }}>flex: 2</Text>
|
|
129
|
+
</View>
|
|
130
|
+
</View>
|
|
131
|
+
</View>
|
|
132
|
+
</Section>
|
|
133
|
+
|
|
134
|
+
<Section title="Panel" description="Elevated surface container">
|
|
135
|
+
<Panel>
|
|
136
|
+
<Text size="sm">Panel content goes here. Use Panel for cards with a subtle background and border.</Text>
|
|
137
|
+
</Panel>
|
|
138
|
+
</Section>
|
|
139
|
+
|
|
140
|
+
<Section title="Card" description="Content card with padding">
|
|
141
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
142
|
+
{(['sm', 'md', 'lg'] as const).map((pad) => (
|
|
143
|
+
<Card key={pad} style={{ flex: 1 }}>
|
|
144
|
+
<Text size="xs" weight="medium">{pad}</Text>
|
|
145
|
+
<Text size="xs" style={{ opacity: 0.5 }}>Card padding</Text>
|
|
146
|
+
</Card>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
</Section>
|
|
150
|
+
|
|
151
|
+
<Section title="Image" description="Image with resize modes">
|
|
152
|
+
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
|
153
|
+
{(['cover', 'contain', 'fill'] as const).map((mode) => (
|
|
154
|
+
<div key={mode} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
|
|
155
|
+
<Image
|
|
156
|
+
source={{ uri: 'https://placehold.co/120x80' }}
|
|
157
|
+
width={120}
|
|
158
|
+
height={80}
|
|
159
|
+
resizeMode={mode}
|
|
160
|
+
style={{ borderRadius: 6 }}
|
|
161
|
+
/>
|
|
162
|
+
<Text size="xs" style={{ opacity: 0.5 }}>{mode}</Text>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
</Section>
|
|
167
|
+
|
|
168
|
+
<Section title="ScrollView" description="Scrollable container that integrates with scroll indicators">
|
|
169
|
+
<div style={{ height: 80, border: '1px dashed rgba(0,0,0,0.2)', borderRadius: 8, overflow: 'hidden' }}>
|
|
170
|
+
<ScrollView>
|
|
171
|
+
{Array.from({ length: 6 }, (_, i) => (
|
|
172
|
+
<div key={i} style={{ padding: '6px 12px', borderBottom: '1px solid rgba(0,0,0,0.06)' }}>
|
|
173
|
+
<Text size="sm">Scroll item {i + 1}</Text>
|
|
174
|
+
</div>
|
|
175
|
+
))}
|
|
176
|
+
</ScrollView>
|
|
177
|
+
</div>
|
|
178
|
+
</Section>
|
|
179
|
+
|
|
180
|
+
<Section title="SimpleScrollView" description="Lightweight scroll wrapper without scroll context — use for nested scrollable areas">
|
|
181
|
+
<div style={{ height: 80, border: '1px dashed rgba(0,0,0,0.2)', borderRadius: 8, overflow: 'hidden' }}>
|
|
182
|
+
<SimpleScrollView style={{ height: '100%', overflowY: 'auto' }}>
|
|
183
|
+
{Array.from({ length: 6 }, (_, i) => (
|
|
184
|
+
<div key={i} style={{ padding: '6px 12px', borderBottom: '1px solid rgba(0,0,0,0.06)' }}>
|
|
185
|
+
<Text size="sm">Item {i + 1}</Text>
|
|
186
|
+
</div>
|
|
187
|
+
))}
|
|
188
|
+
</SimpleScrollView>
|
|
189
|
+
</div>
|
|
190
|
+
</Section>
|
|
191
|
+
|
|
192
|
+
<Section title="PopupPanel" description="Slide-up panel for popups (opened via useRouter().openPopup())">
|
|
193
|
+
<Text size="sm" style={{ opacity: 0.6 }}>
|
|
194
|
+
PopupPanel is a container used inside popups opened with{' '}
|
|
195
|
+
<code style={{ fontSize: 12 }}>useRouter().openPopup()</code>.
|
|
196
|
+
It provides consistent padding and a drag handle.
|
|
197
|
+
</Text>
|
|
198
|
+
<div style={{ marginTop: 8, border: '1px dashed rgba(0,0,0,0.2)', borderRadius: 12, overflow: 'hidden' }}>
|
|
199
|
+
<PopupPanel>
|
|
200
|
+
<Text size="sm">Popup content renders inside PopupPanel</Text>
|
|
201
|
+
</PopupPanel>
|
|
202
|
+
</div>
|
|
203
|
+
</Section>
|
|
204
|
+
|
|
205
|
+
</div>
|
|
206
|
+
</ScrollView>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Tab: Inputs ───────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
function InputsTab(): React.ReactElement {
|
|
213
|
+
const [inputVal, setInputVal] = useState('');
|
|
214
|
+
const [validatedVal, setValidatedVal] = useState('');
|
|
215
|
+
const [enumVal, setEnumVal] = useState<'a' | 'b' | 'c'>('a');
|
|
216
|
+
const [dateVal, setDateVal] = useState<string | null>(null);
|
|
217
|
+
const [dateTimeVal, setDateTimeVal] = useState<string | null>(null);
|
|
218
|
+
const [dateRangeVal, setDateRangeVal] = useState<DateRangeValue>({
|
|
219
|
+
preset: 'last7Days',
|
|
220
|
+
customStartDate: null,
|
|
221
|
+
customEndDate: null,
|
|
222
|
+
});
|
|
223
|
+
const [wheelHour, setWheelHour] = useState(0);
|
|
224
|
+
const [wheelMin, setWheelMin] = useState(0);
|
|
225
|
+
const [selectVal, setSelectVal] = useState<'alpha' | 'beta' | 'gamma' | 'delta' | undefined>(undefined);
|
|
226
|
+
const [animInputVal, setAnimInputVal] = useState('');
|
|
227
|
+
const [animFocused, setAnimFocused] = useState(false);
|
|
228
|
+
const [pressed, setPressed] = useState(false);
|
|
229
|
+
|
|
230
|
+
const hourCol: WheelColumn = {
|
|
231
|
+
values: Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0')),
|
|
232
|
+
selectedIndex: wheelHour,
|
|
233
|
+
onSelect: setWheelHour,
|
|
234
|
+
width: 60,
|
|
235
|
+
};
|
|
236
|
+
const minCol: WheelColumn = {
|
|
237
|
+
values: Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0')),
|
|
238
|
+
selectedIndex: wheelMin,
|
|
239
|
+
onSelect: setWheelMin,
|
|
240
|
+
width: 60,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<ScrollView>
|
|
245
|
+
<div style={{ maxWidth: 640, margin: '0 auto', padding: '16px 16px 48px' }}>
|
|
246
|
+
|
|
247
|
+
<Section title="Button" description="Primary, secondary, and destructive variants">
|
|
248
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
249
|
+
<Button variant="primary">Primary</Button>
|
|
250
|
+
<Button variant="secondary">Secondary</Button>
|
|
251
|
+
<Button variant="destructive">Destructive</Button>
|
|
252
|
+
<Button variant="primary" disabled>Disabled</Button>
|
|
253
|
+
</div>
|
|
254
|
+
</Section>
|
|
255
|
+
|
|
256
|
+
<Section title="Input" description="Controlled text input">
|
|
257
|
+
<Input
|
|
258
|
+
value={inputVal}
|
|
259
|
+
onChange={(e) => setInputVal(e.target.value)}
|
|
260
|
+
placeholder="Type something..."
|
|
261
|
+
/>
|
|
262
|
+
{inputVal && (
|
|
263
|
+
<Text size="xs" style={{ marginTop: 6, opacity: 0.5 }}>Value: {inputVal}</Text>
|
|
264
|
+
)}
|
|
265
|
+
</Section>
|
|
266
|
+
|
|
267
|
+
<Section title="ValidatedTextInput" description="Input with inline validation">
|
|
268
|
+
<ValidatedTextInput
|
|
269
|
+
value={validatedVal}
|
|
270
|
+
onChangeText={setValidatedVal}
|
|
271
|
+
placeholder="Min 3 characters"
|
|
272
|
+
validate={(v) => v.length > 0 && v.length < 3 ? 'Minimum 3 characters required' : undefined}
|
|
273
|
+
/>
|
|
274
|
+
</Section>
|
|
275
|
+
|
|
276
|
+
<Section title="AnimatedInputWrapper" description="Focus ring animation wrapper for any input element">
|
|
277
|
+
<AnimatedInputWrapper isFocused={animFocused}>
|
|
278
|
+
<input
|
|
279
|
+
value={animInputVal}
|
|
280
|
+
onChange={(e) => setAnimInputVal(e.target.value)}
|
|
281
|
+
onFocus={() => setAnimFocused(true)}
|
|
282
|
+
onBlur={() => setAnimFocused(false)}
|
|
283
|
+
placeholder="Click to see focus animation"
|
|
284
|
+
style={{
|
|
285
|
+
width: '100%',
|
|
286
|
+
padding: '10px 12px',
|
|
287
|
+
border: '1px solid rgba(0,0,0,0.15)',
|
|
288
|
+
borderRadius: 8,
|
|
289
|
+
fontSize: 16,
|
|
290
|
+
outline: 'none',
|
|
291
|
+
background: 'transparent',
|
|
292
|
+
color: 'inherit',
|
|
293
|
+
boxSizing: 'border-box',
|
|
294
|
+
}}
|
|
295
|
+
/>
|
|
296
|
+
</AnimatedInputWrapper>
|
|
297
|
+
</Section>
|
|
298
|
+
|
|
299
|
+
<Section title="EnumInput" description="Dropdown selector for enum values">
|
|
300
|
+
<EnumInput
|
|
301
|
+
value={enumVal}
|
|
302
|
+
onChange={setEnumVal}
|
|
303
|
+
options={[
|
|
304
|
+
{ value: 'a', label: 'Option A' },
|
|
305
|
+
{ value: 'b', label: 'Option B' },
|
|
306
|
+
{ value: 'c', label: 'Option C' },
|
|
307
|
+
]}
|
|
308
|
+
/>
|
|
309
|
+
<Text size="xs" style={{ marginTop: 6, opacity: 0.5 }}>Selected: {enumVal}</Text>
|
|
310
|
+
</Section>
|
|
311
|
+
|
|
312
|
+
<Section title="DatePicker" description="Calendar date selector">
|
|
313
|
+
<DatePicker value={dateVal} onValueChanged={setDateVal} />
|
|
314
|
+
{dateVal && <Text size="xs" style={{ marginTop: 6, opacity: 0.5 }}>Selected: {dateVal}</Text>}
|
|
315
|
+
</Section>
|
|
316
|
+
|
|
317
|
+
<Section title="DateTimePicker" description="Date and time selector">
|
|
318
|
+
<DateTimePicker value={dateTimeVal} onValueChanged={setDateTimeVal} />
|
|
319
|
+
{dateTimeVal && <Text size="xs" style={{ marginTop: 6, opacity: 0.5 }}>Selected: {dateTimeVal}</Text>}
|
|
320
|
+
</Section>
|
|
321
|
+
|
|
322
|
+
<Section title="DateRangePicker" description="Preset or custom date range selector">
|
|
323
|
+
<DateRangePicker value={dateRangeVal} onValueChanged={setDateRangeVal} />
|
|
324
|
+
</Section>
|
|
325
|
+
|
|
326
|
+
<Section title="WheelPicker" description="iOS-style scroll wheel selector">
|
|
327
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
|
|
328
|
+
<Text size="lg" weight="bold">
|
|
329
|
+
{String(wheelHour).padStart(2, '0')}:{String(wheelMin).padStart(2, '0')}
|
|
330
|
+
</Text>
|
|
331
|
+
<WheelPicker columns={[hourCol, minCol]} />
|
|
332
|
+
</div>
|
|
333
|
+
</Section>
|
|
334
|
+
|
|
335
|
+
<Section title="SelectView" description="List-based single or multi-select">
|
|
336
|
+
<SelectView
|
|
337
|
+
items={[
|
|
338
|
+
{ value: 'alpha', text: 'Alpha', description: 'First option' },
|
|
339
|
+
{ value: 'beta', text: 'Beta', description: 'Second option' },
|
|
340
|
+
{ value: 'gamma', text: 'Gamma', description: 'Third option' },
|
|
341
|
+
{ value: 'delta', text: 'Delta', description: 'Fourth option' },
|
|
342
|
+
]}
|
|
343
|
+
value={selectVal}
|
|
344
|
+
onChange={setSelectVal}
|
|
345
|
+
/>
|
|
346
|
+
{selectVal && <Text size="xs" style={{ marginTop: 6, opacity: 0.5 }}>Selected: {selectVal}</Text>}
|
|
347
|
+
</Section>
|
|
348
|
+
|
|
349
|
+
<Section title="Pressable" description="Pressable area with visual feedback">
|
|
350
|
+
<Pressable
|
|
351
|
+
onPress={() => setPressed((p) => !p)}
|
|
352
|
+
style={{
|
|
353
|
+
padding: 16,
|
|
354
|
+
borderRadius: 8,
|
|
355
|
+
border: '1px solid rgba(0,0,0,0.1)',
|
|
356
|
+
textAlign: 'center',
|
|
357
|
+
cursor: 'pointer',
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
<Text weight="medium">{pressed ? 'Pressed! (tap again to reset)' : 'Tap me'}</Text>
|
|
361
|
+
</Pressable>
|
|
362
|
+
</Section>
|
|
363
|
+
|
|
364
|
+
</div>
|
|
365
|
+
</ScrollView>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── Tab: Feedback ─────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
function FeedbackTab(): React.ReactElement {
|
|
372
|
+
const [showModal, setShowModal] = useState(false);
|
|
373
|
+
const [showToast, setShowToast] = useState(false);
|
|
374
|
+
const [toastVariant, setToastVariant] = useState<ToastVariant>('info');
|
|
375
|
+
const [showCelebration, setShowCelebration] = useState(false);
|
|
376
|
+
|
|
377
|
+
const confettiFns: Array<{ label: string; fn: () => void }> = [
|
|
378
|
+
{ label: 'Realistic', fn: confettiRealistic },
|
|
379
|
+
{ label: 'Fireworks', fn: confettiFireworks },
|
|
380
|
+
{ label: 'Failure', fn: confettiFailure },
|
|
381
|
+
{ label: 'Stars', fn: confettiStars },
|
|
382
|
+
{ label: 'Cannons', fn: confettiSideCannons },
|
|
383
|
+
{ label: 'Sparkle', fn: confettiSparkle },
|
|
384
|
+
{ label: 'Fall', fn: confettiFall },
|
|
385
|
+
{ label: 'Hearts', fn: confettiHearts },
|
|
386
|
+
{ label: 'Emoji Burst', fn: confettiEmojiBurst },
|
|
387
|
+
{ label: 'Sad Emoji', fn: confettiSadEmoji },
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<ScrollView>
|
|
392
|
+
<div style={{ maxWidth: 640, margin: '0 auto', padding: '16px 16px 48px' }}>
|
|
393
|
+
|
|
394
|
+
<Section title="Modal" description="Overlay dialog with backdrop dismiss">
|
|
395
|
+
<Button variant="primary" onClick={() => setShowModal(true)}>Open Modal</Button>
|
|
396
|
+
{showModal && (
|
|
397
|
+
<Modal onClose={() => setShowModal(false)}>
|
|
398
|
+
<div style={{ padding: 24, maxWidth: 360 }}>
|
|
399
|
+
<Text size="lg" weight="bold">Modal Title</Text>
|
|
400
|
+
<Text size="sm" style={{ marginTop: 8, opacity: 0.6 }}>
|
|
401
|
+
This is the modal body. Click outside or the button below to close.
|
|
402
|
+
</Text>
|
|
403
|
+
<div style={{ marginTop: 16 }}>
|
|
404
|
+
<Button variant="secondary" onClick={() => setShowModal(false)}>Close</Button>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</Modal>
|
|
408
|
+
)}
|
|
409
|
+
</Section>
|
|
410
|
+
|
|
411
|
+
<Section title="AlertPopup" description="Imperative alert dialog (no state required)">
|
|
412
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
413
|
+
<Button variant="secondary" onClick={() => alertShowMessage('This is an info message.')}>
|
|
414
|
+
Show Message
|
|
415
|
+
</Button>
|
|
416
|
+
</div>
|
|
417
|
+
{/* AlertPopup must be mounted to render alerts */}
|
|
418
|
+
<AlertPopup />
|
|
419
|
+
</Section>
|
|
420
|
+
|
|
421
|
+
<Section title="Toast" description="Auto-dismissing notification banner">
|
|
422
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
423
|
+
<EnumInput
|
|
424
|
+
value={toastVariant}
|
|
425
|
+
onChange={setToastVariant}
|
|
426
|
+
options={[
|
|
427
|
+
{ value: 'info', label: 'Info' },
|
|
428
|
+
{ value: 'success', label: 'Success' },
|
|
429
|
+
{ value: 'warning', label: 'Warning' },
|
|
430
|
+
{ value: 'error', label: 'Error' },
|
|
431
|
+
]}
|
|
432
|
+
/>
|
|
433
|
+
<Button
|
|
434
|
+
variant="primary"
|
|
435
|
+
onClick={() => setShowToast(true)}
|
|
436
|
+
disabled={showToast}
|
|
437
|
+
>
|
|
438
|
+
Show Toast
|
|
439
|
+
</Button>
|
|
440
|
+
</div>
|
|
441
|
+
{showToast && (
|
|
442
|
+
<Toast
|
|
443
|
+
message={`This is a ${toastVariant} toast message.`}
|
|
444
|
+
variant={toastVariant}
|
|
445
|
+
onDismiss={() => setShowToast(false)}
|
|
446
|
+
/>
|
|
447
|
+
)}
|
|
448
|
+
</Section>
|
|
449
|
+
|
|
450
|
+
<Section title="Loading" description="Spinner at multiple sizes">
|
|
451
|
+
<div style={{ display: 'flex', gap: 24, alignItems: 'center' }}>
|
|
452
|
+
{(['sm', 'md', 'lg'] as const).map((size) => (
|
|
453
|
+
<div key={size} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
|
|
454
|
+
<Loading size={size} />
|
|
455
|
+
<Text size="xs" style={{ opacity: 0.5 }}>{size}</Text>
|
|
456
|
+
</div>
|
|
457
|
+
))}
|
|
458
|
+
</div>
|
|
459
|
+
</Section>
|
|
460
|
+
|
|
461
|
+
<Section title="AnimatedLoading" description="Loading indicator with animated status messages">
|
|
462
|
+
<AnimatedLoading />
|
|
463
|
+
</Section>
|
|
464
|
+
|
|
465
|
+
<Section title="CelebrationOverlay" description="Fullscreen celebration animation">
|
|
466
|
+
<div style={{ position: 'relative', minHeight: 80 }}>
|
|
467
|
+
<Button variant="primary" onClick={() => setShowCelebration(true)}>
|
|
468
|
+
Celebrate!
|
|
469
|
+
</Button>
|
|
470
|
+
<CelebrationOverlay
|
|
471
|
+
show={showCelebration}
|
|
472
|
+
title="You did it!"
|
|
473
|
+
subtitle="Keep up the great work"
|
|
474
|
+
onDismiss={() => setShowCelebration(false)}
|
|
475
|
+
/>
|
|
476
|
+
</div>
|
|
477
|
+
</Section>
|
|
478
|
+
|
|
479
|
+
<Section title="Confetti" description="10 canvas-confetti presets — fire and forget">
|
|
480
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
481
|
+
{confettiFns.map(({ label, fn }) => (
|
|
482
|
+
<Button key={label} variant="secondary" onClick={() => fn()}>
|
|
483
|
+
{label}
|
|
484
|
+
</Button>
|
|
485
|
+
))}
|
|
486
|
+
</div>
|
|
487
|
+
</Section>
|
|
488
|
+
|
|
489
|
+
</div>
|
|
490
|
+
</ScrollView>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ─── Tab: Lists ────────────────────────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
function ListsTab(): React.ReactElement {
|
|
497
|
+
const flatListRef = useRef<FlatListHandle>(null);
|
|
498
|
+
const [animatedItems, setAnimatedItems] = useState(['Alpha', 'Beta', 'Gamma']);
|
|
499
|
+
const [presenceItems, setPresenceItems] = useState(['One', 'Two', 'Three', 'Four']);
|
|
500
|
+
const [cardItems] = useState<CardStackItem<{ label: string }>[]>([
|
|
501
|
+
{ id: '1', data: { label: 'Card One' } },
|
|
502
|
+
{ id: '2', data: { label: 'Card Two' } },
|
|
503
|
+
{ id: '3', data: { label: 'Card Three' } },
|
|
504
|
+
{ id: '4', data: { label: 'Card Four' } },
|
|
505
|
+
{ id: '5', data: { label: 'Card Five' } },
|
|
506
|
+
]);
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<ScrollView>
|
|
510
|
+
<div style={{ maxWidth: 640, margin: '0 auto', padding: '16px 16px 48px' }}>
|
|
511
|
+
|
|
512
|
+
<Section title="FlatList" description="Virtualized list with scroll management">
|
|
513
|
+
<div style={{ height: 160, overflow: 'hidden', borderRadius: 8, border: '1px solid rgba(0,0,0,0.08)' }}>
|
|
514
|
+
<FlatList
|
|
515
|
+
listRef={flatListRef}
|
|
516
|
+
data={['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6', 'Item 7']}
|
|
517
|
+
keyExtractor={(item) => item}
|
|
518
|
+
renderItem={({ item, index }) => (
|
|
519
|
+
<div
|
|
520
|
+
key={index}
|
|
521
|
+
style={{ padding: '10px 14px', borderBottom: '1px solid rgba(0,0,0,0.06)' }}
|
|
522
|
+
>
|
|
523
|
+
<Text size="sm">{item}</Text>
|
|
524
|
+
</div>
|
|
525
|
+
)}
|
|
526
|
+
/>
|
|
527
|
+
</div>
|
|
528
|
+
</Section>
|
|
529
|
+
|
|
530
|
+
<Section title="AnimatedList / AnimatedListItem" description="List with slide-in animations for new items">
|
|
531
|
+
<div style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
|
|
532
|
+
<Button
|
|
533
|
+
variant="primary"
|
|
534
|
+
onClick={() => {
|
|
535
|
+
const next = `Item ${Date.now() % 10000}`;
|
|
536
|
+
setAnimatedItems((prev) => [next, ...prev]);
|
|
537
|
+
}}
|
|
538
|
+
>
|
|
539
|
+
Add Item
|
|
540
|
+
</Button>
|
|
541
|
+
<Button
|
|
542
|
+
variant="secondary"
|
|
543
|
+
onClick={() => setAnimatedItems((prev) => prev.slice(1))}
|
|
544
|
+
disabled={animatedItems.length === 0}
|
|
545
|
+
>
|
|
546
|
+
Remove First
|
|
547
|
+
</Button>
|
|
548
|
+
</div>
|
|
549
|
+
<AnimatedList>
|
|
550
|
+
{animatedItems.map((item) => (
|
|
551
|
+
<AnimatedListItem key={item} itemKey={item} variant="slide">
|
|
552
|
+
<div style={{ padding: '8px 12px', marginBottom: 4, borderRadius: 6, background: 'rgba(0,0,0,0.04)' }}>
|
|
553
|
+
<Text size="sm">{item}</Text>
|
|
554
|
+
</div>
|
|
555
|
+
</AnimatedListItem>
|
|
556
|
+
))}
|
|
557
|
+
</AnimatedList>
|
|
558
|
+
</Section>
|
|
559
|
+
|
|
560
|
+
<Section title="AnimatedPresenceList" description="List with exit animations when items are removed">
|
|
561
|
+
<div style={{ marginBottom: 10 }}>
|
|
562
|
+
<Button
|
|
563
|
+
variant="secondary"
|
|
564
|
+
onClick={() => setPresenceItems((prev) => prev.slice(1))}
|
|
565
|
+
disabled={presenceItems.length === 0}
|
|
566
|
+
>
|
|
567
|
+
Remove First
|
|
568
|
+
</Button>
|
|
569
|
+
</div>
|
|
570
|
+
<AnimatedPresenceList
|
|
571
|
+
items={presenceItems}
|
|
572
|
+
keyExtractor={(item) => item}
|
|
573
|
+
variant="fade"
|
|
574
|
+
renderItem={(item, _index, isExiting) => (
|
|
575
|
+
<div
|
|
576
|
+
style={{
|
|
577
|
+
padding: '8px 12px',
|
|
578
|
+
marginBottom: 4,
|
|
579
|
+
borderRadius: 6,
|
|
580
|
+
background: 'rgba(0,0,0,0.04)',
|
|
581
|
+
opacity: isExiting ? 0.3 : 1,
|
|
582
|
+
transition: 'opacity 0.2s',
|
|
583
|
+
}}
|
|
584
|
+
>
|
|
585
|
+
<Text size="sm">{item}</Text>
|
|
586
|
+
</div>
|
|
587
|
+
)}
|
|
588
|
+
/>
|
|
589
|
+
{presenceItems.length === 0 && (
|
|
590
|
+
<Button variant="secondary" onClick={() => setPresenceItems(['One', 'Two', 'Three', 'Four'])}>
|
|
591
|
+
Reset
|
|
592
|
+
</Button>
|
|
593
|
+
)}
|
|
594
|
+
</Section>
|
|
595
|
+
|
|
596
|
+
<Section title="Pager" description="Horizontally paginated slides with dot indicators">
|
|
597
|
+
<div style={{ height: 120, overflow: 'hidden', borderRadius: 8, border: '1px solid rgba(0,0,0,0.08)' }}>
|
|
598
|
+
<Pager
|
|
599
|
+
items={[
|
|
600
|
+
<div key="1" style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--app-primary, #2563eb)', borderRadius: 8 }}>
|
|
601
|
+
<Text weight="bold" style={{ color: 'white' }}>Slide 1</Text>
|
|
602
|
+
</div>,
|
|
603
|
+
<div key="2" style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#7c3aed', borderRadius: 8 }}>
|
|
604
|
+
<Text weight="bold" style={{ color: 'white' }}>Slide 2</Text>
|
|
605
|
+
</div>,
|
|
606
|
+
<div key="3" style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#059669', borderRadius: 8 }}>
|
|
607
|
+
<Text weight="bold" style={{ color: 'white' }}>Slide 3</Text>
|
|
608
|
+
</div>,
|
|
609
|
+
]}
|
|
610
|
+
delay={0}
|
|
611
|
+
showArrows
|
|
612
|
+
/>
|
|
613
|
+
</div>
|
|
614
|
+
</Section>
|
|
615
|
+
|
|
616
|
+
<Section title="CardStack" description="Swipeable card stack — drag or use buttons">
|
|
617
|
+
<div style={{ height: 220, position: 'relative', overflow: 'hidden' }}>
|
|
618
|
+
<CardStack
|
|
619
|
+
items={cardItems}
|
|
620
|
+
renderCard={(item, actions) => (
|
|
621
|
+
<Card style={{ height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
|
622
|
+
<Text size="lg" weight="bold">{item.label}</Text>
|
|
623
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
624
|
+
<Button variant="destructive" onClick={actions.dislike}>✕ Pass</Button>
|
|
625
|
+
<Button variant="primary" onClick={actions.like}>✓ Like</Button>
|
|
626
|
+
</div>
|
|
627
|
+
</Card>
|
|
628
|
+
)}
|
|
629
|
+
/>
|
|
630
|
+
</div>
|
|
631
|
+
</Section>
|
|
632
|
+
|
|
633
|
+
<Section title="ResponsiveGrid" description="Grid with automatic responsive column sizing">
|
|
634
|
+
<ResponsiveGrid
|
|
635
|
+
data={['A', 'B', 'C', 'D', 'E', 'F']}
|
|
636
|
+
columns={3}
|
|
637
|
+
spacing={8}
|
|
638
|
+
getItemHeight={(w) => w}
|
|
639
|
+
renderItem={(item, size) => (
|
|
640
|
+
<div
|
|
641
|
+
style={{
|
|
642
|
+
width: size.width,
|
|
643
|
+
height: size.height,
|
|
644
|
+
background: 'var(--app-primary, #2563eb)',
|
|
645
|
+
borderRadius: 8,
|
|
646
|
+
display: 'flex',
|
|
647
|
+
alignItems: 'center',
|
|
648
|
+
justifyContent: 'center',
|
|
649
|
+
}}
|
|
650
|
+
>
|
|
651
|
+
<Text weight="bold" style={{ color: 'white' }}>{item}</Text>
|
|
652
|
+
</div>
|
|
653
|
+
)}
|
|
654
|
+
/>
|
|
655
|
+
</Section>
|
|
656
|
+
|
|
657
|
+
</div>
|
|
658
|
+
</ScrollView>
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ─── Tab: Navigation ───────────────────────────────────────────────────────────
|
|
663
|
+
|
|
664
|
+
type MiniTab = 'alpha' | 'beta' | 'gamma';
|
|
665
|
+
const MINI_TABS: MiniTab[] = ['alpha', 'beta', 'gamma'];
|
|
666
|
+
|
|
667
|
+
type HeaderMiniTab = 'x' | 'y';
|
|
668
|
+
const HEADER_MINI_TABS: HeaderMiniTab[] = ['x', 'y'];
|
|
669
|
+
|
|
670
|
+
function NavigationTab(): React.ReactElement {
|
|
671
|
+
const [miniTab, setMiniTab] = useState<MiniTab>('alpha');
|
|
672
|
+
const [headerMiniTab, setHeaderMiniTab] = useState<HeaderMiniTab>('x');
|
|
673
|
+
const [wizardStep, setWizardStep] = useState(0);
|
|
674
|
+
const [wizardDone, setWizardDone] = useState(false);
|
|
675
|
+
|
|
676
|
+
const miniTabDictionary: TabPickerDictionary<MiniTab> = {
|
|
677
|
+
alpha: { text: 'Alpha', content: <Text size="sm">Content for the Alpha tab.</Text> },
|
|
678
|
+
beta: { text: 'Beta', content: <Text size="sm">Content for the Beta tab.</Text> },
|
|
679
|
+
gamma: { text: 'Gamma', content: <Text size="sm">Content for the Gamma tab.</Text> },
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const headerMiniDictionary: TabPickerDictionary<HeaderMiniTab> = {
|
|
683
|
+
x: { text: 'Option X', content: null },
|
|
684
|
+
y: { text: 'Option Y', content: null },
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
return (
|
|
688
|
+
<ScrollView>
|
|
689
|
+
<div style={{ maxWidth: 640, margin: '0 auto', padding: '16px 16px 48px' }}>
|
|
690
|
+
|
|
691
|
+
<Section title="TabPicker" description="Underline-style tab bar (used for this page's own tabs)">
|
|
692
|
+
<div style={{ border: '1px solid rgba(0,0,0,0.08)', borderRadius: 8, overflow: 'hidden' }}>
|
|
693
|
+
<TabPicker
|
|
694
|
+
tabs={MINI_TABS}
|
|
695
|
+
tabDictionary={miniTabDictionary}
|
|
696
|
+
selectedTab={miniTab}
|
|
697
|
+
setSelectedTab={setMiniTab}
|
|
698
|
+
/>
|
|
699
|
+
<div style={{ padding: 16 }}>
|
|
700
|
+
<TabContent
|
|
701
|
+
tabs={MINI_TABS}
|
|
702
|
+
tabDictionary={miniTabDictionary}
|
|
703
|
+
selectedTab={miniTab}
|
|
704
|
+
/>
|
|
705
|
+
</div>
|
|
706
|
+
</div>
|
|
707
|
+
</Section>
|
|
708
|
+
|
|
709
|
+
<Section title="HeaderTabPicker" description="Pill/segment-style tab selector for headers">
|
|
710
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
711
|
+
<HeaderTabPicker
|
|
712
|
+
tabs={HEADER_MINI_TABS}
|
|
713
|
+
tabDictionary={headerMiniDictionary}
|
|
714
|
+
selectedTab={headerMiniTab}
|
|
715
|
+
setSelectedTab={setHeaderMiniTab}
|
|
716
|
+
/>
|
|
717
|
+
<Text size="sm" style={{ opacity: 0.6 }}>Selected: {headerMiniTab}</Text>
|
|
718
|
+
</div>
|
|
719
|
+
</Section>
|
|
720
|
+
|
|
721
|
+
<Section title="WizardView" description="Multi-step form with progress bar and navigation">
|
|
722
|
+
{wizardDone ? (
|
|
723
|
+
<div style={{ padding: 16, textAlign: 'center' }}>
|
|
724
|
+
<Text size="lg" weight="bold">Wizard completed!</Text>
|
|
725
|
+
<div style={{ marginTop: 12 }}>
|
|
726
|
+
<Button variant="secondary" onClick={() => { setWizardDone(false); setWizardStep(0); }}>
|
|
727
|
+
Reset
|
|
728
|
+
</Button>
|
|
729
|
+
</div>
|
|
730
|
+
</div>
|
|
731
|
+
) : (
|
|
732
|
+
<div style={{ height: 320, position: 'relative', overflow: 'hidden' }}>
|
|
733
|
+
<WizardView
|
|
734
|
+
steps={[
|
|
735
|
+
{
|
|
736
|
+
key: 's1',
|
|
737
|
+
content: (
|
|
738
|
+
<div style={{ padding: 16 }}>
|
|
739
|
+
<Text size="lg" weight="bold">Step 1: Welcome</Text>
|
|
740
|
+
<Text size="sm" style={{ marginTop: 8, opacity: 0.6 }}>
|
|
741
|
+
This is the first step of the wizard. Click Next to continue.
|
|
742
|
+
</Text>
|
|
743
|
+
</div>
|
|
744
|
+
),
|
|
745
|
+
canProceed: true,
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
key: 's2',
|
|
749
|
+
content: (
|
|
750
|
+
<div style={{ padding: 16 }}>
|
|
751
|
+
<Text size="lg" weight="bold">Step 2: Configure</Text>
|
|
752
|
+
<Text size="sm" style={{ marginTop: 8, opacity: 0.6 }}>
|
|
753
|
+
Second step content. Back and Next buttons appear automatically.
|
|
754
|
+
</Text>
|
|
755
|
+
</div>
|
|
756
|
+
),
|
|
757
|
+
canProceed: true,
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
key: 's3',
|
|
761
|
+
content: (
|
|
762
|
+
<div style={{ padding: 16 }}>
|
|
763
|
+
<Text size="lg" weight="bold">Step 3: Confirm</Text>
|
|
764
|
+
<Text size="sm" style={{ marginTop: 8, opacity: 0.6 }}>
|
|
765
|
+
Final step. Press Done to complete the wizard.
|
|
766
|
+
</Text>
|
|
767
|
+
</div>
|
|
768
|
+
),
|
|
769
|
+
canProceed: true,
|
|
770
|
+
},
|
|
771
|
+
]}
|
|
772
|
+
currentStep={wizardStep}
|
|
773
|
+
onStepChange={setWizardStep}
|
|
774
|
+
onComplete={() => setWizardDone(true)}
|
|
775
|
+
progressStyle="bar"
|
|
776
|
+
/>
|
|
777
|
+
</div>
|
|
778
|
+
)}
|
|
779
|
+
</Section>
|
|
780
|
+
|
|
781
|
+
<Section title="SettingGroup" description="Grouped settings rows with label and dividers">
|
|
782
|
+
<SettingGroup
|
|
783
|
+
label="Preferences"
|
|
784
|
+
items={[
|
|
785
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
|
786
|
+
<div>
|
|
787
|
+
<Text size="sm" weight="medium">Notifications</Text>
|
|
788
|
+
<Text size="xs" style={{ opacity: 0.5 }}>Receive push notifications</Text>
|
|
789
|
+
</div>
|
|
790
|
+
<Text size="sm" style={{ opacity: 0.4 }}>On</Text>
|
|
791
|
+
</div>,
|
|
792
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
|
793
|
+
<div>
|
|
794
|
+
<Text size="sm" weight="medium">Dark mode</Text>
|
|
795
|
+
<Text size="xs" style={{ opacity: 0.5 }}>Use system default</Text>
|
|
796
|
+
</div>
|
|
797
|
+
<Text size="sm" style={{ opacity: 0.4 }}>Auto</Text>
|
|
798
|
+
</div>,
|
|
799
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
|
800
|
+
<div>
|
|
801
|
+
<Text size="sm" weight="medium">Language</Text>
|
|
802
|
+
<Text size="xs" style={{ opacity: 0.5 }}>Display language</Text>
|
|
803
|
+
</div>
|
|
804
|
+
<Text size="sm" style={{ opacity: 0.4 }}>English</Text>
|
|
805
|
+
</div>,
|
|
806
|
+
]}
|
|
807
|
+
/>
|
|
808
|
+
</Section>
|
|
809
|
+
|
|
810
|
+
</div>
|
|
811
|
+
</ScrollView>
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ─── Tab: Advanced ─────────────────────────────────────────────────────────────
|
|
816
|
+
|
|
817
|
+
function AdvancedTab(): React.ReactElement {
|
|
818
|
+
const [drawingDone, setDrawingDone] = useState(false);
|
|
819
|
+
const [drawnImage, setDrawnImage] = useState<string | null>(null);
|
|
820
|
+
const [staggerKey, setStaggerKey] = useState(0);
|
|
821
|
+
const opacityAnim = useAnimatedValue(1);
|
|
822
|
+
const [isHidden, setIsHidden] = useState(false);
|
|
823
|
+
|
|
824
|
+
function handleDrawingComplete(result: ImageResult): void {
|
|
825
|
+
setDrawingDone(true);
|
|
826
|
+
setDrawnImage(`data:${result.mime};base64,${result.base64}`);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function toggleOpacity(): void {
|
|
830
|
+
const target = isHidden ? 1 : 0.15;
|
|
831
|
+
void opacityAnim.start(target);
|
|
832
|
+
setIsHidden((prev) => !prev);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return (
|
|
836
|
+
<ScrollView>
|
|
837
|
+
<div style={{ maxWidth: 640, margin: '0 auto', padding: '16px 16px 48px' }}>
|
|
838
|
+
|
|
839
|
+
<Section title="DrawingCanvas" description="Freehand drawing canvas with color picker and eraser">
|
|
840
|
+
<div style={{ maxWidth: 400, margin: '0 auto' }}>
|
|
841
|
+
{drawingDone ? (
|
|
842
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
843
|
+
{drawnImage && (
|
|
844
|
+
<img
|
|
845
|
+
src={drawnImage}
|
|
846
|
+
alt="Your drawing"
|
|
847
|
+
style={{ width: '100%', borderRadius: 8, border: '1px solid rgba(0,0,0,0.1)' }}
|
|
848
|
+
/>
|
|
849
|
+
)}
|
|
850
|
+
<Button
|
|
851
|
+
variant="secondary"
|
|
852
|
+
onClick={() => { setDrawingDone(false); setDrawnImage(null); }}
|
|
853
|
+
>
|
|
854
|
+
Draw Again
|
|
855
|
+
</Button>
|
|
856
|
+
</div>
|
|
857
|
+
) : (
|
|
858
|
+
<DrawingCanvas
|
|
859
|
+
onComplete={handleDrawingComplete}
|
|
860
|
+
onCancel={() => { setDrawingDone(false); setDrawnImage(null); }}
|
|
861
|
+
size={512}
|
|
862
|
+
/>
|
|
863
|
+
)}
|
|
864
|
+
</div>
|
|
865
|
+
</Section>
|
|
866
|
+
|
|
867
|
+
<Section title="TransformWrapper / TransformComponent" description="Pinch-to-zoom and pan for any content">
|
|
868
|
+
<Text size="xs" style={{ opacity: 0.5, marginBottom: 8 }}>Scroll or pinch to zoom, drag to pan</Text>
|
|
869
|
+
<div style={{ overflow: 'hidden', borderRadius: 8, border: '1px solid rgba(0,0,0,0.08)' }}>
|
|
870
|
+
<TransformWrapper>
|
|
871
|
+
<TransformComponent>
|
|
872
|
+
<img
|
|
873
|
+
src="https://placehold.co/400x250"
|
|
874
|
+
alt="Zoom me"
|
|
875
|
+
style={{ width: 400, height: 250, display: 'block' }}
|
|
876
|
+
/>
|
|
877
|
+
</TransformComponent>
|
|
878
|
+
</TransformWrapper>
|
|
879
|
+
</div>
|
|
880
|
+
</Section>
|
|
881
|
+
|
|
882
|
+
<Section title="ScrollAnimatedView" description="Animate children when they scroll into view">
|
|
883
|
+
<Text size="xs" style={{ opacity: 0.5, marginBottom: 8 }}>
|
|
884
|
+
Items below animate in as they enter the viewport (scroll down if needed)
|
|
885
|
+
</Text>
|
|
886
|
+
{['First item', 'Second item', 'Third item'].map((text, i) => (
|
|
887
|
+
<ScrollAnimatedView key={text} animation="slideUp" delay={i * 100} threshold={0.1}>
|
|
888
|
+
<div style={{ padding: '12px 16px', marginBottom: 8, borderRadius: 8, background: 'rgba(0,0,0,0.04)', width: '100%' }}>
|
|
889
|
+
<Text size="sm">{text}</Text>
|
|
890
|
+
</div>
|
|
891
|
+
</ScrollAnimatedView>
|
|
892
|
+
))}
|
|
893
|
+
</Section>
|
|
894
|
+
|
|
895
|
+
<Section title="StaggeredAnimationContainer" description="Staggered entry animation for a group of children">
|
|
896
|
+
<div style={{ marginBottom: 10 }}>
|
|
897
|
+
<Button variant="secondary" onClick={() => setStaggerKey((k) => k + 1)}>
|
|
898
|
+
Replay
|
|
899
|
+
</Button>
|
|
900
|
+
</div>
|
|
901
|
+
<StaggeredAnimationContainer key={staggerKey} animation="fadeIn" baseDelay={100}>
|
|
902
|
+
{['Item A', 'Item B', 'Item C', 'Item D'].map((label) => (
|
|
903
|
+
<div
|
|
904
|
+
key={label}
|
|
905
|
+
style={{ padding: '8px 12px', marginBottom: 6, borderRadius: 6, background: 'rgba(0,0,0,0.04)' }}
|
|
906
|
+
>
|
|
907
|
+
<Text size="sm">{label}</Text>
|
|
908
|
+
</div>
|
|
909
|
+
))}
|
|
910
|
+
</StaggeredAnimationContainer>
|
|
911
|
+
</Section>
|
|
912
|
+
|
|
913
|
+
<Section title="Animated + useAnimatedValue" description="Animate any CSS property with spring physics">
|
|
914
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
915
|
+
<Button variant="secondary" onClick={toggleOpacity}>
|
|
916
|
+
{isHidden ? 'Fade in' : 'Fade out'}
|
|
917
|
+
</Button>
|
|
918
|
+
<Animated.div
|
|
919
|
+
style={{
|
|
920
|
+
opacity: opacityAnim,
|
|
921
|
+
padding: 16,
|
|
922
|
+
borderRadius: 8,
|
|
923
|
+
background: 'var(--app-primary, #2563eb)',
|
|
924
|
+
}}
|
|
925
|
+
>
|
|
926
|
+
<Text weight="bold" style={{ color: 'white' }}>Animated opacity via useAnimatedValue</Text>
|
|
927
|
+
</Animated.div>
|
|
928
|
+
</div>
|
|
929
|
+
</Section>
|
|
930
|
+
|
|
931
|
+
<Section title="FeedbackButton" description="Built-in user feedback button (always present at bottom-right via AppProvider)">
|
|
932
|
+
<Text size="sm" style={{ opacity: 0.6 }}>
|
|
933
|
+
The feedback button is automatically added by <code style={{ fontSize: 12 }}>AppProvider</code> and
|
|
934
|
+
appears at the bottom-right of every page. You can find it at{' '}
|
|
935
|
+
<code style={{ fontSize: 12 }}>[data-id="feedback-button"]</code>.
|
|
936
|
+
</Text>
|
|
937
|
+
<div style={{ marginTop: 12 }}>
|
|
938
|
+
<FeedbackButton />
|
|
939
|
+
</div>
|
|
940
|
+
</Section>
|
|
941
|
+
|
|
942
|
+
</div>
|
|
943
|
+
</ScrollView>
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ─── Page shell ────────────────────────────────────────────────────────────────
|
|
948
|
+
|
|
949
|
+
type TabId = 'layout' | 'inputs' | 'feedback' | 'lists' | 'navigation' | 'advanced';
|
|
950
|
+
const TABS: TabId[] = ['layout', 'inputs', 'feedback', 'lists', 'navigation', 'advanced'];
|
|
951
|
+
|
|
952
|
+
export default function UIComponentsPage(): React.ReactElement {
|
|
953
|
+
const [selectedTab, setSelectedTab] = useSelectedTab<TabId>('layout', TABS, 'ui-components-tab');
|
|
954
|
+
|
|
955
|
+
const tabDictionary: TabPickerDictionary<TabId> = {
|
|
956
|
+
layout: { text: 'Layout', content: <LayoutTab /> },
|
|
957
|
+
inputs: { text: 'Inputs', content: <InputsTab /> },
|
|
958
|
+
feedback: { text: 'Feedback', content: <FeedbackTab /> },
|
|
959
|
+
lists: { text: 'Lists', content: <ListsTab /> },
|
|
960
|
+
navigation: { text: 'Navigation', content: <NavigationTab /> },
|
|
961
|
+
advanced: { text: 'Advanced', content: <AdvancedTab /> },
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
return (
|
|
965
|
+
<PageLayout
|
|
966
|
+
header={
|
|
967
|
+
<div>
|
|
968
|
+
<div style={{ padding: '12px 16px 4px' }}>
|
|
969
|
+
<a href="/" style={{ fontSize: 13, opacity: 0.5, textDecoration: 'none' }}>← Home</a>
|
|
970
|
+
</div>
|
|
971
|
+
<div style={{ padding: '4px 16px 8px' }}>
|
|
972
|
+
<Text size="xl" weight="bold">UI Components</Text>
|
|
973
|
+
<Text size="sm" style={{ opacity: 0.5 }}>Live demos of all built-in components</Text>
|
|
974
|
+
</div>
|
|
975
|
+
<TabPicker
|
|
976
|
+
tabs={TABS}
|
|
977
|
+
tabDictionary={tabDictionary}
|
|
978
|
+
selectedTab={selectedTab}
|
|
979
|
+
setSelectedTab={setSelectedTab}
|
|
980
|
+
/>
|
|
981
|
+
</div>
|
|
982
|
+
}
|
|
983
|
+
>
|
|
984
|
+
<TabContent tabs={TABS} tabDictionary={tabDictionary} selectedTab={selectedTab} />
|
|
985
|
+
</PageLayout>
|
|
986
|
+
);
|
|
987
|
+
}
|
|
@@ -18,6 +18,7 @@ export const pages = definePages({
|
|
|
18
18
|
'user/:userId': definePage<{ userId: string }>(),
|
|
19
19
|
'search': definePage<{ q?: string }>({ auth: false }),
|
|
20
20
|
'ai-test': definePage<{}>({ auth: true }),
|
|
21
|
+
'ui-components': definePage<{}>({ auth: false }),
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
export type AppPages = typeof pages;
|