Compare commits
14 Commits
10a7d0a878
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 19999e7104 | |||
| 813c403fcc | |||
| 08df07f221 | |||
| 98c1d81031 | |||
| 4614cd25a0 | |||
| 7aa2910b12 | |||
| 8e5e5cbef8 | |||
| 57721efbcd | |||
| b3cf6bd02e | |||
| f96079f631 | |||
| 02437358cf | |||
| 2a3a15d7ac | |||
| 0f78505e99 | |||
| 5077aeacfd |
@@ -16,10 +16,13 @@ the vault so Obsidian shows them too. Its changes are recorded with the author
|
|||||||
| Page | What it does |
|
| Page | What it does |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Overview | Live and total notes, what waits for review, folder problems, the service's last scan and last error, a Scan now button, each sync job's last run and last success, and Library suggestions: series moved to Reading in PersonalReader with no Library item, each with Add to the Library (title, medium, AniList id, state now, published) and Dismiss. |
|
| Overview | Live and total notes, what waits for review, folder problems, the service's last scan and last error, a Scan now button, each sync job's last run and last success, and Library suggestions: series moved to Reading in PersonalReader with no Library item, each with Add to the Library (title, medium, AniList id, state now, published) and Dismiss. |
|
||||||
|
| Site pages | Each page of the site with its Page text note (lede, photos, portrait) as thumbnails, **Edit page** or **Add page text**, and links to the notes that fill the page (crafts, games, decks, product lines, settings). |
|
||||||
|
| Before launch | What is still a placeholder, worked out from the notes so it shrinks as they are fixed: pages showing their built-in wording (no Page text note, a draft one, no lede, no About portrait), notes holding a comment, Coming soon, TODO, TBD or lorem, posts not published, and live notes missing the field their page is built around (a craft's blurb, a make's finish date, a deck's take). Each line links to Edit or Add page text. |
|
||||||
| Review | Held changes with the rules' reasons and a line diff against the live version. Accept puts it live (and into the vault, for API changes). Reject keeps the live version; for an edit made in Obsidian, "Reject and put the live version back" also rewrites the file. Earlier decisions are listed by status. |
|
| Review | Held changes with the rules' reasons and a line diff against the live version. Accept puts it live (and into the vault, for API changes). Reject keeps the live version; for an edit made in Obsidian, "Reject and put the live version back" also rewrites the file. Earlier decisions are listed by status. |
|
||||||
| Notes, Drafts | Browse and search every note, or only the unpublished ones. |
|
| Notes, Drafts | Browse and search every note, or only the unpublished ones. Each row has Edit; filtered to one type, the list also offers to add a note of that type. |
|
||||||
| A note | A preview of the body, its fields, what links to it, Publish or Unpublish, and its history. Any earlier version can be compared with the live one and rolled back. |
|
| A note | A preview of the body, its fields, what links to it, Edit, Publish or Unpublish, and its history. Any earlier version can be compared with the live one and rolled back. |
|
||||||
| Posts | Every blog post, drafts first, and **Write a post**: title, topic, kind of post, publish date, summary, a cover picked from the vault or uploaded, the pages it is about, Feature it, case study details, and the Markdown body. Preview shows it rendered with its images; Save draft and Publish write the note to `Posts/`. Images added to the post are uploaded to `_attachments/Posts` and appended as `![[file]]` lines. |
|
| Posts | Every blog post, drafts first, and **Write a post**: title, topic, kind of post, publish date, summary, a cover picked from the vault or uploaded, the pages it is about, Feature it, case study details, and the Markdown body. Preview shows it rendered with its images; Save draft and Publish write the note to `Posts/`. Images added to the post are uploaded to `_attachments/Posts` and appended as `![[file]]` lines. |
|
||||||
|
| Edit | The form for any note (posts open their own editor). It is built from the note type's schema in `@gk/schema`, so it offers exactly the fields the content service accepts: image fields (`format: vault-image`) show a thumbnail, a pick from the vault and an upload into the type's `_attachments` folder; link fields offer note names; lists of rows (photos, specs, prices, socials) can be added to, removed and moved up or down. An emptied field is removed from the note, an unticked optional box is left out, and fields the form cannot show (a list inside a row) stay as they are. The title is the file name, so it is renamed in Obsidian. |
|
||||||
| Images | Everything in `_attachments`, newest first, by folder, with which notes use each one. Upload several at once into a folder, copy an image's `![[file]]` line, and delete images no note uses. |
|
| Images | Everything in `_attachments`, newest first, by folder, with which notes use each one. Upload several at once into a folder, copy an image's `![[file]]` line, and delete images no note uses. |
|
||||||
| Capture | A status for the Now log, or a Library item with the option to post a status about it in the same step. |
|
| Capture | A status for the Now log, or a Library item with the option to post a status about it in the same step. |
|
||||||
| Store | Orders open or closed per category, with the reopen date and turnaround, and open slots or stock per product line. |
|
| Store | Orders open or closed per category, with the reopen date and turnaround, and open slots or stock per product line. |
|
||||||
|
|||||||
@@ -76,4 +76,6 @@ import PageShell from "@gk/ui/components/PageShell.astro";
|
|||||||
</PageShell>
|
</PageShell>
|
||||||
```
|
```
|
||||||
|
|
||||||
The site is dark only for now; light mode comes in a later phase.
|
The site follows the device's light or dark setting until the visitor presses the sun and moon toggle in the header (`ThemeToggle`), which saves their choice in the browser. A script in PageShell's head sets `<html data-theme="light">` or `"dark"` before the page paints; without script the site stays dark. Style light mode with `:root[data-theme="light"]` (in a component, `:global(:root[data-theme="light"]) .x`), not a `prefers-color-scheme` media query, so the toggle can override it. Colors come from the tokens in `tokens.css`, which has the light values too, so a component that uses `var(--gk-…)` needs nothing more. For a translucent color use a channel token, as in `rgba(var(--gk-gold-rgb), 0.35)`, never a hardcoded rgba. In SVG markup use `style="fill:var(--gk-gold)"`.
|
||||||
|
|
||||||
|
Motif images come in a light recolor and, for seals, a gold foil version for each mode. Show a motif with `Motif` (or `Seal`), which renders both files and shows the one for the current mode; a plain `<img>` stays dark. Pass `foil` only to a seal standing alone at 120px or more and half opacity or more. After changing a motif SVG, run `node packages/ui/scripts/light-motifs.mjs` to rewrite the `light/`, `foil/` and `foil-light/` versions.
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
*
|
*
|
||||||
* Each field carries `title` and `description` metadata. The JSON Schema
|
* Each field carries `title` and `description` metadata. The JSON Schema
|
||||||
* export keeps them, so the MCP server, the admin page and any later editor
|
* export keeps them, so the MCP server, the admin page and any later editor
|
||||||
* can render a form or explain a field without a second definition.
|
* can render a form or explain a field without a second definition. Image
|
||||||
|
* and link fields also carry a `format` (`vault-image`, `vault-link`,
|
||||||
|
* `vault-links`) so a form knows to offer a picker.
|
||||||
*/
|
*/
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { normalizeWikilinkInput, wikilinkTarget } from "./names.ts";
|
import { normalizeWikilinkInput, wikilinkTarget } from "./names.ts";
|
||||||
@@ -30,7 +32,7 @@ export const wikilink = (meta: Meta) =>
|
|||||||
normalizeWikilinkInput,
|
normalizeWikilinkInput,
|
||||||
z.string().refine((v) => wikilinkTarget(v) !== null, "Write a link as [[Note name]]"),
|
z.string().refine((v) => wikilinkTarget(v) !== null, "Write a link as [[Note name]]"),
|
||||||
)
|
)
|
||||||
.meta(meta);
|
.meta({ format: "vault-link", ...meta });
|
||||||
|
|
||||||
/** A list of links to other notes. */
|
/** A list of links to other notes. */
|
||||||
export const wikilinks = (meta: Meta) =>
|
export const wikilinks = (meta: Meta) =>
|
||||||
@@ -39,7 +41,7 @@ export const wikilinks = (meta: Meta) =>
|
|||||||
(v) => (Array.isArray(v) ? v.map(normalizeWikilinkInput) : v),
|
(v) => (Array.isArray(v) ? v.map(normalizeWikilinkInput) : v),
|
||||||
z.array(z.string().refine((v) => wikilinkTarget(v) !== null, "Write each link as [[Note name]]")),
|
z.array(z.string().refine((v) => wikilinkTarget(v) !== null, "Write each link as [[Note name]]")),
|
||||||
)
|
)
|
||||||
.meta(meta);
|
.meta({ format: "vault-links", ...meta });
|
||||||
|
|
||||||
/** An image: a vault attachment as `[[file.jpg]]`, or a full https URL. */
|
/** An image: a vault attachment as `[[file.jpg]]`, or a full https URL. */
|
||||||
export const image = (meta: Meta) =>
|
export const image = (meta: Meta) =>
|
||||||
@@ -53,7 +55,7 @@ export const image = (meta: Meta) =>
|
|||||||
"Use a vault image as [[file.jpg]] or a full https URL",
|
"Use a vault image as [[file.jpg]] or a full https URL",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.meta(meta);
|
.meta({ format: "vault-image", ...meta });
|
||||||
|
|
||||||
/** One short line of text. */
|
/** One short line of text. */
|
||||||
export const line = (meta: Meta) => z.string().trim().min(1).meta(meta);
|
export const line = (meta: Meta) => z.string().trim().min(1).meta(meta);
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Writes the light and foil versions of the motif SVGs from the dark originals
|
||||||
|
* in src/assets/motifs, using the inks Greg picked on the light motif board
|
||||||
|
* (2026-10-18), and the light GK monogram in src/assets/brand/light. Run after
|
||||||
|
* a motif or the monogram changes: node packages/ui/scripts/light-motifs.mjs
|
||||||
|
*
|
||||||
|
* light/ deep gold line: the dark inks swapped for Starlight-page inks, glow removed
|
||||||
|
* foil/ the dark seal with its gold as foil, glow kept
|
||||||
|
* foil-light/ the light seal with its gold as foil
|
||||||
|
*/
|
||||||
|
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const dir = join(import.meta.dirname, "../src/assets/motifs");
|
||||||
|
|
||||||
|
// Dark colors as they appear in the motif files, and their light inks.
|
||||||
|
const LIGHT = {
|
||||||
|
"#C9A24A": "#8A6516",
|
||||||
|
"#14A39A": "#0F8C84",
|
||||||
|
"#0E1522": "#F4F2ED",
|
||||||
|
"#101A2A": "#ECE8DF",
|
||||||
|
"#F4F2ED": "#131C2B",
|
||||||
|
"#F2D58A": "#A57C1F",
|
||||||
|
"#F6E7B8": "#8A6516",
|
||||||
|
"#2BC2B7": "#0F8C84",
|
||||||
|
"#9A70F5": "#6A2FE0",
|
||||||
|
"#FFF": "#F4F2ED",
|
||||||
|
};
|
||||||
|
const GOLD = ["#C9A24A", "#F2D58A"];
|
||||||
|
|
||||||
|
const stops = (s) => s.map(([o, c]) => `<stop offset="${o}" stop-color="${c}"/>`).join("");
|
||||||
|
const DARK_FOIL = stops([[0, "#8A6516"], [0.3, "#C9A24A"], [0.5, "#F6E7B8"], [0.68, "#C9A24A"], [1, "#8A6516"]]);
|
||||||
|
const LIGHT_FOIL = stops([[0, "#6F5112"], [0.35, "#A57C1F"], [0.5, "#C9A24A"], [0.65, "#8A6516"], [1, "#6F5112"]]);
|
||||||
|
|
||||||
|
const hex = (c) => new RegExp(c.replace("#", "#") + "(?![0-9A-Fa-f])", "gi");
|
||||||
|
|
||||||
|
function recolor(svg, map) {
|
||||||
|
// One pass, so a swapped color is never swapped again.
|
||||||
|
const keys = Object.keys(map).sort((a, b) => b.length - a.length);
|
||||||
|
const any = new RegExp(`(${keys.join("|")})(?![0-9A-Fa-f])`, "gi");
|
||||||
|
return svg.replace(any, (m) => map[keys.find((k) => k.toLowerCase() === m.toLowerCase())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function foil(svg, gradStops, golds) {
|
||||||
|
const [, , , w, h] = svg.match(/viewBox="([-\d.]+) ([-\d.]+) ([\d.]+) ([\d.]+)"/) ?? [0, 0, 0, 400, 400];
|
||||||
|
const grad = `<linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="${w}" y2="${h}">${gradStops}</linearGradient>`;
|
||||||
|
for (const g of golds) svg = svg.replace(hex(g), "url(#gkfoil)");
|
||||||
|
return svg.includes("<defs>") ? svg.replace("<defs>", `<defs>${grad}`) : svg.replace(/(<svg[^>]*>)/, `$1<defs>${grad}</defs>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glow filters are for the night sky only.
|
||||||
|
const unglow = (svg) => svg.replace(/\sfilter="url\(#[^)]*\)"/g, "");
|
||||||
|
|
||||||
|
for (const sub of ["light", "foil", "foil-light"]) mkdirSync(join(dir, sub), { recursive: true });
|
||||||
|
const files = readdirSync(dir).filter((f) => f.endsWith(".svg"));
|
||||||
|
for (const f of files) {
|
||||||
|
const dark = readFileSync(join(dir, f), "utf8");
|
||||||
|
const light = recolor(unglow(dark), LIGHT);
|
||||||
|
writeFileSync(join(dir, "light", f), light);
|
||||||
|
// Only the seals stand alone large enough for foil.
|
||||||
|
if (/seal|astrolabe|transmutation/.test(f) && !f.startsWith("mini-") && !f.startsWith("filament-banner")) {
|
||||||
|
writeFileSync(join(dir, "foil", f), foil(dark, DARK_FOIL, ["#C9A24A"]));
|
||||||
|
writeFileSync(join(dir, "foil-light", f), foil(light, LIGHT_FOIL, ["#8A6516", "#A57C1F"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`${files.length} motifs written`);
|
||||||
|
|
||||||
|
// The GK monogram on Starlight: a pale disc, deep gold rings and ink letters.
|
||||||
|
const MONOGRAM = {
|
||||||
|
...LIGHT,
|
||||||
|
"#131C2B": "#FFFFFF",
|
||||||
|
"#1F3B49": "#ECE6D8",
|
||||||
|
"#15232F": "#FBF9F4",
|
||||||
|
};
|
||||||
|
const brand = join(dir, "../brand");
|
||||||
|
mkdirSync(join(brand, "light"), { recursive: true });
|
||||||
|
writeFileSync(join(brand, "light", "gk-monogram.svg"), recolor(readFileSync(join(brand, "gk-monogram.svg"), "utf8"), MONOGRAM));
|
||||||
|
console.log("monogram written");
|
||||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F4F2ED" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Moon"><path d="M19.5 14.6A8 8 0 0 1 9.4 4.5a8 8 0 1 0 10.1 10.1z"/><path d="M17 3.5v2.4M15.8 4.7h2.4" stroke="#C9A24A"/></svg>
|
||||||
|
After Width: | Height: | Size: 305 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F4F2ED" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Sun"><circle cx="12" cy="12" r="4.2"/><path d="M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.6 1.6M17.1 17.1l1.6 1.6M5.3 18.7l1.6-1.6M17.1 6.9l1.6-1.6"/></svg>
|
||||||
|
After Width: | Height: | Size: 349 B |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Dice craft seal"><circle cx="200" cy="200" r="190" fill="none" stroke="url(#gkfoil)" stroke-width="2" /><circle cx="200" cy="200" r="184" fill="none" stroke="url(#gkfoil)" stroke-width="0.6" /><defs><linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="400"><stop offset="0" stop-color="#6F5112"/><stop offset="0.35" stop-color="#A57C1F"/><stop offset="0.5" stop-color="#C9A24A"/><stop offset="0.65" stop-color="#8A6516"/><stop offset="1" stop-color="#6F5112"/></linearGradient><path id="cscrdice" d="M 200,30.53 A 169.47,169.47 0 1 1 199.99,30.53"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="11.00" letter-spacing="10.745" fill="url(#gkfoil)"><textPath href="#cscrdice">DICE ✦ AQUA ✦ RESIN ✦ PATIUN ✦ DICE ✦ AQUA ✦ RESIN ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="160" fill="none" stroke="url(#gkfoil)" stroke-width="1.2" /><path d="M200.00,356.00 L64.90,122.00 L335.10,122.00Z" fill="none" stroke="#0F8C84" stroke-width="1.6" stroke-linejoin="round" /><circle cx="200" cy="200" r="78.0" fill="none" stroke="url(#gkfoil)" stroke-width="1" /><path d="M200.0,142.3 L250.0,171.1 L250.0,228.9 L200.0,257.7 L150.0,228.9 L150.0,171.1Z" fill="none" stroke="#131C2B" stroke-width="1.4" stroke-linejoin="round"/><path d="M200.0,228.9 L175.0,185.6 L225.0,185.6Z" fill="none" stroke="#131C2B" stroke-width="1.2" stroke-linejoin="round"/><line x1="200.0" y1="142.3" x2="175.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><line x1="200.0" y1="142.3" x2="225.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><line x1="250.0" y1="171.1" x2="225.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><line x1="250.0" y1="228.9" x2="225.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><line x1="250.0" y1="228.9" x2="200.0" y2="228.9" stroke="#131C2B" stroke-width=".9"/><line x1="200.0" y1="257.7" x2="200.0" y2="228.9" stroke="#131C2B" stroke-width=".9"/><line x1="150.0" y1="228.9" x2="200.0" y2="228.9" stroke="#131C2B" stroke-width=".9"/><line x1="150.0" y1="228.9" x2="175.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><line x1="150.0" y1="171.1" x2="175.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><text x="200.0" y="206.9" font-family="Cinzel,serif" font-weight="700" font-size="18.5" fill="#131C2B" text-anchor="middle" dominant-baseline="middle">20</text><circle cx="103.0" cy="144.0" r="15" fill="#F4F2ED" stroke="url(#gkfoil)" stroke-width="1"/><text x="95.97" y="149.52" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="16" fill="#131C2B" >🜄</text><circle cx="297.0" cy="144.0" r="15" fill="#F4F2ED" stroke="url(#gkfoil)" stroke-width="1"/><g transform="translate(289.21,137.50) scale(0.1024) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g><polygon points="200.00,3.00 201.56,11.44 210.00,13.00 201.56,14.56 200.00,23.00 198.44,14.56 190.00,13.00 198.44,11.44" fill="url(#gkfoil)"/><polygon points="387.00,190.00 388.56,198.44 397.00,200.00 388.56,201.56 387.00,210.00 385.44,201.56 377.00,200.00 385.44,198.44" fill="url(#gkfoil)"/><polygon points="200.00,377.00 201.56,385.44 210.00,387.00 201.56,388.56 200.00,397.00 198.44,388.56 190.00,387.00 198.44,385.44" fill="url(#gkfoil)"/><polygon points="13.00,190.00 14.56,198.44 23.00,200.00 14.56,201.56 13.00,210.00 11.44,201.56 3.00,200.00 11.44,198.44" fill="url(#gkfoil)"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Leather craft seal"><circle cx="200" cy="200" r="190" fill="none" stroke="url(#gkfoil)" stroke-width="2" /><circle cx="200" cy="200" r="184" fill="none" stroke="url(#gkfoil)" stroke-width="0.6" /><defs><linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="400"><stop offset="0" stop-color="#6F5112"/><stop offset="0.35" stop-color="#A57C1F"/><stop offset="0.5" stop-color="#C9A24A"/><stop offset="0.65" stop-color="#8A6516"/><stop offset="1" stop-color="#6F5112"/></linearGradient><path id="cscrleather" d="M 200,32.014999999999986 A 167.985,167.985 0 1 1 199.99,32.014999999999986"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="11.00" letter-spacing="8.973" fill="url(#gkfoil)"><textPath href="#cscrleather">LEATHER ✦ TERRA ✦ HIDE ✦ PATIUN ✦ LEATHER ✦ TERRA ✦ HIDE ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="160" fill="none" stroke="url(#gkfoil)" stroke-width="1.2" /><path d="M200.00,356.00 L64.90,122.00 L335.10,122.00Z" fill="none" stroke="#0F8C84" stroke-width="1.6" stroke-linejoin="round" /><line x1="162.2" y1="290.5" x2="237.8" y2="290.5" stroke="#0F8C84" stroke-width="1.2"/><circle cx="200" cy="200" r="78.0" fill="none" stroke="url(#gkfoil)" stroke-width="1" /><path d="M164.2,145.2 Q188.5,155.0 200.0,155.0 Q211.5,155.0 223.7,150.1 Q235.8,145.2 238.7,159.6 Q241.6,174.0 248.2,181.2 Q254.8,188.5 249.9,201.4 Q245.0,214.4 247.9,228.0 Q250.8,241.6 236.9,248.2 Q223.1,254.8 211.5,250.5 Q200.0,246.2 188.5,250.5 Q176.9,254.8 163.1,248.2 Q149.2,241.6 152.1,228.0 Q155.0,214.4 150.1,201.4 Q145.2,188.5 151.8,181.2 Q158.4,174.0 161.3,159.6 Z" fill="none" stroke="#131C2B" stroke-width="1.4" stroke-linejoin="round"/><path d="M174.2,160.5 Q191.7,167.6 200.0,167.6 Q208.3,167.6 217.0,164.1 Q225.8,160.5 227.8,170.9 Q229.9,181.3 234.7,186.5 Q239.5,191.7 235.9,201.0 Q232.4,210.4 234.5,220.2 Q236.6,229.9 226.6,234.7 Q216.6,239.5 208.3,236.4 Q200.0,233.2 191.7,236.4 Q183.4,239.5 173.4,234.7 Q163.4,229.9 165.5,220.2 Q167.6,210.4 164.1,201.0 Q160.5,191.7 165.3,186.5 Q170.1,181.3 172.2,170.9 Z" fill="none" stroke="#131C2B" stroke-width="1" stroke-dasharray="4 3"/><circle cx="103.0" cy="144.0" r="15" fill="#F4F2ED" stroke="url(#gkfoil)" stroke-width="1"/><text x="95.97" y="149.52" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="16" fill="#131C2B" >🜃</text><circle cx="297.0" cy="144.0" r="15" fill="#F4F2ED" stroke="url(#gkfoil)" stroke-width="1"/><g transform="translate(289.21,137.50) scale(0.1024) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g><polygon points="200.00,3.00 201.56,11.44 210.00,13.00 201.56,14.56 200.00,23.00 198.44,14.56 190.00,13.00 198.44,11.44" fill="url(#gkfoil)"/><polygon points="387.00,190.00 388.56,198.44 397.00,200.00 388.56,201.56 387.00,210.00 385.44,201.56 377.00,200.00 385.44,198.44" fill="url(#gkfoil)"/><polygon points="200.00,377.00 201.56,385.44 210.00,387.00 201.56,388.56 200.00,397.00 198.44,388.56 190.00,387.00 198.44,385.44" fill="url(#gkfoil)"/><polygon points="13.00,190.00 14.56,198.44 23.00,200.00 14.56,201.56 13.00,210.00 11.44,201.56 3.00,200.00 11.44,198.44" fill="url(#gkfoil)"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Print craft seal"><circle cx="200" cy="200" r="190" fill="none" stroke="url(#gkfoil)" stroke-width="2" /><circle cx="200" cy="200" r="184" fill="none" stroke="url(#gkfoil)" stroke-width="0.6" /><defs><linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="400"><stop offset="0" stop-color="#6F5112"/><stop offset="0.35" stop-color="#A57C1F"/><stop offset="0.5" stop-color="#C9A24A"/><stop offset="0.65" stop-color="#8A6516"/><stop offset="1" stop-color="#6F5112"/></linearGradient><path id="cscrprint" d="M 200,32.014999999999986 A 167.985,167.985 0 1 1 199.99,32.014999999999986"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="11.00" letter-spacing="9.692" fill="url(#gkfoil)"><textPath href="#cscrprint">PRINT ✦ IGNIS ✦ LAYER ✦ PATIUN ✦ PRINT ✦ IGNIS ✦ LAYER ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="160" fill="none" stroke="url(#gkfoil)" stroke-width="1.2" /><path d="M200.00,44.00 L335.10,278.00 L64.90,278.00Z" fill="none" stroke="#0F8C84" stroke-width="1.6" stroke-linejoin="round" /><circle cx="200" cy="200" r="78.0" fill="none" stroke="url(#gkfoil)" stroke-width="1" /><path d="M200.0,148.1 L245.2,174.0 L200.0,200.0 L154.8,174.0Z" fill="none" stroke="#131C2B" stroke-width="1.3" stroke-linejoin="round"/><path d="M154.8,174.0 L154.8,226.0 L200.0,251.9 L245.2,226.0 L245.2,174.0 M200.0,200.0 L200.0,251.9" fill="none" stroke="#131C2B" stroke-width="1.3" stroke-linejoin="round"/><path d="M154.8,184.4 L200.0,210.4 L245.2,184.4" fill="none" stroke="#131C2B" stroke-width=".6"/><path d="M154.8,194.8 L200.0,220.8 L245.2,194.8" fill="none" stroke="#131C2B" stroke-width=".6"/><path d="M154.8,205.2 L200.0,231.2 L245.2,205.2" fill="none" stroke="#131C2B" stroke-width=".6"/><path d="M154.8,215.6 L200.0,241.6 L245.2,215.6" fill="none" stroke="#131C2B" stroke-width=".6"/><circle cx="297.0" cy="256.0" r="15" fill="#F4F2ED" stroke="url(#gkfoil)" stroke-width="1"/><text x="289.95" y="261.92" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="16" fill="#131C2B" >🜂</text><circle cx="103.0" cy="256.0" r="15" fill="#F4F2ED" stroke="url(#gkfoil)" stroke-width="1"/><g transform="translate(95.22,249.50) scale(0.1024) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g><polygon points="200.00,3.00 201.56,11.44 210.00,13.00 201.56,14.56 200.00,23.00 198.44,14.56 190.00,13.00 198.44,11.44" fill="url(#gkfoil)"/><polygon points="387.00,190.00 388.56,198.44 397.00,200.00 388.56,201.56 387.00,210.00 385.44,201.56 377.00,200.00 385.44,198.44" fill="url(#gkfoil)"/><polygon points="200.00,377.00 201.56,385.44 210.00,387.00 201.56,388.56 200.00,397.00 198.44,388.56 190.00,387.00 198.44,385.44" fill="url(#gkfoil)"/><polygon points="13.00,190.00 14.56,198.44 23.00,200.00 14.56,201.56 13.00,210.00 11.44,201.56 3.00,200.00 11.44,198.44" fill="url(#gkfoil)"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Kepler nest seal"><circle cx="200" cy="200" r="194" fill="none" stroke="url(#gkfoil)" stroke-width="2" /><circle cx="200" cy="200" r="178" fill="none" stroke="url(#gkfoil)" stroke-width="0.8" /><defs><linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="400"><stop offset="0" stop-color="#6F5112"/><stop offset="0.35" stop-color="#A57C1F"/><stop offset="0.5" stop-color="#C9A24A"/><stop offset="0.65" stop-color="#8A6516"/><stop offset="1" stop-color="#6F5112"/></linearGradient><path id="kpkp" d="M 200,17.4675 A 182.5325,182.5325 0 1 1 199.99,17.4675"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="9.50" letter-spacing="5.187" fill="url(#gkfoil)"><textPath href="#kpkp">HARMONICES MUNDI ✦ PATIUN ✦ DICE · LEATHER · PRINT ✦ HARMONICES MUNDI ✦ PATIUN ✦ DICE · LEATHER · PRINT ✦ </textPath></text><circle cx="200" cy="200" r="176" fill="none" stroke="url(#gkfoil)" stroke-width="0.5" /><path d="M200.00,24.00 L337.60,90.27 L371.59,239.16 L276.36,358.57 L123.64,358.57 L28.41,239.16 L62.40,90.27Z" fill="none" stroke="url(#gkfoil)" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="24.00" r="3.2" fill="#131C2B"/><circle cx="337.60" cy="90.27" r="3.2" fill="#131C2B"/><circle cx="371.59" cy="239.16" r="3.2" fill="#131C2B"/><circle cx="276.36" cy="358.57" r="3.2" fill="#131C2B"/><circle cx="123.64" cy="358.57" r="3.2" fill="#131C2B"/><circle cx="28.41" cy="239.16" r="3.2" fill="#131C2B"/><circle cx="62.40" cy="90.27" r="3.2" fill="#131C2B"/><circle cx="200" cy="200" r="158.57052075082578" fill="none" stroke="#0F8C84" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,41.43 L337.33,120.71 L337.33,279.29 L200.00,358.57 L62.67,279.29 L62.67,120.71Z" fill="none" stroke="#0F8C84" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="41.43" r="2.4" fill="#131C2B"/><circle cx="337.33" cy="120.71" r="2.4" fill="#131C2B"/><circle cx="337.33" cy="279.29" r="2.4" fill="#131C2B"/><circle cx="200.00" cy="358.57" r="2.4" fill="#131C2B"/><circle cx="62.67" cy="279.29" r="2.4" fill="#131C2B"/><circle cx="62.67" cy="120.71" r="2.4" fill="#131C2B"/><circle cx="200" cy="200" r="137.32609926154262" fill="none" stroke="url(#gkfoil)" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,62.67 L330.60,157.56 L280.72,311.10 L119.28,311.10 L69.40,157.56Z" fill="none" stroke="url(#gkfoil)" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="62.67" r="2.4" fill="#131C2B"/><circle cx="330.60" cy="157.56" r="2.4" fill="#131C2B"/><circle cx="280.72" cy="311.10" r="2.4" fill="#131C2B"/><circle cx="119.28" cy="311.10" r="2.4" fill="#131C2B"/><circle cx="69.40" cy="157.56" r="2.4" fill="#131C2B"/><circle cx="200" cy="200" r="111.0991480738089" fill="none" stroke="#0F8C84" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,88.90 L311.10,200.00 L200.00,311.10 L88.90,200.00Z" fill="none" stroke="#0F8C84" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="88.90" r="2.4" fill="#131C2B"/><circle cx="311.10" cy="200.00" r="2.4" fill="#131C2B"/><circle cx="200.00" cy="311.10" r="2.4" fill="#131C2B"/><circle cx="88.90" cy="200.00" r="2.4" fill="#131C2B"/><circle cx="200" cy="200" r="78.55896098703865" fill="none" stroke="url(#gkfoil)" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,121.44 L268.03,239.28 L131.97,239.28Z" fill="none" stroke="url(#gkfoil)" stroke-width="1.3" stroke-linejoin="round" /><circle cx="200.00" cy="121.44" r="2.4" fill="#131C2B"/><circle cx="268.03" cy="239.28" r="2.4" fill="#131C2B"/><circle cx="131.97" cy="239.28" r="2.4" fill="#131C2B"/><circle cx="200" cy="200" r="39.27948049351933" fill="none" stroke="#0F8C84" stroke-width="1.2" /><g transform="translate(177.67,181.34) scale(0.2938) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g><polygon points="200.00,-4.00 201.56,4.44 210.00,6.00 201.56,7.56 200.00,16.00 198.44,7.56 190.00,6.00 198.44,4.44" fill="url(#gkfoil)"/><polygon points="394.00,190.00 395.56,198.44 404.00,200.00 395.56,201.56 394.00,210.00 392.44,201.56 384.00,200.00 392.44,198.44" fill="url(#gkfoil)"/><polygon points="200.00,384.00 201.56,392.44 210.00,394.00 201.56,395.56 200.00,404.00 198.44,395.56 190.00,394.00 198.44,392.44" fill="url(#gkfoil)"/><polygon points="6.00,190.00 7.56,198.44 16.00,200.00 7.56,201.56 6.00,210.00 4.44,201.56 -4.00,200.00 4.44,198.44" fill="url(#gkfoil)"/></svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Layered seal"><circle cx="200" cy="200" r="192" fill="none" stroke="#0F8C84" stroke-width="2" /><circle cx="200" cy="200" r="170" fill="none" stroke="#0F8C84" stroke-width="0.8" /><text x="240.34" y="30.28" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♈︎</text><text x="321.90" y="77.12" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♉︎</text><text x="368.88" y="158.05" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♊︎</text><text x="368.18" y="251.89" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♋︎</text><text x="322.60" y="331.42" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♌︎</text><text x="240.76" y="378.40" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♍︎</text><text x="146.57" y="379.73" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♎︎</text><text x="65.29" y="331.63" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♏︎</text><text x="19.64" y="251.75" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♐︎</text><text x="18.38" y="156.79" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♑︎</text><text x="65.43" y="76.91" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♒︎</text><text x="147.41" y="30.49" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♓︎</text><line x1="200.00" y1="30.00" x2="200.00" y2="8.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="285.00" y1="52.78" x2="296.00" y2="33.72" stroke="#0F8C84" stroke-width="0.8"/><line x1="347.22" y1="115.00" x2="366.28" y2="104.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="370.00" y1="200.00" x2="392.00" y2="200.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="347.22" y1="285.00" x2="366.28" y2="296.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="285.00" y1="347.22" x2="296.00" y2="366.28" stroke="#0F8C84" stroke-width="0.8"/><line x1="200.00" y1="370.00" x2="200.00" y2="392.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="115.00" y1="347.22" x2="104.00" y2="366.28" stroke="#0F8C84" stroke-width="0.8"/><line x1="52.78" y1="285.00" x2="33.72" y2="296.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="30.00" y1="200.00" x2="8.00" y2="200.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="52.78" y1="115.00" x2="33.72" y2="104.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="115.00" y1="52.78" x2="104.00" y2="33.72" stroke="#0F8C84" stroke-width="0.8"/><defs><linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="400"><stop offset="0" stop-color="#6F5112"/><stop offset="0.35" stop-color="#A57C1F"/><stop offset="0.5" stop-color="#C9A24A"/><stop offset="0.65" stop-color="#8A6516"/><stop offset="1" stop-color="#6F5112"/></linearGradient><path id="ltf" d="M 200,49.379999999999995 A 150.62,150.62 0 1 1 199.99,49.379999999999995"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="12.00" letter-spacing="10.483" fill="url(#gkfoil)"><textPath href="#ltf">PATIUN ✦ PATIUN ✦ PATIUN ✦ PATIUN ✦ PATIUN ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="140" fill="none" stroke="url(#gkfoil)" stroke-width="1" /><path d="M200.0,60.0 L340.0,200.0 L200.0,340.0 L60.0,200.0Z" fill="none" stroke="#0F8C84" stroke-width="1.2"/><path d="M299.0,101.0 L299.0,299.0 L101.0,299.0 L101.0,101.0Z" fill="none" stroke="#0F8C84" stroke-width="1.2"/><circle cx="200" cy="200" r="99" fill="none" stroke="url(#gkfoil)" stroke-width="0.9" /><text x="238.56" y="96.53" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="url(#gkfoil)" >🜂</text><text x="302.42" y="250.33" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="url(#gkfoil)" >🜄</text><text x="148.24" y="314.57" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="url(#gkfoil)" >🜁</text><text x="84.38" y="160.02" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="url(#gkfoil)" >🜃</text><path d="M200.0,101.0 L285.7,249.5 L114.3,249.5Z" fill="none" stroke="url(#gkfoil)" stroke-width=".9"/><path d="M200.0,299.0 L114.3,150.5 L285.7,150.5Z" fill="none" stroke="url(#gkfoil)" stroke-width=".9"/><circle cx="200" cy="200" r="48" fill="none" stroke="#0F8C84" stroke-width="1.4" /><g transform="translate(173.67,178.00) scale(0.3465) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Dice craft seal"><circle cx="200" cy="200" r="190" fill="none" stroke="url(#gkfoil)" stroke-width="2" /><circle cx="200" cy="200" r="184" fill="none" stroke="url(#gkfoil)" stroke-width="0.6" /><defs><linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="400"><stop offset="0" stop-color="#8A6516"/><stop offset="0.3" stop-color="#C9A24A"/><stop offset="0.5" stop-color="#F6E7B8"/><stop offset="0.68" stop-color="#C9A24A"/><stop offset="1" stop-color="#8A6516"/></linearGradient><path id="cscrdice" d="M 200,30.53 A 169.47,169.47 0 1 1 199.99,30.53"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="11.00" letter-spacing="10.745" fill="url(#gkfoil)"><textPath href="#cscrdice">DICE ✦ AQUA ✦ RESIN ✦ PATIUN ✦ DICE ✦ AQUA ✦ RESIN ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="160" fill="none" stroke="url(#gkfoil)" stroke-width="1.2" /><path d="M200.00,356.00 L64.90,122.00 L335.10,122.00Z" fill="none" stroke="#14A39A" stroke-width="1.6" stroke-linejoin="round" /><circle cx="200" cy="200" r="78.0" fill="none" stroke="url(#gkfoil)" stroke-width="1" /><path d="M200.0,142.3 L250.0,171.1 L250.0,228.9 L200.0,257.7 L150.0,228.9 L150.0,171.1Z" fill="none" stroke="#F4F2ED" stroke-width="1.4" stroke-linejoin="round"/><path d="M200.0,228.9 L175.0,185.6 L225.0,185.6Z" fill="none" stroke="#F4F2ED" stroke-width="1.2" stroke-linejoin="round"/><line x1="200.0" y1="142.3" x2="175.0" y2="185.6" stroke="#F4F2ED" stroke-width=".9"/><line x1="200.0" y1="142.3" x2="225.0" y2="185.6" stroke="#F4F2ED" stroke-width=".9"/><line x1="250.0" y1="171.1" x2="225.0" y2="185.6" stroke="#F4F2ED" stroke-width=".9"/><line x1="250.0" y1="228.9" x2="225.0" y2="185.6" stroke="#F4F2ED" stroke-width=".9"/><line x1="250.0" y1="228.9" x2="200.0" y2="228.9" stroke="#F4F2ED" stroke-width=".9"/><line x1="200.0" y1="257.7" x2="200.0" y2="228.9" stroke="#F4F2ED" stroke-width=".9"/><line x1="150.0" y1="228.9" x2="200.0" y2="228.9" stroke="#F4F2ED" stroke-width=".9"/><line x1="150.0" y1="228.9" x2="175.0" y2="185.6" stroke="#F4F2ED" stroke-width=".9"/><line x1="150.0" y1="171.1" x2="175.0" y2="185.6" stroke="#F4F2ED" stroke-width=".9"/><text x="200.0" y="206.9" font-family="Cinzel,serif" font-weight="700" font-size="18.5" fill="#F4F2ED" text-anchor="middle" dominant-baseline="middle">20</text><circle cx="103.0" cy="144.0" r="15" fill="#0E1522" stroke="url(#gkfoil)" stroke-width="1"/><text x="95.97" y="149.52" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="16" fill="#F4F2ED" >🜄</text><circle cx="297.0" cy="144.0" r="15" fill="#0E1522" stroke="url(#gkfoil)" stroke-width="1"/><g transform="translate(289.21,137.50) scale(0.1024) translate(-20,-33)"><g fill="none" stroke="#14A39A" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#14A39A"/></g><polygon points="200.00,3.00 201.56,11.44 210.00,13.00 201.56,14.56 200.00,23.00 198.44,14.56 190.00,13.00 198.44,11.44" fill="url(#gkfoil)"/><polygon points="387.00,190.00 388.56,198.44 397.00,200.00 388.56,201.56 387.00,210.00 385.44,201.56 377.00,200.00 385.44,198.44" fill="url(#gkfoil)"/><polygon points="200.00,377.00 201.56,385.44 210.00,387.00 201.56,388.56 200.00,397.00 198.44,388.56 190.00,387.00 198.44,385.44" fill="url(#gkfoil)"/><polygon points="13.00,190.00 14.56,198.44 23.00,200.00 14.56,201.56 13.00,210.00 11.44,201.56 3.00,200.00 11.44,198.44" fill="url(#gkfoil)"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Leather craft seal"><circle cx="200" cy="200" r="190" fill="none" stroke="url(#gkfoil)" stroke-width="2" /><circle cx="200" cy="200" r="184" fill="none" stroke="url(#gkfoil)" stroke-width="0.6" /><defs><linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="400"><stop offset="0" stop-color="#8A6516"/><stop offset="0.3" stop-color="#C9A24A"/><stop offset="0.5" stop-color="#F6E7B8"/><stop offset="0.68" stop-color="#C9A24A"/><stop offset="1" stop-color="#8A6516"/></linearGradient><path id="cscrleather" d="M 200,32.014999999999986 A 167.985,167.985 0 1 1 199.99,32.014999999999986"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="11.00" letter-spacing="8.973" fill="url(#gkfoil)"><textPath href="#cscrleather">LEATHER ✦ TERRA ✦ HIDE ✦ PATIUN ✦ LEATHER ✦ TERRA ✦ HIDE ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="160" fill="none" stroke="url(#gkfoil)" stroke-width="1.2" /><path d="M200.00,356.00 L64.90,122.00 L335.10,122.00Z" fill="none" stroke="#14A39A" stroke-width="1.6" stroke-linejoin="round" /><line x1="162.2" y1="290.5" x2="237.8" y2="290.5" stroke="#14A39A" stroke-width="1.2"/><circle cx="200" cy="200" r="78.0" fill="none" stroke="url(#gkfoil)" stroke-width="1" /><path d="M164.2,145.2 Q188.5,155.0 200.0,155.0 Q211.5,155.0 223.7,150.1 Q235.8,145.2 238.7,159.6 Q241.6,174.0 248.2,181.2 Q254.8,188.5 249.9,201.4 Q245.0,214.4 247.9,228.0 Q250.8,241.6 236.9,248.2 Q223.1,254.8 211.5,250.5 Q200.0,246.2 188.5,250.5 Q176.9,254.8 163.1,248.2 Q149.2,241.6 152.1,228.0 Q155.0,214.4 150.1,201.4 Q145.2,188.5 151.8,181.2 Q158.4,174.0 161.3,159.6 Z" fill="none" stroke="#F4F2ED" stroke-width="1.4" stroke-linejoin="round"/><path d="M174.2,160.5 Q191.7,167.6 200.0,167.6 Q208.3,167.6 217.0,164.1 Q225.8,160.5 227.8,170.9 Q229.9,181.3 234.7,186.5 Q239.5,191.7 235.9,201.0 Q232.4,210.4 234.5,220.2 Q236.6,229.9 226.6,234.7 Q216.6,239.5 208.3,236.4 Q200.0,233.2 191.7,236.4 Q183.4,239.5 173.4,234.7 Q163.4,229.9 165.5,220.2 Q167.6,210.4 164.1,201.0 Q160.5,191.7 165.3,186.5 Q170.1,181.3 172.2,170.9 Z" fill="none" stroke="#F4F2ED" stroke-width="1" stroke-dasharray="4 3"/><circle cx="103.0" cy="144.0" r="15" fill="#0E1522" stroke="url(#gkfoil)" stroke-width="1"/><text x="95.97" y="149.52" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="16" fill="#F4F2ED" >🜃</text><circle cx="297.0" cy="144.0" r="15" fill="#0E1522" stroke="url(#gkfoil)" stroke-width="1"/><g transform="translate(289.21,137.50) scale(0.1024) translate(-20,-33)"><g fill="none" stroke="#14A39A" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#14A39A"/></g><polygon points="200.00,3.00 201.56,11.44 210.00,13.00 201.56,14.56 200.00,23.00 198.44,14.56 190.00,13.00 198.44,11.44" fill="url(#gkfoil)"/><polygon points="387.00,190.00 388.56,198.44 397.00,200.00 388.56,201.56 387.00,210.00 385.44,201.56 377.00,200.00 385.44,198.44" fill="url(#gkfoil)"/><polygon points="200.00,377.00 201.56,385.44 210.00,387.00 201.56,388.56 200.00,397.00 198.44,388.56 190.00,387.00 198.44,385.44" fill="url(#gkfoil)"/><polygon points="13.00,190.00 14.56,198.44 23.00,200.00 14.56,201.56 13.00,210.00 11.44,201.56 3.00,200.00 11.44,198.44" fill="url(#gkfoil)"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Print craft seal"><circle cx="200" cy="200" r="190" fill="none" stroke="url(#gkfoil)" stroke-width="2" /><circle cx="200" cy="200" r="184" fill="none" stroke="url(#gkfoil)" stroke-width="0.6" /><defs><linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="400"><stop offset="0" stop-color="#8A6516"/><stop offset="0.3" stop-color="#C9A24A"/><stop offset="0.5" stop-color="#F6E7B8"/><stop offset="0.68" stop-color="#C9A24A"/><stop offset="1" stop-color="#8A6516"/></linearGradient><path id="cscrprint" d="M 200,32.014999999999986 A 167.985,167.985 0 1 1 199.99,32.014999999999986"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="11.00" letter-spacing="9.692" fill="url(#gkfoil)"><textPath href="#cscrprint">PRINT ✦ IGNIS ✦ LAYER ✦ PATIUN ✦ PRINT ✦ IGNIS ✦ LAYER ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="160" fill="none" stroke="url(#gkfoil)" stroke-width="1.2" /><path d="M200.00,44.00 L335.10,278.00 L64.90,278.00Z" fill="none" stroke="#14A39A" stroke-width="1.6" stroke-linejoin="round" /><circle cx="200" cy="200" r="78.0" fill="none" stroke="url(#gkfoil)" stroke-width="1" /><path d="M200.0,148.1 L245.2,174.0 L200.0,200.0 L154.8,174.0Z" fill="none" stroke="#F4F2ED" stroke-width="1.3" stroke-linejoin="round"/><path d="M154.8,174.0 L154.8,226.0 L200.0,251.9 L245.2,226.0 L245.2,174.0 M200.0,200.0 L200.0,251.9" fill="none" stroke="#F4F2ED" stroke-width="1.3" stroke-linejoin="round"/><path d="M154.8,184.4 L200.0,210.4 L245.2,184.4" fill="none" stroke="#F4F2ED" stroke-width=".6"/><path d="M154.8,194.8 L200.0,220.8 L245.2,194.8" fill="none" stroke="#F4F2ED" stroke-width=".6"/><path d="M154.8,205.2 L200.0,231.2 L245.2,205.2" fill="none" stroke="#F4F2ED" stroke-width=".6"/><path d="M154.8,215.6 L200.0,241.6 L245.2,215.6" fill="none" stroke="#F4F2ED" stroke-width=".6"/><circle cx="297.0" cy="256.0" r="15" fill="#0E1522" stroke="url(#gkfoil)" stroke-width="1"/><text x="289.95" y="261.92" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="16" fill="#F4F2ED" >🜂</text><circle cx="103.0" cy="256.0" r="15" fill="#0E1522" stroke="url(#gkfoil)" stroke-width="1"/><g transform="translate(95.22,249.50) scale(0.1024) translate(-20,-33)"><g fill="none" stroke="#14A39A" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#14A39A"/></g><polygon points="200.00,3.00 201.56,11.44 210.00,13.00 201.56,14.56 200.00,23.00 198.44,14.56 190.00,13.00 198.44,11.44" fill="url(#gkfoil)"/><polygon points="387.00,190.00 388.56,198.44 397.00,200.00 388.56,201.56 387.00,210.00 385.44,201.56 377.00,200.00 385.44,198.44" fill="url(#gkfoil)"/><polygon points="200.00,377.00 201.56,385.44 210.00,387.00 201.56,388.56 200.00,397.00 198.44,388.56 190.00,387.00 198.44,385.44" fill="url(#gkfoil)"/><polygon points="13.00,190.00 14.56,198.44 23.00,200.00 14.56,201.56 13.00,210.00 11.44,201.56 3.00,200.00 11.44,198.44" fill="url(#gkfoil)"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Kepler nest seal"><circle cx="200" cy="200" r="194" fill="none" stroke="url(#gkfoil)" stroke-width="2" /><circle cx="200" cy="200" r="178" fill="none" stroke="url(#gkfoil)" stroke-width="0.8" /><defs><linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="400"><stop offset="0" stop-color="#8A6516"/><stop offset="0.3" stop-color="#C9A24A"/><stop offset="0.5" stop-color="#F6E7B8"/><stop offset="0.68" stop-color="#C9A24A"/><stop offset="1" stop-color="#8A6516"/></linearGradient><path id="kpkp" d="M 200,17.4675 A 182.5325,182.5325 0 1 1 199.99,17.4675"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="9.50" letter-spacing="5.187" fill="url(#gkfoil)"><textPath href="#kpkp">HARMONICES MUNDI ✦ PATIUN ✦ DICE · LEATHER · PRINT ✦ HARMONICES MUNDI ✦ PATIUN ✦ DICE · LEATHER · PRINT ✦ </textPath></text><circle cx="200" cy="200" r="176" fill="none" stroke="url(#gkfoil)" stroke-width="0.5" /><path d="M200.00,24.00 L337.60,90.27 L371.59,239.16 L276.36,358.57 L123.64,358.57 L28.41,239.16 L62.40,90.27Z" fill="none" stroke="url(#gkfoil)" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="24.00" r="3.2" fill="#F4F2ED"/><circle cx="337.60" cy="90.27" r="3.2" fill="#F4F2ED"/><circle cx="371.59" cy="239.16" r="3.2" fill="#F4F2ED"/><circle cx="276.36" cy="358.57" r="3.2" fill="#F4F2ED"/><circle cx="123.64" cy="358.57" r="3.2" fill="#F4F2ED"/><circle cx="28.41" cy="239.16" r="3.2" fill="#F4F2ED"/><circle cx="62.40" cy="90.27" r="3.2" fill="#F4F2ED"/><circle cx="200" cy="200" r="158.57052075082578" fill="none" stroke="#14A39A" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,41.43 L337.33,120.71 L337.33,279.29 L200.00,358.57 L62.67,279.29 L62.67,120.71Z" fill="none" stroke="#14A39A" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="41.43" r="2.4" fill="#F4F2ED"/><circle cx="337.33" cy="120.71" r="2.4" fill="#F4F2ED"/><circle cx="337.33" cy="279.29" r="2.4" fill="#F4F2ED"/><circle cx="200.00" cy="358.57" r="2.4" fill="#F4F2ED"/><circle cx="62.67" cy="279.29" r="2.4" fill="#F4F2ED"/><circle cx="62.67" cy="120.71" r="2.4" fill="#F4F2ED"/><circle cx="200" cy="200" r="137.32609926154262" fill="none" stroke="url(#gkfoil)" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,62.67 L330.60,157.56 L280.72,311.10 L119.28,311.10 L69.40,157.56Z" fill="none" stroke="url(#gkfoil)" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="62.67" r="2.4" fill="#F4F2ED"/><circle cx="330.60" cy="157.56" r="2.4" fill="#F4F2ED"/><circle cx="280.72" cy="311.10" r="2.4" fill="#F4F2ED"/><circle cx="119.28" cy="311.10" r="2.4" fill="#F4F2ED"/><circle cx="69.40" cy="157.56" r="2.4" fill="#F4F2ED"/><circle cx="200" cy="200" r="111.0991480738089" fill="none" stroke="#14A39A" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,88.90 L311.10,200.00 L200.00,311.10 L88.90,200.00Z" fill="none" stroke="#14A39A" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="88.90" r="2.4" fill="#F4F2ED"/><circle cx="311.10" cy="200.00" r="2.4" fill="#F4F2ED"/><circle cx="200.00" cy="311.10" r="2.4" fill="#F4F2ED"/><circle cx="88.90" cy="200.00" r="2.4" fill="#F4F2ED"/><circle cx="200" cy="200" r="78.55896098703865" fill="none" stroke="url(#gkfoil)" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,121.44 L268.03,239.28 L131.97,239.28Z" fill="none" stroke="url(#gkfoil)" stroke-width="1.3" stroke-linejoin="round" /><circle cx="200.00" cy="121.44" r="2.4" fill="#F4F2ED"/><circle cx="268.03" cy="239.28" r="2.4" fill="#F4F2ED"/><circle cx="131.97" cy="239.28" r="2.4" fill="#F4F2ED"/><circle cx="200" cy="200" r="39.27948049351933" fill="none" stroke="#14A39A" stroke-width="1.2" /><g transform="translate(177.67,181.34) scale(0.2938) translate(-20,-33)"><g fill="none" stroke="#14A39A" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#14A39A"/></g><polygon points="200.00,-4.00 201.56,4.44 210.00,6.00 201.56,7.56 200.00,16.00 198.44,7.56 190.00,6.00 198.44,4.44" fill="url(#gkfoil)"/><polygon points="394.00,190.00 395.56,198.44 404.00,200.00 395.56,201.56 394.00,210.00 392.44,201.56 384.00,200.00 392.44,198.44" fill="url(#gkfoil)"/><polygon points="200.00,384.00 201.56,392.44 210.00,394.00 201.56,395.56 200.00,404.00 198.44,395.56 190.00,394.00 198.44,392.44" fill="url(#gkfoil)"/><polygon points="6.00,190.00 7.56,198.44 16.00,200.00 7.56,201.56 6.00,210.00 4.44,201.56 -4.00,200.00 4.44,198.44" fill="url(#gkfoil)"/></svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Layered seal"><circle cx="200" cy="200" r="192" fill="none" stroke="#14A39A" stroke-width="2" /><circle cx="200" cy="200" r="170" fill="none" stroke="#14A39A" stroke-width="0.8" /><text x="240.34" y="30.28" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♈︎</text><text x="321.90" y="77.12" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♉︎</text><text x="368.88" y="158.05" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♊︎</text><text x="368.18" y="251.89" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♋︎</text><text x="322.60" y="331.42" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♌︎</text><text x="240.76" y="378.40" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♍︎</text><text x="146.57" y="379.73" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♎︎</text><text x="65.29" y="331.63" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♏︎</text><text x="19.64" y="251.75" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♐︎</text><text x="18.38" y="156.79" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♑︎</text><text x="65.43" y="76.91" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♒︎</text><text x="147.41" y="30.49" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#14A39A" >♓︎</text><line x1="200.00" y1="30.00" x2="200.00" y2="8.00" stroke="#14A39A" stroke-width="0.8"/><line x1="285.00" y1="52.78" x2="296.00" y2="33.72" stroke="#14A39A" stroke-width="0.8"/><line x1="347.22" y1="115.00" x2="366.28" y2="104.00" stroke="#14A39A" stroke-width="0.8"/><line x1="370.00" y1="200.00" x2="392.00" y2="200.00" stroke="#14A39A" stroke-width="0.8"/><line x1="347.22" y1="285.00" x2="366.28" y2="296.00" stroke="#14A39A" stroke-width="0.8"/><line x1="285.00" y1="347.22" x2="296.00" y2="366.28" stroke="#14A39A" stroke-width="0.8"/><line x1="200.00" y1="370.00" x2="200.00" y2="392.00" stroke="#14A39A" stroke-width="0.8"/><line x1="115.00" y1="347.22" x2="104.00" y2="366.28" stroke="#14A39A" stroke-width="0.8"/><line x1="52.78" y1="285.00" x2="33.72" y2="296.00" stroke="#14A39A" stroke-width="0.8"/><line x1="30.00" y1="200.00" x2="8.00" y2="200.00" stroke="#14A39A" stroke-width="0.8"/><line x1="52.78" y1="115.00" x2="33.72" y2="104.00" stroke="#14A39A" stroke-width="0.8"/><line x1="115.00" y1="52.78" x2="104.00" y2="33.72" stroke="#14A39A" stroke-width="0.8"/><defs><linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="400"><stop offset="0" stop-color="#8A6516"/><stop offset="0.3" stop-color="#C9A24A"/><stop offset="0.5" stop-color="#F6E7B8"/><stop offset="0.68" stop-color="#C9A24A"/><stop offset="1" stop-color="#8A6516"/></linearGradient><path id="ltf" d="M 200,49.379999999999995 A 150.62,150.62 0 1 1 199.99,49.379999999999995"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="12.00" letter-spacing="10.483" fill="url(#gkfoil)"><textPath href="#ltf">PATIUN ✦ PATIUN ✦ PATIUN ✦ PATIUN ✦ PATIUN ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="140" fill="none" stroke="url(#gkfoil)" stroke-width="1" /><path d="M200.0,60.0 L340.0,200.0 L200.0,340.0 L60.0,200.0Z" fill="none" stroke="#14A39A" stroke-width="1.2"/><path d="M299.0,101.0 L299.0,299.0 L101.0,299.0 L101.0,101.0Z" fill="none" stroke="#14A39A" stroke-width="1.2"/><circle cx="200" cy="200" r="99" fill="none" stroke="url(#gkfoil)" stroke-width="0.9" /><text x="238.56" y="96.53" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="url(#gkfoil)" >🜂</text><text x="302.42" y="250.33" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="url(#gkfoil)" >🜄</text><text x="148.24" y="314.57" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="url(#gkfoil)" >🜁</text><text x="84.38" y="160.02" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="url(#gkfoil)" >🜃</text><path d="M200.0,101.0 L285.7,249.5 L114.3,249.5Z" fill="none" stroke="url(#gkfoil)" stroke-width=".9"/><path d="M200.0,299.0 L114.3,150.5 L285.7,150.5Z" fill="none" stroke="url(#gkfoil)" stroke-width=".9"/><circle cx="200" cy="200" r="48" fill="none" stroke="#14A39A" stroke-width="1.4" /><g transform="translate(173.67,178.00) scale(0.3465) translate(-20,-33)"><g fill="none" stroke="#14A39A" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#14A39A"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Dice craft seal"><circle cx="200" cy="200" r="190" fill="none" stroke="#8A6516" stroke-width="2" /><circle cx="200" cy="200" r="184" fill="none" stroke="#8A6516" stroke-width="0.6" /><defs><path id="cscrdice" d="M 200,30.53 A 169.47,169.47 0 1 1 199.99,30.53"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="11.00" letter-spacing="10.745" fill="#8A6516"><textPath href="#cscrdice">DICE ✦ AQUA ✦ RESIN ✦ PATIUN ✦ DICE ✦ AQUA ✦ RESIN ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="160" fill="none" stroke="#8A6516" stroke-width="1.2" /><path d="M200.00,356.00 L64.90,122.00 L335.10,122.00Z" fill="none" stroke="#0F8C84" stroke-width="1.6" stroke-linejoin="round" /><circle cx="200" cy="200" r="78.0" fill="none" stroke="#8A6516" stroke-width="1" /><path d="M200.0,142.3 L250.0,171.1 L250.0,228.9 L200.0,257.7 L150.0,228.9 L150.0,171.1Z" fill="none" stroke="#131C2B" stroke-width="1.4" stroke-linejoin="round"/><path d="M200.0,228.9 L175.0,185.6 L225.0,185.6Z" fill="none" stroke="#131C2B" stroke-width="1.2" stroke-linejoin="round"/><line x1="200.0" y1="142.3" x2="175.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><line x1="200.0" y1="142.3" x2="225.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><line x1="250.0" y1="171.1" x2="225.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><line x1="250.0" y1="228.9" x2="225.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><line x1="250.0" y1="228.9" x2="200.0" y2="228.9" stroke="#131C2B" stroke-width=".9"/><line x1="200.0" y1="257.7" x2="200.0" y2="228.9" stroke="#131C2B" stroke-width=".9"/><line x1="150.0" y1="228.9" x2="200.0" y2="228.9" stroke="#131C2B" stroke-width=".9"/><line x1="150.0" y1="228.9" x2="175.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><line x1="150.0" y1="171.1" x2="175.0" y2="185.6" stroke="#131C2B" stroke-width=".9"/><text x="200.0" y="206.9" font-family="Cinzel,serif" font-weight="700" font-size="18.5" fill="#131C2B" text-anchor="middle" dominant-baseline="middle">20</text><circle cx="103.0" cy="144.0" r="15" fill="#F4F2ED" stroke="#8A6516" stroke-width="1"/><text x="95.97" y="149.52" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="16" fill="#131C2B" >🜄</text><circle cx="297.0" cy="144.0" r="15" fill="#F4F2ED" stroke="#8A6516" stroke-width="1"/><g transform="translate(289.21,137.50) scale(0.1024) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g><polygon points="200.00,3.00 201.56,11.44 210.00,13.00 201.56,14.56 200.00,23.00 198.44,14.56 190.00,13.00 198.44,11.44" fill="#8A6516"/><polygon points="387.00,190.00 388.56,198.44 397.00,200.00 388.56,201.56 387.00,210.00 385.44,201.56 377.00,200.00 385.44,198.44" fill="#8A6516"/><polygon points="200.00,377.00 201.56,385.44 210.00,387.00 201.56,388.56 200.00,397.00 198.44,388.56 190.00,387.00 198.44,385.44" fill="#8A6516"/><polygon points="13.00,190.00 14.56,198.44 23.00,200.00 14.56,201.56 13.00,210.00 11.44,201.56 3.00,200.00 11.44,198.44" fill="#8A6516"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Leather craft seal"><circle cx="200" cy="200" r="190" fill="none" stroke="#8A6516" stroke-width="2" /><circle cx="200" cy="200" r="184" fill="none" stroke="#8A6516" stroke-width="0.6" /><defs><path id="cscrleather" d="M 200,32.014999999999986 A 167.985,167.985 0 1 1 199.99,32.014999999999986"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="11.00" letter-spacing="8.973" fill="#8A6516"><textPath href="#cscrleather">LEATHER ✦ TERRA ✦ HIDE ✦ PATIUN ✦ LEATHER ✦ TERRA ✦ HIDE ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="160" fill="none" stroke="#8A6516" stroke-width="1.2" /><path d="M200.00,356.00 L64.90,122.00 L335.10,122.00Z" fill="none" stroke="#0F8C84" stroke-width="1.6" stroke-linejoin="round" /><line x1="162.2" y1="290.5" x2="237.8" y2="290.5" stroke="#0F8C84" stroke-width="1.2"/><circle cx="200" cy="200" r="78.0" fill="none" stroke="#8A6516" stroke-width="1" /><path d="M164.2,145.2 Q188.5,155.0 200.0,155.0 Q211.5,155.0 223.7,150.1 Q235.8,145.2 238.7,159.6 Q241.6,174.0 248.2,181.2 Q254.8,188.5 249.9,201.4 Q245.0,214.4 247.9,228.0 Q250.8,241.6 236.9,248.2 Q223.1,254.8 211.5,250.5 Q200.0,246.2 188.5,250.5 Q176.9,254.8 163.1,248.2 Q149.2,241.6 152.1,228.0 Q155.0,214.4 150.1,201.4 Q145.2,188.5 151.8,181.2 Q158.4,174.0 161.3,159.6 Z" fill="none" stroke="#131C2B" stroke-width="1.4" stroke-linejoin="round"/><path d="M174.2,160.5 Q191.7,167.6 200.0,167.6 Q208.3,167.6 217.0,164.1 Q225.8,160.5 227.8,170.9 Q229.9,181.3 234.7,186.5 Q239.5,191.7 235.9,201.0 Q232.4,210.4 234.5,220.2 Q236.6,229.9 226.6,234.7 Q216.6,239.5 208.3,236.4 Q200.0,233.2 191.7,236.4 Q183.4,239.5 173.4,234.7 Q163.4,229.9 165.5,220.2 Q167.6,210.4 164.1,201.0 Q160.5,191.7 165.3,186.5 Q170.1,181.3 172.2,170.9 Z" fill="none" stroke="#131C2B" stroke-width="1" stroke-dasharray="4 3"/><circle cx="103.0" cy="144.0" r="15" fill="#F4F2ED" stroke="#8A6516" stroke-width="1"/><text x="95.97" y="149.52" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="16" fill="#131C2B" >🜃</text><circle cx="297.0" cy="144.0" r="15" fill="#F4F2ED" stroke="#8A6516" stroke-width="1"/><g transform="translate(289.21,137.50) scale(0.1024) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g><polygon points="200.00,3.00 201.56,11.44 210.00,13.00 201.56,14.56 200.00,23.00 198.44,14.56 190.00,13.00 198.44,11.44" fill="#8A6516"/><polygon points="387.00,190.00 388.56,198.44 397.00,200.00 388.56,201.56 387.00,210.00 385.44,201.56 377.00,200.00 385.44,198.44" fill="#8A6516"/><polygon points="200.00,377.00 201.56,385.44 210.00,387.00 201.56,388.56 200.00,397.00 198.44,388.56 190.00,387.00 198.44,385.44" fill="#8A6516"/><polygon points="13.00,190.00 14.56,198.44 23.00,200.00 14.56,201.56 13.00,210.00 11.44,201.56 3.00,200.00 11.44,198.44" fill="#8A6516"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Print craft seal"><circle cx="200" cy="200" r="190" fill="none" stroke="#8A6516" stroke-width="2" /><circle cx="200" cy="200" r="184" fill="none" stroke="#8A6516" stroke-width="0.6" /><defs><path id="cscrprint" d="M 200,32.014999999999986 A 167.985,167.985 0 1 1 199.99,32.014999999999986"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="11.00" letter-spacing="9.692" fill="#8A6516"><textPath href="#cscrprint">PRINT ✦ IGNIS ✦ LAYER ✦ PATIUN ✦ PRINT ✦ IGNIS ✦ LAYER ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="160" fill="none" stroke="#8A6516" stroke-width="1.2" /><path d="M200.00,44.00 L335.10,278.00 L64.90,278.00Z" fill="none" stroke="#0F8C84" stroke-width="1.6" stroke-linejoin="round" /><circle cx="200" cy="200" r="78.0" fill="none" stroke="#8A6516" stroke-width="1" /><path d="M200.0,148.1 L245.2,174.0 L200.0,200.0 L154.8,174.0Z" fill="none" stroke="#131C2B" stroke-width="1.3" stroke-linejoin="round"/><path d="M154.8,174.0 L154.8,226.0 L200.0,251.9 L245.2,226.0 L245.2,174.0 M200.0,200.0 L200.0,251.9" fill="none" stroke="#131C2B" stroke-width="1.3" stroke-linejoin="round"/><path d="M154.8,184.4 L200.0,210.4 L245.2,184.4" fill="none" stroke="#131C2B" stroke-width=".6"/><path d="M154.8,194.8 L200.0,220.8 L245.2,194.8" fill="none" stroke="#131C2B" stroke-width=".6"/><path d="M154.8,205.2 L200.0,231.2 L245.2,205.2" fill="none" stroke="#131C2B" stroke-width=".6"/><path d="M154.8,215.6 L200.0,241.6 L245.2,215.6" fill="none" stroke="#131C2B" stroke-width=".6"/><circle cx="297.0" cy="256.0" r="15" fill="#F4F2ED" stroke="#8A6516" stroke-width="1"/><text x="289.95" y="261.92" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="16" fill="#131C2B" >🜂</text><circle cx="103.0" cy="256.0" r="15" fill="#F4F2ED" stroke="#8A6516" stroke-width="1"/><g transform="translate(95.22,249.50) scale(0.1024) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g><polygon points="200.00,3.00 201.56,11.44 210.00,13.00 201.56,14.56 200.00,23.00 198.44,14.56 190.00,13.00 198.44,11.44" fill="#8A6516"/><polygon points="387.00,190.00 388.56,198.44 397.00,200.00 388.56,201.56 387.00,210.00 385.44,201.56 377.00,200.00 385.44,198.44" fill="#8A6516"/><polygon points="200.00,377.00 201.56,385.44 210.00,387.00 201.56,388.56 200.00,397.00 198.44,388.56 190.00,387.00 198.44,385.44" fill="#8A6516"/><polygon points="13.00,190.00 14.56,198.44 23.00,200.00 14.56,201.56 13.00,210.00 11.44,201.56 3.00,200.00 11.44,198.44" fill="#8A6516"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Kepler nest seal"><circle cx="200" cy="200" r="194" fill="none" stroke="#8A6516" stroke-width="2" /><circle cx="200" cy="200" r="178" fill="none" stroke="#8A6516" stroke-width="0.8" /><defs><path id="kpkp" d="M 200,17.4675 A 182.5325,182.5325 0 1 1 199.99,17.4675"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="9.50" letter-spacing="5.187" fill="#8A6516"><textPath href="#kpkp">HARMONICES MUNDI ✦ PATIUN ✦ DICE · LEATHER · PRINT ✦ HARMONICES MUNDI ✦ PATIUN ✦ DICE · LEATHER · PRINT ✦ </textPath></text><circle cx="200" cy="200" r="176" fill="none" stroke="#8A6516" stroke-width="0.5" /><path d="M200.00,24.00 L337.60,90.27 L371.59,239.16 L276.36,358.57 L123.64,358.57 L28.41,239.16 L62.40,90.27Z" fill="none" stroke="#8A6516" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="24.00" r="3.2" fill="#131C2B"/><circle cx="337.60" cy="90.27" r="3.2" fill="#131C2B"/><circle cx="371.59" cy="239.16" r="3.2" fill="#131C2B"/><circle cx="276.36" cy="358.57" r="3.2" fill="#131C2B"/><circle cx="123.64" cy="358.57" r="3.2" fill="#131C2B"/><circle cx="28.41" cy="239.16" r="3.2" fill="#131C2B"/><circle cx="62.40" cy="90.27" r="3.2" fill="#131C2B"/><circle cx="200" cy="200" r="158.57052075082578" fill="none" stroke="#0F8C84" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,41.43 L337.33,120.71 L337.33,279.29 L200.00,358.57 L62.67,279.29 L62.67,120.71Z" fill="none" stroke="#0F8C84" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="41.43" r="2.4" fill="#131C2B"/><circle cx="337.33" cy="120.71" r="2.4" fill="#131C2B"/><circle cx="337.33" cy="279.29" r="2.4" fill="#131C2B"/><circle cx="200.00" cy="358.57" r="2.4" fill="#131C2B"/><circle cx="62.67" cy="279.29" r="2.4" fill="#131C2B"/><circle cx="62.67" cy="120.71" r="2.4" fill="#131C2B"/><circle cx="200" cy="200" r="137.32609926154262" fill="none" stroke="#8A6516" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,62.67 L330.60,157.56 L280.72,311.10 L119.28,311.10 L69.40,157.56Z" fill="none" stroke="#8A6516" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="62.67" r="2.4" fill="#131C2B"/><circle cx="330.60" cy="157.56" r="2.4" fill="#131C2B"/><circle cx="280.72" cy="311.10" r="2.4" fill="#131C2B"/><circle cx="119.28" cy="311.10" r="2.4" fill="#131C2B"/><circle cx="69.40" cy="157.56" r="2.4" fill="#131C2B"/><circle cx="200" cy="200" r="111.0991480738089" fill="none" stroke="#0F8C84" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,88.90 L311.10,200.00 L200.00,311.10 L88.90,200.00Z" fill="none" stroke="#0F8C84" stroke-width="1" stroke-linejoin="round" /><circle cx="200.00" cy="88.90" r="2.4" fill="#131C2B"/><circle cx="311.10" cy="200.00" r="2.4" fill="#131C2B"/><circle cx="200.00" cy="311.10" r="2.4" fill="#131C2B"/><circle cx="88.90" cy="200.00" r="2.4" fill="#131C2B"/><circle cx="200" cy="200" r="78.55896098703865" fill="none" stroke="#8A6516" stroke-width="0.5" stroke-dasharray="1 3"/><path d="M200.00,121.44 L268.03,239.28 L131.97,239.28Z" fill="none" stroke="#8A6516" stroke-width="1.3" stroke-linejoin="round" /><circle cx="200.00" cy="121.44" r="2.4" fill="#131C2B"/><circle cx="268.03" cy="239.28" r="2.4" fill="#131C2B"/><circle cx="131.97" cy="239.28" r="2.4" fill="#131C2B"/><circle cx="200" cy="200" r="39.27948049351933" fill="none" stroke="#0F8C84" stroke-width="1.2" /><g transform="translate(177.67,181.34) scale(0.2938) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g><polygon points="200.00,-4.00 201.56,4.44 210.00,6.00 201.56,7.56 200.00,16.00 198.44,7.56 190.00,6.00 198.44,4.44" fill="#8A6516"/><polygon points="394.00,190.00 395.56,198.44 404.00,200.00 395.56,201.56 394.00,210.00 392.44,201.56 384.00,200.00 392.44,198.44" fill="#8A6516"/><polygon points="200.00,384.00 201.56,392.44 210.00,394.00 201.56,395.56 200.00,404.00 198.44,395.56 190.00,394.00 198.44,392.44" fill="#8A6516"/><polygon points="6.00,190.00 7.56,198.44 16.00,200.00 7.56,201.56 6.00,210.00 4.44,201.56 -4.00,200.00 4.44,198.44" fill="#8A6516"/></svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Layered seal"><circle cx="200" cy="200" r="192" fill="none" stroke="#0F8C84" stroke-width="2" /><circle cx="200" cy="200" r="170" fill="none" stroke="#0F8C84" stroke-width="0.8" /><text x="240.34" y="30.28" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♈︎</text><text x="321.90" y="77.12" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♉︎</text><text x="368.88" y="158.05" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♊︎</text><text x="368.18" y="251.89" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♋︎</text><text x="322.60" y="331.42" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♌︎</text><text x="240.76" y="378.40" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♍︎</text><text x="146.57" y="379.73" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♎︎</text><text x="65.29" y="331.63" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♏︎</text><text x="19.64" y="251.75" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♐︎</text><text x="18.38" y="156.79" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♑︎</text><text x="65.43" y="76.91" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♒︎</text><text x="147.41" y="30.49" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="14" fill="#0F8C84" >♓︎</text><line x1="200.00" y1="30.00" x2="200.00" y2="8.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="285.00" y1="52.78" x2="296.00" y2="33.72" stroke="#0F8C84" stroke-width="0.8"/><line x1="347.22" y1="115.00" x2="366.28" y2="104.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="370.00" y1="200.00" x2="392.00" y2="200.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="347.22" y1="285.00" x2="366.28" y2="296.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="285.00" y1="347.22" x2="296.00" y2="366.28" stroke="#0F8C84" stroke-width="0.8"/><line x1="200.00" y1="370.00" x2="200.00" y2="392.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="115.00" y1="347.22" x2="104.00" y2="366.28" stroke="#0F8C84" stroke-width="0.8"/><line x1="52.78" y1="285.00" x2="33.72" y2="296.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="30.00" y1="200.00" x2="8.00" y2="200.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="52.78" y1="115.00" x2="33.72" y2="104.00" stroke="#0F8C84" stroke-width="0.8"/><line x1="115.00" y1="52.78" x2="104.00" y2="33.72" stroke="#0F8C84" stroke-width="0.8"/><defs><path id="ltf" d="M 200,49.379999999999995 A 150.62,150.62 0 1 1 199.99,49.379999999999995"/></defs><text font-family="Cinzel,serif" font-weight="700" font-size="12.00" letter-spacing="10.483" fill="#8A6516"><textPath href="#ltf">PATIUN ✦ PATIUN ✦ PATIUN ✦ PATIUN ✦ PATIUN ✦ PATIUN ✦ </textPath></text><circle cx="200" cy="200" r="140" fill="none" stroke="#8A6516" stroke-width="1" /><path d="M200.0,60.0 L340.0,200.0 L200.0,340.0 L60.0,200.0Z" fill="none" stroke="#0F8C84" stroke-width="1.2"/><path d="M299.0,101.0 L299.0,299.0 L101.0,299.0 L101.0,101.0Z" fill="none" stroke="#0F8C84" stroke-width="1.2"/><circle cx="200" cy="200" r="99" fill="none" stroke="#8A6516" stroke-width="0.9" /><text x="238.56" y="96.53" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="#8A6516" >🜂</text><text x="302.42" y="250.33" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="#8A6516" >🜄</text><text x="148.24" y="314.57" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="#8A6516" >🜁</text><text x="84.38" y="160.02" font-family="'Noto Sans Symbols','Segoe UI Symbol',serif" font-size="15" fill="#8A6516" >🜃</text><path d="M200.0,101.0 L285.7,249.5 L114.3,249.5Z" fill="none" stroke="#8A6516" stroke-width=".9"/><path d="M200.0,299.0 L114.3,150.5 L285.7,150.5Z" fill="none" stroke="#8A6516" stroke-width=".9"/><circle cx="200" cy="200" r="48" fill="none" stroke="#0F8C84" stroke-width="1.4" /><g transform="translate(173.67,178.00) scale(0.3465) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img" aria-label="Small astrolabe"><circle cx="50" cy="56" r="40" fill="#F4F2ED" stroke="#8A6516" stroke-width="3"/><circle cx="50" cy="8" r="5" fill="none" stroke="#8A6516" stroke-width="2.4" /><path d="M36,18 Q50,10 64,18" fill="none" stroke="#8A6516" stroke-width="2.4"/><line x1="50.00" y1="24.00" x2="50.00" y2="16.00" stroke="#8A6516" stroke-width="2.2"/><line x1="66.00" y1="28.29" x2="70.00" y2="21.36" stroke="#8A6516" stroke-width="2.2"/><line x1="77.71" y1="40.00" x2="84.64" y2="36.00" stroke="#8A6516" stroke-width="2.2"/><line x1="82.00" y1="56.00" x2="90.00" y2="56.00" stroke="#8A6516" stroke-width="2.2"/><line x1="77.71" y1="72.00" x2="84.64" y2="76.00" stroke="#8A6516" stroke-width="2.2"/><line x1="66.00" y1="83.71" x2="70.00" y2="90.64" stroke="#8A6516" stroke-width="2.2"/><line x1="50.00" y1="88.00" x2="50.00" y2="96.00" stroke="#8A6516" stroke-width="2.2"/><line x1="34.00" y1="83.71" x2="30.00" y2="90.64" stroke="#8A6516" stroke-width="2.2"/><line x1="22.29" y1="72.00" x2="15.36" y2="76.00" stroke="#8A6516" stroke-width="2.2"/><line x1="18.00" y1="56.00" x2="10.00" y2="56.00" stroke="#8A6516" stroke-width="2.2"/><line x1="22.29" y1="40.00" x2="15.36" y2="36.00" stroke="#8A6516" stroke-width="2.2"/><line x1="34.00" y1="28.29" x2="30.00" y2="21.36" stroke="#8A6516" stroke-width="2.2"/><circle cx="50" cy="48" r="22" fill="none" stroke="#8A6516" stroke-width="2.2" /><line x1="73.1" y1="28.4" x2="26.9" y2="83.6" stroke="#0F8C84" stroke-width="4" stroke-linecap="round"/><circle cx="50" cy="56" r="7" fill="#F4F2ED" stroke="#8A6516" stroke-width="2"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img" aria-label="Small layered seal"><circle cx="50" cy="50" r="46" fill="none" stroke="#0F8C84" stroke-width="3" /><circle cx="50" cy="50" r="36" fill="none" stroke="#8A6516" stroke-width="2" /><path d="M50.0,14.0 L86.0,50.0 L50.0,86.0 L14.0,50.0Z" fill="none" stroke="#0F8C84" stroke-width="2"/><path d="M75.5,24.5 L75.5,75.5 L24.5,75.5 L24.5,24.5Z" fill="none" stroke="#0F8C84" stroke-width="2"/><g transform="translate(35.64,38.00) scale(0.1890) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 793 B |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img" aria-label="Small tome medallion"><rect x="8" y="4" width="84" height="92" rx="5" fill="#1E2348"/><rect x="15" y="11" width="70" height="78" fill="none" stroke="#8A6516" stroke-width="2.6"/><path transform="translate(15,11) rotate(90)" d="M0,-14 A14,14 0 0 1 14,0" fill="none" stroke="#8A6516" stroke-width="2"/><path transform="translate(85,11) rotate(180)" d="M0,-14 A14,14 0 0 1 14,0" fill="none" stroke="#8A6516" stroke-width="2"/><path transform="translate(85,89) rotate(270)" d="M0,-14 A14,14 0 0 1 14,0" fill="none" stroke="#8A6516" stroke-width="2"/><path transform="translate(15,89) rotate(0)" d="M0,-14 A14,14 0 0 1 14,0" fill="none" stroke="#8A6516" stroke-width="2"/><circle cx="50" cy="50" r="20" fill="none" stroke="#8A6516" stroke-width="2.4" /><g transform="translate(39.22,41.00) scale(0.1417) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img" aria-label="Small transmutation seal"><circle cx="50" cy="50" r="47" fill="none" stroke="#8A6516" stroke-width="3" /><path d="M50.00,14.00 L85.10,58.01 L34.38,82.43 L21.85,27.55 L78.15,27.55 L65.62,82.43 L14.90,58.01 L50.00,14.00" fill="none" stroke="#0F8C84" stroke-width="2.2" stroke-linejoin="round"/><circle cx="50.00" cy="14.00" r="5.2" fill="#F4F2ED" stroke="#8A6516" stroke-width="1.8"/><circle cx="78.15" cy="27.55" r="5.2" fill="#F4F2ED" stroke="#8A6516" stroke-width="1.8"/><circle cx="85.10" cy="58.01" r="5.2" fill="#F4F2ED" stroke="#8A6516" stroke-width="1.8"/><circle cx="65.62" cy="82.43" r="5.2" fill="#F4F2ED" stroke="#8A6516" stroke-width="1.8"/><circle cx="34.38" cy="82.43" r="5.2" fill="#F4F2ED" stroke="#8A6516" stroke-width="1.8"/><circle cx="14.90" cy="58.01" r="5.2" fill="#F4F2ED" stroke="#8A6516" stroke-width="1.8"/><circle cx="21.85" cy="27.55" r="5.2" fill="#F4F2ED" stroke="#8A6516" stroke-width="1.8"/><g transform="translate(36.83,39.00) scale(0.1732) translate(-20,-33)"><g fill="none" stroke="#0F8C84" stroke-width="7.2" stroke-linejoin="round">
|
||||||
|
<path d="M31,46.75 A54.3,54.3 0 1 1 31,146.25 A53.4,53.4 0 0 0 31,46.75 Z"/><circle cx="134" cy="96.5" r="26.9"/></g><circle cx="134" cy="96.5" r="10.3" fill="#0F8C84"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 400"><line x1="565.0" y1="34.9" x2="578.5" y2="43.6" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="591.7" y1="55.4" x2="605.3" y2="72.1" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-dasharray="2 5" stroke-linecap="round"/><line x1="582.8" y1="56.8" x2="567.6" y2="96.5" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="559.7" y1="112.7" x2="557.7" y2="116.1" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="552.6" y1="132.8" x2="552.2" y2="140.5" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="553.1" y1="158.4" x2="561.7" y2="216.6" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="567.7" y1="233.2" x2="580.0" y2="253.6" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="588.7" y1="269.4" x2="603.1" y2="298.0" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="601.1" y1="312.7" x2="584.4" y2="330.7" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="570.7" y1="342.2" x2="535.2" y2="365.1" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="519.1" y1="367.2" x2="497.5" y2="360.0" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="490.2" y1="348.3" x2="490.0" y2="349.4" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><line x1="492.8" y1="331.6" x2="495.0" y2="319.5" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-dasharray="2 5" stroke-linecap="round"/><line x1="505.2" y1="307.9" x2="495.2" y2="311.0" stroke="#0F8C84" stroke-width="1.2" opacity="0.8" stroke-linecap="round"/><polygon points="557.4,23.0 558.5,28.9 564.4,30.0 558.5,31.1 557.4,37.0 556.3,31.1 550.4,30.0 556.3,28.9" fill="#6A2FE0"/><polygon points="586.1,41.4 587.2,47.3 593.1,48.4 587.2,49.6 586.1,55.4 584.9,49.6 579.1,48.4 584.9,47.3" fill="#6A2FE0"/><circle cx="611.0" cy="79.1" r="2.4" fill="#131C2B"/><circle cx="564.3" cy="104.9" r="2.4" fill="#131C2B"/><polygon points="553.1,110.8 555.2,121.7 566.1,123.8 555.2,125.9 553.1,136.8 551.0,125.9 540.1,123.8 551.0,121.7" fill="#8A6516"/><circle cx="551.8" cy="149.5" r="2.4" fill="#131C2B"/><polygon points="563.0,218.5 564.2,224.4 570.0,225.5 564.2,226.7 563.0,232.5 561.9,226.7 556.0,225.5 561.9,224.4" fill="#6A2FE0"/><circle cx="584.6" cy="261.3" r="2.4" fill="#131C2B"/><circle cx="607.2" cy="306.1" r="2.4" fill="#131C2B"/><circle cx="578.3" cy="337.3" r="2.4" fill="#131C2B"/><polygon points="527.7,363.0 528.8,368.9 534.7,370.0 528.8,371.1 527.7,377.0 526.6,371.1 520.7,370.0 526.6,368.9" fill="#6A2FE0"/><circle cx="489.0" cy="357.2" r="2.4" fill="#131C2B"/><circle cx="491.3" cy="340.5" r="2.4" fill="#131C2B"/><circle cx="503.8" cy="308.3" r="2.4" fill="#131C2B"/><polygon points="496.6,303.6 497.7,309.5 503.6,310.6 497.7,311.7 496.6,317.6 495.5,311.7 489.6,310.6 495.5,309.5" fill="#6A2FE0"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -18,17 +18,17 @@ const left = starPath({ x1: 540, x2: 76, y, steps: 8, seed: s, openEnds: false,
|
|||||||
const right = starPath({ x1: 660, x2: 1124, y, steps: 8, seed: s + 1, openEnds: false, dot: 2, star: 4.5 });
|
const right = starPath({ x1: 660, x2: 1124, y, steps: 8, seed: s + 1, openEnds: false, dot: 2, star: 4.5 });
|
||||||
---
|
---
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${W} 40`} aria-hidden="true" class={className} width="100%" style="display: block; width: 100%; height: auto">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${W} 40`} aria-hidden="true" class={className} width="100%" style="display: block; width: 100%; height: auto">
|
||||||
<line x1="40" y1={y} x2="566" y2={y} stroke="#C9A24A" stroke-width="1" />
|
<line x1="40" y1={y} x2="566" y2={y} style="stroke:var(--gk-gold)" stroke-width="1" />
|
||||||
<line x1="634" y1={y} x2="1160" y2={y} stroke="#C9A24A" stroke-width="1" />
|
<line x1="634" y1={y} x2="1160" y2={y} style="stroke:var(--gk-gold)" stroke-width="1" />
|
||||||
<polygon points={starPoints(600, y, 14)} fill="#C9A24A" />
|
<polygon points={starPoints(600, y, 14)} style="fill:var(--gk-gold)" />
|
||||||
<polygon points={starPoints(40, y, 5)} fill="#C9A24A" />
|
<polygon points={starPoints(40, y, 5)} style="fill:var(--gk-gold)" />
|
||||||
<polygon points={starPoints(1160, y, 5)} fill="#C9A24A" />
|
<polygon points={starPoints(1160, y, 5)} style="fill:var(--gk-gold)" />
|
||||||
{
|
{
|
||||||
[left, right].map((p) => (
|
[left, right].map((p) => (
|
||||||
<>
|
<>
|
||||||
{p.links.map((l) => <line {...l} stroke="#2BC2B7" stroke-width="1" stroke-dasharray="1.5 4" stroke-linecap="round" />)}
|
{p.links.map((l) => <line {...l} style="stroke:var(--gk-orn-link)" stroke-width="1" stroke-dasharray="1.5 4" stroke-linecap="round" />)}
|
||||||
{p.marks.map((m) =>
|
{p.marks.map((m) =>
|
||||||
m.kind === "dot" ? <circle cx={m.x} cy={m.y} r={m.r} fill="#F4F2ED" opacity="0.85" /> : <polygon points={starPoints(m.x, m.y, m.r)} fill="#9A70F5" />,
|
m.kind === "dot" ? <circle cx={m.x} cy={m.y} r={m.r} style="fill:var(--gk-orn-dot)" opacity="0.85" /> : <polygon points={starPoints(m.x, m.y, m.r)} style="fill:var(--gk-violet)" />,
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* alternating sides. Never more than two, and a section with a photo gets no
|
* alternating sides. Never more than two, and a section with a photo gets no
|
||||||
* seal: place this in the page background, not in a photo section.
|
* seal: place this in the page background, not in a photo section.
|
||||||
*/
|
*/
|
||||||
|
import Motif from "./Motif.astro";
|
||||||
import { pageSeals, seals, type PageFamily, type SealName } from "./seals.ts";
|
import { pageSeals, seals, type PageFamily, type SealName } from "./seals.ts";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -18,8 +19,8 @@ const first = seal ?? pageSeals[family];
|
|||||||
const other = second === false ? null : (second ?? first);
|
const other = second === false ? null : (second ?? first);
|
||||||
---
|
---
|
||||||
<div class="gk-edge-seals" aria-hidden="true">
|
<div class="gk-edge-seals" aria-hidden="true">
|
||||||
<img src={seals[first]} alt="" class="seal left" width="620" height="620" loading="lazy" decoding="async" />
|
<Motif src={seals[first]} alt="" class="seal left" width="620" height="620" loading="lazy" decoding="async" />
|
||||||
{other && <img src={seals[other]} alt="" class="seal right" width="560" height="560" loading="lazy" decoding="async" />}
|
{other && <Motif src={seals[other]} alt="" class="seal right" width="560" height="560" loading="lazy" decoding="async" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -38,7 +39,7 @@ const other = second === false ? null : (second ?? first);
|
|||||||
top: 760px;
|
top: 760px;
|
||||||
width: 620px;
|
width: 620px;
|
||||||
height: 620px;
|
height: 620px;
|
||||||
opacity: 0.07;
|
opacity: calc(0.07 * var(--gk-ghost));
|
||||||
transform: rotate(-10deg);
|
transform: rotate(-10deg);
|
||||||
}
|
}
|
||||||
.right {
|
.right {
|
||||||
@@ -46,7 +47,7 @@ const other = second === false ? null : (second ?? first);
|
|||||||
top: 64%;
|
top: 64%;
|
||||||
width: 560px;
|
width: 560px;
|
||||||
height: 560px;
|
height: 560px;
|
||||||
opacity: 0.06;
|
opacity: calc(0.06 * var(--gk-ghost));
|
||||||
transform: rotate(8deg);
|
transform: rotate(8deg);
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
|||||||
@@ -39,6 +39,6 @@ const common = { id, name, class: "gk-field", "aria-describedby": describedBy, "
|
|||||||
<style>
|
<style>
|
||||||
.error {
|
.error {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #f0857a;
|
color: var(--gk-danger);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ const corners = [
|
|||||||
{
|
{
|
||||||
corners.map((c) => (
|
corners.map((c) => (
|
||||||
<svg aria-hidden="true" class="gk-corner" width={size} height={size} viewBox="0 0 38 38" style={`${c.pos};transform:${c.t}`}>
|
<svg aria-hidden="true" class="gk-corner" width={size} height={size} viewBox="0 0 38 38" style={`${c.pos};transform:${c.t}`}>
|
||||||
<path d="M2 30 V8 a6 6 0 0 1 6 -6 H30" fill="none" stroke="#C9A24A" stroke-width="1.4" />
|
<path d="M2 30 V8 a6 6 0 0 1 6 -6 H30" fill="none" style="stroke:var(--gk-gold)" stroke-width="1.4" />
|
||||||
<path d="M7 30 V12 a5 5 0 0 1 5 -5 H30" fill="none" stroke="#C9A24A" stroke-width="0.6" opacity="0.7" />
|
<path d="M7 30 V12 a5 5 0 0 1 5 -5 H30" fill="none" style="stroke:var(--gk-gold)" stroke-width="0.6" opacity="0.7" />
|
||||||
<polygon points="2,36 2.6,33.6 5,33 2.6,32.4 2,30 1.4,32.4 -1,33 1.4,33.6" fill="#C9A24A" />
|
<polygon points="2,36 2.6,33.6 5,33 2.6,32.4 2,30 1.4,32.4 -1,33 1.4,33.6" style="fill:var(--gk-gold)" />
|
||||||
</svg>
|
</svg>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ import Star from "./Star.astro";
|
|||||||
.line {
|
.line {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(90deg, transparent, rgba(201, 162, 74, 0.7) 30%, rgba(201, 162, 74, 0.7) 70%, transparent);
|
background: linear-gradient(90deg, transparent, rgba(var(--gk-gold-rgb), 0.7) 30%, rgba(var(--gk-gold-rgb), 0.7) 70%, transparent);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* A motif image that follows the site's light or dark mode: the dark
|
||||||
|
* original, or its light recolor. Both are in the page and CSS shows one
|
||||||
|
* (base.css); the hidden one is lazy, so it never loads. Pass `foil` for a
|
||||||
|
* large standalone seal.
|
||||||
|
*/
|
||||||
|
import type { HTMLAttributes } from "astro/types";
|
||||||
|
import { motifSources } from "./seals.ts";
|
||||||
|
|
||||||
|
interface Props extends Omit<HTMLAttributes<"img">, "src"> {
|
||||||
|
src: string;
|
||||||
|
foil?: boolean;
|
||||||
|
}
|
||||||
|
const { src, foil, class: className, loading = "lazy", ...rest } = Astro.props;
|
||||||
|
const v = motifSources(src, { foil });
|
||||||
|
---
|
||||||
|
{
|
||||||
|
v.light === v.dark ? (
|
||||||
|
<img src={v.dark} class={className} loading={loading} {...rest} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<img src={v.dark} class:list={[className, "gk-motif-dark"]} loading={loading} {...rest} />
|
||||||
|
<img src={v.light} class:list={[className, "gk-motif-light"]} loading={loading} {...rest} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -35,8 +35,32 @@ const fullTitle = title === "Greg Kilmer" ? title : `${title} · Greg Kilmer`;
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="color-scheme" content="dark" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<meta name="theme-color" content="#0E1522" />
|
<meta name="theme-color" content="#0E1522" />
|
||||||
|
<!-- Before first paint: the visitor's saved mode, else the device's. Without script the site stays dark. -->
|
||||||
|
<script is:inline>
|
||||||
|
(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const device = matchMedia("(prefers-color-scheme: light)");
|
||||||
|
const saved = () => {
|
||||||
|
try {
|
||||||
|
const t = localStorage.getItem("gk-theme");
|
||||||
|
return t === "light" || t === "dark" ? t : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const apply = () => {
|
||||||
|
const t = saved() ?? (device.matches ? "light" : "dark");
|
||||||
|
root.dataset.theme = t;
|
||||||
|
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", t === "light" ? "#F4F2ED" : "#0E1522");
|
||||||
|
};
|
||||||
|
apply();
|
||||||
|
device.addEventListener("change", apply);
|
||||||
|
window.addEventListener("storage", apply);
|
||||||
|
window.gkApplyTheme = apply;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
@@ -64,10 +88,12 @@ const fullTitle = title === "Greg Kilmer" ? title : `${title} · Greg Kilmer`;
|
|||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
/* The page's nebula: violet from the top right, teal from the bottom left. */
|
/* The page's nebula: violet from the top right, teal from the bottom left.
|
||||||
|
In light mode it is the dawn wash over faint star chart lines. */
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse at 100% 0%, rgba(106, 47, 224, 0.28), transparent 50%),
|
radial-gradient(ellipse at 100% 0%, rgba(var(--gk-violet-rgb), calc(0.28 * var(--gk-wash))), transparent 50%),
|
||||||
radial-gradient(ellipse at 0% 100%, rgba(20, 163, 154, 0.2), transparent 55%),
|
radial-gradient(ellipse at 0% 100%, rgba(var(--gk-teal-deep-rgb), calc(0.2 * var(--gk-wash))), transparent 55%),
|
||||||
|
var(--gk-chart),
|
||||||
var(--gk-sky);
|
var(--gk-sky);
|
||||||
}
|
}
|
||||||
.gk-main {
|
.gk-main {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
---
|
---
|
||||||
/** One seal as an image, for seal marks, medallions and placeholder art. */
|
/** One seal as an image, for seal marks, medallions and placeholder art. */
|
||||||
|
import Motif from "./Motif.astro";
|
||||||
import { seals, type SealName } from "./seals.ts";
|
import { seals, type SealName } from "./seals.ts";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: SealName;
|
name: SealName;
|
||||||
size?: number;
|
size?: number;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
/** Gold foil, for a seal standing alone at 120px or more. */
|
||||||
|
foil?: boolean;
|
||||||
}
|
}
|
||||||
const { name, size = 40, class: className } = Astro.props;
|
const { name, size = 40, class: className, foil } = Astro.props;
|
||||||
---
|
---
|
||||||
<img src={seals[name]} alt="" aria-hidden="true" width={size} height={size} class={className} loading="lazy" decoding="async" />
|
<Motif src={seals[name]} foil={foil} alt="" aria-hidden="true" width={size} height={size} class={className} loading="lazy" decoding="async" />
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const H = `h${level}` as "h1" | "h2" | "h3";
|
|||||||
max-width: 60%;
|
max-width: 60%;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: linear-gradient(90deg, var(--gk-gold), rgba(201, 162, 74, 0.15) 70%, transparent);
|
background: linear-gradient(90deg, var(--gk-gold), rgba(var(--gk-gold-rgb), 0.15) 70%, transparent);
|
||||||
box-shadow: 0 0 8px rgba(201, 162, 74, 0.35);
|
box-shadow: 0 0 8px rgba(var(--gk-gold-rgb), 0.35);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
/** The site footer: the header divider again, then the monogram line and links. */
|
/** The site footer: the header divider again, then the monogram line and links. */
|
||||||
import monogram from "../assets/brand/gk-monogram.svg?url";
|
import monogram from "../assets/brand/gk-monogram.svg?url";
|
||||||
import Divider from "./Divider.astro";
|
import Divider from "./Divider.astro";
|
||||||
|
import Motif from "./Motif.astro";
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
---
|
---
|
||||||
<footer class="gk-site-footer">
|
<footer class="gk-site-footer">
|
||||||
<div class="gk-wrap"><Divider seed="footer" /></div>
|
<div class="gk-wrap"><Divider seed="footer" /></div>
|
||||||
<div class="gk-wrap row">
|
<div class="gk-wrap row">
|
||||||
<span class="mark"><img src={monogram} alt="" width="40" height="40" />© {year} Greg Kilmer · Web Development · a Patiun LLC company</span>
|
<span class="mark"><Motif src={monogram} alt="" width="40" height="40" />© {year} Greg Kilmer · Web Development · a Patiun LLC company</span>
|
||||||
<span class="links">
|
<span class="links">
|
||||||
<a href="/inquiries" class="gk-text strong">Start an inquiry</a>
|
<a href="/inquiries" class="gk-text strong">Start an inquiry</a>
|
||||||
<a href="/about" class="gk-text gk-quiet">About</a>
|
<a href="/about" class="gk-text gk-quiet">About</a>
|
||||||
@@ -37,6 +38,12 @@ const year = new Date().getFullYear();
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
.mark :global(img) {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
:global(:root[data-theme="light"]) .mark :global(img) {
|
||||||
|
box-shadow: 0 1px 3px rgba(19, 28, 43, 0.14);
|
||||||
|
}
|
||||||
.links {
|
.links {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
import monogram from "../assets/brand/gk-monogram.svg?url";
|
import monogram from "../assets/brand/gk-monogram.svg?url";
|
||||||
import Divider from "./Divider.astro";
|
import Divider from "./Divider.astro";
|
||||||
|
import Motif from "./Motif.astro";
|
||||||
import Star from "./Star.astro";
|
import Star from "./Star.astro";
|
||||||
|
import ThemeToggle from "./ThemeToggle.astro";
|
||||||
import { main, tail, world, type NavKey, type StoreNav } from "./nav.ts";
|
import { main, tail, world, type NavKey, type StoreNav } from "./nav.ts";
|
||||||
import { filamentBanner } from "./seals.ts";
|
import { filamentBanner } from "./seals.ts";
|
||||||
|
|
||||||
@@ -22,32 +24,37 @@ interface Props {
|
|||||||
const { current, store, hero = Astro.slots.has("default") } = Astro.props;
|
const { current, store, hero = Astro.slots.has("default") } = Astro.props;
|
||||||
const inWorld = world.some((w) => w.key === current);
|
const inWorld = world.some((w) => w.key === current);
|
||||||
const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
||||||
|
// Light mode's atlas marks: right ascension hours along the top of the hero,
|
||||||
|
// declination down its right side.
|
||||||
|
const hours = Array.from({ length: 12 }, (_, i) => ({ at: (i / 12) * 100, label: `${i * 2}h` }));
|
||||||
|
const decs = [60, 30, 0, -30].map((d, i) => ({ at: (i / 4) * 100, label: `${d > 0 ? "+" : d < 0 ? "\u2212" : "+"}${Math.abs(d)}°` }));
|
||||||
---
|
---
|
||||||
<header class="gk-site-header">
|
<header class="gk-site-header">
|
||||||
<div class="decor" aria-hidden="true">
|
<div class="decor" aria-hidden="true">
|
||||||
<div class="nebula"></div>
|
<div class="nebula"></div>
|
||||||
<img src={filamentBanner} alt="" class="banner" />
|
<Motif src={filamentBanner} alt="" class="banner" />
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
store ? (
|
store ? (
|
||||||
<nav aria-label="Store" class="nav">
|
<nav aria-label="Store" class="nav">
|
||||||
<a href="/store" class="brand" aria-current={store.current === "/store" ? "page" : undefined}>
|
<a href="/store" class="brand" aria-current={store.current === "/store" ? "page" : undefined}>
|
||||||
<img src={monogram} alt="" width="72" height="72" class="mono" />
|
<Motif src={monogram} alt="" width="72" height="72" class="mono" loading="eager" />
|
||||||
<span class="words">
|
<span class="words">
|
||||||
<span class="name store-name">Store.GregKilmer.com</span>
|
<span class="name store-name">Store.GregKilmer.com</span>
|
||||||
<span class="store-tag">a Patiun LLC workshop</span>
|
<span class="store-tag">a Patiun LLC workshop</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="links">
|
<ul class="links store-links">
|
||||||
{store.links.map((l) => <li><a href={l.href} aria-current={l.href === store.current ? "page" : undefined}>{l.label}</a></li>)}
|
{store.links.map((l) => <li><a href={l.href} aria-current={l.href === store.current ? "page" : undefined}>{l.label}</a></li>)}
|
||||||
<li class="sep" aria-hidden="true"><Star size={12} /></li>
|
<li class="sep" aria-hidden="true"><Star size={12} /></li>
|
||||||
<li><a href="/" class="dim">Back to GregKilmer.com</a></li>
|
<li><a href="/" class="dim">Back to GregKilmer.com</a></li>
|
||||||
|
<li class="mode"><ThemeToggle /></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
) : (
|
) : (
|
||||||
<nav aria-label="Primary" class="nav">
|
<nav aria-label="Primary" class="nav">
|
||||||
<a href="/" class="brand" aria-current={aria("home")}>
|
<a href="/" class="brand" aria-current={aria("home")}>
|
||||||
<img src={monogram} alt="" width="72" height="72" class="mono" />
|
<Motif src={monogram} alt="" width="72" height="72" class="mono" loading="eager" />
|
||||||
<span class="words">
|
<span class="words">
|
||||||
<span class="name">GREG KILMER</span>
|
<span class="name">GREG KILMER</span>
|
||||||
<span class="tag">DEVELOPER<Star size={8} tone="teal" />MAKER<Star size={8} tone="teal" />STORYTELLER</span>
|
<span class="tag">DEVELOPER<Star size={8} tone="teal" />MAKER<Star size={8} tone="teal" />STORYTELLER</span>
|
||||||
@@ -75,6 +82,7 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{tail.map((l) => <li><a href={l.href} aria-current={aria(l.key)} class="dim">{l.label}</a></li>)}
|
{tail.map((l) => <li><a href={l.href} aria-current={aria(l.key)} class="dim">{l.label}</a></li>)}
|
||||||
|
<li class="mode"><ThemeToggle /></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
@@ -82,6 +90,10 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
|||||||
{
|
{
|
||||||
hero && (
|
hero && (
|
||||||
<div class="gk-wrap hero">
|
<div class="gk-wrap hero">
|
||||||
|
<div class="atlas" aria-hidden="true">
|
||||||
|
<span class="ra">{hours.map((h) => <span style={`left:${h.at}%`}>{h.label}</span>)}</span>
|
||||||
|
<span class="dec">{decs.map((d) => <span style={`top:${d.at}%`}>{d.label}</span>)}</span>
|
||||||
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -109,8 +121,8 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse 70% 90% at 100% 0%, rgba(106, 47, 224, 0.28), transparent 70%),
|
radial-gradient(ellipse 70% 90% at 100% 0%, rgba(var(--gk-violet-rgb), calc(0.28 * var(--gk-wash))), transparent 70%),
|
||||||
radial-gradient(ellipse 55% 80% at 0% 70%, rgba(20, 163, 154, 0.18), transparent 70%);
|
radial-gradient(ellipse 55% 80% at 0% 70%, rgba(var(--gk-teal-deep-rgb), calc(0.18 * var(--gk-wash))), transparent 70%);
|
||||||
-webkit-mask-image: linear-gradient(to bottom, #000 45%, transparent 100%);
|
-webkit-mask-image: linear-gradient(to bottom, #000 45%, transparent 100%);
|
||||||
mask-image: linear-gradient(to bottom, #000 45%, transparent 100%);
|
mask-image: linear-gradient(to bottom, #000 45%, transparent 100%);
|
||||||
}
|
}
|
||||||
@@ -120,7 +132,7 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
|||||||
top: -40px;
|
top: -40px;
|
||||||
width: 900px;
|
width: 900px;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
opacity: 0.22;
|
opacity: calc(0.22 * var(--gk-ghost));
|
||||||
}
|
}
|
||||||
.nav {
|
.nav {
|
||||||
max-width: var(--gk-page-max);
|
max-width: var(--gk-page-max);
|
||||||
@@ -133,7 +145,7 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
border-bottom: 1px solid rgba(201, 162, 74, 0.35);
|
border-bottom: 1px solid rgba(var(--gk-gold-rgb), 0.35);
|
||||||
}
|
}
|
||||||
.brand {
|
.brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -148,9 +160,13 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: filter 0.25s ease;
|
transition: filter 0.25s ease;
|
||||||
}
|
}
|
||||||
|
/* On Starlight the pale disc needs a soft edge to sit apart from the page. */
|
||||||
|
:global(:root[data-theme="light"]) .mono {
|
||||||
|
box-shadow: 0 1px 4px rgba(19, 28, 43, 0.14);
|
||||||
|
}
|
||||||
.brand:hover .mono,
|
.brand:hover .mono,
|
||||||
.brand:focus-visible .mono {
|
.brand:focus-visible .mono {
|
||||||
filter: drop-shadow(0 0 6px rgba(216, 179, 90, 0.7)) drop-shadow(0 0 16px rgba(43, 194, 183, 0.45));
|
filter: drop-shadow(0 0 6px rgba(var(--gk-gold-bright-rgb), 0.7)) drop-shadow(0 0 16px rgba(var(--gk-teal-rgb), 0.45));
|
||||||
}
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.mono {
|
.mono {
|
||||||
@@ -197,6 +213,10 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
/* The Store's longer links fit one row at desktop width. */
|
||||||
|
.store-links {
|
||||||
|
column-gap: 14px;
|
||||||
|
}
|
||||||
.links a {
|
.links a {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -215,7 +235,8 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
|||||||
.links a[aria-current] {
|
.links a[aria-current] {
|
||||||
color: var(--gk-teal-bright);
|
color: var(--gk-teal-bright);
|
||||||
}
|
}
|
||||||
.sep {
|
.sep,
|
||||||
|
.mode {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.mw {
|
.mw {
|
||||||
@@ -253,7 +274,7 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
|||||||
}
|
}
|
||||||
.mw-menu a:hover,
|
.mw-menu a:hover,
|
||||||
.mw-menu a:focus-visible {
|
.mw-menu a:focus-visible {
|
||||||
background: rgba(201, 162, 74, 0.1);
|
background: rgba(var(--gk-gold-rgb), 0.1);
|
||||||
}
|
}
|
||||||
.mw-name {
|
.mw-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -276,6 +297,49 @@ const aria = (key: NavKey) => (key === current ? "page" : undefined);
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
.atlas {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:global(:root[data-theme="light"]) .atlas {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--gk-muted);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.ra {
|
||||||
|
position: absolute;
|
||||||
|
left: var(--gk-gutter);
|
||||||
|
right: var(--gk-gutter);
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
.ra span {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.ra span:first-child {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
/* Declination sits in the right gutter, clear of the hero's content. */
|
||||||
|
.dec {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 44px;
|
||||||
|
bottom: 20px;
|
||||||
|
}
|
||||||
|
.dec span {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.dec,
|
||||||
|
.ra span:nth-child(even) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
.divider {
|
.divider {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ const { size = 10, tone = "gold", class: className } = Astro.props;
|
|||||||
const fill = { gold: "var(--gk-gold)", violet: "var(--gk-violet)", teal: "var(--gk-teal-bright)", starlight: "var(--gk-starlight)", outline: "none" }[tone];
|
const fill = { gold: "var(--gk-gold)", violet: "var(--gk-violet)", teal: "var(--gk-teal-bright)", starlight: "var(--gk-starlight)", outline: "none" }[tone];
|
||||||
---
|
---
|
||||||
<svg aria-hidden="true" class={className} width={size} height={size} viewBox="0 0 20 20" style="flex: none">
|
<svg aria-hidden="true" class={className} width={size} height={size} viewBox="0 0 20 20" style="flex: none">
|
||||||
<polygon points={starPoints(10, 10, 10)} fill={fill} stroke={tone === "outline" ? "#4A5F6B" : undefined} stroke-width={tone === "outline" ? 1 : undefined} />
|
<polygon points={starPoints(10, 10, 10)} fill={fill} stroke={tone === "outline" ? "var(--gk-orn-dim)" : undefined} stroke-width={tone === "outline" ? 1 : undefined} />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -31,16 +31,25 @@ const { seal = "transmutation", ratio = "4 / 3", src, alt = "", class: className
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--gk-radius);
|
border-radius: var(--gk-radius);
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse 70% 90% at 100% 0%, rgba(106, 47, 224, 0.28), transparent 70%),
|
radial-gradient(ellipse 70% 90% at 100% 0%, rgba(var(--gk-violet-rgb), calc(0.28 * var(--gk-wash))), transparent 70%),
|
||||||
radial-gradient(ellipse 55% 80% at 0% 70%, rgba(20, 163, 154, 0.18), transparent 70%),
|
radial-gradient(ellipse 55% 80% at 0% 70%, rgba(var(--gk-teal-deep-rgb), calc(0.18 * var(--gk-wash))), transparent 70%),
|
||||||
repeating-linear-gradient(0deg, rgba(244, 242, 237, 0.035) 0 1px, transparent 1px 40px),
|
repeating-linear-gradient(0deg, rgba(var(--gk-star-rgb), 0.035) 0 1px, transparent 1px 40px),
|
||||||
repeating-linear-gradient(90deg, rgba(244, 242, 237, 0.035) 0 1px, transparent 1px 40px),
|
repeating-linear-gradient(90deg, rgba(var(--gk-star-rgb), 0.035) 0 1px, transparent 1px 40px),
|
||||||
var(--gk-sky);
|
var(--gk-sky);
|
||||||
}
|
}
|
||||||
|
/* Light mode: a morning sky tile, a touch cooler than the page. */
|
||||||
|
:global(:root[data-theme="light"]) .gk-art {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 70% 90% at 100% 0%, rgba(106, 47, 224, 0.16), transparent 70%),
|
||||||
|
radial-gradient(ellipse 55% 80% at 0% 70%, rgba(15, 140, 132, 0.14), transparent 70%),
|
||||||
|
repeating-linear-gradient(0deg, rgba(19, 28, 43, 0.06) 0 1px, transparent 1px 40px),
|
||||||
|
repeating-linear-gradient(90deg, rgba(19, 28, 43, 0.06) 0 1px, transparent 1px 40px),
|
||||||
|
#e9e6ee;
|
||||||
|
}
|
||||||
.mat {
|
.mat {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 10px;
|
inset: 10px;
|
||||||
border: 1px solid rgba(201, 162, 74, 0.45);
|
border: 1px solid rgba(var(--gk-gold-rgb), 0.45);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ const bottom = Math.max(...p.marks.map((m) => m.y + m.r)) + 2;
|
|||||||
const span = bottom - top;
|
const span = bottom - top;
|
||||||
---
|
---
|
||||||
<svg aria-hidden="true" class={className} viewBox={`-6 ${top} ${width + 12} ${span}`} width="100%" height={height ?? Math.ceil(span)} preserveAspectRatio="xMidYMid meet" style="display: block">
|
<svg aria-hidden="true" class={className} viewBox={`-6 ${top} ${width + 12} ${span}`} width="100%" height={height ?? Math.ceil(span)} preserveAspectRatio="xMidYMid meet" style="display: block">
|
||||||
{p.links.map((l) => <line {...l} stroke="#2BC2B7" stroke-width="1" stroke-dasharray="1.5 4" stroke-linecap="round" opacity="0.7" />)}
|
{p.links.map((l) => <line {...l} style="stroke:var(--gk-orn-link)" stroke-width="1" stroke-dasharray="1.5 4" stroke-linecap="round" opacity="0.7" />)}
|
||||||
{
|
{
|
||||||
p.marks.map((m) =>
|
p.marks.map((m) =>
|
||||||
m.kind === "dot" ? (
|
m.kind === "dot" ? (
|
||||||
<circle cx={m.x} cy={m.y} r={m.r} fill="#F4F2ED" opacity="0.85" />
|
<circle cx={m.x} cy={m.y} r={m.r} style="fill:var(--gk-orn-dot)" opacity="0.85" />
|
||||||
) : (
|
) : (
|
||||||
<polygon points={starPoints(m.x, m.y, m.r * (m.gold ? 0.75 : 1))} fill={m.gold ? "#C9A24A" : "#9A70F5"} />
|
<polygon points={starPoints(m.x, m.y, m.r * (m.gold ? 0.75 : 1))} fill={m.gold ? "var(--gk-gold)" : "var(--gk-violet)"} />
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Swaps between dark and light mode. Shows the moon in dark mode and the sun
|
||||||
|
* in light. Until it is pressed the site follows the device setting; a press
|
||||||
|
* saves the choice in this browser (PageShell's head script reads it back).
|
||||||
|
*/
|
||||||
|
import Icon from "./Icon.astro";
|
||||||
|
---
|
||||||
|
<button type="button" class="gk-theme-toggle" data-theme-toggle aria-label="Switch to light mode" title="Switch to light mode">
|
||||||
|
<Icon name="moon" size={20} class="moon" />
|
||||||
|
<Icon name="sun" size={20} class="sun" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
for (const b of document.querySelectorAll<HTMLButtonElement>("[data-theme-toggle]")) {
|
||||||
|
const label = () => {
|
||||||
|
const next = document.documentElement.dataset.theme === "light" ? "dark" : "light";
|
||||||
|
b.setAttribute("aria-label", `Switch to ${next} mode`);
|
||||||
|
b.title = `Switch to ${next} mode`;
|
||||||
|
};
|
||||||
|
label();
|
||||||
|
new MutationObserver(label).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
||||||
|
b.addEventListener("click", () => {
|
||||||
|
const next = document.documentElement.dataset.theme === "light" ? "dark" : "light";
|
||||||
|
try {
|
||||||
|
localStorage.setItem("gk-theme", next);
|
||||||
|
} catch {
|
||||||
|
// Private browsing can refuse storage; the swap still holds for this page.
|
||||||
|
}
|
||||||
|
document.documentElement.dataset.theme = next;
|
||||||
|
(window as { gkApplyTheme?: () => void }).gkApplyTheme?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.gk-theme-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
margin: 0 -10px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--gk-soft);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color var(--gk-ease), background var(--gk-ease);
|
||||||
|
}
|
||||||
|
.gk-theme-toggle:hover {
|
||||||
|
color: var(--gk-teal-bright);
|
||||||
|
background: var(--gk-gold-tint);
|
||||||
|
}
|
||||||
|
.gk-theme-toggle:focus-visible {
|
||||||
|
outline: 2px solid var(--gk-teal-bright);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
:global(:root[data-theme="light"]) .gk-theme-toggle :global(.moon),
|
||||||
|
:global(:root:not([data-theme="light"])) .gk-theme-toggle :global(.sun) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,7 +16,7 @@ const marks = ringTicks(size, size >= 60 ? 48 : 32, 4);
|
|||||||
---
|
---
|
||||||
<span class:list={["gk-ring", { medallion }, className]} style={`width:${size}px;height:${size}px`}>
|
<span class:list={["gk-ring", { medallion }, className]} style={`width:${size}px;height:${size}px`}>
|
||||||
<svg aria-hidden="true" width={size} height={size} viewBox={`0 0 ${size} ${size}`} class="ticks">
|
<svg aria-hidden="true" width={size} height={size} viewBox={`0 0 ${size} ${size}`} class="ticks">
|
||||||
{marks.map((m) => <line x1={m.x1} y1={m.y1} x2={m.x2} y2={m.y2} stroke="#C9A24A" stroke-width="0.9" opacity={m.long ? 0.85 : 0.5} />)}
|
{marks.map((m) => <line x1={m.x1} y1={m.y1} x2={m.x2} y2={m.y2} style="stroke:var(--gk-gold)" stroke-width="0.9" opacity={m.long ? 0.85 : 0.5} />)}
|
||||||
</svg>
|
</svg>
|
||||||
<span class="inner"><slot /></span>
|
<span class="inner"><slot /></span>
|
||||||
</span>
|
</span>
|
||||||
@@ -35,7 +35,7 @@ const marks = ringTicks(size, size >= 60 ? 48 : 32, 4);
|
|||||||
}
|
}
|
||||||
.medallion {
|
.medallion {
|
||||||
background: var(--gk-midnight);
|
background: var(--gk-midnight);
|
||||||
box-shadow: 0 0 0 1px rgba(201, 162, 74, 0.55), 0 0 18px rgba(201, 162, 74, 0.25);
|
box-shadow: 0 0 0 1px rgba(var(--gk-gold-rgb), 0.55), 0 0 18px rgba(var(--gk-gold-rgb), 0.25);
|
||||||
}
|
}
|
||||||
.ticks {
|
.ticks {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const lit = (x: number) => value === undefined || x <= width * value + 0.6;
|
|||||||
y1="0"
|
y1="0"
|
||||||
x2={m.x}
|
x2={m.x}
|
||||||
y2={m.long ? height : height / 2}
|
y2={m.long ? height : height / 2}
|
||||||
stroke={lit(m.x) ? "var(--gk-gold)" : "#4A5F6B"}
|
stroke={lit(m.x) ? "var(--gk-gold)" : "var(--gk-orn-dim)"}
|
||||||
stroke-width="1"
|
stroke-width="1"
|
||||||
opacity={m.long ? 0.8 : 0.45}
|
opacity={m.long ? 0.8 : 0.45}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -46,6 +46,33 @@ export type SealName = keyof typeof seals;
|
|||||||
|
|
||||||
export { filamentBanner };
|
export { filamentBanner };
|
||||||
|
|
||||||
|
// Light mode and foil versions (and the light monogram), written by scripts/light-motifs.mjs.
|
||||||
|
const byFile = (m: Record<string, string>) => Object.fromEntries(Object.entries(m).map(([p, u]) => [p.slice(p.lastIndexOf("/") + 1), u]));
|
||||||
|
const darkFiles = byFile({
|
||||||
|
...import.meta.glob<string>("../assets/motifs/*.svg", { query: "?url", import: "default", eager: true }),
|
||||||
|
...import.meta.glob<string>("../assets/brand/gk-monogram.svg", { query: "?url", import: "default", eager: true }),
|
||||||
|
});
|
||||||
|
const lightFiles = byFile({
|
||||||
|
...import.meta.glob<string>("../assets/motifs/light/*.svg", { query: "?url", import: "default", eager: true }),
|
||||||
|
...import.meta.glob<string>("../assets/brand/light/*.svg", { query: "?url", import: "default", eager: true }),
|
||||||
|
});
|
||||||
|
const foilFiles = byFile(import.meta.glob<string>("../assets/motifs/foil/*.svg", { query: "?url", import: "default", eager: true }));
|
||||||
|
const foilLightFiles = byFile(import.meta.glob<string>("../assets/motifs/foil-light/*.svg", { query: "?url", import: "default", eager: true }));
|
||||||
|
const fileOf = Object.fromEntries(Object.entries(darkFiles).map(([f, u]) => [u, f]));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A motif's image for each mode. Foil is only for a seal standing alone at
|
||||||
|
* 120px or more and half opacity or more; smaller or fainter seals stay flat.
|
||||||
|
*/
|
||||||
|
export function motifSources(url: string, { foil = false } = {}): { dark: string; light: string } {
|
||||||
|
const f = fileOf[url];
|
||||||
|
if (!f) return { dark: url, light: url };
|
||||||
|
return {
|
||||||
|
dark: (foil && foilFiles[f]) || url,
|
||||||
|
light: (foil && foilLightFiles[f]) || lightFiles[f] || url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Each page family's edge seal. A craft page uses its own craft seal. */
|
/** Each page family's edge seal. A craft page uses its own craft seal. */
|
||||||
export const pageSeals = {
|
export const pageSeals = {
|
||||||
landing: "transmutation-star-paths",
|
landing: "transmutation-star-paths",
|
||||||
|
|||||||
@@ -146,7 +146,8 @@ export function ringTicks(size: number, count = 48, major = 4): { x1: number; y1
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Constellation dust: sparse specks for a page background as CSS
|
* Constellation dust: sparse specks for a page background as CSS
|
||||||
* radial-gradients, about one in five gold. Static by design.
|
* radial-gradients, about one in five gold and, where the theme sets an
|
||||||
|
* accent (violet in light mode), one in five of the rest in it. Static by design.
|
||||||
*/
|
*/
|
||||||
export function dust(count: number, seed: number): string {
|
export function dust(count: number, seed: number): string {
|
||||||
const rand = seeded(seed);
|
const rand = seeded(seed);
|
||||||
@@ -155,7 +156,8 @@ export function dust(count: number, seed: number): string {
|
|||||||
const size = [1, 1, 1.2, 1.6][Math.floor(rand() * 4)];
|
const size = [1, 1, 1.2, 1.6][Math.floor(rand() * 4)];
|
||||||
const alpha = [0.25, 0.35, 0.6][Math.floor(rand() * 3)];
|
const alpha = [0.25, 0.35, 0.6][Math.floor(rand() * 3)];
|
||||||
const gold = rand() < 0.2;
|
const gold = rand() < 0.2;
|
||||||
const color = gold ? `rgba(216,179,90,${alpha})` : `rgba(236,233,226,${alpha})`;
|
const ink = gold ? "--gk-gold-bright-rgb" : i % 4 === 1 ? "--gk-dust-accent-rgb" : "--gk-text-rgb";
|
||||||
|
const color = `rgba(var(${ink}), ${alpha})`;
|
||||||
specks.push(`radial-gradient(${size}px ${size}px at ${r1(rand() * 100)}% ${r1(rand() * 100)}%, ${color}, transparent)`);
|
specks.push(`radial-gradient(${size}px ${size}px at ${r1(rand() * 100)}% ${r1(rand() * 100)}%, ${color}, transparent)`);
|
||||||
}
|
}
|
||||||
return specks.join(", ");
|
return specks.join(", ");
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ a {
|
|||||||
p a:not([class]) {
|
p a:not([class]) {
|
||||||
color: var(--gk-teal-bright);
|
color: var(--gk-teal-bright);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-color: rgba(43, 194, 183, 0.5);
|
text-decoration-color: rgba(var(--gk-teal-rgb), 0.5);
|
||||||
text-underline-offset: 3px;
|
text-underline-offset: 3px;
|
||||||
transition: color var(--gk-ease), text-decoration-color var(--gk-ease);
|
transition: color var(--gk-ease), text-decoration-color var(--gk-ease);
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ p a:not([class]):hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba(43, 194, 183, 0.35);
|
background: rgba(var(--gk-teal-rgb), 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gk-visually-hidden {
|
.gk-visually-hidden {
|
||||||
@@ -92,3 +92,9 @@ p a:not([class]):hover {
|
|||||||
animation-duration: 0.01ms !important;
|
animation-duration: 0.01ms !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A motif has a dark and a light image; show the one for the mode. */
|
||||||
|
:root:not([data-theme="light"]) .gk-motif-light,
|
||||||
|
:root[data-theme="light"] .gk-motif-dark {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,7 +39,18 @@
|
|||||||
.gk-pri:hover {
|
.gk-pri:hover {
|
||||||
background: var(--gk-teal-bright);
|
background: var(--gk-teal-bright);
|
||||||
color: var(--gk-sky);
|
color: var(--gk-sky);
|
||||||
box-shadow: 0 0 0 4px rgba(43, 194, 183, 0.16);
|
box-shadow: 0 0 0 4px rgba(var(--gk-teal-rgb), 0.16);
|
||||||
|
}
|
||||||
|
/* On Starlight the deep teal carries white text (5.8:1); the lighter teal is too faint. */
|
||||||
|
:root[data-theme="light"] .gk-pri,
|
||||||
|
:root[data-theme="light"] .gk-pri:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
:root[data-theme="light"] .gk-pri {
|
||||||
|
background: var(--gk-teal-bright);
|
||||||
|
}
|
||||||
|
:root[data-theme="light"] .gk-pri:hover {
|
||||||
|
background: #085e58;
|
||||||
}
|
}
|
||||||
.gk-sec {
|
.gk-sec {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -92,7 +103,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.gk-pill.live {
|
.gk-pill.live {
|
||||||
border-color: rgba(43, 194, 183, 0.6);
|
border-color: rgba(var(--gk-teal-rgb), 0.6);
|
||||||
color: var(--gk-teal-bright);
|
color: var(--gk-teal-bright);
|
||||||
}
|
}
|
||||||
.gk-pill.live::before {
|
.gk-pill.live::before {
|
||||||
@@ -102,11 +113,11 @@
|
|||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--gk-teal-bright);
|
background: var(--gk-teal-bright);
|
||||||
box-shadow: 0 0 6px rgba(43, 194, 183, 0.8);
|
box-shadow: 0 0 6px rgba(var(--gk-teal-rgb), 0.8);
|
||||||
}
|
}
|
||||||
/* A state that is not happening now keeps the round pill without the lit dot. */
|
/* A state that is not happening now keeps the round pill without the lit dot. */
|
||||||
.gk-pill.state {
|
.gk-pill.state {
|
||||||
border-color: rgba(43, 194, 183, 0.6);
|
border-color: rgba(var(--gk-teal-rgb), 0.6);
|
||||||
color: var(--gk-teal-bright);
|
color: var(--gk-teal-bright);
|
||||||
}
|
}
|
||||||
.gk-pill.cur {
|
.gk-pill.cur {
|
||||||
@@ -128,7 +139,7 @@
|
|||||||
transition: border-color var(--gk-ease), color var(--gk-ease);
|
transition: border-color var(--gk-ease), color var(--gk-ease);
|
||||||
}
|
}
|
||||||
.gk-chip.core {
|
.gk-chip.core {
|
||||||
border-color: rgba(43, 194, 183, 0.55);
|
border-color: rgba(var(--gk-teal-rgb), 0.55);
|
||||||
color: var(--gk-text);
|
color: var(--gk-text);
|
||||||
}
|
}
|
||||||
a.gk-chip:hover {
|
a.gk-chip:hover {
|
||||||
@@ -206,7 +217,7 @@ a.gk-chip:hover {
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-left: 2px solid var(--gk-gold);
|
border-left: 2px solid var(--gk-gold);
|
||||||
border-radius: 0 var(--gk-radius) var(--gk-radius) 0;
|
border-radius: 0 var(--gk-radius) var(--gk-radius) 0;
|
||||||
background: rgba(201, 162, 74, 0.08);
|
background: rgba(var(--gk-gold-rgb), 0.08);
|
||||||
color: var(--gk-text);
|
color: var(--gk-text);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
@@ -262,7 +273,7 @@ textarea.gk-field {
|
|||||||
background-size: 92% 2px;
|
background-size: 92% 2px;
|
||||||
}
|
}
|
||||||
.gk-field[aria-invalid="true"] {
|
.gk-field[aria-invalid="true"] {
|
||||||
border-color: #f0857a;
|
border-color: var(--gk-danger);
|
||||||
}
|
}
|
||||||
.gk-search {
|
.gk-search {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -324,7 +335,7 @@ textarea.gk-field {
|
|||||||
}
|
}
|
||||||
a.gk-card:hover,
|
a.gk-card:hover,
|
||||||
.gk-card.gk-linked:hover {
|
.gk-card.gk-linked:hover {
|
||||||
border-color: rgba(201, 162, 74, 0.55);
|
border-color: rgba(var(--gk-gold-rgb), 0.55);
|
||||||
}
|
}
|
||||||
a.gk-card:hover::before,
|
a.gk-card:hover::before,
|
||||||
.gk-card.gk-linked:hover::before {
|
.gk-card.gk-linked:hover::before {
|
||||||
@@ -332,7 +343,7 @@ a.gk-card:hover::before,
|
|||||||
}
|
}
|
||||||
.gk-featured {
|
.gk-featured {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid rgba(201, 162, 74, 0.55);
|
border: 1px solid rgba(var(--gk-gold-rgb), 0.55);
|
||||||
border-radius: var(--gk-radius);
|
border-radius: var(--gk-radius);
|
||||||
background: var(--gk-midnight);
|
background: var(--gk-midnight);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* Brand tokens for the all-dark site (Greg's decision, 2026-10-11). Light mode
|
* Brand tokens. Dark is the default (Greg's decision, 2026-10-11); the light
|
||||||
* is a later phase and gets its own values and graphics; nothing here assumes
|
* values below (picked 2026-10-18 on the light motif board: Dawn chart sky,
|
||||||
* it yet.
|
* deep gold line seals) apply when <html data-theme="light">. PageShell sets
|
||||||
|
* that from the device setting, or from the visitor's choice on the toggle.
|
||||||
*/
|
*/
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
@@ -24,6 +25,32 @@
|
|||||||
/* Violet only ever marks a star. On the sky it is lifted to read (#6A2FE0 is 2.7:1). */
|
/* Violet only ever marks a star. On the sky it is lifted to read (#6A2FE0 is 2.7:1). */
|
||||||
--gk-violet: #9a70f5;
|
--gk-violet: #9a70f5;
|
||||||
|
|
||||||
|
--gk-danger: #f0857a;
|
||||||
|
|
||||||
|
/* Ornament inks: star path links and dots, and unlit ticks. */
|
||||||
|
--gk-orn-link: #2bc2b7;
|
||||||
|
--gk-orn-dot: #f4f2ed;
|
||||||
|
--gk-orn-dim: #4a5f6b;
|
||||||
|
|
||||||
|
/* Channels for translucent colors, as rgba(var(--gk-gold-rgb), 0.35). */
|
||||||
|
--gk-gold-rgb: 201, 162, 74;
|
||||||
|
--gk-gold-bright-rgb: 216, 179, 90;
|
||||||
|
--gk-star-rgb: 244, 242, 237;
|
||||||
|
--gk-text-rgb: 236, 233, 226;
|
||||||
|
--gk-teal-rgb: 43, 194, 183;
|
||||||
|
--gk-teal-deep-rgb: 20, 163, 154;
|
||||||
|
--gk-violet-rgb: 106, 47, 224;
|
||||||
|
--gk-sky-rgb: 14, 21, 34;
|
||||||
|
--gk-midnight-rgb: 19, 28, 43;
|
||||||
|
/* Constellation dust: one in five specks takes this instead of the text color. */
|
||||||
|
--gk-dust-accent-rgb: 236, 233, 226;
|
||||||
|
/* How strong the violet and teal washes are; light mode halves them. */
|
||||||
|
--gk-wash: 1;
|
||||||
|
/* The star chart grid behind the page; off in dark, faint ink lines in light. */
|
||||||
|
--gk-chart: none;
|
||||||
|
/* How strong the faint seals behind pages and panels are; light mode lifts them. */
|
||||||
|
--gk-ghost: 1;
|
||||||
|
|
||||||
--gk-filament: linear-gradient(90deg, transparent, var(--gk-gold) 25%, var(--gk-teal-bright) 75%, transparent);
|
--gk-filament: linear-gradient(90deg, transparent, var(--gk-gold) 25%, var(--gk-teal-bright) 75%, transparent);
|
||||||
|
|
||||||
--gk-font-display: Cinzel, "Trajan Pro", Georgia, serif;
|
--gk-font-display: Cinzel, "Trajan Pro", Georgia, serif;
|
||||||
@@ -50,6 +77,50 @@
|
|||||||
--gk-ease: 0.15s;
|
--gk-ease: 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
--gk-sky: #f4f2ed;
|
||||||
|
--gk-midnight: #ffffff;
|
||||||
|
--gk-lines: #d9d2c2;
|
||||||
|
--gk-lines-strong: #b8ad97;
|
||||||
|
--gk-starlight: #131c2b;
|
||||||
|
--gk-text: #131c2b;
|
||||||
|
--gk-soft: #33465a;
|
||||||
|
--gk-muted: #52646f;
|
||||||
|
--gk-placeholder: #5e6e78;
|
||||||
|
/* Teal graphics; teal text, links and the primary button use the deeper teal-bright. */
|
||||||
|
--gk-teal: #0f8c84;
|
||||||
|
--gk-teal-bright: #0a726b;
|
||||||
|
--gk-gold: #8a6516;
|
||||||
|
--gk-gold-bright: #6f5112;
|
||||||
|
--gk-gold-line: rgba(138, 101, 22, 0.55);
|
||||||
|
--gk-gold-tint: rgba(138, 101, 22, 0.1);
|
||||||
|
--gk-violet: #6a2fe0;
|
||||||
|
--gk-danger: #b3261e;
|
||||||
|
|
||||||
|
--gk-orn-link: #0f8c84;
|
||||||
|
--gk-orn-dot: #131c2b;
|
||||||
|
--gk-orn-dim: #a9b3b9;
|
||||||
|
|
||||||
|
--gk-gold-rgb: 138, 101, 22;
|
||||||
|
--gk-gold-bright-rgb: 111, 81, 18;
|
||||||
|
--gk-star-rgb: 19, 28, 43;
|
||||||
|
--gk-text-rgb: 19, 28, 43;
|
||||||
|
--gk-teal-rgb: 10, 114, 107;
|
||||||
|
--gk-teal-deep-rgb: 15, 140, 132;
|
||||||
|
--gk-sky-rgb: 244, 242, 237;
|
||||||
|
--gk-midnight-rgb: 255, 255, 255;
|
||||||
|
--gk-dust-accent-rgb: 106, 47, 224;
|
||||||
|
--gk-wash: 0.5;
|
||||||
|
--gk-ghost: 1.6;
|
||||||
|
--gk-chart:
|
||||||
|
repeating-linear-gradient(0deg, rgba(19, 28, 43, 0.032) 0 1px, transparent 1px 44px),
|
||||||
|
repeating-linear-gradient(90deg, rgba(19, 28, 43, 0.032) 0 1px, transparent 1px 44px);
|
||||||
|
|
||||||
|
--gk-filament: linear-gradient(90deg, transparent, #8a6516 25%, #0f8c84 75%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
:root {
|
:root {
|
||||||
--gk-gutter: 16px;
|
--gk-gutter: 16px;
|
||||||
|
|||||||
@@ -110,3 +110,37 @@ details summary { cursor: pointer; color: var(--muted); margin-top: 10px; }
|
|||||||
form.inline { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 13px; }
|
form.inline { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 13px; }
|
||||||
form.inline button { min-height: 30px; padding: 2px 12px; font-size: 13px; }
|
form.inline button { min-height: 30px; padding: 2px 12px; font-size: 13px; }
|
||||||
.preview img { max-height: 480px; object-fit: contain; }
|
.preview img { max-height: 480px; object-fit: contain; }
|
||||||
|
|
||||||
|
/* Note editor */
|
||||||
|
.note-form .field { margin-bottom: 14px; }
|
||||||
|
.note-form fieldset { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px 12px; margin: 0 0 14px; min-width: 0; }
|
||||||
|
.note-form legend { padding: 0 6px; font-weight: 600; }
|
||||||
|
.note-form .label { font-weight: 600; }
|
||||||
|
.image-pick { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 12px; align-items: start; }
|
||||||
|
.image-pick img, .image-pick .no-image { width: 120px; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; border: 1px solid var(--line); display: block; }
|
||||||
|
.image-pick .no-image { display: grid; place-items: center; color: var(--muted); font-size: 0.85rem; }
|
||||||
|
.image-pick .sub { margin-top: 8px; font-weight: 400; color: var(--muted); }
|
||||||
|
.side .image-pick { grid-template-columns: 1fr; }
|
||||||
|
.side .image-pick img, .side .image-pick .no-image { width: 100%; aspect-ratio: 16 / 9; }
|
||||||
|
.row-item { border-top: 1px solid var(--line); padding: 10px 0; }
|
||||||
|
.row-item:first-of-type { border-top: 0; }
|
||||||
|
.row-item.blank { opacity: 0.85; }
|
||||||
|
.row-fields { display: grid; gap: 0 12px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
||||||
|
.row-fields .field:has(.image-pick) { grid-column: 1 / -1; }
|
||||||
|
.row-tools { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 4px; }
|
||||||
|
.row-tools button { padding: 4px 10px; font-size: 0.85rem; }
|
||||||
|
|
||||||
|
/* Site pages */
|
||||||
|
.site-pages { list-style: none; padding: 0; margin: 0; display: grid; gap: 16px; }
|
||||||
|
.site-pages h2 { margin: 0; font-size: 1.1rem; }
|
||||||
|
.page-images { list-style: none; padding: 0; margin: 8px 0; display: flex; flex-wrap: wrap; gap: 10px; }
|
||||||
|
.page-images li { width: 120px; font-size: 0.8rem; color: var(--muted); }
|
||||||
|
.page-images img { width: 120px; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; border: 1px solid var(--line); display: block; margin-bottom: 4px; }
|
||||||
|
.feeds { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||||
|
.pill-link { border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; font-size: 0.85rem; }
|
||||||
|
|
||||||
|
/* Before launch */
|
||||||
|
.launch ul { list-style: none; padding: 0; margin: 0; }
|
||||||
|
.launch li { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; border-top: 1px solid var(--line); }
|
||||||
|
.launch li:first-child { border-top: 0; }
|
||||||
|
.count-pill { font-size: 0.8rem; border: 1px solid var(--line); border-radius: 999px; padding: 1px 8px; margin-left: 6px; vertical-align: middle; }
|
||||||
|
|||||||
@@ -155,6 +155,15 @@ export class ContentApi {
|
|||||||
notes = (p: { type?: string; q?: string; limit?: number; full?: boolean } = {}) =>
|
notes = (p: { type?: string; q?: string; limit?: number; full?: boolean } = {}) =>
|
||||||
this.get<Summary[]>("/api/notes", { type: p.type, q: p.q, limit: p.limit, full: p.full ? "true" : undefined });
|
this.get<Summary[]>("/api/notes", { type: p.type, q: p.q, limit: p.limit, full: p.full ? "true" : undefined });
|
||||||
fullNotes = (type: string) => this.get<FullNote[]>("/api/notes", { type, full: "true", limit: 1000 });
|
fullNotes = (type: string) => this.get<FullNote[]>("/api/notes", { type, full: "true", limit: 1000 });
|
||||||
|
/** Every note, whole, a thousand at a time. */
|
||||||
|
async allNotes(): Promise<FullNote[]> {
|
||||||
|
const all: FullNote[] = [];
|
||||||
|
for (let offset = 0; ; offset += 1000) {
|
||||||
|
const page = await this.get<FullNote[]>("/api/notes", { full: "true", limit: 1000, offset });
|
||||||
|
all.push(...page);
|
||||||
|
if (page.length < 1000) return all;
|
||||||
|
}
|
||||||
|
}
|
||||||
note = (path: string) => this.get<Note>("/api/note", { path });
|
note = (path: string) => this.get<Note>("/api/note", { path });
|
||||||
noteByName = (name: string) => this.get<Note>("/api/note", { name });
|
noteByName = (name: string) => this.get<Note>("/api/note", { name });
|
||||||
history = (path: string) => this.get<Revision[]>("/api/note/history", { path });
|
history = (path: string) => this.get<Revision[]>("/api/note/history", { path });
|
||||||
|
|||||||
@@ -0,0 +1,456 @@
|
|||||||
|
/**
|
||||||
|
* The note editor: a form for any note type, made from the note type's
|
||||||
|
* schema so it accepts exactly what the content service accepts. Image
|
||||||
|
* fields get a picker (a thumbnail, the vault's images, an upload), link
|
||||||
|
* fields get the note names, and lists of rows (photos, specs, socials) can
|
||||||
|
* be added to, removed and reordered.
|
||||||
|
*
|
||||||
|
* The form round-trips through the note's own data shape: `formData` turns a
|
||||||
|
* posted form back into frontmatter values, and `noteEditor` renders from
|
||||||
|
* them, so a reorder or a failed save shows exactly what was typed.
|
||||||
|
*/
|
||||||
|
import { noteTypes, type NoteType } from "@gk/schema";
|
||||||
|
import { z } from "zod";
|
||||||
|
import type { Attachment, Note } from "./client.ts";
|
||||||
|
import { html, qs, type Html } from "./html.ts";
|
||||||
|
import { noteHref } from "./pages.ts";
|
||||||
|
|
||||||
|
/** One field as the JSON Schema export describes it. */
|
||||||
|
export interface Prop {
|
||||||
|
type?: string;
|
||||||
|
enum?: unknown[];
|
||||||
|
const?: unknown;
|
||||||
|
format?: string;
|
||||||
|
pattern?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
items?: Prop;
|
||||||
|
properties?: Record<string, Prop>;
|
||||||
|
required?: string[];
|
||||||
|
default?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Shape {
|
||||||
|
properties: Record<string, Prop>;
|
||||||
|
required: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fields the service or the file name owns; the editor never sends them. */
|
||||||
|
const OWNED = new Set(["type", "title", "updated", "updatedBy", "created"]);
|
||||||
|
/** Shared fields, shown after the note type's own. */
|
||||||
|
const SHARED = ["publish", "favorite", "cover", "links", "tags", "slug", "agentEdits"];
|
||||||
|
|
||||||
|
const shapes = new Map<string, Shape>();
|
||||||
|
export function shapeOf(type: string): Shape {
|
||||||
|
let s = shapes.get(type);
|
||||||
|
if (!s) {
|
||||||
|
const def = noteTypes[type as NoteType];
|
||||||
|
if (!def) throw new Error(`Unknown note type ${type}`);
|
||||||
|
const js = z.toJSONSchema(def.schema, { io: "input", unrepresentable: "any" }) as { properties: Record<string, Prop>; required?: string[] };
|
||||||
|
s = { properties: js.properties, required: js.required ?? [] };
|
||||||
|
shapes.set(type, s);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether notes of this type have Markdown under the frontmatter. */
|
||||||
|
export const hasBody = (type: string) => noteTypes[type as NoteType]?.body != null;
|
||||||
|
|
||||||
|
export const editHref = (path: string) => `/edit${qs({ path })}`;
|
||||||
|
export const newHref = (type: string, extra: Record<string, string> = {}) => `/edit/new${qs({ type, ...extra })}`;
|
||||||
|
|
||||||
|
/** The `_attachments` folder an upload from this note type goes to. */
|
||||||
|
export function uploadFolder(type: string): string {
|
||||||
|
if (["craft", "station", "tool", "make"].includes(type)) return "Workshop";
|
||||||
|
if (["library-item", "board-game"].includes(type)) return "Library";
|
||||||
|
if (["tabletop-system", "campaign", "card-game", "deck"].includes(type)) return "Tabletop";
|
||||||
|
if (["product-line", "store-settings"].includes(type)) return "Store";
|
||||||
|
if (type === "post") return "Posts";
|
||||||
|
return "Pages";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ field kinds
|
||||||
|
|
||||||
|
type Kind = "bool" | "enum" | "number" | "date" | "month" | "url" | "image" | "link" | "text" | "lines" | "links" | "images" | "choices" | "rows" | "group" | "other";
|
||||||
|
|
||||||
|
const DATE = /^\^\\d\{4\}-\\d\{2\}-\\d\{2\}\$$/;
|
||||||
|
const MONTH = /^\^\\d\{4\}-\\d\{2\}\$$/;
|
||||||
|
|
||||||
|
export function kindOf(p: Prop, nested = false): Kind {
|
||||||
|
if (p.type === "boolean") return "bool";
|
||||||
|
if (p.type === "integer" || p.type === "number") return "number";
|
||||||
|
if (p.type === "string" || (!p.type && p.format)) {
|
||||||
|
if (p.enum) return "enum";
|
||||||
|
if (p.format === "vault-image") return "image";
|
||||||
|
if (p.format === "vault-link") return "link";
|
||||||
|
if (p.format === "uri") return "url";
|
||||||
|
if (p.pattern && DATE.test(p.pattern)) return "date";
|
||||||
|
if (p.pattern && MONTH.test(p.pattern)) return "month";
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
if (p.type === "array" && p.items) {
|
||||||
|
const it = p.items;
|
||||||
|
if (p.format === "vault-links") return "links";
|
||||||
|
if (it.format === "vault-image") return "images";
|
||||||
|
if (it.type === "string" && it.enum) return "choices";
|
||||||
|
if (it.type === "string") return "lines";
|
||||||
|
// Rows of simple fields; a row inside a row is left to Obsidian.
|
||||||
|
if (!nested && it.type === "object" && Object.values(it.properties ?? {}).every((s) => !["rows", "group", "other", "images"].includes(kindOf(s, true)))) return "rows";
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
if (p.type === "object" && !nested && Object.values(p.properties ?? {}).every((s) => ["bool", "enum", "number", "date", "month", "url", "text", "link", "image"].includes(kindOf(s, true)))) return "group";
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ values
|
||||||
|
|
||||||
|
/** `[[file.jpg]]` or `![[file.jpg|300]]` to `file.jpg`; a URL stays as it is. */
|
||||||
|
export const linkName = (v: unknown) =>
|
||||||
|
String(v ?? "")
|
||||||
|
.trim()
|
||||||
|
.replace(/^!?\[\[|\]\]$/g, "")
|
||||||
|
.split("|")[0]!
|
||||||
|
.trim();
|
||||||
|
const wrap = (v: string) => (/^https?:\/\//.test(v) ? v : `[[${v}]]`);
|
||||||
|
const str = (v: unknown) => (v === undefined || v === null ? "" : String(v));
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ reading a form
|
||||||
|
|
||||||
|
export interface Upload {
|
||||||
|
field: string;
|
||||||
|
name: string;
|
||||||
|
bytes: Uint8Array<ArrayBuffer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Turn a posted field into a value, or undefined when it was left empty. */
|
||||||
|
function scalar(kind: Kind, raw: string | null): unknown {
|
||||||
|
const v = (raw ?? "").trim();
|
||||||
|
if (!v) return undefined;
|
||||||
|
if (kind === "number") {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : v;
|
||||||
|
}
|
||||||
|
if (kind === "image" || kind === "link") return wrap(linkName(v));
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lines(raw: string | null, link: boolean): string[] {
|
||||||
|
return (raw ?? "")
|
||||||
|
.split("\n")
|
||||||
|
.map((l) => l.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((l) => (link ? wrap(linkName(l)) : l));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Indexes of the rows the form posted for `name`, in their order on the page. */
|
||||||
|
function rowIndexes(f: URLSearchParams, name: string): number[] {
|
||||||
|
const seen = new Set<number>();
|
||||||
|
const re = new RegExp(`^${name.replace(/\./g, "\\.")}\\.(\\d+)(\\.|$)`);
|
||||||
|
for (const k of f.keys()) {
|
||||||
|
const m = re.exec(k);
|
||||||
|
if (m) seen.add(Number(m[1]));
|
||||||
|
}
|
||||||
|
return [...seen].sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The note's values from a posted editor form. Uploaded images are saved
|
||||||
|
* first (by `save`) and arrive in `uploaded` keyed by the field that chose
|
||||||
|
* them. An empty field comes back as undefined.
|
||||||
|
*/
|
||||||
|
export function formData(type: string, f: URLSearchParams, uploaded: Map<string, string[]> = new Map()): Record<string, unknown> {
|
||||||
|
const shape = shapeOf(type);
|
||||||
|
const { properties } = shape;
|
||||||
|
const required = new Set(shape.required);
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
const field = (name: string, p: Prop, kind: Kind, required = false): unknown => {
|
||||||
|
const up = uploaded.get(name);
|
||||||
|
switch (kind) {
|
||||||
|
case "bool":
|
||||||
|
// An unticked optional box is left out rather than written as false.
|
||||||
|
if (!f.has(`has.${name}`)) return undefined;
|
||||||
|
return f.get(name) === "1" ? true : required || name === "publish" ? false : undefined;
|
||||||
|
case "image":
|
||||||
|
return up?.length ? wrap(up[0]!) : scalar(kind, f.get(name));
|
||||||
|
case "lines":
|
||||||
|
case "links": {
|
||||||
|
const l = lines(f.get(name), kind === "links");
|
||||||
|
return l.length ? l : undefined;
|
||||||
|
}
|
||||||
|
case "choices": {
|
||||||
|
const l = f.getAll(name).filter(Boolean);
|
||||||
|
return l.length ? l : undefined;
|
||||||
|
}
|
||||||
|
case "images": {
|
||||||
|
const l = rowIndexes(f, name)
|
||||||
|
.filter((i) => f.get(`${name}.${i}.remove`) !== "1")
|
||||||
|
.map((i) => scalar("image", f.get(`${name}.${i}`)))
|
||||||
|
.filter((v): v is string => typeof v === "string");
|
||||||
|
for (const u of up ?? []) l.push(wrap(u));
|
||||||
|
return l.length ? l : undefined;
|
||||||
|
}
|
||||||
|
case "rows": {
|
||||||
|
const subs = Object.entries(p.items?.properties ?? {});
|
||||||
|
const rows: Record<string, unknown>[] = [];
|
||||||
|
for (const i of rowIndexes(f, name)) {
|
||||||
|
if (f.get(`${name}.${i}.remove`) === "1") continue;
|
||||||
|
const row: Record<string, unknown> = {};
|
||||||
|
const req = new Set(p.items?.required ?? []);
|
||||||
|
for (const [k, s] of subs) {
|
||||||
|
const v = field(`${name}.${i}.${k}`, s, kindOf(s, true), req.has(k));
|
||||||
|
if (v !== undefined) row[k] = v;
|
||||||
|
}
|
||||||
|
// A row with nothing typed is the blank "add one" row.
|
||||||
|
if (Object.values(row).some((v) => v !== false)) rows.push(row);
|
||||||
|
}
|
||||||
|
return rows.length ? rows : undefined;
|
||||||
|
}
|
||||||
|
case "group": {
|
||||||
|
const g: Record<string, unknown> = {};
|
||||||
|
const req = new Set(p.required ?? []);
|
||||||
|
for (const [k, s] of Object.entries(p.properties ?? {})) {
|
||||||
|
const v = field(`${name}.${k}`, s, kindOf(s, true), req.has(k));
|
||||||
|
if (v !== undefined) g[k] = v;
|
||||||
|
}
|
||||||
|
return Object.keys(g).length ? g : undefined;
|
||||||
|
}
|
||||||
|
case "other":
|
||||||
|
return undefined;
|
||||||
|
default:
|
||||||
|
return scalar(kind, f.get(name));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const [key, p] of Object.entries(properties)) {
|
||||||
|
if (OWNED.has(key)) continue;
|
||||||
|
const kind = kindOf(p);
|
||||||
|
if (kind === "other") continue;
|
||||||
|
out[key] = field(key, p, kind, required.has(key));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The fields to send. On an update, a field left empty is sent as null so
|
||||||
|
* the content service drops it, and fields the form cannot edit are left
|
||||||
|
* out so the note keeps them.
|
||||||
|
*/
|
||||||
|
export function changeFields(values: Record<string, unknown>, update: boolean): Record<string, unknown> {
|
||||||
|
const fields: Record<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(values)) {
|
||||||
|
if (v !== undefined) fields[k] = v;
|
||||||
|
else if (update) fields[k] = null;
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Move row `i` of `name` up or down in posted form values. */
|
||||||
|
export function moveRow(values: Record<string, unknown>, move: string): Record<string, unknown> {
|
||||||
|
const m = /^(.+):(\d+):(up|down)$/.exec(move);
|
||||||
|
if (!m) return values;
|
||||||
|
const [, key, at, dir] = m;
|
||||||
|
const list = values[key!];
|
||||||
|
if (!Array.isArray(list)) return values;
|
||||||
|
const i = Number(at);
|
||||||
|
const j = dir === "up" ? i - 1 : i + 1;
|
||||||
|
if (j < 0 || j >= list.length) return values;
|
||||||
|
const next = [...list];
|
||||||
|
[next[i], next[j]] = [next[j], next[i]];
|
||||||
|
return { ...values, [key!]: next };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ rendering
|
||||||
|
|
||||||
|
export interface EditorDraft {
|
||||||
|
type: string;
|
||||||
|
/** Set when editing a saved note. */
|
||||||
|
path?: string;
|
||||||
|
title: string;
|
||||||
|
publish: boolean;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function draftFromNote(n: Note): EditorDraft {
|
||||||
|
return { type: n.type, path: n.path, title: n.title, publish: n.publish, data: n.data, body: n.body };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Ctx {
|
||||||
|
images: Attachment[];
|
||||||
|
notes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isImage = (name: string) => /\.(png|jpe?g|webp|gif|avif|svg)$/i.test(name);
|
||||||
|
const imageSrc = (v: string) => (/^https:\/\//.test(v) ? v : `/attachments/${encodeURIComponent(v)}`);
|
||||||
|
const id = (name: string) => `e-${name.replace(/[^a-zA-Z0-9]+/g, "-")}`;
|
||||||
|
const ACCEPT = "image/png,image/jpeg,image/webp,image/gif,image/avif";
|
||||||
|
|
||||||
|
function labelFor(key: string, p: Prop) {
|
||||||
|
return p.title ?? key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hint(p: Prop): Html | string {
|
||||||
|
return p.description ? html`<p class="hint">${p.description}</p>` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function imagePicker(name: string, value: string, ctx: Ctx, label: string): Html {
|
||||||
|
const current = linkName(value);
|
||||||
|
const known = ctx.images.some((a) => a.name === current);
|
||||||
|
return html`<div class="image-pick">
|
||||||
|
${current ? html`<img src="${imageSrc(current)}" alt="" loading="lazy">` : html`<div class="no-image">No image</div>`}
|
||||||
|
<div>
|
||||||
|
<label for="${id(name)}">${label}</label>
|
||||||
|
<select id="${id(name)}" name="${name}">
|
||||||
|
<option value="">None</option>
|
||||||
|
${current && !known ? html`<option value="${current}" selected>${current}</option>` : ""}
|
||||||
|
${ctx.images.map((a) => html`<option value="${a.name}" ${a.name === current ? html`selected` : ""}>${a.folder ? `${a.folder} / ` : ""}${a.name}</option>`)}
|
||||||
|
</select>
|
||||||
|
<label for="${id(name)}-up" class="sub">Or upload a new one</label>
|
||||||
|
<input id="${id(name)}-up" name="upload.${name}" type="file" accept="${ACCEPT}">
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function input(name: string, kind: Kind, p: Prop, value: unknown, ctx: Ctx, label: string, required: boolean): Html {
|
||||||
|
const i = id(name);
|
||||||
|
const req = required ? html`required` : "";
|
||||||
|
switch (kind) {
|
||||||
|
case "bool":
|
||||||
|
return html`<input type="hidden" name="has.${name}" value="1"><label class="check"><input type="checkbox" name="${name}" value="1" ${value === true ? html`checked` : ""}> ${label}</label>`;
|
||||||
|
case "enum":
|
||||||
|
return html`<label for="${i}">${label}</label><select id="${i}" name="${name}" ${req}>
|
||||||
|
${required ? "" : html`<option value="">Not set</option>`}
|
||||||
|
${(p.enum ?? []).map((o) => html`<option ${String(o) === str(value) ? html`selected` : ""}>${String(o)}</option>`)}
|
||||||
|
</select>`;
|
||||||
|
case "number":
|
||||||
|
return html`<label for="${i}">${label}</label><input id="${i}" name="${name}" type="number" step="${p.type === "integer" ? "1" : "any"}" value="${str(value)}" ${req}>`;
|
||||||
|
case "date":
|
||||||
|
return html`<label for="${i}">${label}</label><input id="${i}" name="${name}" type="date" value="${str(value).slice(0, 10)}" ${req}>`;
|
||||||
|
case "month":
|
||||||
|
return html`<label for="${i}">${label}</label><input id="${i}" name="${name}" type="month" value="${str(value)}" ${req}>`;
|
||||||
|
case "url":
|
||||||
|
return html`<label for="${i}">${label}</label><input id="${i}" name="${name}" type="url" value="${str(value)}" ${req}>`;
|
||||||
|
case "link":
|
||||||
|
return html`<label for="${i}">${label}</label><input id="${i}" name="${name}" list="note-names" value="${linkName(value)}" placeholder="A note's name" ${req}>`;
|
||||||
|
case "image":
|
||||||
|
return imagePicker(name, str(value), ctx, label);
|
||||||
|
default:
|
||||||
|
return html`<label for="${i}">${label}</label><input id="${i}" name="${name}" value="${str(value)}" ${req}>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldBlock(key: string, p: Prop, value: unknown, ctx: Ctx, required: boolean): Html {
|
||||||
|
const kind = kindOf(p);
|
||||||
|
const label = labelFor(key, p);
|
||||||
|
const i = id(key);
|
||||||
|
switch (kind) {
|
||||||
|
case "lines":
|
||||||
|
case "links": {
|
||||||
|
const list = Array.isArray(value) ? value.map((v) => (kind === "links" ? linkName(v) : str(v))) : [];
|
||||||
|
return html`<div class="field"><label for="${i}">${label}</label>
|
||||||
|
<textarea id="${i}" name="${key}" rows="${Math.max(3, list.length + 1)}" ${kind === "links" ? html`placeholder="One note name per line"` : html`placeholder="One per line"`}>${list.join("\n")}</textarea>
|
||||||
|
${hint(p)}</div>`;
|
||||||
|
}
|
||||||
|
case "choices": {
|
||||||
|
const set = new Set(Array.isArray(value) ? value.map(String) : []);
|
||||||
|
return html`<fieldset class="field"><legend>${label}</legend>${(p.items?.enum ?? []).map(
|
||||||
|
(o) => html`<label class="check"><input type="checkbox" name="${key}" value="${String(o)}" ${set.has(String(o)) ? html`checked` : ""}> ${String(o)}</label>`,
|
||||||
|
)}${hint(p)}</fieldset>`;
|
||||||
|
}
|
||||||
|
case "images": {
|
||||||
|
const list = Array.isArray(value) ? value.map(str) : [];
|
||||||
|
return html`<fieldset class="field rows"><legend>${label}</legend>${hint(p)}
|
||||||
|
${list.map(
|
||||||
|
(v, n) => html`<div class="row-item">
|
||||||
|
${imagePicker(`${key}.${n}`, v, ctx, `Image ${n + 1}`)}
|
||||||
|
${rowTools(key, n, list.length)}
|
||||||
|
</div>`,
|
||||||
|
)}
|
||||||
|
<label for="${i}-add">Add images</label>
|
||||||
|
<input id="${i}-add" name="upload.${key}" type="file" accept="${ACCEPT}" multiple>
|
||||||
|
</fieldset>`;
|
||||||
|
}
|
||||||
|
case "rows": {
|
||||||
|
const list = Array.isArray(value) ? (value as Record<string, unknown>[]) : [];
|
||||||
|
const subs = Object.entries(p.items?.properties ?? {});
|
||||||
|
const req = new Set(p.items?.required ?? []);
|
||||||
|
const row = (r: Record<string, unknown>, n: number, blank: boolean) => html`<div class="row-item ${blank ? "blank" : ""}">
|
||||||
|
${blank ? html`<p class="hint">Add one</p>` : ""}
|
||||||
|
<div class="row-fields">${subs.map(([k, s]) => {
|
||||||
|
const sk = kindOf(s, true);
|
||||||
|
const name = `${key}.${n}.${k}`;
|
||||||
|
if (sk === "lines") {
|
||||||
|
const l = Array.isArray(r[k]) ? (r[k] as unknown[]).map(str) : [];
|
||||||
|
return html`<div class="field"><label for="${id(name)}">${labelFor(k, s)}</label><textarea id="${id(name)}" name="${name}" rows="2" placeholder="One per line">${l.join("\n")}</textarea></div>`;
|
||||||
|
}
|
||||||
|
if (sk === "choices") return html`<div class="field">${labelFor(k, s)}: edit in Obsidian</div>`;
|
||||||
|
return html`<div class="field">${input(name, sk, s, r[k], ctx, labelFor(k, s), !blank && req.has(k))}</div>`;
|
||||||
|
})}</div>
|
||||||
|
${blank ? "" : rowTools(key, n, list.length)}
|
||||||
|
</div>`;
|
||||||
|
return html`<fieldset class="field rows"><legend>${label}</legend>${hint(p)}
|
||||||
|
${list.map((r, n) => row(r, n, false))}
|
||||||
|
${row({}, list.length, true)}
|
||||||
|
</fieldset>`;
|
||||||
|
}
|
||||||
|
case "group": {
|
||||||
|
const g = (value ?? {}) as Record<string, unknown>;
|
||||||
|
const req = new Set(p.required ?? []);
|
||||||
|
return html`<fieldset class="field"><legend>${label}</legend>${hint(p)}<div class="row-fields">${Object.entries(p.properties ?? {}).map(
|
||||||
|
([k, s]) => html`<div class="field">${input(`${key}.${k}`, kindOf(s, true), s, g[k], ctx, labelFor(k, s), req.has(k))}</div>`,
|
||||||
|
)}</div></fieldset>`;
|
||||||
|
}
|
||||||
|
case "other":
|
||||||
|
return value === undefined
|
||||||
|
? html``
|
||||||
|
: html`<div class="field"><span class="label">${label}</span><p class="hint">This field is edited in Obsidian.</p></div>`;
|
||||||
|
default:
|
||||||
|
return html`<div class="field">${input(key, kind, p, value, ctx, label, required)}${kind === "bool" ? "" : hint(p)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowTools(key: string, n: number, count: number): Html {
|
||||||
|
return html`<div class="row-tools">
|
||||||
|
<button type="submit" name="move" value="${key}:${n}:up" formnovalidate ${n === 0 ? html`disabled` : ""}>Move up</button>
|
||||||
|
<button type="submit" name="move" value="${key}:${n}:down" formnovalidate ${n === count - 1 ? html`disabled` : ""}>Move down</button>
|
||||||
|
<label class="check"><input type="checkbox" name="${key}.${n}.remove" value="1"> Remove</label>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function noteEditor(d: EditorDraft, ctx: Ctx): Html {
|
||||||
|
const def = noteTypes[d.type as NoteType];
|
||||||
|
const { properties, required } = shapeOf(d.type);
|
||||||
|
const req = new Set(required);
|
||||||
|
const images = { ...ctx, images: ctx.images.filter((a) => isImage(a.name)) };
|
||||||
|
const own = Object.entries(properties).filter(([k]) => !OWNED.has(k) && !SHARED.includes(k));
|
||||||
|
const shared = SHARED.filter((k) => properties[k]).map((k) => [k, properties[k]!] as const);
|
||||||
|
return html`<h1>${d.path ? html`Edit ${d.title}` : html`New ${def.label.toLowerCase()}`}</h1>
|
||||||
|
<p class="lede">${def.description} ${d.path ? html`Saved as <code>${d.path}</code>. <a href="${noteHref(d.path)}">Note and history</a>` : ""}</p>
|
||||||
|
<form method="post" action="/edit/save" enctype="multipart/form-data" class="note-form">
|
||||||
|
<input type="hidden" name="type" value="${d.type}">
|
||||||
|
${d.path ? html`<input type="hidden" name="path" value="${d.path}">` : ""}
|
||||||
|
<div class="post-grid">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="field"><label for="e-title">Title</label>
|
||||||
|
<input id="e-title" name="title" value="${d.title}" required ${d.path ? html`readonly aria-describedby="e-title-note"` : ""}>
|
||||||
|
${d.path ? html`<p class="hint" id="e-title-note">The title is the note's file name; rename it in Obsidian.</p>` : ""}</div>
|
||||||
|
${own.map(([k, p]) => fieldBlock(k, p, d.data[k], images, req.has(k)))}
|
||||||
|
${def.body !== null
|
||||||
|
? html`<div class="field"><label for="e-body">${d.type === "page" ? "Introduction" : "Body"}</label>
|
||||||
|
<textarea id="e-body" name="body" rows="14" class="body-text">${d.body}</textarea>
|
||||||
|
<p class="hint">${def.body} Markdown; embed a vault image with ![[file.jpg]].</p></div>`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<div class="side">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>On the site</h2>
|
||||||
|
${shared.map(([k, p]) => fieldBlock(k, p, k === "publish" ? d.publish : d.data[k], images, false))}
|
||||||
|
</div>
|
||||||
|
<div class="panel actions stack">
|
||||||
|
<button class="primary" name="action" value="save">Save</button>
|
||||||
|
${d.path ? html`<a href="${noteHref(d.path)}">Cancel</a>` : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<datalist id="note-names">${ctx.notes.map((n) => html`<option value="${n}">`)}</datalist>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Before launch: what on the live site is still a placeholder, worked out
|
||||||
|
* from the notes themselves so it stays true as they change. Pages using
|
||||||
|
* their built-in wording, notes that still hold placeholder text, posts not
|
||||||
|
* published yet, and the thin spots the page designs expect to be filled.
|
||||||
|
*/
|
||||||
|
import type { FullNote } from "./client.ts";
|
||||||
|
import { editHref, newHref } from "./editor.ts";
|
||||||
|
import { html, type Html } from "./html.ts";
|
||||||
|
import { sitePages } from "./sitepages.ts";
|
||||||
|
|
||||||
|
export interface Finding {
|
||||||
|
what: string;
|
||||||
|
/** The note to open, or a page to add. */
|
||||||
|
href: string;
|
||||||
|
action: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Section {
|
||||||
|
title: string;
|
||||||
|
hint: string;
|
||||||
|
findings: Finding[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Text that marks something as not written yet. */
|
||||||
|
const PLACEHOLDER = /<!--|\bcoming soon\b|\bplaceholder\b|\blorem\b|\bTODO\b|\bTBD\b/i;
|
||||||
|
/** Pages whose lede or portrait the site fills in when the note has none. */
|
||||||
|
const BUILT_IN = new Set(["/", "/about", "/now", "/blog", "/library", "/library/shelf", "/inquiries", "/tabletop", "/workshop", "/work", "/store", "/404"]);
|
||||||
|
|
||||||
|
const fieldsText = (n: FullNote) => JSON.stringify(n.data);
|
||||||
|
const edit = (n: FullNote) => (n.type === "post" ? `/posts/edit?path=${encodeURIComponent(n.path)}` : editHref(n.path));
|
||||||
|
|
||||||
|
export function launchCheck(notes: FullNote[]): Section[] {
|
||||||
|
const live = notes.filter((n) => n.publish);
|
||||||
|
const of = (type: string, list = live) => list.filter((n) => n.type === type);
|
||||||
|
const pageNotes = of("page", notes);
|
||||||
|
|
||||||
|
const pages: Finding[] = [];
|
||||||
|
for (const p of sitePages) {
|
||||||
|
if (!BUILT_IN.has(p.page)) continue;
|
||||||
|
const n = pageNotes.find((x) => x.data.page === p.page);
|
||||||
|
if (!n) pages.push({ what: `${p.name} (${p.page}) has no Page text note, so it shows its built-in wording.`, href: newHref("page", { page: p.page, title: p.name }), action: "Add page text" });
|
||||||
|
else if (!n.publish) pages.push({ what: `${p.name} (${p.page}): its Page text note is a draft, so the page shows its built-in wording.`, href: edit(n), action: "Edit" });
|
||||||
|
else if (!n.data.lede && p.page !== "/") pages.push({ what: `${p.name} (${p.page}) has no lede, so it shows the built-in one.`, href: edit(n), action: "Edit" });
|
||||||
|
}
|
||||||
|
const about = pageNotes.find((x) => x.data.page === "/about");
|
||||||
|
if (!about?.data.portrait) pages.push({ what: "About has no portrait, so star-chart art stands in.", href: about ? edit(about) : newHref("page", { page: "/about", title: "About" }), action: about ? "Edit" : "Add page text" });
|
||||||
|
|
||||||
|
const text = notes
|
||||||
|
.filter((n) => !n.path.startsWith("_templates/") && (PLACEHOLDER.test(n.body) || PLACEHOLDER.test(fieldsText(n))))
|
||||||
|
.map((n) => ({ what: `${n.title}${n.publish ? "" : " (draft)"}: ${PLACEHOLDER.exec(n.body)?.[0] ?? PLACEHOLDER.exec(fieldsText(n))?.[0]}`, href: edit(n), action: "Edit" }));
|
||||||
|
|
||||||
|
const drafts = of("post", notes)
|
||||||
|
.filter((n) => !n.publish)
|
||||||
|
.map((n) => ({ what: `${n.title}${n.data.postType ? ` (${String(n.data.postType)})` : ""}`, href: edit(n), action: "Finish" }));
|
||||||
|
|
||||||
|
const thin: Finding[] = [
|
||||||
|
...of("craft").filter((n) => !n.data.blurb).map((n) => ({ what: `Craft ${n.title} has no blurb.`, href: edit(n), action: "Edit" })),
|
||||||
|
...of("make").filter((n) => !n.data.date).map((n) => ({ what: `Make ${n.title} has no finish date, so Recently finished can't order it.`, href: edit(n), action: "Edit" })),
|
||||||
|
...of("campaign").filter((n) => !n.data.blurb).map((n) => ({ what: `Campaign ${n.title} has no blurb.`, href: edit(n), action: "Edit" })),
|
||||||
|
...of("deck").filter((n) => !n.data.take).map((n) => ({ what: `Deck ${n.title} has no take.`, href: edit(n), action: "Edit" })),
|
||||||
|
...of("card-game").filter((n) => !n.data.blurb).map((n) => ({ what: `Card game ${n.title} has no blurb.`, href: edit(n), action: "Edit" })),
|
||||||
|
...of("experience").filter((n) => !n.data.focus && !n.data.earlier).map((n) => ({ what: `${n.title} has no focus line.`, href: edit(n), action: "Edit" })),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ title: "Pages using built-in wording", hint: "Write the page's text so the site says it in your words.", findings: pages },
|
||||||
|
{ title: "Placeholder text in notes", hint: "Notes holding a comment, Coming soon, TODO or similar. A draft note isn't on the site, but it still needs finishing before it goes live.", findings: text },
|
||||||
|
{ title: "Articles not published", hint: "Drafts on the Blog and Work pages.", findings: drafts },
|
||||||
|
{ title: "Thin spots", hint: "Live notes missing a field their page is designed around.", findings: thin },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function launchPage(sections: Section[]): Html {
|
||||||
|
const total = sections.reduce((n, s) => n + s.findings.length, 0);
|
||||||
|
return html`<h1>Before launch</h1>
|
||||||
|
<p class="lede">${total ? html`${total} things are still placeholders or unfinished. The list comes from the notes, so it shrinks as you fix them.` : "Nothing left: every page uses your words and nothing holds placeholder text."}</p>
|
||||||
|
${sections.map(
|
||||||
|
(s) => html`<section class="panel launch">
|
||||||
|
<h2>${s.title} <span class="count-pill">${s.findings.length}</span></h2>
|
||||||
|
<p class="hint">${s.hint}</p>
|
||||||
|
${s.findings.length
|
||||||
|
? html`<ul>${s.findings.map((f) => html`<li><span>${f.what}</span> <a href="${f.href}">${f.action}</a></li>`)}</ul>`
|
||||||
|
: html`<p class="muted">All done.</p>`}
|
||||||
|
</section>`,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ export interface Flash {
|
|||||||
|
|
||||||
const nav: [string, string][] = [
|
const nav: [string, string][] = [
|
||||||
["/", "Overview"],
|
["/", "Overview"],
|
||||||
|
["/site", "Site pages"],
|
||||||
|
["/launch", "Before launch"],
|
||||||
["/review", "Review"],
|
["/review", "Review"],
|
||||||
["/inbox", "Inbox"],
|
["/inbox", "Inbox"],
|
||||||
["/notes", "Notes"],
|
["/notes", "Notes"],
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { html, qs, raw, when, type Html } from "./html.ts";
|
|||||||
import { renderMarkdown } from "./markdown.ts";
|
import { renderMarkdown } from "./markdown.ts";
|
||||||
|
|
||||||
export const noteHref = (path: string) => `/note${qs({ path })}`;
|
export const noteHref = (path: string) => `/note${qs({ path })}`;
|
||||||
|
/** The editor for a note: posts have their own. */
|
||||||
|
const editLink = (n: { path: string; type: string }) => (n.type === "post" ? `/posts/edit${qs({ path: n.path })}` : `/edit${qs({ path: n.path })}`);
|
||||||
|
|
||||||
const authorLabel: Record<string, string> = { greg: "You", agent: "An agent", admin: "Admin page" };
|
const authorLabel: Record<string, string> = { greg: "You", agent: "An agent", admin: "Admin page" };
|
||||||
const who = (a: string) => authorLabel[a] ?? a;
|
const who = (a: string) => authorLabel[a] ?? a;
|
||||||
@@ -134,9 +136,10 @@ export function notesList(p: { title: string; lede: string; action: string; note
|
|||||||
)}</select></div>
|
)}</select></div>
|
||||||
<button>Show</button>
|
<button>Show</button>
|
||||||
</form>
|
</form>
|
||||||
|
${p.type ? html`<p><a class="button" href="${p.type === "post" ? "/posts/new" : `/edit/new${qs({ type: p.type })}`}">Add a ${p.type} note</a></p>` : ""}
|
||||||
${p.notes.length
|
${p.notes.length
|
||||||
? html`<div class="table-wrap"><table><thead><tr><th>Title</th><th>Type</th><th>State</th><th>Changed</th></tr></thead><tbody>
|
? html`<div class="table-wrap"><table><thead><tr><th>Title</th><th>Type</th><th>State</th><th>Changed</th><th></th></tr></thead><tbody>
|
||||||
${p.notes.map((n) => html`<tr><td><a href="${noteHref(n.path)}">${n.title}</a><div class="muted mono">${n.path}</div></td><td>${n.type}</td><td>${publishPill(n.publish)}</td><td>${when(n.acceptedAt)}</td></tr>`)}
|
${p.notes.map((n) => html`<tr><td><a href="${noteHref(n.path)}">${n.title}</a><div class="muted mono">${n.path}</div></td><td>${n.type}</td><td>${publishPill(n.publish)}</td><td>${when(n.acceptedAt)}</td><td><a href="${editLink(n)}">Edit</a></td></tr>`)}
|
||||||
</tbody></table></div>`
|
</tbody></table></div>`
|
||||||
: html`<p class="muted">No notes match.</p>`}`;
|
: html`<p class="muted">No notes match.</p>`}`;
|
||||||
}
|
}
|
||||||
@@ -150,10 +153,11 @@ export function noteView(n: Note, history: Revision[]): Html {
|
|||||||
<p class="lede">${publishPill(n.publish)} ${n.type} · <span class="mono">${n.path}</span> · last changed by ${who(n.author).toLowerCase()} ${when(n.acceptedAt)}</p>
|
<p class="lede">${publishPill(n.publish)} ${n.type} · <span class="mono">${n.path}</span> · last changed by ${who(n.author).toLowerCase()} ${when(n.acceptedAt)}</p>
|
||||||
${n.pending ? html`<div class="flash">A change to this note is waiting: <a href="/review/${n.pending.id}">review it</a>.</div>` : ""}
|
${n.pending ? html`<div class="flash">A change to this note is waiting: <a href="/review/${n.pending.id}">review it</a>.</div>` : ""}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
<a class="button primary" href="${editLink(n)}">Edit</a>
|
||||||
<form method="post" action="/note/publish" class="inline">
|
<form method="post" action="/note/publish" class="inline">
|
||||||
<input type="hidden" name="path" value="${n.path}">
|
<input type="hidden" name="path" value="${n.path}">
|
||||||
<input type="hidden" name="publish" value="${n.publish ? "0" : "1"}">
|
<input type="hidden" name="publish" value="${n.publish ? "0" : "1"}">
|
||||||
<button class="${n.publish ? "" : "primary"}">${n.publish ? "Unpublish" : "Publish"}</button>
|
<button>${n.publish ? "Unpublish" : "Publish"}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<h2>Preview</h2>
|
<h2>Preview</h2>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import { layout, type Flash } from "./layout.ts";
|
|||||||
import * as pages from "./pages.ts";
|
import * as pages from "./pages.ts";
|
||||||
import * as inbox from "./inbox.ts";
|
import * as inbox from "./inbox.ts";
|
||||||
import * as posts from "./posts.ts";
|
import * as posts from "./posts.ts";
|
||||||
|
import * as editor from "./editor.ts";
|
||||||
|
import { sitePagesView } from "./sitepages.ts";
|
||||||
|
import { launchCheck, launchPage } from "./launch.ts";
|
||||||
import { suggestionChange, suggestionsPanel } from "./suggestions.ts";
|
import { suggestionChange, suggestionsPanel } from "./suggestions.ts";
|
||||||
|
|
||||||
const css = readFileSync(new URL("../public/admin.css", import.meta.url), "utf8");
|
const css = readFileSync(new URL("../public/admin.css", import.meta.url), "utf8");
|
||||||
@@ -127,6 +130,26 @@ export function createAdminServer(api: ContentApi, opts: { now?: () => Date } =
|
|||||||
if (note.type !== "post") return new Redirect(pages.noteHref(note.path));
|
if (note.type !== "post") return new Redirect(pages.noteHref(note.path));
|
||||||
return editorPage(posts.draftFromNote(note));
|
return editorPage(posts.draftFromNote(note));
|
||||||
}
|
}
|
||||||
|
case "/site": {
|
||||||
|
const all = await Promise.all(["page", "site-settings", "store-settings", "status-kinds", "skills", "education"].map((t) => api.fullNotes(t)));
|
||||||
|
const singles = new Map(all.slice(1).flatMap((l) => (l[0] ? [[l[0].type, l[0]] as const] : [])));
|
||||||
|
return { title: "Site pages", body: sitePagesView(all[0]!, singles) };
|
||||||
|
}
|
||||||
|
case "/launch":
|
||||||
|
return { title: "Before launch", body: launchPage(launchCheck(await api.allNotes())) };
|
||||||
|
case "/edit": {
|
||||||
|
const note = await api.note(required(q, "path"));
|
||||||
|
if (note.type === "post") return new Redirect(posts.editHref(note.path));
|
||||||
|
return noteEditorPage(editor.draftFromNote(note));
|
||||||
|
}
|
||||||
|
case "/edit/new": {
|
||||||
|
const type = required(q, "type");
|
||||||
|
if (type === "post") return new Redirect("/posts/new");
|
||||||
|
if (!noteTypeNames.includes(type as never)) throw new ApiError(404, `There is no note type ${type}.`, null);
|
||||||
|
const data: Record<string, unknown> = {};
|
||||||
|
if (type === "page" && q.get("page")) data.page = q.get("page");
|
||||||
|
return noteEditorPage({ type, title: q.get("title") ?? "", publish: false, data, body: "" });
|
||||||
|
}
|
||||||
case "/images": {
|
case "/images": {
|
||||||
const folder = q.get("folder") ?? "";
|
const folder = q.get("folder") ?? "";
|
||||||
return { title: "Images", body: posts.imagesPage(await api.attachments(), folder === "-" ? "" : folder) };
|
return { title: "Images", body: posts.imagesPage(await api.attachments(), folder === "-" ? "" : folder) };
|
||||||
@@ -156,6 +179,45 @@ export function createAdminServer(api: ContentApi, opts: { now?: () => Date } =
|
|||||||
return { title: d.title || "Write a post", body: posts.postEditor(d, { images, notes: names, showPreview }) };
|
return { title: d.title || "Write a post", body: posts.postEditor(d, { images, notes: names, showPreview }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function noteEditorPage(d: editor.EditorDraft, reasons: string[] = []): Promise<Page> {
|
||||||
|
const [images, notes] = await Promise.all([api.attachments(), api.notes({ limit: 5000 })]);
|
||||||
|
const names = [...new Set(notes.filter((n) => n.type !== "status").map((n) => n.title))].sort((a, b) => a.localeCompare(b));
|
||||||
|
const body = editor.noteEditor(d, { images, notes: names });
|
||||||
|
return {
|
||||||
|
title: d.title || "New note",
|
||||||
|
body: reasons.length ? html`<div class="flash err" role="alert">Not saved.<ul>${reasons.map((r) => html`<li>${r}</li>`)}</ul></div>${body}` : body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save the note editor: upload chosen images, then reorder a row or save. */
|
||||||
|
async function saveNote(f: Form, files: Upload[]): Promise<Page | Redirect> {
|
||||||
|
const type = required(f, "type");
|
||||||
|
if (!noteTypeNames.includes(type as never)) throw new ApiError(400, `There is no note type ${type}.`, null);
|
||||||
|
const path = f.get("path") || undefined;
|
||||||
|
const uploaded = new Map<string, string[]>();
|
||||||
|
for (const u of files.filter((x) => x.field.startsWith("upload."))) {
|
||||||
|
const key = u.field.slice("upload.".length);
|
||||||
|
uploaded.set(key, [...(uploaded.get(key) ?? []), await api.upload(editor.uploadFolder(type), u.name, u.bytes)]);
|
||||||
|
}
|
||||||
|
let values = editor.formData(type, f, uploaded);
|
||||||
|
const title = (f.get("title") ?? "").trim();
|
||||||
|
const body = (f.get("body") ?? "").replace(/\r\n/g, "\n");
|
||||||
|
const draft = (): editor.EditorDraft => ({ type, path, title, publish: values.publish === true, data: values, body });
|
||||||
|
const move = f.get("move");
|
||||||
|
if (move) {
|
||||||
|
values = editor.moveRow(values, move);
|
||||||
|
return noteEditorPage(draft());
|
||||||
|
}
|
||||||
|
if (!title) return noteEditorPage(draft(), ["A note needs a title."]);
|
||||||
|
const fields = editor.changeFields(values, !!path);
|
||||||
|
const text = editor.hasBody(type) ? body : undefined;
|
||||||
|
const o = path
|
||||||
|
? await api.apply({ op: "update", path, fields, body: text })
|
||||||
|
: await api.apply({ op: "create", data: { type, title, ...fields }, body: text });
|
||||||
|
if (o.status === "rejected") return noteEditorPage(draft(), o.reasons.map((r) => r.message));
|
||||||
|
return after(editor.editHref(o.path), o, "Saved. The site shows it within a minute.");
|
||||||
|
}
|
||||||
|
|
||||||
/** Save the post form: upload its images, then preview, save a draft or publish. */
|
/** Save the post form: upload its images, then preview, save a draft or publish. */
|
||||||
async function savePost(f: Form, files: Upload[]): Promise<Page | Redirect> {
|
async function savePost(f: Form, files: Upload[]): Promise<Page | Redirect> {
|
||||||
const d = posts.draftFromForm(f);
|
const d = posts.draftFromForm(f);
|
||||||
@@ -185,6 +247,8 @@ export function createAdminServer(api: ContentApi, opts: { now?: () => Date } =
|
|||||||
switch (path) {
|
switch (path) {
|
||||||
case "/posts/save":
|
case "/posts/save":
|
||||||
return savePost(f, files);
|
return savePost(f, files);
|
||||||
|
case "/edit/save":
|
||||||
|
return saveNote(f, files);
|
||||||
case "/images/upload": {
|
case "/images/upload": {
|
||||||
const folder = f.get("folder") || "Posts";
|
const folder = f.get("folder") || "Posts";
|
||||||
const added: string[] = [];
|
const added: string[] = [];
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* The Site pages view: each page of the public site, the Page text note
|
||||||
|
* behind it (its lede, introduction and photos) and the kinds of notes that
|
||||||
|
* fill it, so changing what a page shows starts from the page itself.
|
||||||
|
*/
|
||||||
|
import { noteTypes, type NoteType } from "@gk/schema";
|
||||||
|
import type { FullNote } from "./client.ts";
|
||||||
|
import { editHref, linkName, newHref } from "./editor.ts";
|
||||||
|
import { html, qs, type Html } from "./html.ts";
|
||||||
|
|
||||||
|
export interface SitePage {
|
||||||
|
page: string;
|
||||||
|
name: string;
|
||||||
|
/** Note types whose notes appear on the page. */
|
||||||
|
feeds: NoteType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sitePages: SitePage[] = [
|
||||||
|
{ page: "/", name: "Landing", feeds: ["site-settings"] },
|
||||||
|
{ page: "/about", name: "About", feeds: [] },
|
||||||
|
{ page: "/now", name: "Now", feeds: ["status", "status-kinds"] },
|
||||||
|
{ page: "/work", name: "Work", feeds: ["post", "experience"] },
|
||||||
|
{ page: "/resume", name: "Resume", feeds: ["experience", "skills", "education", "site-settings"] },
|
||||||
|
{ page: "/workshop", name: "Workshop", feeds: ["craft", "make", "station", "tool"] },
|
||||||
|
{ page: "/workshop/makes", name: "All makes", feeds: ["make"] },
|
||||||
|
{ page: "/library", name: "Library", feeds: ["library-item"] },
|
||||||
|
{ page: "/library/shelf", name: "Shelf", feeds: ["board-game"] },
|
||||||
|
{ page: "/tabletop", name: "Tabletop", feeds: ["tabletop-system", "campaign", "card-game", "deck"] },
|
||||||
|
{ page: "/blog", name: "Blog", feeds: ["post"] },
|
||||||
|
{ page: "/store", name: "Store", feeds: ["product-line", "store-settings"] },
|
||||||
|
{ page: "/inquiries", name: "Inquiries", feeds: ["site-settings"] },
|
||||||
|
{ page: "/404", name: "Not found", feeds: [] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const imageSrc = (v: string) => (/^https:\/\//.test(v) ? v : `/attachments/${encodeURIComponent(v)}`);
|
||||||
|
|
||||||
|
/** Every image a Page text note shows: portrait, cover and photos. */
|
||||||
|
export function pageImages(n: FullNote): { label: string; file: string }[] {
|
||||||
|
const d = n.data;
|
||||||
|
const out: { label: string; file: string }[] = [];
|
||||||
|
if (d.portrait) out.push({ label: "Portrait", file: linkName(d.portrait) });
|
||||||
|
if (d.cover) out.push({ label: "Cover", file: linkName(d.cover) });
|
||||||
|
if (Array.isArray(d.photos))
|
||||||
|
for (const p of d.photos as { file?: unknown; caption?: unknown; alt?: unknown }[])
|
||||||
|
if (p?.file) out.push({ label: String(p.caption ?? p.alt ?? "Photo"), file: linkName(p.file) });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singletons = new Set<string>(["site-settings", "store-settings", "status-kinds", "skills", "education"]);
|
||||||
|
|
||||||
|
function feedLink(type: NoteType, singles: Map<string, FullNote>): Html {
|
||||||
|
const def = noteTypes[type];
|
||||||
|
const one = singles.get(type);
|
||||||
|
if (singletons.has(type)) {
|
||||||
|
return one
|
||||||
|
? html`<a class="pill-link" href="${editHref(one.path)}">${def.label}</a>`
|
||||||
|
: html`<a class="pill-link" href="${newHref(type)}">${def.label} (add)</a>`;
|
||||||
|
}
|
||||||
|
return html`<a class="pill-link" href="/notes${qs({ type })}">${def.label}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sitePagesView(pageNotes: FullNote[], singles: Map<string, FullNote>): Html {
|
||||||
|
const byPage = new Map(pageNotes.map((n) => [String(n.data.page ?? ""), n]));
|
||||||
|
return html`<h1>Site pages</h1>
|
||||||
|
<p class="lede">Every page of the site with its text and images. Edit a page's lede, introduction and photos here; the lists on a page come from the notes linked under it.</p>
|
||||||
|
<ul class="site-pages">${sitePages.map((p) => {
|
||||||
|
const n = byPage.get(p.page);
|
||||||
|
const images = n ? pageImages(n) : [];
|
||||||
|
return html`<li class="panel">
|
||||||
|
<div class="head-row">
|
||||||
|
<h2>${p.name} <code>${p.page}</code></h2>
|
||||||
|
${n
|
||||||
|
? html`<a class="button primary" href="${editHref(n.path)}">Edit page</a>`
|
||||||
|
: html`<a class="button" href="${newHref("page", { page: p.page, title: p.name })}">Add page text</a>`}
|
||||||
|
</div>
|
||||||
|
${n
|
||||||
|
? html`<p>${n.data.lede ? String(n.data.lede) : html`<span class="muted">No lede yet.</span>`} ${n.publish ? "" : html`<span class="pill draft">Draft</span>`}</p>`
|
||||||
|
: html`<p class="muted">No Page text note yet, so the page uses its built-in wording.</p>`}
|
||||||
|
${images.length
|
||||||
|
? html`<ul class="page-images">${images.map(
|
||||||
|
(i) => html`<li><img src="${imageSrc(i.file)}" alt="" loading="lazy"><span>${i.label}</span></li>`,
|
||||||
|
)}</ul>`
|
||||||
|
: ""}
|
||||||
|
${p.feeds.length ? html`<p class="feeds"><span class="muted">Also on this page:</span> ${p.feeds.map((t) => feedLink(t, singles))}</p>` : ""}
|
||||||
|
</li>`;
|
||||||
|
})}</ul>`;
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ const post = async (path: string, form: Record<string, string>, headers: Record<
|
|||||||
headers: { "content-type": "application/x-www-form-urlencoded", "sec-fetch-site": "same-origin", ...headers },
|
headers: { "content-type": "application/x-www-form-urlencoded", "sec-fetch-site": "same-origin", ...headers },
|
||||||
body: new URLSearchParams(form).toString(),
|
body: new URLSearchParams(form).toString(),
|
||||||
});
|
});
|
||||||
return { status: res.status, location: res.headers.get("location") ?? "" };
|
return { status: res.status, location: res.headers.get("location") ?? "", text: await res.text() };
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("admin page", () => {
|
describe("admin page", () => {
|
||||||
@@ -149,4 +149,48 @@ describe("admin page", () => {
|
|||||||
expect(page.text).toContain("personalreader");
|
expect(page.text).toContain("personalreader");
|
||||||
expect(page.text).toContain("Suwayomi did not answer");
|
expect(page.text).toContain("Suwayomi did not answer");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("lists what is still a placeholder before launch", async () => {
|
||||||
|
await post("/edit/save", { type: "craft", title: "Leather", status: "Practicing", "has.publish": "1", publish: "1" });
|
||||||
|
const text = (await get("/launch")).text;
|
||||||
|
expect(text).toContain("Before launch");
|
||||||
|
expect(text).toContain("Blog (/blog) has no Page text note");
|
||||||
|
expect(text).toContain("Craft Leather has no blurb.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("edits a page's text and images from Site pages", async () => {
|
||||||
|
const site = await get("/site");
|
||||||
|
expect(site.text).toContain("Add page text");
|
||||||
|
expect((await get("/edit/new?type=page&page=%2Fabout&title=About")).text).toContain('value="/about"');
|
||||||
|
|
||||||
|
// A tiny PNG: the content service checks the file's first bytes.
|
||||||
|
const png = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0]);
|
||||||
|
const fd = new FormData();
|
||||||
|
for (const [k, v] of Object.entries({ type: "page", title: "About", page: "/about", lede: "Maker of things", "has.publish": "1", publish: "1", "photos.0.file": "", "photos.0.alt": "" })) fd.append(k, v);
|
||||||
|
fd.append("upload.portrait", new Blob([png], { type: "image/png" }), "Greg Portrait.png");
|
||||||
|
const res = await fetch(base + "/edit/save", { method: "POST", redirect: "manual", headers: { "sec-fetch-site": "same-origin" }, body: fd });
|
||||||
|
expect(res.status).toBe(303);
|
||||||
|
const note = r.store.list({ type: "page" }).find((n) => n.data.page === "/about")!;
|
||||||
|
expect(note.data).toMatchObject({ lede: "Maker of things", portrait: "[[greg-portrait.png]]" });
|
||||||
|
expect(note.publish).toBe(true);
|
||||||
|
expect((await get("/site")).text).toContain("/attachments/greg-portrait.png");
|
||||||
|
|
||||||
|
// Clearing a field removes it; a reorder only redraws the form.
|
||||||
|
await post("/edit/save", { type: "page", path: note.path, title: "About", page: "/about", lede: "", portrait: "greg-portrait.png", "has.publish": "1", publish: "1" });
|
||||||
|
expect(r.store.get(note.path)!.data.lede).toBeUndefined();
|
||||||
|
const moved = await post("/edit/save", {
|
||||||
|
type: "page",
|
||||||
|
path: note.path,
|
||||||
|
title: "About",
|
||||||
|
page: "/about",
|
||||||
|
"photos.0.file": "a.jpg",
|
||||||
|
"photos.0.alt": "A",
|
||||||
|
"photos.1.file": "b.jpg",
|
||||||
|
"photos.1.alt": "B",
|
||||||
|
move: "photos:1:up",
|
||||||
|
});
|
||||||
|
expect(moved.status).toBe(200);
|
||||||
|
expect(moved.text.indexOf('value="b.jpg"')).toBeLessThan(moved.text.indexOf('value="a.jpg"'));
|
||||||
|
expect(r.store.get(note.path)!.data.photos).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { changeFields, formData, kindOf, moveRow, noteEditor, shapeOf } from "../src/editor.ts";
|
||||||
|
|
||||||
|
const form = (pairs: [string, string][]) => new URLSearchParams(pairs);
|
||||||
|
|
||||||
|
describe("note editor fields", () => {
|
||||||
|
it("knows every field of every kind of page it edits", () => {
|
||||||
|
const p = shapeOf("page").properties;
|
||||||
|
expect(kindOf(p.portrait!)).toBe("image");
|
||||||
|
expect(kindOf(p.cover!)).toBe("image");
|
||||||
|
expect(kindOf(p.photos!)).toBe("rows");
|
||||||
|
expect(kindOf(p.links!)).toBe("links");
|
||||||
|
expect(kindOf(p.tags!)).toBe("lines");
|
||||||
|
expect(kindOf(shapeOf("make").properties.photos!)).toBe("images");
|
||||||
|
expect(kindOf(shapeOf("make").properties.date!)).toBe("date");
|
||||||
|
expect(kindOf(shapeOf("experience").properties.start!)).toBe("month");
|
||||||
|
expect(kindOf(shapeOf("product-line").properties.slots!)).toBe("group");
|
||||||
|
expect(kindOf(shapeOf("craft").properties.status!)).toBe("enum");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads a Page text form back into note values", () => {
|
||||||
|
const v = formData(
|
||||||
|
"page",
|
||||||
|
form([
|
||||||
|
["page", "/about"],
|
||||||
|
["lede", " Maker of things "],
|
||||||
|
["portrait", "greg.jpg"],
|
||||||
|
["photos.0.file", "kashi.jpg"],
|
||||||
|
["photos.0.alt", "Kashi asleep"],
|
||||||
|
["photos.0.caption", "Kashi"],
|
||||||
|
["photos.1.file", "tea.jpg"],
|
||||||
|
["photos.1.alt", "Tea"],
|
||||||
|
["photos.1.remove", "1"],
|
||||||
|
["photos.2.file", ""],
|
||||||
|
["photos.2.alt", ""],
|
||||||
|
["has.publish", "1"],
|
||||||
|
["publish", "1"],
|
||||||
|
["has.favorite", "1"],
|
||||||
|
["links", "Resin\n[[Galaxy dice]]\n"],
|
||||||
|
]),
|
||||||
|
new Map([["cover", ["new-cover.jpg"]]]),
|
||||||
|
);
|
||||||
|
expect(v).toMatchObject({
|
||||||
|
page: "/about",
|
||||||
|
lede: "Maker of things",
|
||||||
|
portrait: "[[greg.jpg]]",
|
||||||
|
cover: "[[new-cover.jpg]]",
|
||||||
|
photos: [{ file: "[[kashi.jpg]]", alt: "Kashi asleep", caption: "Kashi" }],
|
||||||
|
publish: true,
|
||||||
|
links: ["[[Resin]]", "[[Galaxy dice]]"],
|
||||||
|
});
|
||||||
|
expect(v.quickFacts).toBeUndefined();
|
||||||
|
expect(v.favorite).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds uploaded photos to an image list and keeps https images", () => {
|
||||||
|
const v = formData(
|
||||||
|
"make",
|
||||||
|
form([
|
||||||
|
["photos.0", "https://example.com/a.jpg"],
|
||||||
|
["photos.1", "b.jpg"],
|
||||||
|
["photos.1.remove", "1"],
|
||||||
|
]),
|
||||||
|
new Map([["photos", ["c.jpg", "d.jpg"]]]),
|
||||||
|
);
|
||||||
|
expect(v.photos).toEqual(["https://example.com/a.jpg", "[[c.jpg]]", "[[d.jpg]]"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears emptied fields on an update and leaves them out on a create", () => {
|
||||||
|
const v = { lede: "Hi", portrait: undefined, photos: undefined };
|
||||||
|
expect(changeFields(v, true)).toEqual({ lede: "Hi", portrait: null, photos: null });
|
||||||
|
expect(changeFields(v, false)).toEqual({ lede: "Hi" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves a row up or down", () => {
|
||||||
|
const v = { photos: ["a", "b", "c"] };
|
||||||
|
expect(moveRow(v, "photos:2:up").photos).toEqual(["a", "c", "b"]);
|
||||||
|
expect(moveRow(v, "photos:0:down").photos).toEqual(["b", "a", "c"]);
|
||||||
|
expect(moveRow(v, "photos:0:up").photos).toEqual(["a", "b", "c"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a picker with a thumbnail for each image", () => {
|
||||||
|
const page = String(
|
||||||
|
noteEditor(
|
||||||
|
{ type: "page", path: "Pages/About.md", title: "About", publish: true, data: { page: "/about", portrait: "[[greg.jpg]]", photos: [{ file: "[[kashi.jpg]]", alt: "Kashi" }] }, body: "Hello" },
|
||||||
|
{ images: [{ name: "greg.jpg", folder: "Pages", size: 1, modified: "", usedBy: [] }], notes: [] },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(page).toContain('<img src="/attachments/greg.jpg"');
|
||||||
|
expect(page).toContain('<img src="/attachments/kashi.jpg"');
|
||||||
|
expect(page).toContain('name="upload.portrait"');
|
||||||
|
expect(page).toContain('value="photos:0:down"');
|
||||||
|
expect(page).toContain(">Hello</textarea>");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,8 +6,12 @@ import SidePanel from "./SidePanel.astro";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: Connected[];
|
items: Connected[];
|
||||||
|
/** A roundup links many pages; past this many the panel counts the rest. */
|
||||||
|
max?: number;
|
||||||
}
|
}
|
||||||
const { items } = Astro.props;
|
const { items: all, max = 8 } = Astro.props;
|
||||||
|
const items = all.length > max + 1 ? all.slice(0, max) : all;
|
||||||
|
const more = all.length - items.length;
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
items.length > 0 && (
|
items.length > 0 && (
|
||||||
@@ -26,11 +30,17 @@ const { items } = Astro.props;
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
{more > 0 && <p class="more">And {more} more, each linked in the post.</p>}
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.more {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--gk-muted);
|
||||||
|
}
|
||||||
.cp {
|
.cp {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const facts = d.facts ?? [];
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
border-left: 1px solid rgba(201, 162, 74, 0.45);
|
border-left: 1px solid rgba(var(--gk-gold-rgb), 0.45);
|
||||||
}
|
}
|
||||||
dt {
|
dt {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
* blog's seal art stands in.
|
* blog's seal art stands in.
|
||||||
*/
|
*/
|
||||||
import Button from "@gk/ui/components/Button.astro";
|
import Button from "@gk/ui/components/Button.astro";
|
||||||
import Chip from "@gk/ui/components/Chip.astro";
|
|
||||||
import GiltCorners from "@gk/ui/components/GiltCorners.astro";
|
import GiltCorners from "@gk/ui/components/GiltCorners.astro";
|
||||||
|
import Motif from "@gk/ui/components/Motif.astro";
|
||||||
import Seal from "@gk/ui/components/Seal.astro";
|
import Seal from "@gk/ui/components/Seal.astro";
|
||||||
import Star from "@gk/ui/components/Star.astro";
|
import Star from "@gk/ui/components/Star.astro";
|
||||||
import StarPath from "@gk/ui/components/StarPath.astro";
|
import StarPath from "@gk/ui/components/StarPath.astro";
|
||||||
@@ -16,6 +16,7 @@ import TickScale from "@gk/ui/components/TickScale.astro";
|
|||||||
import { filamentBanner } from "@gk/ui/components/seals.ts";
|
import { filamentBanner } from "@gk/ui/components/seals.ts";
|
||||||
import { dateLine, nowStatuses, pageLinks, type Post } from "../../lib/blog.ts";
|
import { dateLine, nowStatuses, pageLinks, type Post } from "../../lib/blog.ts";
|
||||||
import { mediaSrc, type SiteIndex } from "../../lib/site.ts";
|
import { mediaSrc, type SiteIndex } from "../../lib/site.ts";
|
||||||
|
import PageChips from "./PageChips.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
post: Post;
|
post: Post;
|
||||||
@@ -39,10 +40,10 @@ const now = nowStatuses(s, post);
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<img src={filamentBanner} alt="" aria-hidden="true" class="banner" />
|
<Motif src={filamentBanner} alt="" aria-hidden="true" class="banner" />
|
||||||
<StarPath strength="wild" steps={7} centerStar={false} class="path" seed={post.slug} />
|
<StarPath strength="wild" steps={7} centerStar={false} class="path" seed={post.slug} />
|
||||||
<TickRing size={196} class="ring">
|
<TickRing size={196} class="ring">
|
||||||
<Seal name="transmutation" size={150} class="seal" />
|
<Seal name="transmutation" size={150} class="seal" foil />
|
||||||
</TickRing>
|
</TickRing>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -62,7 +63,7 @@ const now = nowStatuses(s, post);
|
|||||||
<h2 id="lead-title" class="title"><a href={href}>{post.title}</a></h2>
|
<h2 id="lead-title" class="title"><a href={href}>{post.title}</a></h2>
|
||||||
<span class="underline" aria-hidden="true"></span>
|
<span class="underline" aria-hidden="true"></span>
|
||||||
{post.data.summary && <p class="summary">{post.data.summary}</p>}
|
{post.data.summary && <p class="summary">{post.data.summary}</p>}
|
||||||
{links.length > 0 && <span class="chips">{links.map((l) => <Chip href={l.href}>{l.label}</Chip>)}</span>}
|
{links.length > 0 && <span class="chips"><PageChips links={links} max={4} /></span>}
|
||||||
<Button href={href} arrow class="go">{post.data.postType === "build log" ? "Read the build log" : "Read the post"}</Button>
|
<Button href={href} arrow class="go">{post.data.postType === "build log" ? "Read the build log" : "Read the post"}</Button>
|
||||||
</div>
|
</div>
|
||||||
<GiltCorners />
|
<GiltCorners />
|
||||||
@@ -75,11 +76,11 @@ const now = nowStatuses(s, post);
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--gk-midnight);
|
background: var(--gk-midnight);
|
||||||
border: 1px solid rgba(201, 162, 74, 0.55);
|
border: 1px solid rgba(var(--gk-gold-rgb), 0.55);
|
||||||
border-radius: var(--gk-radius);
|
border-radius: var(--gk-radius);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 0 0 4px var(--gk-midnight),
|
inset 0 0 0 4px var(--gk-midnight),
|
||||||
inset 0 0 0 5px rgba(201, 162, 74, 0.35);
|
inset 0 0 0 5px rgba(var(--gk-gold-rgb), 0.35);
|
||||||
}
|
}
|
||||||
.art {
|
.art {
|
||||||
flex: 1 1 440px;
|
flex: 1 1 440px;
|
||||||
@@ -90,10 +91,10 @@ const now = nowStatuses(s, post);
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse 70% 90% at 100% 0%, rgba(106, 47, 224, 0.28), transparent 70%),
|
radial-gradient(ellipse 70% 90% at 100% 0%, rgba(var(--gk-violet-rgb), calc(0.28 * var(--gk-wash))), transparent 70%),
|
||||||
radial-gradient(ellipse 55% 80% at 0% 70%, rgba(20, 163, 154, 0.18), transparent 70%),
|
radial-gradient(ellipse 55% 80% at 0% 70%, rgba(var(--gk-teal-deep-rgb), calc(0.18 * var(--gk-wash))), transparent 70%),
|
||||||
repeating-linear-gradient(0deg, rgba(244, 242, 237, 0.035) 0 1px, transparent 1px 40px),
|
repeating-linear-gradient(0deg, rgba(var(--gk-star-rgb), 0.035) 0 1px, transparent 1px 40px),
|
||||||
repeating-linear-gradient(90deg, rgba(244, 242, 237, 0.035) 0 1px, transparent 1px 40px),
|
repeating-linear-gradient(90deg, rgba(var(--gk-star-rgb), 0.035) 0 1px, transparent 1px 40px),
|
||||||
var(--gk-sky);
|
var(--gk-sky);
|
||||||
}
|
}
|
||||||
.art.photo {
|
.art.photo {
|
||||||
@@ -104,7 +105,7 @@ const now = nowStatuses(s, post);
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 150%;
|
width: 150%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
opacity: 0.32;
|
opacity: calc(0.32 * var(--gk-ghost));
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.art :global(.path) {
|
.art :global(.path) {
|
||||||
@@ -116,7 +117,12 @@ const now = nowStatuses(s, post);
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
.art :global(.seal) {
|
.art :global(.seal) {
|
||||||
filter: drop-shadow(0 0 16px rgba(201, 162, 74, 0.35));
|
filter: drop-shadow(0 0 16px rgba(var(--gk-gold-rgb), 0.35));
|
||||||
|
}
|
||||||
|
/* Glow is for the night sky; on Starlight the foil stands on its own. */
|
||||||
|
:global(:root[data-theme="light"]) .art :global(.seal) {
|
||||||
|
opacity: 1;
|
||||||
|
filter: none;
|
||||||
}
|
}
|
||||||
.cover {
|
.cover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -129,13 +135,13 @@ const now = nowStatuses(s, post);
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background:
|
background:
|
||||||
linear-gradient(to top, rgba(14, 21, 34, 0.88), transparent 42%),
|
linear-gradient(to top, rgba(var(--gk-sky-rgb), 0.88), transparent 42%),
|
||||||
linear-gradient(to bottom, rgba(14, 21, 34, 0.45), transparent 30%);
|
linear-gradient(to bottom, rgba(var(--gk-sky-rgb), 0.45), transparent 30%);
|
||||||
}
|
}
|
||||||
.mat {
|
.mat {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 14px;
|
inset: 14px;
|
||||||
border: 1px solid rgba(201, 162, 74, 0.5);
|
border: 1px solid rgba(var(--gk-gold-rgb), 0.5);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.no {
|
.no {
|
||||||
@@ -196,8 +202,8 @@ const now = nowStatuses(s, post);
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 18% 50%, var(--gk-teal-bright) 0 2px, transparent 3px),
|
radial-gradient(circle at 18% 50%, var(--gk-teal-bright) 0 2px, transparent 3px),
|
||||||
linear-gradient(90deg, var(--gk-gold), rgba(201, 162, 74, 0.15) 70%, transparent);
|
linear-gradient(90deg, var(--gk-gold), rgba(var(--gk-gold-rgb), 0.15) 70%, transparent);
|
||||||
box-shadow: 0 0 8px rgba(201, 162, 74, 0.35);
|
box-shadow: 0 0 8px rgba(var(--gk-gold-rgb), 0.35);
|
||||||
}
|
}
|
||||||
.summary {
|
.summary {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* A post's page chips. A roundup can link a hundred pages, so past the first
|
||||||
|
* few the rest are counted; the post itself links every one.
|
||||||
|
*/
|
||||||
|
import Chip from "@gk/ui/components/Chip.astro";
|
||||||
|
import type { PageLink } from "../../lib/blog.ts";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
links: PageLink[];
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
const { links, max = 5 } = Astro.props;
|
||||||
|
const shown = links.length > max + 1 ? links.slice(0, max) : links;
|
||||||
|
const more = links.length - shown.length;
|
||||||
|
---
|
||||||
|
{shown.map((l) => <Chip href={l.href}>{l.label}</Chip>)}
|
||||||
|
{more > 0 && <span class="more">and {more} more</span>}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.more {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gk-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,6 +16,7 @@ import { connectedPages, neighbours, nowStatuses, pageLinks, readMinutes, relate
|
|||||||
import { formatDate, mediaSrc, type SiteIndex } from "../../lib/site.ts";
|
import { formatDate, mediaSrc, type SiteIndex } from "../../lib/site.ts";
|
||||||
import ConnectedPages from "./ConnectedPages.astro";
|
import ConnectedPages from "./ConnectedPages.astro";
|
||||||
import CrumbHero from "./CrumbHero.astro";
|
import CrumbHero from "./CrumbHero.astro";
|
||||||
|
import PageChips from "./PageChips.astro";
|
||||||
import PostList from "./PostList.astro";
|
import PostList from "./PostList.astro";
|
||||||
import SidePanel from "./SidePanel.astro";
|
import SidePanel from "./SidePanel.astro";
|
||||||
import StarHeading from "./StarHeading.astro";
|
import StarHeading from "./StarHeading.astro";
|
||||||
@@ -67,7 +68,7 @@ const making = d.topic === "Making";
|
|||||||
<span>{readMinutes(body)} min read</span>
|
<span>{readMinutes(body)} min read</span>
|
||||||
{(links.length > 0 || now.length > 0) && <span aria-hidden="true">·</span>}
|
{(links.length > 0 || now.length > 0) && <span aria-hidden="true">·</span>}
|
||||||
{now.map((st) => <StatusPill>Now · {st.data.kind}</StatusPill>)}
|
{now.map((st) => <StatusPill>Now · {st.data.kind}</StatusPill>)}
|
||||||
{links.map((l) => <Chip href={l.href}>{l.label}</Chip>)}
|
<PageChips links={links} max={8} />
|
||||||
</div>
|
</div>
|
||||||
</CrumbHero>
|
</CrumbHero>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -205,11 +206,11 @@ const making = d.topic === "Making";
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
padding: 48px 56px;
|
padding: 48px 56px;
|
||||||
background: var(--gk-midnight);
|
background: var(--gk-midnight);
|
||||||
border: 1px solid rgba(201, 162, 74, 0.55);
|
border: 1px solid rgba(var(--gk-gold-rgb), 0.55);
|
||||||
border-radius: var(--gk-radius);
|
border-radius: var(--gk-radius);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 0 0 4px var(--gk-midnight),
|
inset 0 0 0 4px var(--gk-midnight),
|
||||||
inset 0 0 0 5px rgba(201, 162, 74, 0.3);
|
inset 0 0 0 5px rgba(var(--gk-gold-rgb), 0.3);
|
||||||
}
|
}
|
||||||
.cover {
|
.cover {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ const { posts, site: s, kicker, emptyTitle = "Nothing written here yet", emptyTe
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
padding: 28px;
|
padding: 28px;
|
||||||
border: 1px dashed rgba(201, 162, 74, 0.45);
|
border: 1px dashed rgba(var(--gk-gold-rgb), 0.45);
|
||||||
border-radius: var(--gk-radius);
|
border-radius: var(--gk-radius);
|
||||||
background: rgba(19, 28, 43, 0.6);
|
background: rgba(var(--gk-midnight-rgb), 0.6);
|
||||||
}
|
}
|
||||||
.words {
|
.words {
|
||||||
flex: 1 1 260px;
|
flex: 1 1 260px;
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
* One post as a fixed-height row: picture, topic and date, title, a
|
* One post as a fixed-height row: picture, topic and date, title, a
|
||||||
* two-line summary, Now chips for linked notes and page-link chips.
|
* two-line summary, Now chips for linked notes and page-link chips.
|
||||||
*/
|
*/
|
||||||
import Chip from "@gk/ui/components/Chip.astro";
|
|
||||||
import Star from "@gk/ui/components/Star.astro";
|
import Star from "@gk/ui/components/Star.astro";
|
||||||
import StatusPill from "@gk/ui/components/StatusPill.astro";
|
import StatusPill from "@gk/ui/components/StatusPill.astro";
|
||||||
import { dateLine, nowStatuses, pageLinks, type Post } from "../../lib/blog.ts";
|
import { dateLine, nowStatuses, pageLinks, type Post } from "../../lib/blog.ts";
|
||||||
import { mediaSrc, type SiteIndex } from "../../lib/site.ts";
|
import { mediaSrc, type SiteIndex } from "../../lib/site.ts";
|
||||||
|
import PageChips from "./PageChips.astro";
|
||||||
import PostThumb from "./PostThumb.astro";
|
import PostThumb from "./PostThumb.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -37,7 +37,7 @@ const now = nowStatuses(s, post);
|
|||||||
<H class="title"><a href={href}>{post.title}</a></H>
|
<H class="title"><a href={href}>{post.title}</a></H>
|
||||||
{post.data.summary && <p class="summary">{post.data.summary}</p>}
|
{post.data.summary && <p class="summary">{post.data.summary}</p>}
|
||||||
<span class="chips">
|
<span class="chips">
|
||||||
{links.length ? links.map((l) => <Chip href={l.href}>{l.label}</Chip>) : <span class="none">Not tied to a page</span>}
|
{links.length ? <PageChips links={links} max={3} /> : <span class="none">Not tied to a page</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -48,7 +48,7 @@ const now = nowStatuses(s, post);
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
padding: 22px 0 20px;
|
padding: 22px 0 20px;
|
||||||
border-top: 1px dashed rgba(244, 242, 237, 0.16);
|
border-top: 1px dashed rgba(var(--gk-star-rgb), 0.16);
|
||||||
}
|
}
|
||||||
.mark {
|
.mark {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* monogram, which also stands in when there is no cover or it fails to load.
|
* monogram, which also stands in when there is no cover or it fails to load.
|
||||||
*/
|
*/
|
||||||
import monogram from "@gk/ui/assets/brand/gk-monogram.svg?url";
|
import monogram from "@gk/ui/assets/brand/gk-monogram.svg?url";
|
||||||
|
import Motif from "@gk/ui/components/Motif.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
src?: string;
|
src?: string;
|
||||||
@@ -16,7 +17,7 @@ const { src, alt = "", width = 176, height = 120, class: className } = Astro.pro
|
|||||||
const mark = Math.round(Math.min(width, height) / 2);
|
const mark = Math.round(Math.min(width, height) / 2);
|
||||||
---
|
---
|
||||||
<span class:list={["thumb", className]} style={`--w:${width}px;--h:${height}px`}>
|
<span class:list={["thumb", className]} style={`--w:${width}px;--h:${height}px`}>
|
||||||
<img src={monogram} alt="" width={mark} height={mark} class="mono" aria-hidden="true" />
|
<Motif src={monogram} alt="" width={mark} height={mark} class="mono" aria-hidden="true" />
|
||||||
{src && <img src={src} alt={alt} loading="lazy" decoding="async" class="photo" onerror="this.remove()" />}
|
{src && <img src={src} alt={alt} loading="lazy" decoding="async" class="photo" onerror="this.remove()" />}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -31,13 +32,13 @@ const mark = Math.round(Math.min(width, height) / 2);
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
outline: 1px solid rgba(201, 162, 74, 0.4);
|
outline: 1px solid rgba(var(--gk-gold-rgb), 0.4);
|
||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
border: 1px solid var(--gk-lines);
|
border: 1px solid var(--gk-lines);
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse at 30% 20%, rgba(106, 47, 224, 0.3), transparent 60%),
|
radial-gradient(ellipse at 30% 20%, rgba(var(--gk-violet-rgb), calc(0.3 * var(--gk-wash))), transparent 60%),
|
||||||
repeating-linear-gradient(0deg, rgba(244, 242, 237, 0.05) 0 1px, transparent 1px 22px),
|
repeating-linear-gradient(0deg, rgba(var(--gk-star-rgb), 0.05) 0 1px, transparent 1px 22px),
|
||||||
repeating-linear-gradient(90deg, rgba(244, 242, 237, 0.05) 0 1px, transparent 1px 22px),
|
repeating-linear-gradient(90deg, rgba(var(--gk-star-rgb), 0.05) 0 1px, transparent 1px 22px),
|
||||||
var(--gk-sky);
|
var(--gk-sky);
|
||||||
}
|
}
|
||||||
.mono {
|
.mono {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const { title, seal, as: H = "h2" } = Astro.props;
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: -26px;
|
right: -26px;
|
||||||
top: -26px;
|
top: -26px;
|
||||||
opacity: 0.14;
|
opacity: calc(0.14 * var(--gk-ghost));
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.head {
|
.head {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const H = `h${level}` as "h2" | "h3";
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 18% 50%, var(--gk-teal-bright) 0 2px, transparent 3px),
|
radial-gradient(circle at 18% 50%, var(--gk-teal-bright) 0 2px, transparent 3px),
|
||||||
linear-gradient(90deg, var(--gk-gold), rgba(201, 162, 74, 0.15) 70%, transparent);
|
linear-gradient(90deg, var(--gk-gold), rgba(var(--gk-gold-rgb), 0.15) 70%, transparent);
|
||||||
box-shadow: 0 0 8px rgba(201, 162, 74, 0.35);
|
box-shadow: 0 0 8px rgba(var(--gk-gold-rgb), 0.35);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ const topicHref = (topic: string) => `/blog/topic/${slugify(topic)}`;
|
|||||||
width: 180px;
|
width: 180px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: linear-gradient(90deg, var(--gk-gold), rgba(201, 162, 74, 0.15) 70%, transparent);
|
background: linear-gradient(90deg, var(--gk-gold), rgba(var(--gk-gold-rgb), 0.15) 70%, transparent);
|
||||||
box-shadow: 0 0 8px rgba(201, 162, 74, 0.35);
|
box-shadow: 0 0 8px rgba(var(--gk-gold-rgb), 0.35);
|
||||||
}
|
}
|
||||||
.all {
|
.all {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const { href, title, line, size = "large" } = Astro.props;
|
|||||||
.large {
|
.large {
|
||||||
min-height: 62px;
|
min-height: 62px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid rgba(244, 242, 237, 0.35);
|
border-bottom: 1px solid rgba(var(--gk-star-rgb), 0.35);
|
||||||
}
|
}
|
||||||
.small {
|
.small {
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const { variant = "hero", src, alt = "Greg Kilmer", seal = "transmutation", clas
|
|||||||
border: 3px solid var(--gk-gold);
|
border: 3px solid var(--gk-gold);
|
||||||
border-radius: var(--gk-radius-lg);
|
border-radius: var(--gk-radius-lg);
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
background: rgba(106, 47, 224, 0.12);
|
background: rgba(var(--gk-violet-rgb), calc(0.12 * var(--gk-wash)));
|
||||||
}
|
}
|
||||||
.hero :global(.art) {
|
.hero :global(.art) {
|
||||||
border-radius: var(--gk-radius);
|
border-radius: var(--gk-radius);
|
||||||
@@ -59,7 +59,7 @@ const { variant = "hero", src, alt = "Greg Kilmer", seal = "transmutation", clas
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(201, 162, 74, 0.55);
|
border: 1px solid rgba(var(--gk-gold-rgb), 0.55);
|
||||||
background: var(--gk-midnight);
|
background: var(--gk-midnight);
|
||||||
}
|
}
|
||||||
.panel :global(.art) {
|
.panel :global(.art) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import monogram from "@gk/ui/assets/brand/gk-monogram.svg?url";
|
import monogram from "@gk/ui/assets/brand/gk-monogram.svg?url";
|
||||||
import Chip from "@gk/ui/components/Chip.astro";
|
import Chip from "@gk/ui/components/Chip.astro";
|
||||||
import Icon from "@gk/ui/components/Icon.astro";
|
import Icon from "@gk/ui/components/Icon.astro";
|
||||||
|
import Motif from "@gk/ui/components/Motif.astro";
|
||||||
import Star from "@gk/ui/components/Star.astro";
|
import Star from "@gk/ui/components/Star.astro";
|
||||||
import type { StatusView } from "../../lib/home.ts";
|
import type { StatusView } from "../../lib/home.ts";
|
||||||
import { formatDate, type SiteIndex } from "../../lib/site.ts";
|
import { formatDate, type SiteIndex } from "../../lib/site.ts";
|
||||||
@@ -25,7 +26,7 @@ const postHref = post ? s.href(post) : null;
|
|||||||
<img src={v.cover} alt="" class="thumb photo" loading="lazy" decoding="async" width="56" height="56" />
|
<img src={v.cover} alt="" class="thumb photo" loading="lazy" decoding="async" width="56" height="56" />
|
||||||
) : (
|
) : (
|
||||||
<span class="thumb tile" aria-hidden="true">
|
<span class="thumb tile" aria-hidden="true">
|
||||||
<img src={monogram} alt="" width="28" height="28" />
|
<Motif src={monogram} alt="" width="28" height="28" />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ const postHref = post ? s.href(post) : null;
|
|||||||
height: 56px;
|
height: 56px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--gk-lines);
|
border: 1px solid var(--gk-lines);
|
||||||
outline: 1px solid rgba(201, 162, 74, 0.35);
|
outline: 1px solid rgba(var(--gk-gold-rgb), 0.35);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
.photo {
|
.photo {
|
||||||
@@ -81,9 +82,9 @@ const postHref = post ? s.href(post) : null;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse at 30% 20%, rgba(106, 47, 224, 0.3), transparent 60%),
|
radial-gradient(ellipse at 30% 20%, rgba(var(--gk-violet-rgb), calc(0.3 * var(--gk-wash))), transparent 60%),
|
||||||
repeating-linear-gradient(0deg, rgba(244, 242, 237, 0.05) 0 1px, transparent 1px 14px),
|
repeating-linear-gradient(0deg, rgba(var(--gk-star-rgb), 0.05) 0 1px, transparent 1px 14px),
|
||||||
repeating-linear-gradient(90deg, rgba(244, 242, 237, 0.05) 0 1px, transparent 1px 14px),
|
repeating-linear-gradient(90deg, rgba(var(--gk-star-rgb), 0.05) 0 1px, transparent 1px 14px),
|
||||||
var(--gk-sky);
|
var(--gk-sky);
|
||||||
}
|
}
|
||||||
.tile img {
|
.tile img {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const points = items.map((_, i) => `${(((i + 0.5) / n) * 1000).toFixed(1)},${lif
|
|||||||
</div>
|
</div>
|
||||||
<div class="sky">
|
<div class="sky">
|
||||||
<svg aria-hidden="true" class="line" viewBox="0 0 1000 154" preserveAspectRatio="none">
|
<svg aria-hidden="true" class="line" viewBox="0 0 1000 154" preserveAspectRatio="none">
|
||||||
<polyline points={points} fill="none" stroke="#C9A24A" stroke-width="1.4" stroke-dasharray="2 6" vector-effect="non-scaling-stroke" />
|
<polyline points={points} fill="none" style="stroke:var(--gk-gold)" stroke-width="1.4" stroke-dasharray="2 6" vector-effect="non-scaling-stroke" />
|
||||||
</svg>
|
</svg>
|
||||||
<ul style={`--n:${n}`}>
|
<ul style={`--n:${n}`}>
|
||||||
{
|
{
|
||||||
@@ -113,7 +113,7 @@ const points = items.map((_, i) => `${(((i + 0.5) / n) * 1000).toFixed(1)},${lif
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 2px var(--gk-gold),
|
0 0 0 2px var(--gk-gold),
|
||||||
0 0 22px rgba(201, 162, 74, 0.45);
|
0 0 22px rgba(var(--gk-gold-rgb), 0.45);
|
||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
a :global(.orb .mat) {
|
a :global(.orb .mat) {
|
||||||
@@ -134,7 +134,7 @@ const points = items.map((_, i) => `${(((i + 0.5) / n) * 1000).toFixed(1)},${lif
|
|||||||
a:hover :global(.orb) {
|
a:hover :global(.orb) {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 2px var(--gk-gold-bright),
|
0 0 0 2px var(--gk-gold-bright),
|
||||||
0 0 30px rgba(201, 162, 74, 0.7);
|
0 0 30px rgba(var(--gk-gold-rgb), 0.7);
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ const status = site.statusFor(game);
|
|||||||
// Players and play time sync from BoardGameGeek; nothing shows until they have.
|
// Players and play time sync from BoardGameGeek; nothing shows until they have.
|
||||||
const facts = [gamePlayers(game), gamePlayTime(game)].filter(Boolean);
|
const facts = [gamePlayers(game), gamePlayTime(game)].filter(Boolean);
|
||||||
const seats = playersOf(game);
|
const seats = playersOf(game);
|
||||||
const hasPost = site.backlinks(game, { type: "post" }).length > 0;
|
const post = site.backlinks(game, { type: "post" })[0];
|
||||||
|
const postHref = post && site.href(post);
|
||||||
|
const hasPost = !!post;
|
||||||
---
|
---
|
||||||
<li
|
<li
|
||||||
id={game.slug}
|
id={game.slug}
|
||||||
@@ -61,9 +63,19 @@ const hasPost = site.backlinks(game, { type: "post" }).length > 0;
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<a class="gk-text gk-quiet bgg" href={`https://boardgamegeek.com/boardgame/${d.bggId}`} rel="external noopener">
|
<span class="links">
|
||||||
BoardGameGeek<span class="gk-visually-hidden">: {game.title}</span>
|
{
|
||||||
</a>
|
post && postHref && (
|
||||||
|
<a href={postHref} class="blog">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5h16v14H4zM8 9h8M8 13h5" /></svg>
|
||||||
|
Blog: {post.title}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<a class="gk-text gk-quiet bgg" href={`https://boardgamegeek.com/boardgame/${d.bggId}`} rel="external noopener">
|
||||||
|
BoardGameGeek<span class="gk-visually-hidden">: {game.title}</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</li>
|
</li>
|
||||||
@@ -134,10 +146,24 @@ const hasPost = site.backlinks(game, { type: "post" }).length > 0;
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--gk-muted);
|
color: var(--gk-muted);
|
||||||
}
|
}
|
||||||
.bgg {
|
.links {
|
||||||
align-self: flex-start;
|
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.blog {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gk-teal-bright);
|
||||||
|
}
|
||||||
|
.bgg {
|
||||||
|
align-self: flex-start;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const { id, dot } = Astro.props;
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 18% 50%, var(--gk-teal-bright) 0 2px, transparent 3px),
|
radial-gradient(circle at 18% 50%, var(--gk-teal-bright) 0 2px, transparent 3px),
|
||||||
linear-gradient(90deg, var(--gk-gold), rgba(201, 162, 74, 0.15) 70%, transparent);
|
linear-gradient(90deg, var(--gk-gold), rgba(var(--gk-gold-rgb), 0.15) 70%, transparent);
|
||||||
box-shadow: 0 0 8px rgba(201, 162, 74, 0.35);
|
box-shadow: 0 0 8px rgba(var(--gk-gold-rgb), 0.35);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -162,8 +162,8 @@ const headId = `line-${line.slug}`;
|
|||||||
max-width: 60%;
|
max-width: 60%;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: linear-gradient(90deg, var(--gk-gold), rgba(201, 162, 74, 0.15) 70%, transparent);
|
background: linear-gradient(90deg, var(--gk-gold), rgba(var(--gk-gold-rgb), 0.15) 70%, transparent);
|
||||||
box-shadow: 0 0 8px rgba(201, 162, 74, 0.35);
|
box-shadow: 0 0 8px rgba(var(--gk-gold-rgb), 0.35);
|
||||||
}
|
}
|
||||||
.summary {
|
.summary {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||