@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 +128 -0
- package/bin/sync.mjs +15 -1
- package/dist/{chunk-VIQ5IYUZ.mjs → chunk-4PNVZETQ.mjs} +50 -17
- package/dist/chunk-4PNVZETQ.mjs.map +1 -0
- package/dist/index.d.mts +21 -31
- package/dist/index.d.ts +21 -31
- package/dist/index.js +402 -130
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +558 -319
- package/dist/index.mjs.map +1 -1
- package/dist/theme.js +49 -16
- package/dist/theme.js.map +1 -1
- package/dist/theme.mjs +1 -1
- package/package.json +2 -2
- package/skills/forest-alkemy-plus/SKILL.md +2 -2
- package/skills/forest-alkemy-plus/references/components.md +2 -2
- package/skills/forest-alkemy-plus/references/patterns.md +3 -3
- package/dist/chunk-VIQ5IYUZ.mjs.map +0 -1
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
|
-
|
|
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
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
1063
|
-
// disabled fg, bevel geometry retained. MUI's own
|
|
1064
|
-
// only swaps color and border-shorthands the bevel
|
|
1065
|
-
// with .Mui-selected at (0,2,0) — so this block sits
|
|
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:
|
|
1101
|
+
backgroundColor: "transparent",
|
|
1069
1102
|
color: (theme.vars || theme).palette.action.disabled,
|
|
1070
1103
|
borderStyle: "solid",
|
|
1071
|
-
borderColor:
|
|
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-
|
|
4024
|
+
//# sourceMappingURL=chunk-4PNVZETQ.mjs.map
|