@dorsk/yubisashi 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 +21 -0
- package/README.md +66 -0
- package/package.json +30 -0
- package/shell/app.css +436 -0
- package/shell/app.js +554 -0
- package/shell/index.html +46 -0
- package/skill/SKILL.md +50 -0
- package/src/cli.ts +259 -0
- package/src/format.ts +43 -0
- package/src/proxy.ts +89 -0
- package/src/server.ts +245 -0
- package/src/store.ts +113 -0
- package/src/transcript.ts +142 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dorsk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# yubisashi 指差し
|
|
2
|
+
|
|
3
|
+
Point at your running app, say what's wrong, and the coding agent working on it gets the
|
|
4
|
+
element's source `file:line`, the components that rendered it, and your comment. It answers,
|
|
5
|
+
edits, and hot-reload shows you the result. The agent's conversation streams in a panel next to
|
|
6
|
+
the app.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## How it works
|
|
11
|
+
|
|
12
|
+
`yubi up` puts a small proxy in front of your dev server. Everything passes through to the app,
|
|
13
|
+
including HMR websockets. Frame-blocking headers are stripped, so the app can be shown inside
|
|
14
|
+
the review shell at `/__yubi/`:
|
|
15
|
+
|
|
16
|
+
- **Comment**: press `C` or the ⌖ button, then click an element. Shift-click adds more
|
|
17
|
+
elements; `Esc` cancels. A glass layer takes the clicks, so the app doesn't react, and
|
|
18
|
+
disabled controls can be picked too.
|
|
19
|
+
- **Source location** comes from Svelte 5's dev-mode `__svelte_meta`, which gives exact
|
|
20
|
+
`file:line:col` plus the component chain. For other frameworks you still get the selector,
|
|
21
|
+
text and HTML.
|
|
22
|
+
- **Conversation tab**: follows the agent's Claude Code transcript live
|
|
23
|
+
(`~/.claude/projects/*/<session>.jsonl`).
|
|
24
|
+
- **Comments tab**: one thread per comment, with status and a pin on the element.
|
|
25
|
+
|
|
26
|
+
Your comments reach the agent through `yubi wait`. The agent runs it as a background command,
|
|
27
|
+
it blocks until you send something, then exits with your messages. Claude Code wakes the agent
|
|
28
|
+
when a background command exits, so it needs no MCP server, no special launch flags, and no
|
|
29
|
+
polling. The same works in any Claude Code session, including cctui ones.
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
## Use
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
# from the app directory, as a background command in the agent's session
|
|
37
|
+
yubi up --target http://localhost:5173 -- npm run dev
|
|
38
|
+
yubi url # the link to open (carries the access token)
|
|
39
|
+
yubi wait # background: exits with the user's next messages
|
|
40
|
+
yubi reply 3 "…" # answer in thread #3
|
|
41
|
+
yubi resolve 3 "…" # close it with what changed
|
|
42
|
+
yubi dismiss 3 "…" # close it without acting
|
|
43
|
+
yubi list [--all]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`skill/SKILL.md` teaches an agent this loop. Link it into `~/.claude/skills/yubisashi` so every
|
|
47
|
+
session knows it.
|
|
48
|
+
|
|
49
|
+
State lives in `.yubisashi/` in the app directory. It's added to `.git/info/exclude`
|
|
50
|
+
automatically and holds the comments, the token and the server address.
|
|
51
|
+
|
|
52
|
+
## Security
|
|
53
|
+
|
|
54
|
+
The API lets anyone who can reach it inject prompts into your agent's session. It binds to
|
|
55
|
+
127.0.0.1 by default, and every API call needs the random token in `.yubisashi/token`, which
|
|
56
|
+
also stops other sites in your browser from posting to it. Think twice before `--host 0.0.0.0`.
|
|
57
|
+
|
|
58
|
+
## Develop
|
|
59
|
+
|
|
60
|
+
Node ≥ 22.18 runs the TypeScript sources directly; there is no build step.
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npm install
|
|
64
|
+
npm test # unit, proxy/API integration, and a Playwright end-to-end run
|
|
65
|
+
npm run check # tsc + biome
|
|
66
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dorsk/yubisashi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Point at your running app, comment, and hand it to the coding agent working on it.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Dorsk <dorsk.dev@gmail.com>",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.18"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"yubi": "src/cli.ts"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"shell",
|
|
17
|
+
"skill"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"check": "tsc -p tsconfig.json && biome check .",
|
|
21
|
+
"format": "biome check --write .",
|
|
22
|
+
"test": "node --test test/*.test.ts"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@biomejs/biome": "^2.2.0",
|
|
26
|
+
"@types/node": "^22.18.0",
|
|
27
|
+
"playwright": "^1.55.0",
|
|
28
|
+
"typescript": "^5.9.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/shell/app.css
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #111214;
|
|
3
|
+
--panel: #18191c;
|
|
4
|
+
--raised: #212226;
|
|
5
|
+
--line: #2c2e33;
|
|
6
|
+
--text: #e6e6e8;
|
|
7
|
+
--muted: #8b8d94;
|
|
8
|
+
--accent: #f0883e;
|
|
9
|
+
--ok: #3fb950;
|
|
10
|
+
--warn: #d29922;
|
|
11
|
+
--font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
12
|
+
--mono: ui-monospace, "JetBrains Mono", "SF Mono", monospace;
|
|
13
|
+
color-scheme: dark;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
* {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
html,
|
|
21
|
+
body {
|
|
22
|
+
height: 100%;
|
|
23
|
+
margin: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
display: flex;
|
|
28
|
+
background: var(--bg);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
font: 13px / 1.45 var(--font);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
button {
|
|
34
|
+
font: inherit;
|
|
35
|
+
color: inherit;
|
|
36
|
+
background: var(--raised);
|
|
37
|
+
border: 1px solid var(--line);
|
|
38
|
+
border-radius: 6px;
|
|
39
|
+
padding: 4px 10px;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
button:hover {
|
|
44
|
+
border-color: var(--muted);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#stage {
|
|
48
|
+
flex: 1;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#bar {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 8px;
|
|
58
|
+
padding: 6px 10px;
|
|
59
|
+
border-bottom: 1px solid var(--line);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#bar a {
|
|
63
|
+
color: var(--muted);
|
|
64
|
+
text-decoration: none;
|
|
65
|
+
padding: 0 4px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#pick.on {
|
|
69
|
+
background: var(--accent);
|
|
70
|
+
border-color: var(--accent);
|
|
71
|
+
color: #111;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#route {
|
|
75
|
+
flex: 1;
|
|
76
|
+
color: var(--muted);
|
|
77
|
+
font-family: var(--mono);
|
|
78
|
+
font-size: 12px;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
white-space: nowrap;
|
|
81
|
+
text-overflow: ellipsis;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#viewport {
|
|
85
|
+
position: relative;
|
|
86
|
+
flex: 1;
|
|
87
|
+
min-height: 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#app {
|
|
91
|
+
width: 100%;
|
|
92
|
+
height: 100%;
|
|
93
|
+
border: 0;
|
|
94
|
+
background: #fff;
|
|
95
|
+
display: block;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#glass {
|
|
99
|
+
position: absolute;
|
|
100
|
+
inset: 0;
|
|
101
|
+
cursor: crosshair;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#glass[hidden] {
|
|
105
|
+
display: none;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#overlay {
|
|
109
|
+
position: absolute;
|
|
110
|
+
inset: 0;
|
|
111
|
+
pointer-events: none;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.box {
|
|
116
|
+
position: absolute;
|
|
117
|
+
border: 2px solid var(--accent);
|
|
118
|
+
border-radius: 3px;
|
|
119
|
+
background: rgb(240 136 62 / 0.08);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.box.hover {
|
|
123
|
+
border-style: dashed;
|
|
124
|
+
background: rgb(240 136 62 / 0.05);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.box .tag {
|
|
128
|
+
position: absolute;
|
|
129
|
+
top: -20px;
|
|
130
|
+
left: -2px;
|
|
131
|
+
padding: 1px 6px;
|
|
132
|
+
border-radius: 3px 3px 3px 0;
|
|
133
|
+
background: var(--accent);
|
|
134
|
+
color: #111;
|
|
135
|
+
font: 11px / 18px var(--mono);
|
|
136
|
+
white-space: nowrap;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.pin {
|
|
140
|
+
position: absolute;
|
|
141
|
+
width: 22px;
|
|
142
|
+
height: 22px;
|
|
143
|
+
margin: -11px 0 0 -11px;
|
|
144
|
+
border-radius: 11px 11px 11px 2px;
|
|
145
|
+
background: var(--accent);
|
|
146
|
+
color: #111;
|
|
147
|
+
font: 600 11px / 22px var(--font);
|
|
148
|
+
text-align: center;
|
|
149
|
+
pointer-events: auto;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
box-shadow: 0 1px 4px rgb(0 0 0 / 0.4);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#panel {
|
|
155
|
+
width: 420px;
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
background: var(--panel);
|
|
159
|
+
border-left: 1px solid var(--line);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#panel > header {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
justify-content: space-between;
|
|
166
|
+
padding: 6px 10px;
|
|
167
|
+
border-bottom: 1px solid var(--line);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.tabs {
|
|
171
|
+
display: flex;
|
|
172
|
+
gap: 4px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.tabs button {
|
|
176
|
+
background: none;
|
|
177
|
+
border-color: transparent;
|
|
178
|
+
color: var(--muted);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.tabs button.active {
|
|
182
|
+
color: var(--text);
|
|
183
|
+
background: var(--raised);
|
|
184
|
+
border-color: var(--line);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#status {
|
|
188
|
+
font-size: 12px;
|
|
189
|
+
color: var(--muted);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#status::before {
|
|
193
|
+
content: "";
|
|
194
|
+
display: inline-block;
|
|
195
|
+
width: 8px;
|
|
196
|
+
height: 8px;
|
|
197
|
+
margin-right: 6px;
|
|
198
|
+
border-radius: 50%;
|
|
199
|
+
background: var(--warn);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#status.on::before {
|
|
203
|
+
background: var(--ok);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.feed {
|
|
207
|
+
flex: 1;
|
|
208
|
+
overflow-y: auto;
|
|
209
|
+
padding: 12px;
|
|
210
|
+
display: flex;
|
|
211
|
+
flex-direction: column;
|
|
212
|
+
gap: 8px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.feed > * {
|
|
216
|
+
flex-shrink: 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.feed[hidden] {
|
|
220
|
+
display: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.empty {
|
|
224
|
+
color: var(--muted);
|
|
225
|
+
text-align: center;
|
|
226
|
+
margin: auto 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.msg {
|
|
230
|
+
max-width: 100%;
|
|
231
|
+
overflow-wrap: anywhere;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.msg.user {
|
|
235
|
+
align-self: flex-end;
|
|
236
|
+
max-width: 88%;
|
|
237
|
+
background: var(--raised);
|
|
238
|
+
border: 1px solid var(--line);
|
|
239
|
+
border-radius: 10px 10px 2px 10px;
|
|
240
|
+
padding: 6px 10px;
|
|
241
|
+
white-space: pre-wrap;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.msg.assistant p {
|
|
245
|
+
margin: 0 0 6px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.msg.tool {
|
|
249
|
+
color: var(--muted);
|
|
250
|
+
font: 11.5px / 1.4 var(--mono);
|
|
251
|
+
white-space: nowrap;
|
|
252
|
+
overflow: hidden;
|
|
253
|
+
text-overflow: ellipsis;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.msg.event {
|
|
257
|
+
align-self: center;
|
|
258
|
+
color: var(--muted);
|
|
259
|
+
font-size: 11.5px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
code {
|
|
263
|
+
font-family: var(--mono);
|
|
264
|
+
font-size: 12px;
|
|
265
|
+
background: var(--raised);
|
|
266
|
+
padding: 0 4px;
|
|
267
|
+
border-radius: 4px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
pre {
|
|
271
|
+
background: var(--bg);
|
|
272
|
+
border: 1px solid var(--line);
|
|
273
|
+
border-radius: 6px;
|
|
274
|
+
padding: 8px;
|
|
275
|
+
overflow-x: auto;
|
|
276
|
+
white-space: pre;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
pre code {
|
|
280
|
+
background: none;
|
|
281
|
+
padding: 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.card {
|
|
285
|
+
border: 1px solid var(--line);
|
|
286
|
+
border-radius: 8px;
|
|
287
|
+
padding: 8px 10px;
|
|
288
|
+
background: var(--raised);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.card.focus {
|
|
292
|
+
border-color: var(--accent);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.card.closed {
|
|
296
|
+
opacity: 0.6;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.card header {
|
|
300
|
+
display: flex;
|
|
301
|
+
align-items: center;
|
|
302
|
+
gap: 8px;
|
|
303
|
+
margin-bottom: 4px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.card .id {
|
|
307
|
+
font-weight: 600;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.badge {
|
|
311
|
+
font-size: 11px;
|
|
312
|
+
padding: 0 6px;
|
|
313
|
+
border-radius: 8px;
|
|
314
|
+
border: 1px solid var(--line);
|
|
315
|
+
color: var(--muted);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.badge.open {
|
|
319
|
+
color: var(--accent);
|
|
320
|
+
border-color: var(--accent);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.badge.resolved {
|
|
324
|
+
color: var(--ok);
|
|
325
|
+
border-color: var(--ok);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.card .where {
|
|
329
|
+
font: 11.5px / 1.4 var(--mono);
|
|
330
|
+
color: var(--muted);
|
|
331
|
+
overflow-wrap: anywhere;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.card .thread {
|
|
335
|
+
display: flex;
|
|
336
|
+
flex-direction: column;
|
|
337
|
+
gap: 4px;
|
|
338
|
+
margin: 6px 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.card .thread .user {
|
|
342
|
+
white-space: pre-wrap;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.card .thread p {
|
|
346
|
+
margin: 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.card .thread .agent {
|
|
350
|
+
color: var(--muted);
|
|
351
|
+
border-left: 2px solid var(--line);
|
|
352
|
+
padding-left: 8px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.card .actions {
|
|
356
|
+
display: flex;
|
|
357
|
+
gap: 6px;
|
|
358
|
+
justify-content: flex-end;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.card .actions button {
|
|
362
|
+
padding: 2px 8px;
|
|
363
|
+
font-size: 12px;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#composer {
|
|
367
|
+
border-top: 1px solid var(--line);
|
|
368
|
+
padding: 8px 10px 10px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
#context {
|
|
372
|
+
display: flex;
|
|
373
|
+
flex-wrap: wrap;
|
|
374
|
+
gap: 4px;
|
|
375
|
+
margin-bottom: 6px;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#context:empty {
|
|
379
|
+
display: none;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.chip {
|
|
383
|
+
display: inline-flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
gap: 4px;
|
|
386
|
+
max-width: 100%;
|
|
387
|
+
padding: 1px 4px 1px 8px;
|
|
388
|
+
border: 1px solid var(--accent);
|
|
389
|
+
border-radius: 10px;
|
|
390
|
+
font: 11.5px / 1.5 var(--mono);
|
|
391
|
+
overflow: hidden;
|
|
392
|
+
white-space: nowrap;
|
|
393
|
+
text-overflow: ellipsis;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.chip button {
|
|
397
|
+
border: 0;
|
|
398
|
+
padding: 0 4px;
|
|
399
|
+
background: none;
|
|
400
|
+
color: var(--muted);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
textarea {
|
|
404
|
+
width: 100%;
|
|
405
|
+
resize: vertical;
|
|
406
|
+
font: inherit;
|
|
407
|
+
color: var(--text);
|
|
408
|
+
background: var(--bg);
|
|
409
|
+
border: 1px solid var(--line);
|
|
410
|
+
border-radius: 8px;
|
|
411
|
+
padding: 8px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
textarea:focus {
|
|
415
|
+
outline: none;
|
|
416
|
+
border-color: var(--muted);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#composer .row {
|
|
420
|
+
display: flex;
|
|
421
|
+
align-items: center;
|
|
422
|
+
justify-content: space-between;
|
|
423
|
+
margin-top: 6px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#hint {
|
|
427
|
+
color: var(--muted);
|
|
428
|
+
font-size: 11.5px;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#composer button[type="submit"] {
|
|
432
|
+
background: var(--accent);
|
|
433
|
+
border-color: var(--accent);
|
|
434
|
+
color: #111;
|
|
435
|
+
font-weight: 600;
|
|
436
|
+
}
|