anentrypoint-design 0.0.311 → 0.0.313

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.311",
3
+ "version": "0.0.313",
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",
@@ -161,7 +161,7 @@ export function AgentChat(props = {}) {
161
161
  avatar, composerContext,
162
162
  followups = [], onFollowupClick,
163
163
  installHint, exportActions = [],
164
- onPasteFiles, onDropFiles,
164
+ onPasteFiles, onDropFiles, onEmoji,
165
165
  shownMessages, onShowEarlier,
166
166
  } = props;
167
167
 
@@ -335,6 +335,7 @@ export function AgentChat(props = {}) {
335
335
  // navigates away from a live session.
336
336
  onPasteFiles,
337
337
  onDropFiles,
338
+ onEmoji,
338
339
  });
339
340
 
340
341
  // Contextual follow-up chips below the last SETTLED assistant turn (claude.ai/
@@ -477,7 +477,7 @@ function flashComposerNote(composerEl, text) {
477
477
  note._dsNoteTimer = setTimeout(() => { note.remove(); }, 2600);
478
478
  }
479
479
 
480
- export function ChatComposer({ value, onInput, onSend, onAttach, onEmoji, onMenu, onCancel, busy, placeholder = 'message…', disabled, disabledReason, label, context, onPasteFiles, onDropFiles }) {
480
+ export function ChatComposer({ value, onInput, onSend, onEmoji, onCancel, busy, placeholder = 'message…', disabled, disabledReason, label, context, onPasteFiles, onDropFiles }) {
481
481
  // Keep a handle to the live textarea so send() reads the actual DOM value
482
482
  // (not the possibly-lagging `value` prop) and so we can sync the DOM value
483
483
  // only when it genuinely differs — re-applying `value` on every parent
@@ -644,9 +644,7 @@ export function ChatComposer({ value, onInput, onSend, onAttach, onEmoji, onMenu
644
644
  // (CSS) to save rows. Middot is kept product typography.
645
645
  h('div', { class: 'chat-composer-hint', 'aria-hidden': 'true' }, 'Enter to send · Shift+Enter for a new line'),
646
646
  h('div', { class: 'chat-composer-toolbar' },
647
- onAttach ? h('button', { type: 'button', class: 'composer-btn', onclick: (e) => { e.preventDefault(); onAttach(e); }, 'aria-label': 'attach file', title: 'attach file' }, Icon('paperclip')) : null,
648
647
  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,
649
- onMenu ? h('button', { type: 'button', class: 'composer-btn', onclick: (e) => { e.preventDefault(); onMenu(e); }, 'aria-label': 'composer menu', title: 'more options' }, Icon('more-horizontal')) : null,
650
648
  busy && onCancel
651
649
  ? h('button', { type: 'button', class: 'send cancel', onclick: (e) => { e.preventDefault(); onCancel(e); }, 'aria-label': 'stop generating', title: 'stop generating (Esc)' }, Icon('square'))
652
650
  : h('button', { type: 'button', class: 'send', disabled: disabled || !(value && value.trim()), onclick: send,
@@ -521,9 +521,9 @@ export function PageHeader({ title, lede, eyebrow, right, compact, dense, id })
521
521
  );
522
522
  }
523
523
 
524
- export function SearchInput({ value = '', placeholder = 'search…', onInput, onSubmit, name = 'q', key, label }) {
525
- return h('input', {
526
- key,
524
+ export function SearchInput({ value = '', placeholder = 'search…', onInput, onSubmit, name = 'q', key, label, resultCount }) {
525
+ const input = h('input', {
526
+ key: 'i',
527
527
  type: 'search',
528
528
  name,
529
529
  class: 'ds-search-input',
@@ -531,12 +531,26 @@ export function SearchInput({ value = '', placeholder = 'search…', onInput, on
531
531
  'aria-label': label || placeholder,
532
532
  value,
533
533
  oninput: onInput ? (e) => onInput(e.target.value, e) : null,
534
- // IME guard: the Enter that commits a CJK composition must not submit.
535
- onkeydown: onSubmit ? (e) => { if (e.key === 'Enter' && !e.isComposing && e.keyCode !== 229) onSubmit(e.target.value, e); } : null
534
+ onkeydown: (e) => {
535
+ // Escape clears the field in place (stays focused) rather than
536
+ // falling through to whatever ancestor Escape handler exists.
537
+ if (e.key === 'Escape' && value) { e.preventDefault(); e.stopPropagation(); if (onInput) onInput('', e); return; }
538
+ // IME guard: the Enter that commits a CJK composition must not submit.
539
+ if (onSubmit && e.key === 'Enter' && !e.isComposing && e.keyCode !== 229) onSubmit(e.target.value, e);
540
+ }
536
541
  });
537
- }
538
-
539
- export function TextField({ label, value = '', type = 'text', placeholder = '', onInput, onChange, name, key, hint, multiline, rows = 4, maxLength, min, max, error, title, 'aria-label': ariaLabel, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy }) {
542
+ if (resultCount == null) return input;
543
+ // Visually-hidden live region announces the result count as it changes,
544
+ // without changing SearchInput's return shape for callers that don't pass it.
545
+ return h('span', { key, class: 'ds-search-input-wrap' },
546
+ input,
547
+ h('span', { key: 'cnt', class: 'sr-only', role: 'status', 'aria-live': 'polite' }, resultCount));
548
+ }
549
+
550
+ export function TextField({ label, value = '', type = 'text', placeholder = '', onInput, onChange, name, key, hint, multiline, rows = 4, maxLength, min, max, error, title, size = 'md', 'aria-label': ariaLabel, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy }) {
551
+ // size: 'sm' | 'md' | 'lg' — md is the base .ds-field control; sm/lg add a
552
+ // wrapper modifier that snaps the control height/padding/font to --ctl-*.
553
+ const sizeCls = size === 'sm' ? ' ds-field--sm' : (size === 'lg' ? ' ds-field--lg' : '');
540
554
  const errorId = error != null ? ((key ? key : 'tf') + '-err') : null;
541
555
  const describedBy = ariaDescribedBy || errorId || null;
542
556
  const input = multiline
@@ -562,7 +576,7 @@ export function TextField({ label, value = '', type = 'text', placeholder = '',
562
576
  oninput: onInput ? (e) => onInput(e.target.value, e) : null,
563
577
  onchange: onChange ? (e) => onChange(e.target.value, e) : null
564
578
  });
565
- return h('label', { key, class: 'ds-field' },
579
+ return h('label', { key, class: 'ds-field' + sizeCls },
566
580
  ...[
567
581
  label != null ? h('span', { key: 'l', class: 'ds-field-label' }, label) : null,
568
582
  input,
@@ -573,7 +587,8 @@ export function TextField({ label, value = '', type = 'text', placeholder = '',
573
587
  );
574
588
  }
575
589
 
576
- export function Select({ label, value = '', options = [], onChange, name, key, placeholder, hint, title, 'aria-label': ariaLabel }) {
590
+ export function Select({ label, value = '', options = [], onChange, name, key, placeholder, hint, title, size = 'md', 'aria-label': ariaLabel }) {
591
+ const sizeCls = size === 'sm' ? ' ds-field--sm' : (size === 'lg' ? ' ds-field--lg' : '');
577
592
  const opts = [];
578
593
  if (placeholder != null) opts.push(h('option', { key: '_ph', value: '', disabled: true, selected: value === '' || value == null }, placeholder));
579
594
  for (const o of options) {
@@ -588,8 +603,9 @@ export function Select({ label, value = '', options = [], onChange, name, key, p
588
603
  title,
589
604
  onchange: onChange ? (e) => onChange(e.target.value, e) : null
590
605
  }, ...opts);
591
- if (label == null && hint == null) return select;
592
- return h('label', { key, class: 'ds-field' },
606
+ if (label == null && hint == null && size === 'md') return select;
607
+ if (label == null && hint == null) return h('label', { key, class: 'ds-field' + sizeCls }, select);
608
+ return h('label', { key, class: 'ds-field' + sizeCls },
593
609
  label != null ? h('span', { key: 'l', class: 'ds-field-label' }, label) : null,
594
610
  select,
595
611
  hint != null ? h('span', { key: 'h', class: 'ds-field-hint' }, hint) : null
@@ -53,7 +53,30 @@ export function Tabs({ items = [], active, onChange, children, 'aria-label': ari
53
53
  if (btn) btn.focus();
54
54
  });
55
55
  };
56
- return h('div', { class: 'ds-ep-tabs' },
56
+ // Position the sliding underline from the active tab's geometry. The ref is
57
+ // on the OUTER .ds-ep-tabs (position:relative) — the indicator is its child,
58
+ // NOT the flex head's (an abspos child of a horizontal flex row mis-sizes to
59
+ // 0 in Chromium). Runs on every render (ref fires after applyDiff) so a tab
60
+ // change re-measures; the CSS transition animates the move. left/width come
61
+ // from the active tab; top sits the bar on the head's bottom edge.
62
+ const positionSlider = (root) => {
63
+ if (!root) return;
64
+ const head = root.querySelector('.ds-ep-tabs-head');
65
+ const active = root.querySelector('.ds-ep-tab.active');
66
+ const ind = root.querySelector('.ds-ep-tab-indicator');
67
+ if (!head || !active || !ind) return;
68
+ // Size via left+right insets, NOT width: an abspos element's inline
69
+ // `width` computes to 0 in some flex-sibling layouts (Chromium), but
70
+ // left+right insets size reliably. left/right animate via the CSS
71
+ // transition on .ds-ep-tab-indicator.
72
+ const rootW = root.offsetWidth;
73
+ const l = active.offsetLeft, w = active.offsetWidth;
74
+ ind.style.left = l + 'px';
75
+ ind.style.right = Math.max(0, rootW - l - w) + 'px';
76
+ ind.style.top = (head.offsetTop + head.offsetHeight - 2) + 'px';
77
+ head.classList.add('has-slider');
78
+ };
79
+ return h('div', { class: 'ds-ep-tabs', ref: positionSlider },
57
80
  h('div', { class: 'ds-ep-tabs-head', role: 'tablist', 'aria-label': ariaLabel || 'tabs' },
58
81
  ...items.map((it, idx) => h('button', {
59
82
  key: it.id,
@@ -69,6 +92,9 @@ export function Tabs({ items = [], active, onChange, children, 'aria-label': ari
69
92
  onkeydown: (e) => onTabKeyDown(e, idx)
70
93
  }, it.label))
71
94
  ),
95
+ // The sliding underline — child of the outer column (see positionSlider).
96
+ // Keyed + decorative. Renders at 0-width until positioned (no-JS: hidden).
97
+ h('span', { key: '__ind', class: 'ds-ep-tab-indicator', 'aria-hidden': 'true' }),
72
98
  h('div', {
73
99
  class: 'ds-ep-tabs-body',
74
100
  role: 'tabpanel',
@@ -277,13 +277,25 @@ export function FileGrid({ files = [], onOpen, onAction, onUp, emptyText = 'No f
277
277
  // One toolbar baseline: filter + select-all + sort sit left, density is
278
278
  // pushed right by the spread. The filter used to be a separate right-aligned
279
279
  // strip ABOVE controls, giving two strips with conflicting alignment.
280
- const filterCtl = filter ? h('input', {
281
- key: 'filter',
282
- class: 'ds-file-filter-input', type: 'search',
283
- value: filter.value || '', placeholder: filter.placeholder || 'Filter files',
284
- 'aria-label': filter.placeholder || 'Filter files in this directory',
285
- oninput: (e) => filter.onInput && filter.onInput(e.target.value),
286
- }) : null;
280
+ const filterCtl = filter ? h('span', { key: 'filterwrap', class: 'ds-file-filter-wrap' },
281
+ h('input', {
282
+ key: 'filter',
283
+ class: 'ds-file-filter-input', type: 'search',
284
+ value: filter.value || '', placeholder: filter.placeholder || 'Filter files',
285
+ 'aria-label': filter.placeholder || 'Filter files in this directory',
286
+ oninput: (e) => filter.onInput && filter.onInput(e.target.value),
287
+ onkeydown: (e) => {
288
+ if (e.key === 'Escape' && filter.value) { e.preventDefault(); e.stopPropagation(); filter.onInput && filter.onInput(''); }
289
+ },
290
+ }),
291
+ // Announces the filtered count as the filter narrows the list, so a
292
+ // screen-reader user gets the same feedback a sighted user reads off
293
+ // the grid without having to re-scan it after every keystroke. `files`
294
+ // here is already the host's filter-applied set (see hasFilter above) -
295
+ // there is no separate pre-filter total available inside this component.
296
+ h('span', { key: 'filtercount', class: 'sr-only', role: 'status', 'aria-live': 'polite' },
297
+ hasFilter ? files.length + (files.length === 1 ? ' file' : ' files') + ' shown' : '')
298
+ ) : null;
287
299
  const leftKids = [filterCtl, selectAllCtl, head].filter(Boolean);
288
300
  const controlsKids = [
289
301
  ...leftKids,
@@ -35,7 +35,12 @@ export function fmtDuration(ms) {
35
35
  export function ConversationList({ sessions = [], selected, groups, search, caption,
36
36
  onSelect, onNew, newLabel = 'New chat',
37
37
  emptyText = 'No conversations yet', loading = false, error = null,
38
- loadingText = 'Loading conversations…' } = {}) {
38
+ loadingText = 'Loading conversations…',
39
+ // hasMore/onLoadMore: the rail's host truncates the underlying
40
+ // session list at some limit (a 200+ conversation user would
41
+ // otherwise never reach older sessions) - mirrors the History
42
+ // tab's existing "load N older" EventList pattern.
43
+ hasMore = false, onLoadMore, loadMoreLabel = 'load more conversations' } = {}) {
39
44
  const rowFor = (s, i) => h('div', {
40
45
  // Stable key: prefer sid, else position - a missing/duplicate sid would make
41
46
  // key undefined and crash webjsx applyDiff ("reading 'key'" of undefined).
@@ -97,7 +102,14 @@ export function ConversationList({ sessions = [], selected, groups, search, capt
97
102
  } else {
98
103
  inner = sessions.map(rowFor);
99
104
  }
100
- const body = h('div', { key: 'body', class: 'ds-session-list', role: 'listbox', 'aria-label': caption || 'Conversations' }, ...inner);
105
+ // The load-more row sits INSIDE the scrollable list body (not the outer
106
+ // .ds-sessions shell) so it scrolls with the rows it extends, matching
107
+ // where a user's eye already is after scrolling to the bottom of the rail.
108
+ const loadMoreRow = (hasMore && onLoadMore && sessions.length)
109
+ ? h('button', { key: 'loadmore', type: 'button', class: 'ds-session-loadmore', onclick: onLoadMore }, loadMoreLabel)
110
+ : null;
111
+ const body = h('div', { key: 'body', class: 'ds-session-list', role: 'listbox', 'aria-label': caption || 'Conversations' },
112
+ ...inner, loadMoreRow);
101
113
 
102
114
  return h('div', { class: 'ds-sessions' },
103
115
  h('div', { key: 'head', class: 'ds-session-head' },
@@ -12,14 +12,22 @@ export function Brand({ name = '247420', leaf } = {}) {
12
12
  leaf || null);
13
13
  }
14
14
 
15
- export function Chip({ tone = '', children }) {
16
- return h('span', { class: 'chip' + (tone ? ' tone-' + tone : '') }, children);
15
+ export function Chip({ tone = '', size = 'md', tag = false, children }) {
16
+ // size: 'sm' | 'md' | 'lg'; tag=true -> rectangular sentence-case variant
17
+ // for dense data (drops the all-caps pill). Both are orthogonal to tone.
18
+ const sizeCls = size === 'sm' ? ' chip--sm' : (size === 'lg' ? ' chip--lg' : '');
19
+ return h('span', { class: 'chip' + sizeCls + (tag ? ' chip--tag' : '') + (tone ? ' tone-' + tone : '') }, children);
17
20
  }
18
21
 
19
- export function Btn({ href, variant = 'default', children, onClick, 'aria-label': ariaLabel, primary, ghost, danger, disabled, class: className, key }) {
22
+ export function Btn({ href, variant = 'default', size = 'md', children, onClick, 'aria-label': ariaLabel, primary, ghost, danger, disabled, class: className, key }) {
20
23
  // Support legacy primary/ghost props for backward compatibility, but prefer variant
21
24
  const resolvedVariant = variant !== 'default' ? variant : (primary ? 'primary' : (ghost ? 'ghost' : (danger ? 'danger' : 'default')));
25
+ // size: 'sm' | 'md' | 'lg' — md is the base .btn rule (no class); sm/lg add a
26
+ // modifier that snaps height/padding/font to the --ctl-* ladder. Unknown
27
+ // sizes fall back to md so a typo never drops the button's base styling.
28
+ const sizeCls = size === 'sm' ? ' btn-sm' : (size === 'lg' ? ' btn-lg' : '');
22
29
  const cls = (resolvedVariant === 'primary' ? 'btn-primary' : (resolvedVariant === 'ghost' ? 'btn-ghost' : (resolvedVariant === 'danger' ? 'btn-primary danger' : 'btn')))
30
+ + sizeCls
23
31
  + (disabled ? ' is-disabled' : '')
24
32
  + (className ? ' ' + className : '');
25
33
  const onclick = (e) => {
@@ -67,8 +75,10 @@ export function IconButton({ icon, onClick, title, size = 'base', variant = 'gho
67
75
  }, Glyph({ children: icon, size }));
68
76
  }
69
77
 
70
- export function Badge({ children, variant = 'default', tone = 'neutral' }) {
71
- return h('span', { class: 'ds-badge ds-badge-' + variant + ' tone-' + tone }, children);
78
+ export function Badge({ children, variant = 'default', tone = 'neutral', size = 'md' }) {
79
+ // size: 'sm' | 'md' | 'lg' — md is the base 18px badge.
80
+ const sizeCls = size === 'sm' ? ' ds-badge--sm' : (size === 'lg' ? ' ds-badge--lg' : '');
81
+ return h('span', { class: 'ds-badge ds-badge-' + variant + sizeCls + ' tone-' + tone }, children);
72
82
  }
73
83
 
74
84
  // Pill — plain non-interactive label chip for tag-like annotations (a phase