@urbicon-ui/blocks 6.34.0 → 6.36.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 (127) hide show
  1. package/README.md +11 -1
  2. package/dist/components/Calendar/CalendarHeader.svelte +32 -26
  3. package/dist/components/Calendar/CalendarMiniMonth.svelte +10 -10
  4. package/dist/components/Calendar/calendar.variants.js +19 -5
  5. package/dist/components/Chat/Chat/Chat.svelte +53 -0
  6. package/dist/components/Chat/Chat/Chat.svelte.d.ts +4 -0
  7. package/dist/components/Chat/Chat/chat.variants.d.ts +20 -0
  8. package/dist/components/Chat/Chat/chat.variants.js +15 -0
  9. package/dist/components/Chat/Chat/index.d.ts +47 -0
  10. package/dist/components/Chat/Chat/index.js +2 -0
  11. package/dist/components/Chat/ChatMessage/ChatMessage.svelte +342 -0
  12. package/dist/components/Chat/ChatMessage/ChatMessage.svelte.d.ts +4 -0
  13. package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +138 -0
  14. package/dist/components/Chat/ChatMessage/chat-message.variants.js +118 -0
  15. package/dist/components/Chat/ChatMessage/index.d.ts +102 -0
  16. package/dist/components/Chat/ChatMessage/index.js +2 -0
  17. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte +301 -0
  18. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte.d.ts +6 -0
  19. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.d.ts +23 -0
  20. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.js +32 -0
  21. package/dist/components/Chat/ChatMessageList/chat-scroll.d.ts +45 -0
  22. package/dist/components/Chat/ChatMessageList/chat-scroll.js +52 -0
  23. package/dist/components/Chat/ChatMessageList/index.d.ts +93 -0
  24. package/dist/components/Chat/ChatMessageList/index.js +3 -0
  25. package/dist/components/Chat/CitationChip/CitationChip.svelte +107 -0
  26. package/dist/components/Chat/CitationChip/CitationChip.svelte.d.ts +4 -0
  27. package/dist/components/Chat/CitationChip/citation-chip.variants.d.ts +40 -0
  28. package/dist/components/Chat/CitationChip/citation-chip.variants.js +42 -0
  29. package/dist/components/Chat/CitationChip/index.d.ts +69 -0
  30. package/dist/components/Chat/CitationChip/index.js +2 -0
  31. package/dist/components/Chat/CodeBlock/CodeBlock.svelte +120 -0
  32. package/dist/components/Chat/CodeBlock/CodeBlock.svelte.d.ts +4 -0
  33. package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +40 -0
  34. package/dist/components/Chat/CodeBlock/code-block.variants.js +51 -0
  35. package/dist/components/Chat/CodeBlock/index.d.ts +55 -0
  36. package/dist/components/Chat/CodeBlock/index.js +2 -0
  37. package/dist/components/Chat/PromptInput/PromptInput.svelte +432 -0
  38. package/dist/components/Chat/PromptInput/PromptInput.svelte.d.ts +4 -0
  39. package/dist/components/Chat/PromptInput/index.d.ts +165 -0
  40. package/dist/components/Chat/PromptInput/index.js +2 -0
  41. package/dist/components/Chat/PromptInput/prompt-input.variants.d.ts +90 -0
  42. package/dist/components/Chat/PromptInput/prompt-input.variants.js +102 -0
  43. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte +107 -0
  44. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte.d.ts +4 -0
  45. package/dist/components/Chat/ReasoningDisclosure/index.d.ts +85 -0
  46. package/dist/components/Chat/ReasoningDisclosure/index.js +2 -0
  47. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.d.ts +20 -0
  48. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.js +31 -0
  49. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte +99 -0
  50. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte.d.ts +9 -0
  51. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte +82 -0
  52. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte.d.ts +9 -0
  53. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte +118 -0
  54. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte.d.ts +4 -0
  55. package/dist/components/Chat/StreamingMarkdown/index.d.ts +86 -0
  56. package/dist/components/Chat/StreamingMarkdown/index.js +2 -0
  57. package/dist/components/Chat/StreamingMarkdown/md-context.d.ts +58 -0
  58. package/dist/components/Chat/StreamingMarkdown/md-context.js +1 -0
  59. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.d.ts +145 -0
  60. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.js +73 -0
  61. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +174 -0
  62. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte.d.ts +4 -0
  63. package/dist/components/Chat/ToolCallCard/index.d.ts +84 -0
  64. package/dist/components/Chat/ToolCallCard/index.js +2 -0
  65. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +38 -0
  66. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +38 -0
  67. package/dist/components/Chat/chat.types.d.ts +82 -0
  68. package/dist/components/Chat/chat.types.js +7 -0
  69. package/dist/components/Chat/index.d.ts +22 -0
  70. package/dist/components/Chat/index.js +14 -0
  71. package/dist/components/Chat/markdown/blocks.d.ts +54 -0
  72. package/dist/components/Chat/markdown/blocks.js +462 -0
  73. package/dist/components/Chat/markdown/inline.d.ts +31 -0
  74. package/dist/components/Chat/markdown/inline.js +446 -0
  75. package/dist/components/Chat/markdown/repair.d.ts +23 -0
  76. package/dist/components/Chat/markdown/repair.js +337 -0
  77. package/dist/components/Chat/markdown/streaming-harness.d.ts +23 -0
  78. package/dist/components/Chat/markdown/streaming-harness.js +62 -0
  79. package/dist/components/Chat/markdown/types.d.ts +139 -0
  80. package/dist/components/Chat/markdown/types.js +13 -0
  81. package/dist/components/Chat/markdown/url-policy.d.ts +33 -0
  82. package/dist/components/Chat/markdown/url-policy.js +109 -0
  83. package/dist/components/FileUpload/FileUpload.svelte +52 -125
  84. package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
  85. package/dist/components/FileUpload/index.d.ts +2 -28
  86. package/dist/components/Planner/PlannerHeader.svelte +16 -13
  87. package/dist/components/Planner/planner.variants.js +14 -3
  88. package/dist/components/index.d.ts +1 -0
  89. package/dist/components/index.js +1 -0
  90. package/dist/i18n/index.d.ts +268 -382
  91. package/dist/i18n/index.js +83 -9
  92. package/dist/internal/core/CoreIconButton.svelte +66 -0
  93. package/dist/internal/core/CoreIconButton.svelte.d.ts +13 -0
  94. package/dist/internal/core/CoreSpinner.svelte +49 -0
  95. package/dist/internal/core/CoreSpinner.svelte.d.ts +8 -0
  96. package/dist/internal/core/spinner-geometry.d.ts +6 -0
  97. package/dist/internal/core/spinner-geometry.js +6 -0
  98. package/dist/mint/registry.d.ts +20 -2
  99. package/dist/mint/registry.js +20 -2
  100. package/dist/primitives/Badge/Badge.svelte +5 -5
  101. package/dist/primitives/Badge/badge.variants.js +17 -1
  102. package/dist/primitives/Button/Button.svelte +4 -2
  103. package/dist/primitives/Collapsible/Collapsible.svelte +5 -0
  104. package/dist/primitives/Dialog/Dialog.svelte +7 -5
  105. package/dist/primitives/Dialog/dialog.variants.d.ts +7 -0
  106. package/dist/primitives/Dialog/dialog.variants.js +17 -0
  107. package/dist/primitives/Drawer/Drawer.svelte +7 -5
  108. package/dist/primitives/Drawer/drawer.variants.d.ts +8 -0
  109. package/dist/primitives/Drawer/drawer.variants.js +17 -0
  110. package/dist/primitives/Spinner/Spinner.svelte +3 -1
  111. package/dist/primitives/SplitPane/SplitPane.svelte +293 -0
  112. package/dist/primitives/SplitPane/SplitPane.svelte.d.ts +4 -0
  113. package/dist/primitives/SplitPane/index.d.ts +88 -0
  114. package/dist/primitives/SplitPane/index.js +2 -0
  115. package/dist/primitives/SplitPane/split-pane.utils.d.ts +73 -0
  116. package/dist/primitives/SplitPane/split-pane.utils.js +94 -0
  117. package/dist/primitives/SplitPane/split-pane.variants.d.ts +40 -0
  118. package/dist/primitives/SplitPane/split-pane.variants.js +56 -0
  119. package/dist/primitives/Toast/Toaster.svelte +17 -2
  120. package/dist/primitives/index.d.ts +2 -0
  121. package/dist/primitives/index.js +1 -0
  122. package/dist/utils/file-intake.d.ts +78 -0
  123. package/dist/utils/file-intake.js +144 -0
  124. package/dist/utils/index.d.ts +1 -0
  125. package/dist/utils/index.js +1 -0
  126. package/dist/utils/variants.js +44 -10
  127. package/package.json +4 -6
