@tangle-network/ui 1.0.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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
- package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.js +15 -0
- package/dist/button-CMQuQEW_.d.ts +17 -0
- package/dist/chat.d.ts +232 -0
- package/dist/chat.js +30 -0
- package/dist/chunk-2NFQRQOD.js +1009 -0
- package/dist/chunk-2VH6PUXD.js +186 -0
- package/dist/chunk-34A66VBG.js +214 -0
- package/dist/chunk-3OI2QKFD.js +0 -0
- package/dist/chunk-4CLN43XT.js +45 -0
- package/dist/chunk-54SQQMMM.js +156 -0
- package/dist/chunk-5Z5ZYMOJ.js +0 -0
- package/dist/chunk-66BNMOVT.js +167 -0
- package/dist/chunk-6BGQA4BQ.js +0 -0
- package/dist/chunk-7UO2ZMRQ.js +133 -0
- package/dist/chunk-BX6AQMUS.js +183 -0
- package/dist/chunk-CD53GZOM.js +59 -0
- package/dist/chunk-CSAIKY36.js +54 -0
- package/dist/chunk-EEE55AVS.js +1201 -0
- package/dist/chunk-GYPQXTJU.js +230 -0
- package/dist/chunk-HFL6R6IF.js +37 -0
- package/dist/chunk-HJKCSXCH.js +737 -0
- package/dist/chunk-LISXUB4D.js +1222 -0
- package/dist/chunk-LQS34IGP.js +0 -0
- package/dist/chunk-MKTSMWVD.js +109 -0
- package/dist/chunk-NKDZ7GZE.js +192 -0
- package/dist/chunk-OEX7NZE3.js +321 -0
- package/dist/chunk-Q56BYXQF.js +61 -0
- package/dist/chunk-Q7EIIWTC.js +0 -0
- package/dist/chunk-REJESC5U.js +117 -0
- package/dist/chunk-RQGKSCEZ.js +0 -0
- package/dist/chunk-RQHJBTEU.js +10 -0
- package/dist/chunk-TMFOPHHN.js +299 -0
- package/dist/chunk-XGKULLYE.js +40 -0
- package/dist/chunk-XIHMJ7ZQ.js +614 -0
- package/dist/chunk-YJ2G3XO5.js +1048 -0
- package/dist/chunk-YNN4O57I.js +754 -0
- package/dist/code-block-DjXf8eOG.d.ts +19 -0
- package/dist/document-editor-pane-A5LT5H4N.js +12 -0
- package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
- package/dist/editor.d.ts +120 -0
- package/dist/editor.js +34 -0
- package/dist/files.d.ts +175 -0
- package/dist/files.js +20 -0
- package/dist/hooks.d.ts +56 -0
- package/dist/hooks.js +41 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +446 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +14 -0
- package/dist/message-BHWbxBtT.d.ts +15 -0
- package/dist/openui.d.ts +115 -0
- package/dist/openui.js +12 -0
- package/dist/parts-dj7AcUg0.d.ts +36 -0
- package/dist/primitives.d.ts +332 -0
- package/dist/primitives.js +191 -0
- package/dist/run-PfLmDAox.d.ts +41 -0
- package/dist/run.d.ts +69 -0
- package/dist/run.js +36 -0
- package/dist/sdk-hooks.d.ts +285 -0
- package/dist/sdk-hooks.js +31 -0
- package/dist/stores.d.ts +17 -0
- package/dist/stores.js +76 -0
- package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
- package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
- package/dist/tool-previews.d.ts +48 -0
- package/dist/tool-previews.js +21 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +45 -0
- package/dist/utils.js +32 -0
- package/package.json +193 -0
- package/src/auth/auth.tsx +228 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/login-layout.tsx +46 -0
- package/src/chat/agent-timeline.stories.tsx +429 -0
- package/src/chat/agent-timeline.tsx +360 -0
- package/src/chat/chat-container.tsx +486 -0
- package/src/chat/chat-input.stories.tsx +142 -0
- package/src/chat/chat-input.tsx +389 -0
- package/src/chat/chat-message.stories.tsx +237 -0
- package/src/chat/chat-message.tsx +129 -0
- package/src/chat/index.ts +18 -0
- package/src/chat/message-list.stories.tsx +336 -0
- package/src/chat/message-list.tsx +79 -0
- package/src/chat/thinking-indicator.stories.tsx +56 -0
- package/src/chat/thinking-indicator.tsx +30 -0
- package/src/chat/user-message.stories.tsx +92 -0
- package/src/chat/user-message.tsx +43 -0
- package/src/editor/document-editor-pane.tsx +351 -0
- package/src/editor/editor-provider.tsx +428 -0
- package/src/editor/editor-toolbar.tsx +130 -0
- package/src/editor/index.ts +31 -0
- package/src/editor/markdown-conversion.ts +21 -0
- package/src/editor/markdown-document-editor.tsx +137 -0
- package/src/editor/tiptap-editor.tsx +331 -0
- package/src/editor/use-editor.ts +221 -0
- package/src/files/file-artifact-pane.tsx +183 -0
- package/src/files/file-preview.tsx +342 -0
- package/src/files/file-tabs.tsx +71 -0
- package/src/files/file-tree.tsx +258 -0
- package/src/files/index.ts +17 -0
- package/src/files/rich-file-tree.stories.tsx +104 -0
- package/src/files/rich-file-tree.test.tsx +42 -0
- package/src/files/rich-file-tree.tsx +232 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-auth.ts +153 -0
- package/src/hooks/use-auto-scroll.ts +59 -0
- package/src/hooks/use-dropdown-menu.ts +40 -0
- package/src/hooks/use-live-time.test.tsx +40 -0
- package/src/hooks/use-live-time.ts +27 -0
- package/src/hooks/use-realtime-session.ts +319 -0
- package/src/hooks/use-run-collapse-state.ts +25 -0
- package/src/hooks/use-run-groups.ts +111 -0
- package/src/hooks/use-sdk-session.ts +575 -0
- package/src/hooks/use-sse-stream.ts +475 -0
- package/src/hooks/use-tool-call-stream.ts +96 -0
- package/src/index.ts +14 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown/code-block.tsx +198 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/markdown.stories.tsx +190 -0
- package/src/markdown/markdown.tsx +62 -0
- package/src/openui/index.ts +20 -0
- package/src/openui/openui-artifact-renderer.tsx +542 -0
- package/src/primitives/artifact-pane.tsx +91 -0
- package/src/primitives/avatar.stories.tsx +95 -0
- package/src/primitives/avatar.tsx +47 -0
- package/src/primitives/badge.stories.tsx +57 -0
- package/src/primitives/badge.tsx +97 -0
- package/src/primitives/button.stories.tsx +48 -0
- package/src/primitives/button.tsx +115 -0
- package/src/primitives/card.stories.tsx +53 -0
- package/src/primitives/card.tsx +98 -0
- package/src/primitives/code-block.stories.tsx +115 -0
- package/src/primitives/code-block.tsx +22 -0
- package/src/primitives/design-tokens.stories.tsx +162 -0
- package/src/primitives/dialog.stories.tsx +176 -0
- package/src/primitives/dialog.tsx +137 -0
- package/src/primitives/drop-zone.stories.tsx +123 -0
- package/src/primitives/drop-zone.tsx +131 -0
- package/src/primitives/dropdown-menu.stories.tsx +122 -0
- package/src/primitives/dropdown-menu.tsx +214 -0
- package/src/primitives/empty-state.stories.tsx +81 -0
- package/src/primitives/empty-state.tsx +40 -0
- package/src/primitives/index.ts +118 -0
- package/src/primitives/input.stories.tsx +113 -0
- package/src/primitives/input.tsx +136 -0
- package/src/primitives/label.stories.tsx +84 -0
- package/src/primitives/label.tsx +24 -0
- package/src/primitives/progress.stories.tsx +93 -0
- package/src/primitives/progress.tsx +50 -0
- package/src/primitives/segmented-control.test.tsx +328 -0
- package/src/primitives/segmented-control.tsx +154 -0
- package/src/primitives/select.stories.tsx +164 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
- package/src/primitives/sidebar-drop-zone.tsx +149 -0
- package/src/primitives/skeleton.stories.tsx +79 -0
- package/src/primitives/skeleton.tsx +55 -0
- package/src/primitives/stat-card.stories.tsx +137 -0
- package/src/primitives/stat-card.tsx +97 -0
- package/src/primitives/switch.stories.tsx +85 -0
- package/src/primitives/switch.tsx +28 -0
- package/src/primitives/table.stories.tsx +170 -0
- package/src/primitives/table.tsx +116 -0
- package/src/primitives/tabs.stories.tsx +180 -0
- package/src/primitives/tabs.tsx +71 -0
- package/src/primitives/terminal-display.stories.tsx +191 -0
- package/src/primitives/terminal-display.tsx +189 -0
- package/src/primitives/theme-toggle.stories.tsx +32 -0
- package/src/primitives/theme-toggle.tsx +96 -0
- package/src/primitives/toast.stories.tsx +155 -0
- package/src/primitives/toast.tsx +190 -0
- package/src/primitives/upload-progress.stories.tsx +120 -0
- package/src/primitives/upload-progress.tsx +110 -0
- package/src/run/expanded-tool-detail.stories.tsx +182 -0
- package/src/run/expanded-tool-detail.tsx +186 -0
- package/src/run/index.ts +13 -0
- package/src/run/inline-thinking-item.stories.tsx +136 -0
- package/src/run/inline-thinking-item.tsx +120 -0
- package/src/run/inline-tool-item.stories.tsx +222 -0
- package/src/run/inline-tool-item.tsx +190 -0
- package/src/run/run-group.stories.tsx +322 -0
- package/src/run/run-group.tsx +569 -0
- package/src/run/run-item-primitives.tsx +17 -0
- package/src/run/tool-call-feed.stories.tsx +294 -0
- package/src/run/tool-call-feed.tsx +192 -0
- package/src/run/tool-call-step.stories.tsx +198 -0
- package/src/run/tool-call-step.tsx +240 -0
- package/src/sdk-hooks.ts +38 -0
- package/src/stores/active-sessions-store.ts +455 -0
- package/src/stores/chat-store.ts +43 -0
- package/src/stores/index.ts +2 -0
- package/src/tool-previews/command-preview.tsx +116 -0
- package/src/tool-previews/diff-preview.tsx +85 -0
- package/src/tool-previews/glob-results-preview.tsx +98 -0
- package/src/tool-previews/grep-results-preview.tsx +157 -0
- package/src/tool-previews/index.ts +22 -0
- package/src/tool-previews/preview-primitives.tsx +84 -0
- package/src/tool-previews/question-preview.tsx +101 -0
- package/src/tool-previews/web-search-preview.tsx +117 -0
- package/src/tool-previews/write-file-preview.tsx +80 -0
- package/src/types/branding.ts +11 -0
- package/src/types/index.ts +5 -0
- package/src/types/message.ts +13 -0
- package/src/types/parts.ts +51 -0
- package/src/types/run.ts +56 -0
- package/src/types/tool-display.ts +41 -0
- package/src/utils/copy-text.ts +30 -0
- package/src/utils/format.test.ts +43 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/time-ago.ts +9 -0
- package/src/utils/tool-display.ts +238 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { UserMessage } from './user-message'
|
|
3
|
+
import type { SessionMessage } from '../types/message'
|
|
4
|
+
import type { SessionPart } from '../types/parts'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof UserMessage> = {
|
|
7
|
+
title: 'Chat/UserMessage',
|
|
8
|
+
component: UserMessage,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'fullscreen',
|
|
11
|
+
backgrounds: { default: 'dark' },
|
|
12
|
+
},
|
|
13
|
+
decorators: [
|
|
14
|
+
(Story) => (
|
|
15
|
+
<div className="min-h-screen bg-[var(--bg-root)] p-8">
|
|
16
|
+
<div className="mx-auto max-w-2xl">
|
|
17
|
+
<Story />
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
),
|
|
21
|
+
],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default meta
|
|
25
|
+
type Story = StoryObj<typeof UserMessage>
|
|
26
|
+
|
|
27
|
+
const makeMessage = (id: string): SessionMessage => ({
|
|
28
|
+
id,
|
|
29
|
+
role: 'user',
|
|
30
|
+
time: { created: Date.now() },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const textPart = (text: string): SessionPart => ({ type: 'text', text })
|
|
34
|
+
|
|
35
|
+
export const Simple: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
message: makeMessage('msg-1'),
|
|
38
|
+
parts: [textPart('How do I implement a binary search tree in TypeScript?')],
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const Multiline: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
message: makeMessage('msg-2'),
|
|
45
|
+
parts: [
|
|
46
|
+
textPart(
|
|
47
|
+
`I have a React component that fetches data on mount but the loading state flickers briefly even when data is cached.\n\nHere's what I'm doing:\n\n\`\`\`tsx\nconst [data, setData] = useState(null)\nconst [loading, setLoading] = useState(true)\n\nuseEffect(() => {\n fetchData().then(setData).finally(() => setLoading(false))\n}, [])\n\`\`\`\n\nWhat's the cleanest fix?`,
|
|
48
|
+
),
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ShortQuestion: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
message: makeMessage('msg-3'),
|
|
56
|
+
parts: [textPart('What is the time complexity of quicksort in the worst case?')],
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const WithMarkdown: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
message: makeMessage('msg-4'),
|
|
63
|
+
parts: [
|
|
64
|
+
textPart(
|
|
65
|
+
`Can you refactor this so it uses **async/await** instead of promise chains?\n\nAlso make sure:\n- Error handling is explicit\n- The return type is \`Promise<Result<T, Error>>\`\n- No unhandled rejections`,
|
|
66
|
+
),
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const LongMessage: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
message: makeMessage('msg-5'),
|
|
74
|
+
parts: [
|
|
75
|
+
textPart(
|
|
76
|
+
`I'm building a distributed rate limiter that needs to work across multiple Node.js instances behind a load balancer. The requirements are:\n\n1. Max 100 requests per minute per user\n2. Sliding window (not fixed)\n3. Redis as the shared store\n4. Must handle Redis failure gracefully (fail open)\n5. Sub-millisecond p99 overhead\n\nI've seen the token bucket and leaky bucket algorithms but I'm not sure which is better here. Can you walk me through the implementation in TypeScript using ioredis, and explain the trade-offs?`,
|
|
77
|
+
),
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const WithActions: Story = {
|
|
83
|
+
args: {
|
|
84
|
+
message: makeMessage('msg-6'),
|
|
85
|
+
parts: [textPart('Write a SQL query to find the top 5 customers by revenue this quarter.')],
|
|
86
|
+
actions: (
|
|
87
|
+
<button className="text-xs text-muted-foreground hover:text-foreground">
|
|
88
|
+
Edit
|
|
89
|
+
</button>
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { memo, type ReactNode } from "react";
|
|
2
|
+
import type { SessionMessage } from "../types/message";
|
|
3
|
+
import type { SessionPart } from "../types/parts";
|
|
4
|
+
|
|
5
|
+
export interface UserMessageProps {
|
|
6
|
+
message: SessionMessage;
|
|
7
|
+
parts: SessionPart[];
|
|
8
|
+
actions?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Simple user message bubble.
|
|
13
|
+
* Renders text parts from the user's message.
|
|
14
|
+
*/
|
|
15
|
+
export const UserMessage = memo(({ message, parts, actions }: UserMessageProps) => {
|
|
16
|
+
const textContent = parts
|
|
17
|
+
.filter((p) => p.type === "text")
|
|
18
|
+
.map((p) => (p as { text: string }).text)
|
|
19
|
+
.join("\n");
|
|
20
|
+
|
|
21
|
+
if (!textContent.trim()) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex justify-end">
|
|
25
|
+
<div className="flex max-w-[78%] flex-col items-end gap-2">
|
|
26
|
+
<div className="w-full rounded-[26px] rounded-br-[12px] bg-[var(--brand-primary)] px-4 py-3 text-white shadow-[0_8px_20px_rgba(15,23,42,0.12)]">
|
|
27
|
+
<div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-white/60">
|
|
28
|
+
You
|
|
29
|
+
</div>
|
|
30
|
+
<div className="whitespace-pre-wrap text-[15px] leading-6.5 text-white">
|
|
31
|
+
{textContent}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
{actions ? (
|
|
35
|
+
<div className="flex flex-wrap items-center justify-end gap-1.5 text-xs text-muted-foreground">
|
|
36
|
+
{actions}
|
|
37
|
+
</div>
|
|
38
|
+
) : null}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
UserMessage.displayName = "UserMessage";
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { PencilLine, Save, Users, Wifi, WifiOff } from "lucide-react";
|
|
5
|
+
import { Markdown } from "../markdown/markdown";
|
|
6
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../primitives/tabs";
|
|
7
|
+
import { cn } from "../lib/utils";
|
|
8
|
+
import { ArtifactPane, type ArtifactPaneProps } from "../primitives/artifact-pane";
|
|
9
|
+
import {
|
|
10
|
+
CollaboratorsList,
|
|
11
|
+
TiptapEditor,
|
|
12
|
+
type TiptapEditorProps,
|
|
13
|
+
} from "./tiptap-editor";
|
|
14
|
+
import {
|
|
15
|
+
EditorProvider,
|
|
16
|
+
type ConnectionState,
|
|
17
|
+
type EditorProviderProps,
|
|
18
|
+
} from "./editor-provider";
|
|
19
|
+
import { MarkdownDocumentEditor } from "./markdown-document-editor";
|
|
20
|
+
import {
|
|
21
|
+
htmlToMarkdown,
|
|
22
|
+
markdownToHtml,
|
|
23
|
+
normalizeMarkdown,
|
|
24
|
+
} from "./markdown-conversion";
|
|
25
|
+
import { useCollaborators, useEditorConnection } from "./use-editor";
|
|
26
|
+
|
|
27
|
+
export type DocumentEditorMode = "preview" | "edit";
|
|
28
|
+
export type DocumentEditorBackend = "local" | "collaborative";
|
|
29
|
+
|
|
30
|
+
export interface DocumentEditorPaneCollaborationConfig
|
|
31
|
+
extends Omit<EditorProviderProps, "children"> {}
|
|
32
|
+
|
|
33
|
+
export interface DocumentEditorPaneProps
|
|
34
|
+
extends Omit<ArtifactPaneProps, "children" | "tabs" | "toolbar" | "emptyState"> {
|
|
35
|
+
tabs?: ArtifactPaneProps["tabs"];
|
|
36
|
+
toolbar?: ReactNode;
|
|
37
|
+
markdown?: string;
|
|
38
|
+
mode?: DocumentEditorMode;
|
|
39
|
+
defaultMode?: DocumentEditorMode;
|
|
40
|
+
onModeChange?: (mode: DocumentEditorMode) => void;
|
|
41
|
+
backend?: DocumentEditorBackend;
|
|
42
|
+
placeholder?: string;
|
|
43
|
+
autoFocus?: boolean;
|
|
44
|
+
readOnly?: boolean;
|
|
45
|
+
onChange?: (markdown: string) => void;
|
|
46
|
+
onSave?: (markdown: string) => Promise<void> | void;
|
|
47
|
+
saving?: boolean;
|
|
48
|
+
saveLabel?: string;
|
|
49
|
+
previewClassName?: string;
|
|
50
|
+
editorClassName?: string;
|
|
51
|
+
collaboration?: DocumentEditorPaneCollaborationConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function connectionTone(state: ConnectionState) {
|
|
55
|
+
switch (state) {
|
|
56
|
+
case "synced":
|
|
57
|
+
return "text-[var(--surface-success-text)] border-[var(--surface-success-border)] bg-[var(--surface-success-bg)]";
|
|
58
|
+
case "connected":
|
|
59
|
+
case "connecting":
|
|
60
|
+
return "text-[var(--surface-info-text)] border-[var(--surface-info-border)] bg-[var(--surface-info-bg)]";
|
|
61
|
+
case "disconnected":
|
|
62
|
+
default:
|
|
63
|
+
return "text-[var(--surface-warning-text)] border-[var(--surface-warning-border)] bg-[var(--surface-warning-bg)]";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function connectionLabel(state: ConnectionState) {
|
|
68
|
+
switch (state) {
|
|
69
|
+
case "synced":
|
|
70
|
+
return "Live synced";
|
|
71
|
+
case "connected":
|
|
72
|
+
return "Connected";
|
|
73
|
+
case "connecting":
|
|
74
|
+
return "Connecting";
|
|
75
|
+
case "disconnected":
|
|
76
|
+
default:
|
|
77
|
+
return "Offline";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function connectionDescription(
|
|
82
|
+
state: ConnectionState,
|
|
83
|
+
collaborators: number,
|
|
84
|
+
readOnly?: boolean,
|
|
85
|
+
) {
|
|
86
|
+
if (readOnly) {
|
|
87
|
+
return state === "disconnected"
|
|
88
|
+
? "Live access is paused. You can keep reading while the editor reconnects."
|
|
89
|
+
: "You are viewing the live document in read-only mode.";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
switch (state) {
|
|
93
|
+
case "synced":
|
|
94
|
+
return collaborators > 0
|
|
95
|
+
? `You and ${collaborators} collaborator${
|
|
96
|
+
collaborators === 1 ? "" : "s"
|
|
97
|
+
} are editing the same document.`
|
|
98
|
+
: "You are editing the live document. Changes sync automatically.";
|
|
99
|
+
case "connected":
|
|
100
|
+
case "connecting":
|
|
101
|
+
return "Connecting the live document. Local edits stay in place while sync catches up.";
|
|
102
|
+
case "disconnected":
|
|
103
|
+
default:
|
|
104
|
+
return "Live updates are paused. You can keep editing and reconnect when the transport is healthy again.";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function CollaborativeDocumentSurface({
|
|
109
|
+
markdown,
|
|
110
|
+
placeholder,
|
|
111
|
+
autoFocus,
|
|
112
|
+
readOnly,
|
|
113
|
+
className,
|
|
114
|
+
onChange,
|
|
115
|
+
}: {
|
|
116
|
+
markdown: string;
|
|
117
|
+
placeholder?: string;
|
|
118
|
+
autoFocus?: boolean;
|
|
119
|
+
readOnly?: boolean;
|
|
120
|
+
className?: string;
|
|
121
|
+
onChange?: (markdown: string) => void;
|
|
122
|
+
}) {
|
|
123
|
+
const { state } = useEditorConnection();
|
|
124
|
+
const { collaborators } = useCollaborators();
|
|
125
|
+
const initialContent = useMemo(() => markdownToHtml(markdown), [markdown]);
|
|
126
|
+
const collaboratorCount = collaborators.length + 1;
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className={cn("flex h-full min-h-0 flex-col gap-3", className)}>
|
|
130
|
+
<div className="flex flex-wrap items-center justify-between gap-3 rounded-[var(--radius-lg)] border border-border bg-card px-3 py-2">
|
|
131
|
+
<div className="min-w-0 space-y-2">
|
|
132
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
133
|
+
<span
|
|
134
|
+
className={cn(
|
|
135
|
+
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 font-medium",
|
|
136
|
+
connectionTone(state),
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
{state === "disconnected" ? (
|
|
140
|
+
<WifiOff className="h-3.5 w-3.5" />
|
|
141
|
+
) : (
|
|
142
|
+
<Wifi className="h-3.5 w-3.5" />
|
|
143
|
+
)}
|
|
144
|
+
{connectionLabel(state)}
|
|
145
|
+
</span>
|
|
146
|
+
<span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-background px-2.5 py-1">
|
|
147
|
+
<Users className="h-3.5 w-3.5" />
|
|
148
|
+
{collaborators.length === 0 ? "Solo editing" : `${collaboratorCount} active`}
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
<p className="text-xs text-muted-foreground">
|
|
152
|
+
{connectionDescription(state, collaborators.length, readOnly)}
|
|
153
|
+
</p>
|
|
154
|
+
</div>
|
|
155
|
+
<CollaboratorsList collaborators={collaborators} />
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<TiptapEditor
|
|
159
|
+
initialContent={initialContent}
|
|
160
|
+
placeholder={placeholder}
|
|
161
|
+
autoFocus={autoFocus}
|
|
162
|
+
readOnly={readOnly}
|
|
163
|
+
className={cn("h-full min-h-[16rem]", className)}
|
|
164
|
+
onUpdate={(editor) => {
|
|
165
|
+
onChange?.(normalizeMarkdown(htmlToMarkdown(editor.getHTML())));
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* DocumentEditorPane — reusable markdown document surface with preview/edit
|
|
174
|
+
* modes and optional collaborative editing backed by Yjs/Hocuspocus.
|
|
175
|
+
*/
|
|
176
|
+
export function DocumentEditorPane({
|
|
177
|
+
eyebrow,
|
|
178
|
+
title,
|
|
179
|
+
subtitle,
|
|
180
|
+
meta,
|
|
181
|
+
headerActions,
|
|
182
|
+
footer,
|
|
183
|
+
className,
|
|
184
|
+
contentClassName,
|
|
185
|
+
tabs,
|
|
186
|
+
toolbar,
|
|
187
|
+
markdown = "",
|
|
188
|
+
mode,
|
|
189
|
+
defaultMode = "preview",
|
|
190
|
+
onModeChange,
|
|
191
|
+
backend = "local",
|
|
192
|
+
placeholder = "Start writing...",
|
|
193
|
+
autoFocus = false,
|
|
194
|
+
readOnly = false,
|
|
195
|
+
onChange,
|
|
196
|
+
onSave,
|
|
197
|
+
saving = false,
|
|
198
|
+
saveLabel = "Save changes",
|
|
199
|
+
previewClassName,
|
|
200
|
+
editorClassName,
|
|
201
|
+
collaboration,
|
|
202
|
+
}: DocumentEditorPaneProps) {
|
|
203
|
+
const [draft, setDraft] = useState(markdown);
|
|
204
|
+
const [uncontrolledMode, setUncontrolledMode] =
|
|
205
|
+
useState<DocumentEditorMode>(defaultMode);
|
|
206
|
+
const activeMode = mode ?? uncontrolledMode;
|
|
207
|
+
const isCollaborative = backend === "collaborative" && Boolean(collaboration);
|
|
208
|
+
const isDirty = normalizeMarkdown(draft) !== normalizeMarkdown(markdown);
|
|
209
|
+
const saveStateLabel = readOnly
|
|
210
|
+
? "Read only"
|
|
211
|
+
: isCollaborative
|
|
212
|
+
? isDirty
|
|
213
|
+
? "Snapshot pending"
|
|
214
|
+
: "Live document current"
|
|
215
|
+
: isDirty
|
|
216
|
+
? "Unsaved changes"
|
|
217
|
+
: "Saved";
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
setDraft(markdown);
|
|
221
|
+
}, [markdown]);
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (mode === undefined) {
|
|
225
|
+
setUncontrolledMode(defaultMode);
|
|
226
|
+
}
|
|
227
|
+
}, [defaultMode, mode]);
|
|
228
|
+
|
|
229
|
+
const setMode = (nextMode: DocumentEditorMode) => {
|
|
230
|
+
if (mode === undefined) {
|
|
231
|
+
setUncontrolledMode(nextMode);
|
|
232
|
+
}
|
|
233
|
+
onModeChange?.(nextMode);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const handleChange = (nextMarkdown: string) => {
|
|
237
|
+
setDraft(nextMarkdown);
|
|
238
|
+
onChange?.(nextMarkdown);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const editorToolbar = (
|
|
242
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
243
|
+
<TabsList
|
|
244
|
+
variant="underline"
|
|
245
|
+
className="h-auto gap-4 border-0 bg-transparent p-0 text-muted-foreground"
|
|
246
|
+
>
|
|
247
|
+
<TabsTrigger
|
|
248
|
+
value="preview"
|
|
249
|
+
variant="underline"
|
|
250
|
+
className="pb-2 data-[state=active]:border-primary data-[state=active]:text-foreground"
|
|
251
|
+
>
|
|
252
|
+
Preview
|
|
253
|
+
</TabsTrigger>
|
|
254
|
+
<TabsTrigger
|
|
255
|
+
value="edit"
|
|
256
|
+
variant="underline"
|
|
257
|
+
className="flex items-center gap-2 pb-2 data-[state=active]:border-primary data-[state=active]:text-foreground"
|
|
258
|
+
>
|
|
259
|
+
<PencilLine className="h-3.5 w-3.5" />
|
|
260
|
+
{isCollaborative ? "Live edit" : "Edit"}
|
|
261
|
+
</TabsTrigger>
|
|
262
|
+
</TabsList>
|
|
263
|
+
|
|
264
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
265
|
+
{toolbar}
|
|
266
|
+
<span className="rounded-full border border-border bg-card px-2.5 py-1 font-medium">
|
|
267
|
+
{isCollaborative ? "Live document" : "Local draft"}
|
|
268
|
+
</span>
|
|
269
|
+
<span className="rounded-full border border-border bg-background px-2.5 py-1">
|
|
270
|
+
{saveStateLabel}
|
|
271
|
+
</span>
|
|
272
|
+
{onSave && !readOnly && (
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={() => void onSave(draft)}
|
|
276
|
+
disabled={saving || !isDirty}
|
|
277
|
+
className="inline-flex items-center gap-2 rounded-[var(--radius-full)] border border-border bg-card px-3 py-1.5 text-xs font-semibold text-foreground transition-colors hover:border-primary/40 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
278
|
+
>
|
|
279
|
+
<Save className="h-3.5 w-3.5" />
|
|
280
|
+
{saving ? "Saving..." : saveLabel}
|
|
281
|
+
</button>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const preview = (
|
|
288
|
+
<div
|
|
289
|
+
className={cn(
|
|
290
|
+
"rounded-[var(--radius-lg)] border border-border bg-background p-5",
|
|
291
|
+
previewClassName,
|
|
292
|
+
)}
|
|
293
|
+
>
|
|
294
|
+
<Markdown className="prose-sm max-w-none">{draft}</Markdown>
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const localEditor = (
|
|
299
|
+
<MarkdownDocumentEditor
|
|
300
|
+
value={draft}
|
|
301
|
+
placeholder={placeholder}
|
|
302
|
+
autoFocus={autoFocus}
|
|
303
|
+
readOnly={readOnly}
|
|
304
|
+
onChange={(nextMarkdown) => {
|
|
305
|
+
handleChange(nextMarkdown);
|
|
306
|
+
}}
|
|
307
|
+
className={editorClassName}
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const collaborativeEditor = collaboration ? (
|
|
312
|
+
<EditorProvider key={collaboration.documentName} {...collaboration}>
|
|
313
|
+
<CollaborativeDocumentSurface
|
|
314
|
+
markdown={draft}
|
|
315
|
+
placeholder={placeholder}
|
|
316
|
+
autoFocus={autoFocus}
|
|
317
|
+
readOnly={readOnly}
|
|
318
|
+
className={editorClassName}
|
|
319
|
+
onChange={handleChange}
|
|
320
|
+
/>
|
|
321
|
+
</EditorProvider>
|
|
322
|
+
) : localEditor;
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<Tabs
|
|
326
|
+
value={activeMode}
|
|
327
|
+
onValueChange={(nextValue) => setMode(nextValue as DocumentEditorMode)}
|
|
328
|
+
className="h-full"
|
|
329
|
+
>
|
|
330
|
+
<ArtifactPane
|
|
331
|
+
eyebrow={eyebrow}
|
|
332
|
+
title={title}
|
|
333
|
+
subtitle={subtitle}
|
|
334
|
+
meta={meta}
|
|
335
|
+
headerActions={headerActions}
|
|
336
|
+
footer={footer}
|
|
337
|
+
tabs={tabs}
|
|
338
|
+
className={className}
|
|
339
|
+
contentClassName={contentClassName}
|
|
340
|
+
toolbar={editorToolbar}
|
|
341
|
+
>
|
|
342
|
+
<TabsContent value="preview" className="mt-0 h-full px-4 py-4">
|
|
343
|
+
{preview}
|
|
344
|
+
</TabsContent>
|
|
345
|
+
<TabsContent value="edit" className="mt-0 h-full px-4 py-4">
|
|
346
|
+
{isCollaborative ? collaborativeEditor : localEditor}
|
|
347
|
+
</TabsContent>
|
|
348
|
+
</ArtifactPane>
|
|
349
|
+
</Tabs>
|
|
350
|
+
);
|
|
351
|
+
}
|