@ps1ui/core 0.0.0 → 0.0.1

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/dist/base.css +378 -0
  3. package/dist/components/Anchor/Anchor.d.mts +12 -0
  4. package/dist/components/Anchor/Anchor.mjs +15 -0
  5. package/dist/components/Anchor/Anchor.mjs.map +1 -0
  6. package/dist/components/Button/Button.d.mts +24 -0
  7. package/dist/components/Button/Button.mjs +17 -0
  8. package/dist/components/Button/Button.mjs.map +1 -0
  9. package/dist/components/Card/Card.d.mts +7 -0
  10. package/dist/components/Card/Card.mjs +14 -0
  11. package/dist/components/Card/Card.mjs.map +1 -0
  12. package/dist/components/Checkbox/Checkbox.d.mts +9 -0
  13. package/dist/components/Checkbox/Checkbox.mjs +23 -0
  14. package/dist/components/Checkbox/Checkbox.mjs.map +1 -0
  15. package/dist/components/Code/Code.d.mts +7 -0
  16. package/dist/components/Code/Code.mjs +14 -0
  17. package/dist/components/Code/Code.mjs.map +1 -0
  18. package/dist/components/CodeBlock/CodeBlock.d.mts +12 -0
  19. package/dist/components/CodeBlock/CodeBlock.mjs +44 -0
  20. package/dist/components/CodeBlock/CodeBlock.mjs.map +1 -0
  21. package/dist/components/CodeBlock/refractor.d.mts +7 -0
  22. package/dist/components/CodeBlock/refractor.mjs +26 -0
  23. package/dist/components/CodeBlock/refractor.mjs.map +1 -0
  24. package/dist/components/Container/Container.d.mts +13 -0
  25. package/dist/components/Container/Container.mjs +24 -0
  26. package/dist/components/Container/Container.mjs.map +1 -0
  27. package/dist/components/Details/Details.d.mts +9 -0
  28. package/dist/components/Details/Details.mjs +20 -0
  29. package/dist/components/Details/Details.mjs.map +1 -0
  30. package/dist/components/Grid/Grid.d.mts +13 -0
  31. package/dist/components/Grid/Grid.mjs +24 -0
  32. package/dist/components/Grid/Grid.mjs.map +1 -0
  33. package/dist/components/GridItem/GridItem.d.mts +10 -0
  34. package/dist/components/GridItem/GridItem.mjs +21 -0
  35. package/dist/components/GridItem/GridItem.mjs.map +1 -0
  36. package/dist/components/Heading/Heading.d.mts +22 -0
  37. package/dist/components/Heading/Heading.mjs +53 -0
  38. package/dist/components/Heading/Heading.mjs.map +1 -0
  39. package/dist/components/Input/Input.d.mts +7 -0
  40. package/dist/components/Input/Input.mjs +15 -0
  41. package/dist/components/Input/Input.mjs.map +1 -0
  42. package/dist/components/Label/Label.d.mts +7 -0
  43. package/dist/components/Label/Label.mjs +14 -0
  44. package/dist/components/Label/Label.mjs.map +1 -0
  45. package/dist/components/List/List.d.mts +11 -0
  46. package/dist/components/List/List.mjs +15 -0
  47. package/dist/components/List/List.mjs.map +1 -0
  48. package/dist/components/ListItem/ListItem.d.mts +7 -0
  49. package/dist/components/ListItem/ListItem.mjs +13 -0
  50. package/dist/components/ListItem/ListItem.mjs.map +1 -0
  51. package/dist/components/PS1Root/PS1Root.d.mts +7 -0
  52. package/dist/components/PS1Root/PS1Root.mjs +13 -0
  53. package/dist/components/PS1Root/PS1Root.mjs.map +1 -0
  54. package/dist/components/Stack/Stack.d.mts +19 -0
  55. package/dist/components/Stack/Stack.mjs +48 -0
  56. package/dist/components/Stack/Stack.mjs.map +1 -0
  57. package/dist/components/Table/Table.d.mts +7 -0
  58. package/dist/components/Table/Table.mjs +25 -0
  59. package/dist/components/Table/Table.mjs.map +1 -0
  60. package/dist/components/Tbody/Tbody.d.mts +7 -0
  61. package/dist/components/Tbody/Tbody.mjs +13 -0
  62. package/dist/components/Tbody/Tbody.mjs.map +1 -0
  63. package/dist/components/Td/Td.d.mts +7 -0
  64. package/dist/components/Td/Td.mjs +13 -0
  65. package/dist/components/Td/Td.mjs.map +1 -0
  66. package/dist/components/Text/Text.d.mts +23 -0
  67. package/dist/components/Text/Text.mjs +34 -0
  68. package/dist/components/Text/Text.mjs.map +1 -0
  69. package/dist/components/Th/Th.d.mts +7 -0
  70. package/dist/components/Th/Th.mjs +13 -0
  71. package/dist/components/Th/Th.mjs.map +1 -0
  72. package/dist/components/Thead/Thead.d.mts +7 -0
  73. package/dist/components/Thead/Thead.mjs +13 -0
  74. package/dist/components/Thead/Thead.mjs.map +1 -0
  75. package/dist/components/Tr/Tr.d.mts +7 -0
  76. package/dist/components/Tr/Tr.mjs +13 -0
  77. package/dist/components/Tr/Tr.mjs.map +1 -0
  78. package/dist/components.css +1870 -0
  79. package/dist/index.d.mts +28 -0
  80. package/dist/index.mjs +25 -0
  81. package/dist/styles.css +1930 -0
  82. package/dist/utils/cx.mjs +8 -0
  83. package/dist/utils/cx.mjs.map +1 -0
  84. package/dist/utils/numbers.mjs +8 -0
  85. package/dist/utils/numbers.mjs.map +1 -0
  86. package/dist/utils/responsive.d.mts +6 -0
  87. package/dist/utils/responsive.mjs +39 -0
  88. package/dist/utils/responsive.mjs.map +1 -0
  89. package/dist/utils/spacing.d.mts +5 -0
  90. package/dist/utils/spacing.mjs +8 -0
  91. package/dist/utils/spacing.mjs.map +1 -0
  92. package/dist/utils/typography.d.mts +5 -0
  93. package/dist/utils/typography.mjs +16 -0
  94. package/dist/utils/typography.mjs.map +1 -0
  95. package/dist/utils/useMergedRef.mjs +30 -0
  96. package/dist/utils/useMergedRef.mjs.map +1 -0
  97. package/dist/utils/useScrollableFocus.mjs +29 -0
  98. package/dist/utils/useScrollableFocus.mjs.map +1 -0
  99. package/package.json +84 -3
