@inkra/react 0.5.0 → 0.8.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 +88 -1
- package/dist/Button.js +1 -1
- package/dist/PencilText.js +8 -0
- package/dist/breadcrumb/index.d.ts +16 -4
- package/dist/breadcrumb/index.js +20 -7
- 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/i18n/catalog.d.ts +1 -1
- package/dist/i18n/catalog.js +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/inventory-grid/index.d.ts +35 -0
- package/dist/inventory-grid/index.js +205 -0
- package/dist/inventory-grid/model.d.ts +56 -0
- package/dist/inventory-grid/model.js +93 -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/pagination/index.d.ts +17 -0
- package/dist/pagination/index.js +30 -6
- package/dist/select/StepperSelect.js +1 -1
- package/dist/styles/base.css +9 -1
- package/dist/styles/breadcrumb.css +96 -5
- package/dist/styles/form.css +10 -0
- package/dist/styles/inventory-grid.css +31 -0
- package/dist/styles/layout.css +85 -0
- package/dist/styles/pagination.css +10 -0
- package/dist/styles/select.css +7 -2
- package/dist/styles.css +249 -8
- package/package.json +8 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface InventoryCell {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
export type InventoryRotation = 0 | 90 | 180 | 270;
|
|
6
|
+
export interface InventoryItem {
|
|
7
|
+
/** Unique across all containers. */
|
|
8
|
+
id: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
cells: readonly InventoryCell[];
|
|
11
|
+
position: InventoryCell;
|
|
12
|
+
rotation?: InventoryRotation;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
rotatable?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface InventoryContainer<Item extends InventoryItem = InventoryItem> {
|
|
17
|
+
id: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
columns: number;
|
|
20
|
+
rows: number;
|
|
21
|
+
items: readonly Item[];
|
|
22
|
+
disabledCells?: readonly InventoryCell[];
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface InventoryPlacement {
|
|
26
|
+
itemId: string;
|
|
27
|
+
containerId: string;
|
|
28
|
+
position: InventoryCell;
|
|
29
|
+
rotation: InventoryRotation;
|
|
30
|
+
}
|
|
31
|
+
export interface InventoryPlacementContext<Item extends InventoryItem = InventoryItem> {
|
|
32
|
+
item: Item;
|
|
33
|
+
source: InventoryContainer<Item>;
|
|
34
|
+
target: InventoryContainer<Item>;
|
|
35
|
+
placement: InventoryPlacement;
|
|
36
|
+
cells: readonly InventoryCell[];
|
|
37
|
+
containers: readonly InventoryContainer<Item>[];
|
|
38
|
+
}
|
|
39
|
+
export type InventoryPlacementRule<Item extends InventoryItem = InventoryItem> = (context: InventoryPlacementContext<Item>) => boolean;
|
|
40
|
+
export type InventoryPlacementResult = {
|
|
41
|
+
valid: true;
|
|
42
|
+
} | {
|
|
43
|
+
valid: false;
|
|
44
|
+
reason: 'missing' | 'disabled' | 'shape' | 'bounds' | 'blocked' | 'occupied' | 'rule';
|
|
45
|
+
};
|
|
46
|
+
export interface InventoryChangeDetails {
|
|
47
|
+
type: 'move' | 'transfer' | 'rotate';
|
|
48
|
+
from: InventoryPlacement;
|
|
49
|
+
to: InventoryPlacement;
|
|
50
|
+
}
|
|
51
|
+
/** Rotations retain the top-left occupied bounding-box anchor. */
|
|
52
|
+
export declare function rotateInventoryCells(cells: readonly InventoryCell[], rotation?: InventoryRotation): InventoryCell[];
|
|
53
|
+
export declare function assertInventoryContainers<Item extends InventoryItem>(containers: readonly InventoryContainer<Item>[]): void;
|
|
54
|
+
export declare function validateInventoryPlacement<Item extends InventoryItem>(containers: readonly InventoryContainer<Item>[], placement: InventoryPlacement, canPlace?: InventoryPlacementRule<Item>): InventoryPlacementResult;
|
|
55
|
+
/** A rejected move preserves the caller's reference and never removes an item. */
|
|
56
|
+
export declare function projectInventoryMove<Item extends InventoryItem>(containers: readonly InventoryContainer<Item>[], placement: InventoryPlacement, canPlace?: InventoryPlacementRule<Item>): readonly InventoryContainer<Item>[];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const key = ({ x, y }) => `${x},${y}`;
|
|
2
|
+
const isCell = ({ x, y }) => Number.isSafeInteger(x) && Number.isSafeInteger(y);
|
|
3
|
+
/** Rotations retain the top-left occupied bounding-box anchor. */
|
|
4
|
+
export function rotateInventoryCells(cells, rotation = 0) {
|
|
5
|
+
if (!cells.length || cells.length > 4096 || !cells.every(isCell) || ![0, 90, 180, 270].includes(rotation))
|
|
6
|
+
return [];
|
|
7
|
+
const rotated = cells.map(({ x, y }) => rotation === 90 ? { x: -y, y: x }
|
|
8
|
+
: rotation === 180 ? { x: -x, y: -y } : rotation === 270 ? { x: y, y: -x } : { x, y });
|
|
9
|
+
const minX = Math.min(...rotated.map(cell => cell.x));
|
|
10
|
+
const minY = Math.min(...rotated.map(cell => cell.y));
|
|
11
|
+
return [...new Map(rotated.map(cell => {
|
|
12
|
+
const normalized = { x: cell.x - minX, y: cell.y - minY };
|
|
13
|
+
return [key(normalized), normalized];
|
|
14
|
+
})).values()].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
15
|
+
}
|
|
16
|
+
export function assertInventoryContainers(containers) {
|
|
17
|
+
const containerIds = new Set();
|
|
18
|
+
const itemIds = new Set();
|
|
19
|
+
for (const container of containers) {
|
|
20
|
+
if (!container.id.trim())
|
|
21
|
+
throw new Error('InventoryGrid: container ids must not be empty.');
|
|
22
|
+
if (containerIds.has(container.id))
|
|
23
|
+
throw new Error(`InventoryGrid: duplicate container id "${container.id}".`);
|
|
24
|
+
containerIds.add(container.id);
|
|
25
|
+
if (!Number.isSafeInteger(container.columns) || !Number.isSafeInteger(container.rows)
|
|
26
|
+
|| container.columns < 1 || container.rows < 1 || container.columns * container.rows > 4096) {
|
|
27
|
+
throw new Error('InventoryGrid: containers require positive integer dimensions and at most 4096 cells.');
|
|
28
|
+
}
|
|
29
|
+
const occupied = new Set((container.disabledCells ?? []).map(cell => {
|
|
30
|
+
if (!isCell(cell) || cell.x < 0 || cell.y < 0 || cell.x >= container.columns || cell.y >= container.rows) {
|
|
31
|
+
throw new Error('InventoryGrid: disabled cells must be inside their container.');
|
|
32
|
+
}
|
|
33
|
+
return key(cell);
|
|
34
|
+
}));
|
|
35
|
+
for (const item of container.items) {
|
|
36
|
+
if (!item.id.trim())
|
|
37
|
+
throw new Error('InventoryGrid: item ids must not be empty.');
|
|
38
|
+
if (itemIds.has(item.id))
|
|
39
|
+
throw new Error(`InventoryGrid: duplicate item id "${item.id}".`);
|
|
40
|
+
itemIds.add(item.id);
|
|
41
|
+
const cells = rotateInventoryCells(item.cells, item.rotation);
|
|
42
|
+
if (!cells.length || !isCell(item.position))
|
|
43
|
+
throw new Error(`InventoryGrid: invalid shape or position for "${item.id}".`);
|
|
44
|
+
for (const cell of cells) {
|
|
45
|
+
const x = cell.x + item.position.x;
|
|
46
|
+
const y = cell.y + item.position.y;
|
|
47
|
+
if (x < 0 || y < 0 || x >= container.columns || y >= container.rows || occupied.has(key({ x, y }))) {
|
|
48
|
+
throw new Error(`InventoryGrid: overlapping, blocked or out-of-bounds item "${item.id}".`);
|
|
49
|
+
}
|
|
50
|
+
occupied.add(key({ x, y }));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function validateInventoryPlacement(containers, placement, canPlace) {
|
|
56
|
+
const source = containers.find(container => container.items.some(item => item.id === placement.itemId));
|
|
57
|
+
const item = source?.items.find(entry => entry.id === placement.itemId);
|
|
58
|
+
const target = containers.find(container => container.id === placement.containerId);
|
|
59
|
+
if (!source || !item || !target)
|
|
60
|
+
return { valid: false, reason: 'missing' };
|
|
61
|
+
if (source.disabled || target.disabled || item.disabled || (item.rotatable === false && placement.rotation !== (item.rotation ?? 0)))
|
|
62
|
+
return { valid: false, reason: 'disabled' };
|
|
63
|
+
const shape = rotateInventoryCells(item.cells, placement.rotation);
|
|
64
|
+
if (!shape.length || !isCell(placement.position))
|
|
65
|
+
return { valid: false, reason: 'shape' };
|
|
66
|
+
const cells = shape.map(cell => ({ x: cell.x + placement.position.x, y: cell.y + placement.position.y }));
|
|
67
|
+
if (cells.some(cell => cell.x < 0 || cell.y < 0 || cell.x >= target.columns || cell.y >= target.rows))
|
|
68
|
+
return { valid: false, reason: 'bounds' };
|
|
69
|
+
const blocked = new Set(target.disabledCells?.map(key));
|
|
70
|
+
if (cells.some(cell => blocked.has(key(cell))))
|
|
71
|
+
return { valid: false, reason: 'blocked' };
|
|
72
|
+
const occupied = new Set(target.items.filter(entry => entry.id !== item.id).flatMap(entry => rotateInventoryCells(entry.cells, entry.rotation).map(cell => key({ x: cell.x + entry.position.x, y: cell.y + entry.position.y }))));
|
|
73
|
+
if (cells.some(cell => occupied.has(key(cell))))
|
|
74
|
+
return { valid: false, reason: 'occupied' };
|
|
75
|
+
if (canPlace && !canPlace({ item, source, target, placement, cells, containers }))
|
|
76
|
+
return { valid: false, reason: 'rule' };
|
|
77
|
+
return { valid: true };
|
|
78
|
+
}
|
|
79
|
+
/** A rejected move preserves the caller's reference and never removes an item. */
|
|
80
|
+
export function projectInventoryMove(containers, placement, canPlace) {
|
|
81
|
+
if (!validateInventoryPlacement(containers, placement, canPlace).valid)
|
|
82
|
+
return containers;
|
|
83
|
+
const source = containers.find(container => container.items.some(item => item.id === placement.itemId));
|
|
84
|
+
const item = source.items.find(entry => entry.id === placement.itemId);
|
|
85
|
+
if (source.id === placement.containerId && item.position.x === placement.position.x && item.position.y === placement.position.y
|
|
86
|
+
&& (item.rotation ?? 0) === placement.rotation)
|
|
87
|
+
return containers;
|
|
88
|
+
const moved = { ...item, position: { ...placement.position }, rotation: placement.rotation };
|
|
89
|
+
return containers.map(container => container.id === source.id && container.id === placement.containerId
|
|
90
|
+
? { ...container, items: container.items.map(entry => entry.id === item.id ? moved : entry) }
|
|
91
|
+
: container.id === source.id ? { ...container, items: container.items.filter(entry => entry.id !== item.id) }
|
|
92
|
+
: container.id === placement.containerId ? { ...container, items: [...container.items, moved] } : container);
|
|
93
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import type { Layout, LayoutItem } from 'react-grid-layout';
|
|
3
|
+
export type GridBoardLayout = Layout;
|
|
4
|
+
export type GridBoardLayoutItem = LayoutItem;
|
|
5
|
+
export interface GridBoardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
|
|
6
|
+
layout?: GridBoardLayout;
|
|
7
|
+
defaultLayout?: GridBoardLayout;
|
|
8
|
+
onLayoutChange?: (layout: GridBoardLayout) => void;
|
|
9
|
+
columns?: number;
|
|
10
|
+
rowHeight?: number;
|
|
11
|
+
gap?: number;
|
|
12
|
+
draggable?: boolean;
|
|
13
|
+
resizable?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** Container width below which cards become a non-editable single column. 0 disables stacking. */
|
|
16
|
+
stackAt?: number;
|
|
17
|
+
compact?: 'vertical' | 'horizontal' | null;
|
|
18
|
+
getItemLabel?: (id: string) => string;
|
|
19
|
+
getActionLabel?: (action: 'move' | 'resize', id: string) => string;
|
|
20
|
+
children: (item: GridBoardLayoutItem) => ReactNode;
|
|
21
|
+
}
|
|
22
|
+
export declare function GridBoard({ layout, defaultLayout, onLayoutChange, columns, rowHeight, gap, draggable, resizable, disabled, stackAt, compact, getItemLabel, getActionLabel, children, className, style, ...props }: GridBoardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { GridItem, useContainerWidth, useGridLayout } from 'react-grid-layout';
|
|
4
|
+
import { cloneLayout, correctBounds, getCompactor, moveElement } from 'react-grid-layout/core';
|
|
5
|
+
import { __iconData as moveIcon } from 'lucide-react/dist/esm/icons/move.mjs';
|
|
6
|
+
import { __iconData as resizeIcon } from 'lucide-react/dist/esm/icons/move-diagonal-2.mjs';
|
|
7
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
8
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
9
|
+
import { PencilText } from '../PencilText.js';
|
|
10
|
+
import { useInkraLocale } from '../i18n/index.js';
|
|
11
|
+
const paths = (icon) => icon.node.flatMap(([tag, attrs]) => tag === 'path' && typeof attrs.d === 'string' ? [{ d: attrs.d }] : []);
|
|
12
|
+
const movePaths = paths(moveIcon);
|
|
13
|
+
const resizePaths = paths(resizeIcon);
|
|
14
|
+
const same = (a, b) => a.length === b.length && a.every((item, index) => {
|
|
15
|
+
const other = b[index];
|
|
16
|
+
return other && item.i === other.i && item.x === other.x && item.y === other.y && item.w === other.w && item.h === other.h;
|
|
17
|
+
});
|
|
18
|
+
const sameConfiguration = (a, b) => same(a, b) && a.every((item, index) => {
|
|
19
|
+
const other = b[index];
|
|
20
|
+
return ['minW', 'minH', 'maxW', 'maxH', 'isDraggable', 'isResizable', 'isBounded'].every(key => item[key] === other[key])
|
|
21
|
+
&& Boolean(item.static) === Boolean(other.static)
|
|
22
|
+
&& item.constraints === other.constraints
|
|
23
|
+
&& (item.resizeHandles ?? []).join(',') === (other.resizeHandles ?? []).join(',');
|
|
24
|
+
});
|
|
25
|
+
function validateBoardLayout(value) {
|
|
26
|
+
if (!Array.isArray(value))
|
|
27
|
+
throw new TypeError('Grid.Board layout must be an array.');
|
|
28
|
+
const ids = new Set();
|
|
29
|
+
for (const item of value) {
|
|
30
|
+
if (!item || typeof item.i !== 'string' || !item.i.trim() || ids.has(item.i)) {
|
|
31
|
+
throw new TypeError('Grid.Board layout items must have unique, non-empty string ids (i).');
|
|
32
|
+
}
|
|
33
|
+
ids.add(item.i);
|
|
34
|
+
for (const key of ['x', 'y', 'w', 'h']) {
|
|
35
|
+
const minimum = key === 'w' || key === 'h' ? 1 : 0;
|
|
36
|
+
if (!Number.isSafeInteger(item[key]) || item[key] < minimum) {
|
|
37
|
+
throw new RangeError(`Grid.Board item "${item.i}" ${key} must be a finite integer >= ${minimum}.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const key of ['minW', 'minH', 'maxW', 'maxH']) {
|
|
41
|
+
const size = item[key];
|
|
42
|
+
if (size !== undefined && !(key.startsWith('max') && size === Infinity) && (!Number.isSafeInteger(size) || size < 1)) {
|
|
43
|
+
throw new RangeError(`Grid.Board item "${item.i}" ${key} must be a positive integer (or Infinity for a maximum).`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if ((item.minW ?? 1) > (item.maxW ?? Infinity) || (item.minH ?? 1) > (item.maxH ?? Infinity)) {
|
|
47
|
+
throw new RangeError(`Grid.Board item "${item.i}" minimum size cannot exceed its maximum.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function GridBoard({ layout, defaultLayout = [], onLayoutChange, columns = 12, rowHeight = 48, gap = 12, draggable = true, resizable = true, disabled = false, stackAt = 480, compact = 'vertical', getItemLabel = id => id, getActionLabel, children, className, style, ...props }) {
|
|
52
|
+
const { t } = useInkraLocale();
|
|
53
|
+
const [local, setLocal] = useState(defaultLayout);
|
|
54
|
+
const current = layout ?? local;
|
|
55
|
+
const cols = Number.isFinite(columns) ? Math.max(1, Math.min(48, Math.trunc(columns) || 12)) : 12;
|
|
56
|
+
const height = Number.isFinite(rowHeight) ? Math.max(24, rowHeight) : 48;
|
|
57
|
+
const spacing = Number.isFinite(gap) ? Math.max(0, gap) : 12;
|
|
58
|
+
const { width, mounted, containerRef } = useContainerWidth({ initialWidth: 800 });
|
|
59
|
+
const stacked = mounted && stackAt > 0 && width < stackAt;
|
|
60
|
+
const compactor = getCompactor(compact);
|
|
61
|
+
const resolved = useMemo(() => {
|
|
62
|
+
validateBoardLayout(current);
|
|
63
|
+
return compactor.compact(correctBounds(cloneLayout(current), { cols }), cols);
|
|
64
|
+
}, [current, cols, compactor]);
|
|
65
|
+
const engine = useGridLayout({ layout: resolved, cols, compactor });
|
|
66
|
+
const pending = useRef(false);
|
|
67
|
+
const latest = useRef({ current, layout, onLayoutChange });
|
|
68
|
+
latest.current = { current: resolved, layout, onLayoutChange };
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (engine.isInteracting || !pending.current)
|
|
71
|
+
return;
|
|
72
|
+
pending.current = false;
|
|
73
|
+
const state = latest.current;
|
|
74
|
+
if (!same(engine.layout, state.current)) {
|
|
75
|
+
const next = cloneLayout(engine.layout);
|
|
76
|
+
if (state.layout === undefined)
|
|
77
|
+
setLocal(next);
|
|
78
|
+
state.onLayoutChange?.(next);
|
|
79
|
+
}
|
|
80
|
+
// Controlled values are requests, including when the owner rejects an update.
|
|
81
|
+
if (state.layout !== undefined)
|
|
82
|
+
engine.setLayout(state.current);
|
|
83
|
+
}, [engine.layout, engine.isInteracting, engine.setLayout]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!engine.isInteracting && !pending.current && !sameConfiguration(resolved, engine.layout))
|
|
86
|
+
engine.setLayout(resolved);
|
|
87
|
+
}, [resolved, engine.layout, engine.isInteracting, engine.setLayout]);
|
|
88
|
+
const actionLabel = (action, id) => getActionLabel?.(action, id)
|
|
89
|
+
?? `${t(action === 'move' ? 'sortable.handle' : 'resizable.label')}: ${getItemLabel(id)}`;
|
|
90
|
+
const keyAdjust = (event, item, resize) => {
|
|
91
|
+
const delta = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[event.key];
|
|
92
|
+
if (!delta || disabled || stacked || item.static || (resize ? !resizable || item.isResizable === false : !draggable || item.isDraggable === false))
|
|
93
|
+
return;
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
pending.current = true;
|
|
96
|
+
const [dx, dy] = delta;
|
|
97
|
+
if (resize) {
|
|
98
|
+
const w = Math.min(item.maxW ?? cols, cols - item.x, Math.max(item.minW ?? 1, item.w + dx));
|
|
99
|
+
const h = Math.min(item.maxH ?? Infinity, Math.max(item.minH ?? 1, item.h + dy));
|
|
100
|
+
engine.onResizeStop(item.i, w, h);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const next = cloneLayout(engine.layout);
|
|
104
|
+
const target = next.find(value => value.i === item.i);
|
|
105
|
+
engine.setLayout(moveElement(next, target, Math.max(0, Math.min(cols - item.w, item.x + dx)), Math.max(0, item.y + dy), true, false, compact, cols));
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
return _jsx("div", { ...props, ref: containerRef, className: ['inkra-grid-board', className].filter(Boolean).join(' '), "data-stacked": stacked || undefined, style: { '--inkra-grid-board-gap': `${spacing}px`, ...style }, children: _jsx("div", { className: "inkra-grid-board__canvas", style: { height: stacked ? undefined : Math.max(0, engine.containerHeight * (height + spacing) - spacing) }, children: engine.layout.map(item => {
|
|
109
|
+
const canMove = draggable && !disabled && !stacked && !item.static && item.isDraggable !== false;
|
|
110
|
+
const canResize = resizable && !disabled && !stacked && !item.static && item.isResizable !== false;
|
|
111
|
+
return _jsx(GridItem, { ...item, cols: cols, containerWidth: width, rowHeight: height, margin: [spacing, spacing], containerPadding: [0, 0], maxRows: Infinity, isBounded: false, isDraggable: canMove, isResizable: canResize, allowMobileScroll: true, dragThreshold: 4, handle: ".inkra-grid-board__move", cancel: ".inkra-grid-board__content", resizeHandles: ['se'], className: "inkra-grid-board__item", usePercentages: !mounted, useCSSTransforms: mounted, onDragStart: (id, x, y) => { pending.current = true; engine.onDragStart(id, x, y); }, onDrag: engine.onDrag, onDragStop: engine.onDragStop, onResizeStart: id => { pending.current = true; engine.onResizeStart(id); }, onResize: (id, w, h) => engine.onResize(id, w, h), onResizeStop: engine.onResizeStop, resizeHandle: (axis, ref) => _jsxs("button", { ref: ref, type: "button", className: `react-resizable-handle react-resizable-handle-${axis} inkra-grid-board__resize`, disabled: !canResize, tabIndex: canResize ? 0 : -1, "aria-label": actionLabel('resize', item.i), onKeyDown: event => keyAdjust(event, item, true), children: [_jsx(PencilSurface, { as: "span", variant: "outline", "aria-hidden": "true", className: "inkra-grid-board__focus" }), _jsx(PencilIcon, { paths: resizePaths, size: 18 })] }), children: _jsx("div", { "data-grid-item": item.i, children: _jsxs(PencilSurface, { variant: "paper", className: "inkra-grid-board__surface", children: [_jsxs("header", { className: "inkra-grid-board__header", children: [_jsx(PencilText, { className: "inkra-grid-board__title", children: getItemLabel(item.i) }), canMove && _jsxs("button", { type: "button", className: "inkra-grid-board__move", "aria-label": actionLabel('move', item.i), onKeyDown: event => keyAdjust(event, item, false), children: [_jsx(PencilSurface, { as: "span", variant: "outline", "aria-hidden": "true", className: "inkra-grid-board__focus" }), _jsx(PencilIcon, { paths: movePaths, size: 18 })] })] }), _jsx("div", { className: "inkra-grid-board__content", children: children(item) })] }) }) }, item.i);
|
|
112
|
+
}) }) });
|
|
113
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import type { ResizableLayout, ResizablePanelProps } from '../resizable/index.js';
|
|
3
|
+
export interface GridTrack {
|
|
4
|
+
id: string;
|
|
5
|
+
minSize?: ResizablePanelProps['minSize'];
|
|
6
|
+
maxSize?: ResizablePanelProps['maxSize'];
|
|
7
|
+
}
|
|
8
|
+
export interface GridResizableProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
9
|
+
columns: readonly GridTrack[];
|
|
10
|
+
rows: readonly GridTrack[];
|
|
11
|
+
columnSizes?: ResizableLayout;
|
|
12
|
+
defaultColumnSizes?: ResizableLayout;
|
|
13
|
+
rowSizes?: ResizableLayout;
|
|
14
|
+
defaultRowSizes?: ResizableLayout;
|
|
15
|
+
onColumnSizesChange?: (sizes: ResizableLayout) => void;
|
|
16
|
+
onRowSizesChange?: (sizes: ResizableLayout) => void;
|
|
17
|
+
onColumnSizesCommit?: (sizes: ResizableLayout) => void;
|
|
18
|
+
onRowSizesCommit?: (sizes: ResizableLayout) => void;
|
|
19
|
+
resizeColumns?: boolean;
|
|
20
|
+
resizeRows?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
getHandleLabel?: (axis: 'column' | 'row', before: string, after: string) => string;
|
|
23
|
+
children: (cell: {
|
|
24
|
+
row: string;
|
|
25
|
+
column: string;
|
|
26
|
+
rowIndex: number;
|
|
27
|
+
columnIndex: number;
|
|
28
|
+
}) => ReactNode;
|
|
29
|
+
}
|
|
30
|
+
/** Shared column sizes keep every row aligned; the existing Resizable engine owns all gestures. */
|
|
31
|
+
export declare const GridResizable: import("react").ForwardRefExoticComponent<GridResizableProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -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
|
+
};
|