@rak200/ui 0.2.5 → 0.2.7
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 +12 -5
- package/dist/button.d.ts.map +1 -1
- package/dist/button.js +9 -0
- package/dist/button.js.map +1 -1
- package/dist/dialog.d.ts +71 -0
- package/dist/dialog.d.ts.map +1 -0
- package/dist/dialog.js +406 -0
- package/dist/dialog.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +34 -0
- package/dist/tokens.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,13 @@ npm install @rak200/ui
|
|
|
37
37
|
<ui-button>Save</ui-button>
|
|
38
38
|
<ui-button variant="secondary">Cancel</ui-button>
|
|
39
39
|
<ui-button disabled>Unavailable</ui-button>
|
|
40
|
+
|
|
41
|
+
<ui-dialog>
|
|
42
|
+
<h2 slot="title">Delete account</h2>
|
|
43
|
+
<p>This cannot be undone.</p>
|
|
44
|
+
<ui-button slot="actions" variant="secondary">Cancel</ui-button>
|
|
45
|
+
<ui-button slot="actions">Delete</ui-button>
|
|
46
|
+
</ui-dialog>
|
|
40
47
|
```
|
|
41
48
|
|
|
42
49
|
Every visual decision is a CSS custom property, so a host restyles the kit without forking it:
|
|
@@ -64,8 +71,8 @@ components; `docs/` is what describes them.
|
|
|
64
71
|
|
|
65
72
|
## Status
|
|
66
73
|
|
|
67
|
-
**v0.**
|
|
68
|
-
sketched: type-checked at the strictest available
|
|
69
|
-
and **asserted against axe** for WCAG A/AA, 100%
|
|
70
|
-
every public symbol documented. The v0
|
|
71
|
-
|
|
74
|
+
**v0.** Three components — `<ui-button>`, `<ui-field>` and `<ui-dialog>` — and the token layer under
|
|
75
|
+
them, built to the ecosystem's full quality bar rather than sketched: type-checked at the strictest available
|
|
76
|
+
setting, formatted, tested in a real browser and **asserted against axe** for WCAG A/AA, 100%
|
|
77
|
+
coverage and **100% mutation score**, scanned, and every public symbol documented. The v0 surface in
|
|
78
|
+
RFC 0016 grows from here — see [ROADMAP.md](ROADMAP.md).
|
package/dist/button.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../src/button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAGjE,+CAA+C;AAC/C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,CAAC;AAEpD;;;;;;;;;;;;;GAaG;AACH,qBAAa,QAAS,SAAQ,UAAU;IACpC,gBAAyB,MAAM,
|
|
1
|
+
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../src/button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAGjE,+CAA+C;AAC/C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,CAAC;AAEpD;;;;;;;;;;;;;GAaG;AACH,qBAAa,QAAS,SAAQ,UAAU;IACpC,gBAAyB,MAAM,0BA6E7B;IAEF,gBAAyB,UAAU;;;;;;;;;MAGjC;IAEF;;;;;;;;OAQG;IACH,OAAO,EAAE,aAAa,CAAa;IAEnC,kFAAkF;IAClF,QAAQ,UAAS;IAER,MAAM,IAAI,cAAc;CAOpC;AAQD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,WAAW,EAAE,QAAQ,CAAC;KACzB;CACJ"}
|
package/dist/button.js
CHANGED
|
@@ -42,6 +42,15 @@ export class UiButton extends LitElement {
|
|
|
42
42
|
border-radius: ${reference('--ui-radius')};
|
|
43
43
|
padding: ${reference('--ui-space')} calc(${reference('--ui-space')} * 2);
|
|
44
44
|
cursor: pointer;
|
|
45
|
+
/* Measured in WebKit and in Chromium under a phone viewport: the platform
|
|
46
|
+
paints its own wash on tap — 40% black in WebKit, the Android blue in
|
|
47
|
+
Chromium — over whatever the component decided, so the pressed colour below
|
|
48
|
+
arrives underneath it and is not what a finger sees.
|
|
49
|
+
|
|
50
|
+
Removing it is only correct because the pressed state exists. Until it did,
|
|
51
|
+
this wash was the ONLY response a touch got, and turning it off would have
|
|
52
|
+
left a button that answers a finger with nothing. */
|
|
53
|
+
-webkit-tap-highlight-color: transparent;
|
|
45
54
|
/* Only the colour moves. The focus ring is deliberately not in this list:
|
|
46
55
|
delaying the affordance that says *this is where you are* is the opposite of
|
|
47
56
|
what it exists to do. */
|
package/dist/button.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.js","sourceRoot":"","sources":["../src/button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAuB,MAAM,KAAK,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAK3C;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,QAAS,SAAQ,UAAU;IAAxC;;
|
|
1
|
+
{"version":3,"file":"button.js","sourceRoot":"","sources":["../src/button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAuB,MAAM,KAAK,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAK3C;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,QAAS,SAAQ,UAAU;IAAxC;;QAqFI;;;;;;;;WAQG;QACH,YAAO,GAAkB,SAAS,CAAC;QAEnC,kFAAkF;QAClF,aAAQ,GAAG,KAAK,CAAC;IASrB,CAAC;aAzG4B,WAAM,GAAG,GAAG,CAAA;;;;;;;2BAOd,SAAS,CAAC,WAAW,CAAC;;6BAEpB,SAAS,CAAC,aAAa,CAAC;uBAC9B,SAAS,CAAC,YAAY,CAAC,SAAS,SAAS,CAAC,YAAY,CAAC;;;;;;;;;;;;;;2CAcnC,SAAS,CAAC,qBAAqB,CAAC;kBACzD,SAAS,CAAC,mBAAmB,CAAC;;;;;;;;;;;iCAWf,SAAS,CAAC,kBAAkB,CAAC;;;;;0BAKpC,SAAS,CAAC,mBAAmB,CAAC;qBACnC,SAAS,CAAC,4BAA4B,CAAC;;;;0BAIlC,SAAS,CAAC,oBAAoB,CAAC;qBACpC,SAAS,CAAC,iBAAiB,CAAC;;;;;;;;;0BASvB,SAAS,CAAC,yBAAyB,CAAC;;;;0BAIpC,SAAS,CAAC,kBAAkB,CAAC;;;;;;;;;0BAS7B,SAAS,CAAC,2BAA2B,CAAC;;;;;0BAKtC,SAAS,CAAC,oBAAoB,CAAC;;;KAGpD,AA7E8B,CA6E7B;aAEuB,eAAU,GAAG;QAClC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;QACxC,QAAQ,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;KAC7C,AAHkC,CAGjC;IAgBO,MAAM;QACX,OAAO,IAAI,CAAA;4BACS,IAAI,CAAC,OAAO,cAAc,IAAI,CAAC,QAAQ;;;SAG1D,CAAC;IACN,CAAC;;AAGL,kFAAkF;AAClF,wFAAwF;AACxF,yFAAyF;AACzF,4EAA4E;AAC5E,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC"}
|
package/dist/dialog.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LitElement, type TemplateResult } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* A modal dialog.
|
|
4
|
+
*
|
|
5
|
+
* It delegates to a real `<dialog>` and opens it with `showModal()`, which is what makes
|
|
6
|
+
* the top layer, the inert background, the focus trap, <kbd>Esc</kbd> and — the half
|
|
7
|
+
* everyone forgets — **focus returning to whatever opened it** the platform's job rather
|
|
8
|
+
* than this file's. What is written here is the part the platform leaves out: the scroll
|
|
9
|
+
* lock, the accessible name across the shadow boundary, and an exit that can be seen.
|
|
10
|
+
*
|
|
11
|
+
* **This is where RFC 0016 expected Zag to arrive, and it does not.** Zag was adopted for
|
|
12
|
+
* dismissable layers and focus trapping, and its dialog machine implements both over a
|
|
13
|
+
* `<div>` — which means giving up the top layer and taking a JS focus trap in exchange for
|
|
14
|
+
* one the user agent enforces. *The platform owns what the platform is good at* is the
|
|
15
|
+
* older rule and the stronger one here. Zag arrives with the first component the platform
|
|
16
|
+
* gives nothing for; `ARCHITECTURE.md` carries that decision for a consumer, and
|
|
17
|
+
* `ROADMAP.md` names the component.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```html
|
|
21
|
+
* <ui-dialog>
|
|
22
|
+
* <h2 slot="title">Delete account</h2>
|
|
23
|
+
* <p>This cannot be undone.</p>
|
|
24
|
+
* <ui-button slot="actions" variant="secondary">Cancel</ui-button>
|
|
25
|
+
* <ui-button slot="actions">Delete</ui-button>
|
|
26
|
+
* </ui-dialog>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare class UiDialog extends LitElement {
|
|
30
|
+
#private;
|
|
31
|
+
static readonly styles: import("lit").CSSResult;
|
|
32
|
+
static readonly properties: {
|
|
33
|
+
open: {
|
|
34
|
+
type: BooleanConstructor;
|
|
35
|
+
reflect: boolean;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Whether the dialog is showing.
|
|
40
|
+
*
|
|
41
|
+
* **The single source of truth, and the only one.** {@link show} and {@link close} set
|
|
42
|
+
* it, <kbd>Esc</kbd> sets it, and the component reacts to it — so an attribute, a
|
|
43
|
+
* property and a method are three ways of saying one thing rather than three paths
|
|
44
|
+
* that can disagree. Reflected, so a host stylesheet can select on it.
|
|
45
|
+
*
|
|
46
|
+
* A plain field rather than the `accessor` keyword, for the reason `src/button.ts`
|
|
47
|
+
* states beside its own.
|
|
48
|
+
*/
|
|
49
|
+
open: boolean;
|
|
50
|
+
constructor();
|
|
51
|
+
connectedCallback(): void;
|
|
52
|
+
disconnectedCallback(): void;
|
|
53
|
+
firstUpdated(): void;
|
|
54
|
+
updated(): void;
|
|
55
|
+
render(): TemplateResult;
|
|
56
|
+
/**
|
|
57
|
+
* Opens the dialog.
|
|
58
|
+
*
|
|
59
|
+
* Sugar over `open = true` rather than a second way in — a method that opened by its
|
|
60
|
+
* own route would be a state the attribute does not describe.
|
|
61
|
+
*/
|
|
62
|
+
show(): void;
|
|
63
|
+
/** Closes the dialog, running the exit. Sugar over `open = false`, for the same reason. */
|
|
64
|
+
close(): void;
|
|
65
|
+
}
|
|
66
|
+
declare global {
|
|
67
|
+
interface HTMLElementTagNameMap {
|
|
68
|
+
'ui-dialog': UiDialog;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.d.ts","sourceRoot":"","sources":["../src/dialog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAoGjE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,QAAS,SAAQ,UAAU;;IACpC,gBAAyB,MAAM,0BAwG7B;IAEF,gBAAyB,UAAU;;;;;MAEjC;IAEF;;;;;;;;;;OAUG;IACH,IAAI,UAAS;;IAyCJ,iBAAiB,IAAI,IAAI;IASzB,oBAAoB,IAAI,IAAI;IAU5B,YAAY,IAAI,IAAI;IAIpB,OAAO,IAAI,IAAI;IAiBf,MAAM,IAAI,cAAc;IAIjC;;;;;OAKG;IACH,IAAI,IAAI,IAAI;IAIZ,2FAA2F;IAC3F,KAAK,IAAI,IAAI;CA0FhB;AAQD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,WAAW,EAAE,QAAQ,CAAC;KACzB;CACJ"}
|
package/dist/dialog.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { LitElement, css, html } from 'lit';
|
|
2
|
+
import { reference } from './reference.js';
|
|
3
|
+
/** The class the exit is carried by, on the dialog for exactly as long as the exit runs. */
|
|
4
|
+
const closing = 'closing';
|
|
5
|
+
/** How many dialogs are holding the page still. */
|
|
6
|
+
let locks = 0;
|
|
7
|
+
/**
|
|
8
|
+
* The inline values the first hold displaced, given back by the last release.
|
|
9
|
+
*
|
|
10
|
+
* No initial value, deliberately: nothing reads it before {@link hold} writes it, and an
|
|
11
|
+
* initialiser here would be a value no test could ever observe.
|
|
12
|
+
*/
|
|
13
|
+
let displaced;
|
|
14
|
+
/**
|
|
15
|
+
* Holds the page still behind a modal, and gives it back when the last one leaves.
|
|
16
|
+
*
|
|
17
|
+
* `<dialog>` gives a modal the top layer, an inert background, a focus trap and focus
|
|
18
|
+
* restoration. It does not stop the page behind it scrolling, and that is the one piece of
|
|
19
|
+
* modality left to write.
|
|
20
|
+
*
|
|
21
|
+
* **The gutter is reserved rather than the scrollbar measured**, which is the difference
|
|
22
|
+
* between a rule and an arithmetic nothing can check. `overflow: hidden` takes the
|
|
23
|
+
* scrollbar away and the page jumps left by its width; the usual answer adds that width
|
|
24
|
+
* back as padding — and the browser this suite runs in has overlay scrollbars, so the
|
|
25
|
+
* width is `0`, every sign error in that arithmetic passes, and the check is theatre.
|
|
26
|
+
* `scrollbar-gutter: stable` asks the browser to keep the space it was already using,
|
|
27
|
+
* with no number to get wrong.
|
|
28
|
+
*
|
|
29
|
+
* Counted rather than flagged, because a dialog opening a dialog is ordinary — a confirm
|
|
30
|
+
* over a form — and the second one closing must not give the page back while the first is
|
|
31
|
+
* still up.
|
|
32
|
+
*/
|
|
33
|
+
function hold() {
|
|
34
|
+
locks += 1;
|
|
35
|
+
if (locks > 1) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const root = document.documentElement;
|
|
39
|
+
displaced = {
|
|
40
|
+
overflow: root.style.overflow,
|
|
41
|
+
gutter: root.style.getPropertyValue('scrollbar-gutter'),
|
|
42
|
+
};
|
|
43
|
+
// Only where there was a scrollbar to lose. `stable` on a page that never had one
|
|
44
|
+
// reserves space that was not there, which is the same layout shift in the other
|
|
45
|
+
// direction.
|
|
46
|
+
if (root.scrollHeight > root.clientHeight) {
|
|
47
|
+
root.style.setProperty('scrollbar-gutter', 'stable');
|
|
48
|
+
}
|
|
49
|
+
root.style.overflow = 'hidden';
|
|
50
|
+
}
|
|
51
|
+
/** Gives the page back, once the last dialog has closed. */
|
|
52
|
+
function release() {
|
|
53
|
+
locks -= 1;
|
|
54
|
+
if (locks > 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const root = document.documentElement;
|
|
58
|
+
// What the host had, not what this module would have chosen. A page that sets its own
|
|
59
|
+
// `overflow` or reserves its own gutter gets it back rather than being cleared.
|
|
60
|
+
root.style.overflow = displaced.overflow;
|
|
61
|
+
root.style.setProperty('scrollbar-gutter', displaced.gutter);
|
|
62
|
+
}
|
|
63
|
+
/** One of the dialog's slots — named, or the default one when the name is empty. */
|
|
64
|
+
function slot(name = '') {
|
|
65
|
+
return Object.assign(document.createElement('slot'), { name });
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The shadow content, which is static all the way down: a `<dialog>` and three slots.
|
|
69
|
+
*
|
|
70
|
+
* Built here rather than queried out of the render root, and the reason is the floors
|
|
71
|
+
* this repository holds. `renderRoot.querySelector('dialog')` is typed `| null`, and that
|
|
72
|
+
* null is unreachable in a component that renders the element unconditionally — an
|
|
73
|
+
* unreachable branch is one no test can cover and no mutant can be killed on, against a
|
|
74
|
+
* coverage floor of 100 and a mutation floor of 100. The honest way to satisfy both is to
|
|
75
|
+
* not create the branch.
|
|
76
|
+
*/
|
|
77
|
+
function build() {
|
|
78
|
+
const dialog = document.createElement('dialog');
|
|
79
|
+
dialog.setAttribute('part', 'dialog');
|
|
80
|
+
dialog.append(slot('title'), slot(), slot('actions'));
|
|
81
|
+
return dialog;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A modal dialog.
|
|
85
|
+
*
|
|
86
|
+
* It delegates to a real `<dialog>` and opens it with `showModal()`, which is what makes
|
|
87
|
+
* the top layer, the inert background, the focus trap, <kbd>Esc</kbd> and — the half
|
|
88
|
+
* everyone forgets — **focus returning to whatever opened it** the platform's job rather
|
|
89
|
+
* than this file's. What is written here is the part the platform leaves out: the scroll
|
|
90
|
+
* lock, the accessible name across the shadow boundary, and an exit that can be seen.
|
|
91
|
+
*
|
|
92
|
+
* **This is where RFC 0016 expected Zag to arrive, and it does not.** Zag was adopted for
|
|
93
|
+
* dismissable layers and focus trapping, and its dialog machine implements both over a
|
|
94
|
+
* `<div>` — which means giving up the top layer and taking a JS focus trap in exchange for
|
|
95
|
+
* one the user agent enforces. *The platform owns what the platform is good at* is the
|
|
96
|
+
* older rule and the stronger one here. Zag arrives with the first component the platform
|
|
97
|
+
* gives nothing for; `ARCHITECTURE.md` carries that decision for a consumer, and
|
|
98
|
+
* `ROADMAP.md` names the component.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```html
|
|
102
|
+
* <ui-dialog>
|
|
103
|
+
* <h2 slot="title">Delete account</h2>
|
|
104
|
+
* <p>This cannot be undone.</p>
|
|
105
|
+
* <ui-button slot="actions" variant="secondary">Cancel</ui-button>
|
|
106
|
+
* <ui-button slot="actions">Delete</ui-button>
|
|
107
|
+
* </ui-dialog>
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export class UiDialog extends LitElement {
|
|
111
|
+
static { this.styles = css `
|
|
112
|
+
/* The dialog is promoted to the top layer, so the host must take no space of its
|
|
113
|
+
own: a <ui-dialog> laid out in the flow would leave a gap on every page where
|
|
114
|
+
the dialog is closed. */
|
|
115
|
+
:host {
|
|
116
|
+
display: contents;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* display is named under [open] and nowhere else, and that is not a style
|
|
120
|
+
choice. The user agent closes a dialog with display: none; a rule here that
|
|
121
|
+
names display unconditionally outranks it, and the closed dialog simply stays
|
|
122
|
+
on the page. The attribute is still there through the exit below, which is what
|
|
123
|
+
lets the transition run at all. */
|
|
124
|
+
dialog[open] {
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-direction: column;
|
|
127
|
+
gap: calc(${reference('--ui-space')} * 2);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
dialog {
|
|
131
|
+
/* The user agent's own 2px border and white background are decisions this
|
|
132
|
+
component owns. Nothing is raised underneath: elevation arrives with the
|
|
133
|
+
component that needs to read as lifted, and a panel over a scrim is already
|
|
134
|
+
separated from the page by the scrim. */
|
|
135
|
+
border: none;
|
|
136
|
+
border-radius: ${reference('--ui-radius')};
|
|
137
|
+
padding: calc(${reference('--ui-space')} * 3);
|
|
138
|
+
background: ${reference('--ui-color-surface')};
|
|
139
|
+
color: ${reference('--ui-color-text')};
|
|
140
|
+
font-family: ${reference('--ui-font')};
|
|
141
|
+
|
|
142
|
+
/* A cap is a layout decision rather than a visual one, so it is not a token
|
|
143
|
+
and opens no category. A host that wants another size reaches
|
|
144
|
+
::part(dialog), which is the surface for what the token set does not name.
|
|
145
|
+
|
|
146
|
+
border-box, and the mutation floor is what found this: a dialog is
|
|
147
|
+
content-box by default, so the cap applied to the content alone and the
|
|
148
|
+
padding was added on top of it — the panel then ran wider than the cap by
|
|
149
|
+
six spacing steps, and the clearance the calc promises was not the
|
|
150
|
+
clearance it left. */
|
|
151
|
+
box-sizing: border-box;
|
|
152
|
+
max-inline-size: min(32rem, calc(100% - ${reference('--ui-space')} * 8));
|
|
153
|
+
|
|
154
|
+
opacity: 1;
|
|
155
|
+
transition: opacity ${reference('--ui-duration-state')}
|
|
156
|
+
${reference('--ui-easing-enter')};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Where the entrance comes *from*. Without it there is nothing to transition out
|
|
160
|
+
of: the dialog goes from display: none to laid out in a single frame, and a
|
|
161
|
+
transition needs a before-change style that an unrendered element does not
|
|
162
|
+
have. */
|
|
163
|
+
@starting-style {
|
|
164
|
+
dialog[open] {
|
|
165
|
+
opacity: 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
dialog::backdrop {
|
|
170
|
+
background: ${reference('--ui-color-scrim')};
|
|
171
|
+
opacity: 1;
|
|
172
|
+
transition: opacity ${reference('--ui-duration-state')}
|
|
173
|
+
${reference('--ui-easing-enter')};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@starting-style {
|
|
177
|
+
dialog[open]::backdrop {
|
|
178
|
+
opacity: 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* The exit, driven from the class rather than from [open] going away: the
|
|
183
|
+
platform removes a closed dialog in the same frame it is closed, so an exit
|
|
184
|
+
waiting on the attribute would never be seen. Both halves are declared together
|
|
185
|
+
because moving together is what lets #exit await one of them. */
|
|
186
|
+
dialog.closing,
|
|
187
|
+
dialog.closing::backdrop {
|
|
188
|
+
opacity: 0;
|
|
189
|
+
transition-timing-function: ${reference('--ui-easing-exit')};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* showModal() focuses the dialog itself when nothing inside it can take focus,
|
|
193
|
+
and a focused thing with no ring is the most common way a component stops being
|
|
194
|
+
usable by keyboard. */
|
|
195
|
+
dialog:focus-visible {
|
|
196
|
+
outline: 2px solid ${reference('--ui-color-focus')};
|
|
197
|
+
outline-offset: 2px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* A heading's own margin would add to the padding and to the gap above it. This
|
|
201
|
+
resets the user agent's default and yields to a decision: for slotted content
|
|
202
|
+
the outer tree wins, so a host's own rule for the element still beats this. */
|
|
203
|
+
slot[name='title']::slotted(*) {
|
|
204
|
+
margin: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* A slot is display: contents until something says otherwise, which is what
|
|
208
|
+
makes this the line that turns three buttons into a row. */
|
|
209
|
+
slot[name='actions'] {
|
|
210
|
+
display: flex;
|
|
211
|
+
flex-wrap: wrap;
|
|
212
|
+
justify-content: flex-end;
|
|
213
|
+
gap: ${reference('--ui-space')};
|
|
214
|
+
}
|
|
215
|
+
`; }
|
|
216
|
+
static { this.properties = {
|
|
217
|
+
open: { type: Boolean, reflect: true },
|
|
218
|
+
}; }
|
|
219
|
+
/** The real `<dialog>` this component is a shell around. */
|
|
220
|
+
#dialog;
|
|
221
|
+
/**
|
|
222
|
+
* Watches the light DOM for a title that changed without the element changing.
|
|
223
|
+
*
|
|
224
|
+
* Built here rather than on connection so it is never absent: a disconnect that has to
|
|
225
|
+
* ask whether the observer exists is a branch no test can reach.
|
|
226
|
+
*/
|
|
227
|
+
#observer;
|
|
228
|
+
constructor() {
|
|
229
|
+
super();
|
|
230
|
+
/**
|
|
231
|
+
* Whether the dialog is showing.
|
|
232
|
+
*
|
|
233
|
+
* **The single source of truth, and the only one.** {@link show} and {@link close} set
|
|
234
|
+
* it, <kbd>Esc</kbd> sets it, and the component reacts to it — so an attribute, a
|
|
235
|
+
* property and a method are three ways of saying one thing rather than three paths
|
|
236
|
+
* that can disagree. Reflected, so a host stylesheet can select on it.
|
|
237
|
+
*
|
|
238
|
+
* A plain field rather than the `accessor` keyword, for the reason `src/button.ts`
|
|
239
|
+
* states beside its own.
|
|
240
|
+
*/
|
|
241
|
+
this.open = false;
|
|
242
|
+
/** The real `<dialog>` this component is a shell around. */
|
|
243
|
+
this.#dialog = build();
|
|
244
|
+
/**
|
|
245
|
+
* Watches the light DOM for a title that changed without the element changing.
|
|
246
|
+
*
|
|
247
|
+
* Built here rather than on connection so it is never absent: a disconnect that has to
|
|
248
|
+
* ask whether the observer exists is a branch no test can reach.
|
|
249
|
+
*/
|
|
250
|
+
this.#observer = new MutationObserver(() => {
|
|
251
|
+
this.#label();
|
|
252
|
+
});
|
|
253
|
+
this.#dialog.addEventListener('cancel', (event) => {
|
|
254
|
+
// Esc reaches a modal as `cancel`, and letting it through closes in the same
|
|
255
|
+
// frame — the exit this component just gained would never be seen. Prevented
|
|
256
|
+
// and routed back through `open`, so however a close was asked for it leaves
|
|
257
|
+
// by the one door.
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
this.open = false;
|
|
260
|
+
});
|
|
261
|
+
this.#dialog.addEventListener('close', () => {
|
|
262
|
+
// The announcement only. The platform fires `close` as a *queued task* rather
|
|
263
|
+
// than synchronously, so this runs a turn after the dialog actually closed —
|
|
264
|
+
// which is fine for telling a host, and was wrong for giving the page back:
|
|
265
|
+
// the lock outlived the dialog by a task, and a second dialog opening inside
|
|
266
|
+
// that window counted against a page nobody was holding any more. Measured on
|
|
267
|
+
// Node 22 in CI and not on Node 24, which is what a timing dependency looks
|
|
268
|
+
// like from the outside. {@link UiDialog.#shut} owns the release instead, at
|
|
269
|
+
// both of the synchronous points where this element closes the dialog.
|
|
270
|
+
this.open = false;
|
|
271
|
+
this.dispatchEvent(new Event('ui-close', { bubbles: true, composed: true }));
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
connectedCallback() {
|
|
275
|
+
super.connectedCallback();
|
|
276
|
+
// The title may be replaced, or its text rewritten in place without any element
|
|
277
|
+
// changing, which is what a host's re-render looks like from here — so
|
|
278
|
+
// characterData matters as much as childList, and both can happen below a wrapper.
|
|
279
|
+
this.#observer.observe(this, { childList: true, characterData: true, subtree: true });
|
|
280
|
+
}
|
|
281
|
+
disconnectedCallback() {
|
|
282
|
+
this.#observer.disconnect();
|
|
283
|
+
// Removing an open dialog takes it out of the top layer and announces nothing, so
|
|
284
|
+
// the page would stay held by a dialog that is no longer anywhere.
|
|
285
|
+
this.#shut();
|
|
286
|
+
super.disconnectedCallback();
|
|
287
|
+
}
|
|
288
|
+
firstUpdated() {
|
|
289
|
+
this.#label();
|
|
290
|
+
}
|
|
291
|
+
updated() {
|
|
292
|
+
// Compared against the platform's own state rather than against Lit's changed
|
|
293
|
+
// properties, and it has to be: on the first update Lit reports every initialised
|
|
294
|
+
// property as changed, so a dialog that has never been open would run the closing
|
|
295
|
+
// path and announce a close that never happened. It is also what makes a re-render
|
|
296
|
+
// for any other reason a no-op instead of a second `showModal()`, which throws.
|
|
297
|
+
if (this.open === this.#dialog.open) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (this.open) {
|
|
301
|
+
this.#enter();
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
void this.#exit();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
render() {
|
|
308
|
+
return html `${this.#dialog}`;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Opens the dialog.
|
|
312
|
+
*
|
|
313
|
+
* Sugar over `open = true` rather than a second way in — a method that opened by its
|
|
314
|
+
* own route would be a state the attribute does not describe.
|
|
315
|
+
*/
|
|
316
|
+
show() {
|
|
317
|
+
this.open = true;
|
|
318
|
+
}
|
|
319
|
+
/** Closes the dialog, running the exit. Sugar over `open = false`, for the same reason. */
|
|
320
|
+
close() {
|
|
321
|
+
this.open = false;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Puts the dialog in the top layer.
|
|
325
|
+
*
|
|
326
|
+
* `showModal()` rather than the `open` attribute, and the distance between the two is
|
|
327
|
+
* the whole component: the attribute opens a dialog *in the flow*, with no top layer,
|
|
328
|
+
* no inert background, no focus trap and no focus restoration. It is the one way to
|
|
329
|
+
* get something that looks modal and traps nobody — which is why this host's `open` is
|
|
330
|
+
* never bound onto the inner element's.
|
|
331
|
+
*/
|
|
332
|
+
#enter() {
|
|
333
|
+
this.#dialog.showModal();
|
|
334
|
+
hold();
|
|
335
|
+
}
|
|
336
|
+
/** Runs the exit, and closes once it has finished. */
|
|
337
|
+
async #exit() {
|
|
338
|
+
const dialog = this.#dialog;
|
|
339
|
+
dialog.classList.add(closing);
|
|
340
|
+
// `finished` rejects when an animation is cancelled, which is exactly what
|
|
341
|
+
// reopening mid-exit does — so what this waits for is settlement rather than
|
|
342
|
+
// fulfilment. `getAnimations` flushes the pending style change before it answers,
|
|
343
|
+
// so the transition the line above started is already in the list.
|
|
344
|
+
//
|
|
345
|
+
// The backdrop is not in that list: a pseudo-element needs `subtree: true`,
|
|
346
|
+
// measured. It does not need to be — the panel and the scrim move the same
|
|
347
|
+
// property over the same duration token, so they start together and end together,
|
|
348
|
+
// and awaiting one is awaiting both. `tests/dialog.test.ts` asserts that shared
|
|
349
|
+
// duration rather than leaving it to this paragraph.
|
|
350
|
+
await Promise.allSettled(dialog.getAnimations().map((animation) => animation.finished));
|
|
351
|
+
dialog.classList.remove(closing);
|
|
352
|
+
// Reopened while the exit was running. The dialog never left the top layer, so
|
|
353
|
+
// there is nothing to close, and taking the class back off is the whole of the
|
|
354
|
+
// way back.
|
|
355
|
+
if (this.open) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
this.#shut();
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Closes the dialog and gives the page back, in that order and in one place.
|
|
362
|
+
*
|
|
363
|
+
* Both ways out run through here — the exit above and the disconnect below — so the
|
|
364
|
+
* release is paired with the close rather than with the event the close will later
|
|
365
|
+
* fire. The guard is the platform's own state rather than a flag this element keeps:
|
|
366
|
+
* a dialog that is not open is holding nothing, so calling this twice gives the page
|
|
367
|
+
* back once.
|
|
368
|
+
*/
|
|
369
|
+
#shut() {
|
|
370
|
+
if (!this.#dialog.open) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
this.#dialog.close();
|
|
374
|
+
release();
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Names the dialog after its own title.
|
|
378
|
+
*
|
|
379
|
+
* **A copy of the text rather than a reference to the element, and that is forced.**
|
|
380
|
+
* APG names a modal with `aria-labelledby` pointing at its heading, and an IDREF does
|
|
381
|
+
* not cross a shadow boundary — so a `<dialog>` in this shadow root cannot point at a
|
|
382
|
+
* heading in the host's tree. `ui-field` answers the same constraint by leaving every
|
|
383
|
+
* associated element in the light DOM; a dialog cannot, because the element that has
|
|
384
|
+
* to reach the top layer is the one this component renders. What does cross is a
|
|
385
|
+
* string, so the name is copied and the observer above keeps the copy honest.
|
|
386
|
+
*/
|
|
387
|
+
#label() {
|
|
388
|
+
const title = this.querySelector(':scope > [slot="title"]');
|
|
389
|
+
const label = title?.textContent.trim() ?? '';
|
|
390
|
+
// A dialog with no title has no accessible name, and a blank `aria-label` would be
|
|
391
|
+
// worse than none: a screen reader announces "dialog" either way, and an attribute
|
|
392
|
+
// that is present and empty is the shape that makes an audit read as handled.
|
|
393
|
+
if (label === '') {
|
|
394
|
+
this.#dialog.removeAttribute('aria-label');
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
this.#dialog.setAttribute('aria-label', label);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Stryker disable next-line StringLiteral: the registration runs once, at import, inside
|
|
402
|
+
// the warm process Stryker switches mutants in — so by the time a mutant on this line is
|
|
403
|
+
// active the element is already defined under the original name, and no test can observe
|
|
404
|
+
// the change. Outside the runner's reach, not an equivalent mutant.
|
|
405
|
+
customElements.define('ui-dialog', UiDialog);
|
|
406
|
+
//# sourceMappingURL=dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.js","sourceRoot":"","sources":["../src/dialog.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { UiButton, type ButtonVariant } from './button.js';
|
|
2
|
+
export { UiDialog } from './dialog.js';
|
|
2
3
|
export { UiField } from './field.js';
|
|
3
4
|
export { tokens, derivedTokens, defaults, formulas, darkScheme, tokenStyleSheet, type Token, type DerivedToken, } from './tokens.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EACf,KAAK,KAAK,EACV,KAAK,YAAY,GACpB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EACf,KAAK,KAAK,EACV,KAAK,YAAY,GACpB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAsB,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,GAGlB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAsB,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,GAGlB,MAAM,aAAa,CAAC"}
|
package/dist/tokens.d.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* it does break is code that **enumerates** the set, an exhaustive `Record<Token, string>`
|
|
32
32
|
* above all. The supported shape is the partial map, which is what a theme is anyway.
|
|
33
33
|
*/
|
|
34
|
-
export declare const tokens: readonly ["--ui-color-accent", "--ui-color-accent-contrast", "--ui-color-surface", "--ui-color-text", "--ui-color-focus", "--ui-color-danger", "--ui-radius", "--ui-space", "--ui-font", "--ui-duration-100", "--ui-easing-state"];
|
|
34
|
+
export declare const tokens: readonly ["--ui-color-accent", "--ui-color-accent-contrast", "--ui-color-surface", "--ui-color-text", "--ui-color-focus", "--ui-color-danger", "--ui-color-scrim", "--ui-radius", "--ui-space", "--ui-font", "--ui-duration-100", "--ui-easing-state", "--ui-easing-enter", "--ui-easing-exit"];
|
|
35
35
|
/** A CSS custom property this package defines and gives a default. */
|
|
36
36
|
export type Token = (typeof tokens)[number];
|
|
37
37
|
/**
|
package/dist/tokens.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,MAAM,iSA6CT,CAAC;AAEX,sEAAsE;AACtE,MAAM,MAAM,KAAK,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5C;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,oIAShB,CAAC;AAEX,wEAAwE;AACxE,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAuCpD,CAAC;AAqBF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAY3D,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAW/D,CAAC;AAKF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAsBxC"}
|
package/dist/tokens.js
CHANGED
|
@@ -38,6 +38,19 @@ export const tokens = [
|
|
|
38
38
|
'--ui-color-text',
|
|
39
39
|
'--ui-color-focus',
|
|
40
40
|
'--ui-color-danger',
|
|
41
|
+
// The dim behind a modal, and a *ground* rather than a derivation even though every
|
|
42
|
+
// other neutral here is derived. A derived neutral mixes toward the text, which is
|
|
43
|
+
// what makes one formula right in both schemes — and it is exactly wrong for this
|
|
44
|
+
// one: on a dark page the text is the light pole, so the mix would *lighten* the
|
|
45
|
+
// page behind the dialog instead of dimming it. A scrim dims in both schemes, so it
|
|
46
|
+
// carries a literal and appears in neither `darkScheme` nor `formulas`.
|
|
47
|
+
//
|
|
48
|
+
// **This departs from RFC 0002**, whose survey of the queued components answered
|
|
49
|
+
// *text at an alpha* for this role. That table asks whether a role needs a new hue,
|
|
50
|
+
// and the answer to that question is still no — what it could not weigh, with no
|
|
51
|
+
// overlay yet written to judge against, is which pole the mix should run toward. The
|
|
52
|
+
// proposal says so in the same breath: read as a judgement, not a measurement.
|
|
53
|
+
'--ui-color-scrim',
|
|
41
54
|
'--ui-radius',
|
|
42
55
|
'--ui-space',
|
|
43
56
|
'--ui-font',
|
|
@@ -52,6 +65,17 @@ export const tokens = [
|
|
|
52
65
|
// ground and a component reads it directly.
|
|
53
66
|
'--ui-duration-100',
|
|
54
67
|
'--ui-easing-state',
|
|
68
|
+
// The pair `--ui-easing-state` promised, arriving with the overlay that has an enter
|
|
69
|
+
// and an exit to name — `ui-dialog`. A state change reverses mid-flight and wants a
|
|
70
|
+
// symmetric curve; an overlay does not reverse, it arrives and it leaves, and the two
|
|
71
|
+
// directions are asked to feel different on purpose.
|
|
72
|
+
//
|
|
73
|
+
// No duration steps come with them. The scale is ordinal with gaps precisely so a step
|
|
74
|
+
// can be inserted when something needs one, and the dialog needs no duration the state
|
|
75
|
+
// step does not already give it — inventing `--ui-duration-200` before a component
|
|
76
|
+
// judges it against something is the claim ROADMAP.md declines to make about Zag.
|
|
77
|
+
'--ui-easing-enter',
|
|
78
|
+
'--ui-easing-exit',
|
|
55
79
|
];
|
|
56
80
|
/**
|
|
57
81
|
* The roles computed from the grounds rather than declared beside them.
|
|
@@ -94,6 +118,11 @@ export const defaults = {
|
|
|
94
118
|
// because no axe rule does.
|
|
95
119
|
'--ui-color-focus': '#b45309',
|
|
96
120
|
'--ui-color-danger': '#b91c1c',
|
|
121
|
+
// Half black. Enough to push the page behind a modal out of the reading order for the
|
|
122
|
+
// eye as well as for the accessibility tree, and not so much that the context a modal
|
|
123
|
+
// is *about* stops being visible. The alpha is the whole point, so this is the one
|
|
124
|
+
// default that is not an opaque hex.
|
|
125
|
+
'--ui-color-scrim': 'rgb(0 0 0 / 0.5)',
|
|
97
126
|
'--ui-radius': '0.375rem',
|
|
98
127
|
'--ui-space': '0.5rem',
|
|
99
128
|
'--ui-font': 'system-ui, sans-serif',
|
|
@@ -108,6 +137,11 @@ export const defaults = {
|
|
|
108
137
|
// for. `enter` will want an ease-out and `exit` an ease-in, and they arrive with the
|
|
109
138
|
// overlays that have an enter and an exit to name.
|
|
110
139
|
'--ui-easing-state': 'ease-in-out',
|
|
140
|
+
// Fast out of the gate and settling at the end, which is what makes an arriving
|
|
141
|
+
// overlay feel like it was already on its way. The exit is its mirror: slow to let go
|
|
142
|
+
// and quick to be gone, so a dismissal does not linger over a decision already made.
|
|
143
|
+
'--ui-easing-enter': 'ease-out',
|
|
144
|
+
'--ui-easing-exit': 'ease-in',
|
|
111
145
|
};
|
|
112
146
|
/**
|
|
113
147
|
* A ground as it is written inside a formula: the name, with its own default behind it.
|
package/dist/tokens.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IAClB,mBAAmB;IACnB,4BAA4B;IAC5B,oBAAoB;IACpB,iBAAiB;IACjB,kBAAkB;IAClB,mBAAmB;IACnB,aAAa;IACb,YAAY;IACZ,WAAW;IACX,kFAAkF;IAClF,sFAAsF;IACtF,+CAA+C;IAC/C,EAAE;IACF,uFAAuF;IACvF,sFAAsF;IACtF,uFAAuF;IACvF,oFAAoF;IACpF,4CAA4C;IAC5C,mBAAmB;IACnB,mBAAmB;
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
|