ply-css 1.3.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 (75) hide show
  1. package/CLAUDE.md +176 -0
  2. package/LICENSE +22 -0
  3. package/PLY.md +646 -0
  4. package/README.md +170 -0
  5. package/dist/css/ply-core.css +6175 -0
  6. package/dist/css/ply-core.min.css +1 -0
  7. package/dist/css/ply-essentials.min.css +1 -0
  8. package/dist/css/ply-helpers.min.css +1 -0
  9. package/dist/css/ply.css +7429 -0
  10. package/dist/css/ply.min.css +1 -0
  11. package/dist/css/styles.css +7432 -0
  12. package/dist/css/styles.min.css +1 -0
  13. package/llms-full.txt +834 -0
  14. package/llms.txt +34 -0
  15. package/package.json +70 -0
  16. package/ply-classes.json +2625 -0
  17. package/snippets/accessible-drag-and-drop.html +122 -0
  18. package/snippets/card.html +58 -0
  19. package/snippets/contact-form.html +49 -0
  20. package/snippets/custom-theme.html +280 -0
  21. package/snippets/dashboard.html +77 -0
  22. package/snippets/data-table.html +64 -0
  23. package/snippets/login-page.html +45 -0
  24. package/snippets/navbar-page.html +39 -0
  25. package/snippets/notifications.html +63 -0
  26. package/snippets/pricing-cards.html +95 -0
  27. package/snippets/responsive-header.html +98 -0
  28. package/snippets/starter-page.html +782 -0
  29. package/snippets/two-column-layout.html +40 -0
  30. package/src/scss/_ply-core-components.scss +32 -0
  31. package/src/scss/_ply.scss +47 -0
  32. package/src/scss/components/_accordion.scss +73 -0
  33. package/src/scss/components/_alignments.scss +64 -0
  34. package/src/scss/components/_autocomplete.scss +28 -0
  35. package/src/scss/components/_blocks-responsive.scss +30 -0
  36. package/src/scss/components/_blocks.scss +39 -0
  37. package/src/scss/components/_buttons.scss +452 -0
  38. package/src/scss/components/_colors.scss +447 -0
  39. package/src/scss/components/_container-queries.scss +35 -0
  40. package/src/scss/components/_cursors.scss +24 -0
  41. package/src/scss/components/_dialog-patterns.scss +176 -0
  42. package/src/scss/components/_dropdown.scss +68 -0
  43. package/src/scss/components/_filterbox.scss +57 -0
  44. package/src/scss/components/_flexible-embed.scss +19 -0
  45. package/src/scss/components/_forms.scss +450 -0
  46. package/src/scss/components/_grid.scss +210 -0
  47. package/src/scss/components/_helpers-core.scss +357 -0
  48. package/src/scss/components/_helpers.scss +466 -0
  49. package/src/scss/components/_labels.scss +105 -0
  50. package/src/scss/components/_livesearch.scss +233 -0
  51. package/src/scss/components/_loader.scss +24 -0
  52. package/src/scss/components/_media-queries.scss +9 -0
  53. package/src/scss/components/_mixins.scss +387 -0
  54. package/src/scss/components/_modal.scss +73 -0
  55. package/src/scss/components/_multi-step-form.scss +190 -0
  56. package/src/scss/components/_navigation-responsive.scss +63 -0
  57. package/src/scss/components/_navigation.scss +592 -0
  58. package/src/scss/components/_notifications.scss +185 -0
  59. package/src/scss/components/_prettyprint.scss +86 -0
  60. package/src/scss/components/_print.scss +74 -0
  61. package/src/scss/components/_progress.scss +32 -0
  62. package/src/scss/components/_reset.scss +365 -0
  63. package/src/scss/components/_rtl.scss +213 -0
  64. package/src/scss/components/_table-interactive.scss +110 -0
  65. package/src/scss/components/_tables.scss +52 -0
  66. package/src/scss/components/_themes.scss +6 -0
  67. package/src/scss/components/_tooltip.scss +35 -0
  68. package/src/scss/components/_typography.scss +565 -0
  69. package/src/scss/components/_upload.scss +19 -0
  70. package/src/scss/components/_variables.scss +129 -0
  71. package/src/scss/ply-core.scss +1 -0
  72. package/src/scss/ply-essentials.scss +15 -0
  73. package/src/scss/ply-helpers.scss +11 -0
  74. package/src/scss/ply-iso.scss +1 -0
  75. package/src/scss/styles.scss +9 -0
