@raylang/elements 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +12 -2
- package/base.css +111 -0
- package/components/accordion/accordion.js +197 -0
- package/components/alert/alert.js +112 -0
- package/components/app-bar/app-bar.js +168 -0
- package/components/app-shell/app-shell.js +198 -0
- package/components/aspect-ratio/aspect-ratio.js +59 -0
- package/components/attachment/attachment.js +333 -0
- package/components/avatar/avatar.js +96 -0
- package/components/badge/badge.js +63 -0
- package/components/blockquote/blockquote.js +128 -0
- package/components/bottom-nav/bottom-nav.js +309 -0
- package/components/breadcrumbs/breadcrumbs.js +175 -0
- package/components/button/button.js +283 -0
- package/components/button-group/button-group.js +72 -0
- package/components/card/card.js +73 -0
- package/components/carousel/carousel.js +435 -0
- package/components/center/center.js +26 -0
- package/components/checkbox/checkbox.js +152 -0
- package/components/checkbox-group/checkbox-group.js +152 -0
- package/components/code/code.js +574 -0
- package/components/color-picker/color-picker.js +775 -0
- package/components/combobox/combobox.js +563 -0
- package/components/command-palette/command-palette.js +510 -0
- package/components/container/container.js +41 -0
- package/components/date-picker/date-picker.js +594 -0
- package/components/dialog/dialog.js +329 -0
- package/components/drawer/drawer.js +139 -0
- package/components/empty-state/empty-state.js +66 -0
- package/components/file-upload/file-upload.js +589 -0
- package/components/grid/grid.js +96 -0
- package/components/group/group.js +40 -0
- package/components/icon/icon.js +58 -0
- package/components/icon-button/icon-button.js +30 -0
- package/components/indicator/indicator.js +112 -0
- package/components/input/input.js +85 -0
- package/components/kbd/kbd.js +60 -0
- package/components/menu/menu.js +296 -0
- package/components/nav-link/nav-link.js +159 -0
- package/components/notification/notification.js +591 -0
- package/components/number-input/number-input.js +241 -0
- package/components/pagination/pagination.js +194 -0
- package/components/password-input/password-input.js +85 -0
- package/components/path-input/path-input.js +137 -0
- package/components/popover/popover.js +85 -0
- package/components/progress/progress.js +109 -0
- package/components/radio/radio.js +253 -0
- package/components/scroll-area/scroll-area.js +158 -0
- package/components/search/search.js +157 -0
- package/components/segmented-control/segmented-control.js +190 -0
- package/components/select/select.js +506 -0
- package/components/separator/separator.js +58 -0
- package/components/sidebar/sidebar.js +252 -0
- package/components/skeleton/skeleton.js +90 -0
- package/components/slider/slider.js +155 -0
- package/components/spinner/spinner.js +39 -0
- package/components/split/split.js +221 -0
- package/components/stack/stack.js +46 -0
- package/components/stat/stat.js +79 -0
- package/components/statusbar/statusbar.js +110 -0
- package/components/stepper/stepper.js +239 -0
- package/components/switch/switch.js +45 -0
- package/components/table/table.js +328 -0
- package/components/tabs/tabs.js +224 -0
- package/components/textarea/textarea.js +105 -0
- package/components/theme-toggle/theme-toggle.js +78 -0
- package/components/timeline/timeline.js +150 -0
- package/components/toggle/toggle.js +130 -0
- package/components/toggle-group/toggle-group.js +216 -0
- package/components/tooltip/tooltip.js +101 -0
- package/core/charts.js +107 -0
- package/core/color.js +110 -0
- package/core/core.js +556 -0
- package/core/dismiss.js +69 -0
- package/core/floating.js +195 -0
- package/core/focus.js +70 -0
- package/core/form.js +235 -0
- package/core/host.js +200 -0
- package/core/hotkey.js +40 -0
- package/core/icons.js +104 -0
- package/core/motion.js +73 -0
- package/core/position.js +91 -0
- package/core/raylang.js +167 -0
- package/core/scroll.js +58 -0
- package/core/styles.js +211 -0
- package/core.js +16 -0
- package/custom-elements.json +18545 -0
- package/index.js +74 -0
- package/package.json +312 -4
- package/ray-elements.d.ts +2570 -0
- package/tokens.css +796 -0
- package/tokens.json +2083 -0
- package/vscode.html-custom-data.json +5499 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// <ray-attachment>: a file in a message composer, an upload list or a chat
|
|
2
|
+
// (shadcn's AI attachment, upload items). Thumbnail or type icon, name, size
|
|
3
|
+
// and an extension badge; optional upload progress, error, remove button and
|
|
4
|
+
// download link. <ray-attachments> lays several out in a wrapping row.
|
|
5
|
+
//
|
|
6
|
+
// el.file = input.files[0]; // fills name, size, type and an image thumbnail
|
|
7
|
+
|
|
8
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
9
|
+
|
|
10
|
+
const styles = css`
|
|
11
|
+
:host {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
vertical-align: top;
|
|
14
|
+
max-width: 100%;
|
|
15
|
+
font-size: var(--ray-font-size-sm);
|
|
16
|
+
--_thumb: 2.75rem;
|
|
17
|
+
}
|
|
18
|
+
:host([hidden]) { display: none; }
|
|
19
|
+
.item {
|
|
20
|
+
position: relative;
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: var(--ray-space-3);
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
width: 16rem;
|
|
26
|
+
max-width: 100%;
|
|
27
|
+
padding: var(--ray-space-2);
|
|
28
|
+
padding-right: var(--ray-space-3);
|
|
29
|
+
color: var(--ray-foreground);
|
|
30
|
+
background: var(--ray-card);
|
|
31
|
+
border: 1px solid var(--ray-border);
|
|
32
|
+
border-radius: var(--ray-radius-default);
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
}
|
|
35
|
+
:host([status="error"]) .item { border-color: var(--ray-destructive); }
|
|
36
|
+
:host([disabled]) .item { opacity: 0.6; }
|
|
37
|
+
.thumb {
|
|
38
|
+
position: relative;
|
|
39
|
+
flex: none;
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
width: var(--_thumb);
|
|
44
|
+
height: var(--_thumb);
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
color: var(--ray-muted-foreground);
|
|
47
|
+
background: var(--ray-muted);
|
|
48
|
+
border-radius: calc(var(--ray-radius-default) - 2px);
|
|
49
|
+
}
|
|
50
|
+
.thumb img { width: 100%; height: 100%; object-fit: cover; }
|
|
51
|
+
.thumb [data-ray-icon] { --ray-icon-size: 1.25rem; }
|
|
52
|
+
:host([status="error"]) .thumb { color: var(--ray-destructive); }
|
|
53
|
+
.badge {
|
|
54
|
+
position: absolute;
|
|
55
|
+
bottom: 3px;
|
|
56
|
+
left: 50%;
|
|
57
|
+
transform: translateX(-50%);
|
|
58
|
+
padding: 0 0.25rem;
|
|
59
|
+
font-size: 0.5625rem;
|
|
60
|
+
font-weight: var(--ray-font-weight-bold);
|
|
61
|
+
line-height: 0.875rem;
|
|
62
|
+
letter-spacing: 0.02em;
|
|
63
|
+
color: var(--ray-muted-foreground);
|
|
64
|
+
background: var(--ray-card);
|
|
65
|
+
border-radius: var(--ray-radius-xs);
|
|
66
|
+
}
|
|
67
|
+
.thumb[data-image] .badge { display: none; }
|
|
68
|
+
.text { display: grid; flex: 1; min-width: 0; gap: 1px; }
|
|
69
|
+
.name-box { display: block; min-width: 0; }
|
|
70
|
+
.name {
|
|
71
|
+
display: block;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
text-overflow: ellipsis;
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
font-weight: var(--ray-font-weight-medium);
|
|
76
|
+
color: inherit;
|
|
77
|
+
text-decoration: none;
|
|
78
|
+
}
|
|
79
|
+
a.name:hover { text-decoration: underline; }
|
|
80
|
+
a.name:focus-visible { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: 2px; border-radius: var(--ray-radius-xs); }
|
|
81
|
+
.meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--ray-font-size-xs); color: var(--ray-muted-foreground); font-variant-numeric: tabular-nums; }
|
|
82
|
+
:host([status="error"]) .meta { color: var(--ray-destructive); }
|
|
83
|
+
.bar {
|
|
84
|
+
position: absolute;
|
|
85
|
+
left: 0;
|
|
86
|
+
right: 0;
|
|
87
|
+
bottom: 0;
|
|
88
|
+
height: 2px;
|
|
89
|
+
background: var(--ray-muted);
|
|
90
|
+
}
|
|
91
|
+
.bar span {
|
|
92
|
+
display: block;
|
|
93
|
+
height: 100%;
|
|
94
|
+
width: var(--_pct, 0%);
|
|
95
|
+
background: var(--ray-primary);
|
|
96
|
+
transition: width var(--ray-duration-base) var(--ray-easing-standard);
|
|
97
|
+
}
|
|
98
|
+
.remove {
|
|
99
|
+
flex: none;
|
|
100
|
+
display: inline-flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
width: 1.5rem;
|
|
104
|
+
height: 1.5rem;
|
|
105
|
+
padding: 0;
|
|
106
|
+
color: var(--ray-muted-foreground);
|
|
107
|
+
background: transparent;
|
|
108
|
+
border: 0;
|
|
109
|
+
border-radius: var(--ray-radius-sm);
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
opacity: 0.7;
|
|
112
|
+
transition: opacity var(--ray-duration-fast) var(--ray-easing-standard), background var(--ray-duration-fast) var(--ray-easing-standard);
|
|
113
|
+
}
|
|
114
|
+
.item:hover .remove, .remove:focus-visible { opacity: 1; }
|
|
115
|
+
.remove:hover { color: var(--ray-foreground); background: var(--ray-default-hover); }
|
|
116
|
+
.remove:disabled { cursor: not-allowed; }
|
|
117
|
+
.remove [data-ray-icon] { --ray-icon-size: 0.875rem; }
|
|
118
|
+
|
|
119
|
+
/* variant="card": the thumbnail on top, square-ish. */
|
|
120
|
+
:host([variant="card"]) .item {
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
align-items: stretch;
|
|
123
|
+
gap: var(--ray-space-2);
|
|
124
|
+
width: 10rem;
|
|
125
|
+
padding: var(--ray-space-2);
|
|
126
|
+
}
|
|
127
|
+
:host([variant="card"]) .thumb { width: 100%; height: auto; aspect-ratio: 4 / 3; }
|
|
128
|
+
:host([variant="card"]) .thumb [data-ray-icon] { --ray-icon-size: 1.75rem; }
|
|
129
|
+
:host([variant="card"]) .badge { bottom: 6px; font-size: 0.625rem; }
|
|
130
|
+
:host([variant="card"]) .text { padding: 0 var(--ray-space-1) var(--ray-space-1); }
|
|
131
|
+
:host([variant="card"]) .remove {
|
|
132
|
+
position: absolute;
|
|
133
|
+
top: calc(var(--ray-space-2) + 4px);
|
|
134
|
+
right: calc(var(--ray-space-2) + 4px);
|
|
135
|
+
background: var(--ray-card);
|
|
136
|
+
box-shadow: var(--ray-shadow-xs);
|
|
137
|
+
opacity: 1;
|
|
138
|
+
}
|
|
139
|
+
[hidden] { display: none !important; }
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
const IMAGE = /^image\//;
|
|
143
|
+
|
|
144
|
+
/** 12 KB, 3.4 MB… (base 1024, like file managers). */
|
|
145
|
+
export function formatBytes(bytes, locale) {
|
|
146
|
+
const n = Number(bytes);
|
|
147
|
+
if (!Number.isFinite(n) || n < 0) return "";
|
|
148
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
149
|
+
let i = 0;
|
|
150
|
+
let v = n;
|
|
151
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
152
|
+
v /= 1024;
|
|
153
|
+
i++;
|
|
154
|
+
}
|
|
155
|
+
const digits = i === 0 || v >= 10 ? 0 : 1;
|
|
156
|
+
return `${new Intl.NumberFormat(locale, { maximumFractionDigits: digits }).format(v)} ${units[i]}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** "report.final.pdf" -> "PDF"; else from the MIME type ("image/png" -> "PNG"). */
|
|
160
|
+
export function extension(name, type) {
|
|
161
|
+
const m = /\.([a-z0-9]{1,5})$/i.exec(String(name ?? ""));
|
|
162
|
+
if (m) return m[1].toUpperCase();
|
|
163
|
+
const sub = String(type ?? "").split("/")[1];
|
|
164
|
+
return sub ? sub.split(/[+;.-]/)[0].slice(0, 5).toUpperCase() : "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The registry icon for a file: "image" for images, "file" otherwise. */
|
|
168
|
+
export function iconFor(type, name) {
|
|
169
|
+
const t = String(type ?? "");
|
|
170
|
+
if (IMAGE.test(t) || /\.(png|jpe?g|gif|webp|svg|avif|bmp)$/i.test(String(name ?? ""))) return "image";
|
|
171
|
+
return "file";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export class RayAttachment extends RayElement {
|
|
175
|
+
static properties = {
|
|
176
|
+
name: { type: String, reflect: true },
|
|
177
|
+
size: { type: Number },
|
|
178
|
+
type: { type: String, reflect: true },
|
|
179
|
+
src: { type: String },
|
|
180
|
+
href: { type: String, reflect: true },
|
|
181
|
+
progress: { type: Number },
|
|
182
|
+
status: { type: String, reflect: true },
|
|
183
|
+
error: { type: String },
|
|
184
|
+
removable: { type: Boolean, reflect: true },
|
|
185
|
+
removeLabel: { type: String },
|
|
186
|
+
variant: { type: String, reflect: true, default: "row" },
|
|
187
|
+
disabled: { type: Boolean, reflect: true },
|
|
188
|
+
};
|
|
189
|
+
static styles = [styles];
|
|
190
|
+
static template = `
|
|
191
|
+
<div class="item" part="item" role="group">
|
|
192
|
+
<span class="thumb" part="thumbnail"><img alt="" hidden><span class="icon" aria-hidden="true"></span><span class="badge" part="badge" aria-hidden="true"></span></span>
|
|
193
|
+
<span class="text"><span class="name-box"></span><span class="meta" part="meta"></span></span>
|
|
194
|
+
<button class="remove focus-ring" part="remove" type="button" hidden><span data-ray-icon="close" aria-hidden="true"></span></button>
|
|
195
|
+
<span class="bar" part="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" hidden><span></span></span>
|
|
196
|
+
</div>`;
|
|
197
|
+
|
|
198
|
+
#file = null;
|
|
199
|
+
#objectUrl = null;
|
|
200
|
+
#failed = null;
|
|
201
|
+
|
|
202
|
+
constructor() {
|
|
203
|
+
super();
|
|
204
|
+
this.$("img").addEventListener("error", () => {
|
|
205
|
+
this.#failed = this.$("img").getAttribute("src");
|
|
206
|
+
this.requestUpdate();
|
|
207
|
+
});
|
|
208
|
+
this.$(".remove").addEventListener("click", () => {
|
|
209
|
+
if (this.disabled) return;
|
|
210
|
+
if (this.emit("ray-remove", { name: this.name ?? "", file: this.#file }, { cancelable: true })) this.remove();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A File to show: fills name, size, type and, for images, a thumbnail. */
|
|
215
|
+
get file() {
|
|
216
|
+
return this.#file;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
set file(f) {
|
|
220
|
+
this.#revoke();
|
|
221
|
+
this.#file = f ?? null;
|
|
222
|
+
if (f) {
|
|
223
|
+
this.name = f.name;
|
|
224
|
+
this.size = f.size;
|
|
225
|
+
this.type = f.type;
|
|
226
|
+
// A Blob has bytes to show; a file the host chose (HostFile) is only a path.
|
|
227
|
+
if (IMAGE.test(f.type) && f instanceof Blob && typeof URL.createObjectURL === "function") {
|
|
228
|
+
this.#objectUrl = URL.createObjectURL(f);
|
|
229
|
+
this.src = this.#objectUrl;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
this.requestUpdate();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
disconnectedCallback() {
|
|
236
|
+
super.disconnectedCallback();
|
|
237
|
+
this.#revoke();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#revoke() {
|
|
241
|
+
if (this.#objectUrl) {
|
|
242
|
+
URL.revokeObjectURL(this.#objectUrl);
|
|
243
|
+
if (this.src === this.#objectUrl) this.src = undefined;
|
|
244
|
+
this.#objectUrl = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The effective status: an explicit one, else "uploading" while progress is set. */
|
|
249
|
+
get state() {
|
|
250
|
+
if (this.status) return this.status;
|
|
251
|
+
return this.progress === undefined || this.progress === null || Number.isNaN(this.progress) ? "done" : "uploading";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
update() {
|
|
255
|
+
const name = this.name ?? "";
|
|
256
|
+
const size = formatBytes(this.size);
|
|
257
|
+
const state = this.state;
|
|
258
|
+
|
|
259
|
+
// Thumbnail (images with a working src) or the type icon + extension badge.
|
|
260
|
+
const thumb = this.$(".thumb");
|
|
261
|
+
const img = this.$("img");
|
|
262
|
+
const showImage = Boolean(this.src) && this.#failed !== this.src && iconFor(this.type, name) === "image";
|
|
263
|
+
img.hidden = !showImage;
|
|
264
|
+
if (showImage && img.getAttribute("src") !== this.src) img.src = this.src;
|
|
265
|
+
thumb.toggleAttribute("data-image", showImage);
|
|
266
|
+
const icon = this.$(".icon");
|
|
267
|
+
icon.hidden = showImage;
|
|
268
|
+
const iconName = state === "error" ? "alert-triangle" : iconFor(this.type, name);
|
|
269
|
+
if (icon.getAttribute("data-ray-icon") !== iconName) {
|
|
270
|
+
icon.setAttribute("data-ray-icon", iconName);
|
|
271
|
+
this.paintIcons(thumb);
|
|
272
|
+
}
|
|
273
|
+
this.$(".badge").textContent = extension(name, this.type);
|
|
274
|
+
|
|
275
|
+
// Name: a link with href, plain text otherwise.
|
|
276
|
+
const box = this.$(".name-box");
|
|
277
|
+
const tag = this.href && !this.disabled ? "a" : "span";
|
|
278
|
+
let el = box.firstElementChild;
|
|
279
|
+
if (!el || el.localName !== tag) {
|
|
280
|
+
el = document.createElement(tag);
|
|
281
|
+
el.className = "name";
|
|
282
|
+
el.setAttribute("part", "name");
|
|
283
|
+
box.replaceChildren(el);
|
|
284
|
+
}
|
|
285
|
+
el.textContent = name;
|
|
286
|
+
el.title = name;
|
|
287
|
+
if (tag === "a") {
|
|
288
|
+
el.href = this.href;
|
|
289
|
+
el.setAttribute("download", name);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Meta line: size, upload state or error.
|
|
293
|
+
const pct = Math.max(0, Math.min(100, Math.round(Number(this.progress) || 0)));
|
|
294
|
+
let meta = size;
|
|
295
|
+
if (state === "uploading") meta = this.progress === undefined || this.progress === null ? "Uploading…" : `Uploading… ${pct}%`;
|
|
296
|
+
else if (state === "error") meta = this.error || "Upload failed";
|
|
297
|
+
this.$(".meta").textContent = meta;
|
|
298
|
+
|
|
299
|
+
const bar = this.$(".bar");
|
|
300
|
+
const uploading = state === "uploading" && this.progress !== undefined && this.progress !== null;
|
|
301
|
+
bar.hidden = !uploading;
|
|
302
|
+
if (uploading) {
|
|
303
|
+
bar.style.setProperty("--_pct", `${pct}%`);
|
|
304
|
+
bar.setAttribute("aria-valuenow", String(pct));
|
|
305
|
+
bar.setAttribute("aria-label", `Uploading ${name}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const remove = this.$(".remove");
|
|
309
|
+
remove.hidden = !this.removable;
|
|
310
|
+
remove.disabled = Boolean(this.disabled);
|
|
311
|
+
remove.setAttribute("aria-label", (this.removeLabel || "Remove {name}").replace("{name}", name));
|
|
312
|
+
|
|
313
|
+
const item = this.$(".item");
|
|
314
|
+
item.setAttribute("aria-label", [name, size].filter(Boolean).join(", "));
|
|
315
|
+
item.toggleAttribute("aria-busy", state === "uploading");
|
|
316
|
+
if (state === "error") item.setAttribute("aria-invalid", "true");
|
|
317
|
+
else item.removeAttribute("aria-invalid");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const listStyles = css`
|
|
322
|
+
:host { display: flex; flex-wrap: wrap; gap: var(--ray-space-2); align-items: flex-start; }
|
|
323
|
+
:host([hidden]) { display: none; }
|
|
324
|
+
`;
|
|
325
|
+
|
|
326
|
+
/** A wrapping row of attachments (layout only). */
|
|
327
|
+
export class RayAttachments extends RayElement {
|
|
328
|
+
static styles = [listStyles];
|
|
329
|
+
static template = `<slot></slot>`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
define("ray-attachment", RayAttachment);
|
|
333
|
+
define("ray-attachments", RayAttachments);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// <ray-avatar>: an image, the initials of `name`, or a placeholder icon.
|
|
2
|
+
|
|
3
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
4
|
+
import { applyColor, radiusStyles, variantStyles } from "../../core/styles.js";
|
|
5
|
+
|
|
6
|
+
const styles = css`
|
|
7
|
+
:host {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
vertical-align: middle;
|
|
10
|
+
--_size: 2.375rem;
|
|
11
|
+
--_r: var(--ray-radius-full);
|
|
12
|
+
}
|
|
13
|
+
:host([size="xs"]) { --_size: 1rem; }
|
|
14
|
+
:host([size="sm"]) { --_size: 1.625rem; }
|
|
15
|
+
:host([size="lg"]) { --_size: 3.5rem; }
|
|
16
|
+
:host([size="xl"]) { --_size: 5.25rem; }
|
|
17
|
+
.avatar {
|
|
18
|
+
display: inline-flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
width: var(--_size);
|
|
22
|
+
height: var(--_size);
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
font-size: calc(var(--_size) * 0.4);
|
|
25
|
+
font-weight: var(--ray-font-weight-bold);
|
|
26
|
+
line-height: 1;
|
|
27
|
+
user-select: none;
|
|
28
|
+
border-radius: var(--_r);
|
|
29
|
+
}
|
|
30
|
+
img { width: 100%; height: 100%; object-fit: cover; }
|
|
31
|
+
.initials svg { width: calc(var(--_size) * 0.6); height: calc(var(--_size) * 0.6); }
|
|
32
|
+
[hidden] { display: none !important; }
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const PLACEHOLDER = `<span data-ray-icon="user" aria-hidden="true"></span>`;
|
|
36
|
+
|
|
37
|
+
/** Up to two initials: "Ada Lovelace" -> "AL", "ray" -> "R". */
|
|
38
|
+
export function initials(name) {
|
|
39
|
+
const words = String(name || "").trim().split(/\s+/).filter(Boolean);
|
|
40
|
+
if (!words.length) return "";
|
|
41
|
+
const first = [...words[0]][0] ?? "";
|
|
42
|
+
const last = words.length > 1 ? ([...words[words.length - 1]][0] ?? "") : "";
|
|
43
|
+
return (first + last).toUpperCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class RayAvatar extends RayElement {
|
|
47
|
+
static properties = {
|
|
48
|
+
src: { type: String, reflect: true },
|
|
49
|
+
alt: { type: String, reflect: true },
|
|
50
|
+
name: { type: String, reflect: true },
|
|
51
|
+
color: { type: String, reflect: true },
|
|
52
|
+
variant: { type: String, reflect: true, default: "light" },
|
|
53
|
+
size: { type: String, reflect: true, default: "md" },
|
|
54
|
+
radius: { type: String, reflect: true },
|
|
55
|
+
};
|
|
56
|
+
static styles = [radiusStyles, variantStyles, styles];
|
|
57
|
+
static template = `<span class="avatar" part="avatar"><img part="image" alt="" hidden><span class="initials" part="initials"></span><slot></slot></span>`;
|
|
58
|
+
|
|
59
|
+
connectedCallback() {
|
|
60
|
+
super.connectedCallback();
|
|
61
|
+
if (!this._wired) {
|
|
62
|
+
this._wired = true;
|
|
63
|
+
this.$("img").addEventListener("error", () => {
|
|
64
|
+
this._failed = this.src;
|
|
65
|
+
this.requestUpdate();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
update() {
|
|
71
|
+
const box = this.$(".avatar");
|
|
72
|
+
box.dataset.variant = this.variant || "light";
|
|
73
|
+
applyColor(box, this.color);
|
|
74
|
+
const img = this.$("img");
|
|
75
|
+
const showImage = Boolean(this.src) && this._failed !== this.src;
|
|
76
|
+
img.hidden = !showImage;
|
|
77
|
+
if (showImage && img.getAttribute("src") !== this.src) img.src = this.src;
|
|
78
|
+
const text = initials(this.name);
|
|
79
|
+
const span = this.$(".initials");
|
|
80
|
+
span.hidden = showImage;
|
|
81
|
+
if (!showImage) {
|
|
82
|
+
if (text) span.textContent = text;
|
|
83
|
+
else {
|
|
84
|
+
span.innerHTML = PLACEHOLDER;
|
|
85
|
+
this.paintIcons(span);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// The host carries the accessible name.
|
|
89
|
+
const label = this.alt || this.name;
|
|
90
|
+
this.setAttribute("role", "img");
|
|
91
|
+
if (label) this.setAttribute("aria-label", label);
|
|
92
|
+
else this.removeAttribute("aria-label");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
define("ray-avatar", RayAvatar);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// <ray-badge>: a short status label.
|
|
2
|
+
|
|
3
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
4
|
+
import { applyColor, applyGradient, radiusStyles, variantStyles } from "../../core/styles.js";
|
|
5
|
+
|
|
6
|
+
const styles = css`
|
|
7
|
+
:host {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
vertical-align: middle;
|
|
10
|
+
--_r: var(--ray-radius-full);
|
|
11
|
+
--_h: 1.25rem;
|
|
12
|
+
--_fs: 0.6875rem;
|
|
13
|
+
--_px: 0.5rem;
|
|
14
|
+
}
|
|
15
|
+
:host([size="xs"]) { --_h: 1rem; --_fs: 0.5625rem; --_px: 0.375rem; }
|
|
16
|
+
:host([size="md"]) { --_h: 1.5rem; --_fs: 0.75rem; --_px: 0.625rem; }
|
|
17
|
+
:host([size="lg"]) { --_h: 1.875rem; --_fs: 0.875rem; --_px: 0.75rem; }
|
|
18
|
+
:host([size="xl"]) { --_h: 2.25rem; --_fs: 1rem; --_px: 1rem; }
|
|
19
|
+
.badge {
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: calc(var(--_px) / 2);
|
|
23
|
+
height: var(--_h);
|
|
24
|
+
padding: 0 var(--_px);
|
|
25
|
+
font-size: var(--_fs);
|
|
26
|
+
font-weight: var(--ray-font-weight-bold);
|
|
27
|
+
letter-spacing: 0.25px;
|
|
28
|
+
text-transform: uppercase;
|
|
29
|
+
line-height: 1;
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
border-radius: var(--_r);
|
|
32
|
+
}
|
|
33
|
+
:host([normal-case]) .badge { text-transform: none; letter-spacing: 0; }
|
|
34
|
+
.dot { display: none; width: 0.5em; height: 0.5em; border-radius: 50%; background: var(--_filled); }
|
|
35
|
+
.badge[data-variant="dot"] {
|
|
36
|
+
color: var(--ray-foreground);
|
|
37
|
+
background: transparent;
|
|
38
|
+
border-color: var(--ray-border);
|
|
39
|
+
}
|
|
40
|
+
.badge[data-variant="dot"] .dot { display: inline-block; }
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
export class RayBadge extends RayElement {
|
|
44
|
+
static properties = {
|
|
45
|
+
variant: { type: String, reflect: true, default: "light" },
|
|
46
|
+
color: { type: String, reflect: true },
|
|
47
|
+
gradient: { type: String, reflect: true },
|
|
48
|
+
size: { type: String, reflect: true, default: "sm" },
|
|
49
|
+
radius: { type: String, reflect: true },
|
|
50
|
+
normalCase: { type: Boolean, reflect: true },
|
|
51
|
+
};
|
|
52
|
+
static styles = [radiusStyles, variantStyles, styles];
|
|
53
|
+
static template = `<span class="badge" part="badge"><span class="dot" part="dot"></span><slot></slot></span>`;
|
|
54
|
+
|
|
55
|
+
update() {
|
|
56
|
+
const el = this.$(".badge");
|
|
57
|
+
el.dataset.variant = this.variant || "light";
|
|
58
|
+
applyColor(el, this.color);
|
|
59
|
+
applyGradient(el, this.gradient);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
define("ray-badge", RayBadge);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// <ray-blockquote>: a quotation set apart from the text (Mantine's
|
|
2
|
+
// Blockquote): a colored edge, a quote mark on it and the attribution
|
|
3
|
+
// below. The content goes into a real <blockquote>, so it is announced as
|
|
4
|
+
// one; `href` (the source's URL) becomes its cite attribute.
|
|
5
|
+
//
|
|
6
|
+
// <ray-blockquote cite="— Grace Hopper" color="teal">
|
|
7
|
+
// The most dangerous phrase in the language is "We've always done it this way."
|
|
8
|
+
// </ray-blockquote>
|
|
9
|
+
//
|
|
10
|
+
// Variants: light (tinted, the default), outline (the edge only) and
|
|
11
|
+
// filled (solid color, for a pull quote in a dark band).
|
|
12
|
+
|
|
13
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
14
|
+
import { applyColor } from "../../core/styles.js";
|
|
15
|
+
|
|
16
|
+
const RADII = ["xs", "sm", "md", "lg", "xl"];
|
|
17
|
+
|
|
18
|
+
const styles = css`
|
|
19
|
+
:host { display: block; --_fs: var(--ray-font-size-md); --_icon: 1.75rem; --_p: var(--ray-space-md); --_edge: 3px; }
|
|
20
|
+
:host([size="sm"]) { --_fs: var(--ray-font-size-sm); --_icon: 1.5rem; }
|
|
21
|
+
:host([size="lg"]) { --_fs: var(--ray-font-size-lg); --_icon: 2.25rem; }
|
|
22
|
+
:host([hide-edge]) { --_edge: 0px; }
|
|
23
|
+
figure { position: relative; margin: 0; }
|
|
24
|
+
/* Room for the mark, so it does not cover what is above or beside. */
|
|
25
|
+
:host([data-icon]) figure { padding: calc(var(--_icon) / 2) 0 0 calc(var(--_icon) / 2); }
|
|
26
|
+
blockquote {
|
|
27
|
+
margin: 0;
|
|
28
|
+
padding: var(--_p) calc(var(--_p) * 1.25);
|
|
29
|
+
font-size: var(--_fs);
|
|
30
|
+
line-height: var(--ray-line-height-lg, 1.6);
|
|
31
|
+
color: var(--ray-foreground);
|
|
32
|
+
background: var(--_light);
|
|
33
|
+
border-inline-start: var(--_edge) solid var(--_filled);
|
|
34
|
+
border-radius: var(--_radius, var(--ray-radius-md));
|
|
35
|
+
}
|
|
36
|
+
:host([variant="outline"]) blockquote { background: transparent; border: 1px solid var(--ray-border); border-inline-start: var(--_edge) solid var(--_filled); }
|
|
37
|
+
:host([variant="outline"][hide-edge]) blockquote { border-inline-start-width: 1px; border-inline-start-color: var(--ray-border); }
|
|
38
|
+
:host([variant="filled"]) blockquote { color: var(--_contrast); background: var(--_filled); border-inline-start-color: transparent; }
|
|
39
|
+
::slotted(p) { margin: 0 0 var(--ray-space-2); }
|
|
40
|
+
::slotted(p:last-of-type) { margin-bottom: 0; }
|
|
41
|
+
|
|
42
|
+
/* The quote mark: a disc on the box's top-start corner. */
|
|
43
|
+
.icon {
|
|
44
|
+
position: absolute;
|
|
45
|
+
top: 0;
|
|
46
|
+
left: 0;
|
|
47
|
+
display: grid;
|
|
48
|
+
place-items: center;
|
|
49
|
+
width: var(--_icon);
|
|
50
|
+
height: var(--_icon);
|
|
51
|
+
color: var(--_filled);
|
|
52
|
+
background: var(--ray-background);
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
box-shadow: 0 0 0 1px var(--ray-border);
|
|
55
|
+
--ray-icon-size: calc(var(--_icon) * 0.5);
|
|
56
|
+
}
|
|
57
|
+
.icon[hidden] { display: none; }
|
|
58
|
+
|
|
59
|
+
.cite {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: var(--ray-space-3);
|
|
63
|
+
margin-top: var(--ray-space-2);
|
|
64
|
+
padding-inline-start: calc(var(--_edge) + var(--_p) * 1.25);
|
|
65
|
+
font-size: var(--ray-font-size-sm);
|
|
66
|
+
font-style: normal;
|
|
67
|
+
color: var(--ray-muted-foreground);
|
|
68
|
+
}
|
|
69
|
+
.cite:not([data-filled]) { display: none; }
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
export class RayBlockquote extends RayElement {
|
|
73
|
+
static properties = {
|
|
74
|
+
cite: { type: String },
|
|
75
|
+
href: { type: String },
|
|
76
|
+
color: { type: String, reflect: true },
|
|
77
|
+
variant: { type: String, reflect: true, default: "light" },
|
|
78
|
+
icon: { type: String, default: "quote" },
|
|
79
|
+
padding: { type: String, reflect: true },
|
|
80
|
+
hideEdge: { type: Boolean, reflect: true },
|
|
81
|
+
size: { type: String, reflect: true, default: "md" },
|
|
82
|
+
radius: { type: String, reflect: true },
|
|
83
|
+
};
|
|
84
|
+
static styles = [styles];
|
|
85
|
+
static template = `
|
|
86
|
+
<figure part="figure">
|
|
87
|
+
<span class="icon" part="icon" aria-hidden="true"><slot name="icon"><span data-ray-icon="quote"></span></slot></span>
|
|
88
|
+
<blockquote part="quote"><slot></slot></blockquote>
|
|
89
|
+
<figcaption class="cite" part="cite"><slot name="cite"><cite class="cite-text"></cite></slot></figcaption>
|
|
90
|
+
</figure>`;
|
|
91
|
+
|
|
92
|
+
constructor() {
|
|
93
|
+
super();
|
|
94
|
+
for (const name of ["cite", "icon"]) {
|
|
95
|
+
this.$(`slot[name="${name}"]`).addEventListener("slotchange", () => this.requestUpdate());
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
update() {
|
|
100
|
+
applyColor(this.$("figure"), this.color);
|
|
101
|
+
const quote = this.$("blockquote");
|
|
102
|
+
if (this.href) quote.setAttribute("cite", this.href);
|
|
103
|
+
else quote.removeAttribute("cite");
|
|
104
|
+
// The quote mark: the icon slot, a registry name, or none.
|
|
105
|
+
const slotted = this.querySelector(':scope > [slot="icon"]');
|
|
106
|
+
const name = this.icon === "none" ? "" : this.icon || "quote";
|
|
107
|
+
const mark = this.$('slot[name="icon"] > span');
|
|
108
|
+
if (mark.dataset.rayIcon !== name) {
|
|
109
|
+
mark.dataset.rayIcon = name;
|
|
110
|
+
this.paintIcons(this.$(".icon"));
|
|
111
|
+
}
|
|
112
|
+
const shown = Boolean(slotted || name);
|
|
113
|
+
this.$(".icon").hidden = !shown;
|
|
114
|
+
this.toggleAttribute("data-icon", shown);
|
|
115
|
+
// The attribution: the cite slot, else the cite text.
|
|
116
|
+
this.$(".cite-text").textContent = this.cite ?? "";
|
|
117
|
+
const hasCite = Boolean(this.querySelector(':scope > [slot="cite"]') || this.cite);
|
|
118
|
+
this.$(".cite").toggleAttribute("data-filled", hasCite);
|
|
119
|
+
const pad = this.padding;
|
|
120
|
+
if (pad) this.style.setProperty("--_p", RADII.includes(pad) ? `var(--ray-space-${pad})` : /^\d+(\.\d+)?$/.test(pad) ? `${pad}px` : pad);
|
|
121
|
+
else this.style.removeProperty("--_p");
|
|
122
|
+
const r = this.radius;
|
|
123
|
+
if (r) this.style.setProperty("--_radius", RADII.includes(r) ? `var(--ray-radius-${r})` : r === "none" ? "0" : r);
|
|
124
|
+
else this.style.removeProperty("--_radius");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
define("ray-blockquote", RayBlockquote);
|