@djangocfg/ui-tools 2.1.315 → 2.1.317

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 (49) hide show
  1. package/dist/TreeRoot-R6XVHYQK.mjs +4 -0
  2. package/dist/{TreeRoot-DO33TIS5.mjs.map → TreeRoot-R6XVHYQK.mjs.map} +1 -1
  3. package/dist/TreeRoot-RAMQSBMO.cjs +19 -0
  4. package/dist/{TreeRoot-NJOZ2DMV.cjs.map → TreeRoot-RAMQSBMO.cjs.map} +1 -1
  5. package/dist/{chunk-MA552EWC.cjs → chunk-44ZTWYAF.cjs} +139 -111
  6. package/dist/chunk-44ZTWYAF.cjs.map +1 -0
  7. package/dist/chunk-KR6B3LVY.mjs +59 -0
  8. package/dist/chunk-KR6B3LVY.mjs.map +1 -0
  9. package/dist/{chunk-E5BP4IXF.mjs → chunk-NTJL2SXK.mjs} +139 -111
  10. package/dist/chunk-NTJL2SXK.mjs.map +1 -0
  11. package/dist/chunk-YXBOAGIM.cjs +63 -0
  12. package/dist/chunk-YXBOAGIM.cjs.map +1 -0
  13. package/dist/file-icon/index.cjs +117 -0
  14. package/dist/file-icon/index.cjs.map +1 -0
  15. package/dist/file-icon/index.d.cts +69 -0
  16. package/dist/file-icon/index.d.ts +69 -0
  17. package/dist/file-icon/index.mjs +112 -0
  18. package/dist/file-icon/index.mjs.map +1 -0
  19. package/dist/index.cjs +140 -180
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +5 -433
  22. package/dist/index.d.ts +5 -433
  23. package/dist/index.mjs +7 -56
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/tree/index.cjs +152 -0
  26. package/dist/tree/index.cjs.map +1 -0
  27. package/dist/tree/index.d.cts +278 -0
  28. package/dist/tree/index.d.ts +278 -0
  29. package/dist/tree/index.mjs +5 -0
  30. package/dist/tree/index.mjs.map +1 -0
  31. package/dist/types-Cclwv4Hl.d.cts +198 -0
  32. package/dist/types-Cclwv4Hl.d.ts +198 -0
  33. package/package.json +16 -10
  34. package/src/tools/FileIcon/FileIcon.tsx +91 -0
  35. package/src/tools/FileIcon/index.ts +9 -0
  36. package/src/tools/FileIcon/loader.ts +47 -0
  37. package/src/tools/FileIcon/treeAdapter.tsx +41 -0
  38. package/src/tools/Tree/README.md +56 -0
  39. package/src/tools/Tree/Tree.story.tsx +48 -0
  40. package/src/tools/Tree/TreeRoot.tsx +15 -5
  41. package/src/tools/Tree/components/TreeRow.tsx +17 -18
  42. package/src/tools/Tree/context/TreeContext.tsx +10 -2
  43. package/src/tools/Tree/hooks/useTreeKeyboard.ts +133 -99
  44. package/src/tools/Tree/index.tsx +2 -0
  45. package/src/tools/Tree/types.ts +36 -2
  46. package/dist/TreeRoot-DO33TIS5.mjs +0 -4
  47. package/dist/TreeRoot-NJOZ2DMV.cjs +0 -19
  48. package/dist/chunk-E5BP4IXF.mjs.map +0 -1
  49. package/dist/chunk-MA552EWC.cjs.map +0 -1
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ var chunkWGEGR3DF_cjs = require('./chunk-WGEGR3DF.cjs');
4
+ var lib = require('@djangocfg/ui-core/lib');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var lucideReact = require('lucide-react');
7
+
8
+ function TreeSkeleton({ rows = 6, className }) {
9
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: lib.cn("flex flex-col gap-1 p-2", className), "aria-hidden": true, children: Array.from({ length: rows }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", style: { paddingLeft: i % 3 * 16 }, children: [
10
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-4 shrink-0 animate-pulse rounded bg-muted" }),
11
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-4 shrink-0 animate-pulse rounded bg-muted" }),
12
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-3 flex-1 animate-pulse rounded bg-muted" })
13
+ ] }, i)) });
14
+ }
15
+ chunkWGEGR3DF_cjs.__name(TreeSkeleton, "TreeSkeleton");
16
+ function TreeError({ children, className }) {
17
+ return /* @__PURE__ */ jsxRuntime.jsxs(
18
+ "div",
19
+ {
20
+ className: lib.cn(
21
+ "flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive",
22
+ className
23
+ ),
24
+ role: "alert",
25
+ children: [
26
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { "aria-hidden": true, className: "mt-0.5 size-4 shrink-0" }),
27
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children })
28
+ ]
29
+ }
30
+ );
31
+ }
32
+ chunkWGEGR3DF_cjs.__name(TreeError, "TreeError");
33
+
34
+ // src/tools/Tree/data/createDemoTree.ts
35
+ function createDemoTree({
36
+ depth = 3,
37
+ breadth = 4,
38
+ rootPrefix = "node"
39
+ } = {}) {
40
+ const make = /* @__PURE__ */ chunkWGEGR3DF_cjs.__name((id, name, level) => {
41
+ if (level >= depth) {
42
+ return { id, data: { name } };
43
+ }
44
+ const children = [];
45
+ for (let i = 0; i < breadth; i++) {
46
+ const childId = `${id}/${i}`;
47
+ children.push(make(childId, `${name}-${i}`, level + 1));
48
+ }
49
+ return { id, data: { name }, children };
50
+ }, "make");
51
+ const roots = [];
52
+ for (let i = 0; i < breadth; i++) {
53
+ roots.push(make(`${rootPrefix}-${i}`, `${rootPrefix} ${i}`, 1));
54
+ }
55
+ return roots;
56
+ }
57
+ chunkWGEGR3DF_cjs.__name(createDemoTree, "createDemoTree");
58
+
59
+ exports.TreeError = TreeError;
60
+ exports.TreeSkeleton = TreeSkeleton;
61
+ exports.createDemoTree = createDemoTree;
62
+ //# sourceMappingURL=chunk-YXBOAGIM.cjs.map
63
+ //# sourceMappingURL=chunk-YXBOAGIM.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tools/Tree/components/TreeSkeleton.tsx","../src/tools/Tree/components/TreeError.tsx","../src/tools/Tree/data/createDemoTree.ts"],"names":["jsx","cn","jsxs","__name","AlertCircle"],"mappings":";;;;;;;AASO,SAAS,YAAA,CAAa,EAAE,IAAA,GAAO,CAAA,EAAG,WAAU,EAAsB;AACvE,EAAA,uBACEA,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAWC,MAAA,CAAG,yBAAA,EAA2B,SAAS,CAAA,EAAG,aAAA,EAAW,IAAA,EAClE,QAAA,EAAA,KAAA,CAAM,IAAA,CAAK,EAAE,QAAQ,IAAA,EAAM,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,EAAG,CAAA,qBACpCC,eAAA,CAAC,KAAA,EAAA,EAAY,SAAA,EAAU,yBAAA,EAA0B,KAAA,EAAO,EAAE,WAAA,EAAc,CAAA,GAAI,CAAA,GAAK,IAAG,EAClF,QAAA,EAAA;AAAA,oBAAAF,cAAA,CAAC,MAAA,EAAA,EAAK,WAAU,gDAAA,EAAiD,CAAA;AAAA,oBACjEA,cAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,gDAAA,EAAiD,CAAA;AAAA,oBACjEA,cAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,2CAAA,EAA4C;AAAA,GAAA,EAAA,EAHpD,CAIV,CACD,CAAA,EACH,CAAA;AAEJ;AAZgBG,wBAAA,CAAA,YAAA,EAAA,cAAA,CAAA;ACCT,SAAS,SAAA,CAAU,EAAE,QAAA,EAAU,SAAA,EAAU,EAAmB;AACjE,EAAA,uBACED,eAAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAWD,MAAAA;AAAA,QACT,qHAAA;AAAA,QACA;AAAA,OACF;AAAA,MACA,IAAA,EAAK,OAAA;AAAA,MAEL,QAAA,EAAA;AAAA,wBAAAD,cAAAA,CAACI,uBAAA,EAAA,EAAY,aAAA,EAAW,IAAA,EAAC,WAAU,wBAAA,EAAyB,CAAA;AAAA,wBAC5DJ,cAAAA,CAAC,MAAA,EAAA,EAAM,QAAA,EAAS;AAAA;AAAA;AAAA,GAClB;AAEJ;AAbgBG,wBAAA,CAAA,SAAA,EAAA,WAAA,CAAA;;;ACKT,SAAS,cAAA,CAAe;AAAA,EAC7B,KAAA,GAAQ,CAAA;AAAA,EACR,OAAA,GAAU,CAAA;AAAA,EACV,UAAA,GAAa;AACf,CAAA,GAII,EAAC,EAAyB;AAC5B,EAAA,MAAM,IAAA,mBAAOA,wBAAA,CAAA,CAAC,EAAA,EAAY,IAAA,EAAc,KAAA,KAAsC;AAC5E,IAAA,IAAI,SAAS,KAAA,EAAO;AAClB,MAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,EAAE,MAAK,EAAE;AAAA,IAC9B;AACA,IAAA,MAAM,WAAiC,EAAC;AACxC,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,OAAA,EAAS,CAAA,EAAA,EAAK;AAChC,MAAA,MAAM,OAAA,GAAU,CAAA,EAAG,EAAE,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA;AAC1B,MAAA,QAAA,CAAS,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,CAAA,EAAG,IAAI,IAAI,CAAC,CAAA,CAAA,EAAI,KAAA,GAAQ,CAAC,CAAC,CAAA;AAAA,IACxD;AACA,IAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,EAAE,IAAA,IAAQ,QAAA,EAAS;AAAA,EACxC,CAAA,EAVa,MAAA,CAAA;AAYb,EAAA,MAAM,QAA8B,EAAC;AACrC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,OAAA,EAAS,CAAA,EAAA,EAAK;AAChC,IAAA,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,CAAA,EAAG,UAAU,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA,EAAI,CAAA,EAAG,UAAU,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA,EAAI,CAAC,CAAC,CAAA;AAAA,EAChE;AACA,EAAA,OAAO,KAAA;AACT;AA1BgBA,wBAAA,CAAA,cAAA,EAAA,gBAAA,CAAA","file":"chunk-YXBOAGIM.cjs","sourcesContent":["'use client';\n\nimport { cn } from '@djangocfg/ui-core/lib';\n\nexport interface TreeSkeletonProps {\n rows?: number;\n className?: string;\n}\n\nexport function TreeSkeleton({ rows = 6, className }: TreeSkeletonProps) {\n return (\n <div className={cn('flex flex-col gap-1 p-2', className)} aria-hidden>\n {Array.from({ length: rows }).map((_, i) => (\n <div key={i} className=\"flex items-center gap-2\" style={{ paddingLeft: (i % 3) * 16 }}>\n <span className=\"size-4 shrink-0 animate-pulse rounded bg-muted\" />\n <span className=\"size-4 shrink-0 animate-pulse rounded bg-muted\" />\n <span className=\"h-3 flex-1 animate-pulse rounded bg-muted\" />\n </div>\n ))}\n </div>\n );\n}\n","'use client';\n\nimport { AlertCircle } from 'lucide-react';\nimport { cn } from '@djangocfg/ui-core/lib';\n\nexport interface TreeErrorProps {\n children: React.ReactNode;\n className?: string;\n}\n\nexport function TreeError({ children, className }: TreeErrorProps) {\n return (\n <div\n className={cn(\n 'flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive',\n className,\n )}\n role=\"alert\"\n >\n <AlertCircle aria-hidden className=\"mt-0.5 size-4 shrink-0\" />\n <span>{children}</span>\n </div>\n );\n}\n","'use client';\n\nimport type { TreeNode } from '../types';\n\nexport interface DemoNode {\n name: string;\n}\n\n/**\n * Build a deterministic synthetic tree for stories and tests.\n *\n * @example\n * const data = createDemoTree({ depth: 4, breadth: 3 });\n * <TreeRoot data={data} getItemName={(n) => n.data.name} />\n */\nexport function createDemoTree({\n depth = 3,\n breadth = 4,\n rootPrefix = 'node',\n}: {\n depth?: number;\n breadth?: number;\n rootPrefix?: string;\n} = {}): TreeNode<DemoNode>[] {\n const make = (id: string, name: string, level: number): TreeNode<DemoNode> => {\n if (level >= depth) {\n return { id, data: { name } };\n }\n const children: TreeNode<DemoNode>[] = [];\n for (let i = 0; i < breadth; i++) {\n const childId = `${id}/${i}`;\n children.push(make(childId, `${name}-${i}`, level + 1));\n }\n return { id, data: { name }, children };\n };\n\n const roots: TreeNode<DemoNode>[] = [];\n for (let i = 0; i < breadth; i++) {\n roots.push(make(`${rootPrefix}-${i}`, `${rootPrefix} ${i}`, 1));\n }\n return roots;\n}\n"]}
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ var chunkWGEGR3DF_cjs = require('../chunk-WGEGR3DF.cjs');
4
+ var react = require('react');
5
+ var lucideReact = require('lucide-react');
6
+ var lib = require('@djangocfg/ui-core/lib');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+
9
+ // src/tools/FileIcon/loader.ts
10
+ var cached;
11
+ var inflight = null;
12
+ async function loadMaterialIcons() {
13
+ if (cached !== void 0) return cached;
14
+ if (inflight) return inflight;
15
+ inflight = (async () => {
16
+ try {
17
+ const specifier = "material-file-icons";
18
+ const mod = await import(
19
+ /* @vite-ignore */
20
+ specifier
21
+ );
22
+ const fn = mod.getIcon ?? mod.default?.getIcon ?? null;
23
+ cached = fn;
24
+ return fn;
25
+ } catch {
26
+ cached = null;
27
+ return null;
28
+ } finally {
29
+ inflight = null;
30
+ }
31
+ })();
32
+ return inflight;
33
+ }
34
+ chunkWGEGR3DF_cjs.__name(loadMaterialIcons, "loadMaterialIcons");
35
+ function getMaterialIconsSync() {
36
+ return cached ?? null;
37
+ }
38
+ chunkWGEGR3DF_cjs.__name(getMaterialIconsSync, "getMaterialIconsSync");
39
+ function FileIcon({
40
+ name,
41
+ isFolder = false,
42
+ isExpanded = false,
43
+ size = 16,
44
+ className
45
+ }) {
46
+ const [svg, setSvg] = react.useState(() => {
47
+ if (isFolder) return null;
48
+ const fn = getMaterialIconsSync();
49
+ return fn ? fn(name)?.svg ?? null : null;
50
+ });
51
+ react.useEffect(() => {
52
+ if (isFolder) {
53
+ setSvg(null);
54
+ return;
55
+ }
56
+ let cancelled = false;
57
+ void loadMaterialIcons().then((fn) => {
58
+ if (cancelled) return;
59
+ setSvg(fn ? fn(name)?.svg ?? null : null);
60
+ });
61
+ return () => {
62
+ cancelled = true;
63
+ };
64
+ }, [isFolder, name]);
65
+ if (isFolder) {
66
+ const Icon = isExpanded ? lucideReact.FolderOpen : lucideReact.Folder;
67
+ return /* @__PURE__ */ jsxRuntime.jsx(
68
+ Icon,
69
+ {
70
+ className: lib.cn("shrink-0 text-amber-500", className),
71
+ style: { width: size, height: size }
72
+ }
73
+ );
74
+ }
75
+ if (!svg) {
76
+ return /* @__PURE__ */ jsxRuntime.jsx(
77
+ lucideReact.File,
78
+ {
79
+ className: lib.cn("shrink-0 text-muted-foreground/80", className),
80
+ style: { width: size, height: size }
81
+ }
82
+ );
83
+ }
84
+ return /* @__PURE__ */ jsxRuntime.jsx(
85
+ "span",
86
+ {
87
+ role: "img",
88
+ "aria-hidden": true,
89
+ className: lib.cn("inline-block shrink-0", className),
90
+ style: { width: size, height: size },
91
+ dangerouslySetInnerHTML: { __html: svg }
92
+ }
93
+ );
94
+ }
95
+ chunkWGEGR3DF_cjs.__name(FileIcon, "FileIcon");
96
+ function createFileIconSlot({
97
+ getName,
98
+ size = 16
99
+ }) {
100
+ return ({ node, isFolder, isExpanded }) => /* @__PURE__ */ jsxRuntime.jsx(
101
+ FileIcon,
102
+ {
103
+ name: getName(node),
104
+ isFolder,
105
+ isExpanded,
106
+ size
107
+ }
108
+ );
109
+ }
110
+ chunkWGEGR3DF_cjs.__name(createFileIconSlot, "createFileIconSlot");
111
+
112
+ exports.FileIcon = FileIcon;
113
+ exports.createFileIconSlot = createFileIconSlot;
114
+ exports.getMaterialIconsSync = getMaterialIconsSync;
115
+ exports.loadMaterialIcons = loadMaterialIcons;
116
+ //# sourceMappingURL=index.cjs.map
117
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tools/FileIcon/loader.ts","../../src/tools/FileIcon/FileIcon.tsx","../../src/tools/FileIcon/treeAdapter.tsx"],"names":["__name","useState","useEffect","FolderOpen","Folder","jsx","cn","FileFallback"],"mappings":"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","file":"index.cjs","sourcesContent":["'use client';\n\n/**\n * Lazy loader for `material-file-icons`. The package is an optional dependency:\n * if it's installed in the consumer's tree, we render its rich VSCode-style\n * SVGs; if not, the loader resolves to `null` and `<FileIcon>` falls back to a\n * Lucide icon. Either way, ui-tools never throws or warns about a missing\n * package.\n *\n * Resolution is cached per-process so we don't re-import on every render.\n */\n\ntype GetIconFn = (name: string) => { svg: string } | undefined;\n\nlet cached: GetIconFn | null | undefined;\nlet inflight: Promise<GetIconFn | null> | null = null;\n\nexport async function loadMaterialIcons(): Promise<GetIconFn | null> {\n if (cached !== undefined) return cached;\n if (inflight) return inflight;\n\n inflight = (async () => {\n try {\n // Computed specifier so bundlers don't try to eagerly resolve it.\n const specifier = 'material-file-icons';\n const mod = (await import(/* @vite-ignore */ specifier)) as {\n getIcon?: GetIconFn;\n default?: { getIcon?: GetIconFn };\n };\n const fn = mod.getIcon ?? mod.default?.getIcon ?? null;\n cached = fn;\n return fn;\n } catch {\n cached = null;\n return null;\n } finally {\n inflight = null;\n }\n })();\n\n return inflight;\n}\n\n/** Synchronous accessor — returns null until `loadMaterialIcons` resolves. */\nexport function getMaterialIconsSync(): GetIconFn | null {\n return cached ?? null;\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\nimport { File as FileFallback, Folder, FolderOpen } from 'lucide-react';\nimport { cn } from '@djangocfg/ui-core/lib';\n\nimport { getMaterialIconsSync, loadMaterialIcons } from './loader';\n\nexport type FileIconSize = 12 | 14 | 16 | 18 | 20 | 24 | 28 | 32 | 40 | 48 | 64;\n\nexport interface FileIconProps {\n /** File name (with extension) or folder name. */\n name: string;\n /** Render a folder icon instead of a file icon. */\n isFolder?: boolean;\n /** Folder open / closed state. Ignored when `isFolder` is false. */\n isExpanded?: boolean;\n /** Pixel size of the icon. */\n size?: FileIconSize;\n className?: string;\n}\n\n/**\n * VSCode-style file icon.\n *\n * - Folders render Lucide `Folder` / `FolderOpen` (amber tint).\n * - Files use `material-file-icons` if it's installed in the consumer; otherwise\n * fall back to a neutral Lucide `File` icon.\n *\n * The optional dependency is loaded lazily on first render — there is zero\n * bundle cost for consumers who never mount this component.\n */\nexport function FileIcon({\n name,\n isFolder = false,\n isExpanded = false,\n size = 16,\n className,\n}: FileIconProps) {\n const [svg, setSvg] = useState<string | null>(() => {\n if (isFolder) return null;\n const fn = getMaterialIconsSync();\n return fn ? (fn(name)?.svg ?? null) : null;\n });\n\n useEffect(() => {\n if (isFolder) {\n setSvg(null);\n return;\n }\n let cancelled = false;\n void loadMaterialIcons().then((fn) => {\n if (cancelled) return;\n setSvg(fn ? (fn(name)?.svg ?? null) : null);\n });\n return () => {\n cancelled = true;\n };\n }, [isFolder, name]);\n\n if (isFolder) {\n const Icon = isExpanded ? FolderOpen : Folder;\n return (\n <Icon\n className={cn('shrink-0 text-amber-500', className)}\n style={{ width: size, height: size }}\n />\n );\n }\n\n if (!svg) {\n return (\n <FileFallback\n className={cn('shrink-0 text-muted-foreground/80', className)}\n style={{ width: size, height: size }}\n />\n );\n }\n\n return (\n <span\n role=\"img\"\n aria-hidden\n className={cn('inline-block shrink-0', className)}\n style={{ width: size, height: size }}\n dangerouslySetInnerHTML={{ __html: svg }}\n />\n );\n}\n\nexport default FileIcon;\n","'use client';\n\nimport type { TreeNode, TreeRowSlot } from '../Tree/types';\nimport { FileIcon, type FileIconSize } from './FileIcon';\n\nexport interface CreateFileIconSlotOptions<T> {\n /**\n * How to read the displayed name (with extension) for a node. Use the same\n * accessor you pass to `<TreeRoot getItemName={...} />`.\n */\n getName: (node: TreeNode<T>) => string;\n /** Pixel size for both file and folder icons. Default: 16. */\n size?: FileIconSize;\n}\n\n/**\n * Build a `renderIcon` slot for `<TreeRoot>` that uses `<FileIcon>` for both\n * leaves and folders.\n *\n * @example\n * ```tsx\n * <TreeRoot\n * data={data}\n * getItemName={(n) => n.data.name}\n * renderIcon={createFileIconSlot({ getName: (n) => n.data.name })}\n * />\n * ```\n */\nexport function createFileIconSlot<T>({\n getName,\n size = 16,\n}: CreateFileIconSlotOptions<T>): TreeRowSlot<T> {\n return ({ node, isFolder, isExpanded }) => (\n <FileIcon\n name={getName(node)}\n isFolder={isFolder}\n isExpanded={isExpanded}\n size={size}\n />\n );\n}\n"]}
@@ -0,0 +1,69 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { T as TreeNode, a as TreeRowSlot } from '../types-Cclwv4Hl.cjs';
3
+ import 'react';
4
+
5
+ type FileIconSize = 12 | 14 | 16 | 18 | 20 | 24 | 28 | 32 | 40 | 48 | 64;
6
+ interface FileIconProps {
7
+ /** File name (with extension) or folder name. */
8
+ name: string;
9
+ /** Render a folder icon instead of a file icon. */
10
+ isFolder?: boolean;
11
+ /** Folder open / closed state. Ignored when `isFolder` is false. */
12
+ isExpanded?: boolean;
13
+ /** Pixel size of the icon. */
14
+ size?: FileIconSize;
15
+ className?: string;
16
+ }
17
+ /**
18
+ * VSCode-style file icon.
19
+ *
20
+ * - Folders render Lucide `Folder` / `FolderOpen` (amber tint).
21
+ * - Files use `material-file-icons` if it's installed in the consumer; otherwise
22
+ * fall back to a neutral Lucide `File` icon.
23
+ *
24
+ * The optional dependency is loaded lazily on first render — there is zero
25
+ * bundle cost for consumers who never mount this component.
26
+ */
27
+ declare function FileIcon({ name, isFolder, isExpanded, size, className, }: FileIconProps): react_jsx_runtime.JSX.Element;
28
+
29
+ interface CreateFileIconSlotOptions<T> {
30
+ /**
31
+ * How to read the displayed name (with extension) for a node. Use the same
32
+ * accessor you pass to `<TreeRoot getItemName={...} />`.
33
+ */
34
+ getName: (node: TreeNode<T>) => string;
35
+ /** Pixel size for both file and folder icons. Default: 16. */
36
+ size?: FileIconSize;
37
+ }
38
+ /**
39
+ * Build a `renderIcon` slot for `<TreeRoot>` that uses `<FileIcon>` for both
40
+ * leaves and folders.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * <TreeRoot
45
+ * data={data}
46
+ * getItemName={(n) => n.data.name}
47
+ * renderIcon={createFileIconSlot({ getName: (n) => n.data.name })}
48
+ * />
49
+ * ```
50
+ */
51
+ declare function createFileIconSlot<T>({ getName, size, }: CreateFileIconSlotOptions<T>): TreeRowSlot<T>;
52
+
53
+ /**
54
+ * Lazy loader for `material-file-icons`. The package is an optional dependency:
55
+ * if it's installed in the consumer's tree, we render its rich VSCode-style
56
+ * SVGs; if not, the loader resolves to `null` and `<FileIcon>` falls back to a
57
+ * Lucide icon. Either way, ui-tools never throws or warns about a missing
58
+ * package.
59
+ *
60
+ * Resolution is cached per-process so we don't re-import on every render.
61
+ */
62
+ type GetIconFn = (name: string) => {
63
+ svg: string;
64
+ } | undefined;
65
+ declare function loadMaterialIcons(): Promise<GetIconFn | null>;
66
+ /** Synchronous accessor — returns null until `loadMaterialIcons` resolves. */
67
+ declare function getMaterialIconsSync(): GetIconFn | null;
68
+
69
+ export { type CreateFileIconSlotOptions, FileIcon, type FileIconProps, type FileIconSize, createFileIconSlot, getMaterialIconsSync, loadMaterialIcons };
@@ -0,0 +1,69 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { T as TreeNode, a as TreeRowSlot } from '../types-Cclwv4Hl.js';
3
+ import 'react';
4
+
5
+ type FileIconSize = 12 | 14 | 16 | 18 | 20 | 24 | 28 | 32 | 40 | 48 | 64;
6
+ interface FileIconProps {
7
+ /** File name (with extension) or folder name. */
8
+ name: string;
9
+ /** Render a folder icon instead of a file icon. */
10
+ isFolder?: boolean;
11
+ /** Folder open / closed state. Ignored when `isFolder` is false. */
12
+ isExpanded?: boolean;
13
+ /** Pixel size of the icon. */
14
+ size?: FileIconSize;
15
+ className?: string;
16
+ }
17
+ /**
18
+ * VSCode-style file icon.
19
+ *
20
+ * - Folders render Lucide `Folder` / `FolderOpen` (amber tint).
21
+ * - Files use `material-file-icons` if it's installed in the consumer; otherwise
22
+ * fall back to a neutral Lucide `File` icon.
23
+ *
24
+ * The optional dependency is loaded lazily on first render — there is zero
25
+ * bundle cost for consumers who never mount this component.
26
+ */
27
+ declare function FileIcon({ name, isFolder, isExpanded, size, className, }: FileIconProps): react_jsx_runtime.JSX.Element;
28
+
29
+ interface CreateFileIconSlotOptions<T> {
30
+ /**
31
+ * How to read the displayed name (with extension) for a node. Use the same
32
+ * accessor you pass to `<TreeRoot getItemName={...} />`.
33
+ */
34
+ getName: (node: TreeNode<T>) => string;
35
+ /** Pixel size for both file and folder icons. Default: 16. */
36
+ size?: FileIconSize;
37
+ }
38
+ /**
39
+ * Build a `renderIcon` slot for `<TreeRoot>` that uses `<FileIcon>` for both
40
+ * leaves and folders.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * <TreeRoot
45
+ * data={data}
46
+ * getItemName={(n) => n.data.name}
47
+ * renderIcon={createFileIconSlot({ getName: (n) => n.data.name })}
48
+ * />
49
+ * ```
50
+ */
51
+ declare function createFileIconSlot<T>({ getName, size, }: CreateFileIconSlotOptions<T>): TreeRowSlot<T>;
52
+
53
+ /**
54
+ * Lazy loader for `material-file-icons`. The package is an optional dependency:
55
+ * if it's installed in the consumer's tree, we render its rich VSCode-style
56
+ * SVGs; if not, the loader resolves to `null` and `<FileIcon>` falls back to a
57
+ * Lucide icon. Either way, ui-tools never throws or warns about a missing
58
+ * package.
59
+ *
60
+ * Resolution is cached per-process so we don't re-import on every render.
61
+ */
62
+ type GetIconFn = (name: string) => {
63
+ svg: string;
64
+ } | undefined;
65
+ declare function loadMaterialIcons(): Promise<GetIconFn | null>;
66
+ /** Synchronous accessor — returns null until `loadMaterialIcons` resolves. */
67
+ declare function getMaterialIconsSync(): GetIconFn | null;
68
+
69
+ export { type CreateFileIconSlotOptions, FileIcon, type FileIconProps, type FileIconSize, createFileIconSlot, getMaterialIconsSync, loadMaterialIcons };
@@ -0,0 +1,112 @@
1
+ import { __name } from '../chunk-CGILA3WO.mjs';
2
+ import { useState, useEffect } from 'react';
3
+ import { FolderOpen, Folder, File } from 'lucide-react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+ import { jsx } from 'react/jsx-runtime';
6
+
7
+ // src/tools/FileIcon/loader.ts
8
+ var cached;
9
+ var inflight = null;
10
+ async function loadMaterialIcons() {
11
+ if (cached !== void 0) return cached;
12
+ if (inflight) return inflight;
13
+ inflight = (async () => {
14
+ try {
15
+ const specifier = "material-file-icons";
16
+ const mod = await import(
17
+ /* @vite-ignore */
18
+ specifier
19
+ );
20
+ const fn = mod.getIcon ?? mod.default?.getIcon ?? null;
21
+ cached = fn;
22
+ return fn;
23
+ } catch {
24
+ cached = null;
25
+ return null;
26
+ } finally {
27
+ inflight = null;
28
+ }
29
+ })();
30
+ return inflight;
31
+ }
32
+ __name(loadMaterialIcons, "loadMaterialIcons");
33
+ function getMaterialIconsSync() {
34
+ return cached ?? null;
35
+ }
36
+ __name(getMaterialIconsSync, "getMaterialIconsSync");
37
+ function FileIcon({
38
+ name,
39
+ isFolder = false,
40
+ isExpanded = false,
41
+ size = 16,
42
+ className
43
+ }) {
44
+ const [svg, setSvg] = useState(() => {
45
+ if (isFolder) return null;
46
+ const fn = getMaterialIconsSync();
47
+ return fn ? fn(name)?.svg ?? null : null;
48
+ });
49
+ useEffect(() => {
50
+ if (isFolder) {
51
+ setSvg(null);
52
+ return;
53
+ }
54
+ let cancelled = false;
55
+ void loadMaterialIcons().then((fn) => {
56
+ if (cancelled) return;
57
+ setSvg(fn ? fn(name)?.svg ?? null : null);
58
+ });
59
+ return () => {
60
+ cancelled = true;
61
+ };
62
+ }, [isFolder, name]);
63
+ if (isFolder) {
64
+ const Icon = isExpanded ? FolderOpen : Folder;
65
+ return /* @__PURE__ */ jsx(
66
+ Icon,
67
+ {
68
+ className: cn("shrink-0 text-amber-500", className),
69
+ style: { width: size, height: size }
70
+ }
71
+ );
72
+ }
73
+ if (!svg) {
74
+ return /* @__PURE__ */ jsx(
75
+ File,
76
+ {
77
+ className: cn("shrink-0 text-muted-foreground/80", className),
78
+ style: { width: size, height: size }
79
+ }
80
+ );
81
+ }
82
+ return /* @__PURE__ */ jsx(
83
+ "span",
84
+ {
85
+ role: "img",
86
+ "aria-hidden": true,
87
+ className: cn("inline-block shrink-0", className),
88
+ style: { width: size, height: size },
89
+ dangerouslySetInnerHTML: { __html: svg }
90
+ }
91
+ );
92
+ }
93
+ __name(FileIcon, "FileIcon");
94
+ function createFileIconSlot({
95
+ getName,
96
+ size = 16
97
+ }) {
98
+ return ({ node, isFolder, isExpanded }) => /* @__PURE__ */ jsx(
99
+ FileIcon,
100
+ {
101
+ name: getName(node),
102
+ isFolder,
103
+ isExpanded,
104
+ size
105
+ }
106
+ );
107
+ }
108
+ __name(createFileIconSlot, "createFileIconSlot");
109
+
110
+ export { FileIcon, createFileIconSlot, getMaterialIconsSync, loadMaterialIcons };
111
+ //# sourceMappingURL=index.mjs.map
112
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tools/FileIcon/loader.ts","../../src/tools/FileIcon/FileIcon.tsx","../../src/tools/FileIcon/treeAdapter.tsx"],"names":["FileFallback","jsx"],"mappings":"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","file":"index.mjs","sourcesContent":["'use client';\n\n/**\n * Lazy loader for `material-file-icons`. The package is an optional dependency:\n * if it's installed in the consumer's tree, we render its rich VSCode-style\n * SVGs; if not, the loader resolves to `null` and `<FileIcon>` falls back to a\n * Lucide icon. Either way, ui-tools never throws or warns about a missing\n * package.\n *\n * Resolution is cached per-process so we don't re-import on every render.\n */\n\ntype GetIconFn = (name: string) => { svg: string } | undefined;\n\nlet cached: GetIconFn | null | undefined;\nlet inflight: Promise<GetIconFn | null> | null = null;\n\nexport async function loadMaterialIcons(): Promise<GetIconFn | null> {\n if (cached !== undefined) return cached;\n if (inflight) return inflight;\n\n inflight = (async () => {\n try {\n // Computed specifier so bundlers don't try to eagerly resolve it.\n const specifier = 'material-file-icons';\n const mod = (await import(/* @vite-ignore */ specifier)) as {\n getIcon?: GetIconFn;\n default?: { getIcon?: GetIconFn };\n };\n const fn = mod.getIcon ?? mod.default?.getIcon ?? null;\n cached = fn;\n return fn;\n } catch {\n cached = null;\n return null;\n } finally {\n inflight = null;\n }\n })();\n\n return inflight;\n}\n\n/** Synchronous accessor — returns null until `loadMaterialIcons` resolves. */\nexport function getMaterialIconsSync(): GetIconFn | null {\n return cached ?? null;\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\nimport { File as FileFallback, Folder, FolderOpen } from 'lucide-react';\nimport { cn } from '@djangocfg/ui-core/lib';\n\nimport { getMaterialIconsSync, loadMaterialIcons } from './loader';\n\nexport type FileIconSize = 12 | 14 | 16 | 18 | 20 | 24 | 28 | 32 | 40 | 48 | 64;\n\nexport interface FileIconProps {\n /** File name (with extension) or folder name. */\n name: string;\n /** Render a folder icon instead of a file icon. */\n isFolder?: boolean;\n /** Folder open / closed state. Ignored when `isFolder` is false. */\n isExpanded?: boolean;\n /** Pixel size of the icon. */\n size?: FileIconSize;\n className?: string;\n}\n\n/**\n * VSCode-style file icon.\n *\n * - Folders render Lucide `Folder` / `FolderOpen` (amber tint).\n * - Files use `material-file-icons` if it's installed in the consumer; otherwise\n * fall back to a neutral Lucide `File` icon.\n *\n * The optional dependency is loaded lazily on first render — there is zero\n * bundle cost for consumers who never mount this component.\n */\nexport function FileIcon({\n name,\n isFolder = false,\n isExpanded = false,\n size = 16,\n className,\n}: FileIconProps) {\n const [svg, setSvg] = useState<string | null>(() => {\n if (isFolder) return null;\n const fn = getMaterialIconsSync();\n return fn ? (fn(name)?.svg ?? null) : null;\n });\n\n useEffect(() => {\n if (isFolder) {\n setSvg(null);\n return;\n }\n let cancelled = false;\n void loadMaterialIcons().then((fn) => {\n if (cancelled) return;\n setSvg(fn ? (fn(name)?.svg ?? null) : null);\n });\n return () => {\n cancelled = true;\n };\n }, [isFolder, name]);\n\n if (isFolder) {\n const Icon = isExpanded ? FolderOpen : Folder;\n return (\n <Icon\n className={cn('shrink-0 text-amber-500', className)}\n style={{ width: size, height: size }}\n />\n );\n }\n\n if (!svg) {\n return (\n <FileFallback\n className={cn('shrink-0 text-muted-foreground/80', className)}\n style={{ width: size, height: size }}\n />\n );\n }\n\n return (\n <span\n role=\"img\"\n aria-hidden\n className={cn('inline-block shrink-0', className)}\n style={{ width: size, height: size }}\n dangerouslySetInnerHTML={{ __html: svg }}\n />\n );\n}\n\nexport default FileIcon;\n","'use client';\n\nimport type { TreeNode, TreeRowSlot } from '../Tree/types';\nimport { FileIcon, type FileIconSize } from './FileIcon';\n\nexport interface CreateFileIconSlotOptions<T> {\n /**\n * How to read the displayed name (with extension) for a node. Use the same\n * accessor you pass to `<TreeRoot getItemName={...} />`.\n */\n getName: (node: TreeNode<T>) => string;\n /** Pixel size for both file and folder icons. Default: 16. */\n size?: FileIconSize;\n}\n\n/**\n * Build a `renderIcon` slot for `<TreeRoot>` that uses `<FileIcon>` for both\n * leaves and folders.\n *\n * @example\n * ```tsx\n * <TreeRoot\n * data={data}\n * getItemName={(n) => n.data.name}\n * renderIcon={createFileIconSlot({ getName: (n) => n.data.name })}\n * />\n * ```\n */\nexport function createFileIconSlot<T>({\n getName,\n size = 16,\n}: CreateFileIconSlotOptions<T>): TreeRowSlot<T> {\n return ({ node, isFolder, isExpanded }) => (\n <FileIcon\n name={getName(node)}\n isFolder={isFolder}\n isExpanded={isExpanded}\n size={size}\n />\n );\n}\n"]}