@glw907/cairn-cms 0.59.0 → 0.60.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/CHANGELOG.md +47 -0
- package/dist/components/CairnAdmin.svelte +3 -0
- package/dist/components/CairnTidySettings.svelte +553 -0
- package/dist/components/CairnTidySettings.svelte.d.ts +32 -0
- package/dist/components/EditPage.svelte +371 -2
- package/dist/components/MarkdownEditor.svelte +168 -1
- package/dist/components/MarkdownEditor.svelte.d.ts +44 -0
- package/dist/components/TidyReview.svelte +463 -0
- package/dist/components/TidyReview.svelte.d.ts +47 -0
- package/dist/components/cairn-admin.css +764 -0
- package/dist/components/editor-tidy.d.ts +31 -0
- package/dist/components/editor-tidy.js +199 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/markdown-directives.d.ts +16 -0
- package/dist/components/markdown-directives.js +34 -0
- package/dist/components/objective-errors.d.ts +30 -0
- package/dist/components/objective-errors.js +113 -0
- package/dist/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
- package/dist/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
- package/dist/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
- package/dist/components/spellcheck-worker.d.ts +80 -0
- package/dist/components/spellcheck-worker.js +161 -0
- package/dist/components/spellcheck.d.ts +146 -0
- package/dist/components/spellcheck.js +541 -0
- package/dist/components/tidy-categorize.d.ts +67 -0
- package/dist/components/tidy-categorize.js +392 -0
- package/dist/components/tidy-diff.d.ts +60 -0
- package/dist/components/tidy-diff.js +147 -0
- package/dist/components/tidy-validate.d.ts +37 -0
- package/dist/components/tidy-validate.js +174 -0
- package/dist/content/compose.d.ts +1 -1
- package/dist/content/compose.js +11 -0
- package/dist/content/site-dictionary.d.ts +31 -0
- package/dist/content/site-dictionary.js +82 -0
- package/dist/content/types.d.ts +25 -0
- package/dist/doctor/checks-local.d.ts +1 -0
- package/dist/doctor/checks-local.js +55 -6
- package/dist/doctor/index.js +2 -1
- package/dist/log/events.d.ts +1 -1
- package/dist/nav/site-config.d.ts +98 -0
- package/dist/nav/site-config.js +132 -0
- package/dist/sveltekit/admin-dispatch.d.ts +2 -0
- package/dist/sveltekit/admin-dispatch.js +6 -2
- package/dist/sveltekit/cairn-admin.d.ts +13 -1
- package/dist/sveltekit/cairn-admin.js +22 -3
- package/dist/sveltekit/content-routes.d.ts +135 -1
- package/dist/sveltekit/content-routes.js +351 -3
- package/dist/sveltekit/tidy-prompt.d.ts +11 -0
- package/dist/sveltekit/tidy-prompt.js +118 -0
- package/package.json +10 -1
- package/src/lib/components/CairnAdmin.svelte +3 -0
- package/src/lib/components/CairnTidySettings.svelte +553 -0
- package/src/lib/components/EditPage.svelte +371 -2
- package/src/lib/components/MarkdownEditor.svelte +168 -1
- package/src/lib/components/TidyReview.svelte +463 -0
- package/src/lib/components/cairn-admin.css +25 -0
- package/src/lib/components/editor-tidy.ts +241 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdown-directives.ts +35 -0
- package/src/lib/components/objective-errors.ts +155 -0
- package/src/lib/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
- package/src/lib/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
- package/src/lib/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
- package/src/lib/components/spellcheck-worker.ts +279 -0
- package/src/lib/components/spellcheck.ts +679 -0
- package/src/lib/components/tidy-categorize.ts +460 -0
- package/src/lib/components/tidy-diff.ts +196 -0
- package/src/lib/components/tidy-validate.ts +202 -0
- package/src/lib/content/compose.ts +11 -1
- package/src/lib/content/site-dictionary.ts +84 -0
- package/src/lib/content/types.ts +25 -0
- package/src/lib/doctor/checks-local.ts +59 -5
- package/src/lib/doctor/index.ts +2 -0
- package/src/lib/log/events.ts +7 -1
- package/src/lib/nav/site-config.ts +197 -0
- package/src/lib/sveltekit/admin-dispatch.ts +7 -3
- package/src/lib/sveltekit/cairn-admin.ts +32 -4
- package/src/lib/sveltekit/content-routes.ts +504 -4
- package/src/lib/sveltekit/tidy-prompt.ts +153 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are recorded here, most recent first.
|
|
4
4
|
|
|
5
|
+
## 0.60.0
|
|
6
|
+
|
|
7
|
+
<!-- release-size: minor -->
|
|
8
|
+
|
|
9
|
+
The editor learns to copy-edit. Two features land together on the markdown source: a spellcheck that
|
|
10
|
+
runs as you write, and an opt-in tidy that reads a draft once with a language model and proposes a
|
|
11
|
+
light copy-edit you review before any of it lands.
|
|
12
|
+
|
|
13
|
+
Spellcheck is on by default. Misspelled words pick up a quiet amber underline, and the correction
|
|
14
|
+
popover offers ranked suggestions, an add-to-dictionary action, and an ignore-for-this-session
|
|
15
|
+
action, all keyboard-reachable. It runs locally on a Web Worker, so no text leaves the browser, and
|
|
16
|
+
it reads the markdown structure: code, links, frontmatter, layout-block machinery, and `media:`
|
|
17
|
+
tokens are never flagged. A second quiet layer catches the objective slips spellcheck misses: a
|
|
18
|
+
doubled word, a double space inside a line, a stray run of punctuation. The dialect is declared once
|
|
19
|
+
per site under `spellcheck.dialect` (default `en-us`), so a British site loads the British word list
|
|
20
|
+
and "colour" reads as correct. The personal dictionary is a git-committed file at
|
|
21
|
+
`src/content/.cairn/dictionary.txt`, so a word one editor adds is shared with the rest through the
|
|
22
|
+
same commit pipeline the content uses.
|
|
23
|
+
|
|
24
|
+
Tidy is opt-in and off until a developer enables it. When on, an editor runs it over the whole
|
|
25
|
+
document or a selection, and cairn reads the draft once through the Anthropic API and computes the
|
|
26
|
+
diff locally. The review is a step-in diff dialog: insertions show in green, deletions struck through
|
|
27
|
+
in red, and the author's original stays in the buffer until they apply. Objective fixes come pre-kept;
|
|
28
|
+
a judgment edit (a configured style normalization, a grammar reword) carries a review-this treatment
|
|
29
|
+
and a plain-language reason, and it is not swept by Accept fixes until confirmed. The prompt is built
|
|
30
|
+
from the site's own convention config and never harmonizes to the author's habits or guesses an
|
|
31
|
+
undeclared style, so an author's voice is preserved. Output is validated as a proofread, not a
|
|
32
|
+
restructure: a result that changes the heading structure, the frontmatter, a `media:` token, a code
|
|
33
|
+
block, or more than a bounded fraction of the wording is discarded with an honest message and the
|
|
34
|
+
document is left untouched. Conventions are edited in a two-tier settings screen and stored in the
|
|
35
|
+
committed site config under `tidy.conventions`.
|
|
36
|
+
|
|
37
|
+
New dependencies: `@codemirror/lint` (the surfacing layer for both spellcheck and the objective-error
|
|
38
|
+
underlines), `@anthropic-ai/sdk` (the Worker-side tidy model call, guarded off the client), and
|
|
39
|
+
`spellchecker-wasm` plus its bundled English dictionary asset (the spellcheck engine, delivered from
|
|
40
|
+
the packaged `dist` so the Worker and the word list reach a consumer build).
|
|
41
|
+
|
|
42
|
+
No consumer action is required for an existing site. Both features are additive. Spellcheck replaces
|
|
43
|
+
the browser's native spell checking with cairn's own, so an upgrading editor sees the new amber
|
|
44
|
+
underline and the in-editor correction popover in place of the browser's right-click menu, with no
|
|
45
|
+
config change needed. Tidy gives a site nothing until a developer turns it on: set `tidy.enabled: true`
|
|
46
|
+
in the site config, add the `ANTHROPIC_API_KEY` Worker secret, and optionally pick a model and
|
|
47
|
+
conventions. `cairn doctor` checks that the key is configured once tidy is enabled. The editor
|
|
48
|
+
walkthrough is in [write in the editor](docs/guides/write-in-the-editor.md), the developer setup is in
|
|
49
|
+
[enable tidy and the editor copy-edit](docs/guides/enable-tidy.md), and the design rationale is in
|
|
50
|
+
[the editor copy-edit](docs/explanation/editor-copyedit.md).
|
|
51
|
+
|
|
5
52
|
## 0.59.0
|
|
6
53
|
|
|
7
54
|
<!-- release-size: minor -->
|
|
@@ -14,6 +14,7 @@ mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
|
14
14
|
import ManageEditors from './ManageEditors.svelte';
|
|
15
15
|
import NavTree from './NavTree.svelte';
|
|
16
16
|
import CairnMediaLibrary from './CairnMediaLibrary.svelte';
|
|
17
|
+
import CairnTidySettings from './CairnTidySettings.svelte';
|
|
17
18
|
import type { AdminData } from '../sveltekit/cairn-admin.js';
|
|
18
19
|
import type { ContentFormFailure } from '../sveltekit/content-routes.js';
|
|
19
20
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
@@ -69,6 +70,8 @@ mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
|
69
70
|
<NavTree data={data.page} />
|
|
70
71
|
{:else if data.view === 'media'}
|
|
71
72
|
<CairnMediaLibrary data={data.page} {form} />
|
|
73
|
+
{:else if data.view === 'settings'}
|
|
74
|
+
<CairnTidySettings data={data.page} />
|
|
72
75
|
{/if}
|
|
73
76
|
</AdminLayout>
|
|
74
77
|
{/if}
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The two-tier tidy settings screen (spec 2.8, Task 15). It follows the approved settings mockup
|
|
4
|
+
(2026-06-20-editor-copyedit-settings-final-mockup.html).
|
|
5
|
+
|
|
6
|
+
Two tiers with a truthful visibility gate:
|
|
7
|
+
- The DEVELOPER tier (the master switch, the API key, the model) is shown READ-ONLY: an editor sees
|
|
8
|
+
that tidy is enabled, a key is configured, and which model runs, but cannot edit any of it. The
|
|
9
|
+
literal deploy-time tokens sit in a marked "For your developer" sub-block.
|
|
10
|
+
- The EDITOR tier (the per-convention config) renders ONLY when tidy is enabled AND the key is
|
|
11
|
+
present (`data.enabled`). When tidy is not enabled, the editor tier is genuinely ABSENT, replaced
|
|
12
|
+
by an honest labelled gate region with a read-only "what your developer needs to do" checklist and
|
|
13
|
+
a "spellcheck still works" reassurance. No teasing disabled controls sit in the tab order.
|
|
14
|
+
|
|
15
|
+
The resting state is the safe default: Fixes on, every style convention off, every variant collapsed.
|
|
16
|
+
Each binary toggle is the shipped check-and-tint aria-pressed button; each variant chooser is the
|
|
17
|
+
shipped pick-one recipe (role="radiogroup" over role="radio" with aria-checked, roving tabindex, the
|
|
18
|
+
check glyph as the non-color cue), never aria-pressed for a pick-one. A generated summary line and the
|
|
19
|
+
section counts live in always-present role="status" / aria-live="polite" regions, so a toggle
|
|
20
|
+
announces the new total; the per-keystroke diff examples are aria-hidden so the region is not chatty.
|
|
21
|
+
|
|
22
|
+
The save commits the conventions block to the same committed YAML the nav editor writes (one config
|
|
23
|
+
home), diffable and shared across editors.
|
|
24
|
+
-->
|
|
25
|
+
<script lang="ts">
|
|
26
|
+
import { untrack } from 'svelte';
|
|
27
|
+
import CsrfField from './CsrfField.svelte';
|
|
28
|
+
import CheckIcon from '@lucide/svelte/icons/check';
|
|
29
|
+
import CircleIcon from '@lucide/svelte/icons/circle';
|
|
30
|
+
import SettingsIcon from '@lucide/svelte/icons/settings';
|
|
31
|
+
import LockIcon from '@lucide/svelte/icons/lock';
|
|
32
|
+
import CodeIcon from '@lucide/svelte/icons/code-xml';
|
|
33
|
+
import ListIcon from '@lucide/svelte/icons/list';
|
|
34
|
+
import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
|
|
35
|
+
import InfoIcon from '@lucide/svelte/icons/info';
|
|
36
|
+
import ArrowRightIcon from '@lucide/svelte/icons/arrow-right';
|
|
37
|
+
import SparklesIcon from '@lucide/svelte/icons/sparkles';
|
|
38
|
+
import type { SettingsData } from '../sveltekit/content-routes.js';
|
|
39
|
+
import type { TidyConventions } from '../nav/site-config.js';
|
|
40
|
+
|
|
41
|
+
interface Props {
|
|
42
|
+
/** The two-tier settings load: the read-only developer facts, the truthful gate flag, and the
|
|
43
|
+
* resolved editor-tier conventions. */
|
|
44
|
+
data: SettingsData;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let { data }: Props = $props();
|
|
48
|
+
|
|
49
|
+
// The working copy of the editor-tier conventions: every control binds to this, and the save posts
|
|
50
|
+
// it. Seeded once from the load's resolved conventions, so the resting state IS the committed state.
|
|
51
|
+
// A fresh load remounts the screen (the route key), so seeding from the initial prop is correct.
|
|
52
|
+
let conv = $state<TidyConventions>(untrack(() => ({ ...data.conventions })));
|
|
53
|
+
|
|
54
|
+
// A multi-position style row: its config key, label, the variants (value + short label), and the
|
|
55
|
+
// diff example for the chosen variant.
|
|
56
|
+
type Variant = { value: string; label: string };
|
|
57
|
+
type StyleRow = {
|
|
58
|
+
key: keyof TidyConventions;
|
|
59
|
+
name: string;
|
|
60
|
+
/** The variants when the row is a multi-position toggle; absent for a plain on/off (en-dash). */
|
|
61
|
+
variants?: Variant[];
|
|
62
|
+
/** The radiogroup's label ("Write times as"). */
|
|
63
|
+
variantLabel?: string;
|
|
64
|
+
/** The generic "what it does" example, shown when the row is off (a hypothetical). */
|
|
65
|
+
egBefore: string;
|
|
66
|
+
egAfter: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// The style conventions, in mockup order. Each maps to one config field. The multi-position rows
|
|
70
|
+
// carry their variants; en-dash is a plain on/off.
|
|
71
|
+
const styleRows: StyleRow[] = [
|
|
72
|
+
{
|
|
73
|
+
key: 'oxfordComma',
|
|
74
|
+
name: 'Oxford comma',
|
|
75
|
+
variantLabel: 'Use the Oxford comma',
|
|
76
|
+
variants: [
|
|
77
|
+
{ value: 'always', label: 'Always' },
|
|
78
|
+
{ value: 'complex-only', label: 'Only in complex lists' },
|
|
79
|
+
{ value: 'never', label: 'Never' },
|
|
80
|
+
],
|
|
81
|
+
egBefore: 'wax, skins and poles',
|
|
82
|
+
egAfter: 'wax, skins, and poles',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: 'emDash',
|
|
86
|
+
name: 'Em-dash style',
|
|
87
|
+
variantLabel: 'Write em dashes as',
|
|
88
|
+
variants: [
|
|
89
|
+
{ value: 'spaced', label: 'Spaced' },
|
|
90
|
+
{ value: 'closed', label: 'Closed' },
|
|
91
|
+
],
|
|
92
|
+
egBefore: 'grooming--early',
|
|
93
|
+
egAfter: 'grooming—early',
|
|
94
|
+
},
|
|
95
|
+
{ key: 'enDashRanges', name: 'En-dash in number ranges', egBefore: '9-11 am', egAfter: '9–11 am' },
|
|
96
|
+
{
|
|
97
|
+
key: 'ellipsis',
|
|
98
|
+
name: 'Ellipsis',
|
|
99
|
+
variantLabel: 'Write ellipses as',
|
|
100
|
+
variants: [
|
|
101
|
+
{ value: 'single-char', label: 'One character' },
|
|
102
|
+
{ value: 'three-dots', label: 'Three dots' },
|
|
103
|
+
],
|
|
104
|
+
egBefore: 'later...',
|
|
105
|
+
egAfter: 'later…',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: 'timeFormat',
|
|
109
|
+
name: 'Time format',
|
|
110
|
+
variantLabel: 'Write times as',
|
|
111
|
+
variants: [
|
|
112
|
+
{ value: '5 PM', label: '5 PM' },
|
|
113
|
+
{ value: '5pm', label: '5pm' },
|
|
114
|
+
{ value: '5 p.m.', label: '5 p.m.' },
|
|
115
|
+
],
|
|
116
|
+
egBefore: 'doors at 5pm',
|
|
117
|
+
egAfter: 'doors at 5 PM',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: 'numberStyle',
|
|
121
|
+
name: 'Number style',
|
|
122
|
+
variantLabel: 'Write numbers as',
|
|
123
|
+
variants: [
|
|
124
|
+
{ value: 'under-ten', label: 'Spell out under ten' },
|
|
125
|
+
{ value: 'under-hundred', label: 'Spell out under 100' },
|
|
126
|
+
{ value: 'always-numerals', label: 'Always numerals' },
|
|
127
|
+
],
|
|
128
|
+
egBefore: '7 inches of snow',
|
|
129
|
+
egAfter: 'seven inches of snow',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: 'measurements',
|
|
133
|
+
name: 'Measurements and units',
|
|
134
|
+
variantLabel: 'Write units as',
|
|
135
|
+
variants: [
|
|
136
|
+
{ value: 'abbreviate', label: 'Abbreviate' },
|
|
137
|
+
{ value: 'spell-out', label: 'Spell out' },
|
|
138
|
+
],
|
|
139
|
+
egBefore: '15 centimeters',
|
|
140
|
+
egAfter: '15 cm',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: 'percent',
|
|
144
|
+
name: 'Percent',
|
|
145
|
+
variantLabel: 'Write percent as',
|
|
146
|
+
variants: [
|
|
147
|
+
{ value: 'sign', label: 'Sign (%)' },
|
|
148
|
+
{ value: 'word', label: 'Word (percent)' },
|
|
149
|
+
],
|
|
150
|
+
egBefore: '30 percent',
|
|
151
|
+
egAfter: '30%',
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
// The advanced (higher-risk) rows: plain on/off booleans behind a disclosure.
|
|
156
|
+
const advancedRows: { key: keyof TidyConventions; name: string; egBefore: string; egAfter: string }[] = [
|
|
157
|
+
{ key: 'smartQuotes', name: 'Curly quotes', egBefore: '"groomed"', egAfter: '“groomed”' },
|
|
158
|
+
{ key: 'brandCaps', name: 'Brand and proper-noun capitals', egBefore: 'github', egAfter: 'GitHub' },
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
// --- whether a row is on, generic over the config shape ---
|
|
162
|
+
// A boolean field is on when true; a multi-position field is on when it carries a variant.
|
|
163
|
+
function rowOn(key: keyof TidyConventions): boolean {
|
|
164
|
+
const v = conv[key];
|
|
165
|
+
return typeof v === 'boolean' ? v : v !== undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// The default variant a multi-position toggle takes when turned on: the first listed (the mockup's
|
|
169
|
+
// leading position). A plain on/off uses true.
|
|
170
|
+
function defaultVariant(row: StyleRow): string | boolean {
|
|
171
|
+
return row.variants ? row.variants[0].value : true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function toggleStyle(row: StyleRow) {
|
|
175
|
+
if (rowOn(row.key)) {
|
|
176
|
+
// Off: a multi-position field collapses to undefined; a boolean field to false.
|
|
177
|
+
(conv[row.key] as unknown) = row.variants ? undefined : false;
|
|
178
|
+
} else {
|
|
179
|
+
(conv[row.key] as unknown) = defaultVariant(row);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function toggleBool(key: keyof TidyConventions) {
|
|
184
|
+
(conv[key] as unknown) = !rowOn(key);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function pickVariant(key: keyof TidyConventions, value: string) {
|
|
188
|
+
(conv[key] as unknown) = value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- the live counts and the generated summary, in the role="status" regions ---
|
|
192
|
+
const styleOnCount = $derived(
|
|
193
|
+
styleRows.filter((r) => rowOn(r.key)).length + advancedRows.filter((r) => rowOn(r.key)).length,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// The "fix" clause names the always-on objective set plus any on style convention; the "leaves
|
|
197
|
+
// alone" clause names what stays untouched. Both are generated from the live config, so the line is
|
|
198
|
+
// always true for any combination.
|
|
199
|
+
const summaryFixes = $derived.by(() => {
|
|
200
|
+
const parts: string[] = [];
|
|
201
|
+
if (conv.fixes) parts.push('spelling', 'grammar', 'doubled words', 'spacing', 'capitals', 'end punctuation');
|
|
202
|
+
if (rowOn('oxfordComma')) parts.push('commas');
|
|
203
|
+
if (rowOn('timeFormat')) parts.push('time format');
|
|
204
|
+
if (rowOn('numberStyle')) parts.push('number style');
|
|
205
|
+
if (rowOn('measurements')) parts.push('units');
|
|
206
|
+
if (rowOn('percent')) parts.push('percent');
|
|
207
|
+
if (rowOn('emDash') || rowOn('enDashRanges')) parts.push('dashes');
|
|
208
|
+
if (rowOn('ellipsis')) parts.push('ellipses');
|
|
209
|
+
if (rowOn('smartQuotes')) parts.push('quotes');
|
|
210
|
+
if (rowOn('brandCaps')) parts.push('brand names');
|
|
211
|
+
return parts.length ? joinList(parts) : 'nothing yet';
|
|
212
|
+
});
|
|
213
|
+
const summaryLeaves = $derived.by(() => {
|
|
214
|
+
const parts: string[] = [];
|
|
215
|
+
if (!rowOn('oxfordComma')) parts.push('commas');
|
|
216
|
+
if (!rowOn('emDash') && !rowOn('enDashRanges')) parts.push('dashes');
|
|
217
|
+
if (!rowOn('numberStyle')) parts.push('number style');
|
|
218
|
+
if (!rowOn('measurements')) parts.push('units');
|
|
219
|
+
if (!rowOn('percent')) parts.push('percent');
|
|
220
|
+
if (!rowOn('smartQuotes')) parts.push('quotes');
|
|
221
|
+
if (!rowOn('brandCaps')) parts.push('brand names');
|
|
222
|
+
return parts.length ? joinList(parts) : 'nothing';
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
function joinList(parts: string[]): string {
|
|
226
|
+
if (parts.length === 1) return parts[0];
|
|
227
|
+
if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
|
|
228
|
+
return `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --- section masters and the safe-default reset ---
|
|
232
|
+
function styleAllOn() {
|
|
233
|
+
for (const row of styleRows) if (!rowOn(row.key)) (conv[row.key] as unknown) = defaultVariant(row);
|
|
234
|
+
for (const row of advancedRows) (conv[row.key] as unknown) = true;
|
|
235
|
+
}
|
|
236
|
+
function styleAllOff() {
|
|
237
|
+
for (const row of styleRows) (conv[row.key] as unknown) = row.variants ? undefined : false;
|
|
238
|
+
for (const row of advancedRows) (conv[row.key] as unknown) = false;
|
|
239
|
+
}
|
|
240
|
+
function fixesAllOff() {
|
|
241
|
+
conv.fixes = false;
|
|
242
|
+
}
|
|
243
|
+
function fixesAllOn() {
|
|
244
|
+
conv.fixes = true;
|
|
245
|
+
}
|
|
246
|
+
// Reset to the safe resting default: Fixes on, every style and advanced toggle off, every variant
|
|
247
|
+
// collapsed. Never named a house style.
|
|
248
|
+
function resetSafeDefault() {
|
|
249
|
+
conv = { fixes: true, enDashRanges: false, smartQuotes: false, brandCaps: false };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// --- the radiogroup roving-tabindex handler, the CairnMediaLibrary triage recipe ---
|
|
253
|
+
// The selected radio is the only tab stop; Arrow/Home/End move the selection and the focus with
|
|
254
|
+
// wraparound. A declared radiogroup owes this keyboard model.
|
|
255
|
+
let radioEls = $state<Record<string, HTMLButtonElement[]>>(
|
|
256
|
+
Object.fromEntries(styleRows.filter((r) => r.variants).map((r) => [String(r.key), []])),
|
|
257
|
+
);
|
|
258
|
+
function onRadioKeydown(e: KeyboardEvent, row: StyleRow, i: number) {
|
|
259
|
+
if (!row.variants) return;
|
|
260
|
+
const n = row.variants.length;
|
|
261
|
+
let next = i;
|
|
262
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = (i + 1) % n;
|
|
263
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = (i - 1 + n) % n;
|
|
264
|
+
else if (e.key === 'Home') next = 0;
|
|
265
|
+
else if (e.key === 'End') next = n - 1;
|
|
266
|
+
else return;
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
pickVariant(row.key, row.variants[next].value);
|
|
269
|
+
radioEls[String(row.key)]?.[next]?.focus();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// The conventions payload the save posts: the live working copy as JSON.
|
|
273
|
+
const conventionsJson = $derived(JSON.stringify(conv));
|
|
274
|
+
|
|
275
|
+
// The shared class for a check-and-tint on/off button (the binary-state idiom, no DaisyUI .toggle).
|
|
276
|
+
function onoffClass(on: boolean): string {
|
|
277
|
+
return `inline-flex h-[30px] items-center gap-1.5 rounded-lg border px-2.5 text-xs font-semibold ${
|
|
278
|
+
on
|
|
279
|
+
? 'border-primary/30 bg-primary/10 text-primary'
|
|
280
|
+
: 'border-[var(--cairn-card-border)] bg-base-100 text-[var(--color-muted)] hover:border-primary/35 hover:text-base-content'
|
|
281
|
+
}`;
|
|
282
|
+
}
|
|
283
|
+
function segClass(on: boolean): string {
|
|
284
|
+
return `inline-flex items-center gap-1.5 px-3 py-1.5 text-xs ${on ? 'bg-primary/10 text-primary font-medium' : 'text-[var(--color-muted)]'}`;
|
|
285
|
+
}
|
|
286
|
+
</script>
|
|
287
|
+
|
|
288
|
+
<div class="mx-auto max-w-3xl px-2 py-2">
|
|
289
|
+
<!-- The office heading recipe: the display face, no eyebrow above the h1 -->
|
|
290
|
+
<h1 class="text-2xl font-bold tracking-tight">Tidy</h1>
|
|
291
|
+
<p class="mt-1.5 max-w-prose text-[0.9375rem] leading-relaxed text-[var(--color-muted)]">
|
|
292
|
+
A light copy-edit from Claude. Choose what tidy is allowed to change. You always review every
|
|
293
|
+
change as a diff before it lands.
|
|
294
|
+
</p>
|
|
295
|
+
|
|
296
|
+
{#if data.saved}
|
|
297
|
+
<div role="status" class="alert alert-success mt-4 text-sm">Tidy settings saved.</div>
|
|
298
|
+
{/if}
|
|
299
|
+
{#if data.error}
|
|
300
|
+
<div role="alert" class="alert alert-error mt-4 text-sm">{data.error}</div>
|
|
301
|
+
{/if}
|
|
302
|
+
|
|
303
|
+
<!-- DEVELOPER TIER, read-only: the three deploy-time facts the editor depends on, model included as
|
|
304
|
+
a stated fact (never an editable control). Shown in both the enabled and gate states. -->
|
|
305
|
+
{#if data.enabled}
|
|
306
|
+
<div class="mt-6 flex items-start gap-3 rounded-2xl border border-[var(--cairn-card-border)] bg-base-200 p-4">
|
|
307
|
+
<span class="mt-0.5 inline-flex h-9 w-9 flex-none items-center justify-center rounded-xl bg-base-content/[0.07] text-[var(--color-muted)]">
|
|
308
|
+
<CodeIcon class="h-5 w-5" aria-hidden="true" />
|
|
309
|
+
</span>
|
|
310
|
+
<div class="min-w-0 flex-1">
|
|
311
|
+
<div class="text-[0.8125rem] font-semibold">Tidy is set up for this site</div>
|
|
312
|
+
<div class="mt-0.5 text-xs leading-relaxed text-[var(--color-muted)]">
|
|
313
|
+
Your developer turned tidy on and chose how it runs. You cannot change these here.
|
|
314
|
+
</div>
|
|
315
|
+
<div class="mt-2.5 flex flex-col gap-1.5">
|
|
316
|
+
<div class="flex items-baseline gap-2 text-[0.8125rem]">
|
|
317
|
+
<span class="inline-flex min-w-[8.5rem] flex-none items-center gap-1.5 font-semibold text-[var(--color-positive-ink)]"><CheckIcon class="h-3.5 w-3.5 flex-none" aria-hidden="true" />Tidy</span>
|
|
318
|
+
<span>On for this site</span>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="flex items-baseline gap-2 text-[0.8125rem]">
|
|
321
|
+
<span class="inline-flex min-w-[8.5rem] flex-none items-center gap-1.5 font-semibold text-[var(--color-positive-ink)]"><CheckIcon class="h-3.5 w-3.5 flex-none" aria-hidden="true" />API key</span>
|
|
322
|
+
<span>Set, and kept on the server</span>
|
|
323
|
+
</div>
|
|
324
|
+
<div class="flex items-baseline gap-2 text-[0.8125rem]">
|
|
325
|
+
<span class="inline-flex min-w-[8.5rem] flex-none items-center gap-1.5 font-semibold text-[var(--color-positive-ink)]"><CheckIcon class="h-3.5 w-3.5 flex-none" aria-hidden="true" />Model</span>
|
|
326
|
+
<span>{data.modelLabel} <span class="text-[var(--color-muted)]">· the careful default for a light copy-edit</span></span>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="mt-3 border-t border-dashed border-[var(--cairn-card-border)] pt-2.5">
|
|
330
|
+
<span class="inline-flex items-center gap-1.5 text-[0.625rem] font-semibold uppercase tracking-wide text-[var(--color-muted)]"><CodeIcon class="h-3 w-3" aria-hidden="true" />For your developer</span>
|
|
331
|
+
<div class="mt-1 text-xs leading-relaxed text-[var(--color-muted)]">
|
|
332
|
+
Tidy is on (<code class="rounded bg-[var(--cairn-code-chip)] px-1 font-mono text-[0.9em]">tidy.enabled</code>), the key rides in an Anthropic Worker secret (<code class="rounded bg-[var(--cairn-code-chip)] px-1 font-mono text-[0.9em]">ANTHROPIC_API_KEY</code>), and the model is <code class="rounded bg-[var(--cairn-code-chip)] px-1 font-mono text-[0.9em]">{data.model}</code>. Switch to <code class="rounded bg-[var(--cairn-code-chip)] px-1 font-mono text-[0.9em]">claude-haiku-4-5</code> for a cheaper, faster run.
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
<span class="mt-0.5 inline-flex flex-none items-center gap-1.5 whitespace-nowrap rounded-full border border-[var(--cairn-card-border)] px-2.5 py-1 text-[0.625rem] font-semibold text-[var(--color-muted)]"><LockIcon class="h-3 w-3" aria-hidden="true" />Set by your developer</span>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<!-- THE GENERATED SUMMARY LINE, inside the live region. Rendered unconditionally so it can
|
|
340
|
+
announce when it changes. -->
|
|
341
|
+
<div role="status" aria-live="polite" class="mb-6 mt-6 flex items-start gap-3 rounded-2xl border border-primary/[0.16] bg-primary/[0.05] p-3.5">
|
|
342
|
+
<span class="mt-0.5 inline-flex h-7 w-7 flex-none items-center justify-center rounded-lg bg-primary/[0.12] text-primary" aria-hidden="true"><ListIcon class="h-4 w-4" /></span>
|
|
343
|
+
<div class="min-w-0 flex-1 text-[0.8125rem] leading-relaxed">
|
|
344
|
+
<span class="font-semibold">Tidy will fix</span> {summaryFixes}.
|
|
345
|
+
<span class="text-[var(--color-muted)]"><b class="font-semibold text-[var(--color-subtle)]">It leaves alone</b> {summaryLeaves}.</span>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<form method="POST" action="?/saveSettings">
|
|
350
|
+
<CsrfField />
|
|
351
|
+
<input type="hidden" name="conventions" value={conventionsJson} />
|
|
352
|
+
|
|
353
|
+
<!-- SECTION: FIXES (the objective errors, one group toggle) -->
|
|
354
|
+
<section class="mb-6">
|
|
355
|
+
<div class="mb-3 flex items-end gap-3 px-0.5">
|
|
356
|
+
<div class="min-w-0 flex-1">
|
|
357
|
+
<h2 class="flex items-center gap-2 text-lg font-bold tracking-tight">
|
|
358
|
+
Fixes
|
|
359
|
+
<span role="status" aria-live="polite" class="rounded-full bg-base-content/[0.06] px-2 py-0.5 text-xs font-semibold tabular-nums text-[var(--color-muted)]">{conv.fixes ? 'On' : 'Off'}<span class="sr-only">, the fixes group is {conv.fixes ? 'on' : 'off'}</span></span>
|
|
360
|
+
</h2>
|
|
361
|
+
<p class="mt-1 max-w-prose text-[0.8125rem] leading-relaxed text-[var(--color-muted)]">Plain errors, not style choices. On by default. Leave them on unless you have a reason not to.</p>
|
|
362
|
+
</div>
|
|
363
|
+
<div class="flex flex-none items-center gap-1">
|
|
364
|
+
<button type="button" class="px-0.5 py-1 text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-primary" onclick={conv.fixes ? fixesAllOff : fixesAllOn}>{conv.fixes ? 'Turn off' : 'Turn on'}</button>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
<div class="overflow-hidden rounded-2xl border border-[var(--color-positive-ink)]/[0.22] bg-base-100 shadow-[var(--cairn-shadow)]">
|
|
368
|
+
<div class="flex items-center gap-4 p-3.5 {conv.fixes ? '' : 'opacity-60'}">
|
|
369
|
+
<div class="min-w-0 flex-1">
|
|
370
|
+
<div class="text-[0.9375rem] font-semibold leading-snug {conv.fixes ? '' : 'text-[var(--color-muted)]'}">Spelling, grammar, doubled words, spacing, capitals, end punctuation</div>
|
|
371
|
+
<div class="mt-1.5 flex flex-wrap items-center gap-1.5 font-mono text-[0.8125rem] leading-snug" aria-hidden="true">
|
|
372
|
+
<span class="mr-0.5 text-[0.6875rem] font-semibold uppercase tracking-wide text-[var(--color-muted)]">changes</span>
|
|
373
|
+
<span class="rounded-sm bg-[color-mix(in_oklab,var(--cairn-error-ink)_18%,transparent)] px-0.5 text-[var(--cairn-error-ink)] line-through">accomodate</span>
|
|
374
|
+
<span class="text-[0.6875rem] text-[var(--color-muted)]">to</span>
|
|
375
|
+
<span class="rounded-sm bg-[color-mix(in_oklab,var(--color-positive-ink)_20%,transparent)] px-0.5 text-[var(--color-positive-ink)]">accommodate</span>
|
|
376
|
+
</div>
|
|
377
|
+
<!-- the "kept as written" cue: regional spelling is never normalized, dialect-aware -->
|
|
378
|
+
<div class="mt-1.5 flex flex-wrap items-center gap-1.5 font-mono text-[0.8125rem] leading-snug" aria-hidden="true">
|
|
379
|
+
<span class="mr-0.5 text-[0.6875rem] font-semibold uppercase tracking-wide text-[var(--color-positive-ink)]">keeps</span>
|
|
380
|
+
<span class="rounded-sm bg-[var(--cairn-code-chip)] px-1">colour</span>
|
|
381
|
+
<span class="text-[0.6875rem] text-[var(--color-muted)]">and</span>
|
|
382
|
+
<span class="rounded-sm bg-[var(--cairn-code-chip)] px-1">organise</span>
|
|
383
|
+
<span class="text-[0.6875rem] text-[var(--color-muted)]">as written, following your site's English</span>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
<span class="flex-none">
|
|
387
|
+
<button type="button" class={onoffClass(conv.fixes)} aria-pressed={conv.fixes} aria-label="Fixes" onclick={() => toggleBool('fixes')}>
|
|
388
|
+
{#if conv.fixes}<CheckIcon class="h-3.5 w-3.5" aria-hidden="true" />On{:else}<CircleIcon class="h-3 w-3 opacity-60" aria-hidden="true" />Off{/if}
|
|
389
|
+
</button>
|
|
390
|
+
</span>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</section>
|
|
394
|
+
|
|
395
|
+
<!-- SECTION: STYLE CONVENTIONS (default off; on reveals an inline variant chooser) -->
|
|
396
|
+
<section class="mb-6">
|
|
397
|
+
<div class="mb-3 flex items-end gap-3 px-0.5">
|
|
398
|
+
<div class="min-w-0 flex-1">
|
|
399
|
+
<h2 class="flex items-center gap-2 text-lg font-bold tracking-tight">
|
|
400
|
+
Style conventions
|
|
401
|
+
<span role="status" aria-live="polite" class="rounded-full bg-base-content/[0.06] px-2 py-0.5 text-xs font-semibold tabular-nums text-[var(--color-muted)]">{styleOnCount} on<span class="sr-only">, {styleOnCount} style {styleOnCount === 1 ? 'convention' : 'conventions'} on</span></span>
|
|
402
|
+
</h2>
|
|
403
|
+
<p class="mt-1 max-w-prose text-[0.8125rem] leading-relaxed text-[var(--color-muted)]">Optional. cairn leaves your style alone until you turn one of these on. Turn one on to pick how it should read everywhere.</p>
|
|
404
|
+
</div>
|
|
405
|
+
<div class="flex flex-none items-center gap-1">
|
|
406
|
+
<button type="button" class="px-0.5 py-1 text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-primary" onclick={styleAllOn}>Turn all on</button>
|
|
407
|
+
<span class="text-xs text-[var(--color-muted)] opacity-40" aria-hidden="true">·</span>
|
|
408
|
+
<button type="button" class="px-0.5 py-1 text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-primary" onclick={styleAllOff}>Turn all off</button>
|
|
409
|
+
<span class="text-xs text-[var(--color-muted)] opacity-40" aria-hidden="true">·</span>
|
|
410
|
+
<button type="button" class="px-0.5 py-1 text-xs text-[var(--color-muted)] underline underline-offset-2 hover:text-primary" onclick={resetSafeDefault}>Reset to typos only</button>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="overflow-hidden rounded-2xl border border-[var(--cairn-card-border)] bg-base-100 shadow-[var(--cairn-shadow)]">
|
|
414
|
+
{#each styleRows as row, ri (row.key)}
|
|
415
|
+
{@const on = rowOn(row.key)}
|
|
416
|
+
<div class="flex gap-4 p-3.5 {ri > 0 ? 'border-t border-[var(--cairn-card-border)]' : ''} {on && row.variants ? 'items-start' : 'items-center'}">
|
|
417
|
+
<div class="min-w-0 flex-1">
|
|
418
|
+
<div class="text-[0.9375rem] font-semibold leading-snug">{row.name}</div>
|
|
419
|
+
{#if on && row.variants}
|
|
420
|
+
<!-- the inline variant chooser, revealed when the row is on: the shipped pick-one
|
|
421
|
+
recipe (radiogroup + radio + aria-checked + roving tabindex + check glyph) -->
|
|
422
|
+
<div class="mt-3 flex flex-col gap-2">
|
|
423
|
+
<div id={`tidy-var-${String(row.key)}`} class="text-[0.6875rem] font-semibold uppercase tracking-wide text-[var(--color-muted)]">{row.variantLabel}</div>
|
|
424
|
+
<div role="radiogroup" aria-labelledby={`tidy-var-${String(row.key)}`} class="inline-flex flex-wrap self-start overflow-hidden rounded-lg border border-[var(--cairn-card-border)] bg-base-100">
|
|
425
|
+
{#each row.variants as variant, vi (variant.value)}
|
|
426
|
+
{@const checked = conv[row.key] === variant.value}
|
|
427
|
+
<button
|
|
428
|
+
bind:this={radioEls[String(row.key)][vi]}
|
|
429
|
+
type="button"
|
|
430
|
+
role="radio"
|
|
431
|
+
aria-checked={checked}
|
|
432
|
+
tabindex={checked ? 0 : -1}
|
|
433
|
+
class="{segClass(checked)} {vi > 0 ? 'border-l border-[var(--cairn-card-border)]' : ''}"
|
|
434
|
+
onclick={() => pickVariant(row.key, variant.value)}
|
|
435
|
+
onkeydown={(e) => onRadioKeydown(e, row, vi)}
|
|
436
|
+
>
|
|
437
|
+
{#if checked}<CheckIcon class="h-3 w-3 flex-none" aria-hidden="true" />{/if}{variant.label}
|
|
438
|
+
</button>
|
|
439
|
+
{/each}
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
{:else}
|
|
443
|
+
<div class="mt-1.5 flex flex-wrap items-center gap-1.5 font-mono text-[0.8125rem] leading-snug {on ? '' : 'opacity-55'}" aria-hidden="true">
|
|
444
|
+
<span class="mr-0.5 text-[0.6875rem] font-semibold uppercase tracking-wide text-[var(--color-muted)]">changes</span>
|
|
445
|
+
<span class="rounded-sm bg-[color-mix(in_oklab,var(--cairn-error-ink)_18%,transparent)] px-0.5 text-[var(--cairn-error-ink)] line-through">{row.egBefore}</span>
|
|
446
|
+
<span class="text-[0.6875rem] text-[var(--color-muted)]">to</span>
|
|
447
|
+
<span class="rounded-sm bg-[color-mix(in_oklab,var(--color-positive-ink)_20%,transparent)] px-0.5 text-[var(--color-positive-ink)]">{row.egAfter}</span>
|
|
448
|
+
</div>
|
|
449
|
+
{/if}
|
|
450
|
+
</div>
|
|
451
|
+
<span class="flex-none {on && row.variants ? 'mt-0.5' : ''}">
|
|
452
|
+
<button type="button" class={onoffClass(on)} aria-pressed={on} aria-label={row.name} onclick={() => toggleStyle(row)}>
|
|
453
|
+
{#if on}<CheckIcon class="h-3.5 w-3.5" aria-hidden="true" />On{:else}<CircleIcon class="h-3 w-3 opacity-60" aria-hidden="true" />Off{/if}
|
|
454
|
+
</button>
|
|
455
|
+
</span>
|
|
456
|
+
</div>
|
|
457
|
+
{/each}
|
|
458
|
+
</div>
|
|
459
|
+
</section>
|
|
460
|
+
|
|
461
|
+
<!-- ADVANCED (default off, gated behind a disclosure, with a short risk note) -->
|
|
462
|
+
<section class="mb-6">
|
|
463
|
+
<details class="overflow-hidden rounded-2xl border border-[var(--cairn-card-border)] bg-base-100 shadow-[var(--cairn-shadow)]">
|
|
464
|
+
<summary class="flex cursor-pointer list-none items-center gap-3 p-3.5">
|
|
465
|
+
<span class="inline-flex h-7 w-7 flex-none items-center justify-center rounded-lg bg-base-content/[0.06] text-[var(--color-muted)]"><SettingsIcon class="h-4 w-4" aria-hidden="true" /></span>
|
|
466
|
+
<span class="min-w-0 flex-1">
|
|
467
|
+
<span class="flex items-center gap-2 text-[0.9375rem] font-semibold">Advanced <span class="rounded-full bg-warning/[0.14] px-2 py-0.5 text-[0.625rem] font-semibold uppercase tracking-wide text-[var(--cairn-warning-ink)]">Higher risk</span></span>
|
|
468
|
+
<span class="mt-0.5 block text-[0.8125rem] leading-snug text-[var(--color-muted)]">Two more changes that need a careful eye. Off by default. Open this only if you want them.</span>
|
|
469
|
+
</span>
|
|
470
|
+
<ArrowRightIcon class="h-4 w-4 flex-none text-[var(--color-muted)]" aria-hidden="true" />
|
|
471
|
+
</summary>
|
|
472
|
+
<div class="border-t border-[var(--cairn-card-border)]">
|
|
473
|
+
<div class="flex items-start gap-2.5 border-b border-[var(--cairn-card-border)] bg-warning/[0.08] p-3.5 text-[0.8125rem] leading-relaxed">
|
|
474
|
+
<TriangleAlertIcon class="mt-0.5 h-4 w-4 flex-none text-[var(--cairn-warning-ink)]" aria-hidden="true" />
|
|
475
|
+
<span>These two reach a little further than the rest, so check the diff with care. <b class="font-semibold">Curly quotes can trip on apostrophes</b>, and brand names only fix from a list cairn keeps. Review every change before it lands, the same as always.</span>
|
|
476
|
+
</div>
|
|
477
|
+
{#each advancedRows as row, ai (row.key)}
|
|
478
|
+
{@const on = rowOn(row.key)}
|
|
479
|
+
<div class="flex items-center gap-4 p-3.5 {ai > 0 ? 'border-t border-[var(--cairn-card-border)]' : ''}">
|
|
480
|
+
<div class="min-w-0 flex-1">
|
|
481
|
+
<div class="text-[0.9375rem] font-semibold leading-snug">{row.name}</div>
|
|
482
|
+
<div class="mt-1.5 flex flex-wrap items-center gap-1.5 font-mono text-[0.8125rem] leading-snug {on ? '' : 'opacity-55'}" aria-hidden="true">
|
|
483
|
+
<span class="mr-0.5 text-[0.6875rem] font-semibold uppercase tracking-wide text-[var(--color-muted)]">changes</span>
|
|
484
|
+
<span class="rounded-sm bg-[color-mix(in_oklab,var(--cairn-error-ink)_18%,transparent)] px-0.5 text-[var(--cairn-error-ink)] line-through">{row.egBefore}</span>
|
|
485
|
+
<span class="text-[0.6875rem] text-[var(--color-muted)]">to</span>
|
|
486
|
+
<span class="rounded-sm bg-[color-mix(in_oklab,var(--color-positive-ink)_20%,transparent)] px-0.5 text-[var(--color-positive-ink)]">{row.egAfter}</span>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
<span class="flex-none">
|
|
490
|
+
<button type="button" class={onoffClass(on)} aria-pressed={on} aria-label={row.name} onclick={() => toggleBool(row.key)}>
|
|
491
|
+
{#if on}<CheckIcon class="h-3.5 w-3.5" aria-hidden="true" />On{:else}<CircleIcon class="h-3 w-3 opacity-60" aria-hidden="true" />Off{/if}
|
|
492
|
+
</button>
|
|
493
|
+
</span>
|
|
494
|
+
</div>
|
|
495
|
+
{/each}
|
|
496
|
+
</div>
|
|
497
|
+
</details>
|
|
498
|
+
</section>
|
|
499
|
+
|
|
500
|
+
<!-- THE "NOT HERE YET" NOTE: honest, non-interactive -->
|
|
501
|
+
<div class="mb-2 rounded-2xl border border-dashed border-[var(--cairn-card-border)] bg-base-content/[0.015] p-4">
|
|
502
|
+
<div class="flex items-center gap-2 text-[0.8125rem] font-semibold"><InfoIcon class="h-4 w-4 text-[var(--color-muted)]" aria-hidden="true" />Not here yet</div>
|
|
503
|
+
<div class="mt-1.5 text-[0.8125rem] leading-relaxed text-[var(--color-muted)]">Two more conventions are held back for now. Both can change how your writing sounds, not just how it looks, so cairn leaves them out until they are safe to offer.</div>
|
|
504
|
+
<ul class="mt-2 flex flex-col gap-1.5">
|
|
505
|
+
<li class="flex items-start gap-2 text-[0.8125rem] leading-snug text-[var(--color-muted)]"><span class="flex-none font-semibold text-base-content">Your own custom rules</span><span class="flex-none opacity-50" aria-hidden="true">·</span><span>free-text instructions can reach into voice</span></li>
|
|
506
|
+
<li class="flex items-start gap-2 text-[0.8125rem] leading-snug text-[var(--color-muted)]"><span class="flex-none font-semibold text-base-content">Heading capitals</span><span class="flex-none opacity-50" aria-hidden="true">·</span><span>retitling your headings is a bigger change than it looks</span></li>
|
|
507
|
+
</ul>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div class="flex items-center gap-3 pt-4">
|
|
511
|
+
<span class="flex min-w-0 flex-1 items-center gap-1.5 text-xs leading-snug text-[var(--color-muted)]">
|
|
512
|
+
<ArrowRightIcon class="h-3.5 w-3.5 flex-none" aria-hidden="true" />Saving commits your choices to the site config, so every editor shares them.
|
|
513
|
+
</span>
|
|
514
|
+
<button type="submit" class="btn btn-primary btn-sm">Save changes</button>
|
|
515
|
+
</div>
|
|
516
|
+
</form>
|
|
517
|
+
{:else}
|
|
518
|
+
<!-- THE VISIBILITY GATE: tidy NOT enabled by the developer. The convention list is genuinely
|
|
519
|
+
absent, not disabled. One honest labelled region names the deploy-time task and who does it,
|
|
520
|
+
with no disabled controls in the tab order. -->
|
|
521
|
+
<div role="region" aria-label="Tidy is not set up" class="mt-6 flex flex-col items-center gap-3 rounded-2xl border border-[var(--cairn-card-border)] bg-base-100 p-10 text-center shadow-[var(--cairn-shadow)]">
|
|
522
|
+
<span class="inline-flex h-12 w-12 items-center justify-center rounded-full bg-base-content/[0.06] text-[var(--color-muted)]"><SparklesIcon class="h-6 w-6" aria-hidden="true" /></span>
|
|
523
|
+
<div class="text-xl font-bold tracking-tight">Tidy is not set up yet</div>
|
|
524
|
+
<div class="max-w-[50ch] text-sm leading-relaxed text-[var(--color-muted)]">
|
|
525
|
+
Tidy uses Claude to copy-edit your drafts, so it sends your writing to Anthropic and costs a
|
|
526
|
+
little per use. That makes it a developer setup, not a switch in here. Once it is on, this page
|
|
527
|
+
is where you choose what it can change.
|
|
528
|
+
</div>
|
|
529
|
+
<div class="mt-1.5 flex w-full max-w-md flex-col gap-2.5 text-left">
|
|
530
|
+
<div class="flex items-start gap-2.5 rounded-xl border border-[var(--cairn-card-border)] bg-base-200 p-3 {data.tidyEnabled ? 'opacity-60' : ''}">
|
|
531
|
+
<span class="flex-none {data.tidyEnabled ? 'text-[var(--color-positive-ink)]' : 'text-[var(--color-subtle)]'}">
|
|
532
|
+
{#if data.tidyEnabled}<CheckIcon class="mt-0.5 h-4 w-4" aria-hidden="true" />{:else}<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-base-content/[0.09] text-[0.6875rem] font-semibold">1</span>{/if}
|
|
533
|
+
</span>
|
|
534
|
+
<span class="text-[0.8125rem] leading-snug">Your developer turns tidy on for the site.<span class="mt-0.5 block text-[var(--color-muted)]">It is one setting in the site config.</span></span>
|
|
535
|
+
</div>
|
|
536
|
+
<div class="flex items-start gap-2.5 rounded-xl border border-[var(--cairn-card-border)] bg-base-200 p-3 {data.keyConfigured ? 'opacity-60' : ''}">
|
|
537
|
+
<span class="flex-none {data.keyConfigured ? 'text-[var(--color-positive-ink)]' : 'text-[var(--color-subtle)]'}">
|
|
538
|
+
{#if data.keyConfigured}<CheckIcon class="mt-0.5 h-4 w-4" aria-hidden="true" />{:else}<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-base-content/[0.09] text-[0.6875rem] font-semibold">2</span>{/if}
|
|
539
|
+
</span>
|
|
540
|
+
<span class="text-[0.8125rem] leading-snug">Your developer adds an Anthropic API key.<span class="mt-0.5 block text-[var(--color-muted)]">It stays on the server and never reaches the browser.</span></span>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="w-full max-w-md text-left">
|
|
544
|
+
<span class="inline-flex items-center gap-1.5 text-[0.625rem] font-semibold uppercase tracking-wide text-[var(--color-muted)]"><CodeIcon class="h-3 w-3" aria-hidden="true" />For your developer</span>
|
|
545
|
+
<div class="mt-1 text-xs leading-relaxed text-[var(--color-muted)]">Set <code class="rounded bg-[var(--cairn-code-chip)] px-1 font-mono text-[0.9em]">tidy.enabled: true</code> in the site config and add the Anthropic key as the <code class="rounded bg-[var(--cairn-code-chip)] px-1 font-mono text-[0.9em]">ANTHROPIC_API_KEY</code> Worker secret. The setup guide has the steps.</div>
|
|
546
|
+
</div>
|
|
547
|
+
<div class="mt-1 flex max-w-lg items-center gap-2.5 rounded-xl border border-[color-mix(in_oklab,var(--color-positive-ink)_22%,var(--cairn-card-border))] bg-[color-mix(in_oklab,var(--color-positive-ink)_8%,var(--color-base-100))] p-3 text-[0.8125rem] text-[var(--color-muted)]">
|
|
548
|
+
<CheckIcon class="h-4 w-4 flex-none text-[var(--color-positive-ink)]" aria-hidden="true" />
|
|
549
|
+
<span><b class="font-semibold text-base-content">Spellcheck is already working.</b> It runs in your browser, so it needs no setup and underlines misspellings as you type.</span>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
{/if}
|
|
553
|
+
</div>
|