@codenhub/styles 0.8.0 → 0.9.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.
Files changed (76) hide show
  1. package/README.es.md +9 -9
  2. package/README.md +8 -8
  3. package/README.pt-BR.md +9 -9
  4. package/dist/aesthetics/chunky-tile.css +1 -1
  5. package/dist/aesthetics/cyber.css +1 -1
  6. package/dist/aesthetics/glass.css +1 -1
  7. package/dist/aesthetics/index.css +1 -1
  8. package/dist/aesthetics/neobrutalism.css +1 -1
  9. package/dist/aesthetics/pixel.css +1 -1
  10. package/dist/aesthetics/sketch.css +1 -1
  11. package/dist/cli.js +231 -62
  12. package/dist/components.css +49 -1
  13. package/dist/index.css +49 -1
  14. package/dist/native.css +49 -1
  15. package/dist/purge/classes.json +1 -0
  16. package/dist/purge/index.css +50 -0
  17. package/dist/theme.css +1 -1
  18. package/docs/changelog/0.8.0.md +1 -1
  19. package/docs/changelog/0.9.0.md +40 -0
  20. package/docs/changelog/index.md +1 -0
  21. package/docs/es/index.md +2 -2
  22. package/docs/es/integrating/tailwind.md +2 -1
  23. package/docs/es/integrating/without-a-bundler.md +60 -31
  24. package/docs/es/setup.md +4 -3
  25. package/docs/es/usage/aesthetics.md +2 -2
  26. package/docs/es/usage/content-and-layout.md +3 -1
  27. package/docs/es/usage/customizing.md +30 -16
  28. package/docs/es/usage/forms.md +2 -2
  29. package/docs/es/usage/index.md +2 -2
  30. package/docs/es/usage/solo.md +2 -2
  31. package/docs/es/usage/theming.md +2 -2
  32. package/docs/es/usage/tooltips.md +2 -2
  33. package/docs/index.md +1 -1
  34. package/docs/integrating/tailwind.md +1 -0
  35. package/docs/integrating/without-a-bundler.md +59 -30
  36. package/docs/pt-BR/index.md +2 -2
  37. package/docs/pt-BR/integrating/tailwind.md +2 -1
  38. package/docs/pt-BR/integrating/without-a-bundler.md +60 -31
  39. package/docs/pt-BR/setup.md +4 -3
  40. package/docs/pt-BR/usage/aesthetics.md +2 -2
  41. package/docs/pt-BR/usage/content-and-layout.md +3 -1
  42. package/docs/pt-BR/usage/customizing.md +30 -16
  43. package/docs/pt-BR/usage/forms.md +2 -2
  44. package/docs/pt-BR/usage/index.md +2 -2
  45. package/docs/pt-BR/usage/solo.md +2 -2
  46. package/docs/pt-BR/usage/theming.md +2 -2
  47. package/docs/pt-BR/usage/tooltips.md +2 -2
  48. package/docs/setup.md +3 -2
  49. package/docs/usage/aesthetics.md +1 -1
  50. package/docs/usage/content-and-layout.md +2 -0
  51. package/docs/usage/customizing.md +29 -15
  52. package/docs/usage/forms.md +1 -1
  53. package/docs/usage/index.md +1 -1
  54. package/docs/usage/solo.md +1 -1
  55. package/docs/usage/theming.md +1 -1
  56. package/docs/usage/tooltips.md +1 -1
  57. package/llms-full.txt +145 -61
  58. package/llms.txt +3 -2
  59. package/package.json +12 -10
  60. package/tw/aesthetics/chunky-tile.css +4 -1
  61. package/tw/aesthetics/cyber.css +4 -1
  62. package/tw/aesthetics/glass.css +5 -0
  63. package/tw/aesthetics/neobrutalism.css +4 -1
  64. package/tw/aesthetics/pixel.css +3 -0
  65. package/tw/aesthetics/sketch.css +18 -0
  66. package/tw/box.css +21 -15
  67. package/tw/components/button.rules.css +1 -1
  68. package/tw/components/feedback.rules.css +22 -3
  69. package/tw/components/form.rules.css +65 -35
  70. package/tw/components/surface.rules.css +8 -5
  71. package/tw/components/tooltip.rules.css +39 -34
  72. package/tw/elevation.css +8 -8
  73. package/tw/native.css +4 -1
  74. package/tw/theme.css +1 -1
  75. package/tw/utilities/content.css +74 -54
  76. package/tw/utilities/layout.css +7 -5
