@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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/package.json +122 -0
  4. package/src/DataGrid/README.md +48 -0
  5. package/src/DataGrid/context/DataGridContext.tsx +442 -0
  6. package/src/DataGrid/context/index.ts +4 -0
  7. package/src/DataGrid/index.ts +34 -0
  8. package/src/DataGrid/lazy.tsx +23 -0
  9. package/src/DataGrid/parts/DataGrid.tsx +125 -0
  10. package/src/DataGrid/parts/DataGridCell.tsx +30 -0
  11. package/src/DataGrid/parts/DataGridHeader.tsx +111 -0
  12. package/src/DataGrid/parts/DataGridPagination.tsx +105 -0
  13. package/src/DataGrid/parts/DataGridRow.tsx +84 -0
  14. package/src/DataGrid/parts/index.ts +7 -0
  15. package/src/DataGrid/types.ts +95 -0
  16. package/src/DataTable/README.md +42 -0
  17. package/src/DataTable/context/DataTableContext.tsx +434 -0
  18. package/src/DataTable/context/index.ts +4 -0
  19. package/src/DataTable/index.ts +33 -0
  20. package/src/DataTable/lazy.tsx +23 -0
  21. package/src/DataTable/parts/DataTable.tsx +142 -0
  22. package/src/DataTable/parts/DataTableHeader.tsx +103 -0
  23. package/src/DataTable/parts/DataTablePagination.tsx +115 -0
  24. package/src/DataTable/parts/DataTableRow.tsx +96 -0
  25. package/src/DataTable/parts/index.ts +6 -0
  26. package/src/DataTable/types.ts +88 -0
  27. package/src/FileIcon/FileIcon.tsx +87 -0
  28. package/src/FileIcon/get-file-icon.ts +73 -0
  29. package/src/FileIcon/icons/icon-data.ts +399 -0
  30. package/src/FileIcon/index.ts +19 -0
  31. package/src/FileIcon/loader.ts +29 -0
  32. package/src/FileIcon/specialFolders.ts +110 -0
  33. package/src/FileIcon/treeAdapter.tsx +49 -0
  34. package/src/JsonTree/JsonViewer.tsx +720 -0
  35. package/src/JsonTree/README.md +142 -0
  36. package/src/JsonTree/index.tsx +11 -0
  37. package/src/JsonTree/lazy.tsx +22 -0
  38. package/src/JsonTree/types.ts +91 -0
  39. package/src/Kanban/context/KanbanContext.tsx +165 -0
  40. package/src/Kanban/context/index.ts +4 -0
  41. package/src/Kanban/index.ts +31 -0
  42. package/src/Kanban/lazy.tsx +23 -0
  43. package/src/Kanban/parts/Kanban.tsx +77 -0
  44. package/src/Kanban/parts/KanbanCard.tsx +76 -0
  45. package/src/Kanban/parts/KanbanColumn.tsx +79 -0
  46. package/src/Kanban/parts/KanbanDragOverlay.tsx +46 -0
  47. package/src/Kanban/parts/index.ts +6 -0
  48. package/src/Kanban/types.ts +45 -0
  49. package/src/Listbox/context/ListboxProvider.tsx +616 -0
  50. package/src/Listbox/context/index.ts +8 -0
  51. package/src/Listbox/index.ts +28 -0
  52. package/src/Listbox/lazy.tsx +31 -0
  53. package/src/Listbox/parts/ListboxGroup.tsx +71 -0
  54. package/src/Listbox/parts/ListboxItem.tsx +151 -0
  55. package/src/Listbox/parts/index.ts +12 -0
  56. package/src/Listbox/types.ts +98 -0
  57. package/src/Masonry/README.md +79 -0
  58. package/src/Masonry/context/MasonryProvider.tsx +1205 -0
  59. package/src/Masonry/context/index.ts +8 -0
  60. package/src/Masonry/index.ts +18 -0
  61. package/src/Masonry/lazy.tsx +20 -0
  62. package/src/Masonry/parts/index.ts +2 -0
  63. package/src/Masonry/types.ts +44 -0
  64. package/src/Timeline/context/TimelineProvider.tsx +474 -0
  65. package/src/Timeline/context/index.ts +16 -0
  66. package/src/Timeline/index.ts +29 -0
  67. package/src/Timeline/lazy.tsx +27 -0
  68. package/src/Timeline/parts/index.ts +2 -0
  69. package/src/Timeline/types.ts +25 -0
  70. package/src/Tree/FinderTree.tsx +42 -0
  71. package/src/Tree/README.md +542 -0
  72. package/src/Tree/TreeDndProvider.tsx +137 -0
  73. package/src/Tree/TreeRoot.tsx +290 -0
  74. package/src/Tree/components/TreeChevron.tsx +48 -0
  75. package/src/Tree/components/TreeContent.tsx +70 -0
  76. package/src/Tree/components/TreeDropIndicator.tsx +65 -0
  77. package/src/Tree/components/TreeEmpty.tsx +22 -0
  78. package/src/Tree/components/TreeEmptyArea.tsx +160 -0
  79. package/src/Tree/components/TreeError.tsx +24 -0
  80. package/src/Tree/components/TreeIcon.tsx +39 -0
  81. package/src/Tree/components/TreeIndentGuides.tsx +33 -0
  82. package/src/Tree/components/TreeLabel.tsx +39 -0
  83. package/src/Tree/components/TreeRenameInput.tsx +114 -0
  84. package/src/Tree/components/TreeRow.tsx +300 -0
  85. package/src/Tree/components/TreeSearchInput.tsx +50 -0
  86. package/src/Tree/components/TreeSkeleton.tsx +22 -0
  87. package/src/Tree/components/index.ts +28 -0
  88. package/src/Tree/context/TreeContext.tsx +409 -0
  89. package/src/Tree/context/TreeContextValue.ts +139 -0
  90. package/src/Tree/context/async-children/collect-ids.ts +27 -0
  91. package/src/Tree/context/async-children/index.ts +8 -0
  92. package/src/Tree/context/async-children/use-async-children.ts +157 -0
  93. package/src/Tree/context/clipboard/index.ts +4 -0
  94. package/src/Tree/context/clipboard/use-clipboard.ts +115 -0
  95. package/src/Tree/context/dnd/index.ts +8 -0
  96. package/src/Tree/context/dnd/use-dnd.ts +194 -0
  97. package/src/Tree/context/expansion/index.ts +4 -0
  98. package/src/Tree/context/expansion/use-expansion.ts +55 -0
  99. package/src/Tree/context/hooks.ts +177 -0
  100. package/src/Tree/context/index.ts +16 -0
  101. package/src/Tree/context/menu/builtin-actions.ts +358 -0
  102. package/src/Tree/context/menu/index.ts +11 -0
  103. package/src/Tree/context/menu/render.tsx +104 -0
  104. package/src/Tree/context/menu/use-resolved-menu.ts +141 -0
  105. package/src/Tree/context/persist/index.ts +4 -0
  106. package/src/Tree/context/persist/use-persist-sync.ts +74 -0
  107. package/src/Tree/context/rename/index.ts +4 -0
  108. package/src/Tree/context/rename/use-rename.ts +113 -0
  109. package/src/Tree/context/selection/index.ts +4 -0
  110. package/src/Tree/context/selection/use-selection.ts +146 -0
  111. package/src/Tree/context/state/index.ts +6 -0
  112. package/src/Tree/context/state/initial.ts +41 -0
  113. package/src/Tree/context/state/reducer.ts +76 -0
  114. package/src/Tree/context/state/types.ts +46 -0
  115. package/src/Tree/data/appearance.ts +237 -0
  116. package/src/Tree/data/childCache.ts +43 -0
  117. package/src/Tree/data/clipboard.ts +33 -0
  118. package/src/Tree/data/createDemoTree.ts +42 -0
  119. package/src/Tree/data/dnd.ts +123 -0
  120. package/src/Tree/data/finderShortcuts.ts +67 -0
  121. package/src/Tree/data/flatten.ts +66 -0
  122. package/src/Tree/data/index.ts +45 -0
  123. package/src/Tree/data/persist.ts +62 -0
  124. package/src/Tree/data/renameUtils.ts +51 -0
  125. package/src/Tree/data/selection.ts +161 -0
  126. package/src/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  127. package/src/Tree/hooks/finder-hotkeys/index.ts +8 -0
  128. package/src/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  129. package/src/Tree/hooks/index.ts +25 -0
  130. package/src/Tree/hooks/keyboard/activation.ts +27 -0
  131. package/src/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  132. package/src/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  133. package/src/Tree/hooks/keyboard/index.ts +10 -0
  134. package/src/Tree/hooks/keyboard/types.ts +39 -0
  135. package/src/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  136. package/src/Tree/hooks/type-ahead/index.ts +5 -0
  137. package/src/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  138. package/src/Tree/hooks/type-ahead/use-tree-type-ahead.ts +89 -0
  139. package/src/Tree/index.tsx +135 -0
  140. package/src/Tree/lazy.tsx +14 -0
  141. package/src/Tree/types/activation.ts +30 -0
  142. package/src/Tree/types/adapter.ts +70 -0
  143. package/src/Tree/types/index.ts +27 -0
  144. package/src/Tree/types/labels.ts +97 -0
  145. package/src/Tree/types/loader.ts +9 -0
  146. package/src/Tree/types/node.ts +38 -0
  147. package/src/Tree/types/root-props.ts +158 -0
  148. package/src/Tree/types/selection.ts +3 -0
  149. package/src/Tree/types/slots.ts +64 -0
  150. package/src/index.ts +21 -0
  151. package/src/stories/_fixtures/categories.ts +51 -0
  152. package/src/stories/_fixtures/finder-store.ts +196 -0
  153. package/src/stories/_fixtures/fs.ts +83 -0
  154. package/src/stories/_fixtures/index.ts +5 -0
  155. package/src/stories/_fixtures/org.ts +46 -0
  156. package/src/stories/_fixtures/status.ts +21 -0
  157. 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,12 @@
1
+ "use client";
2
+
3
+ export {
4
+ ListboxGroup,
5
+ ListboxGroupLabel,
6
+ useListboxGroupContext,
7
+ } from "./ListboxGroup";
8
+ export {
9
+ ListboxItem,
10
+ ListboxItemIndicator,
11
+ useListboxItemContext,
12
+ } from "./ListboxItem";
@@ -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.