@raylang/elements 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/base.css +1 -1
- package/components/accordion/accordion.js +7 -4
- package/components/alert/alert.js +2 -1
- package/components/attachment/attachment.js +4 -5
- package/components/blockquote/blockquote.js +2 -2
- package/components/bottom-nav/bottom-nav.js +3 -3
- package/components/carousel/carousel.js +22 -10
- package/components/checkbox/checkbox.js +5 -2
- package/components/checkbox-group/checkbox-group.js +1 -1
- package/components/code/code.js +16 -11
- package/components/color-picker/color-picker.js +5 -5
- package/components/combobox/combobox.js +2 -1
- package/components/command-palette/command-palette.js +17 -2
- package/components/date-picker/date-picker.js +5 -3
- package/components/dialog/dialog.js +1 -1
- package/components/doc-tabs/doc-tabs.js +255 -0
- package/components/drawer/drawer.js +27 -9
- package/components/icon/icon.js +4 -1
- package/components/icon-button/icon-button.js +4 -0
- package/components/indicator/indicator.js +2 -1
- package/components/input/input.js +13 -1
- package/components/menu/menu.js +43 -8
- package/components/mode-input/mode-input.js +221 -0
- package/components/nav-link/nav-link.js +5 -2
- package/components/notification/notification.js +15 -12
- package/components/progress/progress.js +2 -2
- package/components/radio/radio.js +4 -2
- package/components/scroll-area/scroll-area.js +14 -11
- package/components/search/search.js +3 -3
- package/components/segmented-control/segmented-control.js +5 -3
- package/components/select/select.js +3 -3
- package/components/separator/separator.js +8 -3
- package/components/sidebar/sidebar.js +154 -11
- package/components/slider/slider.js +5 -2
- package/components/split/split.js +25 -7
- package/components/stepper/stepper.js +4 -4
- package/components/switch/switch.js +3 -3
- package/components/table/table.js +5 -5
- package/components/tabs/tabs.js +55 -14
- package/components/textarea/textarea.js +13 -1
- package/components/timeline/timeline.js +13 -8
- package/components/toggle-group/toggle-group.js +6 -4
- package/core/complete.js +252 -0
- package/core/core.js +6 -1
- package/core/direction.js +19 -0
- package/core/icons.js +12 -0
- package/core/position.js +11 -3
- package/core/scroll.js +85 -1
- package/core/styles.js +1 -1
- package/core/textlayer.js +214 -0
- package/core.js +1 -0
- package/custom-elements.json +1487 -143
- package/index.js +2 -0
- package/llms-full.txt +224 -28
- package/llms.txt +26 -4
- package/package.json +9 -1
- package/ray-elements.d.ts +190 -31
- package/vscode.html-custom-data.json +477 -22
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// A layer behind a text control (the <input> of ray-input, the <textarea>
|
|
2
|
+
// of ray-textarea) with the same text laid out the same way, transparent,
|
|
3
|
+
// to paint ranges of it: `highlight` tints tokens such as {{variables}},
|
|
4
|
+
// and the position of any character can be measured (to place suggestions
|
|
5
|
+
// under the caret). The control keeps its own text, caret, selection and
|
|
6
|
+
// IME; the layer only paints behind it.
|
|
7
|
+
|
|
8
|
+
import { css } from "./core.js";
|
|
9
|
+
|
|
10
|
+
/** The layer's look, for the component's styles. */
|
|
11
|
+
export const textLayerStyles = css`
|
|
12
|
+
.control { position: relative; }
|
|
13
|
+
.text-layer {
|
|
14
|
+
position: absolute;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
color: transparent;
|
|
20
|
+
pointer-events: none;
|
|
21
|
+
}
|
|
22
|
+
.text-layer > div { flex: none; white-space: pre; }
|
|
23
|
+
.text-layer.multi { display: block; }
|
|
24
|
+
.text-layer.multi > div { white-space: pre-wrap; overflow-wrap: break-word; }
|
|
25
|
+
.text-layer mark {
|
|
26
|
+
--_tint: color-mix(in srgb, var(--ray-primary) 18%, transparent);
|
|
27
|
+
color: transparent;
|
|
28
|
+
background: var(--_tint);
|
|
29
|
+
border-radius: 3px;
|
|
30
|
+
box-shadow: 0 0 0 1.5px var(--_tint);
|
|
31
|
+
}
|
|
32
|
+
.text-layer mark[data-kind="error"] {
|
|
33
|
+
--_tint: color-mix(in srgb, var(--ray-destructive) 16%, transparent);
|
|
34
|
+
text-decoration: underline wavy var(--ray-destructive);
|
|
35
|
+
text-decoration-skip-ink: none;
|
|
36
|
+
text-underline-offset: 3px;
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
// Copied from the control so the layer's text falls on the same pixels.
|
|
41
|
+
const LAYOUT = [
|
|
42
|
+
"fontFamily", "fontSize", "fontWeight", "fontStyle", "fontStretch", "fontFeatureSettings", "fontVariantLigatures",
|
|
43
|
+
"letterSpacing", "wordSpacing", "lineHeight", "textTransform", "textIndent", "tabSize", "direction",
|
|
44
|
+
"paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The ranges a highlight function gives, checked and sorted: within the
|
|
49
|
+
* text, not overlapping (an overlapping one is dropped), start < end.
|
|
50
|
+
* @param {Array<{start: number, end: number, kind?: string, title?: string}> | null | undefined} ranges
|
|
51
|
+
* @param {number} length
|
|
52
|
+
*/
|
|
53
|
+
export function cleanRanges(ranges, length) {
|
|
54
|
+
const out = [];
|
|
55
|
+
const sorted = (Array.isArray(ranges) ? ranges : [])
|
|
56
|
+
.filter((r) => r && Number.isFinite(r.start) && Number.isFinite(r.end))
|
|
57
|
+
.map((r) => ({ ...r, start: Math.max(0, Math.trunc(r.start)), end: Math.min(length, Math.trunc(r.end)) }))
|
|
58
|
+
.filter((r) => r.start < r.end)
|
|
59
|
+
.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
60
|
+
let at = 0;
|
|
61
|
+
for (const r of sorted) {
|
|
62
|
+
if (r.start < at) continue;
|
|
63
|
+
out.push(r);
|
|
64
|
+
at = r.end;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class TextLayer {
|
|
70
|
+
/** @param {HTMLInputElement | HTMLTextAreaElement} control */
|
|
71
|
+
constructor(control) {
|
|
72
|
+
this.control = control;
|
|
73
|
+
this.multi = control.localName === "textarea";
|
|
74
|
+
this.el = document.createElement("div");
|
|
75
|
+
this.el.className = "text-layer" + (this.multi ? " multi" : "");
|
|
76
|
+
this.el.setAttribute("part", "text-layer");
|
|
77
|
+
this.el.setAttribute("aria-hidden", "true");
|
|
78
|
+
this.text = document.createElement("div");
|
|
79
|
+
this.el.append(this.text);
|
|
80
|
+
control.before(this.el);
|
|
81
|
+
this.ranges = [];
|
|
82
|
+
this._shown = null;
|
|
83
|
+
control.addEventListener("scroll", () => this.scroll(), { passive: true });
|
|
84
|
+
// A one-line input scrolls with the caret without a scroll event: follow
|
|
85
|
+
// it while focused.
|
|
86
|
+
let following = 0;
|
|
87
|
+
const follow = () => {
|
|
88
|
+
this.scroll();
|
|
89
|
+
following = requestAnimationFrame(follow);
|
|
90
|
+
};
|
|
91
|
+
control.addEventListener("focus", () => {
|
|
92
|
+
if (!this.multi && !following) following = requestAnimationFrame(follow);
|
|
93
|
+
});
|
|
94
|
+
control.addEventListener("blur", () => {
|
|
95
|
+
cancelAnimationFrame(following);
|
|
96
|
+
following = 0;
|
|
97
|
+
this.scroll();
|
|
98
|
+
});
|
|
99
|
+
// Placed on the next frame: moving the layer inside the observer's
|
|
100
|
+
// callback would loop it.
|
|
101
|
+
if (typeof ResizeObserver === "function") new ResizeObserver(() => requestAnimationFrame(() => this.place())).observe(control);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Lays out the control's text with `ranges` marked. */
|
|
105
|
+
paint(ranges) {
|
|
106
|
+
const value = this.control.value;
|
|
107
|
+
this.ranges = cleanRanges(ranges, value.length);
|
|
108
|
+
const key = value + "\u0000" + JSON.stringify(this.ranges.map((r) => [r.start, r.end, r.kind ?? ""]));
|
|
109
|
+
if (this._shown !== key) {
|
|
110
|
+
this._shown = key;
|
|
111
|
+
const nodes = [];
|
|
112
|
+
let at = 0;
|
|
113
|
+
for (const r of this.ranges) {
|
|
114
|
+
if (r.start > at) nodes.push(value.slice(at, r.start));
|
|
115
|
+
const mark = document.createElement("mark");
|
|
116
|
+
mark.textContent = value.slice(r.start, r.end);
|
|
117
|
+
const kind = String(r.kind ?? "").replace(/[^a-z0-9-]/gi, "");
|
|
118
|
+
mark.setAttribute("part", kind ? `highlight highlight-${kind}` : "highlight");
|
|
119
|
+
if (kind) mark.dataset.kind = kind;
|
|
120
|
+
nodes.push(mark);
|
|
121
|
+
at = r.end;
|
|
122
|
+
}
|
|
123
|
+
if (at < value.length) nodes.push(value.slice(at));
|
|
124
|
+
// A trailing newline takes a line in a textarea but not in a <div>.
|
|
125
|
+
if (this.multi && value.endsWith("\n")) nodes.push(" ");
|
|
126
|
+
this.text.replaceChildren(...nodes);
|
|
127
|
+
}
|
|
128
|
+
this.place();
|
|
129
|
+
this.scroll();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
place() {
|
|
133
|
+
const c = this.control;
|
|
134
|
+
const cs = getComputedStyle(c);
|
|
135
|
+
for (const k of LAYOUT) this.el.style[k] = cs[k];
|
|
136
|
+
// offsetLeft is measured from the left, whatever the direction.
|
|
137
|
+
this.el.style.left = `${c.offsetLeft}px`;
|
|
138
|
+
this.el.style.top = `${c.offsetTop}px`;
|
|
139
|
+
// clientWidth leaves out a scrollbar: the text wraps at the same width.
|
|
140
|
+
this.el.style.width = `${this.multi ? c.clientWidth : c.offsetWidth}px`;
|
|
141
|
+
this.el.style.height = `${this.multi ? c.clientHeight : c.offsetHeight}px`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
scroll() {
|
|
145
|
+
const c = this.control;
|
|
146
|
+
this.text.style.transform = this.multi ? `translateY(${-c.scrollTop}px)` : `translateX(${-c.scrollLeft}px)`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The viewport rectangle of the character at `offset` (the end of the text: its last one's end). */
|
|
150
|
+
rectAt(offset) {
|
|
151
|
+
const walker = document.createTreeWalker(this.text, NodeFilter.SHOW_TEXT);
|
|
152
|
+
let left = Math.max(0, offset);
|
|
153
|
+
let last = null;
|
|
154
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
155
|
+
last = n;
|
|
156
|
+
if (left < n.length) {
|
|
157
|
+
const range = document.createRange();
|
|
158
|
+
range.setStart(n, left);
|
|
159
|
+
range.setEnd(n, left + 1);
|
|
160
|
+
const r = range.getBoundingClientRect();
|
|
161
|
+
if (r.width || r.height) return r;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
left -= n.length;
|
|
165
|
+
}
|
|
166
|
+
if (last && last.length) {
|
|
167
|
+
const range = document.createRange();
|
|
168
|
+
range.setStart(last, last.length - 1);
|
|
169
|
+
range.setEnd(last, last.length);
|
|
170
|
+
const r = range.getBoundingClientRect();
|
|
171
|
+
if (r.width || r.height) return new DOMRect(r.right, r.top, 0, r.height);
|
|
172
|
+
}
|
|
173
|
+
const c = this.control.getBoundingClientRect();
|
|
174
|
+
return new DOMRect(c.left, c.top, 0, c.height);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The range under a point of the viewport, or null. */
|
|
178
|
+
rangeAt(x, y) {
|
|
179
|
+
const marks = [...this.text.querySelectorAll("mark")];
|
|
180
|
+
for (let i = 0; i < marks.length; i++) {
|
|
181
|
+
for (const r of marks[i].getClientRects()) {
|
|
182
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return this.ranges[i];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Paints `host.highlight(text)` behind `host.control` (ray-input,
|
|
191
|
+
* ray-textarea): the layer is made the first time a highlight function is
|
|
192
|
+
* set. A range's `title` becomes the field's tooltip under the pointer.
|
|
193
|
+
*/
|
|
194
|
+
export function syncHighlight(host) {
|
|
195
|
+
const fn = typeof host.highlight === "function" ? host.highlight : null;
|
|
196
|
+
if (!fn && !host._textLayer) return;
|
|
197
|
+
host._textLayer ??= new TextLayer(host.control);
|
|
198
|
+
if (!host._highlightTitles) {
|
|
199
|
+
host._highlightTitles = true;
|
|
200
|
+
host.control.addEventListener("pointermove", (e) => {
|
|
201
|
+
const title = host._textLayer.rangeAt(e.clientX, e.clientY)?.title ?? "";
|
|
202
|
+
if (host.control.title !== title) host.control.title = title;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
let ranges = [];
|
|
206
|
+
if (fn) {
|
|
207
|
+
try {
|
|
208
|
+
ranges = fn(host.control.value);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error("ray-elements: highlight() threw", err);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
host._textLayer.paint(ranges);
|
|
214
|
+
}
|
package/core.js
CHANGED
|
@@ -8,6 +8,7 @@ export { COLORS, VARIANT_TOKENS, applyColor, spaceValue, sizeStyles, radiusStyle
|
|
|
8
8
|
export { place, pointRect, autoUpdate } from "./core/position.js";
|
|
9
9
|
export { scrollerOf, watchScroll } from "./core/scroll.js";
|
|
10
10
|
export { tabbables, focusFirst } from "./core/focus.js";
|
|
11
|
+
export { isRtl, inlineKey } from "./core/direction.js";
|
|
11
12
|
export { duration, motionAllowed, play, playAll, collapseOut, POP_OUT } from "./core/motion.js";
|
|
12
13
|
export { dismissable } from "./core/dismiss.js";
|
|
13
14
|
export { matchesHotkey, formatHotkey } from "./core/hotkey.js";
|