@verbaly/studio 0.54.0 → 0.56.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 +62 -8
- package/dist/cli.js +175 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.js +175 -8
- package/dist/index.js.map +1 -1
- package/dist/panel/Actions.svelte +248 -0
- package/dist/panel/Health.svelte +184 -0
- package/dist/panel/Marks.svelte +56 -0
- package/dist/panel/Panel.svelte +337 -0
- package/dist/panel/Panel.svelte.d.ts +12 -0
- package/dist/panel/Rail.svelte +303 -0
- package/dist/panel/Row.svelte +340 -0
- package/dist/panel/api.ts +82 -0
- package/dist/panel/index.ts +18 -0
- package/dist/panel/model.ts +150 -0
- package/dist/panel/tokens.css +106 -0
- package/dist/panel/validate.ts +94 -0
- package/dist/panel/wire.ts +29 -0
- package/dist/panel/words.ts +48 -0
- package/dist/ui/assets/index-3A7RArYB.css +1 -0
- package/dist/ui/assets/index-DksZVILQ.js +3 -0
- package/dist/ui/index.html +13 -0
- package/package.json +36 -8
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Entry {
|
|
3
|
+
level: 'ok' | 'warn' | 'error';
|
|
4
|
+
check: string;
|
|
5
|
+
message: string;
|
|
6
|
+
fix?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
entries: Entry[];
|
|
11
|
+
ok: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { entries, ok, onClose }: Props = $props();
|
|
16
|
+
|
|
17
|
+
// doctor names the part of the compiler that answered; this says what it means for your project
|
|
18
|
+
const TITLE: Record<string, string> = {
|
|
19
|
+
config: 'Your settings',
|
|
20
|
+
cli: 'The verbaly command',
|
|
21
|
+
catalogs: 'Your catalogs',
|
|
22
|
+
routing: 'Where the language lives in your urls',
|
|
23
|
+
plugin: 'The piece that wires your build',
|
|
24
|
+
types: 'The generated types',
|
|
25
|
+
sources: 'Reading your code',
|
|
26
|
+
orphans: 'Keys nothing in your code uses',
|
|
27
|
+
bundle: 'What the browser downloads',
|
|
28
|
+
translations: 'The translations themselves',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const bad = $derived(entries.filter((e) => e.level !== 'ok').length);
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<section class="health" aria-label="Health">
|
|
35
|
+
<header>
|
|
36
|
+
<h2>{ok ? 'Nothing is broken' : `${bad} of ${entries.length} need you`}</h2>
|
|
37
|
+
<p>
|
|
38
|
+
This is the same check <code>npx verbaly doctor</code> runs, read out loud. It looks at your
|
|
39
|
+
settings, your catalogs and the way your build is wired, and it never changes anything.
|
|
40
|
+
</p>
|
|
41
|
+
<button class="close" onclick={onClose}>Back to the messages</button>
|
|
42
|
+
</header>
|
|
43
|
+
|
|
44
|
+
<ul>
|
|
45
|
+
<!-- doctor can report the same check twice, so the position is the only unique key here -->
|
|
46
|
+
{#each entries as entry, i (i)}
|
|
47
|
+
<li data-level={entry.level}>
|
|
48
|
+
<span class="mark" aria-hidden="true">
|
|
49
|
+
{#if entry.level === 'ok'}
|
|
50
|
+
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3.4 8.4 3 3 6.2-6.8" /></svg>
|
|
51
|
+
{:else if entry.level === 'warn'}
|
|
52
|
+
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 4v5" /><path d="M8 12h.01" /></svg>
|
|
53
|
+
{:else}
|
|
54
|
+
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8" /></svg>
|
|
55
|
+
{/if}
|
|
56
|
+
</span>
|
|
57
|
+
<span class="body">
|
|
58
|
+
<b>{TITLE[entry.check] ?? entry.check}</b>
|
|
59
|
+
<span class="msg">{entry.message}</span>
|
|
60
|
+
{#if entry.fix}
|
|
61
|
+
<span class="fix">{entry.fix}</span>
|
|
62
|
+
{/if}
|
|
63
|
+
</span>
|
|
64
|
+
<span class="sr">{entry.level}</span>
|
|
65
|
+
</li>
|
|
66
|
+
{/each}
|
|
67
|
+
</ul>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.health {
|
|
72
|
+
flex: 1;
|
|
73
|
+
min-width: 0;
|
|
74
|
+
overflow-y: auto;
|
|
75
|
+
background: var(--paper);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
header {
|
|
79
|
+
padding: 28px 24px 20px;
|
|
80
|
+
border-bottom: 1px solid var(--rule);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
h2 {
|
|
84
|
+
margin: 0;
|
|
85
|
+
font-size: 21px;
|
|
86
|
+
font-weight: 500;
|
|
87
|
+
letter-spacing: -0.014em;
|
|
88
|
+
color: var(--ink);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
header p {
|
|
92
|
+
margin: 8px 0 0;
|
|
93
|
+
max-width: 64ch;
|
|
94
|
+
font-size: 13px;
|
|
95
|
+
color: var(--muted);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.close {
|
|
99
|
+
margin-top: 16px;
|
|
100
|
+
padding: 5px 12px;
|
|
101
|
+
border: 1px solid var(--rule-strong);
|
|
102
|
+
border-radius: var(--radius);
|
|
103
|
+
background: var(--paper);
|
|
104
|
+
color: var(--body);
|
|
105
|
+
font: inherit;
|
|
106
|
+
font-size: 12.5px;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.close:hover {
|
|
111
|
+
background: var(--ground);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ul {
|
|
115
|
+
margin: 0;
|
|
116
|
+
padding: 0;
|
|
117
|
+
list-style: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
li {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: flex-start;
|
|
123
|
+
gap: 12px;
|
|
124
|
+
padding: 14px 24px;
|
|
125
|
+
border-bottom: 1px solid var(--rule);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.mark {
|
|
129
|
+
width: 20px;
|
|
130
|
+
height: 20px;
|
|
131
|
+
flex: none;
|
|
132
|
+
margin-top: 1px;
|
|
133
|
+
display: inline-flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: center;
|
|
136
|
+
border-radius: 50%;
|
|
137
|
+
background: var(--done-tint);
|
|
138
|
+
color: var(--done);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
[data-level='warn'] .mark {
|
|
142
|
+
background: var(--missing-tint);
|
|
143
|
+
color: var(--missing);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
[data-level='error'] .mark {
|
|
147
|
+
background: var(--broken-tint);
|
|
148
|
+
color: var(--broken);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.body {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-direction: column;
|
|
154
|
+
gap: 2px;
|
|
155
|
+
min-width: 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
b {
|
|
159
|
+
font-size: 13.5px;
|
|
160
|
+
font-weight: 500;
|
|
161
|
+
color: var(--ink);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.msg {
|
|
165
|
+
font-size: 13px;
|
|
166
|
+
color: var(--body);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* the remedy is the whole point of a diagnosis, so it never hides behind a disclosure */
|
|
170
|
+
.fix {
|
|
171
|
+
margin-top: 4px;
|
|
172
|
+
font-family: var(--mono);
|
|
173
|
+
font-size: 12px;
|
|
174
|
+
color: var(--draft);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.sr {
|
|
178
|
+
position: absolute;
|
|
179
|
+
width: 1px;
|
|
180
|
+
height: 1px;
|
|
181
|
+
overflow: hidden;
|
|
182
|
+
clip-path: inset(50%);
|
|
183
|
+
}
|
|
184
|
+
</style>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { MessageState } from './model';
|
|
3
|
+
|
|
4
|
+
// Each ink carries its own mark, so the ladder reads in greyscale and to a colourblind eye.
|
|
5
|
+
const { state }: { state: MessageState } = $props();
|
|
6
|
+
|
|
7
|
+
const WORD: Record<MessageState, string> = {
|
|
8
|
+
done: 'translated',
|
|
9
|
+
draft: 'written by a machine',
|
|
10
|
+
missing: 'not translated, shows in your source language',
|
|
11
|
+
broken: 'breaks your site',
|
|
12
|
+
};
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#if state === 'done'}
|
|
16
|
+
<svg viewBox="0 0 22 22" width="15" height="15" aria-hidden="true">
|
|
17
|
+
<path d="M5 11h12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
|
18
|
+
</svg>
|
|
19
|
+
{:else if state === 'missing'}
|
|
20
|
+
<svg viewBox="0 0 22 22" width="15" height="15" aria-hidden="true" fill="none">
|
|
21
|
+
<path
|
|
22
|
+
d="M6 12.5 11 7l5 5.5"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
stroke-width="1.7"
|
|
25
|
+
stroke-linecap="round"
|
|
26
|
+
stroke-linejoin="round"
|
|
27
|
+
/>
|
|
28
|
+
<path d="M11 8v7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
|
29
|
+
</svg>
|
|
30
|
+
{:else if state === 'draft'}
|
|
31
|
+
<svg viewBox="0 0 22 22" width="15" height="15" aria-hidden="true" fill="none">
|
|
32
|
+
<path
|
|
33
|
+
d="M4.5 12.5c1.6-2.2 3.2-2.2 4.8 0s3.2 2.2 4.8 0 3.2-2.2 4.8 0"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
stroke-width="1.7"
|
|
36
|
+
stroke-linecap="round"
|
|
37
|
+
/>
|
|
38
|
+
</svg>
|
|
39
|
+
{:else}
|
|
40
|
+
<svg viewBox="0 0 22 22" width="15" height="15" aria-hidden="true">
|
|
41
|
+
<path d="M11 6v6.4" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
|
42
|
+
<circle cx="11" cy="16" r="1.2" fill="currentColor" />
|
|
43
|
+
</svg>
|
|
44
|
+
{/if}
|
|
45
|
+
<span class="sr">{WORD[state]}</span>
|
|
46
|
+
|
|
47
|
+
<style>
|
|
48
|
+
.sr {
|
|
49
|
+
position: absolute;
|
|
50
|
+
width: 1px;
|
|
51
|
+
height: 1px;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
clip-path: inset(50%);
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import HealthView from './Health.svelte';
|
|
3
|
+
import Rail from './Rail.svelte';
|
|
4
|
+
import RowView from './Row.svelte';
|
|
5
|
+
import { applyState, health, visible, type Panel } from './model';
|
|
6
|
+
import Actions from './Actions.svelte';
|
|
7
|
+
import type { Check, StudioApi } from './api';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
panel: Panel;
|
|
11
|
+
api: StudioApi;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { panel, api }: Props = $props();
|
|
15
|
+
|
|
16
|
+
let editing: string | null = $state(null);
|
|
17
|
+
let note: { text: string; file: string } | null = $state(null);
|
|
18
|
+
let pending: { locale: string; keys: string[] } | null = $state(null);
|
|
19
|
+
let view: 'messages' | 'health' = $state('messages');
|
|
20
|
+
let checks: { ok: boolean; entries: Check[] } | null = $state(null);
|
|
21
|
+
|
|
22
|
+
// doctor reads the disk, so it is asked once at boot and again whenever you open the view
|
|
23
|
+
async function openHealth() {
|
|
24
|
+
view = 'health';
|
|
25
|
+
const result = await api.health();
|
|
26
|
+
if (result.error) {
|
|
27
|
+
note = { text: result.error, file: '' };
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (result.value) checks = result.value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
$effect(() => {
|
|
34
|
+
void api.health().then((result) => {
|
|
35
|
+
if (result.value) checks = result.value;
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// a command rewrote the catalogs, so the panel re-reads them instead of patching key by key
|
|
40
|
+
async function refresh(said: string) {
|
|
41
|
+
const answer = await api.state();
|
|
42
|
+
if (answer.error || !answer.value) {
|
|
43
|
+
note = { text: answer.error ?? '[verbaly] the server sent no state', file: '' };
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// the open editor and the undo both point at keys this run may have moved under them
|
|
47
|
+
editing = null;
|
|
48
|
+
pending = null;
|
|
49
|
+
applyState(panel, answer.value);
|
|
50
|
+
note = { text: said, file: '' };
|
|
51
|
+
const next = await api.health();
|
|
52
|
+
if (next.value) checks = next.value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// the flag says who wrote the text, so both directions are a write and both report what moved
|
|
56
|
+
async function setRead(locale: string, keys: string[] | undefined, undo: boolean) {
|
|
57
|
+
const result = await api.setRead(locale, keys, undo);
|
|
58
|
+
if (result.error) {
|
|
59
|
+
note = { text: result.error, file: '' };
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const moved = new Set(result.value?.keys ?? []);
|
|
63
|
+
for (const row of panel.rows) {
|
|
64
|
+
if (!moved.has(row.key)) continue;
|
|
65
|
+
const cell = row.cells.find((c) => c.locale === locale);
|
|
66
|
+
if (cell?.text) cell.state = undo ? 'draft' : 'done';
|
|
67
|
+
}
|
|
68
|
+
pending = undo ? null : { locale, keys: result.value?.keys ?? [] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// the row shows what the server refused; the panel only celebrates what it accepted
|
|
72
|
+
async function save(locale: string, key: string, text: string): Promise<string | null> {
|
|
73
|
+
const result = await api.save(locale, key, text);
|
|
74
|
+
if (result.error) return result.error;
|
|
75
|
+
const row = panel.rows.find((r) => r.key === key);
|
|
76
|
+
const cell = row?.cells.find((c) => c.locale === locale);
|
|
77
|
+
if (cell) {
|
|
78
|
+
cell.text = text;
|
|
79
|
+
cell.state = text ? 'done' : 'missing';
|
|
80
|
+
cell.issue = undefined;
|
|
81
|
+
}
|
|
82
|
+
note = { text, file: `${locale}.json` };
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// `state` would shadow the rune, so the filter is named for what it reads like at the call site
|
|
87
|
+
let only: 'all' | 'look' | 'missing' | 'draft' | 'broken' = $state('all');
|
|
88
|
+
let text: string = $state('');
|
|
89
|
+
// null means nobody has picked yet, so the default follows the project instead of freezing at mount
|
|
90
|
+
let picked: string[] | null = $state(null);
|
|
91
|
+
|
|
92
|
+
const targets = $derived(panel.locales.filter((locale) => locale !== panel.sourceLocale));
|
|
93
|
+
const shown = $derived(picked ?? targets.slice(0, 1));
|
|
94
|
+
|
|
95
|
+
const rail = $derived(health(panel));
|
|
96
|
+
const rows = $derived(visible(panel.rows, { text, state: only, locales: shown }));
|
|
97
|
+
|
|
98
|
+
// every count is over the languages actually shown, so the rail never promises rows a filter hides
|
|
99
|
+
const counts = $derived.by(() => {
|
|
100
|
+
const out: Record<string, number> = { all: 0, look: 0, missing: 0, draft: 0, broken: 0 };
|
|
101
|
+
for (const id of ['all', 'look', 'missing', 'draft', 'broken'] as const) {
|
|
102
|
+
out[id] = visible(panel.rows, { text, state: id, locales: shown }).length;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// a virtual window is a slice here: it measured 0.000 ms on the real catalog, so no library
|
|
108
|
+
const STEP = 60;
|
|
109
|
+
let limit: number = $state(STEP);
|
|
110
|
+
$effect(() => {
|
|
111
|
+
void text;
|
|
112
|
+
void only;
|
|
113
|
+
void shown;
|
|
114
|
+
limit = STEP;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function onScroll(event: Event) {
|
|
118
|
+
const el = event.currentTarget as HTMLElement;
|
|
119
|
+
if (el.scrollTop + el.clientHeight > el.scrollHeight - 600 && limit < rows.length) {
|
|
120
|
+
limit += STEP;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toggle(locale: string) {
|
|
125
|
+
picked = shown.includes(locale)
|
|
126
|
+
? shown.filter((other: string) => other !== locale)
|
|
127
|
+
: [...shown, locale];
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<div class="shell">
|
|
132
|
+
<header class="bar">
|
|
133
|
+
<span class="brand">verbaly <b>studio</b></span>
|
|
134
|
+
<input
|
|
135
|
+
class="search"
|
|
136
|
+
type="search"
|
|
137
|
+
bind:value={text}
|
|
138
|
+
placeholder="Search the text, not the key"
|
|
139
|
+
aria-label="Search the text, not the key"
|
|
140
|
+
/>
|
|
141
|
+
<span class="root" title={panel.root}>{panel.root}</span>
|
|
142
|
+
</header>
|
|
143
|
+
|
|
144
|
+
{#if panel.problems.length}
|
|
145
|
+
<ul class="problems">
|
|
146
|
+
{#each panel.problems as problem (problem.scope + problem.message)}
|
|
147
|
+
<li><code>{problem.scope}</code> {problem.message}</li>
|
|
148
|
+
{/each}
|
|
149
|
+
</ul>
|
|
150
|
+
{/if}
|
|
151
|
+
|
|
152
|
+
<div class="body">
|
|
153
|
+
<Rail
|
|
154
|
+
health={rail}
|
|
155
|
+
{shown}
|
|
156
|
+
state={only}
|
|
157
|
+
{counts}
|
|
158
|
+
{pending}
|
|
159
|
+
checks={checks ? { bad: checks.entries.filter((e) => e.level !== 'ok').length } : null}
|
|
160
|
+
onToggle={toggle}
|
|
161
|
+
onState={(next) => (only = next)}
|
|
162
|
+
onRead={(locale) => setRead(locale, undefined, false)}
|
|
163
|
+
onUndo={() => pending && setRead(pending.locale, pending.keys, true)}
|
|
164
|
+
onHealth={openHealth}
|
|
165
|
+
>
|
|
166
|
+
{#snippet actions()}
|
|
167
|
+
<Actions {api} {shown} onDone={refresh} />
|
|
168
|
+
{/snippet}
|
|
169
|
+
</Rail>
|
|
170
|
+
|
|
171
|
+
{#if view === 'health'}
|
|
172
|
+
<HealthView
|
|
173
|
+
entries={checks?.entries ?? []}
|
|
174
|
+
ok={checks?.ok ?? true}
|
|
175
|
+
onClose={() => (view = 'messages')}
|
|
176
|
+
/>
|
|
177
|
+
{:else}
|
|
178
|
+
<main class="table" onscroll={onScroll}>
|
|
179
|
+
{#if !shown.length}
|
|
180
|
+
<p class="note">Tick a language in the rail to see its translations.</p>
|
|
181
|
+
{:else if !rows.length}
|
|
182
|
+
<p class="note">Nothing matches. Every message here is in the state you asked for.</p>
|
|
183
|
+
{:else}
|
|
184
|
+
{#each rows.slice(0, limit) as row (row.key)}
|
|
185
|
+
<RowView {row} {shown} {editing} onEdit={(id) => (editing = id)} onSave={save} />
|
|
186
|
+
{/each}
|
|
187
|
+
{#if limit < rows.length}
|
|
188
|
+
<p class="note">{rows.length - limit} more below</p>
|
|
189
|
+
{/if}
|
|
190
|
+
{/if}
|
|
191
|
+
</main>
|
|
192
|
+
{/if}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{#if note}
|
|
196
|
+
<p class="snack" role="status">
|
|
197
|
+
<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
198
|
+
<path d="m3.4 8.4 3 3 6.2-6.8" />
|
|
199
|
+
</svg>
|
|
200
|
+
{#if note.file}Saved in <code>{note.file}</code>{:else}{note.text}{/if}
|
|
201
|
+
<button onclick={() => (note = null)}>Dismiss</button>
|
|
202
|
+
</p>
|
|
203
|
+
{/if}
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<style>
|
|
207
|
+
.shell {
|
|
208
|
+
position: relative;
|
|
209
|
+
display: flex;
|
|
210
|
+
flex-direction: column;
|
|
211
|
+
height: 100%;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* the bar stays ink in both themes: the one brand colour cannot vanish on a theme switch */
|
|
215
|
+
.bar {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
gap: 16px;
|
|
219
|
+
flex: none;
|
|
220
|
+
padding: 0 16px;
|
|
221
|
+
height: 48px;
|
|
222
|
+
background: var(--bar);
|
|
223
|
+
color: var(--on-bar);
|
|
224
|
+
/* the bar keeps its value in both themes, so in dark it needs an edge: it measured 1.01:1 */
|
|
225
|
+
box-shadow: inset 0 -1px 0 rgb(255 255 255 / 0.12);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.brand {
|
|
229
|
+
font-size: 13px;
|
|
230
|
+
letter-spacing: 0.02em;
|
|
231
|
+
opacity: 0.75;
|
|
232
|
+
white-space: nowrap;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.brand b {
|
|
236
|
+
opacity: 1;
|
|
237
|
+
font-weight: 600;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.search {
|
|
241
|
+
flex: 1;
|
|
242
|
+
max-width: 520px;
|
|
243
|
+
padding: 6px 12px;
|
|
244
|
+
border: 1px solid rgb(255 255 255 / 0.16);
|
|
245
|
+
border-radius: var(--radius);
|
|
246
|
+
background: rgb(255 255 255 / 0.06);
|
|
247
|
+
color: var(--on-bar);
|
|
248
|
+
font: inherit;
|
|
249
|
+
font-size: 13px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.search::placeholder {
|
|
253
|
+
color: rgb(255 255 255 / 0.45);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.search:focus-visible {
|
|
257
|
+
outline: 2px solid var(--done);
|
|
258
|
+
outline-offset: 1px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.root {
|
|
262
|
+
margin-left: auto;
|
|
263
|
+
font-family: var(--mono);
|
|
264
|
+
font-size: 11.5px;
|
|
265
|
+
opacity: 0.55;
|
|
266
|
+
max-width: 30ch;
|
|
267
|
+
overflow: hidden;
|
|
268
|
+
text-overflow: ellipsis;
|
|
269
|
+
white-space: nowrap;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.problems {
|
|
273
|
+
margin: 0;
|
|
274
|
+
padding: 8px 16px;
|
|
275
|
+
list-style: none;
|
|
276
|
+
background: var(--broken-tint);
|
|
277
|
+
color: var(--broken);
|
|
278
|
+
font-size: 12.5px;
|
|
279
|
+
border-bottom: 1px solid var(--rule);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.body {
|
|
283
|
+
display: flex;
|
|
284
|
+
flex: 1;
|
|
285
|
+
min-height: 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.table {
|
|
289
|
+
flex: 1;
|
|
290
|
+
min-width: 0;
|
|
291
|
+
overflow-y: auto;
|
|
292
|
+
background: var(--paper);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.note {
|
|
296
|
+
margin: 0;
|
|
297
|
+
padding: 24px 20px;
|
|
298
|
+
color: var(--muted);
|
|
299
|
+
font-size: 13px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* the one thing that moves: it reports a write that really happened, and it waits to be dismissed */
|
|
303
|
+
.snack {
|
|
304
|
+
position: absolute;
|
|
305
|
+
left: 50%;
|
|
306
|
+
bottom: 24px;
|
|
307
|
+
transform: translateX(-50%);
|
|
308
|
+
display: flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
gap: 12px;
|
|
311
|
+
margin: 0;
|
|
312
|
+
height: 44px;
|
|
313
|
+
padding: 0 12px 0 20px;
|
|
314
|
+
background: var(--bar);
|
|
315
|
+
color: var(--on-bar);
|
|
316
|
+
border-radius: var(--radius);
|
|
317
|
+
font-size: 13px;
|
|
318
|
+
white-space: nowrap;
|
|
319
|
+
box-shadow: 0 2px 6px rgb(6 16 5 / 0.18), 0 34px 64px -28px rgb(6 16 5 / 0.4);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.snack svg {
|
|
323
|
+
color: #8ed67f;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.snack button {
|
|
327
|
+
height: 30px;
|
|
328
|
+
padding: 0 12px;
|
|
329
|
+
border: 0;
|
|
330
|
+
border-radius: 4px;
|
|
331
|
+
background: rgb(255 255 255 / 0.14);
|
|
332
|
+
color: inherit;
|
|
333
|
+
font: inherit;
|
|
334
|
+
font-size: 12.5px;
|
|
335
|
+
cursor: pointer;
|
|
336
|
+
}
|
|
337
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type { Panel } from './model';
|
|
3
|
+
import type { StudioApi } from './api';
|
|
4
|
+
|
|
5
|
+
// Written by hand, like @verbaly/svelte's Trans.svelte.d.ts: the build copies the component as
|
|
6
|
+
// source and there is no svelte2tsx step in this repo.
|
|
7
|
+
declare const PanelView: Component<{
|
|
8
|
+
panel: Panel;
|
|
9
|
+
api: StudioApi;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export default PanelView;
|