@stevederico/skateboard-ui 4.10.0 → 4.11.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/CHANGELOG.md +15 -0
- package/dist/App.js +1 -3
- package/dist/components/core/Calendar.js +26 -2
- package/dist/components/core/Command.d.ts +8 -16
- package/dist/components/core/Command.js +25 -19
- package/dist/components/core/Context.js +3 -1
- package/dist/components/core/Utilities.js +13 -4
- package/dist/components/layout/TabBar.js +2 -1
- package/dist/components/views/LandingView.js +1 -1
- package/dist/components/views/NotFound.d.ts +3 -4
- package/dist/components/views/SettingsView.js +8 -3
- package/dist/components/views/SignInView.js +7 -1
- package/dist/components/views/SignUpView.js +8 -1
- package/dist/ui/accordion.js +32 -10
- package/dist/ui/alert-dialog.js +7 -4
- package/dist/ui/avatar.js +14 -1
- package/dist/ui/context-menu.js +62 -9
- package/dist/ui/dialog.js +7 -5
- package/dist/ui/drawer.js +7 -4
- package/dist/ui/dropdown-menu.d.ts +2 -2
- package/dist/ui/dropdown-menu.js +42 -3
- package/dist/ui/field.d.ts +11 -10
- package/dist/ui/field.js +2 -2
- package/dist/ui/hover-card.js +1 -1
- package/dist/ui/input-group.js +3 -1
- package/dist/ui/layer-stack.d.ts +2 -0
- package/dist/ui/layer-stack.js +16 -0
- package/dist/ui/menubar.js +51 -8
- package/dist/ui/popover.js +34 -0
- package/dist/ui/radio-group.js +12 -1
- package/dist/ui/select.js +4 -5
- package/dist/ui/sheet.js +7 -4
- package/dist/ui/slider.js +51 -11
- package/dist/ui/slot.js +5 -3
- package/dist/ui/tabs.js +15 -8
- package/dist/ui/use-dismiss.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
4.11.0
|
|
4
|
+
|
|
5
|
+
Fix context-menu and menubar mouse clicks
|
|
6
|
+
Fix popover focus-into-content on open
|
|
7
|
+
Fix calendar single-select first-click crash
|
|
8
|
+
Share dismiss layer across nested overlays
|
|
9
|
+
Slider pointercancel cleanup and controlled drag
|
|
10
|
+
Fix roving tabindex (radio, tabs, calendar)
|
|
11
|
+
Avatar reconciles cached images
|
|
12
|
+
Command palette single-pass filter and a11y name
|
|
13
|
+
Add aria-expanded to sheet/drawer/alert-dialog
|
|
14
|
+
Guard checkout and subscription fields
|
|
15
|
+
TabBar active match by path segment
|
|
16
|
+
Remove banned type assertions and dead code
|
|
17
|
+
|
|
3
18
|
4.10.0
|
|
4
19
|
|
|
5
20
|
Fix dropdown menu item clicks
|
package/dist/App.js
CHANGED
|
@@ -41,9 +41,7 @@ function AuthRedirect() {
|
|
|
41
41
|
}
|
|
42
42
|
function App({ constants, appRoutes, defaultRoute, landingPage, overrides = {} }) {
|
|
43
43
|
const location = useLocation();
|
|
44
|
-
|
|
45
|
-
document.title = constants.appName;
|
|
46
|
-
}, [constants.appName]);
|
|
44
|
+
// document.title is set by useAppSetup (on navigation) — single source of truth.
|
|
47
45
|
useAppSetup(location);
|
|
48
46
|
const LayoutComponent = overrides.layout || Layout;
|
|
49
47
|
const SettingsComponent = overrides.settings || SettingsView;
|
|
@@ -183,10 +183,31 @@ export function DayPicker({ mode = 'single', selected, onSelect, month: controll
|
|
|
183
183
|
const WeekNumber = components.WeekNumber || DefaultWeekNumber;
|
|
184
184
|
const today = useMemo(() => startOfDay(new Date()), []);
|
|
185
185
|
const grid = useMemo(() => buildGrid(displayMonth, weekStartsOn), [displayMonth, weekStartsOn]);
|
|
186
|
+
// Fallback tabbable cell: keep the grid keyboard-reachable when no day is
|
|
187
|
+
// focused and "today" isn't in the displayed month. Prefer the selected day
|
|
188
|
+
// (when it falls in this month), else the first non-disabled day of the month.
|
|
189
|
+
const fallbackTabbable = useMemo(() => {
|
|
190
|
+
if (focusedDay)
|
|
191
|
+
return null;
|
|
192
|
+
const todayInMonth = isSameMonth(today, displayMonth);
|
|
193
|
+
if (todayInMonth)
|
|
194
|
+
return null;
|
|
195
|
+
if (selected instanceof Date && isSameMonth(selected, displayMonth)) {
|
|
196
|
+
return startOfDay(selected);
|
|
197
|
+
}
|
|
198
|
+
for (const week of grid) {
|
|
199
|
+
for (const date of week) {
|
|
200
|
+
if (isSameMonth(date, displayMonth) && !isDisabled(date, disabled))
|
|
201
|
+
return date;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}, [focusedDay, today, displayMonth, selected, grid, disabled]);
|
|
186
206
|
// Selection handler
|
|
187
207
|
const handleSelect = useCallback((date) => {
|
|
188
208
|
if (mode === 'single') {
|
|
189
|
-
|
|
209
|
+
const isSame = selected instanceof Date && sameDay(selected, date);
|
|
210
|
+
onSelect?.(isSame ? undefined : date);
|
|
190
211
|
return;
|
|
191
212
|
}
|
|
192
213
|
if (mode === 'range') {
|
|
@@ -301,6 +322,9 @@ export function DayPicker({ mode = 'single', selected, onSelect, month: controll
|
|
|
301
322
|
focused
|
|
302
323
|
};
|
|
303
324
|
const dayObj = { date };
|
|
304
|
-
|
|
325
|
+
const isTabbable = Boolean(focused ||
|
|
326
|
+
(!focusedDay && isSameMonth(today, displayMonth) && sameDay(date, today)) ||
|
|
327
|
+
(fallbackTabbable && sameDay(date, fallbackTabbable)));
|
|
328
|
+
return (_jsx("td", { role: "gridcell", className: dayClass, "aria-selected": modSel, "data-selected": modSel ? 'true' : undefined, "data-disabled": dis ? 'true' : undefined, "data-outside": outside ? 'true' : undefined, "data-today": tod ? 'true' : undefined, children: _jsx(DayButton, { day: dayObj, modifiers: modifiers, onClick: () => !dis && handleSelect(date), onFocus: () => handleDayFocus(date), tabIndex: isTabbable ? 0 : -1, "aria-pressed": modSel, "aria-disabled": dis, "aria-current": tod ? 'date' : undefined, className: "", children: date.getDate() }) }, date.toISOString()));
|
|
305
329
|
}) }, wi))) })] })] }) }) }));
|
|
306
330
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type * as React from 'react';
|
|
2
|
-
export interface CommandProps {
|
|
2
|
+
export interface CommandProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
3
3
|
className?: string;
|
|
4
4
|
children?: React.ReactNode;
|
|
5
|
-
[key: string]: any;
|
|
6
5
|
}
|
|
7
6
|
export declare function Command({ className, children, ...props }: CommandProps): React.JSX.Element;
|
|
8
7
|
export declare namespace Command {
|
|
@@ -13,49 +12,42 @@ export declare namespace Command {
|
|
|
13
12
|
var Item: typeof CommandItem;
|
|
14
13
|
var Separator: typeof CommandSeparator;
|
|
15
14
|
}
|
|
16
|
-
export interface CommandInputProps {
|
|
15
|
+
export interface CommandInputProps extends Omit<React.ComponentPropsWithoutRef<'input'>, 'value' | 'onChange'> {
|
|
17
16
|
className?: string;
|
|
18
17
|
placeholder?: string;
|
|
19
18
|
value?: string;
|
|
20
19
|
onValueChange?: (value: string) => void;
|
|
21
|
-
[key: string]: any;
|
|
22
20
|
}
|
|
23
21
|
export declare function CommandInput({ className, placeholder, value: controlledValue, onValueChange, ...props }: CommandInputProps): React.JSX.Element;
|
|
24
|
-
export interface CommandListProps {
|
|
22
|
+
export interface CommandListProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
25
23
|
className?: string;
|
|
26
24
|
children?: React.ReactNode;
|
|
27
|
-
[key: string]: any;
|
|
28
25
|
}
|
|
29
26
|
export declare function CommandList({ className, children, ...props }: CommandListProps): React.JSX.Element;
|
|
30
|
-
export interface CommandEmptyProps {
|
|
27
|
+
export interface CommandEmptyProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
31
28
|
className?: string;
|
|
32
29
|
children?: React.ReactNode;
|
|
33
|
-
[key: string]: any;
|
|
34
30
|
}
|
|
35
31
|
export declare function CommandEmpty({ className, children, ...props }: CommandEmptyProps): React.JSX.Element | null;
|
|
36
|
-
export interface CommandGroupProps {
|
|
32
|
+
export interface CommandGroupProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
37
33
|
className?: string;
|
|
38
34
|
heading?: React.ReactNode;
|
|
39
35
|
children?: React.ReactNode;
|
|
40
|
-
[key: string]: any;
|
|
41
36
|
}
|
|
42
37
|
export declare function CommandGroup({ className, heading, children, ...props }: CommandGroupProps): React.JSX.Element;
|
|
43
|
-
export interface CommandItemProps {
|
|
38
|
+
export interface CommandItemProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'onSelect'> {
|
|
44
39
|
className?: string;
|
|
45
40
|
value?: string;
|
|
46
41
|
onSelect?: (value: string) => void;
|
|
47
42
|
disabled?: boolean;
|
|
48
43
|
children?: React.ReactNode;
|
|
49
|
-
[key: string]: any;
|
|
50
44
|
}
|
|
51
45
|
export declare function CommandItem({ className, value, onSelect, disabled, children, ...props }: CommandItemProps): React.JSX.Element | null;
|
|
52
|
-
export interface CommandSeparatorProps {
|
|
46
|
+
export interface CommandSeparatorProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
53
47
|
className?: string;
|
|
54
|
-
[key: string]: any;
|
|
55
48
|
}
|
|
56
49
|
export declare function CommandSeparator({ className, ...props }: CommandSeparatorProps): React.JSX.Element;
|
|
57
|
-
export interface CommandShortcutProps {
|
|
50
|
+
export interface CommandShortcutProps extends React.ComponentPropsWithoutRef<'span'> {
|
|
58
51
|
className?: string;
|
|
59
|
-
[key: string]: any;
|
|
60
52
|
}
|
|
61
53
|
export declare function CommandShortcut({ className, ...props }: CommandShortcutProps): React.JSX.Element;
|
|
@@ -14,14 +14,22 @@ export function Command({ className, children, ...props }) {
|
|
|
14
14
|
// input's aria-controls / aria-activedescendant can reference them.
|
|
15
15
|
const listboxId = useId();
|
|
16
16
|
const itemsRef = useRef(new Map()); // id -> { value, onSelect, disabled }
|
|
17
|
+
// Bumped on every register/unregister so the visible-id memo (which reads the
|
|
18
|
+
// non-reactive itemsRef) recomputes when the registered item set changes.
|
|
19
|
+
const [registrationVersion, setRegistrationVersion] = useState(0);
|
|
17
20
|
const register = useCallback((id, meta) => {
|
|
18
21
|
itemsRef.current.set(id, meta);
|
|
22
|
+
setRegistrationVersion((v) => v + 1);
|
|
19
23
|
}, []);
|
|
20
24
|
const unregister = useCallback((id) => {
|
|
21
25
|
itemsRef.current.delete(id);
|
|
26
|
+
setRegistrationVersion((v) => v + 1);
|
|
22
27
|
setSelectedId((cur) => (cur === id ? null : cur));
|
|
23
28
|
}, []);
|
|
24
|
-
|
|
29
|
+
// Compute the ordered visible ids and the membership Set once per render,
|
|
30
|
+
// memoized on the filter and the registered item set. Items can then check
|
|
31
|
+
// visibility in O(1) instead of rescanning the whole list each render.
|
|
32
|
+
const visibleIds = useMemo(() => {
|
|
25
33
|
const f = filter.trim().toLowerCase();
|
|
26
34
|
const visible = [];
|
|
27
35
|
for (const [id, meta] of itemsRef.current) {
|
|
@@ -31,24 +39,24 @@ export function Command({ className, children, ...props }) {
|
|
|
31
39
|
visible.push(id);
|
|
32
40
|
}
|
|
33
41
|
return visible;
|
|
34
|
-
}, [filter]);
|
|
35
|
-
|
|
42
|
+
}, [filter, registrationVersion]);
|
|
43
|
+
const visibleSet = useMemo(() => new Set(visibleIds), [visibleIds]);
|
|
44
|
+
const visibleCount = visibleSet.size;
|
|
45
|
+
// Keep selection valid when the visible set changes (filter input or
|
|
46
|
+
// registration), driven by the reactive derived ids rather than the ref.
|
|
36
47
|
useEffect(() => {
|
|
37
|
-
|
|
38
|
-
if (visible.length === 0) {
|
|
48
|
+
if (visibleIds.length === 0) {
|
|
39
49
|
setSelectedId(null);
|
|
40
50
|
return;
|
|
41
51
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}, [filter, visibleIds, selectedId]);
|
|
52
|
+
setSelectedId((cur) => (cur && visibleSet.has(cur) ? cur : visibleIds[0]));
|
|
53
|
+
}, [visibleIds, visibleSet]);
|
|
45
54
|
const move = useCallback((dir) => {
|
|
46
|
-
|
|
47
|
-
if (visible.length === 0)
|
|
55
|
+
if (visibleIds.length === 0)
|
|
48
56
|
return;
|
|
49
|
-
const idx = selectedId ?
|
|
50
|
-
const next = (idx + dir +
|
|
51
|
-
setSelectedId(
|
|
57
|
+
const idx = selectedId ? visibleIds.indexOf(selectedId) : -1;
|
|
58
|
+
const next = (idx + dir + visibleIds.length) % visibleIds.length;
|
|
59
|
+
setSelectedId(visibleIds[next]);
|
|
52
60
|
}, [selectedId, visibleIds]);
|
|
53
61
|
const triggerSelected = useCallback(() => {
|
|
54
62
|
if (!selectedId)
|
|
@@ -56,9 +64,7 @@ export function Command({ className, children, ...props }) {
|
|
|
56
64
|
const meta = itemsRef.current.get(selectedId);
|
|
57
65
|
meta?.onSelect?.(meta.value);
|
|
58
66
|
}, [selectedId]);
|
|
59
|
-
const
|
|
60
|
-
const visibleCount = visibleIds().length;
|
|
61
|
-
const value = useMemo(() => ({ filter, setFilter, selectedId, setSelectedId, listboxId, register, unregister, isVisible, visibleCount, move, triggerSelected }), [filter, selectedId, listboxId, register, unregister, isVisible, visibleCount, move, triggerSelected]);
|
|
67
|
+
const value = useMemo(() => ({ filter, setFilter, selectedId, setSelectedId, listboxId, register, unregister, visibleSet, visibleCount, move, triggerSelected }), [filter, selectedId, listboxId, register, unregister, visibleSet, visibleCount, move, triggerSelected]);
|
|
62
68
|
return (_jsx(CommandContext.Provider, { value: value, children: _jsx("div", { className: className, ...props, children: children }) }));
|
|
63
69
|
}
|
|
64
70
|
export function CommandInput({ className, placeholder, value: controlledValue, onValueChange, ...props }) {
|
|
@@ -93,7 +99,7 @@ export function CommandInput({ className, placeholder, value: controlledValue, o
|
|
|
93
99
|
// The input is the combobox. Real DOM focus stays here during arrow
|
|
94
100
|
// navigation; only aria-activedescendant moves so screen readers announce
|
|
95
101
|
// the active option (WAI-ARIA combobox + activedescendant pattern).
|
|
96
|
-
_jsx("input", { role: "combobox", "aria-autocomplete": "list", "aria-expanded": isExpanded, "aria-controls": listboxId, "aria-activedescendant": activeDescendant, autoComplete: "off", autoCorrect: "off", spellCheck: "false", value: value, onChange: handleChange, onKeyDown: handleKeyDown, placeholder: placeholder, className: className, ...props }));
|
|
102
|
+
_jsx("input", { role: "combobox", "aria-autocomplete": "list", "aria-expanded": isExpanded, "aria-controls": listboxId, "aria-activedescendant": activeDescendant, "aria-label": "Search", autoComplete: "off", autoCorrect: "off", spellCheck: "false", value: value, onChange: handleChange, onKeyDown: handleKeyDown, placeholder: placeholder, className: className, ...props }));
|
|
97
103
|
}
|
|
98
104
|
export function CommandList({ className, children, ...props }) {
|
|
99
105
|
const { listboxId } = useCommand();
|
|
@@ -113,7 +119,7 @@ export function CommandItem({ className, value, onSelect, disabled = false, chil
|
|
|
113
119
|
// This id is both the value tracked in selectedId and the option's DOM id,
|
|
114
120
|
// which is what the input's aria-activedescendant references.
|
|
115
121
|
const id = useId();
|
|
116
|
-
const { register, unregister, selectedId, setSelectedId,
|
|
122
|
+
const { register, unregister, selectedId, setSelectedId, visibleSet } = useCommand();
|
|
117
123
|
const optionRef = useRef(null);
|
|
118
124
|
// Resolve a usable value string — fall back to children text if no value prop
|
|
119
125
|
const resolvedValue = value ?? (typeof children === 'string' ? children : id);
|
|
@@ -128,7 +134,7 @@ export function CommandItem({ className, value, onSelect, disabled = false, chil
|
|
|
128
134
|
if (isSelected)
|
|
129
135
|
optionRef.current?.scrollIntoView({ block: 'nearest' });
|
|
130
136
|
}, [isSelected]);
|
|
131
|
-
if (!
|
|
137
|
+
if (!visibleSet.has(id))
|
|
132
138
|
return null;
|
|
133
139
|
const handleClick = () => {
|
|
134
140
|
if (disabled)
|
|
@@ -90,13 +90,15 @@ export function ContextProvider({ children, constants }) {
|
|
|
90
90
|
const appName = constants.appName || 'skateboard';
|
|
91
91
|
return `${appName.toLowerCase().replace(/\s+/g, '-')}_user`;
|
|
92
92
|
};
|
|
93
|
+
const isUser = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
93
94
|
const getInitialUser = () => {
|
|
94
95
|
try {
|
|
95
96
|
const storageKey = getStorageKey();
|
|
96
97
|
const storedUser = safeLSGetItem(storageKey);
|
|
97
98
|
if (!storedUser || storedUser === "undefined")
|
|
98
99
|
return null;
|
|
99
|
-
|
|
100
|
+
const parsed = JSON.parse(storedUser);
|
|
101
|
+
return isUser(parsed) ? parsed : null;
|
|
100
102
|
}
|
|
101
103
|
catch (e) {
|
|
102
104
|
console.error('Error parsing user data:', e instanceof Error ? e.message : String(e));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useInRouterContext, useNavigate } from 'react-router';
|
|
3
3
|
import { getDispatch } from './Context.js';
|
|
4
4
|
// Constants will be initialized by the app shell
|
|
@@ -389,8 +389,13 @@ export async function showManage(stripeID) {
|
|
|
389
389
|
export async function showCheckout(email, productIndex = 0) {
|
|
390
390
|
try {
|
|
391
391
|
const csrfToken = getCSRFToken();
|
|
392
|
+
const product = getConstants().stripeProducts?.[productIndex];
|
|
393
|
+
if (!product) {
|
|
394
|
+
console.error("No stripe product at index", productIndex);
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
392
397
|
const params = {
|
|
393
|
-
lookup_key:
|
|
398
|
+
lookup_key: product.lookup_key,
|
|
394
399
|
email: email
|
|
395
400
|
};
|
|
396
401
|
const uri = `${getBackendURL()}/checkout`;
|
|
@@ -853,11 +858,15 @@ export function useListData(endpoint, sortFn = null) {
|
|
|
853
858
|
const [data, setData] = useState([]);
|
|
854
859
|
const [loading, setLoading] = useState(true);
|
|
855
860
|
const [error, setError] = useState(null);
|
|
861
|
+
// Keep sortFn in a ref so an inline sort function doesn't retrigger the fetch effect every render
|
|
862
|
+
const sortFnRef = useRef(sortFn);
|
|
863
|
+
sortFnRef.current = sortFn;
|
|
856
864
|
const fetchData = async (signal) => {
|
|
857
865
|
setLoading(true);
|
|
858
866
|
try {
|
|
859
867
|
const result = await apiRequest(endpoint, { signal });
|
|
860
|
-
const
|
|
868
|
+
const sort = sortFnRef.current;
|
|
869
|
+
const sorted = sort ? result.sort(sort) : result;
|
|
861
870
|
setData(sorted);
|
|
862
871
|
setError(null);
|
|
863
872
|
}
|
|
@@ -875,7 +884,7 @@ export function useListData(endpoint, sortFn = null) {
|
|
|
875
884
|
const controller = new AbortController();
|
|
876
885
|
fetchData(controller.signal);
|
|
877
886
|
return () => controller.abort();
|
|
878
|
-
}, [endpoint
|
|
887
|
+
}, [endpoint]);
|
|
879
888
|
return { data, loading, error, refetch: () => fetchData() };
|
|
880
889
|
}
|
|
881
890
|
/**
|
|
@@ -8,6 +8,7 @@ export default function TabBar({ className, ...props }) {
|
|
|
8
8
|
const location = useLocation();
|
|
9
9
|
const { state } = getState();
|
|
10
10
|
const constants = state.constants;
|
|
11
|
+
const currentPage = (location.pathname.split("/")[2] || "").toLowerCase();
|
|
11
12
|
const tabs = [
|
|
12
13
|
...(constants?.pages || []).map((item) => ({
|
|
13
14
|
title: item.title,
|
|
@@ -18,7 +19,7 @@ export default function TabBar({ className, ...props }) {
|
|
|
18
19
|
{ title: 'Settings', icon: 'settings', to: '/app/settings', match: 'settings' },
|
|
19
20
|
];
|
|
20
21
|
return (_jsx("nav", { className: cn("fixed md:hidden bottom-0 inset-x-0 bg-background border-t border-border z-50", className), role: "navigation", "aria-label": "Main navigation", ...props, children: _jsx("div", { className: "flex justify-around items-center pt-2 pb-4", children: tabs.map((tab) => {
|
|
21
|
-
const isActive =
|
|
22
|
+
const isActive = currentPage === tab.match;
|
|
22
23
|
return (_jsxs(Link, { to: tab.to, className: cn("flex flex-col items-center gap-0.5 px-3 transition-colors", isActive ? "text-foreground" : "text-muted-foreground"), "aria-label": tab.title, "aria-current": isActive ? 'page' : undefined, children: [_jsx(DynamicIcon, { name: tab.icon, size: 24, strokeWidth: isActive ? 2 : 1.5 }), _jsx("span", { className: "text-xs", children: tab.title })] }, tab.title));
|
|
23
24
|
}) }) }));
|
|
24
25
|
}
|
|
@@ -51,7 +51,7 @@ export default function LandingView() {
|
|
|
51
51
|
const goApp = () => navigate('/app');
|
|
52
52
|
const navLinks = constants.navLinks || [
|
|
53
53
|
{ label: 'Features', href: '#features' },
|
|
54
|
-
...(constants.stripeProducts?.length > 0 ? [{ label: 'Pricing', href: '#pricing' }] : []),
|
|
54
|
+
...((constants.stripeProducts?.length ?? 0) > 0 ? [{ label: 'Pricing', href: '#pricing' }] : []),
|
|
55
55
|
];
|
|
56
56
|
const footerLinks = constants.footerLinks || [
|
|
57
57
|
...(constants.privacyPolicy ? [PRIVACY_LINK] : []),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type React from 'react';
|
|
1
2
|
/**
|
|
2
3
|
* 404 page displayed for unmatched routes.
|
|
3
4
|
*
|
|
@@ -10,8 +11,6 @@
|
|
|
10
11
|
*
|
|
11
12
|
* <Route path="*" element={<NotFound />} />
|
|
12
13
|
*/
|
|
13
|
-
export interface NotFoundProps {
|
|
14
|
-
className?: string;
|
|
15
|
-
[key: string]: any;
|
|
14
|
+
export interface NotFoundProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
16
15
|
}
|
|
17
|
-
export default function NotFound({ className, ...props }: NotFoundProps):
|
|
16
|
+
export default function NotFound({ className, ...props }: NotFoundProps): React.JSX.Element;
|
|
@@ -30,6 +30,7 @@ export default function SettingsView() {
|
|
|
30
30
|
const navigate = useNavigate();
|
|
31
31
|
const user = state.user;
|
|
32
32
|
const subscription = user?.subscription;
|
|
33
|
+
const stripeID = subscription?.stripeID;
|
|
33
34
|
const showAuth = (constants.noLogin === false || typeof constants.noLogin === 'undefined') && user;
|
|
34
35
|
function signOutClicked() {
|
|
35
36
|
// Route through the canonical /signout flow (clears the server session and
|
|
@@ -40,10 +41,14 @@ export default function SettingsView() {
|
|
|
40
41
|
const status = subscription?.status;
|
|
41
42
|
if (status === null || typeof status === 'undefined')
|
|
42
43
|
return "Free plan";
|
|
44
|
+
const expires = subscription?.expires;
|
|
45
|
+
const expiresDate = typeof expires === "number"
|
|
46
|
+
? new Date(expires * 1000).toLocaleDateString('en-US')
|
|
47
|
+
: null;
|
|
43
48
|
if (status === "active")
|
|
44
|
-
return `Renews ${
|
|
49
|
+
return expiresDate ? `Renews ${expiresDate}` : "Active";
|
|
45
50
|
if (status === "canceled")
|
|
46
|
-
return `Ends ${
|
|
51
|
+
return expiresDate ? `Ends ${expiresDate}` : "Canceled";
|
|
47
52
|
return `Plan ${status}`;
|
|
48
53
|
}
|
|
49
54
|
return (_jsxs("div", { className: "flex-1", children: [_jsx(Header, { title: "Settings", children: _jsx(ThemeToggle, { iconSize: 20 }) }), _jsxs("div", { className: "flex flex-col items-center p-4 gap-4", children: [!user && constants.noLogin !== true && (_jsx(Card, { className: "w-full max-w-lg", children: _jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Account" }), _jsx(CardDescription, { children: "Sign in to access your account settings." }), _jsx(CardAction, { children: _jsx(Button, { variant: "outline", size: "sm", onClick: () => {
|
|
@@ -53,5 +58,5 @@ export default function SettingsView() {
|
|
|
53
58
|
else {
|
|
54
59
|
navigate('/signin');
|
|
55
60
|
}
|
|
56
|
-
}, children: "Sign In" }) })] }) })), showAuth && (_jsx(Card, { className: "w-full max-w-lg", children: _jsxs(CardHeader, { children: [_jsxs(CardTitle, { className: "flex items-center gap-3", children: [_jsx(Avatar, { children: _jsx(AvatarFallback, { className: "bg-app text-white uppercase font-medium text-sm", children: user.name?.split(' ').map(w => w[0]).join('') || "NA" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2 capitalize", children: [user.name || "No User", subscription?.status === "active" && _jsx(Badge, { children: "Pro" })] }), _jsx(CardDescription, { className: "font-normal normal-case", children: user.email || "no@user.com" })] })] }), _jsx(CardAction, { children: _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { render: _jsx(Button, { variant: "outline", size: "sm", children: "Sign Out" }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Sign Out" }), _jsx(AlertDialogDescription, { children: "Are you sure you want to sign out? You'll need to sign in again to access your account." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Cancel" }), _jsx(AlertDialogAction, { onClick: signOutClicked, children: "Sign Out" })] })] })] }) })] }) })), _jsx(Card, { className: "w-full max-w-lg", children: _jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Support" }), _jsx(CardDescription, { children: "How can we help?" }), _jsx(CardAction, { children: _jsx(Button, { variant: "outline", size: "sm", onClick: () => { window.location.href = `mailto:${constants.companyEmail}`; }, children: "Contact" }) })] }) }), showAuth && (_jsx(Card, { className: "w-full max-w-lg", children: _jsxs(CardHeader, { children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: ["Billing", subscription?.status === "active" ? (_jsx(Badge, { children: "Active" })) : subscription?.status === "canceled" ? (_jsx(Badge, { variant: "destructive", children: "Canceled" })) : (_jsx(Badge, { variant: "secondary", children: "Free" }))] }), _jsx(CardDescription, { children: getBillingDescription() }), _jsx(CardAction, { children:
|
|
61
|
+
}, children: "Sign In" }) })] }) })), showAuth && (_jsx(Card, { className: "w-full max-w-lg", children: _jsxs(CardHeader, { children: [_jsxs(CardTitle, { className: "flex items-center gap-3", children: [_jsx(Avatar, { children: _jsx(AvatarFallback, { className: "bg-app text-white uppercase font-medium text-sm", children: user.name?.split(' ').map(w => w[0]).join('') || "NA" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2 capitalize", children: [user.name || "No User", subscription?.status === "active" && _jsx(Badge, { children: "Pro" })] }), _jsx(CardDescription, { className: "font-normal normal-case", children: user.email || "no@user.com" })] })] }), _jsx(CardAction, { children: _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { render: _jsx(Button, { variant: "outline", size: "sm", children: "Sign Out" }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Sign Out" }), _jsx(AlertDialogDescription, { children: "Are you sure you want to sign out? You'll need to sign in again to access your account." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Cancel" }), _jsx(AlertDialogAction, { onClick: signOutClicked, children: "Sign Out" })] })] })] }) })] }) })), _jsx(Card, { className: "w-full max-w-lg", children: _jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Support" }), _jsx(CardDescription, { children: "How can we help?" }), _jsx(CardAction, { children: _jsx(Button, { variant: "outline", size: "sm", onClick: () => { window.location.href = `mailto:${constants.companyEmail}`; }, children: "Contact" }) })] }) }), showAuth && (_jsx(Card, { className: "w-full max-w-lg", children: _jsxs(CardHeader, { children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: ["Billing", subscription?.status === "active" ? (_jsx(Badge, { children: "Active" })) : subscription?.status === "canceled" ? (_jsx(Badge, { variant: "destructive", children: "Canceled" })) : (_jsx(Badge, { variant: "secondary", children: "Free" }))] }), _jsx(CardDescription, { children: getBillingDescription() }), _jsx(CardAction, { children: stripeID ? (_jsx(Button, { variant: "outline", size: "sm", onClick: () => { showManage(stripeID); }, children: "Manage" })) : (_jsx(Button, { variant: "gradient", size: "sm", onClick: () => { showCheckout(user.email); }, children: "Subscribe" })) })] }) }))] }), _jsx("div", { className: "mt-8 text-center pb-24 md:pb-8", children: _jsxs("p", { className: "text-xs text-muted-foreground", children: ["v", constants.version || '0.0.0'] }) })] }));
|
|
57
62
|
}
|
|
@@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader } from "../../shadcn/ui/card.js";
|
|
|
8
8
|
import { Alert, AlertDescription } from "../../shadcn/ui/alert.js";
|
|
9
9
|
import DynamicIcon from '../core/DynamicIcon.js';
|
|
10
10
|
import { getState } from "../core/Context.js";
|
|
11
|
-
import { getBackendURL, useSafeNavigate } from '../core/Utilities.js';
|
|
11
|
+
import { getBackendURL, useSafeNavigate, getAppKey } from '../core/Utilities.js';
|
|
12
12
|
export default function SignInView({ className, embedded = false, onSuccess, onSwitchMode, ...props }) {
|
|
13
13
|
const { state, dispatch } = getState();
|
|
14
14
|
const constants = state.constants;
|
|
@@ -39,6 +39,12 @@ export default function SignInView({ className, embedded = false, onSuccess, onS
|
|
|
39
39
|
});
|
|
40
40
|
if (response.ok) {
|
|
41
41
|
const data = await response.json();
|
|
42
|
+
// Save CSRF token to localStorage for isAuthenticated() check
|
|
43
|
+
const csrfCookie = document.cookie.split('; ').find(row => row.startsWith('csrf_token='));
|
|
44
|
+
const csrfToken = csrfCookie ? csrfCookie.split('=')[1] : data.csrfToken;
|
|
45
|
+
if (csrfToken) {
|
|
46
|
+
localStorage.setItem(getAppKey('csrf'), csrfToken);
|
|
47
|
+
}
|
|
42
48
|
dispatch({ type: 'SET_USER', payload: data });
|
|
43
49
|
if (embedded && onSuccess) {
|
|
44
50
|
onSuccess();
|
|
@@ -16,6 +16,7 @@ export default function SignUpView({ className, embedded = false, onSuccess, onS
|
|
|
16
16
|
const [password, setPassword] = useState('');
|
|
17
17
|
const [name, setName] = useState('');
|
|
18
18
|
const [errorMessage, setErrorMessage] = useState('');
|
|
19
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
19
20
|
const navigate = useSafeNavigate();
|
|
20
21
|
const nameInputRef = useRef(null);
|
|
21
22
|
// Focus the first input on mount
|
|
@@ -26,6 +27,8 @@ export default function SignUpView({ className, embedded = false, onSuccess, onS
|
|
|
26
27
|
}, []);
|
|
27
28
|
async function signUpClicked(e) {
|
|
28
29
|
e.preventDefault();
|
|
30
|
+
if (isSubmitting)
|
|
31
|
+
return;
|
|
29
32
|
// Client-side password validation (matches backend: 6-72 chars)
|
|
30
33
|
if (password.length < 6) {
|
|
31
34
|
setErrorMessage('Password must be at least 6 characters');
|
|
@@ -35,6 +38,7 @@ export default function SignUpView({ className, embedded = false, onSuccess, onS
|
|
|
35
38
|
setErrorMessage('Password must be 72 characters or less');
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
41
|
+
setIsSubmitting(true);
|
|
38
42
|
try {
|
|
39
43
|
const response = await fetch(`${getBackendURL()}/signup`, {
|
|
40
44
|
method: 'POST',
|
|
@@ -66,6 +70,9 @@ export default function SignUpView({ className, embedded = false, onSuccess, onS
|
|
|
66
70
|
console.error('Signup failed:', error);
|
|
67
71
|
setErrorMessage('Server Error');
|
|
68
72
|
}
|
|
73
|
+
finally {
|
|
74
|
+
setIsSubmitting(false);
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
77
|
const formContent = (_jsxs(_Fragment, { children: [errorMessage !== '' && (_jsx(Alert, { variant: "destructive", className: "mb-4", children: _jsx(AlertDescription, { className: "text-center", children: errorMessage }) })), _jsxs("form", { onSubmit: signUpClicked, className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "name", children: "Name" }), _jsx(Input, { ref: nameInputRef, id: "name", placeholder: "John Doe", autoComplete: "name", required: true, value: name, onChange: (e) => {
|
|
71
78
|
setName(e.target.value);
|
|
@@ -76,7 +83,7 @@ export default function SignUpView({ className, embedded = false, onSuccess, onS
|
|
|
76
83
|
} })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "password", children: "Password" }), _jsx(Input, { id: "password", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: "new-password", required: true, minLength: 6, maxLength: 72, value: password, onChange: (e) => {
|
|
77
84
|
setPassword(e.target.value);
|
|
78
85
|
setErrorMessage('');
|
|
79
|
-
} }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Minimum 6 characters" })] }), _jsx(Button, { type: "submit", variant: "gradient", size: "cta", className: "w-full", children: _jsxs("span", { className: "relative z-20 flex items-center justify-center gap-2 drop-shadow-sm", children: [_jsx(DynamicIcon, { name: "sparkles", size: 16, color: "currentColor", strokeWidth: 2, className: "animate-pulse" }), "Sign Up"] }) }), _jsxs("div", { className: "text-center text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Already have an account?" }), " ", _jsx(Button, { variant: "link", className: "p-0 h-auto", onClick: (e) => { e.preventDefault(); embedded && onSwitchMode ? onSwitchMode() : navigate('/signin'); }, children: "Sign In" })] })] }), _jsxs("div", { className: "mt-4 text-center text-xs text-muted-foreground", children: ["By registering you agree to our", " ", _jsx("a", { href: "/terms", className: "underline underline-offset-4 hover:text-foreground", children: "Terms of Service" }), ",", " ", _jsx("a", { href: "/eula", className: "underline underline-offset-4 hover:text-foreground", children: "EULA" }), ",", " ", _jsx("a", { href: "/privacy", className: "underline underline-offset-4 hover:text-foreground", children: "Privacy Policy" })] })] }));
|
|
86
|
+
} }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Minimum 6 characters" })] }), _jsx(Button, { type: "submit", variant: "gradient", size: "cta", className: "w-full", disabled: isSubmitting, children: _jsxs("span", { className: "relative z-20 flex items-center justify-center gap-2 drop-shadow-sm", children: [_jsx(DynamicIcon, { name: "sparkles", size: 16, color: "currentColor", strokeWidth: 2, className: "animate-pulse" }), isSubmitting ? "Signing up..." : "Sign Up"] }) }), _jsxs("div", { className: "text-center text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Already have an account?" }), " ", _jsx(Button, { variant: "link", className: "p-0 h-auto", onClick: (e) => { e.preventDefault(); embedded && onSwitchMode ? onSwitchMode() : navigate('/signin'); }, children: "Sign In" })] })] }), _jsxs("div", { className: "mt-4 text-center text-xs text-muted-foreground", children: ["By registering you agree to our", " ", _jsx("a", { href: "/terms", className: "underline underline-offset-4 hover:text-foreground", children: "Terms of Service" }), ",", " ", _jsx("a", { href: "/eula", className: "underline underline-offset-4 hover:text-foreground", children: "EULA" }), ",", " ", _jsx("a", { href: "/privacy", className: "underline underline-offset-4 hover:text-foreground", children: "Privacy Policy" })] })] }));
|
|
80
87
|
if (embedded) {
|
|
81
88
|
return formContent;
|
|
82
89
|
}
|
package/dist/ui/accordion.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../shadcn/lib/utils.js";
|
|
5
5
|
import { ChevronDownIcon } from "../icons/index.js";
|
|
6
|
+
import { useControllableState } from "./use-controllable-state.js";
|
|
6
7
|
const AccordionContext = React.createContext({
|
|
7
8
|
value: [],
|
|
8
9
|
toggle: () => { },
|
|
@@ -20,14 +21,11 @@ function useItem() {
|
|
|
20
21
|
* `--accordion-panel-height` var or keyframes).
|
|
21
22
|
*/
|
|
22
23
|
function Accordion({ className, value: controlled, defaultValue = [], onValueChange, openMultiple = true, ...props }) {
|
|
23
|
-
const [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
setValue(next);
|
|
29
|
-
onValueChange?.(next);
|
|
30
|
-
};
|
|
24
|
+
const [open, setOpen] = useControllableState({
|
|
25
|
+
value: controlled,
|
|
26
|
+
defaultValue,
|
|
27
|
+
onChange: onValueChange,
|
|
28
|
+
});
|
|
31
29
|
const toggle = (item) => {
|
|
32
30
|
const has = open.includes(item);
|
|
33
31
|
const next = has
|
|
@@ -35,9 +33,33 @@ function Accordion({ className, value: controlled, defaultValue = [], onValueCha
|
|
|
35
33
|
: openMultiple
|
|
36
34
|
? [...open, item]
|
|
37
35
|
: [item];
|
|
38
|
-
|
|
36
|
+
setOpen(next);
|
|
37
|
+
};
|
|
38
|
+
const onKeyDown = (e) => {
|
|
39
|
+
props.onKeyDown?.(e);
|
|
40
|
+
if (e.defaultPrevented)
|
|
41
|
+
return;
|
|
42
|
+
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(e.key))
|
|
43
|
+
return;
|
|
44
|
+
const triggers = Array.from(e.currentTarget.querySelectorAll('[data-slot="accordion-trigger"]:not([disabled])'));
|
|
45
|
+
const current = triggers.findIndex((t) => t === document.activeElement);
|
|
46
|
+
if (current < 0)
|
|
47
|
+
return;
|
|
48
|
+
let next = -1;
|
|
49
|
+
if (e.key === "ArrowDown")
|
|
50
|
+
next = (current + 1) % triggers.length;
|
|
51
|
+
else if (e.key === "ArrowUp")
|
|
52
|
+
next = (current - 1 + triggers.length) % triggers.length;
|
|
53
|
+
else if (e.key === "Home")
|
|
54
|
+
next = 0;
|
|
55
|
+
else if (e.key === "End")
|
|
56
|
+
next = triggers.length - 1;
|
|
57
|
+
if (next >= 0) {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
triggers[next].focus();
|
|
60
|
+
}
|
|
39
61
|
};
|
|
40
|
-
return (_jsx(AccordionContext.Provider, { value: { value: open, toggle }, children: _jsx("div", { "data-slot": "accordion", className: cn("flex w-full flex-col", className), ...props }) }));
|
|
62
|
+
return (_jsx(AccordionContext.Provider, { value: { value: open, toggle }, children: _jsx("div", { "data-slot": "accordion", className: cn("flex w-full flex-col", className), ...props, onKeyDown: onKeyDown }) }));
|
|
41
63
|
}
|
|
42
64
|
function AccordionItem({ className, value, ...props }) {
|
|
43
65
|
const { value: openValues } = React.useContext(AccordionContext);
|
package/dist/ui/alert-dialog.js
CHANGED
|
@@ -23,9 +23,12 @@ function AlertDialog({ open, defaultOpen = false, onOpenChange, children, }) {
|
|
|
23
23
|
onChange: onOpenChange,
|
|
24
24
|
});
|
|
25
25
|
const { titleId, descriptionId, registerTitle, registerDescription, labelledBy, describedBy, } = useDialogLabelling();
|
|
26
|
+
// Stable id wiring the trigger's aria-controls to the content's id.
|
|
27
|
+
const contentId = React.useId();
|
|
26
28
|
return (_jsx(AlertDialogContext.Provider, { value: {
|
|
27
29
|
open: isOpen,
|
|
28
30
|
setOpen,
|
|
31
|
+
contentId,
|
|
29
32
|
titleId,
|
|
30
33
|
descriptionId,
|
|
31
34
|
registerTitle,
|
|
@@ -35,10 +38,10 @@ function AlertDialog({ open, defaultOpen = false, onOpenChange, children, }) {
|
|
|
35
38
|
}, children: children }));
|
|
36
39
|
}
|
|
37
40
|
function AlertDialogTrigger({ className, asChild = false, render, nativeButton: _nativeButton, children, onClick, ...props }) {
|
|
38
|
-
const { open, setOpen } = useAlertDialog();
|
|
41
|
+
const { open, setOpen, contentId } = useAlertDialog();
|
|
39
42
|
const { useSlot, slotChild } = resolveRender(asChild, render, children);
|
|
40
43
|
const Comp = useSlot ? Slot : "button";
|
|
41
|
-
return (_jsx(Comp, { type: useSlot ? undefined : "button", "data-slot": "alert-dialog-trigger", "aria-haspopup": "dialog", "data-state": open ? "open" : "closed", className: cn("cursor-pointer", className), onClick: (e) => {
|
|
44
|
+
return (_jsx(Comp, { type: useSlot ? undefined : "button", "data-slot": "alert-dialog-trigger", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? contentId : undefined, "data-state": open ? "open" : "closed", className: cn("cursor-pointer", className), onClick: (e) => {
|
|
42
45
|
onClick?.(e);
|
|
43
46
|
if (!e.defaultPrevented)
|
|
44
47
|
setOpen(true);
|
|
@@ -51,7 +54,7 @@ function AlertDialogOverlay() {
|
|
|
51
54
|
return null;
|
|
52
55
|
}
|
|
53
56
|
function AlertDialogContent({ className, size = "default", children, ...props }) {
|
|
54
|
-
const { open, setOpen, labelledBy, describedBy } = useAlertDialog();
|
|
57
|
+
const { open, setOpen, contentId, labelledBy, describedBy } = useAlertDialog();
|
|
55
58
|
const ref = React.useRef(null);
|
|
56
59
|
React.useEffect(() => {
|
|
57
60
|
const node = ref.current;
|
|
@@ -68,7 +71,7 @@ function AlertDialogContent({ className, size = "default", children, ...props })
|
|
|
68
71
|
return (_jsx("dialog", { ref: ref, role: "alertdialog", "aria-labelledby": labelledBy, "aria-describedby": describedBy, "data-slot": "alert-dialog-overlay", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
69
72
|
e.preventDefault();
|
|
70
73
|
setOpen(false);
|
|
71
|
-
}, onClose: () => setOpen(false), className: "m-0 grid max-h-none max-w-none place-items-center bg-transparent p-4 backdrop:bg-black/10 backdrop:duration-100 supports-backdrop-filter:backdrop:backdrop-blur-xs open:fixed open:inset-0 open:size-full", children: open ? (_jsx("div", { "data-slot": "alert-dialog-content", "data-size": size, "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, className: cn("group/alert-dialog-content relative grid w-full gap-6 rounded-xl bg-background p-6 text-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95", className), ...props, children: children })) : null }));
|
|
74
|
+
}, onClose: () => setOpen(false), className: "m-0 grid max-h-none max-w-none place-items-center bg-transparent p-4 backdrop:bg-black/10 backdrop:duration-100 supports-backdrop-filter:backdrop:backdrop-blur-xs open:fixed open:inset-0 open:size-full", children: open ? (_jsx("div", { id: contentId, "data-slot": "alert-dialog-content", "data-size": size, "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, className: cn("group/alert-dialog-content relative grid w-full gap-6 rounded-xl bg-background p-6 text-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95", className), ...props, children: children })) : null }));
|
|
72
75
|
}
|
|
73
76
|
function AlertDialogHeader({ className, ...props }) {
|
|
74
77
|
return (_jsx("div", { "data-slot": "alert-dialog-header", className: cn("grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", className), ...props }));
|
package/dist/ui/avatar.js
CHANGED
|
@@ -13,17 +13,30 @@ function Avatar({ className, size = "default", ...props }) {
|
|
|
13
13
|
}
|
|
14
14
|
function AvatarImage({ className, onLoad, onError, src, ...props }) {
|
|
15
15
|
const { status, setStatus } = React.useContext(AvatarContext);
|
|
16
|
+
const imgRef = React.useRef(null);
|
|
16
17
|
// A missing/empty src never fires load or error, so mark it errored up front;
|
|
17
18
|
// otherwise the fallback would sit next to a blank img, each squeezed to half.
|
|
18
19
|
React.useEffect(() => {
|
|
19
20
|
if (!src)
|
|
20
21
|
setStatus("error");
|
|
21
22
|
}, [src, setStatus]);
|
|
23
|
+
// A cached image can finish loading before React wires up onLoad, leaving the
|
|
24
|
+
// fallback stuck over a hidden loaded image. Reconcile against the DOM on mount
|
|
25
|
+
// and on src change (matches Base UI's cached-image handling).
|
|
26
|
+
React.useLayoutEffect(() => {
|
|
27
|
+
const img = imgRef.current;
|
|
28
|
+
if (!img || !img.complete)
|
|
29
|
+
return;
|
|
30
|
+
if (img.naturalWidth > 0)
|
|
31
|
+
setStatus("loaded");
|
|
32
|
+
else
|
|
33
|
+
setStatus("error");
|
|
34
|
+
}, [src, setStatus]);
|
|
22
35
|
// Render nothing until the image has actually loaded — the fallback owns the
|
|
23
36
|
// box until then (matches Base UI, avoids the blank-img squeeze on slow loads).
|
|
24
37
|
if (!src || status === "error")
|
|
25
38
|
return null;
|
|
26
|
-
return (_jsx("img", { src: src, "data-slot": "avatar-image", className: cn("absolute inset-0 aspect-square size-full rounded-full object-cover", status === "loaded" ? undefined : "opacity-0", className), onLoad: (e) => {
|
|
39
|
+
return (_jsx("img", { ref: imgRef, src: src, "data-slot": "avatar-image", className: cn("absolute inset-0 aspect-square size-full rounded-full object-cover", status === "loaded" ? undefined : "opacity-0", className), onLoad: (e) => {
|
|
27
40
|
setStatus("loaded");
|
|
28
41
|
onLoad?.(e);
|
|
29
42
|
}, onError: (e) => {
|