@matteoaliano/forest-ui 1.2.0 → 1.2.2

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/bin/dense.mjs ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ // Dense mode for skill reference docs.
3
+ //
4
+ // ONE transform, lossless: strip the repeated "Import" column from every
5
+ // GFM table. The import path `@matteoaliano/forest-ui` is invariant and
6
+ // stated once at the top of the file, so `import { X } from "..."` on every
7
+ // row is pure redundancy. Names that AREN'T derivable from the component
8
+ // name (family members like TableHead/TableBody, type exports like
9
+ // AppShellNavItem) are folded into the Component cell so nothing is lost.
10
+ //
11
+ // Files with no Import column pass through byte-identical (never inflated).
12
+ //
13
+ // Used as a library by sync.mjs (densifies the shipped plugin/dogfood copies)
14
+ // and as a CLI for inspection:
15
+ // node bin/dense.mjs <file.md> # print dense version to stdout
16
+ // node bin/dense.mjs <file.md> --write # write references/dense/<file>.md
17
+ // node bin/dense.mjs <file.md> --stats # print savings to stderr
18
+ // node bin/dense.mjs <file.md> --check # assert nothing was lost
19
+
20
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
21
+ import { basename, dirname, join } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ // Split a GFM table row on unescaped pipes (unions use `\|` as a literal pipe).
25
+ const cells = (line) =>
26
+ line
27
+ .trim()
28
+ .replace(/^\||\|$/g, "")
29
+ .split(/(?<!\\)\|/)
30
+ .map((c) => c.trim());
31
+
32
+ const bareNames = (str) =>
33
+ [...str.matchAll(/`([^`]+)`/g)].map((m) => m[1].replace(/^type\s+/, ""));
34
+
35
+ export function densify(src) {
36
+ let didStrip = false;
37
+
38
+ const transformTable = (block) => {
39
+ const rows = block.map(cells);
40
+ const importCol = rows[0].findIndex((h) => h === "Import");
41
+ if (importCol === -1) return block; // no Import column → untouched
42
+ didStrip = true;
43
+
44
+ const nameCol = 0; // Component/Hook name is always the first column
45
+ const out = rows.map((cols, i) => {
46
+ if (i > 1) {
47
+ const importNames = (cols[importCol].match(/import\s*\{([^}]*)\}/)?.[1] ?? "")
48
+ .split(",")
49
+ .map((n) => n.trim())
50
+ .filter(Boolean);
51
+ const shown = new Set(bareNames(cols[nameCol]));
52
+ const extras = importNames.filter((n) => !shown.has(n.replace(/^type\s+/, "")));
53
+ if (extras.length) cols[nameCol] += ` — also: ${extras.join(", ")}`;
54
+ }
55
+ return cols.filter((_, c) => c !== importCol);
56
+ });
57
+ return out.map((cols) => `| ${cols.join(" | ")} |`);
58
+ };
59
+
60
+ // Walk the file, transforming each contiguous table block in place.
61
+ const result = [];
62
+ let table = [];
63
+ const flush = () => {
64
+ if (table.length) result.push(...transformTable(table));
65
+ table = [];
66
+ };
67
+ for (const line of src.split("\n")) {
68
+ if (line.trim().startsWith("|")) table.push(line);
69
+ else {
70
+ flush();
71
+ result.push(line);
72
+ }
73
+ }
74
+ flush();
75
+
76
+ // Insert the invariant note once, right after the H1 — only when we stripped
77
+ // something, so files with no Import column stay byte-identical.
78
+ const h1 = result.findIndex((l) => l.startsWith("# "));
79
+ if (didStrip && h1 !== -1) {
80
+ result.splice(
81
+ h1 + 1,
82
+ 0,
83
+ "",
84
+ "> **Dense.** Import column dropped — every name imports from `@matteoaliano/forest-ui`. `— also:` lists extra names/types that import alongside.",
85
+ );
86
+ }
87
+
88
+ return result.join("\n");
89
+ }
90
+
91
+ // ── CLI ──
92
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
93
+ const [file, ...flags] = process.argv.slice(2);
94
+ if (!file) {
95
+ console.error("usage: node bin/dense.mjs <file.md> [--write|--stats|--check]");
96
+ process.exit(1);
97
+ }
98
+ const src = readFileSync(file, "utf8");
99
+ const dense = densify(src);
100
+
101
+ if (flags.includes("--check")) {
102
+ const imported = [...src.matchAll(/import\s*\{([^}]*)\}/g)]
103
+ .flatMap((m) => m[1].split(","))
104
+ .map((n) => n.trim().replace(/^type\s+/, ""))
105
+ .filter(Boolean);
106
+ const lost = imported.filter((n) => !dense.includes(n));
107
+ if (lost.length) {
108
+ console.error(`FAIL: names lost in dense output: ${lost.join(", ")}`);
109
+ process.exit(1);
110
+ }
111
+ console.error(`ok: all ${imported.length} imported names preserved`);
112
+ } else if (flags.includes("--stats")) {
113
+ const est = (s) => Math.round(s.length / 4);
114
+ const pct = (1 - dense.length / src.length) * 100;
115
+ console.error(
116
+ `${basename(file)}: ${src.length} → ${dense.length} chars ` +
117
+ `(~${est(src)} → ~${est(dense)} tokens, -${pct.toFixed(0)}%)`,
118
+ );
119
+ } else if (flags.includes("--write")) {
120
+ const dir = join(dirname(file), "dense");
121
+ mkdirSync(dir, { recursive: true });
122
+ const dest = join(dir, basename(file));
123
+ writeFileSync(dest, dense);
124
+ console.error(`wrote ${dest}`);
125
+ } else {
126
+ process.stdout.write(dense);
127
+ }
128
+ }
package/bin/sync.mjs CHANGED
@@ -14,6 +14,7 @@
14
14
  import { mkdirSync, readdirSync, cpSync, readFileSync, writeFileSync } from "node:fs";
15
15
  import { resolve, dirname } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
+ import { densify } from "./dense.mjs";
17
18
 
18
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
20
  const skillsDir = resolve(__dirname, "..", "skills");
@@ -47,13 +48,26 @@ const skillFolders = readdirSync(skillsDir, { withFileTypes: true })
47
48
  .filter((d) => d.isDirectory())
48
49
  .map((d) => d.name);
49
50
 
51
+ // Densify every .md in the shipped copies. Sources under skills/ stay full
52
+ // and human-readable; consumers transparently get the token-efficient form
53
+ // (same filenames, so SKILL.md links are unchanged). No-op on files without
54
+ // an Import column, so it never inflates anything.
55
+ const densifyTree = (dir) => {
56
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
57
+ const p = resolve(dir, entry.name);
58
+ if (entry.isDirectory()) densifyTree(p);
59
+ else if (entry.name.endsWith(".md")) writeFileSync(p, densify(readFileSync(p, "utf-8")));
60
+ }
61
+ };
62
+
50
63
  let synced = 0;
51
64
  for (const target of targets) {
52
65
  for (const folder of skillFolders) {
53
66
  const dest = resolve(target, folder);
54
67
  mkdirSync(dest, { recursive: true });
55
68
  cpSync(resolve(skillsDir, folder), dest, { recursive: true });
56
- console.log(` ✅ ${dest.replace(repoRoot + "/", "")}/`);
69
+ densifyTree(dest);
70
+ console.log(` ✅ ${dest.replace(repoRoot + "/", "")}/ (dense)`);
57
71
  synced++;
58
72
  }
59
73
  }
@@ -447,6 +447,20 @@ function buildComponents(tokens2) {
447
447
  borderTopRightRadius: radius4.md,
448
448
  opacity: 0.75
449
449
  },
450
+ // Unified disabled treatment across the button family (Button,
451
+ // IconButton, Fab, ButtonGroup, ToggleButton): transparent face,
452
+ // disabled border, disabled text — matching the outlined
453
+ // IconButton reference. Contained's fill is dropped and the bevel
454
+ // geometry re-asserted (MUI's outlined disabled `border: 1px`
455
+ // shorthand would otherwise flatten it).
456
+ "&.Mui-disabled": {
457
+ backgroundColor: "transparent",
458
+ borderColor: border4.disabled,
459
+ borderTopWidth: 1,
460
+ borderRightWidth: 1,
461
+ borderLeftWidth: 1,
462
+ borderBottomWidth: 4
463
+ },
450
464
  "@media (prefers-reduced-motion: reduce)": {
451
465
  transition: "none",
452
466
  "&::before": { display: "none" }
@@ -488,6 +502,13 @@ function buildComponents(tokens2) {
488
502
  "&.MuiButtonGroup-contained .MuiButtonGroup-grouped:not(.Mui-disabled)": {
489
503
  borderColor: fg4.tertiary
490
504
  },
505
+ // Disabled grouped children: MUI's per-position grouped-contained
506
+ // border wins over the child Button's own .Mui-disabled rule on the
507
+ // first/middle buttons — pin the unified disabled border here so the
508
+ // whole group matches the standalone disabled Button.
509
+ "&.MuiButtonGroup-contained .MuiButtonGroup-grouped.Mui-disabled": {
510
+ borderColor: border4.disabled
511
+ },
491
512
  // outlined group → the outlined-Button look (outlinedFill), including
492
513
  // its lightened hover sweep (the forced-contained children would
493
514
  // otherwise sweep at full currentColor strength).
@@ -552,6 +573,13 @@ function buildComponents(tokens2) {
552
573
  },
553
574
  "&.ForestIconButton": {
554
575
  ...bevel3D,
576
+ // Square 32×32 with border-box so the faux-3D bevel (1px top + 4px
577
+ // bottom, 1px left/right) is absorbed rather than added on top —
578
+ // otherwise it rendered a 37×34 control instead of matching the
579
+ // standalone Button's 32px height.
580
+ height: 32,
581
+ width: 32,
582
+ boxSizing: "border-box",
555
583
  boxShadow: tokenShadows.xs,
556
584
  "&:focus-visible": {
557
585
  boxShadow: focusRings4.brandShadowXs
@@ -589,15 +617,13 @@ function buildComponents(tokens2) {
589
617
  filter: "brightness(0.97)"
590
618
  }
591
619
  },
592
- // Disabled parity with the Button variants: MUI IconButton's own
593
- // .Mui-disabled (transparent bg + action.disabled icon) ties with the
594
- // variant faces at (0,2,0) and loses on insertion order pin the
595
- // disabled treatment one class higher.
596
- "&.ForestIconButton-contained.Mui-disabled": {
597
- backgroundColor: (theme.vars || theme).palette.action.disabledBackground,
598
- color: (theme.vars || theme).palette.action.disabled
599
- },
600
- "&.ForestIconButton-outlined.Mui-disabled": {
620
+ // Unified disabled treatment for both variants: transparent face,
621
+ // disabled border, disabled icon bevel geometry retained. This is
622
+ // the reference the rest of the button family matches. The two-class
623
+ // selector out-specifies the single-class variant faces so contained's
624
+ // fill is dropped.
625
+ "&.ForestIconButton.Mui-disabled": {
626
+ backgroundColor: "transparent",
601
627
  color: (theme.vars || theme).palette.action.disabled,
602
628
  borderColor: border4.disabled
603
629
  }
@@ -664,6 +690,13 @@ function buildComponents(tokens2) {
664
690
  "&:focus-visible": {
665
691
  boxShadow: focusRings4.brandShadowSm
666
692
  },
693
+ // Same unified disabled treatment as Button/IconButton: drop the
694
+ // contained fill for a transparent, disabled-bordered face.
695
+ "&.Mui-disabled": {
696
+ backgroundColor: "transparent",
697
+ borderColor: border4.disabled,
698
+ color: (theme.vars || theme).palette.action.disabled
699
+ },
667
700
  "@media (prefers-reduced-motion: reduce)": {
668
701
  transition: "none",
669
702
  "&::before": { display: "none" }
@@ -1059,16 +1092,16 @@ function buildComponents(tokens2) {
1059
1092
  borderTopRightRadius: radius4.md
1060
1093
  }
1061
1094
  },
1062
- // Disabled parity with the disabled contained Button: greyed face +
1063
- // disabled fg, bevel geometry retained. MUI's own .Mui-disabled rule
1064
- // only swaps color and border-shorthands the bevel away — and ties
1065
- // with .Mui-selected at (0,2,0) — so this block sits AFTER selected
1066
- // to win both.
1095
+ // Disabled parity with the rest of the button family: transparent
1096
+ // face, disabled border + fg, bevel geometry retained. MUI's own
1097
+ // .Mui-disabled rule only swaps color and border-shorthands the bevel
1098
+ // away — and ties with .Mui-selected at (0,2,0) — so this block sits
1099
+ // AFTER selected to win both.
1067
1100
  "&.Mui-disabled": {
1068
- backgroundColor: (theme.vars || theme).palette.action.disabledBackground,
1101
+ backgroundColor: "transparent",
1069
1102
  color: (theme.vars || theme).palette.action.disabled,
1070
1103
  borderStyle: "solid",
1071
- borderColor: fg4.tertiary,
1104
+ borderColor: border4.disabled,
1072
1105
  borderTopWidth: 1,
1073
1106
  borderRightWidth: 1,
1074
1107
  borderLeftWidth: 1,
@@ -3988,4 +4021,4 @@ export {
3988
4021
  forestTheme,
3989
4022
  forestThemeOptions
3990
4023
  };
3991
- //# sourceMappingURL=chunk-VIQ5IYUZ.mjs.map
4024
+ //# sourceMappingURL=chunk-4PNVZETQ.mjs.map