@glw907/cairn-cms 0.60.0 → 0.60.1
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 +13 -0
- package/dist/components/AdminLayout.svelte +130 -229
- package/dist/components/CairnAdmin.svelte +10 -42
- package/dist/components/CairnLogo.svelte +1 -6
- package/dist/components/CairnMediaLibrary.svelte +821 -1210
- package/dist/components/CairnTidySettings.svelte +192 -259
- package/dist/components/ComponentForm.svelte +110 -185
- package/dist/components/ComponentInsertDialog.svelte +163 -283
- package/dist/components/ConceptList.svelte +111 -191
- package/dist/components/ConfirmPage.svelte +5 -12
- package/dist/components/CsrfField.svelte +5 -11
- package/dist/components/DeleteDialog.svelte +15 -42
- package/dist/components/EditPage.svelte +665 -1166
- package/dist/components/EditorToolbar.svelte +108 -170
- package/dist/components/IconPicker.svelte +23 -53
- package/dist/components/LinkPicker.svelte +34 -58
- package/dist/components/LoginPage.svelte +14 -27
- package/dist/components/ManageEditors.svelte +3 -15
- package/dist/components/MarkdownEditor.svelte +689 -957
- package/dist/components/MarkdownHelpDialog.svelte +8 -12
- package/dist/components/MediaCaptureCard.svelte +18 -57
- package/dist/components/MediaFigureControl.svelte +32 -71
- package/dist/components/MediaHeroField.svelte +210 -329
- package/dist/components/MediaInsertPopover.svelte +156 -283
- package/dist/components/MediaPicker.svelte +67 -131
- package/dist/components/NavTree.svelte +46 -78
- package/dist/components/RenameDialog.svelte +16 -43
- package/dist/components/ShortcutsDialog.svelte +9 -13
- package/dist/components/ShortcutsGrid.svelte +1 -2
- package/dist/components/TidyReview.svelte +140 -248
- package/dist/components/WebLinkDialog.svelte +19 -40
- package/dist/components/cairn-admin.css +4 -0
- package/dist/components/spellcheck.d.ts +3 -1
- package/dist/components/spellcheck.js +14 -2
- package/dist/delivery/CairnHead.svelte +8 -11
- package/package.json +2 -2
- package/src/lib/components/spellcheck.ts +16 -2
|
@@ -22,267 +22,200 @@ announces the new total; the per-keystroke diff examples are aria-hidden so the
|
|
|
22
22
|
The save commits the conventions block to the same committed YAML the nav editor writes (one config
|
|
23
23
|
home), diffable and shared across editors.
|
|
24
24
|
-->
|
|
25
|
-
<script lang="ts">
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
}`;
|
|
25
|
+
<script lang="ts">import { untrack } from "svelte";
|
|
26
|
+
import CsrfField from "./CsrfField.svelte";
|
|
27
|
+
import CheckIcon from "@lucide/svelte/icons/check";
|
|
28
|
+
import CircleIcon from "@lucide/svelte/icons/circle";
|
|
29
|
+
import SettingsIcon from "@lucide/svelte/icons/settings";
|
|
30
|
+
import LockIcon from "@lucide/svelte/icons/lock";
|
|
31
|
+
import CodeIcon from "@lucide/svelte/icons/code-xml";
|
|
32
|
+
import ListIcon from "@lucide/svelte/icons/list";
|
|
33
|
+
import TriangleAlertIcon from "@lucide/svelte/icons/triangle-alert";
|
|
34
|
+
import InfoIcon from "@lucide/svelte/icons/info";
|
|
35
|
+
import ArrowRightIcon from "@lucide/svelte/icons/arrow-right";
|
|
36
|
+
import SparklesIcon from "@lucide/svelte/icons/sparkles";
|
|
37
|
+
let { data } = $props();
|
|
38
|
+
let conv = $state(untrack(() => ({ ...data.conventions })));
|
|
39
|
+
const styleRows = [
|
|
40
|
+
{
|
|
41
|
+
key: "oxfordComma",
|
|
42
|
+
name: "Oxford comma",
|
|
43
|
+
variantLabel: "Use the Oxford comma",
|
|
44
|
+
variants: [
|
|
45
|
+
{ value: "always", label: "Always" },
|
|
46
|
+
{ value: "complex-only", label: "Only in complex lists" },
|
|
47
|
+
{ value: "never", label: "Never" }
|
|
48
|
+
],
|
|
49
|
+
egBefore: "wax, skins and poles",
|
|
50
|
+
egAfter: "wax, skins, and poles"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "emDash",
|
|
54
|
+
name: "Em-dash style",
|
|
55
|
+
variantLabel: "Write em dashes as",
|
|
56
|
+
variants: [
|
|
57
|
+
{ value: "spaced", label: "Spaced" },
|
|
58
|
+
{ value: "closed", label: "Closed" }
|
|
59
|
+
],
|
|
60
|
+
egBefore: "grooming--early",
|
|
61
|
+
egAfter: "grooming—early"
|
|
62
|
+
},
|
|
63
|
+
{ key: "enDashRanges", name: "En-dash in number ranges", egBefore: "9-11 am", egAfter: "9–11 am" },
|
|
64
|
+
{
|
|
65
|
+
key: "ellipsis",
|
|
66
|
+
name: "Ellipsis",
|
|
67
|
+
variantLabel: "Write ellipses as",
|
|
68
|
+
variants: [
|
|
69
|
+
{ value: "single-char", label: "One character" },
|
|
70
|
+
{ value: "three-dots", label: "Three dots" }
|
|
71
|
+
],
|
|
72
|
+
egBefore: "later...",
|
|
73
|
+
egAfter: "later…"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: "timeFormat",
|
|
77
|
+
name: "Time format",
|
|
78
|
+
variantLabel: "Write times as",
|
|
79
|
+
variants: [
|
|
80
|
+
{ value: "5 PM", label: "5 PM" },
|
|
81
|
+
{ value: "5pm", label: "5pm" },
|
|
82
|
+
{ value: "5 p.m.", label: "5 p.m." }
|
|
83
|
+
],
|
|
84
|
+
egBefore: "doors at 5pm",
|
|
85
|
+
egAfter: "doors at 5 PM"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "numberStyle",
|
|
89
|
+
name: "Number style",
|
|
90
|
+
variantLabel: "Write numbers as",
|
|
91
|
+
variants: [
|
|
92
|
+
{ value: "under-ten", label: "Spell out under ten" },
|
|
93
|
+
{ value: "under-hundred", label: "Spell out under 100" },
|
|
94
|
+
{ value: "always-numerals", label: "Always numerals" }
|
|
95
|
+
],
|
|
96
|
+
egBefore: "7 inches of snow",
|
|
97
|
+
egAfter: "seven inches of snow"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "measurements",
|
|
101
|
+
name: "Measurements and units",
|
|
102
|
+
variantLabel: "Write units as",
|
|
103
|
+
variants: [
|
|
104
|
+
{ value: "abbreviate", label: "Abbreviate" },
|
|
105
|
+
{ value: "spell-out", label: "Spell out" }
|
|
106
|
+
],
|
|
107
|
+
egBefore: "15 centimeters",
|
|
108
|
+
egAfter: "15 cm"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: "percent",
|
|
112
|
+
name: "Percent",
|
|
113
|
+
variantLabel: "Write percent as",
|
|
114
|
+
variants: [
|
|
115
|
+
{ value: "sign", label: "Sign (%)" },
|
|
116
|
+
{ value: "word", label: "Word (percent)" }
|
|
117
|
+
],
|
|
118
|
+
egBefore: "30 percent",
|
|
119
|
+
egAfter: "30%"
|
|
282
120
|
}
|
|
283
|
-
|
|
284
|
-
|
|
121
|
+
];
|
|
122
|
+
const advancedRows = [
|
|
123
|
+
{ key: "smartQuotes", name: "Curly quotes", egBefore: '"groomed"', egAfter: "“groomed”" },
|
|
124
|
+
{ key: "brandCaps", name: "Brand and proper-noun capitals", egBefore: "github", egAfter: "GitHub" }
|
|
125
|
+
];
|
|
126
|
+
function rowOn(key) {
|
|
127
|
+
const v = conv[key];
|
|
128
|
+
return typeof v === "boolean" ? v : v !== void 0;
|
|
129
|
+
}
|
|
130
|
+
function defaultVariant(row) {
|
|
131
|
+
return row.variants ? row.variants[0].value : true;
|
|
132
|
+
}
|
|
133
|
+
function toggleStyle(row) {
|
|
134
|
+
if (rowOn(row.key)) {
|
|
135
|
+
conv[row.key] = row.variants ? void 0 : false;
|
|
136
|
+
} else {
|
|
137
|
+
conv[row.key] = defaultVariant(row);
|
|
285
138
|
}
|
|
139
|
+
}
|
|
140
|
+
function toggleBool(key) {
|
|
141
|
+
conv[key] = !rowOn(key);
|
|
142
|
+
}
|
|
143
|
+
function pickVariant(key, value) {
|
|
144
|
+
conv[key] = value;
|
|
145
|
+
}
|
|
146
|
+
const styleOnCount = $derived(
|
|
147
|
+
styleRows.filter((r) => rowOn(r.key)).length + advancedRows.filter((r) => rowOn(r.key)).length
|
|
148
|
+
);
|
|
149
|
+
const summaryFixes = $derived.by(() => {
|
|
150
|
+
const parts = [];
|
|
151
|
+
if (conv.fixes) parts.push("spelling", "grammar", "doubled words", "spacing", "capitals", "end punctuation");
|
|
152
|
+
if (rowOn("oxfordComma")) parts.push("commas");
|
|
153
|
+
if (rowOn("timeFormat")) parts.push("time format");
|
|
154
|
+
if (rowOn("numberStyle")) parts.push("number style");
|
|
155
|
+
if (rowOn("measurements")) parts.push("units");
|
|
156
|
+
if (rowOn("percent")) parts.push("percent");
|
|
157
|
+
if (rowOn("emDash") || rowOn("enDashRanges")) parts.push("dashes");
|
|
158
|
+
if (rowOn("ellipsis")) parts.push("ellipses");
|
|
159
|
+
if (rowOn("smartQuotes")) parts.push("quotes");
|
|
160
|
+
if (rowOn("brandCaps")) parts.push("brand names");
|
|
161
|
+
return parts.length ? joinList(parts) : "nothing yet";
|
|
162
|
+
});
|
|
163
|
+
const summaryLeaves = $derived.by(() => {
|
|
164
|
+
const parts = [];
|
|
165
|
+
if (!rowOn("oxfordComma")) parts.push("commas");
|
|
166
|
+
if (!rowOn("emDash") && !rowOn("enDashRanges")) parts.push("dashes");
|
|
167
|
+
if (!rowOn("numberStyle")) parts.push("number style");
|
|
168
|
+
if (!rowOn("measurements")) parts.push("units");
|
|
169
|
+
if (!rowOn("percent")) parts.push("percent");
|
|
170
|
+
if (!rowOn("smartQuotes")) parts.push("quotes");
|
|
171
|
+
if (!rowOn("brandCaps")) parts.push("brand names");
|
|
172
|
+
return parts.length ? joinList(parts) : "nothing";
|
|
173
|
+
});
|
|
174
|
+
function joinList(parts) {
|
|
175
|
+
if (parts.length === 1) return parts[0];
|
|
176
|
+
if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
|
|
177
|
+
return `${parts.slice(0, -1).join(", ")}, and ${parts[parts.length - 1]}`;
|
|
178
|
+
}
|
|
179
|
+
function styleAllOn() {
|
|
180
|
+
for (const row of styleRows) if (!rowOn(row.key)) conv[row.key] = defaultVariant(row);
|
|
181
|
+
for (const row of advancedRows) conv[row.key] = true;
|
|
182
|
+
}
|
|
183
|
+
function styleAllOff() {
|
|
184
|
+
for (const row of styleRows) conv[row.key] = row.variants ? void 0 : false;
|
|
185
|
+
for (const row of advancedRows) conv[row.key] = false;
|
|
186
|
+
}
|
|
187
|
+
function fixesAllOff() {
|
|
188
|
+
conv.fixes = false;
|
|
189
|
+
}
|
|
190
|
+
function fixesAllOn() {
|
|
191
|
+
conv.fixes = true;
|
|
192
|
+
}
|
|
193
|
+
function resetSafeDefault() {
|
|
194
|
+
conv = { fixes: true, enDashRanges: false, smartQuotes: false, brandCaps: false };
|
|
195
|
+
}
|
|
196
|
+
let radioEls = $state(
|
|
197
|
+
Object.fromEntries(styleRows.filter((r) => r.variants).map((r) => [String(r.key), []]))
|
|
198
|
+
);
|
|
199
|
+
function onRadioKeydown(e, row, i) {
|
|
200
|
+
if (!row.variants) return;
|
|
201
|
+
const n = row.variants.length;
|
|
202
|
+
let next = i;
|
|
203
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (i + 1) % n;
|
|
204
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (i - 1 + n) % n;
|
|
205
|
+
else if (e.key === "Home") next = 0;
|
|
206
|
+
else if (e.key === "End") next = n - 1;
|
|
207
|
+
else return;
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
pickVariant(row.key, row.variants[next].value);
|
|
210
|
+
radioEls[String(row.key)]?.[next]?.focus();
|
|
211
|
+
}
|
|
212
|
+
const conventionsJson = $derived(JSON.stringify(conv));
|
|
213
|
+
function onoffClass(on) {
|
|
214
|
+
return `inline-flex h-[30px] items-center gap-1.5 rounded-lg border px-2.5 text-xs font-semibold ${on ? "border-primary/30 bg-primary/10 text-primary" : "border-[var(--cairn-card-border)] bg-base-100 text-[var(--color-muted)] hover:border-primary/35 hover:text-base-content"}`;
|
|
215
|
+
}
|
|
216
|
+
function segClass(on) {
|
|
217
|
+
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)]"}`;
|
|
218
|
+
}
|
|
286
219
|
</script>
|
|
287
220
|
|
|
288
221
|
<div class="mx-auto max-w-3xl px-2 py-2">
|