@verbaly/studio 0.56.0 → 0.57.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/README.md +3 -41
- package/dist/cli.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ui/assets/index-3Geaeu_2.js +3 -0
- package/dist/ui/assets/{index-3A7RArYB.css → index-B2gDPqi5.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +5 -27
- package/dist/panel/Actions.svelte +0 -248
- package/dist/panel/Health.svelte +0 -184
- package/dist/panel/Marks.svelte +0 -56
- package/dist/panel/Panel.svelte +0 -337
- package/dist/panel/Panel.svelte.d.ts +0 -12
- package/dist/panel/Rail.svelte +0 -303
- package/dist/panel/Row.svelte +0 -340
- package/dist/panel/api.ts +0 -82
- package/dist/panel/index.ts +0 -18
- package/dist/panel/model.ts +0 -150
- package/dist/panel/tokens.css +0 -106
- package/dist/panel/validate.ts +0 -94
- package/dist/panel/wire.ts +0 -29
- package/dist/panel/words.ts +0 -48
- package/dist/ui/assets/index-DksZVILQ.js +0 -3
package/dist/panel/Rail.svelte
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { FILTER, MARK_AS_READ, UNDO } from './words';
|
|
3
|
-
import type { Snippet } from 'svelte';
|
|
4
|
-
import type { LocaleHealth, MessageState } from './model';
|
|
5
|
-
|
|
6
|
-
type Which = 'all' | 'look' | 'missing' | 'draft' | 'broken';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
health: LocaleHealth[];
|
|
10
|
-
shown: string[];
|
|
11
|
-
state: Which;
|
|
12
|
-
counts: Record<string, number>;
|
|
13
|
-
pending: { locale: string; keys: string[] } | null;
|
|
14
|
-
onToggle: (locale: string) => void;
|
|
15
|
-
onState: (state: Which) => void;
|
|
16
|
-
onRead: (locale: string) => void;
|
|
17
|
-
onUndo: () => void;
|
|
18
|
-
checks: { bad: number } | null;
|
|
19
|
-
onHealth: () => void;
|
|
20
|
-
actions: Snippet;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const {
|
|
24
|
-
health,
|
|
25
|
-
shown,
|
|
26
|
-
state,
|
|
27
|
-
counts,
|
|
28
|
-
pending,
|
|
29
|
-
checks,
|
|
30
|
-
onToggle,
|
|
31
|
-
onState,
|
|
32
|
-
onRead,
|
|
33
|
-
onUndo,
|
|
34
|
-
onHealth,
|
|
35
|
-
actions,
|
|
36
|
-
}: Props = $props();
|
|
37
|
-
|
|
38
|
-
const ORDER: Which[] = ['all', 'look', 'missing', 'draft', 'broken'];
|
|
39
|
-
|
|
40
|
-
// the second line is the state, and it is what makes the rail worth more than a dropdown
|
|
41
|
-
function summary(h: LocaleHealth): string {
|
|
42
|
-
if (h.source) return 'the one you write';
|
|
43
|
-
if (h.broken) return `${h.broken} breaks your site`;
|
|
44
|
-
if (h.missing) return `${h.missing} missing`;
|
|
45
|
-
if (h.draft) return `${h.draft} nobody has read`;
|
|
46
|
-
return 'all read';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function tone(h: LocaleHealth): MessageState {
|
|
50
|
-
if (h.source) return 'done';
|
|
51
|
-
if (h.broken) return 'broken';
|
|
52
|
-
if (h.missing) return 'missing';
|
|
53
|
-
if (h.draft) return 'draft';
|
|
54
|
-
return 'done';
|
|
55
|
-
}
|
|
56
|
-
</script>
|
|
57
|
-
|
|
58
|
-
<nav class="rail" aria-label="Languages and filters">
|
|
59
|
-
<button class="nav" onclick={onHealth}>
|
|
60
|
-
<span>Health</span>
|
|
61
|
-
{#if checks}<span class="bad" class:clean={!checks.bad}>{checks.bad || "ok"}</span>{/if}
|
|
62
|
-
</button>
|
|
63
|
-
|
|
64
|
-
{@render actions()}
|
|
65
|
-
|
|
66
|
-
<h2 class="cap">Languages</h2>
|
|
67
|
-
<ul class="langs">
|
|
68
|
-
{#each health as h (h.locale)}
|
|
69
|
-
<li>
|
|
70
|
-
<label class="lang" class:source={h.source}>
|
|
71
|
-
<input
|
|
72
|
-
type="checkbox"
|
|
73
|
-
checked={shown.includes(h.locale)}
|
|
74
|
-
disabled={h.source}
|
|
75
|
-
onchange={() => onToggle(h.locale)}
|
|
76
|
-
/>
|
|
77
|
-
<span class="meta">
|
|
78
|
-
<span class="name">{h.locale}</span>
|
|
79
|
-
<span class="state" data-state={tone(h)}>{summary(h)}</span>
|
|
80
|
-
</span>
|
|
81
|
-
</label>
|
|
82
|
-
{#if !h.source && h.draft > 0}
|
|
83
|
-
<!-- no confirming dialog: the action is reversible and undo lives right here, not in a toast -->
|
|
84
|
-
<button class="read" onclick={() => onRead(h.locale)}>
|
|
85
|
-
{MARK_AS_READ}
|
|
86
|
-
<span class="n">{h.draft}</span>
|
|
87
|
-
</button>
|
|
88
|
-
{/if}
|
|
89
|
-
</li>
|
|
90
|
-
{/each}
|
|
91
|
-
</ul>
|
|
92
|
-
|
|
93
|
-
{#if pending}
|
|
94
|
-
<p class="undo">
|
|
95
|
-
<span>{pending.keys.length} in {pending.locale} marked as read</span>
|
|
96
|
-
<button onclick={onUndo}>{UNDO}</button>
|
|
97
|
-
</p>
|
|
98
|
-
{/if}
|
|
99
|
-
|
|
100
|
-
<h2 class="cap">Show</h2>
|
|
101
|
-
<ul class="filters">
|
|
102
|
-
{#each ORDER as which (which)}
|
|
103
|
-
<li>
|
|
104
|
-
<button type="button" class:on={state === which} onclick={() => onState(which)}>
|
|
105
|
-
<span>{FILTER[which]}</span>
|
|
106
|
-
<span class="count">{counts[which] ?? 0}</span>
|
|
107
|
-
</button>
|
|
108
|
-
</li>
|
|
109
|
-
{/each}
|
|
110
|
-
</ul>
|
|
111
|
-
</nav>
|
|
112
|
-
|
|
113
|
-
<style>
|
|
114
|
-
.rail {
|
|
115
|
-
width: var(--rail);
|
|
116
|
-
flex: none;
|
|
117
|
-
padding: 18px 14px;
|
|
118
|
-
border-right: 1px solid var(--rule);
|
|
119
|
-
background: var(--ground);
|
|
120
|
-
overflow-y: auto;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.nav {
|
|
124
|
-
display: flex;
|
|
125
|
-
align-items: center;
|
|
126
|
-
gap: 8px;
|
|
127
|
-
width: 100%;
|
|
128
|
-
margin-bottom: 18px;
|
|
129
|
-
padding: 7px 8px;
|
|
130
|
-
border: 0;
|
|
131
|
-
border-radius: var(--radius);
|
|
132
|
-
background: none;
|
|
133
|
-
font: inherit;
|
|
134
|
-
font-size: 13px;
|
|
135
|
-
color: var(--body);
|
|
136
|
-
cursor: pointer;
|
|
137
|
-
text-align: left;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.nav:hover {
|
|
141
|
-
background: var(--hollow);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.bad {
|
|
145
|
-
margin-left: auto;
|
|
146
|
-
padding: 0 7px;
|
|
147
|
-
border-radius: 999px;
|
|
148
|
-
background: var(--broken-tint);
|
|
149
|
-
color: var(--broken);
|
|
150
|
-
font-family: var(--mono);
|
|
151
|
-
font-size: 11px;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.bad.clean {
|
|
155
|
-
background: var(--done-tint);
|
|
156
|
-
color: var(--done);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.cap {
|
|
160
|
-
margin: 0 0 8px;
|
|
161
|
-
padding: 0 6px;
|
|
162
|
-
font-size: 11px;
|
|
163
|
-
font-weight: 600;
|
|
164
|
-
letter-spacing: 0.08em;
|
|
165
|
-
text-transform: uppercase;
|
|
166
|
-
color: var(--muted);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
ul {
|
|
170
|
-
margin: 0 0 22px;
|
|
171
|
-
padding: 0;
|
|
172
|
-
list-style: none;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.lang {
|
|
176
|
-
display: flex;
|
|
177
|
-
align-items: flex-start;
|
|
178
|
-
gap: 10px;
|
|
179
|
-
padding: 7px 6px;
|
|
180
|
-
border-radius: var(--radius);
|
|
181
|
-
cursor: pointer;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.lang:hover {
|
|
185
|
-
background: var(--hollow);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.lang.source {
|
|
189
|
-
cursor: default;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.lang input {
|
|
193
|
-
margin: 4px 0 0;
|
|
194
|
-
accent-color: var(--done);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
.meta {
|
|
198
|
-
display: flex;
|
|
199
|
-
flex-direction: column;
|
|
200
|
-
min-width: 0;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.name {
|
|
204
|
-
font-family: var(--mono);
|
|
205
|
-
font-size: 13px;
|
|
206
|
-
font-weight: 600;
|
|
207
|
-
color: var(--ink);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
.state {
|
|
211
|
-
font-size: 11.5px;
|
|
212
|
-
color: var(--muted);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
.state[data-state='broken'] {
|
|
216
|
-
color: var(--broken);
|
|
217
|
-
}
|
|
218
|
-
.state[data-state='missing'] {
|
|
219
|
-
color: var(--missing);
|
|
220
|
-
}
|
|
221
|
-
.state[data-state='draft'] {
|
|
222
|
-
color: var(--draft);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
.read {
|
|
226
|
-
display: flex;
|
|
227
|
-
align-items: center;
|
|
228
|
-
gap: 6px;
|
|
229
|
-
margin: 2px 0 6px 30px;
|
|
230
|
-
padding: 3px 9px;
|
|
231
|
-
border: 1px solid var(--draft);
|
|
232
|
-
border-radius: 999px;
|
|
233
|
-
background: var(--draft-tint);
|
|
234
|
-
color: var(--draft);
|
|
235
|
-
font: inherit;
|
|
236
|
-
font-size: 11.5px;
|
|
237
|
-
cursor: pointer;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.read .n {
|
|
241
|
-
font-family: var(--mono);
|
|
242
|
-
opacity: 0.8;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/* undo lives in the rail and does not expire: for hundreds of messages a toast is not undo */
|
|
246
|
-
.undo {
|
|
247
|
-
display: flex;
|
|
248
|
-
align-items: center;
|
|
249
|
-
gap: 8px;
|
|
250
|
-
margin: -14px 0 22px;
|
|
251
|
-
padding: 8px 10px;
|
|
252
|
-
border-radius: var(--radius);
|
|
253
|
-
background: var(--hollow);
|
|
254
|
-
font-size: 11.5px;
|
|
255
|
-
color: var(--body);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
.undo button {
|
|
259
|
-
margin-left: auto;
|
|
260
|
-
padding: 3px 10px;
|
|
261
|
-
border: 1px solid var(--rule-strong);
|
|
262
|
-
border-radius: var(--radius);
|
|
263
|
-
background: var(--paper);
|
|
264
|
-
color: var(--ink);
|
|
265
|
-
font: inherit;
|
|
266
|
-
font-size: 11.5px;
|
|
267
|
-
cursor: pointer;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
.filters button {
|
|
271
|
-
display: flex;
|
|
272
|
-
justify-content: space-between;
|
|
273
|
-
align-items: center;
|
|
274
|
-
gap: 8px;
|
|
275
|
-
width: 100%;
|
|
276
|
-
padding: 6px 8px;
|
|
277
|
-
border: 0;
|
|
278
|
-
border-radius: var(--radius);
|
|
279
|
-
background: none;
|
|
280
|
-
font: inherit;
|
|
281
|
-
font-size: 13px;
|
|
282
|
-
color: var(--body);
|
|
283
|
-
cursor: pointer;
|
|
284
|
-
text-align: left;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
.filters button:hover {
|
|
288
|
-
background: var(--hollow);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
.filters button.on {
|
|
292
|
-
background: var(--hollow);
|
|
293
|
-
color: var(--ink);
|
|
294
|
-
font-weight: 600;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/* the number is half the value: it says whether entering is worth it before you enter */
|
|
298
|
-
.count {
|
|
299
|
-
font-family: var(--mono);
|
|
300
|
-
font-size: 12px;
|
|
301
|
-
color: var(--muted);
|
|
302
|
-
}
|
|
303
|
-
</style>
|
package/dist/panel/Row.svelte
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import Marks from './Marks.svelte';
|
|
3
|
-
import { preview } from './validate';
|
|
4
|
-
import { SIGNAL, STATE } from './words';
|
|
5
|
-
import type { Cell, Row } from './model';
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
row: Row;
|
|
9
|
-
shown: string[];
|
|
10
|
-
editing: string | null;
|
|
11
|
-
onEdit: (id: string | null) => void;
|
|
12
|
-
onSave: (locale: string, key: string, text: string) => Promise<string | null>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const { row, shown, editing, onEdit, onSave }: Props = $props();
|
|
16
|
-
const cells = $derived(row.cells.filter((cell) => shown.includes(cell.locale)));
|
|
17
|
-
// the row's own state is the worst of the languages you are comparing, so the tile never lies
|
|
18
|
-
const worst = $derived(
|
|
19
|
-
(['broken', 'missing', 'draft', 'done'] as const).find((s) =>
|
|
20
|
-
cells.some((c) => c.state === s),
|
|
21
|
-
) ?? 'done',
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
let draft: string = $state('');
|
|
25
|
-
let saving: boolean = $state(false);
|
|
26
|
-
let refused: string | null = $state(null);
|
|
27
|
-
|
|
28
|
-
const live = $derived(preview(row.source, draft));
|
|
29
|
-
const id = (cell: Cell) => `${cell.locale} ${row.key}`;
|
|
30
|
-
|
|
31
|
-
function open(cell: Cell) {
|
|
32
|
-
draft = cell.text;
|
|
33
|
-
refused = null;
|
|
34
|
-
onEdit(id(cell));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function commit(cell: Cell) {
|
|
38
|
-
if (saving) return;
|
|
39
|
-
saving = true;
|
|
40
|
-
refused = await onSave(cell.locale, row.key, draft);
|
|
41
|
-
saving = false;
|
|
42
|
-
if (!refused) onEdit(null);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function keys(event: KeyboardEvent, cell: Cell) {
|
|
46
|
-
if (event.key === 'Escape') onEdit(null);
|
|
47
|
-
if (event.key === 'Enter' && !event.shiftKey) {
|
|
48
|
-
event.preventDefault();
|
|
49
|
-
void commit(cell);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
</script>
|
|
53
|
-
|
|
54
|
-
<article class="row" class:open={cells.some((c) => editing === id(c))} data-worst={worst}>
|
|
55
|
-
<span class="tile" data-state={worst}><Marks state={worst} /></span>
|
|
56
|
-
|
|
57
|
-
<span class="row-t">
|
|
58
|
-
<span class="src">{row.source}</span>
|
|
59
|
-
|
|
60
|
-
{#each cells as cell (cell.locale)}
|
|
61
|
-
{#if editing === id(cell)}
|
|
62
|
-
<span class="tl edit">
|
|
63
|
-
<span class="disc" data-state={cell.state}>{cell.locale}</span>
|
|
64
|
-
<!-- svelte-ignore a11y_autofocus -->
|
|
65
|
-
<textarea
|
|
66
|
-
class="tf"
|
|
67
|
-
bind:value={draft}
|
|
68
|
-
autofocus
|
|
69
|
-
rows="2"
|
|
70
|
-
aria-label="Translation for {cell.locale}"
|
|
71
|
-
onkeydown={(event) => keys(event, cell)}
|
|
72
|
-
></textarea>
|
|
73
|
-
</span>
|
|
74
|
-
<span class="editbar">
|
|
75
|
-
{#each live.params as param (param.name)}
|
|
76
|
-
<span class="pm" class:gone={param.inSource && !param.inTarget}
|
|
77
|
-
>{param.inSource && !param.inTarget ? '+' : ''}{{param.name}}</span
|
|
78
|
-
>
|
|
79
|
-
{/each}
|
|
80
|
-
{#each live.problems as problem (problem)}
|
|
81
|
-
<span class="vmsg">{problem}</span>
|
|
82
|
-
{/each}
|
|
83
|
-
<!-- the preview is a subset of the gate, so the server only speaks when it saw more -->
|
|
84
|
-
{#if refused && !live.problems.length}
|
|
85
|
-
<span class="vmsg">{refused}</span>
|
|
86
|
-
{/if}
|
|
87
|
-
<button class="btn sm" disabled={saving} onclick={() => commit(cell)}>
|
|
88
|
-
{saving ? 'Saving' : 'Save'}
|
|
89
|
-
</button>
|
|
90
|
-
<button class="btn sm quiet" onclick={() => onEdit(null)}>Cancel</button>
|
|
91
|
-
<span class="keys">Enter saves · Esc closes</span>
|
|
92
|
-
</span>
|
|
93
|
-
{:else}
|
|
94
|
-
<span class="tl">
|
|
95
|
-
<span class="disc" data-state={cell.state}>{cell.locale}</span>
|
|
96
|
-
{#if cell.text}
|
|
97
|
-
<button class="tv" onclick={() => open(cell)} title={cell.text}>{cell.text}</button>
|
|
98
|
-
{:else}
|
|
99
|
-
<button class="tv none" onclick={() => open(cell)}>{STATE.missing.long}</button>
|
|
100
|
-
{/if}
|
|
101
|
-
{#each cell.signals as signal (signal)}
|
|
102
|
-
<span class="sig" title={SIGNAL[signal] ?? signal}>{SIGNAL[signal] ?? signal}</span>
|
|
103
|
-
{/each}
|
|
104
|
-
</span>
|
|
105
|
-
{/if}
|
|
106
|
-
{/each}
|
|
107
|
-
</span>
|
|
108
|
-
</article>
|
|
109
|
-
|
|
110
|
-
<style>
|
|
111
|
-
.row {
|
|
112
|
-
display: flex;
|
|
113
|
-
align-items: flex-start;
|
|
114
|
-
gap: 16px;
|
|
115
|
-
min-height: 64px;
|
|
116
|
-
padding: 12px 20px;
|
|
117
|
-
border-bottom: 1px solid var(--rule);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.row:hover {
|
|
121
|
-
background: var(--ground);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.row.open {
|
|
125
|
-
background: var(--paper);
|
|
126
|
-
box-shadow: inset 3px 0 0 var(--done);
|
|
127
|
-
outline: 1px solid var(--rule);
|
|
128
|
-
outline-offset: -1px;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.tile {
|
|
132
|
-
width: 26px;
|
|
133
|
-
height: 26px;
|
|
134
|
-
flex: none;
|
|
135
|
-
margin-top: 2px;
|
|
136
|
-
display: inline-flex;
|
|
137
|
-
align-items: center;
|
|
138
|
-
justify-content: center;
|
|
139
|
-
border-radius: var(--radius);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.tile[data-state='done'] {
|
|
143
|
-
color: var(--rule-strong);
|
|
144
|
-
}
|
|
145
|
-
.tile[data-state='draft'] {
|
|
146
|
-
background: var(--draft-tint);
|
|
147
|
-
color: var(--draft);
|
|
148
|
-
}
|
|
149
|
-
.tile[data-state='missing'] {
|
|
150
|
-
background: var(--missing-tint);
|
|
151
|
-
color: var(--missing);
|
|
152
|
-
}
|
|
153
|
-
.tile[data-state='broken'] {
|
|
154
|
-
background: var(--broken);
|
|
155
|
-
color: var(--paper);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.row-t {
|
|
159
|
-
flex: 1;
|
|
160
|
-
min-width: 0;
|
|
161
|
-
display: flex;
|
|
162
|
-
flex-direction: column;
|
|
163
|
-
gap: 3px;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/* the original is context: one line, clipped, quieter than the thing you came to correct */
|
|
167
|
-
.src {
|
|
168
|
-
font-size: 13.5px;
|
|
169
|
-
line-height: 1.3;
|
|
170
|
-
color: var(--muted);
|
|
171
|
-
overflow: hidden;
|
|
172
|
-
text-overflow: ellipsis;
|
|
173
|
-
white-space: nowrap;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.tl {
|
|
177
|
-
display: flex;
|
|
178
|
-
align-items: baseline;
|
|
179
|
-
gap: 8px;
|
|
180
|
-
min-width: 0;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.tl.edit {
|
|
184
|
-
align-items: flex-start;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/* a flag is not a language and Windows will not draw one: a disc with the code, in its state */
|
|
188
|
-
.disc {
|
|
189
|
-
width: 20px;
|
|
190
|
-
height: 20px;
|
|
191
|
-
flex: none;
|
|
192
|
-
transform: translateY(3px);
|
|
193
|
-
display: inline-flex;
|
|
194
|
-
align-items: center;
|
|
195
|
-
justify-content: center;
|
|
196
|
-
border-radius: 50%;
|
|
197
|
-
background: var(--hollow);
|
|
198
|
-
color: var(--body);
|
|
199
|
-
font-family: var(--mono);
|
|
200
|
-
font-size: 9px;
|
|
201
|
-
letter-spacing: -0.02em;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
.disc[data-state='done'] {
|
|
205
|
-
background: var(--done-tint);
|
|
206
|
-
color: var(--done);
|
|
207
|
-
}
|
|
208
|
-
.disc[data-state='draft'] {
|
|
209
|
-
background: var(--draft-tint);
|
|
210
|
-
color: var(--draft);
|
|
211
|
-
}
|
|
212
|
-
.disc[data-state='missing'] {
|
|
213
|
-
background: var(--missing-tint);
|
|
214
|
-
color: var(--missing);
|
|
215
|
-
}
|
|
216
|
-
.disc[data-state='broken'] {
|
|
217
|
-
background: var(--broken-tint);
|
|
218
|
-
color: var(--broken);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
.tv {
|
|
222
|
-
flex: 1;
|
|
223
|
-
min-width: 0;
|
|
224
|
-
padding: 0;
|
|
225
|
-
border: 0;
|
|
226
|
-
background: none;
|
|
227
|
-
text-align: left;
|
|
228
|
-
font-family: var(--serif);
|
|
229
|
-
font-size: 17px;
|
|
230
|
-
line-height: 1.35;
|
|
231
|
-
color: var(--ink);
|
|
232
|
-
overflow: hidden;
|
|
233
|
-
text-overflow: ellipsis;
|
|
234
|
-
white-space: nowrap;
|
|
235
|
-
cursor: text;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/* a signal is a margin note, never a state: it points, it does not judge */
|
|
239
|
-
.sig {
|
|
240
|
-
flex: none;
|
|
241
|
-
font-size: 11px;
|
|
242
|
-
color: var(--draft);
|
|
243
|
-
background: var(--draft-tint);
|
|
244
|
-
border-radius: 4px;
|
|
245
|
-
padding: 1px 6px;
|
|
246
|
-
white-space: nowrap;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.tv.none {
|
|
250
|
-
font-family: var(--sans);
|
|
251
|
-
font-size: 13px;
|
|
252
|
-
font-style: italic;
|
|
253
|
-
color: var(--missing);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
.tf {
|
|
257
|
-
flex: 1;
|
|
258
|
-
min-width: 0;
|
|
259
|
-
font-family: var(--serif);
|
|
260
|
-
font-size: 17px;
|
|
261
|
-
line-height: 1.45;
|
|
262
|
-
color: var(--ink);
|
|
263
|
-
background: var(--paper);
|
|
264
|
-
border: 1px solid var(--done);
|
|
265
|
-
border-radius: var(--radius);
|
|
266
|
-
padding: 6px 10px;
|
|
267
|
-
resize: vertical;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
.tf:focus-visible {
|
|
271
|
-
outline: 2px solid color-mix(in oklab, var(--done) 26%, transparent);
|
|
272
|
-
outline-offset: 1px;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
.editbar {
|
|
276
|
-
display: flex;
|
|
277
|
-
align-items: center;
|
|
278
|
-
gap: 8px;
|
|
279
|
-
margin-top: 10px;
|
|
280
|
-
flex-wrap: wrap;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/* a param is a chip so you can see it survive, and the one that did not is dashed and red */
|
|
284
|
-
.pm {
|
|
285
|
-
font-family: var(--mono);
|
|
286
|
-
font-size: 12.5px;
|
|
287
|
-
color: var(--body);
|
|
288
|
-
background: var(--hollow);
|
|
289
|
-
border-radius: 4px;
|
|
290
|
-
padding: 1px 5px;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
.pm.gone {
|
|
294
|
-
background: var(--broken-tint);
|
|
295
|
-
border: 1px dashed var(--broken);
|
|
296
|
-
color: var(--broken);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
.vmsg {
|
|
300
|
-
display: inline-flex;
|
|
301
|
-
align-items: center;
|
|
302
|
-
gap: 6px;
|
|
303
|
-
font-size: 12.5px;
|
|
304
|
-
color: var(--broken);
|
|
305
|
-
background: var(--broken-tint);
|
|
306
|
-
border-radius: var(--radius);
|
|
307
|
-
padding: 4px 9px;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
.btn {
|
|
311
|
-
height: 30px;
|
|
312
|
-
padding: 0 12px;
|
|
313
|
-
border: 1px solid var(--done);
|
|
314
|
-
border-radius: var(--radius);
|
|
315
|
-
background: var(--done);
|
|
316
|
-
color: var(--paper);
|
|
317
|
-
font: inherit;
|
|
318
|
-
font-size: 12.5px;
|
|
319
|
-
font-weight: 500;
|
|
320
|
-
cursor: pointer;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
.btn.quiet {
|
|
324
|
-
background: var(--paper);
|
|
325
|
-
color: var(--body);
|
|
326
|
-
border-color: var(--rule-strong);
|
|
327
|
-
font-weight: 400;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
.btn:disabled {
|
|
331
|
-
opacity: 0.6;
|
|
332
|
-
cursor: default;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
.keys {
|
|
336
|
-
font-family: var(--mono);
|
|
337
|
-
font-size: 11px;
|
|
338
|
-
color: var(--muted);
|
|
339
|
-
}
|
|
340
|
-
</style>
|
package/dist/panel/api.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { ask, type Answer } from './wire';
|
|
2
|
-
import type { RawState } from './model';
|
|
3
|
-
|
|
4
|
-
// Everything the panel can ask for, as one object it is handed rather than one it imports.
|
|
5
|
-
export interface Saved {
|
|
6
|
-
locale: string;
|
|
7
|
-
key: string;
|
|
8
|
-
clearedDraft: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ReadResult {
|
|
12
|
-
locale: string;
|
|
13
|
-
approved: number;
|
|
14
|
-
keys: string[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface Check {
|
|
18
|
-
level: 'ok' | 'warn' | 'error';
|
|
19
|
-
check: string;
|
|
20
|
-
message: string;
|
|
21
|
-
fix?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface Health {
|
|
25
|
-
ok: boolean;
|
|
26
|
-
entries: Check[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface Added {
|
|
30
|
-
added: Record<string, string[]>;
|
|
31
|
-
found: number;
|
|
32
|
-
messages: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface Plan {
|
|
36
|
-
pending: Record<string, string[]>;
|
|
37
|
-
total: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface Job {
|
|
41
|
-
id: string;
|
|
42
|
-
kind: 'translate';
|
|
43
|
-
state: 'running' | 'done' | 'failed';
|
|
44
|
-
total: number;
|
|
45
|
-
done: number;
|
|
46
|
-
locale?: string;
|
|
47
|
-
message?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface StudioApi {
|
|
51
|
-
state(): Promise<Answer<RawState>>;
|
|
52
|
-
health(): Promise<Answer<Health>>;
|
|
53
|
-
save(locale: string, key: string, text: string): Promise<Answer<Saved>>;
|
|
54
|
-
setRead(locale: string, keys: string[] | undefined, undo: boolean): Promise<Answer<ReadResult>>;
|
|
55
|
-
extract(): Promise<Answer<Added>>;
|
|
56
|
-
// the plan is its own call so that seeing the cost can never start the spending
|
|
57
|
-
plan(locales: string[]): Promise<Answer<Plan>>;
|
|
58
|
-
translate(locales: string[]): Promise<Answer<Job>>;
|
|
59
|
-
job(id?: string): Promise<Answer<Job | null>>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function serverApi(token: string): StudioApi {
|
|
63
|
-
return {
|
|
64
|
-
state: () => ask<RawState>(token, '/api/state'),
|
|
65
|
-
health: () => ask<Health>(token, '/api/health'),
|
|
66
|
-
save: (locale, key, text) =>
|
|
67
|
-
ask<Saved>(token, `/api/message/${encodeURIComponent(locale)}/${encodeURIComponent(key)}`, {
|
|
68
|
-
method: 'PUT',
|
|
69
|
-
body: { text },
|
|
70
|
-
}),
|
|
71
|
-
setRead: (locale, keys, undo) =>
|
|
72
|
-
ask<ReadResult>(token, '/api/approve', { method: 'POST', body: { locale, keys, undo } }),
|
|
73
|
-
extract: () => ask<Added>(token, '/api/extract', { method: 'POST' }),
|
|
74
|
-
plan: (locales) => {
|
|
75
|
-
const query = locales.map((locale) => `locale=${encodeURIComponent(locale)}`).join('&');
|
|
76
|
-
return ask<Plan>(token, `/api/translate${query ? `?${query}` : ''}`);
|
|
77
|
-
},
|
|
78
|
-
translate: (locales) => ask<Job>(token, '/api/translate', { method: 'POST', body: { locales } }),
|
|
79
|
-
// without an id it asks what is running, which is how a reloaded page finds its bar again
|
|
80
|
-
job: (id) => ask<Job | null>(token, id ? `/api/job/${encodeURIComponent(id)}` : '/api/job'),
|
|
81
|
-
};
|
|
82
|
-
}
|