@kitnai/chat 0.6.0 → 0.8.0

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.
Files changed (211) hide show
  1. package/README.md +9 -9
  2. package/dist/custom-elements.json +1676 -881
  3. package/dist/kitn-chat.es.js +36 -36
  4. package/dist/llms/llms-full.txt +316 -155
  5. package/dist/llms/llms.txt +18 -18
  6. package/dist/schemas/card-envelope.schema.json +14 -0
  7. package/dist/schemas/card-event.schema.json +12 -0
  8. package/dist/schemas/confirm.schema.json +65 -0
  9. package/dist/schemas/embed.schema.json +65 -0
  10. package/dist/schemas/form.result.schema.json +7 -0
  11. package/dist/schemas/form.schema.json +33 -0
  12. package/dist/schemas/link.schema.json +56 -0
  13. package/dist/schemas/task-list.result.schema.json +16 -0
  14. package/dist/schemas/task-list.schema.json +78 -0
  15. package/dist/theme.tokens.css +65 -65
  16. package/dist/tsx-B8rCNbgL.js +1 -0
  17. package/dist/typescript-RycA9KXf.js +1 -0
  18. package/frameworks/react/index.tsx +382 -193
  19. package/frameworks/react/runtime.tsx +2 -2
  20. package/llms-full.txt +316 -155
  21. package/llms.txt +18 -18
  22. package/package.json +5 -2
  23. package/src/components/artifact.stories.tsx +138 -0
  24. package/src/components/artifact.tsx +581 -0
  25. package/src/components/attachments.stories.tsx +7 -8
  26. package/src/components/attachments.tsx +2 -2
  27. package/src/components/card.tsx +110 -0
  28. package/src/components/chain-of-thought.stories.tsx +7 -8
  29. package/src/components/chat-container.stories.tsx +7 -8
  30. package/src/components/chat-container.tsx +4 -0
  31. package/src/components/checkpoint.stories.tsx +7 -8
  32. package/src/components/code-block.stories.tsx +8 -9
  33. package/src/components/component-meta.json +3411 -0
  34. package/src/components/confirm-card.stories.tsx +74 -0
  35. package/src/components/confirm-card.tsx +299 -0
  36. package/src/components/context.stories.tsx +7 -8
  37. package/src/components/conversation-item.stories.tsx +7 -8
  38. package/src/components/conversation-item.tsx +2 -2
  39. package/src/components/conversation-list.stories.tsx +7 -8
  40. package/src/components/conversation-list.tsx +1 -1
  41. package/src/components/embed.tsx +196 -0
  42. package/src/components/empty.stories.tsx +8 -9
  43. package/src/components/feedback-bar.stories.tsx +7 -8
  44. package/src/components/file-tree.stories.tsx +73 -0
  45. package/src/components/file-tree.tsx +383 -0
  46. package/src/components/file-upload.stories.tsx +7 -8
  47. package/src/components/form-widgets.tsx +461 -0
  48. package/src/components/form.tsx +796 -0
  49. package/src/components/image.stories.tsx +7 -8
  50. package/src/components/link-card.tsx +194 -0
  51. package/src/components/loader.stories.tsx +7 -8
  52. package/src/components/markdown.stories.tsx +7 -8
  53. package/src/components/message-narrow.stories.tsx +12 -13
  54. package/src/components/message-skills.stories.tsx +16 -17
  55. package/src/components/message.stories.tsx +17 -18
  56. package/src/components/model-switcher.stories.tsx +7 -8
  57. package/src/components/prompt-input.stories.tsx +8 -9
  58. package/src/components/prompt-suggestion.stories.tsx +7 -8
  59. package/src/components/prompt-suggestion.tsx +3 -3
  60. package/src/components/reasoning.stories.tsx +7 -8
  61. package/src/components/scroll-button.stories.tsx +7 -8
  62. package/src/components/slash-command.stories.tsx +8 -9
  63. package/src/components/slash-command.tsx +2 -2
  64. package/src/components/source.stories.tsx +7 -8
  65. package/src/components/source.tsx +1 -1
  66. package/src/components/task-list-card.stories.tsx +78 -0
  67. package/src/components/task-list-card.tsx +388 -0
  68. package/src/components/text-shimmer.stories.tsx +7 -8
  69. package/src/components/thinking-bar.stories.tsx +7 -8
  70. package/src/components/tool.stories.tsx +7 -8
  71. package/src/components/tool.tsx +2 -2
  72. package/src/components/voice-input.stories.tsx +7 -8
  73. package/src/elements/artifact.stories.tsx +291 -0
  74. package/src/elements/artifact.tsx +72 -0
  75. package/src/elements/{kitn-attachments.stories.tsx → attachments.stories.tsx} +11 -11
  76. package/src/elements/attachments.tsx +4 -4
  77. package/src/elements/card.stories.tsx +118 -0
  78. package/src/elements/card.tsx +40 -0
  79. package/src/elements/catalog.stories.tsx +491 -0
  80. package/src/elements/{kitn-chain-of-thought.stories.tsx → chain-of-thought.stories.tsx} +13 -13
  81. package/src/elements/chain-of-thought.tsx +3 -3
  82. package/src/elements/{kitn-chat-scope-picker.stories.tsx → chat-scope-picker.stories.tsx} +10 -10
  83. package/src/elements/chat-scope-picker.tsx +4 -4
  84. package/src/elements/{kitn-chat-workspace.stories.tsx → chat-workspace.stories.tsx} +71 -29
  85. package/src/elements/chat-workspace.tsx +29 -3
  86. package/src/elements/{kitn-chat.stories.tsx → chat.stories.tsx} +61 -16
  87. package/src/elements/chat.tsx +23 -2
  88. package/src/elements/{kitn-checkpoint.stories.tsx → checkpoint.stories.tsx} +11 -11
  89. package/src/elements/checkpoint.tsx +4 -4
  90. package/src/elements/{kitn-code-block.stories.tsx → code-block.stories.tsx} +10 -10
  91. package/src/elements/code-block.tsx +3 -3
  92. package/src/elements/compiled.css +1 -1
  93. package/src/elements/composed-shell.stories.tsx +316 -0
  94. package/src/elements/confirm-card.stories.tsx +186 -0
  95. package/src/elements/confirm-card.tsx +45 -0
  96. package/src/elements/{kitn-context-meter.stories.tsx → context-meter.stories.tsx} +10 -10
  97. package/src/elements/context-meter.tsx +3 -3
  98. package/src/elements/{kitn-conversation-list.stories.tsx → conversation-list.stories.tsx} +35 -22
  99. package/src/elements/conversation-list.tsx +11 -2
  100. package/src/elements/css.ts +1 -1
  101. package/src/elements/define.tsx +10 -10
  102. package/src/elements/element-meta.json +2649 -0
  103. package/src/elements/element-types.d.ts +251 -125
  104. package/src/elements/embed.stories.tsx +197 -0
  105. package/src/elements/embed.tsx +35 -0
  106. package/src/elements/{kitn-empty.stories.tsx → empty.stories.tsx} +12 -12
  107. package/src/elements/empty.tsx +3 -3
  108. package/src/elements/{kitn-feedback-bar.stories.tsx → feedback-bar.stories.tsx} +11 -11
  109. package/src/elements/feedback-bar.tsx +4 -4
  110. package/src/elements/file-tree.stories.tsx +133 -0
  111. package/src/elements/file-tree.tsx +52 -0
  112. package/src/elements/{kitn-file-upload.stories.tsx → file-upload.stories.tsx} +12 -12
  113. package/src/elements/file-upload.tsx +4 -4
  114. package/src/elements/form.stories.tsx +204 -0
  115. package/src/elements/form.tsx +37 -0
  116. package/src/elements/{kitn-image.stories.tsx → image.stories.tsx} +10 -10
  117. package/src/elements/image.tsx +3 -3
  118. package/src/elements/link-card.stories.tsx +193 -0
  119. package/src/elements/link-card.tsx +34 -0
  120. package/src/elements/{kitn-loader.stories.tsx → loader.stories.tsx} +11 -11
  121. package/src/elements/loader.tsx +3 -3
  122. package/src/elements/{kitn-markdown.stories.tsx → markdown.stories.tsx} +10 -10
  123. package/src/elements/markdown.tsx +3 -3
  124. package/src/elements/{kitn-message-skills.stories.tsx → message-skills.stories.tsx} +10 -10
  125. package/src/elements/message-skills.tsx +3 -3
  126. package/src/elements/{kitn-message.stories.tsx → message.stories.tsx} +12 -12
  127. package/src/elements/message.tsx +5 -5
  128. package/src/elements/{kitn-model-switcher.stories.tsx → model-switcher.stories.tsx} +10 -10
  129. package/src/elements/model-switcher.tsx +5 -5
  130. package/src/elements/{kitn-prompt-input.stories.tsx → prompt-input.stories.tsx} +41 -19
  131. package/src/elements/prompt-input.tsx +5 -5
  132. package/src/elements/{kitn-prompt-suggestions.stories.tsx → prompt-suggestions.stories.tsx} +13 -13
  133. package/src/elements/prompt-suggestions.tsx +4 -4
  134. package/src/elements/{kitn-reasoning.stories.tsx → reasoning.stories.tsx} +10 -10
  135. package/src/elements/reasoning.tsx +4 -4
  136. package/src/elements/register.ts +11 -1
  137. package/src/elements/resizable.stories.tsx +200 -0
  138. package/src/elements/resizable.tsx +264 -0
  139. package/src/elements/{kitn-response-stream.stories.tsx → response-stream.stories.tsx} +10 -10
  140. package/src/elements/response-stream.tsx +4 -4
  141. package/src/elements/{kitn-source-list.stories.tsx → source-list.stories.tsx} +11 -11
  142. package/src/elements/{kitn-source.stories.tsx → source.stories.tsx} +12 -12
  143. package/src/elements/source.tsx +5 -5
  144. package/src/elements/styles.css +140 -1
  145. package/src/elements/task-list-card.stories.tsx +194 -0
  146. package/src/elements/task-list-card.tsx +40 -0
  147. package/src/elements/{kitn-text-shimmer.stories.tsx → text-shimmer.stories.tsx} +10 -10
  148. package/src/elements/text-shimmer.tsx +3 -3
  149. package/src/elements/{kitn-thinking-bar.stories.tsx → thinking-bar.stories.tsx} +11 -11
  150. package/src/elements/thinking-bar.tsx +5 -5
  151. package/src/elements/{kitn-tool.stories.tsx → tool.stories.tsx} +10 -10
  152. package/src/elements/tool.tsx +3 -3
  153. package/src/elements/{kitn-voice-input.stories.tsx → voice-input.stories.tsx} +10 -10
  154. package/src/elements/voice-input.tsx +4 -4
  155. package/src/index.ts +94 -2
  156. package/src/primitives/card-contract.ts +60 -0
  157. package/src/primitives/card-host.tsx +35 -0
  158. package/src/primitives/card-routing.ts +79 -0
  159. package/src/primitives/card-schemas/card-envelope.schema.json +14 -0
  160. package/src/primitives/card-schemas/card-event.schema.json +12 -0
  161. package/src/primitives/card-schemas/confirm.schema.json +65 -0
  162. package/src/primitives/card-schemas/embed.schema.json +65 -0
  163. package/src/primitives/card-schemas/form.result.schema.json +7 -0
  164. package/src/primitives/card-schemas/form.schema.json +33 -0
  165. package/src/primitives/card-schemas/link.schema.json +56 -0
  166. package/src/primitives/card-schemas/task-list.result.schema.json +16 -0
  167. package/src/primitives/card-schemas/task-list.schema.json +78 -0
  168. package/src/primitives/card-validate.ts +95 -0
  169. package/src/primitives/embed-providers.ts +254 -0
  170. package/src/primitives/highlighter.ts +4 -0
  171. package/src/primitives/link-preview.ts +87 -0
  172. package/src/primitives/pdf-preview.ts +121 -0
  173. package/src/stories/chat-panel-layout.stories.tsx +2 -1
  174. package/src/stories/chat-scene.tsx +22 -21
  175. package/src/stories/checkpoint-restore.stories.tsx +10 -10
  176. package/src/stories/conversation-with-reasoning.stories.tsx +4 -4
  177. package/src/stories/conversation-with-sources.stories.tsx +7 -7
  178. package/src/stories/docs/Accessibility.mdx +2 -2
  179. package/src/stories/docs/ForAIAgents.mdx +3 -3
  180. package/src/stories/docs/GettingStarted.mdx +2 -2
  181. package/src/stories/docs/Installation.mdx +2 -2
  182. package/src/stories/docs/Integrations.mdx +29 -29
  183. package/src/stories/docs/Introduction.mdx +3 -3
  184. package/src/stories/docs/Theming.mdx +2 -2
  185. package/src/stories/docs/element-controls.ts +60 -0
  186. package/src/stories/docs/theme-editor/theme-editor.tsx +1 -0
  187. package/src/stories/examples/ChoosingComponents.mdx +94 -0
  188. package/src/stories/examples/sample-data.ts +79 -0
  189. package/src/stories/message-actions.stories.tsx +13 -13
  190. package/src/stories/pattern-centered-conversation.stories.tsx +3 -3
  191. package/src/stories/pattern-docked-widget.stories.tsx +1 -1
  192. package/src/stories/pattern-empty-state.stories.tsx +3 -3
  193. package/src/stories/prompt-input-variants.stories.tsx +13 -13
  194. package/src/stories/streaming-response.stories.tsx +3 -3
  195. package/src/stories/typography.stories.tsx +4 -4
  196. package/src/ui/avatar.stories.tsx +7 -8
  197. package/src/ui/badge.stories.tsx +7 -8
  198. package/src/ui/button.stories.tsx +8 -9
  199. package/src/ui/button.tsx +1 -0
  200. package/src/ui/collapsible.stories.tsx +6 -7
  201. package/src/ui/dropdown.stories.tsx +6 -7
  202. package/src/ui/hover-card.stories.tsx +6 -7
  203. package/src/ui/resizable.stories.tsx +74 -9
  204. package/src/ui/resizable.tsx +351 -71
  205. package/src/ui/scroll-area.stories.tsx +6 -7
  206. package/src/ui/scroll-area.tsx +3 -1
  207. package/src/ui/separator.stories.tsx +7 -8
  208. package/src/ui/skeleton.stories.tsx +7 -8
  209. package/src/ui/textarea.stories.tsx +6 -7
  210. package/src/ui/tooltip.stories.tsx +8 -9
  211. package/theme.css +65 -65
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/task-list.schema.json",
4
+ "title": "TaskListCardData",
5
+ "description": "Data payload for a `task-list` card (CardEnvelope.data when type='task-list').",
6
+ "type": "object",
7
+ "required": ["tasks"],
8
+ "properties": {
9
+ "mode": {
10
+ "type": "string",
11
+ "enum": ["select"],
12
+ "default": "select",
13
+ "description": "v1 supports only 'select' (toggle + confirm). 'progress' (live AG-UI status) is a future enum value; restricting it now keeps the wire forward-compatible.",
14
+ "x-kc-mode": true
15
+ },
16
+ "heading": {
17
+ "type": "string",
18
+ "description": "Optional in-body heading; distinct from CardEnvelope.title."
19
+ },
20
+ "tasks": {
21
+ "type": "array",
22
+ "minItems": 1,
23
+ "description": "The selectable rows, rendered in order.",
24
+ "items": {
25
+ "type": "object",
26
+ "required": ["id", "label"],
27
+ "properties": {
28
+ "id": {
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "description": "Stable id; the selected ids are returned in the result. Unique within `tasks`.",
32
+ "x-kc-unique": true
33
+ },
34
+ "label": { "type": "string", "minLength": 1, "description": "Row label." },
35
+ "description": { "type": "string", "description": "Optional secondary line under the label." },
36
+ "checked": {
37
+ "type": "boolean",
38
+ "default": false,
39
+ "description": "Initial checked state of the row."
40
+ },
41
+ "disabled": {
42
+ "type": "boolean",
43
+ "default": false,
44
+ "description": "Row is shown but not toggleable (and excluded from select-all)."
45
+ }
46
+ }
47
+ }
48
+ },
49
+ "selectAll": {
50
+ "type": "boolean",
51
+ "default": false,
52
+ "description": "Render a master select-all checkbox above the list.",
53
+ "x-kc-control": "select-all"
54
+ },
55
+ "confirmLabel": {
56
+ "type": "string",
57
+ "default": "Confirm",
58
+ "description": "Label for the confirm button."
59
+ },
60
+ "allowEmpty": {
61
+ "type": "boolean",
62
+ "default": false,
63
+ "description": "If true, confirm is enabled with zero selected (emits { selected: [] }). If false, confirm is disabled until >=1 selected."
64
+ },
65
+ "min": {
66
+ "type": "integer",
67
+ "minimum": 0,
68
+ "description": "Optional minimum number that must be selected to confirm.",
69
+ "x-kc-select-min": true
70
+ },
71
+ "max": {
72
+ "type": "integer",
73
+ "minimum": 1,
74
+ "description": "Optional maximum selectable; further toggles are blocked once reached.",
75
+ "x-kc-select-max": true
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,95 @@
1
+ // src/primitives/card-validate.ts
2
+ // The single shared lean JSON-Schema validator the contract mandates. Covers the
3
+ // subset cards use; `x-*` keywords (incl. x-kc-*) are ignored. No ajv. Used at
4
+ // every boundary (incoming card data, outgoing payloads) by cards + both transports.
5
+
6
+ export interface JsonSchema {
7
+ type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null';
8
+ const?: unknown;
9
+ enum?: unknown[];
10
+ required?: string[];
11
+ properties?: Record<string, JsonSchema>;
12
+ items?: JsonSchema;
13
+ minimum?: number; maximum?: number;
14
+ exclusiveMinimum?: number; exclusiveMaximum?: number;
15
+ minLength?: number; maxLength?: number;
16
+ pattern?: string;
17
+ minItems?: number; maxItems?: number;
18
+ uniqueItems?: boolean;
19
+ // x-* keywords (e.g. x-kc-widget) are allowed and ignored.
20
+ [key: `x-${string}`]: unknown;
21
+ }
22
+
23
+ export interface ValidationResult {
24
+ valid: boolean;
25
+ errors: string[];
26
+ }
27
+
28
+ function typeOf(v: unknown): string {
29
+ if (v === null) return 'null';
30
+ if (Array.isArray(v)) return 'array';
31
+ return typeof v;
32
+ }
33
+
34
+ function matchesType(v: unknown, t: NonNullable<JsonSchema['type']>): boolean {
35
+ switch (t) {
36
+ case 'integer': return typeof v === 'number' && Number.isInteger(v);
37
+ case 'number': return typeof v === 'number' && Number.isFinite(v);
38
+ case 'array': return Array.isArray(v);
39
+ case 'null': return v === null;
40
+ case 'object': return typeOf(v) === 'object';
41
+ default: return typeof v === t;
42
+ }
43
+ }
44
+
45
+ function walk(schema: JsonSchema, value: unknown, path: string, errors: string[]): void {
46
+ const at = path || '(root)';
47
+ if (schema.type && !matchesType(value, schema.type)) {
48
+ errors.push(`${at}: expected ${schema.type}, got ${typeOf(value)}`);
49
+ return; // type wrong → downstream checks are meaningless
50
+ }
51
+ if ('const' in schema && JSON.stringify(value) !== JSON.stringify(schema.const)) {
52
+ errors.push(`${at}: must equal const ${JSON.stringify(schema.const)}`);
53
+ }
54
+ if (schema.enum && !schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))) {
55
+ errors.push(`${at}: must be one of ${JSON.stringify(schema.enum)}`);
56
+ }
57
+ if (typeof value === 'number') {
58
+ if (schema.minimum !== undefined && value < schema.minimum) errors.push(`${at}: < minimum ${schema.minimum}`);
59
+ if (schema.maximum !== undefined && value > schema.maximum) errors.push(`${at}: > maximum ${schema.maximum}`);
60
+ if (schema.exclusiveMinimum !== undefined && value <= schema.exclusiveMinimum) errors.push(`${at}: <= exclusiveMinimum`);
61
+ if (schema.exclusiveMaximum !== undefined && value >= schema.exclusiveMaximum) errors.push(`${at}: >= exclusiveMaximum`);
62
+ }
63
+ if (typeof value === 'string') {
64
+ if (schema.minLength !== undefined && value.length < schema.minLength) errors.push(`${at}: shorter than minLength ${schema.minLength}`);
65
+ if (schema.maxLength !== undefined && value.length > schema.maxLength) errors.push(`${at}: longer than maxLength ${schema.maxLength}`);
66
+ if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) errors.push(`${at}: does not match pattern`);
67
+ }
68
+ if (Array.isArray(value)) {
69
+ if (schema.minItems !== undefined && value.length < schema.minItems) errors.push(`${at}: fewer than minItems ${schema.minItems}`);
70
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) errors.push(`${at}: more than maxItems ${schema.maxItems}`);
71
+ if (schema.uniqueItems) {
72
+ const seen = new Set(value.map((v) => JSON.stringify(v)));
73
+ if (seen.size !== value.length) errors.push(`${at}: items not unique`);
74
+ }
75
+ if (schema.items) value.forEach((v, i) => walk(schema.items!, v, `${at}[${i}]`, errors));
76
+ }
77
+ if (typeOf(value) === 'object') {
78
+ const obj = value as Record<string, unknown>;
79
+ for (const key of schema.required ?? []) {
80
+ if (!(key in obj) || obj[key] === undefined) errors.push(`${at}.${key}: required`);
81
+ }
82
+ if (schema.properties) {
83
+ for (const [key, sub] of Object.entries(schema.properties)) {
84
+ if (key in obj && obj[key] !== undefined) walk(sub, obj[key], `${at}.${key}`, errors);
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ /** Validate `value` against the lean JSON-Schema subset. */
91
+ export function validateAgainstSchema(schema: JsonSchema, value: unknown): ValidationResult {
92
+ const errors: string[] = [];
93
+ walk(schema, value, '', errors);
94
+ return { valid: errors.length === 0, errors };
95
+ }
@@ -0,0 +1,254 @@
1
+ // src/primitives/embed-providers.ts
2
+ // Pure provider resolution for <kc-embed>: map an EmbedCardData → an embeddable
3
+ // player URL + poster + iframe sandbox/allow. Covers youtube (privacy-enhanced
4
+ // youtube-nocookie), vimeo (dnt=1), and generic (https-only, ORIGIN-ALLOWLISTED).
5
+ // No network, no DOM. See docs/superpowers/specs/2026-06-13-kc-link-embed-cards-design.md.
6
+ import type { CardEnvelope } from './card-contract';
7
+
8
+ /** Media provider for an embed card. */
9
+ export type EmbedProvider = 'youtube' | 'vimeo' | 'generic';
10
+
11
+ /** Lazy media-embed payload (YouTube / Vimeo / generic player URL). */
12
+ export interface EmbedCardData {
13
+ /** Media provider. 'generic' frames `url` directly. */
14
+ provider: EmbedProvider;
15
+ /** Provider video id (youtube/vimeo) when not parsing from `url`. */
16
+ id?: string;
17
+ /** Full media/watch/embed URL. */
18
+ url?: string;
19
+ /** Accessible iframe title + poster label. */
20
+ title?: string;
21
+ /** Thumbnail before play; derived for youtube/vimeo when omitted. */
22
+ poster?: string;
23
+ /** Start offset, seconds. */
24
+ start?: number;
25
+ /** Player aspect ratio. Default '16:9'. */
26
+ aspectRatio?: '16:9' | '4:3' | '1:1' | '9:16';
27
+ }
28
+
29
+ /** The full envelope an agent/server emits for an embed card. */
30
+ export type EmbedCardEnvelope = CardEnvelope<'embed', EmbedCardData>;
31
+
32
+ /** The `type` discriminator for embed cards. */
33
+ export const EMBED_CARD_TYPE = 'embed' as const;
34
+
35
+ export interface ResolvedEmbed {
36
+ /** The iframe src loaded on play (already including autoplay/start params). */
37
+ embedUrl: string;
38
+ /** Poster/thumbnail to show before play (derived when not supplied). */
39
+ posterUrl?: string;
40
+ /** sandbox attribute for the player iframe. */
41
+ sandbox: string;
42
+ /** allow attribute (fullscreen, encrypted-media, picture-in-picture, …). */
43
+ allow: string;
44
+ }
45
+
46
+ // A provider player NEEDS allow-scripts + allow-same-origin (its OWN origin) to run.
47
+ // That is safe here because the framed origin is a KNOWN, trusted video provider on a
48
+ // DIFFERENT origin than the host — same-origin policy still isolates the host page from
49
+ // the provider. allow-popups(-to-escape-sandbox) lets "watch on YouTube" work. The
50
+ // `allow` attribute (not sandbox) governs autoplay/fullscreen/encrypted-media.
51
+ // Contrast <kc-artifact> (allow-scripts allow-forms, NO allow-same-origin) which frames
52
+ // arbitrary consumer HTML and so trusts nothing.
53
+ const PLAYER_SANDBOX =
54
+ 'allow-scripts allow-same-origin allow-presentation allow-popups allow-popups-to-escape-sandbox';
55
+ const PLAYER_ALLOW =
56
+ 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen';
57
+
58
+ // --- generic origin allowlist (security decision) -------------------------
59
+ //
60
+ // A `generic` embed frames an ARBITRARY https URL. An agent-supplied generic URL is a
61
+ // supply-chain risk, so it is REJECTED unless the app has explicitly allowlisted its
62
+ // origin. The allowlist defaults to EMPTY: out of the box, `generic` embeds are blocked.
63
+ const allowedGenericOrigins = new Set<string>();
64
+
65
+ /**
66
+ * App opt-in: allow `generic` embeds whose origin is in this list. Origins are
67
+ * normalized via the URL parser (scheme + host + port). Only https origins are
68
+ * accepted (a non-https origin is ignored). Call once at app startup.
69
+ */
70
+ export function configureEmbedAllowlist(origins: string[]): void {
71
+ for (const o of origins) {
72
+ const origin = normalizeOrigin(o);
73
+ if (origin) allowedGenericOrigins.add(origin);
74
+ }
75
+ }
76
+
77
+ /** True when `url`'s origin has been allowlisted for `generic` embeds. */
78
+ export function isGenericOriginAllowed(url: string): boolean {
79
+ const origin = originOf(url);
80
+ return origin !== undefined && allowedGenericOrigins.has(origin);
81
+ }
82
+
83
+ /** Test-only: clear the generic allowlist so tests stay isolated. */
84
+ export function __resetEmbedAllowlistForTests(): void {
85
+ allowedGenericOrigins.clear();
86
+ }
87
+
88
+ /** Normalize an allowlist entry (a URL or bare origin) to a canonical https origin, or undefined. */
89
+ function normalizeOrigin(input: string): string | undefined {
90
+ const origin = originOf(input) ?? originOf(`https://${input}`);
91
+ if (!origin) return undefined;
92
+ return origin.startsWith('https://') ? origin : undefined;
93
+ }
94
+
95
+ /** The `scheme://host[:port]` origin of a URL, or undefined when unparseable. */
96
+ function originOf(url: string): string | undefined {
97
+ try {
98
+ return new URL(url).origin;
99
+ } catch {
100
+ return undefined;
101
+ }
102
+ }
103
+
104
+ // --- id parsing -----------------------------------------------------------
105
+
106
+ /** Extract a YouTube id from a watch / youtu.be / shorts / embed URL. */
107
+ export function parseYouTubeId(url: string): string | undefined {
108
+ let parsed: URL;
109
+ try {
110
+ parsed = new URL(url);
111
+ } catch {
112
+ return undefined;
113
+ }
114
+ const host = parsed.hostname.replace(/^www\./, '');
115
+ if (host === 'youtu.be') {
116
+ return cleanId(parsed.pathname.slice(1));
117
+ }
118
+ if (host === 'youtube.com' || host === 'm.youtube.com' || host === 'youtube-nocookie.com') {
119
+ const v = parsed.searchParams.get('v');
120
+ if (v) return cleanId(v);
121
+ // /shorts/<id>, /embed/<id>, /v/<id>
122
+ const m = parsed.pathname.match(/^\/(?:shorts|embed|v)\/([^/?#]+)/);
123
+ if (m) return cleanId(m[1]);
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ /** Extract a Vimeo id from a vimeo.com/<id> (or player.vimeo.com/video/<id>) URL. */
129
+ export function parseVimeoId(url: string): string | undefined {
130
+ let parsed: URL;
131
+ try {
132
+ parsed = new URL(url);
133
+ } catch {
134
+ return undefined;
135
+ }
136
+ const host = parsed.hostname.replace(/^www\./, '');
137
+ if (host !== 'vimeo.com' && host !== 'player.vimeo.com') return undefined;
138
+ const m = parsed.pathname.match(/(?:^|\/)(\d+)(?:\/|$)/);
139
+ return m ? m[1] : undefined;
140
+ }
141
+
142
+ /** A provider video id must be the [A-Za-z0-9_-] alphabet (matches the schema pattern). */
143
+ function cleanId(id: string): string | undefined {
144
+ return /^[A-Za-z0-9_-]+$/.test(id) ? id : undefined;
145
+ }
146
+
147
+ // --- resolution -----------------------------------------------------------
148
+
149
+ /**
150
+ * Resolve an EmbedCardData to an embeddable player URL + poster + sandbox/allow.
151
+ * Throws (with a human message) on a missing/invalid provider id, a non-https
152
+ * generic URL, or a generic origin not in the app allowlist — the card turns these
153
+ * into an inline error + an `error` event.
154
+ */
155
+ export function resolveEmbed(data: EmbedCardData): ResolvedEmbed {
156
+ const start = data.start ? `&start=${data.start}` : '';
157
+ switch (data.provider) {
158
+ case 'youtube': {
159
+ const id = data.id ?? (data.url ? parseYouTubeId(data.url) : undefined);
160
+ if (!id) throw new Error('youtube embed: missing or unparseable id/url');
161
+ return {
162
+ embedUrl: `https://www.youtube-nocookie.com/embed/${id}?autoplay=1&rel=0${start}`,
163
+ posterUrl: data.poster ?? `https://i.ytimg.com/vi/${id}/hqdefault.jpg`,
164
+ sandbox: PLAYER_SANDBOX,
165
+ allow: PLAYER_ALLOW,
166
+ };
167
+ }
168
+ case 'vimeo': {
169
+ const id = data.id ?? (data.url ? parseVimeoId(data.url) : undefined);
170
+ if (!id) throw new Error('vimeo embed: missing or unparseable id/url');
171
+ return {
172
+ embedUrl: `https://player.vimeo.com/video/${id}?autoplay=1&dnt=1${
173
+ data.start ? `#t=${data.start}s` : ''
174
+ }`,
175
+ // Vimeo has no static thumbnail URL; rely on a supplied poster (or placeholder).
176
+ posterUrl: data.poster,
177
+ sandbox: PLAYER_SANDBOX,
178
+ allow: PLAYER_ALLOW,
179
+ };
180
+ }
181
+ case 'generic': {
182
+ if (!data.url) throw new Error('generic embed: missing url');
183
+ assertHttpsEmbeddable(data.url);
184
+ if (!isGenericOriginAllowed(data.url)) {
185
+ throw new Error(
186
+ `generic embed: origin not allowlisted — call configureEmbedAllowlist([...]) to permit ${
187
+ originOf(data.url) ?? data.url
188
+ }`,
189
+ );
190
+ }
191
+ return { embedUrl: data.url, posterUrl: data.poster, sandbox: PLAYER_SANDBOX, allow: PLAYER_ALLOW };
192
+ }
193
+ }
194
+ }
195
+
196
+ /** Reject non-https or javascript:/data: player URLs (defense in depth). */
197
+ function assertHttpsEmbeddable(url: string): void {
198
+ let protocol: string;
199
+ try {
200
+ protocol = new URL(url).protocol;
201
+ } catch {
202
+ throw new Error(`generic embed: invalid url "${url}"`);
203
+ }
204
+ if (protocol !== 'https:') {
205
+ throw new Error(`generic embed: only https player URLs are allowed (got ${protocol})`);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * The canonical "watch on the provider" URL for the optional fallback affordance
211
+ * (emitted as `open`/target:'tab' when an embed is blocked). Returns `undefined`
212
+ * when there is nothing useful to link to.
213
+ */
214
+ export function watchUrl(data: EmbedCardData): string | undefined {
215
+ switch (data.provider) {
216
+ case 'youtube': {
217
+ const id = data.id ?? (data.url ? parseYouTubeId(data.url) : undefined);
218
+ return id ? `https://www.youtube.com/watch?v=${id}` : data.url;
219
+ }
220
+ case 'vimeo': {
221
+ const id = data.id ?? (data.url ? parseVimeoId(data.url) : undefined);
222
+ return id ? `https://vimeo.com/${id}` : data.url;
223
+ }
224
+ case 'generic':
225
+ return data.url;
226
+ }
227
+ }
228
+
229
+ /** Human-readable provider label for the fallback affordance ("Open on YouTube"). */
230
+ export function providerLabel(provider: EmbedProvider): string {
231
+ switch (provider) {
232
+ case 'youtube':
233
+ return 'YouTube';
234
+ case 'vimeo':
235
+ return 'Vimeo';
236
+ case 'generic':
237
+ return 'site';
238
+ }
239
+ }
240
+
241
+ /** CSS aspect-ratio value for a card's aspectRatio (default 16:9). */
242
+ export function aspectRatioValue(aspectRatio: EmbedCardData['aspectRatio']): string {
243
+ switch (aspectRatio) {
244
+ case '4:3':
245
+ return '4 / 3';
246
+ case '1:1':
247
+ return '1 / 1';
248
+ case '9:16':
249
+ return '9 / 16';
250
+ case '16:9':
251
+ default:
252
+ return '16 / 9';
253
+ }
254
+ }
@@ -20,6 +20,8 @@ type Loader = () => Promise<unknown>;
20
20
  const DEFAULT_LANGUAGES: Record<string, Loader> = {
21
21
  bash: () => import('@shikijs/langs/bash'),
22
22
  javascript: () => import('@shikijs/langs/javascript'),
23
+ typescript: () => import('@shikijs/langs/typescript'),
24
+ tsx: () => import('@shikijs/langs/tsx'),
23
25
  html: () => import('@shikijs/langs/html'),
24
26
  css: () => import('@shikijs/langs/css'),
25
27
  json: () => import('@shikijs/langs/json'),
@@ -32,6 +34,8 @@ const DEFAULT_THEMES: Record<string, Loader> = {
32
34
 
33
35
  const DEFAULT_ALIASES: Record<string, string> = {
34
36
  js: 'javascript',
37
+ ts: 'typescript',
38
+ jsx: 'tsx',
35
39
  sh: 'bash',
36
40
  shell: 'bash',
37
41
  };
@@ -0,0 +1,87 @@
1
+ // src/primitives/link-preview.ts
2
+ // The optional, app-supplied bare-URL → metadata hook for <kc-link-card>, plus the
3
+ // `link` card's data type. The card stays PURE: it renders from supplied metadata
4
+ // and never touches the network. CORS forbids reading cross-origin HTML in the
5
+ // browser, so there is intentionally NO built-in network implementation here — an
6
+ // app opts in with `configureLinkPreview({ fetchMetadata })` pointing at its OWN
7
+ // backend/proxy. See docs/superpowers/specs/2026-06-13-kc-link-embed-cards-design.md.
8
+ import type { CardEnvelope } from './card-contract';
9
+
10
+ /** Rich link / Open-Graph preview payload. The card renders from this; it never fetches. */
11
+ export interface LinkCardData {
12
+ /** Canonical destination; opened via the contract `open` verb. */
13
+ url: string;
14
+ /** og:title — falls back to the domain. */
15
+ title?: string;
16
+ /** og:description — clamped to 3 lines. */
17
+ description?: string;
18
+ /** og:image — degrades gracefully when missing/broken. */
19
+ image?: string;
20
+ /** Alt for the preview image (defaults to title / decorative). */
21
+ imageAlt?: string;
22
+ /** Site favicon. */
23
+ favicon?: string;
24
+ /** Display domain; derived from `url` when omitted. */
25
+ domain?: string;
26
+ /** og:site_name; preferred over `domain` when present. */
27
+ siteName?: string;
28
+ }
29
+
30
+ /** The full envelope an agent/server emits for a link card. */
31
+ export type LinkCardEnvelope = CardEnvelope<'link', LinkCardData>;
32
+
33
+ /** The `type` discriminator for link cards. */
34
+ export const LINK_CARD_TYPE = 'link' as const;
35
+
36
+ /** App-supplied resolver: a bare URL → (partial) OG metadata. Usually hits YOUR backend. */
37
+ export type LinkMetadataFetcher = (url: string) => Promise<Partial<LinkCardData>>;
38
+
39
+ let fetcher: LinkMetadataFetcher | undefined;
40
+
41
+ /**
42
+ * App opt-in: supply a function (usually hitting YOUR backend/proxy) that resolves
43
+ * a bare URL to OG metadata. CORS forbids reading cross-origin HTML in the browser,
44
+ * so there is intentionally NO default network implementation.
45
+ */
46
+ export function configureLinkPreview(opts: { fetchMetadata: LinkMetadataFetcher }): void {
47
+ fetcher = opts.fetchMetadata;
48
+ }
49
+
50
+ /** True when an app has registered a fetcher (the bare-URL path is available). */
51
+ export function hasLinkPreviewFetcher(): boolean {
52
+ return fetcher !== undefined;
53
+ }
54
+
55
+ /**
56
+ * Used by LinkCard ONLY when the envelope lacks metadata AND a fetcher is set.
57
+ * Returns the merged metadata or throws (card shows its fallback/error state).
58
+ */
59
+ export async function resolveLinkMetadata(url: string): Promise<Partial<LinkCardData>> {
60
+ if (!fetcher) throw new Error('No link-preview fetcher configured');
61
+ return fetcher(url);
62
+ }
63
+
64
+ /** Test-only: clear the configured fetcher so tests stay isolated. */
65
+ export function __resetLinkPreviewForTests(): void {
66
+ fetcher = undefined;
67
+ }
68
+
69
+ /** Derive a clean display domain from a URL (strips a leading `www.`). `undefined` if unparseable. */
70
+ export function deriveDomain(url: string): string | undefined {
71
+ try {
72
+ return new URL(url).hostname.replace(/^www\./, '');
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ }
77
+
78
+ const RENDERABLE_SCHEMES = ['http:', 'https:'];
79
+
80
+ /** True when `url` is a syntactically valid http(s) URL (the only renderable link schemes). */
81
+ export function isRenderableLink(url: string): boolean {
82
+ try {
83
+ return RENDERABLE_SCHEMES.includes(new URL(url).protocol);
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
@@ -0,0 +1,121 @@
1
+ // src/primitives/pdf-preview.ts
2
+ // On-demand PDF renderer built on pdf.js, loaded from a CDN only when a PDF is
3
+ // actually shown — so a component set that never renders a PDF ships and runs with
4
+ // ZERO pdf.js bytes (the ~482 KB gzip library is never fetched). When a PDF does
5
+ // appear, pdf.js is dynamically imported from the pinned CDN build and every page
6
+ // is rendered to a <canvas>. Hosts override the loader (self-host / CSP / pin) or
7
+ // disable inline rendering via configurePdfPreview(). Mirrors highlighter.ts.
8
+
9
+ /** Minimal shape of the pdf.js module + objects we rely on. */
10
+ export interface PdfViewportLike {
11
+ width: number;
12
+ height: number;
13
+ }
14
+ export interface PdfPageLike {
15
+ getViewport(opts: { scale: number }): PdfViewportLike;
16
+ render(opts: {
17
+ canvasContext: CanvasRenderingContext2D | null;
18
+ viewport: PdfViewportLike;
19
+ }): { promise: Promise<void> };
20
+ }
21
+ export interface PdfDocumentLike {
22
+ numPages: number;
23
+ getPage(n: number): Promise<PdfPageLike>;
24
+ }
25
+ export interface PdfjsLike {
26
+ getDocument(src: { url: string }): { promise: Promise<PdfDocumentLike> };
27
+ GlobalWorkerOptions: { workerSrc: string };
28
+ }
29
+
30
+ export interface PdfPreviewOptions {
31
+ /** Turn inline PDF rendering on/off globally. When false, always show the card. */
32
+ enabled?: boolean;
33
+ /** Override the pdf.js module loader (self-host / CSP / version pin). */
34
+ load?: () => Promise<PdfjsLike>;
35
+ /** Worker URL. Default = the matching jsDelivr worker for the pinned version. */
36
+ workerSrc?: string;
37
+ }
38
+
39
+ /** Pinned, exact (reproducible) — NOT a range. */
40
+ const PDFJS_VERSION = '6.0.227';
41
+ const CDN = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${PDFJS_VERSION}/build`;
42
+ const DEFAULT_LOAD = (): Promise<PdfjsLike> =>
43
+ // Template literal + @vite-ignore keeps this a runtime fetch (never bundled).
44
+ import(/* @vite-ignore */ `${CDN}/pdf.min.mjs`) as Promise<PdfjsLike>;
45
+ const DEFAULT_WORKER_SRC = `${CDN}/pdf.worker.min.mjs`;
46
+
47
+ let enabled = true;
48
+ let loader: () => Promise<PdfjsLike> = DEFAULT_LOAD;
49
+ let workerSrc = DEFAULT_WORKER_SRC;
50
+
51
+ export function configurePdfPreview(options: PdfPreviewOptions): void {
52
+ if (options.enabled !== undefined) enabled = options.enabled;
53
+ if (options.workerSrc !== undefined) workerSrc = options.workerSrc;
54
+ if (options.load !== undefined) {
55
+ loader = options.load;
56
+ pdfjsPromise = null;
57
+ }
58
+ }
59
+
60
+ export function isPdfPreviewEnabled(): boolean {
61
+ return enabled;
62
+ }
63
+
64
+ export function __resetPdfPreviewForTests(): void {
65
+ enabled = true;
66
+ loader = DEFAULT_LOAD;
67
+ workerSrc = DEFAULT_WORKER_SRC;
68
+ pdfjsPromise = null;
69
+ }
70
+
71
+ let pdfjsPromise: Promise<PdfjsLike> | null = null;
72
+
73
+ /** Load pdf.js once (singleton); set the worker src. Re-loads if the loader changed. */
74
+ function loadPdfjs(): Promise<PdfjsLike> {
75
+ if (!pdfjsPromise) {
76
+ const active = loader;
77
+ pdfjsPromise = (async () => {
78
+ const mod = await active();
79
+ const pdfjs = ((mod as unknown as { default?: PdfjsLike }).default ?? mod) as PdfjsLike;
80
+ pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
81
+ return pdfjs;
82
+ })();
83
+ }
84
+ return pdfjsPromise;
85
+ }
86
+
87
+ /**
88
+ * Render EVERY page of the PDF at `url` into `container` as stacked <canvas>
89
+ * elements fit to `pxWidth` CSS pixels (rendered at devicePixelRatio for
90
+ * crispness). Clears `container` first. Resolves `{ pages }`. THROWS on
91
+ * load / CORS / parse failure — the caller catches and shows the fallback card.
92
+ */
93
+ export async function renderPdfInto(
94
+ url: string,
95
+ container: HTMLElement,
96
+ pxWidth: number,
97
+ ): Promise<{ pages: number }> {
98
+ const pdfjs = await loadPdfjs();
99
+ const doc = await pdfjs.getDocument({ url }).promise;
100
+ container.replaceChildren();
101
+ const dpr =
102
+ typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1;
103
+ for (let n = 1; n <= doc.numPages; n++) {
104
+ const page = await doc.getPage(n);
105
+ const base = page.getViewport({ scale: 1 });
106
+ const scale = base.width > 0 ? pxWidth / base.width : 1;
107
+ const viewport = page.getViewport({ scale: scale * dpr });
108
+ const canvas = document.createElement('canvas');
109
+ canvas.width = Math.max(1, Math.floor(viewport.width));
110
+ canvas.height = Math.max(1, Math.floor(viewport.height));
111
+ canvas.setAttribute('role', 'img');
112
+ canvas.setAttribute('aria-label', `Page ${n}`);
113
+ canvas.style.width = '100%';
114
+ canvas.style.height = 'auto';
115
+ canvas.style.display = 'block';
116
+ const ctx = canvas.getContext('2d');
117
+ container.appendChild(canvas);
118
+ await page.render({ canvasContext: ctx, viewport }).promise;
119
+ }
120
+ return { pages: doc.numPages };
121
+ }
@@ -31,7 +31,7 @@ const userMsg2 = 'And from what I understand, does that need to be another brows
31
31
 
32
32
  function CopyButton(props: { text: string }) {
33
33
  return (
34
- <button onClick={() => navigator.clipboard.writeText(props.text)}>
34
+ <button aria-label="Copy message" onClick={() => navigator.clipboard.writeText(props.text)}>
35
35
  <Copy size={14} />
36
36
  </button>
37
37
  );
@@ -130,6 +130,7 @@ export const ChatGPTStyle: Story = {
130
130
  <Button
131
131
  size="icon-sm"
132
132
  class="rounded-full"
133
+ aria-label="Send message"
133
134
  disabled={!input().trim()}
134
135
  >
135
136
  <ArrowUp class="size-4" />