@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.
- package/README.md +11 -1
- package/dist/components/Calendar/CalendarHeader.svelte +32 -26
- package/dist/components/Calendar/CalendarMiniMonth.svelte +10 -10
- package/dist/components/Calendar/calendar.variants.js +19 -5
- package/dist/components/Chat/Chat/Chat.svelte +53 -0
- package/dist/components/Chat/Chat/Chat.svelte.d.ts +4 -0
- package/dist/components/Chat/Chat/chat.variants.d.ts +20 -0
- package/dist/components/Chat/Chat/chat.variants.js +15 -0
- package/dist/components/Chat/Chat/index.d.ts +47 -0
- package/dist/components/Chat/Chat/index.js +2 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte +342 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte.d.ts +4 -0
- package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +138 -0
- package/dist/components/Chat/ChatMessage/chat-message.variants.js +118 -0
- package/dist/components/Chat/ChatMessage/index.d.ts +102 -0
- package/dist/components/Chat/ChatMessage/index.js +2 -0
- package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte +301 -0
- package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte.d.ts +6 -0
- package/dist/components/Chat/ChatMessageList/chat-message-list.variants.d.ts +23 -0
- package/dist/components/Chat/ChatMessageList/chat-message-list.variants.js +32 -0
- package/dist/components/Chat/ChatMessageList/chat-scroll.d.ts +45 -0
- package/dist/components/Chat/ChatMessageList/chat-scroll.js +52 -0
- package/dist/components/Chat/ChatMessageList/index.d.ts +93 -0
- package/dist/components/Chat/ChatMessageList/index.js +3 -0
- package/dist/components/Chat/CitationChip/CitationChip.svelte +107 -0
- package/dist/components/Chat/CitationChip/CitationChip.svelte.d.ts +4 -0
- package/dist/components/Chat/CitationChip/citation-chip.variants.d.ts +40 -0
- package/dist/components/Chat/CitationChip/citation-chip.variants.js +42 -0
- package/dist/components/Chat/CitationChip/index.d.ts +69 -0
- package/dist/components/Chat/CitationChip/index.js +2 -0
- package/dist/components/Chat/CodeBlock/CodeBlock.svelte +120 -0
- package/dist/components/Chat/CodeBlock/CodeBlock.svelte.d.ts +4 -0
- package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +40 -0
- package/dist/components/Chat/CodeBlock/code-block.variants.js +51 -0
- package/dist/components/Chat/CodeBlock/index.d.ts +55 -0
- package/dist/components/Chat/CodeBlock/index.js +2 -0
- package/dist/components/Chat/PromptInput/PromptInput.svelte +432 -0
- package/dist/components/Chat/PromptInput/PromptInput.svelte.d.ts +4 -0
- package/dist/components/Chat/PromptInput/index.d.ts +165 -0
- package/dist/components/Chat/PromptInput/index.js +2 -0
- package/dist/components/Chat/PromptInput/prompt-input.variants.d.ts +90 -0
- package/dist/components/Chat/PromptInput/prompt-input.variants.js +102 -0
- package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte +107 -0
- package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte.d.ts +4 -0
- package/dist/components/Chat/ReasoningDisclosure/index.d.ts +85 -0
- package/dist/components/Chat/ReasoningDisclosure/index.js +2 -0
- package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.d.ts +20 -0
- package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.js +31 -0
- package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte +99 -0
- package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte.d.ts +9 -0
- package/dist/components/Chat/StreamingMarkdown/MdInline.svelte +82 -0
- package/dist/components/Chat/StreamingMarkdown/MdInline.svelte.d.ts +9 -0
- package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte +118 -0
- package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte.d.ts +4 -0
- package/dist/components/Chat/StreamingMarkdown/index.d.ts +86 -0
- package/dist/components/Chat/StreamingMarkdown/index.js +2 -0
- package/dist/components/Chat/StreamingMarkdown/md-context.d.ts +58 -0
- package/dist/components/Chat/StreamingMarkdown/md-context.js +1 -0
- package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.d.ts +145 -0
- package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.js +73 -0
- package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +174 -0
- package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte.d.ts +4 -0
- package/dist/components/Chat/ToolCallCard/index.d.ts +84 -0
- package/dist/components/Chat/ToolCallCard/index.js +2 -0
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +38 -0
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +38 -0
- package/dist/components/Chat/chat.types.d.ts +82 -0
- package/dist/components/Chat/chat.types.js +7 -0
- package/dist/components/Chat/index.d.ts +22 -0
- package/dist/components/Chat/index.js +14 -0
- package/dist/components/Chat/markdown/blocks.d.ts +54 -0
- package/dist/components/Chat/markdown/blocks.js +462 -0
- package/dist/components/Chat/markdown/inline.d.ts +31 -0
- package/dist/components/Chat/markdown/inline.js +446 -0
- package/dist/components/Chat/markdown/repair.d.ts +23 -0
- package/dist/components/Chat/markdown/repair.js +337 -0
- package/dist/components/Chat/markdown/streaming-harness.d.ts +23 -0
- package/dist/components/Chat/markdown/streaming-harness.js +62 -0
- package/dist/components/Chat/markdown/types.d.ts +139 -0
- package/dist/components/Chat/markdown/types.js +13 -0
- package/dist/components/Chat/markdown/url-policy.d.ts +33 -0
- package/dist/components/Chat/markdown/url-policy.js +109 -0
- package/dist/components/FileUpload/FileUpload.svelte +52 -125
- package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
- package/dist/components/FileUpload/index.d.ts +2 -28
- package/dist/components/Planner/PlannerHeader.svelte +16 -13
- package/dist/components/Planner/planner.variants.js +14 -3
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/i18n/index.d.ts +268 -382
- package/dist/i18n/index.js +83 -9
- package/dist/internal/core/CoreIconButton.svelte +66 -0
- package/dist/internal/core/CoreIconButton.svelte.d.ts +13 -0
- package/dist/internal/core/CoreSpinner.svelte +49 -0
- package/dist/internal/core/CoreSpinner.svelte.d.ts +8 -0
- package/dist/internal/core/spinner-geometry.d.ts +6 -0
- package/dist/internal/core/spinner-geometry.js +6 -0
- package/dist/mint/registry.d.ts +20 -2
- package/dist/mint/registry.js +20 -2
- package/dist/primitives/Badge/Badge.svelte +5 -5
- package/dist/primitives/Badge/badge.variants.js +17 -1
- package/dist/primitives/Button/Button.svelte +4 -2
- package/dist/primitives/Collapsible/Collapsible.svelte +5 -0
- package/dist/primitives/Dialog/Dialog.svelte +7 -5
- package/dist/primitives/Dialog/dialog.variants.d.ts +7 -0
- package/dist/primitives/Dialog/dialog.variants.js +17 -0
- package/dist/primitives/Drawer/Drawer.svelte +7 -5
- package/dist/primitives/Drawer/drawer.variants.d.ts +8 -0
- package/dist/primitives/Drawer/drawer.variants.js +17 -0
- package/dist/primitives/Spinner/Spinner.svelte +3 -1
- package/dist/primitives/SplitPane/SplitPane.svelte +293 -0
- package/dist/primitives/SplitPane/SplitPane.svelte.d.ts +4 -0
- package/dist/primitives/SplitPane/index.d.ts +88 -0
- package/dist/primitives/SplitPane/index.js +2 -0
- package/dist/primitives/SplitPane/split-pane.utils.d.ts +73 -0
- package/dist/primitives/SplitPane/split-pane.utils.js +94 -0
- package/dist/primitives/SplitPane/split-pane.variants.d.ts +40 -0
- package/dist/primitives/SplitPane/split-pane.variants.js +56 -0
- package/dist/primitives/Toast/Toaster.svelte +17 -2
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/utils/file-intake.d.ts +78 -0
- package/dist/utils/file-intake.js +144 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/variants.js +44 -10
- 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,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,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,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,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
|
+
});
|