@kenlo/sds-design-system 1.0.1 → 1.1.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/README.md +51 -3
- package/components/Drawer/Drawer.css +41 -1
- package/components/Drawer/drawer.marko +79 -2
- package/components/IconButton/IconButton.css +18 -2
- package/components/IconButton/icon-button-variants.js +4 -1
- package/components/Input/Input.css +4 -0
- package/components/Modal/Modal.css +3 -1
- package/components/Toast/Toast.css +86 -3
- package/components/Toast/toast-store.js +61 -32
- package/components/Toast/toast.marko +17 -9
- package/components/Toast/toaster.marko +164 -36
- package/components/Uploader/Uploader.css +10 -15
- package/components/Uploader/uploader.marko +3 -3
- package/foundations/tokens.css +1 -2
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -80,9 +80,18 @@ da do próprio app — se o bucket cair, nenhum produto perde a tipografia junto
|
|
|
80
80
|
ficam fora do pacote pelo motivo oposto: os bytes delas não devem pesar em quem nunca
|
|
81
81
|
escolhe nenhuma.
|
|
82
82
|
|
|
83
|
-
> **
|
|
84
|
-
>
|
|
85
|
-
>
|
|
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.
|
|
86
95
|
|
|
87
96
|
Há duas portas para escrevê-los, e a diferença importa:
|
|
88
97
|
|
|
@@ -151,6 +160,21 @@ toolchain que já tem.
|
|
|
151
160
|
|
|
152
161
|
---
|
|
153
162
|
|
|
163
|
+
## Versão
|
|
164
|
+
|
|
165
|
+
Regra combinada com o Rafael Leobas em 05/11/2026, igual no SDS e no KDLS.
|
|
166
|
+
|
|
167
|
+
- **Na npm** vale o padrão dela: `MAIOR.MENOR.CORREÇÃO`, sem zero à esquerda (`1.1.1`, nunca `1.01.01`).
|
|
168
|
+
- **Correção sobe só o último número** (+0.0.1). É assim que todo site recebe a versão nova sozinho.
|
|
169
|
+
- **O número do meio e o último vão de 0 a 99.** Depois do 99, vira o próximo: `1.19.99` → `1.20.0`;
|
|
170
|
+
`1.99.99` → `2.0.0`. Um teste (`tests/display-version.test.js`) reprova o MR se passar de 99.
|
|
171
|
+
- **A virada para um novo número maior é combinada antes com os times**: quem instalou com `^1.x` não
|
|
172
|
+
recebe `2.0.0` sozinho.
|
|
173
|
+
- **No portal** a versão aparece sempre com largura fixa, `vX.XX.XX` (`1.1.1` aparece como `v1.01.01`).
|
|
174
|
+
- **Sem subir a versão, nada chega aos sites**: a esteira só publica na npm uma versão nova.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
154
178
|
## Documentação
|
|
155
179
|
|
|
156
180
|
O portal tem todos os componentes com controles ao vivo, a tabela de props de cada um e o
|
|
@@ -169,3 +193,27 @@ npm run dev
|
|
|
169
193
|
O pacote é público na npm porque os projetos que o consomem precisam instalá-lo sem
|
|
170
194
|
credencial, igual ao KDLS. **Público não quer dizer aberto:** o código é proprietário da
|
|
171
195
|
Kenlo (`UNLICENSED`) e as contribuições passam pelo repositório no GitLab.
|
|
196
|
+
|
|
197
|
+
## Portal no pages.kenlo.io
|
|
198
|
+
|
|
199
|
+
Decisão do Rafael Leobas, 03/11/2026: portais de design system moram no pages.kenlo.io, com o login
|
|
200
|
+
Google da Kenlo, para a empresa toda navegar e testar; demonstrações de produto (VIRA, CMS 2.0, Site 2.0)
|
|
201
|
+
ficam no claude.ai.
|
|
202
|
+
|
|
203
|
+
**Regra:** toda mudança que entra na `main` atualiza o portal no pages.kenlo.io.
|
|
204
|
+
|
|
205
|
+
**Cabe, mas ainda falta o caminho de publicação.** Medido em 03/11/2026 (main de9f1d9): montado com
|
|
206
|
+
`vite build --base ./` e as fontes embutidas, o portal tem 1,76 MB, abaixo do limite de 5 MB por página,
|
|
207
|
+
e passou nas checagens do próprio `kenlo-pages` (`scripts/catalogo.py` e `scripts/conferir_tela.py`, sem
|
|
208
|
+
erro). Uma sessão não consegue entregá-lo: a porta de MR do Kenlo Brain recebe o conteúdo dentro do
|
|
209
|
+
pedido e os ramos pessoais são bloqueados. Falta quem administra o pages.kenlo.io decidir o caminho
|
|
210
|
+
automático: a esteira do `kenlo-pages` montar o portal a partir deste repositório, ou a esteira daqui
|
|
211
|
+
publicar depois de cada merge.
|
|
212
|
+
|
|
213
|
+
**Enquanto isso:** a esteira daqui já tem o passo `deploy-portal`, que publica o portal num bucket a cada
|
|
214
|
+
merge na `main`, parado à espera de duas variáveis (`SDS_DOCS_BUCKET` e `SDS_GCP_SA_KEY`). Com
|
|
215
|
+
`base: './'` em `portal/vite.config.js`, o portal montado funciona em qualquer pasta (bucket ou
|
|
216
|
+
pages.kenlo.io).
|
|
217
|
+
|
|
218
|
+
## Quem aprova mudanças aqui
|
|
219
|
+
Mudança normal: quem pediu aprova sozinho. Mudança de alto risco (dados de cliente, login, cobrança, segredos, infraestrutura, esteira automática): precisa também de um dos cinco aprovadores. Regra completa: https://pages.kenlo.io/p/kenlo-brain/quem-aprova-o-que/ (Constituição G11 c, e, j).
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
theme and would light the screen up instead of dimming it, which is why they were
|
|
14
14
|
literals at all; --color-scrim is that value as a token. *//* ============================================================
|
|
15
15
|
SDS Drawer
|
|
16
|
-
Width: 600px (default) | full height | slides from right/left
|
|
16
|
+
Width: 600px (default), resizable by dragging the inner edge | full height | slides from right/left
|
|
17
17
|
Header: Bold 24px, border-bottom neutral-30
|
|
18
18
|
Footer: py-16, border-top neutral-30
|
|
19
19
|
============================================================ */
|
|
@@ -51,6 +51,43 @@
|
|
|
51
51
|
box-shadow: 4px 0px 24px var(--color-opacity-20);
|
|
52
52
|
animation: sds-drawer-in-left 250ms var(--ease-standard);
|
|
53
53
|
}
|
|
54
|
+
/* ── Resize handle: 8px hit area on the inner edge, 2px line on hover/drag ── */
|
|
55
|
+
.sds-drawer__resize-handle {
|
|
56
|
+
position: absolute;
|
|
57
|
+
top: 0;
|
|
58
|
+
bottom: 0;
|
|
59
|
+
left: calc(var(--spacing-1) * -1);
|
|
60
|
+
width: var(--spacing-2);
|
|
61
|
+
cursor: col-resize;
|
|
62
|
+
touch-action: none;
|
|
63
|
+
z-index: 1;
|
|
64
|
+
outline: none;
|
|
65
|
+
}
|
|
66
|
+
.sds-drawer--left .sds-drawer__resize-handle {
|
|
67
|
+
left: auto;
|
|
68
|
+
right: calc(var(--spacing-1) * -1);
|
|
69
|
+
}
|
|
70
|
+
.sds-drawer__resize-handle::after {
|
|
71
|
+
content: '';
|
|
72
|
+
position: absolute;
|
|
73
|
+
top: 0;
|
|
74
|
+
bottom: 0;
|
|
75
|
+
left: 50%;
|
|
76
|
+
width: 2px;
|
|
77
|
+
transform: translateX(-50%);
|
|
78
|
+
background: transparent;
|
|
79
|
+
transition: background 120ms ease;
|
|
80
|
+
}
|
|
81
|
+
.sds-drawer__resize-handle:hover::after,
|
|
82
|
+
.sds-drawer--resizing .sds-drawer__resize-handle::after {
|
|
83
|
+
background: var(--color-primary-50);
|
|
84
|
+
}
|
|
85
|
+
.sds-drawer__resize-handle:focus-visible {
|
|
86
|
+
box-shadow: var(--focus-ring);
|
|
87
|
+
}
|
|
88
|
+
.sds-drawer--resizing {
|
|
89
|
+
user-select: none;
|
|
90
|
+
}
|
|
54
91
|
@keyframes sds-drawer-in {
|
|
55
92
|
from { transform: translateX(100%); }
|
|
56
93
|
to { transform: translateX(0); }
|
|
@@ -146,6 +183,9 @@
|
|
|
146
183
|
.sds-drawer {
|
|
147
184
|
width: 100%;
|
|
148
185
|
}
|
|
186
|
+
.sds-drawer__resize-handle {
|
|
187
|
+
display: none;
|
|
188
|
+
}
|
|
149
189
|
.sds-drawer__footer {
|
|
150
190
|
flex-direction: column-reverse;
|
|
151
191
|
align-items: stretch;
|
|
@@ -32,9 +32,23 @@ import { lockScroll, unlockScroll, trapFocus } from "../../foundations/overlay.j
|
|
|
32
32
|
import dismissIcon from "../../icons/generated/fluent-dismiss-regular.marko";
|
|
33
33
|
import backIcon from "../../icons/generated/fluent-chevron-left-regular.marko";
|
|
34
34
|
|
|
35
|
+
static const DEFAULT_WIDTH = 600;
|
|
36
|
+
static const MIN_WIDTH = 400;
|
|
37
|
+
static const MAX_WIDTH_RATIO = 0.9;
|
|
38
|
+
static const KEYBOARD_STEP = 16;
|
|
39
|
+
|
|
40
|
+
static function maxWidth() {
|
|
41
|
+
return Math.max(MIN_WIDTH, Math.round(window.innerWidth * MAX_WIDTH_RATIO));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static function clampWidth(value) {
|
|
45
|
+
return Math.min(maxWidth(), Math.max(MIN_WIDTH, Math.round(value)));
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
class {
|
|
36
49
|
onCreate() {
|
|
37
|
-
|
|
50
|
+
// width stays null until the visitor drags: the CSS default (600px) holds until then.
|
|
51
|
+
this.state = { confirming: false, width: null, resizing: false };
|
|
38
52
|
this.release = null;
|
|
39
53
|
}
|
|
40
54
|
|
|
@@ -80,8 +94,10 @@ class {
|
|
|
80
94
|
}
|
|
81
95
|
|
|
82
96
|
shut() {
|
|
97
|
+
this.stopResize?.();
|
|
83
98
|
if (!this.locked) return;
|
|
84
99
|
this.locked = false;
|
|
100
|
+
this.state.width = null;
|
|
85
101
|
unlockScroll();
|
|
86
102
|
this.release?.();
|
|
87
103
|
this.release = null;
|
|
@@ -121,6 +137,52 @@ class {
|
|
|
121
137
|
this.retrap(this.getEl("panel"));
|
|
122
138
|
}
|
|
123
139
|
|
|
140
|
+
currentWidth() {
|
|
141
|
+
return this.state.width ?? (this.getEl("panel")?.getBoundingClientRect().width || DEFAULT_WIDTH);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
growDirection() {
|
|
145
|
+
return (this.input.placement ?? "right") === "right" ? -1 : 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
startResize(event) {
|
|
149
|
+
if (event.button !== 0) return;
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
const startX = event.clientX;
|
|
152
|
+
const startWidth = this.currentWidth();
|
|
153
|
+
const direction = this.growDirection();
|
|
154
|
+
this.state.resizing = true;
|
|
155
|
+
|
|
156
|
+
const onMove = (moveEvent) => {
|
|
157
|
+
this.state.width = clampWidth(startWidth + (moveEvent.clientX - startX) * direction);
|
|
158
|
+
};
|
|
159
|
+
const onUp = () => {
|
|
160
|
+
this.stopResize = null;
|
|
161
|
+
this.state.resizing = false;
|
|
162
|
+
document.removeEventListener("pointermove", onMove);
|
|
163
|
+
document.removeEventListener("pointerup", onUp);
|
|
164
|
+
document.removeEventListener("pointercancel", onUp);
|
|
165
|
+
};
|
|
166
|
+
this.stopResize = onUp;
|
|
167
|
+
document.addEventListener("pointermove", onMove);
|
|
168
|
+
document.addEventListener("pointerup", onUp);
|
|
169
|
+
document.addEventListener("pointercancel", onUp);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
resizeByKey(event) {
|
|
173
|
+
const direction = this.growDirection();
|
|
174
|
+
const keys = {
|
|
175
|
+
ArrowLeft: () => this.currentWidth() - KEYBOARD_STEP * direction,
|
|
176
|
+
ArrowRight: () => this.currentWidth() + KEYBOARD_STEP * direction,
|
|
177
|
+
Home: () => MIN_WIDTH,
|
|
178
|
+
End: () => maxWidth(),
|
|
179
|
+
};
|
|
180
|
+
const next = keys[event.key];
|
|
181
|
+
if (!next) return;
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
this.state.width = clampWidth(next());
|
|
184
|
+
}
|
|
185
|
+
|
|
124
186
|
confirmAndClose() {
|
|
125
187
|
this.state.confirming = false;
|
|
126
188
|
this.wantsConfirmFocus = false;
|
|
@@ -167,8 +229,10 @@ $ {
|
|
|
167
229
|
const panelClass = [
|
|
168
230
|
"sds-drawer",
|
|
169
231
|
placement !== "right" ? `sds-drawer--${placement}` : "",
|
|
232
|
+
state.resizing ? "sds-drawer--resizing" : "",
|
|
170
233
|
extraClass,
|
|
171
234
|
].filter(Boolean).join(" ");
|
|
235
|
+
const panelStyle = state.width !== null ? `--drawer-width:${state.width}px` : null;
|
|
172
236
|
}
|
|
173
237
|
|
|
174
238
|
<if(isOpen)>
|
|
@@ -181,7 +245,7 @@ $ {
|
|
|
181
245
|
aria-label=title
|
|
182
246
|
on-click("handleOverlayClick")
|
|
183
247
|
>
|
|
184
|
-
<div key="panel" class=panelClass>
|
|
248
|
+
<div key="panel" class=panelClass style=panelStyle>
|
|
185
249
|
<div class="sds-drawer__header">
|
|
186
250
|
<if(onBack)>
|
|
187
251
|
<sds-icon-button variant="neutral" aria-label=backLabel onClick=onBack>
|
|
@@ -236,6 +300,19 @@ $ {
|
|
|
236
300
|
</else>
|
|
237
301
|
</div>
|
|
238
302
|
</if>
|
|
303
|
+
|
|
304
|
+
<div
|
|
305
|
+
class="sds-drawer__resize-handle"
|
|
306
|
+
role="separator"
|
|
307
|
+
aria-orientation="vertical"
|
|
308
|
+
aria-label="Redimensionar drawer"
|
|
309
|
+
aria-valuemin=MIN_WIDTH
|
|
310
|
+
aria-valuemax=(typeof window !== "undefined" ? maxWidth() : null)
|
|
311
|
+
aria-valuenow=(state.width ?? DEFAULT_WIDTH)
|
|
312
|
+
tabindex="0"
|
|
313
|
+
on-pointerdown("startResize")
|
|
314
|
+
on-keydown("resizeByKey")
|
|
315
|
+
/>
|
|
239
316
|
</div>
|
|
240
317
|
|
|
241
318
|
<if(state.confirming)>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*//* ============================================================
|
|
8
8
|
SDS IconButton
|
|
9
9
|
Sizes: 32 | 40 | 48px
|
|
10
|
-
Variants: primary (colored, default) | neutral (gray)
|
|
10
|
+
Variants: primary (colored, default) | neutral (gray) | inverse (white, on photo/dark)
|
|
11
11
|
============================================================ */
|
|
12
12
|
|
|
13
13
|
.sds-icon-button {
|
|
@@ -74,11 +74,27 @@
|
|
|
74
74
|
.sds-icon-button--neutral .sds-icon-button__icon {
|
|
75
75
|
color: var(--color-neutral-70);
|
|
76
76
|
}
|
|
77
|
+
/* ── Inverse: white icon over a photo or a dark surface ──
|
|
78
|
+
Ported from kdls-design-system 2.10.0 (2f15769). That surface never follows the theme,
|
|
79
|
+
so neither do the overlays: a fixed white tint, built from --color-white the way
|
|
80
|
+
--color-scrim is built from --color-black. Not --color-opacity-*, which follows the theme,
|
|
81
|
+
nor --color-opacity-inverse-*, which runs against it — in the dark theme that one turns
|
|
82
|
+
dark, and a dark tint on a dark surface is a hover nobody sees. KDLS writes the same
|
|
83
|
+
10% / 20% as rgba literals. */
|
|
84
|
+
.sds-icon-button--inverse .sds-icon-button__icon {
|
|
85
|
+
color: var(--color-white);
|
|
86
|
+
}
|
|
87
|
+
.sds-icon-button--inverse:hover:not(:disabled):not(.sds-icon-button--loading) {
|
|
88
|
+
background-color: color-mix(in srgb, var(--color-white) 10%, transparent);
|
|
89
|
+
}
|
|
90
|
+
.sds-icon-button--inverse:active:not(:disabled):not(.sds-icon-button--loading) {
|
|
91
|
+
background-color: color-mix(in srgb, var(--color-white) 20%, transparent);
|
|
92
|
+
}
|
|
77
93
|
/* ── Focus ring ── */
|
|
78
94
|
.sds-icon-button:focus-visible {
|
|
79
95
|
box-shadow: var(--focus-ring);
|
|
80
96
|
}
|
|
81
|
-
/* ── Disabled — always last, overrides primary/neutral icon color via cascade order ── */
|
|
97
|
+
/* ── Disabled — always last, overrides primary/neutral/inverse icon color via cascade order ── */
|
|
82
98
|
.sds-icon-button:disabled .sds-icon-button__icon,
|
|
83
99
|
.sds-icon-button[aria-disabled="true"] .sds-icon-button__icon {
|
|
84
100
|
color: var(--color-neutral-40);
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ported from kdls-design-system v2.5.1 (0f3edf8) components/IconButton/IconButton.tsx.
|
|
5
5
|
* Divergence: none. `primary` was already the slot, not a Kenlo value.
|
|
6
|
+
*
|
|
7
|
+
* `inverse` came later, from kdls-design-system 2.10.0 (2f15769): a white glyph for a button
|
|
8
|
+
* over a photo or a dark surface — the Uploader's strip over a preview is the first.
|
|
6
9
|
*/
|
|
7
|
-
export const ICON_BUTTON_VARIANTS = ['primary', 'neutral'];
|
|
10
|
+
export const ICON_BUTTON_VARIANTS = ['primary', 'neutral', 'inverse'];
|
|
8
11
|
export const ICON_BUTTON_SIZES = ['sm', 'md', 'lg'];
|
|
9
12
|
|
|
10
13
|
function assertOneOf(label, value, allowed) {
|
|
@@ -413,6 +413,10 @@
|
|
|
413
413
|
.sds-input--filled.sds-input--textbox .sds-input__label,
|
|
414
414
|
.sds-input--textbox:focus-within .sds-input__label {
|
|
415
415
|
top: var(--spacing-2);
|
|
416
|
+
/* Reset the unfloated rule's translateY(-50%) above — same specificity, later in the
|
|
417
|
+
file, so it otherwise wins the tie and cancels `top`, pinning the label to the
|
|
418
|
+
field's edge instead of 8px below it. */
|
|
419
|
+
transform: none;
|
|
416
420
|
font-size: var(--font-size-label-sm);
|
|
417
421
|
line-height: var(--lh-label-sm);
|
|
418
422
|
}
|
|
@@ -80,7 +80,9 @@
|
|
|
80
80
|
.sds-modal__body {
|
|
81
81
|
flex: 1;
|
|
82
82
|
overflow-y: auto;
|
|
83
|
-
padding
|
|
83
|
+
/* Vertical padding keeps a focused control's ring from clipping against the
|
|
84
|
+
scroll container's edge — the container has no padding to draw into. */
|
|
85
|
+
padding: var(--spacing-1) var(--spacing-6);
|
|
84
86
|
}
|
|
85
87
|
/* ── Footer: pt-[24px] pb-[24px] px-[24px] ── */
|
|
86
88
|
.sds-modal__footer {
|
|
@@ -46,14 +46,78 @@
|
|
|
46
46
|
bottom: var(--spacing-6);
|
|
47
47
|
left: var(--spacing-6);
|
|
48
48
|
z-index: var(--z-notice);
|
|
49
|
-
display: flex;
|
|
50
|
-
flex-direction: column;
|
|
51
|
-
gap: var(--spacing-3);
|
|
52
49
|
pointer-events: none;
|
|
53
50
|
}
|
|
54
51
|
.sds-toaster > * {
|
|
55
52
|
pointer-events: auto;
|
|
56
53
|
}
|
|
54
|
+
/* ── Pile: the front toast plus the edge of the next two (KDLS MR !67) ──
|
|
55
|
+
Each edge is 10px higher and 5% smaller; 10px sits between --spacing-2 and --spacing-3
|
|
56
|
+
and there is no token for a scale. The edges take the front toast's height from
|
|
57
|
+
inset: 0, so nothing is measured in script. */
|
|
58
|
+
.sds-toaster__close-all {
|
|
59
|
+
display: flex;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
margin-bottom: var(--spacing-3);
|
|
62
|
+
}
|
|
63
|
+
.sds-toaster__stack--peek-1 .sds-toaster__close-all {
|
|
64
|
+
margin-bottom: calc(var(--spacing-3) + 10px);
|
|
65
|
+
}
|
|
66
|
+
.sds-toaster__stack--peek-2 .sds-toaster__close-all {
|
|
67
|
+
margin-bottom: calc(var(--spacing-3) + 20px);
|
|
68
|
+
}
|
|
69
|
+
.sds-toaster__list {
|
|
70
|
+
position: relative;
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
gap: var(--spacing-3);
|
|
74
|
+
border-radius: var(--radius-md);
|
|
75
|
+
outline: none;
|
|
76
|
+
}
|
|
77
|
+
.sds-toaster__list--collapsed {
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
}
|
|
80
|
+
.sds-toaster__list:focus-visible {
|
|
81
|
+
box-shadow: var(--focus-ring);
|
|
82
|
+
}
|
|
83
|
+
.sds-toaster__item {
|
|
84
|
+
position: relative;
|
|
85
|
+
z-index: 3;
|
|
86
|
+
transform-origin: 50% 0;
|
|
87
|
+
transition: transform 200ms ease;
|
|
88
|
+
}
|
|
89
|
+
.sds-toaster__item--depth-1,
|
|
90
|
+
.sds-toaster__item--depth-2,
|
|
91
|
+
.sds-toaster__item--depth-3 {
|
|
92
|
+
position: absolute;
|
|
93
|
+
inset: 0;
|
|
94
|
+
}
|
|
95
|
+
.sds-toaster__item--depth-1 {
|
|
96
|
+
z-index: 2;
|
|
97
|
+
transform: translateY(-10px) scale(0.95);
|
|
98
|
+
}
|
|
99
|
+
.sds-toaster__item--depth-2,
|
|
100
|
+
.sds-toaster__item--depth-3 {
|
|
101
|
+
z-index: 1;
|
|
102
|
+
transform: translateY(-20px) scale(0.9);
|
|
103
|
+
}
|
|
104
|
+
.sds-toaster__item--depth-3 {
|
|
105
|
+
z-index: 0;
|
|
106
|
+
opacity: 0;
|
|
107
|
+
}
|
|
108
|
+
.sds-toaster__item--depth-1 .sds-toast,
|
|
109
|
+
.sds-toaster__item--depth-2 .sds-toast,
|
|
110
|
+
.sds-toaster__item--depth-3 .sds-toast {
|
|
111
|
+
height: 100%;
|
|
112
|
+
}
|
|
113
|
+
.sds-toaster__item--depth-1 .sds-toast > *,
|
|
114
|
+
.sds-toaster__item--depth-2 .sds-toast > *,
|
|
115
|
+
.sds-toaster__item--depth-3 .sds-toast > * {
|
|
116
|
+
opacity: 0;
|
|
117
|
+
}
|
|
118
|
+
@media (prefers-reduced-motion: reduce) {
|
|
119
|
+
.sds-toaster__item { transition: none; }
|
|
120
|
+
}
|
|
57
121
|
.sds-toast__icon {
|
|
58
122
|
display: inline-flex;
|
|
59
123
|
align-items: center;
|
|
@@ -65,6 +129,25 @@
|
|
|
65
129
|
.sds-toast__message {
|
|
66
130
|
flex: 1;
|
|
67
131
|
}
|
|
132
|
+
/* ── Action: a line of its own below the message; the ✕ stays on the first line ── */
|
|
133
|
+
.sds-toast--with-action {
|
|
134
|
+
flex-wrap: wrap;
|
|
135
|
+
row-gap: 0;
|
|
136
|
+
}
|
|
137
|
+
.sds-toast--with-action .sds-toast__message {
|
|
138
|
+
min-width: 0;
|
|
139
|
+
overflow-wrap: break-word;
|
|
140
|
+
}
|
|
141
|
+
.sds-toast__action-row {
|
|
142
|
+
order: 1;
|
|
143
|
+
flex-basis: 100%;
|
|
144
|
+
box-sizing: border-box;
|
|
145
|
+
/* Icon (24px) + gap: the button starts where the text starts */
|
|
146
|
+
padding-left: calc(24px + var(--spacing-2));
|
|
147
|
+
}
|
|
148
|
+
.sds-toast--neutral .sds-toast__action-row {
|
|
149
|
+
padding-left: 0;
|
|
150
|
+
}
|
|
68
151
|
.sds-toast__dismiss {
|
|
69
152
|
flex-shrink: 0;
|
|
70
153
|
color: inherit; /* <button> resets color; force inheritance from .sds-toast */
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The list of live toasts,
|
|
2
|
+
* The list of live toasts, the timer of the one in front, and who to tell when it changes.
|
|
3
3
|
*
|
|
4
|
-
* Ported from kdls-design-system
|
|
5
|
-
*
|
|
4
|
+
* Ported from kdls-design-system components/Toast/toast-store.ts — first from v2.5.1
|
|
5
|
+
* (0f3edf8), then the pile from MR !67 (ai-fix/kdls-toast-pilha, 8df83d3). It is already
|
|
6
|
+
* framework-free — a module-level list and a set of listeners. That is what lets
|
|
6
7
|
* `toast(...)` be called from anywhere, including from a catch block that is nowhere near a
|
|
7
8
|
* component, and it survives the move from React to Marko untouched in behaviour.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* Only the front toast — the oldest one still on screen — counts down. The next one starts
|
|
11
|
+
* its own time when the front one leaves: several toasts fired at once pile up, and if they
|
|
12
|
+
* all ran together they would all vanish together, before anyone read the ones behind.
|
|
13
|
+
*
|
|
14
|
+
* The only other change is the class name in the comment below, because the exit animation
|
|
15
|
+
* it points at is now sds-toast--leaving.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
/** Fixed by the design system: 4s, or 8s when the visitor needs time to read a failure. */
|
|
@@ -20,7 +25,9 @@ export const TOAST_EXIT_MS = 220;
|
|
|
20
25
|
let toasts = [];
|
|
21
26
|
let seq = 0;
|
|
22
27
|
const listeners = new Set();
|
|
23
|
-
|
|
28
|
+
/** The front toast's time: { id, startedAt, remaining, timeout }. `timeout` is unset while paused. */
|
|
29
|
+
let countdown = null;
|
|
30
|
+
let paused = false;
|
|
24
31
|
|
|
25
32
|
function emit() {
|
|
26
33
|
const snapshot = [...toasts];
|
|
@@ -37,38 +44,53 @@ export function subscribe(listener) {
|
|
|
37
44
|
return () => { listeners.delete(listener); };
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
function run() {
|
|
48
|
+
if (!countdown || paused) return;
|
|
49
|
+
const { id } = countdown;
|
|
50
|
+
countdown.startedAt = Date.now();
|
|
51
|
+
countdown.timeout = setTimeout(() => startLeaving(id), countdown.remaining);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Starts the time of the front toast, when it is not counting yet. */
|
|
55
|
+
function armFront() {
|
|
56
|
+
if (countdown) return;
|
|
57
|
+
const front = toasts.find((t) => !t.leaving);
|
|
58
|
+
if (!front) return;
|
|
59
|
+
countdown = {
|
|
60
|
+
id: front.id,
|
|
61
|
+
startedAt: Date.now(),
|
|
62
|
+
remaining: front.feedback === 'error' ? DURATION_ERROR : DURATION_DEFAULT,
|
|
63
|
+
};
|
|
64
|
+
run();
|
|
43
65
|
}
|
|
44
66
|
|
|
45
67
|
function startLeaving(id) {
|
|
46
68
|
const item = toasts.find((t) => t.id === id);
|
|
47
69
|
if (!item || item.leaving) return;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
running.delete(id);
|
|
70
|
+
if (countdown?.id === id) {
|
|
71
|
+
clearTimeout(countdown.timeout);
|
|
72
|
+
countdown = null;
|
|
52
73
|
}
|
|
53
74
|
toasts = toasts.map((t) => (t.id === id ? { ...t, leaving: true } : t));
|
|
75
|
+
armFront();
|
|
54
76
|
emit();
|
|
55
77
|
setTimeout(() => remove(id), TOAST_EXIT_MS);
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
function remove(id) {
|
|
59
81
|
toasts = toasts.filter((t) => t.id !== id);
|
|
82
|
+
// The pile left from under the pointer, so no mouseleave will come to resume.
|
|
83
|
+
if (toasts.length === 0) paused = false;
|
|
60
84
|
emit();
|
|
61
85
|
}
|
|
62
86
|
|
|
63
|
-
/** Adds a toast
|
|
87
|
+
/** Adds a toast at the end of the line. Returns the id. */
|
|
64
88
|
export function addToast(message, opts = {}) {
|
|
65
89
|
const id = ++seq;
|
|
66
|
-
const feedback = opts.feedback ?? 'neutral';
|
|
67
|
-
const duration = feedback === 'error' ? DURATION_ERROR : DURATION_DEFAULT;
|
|
68
90
|
const item = {
|
|
69
91
|
id,
|
|
70
92
|
message,
|
|
71
|
-
feedback,
|
|
93
|
+
feedback: opts.feedback ?? 'neutral',
|
|
72
94
|
actionLabel: opts.actionLabel,
|
|
73
95
|
onAction: opts.onAction,
|
|
74
96
|
actionDisabled: opts.actionDisabled,
|
|
@@ -78,8 +100,8 @@ export function addToast(message, opts = {}) {
|
|
|
78
100
|
leaving: false,
|
|
79
101
|
};
|
|
80
102
|
toasts = [...toasts, item];
|
|
103
|
+
armFront();
|
|
81
104
|
emit();
|
|
82
|
-
arm(id, duration);
|
|
83
105
|
return id;
|
|
84
106
|
}
|
|
85
107
|
|
|
@@ -88,30 +110,37 @@ export function dismissToast(id) {
|
|
|
88
110
|
startLeaving(id);
|
|
89
111
|
}
|
|
90
112
|
|
|
113
|
+
/** Closes every toast on screen at once — the pile's "Fechar todos". */
|
|
114
|
+
export function dismissAll() {
|
|
115
|
+
toasts.filter((t) => !t.leaving).forEach((t) => startLeaving(t.id));
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
/**
|
|
92
|
-
* Holds
|
|
93
|
-
*
|
|
119
|
+
* Holds the time where it is, including the turn of whichever toast comes forward next.
|
|
120
|
+
* The container calls this on hover: a toast that vanishes while someone is reaching for
|
|
121
|
+
* its action is a toast that wasted their time.
|
|
94
122
|
*/
|
|
95
123
|
export function pauseAll() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
if (paused) return;
|
|
125
|
+
paused = true;
|
|
126
|
+
if (!countdown?.timeout) return;
|
|
127
|
+
clearTimeout(countdown.timeout);
|
|
128
|
+
countdown.timeout = undefined;
|
|
129
|
+
countdown.remaining = Math.max(0, countdown.remaining - (Date.now() - countdown.startedAt));
|
|
101
130
|
}
|
|
102
131
|
|
|
103
|
-
/** Starts
|
|
132
|
+
/** Starts it again with whatever time was left. */
|
|
104
133
|
export function resumeAll() {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
134
|
+
if (!paused) return;
|
|
135
|
+
paused = false;
|
|
136
|
+
run();
|
|
109
137
|
}
|
|
110
138
|
|
|
111
139
|
/** Tests only: the list is module state and outlives them. */
|
|
112
140
|
export function resetToasts() {
|
|
113
|
-
|
|
114
|
-
|
|
141
|
+
if (countdown) clearTimeout(countdown.timeout);
|
|
142
|
+
countdown = null;
|
|
143
|
+
paused = false;
|
|
115
144
|
toasts = [];
|
|
116
145
|
seq = 0;
|
|
117
146
|
emit();
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
surface because KDLS put it there, marked internal for the same reason: nothing else
|
|
10
10
|
should set it.
|
|
11
11
|
|
|
12
|
+
The action button sits on a line of its own, below the message and aligned with it,
|
|
13
|
+
as KDLS has it since MR !67 (ai-fix/kdls-toast-pilha, 8df83d3): beside the message it
|
|
14
|
+
left the text 136px and a short sentence ran to four or six lines; below it the text
|
|
15
|
+
has the full 256px, and the ✕ stays on the first line, on the right.
|
|
16
|
+
|
|
12
17
|
Divergence: none in behaviour. The four hover and active overlays were rgba literals
|
|
13
18
|
in KDLS's stylesheet, which is what --color-opacity-inverse-* now holds — the regular
|
|
14
19
|
opacity family follows the theme and is backwards on a surface that runs against it. -->
|
|
@@ -49,6 +54,7 @@ $ {
|
|
|
49
54
|
const classes = [
|
|
50
55
|
"sds-toast",
|
|
51
56
|
`sds-toast--${feedback}`,
|
|
57
|
+
actionLabel ? "sds-toast--with-action" : "",
|
|
52
58
|
leaving ? "sds-toast--leaving" : "",
|
|
53
59
|
extraClass,
|
|
54
60
|
].filter(Boolean).join(" ");
|
|
@@ -68,15 +74,17 @@ $ {
|
|
|
68
74
|
<span class="sds-toast__message">${message}</span>
|
|
69
75
|
|
|
70
76
|
<if(actionLabel)>
|
|
71
|
-
<sds-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
<div class="sds-toast__action-row">
|
|
78
|
+
<sds-button
|
|
79
|
+
variant="link"
|
|
80
|
+
label=actionLabel
|
|
81
|
+
onClick=onAction
|
|
82
|
+
disabled=actionDisabled
|
|
83
|
+
disabledTooltip=actionDisabledTooltip
|
|
84
|
+
disabledTooltipPosition=actionDisabledTooltipPosition
|
|
85
|
+
class="sds-toast__action"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
80
88
|
</if>
|
|
81
89
|
|
|
82
90
|
<if(showDismiss)>
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
<!-- sds-toaster — ported from kdls-design-system v2.5.1 (0f3edf8) components/Toast/Toaster.tsx
|
|
1
|
+
<!-- sds-toaster — ported from kdls-design-system v2.5.1 (0f3edf8) components/Toast/Toaster.tsx,
|
|
2
|
+
and the pile from MR !67 (ai-fix/kdls-toast-pilha, 8df83d3).
|
|
2
3
|
|
|
3
4
|
Mount once at the root of the page. It subscribes to the store and renders whatever
|
|
4
|
-
toast(...) has fired,
|
|
5
|
-
|
|
5
|
+
toast(...) has fired, in the corner. No attributes: the position is fixed and the
|
|
6
|
+
timing belongs to the store.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
With more than one toast they pile up: only the first to arrive is shown whole, with
|
|
9
|
+
the edge of the next two above it, each 10px higher and 5% smaller; from the fourth on
|
|
10
|
+
nothing shows. A click on the pile (or Enter, with the focus on it) opens it into a
|
|
11
|
+
column, 12px apart, the newest at the bottom; a click outside or Esc closes it. A click
|
|
12
|
+
on a toast's ✕ or action does not open it. With the pointer over it, the keyboard focus
|
|
13
|
+
on it, or the pile open, a "Fechar todos" button sits centred above it. One toast per
|
|
14
|
+
problem is the rule this exists for: seven refused photos in a column took 85% of a
|
|
15
|
+
laptop screen, and all seven left at once.
|
|
16
|
+
|
|
17
|
+
Hovering pauses the time, including the turn of whichever toast comes forward next. A
|
|
18
|
+
toast that vanishes while someone is reaching for its action has wasted their time, and
|
|
19
|
+
KDLS gets this right — it is carried as is.
|
|
20
|
+
|
|
21
|
+
Keyboard focus that enters a toast behind the front one opens the pile, so the caret
|
|
22
|
+
never lands on a button nobody can see.
|
|
9
23
|
|
|
10
24
|
Divergences:
|
|
11
25
|
|
|
@@ -17,54 +31,168 @@
|
|
|
17
31
|
|
|
18
32
|
It renders nothing at all until the store has something, rather than an empty
|
|
19
33
|
container: an empty <div role="region" aria-label="Notificações"> is a landmark a
|
|
20
|
-
screen reader offers to navigate to and finds nothing inside.
|
|
34
|
+
screen reader offers to navigate to and finds nothing inside.
|
|
35
|
+
|
|
36
|
+
The listeners for a click outside and for Esc live on the document for as long as the
|
|
37
|
+
toaster is mounted and do nothing while the pile is closed. KDLS adds them only while
|
|
38
|
+
it is open; here that would mean re-binding on a state change, and the cost of an idle
|
|
39
|
+
check per key press is nothing. -->
|
|
40
|
+
|
|
41
|
+
import { subscribe, dismissToast, dismissAll, pauseAll, resumeAll } from "./toast-store.js";
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
static const HIDDEN_DEPTH = 3;
|
|
44
|
+
|
|
45
|
+
static function itemClass(index, collapsed) {
|
|
46
|
+
const depth = collapsed ? Math.min(index, HIDDEN_DEPTH) : 0;
|
|
47
|
+
return ["sds-toaster__item", depth ? `sds-toaster__item--depth-${depth}` : ""]
|
|
48
|
+
.filter(Boolean).join(" ");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static function isFocusVisible(el) {
|
|
52
|
+
try {
|
|
53
|
+
return el.matches(":focus-visible");
|
|
54
|
+
} catch {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
23
58
|
|
|
24
59
|
class {
|
|
25
60
|
onCreate() {
|
|
26
|
-
this.state = { items: [] };
|
|
61
|
+
this.state = { items: [], expanded: false, hovered: false, keyboardFocus: false };
|
|
27
62
|
}
|
|
28
63
|
|
|
29
64
|
onMount() {
|
|
30
65
|
// The store calls back immediately with what it already holds, so a toaster mounted
|
|
31
66
|
// after a toast was fired still shows it.
|
|
32
|
-
this.unsubscribe = subscribe((items) => {
|
|
67
|
+
this.unsubscribe = subscribe((items) => {
|
|
68
|
+
this.state.items = items;
|
|
69
|
+
if (items.length < 2) this.state.expanded = false;
|
|
70
|
+
if (!items.length) {
|
|
71
|
+
this.state.hovered = false;
|
|
72
|
+
this.state.keyboardFocus = false;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.onDocumentMouseDown = (event) => {
|
|
77
|
+
if (!this.state.expanded) return;
|
|
78
|
+
// Marko replaces nodes on re-render: look the pile up now, never keep a reference.
|
|
79
|
+
const stack = this.getEl("stack");
|
|
80
|
+
if (!stack || !stack.contains(event.target)) this.state.expanded = false;
|
|
81
|
+
};
|
|
82
|
+
// Capture: the open pile is the top layer, so its Esc runs before a Drawer's or Modal's.
|
|
83
|
+
this.onDocumentKeyDown = (event) => {
|
|
84
|
+
if (!this.state.expanded || event.key !== "Escape") return;
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
this.state.expanded = false;
|
|
87
|
+
};
|
|
88
|
+
document.addEventListener("mousedown", this.onDocumentMouseDown);
|
|
89
|
+
document.addEventListener("keydown", this.onDocumentKeyDown, true);
|
|
33
90
|
}
|
|
34
91
|
|
|
35
92
|
onDestroy() {
|
|
36
93
|
this.unsubscribe?.();
|
|
94
|
+
document.removeEventListener("mousedown", this.onDocumentMouseDown);
|
|
95
|
+
document.removeEventListener("keydown", this.onDocumentKeyDown, true);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get collapsed() {
|
|
99
|
+
return this.state.items.length > 1 && !this.state.expanded;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
enter() {
|
|
103
|
+
this.state.hovered = true;
|
|
104
|
+
pauseAll();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
leave() {
|
|
108
|
+
this.state.hovered = false;
|
|
109
|
+
resumeAll();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
focusIn(event) {
|
|
113
|
+
const target = event.target;
|
|
114
|
+
this.state.keyboardFocus = target === this.getEl("list") || isFocusVisible(target);
|
|
115
|
+
if (this.collapsed && target.closest('[class*="sds-toaster__item--depth-"]')) {
|
|
116
|
+
this.state.expanded = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
focusOut(event) {
|
|
121
|
+
const stack = this.getEl("stack");
|
|
122
|
+
if (!stack || !stack.contains(event.relatedTarget)) this.state.keyboardFocus = false;
|
|
37
123
|
}
|
|
38
124
|
|
|
39
|
-
|
|
40
|
-
|
|
125
|
+
open(event) {
|
|
126
|
+
if (this.collapsed && !event.target.closest("button")) this.state.expanded = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
openByKey(event) {
|
|
130
|
+
if (this.collapsed && event.key === "Enter" && event.target === this.getEl("list")) {
|
|
131
|
+
this.state.expanded = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
closeAll() { dismissAll(); }
|
|
41
136
|
dismiss(id) { dismissToast(id); }
|
|
42
137
|
}
|
|
43
138
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
139
|
+
$ {
|
|
140
|
+
const { items, expanded, hovered, keyboardFocus } = state;
|
|
141
|
+
const stacked = items.length > 1;
|
|
142
|
+
const collapsed = stacked && !expanded;
|
|
143
|
+
const peek = collapsed ? Math.min(items.length - 1, 2) : 0;
|
|
144
|
+
const showCloseAll = stacked && (hovered || keyboardFocus || expanded);
|
|
145
|
+
const stackClass = ["sds-toaster__stack", peek ? `sds-toaster__stack--peek-${peek}` : ""]
|
|
146
|
+
.filter(Boolean).join(" ");
|
|
147
|
+
const listClass = ["sds-toaster__list", collapsed ? "sds-toaster__list--collapsed" : ""]
|
|
148
|
+
.filter(Boolean).join(" ");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
<if(items.length)>
|
|
152
|
+
<div class="sds-toaster" role="region" aria-label="Notificações">
|
|
153
|
+
<div
|
|
154
|
+
key="stack"
|
|
155
|
+
class=stackClass
|
|
156
|
+
on-mouseenter("enter")
|
|
157
|
+
on-mouseleave("leave")
|
|
158
|
+
on-focusin("focusIn")
|
|
159
|
+
on-focusout("focusOut")
|
|
160
|
+
>
|
|
161
|
+
<if(showCloseAll)>
|
|
162
|
+
<div class="sds-toaster__close-all">
|
|
163
|
+
<sds-button variant="pill" label="Fechar todos" onClick=() => component.closeAll()/>
|
|
164
|
+
</div>
|
|
165
|
+
</if>
|
|
166
|
+
<div
|
|
167
|
+
key="list"
|
|
168
|
+
class=listClass
|
|
169
|
+
role=(stacked ? "group" : null)
|
|
170
|
+
aria-label=(stacked ? `${items.length} notificações` : null)
|
|
171
|
+
aria-expanded=(stacked ? String(expanded) : null)
|
|
172
|
+
tabindex=(stacked ? "0" : null)
|
|
173
|
+
on-click("open")
|
|
174
|
+
on-keydown("openByKey")
|
|
175
|
+
>
|
|
176
|
+
<!-- `key` on each toast, so a dismissal removes the right node rather than shifting
|
|
177
|
+
the list up underneath the animation. -->
|
|
178
|
+
<for|item, index| of=items>
|
|
179
|
+
<div key=`item-${item.id}` class=itemClass(index, collapsed)>
|
|
180
|
+
<sds-toast
|
|
181
|
+
key=item.id
|
|
182
|
+
message=item.message
|
|
183
|
+
feedback=item.feedback
|
|
184
|
+
actionLabel=item.actionLabel
|
|
185
|
+
onAction=item.onAction
|
|
186
|
+
actionDisabled=item.actionDisabled
|
|
187
|
+
actionDisabledTooltip=item.actionDisabledTooltip
|
|
188
|
+
actionDisabledTooltipPosition=item.actionDisabledTooltipPosition
|
|
189
|
+
showDismiss=item.showDismiss
|
|
190
|
+
leaving=item.leaving
|
|
191
|
+
onDismiss=() => component.dismiss(item.id)
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
</for>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
69
197
|
</div>
|
|
70
198
|
</if>
|
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
[data-color-scheme="lya"] .sds-uploader--dragging
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
/* The strip that dims a preview is black at 60%, in both themes, built from --color-black
|
|
10
|
+
the way --color-scrim is. Not the scrim itself: that is 70%, a modal's veil over a whole
|
|
11
|
+
page, and every overlay that covers the page reads it — Rafael set the strip lighter, the
|
|
12
|
+
same value KDLS uses. The crop editor's mask below is a veil, and keeps the scrim.
|
|
13
|
+
|
|
14
|
+
Its three buttons are sds-icon-button variant="inverse": the white glyph and the hover
|
|
15
|
+
and press tints come from the button itself, never from rules here. The port carried
|
|
16
|
+
three such rules aimed at --neutral and --24, classes these buttons never had, so none
|
|
17
|
+
applied and the glyphs came out in the primary slot. *//* ============================================================
|
|
14
18
|
SDS Uploader
|
|
15
19
|
============================================================ */
|
|
16
20
|
|
|
@@ -92,16 +96,7 @@
|
|
|
92
96
|
justify-content: flex-end;
|
|
93
97
|
gap: var(--spacing-1);
|
|
94
98
|
padding: var(--spacing-2) var(--spacing-3);
|
|
95
|
-
background: var(--color-
|
|
96
|
-
}
|
|
97
|
-
.sds-uploader__overlay .sds-icon-button--neutral .sds-icon-button__icon {
|
|
98
|
-
color: var(--color-white);
|
|
99
|
-
}
|
|
100
|
-
.sds-uploader__overlay .sds-icon-button--24:hover:not(:disabled)::before {
|
|
101
|
-
background: var(--color-opacity-inverse-10);
|
|
102
|
-
}
|
|
103
|
-
.sds-uploader__overlay .sds-icon-button--24:active:not(:disabled)::before {
|
|
104
|
-
background: var(--color-opacity-inverse-20);
|
|
99
|
+
background: color-mix(in srgb, var(--color-black) 60%, transparent);
|
|
105
100
|
}
|
|
106
101
|
/* ── Edit modal body ── */
|
|
107
102
|
.sds-uploader__edit-body {
|
|
@@ -167,15 +167,15 @@ $ {
|
|
|
167
167
|
<div class=`sds-uploader sds-uploader--filled ${extraClass}`.trim()>
|
|
168
168
|
<img src=value alt=previewAlt class="sds-uploader__preview"/>
|
|
169
169
|
<div class="sds-uploader__overlay">
|
|
170
|
-
<sds-icon-button aria-label=removeLabel onClick=() => component.remove()>
|
|
170
|
+
<sds-icon-button variant="inverse" aria-label=removeLabel onClick=() => component.remove()>
|
|
171
171
|
<@icon><${deleteIcon}/></@icon>
|
|
172
172
|
</sds-icon-button>
|
|
173
173
|
<if(onDownload)>
|
|
174
|
-
<sds-icon-button aria-label=downloadLabel onClick=onDownload>
|
|
174
|
+
<sds-icon-button variant="inverse" aria-label=downloadLabel onClick=onDownload>
|
|
175
175
|
<@icon><${downloadIcon}/></@icon>
|
|
176
176
|
</sds-icon-button>
|
|
177
177
|
</if>
|
|
178
|
-
<sds-icon-button aria-label=editLabel onClick=() => component.openEditor()>
|
|
178
|
+
<sds-icon-button variant="inverse" aria-label=editLabel onClick=() => component.openEditor()>
|
|
179
179
|
<@icon><${cropIcon}/></@icon>
|
|
180
180
|
</sds-icon-button>
|
|
181
181
|
</div>
|
package/foundations/tokens.css
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
- the Kenlo radius scheme named `default` is removed; `soft` is the install
|
|
8
8
|
default and keeps its name.
|
|
9
9
|
SDS owns this file from here on — edit it directly, do not regenerate. */
|
|
10
|
-
/*
|
|
11
|
-
No usage-rule comments here — token usage rules live in
|
|
10
|
+
/* No usage-rule comments here — token usage rules live in
|
|
12
11
|
.claude/tokens-reference.md. General component rules: .claude/component-rules.md. */
|
|
13
12
|
|
|
14
13
|
/* ── The one layout rule this package asserts ──────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenlo/sds-design-system",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "SDS, the Site Design System \u2014 Marko 5 components for Kenlo's public products (Site, Landing Page, Blog)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,11 @@
|
|
|
33
33
|
"test:watch": "jest --watch",
|
|
34
34
|
"gen:icons": "node scripts/gen-icons.mjs",
|
|
35
35
|
"gen:contrast": "node scripts/gen-contrast-tokens.mjs",
|
|
36
|
-
"gen:font": "node scripts/gen-font.mjs"
|
|
36
|
+
"gen:font": "node scripts/gen-font.mjs",
|
|
37
|
+
"check:fonts": "node scripts/check-font-bucket.mjs",
|
|
38
|
+
"parity": "node scripts/parity.mjs",
|
|
39
|
+
"parity:css": "node scripts/css-parity.mjs",
|
|
40
|
+
"parity:defaults": "node scripts/defaults-parity.mjs"
|
|
37
41
|
},
|
|
38
42
|
"peerDependencies": {
|
|
39
43
|
"marko": "^5.38.39"
|