package/llms-full.txt ADDED
@@ -0,0 +1,834 @@
1
+ # plycss (ply) — Complete Documentation
2
+
3
+ > ply is a ratio-based, AI-ready CSS framework with built-in dark mode, WCAG 2.1 AA accessibility, and a small footprint (~20KB gzipped). 421 utility classes, 60+ CSS custom properties, 13 auto-styled semantic elements. No JavaScript. No build step. Install via npm as `plycss` or use the CDN.
4
+
5
+ ---
6
+
7
+ ## Install
8
+
9
+ ### npm + Sass (recommended)
10
+
11
+ ```sh
12
+ npm install ply-css
13
+ ```
14
+
15
+ ```scss
16
+ @use "ply-css/src/scss/ply" as *;
17
+
18
+ // Or import individual modules
19
+ @use "ply-css/src/scss/components/colors" as colors;
20
+ @use "ply-css/src/scss/components/variables" as variables;
21
+ @use "ply-css/src/scss/components/mixins" as mixins;
22
+
23
+ .custom {
24
+ color: colors.$color-blue;
25
+ background: colors.$color-blue-pastel;
26
+ @include mixins.border-bottom-radius(variables.$border-radius);
27
+ }
28
+ ```
29
+
30
+ SCSS source: `src/scss/components/_colors.scss` (full color palette), `_variables.scss` (spacing, fonts, breakpoints), `_mixins.scss` (button generator, gradients, animations).
31
+
32
+ ### CDN (prototyping only)
33
+
34
+ ```html
35
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ply-css@1/dist/css/ply.min.css">
36
+ ```
37
+
38
+ Core bundle (no labels, dropdowns, loaders, print):
39
+
40
+ ```html
41
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ply-css@1/dist/css/ply-core.min.css">
42
+ ```
43
+
44
+ Bundles: `ply.min.css` (~20KB gzip, everything), `ply-core.min.css` (~17KB, grid+buttons+forms+nav+alerts+tables+typography+helpers), `ply-essentials.min.css` (~6KB, grid+helpers), `ply-helpers.min.css` (~4KB, helpers only).
45
+
46
+ ---
47
+
48
+ ## Core Rules
49
+
50
+ 1. **ply is standalone** — Do NOT use Tailwind, Bootstrap, or other CSS frameworks alongside ply.
51
+ 2. **Always wrap `unit-*` inside `units-row`** — they are flex children.
52
+ 3. **Use `units-container`** for page-width centering (1200px max).
53
+ 4. **Use `<button>` for buttons, not `<a>`** — links navigate, buttons act.
54
+ 5. **Wrap forms in `.form`** for styled inputs.
55
+ 6. **Use semantic HTML first** — ply auto-styles `<nav>`, `<table>`, `<code>`, `<pre>`, `<kbd>`, `<blockquote>`, `<mark>`, `<details>`, `<summary>`, `<dialog>`, `<progress>`, `<meter>`, `<hr>`, and headings.
56
+ 7. **Only use documented classes** — do not invent class names. Search `ply-classes.json` first.
57
+ 8. **Add responsive classes** — at minimum `tablet-unit-100` to stack on mobile.
58
+ 9. **Use `--ply-*` custom properties** for colors — never hard-code values that break dark mode.
59
+ 10. **Single-dash class names preferred** — `navbar-centered`, not `navbar--centered`.
60
+
61
+ ---
62
+
63
+ ## Grid System
64
+
65
+ ply uses a ratio-based flexbox grid. Think in percentages, not arbitrary columns.
66
+
67
+ ```html
68
+ <div class="units-container">
69
+ <div class="units-row gap">
70
+ <div class="unit-66 tablet-unit-100">Main content (2/3 width)</div>
71
+ <div class="unit-33 tablet-unit-100">Sidebar (1/3 width)</div>
72
+ </div>
73
+ </div>
74
+ ```
75
+
76
+ - `units-container` — centers content at 1200px max-width
77
+ - `units-row` — creates the flex row
78
+ - `unit-*` — sets width as percentage. Available: 10, 12, 20, 25, 30, 33, 35, 38, 40, 50, 60, 62, 65, 66, 70, 75, 80, 88, 90, 100, auto
79
+ - Responsive prefixes: `tablet-unit-*` (≤ 1024px), `phone-unit-*` (≤ 767px), `small-desktop-unit-*`, `large-screen-unit-*`
80
+ - `gap-xs`, `gap-sm`, `gap`, `gap-lg`, `gap-xl` — spacing between flex children
81
+ - `equal-height` — on `units-row` to stretch all children to the tallest
82
+ - `units-row` can be nested inside units for complex layouts
83
+
84
+ ### Two-Column Layout Example
85
+
86
+ ```html
87
+ <div class="units-container">
88
+ <div class="units-row gap">
89
+ <div class="unit-66 tablet-unit-100">
90
+ <h2>Main Content</h2>
91
+ <p>Two-thirds width on desktop, full width on tablet.</p>
92
+ </div>
93
+ <div class="unit-33 tablet-unit-100">
94
+ <aside>
95
+ <h3>Sidebar</h3>
96
+ <p class="text-secondary">Supplementary content.</p>
97
+ </aside>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Container Queries
106
+
107
+ Container queries let units respond to their parent's width instead of the viewport. Useful for reusable components (cards, widgets, sidebars) that adapt to available space.
108
+
109
+ **Wrapper:** `container-query` — sets `container-type: inline-size` on the parent.
110
+
111
+ **Container prefixes** mirror viewport prefixes:
112
+
113
+ | Container prefix | Breakpoint | Mirrors |
114
+ |---|---|---|
115
+ | `container-phone-unit-*` | 480px | `phone-unit-*` |
116
+ | `container-large-phone-unit-*` | 650px | `large-phone-unit-*` |
117
+ | `container-tablet-unit-*` | 767px | `tablet-unit-*` |
118
+ | `container-small-desktop-unit-*` | 1024px | `small-desktop-unit-*` |
119
+
120
+ All 21 unit sizes available (100, 90, 88, 80, 75, 70, 66, 65, 62, 60, 50, 40, 38, 35, 33, 30, 25, 20, 12, 10, auto).
121
+
122
+ ### Container Query Example
123
+
124
+ ```html
125
+ <!-- A sidebar widget that stacks its columns when the sidebar is narrow -->
126
+ <aside class="container-query">
127
+ <div class="units-row">
128
+ <div class="unit-50 container-tablet-unit-100">
129
+ <p>Left content — stacks when container ≤ 767px</p>
130
+ </div>
131
+ <div class="unit-50 container-tablet-unit-100">
132
+ <p>Right content — stacks when container ≤ 767px</p>
133
+ </div>
134
+ </div>
135
+ </aside>
136
+ ```
137
+
138
+ ### Mixing Viewport and Container Prefixes
139
+
140
+ ```html
141
+ <!-- Viewport prefix stacks on small screens; container prefix stacks when placed in a narrow parent -->
142
+ <div class="container-query">
143
+ <div class="units-row">
144
+ <div class="unit-66 tablet-unit-100 container-small-desktop-unit-100">Main</div>
145
+ <div class="unit-33 tablet-unit-100 container-small-desktop-unit-100">Side</div>
146
+ </div>
147
+ </div>
148
+ ```
149
+
150
+ Container classes use `@container` rules (not `@media`) — they fire based on the `.container-query` element's inline size.
151
+
152
+ ---
153
+
154
+ ## Buttons — Accessible by Default (WCAG 2.1 AA)
155
+
156
+ All ply buttons are WCAG 2.1 AA compliant out of the box. Always use `<button>`, not `<a>`.
157
+
158
+ ### Button Hierarchy
159
+
160
+ - **`btn btn-primary`** — Primary call-to-action. Blue by default, themed via `--ply-btn-default-bg`. **WCAG AA compliant: 4.56:1 contrast ratio** (white text on blue background). In dark mode, contrast is maintained at AA level. `:focus-visible` shows a 2px blue outline for keyboard users. `prefers-contrast: more` strengthens colors further (WCAG 1.4.6).
161
+ - **`btn btn-secondary`** (or plain `btn`) — Secondary actions. Dark gray by default, themed via `--ply-btn-secondary-bg`. WCAG AA compliant.
162
+ - **`btn btn-primary-outline`** / **`btn btn-secondary-outline`** — Outlined variants. Transparent bg, border + text from the theme color. Fills on hover.
163
+ - **`btn btn-ghost`** — Ghost button. Text-only with subtle hover tint.
164
+ - **`btn btn-blue`**, **`btn btn-red`**, **`btn btn-green`**, **`btn btn-yellow`** — Static color buttons with hardcoded hex values. Immune to theming. Use for color-coded actions (e.g., delete = red, success = green). All meet WCAG AA contrast.
165
+
166
+ ### Icon Buttons
167
+
168
+ - **`btn-icon`** — Icon-only button modifier. Equal padding for a square aspect ratio. Always add `aria-label` (no visible text). Combine with `btn-ghost` for toolbar-style actions.
169
+ - For icon + text buttons, use a regular `btn` with an inline SVG — no `btn-icon` needed.
170
+
171
+ ```html
172
+ <button class="btn btn-ghost btn-icon" aria-label="Search"><svg>...</svg></button>
173
+ <button class="btn btn-primary"><svg>...</svg> Save</button>
174
+ ```
175
+
176
+ ### Button Sizes
177
+
178
+ - `btn-big` (alias: `btn-lg`) — larger button
179
+ - `btn-small` (alias: `btn-sm`) — smaller button
180
+ - `btn-smaller` (alias: `btn-xs`) — smallest button
181
+
182
+ ### Button Accessibility Features
183
+
184
+ Every ply button automatically gets:
185
+ - `:focus-visible` with a 2px solid blue outline and 2px offset — visible to keyboard users, invisible to mouse users (WCAG 2.4.7 Focus Visible)
186
+ - `prefers-contrast: more` support — colors become stronger in high-contrast mode (WCAG 1.4.6 Contrast Enhanced)
187
+ - `prefers-reduced-motion: reduce` — disables any hover/active transitions (WCAG 2.3.3)
188
+ - 3:1 non-text contrast on borders and focus indicators (WCAG 1.4.11)
189
+ - No JavaScript required — all button interactions are CSS-based
190
+
191
+ ```html
192
+ <!-- Primary + Secondary pair -->
193
+ <button class="btn btn-primary">Save</button>
194
+ <button class="btn btn-secondary">Cancel</button>
195
+
196
+ <!-- Themed button via CSS custom property -->
197
+ <style>[data-theme="warm"] { --ply-btn-default-bg: #92400e; }</style>
198
+
199
+ <!-- Static color button (ignores theme) -->
200
+ <button class="btn btn-blue">Always Blue</button>
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Dark Mode
206
+
207
+ ply auto-detects `prefers-color-scheme: dark` when no `data-theme` attribute is set on `<html>`. All `--ply-*` custom properties swap automatically — backgrounds, text, borders, buttons, links. WCAG AA contrast is maintained in both light and dark modes.
208
+
209
+ ```html
210
+ <html> <!-- Auto (follows OS preference) -->
211
+ <html data-theme="light"> <!-- Force light -->
212
+ <html data-theme="dark"> <!-- Force dark -->
213
+ <html data-theme="warm"> <!-- Custom theme (no auto dark mode) -->
214
+ ```
215
+
216
+ Components that adapt automatically — no configuration needed:
217
+
218
+ ```html
219
+ <div class="layer-1 padding border-radius">
220
+ <h2>Dashboard</h2>
221
+ <p class="text-secondary">This card adapts to light/dark automatically.</p>
222
+ <button class="btn btn-primary">Action</button>
223
+ </div>
224
+ ```
225
+
226
+ `layer-1` uses `--ply-bg-surface` (white in light, `#262626` in dark). `text-secondary` uses `--ply-color-secondary` (`#525252` light, `#c6c6c6` dark). Every ply class is theme-aware.
227
+
228
+ Toggle with JavaScript:
229
+
230
+ ```js
231
+ document.documentElement.dataset.theme =
232
+ document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Custom Themes
238
+
239
+ Override `--ply-*` custom properties to theme the entire app with one CSS block:
240
+
241
+ ```css
242
+ [data-theme="brand"] {
243
+ --ply-bg-body: #fefce8;
244
+ --ply-bg-surface: #fef9c3;
245
+ --ply-bg-muted: #fef08a;
246
+ --ply-color-body: #1a1a1a;
247
+ --ply-color-headings: #78350f;
248
+ --ply-border-color: #fbbf24;
249
+ --ply-btn-default-bg: #b45309;
250
+ --ply-btn-default-bg-hover: #92400e;
251
+ --ply-btn-default-bg-active: #7c2d12;
252
+ --ply-btn-secondary-bg: #78350f;
253
+ --ply-nav-bg: #fef3c7;
254
+ --ply-nav-border: #f59e0b;
255
+ --ply-font-body: "Palatino Linotype", Palatino, Georgia, serif;
256
+ --ply-font-heading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
257
+ --ply-font-mono: "Fira Code", "Source Code Pro", monospace;
258
+ }
259
+ ```
260
+
261
+ ```html
262
+ <html data-theme="brand">
263
+ ```
264
+
265
+ Setting any `data-theme` value prevents auto dark mode from interfering. All 60+ `--ply-*` variables are listed in the `customProperties` section of `ply-classes.json`.
266
+
267
+ **Important:** If you override colors, verify 4.5:1 contrast for normal text and 3:1 for large text (18px bold / 24px) and UI components (WCAG 1.4.3).
268
+
269
+ ---
270
+
271
+ ## Navigation — Semantic and Responsive
272
+
273
+ ### Basic Navbar
274
+
275
+ ```html
276
+ <nav class="navbar" aria-label="Main navigation">
277
+ <ul>
278
+ <li class="active"><a href="/" aria-current="page">Home</a></li>
279
+ <li><a href="/about">About</a></li>
280
+ <li><a href="/services">Services</a></li>
281
+ <li><a href="/contact">Contact</a></li>
282
+ </ul>
283
+ </nav>
284
+ ```
285
+
286
+ Navbar variants: `navbar-thick`, `navbar-borderless`, `navbar-border-blue`, `navbar-border-green`, `navbar-border-red`, `navbar-border-yellow`, `navbar-centered`.
287
+
288
+ ### Responsive Collapsible Header (CSS-Only, No JavaScript)
289
+
290
+ Use `<details>`/`<summary>` as a hamburger toggle for mobile. The desktop nav shows inline; on mobile it collapses behind a toggle. Fully keyboard-accessible — Enter/Space toggles `<details>` natively.
291
+
292
+ ```html
293
+ <a href="#main" class="skip-link">Skip to main content</a>
294
+
295
+ <header class="sticky" style="top: 0; z-index: 100;">
296
+ <nav class="navbar" aria-label="Main navigation" style="position: relative;">
297
+ <!-- Desktop nav — hidden on mobile (≤ 767px) -->
298
+ <ul class="phone-hide">
299
+ <li class="active"><a href="#" aria-current="page">Home</a></li>
300
+ <li><a href="#">About</a></li>
301
+ <li><a href="#">Services</a></li>
302
+ <li><a href="#">Pricing</a></li>
303
+ <li><a href="#">Contact</a></li>
304
+ </ul>
305
+ <!-- Mobile hamburger — hidden on tablet (≥ 768px) and desktop -->
306
+ <details class="tablet-hide desktop-hide">
307
+ <summary aria-label="Menu">&#9776; Menu</summary>
308
+ <ul>
309
+ <li class="active"><a href="#" aria-current="page">Home</a></li>
310
+ <li><a href="#">About</a></li>
311
+ <li><a href="#">Services</a></li>
312
+ <li><a href="#">Pricing</a></li>
313
+ <li><a href="#">Contact</a></li>
314
+ </ul>
315
+ </details>
316
+ </nav>
317
+ </header>
318
+
319
+ <main id="main">...</main>
320
+ ```
321
+
322
+ Required CSS for the mobile dropdown overlay:
323
+
324
+ ```css
325
+ nav.navbar details > ul {
326
+ position: absolute;
327
+ left: 0; right: 0;
328
+ background: var(--ply-bg-surface);
329
+ border-bottom: 1px solid var(--ply-border-color);
330
+ padding: 0.5rem 0;
331
+ z-index: 99;
332
+ }
333
+ nav.navbar details > ul li { display: block; }
334
+ nav.navbar details > ul li a { display: block; padding: 0.5rem 1rem; }
335
+ nav.navbar details summary { cursor: pointer; padding: 0.5rem 1rem; list-style: none; }
336
+ nav.navbar details summary::-webkit-details-marker { display: none; }
337
+ ```
338
+
339
+ **How it works:**
340
+ - `sticky` + `top: 0` pins the header on scroll
341
+ - `phone-hide` hides the desktop `<ul>` on screens ≤ 767px
342
+ - `tablet-hide desktop-hide` shows the `<details>` toggle only on mobile
343
+ - `<details>`/`<summary>` is natively keyboard-accessible (Enter/Space toggles, no JS)
344
+ - `aria-label="Menu"` on `<summary>` and `aria-current="page"` on the active link for screen readers
345
+
346
+ **Responsive visibility classes:**
347
+ - `phone-hide` — hidden ≤ 767px
348
+ - `tablet-hide` — hidden 768px–1024px
349
+ - `desktop-hide` — hidden ≥ 1025px
350
+ - `phone-only` — visible only ≤ 767px
351
+ - `tablet-only` — visible only 768px–1024px
352
+
353
+ Always include: `<meta name="viewport" content="width=device-width, initial-scale=1.0">`
354
+
355
+ ---
356
+
357
+ ## Cards and Surfaces
358
+
359
+ ```html
360
+ <div class="layer-1 padding-lg margin-bottom border border-radius">
361
+ <h3>Card Title</h3>
362
+ <p class="text-secondary">Description with theme-aware text.</p>
363
+ <button class="btn btn-primary">Learn More</button>
364
+ </div>
365
+ ```
366
+
367
+ - `layer-1` — surface background (`--ply-bg-surface`) + subtle box-shadow
368
+ - `layer-2` — elevated surface (`--ply-bg-muted`) + stronger shadow
369
+ - `padding-xs` (0.25rem), `padding-sm` (0.5rem), `padding` (default), `padding-lg` (1.5rem), `padding-xl` (2rem)
370
+ - `margin-bottom`, `margin-top-lg`, `margin-xl` — directional + size variants
371
+ - `border` — 1px solid using `--ply-border-color` (theme-aware)
372
+ - `border-radius` (default), `border-radius-lg` (0.75rem), `border-radius-xl` (1.5rem)
373
+
374
+ Equal-height cards in a row:
375
+
376
+ ```html
377
+ <div class="units-row gap-lg equal-height">
378
+ <div class="unit-33 tablet-unit-100">
379
+ <div class="layer-1 padding border-radius">Card 1</div>
380
+ </div>
381
+ <div class="unit-33 tablet-unit-100">
382
+ <div class="layer-2 padding border-radius">Card 2 (elevated)</div>
383
+ </div>
384
+ <div class="unit-33 tablet-unit-100">
385
+ <div class="layer-1 padding border-radius">Card 3</div>
386
+ </div>
387
+ </div>
388
+ ```
389
+
390
+ ---
391
+
392
+ ## Typography and Text
393
+
394
+ - `text-primary` — primary text color (theme-aware)
395
+ - `text-secondary` — secondary/muted text
396
+ - `text-tertiary` — tertiary/subtle text
397
+ - `text-sm` — small text
398
+ - `text-lg` — large text
399
+ - `text-center`, `text-left`, `text-right` — alignment
400
+ - `text-balance` — CSS `text-wrap: balance` for headings
401
+ - `no-orphan` — prevents orphaned last words in paragraphs
402
+ - `.h1` through `.h6` — visual heading sizes without semantic heading level
403
+
404
+ ---
405
+
406
+ ## Forms
407
+
408
+ Wrap in `.form` to activate ply input styling:
409
+
410
+ ```html
411
+ <form class="form">
412
+ <label for="name">Name</label>
413
+ <input type="text" id="name" required>
414
+ <label for="email">Email</label>
415
+ <input type="email" id="email" required>
416
+ <button class="btn btn-primary" type="submit">Submit</button>
417
+ </form>
418
+ ```
419
+
420
+ Input sizes: `input-big` (alias: `input-lg`), `input-small` (alias: `input-sm`), `input-smaller` (alias: `input-xs`).
421
+
422
+ Validation states: `input-error`, `input-success`.
423
+
424
+ ### Input Groups
425
+
426
+ Use `.input-groups` for prepend/append addons on inputs. **Buttons must be wrapped** in `.input-append` or `.btn-append` — placing a button directly inside `.input-groups` without a wrapper won't work.
427
+
428
+ ```html
429
+ <!-- Text addon -->
430
+ <div class="input-groups">
431
+ <span class="input-prepend">$</span>
432
+ <input type="text" placeholder="Amount">
433
+ <span class="input-append">.00</span>
434
+ </div>
435
+
436
+ <!-- Button appended via input-append -->
437
+ <div class="input-groups">
438
+ <input type="text" placeholder="Search...">
439
+ <span class="input-append">
440
+ <button class="btn btn-primary">Search</button>
441
+ </span>
442
+ </div>
443
+
444
+ <!-- Button appended via btn-append -->
445
+ <div class="input-groups">
446
+ <input type="text" placeholder="Enter URL">
447
+ <div class="btn-append">
448
+ <button class="btn btn-primary-outline">Go</button>
449
+ </div>
450
+ </div>
451
+ ```
452
+
453
+ Outline buttons in input groups automatically match the input border. Button scale transforms are disabled inside input groups for a clean inline look.
454
+
455
+ ---
456
+
457
+ ## Alerts and Notifications
458
+
459
+ ```html
460
+ <div class="alert">Default alert</div>
461
+ <div class="alert alert-blue">Info alert</div>
462
+ <div class="alert alert-green">Success alert</div>
463
+ <div class="alert alert-red">Error alert</div>
464
+ <div class="alert alert-yellow">Warning alert</div>
465
+ ```
466
+
467
+ Aliases: `alert` = `tools-alert`, `message` = `tools-message`.
468
+
469
+ ---
470
+
471
+ ## Common Patterns
472
+
473
+ - **Equal-height cards** — `equal-height` on `units-row`
474
+ - **Gap between children** — `gap-xs`, `gap-sm`, `gap`, `gap-lg`, `gap-xl`
475
+ - **Prevent orphans** — `no-orphan` on paragraphs, `text-balance` on headings
476
+ - **Card-style links** — `no-link-style` on a container to suppress link color/underline
477
+ - **Sticky elements** — `sticky` class + `top: 0` inline style
478
+ - **Display utilities** — `display-flex`, `display-grid`, `display-block`, `display-none`
479
+
480
+ ---
481
+
482
+ ## Accessibility (WCAG 2.1 AA) — Built Into Every Component
483
+
484
+ ply is built for Section 508 / WCAG 2.1 AA compliance. Every interactive element ships with accessibility features — no extra configuration needed.
485
+
486
+ ### What ply provides automatically
487
+
488
+ - **Focus indicators** — All interactive elements (buttons, links, inputs, nav items, dropdowns, `<summary>`, `<dialog>`) get `:focus-visible` with a 2px solid blue outline and 2px offset. Keyboard users see clear focus rings; mouse users don't. (WCAG 2.4.7 Focus Visible)
489
+ - **Color contrast** — All default color pairings meet 4.5:1 contrast in both light and dark modes. `btn-primary` delivers 4.56:1 contrast (white on blue). The entire brand palette is tuned for AA. (WCAG 1.4.3 Contrast Minimum)
490
+ - **Non-text contrast** — Focus indicators, button borders, and input borders meet 3:1 ratio. (WCAG 1.4.11)
491
+ - **High contrast mode** — `@media (prefers-contrast: more)` strengthens text and link colors beyond AA minimums. (WCAG 1.4.6 Contrast Enhanced)
492
+ - **Reduced motion** — `@media (prefers-reduced-motion: reduce)` disables all CSS animations and transitions. (WCAG 2.3.3)
493
+ - **Dark mode contrast** — `prefers-color-scheme: dark` maintains WCAG AA contrast. (WCAG 1.4.3)
494
+ - **Skip link** — `.skip-link` as the first focusable element lets keyboard users bypass navigation. (WCAG 2.4.1)
495
+ - **Screen reader support** — `.sr-only` hides content visually while keeping it accessible to assistive technology. (WCAG 1.3.1)
496
+ - **Keyboard operability** — All ply interactive elements are natively keyboard-operable via semantic HTML. No custom JS required. (WCAG 2.1.1)
497
+ - **Zero JavaScript** — No ARIA state management failures from framework code. (WCAG 4.1.2)
498
+ - **Sortable table headers** — `th.sortable` gets `:focus-visible` outlines
499
+ - **Pagination** — Focus-visible outlines on all page links, `aria-current="page"` supported
500
+ - **RTL support** — `dir="rtl"` for Arabic, Hebrew, and other RTL languages
501
+
502
+ ### WCAG compliance summary
503
+
504
+ | WCAG Criterion | How ply addresses it |
505
+ |---|---|
506
+ | 1.3.1 Info and Relationships | Semantic HTML auto-styling encourages `<nav>`, `<main>`, `<aside>`, `<table>`, `<details>`, headings. `.sr-only` exposes supplemental info. |
507
+ | 1.4.3 Contrast (Minimum) | All default colors meet 4.5:1. `btn-primary` = 4.56:1. Dark mode maintains AA. |
508
+ | 1.4.6 Contrast (Enhanced) | `prefers-contrast: more` strengthens colors beyond AA. |
509
+ | 1.4.11 Non-Text Contrast | Focus indicators, button borders, input borders all meet 3:1. |
510
+ | 2.1.1 Keyboard | All interactive elements keyboard-operable via native HTML. |
511
+ | 2.3.3 Animation | `prefers-reduced-motion: reduce` disables all animations. |
512
+ | 2.4.1 Bypass Blocks | `.skip-link` for keyboard bypass navigation. |
513
+ | 2.4.7 Focus Visible | `:focus-visible` on every interactive element. |
514
+ | 4.1.2 Name, Role, Value | Zero JS = no ARIA state failures. Native HTML exposes correct roles. |
515
+
516
+ ### What needs application-level work
517
+
518
+ - **Alt text** — Add `alt` to every `<img>`; `alt=""` for decorative. (1.1.1)
519
+ - **Heading hierarchy** — One `<h1>` per page, then h2-h6 in sequence. (1.3.1)
520
+ - **Custom widget ARIA** — JS-powered dropdowns, modals, tabs need `aria-expanded`, `aria-controls`, `role`. (4.1.2)
521
+ - **Custom color contrast** — If you override `--ply-*` variables, verify 4.5:1 for text, 3:1 for large text/UI. (1.4.3)
522
+ - **Keyboard for custom widgets** — Custom JS components must be keyboard-operable. (2.1.1)
523
+ - **Page title** — Every page needs `<title>`. (2.4.2)
524
+ - **Language** — Set `<html lang="en">`. (3.1.1)
525
+ - **Form labels** — Use `<label>` for all inputs. (3.3.2)
526
+ - **Error identification** — Identify errors in text, not just color. (3.3.1)
527
+
528
+ ### AI agent guidance for accessible markup
529
+
530
+ When generating ply markup, always:
531
+ 1. Use semantic elements for landmarks — `<header>`, `<nav>`, `<main>`, `<aside>`, `<footer>`
532
+ 2. Add `aria-label` to custom widgets without visible text labels
533
+ 3. Use `<form role="search">` for search forms
534
+ 4. Include `.skip-link` as the first focusable element
535
+ 5. Add `alt` text to all `<img>` elements
536
+ 6. Maintain heading hierarchy (h1 > h2 > h3, no skipping)
537
+ 7. Label all form inputs with `<label>`
538
+ 8. Set `lang` on `<html>`
539
+ 9. Pair color with text for status indicators
540
+ 10. Prefer native elements over ARIA — `<button>` over `<div role="button">`, `<dialog>` over `<div role="dialog">`
541
+
542
+ ---
543
+
544
+ ## Focus Management & Keyboard Patterns
545
+
546
+ ply provides `:focus-visible` outlines on all native interactive elements. For custom widgets, manage focus yourself.
547
+
548
+ ### Focus Order Strategy
549
+
550
+ 1. **Use semantic HTML first** — `<button>`, `<a>`, `<input>`, `<select>`, `<details>`, `<dialog>` are already in tab order.
551
+ 2. **`tabindex="0"`** — Add to custom interactive elements to include in tab order. ply's `:focus-visible` applies automatically.
552
+ 3. **`tabindex="-1"`** — For programmatic focus targets (modal containers, error summaries). NOT in tab order.
553
+ 4. **Never use `tabindex` > 0** — Overrides natural DOM order.
554
+
555
+ ```html
556
+ <!-- Native — already focusable, ply styles focus -->
557
+ <button class="btn">Save</button>
558
+ <a href="/settings">Settings</a>
559
+
560
+ <!-- Custom interactive element — add tabindex="0" -->
561
+ <div role="button" tabindex="0"
562
+ onclick="doAction()"
563
+ onkeydown="if(event.key==='Enter'||event.key===' ')doAction()">
564
+ Custom Action
565
+ </div>
566
+
567
+ <!-- Programmatic focus target — tabindex="-1" -->
568
+ <div id="error-summary" tabindex="-1" role="alert">
569
+ Please fix the errors below.
570
+ </div>
571
+ <script>
572
+ document.getElementById('error-summary').focus();
573
+ </script>
574
+
575
+ <!-- Screen-reader-only content -->
576
+ <span class="sr-only">3 notifications</span>
577
+ ```
578
+
579
+ ### Arrow Key Navigation (Roving Tabindex)
580
+
581
+ For grouped controls (tabs, toolbars, menu items): one item has `tabindex="0"` (active), the rest have `tabindex="-1"`. Arrow keys move focus within the group.
582
+
583
+ ```html
584
+ <div role="tablist" aria-label="Settings">
585
+ <button role="tab" aria-selected="true" tabindex="0" aria-controls="panel-general">General</button>
586
+ <button role="tab" aria-selected="false" tabindex="-1" aria-controls="panel-security">Security</button>
587
+ <button role="tab" aria-selected="false" tabindex="-1" aria-controls="panel-notifications">Notifications</button>
588
+ </div>
589
+ <div role="tabpanel" id="panel-general" aria-labelledby="tab-general">
590
+ General settings content.
591
+ </div>
592
+ ```
593
+
594
+ ```js
595
+ tablist.addEventListener('keydown', (e) => {
596
+ const tabs = [...tablist.querySelectorAll('[role="tab"]')];
597
+ const current = tabs.indexOf(document.activeElement);
598
+ let next;
599
+ if (e.key === 'ArrowRight') next = (current + 1) % tabs.length;
600
+ else if (e.key === 'ArrowLeft') next = (current - 1 + tabs.length) % tabs.length;
601
+ else if (e.key === 'Home') next = 0;
602
+ else if (e.key === 'End') next = tabs.length - 1;
603
+ else return;
604
+ e.preventDefault();
605
+ tabs[current].setAttribute('tabindex', '-1');
606
+ tabs[current].setAttribute('aria-selected', 'false');
607
+ tabs[next].setAttribute('tabindex', '0');
608
+ tabs[next].setAttribute('aria-selected', 'true');
609
+ tabs[next].focus();
610
+ tabs.forEach((tab, i) => {
611
+ document.getElementById(tab.getAttribute('aria-controls')).hidden = (i !== next);
612
+ });
613
+ });
614
+ ```
615
+
616
+ ### ARIA Attributes Reference
617
+
618
+ | Attribute | When to use | Example |
619
+ |---|---|---|
620
+ | `aria-expanded` | Toggleable content (dropdowns, accordions, menus) | `<button aria-expanded="false" aria-controls="menu">Menu</button>` |
621
+ | `aria-controls` | Links trigger to controlled element | `<button aria-controls="dropdown-1">Options</button>` |
622
+ | `aria-selected` | Active item in tabs, listboxes | `<button role="tab" aria-selected="true">Tab 1</button>` |
623
+ | `aria-current="page"` | Current page in navigation | `<a href="/" aria-current="page">Home</a>` |
624
+ | `aria-live="polite"` | Dynamic content updates | `<div aria-live="polite">3 results found.</div>` |
625
+ | `aria-live="assertive"` | Urgent announcements | `<div aria-live="assertive" role="alert">Session expired.</div>` |
626
+ | `aria-label` | Elements without visible text | `<button aria-label="Close">×</button>` |
627
+ | `aria-labelledby` | Points to another element for label | `<div role="tabpanel" aria-labelledby="tab-1">` |
628
+ | `aria-describedby` | Additional description | `<input aria-describedby="password-hint">` |
629
+
630
+ ### Live Regions for Dynamic Content
631
+
632
+ ```html
633
+ <!-- Polite — announced after current speech -->
634
+ <div aria-live="polite" role="status" class="sr-only" id="search-status"></div>
635
+ <script>
636
+ document.getElementById('search-status').textContent = '12 results found.';
637
+ </script>
638
+
639
+ <!-- Assertive — announced immediately -->
640
+ <div aria-live="assertive" role="alert">
641
+ Your session has expired. Please log in again.
642
+ </div>
643
+ ```
644
+
645
+ ---
646
+
647
+ ## Custom Widget Accessibility Patterns
648
+
649
+ When building custom interactive widgets beyond ply's built-in components, follow these patterns for WCAG 2.1 AA compliance.
650
+
651
+ ### Drag-and-Drop with Full Keyboard Support
652
+
653
+ Drag-and-drop interfaces must be fully keyboard operable (WCAG 2.1.1). Use a listbox pattern with grab/move/drop states and an `aria-live` region for screen reader announcements.
654
+
655
+ ```html
656
+ <!-- Live region for announcements -->
657
+ <div class="sr-only" aria-live="assertive" id="dnd-status"></div>
658
+
659
+ <!-- Visible keyboard instructions -->
660
+ <p class="text-secondary text-sm">
661
+ Keyboard: Tab to list, Space to grab, Up/Down to move, Enter to drop, Escape to cancel.
662
+ </p>
663
+
664
+ <!-- Sortable list with ARIA roles -->
665
+ <ul role="listbox" aria-label="Sortable tasks" id="sortable-list">
666
+ <li role="option" tabindex="0" aria-grabbed="false">Review pull request</li>
667
+ <li role="option" tabindex="-1" aria-grabbed="false">Update documentation</li>
668
+ <li role="option" tabindex="-1" aria-grabbed="false">Fix navigation bug</li>
669
+ </ul>
670
+ ```
671
+
672
+ ```js
673
+ const list = document.getElementById('sortable-list');
674
+ const status = document.getElementById('dnd-status');
675
+ let grabbed = null;
676
+
677
+ list.addEventListener('keydown', (e) => {
678
+ const item = e.target;
679
+ if (item.getAttribute('role') !== 'option') return;
680
+
681
+ switch (e.key) {
682
+ case ' ':
683
+ e.preventDefault();
684
+ if (!grabbed) {
685
+ grabbed = item;
686
+ item.setAttribute('aria-grabbed', 'true');
687
+ status.textContent = `Grabbed ${item.textContent}. Use arrow keys to move, Enter to drop, Escape to cancel.`;
688
+ }
689
+ break;
690
+ case 'ArrowDown':
691
+ e.preventDefault();
692
+ if (grabbed) {
693
+ const next = grabbed.nextElementSibling;
694
+ if (next) {
695
+ list.insertBefore(next, grabbed);
696
+ status.textContent = `Moved down to position ${[...list.children].indexOf(grabbed) + 1}.`;
697
+ }
698
+ }
699
+ break;
700
+ case 'ArrowUp':
701
+ e.preventDefault();
702
+ if (grabbed) {
703
+ const prev = grabbed.previousElementSibling;
704
+ if (prev) {
705
+ list.insertBefore(grabbed, prev);
706
+ status.textContent = `Moved up to position ${[...list.children].indexOf(grabbed) + 1}.`;
707
+ }
708
+ }
709
+ break;
710
+ case 'Enter':
711
+ if (grabbed) {
712
+ e.preventDefault();
713
+ grabbed.setAttribute('aria-grabbed', 'false');
714
+ status.textContent = `Dropped ${grabbed.textContent} at position ${[...list.children].indexOf(grabbed) + 1}.`;
715
+ grabbed = null;
716
+ }
717
+ break;
718
+ case 'Escape':
719
+ if (grabbed) {
720
+ e.preventDefault();
721
+ grabbed.setAttribute('aria-grabbed', 'false');
722
+ status.textContent = 'Reorder cancelled.';
723
+ grabbed = null;
724
+ }
725
+ break;
726
+ }
727
+ });
728
+ ```
729
+
730
+ **Accessibility features in this pattern:**
731
+ - `aria-grabbed` toggles between "true" (grabbed) and "false" (released) — announced by screen readers (WCAG 4.1.2)
732
+ - `aria-live="assertive"` region announces every move to screen readers
733
+ - Roving tabindex — only the focused item has `tabindex="0"`
734
+ - `role="listbox"` + `role="option"` — correct semantics for screen readers
735
+ - Visible keyboard instructions for sighted users
736
+ - `prefers-reduced-motion: reduce` — disable drag animations in CSS
737
+ - Full keyboard alternative: Space=grab, Arrows=move, Enter=drop, Escape=cancel (WCAG 2.1.1)
738
+
739
+ ### Focus Trap for Modals
740
+
741
+ Prefer native `<dialog>` — ply styles it automatically and browsers handle focus trapping. For custom modals:
742
+
743
+ ```js
744
+ function trapFocus(modal) {
745
+ const focusable = modal.querySelectorAll(
746
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
747
+ );
748
+ const first = focusable[0], last = focusable[focusable.length - 1];
749
+ modal.addEventListener('keydown', (e) => {
750
+ if (e.key !== 'Tab') return;
751
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
752
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
753
+ });
754
+ first?.focus();
755
+ }
756
+ ```
757
+
758
+ ### Focus Return After Close
759
+
760
+ Always restore focus to the element that triggered the widget:
761
+
762
+ ```js
763
+ const trigger = document.activeElement;
764
+ modal.showModal();
765
+ modal.addEventListener('close', () => trigger.focus(), { once: true });
766
+ ```
767
+
768
+ ### Custom Widget Accessibility Checklist
769
+
770
+ | Requirement | WCAG Criterion | How to verify |
771
+ |---|---|---|
772
+ | Keyboard operable (no mouse required) | 2.1.1 Keyboard | Tab, Enter, Space, arrow keys all work |
773
+ | Focus indicator visible | 2.4.7 Focus Visible | ply provides `:focus-visible` — don't override `outline: none` |
774
+ | States announced to screen readers | 4.1.2 Name, Role, Value | `aria-grabbed`, `aria-expanded`, `aria-selected` update on interaction |
775
+ | Focus trapped in modals | 2.4.3 Focus Order | Tab does not leave an open dialog |
776
+ | Focus restored on close | 2.4.3 Focus Order | Closing returns focus to the trigger element |
777
+ | Touch targets >= 44x44px | 2.5.5 Target Size | Measure interactive elements |
778
+ | Alternative input available | 2.1.1 Keyboard | Drag-and-drop has keyboard fallback |
779
+ | Screen reader tested | 4.1.2 Name, Role, Value | Verify with VoiceOver (macOS) or NVDA (Windows) |
780
+
781
+ ### Screen Reader Testing (VoiceOver on macOS)
782
+
783
+ | Action | Shortcut |
784
+ |---|---|
785
+ | Turn on/off VoiceOver | Cmd + F5 |
786
+ | Navigate next element | VO + Right Arrow (VO = Ctrl + Option) |
787
+ | Navigate previous | VO + Left Arrow |
788
+ | Activate element | VO + Space |
789
+ | Read current element | VO + F3 |
790
+ | Open rotor (landmarks, headings) | VO + U |
791
+
792
+ **Verify:** Every interactive element reachable by Tab/arrows. Buttons announce label + role. Expanded/collapsed state announced. Dynamic content announced via `aria-live`. Form inputs announce label + required + errors. Heading hierarchy logical.
793
+
794
+ ---
795
+
796
+ ## AI-Friendly Design
797
+
798
+ ply is built for AI code generation:
799
+
800
+ 1. **Single-dash class names** read like English — `units-row`, `equal-height`, `padding`, `border-radius`
801
+ 2. **No abbreviation guessing** — `padding-lg` not `pl-6`, `margin-top` not `mt-4`
802
+ 3. **421 documented classes** in `ply-classes.json` — machine-readable reference
803
+ 4. **Semantic HTML first** — auto-styled elements reduce class clutter
804
+ 5. **AI-friendly aliases** — `btn-lg`/`btn-big`, `btn-sm`/`btn-small`, `alert`/`tools-alert` all work
805
+
806
+ ### Alias Table
807
+
808
+ | Short | Long |
809
+ |---|---|
810
+ | `alert` | `tools-alert` |
811
+ | `alert-blue` | `tools-alert-blue` |
812
+ | `message` | `tools-message` |
813
+ | `btn-lg` | `btn-big` |
814
+ | `btn-sm` | `btn-small` |
815
+ | `btn-xs` | `btn-smaller` |
816
+ | `input-lg` | `input-big` |
817
+ | `input-sm` | `input-small` |
818
+ | `input-xs` | `input-smaller` |
819
+ | `li.active` | `li.on` (navbar) |
820
+ | `bg-black` | `background-black` |
821
+ | `bg-white` | `background-white` |
822
+
823
+ ---
824
+
825
+ ## Anti-Patterns
826
+
827
+ - **DON'T** use Tailwind, Bootstrap, or other frameworks with ply
828
+ - **DON'T** skip semantic HTML — use `<nav>`, `<code>`, `<table>`, `<details>`, `<dialog>` before `<div>`
829
+ - **DON'T** invent class names — only use classes from `ply-classes.json`
830
+ - **DON'T** use `role="button"` on links — use `<button>`
831
+ - **DON'T** put `unit-*` outside `units-row`
832
+ - **DON'T** hard-code colors — use `var(--ply-*)` custom properties
833
+ - **DON'T** forget the `.form` wrapper on forms
834
+ - **DON'T** override `outline: none` on interactive elements — it breaks focus visibility