@echothink-ui/documents 0.1.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.
@@ -0,0 +1,179 @@
1
+ import * as React from "react";
2
+ import {
3
+ Button,
4
+ IconButton,
5
+ LinkButton,
6
+ type EthAction,
7
+ type EthDensity
8
+ } from "@echothink-ui/core";
9
+ import type { DocumentComponentBaseProps, DocumentMode } from "./types";
10
+ import { actionIcon, cx, modeLabel } from "./utils";
11
+
12
+ export interface DocumentToolbarProps extends Omit<
13
+ DocumentComponentBaseProps<HTMLDivElement>,
14
+ "children" | "title"
15
+ > {
16
+ actions: EthAction[];
17
+ formattingActions?: EthAction[];
18
+ mode?: DocumentMode;
19
+ onModeChange?: (mode: DocumentMode) => void;
20
+ density?: EthDensity;
21
+ }
22
+
23
+ const modes: DocumentMode[] = ["edit", "review", "view"];
24
+ type ToolbarActionVariant = "icon" | "button";
25
+
26
+ export function DocumentToolbar({
27
+ actions,
28
+ formattingActions = [],
29
+ mode,
30
+ onModeChange,
31
+ density = "default",
32
+ className,
33
+ ...props
34
+ }: DocumentToolbarProps) {
35
+ return (
36
+ <div
37
+ {...props}
38
+ className={cx("eth-doc-toolbar", className)}
39
+ role="toolbar"
40
+ aria-label="Document toolbar"
41
+ data-eth-component="DocumentToolbar"
42
+ >
43
+ {formattingActions.length ? (
44
+ <div
45
+ className="eth-doc-toolbar__group eth-doc-toolbar__group--formatting"
46
+ role="group"
47
+ aria-label="Formatting"
48
+ >
49
+ {formattingActions.map((action) => renderToolbarAction(action, density, "icon"))}
50
+ </div>
51
+ ) : null}
52
+
53
+ {mode && onModeChange ? (
54
+ <div
55
+ className="eth-doc-toolbar__group eth-doc-toolbar__group--mode"
56
+ role="group"
57
+ aria-label="Document mode"
58
+ >
59
+ {modes.map((candidate) => (
60
+ <Button
61
+ key={candidate}
62
+ className="eth-doc-toolbar__mode-button"
63
+ density={density}
64
+ intent={candidate === mode ? "primary" : "secondary"}
65
+ aria-pressed={candidate === mode}
66
+ onClick={() => onModeChange(candidate)}
67
+ >
68
+ {modeLabel(candidate)}
69
+ </Button>
70
+ ))}
71
+ </div>
72
+ ) : null}
73
+
74
+ {actions.length ? (
75
+ <div
76
+ className="eth-doc-toolbar__group eth-doc-toolbar__group--actions"
77
+ role="group"
78
+ aria-label="Document actions"
79
+ >
80
+ {actions.map((action) => renderToolbarAction(action, density, "button"))}
81
+ </div>
82
+ ) : null}
83
+ </div>
84
+ );
85
+ }
86
+
87
+ function renderToolbarAction(
88
+ action: EthAction,
89
+ density: EthDensity,
90
+ variant: ToolbarActionVariant
91
+ ) {
92
+ const icon = actionIcon(action);
93
+
94
+ if (variant === "button") {
95
+ if (action.href) {
96
+ return (
97
+ <LinkButton
98
+ key={action.id}
99
+ href={action.href}
100
+ density={density}
101
+ intent={action.intent ?? "secondary"}
102
+ aria-disabled={action.disabled}
103
+ tabIndex={action.disabled ? -1 : undefined}
104
+ className={cx(
105
+ "eth-doc-toolbar__action",
106
+ action.disabled && "eth-doc-toolbar__link--disabled"
107
+ )}
108
+ onClick={(event) => {
109
+ if (action.disabled) {
110
+ event.preventDefault();
111
+ return;
112
+ }
113
+ action.onSelect?.();
114
+ }}
115
+ >
116
+ <span className="eth-doc-toolbar__action-icon" aria-hidden="true">
117
+ {icon}
118
+ </span>
119
+ <span>{action.label}</span>
120
+ </LinkButton>
121
+ );
122
+ }
123
+
124
+ return (
125
+ <Button
126
+ key={action.id}
127
+ className="eth-doc-toolbar__action"
128
+ density={density}
129
+ intent={action.intent ?? "secondary"}
130
+ icon={icon}
131
+ disabled={action.disabled}
132
+ onClick={action.onSelect}
133
+ >
134
+ {action.label}
135
+ </Button>
136
+ );
137
+ }
138
+
139
+ if (action.href) {
140
+ return (
141
+ <a
142
+ key={action.id}
143
+ href={action.href}
144
+ aria-label={action.label}
145
+ tabIndex={action.disabled ? -1 : undefined}
146
+ className={cx(
147
+ "eth-button",
148
+ "eth-icon-button",
149
+ "eth-doc-toolbar__icon-link",
150
+ `eth-button--${action.intent ?? "ghost"}`,
151
+ `eth-button--${density}`,
152
+ action.disabled && "eth-doc-toolbar__link--disabled"
153
+ )}
154
+ aria-disabled={action.disabled}
155
+ onClick={(event) => {
156
+ if (action.disabled) {
157
+ event.preventDefault();
158
+ return;
159
+ }
160
+ action.onSelect?.();
161
+ }}
162
+ >
163
+ <span className="eth-button__icon">{icon}</span>
164
+ </a>
165
+ );
166
+ }
167
+
168
+ return (
169
+ <IconButton
170
+ key={action.id}
171
+ density={density}
172
+ intent={action.intent ?? "ghost"}
173
+ label={action.label}
174
+ icon={icon}
175
+ disabled={action.disabled}
176
+ onClick={action.onSelect}
177
+ />
178
+ );
179
+ }
@@ -0,0 +1,81 @@
1
+ import * as React from "react";
2
+ import { Badge } from "@echothink-ui/core";
3
+ import type { DocumentAnnotation, DocumentComponentBaseProps, DocumentMetadataItem } from "./types";
4
+ import { cx, formatDateTime } from "./utils";
5
+
6
+ export interface DocumentViewerProps extends Omit<
7
+ DocumentComponentBaseProps<HTMLElement>,
8
+ "children"
9
+ > {
10
+ title: React.ReactNode;
11
+ content: React.ReactNode;
12
+ metadata?: DocumentMetadataItem[];
13
+ versionRef?: React.ReactNode;
14
+ annotations?: DocumentAnnotation[];
15
+ }
16
+
17
+ export function DocumentViewer({
18
+ title,
19
+ content,
20
+ metadata = [],
21
+ versionRef,
22
+ annotations = [],
23
+ className,
24
+ ...props
25
+ }: DocumentViewerProps) {
26
+ const titleId = React.useId();
27
+ const meta = versionRef ? [...metadata, { label: "Version", value: versionRef }] : metadata;
28
+ const labelledBy = props["aria-labelledby"] ?? titleId;
29
+
30
+ return (
31
+ <article
32
+ {...props}
33
+ aria-labelledby={labelledBy}
34
+ className={cx(
35
+ "eth-doc-viewer",
36
+ annotations.length > 0 && "eth-doc-viewer--with-annotations",
37
+ className
38
+ )}
39
+ data-eth-component="DocumentViewer"
40
+ >
41
+ <header className="eth-doc-viewer__header">
42
+ <h2 id={titleId}>{title}</h2>
43
+ </header>
44
+
45
+ {meta.length ? (
46
+ <dl className="eth-meta-grid eth-doc-viewer__metadata">
47
+ {meta.map((item, index) => (
48
+ <div key={`${String(item.label)}-${index}`}>
49
+ <dt>{item.label}</dt>
50
+ <dd>{item.value}</dd>
51
+ </div>
52
+ ))}
53
+ </dl>
54
+ ) : null}
55
+
56
+ <div className="eth-doc-viewer__body">
57
+ <div className="eth-doc-viewer__content">{content}</div>
58
+
59
+ {annotations.length ? (
60
+ <aside className="eth-doc-viewer__annotations" aria-label="Annotations">
61
+ <h3>Annotations</h3>
62
+ <ul>
63
+ {annotations.map((annotation) => (
64
+ <li key={annotation.id} className="eth-doc-viewer__annotation">
65
+ <div className="eth-doc-viewer__annotation-header">
66
+ <strong>{annotation.author}</strong>
67
+ {annotation.range ? <Badge severity="neutral">{annotation.range}</Badge> : null}
68
+ <time dateTime={annotation.createdAt}>
69
+ {formatDateTime(annotation.createdAt)}
70
+ </time>
71
+ </div>
72
+ <p>{annotation.comment}</p>
73
+ </li>
74
+ ))}
75
+ </ul>
76
+ </aside>
77
+ ) : null}
78
+ </div>
79
+ </article>
80
+ );
81
+ }
@@ -0,0 +1,109 @@
1
+ import * as React from "react";
2
+ import { EmptyState, type EthAction } from "@echothink-ui/core";
3
+ import { DocumentEditorShell } from "./DocumentEditorShell";
4
+ import { DocumentOutline } from "./DocumentOutline";
5
+ import { DocumentToolbar } from "./DocumentToolbar";
6
+ import { DocumentViewer } from "./DocumentViewer";
7
+ import type {
8
+ DocumentAnnotation,
9
+ DocumentComponentBaseProps,
10
+ DocumentHeading,
11
+ DocumentLockState,
12
+ DocumentMetadataItem,
13
+ DocumentMode,
14
+ DocumentOwner,
15
+ DocumentReference
16
+ } from "./types";
17
+
18
+ export interface DocumentWorkspaceTemplateProps
19
+ extends Omit<DocumentComponentBaseProps<HTMLElement>, "children"> {
20
+ title: React.ReactNode;
21
+ documentRef: DocumentReference;
22
+ mode?: DocumentMode;
23
+ lockState?: DocumentLockState;
24
+ lockOwner?: DocumentOwner;
25
+ actions?: EthAction[];
26
+ formattingActions?: EthAction[];
27
+ onModeChange?: (mode: DocumentMode) => void;
28
+ headings?: DocumentHeading[];
29
+ activeHeadingId?: string;
30
+ onSelectHeading?: (id: string) => void;
31
+ inspector?: React.ReactNode;
32
+ toolbar?: React.ReactNode;
33
+ metadata?: DocumentMetadataItem[];
34
+ versionRef?: React.ReactNode;
35
+ annotations?: DocumentAnnotation[];
36
+ content?: React.ReactNode;
37
+ children?: React.ReactNode;
38
+ }
39
+
40
+ export function DocumentWorkspaceTemplate({
41
+ title,
42
+ documentRef,
43
+ mode = "edit",
44
+ lockState = "unlocked",
45
+ lockOwner,
46
+ actions = [],
47
+ formattingActions,
48
+ onModeChange,
49
+ headings = [],
50
+ activeHeadingId,
51
+ onSelectHeading,
52
+ inspector,
53
+ toolbar,
54
+ metadata,
55
+ versionRef,
56
+ annotations,
57
+ content,
58
+ children,
59
+ density,
60
+ ...props
61
+ }: DocumentWorkspaceTemplateProps) {
62
+ const outline = headings.length ? (
63
+ <DocumentOutline
64
+ headings={headings}
65
+ activeHeadingId={activeHeadingId}
66
+ onSelect={onSelectHeading}
67
+ />
68
+ ) : null;
69
+ const resolvedToolbar =
70
+ toolbar ?? (
71
+ <DocumentToolbar
72
+ actions={actions}
73
+ formattingActions={formattingActions}
74
+ mode={mode}
75
+ onModeChange={onModeChange}
76
+ density={density}
77
+ />
78
+ );
79
+ const body =
80
+ mode === "view" && content !== undefined ? (
81
+ <DocumentViewer
82
+ title={title}
83
+ content={content}
84
+ metadata={metadata}
85
+ versionRef={versionRef}
86
+ annotations={annotations}
87
+ />
88
+ ) : (
89
+ children ?? content ?? <EmptyState title="No document content" />
90
+ );
91
+
92
+ return (
93
+ <DocumentEditorShell
94
+ {...props}
95
+ title={title}
96
+ documentRef={documentRef}
97
+ mode={mode}
98
+ lockState={lockState}
99
+ lockOwner={lockOwner}
100
+ toolbar={resolvedToolbar}
101
+ outline={outline}
102
+ inspector={inspector}
103
+ density={density}
104
+ data-eth-component="DocumentWorkspaceTemplate"
105
+ >
106
+ {body}
107
+ </DocumentEditorShell>
108
+ );
109
+ }
@@ -0,0 +1,63 @@
1
+ import type * as React from "react";
2
+ import type { EthAction, EthDensity } from "@echothink-ui/core";
3
+
4
+ export type DocumentMode = "edit" | "review" | "view";
5
+
6
+ export type DocumentLockState =
7
+ | "unlocked"
8
+ | "locked-by-user"
9
+ | "locked-by-agent"
10
+ | "locked-by-other";
11
+
12
+ export interface DocumentOwner {
13
+ id: string;
14
+ label: string;
15
+ }
16
+
17
+ export type DocumentReference =
18
+ | string
19
+ | {
20
+ id: string;
21
+ label?: React.ReactNode;
22
+ href?: string;
23
+ kind?: string;
24
+ version?: string;
25
+ };
26
+
27
+ export interface DocumentMetadataItem {
28
+ label: React.ReactNode;
29
+ value: React.ReactNode;
30
+ }
31
+
32
+ export interface DocumentHeading {
33
+ id: string;
34
+ level: 1 | 2 | 3 | 4;
35
+ text: React.ReactNode;
36
+ href?: string;
37
+ }
38
+
39
+ export interface DocumentAnnotation {
40
+ id: string;
41
+ range?: string;
42
+ comment: React.ReactNode;
43
+ author: React.ReactNode;
44
+ createdAt: string;
45
+ }
46
+
47
+ export interface DocumentPendingChange {
48
+ id: string;
49
+ summary: React.ReactNode;
50
+ diffSnippet?: React.ReactNode;
51
+ }
52
+
53
+ export interface DocumentComponentBaseProps<T extends HTMLElement = HTMLElement>
54
+ extends Omit<React.HTMLAttributes<T>, "title" | "content" | "onSelect"> {
55
+ title?: React.ReactNode;
56
+ density?: EthDensity;
57
+ "data-eth-component"?: string;
58
+ }
59
+
60
+ export interface DocumentToolbarActionGroup {
61
+ actions: EthAction[];
62
+ formattingActions?: EthAction[];
63
+ }
@@ -0,0 +1,88 @@
1
+ import * as React from "react";
2
+ import type { EthAction } from "@echothink-ui/core";
3
+ import {
4
+ AgentRunningIcon,
5
+ ApprovalRequiredIcon,
6
+ DocumentIcon,
7
+ DownloadIcon,
8
+ ExternalLinkIcon,
9
+ SearchIcon,
10
+ StatusIcon
11
+ } from "@echothink-ui/icons";
12
+ import type { DocumentMode, DocumentReference } from "./types";
13
+
14
+ export function cx(...classes: Array<string | false | null | undefined>) {
15
+ return classes.filter(Boolean).join(" ");
16
+ }
17
+
18
+ export function documentRefLabel(documentRef: DocumentReference) {
19
+ if (typeof documentRef === "string") return documentRef;
20
+ if (documentRef.label) return documentRef.label;
21
+ return documentRef.id;
22
+ }
23
+
24
+ export function documentRefId(documentRef: DocumentReference) {
25
+ return typeof documentRef === "string" ? documentRef : documentRef.id;
26
+ }
27
+
28
+ export function formatDateTime(value?: string) {
29
+ if (!value) return "Unknown";
30
+ const date = new Date(value);
31
+ if (Number.isNaN(date.getTime())) return value;
32
+ return date.toLocaleString(undefined, {
33
+ dateStyle: "medium",
34
+ timeStyle: "short"
35
+ });
36
+ }
37
+
38
+ export function modeLabel(mode: DocumentMode) {
39
+ return mode.charAt(0).toUpperCase() + mode.slice(1);
40
+ }
41
+
42
+ export function actionIcon(action: EthAction) {
43
+ const key = `${action.id} ${action.label}`.toLowerCase();
44
+ if (key.includes("bold"))
45
+ return (
46
+ <strong className="eth-doc-toolbar__format-symbol" aria-hidden>
47
+ B
48
+ </strong>
49
+ );
50
+ if (key.includes("italic"))
51
+ return (
52
+ <em className="eth-doc-toolbar__format-symbol" aria-hidden>
53
+ I
54
+ </em>
55
+ );
56
+ if (key.includes("underline"))
57
+ return (
58
+ <span
59
+ className="eth-doc-toolbar__format-symbol eth-doc-toolbar__format-symbol--underline"
60
+ aria-hidden
61
+ >
62
+ U
63
+ </span>
64
+ );
65
+ if (key.includes("strike"))
66
+ return (
67
+ <span
68
+ className="eth-doc-toolbar__format-symbol eth-doc-toolbar__format-symbol--strike"
69
+ aria-hidden
70
+ >
71
+ S
72
+ </span>
73
+ );
74
+ if (key.includes("code"))
75
+ return (
76
+ <span className="eth-doc-toolbar__format-symbol" aria-hidden>
77
+ {"<>"}
78
+ </span>
79
+ );
80
+ if (key.includes("download") || key.includes("export")) return <DownloadIcon size={16} />;
81
+ if (key.includes("share") || key.includes("open")) return <ExternalLinkIcon size={16} />;
82
+ if (key.includes("link") || action.href) return <ExternalLinkIcon size={16} />;
83
+ if (key.includes("search") || key.includes("find")) return <SearchIcon size={16} />;
84
+ if (key.includes("agent")) return <AgentRunningIcon size={16} />;
85
+ if (key.includes("approve") || key.includes("review")) return <ApprovalRequiredIcon size={16} />;
86
+ if (key.includes("status")) return <StatusIcon size={16} />;
87
+ return <DocumentIcon size={16} />;
88
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,39 @@
1
+ import "./styles.css";
2
+
3
+ export {
4
+ DocumentEditorShell,
5
+ type DocumentEditorShellProps
6
+ } from "./components/DocumentEditorShell";
7
+ export { DocumentViewer, type DocumentViewerProps } from "./components/DocumentViewer";
8
+ export { DocumentToolbar, type DocumentToolbarProps } from "./components/DocumentToolbar";
9
+ export { DocumentOutline, type DocumentOutlineProps } from "./components/DocumentOutline";
10
+ export { DocumentLockBadge, type DocumentLockBadgeProps } from "./components/DocumentLockBadge";
11
+ export {
12
+ AgentLockedDocumentPanel,
13
+ type AgentLockedDocumentPanelProps
14
+ } from "./components/AgentLockedDocumentPanel";
15
+ export {
16
+ DocumentWorkspaceTemplate,
17
+ type DocumentWorkspaceTemplateProps
18
+ } from "./components/DocumentWorkspaceTemplate";
19
+ export type {
20
+ DocumentAnnotation,
21
+ DocumentHeading,
22
+ DocumentLockState,
23
+ DocumentMetadataItem,
24
+ DocumentMode,
25
+ DocumentOwner,
26
+ DocumentPendingChange,
27
+ DocumentReference
28
+ } from "./components/types";
29
+
30
+ export const DocumentsComponentNames = [
31
+ "DocumentEditorShell",
32
+ "DocumentViewer",
33
+ "DocumentToolbar",
34
+ "DocumentOutline",
35
+ "DocumentLockBadge",
36
+ "AgentLockedDocumentPanel",
37
+ "DocumentWorkspaceTemplate"
38
+ ] as const;
39
+ export type DocumentsComponentName = (typeof DocumentsComponentNames)[number];