@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.
Files changed (95) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +12 -2
  4. package/base.css +111 -0
  5. package/components/accordion/accordion.js +197 -0
  6. package/components/alert/alert.js +112 -0
  7. package/components/app-bar/app-bar.js +168 -0
  8. package/components/app-shell/app-shell.js +198 -0
  9. package/components/aspect-ratio/aspect-ratio.js +59 -0
  10. package/components/attachment/attachment.js +333 -0
  11. package/components/avatar/avatar.js +96 -0
  12. package/components/badge/badge.js +63 -0
  13. package/components/blockquote/blockquote.js +128 -0
  14. package/components/bottom-nav/bottom-nav.js +309 -0
  15. package/components/breadcrumbs/breadcrumbs.js +175 -0
  16. package/components/button/button.js +283 -0
  17. package/components/button-group/button-group.js +72 -0
  18. package/components/card/card.js +73 -0
  19. package/components/carousel/carousel.js +435 -0
  20. package/components/center/center.js +26 -0
  21. package/components/checkbox/checkbox.js +152 -0
  22. package/components/checkbox-group/checkbox-group.js +152 -0
  23. package/components/code/code.js +574 -0
  24. package/components/color-picker/color-picker.js +775 -0
  25. package/components/combobox/combobox.js +563 -0
  26. package/components/command-palette/command-palette.js +510 -0
  27. package/components/container/container.js +41 -0
  28. package/components/date-picker/date-picker.js +594 -0
  29. package/components/dialog/dialog.js +329 -0
  30. package/components/drawer/drawer.js +139 -0
  31. package/components/empty-state/empty-state.js +66 -0
  32. package/components/file-upload/file-upload.js +589 -0
  33. package/components/grid/grid.js +96 -0
  34. package/components/group/group.js +40 -0
  35. package/components/icon/icon.js +58 -0
  36. package/components/icon-button/icon-button.js +30 -0
  37. package/components/indicator/indicator.js +112 -0
  38. package/components/input/input.js +85 -0
  39. package/components/kbd/kbd.js +60 -0
  40. package/components/menu/menu.js +296 -0
  41. package/components/nav-link/nav-link.js +159 -0
  42. package/components/notification/notification.js +591 -0
  43. package/components/number-input/number-input.js +241 -0
  44. package/components/pagination/pagination.js +194 -0
  45. package/components/password-input/password-input.js +85 -0
  46. package/components/path-input/path-input.js +137 -0
  47. package/components/popover/popover.js +85 -0
  48. package/components/progress/progress.js +109 -0
  49. package/components/radio/radio.js +253 -0
  50. package/components/scroll-area/scroll-area.js +158 -0
  51. package/components/search/search.js +157 -0
  52. package/components/segmented-control/segmented-control.js +190 -0
  53. package/components/select/select.js +506 -0
  54. package/components/separator/separator.js +58 -0
  55. package/components/sidebar/sidebar.js +252 -0
  56. package/components/skeleton/skeleton.js +90 -0
  57. package/components/slider/slider.js +155 -0
  58. package/components/spinner/spinner.js +39 -0
  59. package/components/split/split.js +221 -0
  60. package/components/stack/stack.js +46 -0
  61. package/components/stat/stat.js +79 -0
  62. package/components/statusbar/statusbar.js +110 -0
  63. package/components/stepper/stepper.js +239 -0
  64. package/components/switch/switch.js +45 -0
  65. package/components/table/table.js +328 -0
  66. package/components/tabs/tabs.js +224 -0
  67. package/components/textarea/textarea.js +105 -0
  68. package/components/theme-toggle/theme-toggle.js +78 -0
  69. package/components/timeline/timeline.js +150 -0
  70. package/components/toggle/toggle.js +130 -0
  71. package/components/toggle-group/toggle-group.js +216 -0
  72. package/components/tooltip/tooltip.js +101 -0
  73. package/core/charts.js +107 -0
  74. package/core/color.js +110 -0
  75. package/core/core.js +556 -0
  76. package/core/dismiss.js +69 -0
  77. package/core/floating.js +195 -0
  78. package/core/focus.js +70 -0
  79. package/core/form.js +235 -0
  80. package/core/host.js +200 -0
  81. package/core/hotkey.js +40 -0
  82. package/core/icons.js +104 -0
  83. package/core/motion.js +73 -0
  84. package/core/position.js +91 -0
  85. package/core/raylang.js +167 -0
  86. package/core/scroll.js +58 -0
  87. package/core/styles.js +211 -0
  88. package/core.js +16 -0
  89. package/custom-elements.json +18545 -0
  90. package/index.js +74 -0
  91. package/package.json +312 -4
  92. package/ray-elements.d.ts +2570 -0
  93. package/tokens.css +796 -0
  94. package/tokens.json +2083 -0
  95. package/vscode.html-custom-data.json +5499 -0
