@raylang/elements 0.11.0 → 0.13.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/check/cli.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // node node_modules/@raylang/elements/check/cli.js check index.html (no npm: a raylang app)
7
7
 
8
8
  import { readFileSync, readdirSync, statSync } from "node:fs";
9
- import { join, relative } from "node:path";
9
+ import { dirname, join, relative, resolve } from "node:path";
10
10
  import { checkMarkup, customValues, RULES } from "./check.js";
11
11
  import { iconNames, iconAliases } from "../core/icons.js";
12
12
 
@@ -23,7 +23,8 @@ JSX and TSX files against @raylang/elements ${version()}. A directory is walked
23
23
 
24
24
  Options:
25
25
  --format json the diagnostics as JSON
26
- --icons a,b icon names your app registers (setIcons calls are found by themselves)
26
+ --icons a,b icon names your app registers (setIcons({ … }) and setIcons(X), X an
27
+ object of the file or imported from a local module, are found by themselves)
27
28
  --allow attr=v,… more values of an attribute: your own variant=brand (selectors such as
28
29
  [variant="brand"] in your CSS and extendStyles are found by themselves)
29
30
  --manifest FILE another custom-elements.json (your own components)
@@ -92,7 +93,7 @@ function main(argv) {
92
93
  const filledIcons = new Set(iconNames({ filled: true }));
93
94
  for (const f of scripts) {
94
95
  const src = readFileSync(f, "utf8");
95
- for (const name of registeredIcons(src)) {
96
+ for (const name of registeredIcons(src, f)) {
96
97
  if (name.endsWith(":filled")) filledIcons.add(name.slice(0, -":filled".length));
97
98
  else icons.add(name);
98
99
  }
@@ -132,15 +133,59 @@ function collect(path, files, scripts, explicit) {
132
133
  if (SCRIPTS.has(ext)) scripts.push(path);
133
134
  }
134
135
 
135
- // The keys of setIcons({ inbox: '<svg…>', "file-text": …, "star:filled": …, "ui.remove": … }) calls.
136
- function registeredIcons(src) {
136
+ // The keys of setIcons({ inbox: '<svg…>', "file-text": …, "star:filled": …, "ui.remove": … }) calls,
137
+ // and of setIcons(ICONS) where ICONS is an object literal of the same file
138
+ // (const ICONS = { … }) or one it imports from a local module
139
+ // (import { ICONS } from "./icons.js" → export const ICONS = { … }, or a
140
+ // default import → export default { … }).
141
+ function registeredIcons(src, file) {
137
142
  const out = [];
138
- for (const call of src.matchAll(/setIcons\(\s*\{([\s\S]*?)\}\s*\)/g)) {
139
- for (const key of call[1].matchAll(/(?:^|[,{\s])(?:(["'])([A-Za-z][\w.-]*(?::filled)?)\1|([A-Za-z][\w-]*))\s*:/g)) out.push(key[2] ?? key[3]);
143
+ for (const call of src.matchAll(/setIcons\(\s*\{([\s\S]*?)\}\s*\)/g)) out.push(...objectKeys(call[1]));
144
+ for (const call of src.matchAll(/setIcons\(\s*([A-Za-z_$][\w$]*)\s*\)/g)) {
145
+ const body = objectNamed(src, call[1], file, new Set());
146
+ if (body !== null) out.push(...objectKeys(body));
140
147
  }
141
148
  return out;
142
149
  }
143
150
 
151
+ function objectKeys(body) {
152
+ const out = [];
153
+ for (const key of body.matchAll(/(?:^|[,{\s])(?:(["'])([A-Za-z][\w.-]*(?::filled)?)\1|([A-Za-z][\w-]*))\s*:/g)) out.push(key[2] ?? key[3]);
154
+ return out;
155
+ }
156
+
157
+ const id = (name) => name.replace(/[$]/g, "\\$&");
158
+
159
+ // The body of the object literal bound to `name` in `src` (a const/let/var,
160
+ // or an export of a local module it imports it from), or null.
161
+ function objectNamed(src, name, file, seen) {
162
+ const local = src.match(new RegExp(`(?:^|[\\s;])(?:export\\s+)?(?:const|let|var)\\s+${id(name)}\\s*=\\s*\\{([\\s\\S]*?)\\}\\s*(?:;|\\n|$)`));
163
+ if (local) return local[1];
164
+ const named = src.match(new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*(["'])(\\.[^"']*)\\2`, "g")) ?? [];
165
+ for (const m of named) {
166
+ const [, names, , from] = m.match(/import\s*\{([^}]*)\}\s*from\s*(["'])(\.[^"']*)\2/);
167
+ const spec = names.split(",").map((s) => s.trim().split(/\s+as\s+/)).find((s) => (s[1] ?? s[0]) === name);
168
+ if (spec) return exportedObject(file, from, spec[0], seen);
169
+ }
170
+ const def = src.match(new RegExp(`import\\s+${id(name)}\\s+from\\s*(["'])(\\.[^"']*)\\1`));
171
+ if (def) return exportedObject(file, def[2], "default", seen);
172
+ return null;
173
+ }
174
+
175
+ function exportedObject(file, from, name, seen) {
176
+ let target;
177
+ try {
178
+ target = resolve(dirname(file), from);
179
+ if (seen.has(target)) return null;
180
+ seen.add(target);
181
+ const src = readFileSync(target, "utf8");
182
+ if (name === "default") return src.match(/export\s+default\s*\{([\s\S]*?)\}\s*(?:;|\n|$)/)?.[1] ?? null;
183
+ return objectNamed(src, name, target, seen);
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+
144
189
  try {
145
190
  process.exitCode = main(process.argv.slice(2));
146
191
  } catch (e) {
@@ -32,6 +32,13 @@ function parse(iso) {
32
32
  }
33
33
 
34
34
  /** A UTC Date -> "2026-10-06". */
35
+ /** A dayState() result as a part token, or "" (letters, digits, - and _). */
36
+ function stateToken(v) {
37
+ if (v === null || v === undefined || v === false || v === "") return "";
38
+ const s = String(v).trim();
39
+ return /^[A-Za-z][\w-]*$/.test(s) ? s : "";
40
+ }
41
+
35
42
  function iso(d) {
36
43
  return d.toISOString().slice(0, 10);
37
44
  }
@@ -141,6 +148,7 @@ export class RayCalendar extends RayElement {
141
148
  locale: { type: String },
142
149
  firstDay: { type: Number },
143
150
  size: { type: String, reflect: true, default: "sm" },
151
+ dayState: { attribute: false },
144
152
  };
145
153
  static styles = [sizeStyles, calendarStyles];
146
154
  static template = `
@@ -154,6 +162,11 @@ export class RayCalendar extends RayElement {
154
162
  /** The day the keyboard focus is on (roving tabindex). */
155
163
  #focus = null;
156
164
 
165
+ /** Renders the month again (what dayState marks changed). */
166
+ refresh() {
167
+ this._render();
168
+ }
169
+
157
170
  constructor() {
158
171
  super();
159
172
  this.$(".prev").addEventListener("click", () => this._moveFocus(addMonths(this._focusDate(), -1), false));
@@ -228,6 +241,7 @@ export class RayCalendar extends RayElement {
228
241
  const selected = this.value && parse(this.value) ? this.value : "";
229
242
  const now = iso(today());
230
243
  const focusIso = iso(focus);
244
+ const state = typeof this.dayState === "function" ? this.dayState : null;
231
245
  const body = this.$("tbody");
232
246
  body.replaceChildren();
233
247
  for (let w = 0; w < 6; w++) {
@@ -251,6 +265,13 @@ export class RayCalendar extends RayElement {
251
265
  td.setAttribute("aria-selected", String(sel));
252
266
  b.toggleAttribute("data-selected", sel);
253
267
  if (this._disabled(day)) b.setAttribute("aria-disabled", "true");
268
+ // An app's mark on the day (adherence, events…): data-state and a
269
+ // part token, day-<state>, for ::part(day-<state>) from outside.
270
+ const s = state ? stateToken(state(id)) : "";
271
+ if (s) {
272
+ b.dataset.state = s;
273
+ b.setAttribute("part", `day day-${s}`);
274
+ }
254
275
  td.append(b);
255
276
  tr.append(td);
256
277
  day = addDays(day, 1);
@@ -379,6 +400,7 @@ export class RayDatePicker extends FieldControl {
379
400
  firstDay: { type: Number },
380
401
  clearable: { type: Boolean, reflect: true },
381
402
  open: { type: Boolean, reflect: true },
403
+ native: { type: String, reflect: true, default: "auto" },
382
404
  };
383
405
  static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, pickerStyles];
384
406
  static template = fieldTemplate(
@@ -425,6 +447,17 @@ export class RayDatePicker extends FieldControl {
425
447
  this.open = false;
426
448
  this.trigger.focus();
427
449
  });
450
+ // A date picked in the native control (the system's picker on a phone,
451
+ // reached through the trigger with native, or by the keyboard's form
452
+ // navigation): the model follows it, like a calendar pick.
453
+ this.control.addEventListener("input", (e) => {
454
+ e.stopPropagation();
455
+ this._picked("input");
456
+ });
457
+ this.control.addEventListener("change", (e) => {
458
+ e.stopPropagation();
459
+ this._picked("change");
460
+ });
428
461
  this.panel.addEventListener("toggle", (e) => this._onToggle(e.newState === "open"));
429
462
  this._default = this.getAttribute("value") ?? "";
430
463
  }
@@ -452,12 +485,44 @@ export class RayDatePicker extends FieldControl {
452
485
  cal.locale = this.locale;
453
486
  cal.firstDay = this.firstDay;
454
487
  cal.size = this.size === "xs" ? "xs" : this.size === "lg" || this.size === "xl" ? "md" : "sm";
488
+ // Out of the focus order unless it is the picker: on a phone, a form's
489
+ // previous/next keys would otherwise land on it beside the calendar.
490
+ c.inert = !this._useNative();
455
491
  this.syncField();
456
492
  this._commit();
457
493
  this._paint();
458
494
  if (changed.has("open")) this._setOpen(Boolean(this.open));
459
495
  }
460
496
 
497
+ /** Whether the trigger opens the system's picker instead of the calendar. */
498
+ _useNative() {
499
+ if (this.native === "always") return true;
500
+ if (this.native === "never") return false;
501
+ return typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
502
+ }
503
+
504
+ /** Opens the native control's picker (the trigger, with native). */
505
+ _openNative() {
506
+ const c = this.control;
507
+ try {
508
+ c.showPicker();
509
+ } catch {
510
+ // No user activation or no showPicker(): focusing it opens the picker on a phone.
511
+ c.focus();
512
+ c.click();
513
+ }
514
+ }
515
+
516
+ // The native control changed on its own: sync the model and report it.
517
+ _picked(type) {
518
+ const v = this.control.value;
519
+ if (this.value !== v) this.commitProperty("value", v);
520
+ this._commit();
521
+ this._paint();
522
+ if (this.open) this.open = false;
523
+ this.dispatchEvent(new Event(type, { bubbles: true, composed: type === "input" }));
524
+ }
525
+
461
526
  /** Label, description, error and ARIA go to the button the user operates. */
462
527
  syncField() {
463
528
  super.syncField();
@@ -512,9 +577,13 @@ export class RayDatePicker extends FieldControl {
512
577
  this.dispatchEvent(new Event("change", { bubbles: true }));
513
578
  }
514
579
 
515
- /** Opens or closes the calendar. */
580
+ /** Opens or closes the calendar (with native, opens the system's picker). */
516
581
  toggle(force) {
517
582
  if (this.trigger.disabled) return;
583
+ if (this._useNative() && force !== false) {
584
+ this._openNative();
585
+ return;
586
+ }
518
587
  this.open = force ?? !this.open;
519
588
  }
520
589
 
@@ -78,6 +78,13 @@ const styles = css`
78
78
  border-top-width: 1px;
79
79
  --_from: translateY(100%);
80
80
  }
81
+ /* size="content": as big as what it holds, up to the viewport; the body scrolls past that. */
82
+ :host([size="content"]) dialog { width: max-content; max-width: calc(100vw - 2 * var(--_inset)); }
83
+ :host([size="content"][position="top"]) dialog, :host([size="content"][position="bottom"]) dialog {
84
+ width: calc(100vw - 2 * var(--_inset));
85
+ height: auto;
86
+ max-height: calc(100dvh - 2 * var(--_inset));
87
+ }
81
88
  :host([floating]) dialog { border-width: 1px; }
82
89
  /* A modeless dialog is not in the top layer: stack it above sticky headers
83
90
  and dropdowns (an ancestor with transform/filter would still clip it). */
@@ -147,9 +154,10 @@ export class RayDrawer extends RayDialog {
147
154
  }
148
155
 
149
156
  update(changed) {
150
- // xs…xl, full, or any CSS length.
151
- const size = SIZES[this.size] ?? this.size;
157
+ // xs…xl, full, content (CSS above), or any CSS length.
158
+ const size = this.size === "content" ? "" : SIZES[this.size] ?? this.size;
152
159
  if (size) this.style.setProperty("--_size", size);
160
+ else this.style.removeProperty("--_size");
153
161
  super.update(changed);
154
162
  }
155
163
  }
@@ -10,6 +10,10 @@ import { syncComplete, completeStyles } from "../../core/complete.js";
10
10
  const styles = css`
11
11
  .control[type="search"]::-webkit-search-cancel-button { cursor: pointer; }
12
12
  .control[type="number"] { font-variant-numeric: tabular-nums; }
13
+ /* Pickers: the field's own height and font, not the browser's. */
14
+ .control[type="time"], .control[type="date"], .control[type="datetime-local"] { font-variant-numeric: tabular-nums; }
15
+ .control::-webkit-date-and-time-value { text-align: start; min-height: 1lh; }
16
+ .control::-webkit-calendar-picker-indicator { cursor: pointer; opacity: 0.6; }
13
17
  `;
14
18
 
15
19
  // Attributes copied as-is to the inner <input>.
@@ -73,6 +77,11 @@ export class RayInput extends FieldControl {
73
77
  syncComplete(this);
74
78
  }
75
79
 
80
+ /** The highlight range under a point of the viewport, or null. */
81
+ highlightAt(x, y) {
82
+ return this._textLayer?.rangeAt(x, y) ?? null;
83
+ }
84
+
76
85
  /** Runs `highlight` again (what it marks changed, not the text). */
77
86
  refreshHighlight() {
78
87
  syncHighlight(this);
@@ -142,6 +142,13 @@ export class RayMenu extends Floating {
142
142
  return this.context && this._point ? this._point : super.anchorRect();
143
143
  }
144
144
 
145
+ // A context menu's trigger is the area it serves: a press there, outside
146
+ // the panel, closes it like a system context menu (a right click reopens
147
+ // it at the new point).
148
+ triggerDismisses() {
149
+ return Boolean(this.context);
150
+ }
151
+
145
152
  toggled(open) {
146
153
  if (open) {
147
154
  this._focus(this._focusOnOpen ?? "first");
@@ -226,6 +226,8 @@ const toasterStyles = css`
226
226
  /* Phones: full width at the bottom (or top), wherever the desktop position is. */
227
227
  @media (max-width: 40em) {
228
228
  :host { width: auto; inset-inline: 0; transform: none; padding: var(--ray-space-3); }
229
+ /* Same specificity as the -center rule above, which would keep left: 50%. */
230
+ :host([position$="center"]) { left: 0; right: 0; transform: none; } /* physical */
229
231
  :host([position^="top"]) { top: 0; bottom: auto; }
230
232
  :host(:not([position^="top"])) { bottom: 0; top: auto; }
231
233
  ::slotted(*) { inset-inline: var(--ray-space-3); }
@@ -77,6 +77,11 @@ export class RayTextarea extends FieldControl {
77
77
  syncComplete(this);
78
78
  }
79
79
 
80
+ /** The highlight range under a point of the viewport, or null. */
81
+ highlightAt(x, y) {
82
+ return this._textLayer?.rangeAt(x, y) ?? null;
83
+ }
84
+
80
85
  /** Runs `highlight` again (what it marks changed, not the text). */
81
86
  refreshHighlight() {
82
87
  syncHighlight(this);
package/core/floating.js CHANGED
@@ -35,6 +35,7 @@ export class Floating extends RayElement {
35
35
  open: { type: Boolean, reflect: true },
36
36
  placement: { type: String, reflect: true, default: "bottom" },
37
37
  offset: { type: Number, default: 8 },
38
+ anchor: { attribute: false },
38
39
  };
39
40
  /** popover attribute of the panel ("manual": closing is handled here). */
40
41
  static popoverMode = "manual";
@@ -102,8 +103,15 @@ export class Floating extends RayElement {
102
103
  /** Subclasses attach their trigger listeners here. */
103
104
  wireTrigger(_trigger) {}
104
105
 
105
- /** The rectangle the panel is placed against (default: the trigger). */
106
+ /**
107
+ * The rectangle the panel is placed against: `anchor` when set (an element,
108
+ * a DOMRect or a function returning one: a text range, the pointer), else
109
+ * the trigger.
110
+ */
106
111
  anchorRect() {
112
+ const a = typeof this.anchor === "function" ? this.anchor() : this.anchor;
113
+ if (a instanceof Element) return a.getBoundingClientRect();
114
+ if (a && typeof a.left === "number") return a;
107
115
  return (this.trigger ?? this).getBoundingClientRect();
108
116
  }
109
117
 
@@ -132,7 +140,20 @@ export class Floating extends RayElement {
132
140
  _dismissable() {
133
141
  const ctor = /** @type {typeof Floating} */ (this.constructor);
134
142
  if (this.#undismiss || !ctor.lightDismiss || ctor.popoverMode !== "manual") return;
135
- this.#undismiss = dismissable({ panel: this.panel, owner: this, trigger: this.trigger, close: () => { this.open = false; } });
143
+ // When the trigger dismisses, so does the rest of the host's light DOM
144
+ // (the area is a child of the host): only the panel is inside then.
145
+ const area = this.triggerDismisses();
146
+ this.#undismiss = dismissable({ panel: this.panel, owner: area ? this.panel : this, trigger: area ? null : this.trigger, close: () => { this.open = false; } });
147
+ }
148
+
149
+ /**
150
+ * Whether a press on the trigger counts as outside and closes the panel.
151
+ * Default false: a button that toggles the panel handles its own click
152
+ * (closing it there too would reopen it). A context menu's trigger is the
153
+ * whole area it serves, so there it is true.
154
+ */
155
+ triggerDismisses() {
156
+ return false;
136
157
  }
137
158
 
138
159
  /** The keyframes the panel leaves with (subclasses may change them). */
@@ -166,6 +187,15 @@ export class Floating extends RayElement {
166
187
 
167
188
  _onToggle(e) {
168
189
  const opening = e.newState === "open";
190
+ // `toggle` is a task: `open` may have changed again since the state it
191
+ // reports. An open that a later `open = false` is already animating out,
192
+ // or a close that a later `open = true` re-showed, must not win over
193
+ // that newer value; only the pair of events is still emitted.
194
+ const stale = opening ? !this.open && Boolean(this.#exit) : Boolean(this.open) && this.panel.matches(":popover-open");
195
+ if (stale) {
196
+ this.emit(opening ? "ray-open" : "ray-close");
197
+ return;
198
+ }
169
199
  if (this.popupKind()) this.#trigger?.setAttribute("aria-expanded", String(opening));
170
200
  if (opening) {
171
201
  this.reposition();
package/core/textlayer.js CHANGED
@@ -176,10 +176,18 @@ export class TextLayer {
176
176
 
177
177
  /** The range under a point of the viewport, or null. */
178
178
  rangeAt(x, y) {
179
+ return this.hitAt(x, y)?.range ?? null;
180
+ }
181
+
182
+ /**
183
+ * The range under a point of the viewport and the box of the line of it
184
+ * the point is on (a range may wrap), or null.
185
+ */
186
+ hitAt(x, y) {
179
187
  const marks = [...this.text.querySelectorAll("mark")];
180
188
  for (let i = 0; i < marks.length; i++) {
181
189
  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];
190
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return { range: this.ranges[i], rect: r };
183
191
  }
184
192
  }
185
193
  return null;
@@ -189,7 +197,9 @@ export class TextLayer {
189
197
  /**
190
198
  * Paints `host.highlight(text)` behind `host.control` (ray-input,
191
199
  * 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.
200
+ * set. A range's `title` becomes the field's tooltip under the pointer, and
201
+ * `ray-highlight-hover` fires when the range under it changes (null leaving
202
+ * one), with its box, so an app can show its own popover over a range.
193
203
  */
194
204
  export function syncHighlight(host) {
195
205
  const fn = typeof host.highlight === "function" ? host.highlight : null;
@@ -197,10 +207,18 @@ export function syncHighlight(host) {
197
207
  host._textLayer ??= new TextLayer(host.control);
198
208
  if (!host._highlightTitles) {
199
209
  host._highlightTitles = true;
200
- host.control.addEventListener("pointermove", (e) => {
201
- const title = host._textLayer.rangeAt(e.clientX, e.clientY)?.title ?? "";
210
+ let over = null;
211
+ const hover = (x, y) => {
212
+ const hit = host._textLayer.hitAt(x, y);
213
+ const title = hit?.range.title ?? "";
202
214
  if (host.control.title !== title) host.control.title = title;
203
- });
215
+ const range = hit?.range ?? null;
216
+ if (range === over) return;
217
+ over = range;
218
+ host.emit("ray-highlight-hover", { range, rect: hit?.rect ?? null });
219
+ };
220
+ host.control.addEventListener("pointermove", (e) => hover(e.clientX, e.clientY));
221
+ host.control.addEventListener("pointerleave", () => hover(-1, -1));
204
222
  }
205
223
  let ranges = [];
206
224
  if (fn) {
@@ -3196,7 +3196,7 @@
3196
3196
  "name": "RayDatePicker",
3197
3197
  "tagName": "ray-date-picker",
3198
3198
  "customElement": true,
3199
- "description": "Values are ISO dates (\"2026-10-06\"), submitted as such. ArrowDown on the field opens the calendar; Escape closes it.",
3199
+ "description": "Values are ISO dates (\"2026-10-06\"), submitted as such. ArrowDown on the field opens the calendar; Escape closes it. On a touch screen the trigger opens the system's picker instead (native).",
3200
3200
  "superclass": {
3201
3201
  "name": "FieldControl"
3202
3202
  },
@@ -3275,6 +3275,15 @@
3275
3275
  "description": "The calendar dropdown is shown.",
3276
3276
  "fieldName": "open"
3277
3277
  },
3278
+ {
3279
+ "name": "native",
3280
+ "type": {
3281
+ "text": "\"auto\" | \"always\" | \"never\""
3282
+ },
3283
+ "description": "Which picker the trigger opens: the system's (the native date input's, the usual one on a phone) or the calendar dropdown. auto: the system's on a touch screen (pointer: coarse), else the calendar. A date picked in the system's picker is the value like one picked in the calendar.",
3284
+ "fieldName": "native",
3285
+ "default": "auto"
3286
+ },
3278
3287
  {
3279
3288
  "name": "name",
3280
3289
  "type": {
@@ -3452,6 +3461,17 @@
3452
3461
  "attribute": "open",
3453
3462
  "reflects": true
3454
3463
  },
3464
+ {
3465
+ "kind": "field",
3466
+ "name": "native",
3467
+ "type": {
3468
+ "text": "\"auto\" | \"always\" | \"never\""
3469
+ },
3470
+ "description": "Which picker the trigger opens: the system's (the native date input's, the usual one on a phone) or the calendar dropdown. auto: the system's on a touch screen (pointer: coarse), else the calendar. A date picked in the system's picker is the value like one picked in the calendar.",
3471
+ "attribute": "native",
3472
+ "reflects": true,
3473
+ "default": "auto"
3474
+ },
3455
3475
  {
3456
3476
  "kind": "field",
3457
3477
  "name": "name",
@@ -3746,6 +3766,19 @@
3746
3766
  "reflects": true,
3747
3767
  "default": "sm"
3748
3768
  },
3769
+ {
3770
+ "kind": "field",
3771
+ "name": "dayState",
3772
+ "type": {
3773
+ "text": "((date: string) => string | null | undefined) | null"
3774
+ },
3775
+ "description": "Marks days: called with each day's ISO date on every render, it returns a state name (letters, digits, - and _) or nothing. The day button gets data-state and the part day-<state>, so ::part(day-taken) (or ::part(day-missed)::after for a dot) styles it from outside. Call refresh() when what it marks changed but the month did not."
3776
+ },
3777
+ {
3778
+ "kind": "method",
3779
+ "name": "refresh",
3780
+ "description": "Renders the month again: when what dayState marks changed but the text did not."
3781
+ },
3749
3782
  {
3750
3783
  "kind": "method",
3751
3784
  "name": "focusDay",
@@ -3786,7 +3819,7 @@
3786
3819
  },
3787
3820
  {
3788
3821
  "name": "day",
3789
- "description": "Each day button ([aria-selected], [aria-current=date], [aria-disabled], [data-outside])."
3822
+ "description": "Each day button (data-date=\"YYYY-MM-DD\", [data-selected], [aria-current=date], [aria-disabled], [data-outside], and with dayState data-state plus the part day-<state>)."
3790
3823
  }
3791
3824
  ]
3792
3825
  }
@@ -4377,9 +4410,9 @@
4377
4410
  {
4378
4411
  "name": "type",
4379
4412
  "type": {
4380
- "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\""
4413
+ "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\" | \"time\" | \"date\" | \"datetime-local\""
4381
4414
  },
4382
- "description": "Input type.",
4415
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
4383
4416
  "fieldName": "type",
4384
4417
  "default": "text"
4385
4418
  },
@@ -4584,9 +4617,9 @@
4584
4617
  "kind": "field",
4585
4618
  "name": "type",
4586
4619
  "type": {
4587
- "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\""
4620
+ "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\" | \"time\" | \"date\" | \"datetime-local\""
4588
4621
  },
4589
- "description": "Input type.",
4622
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
4590
4623
  "attribute": "type",
4591
4624
  "reflects": true,
4592
4625
  "default": "text"
@@ -4746,6 +4779,11 @@
4746
4779
  "name": "select",
4747
4780
  "description": "Selects the text."
4748
4781
  },
4782
+ {
4783
+ "kind": "method",
4784
+ "name": "highlightAt",
4785
+ "description": "The highlight range under a point of the viewport (clientX, clientY), or null."
4786
+ },
4749
4787
  {
4750
4788
  "kind": "method",
4751
4789
  "name": "refreshHighlight",
@@ -4760,6 +4798,13 @@
4760
4798
  },
4761
4799
  "description": "A suggestion was accepted (the text already has it)."
4762
4800
  },
4801
+ {
4802
+ "name": "ray-highlight-hover",
4803
+ "type": {
4804
+ "text": "CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>"
4805
+ },
4806
+ "description": "The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title."
4807
+ },
4763
4808
  {
4764
4809
  "name": "input",
4765
4810
  "type": {
@@ -4923,9 +4968,9 @@
4923
4968
  {
4924
4969
  "name": "type",
4925
4970
  "type": {
4926
- "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\""
4971
+ "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\" | \"time\" | \"date\" | \"datetime-local\""
4927
4972
  },
4928
- "description": "Input type.",
4973
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
4929
4974
  "fieldName": "type",
4930
4975
  "default": "text"
4931
4976
  },
@@ -5113,9 +5158,9 @@
5113
5158
  "kind": "field",
5114
5159
  "name": "type",
5115
5160
  "type": {
5116
- "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\""
5161
+ "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\" | \"time\" | \"date\" | \"datetime-local\""
5117
5162
  },
5118
- "description": "Input type.",
5163
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
5119
5164
  "attribute": "type",
5120
5165
  "reflects": true,
5121
5166
  "default": "text"
@@ -5301,6 +5346,13 @@
5301
5346
  },
5302
5347
  "description": "A suggestion was accepted (the text already has it)."
5303
5348
  },
5349
+ {
5350
+ "name": "ray-highlight-hover",
5351
+ "type": {
5352
+ "text": "CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>"
5353
+ },
5354
+ "description": "The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title."
5355
+ },
5304
5356
  {
5305
5357
  "name": "input",
5306
5358
  "type": {
@@ -5966,9 +6018,9 @@
5966
6018
  {
5967
6019
  "name": "type",
5968
6020
  "type": {
5969
- "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\""
6021
+ "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\" | \"time\" | \"date\" | \"datetime-local\""
5970
6022
  },
5971
- "description": "Input type.",
6023
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
5972
6024
  "fieldName": "type",
5973
6025
  "default": "text"
5974
6026
  },
@@ -6128,9 +6180,9 @@
6128
6180
  "kind": "field",
6129
6181
  "name": "type",
6130
6182
  "type": {
6131
- "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\""
6183
+ "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\" | \"time\" | \"date\" | \"datetime-local\""
6132
6184
  },
6133
- "description": "Input type.",
6185
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
6134
6186
  "attribute": "type",
6135
6187
  "reflects": true,
6136
6188
  "default": "text"
@@ -6301,6 +6353,13 @@
6301
6353
  },
6302
6354
  "description": "A suggestion was accepted (the text already has it)."
6303
6355
  },
6356
+ {
6357
+ "name": "ray-highlight-hover",
6358
+ "type": {
6359
+ "text": "CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>"
6360
+ },
6361
+ "description": "The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title."
6362
+ },
6304
6363
  {
6305
6364
  "name": "input",
6306
6365
  "type": {
@@ -6403,9 +6462,9 @@
6403
6462
  {
6404
6463
  "name": "type",
6405
6464
  "type": {
6406
- "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\""
6465
+ "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\" | \"time\" | \"date\" | \"datetime-local\""
6407
6466
  },
6408
- "description": "Input type.",
6467
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
6409
6468
  "fieldName": "type",
6410
6469
  "default": "text"
6411
6470
  },
@@ -6575,9 +6634,9 @@
6575
6634
  "kind": "field",
6576
6635
  "name": "type",
6577
6636
  "type": {
6578
- "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\""
6637
+ "text": "\"text\" | \"email\" | \"password\" | \"search\" | \"number\" | \"url\" | \"tel\" | \"time\" | \"date\" | \"datetime-local\""
6579
6638
  },
6580
- "description": "Input type.",
6639
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
6581
6640
  "attribute": "type",
6582
6641
  "reflects": true,
6583
6642
  "default": "text"
@@ -6765,6 +6824,13 @@
6765
6824
  },
6766
6825
  "description": "A suggestion was accepted (the text already has it)."
6767
6826
  },
6827
+ {
6828
+ "name": "ray-highlight-hover",
6829
+ "type": {
6830
+ "text": "CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>"
6831
+ },
6832
+ "description": "The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title."
6833
+ },
6768
6834
  {
6769
6835
  "name": "input",
6770
6836
  "type": {
@@ -9540,6 +9606,11 @@
9540
9606
  "attribute": "radius",
9541
9607
  "reflects": true
9542
9608
  },
9609
+ {
9610
+ "kind": "method",
9611
+ "name": "highlightAt",
9612
+ "description": "The highlight range under a point of the viewport (clientX, clientY), or null."
9613
+ },
9543
9614
  {
9544
9615
  "kind": "method",
9545
9616
  "name": "refreshHighlight",
@@ -9554,6 +9625,13 @@
9554
9625
  },
9555
9626
  "description": "A suggestion was accepted (the text already has it)."
9556
9627
  },
9628
+ {
9629
+ "name": "ray-highlight-hover",
9630
+ "type": {
9631
+ "text": "CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>"
9632
+ },
9633
+ "description": "The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title."
9634
+ },
9557
9635
  {
9558
9636
  "name": "input",
9559
9637
  "type": {
@@ -13145,9 +13223,9 @@
13145
13223
  {
13146
13224
  "name": "size",
13147
13225
  "type": {
13148
- "text": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\" | (string & {})"
13226
+ "text": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\" | \"content\" | (string & {})"
13149
13227
  },
13150
- "description": "Width (left/right) or height (top/bottom): xs…xl, full, or any CSS length.",
13228
+ "description": "Width (left/right) or height (top/bottom): xs…xl, full, content (as big as what it holds, up to the viewport: a bottom sheet) or any CSS length.",
13151
13229
  "fieldName": "size",
13152
13230
  "default": "md"
13153
13231
  },
@@ -13238,9 +13316,9 @@
13238
13316
  "kind": "field",
13239
13317
  "name": "size",
13240
13318
  "type": {
13241
- "text": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\" | (string & {})"
13319
+ "text": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\" | \"content\" | (string & {})"
13242
13320
  },
13243
- "description": "Width (left/right) or height (top/bottom): xs…xl, full, or any CSS length.",
13321
+ "description": "Width (left/right) or height (top/bottom): xs…xl, full, content (as big as what it holds, up to the viewport: a bottom sheet) or any CSS length.",
13244
13322
  "attribute": "size",
13245
13323
  "reflects": true,
13246
13324
  "default": "md"
@@ -13496,6 +13574,14 @@
13496
13574
  "attribute": "native",
13497
13575
  "reflects": true
13498
13576
  },
13577
+ {
13578
+ "kind": "field",
13579
+ "name": "anchor",
13580
+ "type": {
13581
+ "text": "Element | DOMRect | (() => DOMRect) | null"
13582
+ },
13583
+ "description": "What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it."
13584
+ },
13499
13585
  {
13500
13586
  "kind": "field",
13501
13587
  "name": "offset",
@@ -13571,7 +13657,7 @@
13571
13657
  "slots": [
13572
13658
  {
13573
13659
  "name": "trigger",
13574
- "description": "The button that opens it, or the area for a context menu."
13660
+ "description": "The button that opens it, or the area for a context menu (a press on the area outside the open menu closes it; a right click reopens it at the new point)."
13575
13661
  },
13576
13662
  {
13577
13663
  "name": "",
@@ -13940,6 +14026,14 @@
13940
14026
  "reflects": false,
13941
14027
  "default": "8"
13942
14028
  },
14029
+ {
14030
+ "kind": "field",
14031
+ "name": "anchor",
14032
+ "type": {
14033
+ "text": "Element | DOMRect | (() => DOMRect) | null"
14034
+ },
14035
+ "description": "What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it."
14036
+ },
13943
14037
  {
13944
14038
  "kind": "field",
13945
14039
  "name": "width",
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # ray-elements
2
2
 
3
- > Web components (custom elements, Shadow DOM, no dependencies) for any web app: plain HTML, React, Vue or a raylang app. npm: `@raylang/elements`, `@raylang/react`, `@raylang/vue`; raylang: the `elements` package. This file describes version 0.11.0.
3
+ > Web components (custom elements, Shadow DOM, no dependencies) for any web app: plain HTML, React, Vue or a raylang app. npm: `@raylang/elements`, `@raylang/react`, `@raylang/vue`; raylang: the `elements` package. This file describes version 0.13.0.
4
4
 
5
5
  ray-elements is a design system of standard web components (custom elements
6
6
  with Shadow DOM, no dependencies): `<ray-button>`, `<ray-input>`,
@@ -701,7 +701,7 @@ Import: `import "@raylang/elements/date-picker";` · Category: Inputs
701
701
 
702
702
  #### `<ray-date-picker>` (RayDatePicker, extends FieldControl)
703
703
 
704
- Values are ISO dates ("2026-10-06"), submitted as such. ArrowDown on the field opens the calendar; Escape closes it.
704
+ Values are ISO dates ("2026-10-06"), submitted as such. ArrowDown on the field opens the calendar; Escape closes it. On a touch screen the trigger opens the system's picker instead (native).
705
705
 
706
706
  Properties:
707
707
 
@@ -714,6 +714,7 @@ Properties:
714
714
  - `firstDay` (attribute `first-day`): number. First day of the week, 0 (Sunday) to 6. Default: the locale's.
715
715
  - `clearable`: boolean, reflects. Shows a button that clears the date.
716
716
  - `open`: boolean, reflects. The calendar dropdown is shown.
717
+ - `native`: `auto` | `always` | `never`, default `auto`, reflects. Which picker the trigger opens: the system's (the native date input's, the usual one on a phone) or the calendar dropdown. auto: the system's on a touch screen (pointer: coarse), else the calendar. A date picked in the system's picker is the value like one picked in the calendar.
717
718
  - `name`: string, reflects. Form field name.
718
719
  - `label`: string. Visible label (also the accessible name).
719
720
  - `description`: string. Help text.
@@ -760,6 +761,7 @@ Properties:
760
761
  - `locale`: string. Locale for month and weekday names. Default: the nearest lang attribute, else the browser's.
761
762
  - `firstDay` (attribute `first-day`): number. First day of the week, 0 (Sunday) to 6. Default: the locale's.
762
763
  - `size`: `xs` | `sm` | `md` | `lg` | `xl`, default `sm`, reflects. Day cell size and font size.
764
+ - `dayState` (property only): `((date: string) => string | null | undefined) | null`. Marks days: called with each day's ISO date on every render, it returns a state name (letters, digits, - and _) or nothing. The day button gets data-state and the part day-<state>, so ::part(day-taken) (or ::part(day-missed)::after for a dot) styles it from outside. Call refresh() when what it marks changed but the month did not.
763
765
 
764
766
  Events:
765
767
 
@@ -771,10 +773,11 @@ Parts (`::part()`):
771
773
  - `caption`: The month and year.
772
774
  - `nav`: The previous/next month buttons.
773
775
  - `grid`: The day grid (role=grid).
774
- - `day`: Each day button ([aria-selected], [aria-current=date], [aria-disabled], [data-outside]).
776
+ - `day`: Each day button (data-date="YYYY-MM-DD", [data-selected], [aria-current=date], [aria-disabled], [data-outside], and with dayState data-state plus the part day-<state>).
775
777
 
776
778
  Methods and properties:
777
779
 
780
+ - `refresh(): void`: Renders the month again: when what dayState marks changed but the text did not.
778
781
  - `focusDay(): void`: Focuses the day the keyboard is on (the selected day, else today).
779
782
  - `readonly focusedDate: string`: The day the keyboard is on (ISO).
780
783
 
@@ -884,7 +887,7 @@ Also forwards placeholder, autocomplete, inputmode, pattern, min, max, step, min
884
887
  Properties:
885
888
 
886
889
  - `value`: string. Current value. The value attribute is the initial value (restored on form reset).
887
- - `type`: `text` | `email` | `password` | `search` | `number` | `url` | `tel`, default `text`, reflects. Input type.
890
+ - `type`: `text` | `email` | `password` | `search` | `number` | `url` | `tel` | `time` | `date` | `datetime-local`, default `text`, reflects. Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: "HH:MM", "YYYY-MM-DD", "YYYY-MM-DDTHH:MM".
888
891
  - `name`: string, reflects. Form field name.
889
892
  - `label`: string. Visible label (also the accessible name).
890
893
  - `description`: string. Help text under the label.
@@ -918,6 +921,7 @@ Attributes (forwarded to the inner element):
918
921
  Events:
919
922
 
920
923
  - `ray-complete` (detail `{ item: CompletionItem; from: number; to: number }`): A suggestion was accepted (the text already has it).
924
+ - `ray-highlight-hover` (detail `{ range: HighlightRange | null; rect: DOMRect | null }`): The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title.
921
925
  - `input`: On every edit (value already updated).
922
926
  - `change`: When an edit is committed (blur, Enter).
923
927
 
@@ -944,6 +948,7 @@ Parts (`::part()`):
944
948
  Methods and properties:
945
949
 
946
950
  - `select(): void`: Selects the text.
951
+ - `highlightAt(x: number, y: number): HighlightRange | null`: The highlight range under a point of the viewport (clientX, clientY), or null.
947
952
  - `refreshHighlight(): void`: Runs highlight again, when what it marks changed but the text did not.
948
953
 
949
954
  Vue `v-model`: `value` (updated on input).
@@ -1480,6 +1485,7 @@ Attributes (forwarded to the inner element):
1480
1485
  Events:
1481
1486
 
1482
1487
  - `ray-complete` (detail `{ item: CompletionItem; from: number; to: number }`): A suggestion was accepted (the text already has it).
1488
+ - `ray-highlight-hover` (detail `{ range: HighlightRange | null; rect: DOMRect | null }`): The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title.
1483
1489
  - `input`: On every edit.
1484
1490
  - `change`: When an edit is committed.
1485
1491
 
@@ -1497,6 +1503,7 @@ Parts (`::part()`):
1497
1503
 
1498
1504
  Methods and properties:
1499
1505
 
1506
+ - `highlightAt(x: number, y: number): HighlightRange | null`: The highlight range under a point of the viewport (clientX, clientY), or null.
1500
1507
  - `refreshHighlight(): void`: Runs highlight again, when what it marks changed but the text did not.
1501
1508
 
1502
1509
  Vue `v-model`: `value` (updated on input).
@@ -2144,7 +2151,7 @@ Properties:
2144
2151
  - `label`: string. Heading (also the accessible name).
2145
2152
  - `description`: string. A line under the title (the drawer's accessible description).
2146
2153
  - `position`: `end` | `start` | `right` | `left` | `top` | `bottom`, default `end`, reflects. The edge it slides from: start/end follow the writing direction; left/right are sides of the screen.
2147
- - `size`: `"xs" | "sm" | "md" | "lg" | "xl" | "full" | (string & {})`, default `md`, reflects. Width (left/right) or height (top/bottom): xs…xl, full, or any CSS length.
2154
+ - `size`: `"xs" | "sm" | "md" | "lg" | "xl" | "full" | "content" | (string & {})`, default `md`, reflects. Width (left/right) or height (top/bottom): xs…xl, full, content (as big as what it holds, up to the viewport: a bottom sheet) or any CSS length.
2148
2155
  - `modeless`: boolean, reflects. No backdrop nor scroll lock: the page stays interactive (a side panel).
2149
2156
  - `floating`: boolean, reflects. Detached from the edge, with rounded corners.
2150
2157
  - `persistent`: boolean, reflects. Escape and backdrop clicks do not close it.
@@ -2191,6 +2198,7 @@ Properties:
2191
2198
  - `context`: boolean, reflects. Opens on right click (contextmenu) over the trigger area, at the pointer.
2192
2199
  - `keepOpen` (attribute `keep-open`): boolean, reflects. Choosing an item does not close the menu.
2193
2200
  - `native`: boolean, reflects. With a native host (ray-elements/core/raylang.js in a std/ui window), opens as the system's context menu at the pointer: items and dividers, same ray-select.
2201
+ - `anchor` (property only): `Element | DOMRect | (() => DOMRect) | null`. What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it.
2194
2202
 
2195
2203
  Also the properties of Floating: `offset`.
2196
2204
 
@@ -2203,7 +2211,7 @@ Events:
2203
2211
 
2204
2212
  Slots:
2205
2213
 
2206
- - `trigger`: The button that opens it, or the area for a context menu.
2214
+ - `trigger`: The button that opens it, or the area for a context menu (a press on the area outside the open menu closes it; a right click reopens it at the new point).
2207
2215
  - (default): ray-menu-item, ray-menu-label and ray-menu-divider elements.
2208
2216
 
2209
2217
  Parts (`::part()`):
@@ -2294,6 +2302,7 @@ Properties:
2294
2302
  - `open`: boolean, reflects. Shown.
2295
2303
  - `placement`: `bottom` | `bottom-start` | `bottom-end` | `top` | `top-start` | `top-end` | `left` | `left-start` | `left-end` | `right` | `right-start` | `right-end`, default `bottom`, reflects. Preferred side and alignment: top/bottom/left/right are sides of the screen; on top and bottom, -start/-end follow the writing direction (right to left, "bottom-start" lines up the right edges).
2296
2304
  - `offset`: number, default `8`. Gap to the trigger in px.
2305
+ - `anchor` (property only): `Element | DOMRect | (() => DOMRect) | null`. What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it.
2297
2306
  - `width`: string. Panel width (px number or CSS length). Default: its content.
2298
2307
  - `matchWidth` (attribute `match-width`): boolean. At least as wide as the trigger.
2299
2308
  - `padding`: `0` | `xs` | `sm` | `md` | `lg`, default `md`, reflects. Panel padding.
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # ray-elements
2
2
 
3
- > Web components (custom elements, Shadow DOM, no dependencies) for any web app: plain HTML, React, Vue or a raylang app. npm: `@raylang/elements`, `@raylang/react`, `@raylang/vue`; raylang: the `elements` package. This file describes version 0.11.0.
3
+ > Web components (custom elements, Shadow DOM, no dependencies) for any web app: plain HTML, React, Vue or a raylang app. npm: `@raylang/elements`, `@raylang/react`, `@raylang/vue`; raylang: the `elements` package. This file describes version 0.13.0.
4
4
 
5
5
  ray-elements is a design system of standard web components (custom elements
6
6
  with Shadow DOM, no dependencies): `<ray-button>`, `<ray-input>`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raylang/elements",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Web components for any app, with raylang extras (shadcn/ui + Mantine inspired)",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": ["web-components", "custom-elements", "design-system", "ui", "raylang"],
package/ray-elements.d.ts CHANGED
@@ -656,7 +656,7 @@ export interface RayComboboxEventMap extends HTMLElementEventMap {
656
656
  }
657
657
 
658
658
  // --- Date picker (date-picker)
659
- /** <ray-date-picker> Values are ISO dates ("2026-10-06"), submitted as such. ArrowDown on the field opens the calendar; Escape closes it. */
659
+ /** <ray-date-picker> Values are ISO dates ("2026-10-06"), submitted as such. ArrowDown on the field opens the calendar; Escape closes it. On a touch screen the trigger opens the system's picker instead (native). */
660
660
  export declare class RayDatePicker extends FieldControl {
661
661
  /** The chosen date, ISO "YYYY-MM-DD" ("" for none). The value attribute is the initial value (restored on form reset). */
662
662
  value: string | undefined;
@@ -676,6 +676,8 @@ export declare class RayDatePicker extends FieldControl {
676
676
  clearable: boolean | undefined;
677
677
  /** The calendar dropdown is shown. */
678
678
  open: boolean | undefined;
679
+ /** Which picker the trigger opens: the system's (the native date input's, the usual one on a phone) or the calendar dropdown. auto: the system's on a touch screen (pointer: coarse), else the calendar. A date picked in the system's picker is the value like one picked in the calendar. */
680
+ native: "auto" | "always" | "never";
679
681
  /** Form field name. */
680
682
  name: string | undefined;
681
683
  /** Visible label (also the accessible name). */
@@ -725,6 +727,10 @@ export declare class RayCalendar extends RayElement {
725
727
  firstDay: number | undefined;
726
728
  /** Day cell size and font size. */
727
729
  size: "xs" | "sm" | "md" | "lg" | "xl";
730
+ /** Marks days: called with each day's ISO date on every render, it returns a state name (letters, digits, - and _) or nothing. The day button gets data-state and the part day-<state>, so ::part(day-taken) (or ::part(day-missed)::after for a dot) styles it from outside. Call refresh() when what it marks changed but the month did not. */
731
+ dayState: ((date: string) => string | null | undefined) | null;
732
+ /** Renders the month again: when what dayState marks changed but the text did not. */
733
+ refresh(): void;
728
734
  /** Focuses the day the keyboard is on (the selected day, else today). */
729
735
  focusDay(): void;
730
736
  /** The day the keyboard is on (ISO). */
@@ -821,8 +827,8 @@ export type CompletionResult = { from: number; to: number; items: CompletionItem
821
827
  export declare class RayInput extends FieldControl {
822
828
  /** Current value. The value attribute is the initial value (restored on form reset). */
823
829
  value: string | undefined;
824
- /** Input type. */
825
- type: "text" | "email" | "password" | "search" | "number" | "url" | "tel";
830
+ /** Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: "HH:MM", "YYYY-MM-DD", "YYYY-MM-DDTHH:MM". */
831
+ type: "text" | "email" | "password" | "search" | "number" | "url" | "tel" | "time" | "date" | "datetime-local";
826
832
  /** Form field name. */
827
833
  name: string | undefined;
828
834
  /** Visible label (also the accessible name). */
@@ -855,6 +861,8 @@ export declare class RayInput extends FieldControl {
855
861
  suggestionsLabel: string;
856
862
  /** Selects the text. */
857
863
  select(): void;
864
+ /** The highlight range under a point of the viewport (clientX, clientY), or null. */
865
+ highlightAt(x: number, y: number): HighlightRange | null;
858
866
  /** Runs highlight again, when what it marks changed but the text did not. */
859
867
  refreshHighlight(): void;
860
868
  addEventListener<K extends keyof RayInputEventMap>(type: K, listener: (this: RayInput, ev: RayInputEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
@@ -865,6 +873,8 @@ export declare class RayInput extends FieldControl {
865
873
  export interface RayInputEventMap extends HTMLElementEventMap {
866
874
  /** A suggestion was accepted (the text already has it). */
867
875
  "ray-complete": CustomEvent<{ item: CompletionItem; from: number; to: number }>;
876
+ /** The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title. */
877
+ "ray-highlight-hover": CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>;
868
878
  }
869
879
 
870
880
  // --- Mode input (mode-input)
@@ -900,6 +910,8 @@ export interface RayModeInputEventMap extends HTMLElementEventMap {
900
910
  "ray-mode-change": CustomEvent<{ mode: string; previous: string | null; option: ModeOption }>;
901
911
  /** A suggestion was accepted (the text already has it). */
902
912
  "ray-complete": CustomEvent<{ item: CompletionItem; from: number; to: number }>;
913
+ /** The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title. */
914
+ "ray-highlight-hover": CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>;
903
915
  }
904
916
 
905
917
  // --- Number input (number-input)
@@ -960,6 +972,8 @@ export interface RayPasswordInputEventMap extends HTMLElementEventMap {
960
972
  "ray-visibility-change": CustomEvent<{ visible: boolean }>;
961
973
  /** A suggestion was accepted (the text already has it). */
962
974
  "ray-complete": CustomEvent<{ item: CompletionItem; from: number; to: number }>;
975
+ /** The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title. */
976
+ "ray-highlight-hover": CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>;
963
977
  }
964
978
 
965
979
  // --- Path input (path-input)
@@ -991,6 +1005,8 @@ export interface RayPathInputEventMap extends HTMLElementEventMap {
991
1005
  "ray-error": CustomEvent<{ error: Error }>;
992
1006
  /** A suggestion was accepted (the text already has it). */
993
1007
  "ray-complete": CustomEvent<{ item: CompletionItem; from: number; to: number }>;
1008
+ /** The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title. */
1009
+ "ray-highlight-hover": CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>;
994
1010
  }
995
1011
 
996
1012
  // --- Radio group (radio)
@@ -1250,6 +1266,8 @@ export declare class RayTextarea extends FieldControl {
1250
1266
  complete: ((text: string, caret: number) => CompletionResult | null | Promise<CompletionResult | null>) | null;
1251
1267
  /** Accessible name of the suggestion list. */
1252
1268
  suggestionsLabel: string;
1269
+ /** The highlight range under a point of the viewport (clientX, clientY), or null. */
1270
+ highlightAt(x: number, y: number): HighlightRange | null;
1253
1271
  /** Runs highlight again, when what it marks changed but the text did not. */
1254
1272
  refreshHighlight(): void;
1255
1273
  addEventListener<K extends keyof RayTextareaEventMap>(type: K, listener: (this: RayTextarea, ev: RayTextareaEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
@@ -1260,6 +1278,8 @@ export declare class RayTextarea extends FieldControl {
1260
1278
  export interface RayTextareaEventMap extends HTMLElementEventMap {
1261
1279
  /** A suggestion was accepted (the text already has it). */
1262
1280
  "ray-complete": CustomEvent<{ item: CompletionItem; from: number; to: number }>;
1281
+ /** The highlight range under the pointer changed: the range and the box of the line of it the pointer is on, or null on leaving a range. Anchor a ray-popover to rect (its anchor property) to show more than the title. */
1282
+ "ray-highlight-hover": CustomEvent<{ range: HighlightRange | null; rect: DOMRect | null }>;
1263
1283
  }
1264
1284
 
1265
1285
  // --- Bottom nav (bottom-nav)
@@ -1702,8 +1722,8 @@ export declare class RayDrawer extends RayElement {
1702
1722
  description: string | undefined;
1703
1723
  /** The edge it slides from: start/end follow the writing direction; left/right are sides of the screen. */
1704
1724
  position: "end" | "start" | "right" | "left" | "top" | "bottom";
1705
- /** Width (left/right) or height (top/bottom): xs…xl, full, or any CSS length. */
1706
- size: "xs" | "sm" | "md" | "lg" | "xl" | "full" | (string & {});
1725
+ /** Width (left/right) or height (top/bottom): xs…xl, full, content (as big as what it holds, up to the viewport: a bottom sheet) or any CSS length. */
1726
+ size: "xs" | "sm" | "md" | "lg" | "xl" | "full" | "content" | (string & {});
1707
1727
  /** No backdrop nor scroll lock: the page stays interactive (a side panel). */
1708
1728
  modeless: boolean | undefined;
1709
1729
  /** Detached from the edge, with rounded corners. */
@@ -1747,6 +1767,8 @@ export declare class RayMenu extends Floating {
1747
1767
  keepOpen: boolean | undefined;
1748
1768
  /** With a native host (ray-elements/core/raylang.js in a std/ui window), opens as the system's context menu at the pointer: items and dividers, same ray-select. */
1749
1769
  native: boolean | undefined;
1770
+ /** What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it. */
1771
+ anchor: Element | DOMRect | (() => DOMRect) | null;
1750
1772
  /** Opens the items as the system's context menu (needs a native host); resolves to the chosen item, or null. */
1751
1773
  popupNative(): Promise<RayMenuItem | null>;
1752
1774
  /** Opens it. */
@@ -1807,6 +1829,8 @@ export declare class RayPopover extends Floating {
1807
1829
  placement: "bottom" | "bottom-start" | "bottom-end" | "top" | "top-start" | "top-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end";
1808
1830
  /** Gap to the trigger in px. */
1809
1831
  offset: number;
1832
+ /** What the panel is placed against instead of the trigger: an element, a DOMRect or a function returning one (a text range, the pointer; read again on every reposition). The trigger still opens it. */
1833
+ anchor: Element | DOMRect | (() => DOMRect) | null;
1810
1834
  /** Panel width (px number or CSS length). Default: its content. */
1811
1835
  width: string | undefined;
1812
1836
  /** At least as wide as the trigger. */
@@ -1151,7 +1151,7 @@
1151
1151
  },
1152
1152
  {
1153
1153
  "name": "ray-date-picker",
1154
- "description": "Values are ISO dates (\"2026-10-06\"), submitted as such. ArrowDown on the field opens the calendar; Escape closes it.",
1154
+ "description": "Values are ISO dates (\"2026-10-06\"), submitted as such. ArrowDown on the field opens the calendar; Escape closes it. On a touch screen the trigger opens the system's picker instead (native).",
1155
1155
  "attributes": [
1156
1156
  {
1157
1157
  "name": "value",
@@ -1205,6 +1205,21 @@
1205
1205
  "description": "The calendar dropdown is shown.",
1206
1206
  "valueSet": "v"
1207
1207
  },
1208
+ {
1209
+ "name": "native",
1210
+ "description": "Which picker the trigger opens: the system's (the native date input's, the usual one on a phone) or the calendar dropdown. auto: the system's on a touch screen (pointer: coarse), else the calendar. A date picked in the system's picker is the value like one picked in the calendar.",
1211
+ "values": [
1212
+ {
1213
+ "name": "auto"
1214
+ },
1215
+ {
1216
+ "name": "always"
1217
+ },
1218
+ {
1219
+ "name": "never"
1220
+ }
1221
+ ]
1222
+ },
1208
1223
  {
1209
1224
  "name": "name",
1210
1225
  "description": "Form field name."
@@ -1467,7 +1482,7 @@
1467
1482
  },
1468
1483
  {
1469
1484
  "name": "type",
1470
- "description": "Input type.",
1485
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
1471
1486
  "values": [
1472
1487
  {
1473
1488
  "name": "text"
@@ -1489,6 +1504,15 @@
1489
1504
  },
1490
1505
  {
1491
1506
  "name": "tel"
1507
+ },
1508
+ {
1509
+ "name": "time"
1510
+ },
1511
+ {
1512
+ "name": "date"
1513
+ },
1514
+ {
1515
+ "name": "datetime-local"
1492
1516
  }
1493
1517
  ]
1494
1518
  },
@@ -1699,7 +1723,7 @@
1699
1723
  },
1700
1724
  {
1701
1725
  "name": "type",
1702
- "description": "Input type.",
1726
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
1703
1727
  "values": [
1704
1728
  {
1705
1729
  "name": "text"
@@ -1721,6 +1745,15 @@
1721
1745
  },
1722
1746
  {
1723
1747
  "name": "tel"
1748
+ },
1749
+ {
1750
+ "name": "time"
1751
+ },
1752
+ {
1753
+ "name": "date"
1754
+ },
1755
+ {
1756
+ "name": "datetime-local"
1724
1757
  }
1725
1758
  ]
1726
1759
  },
@@ -2085,7 +2118,7 @@
2085
2118
  },
2086
2119
  {
2087
2120
  "name": "type",
2088
- "description": "Input type.",
2121
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
2089
2122
  "values": [
2090
2123
  {
2091
2124
  "name": "text"
@@ -2107,6 +2140,15 @@
2107
2140
  },
2108
2141
  {
2109
2142
  "name": "tel"
2143
+ },
2144
+ {
2145
+ "name": "time"
2146
+ },
2147
+ {
2148
+ "name": "date"
2149
+ },
2150
+ {
2151
+ "name": "datetime-local"
2110
2152
  }
2111
2153
  ]
2112
2154
  },
@@ -2304,7 +2346,7 @@
2304
2346
  },
2305
2347
  {
2306
2348
  "name": "type",
2307
- "description": "Input type.",
2349
+ "description": "Input type. time, date and datetime-local show the browser's picker (the native one on phones); value is what the input gives: \"HH:MM\", \"YYYY-MM-DD\", \"YYYY-MM-DDTHH:MM\".",
2308
2350
  "values": [
2309
2351
  {
2310
2352
  "name": "text"
@@ -2326,6 +2368,15 @@
2326
2368
  },
2327
2369
  {
2328
2370
  "name": "tel"
2371
+ },
2372
+ {
2373
+ "name": "time"
2374
+ },
2375
+ {
2376
+ "name": "date"
2377
+ },
2378
+ {
2379
+ "name": "datetime-local"
2329
2380
  }
2330
2381
  ]
2331
2382
  },
@@ -4464,7 +4515,7 @@
4464
4515
  },
4465
4516
  {
4466
4517
  "name": "size",
4467
- "description": "Width (left/right) or height (top/bottom): xs…xl, full, or any CSS length.",
4518
+ "description": "Width (left/right) or height (top/bottom): xs…xl, full, content (as big as what it holds, up to the viewport: a bottom sheet) or any CSS length.",
4468
4519
  "values": [
4469
4520
  {
4470
4521
  "name": "xs"
@@ -4483,6 +4534,9 @@
4483
4534
  },
4484
4535
  {
4485
4536
  "name": "full"
4537
+ },
4538
+ {
4539
+ "name": "content"
4486
4540
  }
4487
4541
  ]
4488
4542
  },