@raylang/elements 0.0.0-stage → 0.3.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/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +12 -2
- package/base.css +111 -0
- package/components/accordion/accordion.js +197 -0
- package/components/alert/alert.js +112 -0
- package/components/app-bar/app-bar.js +168 -0
- package/components/app-shell/app-shell.js +198 -0
- package/components/aspect-ratio/aspect-ratio.js +59 -0
- package/components/attachment/attachment.js +333 -0
- package/components/avatar/avatar.js +96 -0
- package/components/badge/badge.js +63 -0
- package/components/blockquote/blockquote.js +128 -0
- package/components/bottom-nav/bottom-nav.js +309 -0
- package/components/breadcrumbs/breadcrumbs.js +175 -0
- package/components/button/button.js +283 -0
- package/components/button-group/button-group.js +72 -0
- package/components/card/card.js +73 -0
- package/components/carousel/carousel.js +435 -0
- package/components/center/center.js +26 -0
- package/components/checkbox/checkbox.js +152 -0
- package/components/checkbox-group/checkbox-group.js +152 -0
- package/components/code/code.js +574 -0
- package/components/color-picker/color-picker.js +775 -0
- package/components/combobox/combobox.js +563 -0
- package/components/command-palette/command-palette.js +510 -0
- package/components/container/container.js +41 -0
- package/components/date-picker/date-picker.js +594 -0
- package/components/dialog/dialog.js +329 -0
- package/components/drawer/drawer.js +139 -0
- package/components/empty-state/empty-state.js +66 -0
- package/components/file-upload/file-upload.js +589 -0
- package/components/grid/grid.js +96 -0
- package/components/group/group.js +40 -0
- package/components/icon/icon.js +58 -0
- package/components/icon-button/icon-button.js +30 -0
- package/components/indicator/indicator.js +112 -0
- package/components/input/input.js +85 -0
- package/components/kbd/kbd.js +60 -0
- package/components/menu/menu.js +296 -0
- package/components/nav-link/nav-link.js +159 -0
- package/components/notification/notification.js +591 -0
- package/components/number-input/number-input.js +241 -0
- package/components/pagination/pagination.js +194 -0
- package/components/password-input/password-input.js +85 -0
- package/components/path-input/path-input.js +137 -0
- package/components/popover/popover.js +85 -0
- package/components/progress/progress.js +109 -0
- package/components/radio/radio.js +253 -0
- package/components/scroll-area/scroll-area.js +158 -0
- package/components/search/search.js +157 -0
- package/components/segmented-control/segmented-control.js +190 -0
- package/components/select/select.js +506 -0
- package/components/separator/separator.js +58 -0
- package/components/sidebar/sidebar.js +252 -0
- package/components/skeleton/skeleton.js +90 -0
- package/components/slider/slider.js +155 -0
- package/components/spinner/spinner.js +39 -0
- package/components/split/split.js +221 -0
- package/components/stack/stack.js +46 -0
- package/components/stat/stat.js +79 -0
- package/components/statusbar/statusbar.js +110 -0
- package/components/stepper/stepper.js +239 -0
- package/components/switch/switch.js +45 -0
- package/components/table/table.js +328 -0
- package/components/tabs/tabs.js +224 -0
- package/components/textarea/textarea.js +105 -0
- package/components/theme-toggle/theme-toggle.js +78 -0
- package/components/timeline/timeline.js +150 -0
- package/components/toggle/toggle.js +130 -0
- package/components/toggle-group/toggle-group.js +216 -0
- package/components/tooltip/tooltip.js +101 -0
- package/core/charts.js +107 -0
- package/core/color.js +110 -0
- package/core/core.js +556 -0
- package/core/dismiss.js +69 -0
- package/core/floating.js +195 -0
- package/core/focus.js +70 -0
- package/core/form.js +235 -0
- package/core/host.js +200 -0
- package/core/hotkey.js +40 -0
- package/core/icons.js +104 -0
- package/core/motion.js +73 -0
- package/core/position.js +91 -0
- package/core/raylang.js +167 -0
- package/core/scroll.js +58 -0
- package/core/styles.js +211 -0
- package/core.js +16 -0
- package/custom-elements.json +18545 -0
- package/index.js +74 -0
- package/package.json +312 -4
- package/ray-elements.d.ts +2570 -0
- package/tokens.css +796 -0
- package/tokens.json +2083 -0
- package/vscode.html-custom-data.json +5499 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// <ray-dialog>: ray-elements's modal (Mantine's Modal, shadcn's Dialog) on the
|
|
2
|
+
// native <dialog>: top layer, the rest of the page inert, focus trapped and
|
|
3
|
+
// restored, Escape to close. Stacks: a dialog opened from another one goes
|
|
4
|
+
// on top of it.
|
|
5
|
+
//
|
|
6
|
+
// `label` is the heading (not `title`, which would show the browser tooltip
|
|
7
|
+
// over the whole dialog). Any slotted element with data-dialog-close closes
|
|
8
|
+
// it, with that attribute's value as returnValue. A <form method="dialog">
|
|
9
|
+
// in the light DOM cannot reach the shadow <dialog>: use data-dialog-close.
|
|
10
|
+
//
|
|
11
|
+
// slot="overlay" renders inside the modal <dialog>, so it is not made inert:
|
|
12
|
+
// <ray-toaster> moves itself there while a dialog is open.
|
|
13
|
+
|
|
14
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
15
|
+
import { focusFirst } from "../../core/focus.js";
|
|
16
|
+
import { getHost, hostCan } from "../../core/host.js";
|
|
17
|
+
import { overlayStyles, applyOverlayBlur } from "../../core/styles.js";
|
|
18
|
+
import { play, playAll } from "../../core/motion.js";
|
|
19
|
+
import "../button/button.js";
|
|
20
|
+
|
|
21
|
+
const styles = css`
|
|
22
|
+
:host { display: contents; --_w: 27.5rem; }
|
|
23
|
+
:host([size="xs"]) { --_w: 20rem; }
|
|
24
|
+
:host([size="sm"]) { --_w: 23.75rem; }
|
|
25
|
+
:host([size="lg"]) { --_w: 38.75rem; }
|
|
26
|
+
:host([size="xl"]) { --_w: 48.75rem; }
|
|
27
|
+
:host([size="full"]) { --_w: 100%; }
|
|
28
|
+
dialog {
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
width: min(var(--_w), calc(100vw - 2rem));
|
|
31
|
+
max-height: calc(100dvh - 4rem);
|
|
32
|
+
padding: 0;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
color: var(--ray-card-foreground);
|
|
35
|
+
background: var(--ray-card);
|
|
36
|
+
border: 1px solid var(--ray-overlay-border);
|
|
37
|
+
background-clip: padding-box;
|
|
38
|
+
border-radius: var(--_radius, var(--ray-radius-lg));
|
|
39
|
+
box-shadow: var(--ray-shadow-xl);
|
|
40
|
+
}
|
|
41
|
+
/* Without height it is as tall as its content; with it, fixed (the body scrolls). */
|
|
42
|
+
:host([height]) dialog { height: min(var(--_h), calc(100dvh - 4rem)); }
|
|
43
|
+
:host([size="full"]) dialog { width: calc(100vw - 2rem); height: calc(100dvh - 2rem); max-height: none; }
|
|
44
|
+
dialog[open] { display: flex; flex-direction: column; animation: ray-dialog-in var(--ray-duration-base) var(--ray-easing-out); }
|
|
45
|
+
dialog::backdrop { animation: ray-fade-in var(--ray-duration-base) var(--ray-easing-out); }
|
|
46
|
+
:host([position="top"]) dialog { margin-top: 5vh; }
|
|
47
|
+
@keyframes ray-dialog-in { from { opacity: 0; transform: translateY(-0.5rem) scale(0.98); } }
|
|
48
|
+
@keyframes ray-fade-in { from { opacity: 0; } }
|
|
49
|
+
.header {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: var(--ray-space-3);
|
|
53
|
+
padding: var(--ray-space-md) var(--ray-space-md) 0;
|
|
54
|
+
}
|
|
55
|
+
.header[data-empty] { display: none; }
|
|
56
|
+
.header:has(.description:not([hidden])) { align-items: flex-start; }
|
|
57
|
+
/* The header slot and the close button, centered on the title's line
|
|
58
|
+
whether or not a description follows it. */
|
|
59
|
+
.actions { display: flex; flex: none; align-items: center; gap: var(--ray-space-3); height: calc(var(--ray-font-size-lg) * var(--ray-line-height-heading)); }
|
|
60
|
+
.heading { display: grid; flex: 1; gap: var(--ray-space-1); min-width: 0; }
|
|
61
|
+
.title { margin: 0; font-size: var(--ray-font-size-lg); font-weight: var(--ray-font-weight-semibold); line-height: var(--ray-line-height-heading); }
|
|
62
|
+
.description { margin: 0; font-size: var(--ray-font-size-sm); color: var(--ray-muted-foreground); }
|
|
63
|
+
.close {
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
width: var(--ray-control-height-xs);
|
|
68
|
+
height: var(--ray-control-height-xs);
|
|
69
|
+
margin-left: auto;
|
|
70
|
+
padding: 0;
|
|
71
|
+
color: var(--ray-muted-foreground);
|
|
72
|
+
background: transparent;
|
|
73
|
+
border: 0;
|
|
74
|
+
border-radius: var(--ray-radius-sm);
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
}
|
|
77
|
+
.close:hover { background: var(--ray-accent); color: var(--ray-foreground); }
|
|
78
|
+
.close svg { width: 1rem; height: 1rem; }
|
|
79
|
+
/* flex-basis auto: Safari collapses a 0% basis under a max-height only. */
|
|
80
|
+
.body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: var(--ray-space-md); }
|
|
81
|
+
.footer { display: flex; justify-content: flex-end; gap: var(--ray-space-2); padding: 0 var(--ray-space-md) var(--ray-space-md); }
|
|
82
|
+
.footer:not([data-filled]) { display: none; }
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
let scrollLocks = 0;
|
|
86
|
+
function lockScroll(on) {
|
|
87
|
+
scrollLocks += on ? 1 : -1;
|
|
88
|
+
document.documentElement.style.overflow = scrollLocks > 0 ? "hidden" : "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class RayDialog extends RayElement {
|
|
92
|
+
static properties = {
|
|
93
|
+
open: { type: Boolean, reflect: true },
|
|
94
|
+
label: { type: String },
|
|
95
|
+
size: { type: String, reflect: true, default: "md" },
|
|
96
|
+
position: { type: String, reflect: true, default: "center" },
|
|
97
|
+
persistent: { type: Boolean, reflect: true },
|
|
98
|
+
hideClose: { type: Boolean, reflect: true },
|
|
99
|
+
overlayBlur: { type: String },
|
|
100
|
+
description: { type: String },
|
|
101
|
+
height: { type: String, reflect: true },
|
|
102
|
+
radius: { type: String, reflect: true },
|
|
103
|
+
};
|
|
104
|
+
static styles = [overlayStyles, styles];
|
|
105
|
+
static template = `
|
|
106
|
+
<dialog part="dialog" aria-labelledby="title">
|
|
107
|
+
<header class="header" part="header">
|
|
108
|
+
<div class="heading"><h2 class="title" id="title" part="title"></h2><p class="description" id="description" part="description" hidden></p></div>
|
|
109
|
+
<div class="actions"><slot name="header"></slot><button class="close focus-ring" part="close" type="button" aria-label="Close"><span data-ray-icon="close" aria-hidden="true"></span></button></div>
|
|
110
|
+
</header>
|
|
111
|
+
<div class="body" part="body"><slot></slot></div>
|
|
112
|
+
<footer class="footer" part="footer"><slot name="footer"></slot></footer>
|
|
113
|
+
<slot name="overlay"></slot>
|
|
114
|
+
</dialog>`;
|
|
115
|
+
|
|
116
|
+
constructor() {
|
|
117
|
+
super();
|
|
118
|
+
this.returnValue = "";
|
|
119
|
+
this.dialog = this.$("dialog");
|
|
120
|
+
this.dialog.addEventListener("cancel", (e) => {
|
|
121
|
+
// Escape: always handled here (persistent / cancelable request).
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
this._request("escape");
|
|
124
|
+
});
|
|
125
|
+
this.dialog.addEventListener("close", () => this._closed());
|
|
126
|
+
// A non-modal <dialog> fires no `cancel` on Escape: do it here.
|
|
127
|
+
this.dialog.addEventListener("keydown", (e) => {
|
|
128
|
+
if (e.key === "Escape" && !this.dialog.matches(":modal")) {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
this._request("escape");
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
this.dialog.addEventListener("click", (e) => {
|
|
134
|
+
if (e.target === this.dialog && this._outside(e)) this._request("backdrop");
|
|
135
|
+
});
|
|
136
|
+
this.$(".close").addEventListener("click", () => this._request("button"));
|
|
137
|
+
this.addEventListener("click", (e) => {
|
|
138
|
+
const closer = e.target.closest?.("[data-dialog-close]");
|
|
139
|
+
if (closer && this.contains(closer)) this.close(closer.getAttribute("data-dialog-close") ?? "");
|
|
140
|
+
});
|
|
141
|
+
const footer = this.$('slot[name="footer"]');
|
|
142
|
+
footer.addEventListener("slotchange", () => {
|
|
143
|
+
footer.parentElement.toggleAttribute("data-filled", footer.assignedNodes().length > 0);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
show() { this.open = true; }
|
|
148
|
+
|
|
149
|
+
/** Protected hook: modal (backdrop, page inert, scroll locked) or not. */
|
|
150
|
+
isModal() {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Closes with an optional return value (available as `returnValue` and in ray-close). */
|
|
155
|
+
close(returnValue = "") {
|
|
156
|
+
this.returnValue = returnValue;
|
|
157
|
+
this.open = false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
update(changed) {
|
|
161
|
+
if (changed.has("overlayBlur")) applyOverlayBlur(this, this.overlayBlur);
|
|
162
|
+
this.$("#title").textContent = this.label ?? "";
|
|
163
|
+
const description = this.$("#description");
|
|
164
|
+
if (description) {
|
|
165
|
+
description.textContent = this.description ?? "";
|
|
166
|
+
description.hidden = !this.description;
|
|
167
|
+
if (this.description) this.dialog.setAttribute("aria-describedby", "description");
|
|
168
|
+
else this.dialog.removeAttribute("aria-describedby");
|
|
169
|
+
}
|
|
170
|
+
this._dimensions();
|
|
171
|
+
this.$(".close").hidden = Boolean(this.hideClose);
|
|
172
|
+
this.$(".header").toggleAttribute("data-empty", !this.label && this.hideClose && !this.querySelector('[slot="header"]'));
|
|
173
|
+
if (this.label) this.dialog.setAttribute("aria-labelledby", "title");
|
|
174
|
+
else this.dialog.removeAttribute("aria-labelledby");
|
|
175
|
+
if (changed.has("open") || this._first === undefined) {
|
|
176
|
+
this._first = false;
|
|
177
|
+
if (this.open && this._exit) {
|
|
178
|
+
// Reopened while animating out: stay open.
|
|
179
|
+
this._exit.cancel();
|
|
180
|
+
this._exit = null;
|
|
181
|
+
} else if (this.open && !this.dialog.open) {
|
|
182
|
+
this._show();
|
|
183
|
+
} else if (!this.open && this.dialog.open && !this._exit) {
|
|
184
|
+
this._leave();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// size beyond the presets, height and radius: any CSS length. A percentage
|
|
190
|
+
// is of the window (70% wide = 70vw, 70% high = 70dvh), but never smaller
|
|
191
|
+
// than a comfortable minimum (40rem × 30rem), so on a phone it fills it.
|
|
192
|
+
_dimensions() {
|
|
193
|
+
const length = (v, unit, floor) => (/^\d+(\.\d+)?%$/.test(v) ? `max(${v.replace("%", unit)}, ${floor})` : v);
|
|
194
|
+
const custom = this.size && !["xs", "sm", "md", "lg", "xl", "full"].includes(this.size);
|
|
195
|
+
if (custom && this.localName === "ray-dialog") this.style.setProperty("--_w", length(this.size, "vw", "40rem"));
|
|
196
|
+
else if (this.localName === "ray-dialog") this.style.removeProperty("--_w");
|
|
197
|
+
if (this.height) this.style.setProperty("--_h", length(this.height, "dvh", "30rem"));
|
|
198
|
+
else this.style.removeProperty("--_h");
|
|
199
|
+
const r = this.radius;
|
|
200
|
+
if (r) this.style.setProperty("--_radius", ["xs", "sm", "md", "lg", "xl"].includes(r) ? `var(--ray-radius-${r})` : r === "none" ? "0" : r);
|
|
201
|
+
else this.style.removeProperty("--_radius");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Protected hook: the keyframes the dialog leaves with. */
|
|
205
|
+
exitKeyframes() {
|
|
206
|
+
return [
|
|
207
|
+
{ opacity: 1, transform: "none" },
|
|
208
|
+
{ opacity: 0, transform: "translateY(-0.25rem) scale(0.98)" },
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Animate the dialog and its backdrop out, then really close it.
|
|
213
|
+
async _leave() {
|
|
214
|
+
const exit = playAll([
|
|
215
|
+
play(this.dialog, this.exitKeyframes()),
|
|
216
|
+
play(this.dialog, [{ opacity: 1 }, { opacity: 0 }], { pseudoElement: "::backdrop" }),
|
|
217
|
+
]);
|
|
218
|
+
this._exit = exit;
|
|
219
|
+
await exit.finished;
|
|
220
|
+
if (this._exit !== exit) return;
|
|
221
|
+
this._exit = null;
|
|
222
|
+
if (!this.open && this.dialog.open) this.dialog.close(this.returnValue);
|
|
223
|
+
exit.cancel();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_show() {
|
|
227
|
+
this._returnFocus = document.activeElement;
|
|
228
|
+
this.returnValue = "";
|
|
229
|
+
this._locked = this.isModal();
|
|
230
|
+
if (this._locked) {
|
|
231
|
+
this.dialog.showModal();
|
|
232
|
+
lockScroll(true);
|
|
233
|
+
} else {
|
|
234
|
+
this.dialog.show();
|
|
235
|
+
}
|
|
236
|
+
// Focus the content's first focusable (or [autofocus]); else the close
|
|
237
|
+
// button. Content created along with the dialog (confirmDialog) renders
|
|
238
|
+
// its own focusable parts a microtask later: try again then.
|
|
239
|
+
const focusContent = () => focusFirst(this, { lightOnly: true });
|
|
240
|
+
if (!focusContent()) this.$(".close").focus();
|
|
241
|
+
queueMicrotask(() => {
|
|
242
|
+
const inside = this.contains(document.activeElement) && document.activeElement !== this;
|
|
243
|
+
if (this.open && !inside) focusContent();
|
|
244
|
+
});
|
|
245
|
+
this.emit("ray-open");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_closed() {
|
|
249
|
+
if (this._locked) lockScroll(false);
|
|
250
|
+
this._locked = false;
|
|
251
|
+
if (this.open) this.commitProperty("open", false);
|
|
252
|
+
const back = this._returnFocus;
|
|
253
|
+
this._returnFocus = null;
|
|
254
|
+
if (back && typeof back.focus === "function" && back.isConnected) back.focus({ preventScroll: true });
|
|
255
|
+
this.emit("ray-close", { returnValue: this.returnValue });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_request(source) {
|
|
259
|
+
if (this.persistent && source !== "button") return;
|
|
260
|
+
if (!this.emit("ray-request-close", { source }, { cancelable: true })) return;
|
|
261
|
+
this.close("");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_outside(e) {
|
|
265
|
+
const r = this.dialog.getBoundingClientRect();
|
|
266
|
+
return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
disconnectedCallback() {
|
|
270
|
+
super.disconnectedCallback();
|
|
271
|
+
// Removed while open: no `close` event reaches a disconnected element,
|
|
272
|
+
// so undo here what opening did.
|
|
273
|
+
if (this.dialog.open) this.dialog.close();
|
|
274
|
+
if (this._locked) {
|
|
275
|
+
lockScroll(false);
|
|
276
|
+
this._locked = false;
|
|
277
|
+
}
|
|
278
|
+
const toaster = this.querySelector(":scope > ray-toaster");
|
|
279
|
+
if (toaster) {
|
|
280
|
+
toaster.removeAttribute("slot");
|
|
281
|
+
document.body.append(toaster);
|
|
282
|
+
toaster.raise?.();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
define("ray-dialog", RayDialog);
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* A confirmation dialog. Resolves true when confirmed, false otherwise.
|
|
291
|
+
* @param {{ label?: string, message?: string, confirmLabel?: string, cancelLabel?: string, color?: string }} options
|
|
292
|
+
* @returns {Promise<boolean>}
|
|
293
|
+
*/
|
|
294
|
+
export function confirmDialog({ label = "Are you sure?", message = "", confirmLabel = "Confirm", cancelLabel = "Cancel", color = "", native = true } = {}) {
|
|
295
|
+
// With a native host (core/host.js), the system's modal asks instead; a
|
|
296
|
+
// red confirmation (a destructive action) gets the warning style.
|
|
297
|
+
if (native && hostCan("confirm")) {
|
|
298
|
+
const style = color === "red" ? "warning" : "";
|
|
299
|
+
return getHost().confirm({ title: label, message, confirmLabel, cancelLabel, style });
|
|
300
|
+
}
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
const d = document.createElement("ray-dialog");
|
|
303
|
+
d.label = label;
|
|
304
|
+
d.size = "sm";
|
|
305
|
+
const p = document.createElement("p");
|
|
306
|
+
p.style.margin = "0";
|
|
307
|
+
p.textContent = message;
|
|
308
|
+
const footer = document.createElement("div");
|
|
309
|
+
footer.slot = "footer";
|
|
310
|
+
footer.style.display = "contents";
|
|
311
|
+
const cancel = document.createElement("ray-button");
|
|
312
|
+
cancel.variant = "default";
|
|
313
|
+
cancel.textContent = cancelLabel;
|
|
314
|
+
cancel.setAttribute("data-dialog-close", "cancel");
|
|
315
|
+
const ok = document.createElement("ray-button");
|
|
316
|
+
if (color) ok.color = color;
|
|
317
|
+
ok.textContent = confirmLabel;
|
|
318
|
+
ok.setAttribute("data-dialog-close", "confirm");
|
|
319
|
+
ok.setAttribute("autofocus", "");
|
|
320
|
+
footer.append(cancel, ok);
|
|
321
|
+
d.append(p, footer);
|
|
322
|
+
d.addEventListener("ray-close", (e) => {
|
|
323
|
+
d.remove();
|
|
324
|
+
resolve(e.detail.returnValue === "confirm");
|
|
325
|
+
}, { once: true });
|
|
326
|
+
document.body.append(d);
|
|
327
|
+
d.open = true;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// <ray-drawer>: a panel that slides in from an edge (Mantine's Drawer).
|
|
2
|
+
//
|
|
3
|
+
// It is a RayDialog with another look: the same native <dialog>, focus
|
|
4
|
+
// handling, Escape, persistent, data-dialog-close, returnValue, scroll lock
|
|
5
|
+
// and toasts slot. `modeless` makes it a side panel that leaves the page
|
|
6
|
+
// interactive (no backdrop, no scroll lock; Escape still closes it).
|
|
7
|
+
|
|
8
|
+
import { css, define } from "../../core/core.js";
|
|
9
|
+
import { RayDialog } from "../dialog/dialog.js";
|
|
10
|
+
import { overlayStyles } from "../../core/styles.js";
|
|
11
|
+
|
|
12
|
+
const SIZES = { xs: "20rem", sm: "23.75rem", md: "27.5rem", lg: "38.75rem", xl: "48.75rem", full: "100%" };
|
|
13
|
+
|
|
14
|
+
const styles = css`
|
|
15
|
+
:host { display: contents; --_size: 27.5rem; --_inset: 0px; --_r: 0px; }
|
|
16
|
+
:host([floating]) { --_inset: var(--ray-space-3); --_r: var(--ray-radius-lg); }
|
|
17
|
+
dialog {
|
|
18
|
+
position: fixed;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
max-width: none;
|
|
21
|
+
max-height: none;
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
color: var(--ray-card-foreground);
|
|
26
|
+
background: var(--ray-card);
|
|
27
|
+
border: 0 solid var(--ray-overlay-border);
|
|
28
|
+
background-clip: padding-box;
|
|
29
|
+
border-radius: var(--_r);
|
|
30
|
+
box-shadow: var(--ray-shadow-xl);
|
|
31
|
+
}
|
|
32
|
+
dialog[open] { display: flex; flex-direction: column; }
|
|
33
|
+
dialog::backdrop { animation: ray-fade-in var(--ray-duration-base) var(--ray-easing-out); }
|
|
34
|
+
|
|
35
|
+
:host(:not([position])) dialog, :host([position="right"]) dialog {
|
|
36
|
+
inset: var(--_inset) var(--_inset) var(--_inset) auto;
|
|
37
|
+
width: min(var(--_size), calc(100vw - 2 * var(--_inset)));
|
|
38
|
+
height: calc(100dvh - 2 * var(--_inset));
|
|
39
|
+
border-left-width: 1px;
|
|
40
|
+
--_from: translateX(100%);
|
|
41
|
+
}
|
|
42
|
+
:host([position="left"]) dialog {
|
|
43
|
+
inset: var(--_inset) auto var(--_inset) var(--_inset);
|
|
44
|
+
width: min(var(--_size), calc(100vw - 2 * var(--_inset)));
|
|
45
|
+
height: calc(100dvh - 2 * var(--_inset));
|
|
46
|
+
border-right-width: 1px;
|
|
47
|
+
--_from: translateX(-100%);
|
|
48
|
+
}
|
|
49
|
+
:host([position="top"]) dialog {
|
|
50
|
+
inset: var(--_inset) var(--_inset) auto var(--_inset);
|
|
51
|
+
width: calc(100vw - 2 * var(--_inset));
|
|
52
|
+
height: min(var(--_size), calc(100dvh - 2 * var(--_inset)));
|
|
53
|
+
border-bottom-width: 1px;
|
|
54
|
+
--_from: translateY(-100%);
|
|
55
|
+
}
|
|
56
|
+
:host([position="bottom"]) dialog {
|
|
57
|
+
inset: auto var(--_inset) var(--_inset) var(--_inset);
|
|
58
|
+
width: calc(100vw - 2 * var(--_inset));
|
|
59
|
+
height: min(var(--_size), calc(100dvh - 2 * var(--_inset)));
|
|
60
|
+
border-top-width: 1px;
|
|
61
|
+
--_from: translateY(100%);
|
|
62
|
+
}
|
|
63
|
+
:host([floating]) dialog { border-width: 1px; }
|
|
64
|
+
/* A modeless dialog is not in the top layer: stack it above sticky headers
|
|
65
|
+
and dropdowns (an ancestor with transform/filter would still clip it). */
|
|
66
|
+
dialog:not(:modal) { z-index: var(--ray-z-overlay); }
|
|
67
|
+
dialog[open] { animation: ray-drawer-in var(--ray-duration-slow) var(--ray-easing-out); }
|
|
68
|
+
@keyframes ray-drawer-in { from { transform: var(--_from); } }
|
|
69
|
+
@keyframes ray-fade-in { from { opacity: 0; } }
|
|
70
|
+
|
|
71
|
+
.header { display: flex; align-items: center; gap: var(--ray-space-3); padding: var(--ray-space-md); border-bottom: 1px solid var(--ray-border); }
|
|
72
|
+
.header[data-empty] { display: none; }
|
|
73
|
+
.header:has(.description:not([hidden])) { align-items: flex-start; }
|
|
74
|
+
/* The header slot and the close button, centered on the title's line
|
|
75
|
+
whether or not a description follows it. */
|
|
76
|
+
.actions { display: flex; flex: none; align-items: center; gap: var(--ray-space-3); height: calc(var(--ray-font-size-lg) * var(--ray-line-height-heading)); }
|
|
77
|
+
.heading { display: grid; flex: 1; gap: var(--ray-space-1); min-width: 0; }
|
|
78
|
+
.title { margin: 0; font-size: var(--ray-font-size-lg); font-weight: var(--ray-font-weight-semibold); line-height: var(--ray-line-height-heading); }
|
|
79
|
+
.description { margin: 0; font-size: var(--ray-font-size-sm); color: var(--ray-muted-foreground); }
|
|
80
|
+
.close {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
width: var(--ray-control-height-xs);
|
|
85
|
+
height: var(--ray-control-height-xs);
|
|
86
|
+
margin-left: auto;
|
|
87
|
+
padding: 0;
|
|
88
|
+
color: var(--ray-muted-foreground);
|
|
89
|
+
background: transparent;
|
|
90
|
+
border: 0;
|
|
91
|
+
border-radius: var(--ray-radius-sm);
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
}
|
|
94
|
+
.close:hover { background: var(--ray-accent); color: var(--ray-foreground); }
|
|
95
|
+
.close svg { width: 1rem; height: 1rem; }
|
|
96
|
+
.body { flex: 1; overflow: auto; padding: var(--ray-space-md); }
|
|
97
|
+
.footer { display: flex; justify-content: flex-end; gap: var(--ray-space-2); padding: var(--ray-space-md); border-top: 1px solid var(--ray-border); }
|
|
98
|
+
.footer:not([data-filled]) { display: none; }
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
export class RayDrawer extends RayDialog {
|
|
102
|
+
static properties = {
|
|
103
|
+
...RayDialog.properties,
|
|
104
|
+
position: { type: String, reflect: true, default: "right" },
|
|
105
|
+
size: { type: String, reflect: true, default: "md" },
|
|
106
|
+
modeless: { type: Boolean, reflect: true },
|
|
107
|
+
floating: { type: Boolean, reflect: true },
|
|
108
|
+
};
|
|
109
|
+
static styles = [overlayStyles, styles];
|
|
110
|
+
static template = `
|
|
111
|
+
<dialog part="drawer" aria-labelledby="title">
|
|
112
|
+
<header class="header" part="header">
|
|
113
|
+
<div class="heading"><h2 class="title" id="title" part="title"></h2><p class="description" id="description" part="description" hidden></p></div>
|
|
114
|
+
<div class="actions"><slot name="header"></slot><button class="close focus-ring" part="close" type="button" aria-label="Close"><span data-ray-icon="close" aria-hidden="true"></span></button></div>
|
|
115
|
+
</header>
|
|
116
|
+
<div class="body" part="body"><slot></slot></div>
|
|
117
|
+
<footer class="footer" part="footer"><slot name="footer"></slot></footer>
|
|
118
|
+
<slot name="overlay"></slot>
|
|
119
|
+
</dialog>`;
|
|
120
|
+
|
|
121
|
+
isModal() {
|
|
122
|
+
return !this.modeless;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Leaves through the edge it came from (the --_from of its entry). */
|
|
126
|
+
exitKeyframes() {
|
|
127
|
+
const to = getComputedStyle(this.dialog).getPropertyValue("--_from").trim() || "translateX(100%)";
|
|
128
|
+
return [{ transform: "none" }, { transform: to }];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
update(changed) {
|
|
132
|
+
// xs…xl, full, or any CSS length.
|
|
133
|
+
const size = SIZES[this.size] ?? this.size;
|
|
134
|
+
if (size) this.style.setProperty("--_size", size);
|
|
135
|
+
super.update(changed);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
define("ray-drawer", RayDrawer);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// <ray-empty-state>: what a list, table or pane shows when there is nothing
|
|
2
|
+
// in it: an icon, a title, a line of text and the action that fills it.
|
|
3
|
+
|
|
4
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
5
|
+
|
|
6
|
+
const styles = css`
|
|
7
|
+
:host { display: block; }
|
|
8
|
+
.root {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
align-items: center;
|
|
12
|
+
gap: var(--ray-space-2);
|
|
13
|
+
padding: var(--ray-space-8) var(--ray-space-4);
|
|
14
|
+
text-align: center;
|
|
15
|
+
}
|
|
16
|
+
:host([compact]) .root { padding: var(--ray-space-4); }
|
|
17
|
+
:host([with-border]) .root { border: 1px dashed var(--ray-border); border-radius: var(--ray-radius-default); }
|
|
18
|
+
.icon {
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
width: 3rem;
|
|
23
|
+
height: 3rem;
|
|
24
|
+
margin-bottom: var(--ray-space-1);
|
|
25
|
+
color: var(--ray-muted-foreground);
|
|
26
|
+
background: var(--ray-muted);
|
|
27
|
+
border-radius: var(--ray-radius-full);
|
|
28
|
+
}
|
|
29
|
+
.icon:not([data-filled]) { display: none; }
|
|
30
|
+
::slotted(svg) { width: 1.5rem; height: 1.5rem; }
|
|
31
|
+
.title { margin: 0; font-size: var(--ray-font-size-md); font-weight: var(--ray-font-weight-semibold); color: var(--ray-foreground); }
|
|
32
|
+
.title:empty { display: none; }
|
|
33
|
+
.text { max-width: 28rem; font-size: var(--ray-font-size-sm); color: var(--ray-muted-foreground); }
|
|
34
|
+
.actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--ray-space-2); margin-top: var(--ray-space-2); }
|
|
35
|
+
.actions:not([data-filled]) { display: none; }
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
export class RayEmptyState extends RayElement {
|
|
39
|
+
static properties = {
|
|
40
|
+
label: { type: String },
|
|
41
|
+
compact: { type: Boolean, reflect: true },
|
|
42
|
+
withBorder: { type: Boolean, reflect: true },
|
|
43
|
+
};
|
|
44
|
+
static styles = [styles];
|
|
45
|
+
static template = `
|
|
46
|
+
<div class="root" part="root">
|
|
47
|
+
<span class="icon" part="icon"><slot name="icon"></slot></span>
|
|
48
|
+
<p class="title" part="title"></p>
|
|
49
|
+
<div class="text" part="text"><slot></slot></div>
|
|
50
|
+
<div class="actions" part="actions"><slot name="actions"></slot></div>
|
|
51
|
+
</div>`;
|
|
52
|
+
|
|
53
|
+
constructor() {
|
|
54
|
+
super();
|
|
55
|
+
for (const name of ["icon", "actions"]) {
|
|
56
|
+
const slot = this.$(`slot[name="${name}"]`);
|
|
57
|
+
slot.addEventListener("slotchange", () => slot.parentElement.toggleAttribute("data-filled", slot.assignedNodes().length > 0));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
update() {
|
|
62
|
+
this.$(".title").textContent = this.label ?? "";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
define("ray-empty-state", RayEmptyState);
|