anentrypoint-design 0.0.194 → 0.0.196

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.194",
3
+ "version": "0.0.196",
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",
@@ -129,7 +129,11 @@ export function AgentChat(props = {}) {
129
129
  // accumulated source and swaps the entire bubble innerHTML on every frame
130
130
  // (O(n^2) over the turn, with a visible reflow). Downgrade md -> text
131
131
  // mid-stream; the settled turn below renders real markdown once.
132
- if (isStreaming && part.kind === 'md') parts.push({ kind: 'text', text: part.text });
132
+ // Carry a `mdShell` flag so the streaming-text bubble uses the same
133
+ // container shape (.chat-md padding/spacing) the settled markdown will
134
+ // use — only the inner content swaps on settle, so the bubble box does
135
+ // not reflow/jump when the turn finishes and renders real markdown.
136
+ if (isStreaming && part.kind === 'md') parts.push({ kind: 'text', text: part.text, mdShell: true });
133
137
  else parts.push(part);
134
138
  }
135
139
  }
@@ -19,6 +19,21 @@ export function fmtBytes(n) {
19
19
  return (n / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
20
20
  }
21
21
 
22
+ // Reject dangerous URL schemes (javascript:, data:, vbscript:, file:) so an
23
+ // inline markdown link or an image src built from untrusted text can't smuggle a
24
+ // script-executing or data-exfiltrating URL past the inline renderer (which does
25
+ // NOT pass through DOMPurify the way the full md path does). http(s), mailto,
26
+ // protocol-relative, root/relative, and anchor links are allowed.
27
+ export function safeUrl(url) {
28
+ const s = String(url == null ? '' : url).trim();
29
+ if (!s) return null;
30
+ // Allow relative / anchor / protocol-relative without a scheme.
31
+ if (/^(\/|\.|#|\?)/.test(s) || s.startsWith('//')) return s;
32
+ const scheme = (s.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/) || [])[1];
33
+ if (!scheme) return s; // schemeless relative
34
+ return /^(https?|mailto|tel)$/i.test(scheme) ? s : null;
35
+ }
36
+
22
37
  // Inline-only markdown subset; safe for chat bubbles.
23
38
  export function renderInline(text) {
24
39
  if (text == null) return [];
@@ -31,7 +46,13 @@ export function renderInline(text) {
31
46
  if (m[2] != null) push(h('strong', { key: 's' + i }, m[2]));
32
47
  else if (m[3] != null) push(h('em', { key: 's' + i }, m[3]));
33
48
  else if (m[4] != null) push(h('code', { key: 's' + i, class: 'chat-tick' }, m[4]));
34
- else if (m[5] != null) push(h('a', { key: 's' + i, href: m[6], target: '_blank', rel: 'noopener' }, m[5]));
49
+ else if (m[5] != null) {
50
+ const safe = safeUrl(m[6]);
51
+ // A link with a rejected (unsafe) scheme degrades to its plain label
52
+ // text rather than a clickable, scheme-smuggling anchor.
53
+ if (safe) push(h('a', { key: 's' + i, href: safe, target: '_blank', rel: 'noopener noreferrer' }, m[5]));
54
+ else push(h('span', { key: 's' + i }, m[5]));
55
+ }
35
56
  last = m.index + m[0].length; i += 1;
36
57
  }
37
58
  if (last < text.length) push(h('span', { key: 's' + i + 'a' }, text.slice(last)));
@@ -133,7 +154,13 @@ function ToolCallNode(p) {
133
154
  h('pre', { class: 'chat-tool-pre' }, h('code', {}, argsText))),
134
155
  resultText ? h('div', { class: 'chat-tool-section' },
135
156
  h('div', { class: 'chat-tool-section-label' }, p.error ? 'error' : 'result'),
136
- h('pre', { class: 'chat-tool-pre' + (p.error ? ' is-error' : '') }, h('code', {}, resultText))) : null
157
+ h('pre', { class: 'chat-tool-pre' + (p.error ? ' is-error' : '') }, h('code', {}, resultText)))
158
+ // A finished tool with no output would otherwise render no result
159
+ // section, reading identically to a still-running tool. Show an
160
+ // explicit placeholder so "done, empty" is distinguishable.
161
+ : (status === 'done' ? h('div', { class: 'chat-tool-section' },
162
+ h('div', { class: 'chat-tool-section-label' }, 'result'),
163
+ h('pre', { class: 'chat-tool-pre chat-tool-empty' }, h('code', {}, '(no output)'))) : null)
137
164
  )
138
165
  );
139
166
  }
@@ -146,16 +173,24 @@ function ThinkingNode(p) {
146
173
  }
147
174
 
