@djangocfg/widget-data 0.1.1
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 +55 -0
- package/package.json +122 -0
- package/src/DataGrid/README.md +48 -0
- package/src/DataGrid/context/DataGridContext.tsx +442 -0
- package/src/DataGrid/context/index.ts +4 -0
- package/src/DataGrid/index.ts +34 -0
- package/src/DataGrid/lazy.tsx +23 -0
- package/src/DataGrid/parts/DataGrid.tsx +125 -0
- package/src/DataGrid/parts/DataGridCell.tsx +30 -0
- package/src/DataGrid/parts/DataGridHeader.tsx +111 -0
- package/src/DataGrid/parts/DataGridPagination.tsx +105 -0
- package/src/DataGrid/parts/DataGridRow.tsx +84 -0
- package/src/DataGrid/parts/index.ts +7 -0
- package/src/DataGrid/types.ts +95 -0
- package/src/DataTable/README.md +42 -0
- package/src/DataTable/context/DataTableContext.tsx +434 -0
- package/src/DataTable/context/index.ts +4 -0
- package/src/DataTable/index.ts +33 -0
- package/src/DataTable/lazy.tsx +23 -0
- package/src/DataTable/parts/DataTable.tsx +142 -0
- package/src/DataTable/parts/DataTableHeader.tsx +103 -0
- package/src/DataTable/parts/DataTablePagination.tsx +115 -0
- package/src/DataTable/parts/DataTableRow.tsx +96 -0
- package/src/DataTable/parts/index.ts +6 -0
- package/src/DataTable/types.ts +88 -0
- package/src/FileIcon/FileIcon.tsx +87 -0
- package/src/FileIcon/get-file-icon.ts +73 -0
- package/src/FileIcon/icons/icon-data.ts +399 -0
- package/src/FileIcon/index.ts +19 -0
- package/src/FileIcon/loader.ts +29 -0
- package/src/FileIcon/specialFolders.ts +110 -0
- package/src/FileIcon/treeAdapter.tsx +49 -0
- package/src/JsonTree/JsonViewer.tsx +720 -0
- package/src/JsonTree/README.md +142 -0
- package/src/JsonTree/index.tsx +11 -0
- package/src/JsonTree/lazy.tsx +22 -0
- package/src/JsonTree/types.ts +91 -0
- package/src/Kanban/context/KanbanContext.tsx +165 -0
- package/src/Kanban/context/index.ts +4 -0
- package/src/Kanban/index.ts +31 -0
- package/src/Kanban/lazy.tsx +23 -0
- package/src/Kanban/parts/Kanban.tsx +77 -0
- package/src/Kanban/parts/KanbanCard.tsx +76 -0
- package/src/Kanban/parts/KanbanColumn.tsx +79 -0
- package/src/Kanban/parts/KanbanDragOverlay.tsx +46 -0
- package/src/Kanban/parts/index.ts +6 -0
- package/src/Kanban/types.ts +45 -0
- package/src/Listbox/context/ListboxProvider.tsx +616 -0
- package/src/Listbox/context/index.ts +8 -0
- package/src/Listbox/index.ts +28 -0
- package/src/Listbox/lazy.tsx +31 -0
- package/src/Listbox/parts/ListboxGroup.tsx +71 -0
- package/src/Listbox/parts/ListboxItem.tsx +151 -0
- package/src/Listbox/parts/index.ts +12 -0
- package/src/Listbox/types.ts +98 -0
- package/src/Masonry/README.md +79 -0
- package/src/Masonry/context/MasonryProvider.tsx +1205 -0
- package/src/Masonry/context/index.ts +8 -0
- package/src/Masonry/index.ts +18 -0
- package/src/Masonry/lazy.tsx +20 -0
- package/src/Masonry/parts/index.ts +2 -0
- package/src/Masonry/types.ts +44 -0
- package/src/Timeline/context/TimelineProvider.tsx +474 -0
- package/src/Timeline/context/index.ts +16 -0
- package/src/Timeline/index.ts +29 -0
- package/src/Timeline/lazy.tsx +27 -0
- package/src/Timeline/parts/index.ts +2 -0
- package/src/Timeline/types.ts +25 -0
- package/src/Tree/FinderTree.tsx +42 -0
- package/src/Tree/README.md +542 -0
- package/src/Tree/TreeDndProvider.tsx +137 -0
- package/src/Tree/TreeRoot.tsx +290 -0
- package/src/Tree/components/TreeChevron.tsx +48 -0
- package/src/Tree/components/TreeContent.tsx +70 -0
- package/src/Tree/components/TreeDropIndicator.tsx +65 -0
- package/src/Tree/components/TreeEmpty.tsx +22 -0
- package/src/Tree/components/TreeEmptyArea.tsx +160 -0
- package/src/Tree/components/TreeError.tsx +24 -0
- package/src/Tree/components/TreeIcon.tsx +39 -0
- package/src/Tree/components/TreeIndentGuides.tsx +33 -0
- package/src/Tree/components/TreeLabel.tsx +39 -0
- package/src/Tree/components/TreeRenameInput.tsx +114 -0
- package/src/Tree/components/TreeRow.tsx +300 -0
- package/src/Tree/components/TreeSearchInput.tsx +50 -0
- package/src/Tree/components/TreeSkeleton.tsx +22 -0
- package/src/Tree/components/index.ts +28 -0
- package/src/Tree/context/TreeContext.tsx +409 -0
- package/src/Tree/context/TreeContextValue.ts +139 -0
- package/src/Tree/context/async-children/collect-ids.ts +27 -0
- package/src/Tree/context/async-children/index.ts +8 -0
- package/src/Tree/context/async-children/use-async-children.ts +157 -0
- package/src/Tree/context/clipboard/index.ts +4 -0
- package/src/Tree/context/clipboard/use-clipboard.ts +115 -0
- package/src/Tree/context/dnd/index.ts +8 -0
- package/src/Tree/context/dnd/use-dnd.ts +194 -0
- package/src/Tree/context/expansion/index.ts +4 -0
- package/src/Tree/context/expansion/use-expansion.ts +55 -0
- package/src/Tree/context/hooks.ts +177 -0
- package/src/Tree/context/index.ts +16 -0
- package/src/Tree/context/menu/builtin-actions.ts +358 -0
- package/src/Tree/context/menu/index.ts +11 -0
- package/src/Tree/context/menu/render.tsx +104 -0
- package/src/Tree/context/menu/use-resolved-menu.ts +141 -0
- package/src/Tree/context/persist/index.ts +4 -0
- package/src/Tree/context/persist/use-persist-sync.ts +74 -0
- package/src/Tree/context/rename/index.ts +4 -0
- package/src/Tree/context/rename/use-rename.ts +113 -0
- package/src/Tree/context/selection/index.ts +4 -0
- package/src/Tree/context/selection/use-selection.ts +146 -0
- package/src/Tree/context/state/index.ts +6 -0
- package/src/Tree/context/state/initial.ts +41 -0
- package/src/Tree/context/state/reducer.ts +76 -0
- package/src/Tree/context/state/types.ts +46 -0
- package/src/Tree/data/appearance.ts +237 -0
- package/src/Tree/data/childCache.ts +43 -0
- package/src/Tree/data/clipboard.ts +33 -0
- package/src/Tree/data/createDemoTree.ts +42 -0
- package/src/Tree/data/dnd.ts +123 -0
- package/src/Tree/data/finderShortcuts.ts +67 -0
- package/src/Tree/data/flatten.ts +66 -0
- package/src/Tree/data/index.ts +45 -0
- package/src/Tree/data/persist.ts +62 -0
- package/src/Tree/data/renameUtils.ts +51 -0
- package/src/Tree/data/selection.ts +161 -0
- package/src/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
- package/src/Tree/hooks/finder-hotkeys/index.ts +8 -0
- package/src/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
- package/src/Tree/hooks/index.ts +25 -0
- package/src/Tree/hooks/keyboard/activation.ts +27 -0
- package/src/Tree/hooks/keyboard/arrow-nav.ts +26 -0
- package/src/Tree/hooks/keyboard/expand-collapse.ts +54 -0
- package/src/Tree/hooks/keyboard/index.ts +10 -0
- package/src/Tree/hooks/keyboard/types.ts +39 -0
- package/src/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
- package/src/Tree/hooks/type-ahead/index.ts +5 -0
- package/src/Tree/hooks/type-ahead/match-prefix.ts +42 -0
- package/src/Tree/hooks/type-ahead/use-tree-type-ahead.ts +89 -0
- package/src/Tree/index.tsx +135 -0
- package/src/Tree/lazy.tsx +14 -0
- package/src/Tree/types/activation.ts +30 -0
- package/src/Tree/types/adapter.ts +70 -0
- package/src/Tree/types/index.ts +27 -0
- package/src/Tree/types/labels.ts +97 -0
- package/src/Tree/types/loader.ts +9 -0
- package/src/Tree/types/node.ts +38 -0
- package/src/Tree/types/root-props.ts +158 -0
- package/src/Tree/types/selection.ts +3 -0
- package/src/Tree/types/slots.ts +64 -0
- package/src/index.ts +21 -0
- package/src/stories/_fixtures/categories.ts +51 -0
- package/src/stories/_fixtures/finder-store.ts +196 -0
- package/src/stories/_fixtures/fs.ts +83 -0
- package/src/stories/_fixtures/index.ts +5 -0
- package/src/stories/_fixtures/org.ts +46 -0
- package/src/stories/_fixtures/status.ts +21 -0
- package/src/styles/index.css +10 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { useComposedRefs } from "@djangocfg/ui-core/lib";
|
|
6
|
+
import { cn } from "@djangocfg/ui-core/lib";
|
|
7
|
+
import type { ListboxGroupProps, ListboxGroupLabelProps } from "../types";
|
|
8
|
+
|
|
9
|
+
const GROUP_NAME = "ListboxGroup";
|
|
10
|
+
const GROUP_LABEL_NAME = "ListboxGroupLabel";
|
|
11
|
+
|
|
12
|
+
interface ListboxGroupContextValue {
|
|
13
|
+
id: string;
|
|
14
|
+
labelId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ListboxGroupContext = React.createContext<ListboxGroupContextValue | null>(null);
|
|
18
|
+
ListboxGroupContext.displayName = GROUP_NAME;
|
|
19
|
+
|
|
20
|
+
function useListboxGroupContext(name: string) {
|
|
21
|
+
const context = React.useContext(ListboxGroupContext);
|
|
22
|
+
if (!context) {
|
|
23
|
+
throw new Error(`\`${name}\` must be used within \`${GROUP_NAME}\``);
|
|
24
|
+
}
|
|
25
|
+
return context;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ListboxGroup = React.forwardRef<HTMLDivElement, ListboxGroupProps>(
|
|
29
|
+
(props, forwardedRef) => {
|
|
30
|
+
const { asChild, className, ...groupProps } = props;
|
|
31
|
+
const id = React.useId();
|
|
32
|
+
const labelId = React.useId();
|
|
33
|
+
|
|
34
|
+
const groupContextValue = React.useMemo(() => ({ id, labelId }), [id, labelId]);
|
|
35
|
+
const GroupPrimitive = asChild ? Slot : "div";
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<ListboxGroupContext.Provider value={groupContextValue}>
|
|
39
|
+
<GroupPrimitive
|
|
40
|
+
role="group"
|
|
41
|
+
id={id}
|
|
42
|
+
aria-labelledby={labelId}
|
|
43
|
+
className={cn("", className)}
|
|
44
|
+
{...groupProps}
|
|
45
|
+
ref={forwardedRef}
|
|
46
|
+
/>
|
|
47
|
+
</ListboxGroupContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
ListboxGroup.displayName = GROUP_NAME;
|
|
52
|
+
|
|
53
|
+
const ListboxGroupLabel = React.forwardRef<HTMLDivElement, ListboxGroupLabelProps>(
|
|
54
|
+
(props, forwardedRef) => {
|
|
55
|
+
const { asChild, className, ...labelProps } = props;
|
|
56
|
+
const groupContext = useListboxGroupContext(GROUP_LABEL_NAME);
|
|
57
|
+
const LabelPrimitive = asChild ? Slot : "div";
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<LabelPrimitive
|
|
61
|
+
id={groupContext.labelId}
|
|
62
|
+
className={cn("px-2 py-1.5 text-sm font-semibold text-muted-foreground", className)}
|
|
63
|
+
{...labelProps}
|
|
64
|
+
ref={forwardedRef}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
ListboxGroupLabel.displayName = GROUP_LABEL_NAME;
|
|
70
|
+
|
|
71
|
+
export { ListboxGroup, ListboxGroupLabel, useListboxGroupContext, ListboxGroupContext };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { useComposedRefs } from "@djangocfg/ui-core/lib";
|
|
6
|
+
import { composeEventHandlers } from "@djangocfg/ui-core/lib";
|
|
7
|
+
import { cn } from "@djangocfg/ui-core/lib";
|
|
8
|
+
import { useListboxContext, useListboxState } from "../context/ListboxProvider";
|
|
9
|
+
import { ListboxGroupContext } from "./ListboxGroup";
|
|
10
|
+
import type { ListboxItemProps, ListboxItemIndicatorProps } from "../types";
|
|
11
|
+
|
|
12
|
+
const ITEM_NAME = "ListboxItem";
|
|
13
|
+
const ITEM_INDICATOR_NAME = "ListboxItemIndicator";
|
|
14
|
+
|
|
15
|
+
const ListboxItemContext = React.createContext<{ isSelected: boolean } | null>(null);
|
|
16
|
+
ListboxItemContext.displayName = ITEM_NAME;
|
|
17
|
+
|
|
18
|
+
function useListboxItemContext(name: string) {
|
|
19
|
+
const context = React.useContext(ListboxItemContext);
|
|
20
|
+
if (!context) {
|
|
21
|
+
throw new Error(`\`${name}\` must be used within \`${ITEM_NAME}\``);
|
|
22
|
+
}
|
|
23
|
+
return context;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
|
|
27
|
+
|
|
28
|
+
const ListboxItem = React.forwardRef<HTMLDivElement, ListboxItemProps>(
|
|
29
|
+
(props, forwardedRef) => {
|
|
30
|
+
const { asChild, value, disabled = false, onSelect, className, ...itemProps } = props;
|
|
31
|
+
const context = useListboxContext(ITEM_NAME);
|
|
32
|
+
const groupContext = React.useContext(ListboxGroupContext);
|
|
33
|
+
const itemRef = React.useRef<HTMLDivElement>(null);
|
|
34
|
+
const composedRef = useComposedRefs(itemRef, forwardedRef);
|
|
35
|
+
|
|
36
|
+
const isSelected = useListboxState((state) => state.selectedValues.has(value));
|
|
37
|
+
const isHighlighted = useListboxState((state) => state.highlightedValue === value);
|
|
38
|
+
const isDisabled = disabled || context.disabled;
|
|
39
|
+
|
|
40
|
+
useIsomorphicLayoutEffect(() => {
|
|
41
|
+
if (value === "") {
|
|
42
|
+
throw new Error(`${ITEM_NAME} value cannot be an empty string`);
|
|
43
|
+
}
|
|
44
|
+
return context.onItemRegister(
|
|
45
|
+
{ ref: itemRef, value, disabled: isDisabled, onSelect },
|
|
46
|
+
groupContext?.id,
|
|
47
|
+
);
|
|
48
|
+
}, [value, isDisabled, context.onItemRegister, groupContext?.id, onSelect]);
|
|
49
|
+
|
|
50
|
+
const onBlur = React.useCallback(() => {
|
|
51
|
+
context.onItemBlur();
|
|
52
|
+
}, [context.onItemBlur]);
|
|
53
|
+
|
|
54
|
+
const onClick = React.useCallback(
|
|
55
|
+
(event: React.MouseEvent) => {
|
|
56
|
+
if (!isDisabled) {
|
|
57
|
+
const isMultipleSelectionKey =
|
|
58
|
+
context.multiple && (context.multiple === true || event.ctrlKey || event.metaKey);
|
|
59
|
+
context.onItemSelect(value, isMultipleSelectionKey);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
[context.onItemSelect, value, isDisabled, context.multiple],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const onFocus = React.useCallback(() => {
|
|
66
|
+
if (!isDisabled) {
|
|
67
|
+
context.onItemFocus(value);
|
|
68
|
+
context.onItemHighlight(value);
|
|
69
|
+
}
|
|
70
|
+
}, [context.onItemFocus, context.onItemHighlight, isDisabled, value]);
|
|
71
|
+
|
|
72
|
+
const onKeyDown = React.useCallback(
|
|
73
|
+
(event: React.KeyboardEvent) => {
|
|
74
|
+
if (event.key === "Tab") {
|
|
75
|
+
if (!event.shiftKey) {
|
|
76
|
+
context.onItemBlur();
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[context.onItemBlur],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const onPointerLeave = React.useCallback(() => {
|
|
85
|
+
context.onItemHighlight(null);
|
|
86
|
+
}, [context.onItemHighlight]);
|
|
87
|
+
|
|
88
|
+
const onPointerMove = React.useCallback(() => {
|
|
89
|
+
if (!isDisabled) {
|
|
90
|
+
context.onItemHighlight(value);
|
|
91
|
+
}
|
|
92
|
+
}, [context.onItemHighlight, isDisabled, value]);
|
|
93
|
+
|
|
94
|
+
const ItemPrimitive = asChild ? Slot : "div";
|
|
95
|
+
|
|
96
|
+
const itemContextValue = React.useMemo(() => ({ isSelected }), [isSelected]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<ListboxItemContext.Provider value={itemContextValue}>
|
|
100
|
+
<ItemPrimitive
|
|
101
|
+
role="option"
|
|
102
|
+
aria-selected={isSelected}
|
|
103
|
+
data-selected={isSelected ? "" : undefined}
|
|
104
|
+
data-highlighted={isHighlighted ? "" : undefined}
|
|
105
|
+
data-disabled={isDisabled ? "" : undefined}
|
|
106
|
+
tabIndex={isDisabled ? undefined : -1}
|
|
107
|
+
className={cn(
|
|
108
|
+
"relative flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors",
|
|
109
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
110
|
+
"focus:bg-accent focus:text-accent-foreground",
|
|
111
|
+
isSelected && "bg-accent text-accent-foreground",
|
|
112
|
+
isDisabled && "pointer-events-none opacity-50",
|
|
113
|
+
className,
|
|
114
|
+
)}
|
|
115
|
+
{...itemProps}
|
|
116
|
+
ref={composedRef}
|
|
117
|
+
onClick={composeEventHandlers(itemProps.onClick, onClick)}
|
|
118
|
+
onFocus={composeEventHandlers(itemProps.onFocus, onFocus)}
|
|
119
|
+
onBlur={composeEventHandlers(itemProps.onBlur, onBlur)}
|
|
120
|
+
onKeyDown={composeEventHandlers(itemProps.onKeyDown, onKeyDown)}
|
|
121
|
+
onPointerLeave={composeEventHandlers(itemProps.onPointerLeave, onPointerLeave)}
|
|
122
|
+
onPointerMove={composeEventHandlers(itemProps.onPointerMove, onPointerMove)}
|
|
123
|
+
/>
|
|
124
|
+
</ListboxItemContext.Provider>
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
ListboxItem.displayName = ITEM_NAME;
|
|
129
|
+
|
|
130
|
+
const ListboxItemIndicator = React.forwardRef<HTMLSpanElement, ListboxItemIndicatorProps>(
|
|
131
|
+
(props, forwardedRef) => {
|
|
132
|
+
const { forceMount = false, asChild, className, ...indicatorProps } = props;
|
|
133
|
+
const itemContext = useListboxItemContext(ITEM_INDICATOR_NAME);
|
|
134
|
+
|
|
135
|
+
if (!forceMount && !itemContext.isSelected) return null;
|
|
136
|
+
|
|
137
|
+
const IndicatorPrimitive = asChild ? Slot : "span";
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<IndicatorPrimitive
|
|
141
|
+
aria-hidden="true"
|
|
142
|
+
className={cn("mr-2 inline-flex size-4 items-center justify-center", className)}
|
|
143
|
+
{...indicatorProps}
|
|
144
|
+
ref={forwardedRef}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
ListboxItemIndicator.displayName = ITEM_INDICATOR_NAME;
|
|
150
|
+
|
|
151
|
+
export { ListboxItem, ListboxItemIndicator, useListboxItemContext, ListboxItemContext };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
export type ListboxValue<Multiple extends boolean = false> = Multiple extends true
|
|
6
|
+
? string[]
|
|
7
|
+
: string;
|
|
8
|
+
|
|
9
|
+
export interface ListboxState {
|
|
10
|
+
selectedValues: Set<string>;
|
|
11
|
+
focusedValue: string | null;
|
|
12
|
+
highlightedValue: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type StoreListener = () => void;
|
|
16
|
+
export type StoreStateSelector<T> = (state: ListboxState) => T;
|
|
17
|
+
|
|
18
|
+
export interface ListboxStore {
|
|
19
|
+
onSubscribe: (callback: StoreListener) => () => void;
|
|
20
|
+
getSnapshot: () => ListboxState;
|
|
21
|
+
onStateChange: <K extends keyof ListboxState>(
|
|
22
|
+
key: K,
|
|
23
|
+
value: ListboxState[K],
|
|
24
|
+
silent?: boolean,
|
|
25
|
+
) => void;
|
|
26
|
+
onEmit: () => void;
|
|
27
|
+
onSelectedStateChange: (value: string, multiple?: boolean) => void;
|
|
28
|
+
onClearSelection: () => void;
|
|
29
|
+
onHighlightedValueChange: (value: string | null) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ItemData {
|
|
33
|
+
value: string;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
onSelect?: (value: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ItemElement = HTMLDivElement;
|
|
39
|
+
|
|
40
|
+
export interface CollectionItem extends ItemData {
|
|
41
|
+
ref: React.RefObject<ItemElement | null>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type CollectionItemMap = Map<
|
|
45
|
+
React.RefObject<ItemElement | null>,
|
|
46
|
+
CollectionItem
|
|
47
|
+
>;
|
|
48
|
+
|
|
49
|
+
export type CollectionGroupMap = Map<string, Set<React.RefObject<ItemElement | null>>>;
|
|
50
|
+
|
|
51
|
+
export interface ListboxContextValue<Multiple extends boolean = false> {
|
|
52
|
+
store: ListboxStore;
|
|
53
|
+
onItemRegister: (item: CollectionItem, groupId?: string) => () => void;
|
|
54
|
+
onItemSelect: (value: string, isMultipleEvent?: boolean) => void;
|
|
55
|
+
onItemFocus: (value: string) => void;
|
|
56
|
+
onItemBlur: () => void;
|
|
57
|
+
onItemHighlight: (value: string | null) => void;
|
|
58
|
+
dir: "ltr" | "rtl";
|
|
59
|
+
disabled: boolean;
|
|
60
|
+
multiple: Multiple;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ListboxRootProps<Multiple extends boolean = false>
|
|
64
|
+
extends React.ComponentPropsWithoutRef<"div"> {
|
|
65
|
+
defaultValue?: ListboxValue<Multiple>;
|
|
66
|
+
value?: ListboxValue<Multiple>;
|
|
67
|
+
onValueChange?: (value: ListboxValue<Multiple>) => void;
|
|
68
|
+
dir?: "ltr" | "rtl";
|
|
69
|
+
asChild?: boolean;
|
|
70
|
+
disabled?: boolean;
|
|
71
|
+
loop?: boolean;
|
|
72
|
+
multiple?: Multiple;
|
|
73
|
+
virtual?: boolean;
|
|
74
|
+
orientation?: "horizontal" | "vertical" | "mixed";
|
|
75
|
+
name?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ListboxGroupProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
79
|
+
asChild?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ListboxGroupLabelProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
83
|
+
asChild?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ListboxItemProps
|
|
87
|
+
extends Omit<React.ComponentPropsWithoutRef<"div">, "onSelect"> {
|
|
88
|
+
value: string;
|
|
89
|
+
disabled?: boolean;
|
|
90
|
+
asChild?: boolean;
|
|
91
|
+
onSelect?: (value: string) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ListboxItemIndicatorProps
|
|
95
|
+
extends React.ComponentPropsWithoutRef<"span"> {
|
|
96
|
+
forceMount?: boolean;
|
|
97
|
+
asChild?: boolean;
|
|
98
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Masonry
|
|
2
|
+
|
|
3
|
+
Virtualised Pinterest-style masonry grid. Items of arbitrary heights are
|
|
4
|
+
placed into N columns; each new item lands in the currently-shortest
|
|
5
|
+
column. Window-scroll virtualised — only items within the scroll
|
|
6
|
+
overscan are mounted, so the layout scales to thousands of tiles.
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { Masonry, MasonryItem } from '@djangocfg/ui-tools/masonry';
|
|
10
|
+
|
|
11
|
+
<Masonry columnCount={3} gap={12}>
|
|
12
|
+
{items.map((item) => (
|
|
13
|
+
<MasonryItem key={item.id} className="rounded-md border bg-card p-4">
|
|
14
|
+
<h4>{item.title}</h4>
|
|
15
|
+
<p>{item.body}</p>
|
|
16
|
+
</MasonryItem>
|
|
17
|
+
))}
|
|
18
|
+
</Masonry>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Picking the column model
|
|
22
|
+
|
|
23
|
+
Pass **either** `columnCount` **or** `columnWidth` — not both.
|
|
24
|
+
|
|
25
|
+
| Mode | Use when | Behaviour |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `columnCount={n}` | You want exactly N columns regardless of width | Column width = `(container - gaps) / n` |
|
|
28
|
+
| `columnWidth={px}` | You want responsive columns | Column count = `floor(container / (px + gap))`, clamped to `maxColumnCount` |
|
|
29
|
+
|
|
30
|
+
Defaults: `columnWidth=200`, `gap=12`, `overscan=2` (viewports above and below the visible area).
|
|
31
|
+
|
|
32
|
+
## Rules of use
|
|
33
|
+
|
|
34
|
+
These come from the window-scroll virtualisation model. Violating them produces an empty or zero-height grid.
|
|
35
|
+
|
|
36
|
+
1. **Do not wrap `<Masonry>` in a fixed-height ancestor.** It measures `document.documentElement` and `window.scrollY`. Putting it inside `height: 600px; overflow: auto` makes it think the viewport is 600px and breaks scroll detection. If you need a scrollable region, use a non-virtualised CSS-columns grid instead.
|
|
37
|
+
2. **Do not put inline `height` on `<MasonryItem>` children.** The item's content drives measurement via `ResizeObserver`; an inline height stops the observer from reflecting the real layout.
|
|
38
|
+
3. **`fallback` is optional but recommended.** Without it Masonry renders a built-in muted skeleton on the first paint (before items are measured). Pass `fallback={<MySkeleton />}` if you want a specific look.
|
|
39
|
+
|
|
40
|
+
## Props
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
interface MasonryProps {
|
|
44
|
+
// Layout
|
|
45
|
+
columnCount?: number; // fixed N
|
|
46
|
+
columnWidth?: number; // responsive (default 200)
|
|
47
|
+
maxColumnCount?: number; // upper bound when columnWidth is used
|
|
48
|
+
gap?: number | { row: number; column: number }; // default 12
|
|
49
|
+
linear?: boolean; // round-robin instead of shortest-column
|
|
50
|
+
|
|
51
|
+
// Measurement
|
|
52
|
+
itemHeight?: number; // estimate for un-measured items (default 300)
|
|
53
|
+
defaultWidth?: number; // SSR fallback width
|
|
54
|
+
defaultHeight?: number; // SSR fallback height
|
|
55
|
+
overscan?: number; // viewports above/below to render (default 2)
|
|
56
|
+
|
|
57
|
+
// Perf
|
|
58
|
+
scrollFps?: number; // scroll listener throttle (default 12)
|
|
59
|
+
|
|
60
|
+
// UI
|
|
61
|
+
fallback?: React.ReactNode; // first-paint placeholder (auto-skeleton if omitted)
|
|
62
|
+
|
|
63
|
+
// Standard div props
|
|
64
|
+
className?: string;
|
|
65
|
+
style?: React.CSSProperties;
|
|
66
|
+
asChild?: boolean;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## When *not* to use this
|
|
71
|
+
|
|
72
|
+
- **You need scrollable-container masonry** (modal, sidebar, side-by-side panes). This component virtualises against `window`. Use a CSS `column-count` grid or a different lib.
|
|
73
|
+
- **You have ≤ 30 items.** Virtualisation overhead isn't worth it — a plain CSS multi-column layout is simpler and SSR-friendly:
|
|
74
|
+
```tsx
|
|
75
|
+
<div className="columns-3 gap-3 [&>*]:break-inside-avoid">
|
|
76
|
+
{items.map((it) => <Card key={it.id}>…</Card>)}
|
|
77
|
+
</div>
|
|
78
|
+
```
|
|
79
|
+
- **You need drag-and-drop reordering.** Not supported. Pair with `@dnd-kit` on top of an unvirtualised grid instead.
|