ply-css 1.6.0 → 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
@@ -44,6 +44,7 @@ Create a custom theme by defining a `data-theme` value and overriding `--ply-*`
44
44
  --ply-color-body: #1a1a1a;
45
45
  --ply-color-headings: #78350f;
46
46
  --ply-border-color: #fbbf24;
47
+ --ply-border-radius: 0.375rem;
47
48
  --ply-color-accent: #b45309;
48
49
  --ply-btn-default-bg: #b45309; /* Controls btn-primary + links */
49
50
  --ply-btn-default-bg-hover: #92400e;
@@ -166,6 +167,20 @@ For quick demos — gives you ply's classes and dark mode, but no Sass variables
166
167
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ply-css@1/dist/css/ply.min.css">
167
168
  ```
168
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
+
169
184
  ## File Structure
170
185
 
171
186
  - `src/scss/` — SCSS source (modern `@use`/`@forward` modules). **Use this when the project has a build step.**
@@ -173,6 +188,9 @@ For quick demos — gives you ply's classes and dark mode, but no Sass variables
173
188
  - `components/_variables.scss` — Spacing, font sizes, breakpoints, border radius
174
189
  - `components/_mixins.scss` — Button generator, clearfix, gradients, arrows, animations
175
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
176
194
  - `PLY.md` — Complete AI instruction file with class reference
177
195
  - `ply-classes.json` — Machine-readable class reference
178
196
  - `snippets/` — Copy-paste HTML examples
package/PLY.md CHANGED
@@ -27,6 +27,7 @@ Create a custom theme by defining a `data-theme` value and overriding `--ply-*`
27
27
  --ply-color-body: #1a1a1a;
28
28
  --ply-color-headings: #78350f;
29
29
  --ply-border-color: #fbbf24;
30
+ --ply-border-radius: 0.375rem;
30
31
  --ply-color-accent: #b45309;
31
32
  --ply-btn-default-bg: #b45309;
32
33
  --ply-btn-default-bg-hover: #92400e;
@@ -355,7 +356,7 @@ See `snippets/responsive-header.html` for a full working example.
355
356
  - **`border-top`**, **`border-right`**, **`border-bottom`**, **`border-left`** — Single-side borders.
356
357
  - **`border-thick`** — 3px solid border (all sides). Also `border-top-thick`, `border-right-thick`, `border-bottom-thick`, `border-left-thick`.
357
358
  - **`no-border`** — Remove all borders. Also `no-border-top`, `no-border-right`, `no-border-bottom`, `no-border-left`.
358
- - **`border-radius`** — Default border radius. `border-radius-lg` (0.75rem), `border-radius-xl` (1.5rem), `circle` (100%).
359
+ - **`border-radius`** — Uses `var(--ply-border-radius)` (default 0.25rem, themeable). `border-radius-lg`, `border-radius-xl`, `circle` (100%).
359
360
 
360
361
  ### Other Common Patterns
361
362
 
@@ -728,3 +729,39 @@ Ready-to-use HTML examples are in the `snippets/` directory:
728
729
  | `ply-core.min.css` | Grid, buttons, forms, nav, alerts, tables, typography, essential helpers | ~17KB |
729
730
  | `ply-essentials.min.css` | Grid, helpers, alignments, blocks only | ~7KB |
730
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
 
@@ -99,6 +99,7 @@ Override `--ply-*` CSS custom properties to create any theme:
99
99
  --ply-color-accent: #92400e; /* Icons, badges, section accents */
100
100
  --ply-btn-default-bg: #92400e; /* Primary button + links */
101
101
  --ply-btn-secondary-bg: #78350f; /* Secondary button */
102
+ --ply-border-radius: 0.375rem; /* Global border radius */
102
103
  --ply-btn-border-radius: 0.5rem; /* Button corner radius */
103
104
  --ply-font-body: Palatino, Georgia, serif;
104
105
  --ply-font-heading: Palatino, Georgia, serif;
@@ -131,6 +132,54 @@ For AI agents (Claude, Cursor, Copilot, Replit AI):
131
132
 
132
133
  ply is standalone — it should not be used alongside Tailwind, Bootstrap, or other CSS frameworks.
133
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
+
134
183
  ## Development
135
184
 
136
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
+ });