@inkra/react 0.6.0 → 0.9.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 +166 -1
- package/dist/Button.js +1 -1
- package/dist/PencilIcon.d.ts +14 -1
- package/dist/PencilIcon.js +88 -52
- package/dist/PencilText.js +8 -0
- package/dist/form/Textarea.d.ts +4 -0
- package/dist/form/Textarea.js +14 -8
- package/dist/form/index.d.ts +1 -1
- package/dist/form/textareaSizing.d.ts +29 -0
- package/dist/form/textareaSizing.js +153 -0
- package/dist/guide/index.d.ts +77 -0
- package/dist/guide/index.js +379 -0
- package/dist/i18n/catalog.d.ts +1 -1
- package/dist/i18n/catalog.js +15 -0
- package/dist/image-viewer/index.d.ts +2 -0
- package/dist/image-viewer/index.js +28 -28
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/infinite-scroll/gesture.d.ts +19 -0
- package/dist/infinite-scroll/gesture.js +218 -0
- package/dist/infinite-scroll/index.d.ts +55 -0
- package/dist/infinite-scroll/index.js +543 -0
- package/dist/infinite-scroll/model.d.ts +33 -0
- package/dist/infinite-scroll/model.js +168 -0
- package/dist/internal/iconFill.d.ts +16 -0
- package/dist/internal/iconFill.js +275 -0
- package/dist/inventory-grid/index.d.ts +35 -0
- package/dist/inventory-grid/index.js +215 -0
- package/dist/inventory-grid/model.d.ts +56 -0
- package/dist/inventory-grid/model.js +93 -0
- package/dist/inventory-grid/outline.d.ts +9 -0
- package/dist/inventory-grid/outline.js +54 -0
- package/dist/layout/GridBoard.d.ts +22 -0
- package/dist/layout/GridBoard.js +113 -0
- package/dist/layout/GridResizable.d.ts +31 -0
- package/dist/layout/GridResizable.js +35 -0
- package/dist/layout/index.d.ts +19 -4
- package/dist/layout/index.js +45 -9
- package/dist/layout/swipeNavigation.d.ts +15 -0
- package/dist/layout/swipeNavigation.js +121 -0
- package/dist/loading/index.d.ts +58 -0
- package/dist/loading/index.js +188 -0
- package/dist/loading/locales.d.ts +15 -0
- package/dist/loading/locales.js +34 -0
- package/dist/loading/model.d.ts +6 -0
- package/dist/loading/model.js +29 -0
- package/dist/masonry/index.d.ts +25 -0
- package/dist/masonry/index.js +227 -0
- package/dist/masonry/layout.d.ts +35 -0
- package/dist/masonry/layout.js +67 -0
- package/dist/number-ticker/RollingValue.d.ts +6 -0
- package/dist/number-ticker/RollingValue.js +10 -0
- package/dist/number-ticker/index.d.ts +4 -0
- package/dist/number-ticker/index.js +55 -7
- package/dist/number-ticker/rolling.d.ts +35 -0
- package/dist/number-ticker/rolling.js +87 -0
- package/dist/pagination/index.d.ts +17 -0
- package/dist/pagination/index.js +30 -6
- package/dist/styles/base.css +9 -1
- package/dist/styles/form.css +10 -0
- package/dist/styles/guide.css +202 -0
- package/dist/styles/image-viewer.css +20 -0
- package/dist/styles/infinite-scroll.css +171 -0
- package/dist/styles/inventory-grid.css +43 -0
- package/dist/styles/layout.css +85 -0
- package/dist/styles/loading.css +348 -0
- package/dist/styles/masonry.css +68 -0
- package/dist/styles/number-ticker.css +46 -0
- package/dist/styles/pagination.css +10 -0
- package/dist/styles.css +1162 -146
- package/dist/toast/manager.d.ts +4 -4
- package/package.json +33 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, forwardRef, useId, useState } from 'react';
|
|
3
|
+
import { Resizable } from '../resizable/index.js';
|
|
4
|
+
const equal = (a, b) => Object.keys(a).length === Object.keys(b).length
|
|
5
|
+
&& Object.keys(a).every(key => Math.abs(a[key] - (b[key] ?? -1)) < 0.001);
|
|
6
|
+
const even = (tracks) => Object.fromEntries(tracks.map(track => [track.id, 100 / tracks.length]));
|
|
7
|
+
const scoped = (layout, prefix) => Object.fromEntries(Object.entries(layout).map(([key, value]) => [`${prefix}${key}`, value]));
|
|
8
|
+
const unscoped = (layout, prefix) => Object.fromEntries(Object.entries(layout).map(([key, value]) => [key.slice(prefix.length), value]));
|
|
9
|
+
/** Shared column sizes keep every row aligned; the existing Resizable engine owns all gestures. */
|
|
10
|
+
export const GridResizable = forwardRef(function GridResizable({ columns, rows, columnSizes, defaultColumnSizes, rowSizes, defaultRowSizes, onColumnSizesChange, onRowSizesChange, onColumnSizesCommit, onRowSizesCommit, resizeColumns = true, resizeRows = true, disabled = false, getHandleLabel, className, children, ...props }, ref) {
|
|
11
|
+
const id = useId();
|
|
12
|
+
const [localColumns, setColumns] = useState(() => defaultColumnSizes ?? even(columns));
|
|
13
|
+
const [localRows, setRows] = useState(() => defaultRowSizes ?? even(rows));
|
|
14
|
+
const currentColumns = columnSizes ?? localColumns;
|
|
15
|
+
const currentRows = rowSizes ?? localRows;
|
|
16
|
+
const rowPrefix = `${id}-row-`;
|
|
17
|
+
const changeColumns = (next) => {
|
|
18
|
+
if (equal(next, currentColumns))
|
|
19
|
+
return;
|
|
20
|
+
if (columnSizes === undefined)
|
|
21
|
+
setColumns(next);
|
|
22
|
+
onColumnSizesChange?.(next);
|
|
23
|
+
};
|
|
24
|
+
const changeRows = (next) => {
|
|
25
|
+
if (equal(next, currentRows))
|
|
26
|
+
return;
|
|
27
|
+
if (rowSizes === undefined)
|
|
28
|
+
setRows(next);
|
|
29
|
+
onRowSizesChange?.(next);
|
|
30
|
+
};
|
|
31
|
+
return _jsx("div", { ...props, ref: ref, className: ['inkra-grid-resizable', className].filter(Boolean).join(' '), children: _jsx(Resizable.Root, { id: `${id}-rows`, orientation: "vertical", disabled: disabled || !resizeRows, layout: scoped(currentRows, rowPrefix), onLayoutChange: next => changeRows(unscoped(next, rowPrefix)), onLayoutCommit: next => onRowSizesCommit?.(unscoped(next, rowPrefix)), children: rows.map((row, rowIndex) => {
|
|
32
|
+
const columnPrefix = `${id}-${row.id}-column-`;
|
|
33
|
+
return _jsxs(Fragment, { children: [rowIndex > 0 && _jsx(Resizable.Handle, { label: getHandleLabel?.('row', rows[rowIndex - 1].id, row.id) }), _jsx(Resizable.Panel, { ...row, id: `${rowPrefix}${row.id}`, children: _jsx(Resizable.Root, { id: `${id}-${row.id}-columns`, disabled: disabled || !resizeColumns, layout: scoped(currentColumns, columnPrefix), onLayoutChange: next => changeColumns(unscoped(next, columnPrefix)), onLayoutCommit: next => onColumnSizesCommit?.(unscoped(next, columnPrefix)), children: columns.map((column, columnIndex) => _jsxs(Fragment, { children: [columnIndex > 0 && _jsx(Resizable.Handle, { label: getHandleLabel?.('column', columns[columnIndex - 1].id, column.id) }), _jsx(Resizable.Panel, { ...column, id: `${columnPrefix}${column.id}`, children: _jsx("div", { className: "inkra-grid-resizable__cell", children: children({ row: row.id, column: column.id, rowIndex, columnIndex }) }) })] }, column.id)) }) })] }, row.id);
|
|
34
|
+
}) }) });
|
|
35
|
+
});
|
package/dist/layout/index.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type { CSSProperties, HTMLAttributes } from 'react';
|
|
1
|
+
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import { GridBoard } from './GridBoard.js';
|
|
3
|
+
import { type LayoutSwipeNavigation } from './swipeNavigation.js';
|
|
4
|
+
export type { LayoutSwipeNavigation } from './swipeNavigation.js';
|
|
5
|
+
export { GridResizable } from './GridResizable.js';
|
|
6
|
+
export type { GridResizableProps, GridTrack } from './GridResizable.js';
|
|
7
|
+
export { GridBoard } from './GridBoard.js';
|
|
8
|
+
export type { GridBoardProps, GridBoardLayout, GridBoardLayoutItem } from './GridBoard.js';
|
|
2
9
|
export type LayoutSpace = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16;
|
|
3
10
|
export type LayoutElement = 'div' | 'section' | 'article' | 'nav' | 'header' | 'footer' | 'main' | 'aside' | 'ul' | 'ol' | 'li';
|
|
4
11
|
export type LayoutAlign = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
|
@@ -30,7 +37,10 @@ export interface GridProps extends LayoutElementProps {
|
|
|
30
37
|
columnGap?: LayoutSpace;
|
|
31
38
|
align?: LayoutAlign;
|
|
32
39
|
}
|
|
33
|
-
export declare const Grid: import("react").ForwardRefExoticComponent<GridProps & import("react").RefAttributes<HTMLElement
|
|
40
|
+
export declare const Grid: import("react").ForwardRefExoticComponent<GridProps & import("react").RefAttributes<HTMLElement>> & {
|
|
41
|
+
Resizable: import("react").ForwardRefExoticComponent<import("./GridResizable.js").GridResizableProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
42
|
+
Board: typeof GridBoard;
|
|
43
|
+
};
|
|
34
44
|
export interface ContainerProps extends LayoutElementProps {
|
|
35
45
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
36
46
|
padding?: LayoutSpace;
|
|
@@ -40,6 +50,8 @@ export declare const Container: import("react").ForwardRefExoticComponent<Contai
|
|
|
40
50
|
export interface LayoutProps extends LayoutElementProps {
|
|
41
51
|
gap?: LayoutSpace;
|
|
42
52
|
collapseAt?: 'sm' | 'md' | 'never';
|
|
53
|
+
sidebarWidth?: CSSProperties['width'];
|
|
54
|
+
swipeNavigation?: LayoutSwipeNavigation;
|
|
43
55
|
}
|
|
44
56
|
export interface LayoutBodyProps extends LayoutElementProps {
|
|
45
57
|
gap?: LayoutSpace;
|
|
@@ -51,13 +63,16 @@ export interface LayoutRegionProps extends LayoutElementProps {
|
|
|
51
63
|
export interface LayoutSidebarProps extends LayoutRegionProps {
|
|
52
64
|
width?: CSSProperties['width'];
|
|
53
65
|
}
|
|
66
|
+
export interface LayoutHeaderProps extends LayoutRegionProps {
|
|
67
|
+
sidebar?: ReactNode;
|
|
68
|
+
sidebarDivided?: boolean;
|
|
69
|
+
}
|
|
54
70
|
export type LayoutContentProps = Omit<LayoutRegionProps, 'divided'>;
|
|
55
71
|
export declare const Layout: Readonly<{
|
|
56
72
|
Root: import("react").ForwardRefExoticComponent<LayoutProps & import("react").RefAttributes<HTMLElement>>;
|
|
57
|
-
Header: import("react").ForwardRefExoticComponent<
|
|
73
|
+
Header: import("react").ForwardRefExoticComponent<LayoutHeaderProps & import("react").RefAttributes<HTMLElement>>;
|
|
58
74
|
Body: import("react").ForwardRefExoticComponent<LayoutBodyProps & import("react").RefAttributes<HTMLElement>>;
|
|
59
75
|
Sidebar: import("react").ForwardRefExoticComponent<LayoutSidebarProps & import("react").RefAttributes<HTMLElement>>;
|
|
60
76
|
Content: import("react").ForwardRefExoticComponent<LayoutContentProps & import("react").RefAttributes<HTMLElement>>;
|
|
61
77
|
Footer: import("react").ForwardRefExoticComponent<LayoutRegionProps & import("react").RefAttributes<HTMLElement>>;
|
|
62
78
|
}>;
|
|
63
|
-
export {};
|
package/dist/layout/index.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { createElement, forwardRef } from 'react';
|
|
2
|
+
import { createContext, createElement, forwardRef, useContext, useRef } from 'react';
|
|
3
3
|
import { PencilLine } from '../PencilLine.js';
|
|
4
|
+
import { GridResizable } from './GridResizable.js';
|
|
5
|
+
import { GridBoard } from './GridBoard.js';
|
|
6
|
+
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
7
|
+
import { createLayoutSwipeNavigation } from './swipeNavigation.js';
|
|
8
|
+
export { GridResizable } from './GridResizable.js';
|
|
9
|
+
export { GridBoard } from './GridBoard.js';
|
|
4
10
|
const Box = forwardRef(function Box({ as = 'div', ...props }, ref) {
|
|
5
11
|
return createElement(as, { ...props, ref });
|
|
6
12
|
});
|
|
@@ -17,7 +23,7 @@ export const Stack = forwardRef(function Stack({ direction = 'column', className
|
|
|
17
23
|
return _jsx(Flex, { ...props, ref: ref, direction: direction, className: classes('inkra-stack', className) });
|
|
18
24
|
});
|
|
19
25
|
const columnCount = (value, fallback) => value === undefined || !Number.isFinite(value) ? fallback : Math.min(12, Math.max(1, Math.trunc(value)));
|
|
20
|
-
|
|
26
|
+
const GridRoot = forwardRef(function Grid({ as: Element = 'div', columns = 1, gap = 3, rowGap = gap, columnGap = gap, align = 'stretch', className, style, ...props }, ref) {
|
|
21
27
|
const responsive = typeof columns === 'number' ? { base: columns } : columns;
|
|
22
28
|
const base = columnCount(responsive.base, 1);
|
|
23
29
|
const sm = columnCount(responsive.sm, base);
|
|
@@ -29,29 +35,59 @@ export const Grid = forwardRef(function Grid({ as: Element = 'div', columns = 1,
|
|
|
29
35
|
};
|
|
30
36
|
return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-grid', className), style: resolvedStyle });
|
|
31
37
|
});
|
|
38
|
+
export const Grid = /* @__PURE__ */ Object.assign(GridRoot, { Resizable: GridResizable, Board: GridBoard });
|
|
32
39
|
const containerWidths = { sm: '640px', md: '768px', lg: '1024px', xl: '1280px', full: 'none' };
|
|
33
40
|
export const Container = forwardRef(function Container({ as: Element = 'div', size = 'lg', padding = 5, centered = true, className, style, ...props }, ref) {
|
|
34
41
|
return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-container', className), style: { maxWidth: containerWidths[size], paddingInline: space(padding),
|
|
35
42
|
marginInline: centered ? 'auto' : 0, ...style } });
|
|
36
43
|
});
|
|
37
|
-
const
|
|
38
|
-
|
|
44
|
+
const LayoutColumnContext = createContext(false);
|
|
45
|
+
const normalizeSidebarWidth = (width = 240) => typeof width === 'number' ? (Number.isFinite(width) ? Math.max(0, width) : 240) : width;
|
|
46
|
+
const isCollapsedWidth = (width) => width === 0 || (typeof width === 'string' && /^0(?:\.0+)?(?:[a-z]+|%)?$/i.test(width.trim()));
|
|
47
|
+
const LayoutRoot = forwardRef(function LayoutRoot({ as: Element = 'div', gap = 0, collapseAt = 'sm', sidebarWidth = 240, className, style, swipeNavigation, onTouchStart, ...props }, ref) {
|
|
48
|
+
const sharedWidth = normalizeSidebarWidth(sidebarWidth);
|
|
49
|
+
const resolvedStyle = {
|
|
50
|
+
'--inkra-layout-shared-sidebar-width': typeof sharedWidth === 'number' ? `${sharedWidth}px` : sharedWidth,
|
|
51
|
+
gap: space(gap), ...style,
|
|
52
|
+
};
|
|
53
|
+
const options = useRef(swipeNavigation);
|
|
54
|
+
const gesture = useRef(null);
|
|
55
|
+
if (!gesture.current)
|
|
56
|
+
gesture.current = createLayoutSwipeNavigation(() => options.current);
|
|
57
|
+
useBrowserLayoutEffect(() => { options.current = swipeNavigation; });
|
|
58
|
+
useBrowserLayoutEffect(() => { gesture.current?.cancel(); }, [swipeNavigation?.enabled, swipeNavigation?.open, swipeNavigation?.side, swipeNavigation?.edgeWidth, swipeNavigation?.threshold]);
|
|
59
|
+
useBrowserLayoutEffect(() => () => gesture.current?.dispose(), []);
|
|
60
|
+
return _jsx(LayoutColumnContext.Provider, { value: isCollapsedWidth(sharedWidth), children: _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout', className), onTouchStart: event => {
|
|
61
|
+
onTouchStart?.(event);
|
|
62
|
+
if (!event.defaultPrevented)
|
|
63
|
+
gesture.current?.start(event.nativeEvent, event.currentTarget);
|
|
64
|
+
}, "data-collapse-at": collapseAt, style: resolvedStyle }) });
|
|
39
65
|
});
|
|
40
66
|
const LayoutBody = forwardRef(function LayoutBody({ as: Element = 'div', gap = 0, className, style, ...props }, ref) {
|
|
41
67
|
return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__body', className), style: { gap: space(gap), ...style } });
|
|
42
68
|
});
|
|
43
|
-
const LayoutHeader = forwardRef(function LayoutHeader({ as: Element = 'header', padding = 5, divided = false, className, style, children, ...props }, ref) {
|
|
44
|
-
|
|
69
|
+
const LayoutHeader = forwardRef(function LayoutHeader({ as: Element = 'header', padding = 5, divided = false, sidebar, sidebarDivided = false, className, style, children, ...props }, ref) {
|
|
70
|
+
const collapsed = useContext(LayoutColumnContext);
|
|
71
|
+
const split = sidebar !== undefined && sidebar !== null && typeof sidebar !== 'boolean';
|
|
72
|
+
const resolvedStyle = {
|
|
73
|
+
'--inkra-layout-header-padding': space(padding),
|
|
74
|
+
padding: split ? 0 : space(padding), ...style,
|
|
75
|
+
};
|
|
76
|
+
return _jsxs(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__header', className), "data-shared-sidebar": split || undefined, style: resolvedStyle, children: [split ? _jsxs(_Fragment, { children: [_jsxs("div", { className: "inkra-layout__header-sidebar", hidden: collapsed, inert: collapsed || undefined, "aria-hidden": collapsed || undefined, children: [sidebar, sidebarDivided && _jsx(PencilLine, { className: "inkra-layout__header-sidebar-divider", orientation: "vertical" })] }), _jsx("div", { className: "inkra-layout__header-content", children: children })] }) : children, divided && _jsx(PencilLine, { className: "inkra-layout__divider" })] });
|
|
45
77
|
});
|
|
46
78
|
const LayoutFooter = forwardRef(function LayoutFooter({ as: Element = 'footer', padding = 5, divided = false, className, style, children, ...props }, ref) {
|
|
47
79
|
return _jsxs(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__footer', className), style: { padding: space(padding), ...style }, children: [divided && _jsx(PencilLine, { className: "inkra-layout__divider" }), children] });
|
|
48
80
|
});
|
|
49
|
-
const LayoutSidebar = forwardRef(function LayoutSidebar({ as: Element = 'aside', width
|
|
81
|
+
const LayoutSidebar = forwardRef(function LayoutSidebar({ as: Element = 'aside', width, padding = 5, divided = false, className, style, children, hidden, inert, ...props }, ref) {
|
|
82
|
+
const sharedCollapsed = useContext(LayoutColumnContext);
|
|
83
|
+
const resolvedWidth = width === undefined ? undefined : normalizeSidebarWidth(width);
|
|
84
|
+
const collapsed = resolvedWidth === undefined ? sharedCollapsed : isCollapsedWidth(resolvedWidth);
|
|
50
85
|
const resolvedStyle = {
|
|
51
|
-
'--inkra-layout-sidebar-width':
|
|
86
|
+
'--inkra-layout-sidebar-width': resolvedWidth === undefined ? 'var(--inkra-layout-shared-sidebar-width, 240px)'
|
|
87
|
+
: typeof resolvedWidth === 'number' ? `${resolvedWidth}px` : resolvedWidth,
|
|
52
88
|
padding: space(padding), ...style,
|
|
53
89
|
};
|
|
54
|
-
return _jsxs(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__sidebar', className), style: resolvedStyle, children: [children, divided && _jsxs(_Fragment, { children: [_jsx(PencilLine, { className: "inkra-layout__sidebar-divider", orientation: "vertical" }), _jsx(PencilLine, { className: "inkra-layout__sidebar-divider-mobile" })] })] });
|
|
90
|
+
return _jsxs(Box, { as: Element, ...props, ref: ref, hidden: collapsed || hidden, inert: collapsed || inert, "aria-hidden": collapsed ? true : props['aria-hidden'], className: classes('inkra-layout__sidebar', className), style: resolvedStyle, children: [children, divided && _jsxs(_Fragment, { children: [_jsx(PencilLine, { className: "inkra-layout__sidebar-divider", orientation: "vertical" }), _jsx(PencilLine, { className: "inkra-layout__sidebar-divider-mobile" })] })] });
|
|
55
91
|
});
|
|
56
92
|
const LayoutContent = forwardRef(function LayoutContent({ as: Element = 'div', padding = 5, className, style, ...props }, ref) {
|
|
57
93
|
return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__content', className), style: { padding: space(padding), ...style } });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface LayoutSwipeNavigation {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
side?: 'start' | 'end';
|
|
6
|
+
/** Opening starts within this many local CSS pixels of the sidebar edge. */
|
|
7
|
+
edgeWidth?: number;
|
|
8
|
+
/** Minimum horizontal travel before release commits the gesture. */
|
|
9
|
+
threshold?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function createLayoutSwipeNavigation(read: () => LayoutSwipeNavigation | undefined): {
|
|
12
|
+
start: (event: TouchEvent, root: HTMLElement) => void;
|
|
13
|
+
cancel: () => void;
|
|
14
|
+
dispose: () => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const ignored = 'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="slider"], [role="spinbutton"], [role="combobox"], [role="separator"], [role="listbox"], video, audio, [data-layout-swipe-ignore]';
|
|
2
|
+
const positive = (value, fallback) => value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
3
|
+
// Listen only for the lifetime of one edge/closing gesture. Vertical scroll and
|
|
4
|
+
// nested horizontal tools keep their native behavior; no global touch-action override.
|
|
5
|
+
export function createLayoutSwipeNavigation(read) {
|
|
6
|
+
let cancel = () => { };
|
|
7
|
+
let clearClick = () => { };
|
|
8
|
+
function start(event, root) {
|
|
9
|
+
clearClick();
|
|
10
|
+
cancel();
|
|
11
|
+
const options = read();
|
|
12
|
+
const view = root.ownerDocument.defaultView;
|
|
13
|
+
if (!options || options.enabled === false || !view || event.defaultPrevented || event.touches.length !== 1)
|
|
14
|
+
return;
|
|
15
|
+
const target = event.target;
|
|
16
|
+
if (!target?.closest || target.closest(ignored) || target.closest('.inkra-layout') !== root)
|
|
17
|
+
return;
|
|
18
|
+
for (let node = target; node && node !== root; node = node.parentElement) {
|
|
19
|
+
if (node.scrollWidth > node.clientWidth + 1 && /auto|scroll/.test(view.getComputedStyle(node).overflowX))
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const rect = root.getBoundingClientRect();
|
|
23
|
+
if (!rect.width || !root.clientWidth)
|
|
24
|
+
return;
|
|
25
|
+
const scale = root.clientWidth / rect.width;
|
|
26
|
+
const touch = event.touches[0];
|
|
27
|
+
const rtl = view.getComputedStyle(root).direction === 'rtl';
|
|
28
|
+
const right = (options.side === 'end') !== rtl;
|
|
29
|
+
const edge = Math.min(root.clientWidth / 2, positive(options.edgeWidth, 24));
|
|
30
|
+
const startX = (touch.clientX - rect.left) * scale;
|
|
31
|
+
if (!options.open && (right ? root.clientWidth - startX : startX) > edge)
|
|
32
|
+
return;
|
|
33
|
+
const direction = (right ? -1 : 1) * (options.open ? -1 : 1);
|
|
34
|
+
const threshold = positive(options.threshold, 48);
|
|
35
|
+
const doc = root.ownerDocument;
|
|
36
|
+
let locked = false;
|
|
37
|
+
let stopped = false;
|
|
38
|
+
let travel = 0;
|
|
39
|
+
const suppressClick = () => {
|
|
40
|
+
clearClick();
|
|
41
|
+
const block = (click) => {
|
|
42
|
+
if (!click.detail || ('pointerType' in click && click.pointerType !== 'touch'))
|
|
43
|
+
return;
|
|
44
|
+
click.preventDefault();
|
|
45
|
+
click.stopImmediatePropagation();
|
|
46
|
+
clearClick();
|
|
47
|
+
};
|
|
48
|
+
root.addEventListener('click', block, { capture: true });
|
|
49
|
+
const timer = view.setTimeout(() => clearClick(), 700);
|
|
50
|
+
clearClick = () => {
|
|
51
|
+
root.removeEventListener('click', block, { capture: true });
|
|
52
|
+
view.clearTimeout(timer);
|
|
53
|
+
clearClick = () => { };
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const cleanup = () => {
|
|
57
|
+
stopped = true;
|
|
58
|
+
doc.removeEventListener('touchstart', multi);
|
|
59
|
+
doc.removeEventListener('touchmove', move);
|
|
60
|
+
doc.removeEventListener('touchend', end);
|
|
61
|
+
doc.removeEventListener('touchcancel', abort);
|
|
62
|
+
view.removeEventListener('blur', abort);
|
|
63
|
+
if (locked)
|
|
64
|
+
suppressClick();
|
|
65
|
+
cancel = () => { };
|
|
66
|
+
};
|
|
67
|
+
const valid = () => {
|
|
68
|
+
const next = read();
|
|
69
|
+
return root.isConnected && next && next.enabled !== false && next.open === options.open
|
|
70
|
+
&& (next.side ?? 'start') === (options.side ?? 'start')
|
|
71
|
+
&& (view.getComputedStyle(root).direction === 'rtl') === rtl
|
|
72
|
+
&& Math.abs(root.getBoundingClientRect().width - rect.width) < 1;
|
|
73
|
+
};
|
|
74
|
+
const abort = () => cleanup();
|
|
75
|
+
const multi = (next) => { if (next.touches.length !== 1)
|
|
76
|
+
cleanup(); };
|
|
77
|
+
const move = (next) => {
|
|
78
|
+
if (stopped)
|
|
79
|
+
return;
|
|
80
|
+
const point = Array.from(next.touches).find(item => item.identifier === touch.identifier);
|
|
81
|
+
if (!valid() || next.defaultPrevented || next.touches.length !== 1 || !point) {
|
|
82
|
+
cleanup();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const dx = (point.clientX - touch.clientX) * scale;
|
|
86
|
+
const dy = (point.clientY - touch.clientY) * scale;
|
|
87
|
+
if (!locked) {
|
|
88
|
+
if (Math.max(Math.abs(dx), Math.abs(dy)) < 8)
|
|
89
|
+
return;
|
|
90
|
+
if (dx * direction <= 0 || Math.abs(dx) < Math.abs(dy) * 1.25) {
|
|
91
|
+
cleanup();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!next.cancelable) {
|
|
96
|
+
cleanup();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
locked = true;
|
|
100
|
+
travel = Math.abs(dx) >= Math.abs(dy) * 1.25 ? dx * direction : 0;
|
|
101
|
+
next.preventDefault();
|
|
102
|
+
};
|
|
103
|
+
const end = (next) => {
|
|
104
|
+
const point = Array.from(next.changedTouches).find(item => item.identifier === touch.identifier);
|
|
105
|
+
const dx = point ? (point.clientX - touch.clientX) * scale : 0;
|
|
106
|
+
const dy = point ? (point.clientY - touch.clientY) * scale : 0;
|
|
107
|
+
const commit = !next.defaultPrevented && next.touches.length === 0 && valid() && locked
|
|
108
|
+
&& point && travel >= threshold && dx * direction >= threshold && Math.abs(dx) >= Math.abs(dy) * 1.25;
|
|
109
|
+
cleanup();
|
|
110
|
+
if (commit)
|
|
111
|
+
read()?.onOpenChange(!options.open);
|
|
112
|
+
};
|
|
113
|
+
doc.addEventListener('touchstart', multi, { passive: true });
|
|
114
|
+
doc.addEventListener('touchmove', move, { passive: false });
|
|
115
|
+
doc.addEventListener('touchend', end);
|
|
116
|
+
doc.addEventListener('touchcancel', abort);
|
|
117
|
+
view.addEventListener('blur', abort);
|
|
118
|
+
cancel = cleanup;
|
|
119
|
+
}
|
|
120
|
+
return { start, cancel: () => cancel(), dispose: () => { cancel(); clearClick(); } };
|
|
121
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import { type LoadingLabels } from './locales.js';
|
|
3
|
+
import { type LoadingTipOrder } from './model.js';
|
|
4
|
+
export type { LoadingLabels } from './locales.js';
|
|
5
|
+
export type { LoadingTipOrder } from './model.js';
|
|
6
|
+
export type LoadingMode = 'inline' | 'region' | 'fullscreen';
|
|
7
|
+
export type LoadingPreset = 'brand-center' | 'scene-bottom' | 'intel' | 'staged' | 'edge' | 'skeleton';
|
|
8
|
+
export type LoadingAnimation = 'progress' | 'orbit' | 'dots' | 'pulse' | 'trail';
|
|
9
|
+
export type LoadingStatus = 'loading' | 'error' | 'complete';
|
|
10
|
+
export type LoadingStageState = 'pending' | 'active' | 'complete' | 'error';
|
|
11
|
+
export type LoadingSlot = 'top-start' | 'top-end' | 'center' | 'bottom-start' | 'bottom-center' | 'bottom-end';
|
|
12
|
+
export type LoadingTipTransition = 'fade' | 'slide' | 'none';
|
|
13
|
+
export interface LoadingStage {
|
|
14
|
+
id: string;
|
|
15
|
+
label: ReactNode;
|
|
16
|
+
state?: LoadingStageState;
|
|
17
|
+
description?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export interface LoadingProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> {
|
|
20
|
+
active?: boolean;
|
|
21
|
+
mode?: LoadingMode;
|
|
22
|
+
preset?: LoadingPreset;
|
|
23
|
+
animation?: LoadingAnimation;
|
|
24
|
+
status?: LoadingStatus;
|
|
25
|
+
blocking?: boolean;
|
|
26
|
+
lockScroll?: boolean;
|
|
27
|
+
delay?: number;
|
|
28
|
+
minDuration?: number;
|
|
29
|
+
logo?: ReactNode;
|
|
30
|
+
title?: ReactNode;
|
|
31
|
+
description?: ReactNode;
|
|
32
|
+
background?: ReactNode;
|
|
33
|
+
skeleton?: ReactNode;
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
slots?: Partial<Record<LoadingSlot, ReactNode>>;
|
|
36
|
+
progress?: number | null;
|
|
37
|
+
max?: number;
|
|
38
|
+
progressLabel?: ReactNode;
|
|
39
|
+
stages?: readonly LoadingStage[];
|
|
40
|
+
activeStage?: string;
|
|
41
|
+
tips?: readonly ReactNode[];
|
|
42
|
+
tipOrder?: LoadingTipOrder;
|
|
43
|
+
tipInterval?: number;
|
|
44
|
+
tipAutoPlay?: boolean;
|
|
45
|
+
tipPaused?: boolean;
|
|
46
|
+
defaultTipPaused?: boolean;
|
|
47
|
+
onTipPausedChange?: (paused: boolean) => void;
|
|
48
|
+
tipIndex?: number;
|
|
49
|
+
defaultTipIndex?: number;
|
|
50
|
+
onTipIndexChange?: (index: number) => void;
|
|
51
|
+
tipTransition?: LoadingTipTransition;
|
|
52
|
+
showTipControls?: boolean;
|
|
53
|
+
labels?: Partial<LoadingLabels>;
|
|
54
|
+
onCancel?: () => void;
|
|
55
|
+
onRetry?: () => void;
|
|
56
|
+
onContinue?: () => void;
|
|
57
|
+
}
|
|
58
|
+
export declare const Loading: import("react").ForwardRefExoticComponent<LoadingProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { FocusScope } from 'react-aria/FocusScope';
|
|
3
|
+
import { ariaHideOutside } from 'react-aria/private/overlays/ariaHideOutside';
|
|
4
|
+
import { usePreventScroll } from 'react-aria/usePreventScroll';
|
|
5
|
+
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
7
|
+
import { Button } from '../Button.js';
|
|
8
|
+
import { PencilLine } from '../PencilLine.js';
|
|
9
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
10
|
+
import { PencilText } from '../PencilText.js';
|
|
11
|
+
import { Progress, Skeleton } from '../display/index.js';
|
|
12
|
+
import { useInkraLocale } from '../i18n/index.js';
|
|
13
|
+
import { PortalThemeBridge, PortalThemeProvider, usePortalTriggerRef, } from '../overlay/themeBridge.js';
|
|
14
|
+
import { formatLoadingLabel, resolveLoadingLabels } from './locales.js';
|
|
15
|
+
import { finiteDuration, nextTipIndex, normalizeLoadingMax, normalizeLoadingProgress, normalizeTipIndex, } from './model.js';
|
|
16
|
+
function classes(...values) {
|
|
17
|
+
return values.filter(Boolean).join(' ');
|
|
18
|
+
}
|
|
19
|
+
function assignRef(ref, value) {
|
|
20
|
+
if (typeof ref === 'function')
|
|
21
|
+
ref(value);
|
|
22
|
+
else if (ref)
|
|
23
|
+
ref.current = value;
|
|
24
|
+
}
|
|
25
|
+
function useTimedPresence(active, delay, minDuration) {
|
|
26
|
+
const startsVisible = active && delay === 0;
|
|
27
|
+
const [present, setPresent] = useState(startsVisible);
|
|
28
|
+
const visibleAt = useRef(startsVisible ? Date.now() : null);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
let timer;
|
|
31
|
+
if (active) {
|
|
32
|
+
if (present)
|
|
33
|
+
return undefined;
|
|
34
|
+
timer = setTimeout(() => {
|
|
35
|
+
visibleAt.current = Date.now();
|
|
36
|
+
setPresent(true);
|
|
37
|
+
}, delay);
|
|
38
|
+
}
|
|
39
|
+
else if (present) {
|
|
40
|
+
const elapsed = visibleAt.current == null ? minDuration : Date.now() - visibleAt.current;
|
|
41
|
+
timer = setTimeout(() => {
|
|
42
|
+
visibleAt.current = null;
|
|
43
|
+
setPresent(false);
|
|
44
|
+
}, Math.max(0, minDuration - elapsed));
|
|
45
|
+
}
|
|
46
|
+
return () => clearTimeout(timer);
|
|
47
|
+
}, [active, delay, minDuration, present]);
|
|
48
|
+
return present;
|
|
49
|
+
}
|
|
50
|
+
function useControllableTipIndex(controlledIndex, defaultIndex, length, onChange) {
|
|
51
|
+
const [uncontrolledIndex, setUncontrolledIndex] = useState(() => normalizeTipIndex(defaultIndex, length));
|
|
52
|
+
const controlled = controlledIndex !== undefined;
|
|
53
|
+
const index = normalizeTipIndex(controlled ? controlledIndex : uncontrolledIndex, length);
|
|
54
|
+
const setIndex = useCallback((next) => {
|
|
55
|
+
const normalized = normalizeTipIndex(next, length);
|
|
56
|
+
if (!controlled)
|
|
57
|
+
setUncontrolledIndex(normalized);
|
|
58
|
+
if (normalized !== index)
|
|
59
|
+
onChange?.(normalized);
|
|
60
|
+
}, [controlled, index, length, onChange]);
|
|
61
|
+
return [index, setIndex];
|
|
62
|
+
}
|
|
63
|
+
function useControllableBoolean(value, defaultValue, onChange) {
|
|
64
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
65
|
+
const controlled = value !== undefined;
|
|
66
|
+
const current = controlled ? value : uncontrolledValue;
|
|
67
|
+
const setCurrent = useCallback((next) => {
|
|
68
|
+
if (!controlled)
|
|
69
|
+
setUncontrolledValue(next);
|
|
70
|
+
if (next !== current)
|
|
71
|
+
onChange?.(next);
|
|
72
|
+
}, [controlled, current, onChange]);
|
|
73
|
+
return [current, setCurrent];
|
|
74
|
+
}
|
|
75
|
+
function DefaultSkeleton() {
|
|
76
|
+
return _jsxs("div", { className: "inkra-loading__skeleton", "aria-hidden": "true", children: [_jsx(Skeleton, { height: 28, width: "42%" }), _jsx(Skeleton, { height: 16, width: "88%" }), _jsx(Skeleton, { height: 16, width: "76%" }), _jsxs("div", { className: "inkra-loading__skeleton-grid", children: [_jsx(Skeleton, { height: 96 }), _jsx(Skeleton, { height: 96 }), _jsx(Skeleton, { height: 96 })] })] });
|
|
77
|
+
}
|
|
78
|
+
function LoadingAnimationVisual({ animation, seed }) {
|
|
79
|
+
const marks = [0, 1, 2];
|
|
80
|
+
return _jsx("span", { className: "inkra-loading__animation-visual", "data-kind": animation, "aria-hidden": "true", children: animation === 'trail'
|
|
81
|
+
? marks.map(index => _jsx(PencilLine, { className: "inkra-loading__animation-mark", seed: `${seed}-trail-${index}` }, index))
|
|
82
|
+
: marks.map(index => _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-loading__animation-mark", radius: 8, seed: `${seed}-${animation}-${index}`, variant: animation === 'pulse' ? 'outline' : 'quiet' }, index)) });
|
|
83
|
+
}
|
|
84
|
+
function stageState(stage, activeStage) {
|
|
85
|
+
if (stage.state)
|
|
86
|
+
return stage.state;
|
|
87
|
+
return stage.id === activeStage ? 'active' : 'pending';
|
|
88
|
+
}
|
|
89
|
+
function LoadingLayer({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, activeStage, animation = 'progress', background, blocking, children, className, defaultTipIndex, defaultTipPaused = false, description, forwardedRef, labelsResolved, lockScroll, logo, max, mode = 'inline', onCancel, onContinue, onRetry, onTipIndexChange, onTipPausedChange, preset = 'brand-center', present, progress, progressLabel, showTipControls = false, skeleton, slots, stages = [], status = 'loading', style, tipAutoPlay = true, tipIndex, tipInterval, tipOrder = 'sequence', tipPaused, tips = [], tipTransition = 'fade', title, ...props }) {
|
|
90
|
+
const id = useId();
|
|
91
|
+
const rootRef = useRef(null);
|
|
92
|
+
const titleId = `${id}-title`;
|
|
93
|
+
const resolvedMax = normalizeLoadingMax(max);
|
|
94
|
+
const resolvedProgress = normalizeLoadingProgress(progress, resolvedMax);
|
|
95
|
+
const showCustomAnimation = animation !== 'progress'
|
|
96
|
+
&& status === 'loading'
|
|
97
|
+
&& (progress === undefined || progress === null);
|
|
98
|
+
const showProgress = progress !== undefined
|
|
99
|
+
&& (animation === 'progress' || progress !== null);
|
|
100
|
+
const interval = Math.max(1000, finiteDuration(tipInterval, 5000));
|
|
101
|
+
const [currentTip, setCurrentTip] = useControllableTipIndex(tipIndex, defaultTipIndex, tips.length, onTipIndexChange);
|
|
102
|
+
const [tipsPaused, setTipsPaused] = useControllableBoolean(tipPaused, defaultTipPaused, onTipPausedChange);
|
|
103
|
+
const activeStageIndex = stages.findIndex(stage => stage.id === activeStage || stageState(stage, activeStage) === 'active');
|
|
104
|
+
const stageAnnouncement = stages.length > 0 && activeStageIndex >= 0
|
|
105
|
+
? formatLoadingLabel(labelsResolved.stageProgress, { current: activeStageIndex + 1, total: stages.length })
|
|
106
|
+
: undefined;
|
|
107
|
+
const defaultStatus = labelsResolved[status];
|
|
108
|
+
const effectiveBlocking = mode === 'fullscreen' && (blocking ?? preset !== 'edge');
|
|
109
|
+
const effectiveLockScroll = mode === 'fullscreen' && (lockScroll ?? effectiveBlocking);
|
|
110
|
+
usePreventScroll({ isDisabled: !(effectiveLockScroll && present) });
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!effectiveBlocking || !present || !rootRef.current)
|
|
113
|
+
return undefined;
|
|
114
|
+
return ariaHideOutside([rootRef.current], { shouldUseInert: true });
|
|
115
|
+
}, [effectiveBlocking, present]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!present || tips.length <= 1 || !tipAutoPlay || tipsPaused || status !== 'loading')
|
|
118
|
+
return undefined;
|
|
119
|
+
const timer = setInterval(() => setCurrentTip(nextTipIndex(currentTip, tips.length, tipOrder)), interval);
|
|
120
|
+
return () => clearInterval(timer);
|
|
121
|
+
}, [currentTip, interval, present, setCurrentTip, status, tipAutoPlay, tipOrder, tips.length, tipsPaused]);
|
|
122
|
+
const renderedTitle = title ?? defaultStatus;
|
|
123
|
+
const showActions = Boolean(onCancel || status === 'error' && onRetry || status === 'complete' && onContinue);
|
|
124
|
+
const hasInteractiveContent = showActions || showTipControls || children != null
|
|
125
|
+
|| Object.values(slots ?? {}).some(node => node != null);
|
|
126
|
+
const semanticRole = effectiveBlocking ? 'dialog' : hasInteractiveContent ? 'region' : 'status';
|
|
127
|
+
const content = _jsxs("div", { ...props, "aria-busy": status === 'loading' || undefined, "aria-label": ariaLabel ?? (ariaLabelledBy || typeof renderedTitle === 'string' ? undefined : defaultStatus), "aria-labelledby": ariaLabelledBy ?? (typeof renderedTitle === 'string' ? titleId : undefined), "aria-live": semanticRole === 'status' ? 'polite' : undefined, "aria-modal": effectiveBlocking || undefined, className: classes('inkra-loading', className), "data-animation": animation, "data-blocking": effectiveBlocking || undefined, "data-mode": mode, "data-preset": preset, "data-status": status, ref: node => {
|
|
128
|
+
rootRef.current = node;
|
|
129
|
+
assignRef(forwardedRef, node);
|
|
130
|
+
}, role: semanticRole, style: style, tabIndex: effectiveBlocking ? -1 : undefined, children: [background == null ? null : _jsx("div", { className: "inkra-loading__background", "aria-hidden": "true", children: background }), _jsx("div", { className: "inkra-loading__veil", "aria-hidden": "true" }), semanticRole === 'region' ? _jsx("span", { className: "inkra-loading__sr-status", role: "status", children: defaultStatus }) : null, _jsx("div", { className: "inkra-loading__layout", children: _jsxs("div", { className: "inkra-loading__main", children: [logo == null ? null : _jsx("div", { className: "inkra-loading__logo", children: logo }), _jsxs("div", { className: "inkra-loading__copy", children: [typeof renderedTitle === 'string'
|
|
131
|
+
? _jsx(PencilText, { as: "div", className: "inkra-loading__title", id: titleId, seed: `${id}-title`, children: renderedTitle })
|
|
132
|
+
: _jsx("div", { className: "inkra-loading__title", id: titleId, children: renderedTitle }), description == null ? null : typeof description === 'string'
|
|
133
|
+
? _jsx(PencilText, { as: "div", className: "inkra-loading__description", seed: `${id}-description`, children: description })
|
|
134
|
+
: _jsx("div", { className: "inkra-loading__description", children: description })] }), showCustomAnimation ? _jsxs("div", { className: "inkra-loading__animation-progress", children: [progressLabel == null ? null : typeof progressLabel === 'string'
|
|
135
|
+
? _jsx(PencilText, { as: "div", className: "inkra-loading__animation-label", children: progressLabel })
|
|
136
|
+
: _jsx("div", { className: "inkra-loading__animation-label", children: progressLabel }), _jsx("div", { "aria-label": typeof progressLabel === 'string' ? progressLabel : defaultStatus, className: "inkra-loading__animation", role: "progressbar", children: _jsx(LoadingAnimationVisual, { animation: animation, seed: `${id}-animation` }) })] }) : null, preset === 'skeleton' ? (skeleton ?? _jsx(DefaultSkeleton, {})) : null, stages.length > 0 ? _jsx("div", { className: "inkra-loading__stages", "aria-label": stageAnnouncement, children: stages.map((stage, index) => {
|
|
137
|
+
const state = stageState(stage, activeStage);
|
|
138
|
+
return _jsxs("div", { className: "inkra-loading__stage", "data-state": state, children: [_jsx(PencilSurface, { className: "inkra-loading__stage-marker", variant: state === 'active' ? 'selection' : state === 'error' ? 'danger' : 'outline', radius: 8, children: _jsx(PencilText, { children: state === 'complete' ? '✓' : state === 'error' ? '!' : String(index + 1) }) }), _jsxs("div", { className: "inkra-loading__stage-copy", children: [typeof stage.label === 'string' ? _jsx(PencilText, { children: stage.label }) : stage.label, stage.description == null ? null : _jsx("span", { className: "inkra-loading__stage-description", children: stage.description })] })] }, stage.id);
|
|
139
|
+
}) }) : null, showProgress ? _jsx(Progress, { "aria-label": typeof progressLabel === 'string' ? progressLabel : defaultStatus, className: "inkra-loading__progress", label: progressLabel, max: resolvedMax, shape: preset === 'brand-center' ? 'circular' : 'linear', size: preset === 'brand-center' ? 'sm' : 'md', value: resolvedProgress ?? null, children: resolvedProgress == null || preset === 'brand-center' ? null : `${Math.round(resolvedProgress / resolvedMax * 100)}%` }) : null, tips.length > 0 ? _jsxs("div", { className: "inkra-loading__tips", "data-transition": tipTransition, children: [_jsx("div", { className: "inkra-loading__tip", children: tips[currentTip] }, `${currentTip}-${tipTransition}`), showTipControls && tips.length > 1 ? _jsxs("div", { className: "inkra-loading__tip-controls", children: [_jsx(Button, { "aria-label": labelsResolved.previousTip, size: "icon-sm", variant: "ghost", onClick: () => setCurrentTip(currentTip - 1), children: "\u2190" }), _jsxs("span", { className: "inkra-loading__tip-position", "aria-live": "off", children: [currentTip + 1, " / ", tips.length] }), tipAutoPlay ? _jsx(Button, { "aria-label": tipsPaused ? labelsResolved.resumeTips : labelsResolved.pauseTips, size: "icon-sm", variant: "ghost", onClick: () => setTipsPaused(!tipsPaused), children: tipsPaused ? '▶' : 'Ⅱ' }) : null, _jsx(Button, { "aria-label": labelsResolved.nextTip, size: "icon-sm", variant: "ghost", onClick: () => setCurrentTip(currentTip + 1), children: "\u2192" })] }) : null] }) : null, children == null ? null : _jsx("div", { className: "inkra-loading__content-layer", children: children }), showActions ? _jsxs("div", { className: "inkra-loading__actions", children: [onCancel ? _jsx(Button, { variant: "ghost", onClick: onCancel, children: labelsResolved.cancel }) : null, status === 'error' && onRetry ? _jsx(Button, { onClick: onRetry, children: labelsResolved.retry }) : null, status === 'complete' && onContinue ? _jsx(Button, { onClick: onContinue, children: labelsResolved.continue }) : null] }) : null] }) }), Object.entries(slots ?? {}).map(([slot, node]) => node == null ? null :
|
|
140
|
+
_jsx("div", { className: "inkra-loading__slot", "data-slot": slot, children: node }, slot)), preset === 'edge' ? _jsx(PencilLine, { className: "inkra-loading__edge-line", seed: `${id}-edge` }) : null] });
|
|
141
|
+
return effectiveBlocking
|
|
142
|
+
? _jsx(FocusScope, { contain: true, restoreFocus: true, autoFocus: true, children: content })
|
|
143
|
+
: content;
|
|
144
|
+
}
|
|
145
|
+
function FullscreenPortal(props) {
|
|
146
|
+
const [mounted, setMounted] = useState(false);
|
|
147
|
+
const anchorRef = usePortalTriggerRef(null);
|
|
148
|
+
const restoreFocusRef = useRef(typeof document !== 'undefined' && document.activeElement instanceof HTMLElement
|
|
149
|
+
? document.activeElement
|
|
150
|
+
: null);
|
|
151
|
+
const restoreGenerationRef = useRef(0);
|
|
152
|
+
useEffect(() => setMounted(true), []);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const generation = ++restoreGenerationRef.current;
|
|
155
|
+
return () => {
|
|
156
|
+
const target = restoreFocusRef.current;
|
|
157
|
+
if (!target?.isConnected || typeof requestAnimationFrame !== 'function')
|
|
158
|
+
return;
|
|
159
|
+
requestAnimationFrame(() => {
|
|
160
|
+
if (restoreGenerationRef.current !== generation || !target.isConnected)
|
|
161
|
+
return;
|
|
162
|
+
if (document.activeElement === document.body)
|
|
163
|
+
target.focus({ preventScroll: true });
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
}, []);
|
|
167
|
+
const layer = _jsx(PortalThemeBridge, { children: _jsx(LoadingLayer, { ...props }) });
|
|
168
|
+
return _jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "inkra-loading__theme-anchor", ref: anchorRef }), mounted && typeof document !== 'undefined' ? createPortal(layer, document.body) : layer] });
|
|
169
|
+
}
|
|
170
|
+
export const Loading = forwardRef(function Loading({ active = true, delay, labels, minDuration, mode = 'inline', ...props }, ref) {
|
|
171
|
+
const { locale } = useInkraLocale();
|
|
172
|
+
const resolvedDelay = finiteDuration(delay, 0);
|
|
173
|
+
const resolvedMinDuration = finiteDuration(minDuration, 0);
|
|
174
|
+
const present = useTimedPresence(active, resolvedDelay, resolvedMinDuration);
|
|
175
|
+
const labelsResolved = useMemo(() => resolveLoadingLabels(locale, labels), [labels, locale]);
|
|
176
|
+
if (!present)
|
|
177
|
+
return null;
|
|
178
|
+
const layerProps = {
|
|
179
|
+
...props,
|
|
180
|
+
forwardedRef: ref,
|
|
181
|
+
labelsResolved,
|
|
182
|
+
mode,
|
|
183
|
+
present,
|
|
184
|
+
};
|
|
185
|
+
return mode === 'fullscreen'
|
|
186
|
+
? _jsx(PortalThemeProvider, { children: _jsx(FullscreenPortal, { ...layerProps }) })
|
|
187
|
+
: _jsx(LoadingLayer, { ...layerProps });
|
|
188
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface LoadingLabels {
|
|
2
|
+
loading: string;
|
|
3
|
+
error: string;
|
|
4
|
+
complete: string;
|
|
5
|
+
cancel: string;
|
|
6
|
+
retry: string;
|
|
7
|
+
continue: string;
|
|
8
|
+
previousTip: string;
|
|
9
|
+
nextTip: string;
|
|
10
|
+
pauseTips: string;
|
|
11
|
+
resumeTips: string;
|
|
12
|
+
stageProgress: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function resolveLoadingLabels(locale: string, overrides?: Partial<LoadingLabels>): LoadingLabels;
|
|
15
|
+
export declare function formatLoadingLabel(template: string, values: Record<string, string | number>): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { resolveInkraLocale } from '../i18n/index.js';
|
|
2
|
+
const loadingLabels = {
|
|
3
|
+
en: {
|
|
4
|
+
loading: 'Loading', error: 'Unable to continue', complete: 'Ready', cancel: 'Cancel', retry: 'Retry',
|
|
5
|
+
continue: 'Continue', previousTip: 'Previous tip', nextTip: 'Next tip', pauseTips: 'Pause tips',
|
|
6
|
+
resumeTips: 'Resume tips', stageProgress: 'Step {current} of {total}',
|
|
7
|
+
},
|
|
8
|
+
'zh-CN': {
|
|
9
|
+
loading: '正在加载', error: '暂时无法继续', complete: '准备就绪', cancel: '取消', retry: '重试',
|
|
10
|
+
continue: '继续', previousTip: '上一条提示', nextTip: '下一条提示', pauseTips: '暂停提示',
|
|
11
|
+
resumeTips: '继续提示', stageProgress: '第 {current} 步,共 {total} 步',
|
|
12
|
+
},
|
|
13
|
+
ja: {
|
|
14
|
+
loading: '読み込み中', error: '続行できません', complete: '準備完了', cancel: 'キャンセル', retry: '再試行',
|
|
15
|
+
continue: '続ける', previousTip: '前のヒント', nextTip: '次のヒント', pauseTips: 'ヒントを一時停止',
|
|
16
|
+
resumeTips: 'ヒントを再開', stageProgress: '{total} ステップ中 {current}',
|
|
17
|
+
},
|
|
18
|
+
ko: {
|
|
19
|
+
loading: '불러오는 중', error: '계속할 수 없습니다', complete: '준비 완료', cancel: '취소', retry: '다시 시도',
|
|
20
|
+
continue: '계속', previousTip: '이전 도움말', nextTip: '다음 도움말', pauseTips: '도움말 일시 정지',
|
|
21
|
+
resumeTips: '도움말 계속', stageProgress: '{total}단계 중 {current}단계',
|
|
22
|
+
},
|
|
23
|
+
fr: {
|
|
24
|
+
loading: 'Chargement', error: 'Impossible de continuer', complete: 'Prêt', cancel: 'Annuler', retry: 'Réessayer',
|
|
25
|
+
continue: 'Continuer', previousTip: 'Conseil précédent', nextTip: 'Conseil suivant', pauseTips: 'Suspendre les conseils',
|
|
26
|
+
resumeTips: 'Reprendre les conseils', stageProgress: 'Étape {current} sur {total}',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
export function resolveLoadingLabels(locale, overrides) {
|
|
30
|
+
return { ...loadingLabels[resolveInkraLocale(locale)], ...overrides };
|
|
31
|
+
}
|
|
32
|
+
export function formatLoadingLabel(template, values) {
|
|
33
|
+
return template.replace(/\{([A-Za-z][A-Za-z0-9]*)\}/g, (match, name) => Object.prototype.hasOwnProperty.call(values, name) ? String(values[name]) : match);
|
|
34
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type LoadingTipOrder = 'sequence' | 'random';
|
|
2
|
+
export declare function finiteDuration(value: number | undefined, fallback: number): number;
|
|
3
|
+
export declare function normalizeLoadingMax(max: number | undefined): number;
|
|
4
|
+
export declare function normalizeLoadingProgress(value: number | null | undefined, max: number): number | null | undefined;
|
|
5
|
+
export declare function normalizeTipIndex(index: number | undefined, length: number): number;
|
|
6
|
+
export declare function nextTipIndex(current: number, length: number, order: LoadingTipOrder, random?: () => number): number;
|