@inkra/react 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +316 -2
- package/THIRD_PARTY_NOTICES.md +39 -0
- package/Yozai-OFL.txt +94 -0
- package/dist/Button.d.ts +36 -0
- package/dist/Button.js +89 -0
- package/dist/PencilIcon.d.ts +22 -0
- package/dist/PencilIcon.js +210 -0
- package/dist/PencilLine.d.ts +10 -0
- package/dist/PencilLine.js +91 -0
- package/dist/PencilSurface.d.ts +19 -0
- package/dist/PencilSurface.js +19 -0
- package/dist/PencilText.d.ts +16 -0
- package/dist/PencilText.js +338 -0
- package/dist/accordion/Accordion.d.ts +27 -0
- package/dist/accordion/Accordion.js +40 -0
- package/dist/accordion/index.d.ts +2 -0
- package/dist/accordion/index.js +1 -0
- package/dist/alert-dialog/index.d.ts +64 -0
- package/dist/alert-dialog/index.js +109 -0
- package/dist/assets/ui-hand-yozai.woff2 +0 -0
- package/dist/audio-player/index.d.ts +60 -0
- package/dist/audio-player/index.js +101 -0
- package/dist/breadcrumb/index.d.ts +16 -0
- package/dist/breadcrumb/index.js +25 -0
- package/dist/calendar/index.d.ts +21 -0
- package/dist/calendar/index.js +41 -0
- package/dist/carousel/index.d.ts +51 -0
- package/dist/carousel/index.js +290 -0
- package/dist/choice-grid/index.d.ts +38 -0
- package/dist/choice-grid/index.js +85 -0
- package/dist/color-picker/color.d.ts +15 -0
- package/dist/color-picker/color.js +55 -0
- package/dist/color-picker/index.d.ts +31 -0
- package/dist/color-picker/index.js +69 -0
- package/dist/command/index.d.ts +23 -0
- package/dist/command/index.js +39 -0
- package/dist/content/index.d.ts +106 -0
- package/dist/content/index.js +80 -0
- package/dist/context-menu/index.d.ts +76 -0
- package/dist/context-menu/index.js +240 -0
- package/dist/data-table/index.d.ts +47 -0
- package/dist/data-table/index.js +91 -0
- package/dist/date-picker/index.d.ts +50 -0
- package/dist/date-picker/index.js +180 -0
- package/dist/day-agenda/index.d.ts +43 -0
- package/dist/day-agenda/index.js +47 -0
- package/dist/day-agenda/layout.d.ts +20 -0
- package/dist/day-agenda/layout.js +60 -0
- package/dist/display/ProgressRing.d.ts +6 -0
- package/dist/display/ProgressRing.js +146 -0
- package/dist/display/index.d.ts +59 -0
- package/dist/display/index.js +75 -0
- package/dist/dockable-panel/index.d.ts +41 -0
- package/dist/dockable-panel/index.js +253 -0
- package/dist/dockable-panel/model.d.ts +21 -0
- package/dist/dockable-panel/model.js +61 -0
- package/dist/drawer/index.d.ts +63 -0
- package/dist/drawer/index.js +86 -0
- package/dist/dropdown-menu/index.d.ts +34 -0
- package/dist/dropdown-menu/index.js +40 -0
- package/dist/editable/index.d.ts +53 -0
- package/dist/editable/index.js +162 -0
- package/dist/empty/index.d.ts +21 -0
- package/dist/empty/index.js +23 -0
- package/dist/flip/index.d.ts +49 -0
- package/dist/flip/index.js +138 -0
- package/dist/form/EditablePencilText.d.ts +13 -0
- package/dist/form/EditablePencilText.js +323 -0
- package/dist/form/Field.d.ts +13 -0
- package/dist/form/Field.js +26 -0
- package/dist/form/Input.d.ts +27 -0
- package/dist/form/Input.js +97 -0
- package/dist/form/InputAction.d.ts +7 -0
- package/dist/form/InputAction.js +24 -0
- package/dist/form/Textarea.d.ts +17 -0
- package/dist/form/Textarea.js +102 -0
- package/dist/form/context.d.ts +12 -0
- package/dist/form/context.js +10 -0
- package/dist/form/controlProps.d.ts +23 -0
- package/dist/form/controlProps.js +20 -0
- package/dist/form/index.d.ts +7 -0
- package/dist/form/index.js +3 -0
- package/dist/form/types.d.ts +6 -0
- package/dist/form/types.js +1 -0
- package/dist/heatmap/index.d.ts +59 -0
- package/dist/heatmap/index.js +248 -0
- package/dist/hotkey/index.d.ts +37 -0
- package/dist/hotkey/index.js +130 -0
- package/dist/hotkey/model.d.ts +27 -0
- package/dist/hotkey/model.js +97 -0
- package/dist/hover-card/index.d.ts +33 -0
- package/dist/hover-card/index.js +141 -0
- package/dist/image-cropper/index.d.ts +82 -0
- package/dist/image-cropper/index.js +261 -0
- package/dist/image-cropper/model.d.ts +42 -0
- package/dist/image-cropper/model.js +94 -0
- package/dist/image-viewer/index.d.ts +79 -0
- package/dist/image-viewer/index.js +220 -0
- package/dist/image-viewer/model.d.ts +9 -0
- package/dist/image-viewer/model.js +18 -0
- package/dist/images/layers-2x.png +0 -0
- package/dist/images/layers.png +0 -0
- package/dist/images/marker-icon.png +0 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +69 -0
- package/dist/input-number/index.d.ts +16 -0
- package/dist/input-number/index.js +77 -0
- package/dist/internal/InputActionContext.d.ts +1 -0
- package/dist/internal/InputActionContext.js +3 -0
- package/dist/internal/cache.d.ts +14 -0
- package/dist/internal/cache.js +54 -0
- package/dist/internal/line.d.ts +4 -0
- package/dist/internal/line.js +21 -0
- package/dist/internal/paper.d.ts +1 -0
- package/dist/internal/paper.js +65 -0
- package/dist/internal/random.d.ts +3 -0
- package/dist/internal/random.js +30 -0
- package/dist/internal/recovery.d.ts +1 -0
- package/dist/internal/recovery.js +52 -0
- package/dist/internal/renderTokens.d.ts +10 -0
- package/dist/internal/renderTokens.js +13 -0
- package/dist/internal/stroke.d.ts +24 -0
- package/dist/internal/stroke.js +144 -0
- package/dist/internal/surfaceRenderer.d.ts +41 -0
- package/dist/internal/surfaceRenderer.js +178 -0
- package/dist/internal/textQueue.d.ts +1 -0
- package/dist/internal/textQueue.js +37 -0
- package/dist/internal/textRenderer.d.ts +28 -0
- package/dist/internal/textRenderer.js +239 -0
- package/dist/internal/travelIcons.d.ts +24 -0
- package/dist/internal/travelIcons.js +14 -0
- package/dist/internal/useBrowserLayoutEffect.d.ts +2 -0
- package/dist/internal/useBrowserLayoutEffect.js +3 -0
- package/dist/internal/usePencilSurface.d.ts +3 -0
- package/dist/internal/usePencilSurface.js +116 -0
- package/dist/internal/useSelectionDrawing.d.ts +2 -0
- package/dist/internal/useSelectionDrawing.js +90 -0
- package/dist/internal/wash.d.ts +16 -0
- package/dist/internal/wash.js +149 -0
- package/dist/journey-map/MapScale.d.ts +4 -0
- package/dist/journey-map/MapScale.js +54 -0
- package/dist/journey-map/index.d.ts +39 -0
- package/dist/journey-map/index.js +197 -0
- package/dist/journey-map/terrain.d.ts +8 -0
- package/dist/journey-map/terrain.js +127 -0
- package/dist/keypad/index.d.ts +23 -0
- package/dist/keypad/index.js +63 -0
- package/dist/keypad/model.d.ts +20 -0
- package/dist/keypad/model.js +33 -0
- package/dist/layout/index.d.ts +63 -0
- package/dist/layout/index.js +62 -0
- package/dist/log/index.d.ts +45 -0
- package/dist/log/index.js +159 -0
- package/dist/log/model.d.ts +16 -0
- package/dist/log/model.js +13 -0
- package/dist/node-graph/index.d.ts +123 -0
- package/dist/node-graph/index.js +304 -0
- package/dist/number-ticker/index.d.ts +29 -0
- package/dist/number-ticker/index.js +132 -0
- package/dist/number-ticker/model.d.ts +8 -0
- package/dist/number-ticker/model.js +34 -0
- package/dist/overflow-list/index.d.ts +39 -0
- package/dist/overflow-list/index.js +230 -0
- package/dist/overflow-list/layout.d.ts +19 -0
- package/dist/overflow-list/layout.js +44 -0
- package/dist/overlay/Overlay.d.ts +65 -0
- package/dist/overlay/Overlay.js +77 -0
- package/dist/overlay/Popover.d.ts +43 -0
- package/dist/overlay/Popover.js +45 -0
- package/dist/overlay/index.d.ts +4 -0
- package/dist/overlay/index.js +2 -0
- package/dist/overlay/themeBridge.d.ts +8 -0
- package/dist/overlay/themeBridge.js +98 -0
- package/dist/pagination/index.d.ts +23 -0
- package/dist/pagination/index.js +41 -0
- package/dist/pagination/model.d.ts +4 -0
- package/dist/pagination/model.js +42 -0
- package/dist/pan-zoom/index.d.ts +71 -0
- package/dist/pan-zoom/index.js +175 -0
- package/dist/pencil.d.ts +10 -0
- package/dist/pencil.js +39 -0
- package/dist/radar/index.d.ts +54 -0
- package/dist/radar/index.js +220 -0
- package/dist/range-slider/index.d.ts +16 -0
- package/dist/range-slider/index.js +22 -0
- package/dist/rating/index.d.ts +28 -0
- package/dist/rating/index.js +127 -0
- package/dist/resizable/index.d.ts +32 -0
- package/dist/resizable/index.js +50 -0
- package/dist/scroll-area/index.d.ts +36 -0
- package/dist/scroll-area/index.js +44 -0
- package/dist/segmented-progress/SegmentedProgress.d.ts +17 -0
- package/dist/segmented-progress/SegmentedProgress.js +42 -0
- package/dist/select/MultipleSelect.d.ts +44 -0
- package/dist/select/MultipleSelect.js +124 -0
- package/dist/select/SearchableSelect.d.ts +45 -0
- package/dist/select/SearchableSelect.js +50 -0
- package/dist/select/Select.d.ts +109 -0
- package/dist/select/Select.js +205 -0
- package/dist/select/index.d.ts +4 -0
- package/dist/select/index.js +2 -0
- package/dist/selection/Checkbox.d.ts +9 -0
- package/dist/selection/Checkbox.js +37 -0
- package/dist/selection/RadioGroup.d.ts +23 -0
- package/dist/selection/RadioGroup.js +82 -0
- package/dist/selection/Switch.d.ts +8 -0
- package/dist/selection/Switch.js +36 -0
- package/dist/selection/index.d.ts +6 -0
- package/dist/selection/index.js +3 -0
- package/dist/selection/useNativeChecked.d.ts +13 -0
- package/dist/selection/useNativeChecked.js +27 -0
- package/dist/slider/index.d.ts +11 -0
- package/dist/slider/index.js +45 -0
- package/dist/slot/index.d.ts +24 -0
- package/dist/slot/index.js +29 -0
- package/dist/sortable/index.d.ts +50 -0
- package/dist/sortable/index.js +278 -0
- package/dist/sortable/model.d.ts +30 -0
- package/dist/sortable/model.js +64 -0
- package/dist/sparkline/index.d.ts +19 -0
- package/dist/sparkline/index.js +240 -0
- package/dist/sparkline/model.d.ts +16 -0
- package/dist/sparkline/model.js +28 -0
- package/dist/steps/index.d.ts +16 -0
- package/dist/steps/index.js +37 -0
- package/dist/styles.css +8226 -0
- package/dist/tabs/index.d.ts +22 -0
- package/dist/tabs/index.js +42 -0
- package/dist/tag-input/index.d.ts +69 -0
- package/dist/tag-input/index.js +325 -0
- package/dist/tag-input/model.d.ts +14 -0
- package/dist/tag-input/model.js +38 -0
- package/dist/time-picker/index.d.ts +15 -0
- package/dist/time-picker/index.js +168 -0
- package/dist/timeline/index.d.ts +14 -0
- package/dist/timeline/index.js +16 -0
- package/dist/toast/index.d.ts +27 -0
- package/dist/toast/index.js +36 -0
- package/dist/toggle-group/index.d.ts +17 -0
- package/dist/toggle-group/index.js +26 -0
- package/dist/tokens.css +356 -0
- package/dist/tooltip/Tooltip.d.ts +24 -0
- package/dist/tooltip/Tooltip.js +32 -0
- package/dist/tooltip/index.d.ts +2 -0
- package/dist/tooltip/index.js +1 -0
- package/dist/transfer/index.d.ts +64 -0
- package/dist/transfer/index.js +174 -0
- package/dist/transfer/model.d.ts +12 -0
- package/dist/transfer/model.js +33 -0
- package/dist/tree/index.d.ts +39 -0
- package/dist/tree/index.js +36 -0
- package/dist/tree-select/index.d.ts +59 -0
- package/dist/tree-select/index.js +142 -0
- package/dist/tree-select/model.d.ts +31 -0
- package/dist/tree-select/model.js +135 -0
- package/dist/typewriter/index.d.ts +21 -0
- package/dist/typewriter/index.js +101 -0
- package/dist/typewriter/model.d.ts +7 -0
- package/dist/typewriter/model.js +45 -0
- package/dist/upload/index.d.ts +59 -0
- package/dist/upload/index.js +230 -0
- package/dist/virtual-list/index.d.ts +44 -0
- package/dist/virtual-list/index.js +128 -0
- package/dist/virtual-list/model.d.ts +7 -0
- package/dist/virtual-list/model.js +27 -0
- package/package.json +119 -4
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function uniqueTransferItems(items) {
|
|
2
|
+
const seen = new Set();
|
|
3
|
+
return items.filter(item => {
|
|
4
|
+
if (seen.has(item.id))
|
|
5
|
+
return false;
|
|
6
|
+
seen.add(item.id);
|
|
7
|
+
return true;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export function normalizeTargetKeys(items, targetKeys) {
|
|
11
|
+
const uniqueItems = uniqueTransferItems(items);
|
|
12
|
+
const requested = new Set(targetKeys);
|
|
13
|
+
const knownIds = new Set(uniqueItems.map(item => item.id));
|
|
14
|
+
const orderedKnown = uniqueItems.filter(item => requested.has(item.id)).map(item => item.id);
|
|
15
|
+
const unknown = targetKeys.filter((key, index) => !knownIds.has(key) && targetKeys.indexOf(key) === index);
|
|
16
|
+
return [...orderedKnown, ...unknown];
|
|
17
|
+
}
|
|
18
|
+
export function splitTransferItems(items, targetKeys) {
|
|
19
|
+
const target = new Set(targetKeys);
|
|
20
|
+
const uniqueItems = uniqueTransferItems(items);
|
|
21
|
+
return {
|
|
22
|
+
source: uniqueItems.filter(item => !target.has(item.id)),
|
|
23
|
+
target: uniqueItems.filter(item => target.has(item.id)),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function moveTransferKeys(items, targetKeys, movedKeys, direction) {
|
|
27
|
+
const next = new Set(targetKeys);
|
|
28
|
+
if (direction === 'to-target')
|
|
29
|
+
movedKeys.forEach(key => next.add(key));
|
|
30
|
+
else
|
|
31
|
+
movedKeys.forEach(key => next.delete(key));
|
|
32
|
+
return normalizeTargetKeys(items, [...next]);
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Key, Selection, TreeItemProps as AriaTreeItemProps, TreeProps as AriaTreeProps } from 'react-aria-components/Tree';
|
|
2
|
+
import type { CSSProperties, HTMLAttributes, ReactElement, ReactNode, RefAttributes } from 'react';
|
|
3
|
+
export type TreeKey = Key;
|
|
4
|
+
export type TreeSelection = Selection;
|
|
5
|
+
export type TreeRootProps<T extends object = object> = Omit<AriaTreeProps<T>, 'className' | 'style' | 'renderEmptyState'> & {
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: CSSProperties;
|
|
8
|
+
emptyState?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
export type TreeItemProps<T = object> = Omit<AriaTreeItemProps<T>, 'className'> & {
|
|
11
|
+
className?: string;
|
|
12
|
+
};
|
|
13
|
+
export interface TreeItemContentState {
|
|
14
|
+
isExpanded: boolean;
|
|
15
|
+
hasChildItems: boolean;
|
|
16
|
+
isSelected: boolean;
|
|
17
|
+
isDisabled: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface TreeItemContentProps {
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
className?: string;
|
|
22
|
+
icon?: ReactNode | ((state: TreeItemContentState) => ReactNode);
|
|
23
|
+
disclosureIcon?: ReactNode | ((state: TreeItemContentState) => ReactNode);
|
|
24
|
+
expandLabel?: string;
|
|
25
|
+
collapseLabel?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface TreeItemTitleProps extends HTMLAttributes<HTMLSpanElement> {
|
|
28
|
+
}
|
|
29
|
+
export interface TreeItemDescriptionProps extends HTMLAttributes<HTMLSpanElement> {
|
|
30
|
+
}
|
|
31
|
+
export declare const Tree: {
|
|
32
|
+
Root: <T extends object = object>(props: TreeRootProps<T> & RefAttributes<HTMLDivElement>) => ReactElement | null;
|
|
33
|
+
Item: import("react").ForwardRefExoticComponent<Omit<AriaTreeItemProps<object>, "className"> & {
|
|
34
|
+
className?: string;
|
|
35
|
+
} & RefAttributes<HTMLDivElement>>;
|
|
36
|
+
ItemContent: import("react").ForwardRefExoticComponent<TreeItemContentProps & RefAttributes<Element>>;
|
|
37
|
+
ItemTitle: import("react").ForwardRefExoticComponent<TreeItemTitleProps & RefAttributes<HTMLSpanElement>>;
|
|
38
|
+
ItemDescription: import("react").ForwardRefExoticComponent<TreeItemDescriptionProps & RefAttributes<HTMLSpanElement>>;
|
|
39
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Tree as AriaTree, TreeItem as AriaTreeItem, TreeItemContent as AriaTreeItemContent, } from 'react-aria-components/Tree';
|
|
3
|
+
import { Button as AriaButton } from 'react-aria-components/Button';
|
|
4
|
+
import { forwardRef } from 'react';
|
|
5
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
6
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
7
|
+
import { PencilText } from '../PencilText.js';
|
|
8
|
+
const chevronIcon = [{ d: 'M 7 10 L 12 15 L 17 10' }];
|
|
9
|
+
function TreeRootInner({ className, emptyState = 'No items', selectionMode = 'single', selectionBehavior = 'toggle', ...props }, ref) {
|
|
10
|
+
return _jsx(AriaTree, { ...props, ref: ref, className: ['inkra-tree', className].filter(Boolean).join(' '), selectionMode: selectionMode, selectionBehavior: selectionBehavior, renderEmptyState: () => _jsx("div", { className: "inkra-tree__empty", children: emptyState }) });
|
|
11
|
+
}
|
|
12
|
+
const TreeRoot = forwardRef(TreeRootInner);
|
|
13
|
+
const TreeItem = forwardRef(function TreeItem({ className, ...props }, ref) {
|
|
14
|
+
return _jsx(AriaTreeItem, { ...props, ref: ref, className: ['inkra-tree__item', className].filter(Boolean).join(' ') });
|
|
15
|
+
});
|
|
16
|
+
const TreeItemContent = forwardRef(function TreeItemContent({ children, className, icon, disclosureIcon, expandLabel = 'Expand item', collapseLabel = 'Collapse item', }, ref) {
|
|
17
|
+
return _jsx(AriaTreeItemContent, { ref: ref, children: ({ hasChildItems, isExpanded, isSelected, isFocusVisible, isDisabled, level, id, }) => {
|
|
18
|
+
const state = { isExpanded, hasChildItems, isSelected, isDisabled };
|
|
19
|
+
const resolvedIcon = typeof icon === 'function' ? icon(state) : icon;
|
|
20
|
+
const resolvedDisclosure = typeof disclosureIcon === 'function' ? disclosureIcon(state) : disclosureIcon;
|
|
21
|
+
return _jsxs("div", { className: ['inkra-tree__item-content', className].filter(Boolean).join(' '), "data-expanded": isExpanded || undefined, "data-selected": isSelected || undefined, "data-disabled": isDisabled || undefined, "data-has-icon": resolvedIcon != null || undefined, style: { '--inkra-tree-level': level }, children: [isSelected && _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-tree__selection", variant: "selection", seed: `tree-${String(id)}-selection` }), isFocusVisible && _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-tree__focus", variant: "outline", seed: `tree-${String(id)}-focus` }), hasChildItems ? _jsx(AriaButton, { slot: "chevron", className: "inkra-tree__disclosure", "aria-label": isExpanded ? collapseLabel : expandLabel, children: ({ isFocusVisible: disclosureFocused }) => _jsxs(_Fragment, { children: [disclosureFocused && _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-tree__disclosure-focus", variant: "outline" }), _jsx("span", { className: "inkra-tree__disclosure-icon", "aria-hidden": "true", children: disclosureIcon === undefined ? _jsx(PencilIcon, { className: "inkra-tree__chevron", paths: chevronIcon, size: 18, sourceSize: 24 }) : resolvedDisclosure })] }) }) : _jsx("span", { className: "inkra-tree__disclosure-spacer", "aria-hidden": "true" }), resolvedIcon != null && _jsx("span", { className: "inkra-tree__icon", "aria-hidden": "true", children: resolvedIcon }), _jsx("span", { className: "inkra-tree__copy", children: children })] });
|
|
22
|
+
} });
|
|
23
|
+
});
|
|
24
|
+
const TreeItemTitle = forwardRef(function TreeItemTitle({ className, children, ...props }, ref) {
|
|
25
|
+
return _jsx("span", { ...props, ref: ref, className: ['inkra-tree__title', className].filter(Boolean).join(' '), children: typeof children === 'string' ? _jsx(PencilText, { children: children }) : children });
|
|
26
|
+
});
|
|
27
|
+
const TreeItemDescription = forwardRef(function TreeItemDescription({ className, children, ...props }, ref) {
|
|
28
|
+
return _jsx("span", { ...props, ref: ref, className: ['inkra-tree__description', className].filter(Boolean).join(' '), children: children });
|
|
29
|
+
});
|
|
30
|
+
export const Tree = {
|
|
31
|
+
Root: TreeRoot,
|
|
32
|
+
Item: TreeItem,
|
|
33
|
+
ItemContent: TreeItemContent,
|
|
34
|
+
ItemTitle: TreeItemTitle,
|
|
35
|
+
ItemDescription: TreeItemDescription,
|
|
36
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import type { TreeSelectBranchSelection, TreeSelectNode, TreeSelectSelectionPropagation } from './model.js';
|
|
3
|
+
export type { TreeSelectBranchSelection, TreeSelectNode, TreeSelectSelectionPropagation, } from './model.js';
|
|
4
|
+
export interface TreeSelectRenderLabelState {
|
|
5
|
+
branch: boolean;
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
selected: boolean;
|
|
8
|
+
query: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TreeSelectChangeDetails {
|
|
11
|
+
id?: string;
|
|
12
|
+
node?: TreeSelectNode;
|
|
13
|
+
reason: 'select' | 'clear';
|
|
14
|
+
selected?: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface TreeSelectCommonProps extends Omit<HTMLAttributes<HTMLDivElement>, 'defaultValue' | 'onChange' | 'children'> {
|
|
17
|
+
items: readonly TreeSelectNode[];
|
|
18
|
+
branchSelection?: TreeSelectBranchSelection;
|
|
19
|
+
selectionPropagation?: TreeSelectSelectionPropagation;
|
|
20
|
+
renderLabel?: (node: TreeSelectNode, state: TreeSelectRenderLabelState) => ReactNode;
|
|
21
|
+
renderValue?: (nodes: readonly TreeSelectNode[], ids: readonly string[]) => ReactNode;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
searchPlaceholder?: string;
|
|
24
|
+
emptyContent?: ReactNode;
|
|
25
|
+
clearable?: boolean;
|
|
26
|
+
clearLabel?: string;
|
|
27
|
+
treeLabel?: string;
|
|
28
|
+
expandLabel?: string;
|
|
29
|
+
collapseLabel?: string;
|
|
30
|
+
size?: 'sm' | 'md' | 'lg';
|
|
31
|
+
name?: string;
|
|
32
|
+
form?: string;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
readOnly?: boolean;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
invalid?: boolean;
|
|
37
|
+
open?: boolean;
|
|
38
|
+
defaultOpen?: boolean;
|
|
39
|
+
onOpenChange?: (open: boolean) => void;
|
|
40
|
+
expandedIds?: readonly string[];
|
|
41
|
+
defaultExpandedIds?: readonly string[];
|
|
42
|
+
onExpandedIdsChange?: (ids: string[]) => void;
|
|
43
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
44
|
+
align?: 'start' | 'center' | 'end';
|
|
45
|
+
sideOffset?: number;
|
|
46
|
+
collisionPadding?: number;
|
|
47
|
+
}
|
|
48
|
+
export type TreeSelectProps = TreeSelectCommonProps & ({
|
|
49
|
+
multiple?: false;
|
|
50
|
+
value?: string | null;
|
|
51
|
+
defaultValue?: string | null;
|
|
52
|
+
onValueChange?: (value: string | null, details: TreeSelectChangeDetails) => void;
|
|
53
|
+
} | {
|
|
54
|
+
multiple: true;
|
|
55
|
+
value?: readonly string[];
|
|
56
|
+
defaultValue?: readonly string[];
|
|
57
|
+
onValueChange?: (value: string[], details: TreeSelectChangeDetails) => void;
|
|
58
|
+
});
|
|
59
|
+
export declare const TreeSelect: import("react").ForwardRefExoticComponent<TreeSelectProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { Button } from '../Button.js';
|
|
4
|
+
import { Input } from '../form/Input.js';
|
|
5
|
+
import { mergeAriaIds, useFieldContext } from '../form/context.js';
|
|
6
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
7
|
+
import { PencilText } from '../PencilText.js';
|
|
8
|
+
import { Popover } from '../overlay/index.js';
|
|
9
|
+
import { Tree } from '../tree/index.js';
|
|
10
|
+
import { filterTreeSelectNodes, indexTreeSelectNodes, updateTreeSelectSelection, } from './model.js';
|
|
11
|
+
const chevronPaths = [{ d: 'M6 9l6 6 6-6' }];
|
|
12
|
+
const clearPaths = [{ d: 'M6 6l12 12M18 6L6 18' }];
|
|
13
|
+
const searchPaths = [{ d: 'M21 21l-4.5-4.5' }, { d: 'M18 10a8 8 0 1 1-16 0a8 8 0 1 1 16 0' }];
|
|
14
|
+
function selectionFromProps(props, fallback = []) {
|
|
15
|
+
const raw = props.value !== undefined ? props.value : props.defaultValue;
|
|
16
|
+
if (props.multiple)
|
|
17
|
+
return Array.isArray(raw) ? [...new Set(raw)] : [...fallback];
|
|
18
|
+
return typeof raw === 'string' && raw ? [raw] : raw === null ? [] : [...fallback];
|
|
19
|
+
}
|
|
20
|
+
function keys(selection) {
|
|
21
|
+
return selection === 'all' ? [] : [...selection].map(String);
|
|
22
|
+
}
|
|
23
|
+
export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
24
|
+
const { items, branchSelection = 'leaf', selectionPropagation = 'none', renderLabel, renderValue, placeholder = 'Select an item', searchPlaceholder = 'Search tree', emptyContent = 'No matching items', clearable = true, clearLabel = 'Clear selection', treeLabel = 'Tree options', expandLabel = 'Expand item', collapseLabel = 'Collapse item', size = 'md', name, form, disabled, readOnly, required, invalid, open: controlledOpen, defaultOpen = false, onOpenChange, expandedIds, defaultExpandedIds = [], onExpandedIdsChange, side = 'bottom', align = 'start', sideOffset = 8, collisionPadding = 8, id, className, multiple: _multiple, value: _value, defaultValue: _defaultValue, onValueChange: _onValueChange, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, ...rest } = props;
|
|
25
|
+
const field = useFieldContext();
|
|
26
|
+
const generatedId = useId();
|
|
27
|
+
const controlId = field?.controlId ?? id ?? `inkra-tree-select-${generatedId}`;
|
|
28
|
+
const resolvedDisabled = Boolean(disabled || field?.disabled);
|
|
29
|
+
const resolvedReadOnly = Boolean(readOnly || field?.readOnly);
|
|
30
|
+
const resolvedRequired = Boolean(required || field?.required);
|
|
31
|
+
const resolvedInvalid = Boolean(invalid || field?.invalid);
|
|
32
|
+
const describedBy = mergeAriaIds(ariaDescribedBy, field?.descriptionId, field?.errorId);
|
|
33
|
+
const normalized = useMemo(() => indexTreeSelectNodes(items), [items]);
|
|
34
|
+
const defaultSelection = useRef(selectionFromProps(props));
|
|
35
|
+
const controlledSelection = props.value !== undefined;
|
|
36
|
+
const [uncontrolledSelection, setUncontrolledSelection] = useState(defaultSelection.current);
|
|
37
|
+
const selectedIds = controlledSelection ? selectionFromProps(props) : uncontrolledSelection;
|
|
38
|
+
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
39
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
40
|
+
const isOpen = controlledOpen ?? uncontrolledOpen;
|
|
41
|
+
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(() => [...new Set(defaultExpandedIds)]);
|
|
42
|
+
const baseExpanded = expandedIds ?? uncontrolledExpanded;
|
|
43
|
+
const [query, setQuery] = useState('');
|
|
44
|
+
const filtered = useMemo(() => filterTreeSelectNodes(normalized.nodes, query), [normalized.nodes, query]);
|
|
45
|
+
const effectiveExpanded = useMemo(() => new Set([...baseExpanded, ...filtered.expandedIds]), [baseExpanded, filtered.expandedIds]);
|
|
46
|
+
const triggerRef = useRef(null);
|
|
47
|
+
const searchRef = useRef(null);
|
|
48
|
+
const treeRef = useRef(null);
|
|
49
|
+
const formAnchorRef = useRef(null);
|
|
50
|
+
const selectedNodes = selectedIds
|
|
51
|
+
.map(selectedId => normalized.byId.get(selectedId))
|
|
52
|
+
.filter((node) => node !== undefined);
|
|
53
|
+
const setOpen = (next) => {
|
|
54
|
+
if (resolvedDisabled || resolvedReadOnly)
|
|
55
|
+
return;
|
|
56
|
+
if (controlledOpen === undefined)
|
|
57
|
+
setUncontrolledOpen(next);
|
|
58
|
+
if (!next)
|
|
59
|
+
setQuery('');
|
|
60
|
+
onOpenChange?.(next);
|
|
61
|
+
};
|
|
62
|
+
const emitSelection = (next, details) => {
|
|
63
|
+
if (!controlledSelection)
|
|
64
|
+
setUncontrolledSelection(next);
|
|
65
|
+
if (props.multiple)
|
|
66
|
+
props.onValueChange?.(next, details);
|
|
67
|
+
else
|
|
68
|
+
props.onValueChange?.(next[0] ?? null, details);
|
|
69
|
+
};
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (controlledSelection)
|
|
72
|
+
return;
|
|
73
|
+
const currentForm = formAnchorRef.current?.form;
|
|
74
|
+
if (!currentForm)
|
|
75
|
+
return;
|
|
76
|
+
const reset = () => {
|
|
77
|
+
setUncontrolledSelection(defaultSelection.current);
|
|
78
|
+
setQuery('');
|
|
79
|
+
if (controlledOpen === undefined)
|
|
80
|
+
setUncontrolledOpen(false);
|
|
81
|
+
};
|
|
82
|
+
currentForm.addEventListener('reset', reset);
|
|
83
|
+
return () => currentForm.removeEventListener('reset', reset);
|
|
84
|
+
}, [controlledOpen, controlledSelection]);
|
|
85
|
+
const handleSelectionChange = (nextSelection) => {
|
|
86
|
+
if (resolvedDisabled || resolvedReadOnly)
|
|
87
|
+
return;
|
|
88
|
+
const nextKeys = new Set(keys(nextSelection));
|
|
89
|
+
const added = [...nextKeys].find(item => !selectedSet.has(item));
|
|
90
|
+
const removed = selectedIds.find(item => filtered.visibleIds.has(item) && !nextKeys.has(item));
|
|
91
|
+
const toggledId = added ?? removed;
|
|
92
|
+
if (!toggledId)
|
|
93
|
+
return;
|
|
94
|
+
const selected = added === toggledId;
|
|
95
|
+
const next = updateTreeSelectSelection(normalized.nodes, selectedIds, toggledId, selected, {
|
|
96
|
+
multiple: Boolean(props.multiple),
|
|
97
|
+
branchSelection,
|
|
98
|
+
selectionPropagation,
|
|
99
|
+
});
|
|
100
|
+
if (next.length === selectedIds.length && next.every((item, index) => item === selectedIds[index]))
|
|
101
|
+
return;
|
|
102
|
+
emitSelection(next, {
|
|
103
|
+
id: toggledId,
|
|
104
|
+
node: normalized.byId.get(toggledId),
|
|
105
|
+
reason: 'select',
|
|
106
|
+
selected,
|
|
107
|
+
});
|
|
108
|
+
if (!props.multiple && selected)
|
|
109
|
+
setOpen(false);
|
|
110
|
+
};
|
|
111
|
+
const handleExpandedChange = (nextKeys) => {
|
|
112
|
+
const next = [...nextKeys].map(String).filter(item => !filtered.expandedIds.has(item) || baseExpanded.includes(item));
|
|
113
|
+
if (expandedIds === undefined)
|
|
114
|
+
setUncontrolledExpanded(next);
|
|
115
|
+
onExpandedIdsChange?.(next);
|
|
116
|
+
};
|
|
117
|
+
const handleSearchKeyDown = (event) => {
|
|
118
|
+
if (event.key !== 'ArrowDown' || filtered.nodes.length === 0)
|
|
119
|
+
return;
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
treeRef.current?.focus();
|
|
122
|
+
};
|
|
123
|
+
const renderNodes = (nodes) => nodes.map(node => {
|
|
124
|
+
const branch = Boolean(node.children?.length);
|
|
125
|
+
const label = renderLabel?.(node, {
|
|
126
|
+
branch,
|
|
127
|
+
disabled: Boolean(node.disabled),
|
|
128
|
+
selected: selectedSet.has(node.id),
|
|
129
|
+
query,
|
|
130
|
+
}) ?? node.label;
|
|
131
|
+
return _jsxs(Tree.Item, { className: "inkra-tree-select__item", id: node.id, isDisabled: node.disabled, textValue: node.label, children: [_jsxs(Tree.ItemContent, { collapseLabel: collapseLabel, expandLabel: expandLabel, children: [_jsx(Tree.ItemTitle, { children: label }), node.description ? _jsx(Tree.ItemDescription, { children: node.description }) : null] }), node.children?.length ? renderNodes(node.children) : null] }, node.id);
|
|
132
|
+
});
|
|
133
|
+
const fallbackValue = selectedIds.map(selectedId => normalized.byId.get(selectedId)?.label ?? selectedId).join(', ');
|
|
134
|
+
const valueContent = renderValue?.(selectedNodes, selectedIds)
|
|
135
|
+
?? (fallbackValue ? _jsx(PencilText, { ellipsis: true, children: fallbackValue }) : _jsx(PencilText, { ellipsis: true, children: placeholder }));
|
|
136
|
+
const indicatorSize = size === 'sm' ? 24 : size === 'lg' ? 32 : 28;
|
|
137
|
+
const exposedOpen = resolvedDisabled || resolvedReadOnly ? false : isOpen;
|
|
138
|
+
return _jsxs("div", { ...rest, className: ['inkra-tree-select', className].filter(Boolean).join(' '), "data-disabled": resolvedDisabled || undefined, "data-invalid": resolvedInvalid || undefined, "data-readonly": resolvedReadOnly || undefined, "data-size": size, ref: ref, children: [_jsxs(Popover.Root, { open: exposedOpen, onOpenChange: setOpen, children: [_jsxs("div", { className: "inkra-tree-select__control", children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { "aria-describedby": describedBy, "aria-invalid": resolvedInvalid || undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-readonly": resolvedReadOnly || undefined, "aria-required": resolvedRequired || undefined, className: "inkra-tree-select__trigger", disabled: resolvedDisabled, endIcon: _jsx(PencilIcon, { paths: chevronPaths, size: indicatorSize, sourceSize: 24 }), fullWidth: true, id: controlId, ref: triggerRef, size: size, variant: "outline", children: _jsx("span", { className: "inkra-tree-select__value", "data-placeholder": !selectedIds.length || undefined, children: valueContent }) }) }), clearable ? _jsx(Button, { "aria-label": clearLabel, className: "inkra-tree-select__clear", "data-visible": selectedIds.length > 0 || undefined, disabled: resolvedDisabled || resolvedReadOnly || selectedIds.length === 0, onClick: () => emitSelection([], { reason: 'clear' }), size: size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon', tabIndex: selectedIds.length ? undefined : -1, variant: "ghost", children: _jsx(PencilIcon, { paths: clearPaths, size: 24, sourceSize: 24 }) }) : null] }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { align: align, "aria-label": treeLabel, className: "inkra-tree-select__popover", collisionPadding: collisionPadding, onOpenAutoFocus: (event) => {
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
searchRef.current?.focus();
|
|
141
|
+
}, side: side, sideOffset: sideOffset, children: [_jsx("div", { className: "inkra-tree-select__search", children: _jsx(Input, { "aria-label": searchPlaceholder, clearable: true, clearButtonLabel: clearLabel, onKeyDown: handleSearchKeyDown, onValueChange: setQuery, placeholder: searchPlaceholder, ref: searchRef, size: size, startIcon: _jsx(PencilIcon, { paths: searchPaths, size: 22, sourceSize: 24 }), type: "search", value: query, variant: "underline" }) }), _jsx(Tree.Root, { "aria-label": treeLabel, className: "inkra-tree-select__tree", emptyState: emptyContent, expandedKeys: effectiveExpanded, onExpandedChange: handleExpandedChange, onSelectionChange: handleSelectionChange, ref: treeRef, selectedKeys: new Set(selectedIds), selectionMode: props.multiple ? 'multiple' : 'single', children: renderNodes(filtered.nodes) })] }) })] }), _jsx("input", { "aria-hidden": "true", className: "inkra-tree-select__form-anchor", form: form, ref: formAnchorRef, tabIndex: -1, type: "hidden" }), name && !resolvedDisabled ? selectedIds.map(selectedId => _jsx("input", { form: form, name: name, type: "hidden", value: selectedId }, selectedId)) : null] });
|
|
142
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface TreeSelectNode {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
keywords?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
children?: readonly TreeSelectNode[];
|
|
8
|
+
}
|
|
9
|
+
export type TreeSelectBranchSelection = 'leaf' | 'all';
|
|
10
|
+
export type TreeSelectSelectionPropagation = 'none' | 'down' | 'both';
|
|
11
|
+
export interface TreeSelectIndex {
|
|
12
|
+
nodes: readonly TreeSelectNode[];
|
|
13
|
+
orderedIds: readonly string[];
|
|
14
|
+
byId: ReadonlyMap<string, TreeSelectNode>;
|
|
15
|
+
parentById: ReadonlyMap<string, string | null>;
|
|
16
|
+
branchIds: ReadonlySet<string>;
|
|
17
|
+
disabledIds: ReadonlySet<string>;
|
|
18
|
+
}
|
|
19
|
+
export interface FilteredTreeSelectNodes {
|
|
20
|
+
nodes: readonly TreeSelectNode[];
|
|
21
|
+
visibleIds: ReadonlySet<string>;
|
|
22
|
+
expandedIds: ReadonlySet<string>;
|
|
23
|
+
}
|
|
24
|
+
export declare function normalizeTreeSelectNodes(nodes: readonly TreeSelectNode[]): readonly TreeSelectNode[];
|
|
25
|
+
export declare function indexTreeSelectNodes(nodes: readonly TreeSelectNode[]): TreeSelectIndex;
|
|
26
|
+
export declare function filterTreeSelectNodes(nodes: readonly TreeSelectNode[], query: string): FilteredTreeSelectNodes;
|
|
27
|
+
export declare function updateTreeSelectSelection(nodes: readonly TreeSelectNode[], current: readonly string[], toggledId: string, selected: boolean, options: {
|
|
28
|
+
multiple: boolean;
|
|
29
|
+
branchSelection: TreeSelectBranchSelection;
|
|
30
|
+
selectionPropagation: TreeSelectSelectionPropagation;
|
|
31
|
+
}): string[];
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
function normalizeCopy(nodes, seen) {
|
|
2
|
+
const result = [];
|
|
3
|
+
for (const node of nodes) {
|
|
4
|
+
if (!node.id || seen.has(node.id))
|
|
5
|
+
continue;
|
|
6
|
+
seen.add(node.id);
|
|
7
|
+
const children = normalizeCopy(node.children ?? [], seen);
|
|
8
|
+
result.push({ ...node, children: children.length ? children : undefined });
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
export function normalizeTreeSelectNodes(nodes) {
|
|
13
|
+
return normalizeCopy(nodes, new Set());
|
|
14
|
+
}
|
|
15
|
+
export function indexTreeSelectNodes(nodes) {
|
|
16
|
+
const normalized = normalizeTreeSelectNodes(nodes);
|
|
17
|
+
const orderedIds = [];
|
|
18
|
+
const byId = new Map();
|
|
19
|
+
const parentById = new Map();
|
|
20
|
+
const branchIds = new Set();
|
|
21
|
+
const disabledIds = new Set();
|
|
22
|
+
const visit = (items, parentId) => {
|
|
23
|
+
for (const node of items) {
|
|
24
|
+
orderedIds.push(node.id);
|
|
25
|
+
byId.set(node.id, node);
|
|
26
|
+
parentById.set(node.id, parentId);
|
|
27
|
+
if (node.disabled)
|
|
28
|
+
disabledIds.add(node.id);
|
|
29
|
+
if (node.children?.length) {
|
|
30
|
+
branchIds.add(node.id);
|
|
31
|
+
visit(node.children, node.id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
visit(normalized, null);
|
|
36
|
+
return { nodes: normalized, orderedIds, byId, parentById, branchIds, disabledIds };
|
|
37
|
+
}
|
|
38
|
+
function addVisibleSubtree(node, visibleIds, expandedIds) {
|
|
39
|
+
visibleIds.add(node.id);
|
|
40
|
+
if (node.children?.length) {
|
|
41
|
+
expandedIds.add(node.id);
|
|
42
|
+
for (const child of node.children)
|
|
43
|
+
addVisibleSubtree(child, visibleIds, expandedIds);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function filterTreeSelectNodes(nodes, query) {
|
|
47
|
+
const normalized = normalizeTreeSelectNodes(nodes);
|
|
48
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
49
|
+
const visibleIds = new Set();
|
|
50
|
+
const expandedIds = new Set();
|
|
51
|
+
if (!needle) {
|
|
52
|
+
for (const node of normalized)
|
|
53
|
+
addVisibleSubtree(node, visibleIds, new Set());
|
|
54
|
+
return { nodes: normalized, visibleIds, expandedIds };
|
|
55
|
+
}
|
|
56
|
+
const visit = (node) => {
|
|
57
|
+
const haystack = `${node.label} ${node.keywords ?? ''}`.toLocaleLowerCase();
|
|
58
|
+
if (haystack.includes(needle)) {
|
|
59
|
+
addVisibleSubtree(node, visibleIds, expandedIds);
|
|
60
|
+
return node;
|
|
61
|
+
}
|
|
62
|
+
const children = (node.children ?? []).map(visit).filter((child) => child !== null);
|
|
63
|
+
if (!children.length)
|
|
64
|
+
return null;
|
|
65
|
+
visibleIds.add(node.id);
|
|
66
|
+
expandedIds.add(node.id);
|
|
67
|
+
return { ...node, children };
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
nodes: normalized.map(visit).filter((node) => node !== null),
|
|
71
|
+
visibleIds,
|
|
72
|
+
expandedIds,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function isSelectable(index, id, branchSelection) {
|
|
76
|
+
return index.byId.has(id)
|
|
77
|
+
&& !index.disabledIds.has(id)
|
|
78
|
+
&& (branchSelection === 'all' || !index.branchIds.has(id));
|
|
79
|
+
}
|
|
80
|
+
function descendants(index, id) {
|
|
81
|
+
const node = index.byId.get(id);
|
|
82
|
+
if (!node?.children?.length)
|
|
83
|
+
return [];
|
|
84
|
+
const result = [];
|
|
85
|
+
const visit = (items) => {
|
|
86
|
+
for (const child of items) {
|
|
87
|
+
result.push(child.id);
|
|
88
|
+
if (child.children?.length)
|
|
89
|
+
visit(child.children);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
visit(node.children);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
function orderedSelection(index, selected, previous) {
|
|
96
|
+
const known = index.orderedIds.filter(id => selected.has(id));
|
|
97
|
+
const unknown = previous.filter((id, position) => !index.byId.has(id) && previous.indexOf(id) === position);
|
|
98
|
+
return [...known, ...unknown];
|
|
99
|
+
}
|
|
100
|
+
export function updateTreeSelectSelection(nodes, current, toggledId, selected, options) {
|
|
101
|
+
const index = indexTreeSelectNodes(nodes);
|
|
102
|
+
if (!isSelectable(index, toggledId, options.branchSelection))
|
|
103
|
+
return [...current];
|
|
104
|
+
if (!options.multiple)
|
|
105
|
+
return selected ? [toggledId] : [];
|
|
106
|
+
const next = new Set(current);
|
|
107
|
+
const affected = [toggledId];
|
|
108
|
+
if (options.selectionPropagation !== 'none' && index.branchIds.has(toggledId)) {
|
|
109
|
+
affected.push(...descendants(index, toggledId));
|
|
110
|
+
}
|
|
111
|
+
for (const id of affected) {
|
|
112
|
+
if (!isSelectable(index, id, options.branchSelection))
|
|
113
|
+
continue;
|
|
114
|
+
if (selected)
|
|
115
|
+
next.add(id);
|
|
116
|
+
else
|
|
117
|
+
next.delete(id);
|
|
118
|
+
}
|
|
119
|
+
if (options.selectionPropagation === 'both' && options.branchSelection === 'all') {
|
|
120
|
+
const branches = index.orderedIds.filter(id => index.branchIds.has(id)).reverse();
|
|
121
|
+
for (const branchId of branches) {
|
|
122
|
+
if (index.disabledIds.has(branchId))
|
|
123
|
+
continue;
|
|
124
|
+
const selectableDescendants = descendants(index, branchId)
|
|
125
|
+
.filter(id => isSelectable(index, id, options.branchSelection));
|
|
126
|
+
if (!selectableDescendants.length)
|
|
127
|
+
continue;
|
|
128
|
+
if (selectableDescendants.every(id => next.has(id)))
|
|
129
|
+
next.add(branchId);
|
|
130
|
+
else
|
|
131
|
+
next.delete(branchId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return orderedSelection(index, next, current);
|
|
135
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
export interface TypewriterHandle {
|
|
3
|
+
pause: () => void;
|
|
4
|
+
resume: () => void;
|
|
5
|
+
complete: () => void;
|
|
6
|
+
restart: () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface TypewriterProps extends Omit<ComponentPropsWithoutRef<'span'>, 'children'> {
|
|
9
|
+
children: string;
|
|
10
|
+
speed?: number;
|
|
11
|
+
punctuationDelay?: number;
|
|
12
|
+
startDelay?: number;
|
|
13
|
+
paused?: boolean;
|
|
14
|
+
defaultPaused?: boolean;
|
|
15
|
+
locale?: string | string[];
|
|
16
|
+
onPausedChange?: (paused: boolean) => void;
|
|
17
|
+
onComplete?: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare const Typewriter: import("react").ForwardRefExoticComponent<TypewriterProps & import("react").RefAttributes<TypewriterHandle>>;
|
|
20
|
+
export { isTypewriterPunctuation, splitTypewriterGraphemes, typewriterDelayAfter, } from './model.js';
|
|
21
|
+
export type { TypewriterDelayOptions } from './model.js';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { PencilText } from '../PencilText.js';
|
|
4
|
+
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
5
|
+
import { splitTypewriterGraphemes, typewriterDelayAfter } from './model.js';
|
|
6
|
+
function usePrefersReducedMotion() {
|
|
7
|
+
const [reduced, setReduced] = useState(false);
|
|
8
|
+
useBrowserLayoutEffect(() => {
|
|
9
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
|
|
10
|
+
return undefined;
|
|
11
|
+
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
12
|
+
const update = () => setReduced(query.matches);
|
|
13
|
+
update();
|
|
14
|
+
query.addEventListener('change', update);
|
|
15
|
+
return () => query.removeEventListener('change', update);
|
|
16
|
+
}, []);
|
|
17
|
+
return reduced;
|
|
18
|
+
}
|
|
19
|
+
function safeDuration(value) {
|
|
20
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
21
|
+
}
|
|
22
|
+
export const Typewriter = forwardRef(function Typewriter({ children, className, defaultPaused = false, locale, onComplete, onPausedChange, paused, punctuationDelay = 240, speed = 48, startDelay = 0, ...props }, forwardedRef) {
|
|
23
|
+
const graphemes = useMemo(() => splitTypewriterGraphemes(children, locale), [children, locale]);
|
|
24
|
+
const [internalPaused, setInternalPaused] = useState(defaultPaused);
|
|
25
|
+
const [sequence, setSequence] = useState({ text: children, revealed: 0, cycle: 0 });
|
|
26
|
+
const completedCycleRef = useRef(null);
|
|
27
|
+
const onCompleteRef = useRef(onComplete);
|
|
28
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
29
|
+
const isPaused = paused ?? internalPaused;
|
|
30
|
+
const activeSequence = sequence.text === children
|
|
31
|
+
? sequence
|
|
32
|
+
: { text: children, revealed: 0, cycle: sequence.cycle + 1 };
|
|
33
|
+
const revealed = Math.min(activeSequence.revealed, graphemes.length);
|
|
34
|
+
const complete = revealed >= graphemes.length;
|
|
35
|
+
onCompleteRef.current = onComplete;
|
|
36
|
+
const requestPaused = (nextPaused) => {
|
|
37
|
+
if (paused === undefined)
|
|
38
|
+
setInternalPaused(nextPaused);
|
|
39
|
+
if (nextPaused !== isPaused)
|
|
40
|
+
onPausedChange?.(nextPaused);
|
|
41
|
+
};
|
|
42
|
+
const revealAll = () => {
|
|
43
|
+
setSequence(current => ({
|
|
44
|
+
text: children,
|
|
45
|
+
revealed: graphemes.length,
|
|
46
|
+
cycle: current.text === children ? current.cycle : current.cycle + 1,
|
|
47
|
+
}));
|
|
48
|
+
};
|
|
49
|
+
const restart = () => {
|
|
50
|
+
completedCycleRef.current = null;
|
|
51
|
+
setSequence(current => ({ text: children, revealed: 0, cycle: current.cycle + 1 }));
|
|
52
|
+
};
|
|
53
|
+
useImperativeHandle(forwardedRef, () => ({
|
|
54
|
+
pause: () => requestPaused(true),
|
|
55
|
+
resume: () => requestPaused(false),
|
|
56
|
+
complete: revealAll,
|
|
57
|
+
restart,
|
|
58
|
+
}));
|
|
59
|
+
useBrowserLayoutEffect(() => {
|
|
60
|
+
if (sequence.text === children)
|
|
61
|
+
return;
|
|
62
|
+
completedCycleRef.current = null;
|
|
63
|
+
setSequence(current => ({ text: children, revealed: 0, cycle: current.cycle + 1 }));
|
|
64
|
+
}, [children, sequence.text]);
|
|
65
|
+
useBrowserLayoutEffect(() => {
|
|
66
|
+
if (!reducedMotion || complete)
|
|
67
|
+
return;
|
|
68
|
+
revealAll();
|
|
69
|
+
}, [complete, reducedMotion, graphemes.length, children]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!complete || completedCycleRef.current === activeSequence.cycle)
|
|
72
|
+
return;
|
|
73
|
+
completedCycleRef.current = activeSequence.cycle;
|
|
74
|
+
onCompleteRef.current?.();
|
|
75
|
+
}, [activeSequence.cycle, complete]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (complete || isPaused || reducedMotion)
|
|
78
|
+
return undefined;
|
|
79
|
+
const delay = revealed === 0
|
|
80
|
+
? safeDuration(startDelay)
|
|
81
|
+
: typewriterDelayAfter(graphemes[revealed - 1], { speed, punctuationDelay });
|
|
82
|
+
const timer = window.setTimeout(() => {
|
|
83
|
+
setSequence(current => current.text === children
|
|
84
|
+
? { ...current, revealed: Math.min(graphemes.length, current.revealed + 1) }
|
|
85
|
+
: { text: children, revealed: 1, cycle: current.cycle + 1 });
|
|
86
|
+
}, delay);
|
|
87
|
+
return () => window.clearTimeout(timer);
|
|
88
|
+
}, [children, complete, graphemes, isPaused, punctuationDelay, reducedMotion, revealed, speed, startDelay]);
|
|
89
|
+
const classes = ['inkra-typewriter', className].filter(Boolean).join(' ');
|
|
90
|
+
const state = complete ? 'complete' : isPaused ? 'paused' : 'typing';
|
|
91
|
+
return _jsxs("span", { ...props, className: classes, "data-state": state, "data-revealed": revealed, "data-total": graphemes.length, children: [_jsx("span", { "aria-hidden": "true", className: "inkra-typewriter__visual", children: graphemes.map((grapheme, index) => {
|
|
92
|
+
const isRevealed = index < revealed;
|
|
93
|
+
if (grapheme === '\n')
|
|
94
|
+
return _jsx("br", {}, `line-${index}`);
|
|
95
|
+
if (/^\s+$/u.test(grapheme)) {
|
|
96
|
+
return _jsx("span", { className: "inkra-typewriter__space", "data-revealed": isRevealed, children: grapheme }, `space-${index}`);
|
|
97
|
+
}
|
|
98
|
+
return _jsx(PencilText, { className: "inkra-typewriter__grapheme", "data-revealed": isRevealed, seed: `typewriter-${index}-${grapheme}`, children: grapheme }, `${index}-${grapheme}`);
|
|
99
|
+
}) }), _jsx("span", { className: "inkra-typewriter__accessible", children: children })] });
|
|
100
|
+
});
|
|
101
|
+
export { isTypewriterPunctuation, splitTypewriterGraphemes, typewriterDelayAfter, } from './model.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface TypewriterDelayOptions {
|
|
2
|
+
speed: number;
|
|
3
|
+
punctuationDelay: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function splitTypewriterGraphemes(value: string, locale?: string | string[]): string[];
|
|
6
|
+
export declare function isTypewriterPunctuation(grapheme: string): boolean;
|
|
7
|
+
export declare function typewriterDelayAfter(grapheme: string, { speed, punctuationDelay }: TypewriterDelayOptions): number;
|