basis-design 0.0.0-stage → 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.
- package/LICENSE +177 -0
- package/NOTICE +7 -0
- package/README.md +135 -3
- package/bin/basis.js +6 -0
- package/index.html +16 -0
- package/package.json +66 -4
- package/preview.html +12 -0
- package/server/agent.ts +845 -0
- package/server/brief.ts +355 -0
- package/server/chatlog.ts +57 -0
- package/server/exec.ts +77 -0
- package/server/export.ts +180 -0
- package/server/guidelines.md +379 -0
- package/server/history.ts +107 -0
- package/server/index.ts +414 -0
- package/server/paths.ts +57 -0
- package/server/previews.ts +71 -0
- package/server/project.ts +151 -0
- package/server/projects.ts +213 -0
- package/server/reach.ts +192 -0
- package/server/rebase.ts +46 -0
- package/server/reviews.ts +156 -0
- package/server/setup.ts +219 -0
- package/server/snapshots.ts +67 -0
- package/server/workspace.ts +423 -0
- package/shared/annotations.ts +43 -0
- package/shared/contracts.ts +190 -0
- package/shared/contrast.ts +77 -0
- package/shared/design.ts +556 -0
- package/shared/diff.ts +26 -0
- package/shared/fixed-colors.ts +91 -0
- package/shared/frame.ts +223 -0
- package/shared/pattern-contracts.ts +229 -0
- package/shared/protocol.ts +226 -0
- package/shared/review.ts +145 -0
- package/shared/tokens.ts +102 -0
- package/src/App.tsx +22 -0
- package/src/Workbench.tsx +341 -0
- package/src/app.css +103 -0
- package/src/bridge.ts +163 -0
- package/src/chat/ChatPanel.module.css +759 -0
- package/src/chat/ChatPanel.tsx +326 -0
- package/src/chat/Composer.tsx +290 -0
- package/src/chat/Markdown.tsx +67 -0
- package/src/chat/images.ts +46 -0
- package/src/icon.svg +1 -0
- package/src/main.tsx +10 -0
- package/src/preview/Foundations.module.css +346 -0
- package/src/preview/Foundations.tsx +389 -0
- package/src/preview/FrameBar.tsx +74 -0
- package/src/preview/Gallery.module.css +232 -0
- package/src/preview/Gallery.tsx +163 -0
- package/src/preview/Impact.tsx +217 -0
- package/src/preview/Preview.module.css +103 -0
- package/src/preview/Preview.tsx +167 -0
- package/src/preview/Review.module.css +192 -0
- package/src/preview/Review.tsx +456 -0
- package/src/preview/SectionBoundary.tsx +70 -0
- package/src/preview/a11y.ts +123 -0
- package/src/preview/color.ts +53 -0
- package/src/preview/ds-module.d.ts +2 -0
- package/src/preview/ds.ts +33 -0
- package/src/preview/errors.ts +83 -0
- package/src/preview/main.tsx +283 -0
- package/src/preview/narrow.ts +95 -0
- package/src/preview/picker/PickLayer.module.css +90 -0
- package/src/preview/picker/PickLayer.tsx +151 -0
- package/src/preview/picker/annotate.ts +93 -0
- package/src/preview/preview.css +102 -0
- package/src/preview/progressive.ts +62 -0
- package/src/preview/specimens.tsx +379 -0
- package/src/shell/ChatResizer.tsx +69 -0
- package/src/shell/CreateDialog.tsx +132 -0
- package/src/shell/ExportDialog.tsx +232 -0
- package/src/shell/HistoryMenu.tsx +95 -0
- package/src/shell/InspectBanner.tsx +147 -0
- package/src/shell/PreviewFrame.tsx +101 -0
- package/src/shell/ProjectMenu.tsx +364 -0
- package/src/shell/ReviewBar.tsx +224 -0
- package/src/shell/Shell.module.css +1067 -0
- package/src/shell/Sidebar.module.css +211 -0
- package/src/shell/Sidebar.tsx +80 -0
- package/src/shell/TopBar.tsx +83 -0
- package/src/shell/history.ts +64 -0
- package/src/shell/navigation.tsx +150 -0
- package/src/shell/prefs.ts +81 -0
- package/src/shell/useReview.ts +114 -0
- package/template/LICENSE +16 -0
- package/template/components/Button.module.css +151 -0
- package/template/components/Button.specimen.tsx +74 -0
- package/template/components/Button.tsx +41 -0
- package/template/components/Controls.module.css +174 -0
- package/template/components/Controls.specimen.tsx +54 -0
- package/template/components/Controls.tsx +58 -0
- package/template/components/Display.module.css +251 -0
- package/template/components/Display.specimen.tsx +261 -0
- package/template/components/Display.tsx +126 -0
- package/template/components/Field.module.css +109 -0
- package/template/components/Field.specimen.tsx +138 -0
- package/template/components/Field.tsx +94 -0
- package/template/components/Overlay.module.css +237 -0
- package/template/components/Overlay.specimen.tsx +60 -0
- package/template/components/Overlay.tsx +69 -0
- package/template/components/Tabs.module.css +140 -0
- package/template/components/Tabs.specimen.tsx +51 -0
- package/template/components/Tabs.tsx +117 -0
- package/template/components/contracts.ts +24 -0
- package/template/components/index.ts +7 -0
- package/template/components/specimen.ts +88 -0
- package/template/components/util.ts +6 -0
- package/template/contract-check.ts +22 -0
- package/template/patterns/AppShell.module.css +36 -0
- package/template/patterns/AppShell.specimen.tsx +114 -0
- package/template/patterns/AppShell.tsx +25 -0
- package/template/patterns/Chat.module.css +437 -0
- package/template/patterns/Chat.specimen.tsx +298 -0
- package/template/patterns/Chat.tsx +236 -0
- package/template/patterns/Composer.module.css +184 -0
- package/template/patterns/Composer.specimen.tsx +106 -0
- package/template/patterns/Composer.tsx +183 -0
- package/template/patterns/DataTable.module.css +190 -0
- package/template/patterns/DataTable.specimen.tsx +189 -0
- package/template/patterns/DataTable.tsx +241 -0
- package/template/patterns/EmptyState.module.css +57 -0
- package/template/patterns/EmptyState.specimen.tsx +71 -0
- package/template/patterns/EmptyState.tsx +36 -0
- package/template/patterns/FormSection.module.css +69 -0
- package/template/patterns/FormSection.specimen.tsx +75 -0
- package/template/patterns/FormSection.tsx +40 -0
- package/template/patterns/Navbar.module.css +265 -0
- package/template/patterns/Navbar.specimen.tsx +118 -0
- package/template/patterns/Navbar.tsx +124 -0
- package/template/patterns/PageHeader.module.css +98 -0
- package/template/patterns/PageHeader.specimen.tsx +75 -0
- package/template/patterns/PageHeader.tsx +63 -0
- package/template/patterns/SidebarNav.module.css +186 -0
- package/template/patterns/SidebarNav.specimen.tsx +98 -0
- package/template/patterns/SidebarNav.tsx +81 -0
- package/template/patterns/StatCard.module.css +81 -0
- package/template/patterns/StatCard.specimen.tsx +61 -0
- package/template/patterns/StatCard.tsx +83 -0
- package/template/patterns/contracts.ts +25 -0
- package/template/patterns/icons.tsx +68 -0
- package/template/patterns/index.ts +12 -0
- package/template/patterns/specimen-data.ts +112 -0
- package/template/views/TeamMembers.specimen.tsx +147 -0
- package/vite.config.ts +9 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: grid;
|
|
3
|
+
gap: var(--space-2);
|
|
4
|
+
min-width: 0;
|
|
5
|
+
font-family: var(--font-sans);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.composer {
|
|
9
|
+
display: grid;
|
|
10
|
+
gap: var(--space-2);
|
|
11
|
+
min-width: 0;
|
|
12
|
+
padding: var(--space-3);
|
|
13
|
+
border: var(--input-border-width) solid var(--input-border-color);
|
|
14
|
+
border-radius: var(--radius-lg);
|
|
15
|
+
background: var(--input-bg);
|
|
16
|
+
box-shadow: var(--shadow-sm);
|
|
17
|
+
cursor: text;
|
|
18
|
+
transition: var(--input-transition);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.composer:hover:not([data-disabled], :focus-within) {
|
|
22
|
+
border-color: var(--input-border-color-hover);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.composer:focus-within,
|
|
26
|
+
.composer[data-force="focus"] {
|
|
27
|
+
border-color: var(--color-focus-ring);
|
|
28
|
+
box-shadow:
|
|
29
|
+
var(--shadow-sm),
|
|
30
|
+
0 0 0 var(--focus-ring-width) color-mix(in srgb, var(--color-focus-ring) 30%, transparent);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.composer[data-disabled] {
|
|
34
|
+
border-color: var(--color-border);
|
|
35
|
+
background: var(--color-disabled-bg);
|
|
36
|
+
box-shadow: none;
|
|
37
|
+
cursor: not-allowed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.field {
|
|
41
|
+
display: block;
|
|
42
|
+
width: 100%;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
max-height: calc(var(--composer-rows, 8) * 1lh + var(--space-1));
|
|
45
|
+
margin: 0;
|
|
46
|
+
padding: var(--space-1) var(--space-1) 0;
|
|
47
|
+
border: 0;
|
|
48
|
+
background: transparent;
|
|
49
|
+
color: var(--color-text);
|
|
50
|
+
font-family: inherit;
|
|
51
|
+
font-size: var(--input-font-size);
|
|
52
|
+
line-height: var(--leading-normal);
|
|
53
|
+
overflow-y: auto;
|
|
54
|
+
resize: none;
|
|
55
|
+
outline: none;
|
|
56
|
+
box-sizing: border-box;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.field::placeholder {
|
|
60
|
+
color: var(--color-text-subtle);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.field:disabled {
|
|
64
|
+
color: var(--color-disabled-text);
|
|
65
|
+
cursor: not-allowed;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.attachments {
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-wrap: wrap;
|
|
71
|
+
gap: var(--space-2);
|
|
72
|
+
margin: 0;
|
|
73
|
+
padding: 0;
|
|
74
|
+
list-style: none;
|
|
75
|
+
cursor: default;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.attachment {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: var(--space-2);
|
|
82
|
+
min-width: 0;
|
|
83
|
+
max-width: 240px;
|
|
84
|
+
padding: var(--space-1) var(--space-1) var(--space-1) var(--space-2);
|
|
85
|
+
border: var(--border-thin) solid var(--color-border);
|
|
86
|
+
border-radius: var(--radius-md);
|
|
87
|
+
background: var(--color-surface-sunken);
|
|
88
|
+
color: var(--color-text);
|
|
89
|
+
font-size: var(--text-sm);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.fileIcon {
|
|
93
|
+
display: grid;
|
|
94
|
+
flex: none;
|
|
95
|
+
place-items: center;
|
|
96
|
+
width: 28px;
|
|
97
|
+
height: 28px;
|
|
98
|
+
border-radius: var(--radius-sm);
|
|
99
|
+
background: var(--color-surface);
|
|
100
|
+
color: var(--color-text-muted);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.fileText {
|
|
104
|
+
display: grid;
|
|
105
|
+
min-width: 0;
|
|
106
|
+
line-height: var(--leading-snug);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.fileName {
|
|
110
|
+
overflow: hidden;
|
|
111
|
+
font-weight: var(--weight-medium);
|
|
112
|
+
text-overflow: ellipsis;
|
|
113
|
+
white-space: nowrap;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.fileMeta {
|
|
117
|
+
color: var(--color-text-muted);
|
|
118
|
+
font-size: var(--text-xs);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.attachment[data-uploading] .fileName {
|
|
122
|
+
color: var(--color-text-muted);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.remove {
|
|
126
|
+
display: grid;
|
|
127
|
+
flex: none;
|
|
128
|
+
place-items: center;
|
|
129
|
+
width: 24px;
|
|
130
|
+
height: 24px;
|
|
131
|
+
padding: 0;
|
|
132
|
+
border: 0;
|
|
133
|
+
border-radius: var(--radius-sm);
|
|
134
|
+
background: transparent;
|
|
135
|
+
color: var(--color-text-muted);
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
transition: background-color var(--duration-fast) var(--ease-standard);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.remove:hover:not(:disabled) {
|
|
141
|
+
background: var(--color-secondary);
|
|
142
|
+
color: var(--color-text);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.remove:focus-visible {
|
|
146
|
+
outline: var(--focus-ring-width) solid var(--color-focus-ring);
|
|
147
|
+
outline-offset: 1px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.remove:disabled {
|
|
151
|
+
cursor: not-allowed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.toolbar {
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
gap: var(--space-1);
|
|
158
|
+
min-width: 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.spacer {
|
|
162
|
+
flex: 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Square icon buttons; the attribute keeps this ahead of the button's own size rules. */
|
|
166
|
+
.toolbar .iconButton[data-ds="button"] {
|
|
167
|
+
width: var(--button-height-sm);
|
|
168
|
+
padding-inline: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.stop {
|
|
172
|
+
width: 10px;
|
|
173
|
+
height: 10px;
|
|
174
|
+
border-radius: var(--radius-xs);
|
|
175
|
+
background: currentColor;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.footer {
|
|
179
|
+
margin: 0;
|
|
180
|
+
color: var(--color-text-muted);
|
|
181
|
+
font-size: var(--text-xs);
|
|
182
|
+
line-height: var(--leading-snug);
|
|
183
|
+
text-align: center;
|
|
184
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Button } from "../components/index.ts";
|
|
3
|
+
import { defineSpecimen } from "../components/specimen.ts";
|
|
4
|
+
import { Composer, type ComposerAttachment, type ComposerProps } from "./Composer.tsx";
|
|
5
|
+
import { Icon } from "./icons.tsx";
|
|
6
|
+
|
|
7
|
+
const FILES: ComposerAttachment[] = [
|
|
8
|
+
{ id: "a1", name: "churn-report-q3.pdf", meta: "PDF · 2.4 MB" },
|
|
9
|
+
{ id: "a2", name: "survey-responses.csv", meta: "CSV · 380 KB" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const LONG =
|
|
13
|
+
"Here is what I need for the board update on Thursday.\n\n" +
|
|
14
|
+
"1. A one-paragraph summary of why churn rose in September, using the attached report.\n" +
|
|
15
|
+
"2. The three customer quotes from the survey that explain it best.\n" +
|
|
16
|
+
"3. What we already changed in onboarding, and what we plan next quarter.\n" +
|
|
17
|
+
"4. One chart: monthly churn for the last twelve months, with the pricing change marked.\n\n" +
|
|
18
|
+
"Keep it under a page, plain language, no internal team names. If something in the data looks off, " +
|
|
19
|
+
"point it out instead of smoothing it over.";
|
|
20
|
+
|
|
21
|
+
const mode = (
|
|
22
|
+
<Button variant="ghost" size="sm">
|
|
23
|
+
Balanced
|
|
24
|
+
<Icon name="chevron-down" size={12} />
|
|
25
|
+
</Button>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/** A composer that keeps its own text and attachments, so every state can be typed in and sent. */
|
|
29
|
+
function Live({
|
|
30
|
+
text = "",
|
|
31
|
+
files = [],
|
|
32
|
+
...rest
|
|
33
|
+
}: Omit<ComposerProps, "value" | "attachments"> & { text?: string; files?: ComposerAttachment[] }) {
|
|
34
|
+
const [value, setValue] = useState(text);
|
|
35
|
+
const [attachments, setAttachments] = useState(files);
|
|
36
|
+
return (
|
|
37
|
+
<Composer
|
|
38
|
+
value={value}
|
|
39
|
+
onValueChange={setValue}
|
|
40
|
+
onSubmit={() => {
|
|
41
|
+
setValue("");
|
|
42
|
+
setAttachments([]);
|
|
43
|
+
}}
|
|
44
|
+
attachments={attachments}
|
|
45
|
+
onAttach={() =>
|
|
46
|
+
setAttachments((current) => [
|
|
47
|
+
...current,
|
|
48
|
+
{ id: `f${current.length + 1}`, name: `notes-${current.length + 1}.md`, meta: "Markdown · 4 KB" },
|
|
49
|
+
])
|
|
50
|
+
}
|
|
51
|
+
onRemoveAttachment={(id) => setAttachments((current) => current.filter((file) => file.id !== id))}
|
|
52
|
+
tools={mode}
|
|
53
|
+
{...rest}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Stop ends the reply, after which the composer can send again. */
|
|
59
|
+
function Replying() {
|
|
60
|
+
const [busy, setBusy] = useState(true);
|
|
61
|
+
return <Live busy={busy} onStop={() => setBusy(false)} placeholder="Reply…" />;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default defineSpecimen({
|
|
65
|
+
id: "composer",
|
|
66
|
+
title: "Composer",
|
|
67
|
+
group: "Chat",
|
|
68
|
+
description:
|
|
69
|
+
"Where a message is written. The field grows with its content, Enter sends and Shift+Enter starts a new line. " +
|
|
70
|
+
"While a reply is on its way, Send becomes Stop.",
|
|
71
|
+
order: 74,
|
|
72
|
+
blocks: [
|
|
73
|
+
{
|
|
74
|
+
kind: "states",
|
|
75
|
+
states: [
|
|
76
|
+
{ label: "empty", render: () => <Live placeholder="Ask anything about your workspace…" /> },
|
|
77
|
+
{
|
|
78
|
+
label: "written",
|
|
79
|
+
render: () => <Live text="Summarise the open support tickets from this week, grouped by product area." />,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: "attachments",
|
|
83
|
+
render: () => (
|
|
84
|
+
<Live
|
|
85
|
+
text="What explains the rise in churn?"
|
|
86
|
+
files={[...FILES, { id: "a3", name: "interviews.docx", meta: "Uploading…", uploading: true }]}
|
|
87
|
+
/>
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
label: "replying",
|
|
92
|
+
render: Replying,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: "focus",
|
|
96
|
+
render: () => <Live force="focus" footer="Answers can be wrong. Check anything important." />,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
label: "disabled",
|
|
100
|
+
render: () => <Live disabled placeholder="You have used this month's messages. They renew on the 1st." />,
|
|
101
|
+
},
|
|
102
|
+
{ label: "long content", render: () => <Live text={LONG} files={FILES} maxRows={6} /> },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode } from "react";
|
|
2
|
+
import { Button, Spinner } from "../components/index.ts";
|
|
3
|
+
import { Icon } from "./icons.tsx";
|
|
4
|
+
import styles from "./Composer.module.css";
|
|
5
|
+
|
|
6
|
+
export interface ComposerAttachment {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
/** Type and size, such as "PDF · 2.4 MB". */
|
|
10
|
+
meta?: string;
|
|
11
|
+
/** Still uploading: shows a spinner and holds Send back until it is done. */
|
|
12
|
+
uploading?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ComposerProps {
|
|
16
|
+
/** The text, for a controlled composer. Leave it out and the composer keeps its own. */
|
|
17
|
+
value?: string;
|
|
18
|
+
onValueChange?: (value: string) => void;
|
|
19
|
+
/** Enter or the Send button. A composer that keeps its own text clears it afterwards. */
|
|
20
|
+
onSubmit?: (text: string) => void;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
/** Accessible name of the text field; defaults to the placeholder. */
|
|
23
|
+
label?: string;
|
|
24
|
+
attachments?: ComposerAttachment[];
|
|
25
|
+
/** Shows the attach button. */
|
|
26
|
+
onAttach?: () => void;
|
|
27
|
+
onRemoveAttachment?: (id: string) => void;
|
|
28
|
+
/** A reply is on its way: Send becomes Stop (with `onStop`) and Enter no longer sends. */
|
|
29
|
+
busy?: boolean;
|
|
30
|
+
onStop?: () => void;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Controls at the start of the toolbar, such as a mode picker. */
|
|
33
|
+
tools?: ReactNode;
|
|
34
|
+
/** A line under the composer: a disclaimer, a limit or a hint. */
|
|
35
|
+
footer?: ReactNode;
|
|
36
|
+
/** Rows the field grows to before it scrolls. */
|
|
37
|
+
maxRows?: number;
|
|
38
|
+
autoFocus?: boolean;
|
|
39
|
+
/** Pins the focus ring, for the gallery. */
|
|
40
|
+
force?: "focus";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Where a message is written: a text field that grows with its content, attachments, and Send or Stop. */
|
|
44
|
+
export function Composer({
|
|
45
|
+
value,
|
|
46
|
+
onValueChange,
|
|
47
|
+
onSubmit,
|
|
48
|
+
placeholder = "Write a message…",
|
|
49
|
+
label,
|
|
50
|
+
attachments = [],
|
|
51
|
+
onAttach,
|
|
52
|
+
onRemoveAttachment,
|
|
53
|
+
busy = false,
|
|
54
|
+
onStop,
|
|
55
|
+
disabled = false,
|
|
56
|
+
tools,
|
|
57
|
+
footer,
|
|
58
|
+
maxRows = 8,
|
|
59
|
+
autoFocus,
|
|
60
|
+
force,
|
|
61
|
+
}: ComposerProps) {
|
|
62
|
+
const [own, setOwn] = useState("");
|
|
63
|
+
const text = value ?? own;
|
|
64
|
+
const field = useRef<HTMLTextAreaElement>(null);
|
|
65
|
+
|
|
66
|
+
// Grow with the content up to maxRows (capped in CSS), then scroll.
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
const element = field.current;
|
|
69
|
+
if (!element) return;
|
|
70
|
+
element.style.height = "auto";
|
|
71
|
+
element.style.height = `${element.scrollHeight}px`;
|
|
72
|
+
}, [text, maxRows]);
|
|
73
|
+
|
|
74
|
+
const change = (next: string) => {
|
|
75
|
+
if (value === undefined) setOwn(next);
|
|
76
|
+
onValueChange?.(next);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const uploading = attachments.some((attachment) => attachment.uploading);
|
|
80
|
+
const canSend = !disabled && !busy && !uploading && (text.trim() !== "" || attachments.length > 0);
|
|
81
|
+
|
|
82
|
+
const submit = () => {
|
|
83
|
+
if (!canSend) return;
|
|
84
|
+
onSubmit?.(text);
|
|
85
|
+
if (value === undefined) setOwn("");
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
89
|
+
if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return;
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
submit();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className={styles.root} data-ds="composer">
|
|
96
|
+
<div
|
|
97
|
+
className={styles.composer}
|
|
98
|
+
data-busy={busy || undefined}
|
|
99
|
+
data-disabled={disabled || undefined}
|
|
100
|
+
data-force={force}
|
|
101
|
+
style={{ "--composer-rows": maxRows } as CSSProperties}
|
|
102
|
+
onMouseDown={(event) => {
|
|
103
|
+
// A click on the padding or the empty toolbar puts the caret in the field.
|
|
104
|
+
if ((event.target as HTMLElement).closest("button, a, input, select, textarea, label, li")) return;
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
field.current?.focus();
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{attachments.length > 0 && (
|
|
110
|
+
<ul className={styles.attachments} aria-label="Attachments">
|
|
111
|
+
{attachments.map((attachment) => (
|
|
112
|
+
<li key={attachment.id} className={styles.attachment} data-uploading={attachment.uploading || undefined}>
|
|
113
|
+
<span className={styles.fileIcon}>
|
|
114
|
+
{attachment.uploading ? <Spinner size={14} /> : <Icon name="file" size={14} />}
|
|
115
|
+
</span>
|
|
116
|
+
<span className={styles.fileText}>
|
|
117
|
+
<span className={styles.fileName}>{attachment.name}</span>
|
|
118
|
+
{attachment.meta && <span className={styles.fileMeta}>{attachment.meta}</span>}
|
|
119
|
+
</span>
|
|
120
|
+
{onRemoveAttachment && (
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
className={styles.remove}
|
|
124
|
+
aria-label={`Remove ${attachment.name}`}
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
onClick={() => onRemoveAttachment(attachment.id)}
|
|
127
|
+
>
|
|
128
|
+
<Icon name="close" size={12} />
|
|
129
|
+
</button>
|
|
130
|
+
)}
|
|
131
|
+
</li>
|
|
132
|
+
))}
|
|
133
|
+
</ul>
|
|
134
|
+
)}
|
|
135
|
+
<textarea
|
|
136
|
+
ref={field}
|
|
137
|
+
className={styles.field}
|
|
138
|
+
rows={1}
|
|
139
|
+
value={text}
|
|
140
|
+
placeholder={placeholder}
|
|
141
|
+
aria-label={label ?? placeholder}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
autoFocus={autoFocus}
|
|
144
|
+
onChange={(event) => change(event.target.value)}
|
|
145
|
+
onKeyDown={keyDown}
|
|
146
|
+
/>
|
|
147
|
+
<div className={styles.toolbar}>
|
|
148
|
+
{onAttach && (
|
|
149
|
+
<Button
|
|
150
|
+
variant="ghost"
|
|
151
|
+
size="sm"
|
|
152
|
+
className={styles.iconButton}
|
|
153
|
+
aria-label="Attach files"
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
onClick={onAttach}
|
|
156
|
+
>
|
|
157
|
+
<Icon name="plus" />
|
|
158
|
+
</Button>
|
|
159
|
+
)}
|
|
160
|
+
{tools}
|
|
161
|
+
<span className={styles.spacer} />
|
|
162
|
+
{busy && onStop ? (
|
|
163
|
+
<Button variant="secondary" size="sm" className={styles.iconButton} aria-label="Stop" onClick={onStop}>
|
|
164
|
+
<span className={styles.stop} />
|
|
165
|
+
</Button>
|
|
166
|
+
) : (
|
|
167
|
+
<Button
|
|
168
|
+
size="sm"
|
|
169
|
+
className={styles.iconButton}
|
|
170
|
+
aria-label="Send"
|
|
171
|
+
disabled={!canSend}
|
|
172
|
+
loading={busy}
|
|
173
|
+
onClick={submit}
|
|
174
|
+
>
|
|
175
|
+
<Icon name="arrow-up" />
|
|
176
|
+
</Button>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
{footer && <p className={styles.footer}>{footer}</p>}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
.frame {
|
|
2
|
+
--_cell-y: var(--space-3);
|
|
3
|
+
--_cell-x: var(--space-4);
|
|
4
|
+
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
border: var(--card-border-width) solid var(--color-border);
|
|
7
|
+
border-radius: var(--card-radius);
|
|
8
|
+
background: var(--color-surface);
|
|
9
|
+
color: var(--color-text);
|
|
10
|
+
font-family: var(--font-sans);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.frame[data-density="compact"] {
|
|
14
|
+
--_cell-y: var(--space-2);
|
|
15
|
+
--_cell-x: var(--space-3);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.scroll {
|
|
19
|
+
overflow-x: auto;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.table {
|
|
23
|
+
width: 100%;
|
|
24
|
+
border-collapse: collapse;
|
|
25
|
+
font-size: var(--text-sm);
|
|
26
|
+
line-height: var(--leading-snug);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.caption {
|
|
30
|
+
position: absolute;
|
|
31
|
+
width: 1px;
|
|
32
|
+
height: 1px;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
clip-path: inset(50%);
|
|
35
|
+
white-space: nowrap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.table th,
|
|
39
|
+
.table td {
|
|
40
|
+
padding: var(--_cell-y) var(--_cell-x);
|
|
41
|
+
border-bottom: var(--border-thin) solid var(--color-border);
|
|
42
|
+
text-align: left;
|
|
43
|
+
vertical-align: middle;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.table th {
|
|
47
|
+
background: var(--color-surface-sunken);
|
|
48
|
+
color: var(--color-text-muted);
|
|
49
|
+
font-size: var(--text-xs);
|
|
50
|
+
font-weight: var(--weight-semibold);
|
|
51
|
+
letter-spacing: var(--tracking-wide);
|
|
52
|
+
text-transform: uppercase;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.table [data-align="end"] {
|
|
57
|
+
text-align: right;
|
|
58
|
+
font-variant-numeric: tabular-nums;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.table tbody tr:last-child td {
|
|
62
|
+
border-bottom: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.table tbody tr {
|
|
66
|
+
transition: background-color var(--duration-fast) var(--ease-standard);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.table tbody tr:hover,
|
|
70
|
+
.table tbody tr[data-force="hover"] {
|
|
71
|
+
background: color-mix(in srgb, var(--color-secondary) 60%, transparent);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.table tbody tr[data-selected] {
|
|
75
|
+
background: var(--color-primary-subtle);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.check {
|
|
79
|
+
width: 1%;
|
|
80
|
+
padding-right: 0 !important;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.sort {
|
|
84
|
+
display: inline-flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: var(--space-1);
|
|
87
|
+
margin: -2px -4px;
|
|
88
|
+
padding: 2px 4px;
|
|
89
|
+
border: 0;
|
|
90
|
+
border-radius: var(--radius-xs);
|
|
91
|
+
background: transparent;
|
|
92
|
+
color: inherit;
|
|
93
|
+
font: inherit;
|
|
94
|
+
letter-spacing: inherit;
|
|
95
|
+
text-transform: inherit;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.sort svg {
|
|
100
|
+
opacity: 0.5;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.sort:hover,
|
|
104
|
+
.sort[data-active] {
|
|
105
|
+
color: var(--color-text);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.sort[data-active] svg {
|
|
109
|
+
opacity: 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.sort:focus-visible {
|
|
113
|
+
outline: var(--focus-ring-width) solid var(--color-focus-ring);
|
|
114
|
+
outline-offset: var(--focus-ring-offset);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.empty {
|
|
118
|
+
padding: var(--space-10) var(--_cell-x) !important;
|
|
119
|
+
color: var(--color-text-muted);
|
|
120
|
+
text-align: center !important;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.footer {
|
|
124
|
+
padding: var(--space-2) var(--_cell-x);
|
|
125
|
+
border-top: var(--border-thin) solid var(--color-border);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Pagination */
|
|
129
|
+
|
|
130
|
+
.pagination {
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-wrap: wrap;
|
|
133
|
+
align-items: center;
|
|
134
|
+
justify-content: space-between;
|
|
135
|
+
gap: var(--space-3);
|
|
136
|
+
font-family: var(--font-sans);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.summary {
|
|
140
|
+
color: var(--color-text-muted);
|
|
141
|
+
font-size: var(--text-sm);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.pages {
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: var(--space-1);
|
|
148
|
+
margin-left: auto;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.page {
|
|
152
|
+
display: grid;
|
|
153
|
+
place-items: center;
|
|
154
|
+
min-width: var(--button-height-sm);
|
|
155
|
+
height: var(--button-height-sm);
|
|
156
|
+
padding: 0 var(--space-2);
|
|
157
|
+
border: 0;
|
|
158
|
+
border-radius: var(--button-radius);
|
|
159
|
+
background: transparent;
|
|
160
|
+
color: var(--color-text-muted);
|
|
161
|
+
font: inherit;
|
|
162
|
+
font-size: var(--text-sm);
|
|
163
|
+
font-variant-numeric: tabular-nums;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
transition:
|
|
166
|
+
background-color var(--duration-fast) var(--ease-standard),
|
|
167
|
+
color var(--duration-fast) var(--ease-standard);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.page:hover {
|
|
171
|
+
background: var(--color-secondary);
|
|
172
|
+
color: var(--color-text);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.page[aria-current="page"] {
|
|
176
|
+
background: var(--color-primary);
|
|
177
|
+
color: var(--color-on-primary);
|
|
178
|
+
font-weight: var(--weight-semibold);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.page:focus-visible {
|
|
182
|
+
outline: var(--focus-ring-width) solid var(--color-focus-ring);
|
|
183
|
+
outline-offset: var(--focus-ring-offset);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.gap {
|
|
187
|
+
min-width: 20px;
|
|
188
|
+
color: var(--color-text-subtle);
|
|
189
|
+
text-align: center;
|
|
190
|
+
}
|