@a4ui/core 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -37
- package/dist/elements.css +79 -9
- package/dist/full.css +3112 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +3392 -2537
- package/dist/lib/flyToCart.d.ts +25 -0
- package/dist/ui/Curtain.d.ts +44 -0
- package/dist/ui/Expandable.d.ts +34 -0
- package/dist/ui/FillText.d.ts +18 -0
- package/dist/ui/HoldToConfirm.d.ts +24 -0
- package/dist/ui/LoadingDots.d.ts +20 -0
- package/dist/ui/MultiStateBadge.d.ts +23 -0
- package/dist/ui/NotificationStack.d.ts +32 -0
- package/dist/ui/NowPlaying.d.ts +21 -0
- package/dist/ui/Parallax.d.ts +21 -0
- package/dist/ui/ScrambleText.d.ts +22 -0
- package/dist/ui/TextReveal.d.ts +23 -0
- package/package.json +8 -1
- package/src/charts/BarChart.tsx +55 -0
- package/src/charts/DonutChart.tsx +127 -0
- package/src/charts/Sparkline.tsx +101 -0
- package/src/charts/index.ts +5 -0
- package/src/commerce/CartLine.tsx +84 -0
- package/src/commerce/CartSummary.tsx +70 -0
- package/src/commerce/FilterGroup.tsx +70 -0
- package/src/commerce/PriceTag.tsx +62 -0
- package/src/commerce/ProductCard.tsx +121 -0
- package/src/commerce/ProductGrid.tsx +27 -0
- package/src/commerce/QuantityStepper.tsx +59 -0
- package/src/commerce/index.ts +10 -0
- package/src/elements.tsx +167 -0
- package/src/index.ts +186 -0
- package/src/layout/AppShell.tsx +102 -0
- package/src/layout/ChristmasBackground.tsx +177 -0
- package/src/layout/EffectsToggle.tsx +41 -0
- package/src/layout/NavGroup.tsx +45 -0
- package/src/layout/SnowScenery.tsx +76 -0
- package/src/layout/SpaceBackground.tsx +459 -0
- package/src/layout/ThemeToggle.tsx +40 -0
- package/src/layout/ThemedScenery.tsx +179 -0
- package/src/layout/sceneEffects.ts +49 -0
- package/src/lib/cn.ts +17 -0
- package/src/lib/effects.ts +71 -0
- package/src/lib/flyToCart.ts +103 -0
- package/src/lib/media.ts +27 -0
- package/src/lib/motion.ts +191 -0
- package/src/lib/theme.ts +113 -0
- package/src/lib/virtual.ts +33 -0
- package/src/styles/space.css +510 -0
- package/src/styles/tokens.css +213 -0
- package/src/themes/index.ts +102 -0
- package/src/themes/palettes.ts +368 -0
- package/src/ui/Accordion.tsx +65 -0
- package/src/ui/Affix.tsx +72 -0
- package/src/ui/Alert.tsx +55 -0
- package/src/ui/AlertDialog.tsx +58 -0
- package/src/ui/Anchor.tsx +100 -0
- package/src/ui/Avatar.tsx +34 -0
- package/src/ui/AvatarGroup.tsx +55 -0
- package/src/ui/BackToTop.tsx +51 -0
- package/src/ui/Badge.tsx +46 -0
- package/src/ui/BottomNavigation.tsx +65 -0
- package/src/ui/Breadcrumb.tsx +64 -0
- package/src/ui/Button.tsx +49 -0
- package/src/ui/Calendar.tsx +40 -0
- package/src/ui/CalendarHeatmap.tsx +180 -0
- package/src/ui/Card.tsx +84 -0
- package/src/ui/Carousel.tsx +121 -0
- package/src/ui/Cascader.tsx +158 -0
- package/src/ui/Checkbox.tsx +35 -0
- package/src/ui/Clock.tsx +221 -0
- package/src/ui/Collapse.tsx +48 -0
- package/src/ui/ColorPicker.tsx +89 -0
- package/src/ui/Combobox.tsx +67 -0
- package/src/ui/Command.tsx +158 -0
- package/src/ui/Comment.tsx +91 -0
- package/src/ui/ContextMenu.tsx +58 -0
- package/src/ui/Countdown.tsx +123 -0
- package/src/ui/Curtain.tsx +348 -0
- package/src/ui/DataGrid.tsx +177 -0
- package/src/ui/DateField.tsx +177 -0
- package/src/ui/DateRangePicker.tsx +204 -0
- package/src/ui/DateTimeField.tsx +68 -0
- package/src/ui/Descriptions.tsx +56 -0
- package/src/ui/Drawer.tsx +72 -0
- package/src/ui/Dropdown.tsx +71 -0
- package/src/ui/Dropzone.tsx +88 -0
- package/src/ui/Empty.tsx +53 -0
- package/src/ui/Expandable.tsx +229 -0
- package/src/ui/FileUpload.tsx +198 -0
- package/src/ui/FillText.tsx +63 -0
- package/src/ui/FloatingActionButton.tsx +48 -0
- package/src/ui/Form.tsx +108 -0
- package/src/ui/Highlight.tsx +53 -0
- package/src/ui/HoldToConfirm.tsx +141 -0
- package/src/ui/HoverCard.tsx +42 -0
- package/src/ui/Image.tsx +66 -0
- package/src/ui/Input.tsx +38 -0
- package/src/ui/Kbd.tsx +25 -0
- package/src/ui/List.tsx +71 -0
- package/src/ui/LoadingDots.tsx +75 -0
- package/src/ui/Marquee.tsx +48 -0
- package/src/ui/Mentions.tsx +170 -0
- package/src/ui/Meter.tsx +57 -0
- package/src/ui/Modal.tsx +106 -0
- package/src/ui/MultiSelect.tsx +236 -0
- package/src/ui/MultiStateBadge.tsx +93 -0
- package/src/ui/NotificationCenter.tsx +103 -0
- package/src/ui/NotificationStack.tsx +104 -0
- package/src/ui/NowPlaying.tsx +100 -0
- package/src/ui/NumberInput.tsx +48 -0
- package/src/ui/PageHeader.tsx +60 -0
- package/src/ui/Pagination.tsx +62 -0
- package/src/ui/Parallax.tsx +51 -0
- package/src/ui/Popover.tsx +42 -0
- package/src/ui/Portal.tsx +34 -0
- package/src/ui/Progress.tsx +55 -0
- package/src/ui/RadioGroup.tsx +65 -0
- package/src/ui/Rating.tsx +98 -0
- package/src/ui/Result.tsx +75 -0
- package/src/ui/RingProgress.tsx +75 -0
- package/src/ui/ScrambleText.tsx +95 -0
- package/src/ui/SegmentedControl.tsx +61 -0
- package/src/ui/Select.tsx +55 -0
- package/src/ui/Separator.tsx +39 -0
- package/src/ui/Skeleton.tsx +23 -0
- package/src/ui/Slider.tsx +57 -0
- package/src/ui/Sortable.tsx +174 -0
- package/src/ui/SpeedDial.tsx +85 -0
- package/src/ui/Spinner.tsx +33 -0
- package/src/ui/Splitter.tsx +115 -0
- package/src/ui/Stat.tsx +74 -0
- package/src/ui/Stepper.tsx +134 -0
- package/src/ui/Switch.tsx +44 -0
- package/src/ui/Table.tsx +128 -0
- package/src/ui/Tabs.tsx +72 -0
- package/src/ui/TagInput.tsx +85 -0
- package/src/ui/TextReveal.tsx +93 -0
- package/src/ui/Textarea.tsx +38 -0
- package/src/ui/TimeField.tsx +298 -0
- package/src/ui/Timeline.tsx +89 -0
- package/src/ui/Toast.tsx +68 -0
- package/src/ui/Toggle.tsx +41 -0
- package/src/ui/ToggleGroup.tsx +59 -0
- package/src/ui/Tooltip.tsx +42 -0
- package/src/ui/Tour.tsx +197 -0
- package/src/ui/Transfer.tsx +112 -0
- package/src/ui/Tree.tsx +100 -0
- package/src/ui/TreeSelect.tsx +151 -0
- package/src/ui/VirtualList.tsx +81 -0
- package/src/ui/internal/CalendarCore.tsx +276 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// A4ui — public entry point.
|
|
2
|
+
//
|
|
3
|
+
// Components are extracted incrementally from the source project
|
|
4
|
+
// (sonora precision/alfredorust/solid/src/components). As each lands under
|
|
5
|
+
// src/ui/ or src/layout/, re-export it here. See CLAUDE.md → "Extraction plan".
|
|
6
|
+
//
|
|
7
|
+
// Consumers import the styles once (tokens + motion) and the components:
|
|
8
|
+
// import '@a4ui/core/styles.css'
|
|
9
|
+
// import { Button, Card, Modal } from '@a4ui/core'
|
|
10
|
+
|
|
11
|
+
export const A4UI_VERSION = '0.13.0'
|
|
12
|
+
|
|
13
|
+
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
|
+
export { cn } from './lib/cn'
|
|
15
|
+
export { useTheme, toggleTheme, setTheme, storedTheme, applyTheme, toggled, type Theme } from './lib/theme'
|
|
16
|
+
export { useEffects, isCalm, setEffects } from './lib/effects'
|
|
17
|
+
export {
|
|
18
|
+
prefersReducedMotion,
|
|
19
|
+
motionReduced,
|
|
20
|
+
useMotionForced,
|
|
21
|
+
setMotionForced,
|
|
22
|
+
createCountUp,
|
|
23
|
+
animateIn,
|
|
24
|
+
revealOnScroll,
|
|
25
|
+
// Motion (motion.dev) imperative API, re-exported so the whole app shares one
|
|
26
|
+
// engine (used by Stat's entrance / count-up; reach for these for scroll-linked
|
|
27
|
+
// reveals, staggered lists, springs, etc.).
|
|
28
|
+
animate,
|
|
29
|
+
inView,
|
|
30
|
+
scroll,
|
|
31
|
+
stagger,
|
|
32
|
+
spring,
|
|
33
|
+
} from './lib/motion'
|
|
34
|
+
export { useMediaQuery } from './lib/media'
|
|
35
|
+
export { remeasureAfterLayout } from './lib/virtual'
|
|
36
|
+
|
|
37
|
+
// UI components (src/ui) — all 18 extracted. See CLAUDE.md.
|
|
38
|
+
export { Accordion, type AccordionItem } from './ui/Accordion'
|
|
39
|
+
export { Badge, type BadgeTone } from './ui/Badge'
|
|
40
|
+
export { Button, type ButtonVariant } from './ui/Button'
|
|
41
|
+
export { Card, CardHeader, CardTitle, CardContent } from './ui/Card'
|
|
42
|
+
export { Checkbox } from './ui/Checkbox'
|
|
43
|
+
export { DateField } from './ui/DateField'
|
|
44
|
+
export { TimeField, type TimeFieldProps } from './ui/TimeField'
|
|
45
|
+
export { DateTimeField, type DateTimeFieldProps } from './ui/DateTimeField'
|
|
46
|
+
export { DateRangePicker, type DateRange, type DateRangePickerProps } from './ui/DateRangePicker'
|
|
47
|
+
export { FormField, FormLabel, FormControl, FormDescription, FormError, type FormFieldProps } from './ui/Form'
|
|
48
|
+
export { FileUpload, type UploadFile, type FileUploadProps } from './ui/FileUpload'
|
|
49
|
+
export { MultiSelect, type MultiSelectOption, type MultiSelectProps } from './ui/MultiSelect'
|
|
50
|
+
export { Drawer } from './ui/Drawer'
|
|
51
|
+
export { Dropdown, type DropdownItem } from './ui/Dropdown'
|
|
52
|
+
export { Dropzone } from './ui/Dropzone'
|
|
53
|
+
export { Input } from './ui/Input'
|
|
54
|
+
export { Modal } from './ui/Modal'
|
|
55
|
+
export { PageHeader } from './ui/PageHeader'
|
|
56
|
+
export { Pagination } from './ui/Pagination'
|
|
57
|
+
export { Select } from './ui/Select'
|
|
58
|
+
export { Spinner } from './ui/Spinner'
|
|
59
|
+
export { Stat, type StatTone } from './ui/Stat'
|
|
60
|
+
export { Table, TableHead, TableBody, TableRow, TableHeadCell, TableCell } from './ui/Table'
|
|
61
|
+
export { VirtualList } from './ui/VirtualList'
|
|
62
|
+
export { Tabs, type TabItem } from './ui/Tabs'
|
|
63
|
+
export { toast, Toaster, type ToastTone } from './ui/Toast'
|
|
64
|
+
export { Switch } from './ui/Switch'
|
|
65
|
+
export { RadioGroup, type RadioOption } from './ui/RadioGroup'
|
|
66
|
+
export { Textarea } from './ui/Textarea'
|
|
67
|
+
export { Tooltip } from './ui/Tooltip'
|
|
68
|
+
export { Popover } from './ui/Popover'
|
|
69
|
+
export { HoverCard } from './ui/HoverCard'
|
|
70
|
+
export { Alert, type AlertTone } from './ui/Alert'
|
|
71
|
+
export { Avatar } from './ui/Avatar'
|
|
72
|
+
export { Skeleton } from './ui/Skeleton'
|
|
73
|
+
export { Separator } from './ui/Separator'
|
|
74
|
+
export { Progress } from './ui/Progress'
|
|
75
|
+
export { Meter } from './ui/Meter'
|
|
76
|
+
export { Slider } from './ui/Slider'
|
|
77
|
+
export { NumberInput } from './ui/NumberInput'
|
|
78
|
+
export { Toggle } from './ui/Toggle'
|
|
79
|
+
export { ToggleGroup, type ToggleGroupOption } from './ui/ToggleGroup'
|
|
80
|
+
export { SegmentedControl, type SegmentedOption } from './ui/SegmentedControl'
|
|
81
|
+
export { Breadcrumb, type BreadcrumbItem } from './ui/Breadcrumb'
|
|
82
|
+
export { AlertDialog } from './ui/AlertDialog'
|
|
83
|
+
export { ContextMenu, type ContextMenuItem } from './ui/ContextMenu'
|
|
84
|
+
export { Combobox } from './ui/Combobox'
|
|
85
|
+
export { Carousel, type CarouselProps } from './ui/Carousel'
|
|
86
|
+
export { Stepper, type StepItem, type StepperProps } from './ui/Stepper'
|
|
87
|
+
export { Timeline, type TimelineItem, type TimelineTone, type TimelineProps } from './ui/Timeline'
|
|
88
|
+
export { Rating, type RatingProps } from './ui/Rating'
|
|
89
|
+
export { Empty, type EmptyProps } from './ui/Empty'
|
|
90
|
+
export { Calendar, type CalendarProps } from './ui/Calendar'
|
|
91
|
+
export { Tree, type TreeNode, type TreeProps } from './ui/Tree'
|
|
92
|
+
export { Kbd } from './ui/Kbd'
|
|
93
|
+
export { AvatarGroup, type AvatarGroupProps } from './ui/AvatarGroup'
|
|
94
|
+
export { Descriptions, type DescriptionItem, type DescriptionsProps } from './ui/Descriptions'
|
|
95
|
+
export { Result, type ResultStatus, type ResultProps } from './ui/Result'
|
|
96
|
+
export { Splitter, type SplitterProps } from './ui/Splitter'
|
|
97
|
+
export { Command, type CommandItem, type CommandProps } from './ui/Command'
|
|
98
|
+
export { TagInput, type TagInputProps } from './ui/TagInput'
|
|
99
|
+
export { Collapse } from './ui/Collapse'
|
|
100
|
+
export { RingProgress, type RingProgressProps } from './ui/RingProgress'
|
|
101
|
+
export { BackToTop, type BackToTopProps } from './ui/BackToTop'
|
|
102
|
+
export {
|
|
103
|
+
FloatingActionButton,
|
|
104
|
+
type FloatingActionButtonProps,
|
|
105
|
+
type FloatingActionButtonPosition,
|
|
106
|
+
} from './ui/FloatingActionButton'
|
|
107
|
+
export { Anchor, type AnchorItem, type AnchorProps } from './ui/Anchor'
|
|
108
|
+
export { Highlight, type HighlightProps } from './ui/Highlight'
|
|
109
|
+
export { List, type ListItem } from './ui/List'
|
|
110
|
+
export { Countdown, type CountdownProps } from './ui/Countdown'
|
|
111
|
+
export { Affix } from './ui/Affix'
|
|
112
|
+
export { Transfer, type TransferItem, type TransferProps } from './ui/Transfer'
|
|
113
|
+
export { Image, type ImageProps } from './ui/Image'
|
|
114
|
+
export { SpeedDial, type SpeedDialAction, type SpeedDialProps } from './ui/SpeedDial'
|
|
115
|
+
export { BottomNavigation, type BottomNavItem, type BottomNavigationProps } from './ui/BottomNavigation'
|
|
116
|
+
export { Marquee } from './ui/Marquee'
|
|
117
|
+
export { DataGrid, type DataGridColumn, type DataGridProps } from './ui/DataGrid'
|
|
118
|
+
export { TreeSelect, type TreeSelectProps } from './ui/TreeSelect'
|
|
119
|
+
export { Cascader, type CascaderOption, type CascaderProps } from './ui/Cascader'
|
|
120
|
+
export { Mentions, type MentionOption, type MentionsProps } from './ui/Mentions'
|
|
121
|
+
export { Tour, type TourStep, type TourProps } from './ui/Tour'
|
|
122
|
+
export {
|
|
123
|
+
NotificationCenter,
|
|
124
|
+
type NotificationItem,
|
|
125
|
+
type NotificationCenterProps,
|
|
126
|
+
} from './ui/NotificationCenter'
|
|
127
|
+
export { ColorPicker, type ColorPickerProps } from './ui/ColorPicker'
|
|
128
|
+
export { Comment, type CommentData, type CommentProps } from './ui/Comment'
|
|
129
|
+
export { CalendarHeatmap, type HeatmapValue, type CalendarHeatmapProps } from './ui/CalendarHeatmap'
|
|
130
|
+
export { Portal, type PortalProps } from './ui/Portal'
|
|
131
|
+
export { Sortable, type SortableProps } from './ui/Sortable'
|
|
132
|
+
export { Clock, type ClockProps } from './ui/Clock'
|
|
133
|
+
|
|
134
|
+
// Motion components (src/ui) — animation primitives built on the `motion` engine
|
|
135
|
+
// (adapted from motion.dev examples). All tree-shakeable and reduced-motion aware;
|
|
136
|
+
// `motion` is external, so importing one of these is the only thing that pulls it.
|
|
137
|
+
export { ScrambleText, type ScrambleTextProps } from './ui/ScrambleText'
|
|
138
|
+
export { TextReveal, type TextRevealProps } from './ui/TextReveal'
|
|
139
|
+
export { HoldToConfirm, type HoldToConfirmProps } from './ui/HoldToConfirm'
|
|
140
|
+
export { LoadingDots, type LoadingDotsProps } from './ui/LoadingDots'
|
|
141
|
+
export { Curtain, type CurtainProps, type CurtainVariant } from './ui/Curtain'
|
|
142
|
+
export { Parallax, type ParallaxProps } from './ui/Parallax'
|
|
143
|
+
export { FillText, type FillTextProps } from './ui/FillText'
|
|
144
|
+
export {
|
|
145
|
+
NotificationStack,
|
|
146
|
+
type NotificationStackProps,
|
|
147
|
+
type StackNotification,
|
|
148
|
+
} from './ui/NotificationStack'
|
|
149
|
+
export { MultiStateBadge, type MultiStateBadgeProps, type BadgeState } from './ui/MultiStateBadge'
|
|
150
|
+
export { NowPlaying, type NowPlayingProps } from './ui/NowPlaying'
|
|
151
|
+
export { Expandable, type ExpandableProps } from './ui/Expandable'
|
|
152
|
+
export { flyToCart, type FlyToCartOptions } from './lib/flyToCart'
|
|
153
|
+
|
|
154
|
+
// Layout (src/layout) — generic shell + backdrop + toggles. App-coupled pieces
|
|
155
|
+
// (Sidebar, Topbar, CompanySwitcher, DemoBanner, CommandPalette) stay in the app.
|
|
156
|
+
export { AppShell } from './layout/AppShell'
|
|
157
|
+
export { SpaceBackground } from './layout/SpaceBackground'
|
|
158
|
+
export { ThemedScenery, type ThemedSceneryProps } from './layout/ThemedScenery'
|
|
159
|
+
export { SnowScenery } from './layout/SnowScenery'
|
|
160
|
+
export { ChristmasBackground } from './layout/ChristmasBackground'
|
|
161
|
+
export { ThemeToggle } from './layout/ThemeToggle'
|
|
162
|
+
export { EffectsToggle } from './layout/EffectsToggle'
|
|
163
|
+
export { NavGroup } from './layout/NavGroup'
|
|
164
|
+
|
|
165
|
+
// Themes (src/themes) — swappable color palettes (Space is the default). Pick one
|
|
166
|
+
// with `selectTheme('dino')`, restore the saved one with `initTheme()`, or build
|
|
167
|
+
// your own from a ThemeDefinition. Distinct from the light/dark `setTheme` above.
|
|
168
|
+
export {
|
|
169
|
+
themes,
|
|
170
|
+
space,
|
|
171
|
+
dino,
|
|
172
|
+
doctor,
|
|
173
|
+
scientist,
|
|
174
|
+
soccer,
|
|
175
|
+
snow,
|
|
176
|
+
christmas,
|
|
177
|
+
activeTheme,
|
|
178
|
+
selectTheme,
|
|
179
|
+
applyThemeDefinition,
|
|
180
|
+
initTheme,
|
|
181
|
+
themeToCss,
|
|
182
|
+
themeToJson,
|
|
183
|
+
TOKEN_ORDER,
|
|
184
|
+
type ThemeDefinition,
|
|
185
|
+
type Palette,
|
|
186
|
+
} from './themes'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Generic app shell: the fixed SpaceBackground (z-0) behind a flex layout of an
|
|
2
|
+
// optional sidebar slot + a content column (optional banner + topbar + routed
|
|
3
|
+
// <main>). Unlike the source app's AppShell, this is slot-based and unopinionated
|
|
4
|
+
// about the sidebar — pass your own via the `sidebar` prop, and it manages its
|
|
5
|
+
// own width/collapse (the flex layout reflows the content column automatically,
|
|
6
|
+
// so no --sidebar-w padding coupling). The <main> is wrapped in Suspense + an
|
|
7
|
+
// ErrorBoundary so a route chunk failing or still loading never blanks the shell.
|
|
8
|
+
import { ErrorBoundary, Show, Suspense, type JSX, type ParentProps } from 'solid-js'
|
|
9
|
+
|
|
10
|
+
import { Button } from '../ui/Button'
|
|
11
|
+
import { Spinner } from '../ui/Spinner'
|
|
12
|
+
import { SpaceBackground } from './SpaceBackground'
|
|
13
|
+
|
|
14
|
+
/** Props for {@link AppShell}. All slots are optional — pass only what your app needs. */
|
|
15
|
+
interface AppShellProps extends ParentProps {
|
|
16
|
+
/** Left column (e.g. a Sidebar). Rendered as a flex child; owns its own width. */
|
|
17
|
+
sidebar?: JSX.Element
|
|
18
|
+
/** Top bar inside the content column, above <main>. */
|
|
19
|
+
topbar?: JSX.Element
|
|
20
|
+
/** Full-width strip above the topbar (e.g. a demo/announcement banner). */
|
|
21
|
+
banner?: JSX.Element
|
|
22
|
+
/** Fixed z-0 backdrop. Defaults to <SpaceBackground/>; pass a node to override,
|
|
23
|
+
or `null` for a plain background. */
|
|
24
|
+
background?: JSX.Element
|
|
25
|
+
/** Max content width for <main>. Default '1400px'. */
|
|
26
|
+
maxWidth?: string
|
|
27
|
+
/** Override the default error fallback shown when a route throws. Receives
|
|
28
|
+
the caught error and a `reset` callback that re-renders the ErrorBoundary's
|
|
29
|
+
children. */
|
|
30
|
+
errorFallback?: (err: unknown, reset: () => void) => JSX.Element
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function defaultErrorFallback(err: unknown, reset: () => void): JSX.Element {
|
|
34
|
+
return (
|
|
35
|
+
<div class="mx-auto max-w-md py-20 text-center">
|
|
36
|
+
<p class="text-lg font-semibold">Something went wrong loading this page</p>
|
|
37
|
+
<p class="mt-1 break-words text-sm text-muted-foreground">{String((err as Error)?.message ?? err)}</p>
|
|
38
|
+
<div class="mt-4 flex justify-center gap-2">
|
|
39
|
+
<Button variant="outline" onClick={reset}>
|
|
40
|
+
Retry
|
|
41
|
+
</Button>
|
|
42
|
+
<Button onClick={() => window.location.reload()}>Reload</Button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generic app shell: a fixed {@link SpaceBackground} behind a flex layout of
|
|
50
|
+
* an optional sidebar slot plus a content column (optional banner + topbar +
|
|
51
|
+
* routed `<main>`). Slot-based and unopinionated about the sidebar — pass
|
|
52
|
+
* your own component via `sidebar` and it manages its own width/collapse.
|
|
53
|
+
* `<main>` is wrapped in `Suspense` + an `ErrorBoundary` so a route chunk
|
|
54
|
+
* failing or still loading never blanks the shell.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* <AppShell sidebar={<Sidebar />} topbar={<Topbar />}>
|
|
59
|
+
* <Route />
|
|
60
|
+
* </AppShell>
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function AppShell(props: ParentProps<AppShellProps>): JSX.Element {
|
|
64
|
+
const fallback = (err: unknown, reset: () => void) =>
|
|
65
|
+
(props.errorFallback ?? defaultErrorFallback)(err, reset)
|
|
66
|
+
return (
|
|
67
|
+
<div class="relative min-h-screen">
|
|
68
|
+
<Show when={props.background !== null} fallback={null}>
|
|
69
|
+
{props.background ?? <SpaceBackground />}
|
|
70
|
+
</Show>
|
|
71
|
+
<div class="relative z-10 flex min-h-screen">
|
|
72
|
+
{props.sidebar}
|
|
73
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
74
|
+
{props.banner}
|
|
75
|
+
{props.topbar}
|
|
76
|
+
<main
|
|
77
|
+
class="relative mx-auto w-full min-w-0 flex-1 px-4 py-6 sm:px-6"
|
|
78
|
+
style={{ 'max-width': props.maxWidth ?? '1400px' }}
|
|
79
|
+
>
|
|
80
|
+
{/* Never blank the content column: Suspense shows a spinner while a
|
|
81
|
+
route's chunk streams in, and the ErrorBoundary catches any other
|
|
82
|
+
runtime error with a friendly retry/reload — the shell stays put. */}
|
|
83
|
+
<ErrorBoundary fallback={fallback}>
|
|
84
|
+
<Suspense
|
|
85
|
+
fallback={
|
|
86
|
+
<div class="flex justify-center py-24 text-muted-foreground">
|
|
87
|
+
<Spinner />
|
|
88
|
+
</div>
|
|
89
|
+
}
|
|
90
|
+
>
|
|
91
|
+
{/* Routed content. No page cross-fade transition: solid-transition-group
|
|
92
|
+
left the outgoing (lazy) page mounted, which stacked its height
|
|
93
|
+
below the new page and created a huge phantom scroll region. */}
|
|
94
|
+
{props.children}
|
|
95
|
+
</Suspense>
|
|
96
|
+
</ErrorBoundary>
|
|
97
|
+
</main>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Christmas scenery for the "christmas" theme: a cozy night sky, drifting snow, a
|
|
2
|
+
// twinkling light garland across the top, a pine tree, and — instead of a
|
|
3
|
+
// shooting star — Santa's sleigh crossing the sky (ambiently + on a background
|
|
4
|
+
// click). Shares the cursor-glow / magnetic / edge-glow pointer effects. Motion
|
|
5
|
+
// is skipped under motionReduced().
|
|
6
|
+
import { createEffect, For, onCleanup, onMount, type JSX } from 'solid-js'
|
|
7
|
+
|
|
8
|
+
import { motionReduced } from '../lib/motion'
|
|
9
|
+
import { bindPointerFx } from './sceneEffects'
|
|
10
|
+
|
|
11
|
+
const FLAKE_COUNT = 60
|
|
12
|
+
const LIGHT_COLORS = ['#ff5a5a', '#4ade80', '#ffd23f', '#5aa9ff', '#ff8ad8']
|
|
13
|
+
const GARLAND = Array.from({ length: 22 }, (_, i) => i)
|
|
14
|
+
|
|
15
|
+
function buildFlakes(animate: boolean): string {
|
|
16
|
+
const rnd = Math.random
|
|
17
|
+
let html = ''
|
|
18
|
+
for (let i = 0; i < FLAKE_COUNT; i++) {
|
|
19
|
+
const size = (2 + rnd() * 4).toFixed(1)
|
|
20
|
+
const op = (0.4 + rnd() * 0.6).toFixed(2)
|
|
21
|
+
const left = (rnd() * 100).toFixed(2)
|
|
22
|
+
const dur = (6 + rnd() * 9).toFixed(1)
|
|
23
|
+
const delay = (-rnd() * 15).toFixed(1)
|
|
24
|
+
const sway = (rnd() * 2 - 1) * (8 + rnd() * 12)
|
|
25
|
+
const anim = animate
|
|
26
|
+
? `animation:snowFall ${dur}s linear ${delay}s infinite;`
|
|
27
|
+
: `top:${(rnd() * 100).toFixed(1)}%;`
|
|
28
|
+
html +=
|
|
29
|
+
`<span class="flake" style="left:${left}%;width:${size}px;height:${size}px;opacity:${op};` +
|
|
30
|
+
`--sway:${sway.toFixed(0)}px;${anim}"></span>`
|
|
31
|
+
}
|
|
32
|
+
return html
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Festive backdrop for the `christmas` theme: night sky, falling snow, a
|
|
37
|
+
* twinkling light garland, a pine tree, and Santa's sleigh flying across (ambient
|
|
38
|
+
* + on background click). Mount as {@link AppShell}'s `background`. Motion is
|
|
39
|
+
* skipped under `motionReduced()`.
|
|
40
|
+
*/
|
|
41
|
+
export function ChristmasBackground(): JSX.Element {
|
|
42
|
+
let root!: HTMLDivElement
|
|
43
|
+
let field!: HTMLDivElement
|
|
44
|
+
|
|
45
|
+
createEffect(() => {
|
|
46
|
+
field.innerHTML = buildFlakes(!motionReduced())
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
onMount(() => {
|
|
50
|
+
const cleanups: Array<() => void> = []
|
|
51
|
+
const timers: number[] = []
|
|
52
|
+
|
|
53
|
+
// Santa's sleigh crosses from one side to the other with a gentle arc.
|
|
54
|
+
const launchSanta = (rtl: boolean): void => {
|
|
55
|
+
const el = document.createElement('span')
|
|
56
|
+
el.textContent = '🎅🛷'
|
|
57
|
+
const top = 6 + Math.random() * 26
|
|
58
|
+
const size = 30 + Math.random() * 16
|
|
59
|
+
el.style.cssText =
|
|
60
|
+
`position:absolute;top:${top}vh;${rtl ? 'right' : 'left'}:-14%;font-size:${size.toFixed(0)}px;` +
|
|
61
|
+
`white-space:nowrap;pointer-events:none;filter:drop-shadow(0 0 6px hsl(var(--primary) / 0.5));`
|
|
62
|
+
if (rtl) el.style.transform = 'scaleX(-1)'
|
|
63
|
+
root.appendChild(el)
|
|
64
|
+
const dist = window.innerWidth * 1.3
|
|
65
|
+
const ex = rtl ? -dist : dist
|
|
66
|
+
const dur = 6500 + Math.random() * 4000
|
|
67
|
+
const anim = el.animate(
|
|
68
|
+
[
|
|
69
|
+
{ opacity: 0, transform: `translate(0,0) ${rtl ? 'scaleX(-1)' : ''}` },
|
|
70
|
+
{
|
|
71
|
+
opacity: 1,
|
|
72
|
+
transform: `translate(${ex * 0.15}px,-18px) ${rtl ? 'scaleX(-1)' : ''}`,
|
|
73
|
+
offset: 0.15,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
opacity: 1,
|
|
77
|
+
transform: `translate(${ex * 0.85}px,-18px) ${rtl ? 'scaleX(-1)' : ''}`,
|
|
78
|
+
offset: 0.85,
|
|
79
|
+
},
|
|
80
|
+
{ opacity: 0, transform: `translate(${ex}px,0) ${rtl ? 'scaleX(-1)' : ''}` },
|
|
81
|
+
],
|
|
82
|
+
{ duration: dur, easing: 'cubic-bezier(.4,0,.6,1)', fill: 'forwards' },
|
|
83
|
+
)
|
|
84
|
+
anim.onfinish = () => el.remove()
|
|
85
|
+
timers.push(window.setTimeout(() => el.remove(), dur + 400))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const INTERACTIVE =
|
|
89
|
+
'a, button, input, select, textarea, label, [role="button"], [role="tab"], [role="menuitem"], [role="dialog"], [contenteditable]'
|
|
90
|
+
const onClick = (e: MouseEvent): void => {
|
|
91
|
+
if ((e.target as Element | null)?.closest?.(INTERACTIVE)) return
|
|
92
|
+
launchSanta(Math.random() < 0.5)
|
|
93
|
+
}
|
|
94
|
+
document.addEventListener('click', onClick)
|
|
95
|
+
cleanups.push(() => document.removeEventListener('click', onClick))
|
|
96
|
+
|
|
97
|
+
if (!motionReduced()) {
|
|
98
|
+
cleanups.push(bindPointerFx(root))
|
|
99
|
+
const loop = (): void => {
|
|
100
|
+
launchSanta(Math.random() < 0.5)
|
|
101
|
+
timers.push(window.setTimeout(loop, 9000 + Math.random() * 9000))
|
|
102
|
+
}
|
|
103
|
+
timers.push(window.setTimeout(loop, 3000))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
onCleanup(() => {
|
|
107
|
+
cleanups.forEach((fn) => fn())
|
|
108
|
+
timers.forEach((t) => clearTimeout(t))
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div id="xmas" ref={root} aria-hidden="true">
|
|
114
|
+
<div class="xmas-sky" />
|
|
115
|
+
<div ref={field} class="absolute inset-0" />
|
|
116
|
+
|
|
117
|
+
{/* Light garland across the top */}
|
|
118
|
+
<div style={{ position: 'absolute', top: '0', left: '0', right: '0', height: '26px' }}>
|
|
119
|
+
<For each={GARLAND}>
|
|
120
|
+
{(i) => {
|
|
121
|
+
const c = LIGHT_COLORS[i % LIGHT_COLORS.length]
|
|
122
|
+
return (
|
|
123
|
+
<span
|
|
124
|
+
class="light"
|
|
125
|
+
style={{
|
|
126
|
+
left: `${((i / (GARLAND.length - 1)) * 100).toFixed(2)}%`,
|
|
127
|
+
background: c,
|
|
128
|
+
color: c,
|
|
129
|
+
'animation-delay': `${(i % 5) * 0.3}s`,
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
)
|
|
133
|
+
}}
|
|
134
|
+
</For>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Pine tree (accent = pine green, so it follows the theme) */}
|
|
138
|
+
<svg
|
|
139
|
+
viewBox="0 0 200 270"
|
|
140
|
+
style={{
|
|
141
|
+
position: 'absolute',
|
|
142
|
+
bottom: '0',
|
|
143
|
+
left: '50%',
|
|
144
|
+
transform: 'translateX(-50%)',
|
|
145
|
+
width: '240px',
|
|
146
|
+
height: 'auto',
|
|
147
|
+
opacity: '0.9',
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<rect x="92" y="228" width="16" height="42" fill="hsl(25 45% 28%)" />
|
|
151
|
+
<polygon points="100,64 60,150 140,150" fill="hsl(var(--accent))" />
|
|
152
|
+
<polygon points="100,110 48,196 152,196" fill="hsl(var(--accent))" />
|
|
153
|
+
<polygon points="100,158 38,240 162,240" fill="hsl(var(--accent))" />
|
|
154
|
+
<polygon points="100,40 106,58 125,58 110,70 116,88 100,77 84,88 90,70 75,58 94,58" fill="#ffd23f" />
|
|
155
|
+
<circle cx="82" cy="180" r="5" fill="#ff5a5a" />
|
|
156
|
+
<circle cx="120" cy="205" r="5" fill="#5aa9ff" />
|
|
157
|
+
<circle cx="100" cy="150" r="5" fill="#ffd23f" />
|
|
158
|
+
<circle cx="72" cy="225" r="5" fill="#ff8ad8" />
|
|
159
|
+
<circle cx="132" cy="228" r="5" fill="#ffd23f" />
|
|
160
|
+
</svg>
|
|
161
|
+
|
|
162
|
+
<div
|
|
163
|
+
id="cursorGlow"
|
|
164
|
+
style={{
|
|
165
|
+
position: 'absolute',
|
|
166
|
+
width: '520px',
|
|
167
|
+
height: '520px',
|
|
168
|
+
'border-radius': '50%',
|
|
169
|
+
background: 'radial-gradient(circle, hsl(var(--primary) / 0.1), transparent 70%)',
|
|
170
|
+
transform: 'translate(-50%,-50%)',
|
|
171
|
+
opacity: '0',
|
|
172
|
+
transition: 'opacity .4s ease',
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Single "visual effects" toggle. Pressed (default) = full space-glass
|
|
2
|
+
// experience: translucent glass, starfield, animations. Un-press it for calm
|
|
3
|
+
// mode (opaque surfaces, no starfield, no motion) — friendlier for contrast,
|
|
4
|
+
// motion sensitivity and low-power devices.
|
|
5
|
+
import { Sparkles } from 'lucide-solid'
|
|
6
|
+
|
|
7
|
+
import { setEffects, useEffects } from '../lib/effects'
|
|
8
|
+
|
|
9
|
+
/** Props for {@link EffectsToggle}. */
|
|
10
|
+
interface EffectsToggleProps {
|
|
11
|
+
/** Accessible label / tooltip text. Defaults to `'Visual effects'`. */
|
|
12
|
+
label?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Icon button that toggles the "visual effects" switch: pressed (default) is
|
|
17
|
+
* the full space-glass experience (translucent glass, starfield, animation);
|
|
18
|
+
* un-pressed is calm mode (opaque surfaces, no starfield, no motion) — useful
|
|
19
|
+
* for contrast, motion sensitivity, and low-power devices. Wraps
|
|
20
|
+
* {@link useEffects} + {@link setEffects}.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <EffectsToggle label="Toggle effects" />
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function EffectsToggle(props: EffectsToggleProps) {
|
|
28
|
+
const on = useEffects()
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
class="grid h-9 w-9 place-items-center rounded-lg text-muted-foreground transition hover:bg-muted hover:text-foreground aria-pressed:text-primary"
|
|
33
|
+
aria-pressed={on()}
|
|
34
|
+
aria-label={props.label ?? 'Visual effects'}
|
|
35
|
+
title={on() ? 'Visual effects on — click for calm mode' : 'Calm mode — click to enable effects'}
|
|
36
|
+
onClick={() => setEffects(!on())}
|
|
37
|
+
>
|
|
38
|
+
<Sparkles class="h-[18px] w-[18px]" />
|
|
39
|
+
</button>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Collapsible sidebar category — native <details>/<summary>, no JS state, chevron
|
|
2
|
+
// rotates via the `group-[[open]]:rotate-90` arbitrary-variant selector. Open by
|
|
3
|
+
// default (every group starts expanded).
|
|
4
|
+
import type { JSX, ParentProps } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
/** Props for {@link NavGroup}. */
|
|
7
|
+
interface NavGroupProps extends ParentProps {
|
|
8
|
+
/** Category heading shown in the summary row (rendered uppercase). */
|
|
9
|
+
title: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Collapsible sidebar nav category. Uses native `<details>`/`<summary>` (no
|
|
14
|
+
* JS state), with a chevron that rotates via a `group-[[open]]` CSS variant.
|
|
15
|
+
* Expanded by default; wrap a list of nav links as `children`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <NavGroup title="Settings">
|
|
20
|
+
* <NavLink href="/settings/profile">Profile</NavLink>
|
|
21
|
+
* <NavLink href="/settings/billing">Billing</NavLink>
|
|
22
|
+
* </NavGroup>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function NavGroup(props: NavGroupProps): JSX.Element {
|
|
26
|
+
return (
|
|
27
|
+
<details open class="group">
|
|
28
|
+
<summary class="mt-3 flex cursor-pointer list-none select-none items-center justify-between rounded-md px-3 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground transition-colors hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden">
|
|
29
|
+
<span>{props.title}</span>
|
|
30
|
+
<svg
|
|
31
|
+
class="h-3.5 w-3.5 shrink-0 transition-transform duration-200 group-[[open]]:rotate-90"
|
|
32
|
+
viewBox="0 0 20 20"
|
|
33
|
+
fill="none"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
stroke-width="2"
|
|
36
|
+
stroke-linecap="round"
|
|
37
|
+
stroke-linejoin="round"
|
|
38
|
+
>
|
|
39
|
+
<path d="M7 5l6 5-6 5" />
|
|
40
|
+
</svg>
|
|
41
|
+
</summary>
|
|
42
|
+
<div class="ml-4 mt-1 space-y-0.5 border-l border-border pl-2">{props.children}</div>
|
|
43
|
+
</details>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Snow scenery for the "snow" theme: snowflakes drifting DOWN with a sideways
|
|
2
|
+
// sway, a snow bank building at the bottom, and a frosted cap on cards (via the
|
|
3
|
+
// `data-scene="snow"` flag on <html>). Shares the cursor glow / magnetic / edge-
|
|
4
|
+
// glow pointer effects. Motion is skipped under motionReduced().
|
|
5
|
+
import { createEffect, onCleanup, onMount, type JSX } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
import { motionReduced } from '../lib/motion'
|
|
8
|
+
import { bindPointerFx } from './sceneEffects'
|
|
9
|
+
|
|
10
|
+
const FLAKE_COUNT = 80
|
|
11
|
+
|
|
12
|
+
function buildFlakes(animate: boolean): string {
|
|
13
|
+
const rnd = Math.random
|
|
14
|
+
let html = ''
|
|
15
|
+
for (let i = 0; i < FLAKE_COUNT; i++) {
|
|
16
|
+
const size = (2 + rnd() * 5).toFixed(1)
|
|
17
|
+
const op = (0.4 + rnd() * 0.6).toFixed(2)
|
|
18
|
+
const left = (rnd() * 100).toFixed(2)
|
|
19
|
+
const dur = (5 + rnd() * 9).toFixed(1)
|
|
20
|
+
const delay = (-rnd() * 14).toFixed(1)
|
|
21
|
+
const sway = (rnd() * 2 - 1) * (8 + rnd() * 14)
|
|
22
|
+
const blur = rnd() < 0.3 ? 'filter:blur(1px);' : ''
|
|
23
|
+
const anim = animate
|
|
24
|
+
? `animation:snowFall ${dur}s linear ${delay}s infinite;`
|
|
25
|
+
: `top:${(rnd() * 100).toFixed(1)}%;`
|
|
26
|
+
html +=
|
|
27
|
+
`<span class="flake" style="left:${left}%;width:${size}px;height:${size}px;opacity:${op};` +
|
|
28
|
+
`--sway:${sway.toFixed(0)}px;${blur}${anim}"></span>`
|
|
29
|
+
}
|
|
30
|
+
return html
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Falling-snow backdrop for the `snow` theme: snowflakes drift down, a snow bank
|
|
35
|
+
* gathers at the bottom, and cards get a frosted cap. Mount as {@link AppShell}'s
|
|
36
|
+
* `background`. Motion is skipped under `motionReduced()`.
|
|
37
|
+
*/
|
|
38
|
+
export function SnowScenery(): JSX.Element {
|
|
39
|
+
let root!: HTMLDivElement
|
|
40
|
+
let field!: HTMLDivElement
|
|
41
|
+
|
|
42
|
+
createEffect(() => {
|
|
43
|
+
field.innerHTML = buildFlakes(!motionReduced())
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
onMount(() => {
|
|
47
|
+
document.documentElement.dataset.scene = 'snow'
|
|
48
|
+
const cleanups: Array<() => void> = []
|
|
49
|
+
if (!motionReduced()) cleanups.push(bindPointerFx(root))
|
|
50
|
+
onCleanup(() => {
|
|
51
|
+
if (document.documentElement.dataset.scene === 'snow') delete document.documentElement.dataset.scene
|
|
52
|
+
cleanups.forEach((fn) => fn())
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div id="snow" ref={root} aria-hidden="true">
|
|
58
|
+
<div class="snow-sky" />
|
|
59
|
+
<div ref={field} class="absolute inset-0" />
|
|
60
|
+
<div class="snow-bank" />
|
|
61
|
+
<div
|
|
62
|
+
id="cursorGlow"
|
|
63
|
+
style={{
|
|
64
|
+
position: 'absolute',
|
|
65
|
+
width: '520px',
|
|
66
|
+
height: '520px',
|
|
67
|
+
'border-radius': '50%',
|
|
68
|
+
background: 'radial-gradient(circle, hsl(var(--primary) / 0.1), transparent 70%)',
|
|
69
|
+
transform: 'translate(-50%,-50%)',
|
|
70
|
+
opacity: '0',
|
|
71
|
+
transition: 'opacity .4s ease',
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|