@raylang/elements 0.7.0 → 0.9.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/base.css +1 -1
- package/components/accordion/accordion.js +7 -4
- package/components/alert/alert.js +2 -1
- package/components/attachment/attachment.js +4 -5
- package/components/blockquote/blockquote.js +2 -2
- package/components/bottom-nav/bottom-nav.js +3 -3
- package/components/carousel/carousel.js +22 -10
- package/components/checkbox/checkbox.js +5 -2
- package/components/checkbox-group/checkbox-group.js +1 -1
- package/components/code/code.js +16 -11
- package/components/color-picker/color-picker.js +5 -5
- package/components/combobox/combobox.js +2 -1
- package/components/command-palette/command-palette.js +17 -2
- package/components/date-picker/date-picker.js +5 -3
- package/components/dialog/dialog.js +1 -1
- package/components/doc-tabs/doc-tabs.js +255 -0
- package/components/drawer/drawer.js +27 -9
- package/components/icon/icon.js +4 -1
- package/components/icon-button/icon-button.js +4 -0
- package/components/indicator/indicator.js +2 -1
- package/components/input/input.js +13 -1
- package/components/menu/menu.js +43 -8
- package/components/mode-input/mode-input.js +221 -0
- package/components/nav-link/nav-link.js +5 -2
- package/components/notification/notification.js +15 -12
- package/components/progress/progress.js +2 -2
- package/components/radio/radio.js +4 -2
- package/components/scroll-area/scroll-area.js +14 -11
- package/components/search/search.js +3 -3
- package/components/segmented-control/segmented-control.js +5 -3
- package/components/select/select.js +3 -3
- package/components/separator/separator.js +8 -3
- package/components/sidebar/sidebar.js +154 -11
- package/components/slider/slider.js +5 -2
- package/components/split/split.js +25 -7
- package/components/stepper/stepper.js +4 -4
- package/components/switch/switch.js +3 -3
- package/components/table/table.js +5 -5
- package/components/tabs/tabs.js +55 -14
- package/components/textarea/textarea.js +13 -1
- package/components/timeline/timeline.js +13 -8
- package/components/toggle-group/toggle-group.js +6 -4
- package/core/complete.js +252 -0
- package/core/core.js +6 -1
- package/core/direction.js +19 -0
- package/core/icons.js +12 -0
- package/core/position.js +11 -3
- package/core/scroll.js +85 -1
- package/core/styles.js +1 -1
- package/core/textlayer.js +214 -0
- package/core.js +1 -0
- package/custom-elements.json +1487 -143
- package/index.js +2 -0
- package/llms-full.txt +224 -28
- package/llms.txt +26 -4
- package/package.json +9 -1
- package/ray-elements.d.ts +190 -31
- package/vscode.html-custom-data.json +477 -22
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
// <ray-sidebar>: the side navigation of an app shell (shadcn's Sidebar).
|
|
2
2
|
// Header, content and footer slots; on wide screens a panel that can
|
|
3
|
-
// collapse to an icon rail
|
|
3
|
+
// collapse (`collapsed`) to an icon rail, or out of view with
|
|
4
|
+
// collapse="offcanvas"; below `breakpoint` an off-canvas
|
|
4
5
|
// drawer (`open`): a modal <dialog> sliding in from its side, with focus
|
|
5
6
|
// trapped, Escape and backdrop click closing it.
|
|
6
7
|
//
|
|
7
8
|
// toggle() does the right thing for the screen: collapse/expand on wide
|
|
8
9
|
// screens, open/close the drawer on narrow ones — wire it to a menu button.
|
|
9
10
|
//
|
|
11
|
+
// With `resizable`, its inner edge is a separator (WAI-ARIA window splitter)
|
|
12
|
+
// that drags or arrow-keys the width between min-width and max-width;
|
|
13
|
+
// `autosave="key"` keeps it (localStorage).
|
|
14
|
+
//
|
|
10
15
|
// <ray-sidebar-group label="Projects">: a titled section of nav links; the
|
|
11
16
|
// title hides when the sidebar collapses.
|
|
12
17
|
|
|
13
18
|
import { RayElement, css, define } from "../../core/core.js";
|
|
14
19
|
import { overlayStyles } from "../../core/styles.js";
|
|
20
|
+
import { isRtl } from "../../core/direction.js";
|
|
15
21
|
import { play, playAll } from "../../core/motion.js";
|
|
16
22
|
|
|
17
23
|
const styles = css`
|
|
@@ -26,6 +32,10 @@ const styles = css`
|
|
|
26
32
|
transition: width var(--ray-duration-base) var(--ray-easing-standard);
|
|
27
33
|
}
|
|
28
34
|
:host([collapsed]:not([data-mobile])) { width: var(--_rail); }
|
|
35
|
+
/* offcanvas: collapsed is out of view, and out of the tab order once the
|
|
36
|
+
width has animated to 0 (visibility waits for it; showing does not). */
|
|
37
|
+
:host([collapse="offcanvas"][collapsed]:not([data-mobile])) { width: 0; padding: 0; }
|
|
38
|
+
:host([collapse="offcanvas"][collapsed]:not([data-mobile])) .panel { visibility: hidden; transition: visibility 0s var(--ray-duration-base); }
|
|
29
39
|
:host([data-mobile]) { width: 0; }
|
|
30
40
|
.panel {
|
|
31
41
|
box-sizing: border-box;
|
|
@@ -38,7 +48,11 @@ const styles = css`
|
|
|
38
48
|
background: var(--ray-sidebar, color-mix(in srgb, var(--ray-muted) 70%, var(--ray-background)));
|
|
39
49
|
border-inline-end: 1px solid var(--ray-border);
|
|
40
50
|
}
|
|
41
|
-
:
|
|
51
|
+
/* side: start/end follow the writing direction, left/right are sides of
|
|
52
|
+
the screen; the border faces the content. */
|
|
53
|
+
:host([side="end"]) .panel { border-inline-end: 0; border-inline-start: 1px solid var(--ray-border); }
|
|
54
|
+
:host([side="left"]) .panel { border: 0; border-right: 1px solid var(--ray-border); } /* physical */
|
|
55
|
+
:host([side="right"]) .panel { border: 0; border-left: 1px solid var(--ray-border); } /* physical */
|
|
42
56
|
:host([variant="floating"]:not([data-mobile])) { padding: var(--ray-space-2); box-sizing: border-box; }
|
|
43
57
|
:host([variant="floating"]) .panel { border: 1px solid var(--ray-border); border-radius: var(--ray-radius-lg); box-shadow: var(--ray-shadow-sm); }
|
|
44
58
|
.header, .footer { flex: none; padding: var(--ray-space-2); }
|
|
@@ -49,7 +63,8 @@ const styles = css`
|
|
|
49
63
|
dialog {
|
|
50
64
|
--_from: -100%;
|
|
51
65
|
position: fixed;
|
|
52
|
-
inset: 0
|
|
66
|
+
inset-block: 0;
|
|
67
|
+
inset-inline: 0 auto;
|
|
53
68
|
box-sizing: border-box;
|
|
54
69
|
width: min(var(--_w), calc(100vw - 3rem));
|
|
55
70
|
max-width: none;
|
|
@@ -61,25 +76,57 @@ const styles = css`
|
|
|
61
76
|
background: transparent;
|
|
62
77
|
overflow: visible;
|
|
63
78
|
}
|
|
64
|
-
:
|
|
79
|
+
dialog:dir(rtl) { --_from: 100%; }
|
|
80
|
+
:host([side="end"]) dialog { inset-inline: auto 0; --_from: 100%; }
|
|
81
|
+
:host([side="end"]) dialog:dir(rtl) { --_from: -100%; }
|
|
82
|
+
:host([side="left"]) dialog { inset: 0 auto 0 0; --_from: -100%; } /* physical */
|
|
83
|
+
:host([side="right"]) dialog { inset: 0 0 0 auto; --_from: 100%; } /* physical */
|
|
65
84
|
dialog[open] { display: block; animation: ray-sidebar-in var(--ray-duration-slow) var(--ray-easing-out); }
|
|
66
85
|
dialog .panel { box-shadow: var(--ray-shadow-xl); background: var(--ray-card); }
|
|
67
86
|
@keyframes ray-sidebar-in { from { transform: translateX(var(--_from)); } }
|
|
68
|
-
:host([side="right"]) dialog { --_from: 100%; }
|
|
69
87
|
dialog::backdrop { animation: ray-fade-in var(--ray-duration-base) var(--ray-easing-out); }
|
|
70
88
|
@keyframes ray-fade-in { from { opacity: 0; } }
|
|
89
|
+
|
|
90
|
+
/* resizable: a separator on the edge that faces the content. */
|
|
91
|
+
:host { position: relative; }
|
|
92
|
+
.resizer { display: none; position: absolute; z-index: 1; inset-block: 0; inset-inline-end: -0.25rem; width: 0.5rem; cursor: col-resize; touch-action: none; outline: none; }
|
|
93
|
+
:host([resizable]:not([collapsed]):not([data-mobile])) .resizer { display: block; }
|
|
94
|
+
:host([side="end"]) .resizer { inset-inline-end: auto; inset-inline-start: -0.25rem; }
|
|
95
|
+
:host([side="left"]) .resizer { inset-inline: auto; right: -0.25rem; } /* physical */
|
|
96
|
+
:host([side="right"]) .resizer { inset-inline: auto; left: -0.25rem; } /* physical */
|
|
97
|
+
.resizer::before {
|
|
98
|
+
content: "";
|
|
99
|
+
position: absolute;
|
|
100
|
+
inset-block: 0;
|
|
101
|
+
left: 50%; /* physical */
|
|
102
|
+
width: 3px;
|
|
103
|
+
background: transparent;
|
|
104
|
+
transform: translateX(-50%);
|
|
105
|
+
transition: background-color var(--ray-duration-fast) var(--ray-easing-standard);
|
|
106
|
+
}
|
|
107
|
+
.resizer:hover::before, .resizer:focus-visible::before, :host([data-dragging]) .resizer::before { background: var(--ray-primary); }
|
|
108
|
+
:host([data-dragging]) { transition: none; user-select: none; cursor: col-resize; }
|
|
71
109
|
`;
|
|
72
110
|
|
|
111
|
+
const STEP = 10;
|
|
112
|
+
const BIG_STEP = 50;
|
|
113
|
+
|
|
73
114
|
export class RaySidebar extends RayElement {
|
|
74
115
|
static properties = {
|
|
75
116
|
collapsed: { type: Boolean, reflect: true },
|
|
117
|
+
collapse: { type: String, reflect: true, default: "icon" },
|
|
76
118
|
open: { type: Boolean, reflect: true },
|
|
77
|
-
side: { type: String, reflect: true, default: "
|
|
119
|
+
side: { type: String, reflect: true, default: "start" },
|
|
78
120
|
variant: { type: String, reflect: true, default: "sidebar" },
|
|
79
121
|
width: { type: String },
|
|
80
122
|
collapsedWidth: { type: String },
|
|
81
123
|
breakpoint: { type: String, default: "48em" },
|
|
82
124
|
label: { type: String, default: "Sidebar" },
|
|
125
|
+
resizable: { type: Boolean, reflect: true },
|
|
126
|
+
minWidth: { type: Number, default: 192 },
|
|
127
|
+
maxWidth: { type: Number, default: 480 },
|
|
128
|
+
autosave: { type: String },
|
|
129
|
+
resizeLabel: { type: String, default: "Resize sidebar" },
|
|
83
130
|
};
|
|
84
131
|
static styles = [overlayStyles, styles];
|
|
85
132
|
static template = `
|
|
@@ -88,7 +135,8 @@ export class RaySidebar extends RayElement {
|
|
|
88
135
|
<nav class="content" part="content"><slot></slot></nav>
|
|
89
136
|
<div class="footer" part="footer"><slot name="footer"></slot></div>
|
|
90
137
|
</div>
|
|
91
|
-
<dialog part="drawer"></dialog
|
|
138
|
+
<dialog part="drawer"></dialog>
|
|
139
|
+
<div class="resizer" part="resizer" role="separator" tabindex="0" aria-orientation="vertical"></div>`;
|
|
92
140
|
|
|
93
141
|
#mq = null;
|
|
94
142
|
#exit = null;
|
|
@@ -120,11 +168,27 @@ export class RaySidebar extends RayElement {
|
|
|
120
168
|
});
|
|
121
169
|
// New nav links and groups pick up the collapsed state.
|
|
122
170
|
this.$(".content slot").addEventListener("slotchange", () => this._syncCollapsed());
|
|
171
|
+
this._px = null;
|
|
172
|
+
this.resizer = this.$(".resizer");
|
|
173
|
+
this.resizer.addEventListener("pointerdown", (e) => this._down(e));
|
|
174
|
+
this.resizer.addEventListener("keydown", (e) => this._resizeKey(e));
|
|
123
175
|
}
|
|
124
176
|
|
|
125
177
|
connectedCallback() {
|
|
126
178
|
super.connectedCallback();
|
|
127
179
|
this._watch();
|
|
180
|
+
if (this.autosave && !this._restored) {
|
|
181
|
+
this._restored = true;
|
|
182
|
+
try {
|
|
183
|
+
const saved = Number(localStorage.getItem(`ray-sidebar:${this.autosave}`));
|
|
184
|
+
if (saved > 0) {
|
|
185
|
+
this._px = saved;
|
|
186
|
+
this.requestUpdate();
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// No storage: the default width is fine.
|
|
190
|
+
}
|
|
191
|
+
}
|
|
128
192
|
}
|
|
129
193
|
|
|
130
194
|
disconnectedCallback() {
|
|
@@ -168,13 +232,21 @@ export class RaySidebar extends RayElement {
|
|
|
168
232
|
}
|
|
169
233
|
|
|
170
234
|
update(changed) {
|
|
171
|
-
|
|
235
|
+
// A later change of `width` wins over a dragged or restored one.
|
|
236
|
+
if (changed.has("width") && this._updated) this._px = null;
|
|
237
|
+
this._updated = true;
|
|
238
|
+
if (this._px !== null) this.style.setProperty("--_w", `${this._clamp(this._px)}px`);
|
|
239
|
+
else if (this.width) this.style.setProperty("--_w", this.width);
|
|
172
240
|
else this.style.removeProperty("--_w");
|
|
241
|
+
this.resizer.setAttribute("aria-label", this.resizeLabel ?? "Resize sidebar");
|
|
242
|
+
this.resizer.setAttribute("aria-valuemin", String(this.minWidth ?? 0));
|
|
243
|
+
this.resizer.setAttribute("aria-valuemax", String(this.maxWidth ?? 0));
|
|
244
|
+
queueMicrotask(() => this.resizer.setAttribute("aria-valuenow", String(Math.round(this._width()))));
|
|
173
245
|
if (this.collapsedWidth) this.style.setProperty("--_rail", this.collapsedWidth);
|
|
174
246
|
else this.style.removeProperty("--_rail");
|
|
175
247
|
this.$(".content").setAttribute("aria-label", this.label ?? "Sidebar");
|
|
176
248
|
if (changed.has("breakpoint") && this.isConnected) this._watch();
|
|
177
|
-
if (changed.has("collapsed")) this._syncCollapsed();
|
|
249
|
+
if (changed.has("collapsed") || changed.has("collapse")) this._syncCollapsed();
|
|
178
250
|
if (changed.has("open") && this.mobile) {
|
|
179
251
|
if (this.open && this.#exit) {
|
|
180
252
|
this.#exit.cancel();
|
|
@@ -187,13 +259,84 @@ export class RaySidebar extends RayElement {
|
|
|
187
259
|
}
|
|
188
260
|
}
|
|
189
261
|
|
|
262
|
+
/** The expanded width in px (not mid-transition when it was set here). */
|
|
263
|
+
_width() {
|
|
264
|
+
return this._px !== null ? this._clamp(this._px) : this.getBoundingClientRect().width;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
_clamp(px) {
|
|
268
|
+
return Math.round(Math.max(this.minWidth ?? 0, Math.min(px, this.maxWidth ?? Infinity)));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The screen side it sits on: start and end follow the writing direction. */
|
|
272
|
+
_onLeft() {
|
|
273
|
+
if (this.side === "left" || this.side === "right") return this.side === "left";
|
|
274
|
+
return (this.side === "end") === isRtl(this);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
_resizeTo(px, commit) {
|
|
278
|
+
this._px = this._clamp(px);
|
|
279
|
+
this.requestUpdate();
|
|
280
|
+
if (!commit) return;
|
|
281
|
+
if (this.autosave) {
|
|
282
|
+
try {
|
|
283
|
+
localStorage.setItem(`ray-sidebar:${this.autosave}`, String(this._px));
|
|
284
|
+
} catch {
|
|
285
|
+
// Not persisted: fine.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.emit("ray-resize", { width: this._px });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
_down(e) {
|
|
292
|
+
if (e.button !== 0) return;
|
|
293
|
+
e.preventDefault();
|
|
294
|
+
this.resizer.focus();
|
|
295
|
+
try {
|
|
296
|
+
this.resizer.setPointerCapture(e.pointerId);
|
|
297
|
+
} catch {
|
|
298
|
+
// The pointer is already gone: drag without capture.
|
|
299
|
+
}
|
|
300
|
+
this.setAttribute("data-dragging", "");
|
|
301
|
+
const rect = this.getBoundingClientRect();
|
|
302
|
+
const left = this._onLeft();
|
|
303
|
+
const at = (ev) => (left ? ev.clientX - rect.left : rect.right - ev.clientX);
|
|
304
|
+
const move = (ev) => this._resizeTo(at(ev), false);
|
|
305
|
+
const up = (ev) => {
|
|
306
|
+
this.resizer.removeEventListener("pointermove", move);
|
|
307
|
+
this.resizer.removeEventListener("pointerup", up);
|
|
308
|
+
this.resizer.removeEventListener("pointercancel", up);
|
|
309
|
+
this.removeAttribute("data-dragging");
|
|
310
|
+
this._resizeTo(at(ev), true);
|
|
311
|
+
};
|
|
312
|
+
this.resizer.addEventListener("pointermove", move);
|
|
313
|
+
this.resizer.addEventListener("pointerup", up);
|
|
314
|
+
this.resizer.addEventListener("pointercancel", up);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Arrows move the edge on screen: toward the content widens it.
|
|
318
|
+
_resizeKey(e) {
|
|
319
|
+
const step = e.shiftKey ? BIG_STEP : STEP;
|
|
320
|
+
const current = this._width();
|
|
321
|
+
const wider = this._onLeft() ? "ArrowRight" : "ArrowLeft";
|
|
322
|
+
const narrower = this._onLeft() ? "ArrowLeft" : "ArrowRight";
|
|
323
|
+
let next = null;
|
|
324
|
+
if (e.key === wider) next = current + step;
|
|
325
|
+
else if (e.key === narrower) next = current - step;
|
|
326
|
+
else if (e.key === "Home") next = this.minWidth ?? 0;
|
|
327
|
+
else if (e.key === "End") next = this.maxWidth ?? current;
|
|
328
|
+
if (next === null) return;
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
this._resizeTo(next, true);
|
|
331
|
+
}
|
|
332
|
+
|
|
190
333
|
_show() {
|
|
191
334
|
this.dialog.showModal();
|
|
192
335
|
this.emit("ray-open");
|
|
193
336
|
}
|
|
194
337
|
|
|
195
338
|
async _leave() {
|
|
196
|
-
const to = this.
|
|
339
|
+
const to = `translateX(${getComputedStyle(this.dialog).getPropertyValue("--_from").trim() || "-100%"})`;
|
|
197
340
|
const exit = playAll([
|
|
198
341
|
play(this.dialog, [{ transform: "none" }, { transform: to }]),
|
|
199
342
|
play(this.dialog, [{ opacity: 1 }, { opacity: 0 }], { pseudoElement: "::backdrop" }),
|
|
@@ -209,7 +352,7 @@ export class RaySidebar extends RayElement {
|
|
|
209
352
|
|
|
210
353
|
// The rail shows icons only: nav links and group titles follow.
|
|
211
354
|
_syncCollapsed() {
|
|
212
|
-
const rail = Boolean(this.collapsed) && !this.mobile;
|
|
355
|
+
const rail = Boolean(this.collapsed) && !this.mobile && this.collapse !== "offcanvas";
|
|
213
356
|
for (const el of this.querySelectorAll("ray-nav-link")) el.iconOnly = rail;
|
|
214
357
|
for (const el of this.querySelectorAll("ray-sidebar-group")) el.toggleAttribute("collapsed", rail);
|
|
215
358
|
}
|
|
@@ -33,9 +33,11 @@ const styles = css`
|
|
|
33
33
|
input::-webkit-slider-runnable-track {
|
|
34
34
|
height: var(--_track);
|
|
35
35
|
border-radius: var(--ray-radius-full);
|
|
36
|
-
background: linear-gradient(to right, var(--_filled) var(--_pct), var(--ray-muted) var(--_pct));
|
|
36
|
+
background: linear-gradient(to right, var(--_filled) var(--_pct), var(--ray-muted) var(--_pct)); /* physical */
|
|
37
37
|
box-shadow: inset 0 0 0 1px var(--ray-border);
|
|
38
38
|
}
|
|
39
|
+
/* Right to left the native range runs from the right: so does its fill. */
|
|
40
|
+
input:dir(rtl)::-webkit-slider-runnable-track { background: linear-gradient(to left, var(--_filled) var(--_pct), var(--ray-muted) var(--_pct)); } /* physical */
|
|
39
41
|
input::-moz-range-track { height: var(--_track); border-radius: var(--ray-radius-full); background: var(--ray-muted); }
|
|
40
42
|
input::-moz-range-progress { height: var(--_track); border-radius: var(--ray-radius-full); background: var(--_filled); }
|
|
41
43
|
input::-webkit-slider-thumb {
|
|
@@ -62,6 +64,7 @@ const styles = css`
|
|
|
62
64
|
.marks { position: relative; height: 1.25rem; margin: var(--ray-space-1) calc(var(--_thumb) / 2) 0; font-size: var(--ray-font-size-xs); color: var(--ray-muted-foreground); }
|
|
63
65
|
.marks:empty { display: none; }
|
|
64
66
|
.mark { position: absolute; transform: translateX(-50%); white-space: nowrap; }
|
|
67
|
+
.mark:dir(rtl) { transform: translateX(50%); }
|
|
65
68
|
.mark::before { content: ""; display: block; width: 1px; height: 0.25rem; margin: 0 auto 0.125rem; background: var(--ray-border); }
|
|
66
69
|
`;
|
|
67
70
|
|
|
@@ -137,7 +140,7 @@ export class RaySlider extends FieldControl {
|
|
|
137
140
|
const v = typeof m === "number" ? m : m.value;
|
|
138
141
|
const span = document.createElement("span");
|
|
139
142
|
span.className = "mark";
|
|
140
|
-
span.style.
|
|
143
|
+
span.style.insetInlineStart = `${((v - min) / (max - min || 1)) * 100}%`;
|
|
141
144
|
span.textContent = typeof m === "number" ? String(m) : (m.label ?? String(m.value));
|
|
142
145
|
box.append(span);
|
|
143
146
|
}
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
//
|
|
5
5
|
// The divider is a focusable separator (WAI-ARIA window splitter): arrows
|
|
6
6
|
// move it (Shift: faster), Home/End go to min/max, Enter collapses the start
|
|
7
|
-
// pane. With `autosave="key"` the size
|
|
7
|
+
// pane. With `autosave="key"` the size and the collapsed state survive
|
|
8
|
+
// reloads (localStorage).
|
|
8
9
|
|
|
9
10
|
import { RayElement, css, define } from "../../core/core.js";
|
|
11
|
+
import { isRtl, inlineKey } from "../../core/direction.js";
|
|
10
12
|
|
|
11
13
|
const styles = css`
|
|
12
14
|
:host {
|
|
@@ -34,13 +36,13 @@ const styles = css`
|
|
|
34
36
|
position: absolute;
|
|
35
37
|
top: 0;
|
|
36
38
|
bottom: 0;
|
|
37
|
-
left: 50%;
|
|
39
|
+
left: 50%; /* physical */
|
|
38
40
|
width: 1px;
|
|
39
41
|
background: var(--ray-border);
|
|
40
42
|
transform: translateX(-50%);
|
|
41
43
|
transition: background-color var(--ray-duration-fast) var(--ray-easing-standard), width var(--ray-duration-fast) var(--ray-easing-standard);
|
|
42
44
|
}
|
|
43
|
-
:host([orientation="vertical"]) .divider::before { top: 50%; bottom: auto;
|
|
45
|
+
:host([orientation="vertical"]) .divider::before { top: 50%; bottom: auto; inset-inline: 0; width: auto; height: 1px; transform: translateY(-50%); }
|
|
44
46
|
:host(:not([disabled])) .divider:hover::before,
|
|
45
47
|
.divider:focus-visible::before,
|
|
46
48
|
:host([data-dragging]) .divider::before { width: 3px; background: var(--ray-primary); }
|
|
@@ -87,6 +89,9 @@ export class RaySplit extends RayElement {
|
|
|
87
89
|
try {
|
|
88
90
|
const saved = Number(localStorage.getItem(`ray-split:${this.autosave}`));
|
|
89
91
|
if (saved > 0) this._px = saved;
|
|
92
|
+
// "1" or "0"; nothing saved keeps the markup's collapsed.
|
|
93
|
+
const collapsed = localStorage.getItem(`ray-split:${this.autosave}:collapsed`);
|
|
94
|
+
if (collapsed !== null) this.collapsed = collapsed === "1";
|
|
90
95
|
} catch {
|
|
91
96
|
// No storage: the default position is fine.
|
|
92
97
|
}
|
|
@@ -108,6 +113,14 @@ export class RaySplit extends RayElement {
|
|
|
108
113
|
// A later change of `position` wins over a dragged/restored size; the
|
|
109
114
|
// first update (position from the markup) does not.
|
|
110
115
|
if (changed.has("position") && this._updated) this._px = null;
|
|
116
|
+
// Collapsed by the divider or by script after the first update: saved.
|
|
117
|
+
if (changed.has("collapsed") && this._updated && this.autosave) {
|
|
118
|
+
try {
|
|
119
|
+
localStorage.setItem(`ray-split:${this.autosave}:collapsed`, this.collapsed ? "1" : "0");
|
|
120
|
+
} catch {
|
|
121
|
+
// Not persisted: fine.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
111
124
|
this._updated = true;
|
|
112
125
|
const min = Math.max(0, this.min ?? 0);
|
|
113
126
|
const max = this.max ?? null;
|
|
@@ -182,13 +195,16 @@ export class RaySplit extends RayElement {
|
|
|
182
195
|
this.setAttribute("data-dragging", "");
|
|
183
196
|
const rect = this.getBoundingClientRect();
|
|
184
197
|
const vertical = this._vertical();
|
|
185
|
-
|
|
198
|
+
// The start pane's size: right to left it is on the right.
|
|
199
|
+
const rtl = !vertical && isRtl(this);
|
|
200
|
+
const at = (ev) => (vertical ? ev.clientY - rect.top : rtl ? rect.right - ev.clientX : ev.clientX - rect.left);
|
|
201
|
+
const move = (ev) => this._resizeTo(at(ev), false);
|
|
186
202
|
const up = (ev) => {
|
|
187
203
|
this.divider.removeEventListener("pointermove", move);
|
|
188
204
|
this.divider.removeEventListener("pointerup", up);
|
|
189
205
|
this.divider.removeEventListener("pointercancel", up);
|
|
190
206
|
this.removeAttribute("data-dragging");
|
|
191
|
-
this._resizeTo(
|
|
207
|
+
this._resizeTo(at(ev), true);
|
|
192
208
|
};
|
|
193
209
|
this.divider.addEventListener("pointermove", move);
|
|
194
210
|
this.divider.addEventListener("pointerup", up);
|
|
@@ -202,9 +218,11 @@ export class RaySplit extends RayElement {
|
|
|
202
218
|
const current = this.collapsed ? 0 : this.size;
|
|
203
219
|
const less = vertical ? "ArrowUp" : "ArrowLeft";
|
|
204
220
|
const more = vertical ? "ArrowDown" : "ArrowRight";
|
|
221
|
+
// Right to left, ArrowLeft moves the divider away from the start pane.
|
|
222
|
+
const key = vertical ? e.key : inlineKey(e.key, this);
|
|
205
223
|
let next = null;
|
|
206
|
-
if (
|
|
207
|
-
else if (
|
|
224
|
+
if (key === less) next = current - step;
|
|
225
|
+
else if (key === more) next = current + step;
|
|
208
226
|
else if (e.key === "Home") next = this.min ?? 0;
|
|
209
227
|
else if (e.key === "End") next = this.max ?? this._total();
|
|
210
228
|
else if (e.key === "Enter") {
|
|
@@ -30,7 +30,7 @@ const stepperStyles = css`
|
|
|
30
30
|
padding: 0;
|
|
31
31
|
font: inherit;
|
|
32
32
|
color: inherit;
|
|
33
|
-
text-align:
|
|
33
|
+
text-align: start;
|
|
34
34
|
background: none;
|
|
35
35
|
border: 0;
|
|
36
36
|
border-radius: var(--ray-radius-md);
|
|
@@ -58,7 +58,7 @@ const stepperStyles = css`
|
|
|
58
58
|
.step[data-state="active"] .indicator { color: var(--_light-color); background: var(--ray-background); border-color: var(--_filled); }
|
|
59
59
|
.step[data-state="done"] .indicator { color: var(--_contrast); background: var(--_filled); border-color: var(--_filled); }
|
|
60
60
|
.step[data-error] .indicator { color: #fff; background: var(--ray-red-filled); border-color: var(--ray-red-filled); }
|
|
61
|
-
.spinner { width: 50%; height: 50%; border: 2px solid currentColor; border-
|
|
61
|
+
.spinner { width: 50%; height: 50%; border: 2px solid currentColor; border-inline-end-color: transparent; border-radius: 50%; animation: ray-step-spin 0.8s linear infinite; }
|
|
62
62
|
@keyframes ray-step-spin { to { rotate: 1turn; } }
|
|
63
63
|
.text { display: grid; min-width: 0; }
|
|
64
64
|
.label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--ray-font-size-sm); font-weight: var(--ray-font-weight-medium); }
|
|
@@ -69,7 +69,7 @@ const stepperStyles = css`
|
|
|
69
69
|
.connector { flex: 1; min-width: var(--ray-space-4); height: 2px; border-radius: 1px; background: var(--ray-border); transition: background-color var(--ray-duration-base); }
|
|
70
70
|
.step[data-state="done"] .connector { background: var(--_filled); }
|
|
71
71
|
.step:last-child .connector { display: none; }
|
|
72
|
-
:host([orientation="vertical"]) .connector { flex: none; width: 2px; min-width: 0; height: auto; min-height: var(--ray-space-6); margin: var(--ray-space-1)
|
|
72
|
+
:host([orientation="vertical"]) .connector { flex: none; width: 2px; min-width: 0; height: auto; min-height: var(--ray-space-6); margin-block: var(--ray-space-1); margin-inline-start: calc(var(--_icon) / 2 - 1px); }
|
|
73
73
|
/* Narrow (labels-below or a narrow stepper): the labels under the numbers. */
|
|
74
74
|
:host([icon-position="top"]) .step { align-items: flex-start; }
|
|
75
75
|
:host([icon-position="top"]) .head { flex-direction: column; align-items: center; text-align: center; gap: var(--ray-space-1); }
|
|
@@ -91,7 +91,7 @@ export class RayStepper extends RayElement {
|
|
|
91
91
|
static properties = {
|
|
92
92
|
active: { type: Number, reflect: true, default: 0 },
|
|
93
93
|
orientation: { type: String, reflect: true, default: "horizontal" },
|
|
94
|
-
iconPosition: { type: String, reflect: true, attribute: "icon-position", default: "
|
|
94
|
+
iconPosition: { type: String, reflect: true, attribute: "icon-position", default: "start" },
|
|
95
95
|
allowStepSelect: { type: Boolean, reflect: true, attribute: "allow-step-select" },
|
|
96
96
|
linear: { type: Boolean, default: true },
|
|
97
97
|
size: { type: String, reflect: true, default: "md" },
|
|
@@ -18,7 +18,7 @@ const styles = css`
|
|
|
18
18
|
.thumb {
|
|
19
19
|
position: absolute;
|
|
20
20
|
top: 50%;
|
|
21
|
-
|
|
21
|
+
inset-inline-start: 2px;
|
|
22
22
|
width: calc(var(--_s) - 6px);
|
|
23
23
|
height: calc(var(--_s) - 6px);
|
|
24
24
|
background: var(--ray-white);
|
|
@@ -26,12 +26,12 @@ const styles = css`
|
|
|
26
26
|
border-radius: var(--_r);
|
|
27
27
|
box-shadow: var(--ray-shadow-xs);
|
|
28
28
|
transform: translateY(-50%);
|
|
29
|
-
transition:
|
|
29
|
+
transition: inset-inline-start var(--ray-duration-base) var(--ray-easing-standard);
|
|
30
30
|
}
|
|
31
31
|
.control:checked + .indicator { background: var(--_filled); border-color: var(--_filled); }
|
|
32
32
|
/* The thumb takes the text color of the filled shade: white on dark fills,
|
|
33
33
|
black on light ones (yellow, or "dark" in the dark scheme). */
|
|
34
|
-
.control:checked + .indicator .thumb {
|
|
34
|
+
.control:checked + .indicator .thumb { inset-inline-start: calc(100% - var(--_s) + 4px); border-color: transparent; background: var(--_contrast); }
|
|
35
35
|
/* Disabled: a muted track (gray when on, like a disabled checkbox) and a
|
|
36
36
|
plain thumb that stays visible on it, on its side. */
|
|
37
37
|
.control:disabled + .indicator .thumb { background: var(--ray-background); border-color: var(--ray-border); box-shadow: none; }
|
|
@@ -27,14 +27,14 @@ const styles = css`
|
|
|
27
27
|
.scroll { max-height: var(--ray-table-max-height, none); overflow: auto; }
|
|
28
28
|
:host([with-border]) .scroll { border: 1px solid var(--ray-border); border-radius: var(--ray-radius-default); }
|
|
29
29
|
table { width: 100%; border-collapse: separate; border-spacing: 0; font-variant-numeric: tabular-nums; }
|
|
30
|
-
caption { padding: var(--_py) var(--_px); text-align:
|
|
31
|
-
th, td { padding: var(--_py) var(--_px); text-align:
|
|
32
|
-
:host([with-column-borders]) th + th, :host([with-column-borders]) td + td { border-
|
|
30
|
+
caption { padding: var(--_py) var(--_px); text-align: start; font-weight: var(--ray-font-weight-semibold); }
|
|
31
|
+
th, td { padding: var(--_py) var(--_px); text-align: start; vertical-align: middle; border-bottom: 1px solid var(--ray-border); }
|
|
32
|
+
:host([with-column-borders]) th + th, :host([with-column-borders]) td + td { border-inline-start: 1px solid var(--ray-border); }
|
|
33
33
|
tbody tr:last-child td { border-bottom: 0; }
|
|
34
34
|
:host(:not([with-border])) tbody tr:last-child td { border-bottom: 1px solid var(--ray-border); }
|
|
35
35
|
th { font-size: var(--ray-font-size-xs); font-weight: var(--ray-font-weight-semibold); color: var(--ray-muted-foreground); white-space: nowrap; background: var(--ray-background); }
|
|
36
36
|
:host([sticky-header]) thead th { position: sticky; top: 0; z-index: 1; box-shadow: 0 1px 0 var(--ray-border); }
|
|
37
|
-
[data-align="right"] { text-align: right; }
|
|
37
|
+
[data-align="right"] { text-align: right; } /* physical */
|
|
38
38
|
[data-align="center"] { text-align: center; }
|
|
39
39
|
:host([striped]) tbody tr:nth-child(even) td { background: color-mix(in srgb, var(--ray-muted) 60%, transparent); }
|
|
40
40
|
tbody tr:hover td { background: var(--ray-default-hover); }
|
|
@@ -61,7 +61,7 @@ const styles = css`
|
|
|
61
61
|
th[aria-sort="ascending"] .sort, th[aria-sort="descending"] .sort { color: var(--ray-foreground); }
|
|
62
62
|
th[aria-sort="ascending"] .sort svg, th[aria-sort="descending"] .sort svg { opacity: 1; }
|
|
63
63
|
th[aria-sort="descending"] .sort svg { transform: rotate(180deg); }
|
|
64
|
-
.check { width: 1%; padding-
|
|
64
|
+
.check { width: 1%; padding-inline-end: 0; }
|
|
65
65
|
.check input { width: 1rem; height: 1rem; margin: 0; accent-color: var(--ray-primary); cursor: pointer; vertical-align: middle; }
|
|
66
66
|
.empty td { padding: var(--ray-space-8) var(--_px); text-align: center; color: var(--ray-muted-foreground); }
|
|
67
67
|
.empty:hover td { background: inherit; }
|
package/components/tabs/tabs.js
CHANGED
|
@@ -3,12 +3,25 @@
|
|
|
3
3
|
// Tabs and panels are light-DOM children, so their roles, ids,
|
|
4
4
|
// aria-controls and aria-labelledby work as plain attributes. A tab and its
|
|
5
5
|
// panel share a `value`.
|
|
6
|
+
//
|
|
7
|
+
// Too many tabs for the width: the list scrolls sideways, fades the edge that
|
|
8
|
+
// hides tabs (the host carries data-overflow-start / data-overflow-end), turns
|
|
9
|
+
// a vertical wheel into a sideways scroll and keeps the selected tab in view.
|
|
6
10
|
|
|
7
11
|
import { RayElement, css, define, uid } from "../../core/core.js";
|
|
12
|
+
import { inlineKey } from "../../core/direction.js";
|
|
13
|
+
import { SideScroller, sideScrollerStyles } from "../../core/scroll.js";
|
|
8
14
|
import { applyColor } from "../../core/styles.js";
|
|
9
15
|
|
|
10
16
|
const tabsStyles = css`
|
|
11
17
|
:host { display: block; }
|
|
18
|
+
/* The size scale, inherited by the ray-tab children (own names: they reach
|
|
19
|
+
the panels' content too). */
|
|
20
|
+
:host { --_tab-h: var(--ray-control-height-sm); --_tab-fs: var(--ray-font-size-sm); --_tab-px: var(--ray-space-4); --_tab-gap: var(--ray-space-2); }
|
|
21
|
+
:host([size="xs"]) { --_tab-h: var(--ray-control-height-xs); --_tab-fs: var(--ray-font-size-xs); --_tab-px: var(--ray-space-3); --_tab-gap: var(--ray-space-1); }
|
|
22
|
+
:host([size="md"]) { --_tab-h: var(--ray-control-height-md); --_tab-fs: var(--ray-font-size-md); --_tab-px: var(--ray-space-5); }
|
|
23
|
+
:host([size="lg"]) { --_tab-h: var(--ray-control-height-lg); --_tab-fs: var(--ray-font-size-lg); --_tab-px: var(--ray-space-6); --_tab-gap: var(--ray-space-3); }
|
|
24
|
+
:host([size="xl"]) { --_tab-h: var(--ray-control-height-xl); --_tab-fs: var(--ray-font-size-xl); --_tab-px: var(--ray-space-8); --_tab-gap: var(--ray-space-3); }
|
|
12
25
|
:host([orientation="vertical"]) { display: flex; gap: var(--ray-space-4); }
|
|
13
26
|
/* Too many tabs for the width: the list scrolls sideways instead of
|
|
14
27
|
wrapping (a wrapped underline reads as two separate tab bars). The
|
|
@@ -24,9 +37,9 @@ const tabsStyles = css`
|
|
|
24
37
|
}
|
|
25
38
|
.list::-webkit-scrollbar { display: none; }
|
|
26
39
|
.list ::slotted(ray-tab) { flex: none; }
|
|
27
|
-
:host([orientation="vertical"]) .list { flex-direction: column; overflow-x: visible; box-shadow: none; border-
|
|
40
|
+
:host([orientation="vertical"]) .list { flex-direction: column; overflow-x: visible; box-shadow: none; border-inline-end: 2px solid var(--ray-border); }
|
|
28
41
|
:host([variant="outline"]) .list { --_line: 1px; }
|
|
29
|
-
:host([variant="outline"][orientation="vertical"]) .list { border-
|
|
42
|
+
:host([variant="outline"][orientation="vertical"]) .list { border-inline-end-width: 1px; }
|
|
30
43
|
:host([variant="pills"]) .list { gap: var(--ray-space-1); border: 0; box-shadow: none; }
|
|
31
44
|
:host([grow]) .list ::slotted(ray-tab) { flex: 1 0 auto; }
|
|
32
45
|
.panels { flex: 1; min-width: 0; padding-top: var(--ray-space-3); }
|
|
@@ -41,8 +54,9 @@ export class RayTabs extends RayElement {
|
|
|
41
54
|
color: { type: String, reflect: true },
|
|
42
55
|
activation: { type: String, default: "auto" },
|
|
43
56
|
grow: { type: Boolean, reflect: true },
|
|
57
|
+
size: { type: String, reflect: true, default: "sm" },
|
|
44
58
|
};
|
|
45
|
-
static styles = [tabsStyles];
|
|
59
|
+
static styles = [sideScrollerStyles, tabsStyles];
|
|
46
60
|
static template = `<div class="list" part="list" role="tablist"><slot name="tab"></slot></div><div class="panels" part="panels"><slot></slot></div>`;
|
|
47
61
|
|
|
48
62
|
constructor() {
|
|
@@ -55,6 +69,20 @@ export class RayTabs extends RayElement {
|
|
|
55
69
|
for (const slot of this.shadowRoot.querySelectorAll("slot")) {
|
|
56
70
|
slot.addEventListener("slotchange", () => this.requestUpdate());
|
|
57
71
|
}
|
|
72
|
+
this._scroller = new SideScroller(this, this.$(".list"), {
|
|
73
|
+
active: () => this.orientation !== "vertical",
|
|
74
|
+
onResize: () => this.requestUpdate(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
connectedCallback() {
|
|
79
|
+
super.connectedCallback();
|
|
80
|
+
this._scroller.connect();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
disconnectedCallback() {
|
|
84
|
+
super.disconnectedCallback();
|
|
85
|
+
this._scroller.disconnect();
|
|
58
86
|
}
|
|
59
87
|
|
|
60
88
|
get tabs() {
|
|
@@ -98,6 +126,11 @@ export class RayTabs extends RayElement {
|
|
|
98
126
|
panel.tabIndex = 0;
|
|
99
127
|
panel.hidden = panel.value !== this.value;
|
|
100
128
|
}
|
|
129
|
+
// Once laid out: a list with no width yet tries again on the next update.
|
|
130
|
+
if (this.value !== this._revealed && this._scroller.reveal(tabs.find((t) => t.value === this.value))) {
|
|
131
|
+
this._revealed = this.value;
|
|
132
|
+
}
|
|
133
|
+
this._scroller.update();
|
|
101
134
|
}
|
|
102
135
|
|
|
103
136
|
_select(tab, user) {
|
|
@@ -114,9 +147,11 @@ export class RayTabs extends RayElement {
|
|
|
114
147
|
const vertical = this.orientation === "vertical";
|
|
115
148
|
const next = vertical ? "ArrowDown" : "ArrowRight";
|
|
116
149
|
const prev = vertical ? "ArrowUp" : "ArrowLeft";
|
|
150
|
+
// Right to left, ArrowLeft is the next tab.
|
|
151
|
+
const key = vertical ? e.key : inlineKey(e.key, this);
|
|
117
152
|
let target = null;
|
|
118
|
-
if (
|
|
119
|
-
else if (
|
|
153
|
+
if (key === next) target = tabs[(i + 1) % tabs.length];
|
|
154
|
+
else if (key === prev) target = tabs[(i - 1 + tabs.length) % tabs.length];
|
|
120
155
|
else if (e.key === "Home") target = tabs[0];
|
|
121
156
|
else if (e.key === "End") target = tabs[tabs.length - 1];
|
|
122
157
|
else if ((e.key === "Enter" || e.key === " ") && this.activation === "manual") {
|
|
@@ -136,10 +171,10 @@ const tabStyles = css`
|
|
|
136
171
|
display: inline-flex;
|
|
137
172
|
align-items: center;
|
|
138
173
|
justify-content: center;
|
|
139
|
-
gap: var(--ray-space-2);
|
|
140
|
-
min-height: var(--ray-control-height-sm);
|
|
141
|
-
padding: 0 var(--ray-space-4);
|
|
142
|
-
font-size: var(--ray-font-size-sm);
|
|
174
|
+
gap: var(--_tab-gap, var(--ray-space-2));
|
|
175
|
+
min-height: var(--_tab-h, var(--ray-control-height-sm));
|
|
176
|
+
padding: 0 var(--_tab-px, var(--ray-space-4));
|
|
177
|
+
font-size: var(--_tab-fs, var(--ray-font-size-sm));
|
|
143
178
|
font-weight: var(--ray-font-weight-medium);
|
|
144
179
|
color: var(--ray-muted-foreground);
|
|
145
180
|
white-space: nowrap;
|
|
@@ -157,14 +192,14 @@ const tabStyles = css`
|
|
|
157
192
|
:host([data-variant="default"]) { border-bottom: 2px solid transparent; }
|
|
158
193
|
:host([data-variant="default"]:hover:not([data-active])) { border-bottom-color: var(--ray-border); background: var(--ray-muted); }
|
|
159
194
|
:host([data-variant="default"][data-active]) { border-bottom-color: var(--_filled, var(--ray-primary)); }
|
|
160
|
-
:host([data-variant="default"][data-vertical]) { margin: 0 -2px
|
|
161
|
-
:host([data-variant="default"][data-vertical][data-active]) { border-
|
|
195
|
+
:host([data-variant="default"][data-vertical]) { margin: 0; margin-inline-end: -2px; justify-content: flex-start; border-bottom: 0; border-inline-end: 2px solid transparent; }
|
|
196
|
+
:host([data-variant="default"][data-vertical][data-active]) { border-inline-end-color: var(--_filled, var(--ray-primary)); }
|
|
162
197
|
|
|
163
198
|
/* outline */
|
|
164
199
|
:host([data-variant="outline"]) { border: 1px solid transparent; border-radius: var(--ray-radius-default) var(--ray-radius-default) 0 0; }
|
|
165
200
|
:host([data-variant="outline"][data-active]) { border-color: var(--ray-border); border-bottom-color: var(--ray-background); background: var(--ray-background); }
|
|
166
|
-
:host([data-variant="outline"][data-vertical]) { margin: 0 -1px
|
|
167
|
-
:host([data-variant="outline"][data-vertical][data-active]) { border-bottom-color: var(--ray-border); border-
|
|
201
|
+
:host([data-variant="outline"][data-vertical]) { margin: 0; margin-inline-end: -1px; justify-content: flex-start; border-radius: 0; border-start-start-radius: var(--ray-radius-default); border-end-start-radius: var(--ray-radius-default); }
|
|
202
|
+
:host([data-variant="outline"][data-vertical][data-active]) { border-bottom-color: var(--ray-border); border-inline-end-color: var(--ray-background); }
|
|
168
203
|
|
|
169
204
|
/* pills */
|
|
170
205
|
:host([data-variant="pills"]) { border-radius: var(--ray-radius-default); }
|
|
@@ -174,15 +209,20 @@ const tabStyles = css`
|
|
|
174
209
|
.section { display: inline-flex; }
|
|
175
210
|
.section:not([data-filled]) { display: none; }
|
|
176
211
|
::slotted(svg) { width: 1rem; height: 1rem; }
|
|
212
|
+
/* count: a small number after the label, lighter than a badge. */
|
|
213
|
+
.count { font-size: 0.85em; font-variant-numeric: tabular-nums; color: var(--ray-muted-foreground); }
|
|
214
|
+
.count:empty { display: none; }
|
|
215
|
+
:host([data-active]) .count, :host(:hover) .count { color: inherit; opacity: 0.75; }
|
|
177
216
|
`;
|
|
178
217
|
|
|
179
218
|
export class RayTab extends RayElement {
|
|
180
219
|
static properties = {
|
|
181
220
|
value: { type: String, reflect: true },
|
|
182
221
|
disabled: { type: Boolean, reflect: true },
|
|
222
|
+
count: { type: String, reflect: true },
|
|
183
223
|
};
|
|
184
224
|
static styles = [tabStyles];
|
|
185
|
-
static template = `<span class="section" part="left-section"><slot name="left"></slot></span><slot></slot><span class="section" part="right-section"><slot name="right"></slot></span>`;
|
|
225
|
+
static template = `<span class="section" part="left-section"><slot name="left"></slot></span><slot></slot><span class="count" part="count"></span><span class="section" part="right-section"><slot name="right"></slot></span>`;
|
|
186
226
|
|
|
187
227
|
constructor() {
|
|
188
228
|
super();
|
|
@@ -198,6 +238,7 @@ export class RayTab extends RayElement {
|
|
|
198
238
|
}
|
|
199
239
|
|
|
200
240
|
update(changed) {
|
|
241
|
+
this.$(".count").textContent = this.count ?? "";
|
|
201
242
|
if (changed.has("value") || changed.has("disabled")) this.closest("ray-tabs")?.requestUpdate();
|
|
202
243
|
}
|
|
203
244
|
}
|