anentrypoint-design 0.0.345 → 0.0.347
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/chat.css +29 -0
- package/dist/247420.css +65 -0
- package/dist/247420.js +22 -22
- package/package.json +1 -1
- package/src/components/agent-chat.js +3 -0
- package/src/components/chat.js +83 -3
- package/src/components/files-modals.js +40 -0
- package/src/components/overlay-primitives.js +76 -0
- package/src/components.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.347",
|
|
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",
|
|
@@ -223,6 +223,7 @@ export function AgentChat(props = {}) {
|
|
|
223
223
|
installHint, exportActions = [],
|
|
224
224
|
onPasteFiles, onDropFiles, onEmoji,
|
|
225
225
|
shownMessages, onShowEarlier,
|
|
226
|
+
streamingSince, detectAttachment,
|
|
226
227
|
// Optional inline content viewer beside the thread (a docstudio-cue
|
|
227
228
|
// addition: its chat view keeps a live document/PDF preview open next to
|
|
228
229
|
// the conversation instead of forcing a separate tab/window). The host
|
|
@@ -411,6 +412,8 @@ export function AgentChat(props = {}) {
|
|
|
411
412
|
onPasteFiles,
|
|
412
413
|
onDropFiles,
|
|
413
414
|
onEmoji,
|
|
415
|
+
streamingSince,
|
|
416
|
+
detectAttachment,
|
|
414
417
|
});
|
|
415
418
|
|
|
416
419
|
// Contextual follow-up chips below the last SETTLED assistant turn (claude.ai/
|
package/src/components/chat.js
CHANGED
|
@@ -241,7 +241,44 @@ export function flashComposerNote(composerEl, text) {
|
|
|
241
241
|
showNext();
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
// Cached once per session: coarse-pointer (touch/no-hover) devices get a
|
|
245
|
+
// newline on Enter instead of send (mirrors the one-time-cache pattern
|
|
246
|
+
// editor-primitives.js uses for its own pointer/matchMedia checks).
|
|
247
|
+
let _coarsePointerCache = null;
|
|
248
|
+
function isCoarsePointer() {
|
|
249
|
+
if (_coarsePointerCache != null) return _coarsePointerCache;
|
|
250
|
+
_coarsePointerCache = !!(typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
|
|
251
|
+
return _coarsePointerCache;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// m:ss elapsed-time formatter for the streaming counter.
|
|
255
|
+
function fmtElapsedMs(ms) {
|
|
256
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
257
|
+
const m = Math.floor(total / 60);
|
|
258
|
+
const s = total % 60;
|
|
259
|
+
return m + ':' + String(s).padStart(2, '0');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// A small ticking `m:ss` counter, keyed off the streamingSince timestamp so a
|
|
263
|
+
// parent re-render does not reset the interval — the ref only (re)starts the
|
|
264
|
+
// interval when the timestamp actually changes, and clears it when streaming
|
|
265
|
+
// goes false or the node unmounts.
|
|
266
|
+
function ChatComposerElapsed({ streamingSince }) {
|
|
267
|
+
return h('span', {
|
|
268
|
+
class: 'chat-composer-elapsed', role: 'status', 'aria-live': 'off',
|
|
269
|
+
ref: (el) => {
|
|
270
|
+
if (!el) return;
|
|
271
|
+
if (el._dsElapsedTimer && el._dsElapsedSince === streamingSince) return; // already ticking for this timestamp
|
|
272
|
+
if (el._dsElapsedTimer) clearInterval(el._dsElapsedTimer);
|
|
273
|
+
el._dsElapsedSince = streamingSince;
|
|
274
|
+
const tick = () => { el.textContent = fmtElapsedMs(Date.now() - streamingSince); };
|
|
275
|
+
tick();
|
|
276
|
+
el._dsElapsedTimer = setInterval(tick, 1000);
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function ChatComposer({ value, onInput, onSend, onEmoji, onCancel, busy, placeholder = 'message…', disabled, disabledReason, label, context, onPasteFiles, onDropFiles, streamingSince, detectAttachment }) {
|
|
245
282
|
// Keep a handle to the live textarea so send() reads the actual DOM value
|
|
246
283
|
// (not the possibly-lagging `value` prop) and so we can sync the DOM value
|
|
247
284
|
// only when it genuinely differs — re-applying `value` on every parent
|
|
@@ -280,9 +317,46 @@ export function ChatComposer({ value, onInput, onSend, onEmoji, onCancel, busy,
|
|
|
280
317
|
}
|
|
281
318
|
};
|
|
282
319
|
let autoGrowScheduled = false;
|
|
320
|
+
// detectAttachment(text) -> {type,label,id}|null runs on every input change;
|
|
321
|
+
// the badge above the textarea shows/clears based on its result, and clears
|
|
322
|
+
// outright when the textarea empties (mirrors the dismissible-badge pattern
|
|
323
|
+
// via a DOM-owned dismissed flag so a re-render with the same detection
|
|
324
|
+
// doesn't resurrect a badge the user just dismissed).
|
|
325
|
+
const updateDetectedBadge = (composerEl, text) => {
|
|
326
|
+
if (!composerEl) return;
|
|
327
|
+
let badge = composerEl.querySelector('.chat-composer-detected-badge');
|
|
328
|
+
const detected = (text && detectAttachment) ? detectAttachment(text) : null;
|
|
329
|
+
if (!detected) {
|
|
330
|
+
if (badge) badge.remove();
|
|
331
|
+
composerEl._dsDetectedId = null;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (composerEl._dsDismissedId === detected.id) return; // user dismissed this exact detection
|
|
335
|
+
if (composerEl._dsDetectedId === detected.id && badge) return; // unchanged
|
|
336
|
+
composerEl._dsDetectedId = detected.id;
|
|
337
|
+
if (!badge) {
|
|
338
|
+
badge = document.createElement('div');
|
|
339
|
+
badge.className = 'chat-composer-detected-badge';
|
|
340
|
+
badge.setAttribute('role', 'status');
|
|
341
|
+
composerEl.insertBefore(badge, composerEl.firstChild);
|
|
342
|
+
}
|
|
343
|
+
badge.textContent = '';
|
|
344
|
+
const label = document.createElement('span');
|
|
345
|
+
label.className = 'chat-composer-detected-label';
|
|
346
|
+
label.textContent = detected.label;
|
|
347
|
+
badge.appendChild(label);
|
|
348
|
+
const dismiss = document.createElement('button');
|
|
349
|
+
dismiss.type = 'button';
|
|
350
|
+
dismiss.className = 'chat-composer-detected-dismiss';
|
|
351
|
+
dismiss.setAttribute('aria-label', 'dismiss ' + detected.label);
|
|
352
|
+
dismiss.textContent = 'x';
|
|
353
|
+
dismiss.onclick = (e) => { e.preventDefault(); composerEl._dsDismissedId = detected.id; badge.remove(); };
|
|
354
|
+
badge.appendChild(dismiss);
|
|
355
|
+
};
|
|
283
356
|
const autoGrow = (e) => {
|
|
284
357
|
const ta = e.target;
|
|
285
358
|
if (onInput) onInput(ta.value);
|
|
359
|
+
if (detectAttachment) updateDetectedBadge(ta.closest('.chat-composer'), ta.value);
|
|
286
360
|
// Debounce scrollHeight read with rAF to prevent sync reflow thrashing
|
|
287
361
|
if (!autoGrowScheduled) {
|
|
288
362
|
autoGrowScheduled = true;
|
|
@@ -306,6 +380,7 @@ export function ChatComposer({ value, onInput, onSend, onEmoji, onCancel, busy,
|
|
|
306
380
|
el.style.height = 'auto';
|
|
307
381
|
const cap = parseFloat(getComputedStyle(el).maxHeight) || 200;
|
|
308
382
|
el.style.height = Math.min(el.scrollHeight, cap) + 'px';
|
|
383
|
+
if (detectAttachment) updateDetectedBadge(el.closest('.chat-composer'), next);
|
|
309
384
|
};
|
|
310
385
|
// Optional context line shown above the textarea: agent / model / cwd at the
|
|
311
386
|
// point of typing (the way Claude-Desktop surfaces the active target inline).
|
|
@@ -418,13 +493,18 @@ export function ChatComposer({ value, onInput, onSend, onEmoji, onCancel, busy,
|
|
|
418
493
|
}
|
|
419
494
|
// IME guard: the Enter that commits a CJK composition must never
|
|
420
495
|
// send (isComposing; keyCode 229 covers older engines).
|
|
421
|
-
|
|
496
|
+
// Coarse-pointer (touch) devices get a newline on Enter instead of
|
|
497
|
+
// send — there is no keyboard shortcut discoverability benefit on
|
|
498
|
+
// touch, and Enter-to-send is a frequent accidental-send source on
|
|
499
|
+
// phones/tablets where "Tap Send to send" is the predictable model.
|
|
500
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && e.keyCode !== 229 && !isCoarsePointer()) { e.preventDefault(); send(); }
|
|
422
501
|
if (e.key === ';' && e.ctrlKey) { e.preventDefault(); onEmoji && onEmoji(e); }
|
|
423
502
|
} }),
|
|
424
503
|
// Enter-to-send affordance (Claude-Desktop style): a muted hint visible
|
|
425
504
|
// at rest so it's discoverable without focusing the composer first;
|
|
426
505
|
// hidden under 420px (CSS) to save rows. Middot is kept product typography.
|
|
427
|
-
h('div', { class: 'chat-composer-hint' }, 'Enter to send · Shift+Enter for a new line'),
|
|
506
|
+
h('div', { class: 'chat-composer-hint' }, isCoarsePointer() ? 'Tap Send to send' : 'Enter to send · Shift+Enter for a new line'),
|
|
507
|
+
(busy && streamingSince) ? ChatComposerElapsed({ streamingSince }) : null,
|
|
428
508
|
h('div', { class: 'chat-composer-toolbar' },
|
|
429
509
|
onEmoji ? h('button', { type: 'button', class: 'composer-btn', onclick: (e) => { e.preventDefault(); onEmoji(e); }, 'aria-label': 'emoji picker', title: 'emoji picker (Ctrl+;)' }, Icon('smile')) : null,
|
|
430
510
|
busy && onCancel
|
|
@@ -231,6 +231,46 @@ export function PromptDialog({ title = 'Enter a name', value = '', placeholder =
|
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// CountdownDialog — a modal with a role=status line ticking down from
|
|
235
|
+
// `seconds` to 0 once per second, auto-firing onExpire at zero. Composes on
|
|
236
|
+
// top of the same Modal() shell ConfirmDialog/PromptDialog use, so it
|
|
237
|
+
// inherits Backdrop's focus-trap + Escape/backdrop-dismiss handling for free
|
|
238
|
+
// rather than reimplementing dialog plumbing.
|
|
239
|
+
export function CountdownDialog({ title = 'Are you sure?', message, seconds = 10, onExpire, actions } = {}) {
|
|
240
|
+
const startSeconds = Math.max(0, Math.floor(seconds));
|
|
241
|
+
return Modal({
|
|
242
|
+
onClose: undefined, // no implicit dismiss path unless the caller supplies one via `actions`
|
|
243
|
+
kind: 'small',
|
|
244
|
+
head: title,
|
|
245
|
+
body: [
|
|
246
|
+
message || '',
|
|
247
|
+
h('p', {
|
|
248
|
+
class: 'ds-countdown-status', role: 'status', 'aria-live': 'polite',
|
|
249
|
+
ref: (el) => {
|
|
250
|
+
if (!el || el._dsCountdownTimer) return;
|
|
251
|
+
let remaining = startSeconds;
|
|
252
|
+
const render = () => { el.textContent = remaining + (remaining === 1 ? ' second' : ' seconds') + ' remaining'; };
|
|
253
|
+
render();
|
|
254
|
+
el._dsCountdownTimer = setInterval(() => {
|
|
255
|
+
remaining -= 1;
|
|
256
|
+
if (remaining <= 0) {
|
|
257
|
+
clearInterval(el._dsCountdownTimer);
|
|
258
|
+
el._dsCountdownTimer = null;
|
|
259
|
+
remaining = 0;
|
|
260
|
+
render();
|
|
261
|
+
if (onExpire) onExpire();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
render();
|
|
265
|
+
}, 1000);
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
modalError(null),
|
|
269
|
+
].filter(Boolean),
|
|
270
|
+
actions: actions || [],
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
234
274
|
export function FilePreviewMedia({ src, type = 'other', name } = {}) {
|
|
235
275
|
if (type === 'image') {
|
|
236
276
|
// Fit-to-pane (default) vs actual-size (1:1) toggle + a checkerboard so
|
|
@@ -239,6 +239,82 @@ export function Dropdown({ trigger, items = [], onSelect, placement = 'bottom-st
|
|
|
239
239
|
: h('button', { type: 'button', class: 'ds-dropdown-trigger', ref: refFn }, child || 'Menu');
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
// PermissionMenu — a role=menu of role=menuitemcheckbox rows, one per
|
|
243
|
+
// category, with roving tabindex + Arrow-up/down/Home/End navigation and
|
|
244
|
+
// Escape-closes-and-restores-focus, plus "Approve all"/"Revoke all" actions.
|
|
245
|
+
// Mirrors Dropdown's own open/close + outside-click wiring (a portaled menu
|
|
246
|
+
// element, a document-level mousedown listener, focus restored to the
|
|
247
|
+
// trigger on close) rather than reimplementing that plumbing.
|
|
248
|
+
export function PermissionMenu({ trigger, categories = [], approved = [], onToggle, onToggleAll, placement = 'bottom-start', ariaLabel = 'Permissions' } = {}) {
|
|
249
|
+
let triggerEl = null, open = false, menuEl = null, floating = null;
|
|
250
|
+
const isApproved = (id) => approved.indexOf(id) !== -1;
|
|
251
|
+
const liveItems = () => menuEl ? [...menuEl.querySelectorAll('[role="menuitemcheckbox"]')] : [];
|
|
252
|
+
const focusItem = (idx) => { const items = liveItems(); if (!items.length) return; items[((idx % items.length) + items.length) % items.length].focus(); };
|
|
253
|
+
const onDown = (e) => { if (menuEl && menuEl.contains(e.target)) return; if (triggerEl && triggerEl.contains(e.target)) return; close(false); };
|
|
254
|
+
const close = (restore = true) => {
|
|
255
|
+
if (!open) return; open = false;
|
|
256
|
+
if (floating) { floating.dispose(); floating = null; }
|
|
257
|
+
if (menuEl && menuEl.parentNode) menuEl.parentNode.removeChild(menuEl);
|
|
258
|
+
menuEl = null;
|
|
259
|
+
document.removeEventListener('mousedown', onDown, true);
|
|
260
|
+
if (triggerEl) triggerEl.setAttribute('aria-expanded', 'false');
|
|
261
|
+
if (restore && triggerEl) triggerEl.focus();
|
|
262
|
+
};
|
|
263
|
+
const toggle = (cat) => { if (onToggle) onToggle(cat.id, !isApproved(cat.id)); };
|
|
264
|
+
const onMenuKey = (e) => {
|
|
265
|
+
const items = liveItems(), idx = items.indexOf(document.activeElement);
|
|
266
|
+
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
267
|
+
else if (e.key === 'ArrowDown') { e.preventDefault(); focusItem(idx + 1); }
|
|
268
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); focusItem(idx - 1); }
|
|
269
|
+
else if (e.key === 'Home') { e.preventDefault(); focusItem(0); }
|
|
270
|
+
else if (e.key === 'End') { e.preventDefault(); focusItem(items.length - 1); }
|
|
271
|
+
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (idx >= 0) items[idx].click(); }
|
|
272
|
+
};
|
|
273
|
+
const renderMenu = () => {
|
|
274
|
+
const rows = categories.map((cat, i) => h('button', {
|
|
275
|
+
key: cat.id || i, type: 'button', role: 'menuitemcheckbox',
|
|
276
|
+
'aria-checked': isApproved(cat.id) ? 'true' : 'false',
|
|
277
|
+
class: 'ov-perm-item' + (isApproved(cat.id) ? ' is-approved' : ''),
|
|
278
|
+
tabindex: '-1',
|
|
279
|
+
onclick: () => toggle(cat),
|
|
280
|
+
}, h('span', { class: 'ov-perm-label' }, cat.label || cat.id)));
|
|
281
|
+
const actionsRow = h('div', { class: 'ov-perm-actions' },
|
|
282
|
+
h('button', { type: 'button', class: 'ov-perm-action', onclick: () => onToggleAll && onToggleAll(true) }, 'Approve all'),
|
|
283
|
+
h('button', { type: 'button', class: 'ov-perm-action', onclick: () => onToggleAll && onToggleAll(false) }, 'Revoke all'));
|
|
284
|
+
return h('div', { class: 'ov-perm-list' }, ...rows, actionsRow);
|
|
285
|
+
};
|
|
286
|
+
const openMenu = (focusFirst = true) => {
|
|
287
|
+
if (open || !triggerEl) return;
|
|
288
|
+
open = true;
|
|
289
|
+
menuEl = document.createElement('div');
|
|
290
|
+
menuEl.className = 'ds-popover ov-perm-menu';
|
|
291
|
+
menuEl.setAttribute('role', 'menu');
|
|
292
|
+
menuEl.setAttribute('aria-label', ariaLabel);
|
|
293
|
+
menuEl.tabIndex = -1;
|
|
294
|
+
webjsx.applyDiff(menuEl, renderMenu());
|
|
295
|
+
document.body.appendChild(menuEl);
|
|
296
|
+
menuEl.addEventListener('keydown', onMenuKey);
|
|
297
|
+
floating = useFloating(triggerEl, menuEl, { placement, offset: FLOAT_OFFSET_DROPDOWN });
|
|
298
|
+
document.addEventListener('mousedown', onDown, true);
|
|
299
|
+
triggerEl.setAttribute('aria-expanded', 'true');
|
|
300
|
+
if (focusFirst) queueMicrotask(() => focusItem(0));
|
|
301
|
+
};
|
|
302
|
+
const onTrigClick = () => { if (open) close(false); else openMenu(true); };
|
|
303
|
+
const onTrigKey = (e) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!open) openMenu(true); else focusItem(0); } };
|
|
304
|
+
const refFn = (el) => {
|
|
305
|
+
if (!el || el._dsPermMenu) return;
|
|
306
|
+
el._dsPermMenu = true; triggerEl = el;
|
|
307
|
+
el.addEventListener('click', onTrigClick);
|
|
308
|
+
el.addEventListener('keydown', onTrigKey);
|
|
309
|
+
el.setAttribute('aria-haspopup', 'menu');
|
|
310
|
+
el.setAttribute('aria-expanded', 'false');
|
|
311
|
+
};
|
|
312
|
+
const child = (typeof trigger === 'function') ? trigger() : trigger;
|
|
313
|
+
return (child && child.type)
|
|
314
|
+
? webjsx.createElement(child.type, { ...(child.props || {}), ref: refFn }, ...(child.children || []))
|
|
315
|
+
: h('button', { type: 'button', class: 'ov-perm-trigger', ref: refFn }, child || 'Permissions');
|
|
316
|
+
}
|
|
317
|
+
|
|
242
318
|
// Clamp a fixed-position box to the viewport given desired top-left coords.
|
|
243
319
|
function _clampToViewport(x, y, w, h, margin = CLAMP_MARGIN) {
|
|
244
320
|
const vw = (typeof window !== 'undefined' ? window.innerWidth : 1024);
|
package/src/components.js
CHANGED
|
@@ -40,7 +40,7 @@ export {
|
|
|
40
40
|
} from './components/files.js';
|
|
41
41
|
|
|
42
42
|
export {
|
|
43
|
-
ConfirmDialog, PromptDialog,
|
|
43
|
+
ConfirmDialog, PromptDialog, CountdownDialog,
|
|
44
44
|
FilePreviewMedia, FilePreviewCode, FilePreviewText, FileViewer, FilePreviewPane
|
|
45
45
|
} from './components/files-modals.js';
|
|
46
46
|
|
|
@@ -90,7 +90,7 @@ export {
|
|
|
90
90
|
export {
|
|
91
91
|
Tooltip, Popover, Dropdown, useLongPress, useFloating,
|
|
92
92
|
CommandPalette, EmojiPicker, BootOverlay, SettingsPopover,
|
|
93
|
-
AuthModal, VideoLightbox
|
|
93
|
+
AuthModal, VideoLightbox, PermissionMenu
|
|
94
94
|
} from './components/overlay-primitives.js';
|
|
95
95
|
|
|
96
96
|
export {
|