cursedbelt-core 2.4.0 → 2.5.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.
@@ -35,6 +35,21 @@ export interface FileTreeNode {
35
35
  /** Stable across re-derivation — it is the React key, the selection value and the drag id. */
36
36
  id: string;
37
37
  name: string;
38
+ /**
39
+ * Drawn right after the name, a step quieter — a file's extension, `.md`.
40
+ *
41
+ * For a consumer whose extension is DERIVED rather than part of the stored name
42
+ * (`apps/desk` works a note's suffix out from its format): *"show the extensions in the
43
+ * file tree like vscode does. I need to be able to tell what type each file is"*. Quieter
44
+ * because the name is what you scan FOR and the suffix is what you scan BY.
45
+ *
46
+ * 🔴 It is part of the name everywhere a person types one: {@link matchTypeahead} and
47
+ * {@link filterFileTree} match `name + suffix`, because what is drawn and what can be
48
+ * typed are the same string or they are a bug (*"I typed .json but it said 'Nothing is
49
+ * called that'"*). A rename opens on `name + suffix` with only the name selected — VS
50
+ * Code's field — and hands back whatever was typed, suffix and all.
51
+ */
52
+ suffix?: string;
38
53
  kind: FileTreeNodeKind;
39
54
  /** Folders only. A folder with `[]` is empty; a file's is ignored. */
40
55
  children?: readonly FileTreeNode[];
@@ -300,6 +315,8 @@ export declare function accessMarkCopy(access: FileTreeAccess | undefined, copy?
300
315
  * Typeahead is what makes a long tree usable without a search box — `bud` jumps to
301
316
  * `budget-2026` — and wrapping is what stops it dying at the bottom of the list.
302
317
  */
318
+ /** The name as the row draws it — with its {@link FileTreeNode.suffix}. */
319
+ export declare function drawnName(node: FileTreeNode): string;
303
320
  export declare function matchTypeahead(rows: readonly FileTreeRow[], query: string, fromIndex: number): number;
304
321
  /**
305
322
  * The tree narrowed to what matches, with the folders on the way to a match kept.
@@ -308,6 +308,10 @@ export function accessMarkCopy(access, copy) {
308
308
  * Typeahead is what makes a long tree usable without a search box — `bud` jumps to
309
309
  * `budget-2026` — and wrapping is what stops it dying at the bottom of the list.
310
310
  */
311
+ /** The name as the row draws it — with its {@link FileTreeNode.suffix}. */
312
+ export function drawnName(node) {
313
+ return node.suffix ? `${node.name}${node.suffix}` : node.name;
314
+ }
311
315
  export function matchTypeahead(rows, query, fromIndex) {
312
316
  if (!query)
313
317
  return -1;
@@ -315,7 +319,7 @@ export function matchTypeahead(rows, query, fromIndex) {
315
319
  for (let step = 1; step <= rows.length; step += 1) {
316
320
  const index = (fromIndex + step + rows.length) % rows.length;
317
321
  const row = rows[index];
318
- if (row?.node.name.toLowerCase().startsWith(needle))
322
+ if (row && drawnName(row.node).toLowerCase().startsWith(needle))
319
323
  return index;
320
324
  }
321
325
  return -1;
@@ -332,7 +336,7 @@ export function filterFileTree(nodes, query) {
332
336
  if (!needle)
333
337
  return nodes;
334
338
  const keep = (node) => {
335
- const hit = node.name.toLowerCase().includes(needle);
339
+ const hit = drawnName(node).toLowerCase().includes(needle);
336
340
  if (hit)
337
341
  return node;
338
342
  const kids = childrenOf(node)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-core",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "Framework-agnostic core of the cursedbelt split — isomorphic domain logic. cwip below it, cursedbelt-server and cursedbelt (react) above it.",
@@ -13,6 +13,7 @@ import {
13
13
  resolveDrop,
14
14
  resolveKeyboardMove,
15
15
  describeDrop,
16
+ drawnName,
16
17
  } from './fileTreeModel.js';
17
18
 
18
19
  /**
@@ -526,3 +527,25 @@ describe('accessMarkCopy', () => {
526
527
  expect(accessMarkCopy(undefined)).toBe('');
527
528
  });
528
529
  });
530
+
531
+ /** `suffix` — a derived extension, drawn and matched as part of the name. */
532
+ describe('a suffix is part of the name a person types', () => {
533
+ const notes: FileTreeNode = { id: 'n', name: 'notes', suffix: '.md', kind: 'file' };
534
+ const nodes: FileTreeNode[] = [folder('docs', 'Docs', [notes]), { id: 'b', name: 'budget', kind: 'file' }];
535
+
536
+ test('drawnName carries it, and a node without one is its name', () => {
537
+ expect(drawnName(notes)).toBe('notes.md');
538
+ expect(drawnName({ id: 'b', name: 'budget', kind: 'file' })).toBe('budget');
539
+ });
540
+
541
+ test('typeahead matches through the dot', () => {
542
+ const rows = flattenFileTree(nodes, []);
543
+ expect(matchTypeahead(rows, 'notes.m', 0)).toBe(rows.findIndex((row) => row.node.id === 'n'));
544
+ });
545
+
546
+ test('a filter for the extension finds the file', () => {
547
+ const kept = filterFileTree(nodes, '.md');
548
+ expect(kept.map((node) => node.id)).toEqual(['docs']);
549
+ expect((kept[0]?.children ?? []).map((node) => node.id)).toEqual(['n']);
550
+ });
551
+ });
@@ -38,6 +38,21 @@ export interface FileTreeNode {
38
38
  /** Stable across re-derivation — it is the React key, the selection value and the drag id. */
39
39
  id: string;
40
40
  name: string;
41
+ /**
42
+ * Drawn right after the name, a step quieter — a file's extension, `.md`.
43
+ *
44
+ * For a consumer whose extension is DERIVED rather than part of the stored name
45
+ * (`apps/desk` works a note's suffix out from its format): *"show the extensions in the
46
+ * file tree like vscode does. I need to be able to tell what type each file is"*. Quieter
47
+ * because the name is what you scan FOR and the suffix is what you scan BY.
48
+ *
49
+ * 🔴 It is part of the name everywhere a person types one: {@link matchTypeahead} and
50
+ * {@link filterFileTree} match `name + suffix`, because what is drawn and what can be
51
+ * typed are the same string or they are a bug (*"I typed .json but it said 'Nothing is
52
+ * called that'"*). A rename opens on `name + suffix` with only the name selected — VS
53
+ * Code's field — and hands back whatever was typed, suffix and all.
54
+ */
55
+ suffix?: string;
41
56
  kind: FileTreeNodeKind;
42
57
  /** Folders only. A folder with `[]` is empty; a file's is ignored. */
43
58
  children?: readonly FileTreeNode[];
@@ -530,6 +545,11 @@ export function accessMarkCopy(
530
545
  * Typeahead is what makes a long tree usable without a search box — `bud` jumps to
531
546
  * `budget-2026` — and wrapping is what stops it dying at the bottom of the list.
532
547
  */
548
+ /** The name as the row draws it — with its {@link FileTreeNode.suffix}. */
549
+ export function drawnName(node: FileTreeNode): string {
550
+ return node.suffix ? `${node.name}${node.suffix}` : node.name;
551
+ }
552
+
533
553
  export function matchTypeahead(
534
554
  rows: readonly FileTreeRow[],
535
555
  query: string,
@@ -540,7 +560,7 @@ export function matchTypeahead(
540
560
  for (let step = 1; step <= rows.length; step += 1) {
541
561
  const index = (fromIndex + step + rows.length) % rows.length;
542
562
  const row = rows[index];
543
- if (row?.node.name.toLowerCase().startsWith(needle)) return index;
563
+ if (row && drawnName(row.node).toLowerCase().startsWith(needle)) return index;
544
564
  }
545
565
  return -1;
546
566
  }
@@ -559,7 +579,7 @@ export function filterFileTree(
559
579
  const needle = query.trim().toLowerCase();
560
580
  if (!needle) return nodes;
561
581
  const keep = (node: FileTreeNode): FileTreeNode | null => {
562
- const hit = node.name.toLowerCase().includes(needle);
582
+ const hit = drawnName(node).toLowerCase().includes(needle);
563
583
  if (hit) return node;
564
584
  const kids = childrenOf(node)
565
585
  .map(keep)