@kenlo/sds-design-system 0.9.9 → 1.0.2
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/README.md +58 -7
- package/components/Avatar/Avatar.css +22 -0
- package/components/Button/Button.css +95 -8
- package/components/Card/Card.css +16 -6
- package/components/Card/card-variants.js +18 -0
- package/components/Card/card.marko +12 -2
- package/components/Checkbox/Checkbox.css +56 -0
- package/components/ColorPicker/ColorPicker.css +25 -2
- package/components/ColorPicker/color-picker.marko +25 -4
- package/components/DatePicker/DatePicker.css +22 -0
- package/components/Floating/focus-when-ready.js +59 -0
- package/components/Incrementer/Incrementer.css +22 -0
- package/components/Input/Input.css +25 -2
- package/components/Menu/Menu.css +21 -11
- package/components/Menu/menu-variants.js +12 -0
- package/components/Menu/menu.marko +11 -7
- package/components/Pill/Pill.css +46 -0
- package/components/Radio/Radio.css +75 -0
- package/components/Select/Select.css +21 -1
- package/components/Select/select-options.js +10 -1
- package/components/Select/select.marko +112 -5
- package/components/Table/Table.css +73 -2
- package/components/Table/table-column-picker.marko +161 -0
- package/components/Table/table-logic.js +56 -0
- package/components/Table/table.marko +50 -12
- package/components/TextEditor/TextEditor.css +2 -2
- package/components/Toggle/Toggle.css +40 -0
- package/components/Tooltip/Tooltip.css +2 -2
- package/components/Uploader/Uploader.css +23 -0
- package/foundations/custom-scheme.js +128 -10
- package/foundations/fonts/OFL.txt +114 -0
- package/foundations/fonts/fonts.json +152 -0
- package/foundations/fonts/manrope-cyrillic-ext.woff2 +0 -0
- package/foundations/fonts/manrope-cyrillic.woff2 +0 -0
- package/foundations/fonts/manrope-greek.woff2 +0 -0
- package/foundations/fonts/manrope-latin-ext.woff2 +0 -0
- package/foundations/fonts/manrope-latin.woff2 +0 -0
- package/foundations/fonts/manrope-vietnamese.woff2 +0 -0
- package/foundations/tokens.css +1095 -15
- package/icons/generated/fluent-chevron-down-filled.marko +3 -0
- package/icons/generated/fluent-chevron-up-filled.marko +3 -0
- package/icons/generated/fluent-column-triple-regular.marko +3 -0
- package/icons/manifest.json +3 -0
- package/marko.json +3 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -37,6 +37,12 @@ import "@kenlo/sds-design-system/components/Button/Button.css";
|
|
|
37
37
|
As tags não precisam de import. O `marko.json` na raiz do pacote é o catálogo: o compilador
|
|
38
38
|
do Marko acha `<sds-button>` sozinho.
|
|
39
39
|
|
|
40
|
+
**A tipografia vem junto no passo 1, e não há passo para esquecer.** A Manrope — a fonte
|
|
41
|
+
padrão — viaja dentro do pacote, e as regras `@font-face` estão no próprio `tokens.css`,
|
|
42
|
+
que é o arquivo que declara qual fonte usar. Nada é pedido ao Google enquanto a página
|
|
43
|
+
carrega: o visitante do site é cliente da imobiliária, e o IP dele não sai daqui. As outras
|
|
44
|
+
cinco opções de `data-font` estão descritas em **Tema**, logo abaixo.
|
|
45
|
+
|
|
40
46
|
```marko
|
|
41
47
|
<sds-button variant="primary" onClick=salvar>Salvar</sds-button>
|
|
42
48
|
|
|
@@ -61,6 +67,32 @@ Quatro atributos no `<html>` decidem a aparência inteira. Um atributo no valor
|
|
|
61
67
|
| `data-radius` | `flat`, `soft`, `medium`, `rounded` | `soft` |
|
|
62
68
|
| `data-font` | `manrope`, `inter`, `helvetica`, `source-sans`, `nunito`, `montserrat` | `manrope` |
|
|
63
69
|
|
|
70
|
+
**De onde cada fonte vem**, porque isso decide o que o visitante baixa:
|
|
71
|
+
|
|
72
|
+
| | onde mora | o que o visitante baixa |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `manrope` (padrão) | dentro do pacote | ~24 KB — só o subset do idioma dele |
|
|
75
|
+
| `helvetica` | na máquina do visitante | nada |
|
|
76
|
+
| as outras quatro | bucket da Kenlo, publicado pelo KDLS | só o subset do idioma, e só se você escolher |
|
|
77
|
+
|
|
78
|
+
A Manrope é embarcada de propósito: a fonte padrão não pode depender de rede nenhuma além
|
|
79
|
+
da do próprio app — se o bucket cair, nenhum produto perde a tipografia junto. As opcionais
|
|
80
|
+
ficam fora do pacote pelo motivo oposto: os bytes delas não devem pesar em quem nunca
|
|
81
|
+
escolhe nenhuma.
|
|
82
|
+
|
|
83
|
+
> **As quatro opcionais carregam desde 21/09**, quando o CORS entrou no bucket. Conferido
|
|
84
|
+
> aqui com `npm run check:fonts`: 24 de 24 utilizáveis, e num navegador limpo as quatro
|
|
85
|
+
> famílias renderizam.
|
|
86
|
+
>
|
|
87
|
+
> **Com uma ressalva que vale saber.** Entre a publicação do bucket e o conserto do CORS,
|
|
88
|
+
> houve uma janela em que os arquivos respondiam sem o cabeçalho. Um navegador que passou
|
|
89
|
+
> por uma dessas URLs nessa janela **guardou a resposta ruim** — e elas são servidas
|
|
90
|
+
> `immutable`, com um ano de cache, então ele não revalida. Essa pessoa continua vendo o
|
|
91
|
+
> fallback, e consertar o servidor não a alcança: só um nome de arquivo novo alcança.
|
|
92
|
+
>
|
|
93
|
+
> A `manrope` e a `helvetica` nunca dependeram disso — e essa é exatamente a razão de a
|
|
94
|
+
> padrão vir embarcada.
|
|
95
|
+
|
|
64
96
|
Há duas portas para escrevê-los, e a diferença importa:
|
|
65
97
|
|
|
66
98
|
```js
|
|
@@ -80,20 +112,39 @@ visitante. A metade servidor existe para evitar isso.
|
|
|
80
112
|
|
|
81
113
|
## Cor do cliente
|
|
82
114
|
|
|
83
|
-
|
|
115
|
+
Os dez esquemas da tabela acima são os prontos. Um site com marca própria usa as **suas**
|
|
116
|
+
cores, e uma marca tem duas: um **destaque** e uma **complementar**. As duas entram em tempo
|
|
117
|
+
de execução, e há três jeitos de usá-las:
|
|
118
|
+
|
|
119
|
+
| | destaque | complementar |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| **1 sólida** | preenche o slot primário | não é aplicada |
|
|
122
|
+
| **2 sólidas** | preenche o slot primário | pinta o botão `ghost`, e nada mais |
|
|
123
|
+
| **gradiente** | primeiro stop | último stop |
|
|
84
124
|
|
|
85
125
|
```js
|
|
86
|
-
import { applyCustomColor, applyCustomGradient,
|
|
126
|
+
import { applyCustomColor, applyComplementColor, applyCustomGradient,
|
|
127
|
+
clearComplementColor, clearCustomScheme }
|
|
87
128
|
from "@kenlo/sds-design-system/foundations/custom-scheme.js";
|
|
88
129
|
|
|
89
|
-
applyCustomColor("#2E7D32"); // nove tons sólidos
|
|
90
|
-
|
|
91
|
-
|
|
130
|
+
applyCustomColor("#2E7D32"); // nove tons sólidos no slot primário
|
|
131
|
+
applyComplementColor("#BF71E5"); // nove tons em --color-complement-10..90
|
|
132
|
+
applyCustomGradient(["#2E7D32", "#BF71E5"]); // nove gradientes
|
|
133
|
+
clearComplementColor(); // derruba só a complementar
|
|
134
|
+
clearCustomScheme(); // derruba as duas
|
|
92
135
|
```
|
|
93
136
|
|
|
94
|
-
|
|
137
|
+
O destaque preenche `--color-primary-10..90`, os companheiros `-solid` e o
|
|
95
138
|
`--color-on-primary` — este último escolhido por luminância relativa (WCAG), para o texto
|
|
96
|
-
sobre a cor ficar legível seja ela qual for.
|
|
139
|
+
sobre a cor ficar legível seja ela qual for.
|
|
140
|
+
|
|
141
|
+
**A complementar pinta um controle só: o botão `ghost`**, que é o contornado. É decisão, não
|
|
142
|
+
limitação — uma segunda cor solta no sistema é como o sistema deixa de ter uma cor.
|
|
143
|
+
|
|
144
|
+
E ela é uma **camada**, não um terceiro modo: mora no seu próprio `<style>`, então trocar o
|
|
145
|
+
destaque não a derruba junto. Enquanto você não definir nenhuma, o ghost fica exatamente
|
|
146
|
+
como sempre foi — a leitura é `var(--color-complement-50, var(--color-primary-50-solid))`,
|
|
147
|
+
o fallback responde, e nada muda para quem nunca usar isso.
|
|
97
148
|
|
|
98
149
|
---
|
|
99
150
|
|
|
@@ -108,3 +108,25 @@
|
|
|
108
108
|
object-fit: cover;
|
|
109
109
|
border-radius: inherit;
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
/* ── The site's gradient, on the stroke ────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
`border-color` rejects a gradient, so a stroke normally reads the `-solid` companion —
|
|
115
|
+
the gradient's first stop, flat. These rules paint it properly instead: the gradient on
|
|
116
|
+
`border-box`, the surface colour on `padding-box`, over a transparent border. Only the
|
|
117
|
+
border's own 2px show the gradient, and `border-radius` still works, which is what
|
|
118
|
+
`border-image` would cost.
|
|
119
|
+
|
|
120
|
+
`[data-primary-gradient]` is written by `applyCustomGradient` and removed by
|
|
121
|
+
`applyCustomColor` — a stylesheet cannot ask what a custom property contains, so the
|
|
122
|
+
code that fills the slot says so. KDLS wrote the same rules under
|
|
123
|
+
`[data-color-scheme="lya"]`, its own brand gradient; SDS deleted every `lya` block with
|
|
124
|
+
the brand and these went with them. Keyed on the marker they serve any site's gradient.
|
|
125
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
126
|
+
|
|
127
|
+
[data-primary-gradient] .sds-avatar--clickable:hover {
|
|
128
|
+
background:
|
|
129
|
+
linear-gradient(var(--component-bg-color), var(--component-bg-color)) padding-box,
|
|
130
|
+
var(--color-primary-50) border-box;
|
|
131
|
+
border-color: transparent;
|
|
132
|
+
}
|
|
@@ -94,15 +94,32 @@
|
|
|
94
94
|
VARIANT: primary (filled)
|
|
95
95
|
Uses --color-primary-* → follows data-color-scheme
|
|
96
96
|
────────────────────────────────────────────── */
|
|
97
|
+
/* `background-origin` travels with every one of these, and it has to.
|
|
98
|
+
|
|
99
|
+
`.sds-button` carries `border: 2px solid transparent`, so a primary button comes out the
|
|
100
|
+
same size as a ghost one, which has a visible border. With a solid colour that border is
|
|
101
|
+
invisible: the colour paints the whole border box. With a GRADIENT — and the primary slot
|
|
102
|
+
is allowed to hold one — the background is sized to the box INSIDE the border, painted
|
|
103
|
+
over the border anyway, and the 2px strip is filled with the next tile: the left edge
|
|
104
|
+
shows the colour from the far right, all the way round. Rafael saw it as a strange stroke
|
|
105
|
+
on the picker's "Aplicar".
|
|
106
|
+
|
|
107
|
+
Declaring it once on `.sds-button` does not work, and that is the trap: the `background`
|
|
108
|
+
shorthand RESETS `background-origin` to `padding-box`, and the shorthand is what every
|
|
109
|
+
rule below uses — because the slot may hold a colour or a gradient and only the shorthand
|
|
110
|
+
takes both. So it is repeated wherever the shorthand appears. */
|
|
97
111
|
.sds-button--primary {
|
|
98
112
|
background: var(--color-primary-50);
|
|
113
|
+
background-origin: border-box;
|
|
99
114
|
color: var(--color-on-primary);
|
|
100
115
|
}
|
|
101
116
|
.sds-button--primary:hover:not(:disabled):not(.sds-button--loading) {
|
|
102
117
|
background: var(--color-primary-60);
|
|
118
|
+
background-origin: border-box;
|
|
103
119
|
}
|
|
104
120
|
.sds-button--primary:active:not(:disabled):not(.sds-button--loading) {
|
|
105
121
|
background: var(--color-primary-70);
|
|
122
|
+
background-origin: border-box;
|
|
106
123
|
}
|
|
107
124
|
.sds-button--primary:focus-visible {
|
|
108
125
|
box-shadow: var(--focus-ring);
|
|
@@ -110,20 +127,37 @@
|
|
|
110
127
|
/* ──────────────────────────────────────────────
|
|
111
128
|
VARIANT: ghost (outlined / ghost)
|
|
112
129
|
────────────────────────────────────────────── */
|
|
130
|
+
/* The outlined control is the one thing the site's COMPLEMENTARY colour paints.
|
|
131
|
+
|
|
132
|
+
A brand has a highlight and a complement. The highlight fills the primary slot and
|
|
133
|
+
paints everything; the complement paints this. The product that consumes SDS already
|
|
134
|
+
models both, and until now the design system could only receive the first.
|
|
135
|
+
|
|
136
|
+
Every declaration reads the complement THROUGH a fallback to the primary tone it used
|
|
137
|
+
to read, and to the same tone — never a neighbouring one. So a consumer that sets no
|
|
138
|
+
complement gets this button exactly as it has always been, and clearing the complement
|
|
139
|
+
reverts it with nothing that has to know how to undo anything.
|
|
140
|
+
|
|
141
|
+
The primary fallback keeps its `-solid` companion because the primary slot may hold a
|
|
142
|
+
gradient and `color` and `border-color` reject one. The complement never needs the
|
|
143
|
+
twin: that slot is solid by definition, and in gradient mode it is not used at all.
|
|
144
|
+
See foundations/custom-scheme.js, applyComplementColor. */
|
|
113
145
|
.sds-button--ghost {
|
|
114
146
|
background-color: transparent;
|
|
115
|
-
border-color: var(--color-primary-50-solid);
|
|
116
|
-
color: var(--color-primary-50-solid);
|
|
147
|
+
border-color: var(--color-complement-50, var(--color-primary-50-solid));
|
|
148
|
+
color: var(--color-complement-50, var(--color-primary-50-solid));
|
|
117
149
|
}
|
|
118
150
|
.sds-button--ghost:hover:not(:disabled):not(.sds-button--loading) {
|
|
119
|
-
background: var(--color-primary-10);
|
|
120
|
-
|
|
121
|
-
color: var(--color-primary-60-solid);
|
|
151
|
+
background: var(--color-complement-10, var(--color-primary-10));
|
|
152
|
+
background-origin: border-box;
|
|
153
|
+
border-color: var(--color-complement-60, var(--color-primary-60-solid));
|
|
154
|
+
color: var(--color-complement-60, var(--color-primary-60-solid));
|
|
122
155
|
}
|
|
123
156
|
.sds-button--ghost:active:not(:disabled):not(.sds-button--loading) {
|
|
124
|
-
background: var(--color-primary-20);
|
|
125
|
-
|
|
126
|
-
color: var(--color-primary-70-solid);
|
|
157
|
+
background: var(--color-complement-20, var(--color-primary-20));
|
|
158
|
+
background-origin: border-box;
|
|
159
|
+
border-color: var(--color-complement-70, var(--color-primary-70-solid));
|
|
160
|
+
color: var(--color-complement-70, var(--color-primary-70-solid));
|
|
127
161
|
}
|
|
128
162
|
.sds-button--ghost:focus-visible {
|
|
129
163
|
box-shadow: var(--focus-ring);
|
|
@@ -164,14 +198,17 @@
|
|
|
164
198
|
font-weight: var(--font-weight-medium);
|
|
165
199
|
line-height: var(--lh-body-md);
|
|
166
200
|
background: var(--color-primary-50);
|
|
201
|
+
background-origin: border-box;
|
|
167
202
|
color: var(--color-on-primary);
|
|
168
203
|
border: 1px solid var(--color-primary-50-solid);
|
|
169
204
|
}
|
|
170
205
|
.sds-button--pillFilled:hover:not(:disabled):not(.sds-button--loading) {
|
|
171
206
|
background: var(--color-primary-60);
|
|
207
|
+
background-origin: border-box;
|
|
172
208
|
}
|
|
173
209
|
.sds-button--pillFilled:active:not(:disabled):not(.sds-button--loading) {
|
|
174
210
|
background: var(--color-primary-70);
|
|
211
|
+
background-origin: border-box;
|
|
175
212
|
}
|
|
176
213
|
.sds-button--pillFilled:focus-visible {
|
|
177
214
|
box-shadow: var(--focus-ring);
|
|
@@ -338,3 +375,53 @@
|
|
|
338
375
|
|
|
339
376
|
[data-radius="rounded"] .sds-button--md { border-radius: var(--radius-full); }
|
|
340
377
|
[data-radius="rounded"] .sds-button--lg { border-radius: var(--radius-full); }
|
|
378
|
+
|
|
379
|
+
/* ── The site's gradient, on the stroke ────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
`border-color` rejects a gradient, so a stroke normally reads the `-solid` companion —
|
|
382
|
+
the gradient's first stop, flat. These rules paint it properly instead: the gradient on
|
|
383
|
+
`border-box`, the surface colour on `padding-box`, over a transparent border. Only the
|
|
384
|
+
border's own 2px show the gradient, and `border-radius` still works, which is what
|
|
385
|
+
`border-image` would cost.
|
|
386
|
+
|
|
387
|
+
`[data-primary-gradient]` is written by `applyCustomGradient` and removed by
|
|
388
|
+
`applyCustomColor` — a stylesheet cannot ask what a custom property contains, so the
|
|
389
|
+
code that fills the slot says so. KDLS wrote the same rules under
|
|
390
|
+
`[data-color-scheme="lya"]`, its own brand gradient; SDS deleted every `lya` block with
|
|
391
|
+
the brand and these went with them. Keyed on the marker they serve any site's gradient.
|
|
392
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
393
|
+
|
|
394
|
+
[data-primary-gradient] .sds-button--ghost {
|
|
395
|
+
background:
|
|
396
|
+
linear-gradient(var(--component-bg-color), var(--component-bg-color)) padding-box,
|
|
397
|
+
var(--color-primary-50) border-box;
|
|
398
|
+
border-color: transparent;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
[data-primary-gradient] .sds-button--ghost:hover:not(:disabled):not(.sds-button--loading) {
|
|
402
|
+
background:
|
|
403
|
+
var(--color-primary-10) padding-box,
|
|
404
|
+
var(--color-primary-50) border-box;
|
|
405
|
+
border-color: transparent;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
[data-primary-gradient] .sds-button--ghost:active:not(:disabled):not(.sds-button--loading) {
|
|
409
|
+
background:
|
|
410
|
+
var(--color-primary-20) padding-box,
|
|
411
|
+
var(--color-primary-50) border-box;
|
|
412
|
+
border-color: transparent;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
[data-primary-gradient] .sds-button--ghost:focus-visible {
|
|
416
|
+
background:
|
|
417
|
+
linear-gradient(var(--component-bg-color), var(--component-bg-color)) padding-box,
|
|
418
|
+
var(--color-primary-50) border-box;
|
|
419
|
+
border-color: transparent;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
[data-primary-gradient] .sds-button--pill:hover:not(:disabled):not(.sds-button--loading) {
|
|
423
|
+
background:
|
|
424
|
+
linear-gradient(var(--component-bg-color), var(--component-bg-color)) padding-box,
|
|
425
|
+
var(--color-primary-50) border-box;
|
|
426
|
+
border-color: transparent;
|
|
427
|
+
}
|
package/components/Card/Card.css
CHANGED
|
@@ -33,16 +33,26 @@
|
|
|
33
33
|
.sds-card--hoverable:focus-visible {
|
|
34
34
|
box-shadow: var(--focus-ring);
|
|
35
35
|
}
|
|
36
|
-
/*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
/* Default: top, full width, no proportion — the cover is as tall as its picture unless
|
|
37
|
+
the caller says otherwise, with coverHeight (pixels) or coverRatio (a proportion).
|
|
38
|
+
|
|
39
|
+
This comment used to claim a 16/9 default. It was never true: the class below existed
|
|
40
|
+
and the template never emitted it. Making it the default now would resize every card
|
|
41
|
+
already published, so the ratio is opt-in and the comment is what changed.
|
|
42
|
+
|
|
43
|
+
horizontal: left column. A ratio there also releases the fixed width, otherwise both
|
|
44
|
+
dimensions are explicit and the proportion has nothing to do. ── */
|
|
40
45
|
.sds-card__cover {
|
|
41
46
|
position: relative;
|
|
42
47
|
line-height: 0;
|
|
43
48
|
}
|
|
44
|
-
.sds-card__cover--16-9 {
|
|
45
|
-
aspect-ratio:
|
|
49
|
+
.sds-card__cover--16-9 { aspect-ratio: 16 / 9; }
|
|
50
|
+
.sds-card__cover--4-3 { aspect-ratio: 4 / 3; }
|
|
51
|
+
.sds-card__cover--3-2 { aspect-ratio: 3 / 2; }
|
|
52
|
+
.sds-card__cover--1-1 { aspect-ratio: 1 / 1; }
|
|
53
|
+
|
|
54
|
+
.sds-card--horizontal .sds-card__cover[class*="sds-card__cover--"] {
|
|
55
|
+
width: auto;
|
|
46
56
|
}
|
|
47
57
|
.sds-card__cover img {
|
|
48
58
|
display: block;
|
|
@@ -22,3 +22,21 @@ export const assertCardSize = (v) => assertOneOf('size', v, CARD_SIZES);
|
|
|
22
22
|
// `align` is optional in both places that take it, so undefined passes through.
|
|
23
23
|
export const assertAlign = (v) => (v === undefined ? v : assertOneOf('align', v, ALIGNMENTS));
|
|
24
24
|
export const assertColumns = (v) => assertOneOf('columns', v, INFO_ROW_COLUMNS);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The cover's proportion.
|
|
28
|
+
*
|
|
29
|
+
* Card.css has claimed since it was written that the cover defaults to 16/9, and the
|
|
30
|
+
* class that would do it — .sds-card__cover--16-9 — was never emitted by the template.
|
|
31
|
+
* This is that rule, made real and made opt-in: defaulting it now would change the shape
|
|
32
|
+
* of every card already using this component.
|
|
33
|
+
*
|
|
34
|
+
* Asked for by the Site layer, which needs 4/3 on a vertical property card and 3/2 on a
|
|
35
|
+
* horizontal one (Rafael, 2026-09-17).
|
|
36
|
+
*/
|
|
37
|
+
export const COVER_RATIOS = ['16/9', '4/3', '3/2', '1/1'];
|
|
38
|
+
|
|
39
|
+
export const assertCoverRatio = (v) => (v === undefined ? v : assertOneOf('coverRatio', v, COVER_RATIOS));
|
|
40
|
+
|
|
41
|
+
// '4/3' -> '4-3', which is what the class name uses.
|
|
42
|
+
export const coverRatioSlug = (v) => v.replace('/', '-');
|
|
@@ -11,13 +11,14 @@ nothing. A property listing renders twenty of these.
|
|
|
11
11
|
It is a <button> when it has a click handler, rather than a div with a handler and no
|
|
12
12
|
keyboard path. -->
|
|
13
13
|
|
|
14
|
-
import { assertCardSize } from "./card-variants.js";
|
|
14
|
+
import { assertCardSize, assertCoverRatio, coverRatioSlug } from "./card-variants.js";
|
|
15
15
|
|
|
16
16
|
$ {
|
|
17
17
|
const {
|
|
18
18
|
cover,
|
|
19
19
|
coverWidth,
|
|
20
20
|
coverHeight,
|
|
21
|
+
coverRatio,
|
|
21
22
|
coverBadges,
|
|
22
23
|
horizontal = false,
|
|
23
24
|
size = "md",
|
|
@@ -32,6 +33,7 @@ $ {
|
|
|
32
33
|
} = input;
|
|
33
34
|
|
|
34
35
|
assertCardSize(size);
|
|
36
|
+
assertCoverRatio(coverRatio);
|
|
35
37
|
|
|
36
38
|
const renderable = (value) => (value && value.renderBody) || value;
|
|
37
39
|
const interactive = Boolean(onClick);
|
|
@@ -62,6 +64,14 @@ $ {
|
|
|
62
64
|
horizontal && coverWidth ? `width: ${coverWidth}px` : "",
|
|
63
65
|
!horizontal && coverHeight ? `height: ${coverHeight}px` : "",
|
|
64
66
|
].filter(Boolean).join("; ") || null;
|
|
67
|
+
|
|
68
|
+
// An explicit pixel dimension is the caller being specific; the ratio is the shape's
|
|
69
|
+
// default. Whichever dimension this orientation can honour, if given, wins.
|
|
70
|
+
const pixelWins = horizontal ? Boolean(coverWidth) : Boolean(coverHeight);
|
|
71
|
+
const coverClass = [
|
|
72
|
+
"sds-card__cover",
|
|
73
|
+
coverRatio && !pixelWins ? `sds-card__cover--${coverRatioSlug(coverRatio)}` : "",
|
|
74
|
+
].filter(Boolean).join(" ");
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
<macro|{ }| name="inner">
|
|
@@ -69,7 +79,7 @@ $ {
|
|
|
69
79
|
// skeleton block, promising a picture that was never going to arrive — and once loaded
|
|
70
80
|
// the card jumped as that block disappeared. KDLS gates on the cover alone.
|
|
71
81
|
<if(cover)>
|
|
72
|
-
<div class=
|
|
82
|
+
<div class=coverClass style=coverStyle>
|
|
73
83
|
<if(loading)>
|
|
74
84
|
<sds-skeleton-image fill/>
|
|
75
85
|
</if>
|
|
@@ -247,3 +247,59 @@
|
|
|
247
247
|
/* Indeterminate is drawn from a class rather than the :indeterminate pseudo-class.
|
|
248
248
|
That property can only be set from JavaScript, and this component does not hydrate —
|
|
249
249
|
with appearance:none the browser draws nothing here anyway, so the class is enough. */
|
|
250
|
+
|
|
251
|
+
/* ── The site's gradient, on the stroke ────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
`border-color` rejects a gradient, so a stroke normally reads the `-solid` companion —
|
|
254
|
+
the gradient's first stop, flat. These rules paint it properly instead: the gradient on
|
|
255
|
+
`border-box`, the surface colour on `padding-box`, over a transparent border. Only the
|
|
256
|
+
border's own 2px show the gradient, and `border-radius` still works, which is what
|
|
257
|
+
`border-image` would cost.
|
|
258
|
+
|
|
259
|
+
`[data-primary-gradient]` is written by `applyCustomGradient` and removed by
|
|
260
|
+
`applyCustomColor` — a stylesheet cannot ask what a custom property contains, so the
|
|
261
|
+
code that fills the slot says so. KDLS wrote the same rules under
|
|
262
|
+
`[data-color-scheme="lya"]`, its own brand gradient; SDS deleted every `lya` block with
|
|
263
|
+
the brand and these went with them. Keyed on the marker they serve any site's gradient.
|
|
264
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
265
|
+
|
|
266
|
+
[data-primary-gradient] .sds-checkbox:hover:not(.sds-checkbox--disabled):not(.sds-checkbox--checked):not(.sds-checkbox--indeterminate) .sds-checkbox__box,
|
|
267
|
+
[data-primary-gradient] .sds-checkbox:active:not(.sds-checkbox--disabled):not(.sds-checkbox--checked):not(.sds-checkbox--indeterminate) .sds-checkbox__box {
|
|
268
|
+
background:
|
|
269
|
+
linear-gradient(var(--component-bg-color), var(--component-bg-color)) padding-box,
|
|
270
|
+
var(--color-primary-50) border-box;
|
|
271
|
+
border-color: transparent;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ── And when the gradient FILLS the box, it has no border to fight ────────────
|
|
275
|
+
|
|
276
|
+
A checked box paints the slot as its fill and the `-solid` companion as its border. With
|
|
277
|
+
a solid colour those are the same colour and the border cannot be seen. With a gradient
|
|
278
|
+
they are not: the fill runs from one stop to the other while the 2px ring stays flat at
|
|
279
|
+
the first, so a 20px box gets a hard edge of the wrong colour all the way round — and
|
|
280
|
+
the fill, sized to the box inside that ring and tiled into it, shows the far end there
|
|
281
|
+
as well.
|
|
282
|
+
|
|
283
|
+
`border: none` is the answer, and it is KDLS's: under a gradient the fill IS the whole
|
|
284
|
+
box, so there is nothing left to ring.
|
|
285
|
+
|
|
286
|
+
The touched state brightens rather than stepping to tone 60. Stepping a gradient's tone
|
|
287
|
+
moves every stop at once, which reads as a different colour rather than as the same
|
|
288
|
+
control being pressed. ─────────────────────────────────────────────────────── */
|
|
289
|
+
[data-primary-gradient] .sds-checkbox--checked .sds-checkbox__box,
|
|
290
|
+
[data-primary-gradient] .sds-checkbox--indeterminate .sds-checkbox__box,
|
|
291
|
+
[data-primary-gradient] .sds-checkbox__input:checked ~ .sds-checkbox__box {
|
|
292
|
+
background: var(--color-primary-50);
|
|
293
|
+
border: none;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
[data-primary-gradient] .sds-checkbox--checked:hover:not(.sds-checkbox--disabled) .sds-checkbox__box,
|
|
297
|
+
[data-primary-gradient] .sds-checkbox--checked:active:not(.sds-checkbox--disabled) .sds-checkbox__box,
|
|
298
|
+
[data-primary-gradient] .sds-checkbox--indeterminate:hover:not(.sds-checkbox--disabled) .sds-checkbox__box,
|
|
299
|
+
[data-primary-gradient] .sds-checkbox--indeterminate:active:not(.sds-checkbox--disabled) .sds-checkbox__box,
|
|
300
|
+
[data-primary-gradient] .sds-checkbox:hover:not(.sds-checkbox--disabled) .sds-checkbox__input:checked ~ .sds-checkbox__box,
|
|
301
|
+
[data-primary-gradient] .sds-checkbox:active:not(.sds-checkbox--disabled) .sds-checkbox__input:checked ~ .sds-checkbox__box {
|
|
302
|
+
background: var(--color-primary-50);
|
|
303
|
+
border: none;
|
|
304
|
+
filter: brightness(1.1);
|
|
305
|
+
}
|
|
@@ -218,8 +218,8 @@ button.sds-colorpicker__trigger:disabled {
|
|
|
218
218
|
/* The canvas and the sliders take focus now, so they have to show it. */
|
|
219
219
|
.sds-colorpicker__canvas:focus-visible,
|
|
220
220
|
.sds-colorpicker__slider:focus-visible {
|
|
221
|
-
outline: var(--focus-ring-
|
|
222
|
-
outline-offset:
|
|
221
|
+
outline: var(--focus-ring-outline);
|
|
222
|
+
outline-offset: var(--focus-ring-outline-offset);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/* ── Overlay ──
|
|
@@ -251,3 +251,26 @@ button.sds-colorpicker__trigger:disabled {
|
|
|
251
251
|
.sds-colorpicker--headless {
|
|
252
252
|
display: contents;
|
|
253
253
|
}
|
|
254
|
+
|
|
255
|
+
/* ── The site's gradient, on the stroke ────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
`border-color` rejects a gradient, so a stroke normally reads the `-solid` companion —
|
|
258
|
+
the gradient's first stop, flat. These rules paint it properly instead: the gradient on
|
|
259
|
+
`border-box`, the surface colour on `padding-box`, over a transparent border. Only the
|
|
260
|
+
border's own 2px show the gradient, and `border-radius` still works, which is what
|
|
261
|
+
`border-image` would cost.
|
|
262
|
+
|
|
263
|
+
`[data-primary-gradient]` is written by `applyCustomGradient` and removed by
|
|
264
|
+
`applyCustomColor` — a stylesheet cannot ask what a custom property contains, so the
|
|
265
|
+
code that fills the slot says so. KDLS wrote the same rules under
|
|
266
|
+
`[data-color-scheme="lya"]`, its own brand gradient; SDS deleted every `lya` block with
|
|
267
|
+
the brand and these went with them. Keyed on the marker they serve any site's gradient.
|
|
268
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
269
|
+
|
|
270
|
+
[data-primary-gradient] .sds-colorpicker__trigger:hover:not(.sds-colorpicker__trigger--disabled),
|
|
271
|
+
[data-primary-gradient] .sds-colorpicker__trigger--open {
|
|
272
|
+
background:
|
|
273
|
+
linear-gradient(var(--component-bg-color), var(--component-bg-color)) padding-box,
|
|
274
|
+
var(--color-primary-50) border-box;
|
|
275
|
+
border-color: transparent;
|
|
276
|
+
}
|
|
@@ -65,7 +65,11 @@ class {
|
|
|
65
65
|
this.state.sat = s;
|
|
66
66
|
this.state.bri = v;
|
|
67
67
|
this.state.opacity = startingAlpha(input.value);
|
|
68
|
-
this.
|
|
68
|
+
// The argument, not `this.input`. Marko has not assigned it yet at this point — the
|
|
69
|
+
// same reason onCreate takes one, five lines above. Falling through to `this.input`
|
|
70
|
+
// read `disableAlpha` of undefined, and the throw escaped into the update: the page
|
|
71
|
+
// stopped hydrating, the colour stayed applied and the picker stayed open.
|
|
72
|
+
this.syncHexInput(input);
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
get currentHex() {
|
|
@@ -222,12 +226,29 @@ class {
|
|
|
222
226
|
}
|
|
223
227
|
}
|
|
224
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Reports the colour and closes — and does everything it needs from `this.input` BEFORE
|
|
231
|
+
* either, because either can take this component away.
|
|
232
|
+
*
|
|
233
|
+
* A caller closes on the report, and closing this picker unmounts it. So the line after a
|
|
234
|
+
* call out can be running on a component that no longer exists, where `this.input` is
|
|
235
|
+
* undefined. It threw exactly there; the error escaped into Marko's update, and from that
|
|
236
|
+
* point nothing on the page hydrated again — the colour had been applied and the app was
|
|
237
|
+
* dead, with a theme pill still showing the old value beside a token that had changed.
|
|
238
|
+
*
|
|
239
|
+
* So: read first, then close, then report through the captured reference. Closing goes
|
|
240
|
+
* through `setOpen` like every other way out of this panel — writing `state.open`
|
|
241
|
+
* directly closed it only when nobody owned `open`, which left a controlled caller told
|
|
242
|
+
* about the colour and never told to close.
|
|
243
|
+
*/
|
|
225
244
|
apply() {
|
|
226
|
-
const
|
|
245
|
+
const { onChange, disableAlpha } = this.input;
|
|
246
|
+
const hex = disableAlpha
|
|
227
247
|
? this.currentHex
|
|
228
248
|
: this.currentHex + alphaToHex(this.state.opacity);
|
|
229
|
-
|
|
230
|
-
this.
|
|
249
|
+
|
|
250
|
+
this.setOpen(false);
|
|
251
|
+
onChange?.(hex);
|
|
231
252
|
}
|
|
232
253
|
}
|
|
233
254
|
|
|
@@ -427,3 +427,25 @@
|
|
|
427
427
|
[data-radius="rounded"] .sds-datepicker__day { border-radius: var(--radius-full); }
|
|
428
428
|
[data-radius="rounded"] .sds-datepicker__month-item { border-radius: var(--radius-full); }
|
|
429
429
|
[data-radius="rounded"] .sds-datepicker__year-item { border-radius: var(--radius-full); }
|
|
430
|
+
|
|
431
|
+
/* ── The site's gradient, on the stroke ────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
`border-color` rejects a gradient, so a stroke normally reads the `-solid` companion —
|
|
434
|
+
the gradient's first stop, flat. These rules paint it properly instead: the gradient on
|
|
435
|
+
`border-box`, the surface colour on `padding-box`, over a transparent border. Only the
|
|
436
|
+
border's own 2px show the gradient, and `border-radius` still works, which is what
|
|
437
|
+
`border-image` would cost.
|
|
438
|
+
|
|
439
|
+
`[data-primary-gradient]` is written by `applyCustomGradient` and removed by
|
|
440
|
+
`applyCustomColor` — a stylesheet cannot ask what a custom property contains, so the
|
|
441
|
+
code that fills the slot says so. KDLS wrote the same rules under
|
|
442
|
+
`[data-color-scheme="lya"]`, its own brand gradient; SDS deleted every `lya` block with
|
|
443
|
+
the brand and these went with them. Keyed on the marker they serve any site's gradient.
|
|
444
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
445
|
+
|
|
446
|
+
[data-primary-gradient] .sds-datepicker__day--today {
|
|
447
|
+
background:
|
|
448
|
+
linear-gradient(var(--component-bg-color), var(--component-bg-color)) padding-box,
|
|
449
|
+
var(--color-primary-50) border-box;
|
|
450
|
+
border-color: transparent;
|
|
451
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put the caret inside a floating panel, once the panel can actually take it.
|
|
3
|
+
*
|
|
4
|
+
* `sds-floating` renders its panel `visibility: hidden` until it has measured and placed
|
|
5
|
+
* it — see Floating.css: a panel nobody has measured is 0×0, 0×0 fits anywhere, and a flip
|
|
6
|
+
* decided on that basis puts the panel on the wrong side in front of the reader. The
|
|
7
|
+
* reveal happens one frame later, in `afterOpen`.
|
|
8
|
+
*
|
|
9
|
+
* A component that opens a panel and wants the caret inside it runs on the render where
|
|
10
|
+
* the panel exists and is still hidden. `focus()` there is a no-op, and a silent one:
|
|
11
|
+
* nothing throws, nothing returns false, `document.activeElement` simply does not move.
|
|
12
|
+
* Measured in Chromium on 2026-09-17, sampling every frame after a click on an
|
|
13
|
+
* sds-select's trigger:
|
|
14
|
+
*
|
|
15
|
+
* frame 1 — panel present, visibility hidden, not positioned ← the focus call
|
|
16
|
+
* frame 2 — visibility visible, positioned ← nothing asks again
|
|
17
|
+
*
|
|
18
|
+
* So: ask, check whether the caret arrived, and ask again on the next frame if it did not.
|
|
19
|
+
* Bounded, because a panel that never becomes visible is a panel that was closed, and a
|
|
20
|
+
* retry with no end is worse than a caret that did not move. In the measurement above it
|
|
21
|
+
* took three tries.
|
|
22
|
+
*
|
|
23
|
+
* The target arrives as a LOOKUP rather than an element. Marko replaces the node on
|
|
24
|
+
* re-render, and a reference held across one points at something detached — focus() on it
|
|
25
|
+
* throws nothing and moves nothing, which is the same silent failure by another route.
|
|
26
|
+
*
|
|
27
|
+
* jsdom has no layout and so honours focus() on a hidden element. This rule therefore
|
|
28
|
+
* cannot be caught by rendering a component in a test; it is tested directly, with the
|
|
29
|
+
* first attempt made to fail the way the browser fails it.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** How many times to ask in total, including the first. Three sufficed when measured. */
|
|
33
|
+
const DEFAULT_ATTEMPTS = 5;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {() => (HTMLElement|null|undefined)} lookup finds the element to focus, now
|
|
37
|
+
* @param {object} [options]
|
|
38
|
+
* @param {number} [options.attempts] total tries, including the first
|
|
39
|
+
* @param {(cb: () => void) => void} [options.schedule] when to try again; a frame by
|
|
40
|
+
* default, and injectable so the rule can be stepped by hand in a test
|
|
41
|
+
*/
|
|
42
|
+
export function focusWhenReady(lookup, { attempts = DEFAULT_ATTEMPTS, schedule } = {}) {
|
|
43
|
+
const later = schedule
|
|
44
|
+
?? ((callback) => (typeof requestAnimationFrame === 'function'
|
|
45
|
+
? requestAnimationFrame(() => callback())
|
|
46
|
+
: setTimeout(callback, 16)));
|
|
47
|
+
|
|
48
|
+
const tryOnce = (left) => {
|
|
49
|
+
const target = lookup();
|
|
50
|
+
if (!target) return;
|
|
51
|
+
|
|
52
|
+
target.focus({ preventScroll: true });
|
|
53
|
+
if (document.activeElement === target || left <= 1) return;
|
|
54
|
+
|
|
55
|
+
later(() => tryOnce(left - 1));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
tryOnce(attempts);
|
|
59
|
+
}
|
|
@@ -92,3 +92,25 @@
|
|
|
92
92
|
border-color: var(--border-interactive-color);
|
|
93
93
|
color: var(--color-neutral-40);
|
|
94
94
|
}
|
|
95
|
+
|
|
96
|
+
/* ── The site's gradient, on the stroke ────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
`border-color` rejects a gradient, so a stroke normally reads the `-solid` companion —
|
|
99
|
+
the gradient's first stop, flat. These rules paint it properly instead: the gradient on
|
|
100
|
+
`border-box`, the surface colour on `padding-box`, over a transparent border. Only the
|
|
101
|
+
border's own 2px show the gradient, and `border-radius` still works, which is what
|
|
102
|
+
`border-image` would cost.
|
|
103
|
+
|
|
104
|
+
`[data-primary-gradient]` is written by `applyCustomGradient` and removed by
|
|
105
|
+
`applyCustomColor` — a stylesheet cannot ask what a custom property contains, so the
|
|
106
|
+
code that fills the slot says so. KDLS wrote the same rules under
|
|
107
|
+
`[data-color-scheme="lya"]`, its own brand gradient; SDS deleted every `lya` block with
|
|
108
|
+
the brand and these went with them. Keyed on the marker they serve any site's gradient.
|
|
109
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
110
|
+
|
|
111
|
+
[data-primary-gradient] .sds-incrementer:hover:not(.sds-incrementer--disabled) {
|
|
112
|
+
background:
|
|
113
|
+
linear-gradient(var(--component-bg-color), var(--component-bg-color)) padding-box,
|
|
114
|
+
var(--color-primary-50) border-box;
|
|
115
|
+
border-color: transparent;
|
|
116
|
+
}
|