@motion-proto/live-tokens 0.95.0 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/bin/set-colors.mjs +1 -0
- package/bin/setup-claude.mjs +2 -2
- package/dist-plugin/setColors/index.cjs +7 -1
- package/dist-plugin/setColors/index.d.cts +9 -4
- package/dist-plugin/setColors/index.d.ts +9 -4
- package/dist-plugin/setColors/index.js +7 -1
- package/dist-plugin/setGeometry/index.d.cts +1 -1
- package/dist-plugin/setGeometry/index.d.ts +1 -1
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.cts → themeTypes-BaTQUyac.d.cts} +10 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.ts → themeTypes-BaTQUyac.d.ts} +10 -1
- package/package.json +4 -2
- package/src/demo/TestingLoops.svelte +39 -16
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +0 -7
- package/src/editor/core/store/editorPersistence.ts +9 -1
- package/src/editor/core/store/editorRenderer.ts +2 -0
- package/src/editor/core/store/editorStore.ts +25 -0
- package/src/editor/core/store/editorTypes.ts +9 -0
- package/src/editor/core/themes/buildColors.ts +16 -5
- package/src/editor/core/themes/slices/customColors.ts +226 -0
- package/src/editor/core/themes/themeTypes.ts +10 -0
- package/src/editor/docs/content/editing-tokens.md +28 -0
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/pages/EditorShell.svelte +1 -0
- package/src/editor/ui/CustomColorReplaceStep.svelte +118 -0
- package/src/editor/ui/UIPaletteSelector.svelte +338 -15
- package/src/editor/ui/UITokenSelector.svelte +7 -12
- package/src/editor/ui/VariablesTab.svelte +3 -0
- package/src/editor/ui/anchorToTrigger.ts +63 -0
- package/src/editor/ui/sections/CustomColorsSection.svelte +254 -0
- package/src/system/components/ImageLightbox.svelte +17 -3
- package/src/testing-js/page-compliance.contract.js +31 -5
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/editor/ui/keepInViewport.ts +0 -84
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import { get } from 'svelte/store';
|
|
4
|
+
import {
|
|
5
|
+
editorState,
|
|
6
|
+
beginScope,
|
|
7
|
+
commitScope,
|
|
8
|
+
cancelScope,
|
|
9
|
+
beginSliderGesture,
|
|
10
|
+
setCustomColor,
|
|
11
|
+
removeCustomColor,
|
|
12
|
+
replaceAndRemoveCustomColor,
|
|
13
|
+
type Scope,
|
|
14
|
+
} from '../../core/store/editorStore';
|
|
15
|
+
import type { Oklch } from '../../core/palettes/oklch';
|
|
16
|
+
import ColorEditPanel from '../ColorEditPanel.svelte';
|
|
17
|
+
import CustomColorReplaceStep from '../CustomColorReplaceStep.svelte';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
copiedVar?: string | null;
|
|
21
|
+
oncopy?: (variable: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let { copiedVar = null, oncopy }: Props = $props();
|
|
25
|
+
|
|
26
|
+
const PREFIX = '--color-custom-';
|
|
27
|
+
const customNumber = (v: string) => v.slice(PREFIX.length);
|
|
28
|
+
|
|
29
|
+
let tokens = $derived(
|
|
30
|
+
[...$editorState.customColors.tokens]
|
|
31
|
+
.sort((a, b) => Number(customNumber(a.variable)) - Number(customNumber(b.variable))),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/** An open edit of one custom colour, or the replace step of its remove. */
|
|
35
|
+
interface Session {
|
|
36
|
+
mode: 'edit' | 'replace';
|
|
37
|
+
variable: string;
|
|
38
|
+
draft: Oklch;
|
|
39
|
+
}
|
|
40
|
+
let session = $state<Session | null>(null);
|
|
41
|
+
// Held outside $state: the store matches an open scope by identity, and a
|
|
42
|
+
// proxied handle would never match.
|
|
43
|
+
let sessionScope: Scope | null = null;
|
|
44
|
+
|
|
45
|
+
function tokenColor(variable: string): Oklch | undefined {
|
|
46
|
+
return get(editorState).customColors.tokens.find((t) => t.variable === variable)?.color;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function openEdit(variable: string) {
|
|
50
|
+
const color = tokenColor(variable);
|
|
51
|
+
if (!color) return;
|
|
52
|
+
sessionScope = beginScope({ label: `edit custom colour ${variable}`, collapseToOne: true, clipUndoFloor: true });
|
|
53
|
+
session = { mode: 'edit', variable, draft: { ...color } };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function editDraft(h: number, c: number, lPct: number) {
|
|
57
|
+
if (!session) return;
|
|
58
|
+
session.draft = { l: lPct / 100, c, h };
|
|
59
|
+
setCustomColor(session.variable, session.draft);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function closeEdit(commit: boolean) {
|
|
63
|
+
if (sessionScope) {
|
|
64
|
+
if (commit) commitScope(sessionScope);
|
|
65
|
+
else cancelScope(sessionScope);
|
|
66
|
+
}
|
|
67
|
+
sessionScope = null;
|
|
68
|
+
session = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function requestRemove(variable: string) {
|
|
72
|
+
if (removeCustomColor(variable)) return;
|
|
73
|
+
const color = tokenColor(variable);
|
|
74
|
+
if (color) session = { mode: 'replace', variable, draft: { ...color } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function confirmReplace(replacement: string) {
|
|
78
|
+
if (!session) return;
|
|
79
|
+
const { variable } = session;
|
|
80
|
+
session = null;
|
|
81
|
+
replaceAndRemoveCustomColor(variable, replacement);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onDestroy(() => {
|
|
85
|
+
if (sessionScope) cancelScope(sessionScope);
|
|
86
|
+
});
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<section class="section" id="custom-colors">
|
|
90
|
+
<h2 class="section-title">Custom Colours</h2>
|
|
91
|
+
<p class="editor-intro">A custom colour holds its own OKLCH value, and every slot that names it follows its edits. Add one from the Custom row of any colour picker. Removing a colour in use first asks which token takes its place.</p>
|
|
92
|
+
{#if tokens.length === 0}
|
|
93
|
+
<p class="custom-empty">This theme has no custom colours.</p>
|
|
94
|
+
{:else}
|
|
95
|
+
<div class="custom-grid">
|
|
96
|
+
{#each tokens as token (token.variable)}
|
|
97
|
+
{@const active = session?.variable === token.variable}
|
|
98
|
+
<div class={['custom-item', { active }]}>
|
|
99
|
+
<div class="custom-box" style="background: var({token.variable});"></div>
|
|
100
|
+
<div class="token-info">
|
|
101
|
+
<button class={['token-variable copyable', { copied: copiedVar === token.variable }]} onclick={() => oncopy?.(token.variable)}>{copiedVar === token.variable ? 'copied!' : token.variable}</button>
|
|
102
|
+
{#if !active}
|
|
103
|
+
<div class="custom-actions">
|
|
104
|
+
<button
|
|
105
|
+
class="custom-btn"
|
|
106
|
+
aria-label="Edit {token.variable}"
|
|
107
|
+
disabled={session !== null}
|
|
108
|
+
onclick={() => openEdit(token.variable)}
|
|
109
|
+
>Edit</button>
|
|
110
|
+
<button
|
|
111
|
+
class="custom-btn"
|
|
112
|
+
aria-label="Remove {token.variable}"
|
|
113
|
+
disabled={session !== null}
|
|
114
|
+
onclick={() => requestRemove(token.variable)}
|
|
115
|
+
>Remove</button>
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
119
|
+
{#if active && session}
|
|
120
|
+
<div class="custom-editor-host">
|
|
121
|
+
{#if session.mode === 'edit'}
|
|
122
|
+
<ColorEditPanel
|
|
123
|
+
title="Custom › {customNumber(session.variable)}"
|
|
124
|
+
hue={session.draft.h}
|
|
125
|
+
chroma={session.draft.c}
|
|
126
|
+
lightness={session.draft.l * 100}
|
|
127
|
+
onHueChromaChange={editDraft}
|
|
128
|
+
onSliderStart={() => beginSliderGesture(`edit ${session?.variable ?? 'custom colour'}`)}
|
|
129
|
+
onConfirm={() => closeEdit(true)}
|
|
130
|
+
onCancel={() => closeEdit(false)}
|
|
131
|
+
/>
|
|
132
|
+
{:else}
|
|
133
|
+
<CustomColorReplaceStep
|
|
134
|
+
variable={session.variable}
|
|
135
|
+
color={session.draft}
|
|
136
|
+
onconfirm={confirmReplace}
|
|
137
|
+
oncancel={() => (session = null)}
|
|
138
|
+
/>
|
|
139
|
+
{/if}
|
|
140
|
+
</div>
|
|
141
|
+
{/if}
|
|
142
|
+
</div>
|
|
143
|
+
{/each}
|
|
144
|
+
</div>
|
|
145
|
+
{/if}
|
|
146
|
+
</section>
|
|
147
|
+
|
|
148
|
+
<style>
|
|
149
|
+
.section {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: var(--ui-space-24);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.section-title {
|
|
156
|
+
font-size: var(--ui-font-size-2xl);
|
|
157
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
158
|
+
color: var(--ui-text-primary);
|
|
159
|
+
margin: 0;
|
|
160
|
+
padding-bottom: var(--ui-space-8);
|
|
161
|
+
border-bottom: 2px solid var(--ui-border-high);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.editor-intro,
|
|
165
|
+
.custom-empty {
|
|
166
|
+
font-size: var(--ui-font-size-md);
|
|
167
|
+
color: var(--ui-text-tertiary);
|
|
168
|
+
margin: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.token-variable.copyable {
|
|
172
|
+
all: unset;
|
|
173
|
+
font-size: var(--ui-font-size-md);
|
|
174
|
+
color: var(--ui-text-tertiary);
|
|
175
|
+
font-family: var(--ui-font-mono);
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
transition: color var(--ui-transition-fast);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.token-variable.copyable:hover {
|
|
181
|
+
color: var(--ui-text-accent);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.token-variable.copyable.copied {
|
|
185
|
+
color: var(--ui-text-success);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.custom-grid {
|
|
189
|
+
display: grid;
|
|
190
|
+
grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr));
|
|
191
|
+
gap: var(--ui-space-16);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.custom-item {
|
|
195
|
+
display: flex;
|
|
196
|
+
flex-direction: column;
|
|
197
|
+
gap: var(--ui-space-8);
|
|
198
|
+
padding: var(--ui-space-12);
|
|
199
|
+
border: 1px solid var(--ui-border-low);
|
|
200
|
+
border-radius: var(--ui-radius-lg);
|
|
201
|
+
background: var(--ui-surface-lowest);
|
|
202
|
+
min-width: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.custom-item.active {
|
|
206
|
+
grid-column: 1 / -1;
|
|
207
|
+
border-color: var(--ui-text-primary);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.custom-box {
|
|
211
|
+
height: 3rem;
|
|
212
|
+
border-radius: var(--ui-radius-md);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.token-info {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
justify-content: space-between;
|
|
219
|
+
gap: var(--ui-space-8);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.custom-actions {
|
|
223
|
+
display: flex;
|
|
224
|
+
gap: var(--ui-space-4);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.custom-btn {
|
|
228
|
+
padding: var(--ui-space-2) var(--ui-space-10);
|
|
229
|
+
background: var(--ui-surface-low);
|
|
230
|
+
border: 1px solid var(--ui-border-low);
|
|
231
|
+
border-radius: var(--ui-radius-sm);
|
|
232
|
+
color: var(--ui-text-secondary);
|
|
233
|
+
font-size: var(--ui-font-size-xs);
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
font-family: inherit;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.custom-btn:hover:not(:disabled) {
|
|
239
|
+
color: var(--ui-text-primary);
|
|
240
|
+
border-color: var(--ui-border);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.custom-btn:disabled {
|
|
244
|
+
cursor: default;
|
|
245
|
+
opacity: 0.5;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.custom-editor-host {
|
|
249
|
+
max-width: 28rem;
|
|
250
|
+
margin-top: var(--ui-space-8);
|
|
251
|
+
padding-top: var(--ui-space-12);
|
|
252
|
+
border-top: 1px dashed var(--ui-border-low);
|
|
253
|
+
}
|
|
254
|
+
</style>
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
props: {
|
|
13
13
|
thumbAspect:
|
|
14
14
|
'the ratio the thumbnail box holds, when it should not be the image\'s own. The image still opens whole, at its own ratio.',
|
|
15
|
+
thumbSrc:
|
|
16
|
+
'a smaller file for the closed thumbnail, when the full image is far larger than the tile. The lightbox still opens `src`.',
|
|
15
17
|
},
|
|
16
18
|
} satisfies CatalogueEntry;
|
|
17
19
|
</script>
|
|
@@ -39,6 +41,8 @@
|
|
|
39
41
|
maxWidth?: number | string | undefined;
|
|
40
42
|
/** The thumbnail box's ratio, when it differs from the image's own. */
|
|
41
43
|
thumbAspect?: number | undefined;
|
|
44
|
+
/** A smaller file for the closed thumbnail; the modal still opens `src`. */
|
|
45
|
+
thumbSrc?: string | undefined;
|
|
42
46
|
/** Closed-thumbnail object-fit. `cover` crops the thumbnail to fill its box;
|
|
43
47
|
the expanded modal always uses `contain` so the whole image stays visible.
|
|
44
48
|
`cover` only crops when the thumbnail box differs from the image's shape
|
|
@@ -70,7 +74,7 @@
|
|
|
70
74
|
The modal portals to <body>, out of reach of a wrapper around the tile,
|
|
71
75
|
so a page that wants its own scrim has to say so here. */
|
|
72
76
|
scrim?: string | undefined;
|
|
73
|
-
/** When true, shows a bottom toolbar (zoom in/out + percent) and enables wheel/drag zoom inside the open modal. Either way, a click on the unzoomed image closes. */
|
|
77
|
+
/** When true, shows a bottom toolbar (zoom in/out + percent) and enables wheel/drag zoom inside the open modal. Either way, a click on the unzoomed image closes. The percent measures the image against its natural resolution: 100% shows one source pixel per CSS pixel, so a large image opens fitted below 100%. Until the natural size is known it measures against the fitted view. */
|
|
74
78
|
extended?: boolean;
|
|
75
79
|
/** Maximum zoom, as a multiple of the image's natural resolution: `1` = 100%
|
|
76
80
|
of the source's real pixels (1 source px = 1 screen px), `2` = 200%. The
|
|
@@ -106,6 +110,7 @@
|
|
|
106
110
|
images = undefined,
|
|
107
111
|
maxWidth = undefined,
|
|
108
112
|
thumbAspect = undefined,
|
|
113
|
+
thumbSrc = undefined,
|
|
109
114
|
fit = 'contain',
|
|
110
115
|
shadow = 'box',
|
|
111
116
|
surface = undefined,
|
|
@@ -664,7 +669,14 @@
|
|
|
664
669
|
};
|
|
665
670
|
});
|
|
666
671
|
|
|
667
|
-
|
|
672
|
+
// `scale` is relative to the fitted stage; the readout reports the share of
|
|
673
|
+
// the source's real pixels, the unit `maxZoom` takes.
|
|
674
|
+
let percentLabel = $derived.by(() => {
|
|
675
|
+
const nw = naturalWidthOf(current);
|
|
676
|
+
const fitW = viewportTarget().width;
|
|
677
|
+
const ratio = nw && fitW ? (scale * fitW) / nw : scale;
|
|
678
|
+
return `${Math.round(ratio * 100)}%`;
|
|
679
|
+
});
|
|
668
680
|
</script>
|
|
669
681
|
|
|
670
682
|
<div
|
|
@@ -687,7 +699,9 @@
|
|
|
687
699
|
aria-expanded={open}
|
|
688
700
|
onclick={openLightbox}
|
|
689
701
|
>
|
|
690
|
-
|
|
702
|
+
<!-- A thumbnail's pixels understate the full image, so it never feeds the
|
|
703
|
+
measured size behind the tile aspect and the zoom caps. -->
|
|
704
|
+
<img src={thumbSrc ?? cover?.src} alt={cover?.alt} draggable="false" onload={thumbSrc ? undefined : onCoverLoad} />
|
|
691
705
|
</button>
|
|
692
706
|
{#if overlay}
|
|
693
707
|
<!-- Beside the thumb, not in it: the thumb clips its corners. Hidden while
|
|
@@ -311,6 +311,23 @@ function variantPattern(label) {
|
|
|
311
311
|
const keys = [.../* @__PURE__ */ new Set([words.join("-"), ...words])].map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
312
312
|
return `(?:^|-)(?:${keys.join("|")})$`;
|
|
313
313
|
}
|
|
314
|
+
function stateMarkers(contract) {
|
|
315
|
+
if (isInapplicable(contract.states)) return [];
|
|
316
|
+
const markers = [];
|
|
317
|
+
for (const state of contract.states) {
|
|
318
|
+
if (restingState(state.state)) continue;
|
|
319
|
+
const attributes = [];
|
|
320
|
+
for (const [part, attrs] of Object.entries(state.attributes ?? {})) {
|
|
321
|
+
const declared = contract.parts[part];
|
|
322
|
+
if (declared === void 0 || typeof declared !== "string" && declared.portal) continue;
|
|
323
|
+
const selector = part === contract.root ? null : typeof declared === "string" ? declared : declared.selector;
|
|
324
|
+
for (const [name, value] of Object.entries(attrs)) attributes.push({ selector, name, value });
|
|
325
|
+
}
|
|
326
|
+
if (!state.forceClass && !attributes.some((a) => a.value !== null)) continue;
|
|
327
|
+
markers.push({ forceClass: state.forceClass ?? null, attributes });
|
|
328
|
+
}
|
|
329
|
+
return markers;
|
|
330
|
+
}
|
|
314
331
|
function paintChecks(contract, map) {
|
|
315
332
|
const checks = [];
|
|
316
333
|
for (const [part, properties] of Object.entries(map)) {
|
|
@@ -354,7 +371,7 @@ function restingPaintSpec(contract) {
|
|
|
354
371
|
}
|
|
355
372
|
}
|
|
356
373
|
if (entries.length === 0) return null;
|
|
357
|
-
return { id: contract.id, root: rootSelector, entries };
|
|
374
|
+
return { id: contract.id, root: rootSelector, entries, away: stateMarkers(contract) };
|
|
358
375
|
}
|
|
359
376
|
var TEXT_AXES = ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing"];
|
|
360
377
|
var BLOCK_DISPLAYS = ["block", "flow-root", "list-item", "flex", "grid", "table-cell", "table-caption"];
|
|
@@ -523,24 +540,33 @@ var PageHarness = class _PageHarness {
|
|
|
523
540
|
let asserted = 0;
|
|
524
541
|
const root = document.querySelector(container);
|
|
525
542
|
if (!root) return { instances, asserted, failures, uncovered };
|
|
526
|
-
const normalize = (node, css, raw) => {
|
|
543
|
+
const normalize = (node, css, raw, rendered = false) => {
|
|
527
544
|
const probe = document.createElement("div");
|
|
528
|
-
|
|
545
|
+
if (rendered) {
|
|
546
|
+
probe.style.visibility = "hidden";
|
|
547
|
+
probe.style.position = getComputedStyle(node).position;
|
|
548
|
+
} else probe.style.display = "none";
|
|
529
549
|
probe.style.borderStyle = "solid";
|
|
530
550
|
probe.style.outlineStyle = "solid";
|
|
531
551
|
probe.style.fontSize = getComputedStyle(node).fontSize;
|
|
532
552
|
(node.parentElement ?? document.body).appendChild(probe);
|
|
533
553
|
probe.style.setProperty(css.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`), raw);
|
|
534
|
-
const computed = getComputedStyle(probe)[css];
|
|
554
|
+
const computed = (getComputedStyle(probe)[css] ?? "").trim();
|
|
535
555
|
probe.remove();
|
|
536
|
-
return
|
|
556
|
+
return !rendered && computed.endsWith("%") ? normalize(node, css, raw, true) : computed;
|
|
537
557
|
};
|
|
558
|
+
const atRest = (element, markers) => !markers.some((marker) => (marker.forceClass === null || element.classList.contains(marker.forceClass)) && marker.attributes.every(({ selector, name, value }) => {
|
|
559
|
+
const part = selector === null ? element : element.querySelector(selector);
|
|
560
|
+
if (!part) return false;
|
|
561
|
+
return value === null ? !part.hasAttribute(name) : part.getAttribute(name) === value;
|
|
562
|
+
}));
|
|
538
563
|
for (const spec of list) {
|
|
539
564
|
const patterns = spec.entries.map((entry) => entry.pattern === null ? null : new RegExp(entry.pattern));
|
|
540
565
|
const variants = [...new Set(spec.entries.map((entry) => entry.variant).filter((v) => v !== null))];
|
|
541
566
|
for (const element of root.querySelectorAll(spec.root)) {
|
|
542
567
|
if (element.closest(chrome)) continue;
|
|
543
568
|
instances++;
|
|
569
|
+
if (!atRest(element, spec.away)) continue;
|
|
544
570
|
const classes = [...element.classList];
|
|
545
571
|
const line = window.__liveTokensPageLine(element, source);
|
|
546
572
|
let covered = false;
|