anentrypoint-design 0.0.425 → 0.0.426

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/app-shell.css +5 -0
  2. package/chat.css +41 -0
  3. package/colors_and_type.css +86 -42
  4. package/dist/247420.css +544 -82
  5. package/dist/247420.js +34 -28
  6. package/package.json +1 -1
  7. package/src/components/calendar/calendar.js +144 -0
  8. package/src/components/calendar/date-picker.js +106 -0
  9. package/src/components/calendar/grid.js +79 -0
  10. package/src/components/calendar.js +15 -0
  11. package/src/components/carousel.js +53 -0
  12. package/src/components/collab/cursors.js +68 -0
  13. package/src/components/collab/presence.js +29 -0
  14. package/src/components/collab.js +11 -0
  15. package/src/components/content/otp-input.js +91 -0
  16. package/src/components/content.js +2 -1
  17. package/src/components/context-pane/meter.js +42 -0
  18. package/src/components/context-pane/pane.js +127 -0
  19. package/src/components/context-pane/treemap.js +81 -0
  20. package/src/components/context-pane/xray.js +30 -0
  21. package/src/components/context-pane.js +12 -127
  22. package/src/components/data-density/progress.js +20 -0
  23. package/src/components/data-density.js +3 -0
  24. package/src/components/editor-primitives/layout.js +12 -0
  25. package/src/components/editor-primitives.js +2 -2
  26. package/src/components/overlay-primitives/hover-card.js +60 -0
  27. package/src/components/overlay-primitives/menubar.js +65 -0
  28. package/src/components/overlay-primitives.js +4 -0
  29. package/src/components/shell/icons.js +4 -1
  30. package/src/components/slider.js +66 -0
  31. package/src/components.js +21 -5
  32. package/src/css/app-shell/base.css +16 -0
  33. package/src/css/app-shell/calendar.css +131 -0
  34. package/src/css/app-shell/carousel.css +44 -0
  35. package/src/css/app-shell/chat-polish.css +1 -1
  36. package/src/css/app-shell/collab.css +106 -0
  37. package/src/css/app-shell/hero-content.css +34 -38
  38. package/src/css/app-shell/kits-appended.css +1 -1
  39. package/src/css/app-shell/otp-input.css +31 -0
  40. package/src/css/app-shell/slider.css +53 -0
  41. package/src/page-html/page-styles.js +7 -1
  42. package/types/components.d.ts +299 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.425",
