@kurtbruns/kestrel 1.5.0 → 1.6.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 +53 -1
- package/cli/context.mjs +20 -11
- package/cli/dev-api.mjs +73 -9
- package/cli/dev-server.mjs +72 -0
- package/cli/dev.mjs +39 -11
- package/cli/init.mjs +8 -2
- package/cli/reset.mjs +2 -3
- package/cli/seed.mjs +7 -5
- package/cli/sync-migrations.mjs +5 -1
- package/dist/public/dashboard/app.js +362 -324
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +600 -126
- package/dist/worker/index.js +1111 -802
- package/docs/get-started/02-deploy.md +1 -1
- package/docs/get-started/05-verify.md +1 -1
- package/docs/reference/01-configuration.md +1 -1
- package/migrations/0003_post_slug_auto.sql +14 -0
- package/package.json +4 -5
- package/template/claude/CLAUDE.md +2 -2
- package/template/package.json +5 -0
- package/cli/dev-port.mjs +0 -57
|
@@ -1169,6 +1169,7 @@ button:active {
|
|
|
1169
1169
|
button.primary,
|
|
1170
1170
|
button.secondary,
|
|
1171
1171
|
button.ghost,
|
|
1172
|
+
a.ghost.btn,
|
|
1172
1173
|
button.danger,
|
|
1173
1174
|
button.danger-subtle {
|
|
1174
1175
|
min-height: 35px;
|
|
@@ -1212,7 +1213,8 @@ button.secondary:hover {
|
|
|
1212
1213
|
/* Ghost — the quiet neutral role: back, cancel, a menu trigger, a pager step, a copy
|
|
1213
1214
|
control. The default for anything not asking for attention; absorbs the old
|
|
1214
1215
|
.ghost-btn / .set-btn-ghost idioms. */
|
|
1215
|
-
button.ghost
|
|
1216
|
+
button.ghost,
|
|
1217
|
+
a.ghost.btn {
|
|
1216
1218
|
display: inline-flex;
|
|
1217
1219
|
align-items: center;
|
|
1218
1220
|
gap: 6px;
|
|
@@ -1223,10 +1225,19 @@ button.ghost {
|
|
|
1223
1225
|
padding: 6px 12px;
|
|
1224
1226
|
border-radius: 6px;
|
|
1225
1227
|
}
|
|
1226
|
-
button.ghost:hover
|
|
1228
|
+
button.ghost:hover,
|
|
1229
|
+
a.ghost.btn:hover {
|
|
1227
1230
|
color: var(--fg);
|
|
1228
1231
|
border-color: var(--muted);
|
|
1229
1232
|
}
|
|
1233
|
+
/* A link that wears Ghost (`a.ghost.btn`: Web version ↗ goes to a page rather than
|
|
1234
|
+
acting on one), with a link's own underline and transition taken off so it reads as
|
|
1235
|
+
the buttons beside it. The plain `a.ghost` is the sidebar's text-link Sign out. */
|
|
1236
|
+
a.ghost.btn {
|
|
1237
|
+
text-decoration: none;
|
|
1238
|
+
line-height: normal;
|
|
1239
|
+
transition: border-color 0.12s;
|
|
1240
|
+
}
|
|
1230
1241
|
/* A copy control flashes green (--ok) once the clipboard write lands. */
|
|
1231
1242
|
button.ghost.copied {
|
|
1232
1243
|
color: var(--ok-fg);
|
|
@@ -1699,6 +1710,83 @@ th.num .th-sort {
|
|
|
1699
1710
|
align-items: center;
|
|
1700
1711
|
gap: 10px;
|
|
1701
1712
|
}
|
|
1713
|
+
/* A post's web version (#/web/post/…): the post page as it will look, filling the window
|
|
1714
|
+
with no app chrome, since it opens in a tab of its own. The window scrolls the page. */
|
|
1715
|
+
body.web-mode .sidebar,
|
|
1716
|
+
body.web-mode .nav-toggle {
|
|
1717
|
+
display: none;
|
|
1718
|
+
}
|
|
1719
|
+
body.web-mode .content {
|
|
1720
|
+
max-width: none;
|
|
1721
|
+
margin: 0;
|
|
1722
|
+
padding: 0;
|
|
1723
|
+
}
|
|
1724
|
+
/* The page sits under one strip that says where the post stands (DESIGN §7). The strip is
|
|
1725
|
+
the editor's, above the frame, and the frame is as tall as the post, so the window scrolls
|
|
1726
|
+
and the strip goes up with the page like the test pages' strip. It wears the brand accent
|
|
1727
|
+
like the reader surface's dev pill, its tag as the pill's inverted chip, and its content
|
|
1728
|
+
sits in the default template's column so it lines up with the post. The test pages carry
|
|
1729
|
+
the same strip in the reader shell (READER_BANNER_STYLE in src/lib/page.ts); keep the two
|
|
1730
|
+
in sync. */
|
|
1731
|
+
/* At least the window's height, so a post shorter than the window still has its own
|
|
1732
|
+
ground to the bottom: the frame grows into what's left, and the framed page's background
|
|
1733
|
+
fills it. */
|
|
1734
|
+
.web-page {
|
|
1735
|
+
display: flex;
|
|
1736
|
+
flex-direction: column;
|
|
1737
|
+
min-height: 100dvh;
|
|
1738
|
+
}
|
|
1739
|
+
.web-bar {
|
|
1740
|
+
flex: none;
|
|
1741
|
+
background: var(--accent);
|
|
1742
|
+
color: var(--accent-on);
|
|
1743
|
+
font-size: 13.5px;
|
|
1744
|
+
font-weight: 500;
|
|
1745
|
+
line-height: 1.4;
|
|
1746
|
+
}
|
|
1747
|
+
/* The tag runs in, inline with the sentence, so on a phone the sentence wraps beneath it at
|
|
1748
|
+
the strip's full width rather than in a narrow column beside it. */
|
|
1749
|
+
.web-bar-in {
|
|
1750
|
+
/* The default template's 664px column inside its 12px gutter, with the column's 32px
|
|
1751
|
+
padding, so the strip's text starts on the post's left edge at any width over a phone's. */
|
|
1752
|
+
max-width: 688px;
|
|
1753
|
+
margin: 0 auto;
|
|
1754
|
+
padding: 10px 44px;
|
|
1755
|
+
text-align: left;
|
|
1756
|
+
}
|
|
1757
|
+
.web-bar-tag {
|
|
1758
|
+
display: inline-block;
|
|
1759
|
+
margin-right: 10px;
|
|
1760
|
+
font-size: 10px;
|
|
1761
|
+
letter-spacing: 0.09em;
|
|
1762
|
+
text-transform: uppercase;
|
|
1763
|
+
font-weight: 700;
|
|
1764
|
+
padding: 1px 7px;
|
|
1765
|
+
border-radius: 999px;
|
|
1766
|
+
background: var(--accent-on);
|
|
1767
|
+
color: var(--accent);
|
|
1768
|
+
}
|
|
1769
|
+
.web-bar-text {
|
|
1770
|
+
overflow-wrap: anywhere;
|
|
1771
|
+
}
|
|
1772
|
+
@media (max-width: 560px) {
|
|
1773
|
+
.web-bar-in {
|
|
1774
|
+
padding: 10px 16px;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
.web-frame {
|
|
1778
|
+
display: block;
|
|
1779
|
+
flex: 1 0 auto;
|
|
1780
|
+
width: 100%;
|
|
1781
|
+
/* The post's height once measured; until then a window's, so the page doesn't flash short. */
|
|
1782
|
+
height: 100vh;
|
|
1783
|
+
border: 0;
|
|
1784
|
+
}
|
|
1785
|
+
.web-error {
|
|
1786
|
+
max-width: 640px;
|
|
1787
|
+
margin: 48px auto;
|
|
1788
|
+
padding: 0 16px;
|
|
1789
|
+
}
|
|
1702
1790
|
/* Sent record view (#148): a read-only delivery record. The outcome tiles reuse the
|
|
1703
1791
|
semantic feedback scales (--ok / --warn / --danger, DESIGN §3); a status badge, not
|
|
1704
1792
|
a feedback color, still labels the send's lifecycle. */
|
|
@@ -2332,6 +2420,13 @@ th.num .th-sort {
|
|
|
2332
2420
|
.composer-foot:hover {
|
|
2333
2421
|
color: var(--fg);
|
|
2334
2422
|
}
|
|
2423
|
+
/* On the Preview tab the image foot dims with the toolbar (`.toolbar.off`): a click there
|
|
2424
|
+
would insert into the hidden Edit text. A drop on the composer still works; it switches
|
|
2425
|
+
to Edit first, so the result is seen. */
|
|
2426
|
+
.composer-foot.off {
|
|
2427
|
+
opacity: 0.4;
|
|
2428
|
+
pointer-events: none;
|
|
2429
|
+
}
|
|
2335
2430
|
.composer-foot svg {
|
|
2336
2431
|
width: 16px;
|
|
2337
2432
|
height: 16px;
|
|
@@ -2437,29 +2532,52 @@ th.num .th-sort {
|
|
|
2437
2532
|
}
|
|
2438
2533
|
|
|
2439
2534
|
/* toasts (live region) */
|
|
2535
|
+
/* Spans the viewport and centers its toasts, rather than left: 50% plus a transform,
|
|
2536
|
+
which caps a toast's width at half the screen and wraps a sentence on a phone. */
|
|
2440
2537
|
#toasts {
|
|
2441
2538
|
position: fixed;
|
|
2442
2539
|
bottom: 20px;
|
|
2443
|
-
left:
|
|
2444
|
-
|
|
2540
|
+
left: 16px;
|
|
2541
|
+
right: 16px;
|
|
2445
2542
|
display: flex;
|
|
2446
2543
|
flex-direction: column;
|
|
2447
|
-
gap: 8px;
|
|
2448
2544
|
align-items: center;
|
|
2449
2545
|
z-index: var(--z-toast);
|
|
2450
2546
|
pointer-events: none;
|
|
2451
2547
|
}
|
|
2548
|
+
/* Spaced by margin, not the column's gap, so a leaving toast can collapse its share. */
|
|
2452
2549
|
.toast {
|
|
2550
|
+
box-sizing: border-box;
|
|
2551
|
+
max-width: 480px;
|
|
2552
|
+
margin-top: 8px;
|
|
2553
|
+
overflow: hidden;
|
|
2453
2554
|
background: var(--fg);
|
|
2454
2555
|
color: var(--bg);
|
|
2455
2556
|
padding: 10px 16px;
|
|
2456
2557
|
border-radius: 8px;
|
|
2457
2558
|
font-size: 14px;
|
|
2458
2559
|
opacity: 0;
|
|
2459
|
-
|
|
2560
|
+
transform: translateY(6px);
|
|
2561
|
+
transition:
|
|
2562
|
+
opacity 0.2s,
|
|
2563
|
+
transform 0.2s;
|
|
2460
2564
|
}
|
|
2461
2565
|
.toast.show {
|
|
2462
2566
|
opacity: 1;
|
|
2567
|
+
transform: none;
|
|
2568
|
+
}
|
|
2569
|
+
/* Leaving: fade while the height (set inline by leaveToast, since a transition can't run
|
|
2570
|
+
from auto), padding, and spacing collapse, so the stack settles smoothly. 0.3s is
|
|
2571
|
+
TOAST_LEAVE_MS in client/ui/widgets.ts. */
|
|
2572
|
+
.toast.leaving {
|
|
2573
|
+
opacity: 0;
|
|
2574
|
+
margin-top: 0;
|
|
2575
|
+
padding-block: 0;
|
|
2576
|
+
transition:
|
|
2577
|
+
opacity 0.2s,
|
|
2578
|
+
height 0.3s,
|
|
2579
|
+
margin-top 0.3s,
|
|
2580
|
+
padding-block 0.3s;
|
|
2463
2581
|
}
|
|
2464
2582
|
|
|
2465
2583
|
/* modal */
|
|
@@ -2784,7 +2902,7 @@ textarea.is-invalid {
|
|
|
2784
2902
|
.info.tip-below:focus::after {
|
|
2785
2903
|
transform: translateY(0);
|
|
2786
2904
|
}
|
|
2787
|
-
/* "Auto-generate from subject" toggle, grouped just under the slug field. */
|
|
2905
|
+
/* The "Auto-generate from subject" toggle, grouped just under the slug field. */
|
|
2788
2906
|
.slug-auto-toggle {
|
|
2789
2907
|
display: inline-flex;
|
|
2790
2908
|
align-items: center;
|
|
@@ -2802,7 +2920,7 @@ textarea.is-invalid {
|
|
|
2802
2920
|
accent-color: var(--ring);
|
|
2803
2921
|
cursor: pointer;
|
|
2804
2922
|
}
|
|
2805
|
-
/* Slug is muted while it
|
|
2923
|
+
/* Slug is muted while it follows the subject; normal once set by hand. */
|
|
2806
2924
|
#f-slug.slug-auto {
|
|
2807
2925
|
color: var(--muted);
|
|
2808
2926
|
}
|
|
@@ -3411,44 +3529,6 @@ button.api-filter-clear svg {
|
|
|
3411
3529
|
margin: 6px 0 0;
|
|
3412
3530
|
font-size: 14px;
|
|
3413
3531
|
}
|
|
3414
|
-
.api-ex {
|
|
3415
|
-
margin-top: 10px;
|
|
3416
|
-
}
|
|
3417
|
-
.api-ex-head {
|
|
3418
|
-
display: flex;
|
|
3419
|
-
align-items: center;
|
|
3420
|
-
justify-content: space-between;
|
|
3421
|
-
margin: 8px 0 4px;
|
|
3422
|
-
}
|
|
3423
|
-
/* A code block's Copy (DESIGN §1, a control outside the five roles): the word and its
|
|
3424
|
-
glyph, no fill or border, quiet on the label line it shares. The glyph turns to a check
|
|
3425
|
-
in the ok hue for a moment once the copy has landed; the word stays. */
|
|
3426
|
-
button.api-copy {
|
|
3427
|
-
display: inline-flex;
|
|
3428
|
-
align-items: center;
|
|
3429
|
-
gap: 5px;
|
|
3430
|
-
border: 0;
|
|
3431
|
-
background: transparent;
|
|
3432
|
-
color: var(--muted);
|
|
3433
|
-
font-size: 12px;
|
|
3434
|
-
padding: 2px 6px;
|
|
3435
|
-
border-radius: 5px;
|
|
3436
|
-
}
|
|
3437
|
-
button.api-copy:hover {
|
|
3438
|
-
color: var(--fg);
|
|
3439
|
-
background: var(--hover);
|
|
3440
|
-
}
|
|
3441
|
-
/* No pressed nudge: on a quiet word the button-wide half-pixel drop reads as a jump. */
|
|
3442
|
-
button.api-copy:active {
|
|
3443
|
-
transform: none;
|
|
3444
|
-
}
|
|
3445
|
-
button.api-copy svg {
|
|
3446
|
-
width: 13px;
|
|
3447
|
-
height: 13px;
|
|
3448
|
-
}
|
|
3449
|
-
button.api-copy.copied svg {
|
|
3450
|
-
color: var(--ok-fg);
|
|
3451
|
-
}
|
|
3452
3532
|
/* A query parameter the route can't be called without. */
|
|
3453
3533
|
.api-req {
|
|
3454
3534
|
margin-left: 4px;
|
|
@@ -3458,22 +3538,7 @@ button.api-copy.copied svg {
|
|
|
3458
3538
|
border-radius: 4px;
|
|
3459
3539
|
padding: 0 4px;
|
|
3460
3540
|
}
|
|
3461
|
-
.api-ex-label {
|
|
3462
|
-
display: block;
|
|
3463
|
-
font-size: 12px;
|
|
3464
|
-
font-weight: 600;
|
|
3465
|
-
color: var(--muted);
|
|
3466
|
-
}
|
|
3467
3541
|
|
|
3468
|
-
.api-ex pre {
|
|
3469
|
-
margin: 0;
|
|
3470
|
-
overflow-x: auto;
|
|
3471
|
-
background: var(--bg);
|
|
3472
|
-
border: 1px solid var(--line);
|
|
3473
|
-
border-radius: 6px;
|
|
3474
|
-
padding: 10px 12px;
|
|
3475
|
-
font: 12.5px / 1.5 var(--mono);
|
|
3476
|
-
}
|
|
3477
3542
|
.api-empty {
|
|
3478
3543
|
margin: 20px 0;
|
|
3479
3544
|
}
|
|
@@ -3852,9 +3917,6 @@ button.api-copy.copied svg {
|
|
|
3852
3917
|
border-radius: 6px;
|
|
3853
3918
|
padding: 6px 9px;
|
|
3854
3919
|
}
|
|
3855
|
-
.pub-row button.ghost {
|
|
3856
|
-
flex: 0 0 auto;
|
|
3857
|
-
}
|
|
3858
3920
|
.pub-foot {
|
|
3859
3921
|
margin-top: 4px;
|
|
3860
3922
|
}
|
|
@@ -3928,6 +3990,16 @@ button.api-copy.copied svg {
|
|
|
3928
3990
|
.setup-steps li button {
|
|
3929
3991
|
flex: 0 0 auto;
|
|
3930
3992
|
}
|
|
3993
|
+
/* The setup step's Copy keeps the step's button look, with the shared Copy's glyph. */
|
|
3994
|
+
.setup-steps li button[data-copy] {
|
|
3995
|
+
display: inline-flex;
|
|
3996
|
+
align-items: center;
|
|
3997
|
+
gap: 5px;
|
|
3998
|
+
}
|
|
3999
|
+
.setup-steps li button[data-copy] svg {
|
|
4000
|
+
width: 14px;
|
|
4001
|
+
height: 14px;
|
|
4002
|
+
}
|
|
3931
4003
|
.setup-url {
|
|
3932
4004
|
font: 12.5px var(--mono);
|
|
3933
4005
|
color: var(--muted);
|
|
@@ -3968,6 +4040,75 @@ button.api-copy.copied svg {
|
|
|
3968
4040
|
.set-sec {
|
|
3969
4041
|
margin-bottom: 34px;
|
|
3970
4042
|
}
|
|
4043
|
+
|
|
4044
|
+
/* --- copying text out of the app (DESIGN §1, ui/copy.ts) ---
|
|
4045
|
+
A labelled block (a code block, or the API reference's query and content-type tables):
|
|
4046
|
+
its label line, then the block. The Copy is a control outside the five roles: the word
|
|
4047
|
+
and its glyph, no fill or border, quiet on the line it shares; the glyph turns to a
|
|
4048
|
+
check in the ok hue for a moment once a copy has landed, and the word stays. A Copy
|
|
4049
|
+
beside a single value (an address) is the same control. --- */
|
|
4050
|
+
.block {
|
|
4051
|
+
margin-top: 10px;
|
|
4052
|
+
}
|
|
4053
|
+
.block-head {
|
|
4054
|
+
display: flex;
|
|
4055
|
+
align-items: center;
|
|
4056
|
+
justify-content: space-between;
|
|
4057
|
+
margin: 8px 0 4px;
|
|
4058
|
+
}
|
|
4059
|
+
button.copy-btn {
|
|
4060
|
+
display: inline-flex;
|
|
4061
|
+
align-items: center;
|
|
4062
|
+
gap: 5px;
|
|
4063
|
+
border: 0;
|
|
4064
|
+
background: transparent;
|
|
4065
|
+
color: var(--muted);
|
|
4066
|
+
font-size: 13px;
|
|
4067
|
+
padding: 4px 8px;
|
|
4068
|
+
border-radius: 5px;
|
|
4069
|
+
}
|
|
4070
|
+
button.copy-btn:hover {
|
|
4071
|
+
color: var(--fg);
|
|
4072
|
+
background: var(--hover);
|
|
4073
|
+
}
|
|
4074
|
+
/* No pressed nudge: on a quiet word the button-wide half-pixel drop reads as a jump. */
|
|
4075
|
+
button.copy-btn:active {
|
|
4076
|
+
transform: none;
|
|
4077
|
+
}
|
|
4078
|
+
button.copy-btn svg {
|
|
4079
|
+
width: 14px;
|
|
4080
|
+
height: 14px;
|
|
4081
|
+
}
|
|
4082
|
+
/* Any Copy that has just landed, whatever button it rides (ui/copy.ts). */
|
|
4083
|
+
button.copied svg {
|
|
4084
|
+
color: var(--ok-fg);
|
|
4085
|
+
}
|
|
4086
|
+
.block-label {
|
|
4087
|
+
display: block;
|
|
4088
|
+
font-size: 12px;
|
|
4089
|
+
font-weight: 600;
|
|
4090
|
+
color: var(--muted);
|
|
4091
|
+
}
|
|
4092
|
+
.block pre {
|
|
4093
|
+
margin: 0;
|
|
4094
|
+
overflow-x: auto;
|
|
4095
|
+
background: var(--bg);
|
|
4096
|
+
border: 1px solid var(--line);
|
|
4097
|
+
border-radius: 6px;
|
|
4098
|
+
padding: 10px 12px;
|
|
4099
|
+
font: 12.5px / 1.5 var(--mono);
|
|
4100
|
+
}
|
|
4101
|
+
/* A Copy beside a value keeps its size however long the value runs. */
|
|
4102
|
+
.pub-row button.copy-btn {
|
|
4103
|
+
flex: 0 0 auto;
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
/* --- groups: the sections kept together by whom they serve (DESIGN §5). A group has no
|
|
4107
|
+
heading, since each section's title already says what it is; a wider gap than the one
|
|
4108
|
+
between sections is all that separates one group from the next. --- */
|
|
4109
|
+
.set-group + .set-group {
|
|
4110
|
+
margin-top: 22px;
|
|
4111
|
+
}
|
|
3971
4112
|
.set-sec-head {
|
|
3972
4113
|
display: flex;
|
|
3973
4114
|
align-items: center;
|
|
@@ -4065,7 +4206,7 @@ button.api-copy.copied svg {
|
|
|
4065
4206
|
padding: 18px;
|
|
4066
4207
|
}
|
|
4067
4208
|
|
|
4068
|
-
/* Kestrel-accent links inside the settings cards (operator-guide links
|
|
4209
|
+
/* Kestrel-accent links inside the settings cards (operator-guide links). */
|
|
4069
4210
|
.set-link {
|
|
4070
4211
|
color: var(--accent);
|
|
4071
4212
|
font-weight: 550;
|
|
@@ -4087,6 +4228,11 @@ button.api-copy.copied svg {
|
|
|
4087
4228
|
flex-direction: column;
|
|
4088
4229
|
gap: 8px;
|
|
4089
4230
|
}
|
|
4231
|
+
/* The hint and the not-square warning stack in one column, under the tile on a wide
|
|
4232
|
+
screen and beside it where the slot becomes a row. */
|
|
4233
|
+
.set-logo-notes {
|
|
4234
|
+
min-width: 0;
|
|
4235
|
+
}
|
|
4090
4236
|
.set-logo-tile {
|
|
4091
4237
|
width: 100%;
|
|
4092
4238
|
aspect-ratio: 1;
|
|
@@ -4481,16 +4627,6 @@ button.set-field-remove {
|
|
|
4481
4627
|
.set-ce-edit {
|
|
4482
4628
|
padding: 18px;
|
|
4483
4629
|
}
|
|
4484
|
-
.set-ce-lock {
|
|
4485
|
-
display: flex;
|
|
4486
|
-
align-items: center;
|
|
4487
|
-
gap: 6px;
|
|
4488
|
-
}
|
|
4489
|
-
.set-ce-lock svg {
|
|
4490
|
-
width: 13px;
|
|
4491
|
-
height: 13px;
|
|
4492
|
-
flex: 0 0 auto;
|
|
4493
|
-
}
|
|
4494
4630
|
.set-ce-editfoot {
|
|
4495
4631
|
display: flex;
|
|
4496
4632
|
align-items: center;
|
|
@@ -4589,89 +4725,194 @@ button.set-field-remove {
|
|
|
4589
4725
|
flex: 1;
|
|
4590
4726
|
}
|
|
4591
4727
|
|
|
4592
|
-
/* --- ways to subscribe:
|
|
4593
|
-
|
|
4728
|
+
/* --- ways to subscribe (DESIGN §5): one card, the two ways as bands in reading order,
|
|
4729
|
+
the page then the form, split by a full-width rule like the other cards' bands. Each
|
|
4730
|
+
band has the same head (a glyph, a title, one line) so they read as two options. The
|
|
4731
|
+
card folds by the section's own width, since the sidebar decides how wide Settings is. --- */
|
|
4732
|
+
.set-sec-subscribe {
|
|
4733
|
+
container-type: inline-size;
|
|
4734
|
+
}
|
|
4735
|
+
.set-way + .set-way {
|
|
4736
|
+
border-top: 1px solid var(--line);
|
|
4737
|
+
}
|
|
4738
|
+
/* A control that switches what the band shows (the embed's Styled | Plain) sits at
|
|
4739
|
+
the head's right end, as the reader pages' width toggle does. */
|
|
4740
|
+
.set-way-head {
|
|
4594
4741
|
display: flex;
|
|
4595
|
-
align-items:
|
|
4742
|
+
align-items: flex-start;
|
|
4596
4743
|
justify-content: space-between;
|
|
4597
|
-
gap:
|
|
4598
|
-
|
|
4599
|
-
|
|
4744
|
+
gap: 10px 16px;
|
|
4745
|
+
padding: 18px 18px 0;
|
|
4746
|
+
}
|
|
4747
|
+
/* The name takes the room the control leaves, so the control keeps its size. */
|
|
4748
|
+
.set-way-name {
|
|
4749
|
+
flex: 1 1 0;
|
|
4750
|
+
min-width: 0;
|
|
4600
4751
|
}
|
|
4601
|
-
.
|
|
4752
|
+
/* Set like a field label, its glyph like a note's, so the band ranks with the other cards. */
|
|
4753
|
+
.set-way-title {
|
|
4754
|
+
display: flex;
|
|
4755
|
+
align-items: center;
|
|
4756
|
+
gap: 7px;
|
|
4602
4757
|
margin: 0;
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
border-radius: 8px;
|
|
4607
|
-
padding: 12px 14px;
|
|
4608
|
-
font: 12.5px / 1.6 var(--mono);
|
|
4758
|
+
font-size: 13px;
|
|
4759
|
+
font-weight: 600;
|
|
4760
|
+
color: var(--fg);
|
|
4609
4761
|
}
|
|
4610
|
-
.set-
|
|
4611
|
-
|
|
4762
|
+
.set-way-title svg {
|
|
4763
|
+
width: 15px;
|
|
4764
|
+
height: 15px;
|
|
4765
|
+
flex: 0 0 auto;
|
|
4766
|
+
fill: currentColor;
|
|
4767
|
+
color: var(--muted);
|
|
4768
|
+
}
|
|
4769
|
+
.set-way-line {
|
|
4770
|
+
margin: 3px 0 0;
|
|
4771
|
+
font-size: 12.5px;
|
|
4772
|
+
color: var(--muted);
|
|
4773
|
+
line-height: 1.5;
|
|
4774
|
+
}
|
|
4775
|
+
.set-way-body {
|
|
4776
|
+
padding: 10px 18px 18px;
|
|
4777
|
+
}
|
|
4778
|
+
/* The address is a value to copy, not a field: plain monospace, its Copy and Open beside it. */
|
|
4779
|
+
.set-way-addr {
|
|
4780
|
+
display: flex;
|
|
4781
|
+
align-items: center;
|
|
4782
|
+
flex-wrap: wrap;
|
|
4783
|
+
gap: 4px 6px;
|
|
4784
|
+
min-width: 0;
|
|
4785
|
+
}
|
|
4786
|
+
.set-addr {
|
|
4787
|
+
font: 13.5px var(--mono);
|
|
4788
|
+
color: var(--fg);
|
|
4789
|
+
overflow-wrap: anywhere;
|
|
4790
|
+
margin-right: 4px;
|
|
4791
|
+
}
|
|
4792
|
+
/* The form as it will look beside the code that makes it, both always shown, edge to
|
|
4793
|
+
edge in the card like the reader pages' outline beside its frame. The preview grows to
|
|
4794
|
+
480px, near the styled form's own 460px cap, so it shows the form about as wide as a
|
|
4795
|
+
site will; the code keeps 280px, so on a narrower card the preview gives way first. */
|
|
4796
|
+
.set-embed {
|
|
4797
|
+
display: grid;
|
|
4798
|
+
grid-template-columns: minmax(240px, 480px) minmax(280px, 1fr);
|
|
4799
|
+
margin-top: 16px;
|
|
4800
|
+
border-top: 1px solid var(--line);
|
|
4612
4801
|
}
|
|
4613
4802
|
.set-embed-preview {
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
padding: 18px 16px;
|
|
4803
|
+
display: flex;
|
|
4804
|
+
align-items: center;
|
|
4805
|
+
padding: 18px;
|
|
4618
4806
|
background: var(--sunken);
|
|
4807
|
+
border-right: 1px solid var(--line);
|
|
4808
|
+
min-width: 0;
|
|
4619
4809
|
}
|
|
4620
|
-
.set-embed-
|
|
4621
|
-
|
|
4622
|
-
font-weight: 650;
|
|
4623
|
-
letter-spacing: 0.07em;
|
|
4624
|
-
text-transform: uppercase;
|
|
4625
|
-
color: var(--muted);
|
|
4626
|
-
margin-bottom: 12px;
|
|
4810
|
+
.set-embed-preview > form {
|
|
4811
|
+
width: 100%;
|
|
4627
4812
|
}
|
|
4628
|
-
/*
|
|
4629
|
-
.
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4813
|
+
/* The code wraps rather than scrolling sideways, so the Styled snippet's long lines read
|
|
4814
|
+
whole in the column; the copied text is unchanged. */
|
|
4815
|
+
.set-embed-code {
|
|
4816
|
+
margin: 0;
|
|
4817
|
+
min-width: 0;
|
|
4818
|
+
padding: 12px 14px;
|
|
4819
|
+
max-height: 220px;
|
|
4820
|
+
overflow: auto;
|
|
4821
|
+
font: 12px / 1.55 var(--mono);
|
|
4635
4822
|
}
|
|
4636
|
-
.set-
|
|
4637
|
-
|
|
4638
|
-
|
|
4823
|
+
.set-embed-code code {
|
|
4824
|
+
white-space: pre-wrap;
|
|
4825
|
+
overflow-wrap: anywhere;
|
|
4639
4826
|
}
|
|
4640
|
-
|
|
4641
|
-
|
|
4827
|
+
/* The band ends in its one act: read top to bottom, choose, see the form and its code,
|
|
4828
|
+
then copy. A strip across the card's foot, as Notifications ends in its test row. */
|
|
4829
|
+
.set-embed-foot {
|
|
4830
|
+
display: flex;
|
|
4831
|
+
justify-content: flex-end;
|
|
4832
|
+
padding: 12px 18px;
|
|
4833
|
+
border-top: 1px solid var(--line);
|
|
4834
|
+
}
|
|
4835
|
+
/* Copy HTML is Secondary with the shared Copy's word and glyph, as the setup step's is. */
|
|
4836
|
+
button.set-embed-copy {
|
|
4837
|
+
display: inline-flex;
|
|
4838
|
+
align-items: center;
|
|
4839
|
+
gap: 6px;
|
|
4840
|
+
}
|
|
4841
|
+
button.set-embed-copy svg {
|
|
4842
|
+
width: 14px;
|
|
4843
|
+
height: 14px;
|
|
4844
|
+
}
|
|
4845
|
+
@container (max-width: 620px) {
|
|
4846
|
+
/* No room beside the title: the toggle drops under the band's line, at its left. */
|
|
4847
|
+
.set-way-head {
|
|
4848
|
+
flex-direction: column;
|
|
4849
|
+
}
|
|
4850
|
+
.set-way-name {
|
|
4851
|
+
flex-basis: auto;
|
|
4852
|
+
}
|
|
4853
|
+
.set-embed {
|
|
4854
|
+
grid-template-columns: minmax(0, 1fr);
|
|
4855
|
+
}
|
|
4856
|
+
.set-embed-preview {
|
|
4857
|
+
border-right: 0;
|
|
4858
|
+
border-bottom: 1px solid var(--line);
|
|
4859
|
+
}
|
|
4642
4860
|
}
|
|
4861
|
+
/* Rendered form previews (inert: they never submit here). The plain form is bare
|
|
4862
|
+
markup, so it previews with the browser's own controls and inline flow, as a page
|
|
4863
|
+
with no styles of its own would show it, never the admin's inputs. */
|
|
4864
|
+
.set-pf-plain,
|
|
4865
|
+
.set-pf-plain :is(label, input, button) {
|
|
4866
|
+
all: revert;
|
|
4867
|
+
}
|
|
4868
|
+
.set-pf-plain input {
|
|
4869
|
+
margin: 4px 4px 4px 0;
|
|
4870
|
+
}
|
|
4871
|
+
/* The styled form wears the hosted subscribe page's look (SPEC §5), at the snippet's own
|
|
4872
|
+
sizes, so the preview is the form a publisher copies. */
|
|
4643
4873
|
.set-pf-styled {
|
|
4644
|
-
max-width:
|
|
4874
|
+
max-width: 460px;
|
|
4875
|
+
font-size: 16px;
|
|
4645
4876
|
}
|
|
4646
4877
|
.set-pf-styled .l {
|
|
4647
4878
|
display: block;
|
|
4648
|
-
font
|
|
4649
|
-
|
|
4650
|
-
|
|
4879
|
+
font:
|
|
4880
|
+
600 22px / 1.2 Georgia,
|
|
4881
|
+
"Times New Roman",
|
|
4882
|
+
serif;
|
|
4883
|
+
letter-spacing: -0.01em;
|
|
4884
|
+
margin-bottom: 12px;
|
|
4651
4885
|
color: var(--fg);
|
|
4652
4886
|
}
|
|
4653
4887
|
.set-pf-styled .rowf {
|
|
4654
4888
|
display: flex;
|
|
4655
|
-
|
|
4889
|
+
flex-wrap: wrap;
|
|
4890
|
+
gap: 10px;
|
|
4656
4891
|
}
|
|
4657
4892
|
.set-pf-styled input {
|
|
4658
|
-
flex: 1;
|
|
4893
|
+
flex: 1 1 220px;
|
|
4659
4894
|
min-width: 0;
|
|
4895
|
+
padding: 11px 13px;
|
|
4896
|
+
border-radius: 9px;
|
|
4897
|
+
font-size: 16px;
|
|
4660
4898
|
}
|
|
4661
4899
|
/* The styled subscribe-form preview shows reader-facing chrome, not an admin control,
|
|
4662
|
-
so it isn't one of the five button roles
|
|
4900
|
+
so it isn't one of the five button roles, but it rides the neutral --ink token
|
|
4663
4901
|
(was a hardcoded near-black that vanished in dark mode) so it themes correctly. */
|
|
4664
4902
|
.set-pf-styled .sub-btn {
|
|
4665
4903
|
flex: 0 0 auto;
|
|
4904
|
+
padding: 11px 20px;
|
|
4905
|
+
border-radius: 9px;
|
|
4666
4906
|
background: var(--ink);
|
|
4667
4907
|
color: var(--ink-on);
|
|
4668
4908
|
border-color: var(--ink);
|
|
4909
|
+
font-size: 15px;
|
|
4669
4910
|
font-weight: 600;
|
|
4670
4911
|
white-space: nowrap;
|
|
4671
4912
|
}
|
|
4672
4913
|
.set-pf-fine {
|
|
4673
|
-
margin:
|
|
4674
|
-
font-size:
|
|
4914
|
+
margin: 12px 0 0;
|
|
4915
|
+
font-size: 12.5px;
|
|
4675
4916
|
color: var(--muted);
|
|
4676
4917
|
}
|
|
4677
4918
|
|
|
@@ -5028,10 +5269,6 @@ button.set-field-remove {
|
|
|
5028
5269
|
background: var(--accent-soft);
|
|
5029
5270
|
border-color: color-mix(in srgb, var(--accent) 30%, var(--line-soft));
|
|
5030
5271
|
}
|
|
5031
|
-
.set-icon-btn.copied {
|
|
5032
|
-
color: var(--ok-fg);
|
|
5033
|
-
border-color: color-mix(in srgb, var(--ok-fg) 40%, var(--line-soft));
|
|
5034
|
-
}
|
|
5035
5272
|
/* Required-token pills (issue #123 revisit): live green/red for the two tokens whose
|
|
5036
5273
|
absence blocks a save, shown upper-right of the editor toolbar. */
|
|
5037
5274
|
.set-tpl-required {
|
|
@@ -5184,6 +5421,243 @@ button.set-field-remove {
|
|
|
5184
5421
|
color: var(--muted);
|
|
5185
5422
|
}
|
|
5186
5423
|
|
|
5424
|
+
/* --- Reader pages: an outline of every page a reader can land on, beside one framed page ---
|
|
5425
|
+
The journeys are the outline's headings, set like section titles and never controls;
|
|
5426
|
+
the pages are its rows. A journey's steps are numbered on a thread, peers carry a dot,
|
|
5427
|
+
and an off-path page carries a dashed branch mark. The section folds by its own width,
|
|
5428
|
+
not the window's (the sidebar decides how wide Settings is): too narrow for the outline,
|
|
5429
|
+
it becomes one grouped select, and the width toggle goes, since the frame is already
|
|
5430
|
+
no wider than a phone. The page brings its own ground, so the frame has none. */
|
|
5431
|
+
.set-rp-sec {
|
|
5432
|
+
container-type: inline-size;
|
|
5433
|
+
}
|
|
5434
|
+
.set-rp {
|
|
5435
|
+
display: grid;
|
|
5436
|
+
grid-template-columns: 210px minmax(0, 1fr);
|
|
5437
|
+
overflow: hidden;
|
|
5438
|
+
}
|
|
5439
|
+
.set-rp-nav {
|
|
5440
|
+
border-right: 1px solid var(--line);
|
|
5441
|
+
background: var(--sunken);
|
|
5442
|
+
padding: 14px 10px 18px;
|
|
5443
|
+
}
|
|
5444
|
+
.set-rp-journey + .set-rp-journey {
|
|
5445
|
+
margin-top: 16px;
|
|
5446
|
+
}
|
|
5447
|
+
.set-rp-jhead {
|
|
5448
|
+
display: flex;
|
|
5449
|
+
align-items: center;
|
|
5450
|
+
gap: 7px;
|
|
5451
|
+
margin: 0 6px 6px;
|
|
5452
|
+
font-size: 11px;
|
|
5453
|
+
font-weight: 650;
|
|
5454
|
+
letter-spacing: 0.07em;
|
|
5455
|
+
text-transform: uppercase;
|
|
5456
|
+
color: var(--muted);
|
|
5457
|
+
}
|
|
5458
|
+
.set-rp-jhead svg {
|
|
5459
|
+
width: 13px;
|
|
5460
|
+
height: 13px;
|
|
5461
|
+
}
|
|
5462
|
+
.set-rp-journey.current .set-rp-jhead {
|
|
5463
|
+
color: var(--fg);
|
|
5464
|
+
}
|
|
5465
|
+
.set-rp-list {
|
|
5466
|
+
list-style: none;
|
|
5467
|
+
margin: 0;
|
|
5468
|
+
padding: 0;
|
|
5469
|
+
position: relative;
|
|
5470
|
+
}
|
|
5471
|
+
/* The thread behind a journey's numbered steps, from the first mark to the last row. */
|
|
5472
|
+
.set-rp-list.steps::before {
|
|
5473
|
+
content: "";
|
|
5474
|
+
position: absolute;
|
|
5475
|
+
left: 17px;
|
|
5476
|
+
top: 14px;
|
|
5477
|
+
bottom: 14px;
|
|
5478
|
+
width: 1px;
|
|
5479
|
+
background: var(--line-soft);
|
|
5480
|
+
}
|
|
5481
|
+
.set-rp-item {
|
|
5482
|
+
width: 100%;
|
|
5483
|
+
display: flex;
|
|
5484
|
+
align-items: center;
|
|
5485
|
+
gap: 9px;
|
|
5486
|
+
text-align: left;
|
|
5487
|
+
border: 0;
|
|
5488
|
+
background: transparent;
|
|
5489
|
+
border-radius: 7px;
|
|
5490
|
+
padding: 5px 8px;
|
|
5491
|
+
font-size: 13px;
|
|
5492
|
+
font-weight: 400;
|
|
5493
|
+
color: var(--fg);
|
|
5494
|
+
cursor: pointer;
|
|
5495
|
+
}
|
|
5496
|
+
.set-rp-item:hover {
|
|
5497
|
+
background: var(--hover);
|
|
5498
|
+
}
|
|
5499
|
+
.set-rp-item[aria-current="true"] {
|
|
5500
|
+
background: var(--accent-soft);
|
|
5501
|
+
color: var(--accent);
|
|
5502
|
+
font-weight: 600;
|
|
5503
|
+
}
|
|
5504
|
+
.set-rp-item.off {
|
|
5505
|
+
color: var(--muted);
|
|
5506
|
+
}
|
|
5507
|
+
.set-rp-item.off[aria-current="true"] {
|
|
5508
|
+
color: var(--accent);
|
|
5509
|
+
}
|
|
5510
|
+
.set-rp-mark {
|
|
5511
|
+
position: relative;
|
|
5512
|
+
z-index: 1;
|
|
5513
|
+
flex: 0 0 auto;
|
|
5514
|
+
display: grid;
|
|
5515
|
+
place-items: center;
|
|
5516
|
+
width: 19px;
|
|
5517
|
+
height: 19px;
|
|
5518
|
+
border-radius: 50%;
|
|
5519
|
+
border: 1px solid var(--line-soft);
|
|
5520
|
+
background: var(--card);
|
|
5521
|
+
font-size: 10.5px;
|
|
5522
|
+
font-weight: 650;
|
|
5523
|
+
color: var(--muted);
|
|
5524
|
+
}
|
|
5525
|
+
.set-rp-item[aria-current="true"] .set-rp-mark {
|
|
5526
|
+
border-color: var(--accent);
|
|
5527
|
+
color: var(--accent);
|
|
5528
|
+
}
|
|
5529
|
+
.set-rp-item.off .set-rp-mark {
|
|
5530
|
+
border-style: dashed;
|
|
5531
|
+
background: transparent;
|
|
5532
|
+
}
|
|
5533
|
+
.set-rp-dot {
|
|
5534
|
+
width: 3px;
|
|
5535
|
+
height: 3px;
|
|
5536
|
+
border-radius: 50%;
|
|
5537
|
+
background: currentColor;
|
|
5538
|
+
}
|
|
5539
|
+
.set-rp-mark svg {
|
|
5540
|
+
width: 11px;
|
|
5541
|
+
height: 11px;
|
|
5542
|
+
}
|
|
5543
|
+
.set-rp-pick {
|
|
5544
|
+
display: none;
|
|
5545
|
+
}
|
|
5546
|
+
.set-rp-pick select {
|
|
5547
|
+
width: 100%;
|
|
5548
|
+
}
|
|
5549
|
+
.set-rp-main {
|
|
5550
|
+
min-width: 0;
|
|
5551
|
+
display: flex;
|
|
5552
|
+
flex-direction: column;
|
|
5553
|
+
}
|
|
5554
|
+
.set-rp-top {
|
|
5555
|
+
display: flex;
|
|
5556
|
+
align-items: flex-start;
|
|
5557
|
+
justify-content: space-between;
|
|
5558
|
+
gap: 12px;
|
|
5559
|
+
padding: 14px 16px 12px;
|
|
5560
|
+
border-bottom: 1px solid var(--line);
|
|
5561
|
+
}
|
|
5562
|
+
.set-rp-crumb {
|
|
5563
|
+
display: flex;
|
|
5564
|
+
flex-wrap: wrap;
|
|
5565
|
+
align-items: center;
|
|
5566
|
+
gap: 6px;
|
|
5567
|
+
margin: 0 0 3px;
|
|
5568
|
+
font-size: 14px;
|
|
5569
|
+
font-weight: 650;
|
|
5570
|
+
}
|
|
5571
|
+
.set-rp-crumb-j {
|
|
5572
|
+
color: var(--muted);
|
|
5573
|
+
font-weight: 500;
|
|
5574
|
+
}
|
|
5575
|
+
.set-rp-crumb-sep {
|
|
5576
|
+
color: var(--line-soft);
|
|
5577
|
+
font-weight: 400;
|
|
5578
|
+
}
|
|
5579
|
+
.set-rp-note {
|
|
5580
|
+
margin: 0;
|
|
5581
|
+
font-size: 12.5px;
|
|
5582
|
+
line-height: 1.5;
|
|
5583
|
+
color: var(--muted);
|
|
5584
|
+
max-width: 60ch;
|
|
5585
|
+
}
|
|
5586
|
+
.set-rp .set-wtog {
|
|
5587
|
+
flex: 0 0 auto;
|
|
5588
|
+
}
|
|
5589
|
+
.set-rp .wtog-btn {
|
|
5590
|
+
display: inline-flex;
|
|
5591
|
+
align-items: center;
|
|
5592
|
+
gap: 5px;
|
|
5593
|
+
}
|
|
5594
|
+
.set-rp .wtog-btn svg {
|
|
5595
|
+
width: 13px;
|
|
5596
|
+
height: 13px;
|
|
5597
|
+
}
|
|
5598
|
+
/* Where a reader meets the page, and that this one is a sample: the cue a strip inside
|
|
5599
|
+
the page would otherwise carry, kept in the app's chrome around it. */
|
|
5600
|
+
.set-rp-at {
|
|
5601
|
+
display: flex;
|
|
5602
|
+
flex-wrap: wrap;
|
|
5603
|
+
justify-content: space-between;
|
|
5604
|
+
gap: 4px 10px;
|
|
5605
|
+
padding: 7px 16px;
|
|
5606
|
+
border-bottom: 1px solid var(--line);
|
|
5607
|
+
font-size: 11.5px;
|
|
5608
|
+
color: var(--muted);
|
|
5609
|
+
}
|
|
5610
|
+
.set-rp-at .mono {
|
|
5611
|
+
overflow-wrap: anywhere;
|
|
5612
|
+
}
|
|
5613
|
+
.set-rp-stage.phone {
|
|
5614
|
+
padding: 18px 12px;
|
|
5615
|
+
background: var(--sunken);
|
|
5616
|
+
}
|
|
5617
|
+
.set-rp-frame {
|
|
5618
|
+
display: block;
|
|
5619
|
+
width: 100%;
|
|
5620
|
+
min-height: 320px;
|
|
5621
|
+
border: 0;
|
|
5622
|
+
}
|
|
5623
|
+
.set-rp-stage.phone .set-rp-frame {
|
|
5624
|
+
width: 375px;
|
|
5625
|
+
max-width: 100%;
|
|
5626
|
+
margin: 0 auto;
|
|
5627
|
+
border: 1px solid var(--line);
|
|
5628
|
+
border-radius: 10px;
|
|
5629
|
+
}
|
|
5630
|
+
.set-rp-error {
|
|
5631
|
+
margin: 0;
|
|
5632
|
+
padding: 12px 16px;
|
|
5633
|
+
font-size: 13px;
|
|
5634
|
+
color: var(--danger-fg);
|
|
5635
|
+
}
|
|
5636
|
+
@container (max-width: 640px) {
|
|
5637
|
+
.set-rp {
|
|
5638
|
+
grid-template-columns: minmax(0, 1fr);
|
|
5639
|
+
}
|
|
5640
|
+
.set-rp-nav,
|
|
5641
|
+
.set-rp .set-wtog {
|
|
5642
|
+
display: none;
|
|
5643
|
+
}
|
|
5644
|
+
.set-rp-pick {
|
|
5645
|
+
display: block;
|
|
5646
|
+
padding: 12px 14px;
|
|
5647
|
+
border-bottom: 1px solid var(--line);
|
|
5648
|
+
background: var(--sunken);
|
|
5649
|
+
}
|
|
5650
|
+
.set-rp-stage.phone {
|
|
5651
|
+
padding: 0;
|
|
5652
|
+
background: none;
|
|
5653
|
+
}
|
|
5654
|
+
.set-rp-stage.phone .set-rp-frame {
|
|
5655
|
+
width: 100%;
|
|
5656
|
+
border: 0;
|
|
5657
|
+
border-radius: 0;
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
|
|
5187
5661
|
/* --- shared unsaved-changes bar -------------------------------------------------
|
|
5188
5662
|
One full-width banner fixed to the bottom of the viewport, used by every surface
|
|
5189
5663
|
with an explicit save + a revertible baseline (Settings and the Template page).
|