@raylang/elements 0.0.0-stage → 0.4.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,168 @@
|
|
|
1
|
+
// <ray-app-bar>: the top bar of a phone screen (Material's top app bar, the
|
|
2
|
+
// iOS navigation bar): a back or menu button, the title (and a subtitle),
|
|
3
|
+
// actions, and an optional row under it (tabs, a search field).
|
|
4
|
+
//
|
|
5
|
+
// scroll-behavior="elevate" (default) draws its border and shadow once the content
|
|
6
|
+
// scrolls; scroll-behavior="hide" also slides it away while scrolling down and back
|
|
7
|
+
// on the way up. It follows the scroller of its ray-app-shell, else
|
|
8
|
+
// `scroll-target`, else the window. Clear of the notch (safe-area inset).
|
|
9
|
+
|
|
10
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
11
|
+
import { scrollerOf, watchScroll } from "../../core/scroll.js";
|
|
12
|
+
|
|
13
|
+
const styles = css`
|
|
14
|
+
:host {
|
|
15
|
+
--_h: 3.5rem;
|
|
16
|
+
--_bg: var(--ray-background);
|
|
17
|
+
--_fg: var(--ray-foreground);
|
|
18
|
+
--_muted: var(--ray-muted-foreground);
|
|
19
|
+
display: grid;
|
|
20
|
+
grid-template-rows: 1fr;
|
|
21
|
+
position: relative;
|
|
22
|
+
z-index: 2;
|
|
23
|
+
transition: grid-template-rows var(--ray-duration-base) var(--ray-easing-standard), translate var(--ray-duration-base) var(--ray-easing-standard);
|
|
24
|
+
}
|
|
25
|
+
/* Hidden above a scroller (in a shell): its row closes, the content takes
|
|
26
|
+
the room. Over the window (sticky): it slides up. */
|
|
27
|
+
:host([data-concealed="flow"]) { grid-template-rows: 0fr; clip-path: inset(0); }
|
|
28
|
+
:host([data-concealed="over"]) { translate: 0 -100%; }
|
|
29
|
+
:host([hidden]) { display: none; }
|
|
30
|
+
:host([sticky]) { position: sticky; top: 0; }
|
|
31
|
+
:host([variant="filled"]) { --_bg: var(--ray-primary-filled); --_fg: var(--ray-primary-contrast); --_muted: color-mix(in srgb, var(--ray-primary-contrast) 75%, transparent); }
|
|
32
|
+
:host([variant="transparent"]) { --_bg: transparent; }
|
|
33
|
+
:host([size="sm"]) { --_h: 3rem; }
|
|
34
|
+
:host([size="lg"]) { --_h: 4rem; }
|
|
35
|
+
/* The row that closes (no padding, no border: it can reach 0). */
|
|
36
|
+
.fold { min-height: 0; }
|
|
37
|
+
.bar {
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
padding-top: env(safe-area-inset-top, 0px);
|
|
40
|
+
color: var(--_fg);
|
|
41
|
+
background: var(--_bg);
|
|
42
|
+
border-bottom: 1px solid transparent;
|
|
43
|
+
transition: border-color var(--ray-duration-fast), box-shadow var(--ray-duration-fast), background-color var(--ray-duration-fast);
|
|
44
|
+
}
|
|
45
|
+
:host([data-elevated]) .bar { border-bottom-color: var(--ray-border); box-shadow: var(--ray-shadow-sm); }
|
|
46
|
+
:host([data-elevated][variant="transparent"]) .bar { background: var(--ray-background); }
|
|
47
|
+
:host([variant="filled"][data-elevated]) .bar { border-bottom-color: transparent; }
|
|
48
|
+
.row {
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: var(--ray-space-1);
|
|
53
|
+
min-height: var(--_h);
|
|
54
|
+
padding-inline: max(var(--ray-space-2), env(safe-area-inset-left, 0px)) max(var(--ray-space-2), env(safe-area-inset-right, 0px));
|
|
55
|
+
}
|
|
56
|
+
:host([center]) .row { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); }
|
|
57
|
+
.start, .end { display: flex; align-items: center; gap: var(--ray-space-1); }
|
|
58
|
+
.end { justify-content: flex-end; }
|
|
59
|
+
.title { display: grid; min-width: 0; padding-inline: var(--ray-space-2); }
|
|
60
|
+
:host([center]) .title { text-align: center; }
|
|
61
|
+
.label {
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
text-overflow: ellipsis;
|
|
64
|
+
white-space: nowrap;
|
|
65
|
+
font-size: var(--ray-font-size-lg);
|
|
66
|
+
font-weight: var(--ray-font-weight-bold, 700);
|
|
67
|
+
line-height: 1.25;
|
|
68
|
+
}
|
|
69
|
+
:host([size="sm"]) .label { font-size: var(--ray-font-size-md); }
|
|
70
|
+
.description { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--ray-font-size-xs); color: var(--_muted); }
|
|
71
|
+
.label:empty, .description:empty { display: none; }
|
|
72
|
+
.bottom { padding: 0 var(--ray-space-2) var(--ray-space-2); }
|
|
73
|
+
.bottom:not([data-filled]), .start:not([data-filled]) { display: none; }
|
|
74
|
+
:host([center]) .start:not([data-filled]) { display: flex; }
|
|
75
|
+
@media (prefers-reduced-motion: reduce) { :host { transition: none; } }
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
export class RayAppBar extends RayElement {
|
|
79
|
+
static properties = {
|
|
80
|
+
label: { type: String },
|
|
81
|
+
description: { type: String },
|
|
82
|
+
center: { type: Boolean, reflect: true },
|
|
83
|
+
variant: { type: String, reflect: true, default: "default" },
|
|
84
|
+
size: { type: String, reflect: true, default: "md" },
|
|
85
|
+
// Not `scroll`: that would hide the element's scroll() method.
|
|
86
|
+
scrollBehavior: { type: String, attribute: "scroll-behavior", reflect: true, default: "elevate" },
|
|
87
|
+
sticky: { type: Boolean, reflect: true },
|
|
88
|
+
scrollTarget: { type: String, attribute: "scroll-target" },
|
|
89
|
+
};
|
|
90
|
+
static styles = [styles];
|
|
91
|
+
static template = `
|
|
92
|
+
<div class="fold"><div class="bar" part="bar">
|
|
93
|
+
<div class="row">
|
|
94
|
+
<div class="start" part="start"><slot name="start"></slot></div>
|
|
95
|
+
<div class="title" part="title"><span class="label"></span><span class="description"></span><slot></slot></div>
|
|
96
|
+
<div class="end" part="end"><slot name="end"></slot></div>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="bottom" part="bottom"><slot name="bottom"></slot></div>
|
|
99
|
+
</div></div>`;
|
|
100
|
+
|
|
101
|
+
#stop = null;
|
|
102
|
+
#lock = 0;
|
|
103
|
+
|
|
104
|
+
constructor() {
|
|
105
|
+
super();
|
|
106
|
+
for (const name of ["start", "bottom"]) {
|
|
107
|
+
const slot = this.$(`slot[name="${name}"]`);
|
|
108
|
+
slot.addEventListener("slotchange", () => slot.parentElement.toggleAttribute("data-filled", slot.assignedNodes().length > 0));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
connectedCallback() {
|
|
113
|
+
super.connectedCallback();
|
|
114
|
+
for (const name of ["start", "bottom"]) {
|
|
115
|
+
const slot = this.$(`slot[name="${name}"]`);
|
|
116
|
+
slot.parentElement.toggleAttribute("data-filled", slot.assignedNodes().length > 0);
|
|
117
|
+
}
|
|
118
|
+
// The shell (or the target) may upgrade after the bar: watch once settled.
|
|
119
|
+
queueMicrotask(() => this._watch());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
disconnectedCallback() {
|
|
123
|
+
super.disconnectedCallback();
|
|
124
|
+
this.#stop?.();
|
|
125
|
+
this.#stop = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Whether scroll-behavior="hide" has slid it away. */
|
|
129
|
+
get concealed() {
|
|
130
|
+
return this.hasAttribute("data-concealed");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
update(changed) {
|
|
134
|
+
this.$(".label").textContent = this.label ?? "";
|
|
135
|
+
this.$(".description").textContent = this.description ?? "";
|
|
136
|
+
if (changed.has("scrollBehavior") || changed.has("scrollTarget")) this._watch();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_watch() {
|
|
140
|
+
this.#stop?.();
|
|
141
|
+
this.#stop = null;
|
|
142
|
+
this.removeAttribute("data-concealed");
|
|
143
|
+
this.toggleAttribute("data-elevated", false);
|
|
144
|
+
if (!this.isConnected || this.scrollBehavior === "none") return;
|
|
145
|
+
const scroller = scrollerOf(this, this.scrollTarget);
|
|
146
|
+
// In a shell the bar sits above the scroller: hiding gives its room to
|
|
147
|
+
// the content. Over the window it slides away.
|
|
148
|
+
const inFlow = scroller !== window;
|
|
149
|
+
this.#stop = watchScroll(scroller, ({ y, direction }) => {
|
|
150
|
+
this.toggleAttribute("data-elevated", y > 0);
|
|
151
|
+
if (this.scrollBehavior !== "hide") return;
|
|
152
|
+
if (y <= 0) this.removeAttribute("data-concealed");
|
|
153
|
+
else this._conceal(direction === "down" && y > this.offsetHeight + 8, inFlow);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_conceal(on, inFlow = true) {
|
|
158
|
+
if (on === this.concealed) return;
|
|
159
|
+
// Closing its row resizes the scroller, which can move the scroll
|
|
160
|
+
// position (at the end of the content): ignore that for a moment.
|
|
161
|
+
if (performance.now() < this.#lock) return;
|
|
162
|
+
this.#lock = performance.now() + 400;
|
|
163
|
+
if (on) this.setAttribute("data-concealed", inFlow ? "flow" : "over");
|
|
164
|
+
else this.removeAttribute("data-concealed");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
define("ray-app-bar", RayAppBar);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// <ray-app-shell>: the frame of an app (Mantine's AppShell): header,
|
|
2
|
+
// navbar, main, aside, footer, and a bottom bar for phones.
|
|
3
|
+
//
|
|
4
|
+
// Wide: the header across the top (or beside the navbar with
|
|
5
|
+
// layout="alt"), the navbar and the aside on the sides of the main content.
|
|
6
|
+
// Narrow (below `breakpoint`, the shell's own width): one column, header,
|
|
7
|
+
// main, footer and the `bottom` slot (a ray-bottom-nav); the navbar and the
|
|
8
|
+
// aside take no room (a ray-sidebar there still opens as a drawer).
|
|
9
|
+
//
|
|
10
|
+
// The main content scrolls, the bars stay: `scroller` is the element that
|
|
11
|
+
// scrolls (ray-app-bar and ray-bottom-nav find it on their own).
|
|
12
|
+
//
|
|
13
|
+
// Under a transparent title bar (a std/ui full_content window with
|
|
14
|
+
// core/raylang.js; see titlebar() in core/host.js) the header is the title
|
|
15
|
+
// bar: it is at least as tall, leaves room for the window buttons and drags
|
|
16
|
+
// the window. With layout="alt", or without a header, the navbar and the
|
|
17
|
+
// main content start below the title bar instead.
|
|
18
|
+
|
|
19
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
20
|
+
import { spaceValue } from "../../core/styles.js";
|
|
21
|
+
import { titlebar, dragRegion, onHostChange } from "../../core/host.js";
|
|
22
|
+
|
|
23
|
+
const styles = css`
|
|
24
|
+
:host {
|
|
25
|
+
display: block;
|
|
26
|
+
height: 100dvh;
|
|
27
|
+
min-height: 0;
|
|
28
|
+
color: var(--ray-foreground);
|
|
29
|
+
background: var(--ray-background);
|
|
30
|
+
}
|
|
31
|
+
:host([hidden]) { display: none; }
|
|
32
|
+
.shell {
|
|
33
|
+
display: grid;
|
|
34
|
+
height: 100%;
|
|
35
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
36
|
+
grid-template-rows: auto minmax(0, 1fr) auto auto;
|
|
37
|
+
grid-template-areas:
|
|
38
|
+
"header header header"
|
|
39
|
+
"navbar main aside"
|
|
40
|
+
"footer footer footer"
|
|
41
|
+
"bottom bottom bottom";
|
|
42
|
+
}
|
|
43
|
+
:host([layout="alt"]) .shell {
|
|
44
|
+
grid-template-areas:
|
|
45
|
+
"navbar header aside"
|
|
46
|
+
"navbar main aside"
|
|
47
|
+
"navbar footer aside"
|
|
48
|
+
"bottom bottom bottom";
|
|
49
|
+
}
|
|
50
|
+
.header { grid-area: header; min-width: 0; }
|
|
51
|
+
.navbar { grid-area: navbar; display: flex; min-height: 0; }
|
|
52
|
+
.aside { grid-area: aside; display: flex; min-height: 0; }
|
|
53
|
+
.footer { grid-area: footer; min-width: 0; }
|
|
54
|
+
.bottom { grid-area: bottom; min-width: 0; display: none; }
|
|
55
|
+
.main {
|
|
56
|
+
grid-area: main;
|
|
57
|
+
position: relative;
|
|
58
|
+
min-width: 0;
|
|
59
|
+
min-height: 0;
|
|
60
|
+
overflow: auto;
|
|
61
|
+
overscroll-behavior: contain;
|
|
62
|
+
padding: var(--_p, 0);
|
|
63
|
+
}
|
|
64
|
+
.header:not([data-filled]), .footer:not([data-filled]), .navbar:not([data-filled]), .aside:not([data-filled]) { display: none; }
|
|
65
|
+
|
|
66
|
+
/* Narrow: one column; the side panels take no room but stay rendered
|
|
67
|
+
(a sidebar's drawer is a top-layer dialog inside them). */
|
|
68
|
+
:host([data-mobile]) .shell {
|
|
69
|
+
grid-template-columns: minmax(0, 1fr);
|
|
70
|
+
grid-template-areas: "header" "main" "footer" "bottom";
|
|
71
|
+
}
|
|
72
|
+
:host([data-mobile]) .navbar, :host([data-mobile]) .aside {
|
|
73
|
+
position: absolute;
|
|
74
|
+
width: 0;
|
|
75
|
+
height: 0;
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
}
|
|
78
|
+
:host([data-mobile]) .bottom[data-filled] { display: block; }
|
|
79
|
+
|
|
80
|
+
/* Under a transparent title bar (data-titlebar, from the host). */
|
|
81
|
+
:host([data-titlebar]) .header { min-height: var(--ray-titlebar-height, 0px); }
|
|
82
|
+
:host([data-titlebar]:not([layout="alt"])) .header,
|
|
83
|
+
:host([data-titlebar][data-mobile]) .header { padding-inline-start: var(--ray-titlebar-inset, 0px); }
|
|
84
|
+
:host([data-titlebar][layout="alt"]:not([data-mobile])) .navbar,
|
|
85
|
+
:host([data-titlebar][data-no-header]) .navbar,
|
|
86
|
+
:host([data-titlebar][data-no-header]) .aside { padding-top: var(--ray-titlebar-height, 0px); }
|
|
87
|
+
:host([data-titlebar][data-no-header]) .main { padding-top: calc(var(--_p, 0px) + var(--ray-titlebar-height, 0px)); }
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
/** A CSS length in px, against the element's font size for em/rem. */
|
|
91
|
+
function toPx(value, el) {
|
|
92
|
+
const m = String(value ?? "").trim().match(/^(\d+(?:\.\d+)?)(px|em|rem)?$/);
|
|
93
|
+
if (!m) return 768;
|
|
94
|
+
const n = Number(m[1]);
|
|
95
|
+
if (m[2] === "rem") return n * parseFloat(getComputedStyle(document.documentElement).fontSize || "16");
|
|
96
|
+
if (m[2] === "em") return n * parseFloat(getComputedStyle(el).fontSize || "16");
|
|
97
|
+
return n;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class RayAppShell extends RayElement {
|
|
101
|
+
static properties = {
|
|
102
|
+
layout: { type: String, reflect: true, default: "default" },
|
|
103
|
+
breakpoint: { type: String, default: "48em" },
|
|
104
|
+
padding: { type: String, reflect: true },
|
|
105
|
+
};
|
|
106
|
+
static styles = [styles];
|
|
107
|
+
static template = `
|
|
108
|
+
<div class="shell" part="shell">
|
|
109
|
+
<div class="header" part="header"><slot name="header"></slot></div>
|
|
110
|
+
<div class="navbar" part="navbar"><slot name="navbar"></slot></div>
|
|
111
|
+
<div class="main" part="main"><slot></slot></div>
|
|
112
|
+
<div class="aside" part="aside"><slot name="aside"></slot></div>
|
|
113
|
+
<div class="footer" part="footer"><slot name="footer"></slot></div>
|
|
114
|
+
<div class="bottom" part="bottom"><slot name="bottom"></slot></div>
|
|
115
|
+
</div>`;
|
|
116
|
+
|
|
117
|
+
#ro = null;
|
|
118
|
+
#syncs = [];
|
|
119
|
+
#stopHost = null;
|
|
120
|
+
/** @type {Map<Element, () => void>} */
|
|
121
|
+
#drags = new Map();
|
|
122
|
+
|
|
123
|
+
constructor() {
|
|
124
|
+
super();
|
|
125
|
+
for (const name of ["header", "navbar", "aside", "footer", "bottom"]) {
|
|
126
|
+
const slot = this.$(`slot[name="${name}"]`);
|
|
127
|
+
const sync = () => {
|
|
128
|
+
slot.parentElement.toggleAttribute("data-filled", slot.assignedNodes().length > 0);
|
|
129
|
+
if (name === "header") this.#chrome();
|
|
130
|
+
};
|
|
131
|
+
slot.addEventListener("slotchange", sync);
|
|
132
|
+
this.#syncs.push(sync);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The title bar: the shell's attributes, and the header elements as drag handles.
|
|
137
|
+
#chrome() {
|
|
138
|
+
const bar = this.isConnected && titlebar();
|
|
139
|
+
this.toggleAttribute("data-titlebar", Boolean(bar));
|
|
140
|
+
const header = this.$('slot[name="header"]').assignedElements();
|
|
141
|
+
this.toggleAttribute("data-no-header", header.length === 0);
|
|
142
|
+
for (const [el, undo] of this.#drags) {
|
|
143
|
+
if (!bar || !header.includes(el)) {
|
|
144
|
+
undo();
|
|
145
|
+
this.#drags.delete(el);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (bar) for (const el of header) if (!this.#drags.has(el)) this.#drags.set(el, dragRegion(el));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The element that scrolls the main content. */
|
|
152
|
+
get scroller() {
|
|
153
|
+
return this.$(".main");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Whether the shell is in its narrow (phone) layout. */
|
|
157
|
+
get mobile() {
|
|
158
|
+
return this.hasAttribute("data-mobile");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
connectedCallback() {
|
|
162
|
+
super.connectedCallback();
|
|
163
|
+
for (const sync of this.#syncs) sync();
|
|
164
|
+
// The layout follows the shell's own width: right in a phone-sized
|
|
165
|
+
// frame as in a full window. Measured at once too (the observer reports
|
|
166
|
+
// before the next paint, and never in a page that is not rendered).
|
|
167
|
+
this.#ro = new ResizeObserver(() => this._measure());
|
|
168
|
+
this.#ro.observe(this);
|
|
169
|
+
this._measure();
|
|
170
|
+
this.#stopHost ??= onHostChange(() => this.#chrome());
|
|
171
|
+
this.#chrome();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
disconnectedCallback() {
|
|
175
|
+
super.disconnectedCallback();
|
|
176
|
+
this.#ro?.disconnect();
|
|
177
|
+
this.#ro = null;
|
|
178
|
+
this.#stopHost?.();
|
|
179
|
+
this.#stopHost = null;
|
|
180
|
+
this.#chrome();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
update(changed) {
|
|
184
|
+
const p = spaceValue(this.padding);
|
|
185
|
+
if (p) this.style.setProperty("--_p", p);
|
|
186
|
+
else this.style.removeProperty("--_p");
|
|
187
|
+
if (changed.has("breakpoint")) this._measure();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_measure() {
|
|
191
|
+
const mobile = this.getBoundingClientRect().width < toPx(this.breakpoint, this);
|
|
192
|
+
if (mobile === this.mobile) return;
|
|
193
|
+
this.toggleAttribute("data-mobile", mobile);
|
|
194
|
+
this.emit("ray-breakpoint", { mobile });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
define("ray-app-shell", RayAppShell);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// <ray-aspect-ratio>: a box with a fixed width/height ratio; its media
|
|
2
|
+
// (img, video, iframe, any element) fills it.
|
|
3
|
+
|
|
4
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
5
|
+
|
|
6
|
+
const RADII = ["xs", "sm", "md", "lg", "xl", "full"];
|
|
7
|
+
|
|
8
|
+
const styles = css`
|
|
9
|
+
/* Defaults set on the host: an ancestor's private --_r (a card's or a
|
|
10
|
+
button's radius) would otherwise inherit into it. */
|
|
11
|
+
:host {
|
|
12
|
+
--_ratio: 16 / 9;
|
|
13
|
+
--_r: 0;
|
|
14
|
+
display: block;
|
|
15
|
+
position: relative;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
aspect-ratio: var(--_ratio);
|
|
18
|
+
border-radius: var(--_r);
|
|
19
|
+
}
|
|
20
|
+
:host([hidden]) { display: none; }
|
|
21
|
+
::slotted(*) {
|
|
22
|
+
position: absolute;
|
|
23
|
+
inset: 0;
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: 100%;
|
|
26
|
+
object-fit: cover;
|
|
27
|
+
}
|
|
28
|
+
:host([fit="contain"]) ::slotted(*) { object-fit: contain; }
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
/** "16/9", "16:9", "1.5", 2 -> a CSS aspect-ratio value ("16 / 9"); null if invalid. */
|
|
32
|
+
export function parseRatio(value) {
|
|
33
|
+
const s = String(value ?? "").trim();
|
|
34
|
+
const m = s.match(/^(\d+(?:\.\d+)?)\s*[/:]\s*(\d+(?:\.\d+)?)$/);
|
|
35
|
+
if (m && Number(m[2]) > 0) return `${Number(m[1])} / ${Number(m[2])}`;
|
|
36
|
+
const n = Number(s);
|
|
37
|
+
return s && Number.isFinite(n) && n > 0 ? String(n) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class RayAspectRatio extends RayElement {
|
|
41
|
+
static properties = {
|
|
42
|
+
ratio: { type: String, reflect: true, default: "16/9" },
|
|
43
|
+
fit: { type: String, reflect: true, default: "cover" },
|
|
44
|
+
radius: { type: String, reflect: true },
|
|
45
|
+
};
|
|
46
|
+
static styles = [styles];
|
|
47
|
+
static template = `<slot></slot>`;
|
|
48
|
+
|
|
49
|
+
update() {
|
|
50
|
+
const r = parseRatio(this.ratio);
|
|
51
|
+
if (r) this.style.setProperty("--_ratio", r);
|
|
52
|
+
else this.style.removeProperty("--_ratio");
|
|
53
|
+
const rad = this.radius;
|
|
54
|
+
if (rad) this.style.setProperty("--_r", RADII.includes(rad) ? `var(--ray-radius-${rad})` : rad);
|
|
55
|
+
else this.style.removeProperty("--_r");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
define("ray-aspect-ratio", RayAspectRatio);
|