3
+ "version": "0.0.426",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -0,0 +1,144 @@
1
+ // Calendar — controlled date-grid primitive. mode='single' calls
2
+ // onSelect(date) on click/Enter; mode='range' uses a two-click anchor
3
+ // (first click sets the range start, second sets the end) with a
4
+ // hover-preview highlight between anchor and the hovered cell. selected/
5
+ // month are owned entirely by the caller; the only state this module keeps
6
+ // is UI-only (hover-preview cell, keyboard-focused cell) via `ref`-captured
7
+ // DOM roving tabindex, matching the FileGrid/DensityPicker roving pattern in
8
+ // this codebase (querySelectorAll the live day buttons, move focus by index,
9
+ // tabindex follows focus) rather than reimplementing useRovingMenu's popup-
10
+ // menu open/close machinery, which this inline grid does not need.
11
+
12
+ import * as webjsx from '../../../vendor/webjsx/index.js';
13
+ import { Icon } from '../shell.js';
14
+ import { getLocale } from '../../i18n.js';
15
+ import {
16
+ WEEKDAY_LABELS, buildMonthGrid, key, sameDay, addMonths, isBefore, isAfter,
17
+ isDisabled, monthLabel,
18
+ } from './grid.js';
19
+ const h = webjsx.createElement;
20
+
21
+ // Module-scoped hover-preview state for range mode, keyed by the grid's own
22
+ // wrapper element — pure UI-only concern (never handed back to the caller),
23
+ // same shape as Popover's WeakMap-keyed instance bookkeeping.
24
+ const _hoverPreview = new WeakMap();
25
+
26
+ function rangeBounds(selected) {
27
+ if (!selected) return { from: null, to: null };
28
+ const { from, to } = selected;
29
+ if (from && to && isAfter(from, to)) return { from: to, to: from };
30
+ return { from, to };
31
+ }
32
+
33
+ /**
34
+ * A month date-grid. Fully controlled: `selected`/`month` are owned by the
35
+ * caller, this component holds no selection state of its own.
36
+ *
37
+ * @param {Object} props
38
+ * @param {'single'|'range'} [props.mode='single']
39
+ * @param {Date|{from:?Date,to:?Date}} [props.selected] - a Date in single mode, `{from,to}` in range mode.
40
+ * @param {Function} [props.onSelect] - single mode: `onSelect(date)`. range mode: `onSelect({from,to})`.
41
+ * @param {Date} props.month - the currently-displayed month (any date within it).
42
+ * @param {Function} [props.onMonthChange] - `onMonthChange(newMonthDate)`, fired by the prev/next nav.
43
+ * @param {Date} [props.minDate]
44
+ * @param {Date} [props.maxDate]
45
+ * @param {string} [props.locale] - BCP-47 locale for weekday/month labels; defaults to the SDK's active locale.
46
+ * @returns {*} webjsx vnode
47
+ */
48
+ export function Calendar({ mode = 'single', selected, onSelect, month, onMonthChange, minDate, maxDate, locale = getLocale() } = {}) {
49
+ const monthDate = month ? new Date(month) : new Date();
50
+ const cells = buildMonthGrid(monthDate);
51
+ const today = new Date();
52
+ const { from, to } = mode === 'range' ? rangeBounds(selected) : { from: null, to: null };
53
+
54
+ const onDayActivate = (date) => {
55
+ if (isDisabled(date, minDate, maxDate) || !onSelect) return;
56
+ if (mode === 'single') { onSelect(date); return; }
57
+ // Range: no anchor yet, or a full range already picked -> start fresh.
58
+ if (!from || (from && to)) { onSelect({ from: date, to: null }); return; }
59
+ // One anchor set -> this click closes the range (either order).
60
+ onSelect(isBefore(date, from) ? { from: date, to: from } : { from, to: date });
61
+ };
62
+
63
+ const onGridKeyDown = (e) => {
64
+ const grid = e.currentTarget;
65
+ const days = Array.from(grid.querySelectorAll('.ds-cal-day:not([disabled])'));
66
+ const cur = days.indexOf(document.activeElement);
67
+ let target = -1;
68
+ if (e.key === 'ArrowRight') target = cur + 1;
69
+ else if (e.key === 'ArrowLeft') target = cur - 1;
70
+ else if (e.key === 'ArrowDown') target = cur + 7;
71
+ else if (e.key === 'ArrowUp') target = cur - 7;
72
+ else if (e.key === 'Home') target = 0;
73
+ else if (e.key === 'End') target = days.length - 1;
74
+ else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); days[cur]?.click(); return; }
75
+ else return;
76
+ e.preventDefault();
77
+ if (target < 0 || target >= days.length) return;
78
+ days[target].focus();
79
+ };
80
+
81
+ const onDayHover = (date) => {
82
+ if (mode !== 'range' || !from || to) return;
83
+ _hoverPreview.set(monthDate, date);
84
+ };
85
+
86
+ const hoverDate = mode === 'range' ? _hoverPreview.get(monthDate) : null;
87
+ const previewFrom = from && !to && hoverDate ? (isBefore(hoverDate, from) ? hoverDate : from) : null;
88
+ const previewTo = from && !to && hoverDate ? (isBefore(hoverDate, from) ? from : hoverDate) : null;
89
+
90
+ const dayCell = (cell, idx) => {
91
+ const disabled = isDisabled(cell.date, minDate, maxDate);
92
+ const isToday = sameDay(cell.date, today);
93
+ const isSelected = mode === 'single' ? sameDay(cell.date, selected) : (sameDay(cell.date, from) || sameDay(cell.date, to));
94
+ const isRangeStart = mode === 'range' && from && sameDay(cell.date, from);
95
+ const isRangeEnd = mode === 'range' && to && sameDay(cell.date, to);
96
+ const inCommittedRange = mode === 'range' && from && to && !isBefore(cell.date, from) && !isAfter(cell.date, to) && !isRangeStart && !isRangeEnd;
97
+ const inPreviewRange = mode === 'range' && previewFrom && previewTo
98
+ && !isBefore(cell.date, previewFrom) && !isAfter(cell.date, previewTo);
99
+ const cls = ['ds-cal-day',
100
+ !cell.inMonth ? 'ds-cal-day-outside' : '',
101
+ isToday ? 'ds-cal-day-today' : '',
102
+ isSelected ? 'ds-cal-day-selected' : '',
103
+ isRangeStart ? 'ds-cal-day-range-start' : '',
104
+ isRangeEnd ? 'ds-cal-day-range-end' : '',
105
+ (inCommittedRange || (inPreviewRange && !isSelected)) ? 'ds-cal-day-in-range' : '',
106
+ disabled ? 'ds-cal-day-disabled' : ''].filter(Boolean).join(' ');
107
+ // Roving tabindex: only one day is a tab stop at a time (the selected
108
+ // day, today, or the first in-month day as fallback), matching
109
+ // DensityPicker's rovingRadio contract in files/grid-controls.js.
110
+ const isTabStop = isSelected || (!selected && isToday) || (!selected && !isToday && idx === cells.findIndex(c => c.inMonth));
111
+ return h('button', {
112
+ key: key(cell.date),
113
+ type: 'button',
114
+ class: cls,
115
+ disabled: disabled ? true : null,
116
+ 'aria-pressed': isSelected ? 'true' : 'false',
117
+ 'aria-current': isToday ? 'date' : null,
118
+ 'aria-label': cell.date.toDateString(),
119
+ tabindex: isTabStop ? '0' : '-1',
120
+ onclick: () => onDayActivate(cell.date),
121
+ onmouseenter: () => onDayHover(cell.date),
122
+ }, String(cell.date.getDate()));
123
+ };
124
+
125
+ return h('div', { class: 'ds-cal' },
126
+ h('div', { class: 'ds-cal-head' },
127
+ h('button', {
128
+ type: 'button', class: 'ds-cal-nav ds-cal-nav-prev',
129
+ 'aria-label': 'previous month',
130
+ onclick: () => onMonthChange && onMonthChange(addMonths(monthDate, -1)),
131
+ }, Icon('chevron-left', { size: 16 })),
132
+ h('span', { class: 'ds-cal-title' }, monthLabel(monthDate, locale)),
133
+ h('button', {
134
+ type: 'button', class: 'ds-cal-nav ds-cal-nav-next',
135
+ 'aria-label': 'next month',
136
+ onclick: () => onMonthChange && onMonthChange(addMonths(monthDate, 1)),
137
+ }, Icon('chevron-right', { size: 16 }))),
138
+ h('div', { class: 'ds-cal-weekdays', 'aria-hidden': 'true' },
139
+ ...WEEKDAY_LABELS.map((w, i) => h('span', { key: i, class: 'ds-cal-weekday-label' }, w))),
140
+ h('div', {
141
+ class: 'ds-cal-grid', role: 'grid', 'aria-label': monthLabel(monthDate, locale),
142
+ onkeydown: onGridKeyDown,
143
+ }, ...cells.map((c, i) => dayCell(c, i))));
144
+ }
@@ -0,0 +1,106 @@
1
+ // DatePicker / DateRangePicker — trigger button + Popover-hosted Calendar.
2
+ // Fully controlled, matching Dialog/Drawer/Popover's shape exactly: the
3
+ // CALLER owns `open`/`onOpenChange` and `month`/`onMonthChange` as real props
4
+ // (no internal open-state toggle). The one DOM-timing wrinkle: Popover needs
5
+ // the trigger's live DOM node as `anchorEl` at the moment `open` flips true,
6
+ // but that node is a same-render sibling, so a `ref` callback captured during
7
+ // THIS render is too late (Popover is constructed synchronously, before
8
+ // webjsx attaches anything to the DOM). chat/composer.js's EmojiPicker
9
+ // trigger hits the same problem and solves it the same way: look the trigger
10
+ // up via `document.querySelector` (already mounted from the prior closed-
11
+ // state render) rather than a same-pass ref value. `name` gives the query a
12
+ // stable per-instance scope so two pickers on one page don't collide.
13
+ import * as webjsx from '../../../vendor/webjsx/index.js';
14
+ import { Popover } from '../overlay-primitives/popover.js';
15
+ import { Icon } from '../shell.js';
16
+ import { getLocale } from '../../i18n.js';
17
+ import { Calendar } from './calendar.js';
18
+ import { formatDate } from './grid.js';
19
+ const h = webjsx.createElement;
20
+
21
+ function findAnchor(name) {
22
+ return typeof document !== 'undefined' ? document.querySelector('.ds-dp-trigger[data-dp-name="' + name + '"]') : null;
23
+ }
24
+
25
+ /**
26
+ * Trigger button that opens a Popover hosting a single-mode Calendar.
27
+ *
28
+ * @param {Object} props
29
+ * @param {Date} [props.value] - the selected date.
30
+ * @param {Function} [props.onChange] - `onChange(date)`, fired on day select.
31
+ * @param {boolean} [props.open] - popover open state, owned by the caller.
32
+ * @param {Function} [props.onOpenChange] - `onOpenChange(nextOpen)`, fired by the trigger click and on close (Escape/outside-click/selection).
33
+ * @param {Date} [props.month] - displayed month; defaults to `value` or today when omitted.
34
+ * @param {Function} [props.onMonthChange] - `onMonthChange(newMonthDate)`, fired by the prev/next nav.
35
+ * @param {string} [props.placeholder='Select date'] - trigger label when `value` is unset.
36
+ * @param {Date} [props.minDate]
37
+ * @param {Date} [props.maxDate]
38
+ * @param {string} [props.name='dp'] - stable id distinguishing multiple pickers' anchor lookup; set explicitly when rendering more than one DatePicker on a page.
39
+ * @param {string} [props.locale]
40
+ * @returns {*} webjsx vnode
41
+ */
42
+ export function DatePicker({ value, onChange, open = false, onOpenChange, month, onMonthChange, placeholder = 'Select date', minDate, maxDate, name = 'dp', locale = getLocale() } = {}) {
43
+ const close = () => onOpenChange && onOpenChange(false);
44
+ const displayedMonth = month || value || new Date();
45
+ const label = value ? formatDate(value, locale) : placeholder;
46
+ const trigger = h('button', {
47
+ type: 'button', class: 'ds-dp-trigger', 'data-dp-name': name,
48
+ 'aria-haspopup': 'dialog', 'aria-expanded': open ? 'true' : 'false',
49
+ onclick: () => onOpenChange && onOpenChange(!open),
50
+ }, Icon('page', { size: 15 }), h('span', { class: 'ds-dp-trigger-label' }, label));
51
+ const popover = Popover({
52
+ open, anchorEl: open ? findAnchor(name) : null, onClose: close,
53
+ ariaLabel: 'choose date',
54
+ children: Calendar({
55
+ mode: 'single',
56
+ selected: value,
57
+ month: displayedMonth,
58
+ onMonthChange: (m) => onMonthChange && onMonthChange(m),
59
+ onSelect: (d) => { onChange && onChange(d); close(); },
60
+ minDate, maxDate, locale,
61
+ }),
62
+ });
63
+ return h('span', { class: 'ds-dp' }, trigger, popover);
64
+ }
65
+
66
+ /**
67
+ * Trigger button that opens a Popover hosting a range-mode Calendar.
68
+ *
69
+ * @param {Object} props
70
+ * @param {{from:?Date,to:?Date}} [props.value]
71
+ * @param {Function} [props.onChange] - `onChange({from,to})`, fired on each click.
72
+ * @param {boolean} [props.open] - popover open state, owned by the caller.
73
+ * @param {Function} [props.onOpenChange] - `onOpenChange(nextOpen)`; also fired with `false` once both ends of the range are picked.
74
+ * @param {Date} [props.month]
75
+ * @param {Function} [props.onMonthChange]
76
+ * @param {string} [props.placeholder='Select dates']
77
+ * @param {Date} [props.minDate]
78
+ * @param {Date} [props.maxDate]
79
+ * @param {string} [props.name='drp'] - stable id distinguishing multiple pickers' anchor lookup.
80
+ * @param {string} [props.locale]
81
+ * @returns {*} webjsx vnode
82
+ */
83
+ export function DateRangePicker({ value, onChange, open = false, onOpenChange, month, onMonthChange, placeholder = 'Select dates', minDate, maxDate, name = 'drp', locale = getLocale() } = {}) {
84
+ const close = () => onOpenChange && onOpenChange(false);
85
+ const from = value && value.from, to = value && value.to;
86
+ const displayedMonth = month || from || new Date();
87
+ const label = from ? (formatDate(from, locale) + ' – ' + (to ? formatDate(to, locale) : '…')) : placeholder;
88
+ const trigger = h('button', {
89
+ type: 'button', class: 'ds-dp-trigger', 'data-dp-name': name,
90
+ 'aria-haspopup': 'dialog', 'aria-expanded': open ? 'true' : 'false',
91
+ onclick: () => onOpenChange && onOpenChange(!open),
92
+ }, Icon('page', { size: 15 }), h('span', { class: 'ds-dp-trigger-label' }, label));
93
+ const popover = Popover({
94
+ open, anchorEl: open ? findAnchor(name) : null, onClose: close,
95
+ ariaLabel: 'choose date range',
96
+ children: Calendar({
97
+ mode: 'range',
98
+ selected: { from, to },
99
+ month: displayedMonth,
100
+ onMonthChange: (m) => onMonthChange && onMonthChange(m),
101
+ onSelect: (range) => { onChange && onChange(range); if (range.from && range.to) close(); },
102
+ minDate, maxDate, locale,
103
+ }),
104
+ });
105
+ return h('span', { class: 'ds-dp ds-drp' }, trigger, popover);
106
+ }
@@ -0,0 +1,79 @@
1
+ // Pure date-grid math for Calendar — no DOM, no webjsx. Builds the 6x7 cell
2
+ // matrix for a given month (leading/trailing days from adjacent months
3
+ // included so every row is full), plus small date-key/compare helpers shared
4
+ // by grid.js, calendar.js and the picker shells. All dates are normalized to
5
+ // local-midnight Date objects; `key(d)` ('YYYY-MM-DD') is the identity used
6
+ // for selection/range membership comparisons instead of Date reference
7
+ // equality (which two independently-constructed Dates never satisfy).
8
+
9
+ export const WEEKDAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
10
+
11
+ export function startOfDay(d) {
12
+ const n = new Date(d);
13
+ n.setHours(0, 0, 0, 0);
14
+ return n;
15
+ }
16
+
17
+ export function key(d) {
18
+ if (!d) return null;
19
+ const n = startOfDay(d);
20
+ return n.getFullYear() + '-' + String(n.getMonth() + 1).padStart(2, '0') + '-' + String(n.getDate()).padStart(2, '0');
21
+ }
22
+
23
+ export function sameDay(a, b) {
24
+ return !!a && !!b && key(a) === key(b);
25
+ }
26
+
27
+ export function addMonths(monthDate, delta) {
28
+ const n = new Date(monthDate);
29
+ n.setDate(1);
30
+ n.setMonth(n.getMonth() + delta);
31
+ return n;
32
+ }
33
+
34
+ export function addDays(d, delta) {
35
+ const n = startOfDay(d);
36
+ n.setDate(n.getDate() + delta);
37
+ return n;
38
+ }
39
+
40
+ export function isBefore(a, b) {
41
+ return startOfDay(a).getTime() < startOfDay(b).getTime();
42
+ }
43
+
44
+ export function isAfter(a, b) {
45
+ return startOfDay(a).getTime() > startOfDay(b).getTime();
46
+ }
47
+
48
+ export function isDisabled(d, minDate, maxDate) {
49
+ if (minDate && isBefore(d, minDate)) return true;
50
+ if (maxDate && isAfter(d, maxDate)) return true;
51
+ return false;
52
+ }
53
+
54
+ // Build the 42-cell (6 week rows x 7) matrix for `monthDate`'s month. Each
55
+ // cell: { date, inMonth }. Always 6 rows so the grid height never reflows
56
+ // between months (a 4-row Feb next to a 6-row Oct would otherwise jump the
57
+ // popover/page height under it).
58
+ export function buildMonthGrid(monthDate) {
59
+ const first = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
60
+ const startOffset = first.getDay(); // 0=Sun
61
+ const gridStart = addDays(first, -startOffset);
62
+ const cells = [];
63
+ for (let i = 0; i < 42; i++) {
64
+ const date = addDays(gridStart, i);
65
+ cells.push({ date, inMonth: date.getMonth() === monthDate.getMonth() });
66
+ }
67
+ return cells;
68
+ }
69
+
70
+ export function monthLabel(monthDate, locale) {
71
+ try { return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(monthDate); }
72
+ catch { return monthDate.toDateString(); }
73
+ }
74
+
75
+ export function formatDate(d, locale) {
76
+ if (!d) return '';
77
+ try { return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(d); }
78
+ catch { return startOfDay(d).toDateString(); }
79
+ }
@@ -0,0 +1,15 @@
1
+ // Calendar / DatePicker / DateRangePicker — date-grid primitive plus its two
2
+ // trigger+popover shells. Barrel over ./calendar/*.js submodules (each stays
3
+ // single-responsibility and under the 200-line cap); the public export
4
+ // surface here matches the group-barrel shape used by editor-primitives.js /
5
+ // overlay-primitives.js (import-then-bare-export, never `export … from`).
6
+
7
+ import { Calendar } from './calendar/calendar.js';
8
+ import { DatePicker, DateRangePicker } from './calendar/date-picker.js';
9
+ import { WEEKDAY_LABELS, buildMonthGrid, formatDate, monthLabel } from './calendar/grid.js';
10
+
11
+ export {
12
+ Calendar,
13
+ DatePicker, DateRangePicker,
14
+ WEEKDAY_LABELS, buildMonthGrid, formatDate, monthLabel,
15
+ };
@@ -0,0 +1,53 @@
1
+ // Carousel — horizontal (or vertical) scroll-snap content carousel. Native
2
+ // CSS scroll-snap does the heavy lifting; prev/next Btns call scrollBy() on
3
+ // the track element via a `ref` callback (the same raw-DOM-node-reference
4
+ // pattern used across the codebase, e.g. editor-primitives/split-panel.js's
5
+ // `ref: (el) => { rootEl = el; }`). No drag-library dependency.
6
+
7
+ import * as webjsx from '../../vendor/webjsx/index.js';
8
+ import { Btn } from './shell/atoms.js';
9
+ import { Icon } from './shell/icons.js';
10
+ const h = webjsx.createElement;
11
+
12
+ /**
13
+ * A scroll-snap content carousel with prev/next controls.
14
+ *
15
+ * @param {Object} [props]
16
+ * @param {Array} [props.items=[]]
17
+ * @param {Function} props.renderItem - (item, index) => vnode.
18
+ * @param {'horizontal'|'vertical'} [props.orientation='horizontal']
19
+ * @param {string} [props.label] - accessible name for the region.
20
+ * @param {*} [props.key]
21
+ * @returns {*} webjsx vnode
22
+ */
23
+ export function Carousel({ items = [], renderItem, orientation = 'horizontal', label = 'carousel', key } = {}) {
24
+ const isVert = orientation === 'vertical';
25
+ let trackEl = null;
26
+ const scrollByPage = (dir) => {
27
+ if (!trackEl) return;
28
+ const delta = isVert
29
+ ? trackEl.clientHeight * 0.9 * dir
30
+ : trackEl.clientWidth * 0.9 * dir;
31
+ trackEl.scrollBy(isVert ? { top: delta, behavior: 'smooth' } : { left: delta, behavior: 'smooth' });
32
+ };
33
+ const track = h('div', {
34
+ key: 'track',
35
+ class: 'ds-carousel-track' + (isVert ? ' ds-carousel-track--vertical' : ''),
36
+ role: 'group',
37
+ 'aria-label': label,
38
+ ref: (el) => { trackEl = el; }
39
+ }, ...items.map((item, i) => h('div', { key: 'item-' + i, class: 'ds-carousel-item' }, renderItem(item, i))));
40
+ return h('div', { key, class: 'ds-carousel' + (isVert ? ' ds-carousel--vertical' : '') },
41
+ Btn({
42
+ key: 'prev', variant: 'ghost', class: 'ds-carousel-prev',
43
+ 'aria-label': 'previous', onClick: () => scrollByPage(-1),
44
+ children: Icon(isVert ? 'chevron-up' : 'chevron-left')
45
+ }),
46
+ track,
47
+ Btn({
48
+ key: 'next', variant: 'ghost', class: 'ds-carousel-next',
49
+ 'aria-label': 'next', onClick: () => scrollByPage(1),
50
+ children: Icon(isVert ? 'chevron-down' : 'chevron-right')
51
+ })
52
+ );
53
+ }
@@ -0,0 +1,68 @@
1
+ // LiveCursorOverlay + RemoteSelectionRings + RecentEditHighlightFlash — the
2
+ // real-time multiplayer layer: remote collaborators' pointer positions, text
3
+ // selections, and just-edited regions. No existing precedent in this kit
4
+ // (community/presence.js is Discord-style voice/member presence, a different
5
+ // shape entirely) — these three are full-bleed overlays meant to sit inside
6
+ // a positioned editor/canvas container, `pointer-events: none` throughout so
7
+ // they never intercept the local user's own input.
8
+
9
+ import * as webjsx from '../../../vendor/webjsx/index.js';
10
+ import { Icon } from '../shell.js';
11
+ const h = webjsx.createElement;
12
+
13
+ // LiveCursorOverlay({ cursors }) — cursors: [{ userId, x, y, color, label }]
14
+ export function LiveCursorOverlay({ cursors = [] } = {}) {
15
+ return h('div', { class: 'ds-collab-cursor-overlay', 'aria-hidden': 'true' },
16
+ ...cursors.map((c) => h('div', {
17
+ key: c.userId, class: 'ds-collab-cursor', style: `left:${c.x}px;top:${c.y}px;color:${c.color}`,
18
+ },
19
+ Icon('cursor', { size: 18 }),
20
+ c.label ? h('span', { class: 'ds-collab-cursor-label', style: `background:${c.color}` }, c.label) : null)));
21
+ }
22
+
23
+ // LiveCursorOverlay above takes FLAT `x`/`y`, so a caller reasonably passes the
24
+ // same flat shape here — and `s.rect.left` on a flat object throws a bare
25
+ // TypeError during synchronous mount, which blanks the WHOLE kit rather than one
26
+ // specimen. Accept either shape, and if neither is present fail loudly naming
27
+ // the offending entry, per the fail-fast-with-exact-state invariant.
28
+ function rectOf(entry, component) {
29
+ const r = entry.rect || entry;
30
+ const has = (v) => typeof v === 'number' && Number.isFinite(v);
31
+ const left = has(r.left) ? r.left : r.x;
32
+ const top = has(r.top) ? r.top : r.y;
33
+ if (!has(left) || !has(top) || !has(r.width) || !has(r.height)) {
34
+ throw new Error(`${component}: entry needs {rect:{top,left,width,height}} or flat {x,y,width,height}; got ${JSON.stringify(entry)}`);
35
+ }
36
+ return { left, top, width: r.width, height: r.height };
37
+ }
38
+
39
+ // RemoteSelectionRings({ selections }) — selections: [{ userId, rect: {top,left,width,height}, color }]
40
+ // A flat { x, y, width, height } entry is accepted too, matching LiveCursorOverlay.
41
+ export function RemoteSelectionRings({ selections = [] } = {}) {
42
+ return h('div', { class: 'ds-collab-selection-overlay', 'aria-hidden': 'true' },
43
+ ...selections.map((s) => {
44
+ const r = rectOf(s, 'RemoteSelectionRings');
45
+ return h('div', {
46
+ key: s.userId,
47
+ class: 'ds-collab-selection-ring',
48
+ style: `left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;--ring-color:${s.color}`,
49
+ });
50
+ }));
51
+ }
52
+
53
+ // RecentEditHighlightFlash({ edits }) — edits: [{ rect, color, timestamp }]
54
+ // A flat { x, y, width, height } entry is accepted too, matching LiveCursorOverlay.
55
+ // Brief fade-out background flash per recently-edited region. Reduced-motion
56
+ // users get an instant flash with no animation (the `ds-no-anim` modifier
57
+ // class, toggled off any transition/animation in CSS) instead of a fade.
58
+ export function RecentEditHighlightFlash({ edits = [] } = {}) {
59
+ return h('div', { class: 'ds-collab-flash-overlay', 'aria-hidden': 'true' },
60
+ ...edits.map((e, i) => {
61
+ const r = rectOf(e, 'RecentEditHighlightFlash');
62
+ return h('div', {
63
+ key: e.timestamp != null ? String(e.timestamp) + i : i,
64
+ class: 'ds-collab-flash',
65
+ style: `left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;--flash-color:${e.color}`,
66
+ });
67
+ }));
68
+ }
@@ -0,0 +1,29 @@
1
+ // AgentPresenceChip + PresenceBar — small avatar-chip-in-a-row collaborator
2
+ // presence, generalized from community/presence.js's Discord-style VoiceUser/
3
+ // MemberItem chips (same avatar-circle-with-initial + name-label shape, same
4
+ // --avatar-bg custom-property color hook) but for arbitrary collaborator
5
+ // identity rather than voice/member semantics specifically — prefixed
6
+ // ds-collab-chip (not cm-*) since this is a conceptually different feature
7
+ // living in its own group.
8
+
9
+ import * as webjsx from '../../../vendor/webjsx/index.js';
10
+ import { avatarInitial } from '../content.js';
11
+ const h = webjsx.createElement;
12
+
13
+ // AgentPresenceChip({ userId, label, color, status })
14
+ // status: 'active' | 'idle' | 'offline' — mirrors MemberItem's status dot.
15
+ export function AgentPresenceChip({ userId, label, color, status = 'active', key } = {}) {
16
+ const initial = avatarInitial(label || userId);
17
+ return h('div', { key, class: 'ds-collab-chip', title: label || userId },
18
+ h('div', { class: 'ds-collab-chip-avatar', style: color ? `--avatar-bg:${color}` : null },
19
+ h('span', { class: 'ds-collab-chip-status ds-collab-chip-status-' + status }),
20
+ initial),
21
+ h('span', { class: 'ds-collab-chip-name' }, label || userId));
22
+ }
23
+
24
+ // PresenceBar({ users }) — users: [{ userId, label, color, status }]
25
+ export function PresenceBar({ users = [] } = {}) {
26
+ if (!users.length) return h('div', { class: 'ds-collab-bar ds-collab-bar-empty', role: 'status' }, 'no collaborators online');
27
+ return h('div', { class: 'ds-collab-bar', role: 'group', 'aria-label': 'collaborators online' },
28
+ ...users.map((u) => AgentPresenceChip({ ...u, key: u.userId })));
29
+ }
@@ -0,0 +1,11 @@
1
+ // Collab — real-time multiplayer co-editing presence: live remote cursors,
2
+ // selection rings, recent-edit flashes, and a generalized collaborator
3
+ // presence chip/bar (visually related to community/presence.js's Discord-
4
+ // style voice/member chips but for arbitrary collaborator identity, not
5
+ // voice/member semantics). Group barrel over ./collab/*.js submodules,
6
+ // following the same group-barrel pattern as community.js/editor-primitives.js.
7
+
8
+ import { LiveCursorOverlay, RemoteSelectionRings, RecentEditHighlightFlash } from './collab/cursors.js';
9
+ import { AgentPresenceChip, PresenceBar } from './collab/presence.js';
10
+
11
+ export { LiveCursorOverlay, RemoteSelectionRings, RecentEditHighlightFlash, AgentPresenceChip, PresenceBar };
@@ -0,0 +1,91 @@
1
+ // InputOTP — segmented PIN/code-entry input. `length` real <input> boxes
2
+ // (not a single overlaid input) so each box gets a real accessible name and
3
+ // native text-cursor behavior; auto-advance-on-type, backspace-retreat, and
4
+ // paste-splits-across-boxes are wired by hand since no browser gives this
5
+ // pattern for free. First box carries autocomplete="one-time-code" so mobile
6
+ // SMS/keychain autofill still targets the group.
7
+
8
+ import * as webjsx from '../../../vendor/webjsx/index.js';
9
+ const h = webjsx.createElement;
10
+
11
+ let _uid = 0;
12
+ function uid(prefix) { _uid += 1; return prefix + '-' + _uid; }
13
+
14
+ /**
15
+ * Segmented one-time-code / PIN entry.
16
+ *
17
+ * @param {Object} [props]
18
+ * @param {number} [props.length=6] - number of boxes.
19
+ * @param {string} [props.value=''] - the full code so far (controlled).
20
+ * @param {Function} [props.onChange] - called with (nextValue:string, event) on every edit.
21
+ * @param {Function} [props.onComplete] - called with (code:string) once all boxes are filled.
22
+ * @param {boolean} [props.disabled]
23
+ * @param {boolean} [props.error]
24
+ * @param {string} [props.label] - accessible name for the group.
25
+ * @param {*} [props.key]
26
+ * @returns {*} webjsx vnode
27
+ */
28
+ export function InputOTP({ length = 6, value = '', onChange, onComplete, disabled, error, label = 'code', key } = {}) {
29
+ const groupId = uid('ds-otp');
30
+ const chars = value.split('').slice(0, length);
31
+ const setValue = (next, e) => {
32
+ const clipped = next.slice(0, length);
33
+ if (onChange) onChange(clipped, e);
34
+ if (clipped.length === length && onComplete) onComplete(clipped);
35
+ };
36
+ const focusBox = (root, idx) => {
37
+ const boxes = root.querySelectorAll('.ds-otp-box');
38
+ if (boxes[idx]) boxes[idx].focus();
39
+ };
40
+ const boxes = [];
41
+ for (let i = 0; i < length; i++) {
42
+ const ch = chars[i] || '';
43
+ boxes.push(h('input', {
44
+ key: 'b' + i,
45
+ type: 'text',
46
+ inputmode: 'numeric',
47
+ autocomplete: i === 0 ? 'one-time-code' : 'off',
48
+ class: 'ds-otp-box' + (ch ? ' ds-otp-box-filled' : '') + (error ? ' ds-otp-box-error' : ''),
49
+ maxlength: '1',
50
+ value: ch,
51
+ disabled: disabled ? true : null,
52
+ 'aria-label': label + ' digit ' + (i + 1) + ' of ' + length,
53
+ 'aria-invalid': error ? 'true' : null,
54
+ oninput: (e) => {
55
+ const raw = e.target.value.replace(/\s/g, '');
56
+ const c = raw.slice(-1);
57
+ const arr = chars.slice();
58
+ arr[i] = c;
59
+ setValue(arr.join('').slice(0, length), e);
60
+ if (c && i < length - 1) focusBox(e.currentTarget.closest('.ds-otp'), i + 1);
61
+ },
62
+ onkeydown: (e) => {
63
+ if (e.key === 'Backspace' && !e.currentTarget.value && i > 0) {
64
+ e.preventDefault();
65
+ const arr = chars.slice();
66
+ arr[i - 1] = '';
67
+ setValue(arr.join(''), e);
68
+ focusBox(e.currentTarget.closest('.ds-otp'), i - 1);
69
+ } else if (e.key === 'ArrowLeft' && i > 0) {
70
+ e.preventDefault();
71
+ focusBox(e.currentTarget.closest('.ds-otp'), i - 1);
72
+ } else if (e.key === 'ArrowRight' && i < length - 1) {
73
+ e.preventDefault();
74
+ focusBox(e.currentTarget.closest('.ds-otp'), i + 1);
75
+ }
76
+ },
77
+ onpaste: (e) => {
78
+ e.preventDefault();
79
+ const text = (e.clipboardData || window.clipboardData).getData('text').replace(/\s/g, '');
80
+ if (!text) return;
81
+ setValue(text.slice(0, length), e);
82
+ const root = e.currentTarget.closest('.ds-otp');
83
+ const landAt = Math.min(text.length, length) - 1;
84
+ if (landAt >= 0) focusBox(root, landAt);
85
+ }
86
+ }));
87
+ }
88
+ return h('div', { key, id: groupId, class: 'ds-otp', role: 'group', 'aria-label': label },
89
+ ...boxes
90
+ );
91
+ }
@@ -15,6 +15,7 @@ import { WorksList, WritingList, EventList } from './content/lists.js';
15
15
  import { Kpi, Sparkline, BarChart } from './content/charts.js';
16
16
  import { Table, HealthTable, ProcessRegistryTable } from './content/table.js';
17
17
  import { SearchInput, TextField, Select, Form } from './content/fields.js';
18
+ import { InputOTP } from './content/otp-input.js';
18
19
  import { Spinner, Skeleton, Alert, FilterPills } from './content/feedback.js';
19
20
  import { HomeView, ProjectView } from './content/views.js';
20
21
 
@@ -27,7 +28,7 @@ export {
27
28
  WorksList, WritingList, EventList,
28
29
  Kpi, Sparkline, BarChart,
29
30
  Table, HealthTable, ProcessRegistryTable,
30
- SearchInput, TextField, Select, Form,
31
+ SearchInput, TextField, Select, Form, InputOTP,
31
32
  Spinner, Skeleton, Alert, FilterPills,
32
33
  HomeView, ProjectView,
33
34
  };