@@ -0,0 +1,1870 @@
1
+ /* ==========================================================================
2
+ * Cascade & overrides — why this file does NOT use @layer
3
+ * --------------------------------------------------------------------------
4
+ * Wrapping these styles in `@layer ps1ui { … }` was considered and rejected:
5
+ * unlayered styles beat layered ones regardless of specificity, and host
6
+ * design systems (this entry's target audience) invariably ship unlayered
7
+ * element-level rules (`button { … }`, `input { … }`, `a { … }`). Layering
8
+ * ps1ui would let every such host rule silently override component classes,
9
+ * breaking components by default exactly where this entry is meant to be
10
+ * embedded. So component rules stay unlayered, and selectors like
11
+ * `.ps1ui-checkbox:checked:hover:not(:disabled)` keep real specificity.
12
+ *
13
+ * Overriding ps1ui styles, in preference order:
14
+ * 1. Redefine `--ps1ui-*` design tokens (the supported theming surface) —
15
+ * colors, spacing, radii, fonts all flow from tokens.css.
16
+ * 2. Match or exceed the component selector's specificity, e.g.
17
+ * `.my-app .ps1ui-button { … }`.
18
+ * 3. If YOUR system is fully layered, import this entry into a low layer
19
+ * yourself — `@import "@ps1ui/core/components.css" layer(vendor);` —
20
+ * and your layered rules above it win without extra specificity.
21
+ * ========================================================================== */
22
+
23
+ /* Palette hexes are mirrored in .storybook/ps1uiTheme.ts — see note there. */
24
+
25
+ :root {
26
+ --ps1ui-font-mono:
27
+ "JetBrains Mono Variable", ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", Consolas,
28
+ monospace;
29
+
30
+ --ps1ui-color-bg: #0b0f14;
31
+ --ps1ui-color-surface: #0f1520;
32
+ --ps1ui-color-border: #21262d;
33
+ --ps1ui-color-border-strong: #30363d;
34
+ --ps1ui-color-fg: #c7d5df;
35
+ --ps1ui-color-fg-muted: #8b98a5;
36
+ /* fg-subtle must maintain ≥ 4.5:1 (WCAG AA) vs BOTH --ps1ui-color-bg and --ps1ui-color-surface.
37
+ Surface is the tighter case; verified by Text.contrast.test.tsx. */
38
+ --ps1ui-color-fg-subtle: #7a8593;
39
+ --ps1ui-color-primary: #7ee787;
40
+ --ps1ui-color-primary-hover: #56d364;
41
+ --ps1ui-color-primary-active: #3fb950;
42
+ --ps1ui-color-primary-fg: #0b0f14;
43
+ --ps1ui-color-accent: #ffa657;
44
+ --ps1ui-color-focus-ring-soft: color-mix(in srgb, var(--ps1ui-color-primary) 14%, transparent);
45
+ --ps1ui-shadow-focus-ring: 0 0 0 3px var(--ps1ui-color-focus-ring-soft);
46
+
47
+ /* Syntax-highlight palette for CodeBlock. Every value is verified ≥ 4.5:1 vs
48
+ BOTH --ps1ui-color-bg and --ps1ui-color-surface by CodeBlock.contrast.test.tsx. */
49
+ --ps1ui-code-comment: #7a8593;
50
+ --ps1ui-code-string: #f0c880;
51
+ --ps1ui-code-keyword: #ff7b95;
52
+ --ps1ui-code-function: #79c0ff;
53
+ --ps1ui-code-constant: #ffa657;
54
+ --ps1ui-code-type: #5eead4;
55
+ --ps1ui-code-tag: #ff9587;
56
+ --ps1ui-code-attr: #ffdc99;
57
+ --ps1ui-code-punctuation: #7a8593;
58
+ /* Diff-specific: green = added, rose = removed. Kept separate from tag/string so
59
+ the semantic overlap doesn't leak between grammars (a removed diff line has
60
+ nothing to do with an HTML <tag>, and an added line isn't a string literal). */
61
+ --ps1ui-code-inserted: #7ee787;
62
+ --ps1ui-code-deleted: #ff8896;
63
+
64
+ --ps1ui-radius-md: 3px;
65
+ --ps1ui-radius-lg: 4px;
66
+
67
+ --ps1ui-space-xs: 4px;
68
+ --ps1ui-space-sm: 8px;
69
+ --ps1ui-space-md: 12px;
70
+ --ps1ui-space-lg: 16px;
71
+ --ps1ui-space-xl: 24px;
72
+ --ps1ui-space-2xl: 32px;
73
+
74
+ /* Vertical padding for 1px-bordered controls (Button, Input): border +
75
+ padding = --ps1ui-space-sm (8px), so this is a derived value, not a new
76
+ spacing step. */
77
+ --ps1ui-control-padding-y: 7px;
78
+
79
+ /* Container max-widths, in rem so they scale with the user's browser
80
+ font-size preference (WCAG-friendly). Numeric equivalents at the
81
+ browser-default 16px root: 40 = 640, 48 = 768, 64 = 1024, 80 = 1280.
82
+ Doubles as the breakpoint scale — see --ps1ui-breakpoint-* below. */
83
+ --ps1ui-container-sm: 40rem;
84
+ --ps1ui-container-md: 48rem;
85
+ --ps1ui-container-lg: 64rem;
86
+ --ps1ui-container-xl: 80rem;
87
+
88
+ /* Responsive breakpoints, in rem for the same accessibility reason.
89
+ Values are identical to --ps1ui-container-* by design: a Container
90
+ of size=lg (64rem) fills at exactly the `lg` breakpoint. These are
91
+ documentation tokens — @container / @media conditions cannot use
92
+ `var()` per the CSS spec, so the same numeric values are also
93
+ hardcoded in each component's CSS. Keep in sync. */
94
+ --ps1ui-breakpoint-sm: 40rem;
95
+ --ps1ui-breakpoint-md: 48rem;
96
+ --ps1ui-breakpoint-lg: 64rem;
97
+ --ps1ui-breakpoint-xl: 80rem;
98
+
99
+ /* Type scale, in rem so text tracks the user's browser font-size preference
100
+ (same rationale as the container/breakpoint tokens above). All values sit
101
+ on the 0.125rem (2px) grid; px equivalents at the default 16px root:
102
+ 12 / 14 / 16 / 18 / 22 / 26 / 32. */
103
+ --ps1ui-font-size-xs: 0.75rem;
104
+ --ps1ui-font-size-sm: 0.875rem;
105
+ --ps1ui-font-size-md: 1rem;
106
+ --ps1ui-font-size-lg: 1.125rem;
107
+ --ps1ui-font-size-xl: 1.375rem;
108
+ --ps1ui-font-size-2xl: 1.625rem;
109
+ --ps1ui-font-size-3xl: 2rem;
110
+
111
+ --ps1ui-transition-fast: 120ms ease;
112
+ }
113
+
114
+ /* PS1Root establishes the app-level responsive containment context.
115
+ `container-type: inline-size` turns this element into a query context for
116
+ `@container` rules used by responsive ps1ui components. Named `ps1ui-root`
117
+ for future targeted queries; unnamed queries in component CSS match this
118
+ via nearest-ancestor lookup. */
119
+
120
+ /* Containment side-effect defense (align-self / justify-self / min-width) lives
121
+ in components.css's shared selector list — see the block header there. */
122
+
123
+ .ps1ui-root {
124
+ container: ps1ui-root / inline-size;
125
+ }
126
+
127
+ .ps1ui-anchor {
128
+ font-family: var(--ps1ui-font-mono);
129
+ text-decoration: underline;
130
+ text-underline-offset: 2px;
131
+ text-decoration-thickness: 1px;
132
+ border-radius: var(--ps1ui-radius-md);
133
+ cursor: pointer;
134
+ transition:
135
+ color var(--ps1ui-transition-fast),
136
+ text-decoration-color var(--ps1ui-transition-fast);
137
+ }
138
+
139
+ /* outline (not box-shadow) so a wrapping inline anchor renders as connected fragments
140
+ per line-box instead of two disjoint rectangles. Modern browsers follow border-radius. */
141
+
142
+ .ps1ui-anchor:focus-visible {
143
+ outline: 2px solid var(--ps1ui-color-primary);
144
+ outline-offset: 2px;
145
+ }
146
+
147
+ /* text-decoration-color defaults to currentColor, so setting only `color` per state
148
+ also shifts the underline. subtle's default intentionally decouples the two — the
149
+ underline is dimmer than the text — hence the explicit override there. */
150
+
151
+ .ps1ui-anchor--primary {
152
+ color: var(--ps1ui-color-primary);
153
+ }
154
+
155
+ .ps1ui-anchor--primary:hover {
156
+ color: var(--ps1ui-color-primary-hover);
157
+ }
158
+
159
+ .ps1ui-anchor--primary:active {
160
+ color: var(--ps1ui-color-primary-active);
161
+ }
162
+
163
+ .ps1ui-anchor--subtle {
164
+ color: inherit;
165
+ text-decoration-color: var(--ps1ui-color-fg-subtle);
166
+ }
167
+
168
+ /* Reset text-decoration-color to currentColor so the underline follows the
169
+ hover/active color shift; without this it would stay pinned to fg-subtle
170
+ from the base rule. */
171
+
172
+ .ps1ui-anchor--subtle:hover {
173
+ color: var(--ps1ui-color-primary);
174
+ text-decoration-color: currentColor;
175
+ }
176
+
177
+ .ps1ui-anchor--subtle:active {
178
+ color: var(--ps1ui-color-primary-active);
179
+ text-decoration-color: currentColor;
180
+ }
181
+
182
+ .ps1ui-button {
183
+ display: inline-flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ gap: var(--ps1ui-space-sm);
187
+ font-family: var(--ps1ui-font-mono);
188
+ font-size: var(--ps1ui-font-size-sm);
189
+ /* Applied at base rather than per-variant so primary and secondary read as
190
+ equal-weight siblings — variant should encode affordance (fill vs outline),
191
+ not typography weight. A per-variant weight difference used to make
192
+ secondary feel "one size smaller" next to primary. */
193
+ font-weight: 600;
194
+ line-height: 1;
195
+ /* 14px: 2× the 7px vertical rhythm; deliberately off the space scale. */
196
+ padding: var(--ps1ui-control-padding-y) 14px;
197
+ border-radius: var(--ps1ui-radius-md);
198
+ border: 1px solid transparent;
199
+ cursor: pointer;
200
+ -webkit-user-select: none;
201
+ -moz-user-select: none;
202
+ user-select: none;
203
+ transition:
204
+ background var(--ps1ui-transition-fast),
205
+ border-color var(--ps1ui-transition-fast),
206
+ color var(--ps1ui-transition-fast);
207
+ }
208
+
209
+ .ps1ui-button:focus-visible {
210
+ outline: none;
211
+ border-color: var(--ps1ui-color-primary);
212
+ box-shadow: var(--ps1ui-shadow-focus-ring);
213
+ }
214
+
215
+ .ps1ui-button:disabled {
216
+ cursor: not-allowed;
217
+ }
218
+
219
+ .ps1ui-button--primary {
220
+ background: var(--ps1ui-color-primary);
221
+ color: var(--ps1ui-color-primary-fg);
222
+ border-color: var(--ps1ui-color-primary);
223
+ }
224
+
225
+ .ps1ui-button--primary:hover:not(:disabled) {
226
+ background: var(--ps1ui-color-primary-hover);
227
+ border-color: var(--ps1ui-color-primary-hover);
228
+ }
229
+
230
+ .ps1ui-button--primary:active:not(:disabled) {
231
+ background: var(--ps1ui-color-primary-active);
232
+ border-color: var(--ps1ui-color-primary-active);
233
+ }
234
+
235
+ .ps1ui-button--primary:disabled {
236
+ background: transparent;
237
+ color: var(--ps1ui-color-fg-subtle);
238
+ border-color: var(--ps1ui-color-border);
239
+ }
240
+
241
+ .ps1ui-button--secondary {
242
+ background: transparent;
243
+ color: var(--ps1ui-color-fg);
244
+ /* fg-subtle (~5:1 vs --ps1ui-color-bg) instead of border-strong (~1.7:1) so
245
+ the 1px outline actually defines the click target on the dark canvas —
246
+ otherwise the boundary reads as a ghost and the button visually shrinks
247
+ down to its text glyphs next to a solid primary. */
248
+ border-color: var(--ps1ui-color-fg-subtle);
249
+ }
250
+
251
+ .ps1ui-button--secondary:hover:not(:disabled) {
252
+ border-color: var(--ps1ui-color-primary);
253
+ background: color-mix(in srgb, var(--ps1ui-color-primary) 6%, transparent);
254
+ }
255
+
256
+ .ps1ui-button--secondary:active:not(:disabled) {
257
+ border-color: var(--ps1ui-color-primary-active);
258
+ background: color-mix(in srgb, var(--ps1ui-color-primary-active) 10%, transparent);
259
+ }
260
+
261
+ .ps1ui-button--secondary:disabled {
262
+ color: var(--ps1ui-color-fg-subtle);
263
+ border-color: var(--ps1ui-color-border);
264
+ }
265
+
266
+ .ps1ui-checkbox {
267
+ -moz-appearance: none;
268
+ appearance: none;
269
+ -webkit-appearance: none;
270
+ width: 16px;
271
+ height: 16px;
272
+ margin: 0;
273
+ padding: 0;
274
+ box-sizing: border-box;
275
+ display: inline-block;
276
+ position: relative;
277
+ vertical-align: middle;
278
+ flex-shrink: 0;
279
+ font-family: var(--ps1ui-font-mono);
280
+ color: var(--ps1ui-color-primary-fg);
281
+ background: var(--ps1ui-color-bg);
282
+ border: 1px solid var(--ps1ui-color-border-strong);
283
+ border-radius: var(--ps1ui-radius-md);
284
+ cursor: pointer;
285
+ transition:
286
+ background var(--ps1ui-transition-fast),
287
+ border-color var(--ps1ui-transition-fast),
288
+ box-shadow var(--ps1ui-transition-fast);
289
+ }
290
+
291
+ .ps1ui-checkbox:hover:not(:disabled) {
292
+ border-color: var(--ps1ui-color-primary);
293
+ }
294
+
295
+ .ps1ui-checkbox:focus-visible {
296
+ outline: none;
297
+ border-color: var(--ps1ui-color-primary);
298
+ box-shadow: var(--ps1ui-shadow-focus-ring);
299
+ }
300
+
301
+ .ps1ui-checkbox:checked,
302
+ .ps1ui-checkbox:indeterminate {
303
+ background: var(--ps1ui-color-primary);
304
+ border-color: var(--ps1ui-color-primary);
305
+ }
306
+
307
+ .ps1ui-checkbox:checked:hover:not(:disabled),
308
+ .ps1ui-checkbox:indeterminate:hover:not(:disabled) {
309
+ background: var(--ps1ui-color-primary-hover);
310
+ border-color: var(--ps1ui-color-primary-hover);
311
+ }
312
+
313
+ .ps1ui-checkbox:checked:active:not(:disabled),
314
+ .ps1ui-checkbox:indeterminate:active:not(:disabled) {
315
+ background: var(--ps1ui-color-primary-active);
316
+ border-color: var(--ps1ui-color-primary-active);
317
+ }
318
+
319
+ /* Checkmark: classic rotated-L trick — draws the right + bottom border of a small
320
+ rectangle, then rotates 45deg so it reads as a ✓. `:not(:indeterminate)` makes
321
+ precedence explicit for the mixed DOM state (both .checked and .indeterminate
322
+ true) so the indeterminate bar wins without relying on source order. Uses
323
+ `currentColor` so the disabled state can dim the mark by changing `color`.
324
+ Depends on the pseudo's initial `box-sizing: content-box` so the 2px borders
325
+ sit OUTSIDE the 4×8 content — base.css's reset intentionally excludes pseudo
326
+ elements from its `box-sizing: border-box` block for exactly this pattern.
327
+ Rendered geometry locked in by Checkbox.checkmark-geometry.test.tsx. */
328
+
329
+ .ps1ui-checkbox:checked:not(:indeterminate)::after {
330
+ content: "";
331
+ position: absolute;
332
+ left: 4px;
333
+ top: 1px;
334
+ width: 4px;
335
+ height: 8px;
336
+ border-style: solid;
337
+ border-color: currentColor;
338
+ border-width: 0 2px 2px 0;
339
+ transform: rotate(45deg);
340
+ }
341
+
342
+ .ps1ui-checkbox:indeterminate::after {
343
+ content: "";
344
+ position: absolute;
345
+ left: 2px;
346
+ top: 6px;
347
+ width: 10px;
348
+ height: 2px;
349
+ background: currentColor;
350
+ border-radius: 1px;
351
+ }
352
+
353
+ /* forced-colors force-replaces author backgrounds, so the background-drawn
354
+ indeterminate bar would vanish — redraw it as a border, which survives as
355
+ system ink (same 6–8px band; the checked ✓ is already border-drawn and
356
+ needs nothing). The focus-outline counterpart lives in the grouped
357
+ forced-colors rule in styles/components.css. Tested in
358
+ src/styles/forced-colors.test.tsx. */
359
+
360
+ @media (forced-colors: active) {
361
+ .ps1ui-checkbox:indeterminate::after {
362
+ background: none;
363
+ height: 0;
364
+ border-top: 2px solid currentColor;
365
+ border-radius: 0;
366
+ }
367
+ }
368
+
369
+ .ps1ui-checkbox:disabled {
370
+ cursor: not-allowed;
371
+ color: var(--ps1ui-color-fg-subtle);
372
+ background: var(--ps1ui-color-surface);
373
+ border-color: var(--ps1ui-color-border);
374
+ }
375
+
376
+ .ps1ui-checkbox:disabled:checked,
377
+ .ps1ui-checkbox:disabled:indeterminate {
378
+ background: var(--ps1ui-color-surface);
379
+ border-color: var(--ps1ui-color-border);
380
+ }
381
+
382
+ /* Input custom properties emitted by Container.tsx per set responsive-prop
383
+ breakpoint (`--_container-<axis>-<bp>`). Registered as non-inheriting for
384
+ the same reason as Stack — see Stack.css's @property block for the full
385
+ rationale (CSS Custom Properties Level 1 §2 inheritance default;
386
+ `unset`/`revert` won't block; only `initial` or `@property inherits:false`
387
+ works). Any new responsive axis added to Container.tsx must add matching
388
+ @property blocks here. */
389
+
390
+ @property --_container-size-base {
391
+ syntax: "*";
392
+ inherits: false;
393
+ }
394
+
395
+ @property --_container-size-sm {
396
+ syntax: "*";
397
+ inherits: false;
398
+ }
399
+
400
+ @property --_container-size-md {
401
+ syntax: "*";
402
+ inherits: false;
403
+ }
404
+
405
+ @property --_container-size-lg {
406
+ syntax: "*";
407
+ inherits: false;
408
+ }
409
+
410
+ @property --_container-size-xl {
411
+ syntax: "*";
412
+ inherits: false;
413
+ }
414
+
415
+ @property --_container-px-base {
416
+ syntax: "*";
417
+ inherits: false;
418
+ }
419
+
420
+ @property --_container-px-sm {
421
+ syntax: "*";
422
+ inherits: false;
423
+ }
424
+
425
+ @property --_container-px-md {
426
+ syntax: "*";
427
+ inherits: false;
428
+ }
429
+
430
+ @property --_container-px-lg {
431
+ syntax: "*";
432
+ inherits: false;
433
+ }
434
+
435
+ @property --_container-px-xl {
436
+ syntax: "*";
437
+ inherits: false;
438
+ }
439
+
440
+ /* Container — max-width + horizontal padding + auto centering.
441
+ Establishes an inline-size containment context so descendants respond to
442
+ Container's width via `@container` queries. Named `ps1ui-container` for
443
+ future targeted queries.
444
+
445
+ Responsive values propagate via a CSS custom-property cascade. The
446
+ component emits `--_container-size-{bp}` / `--_container-px-{bp}` via
447
+ inline style (see Container.tsx). Each `@container (min-width: X)` block
448
+ below overrides `--_size` / `--_px` with the value for the matching
449
+ breakpoint — the fallback chain in `--_size-{bp}` inherits the previous
450
+ breakpoint's effective value so a partial responsive object cascades
451
+ naturally.
452
+
453
+ Breakpoint thresholds mirror --ps1ui-breakpoint-* in tokens.css; kept
454
+ inline as literal values because `@container` conditions cannot resolve
455
+ `var()`. Keep them in sync. */
456
+
457
+ /* Containment side-effect defense (align-self / justify-self / min-width) lives
458
+ in components.css's shared selector list — see the block header there. */
459
+
460
+ .ps1ui-container {
461
+ container: ps1ui-container / inline-size;
462
+ width: 100%;
463
+ margin-inline: auto;
464
+
465
+ --_size-base: var(--_container-size-base, var(--ps1ui-container-lg));
466
+ --_size-sm: var(--_container-size-sm, var(--_size-base));
467
+ --_size-md: var(--_container-size-md, var(--_size-sm));
468
+ --_size-lg: var(--_container-size-lg, var(--_size-md));
469
+ --_size-xl: var(--_container-size-xl, var(--_size-lg));
470
+
471
+ --_px-base: var(--_container-px-base, var(--ps1ui-space-lg));
472
+ --_px-sm: var(--_container-px-sm, var(--_px-base));
473
+ --_px-md: var(--_container-px-md, var(--_px-sm));
474
+ --_px-lg: var(--_container-px-lg, var(--_px-md));
475
+ --_px-xl: var(--_container-px-xl, var(--_px-lg));
476
+
477
+ --_size: var(--_size-base);
478
+ --_px: var(--_px-base);
479
+
480
+ max-width: var(--_size);
481
+ padding-inline: var(--_px);
482
+ }
483
+
484
+ @container (min-width: 40rem) {
485
+ .ps1ui-container {
486
+ --_size: var(--_size-sm);
487
+ --_px: var(--_px-sm);
488
+ }
489
+ }
490
+
491
+ @container (min-width: 48rem) {
492
+ .ps1ui-container {
493
+ --_size: var(--_size-md);
494
+ --_px: var(--_px-md);
495
+ }
496
+ }
497
+
498
+ @container (min-width: 64rem) {
499
+ .ps1ui-container {
500
+ --_size: var(--_size-lg);
501
+ --_px: var(--_px-lg);
502
+ }
503
+ }
504
+
505
+ @container (min-width: 80rem) {
506
+ .ps1ui-container {
507
+ --_size: var(--_size-xl);
508
+ --_px: var(--_px-xl);
509
+ }
510
+ }
511
+
512
+ /* Input custom properties emitted by Grid.tsx per set responsive-prop
513
+ breakpoint (`--_grid-<axis>-<bp>`). Registered as non-inheriting for the
514
+ same reason as Stack — see Stack.css's @property block for the full
515
+ rationale. Any new responsive axis added to Grid.tsx must add matching
516
+ @property blocks here. */
517
+
518
+ @property --_grid-columns-base {
519
+ syntax: "*";
520
+ inherits: false;
521
+ }
522
+
523
+ @property --_grid-columns-sm {
524
+ syntax: "*";
525
+ inherits: false;
526
+ }
527
+
528
+ @property --_grid-columns-md {
529
+ syntax: "*";
530
+ inherits: false;
531
+ }
532
+
533
+ @property --_grid-columns-lg {
534
+ syntax: "*";
535
+ inherits: false;
536
+ }
537
+
538
+ @property --_grid-columns-xl {
539
+ syntax: "*";
540
+ inherits: false;
541
+ }
542
+
543
+ @property --_grid-gap-base {
544
+ syntax: "*";
545
+ inherits: false;
546
+ }
547
+
548
+ @property --_grid-gap-sm {
549
+ syntax: "*";
550
+ inherits: false;
551
+ }
552
+
553
+ @property --_grid-gap-md {
554
+ syntax: "*";
555
+ inherits: false;
556
+ }
557
+
558
+ @property --_grid-gap-lg {
559
+ syntax: "*";
560
+ inherits: false;
561
+ }
562
+
563
+ @property --_grid-gap-xl {
564
+ syntax: "*";
565
+ inherits: false;
566
+ }
567
+
568
+ /* Grid — display:grid + N-column tracks + gap.
569
+ Establishes an inline-size containment context so descendants respond to
570
+ Grid's width via `@container` queries. Named `ps1ui-grid` for future
571
+ targeted queries.
572
+
573
+ Responsive values propagate via a CSS custom-property cascade. The
574
+ component emits `--_grid-columns-{bp}` / `--_grid-gap-{bp}` via inline
575
+ style (see Grid.tsx). Each `@container (min-width: X)` block below
576
+ overrides `--_columns` / `--_gap` with the value for the matching
577
+ breakpoint; the fallback chain in `--_columns-{bp}` inherits the
578
+ previous breakpoint's effective value so a partial responsive object
579
+ cascades naturally.
580
+
581
+ Breakpoint thresholds mirror --ps1ui-breakpoint-* in tokens.css; kept
582
+ inline as literal values because `@container` conditions cannot resolve
583
+ `var()`. Keep them in sync. */
584
+
585
+ /* Containment side-effect defense (align-self / justify-self / min-width) lives
586
+ in components.css's shared selector list — see the block header there. */
587
+
588
+ .ps1ui-grid {
589
+ container: ps1ui-grid / inline-size;
590
+ display: grid;
591
+
592
+ --_columns-base: var(--_grid-columns-base, 1);
593
+ --_columns-sm: var(--_grid-columns-sm, var(--_columns-base));
594
+ --_columns-md: var(--_grid-columns-md, var(--_columns-sm));
595
+ --_columns-lg: var(--_grid-columns-lg, var(--_columns-md));
596
+ --_columns-xl: var(--_grid-columns-xl, var(--_columns-lg));
597
+
598
+ --_gap-base: var(--_grid-gap-base, var(--ps1ui-space-md));
599
+ --_gap-sm: var(--_grid-gap-sm, var(--_gap-base));
600
+ --_gap-md: var(--_grid-gap-md, var(--_gap-sm));
601
+ --_gap-lg: var(--_grid-gap-lg, var(--_gap-md));
602
+ --_gap-xl: var(--_grid-gap-xl, var(--_gap-lg));
603
+
604
+ --_columns: var(--_columns-base);
605
+ --_gap: var(--_gap-base);
606
+
607
+ grid-template-columns: repeat(var(--_columns), minmax(0, 1fr));
608
+ gap: var(--_gap);
609
+ }
610
+
611
+ @container (min-width: 40rem) {
612
+ .ps1ui-grid {
613
+ --_columns: var(--_columns-sm);
614
+ --_gap: var(--_gap-sm);
615
+ }
616
+ }
617
+
618
+ @container (min-width: 48rem) {
619
+ .ps1ui-grid {
620
+ --_columns: var(--_columns-md);
621
+ --_gap: var(--_gap-md);
622
+ }
623
+ }
624
+
625
+ @container (min-width: 64rem) {
626
+ .ps1ui-grid {
627
+ --_columns: var(--_columns-lg);
628
+ --_gap: var(--_gap-lg);
629
+ }
630
+ }
631
+
632
+ @container (min-width: 80rem) {
633
+ .ps1ui-grid {
634
+ --_columns: var(--_columns-xl);
635
+ --_gap: var(--_gap-xl);
636
+ }
637
+ }
638
+
639
+ /* Input custom properties emitted by GridItem.tsx per set responsive-prop
640
+ breakpoint (`--_griditem-col-span-<bp>`). Registered as non-inheriting for
641
+ the same reason as Stack — see Stack.css's @property block for the full
642
+ rationale. Any new responsive axis added to GridItem.tsx must add matching
643
+ @property blocks here. */
644
+
645
+ @property --_griditem-col-span-base {
646
+ syntax: "*";
647
+ inherits: false;
648
+ }
649
+
650
+ @property --_griditem-col-span-sm {
651
+ syntax: "*";
652
+ inherits: false;
653
+ }
654
+
655
+ @property --_griditem-col-span-md {
656
+ syntax: "*";
657
+ inherits: false;
658
+ }
659
+
660
+ @property --_griditem-col-span-lg {
661
+ syntax: "*";
662
+ inherits: false;
663
+ }
664
+
665
+ @property --_griditem-col-span-xl {
666
+ syntax: "*";
667
+ inherits: false;
668
+ }
669
+
670
+ /* GridItem — child of Grid that spans multiple column tracks.
671
+ GridItem is a leaf: it queries an ancestor containment context (typically
672
+ the parent Grid, which establishes `container: ps1ui-grid / inline-size`),
673
+ but does NOT establish one itself. Establishing containment on every
674
+ GridItem would fragment the query surface for descendants and impose a
675
+ `contain: layout` side effect on every cell — needless overhead for a
676
+ component whose only job is to declare its own column span.
677
+
678
+ Responsive values propagate via a CSS custom-property cascade. The
679
+ component emits `--_griditem-col-span-{bp}` via inline style (see
680
+ GridItem.tsx). Each `@container (min-width: X)` block below overrides
681
+ `--_col-span` with the value for the matching breakpoint; the fallback
682
+ chain in `--_col-span-{bp}` inherits the previous breakpoint's effective
683
+ value so a partial responsive object cascades naturally.
684
+
685
+ Breakpoint thresholds mirror --ps1ui-breakpoint-* in tokens.css; kept
686
+ inline as literal values because `@container` conditions cannot resolve
687
+ `var()`. Keep them in sync.
688
+
689
+ Fallback default: `1` — a bare `<GridItem>` with no colSpan behaves
690
+ exactly like a plain grid child (`grid-column: span 1`), so wrapping
691
+ existing Grid children in `<GridItem>` never changes layout by itself.
692
+
693
+ Used outside a display:grid parent, `grid-column` is an inert property —
694
+ the item still renders, the declaration is simply ignored. That matches
695
+ the "no runtime enforcement of parent type" convention used across the
696
+ library. */
697
+
698
+ .ps1ui-griditem {
699
+ --_col-span-base: var(--_griditem-col-span-base, 1);
700
+ --_col-span-sm: var(--_griditem-col-span-sm, var(--_col-span-base));
701
+ --_col-span-md: var(--_griditem-col-span-md, var(--_col-span-sm));
702
+ --_col-span-lg: var(--_griditem-col-span-lg, var(--_col-span-md));
703
+ --_col-span-xl: var(--_griditem-col-span-xl, var(--_col-span-lg));
704
+
705
+ --_col-span: var(--_col-span-base);
706
+
707
+ grid-column: span var(--_col-span);
708
+ }
709
+
710
+ @container (min-width: 40rem) {
711
+ .ps1ui-griditem {
712
+ --_col-span: var(--_col-span-sm);
713
+ }
714
+ }
715
+
716
+ @container (min-width: 48rem) {
717
+ .ps1ui-griditem {
718
+ --_col-span: var(--_col-span-md);
719
+ }
720
+ }
721
+
722
+ @container (min-width: 64rem) {
723
+ .ps1ui-griditem {
724
+ --_col-span: var(--_col-span-lg);
725
+ }
726
+ }
727
+
728
+ @container (min-width: 80rem) {
729
+ .ps1ui-griditem {
730
+ --_col-span: var(--_col-span-xl);
731
+ }
732
+ }
733
+
734
+ .ps1ui-input {
735
+ display: block;
736
+ width: 100%;
737
+ box-sizing: border-box;
738
+ font-family: var(--ps1ui-font-mono);
739
+ font-size: var(--ps1ui-font-size-sm);
740
+ line-height: 1.4;
741
+ color: var(--ps1ui-color-fg);
742
+ background: var(--ps1ui-color-bg);
743
+ border: 1px solid var(--ps1ui-color-border-strong);
744
+ border-radius: var(--ps1ui-radius-md);
745
+ padding: var(--ps1ui-control-padding-y) var(--ps1ui-space-md);
746
+ transition:
747
+ border-color var(--ps1ui-transition-fast),
748
+ box-shadow var(--ps1ui-transition-fast);
749
+ }
750
+
751
+ .ps1ui-input::-moz-placeholder {
752
+ color: var(--ps1ui-color-fg-subtle);
753
+ }
754
+
755
+ .ps1ui-input::placeholder {
756
+ color: var(--ps1ui-color-fg-subtle);
757
+ }
758
+
759
+ .ps1ui-input:hover:not(:disabled) {
760
+ border-color: var(--ps1ui-color-primary);
761
+ }
762
+
763
+ .ps1ui-input:focus {
764
+ outline: none;
765
+ border-color: var(--ps1ui-color-primary);
766
+ box-shadow: var(--ps1ui-shadow-focus-ring);
767
+ }
768
+
769
+ .ps1ui-input:disabled {
770
+ cursor: not-allowed;
771
+ color: var(--ps1ui-color-fg-subtle);
772
+ border-color: var(--ps1ui-color-border);
773
+ background: var(--ps1ui-color-surface);
774
+ }
775
+
776
+ .ps1ui-label {
777
+ display: inline-block;
778
+ font-family: var(--ps1ui-font-mono);
779
+ font-size: var(--ps1ui-font-size-sm);
780
+ font-weight: 500;
781
+ line-height: 1.6;
782
+ color: var(--ps1ui-color-fg);
783
+ }
784
+
785
+ /* List — semantic <ul>/<ol> wrapper with terminal-style markers.
786
+
787
+ The global reset in base.css sets `list-style: none` on every <ul>/<ol>,
788
+ which strips the UA bullet/number glyph. List re-introduces a marker via
789
+ `::before` on each direct <li> — `-` for unordered, `NN.` for ordered —
790
+ so the "reset removes bullets" side effect turns into an on-brand
791
+ terminal glyph rather than a hole.
792
+
793
+ The ordered marker uses `counter(list-item)` — the browser-maintained
794
+ list-item counter that already honours `<ol start>`, `<ol reversed>`,
795
+ and per-item `<li value>`. Using a hand-rolled `counter-reset` /
796
+ `counter-increment` would silently ignore all three attributes and
797
+ diverge from what assistive tech announcing the native counter reads.
798
+
799
+ `::before` (not `::marker`) is used because `::marker` styleability
800
+ lands late in some engines (custom `content` on `::marker` only reached
801
+ Safari 17) and because the inline-block `::before` + negative margin
802
+ pattern composes cleanly with block-flow content — a nested <List>
803
+ inside a <li> flows below the item text, with its own indent stacking
804
+ on top of the outer's. An earlier `display: grid` layout collided here
805
+ (nested list fell into the marker column and shoved the outer text
806
+ right); block flow avoids all of it.
807
+
808
+ Forced-colors (Windows High Contrast) repaints the `::before` glyph
809
+ with `CanvasText` — the marker survives as system-foreground ink; no
810
+ forced-colors-specific defense is needed. */
811
+
812
+ .ps1ui-list {
813
+ display: flex;
814
+ flex-direction: column;
815
+ gap: var(--ps1ui-space-xs);
816
+ padding: 0;
817
+ margin: 0;
818
+ color: var(--ps1ui-color-fg);
819
+ font-family: var(--ps1ui-font-mono);
820
+ font-size: var(--ps1ui-font-size-sm);
821
+ line-height: 1.6;
822
+ }
823
+
824
+ /* Hanging indent — `padding-left` reserves the marker column; the ::before
825
+ is inline-block with a negative `margin-left` that pulls it into that
826
+ reservation. Wrapped item text continues from the padding-left position
827
+ on line 2+, so long items don't slip back under the marker. Block-flow
828
+ content nested inside the <li> (a sub <List>, a paragraph, etc.) also
829
+ respects the padding-left, so it renders indented under the item's text
830
+ rather than under the marker.
831
+
832
+ Marker column widths are hard-coded per variant (not lifted to a custom
833
+ property) because the same numeric value shows up in `padding-left` on
834
+ the <li> and in `width` / `margin-left` on the ::before; a shared var
835
+ would need to be either non-inheriting (breaking pseudo-element access)
836
+ or inheriting (leaking into descendants and violating the codebase's
837
+ `--_*` = non-inheriting convention). Duplicating a `1ch` / `3ch`
838
+ literal is the smaller cost.
839
+
840
+ Ordered's `3ch` marker column fits `1.` through `99.`; lists past 99
841
+ items visually break the right-align column — an acceptable trade for
842
+ the common < 100 case. */
843
+
844
+ .ps1ui-list--unordered > li {
845
+ padding-left: calc(1ch + var(--ps1ui-space-sm));
846
+ }
847
+
848
+ .ps1ui-list--ordered > li {
849
+ padding-left: calc(3ch + var(--ps1ui-space-sm));
850
+ }
851
+
852
+ .ps1ui-list > li::before {
853
+ display: inline-block;
854
+ padding-right: var(--ps1ui-space-sm);
855
+ color: var(--ps1ui-color-fg-muted);
856
+ font-variant-numeric: tabular-nums;
857
+ text-align: right;
858
+ }
859
+
860
+ .ps1ui-list--unordered > li::before {
861
+ content: "-";
862
+ width: 1ch;
863
+ margin-left: calc((1ch + var(--ps1ui-space-sm)) * -1);
864
+ }
865
+
866
+ .ps1ui-list--ordered > li::before {
867
+ content: counter(list-item) ".";
868
+ width: 3ch;
869
+ margin-left: calc((3ch + var(--ps1ui-space-sm)) * -1);
870
+ }
871
+
872
+ /* Breathing room above a nested <List>. Descendant combinator (not child)
873
+ so the rule still applies when a consumer wraps the sublist in a layout
874
+ primitive — `<li><Stack><List>…</List></Stack></li>` gets the same gap
875
+ as `<li><List>…</List></li>`. The outer flex `gap` on `.ps1ui-list`
876
+ only reaches direct children, so without this rule any nested list
877
+ loses breathing room. */
878
+
879
+ .ps1ui-list li .ps1ui-list {
880
+ margin-top: var(--ps1ui-space-xs);
881
+ }
882
+
883
+ /* ListItem — no rules of its own.
884
+
885
+ The `.ps1ui-listitem` class (naming parallels `.ps1ui-griditem`) is a
886
+ stable API hook for consumers. All marker / padding / hanging-indent
887
+ rules live on `.ps1ui-list > li` in List.css so a bare `<li>` inside
888
+ `<List>` continues to render identically to `<ListItem>`. Keeping this
889
+ file present (and imported from components.css) matches the
890
+ per-component file layout the `new-component` skill enforces. */
891
+
892
+ .ps1ui-card {
893
+ background: var(--ps1ui-color-surface);
894
+ border: 1px solid var(--ps1ui-color-border);
895
+ border-radius: var(--ps1ui-radius-lg);
896
+ padding: var(--ps1ui-space-xl);
897
+ color: var(--ps1ui-color-fg);
898
+ font-family: var(--ps1ui-font-mono);
899
+ font-size: var(--ps1ui-font-size-sm);
900
+ line-height: 1.6;
901
+ }
902
+
903
+ /* Code — inline code chip.
904
+
905
+ Companion to <CodeBlock>, but for inline fragments that live inside body
906
+ text: `<Text>call <Code>useState()</Code> to add state.</Text>`.
907
+
908
+ Font-size is `0.9em` (relative) so the chip auto-scales with the surrounding
909
+ text. Inside a default `<Text size="sm">` (14 px) the chip renders at
910
+ ~12.6 px, matching a small-caps rhythm without overriding the parent's
911
+ size axis. Consumers who want an absolute size still can — an inline
912
+ `style={{ fontSize: ... }}` outranks this rule.
913
+
914
+ `overflow-wrap: anywhere` lets long identifiers (`veryLongIdentifier`,
915
+ `a.b.c.d`, file paths) break inside a narrow container instead of forcing
916
+ a horizontal scroll on the parent. `break-word` was rejected because it
917
+ only fires when NO other soft-wrap opportunity exists on the line — that
918
+ fails the common "long path inside a short label" case. `anywhere` is
919
+ the correct choice for inline code fragments where preserving whole-word
920
+ integrity has no value (code has no natural word breaks).
921
+
922
+ Font-family is set explicitly (rather than relying on base.css's `code {
923
+ font-family: var(--ps1ui-font-mono); }` reset) because Code must render
924
+ the same way when consumed via the components-only CSS entry, which
925
+ ships no reset. Same reason Card/Text/etc. redeclare font-family.
926
+
927
+ No variant axis: inline code is overwhelmingly used at a single visual
928
+ weight. Consumers who need a colored fragment can override via inline
929
+ style (`style={{ color: "var(--ps1ui-color-primary)" }}`) or a caller
930
+ className — the base chip stays a single API surface.
931
+
932
+ `padding-block: 0.1em` assumes the parent's line-height leaves at least
933
+ ~0.1em of vertical slack per side. Inline padding does NOT extend the
934
+ line box (per CSS 2.1 §10.8), so an outer `line-height: 1` container
935
+ would paint the chip's background/border into the adjacent line. All
936
+ in-repo callers (Text = 1.6, Heading = 1.3) leave ample slack;
937
+ consumers who tighten line-height below ~1.2 need to widen it around
938
+ inline `<Code>` themselves. */
939
+
940
+ .ps1ui-code {
941
+ font-family: var(--ps1ui-font-mono);
942
+ font-size: 0.9em;
943
+ padding: 0.1em 0.35em;
944
+ background: var(--ps1ui-color-surface);
945
+ border: 1px solid var(--ps1ui-color-border);
946
+ border-radius: var(--ps1ui-radius-md);
947
+ color: var(--ps1ui-color-fg);
948
+ overflow-wrap: anywhere;
949
+ }
950
+
951
+ .ps1ui-codeblock {
952
+ margin: 0;
953
+ padding: var(--ps1ui-space-md) var(--ps1ui-space-lg);
954
+ background: var(--ps1ui-color-surface);
955
+ border: 1px solid var(--ps1ui-color-border);
956
+ border-radius: var(--ps1ui-radius-lg);
957
+ color: var(--ps1ui-color-fg);
958
+ font-family: var(--ps1ui-font-mono);
959
+ font-size: var(--ps1ui-font-size-sm);
960
+ line-height: 1.6;
961
+ overflow-x: auto;
962
+ -moz-tab-size: 2;
963
+ -o-tab-size: 2;
964
+ tab-size: 2;
965
+ /* Explicit `left` (not `start`) — <pre> defaults to text-align: inherit, so a
966
+ consumer that centers text on an ancestor would center each code line too.
967
+ `left` (rather than `start`) is deliberate: source code is inherently LTR
968
+ and should stay left-aligned even inside an RTL context. */
969
+ text-align: left;
970
+ /* Force LTR text flow so code renders identically inside `<html dir="rtl">` /
971
+ `<div dir="rtl">` ancestors — the character order and the horizontal scroll
972
+ direction both stay consistent with what a developer expects from code. */
973
+ direction: ltr;
974
+ /* Explicit `pre` overrides consumer reset CSS that globally sets
975
+ `pre { white-space: pre-wrap; }` — wrapping would defeat overflow-x: auto
976
+ and collapse the scrollable-region focus semantics. Per-instance opt-in
977
+ to wrapping is still possible via `style={{ whiteSpace: "pre-wrap" }}`. */
978
+ white-space: pre;
979
+ }
980
+
981
+ .ps1ui-codeblock:focus-visible {
982
+ outline: none;
983
+ border-color: var(--ps1ui-color-primary);
984
+ box-shadow: var(--ps1ui-shadow-focus-ring);
985
+ }
986
+
987
+ .ps1ui-codeblock code {
988
+ font-family: inherit;
989
+ font-size: inherit;
990
+ color: inherit;
991
+ background: none;
992
+ }
993
+
994
+ /* Prism token classes, mapped to ps1ui code palette (see tokens.css).
995
+ Refractor emits hast <span class="token <type>"> nodes; keep this list
996
+ in sync with the token types produced by Prism grammars. */
997
+
998
+ .ps1ui-codeblock .token.comment,
999
+ .ps1ui-codeblock .token.prolog,
1000
+ .ps1ui-codeblock .token.doctype,
1001
+ .ps1ui-codeblock .token.cdata {
1002
+ color: var(--ps1ui-code-comment);
1003
+ font-style: italic;
1004
+ }
1005
+
1006
+ .ps1ui-codeblock .token.punctuation,
1007
+ .ps1ui-codeblock .token.operator {
1008
+ color: var(--ps1ui-code-punctuation);
1009
+ }
1010
+
1011
+ .ps1ui-codeblock .token.property,
1012
+ .ps1ui-codeblock .token.tag,
1013
+ .ps1ui-codeblock .token.symbol {
1014
+ color: var(--ps1ui-code-tag);
1015
+ }
1016
+
1017
+ .ps1ui-codeblock .token.attr-name {
1018
+ color: var(--ps1ui-code-attr);
1019
+ }
1020
+
1021
+ .ps1ui-codeblock .token.string,
1022
+ .ps1ui-codeblock .token.char,
1023
+ .ps1ui-codeblock .token.url,
1024
+ .ps1ui-codeblock .token.attr-value,
1025
+ .ps1ui-codeblock .token.regex {
1026
+ color: var(--ps1ui-code-string);
1027
+ }
1028
+
1029
+ /* diff: line-level markers get their own hue so "+" reads as added (green) and
1030
+ "-" reads as removed (rose). Distinct from tag/string groups on purpose. */
1031
+
1032
+ .ps1ui-codeblock .token.inserted {
1033
+ color: var(--ps1ui-code-inserted);
1034
+ }
1035
+
1036
+ .ps1ui-codeblock .token.deleted {
1037
+ color: var(--ps1ui-code-deleted);
1038
+ }
1039
+
1040
+ .ps1ui-codeblock .token.number,
1041
+ .ps1ui-codeblock .token.boolean,
1042
+ .ps1ui-codeblock .token.constant,
1043
+ .ps1ui-codeblock .token.entity {
1044
+ color: var(--ps1ui-code-constant);
1045
+ }
1046
+
1047
+ .ps1ui-codeblock .token.class-name,
1048
+ .ps1ui-codeblock .token.builtin {
1049
+ color: var(--ps1ui-code-type);
1050
+ }
1051
+
1052
+ .ps1ui-codeblock .token.function,
1053
+ .ps1ui-codeblock .token.method {
1054
+ color: var(--ps1ui-code-function);
1055
+ }
1056
+
1057
+ .ps1ui-codeblock .token.keyword,
1058
+ .ps1ui-codeblock .token.selector,
1059
+ .ps1ui-codeblock .token.atrule,
1060
+ .ps1ui-codeblock .token.important {
1061
+ color: var(--ps1ui-code-keyword);
1062
+ }
1063
+
1064
+ .ps1ui-codeblock .token.important,
1065
+ .ps1ui-codeblock .token.bold {
1066
+ font-weight: 600;
1067
+ }
1068
+
1069
+ .ps1ui-codeblock .token.italic {
1070
+ font-style: italic;
1071
+ }
1072
+
1073
+ .ps1ui-details {
1074
+ background: var(--ps1ui-color-surface);
1075
+ border: 1px solid var(--ps1ui-color-border);
1076
+ border-radius: var(--ps1ui-radius-md);
1077
+ color: var(--ps1ui-color-fg);
1078
+ font-family: var(--ps1ui-font-mono);
1079
+ font-size: var(--ps1ui-font-size-sm);
1080
+ line-height: 1.6;
1081
+ }
1082
+
1083
+ .ps1ui-details__summary {
1084
+ padding: var(--ps1ui-control-padding-y) var(--ps1ui-space-md);
1085
+ cursor: pointer;
1086
+ -webkit-user-select: none;
1087
+ -moz-user-select: none;
1088
+ user-select: none;
1089
+ color: var(--ps1ui-color-fg);
1090
+ border-radius: var(--ps1ui-radius-md);
1091
+ transition:
1092
+ color var(--ps1ui-transition-fast),
1093
+ box-shadow var(--ps1ui-transition-fast);
1094
+ /* base.css's reset keeps summary's default `display: list-item`; kill the
1095
+ marker so our ::before glyph is the only disclosure indicator. */
1096
+ list-style: none;
1097
+ }
1098
+
1099
+ /* Older WebKit (Safari < 17) renders a native disclosure triangle via its
1100
+ private ::-webkit-details-marker pseudo — `list-style: none` alone doesn't
1101
+ suppress it there. Explicitly hide it so ::before is the only glyph. */
1102
+
1103
+ .ps1ui-details__summary::-webkit-details-marker {
1104
+ display: none;
1105
+ }
1106
+
1107
+ /* Terminal-style disclosure marker: `▸` when closed, `▾` when open. Fixed
1108
+ inline-block width so the summary text baseline stays put across states —
1109
+ the two glyphs don't measure identically in the JetBrains Mono fallback
1110
+ monospace stack. Colored with fg-muted so the arrow reads as chrome, not
1111
+ content; ::before is not exposed to the a11y tree so this is safe. */
1112
+
1113
+ .ps1ui-details__summary::before {
1114
+ content: "▸";
1115
+ display: inline-block;
1116
+ width: 1.25ch;
1117
+ margin-right: var(--ps1ui-space-sm);
1118
+ color: var(--ps1ui-color-fg-muted);
1119
+ }
1120
+
1121
+ .ps1ui-details[open] > .ps1ui-details__summary::before {
1122
+ content: "▾";
1123
+ }
1124
+
1125
+ .ps1ui-details__summary:hover {
1126
+ color: var(--ps1ui-color-primary);
1127
+ }
1128
+
1129
+ /* Focus ring lives on the summary (the interactive element), not on details
1130
+ itself — .ps1ui-details__summary:focus-visible is registered in
1131
+ styles/components.css's forced-colors grouped selector so the box-shadow
1132
+ ring falls back to a real outline in high-contrast mode. */
1133
+
1134
+ .ps1ui-details__summary:focus-visible {
1135
+ outline: none;
1136
+ box-shadow: var(--ps1ui-shadow-focus-ring);
1137
+ }
1138
+
1139
+ .ps1ui-details__body {
1140
+ padding: var(--ps1ui-space-md);
1141
+ border-top: 1px solid var(--ps1ui-color-border);
1142
+ }
1143
+
1144
+ /* Input custom properties emitted by Stack.tsx per set responsive-prop
1145
+ breakpoint (`--_stack-<axis>-<bp>`). Registered as non-inheriting so a
1146
+ parent Stack's `--_stack-gap-md` (etc.) does NOT cascade into a nested
1147
+ Stack — CSS custom properties inherit by default (css-variables §2), and
1148
+ without this guard the `var(--_stack-<axis>-<bp>, fallback)` chain below
1149
+ picks up the ancestor's value on every descendant and silently overrides
1150
+ the child's own responsive resolution when the matching @container query
1151
+ fires. `syntax: "*"` lets us omit `initial-value` — the default value is
1152
+ the guaranteed-invalid value, which makes the fallback branch of each
1153
+ `var()` in the intermediate chain fire exactly as intended. Only inline
1154
+ styles emitted by Stack.tsx (author-origin, inline specificity) set these
1155
+ per-element, which beats the @property registration in the cascade. If a
1156
+ new responsive axis is added to Stack.tsx, a matching @property block MUST
1157
+ be added here; the nested-Stack regression tests in Stack.test.tsx cover
1158
+ every axis so a forgotten one trips CI immediately. `unset` / `revert` /
1159
+ `revert-layer` are NOT interchangeable with this approach for custom
1160
+ properties — they resolve to the inherited value, defeating the block. */
1161
+
1162
+ @property --_stack-direction-base {
1163
+ syntax: "*";
1164
+ inherits: false;
1165
+ }
1166
+
1167
+ @property --_stack-direction-sm {
1168
+ syntax: "*";
1169
+ inherits: false;
1170
+ }
1171
+
1172
+ @property --_stack-direction-md {
1173
+ syntax: "*";
1174
+ inherits: false;
1175
+ }
1176
+
1177
+ @property --_stack-direction-lg {
1178
+ syntax: "*";
1179
+ inherits: false;
1180
+ }
1181
+
1182
+ @property --_stack-direction-xl {
1183
+ syntax: "*";
1184
+ inherits: false;
1185
+ }
1186
+
1187
+ @property --_stack-gap-base {
1188
+ syntax: "*";
1189
+ inherits: false;
1190
+ }
1191
+
1192
+ @property --_stack-gap-sm {
1193
+ syntax: "*";
1194
+ inherits: false;
1195
+ }
1196
+
1197
+ @property --_stack-gap-md {
1198
+ syntax: "*";
1199
+ inherits: false;
1200
+ }
1201
+
1202
+ @property --_stack-gap-lg {
1203
+ syntax: "*";
1204
+ inherits: false;
1205
+ }
1206
+
1207
+ @property --_stack-gap-xl {
1208
+ syntax: "*";
1209
+ inherits: false;
1210
+ }
1211
+
1212
+ @property --_stack-align-base {
1213
+ syntax: "*";
1214
+ inherits: false;
1215
+ }
1216
+
1217
+ @property --_stack-align-sm {
1218
+ syntax: "*";
1219
+ inherits: false;
1220
+ }
1221
+
1222
+ @property --_stack-align-md {
1223
+ syntax: "*";
1224
+ inherits: false;
1225
+ }
1226
+
1227
+ @property --_stack-align-lg {
1228
+ syntax: "*";
1229
+ inherits: false;
1230
+ }
1231
+
1232
+ @property --_stack-align-xl {
1233
+ syntax: "*";
1234
+ inherits: false;
1235
+ }
1236
+
1237
+ @property --_stack-justify-base {
1238
+ syntax: "*";
1239
+ inherits: false;
1240
+ }
1241
+
1242
+ @property --_stack-justify-sm {
1243
+ syntax: "*";
1244
+ inherits: false;
1245
+ }
1246
+
1247
+ @property --_stack-justify-md {
1248
+ syntax: "*";
1249
+ inherits: false;
1250
+ }
1251
+
1252
+ @property --_stack-justify-lg {
1253
+ syntax: "*";
1254
+ inherits: false;
1255
+ }
1256
+
1257
+ @property --_stack-justify-xl {
1258
+ syntax: "*";
1259
+ inherits: false;
1260
+ }
1261
+
1262
+ @property --_stack-wrap-base {
1263
+ syntax: "*";
1264
+ inherits: false;
1265
+ }
1266
+
1267
+ @property --_stack-wrap-sm {
1268
+ syntax: "*";
1269
+ inherits: false;
1270
+ }
1271
+
1272
+ @property --_stack-wrap-md {
1273
+ syntax: "*";
1274
+ inherits: false;
1275
+ }
1276
+
1277
+ @property --_stack-wrap-lg {
1278
+ syntax: "*";
1279
+ inherits: false;
1280
+ }
1281
+
1282
+ @property --_stack-wrap-xl {
1283
+ syntax: "*";
1284
+ inherits: false;
1285
+ }
1286
+
1287
+ /* Stack — flexbox composition of children along one axis.
1288
+ Establishes an inline-size containment context so descendants respond to
1289
+ Stack's width via `@container` queries. Named `ps1ui-stack` for future
1290
+ targeted queries.
1291
+
1292
+ Responsive values propagate via a CSS custom-property cascade. The
1293
+ component emits `--_stack-{axis}-{bp}` via inline style (see Stack.tsx).
1294
+ Each `@container (min-width: X)` block below overrides `--_{axis}` with
1295
+ the value for the matching breakpoint; the fallback chain in
1296
+ `--_{axis}-{bp}` inherits the previous breakpoint's effective value so a
1297
+ partial responsive object cascades naturally.
1298
+
1299
+ Breakpoint thresholds mirror --ps1ui-breakpoint-* in tokens.css; kept
1300
+ inline as literal values because `@container` conditions cannot resolve
1301
+ `var()`. Keep them in sync.
1302
+
1303
+ Fallback defaults:
1304
+ - direction: `column` — vertical stacks are the mobile-first default
1305
+ for a column-major layout primitive
1306
+ - gap: `--ps1ui-space-md` — matches Container's default padding scale
1307
+ - align: `normal` — the CSS box-alignment initial value; in a flex
1308
+ context, `normal` behaves as `stretch`, matching Flexbox's default
1309
+ - justify: `normal` — behaves as `flex-start` in a flex context
1310
+ - wrap: `nowrap` — the flex-wrap CSS initial value
1311
+ Each default matches the property's own initial-value behaviour so
1312
+ omitting the corresponding prop is equivalent to not writing the
1313
+ declaration at all. */
1314
+
1315
+ /* Containment side-effect defense (align-self / justify-self / min-width) lives
1316
+ in components.css's shared selector list — see the block header there. */
1317
+
1318
+ .ps1ui-stack {
1319
+ container: ps1ui-stack / inline-size;
1320
+ display: flex;
1321
+
1322
+ --_direction-base: var(--_stack-direction-base, column);
1323
+ --_direction-sm: var(--_stack-direction-sm, var(--_direction-base));
1324
+ --_direction-md: var(--_stack-direction-md, var(--_direction-sm));
1325
+ --_direction-lg: var(--_stack-direction-lg, var(--_direction-md));
1326
+ --_direction-xl: var(--_stack-direction-xl, var(--_direction-lg));
1327
+
1328
+ --_gap-base: var(--_stack-gap-base, var(--ps1ui-space-md));
1329
+ --_gap-sm: var(--_stack-gap-sm, var(--_gap-base));
1330
+ --_gap-md: var(--_stack-gap-md, var(--_gap-sm));
1331
+ --_gap-lg: var(--_stack-gap-lg, var(--_gap-md));
1332
+ --_gap-xl: var(--_stack-gap-xl, var(--_gap-lg));
1333
+
1334
+ --_align-base: var(--_stack-align-base, normal);
1335
+ --_align-sm: var(--_stack-align-sm, var(--_align-base));
1336
+ --_align-md: var(--_stack-align-md, var(--_align-sm));
1337
+ --_align-lg: var(--_stack-align-lg, var(--_align-md));
1338
+ --_align-xl: var(--_stack-align-xl, var(--_align-lg));
1339
+
1340
+ --_justify-base: var(--_stack-justify-base, normal);
1341
+ --_justify-sm: var(--_stack-justify-sm, var(--_justify-base));
1342
+ --_justify-md: var(--_stack-justify-md, var(--_justify-sm));
1343
+ --_justify-lg: var(--_stack-justify-lg, var(--_justify-md));
1344
+ --_justify-xl: var(--_stack-justify-xl, var(--_justify-lg));
1345
+
1346
+ --_wrap-base: var(--_stack-wrap-base, nowrap);
1347
+ --_wrap-sm: var(--_stack-wrap-sm, var(--_wrap-base));
1348
+ --_wrap-md: var(--_stack-wrap-md, var(--_wrap-sm));
1349
+ --_wrap-lg: var(--_stack-wrap-lg, var(--_wrap-md));
1350
+ --_wrap-xl: var(--_stack-wrap-xl, var(--_wrap-lg));
1351
+
1352
+ --_direction: var(--_direction-base);
1353
+ --_gap: var(--_gap-base);
1354
+ --_align: var(--_align-base);
1355
+ --_justify: var(--_justify-base);
1356
+ --_wrap: var(--_wrap-base);
1357
+
1358
+ flex-direction: var(--_direction);
1359
+ gap: var(--_gap);
1360
+ align-items: var(--_align);
1361
+ justify-content: var(--_justify);
1362
+ flex-wrap: var(--_wrap);
1363
+ }
1364
+
1365
+ @container (min-width: 40rem) {
1366
+ .ps1ui-stack {
1367
+ --_direction: var(--_direction-sm);
1368
+ --_gap: var(--_gap-sm);
1369
+ --_align: var(--_align-sm);
1370
+ --_justify: var(--_justify-sm);
1371
+ --_wrap: var(--_wrap-sm);
1372
+ }
1373
+ }
1374
+
1375
+ @container (min-width: 48rem) {
1376
+ .ps1ui-stack {
1377
+ --_direction: var(--_direction-md);
1378
+ --_gap: var(--_gap-md);
1379
+ --_align: var(--_align-md);
1380
+ --_justify: var(--_justify-md);
1381
+ --_wrap: var(--_wrap-md);
1382
+ }
1383
+ }
1384
+
1385
+ @container (min-width: 64rem) {
1386
+ .ps1ui-stack {
1387
+ --_direction: var(--_direction-lg);
1388
+ --_gap: var(--_gap-lg);
1389
+ --_align: var(--_align-lg);
1390
+ --_justify: var(--_justify-lg);
1391
+ --_wrap: var(--_wrap-lg);
1392
+ }
1393
+ }
1394
+
1395
+ @container (min-width: 80rem) {
1396
+ .ps1ui-stack {
1397
+ --_direction: var(--_direction-xl);
1398
+ --_gap: var(--_gap-xl);
1399
+ --_align: var(--_align-xl);
1400
+ --_justify: var(--_justify-xl);
1401
+ --_wrap: var(--_wrap-xl);
1402
+ }
1403
+ }
1404
+
1405
+ /* Table — semantic <table> with terminal-style grid borders, wrapped in a
1406
+ horizontal-scroll container.
1407
+
1408
+ Scroll wrapper: `display: table` boxes don't honor overflow, so Table.tsx
1409
+ renders a dedicated `__scroller` div with `overflow-x: auto`. It follows
1410
+ CodeBlock's keyboard contract — tabIndex=0 only while the table actually
1411
+ overflows — and reuses the shared box-shadow focus ring, so
1412
+ `.ps1ui-table__scroller:focus-visible` is registered in the forced-colors
1413
+ grouped selector in styles/components.css (box-shadow is force-stripped in
1414
+ high-contrast mode; the grouped rule restores a real outline).
1415
+
1416
+ Grid borders: `border-collapse: collapse` + a uniform 1px cell border in
1417
+ --ps1ui-color-border draw the closed single-stroke grid a terminal
1418
+ box-drawing table (┌─┬─┐) renders — collapsing is what merges adjacent
1419
+ cell edges into one shared rule. The header row separates on two axes:
1420
+ a surface background and a border-strong bottom rule. That rule is
1421
+ declared on the <th> (cell origin) deliberately — in border-collapse
1422
+ conflict resolution, equal-width equal-style borders from two adjacent
1423
+ cells resolve to the topmost cell's color, so the th's strong bottom edge
1424
+ wins over the first body row's plain top edge. Declaring it on the row or
1425
+ row group instead would lose (cell origin outranks both).
1426
+
1427
+ All rules target `.ps1ui-table <element>` selectors, not the
1428
+ ps1ui-thead/tbody/tr/th/td wrapper classes: a bare <thead>/<tr>/<th>/<td>
1429
+ inside <Table> must render identically to the wrapper components (same
1430
+ contract as `.ps1ui-list > li` vs ListItem). The wrapper classes are
1431
+ stable hooks only.
1432
+
1433
+ Self-sufficiency: `border-collapse` and the <th> font-weight/text-align
1434
+ neutralizations are declared here even though base.css's reset also
1435
+ collapses table borders — the components.css entry ships without the
1436
+ reset, and UA <th> defaults (bold, center) would leak through. */
1437
+
1438
+ .ps1ui-table__scroller {
1439
+ overflow-x: auto;
1440
+ }
1441
+
1442
+ .ps1ui-table__scroller:focus-visible {
1443
+ outline: none;
1444
+ box-shadow: var(--ps1ui-shadow-focus-ring);
1445
+ }
1446
+
1447
+ /* No width override — the UA's shrink-to-fit keeps the grid hugging its
1448
+ content like a box-drawing table. Call sites that want a full-bleed table
1449
+ (e.g. a props table filling a docs column) opt in with `width: 100%`. */
1450
+
1451
+ .ps1ui-table {
1452
+ border-collapse: collapse;
1453
+ color: var(--ps1ui-color-fg);
1454
+ font-family: var(--ps1ui-font-mono);
1455
+ font-size: var(--ps1ui-font-size-sm);
1456
+ line-height: 1.6;
1457
+ }
1458
+
1459
+ .ps1ui-table th,
1460
+ .ps1ui-table td {
1461
+ border: 1px solid var(--ps1ui-color-border);
1462
+ padding: var(--ps1ui-space-sm) var(--ps1ui-space-md);
1463
+ /* `start` (not `left`): tables carry natural-language content and should
1464
+ mirror in RTL contexts — unlike CodeBlock, which pins `left` because
1465
+ source code stays LTR. Explicit on both cell kinds because the UA
1466
+ default for <th> is center. */
1467
+ text-align: start;
1468
+ /* Multi-line cells align their first lines instead of drifting toward
1469
+ vertical centers of unequal-height neighbors. */
1470
+ vertical-align: top;
1471
+ }
1472
+
1473
+ .ps1ui-table th {
1474
+ background: var(--ps1ui-color-surface);
1475
+ font-weight: 600;
1476
+ }
1477
+
1478
+ .ps1ui-table thead th {
1479
+ border-bottom-color: var(--ps1ui-color-border-strong);
1480
+ }
1481
+
1482
+ /* Thead — no rules of its own.
1483
+
1484
+ The `.ps1ui-thead` class is a stable API hook for consumers. All header
1485
+ rendering (surface background, strong bottom rule) lives on
1486
+ `.ps1ui-table thead th` in Table.css so a bare <thead> inside <Table>
1487
+ continues to render identically to <Thead>. Keeping this file present
1488
+ (and imported from components.css) matches the per-component file layout
1489
+ the `new-component` skill enforces — same precedent as ListItem.css. */
1490
+
1491
+ /* Tbody — no rules of its own.
1492
+
1493
+ The `.ps1ui-tbody` class is a stable API hook for consumers. All cell
1494
+ rendering lives on `.ps1ui-table th / td` in Table.css so a bare <tbody>
1495
+ inside <Table> continues to render identically to <Tbody>. Keeping this
1496
+ file present (and imported from components.css) matches the per-component
1497
+ file layout the `new-component` skill enforces — same precedent as
1498
+ ListItem.css. */
1499
+
1500
+ /* Tr — no rules of its own.
1501
+
1502
+ The `.ps1ui-tr` class is a stable API hook for consumers. All cell
1503
+ rendering lives on `.ps1ui-table th / td` in Table.css so a bare <tr>
1504
+ inside <Table> continues to render identically to <Tr>. Keeping this file
1505
+ present (and imported from components.css) matches the per-component file
1506
+ layout the `new-component` skill enforces — same precedent as
1507
+ ListItem.css. */
1508
+
1509
+ /* Th — no rules of its own.
1510
+
1511
+ The `.ps1ui-th` class is a stable API hook for consumers. All header-cell
1512
+ rendering (grid border, padding, surface background, weight) lives on
1513
+ `.ps1ui-table th` in Table.css so a bare <th> inside <Table> continues to
1514
+ render identically to <Th>. Keeping this file present (and imported from
1515
+ components.css) matches the per-component file layout the `new-component`
1516
+ skill enforces — same precedent as ListItem.css. */
1517
+
1518
+ /* Td — no rules of its own.
1519
+
1520
+ The `.ps1ui-td` class is a stable API hook for consumers. All data-cell
1521
+ rendering (grid border, padding, alignment) lives on `.ps1ui-table td` in
1522
+ Table.css so a bare <td> inside <Table> continues to render identically to
1523
+ <Td>. Keeping this file present (and imported from components.css) matches
1524
+ the per-component file layout the `new-component` skill enforces — same
1525
+ precedent as ListItem.css. */
1526
+
1527
+ /* Input custom properties emitted by Text.tsx per set responsive-prop
1528
+ breakpoint (`--_text-<axis>-<bp>`). Registered as non-inheriting for the
1529
+ same reason as Stack — see Stack.css's @property block for the full
1530
+ rationale. */
1531
+
1532
+ @property --_text-size-base {
1533
+ syntax: "*";
1534
+ inherits: false;
1535
+ }
1536
+
1537
+ @property --_text-size-sm {
1538
+ syntax: "*";
1539
+ inherits: false;
1540
+ }
1541
+
1542
+ @property --_text-size-md {
1543
+ syntax: "*";
1544
+ inherits: false;
1545
+ }
1546
+
1547
+ @property --_text-size-lg {
1548
+ syntax: "*";
1549
+ inherits: false;
1550
+ }
1551
+
1552
+ @property --_text-size-xl {
1553
+ syntax: "*";
1554
+ inherits: false;
1555
+ }
1556
+
1557
+ @property --_text-weight-base {
1558
+ syntax: "*";
1559
+ inherits: false;
1560
+ }
1561
+
1562
+ @property --_text-weight-sm {
1563
+ syntax: "*";
1564
+ inherits: false;
1565
+ }
1566
+
1567
+ @property --_text-weight-md {
1568
+ syntax: "*";
1569
+ inherits: false;
1570
+ }
1571
+
1572
+ @property --_text-weight-lg {
1573
+ syntax: "*";
1574
+ inherits: false;
1575
+ }
1576
+
1577
+ @property --_text-weight-xl {
1578
+ syntax: "*";
1579
+ inherits: false;
1580
+ }
1581
+
1582
+ /* Text — typography primitive.
1583
+ Text is a leaf component: it queries an ancestor containment context (a
1584
+ PS1Root / Container / Grid / Stack), but does NOT establish one itself.
1585
+ Establishing containment on every Text would fragment the query surface
1586
+ for descendants and impose a `contain: layout` side effect on every
1587
+ paragraph — needless overhead for a component that carries no children
1588
+ worth measuring.
1589
+
1590
+ Responsive values propagate via a CSS custom-property cascade. The
1591
+ component emits `--_text-size-{bp}` / `--_text-weight-{bp}` via inline
1592
+ style. Each `@container (min-width: X)` block below overrides `--_size`
1593
+ / `--_weight` with the value for the matching breakpoint; the fallback
1594
+ chain in `--_size-{bp}` inherits the previous breakpoint's effective
1595
+ value so a partial responsive object cascades naturally.
1596
+
1597
+ `weight` fallback is `initial` (= normal / 400). Consumers who want bold
1598
+ text pass `weight="bold"` explicitly; a bare `<Text as="strong">` renders
1599
+ as normal weight (Text is the source of truth for its own typography,
1600
+ NOT the UA `<strong>` style). Users who need the UA-strong appearance
1601
+ should pair `as="strong"` with `weight="bold"` for consistency across
1602
+ engines. */
1603
+
1604
+ .ps1ui-text {
1605
+ margin: 0;
1606
+ font-family: var(--ps1ui-font-mono);
1607
+ line-height: 1.6;
1608
+
1609
+ --_size-base: var(--_text-size-base, var(--ps1ui-font-size-sm));
1610
+ --_size-sm: var(--_text-size-sm, var(--_size-base));
1611
+ --_size-md: var(--_text-size-md, var(--_size-sm));
1612
+ --_size-lg: var(--_text-size-lg, var(--_size-md));
1613
+ --_size-xl: var(--_text-size-xl, var(--_size-lg));
1614
+
1615
+ --_weight-base: var(--_text-weight-base, initial);
1616
+ --_weight-sm: var(--_text-weight-sm, var(--_weight-base));
1617
+ --_weight-md: var(--_text-weight-md, var(--_weight-sm));
1618
+ --_weight-lg: var(--_text-weight-lg, var(--_weight-md));
1619
+ --_weight-xl: var(--_text-weight-xl, var(--_weight-lg));
1620
+
1621
+ --_size: var(--_size-base);
1622
+ --_weight: var(--_weight-base);
1623
+
1624
+ font-size: var(--_size);
1625
+ font-weight: var(--_weight);
1626
+ }
1627
+
1628
+ @container (min-width: 40rem) {
1629
+ .ps1ui-text {
1630
+ --_size: var(--_size-sm);
1631
+ --_weight: var(--_weight-sm);
1632
+ }
1633
+ }
1634
+
1635
+ @container (min-width: 48rem) {
1636
+ .ps1ui-text {
1637
+ --_size: var(--_size-md);
1638
+ --_weight: var(--_weight-md);
1639
+ }
1640
+ }
1641
+
1642
+ @container (min-width: 64rem) {
1643
+ .ps1ui-text {
1644
+ --_size: var(--_size-lg);
1645
+ --_weight: var(--_weight-lg);
1646
+ }
1647
+ }
1648
+
1649
+ @container (min-width: 80rem) {
1650
+ .ps1ui-text {
1651
+ --_size: var(--_size-xl);
1652
+ --_weight: var(--_weight-xl);
1653
+ }
1654
+ }
1655
+
1656
+ .ps1ui-text--truncate {
1657
+ overflow: hidden;
1658
+ white-space: nowrap;
1659
+ text-overflow: ellipsis;
1660
+ max-width: 100%;
1661
+ }
1662
+
1663
+ .ps1ui-text--truncate-inline {
1664
+ display: inline-block;
1665
+ }
1666
+
1667
+ .ps1ui-text--body {
1668
+ color: var(--ps1ui-color-fg);
1669
+ }
1670
+
1671
+ .ps1ui-text--muted {
1672
+ color: var(--ps1ui-color-fg-muted);
1673
+ }
1674
+
1675
+ .ps1ui-text--subtle {
1676
+ color: var(--ps1ui-color-fg-subtle);
1677
+ }
1678
+
1679
+ .ps1ui-text--primary {
1680
+ color: var(--ps1ui-color-primary);
1681
+ }
1682
+
1683
+ .ps1ui-text--accent {
1684
+ color: var(--ps1ui-color-accent);
1685
+ }
1686
+
1687
+ /* Input custom properties emitted by Heading.tsx per set responsive-prop
1688
+ breakpoint (`--_heading-<axis>-<bp>`). Registered as non-inheriting for
1689
+ the same reason as Stack — see Stack.css's @property block for the full
1690
+ rationale. */
1691
+
1692
+ @property --_heading-size-base {
1693
+ syntax: "*";
1694
+ inherits: false;
1695
+ }
1696
+
1697
+ @property --_heading-size-sm {
1698
+ syntax: "*";
1699
+ inherits: false;
1700
+ }
1701
+
1702
+ @property --_heading-size-md {
1703
+ syntax: "*";
1704
+ inherits: false;
1705
+ }
1706
+
1707
+ @property --_heading-size-lg {
1708
+ syntax: "*";
1709
+ inherits: false;
1710
+ }
1711
+
1712
+ @property --_heading-size-xl {
1713
+ syntax: "*";
1714
+ inherits: false;
1715
+ }
1716
+
1717
+ @property --_heading-weight-base {
1718
+ syntax: "*";
1719
+ inherits: false;
1720
+ }
1721
+
1722
+ @property --_heading-weight-sm {
1723
+ syntax: "*";
1724
+ inherits: false;
1725
+ }
1726
+
1727
+ @property --_heading-weight-md {
1728
+ syntax: "*";
1729
+ inherits: false;
1730
+ }
1731
+
1732
+ @property --_heading-weight-lg {
1733
+ syntax: "*";
1734
+ inherits: false;
1735
+ }
1736
+
1737
+ @property --_heading-weight-xl {
1738
+ syntax: "*";
1739
+ inherits: false;
1740
+ }
1741
+
1742
+ /* Heading — semantic heading primitive with level-driven typography.
1743
+ Like Text, Heading is a leaf: it queries an ancestor containment context
1744
+ but does not establish one itself. See Text.css for the rationale.
1745
+
1746
+ Responsive values propagate via a CSS custom-property cascade. The
1747
+ component emits `--_heading-size-{bp}` / `--_heading-weight-{bp}` via
1748
+ inline style, always at least at `-base` (Heading.tsx normalizes the
1749
+ level's default into the base breakpoint if the caller didn't supply
1750
+ one). Each `@container (min-width: X)` block below overrides `--_size`
1751
+ / `--_weight` for the matching breakpoint; the fallback chain inherits
1752
+ the previous breakpoint's effective value so a partial responsive object
1753
+ cascades naturally.
1754
+
1755
+ Heading.tsx contract: `--_heading-size-base` and `--_heading-weight-base`
1756
+ are ALWAYS emitted via inline style (see Heading.tsx — `size` and
1757
+ `weight` both pass through `withResponsiveBase(..., LEVEL_DEFAULTS[level].
1758
+ size|weight)`, which guarantees the `base` breakpoint is populated for
1759
+ every render). The `--_size-base` / `--_weight-base` fallback branches
1760
+ below are therefore defensive floors only — they should never be reached
1761
+ in normal rendering. If Heading.tsx ever changes to NOT normalize the
1762
+ base breakpoint, these floors keep the component in a legible default
1763
+ (font-size sm, font-weight normal). Text.css uses a live fallback path
1764
+ because Text.tsx does not normalize its size/weight props. */
1765
+
1766
+ .ps1ui-heading {
1767
+ margin: 0;
1768
+ font-family: var(--ps1ui-font-mono);
1769
+ color: var(--ps1ui-color-fg);
1770
+ line-height: 1.3;
1771
+
1772
+ /* Defensive-floor fallbacks. Never reached under normal render — see
1773
+ the comment above. Kept as font-size sm / weight normal so a
1774
+ bypassed Heading.tsx contract still yields legible output. */
1775
+ --_size-base: var(--_heading-size-base, var(--ps1ui-font-size-sm));
1776
+ --_size-sm: var(--_heading-size-sm, var(--_size-base));
1777
+ --_size-md: var(--_heading-size-md, var(--_size-sm));
1778
+ --_size-lg: var(--_heading-size-lg, var(--_size-md));
1779
+ --_size-xl: var(--_heading-size-xl, var(--_size-lg));
1780
+
1781
+ --_weight-base: var(--_heading-weight-base, normal);
1782
+ --_weight-sm: var(--_heading-weight-sm, var(--_weight-base));
1783
+ --_weight-md: var(--_heading-weight-md, var(--_weight-sm));
1784
+ --_weight-lg: var(--_heading-weight-lg, var(--_weight-md));
1785
+ --_weight-xl: var(--_heading-weight-xl, var(--_weight-lg));
1786
+
1787
+ --_size: var(--_size-base);
1788
+ --_weight: var(--_weight-base);
1789
+
1790
+ font-size: var(--_size);
1791
+ font-weight: var(--_weight);
1792
+ }
1793
+
1794
+ @container (min-width: 40rem) {
1795
+ .ps1ui-heading {
1796
+ --_size: var(--_size-sm);
1797
+ --_weight: var(--_weight-sm);
1798
+ }
1799
+ }
1800
+
1801
+ @container (min-width: 48rem) {
1802
+ .ps1ui-heading {
1803
+ --_size: var(--_size-md);
1804
+ --_weight: var(--_weight-md);
1805
+ }
1806
+ }
1807
+
1808
+ @container (min-width: 64rem) {
1809
+ .ps1ui-heading {
1810
+ --_size: var(--_size-lg);
1811
+ --_weight: var(--_weight-lg);
1812
+ }
1813
+ }
1814
+
1815
+ @container (min-width: 80rem) {
1816
+ .ps1ui-heading {
1817
+ --_size: var(--_size-xl);
1818
+ --_weight: var(--_weight-xl);
1819
+ }
1820
+ }
1821
+
1822
+ /* Containment side-effect defense — applied uniformly to every primitive that
1823
+ establishes a `container-type: inline-size` context (i.e. every layout
1824
+ primitive whose responsive props drive `@container` queries).
1825
+
1826
+ `container-type: inline-size` implies `contain: inline-size`, which treats
1827
+ the element's intrinsic inline-size as 0 for parent sizing. Placing any of
1828
+ these primitives inside a shrink-to-fit flex/grid parent (`align-items:
1829
+ flex-start`, `justify-items: start`, etc.) would otherwise collapse them to
1830
+ 0 wide, silently degrading every `@container`-driven descendant.
1831
+
1832
+ `align-self: stretch` and `justify-self: stretch` force fill-cross-axis /
1833
+ fill-inline-track in flex and grid contexts respectively (both no-ops in
1834
+ block context). `min-width: 0` lets the primitive shrink below content-min
1835
+ inside a narrow row parent so inner overflow-scroll surfaces (CodeBlock)
1836
+ take over instead of bursting the row open.
1837
+
1838
+ Adding a new responsive-container primitive means adding its class to this
1839
+ selector list — that's the load-bearing invariant. Per-primitive regression
1840
+ tests (Stack.test.tsx / Container.test.tsx / Grid.test.tsx / PS1Root.test.tsx's
1841
+ "resists collapse via align-self: stretch in shrink-wrap flex parent") catch
1842
+ accidental removal. */
1843
+
1844
+ .ps1ui-root,
1845
+ .ps1ui-container,
1846
+ .ps1ui-grid,
1847
+ .ps1ui-stack {
1848
+ align-self: stretch;
1849
+ justify-self: stretch;
1850
+ min-width: 0;
1851
+ }
1852
+
1853
+ /* forced-colors (Windows High Contrast) strips box-shadow, so every component
1854
+ whose focus ring is box-shadow-based needs a real outline there — one rule,
1855
+ one owner, same precedent as the containment grouped selector above. Add new
1856
+ box-shadow-focus components to this list. Component-specific forced-colors
1857
+ adjustments (e.g. Checkbox's indeterminate bar) stay in their own CSS.
1858
+ Tested in src/styles/forced-colors.test.tsx. */
1859
+
1860
+ @media (forced-colors: active) {
1861
+ .ps1ui-button:focus-visible,
1862
+ .ps1ui-checkbox:focus-visible,
1863
+ .ps1ui-codeblock:focus-visible,
1864
+ .ps1ui-details__summary:focus-visible,
1865
+ .ps1ui-input:focus,
1866
+ .ps1ui-table__scroller:focus-visible {
1867
+ outline: 2px solid CanvasText;
1868
+ outline-offset: 2px;
1869
+ }
1870
+ }