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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.345",
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/
@@ -241,7 +241,44 @@ export function flashComposerNote(composerEl, text) {
241
241
  showNext();
242
242
  }
243
243
 
244
- export function ChatComposer({ value, onInput, onSend, onEmoji, onCancel, busy, placeholder = 'message…', disabled, disabledReason, label, context, onPasteFiles, onDropFiles }) {
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
- if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && e.keyCode !== 229) { e.preventDefault(); send(); }
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 {