anentrypoint-design 0.0.381 → 0.0.383
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/dist/247420.css +406 -180
- package/dist/247420.js +26 -26
- package/package.json +1 -1
- package/src/components/content/avatar.js +31 -0
- package/src/components/content/charts.js +50 -0
- package/src/components/content/cli.js +46 -0
- package/src/components/content/feedback.js +77 -0
- package/src/components/content/fields.js +136 -0
- package/src/components/content/hero.js +146 -0
- package/src/components/content/lists.js +85 -0
- package/src/components/content/panel.js +79 -0
- package/src/components/content/row.js +115 -0
- package/src/components/content/table.js +97 -0
- package/src/components/content/views.js +81 -0
- package/src/components/content.js +29 -861
- package/src/components/editor-primitives/batch.js +60 -0
- package/src/components/editor-primitives/chrome.js +146 -0
- package/src/components/editor-primitives/collapse.js +46 -0
- package/src/components/editor-primitives/context-menu.js +127 -0
- package/src/components/editor-primitives/diagnostics.js +44 -0
- package/src/components/editor-primitives/focus-trap.js +26 -0
- package/src/components/editor-primitives/json-viewer.js +123 -0
- package/src/components/editor-primitives/layout.js +89 -0
- package/src/components/editor-primitives/modals.js +89 -0
- package/src/components/editor-primitives/pager.js +74 -0
- package/src/components/editor-primitives/property-grid.js +57 -0
- package/src/components/editor-primitives/shared.js +18 -0
- package/src/components/editor-primitives/split-panel.js +101 -0
- package/src/components/editor-primitives/toast.js +59 -0
- package/src/components/editor-primitives/tree.js +75 -0
- package/src/components/editor-primitives.js +35 -1048
- package/src/components/files-modals.js +1 -1
- package/src/components/overlay-primitives/approval-prompt.js +38 -0
- package/src/components/overlay-primitives/auth-modal.js +89 -0
- package/src/components/overlay-primitives/command-palette.js +101 -0
- package/src/components/overlay-primitives/emoji-picker.js +103 -0
- package/src/components/overlay-primitives/floating.js +146 -0
- package/src/components/overlay-primitives/full-screen.js +45 -0
- package/src/components/overlay-primitives/menus.js +135 -0
- package/src/components/overlay-primitives/popover.js +51 -0
- package/src/components/overlay-primitives/roving-menu.js +73 -0
- package/src/components/overlay-primitives/settings-popover.js +85 -0
- package/src/components/overlay-primitives/tooltip.js +55 -0
- package/src/components/overlay-primitives.js +33 -855
- package/src/css/app-shell/base.css +54 -1
- package/src/css/app-shell/chat-polish.css +39 -32
- package/src/css/app-shell/data-density.css +25 -21
- package/src/css/app-shell/files.css +37 -29
- package/src/css/app-shell/kits-appended.css +92 -50
- package/src/css/app-shell/responsive.css +73 -35
- package/src/css/app-shell/responsive2-workspace.css +16 -4
- package/src/css/app-shell/states-interactions.css +21 -4
- package/src/css/app-shell/topbar.css +5 -1
- package/src/kits/os/freddie-dashboard.css +2 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Layout primitives — the static structural shapes an editor lays its panes
|
|
2
|
+
// out with: Dock (five-region frame), the BP_* breakpoint scale +
|
|
3
|
+
// useMediaQuery, Grid/GridItem (24-column responsive layout) and Divider.
|
|
4
|
+
// Interactive resizing lives in ./split-panel.js; progressive disclosure in
|
|
5
|
+
// ./collapse.js.
|
|
6
|
+
|
|
7
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
8
|
+
import { kids } from './shared.js';
|
|
9
|
+
const h = webjsx.createElement;
|
|
10
|
+
|
|
11
|
+
export function Dock({ top, left, right, bottom, center } = {}) {
|
|
12
|
+
return h('div', { class: 'ds-ep-dock' },
|
|
13
|
+
top != null ? h('div', { class: 'ds-ep-dock-top' }, ...kids(top)) : null,
|
|
14
|
+
left != null ? h('div', { class: 'ds-ep-dock-left' }, ...kids(left)) : null,
|
|
15
|
+
h('div', { class: 'ds-ep-dock-center' }, ...kids(center)),
|
|
16
|
+
right != null ? h('div', { class: 'ds-ep-dock-right' }, ...kids(right)) : null,
|
|
17
|
+
bottom != null ? h('div', { class: 'ds-ep-dock-bottom' }, ...kids(bottom)) : null
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Breakpoints + useMediaQuery
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
export const BP_SM = 480;
|
|
25
|
+
export const BP_MD = 768;
|
|
26
|
+
export const BP_LG = 1024;
|
|
27
|
+
export const BP_XL = 1440;
|
|
28
|
+
|
|
29
|
+
export function useMediaQuery(query) {
|
|
30
|
+
if (typeof window === 'undefined' || !window.matchMedia) {
|
|
31
|
+
return { matches: false, addListener: () => {}, removeListener: () => {} };
|
|
32
|
+
}
|
|
33
|
+
const mql = window.matchMedia(query);
|
|
34
|
+
return {
|
|
35
|
+
get matches() { return mql.matches; },
|
|
36
|
+
addListener(fn) { mql.addEventListener ? mql.addEventListener('change', fn) : mql.addListener(fn); },
|
|
37
|
+
removeListener(fn) { mql.removeEventListener ? mql.removeEventListener('change', fn) : mql.removeListener(fn); },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Grid / GridItem — 24-column responsive layout primitive (screen-real-estate
|
|
43
|
+
// density: dense multi-column panels without a hand-rolled grid-template-
|
|
44
|
+
// columns per consumer). Column-span props are integers 1-24 (or `true` for
|
|
45
|
+
// full-width/auto-grow, or `0` to hide at that breakpoint) evaluated at four
|
|
46
|
+
// tiers mirroring BP_SM/MD/LG/XL (480/768/1024/1440) via media queries in
|
|
47
|
+
// editor-primitives.css — no JS-side matchMedia needed, CSS custom
|
|
48
|
+
// properties + @media do the layout work so it degrades gracefully with
|
|
49
|
+
// SSR/no-hydration. Grid itself is a flex row wrapper; GridItem computes
|
|
50
|
+
// flex-basis/max-width from its span at each tier.
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
export function Grid({ gap, justify, align, children, key } = {}) {
|
|
53
|
+
const style = [
|
|
54
|
+
gap != null ? `gap:${typeof gap === 'number' ? gap + 'px' : gap}` : '',
|
|
55
|
+
justify ? `justify-content:${justify}` : '',
|
|
56
|
+
align ? `align-items:${align}` : '',
|
|
57
|
+
].filter(Boolean).join(';');
|
|
58
|
+
return h('div', { key, class: 'ds-ep-grid', style: style || null }, children);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function gridSpanStyle(prefix, val) {
|
|
62
|
+
if (val === undefined) return '';
|
|
63
|
+
if (val === true) return `--${prefix}-basis:100%;--${prefix}-grow:1;--${prefix}-display:inherit;`;
|
|
64
|
+
if (val === 0) return `--${prefix}-display:none;`;
|
|
65
|
+
const pct = Math.max(0, Math.min(100, (100 / 24) * val));
|
|
66
|
+
return `--${prefix}-basis:${pct}%;--${prefix}-grow:0;--${prefix}-display:inherit;`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function GridItem({ xs, sm, md, lg, xl, children, key } = {}) {
|
|
70
|
+
const style = [
|
|
71
|
+
gridSpanStyle('xs', xs),
|
|
72
|
+
gridSpanStyle('sm', sm),
|
|
73
|
+
gridSpanStyle('md', md),
|
|
74
|
+
gridSpanStyle('lg', lg),
|
|
75
|
+
gridSpanStyle('xl', xl),
|
|
76
|
+
].join('');
|
|
77
|
+
return h('div', { key, class: 'ds-ep-grid-item', style: style || null }, children);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Divider — plain rule, optional centered text label, optional vertical
|
|
82
|
+
// orientation (for segmenting dense panels without a full Section wrapper).
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
export function Divider({ label, vertical = false, key } = {}) {
|
|
85
|
+
if (vertical) return h('span', { key, class: 'ds-ep-divider ds-ep-divider-vertical', role: 'separator', 'aria-orientation': 'vertical' });
|
|
86
|
+
if (!label) return h('hr', { key, class: 'ds-ep-divider' });
|
|
87
|
+
return h('div', { key, class: 'ds-ep-divider ds-ep-divider-labeled', role: 'separator' },
|
|
88
|
+
h('span', { class: 'ds-ep-divider-label' }, label));
|
|
89
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Modal surfaces — Drawer (slide-in from an edge) and Dialog (centered
|
|
2
|
+
// modal with an actions row). Both are controlled via `open`/`onClose`,
|
|
3
|
+
// backdrop-dismissable (Dialog opt-in via `dismissible`), Escape-closing,
|
|
4
|
+
// and Tab-trapped through the shared trapTabKey helper.
|
|
5
|
+
|
|
6
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
7
|
+
import { kids, FOCUSABLE_SEL, trapTabKey } from './shared.js';
|
|
8
|
+
const h = webjsx.createElement;
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Drawer — slide-in from side. side='left'|'right'|'bottom'.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export function Drawer({ side = 'left', open = false, onClose, children, ariaLabel } = {}) {
|
|
14
|
+
if (!open) return null;
|
|
15
|
+
const onKey = (e) => { if (e.key === 'Escape') { e.preventDefault(); onClose && onClose(); } };
|
|
16
|
+
return h('div', {
|
|
17
|
+
class: 'ds-ep-drawer-backdrop',
|
|
18
|
+
onmousedown: (e) => { if (e.target === e.currentTarget) onClose && onClose(); },
|
|
19
|
+
},
|
|
20
|
+
h('div', {
|
|
21
|
+
class: 'ds-ep-drawer side-' + side,
|
|
22
|
+
role: 'dialog',
|
|
23
|
+
'aria-modal': 'true',
|
|
24
|
+
'aria-label': ariaLabel || 'Drawer',
|
|
25
|
+
tabindex: '-1',
|
|
26
|
+
onkeydown: onKey,
|
|
27
|
+
ref: (el) => {
|
|
28
|
+
if (!el || el._dsTrap) return;
|
|
29
|
+
el._dsTrap = true;
|
|
30
|
+
el.addEventListener('keydown', (e) => trapTabKey(el, e));
|
|
31
|
+
// setTimeout(0), not queueMicrotask — see Dialog's identical comment
|
|
32
|
+
// below: the opening click's own default focus can win a same-tick race.
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
const f = el.querySelector(FOCUSABLE_SEL);
|
|
35
|
+
(f || el).focus();
|
|
36
|
+
}, 0);
|
|
37
|
+
},
|
|
38
|
+
}, ...kids(children))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Dialog — modal. actions = [{label, onClick, kind?}], dismissible (backdrop).
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
export function Dialog({ title, open = false, onClose, children, actions = [], dismissible = false, ariaLabel } = {}) {
|
|
46
|
+
if (!open) return null;
|
|
47
|
+
const opener = (typeof document !== 'undefined') ? document.activeElement : null;
|
|
48
|
+
const close = () => {
|
|
49
|
+
if (onClose) onClose();
|
|
50
|
+
if (opener && opener.focus) queueMicrotask(() => opener.focus());
|
|
51
|
+
};
|
|
52
|
+
const onKey = (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } };
|
|
53
|
+
return h('div', {
|
|
54
|
+
class: 'ds-ep-dialog-backdrop',
|
|
55
|
+
onmousedown: (e) => { if (dismissible && e.target === e.currentTarget) close(); },
|
|
56
|
+
},
|
|
57
|
+
h('div', {
|
|
58
|
+
class: 'ds-ep-dialog',
|
|
59
|
+
role: 'dialog',
|
|
60
|
+
'aria-modal': 'true',
|
|
61
|
+
'aria-label': ariaLabel || title || 'Dialog',
|
|
62
|
+
tabindex: '-1',
|
|
63
|
+
onkeydown: onKey,
|
|
64
|
+
ref: (el) => {
|
|
65
|
+
if (!el || el._dsTrap) return;
|
|
66
|
+
el._dsTrap = true;
|
|
67
|
+
el.addEventListener('keydown', (e) => trapTabKey(el, e));
|
|
68
|
+
// setTimeout(0), not queueMicrotask: the triggering click's own default
|
|
69
|
+
// focus-on-click can win a same-tick microtask race and leave focus on
|
|
70
|
+
// the trigger button instead of the dialog, breaking Escape/Tab-trap
|
|
71
|
+
// for keyboard users (keydown only bubbles from the focused element).
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
const f = el.querySelector(FOCUSABLE_SEL);
|
|
74
|
+
(f || el).focus();
|
|
75
|
+
}, 0);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
title != null ? h('div', { class: 'ds-ep-dialog-head' }, h('h2', { class: 'ds-ep-dialog-title' }, title)) : null,
|
|
79
|
+
h('div', { class: 'ds-ep-dialog-body' }, ...kids(children)),
|
|
80
|
+
actions && actions.length ? h('div', { class: 'ds-ep-dialog-actions' },
|
|
81
|
+
...actions.map((a, i) => h('button', {
|
|
82
|
+
key: i, type: 'button',
|
|
83
|
+
class: 'ds-ep-dialog-btn' + (a.kind ? (' kind-' + a.kind) : ''),
|
|
84
|
+
onclick: (e) => { if (a.onClick) a.onClick(e); if (a.close !== false) close(); }
|
|
85
|
+
}, a.label))
|
|
86
|
+
) : null
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Pager — prev/next paginator with a page label. Generalizes gmsniff's
|
|
3
|
+
// gm-pager. page is 1-indexed; pageCount<=1 disables both buttons (no
|
|
4
|
+
// divide-by-zero, no dead-end enabled control). total (optional) renders an
|
|
5
|
+
// item-count suffix ("42 items") alongside the page label.
|
|
6
|
+
//
|
|
7
|
+
// numbered=true switches to a compact numbered-button row (screen-real-estate
|
|
8
|
+
// dense mode) instead of the prev/next label: always shows first, last, the
|
|
9
|
+
// current page, and up to `siblingCount` neighbors either side, collapsing
|
|
10
|
+
// gaps into an ellipsis span. Falls back to prev/next automatically when
|
|
11
|
+
// pageCount<=1. The prev/next contract (default) is untouched.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
15
|
+
const h = webjsx.createElement;
|
|
16
|
+
|
|
17
|
+
function buildPageRange(count, page, siblingCount) {
|
|
18
|
+
const limit = siblingCount * 2 + 3; // first + last + current + siblings
|
|
19
|
+
if (count <= limit) return Array.from({ length: count }, (_, i) => i + 1);
|
|
20
|
+
const pages = new Set([1, count, page]);
|
|
21
|
+
for (let i = 1; i <= siblingCount; i++) {
|
|
22
|
+
pages.add(page - i);
|
|
23
|
+
pages.add(page + i);
|
|
24
|
+
}
|
|
25
|
+
const sorted = [...pages].filter((p) => p >= 1 && p <= count).sort((a, b) => a - b);
|
|
26
|
+
const result = [];
|
|
27
|
+
let prev = null;
|
|
28
|
+
for (const p of sorted) {
|
|
29
|
+
if (prev !== null && p - prev > 1) result.push('...');
|
|
30
|
+
result.push(p);
|
|
31
|
+
prev = p;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function Pager({ page = 1, pageCount = 1, onPage, total, itemLabel = 'items', numbered = false, siblingCount = 1 } = {}) {
|
|
37
|
+
const safeCount = Math.max(1, pageCount || 1);
|
|
38
|
+
const safePage = Math.min(Math.max(1, page || 1), safeCount);
|
|
39
|
+
const atStart = safePage <= 1;
|
|
40
|
+
const atEnd = safePage >= safeCount;
|
|
41
|
+
const prevBtn = h('button', {
|
|
42
|
+
type: 'button', class: 'ds-ep-pager-btn', disabled: atStart ? 'disabled' : null,
|
|
43
|
+
'aria-label': 'previous page',
|
|
44
|
+
onclick: () => { if (!atStart && onPage) onPage(safePage - 1); },
|
|
45
|
+
}, '<-');
|
|
46
|
+
const nextBtn = h('button', {
|
|
47
|
+
type: 'button', class: 'ds-ep-pager-btn', disabled: atEnd ? 'disabled' : null,
|
|
48
|
+
'aria-label': 'next page',
|
|
49
|
+
onclick: () => { if (!atEnd && onPage) onPage(safePage + 1); },
|
|
50
|
+
}, '->');
|
|
51
|
+
if (numbered) {
|
|
52
|
+
const range = buildPageRange(safeCount, safePage, Math.max(1, siblingCount || 1));
|
|
53
|
+
return h('div', { class: 'ds-ep-pager ds-ep-pager-numbered', role: 'group', 'aria-label': 'pagination' },
|
|
54
|
+
prevBtn,
|
|
55
|
+
...range.map((p, i) => p === '...'
|
|
56
|
+
? h('span', { key: 'ellipsis-' + i, class: 'ds-ep-pager-ellipsis' }, '...')
|
|
57
|
+
: h('button', {
|
|
58
|
+
key: 'p' + p, type: 'button',
|
|
59
|
+
class: 'ds-ep-pager-num' + (p === safePage ? ' is-current' : ''),
|
|
60
|
+
'aria-current': p === safePage ? 'page' : null,
|
|
61
|
+
'aria-label': 'page ' + p,
|
|
62
|
+
onclick: () => { if (p !== safePage && onPage) onPage(p); },
|
|
63
|
+
}, String(p))),
|
|
64
|
+
nextBtn,
|
|
65
|
+
total != null ? h('span', { class: 'ds-ep-pager-total' }, total + ' ' + itemLabel) : null
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return h('div', { class: 'ds-ep-pager', role: 'group', 'aria-label': 'pagination' },
|
|
69
|
+
prevBtn,
|
|
70
|
+
h('span', { class: 'ds-ep-pager-label' },
|
|
71
|
+
'page ' + safePage + ' / ' + safeCount + (total != null ? ' (' + total + ' ' + itemLabel + ')' : '')),
|
|
72
|
+
nextBtn
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Property grid — the EDITABLE property-inspector family: PropertyGrid
|
|
2
|
+
// container, PropertyField (label + control + hint), PropertyGridRow (a
|
|
3
|
+
// divider-separated row wrapper) and InlineEditableField (borderless-until-
|
|
4
|
+
// focus text/textarea input with an explicit error state). Read-only debug
|
|
5
|
+
// readouts live in ./diagnostics.js instead.
|
|
6
|
+
|
|
7
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
8
|
+
import { kids } from './shared.js';
|
|
9
|
+
const h = webjsx.createElement;
|
|
10
|
+
|
|
11
|
+
export function PropertyGrid({ children } = {}) {
|
|
12
|
+
return h('div', { class: 'ds-ep-propgrid', role: 'group' }, ...kids(children));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function PropertyField({ label, hint, inline = false, children } = {}) {
|
|
16
|
+
return h('label', { class: 'ds-ep-propfield' + (inline ? ' inline' : '') },
|
|
17
|
+
h('span', { class: 'ds-ep-propfield-label' }, label),
|
|
18
|
+
h('span', { class: 'ds-ep-propfield-value' }, ...kids(children)),
|
|
19
|
+
hint != null ? h('span', { class: 'ds-ep-propfield-hint' }, hint) : null
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// PropertyGridRow — a PropertyGrid row wrapper with a bottom-border divider
|
|
25
|
+
// (last-child border suppressed), for editors that need a stronger per-row
|
|
26
|
+
// visual separation than the default PropertyGrid gap gives (e.g. a list of
|
|
27
|
+
// independently-editable records like PRD/mutable rows). Generalizes
|
|
28
|
+
// gmsniff's gm-propgrid-row.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
export function PropertyGridRow({ children, key } = {}) {
|
|
31
|
+
return h('div', { key, class: 'ds-ep-propgrid-row' }, ...kids(children));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// InlineEditableField — a borderless-until-focus text input that inherits
|
|
36
|
+
// surrounding font (no boxed input chrome), with an explicit error state
|
|
37
|
+
// (aria-invalid + danger-token border) for live per-field validation.
|
|
38
|
+
// Generalizes gmsniff's gm-inline-input / gm-field-error pair. Renders a
|
|
39
|
+
// <textarea> when multiline is set (for longer free-text edits), else a
|
|
40
|
+
// single-line <input>.
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
export function InlineEditableField({ value = '', placeholder, onInput, onChange, error, multiline = false, rows = 3, ariaLabel, disabled = false } = {}) {
|
|
43
|
+
const cls = 'ds-ep-inline-input' + (error ? ' has-error' : '');
|
|
44
|
+
const common = {
|
|
45
|
+
class: cls,
|
|
46
|
+
value,
|
|
47
|
+
placeholder,
|
|
48
|
+
disabled: disabled ? 'disabled' : null,
|
|
49
|
+
'aria-label': ariaLabel,
|
|
50
|
+
'aria-invalid': error ? 'true' : null,
|
|
51
|
+
oninput: onInput ? (e) => onInput(e.target.value, e) : null,
|
|
52
|
+
onchange: onChange ? (e) => onChange(e.target.value, e) : null,
|
|
53
|
+
};
|
|
54
|
+
return multiline
|
|
55
|
+
? h('textarea', { ...common, rows })
|
|
56
|
+
: h('input', { ...common, type: 'text' });
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Editor-primitives internal shared helpers — NOT part of the public export
|
|
2
|
+
// surface (src/components.js re-exports none of these). `kids` normalizes a
|
|
3
|
+
// children prop to an array; FOCUSABLE_SEL/trapTabKey are the focus-trap
|
|
4
|
+
// mechanics shared by FocusTrap, Drawer and Dialog.
|
|
5
|
+
|
|
6
|
+
export function kids(c) { return c == null ? [] : (Array.isArray(c) ? c : [c]); }
|
|
7
|
+
|
|
8
|
+
export const FOCUSABLE_SEL = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
9
|
+
|
|
10
|
+
export function trapTabKey(rootEl, e) {
|
|
11
|
+
if (e.key !== 'Tab') return;
|
|
12
|
+
const nodes = rootEl.querySelectorAll(FOCUSABLE_SEL);
|
|
13
|
+
if (!nodes.length) { e.preventDefault(); return; }
|
|
14
|
+
const first = nodes[0], last = nodes[nodes.length - 1];
|
|
15
|
+
const active = (rootEl.getRootNode && rootEl.getRootNode().activeElement) || document.activeElement;
|
|
16
|
+
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
|
|
17
|
+
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
|
|
18
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Resizable split — ResizeHandle (the draggable/keyboard-nudgeable separator)
|
|
2
|
+
// and SplitPanel (two children separated by one). Stateful via DOM: the
|
|
3
|
+
// dragged size is persisted across applyDiff re-renders by the pane's ref.
|
|
4
|
+
|
|
5
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
6
|
+
import { kids } from './shared.js';
|
|
7
|
+
const h = webjsx.createElement;
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// ResizeHandle — splitter, axis = 'horizontal' (vertical bar, horiz drag)
|
|
11
|
+
// or 'vertical' (horizontal bar, vertical drag). onResize(delta:px).
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export function ResizeHandle({ axis = 'horizontal', onResize, ariaLabel } = {}) {
|
|
14
|
+
const isH = axis === 'horizontal';
|
|
15
|
+
let dragOrigin = null;
|
|
16
|
+
const step = 8;
|
|
17
|
+
const emit = (dx, dy) => { if (onResize) onResize(isH ? dx : dy); };
|
|
18
|
+
const onPointerDown = (e) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
dragOrigin = { x: e.clientX, y: e.clientY };
|
|
21
|
+
e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId);
|
|
22
|
+
};
|
|
23
|
+
const onPointerMove = (e) => {
|
|
24
|
+
if (!dragOrigin) return;
|
|
25
|
+
const dx = e.clientX - dragOrigin.x;
|
|
26
|
+
const dy = e.clientY - dragOrigin.y;
|
|
27
|
+
dragOrigin = { x: e.clientX, y: e.clientY };
|
|
28
|
+
emit(dx, dy);
|
|
29
|
+
};
|
|
30
|
+
const onPointerUp = (e) => {
|
|
31
|
+
dragOrigin = null;
|
|
32
|
+
try { e.currentTarget.releasePointerCapture && e.currentTarget.releasePointerCapture(e.pointerId); } catch { /* swallow: pointer capture may already be released, drag end still proceeds */ }
|
|
33
|
+
};
|
|
34
|
+
const onKeyDown = (e) => {
|
|
35
|
+
const k = e.key;
|
|
36
|
+
if (isH) {
|
|
37
|
+
if (k === 'ArrowLeft') { e.preventDefault(); emit(-step, 0); }
|
|
38
|
+
else if (k === 'ArrowRight') { e.preventDefault(); emit(step, 0); }
|
|
39
|
+
else if (k === 'Home') { e.preventDefault(); emit(-1e6, 0); }
|
|
40
|
+
else if (k === 'End') { e.preventDefault(); emit(1e6, 0); }
|
|
41
|
+
} else {
|
|
42
|
+
if (k === 'ArrowUp') { e.preventDefault(); emit(0, -step); }
|
|
43
|
+
else if (k === 'ArrowDown') { e.preventDefault(); emit(0, step); }
|
|
44
|
+
else if (k === 'Home') { e.preventDefault(); emit(0, -1e6); }
|
|
45
|
+
else if (k === 'End') { e.preventDefault(); emit(0, 1e6); }
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return h('div', {
|
|
49
|
+
class: 'ds-ep-resize ' + (isH ? 'axis-h' : 'axis-v'),
|
|
50
|
+
role: 'separator',
|
|
51
|
+
tabindex: '0',
|
|
52
|
+
'aria-orientation': isH ? 'vertical' : 'horizontal',
|
|
53
|
+
'aria-label': ariaLabel || 'Resize',
|
|
54
|
+
onpointerdown: onPointerDown,
|
|
55
|
+
onpointermove: onPointerMove,
|
|
56
|
+
onpointerup: onPointerUp,
|
|
57
|
+
onpointercancel: onPointerUp,
|
|
58
|
+
onkeydown: onKeyDown,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// SplitPanel — two children separated by a ResizeHandle. Stateful via DOM.
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
export function SplitPanel({ orientation = 'horizontal', initial = '50%', min = 80, max = Infinity, children } = {}) {
|
|
66
|
+
const isH = orientation === 'horizontal';
|
|
67
|
+
const ks = kids(children);
|
|
68
|
+
const first = ks[0] || null;
|
|
69
|
+
const second = ks[1] || null;
|
|
70
|
+
const sizeProp = isH ? 'width' : 'height';
|
|
71
|
+
const initStyle = typeof initial === 'number' ? initial + 'px' : initial;
|
|
72
|
+
let rootEl = null;
|
|
73
|
+
// The dragged size is persisted here so a re-render (applyDiff reconciling
|
|
74
|
+
// the pane's style back to the initial value) does NOT reset the user's
|
|
75
|
+
// resize. onResize records it; the pane's ref re-applies it after each diff.
|
|
76
|
+
let draggedSize = null;
|
|
77
|
+
const applySize = (a) => {
|
|
78
|
+
if (!a) return;
|
|
79
|
+
if (draggedSize != null) { a.style[sizeProp] = draggedSize + 'px'; a.style.flex = '0 0 auto'; }
|
|
80
|
+
};
|
|
81
|
+
const onResize = (delta) => {
|
|
82
|
+
if (!rootEl) return;
|
|
83
|
+
const a = rootEl.firstChild;
|
|
84
|
+
if (!a) return;
|
|
85
|
+
const rect = a.getBoundingClientRect();
|
|
86
|
+
const curr = isH ? rect.width : rect.height;
|
|
87
|
+
const total = isH ? rootEl.getBoundingClientRect().width : rootEl.getBoundingClientRect().height;
|
|
88
|
+
const next = Math.max(min, Math.min(max === Infinity ? total - min : max, curr + delta));
|
|
89
|
+
draggedSize = next;
|
|
90
|
+
a.style[sizeProp] = next + 'px';
|
|
91
|
+
a.style.flex = '0 0 auto';
|
|
92
|
+
};
|
|
93
|
+
return h('div', {
|
|
94
|
+
class: 'ds-ep-split ' + (isH ? 'horiz' : 'vert'),
|
|
95
|
+
ref: (el) => { rootEl = el; }
|
|
96
|
+
},
|
|
97
|
+
h('div', { class: 'ds-ep-split-pane', style: '--split-size:' + initStyle + ';flex:0 0 auto', ref: applySize }, first),
|
|
98
|
+
ResizeHandle({ axis: isH ? 'horizontal' : 'vertical', onResize }),
|
|
99
|
+
h('div', { class: 'ds-ep-split-pane grow', style: 'flex:1 1 0;min-' + sizeProp + ':0' }, second)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Toast — Toast({message,kind,duration}) component + imperative toast(opts).
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
6
|
+
const h = webjsx.createElement;
|
|
7
|
+
|
|
8
|
+
export function Toast({ message, kind = 'info', duration = 3000, onClose } = {}) {
|
|
9
|
+
return h('div', {
|
|
10
|
+
class: 'ds-ep-toast kind-' + kind,
|
|
11
|
+
role: 'status',
|
|
12
|
+
'aria-live': 'polite',
|
|
13
|
+
ref: (el) => {
|
|
14
|
+
if (!el || el._dsToast) return;
|
|
15
|
+
el._dsToast = true;
|
|
16
|
+
if (duration > 0) setTimeout(() => { onClose && onClose(); el.classList.add('leaving'); }, duration);
|
|
17
|
+
}
|
|
18
|
+
}, message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let _toastHostEl = null;
|
|
22
|
+
function ensureToastHost() {
|
|
23
|
+
if (typeof document === 'undefined') return null;
|
|
24
|
+
if (_toastHostEl && document.body.contains(_toastHostEl)) return _toastHostEl;
|
|
25
|
+
_toastHostEl = document.createElement('div');
|
|
26
|
+
_toastHostEl.className = 'ds-ep-toast-host';
|
|
27
|
+
document.body.appendChild(_toastHostEl);
|
|
28
|
+
return _toastHostEl;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function toast({ message, kind = 'info', duration = 3000, actionLabel, onAction } = {}) {
|
|
32
|
+
const host = ensureToastHost();
|
|
33
|
+
if (!host) return () => {};
|
|
34
|
+
const el = document.createElement('div');
|
|
35
|
+
el.className = 'ds-ep-toast kind-' + kind;
|
|
36
|
+
el.setAttribute('role', 'status');
|
|
37
|
+
el.setAttribute('aria-live', 'polite');
|
|
38
|
+
const text = document.createElement('span');
|
|
39
|
+
text.className = 'ds-ep-toast-msg';
|
|
40
|
+
text.textContent = message;
|
|
41
|
+
el.appendChild(text);
|
|
42
|
+
const dismiss = () => {
|
|
43
|
+
if (!el.parentNode) return;
|
|
44
|
+
el.classList.add('leaving');
|
|
45
|
+
setTimeout(() => { el.parentNode && el.parentNode.removeChild(el); }, 200);
|
|
46
|
+
};
|
|
47
|
+
if (actionLabel && onAction) {
|
|
48
|
+
el.classList.add('has-action');
|
|
49
|
+
const btn = document.createElement('button');
|
|
50
|
+
btn.type = 'button';
|
|
51
|
+
btn.className = 'ds-ep-toast-action';
|
|
52
|
+
btn.textContent = actionLabel;
|
|
53
|
+
btn.onclick = () => onAction(dismiss);
|
|
54
|
+
el.appendChild(btn);
|
|
55
|
+
}
|
|
56
|
+
host.appendChild(el);
|
|
57
|
+
if (duration > 0) setTimeout(dismiss, duration);
|
|
58
|
+
return dismiss;
|
|
59
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Tree — TreeView container + TreeItem row, implementing the WAI-ARIA tree
|
|
2
|
+
// keyboard model (Up/Down between visible rows, Right expands/enters, Left
|
|
3
|
+
// collapses, Enter/Space activate, Home/End jump). Visuals via
|
|
4
|
+
// editor-primitives.css; no hex/rgba literals here.
|
|
5
|
+
|
|
6
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
7
|
+
import { Icon } from '../shell.js';
|
|
8
|
+
import { kids } from './shared.js';
|
|
9
|
+
const h = webjsx.createElement;
|
|
10
|
+
|
|
11
|
+
export function TreeView({ children } = {}) {
|
|
12
|
+
return h('div', { class: 'ds-ep-tree', role: 'tree' }, ...kids(children));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function TreeItem({ label, glyph, tag, depth = 0, selected = false, expanded = false, onSelect, onToggle, children, hasChildren } = {}) {
|
|
16
|
+
// Support legacy 'hasChildren' prop for future; infer from children param
|
|
17
|
+
const hasKids = hasChildren != null ? hasChildren : (children != null);
|
|
18
|
+
// Tree keyboard model (WAI-ARIA): Up/Down move between visible rows, Right expands/enters,
|
|
19
|
+
// Left collapses/moves to parent, Enter/Space activate, Home/End jump to first/last visible.
|
|
20
|
+
const onRowKeyDown = (e) => {
|
|
21
|
+
const row = e.currentTarget;
|
|
22
|
+
const tree = row.closest('[role="tree"]');
|
|
23
|
+
if (!tree) return;
|
|
24
|
+
const rows = Array.from(tree.querySelectorAll('.ds-ep-tree-row'));
|
|
25
|
+
const idx = rows.indexOf(row);
|
|
26
|
+
if (idx < 0) return;
|
|
27
|
+
const move = (i) => {
|
|
28
|
+
const r = rows[Math.max(0, Math.min(rows.length - 1, i))];
|
|
29
|
+
if (r) r.focus();
|
|
30
|
+
};
|
|
31
|
+
switch (e.key) {
|
|
32
|
+
case 'ArrowDown': e.preventDefault(); move(idx + 1); break;
|
|
33
|
+
case 'ArrowUp': e.preventDefault(); move(idx - 1); break;
|
|
34
|
+
case 'Home': e.preventDefault(); move(0); break;
|
|
35
|
+
case 'End': e.preventDefault(); move(rows.length - 1); break;
|
|
36
|
+
case 'ArrowRight':
|
|
37
|
+
if (hasKids && !expanded && onToggle) { e.preventDefault(); onToggle(); }
|
|
38
|
+
else if (hasKids && expanded) { e.preventDefault(); move(idx + 1); }
|
|
39
|
+
break;
|
|
40
|
+
case 'ArrowLeft':
|
|
41
|
+
if (hasKids && expanded && onToggle) { e.preventDefault(); onToggle(); }
|
|
42
|
+
break;
|
|
43
|
+
case 'Enter':
|
|
44
|
+
case ' ':
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
if (onSelect) onSelect();
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return h('div', {
|
|
51
|
+
class: 'ds-ep-tree-item' + (selected ? ' selected' : ''),
|
|
52
|
+
role: 'treeitem',
|
|
53
|
+
'aria-selected': selected ? 'true' : 'false',
|
|
54
|
+
'aria-expanded': hasKids ? String(!!expanded) : null,
|
|
55
|
+
'aria-level': depth + 1
|
|
56
|
+
},
|
|
57
|
+
h('div', {
|
|
58
|
+
class: 'ds-ep-tree-row',
|
|
59
|
+
style: 'padding-left:calc(' + depth + ' * var(--tree-indent,12px) + var(--tree-base-indent,6px))',
|
|
60
|
+
tabindex: selected ? '0' : '-1',
|
|
61
|
+
onclick: () => onSelect && onSelect(),
|
|
62
|
+
onkeydown: onRowKeyDown
|
|
63
|
+
},
|
|
64
|
+
h('span', {
|
|
65
|
+
class: 'ds-ep-tree-twist' + (expanded ? ' open' : ''),
|
|
66
|
+
'aria-hidden': 'true',
|
|
67
|
+
onclick: (e) => { e.stopPropagation(); if (hasKids && onToggle) onToggle(); }
|
|
68
|
+
}, hasKids ? Icon('chevron-right') : ''),
|
|
69
|
+
glyph != null ? h('span', { class: 'ds-ep-tree-glyph', 'aria-hidden': 'true' }, glyph) : null,
|
|
70
|
+
h('span', { class: 'ds-ep-tree-label' }, label),
|
|
71
|
+
tag != null ? h('span', { class: 'ds-ep-tree-tag' }, tag) : null
|
|
72
|
+
),
|
|
73
|
+
hasKids && expanded ? h('div', { class: 'ds-ep-tree-children', role: 'group' }, ...kids(children)) : null
|
|
74
|
+
);
|
|
75
|
+
}
|