@@ -0,0 +1,309 @@
1
+ // <ray-bottom-nav>: the bottom navigation bar of a phone app (Material's
2
+ // navigation bar, the iOS tab bar): three to five destinations, an icon and
3
+ // a label each, the current one marked by a pill behind its icon.
4
+ //
5
+ // Links (<ray-bottom-nav-item href>) mark the page they lead to
6
+ // (aria-current="page"); without href it is a control: `value` and
7
+ // `change`, like a segmented control. An item with `action` is a button
8
+ // (a prominent "New" in the middle): it fires ray-select, it is not a
9
+ // destination.
10
+ //
11
+ // hide-on-scroll slides it away while the content scrolls down (the
12
+ // scroller of its ray-app-shell, `scroll-target` or the window); `fixed`
13
+ // pins it to the bottom of the window. Clear of the home indicator
14
+ // (safe-area inset).
15
+
16
+ import { RayElement, css, define } from "../../core/core.js";
17
+ import { applyColor } from "../../core/styles.js";
18
+ import { scrollerOf, watchScroll } from "../../core/scroll.js";
19
+
20
+ // An item's value: its own, else its href, else its label.
21
+ const valueOf = (item) => item.value ?? item.href ?? item.label;
22
+
23
+ const navStyles = css`
24
+ :host {
25
+ display: grid;
26
+ grid-template-rows: 1fr;
27
+ position: relative;
28
+ z-index: 2;
29
+ transition: grid-template-rows var(--ray-duration-base) var(--ray-easing-standard), translate var(--ray-duration-base) var(--ray-easing-standard);
30
+ }
31
+ :host([hidden]) { display: none; }
32
+ :host([fixed]) { position: fixed; inset: auto 0 0 0; }
33
+ /* Its border, padding and raised action would still show: clip them. */
34
+ :host([data-concealed="flow"]) { grid-template-rows: 0fr; clip-path: inset(0); }
35
+ :host([data-concealed="over"]) { translate: 0 100%; }
36
+ .fold { min-height: 0; }
37
+ nav {
38
+ box-sizing: border-box;
39
+ display: grid;
40
+ grid-auto-columns: minmax(0, 1fr);
41
+ grid-auto-flow: column;
42
+ align-items: stretch;
43
+ padding: var(--ray-space-1) max(var(--ray-space-1), env(safe-area-inset-right, 0px)) max(var(--ray-space-1), env(safe-area-inset-bottom, 0px)) max(var(--ray-space-1), env(safe-area-inset-left, 0px));
44
+ background: var(--ray-bottom-nav, var(--ray-background));
45
+ border-top: 1px solid var(--ray-border);
46
+ }
47
+ :host([variant="floating"]) { padding: 0 var(--ray-space-3) max(var(--ray-space-3), env(safe-area-inset-bottom, 0px)); }
48
+ :host([variant="floating"][data-concealed="flow"]) { padding-bottom: 0; }
49
+ :host([variant="floating"]) nav { padding-bottom: var(--ray-space-1); border: 1px solid var(--ray-border); border-radius: var(--ray-radius-xl); box-shadow: var(--ray-shadow-lg); overflow: visible; }
50
+ @media (prefers-reduced-motion: reduce) { :host { transition: none; } }
51
+ `;
52
+
53
+ const itemStyles = css`
54
+ :host {
55
+ display: block;
56
+ min-width: 0;
57
+ }
58
+ :host([hidden]) { display: none; }
59
+ .slot-host { display: contents; }
60
+ .item {
61
+ box-sizing: border-box;
62
+ display: flex;
63
+ flex-direction: column;
64
+ align-items: center;
65
+ justify-content: center;
66
+ gap: 0.25rem;
67
+ width: 100%;
68
+ min-height: 3.5rem;
69
+ padding: 0.375rem 0.25rem;
70
+ margin: 0;
71
+ font: inherit;
72
+ color: var(--ray-muted-foreground);
73
+ text-decoration: none;
74
+ background: none;
75
+ border: 0;
76
+ border-radius: var(--ray-radius-md);
77
+ cursor: pointer;
78
+ -webkit-tap-highlight-color: transparent;
79
+ transition: color var(--ray-duration-fast);
80
+ }
81
+ .item:focus-visible { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: -2px; }
82
+ .item:hover { color: var(--ray-foreground); }
83
+ :host([active]) .item { color: var(--_light-color); }
84
+ :host([disabled]) .item { opacity: 0.45; cursor: not-allowed; }
85
+ .icon-wrap {
86
+ position: relative;
87
+ display: grid;
88
+ place-items: center;
89
+ width: 3.5rem;
90
+ height: 2rem;
91
+ border-radius: var(--ray-radius-full);
92
+ --ray-icon-size: 1.375rem;
93
+ --ray-icon-stroke: 1.75;
94
+ }
95
+ .pill {
96
+ position: absolute;
97
+ inset: 0;
98
+ border-radius: inherit;
99
+ background: var(--_light);
100
+ opacity: 0;
101
+ scale: 0.6 1;
102
+ transition: opacity var(--ray-duration-fast), scale var(--ray-duration-base) var(--ray-easing-standard);
103
+ }
104
+ :host([active]) .pill { opacity: 1; scale: 1; }
105
+ :host([data-indicator="none"]) .pill { display: none; }
106
+ :host([data-indicator="line"]) .pill { inset: auto 1rem -0.25rem; height: 0.1875rem; background: var(--_filled); }
107
+ .icon, ::slotted([slot="icon"]) { position: relative; }
108
+ .icon:empty { display: none; }
109
+ .badge {
110
+ position: absolute;
111
+ top: 0;
112
+ left: calc(50% + 0.375rem);
113
+ min-width: 1rem;
114
+ height: 1rem;
115
+ padding: 0 0.25rem;
116
+ box-sizing: border-box;
117
+ font-size: 0.625rem;
118
+ font-weight: 700;
119
+ line-height: 1rem;
120
+ text-align: center;
121
+ color: #fff;
122
+ background: var(--ray-red-filled);
123
+ border: 2px solid var(--ray-bottom-nav, var(--ray-background));
124
+ border-radius: var(--ray-radius-full);
125
+ box-sizing: content-box;
126
+ }
127
+ .badge:empty:not([data-dot]) { display: none; }
128
+ .badge[data-dot] { min-width: 0; width: 0.5rem; height: 0.5rem; padding: 0; top: 0.25rem; left: calc(50% + 0.5rem); }
129
+ .label {
130
+ max-width: 100%;
131
+ overflow: hidden;
132
+ text-overflow: ellipsis;
133
+ white-space: nowrap;
134
+ font-size: var(--ray-font-size-xs);
135
+ font-weight: var(--ray-font-weight-medium, 500);
136
+ line-height: 1.2;
137
+ }
138
+ :host([active]) .label { font-weight: 700; }
139
+ :host([data-labels="active"]:not([active])) .label, :host([data-labels="never"]) .label {
140
+ position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
141
+ }
142
+ /* action: a raised round button in the bar. */
143
+ :host([action]) .icon-wrap {
144
+ width: 3.25rem;
145
+ height: 3.25rem;
146
+ margin-top: -1.25rem;
147
+ color: var(--_contrast);
148
+ background: var(--_filled);
149
+ box-shadow: var(--ray-shadow-md);
150
+ transition: scale var(--ray-duration-fast);
151
+ --ray-icon-size: 1.5rem;
152
+ --ray-icon-stroke: 2;
153
+ }
154
+ :host([action]) .item:active .icon-wrap { scale: 0.94; }
155
+ :host([action]) .pill { display: none; }
156
+ :host([action]) .item { color: var(--ray-foreground); }
157
+ @media (prefers-reduced-motion: reduce) { .pill { transition: none; } }
158
+ `;
159
+
160
+ export class RayBottomNavItem extends RayElement {
161
+ static properties = {
162
+ value: { type: String, reflect: true },
163
+ label: { type: String },
164
+ icon: { type: String },
165
+ href: { type: String, reflect: true },
166
+ badge: { type: String },
167
+ dot: { type: Boolean, reflect: true },
168
+ active: { type: Boolean, reflect: true },
169
+ disabled: { type: Boolean, reflect: true },
170
+ action: { type: Boolean, reflect: true },
171
+ };
172
+ static styles = [itemStyles];
173
+ static delegatesFocus = true;
174
+ static template = `<span class="slot-host"></span>`;
175
+
176
+ #tag = "";
177
+
178
+ update() {
179
+ // A link for a destination with href, a button otherwise.
180
+ const tag = this.href && !this.action ? "a" : "button";
181
+ if (tag !== this.#tag) {
182
+ this.#tag = tag;
183
+ const host = this.$(".slot-host");
184
+ host.innerHTML = `<${tag} class="item" part="item"><span class="icon-wrap"><span class="pill" part="indicator"></span><span class="icon" data-ray-icon=""></span><slot name="icon"></slot><span class="badge" part="badge"></span></span><span class="label" part="label"></span></${tag}>`;
185
+ if (tag === "button") host.firstElementChild.type = "button";
186
+ }
187
+ const item = this.$(".item");
188
+ if (tag === "a") {
189
+ if (this.disabled) item.removeAttribute("href");
190
+ else item.setAttribute("href", this.href);
191
+ } else {
192
+ item.disabled = Boolean(this.disabled);
193
+ }
194
+ if (this.active && !this.action) item.setAttribute("aria-current", "page");
195
+ else item.removeAttribute("aria-current");
196
+ const icon = this.$(".icon");
197
+ icon.setAttribute("data-ray-icon", this.icon ?? "");
198
+ this.paintIcons();
199
+ this.$(".label").textContent = this.label ?? "";
200
+ const badge = this.$(".badge");
201
+ badge.textContent = this.dot ? "" : (this.badge ?? "");
202
+ badge.toggleAttribute("data-dot", Boolean(this.dot));
203
+ // The badge is read with the label ("Messages, 3").
204
+ if (this.badge && !this.dot) item.setAttribute("aria-label", `${this.label ?? ""}, ${this.badge}`);
205
+ else if (this.dot) item.setAttribute("aria-label", `${this.label ?? ""}, new`);
206
+ else item.removeAttribute("aria-label");
207
+ }
208
+ }
209
+
210
+ export class RayBottomNav extends RayElement {
211
+ static properties = {
212
+ value: { type: String, reflect: true },
213
+ label: { type: String, default: "Main" },
214
+ labels: { type: String, reflect: true, default: "always" },
215
+ indicator: { type: String, reflect: true, default: "pill" },
216
+ color: { type: String, reflect: true },
217
+ variant: { type: String, reflect: true, default: "default" },
218
+ hideOnScroll: { type: Boolean, reflect: true, attribute: "hide-on-scroll" },
219
+ fixed: { type: Boolean, reflect: true },
220
+ scrollTarget: { type: String, attribute: "scroll-target" },
221
+ };
222
+ static styles = [navStyles];
223
+ // The row that closes is .fold (no padding, no border: it can reach 0).
224
+ static template = `<div class="fold"><nav part="nav"><slot></slot></nav></div>`;
225
+
226
+ #stop = null;
227
+ #lock = 0;
228
+
229
+ constructor() {
230
+ super();
231
+ this.$("slot").addEventListener("slotchange", () => this._sync());
232
+ this.addEventListener("click", (e) => {
233
+ const item = e.target.closest?.("ray-bottom-nav-item");
234
+ if (!item || item.parentElement !== this || item.disabled) return;
235
+ if (item.action) {
236
+ this.emit("ray-select", { value: valueOf(item) ?? "" });
237
+ return;
238
+ }
239
+ const value = valueOf(item);
240
+ if (value !== undefined && value !== this.value) {
241
+ this.commitProperty("value", value);
242
+ this.dispatchEvent(new Event("change", { bubbles: true }));
243
+ }
244
+ });
245
+ }
246
+
247
+ /** The destinations and actions, in order. */
248
+ get items() {
249
+ return [...this.children].filter((el) => el.localName === "ray-bottom-nav-item");
250
+ }
251
+
252
+ connectedCallback() {
253
+ super.connectedCallback();
254
+ queueMicrotask(() => this._watch());
255
+ }
256
+
257
+ disconnectedCallback() {
258
+ super.disconnectedCallback();
259
+ this.#stop?.();
260
+ this.#stop = null;
261
+ }
262
+
263
+ update(changed) {
264
+ this.$("nav").setAttribute("aria-label", this.label || "Main");
265
+ this._sync();
266
+ if (changed.has("hideOnScroll") || changed.has("scrollTarget") || changed.has("fixed")) this._watch();
267
+ }
268
+
269
+ // Marks the current item. Without a value: the link to this page, or
270
+ // the first destination of a control.
271
+ _sync() {
272
+ const items = this.items;
273
+ if (this.value === undefined) {
274
+ const here = items.find((i) => i.href && !i.action && new URL(i.href, document.baseURI).pathname === location.pathname);
275
+ if (here) this.commitProperty("value", valueOf(here));
276
+ // A control (no links) starts on its first destination.
277
+ else if (items.length && !items.some((i) => i.href)) {
278
+ const first = items.find((i) => !i.action && !i.disabled);
279
+ if (first) this.commitProperty("value", valueOf(first));
280
+ }
281
+ }
282
+ for (const item of items) {
283
+ item.active = !item.action && valueOf(item) !== undefined && valueOf(item) === this.value;
284
+ item.setAttribute("data-labels", this.labels || "always");
285
+ item.setAttribute("data-indicator", this.indicator || "pill");
286
+ applyColor(item, this.color);
287
+ }
288
+ }
289
+
290
+ _watch() {
291
+ this.#stop?.();
292
+ this.#stop = null;
293
+ this.removeAttribute("data-concealed");
294
+ if (!this.isConnected || !this.hideOnScroll) return;
295
+ const scroller = scrollerOf(this, this.scrollTarget);
296
+ const mode = this.fixed || scroller === window ? "over" : "flow";
297
+ this.#stop = watchScroll(scroller, ({ y, direction }) => {
298
+ const on = direction === "down" && y > 24;
299
+ if (y <= 0) { this.removeAttribute("data-concealed"); return; }
300
+ if (on === this.hasAttribute("data-concealed") || performance.now() < this.#lock) return;
301
+ this.#lock = performance.now() + 400;
302
+ if (on) this.setAttribute("data-concealed", mode);
303
+ else this.removeAttribute("data-concealed");
304
+ });
305
+ }
306
+ }
307
+
308
+ define("ray-bottom-nav-item", RayBottomNavItem);
309
+ define("ray-bottom-nav", RayBottomNav);
@@ -0,0 +1,175 @@
1
+ // <ray-breadcrumbs> + <ray-breadcrumb>: where a page sits (Mantine's
2
+ // Breadcrumbs, the WAI-ARIA breadcrumb pattern): a labelled nav of links,
3
+ // the last one the current page (aria-current="page").
4
+ //
5
+ // separator: any text ("/", "›", "·") or "chevron" (an icon). max-items
6
+ // collapses the middle of a long path into a "…" button that expands it.
7
+
8
+ import { RayElement, css, define } from "../../core/core.js";
9
+
10
+ const trailStyles = css`
11
+ :host { display: block; min-width: 0; }
12
+ :host([hidden]) { display: none; }
13
+ ol { display: flex; flex-wrap: wrap; align-items: center; gap: 0; margin: 0; padding: 0; list-style: none; }
14
+ `;
15
+
16
+ const itemStyles = css`
17
+ :host { display: inline-flex; align-items: center; min-width: 0; font-size: var(--_fs, var(--ray-font-size-sm)); }
18
+ :host([hidden]), :host([data-collapsed]) { display: none; }
19
+ .sep {
20
+ display: inline-flex;
21
+ align-items: center;
22
+ padding: 0 var(--ray-space-2);
23
+ color: var(--ray-muted-foreground);
24
+ user-select: none;
25
+ }
26
+ .sep svg { width: 0.9em; height: 0.9em; }
27
+ :host([data-first]) .sep.before { display: none; }
28
+ .more, .after { display: none; }
29
+ :host([data-after-more]) .more, :host([data-after-more]) .after { display: inline-flex; }
30
+ .more {
31
+ align-items: center;
32
+ justify-content: center;
33
+ min-width: 1.75rem;
34
+ height: 1.5rem;
35
+ padding: 0 0.25rem;
36
+ font: inherit;
37
+ letter-spacing: 0.1em;
38
+ color: var(--ray-muted-foreground);
39
+ background: transparent;
40
+ border: 0;
41
+ border-radius: var(--ray-radius-sm);
42
+ cursor: pointer;
43
+ }
44
+ .more:hover { color: var(--ray-foreground); background: var(--ray-default-hover); }
45
+ .more:focus-visible, .link:focus-visible { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: 2px; }
46
+ .link {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ gap: var(--ray-space-1);
50
+ min-width: 0;
51
+ max-width: var(--ray-breadcrumb-max, 16rem);
52
+ color: var(--ray-muted-foreground);
53
+ text-decoration: none;
54
+ border-radius: var(--ray-radius-xs);
55
+ --ray-icon-size: 1em;
56
+ }
57
+ .text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
58
+ a.link:hover { color: var(--ray-foreground); text-decoration: underline; text-underline-offset: 0.2em; }
59
+ :host([data-current]) .link { color: var(--ray-foreground); font-weight: var(--ray-font-weight-medium); }
60
+ `;
61
+
62
+ export class RayBreadcrumbs extends RayElement {
63
+ static properties = {
64
+ separator: { type: String, default: "/" },
65
+ maxItems: { type: Number, attribute: "max-items" },
66
+ label: { type: String, default: "Breadcrumb" },
67
+ size: { type: String, reflect: true, default: "sm" },
68
+ };
69
+ static styles = [trailStyles];
70
+ static template = `<nav part="nav"><ol part="list"><slot></slot></ol></nav>`;
71
+
72
+ #expanded = false;
73
+
74
+ constructor() {
75
+ super();
76
+ this.$("slot").addEventListener("slotchange", () => this.requestUpdate());
77
+ this.addEventListener("ray-breadcrumbs-expand", (e) => {
78
+ e.stopPropagation();
79
+ this.expand();
80
+ });
81
+ }
82
+
83
+ /** The crumbs, in order. */
84
+ get items() {
85
+ return [...this.children].filter((el) => el.localName === "ray-breadcrumb");
86
+ }
87
+
88
+ /** Shows every crumb of a collapsed path. */
89
+ expand() {
90
+ this.#expanded = true;
91
+ this.requestUpdate();
92
+ // Focus moves to the first crumb that was hidden.
93
+ queueMicrotask(() => this.items[1]?.focus());
94
+ }
95
+
96
+ update() {
97
+ this.$("nav").setAttribute("aria-label", this.label || "Breadcrumb");
98
+ const sizes = { xs: "var(--ray-font-size-xs)", sm: "var(--ray-font-size-sm)", md: "var(--ray-font-size-md)", lg: "var(--ray-font-size-lg)" };
99
+ this.style.setProperty("--_fs", sizes[this.size] ?? sizes.sm);
100
+ const items = this.items;
101
+ const explicit = items.some((i) => i.current);
102
+ const max = this.maxItems ?? 0;
103
+ // Collapsed: the first crumb, "…", then the last (max - 1).
104
+ const collapse = !this.#expanded && max >= 2 && items.length > max;
105
+ const hiddenUntil = collapse ? items.length - (max - 1) : 0;
106
+ items.forEach((item, i) => {
107
+ item.toggleAttribute("data-first", i === 0);
108
+ item.toggleAttribute("data-current", explicit ? Boolean(item.current) : i === items.length - 1);
109
+ item.toggleAttribute("data-collapsed", collapse && i > 0 && i < hiddenUntil);
110
+ item.toggleAttribute("data-after-more", collapse && i === hiddenUntil);
111
+ item._setSeparator(this.separator || "/");
112
+ });
113
+ }
114
+ }
115
+
116
+ export class RayBreadcrumb extends RayElement {
117
+ static properties = {
118
+ href: { type: String, reflect: true },
119
+ current: { type: Boolean, reflect: true },
120
+ };
121
+ static styles = [itemStyles];
122
+ static delegatesFocus = true;
123
+ static template = `<span class="sep before" part="separator" aria-hidden="true"></span><button class="more" type="button" part="more" aria-label="Show the whole path">…</button><span class="sep after" aria-hidden="true"></span><span class="slot-host"></span>`;
124
+
125
+ #tag = "";
126
+ #sep = "";
127
+
128
+ static get observedAttributes() {
129
+ return [...super.observedAttributes, "data-current"];
130
+ }
131
+
132
+ attributeChangedCallback(attr, old, value) {
133
+ if (attr === "data-current") return this.requestUpdate();
134
+ super.attributeChangedCallback(attr, old, value);
135
+ }
136
+
137
+ constructor() {
138
+ super();
139
+ this.setAttribute("role", "listitem");
140
+ this.$(".more").addEventListener("click", () => this.dispatchEvent(new CustomEvent("ray-breadcrumbs-expand", { bubbles: true })));
141
+ }
142
+
143
+ connectedCallback() {
144
+ super.connectedCallback();
145
+ this.setAttribute("role", "listitem");
146
+ }
147
+
148
+ /** Protected: the separator the trail draws before this crumb. */
149
+ _setSeparator(sep) {
150
+ if (sep === this.#sep) return;
151
+ this.#sep = sep;
152
+ for (const el of this.shadowRoot.querySelectorAll(".sep")) {
153
+ if (sep === "chevron") el.innerHTML = `<span data-ray-icon="chevron-right"></span>`;
154
+ else el.textContent = sep;
155
+ }
156
+ this.paintIcons();
157
+ }
158
+
159
+ update() {
160
+ const current = this.hasAttribute("data-current");
161
+ // The current page and a crumb without href are text, the others links.
162
+ const tag = this.href && !current ? "a" : "span";
163
+ if (tag !== this.#tag) {
164
+ this.#tag = tag;
165
+ this.$(".slot-host").innerHTML = `<${tag} class="link" part="link"><slot name="icon"></slot><span class="text"><slot></slot></span></${tag}>`;
166
+ }
167
+ const link = this.$(".link");
168
+ if (tag === "a") link.setAttribute("href", this.href);
169
+ if (current) link.setAttribute("aria-current", "page");
170
+ else link.removeAttribute("aria-current");
171
+ }
172
+ }
173
+
174
+ define("ray-breadcrumb", RayBreadcrumb);
175
+ define("ray-breadcrumbs", RayBreadcrumbs);