create-shibumi 0.2.5 → 0.2.8
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/package.json +3 -2
- package/scripts/shibumi.lock.json +2 -2
- package/scripts/ship.lock.json +2 -2
- package/src/templates/full-stack/agents.md +1 -1
- package/src/templates/full-stack/public/app.js +7 -2
- package/src/templates/full-stack/public/style.css +128 -38
- package/src/templates/full-stack/public/vendor/shibumi.css +422 -0
- package/src/templates/full-stack/src/app.ts +54 -9
- package/src/templates/full-stack/src/db/migrations/0002_counter.sql +6 -0
- package/src/templates/full-stack/src/db/schema.ts +8 -0
- package/src/templates/full-stack/test/app.test.ts +25 -2
- package/src/templates/shibumi.ts +2 -2
- package/src/templates/ship.ts +18 -15
- package/src/templates/web/public/style.css +128 -38
- package/src/templates/web/public/vendor/shibumi.css +422 -0
- package/src/templates/web/src/app.ts +17 -7
- package/src/templates/web/test/app.test.ts +1 -1
package/src/templates/ship.ts
CHANGED
|
@@ -25,7 +25,7 @@ const SERVER_HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/;
|
|
|
25
25
|
const COMMIT = /^[a-f0-9]{40}$/;
|
|
26
26
|
const SERVER_CLI = "~/.local/bin/shibumi-server";
|
|
27
27
|
const LATEST_SOURCE = "https://shibumistack.dev/ship/latest.ts";
|
|
28
|
-
const CURRENT_SOURCE = "https://shibumistack.dev/ship/
|
|
28
|
+
const CURRENT_SOURCE = "https://shibumistack.dev/ship/v46.ts";
|
|
29
29
|
let sshControlDirectory: string | undefined;
|
|
30
30
|
let sshControlTarget: string | undefined;
|
|
31
31
|
|
|
@@ -1703,13 +1703,13 @@ function portIsBusy(port: number): Promise<boolean> {
|
|
|
1703
1703
|
});
|
|
1704
1704
|
}
|
|
1705
1705
|
|
|
1706
|
-
export function formatDevStartup(port: number, domain: string, time: string, color = false): string {
|
|
1706
|
+
export function formatDevStartup(port: number, domain: string | undefined, time: string, color = false): string {
|
|
1707
1707
|
const paint = (code: string, value: string) => color ? `\x1b[${code}m${value}\x1b[0m` : value;
|
|
1708
1708
|
const row = (label: string, url: string) => `${paint("2", "┃")} ${label.padEnd(8)} ${paint("34", url)}`;
|
|
1709
1709
|
return [
|
|
1710
1710
|
`${paint("38;5;208", "渋み")} ship dev`,
|
|
1711
1711
|
row("Local", `http://localhost:${port}/`),
|
|
1712
|
-
row("Remote", `https://${domain}`),
|
|
1712
|
+
...(domain ? [row("Remote", `https://${domain}`)] : []),
|
|
1713
1713
|
`${paint("2", time)} starting app dev server...`,
|
|
1714
1714
|
].join("\n");
|
|
1715
1715
|
}
|
|
@@ -1719,30 +1719,33 @@ function localTime(date = new Date()): string {
|
|
|
1719
1719
|
}
|
|
1720
1720
|
|
|
1721
1721
|
async function runDev(): Promise<void> {
|
|
1722
|
+
// Dev must work on a fresh scaffold, before any server exists: without
|
|
1723
|
+
// setup, fall back to the Shibumi port convention (registered apps get the
|
|
1724
|
+
// first free port above 9000) and skip the Remote row.
|
|
1722
1725
|
const config = await readConfig();
|
|
1723
|
-
|
|
1724
|
-
if (await portIsBusy(
|
|
1726
|
+
const port = config?.port ?? 9000;
|
|
1727
|
+
if (await portIsBusy(port)) {
|
|
1725
1728
|
const lsof = Bun.which("lsof");
|
|
1726
1729
|
const fuser = Bun.which("fuser");
|
|
1727
|
-
if (!lsof && !fuser) throw new Error(`Port ${
|
|
1730
|
+
if (!lsof && !fuser) throw new Error(`Port ${port} is already in use.\n\nNext: stop that process, then run bun dev again.`);
|
|
1728
1731
|
const found = lsof
|
|
1729
|
-
? await run([lsof, "-nP", `-iTCP:${
|
|
1730
|
-
: await run([fuser!, "-n", "tcp", String(
|
|
1732
|
+
? await run([lsof, "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { allowFailure: true })
|
|
1733
|
+
: await run([fuser!, "-n", "tcp", String(port)], { allowFailure: true });
|
|
1731
1734
|
const pids = [...new Set(`${found.stdout} ${found.stderr}`.split(/\s+/).filter((value) => /^\d+$/.test(value)).map(Number))];
|
|
1732
|
-
if (pids.length === 0) throw new Error(`Port ${
|
|
1735
|
+
if (pids.length === 0) throw new Error(`Port ${port} is already in use.\n\nNext: stop that process, then run bun dev again.`);
|
|
1733
1736
|
const details = await run(["ps", "-o", "pid=,comm=", "-p", pids.join(",")], { allowFailure: true });
|
|
1734
|
-
log.warn(`Port ${
|
|
1737
|
+
log.warn(`Port ${port} is in use${details.stdout.trim() ? `:\n${details.stdout.trim()}` : ""}`);
|
|
1735
1738
|
const accepted = await confirm({ message: "Stop it and start this project?", initialValue: false });
|
|
1736
1739
|
if (isCancel(accepted) || !accepted) return;
|
|
1737
1740
|
for (const pid of pids) process.kill(pid, "SIGTERM");
|
|
1738
1741
|
const deadline = Date.now() + 5_000;
|
|
1739
|
-
while (await portIsBusy(
|
|
1740
|
-
if (await portIsBusy(
|
|
1742
|
+
while (await portIsBusy(port) && Date.now() < deadline) await Bun.sleep(100);
|
|
1743
|
+
if (await portIsBusy(port)) throw new Error(`Port ${port} did not stop.\n\nNext: stop PID ${pids.join(", ")} manually, then run bun dev again.`);
|
|
1741
1744
|
}
|
|
1742
|
-
process.stdout.write(`${formatDevStartup(
|
|
1745
|
+
process.stdout.write(`${formatDevStartup(port, config?.domain, localTime(), supportsTerminalColor())}\n`);
|
|
1743
1746
|
const child = Bun.spawn([process.execPath, "run", "dev:app"], {
|
|
1744
1747
|
cwd: root,
|
|
1745
|
-
env: { ...process.env, PORT: String(
|
|
1748
|
+
env: { ...process.env, PORT: String(port), SHIBUMI_PORT: String(port) },
|
|
1746
1749
|
stdin: "inherit",
|
|
1747
1750
|
stdout: "inherit",
|
|
1748
1751
|
stderr: "inherit",
|
|
@@ -1808,7 +1811,7 @@ export async function runShip(): Promise<void> {
|
|
|
1808
1811
|
? await confirm({ message: "Ship now?", initialValue: true })
|
|
1809
1812
|
: false;
|
|
1810
1813
|
if (shipNow !== true || isCancel(shipNow)) {
|
|
1811
|
-
outro(`${accent("Next:")} ${result.config.trigger === "github-push" ?
|
|
1814
|
+
outro(`${accent("Next:")} ${result.config.trigger === "github-push" ? `git push origin ${result.config.branch} to deploy` : "bun ship"}`);
|
|
1812
1815
|
return;
|
|
1813
1816
|
}
|
|
1814
1817
|
}
|
|
@@ -1,60 +1,150 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/* App styles on top of the vendored shibumi.css tokens (public/vendor/).
|
|
2
|
+
Edit freely; this file is yours. Tokens: --paper --ink --muted --line
|
|
3
|
+
--accent, type scale --text-xs..--text-2xl, --font-sans/serif/mono. */
|
|
4
|
+
|
|
5
|
+
/* The vendored shibumi.css sets a kozo paper background for the sites; apps
|
|
6
|
+
stay flat, and this override also stops the browser fetching the image. */
|
|
7
|
+
body {
|
|
8
|
+
background-image: none;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
--accent: #ff8648;
|
|
13
|
-
}
|
|
11
|
+
.scaffold {
|
|
12
|
+
max-width: 40.625rem;
|
|
13
|
+
margin: 0 auto;
|
|
14
|
+
padding: clamp(3rem, 9vh, 6.5rem) 1.5rem 4rem;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
.masthead {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: baseline;
|
|
20
|
+
gap: 0.6rem;
|
|
21
|
+
margin: 0 0 3.5rem;
|
|
22
|
+
padding-bottom: 1rem;
|
|
23
|
+
border-bottom: 1px solid var(--line);
|
|
24
|
+
color: var(--muted);
|
|
25
|
+
font-size: var(--text-sm);
|
|
26
|
+
letter-spacing: 0.04em;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
padding: 4rem 1.5rem;
|
|
29
|
+
.masthead-mark {
|
|
30
|
+
color: var(--accent);
|
|
31
|
+
font-size: var(--text-md);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
h1 {
|
|
32
|
-
|
|
33
|
-
font-
|
|
34
|
+
.scaffold h1 {
|
|
35
|
+
margin: 0 0 0.75rem;
|
|
36
|
+
font-size: clamp(2.6rem, 7vw, 3.8rem);
|
|
37
|
+
font-weight: 450;
|
|
38
|
+
letter-spacing: -0.03em;
|
|
39
|
+
line-height: 1.05;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
.lede {
|
|
43
|
+
margin: 0 0 3rem;
|
|
44
|
+
max-width: 34rem;
|
|
45
|
+
color: var(--muted);
|
|
46
|
+
font-size: var(--text-lg);
|
|
47
|
+
line-height: 1.55;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
.demo {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 1rem;
|
|
54
|
+
margin-bottom: 3.25rem;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
button {
|
|
48
|
-
font:
|
|
49
|
-
padding: 0.
|
|
57
|
+
.demo button {
|
|
58
|
+
font: 500 var(--text-md) var(--font-sans);
|
|
59
|
+
padding: 0.5rem 1.3rem;
|
|
50
60
|
border: 1px solid var(--ink);
|
|
61
|
+
border-radius: 0.35rem;
|
|
51
62
|
background: transparent;
|
|
52
63
|
color: inherit;
|
|
53
|
-
border-radius: 4px;
|
|
54
64
|
cursor: pointer;
|
|
65
|
+
transition: border-color 160ms ease, color 160ms ease;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.demo button:hover,
|
|
69
|
+
.demo button:focus-visible {
|
|
70
|
+
border-color: var(--accent);
|
|
71
|
+
color: var(--accent);
|
|
55
72
|
}
|
|
56
73
|
|
|
57
|
-
output {
|
|
58
|
-
|
|
74
|
+
.demo output {
|
|
75
|
+
min-width: 2ch;
|
|
76
|
+
font-size: var(--text-lg);
|
|
59
77
|
font-variant-numeric: tabular-nums;
|
|
60
78
|
}
|
|
79
|
+
|
|
80
|
+
.demo-hint {
|
|
81
|
+
color: var(--muted);
|
|
82
|
+
font-size: var(--text-sm);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.endpoints {
|
|
86
|
+
margin: 0;
|
|
87
|
+
padding: 0;
|
|
88
|
+
list-style: none;
|
|
89
|
+
border-top: 1px solid var(--line);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main .endpoints a {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: baseline;
|
|
95
|
+
justify-content: space-between;
|
|
96
|
+
gap: 1.5rem;
|
|
97
|
+
padding: 0.9rem 0.25rem;
|
|
98
|
+
border-bottom: 1px solid var(--line);
|
|
99
|
+
text-decoration: none;
|
|
100
|
+
text-decoration-color: transparent;
|
|
101
|
+
transition: background 160ms ease;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
main .endpoints a:hover,
|
|
105
|
+
main .endpoints a:focus-visible {
|
|
106
|
+
background: var(--faint);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.endpoint-path {
|
|
110
|
+
font-family: var(--font-mono);
|
|
111
|
+
font-size: var(--text-sm);
|
|
112
|
+
}
|
|
113
|
+
/* Inline code as rounded chips on the faint layer. */
|
|
114
|
+
.scaffold code {
|
|
115
|
+
padding: 0.12em 0.42em;
|
|
116
|
+
border-radius: 0.35rem;
|
|
117
|
+
background: var(--faint);
|
|
118
|
+
font-family: var(--font-mono);
|
|
119
|
+
font-size: 0.85em;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
main .endpoints a:hover .endpoint-path {
|
|
124
|
+
color: var(--accent);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.endpoints span {
|
|
128
|
+
color: var(--muted);
|
|
129
|
+
font-size: var(--text-sm);
|
|
130
|
+
text-align: right;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.colophon {
|
|
134
|
+
margin-top: 3.5rem;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.colophon p {
|
|
138
|
+
margin: 0 0 0.5rem;
|
|
139
|
+
color: var(--muted);
|
|
140
|
+
font-size: var(--text-sm);
|
|
141
|
+
line-height: 1.7;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.colophon code {
|
|
145
|
+
color: var(--ink);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.colophon-links {
|
|
149
|
+
font-size: var(--text-sm);
|
|
150
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/* shibumi.css — canonical shared layer for shibumistack.dev, shibumi-server, shibumi-forms.
|
|
2
|
+
Source of truth: shibumistack.dev repo public/shibumi.css. Do not edit vendored copies. */
|
|
3
|
+
|
|
4
|
+
/* shared.css: common styles for all pages */
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
color-scheme: light dark;
|
|
8
|
+
/* palette copied from shibumi-server site/styles.css */
|
|
9
|
+
--paper: light-dark(#f5f0e4, #1b130f);
|
|
10
|
+
--ink: light-dark(#272016, #eee5d7);
|
|
11
|
+
--muted: light-dark(#746b5d, #aaa092);
|
|
12
|
+
--faint: light-dark(rgba(39, 32, 22, 0.055), rgba(238, 229, 215, 0.07));
|
|
13
|
+
--line: light-dark(rgba(39, 32, 22, 0.14), rgba(238, 229, 215, 0.16));
|
|
14
|
+
--orange: light-dark(#e95f19, #ff8648);
|
|
15
|
+
--orange-soft: light-dark(#f8dfcd, #3c2117);
|
|
16
|
+
--green: light-dark(#26723e, #75d58a);
|
|
17
|
+
--danger: light-dark(#a13b2c, #e36c59);
|
|
18
|
+
--code: light-dark(#201c16, #120e0b);
|
|
19
|
+
--code-ink: #e9e2d5;
|
|
20
|
+
/* aliases so existing components pick up the server palette */
|
|
21
|
+
--bg: var(--paper);
|
|
22
|
+
--text: var(--ink);
|
|
23
|
+
--quiet: var(--faint);
|
|
24
|
+
--accent: var(--orange);
|
|
25
|
+
--accent-soft: var(--orange-soft);
|
|
26
|
+
--accent-hover: light-dark(#d15515, #ff9a66);
|
|
27
|
+
--success: var(--green);
|
|
28
|
+
--success-text: var(--green);
|
|
29
|
+
--success-ink: #17331f;
|
|
30
|
+
--terminal-success: light-dark(#3d8f50, #78e88f);
|
|
31
|
+
--terminal-active: light-dark(#16839a, #62dff0);
|
|
32
|
+
--terminal-info: light-dark(#397fc0, #74b6ff);
|
|
33
|
+
--max: 72.5rem;
|
|
34
|
+
--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
35
|
+
--font-serif: ui-serif, Georgia, "Iowan Old Style", "Apple Garamond", Cambria, serif;
|
|
36
|
+
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
37
|
+
--text-xs: 0.75rem; /* 12px tiny */
|
|
38
|
+
--text-sm: 0.875rem; /* 14px small */
|
|
39
|
+
--text-md: 1rem; /* 16px normal / UI */
|
|
40
|
+
--text-base: 1.125rem; /* 18px body */
|
|
41
|
+
--text-lg: 1.3125rem; /* 21px */
|
|
42
|
+
--text-xl: 1.563rem; /* 25px */
|
|
43
|
+
--text-2xl: 1.953rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[data-theme="dark"] { color-scheme: dark; }
|
|
47
|
+
[data-theme="light"] { color-scheme: light; }
|
|
48
|
+
|
|
49
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
50
|
+
html { min-height: 100%; }
|
|
51
|
+
|
|
52
|
+
body {
|
|
53
|
+
margin: 0;
|
|
54
|
+
min-height: 100svh;
|
|
55
|
+
font-family: var(--font-sans);
|
|
56
|
+
font-size: var(--text-base);
|
|
57
|
+
font-weight: 400;
|
|
58
|
+
line-height: 1.65;
|
|
59
|
+
color: var(--text);
|
|
60
|
+
background: var(--bg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Kozo paper texture, light theme only; dark stays flat ink. The image lives
|
|
64
|
+
next to this stylesheet in every consumer, hence the relative URL. */
|
|
65
|
+
body {
|
|
66
|
+
background-image: url("kozo.webp");
|
|
67
|
+
background-size: cover;
|
|
68
|
+
background-position: center;
|
|
69
|
+
background-attachment: fixed;
|
|
70
|
+
background-repeat: no-repeat;
|
|
71
|
+
}
|
|
72
|
+
@media (prefers-color-scheme: dark) {
|
|
73
|
+
body { background-image: none; }
|
|
74
|
+
}
|
|
75
|
+
[data-theme="light"] body { background-image: url("kozo.webp"); }
|
|
76
|
+
[data-theme="dark"] body { background-image: none; }
|
|
77
|
+
|
|
78
|
+
h1, h2, h3 {
|
|
79
|
+
font-family: var(--font-serif);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
a { color: inherit; }
|
|
83
|
+
|
|
84
|
+
main a:not(.btn):not(.button) {
|
|
85
|
+
text-decoration: underline;
|
|
86
|
+
text-decoration-color: color-mix(in srgb, var(--accent) 30%, transparent);
|
|
87
|
+
text-underline-offset: 0.18em;
|
|
88
|
+
transition: text-decoration-color 160ms ease;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main a:not(.btn):not(.button):hover,
|
|
92
|
+
main a:not(.btn):not(.button):focus-visible {
|
|
93
|
+
text-decoration-color: var(--accent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.shell {
|
|
97
|
+
width: min(var(--max), calc(100% - 2rem));
|
|
98
|
+
margin: 0 auto;
|
|
99
|
+
padding: 5.5rem 0 0;
|
|
100
|
+
position: relative;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ---- header / nav ---- */
|
|
104
|
+
|
|
105
|
+
.shell > header,
|
|
106
|
+
.site-header {
|
|
107
|
+
position: fixed;
|
|
108
|
+
top: 0;
|
|
109
|
+
left: 50%;
|
|
110
|
+
z-index: 20;
|
|
111
|
+
width: min(var(--max), calc(100% - 2rem));
|
|
112
|
+
display: flex;
|
|
113
|
+
justify-content: space-between;
|
|
114
|
+
align-items: center;
|
|
115
|
+
gap: 1rem;
|
|
116
|
+
padding: 1rem 0;
|
|
117
|
+
transform: translateX(-50%);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.shell > header::before,
|
|
121
|
+
.site-header::before {
|
|
122
|
+
content: "";
|
|
123
|
+
position: fixed;
|
|
124
|
+
top: 0;
|
|
125
|
+
left: 50%;
|
|
126
|
+
z-index: -1;
|
|
127
|
+
width: 100vw;
|
|
128
|
+
height: 100%;
|
|
129
|
+
transform: translateX(-50%);
|
|
130
|
+
background: light-dark(rgb(245 240 228 / 0%), rgb(27 19 15 / 72%));
|
|
131
|
+
backdrop-filter: blur(1.25rem);
|
|
132
|
+
-webkit-backdrop-filter: blur(1.25rem);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.mark {
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: 0.75rem;
|
|
139
|
+
color: var(--muted);
|
|
140
|
+
text-decoration: none;
|
|
141
|
+
font-family: var(--font-sans);
|
|
142
|
+
font-size: 1.125rem;
|
|
143
|
+
font-weight: 400;
|
|
144
|
+
letter-spacing: 0.03rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.mark-tld {
|
|
148
|
+
color: light-dark(rgba(92, 88, 82, 0.5), rgba(189, 183, 173, 0.62));
|
|
149
|
+
font-weight: 300;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.mark img,
|
|
153
|
+
img.mark-logo {
|
|
154
|
+
width: 2.125rem;
|
|
155
|
+
height: 2.125rem;
|
|
156
|
+
border-radius: 0.5rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
[data-theme="dark"] .mark .logo-light,
|
|
160
|
+
[data-theme="light"] .mark .logo-dark { display: none; }
|
|
161
|
+
|
|
162
|
+
main, .page { min-width: 0; }
|
|
163
|
+
|
|
164
|
+
.eyebrow {
|
|
165
|
+
margin: 0 0 1rem;
|
|
166
|
+
color: var(--accent);
|
|
167
|
+
font: 300 var(--text-md)/1.2 var(--font-mono);
|
|
168
|
+
letter-spacing: 0.14em;
|
|
169
|
+
text-transform: uppercase;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
nav {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 1rem;
|
|
176
|
+
font-family: var(--font-sans);
|
|
177
|
+
font-size: var(--text-md);
|
|
178
|
+
color: var(--muted);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
nav a {
|
|
182
|
+
text-decoration: none;
|
|
183
|
+
border-bottom: 1px solid transparent;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
nav a:hover,
|
|
187
|
+
nav a[aria-current="page"] {
|
|
188
|
+
border-bottom-color: var(--accent);
|
|
189
|
+
color: var(--text);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.nav-links {
|
|
193
|
+
display: flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
gap: 1rem;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.nav-actions {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 1rem;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.nav-icons {
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
gap: 0.75rem;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.github-link {
|
|
211
|
+
display: inline-flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
border-bottom: 0;
|
|
214
|
+
color: var(--muted);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.github-link svg {
|
|
218
|
+
width: 1.05rem;
|
|
219
|
+
height: 1.05rem;
|
|
220
|
+
fill: currentColor;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.github-link:hover {
|
|
224
|
+
color: var(--text);
|
|
225
|
+
border-bottom: 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.stack-menu { position: relative; }
|
|
229
|
+
|
|
230
|
+
.stack-menu summary {
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
list-style: none;
|
|
233
|
+
transition: color 160ms ease;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.stack-menu summary::-webkit-details-marker { display: none; }
|
|
237
|
+
.stack-menu summary:hover,
|
|
238
|
+
.stack-menu summary:focus-visible,
|
|
239
|
+
.stack-menu[open] summary,
|
|
240
|
+
.stack-menu:has(a[aria-current="page"]) summary { color: var(--text); }
|
|
241
|
+
|
|
242
|
+
.stack-menu summary:focus-visible {
|
|
243
|
+
border-radius: 0.2rem;
|
|
244
|
+
outline: 2px solid var(--accent);
|
|
245
|
+
outline-offset: 4px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.stack-popover {
|
|
249
|
+
position: absolute;
|
|
250
|
+
top: calc(100% + 0.75rem);
|
|
251
|
+
right: 0;
|
|
252
|
+
z-index: 30;
|
|
253
|
+
width: 14rem;
|
|
254
|
+
padding: 0.45rem;
|
|
255
|
+
border-radius: 0.5rem;
|
|
256
|
+
background: light-dark(rgb(255 250 240 / 78%), rgb(48 35 29 / 72%));
|
|
257
|
+
-webkit-backdrop-filter: blur(0.5rem);
|
|
258
|
+
backdrop-filter: blur(0.5rem);
|
|
259
|
+
box-shadow: 0 0.8rem 2.5rem light-dark(rgb(20 14 9 / 18%), rgb(0 0 0 / 58%));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.stack-popover a {
|
|
263
|
+
display: block;
|
|
264
|
+
padding: 0.65rem 0.75rem;
|
|
265
|
+
border: 0;
|
|
266
|
+
border-radius: 0.3rem;
|
|
267
|
+
color: var(--muted);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.stack-popover a:hover,
|
|
271
|
+
.stack-popover a:focus-visible {
|
|
272
|
+
border: 0;
|
|
273
|
+
outline: none;
|
|
274
|
+
background: var(--quiet);
|
|
275
|
+
color: var(--text);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.stack-popover a[aria-current="page"] { color: var(--accent); }
|
|
279
|
+
|
|
280
|
+
/* first menu in nav (About) opens aligned to its left edge */
|
|
281
|
+
.nav-links > .stack-menu:first-child .stack-popover { left: 0; right: auto; }
|
|
282
|
+
.stack-popover span,
|
|
283
|
+
.stack-popover small { display: block; }
|
|
284
|
+
.stack-popover span { color: inherit; font-weight: 700; }
|
|
285
|
+
.stack-popover small { margin-top: 0.1rem; color: var(--muted); font-size: var(--text-xs); }
|
|
286
|
+
|
|
287
|
+
/* ---- theme toggle ---- */
|
|
288
|
+
|
|
289
|
+
.theme-toggle {
|
|
290
|
+
appearance: none;
|
|
291
|
+
display: inline-grid;
|
|
292
|
+
place-items: center;
|
|
293
|
+
width: 1.7rem;
|
|
294
|
+
height: 1.7rem;
|
|
295
|
+
border: 0;
|
|
296
|
+
background: transparent;
|
|
297
|
+
color: var(--muted);
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
padding: 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.theme-toggle:hover { color: var(--text); }
|
|
303
|
+
|
|
304
|
+
.theme-toggle svg {
|
|
305
|
+
width: 1.1rem;
|
|
306
|
+
height: 1.1rem;
|
|
307
|
+
fill: none;
|
|
308
|
+
stroke: currentColor;
|
|
309
|
+
stroke-width: 1.8;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
[data-theme="dark"] .theme-toggle .icon-sun,
|
|
313
|
+
[data-theme="light"] .theme-toggle .icon-moon { display: none; }
|
|
314
|
+
[data-theme="dark"] .theme-toggle .icon-moon,
|
|
315
|
+
[data-theme="light"] .theme-toggle .icon-sun { display: block; }
|
|
316
|
+
/* alias var names used by server/forms stylesheets */
|
|
317
|
+
:root {
|
|
318
|
+
--sans: var(--font-sans);
|
|
319
|
+
--serif: var(--font-serif);
|
|
320
|
+
--mono: var(--font-mono);
|
|
321
|
+
--fs-xs: var(--text-xs);
|
|
322
|
+
--fs-sm: var(--text-sm);
|
|
323
|
+
--fs-md: var(--text-md);
|
|
324
|
+
--fs-base: var(--text-base);
|
|
325
|
+
--fs-lg: var(--text-lg);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ---- footer ---- */
|
|
329
|
+
|
|
330
|
+
.site-footer {
|
|
331
|
+
margin-top: clamp(4rem, 8vw, 6rem);
|
|
332
|
+
padding: 1.5rem 0;
|
|
333
|
+
border-top: 1px solid color-mix(in srgb, var(--line) 50%, transparent);
|
|
334
|
+
font-family: var(--font-sans);
|
|
335
|
+
font-size: var(--text-sm);
|
|
336
|
+
color: var(--muted);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.footer-line {
|
|
340
|
+
display: flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
justify-content: space-between;
|
|
343
|
+
gap: 1rem;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.footer-line nav {
|
|
347
|
+
display: flex;
|
|
348
|
+
align-items: center;
|
|
349
|
+
gap: 1.25rem;
|
|
350
|
+
font: inherit;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.site-footer .stack-popover {
|
|
354
|
+
top: auto;
|
|
355
|
+
right: auto;
|
|
356
|
+
bottom: calc(100% + 0.75rem);
|
|
357
|
+
left: 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.footer-meta {
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: center;
|
|
363
|
+
gap: 0.3rem;
|
|
364
|
+
margin: 0.75rem 0 0;
|
|
365
|
+
font-size: var(--text-xs);
|
|
366
|
+
opacity: 0.75;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.site-footer a {
|
|
370
|
+
text-decoration: none;
|
|
371
|
+
color: var(--muted);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.site-footer a:hover {
|
|
375
|
+
color: var(--accent);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.footer-heart {
|
|
379
|
+
width: 0.875rem;
|
|
380
|
+
height: 0.875rem;
|
|
381
|
+
fill: currentColor;
|
|
382
|
+
color: var(--danger);
|
|
383
|
+
opacity: 0.5;
|
|
384
|
+
transition: opacity 150ms;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.footer-meta:hover .footer-heart {
|
|
388
|
+
opacity: 1;
|
|
389
|
+
animation: heart-pulse 1s ease-in-out infinite;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
@keyframes heart-pulse {
|
|
393
|
+
0%, 100% { transform: scale(1); }
|
|
394
|
+
50% { transform: scale(1.3); }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.sr-only {
|
|
398
|
+
position: absolute;
|
|
399
|
+
width: 1px;
|
|
400
|
+
height: 1px;
|
|
401
|
+
margin: -1px;
|
|
402
|
+
padding: 0;
|
|
403
|
+
overflow: hidden;
|
|
404
|
+
clip: rect(0 0 0 0);
|
|
405
|
+
white-space: nowrap;
|
|
406
|
+
border: 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
/* green pulse on the box around a just-used copy button */
|
|
411
|
+
@keyframes copy-glow {
|
|
412
|
+
0%, 100% { box-shadow: 0 0 0 0 transparent; }
|
|
413
|
+
35% {
|
|
414
|
+
box-shadow:
|
|
415
|
+
0 0 0 3px color-mix(in srgb, var(--success) 30%, transparent),
|
|
416
|
+
0 0 1.5rem color-mix(in srgb, var(--success) 40%, transparent);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
:has(> .copied), :has(> .is-copied) {
|
|
421
|
+
animation: copy-glow 1.2s ease-in-out;
|
|
422
|
+
}
|