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