@muten/shadcn 0.0.2 → 0.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muten/shadcn",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "shadcn-style component registry for muten - own-the-source parts you add with `muten add`.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,12 @@
1
- // TreeView host (muten Custom). inputs: { data } (nested nodes: { name, children?: [...] }). handlers: { select(path) }.
2
- // Folders expand/collapse; clicking selects and emits its "/path". Falls back to a sample tree when no data is
3
- // passed - edit SAMPLE or feed your own `data` (e.g. your project's file tree) for the real thing.
1
+ // TreeView host (muten Custom). TWO modes, chosen by whether you pass key names:
2
+ // nested (default): data = [{ name, children? }] - folders expand, a leaf emits its "/path".
3
+ // flat (pass keys): data = a FLAT list of records; `groupKey` buckets them into folders, `labelKey` is the
4
+ // visible text, `valueKey` is the payload - a leaf emits node[valueKey] (NOT a path). This is
5
+ // how you feed a `query` of rows straight in (e.g. tilesets) without nesting them in muten.
6
+ // `selected` (optional, controlled): highlights the matching leaf (its value in flat mode, its path in nested) and
7
+ // auto-expands the folder holding it - for the row your app auto-opens on load.
8
+ // Folders ONLY expand; only leaves emit `select`. inputs: { data, labelKey, valueKey, groupKey, selected }.
9
+ // handlers: { select(value) }. A `query` passed as data arrives as { loading, data:[…] } - unwrapped here.
4
10
  export function mount(el, inputs, handlers) {
5
11
  const FOLDER = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>';
6
12
  const FILE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>';
@@ -15,18 +21,55 @@ export function mount(el, inputs, handlers) {
15
21
  { name: 'package.json' },
16
22
  ];
17
23
 
18
- let data = Array.isArray(inputs.data) && inputs.data.length ? inputs.data : SAMPLE;
19
- let selected = '';
24
+ // A query arrives as { loading, data:[…] }; a plain list as the array; else nothing.
25
+ const rows = (d) => Array.isArray(d) ? d : (d && Array.isArray(d.data) ? d.data : null);
26
+
27
+ // Normalize either mode into the internal nested shape: folder = { name, children:[…] }, leaf = { name, value }.
28
+ const toTree = (ins) => {
29
+ const lk = ins.labelKey, vk = ins.valueKey, gk = ins.groupKey;
30
+ const list = rows(ins.data);
31
+ if ((lk || vk || gk) && list) {
32
+ const groups = new Map(); const loose = [];
33
+ for (const row of list) {
34
+ const label = lk ? row[lk] : (row && row.name != null ? row.name : String(row));
35
+ const value = vk ? row[vk] : label;
36
+ const leaf = { name: String(label), value };
37
+ const g = gk ? row[gk] : '';
38
+ if (g == null || g === '') { loose.push(leaf); continue; }
39
+ if (!groups.has(g)) groups.set(g, { name: String(g), children: [] });
40
+ groups.get(g).children.push(leaf);
41
+ }
42
+ return [...groups.values(), ...loose];
43
+ }
44
+ return list && list.length ? list : SAMPLE;
45
+ };
46
+
47
+ const idOf = (node, path) => Array.isArray(node.children) ? path : (node.value != null ? String(node.value) : path);
48
+
49
+ let data = toTree(inputs);
50
+ let selected = inputs.selected != null ? String(inputs.selected) : '';
20
51
  const expanded = new Set();
21
- for (const n of data) if (Array.isArray(n.children)) expanded.add('/' + n.name); // first level open
52
+
53
+ // expand the ancestor folders of the leaf whose id === selected; true if found in this subtree
54
+ const expandTo = (nodes, parent) => {
55
+ for (const node of nodes) {
56
+ const path = parent + '/' + node.name;
57
+ if (Array.isArray(node.children)) { if (expandTo(node.children, path)) { expanded.add(path); return true; } }
58
+ else if (idOf(node, path) === selected) return true;
59
+ }
60
+ return false;
61
+ };
62
+ const seed = () => { for (const n of data) if (Array.isArray(n.children)) expanded.add('/' + n.name); if (selected) expandTo(data, ''); };
63
+ seed();
22
64
 
23
65
  const build = (nodes, container, depth, parent) => {
24
66
  for (const node of nodes) {
25
67
  const path = parent + '/' + node.name;
26
68
  const isFolder = Array.isArray(node.children);
69
+ const id = idOf(node, path);
27
70
  const row = document.createElement('button'); row.type = 'button'; row.className = 'tree-row';
28
71
  row.style.paddingLeft = (depth * 16 + 6) + 'px';
29
- if (path === selected) row.classList.add('tree-row-selected');
72
+ if (!isFolder && id === selected) row.classList.add('tree-row-selected');
30
73
  const chev = document.createElement('span'); chev.className = 'tree-chevron' + (isFolder ? '' : ' tree-chevron-hidden');
31
74
  // lucide chevron-down (open) / chevron-right (closed)
32
75
  chev.innerHTML = isFolder ? '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="' + (expanded.has(path) ? 'm6 9 6 6 6-6' : 'm9 18 6-6-6-6') + '"/></svg>' : '';
@@ -34,8 +77,8 @@ export function mount(el, inputs, handlers) {
34
77
  const label = document.createElement('span'); label.className = 'tree-label'; label.textContent = node.name;
35
78
  row.appendChild(chev); row.appendChild(ic); row.appendChild(label);
36
79
  row.addEventListener('click', () => {
37
- if (isFolder) { expanded.has(path) ? expanded.delete(path) : expanded.add(path); }
38
- selected = path; if (handlers.select) handlers.select(path);
80
+ if (isFolder) { expanded.has(path) ? expanded.delete(path) : expanded.add(path); } // folders: expand only
81
+ else { selected = id; if (handlers.select) handlers.select(node.value != null ? node.value : path); } // leaves emit
39
82
  render();
40
83
  });
41
84
  container.appendChild(row);
@@ -45,5 +88,9 @@ export function mount(el, inputs, handlers) {
45
88
  const render = () => { el.innerHTML = ''; const root = document.createElement('div'); root.className = 'tree'; build(data, root, 0, ''); el.appendChild(root); };
46
89
 
47
90
  render();
48
- return (n) => { if (Array.isArray(n.data) && n.data.length) data = n.data; render(); };
91
+ return (n) => {
92
+ data = toTree(n);
93
+ if (n.selected != null) { selected = String(n.selected); if (selected) expandTo(data, ''); } // re-open to the controlled leaf
94
+ render();
95
+ };
49
96
  }
@@ -1,8 +1,14 @@
1
- # TreeView - a nested folder/file tree (expandable, selectable, icons + indent). The PAGE owns the selected path:
2
- # state { tree = [] : list<text> picked = "" : text } action onPick(p: text) mutates picked { picked.set(p) }
3
- # TreeView(data: tree, onSelect: onPick)
4
- # Pass your own nested data ({ name, children?: [...] }); with an empty list it renders a sample file tree you
5
- # can edit in src/components/TreeView.js. shadcn-for-muten · Custom · multilevel (Collapsible/Accordion are single-level).
6
- part TreeView(data: list, onSelect: action) {
7
- Custom TreeView inputs(data: $data) on(select: $onSelect) class("tree-view")
1
+ # Tree View - a file/folder tree (Custom). TWO modes:
2
+ # nested (default): pass a nested `data` of { name, children }; leaves emit their "/path". Leave the keys "".
3
+ # TreeView(data: fileTree, labelKey: "", valueKey: "", groupKey: "", selected: "", onSelect: pick)
4
+ # flat (pass keys): feed a FLAT list (e.g. a `query` of rows) and let the tree group + key it for you -
5
+ # groupKey buckets rows into folders, labelKey is the text, valueKey is the payload a leaf emits:
6
+ # state { tiles = query tilesets : list<Tile> tileUrl = "" : text }
7
+ # action pick(u: text) mutates tileUrl { tileUrl.set(u) }
8
+ # TreeView(data: tiles, labelKey: "tname", valueKey: "url", groupKey: "folder", selected: tileUrl, onSelect: pick)
9
+ # `selected` highlights + auto-opens that leaf. Folders only expand; only leaves emit select. (muten parts need
10
+ # every arg, so pass "" for the keys you don't use - an empty key = nested mode.)
11
+ # shadcn-for-muten · Custom · host JS is src/components/TreeView.js (copied by `muten add tree-view`).
12
+ part TreeView(data: list, labelKey: text, valueKey: text, groupKey: text, selected: text, onSelect: action) {
13
+ Custom TreeView inputs(data: $data, labelKey: $labelKey, valueKey: $valueKey, groupKey: $groupKey, selected: $selected) on(select: $onSelect) class("tree-view")
8
14
  }