@rokkit/ui 1.3.1 → 1.3.2

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.
@@ -0,0 +1,6 @@
1
+ import type { ChatComposerProps } from '../types/chat.js';
2
+ declare const ChatComposer: import("svelte").Component<ChatComposerProps, {
3
+ focus: () => void;
4
+ }, "value">;
5
+ type ChatComposer = ReturnType<typeof ChatComposer>;
6
+ export default ChatComposer;
@@ -0,0 +1,4 @@
1
+ import type { ChatHistoryProps } from '../types/chat.js';
2
+ declare const ChatHistory: import("svelte").Component<ChatHistoryProps, {}, "">;
3
+ type ChatHistory = ReturnType<typeof ChatHistory>;
4
+ export default ChatHistory;
@@ -0,0 +1,4 @@
1
+ import type { ChatMessageProps } from '../types/chat.js';
2
+ declare const ChatMessage: import("svelte").Component<ChatMessageProps, {}, "">;
3
+ type ChatMessage = ReturnType<typeof ChatMessage>;
4
+ export default ChatMessage;
@@ -0,0 +1,4 @@
1
+ import type { ChatShellProps } from '../types/chat.js';
2
+ declare const ChatShell: import("svelte").Component<ChatShellProps, {}, "value">;
3
+ type ChatShell = ReturnType<typeof ChatShell>;
4
+ export default ChatShell;
@@ -0,0 +1,4 @@
1
+ import type { ChatTimelineProps } from '../types/chat.js';
2
+ declare const ChatTimeline: import("svelte").Component<ChatTimelineProps, {}, "">;
3
+ type ChatTimeline = ReturnType<typeof ChatTimeline>;
4
+ export default ChatTimeline;
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ /** Format an ISO timestamp as a short relative label ("5m ago"), or a clock time when relative=false. */
2
+ export declare function formatRelativeTime(timestamp: string, relative?: boolean): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/ui",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
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.1",
49
- "@rokkit/data": "1.3.1",
50
- "@rokkit/states": "1.3.1",
51
- "@rokkit/actions": "1.3.1",
48
+ "@rokkit/core": "1.3.2",
49
+ "@rokkit/data": "1.3.2",
50
+ "@rokkit/states": "1.3.2",
51
+ "@rokkit/actions": "1.3.2",
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>
@@ -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
+ }
@@ -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
+ }