ply-css 1.6.1 → 1.7.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.
package/CLAUDE.md CHANGED
@@ -167,6 +167,20 @@ For quick demos — gives you ply's classes and dark mode, but no Sass variables
167
167
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ply-css@1/dist/css/ply.min.css">
168
168
  ```
169
169
 
170
+ ## Tree-Shaking
171
+
172
+ For production builds, purge unused ply classes with the built-in `ply-purge` CLI or PostCSS plugin. Typical result: ~5KB gzipped per page.
173
+
174
+ - **PostCSS plugin**: `require('ply-css/purge')` — recommended for build pipelines
175
+ - **CLI**: `npx ply-purge --css <file> --content '<glob>' -o <output>`
176
+ - Auto-safelists dynamically-toggled classes (`active`, `sort-asc`, responsive grid variants)
177
+
178
+ See PLY.md "Tree-Shaking" section for full usage examples.
179
+
180
+ ## Semantic Color Tokens
181
+
182
+ Use `--ply-color-error` and `--ply-color-success` for error/success states instead of hardcoding red/green. These tokens are used by `.input-error`, `.input-success`, `.error`, `.success`, `.required`, and multi-step form states. They're themeable via custom `data-theme` overrides.
183
+
170
184
  ## File Structure
171
185
 
172
186
  - `src/scss/` — SCSS source (modern `@use`/`@forward` modules). **Use this when the project has a build step.**
@@ -174,6 +188,9 @@ For quick demos — gives you ply's classes and dark mode, but no Sass variables
174
188
  - `components/_variables.scss` — Spacing, font sizes, breakpoints, border radius
175
189
  - `components/_mixins.scss` — Button generator, clearfix, gradients, arrows, animations
176
190
  - `dist/css/` — Compiled CSS bundles (for CDN or direct linking)
191
+ - `bin/ply-purge.js` — Standalone CLI for tree-shaking unused CSS
192
+ - `purge.js` — PostCSS plugin for tree-shaking in build pipelines
193
+ - `safelist.js` — Shared safelist for dynamically-toggled classes
177
194
  - `PLY.md` — Complete AI instruction file with class reference
178
195
  - `ply-classes.json` — Machine-readable class reference
179
196
  - `snippets/` — Copy-paste HTML examples
package/PLY.md CHANGED
@@ -729,3 +729,39 @@ Ready-to-use HTML examples are in the `snippets/` directory:
729
729
  | `ply-core.min.css` | Grid, buttons, forms, nav, alerts, tables, typography, essential helpers | ~17KB |
730
730
  | `ply-essentials.min.css` | Grid, helpers, alignments, blocks only | ~7KB |
731
731
  | `ply-helpers.min.css` | Helper utilities only | ~5KB |
732
+
733
+ ## Tree-Shaking (Purge Unused CSS)
734
+
735
+ For production builds, purge unused ply classes to get bundle sizes comparable
736
+ to Tailwind's JIT output (~5KB gzipped for a typical page).
737
+
738
+ ### PostCSS Plugin (recommended for build pipelines)
739
+
740
+ ```sh
741
+ npm install -D @fullhuman/postcss-purgecss
742
+ ```
743
+
744
+ ```js
745
+ // postcss.config.js
746
+ const plyPurge = require('ply-css/purge');
747
+
748
+ module.exports = {
749
+ plugins: [
750
+ plyPurge({ content: ['./src/**/*.{html,jsx,tsx,vue}'] }),
751
+ ],
752
+ };
753
+ ```
754
+
755
+ ### CLI (standalone or CI)
756
+
757
+ ```sh
758
+ npm install -D purgecss
759
+ npx ply-purge --css node_modules/ply-css/dist/css/ply.min.css \
760
+ --content 'src/**/*.{html,jsx,tsx}' \
761
+ -o public/ply.css
762
+ ```
763
+
764
+ The purge tool auto-safelists dynamically-toggled classes (`active`,
765
+ `sort-asc`, responsive grid variants) so they aren't incorrectly removed.
766
+ Extend the safelist with `--safelist <class>` (CLI) or `safelist` option
767
+ (PostCSS plugin).
package/README.md CHANGED
@@ -37,7 +37,7 @@ CSS frameworks were designed for humans reading documentation. But increasingly,
37
37
  - **Start semantic** — ply automatically styles `<nav>`, `<table>`, `<code>`, `<blockquote>`, `<details>`, `<dialog>`, and more. Start with what HTML gives you, then reach for classes when you need them.
38
38
  - **AI-native** — ships with `PLY.md` (AI instruction file) and `ply-classes.json` (machine-readable class reference). Class names are predictable: `.alert-blue`, `.btn-sm`, `.unit-50`.
39
39
  - **Accessible by default** — `:focus-visible` outlines on all interactive elements (including `<summary>` and legacy components), `prefers-reduced-motion`, `prefers-color-scheme` dark mode, semantic HTML styling, WCAG AA contrast in both light and dark themes. Published [VPAT 2.5](https://plycss.com/docs/vpat) documenting conformance against all WCAG 2.1 Level A and AA criteria.
40
- - **Small footprint** — ~21KB gzipped (full), ~17KB (core). No JavaScript runtime, no build step, no tree-shaking.
40
+ - **Small footprint** — ~21KB gzipped (full), ~17KB (core), ~5KB with tree-shaking. No JavaScript runtime, no build step required.
41
41
  - **Ratio-based grid** — think in percentages, not arbitrary columns. `unit-50` is 50%, `unit-33` is 33%. Responsive prefixes: `tablet-unit-*`, `phone-unit-*`.
42
42
  - **Custom theming** — override `--ply-*` CSS custom properties to create any theme. Light and dark modes built in.
43
43
 
@@ -132,6 +132,54 @@ For AI agents (Claude, Cursor, Copilot, Replit AI):
132
132
 
133
133
  ply is standalone — it should not be used alongside Tailwind, Bootstrap, or other CSS frameworks.
134
134
 
135
+ ## Tree-Shaking (Purge Unused CSS)
136
+
137
+ ply ships all 457 classes in every bundle. For production, you can purge unused
138
+ classes to get Tailwind-level bundle sizes (~5KB gzipped for a typical page).
139
+
140
+ ### PostCSS Plugin
141
+
142
+ The recommended approach for projects with an existing PostCSS pipeline:
143
+
144
+ ```sh
145
+ npm install -D @fullhuman/postcss-purgecss
146
+ ```
147
+
148
+ ```js
149
+ // postcss.config.js
150
+ const plyPurge = require('ply-css/purge');
151
+
152
+ module.exports = {
153
+ plugins: [
154
+ plyPurge({ content: ['./src/**/*.{html,jsx,tsx,vue}'] }),
155
+ ],
156
+ };
157
+ ```
158
+
159
+ ### CLI
160
+
161
+ For standalone use or CI pipelines:
162
+
163
+ ```sh
164
+ npm install -D purgecss
165
+ npx ply-purge --css node_modules/ply-css/dist/css/ply.min.css \
166
+ --content 'src/**/*.{html,jsx,tsx}' \
167
+ -o public/ply.css
168
+ ```
169
+
170
+ ### Results
171
+
172
+ | Scenario | Before | After (gzipped) | Reduction |
173
+ |----------|--------|-----------------|-----------|
174
+ | Single page (card) | 21 KB | ~5 KB | ~75% |
175
+ | All snippets (13 pages) | 21 KB | ~11 KB | ~48% |
176
+ | Real-world app page | 21 KB | ~5.5 KB | ~74% |
177
+
178
+ The purge tool auto-safelists dynamically-toggled classes (`active`,
179
+ `sort-asc`, etc.) and responsive grid variants so they aren't incorrectly
180
+ removed. Pass additional safelisted classes with `--safelist` (CLI) or the
181
+ `safelist` option (PostCSS).
182
+
135
183
  ## Development
136
184
 
137
185
  ```sh
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ply-purge — Standalone CLI to tree-shake unused ply CSS.
5
+ *
6
+ * Usage:
7
+ * npx ply-purge --css dist/css/ply.min.css --content 'src/**\/*.html' -o dist/css/ply.purged.css
8
+ * npx ply-purge --css node_modules/ply-css/dist/css/ply.min.css --content 'app/**\/*.tsx' --content 'components/**\/*.tsx'
9
+ *
10
+ * Options:
11
+ * --css <file> Input CSS file (required)
12
+ * --content <glob> Content glob to scan for used classes (repeatable, required)
13
+ * -o, --output <file> Output file (defaults to <input>.purged.css)
14
+ * --safelist <class> Additional class names to keep (repeatable)
15
+ * --json Print stats as JSON instead of human-readable
16
+ * -h, --help Show help
17
+ */
18
+
19
+ "use strict";
20
+
21
+ const fs = require("fs");
22
+ const path = require("path");
23
+
24
+ const args = process.argv.slice(2);
25
+
26
+ if (args.includes("-h") || args.includes("--help") || args.length === 0) {
27
+ console.log(`
28
+ ply-purge — Tree-shake unused ply CSS
29
+
30
+ Usage:
31
+ ply-purge --css <file> --content <glob> [-o <output>]
32
+
33
+ Options:
34
+ --css <file> Input CSS file to purge
35
+ --content <glob> Glob pattern for content files (repeatable)
36
+ -o, --output <file> Output file (default: <name>.purged.css)
37
+ --safelist <class> Extra class to preserve (repeatable)
38
+ --json Output stats as JSON
39
+ -h, --help Show this help
40
+
41
+ Examples:
42
+ ply-purge --css node_modules/ply-css/dist/css/ply.min.css \\
43
+ --content 'src/**/*.{html,jsx,tsx}' \\
44
+ -o public/ply.css
45
+
46
+ ply-purge --css dist/css/ply.min.css \\
47
+ --content 'app/**/*.tsx' --content 'components/**/*.tsx'
48
+ `.trim());
49
+ process.exit(0);
50
+ }
51
+
52
+ function parseArgs(argv) {
53
+ const result = { content: [], safelist: [], json: false };
54
+ let i = 0;
55
+ while (i < argv.length) {
56
+ const arg = argv[i];
57
+ if (arg === "--css" && argv[i + 1]) {
58
+ result.css = argv[++i];
59
+ } else if (arg === "--content" && argv[i + 1]) {
60
+ result.content.push(argv[++i]);
61
+ } else if ((arg === "-o" || arg === "--output") && argv[i + 1]) {
62
+ result.output = argv[++i];
63
+ } else if (arg === "--safelist" && argv[i + 1]) {
64
+ result.safelist.push(argv[++i]);
65
+ } else if (arg === "--json") {
66
+ result.json = true;
67
+ }
68
+ i++;
69
+ }
70
+ return result;
71
+ }
72
+
73
+ async function main() {
74
+ const opts = parseArgs(args);
75
+
76
+ if (!opts.css) {
77
+ console.error("Error: --css is required");
78
+ process.exit(1);
79
+ }
80
+ if (opts.content.length === 0) {
81
+ console.error("Error: at least one --content glob is required");
82
+ process.exit(1);
83
+ }
84
+
85
+ let PurgeCSS;
86
+ try {
87
+ PurgeCSS = require("purgecss").PurgeCSS;
88
+ } catch {
89
+ console.error(
90
+ "ply-purge requires purgecss.\n" +
91
+ "Install it: npm install -D purgecss"
92
+ );
93
+ process.exit(1);
94
+ }
95
+
96
+ const { safelistClasses, safelistPatterns } = require("../safelist");
97
+
98
+ const cssPath = path.resolve(opts.css);
99
+ if (!fs.existsSync(cssPath)) {
100
+ console.error(`Error: CSS file not found: ${cssPath}`);
101
+ process.exit(1);
102
+ }
103
+
104
+ const rawCss = fs.readFileSync(cssPath, "utf8");
105
+ const originalSize = Buffer.byteLength(rawCss, "utf8");
106
+
107
+ const purger = new PurgeCSS();
108
+ const results = await purger.purge({
109
+ content: opts.content,
110
+ css: [{ raw: rawCss }],
111
+ safelist: {
112
+ standard: [...safelistClasses, ...opts.safelist],
113
+ deep: safelistPatterns,
114
+ greedy: [/^:root$/, /^html$/, /^body$/],
115
+ },
116
+ defaultExtractor: (content) => {
117
+ const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [];
118
+ const innerMatches = content.match(/[^<>"'`\s.(){}[\]#,;]+/g) || [];
119
+ return [...new Set([...broadMatches, ...innerMatches])];
120
+ },
121
+ });
122
+
123
+ if (results.length === 0) {
124
+ console.error("Error: PurgeCSS returned no results");
125
+ process.exit(1);
126
+ }
127
+
128
+ const purgedCss = results[0].css;
129
+ const purgedSize = Buffer.byteLength(purgedCss, "utf8");
130
+
131
+ const outputPath = opts.output
132
+ ? path.resolve(opts.output)
133
+ : cssPath.replace(/(\.\w+)$/, ".purged$1");
134
+
135
+ const outputDir = path.dirname(outputPath);
136
+ if (!fs.existsSync(outputDir)) {
137
+ fs.mkdirSync(outputDir, { recursive: true });
138
+ }
139
+
140
+ fs.writeFileSync(outputPath, purgedCss, "utf8");
141
+
142
+ const reduction = (((originalSize - purgedSize) / originalSize) * 100).toFixed(1);
143
+
144
+ if (opts.json) {
145
+ console.log(
146
+ JSON.stringify({
147
+ input: opts.css,
148
+ output: path.relative(process.cwd(), outputPath),
149
+ originalBytes: originalSize,
150
+ purgedBytes: purgedSize,
151
+ reductionPercent: parseFloat(reduction),
152
+ })
153
+ );
154
+ } else {
155
+ console.log(`ply-purge complete`);
156
+ console.log(` Input: ${opts.css} (${formatBytes(originalSize)})`);
157
+ console.log(
158
+ ` Output: ${path.relative(process.cwd(), outputPath)} (${formatBytes(purgedSize)})`
159
+ );
160
+ console.log(` Reduction: ${reduction}% removed`);
161
+ }
162
+ }
163
+
164
+ function formatBytes(bytes) {
165
+ if (bytes < 1024) return `${bytes} B`;
166
+ return `${(bytes / 1024).toFixed(1)} KB`;
167
+ }
168
+
169
+ main().catch((err) => {
170
+ console.error(err.message || err);
171
+ process.exit(1);
172
+ });
@@ -25,13 +25,16 @@
25
25
  --ply-color-link-hover: #0347c6;
26
26
  --ply-color-link-hover: color-mix(in oklch, var(--ply-btn-default-bg), black 15%);
27
27
  --ply-color-focus: #0f62fe;
28
- --ply-color-field-bg: #f4f4f4;
29
- --ply-color-input-border: #8d8d8d;
30
- --ply-color-input-bg: #f4f4f4;
31
- --ply-color-code-bg: #f4f4f4;
32
- --ply-color-code-border: #e0e0e0;
33
- --ply-color-table-border: #e0e0e0;
34
- --ply-color-table-stripped: #f4f4f4;
28
+ --ply-color-field-bg: var(--ply-bg-surface-alt);
29
+ --ply-color-input-border: var(--ply-border-strong);
30
+ --ply-color-input-bg: var(--ply-bg-surface-alt);
31
+ --ply-color-error: var(--ply-red-1);
32
+ --ply-color-success: var(--ply-green-1);
33
+ --ply-color-code-bg: var(--ply-bg-surface-alt);
34
+ --ply-color-code-border: var(--ply-border-color);
35
+ --ply-color-table-border: var(--ply-border-color);
36
+ --ply-color-table-striped: #f4f4f4;
37
+ --ply-color-table-stripped: var(--ply-color-table-striped);
35
38
  --ply-color-table-hovered: #e8e8e8;
36
39
  --ply-color-accent: #0353e9;
37
40
  --ply-btn-default-bg: #0353e9;
@@ -46,14 +49,14 @@
46
49
  --ply-btn-secondary-bg-active: color-mix(in oklch, var(--ply-btn-secondary-bg), black 25%);
47
50
  --ply-border-radius: 0.25rem;
48
51
  --ply-btn-border-radius: 2rem;
49
- --ply-nav-bg: #ffffff;
50
- --ply-nav-color: #161616;
51
- --ply-nav-border: #161616;
52
+ --ply-nav-bg: var(--ply-bg-body);
53
+ --ply-nav-color: var(--ply-color-body);
54
+ --ply-nav-border: var(--ply-color-body);
52
55
  --ply-nav-hover: #e8e8e8;
53
56
  --ply-nav-active-color: #525252;
54
- --ply-layer-0: #ffffff;
55
- --ply-layer-1: #f4f4f4;
56
- --ply-layer-2: #e0e0e0;
57
+ --ply-layer-0: var(--ply-bg-body);
58
+ --ply-layer-1: var(--ply-bg-surface-alt);
59
+ --ply-layer-2: var(--ply-bg-muted);
57
60
  --ply-layer-3: #c6c6c6;
58
61
  --ply-shadow-1: 0 1px 3px rgba(0, 0, 0, 0.08);
59
62
  --ply-shadow-2: 0 2px 8px rgba(0, 0, 0, 0.1);
@@ -150,13 +153,8 @@
150
153
  --ply-color-link-hover: #619bff;
151
154
  --ply-color-link-hover: color-mix(in oklch, #4589ff, white 15%);
152
155
  --ply-color-focus: #0f62fe;
153
- --ply-color-field-bg: #262626;
154
- --ply-color-input-border: #6f6f6f;
155
- --ply-color-input-bg: #262626;
156
- --ply-color-code-bg: #262626;
157
- --ply-color-code-border: #393939;
158
- --ply-color-table-border: #393939;
159
- --ply-color-table-stripped: #1c1c1c;
156
+ --ply-color-table-striped: #1c1c1c;
157
+ --ply-color-table-stripped: var(--ply-color-table-striped);
160
158
  --ply-color-table-hovered: #353535;
161
159
  --ply-color-accent: #4589ff;
162
160
  --ply-btn-default-bg: #0f62fe;
@@ -172,14 +170,8 @@
172
170
  --ply-btn-default-color: #fff;
173
171
  --ply-btn-secondary-color: #161616;
174
172
  --ply-btn-ghost-color: #4589ff;
175
- --ply-nav-bg: #161616;
176
- --ply-nav-color: #f4f4f4;
177
- --ply-nav-border: #f4f4f4;
178
173
  --ply-nav-hover: #353535;
179
174
  --ply-nav-active-color: #a8a8a8;
180
- --ply-layer-0: #161616;
181
- --ply-layer-1: #262626;
182
- --ply-layer-2: #393939;
183
175
  --ply-layer-3: #525252;
184
176
  --ply-shadow-1: 0 1px 3px rgba(0, 0, 0, 0.2);
185
177
  --ply-shadow-2: 0 2px 8px rgba(0, 0, 0, 0.3);
@@ -266,13 +258,8 @@
266
258
  --ply-color-link-hover: #619bff;
267
259
  --ply-color-link-hover: color-mix(in oklch, #4589ff, white 15%);
268
260
  --ply-color-focus: #0f62fe;
269
- --ply-color-field-bg: #262626;
270
- --ply-color-input-border: #6f6f6f;
271
- --ply-color-input-bg: #262626;
272
- --ply-color-code-bg: #262626;
273
- --ply-color-code-border: #393939;
274
- --ply-color-table-border: #393939;
275
- --ply-color-table-stripped: #1c1c1c;
261
+ --ply-color-table-striped: #1c1c1c;
262
+ --ply-color-table-stripped: var(--ply-color-table-striped);
276
263
  --ply-color-table-hovered: #353535;
277
264
  --ply-color-accent: #4589ff;
278
265
  --ply-btn-default-bg: #0f62fe;
@@ -288,14 +275,8 @@
288
275
  --ply-btn-default-color: #fff;
289
276
  --ply-btn-secondary-color: #161616;
290
277
  --ply-btn-ghost-color: #4589ff;
291
- --ply-nav-bg: #161616;
292
- --ply-nav-color: #f4f4f4;
293
- --ply-nav-border: #f4f4f4;
294
278
  --ply-nav-hover: #353535;
295
279
  --ply-nav-active-color: #a8a8a8;
296
- --ply-layer-0: #161616;
297
- --ply-layer-1: #262626;
298
- --ply-layer-2: #393939;
299
280
  --ply-layer-3: #525252;
300
281
  --ply-shadow-1: 0 1px 3px rgba(0, 0, 0, 0.2);
301
282
  --ply-shadow-2: 0 2px 8px rgba(0, 0, 0, 0.3);
@@ -1234,15 +1215,15 @@ meter {
1234
1215
  .req,
1235
1216
  .required {
1236
1217
  font-weight: normal;
1237
- color: var(--ply-red-1);
1218
+ color: var(--ply-color-error);
1238
1219
  }
1239
1220
 
1240
1221
  .error {
1241
- color: var(--ply-red-1);
1222
+ color: var(--ply-color-error);
1242
1223
  }
1243
1224
 
1244
1225
  .success {
1245
- color: var(--ply-green-1);
1226
+ color: var(--ply-color-success);
1246
1227
  }
1247
1228
 
1248
1229
  .text-xs {
@@ -2587,8 +2568,9 @@ table.table-stroked th {
2587
2568
  border-bottom: 1px solid var(--ply-color-table-border, #ccc);
2588
2569
  }
2589
2570
 
2571
+ table.table-striped tbody tr:nth-child(odd) td,
2590
2572
  table.table-stripped tbody tr:nth-child(odd) td {
2591
- background: var(--ply-color-table-stripped, #f8f8f8);
2573
+ background: var(--ply-color-table-striped, var(--ply-color-table-stripped, #f8f8f8));
2592
2574
  }
2593
2575
 
2594
2576
  table.table-hovered tbody tr:hover td {
@@ -3716,16 +3698,16 @@ input.input-error,
3716
3698
  textarea.input-error,
3717
3699
  select.input-error,
3718
3700
  .input-error {
3719
- border-color: #de2c3b;
3720
- box-shadow: 0 0 0 2px rgba(222, 44, 59, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2) inset;
3701
+ border-color: var(--ply-color-error);
3702
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--ply-color-error) 30%, transparent), 0 1px 2px rgba(0, 0, 0, 0.2) inset;
3721
3703
  }
3722
3704
 
3723
3705
  input.input-success,
3724
3706
  textarea.input-success,
3725
3707
  select.input-success,
3726
3708
  .input-success {
3727
- border-color: #1a7a32;
3728
- box-shadow: 0 0 0 2px rgba(26, 122, 50, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2) inset;
3709
+ border-color: var(--ply-color-success);
3710
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--ply-color-success) 30%, transparent), 0 1px 2px rgba(0, 0, 0, 0.2) inset;
3729
3711
  }
3730
3712
 
3731
3713
  input.input-gray,