@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,510 @@
|
|
|
1
|
+
// <ray-command-palette>: search and run actions from the keyboard (Mantine's
|
|
2
|
+
// Spotlight, ray-sublime's palette). A modal <dialog> with the WAI-ARIA
|
|
3
|
+
// combobox + listbox pattern: focus stays in the search field, arrows move
|
|
4
|
+
// the active option (aria-activedescendant), Enter runs it, Escape closes.
|
|
5
|
+
//
|
|
6
|
+
// palette.actions = [
|
|
7
|
+
// { id: "open", label: "Open file…", group: "File", shortcut: "⌘O", keywords: ["load"],
|
|
8
|
+
// onSelect: () => openFile() },
|
|
9
|
+
// ];
|
|
10
|
+
//
|
|
11
|
+
// Running an action fires ray-select ({ action }) and calls action.onSelect.
|
|
12
|
+
// `hotkey` ("mod+k" by default; mod = ⌘ on macOS, Ctrl elsewhere) toggles it.
|
|
13
|
+
//
|
|
14
|
+
// With `native-menus` and a native host that offers "menu_items" (a std/ui
|
|
15
|
+
// window with core/raylang.js), it also lists the app's menu bar, grouped by
|
|
16
|
+
// menu, with their shortcuts; running one gives the program the same event
|
|
17
|
+
// as the menu (action.native is true, action.id is "menu:<tag>").
|
|
18
|
+
|
|
19
|
+
import { RayElement, css, define, getIcon } from "../../core/core.js";
|
|
20
|
+
import { matchesHotkey, formatHotkey } from "../../core/hotkey.js";
|
|
21
|
+
import { getHost, hostCan } from "../../core/host.js";
|
|
22
|
+
import { play, playAll } from "../../core/motion.js";
|
|
23
|
+
import { overlayStyles, applyOverlayBlur } from "../../core/styles.js";
|
|
24
|
+
|
|
25
|
+
const styles = css`
|
|
26
|
+
:host { display: contents; }
|
|
27
|
+
dialog {
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
width: min(37.5rem, calc(100vw - 2rem));
|
|
30
|
+
max-height: min(32rem, calc(100dvh - 14vh - 1rem));
|
|
31
|
+
margin: 12vh auto auto;
|
|
32
|
+
padding: 0;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
color: var(--ray-popover-foreground);
|
|
35
|
+
background: var(--ray-popover);
|
|
36
|
+
border: 1px solid var(--ray-overlay-border);
|
|
37
|
+
background-clip: padding-box;
|
|
38
|
+
border-radius: var(--ray-radius-lg);
|
|
39
|
+
box-shadow: var(--ray-shadow-xl);
|
|
40
|
+
}
|
|
41
|
+
dialog[open] { display: flex; flex-direction: column; animation: ray-palette-in var(--ray-duration-base) var(--ray-easing-out); }
|
|
42
|
+
@keyframes ray-palette-in { from { opacity: 0; transform: translateY(-0.5rem) scale(0.98); } }
|
|
43
|
+
/* The search field: a filled, inset box (the look of <ray-search>). */
|
|
44
|
+
.search {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex: none;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: var(--ray-space-2);
|
|
49
|
+
height: 2.75rem;
|
|
50
|
+
margin: var(--ray-space-2);
|
|
51
|
+
padding: 0 var(--ray-space-3);
|
|
52
|
+
background: var(--ray-muted);
|
|
53
|
+
/* Concentric with the dialog: its radius minus the margin. */
|
|
54
|
+
border-radius: calc(var(--ray-radius-lg) - var(--ray-space-2));
|
|
55
|
+
}
|
|
56
|
+
.search svg { width: 1rem; height: 1rem; color: var(--ray-muted-foreground); }
|
|
57
|
+
input {
|
|
58
|
+
flex: 1;
|
|
59
|
+
min-width: 0;
|
|
60
|
+
height: 100%;
|
|
61
|
+
padding: 0;
|
|
62
|
+
font: inherit;
|
|
63
|
+
font-size: var(--ray-font-size-sm);
|
|
64
|
+
color: inherit;
|
|
65
|
+
background: transparent;
|
|
66
|
+
border: 0;
|
|
67
|
+
outline: none;
|
|
68
|
+
}
|
|
69
|
+
input::placeholder { color: var(--ray-muted-foreground); }
|
|
70
|
+
/* flex-basis auto: with only a max-height on the dialog, Safari resolves
|
|
71
|
+
a 0% basis to zero and the list collapses. */
|
|
72
|
+
.list { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0 var(--ray-space-2) var(--ray-space-2); scroll-padding: var(--ray-space-2); }
|
|
73
|
+
.group-label { padding: var(--ray-space-2) var(--ray-space-2) var(--ray-space-1); font-size: var(--ray-font-size-xs); font-weight: var(--ray-font-weight-medium); color: var(--ray-muted-foreground); }
|
|
74
|
+
.option {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: var(--ray-space-2);
|
|
78
|
+
min-height: 2.5rem;
|
|
79
|
+
padding: var(--ray-space-1) var(--ray-space-3);
|
|
80
|
+
border-radius: var(--ray-radius-md);
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
user-select: none;
|
|
83
|
+
}
|
|
84
|
+
.option[aria-selected="true"] { background: var(--ray-accent); color: var(--ray-accent-foreground); }
|
|
85
|
+
.option[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
|
|
86
|
+
.icon { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 1.25rem; height: 1.25rem; color: var(--ray-muted-foreground); }
|
|
87
|
+
.option[aria-selected="true"] .icon { color: inherit; }
|
|
88
|
+
.icon svg { width: 1rem; height: 1rem; }
|
|
89
|
+
.text { display: flex; flex: 1; flex-direction: column; min-width: 0; }
|
|
90
|
+
.label, .description { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
91
|
+
.label { font-size: var(--ray-font-size-sm); }
|
|
92
|
+
.description { font-size: var(--ray-font-size-xs); color: var(--ray-muted-foreground); }
|
|
93
|
+
mark { color: inherit; background: none; font-weight: var(--ray-font-weight-bold); text-decoration: underline; text-decoration-color: var(--ray-primary); text-underline-offset: 2px; }
|
|
94
|
+
.shortcut { flex: none; display: inline-flex; gap: 0.125rem; }
|
|
95
|
+
.shortcut span {
|
|
96
|
+
min-width: 1.25rem;
|
|
97
|
+
padding: 0 0.3rem;
|
|
98
|
+
font-family: var(--ray-font-mono);
|
|
99
|
+
font-size: 0.6875rem;
|
|
100
|
+
line-height: 1.25rem;
|
|
101
|
+
text-align: center;
|
|
102
|
+
color: var(--ray-muted-foreground);
|
|
103
|
+
background: var(--ray-background);
|
|
104
|
+
border: 1px solid var(--ray-border);
|
|
105
|
+
border-radius: var(--ray-radius-sm);
|
|
106
|
+
}
|
|
107
|
+
.empty { padding: var(--ray-space-8) var(--ray-space-4); text-align: center; font-size: var(--ray-font-size-sm); color: var(--ray-muted-foreground); }
|
|
108
|
+
.empty[hidden] { display: none; }
|
|
109
|
+
.hint { display: flex; gap: var(--ray-space-4); padding: var(--ray-space-2) var(--ray-space-4); font-size: var(--ray-font-size-xs); color: var(--ray-muted-foreground); border-top: 1px solid var(--ray-border); }
|
|
110
|
+
.hint b {
|
|
111
|
+
display: inline-block;
|
|
112
|
+
min-width: 1.25rem;
|
|
113
|
+
margin-right: var(--ray-space-1);
|
|
114
|
+
padding: 0 0.3rem;
|
|
115
|
+
font-family: var(--ray-font-mono);
|
|
116
|
+
font-size: 0.6875rem;
|
|
117
|
+
font-weight: var(--ray-font-weight-medium);
|
|
118
|
+
line-height: 1.125rem;
|
|
119
|
+
text-align: center;
|
|
120
|
+
color: var(--ray-foreground);
|
|
121
|
+
background: var(--ray-muted);
|
|
122
|
+
border: 1px solid var(--ray-border);
|
|
123
|
+
border-radius: var(--ray-radius-sm);
|
|
124
|
+
}
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
const SEARCH_ICON = `<span data-ray-icon="search" aria-hidden="true"></span>`;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* How well `action` matches `query` (0 = not at all). Every word of the query
|
|
131
|
+
* must appear in order in the action's text; prefixes and word starts of the
|
|
132
|
+
* label score higher.
|
|
133
|
+
*/
|
|
134
|
+
export function score(action, query) {
|
|
135
|
+
const q = query.trim().toLowerCase();
|
|
136
|
+
if (!q) return 1;
|
|
137
|
+
const label = String(action.label ?? "").toLowerCase();
|
|
138
|
+
const extra = [action.description, action.group, ...(Array.isArray(action.keywords) ? action.keywords : [action.keywords])]
|
|
139
|
+
.filter(Boolean).join(" ").toLowerCase();
|
|
140
|
+
let total = 0;
|
|
141
|
+
for (const word of q.split(/\s+/)) {
|
|
142
|
+
let s = 0;
|
|
143
|
+
if (label.startsWith(word)) s = 8;
|
|
144
|
+
else if (new RegExp(`(^|[\\s\\-_./])${escapeRe(word)}`).test(label)) s = 6;
|
|
145
|
+
else if (label.includes(word)) s = 4;
|
|
146
|
+
else if (extra.includes(word)) s = 3;
|
|
147
|
+
else if (subsequence(word, label)) s = 1;
|
|
148
|
+
else return 0;
|
|
149
|
+
total += s;
|
|
150
|
+
}
|
|
151
|
+
return total;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Whether `needle`'s letters appear in order in `hay`, close together
|
|
156
|
+
* (within twice the needle's length): "opf" matches "open file" but "run"
|
|
157
|
+
* does not match "format document".
|
|
158
|
+
*/
|
|
159
|
+
function subsequence(needle, hay) {
|
|
160
|
+
if (needle.length < 2) return false;
|
|
161
|
+
for (let start = hay.indexOf(needle[0]); start >= 0; start = hay.indexOf(needle[0], start + 1)) {
|
|
162
|
+
let i = 1;
|
|
163
|
+
let end = start;
|
|
164
|
+
for (let j = start + 1; j < hay.length && i < needle.length; j++) {
|
|
165
|
+
if (hay[j] === needle[i]) {
|
|
166
|
+
i++;
|
|
167
|
+
end = j;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (i === needle.length && end - start + 1 <= needle.length * 2) return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function escapeRe(s) {
|
|
176
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export class RayCommandPalette extends RayElement {
|
|
180
|
+
static properties = {
|
|
181
|
+
open: { type: Boolean, reflect: true },
|
|
182
|
+
actions: { attribute: false },
|
|
183
|
+
query: { type: String, default: "" },
|
|
184
|
+
placeholder: { type: String, default: "Search commands…" },
|
|
185
|
+
hotkey: { type: String, default: "mod+k" },
|
|
186
|
+
limit: { type: Number, default: 50 },
|
|
187
|
+
emptyLabel: { type: String, default: "Nothing found" },
|
|
188
|
+
label: { type: String, default: "Command palette" },
|
|
189
|
+
hideHint: { type: Boolean, reflect: true },
|
|
190
|
+
overlayBlur: { type: String },
|
|
191
|
+
nativeMenus: { type: Boolean, attribute: "native-menus", reflect: true },
|
|
192
|
+
};
|
|
193
|
+
static styles = [overlayStyles, styles];
|
|
194
|
+
static template = `
|
|
195
|
+
<dialog part="dialog">
|
|
196
|
+
<div class="search" part="search">${SEARCH_ICON}<input id="input" part="input" role="combobox" aria-expanded="true" aria-controls="list" aria-autocomplete="list" autocomplete="off" spellcheck="false"></div>
|
|
197
|
+
<div class="list" id="list" role="listbox" part="list"></div>
|
|
198
|
+
<div class="empty" part="empty" hidden></div>
|
|
199
|
+
<div class="hint" part="hint"><span><b>↑↓</b> navigate</span><span><b>↵</b> run</span><span><b>esc</b> close</span></div>
|
|
200
|
+
<slot name="overlay"></slot>
|
|
201
|
+
</dialog>`;
|
|
202
|
+
|
|
203
|
+
constructor() {
|
|
204
|
+
super();
|
|
205
|
+
this.dialog = this.$("dialog");
|
|
206
|
+
this.input = this.$("input");
|
|
207
|
+
this.list = this.$(".list");
|
|
208
|
+
this._active = 0;
|
|
209
|
+
this._shown = [];
|
|
210
|
+
this._menuActions = [];
|
|
211
|
+
this.input.addEventListener("input", () => {
|
|
212
|
+
this.commitProperty("query", this.input.value);
|
|
213
|
+
this._active = 0;
|
|
214
|
+
});
|
|
215
|
+
this.input.addEventListener("keydown", (e) => this._key(e));
|
|
216
|
+
this.list.addEventListener("click", (e) => {
|
|
217
|
+
const opt = e.target.closest(".option");
|
|
218
|
+
if (opt) this._run(Number(opt.dataset.index));
|
|
219
|
+
});
|
|
220
|
+
this.list.addEventListener("pointermove", (e) => {
|
|
221
|
+
const opt = e.target.closest(".option");
|
|
222
|
+
if (opt && Number(opt.dataset.index) !== this._active) {
|
|
223
|
+
this._active = Number(opt.dataset.index);
|
|
224
|
+
this._paintActive();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
this.dialog.addEventListener("close", () => {
|
|
228
|
+
if (this.open) this.commitProperty("open", false);
|
|
229
|
+
this._returnFocus?.focus?.({ preventScroll: true });
|
|
230
|
+
this._returnFocus = null;
|
|
231
|
+
this.emit("ray-close");
|
|
232
|
+
});
|
|
233
|
+
this.dialog.addEventListener("click", (e) => {
|
|
234
|
+
if (e.target !== this.dialog) return;
|
|
235
|
+
const r = this.dialog.getBoundingClientRect();
|
|
236
|
+
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) this.hide();
|
|
237
|
+
});
|
|
238
|
+
this._onHotkey = (e) => {
|
|
239
|
+
// Another palette with the same hotkey already took it: one opens, not all.
|
|
240
|
+
if (e.defaultPrevented) return;
|
|
241
|
+
if (this._matchesHotkey(e)) {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
this.toggle();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
connectedCallback() {
|
|
249
|
+
super.connectedCallback();
|
|
250
|
+
document.addEventListener("keydown", this._onHotkey);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
disconnectedCallback() {
|
|
254
|
+
super.disconnectedCallback();
|
|
255
|
+
document.removeEventListener("keydown", this._onHotkey);
|
|
256
|
+
if (this.dialog.open) this.dialog.close();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
show() { this.open = true; }
|
|
260
|
+
hide() { this.open = false; }
|
|
261
|
+
toggle() { this.open = !this.open; }
|
|
262
|
+
|
|
263
|
+
/** The actions matching the current query, best first, grouped. */
|
|
264
|
+
get results() {
|
|
265
|
+
const all = (Array.isArray(this.actions) ? this.actions : []).concat(this._menuActions);
|
|
266
|
+
const scored = all
|
|
267
|
+
.map((action, i) => ({ action, i, s: score(action, this.query ?? "") }))
|
|
268
|
+
.filter((x) => x.s > 0);
|
|
269
|
+
if ((this.query ?? "").trim()) scored.sort((a, b) => b.s - a.s || a.i - b.i);
|
|
270
|
+
const limited = scored.slice(0, this.limit ?? 50).map((x) => x.action);
|
|
271
|
+
// Keep groups together, in the order their best match appears.
|
|
272
|
+
const order = [];
|
|
273
|
+
for (const a of limited) if (!order.includes(a.group ?? "")) order.push(a.group ?? "");
|
|
274
|
+
return order.flatMap((g) => limited.filter((a) => (a.group ?? "") === g));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
update(changed) {
|
|
278
|
+
if (changed.has("overlayBlur")) applyOverlayBlur(this, this.overlayBlur);
|
|
279
|
+
this.input.placeholder = this.placeholder ?? "";
|
|
280
|
+
this.input.setAttribute("aria-label", this.label ?? "Command palette");
|
|
281
|
+
this.dialog.setAttribute("aria-label", this.label ?? "Command palette");
|
|
282
|
+
if (this.input.value !== (this.query ?? "")) this.input.value = this.query ?? "";
|
|
283
|
+
this.$(".hint").hidden = Boolean(this.hideHint);
|
|
284
|
+
this._render();
|
|
285
|
+
if (changed.has("open") || this._first === undefined) {
|
|
286
|
+
this._first = false;
|
|
287
|
+
if (this.open && this._exit) {
|
|
288
|
+
// Reopened while animating out.
|
|
289
|
+
this._exit.cancel();
|
|
290
|
+
this._exit = null;
|
|
291
|
+
} else if (this.open && !this.dialog.open) {
|
|
292
|
+
this._returnFocus = document.activeElement;
|
|
293
|
+
this.dialog.showModal();
|
|
294
|
+
this.input.focus();
|
|
295
|
+
this.input.select();
|
|
296
|
+
this._loadMenus();
|
|
297
|
+
this.emit("ray-open");
|
|
298
|
+
} else if (!this.open && this.dialog.open && !this._exit) {
|
|
299
|
+
this._leave();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Refreshes the native menu items (with `native-menus`); resolves to them. */
|
|
305
|
+
async _loadMenus() {
|
|
306
|
+
if (!this.nativeMenus || !hostCan("menu_items")) {
|
|
307
|
+
if (this._menuActions.length) {
|
|
308
|
+
this._menuActions = [];
|
|
309
|
+
this.requestUpdate();
|
|
310
|
+
}
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
const host = getHost();
|
|
314
|
+
let items = [];
|
|
315
|
+
try {
|
|
316
|
+
items = await host.menuItems();
|
|
317
|
+
} catch {
|
|
318
|
+
items = [];
|
|
319
|
+
}
|
|
320
|
+
this._menuActions = items.map((m) => ({
|
|
321
|
+
id: `menu:${m.tag}`,
|
|
322
|
+
label: m.title,
|
|
323
|
+
group: m.menu,
|
|
324
|
+
shortcut: m.shortcut ? formatHotkey(m.shortcut.replace(/\bcmd\b/i, "mod")) : "",
|
|
325
|
+
disabled: !m.enabled,
|
|
326
|
+
native: true,
|
|
327
|
+
tag: m.tag,
|
|
328
|
+
onSelect: () => host.runMenu(m.tag),
|
|
329
|
+
}));
|
|
330
|
+
this.requestUpdate();
|
|
331
|
+
return this._menuActions;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Animate out (panel and backdrop), then really close.
|
|
335
|
+
async _leave() {
|
|
336
|
+
const exit = playAll([
|
|
337
|
+
play(this.dialog, [{ opacity: 1, transform: "none" }, { opacity: 0, transform: "translateY(-0.25rem) scale(0.98)" }], { speed: "fast" }),
|
|
338
|
+
play(this.dialog, [{ opacity: 1 }, { opacity: 0 }], { speed: "fast", pseudoElement: "::backdrop" }),
|
|
339
|
+
]);
|
|
340
|
+
this._exit = exit;
|
|
341
|
+
await exit.finished;
|
|
342
|
+
if (this._exit !== exit) return;
|
|
343
|
+
this._exit = null;
|
|
344
|
+
if (!this.open && this.dialog.open) this.dialog.close();
|
|
345
|
+
exit.cancel();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_render() {
|
|
349
|
+
const results = this.results;
|
|
350
|
+
this._shown = results;
|
|
351
|
+
if (this._active >= results.length) this._active = Math.max(0, results.length - 1);
|
|
352
|
+
const frag = document.createDocumentFragment();
|
|
353
|
+
let group = null;
|
|
354
|
+
let container = frag;
|
|
355
|
+
results.forEach((a, index) => {
|
|
356
|
+
const g = a.group ?? "";
|
|
357
|
+
if (g !== group) {
|
|
358
|
+
group = g;
|
|
359
|
+
if (g) {
|
|
360
|
+
const box = document.createElement("div");
|
|
361
|
+
box.setAttribute("role", "group");
|
|
362
|
+
const label = document.createElement("div");
|
|
363
|
+
label.className = "group-label";
|
|
364
|
+
label.id = `group-${index}`;
|
|
365
|
+
label.setAttribute("role", "presentation");
|
|
366
|
+
label.textContent = g;
|
|
367
|
+
box.setAttribute("aria-labelledby", label.id);
|
|
368
|
+
box.append(label);
|
|
369
|
+
frag.append(box);
|
|
370
|
+
container = box;
|
|
371
|
+
} else {
|
|
372
|
+
container = frag;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
container.append(this._option(a, index));
|
|
376
|
+
});
|
|
377
|
+
this.list.replaceChildren(frag);
|
|
378
|
+
const empty = this.$(".empty");
|
|
379
|
+
empty.hidden = results.length > 0;
|
|
380
|
+
empty.textContent = this.emptyLabel ?? "Nothing found";
|
|
381
|
+
this._paintActive();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_option(a, index) {
|
|
385
|
+
const opt = document.createElement("div");
|
|
386
|
+
opt.className = "option";
|
|
387
|
+
opt.id = `option-${index}`;
|
|
388
|
+
opt.dataset.index = String(index);
|
|
389
|
+
opt.setAttribute("role", "option");
|
|
390
|
+
opt.setAttribute("part", "option");
|
|
391
|
+
if (a.disabled) opt.setAttribute("aria-disabled", "true");
|
|
392
|
+
if (a.icon) {
|
|
393
|
+
const icon = document.createElement("span");
|
|
394
|
+
icon.className = "icon";
|
|
395
|
+
icon.setAttribute("aria-hidden", "true");
|
|
396
|
+
// A Node, a registered icon name ("file", "search"…) or any text (an emoji, "▶").
|
|
397
|
+
if (a.icon instanceof Node) icon.append(a.icon);
|
|
398
|
+
else if (getIcon(String(a.icon))) icon.innerHTML = getIcon(String(a.icon));
|
|
399
|
+
else icon.textContent = String(a.icon);
|
|
400
|
+
opt.append(icon);
|
|
401
|
+
}
|
|
402
|
+
const text = document.createElement("span");
|
|
403
|
+
text.className = "text";
|
|
404
|
+
const label = document.createElement("span");
|
|
405
|
+
label.className = "label";
|
|
406
|
+
appendHighlighted(label, String(a.label ?? ""), this.query ?? "");
|
|
407
|
+
text.append(label);
|
|
408
|
+
if (a.description) {
|
|
409
|
+
const d = document.createElement("span");
|
|
410
|
+
d.className = "description";
|
|
411
|
+
d.textContent = a.description;
|
|
412
|
+
text.append(d);
|
|
413
|
+
}
|
|
414
|
+
opt.append(text);
|
|
415
|
+
if (a.shortcut) {
|
|
416
|
+
const sc = document.createElement("span");
|
|
417
|
+
sc.className = "shortcut";
|
|
418
|
+
sc.setAttribute("aria-label", `Shortcut ${a.shortcut}`);
|
|
419
|
+
for (const k of String(a.shortcut).split(/\s*\+\s*|\s+/).filter(Boolean)) {
|
|
420
|
+
const key = document.createElement("span");
|
|
421
|
+
key.textContent = k;
|
|
422
|
+
sc.append(key);
|
|
423
|
+
}
|
|
424
|
+
opt.append(sc);
|
|
425
|
+
}
|
|
426
|
+
return opt;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_paintActive() {
|
|
430
|
+
const opts = this.list.querySelectorAll(".option");
|
|
431
|
+
opts.forEach((o) => o.setAttribute("aria-selected", String(Number(o.dataset.index) === this._active)));
|
|
432
|
+
const active = this.list.querySelector(`.option[data-index="${this._active}"]`);
|
|
433
|
+
if (active) {
|
|
434
|
+
this.input.setAttribute("aria-activedescendant", active.id);
|
|
435
|
+
active.scrollIntoView({ block: "nearest" });
|
|
436
|
+
} else {
|
|
437
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
_key(e) {
|
|
442
|
+
const n = this._shown.length;
|
|
443
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
444
|
+
e.preventDefault();
|
|
445
|
+
if (!n) return;
|
|
446
|
+
this._active = (this._active + (e.key === "ArrowDown" ? 1 : -1) + n) % n;
|
|
447
|
+
this._paintActive();
|
|
448
|
+
} else if (e.key === "Home" && e.ctrlKey) {
|
|
449
|
+
this._active = 0;
|
|
450
|
+
this._paintActive();
|
|
451
|
+
} else if (e.key === "End" && e.ctrlKey) {
|
|
452
|
+
this._active = Math.max(0, n - 1);
|
|
453
|
+
this._paintActive();
|
|
454
|
+
} else if (e.key === "Enter") {
|
|
455
|
+
e.preventDefault();
|
|
456
|
+
this._run(this._active);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
_run(index) {
|
|
461
|
+
const action = this._shown[index];
|
|
462
|
+
if (!action || action.disabled) return;
|
|
463
|
+
this.hide();
|
|
464
|
+
this.emit("ray-select", { action });
|
|
465
|
+
if (typeof action.onSelect === "function") action.onSelect(action);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
_matchesHotkey(e) {
|
|
469
|
+
return matchesHotkey(e, this.hotkey);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Appends `text` to `el`, wrapping the query words found in it in <mark>. */
|
|
475
|
+
function appendHighlighted(el, text, query) {
|
|
476
|
+
const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
477
|
+
if (!words.length) {
|
|
478
|
+
el.textContent = text;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const lower = text.toLowerCase();
|
|
482
|
+
const marks = new Array(text.length).fill(false);
|
|
483
|
+
for (const w of words) {
|
|
484
|
+
const at = lower.indexOf(w);
|
|
485
|
+
if (at >= 0) for (let i = at; i < at + w.length; i++) marks[i] = true;
|
|
486
|
+
}
|
|
487
|
+
let run = "";
|
|
488
|
+
let on = marks[0];
|
|
489
|
+
const flush = () => {
|
|
490
|
+
if (!run) return;
|
|
491
|
+
if (on) {
|
|
492
|
+
const m = document.createElement("mark");
|
|
493
|
+
m.textContent = run;
|
|
494
|
+
el.append(m);
|
|
495
|
+
} else {
|
|
496
|
+
el.append(run);
|
|
497
|
+
}
|
|
498
|
+
run = "";
|
|
499
|
+
};
|
|
500
|
+
for (let i = 0; i < text.length; i++) {
|
|
501
|
+
if (marks[i] !== on) {
|
|
502
|
+
flush();
|
|
503
|
+
on = marks[i];
|
|
504
|
+
}
|
|
505
|
+
run += text[i];
|
|
506
|
+
}
|
|
507
|
+
flush();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
define("ray-command-palette", RayCommandPalette);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// <ray-container>: its content centered, up to a maximum width, with side
|
|
2
|
+
// padding (Mantine's Container). `fluid` drops the maximum.
|
|
3
|
+
|
|
4
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
5
|
+
import { spaceValue } from "../../core/styles.js";
|
|
6
|
+
|
|
7
|
+
export const CONTAINER_SIZES = { xs: "33.75rem", sm: "45rem", md: "60rem", lg: "71.25rem", xl: "82.5rem" };
|
|
8
|
+
|
|
9
|
+
const styles = css`
|
|
10
|
+
:host {
|
|
11
|
+
--_max: ${CONTAINER_SIZES.md};
|
|
12
|
+
--_px: var(--ray-space-md);
|
|
13
|
+
display: block;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
width: 100%;
|
|
16
|
+
max-width: var(--_max);
|
|
17
|
+
margin-inline: auto;
|
|
18
|
+
padding-inline: var(--_px);
|
|
19
|
+
}
|
|
20
|
+
:host([hidden]) { display: none; }
|
|
21
|
+
:host([fluid]) { max-width: none; }
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export class RayContainer extends RayElement {
|
|
25
|
+
static properties = {
|
|
26
|
+
size: { type: String, reflect: true, default: "md" },
|
|
27
|
+
padding: { type: String, reflect: true, default: "md" },
|
|
28
|
+
fluid: { type: Boolean, reflect: true },
|
|
29
|
+
};
|
|
30
|
+
static styles = [styles];
|
|
31
|
+
static template = `<slot></slot>`;
|
|
32
|
+
|
|
33
|
+
update() {
|
|
34
|
+
this.style.setProperty("--_max", CONTAINER_SIZES[this.size] ?? (this.size || CONTAINER_SIZES.md));
|
|
35
|
+
const px = spaceValue(this.padding);
|
|
36
|
+
if (px) this.style.setProperty("--_px", px);
|
|
37
|
+
else this.style.removeProperty("--_px");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
define("ray-container", RayContainer);
|