@kurtbruns/kestrel 1.9.0 → 1.10.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 +45 -1
- package/README.md +3 -1
- package/cli/context.mjs +5 -1
- package/dist/public/dashboard/app.js +367 -256
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +493 -24
- package/dist/worker/index.js +2764 -2067
- package/docs/README.md +3 -4
- package/docs/get-started/04-ses.md +53 -13
- package/docs/get-started/05-verify.md +5 -3
- package/docs/get-started/06-go-live.md +6 -4
- package/docs/guides/01-connect-claude.md +4 -0
- package/docs/guides/03-notifications.md +1 -1
- package/docs/publishing/01-your-list.md +86 -0
- package/docs/publishing/02-sending-rules.md +35 -0
- package/docs/publishing/03-name-your-publication.md +106 -0
- package/docs/publishing/04-write-a-post.md +98 -0
- package/docs/publishing/05-scheduling-and-sending.md +72 -0
- package/docs/publishing/06-after-a-send.md +109 -0
- package/docs/publishing/07-working-with-claude.md +48 -0
- package/docs/{reference/03-import-format.md → publishing/08-import-format.md} +12 -12
- package/docs/publishing/09-share-your-subscribe-link.md +73 -0
- package/docs/publishing/10-check-your-setup.md +48 -0
- package/docs/publishing/README.md +26 -0
- package/docs/reference/01-configuration.md +2 -2
- package/docs/reference/02-sending-domain-dns.md +4 -2
- package/package.json +2 -1
- package/template/claude/CLAUDE.md +5 -2
- package/template/claude/skills/migrate-list/SKILL.md +56 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Kestrel</title>
|
|
7
7
|
<link rel="icon" href="/favicon.svg">
|
|
8
|
-
<link rel="stylesheet" href="./styles.css?v=
|
|
8
|
+
<link rel="stylesheet" href="./styles.css?v=abd15153">
|
|
9
9
|
<script>
|
|
10
10
|
// Sidebar mode guard — set the width-based data-nav on <html> before first paint so the
|
|
11
11
|
// sidebar renders in its final width with no flash. The collapse toggle's override is
|
|
@@ -113,6 +113,6 @@
|
|
|
113
113
|
</section>
|
|
114
114
|
<div id="toasts" role="status" aria-live="polite"></div>
|
|
115
115
|
|
|
116
|
-
<script src="./app.js?v=
|
|
116
|
+
<script src="./app.js?v=183fc216"></script>
|
|
117
117
|
</body>
|
|
118
118
|
</html>
|
|
@@ -438,6 +438,39 @@ body.nav-resizing .sidebar {
|
|
|
438
438
|
font-weight: 600;
|
|
439
439
|
margin: 0 0 10px;
|
|
440
440
|
}
|
|
441
|
+
/* The tab per doc set under the docs header: an underlined strip, the current set in the
|
|
442
|
+
foreground over an accent rule, the way tabs read as switchable. Links, since each set's
|
|
443
|
+
index is its own address. */
|
|
444
|
+
.docs-tabs {
|
|
445
|
+
display: flex;
|
|
446
|
+
gap: 24px;
|
|
447
|
+
border-bottom: 1px solid var(--line);
|
|
448
|
+
margin: 0 0 20px;
|
|
449
|
+
}
|
|
450
|
+
.docs-tabs a {
|
|
451
|
+
padding: 8px 0 10px;
|
|
452
|
+
margin-bottom: -1px;
|
|
453
|
+
border-bottom: 2px solid transparent;
|
|
454
|
+
color: var(--muted);
|
|
455
|
+
font-size: 15px;
|
|
456
|
+
font-weight: 600;
|
|
457
|
+
text-decoration: none;
|
|
458
|
+
}
|
|
459
|
+
.docs-tabs a:hover {
|
|
460
|
+
color: var(--fg);
|
|
461
|
+
}
|
|
462
|
+
.docs-tabs a[aria-current="page"] {
|
|
463
|
+
color: var(--fg);
|
|
464
|
+
border-bottom-color: var(--accent);
|
|
465
|
+
}
|
|
466
|
+
/* A doc page's set, in the eyebrow over its title: the link back to that set's index. */
|
|
467
|
+
.doc .doc-set a {
|
|
468
|
+
color: var(--muted);
|
|
469
|
+
text-decoration: none;
|
|
470
|
+
}
|
|
471
|
+
.doc .doc-set a:hover {
|
|
472
|
+
color: var(--accent);
|
|
473
|
+
}
|
|
441
474
|
.docs-index > h1 {
|
|
442
475
|
font-size: 28px;
|
|
443
476
|
line-height: 1.15;
|
|
@@ -1492,12 +1525,18 @@ td a:hover {
|
|
|
1492
1525
|
text-decoration: underline;
|
|
1493
1526
|
}
|
|
1494
1527
|
|
|
1528
|
+
/* A badge is a plain label, so it shows the default cursor rather than the text one. In a
|
|
1529
|
+
row or card that opens on click it keeps the container's pointer. */
|
|
1495
1530
|
.badge {
|
|
1496
1531
|
display: inline-block;
|
|
1497
1532
|
padding: 2px 8px;
|
|
1498
1533
|
border-radius: 999px;
|
|
1499
1534
|
font-size: 12px;
|
|
1500
1535
|
border: 1px solid var(--line);
|
|
1536
|
+
cursor: default;
|
|
1537
|
+
}
|
|
1538
|
+
.clickable .badge {
|
|
1539
|
+
cursor: inherit;
|
|
1501
1540
|
}
|
|
1502
1541
|
.badge.draft {
|
|
1503
1542
|
background: var(--status-draft-bg);
|
|
@@ -1545,6 +1584,16 @@ td a:hover {
|
|
|
1545
1584
|
color: var(--danger-fg);
|
|
1546
1585
|
border-color: var(--danger-line);
|
|
1547
1586
|
}
|
|
1587
|
+
/* A suppressed address's consent status, kept but not in effect, takes the unsubscribed
|
|
1588
|
+
grey: grey is "not mailed", whatever the label says, so a confirmed-but-suppressed row
|
|
1589
|
+
never reads as one that is mailed. The red flag says why. It overrides the status's own
|
|
1590
|
+
hue (.badge.confirmed and the rest, same specificity) by coming after them, so keep it
|
|
1591
|
+
below them. */
|
|
1592
|
+
.badge.held {
|
|
1593
|
+
background: var(--status-draft-bg);
|
|
1594
|
+
color: var(--status-draft-fg);
|
|
1595
|
+
border-color: var(--status-draft-line);
|
|
1596
|
+
}
|
|
1548
1597
|
/* Suppression is an overlay on the address, not a consent status, so it rides next to
|
|
1549
1598
|
the email rather than in the Status column — sized down to read as secondary. */
|
|
1550
1599
|
.row-flag {
|
|
@@ -1554,6 +1603,128 @@ td a:hover {
|
|
|
1554
1603
|
vertical-align: middle;
|
|
1555
1604
|
}
|
|
1556
1605
|
|
|
1606
|
+
/* The Subscribers summary: the counts, with the Legend toggle at the card's right edge
|
|
1607
|
+
(at the end of the counts' last line when they wrap), and the legend opening below
|
|
1608
|
+
across the card. The legend is in the page's own colors (not an inverted tooltip),
|
|
1609
|
+
each pill drawn as the list draws it beside its definition. */
|
|
1610
|
+
.sub-summary-head {
|
|
1611
|
+
display: flex;
|
|
1612
|
+
align-items: flex-end;
|
|
1613
|
+
justify-content: space-between;
|
|
1614
|
+
gap: 8px 16px;
|
|
1615
|
+
}
|
|
1616
|
+
/* The negative margin lines the first count's text up with the card's edge. */
|
|
1617
|
+
#subCounts {
|
|
1618
|
+
gap: 10px 12px;
|
|
1619
|
+
margin-left: -8px;
|
|
1620
|
+
}
|
|
1621
|
+
/* Each count is a filter (DESIGN §3): it reads as the plain count it was, and shows on
|
|
1622
|
+
hover that it can be chosen. The chosen one takes the Secondary palette of a pressed
|
|
1623
|
+
toggle, since it switches what you're looking at and commits nothing. */
|
|
1624
|
+
.sub-count {
|
|
1625
|
+
border: 0;
|
|
1626
|
+
background: transparent;
|
|
1627
|
+
color: inherit;
|
|
1628
|
+
font-size: inherit;
|
|
1629
|
+
padding: 4px 8px;
|
|
1630
|
+
margin: -4px 0;
|
|
1631
|
+
border-radius: 6px;
|
|
1632
|
+
}
|
|
1633
|
+
.sub-count:hover {
|
|
1634
|
+
background: var(--hover);
|
|
1635
|
+
color: var(--fg);
|
|
1636
|
+
}
|
|
1637
|
+
.sub-count[aria-pressed="true"] {
|
|
1638
|
+
background: var(--accent-soft);
|
|
1639
|
+
color: var(--accent);
|
|
1640
|
+
}
|
|
1641
|
+
/* Forced colors drop the tint, so the chosen count keeps an outline there. */
|
|
1642
|
+
@media (forced-colors: active) {
|
|
1643
|
+
.sub-count[aria-pressed="true"] {
|
|
1644
|
+
outline: 1px solid ButtonText;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
/* The padding and equal negative margin give a 24px target without moving the label. */
|
|
1648
|
+
.sub-legend-toggle {
|
|
1649
|
+
flex: none;
|
|
1650
|
+
display: inline-flex;
|
|
1651
|
+
align-items: center;
|
|
1652
|
+
gap: 8px;
|
|
1653
|
+
padding: 4px 6px;
|
|
1654
|
+
margin: -4px -6px;
|
|
1655
|
+
border: 0;
|
|
1656
|
+
background: transparent;
|
|
1657
|
+
color: var(--muted);
|
|
1658
|
+
font-size: 13px;
|
|
1659
|
+
border-radius: 4px;
|
|
1660
|
+
}
|
|
1661
|
+
.sub-legend-toggle:hover {
|
|
1662
|
+
color: var(--fg);
|
|
1663
|
+
}
|
|
1664
|
+
.sub-legend-toggle::after {
|
|
1665
|
+
content: "";
|
|
1666
|
+
width: 6px;
|
|
1667
|
+
height: 6px;
|
|
1668
|
+
margin-top: -3px;
|
|
1669
|
+
border-right: 2px solid currentColor;
|
|
1670
|
+
border-bottom: 2px solid currentColor;
|
|
1671
|
+
transform: rotate(45deg);
|
|
1672
|
+
}
|
|
1673
|
+
.sub-legend-toggle[aria-expanded="true"]::after {
|
|
1674
|
+
margin-top: 3px;
|
|
1675
|
+
transform: rotate(-135deg);
|
|
1676
|
+
}
|
|
1677
|
+
/* One grid for every group, each list a subgrid, so every definition starts at the same
|
|
1678
|
+
edge. Each group is named by an h2 in the app's label style, and the panel has no title of
|
|
1679
|
+
its own: the Legend toggle already names it. Its headings are a step smaller and more
|
|
1680
|
+
widely tracked than the base label, and set apart by space alone (a group's break, 28px,
|
|
1681
|
+
is three and a half rows'), so the one rule in the panel is the divider under the counts.
|
|
1682
|
+
A paragraph keeps the app's 1rem beneath it. The panel carries the divider across the
|
|
1683
|
+
card; its body is the container that caps the line length, once, for everything in it. */
|
|
1684
|
+
.sub-legend {
|
|
1685
|
+
margin-top: 14px;
|
|
1686
|
+
padding-top: 18px;
|
|
1687
|
+
border-top: 1px solid var(--line);
|
|
1688
|
+
font-size: 13px;
|
|
1689
|
+
line-height: 1.45;
|
|
1690
|
+
}
|
|
1691
|
+
.sub-legend-body {
|
|
1692
|
+
display: grid;
|
|
1693
|
+
grid-template-columns: max-content 1fr;
|
|
1694
|
+
column-gap: 16px;
|
|
1695
|
+
max-width: 88ch;
|
|
1696
|
+
}
|
|
1697
|
+
.sub-legend h2 {
|
|
1698
|
+
grid-column: 1 / -1;
|
|
1699
|
+
margin: 28px 0 10px;
|
|
1700
|
+
font-size: 12px;
|
|
1701
|
+
letter-spacing: 0.08em;
|
|
1702
|
+
}
|
|
1703
|
+
.sub-legend h2:first-child {
|
|
1704
|
+
margin-top: 0;
|
|
1705
|
+
}
|
|
1706
|
+
.sub-legend dl {
|
|
1707
|
+
grid-column: 1 / -1;
|
|
1708
|
+
display: grid;
|
|
1709
|
+
grid-template-columns: subgrid;
|
|
1710
|
+
row-gap: 8px;
|
|
1711
|
+
align-items: baseline;
|
|
1712
|
+
margin: 0;
|
|
1713
|
+
}
|
|
1714
|
+
.sub-legend dd {
|
|
1715
|
+
margin: 0;
|
|
1716
|
+
}
|
|
1717
|
+
/* An act's name is a term, not a heading: lighter than bold, so it doesn't outweigh the
|
|
1718
|
+
pills in the column beside it. */
|
|
1719
|
+
.sub-legend-act {
|
|
1720
|
+
font-weight: 500;
|
|
1721
|
+
}
|
|
1722
|
+
.sub-legend p {
|
|
1723
|
+
grid-column: 1 / -1;
|
|
1724
|
+
margin: 0 0 1rem;
|
|
1725
|
+
color: var(--muted);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1557
1728
|
/* list views (posts / subscribers / sends): search on the left, filters pinned right */
|
|
1558
1729
|
.list-toolbar {
|
|
1559
1730
|
display: flex;
|
|
@@ -1617,8 +1788,17 @@ td a:hover {
|
|
|
1617
1788
|
.list-table .c-status {
|
|
1618
1789
|
width: 128px;
|
|
1619
1790
|
}
|
|
1791
|
+
/* Wide enough for the longest date on one line, a date outside the current year with
|
|
1792
|
+
its year ("Sep 30, 2027, 12:00 AM"), with room for a wider system font. Narrower
|
|
1793
|
+
screens, until the table stacks, keep the room for the text column instead (the Drafts
|
|
1794
|
+
table has two date columns), so an off-year date wraps there. */
|
|
1620
1795
|
.list-table .c-date {
|
|
1621
|
-
width:
|
|
1796
|
+
width: 176px;
|
|
1797
|
+
}
|
|
1798
|
+
@media (max-width: 840px) {
|
|
1799
|
+
.list-table .c-date {
|
|
1800
|
+
width: 148px;
|
|
1801
|
+
}
|
|
1622
1802
|
}
|
|
1623
1803
|
.list-table .c-num {
|
|
1624
1804
|
width: 104px;
|
|
@@ -1633,6 +1813,14 @@ td a:hover {
|
|
|
1633
1813
|
.list-table .c-act {
|
|
1634
1814
|
width: 56px;
|
|
1635
1815
|
}
|
|
1816
|
+
/* Subscribers: how each joined ("Imported · Sendy, single opt-in"), which may wrap to two
|
|
1817
|
+
lines; the table takes a wider floor so the email column keeps room beside it. */
|
|
1818
|
+
.list-table .c-how {
|
|
1819
|
+
width: 152px;
|
|
1820
|
+
}
|
|
1821
|
+
.list-table.subs-table {
|
|
1822
|
+
min-width: 680px;
|
|
1823
|
+
}
|
|
1636
1824
|
|
|
1637
1825
|
.dash-section table {
|
|
1638
1826
|
border-color: var(--line);
|
|
@@ -1788,6 +1976,7 @@ body.web-mode .content {
|
|
|
1788
1976
|
border-radius: 999px;
|
|
1789
1977
|
background: var(--accent-on);
|
|
1790
1978
|
color: var(--accent);
|
|
1979
|
+
cursor: default;
|
|
1791
1980
|
}
|
|
1792
1981
|
.web-bar-text {
|
|
1793
1982
|
overflow-wrap: anywhere;
|
|
@@ -1965,13 +2154,15 @@ body.web-mode .content {
|
|
|
1965
2154
|
/* An outcome cell reuses the tile swatch palette (sw-ok / sw-warn / sw-danger /
|
|
1966
2155
|
sw-sending / sw-neutral) so a row reads the same bucket as its tile above. The
|
|
1967
2156
|
swatch + label hold one line — nowrap so the label never breaks mid-word in the
|
|
1968
|
-
fixed-width column; the consequence
|
|
1969
|
-
|
|
2157
|
+
fixed-width column; the consequence is said in the reconciliation line above, not
|
|
2158
|
+
inline or on hover (DESIGN §11), so the row stays a clean one-liner. A plain label,
|
|
2159
|
+
so the default cursor. */
|
|
1970
2160
|
.rec-out {
|
|
1971
2161
|
display: inline-flex;
|
|
1972
2162
|
align-items: center;
|
|
1973
2163
|
gap: 7px;
|
|
1974
2164
|
white-space: nowrap;
|
|
2165
|
+
cursor: default;
|
|
1975
2166
|
}
|
|
1976
2167
|
.rec-people-empty {
|
|
1977
2168
|
margin: 14px 0 0;
|
|
@@ -1997,7 +2188,7 @@ body.web-mode .content {
|
|
|
1997
2188
|
}
|
|
1998
2189
|
/* The derived-phase pill (progressing / retrying / backing-off / needs-attention /
|
|
1999
2190
|
settling / …). Tones reuse the status + semantic palettes so a phase reads the same
|
|
2000
|
-
hue as its badge elsewhere. */
|
|
2191
|
+
hue as its badge elsewhere. A plain label, so the default cursor (DESIGN §11). */
|
|
2001
2192
|
.phase-pill {
|
|
2002
2193
|
display: inline-block;
|
|
2003
2194
|
padding: 2px 10px;
|
|
@@ -2006,6 +2197,7 @@ body.web-mode .content {
|
|
|
2006
2197
|
font-weight: 600;
|
|
2007
2198
|
border: 1px solid var(--line);
|
|
2008
2199
|
white-space: nowrap;
|
|
2200
|
+
cursor: default;
|
|
2009
2201
|
}
|
|
2010
2202
|
.phase-pill.tone-sending {
|
|
2011
2203
|
background: var(--status-sending-bg);
|
|
@@ -2189,8 +2381,9 @@ body.web-mode .content {
|
|
|
2189
2381
|
font-size: 12.5px;
|
|
2190
2382
|
color: var(--warn-fg);
|
|
2191
2383
|
}
|
|
2192
|
-
/* A scheduled send while no provider is connected (DESIGN §4): the countdown stays,
|
|
2193
|
-
line beneath it in
|
|
2384
|
+
/* A scheduled send's card while no provider is connected (DESIGN §4): the countdown stays,
|
|
2385
|
+
and a line beneath it in amber says the send will be held when it fires. The editor's
|
|
2386
|
+
violet banner says it as a .sched-note instead, since amber text doesn't read on violet. */
|
|
2194
2387
|
.sched-held {
|
|
2195
2388
|
margin-top: 4px;
|
|
2196
2389
|
font-size: 12.5px;
|
|
@@ -2780,6 +2973,11 @@ button.linkbtn:hover {
|
|
|
2780
2973
|
.page-head {
|
|
2781
2974
|
margin-bottom: 16px;
|
|
2782
2975
|
}
|
|
2976
|
+
/* A head with several acts (Subscribers: Import, Export, Add subscriber) puts them under
|
|
2977
|
+
the title when the two don't fit side by side, rather than squeezing either. */
|
|
2978
|
+
.spread.page-head {
|
|
2979
|
+
flex-wrap: wrap;
|
|
2980
|
+
}
|
|
2783
2981
|
.page-head-row {
|
|
2784
2982
|
display: flex;
|
|
2785
2983
|
align-items: center;
|
|
@@ -2998,18 +3196,6 @@ textarea.is-invalid {
|
|
|
2998
3196
|
visibility: visible;
|
|
2999
3197
|
transform: translateY(0);
|
|
3000
3198
|
}
|
|
3001
|
-
/* Drop the tip below the icon instead of above it. The default opens upward,
|
|
3002
|
-
which clips against the top of the viewport when the icon sits at the top of a
|
|
3003
|
-
page (e.g. the Subscribers counts card); this variant opens downward there. */
|
|
3004
|
-
.info.tip-below::after {
|
|
3005
|
-
top: calc(100% + 8px);
|
|
3006
|
-
bottom: auto;
|
|
3007
|
-
transform: translateY(-2px);
|
|
3008
|
-
}
|
|
3009
|
-
.info.tip-below:hover::after,
|
|
3010
|
-
.info.tip-below:focus::after {
|
|
3011
|
-
transform: translateY(0);
|
|
3012
|
-
}
|
|
3013
3199
|
/* The "Auto-generate from subject" toggle, grouped just under the slug field. */
|
|
3014
3200
|
.slug-auto-toggle {
|
|
3015
3201
|
display: inline-flex;
|
|
@@ -3075,10 +3261,12 @@ textarea.is-invalid {
|
|
|
3075
3261
|
margin-top: 6px;
|
|
3076
3262
|
font-size: 13px;
|
|
3077
3263
|
}
|
|
3078
|
-
/* The scheduled banner's
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3264
|
+
/* The scheduled banner's notes under the countdown (DESIGN §4): a send that will be held
|
|
3265
|
+
with no provider connected, and one too large for D1's daily writes on Workers Free's
|
|
3266
|
+
limits. Each keeps the banner's own violet, since amber words on violet fall short of
|
|
3267
|
+
contrast and mix two of the color meanings, and an amber ⚠️ leads it to say it's worth a
|
|
3268
|
+
look. Held is the stronger of the two (the send won't go out), so it keeps the weight. */
|
|
3269
|
+
.sched-note {
|
|
3082
3270
|
display: flex;
|
|
3083
3271
|
gap: 6px;
|
|
3084
3272
|
align-items: baseline;
|
|
@@ -3089,6 +3277,9 @@ textarea.is-invalid {
|
|
|
3089
3277
|
.sched-note-mark {
|
|
3090
3278
|
flex: none;
|
|
3091
3279
|
}
|
|
3280
|
+
.sched-note-held {
|
|
3281
|
+
font-weight: 500;
|
|
3282
|
+
}
|
|
3092
3283
|
.sched-remade-mark {
|
|
3093
3284
|
margin-top: 4px;
|
|
3094
3285
|
font-size: 12.5px;
|
|
@@ -4053,6 +4244,13 @@ button.api-filter-clear svg {
|
|
|
4053
4244
|
font-size: 13px;
|
|
4054
4245
|
font-variant-numeric: tabular-nums;
|
|
4055
4246
|
}
|
|
4247
|
+
/* A step already done (the list has subscribers): a check in place of its number. */
|
|
4248
|
+
.setup-steps li.setup-done::before {
|
|
4249
|
+
content: "✓";
|
|
4250
|
+
color: var(--ok-fg);
|
|
4251
|
+
border-color: var(--ok-line);
|
|
4252
|
+
background: var(--ok-bg);
|
|
4253
|
+
}
|
|
4056
4254
|
.setup-step-main {
|
|
4057
4255
|
flex: 1 1 auto;
|
|
4058
4256
|
min-width: 0;
|
|
@@ -4208,7 +4406,8 @@ button.copied svg {
|
|
|
4208
4406
|
}
|
|
4209
4407
|
|
|
4210
4408
|
/* Intent chips — the grouping made visible. Editable wears the Kestrel accent;
|
|
4211
|
-
read-only wears a warn tone; copy-out stays muted.
|
|
4409
|
+
read-only wears a warn tone; copy-out stays muted. Plain labels, so the default
|
|
4410
|
+
cursor (DESIGN §11). */
|
|
4212
4411
|
.set-chip {
|
|
4213
4412
|
display: inline-flex;
|
|
4214
4413
|
align-items: center;
|
|
@@ -4221,6 +4420,7 @@ button.copied svg {
|
|
|
4221
4420
|
color: var(--muted);
|
|
4222
4421
|
background: var(--card);
|
|
4223
4422
|
white-space: nowrap;
|
|
4423
|
+
cursor: default;
|
|
4224
4424
|
}
|
|
4225
4425
|
.set-chip svg {
|
|
4226
4426
|
width: 12px;
|
|
@@ -4474,6 +4674,12 @@ button.set-field-remove {
|
|
|
4474
4674
|
font-size: 13.5px;
|
|
4475
4675
|
overflow-x: auto;
|
|
4476
4676
|
}
|
|
4677
|
+
/* A value with one quiet line of explanation under it (the From address and where replies go). */
|
|
4678
|
+
.set-kv > .set-kv-stack {
|
|
4679
|
+
flex-direction: column;
|
|
4680
|
+
align-items: flex-start;
|
|
4681
|
+
gap: 3px;
|
|
4682
|
+
}
|
|
4477
4683
|
.set-kv > div:nth-last-child(1),
|
|
4478
4684
|
.set-kv > div:nth-last-child(2) {
|
|
4479
4685
|
border-bottom: 0;
|
|
@@ -4482,6 +4688,7 @@ button.set-field-remove {
|
|
|
4482
4688
|
font-family: var(--mono);
|
|
4483
4689
|
font-size: 13px;
|
|
4484
4690
|
}
|
|
4691
|
+
/* A status pill in the settings reflection: a plain label, so the default cursor (DESIGN §11). */
|
|
4485
4692
|
.set-pill {
|
|
4486
4693
|
display: inline-flex;
|
|
4487
4694
|
align-items: center;
|
|
@@ -4494,6 +4701,7 @@ button.set-field-remove {
|
|
|
4494
4701
|
color: var(--muted);
|
|
4495
4702
|
border: 1px solid var(--line-soft);
|
|
4496
4703
|
white-space: nowrap;
|
|
4704
|
+
cursor: default;
|
|
4497
4705
|
}
|
|
4498
4706
|
.set-pill.ok {
|
|
4499
4707
|
background: var(--ok-bg);
|
|
@@ -5366,6 +5574,7 @@ button.set-embed-copy svg {
|
|
|
5366
5574
|
border-radius: 999px;
|
|
5367
5575
|
border: 1px solid var(--line-soft);
|
|
5368
5576
|
color: var(--muted);
|
|
5577
|
+
cursor: default;
|
|
5369
5578
|
}
|
|
5370
5579
|
.set-req-pill .dot {
|
|
5371
5580
|
width: 6px;
|
|
@@ -5916,8 +6125,268 @@ body.has-savebar .content {
|
|
|
5916
6125
|
.posts-table td.updated::before {
|
|
5917
6126
|
content: " · edited ";
|
|
5918
6127
|
}
|
|
5919
|
-
|
|
6128
|
+
}
|
|
6129
|
+
|
|
6130
|
+
/* The Subscribers table has four fixed columns beside its email, so it runs out of room
|
|
6131
|
+
well above phone width, and where depends on the sidebar (a rail up to 1024px, then
|
|
6132
|
+
full), not the viewport. So it stacks by its own width, measured on its wrapper,
|
|
6133
|
+
whenever the email would get less than about 250px: the same stacked row as the
|
|
6134
|
+
phone layout above, repeated here because a container query can't share its rules. */
|
|
6135
|
+
.subs-wrap {
|
|
6136
|
+
container-type: inline-size;
|
|
6137
|
+
}
|
|
6138
|
+
@container (max-width: 759px) {
|
|
6139
|
+
.list-table.subs-table.stacks,
|
|
6140
|
+
.list-table.subs-table.stacks tbody,
|
|
6141
|
+
.list-table.subs-table.stacks tr {
|
|
6142
|
+
display: block;
|
|
6143
|
+
min-width: 0;
|
|
6144
|
+
}
|
|
6145
|
+
.list-table.subs-table.stacks colgroup,
|
|
6146
|
+
.list-table.subs-table.stacks thead {
|
|
6147
|
+
display: none;
|
|
6148
|
+
}
|
|
6149
|
+
.list-table.subs-table.stacks tr {
|
|
6150
|
+
position: relative;
|
|
6151
|
+
padding: 11px 14px;
|
|
6152
|
+
border-bottom: 1px solid var(--line);
|
|
6153
|
+
}
|
|
6154
|
+
.list-table.subs-table.stacks tr:last-child {
|
|
6155
|
+
border-bottom: 0;
|
|
6156
|
+
}
|
|
6157
|
+
.list-table.subs-table.stacks td {
|
|
6158
|
+
display: inline;
|
|
6159
|
+
padding: 0;
|
|
6160
|
+
border: 0;
|
|
6161
|
+
font-size: 13px;
|
|
6162
|
+
text-align: left;
|
|
6163
|
+
}
|
|
6164
|
+
.list-table.subs-table.stacks td:first-child {
|
|
6165
|
+
display: block;
|
|
6166
|
+
margin-bottom: 4px;
|
|
6167
|
+
font-size: 14px;
|
|
6168
|
+
padding-right: 36px;
|
|
6169
|
+
}
|
|
6170
|
+
.list-table.subs-table.stacks td.act {
|
|
6171
|
+
position: absolute;
|
|
6172
|
+
top: 6px;
|
|
6173
|
+
right: 6px;
|
|
6174
|
+
width: auto;
|
|
6175
|
+
}
|
|
6176
|
+
.list-table.subs-table.stacks td.empty {
|
|
6177
|
+
display: none;
|
|
6178
|
+
}
|
|
6179
|
+
/* Subscribers: status pill · how they joined · joined <date>. */
|
|
6180
|
+
.subs-table td.how::before {
|
|
6181
|
+
content: " · ";
|
|
6182
|
+
}
|
|
5920
6183
|
.subs-table td.joined::before {
|
|
5921
6184
|
content: " · joined ";
|
|
5922
6185
|
}
|
|
5923
6186
|
}
|
|
6187
|
+
|
|
6188
|
+
/* --- Import (DESIGN §3, Import subscribers): the format before the file, the plan of the
|
|
6189
|
+
file chosen with what the commit needs, then the result. Cards on the page ground, the
|
|
6190
|
+
drop zone dashed so it reads as a place to put something. --- */
|
|
6191
|
+
.imp {
|
|
6192
|
+
max-width: 820px;
|
|
6193
|
+
}
|
|
6194
|
+
.imp-card h2 {
|
|
6195
|
+
margin: 0 0 10px;
|
|
6196
|
+
}
|
|
6197
|
+
/* Where the page moves focus after a plan, a re-plan, or the result: a place to read from,
|
|
6198
|
+
not a control, so it takes no ring. */
|
|
6199
|
+
.imp-file:focus,
|
|
6200
|
+
.imp-act-error:focus,
|
|
6201
|
+
#impDoneHead:focus {
|
|
6202
|
+
outline: none;
|
|
6203
|
+
}
|
|
6204
|
+
.imp-card-head {
|
|
6205
|
+
display: flex;
|
|
6206
|
+
align-items: center;
|
|
6207
|
+
justify-content: space-between;
|
|
6208
|
+
gap: 12px;
|
|
6209
|
+
flex-wrap: wrap;
|
|
6210
|
+
margin-bottom: 10px;
|
|
6211
|
+
}
|
|
6212
|
+
.imp-card-head h2 {
|
|
6213
|
+
margin: 0;
|
|
6214
|
+
}
|
|
6215
|
+
.imp-text {
|
|
6216
|
+
font-size: 13.5px;
|
|
6217
|
+
line-height: 1.55;
|
|
6218
|
+
margin: 0 0 10px;
|
|
6219
|
+
}
|
|
6220
|
+
.imp-sample,
|
|
6221
|
+
.imp-ref {
|
|
6222
|
+
font-size: 12.5px;
|
|
6223
|
+
margin: 4px 0 8px;
|
|
6224
|
+
}
|
|
6225
|
+
.imp-sample td,
|
|
6226
|
+
.imp-ref td {
|
|
6227
|
+
vertical-align: top;
|
|
6228
|
+
}
|
|
6229
|
+
.imp-sample td {
|
|
6230
|
+
font-family: var(--mono);
|
|
6231
|
+
white-space: nowrap;
|
|
6232
|
+
}
|
|
6233
|
+
.imp-links {
|
|
6234
|
+
margin-top: 10px;
|
|
6235
|
+
}
|
|
6236
|
+
.imp-drop {
|
|
6237
|
+
display: flex;
|
|
6238
|
+
flex-direction: column;
|
|
6239
|
+
align-items: center;
|
|
6240
|
+
gap: 8px;
|
|
6241
|
+
padding: 26px 16px;
|
|
6242
|
+
border: 2px dashed var(--line-soft);
|
|
6243
|
+
border-radius: 10px;
|
|
6244
|
+
background: var(--card);
|
|
6245
|
+
text-align: center;
|
|
6246
|
+
}
|
|
6247
|
+
.imp-drop.over {
|
|
6248
|
+
border-color: var(--accent);
|
|
6249
|
+
background: var(--accent-soft);
|
|
6250
|
+
}
|
|
6251
|
+
.imp-drop > svg {
|
|
6252
|
+
width: 22px;
|
|
6253
|
+
height: 22px;
|
|
6254
|
+
color: var(--muted);
|
|
6255
|
+
}
|
|
6256
|
+
.imp-drop-line {
|
|
6257
|
+
margin: 0;
|
|
6258
|
+
font-size: 13.5px;
|
|
6259
|
+
}
|
|
6260
|
+
.imp-drop .field-hint {
|
|
6261
|
+
margin: 0;
|
|
6262
|
+
}
|
|
6263
|
+
.imp-file-error {
|
|
6264
|
+
margin-top: 10px;
|
|
6265
|
+
}
|
|
6266
|
+
.imp-file-error .field-error {
|
|
6267
|
+
margin: 0 0 10px;
|
|
6268
|
+
font-size: 13.5px;
|
|
6269
|
+
}
|
|
6270
|
+
.imp-file-row {
|
|
6271
|
+
display: flex;
|
|
6272
|
+
align-items: center;
|
|
6273
|
+
justify-content: space-between;
|
|
6274
|
+
gap: 12px;
|
|
6275
|
+
flex-wrap: wrap;
|
|
6276
|
+
}
|
|
6277
|
+
.imp-file-name {
|
|
6278
|
+
overflow-wrap: anywhere;
|
|
6279
|
+
}
|
|
6280
|
+
.imp-plain {
|
|
6281
|
+
margin-top: 14px;
|
|
6282
|
+
padding-top: 12px;
|
|
6283
|
+
border-top: 1px solid var(--line);
|
|
6284
|
+
display: flex;
|
|
6285
|
+
align-items: center;
|
|
6286
|
+
gap: 10px;
|
|
6287
|
+
flex-wrap: wrap;
|
|
6288
|
+
}
|
|
6289
|
+
.imp-plain .field-hint {
|
|
6290
|
+
flex-basis: 100%;
|
|
6291
|
+
margin: 0;
|
|
6292
|
+
}
|
|
6293
|
+
.imp-plain-label {
|
|
6294
|
+
font-size: 13.5px;
|
|
6295
|
+
}
|
|
6296
|
+
.imp-outcomes {
|
|
6297
|
+
margin: 0 0 10px;
|
|
6298
|
+
padding-left: 18px;
|
|
6299
|
+
font-size: 13.5px;
|
|
6300
|
+
line-height: 1.55;
|
|
6301
|
+
}
|
|
6302
|
+
.imp-outcomes li + li {
|
|
6303
|
+
margin-top: 4px;
|
|
6304
|
+
}
|
|
6305
|
+
.imp-kept-head {
|
|
6306
|
+
font-size: 13px;
|
|
6307
|
+
font-weight: 600;
|
|
6308
|
+
margin: 14px 0 4px;
|
|
6309
|
+
}
|
|
6310
|
+
/* The same amber box the Schedule dialog gives D1's daily-writes warning (DESIGN §4). */
|
|
6311
|
+
.hint.daily-writes.imp-warn {
|
|
6312
|
+
color: var(--warn-fg);
|
|
6313
|
+
background: var(--warn-bg);
|
|
6314
|
+
border: 1px solid var(--warn-line);
|
|
6315
|
+
border-radius: 8px;
|
|
6316
|
+
padding: 10px 12px;
|
|
6317
|
+
font-size: 13px;
|
|
6318
|
+
line-height: 1.5;
|
|
6319
|
+
margin: 12px 0 0;
|
|
6320
|
+
}
|
|
6321
|
+
.imp-check {
|
|
6322
|
+
display: flex;
|
|
6323
|
+
align-items: flex-start;
|
|
6324
|
+
gap: 10px;
|
|
6325
|
+
font-size: 13.5px;
|
|
6326
|
+
line-height: 1.5;
|
|
6327
|
+
margin: 10px 0 0;
|
|
6328
|
+
cursor: pointer;
|
|
6329
|
+
}
|
|
6330
|
+
.imp-check input {
|
|
6331
|
+
width: auto;
|
|
6332
|
+
margin: 3px 0 0;
|
|
6333
|
+
flex: 0 0 auto;
|
|
6334
|
+
cursor: pointer;
|
|
6335
|
+
}
|
|
6336
|
+
.imp-check + .field-error {
|
|
6337
|
+
margin-left: 24px;
|
|
6338
|
+
}
|
|
6339
|
+
.imp-attest-hint {
|
|
6340
|
+
margin: 6px 0 0 24px;
|
|
6341
|
+
}
|
|
6342
|
+
.imp-act-error {
|
|
6343
|
+
margin-top: 14px;
|
|
6344
|
+
}
|
|
6345
|
+
.imp-actions {
|
|
6346
|
+
display: flex;
|
|
6347
|
+
justify-content: flex-end;
|
|
6348
|
+
align-items: center;
|
|
6349
|
+
gap: 14px;
|
|
6350
|
+
margin-top: 18px;
|
|
6351
|
+
}
|
|
6352
|
+
/* View subscriber's title is the address, so a long one breaks rather than widens the dialog. */
|
|
6353
|
+
.sub-title {
|
|
6354
|
+
overflow-wrap: anywhere;
|
|
6355
|
+
}
|
|
6356
|
+
.imp-dl,
|
|
6357
|
+
.sub-dl {
|
|
6358
|
+
display: grid;
|
|
6359
|
+
grid-template-columns: max-content 1fr;
|
|
6360
|
+
gap: 6px 16px;
|
|
6361
|
+
margin: 0;
|
|
6362
|
+
font-size: 13.5px;
|
|
6363
|
+
}
|
|
6364
|
+
.imp-dl dt,
|
|
6365
|
+
.sub-dl dt {
|
|
6366
|
+
color: var(--muted);
|
|
6367
|
+
}
|
|
6368
|
+
.imp-dl dd,
|
|
6369
|
+
.sub-dl dd {
|
|
6370
|
+
margin: 0;
|
|
6371
|
+
overflow-wrap: anywhere;
|
|
6372
|
+
}
|
|
6373
|
+
.sub-evidence {
|
|
6374
|
+
margin: 0;
|
|
6375
|
+
white-space: pre-wrap;
|
|
6376
|
+
background: var(--bg);
|
|
6377
|
+
border: 1px solid var(--line);
|
|
6378
|
+
border-radius: 6px;
|
|
6379
|
+
padding: 8px 10px;
|
|
6380
|
+
font: 12px / 1.5 var(--mono);
|
|
6381
|
+
}
|
|
6382
|
+
@media (max-width: 540px) {
|
|
6383
|
+
.imp-dl,
|
|
6384
|
+
.sub-dl {
|
|
6385
|
+
grid-template-columns: 1fr;
|
|
6386
|
+
gap: 2px;
|
|
6387
|
+
}
|
|
6388
|
+
.imp-dl dd,
|
|
6389
|
+
.sub-dl dd {
|
|
6390
|
+
margin-bottom: 8px;
|
|
6391
|
+
}
|
|
6392
|
+
}
|