package/dist/cli.js CHANGED
@@ -1,15 +1,213 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
2
  import { existsSync, readFileSync } from "node:fs";
4
3
  import * as fs from "node:fs/promises";
5
4
  import path from "node:path";
6
- import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { fileURLToPath } from "node:url";
7
6
  import { parseArgs } from "node:util";
7
+ // src/cli/purge.ts
8
+ const GROUPS = /* @__PURE__ */ new Set([
9
+ "@media",
10
+ "@supports",
11
+ "@layer",
12
+ "@container",
13
+ "@scope",
14
+ "@starting-style"
15
+ ]);
16
+ const IDENT = /^(?:\\[0-9a-fA-F]{1,6}\s?|\\.|[\w-]|\P{ASCII})+/u;
17
+ const ANY_OF = /* @__PURE__ */ new Set([
18
+ "is",
19
+ "where",
20
+ "has"
21
+ ]);
22
+ /** Returns the index just past the string or comment that starts at `index`, or `index` when none does. */
23
+ function skipInert(css, index) {
24
+ const char = css[index];
25
+ if (char === "\"" || char === "'") {
26
+ for (let i = index + 1; i < css.length; i++) if (css[i] === "\\") i++;
27
+ else if (css[i] === char) return i + 1;
28
+ return css.length;
29
+ }
30
+ if (char === "/" && css[index + 1] === "*") {
31
+ const end = css.indexOf("*/", index + 2);
32
+ return end === -1 ? css.length : end + 2;
33
+ }
34
+ return index;
35
+ }
36
+ /** Returns the index of the first `stop` character at bracket depth zero from `from`, or `end` when there is none. */
37
+ function findTop(css, from, end, stop) {
38
+ let depth = 0;
39
+ for (let i = from; i < end; i++) {
40
+ const next = skipInert(css, i);
41
+ if (next !== i) {
42
+ i = next - 1;
43
+ continue;
44
+ }
45
+ const char = css[i];
46
+ if (char === "\\") i++;
47
+ else if (depth === 0 && stop.includes(char)) return i;
48
+ else if (char === "(" || char === "[" || char === "{") depth++;
49
+ else if (char === ")" || char === "]" || char === "}") depth--;
50
+ }
51
+ return end;
52
+ }
53
+ function splitTop(text, separator) {
54
+ const parts = [];
55
+ let from = 0;
56
+ while (from <= text.length) {
57
+ const at = findTop(text, from, text.length, separator);
58
+ parts.push(text.slice(from, at));
59
+ from = at + 1;
60
+ }
61
+ return parts;
62
+ }
63
+ function parse(css, start = 0, end = css.length) {
64
+ const nodes = [];
65
+ let index = start;
66
+ while (index < end) {
67
+ let comment = index;
68
+ while (comment < end && /\s/.test(css[comment])) comment++;
69
+ if (css.startsWith("/*", comment)) {
70
+ const after = skipInert(css, comment);
71
+ nodes.push({
72
+ kind: "statement",
73
+ text: css.slice(index, after)
74
+ });
75
+ index = after;
76
+ continue;
77
+ }
78
+ const open = findTop(css, index, end, "{;");
79
+ if (open >= end || css[open] === ";") {
80
+ nodes.push({
81
+ kind: "statement",
82
+ text: css.slice(index, Math.min(open + 1, end))
83
+ });
84
+ index = open + 1;
85
+ continue;
86
+ }
87
+ const close = findTop(css, open + 1, end, "}");
88
+ const prelude = css.slice(index, open);
89
+ const atRule = /^\s*(@[\w-]+)/.exec(prelude)?.[1];
90
+ if (atRule !== void 0 && GROUPS.has(atRule)) nodes.push({
91
+ kind: "group",
92
+ prelude,
93
+ children: parse(css, open + 1, close)
94
+ });
95
+ else nodes.push({
96
+ kind: "block",
97
+ prelude,
98
+ body: css.slice(open + 1, close),
99
+ isRule: atRule === void 0
100
+ });
101
+ index = close + 1;
102
+ }
103
+ return nodes;
104
+ }
105
+ function serialize(nodes) {
106
+ let css = "";
107
+ for (const node of nodes) if (node.kind === "statement") css += node.text;
108
+ else if (node.kind === "group") css += `${node.prelude}{${serialize(node.children)}}`;
109
+ else css += `${node.prelude}{${node.body}}`;
110
+ return css;
111
+ }
112
+ /** Writes the tree one declaration to a line, for `--no-minify`. */
113
+ function format(nodes, indent = "") {
114
+ let css = "";
115
+ for (const node of nodes) if (node.kind === "statement") css += `${indent}${node.text.trim()}\n`;
116
+ else if (node.kind === "group") css += `${indent}${node.prelude.trim()} {\n${format(node.children, `${indent} `)}${indent}}\n`;
117
+ else if (node.isRule) {
118
+ const selectors = splitTop(node.prelude, ",").map((selector) => selector.trim());
119
+ const declarations = splitTop(node.body, ";").filter((declaration) => declaration.trim() !== "");
120
+ css += `${indent}${selectors.join(`,\n${indent}`)} {\n`;
121
+ css += declarations.map((declaration) => `${indent} ${declaration.trim()};\n`).join("");
122
+ css += `${indent}}\n`;
123
+ } else css += `${indent}${node.prelude.trim()} { ${node.body} }\n`;
124
+ return css;
125
+ }
126
+ function unescapeIdent(raw) {
127
+ return raw.replace(/\\([0-9a-fA-F]{1,6})\s?|\\(.)/g, (_, hex, char) => {
128
+ if (hex === void 0) return char;
129
+ const code = Number.parseInt(hex, 16);
130
+ return code === 0 || code >= 55296 && code <= 57343 || code > 1114111 ? "�" : String.fromCodePoint(code);
131
+ });
132
+ }
133
+ /**
134
+ * Says whether a selector can match in markup whose classes are all in `used`.
135
+ * Every class the selector requires has to be there. A class inside `:not()` is
136
+ * never required, and of the selectors inside `:is()`, `:where()`, and `:has()`
137
+ * one is enough. `onClass`, when given, is told every class name it reads.
138
+ */
139
+ function canMatch(selector, used, onClass) {
140
+ let isMatch = true;
141
+ for (let i = 0; i < selector.length; i++) {
142
+ const next = skipInert(selector, i);
143
+ if (next !== i) {
144
+ i = next - 1;
145
+ continue;
146
+ }
147
+ const char = selector[i];
148
+ if (char === "\\") i++;
149
+ else if (char === "[") i = findTop(selector, i + 1, selector.length, "]");
150
+ else if (char === ".") {
151
+ const raw = IDENT.exec(selector.slice(i + 1))?.[0];
152
+ if (raw !== void 0) {
153
+ const name = unescapeIdent(raw);
154
+ onClass?.(name);
155
+ isMatch &&= used.has(name);
156
+ i += raw.length;
157
+ }
158
+ } else if (char === ":") {
159
+ const pseudo = /^::?([\w-]+)\(/.exec(selector.slice(i));
160
+ if (pseudo !== null) {
161
+ const open = i + pseudo[0].length;
162
+ const close = findTop(selector, open, selector.length, ")");
163
+ const branches = splitTop(selector.slice(open, close), ",");
164
+ if (ANY_OF.has(pseudo[1])) {
165
+ const results = branches.map((branch) => canMatch(branch, used, onClass));
166
+ isMatch &&= results.includes(true);
167
+ } else if (onClass !== void 0) for (const branch of branches) canMatch(branch, used, onClass);
168
+ i = close;
169
+ }
170
+ }
171
+ if (!isMatch && onClass === void 0) return false;
172
+ }
173
+ return isMatch;
174
+ }
175
+ function isLayer(node) {
176
+ return node.kind === "group" && /^\s*@layer\b/.test(node.prelude);
177
+ }
178
+ function isEmptyLayer(node) {
179
+ return node.kind === "group" && isLayer(node) && node.children.every(isEmptyLayer);
180
+ }
181
+ /**
182
+ * Returns the tree without the rules that cannot match `used`. A selector list
183
+ * keeps only its selectors that can. A `@layer` block emptied this way stays,
184
+ * because where it first stands fixes the layer's place in the cascade, unless
185
+ * it is all that is left of a `@media` or `@supports` block: those hold a
186
+ * variant's rules, which come after the layer's first block.
187
+ */
188
+ function purge(nodes, used) {
189
+ const kept = [];
190
+ for (const node of nodes) if (node.kind === "group") {
191
+ const children = purge(node.children, used);
192
+ if (isLayer(node) ? true : children.some((child) => !isEmptyLayer(child))) kept.push({
193
+ ...node,
194
+ children
195
+ });
196
+ } else if (node.kind === "block" && node.isRule) {
197
+ const selectors = splitTop(node.prelude, ",").filter((selector) => canMatch(selector, used));
198
+ if (selectors.length > 0) kept.push({
199
+ ...node,
200
+ prelude: selectors.join(",")
201
+ });
202
+ } else kept.push(node);
203
+ return kept;
204
+ }
8
205
  // src/cli/run.ts
9
- const BIN = "codenhub-styles";
206
+ const BIN = "chs";
10
207
  const MIN_NODE_MAJOR = 24;
11
- const TAILWIND_PACKAGES = ["@tailwindcss/cli", "tailwindcss"];
12
- const TW_AESTHETIC_EXPORT = /^\.\/tw\/aesthetics\/([^/]+)$/;
208
+ const AESTHETIC_EXPORT = /^\.\/aesthetics\/([^/]+)$/;
209
+ const PURGE_SOURCE = "dist/purge/index.css";
210
+ const PURGE_CLASSES = "dist/purge/classes.json";
13
211
  const USAGE = `Usage: ${BIN} build --content <glob> [--content <glob> ...] --output <file> [options]
14
212
 
15
213
  Builds a StyleKit stylesheet holding only the classes your templates use.
@@ -22,7 +220,6 @@ Options:
22
220
  --no-minify Write readable CSS instead of minified CSS.
23
221
  --help Show this help.
24
222
  --version Show the version.`;
25
- const POLYFILLS_NONE = 0;
26
223
  async function run(argv, io) {
27
224
  try {
28
225
  const parsed = parseCommandLine(argv);
@@ -90,47 +287,46 @@ function parseFlags(argv) {
90
287
  }
91
288
  async function build(options, io) {
92
289
  assertNodeVersion(io.nodeVersion);
93
- const tailwind = loadTailwind(io);
94
290
  const validAesthetics = readAestheticNames();
95
291
  const unknown = options.aesthetics.find((name) => !validAesthetics.includes(name));
96
292
  if (unknown !== void 0) throw new Error(`unknown aesthetic "${unknown}"; choose from ${validAesthetics.join(", ")}.`);
97
- await Promise.all(options.contentGlobs.map((pattern) => assertMatches(pattern, io.cwd)));
293
+ const files = new Set((await Promise.all(options.contentGlobs.map((pattern) => findFiles(pattern, io.cwd)))).flat());
294
+ const contents = [];
295
+ for (const file of files) contents.push(await fs.readFile(file, "utf8"));
296
+ const droppable = new Set(JSON.parse(await readPackageFile(PURGE_CLASSES)));
297
+ const found = new Set(contents.flatMap((content) => findClasses(content, droppable)));
298
+ const used = { has: (name) => found.has(name) || !droppable.has(name) };
299
+ const stylesheets = await Promise.all([PURGE_SOURCE, ...options.aesthetics.map((name) => resolveExport(`./aesthetics/${name}`))].map(readPackageFile));
300
+ const write = options.shouldMinify ? serialize : format;
301
+ const css = stylesheets.map((stylesheet) => write(purge(parse(stylesheet), used))).join("");
98
302
  const output = path.resolve(io.cwd, options.output);
99
- const input = [
100
- `@import ${cssString(resolveExport("./tw"))};`,
101
- ...options.aesthetics.map((name) => `@import ${cssString(resolveExport(`./tw/aesthetics/${name}`))};`),
102
- ...options.contentGlobs.map((pattern) => `@source ${cssString(toCssPath(path.resolve(io.cwd, pattern)))};`)
103
- ].join("\n");
303
+ const temporary = `${output}.${process.pid}.tmp`;
104
304
  try {
105
- const css = await compileStylesheet(tailwind, input, io.cwd, options.shouldMinify);
106
305
  await fs.mkdir(path.dirname(output), { recursive: true });
107
- await fs.writeFile(output, css);
306
+ await fs.writeFile(temporary, css);
307
+ await fs.rename(temporary, output);
108
308
  } catch (error) {
109
- throw new Error(`Tailwind failed to build the stylesheet: ${error instanceof Error ? error.message : String(error)}`);
309
+ await fs.rm(temporary, { force: true }).catch(() => void 0);
310
+ throw new Error(`cannot write the stylesheet: ${error instanceof Error ? error.message : String(error)}`);
110
311
  }
111
312
  return output;
112
313
  }
113
- async function assertMatches(pattern, cwd) {
314
+ /** Returns the files a `--content` glob matches, as absolute paths, and fails when there are none. */
315
+ async function findFiles(pattern, cwd) {
316
+ const files = [];
114
317
  for await (const entry of fs.glob(pattern, {
115
318
  cwd,
116
319
  withFileTypes: true
117
- })) if (entry.isFile()) return;
118
- throw new Error(`--content "${pattern}" matches no file in ${cwd}; check the path, or run from the project root.`);
119
- }
120
- async function compileStylesheet(tailwind, input, cwd, shouldMinify) {
121
- const compiler = await tailwind.compile(input, {
122
- base: cwd,
123
- from: path.join(cwd, "codenhub-styles.css"),
124
- polyfills: POLYFILLS_NONE,
125
- onDependency() {}
126
- });
127
- const scanner = new tailwind.Scanner({ sources: compiler.sources });
128
- const css = compiler.build(scanner.scan());
129
- return shouldMinify ? tailwind.optimize(css, { minify: true }).code : css;
320
+ })) if (entry.isFile()) files.push(path.resolve(entry.parentPath, entry.name));
321
+ if (files.length === 0) throw new Error(`--content "${pattern}" matches no file in ${cwd}; check the path, or run from the project root.`);
322
+ return files;
323
+ }
324
+ function findClasses(content, known) {
325
+ return content.split(/[^\w:-]+/).flatMap((word) => known.has(word) ? [word] : word.split(":")).filter((word) => known.has(word));
130
326
  }
131
327
  function readAestheticNames() {
132
328
  const { exports } = readManifest();
133
- return Object.keys(exports).map((subpath) => TW_AESTHETIC_EXPORT.exec(subpath)?.[1]).filter((name) => name !== void 0).sort();
329
+ return Object.keys(exports).map((subpath) => AESTHETIC_EXPORT.exec(subpath)?.[1]).filter((name) => name !== void 0).sort();
134
330
  }
135
331
  /** Returns the nearest folder at or above `from` that holds a `package.json`. */
136
332
  function findPackageRoot(from) {
@@ -144,45 +340,18 @@ function readManifest() {
144
340
  return JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"));
145
341
  }
146
342
  function resolveExport(subpath) {
147
- const target = readManifest().exports[subpath].import;
148
- return pathToFileURL(path.resolve(packageRoot, target)).href;
343
+ return readManifest().exports[subpath].import;
344
+ }
345
+ function readPackageFile(relativePath) {
346
+ return fs.readFile(path.resolve(packageRoot, relativePath), "utf8");
149
347
  }
150
348
  function assertNodeVersion(nodeVersion) {
151
349
  if (Number.parseInt(nodeVersion, 10) < MIN_NODE_MAJOR) throw new Error(`this command needs Node.js ${MIN_NODE_MAJOR} or newer; this is Node.js ${nodeVersion}.`);
152
350
  }
153
- /** Loads Tailwind's compiler from where `@tailwindcss/cli` is installed, after checking both Tailwind packages are. */
154
- function loadTailwind(io) {
155
- const manifests = TAILWIND_PACKAGES.map((name) => resolveManifest(name, io));
156
- const missing = TAILWIND_PACKAGES.filter((_, index) => manifests[index] === void 0);
157
- if (missing.length > 0) throw new Error(`this command needs ${missing.join(" and ")}, which ${missing.length > 1 ? "are" : "is"} not installed. Run: npm install --save-dev ${missing.join(" ")}`);
158
- const requireFromCli = createRequire(manifests[0]);
159
- const { compile, optimize } = requireFromCli("@tailwindcss/node");
160
- const { Scanner } = requireFromCli("@tailwindcss/oxide");
161
- return {
162
- compile,
163
- optimize,
164
- Scanner
165
- };
166
- }
167
- function resolveManifest(name, io) {
168
- try {
169
- return io.resolvePackage(`${name}/package.json`);
170
- } catch (error) {
171
- if (error.code === "MODULE_NOT_FOUND") return;
172
- throw error;
173
- }
174
- }
175
- function toCssPath(filePath) {
176
- return filePath.split(path.sep).join("/");
177
- }
178
- function cssString(value) {
179
- return `"${value.replace(/["\\]/g, "\\$&")}"`;
180
- }
181
351
  // src/cli/cli.ts
182
352
  process.exitCode = await run(process.argv.slice(2), {
183
353
  cwd: process.cwd(),
184
354
  nodeVersion: process.versions.node,
185
- resolvePackage: createRequire(import.meta.url).resolve,
186
355
  stdout: (line) => process.stdout.write(`${line}\n`),
187
356
  stderr: (line) => process.stderr.write(`${line}\n`)
188
357
  });