@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.
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,224 @@
1
+ // <ray-tabs> + <ray-tab> + <ray-tab-panel> (WAI-ARIA tabs pattern).
2
+ //
3
+ // Tabs and panels are light-DOM children, so their roles, ids,
4
+ // aria-controls and aria-labelledby work as plain attributes. A tab and its
5
+ // panel share a `value`.
6
+
7
+ import { RayElement, css, define, uid } from "../../core/core.js";
8
+ import { applyColor } from "../../core/styles.js";
9
+
10
+ const tabsStyles = css`
11
+ :host { display: block; }
12
+ :host([orientation="vertical"]) { display: flex; gap: var(--ray-space-4); }
13
+ /* Too many tabs for the width: the list scrolls sideways instead of
14
+ wrapping (a wrapped underline reads as two separate tab bars). The
15
+ baseline is an inset shadow, so a tab's own bottom border paints over
16
+ it without a negative margin that the scroller would clip. */
17
+ .list {
18
+ --_line: 2px;
19
+ display: flex;
20
+ gap: 0;
21
+ overflow-x: auto;
22
+ scrollbar-width: none;
23
+ box-shadow: inset 0 calc(-1 * var(--_line)) 0 var(--ray-border);
24
+ }
25
+ .list::-webkit-scrollbar { display: none; }
26
+ .list ::slotted(ray-tab) { flex: none; }
27
+ :host([orientation="vertical"]) .list { flex-direction: column; overflow-x: visible; box-shadow: none; border-right: 2px solid var(--ray-border); }
28
+ :host([variant="outline"]) .list { --_line: 1px; }
29
+ :host([variant="outline"][orientation="vertical"]) .list { border-right-width: 1px; }
30
+ :host([variant="pills"]) .list { gap: var(--ray-space-1); border: 0; box-shadow: none; }
31
+ :host([grow]) .list ::slotted(ray-tab) { flex: 1 0 auto; }
32
+ .panels { flex: 1; min-width: 0; padding-top: var(--ray-space-3); }
33
+ :host([orientation="vertical"]) .panels { padding-top: 0; }
34
+ `;
35
+
36
+ export class RayTabs extends RayElement {
37
+ static properties = {
38
+ value: { type: String },
39
+ orientation: { type: String, reflect: true, default: "horizontal" },
40
+ variant: { type: String, reflect: true, default: "default" },
41
+ color: { type: String, reflect: true },
42
+ activation: { type: String, default: "auto" },
43
+ grow: { type: Boolean, reflect: true },
44
+ };
45
+ static styles = [tabsStyles];
46
+ static template = `<div class="list" part="list" role="tablist"><slot name="tab"></slot></div><div class="panels" part="panels"><slot></slot></div>`;
47
+
48
+ constructor() {
49
+ super();
50
+ this.addEventListener("click", (e) => {
51
+ const tab = e.target.closest?.("ray-tab");
52
+ if (tab && this.tabs.includes(tab) && !tab.disabled) this._select(tab, true);
53
+ });
54
+ this.addEventListener("keydown", (e) => this._key(e));
55
+ for (const slot of this.shadowRoot.querySelectorAll("slot")) {
56
+ slot.addEventListener("slotchange", () => this.requestUpdate());
57
+ }
58
+ }
59
+
60
+ get tabs() {
61
+ return [...this.querySelectorAll("ray-tab")].filter((t) => t.closest("ray-tabs") === this);
62
+ }
63
+
64
+ get panels() {
65
+ return [...this.querySelectorAll("ray-tab-panel")].filter((p) => p.closest("ray-tabs") === this);
66
+ }
67
+
68
+ update() {
69
+ const tabs = this.tabs;
70
+ const panels = this.panels;
71
+ if (this.value === undefined || !tabs.some((t) => t.value === this.value && !t.disabled)) {
72
+ const first = tabs.find((t) => !t.disabled);
73
+ if (first) this.commitProperty("value", first.value);
74
+ }
75
+ const vertical = this.orientation === "vertical";
76
+ this.$(".list").setAttribute("aria-orientation", vertical ? "vertical" : "horizontal");
77
+ applyColor(this, this.color);
78
+ for (const tab of tabs) {
79
+ const panel = panels.find((p) => p.value === tab.value);
80
+ const active = tab.value === this.value;
81
+ tab.id ||= uid("tab");
82
+ tab.setAttribute("role", "tab");
83
+ tab.setAttribute("aria-selected", String(active));
84
+ tab.tabIndex = active ? 0 : -1;
85
+ tab.toggleAttribute("data-active", active);
86
+ tab.dataset.variant = this.variant || "default";
87
+ tab.toggleAttribute("data-vertical", vertical);
88
+ if (tab.disabled) tab.setAttribute("aria-disabled", "true");
89
+ else tab.removeAttribute("aria-disabled");
90
+ if (panel) {
91
+ panel.id ||= uid("tabpanel");
92
+ tab.setAttribute("aria-controls", panel.id);
93
+ panel.setAttribute("aria-labelledby", tab.id);
94
+ }
95
+ }
96
+ for (const panel of panels) {
97
+ panel.setAttribute("role", "tabpanel");
98
+ panel.tabIndex = 0;
99
+ panel.hidden = panel.value !== this.value;
100
+ }
101
+ }
102
+
103
+ _select(tab, user) {
104
+ const changed = tab.value !== this.value;
105
+ this.value = tab.value;
106
+ if (user && changed) this.dispatchEvent(new Event("change", { bubbles: true }));
107
+ }
108
+
109
+ _key(e) {
110
+ const tab = e.target.closest?.("ray-tab");
111
+ const tabs = this.tabs.filter((t) => !t.disabled);
112
+ const i = tabs.indexOf(tab);
113
+ if (i < 0) return;
114
+ const vertical = this.orientation === "vertical";
115
+ const next = vertical ? "ArrowDown" : "ArrowRight";
116
+ const prev = vertical ? "ArrowUp" : "ArrowLeft";
117
+ let target = null;
118
+ if (e.key === next) target = tabs[(i + 1) % tabs.length];
119
+ else if (e.key === prev) target = tabs[(i - 1 + tabs.length) % tabs.length];
120
+ else if (e.key === "Home") target = tabs[0];
121
+ else if (e.key === "End") target = tabs[tabs.length - 1];
122
+ else if ((e.key === "Enter" || e.key === " ") && this.activation === "manual") {
123
+ e.preventDefault();
124
+ this._select(tab, true);
125
+ return;
126
+ }
127
+ if (!target) return;
128
+ e.preventDefault();
129
+ target.focus();
130
+ if (this.activation !== "manual") this._select(target, true);
131
+ }
132
+ }
133
+
134
+ const tabStyles = css`
135
+ :host {
136
+ display: inline-flex;
137
+ align-items: center;
138
+ 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);
143
+ font-weight: var(--ray-font-weight-medium);
144
+ color: var(--ray-muted-foreground);
145
+ white-space: nowrap;
146
+ cursor: pointer;
147
+ user-select: none;
148
+ outline: none;
149
+ transition: color var(--ray-duration-fast) var(--ray-easing-standard), background-color var(--ray-duration-fast) var(--ray-easing-standard), border-color var(--ray-duration-fast) var(--ray-easing-standard);
150
+ }
151
+ :host(:focus-visible) { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: -2px; border-radius: var(--ray-radius-sm); }
152
+ :host(:hover) { color: var(--ray-foreground); }
153
+ :host([data-active]) { color: var(--ray-foreground); }
154
+ :host([disabled]) { opacity: 0.5; cursor: not-allowed; }
155
+
156
+ /* default: underline */
157
+ :host([data-variant="default"]) { border-bottom: 2px solid transparent; }
158
+ :host([data-variant="default"]:hover:not([data-active])) { border-bottom-color: var(--ray-border); background: var(--ray-muted); }
159
+ :host([data-variant="default"][data-active]) { border-bottom-color: var(--_filled, var(--ray-primary)); }
160
+ :host([data-variant="default"][data-vertical]) { margin: 0 -2px 0 0; justify-content: flex-start; border-bottom: 0; border-right: 2px solid transparent; }
161
+ :host([data-variant="default"][data-vertical][data-active]) { border-right-color: var(--_filled, var(--ray-primary)); }
162
+
163
+ /* outline */
164
+ :host([data-variant="outline"]) { border: 1px solid transparent; border-radius: var(--ray-radius-default) var(--ray-radius-default) 0 0; }
165
+ :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 0 0; justify-content: flex-start; border-radius: var(--ray-radius-default) 0 0 var(--ray-radius-default); }
167
+ :host([data-variant="outline"][data-vertical][data-active]) { border-bottom-color: var(--ray-border); border-right-color: var(--ray-background); }
168
+
169
+ /* pills */
170
+ :host([data-variant="pills"]) { border-radius: var(--ray-radius-default); }
171
+ :host([data-variant="pills"]:hover:not([data-active])) { background: var(--ray-accent); }
172
+ :host([data-variant="pills"][data-active]) { color: var(--_contrast, var(--ray-primary-foreground)); background: var(--_filled, var(--ray-primary)); }
173
+
174
+ .section { display: inline-flex; }
175
+ .section:not([data-filled]) { display: none; }
176
+ ::slotted(svg) { width: 1rem; height: 1rem; }
177
+ `;
178
+
179
+ export class RayTab extends RayElement {
180
+ static properties = {
181
+ value: { type: String, reflect: true },
182
+ disabled: { type: Boolean, reflect: true },
183
+ };
184
+ 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>`;
186
+
187
+ constructor() {
188
+ super();
189
+ for (const slot of this.shadowRoot.querySelectorAll("slot[name]")) {
190
+ slot.addEventListener("slotchange", () => slot.parentElement.toggleAttribute("data-filled", slot.assignedNodes().length > 0));
191
+ }
192
+ }
193
+
194
+ connectedCallback() {
195
+ // Tabs render in the tablist of <ray-tabs>.
196
+ if (this.slot !== "tab") this.slot = "tab";
197
+ super.connectedCallback();
198
+ }
199
+
200
+ update(changed) {
201
+ if (changed.has("value") || changed.has("disabled")) this.closest("ray-tabs")?.requestUpdate();
202
+ }
203
+ }
204
+
205
+ const panelStyles = css`
206
+ :host { display: block; outline: none; }
207
+ :host([hidden]) { display: none; }
208
+ /* The newly selected panel fades in (display none → block restarts it). */
209
+ :host(:not([hidden])) { animation: ray-panel-in var(--ray-duration-base) var(--ray-easing-out); }
210
+ @keyframes ray-panel-in { from { opacity: 0; transform: translateY(2px); } }
211
+ :host(:focus-visible) { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: 2px; border-radius: var(--ray-radius-sm); }
212
+ `;
213
+
214
+ export class RayTabPanel extends RayElement {
215
+ static properties = {
216
+ value: { type: String, reflect: true },
217
+ };
218
+ static styles = [panelStyles];
219
+ static template = `<slot></slot>`;
220
+ }
221
+
222
+ define("ray-tabs", RayTabs);
223
+ define("ray-tab", RayTab);
224
+ define("ray-tab-panel", RayTabPanel);
@@ -0,0 +1,105 @@
1
+ // <ray-textarea>: a multi-line text field, optionally growing with its content.
2
+
3
+ import { css, define } from "../../core/core.js";
4
+ import { sizeStyles, radiusStyles } from "../../core/styles.js";
5
+ import { FieldControl, fieldStyles, boxStyles, fieldTemplate } from "../../core/form.js";
6
+
7
+ const styles = css`
8
+ .box { align-items: stretch; }
9
+ .control {
10
+ padding-block: calc(var(--_px) * 0.4);
11
+ line-height: var(--ray-line-height-md);
12
+ resize: vertical;
13
+ }
14
+ :host([autosize]) .control { resize: none; overflow: hidden; }
15
+ :host([resize="none"]) .control { resize: none; }
16
+ :host([resize="both"]) .control { resize: both; }
17
+ `;
18
+
19
+ const PASSTHROUGH = ["placeholder", "autocomplete", "minlength", "maxlength", "spellcheck", "wrap"];
20
+
21
+ export class RayTextarea extends FieldControl {
22
+ static properties = {
23
+ ...FieldControl.fieldProperties,
24
+ value: { type: String },
25
+ rows: { type: Number, default: 3 },
26
+ autosize: { type: Boolean, reflect: true },
27
+ maxRows: { type: Number },
28
+ readonly: { type: Boolean, reflect: true },
29
+ };
30
+ static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, styles];
31
+ static template = fieldTemplate(`<textarea id="control" class="control" part="textarea"></textarea>`, { sections: false });
32
+
33
+ static get observedAttributes() {
34
+ return [...super.observedAttributes, ...PASSTHROUGH];
35
+ }
36
+
37
+ attributeChangedCallback(attr, old, value) {
38
+ if (PASSTHROUGH.includes(attr)) {
39
+ if (value === null) this.control?.removeAttribute(attr);
40
+ else this.control?.setAttribute(attr, value);
41
+ return;
42
+ }
43
+ super.attributeChangedCallback(attr, old, value);
44
+ }
45
+
46
+ wire() {
47
+ for (const attr of PASSTHROUGH) {
48
+ const v = this.getAttribute(attr);
49
+ if (v !== null) this.control.setAttribute(attr, v);
50
+ }
51
+ // Initial value: the attribute, else the element's text (like <textarea>).
52
+ this._default = this.getAttribute("value") ?? this.textContent ?? "";
53
+ if (this.value === undefined) this.commitProperty("value", this._default);
54
+ this.control.addEventListener("input", () => {
55
+ this.commitProperty("value", this.control.value);
56
+ this._commit();
57
+ this._fit();
58
+ });
59
+ this.control.addEventListener("change", (e) => this.forward(e));
60
+ }
61
+
62
+ update() {
63
+ const c = this.control;
64
+ const v = this.value ?? "";
65
+ if (c.value !== v) c.value = v;
66
+ c.rows = this.rows || 3;
67
+ c.readOnly = Boolean(this.readonly);
68
+ this.syncField();
69
+ this._commit();
70
+ this._fit();
71
+ }
72
+
73
+ _commit() {
74
+ this.internals.setFormValue(this.value ?? "");
75
+ this.syncValidity();
76
+ }
77
+
78
+ // Autosize: grow to the content, up to maxRows.
79
+ _fit() {
80
+ const c = this.control;
81
+ if (!this.autosize) {
82
+ c.style.removeProperty("height");
83
+ return;
84
+ }
85
+ c.style.height = "auto";
86
+ let h = c.scrollHeight;
87
+ if (this.maxRows) {
88
+ const line = parseFloat(getComputedStyle(c).lineHeight) || 20;
89
+ const pad = c.offsetHeight - c.clientHeight + parseFloat(getComputedStyle(c).paddingTop) * 2;
90
+ h = Math.min(h, line * this.maxRows + pad);
91
+ c.style.overflowY = c.scrollHeight > h ? "auto" : "hidden";
92
+ }
93
+ c.style.height = `${h}px`;
94
+ }
95
+
96
+ formResetCallback() {
97
+ this.value = this._default;
98
+ }
99
+
100
+ formStateRestoreCallback(state) {
101
+ this.value = String(state ?? "");
102
+ }
103
+ }
104
+
105
+ define("ray-textarea", RayTextarea);
@@ -0,0 +1,78 @@
1
+ // <ray-theme-toggle>: cycles the document theme system -> light -> dark.
2
+
3
+ import { RayElement, css, define, getTheme, setTheme, onThemeChange } from "../../core/core.js";
4
+
5
+ const ORDER = ["system", "light", "dark"];
6
+ const LABEL = { system: "System theme", light: "Light theme", dark: "Dark theme" };
7
+
8
+ // Icon of each theme (from the icon registry).
9
+ const ICONS = { system: "monitor", light: "sun", dark: "moon" };
10
+
11
+ const styles = css`
12
+ :host {
13
+ display: inline-flex;
14
+ --_size: var(--ray-control-height-sm);
15
+ }
16
+ :host([size="xs"]) { --_size: var(--ray-control-height-xs); }
17
+ :host([size="md"]) { --_size: var(--ray-control-height-md); }
18
+ :host([size="lg"]) { --_size: var(--ray-control-height-lg); }
19
+ :host([size="xl"]) { --_size: var(--ray-control-height-xl); }
20
+ button {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ width: var(--_size);
25
+ height: var(--_size);
26
+ padding: 0;
27
+ color: var(--ray-foreground);
28
+ background: transparent;
29
+ border: 1px solid var(--ray-border);
30
+ border-radius: var(--ray-radius-default);
31
+ cursor: pointer;
32
+ transition: background var(--ray-duration-fast) var(--ray-easing-standard);
33
+ }
34
+ button:hover { background: var(--ray-accent); }
35
+ .icon svg { width: calc(var(--_size) * 0.55); height: calc(var(--_size) * 0.55); }
36
+ `;
37
+
38
+ export class RayThemeToggle extends RayElement {
39
+ static properties = {
40
+ size: { type: String, reflect: true, default: "sm" },
41
+ };
42
+ static styles = [styles];
43
+ static delegatesFocus = true;
44
+ static template = `<button part="button" class="focus-ring" type="button"><span class="icon" data-ray-icon="monitor" aria-hidden="true"></span></button>`;
45
+
46
+ connectedCallback() {
47
+ super.connectedCallback();
48
+ this._button = this.$("button");
49
+ this._onClick = () => {
50
+ const next = ORDER[(ORDER.indexOf(getTheme()) + 1) % ORDER.length];
51
+ setTheme(next);
52
+ };
53
+ this._button.addEventListener("click", this._onClick);
54
+ this._stop = onThemeChange(() => this.requestUpdate());
55
+ this._paint();
56
+ }
57
+
58
+ disconnectedCallback() {
59
+ super.disconnectedCallback();
60
+ this._button.removeEventListener("click", this._onClick);
61
+ this._stop?.();
62
+ }
63
+
64
+ update() {
65
+ this._paint();
66
+ }
67
+
68
+ _paint() {
69
+ const theme = getTheme();
70
+ const next = ORDER[(ORDER.indexOf(theme) + 1) % ORDER.length];
71
+ this.$(".icon").setAttribute("data-ray-icon", ICONS[theme]);
72
+ this.paintIcons(this._button);
73
+ this._button.setAttribute("aria-label", `${LABEL[theme]} (switch to ${next})`);
74
+ this._button.title = LABEL[theme];
75
+ }
76
+ }
77
+
78
+ define("ray-theme-toggle", RayThemeToggle);
@@ -0,0 +1,150 @@
1
+ // <ray-timeline> + <ray-timeline-item>: events in order along a line
2
+ // (Mantine's Timeline): activity, history, a deploy's stages. Items up to
3
+ // `active` are reached: a filled bullet and a colored line. A bullet is a
4
+ // dot, an icon, or anything in the bullet slot (an avatar).
5
+
6
+ import { RayElement, css, define } from "../../core/core.js";
7
+ import { applyColor } from "../../core/styles.js";
8
+
9
+ const timelineStyles = css`
10
+ :host { display: block; --_bullet: 1.25rem; --_line: 2px; }
11
+ :host([hidden]) { display: none; }
12
+ .list { display: flex; flex-direction: column; }
13
+ `;
14
+
15
+ const itemStyles = css`
16
+ :host {
17
+ position: relative;
18
+ display: grid;
19
+ grid-template-columns: var(--_bullet) minmax(0, 1fr);
20
+ column-gap: var(--ray-space-3);
21
+ padding-bottom: var(--ray-space-6);
22
+ }
23
+ :host([hidden]) { display: none; }
24
+ :host([data-last]) { padding-bottom: 0; }
25
+ :host([data-align="right"]) { grid-template-columns: minmax(0, 1fr) var(--_bullet); text-align: right; }
26
+ :host([data-align="right"]) .bullet-col { grid-column: 2; grid-row: 1; }
27
+ :host([data-align="right"]) .body { grid-column: 1; grid-row: 1; }
28
+ .bullet-col { position: relative; display: flex; justify-content: center; }
29
+ .bullet {
30
+ position: relative;
31
+ z-index: 1;
32
+ box-sizing: border-box;
33
+ display: grid;
34
+ place-items: center;
35
+ width: var(--_bullet);
36
+ height: var(--_bullet);
37
+ color: var(--ray-muted-foreground);
38
+ background: var(--ray-background);
39
+ border: var(--_line) solid var(--ray-border);
40
+ border-radius: 50%;
41
+ --ray-icon-size: calc(var(--_bullet) * 0.6);
42
+ --ray-icon-stroke: 2.25;
43
+ }
44
+ :host([data-reached]) .bullet { color: var(--_contrast); background: var(--_filled); border-color: var(--_filled); }
45
+ :host([data-has-bullet]) .bullet { border: 0; background: none; overflow: visible; }
46
+ .line {
47
+ position: absolute;
48
+ top: var(--_bullet);
49
+ bottom: calc(-1 * var(--ray-space-6));
50
+ left: calc(50% - var(--_line) / 2);
51
+ width: 0;
52
+ border-left: var(--_line) var(--_line-style, solid) var(--ray-border);
53
+ }
54
+ :host([data-last]) .line { display: none; }
55
+ :host([data-line-reached]) .line { border-left-color: var(--_filled); }
56
+ :host([line-variant="dashed"]) .line { --_line-style: dashed; }
57
+ :host([line-variant="dotted"]) .line { --_line-style: dotted; }
58
+ .body { min-width: 0; padding-top: calc((var(--_bullet) - 1.4em) / 2); }
59
+ .title { display: block; font-weight: var(--ray-font-weight-medium); line-height: 1.4; }
60
+ .title:empty { display: none; }
61
+ .content { font-size: var(--ray-font-size-sm); color: var(--ray-muted-foreground); }
62
+ .content ::slotted(*) { margin-block: var(--ray-space-1) 0; }
63
+ .time { display: block; margin-top: var(--ray-space-1); font-size: var(--ray-font-size-xs); color: var(--ray-muted-foreground); opacity: 0.85; }
64
+ .time:empty { display: none; }
65
+ `;
66
+
67
+ export class RayTimeline extends RayElement {
68
+ static properties = {
69
+ active: { type: Number, reflect: true, default: -1 },
70
+ align: { type: String, reflect: true, default: "left" },
71
+ bulletSize: { type: String, attribute: "bullet-size" },
72
+ lineWidth: { type: Number, attribute: "line-width" },
73
+ color: { type: String, reflect: true },
74
+ reverseActive: { type: Boolean, reflect: true, attribute: "reverse-active" },
75
+ };
76
+ static styles = [timelineStyles];
77
+ static template = `<div class="list" part="list"><slot></slot></div>`;
78
+
79
+ constructor() {
80
+ super();
81
+ this.$("slot").addEventListener("slotchange", () => this.requestUpdate());
82
+ }
83
+
84
+ connectedCallback() {
85
+ super.connectedCallback();
86
+ if (!this.hasAttribute("role")) this.setAttribute("role", "list");
87
+ }
88
+
89
+ /** The items, in order. */
90
+ get items() {
91
+ return [...this.children].filter((el) => el.localName === "ray-timeline-item");
92
+ }
93
+
94
+ update() {
95
+ const items = this.items;
96
+ const n = items.length;
97
+ const active = this.active ?? -1;
98
+ if (this.bulletSize) this.style.setProperty("--_bullet", /^\d+(\.\d+)?$/.test(this.bulletSize) ? `${this.bulletSize}px` : this.bulletSize);
99
+ else this.style.removeProperty("--_bullet");
100
+ if (this.lineWidth) this.style.setProperty("--_line", `${this.lineWidth}px`);
101
+ else this.style.removeProperty("--_line");
102
+ applyColor(this, this.color);
103
+ items.forEach((item, i) => {
104
+ // reverse-active: reached from the end (the latest event at the top).
105
+ const k = this.reverseActive ? n - 1 - i : i;
106
+ item.toggleAttribute("data-reached", active >= 0 && k <= active);
107
+ item.toggleAttribute("data-line-reached", active >= 0 && (this.reverseActive ? k <= active && k > 0 : k < active));
108
+ item.toggleAttribute("data-last", i === n - 1);
109
+ item.setAttribute("data-align", this.align === "right" ? "right" : "left");
110
+ });
111
+ }
112
+ }
113
+
114
+ export class RayTimelineItem extends RayElement {
115
+ static properties = {
116
+ label: { type: String },
117
+ time: { type: String },
118
+ icon: { type: String },
119
+ color: { type: String, reflect: true },
120
+ lineVariant: { type: String, reflect: true, attribute: "line-variant", default: "solid" },
121
+ };
122
+ static styles = [itemStyles];
123
+ static template = `
124
+ <div class="bullet-col"><span class="bullet" part="bullet"><span class="icon" data-ray-icon=""></span><slot name="bullet"></slot></span><span class="line" part="line"></span></div>
125
+ <div class="body" part="body"><span class="title" part="title"></span><div class="content"><slot></slot></div><span class="time" part="time"></span></div>`;
126
+
127
+ constructor() {
128
+ super();
129
+ const slot = this.$('slot[name="bullet"]');
130
+ slot.addEventListener("slotchange", () => this.toggleAttribute("data-has-bullet", slot.assignedElements().length > 0));
131
+ }
132
+
133
+ connectedCallback() {
134
+ super.connectedCallback();
135
+ this.setAttribute("role", "listitem");
136
+ }
137
+
138
+ update() {
139
+ this.$(".title").textContent = this.label ?? "";
140
+ this.$(".time").textContent = this.time ?? "";
141
+ this.$(".icon").setAttribute("data-ray-icon", this.icon ?? "");
142
+ this.paintIcons();
143
+ // Its own color, else the timeline's (inherited custom properties).
144
+ if (this.color) applyColor(this, this.color);
145
+ else for (const p of [...this.style]) if (p.startsWith("--_")) this.style.removeProperty(p);
146
+ }
147
+ }
148
+
149
+ define("ray-timeline-item", RayTimelineItem);
150
+ define("ray-timeline", RayTimeline);