@@ -0,0 +1,93 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { ChatMessageProps } from '../ChatMessage/index.js';
4
+ import type { ChatMessageData } from '../chat.types.js';
5
+ import type { MarkdownUrlPolicy } from '../markdown/types.js';
6
+ import type { ChatMessageListSlots } from './chat-message-list.variants.js';
7
+ /** Render context handed to the per-message override snippet. */
8
+ export interface ChatMessageListItemContext {
9
+ message: ChatMessageData;
10
+ index: number;
11
+ isLast: boolean;
12
+ }
13
+ /**
14
+ * @description Scrollable conversation log with a stick-to-bottom engine: follows
15
+ * streaming content while the reader is at the bottom, breaks off on upward scroll,
16
+ * and offers a floating jump-back button with a new-message counter. Anchors the
17
+ * scroll position when older history is prepended. Announces generation start and
18
+ * the completed answer once to screen readers instead of every token. Renders
19
+ * ChatMessage per entry by default; override per message via the `message` snippet.
20
+ *
21
+ * @tag ai
22
+ * @tag display
23
+ * @related ChatMessage
24
+ * @related Chat
25
+ * @related PromptInput
26
+ * @stability experimental
27
+ *
28
+ * @example
29
+ * ```svelte
30
+ * <ChatMessageList
31
+ * {messages}
32
+ * onRegenerate={(m) => regenerate(m.id)}
33
+ * onStickChange={(stuck) => (following = stuck)}
34
+ * />
35
+ * ```
36
+ */
37
+ export interface ChatMessageListProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
38
+ /**
39
+ * The conversation, oldest first. The component never mutates it. Note:
40
+ * rest attributes (incl. a raw `onscroll`) land on the non-scrolling root —
41
+ * observe follow-state via `onStickChange` instead.
42
+ */
43
+ messages: ChatMessageData[];
44
+ /** Per-message renderer override. Default: `<ChatMessage>` wired with the props below. */
45
+ message?: Snippet<[ChatMessageListItemContext]>;
46
+ /** Empty-state content. Default: an EmptyState with `emptyTitle`/`emptyDescription`. */
47
+ empty?: Snippet;
48
+ /** URL policy handed to every default ChatMessage (links, citations, attachments). */
49
+ urlPolicy?: MarkdownUrlPolicy;
50
+ /** Part-renderer overrides handed to every default ChatMessage. */
51
+ partRenderers?: ChatMessageProps['partRenderers'];
52
+ /** Message layout handed to every default ChatMessage. */
53
+ layout?: ChatMessageProps['layout'];
54
+ /** Message density handed to every default ChatMessage. */
55
+ density?: ChatMessageProps['density'];
56
+ /** Regenerate handler — wired only to the last message when it is an assistant message. */
57
+ onRegenerate?: (message: ChatMessageData) => void;
58
+ /** Retry handler for messages in `error`/`aborted` status. */
59
+ onRetry?: (message: ChatMessageData) => void;
60
+ /** Fires when the list starts (`true`) or stops (`false`) following new content. */
61
+ onStickChange?: (stuck: boolean) => void;
62
+ /** Accessible name of the scrollable conversation region. */
63
+ listLabel?: string;
64
+ /** Accessible label of the jump button when there are no new messages. */
65
+ scrollToBottomLabel?: string;
66
+ /** Label suffix of the jump button while new messages are pending (prefixed with the count). */
67
+ newMessagesLabel?: string;
68
+ /** Screen-reader announcement when an assistant message starts streaming. */
69
+ generatingLabel?: string;
70
+ /** Screen-reader announcement when a stream ends in `error` without text. */
71
+ errorLabel?: string;
72
+ /** Screen-reader announcement when a stream ends in `aborted` without text. */
73
+ abortedLabel?: string;
74
+ /** Empty-state heading. */
75
+ emptyTitle?: string;
76
+ /** Empty-state description. */
77
+ emptyDescription?: string;
78
+ /** Extra classes merged onto the root element. */
79
+ class?: string;
80
+ /** Remove all default tv classes. */
81
+ unstyled?: boolean;
82
+ /** Per-slot class overrides. Slots: root | viewport | content | empty | newButton */
83
+ slotClasses?: Partial<Record<ChatMessageListSlots, string>>;
84
+ /**
85
+ * Apply a named preset registered via `<BlocksProvider presets={{ ChatMessageList: {...} }}>`.
86
+ * Prefer this over `class` overrides when the requested look falls outside the
87
+ * semantic intent palette.
88
+ */
89
+ preset?: string;
90
+ }
91
+ export { default as ChatMessageList } from './ChatMessageList.svelte';
92
+ export { type ChatMessageListVariants, chatMessageListVariants } from './chat-message-list.variants.js';
93
+ export { appendedCount, classifyTransition, distanceFromBottom, type ListIdSnapshot, type ListTransition, resolveScrollIntent, type ScrollSnapshot } from './chat-scroll.js';
@@ -0,0 +1,3 @@
1
+ export { default as ChatMessageList } from './ChatMessageList.svelte';
2
+ export { chatMessageListVariants } from './chat-message-list.variants.js';
3
+ export { appendedCount, classifyTransition, distanceFromBottom, resolveScrollIntent } from './chat-scroll.js';
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ import { Popover } from '../../../primitives';
3
+ import { getBlocksConfig, resolveSlotClasses } from '../../../provider';
4
+ import { resolveIcon } from '../../../icons';
5
+ import LinkIconDefault from '../../../icons/LinkIcon.svelte';
6
+ import { checkLinkUrl } from '../markdown/url-policy.js';
7
+ import { citationChipVariants } from './citation-chip.variants';
8
+ import type { CitationChipProps } from './index';
9
+
10
+ const LinkIcon = resolveIcon('link', LinkIconDefault);
11
+
12
+ let {
13
+ source,
14
+ index,
15
+ citationStyle = 'numeric',
16
+ urlPolicy,
17
+ openLabel = 'Open source',
18
+ label,
19
+ class: className,
20
+ unstyled: unstyledProp = false,
21
+ slotClasses: slotClassesProp = {},
22
+ preset,
23
+ ...restProps
24
+ }: CitationChipProps = $props();
25
+
26
+ const blocksConfig = getBlocksConfig();
27
+ const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
28
+
29
+ const variantProps = $derived({ citationStyle });
30
+ const styles = $derived(citationChipVariants(variantProps));
31
+ const slotClasses = $derived(
32
+ resolveSlotClasses(blocksConfig, 'CitationChip', preset, variantProps, slotClassesProp)
33
+ );
34
+
35
+ // Numeric markers show the 1-based ordinal, falling back to the raw id when
36
+ // no index is supplied (standalone reference lists). Label markers show the
37
+ // (truncated) title.
38
+ const displayLabel = $derived(
39
+ citationStyle === 'label' ? source.title : index != null ? String(index) : source.id
40
+ );
41
+
42
+ const ariaLabel = $derived(
43
+ label ?? (index != null ? `Source ${index}: ${source.title}` : `Source: ${source.title}`)
44
+ );
45
+
46
+ // Run the same strict URL policy as the streaming-markdown engine: a blocked
47
+ // or absent URL yields no link, only the title/snippet.
48
+ const linkCheck = $derived(
49
+ source.url !== undefined ? checkLinkUrl(source.url, urlPolicy) : ({ ok: false } as const)
50
+ );
51
+ const linkHref = $derived(linkCheck.ok ? linkCheck.href : undefined);
52
+ </script>
53
+
54
+ <!-- The aria-label on Popover lands on the panel via its restProps spread, so
55
+ the opened popover is not an unnamed dialog (axe: aria-dialog-name). -->
56
+ <Popover placement="bottom-start" aria-label={ariaLabel}>
57
+ {#snippet trigger()}
58
+ <button
59
+ type="button"
60
+ class={unstyled
61
+ ? [slotClasses?.trigger, className].filter(Boolean).join(' ')
62
+ : styles.trigger({ class: [slotClasses?.trigger, className] })}
63
+ aria-label={ariaLabel}
64
+ {...restProps}
65
+ >
66
+ <!-- truncate must sit on an inner block-ish span: on the inline-flex
67
+ trigger itself the ellipsis never renders and justify-center clips
68
+ the START of long labels. -->
69
+ <span class="min-w-0 truncate">{displayLabel}</span>
70
+ </button>
71
+ {/snippet}
72
+ <div
73
+ class={unstyled
74
+ ? (slotClasses?.popover ?? '')
75
+ : styles.popover({ class: slotClasses?.popover })}
76
+ >
77
+ <span
78
+ class={unstyled ? (slotClasses?.title ?? '') : styles.title({ class: slotClasses?.title })}
79
+ >
80
+ {source.title}
81
+ </span>
82
+ {#if source.snippet}
83
+ <p
84
+ class={unstyled
85
+ ? (slotClasses?.snippet ?? '')
86
+ : styles.snippet({ class: slotClasses?.snippet })}
87
+ >
88
+ {source.snippet}
89
+ </p>
90
+ {/if}
91
+ {#if linkHref}
92
+ <a
93
+ href={linkHref}
94
+ target="_blank"
95
+ rel="noopener noreferrer"
96
+ class={unstyled ? (slotClasses?.link ?? '') : styles.link({ class: slotClasses?.link })}
97
+ >
98
+ <LinkIcon
99
+ class={unstyled
100
+ ? (slotClasses?.linkIcon ?? '')
101
+ : styles.linkIcon({ class: slotClasses?.linkIcon })}
102
+ />
103
+ {openLabel}
104
+ </a>
105
+ {/if}
106
+ </div>
107
+ </Popover>
@@ -0,0 +1,4 @@
1
+ import type { CitationChipProps } from './index.js';
2
+ declare const CitationChip: import("svelte").Component<CitationChipProps, {}, "">;
3
+ type CitationChip = ReturnType<typeof CitationChip>;
4
+ export default CitationChip;
@@ -0,0 +1,40 @@
1
+ import { type SlotNames, type VariantProps } from '../../../utils/variants.js';
2
+ export declare const citationChipVariants: ((props?: {
3
+ citationStyle?: "numeric" | "label" | undefined;
4
+ } | undefined) => {
5
+ trigger: (props?: ({
6
+ citationStyle?: "numeric" | "label" | undefined;
7
+ } & {
8
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
9
+ }) | undefined) => string;
10
+ popover: (props?: ({
11
+ citationStyle?: "numeric" | "label" | undefined;
12
+ } & {
13
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
14
+ }) | undefined) => string;
15
+ title: (props?: ({
16
+ citationStyle?: "numeric" | "label" | undefined;
17
+ } & {
18
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
19
+ }) | undefined) => string;
20
+ snippet: (props?: ({
21
+ citationStyle?: "numeric" | "label" | undefined;
22
+ } & {
23
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
24
+ }) | undefined) => string;
25
+ link: (props?: ({
26
+ citationStyle?: "numeric" | "label" | undefined;
27
+ } & {
28
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
29
+ }) | undefined) => string;
30
+ linkIcon: (props?: ({
31
+ citationStyle?: "numeric" | "label" | undefined;
32
+ } & {
33
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
34
+ }) | undefined) => string;
35
+ }) & {
36
+ readonly config: import("../../../utils/variants.js").TVConfig;
37
+ };
38
+ export type CitationChipVariants = VariantProps<typeof citationChipVariants>;
39
+ /** Slot names derived from the tv() config — single source of truth for slotClasses. */
40
+ export type CitationChipSlots = SlotNames<typeof citationChipVariants>;
@@ -0,0 +1,42 @@
1
+ import { tv } from '../../../utils/variants.js';
2
+ export const citationChipVariants = tv({
3
+ slots: {
4
+ // Compact inline chip in the Badge soft-primary idiom. `align-baseline`
5
+ // + `leading-none` keep it seated on the text baseline so an inline
6
+ // citation marker never bumps the surrounding line height.
7
+ trigger: [
8
+ 'inline-flex items-center justify-center align-baseline',
9
+ 'bg-primary-subtle text-primary-emphasis',
10
+ 'text-xs font-medium leading-none tabular-nums',
11
+ 'rounded-full px-1.5 py-0.5 cursor-pointer',
12
+ 'transition-colors duration-[var(--blocks-duration-fast)] ease-out',
13
+ 'hover:bg-primary/15',
14
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
15
+ ],
16
+ // Content wrapper INSIDE the Popover panel — the panel itself already
17
+ // supplies surface / border / shadow / padding, so this only lays out.
18
+ popover: 'flex flex-col gap-1.5 max-w-xs',
19
+ title: 'font-medium text-text-primary text-sm',
20
+ snippet: 'text-text-secondary text-sm line-clamp-3',
21
+ link: [
22
+ 'inline-flex items-center gap-1 self-start',
23
+ 'text-sm text-primary rounded-modify',
24
+ 'hover:underline',
25
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
26
+ ],
27
+ linkIcon: 'h-3.5 w-3.5 shrink-0'
28
+ },
29
+ variants: {
30
+ citationStyle: {
31
+ // Numeric marker — a fixed-width numeric pill; no truncation.
32
+ numeric: {},
33
+ // Label marker — carries the source title, capped so a long title
34
+ // can't stretch the chip across the line. The ellipsis itself lives on
35
+ // an inner span (truncate doesn't render on the inline-flex trigger).
36
+ label: { trigger: 'max-w-[16ch]' }
37
+ }
38
+ },
39
+ defaultVariants: {
40
+ citationStyle: 'numeric'
41
+ }
42
+ });
@@ -0,0 +1,69 @@
1
+ import type { HTMLButtonAttributes } from 'svelte/elements';
2
+ import type { MarkdownUrlPolicy } from '../markdown/types.js';
3
+ import type { CitationChipSlots } from './citation-chip.variants.js';
4
+ /**
5
+ * A cited source surfaced behind a `[id]` citation marker. `id` keys the
6
+ * source to its marker; `title` is always shown, `url` / `snippet` are optional
7
+ * and only render when present (and, for `url`, when the URL policy allows it).
8
+ */
9
+ export interface CitationSource {
10
+ /** Stable id — matches the `[id]` marker in the streamed markdown. */
11
+ id: string;
12
+ /** Human-readable source title, always shown in the popover (and as the chip label under `citationStyle="label"`). */
13
+ title: string;
14
+ /** Source URL. Rendered as an outbound link only when it passes the `urlPolicy`; a blocked or absent URL shows title/snippet with no link. */
15
+ url?: string;
16
+ /** Short excerpt shown under the title, clamped to ~3 lines. */
17
+ snippet?: string;
18
+ }
19
+ /**
20
+ * @description Compact source marker rendered for a `[id]` citation. StreamingMarkdown wires it up automatically from its `sources` prop — each in-text marker becomes a CitationChip whose click opens a Popover with the source title, snippet, and a policy-checked outbound link. Also usable standalone for source footers / reference lists outside a streamed message.
21
+ * @tag ai
22
+ * @related StreamingMarkdown
23
+ * @related Badge
24
+ * @related Popover
25
+ * @stability experimental
26
+ */
27
+ export interface CitationChipProps extends Omit<HTMLButtonAttributes, 'class' | 'children'> {
28
+ /** The cited source. Required. */
29
+ source: CitationSource;
30
+ /** 1-based ordinal shown as the chip label under `citationStyle="numeric"`. Falls back to `source.id` when omitted. */
31
+ index?: number;
32
+ /**
33
+ * What the chip shows: `numeric` renders `index` (or `source.id` as a
34
+ * fallback) as a compact numeric pill; `label` renders the (truncated)
35
+ * `source.title`.
36
+ * @default 'numeric'
37
+ */
38
+ citationStyle?: 'numeric' | 'label';
39
+ /**
40
+ * URL policy applied to `source.url` before it becomes a link (same strict
41
+ * default as the streaming-markdown engine — untrusted LLM output). A blocked
42
+ * URL yields no link in the popover, only title/snippet.
43
+ */
44
+ urlPolicy?: MarkdownUrlPolicy;
45
+ /**
46
+ * Text of the outbound link in the popover.
47
+ * @default 'Open source'
48
+ */
49
+ openLabel?: string;
50
+ /**
51
+ * Override the trigger's `aria-label`. Defaults to
52
+ * `Source {index}: {title}` (or `Source: {title}` without an index).
53
+ */
54
+ label?: string;
55
+ /** Extra classes merged onto the trigger chip (the root slot). */
56
+ class?: string;
57
+ /** Strip all default tv() classes; combine with `class` / `slotClasses` for a custom look. */
58
+ unstyled?: boolean;
59
+ /** Per-slot class overrides. Slots: `trigger` (root chip), `popover` (content wrapper), `title`, `snippet`, `link`, `linkIcon`. */
60
+ slotClasses?: Partial<Record<CitationChipSlots, string>>;
61
+ /**
62
+ * Apply a named preset registered via `<BlocksProvider presets={{ CitationChip: {...} }}>`.
63
+ * Prefer this over `class` overrides when the requested look falls outside the
64
+ * semantic intent palette.
65
+ */
66
+ preset?: string;
67
+ }
68
+ export { default as CitationChip } from './CitationChip.svelte';
69
+ export { type CitationChipSlots, type CitationChipVariants, citationChipVariants } from './citation-chip.variants.js';
@@ -0,0 +1,2 @@
1
+ export { default as CitationChip } from './CitationChip.svelte';
2
+ export { citationChipVariants } from './citation-chip.variants.js';
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ import { getBlocksConfig, resolveSlotClasses } from '../../../provider';
3
+ import { resolveIcon } from '../../../icons';
4
+ import CopyIconDefault from '../../../icons/CopyIcon.svelte';
5
+ import CheckIconDefault from '../../../icons/CheckIcon.svelte';
6
+ import { codeBlockVariants, type CodeBlockVariants } from './code-block.variants';
7
+ import type { CodeBlockProps } from './index';
8
+
9
+ let {
10
+ code,
11
+ lang,
12
+ showCopy = true,
13
+ wrap = false,
14
+ copyLabel = 'Copy code',
15
+ copiedLabel = 'Copied',
16
+ onCopy,
17
+ actions,
18
+ class: className,
19
+ unstyled: unstyledProp = false,
20
+ slotClasses: slotClassesProp = {},
21
+ preset,
22
+ ...restProps
23
+ }: CodeBlockProps = $props();
24
+
25
+ const blocksConfig = getBlocksConfig();
26
+ const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
27
+
28
+ const CopyIcon = resolveIcon('copy', CopyIconDefault);
29
+ const CheckIcon = resolveIcon('check', CheckIconDefault);
30
+
31
+ const variantProps: CodeBlockVariants = $derived({ wrap });
32
+ const styles = $derived(codeBlockVariants(variantProps));
33
+ const slotClasses = $derived(
34
+ resolveSlotClasses(blocksConfig, 'CodeBlock', preset, variantProps, slotClassesProp)
35
+ );
36
+
37
+ function cls(name: keyof typeof slotClasses, extra?: string | (string | undefined)[]) {
38
+ if (unstyled) {
39
+ const own = slotClasses?.[name];
40
+ return [own, ...(Array.isArray(extra) ? extra : [extra])].filter(Boolean).join(' ');
41
+ }
42
+ const slotFns = styles as Record<string, (args: { class?: unknown }) => string>;
43
+ return slotFns[name]({
44
+ class: [slotClasses?.[name], ...(Array.isArray(extra) ? extra : [extra])]
45
+ });
46
+ }
47
+
48
+ const showHeader = $derived(!!lang || showCopy || !!actions);
49
+ const regionLabel = $derived(lang ? `${lang} code` : 'Code');
50
+
51
+ let copied = $state(false);
52
+ let resetTimer: ReturnType<typeof setTimeout> | undefined;
53
+
54
+ async function copyCode() {
55
+ try {
56
+ await navigator.clipboard.writeText(code);
57
+ copied = true;
58
+ if (resetTimer) clearTimeout(resetTimer);
59
+ resetTimer = setTimeout(() => {
60
+ copied = false;
61
+ resetTimer = undefined;
62
+ }, 2000);
63
+ onCopy?.(code);
64
+ } catch (err) {
65
+ // Never swallow silently — but leave state untouched so the UI does not
66
+ // falsely confirm a copy that failed (e.g. denied clipboard permission).
67
+ console.error('CodeBlock: failed to copy code', err);
68
+ }
69
+ }
70
+
71
+ // Clear a pending reset on unmount so the timer can't fire into a torn-down component.
72
+ $effect(() => () => {
73
+ if (resetTimer) clearTimeout(resetTimer);
74
+ });
75
+ </script>
76
+
77
+ <div class={cls('root', className)} {...restProps}>
78
+ {#if showHeader}
79
+ <div class={cls('header')}>
80
+ <span class={cls('langLabel')}>{lang ?? ''}</span>
81
+ <div class="flex items-center gap-1">
82
+ {@render actions?.()}
83
+ {#if showCopy}
84
+ <button
85
+ type="button"
86
+ class={cls('copyButton')}
87
+ onclick={copyCode}
88
+ aria-label={copied ? copiedLabel : copyLabel}
89
+ >
90
+ {#if copied}
91
+ <CheckIcon size={14} />
92
+ {:else}
93
+ <CopyIcon size={14} />
94
+ {/if}
95
+ <span>{copied ? copiedLabel : copyLabel}</span>
96
+ </button>
97
+ {/if}
98
+ </div>
99
+ </div>
100
+ {/if}
101
+
102
+ <!--
103
+ Scrollable code region: tabindex=0 + role=region + aria-label so keyboard
104
+ users can reach and scroll the overflow (WCAG 2.1.1). Svelte flags tabindex
105
+ on a "noninteractive" element, but a focusable scroll container is the
106
+ sanctioned exception here.
107
+ -->
108
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
109
+ <pre class={cls('pre')} tabindex="0" role="region" aria-label={regionLabel}><code
110
+ class={cls('code')}>{code}</code
111
+ ></pre>
112
+
113
+ <!--
114
+ Copy confirmation for screen readers. A label change on the button the user
115
+ just activated is not a reliable announcement — a live status region is. It
116
+ must exist in the DOM before `copied` flips, so it always renders and only
117
+ its text content changes.
118
+ -->
119
+ <span class="sr-only" role="status">{copied ? copiedLabel : ''}</span>
120
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { CodeBlockProps } from './index.js';
2
+ declare const CodeBlock: import("svelte").Component<CodeBlockProps, {}, "">;
3
+ type CodeBlock = ReturnType<typeof CodeBlock>;
4
+ export default CodeBlock;
@@ -0,0 +1,40 @@
1
+ import { type SlotNames, type VariantProps } from '../../../utils/variants.js';
2
+ export declare const codeBlockVariants: ((props?: {
3
+ wrap?: boolean | undefined;
4
+ } | undefined) => {
5
+ root: (props?: ({
6
+ wrap?: boolean | undefined;
7
+ } & {
8
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
9
+ }) | undefined) => string;
10
+ header: (props?: ({
11
+ wrap?: boolean | undefined;
12
+ } & {
13
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
14
+ }) | undefined) => string;
15
+ langLabel: (props?: ({
16
+ wrap?: boolean | undefined;
17
+ } & {
18
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
19
+ }) | undefined) => string;
20
+ copyButton: (props?: ({
21
+ wrap?: boolean | undefined;
22
+ } & {
23
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
24
+ }) | undefined) => string;
25
+ pre: (props?: ({
26
+ wrap?: boolean | undefined;
27
+ } & {
28
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
29
+ }) | undefined) => string;
30
+ code: (props?: ({
31
+ wrap?: boolean | undefined;
32
+ } & {
33
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
34
+ }) | undefined) => string;
35
+ }) & {
36
+ readonly config: import("../../../utils/variants.js").TVConfig;
37
+ };
38
+ export type CodeBlockVariants = VariantProps<typeof codeBlockVariants>;
39
+ /** Slot names derived from the tv() config — single source of truth for slotClasses. */
40
+ export type CodeBlockSlots = SlotNames<typeof codeBlockVariants>;
@@ -0,0 +1,51 @@
1
+ import { tv } from '../../../utils/variants.js';
2
+ export const codeBlockVariants = tv({
3
+ slots: {
4
+ // Architectural surface — an elevated card, structure radius, clipped so the
5
+ // scrollable body respects the rounded corners.
6
+ root: [
7
+ 'relative block w-full overflow-hidden box-border',
8
+ 'bg-surface-elevated border border-border-subtle rounded-contain'
9
+ ],
10
+ // Header only renders when there is something to show (lang / copy / actions).
11
+ header: [
12
+ 'flex items-center justify-between gap-2',
13
+ 'px-3 py-1.5 border-b border-border-subtle'
14
+ ],
15
+ langLabel: ['font-mono text-xs text-text-tertiary select-none'],
16
+ copyButton: [
17
+ 'inline-flex items-center gap-1.5',
18
+ 'px-2 py-1 rounded-modify',
19
+ 'font-mono text-xs text-text-secondary',
20
+ 'cursor-pointer select-none',
21
+ 'transition-[color,background-color] duration-[var(--blocks-duration-fast)] ease-out',
22
+ 'hover:bg-surface-hover hover:text-text-primary',
23
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2'
24
+ ],
25
+ // The scrollable region. Body horizontal scroll stays INSIDE the block —
26
+ // never the page. Focusable + role=region so keyboard users can reach the
27
+ // scroll (a11y for scrollable content).
28
+ pre: [
29
+ 'm-0 px-3 py-2.5',
30
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset'
31
+ ],
32
+ code: ['font-mono text-sm text-text-primary']
33
+ },
34
+ variants: {
35
+ // false → single-line semantics, horizontal scroll inside the block.
36
+ // true → soft-wrap long lines, no horizontal scroll.
37
+ wrap: {
38
+ true: {
39
+ pre: 'whitespace-pre-wrap break-words',
40
+ code: 'whitespace-pre-wrap break-words'
41
+ },
42
+ false: {
43
+ pre: 'overflow-x-auto',
44
+ code: 'whitespace-pre'
45
+ }
46
+ }
47
+ },
48
+ defaultVariants: {
49
+ wrap: false
50
+ }
51
+ });