@passui/react 0.0.0-stage → 0.1.15
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/LICENSE +21 -0
- package/README.md +83 -2
- package/package.json +96 -5
- package/src/appearance/index.ts +2 -0
- package/src/appearance/platform.tsx +123 -0
- package/src/appearance/theme.tsx +535 -0
- package/src/components/ambient/ambient.docs.tsx +68 -0
- package/src/components/ambient/ambient.tsx +43 -0
- package/src/components/ambient/examples/ambient-images.tsx +30 -0
- package/src/components/ambient/examples/ambient-now-playing.tsx +55 -0
- package/src/components/ambient/examples/ambient-showcase.tsx +33 -0
- package/src/components/ambient/examples/ambient-usage.tsx +19 -0
- package/src/components/ambient/index.ts +1 -0
- package/src/components/button/button-styles.ts +60 -0
- package/src/components/button/button.docs.tsx +116 -0
- package/src/components/button/button.tsx +91 -0
- package/src/components/button/examples/button-disabled.tsx +19 -0
- package/src/components/button/examples/button-icon.tsx +39 -0
- package/src/components/button/examples/button-link.tsx +12 -0
- package/src/components/button/examples/button-pending.tsx +27 -0
- package/src/components/button/examples/button-showcase.tsx +115 -0
- package/src/components/button/examples/button-sizes.tsx +12 -0
- package/src/components/button/examples/button-usage.tsx +12 -0
- package/src/components/button/examples/button-variants.tsx +15 -0
- package/src/components/button/examples/button-with-icon.tsx +32 -0
- package/src/components/button/examples/icon-button-player.tsx +38 -0
- package/src/components/button/examples/icon-button-showcase.tsx +83 -0
- package/src/components/button/examples/icon-button-usage.tsx +11 -0
- package/src/components/button/examples/icon-button-variants.tsx +39 -0
- package/src/components/button/icon-button.docs.tsx +68 -0
- package/src/components/button/icon-button.tsx +73 -0
- package/src/components/button/pending-spinner.tsx +14 -0
- package/src/components/button/use-pending-indicator.ts +27 -0
- package/src/components/chip/chip.docs.tsx +37 -0
- package/src/components/chip/chip.tsx +35 -0
- package/src/components/chip/examples/chip-in-rows.tsx +19 -0
- package/src/components/chip/examples/chip-showcase.tsx +51 -0
- package/src/components/chip/examples/chip-tones.tsx +13 -0
- package/src/components/chip/examples/chip-usage.tsx +5 -0
- package/src/components/compact-date-picker/calendar-month.ts +131 -0
- package/src/components/compact-date-picker/compact-date-picker.docs.tsx +81 -0
- package/src/components/compact-date-picker/compact-date-picker.tsx +438 -0
- package/src/components/compact-date-picker/examples/compact-date-picker-showcase.tsx +35 -0
- package/src/components/compact-date-picker/examples/compact-date-picker-usage.tsx +22 -0
- package/src/components/compact-time-picker/compact-time-picker.docs.tsx +80 -0
- package/src/components/compact-time-picker/compact-time-picker.tsx +432 -0
- package/src/components/compact-time-picker/examples/compact-time-picker-showcase.tsx +31 -0
- package/src/components/compact-time-picker/examples/compact-time-picker-usage.tsx +21 -0
- package/src/components/compact-time-picker/time-value.ts +67 -0
- package/src/components/content-view/content-view.docs.tsx +61 -0
- package/src/components/content-view/content-view.tsx +197 -0
- package/src/components/content-view/examples/content-view-header-action.tsx +33 -0
- package/src/components/content-view/examples/content-view-showcase.tsx +65 -0
- package/src/components/content-view/examples/content-view-stats.tsx +35 -0
- package/src/components/content-view/examples/content-view-usage.tsx +14 -0
- package/src/components/content-view/index.ts +1 -0
- package/src/components/context-menu/conceal-menu-source.ts +33 -0
- package/src/components/context-menu/context-menu-host.tsx +445 -0
- package/src/components/context-menu/context-menu-store.ts +165 -0
- package/src/components/context-menu/context-menu.docs.tsx +92 -0
- package/src/components/context-menu/examples/context-menu-checked.tsx +37 -0
- package/src/components/context-menu/examples/context-menu-list.tsx +85 -0
- package/src/components/context-menu/examples/context-menu-showcase.tsx +56 -0
- package/src/components/context-menu/examples/context-menu-toolbar.tsx +63 -0
- package/src/components/context-menu/examples/context-menu-usage.tsx +24 -0
- package/src/components/context-menu/menu-position.ts +55 -0
- package/src/components/context-menu/menu-reveal.ts +75 -0
- package/src/components/context-menu/use-context-menu.ts +90 -0
- package/src/components/conversation/conversation.docs.tsx +88 -0
- package/src/components/conversation/conversation.tsx +279 -0
- package/src/components/conversation/examples/conversation-composer.tsx +36 -0
- package/src/components/conversation/examples/conversation-days.tsx +48 -0
- package/src/components/conversation/examples/conversation-showcase.tsx +71 -0
- package/src/components/conversation/examples/conversation-usage.tsx +28 -0
- package/src/components/conversation/index.ts +1 -0
- package/src/components/date-picker/date-picker.docs.tsx +88 -0
- package/src/components/date-picker/date-picker.tsx +103 -0
- package/src/components/date-picker/date-value.ts +37 -0
- package/src/components/date-picker/examples/date-picker-locales.tsx +41 -0
- package/src/components/date-picker/examples/date-picker-past-only.tsx +29 -0
- package/src/components/date-picker/examples/date-picker-showcase.tsx +49 -0
- package/src/components/date-picker/examples/date-picker-usage.tsx +22 -0
- package/src/components/dialog/dialog-host.tsx +156 -0
- package/src/components/dialog/dialog-store.ts +56 -0
- package/src/components/dialog/dialog.docs.tsx +114 -0
- package/src/components/dialog/dialog.tsx +200 -0
- package/src/components/dialog/examples/dialog-confirm.tsx +24 -0
- package/src/components/dialog/examples/dialog-form.tsx +46 -0
- package/src/components/dialog/examples/dialog-inline.tsx +48 -0
- package/src/components/dialog/examples/dialog-showcase.tsx +53 -0
- package/src/components/dialog/examples/dialog-usage.tsx +31 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/labeled-content/examples/labeled-content-descriptions.tsx +37 -0
- package/src/components/labeled-content/examples/labeled-content-showcase.tsx +54 -0
- package/src/components/labeled-content/examples/labeled-content-usage.tsx +22 -0
- package/src/components/labeled-content/examples/labeled-content-values.tsx +26 -0
- package/src/components/labeled-content/examples/labeled-content-wide.tsx +46 -0
- package/src/components/labeled-content/labeled-content.docs.tsx +69 -0
- package/src/components/labeled-content/labeled-content.tsx +66 -0
- package/src/components/locale-switcher/examples/locale-switcher-native-names.tsx +30 -0
- package/src/components/locale-switcher/examples/locale-switcher-showcase.tsx +46 -0
- package/src/components/locale-switcher/examples/locale-switcher-translated-page.tsx +35 -0
- package/src/components/locale-switcher/examples/locale-switcher-usage.tsx +20 -0
- package/src/components/locale-switcher/index.ts +1 -0
- package/src/components/locale-switcher/locale-switcher.docs.tsx +91 -0
- package/src/components/locale-switcher/locale-switcher.tsx +252 -0
- package/src/components/picker/examples/picker-details.tsx +44 -0
- package/src/components/picker/examples/picker-searchable.tsx +48 -0
- package/src/components/picker/examples/picker-showcase.tsx +66 -0
- package/src/components/picker/examples/picker-usage.tsx +20 -0
- package/src/components/picker/picker.docs.tsx +68 -0
- package/src/components/picker/picker.tsx +493 -0
- package/src/components/popover/examples/popover-help.tsx +57 -0
- package/src/components/popover/examples/popover-menu.tsx +51 -0
- package/src/components/popover/examples/popover-showcase.tsx +61 -0
- package/src/components/popover/examples/popover-usage.tsx +31 -0
- package/src/components/popover/popover.docs.tsx +84 -0
- package/src/components/popover/popover.tsx +138 -0
- package/src/components/profile-image/examples/profile-image-fallbacks.tsx +29 -0
- package/src/components/profile-image/examples/profile-image-group.tsx +34 -0
- package/src/components/profile-image/examples/profile-image-showcase.tsx +30 -0
- package/src/components/profile-image/examples/profile-image-usage.tsx +14 -0
- package/src/components/profile-image/profile-image.docs.tsx +81 -0
- package/src/components/profile-image/profile-image.tsx +80 -0
- package/src/components/search-field/examples/search-field-filter.tsx +53 -0
- package/src/components/search-field/examples/search-field-showcase.tsx +56 -0
- package/src/components/search-field/examples/search-field-toolbar.tsx +27 -0
- package/src/components/search-field/examples/search-field-usage.tsx +18 -0
- package/src/components/search-field/search-field.docs.tsx +58 -0
- package/src/components/search-field/search-field.tsx +29 -0
- package/src/components/search-list/examples/search-list-dropdown.tsx +53 -0
- package/src/components/search-list/examples/search-list-groups.tsx +64 -0
- package/src/components/search-list/examples/search-list-showcase.tsx +67 -0
- package/src/components/search-list/examples/search-list-usage.tsx +27 -0
- package/src/components/search-list/search-list.docs.tsx +87 -0
- package/src/components/search-list/search-list.tsx +265 -0
- package/src/components/section/examples/section-actions.tsx +35 -0
- package/src/components/section/examples/section-groups.tsx +41 -0
- package/src/components/section/examples/section-showcase.tsx +63 -0
- package/src/components/section/examples/section-usage.tsx +19 -0
- package/src/components/section/section.docs.tsx +51 -0
- package/src/components/section/section.tsx +41 -0
- package/src/components/secure-field/examples/secure-field-new-password.tsx +56 -0
- package/src/components/secure-field/examples/secure-field-reveal.tsx +20 -0
- package/src/components/secure-field/examples/secure-field-showcase.tsx +62 -0
- package/src/components/secure-field/examples/secure-field-usage.tsx +19 -0
- package/src/components/secure-field/secure-field.docs.tsx +72 -0
- package/src/components/secure-field/secure-field.tsx +10 -0
- package/src/components/segmented-control/examples/segmented-control-overflow.tsx +28 -0
- package/src/components/segmented-control/examples/segmented-control-showcase.tsx +59 -0
- package/src/components/segmented-control/examples/segmented-control-usage.tsx +20 -0
- package/src/components/segmented-control/examples/segmented-control-views.tsx +32 -0
- package/src/components/segmented-control/segmented-control.docs.tsx +61 -0
- package/src/components/segmented-control/segmented-control.tsx +122 -0
- package/src/components/sheet/examples/sheet-choices.tsx +58 -0
- package/src/components/sheet/examples/sheet-share.tsx +51 -0
- package/src/components/sheet/examples/sheet-showcase.tsx +74 -0
- package/src/components/sheet/examples/sheet-usage.tsx +32 -0
- package/src/components/sheet/modal-environment.ts +113 -0
- package/src/components/sheet/sheet-portal.tsx +89 -0
- package/src/components/sheet/sheet.docs.tsx +75 -0
- package/src/components/sheet/sheet.tsx +112 -0
- package/src/components/sheet/use-sheet-focus.ts +130 -0
- package/src/components/sidebar/examples/sidebar-groups.tsx +133 -0
- package/src/components/sidebar/examples/sidebar-showcase.tsx +123 -0
- package/src/components/sidebar/examples/sidebar-unread.tsx +66 -0
- package/src/components/sidebar/examples/sidebar-usage.tsx +52 -0
- package/src/components/sidebar/sidebar.docs.tsx +135 -0
- package/src/components/sidebar/sidebar.tsx +69 -0
- package/src/components/stat-tile/examples/stat-tile-filters.tsx +32 -0
- package/src/components/stat-tile/examples/stat-tile-grid.tsx +21 -0
- package/src/components/stat-tile/examples/stat-tile-showcase.tsx +52 -0
- package/src/components/stat-tile/examples/stat-tile-usage.tsx +13 -0
- package/src/components/stat-tile/index.ts +1 -0
- package/src/components/stat-tile/stat-tile.docs.tsx +60 -0
- package/src/components/stat-tile/stat-tile.tsx +68 -0
- package/src/components/status-indicator/examples/status-indicator-live.tsx +34 -0
- package/src/components/status-indicator/examples/status-indicator-showcase.tsx +39 -0
- package/src/components/status-indicator/examples/status-indicator-states.tsx +17 -0
- package/src/components/status-indicator/examples/status-indicator-usage.tsx +9 -0
- package/src/components/status-indicator/status-indicator.docs.tsx +43 -0
- package/src/components/status-indicator/status-indicator.tsx +35 -0
- package/src/components/swatch-picker/examples/swatch-picker-accents.tsx +42 -0
- package/src/components/swatch-picker/examples/swatch-picker-labels.tsx +45 -0
- package/src/components/swatch-picker/examples/swatch-picker-showcase.tsx +70 -0
- package/src/components/swatch-picker/examples/swatch-picker-usage.tsx +39 -0
- package/src/components/swatch-picker/swatch-picker.docs.tsx +60 -0
- package/src/components/swatch-picker/swatch-picker.tsx +96 -0
- package/src/components/tab-bar/examples/tab-bar-five-tabs.tsx +66 -0
- package/src/components/tab-bar/examples/tab-bar-minimize.tsx +71 -0
- package/src/components/tab-bar/examples/tab-bar-showcase.tsx +82 -0
- package/src/components/tab-bar/examples/tab-bar-usage.tsx +54 -0
- package/src/components/tab-bar/tab-bar.docs.tsx +136 -0
- package/src/components/tab-bar/tab-bar.tsx +148 -0
- package/src/components/tab-view/examples/tab-view-keyboard.tsx +39 -0
- package/src/components/tab-view/examples/tab-view-settings.tsx +56 -0
- package/src/components/tab-view/examples/tab-view-showcase.tsx +53 -0
- package/src/components/tab-view/examples/tab-view-usage.tsx +23 -0
- package/src/components/tab-view/tab-view.docs.tsx +77 -0
- package/src/components/tab-view/tab-view.tsx +130 -0
- package/src/components/table/examples/table-empty.tsx +53 -0
- package/src/components/table/examples/table-invoices.tsx +102 -0
- package/src/components/table/examples/table-selection.tsx +150 -0
- package/src/components/table/examples/table-showcase.tsx +115 -0
- package/src/components/table/examples/table-usage.tsx +67 -0
- package/src/components/table/examples/table-wide.tsx +75 -0
- package/src/components/table/table.docs.tsx +131 -0
- package/src/components/table/table.tsx +149 -0
- package/src/components/text-editor/examples/text-editor-comment.tsx +48 -0
- package/src/components/text-editor/examples/text-editor-count.tsx +38 -0
- package/src/components/text-editor/examples/text-editor-showcase.tsx +58 -0
- package/src/components/text-editor/examples/text-editor-usage.tsx +25 -0
- package/src/components/text-editor/text-editor.docs.tsx +61 -0
- package/src/components/text-editor/text-editor.tsx +19 -0
- package/src/components/text-field/examples/text-field-icons.tsx +72 -0
- package/src/components/text-field/examples/text-field-showcase.tsx +50 -0
- package/src/components/text-field/examples/text-field-usage.tsx +18 -0
- package/src/components/text-field/examples/text-field-validation.tsx +27 -0
- package/src/components/text-field/text-field.docs.tsx +118 -0
- package/src/components/text-field/text-field.tsx +135 -0
- package/src/components/toast/examples/toast-action.tsx +32 -0
- package/src/components/toast/examples/toast-promise.tsx +45 -0
- package/src/components/toast/examples/toast-showcase.tsx +49 -0
- package/src/components/toast/examples/toast-usage.tsx +13 -0
- package/src/components/toast/toast-store.ts +166 -0
- package/src/components/toast/toast.docs.tsx +79 -0
- package/src/components/toast/toaster.tsx +374 -0
- package/src/components/toggle/examples/toggle-checkboxes.tsx +43 -0
- package/src/components/toggle/examples/toggle-settings.tsx +31 -0
- package/src/components/toggle/examples/toggle-showcase.tsx +59 -0
- package/src/components/toggle/examples/toggle-usage.tsx +13 -0
- package/src/components/toggle/toggle.docs.tsx +69 -0
- package/src/components/toggle/toggle.tsx +85 -0
- package/src/components/tooltip/examples/tooltip-icon-buttons.tsx +27 -0
- package/src/components/tooltip/examples/tooltip-shortcuts.tsx +38 -0
- package/src/components/tooltip/examples/tooltip-showcase.tsx +44 -0
- package/src/components/tooltip/examples/tooltip-usage.tsx +13 -0
- package/src/components/tooltip/tooltip-host.tsx +156 -0
- package/src/components/tooltip/tooltip.docs.tsx +56 -0
- package/src/components/view-controller/examples/view-controller-action.tsx +27 -0
- package/src/components/view-controller/examples/view-controller-settings.tsx +31 -0
- package/src/components/view-controller/examples/view-controller-showcase.tsx +45 -0
- package/src/components/view-controller/examples/view-controller-usage.tsx +19 -0
- package/src/components/view-controller/index.ts +1 -0
- package/src/components/view-controller/view-controller.docs.tsx +71 -0
- package/src/components/view-controller/view-controller.tsx +81 -0
- package/src/docs/index.ts +3 -0
- package/src/docs/registry.ts +113 -0
- package/src/docs/snippet.ts +17 -0
- package/src/docs/types.ts +180 -0
- package/src/hooks/scroll/minimize-decision.ts +13 -0
- package/src/hooks/use-anchored-placement.ts +43 -0
- package/src/hooks/use-dismiss.ts +58 -0
- package/src/hooks/use-media-query.ts +106 -0
- package/src/hooks/use-minimize-on-scroll.ts +92 -0
- package/src/hooks/use-visual-viewport.ts +76 -0
- package/src/hooks/viewport/types.ts +19 -0
- package/src/hooks/viewport/viewport-metrics.ts +25 -0
- package/src/icons/icons.tsx +298 -0
- package/src/index.ts +78 -0
- package/src/lib/anchored-placement.ts +92 -0
- package/src/lib/class-names.ts +21 -0
- package/src/lib/date.ts +96 -0
- package/src/lib/floating-layers.ts +28 -0
- package/src/lib/haptics.ts +25 -0
- package/src/lib/language.ts +21 -0
- package/src/lib/screen-edges.ts +45 -0
- package/src/lib/shapes.ts +70 -0
- package/src/motion/transitions.ts +30 -0
- package/src/styles/base.css +1 -0
- package/src/styles/styles.css +3 -0
- package/src/styles/theme.css +1 -0
- package/src/styles/typeset.css +1 -0
- package/src/styles/utilities.css +1 -0
- package/src/tokens.ts +1 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
memo,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react"
|
|
11
|
+
import type {
|
|
12
|
+
Dispatch,
|
|
13
|
+
PropsWithChildren,
|
|
14
|
+
ReactNode,
|
|
15
|
+
SetStateAction,
|
|
16
|
+
} from "react"
|
|
17
|
+
|
|
18
|
+
type ValueObject = {
|
|
19
|
+
[themeName: string]: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Props for the useTheme hook
|
|
24
|
+
* @author la niina
|
|
25
|
+
*/
|
|
26
|
+
export type UseThemeProps = {
|
|
27
|
+
/** List of all available theme names */
|
|
28
|
+
themes: Array<string>
|
|
29
|
+
/** Forced theme name for the current page */
|
|
30
|
+
forcedTheme?: string | undefined
|
|
31
|
+
/** Update the theme */
|
|
32
|
+
setTheme: Dispatch<SetStateAction<string>>
|
|
33
|
+
/** Active theme name */
|
|
34
|
+
theme?: string | undefined
|
|
35
|
+
/** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
|
|
36
|
+
systemTheme?: SystemTheme | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SystemTheme = "dark" | "light"
|
|
40
|
+
|
|
41
|
+
export type Attribute = `data-${string}` | "class"
|
|
42
|
+
|
|
43
|
+
export interface ThemeProviderProps
|
|
44
|
+
extends PropsWithChildren {
|
|
45
|
+
/** List of all available theme names */
|
|
46
|
+
themes?: Array<string> | undefined
|
|
47
|
+
/** Forced theme name for the current page */
|
|
48
|
+
forcedTheme?: string | undefined
|
|
49
|
+
/** Whether to switch between dark and light themes based on prefers-color-scheme */
|
|
50
|
+
enableSystem?: boolean | undefined
|
|
51
|
+
/** Disable all CSS transitions when switching themes */
|
|
52
|
+
disableTransitionOnChange?: boolean | undefined
|
|
53
|
+
/** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
|
|
54
|
+
enableColorScheme?: boolean | undefined
|
|
55
|
+
/** Key used to store theme setting in localStorage */
|
|
56
|
+
storageKey?: string | undefined
|
|
57
|
+
/** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
|
|
58
|
+
defaultTheme?: string | undefined
|
|
59
|
+
/** HTML attribute modified based on the active theme. Accepts `class`, `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.), or an array which could include both */
|
|
60
|
+
attribute?: Attribute | Array<Attribute> | undefined
|
|
61
|
+
/** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
|
|
62
|
+
value?: ValueObject | undefined
|
|
63
|
+
/** Nonce string to pass to the inline script for CSP headers */
|
|
64
|
+
nonce?: string | undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const colorSchemes = ["light", "dark"]
|
|
68
|
+
const MEDIA = "(prefers-color-scheme: dark)"
|
|
69
|
+
const isServer = typeof window === "undefined"
|
|
70
|
+
const ThemeContext = createContext<UseThemeProps | undefined>(
|
|
71
|
+
undefined
|
|
72
|
+
)
|
|
73
|
+
const defaultContext: UseThemeProps = {
|
|
74
|
+
setTheme: () => {
|
|
75
|
+
/* no-op */
|
|
76
|
+
},
|
|
77
|
+
themes: [],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hook to access the theme context
|
|
82
|
+
* @returns {UseThemeProps} theme context
|
|
83
|
+
* @example
|
|
84
|
+
* const { theme, setTheme } = useTheme()
|
|
85
|
+
*/
|
|
86
|
+
export const useTheme = (): UseThemeProps =>
|
|
87
|
+
useContext(ThemeContext) ?? defaultContext
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Theme provider component is used to wrap your app &
|
|
91
|
+
* provide theme context to all components. It can be used to switch
|
|
92
|
+
* between themes.
|
|
93
|
+
* @author la niina
|
|
94
|
+
* @param {ThemeProviderProps} props
|
|
95
|
+
* @returns {ReactNode} children
|
|
96
|
+
* @example
|
|
97
|
+
* <ThemeProvider attribute="class" disableTransitionOnChange>
|
|
98
|
+
* <App />
|
|
99
|
+
* </ThemeProvider>
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* <ThemeProvider themes={["light", "queer", "dark", "system"]} defaultTheme="light">
|
|
103
|
+
* <App />
|
|
104
|
+
* </ThemeProvider>
|
|
105
|
+
*/
|
|
106
|
+
export const ThemeProvider = (
|
|
107
|
+
props: ThemeProviderProps
|
|
108
|
+
): ReactNode => {
|
|
109
|
+
const context = useContext(ThemeContext)
|
|
110
|
+
// Ignore nested context providers, just passthrough children
|
|
111
|
+
if (context) {
|
|
112
|
+
return props.children
|
|
113
|
+
}
|
|
114
|
+
return <Theme {...props} />
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const defaultThemes = ["light", "dark"]
|
|
118
|
+
const Theme = ({
|
|
119
|
+
forcedTheme,
|
|
120
|
+
disableTransitionOnChange = false,
|
|
121
|
+
enableSystem = true,
|
|
122
|
+
enableColorScheme = true,
|
|
123
|
+
storageKey = "theme",
|
|
124
|
+
themes = defaultThemes,
|
|
125
|
+
defaultTheme = enableSystem ? "system" : "light",
|
|
126
|
+
attribute = "data-theme",
|
|
127
|
+
value,
|
|
128
|
+
children,
|
|
129
|
+
nonce,
|
|
130
|
+
}: ThemeProviderProps) => {
|
|
131
|
+
const [theme, setThemeState] = useState(() =>
|
|
132
|
+
getTheme(storageKey, defaultTheme)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const applyClassAttribute = useCallback(
|
|
136
|
+
(
|
|
137
|
+
name: string | undefined,
|
|
138
|
+
attrValues: Array<string>
|
|
139
|
+
) => {
|
|
140
|
+
const d = document.documentElement
|
|
141
|
+
d.classList.remove(...attrValues)
|
|
142
|
+
if (name) {
|
|
143
|
+
d.classList.add(name)
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
[]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
const applyDataAttribute = useCallback(
|
|
150
|
+
(attr: string, name: string | undefined) => {
|
|
151
|
+
const d = document.documentElement
|
|
152
|
+
if (name) {
|
|
153
|
+
d.setAttribute(attr, name)
|
|
154
|
+
} else {
|
|
155
|
+
d.removeAttribute(attr)
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
[]
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const applyAttributesToDOM = useCallback(
|
|
162
|
+
(resolved: string) => {
|
|
163
|
+
const attributeList = Array.isArray(attribute)
|
|
164
|
+
? attribute
|
|
165
|
+
: [attribute]
|
|
166
|
+
const attrValues = value
|
|
167
|
+
? Object.values(value)
|
|
168
|
+
: themes
|
|
169
|
+
const name = value ? value[resolved] : resolved
|
|
170
|
+
|
|
171
|
+
for (const attr of attributeList) {
|
|
172
|
+
if (attr === "class") {
|
|
173
|
+
applyClassAttribute(name, attrValues)
|
|
174
|
+
} else if (attr.startsWith("data-")) {
|
|
175
|
+
applyDataAttribute(attr, name)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
[
|
|
180
|
+
attribute,
|
|
181
|
+
themes,
|
|
182
|
+
value,
|
|
183
|
+
applyClassAttribute,
|
|
184
|
+
applyDataAttribute,
|
|
185
|
+
]
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
const applyColorScheme = useCallback(
|
|
189
|
+
(resolved: string) => {
|
|
190
|
+
if (!enableColorScheme) {
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
const fallback = colorSchemes.includes(
|
|
194
|
+
defaultTheme
|
|
195
|
+
)
|
|
196
|
+
? defaultTheme
|
|
197
|
+
: null
|
|
198
|
+
const colorScheme = colorSchemes.includes(
|
|
199
|
+
resolved
|
|
200
|
+
)
|
|
201
|
+
? resolved
|
|
202
|
+
: fallback
|
|
203
|
+
document.documentElement.style.colorScheme =
|
|
204
|
+
colorScheme || ""
|
|
205
|
+
},
|
|
206
|
+
[enableColorScheme, defaultTheme]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
// apply selected theme function (light, dark, system)
|
|
210
|
+
const applyTheme = useCallback(
|
|
211
|
+
(nextTheme: string | undefined) => {
|
|
212
|
+
if (!nextTheme) {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
const resolved =
|
|
216
|
+
nextTheme === "system" && enableSystem
|
|
217
|
+
? getSystemTheme()
|
|
218
|
+
: nextTheme
|
|
219
|
+
const enable = disableTransitionOnChange
|
|
220
|
+
? disableAnimation()
|
|
221
|
+
: null
|
|
222
|
+
applyAttributesToDOM(resolved)
|
|
223
|
+
applyColorScheme(resolved)
|
|
224
|
+
enable?.()
|
|
225
|
+
},
|
|
226
|
+
[
|
|
227
|
+
enableSystem,
|
|
228
|
+
disableTransitionOnChange,
|
|
229
|
+
applyAttributesToDOM,
|
|
230
|
+
applyColorScheme,
|
|
231
|
+
]
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// Set theme state and save to local storage
|
|
235
|
+
const setTheme = useCallback(
|
|
236
|
+
(newValue: SetStateAction<string>) => {
|
|
237
|
+
const newTheme =
|
|
238
|
+
typeof newValue === "function"
|
|
239
|
+
? newValue(theme ?? "")
|
|
240
|
+
: newValue
|
|
241
|
+
setThemeState(newTheme)
|
|
242
|
+
|
|
243
|
+
// Save to storage
|
|
244
|
+
try {
|
|
245
|
+
localStorage.setItem(storageKey, newTheme)
|
|
246
|
+
} catch {
|
|
247
|
+
// localStorage might not be available
|
|
248
|
+
console.error(
|
|
249
|
+
"Failed to save theme to localStorage"
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
[theme, storageKey]
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const handleMediaQuery = useCallback(
|
|
257
|
+
(_event: MediaQueryListEvent | MediaQueryList) => {
|
|
258
|
+
if (
|
|
259
|
+
theme === "system" &&
|
|
260
|
+
enableSystem &&
|
|
261
|
+
!forcedTheme
|
|
262
|
+
) {
|
|
263
|
+
applyTheme("system")
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
[applyTheme, enableSystem, forcedTheme, theme]
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
// Always listen to System preference
|
|
270
|
+
React.useEffect(() => {
|
|
271
|
+
if (isServer) {
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const media = window.matchMedia(MEDIA)
|
|
276
|
+
// Intentionally use deprecated listener methods to support iOS 13 and older browsers
|
|
277
|
+
media.addEventListener("change", handleMediaQuery)
|
|
278
|
+
handleMediaQuery(media)
|
|
279
|
+
return () =>
|
|
280
|
+
media.removeEventListener(
|
|
281
|
+
"change",
|
|
282
|
+
handleMediaQuery
|
|
283
|
+
)
|
|
284
|
+
}, [handleMediaQuery])
|
|
285
|
+
|
|
286
|
+
// localStorage event handling, allow to sync theme changes between tabs
|
|
287
|
+
React.useEffect(() => {
|
|
288
|
+
if (isServer) {
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const handleStorage = (e: StorageEvent) => {
|
|
293
|
+
if (e.key !== storageKey) {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
// If default theme set, use it if localstorage === null (happens on local storage manual deletion)
|
|
297
|
+
const newTheme = e.newValue || defaultTheme
|
|
298
|
+
setTheme(newTheme)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
window.addEventListener("storage", handleStorage)
|
|
302
|
+
return () =>
|
|
303
|
+
window.removeEventListener(
|
|
304
|
+
"storage",
|
|
305
|
+
handleStorage
|
|
306
|
+
)
|
|
307
|
+
}, [defaultTheme, setTheme, storageKey])
|
|
308
|
+
|
|
309
|
+
// Whenever theme or forcedTheme changes, apply it
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
applyTheme(forcedTheme ?? theme)
|
|
312
|
+
}, [applyTheme, forcedTheme, theme])
|
|
313
|
+
|
|
314
|
+
const providerValue = useMemo(
|
|
315
|
+
() => ({
|
|
316
|
+
theme,
|
|
317
|
+
setTheme,
|
|
318
|
+
forcedTheme,
|
|
319
|
+
themes: enableSystem
|
|
320
|
+
? [...themes, "system"]
|
|
321
|
+
: themes,
|
|
322
|
+
systemTheme: enableSystem
|
|
323
|
+
? (getSystemTheme() as
|
|
324
|
+
| "dark"
|
|
325
|
+
| "light"
|
|
326
|
+
| undefined)
|
|
327
|
+
: undefined,
|
|
328
|
+
}),
|
|
329
|
+
[theme, forcedTheme, enableSystem, themes, setTheme]
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<ThemeContext.Provider value={providerValue}>
|
|
334
|
+
<ThemeScript
|
|
335
|
+
{...{
|
|
336
|
+
forcedTheme,
|
|
337
|
+
storageKey,
|
|
338
|
+
attribute,
|
|
339
|
+
enableSystem,
|
|
340
|
+
enableColorScheme,
|
|
341
|
+
defaultTheme,
|
|
342
|
+
value,
|
|
343
|
+
themes,
|
|
344
|
+
nonce,
|
|
345
|
+
}}
|
|
346
|
+
/>
|
|
347
|
+
{children}
|
|
348
|
+
</ThemeContext.Provider>
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const ThemeScript = memo(
|
|
353
|
+
({
|
|
354
|
+
forcedTheme,
|
|
355
|
+
storageKey,
|
|
356
|
+
attribute,
|
|
357
|
+
enableSystem,
|
|
358
|
+
enableColorScheme,
|
|
359
|
+
defaultTheme,
|
|
360
|
+
value,
|
|
361
|
+
themes,
|
|
362
|
+
nonce,
|
|
363
|
+
}: Omit<ThemeProviderProps, "children"> & {
|
|
364
|
+
defaultTheme: string
|
|
365
|
+
}) => {
|
|
366
|
+
const scriptArgs = JSON.stringify([
|
|
367
|
+
attribute,
|
|
368
|
+
storageKey,
|
|
369
|
+
defaultTheme,
|
|
370
|
+
forcedTheme,
|
|
371
|
+
themes,
|
|
372
|
+
value,
|
|
373
|
+
enableSystem,
|
|
374
|
+
enableColorScheme,
|
|
375
|
+
]).slice(1, -1)
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<script
|
|
379
|
+
// Run the theme bootstrap with serialized provider configuration before first paint.
|
|
380
|
+
dangerouslySetInnerHTML={{
|
|
381
|
+
__html: `(${script.toString()})(${scriptArgs})`,
|
|
382
|
+
}}
|
|
383
|
+
nonce={nonce}
|
|
384
|
+
suppressHydrationWarning
|
|
385
|
+
/>
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
ThemeScript.displayName = "ThemeScript"
|
|
391
|
+
|
|
392
|
+
// Helpers
|
|
393
|
+
const getTheme = (key: string, fallback?: string) => {
|
|
394
|
+
if (isServer) {
|
|
395
|
+
return fallback
|
|
396
|
+
}
|
|
397
|
+
let theme: string | undefined
|
|
398
|
+
try {
|
|
399
|
+
theme = localStorage.getItem(key) || undefined
|
|
400
|
+
} catch {
|
|
401
|
+
// localStorage might not be available
|
|
402
|
+
}
|
|
403
|
+
return theme || fallback
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const disableAnimation = () => {
|
|
407
|
+
const css = document.createElement("style")
|
|
408
|
+
css.appendChild(
|
|
409
|
+
document.createTextNode(
|
|
410
|
+
"*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}"
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
document.head.appendChild(css)
|
|
414
|
+
|
|
415
|
+
return () => {
|
|
416
|
+
// Force restyle
|
|
417
|
+
window.getComputedStyle(document.body)
|
|
418
|
+
// Wait for next tick before removing
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
document.head.removeChild(css)
|
|
421
|
+
}, 1)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const getSystemTheme = (
|
|
426
|
+
e?: MediaQueryList | MediaQueryListEvent
|
|
427
|
+
) => {
|
|
428
|
+
if (isServer) {
|
|
429
|
+
return "light"
|
|
430
|
+
}
|
|
431
|
+
const event = e ?? window.matchMedia(MEDIA)
|
|
432
|
+
const isDark = event.matches
|
|
433
|
+
const systemTheme = isDark ? "dark" : "light"
|
|
434
|
+
return systemTheme
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/*
|
|
438
|
+
This file is adapted from next-themes to work with tanstack start.
|
|
439
|
+
next-themes can be found at https://github.com/pacocoursey/next-themes under the MIT license.
|
|
440
|
+
*/
|
|
441
|
+
|
|
442
|
+
export const script = (
|
|
443
|
+
attribute: Attribute | Array<Attribute>,
|
|
444
|
+
storageKey: string,
|
|
445
|
+
defaultTheme: string,
|
|
446
|
+
forcedTheme: string | undefined,
|
|
447
|
+
themes: Array<string>,
|
|
448
|
+
value: ValueObject | undefined,
|
|
449
|
+
enableSystem: boolean,
|
|
450
|
+
enableColorScheme: boolean
|
|
451
|
+
) => {
|
|
452
|
+
const el = document.documentElement
|
|
453
|
+
const systemThemes = ["light", "dark"]
|
|
454
|
+
const attributes = Array.isArray(attribute)
|
|
455
|
+
? attribute
|
|
456
|
+
: [attribute]
|
|
457
|
+
const attrValues = value ? Object.values(value) : themes
|
|
458
|
+
|
|
459
|
+
function applyClassAttr(name: string | undefined) {
|
|
460
|
+
el.classList.remove(...attrValues)
|
|
461
|
+
if (name) {
|
|
462
|
+
el.classList.add(name)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function applyDataAttr(
|
|
467
|
+
attr: string,
|
|
468
|
+
name: string | undefined
|
|
469
|
+
) {
|
|
470
|
+
if (name) {
|
|
471
|
+
el.setAttribute(attr, name)
|
|
472
|
+
} else {
|
|
473
|
+
el.removeAttribute(attr)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function updateDOM(theme: string) {
|
|
478
|
+
const name = value ? value[theme] : theme
|
|
479
|
+
|
|
480
|
+
for (const attr of attributes) {
|
|
481
|
+
if (attr === "class") {
|
|
482
|
+
applyClassAttr(name)
|
|
483
|
+
} else if (attr.startsWith("data-")) {
|
|
484
|
+
applyDataAttr(attr, name)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
setColorScheme(theme)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function setColorScheme(theme: string) {
|
|
492
|
+
if (!enableColorScheme) {
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const fallback = systemThemes.includes(defaultTheme)
|
|
497
|
+
? defaultTheme
|
|
498
|
+
: null
|
|
499
|
+
const colorScheme = systemThemes.includes(theme)
|
|
500
|
+
? theme
|
|
501
|
+
: fallback
|
|
502
|
+
el.style.colorScheme = colorScheme || ""
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function resolveSystemTheme() {
|
|
506
|
+
return window.matchMedia(
|
|
507
|
+
"(prefers-color-scheme: dark)"
|
|
508
|
+
).matches
|
|
509
|
+
? "dark"
|
|
510
|
+
: "light"
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (forcedTheme) {
|
|
514
|
+
const resolvedForcedTheme =
|
|
515
|
+
forcedTheme === "system" && enableSystem
|
|
516
|
+
? resolveSystemTheme()
|
|
517
|
+
: forcedTheme
|
|
518
|
+
updateDOM(resolvedForcedTheme)
|
|
519
|
+
} else {
|
|
520
|
+
try {
|
|
521
|
+
const themeName =
|
|
522
|
+
localStorage.getItem(storageKey) ||
|
|
523
|
+
defaultTheme
|
|
524
|
+
const isSystem =
|
|
525
|
+
enableSystem && themeName === "system"
|
|
526
|
+
const theme = isSystem
|
|
527
|
+
? resolveSystemTheme()
|
|
528
|
+
: themeName
|
|
529
|
+
updateDOM(theme)
|
|
530
|
+
} catch {
|
|
531
|
+
// localStorage might not be available
|
|
532
|
+
updateDOM(defaultTheme)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AmbientNowPlaying } from "./examples/ambient-now-playing"
|
|
2
|
+
import { Ambient } from "./ambient"
|
|
3
|
+
import { defineDocs } from "../../docs/types"
|
|
4
|
+
import { AmbientImages } from "./examples/ambient-images"
|
|
5
|
+
import { AmbientShowcase } from "./examples/ambient-showcase"
|
|
6
|
+
import { AmbientUsage } from "./examples/ambient-usage"
|
|
7
|
+
|
|
8
|
+
// src is any image URL; the playground offers the three sample images.
|
|
9
|
+
type DemoProps = {
|
|
10
|
+
image: "aiko" | "kenji" | "mira" | "none"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ambientDocs = defineDocs<DemoProps>()({
|
|
14
|
+
slug: "ambient",
|
|
15
|
+
name: "Ambient",
|
|
16
|
+
importPath: "@passui/react/ambient",
|
|
17
|
+
controls: {
|
|
18
|
+
image: {
|
|
19
|
+
kind: "select",
|
|
20
|
+
options: {
|
|
21
|
+
aiko: true,
|
|
22
|
+
kenji: true,
|
|
23
|
+
mira: true,
|
|
24
|
+
none: true,
|
|
25
|
+
},
|
|
26
|
+
default: "aiko",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
render: ({ image }) => (
|
|
30
|
+
<Ambient
|
|
31
|
+
key={image}
|
|
32
|
+
src={
|
|
33
|
+
image === "none"
|
|
34
|
+
? undefined
|
|
35
|
+
: `/avatars/${image}.svg`
|
|
36
|
+
}
|
|
37
|
+
className="flex w-72 flex-col items-center gap-3 py-12 text-center"
|
|
38
|
+
>
|
|
39
|
+
<img
|
|
40
|
+
src={
|
|
41
|
+
image === "none"
|
|
42
|
+
? "/avatars/aiko.svg"
|
|
43
|
+
: `/avatars/${image}.svg`
|
|
44
|
+
}
|
|
45
|
+
alt=""
|
|
46
|
+
className="size-24 rounded-3xl shadow-sm"
|
|
47
|
+
/>
|
|
48
|
+
<p className="text-headline font-semibold text-label">
|
|
49
|
+
Now playing
|
|
50
|
+
</p>
|
|
51
|
+
</Ambient>
|
|
52
|
+
),
|
|
53
|
+
childrenSnippet: ({ image }) => ({
|
|
54
|
+
imports: [],
|
|
55
|
+
omit: ["image"],
|
|
56
|
+
extraProps:
|
|
57
|
+
image === "none"
|
|
58
|
+
? undefined
|
|
59
|
+
: { src: `/avatars/${image}.svg` },
|
|
60
|
+
children: `\n <img src="/avatars/${image === "none" ? "aiko" : image}.svg" alt="" />\n`,
|
|
61
|
+
}),
|
|
62
|
+
usage: AmbientUsage,
|
|
63
|
+
showcase: AmbientShowcase,
|
|
64
|
+
examples: [
|
|
65
|
+
{ id: "images", component: AmbientImages },
|
|
66
|
+
{ id: "now-playing", component: AmbientNowPlaying },
|
|
67
|
+
],
|
|
68
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef } from "react"
|
|
2
|
+
import { cn } from "../../lib/class-names"
|
|
3
|
+
|
|
4
|
+
type AmbientProps = ComponentPropsWithoutRef<"section"> & {
|
|
5
|
+
/** The image whose colors glow behind the content. */
|
|
6
|
+
src?: string | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Content with its image's colors glowing softly behind it, no card or
|
|
11
|
+
* edge: a blown-up, blurred copy of the image masked into a circle that
|
|
12
|
+
* fades into the page, the way Apple lets a photo light its surroundings.
|
|
13
|
+
*/
|
|
14
|
+
export function Ambient({
|
|
15
|
+
src,
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
...props
|
|
19
|
+
}: AmbientProps) {
|
|
20
|
+
return (
|
|
21
|
+
<section
|
|
22
|
+
className={cn("relative isolate", className)}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
{src && (
|
|
26
|
+
<img
|
|
27
|
+
aria-hidden
|
|
28
|
+
src={src}
|
|
29
|
+
alt=""
|
|
30
|
+
className={cn(
|
|
31
|
+
"pointer-events-none absolute top-[38%] left-1/2 -z-10 size-[min(32rem,100vw)] -translate-x-1/2 -translate-y-1/2",
|
|
32
|
+
"rounded-full object-cover opacity-20 blur-3xl",
|
|
33
|
+
// on a dark page the same light blooms far wider, so it shrinks and dims
|
|
34
|
+
"dark:size-[min(20rem,80vw)] dark:opacity-15 dark:blur-2xl",
|
|
35
|
+
"mask-[radial-gradient(closest-side,black_35%,transparent)]",
|
|
36
|
+
"animate-in fade-in-0 zoom-in-90 duration-1000"
|
|
37
|
+
)}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
{children}
|
|
41
|
+
</section>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Ambient } from "@passui/react/ambient"
|
|
2
|
+
|
|
3
|
+
const people = [
|
|
4
|
+
{ name: "Aiko", src: "/avatars/aiko.svg" },
|
|
5
|
+
{ name: "Kenji", src: "/avatars/kenji.svg" },
|
|
6
|
+
{ name: "Mira", src: "/avatars/mira.svg" },
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
export function AmbientImages() {
|
|
10
|
+
return (
|
|
11
|
+
<div className="grid w-full max-w-xl grid-cols-3 gap-2">
|
|
12
|
+
{people.map((person) => (
|
|
13
|
+
<Ambient
|
|
14
|
+
key={person.name}
|
|
15
|
+
src={person.src}
|
|
16
|
+
className="flex flex-col items-center gap-2 py-8"
|
|
17
|
+
>
|
|
18
|
+
<img
|
|
19
|
+
src={person.src}
|
|
20
|
+
alt=""
|
|
21
|
+
className="size-14 rounded-full"
|
|
22
|
+
/>
|
|
23
|
+
<span className="text-subheadline font-medium text-label">
|
|
24
|
+
{person.name}
|
|
25
|
+
</span>
|
|
26
|
+
</Ambient>
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|