148
175
  const PART_RENDERERS = {
149
- text: (p) => h('div', { class: 'chat-bubble' }, ...renderInline(p.text || '')),
176
+ text: (p) => h('div', { class: 'chat-bubble' + (p.mdShell ? ' chat-md' : '') }, ...renderInline(p.text || '')),
150
177
  md: (p) => MdNode(p),
151
178
  code: (p) => CodeNode(p),
152
179
  tool: (p) => ToolCallNode(p),
153
180
  tool_call: (p) => ToolCallNode(p),
154
181
  tool_result: (p) => ToolCallNode({ ...p, name: p.name || 'tool_result', result: p.text != null ? p.text : p.result }),
155
182
  thinking: (p) => ThinkingNode(p),
156
- image: (p) => h('a', { class: 'chat-image', href: p.href || p.src, target: '_blank', rel: 'noopener', 'aria-label': p.alt || `embedded image: ${p.src}` },
157
- h('img', { src: p.src, alt: p.alt || `embedded image from ${p.src}`, loading: 'lazy' }),
158
- p.caption ? h('span', { class: 'cap' }, p.caption) : null),
183
+ image: (p) => {
184
+ // Guard both the wrapping link and the img src against unsafe schemes
185
+ // (e.g. a data:text/html src) so an embedded-image part from untrusted
186
+ // markdown can't smuggle an active payload.
187
+ const imgSrc = safeUrl(p.src);
188
+ const linkHref = safeUrl(p.href || p.src);
189
+ if (!imgSrc) return h('span', { class: 'chat-image-blocked' }, p.alt || 'image blocked (unsafe url)');
190
+ return h('a', { class: 'chat-image', href: linkHref || imgSrc, target: '_blank', rel: 'noopener noreferrer', 'aria-label': p.alt || `embedded image: ${imgSrc}` },
191
+ h('img', { src: imgSrc, alt: p.alt || `embedded image from ${imgSrc}`, loading: 'lazy' }),
192
+ p.caption ? h('span', { class: 'cap' }, p.caption) : null);
193
+ },
159
194
  pdf: (p) => h('div', { class: 'chat-pdf' },
160
195
  h('div', { class: 'chat-pdf-head' },
161
196
  h('span', { class: 'glyph', 'aria-hidden': 'true' }, Icon('file-pdf', { size: 18 })),
@@ -171,7 +206,7 @@ const PART_RENDERERS = {
171
206
  h('span', { class: 'size' }, [p.kindLabel || (p.name || '').split('.').pop().toUpperCase(), p.size != null ? fmtBytes(p.size) : null].filter(Boolean).join(' · '))
172
207
  ),
173
208
  h('span', { class: 'go', 'aria-hidden': 'true' }, Icon('arrow-down'))),
174
- link: (p) => h('a', { class: 'chat-link', href: p.href, target: '_blank', rel: 'noopener', 'aria-label': `link: ${p.title || p.href}` },
209
+ link: (p) => h('a', { class: 'chat-link', href: safeUrl(p.href) || '#', target: '_blank', rel: 'noopener noreferrer', 'aria-label': `link: ${p.title || p.href}` },
175
210
  p.thumb ? h('img', { class: 'thumb', src: p.thumb, alt: `preview for ${p.title || p.href}` }) : null,
176
211
  h('span', { class: 'meta' },
177
212
  h('span', { class: 'host' }, p.host || (() => { try { return new URL(p.href).host; } catch { return ''; } })()),
@@ -293,7 +328,6 @@ export function Chat({ title = 'chat', sub, messages = [], composer, header, sug
293
328
  const msgCount = messages.length;
294
329
  return h('div', { class: 'chat' },
295
330
  header || h('div', { class: 'chat-head', role: 'banner' },
296
- h('span', { class: 'dot', 'aria-hidden': 'true' }),
297
331
  h('h2', { class: 'ds-chat-title' }, title),
298
332
  sub ? h('span', { class: 'sub', 'aria-label': `subtitle: ${sub}` }, ' · ' + sub) : null,
299
333
  h('span', { class: 'spread' }),
@@ -253,8 +253,11 @@ export function ProjectView({ project = {}, copied, onCopy } = {}) {
253
253
  ].filter(Boolean).flat();
254
254
  }
255
255
 
256
- export function PageHeader({ title, lede, eyebrow, right }) {
257
- return h('section', { class: 'ds-section' },
256
+ export function PageHeader({ title, lede, eyebrow, right, compact }) {
257
+ // `compact` drops the large leading/trailing section margins so a PageHeader
258
+ // used as a page's first element top-aligns cleanly without the consumer
259
+ // having to !important-override the .ds-section margin.
260
+ return h('section', { class: 'ds-section' + (compact ? ' ds-section-compact' : '') },
258
261
  eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
259
262
  title != null ? h('h1', {}, title) : null,
260
263
  lede != null ? h('p', { class: 'lede' }, lede) : null,