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.
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.76";
1
+ export declare const CLI_VERSION = "0.1.77";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.76";
2
+ export const CLI_VERSION = "0.1.77";
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-app",
3
- "version": "0.1.76",
3
+ "version": "0.1.77",
4
4
  "type": "module",
5
5
  "main": "./dist/server/index.js",
6
6
  "exports": {
@@ -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;