@rokkit/ui 1.3.1 → 1.3.3
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/dist/components/ChatComposer.svelte.d.ts +6 -0
- package/dist/components/ChatHistory.svelte.d.ts +4 -0
- package/dist/components/ChatMessage.svelte.d.ts +4 -0
- package/dist/components/ChatShell.svelte.d.ts +4 -0
- package/dist/components/ChatTimeline.svelte.d.ts +4 -0
- package/dist/components/ItemSwitch.svelte.d.ts +15 -0
- package/dist/components/ItemToggle.svelte.d.ts +16 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/types/chat.d.ts +73 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/item-switch.d.ts +27 -0
- package/dist/types/item-toggle.d.ts +45 -0
- package/dist/utils/relative-time.d.ts +2 -0
- package/package.json +5 -5
- package/src/components/ChatComposer.svelte +48 -0
- package/src/components/ChatHistory.svelte +42 -0
- package/src/components/ChatMessage.svelte +27 -0
- package/src/components/ChatShell.svelte +41 -0
- package/src/components/ChatTimeline.svelte +32 -0
- package/src/components/ItemSwitch.svelte +40 -0
- package/src/components/ItemToggle.svelte +103 -0
- package/src/components/index.ts +7 -0
- package/src/index.ts +6 -1
- package/src/types/chat.ts +81 -0
- package/src/types/index.ts +9 -0
- package/src/types/item-switch.ts +34 -0
- package/src/types/item-toggle.ts +52 -0
- package/src/utils/relative-time.ts +12 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ItemSwitch — inert switch-shaped renderer for use inside a List / Menu / Tree
|
|
3
|
+
* item snippet.
|
|
4
|
+
*
|
|
5
|
+
* Renders `<ItemContent>` for the label / icon and then a purely visual switch
|
|
6
|
+
* (span-based, aria-hidden) that reflects the item's boolean state via
|
|
7
|
+
* `aria-checked`. Because nothing interactive is nested inside the parent
|
|
8
|
+
* `<button data-list-item>`, the HTML parser leaves the markup intact and the
|
|
9
|
+
* whole row remains a single click target — clicking the row triggers the
|
|
10
|
+
* parent's `onselect` flow, which the consumer uses to flip the state.
|
|
11
|
+
*/
|
|
12
|
+
import type { ItemSwitchProps } from '../types/item-switch.js';
|
|
13
|
+
declare const ItemSwitch: import("svelte").Component<ItemSwitchProps, {}, "">;
|
|
14
|
+
type ItemSwitch = ReturnType<typeof ItemSwitch>;
|
|
15
|
+
export default ItemSwitch;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ItemToggle — compact toggle-group renderer for use inside a List / Menu / Tree
|
|
3
|
+
* item snippet.
|
|
4
|
+
*
|
|
5
|
+
* Each option is rendered as `<span role="radio" tabindex="-1">` with its own
|
|
6
|
+
* click handler. Navigator's `isNestedInteractive` guard defers to these
|
|
7
|
+
* nested [role="radio"] elements so the row's select flow is NOT triggered
|
|
8
|
+
* when a user picks an option. Consumers wire the state via `onchange`.
|
|
9
|
+
*
|
|
10
|
+
* The wrapping element remains a `<span>` (non-interactive) so it stays legal
|
|
11
|
+
* inside the parent `<button data-list-item>`.
|
|
12
|
+
*/
|
|
13
|
+
import type { ItemToggleProps } from '../types/item-toggle.js';
|
|
14
|
+
declare const ItemToggle: import("svelte").Component<ItemToggleProps, {}, "">;
|
|
15
|
+
type ItemToggle = ReturnType<typeof ItemToggle>;
|
|
16
|
+
export default ItemToggle;
|
|
@@ -19,6 +19,8 @@ export { default as Tree } from './Tree.svelte';
|
|
|
19
19
|
export { default as LazyTree } from './LazyTree.svelte';
|
|
20
20
|
export { default as Connector } from './Connector.svelte';
|
|
21
21
|
export { default as ItemContent } from './ItemContent.svelte';
|
|
22
|
+
export { default as ItemSwitch } from './ItemSwitch.svelte';
|
|
23
|
+
export { default as ItemToggle } from './ItemToggle.svelte';
|
|
22
24
|
export { default as FloatingAction } from './FloatingAction.svelte';
|
|
23
25
|
export { default as PaletteManager } from './PaletteManager.svelte';
|
|
24
26
|
export { default as Tilt } from './Tilt.svelte';
|
|
@@ -53,3 +55,8 @@ export { default as Avatar } from './Avatar.svelte';
|
|
|
53
55
|
export { default as Tooltip } from './Tooltip.svelte';
|
|
54
56
|
export { default as NavContent } from './NavContent.svelte';
|
|
55
57
|
export { default as ResponsiveGrid } from './ResponsiveGrid.svelte';
|
|
58
|
+
export { default as ChatMessage } from './ChatMessage.svelte';
|
|
59
|
+
export { default as ChatTimeline } from './ChatTimeline.svelte';
|
|
60
|
+
export { default as ChatComposer } from './ChatComposer.svelte';
|
|
61
|
+
export { default as ChatHistory } from './ChatHistory.svelte';
|
|
62
|
+
export { default as ChatShell } from './ChatShell.svelte';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Button, ButtonGroup, Code, CodeBlock, CodeGroup, Frame, Menu, Dropdown, Select, MultiSelect, Toolbar, ToolbarGroup, Tabs, Toggle, Swatch, List, Tree, LazyTree, LockMode, FloatingAction, FloatingNavigation, PaletteManager, Tilt, Shine, BreadCrumbs, Card, ProgressBar, Carousel, Pill, Rating, Connector, ItemContent, Stepper, Switch, Table, TreeTable, SearchFilter, Range, Timeline, Reveal, Grid, UploadTarget, UploadFileStatus, UploadProgress, StatusList, Message, AlertList, Divider, Stack, Badge, Avatar, Tooltip, NavContent, ResponsiveGrid, CommandPalette } from './components/index.js';
|
|
1
|
+
export { Button, ButtonGroup, Code, CodeBlock, CodeGroup, Frame, Menu, Dropdown, Select, MultiSelect, Toolbar, ToolbarGroup, Tabs, Toggle, Swatch, List, Tree, LazyTree, LockMode, FloatingAction, FloatingNavigation, PaletteManager, Tilt, Shine, BreadCrumbs, Card, ProgressBar, Carousel, Pill, Rating, Connector, ItemContent, Stepper, Switch, Table, TreeTable, SearchFilter, Range, Timeline, Reveal, Grid, UploadTarget, UploadFileStatus, UploadProgress, StatusList, Message, AlertList, Divider, Stack, Badge, Avatar, Tooltip, NavContent, ResponsiveGrid, CommandPalette, ChatMessage, ChatTimeline, ChatComposer, ChatHistory, ChatShell } from './components/index.js';
|
|
2
2
|
export { default as MarkdownRenderer } from './MarkdownRenderer.svelte';
|
|
3
3
|
export type { MarkdownPlugin } from './markdown-plugin.js';
|
|
4
4
|
export { highlightCode, preloadHighlighter, getSupportedLanguages } from './utils/shiki.js';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type ChatRole = 'user' | 'assistant' | 'system';
|
|
3
|
+
export type ChatStatus = 'streaming' | 'error' | 'done';
|
|
4
|
+
export interface ChatMessage<T = unknown> {
|
|
5
|
+
id: string;
|
|
6
|
+
role: ChatRole;
|
|
7
|
+
/** Default body, rendered as markdown by the built-in body snippet. */
|
|
8
|
+
text?: string;
|
|
9
|
+
timestamp?: string;
|
|
10
|
+
/** Drives the optional typing/error affordance; consumer sets it. */
|
|
11
|
+
status?: ChatStatus;
|
|
12
|
+
/** Arbitrary payload (parts/blocks/chart spec/form schema) the consumer's `message` snippet reads. */
|
|
13
|
+
data?: T;
|
|
14
|
+
}
|
|
15
|
+
export interface ConversationSummary {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
timestamp?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ChatMessageProps {
|
|
21
|
+
message: ChatMessage;
|
|
22
|
+
relativeTime?: boolean;
|
|
23
|
+
body?: Snippet<[ChatMessage]>;
|
|
24
|
+
avatar?: Snippet<[ChatMessage]>;
|
|
25
|
+
label?: Snippet<[ChatMessage]>;
|
|
26
|
+
}
|
|
27
|
+
export interface ChatComposerProps {
|
|
28
|
+
value?: string;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
busy?: boolean;
|
|
32
|
+
onsubmit?: (text: string) => void;
|
|
33
|
+
onchange?: (value: string) => void;
|
|
34
|
+
suggestions?: Snippet;
|
|
35
|
+
toolbar?: Snippet;
|
|
36
|
+
leading?: Snippet;
|
|
37
|
+
}
|
|
38
|
+
export interface ChatHistoryProps {
|
|
39
|
+
conversations?: ConversationSummary[];
|
|
40
|
+
activeId?: string | null;
|
|
41
|
+
relativeTime?: boolean;
|
|
42
|
+
onselect?: (id: string) => void;
|
|
43
|
+
onnew?: () => void;
|
|
44
|
+
ondelete?: (id: string) => void;
|
|
45
|
+
item?: Snippet<[ConversationSummary]>;
|
|
46
|
+
empty?: Snippet;
|
|
47
|
+
header?: Snippet;
|
|
48
|
+
}
|
|
49
|
+
export interface ChatTimelineProps {
|
|
50
|
+
messages?: ChatMessage[];
|
|
51
|
+
relativeTime?: boolean;
|
|
52
|
+
autoscroll?: boolean;
|
|
53
|
+
message?: Snippet<[ChatMessage]>;
|
|
54
|
+
empty?: Snippet;
|
|
55
|
+
}
|
|
56
|
+
export interface ChatShellProps {
|
|
57
|
+
messages?: ChatMessage[];
|
|
58
|
+
conversations?: ConversationSummary[];
|
|
59
|
+
activeConversationId?: string | null;
|
|
60
|
+
value?: string;
|
|
61
|
+
placeholder?: string;
|
|
62
|
+
busy?: boolean;
|
|
63
|
+
onsubmit?: (text: string) => void;
|
|
64
|
+
onchange?: (value: string) => void;
|
|
65
|
+
onselectConversation?: (id: string) => void;
|
|
66
|
+
onnew?: () => void;
|
|
67
|
+
message?: Snippet<[ChatMessage]>;
|
|
68
|
+
suggestions?: Snippet;
|
|
69
|
+
toolbar?: Snippet;
|
|
70
|
+
leading?: Snippet;
|
|
71
|
+
historyItem?: Snippet<[ConversationSummary]>;
|
|
72
|
+
empty?: Snippet;
|
|
73
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export * from './upload-file-status.js';
|
|
|
22
22
|
export * from './upload-progress.js';
|
|
23
23
|
export * from './tooltip.js';
|
|
24
24
|
export * from './item-content.js';
|
|
25
|
+
export * from './item-switch.js';
|
|
26
|
+
export * from './item-toggle.js';
|
|
25
27
|
export * from './status-list.js';
|
|
26
28
|
export * from './alert-list.js';
|
|
27
29
|
export * from './avatar.js';
|
|
@@ -43,3 +45,4 @@ export * from './shine.js';
|
|
|
43
45
|
export * from './stack.js';
|
|
44
46
|
export * from './stepper.js';
|
|
45
47
|
export * from './tilt.js';
|
|
48
|
+
export * from './chat.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ItemSwitch Component Types
|
|
3
|
+
*
|
|
4
|
+
* Renders a switch-shaped visual inside a data-driven item snippet
|
|
5
|
+
* (List / Menu / Tree). The whole row is the interactive target — clicking it
|
|
6
|
+
* toggles the underlying data through the parent's onselect flow. ItemSwitch
|
|
7
|
+
* itself renders inert markup (spans + attributes), so it never introduces a
|
|
8
|
+
* nested interactive element inside the item's `<button>` wrapper.
|
|
9
|
+
*/
|
|
10
|
+
import type { ProxyItem } from '@rokkit/states';
|
|
11
|
+
export interface ItemSwitchProps {
|
|
12
|
+
/** ProxyItem for the current row. */
|
|
13
|
+
proxy: ProxyItem;
|
|
14
|
+
/**
|
|
15
|
+
* Field on the item that holds the boolean checked state.
|
|
16
|
+
* @default 'checked'
|
|
17
|
+
*/
|
|
18
|
+
field?: string;
|
|
19
|
+
/** Size variant — feeds into `data-switch-size` for theming. */
|
|
20
|
+
size?: 'sm' | 'md' | 'lg';
|
|
21
|
+
/** Render the leading avatar / icon (via ItemContent). @default true */
|
|
22
|
+
showIcon?: boolean;
|
|
23
|
+
/** Render subtext under the label (via ItemContent). @default true */
|
|
24
|
+
showSubtext?: boolean;
|
|
25
|
+
/** Optional class on the wrapper span. */
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ItemToggle Component Types
|
|
3
|
+
*
|
|
4
|
+
* Renders a compact toggle-group visual inside a data-driven item snippet.
|
|
5
|
+
* Options come from the item's data (via `optionsField`). Each option is a
|
|
6
|
+
* span with `role="radio"` and its own click handler — Navigator defers to
|
|
7
|
+
* these nested interactives via its isNestedInteractive guard.
|
|
8
|
+
*
|
|
9
|
+
* Consumers wire state via `onchange`. ItemToggle does not mutate the item
|
|
10
|
+
* data directly.
|
|
11
|
+
*/
|
|
12
|
+
import type { ProxyItem } from '@rokkit/states';
|
|
13
|
+
export type ItemToggleOption = string | number | boolean | {
|
|
14
|
+
label?: string;
|
|
15
|
+
value?: unknown;
|
|
16
|
+
icon?: string;
|
|
17
|
+
};
|
|
18
|
+
export interface ItemToggleProps {
|
|
19
|
+
/** ProxyItem for the current row. */
|
|
20
|
+
proxy: ProxyItem;
|
|
21
|
+
/**
|
|
22
|
+
* Field on the item that holds the currently selected value.
|
|
23
|
+
* @default 'value'
|
|
24
|
+
*/
|
|
25
|
+
field?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Field on the item that holds the array of options.
|
|
28
|
+
* @default 'options'
|
|
29
|
+
*/
|
|
30
|
+
optionsField?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Called when the user picks an option. Consumers update the item data.
|
|
33
|
+
*/
|
|
34
|
+
onchange?: (value: unknown, option: ItemToggleOption, proxy: ProxyItem) => void;
|
|
35
|
+
/** Size variant. */
|
|
36
|
+
size?: 'sm' | 'md' | 'lg';
|
|
37
|
+
/** Render the leading avatar / icon (via ItemContent). @default true */
|
|
38
|
+
showIcon?: boolean;
|
|
39
|
+
/** Render subtext under the label (via ItemContent). @default true */
|
|
40
|
+
showSubtext?: boolean;
|
|
41
|
+
/** Show textual labels beside the option (falls back to option value). @default true */
|
|
42
|
+
showLabels?: boolean;
|
|
43
|
+
/** Optional class on the wrapper span. */
|
|
44
|
+
class?: string;
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/ui",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "Data driven UI components for Rokkit applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"dropdown"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@rokkit/core": "1.3.
|
|
49
|
-
"@rokkit/data": "1.3.
|
|
50
|
-
"@rokkit/states": "1.3.
|
|
51
|
-
"@rokkit/actions": "1.3.
|
|
48
|
+
"@rokkit/core": "1.3.3",
|
|
49
|
+
"@rokkit/data": "1.3.3",
|
|
50
|
+
"@rokkit/states": "1.3.3",
|
|
51
|
+
"@rokkit/actions": "1.3.3",
|
|
52
52
|
"marked": "^15.0.0",
|
|
53
53
|
"dompurify": "^3.0.0"
|
|
54
54
|
},
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChatComposerProps } from '../types/chat.js'
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
value = $bindable(''),
|
|
6
|
+
placeholder = '',
|
|
7
|
+
disabled = false,
|
|
8
|
+
busy = false,
|
|
9
|
+
onsubmit,
|
|
10
|
+
onchange,
|
|
11
|
+
suggestions,
|
|
12
|
+
toolbar,
|
|
13
|
+
leading
|
|
14
|
+
}: ChatComposerProps = $props()
|
|
15
|
+
|
|
16
|
+
let textarea: HTMLTextAreaElement
|
|
17
|
+
|
|
18
|
+
export function focus() {
|
|
19
|
+
textarea?.focus()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
23
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
24
|
+
e.preventDefault()
|
|
25
|
+
if (!disabled && !busy && value.trim()) onsubmit?.(value)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleInput() {
|
|
30
|
+
onchange?.(value)
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div data-chat-composer data-busy={busy || undefined}>
|
|
35
|
+
{#if leading}{@render leading()}{/if}
|
|
36
|
+
{#if suggestions}{@render suggestions()}{/if}
|
|
37
|
+
<textarea
|
|
38
|
+
bind:this={textarea}
|
|
39
|
+
bind:value
|
|
40
|
+
{placeholder}
|
|
41
|
+
{disabled}
|
|
42
|
+
rows="2"
|
|
43
|
+
data-chat-input
|
|
44
|
+
oninput={handleInput}
|
|
45
|
+
onkeydown={handleKeydown}
|
|
46
|
+
></textarea>
|
|
47
|
+
{#if toolbar}{@render toolbar()}{/if}
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatRelativeTime } from '../utils/relative-time.js'
|
|
3
|
+
import type { ChatHistoryProps } from '../types/chat.js'
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
conversations = [],
|
|
7
|
+
activeId = null,
|
|
8
|
+
relativeTime = true,
|
|
9
|
+
onselect,
|
|
10
|
+
onnew,
|
|
11
|
+
ondelete,
|
|
12
|
+
item,
|
|
13
|
+
empty,
|
|
14
|
+
header
|
|
15
|
+
}: ChatHistoryProps = $props()
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<div data-chat-history>
|
|
19
|
+
{#if header}{@render header()}{/if}
|
|
20
|
+
{#if onnew}<button type="button" data-chat-history-new onclick={() => onnew()}>New conversation</button>{/if}
|
|
21
|
+
{#if conversations.length === 0}
|
|
22
|
+
{#if empty}{@render empty()}{/if}
|
|
23
|
+
{:else}
|
|
24
|
+
{#each conversations as conversation (conversation.id)}
|
|
25
|
+
{#if item}
|
|
26
|
+
{@render item(conversation)}
|
|
27
|
+
{:else}
|
|
28
|
+
<div data-chat-history-row data-active={conversation.id === activeId || undefined}>
|
|
29
|
+
<button type="button" data-chat-history-item onclick={() => onselect?.(conversation.id)}>
|
|
30
|
+
<span data-chat-history-title>{conversation.title}</span>
|
|
31
|
+
{#if conversation.timestamp}
|
|
32
|
+
<time data-chat-history-time>{formatRelativeTime(conversation.timestamp, relativeTime)}</time>
|
|
33
|
+
{/if}
|
|
34
|
+
</button>
|
|
35
|
+
{#if ondelete}
|
|
36
|
+
<button type="button" data-chat-history-delete aria-label="Delete conversation" onclick={() => ondelete(conversation.id)}>×</button>
|
|
37
|
+
{/if}
|
|
38
|
+
</div>
|
|
39
|
+
{/if}
|
|
40
|
+
{/each}
|
|
41
|
+
{/if}
|
|
42
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import MarkdownRenderer from '../MarkdownRenderer.svelte'
|
|
3
|
+
import { formatRelativeTime } from '../utils/relative-time.js'
|
|
4
|
+
import type { ChatMessageProps } from '../types/chat.js'
|
|
5
|
+
|
|
6
|
+
const { message, relativeTime = true, body, avatar, label }: ChatMessageProps = $props()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<div data-chat-message data-role={message.role} data-status={message.status || undefined}>
|
|
10
|
+
{#if avatar}{@render avatar(message)}{/if}
|
|
11
|
+
<div data-chat-bubble>
|
|
12
|
+
{#if label}{@render label(message)}{/if}
|
|
13
|
+
<div data-chat-body>
|
|
14
|
+
{#if body}
|
|
15
|
+
{@render body(message)}
|
|
16
|
+
{:else if message.text}
|
|
17
|
+
<MarkdownRenderer markdown={message.text} />
|
|
18
|
+
{/if}
|
|
19
|
+
{#if message.status === 'streaming'}<span data-chat-caret aria-hidden="true"></span>{/if}
|
|
20
|
+
</div>
|
|
21
|
+
{#if message.timestamp}
|
|
22
|
+
<time data-chat-time datetime={message.timestamp}
|
|
23
|
+
>{formatRelativeTime(message.timestamp, relativeTime)}</time
|
|
24
|
+
>
|
|
25
|
+
{/if}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ChatTimeline from './ChatTimeline.svelte'
|
|
3
|
+
import ChatComposer from './ChatComposer.svelte'
|
|
4
|
+
import ChatHistory from './ChatHistory.svelte'
|
|
5
|
+
import type { ChatShellProps } from '../types/chat.js'
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
messages = [],
|
|
9
|
+
conversations,
|
|
10
|
+
activeConversationId = null,
|
|
11
|
+
value = $bindable(''),
|
|
12
|
+
placeholder,
|
|
13
|
+
busy = false,
|
|
14
|
+
onsubmit,
|
|
15
|
+
onchange,
|
|
16
|
+
onselectConversation,
|
|
17
|
+
onnew,
|
|
18
|
+
message,
|
|
19
|
+
suggestions,
|
|
20
|
+
toolbar,
|
|
21
|
+
leading,
|
|
22
|
+
historyItem,
|
|
23
|
+
empty
|
|
24
|
+
}: ChatShellProps = $props()
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<div data-chat-shell data-has-history={conversations ? '' : undefined}>
|
|
28
|
+
{#if conversations}
|
|
29
|
+
<ChatHistory
|
|
30
|
+
{conversations}
|
|
31
|
+
activeId={activeConversationId}
|
|
32
|
+
onselect={onselectConversation}
|
|
33
|
+
{onnew}
|
|
34
|
+
item={historyItem}
|
|
35
|
+
/>
|
|
36
|
+
{/if}
|
|
37
|
+
<div data-chat-main>
|
|
38
|
+
<ChatTimeline {messages} {message} {empty} />
|
|
39
|
+
<ChatComposer bind:value {placeholder} {busy} {onsubmit} {onchange} {suggestions} {toolbar} {leading} />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ChatMessage from './ChatMessage.svelte'
|
|
3
|
+
import type { ChatTimelineProps } from '../types/chat.js'
|
|
4
|
+
|
|
5
|
+
let { messages = [], relativeTime = true, autoscroll = true, message, empty }: ChatTimelineProps = $props()
|
|
6
|
+
|
|
7
|
+
let container: HTMLElement | undefined = $state()
|
|
8
|
+
|
|
9
|
+
// Auto-scroll to the newest content when messages grow or the last message streams.
|
|
10
|
+
$effect(() => {
|
|
11
|
+
const last = messages[messages.length - 1]
|
|
12
|
+
// touch reactive deps so the effect re-runs on append + streaming updates
|
|
13
|
+
void messages.length
|
|
14
|
+
void last?.text
|
|
15
|
+
void last?.status
|
|
16
|
+
if (autoscroll && container) container.scrollTop = container.scrollHeight
|
|
17
|
+
})
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div bind:this={container} data-chat-timeline>
|
|
21
|
+
{#if messages.length === 0}
|
|
22
|
+
{#if empty}{@render empty()}{/if}
|
|
23
|
+
{:else}
|
|
24
|
+
{#each messages as msg (msg.id)}
|
|
25
|
+
{#if message}
|
|
26
|
+
{@render message(msg)}
|
|
27
|
+
{:else}
|
|
28
|
+
<ChatMessage message={msg} {relativeTime} />
|
|
29
|
+
{/if}
|
|
30
|
+
{/each}
|
|
31
|
+
{/if}
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ItemSwitch — inert switch-shaped renderer for use inside a List / Menu / Tree
|
|
4
|
+
* item snippet.
|
|
5
|
+
*
|
|
6
|
+
* Renders `<ItemContent>` for the label / icon and then a purely visual switch
|
|
7
|
+
* (span-based, aria-hidden) that reflects the item's boolean state via
|
|
8
|
+
* `aria-checked`. Because nothing interactive is nested inside the parent
|
|
9
|
+
* `<button data-list-item>`, the HTML parser leaves the markup intact and the
|
|
10
|
+
* whole row remains a single click target — clicking the row triggers the
|
|
11
|
+
* parent's `onselect` flow, which the consumer uses to flip the state.
|
|
12
|
+
*/
|
|
13
|
+
import type { ItemSwitchProps } from '../types/item-switch.js'
|
|
14
|
+
import ItemContent from './ItemContent.svelte'
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
proxy,
|
|
18
|
+
field = 'checked',
|
|
19
|
+
size = 'md',
|
|
20
|
+
showIcon = true,
|
|
21
|
+
showSubtext = true,
|
|
22
|
+
class: className
|
|
23
|
+
}: ItemSwitchProps = $props()
|
|
24
|
+
|
|
25
|
+
const checked = $derived(Boolean(proxy.get(field)))
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<ItemContent {proxy} {showIcon} {showSubtext} />
|
|
29
|
+
<span
|
|
30
|
+
data-item-switch
|
|
31
|
+
data-switch
|
|
32
|
+
data-switch-size={size}
|
|
33
|
+
aria-checked={checked}
|
|
34
|
+
aria-hidden="true"
|
|
35
|
+
class={className || undefined}
|
|
36
|
+
>
|
|
37
|
+
<span data-switch-track>
|
|
38
|
+
<span data-switch-thumb></span>
|
|
39
|
+
</span>
|
|
40
|
+
</span>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ItemToggle — compact toggle-group renderer for use inside a List / Menu / Tree
|
|
4
|
+
* item snippet.
|
|
5
|
+
*
|
|
6
|
+
* Each option is rendered as `<span role="radio" tabindex="-1">` with its own
|
|
7
|
+
* click handler. Navigator's `isNestedInteractive` guard defers to these
|
|
8
|
+
* nested [role="radio"] elements so the row's select flow is NOT triggered
|
|
9
|
+
* when a user picks an option. Consumers wire the state via `onchange`.
|
|
10
|
+
*
|
|
11
|
+
* The wrapping element remains a `<span>` (non-interactive) so it stays legal
|
|
12
|
+
* inside the parent `<button data-list-item>`.
|
|
13
|
+
*/
|
|
14
|
+
import type { ItemToggleProps, ItemToggleOption } from '../types/item-toggle.js'
|
|
15
|
+
import ItemContent from './ItemContent.svelte'
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
proxy,
|
|
19
|
+
field = 'value',
|
|
20
|
+
optionsField = 'options',
|
|
21
|
+
onchange,
|
|
22
|
+
size = 'md',
|
|
23
|
+
showIcon = true,
|
|
24
|
+
showSubtext = true,
|
|
25
|
+
showLabels = true,
|
|
26
|
+
class: className
|
|
27
|
+
}: ItemToggleProps = $props()
|
|
28
|
+
|
|
29
|
+
const options = $derived<ItemToggleOption[]>(
|
|
30
|
+
Array.isArray(proxy.get(optionsField)) ? (proxy.get(optionsField) as ItemToggleOption[]) : []
|
|
31
|
+
)
|
|
32
|
+
const currentValue = $derived(proxy.get(field))
|
|
33
|
+
|
|
34
|
+
function extractValue(opt: ItemToggleOption): unknown {
|
|
35
|
+
if (opt && typeof opt === 'object') {
|
|
36
|
+
const o = opt as { value?: unknown }
|
|
37
|
+
return 'value' in o ? o.value : opt
|
|
38
|
+
}
|
|
39
|
+
return opt
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractLabel(opt: ItemToggleOption): string {
|
|
43
|
+
if (opt && typeof opt === 'object') {
|
|
44
|
+
const o = opt as { label?: string; value?: unknown }
|
|
45
|
+
if (o.label !== undefined && o.label !== null) return String(o.label)
|
|
46
|
+
return String(extractValue(opt))
|
|
47
|
+
}
|
|
48
|
+
return String(opt)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractIcon(opt: ItemToggleOption): string | undefined {
|
|
52
|
+
if (opt && typeof opt === 'object') {
|
|
53
|
+
const o = opt as { icon?: string }
|
|
54
|
+
return o.icon
|
|
55
|
+
}
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleClick(event: MouseEvent, opt: ItemToggleOption) {
|
|
60
|
+
event.stopPropagation()
|
|
61
|
+
const value = extractValue(opt)
|
|
62
|
+
if (value === currentValue) return
|
|
63
|
+
onchange?.(value, opt, proxy)
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<ItemContent {proxy} {showIcon} {showSubtext} />
|
|
68
|
+
<span
|
|
69
|
+
data-item-toggle
|
|
70
|
+
data-item-toggle-size={size}
|
|
71
|
+
role="radiogroup"
|
|
72
|
+
class={className || undefined}
|
|
73
|
+
>
|
|
74
|
+
{#each options as opt (extractValue(opt))}
|
|
75
|
+
{@const value = extractValue(opt)}
|
|
76
|
+
{@const label = extractLabel(opt)}
|
|
77
|
+
{@const icon = extractIcon(opt)}
|
|
78
|
+
{@const selected = value === currentValue}
|
|
79
|
+
<span
|
|
80
|
+
data-item-toggle-option
|
|
81
|
+
data-selected={selected || undefined}
|
|
82
|
+
role="radio"
|
|
83
|
+
tabindex="-1"
|
|
84
|
+
aria-checked={selected}
|
|
85
|
+
aria-label={label}
|
|
86
|
+
onclick={(event) => handleClick(event, opt)}
|
|
87
|
+
onkeydown={(event) => {
|
|
88
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
89
|
+
event.preventDefault()
|
|
90
|
+
event.stopPropagation()
|
|
91
|
+
handleClick(event as unknown as MouseEvent, opt)
|
|
92
|
+
}
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{#if icon}
|
|
96
|
+
<span data-item-toggle-icon class={icon} aria-hidden="true"></span>
|
|
97
|
+
{/if}
|
|
98
|
+
{#if showLabels}
|
|
99
|
+
<span data-item-toggle-label>{label}</span>
|
|
100
|
+
{/if}
|
|
101
|
+
</span>
|
|
102
|
+
{/each}
|
|
103
|
+
</span>
|
package/src/components/index.ts
CHANGED
|
@@ -20,6 +20,8 @@ export { default as Tree } from './Tree.svelte'
|
|
|
20
20
|
export { default as LazyTree } from './LazyTree.svelte'
|
|
21
21
|
export { default as Connector } from './Connector.svelte'
|
|
22
22
|
export { default as ItemContent } from './ItemContent.svelte'
|
|
23
|
+
export { default as ItemSwitch } from './ItemSwitch.svelte'
|
|
24
|
+
export { default as ItemToggle } from './ItemToggle.svelte'
|
|
23
25
|
export { default as FloatingAction } from './FloatingAction.svelte'
|
|
24
26
|
export { default as PaletteManager } from './PaletteManager.svelte'
|
|
25
27
|
export { default as Tilt } from './Tilt.svelte'
|
|
@@ -54,3 +56,8 @@ export { default as Avatar } from './Avatar.svelte'
|
|
|
54
56
|
export { default as Tooltip } from './Tooltip.svelte'
|
|
55
57
|
export { default as NavContent } from './NavContent.svelte'
|
|
56
58
|
export { default as ResponsiveGrid } from './ResponsiveGrid.svelte'
|
|
59
|
+
export { default as ChatMessage } from './ChatMessage.svelte'
|
|
60
|
+
export { default as ChatTimeline } from './ChatTimeline.svelte'
|
|
61
|
+
export { default as ChatComposer } from './ChatComposer.svelte'
|
|
62
|
+
export { default as ChatHistory } from './ChatHistory.svelte'
|
|
63
|
+
export { default as ChatShell } from './ChatShell.svelte'
|
package/src/index.ts
CHANGED
|
@@ -54,7 +54,12 @@ export {
|
|
|
54
54
|
Tooltip,
|
|
55
55
|
NavContent,
|
|
56
56
|
ResponsiveGrid,
|
|
57
|
-
CommandPalette
|
|
57
|
+
CommandPalette,
|
|
58
|
+
ChatMessage,
|
|
59
|
+
ChatTimeline,
|
|
60
|
+
ChatComposer,
|
|
61
|
+
ChatHistory,
|
|
62
|
+
ChatShell
|
|
58
63
|
} from './components/index.js'
|
|
59
64
|
|
|
60
65
|
export { default as MarkdownRenderer } from './MarkdownRenderer.svelte'
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte'
|
|
2
|
+
|
|
3
|
+
export type ChatRole = 'user' | 'assistant' | 'system'
|
|
4
|
+
export type ChatStatus = 'streaming' | 'error' | 'done'
|
|
5
|
+
|
|
6
|
+
export interface ChatMessage<T = unknown> {
|
|
7
|
+
id: string
|
|
8
|
+
role: ChatRole
|
|
9
|
+
/** Default body, rendered as markdown by the built-in body snippet. */
|
|
10
|
+
text?: string
|
|
11
|
+
timestamp?: string
|
|
12
|
+
/** Drives the optional typing/error affordance; consumer sets it. */
|
|
13
|
+
status?: ChatStatus
|
|
14
|
+
/** Arbitrary payload (parts/blocks/chart spec/form schema) the consumer's `message` snippet reads. */
|
|
15
|
+
data?: T
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ConversationSummary {
|
|
19
|
+
id: string
|
|
20
|
+
title: string
|
|
21
|
+
timestamp?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ChatMessageProps {
|
|
25
|
+
message: ChatMessage
|
|
26
|
+
relativeTime?: boolean
|
|
27
|
+
body?: Snippet<[ChatMessage]>
|
|
28
|
+
avatar?: Snippet<[ChatMessage]>
|
|
29
|
+
label?: Snippet<[ChatMessage]>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ChatComposerProps {
|
|
33
|
+
value?: string
|
|
34
|
+
placeholder?: string
|
|
35
|
+
disabled?: boolean
|
|
36
|
+
busy?: boolean
|
|
37
|
+
onsubmit?: (text: string) => void
|
|
38
|
+
onchange?: (value: string) => void
|
|
39
|
+
suggestions?: Snippet
|
|
40
|
+
toolbar?: Snippet
|
|
41
|
+
leading?: Snippet
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ChatHistoryProps {
|
|
45
|
+
conversations?: ConversationSummary[]
|
|
46
|
+
activeId?: string | null
|
|
47
|
+
relativeTime?: boolean
|
|
48
|
+
onselect?: (id: string) => void
|
|
49
|
+
onnew?: () => void
|
|
50
|
+
ondelete?: (id: string) => void
|
|
51
|
+
item?: Snippet<[ConversationSummary]>
|
|
52
|
+
empty?: Snippet
|
|
53
|
+
header?: Snippet
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ChatTimelineProps {
|
|
57
|
+
messages?: ChatMessage[]
|
|
58
|
+
relativeTime?: boolean
|
|
59
|
+
autoscroll?: boolean
|
|
60
|
+
message?: Snippet<[ChatMessage]>
|
|
61
|
+
empty?: Snippet
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ChatShellProps {
|
|
65
|
+
messages?: ChatMessage[]
|
|
66
|
+
conversations?: ConversationSummary[]
|
|
67
|
+
activeConversationId?: string | null
|
|
68
|
+
value?: string
|
|
69
|
+
placeholder?: string
|
|
70
|
+
busy?: boolean
|
|
71
|
+
onsubmit?: (text: string) => void
|
|
72
|
+
onchange?: (value: string) => void
|
|
73
|
+
onselectConversation?: (id: string) => void
|
|
74
|
+
onnew?: () => void
|
|
75
|
+
message?: Snippet<[ChatMessage]>
|
|
76
|
+
suggestions?: Snippet
|
|
77
|
+
toolbar?: Snippet
|
|
78
|
+
leading?: Snippet
|
|
79
|
+
historyItem?: Snippet<[ConversationSummary]>
|
|
80
|
+
empty?: Snippet
|
|
81
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -64,6 +64,12 @@ export * from './tooltip.js'
|
|
|
64
64
|
// ItemContent Types
|
|
65
65
|
export * from './item-content.js'
|
|
66
66
|
|
|
67
|
+
// ItemSwitch Types
|
|
68
|
+
export * from './item-switch.js'
|
|
69
|
+
|
|
70
|
+
// ItemToggle Types
|
|
71
|
+
export * from './item-toggle.js'
|
|
72
|
+
|
|
67
73
|
// StatusList Types
|
|
68
74
|
export * from './status-list.js'
|
|
69
75
|
|
|
@@ -126,3 +132,6 @@ export * from './stepper.js'
|
|
|
126
132
|
|
|
127
133
|
// Tilt Types
|
|
128
134
|
export * from './tilt.js'
|
|
135
|
+
|
|
136
|
+
// Chat Types
|
|
137
|
+
export * from './chat.js'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ItemSwitch Component Types
|
|
3
|
+
*
|
|
4
|
+
* Renders a switch-shaped visual inside a data-driven item snippet
|
|
5
|
+
* (List / Menu / Tree). The whole row is the interactive target — clicking it
|
|
6
|
+
* toggles the underlying data through the parent's onselect flow. ItemSwitch
|
|
7
|
+
* itself renders inert markup (spans + attributes), so it never introduces a
|
|
8
|
+
* nested interactive element inside the item's `<button>` wrapper.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ProxyItem } from '@rokkit/states'
|
|
12
|
+
|
|
13
|
+
export interface ItemSwitchProps {
|
|
14
|
+
/** ProxyItem for the current row. */
|
|
15
|
+
proxy: ProxyItem
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Field on the item that holds the boolean checked state.
|
|
19
|
+
* @default 'checked'
|
|
20
|
+
*/
|
|
21
|
+
field?: string
|
|
22
|
+
|
|
23
|
+
/** Size variant — feeds into `data-switch-size` for theming. */
|
|
24
|
+
size?: 'sm' | 'md' | 'lg'
|
|
25
|
+
|
|
26
|
+
/** Render the leading avatar / icon (via ItemContent). @default true */
|
|
27
|
+
showIcon?: boolean
|
|
28
|
+
|
|
29
|
+
/** Render subtext under the label (via ItemContent). @default true */
|
|
30
|
+
showSubtext?: boolean
|
|
31
|
+
|
|
32
|
+
/** Optional class on the wrapper span. */
|
|
33
|
+
class?: string
|
|
34
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ItemToggle Component Types
|
|
3
|
+
*
|
|
4
|
+
* Renders a compact toggle-group visual inside a data-driven item snippet.
|
|
5
|
+
* Options come from the item's data (via `optionsField`). Each option is a
|
|
6
|
+
* span with `role="radio"` and its own click handler — Navigator defers to
|
|
7
|
+
* these nested interactives via its isNestedInteractive guard.
|
|
8
|
+
*
|
|
9
|
+
* Consumers wire state via `onchange`. ItemToggle does not mutate the item
|
|
10
|
+
* data directly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ProxyItem } from '@rokkit/states'
|
|
14
|
+
|
|
15
|
+
export type ItemToggleOption = string | number | boolean | { label?: string; value?: unknown; icon?: string }
|
|
16
|
+
|
|
17
|
+
export interface ItemToggleProps {
|
|
18
|
+
/** ProxyItem for the current row. */
|
|
19
|
+
proxy: ProxyItem
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Field on the item that holds the currently selected value.
|
|
23
|
+
* @default 'value'
|
|
24
|
+
*/
|
|
25
|
+
field?: string
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Field on the item that holds the array of options.
|
|
29
|
+
* @default 'options'
|
|
30
|
+
*/
|
|
31
|
+
optionsField?: string
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Called when the user picks an option. Consumers update the item data.
|
|
35
|
+
*/
|
|
36
|
+
onchange?: (value: unknown, option: ItemToggleOption, proxy: ProxyItem) => void
|
|
37
|
+
|
|
38
|
+
/** Size variant. */
|
|
39
|
+
size?: 'sm' | 'md' | 'lg'
|
|
40
|
+
|
|
41
|
+
/** Render the leading avatar / icon (via ItemContent). @default true */
|
|
42
|
+
showIcon?: boolean
|
|
43
|
+
|
|
44
|
+
/** Render subtext under the label (via ItemContent). @default true */
|
|
45
|
+
showSubtext?: boolean
|
|
46
|
+
|
|
47
|
+
/** Show textual labels beside the option (falls back to option value). @default true */
|
|
48
|
+
showLabels?: boolean
|
|
49
|
+
|
|
50
|
+
/** Optional class on the wrapper span. */
|
|
51
|
+
class?: string
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Format an ISO timestamp as a short relative label ("5m ago"), or a clock time when relative=false. */
|
|
2
|
+
export function formatRelativeTime(timestamp: string, relative = true): string {
|
|
3
|
+
const ms = new Date(timestamp).getTime()
|
|
4
|
+
if (Number.isNaN(ms)) return ''
|
|
5
|
+
if (!relative) return new Date(ms).toLocaleTimeString()
|
|
6
|
+
const minutes = Math.round((Date.now() - ms) / 60000)
|
|
7
|
+
if (minutes < 1) return 'just now'
|
|
8
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
9
|
+
const hours = Math.round(minutes / 60)
|
|
10
|
+
if (hours < 24) return `${hours}h ago`
|
|
11
|
+
return `${Math.round(hours / 24)}d ago`
|
|
12
|
+
}
|