diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5d5292f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # The pnpm version comes from "packageManager" in package.json. + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm test + - run: pnpm build diff --git a/.gitignore b/.gitignore index cbcb82e..9671879 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ .superpowers/ .DS_Store node_modules/ + +# website +apps/website/public/brand/ +apps/website/dist/ +apps/website/.astro/ +apps/website/.wrangler/ diff --git a/.impeccable.md b/.impeccable.md new file mode 100644 index 0000000..5f08573 --- /dev/null +++ b/.impeccable.md @@ -0,0 +1,39 @@ +# Design Context: jscebu.org + +Distilled from the approved specs: +- `docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md` +- `docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md` + +The brand rules themselves live in `packages/brand` and at https://www.jscebu.org/brand.md. This file covers who the site is for and how it should feel. + +## Audience + +- **Developers in Cebu:** students, juniors and working devs who come to meetups, workshops and Hacktoberfest Cebu. +- **Volunteers, co-leads and partner communities:** they make things for JS Cebu. +- **AI agents:** they make on-brand graphics and pages, given only the brand link. + +## Use cases + +- Find the next event. For now that means Cebby and Facebook. +- See that the community is real and active: real photos, real projects. +- Find where to join. +- Get the logos and brand rules. + +## Personality + +- **The website uses the Terminal personality:** ink background, a faint grid, yellow light. +- **The two signatures carry recognition:** the block cursor at the end of the primary headline, and the logo as a tilted die-cut sticker. +- **Voice:** + - English first, friendly and direct, like a fellow developer. + - Bisaya only lightly, and only approved phrases. +- **Photos:** real and never filtered. +- **No hexagon motifs** beyond the logo itself. + +## Motion + +Added 2026-10-28, because Dorell said "a little bit of animations. right now, it's zero". + +- **A little, and purposeful.** Motion follows the brand's two signatures: text arriving like terminal output, and the sticker being slapped on. +- **Easing and timing:** decelerating curves (ease-out-expo or quart), never bounce. Entrances run 400–700ms; feedback runs 100–250ms. Animate only transform and opacity. +- **One hero moment per page,** plus quiet reveals as sections scroll in and small hover and press feedback on things you can click. +- **Under `prefers-reduced-motion: reduce`, nothing moves,** and the cursor doesn't blink. Content never depends on JavaScript to become visible. diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5493447 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing + +Thanks for helping JavaScript Cebu. + +## How changes ship + +Every change, from a person or an AI agent, follows the same path: + +1. **Intent.** Open an issue, or start a conversation, that says what should change and why. +2. **Plan.** Larger changes get a written spec and plan under `docs/superpowers/` before anyone writes code. +3. **Approval.** A JS Cebu lead approves the plan. +4. **Implementation.** Build it on a branch and open a pull request. +5. **Review.** + - CI runs `pnpm install --frozen-lockfile && pnpm test && pnpm build` on every pull request. + - A reviewer reads the whole branch. +6. **Merge.** Merging to `main` deploys www.jscebu.org. + +## Deploys + +**Merging to `main` is the deploy.** Cloudflare Workers Builds builds `main` and deploys the `jscebu-org` Worker with these settings: + +| Setting | Value | +|---|---| +| Root directory | repo root | +| Build command | `pnpm install --frozen-lockfile && pnpm test && pnpm --filter website build` | +| Deploy command | `pnpm --filter website exec wrangler deploy` | +| Other branches | `pnpm --filter website exec wrangler versions upload` (a preview URL per branch) | + +Node comes from `.nvmrc` and pnpm from `packageManager` in `package.json`. A failing test or post-build check stops the deploy. + +Don't run `wrangler deploy` by hand. + +Be careful with `routes` in `apps/website/wrangler.jsonc`. www.jscebu.org is attached there as a Custom Domain. Deploys from CI replace any existing DNS record for a Custom Domain they list, so add or change a domain only through a reviewed pull request. + +## Brand changes are releases + +`packages/brand` is the JS Cebu design system. A change to a color, font, rule, logo or template is a **versioned release** (for example v0.1 → v0.2), not a drive-by edit: + +1. Bump `version` in `packages/brand/src/brand.ts`. +2. Add a `changelog` entry describing the change. +3. A JS Cebu lead approves it before merge. + +Bisaya copy appears only after a lead approves the phrase, which means its `status` is `approved`. + +## Before you open a PR + +```sh +pnpm install +pnpm test +pnpm build +``` + +All tests and checks must pass. CI runs the same commands. + +## Never commit secrets + +This repository is public. Never commit tokens, API keys, form responses or personal data. Deploy credentials live in Cloudflare and GitHub. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2be4cd7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2026 JavaScript Cebu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +The MIT license covers code only. The JavaScript Cebu name, logos and brand +assets are not open-licensed; see packages/brand/LICENSE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e5e957 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# jscebu.org + +The website and design system of [JavaScript Cebu](https://www.jscebu.org), live at . + +| Path | What it is | +|---|---| +| `packages/brand` | The JS Cebu design system: brand data, the generators for `tokens.css`, `brand.md` and `llms.txt`, logos, templates and an HTML checker for the brand's MUST rules | +| `apps/website` | The Astro site: `/`, `/projects`, `/about`, `/links` and `/brand`. It deploys as the Cloudflare Worker `jscebu-org`, which serves www.jscebu.org | + +- Brand guidelines: +- For AI agents: +- Design specs: [design system](docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md) · [site v1](docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md) + +## How changes ship + +1. Intent +2. Plan +3. Approval +4. Implementation +5. Review +6. Merge + +**Merging to `main` deploys to production automatically** through Cloudflare Workers Builds. See [CONTRIBUTING.md](CONTRIBUTING.md). + +## Develop + +```sh +pnpm install +pnpm test +pnpm build # builds the site and runs the post-build checks +pnpm --filter website dev # local preview +``` + +Code is MIT-licensed ([LICENSE](LICENSE)). The JS Cebu name and logos are not; see [packages/brand/LICENSE](packages/brand/LICENSE). diff --git a/apps/website/astro.config.mjs b/apps/website/astro.config.mjs new file mode 100644 index 0000000..ded98a5 --- /dev/null +++ b/apps/website/astro.config.mjs @@ -0,0 +1,10 @@ +// @ts-check +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + site: 'https://www.jscebu.org', + output: 'static', + // Astro 7 defaults to 'jsx' whitespace handling, which can drop the space + // before the cursor's word wrapper. Plain collapsing keeps it. + compressHTML: true, +}); diff --git a/apps/website/package.json b/apps/website/package.json new file mode 100644 index 0000000..e052f09 --- /dev/null +++ b/apps/website/package.json @@ -0,0 +1,28 @@ +{ + "name": "website", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "sync": "tsx scripts/sync-brand.ts", + "dev": "pnpm sync && astro dev", + "build": "pnpm sync && astro build && tsx scripts/check-dist.ts", + "preview": "astro preview", + "test": "vitest run" + }, + "devDependencies": { + "linkedom": "~0.18.14", + "tsx": "^4.23.15", + "vitest": "^5.0.5", + "wrangler": "^4.153.0", + "yaml": "^2.9.1" + }, + "dependencies": { + "@fontsource-variable/archivo": "^5.3.0", + "@fontsource-variable/inter-tight": "^5.3.0", + "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource-variable/space-grotesk": "^5.3.0", + "@jscebu/brand": "workspace:*", + "astro": "~7.4.2" + } +} diff --git a/apps/website/public/_headers b/apps/website/public/_headers new file mode 100644 index 0000000..ae3577d --- /dev/null +++ b/apps/website/public/_headers @@ -0,0 +1,8 @@ +/brand.md + Content-Type: text/markdown; charset=utf-8 + Access-Control-Allow-Origin: * +/llms.txt + Content-Type: text/plain; charset=utf-8 + Access-Control-Allow-Origin: * +/brand/* + Access-Control-Allow-Origin: * diff --git a/apps/website/public/_redirects b/apps/website/public/_redirects new file mode 100644 index 0000000..c3013be --- /dev/null +++ b/apps/website/public/_redirects @@ -0,0 +1,13 @@ +# Old Notion-era jscebu.org URLs (spec: docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md §5). +# Targets end in / because folders are served at /name/; a bare /name would redirect twice. +/mission-vision /about/ 301 +/members /about/#organizers 301 +/iniatives /about/ 301 +/initiatives /about/ 301 +/official-links /links/ 301 +/community-projects /projects/ 301 +# Temporary: /events becomes a real page once events come from Cebby. +/events /#events 302 +/promotional-materials /brand/ 301 +/annual-hacktoberfest https://hacktoberfest.jscebu.org/ 301 +/volunteers-training-workshop /about/ 301 diff --git a/apps/website/scripts/check-dist.ts b/apps/website/scripts/check-dist.ts new file mode 100644 index 0000000..497fb70 --- /dev/null +++ b/apps/website/scripts/check-dist.ts @@ -0,0 +1,150 @@ +/** + * Post-build checks over dist/ and src/. Run after `astro build`; exits 1 with a list on any failure. + * - The cursor never wraps alone (and HTML compression didn't eat the space before its word). + * - Every link to this site, on every page and in brand.md and llms.txt, resolves to a built file. + * - Every redirect in _redirects that points at this site lands on a built file. + * - Every rule appears on both the page and brand.md ("one source, two readers"). + * - No color or font-family literals in src/: everything comes from --jc-* variables. + */ +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { brand, lintHtml, type Brand } from '@jscebu/brand'; +import { parseHTML } from 'linkedom'; + +const SITE = 'https://www.jscebu.org'; + +export function findCursorViolations(html: string): string[] { + const out = lintHtml(html) + .filter((i) => i.rule === 'cursorAttached') + .map((i) => i.message); + const { document } = parseHTML(`${html}`); + for (const wrap of document.querySelectorAll('.jc-nw')) { + const prev = wrap.previousSibling; + if (prev?.nodeType === 3 && /\S$/.test(prev.textContent ?? '')) { + out.push(`No space before "${wrap.textContent}": the last word is glued to "${prev.textContent!.trim().split(/\s+/).pop()}".`); + } + } + return out; +} + +export function findMissingLinks(text: string, fileExists: (path: string) => boolean): string[] { + const { document } = parseHTML(`${text}`); + const urls = [ + ...[...document.querySelectorAll('[href], [src]')].map((el) => el.getAttribute('href') ?? el.getAttribute('src') ?? ''), + ...[...text.matchAll(/https:\/\/www\.jscebu\.org\/[^\s"'<>)\]|`]*/g)].map((m) => m[0].replace(/[.,;:]+$/, '')), + ]; + const paths = urls + .map((u) => (u.startsWith(SITE) ? u.slice(SITE.length) || '/' : u)) + .filter((u) => u.startsWith('/') && !u.startsWith('//')) + .map((u) => u.replace(/[?#].*$/, '')); + return [...new Set(paths)].filter((p) => !fileExists(p)); +} + +const normalize = (s: string) => s.replaceAll('`', '').replace(/\s+/g, ' ').trim(); + +export function findParityGaps(pageText: string, md: string, rules: string[]): string[] { + const page = normalize(pageText); + const doc = normalize(md); + return rules.flatMap((rule) => { + const r = normalize(rule); + return [ + ...(page.includes(r) ? [] : [`Missing from the /brand page: ${r}`]), + ...(doc.includes(r) ? [] : [`Missing from brand.md: ${r}`]), + ]; + }); +} + +const HEX = /(? + text.split('\n').flatMap((line, i) => { + const hit = line.match(HEX) ?? line.match(COLOR_FN) ?? line.match(FONT_FAMILY); + return hit ? [`${path}:${i + 1}: "${hit[0].trim()}" (use a --jc-* variable)`] : []; + }), + ); +} + +/** Every rule list that must read the same on the page and in brand.md. */ +export function parityRules(b: Brand): string[] { + return [ + ...b.must.map((r) => r.text), + ...b.logoRules, + ...b.signatures.flatMap((s) => s.rules), + ...b.personalities.flatMap((p) => [...p.recipe, ...p.dont]), + ...b.voice.principles, + ]; +} + +/** Parse a Cloudflare `_redirects` file: `source destination [status]`, status defaulting to 302. */ +export function parseRedirects(text: string): { from: string; to: string; status: number }[] { + return text + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + .map((line) => { + const [from, to, status] = line.split(/\s+/); + return { from, to, status: status ? Number(status) : 302 }; + }); +} + +function walk(dir: string): string[] { + return readdirSync(dir).flatMap((name) => { + const p = join(dir, name); + return statSync(p).isDirectory() ? walk(p) : [p]; + }); +} + +function main(): void { + const app = fileURLToPath(new URL('..', import.meta.url)); + const dist = join(app, 'dist'); + const read = (p: string) => readFileSync(join(dist, p), 'utf8'); + /** A URL path is built when it names a file, or ends in / over a folder with index.html. A bare folder path would cost a redirect. */ + const exists = (urlPath: string) => { + const p = join(dist, decodeURIComponent(urlPath)); + if (urlPath.endsWith('/')) return existsSync(join(p, 'index.html')); + return existsSync(p) && statSync(p).isFile(); + }; + + const required = [ + 'index.html', 'projects/index.html', 'about/index.html', 'links/index.html', '404.html', + 'brand/index.html', 'brand.md', 'brand/tokens.css', 'llms.txt', 'sitemap.xml', 'robots.txt', '_redirects', + 'brand/logo/sticker.svg', 'brand/templates/event-post.html', + ]; + const pages = walk(dist).filter((p) => p.endsWith('.html') && !p.startsWith(join(dist, 'brand', 'templates'))).map((p) => relative(dist, p)); + const brandHtml = read('brand/index.html'); + const md = read('brand.md'); + const redirects = existsSync(join(dist, '_redirects')) ? parseRedirects(read('_redirects')) : []; + const src = walk(join(app, 'src')).map((p) => ({ path: relative(app, p), text: readFileSync(p, 'utf8') })); + + const errors = [ + ...required.filter((f) => !existsSync(join(dist, f))).map((f) => `dist/${f} was not built`), + ...pages.flatMap((page) => { + const html = read(page); + return [ + ...findCursorViolations(html).map((m) => `${page} cursor: ${m}`), + ...findMissingLinks(html, exists).map((p) => `${page} links to ${p}, which was not built`), + ]; + }), + ...findMissingLinks(md, exists).map((p) => `brand.md links to ${p}, which was not built`), + ...findMissingLinks(read('llms.txt'), exists).map((p) => `llms.txt links to ${p}, which was not built`), + ...redirects + .filter((r) => r.to.startsWith('/') && !exists(r.to.replace(/#.*$/, ''))) + .map((r) => `_redirects sends ${r.from} to ${r.to}, which was not built`), + ...findParityGaps(parseHTML(brandHtml).document.body.textContent ?? '', md, parityRules(brand)), + ...findLiterals(src), + ]; + + if (errors.length) { + console.error(`check-dist: ${errors.length} problem(s)\n${errors.map((e) => ` - ${e}`).join('\n')}`); + process.exit(1); + } + console.log( + `check-dist: ok (${required.length} files, ${pages.length} pages: cursor and links; ${redirects.length} redirects; /brand parity; ${src.length} source files free of literals)`, + ); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main(); diff --git a/apps/website/scripts/sync-brand.ts b/apps/website/scripts/sync-brand.ts new file mode 100644 index 0000000..bfa2c28 --- /dev/null +++ b/apps/website/scripts/sync-brand.ts @@ -0,0 +1,17 @@ +/** Copy the brand package's logos, photos and templates into public/brand/ (gitignored). */ +import { cpSync, mkdirSync, rmSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const brandPkg = fileURLToPath(new URL('../../../packages/brand/', import.meta.url)); +const target = fileURLToPath(new URL('../public/brand/', import.meta.url)); + +rmSync(target, { recursive: true, force: true }); +mkdirSync(target, { recursive: true }); +for (const [from, to] of [ + ['assets/logo', 'logo'], + ['assets/photos', 'photos'], + ['templates', 'templates'], +]) { + cpSync(brandPkg + from, target + to, { recursive: true }); +} +console.log(`sync-brand: copied logo, photos and templates to ${target}`); diff --git a/apps/website/scripts/url-matrix.sh b/apps/website/scripts/url-matrix.sh new file mode 100755 index 0000000..21f7e09 --- /dev/null +++ b/apps/website/scripts/url-matrix.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Check a deployed (or `wrangler dev`) jscebu.org against the v1 spec: old Notion-era URLs +# redirect per §5, pages return 200, brand.md keeps its agent-facing headers, misses 404. +# Usage: scripts/url-matrix.sh https://www.jscebu.org +set -uo pipefail + +base=${1:?usage: url-matrix.sh BASE_URL} +base=${base%/} +host=$(printf '%s' "$base" | sed -E 's#^https?://([^/]+).*#\1#') +fail=0 + +# fetch PATH → sets status, location (host-relative when on the same host) and headers +fetch() { + headers=$(curl -s -o /dev/null -D - --max-time 30 "$base$1" | tr -d '\r') + status=$(printf '%s\n' "$headers" | head -1 | awk '{print $2}') + location=$(printf '%s\n' "$headers" | grep -i '^location:' | head -1 | sed -E 's/^[Ll]ocation: *//; s#^https?://'"$host"'##') +} + +report() { # ok? label detail + if [ "$1" = ok ]; then printf 'ok %s\n' "$2"; else printf 'FAIL %s (%s)\n' "$2" "$3"; fail=1; fi +} + +redirect() { # path target status + fetch "$1" + if [ "$status" = "$3" ] && [ "$location" = "$2" ]; then report ok "$1 → $3 $2" + else report fail "$1 → $3 $2" "got $status ${location:-no location}"; fi +} + +page() { # path [content-type prefix] + fetch "$1" + local type + type=$(printf '%s\n' "$headers" | grep -i '^content-type:' | head -1 | sed -E 's/^[Cc]ontent-[Tt]ype: *//') + if [ "$status" = 200 ] && [[ "$type" == ${2:-}* ]]; then report ok "$1 200 ${type}" + else report fail "$1 200 ${2:-}" "got $status $type"; fi +} + +redirect /mission-vision /about/ 301 +redirect /members /about/#organizers 301 +redirect /iniatives /about/ 301 +redirect /initiatives /about/ 301 +redirect /official-links /links/ 301 +redirect /community-projects /projects/ 301 +redirect /events /#events 302 +redirect /promotional-materials /brand/ 301 +redirect /annual-hacktoberfest https://hacktoberfest.jscebu.org/ 301 +redirect /volunteers-training-workshop /about/ 301 + +for p in / /projects/ /about/ /links/ /brand/; do page "$p" text/html; done +page /brand.md 'text/markdown; charset=utf-8' +if printf '%s\n' "$headers" | grep -qi '^access-control-allow-origin: \*'; then report ok '/brand.md CORS *'; else report fail '/brand.md CORS *' 'header missing'; fi +page /llms.txt 'text/plain; charset=utf-8' +page /brand/tokens.css text/css +page /brand/logo/sticker.svg image/svg+xml +page /sitemap.xml application/xml +page /robots.txt text/plain + +fetch /this-page-does-not-exist +if [ "$status" = 404 ]; then report ok '/this-page-does-not-exist 404'; else report fail '/this-page-does-not-exist 404' "got $status"; fi + +exit $fail diff --git a/apps/website/src/assets/projects/cebby.png b/apps/website/src/assets/projects/cebby.png new file mode 100644 index 0000000..3dcbb74 Binary files /dev/null and b/apps/website/src/assets/projects/cebby.png differ diff --git a/apps/website/src/components/Cursor.astro b/apps/website/src/components/Cursor.astro new file mode 100644 index 0000000..7dc00e4 --- /dev/null +++ b/apps/website/src/components/Cursor.astro @@ -0,0 +1,34 @@ +--- +/** + * A headline's text with the brand cursor after the last word. The last word and + * the cursor share a .jc-nw wrapper so the cursor can never wrap onto its own line. + * With `animate`, each word rises in turn (styles in Site.astro); the cursor is there from the start. + */ +interface Props { + text: string; + blink?: boolean; + animate?: boolean; +} + +const { text, blink = false, animate = false } = Astro.props; +const words = text.trim().split(/\s+/); +const last = words.pop()!; +const cursorClass = ['jc-cursor', { 'jc-cursor--blink': blink }]; +--- + +{ + animate ? ( + <> + {words.map((word, i) => ( + <> + {word}{' '} + + ))} + {last} + + ) : ( + <> + {words.length ? `${words.join(' ')} ` : ''}{last} + + ) +} diff --git a/apps/website/src/components/Frame.astro b/apps/website/src/components/Frame.astro new file mode 100644 index 0000000..2d08b60 --- /dev/null +++ b/apps/website/src/components/Frame.astro @@ -0,0 +1,59 @@ +--- +/** A scaled-to-fit iframe for a fixed-size piece (an example or a template). */ +interface Props { + width: number; + height: number; + title: string; + src?: string; + srcdoc?: string; +} + +const { width, height, title, src, srcdoc } = Astro.props; +--- + +
+ +
+ + + + diff --git a/apps/website/src/components/PersonalityCard.astro b/apps/website/src/components/PersonalityCard.astro new file mode 100644 index 0000000..0d874d1 --- /dev/null +++ b/apps/website/src/components/PersonalityCard.astro @@ -0,0 +1,110 @@ +--- +import { brand, type Personality } from '@jscebu/brand'; +import { exampleDoc } from '../lib/view'; +import Frame from './Frame.astro'; +import RuleList from './RuleList.astro'; + +interface Props { + p: Personality; +} + +const { p } = Astro.props; +const font = (id: string) => brand.fonts.find((f) => f.id === id)!.family; +const chips = [ + ['bg', p.colors.bg], + ['text', p.colors.text], + ['muted', p.colors.muted], + ['accent', p.colors.accent], + ['cursor', p.colors.cursor], +] as const; +/** The examples' natural sizes, so they scale to fit on small screens. */ +const size: Record = { poster: [540, 540], terminal: [760, 470], journal: [640, 860] }; +--- + +
+
+

{p.name}

+

{p.use}

+
+ +
    + {chips.map(([k, hex]) =>
  • {k} {hex}
  • )} +
+ +

+ {font(p.type.headline.font)} {p.type.headline.weight}{p.type.headline.uppercase ? ' uppercase' : ''} · {font(p.type.body.font)} {p.type.body.weight} · {font(p.type.mono.font)} {p.type.mono.weight} +

+ +
+
+

Recipe

+ +
+
+

Don't

+ +
+
+ +

Example

+ +
+ + diff --git a/apps/website/src/components/ProjectCard.astro b/apps/website/src/components/ProjectCard.astro new file mode 100644 index 0000000..eeda270 --- /dev/null +++ b/apps/website/src/components/ProjectCard.astro @@ -0,0 +1,76 @@ +--- +import { render, type CollectionEntry } from 'astro:content'; + +interface Props { + entry: CollectionEntry<'projects'>; + /** Show the longer description from the Markdown body, not just the summary. */ + full?: boolean; +} + +const { entry, full = false } = Astro.props; +const { name, url, repo, summary, image, status } = entry.data; +const { Content } = await render(entry); +const host = new URL(url).hostname.replace(/^www\./, ''); +--- + +
+ +
+
+

{name}

+ {status === 'archived' && archived} +
+

{summary}

+ {full &&
} +

+ {host} ↗ + {repo && source ↗} +

+
+
+ + diff --git a/apps/website/src/components/RuleList.astro b/apps/website/src/components/RuleList.astro new file mode 100644 index 0000000..e2d1bab --- /dev/null +++ b/apps/website/src/components/RuleList.astro @@ -0,0 +1,52 @@ +--- +import { inline } from '../lib/view'; + +interface Props { + items: string[]; + tone?: 'plain' | 'check' | 'dont'; +} + +const { items, tone = 'plain' } = Astro.props; +--- + +
    + {items.map((text) =>
  • {inline(text).map(({ part, code }) => (code ? {part} : part))}
  • )} +
+ + diff --git a/apps/website/src/components/Section.astro b/apps/website/src/components/Section.astro new file mode 100644 index 0000000..06c40c5 --- /dev/null +++ b/apps/website/src/components/Section.astro @@ -0,0 +1,45 @@ +--- +interface Props { + id: string; + number: string; + title: string; + tag?: string; +} + +const { id, number, title, tag } = Astro.props; +--- + +
+
+

{number} · {title}

+ {tag && {tag}} +
+ +
+ + diff --git a/apps/website/src/components/SiteFooter.astro b/apps/website/src/components/SiteFooter.astro new file mode 100644 index 0000000..60b134c --- /dev/null +++ b/apps/website/src/components/SiteFooter.astro @@ -0,0 +1,60 @@ +--- +import { brand } from '@jscebu/brand'; + +const links = [ + ['/projects/', 'Projects'], + ['/about/', 'About'], + ['/links/', 'Links'], + ['/brand/', 'Brand'], + ['/brand.md', 'brand.md'], + ['/llms.txt', 'llms.txt'], + ['https://github.com/javascriptcebu/jscebu.org', 'GitHub'], +]; +const mark = brand.assets.find((a) => a.id === 'mark-color')!.path; +--- + +
+
+ JavaScript Cebu +
    + {links.map(([href, label]) =>
  • {label}
  • )} +
+ +
+
+ + diff --git a/apps/website/src/components/SiteNav.astro b/apps/website/src/components/SiteNav.astro new file mode 100644 index 0000000..7e241de --- /dev/null +++ b/apps/website/src/components/SiteNav.astro @@ -0,0 +1,91 @@ +--- +import { brand } from '@jscebu/brand'; + +interface Props { + current?: 'home' | 'projects' | 'about' | 'links' | 'brand'; +} + +const { current } = Astro.props; +const items = [ + { href: '/projects/', label: 'Projects', key: 'projects' }, + { href: '/about/', label: 'About', key: 'about' }, + { href: '/brand/', label: 'Brand', key: 'brand' }, +] as const; +const lockup = brand.assets.find((a) => a.id === 'lockup-dark')!.path; +const mark = brand.assets.find((a) => a.id === 'mark-color')!.path; +--- + + + + diff --git a/apps/website/src/components/Sticker.astro b/apps/website/src/components/Sticker.astro new file mode 100644 index 0000000..255646c --- /dev/null +++ b/apps/website/src/components/Sticker.astro @@ -0,0 +1,58 @@ +--- +/** The logo as a tilted die-cut sticker. The brand allows 6–10° either way. `slap` sticks it on as the page loads. */ +interface Props { + size: string; + tilt?: number; + alt?: string; + class?: string; + slap?: boolean; + /** When the slap starts, so it can land after the headline. */ + delay?: number; +} + +const { size, tilt = -8, alt = '', class: className, slap = false, delay = 600 } = Astro.props; +if (Math.abs(tilt) < 6 || Math.abs(tilt) > 10) { + throw new Error(`Sticker tilt must be 6–10° either way, got ${tilt}`); +} +--- + +{alt} + + diff --git a/apps/website/src/components/Swatch.astro b/apps/website/src/components/Swatch.astro new file mode 100644 index 0000000..e9e8105 --- /dev/null +++ b/apps/website/src/components/Swatch.astro @@ -0,0 +1,54 @@ +--- +import type { Hex } from '@jscebu/brand'; + +interface Props { + name: string; + hex: Hex; + variable?: string; + role?: string; +} + +const { name, hex, variable, role } = Astro.props; +--- + +
+
+
+ {name} + {hex} + {variable && {variable}} + {role &&

{role}

} +
+
+ + diff --git a/apps/website/src/content.config.ts b/apps/website/src/content.config.ts new file mode 100644 index 0000000..3801b7c --- /dev/null +++ b/apps/website/src/content.config.ts @@ -0,0 +1,10 @@ +import { defineCollection } from 'astro:content'; +import { file, glob } from 'astro/loaders'; +import { linkSchema, organizerSchema, pageSchema, projectSchema } from './content/schemas'; + +export const collections = { + projects: defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/projects' }), schema: projectSchema }), + organizers: defineCollection({ loader: file('src/content/organizers.json'), schema: organizerSchema }), + links: defineCollection({ loader: file('src/content/links.json'), schema: linkSchema }), + pages: defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/pages' }), schema: pageSchema }), +}; diff --git a/apps/website/src/content/links.json b/apps/website/src/content/links.json new file mode 100644 index 0000000..21f8a6a --- /dev/null +++ b/apps/website/src/content/links.json @@ -0,0 +1,6 @@ +[ + { "id": "facebook", "label": "Facebook", "url": "https://www.facebook.com/JavaScriptCebu/", "handle": "JavaScriptCebu", "order": 1 }, + { "id": "x", "label": "X (Twitter)", "url": "https://twitter.com/javascriptcebu", "handle": "@JavaScriptCebu", "order": 2 }, + { "id": "github", "label": "GitHub", "url": "https://github.com/javascriptcebu/", "handle": "javascriptcebu", "order": 3 }, + { "id": "email", "label": "Email", "url": "mailto:hello@jscebu.org", "handle": "hello@jscebu.org", "order": 4 } +] diff --git a/apps/website/src/content/organizers.json b/apps/website/src/content/organizers.json new file mode 100644 index 0000000..0b76a49 --- /dev/null +++ b/apps/website/src/content/organizers.json @@ -0,0 +1,6 @@ +[ + { "id": "cyrus-hiyas", "name": "Cyrus Hiyas", "role": "Lead", "community": "Angular Cebu", "order": 1 }, + { "id": "dorell-james", "name": "Dorell James", "role": "Lead", "community": "React Cebu", "order": 2 }, + { "id": "harlequin-doyon", "name": "Harlequin Doyon", "role": "Lead", "community": "Vue.js Cebu", "order": 3 }, + { "id": "sieg-balona", "name": "Sieg Balona", "role": "Co-Lead", "community": "React Cebu", "order": 4 } +] diff --git a/apps/website/src/content/pages/about.md b/apps/website/src/content/pages/about.md new file mode 100644 index 0000000..09685f2 --- /dev/null +++ b/apps/website/src/content/pages/about.md @@ -0,0 +1,27 @@ +--- +title: About JS Cebu +description: JavaScript Cebu's mission, vision, history and organizers. +--- + +## Our mission + +JavaScript Cebu aims to empower and unite developers by providing a collaborative platform for learning, sharing, and innovation in JavaScript and related technologies. We strive to enhance the technological landscape of Cebu through community-driven events such as meetups, codecamps, and other educational gatherings. + +## Our vision + +To be the leading catalyst for technological advancement and community collaboration in Cebu, fostering a vibrant ecosystem where developers can grow, innovate, and contribute to the global tech scene. + +## How we measure success + +We use the following metrics time to time to see how we're doing: + +- **Growth in Membership:** Increasing the number of active participants and registered members attending our events. +- **Event Frequency and Diversity:** Successfully hosting a variety of events regularly, including meetups, codecamps, and special tech talks. +- **Community Engagement:** High levels of engagement in community discussions, projects, and collaborations as evidenced by feedback, social media interactions, and community-driven projects. +- **Partnerships and Sponsorships:** Establishing strong partnerships with local and international tech companies and securing sponsorships that support the sustainability and expansion of our events. +- **Success Stories:** Documenting and showcasing success stories of members who have advanced in their careers or launched successful projects as a result of our community support. +- **Positive Remarks:** We always aim to be the type of organization for the people and by the people. + +## History + +JavaScript Cebu's GitHub organization was created in December 2018, and the community launched in 2019, sprouting from Laravel Cebu and Vue.js Cebu. For years it was home to three sub-communities, Angular Cebu, React Cebu and Vue.js Cebu, which ran their own meetups and workshops under the JS Cebu banner. Today JS Cebu runs as one community for everyone building with JavaScript in Cebu, and it co-presents Hacktoberfest Cebu every October. diff --git a/apps/website/src/content/projects/cebby.md b/apps/website/src/content/projects/cebby.md new file mode 100644 index 0000000..49f4abc --- /dev/null +++ b/apps/website/src/content/projects/cebby.md @@ -0,0 +1,12 @@ +--- +name: Cebby +url: https://www.getcebby.com +repo: https://github.com/getcebby/cebby +summary: Cebby is a public discovery layer for Cebu tech events. +image: ../../assets/projects/cebby.png +status: active +featured: true +order: 1 +--- + +Cebby gathers Cebu tech events from Luma, Eventbrite, Meetup and Facebook into one calm, glanceable feed, so you can see what's happening this week without following every page. It's built by the community and open source. diff --git a/apps/website/src/content/schemas.ts b/apps/website/src/content/schemas.ts new file mode 100644 index 0000000..30a3a83 --- /dev/null +++ b/apps/website/src/content/schemas.ts @@ -0,0 +1,36 @@ +import { z } from 'astro/zod'; +import type { SchemaContext } from 'astro:content'; + +/** Takes Astro's schema context so `image()` resolves local images; tests pass a stand-in. */ +export const projectSchema = ({ image }: Pick) => + z.object({ + name: z.string(), + url: z.string().url(), + repo: z.string().url().optional(), + summary: z.string(), + image: image(), + status: z.enum(['active', 'archived']), + featured: z.boolean(), + order: z.number(), + }); + +export const organizerSchema = z.object({ + id: z.string(), + name: z.string(), + role: z.string(), + community: z.string().optional(), + order: z.number(), +}); + +export const linkSchema = z.object({ + id: z.string(), + label: z.string(), + url: z.string(), + handle: z.string().optional(), + order: z.number(), +}); + +export const pageSchema = z.object({ + title: z.string(), + description: z.string(), +}); diff --git a/apps/website/src/layouts/Site.astro b/apps/website/src/layouts/Site.astro new file mode 100644 index 0000000..4367656 --- /dev/null +++ b/apps/website/src/layouts/Site.astro @@ -0,0 +1,149 @@ +--- +import SiteFooter from '../components/SiteFooter.astro'; +import SiteNav from '../components/SiteNav.astro'; +import Terminal from './Terminal.astro'; + +interface Props { + title: string; + description: string; + current?: 'home' | 'projects' | 'about' | 'links' | 'brand'; + index?: boolean; +} + +const { title, description, current, index } = Astro.props; +--- + + + +
+ +
+ +
+ + + + diff --git a/apps/website/src/layouts/Terminal.astro b/apps/website/src/layouts/Terminal.astro new file mode 100644 index 0000000..d333b68 --- /dev/null +++ b/apps/website/src/layouts/Terminal.astro @@ -0,0 +1,61 @@ +--- +import '@fontsource-variable/archivo'; +import '@fontsource-variable/space-grotesk'; +import '@fontsource-variable/inter-tight'; +import '@fontsource-variable/jetbrains-mono'; +import { brand, tokensCss } from '@jscebu/brand'; + +interface Props { + title: string; + description: string; + /** False for pages search engines shouldn't list, like the 404 page. */ + index?: boolean; +} + +const { title, description, index = true } = Astro.props; +const canonical = new URL(Astro.url.pathname, Astro.site); +const shareImage = new URL(brand.assets.find((a) => a.id === 'photo')!.path, Astro.site); +--- + + + + + + + {title} + + {index ? : } + + + + + {index && } + + + + + + + + + + + + diff --git a/apps/website/src/lib/view.ts b/apps/website/src/lib/view.ts new file mode 100644 index 0000000..97d2bab --- /dev/null +++ b/apps/website/src/lib/view.ts @@ -0,0 +1,23 @@ +import { brand, type Asset, type FontToken } from '@jscebu/brand'; + +/** Split rule text on backticks so `code` spans can render as . */ +export const inline = (text: string) => text.split('`').map((part, i) => ({ part, code: i % 2 === 1 })); + +/** The tokens.css variable that holds a font's stack. */ +export function fontVar(f: FontToken): string { + if (f.id === 'jetbrainsMono') return 'var(--jc-font-mono)'; + if (f.id === 'roboto') return 'var(--jc-font-wordmark)'; + const p = brand.personalities.find((x) => x.type.headline.font === f.id); + if (!p) throw new Error(`No personality uses ${f.id}`); + return `var(--jc-font-${p.id})`; +} + +export function asset(id: string): Asset { + const a = brand.assets.find((x) => x.id === id); + if (!a) throw new Error(`Unknown asset ${id}`); + return a; +} + +/** A standalone document for an example snippet, with brand URLs made same-origin so previews work before launch. */ +export const exampleDoc = (html: string) => + `${html.replaceAll(`${brand.site}/brand/`, '/brand/')}`; diff --git a/apps/website/src/pages/404.astro b/apps/website/src/pages/404.astro new file mode 100644 index 0000000..c855b82 --- /dev/null +++ b/apps/website/src/pages/404.astro @@ -0,0 +1,18 @@ +--- +import Cursor from '../components/Cursor.astro'; +import Site from '../layouts/Site.astro'; +--- + + +
+

// 404

+

+

That page doesn't exist. Try the home page or the brand guidelines.

+
+
+ + diff --git a/apps/website/src/pages/about.astro b/apps/website/src/pages/about.astro new file mode 100644 index 0000000..898cff1 --- /dev/null +++ b/apps/website/src/pages/about.astro @@ -0,0 +1,99 @@ +--- +import { getCollection, getEntry, render } from 'astro:content'; +import Cursor from '../components/Cursor.astro'; +import Site from '../layouts/Site.astro'; + +const page = (await getEntry('pages', 'about'))!; +const { Content } = await render(page); +const organizers = (await getCollection('organizers')).sort((a, b) => a.data.order - b.data.order); +--- + + +
+
+

// about

+

+
+
+ +
+
+

Organizers

+
    + { + organizers.map(({ data }, i) => ( +
  • + {data.name} + + {data.role} + {data.community && ` · ${data.community}`} + +
  • + )) + } +
+
+
+
+ + diff --git a/apps/website/src/pages/brand.md.ts b/apps/website/src/pages/brand.md.ts new file mode 100644 index 0000000..c18908b --- /dev/null +++ b/apps/website/src/pages/brand.md.ts @@ -0,0 +1,5 @@ +import type { APIRoute } from 'astro'; +import { brand, brandMd } from '@jscebu/brand'; + +export const GET: APIRoute = () => + new Response(brandMd(brand), { headers: { 'Content-Type': 'text/markdown; charset=utf-8' } }); diff --git a/apps/website/src/pages/brand/index.astro b/apps/website/src/pages/brand/index.astro new file mode 100644 index 0000000..14e9039 --- /dev/null +++ b/apps/website/src/pages/brand/index.astro @@ -0,0 +1,685 @@ +--- +import { brand, contrastRatio } from '@jscebu/brand'; +import Cursor from '../../components/Cursor.astro'; +import Frame from '../../components/Frame.astro'; +import PersonalityCard from '../../components/PersonalityCard.astro'; +import RuleList from '../../components/RuleList.astro'; +import Section from '../../components/Section.astro'; +import Sticker from '../../components/Sticker.astro'; +import Swatch from '../../components/Swatch.astro'; +import Site from '../../layouts/Site.astro'; +import { asset, fontVar } from '../../lib/view'; + +const agentUrl = `${brand.site}/brand.md`; + +/** Each logo version on the background it is meant for. */ +const logos = [ + { id: 'lockup-light', bg: 'paper' }, + { id: 'lockup-dark', bg: 'ink' }, + { id: 'lockup-ink', bg: 'yellow' }, + { id: 'lockup-white', bg: 'photo' }, + { id: 'sticker', bg: 'yellow' }, + { id: 'mark-color', bg: 'paper' }, + { id: 'mark-ink', bg: 'yellow' }, + { id: 'mark-white', bg: 'ink' }, +].map((l) => ({ ...l, asset: asset(l.id) })); + +const nav = [ + ['logo', 'Logo'], + ['color', 'Color'], + ['type', 'Type'], + ['signatures', 'Signatures'], + ['personalities', 'Personalities'], + ['voice', 'Voice'], + ['downloads', 'Downloads'], +]; +const sample = 'Aa Bb Cc 0123456789'; +--- + + + + +
+
+
+

// brand · v{brand.version}

+

+

Everything you need to make something for JavaScript Cebu: logos, colors, type, rules and templates. Using an AI agent? Give it the brand.md link. It has the same rules in a form agents can follow.

+ + +
+ +
+

Check every piece

+ r.text)} tone="check" /> +
+
+ + + +
+
+ {brand.colors.map((c) => )} +
+

Text pairs, checked against WCAG AA

+
    + { + brand.textPairs.map((p) => ( +
  • + Aa + {contrastRatio(p.fg, p.bg).toFixed(1)}:1 + {p.where} +
  • + )) + } +
+
+ +
+
    + { + brand.fonts.map((f) => ( +
  • +
    + {f.family} + {f.weights.join(' · ')} +
    + {f.id === 'roboto' ? ( + The JAVASCRIPT / CEBU wordmark in Roboto Black + ) : ( + f.weights.map((w) =>

    {f.family} · {sample}

    ) + )} +

    {f.role}

    + Google Fonts ↗ +
  • + )) + } +
+
+ +
+
    + { + brand.signatures.map((s) => ( +
  1. +
    + 0{s.rank} +

    {s.name}

    + {s.status} +
    +

    {s.summary}

    +
    + {s.id === 'cursor' && {'word'}} + {s.id === 'sticker' && The JS Cebu sticker, tilted} + {s.id === 'bisaya' &&

    {brand.voice.phrases.map((p) => p.bisaya).join(' · ')}{brand.voice.phrases.some((p) => p.status === 'draft') && drafts, not yet approved}

    } + {s.id === 'photos' && The JS Cebu community at a meetup} +
    + +
  2. + )) + } +
+
+ +
+

Three looks, kept distinct: different typefaces, layouts and moods. Yellow and the signatures tie them together. Each example below is the same HTML that brand.md gives agents.

+
+ {brand.personalities.map((p) => )} +
+
+ +
+

After every event, the facebook.com/javascriptcebu cover becomes a real photo from it. Only the sticker, a caption and a dark gradient go on top.

+ +

{asset('fb-cover').label} · {asset('fb-cover').use}

+
+ +
+ +
+ + + + + + { + brand.voice.phrases.map((p) => ( + + + + + + + )) + } + +
BisayaEnglishUseStatus
{p.bisaya}{p.english}{p.use}{p.status === 'draft' ? 'draft, not yet approved' : 'approved'}
+
+
+ +
+ +
+ +
+ { + brand.changelog.map((v) => ( +
+

v{v.version} {v.date}

+ +
+ )) + } +
+
+
+ + + + diff --git a/apps/website/src/pages/brand/tokens.css.ts b/apps/website/src/pages/brand/tokens.css.ts new file mode 100644 index 0000000..5b1a8f8 --- /dev/null +++ b/apps/website/src/pages/brand/tokens.css.ts @@ -0,0 +1,5 @@ +import type { APIRoute } from 'astro'; +import { brand, tokensCss } from '@jscebu/brand'; + +export const GET: APIRoute = () => + new Response(tokensCss(brand), { headers: { 'Content-Type': 'text/css; charset=utf-8' } }); diff --git a/apps/website/src/pages/index.astro b/apps/website/src/pages/index.astro new file mode 100644 index 0000000..422d930 --- /dev/null +++ b/apps/website/src/pages/index.astro @@ -0,0 +1,247 @@ +--- +import { brand } from '@jscebu/brand'; +import { getCollection } from 'astro:content'; +import Cursor from '../components/Cursor.astro'; +import ProjectCard from '../components/ProjectCard.astro'; +import Sticker from '../components/Sticker.astro'; +import Site from '../layouts/Site.astro'; + +const featured = (await getCollection('projects', ({ data }) => data.featured)).sort((a, b) => a.data.order - b.data.order); +const links = (await getCollection('links')).sort((a, b) => a.data.order - b.data.order); +const photo = brand.assets.find((a) => a.id === 'photo')!.path; + +const work = [ + { title: 'Meetups', text: 'Talks, demos and lightning talks from developers building in Cebu.' }, + { title: 'Workshops', text: 'Hands-on sessions, from automated testing to building with AI.' }, + { title: 'Hacktoberfest Cebu', text: 'A month of open source every October, with partner communities.', href: 'https://hacktoberfest.jscebu.org' }, +]; +--- + + +
+
+
+

// cebu's javascript community

+

+

Meetups, workshops and Hacktoberfest, run by developers in Cebu.

+ +
+ +
+ +
+

What we do

+ +
+ +
+

Upcoming events

+

Our next meetups and workshops, along with every other Cebu tech event, are on Cebby.

+ +
+ +
+

Featured project

+
+ {featured.map((entry) => )} +
+ All projects → +
+ +
+
+ The JS Cebu community at a meetup +
The JS Cebu community at a meetup
+
+
+ +
+

Join us

+

Come to a meetup, follow along online, or say hello.

+ +
+
+
+ + diff --git a/apps/website/src/pages/links.astro b/apps/website/src/pages/links.astro new file mode 100644 index 0000000..7e82bba --- /dev/null +++ b/apps/website/src/pages/links.astro @@ -0,0 +1,69 @@ +--- +import { getCollection } from 'astro:content'; +import Cursor from '../components/Cursor.astro'; +import Site from '../layouts/Site.astro'; + +const links = (await getCollection('links')).sort((a, b) => a.data.order - b.data.order); +--- + + +
+
+

// official links

+

+
+ +
+
+ + diff --git a/apps/website/src/pages/llms.txt.ts b/apps/website/src/pages/llms.txt.ts new file mode 100644 index 0000000..e4a2cb5 --- /dev/null +++ b/apps/website/src/pages/llms.txt.ts @@ -0,0 +1,5 @@ +import type { APIRoute } from 'astro'; +import { brand, llmsTxt } from '@jscebu/brand'; + +export const GET: APIRoute = () => + new Response(llmsTxt(brand), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }); diff --git a/apps/website/src/pages/projects.astro b/apps/website/src/pages/projects.astro new file mode 100644 index 0000000..514e8d4 --- /dev/null +++ b/apps/website/src/pages/projects.astro @@ -0,0 +1,30 @@ +--- +import { getCollection } from 'astro:content'; +import Cursor from '../components/Cursor.astro'; +import ProjectCard from '../components/ProjectCard.astro'; +import Site from '../layouts/Site.astro'; + +const projects = (await getCollection('projects')).sort((a, b) => a.data.order - b.data.order); +--- + + +
+
+

// projects

+

+

Things the JS Cebu community builds and runs, in the open.

+
+
+ {projects.map((entry, i) =>
)} +
+
+
+ + diff --git a/apps/website/src/pages/robots.txt.ts b/apps/website/src/pages/robots.txt.ts new file mode 100644 index 0000000..6c12151 --- /dev/null +++ b/apps/website/src/pages/robots.txt.ts @@ -0,0 +1,6 @@ +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = ({ site }) => + new Response(`User-agent: *\nAllow: /\n\nSitemap: ${new URL('/sitemap.xml', site)}\n`, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); diff --git a/apps/website/src/pages/sitemap.xml.ts b/apps/website/src/pages/sitemap.xml.ts new file mode 100644 index 0000000..bc01f7c --- /dev/null +++ b/apps/website/src/pages/sitemap.xml.ts @@ -0,0 +1,10 @@ +import type { APIRoute } from 'astro'; + +const pages = ['/', '/projects/', '/about/', '/links/', '/brand/']; + +export const GET: APIRoute = ({ site }) => { + const urls = pages.map((p) => ` ${new URL(p, site)}`).join('\n'); + return new Response(`\n\n${urls}\n\n`, { + headers: { 'Content-Type': 'application/xml; charset=utf-8' }, + }); +}; diff --git a/apps/website/test/check-dist.test.ts b/apps/website/test/check-dist.test.ts new file mode 100644 index 0000000..21e470a --- /dev/null +++ b/apps/website/test/check-dist.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest'; +import { brand } from '@jscebu/brand'; +import { findCursorViolations, findLiterals, findMissingLinks, findParityGaps, parityRules } from '../scripts/check-dist.ts'; + +const cursor = ''; + +describe('findCursorViolations', () => { + it('accepts the last word wrapped with the cursor', () => { + expect(findCursorViolations(`

How JS Cebu looks${cursor}

`)).toEqual([]); + }); + + it('flags a bare cursor', () => { + expect(findCursorViolations(`

How JS Cebu looks ${cursor}

`)).toHaveLength(1); + }); + + it('flags a wrapper glued to the previous word (whitespace eaten by HTML compression)', () => { + expect(findCursorViolations(`

How JS Cebulooks${cursor}

`)).toHaveLength(1); + }); +}); + +describe('findMissingLinks', () => { + const exists = (path: string) => path !== '/brand/logo/missing.svg'; + + it('reports a missing /brand asset, root-relative or absolute', () => { + expect(findMissingLinks('', exists)).toEqual(['/brand/logo/missing.svg']); + expect(findMissingLinks('See https://www.jscebu.org/brand/logo/missing.svg.', exists)).toEqual(['/brand/logo/missing.svg']); + }); + + it('checks every path on this site, so a link to a redirect source counts as missing', () => { + expect(findMissingLinks('xy', (p) => p === '/about/')).toEqual(['/events']); + }); + + it('ignores other origins, mailto links and in-page anchors', () => { + expect(findMissingLinks('ymj', () => false)).toEqual([]); + }); +}); + +describe('findParityGaps', () => { + const rules = ['JS Cebu yellow `#FFDF00` appears.', 'No hexagon shapes besides the logo.']; + + it('passes when every rule is in both the page and brand.md', () => { + expect(findParityGaps('JS Cebu yellow #FFDF00 appears. No hexagon shapes besides the logo.', `- [ ] ${rules[0]}\n- [ ] ${rules[1]}`, rules)).toEqual([]); + }); + + it('reports a rule missing from either side', () => { + const gaps = findParityGaps('JS Cebu yellow #FFDF00 appears.', `- [ ] ${rules[1]}`, rules); + expect(gaps).toHaveLength(2); + expect(gaps.join('\n')).toMatch(/page.*hexagon/i); + expect(gaps.join('\n')).toMatch(/brand\.md.*yellow/i); + }); +}); + +describe('findLiterals', () => { + it('flags hex colors and font families in source', () => { + const found = findLiterals([{ path: 'src/a.astro', text: '' }]); + expect(found).toHaveLength(2); + expect(found[0]).toMatch(/^src\/a\.astro:2:/); + }); + + it('allows the brand variables', () => { + expect(findLiterals([{ path: 'src/b.astro', text: 'p { color: var(--jc-yellow); font-family: var(--jc-font-mono); }' }])).toEqual([]); + }); +}); + +describe('parityRules', () => { + it('covers every rule list the page and brand.md both show', () => { + const rules = parityRules(brand); + const expected = [ + ...brand.must.map((r) => r.text), + ...brand.logoRules, + ...brand.signatures.flatMap((s) => s.rules), + ...brand.personalities.flatMap((p) => [...p.recipe, ...p.dont]), + ...brand.voice.principles, + ]; + expect(rules).toEqual(expect.arrayContaining(expected)); + expect(rules).toHaveLength(expected.length); + }); +}); diff --git a/apps/website/test/content.test.ts b/apps/website/test/content.test.ts new file mode 100644 index 0000000..d08b4f4 --- /dev/null +++ b/apps/website/test/content.test.ts @@ -0,0 +1,44 @@ +import { readFileSync } from 'node:fs'; +import { z } from 'astro/zod'; +import { parse } from 'yaml'; +import { describe, expect, it } from 'vitest'; +import { linkSchema, organizerSchema, pageSchema, projectSchema } from '../src/content/schemas.ts'; + +const content = new URL('../src/content/', import.meta.url); +const read = (path: string) => readFileSync(new URL(path, content), 'utf8'); +const frontmatter = (md: string) => parse(md.match(/^---\n([\s\S]*?)\n---/)![1]); + +describe('site content', () => { + it('lists the four organizers from /members, in order', () => { + const organizers = z.array(organizerSchema).parse(JSON.parse(read('organizers.json'))); + expect(organizers.map((o) => [o.name, o.role])).toEqual([ + ['Cyrus Hiyas', 'Lead'], + ['Dorell James', 'Lead'], + ['Harlequin Doyon', 'Lead'], + ['Sieg Balona', 'Co-Lead'], + ]); + }); + + it('lists the official channels from /official-links', () => { + const links = z.array(linkSchema).parse(JSON.parse(read('links.json'))); + expect(links.map((l) => l.url)).toEqual( + expect.arrayContaining([ + 'https://www.facebook.com/JavaScriptCebu/', + 'https://twitter.com/javascriptcebu', + 'https://github.com/javascriptcebu/', + 'mailto:hello@jscebu.org', + ]), + ); + }); + + it('features Cebby as the only project', () => { + const cebby = projectSchema({ image: () => z.string() }).parse(frontmatter(read('projects/cebby.md'))); + expect(cebby).toMatchObject({ name: 'Cebby', url: 'https://www.getcebby.com', featured: true, status: 'active' }); + }); + + it('keeps the mission verbatim on the about page', () => { + const about = read('pages/about.md'); + pageSchema.parse(frontmatter(about)); + expect(about).toContain('JavaScript Cebu aims to empower and unite developers'); + }); +}); diff --git a/apps/website/test/motion.test.ts b/apps/website/test/motion.test.ts new file mode 100644 index 0000000..db43fe7 --- /dev/null +++ b/apps/website/test/motion.test.ts @@ -0,0 +1,13 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const site = readFileSync(new URL('../src/layouts/Site.astro', import.meta.url), 'utf8'); + +describe('scroll reveal', () => { + it('only hides not-yet-revealed content on screens, so printing or saving as PDF shows everything', () => { + const hide = site.indexOf('.motion [data-reveal] {'); + expect(hide).toBeGreaterThan(0); + const media = site.slice(site.lastIndexOf('@media', hide), hide); + expect(media).toMatch(/^@media screen and \(prefers-reduced-motion: no-preference\)/); + }); +}); diff --git a/apps/website/test/redirects.test.ts b/apps/website/test/redirects.test.ts new file mode 100644 index 0000000..27b0f49 --- /dev/null +++ b/apps/website/test/redirects.test.ts @@ -0,0 +1,30 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { parseRedirects } from '../scripts/check-dist.ts'; + +describe('parseRedirects', () => { + it('skips comments and blank lines and defaults the status to 302', () => { + expect(parseRedirects('# old pages\n\n/a /b\n/c https://x.example 301\n')).toEqual([ + { from: '/a', to: '/b', status: 302 }, + { from: '/c', to: 'https://x.example', status: 301 }, + ]); + }); +}); + +describe('public/_redirects', () => { + it('sends every old jscebu.org URL where the v1 spec says (§5)', () => { + const rules = parseRedirects(readFileSync(new URL('../public/_redirects', import.meta.url), 'utf8')); + expect(rules).toEqual([ + { from: '/mission-vision', to: '/about/', status: 301 }, + { from: '/members', to: '/about/#organizers', status: 301 }, + { from: '/iniatives', to: '/about/', status: 301 }, + { from: '/initiatives', to: '/about/', status: 301 }, + { from: '/official-links', to: '/links/', status: 301 }, + { from: '/community-projects', to: '/projects/', status: 301 }, + { from: '/events', to: '/#events', status: 302 }, + { from: '/promotional-materials', to: '/brand/', status: 301 }, + { from: '/annual-hacktoberfest', to: 'https://hacktoberfest.jscebu.org/', status: 301 }, + { from: '/volunteers-training-workshop', to: '/about/', status: 301 }, + ]); + }); +}); diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json new file mode 100644 index 0000000..17a7c31 --- /dev/null +++ b/apps/website/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "allowImportingTsExtensions": true, + "types": ["node"] + }, + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist", "public"] +} diff --git a/apps/website/worker/index.ts b/apps/website/worker/index.ts new file mode 100644 index 0000000..12f7a61 --- /dev/null +++ b/apps/website/worker/index.ts @@ -0,0 +1,11 @@ +/** Runs only on asset misses: answer with the site's 404 page and a real 404 status. */ +interface Env { + ASSETS: { fetch: (input: Request | URL | string) => Promise }; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const page = await env.ASSETS.fetch(new URL('/404.html', request.url)); + return new Response(page.body, { status: 404, headers: page.headers }); + }, +}; diff --git a/apps/website/wrangler.jsonc b/apps/website/wrangler.jsonc new file mode 100644 index 0000000..4ccf7b0 --- /dev/null +++ b/apps/website/wrangler.jsonc @@ -0,0 +1,16 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "jscebu-org", + "main": "worker/index.ts", + "compatibility_date": "2026-10-28", + "workers_dev": true, + // Static assets are served first; the Worker only runs when no asset matches. + "assets": { + "directory": "./dist", + "binding": "ASSETS", + "not_found_handling": "404-page" + }, + // www.jscebu.org is attached as a Workers Custom Domain (Cloudflare manages its DNS + // record and certificate). The apex jscebu.org still redirects to www via Vercel. + "routes": [{ "pattern": "www.jscebu.org", "custom_domain": true }] +} diff --git a/docs/superpowers/plans/2026-10-28-jscebu-site-v1.md b/docs/superpowers/plans/2026-10-28-jscebu-site-v1.md new file mode 100644 index 0000000..366d897 --- /dev/null +++ b/docs/superpowers/plans/2026-10-28-jscebu-site-v1.md @@ -0,0 +1,199 @@ +# jscebu.org v1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship the jscebu.org landing page plus `/projects` (Cebby), `/about` and `/links` on the existing Astro app, redirect every old Notion URL, and swap www.jscebu.org to the `jscebu-org` Worker. + +**Architecture:** +- The app is the static Astro 7 site in `apps/website`. +- Content lives in Markdown/JSON content collections, validated with zod schemas. +- All pages share a `Site` layout (nav and footer) on top of the existing `Terminal` layout. +- Old URLs are redirected by a `_redirects` file that Workers static assets applies before asset matching. +- `check-dist` grows to cover every built page. +- The swap attaches `www.jscebu.org` to the Worker as a Custom Domain. + +**Tech Stack:** Astro 7.4 (content layer: `glob` and `file` loaders from `astro/loaders`, zod from `astro/zod`), Vitest 5, Wrangler 4, linkedom. + +**Spec:** `docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md`. It builds on `docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md`. + +## Global Constraints + +- Every page uses the Terminal personality. There must be no hex, `rgb()`, `hsl()` or font-family literals in `apps/website/src`; `check-dist` enforces this. +- The primary headline on each page ends with ``, wrapped in `.jc-nw`. There is one cursor per page, and only the landing hero's cursor blinks. +- No Bisaya copy until Dorell approves the phrases. +- Content comes from the current site: + - Mission and vision, verbatim from `/mission-vision`. + - The four leads, from `/members`. + - The channels, from `/official-links`. + - Cebby's description: "Cebby is a public discovery layer for Cebu tech events", linking https://www.getcebby.com and https://github.com/getcebby/cebby. +- The upcoming-events links are https://www.getcebby.com and https://www.facebook.com/JavaScriptCebu/events. +- There is no blog link, because blog.jscebu.org returns 403. +- Work happens on branch `feat/site-v1`, stacked on `feat/brand-v0.1`. The PR targets `feat/brand-v0.1` until PR #1 merges. +- Commit messages: an imperative subject of about 50 characters; the body has one line per paragraph with no hard wrapping. +- **Outward actions:** + - The workers.dev preview deploy is approved. + - Deleting the www DNS record and attaching the custom domain each need Dorell's explicit "go". + +## Review Focus + +1. **Someone following an old link** (`/members`, `/iniatives`, `/events`, the three Notion "page not found" pages): they must land on the target in spec §5 with its status. A redirect into a 404 is a failure. Owners: Task 4 (`_redirects` table test) and Tasks 5–6 (live URL matrix). +2. **A visitor on a 320px phone:** no horizontal scroll on `/`, `/projects`, `/about`, `/links`, `/brand`. Owner: Task 5 (iframe harness, since headless Chrome won't lay out below 500px). +3. **A nav or footer link to a path that doesn't exist:** the build must fail. Owner: Task 4 (`findMissingLinks` over every page, with no "ours" filter, and redirect sources counting as missing). +4. **An agent fetching `/brand.md` after the swap:** it must still get `text/markdown; charset=utf-8` with CORS. Owner: Task 6. +5. **The swap gap:** between deleting the CNAME and attaching the domain, www is down. The rollback commands must be written down and tested for syntax before the "go". Owner: Task 6. + +--- + +## File Structure + +``` +apps/website/ + src/content.config.ts collections: projects, organizers, links, pages + src/content/schemas.ts zod schemas (shared by config and tests) + src/content/projects/cebby.md src/content/organizers.json src/content/links.json + src/content/pages/about.md + src/assets/projects/cebby.png Cebby icon (copied from getcebby/cebby) + src/layouts/Site.astro Terminal + SiteNav + slot + SiteFooter + src/components/SiteNav.astro SiteFooter.astro ProjectCard.astro + src/pages/index.astro projects.astro about.astro links.astro sitemap.xml.ts robots.txt.ts + public/_redirects + scripts/check-dist.ts all pages; parseRedirects + scripts/url-matrix.sh old/new URL checks against a base URL + test/content.test.ts test/redirects.test.ts +``` + +--- + +### Task 1: Content collections and data + +**Files:** Create `src/content.config.ts`, `src/content/schemas.ts`, the content files above and `src/assets/projects/cebby.png`. Test: `test/content.test.ts`. + +**Interfaces (produces):** +- `projectSchema({ image })`: + - `name: string`, `url: url`, `repo?: url`, `summary: string` + - `image: image()` + - `status: 'active' | 'archived'`, `featured: boolean`, `order: number` +- `organizerSchema`: `{ id, name, role, community?, order }`. The `file()` loader needs an `id` per entry. +- `linkSchema`: `{ id, label, url, handle?, order }`. +- `pageSchema`: `{ title, description }`. +- `collections = { projects: glob('**/*.md', base: 'src/content/projects'), organizers: file('src/content/organizers.json'), links: file('src/content/links.json'), pages: glob('**/*.md', base: 'src/content/pages') }`. + +- [ ] **Step 1: Write the failing test.** `content.test.ts` reads the JSON files and the frontmatter of each Markdown file. It parses them with the schemas; for the image field, a stub `z.string()` stands in. Assert: + - The organizers are exactly Cyrus Hiyas, Dorell James, Harlequin Doyon and Sieg Balona, with roles Lead, Lead, Lead, Co-Lead, in that order. + - The links include `https://www.facebook.com/JavaScriptCebu/`, `https://twitter.com/javascriptcebu`, `https://github.com/javascriptcebu/` and `mailto:hello@jscebu.org`. + - The only project is Cebby (`featured: true`, url `https://www.getcebby.com`). + - `about.md` contains the mission's first sentence verbatim: "JavaScript Cebu aims to empower and unite developers". +- [ ] **Step 2: Run** `pnpm --filter website test`. Expected: FAIL (the schemas module is missing). +- [ ] **Step 3: Implement** the schemas, config and data. + - `about.md` holds the Mission, Vision and "How we measure success" sections verbatim, plus a short History section (spec §3). + - Copy Cebby's icon from `~/Projects/cebby/apps/pwa/public/` (choose the largest PNG app icon). +- [ ] **Step 4: Run** the tests, then `pnpm --filter website build`. Expected: PASS, and Astro reports the 4 collections. +- [ ] **Step 5: Commit:** `feat(website): add content collections for the site`. + +### Task 2: Shared layout, and the /projects, /about and /links pages + +**Files:** Create `src/layouts/Site.astro`, `src/components/{SiteNav,SiteFooter,ProjectCard}.astro`, and `src/pages/{projects,about,links}.astro`. Modify `src/pages/brand/index.astro` and `src/pages/404.astro` to use `Site`. + +**Interfaces:** +- `Site.astro` props: `{ title: string; description: string; current?: 'home' | 'projects' | 'about' | 'links' | 'brand' }`. + - It renders `Terminal`, then `SiteNav`, a `
` slot, and `SiteFooter`. +- `SiteNav`: + - The lockup-dark logo links to `/`. Then the links Projects, About, Brand. + - `aria-current="page"` on the current link. + - It collapses to the logo plus links on one line from 320px up; no hamburger menu. +- `/brand` keeps its section links as a second row under `SiteNav`, and keeps its `brand.md` link. +- `SiteFooter`: + - Links to Projects, About, Links, Brand, `/brand.md`, the GitHub repo and `/llms.txt`. + - The license sentence from `/brand`'s footer. +- `ProjectCard` props: `{ entry: CollectionEntry<'projects'> }`. +- Each page's H1 ends with `` (non-blinking): + - `/projects` "Community projects" + - `/about` "About JS Cebu" + - `/links` "Find JS Cebu" + +- [ ] **Step 1:** Build the layout and pages from the collections (`getCollection`, `render`). +- [ ] **Step 2: Run** `pnpm --filter website build`. Expected: `check-dist` passes, including `/brand` parity. +- [ ] **Step 3: Commit:** `feat(website): add site layout and about, links, projects`. + +### Task 3: The landing page + +**Files:** Create `src/pages/index.astro`. + +- [ ] **Step 1:** Build `/` with `Site` (`current: 'home'`). Sections, in order, with these ids: + - `hero`: + - The label `// cebu's javascript community`. + - The H1 ``. + - One line: "Meetups, workshops and Hacktoberfest, run by developers in Cebu." + - Buttons: "Upcoming events ↓" (`#events`) and "Join us" (`#join`). + - `` on the right on desktop, in the flow on mobile, as on `/brand`. + - `what-we-do`: three cards (Meetups, Workshops, Hacktoberfest Cebu), each one line. Hacktoberfest links to `https://hacktoberfest.jscebu.org`. + - `events`: a heading, one line saying events are listed on Cebby, and buttons to Cebby (primary) and Facebook events. + - `projects`: the featured project's `ProjectCard`, plus a link to `/projects`. + - `community`: the real community photo (`.jc-photo`, unfiltered, `/brand/photos/community-meetup.jpg`), with a mono caption. + - `join`: the `links` collection as buttons. +- [ ] **Step 2: Run** `pnpm --filter website build`. Expected: PASS. +- [ ] **Step 3: Commit:** `feat(website): add the landing page`. + +### Task 4: Every page checked, redirects, sitemap + +**Files:** Modify `scripts/check-dist.ts`. Create `public/_redirects`, `src/pages/{sitemap.xml,robots.txt}.ts` and `test/redirects.test.ts`. Modify `test/check-dist.test.ts`. + +**Interfaces:** +- `parseRedirects(text: string): { from: string; to: string; status: number }[]`. Comments and blank lines are skipped; the status defaults to 302. +- `findMissingLinks(text, fileExists)` checks every root-relative and `https://www.jscebu.org/…` path; the "ours" filter is removed. Redirect sources are not files, so linking to one counts as missing. +- `check-dist` runs the cursor and link checks over every `dist/**/*.html`, `brand.md` and `llms.txt`. Parity stays on `/brand`, and the literal check stays on `src/**`. It also checks that every redirect target that is a path on this site exists. + +- [ ] **Step 1: Write the failing tests.** + - `redirects.test.ts` reads `public/_redirects` and asserts it equals spec §5, row for row: + - `/mission-vision` → `/about` 301 + - `/members` → `/about#organizers` 301 + - `/iniatives` → `/about` 301 + - `/initiatives` → `/about` 301 + - `/official-links` → `/links` 301 + - `/community-projects` → `/projects` 301 + - `/events` → `/#events` 302 + - `/promotional-materials` → `/brand` 301 + - `/annual-hacktoberfest` → `https://hacktoberfest.jscebu.org` 301 + - `/volunteers-training-workshop` → `/about` 301 + - In `check-dist.test.ts`, replace the "ignores pages the legacy site serves" case with: `/events` is reported missing when it isn't a file. +- [ ] **Step 2: Run** `pnpm --filter website test`. Expected: FAIL. +- [ ] **Step 3: Implement.** + - `sitemap.xml` lists `/`, `/projects`, `/about`, `/links` and `/brand/` as absolute URLs. + - `robots.txt` allows everything and points to the sitemap. +- [ ] **Step 4: Run** `pnpm test && pnpm --filter website build`. Expected: PASS. +- [ ] **Step 5: Commit:** `feat(website): redirect old URLs and check every page`. + +### Task 5: Preview on workers.dev (CHECKPOINT: Dorell reviews) + +**Files:** Create `scripts/url-matrix.sh`. + +- [ ] **Step 1:** Write `scripts/url-matrix.sh BASE`. It curls each row of spec §5 without following redirects and checks the status and `Location`. It also checks: + - `/`, `/projects`, `/about`, `/links` and `/brand/` return 200. + - `/brand.md` returns 200 with `text/markdown; charset=utf-8` and `access-control-allow-origin: *`. + - `/nope` returns 404. + - It prints one line per check and exits 1 on any mismatch. +- [ ] **Step 2: Run** it against `npx wrangler dev --local`. Expected: all pass. +- [ ] **Step 3: Deploy the preview.** Run `pnpm --filter website build && pnpm --filter website exec wrangler deploy`. Then run `scripts/url-matrix.sh https://jscebu-org..workers.dev`. Expected: all pass. +- [ ] **Step 4:** Screenshot `/`, `/projects`, `/about` and `/links` at 1280px, and at a true 320px through the iframe harness, with reduced motion forced. Check for no horizontal scroll and a visible cursor. +- [ ] **Step 5:** Push, then open a PR from `feat/site-v1` into `feat/brand-v0.1`. +- [ ] **CHECKPOINT:** share the preview URL. Dorell approves the copy and the pages, or asks for changes. + +### Task 6: Swap www.jscebu.org (CHECKPOINT: explicit "go") + +- [ ] **Step 1: Pre-flight (read-only).** + - `npx wrangler whoami` shows the account. + - Confirm the `jscebu.org` zone is in it, using the Cloudflare dashboard or an API zone listing with that account. + - Record the current DNS records for `www` and the apex in the ledger. + - Write the rollback commands into the ledger. +- [ ] **Step 2: CHECKPOINT.** Get Dorell's "go" for the swap window. +- [ ] **Step 3: Delete the www CNAME** (`cname.vercel-dns.com`). Use the API if wrangler's token has DNS edit; otherwise Dorell deletes it in the dashboard. +- [ ] **Step 4: Attach the domain.** Add `"routes": [{ "pattern": "www.jscebu.org", "custom_domain": true }]` to `wrangler.jsonc` and replace the "no routes yet" comment. Then run `wrangler deploy`. +- [ ] **Step 5: Verify.** Run `scripts/url-matrix.sh https://www.jscebu.org` until it passes. Certificate issuance can take a few minutes. + - Also check that `https://jscebu.org` still returns `308 → https://www.jscebu.org/` from Vercel. + - If www isn't serving within 15 minutes, roll back. +- [ ] **Step 6: Commit:** `feat(website): serve www.jscebu.org from the Worker`. Update project memory with the launch date and the follow-ups: + - Retire Vercel and Notion, and move the apex redirect to Cloudflare. + - Remove www from the Vercel project. + - Workers Builds auto-deploy. + - The Cebby events endpoint. + - Design-system Task 9 against the live `/brand`. diff --git a/docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md b/docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md index 9fcf9c3..c9eba43 100644 --- a/docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md +++ b/docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md @@ -87,7 +87,7 @@ Success means **you send one link, and what comes back is recognizably JS Cebu w | Full-color mark | The source of the sticker | | Flat lockup, light | Mark plus black "JAVASCRIPT / CEBU" wordmark, on light backgrounds | | Flat lockup, dark | Mark plus white wordmark, on dark backgrounds | -| One-color mark and lockup, ink and white | Required on yellow backgrounds. The white separators and ring gap become **see-through**, so the background shows through the "JS" lines (prototyped and approved in the brainstorm). | +| One-color mark and lockup, ink and white | Required on yellow backgrounds. The white separators and ring gap become **see-through**, so the background shows through the "JS" lines (prototyped and approved in the brainstorm). **[Proposed amendment, 2026-10-28, awaiting Dorell's confirmation]** On yellow, use **ink**: white on yellow is only 1.3:1. White one-color is for dark backgrounds and photos. | | Sticker | The full-color mark with a white die-cut outline and an optional soft shadow (§4.3) | **Source: the original PSD.** Dorell provided it on 2026-10-27, at `~/Downloads/js-cebu-logo.psd` (1166×1299, RGBA, sha256 `d7c26803…2ad2dab`). diff --git a/docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md b/docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md new file mode 100644 index 0000000..f1e0801 --- /dev/null +++ b/docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md @@ -0,0 +1,171 @@ +# jscebu.org v1: Design + +- **Date:** 2026-10-28 +- **Owner:** Dorell James +- **Status:** design for approval +- **Scope:** project 2 of 3 (see the design-system spec, "Project order"), first release +- **Builds on:** `docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md` (the brand, `apps/website`, `/brand`) + +Labels: **[Decided]** Dorell's call · **[Proposed]** Claude's call, change freely · **[Builder]** left to whoever builds it. + +## 1. Intent + +Move www.jscebu.org off Notion and onto the new Astro site now, with what exists today, and add events once Cebby can serve them. + +- **[Decided]** "Ship the landing page for now and what's available." +- **[Decided]** Build the site first, then swap all of www.jscebu.org to it. + - This **supersedes Task 8 of the design-system plan**, which was a `/brand`-only route cutover. + - There is no partial cutover. + +## 2. Goals, non-goals, anti-goals + +**Goals** +- A landing page and the few pages the old site really had, all built on the design system. +- www.jscebu.org served by the `jscebu-org` Cloudflare Worker. +- Every old URL lands somewhere sensible. + +**Non-goals for v1** (later releases) +- `/events` pages and the Cebby endpoint (§6). +- A Hacktoberfest page (it has its own site). +- Event recaps, RSS, a blog. +- Moving the apex domain off Vercel. + +**Anti-goals** +- An old link that now 404s without a reason. +- An API token reaching the browser or the repo. +- Hand-typed colors or fonts. The design-system rules and `check-dist` apply to every page. + +## 3. Pages + +All pages use the **Terminal** personality and share one nav and footer. +- **Nav:** the dark flat lockup, then Projects, About, Brand. +- **Footer:** links, the code license and the brand-assets note. + +| Path | Content | Source | +|---|---|---| +| `/` | **Hero:** "Build things. Ship them. Together" with the blinking cursor, the sticker, and a one-line intro. **What we do:** meetups, workshops, Hacktoberfest Cebu. **Upcoming events** (`#events`): buttons to Cebby and to JS Cebu's Facebook events. **Featured project:** Cebby. **Community photo:** real and unfiltered. **Join:** Facebook, GitHub, X, email. | site content (§4) | +| `/projects` | Project cards. Cebby only in v1, as **[Decided]** "Cebby first". | `projects` collection | +| `/about` | Mission and vision, plus "how we measure success" (moved verbatim from `/mission-vision`). History: GitHub org 2018, launched 2019 from Laravel Cebu and Vue.js Cebu, with the Angular, React and Vue Cebu sub-communities as history. Organizers (`#organizers`): the four leads from `/members`, as listed today. | `about.md` + `organizers` collection | +| `/links` | The official channels: Facebook, X, GitHub, website, hello@jscebu.org. | `links` collection | +| `/brand`, `/brand.md`, `/brand/*`, `/llms.txt`, `/404` | Already built. `/brand` switches to the shared nav, keeping its section links. | unchanged | +| `/sitemap.xml`, `/robots.txt` | **[Proposed]** For search engines, generated by a small endpoint rather than an integration. The old sitemap returns 500. | build | + +- **[Proposed]** No Bisaya on these pages until Dorell approves the phrases. Approval is part of the design-system PR review. +- **[Builder]** Final copy, written in the brand voice. Dorell reviews it on the preview. + +## 4. Content model + +- **[Decided]** Markdown in the repo, using Astro content collections ("or any new one today"). + - **[Builder]** Use Astro 7's current content-layer API (loaders) and check its docs at build time. +- **[Proposed]** Collections, with schemas validated at build: + - **`projects`:** one Markdown file per project. Fields: + - `name`, `url`, `repo?`, `summary` + - `image` (a local logo or screenshot) + - `status` (`active` | `archived`) + - `featured: boolean` + - `order` + - Body: a longer description. + - **`organizers`:** one data file. Fields: `name`, `role`, `community?`, `order`. + - **`links`:** one data file. Fields: `label`, `url`, `handle?`, `order`. +- `about.md` is a single Markdown page. +- The landing-page copy lives in the page itself. +- **Cebby's card content:** "Cebby is a public discovery layer for Cebu tech events". It links to https://www.getcebby.com and github.com/getcebby/cebby, with its icon copied from the Cebby repo. + +## 5. Old URLs + +Redirects ship as a `_redirects` file served by Workers static assets. Targets end in `/` because Workers serves `about/index.html` at `/about/` and 307s `/about` to it; a slash-less target would redirect twice. **[Builder]** Confirm the file format against the current Cloudflare docs. + +| Old URL (today) | v1 | Status | +|---|---|---| +| `/mission-vision` | `/about/` | 301 | +| `/members` | `/about/#organizers` | 301 | +| `/iniatives` (sic), `/initiatives` | `/about/` | 301 | +| `/official-links` | `/links/` | 301 | +| `/community-projects` (a 2023 Hacktoberfest repo list) | `/projects/` | 301 | +| `/events` | `/#events` | **302**, because `/events` becomes a real page later | +| `/promotional-materials` (Notion "page not found" today) | `/brand/` | 301 | +| `/annual-hacktoberfest` (Notion "page not found" today) | `https://hacktoberfest.jscebu.org/` | 301 | +| `/volunteers-training-workshop` (Notion "page not found" today) | `/about/` | 301 | +| `/feed` (500 today) | 404 | None. v1 has no feed. | +| `/sitemap.xml` (500 today) | the new sitemap | | +| anything else | the branded 404 page | | + +## 6. Later: events from Cebby + +Recorded so v1 doesn't paint it into a corner. This gets built in its own release. + +- **[Decided]** + - Events come from Cebby's API. + - Cebby gains an endpoint that returns JS Cebu's events, protected by an API token. + - jscebu.org pulls it server-side, so the token never reaches the browser or the repo. +- **What exists today in Cebby** (`getcebby/cebby`, `apps/pwa`): + - Astro 5 on Cloudflare Workers, with server-side rendering. + - Supabase tables `organizations`, `accounts`, `events`, `event_organizers`. + - JS Cebu is in the data (`javascript-cebu`). + - Existing routes live under `src/pages/api/`. +- **[Builder, then]** Fetch at build time with a content loader, or per request with a live collection, which needs the Cloudflare adapter. Decide when building it. +- **[Decided, input for that release]** "We've got lots of events and I don't want to build it in a way that's one list of top to bottom." + - Dorell shared the Facebook past-events page on 2026-10-28. It holds about 50 events, from Jan 2019 to Oct 2026. + - **Series:** React Series: Zero to Hero 1–5 plus a hackathon (2019); six AI4Devs Workshops (Jun 2025); Automated Testing workshops (2024–2025). + - **Hacktoberfest Cebu stages, per edition:** kickoff or opening, online midpoint, deployment day, awarding. Editions ran 2020, 2021, 2023, 2024, 2025 and 2026. + - **Hosts besides JS Cebu:** React Cebu, Vue.js Cebu, DEVCON Cebu, enspace Cebu, PWA Pilipinas, Cebu Tech Communities. + - **Recurring venues:** DOST Cebu PSTO, enspace Cebu, LegalMatch, Zero-Ten Park. + - **Facebook lists some events twice.** + - The browse design (grouping by year, series or edition, filters, highlights) is decided in that release, not v1. + +## 7. Going live + +**Before the swap** +- Confirm the jscebu.org zone is in the Cloudflare account wrangler uses (`@dorelljames`). +- Deploy the Worker to workers.dev and check every page and redirect there. +- **[Proposed]** These are the go/no-go gate. + +**The swap: [Proposed]** +- Record the current DNS records. +- Delete the `www` CNAME to `cname.vercel-dns.com`. +- Attach `www.jscebu.org` to the `jscebu-org` Worker as a **Workers Custom Domain**. Cloudflare creates the DNS record and the certificate, and there is no origin, so the zone's SSL mode doesn't matter. +- Expect a gap of seconds to minutes between the delete and the attach. +- Needs Dorell's explicit "go". + +**After the swap** +- Run the §5 URL matrix against www, plus `/brand.md` headers. + +**Rollback** +- Remove the custom domain. +- Recreate the `www` CNAME to `cname.vercel-dns.com`, DNS-only. + +**Apex** +- `jscebu.org` keeps its working Vercel `308 → www` for now. +- Retiring Vercel is a follow-up. + +**Deploys** +- **[Decided, 2026-10-28]** Merging to `main` deploys, through Cloudflare Workers Builds connected to the GitHub repo. The commands are in `CONTRIBUTING.md`. +- The flow is intent → plan → approval → implementation → review → merge. +- **What actually happened at launch:** Dorell attached `www.jscebu.org` as a Custom Domain in the dashboard after reviewing the preview. `wrangler.jsonc` records it. + +**Branches** +- This work is on `feat/site-v1`, stacked on the design-system PR (#1). +- Its PR targets `feat/brand-v0.1` until #1 merges. + +## 8. Acceptance + +1. Every page in §3 builds. +2. `check-dist` passes on all pages: + - It covers cursor wrapping, internal links (now every path, since the whole site is ours), parity on `/brand`, and no literals. + - The redirect table is tested against `_redirects`. +3. The pages hold at 320px with no horizontal scroll, and the cursor is still under reduced motion. +4. On workers.dev and then on www: + - Every §5 row returns its listed status and target. + - `/brand.md` returns `text/markdown; charset=utf-8`. +5. Dorell looks at the preview and says ship it. +6. After launch, the design-system plan's Task 9 runs against the live `https://www.jscebu.org/brand`: a fresh agent designs a landing page and a social graphic. + +## 9. Left to the builder + +- Final copy. +- The section layout within the Terminal recipe. +- The project card design. +- The Astro collections API details. +- The `_redirects` syntax. +- The sitemap endpoint. +- Cebby's icon file. diff --git a/package.json b/package.json new file mode 100644 index 0000000..62293b0 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "jscebu", + "private": true, + "type": "module", + "packageManager": "pnpm@11.3.0", + "engines": { + "node": ">=24" + }, + "scripts": { + "test": "pnpm -r --if-present test", + "build": "pnpm -r --if-present build" + }, + "devDependencies": { + "@types/node": "^24.19.2", + "typescript": "^7.0.2", + "vitest": "^5.0.5" + } +} diff --git a/packages/brand/LICENSE b/packages/brand/LICENSE new file mode 100644 index 0000000..f9cf141 --- /dev/null +++ b/packages/brand/LICENSE @@ -0,0 +1,14 @@ +JavaScript Cebu brand assets + +The JavaScript Cebu name, the hexagon logo in every version (SVG, PNG and the +source PSD), the wordmark lockups and the sticker are © JavaScript Cebu. They +are not covered by the MIT license at the root of this repository. + +You may use them to make things for or about JavaScript Cebu (event graphics, +recaps, sponsor walls, talks, community pages) as described at +https://www.jscebu.org/brand. Don't alter the logo, and don't use it in a way +that suggests JavaScript Cebu endorses something it doesn't. + +The code in this package (generators, scripts, templates) is MIT-licensed like +the rest of the repository. Fonts keep their own licenses (SIL Open Font +License or Apache 2.0). diff --git a/packages/brand/assets/logo/lockup-dark.png b/packages/brand/assets/logo/lockup-dark.png new file mode 100644 index 0000000..9f91146 Binary files /dev/null and b/packages/brand/assets/logo/lockup-dark.png differ diff --git a/packages/brand/assets/logo/lockup-dark.svg b/packages/brand/assets/logo/lockup-dark.svg new file mode 100644 index 0000000..682d3ed --- /dev/null +++ b/packages/brand/assets/logo/lockup-dark.svg @@ -0,0 +1 @@ +JavaScript Cebu diff --git a/packages/brand/assets/logo/lockup-ink.png b/packages/brand/assets/logo/lockup-ink.png new file mode 100644 index 0000000..451c68f Binary files /dev/null and b/packages/brand/assets/logo/lockup-ink.png differ diff --git a/packages/brand/assets/logo/lockup-ink.svg b/packages/brand/assets/logo/lockup-ink.svg new file mode 100644 index 0000000..f9c4049 --- /dev/null +++ b/packages/brand/assets/logo/lockup-ink.svg @@ -0,0 +1 @@ +JavaScript Cebu diff --git a/packages/brand/assets/logo/lockup-light.png b/packages/brand/assets/logo/lockup-light.png new file mode 100644 index 0000000..347d075 Binary files /dev/null and b/packages/brand/assets/logo/lockup-light.png differ diff --git a/packages/brand/assets/logo/lockup-light.svg b/packages/brand/assets/logo/lockup-light.svg new file mode 100644 index 0000000..85eb07f --- /dev/null +++ b/packages/brand/assets/logo/lockup-light.svg @@ -0,0 +1 @@ +JavaScript Cebu diff --git a/packages/brand/assets/logo/lockup-white.png b/packages/brand/assets/logo/lockup-white.png new file mode 100644 index 0000000..594f25c Binary files /dev/null and b/packages/brand/assets/logo/lockup-white.png differ diff --git a/packages/brand/assets/logo/lockup-white.svg b/packages/brand/assets/logo/lockup-white.svg new file mode 100644 index 0000000..c5103ee --- /dev/null +++ b/packages/brand/assets/logo/lockup-white.svg @@ -0,0 +1 @@ +JavaScript Cebu diff --git a/packages/brand/assets/logo/mark-color.png b/packages/brand/assets/logo/mark-color.png new file mode 100644 index 0000000..a607644 Binary files /dev/null and b/packages/brand/assets/logo/mark-color.png differ diff --git a/packages/brand/assets/logo/mark-color.svg b/packages/brand/assets/logo/mark-color.svg new file mode 100644 index 0000000..87eb039 --- /dev/null +++ b/packages/brand/assets/logo/mark-color.svg @@ -0,0 +1 @@ +JavaScript Cebu diff --git a/packages/brand/assets/logo/mark-ink.png b/packages/brand/assets/logo/mark-ink.png new file mode 100644 index 0000000..586e997 Binary files /dev/null and b/packages/brand/assets/logo/mark-ink.png differ diff --git a/packages/brand/assets/logo/mark-ink.svg b/packages/brand/assets/logo/mark-ink.svg new file mode 100644 index 0000000..1983096 --- /dev/null +++ b/packages/brand/assets/logo/mark-ink.svg @@ -0,0 +1 @@ +JavaScript Cebu diff --git a/packages/brand/assets/logo/mark-white.png b/packages/brand/assets/logo/mark-white.png new file mode 100644 index 0000000..653d12d Binary files /dev/null and b/packages/brand/assets/logo/mark-white.png differ diff --git a/packages/brand/assets/logo/mark-white.svg b/packages/brand/assets/logo/mark-white.svg new file mode 100644 index 0000000..4c1c7db --- /dev/null +++ b/packages/brand/assets/logo/mark-white.svg @@ -0,0 +1 @@ +JavaScript Cebu diff --git a/packages/brand/assets/logo/sticker-tilted.png b/packages/brand/assets/logo/sticker-tilted.png new file mode 100644 index 0000000..0457289 Binary files /dev/null and b/packages/brand/assets/logo/sticker-tilted.png differ diff --git a/packages/brand/assets/logo/sticker.png b/packages/brand/assets/logo/sticker.png new file mode 100644 index 0000000..2b58833 Binary files /dev/null and b/packages/brand/assets/logo/sticker.png differ diff --git a/packages/brand/assets/logo/sticker.svg b/packages/brand/assets/logo/sticker.svg new file mode 100644 index 0000000..1113c66 --- /dev/null +++ b/packages/brand/assets/logo/sticker.svg @@ -0,0 +1 @@ +JavaScript Cebu diff --git a/packages/brand/assets/photos/community-meetup.jpg b/packages/brand/assets/photos/community-meetup.jpg new file mode 100644 index 0000000..9097574 Binary files /dev/null and b/packages/brand/assets/photos/community-meetup.jpg differ diff --git a/packages/brand/package.json b/packages/brand/package.json new file mode 100644 index 0000000..9a80523 --- /dev/null +++ b/packages/brand/package.json @@ -0,0 +1,25 @@ +{ + "name": "@jscebu/brand", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "vitest run", + "build:logos": "tsx scripts/logo/build-logos.ts" + }, + "devDependencies": { + "@fontsource/roboto": "^5.3.0", + "@resvg/resvg-js": "^2.6.2", + "@types/opentype.js": "^1.3.10", + "@types/pngjs": "^6.0.5", + "opentype.js": "^2.0.0", + "pngjs": "^7.0.0", + "tsx": "^4.23.15" + }, + "dependencies": { + "linkedom": "~0.18.14" + } +} diff --git a/packages/brand/scripts/logo/build-logos.ts b/packages/brand/scripts/logo/build-logos.ts new file mode 100644 index 0000000..b1a5e8d --- /dev/null +++ b/packages/brand/scripts/logo/build-logos.ts @@ -0,0 +1,185 @@ +/** + * Build every logo variant from the traced mark (source/mark-paths.json) and + * the Roboto wordmark: SVG masters plus PNG exports in assets/logo/. + * Idempotent; run with `pnpm --filter @jscebu/brand build:logos`. + */ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { Resvg } from '@resvg/resvg-js'; +import opentype from 'opentype.js'; +import { brand } from '../../src/brand.ts'; + +const root = new URL('../../', import.meta.url); +const out = new URL('assets/logo/', root); +const require = createRequire(import.meta.url); + +type Grad = { x1: number; y1: number; x2: number; y2: number; stops: { offset: number; color: string }[] }; +const mark: { + viewBox: [number, number, number, number]; + paths: Record<'ring' | 'inner' | 'blue' | 'green' | 'red' | 'outer', string>; + gradients: Record<'ring' | 'blue' | 'green' | 'red', Grad>; +} = JSON.parse(readFileSync(new URL('source/mark-paths.json', root), 'utf8')); + +const [, , MARK_W, MARK_H] = mark.viewBox; +const hex = (id: 'yellow' | 'ink' | 'paper') => brand.colors.find((c) => c.id === id)!.hex; +const INK = hex('ink'); +const WHITE = '#FFFFFF'; + +/** The sticker's white die-cut edge, as a share of the mark's width (brand rule). */ +const STICKER_EDGE = 0.05 * MARK_W; +const STICKER_TILT = -8; + +/** + * Lockup geometry measured from the dark-pixel bounding boxes of + * javascriptcebu/ai-jscebu public/js-horizontal.png (5045×1284), the 2025 lockup. + */ +const LOCKUP = { + width: 5045, + height: 1284, + /** The mark fills the full height at x 0. */ + markHeight: 1284, + lines: [ + { text: 'JAVASCRIPT', weight: 900, left: 1392, top: 312, width: 3653, height: 447 }, + { text: 'CEBU', weight: 900, left: 1400, top: 817, width: 803, height: 221 }, + ], +} as const; + +// ---------------------------------------------------------------- mark + +type Style = { kind: 'color'; prefix: string } | { kind: 'one'; color: string }; + +function markBody(style: Style): { defs: string; body: string } { + const { paths: p } = mark; + if (style.kind === 'one') { + const fill = `fill="${style.color}"`; + return { + defs: '', + body: [ + ``, + ``, + ``, + ``, + ].join(''), + }; + } + const id = (k: string) => `${style.prefix}-${k}`; + const defs = (Object.entries(mark.gradients) as [string, Grad][]) + .map( + ([k, g]) => + `` + + g.stops.map((s) => ``).join('') + + '', + ) + .join(''); + return { + defs, + body: [ + // The white stroke tucks under the ring so no background seam shows between them. + ``, + ``, + ``, + ``, + ``, + ].join(''), + }; +} + +const svgDoc = (viewBox: number[], width: number, height: number, defs: string, body: string) => + `` + + `JavaScript Cebu${defs ? `${defs}` : ''}${body}\n`; + +function round(n: number): number { + return Math.round(n * 100) / 100; +} + +function markSvg(style: Style): string { + const { defs, body } = markBody(style); + return svgDoc([0, 0, MARK_W, MARK_H], MARK_W, MARK_H, defs, body); +} + +function stickerParts(prefix: string) { + const { defs, body } = markBody({ kind: 'color', prefix }); + const edge = ``; + return { defs, body: edge + body, box: [-STICKER_EDGE, -STICKER_EDGE, MARK_W + 2 * STICKER_EDGE, MARK_H + 2 * STICKER_EDGE] }; +} + +function stickerSvg(): string { + const { defs, body, box } = stickerParts('jcst'); + return svgDoc(box, box[2], box[3], defs, body); +} + +function stickerTiltedSvg(): string { + const { defs, body, box } = stickerParts('jcstt'); + const [x, y, w, h] = box; + const rad = (Math.abs(STICKER_TILT) * Math.PI) / 180; + const rw = w * Math.cos(rad) + h * Math.sin(rad); + const rh = w * Math.sin(rad) + h * Math.cos(rad); + const inner = `${body}`; + return svgDoc([-rw / 2, -rh / 2, rw, rh], rw, rh, defs, inner); +} + +// ---------------------------------------------------------------- wordmark + +const fonts = new Map(); +function roboto(weight: number): opentype.Font { + if (!fonts.has(weight)) { + const file = require.resolve(`@fontsource/roboto/files/roboto-latin-${weight}-normal.woff`); + const buf = readFileSync(file); + fonts.set(weight, opentype.parse(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength))); + } + return fonts.get(weight)!; +} + +function layout(font: opentype.Font, text: string, size: number, tracking: number, x0 = 0, baseline = 0) { + // charToGlyph skips GSUB, which opentype.js can't parse for Roboto; caps need no ligatures. + const glyphs = [...text].map((ch) => font.charToGlyph(ch)); + const path = new opentype.Path(); + let x = x0; + glyphs.forEach((g, i) => { + path.extend(g.getPath(x, baseline, size)); + const next = glyphs[i + 1]; + if (next) x += ((g.advanceWidth ?? 0) + font.getKerningValue(g, next)) * (size / font.unitsPerEm) + tracking; + }); + return path; +} + +/** Fit a line's ink box to the measured box: size from the height, tracking from the width. */ +function wordmarkLine(line: (typeof LOCKUP.lines)[number]): string { + const font = roboto(line.weight); + const probe = layout(font, line.text, 1000, 0).getBoundingBox(); + const size = (1000 * line.height) / (probe.y2 - probe.y1); + const untracked = layout(font, line.text, size, 0).getBoundingBox(); + const tracking = (line.width - (untracked.x2 - untracked.x1)) / (line.text.length - 1); + const placed = layout(font, line.text, size, tracking).getBoundingBox(); + return layout(font, line.text, size, tracking, line.left - placed.x1, line.top - placed.y1).toPathData(2); +} + +function lockupSvg(markStyle: Style, textColor: string): string { + const { defs, body } = markBody(markStyle); + const scale = LOCKUP.markHeight / MARK_H; + const words = LOCKUP.lines.map((l) => ``).join(''); + return svgDoc([0, 0, LOCKUP.width, LOCKUP.height], LOCKUP.width, LOCKUP.height, defs, `${body}${words}`); +} + +// ---------------------------------------------------------------- write + +const files: Record = { + 'mark-color': { svg: markSvg({ kind: 'color', prefix: 'jcmc' }), png: 1024 }, + 'mark-ink': { svg: markSvg({ kind: 'one', color: INK }), png: 1024 }, + 'mark-white': { svg: markSvg({ kind: 'one', color: WHITE }), png: 1024 }, + sticker: { svg: stickerSvg(), png: 1024 }, + 'lockup-light': { svg: lockupSvg({ kind: 'color', prefix: 'jcll' }, INK), png: 2048 }, + 'lockup-dark': { svg: lockupSvg({ kind: 'color', prefix: 'jcld' }, WHITE), png: 2048 }, + 'lockup-ink': { svg: lockupSvg({ kind: 'one', color: INK }, INK), png: 2048 }, + 'lockup-white': { svg: lockupSvg({ kind: 'one', color: WHITE }, WHITE), png: 2048 }, +}; + +const png = (svg: string, width: number) => new Resvg(svg, { fitTo: { mode: 'width', value: width } }).render().asPng(); + +mkdirSync(out, { recursive: true }); +for (const [name, f] of Object.entries(files)) { + writeFileSync(new URL(`${name}.svg`, out), f.svg); + writeFileSync(new URL(`${name}.png`, out), png(f.svg, f.png)); +} +writeFileSync(new URL('sticker-tilted.png', out), png(stickerTiltedSvg(), 1024)); +console.log(`Wrote ${Object.keys(files).length} SVGs and ${Object.keys(files).length + 1} PNGs to ${out.pathname}`); diff --git a/packages/brand/scripts/logo/trace.py b/packages/brand/scripts/logo/trace.py new file mode 100644 index 0000000..b1898c9 --- /dev/null +++ b/packages/brand/scripts/logo/trace.py @@ -0,0 +1,157 @@ +"""Trace the JS Cebu mark from the original PSD into SVG paths (one-off). + +Usage: + uv run --with psd-tools==1.26.0 --with numpy --with scipy --with potracer==0.0.4 --with pillow \ + python -I packages/brand/scripts/logo/trace.py packages/brand/source/js-cebu-logo.psd packages/brand/source + +Writes to : mark-flattened.png, regions/.png and mark-paths.json. +The editable "Group 1" in the PSD is cropped at the canvas edge, so this only +reads the flattened copy "Group 2 > Group 1 copy 2", which is the ground truth. +""" + +import colorsys +import json +import sys +from pathlib import Path + +import numpy as np +import potrace +from PIL import Image +from psd_tools import PSDImage +from scipy import ndimage + +# Gradient angles from the PSD's live Gradient Overlay effects (Photoshop degrees, +# counterclockwise from +x). The stop colors come from the flattened pixels instead, +# because spec §4.1 says the flattened pixels win. +ANGLES = {"ring": 87, "blue": 61, "green": 90, "red": 90} +STOP_OFFSETS = [0, 0.25, 0.5, 0.75, 1] + + +def flattened_mark(psd_path: Path) -> Image.Image: + psd = PSDImage.open(psd_path) + group = next(layer for layer in psd if layer.name == "Group 2") + mark = next(layer for layer in group if layer.name == "Group 1 copy 2") + return mark.topil().convert("RGBA") + + +def build_masks(rgba: np.ndarray) -> dict[str, np.ndarray]: + rgb, alpha = rgba[..., :3].astype(int), rgba[..., 3] + white = (alpha > 0) & (rgb.min(axis=2) >= 225) + colored = (alpha > 128) & ~white + labels, n = ndimage.label(colored, structure=np.ones((3, 3))) + sizes = ndimage.sum(colored, labels, range(1, n + 1)) + biggest = [int(i) + 1 for i in np.argsort(sizes)[::-1][:4]] + + def bbox_area(label: int) -> int: + ys, xs = np.nonzero(labels == label) + return (ys.max() - ys.min()) * (xs.max() - xs.min()) + + ring_label = max(biggest, key=bbox_area) + masks = {"ring": labels == ring_label} + for label in biggest: + if label == ring_label: + continue + m = labels == label + r, g, b = (rgb[m].mean(axis=0) / 255).tolist() + hue = colorsys.rgb_to_hsv(r, g, b)[0] * 360 + region = "blue" if 180 <= hue <= 260 else "green" if 70 <= hue <= 170 else "red" + if region in masks: + raise SystemExit(f"Two components classified as {region} (hue {hue:.0f})") + masks[region] = m + missing = {"blue", "green", "red"} - masks.keys() + if missing: + raise SystemExit(f"Missing regions: {missing}") + + masks["outer"] = ndimage.binary_fill_holes(masks["ring"]) + masks["inner"] = masks["outer"] & ~masks["ring"] + if masks["inner"].sum() == 0: + raise SystemExit("The ring has no hole; cannot derive inner/outer") + return {k: ndimage.binary_opening(v, structure=np.ones((3, 3))) for k, v in masks.items()} + + +def fmt(v: float) -> str: + return f"{v:.2f}".rstrip("0").rstrip(".") + + +def trace(mask: np.ndarray) -> str: + # potracer treats low values as ink (like black on a white page), so invert. + bitmap = potrace.Bitmap(~mask) + curves = bitmap.trace(turdsize=2, alphamax=1.0, opticurve=True, opttolerance=0.2) + parts = [] + for curve in curves: + p = curve.start_point + parts.append(f"M{fmt(p.x)} {fmt(p.y)}") + for seg in curve.segments: + if seg.is_corner: + parts.append(f"L{fmt(seg.c.x)} {fmt(seg.c.y)}L{fmt(seg.end_point.x)} {fmt(seg.end_point.y)}") + else: + parts.append( + f"C{fmt(seg.c1.x)} {fmt(seg.c1.y)} {fmt(seg.c2.x)} {fmt(seg.c2.y)} " + f"{fmt(seg.end_point.x)} {fmt(seg.end_point.y)}" + ) + parts.append("Z") + return "".join(parts) + + +def gradient(rgba: np.ndarray, mask: np.ndarray, angle: float) -> dict: + """A linearGradient in objectBoundingBox units whose isolines are perpendicular + to `angle` in user space, with stop colors sampled from the flattened pixels.""" + rad = np.deg2rad(angle) + dx, dy = np.cos(rad), -np.sin(rad) # image y points down + core = ndimage.binary_erosion(mask, structure=np.ones((3, 3)), iterations=3) + ys, xs = np.nonzero(core) + t = xs * dx + ys * dy + p3, p97 = np.percentile(t, [3, 97]) + + stops = [] + for offset in STOP_OFFSETS: + band = np.abs(t - (p3 + offset * (p97 - p3))) <= 3 + color = np.median(rgba[ys[band], xs[band], :3], axis=0).round().astype(int) + stops.append({"offset": offset, "color": "#{:02X}{:02X}{:02X}".format(*color)}) + + # Bounding box of the full region, which is what objectBoundingBox refers to. + mys, mxs = np.nonzero(mask) + bx, by = mxs.min(), mys.min() + bw, bh = mxs.max() + 1 - bx, mys.max() + 1 - by + # In bbox units the gradient vector must be (bw*dx, bh*dy) scaled so that + # g = (t - p3) / (p97 - p3) holds for every user-space point. + norm = (bw * dx) ** 2 + (bh * dy) ** 2 + lam = (p3 - (bx * dx + by * dy)) / norm + s = (p97 - p3) / norm + x1, y1 = lam * bw * dx, lam * bh * dy + return { + "x1": round(float(x1), 4), + "y1": round(float(y1), 4), + "x2": round(float(x1 + s * bw * dx), 4), + "y2": round(float(y1 + s * bh * dy), 4), + "stops": stops, + } + + +def main(psd_path: str, outdir: str) -> None: + out = Path(outdir) + (out / "regions").mkdir(parents=True, exist_ok=True) + mark = flattened_mark(Path(psd_path)) + mark.save(out / "mark-flattened.png") + rgba = np.asarray(mark) + h, w = rgba.shape[:2] + + masks = build_masks(rgba) + for region, m in masks.items(): + Image.fromarray((m * 255).astype(np.uint8), "L").save(out / "regions" / f"{region}.png") + + order = ["ring", "inner", "blue", "green", "red", "outer"] + result = { + "viewBox": [0, 0, w, h], + "paths": {region: trace(masks[region]) for region in order}, + "gradients": {region: gradient(rgba, masks[region], a) for region, a in ANGLES.items()}, + } + (out / "mark-paths.json").write_text(json.dumps(result, indent=2) + "\n") + for region in order: + print(f"{region}: {int(masks[region].sum())} px, {len(result['paths'][region])} chars of path") + for region, g in result["gradients"].items(): + print(f"{region} gradient: " + " → ".join(s["color"] for s in g["stops"])) + + +if __name__ == "__main__": + main(*sys.argv[1:3]) diff --git a/packages/brand/source/js-cebu-logo.psd b/packages/brand/source/js-cebu-logo.psd new file mode 100644 index 0000000..63cbde1 Binary files /dev/null and b/packages/brand/source/js-cebu-logo.psd differ diff --git a/packages/brand/source/js-horizontal.png b/packages/brand/source/js-horizontal.png new file mode 100644 index 0000000..5a15bcc Binary files /dev/null and b/packages/brand/source/js-horizontal.png differ diff --git a/packages/brand/source/mark-flattened.png b/packages/brand/source/mark-flattened.png new file mode 100644 index 0000000..ea3cce3 Binary files /dev/null and b/packages/brand/source/mark-flattened.png differ diff --git a/packages/brand/source/mark-paths.json b/packages/brand/source/mark-paths.json new file mode 100644 index 0000000..3461aac --- /dev/null +++ b/packages/brand/source/mark-paths.json @@ -0,0 +1,130 @@ +{ + "viewBox": [ + 0, + 0, + 1005, + 1006 + ], + "paths": { + "ring": "M382 1005C382 1004.45 381.16 1004 380.13 1004C379.1 1004 373.82 1002.88 368.4 1001.5C356.11 998.38 340.65 990.91 330.36 983.12C325.85 979.71 295.61 950.13 259.52 913.83C224.88 879 158.47 812.25 111.93 765.5C53.77 707.08 25.68 678.15 22.07 673C12.26 658.99 4.13 640.01 2.42 627.1C2.05 624.29 1.35 622 0.87 622C-0.29 622 -0.25 592.45 0.91 590.24C1.42 589.28 2.37 585.8 3.04 582.5C3.71 579.2 7.35 565.02 11.14 551C14.93 536.98 22.28 509.75 27.49 490.5C32.69 471.25 41.01 440.43 45.98 422C50.94 403.57 56.79 381.98 58.97 374C62.96 359.42 69.02 336.94 83.93 281.5C88.37 265 93.79 244.97 95.97 237C98.15 229.03 102.65 212.48 105.97 200.23C114.82 167.55 121.63 154.38 138.48 137.4C151.17 124.6 165.49 115.36 180.69 110.16C186.28 108.25 215.44 100.29 249 91.52C255.88 89.73 266 87.05 271.5 85.56C286.43 81.54 292.07 80.04 333.5 69.05C391.65 53.62 397.45 52.08 412.5 48.04C420.2 45.97 431.23 43.04 437 41.53C442.77 40.02 450.43 37.96 454 36.97C457.57 35.97 471.98 32.15 486 28.47C500.02 24.79 516.9 20.31 523.5 18.53C530.1 16.74 547.2 12.25 561.5 8.55C575.8 4.85 589.08 1.39 591 0.85C596.31 -0.63 623.29 0.34 631.49 2.31C648.22 6.34 662.51 12.85 675.13 22.2C683.37 28.31 971.79 317.61 980.34 328.34C990.62 341.25 997.49 354.9 1001.71 370.76C1005.19 383.86 1006.06 407.45 1003.52 419.92C1002.12 426.78 990.03 472.28 976 523.5C971.95 538.29 963.63 569.08 955.48 599.5C951.79 613.25 947.3 629.9 945.51 636.5C943.71 643.1 940.35 655.48 938.03 664C935.72 672.52 932.55 684.23 930.98 690C929.41 695.77 925.35 710.85 921.96 723.5C893.97 827.85 894.14 827.26 888.05 839.5C881.9 851.86 875.69 860.14 864.59 870.8C846.49 888.18 835.2 893.43 791.5 904.84C775.6 908.99 770.93 910.23 720.5 923.69C702.9 928.39 682.88 933.71 676 935.52C669.12 937.32 647.08 943.18 627 948.54C606.92 953.9 584.88 959.75 578 961.55C571.12 963.34 554.25 967.82 540.5 971.5C526.75 975.18 513.02 978.85 510 979.64C506.98 980.44 499.32 982.48 493 984.16C430.82 1000.75 418.11 1004 415.44 1004C414.1 1004 413 1004.45 413 1005C413 1005.62 407.17 1006 397.5 1006C387.83 1006 382 1005.62 382 1005ZM443 949.97C450.92 948.99 457.21 947.79 463 946.17C464.93 945.63 471.68 943.83 478 942.17C484.32 940.51 492.2 938.42 495.5 937.52C498.8 936.62 508.25 934.09 516.5 931.91C524.75 929.74 535.77 926.8 541 925.38C546.23 923.97 552.08 922.45 554 922.01C555.92 921.56 560.88 920.25 565 919.1C569.12 917.94 578.35 915.45 585.5 913.56C592.65 911.67 601.88 909.23 606 908.13C610.12 907.04 618 904.94 623.5 903.48C629 902.02 635.08 900.38 637 899.85C638.92 899.31 642.08 898.49 644 898.03C645.92 897.57 650.2 896.45 653.5 895.54C656.8 894.64 662.88 892.98 667 891.86C671.12 890.74 676.08 889.38 678 888.84C679.92 888.31 683.08 887.5 685 887.05C686.92 886.6 694.58 884.6 702 882.61C709.42 880.61 722.25 877.21 730.5 875.05C738.75 872.89 748.2 870.42 751.5 869.55C780.48 861.97 799.03 851.93 816.88 834.16C831.17 819.94 842.72 800.9 847.93 783C849.13 778.88 850.5 774.15 850.98 772.5C851.46 770.85 853.46 762.75 855.42 754.5C857.39 746.25 859.45 737.81 860 735.75C860.55 733.69 861.45 730.2 862 728C862.55 725.8 863.44 722.31 863.97 720.25C864.51 718.19 867.45 706.15 870.51 693.5C873.56 680.85 876.49 668.92 877.02 667C877.54 665.08 880.02 654.95 882.52 644.5C885.02 634.05 887.46 624.15 887.94 622.5C888.43 620.85 890.45 612.75 892.44 604.5C894.43 596.25 896.46 587.92 896.95 586C898.96 578.05 900.05 573.8 900.98 570.25C902.29 565.24 907.99 542.46 909.11 537.75C909.6 535.69 910.41 532.31 910.9 530.25C911.4 528.19 912.25 524.48 912.8 522C913.35 519.52 915.62 510.3 917.85 501.5C933.56 439.34 932.38 445.37 932.77 425.5C933.2 404.13 931.56 393.68 925.12 376.56C916.74 354.34 910.98 346.46 878.63 312.93C871.96 306.02 862.94 296.57 858.58 291.93C854.22 287.29 845.7 278.32 839.64 272C833.58 265.68 825.01 256.68 820.59 252C816.18 247.32 807.83 238.55 802.04 232.5C796.24 226.44 787.67 217.44 782.99 212.5C778.3 207.55 769.3 198.1 762.99 191.5C742.98 170.6 716.35 142.63 712.99 139C701.32 126.38 673.31 97.22 666.56 90.67C662.13 86.36 654.11 79.9 648.74 76.31C643.38 72.72 638.74 69.38 638.43 68.89C638.13 68.4 637.23 68 636.43 68C635.63 68 632.85 67.06 630.24 65.9C622.87 62.63 621.93 62.28 615 60.15C601.03 55.85 591.57 54.74 573.5 55.28C559.36 55.71 554.82 56.24 546.5 58.46C541 59.92 531.55 62.41 525.5 63.98C514.72 66.79 510.1 68.03 495 72.15C490.88 73.28 485.93 74.57 484 75.03C482.07 75.49 478.93 76.31 477 76.84C475.07 77.38 470.12 78.74 466 79.86C461.88 80.98 455.8 82.64 452.5 83.54C449.2 84.45 444.93 85.57 443 86.03C441.07 86.49 437.7 87.38 435.5 88C433.3 88.63 430.38 89.38 429 89.68C427.62 89.98 422.62 91.29 417.87 92.59C403.49 96.54 398.06 97.99 384.5 101.48C377.35 103.32 368.12 105.81 364 107.01C359.88 108.21 354.81 109.6 352.75 110.09C341.75 112.75 332.36 115.21 320.5 118.52C312.68 120.71 300.42 123.94 296 124.98C294.07 125.44 289.8 126.55 286.5 127.46C283.2 128.36 277.12 130.02 273 131.14C268.88 132.26 263.93 133.62 262 134.16C260.07 134.69 256.93 135.52 255 135.99C229.15 142.31 209.26 152.36 193.14 167.25C174.78 184.19 161.11 206.92 155.98 229C155.54 230.93 153.54 238.8 151.54 246.5C149.55 254.2 147.46 262.52 146.91 265C146.36 267.48 145.49 271.07 145 273C144.5 274.93 143.66 278.3 143.13 280.5C140.27 292.41 137.92 301.92 137.02 305.25C136.46 307.31 135.55 310.8 135 313C134.45 315.2 133.57 318.69 133.05 320.75C132.53 322.81 129.63 334.62 126.61 347C123.58 359.38 118.81 378.73 115.99 390C113.18 401.27 110.43 412.52 109.89 415C109.35 417.48 108.51 421.07 108.03 423C107.56 424.93 105.34 433.93 103.1 443C100.86 452.07 98.57 461.19 98.01 463.25C96.29 469.62 95.2 473.97 91.44 489.5C89.44 497.75 87.41 505.85 86.93 507.5C85.48 512.44 75.22 555.03 73.92 561.5C72.37 569.16 72.35 597.65 73.88 605.5C78.1 627.13 87.16 647.39 99.35 662.5C103.45 667.58 110.96 675.68 129.96 695.51C135.76 701.56 144.11 710.33 148.52 715.01C164.93 732.38 192.89 761.79 205.04 774.44C211.93 781.62 222.04 792.23 227.5 798C232.95 803.77 242.38 813.69 248.46 820.03C254.53 826.36 268.04 840.54 278.48 851.53C288.93 862.51 301.53 875.77 306.48 880.99C342.18 918.55 344.9 921.18 355.04 928.11C381.53 946.2 412.07 953.79 443 949.97Z", + "inner": "M414.5 950.34C391.37 948.48 361.71 935.64 344.64 920.11C338.62 914.63 332.15 907.99 306.48 880.99C301.53 875.77 288.93 862.51 278.48 851.53C268.04 840.54 254.53 826.36 248.46 820.03C242.38 813.69 232.95 803.77 227.5 798C222.04 792.23 211.93 781.62 205.04 774.44C192.89 761.79 164.93 732.38 148.52 715.01C144.11 710.33 135.76 701.56 129.96 695.51C110.96 675.68 103.45 667.58 99.35 662.5C87.16 647.39 78.1 627.13 73.88 605.5C72.35 597.65 72.37 569.16 73.92 561.5C75.22 555.03 85.48 512.44 86.93 507.5C87.41 505.85 89.44 497.75 91.44 489.5C95.2 473.97 96.29 469.62 98.01 463.25C98.57 461.19 100.86 452.07 103.1 443C105.34 433.93 107.56 424.93 108.03 423C108.51 421.07 109.35 417.48 109.89 415C110.43 412.52 113.18 401.27 115.99 390C118.81 378.73 123.58 359.38 126.61 347C129.63 334.62 132.53 322.81 133.05 320.75C133.57 318.69 134.45 315.2 135 313C135.55 310.8 136.46 307.31 137.02 305.25C137.92 301.92 140.27 292.41 143.13 280.5C143.66 278.3 144.5 274.93 145 273C145.49 271.07 146.36 267.48 146.91 265C147.46 262.52 149.55 254.2 151.54 246.5C153.54 238.8 155.54 230.93 155.98 229C161.11 206.92 174.78 184.19 193.14 167.25C209.26 152.36 229.15 142.31 255 135.99C256.93 135.52 260.07 134.69 262 134.16C263.93 133.62 268.88 132.26 273 131.14C277.12 130.02 283.2 128.36 286.5 127.46C289.8 126.55 294.07 125.44 296 124.98C300.42 123.94 312.68 120.71 320.5 118.52C332.36 115.21 341.75 112.75 352.75 110.09C354.81 109.6 359.88 108.21 364 107.01C368.12 105.81 377.35 103.32 384.5 101.48C398.06 97.99 403.49 96.54 417.87 92.59C422.62 91.29 427.62 89.98 429 89.68C430.38 89.38 433.3 88.63 435.5 88C437.7 87.38 441.07 86.49 443 86.03C444.93 85.57 449.2 84.45 452.5 83.54C455.8 82.64 461.88 80.98 466 79.86C470.12 78.74 475.07 77.38 477 76.84C478.93 76.31 482.07 75.49 484 75.03C485.93 74.57 490.88 73.28 495 72.15C510.1 68.03 514.72 66.79 525.5 63.98C531.55 62.41 541 59.92 546.5 58.46C554.82 56.24 559.36 55.71 573.5 55.28C591.57 54.74 601.03 55.85 615 60.15C621.93 62.28 622.87 62.63 630.24 65.9C632.85 67.06 635.63 68 636.43 68C637.23 68 638.13 68.4 638.43 68.89C638.74 69.38 643.38 72.72 648.74 76.31C654.11 79.9 662.13 86.36 666.56 90.67C673.31 97.22 701.32 126.38 712.99 139C716.35 142.63 742.98 170.6 762.99 191.5C769.3 198.1 778.3 207.55 782.99 212.5C787.67 217.44 796.24 226.44 802.04 232.5C807.83 238.55 816.18 247.32 820.59 252C825.01 256.68 833.58 265.68 839.64 272C845.7 278.32 854.22 287.29 858.58 291.93C862.94 296.57 871.96 306.02 878.63 312.93C910.98 346.46 916.74 354.34 925.12 376.56C931.56 393.68 933.2 404.13 932.77 425.5C932.38 445.37 933.56 439.34 917.85 501.5C915.62 510.3 913.35 519.52 912.8 522C912.25 524.48 911.4 528.19 910.9 530.25C910.41 532.31 909.6 535.69 909.11 537.75C907.99 542.46 902.29 565.24 900.98 570.25C900.05 573.8 898.96 578.05 896.95 586C896.46 587.92 894.43 596.25 892.44 604.5C890.45 612.75 888.43 620.85 887.94 622.5C887.46 624.15 885.02 634.05 882.52 644.5C880.02 654.95 877.54 665.08 877.02 667C876.49 668.92 873.56 680.85 870.51 693.5C867.45 706.15 864.51 718.19 863.97 720.25C863.44 722.31 862.55 725.8 862 728C861.45 730.2 860.55 733.69 860 735.75C859.45 737.81 857.39 746.25 855.42 754.5C853.46 762.75 851.46 770.85 850.98 772.5C850.5 774.15 849.13 778.88 847.93 783C842.72 800.9 831.17 819.94 816.88 834.16C799.03 851.93 780.48 861.97 751.5 869.55C748.2 870.42 738.75 872.89 730.5 875.05C722.25 877.21 709.42 880.61 702 882.61C694.58 884.6 686.92 886.6 685 887.05C683.08 887.5 679.92 888.31 678 888.84C676.08 889.38 671.12 890.74 667 891.86C662.88 892.98 656.8 894.64 653.5 895.54C650.2 896.45 645.92 897.57 644 898.03C642.08 898.49 638.92 899.31 637 899.85C635.08 900.38 629 902.02 623.5 903.48C618 904.94 610.12 907.04 606 908.13C601.88 909.23 592.65 911.67 585.5 913.56C578.35 915.45 569.12 917.94 565 919.1C560.88 920.25 555.92 921.56 554 922.01C552.08 922.45 546.23 923.97 541 925.38C535.77 926.8 524.75 929.74 516.5 931.91C508.25 934.09 498.8 936.62 495.5 937.52C492.2 938.42 484.32 940.51 478 942.17C471.68 943.83 464.93 945.63 463 946.17C448.88 950.13 430.97 951.67 414.5 950.34Z", + "blue": "M260.5 694.46C243.03 692.88 236.77 691.91 221 688.32C209.02 685.6 184.98 678.44 179.5 675.96C178.4 675.47 173.9 673.66 169.5 671.94C159.29 667.96 155.29 666.16 140.13 658.74C128.06 652.83 127.59 652.48 121.36 644.6C107.86 627.52 100 604.97 100 583.3C100 578.24 100.48 571.71 101.07 568.8C102.2 563.2 110.28 529.12 111.99 522.75C112.55 520.69 113.44 517.31 113.98 515.25C115.49 509.46 136.45 424.23 142.57 399C145.57 386.62 148.43 375.15 148.93 373.5C149.44 371.85 151.9 361.95 154.42 351.5C156.93 341.05 159.44 330.81 159.99 328.75C161.86 321.8 162.91 317.62 174.06 272.5C185.66 225.54 187.35 220.51 195.94 207.22C201.07 199.28 213.64 186.15 221.4 180.61C232.09 172.99 243.66 167.8 258.5 163.97C270.36 160.91 288.2 156.21 294.5 154.48C297.8 153.58 303.65 152.01 307.5 151C311.35 149.99 317.2 148.43 320.5 147.53C323.8 146.64 330.77 144.77 336 143.38C341.23 141.99 347.98 140.19 351 139.38C366.88 135.13 384.49 130.47 401.5 126C420.11 121.12 444.54 114.63 450.5 113C452.15 112.54 456.2 111.43 459.5 110.53C462.8 109.62 470.45 107.58 476.5 106C497.77 100.42 513.58 96.22 521.5 94.04C559.29 83.6 572.17 81.43 586.51 83.06C596.6 84.2 606.8 86.6 608.88 88.32C610.97 90.05 609.85 117.22 605.94 160C603.91 182.16 601.56 200.75 597.96 223C595.39 238.86 595.08 240.67 592.09 257C588.87 274.53 576.8 323.4 570.97 342.5C564.39 364.06 547.62 411.8 542.52 423.5C541.8 425.15 540.01 429.86 538.55 433.97C537.08 438.08 535.19 443.03 534.34 444.97C532.41 449.39 532.6 448.9 523.95 470.5C519.99 480.4 515.67 490.86 514.37 493.75C513.07 496.64 510.88 501.49 509.5 504.53C508.12 507.57 507 510.27 507 510.53C507 511.45 486.05 553.58 481.91 561C454.21 610.57 423.76 644.81 388.04 666.54C374.37 674.85 365.29 679 346.5 685.49C333.07 690.14 315.11 693.45 295.05 694.97C279.58 696.15 279.2 696.15 260.5 694.46Z", + "green": "M321.87 856.25C289.51 822.11 280.45 812.59 266.88 798.52C261.04 792.46 252.04 783.04 246.88 777.58C241.72 772.12 231.62 761.55 224.43 754.08C194.01 722.49 183 710.45 183 708.77C183 706.54 183.36 706.55 191.12 709.06C218.09 717.76 251.68 722.95 281.21 722.98C297.87 723 305.43 722.42 321 719.94C334.05 717.85 356.08 712.21 363.5 709.05C364.6 708.58 368.2 707.16 371.5 705.9C390.99 698.45 416.71 682.59 434.5 667.04C439.77 662.42 455.62 646.56 460.02 641.49C470.3 629.63 488 606.28 488 604.58C488 604.21 488.47 603.37 489.04 602.71C493.35 597.67 504.95 577.24 516.97 553.5C525.73 536.21 528.22 530.96 535.03 515.5C536.24 512.75 538.03 508.7 539.02 506.5C540.01 504.3 542.47 498.45 544.51 493.5C546.54 488.55 548.78 483.15 549.48 481.5C550.19 479.85 552.89 473.1 555.49 466.5C558.1 459.9 560.58 453.6 561.01 452.5C561.44 451.4 563.92 445.1 566.51 438.5C569.1 431.9 571.59 425.6 572.04 424.5C572.48 423.4 574.5 418 576.51 412.5C578.53 407 581.03 400.3 582.09 397.62C583.14 394.93 584 392.11 584 391.35C584 390.59 584.44 389.19 584.98 388.24C585.88 386.65 591.33 370.22 596.84 352.5C600.65 340.21 606.79 317.5 609.87 304.25C610.35 302.19 611.93 295.47 613.37 289.31C616.24 277.12 619.57 260.94 621.15 251.5C622.32 244.55 623.46 237.95 624.95 229.5C630.15 200.04 633.72 167.24 636.1 127.25C637.38 105.7 637.48 105 639.53 105C641.75 105 644.13 107.35 677.01 141.97C697.44 163.48 710.4 177.06 719.5 186.48C724.83 192 738.35 206.2 769 238.47C780.86 250.96 794.32 265.09 817.85 289.77C829.8 302.3 836.01 309.53 835.65 310.48C835.34 311.28 833.83 312.23 832.29 312.59C828.81 313.4 791.23 325.68 788.5 326.9C787.4 327.4 784.92 328.34 783 329C781.08 329.66 778.6 330.56 777.5 331C747.92 342.78 725.68 353.08 708.75 362.84C706.69 364.03 703.31 365.94 701.25 367.09C696.48 369.75 675.58 383.59 673.82 385.25C673.1 385.94 669.82 388.52 666.55 391C656.77 398.4 642.17 412.85 633.72 423.5C622.96 437.07 617.45 446.59 610.03 464.5C608.04 469.29 606.11 476.15 603.82 486.5C602.09 494.36 602.11 525.51 603.86 533C604.56 536.02 605.81 541.35 606.64 544.83C608.28 551.78 615.1 571.89 618.22 579C619.31 581.48 620.59 584.4 621.07 585.5C621.55 586.6 622.89 589.98 624.05 593C625.93 597.91 627.06 600.62 630.08 607.5C631.39 610.46 637.15 627.96 638.15 632C649.07 676.02 636.31 715.02 598.33 753.61C591.42 760.64 581.54 769.79 577.9 772.54C576.46 773.62 570.82 777.9 565.36 782.06C557.08 788.37 524.75 810 523.6 810C523.41 810 518.81 812.52 513.38 815.61C504.82 820.46 466.14 839.77 456 844.24C454.07 845.09 450.02 846.9 447 848.26C440.56 851.16 393.66 869.91 390 871.05C388.62 871.48 383 873.48 377.5 875.49C360.49 881.7 353.25 884 350.66 884C348.7 884 342.58 878.11 321.87 856.25Z", + "red": "M415 921.9C399.41 918.83 393.74 917.06 383.96 912.2C374.17 907.34 373.99 905.52 383 902.22C386.57 900.91 390.4 899.46 391.5 898.99C392.6 898.52 397.55 896.7 402.5 894.95C407.45 893.19 419.38 888.6 429 884.75C446.08 877.91 449.93 876.29 460.78 871.39C463.68 870.07 466.23 869 466.43 869C468.14 869 515.93 845.36 524.79 840.12C527.71 838.41 530.26 837 530.46 837C531.42 837 548.01 826.77 562 817.54C587.64 800.64 597.05 793.06 616.6 773.56C637.95 752.28 651.11 733.18 659.77 710.91C667.08 692.15 669.01 681.68 668.98 661C668.95 633.55 663.65 613.81 644.31 569C641.84 563.3 641.29 561.86 638.5 554C634.07 541.54 632.74 536.95 630.84 527.5C629.24 519.5 629.31 496.91 630.96 490C639.39 454.84 660.05 427.75 699 400.79C731.94 377.98 776.72 357.78 832.5 340.55C839.1 338.51 846.29 336.28 848.48 335.59C851.66 334.6 852.46 333.86 852.48 331.94C852.52 326.81 855.74 328.93 868.61 342.55C888.37 363.47 895.4 374 900.91 390.91C907.65 411.6 907.54 426.71 900.45 455.5C897.88 465.95 895.41 476.07 894.96 478C893.64 483.75 888.1 506.21 887.01 510.25C886.45 512.31 885.59 515.69 885.08 517.75C884.58 519.81 883.69 523.3 883.11 525.5C881.85 530.25 864.51 600.51 849.04 663.5C842.97 688.25 837.51 710.3 836.91 712.5C835.61 717.33 834.85 720.36 831.52 734C830.12 739.77 828.52 746.19 827.98 748.25C826.58 753.61 825.3 758.76 823.95 764.5C819.56 783.14 811.53 798.17 798.79 811.58C783.52 827.66 769.96 835.24 744.47 841.97C737.89 843.71 730.02 845.8 727 846.61C711.76 850.7 693.99 855.42 680.5 858.95C672.25 861.11 662.8 863.61 659.5 864.52C651.77 866.63 640.83 869.54 629.5 872.49C624.55 873.78 616.9 875.82 612.5 877.03C608.1 878.23 601.35 880.03 597.5 881.03C593.65 882.03 587.8 883.59 584.5 884.49C581.2 885.4 573.33 887.5 567 889.15C560.67 890.8 554.15 892.53 552.5 892.99C550.85 893.45 543.2 895.47 535.5 897.48C527.8 899.49 518.35 902.05 514.5 903.16C510.65 904.28 505.93 905.55 504 906C502.07 906.44 495.32 908.19 489 909.88C460.08 917.63 449.94 920.17 442.5 921.52C433.7 923.11 421.97 923.27 415 921.9Z", + "outer": "M382 1005C382 1004.45 381.16 1004 380.13 1004C379.1 1004 373.82 1002.88 368.4 1001.5C356.11 998.38 340.65 990.91 330.36 983.12C325.85 979.71 295.61 950.13 259.52 913.83C224.88 879 158.47 812.25 111.93 765.5C53.77 707.08 25.68 678.15 22.07 673C12.26 658.99 4.13 640.01 2.42 627.1C2.05 624.29 1.35 622 0.87 622C-0.29 622 -0.25 592.45 0.91 590.24C1.42 589.28 2.37 585.8 3.04 582.5C3.71 579.2 7.35 565.02 11.14 551C14.93 536.98 22.28 509.75 27.49 490.5C32.69 471.25 41.01 440.43 45.98 422C50.94 403.57 56.79 381.98 58.97 374C62.96 359.42 69.02 336.94 83.93 281.5C88.37 265 93.79 244.97 95.97 237C98.15 229.03 102.65 212.48 105.97 200.23C114.82 167.55 121.63 154.38 138.48 137.4C151.17 124.6 165.49 115.36 180.69 110.16C186.28 108.25 215.44 100.29 249 91.52C255.88 89.73 266 87.05 271.5 85.56C286.43 81.54 292.07 80.04 333.5 69.05C391.65 53.62 397.45 52.08 412.5 48.04C420.2 45.97 431.23 43.04 437 41.53C442.77 40.02 450.43 37.96 454 36.97C457.57 35.97 471.98 32.15 486 28.47C500.02 24.79 516.9 20.31 523.5 18.53C530.1 16.74 547.2 12.25 561.5 8.55C575.8 4.85 589.08 1.39 591 0.85C596.31 -0.63 623.29 0.34 631.49 2.31C648.22 6.34 662.51 12.85 675.13 22.2C683.37 28.31 971.79 317.61 980.34 328.34C990.62 341.25 997.49 354.9 1001.71 370.76C1005.19 383.86 1006.06 407.45 1003.52 419.92C1002.12 426.78 990.03 472.28 976 523.5C971.95 538.29 963.63 569.08 955.48 599.5C951.79 613.25 947.3 629.9 945.51 636.5C943.71 643.1 940.35 655.48 938.03 664C935.72 672.52 932.55 684.23 930.98 690C929.41 695.77 925.35 710.85 921.96 723.5C893.97 827.85 894.14 827.26 888.05 839.5C881.9 851.86 875.69 860.14 864.59 870.8C846.49 888.18 835.2 893.43 791.5 904.84C775.6 908.99 770.93 910.23 720.5 923.69C702.9 928.39 682.88 933.71 676 935.52C669.12 937.32 647.08 943.18 627 948.54C606.92 953.9 584.88 959.75 578 961.55C571.12 963.34 554.25 967.82 540.5 971.5C526.75 975.18 513.02 978.85 510 979.64C506.98 980.44 499.32 982.48 493 984.16C430.82 1000.75 418.11 1004 415.44 1004C414.1 1004 413 1004.45 413 1005C413 1005.62 407.17 1006 397.5 1006C387.83 1006 382 1005.62 382 1005Z" + }, + "gradients": { + "ring": { + "x1": -0.0491, + "y1": 0.9382, + "x2": -0.0003, + "y2": 0.0054, + "stops": [ + { + "offset": 0, + "color": "#FF8F00" + }, + { + "offset": 0.25, + "color": "#FF8F00" + }, + { + "offset": 0.5, + "color": "#FFB900" + }, + { + "offset": 0.75, + "color": "#FFD700" + }, + { + "offset": 1, + "color": "#FFDF00" + } + ] + }, + "blue": { + "x1": -0.3103, + "y1": 0.674, + "x2": 0.1131, + "y2": -0.2457, + "stops": [ + { + "offset": 0, + "color": "#4D72BF" + }, + { + "offset": 0.25, + "color": "#4CA6DD" + }, + { + "offset": 0.5, + "color": "#47C8F0" + }, + { + "offset": 0.75, + "color": "#47C8F0" + }, + { + "offset": 1, + "color": "#47C8F0" + } + ] + }, + "green": { + "x1": -0.0, + "y1": 0.9409, + "x2": -0.0, + "y2": 0.1078, + "stops": [ + { + "offset": 0, + "color": "#007216" + }, + { + "offset": 0.25, + "color": "#2A9800" + }, + { + "offset": 0.5, + "color": "#4CB300" + }, + { + "offset": 0.75, + "color": "#61C800" + }, + { + "offset": 1, + "color": "#63CA00" + } + ] + }, + "red": { + "x1": -0.0, + "y1": 0.936, + "x2": -0.0, + "y2": 0.0825, + "stops": [ + { + "offset": 0, + "color": "#BD36B4" + }, + { + "offset": 0.25, + "color": "#D34881" + }, + { + "offset": 0.5, + "color": "#E35261" + }, + { + "offset": 0.75, + "color": "#F15940" + }, + { + "offset": 1, + "color": "#FB5F20" + } + ] + } + } +} diff --git a/packages/brand/source/regions/blue.png b/packages/brand/source/regions/blue.png new file mode 100644 index 0000000..3e2874e Binary files /dev/null and b/packages/brand/source/regions/blue.png differ diff --git a/packages/brand/source/regions/green.png b/packages/brand/source/regions/green.png new file mode 100644 index 0000000..457bd6e Binary files /dev/null and b/packages/brand/source/regions/green.png differ diff --git a/packages/brand/source/regions/inner.png b/packages/brand/source/regions/inner.png new file mode 100644 index 0000000..81cb0ba Binary files /dev/null and b/packages/brand/source/regions/inner.png differ diff --git a/packages/brand/source/regions/outer.png b/packages/brand/source/regions/outer.png new file mode 100644 index 0000000..d8e673a Binary files /dev/null and b/packages/brand/source/regions/outer.png differ diff --git a/packages/brand/source/regions/red.png b/packages/brand/source/regions/red.png new file mode 100644 index 0000000..ec4f762 Binary files /dev/null and b/packages/brand/source/regions/red.png differ diff --git a/packages/brand/source/regions/ring.png b/packages/brand/source/regions/ring.png new file mode 100644 index 0000000..948f386 Binary files /dev/null and b/packages/brand/source/regions/ring.png differ diff --git a/packages/brand/src/brand.ts b/packages/brand/src/brand.ts new file mode 100644 index 0000000..490cabb --- /dev/null +++ b/packages/brand/src/brand.ts @@ -0,0 +1,337 @@ +import type { Asset, Brand, FontId, FontToken, Hex } from './types.ts'; + +const yellow: Hex = '#FFDF00'; +const ink: Hex = '#0E0F11'; +const paper: Hex = '#F4F1EA'; + +/** Terminal's background grid and hairlines: white at low opacity over ink. */ +export const terminalGrid = 'rgba(255,255,255,0.045)'; +export const terminalLine = 'rgba(255,255,255,0.08)'; + +const google = (family: string, weights: number[]) => + `https://fonts.googleapis.com/css2?family=${family.replaceAll(' ', '+')}:wght@${weights.join(';')}&display=swap`; + +/** Each logo file ships as an SVG master and a PNG export at the same path. */ +const logo = (id: string, label: string, use: string): Asset[] => [ + { id, label: `${label} (SVG)`, path: `/brand/logo/${id}.svg`, use }, + { id: `${id}-png`, label: `${label} (PNG)`, path: `/brand/logo/${id}.png`, use }, +]; + +const fonts: FontToken[] = [ + { + id: 'archivo', + family: 'Archivo', + cssStack: "'Archivo Variable', 'Archivo', 'Arial Black', sans-serif", + googleFontsUrl: google('Archivo', [600, 900]), + fontsourcePackage: '@fontsource-variable/archivo', + weights: [600, 900], + role: 'Poster headlines (900, uppercase) and body (600). A heavy grotesque that sits well beside the wordmark.', + }, + { + id: 'spaceGrotesk', + family: 'Space Grotesk', + cssStack: "'Space Grotesk Variable', 'Space Grotesk', system-ui, sans-serif", + googleFontsUrl: google('Space Grotesk', [400, 700]), + fontsourcePackage: '@fontsource-variable/space-grotesk', + weights: [400, 700], + role: 'Terminal headlines (700) and body (400).', + }, + { + id: 'interTight', + family: 'Inter Tight', + cssStack: "'Inter Tight Variable', 'Inter Tight', system-ui, sans-serif", + googleFontsUrl: google('Inter Tight', [400, 800]), + fontsourcePackage: '@fontsource-variable/inter-tight', + weights: [400, 800], + role: 'Journal headlines (800, sentence case) and body (400).', + }, + { + id: 'jetbrainsMono', + family: 'JetBrains Mono', + cssStack: "'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace", + googleFontsUrl: google('JetBrains Mono', [400, 600]), + fontsourcePackage: '@fontsource-variable/jetbrains-mono', + weights: [400, 600], + role: 'Labels, metadata, the event line and code, in every personality.', + }, + { + id: 'roboto', + family: 'Roboto', + cssStack: "'Roboto', sans-serif", + googleFontsUrl: google('Roboto', [900]), + fontsourcePackage: '@fontsource/roboto', + weights: [900], + role: 'The wordmark only (Roboto Black). It is already outlined in the logo files, so never set other text in it.', + }, +]; + +/** One Google Fonts stylesheet link for the given fonts, as used in the examples. */ +const fontLink = (...ids: FontId[]) => { + const families = ids.map((id) => { + const f = fonts.find((x) => x.id === id)!; + return `family=${f.family.replaceAll(' ', '+')}:wght@${f.weights.join(';')}`; + }); + return ``; +}; + +const tokensLink = ''; + +export const brand: Brand = { + name: 'JavaScript Cebu', + version: '0.1.0', + site: 'https://www.jscebu.org', + + colors: [ + { id: 'yellow', hex: yellow, role: 'JS Cebu yellow. Leads every piece: backgrounds, headlines on ink, buttons, the cursor, the highlighter. Never a text color on paper.' }, + { id: 'ink', hex: ink, role: 'Near-black. Text on yellow and paper, the Terminal background, the Poster footer bar.' }, + { id: 'paper', hex: paper, role: 'Warm off-white. The Journal background.' }, + ], + + fonts, + + personalities: [ + { + id: 'poster', + name: 'Poster', + use: 'Social media posts and event graphics.', + colors: { bg: yellow, text: ink, muted: '#4A4316', accent: ink, accentText: yellow, cursor: ink }, + type: { + headline: { font: 'archivo', weight: 900, uppercase: true }, + body: { font: 'archivo', weight: 600 }, + mono: { font: 'jetbrainsMono', weight: 600 }, + }, + recipe: [ + 'Background: yellow. Text: ink.', + 'Margins: about 7% of the canvas width on every side.', + 'Label above the headline: JetBrains Mono 600, uppercase, letter-spacing 0.08em, muted color.', + 'Headline: Archivo 900, uppercase, line-height 0.9, letter-spacing -0.02em, about 10% of the canvas width. It ends with the ink cursor.', + 'Agenda: up to 3 lines in Archivo 600, each starting with a mono time or label.', + 'Logo: the sticker in a top corner at about 20% of the canvas width, or the ink one-color lockup.', + 'Footer: a full-width ink bar holding the event line in JetBrains Mono 600, yellow on ink, e.g. SAT NOV 21 · 2PM · CEBU CITY · JSCEBU.ORG.', + ], + dont: [ + 'No full-color flat logo on yellow. Use the sticker or the one-color lockup.', + 'No sentence-case or light-weight headlines. That is Journal.', + 'No gradients, extra colors or hexagon shapes.', + 'No more than one cursor.', + ], + exampleHtml: `${tokensLink} +${fontLink('archivo', 'jetbrainsMono')} +
+ JavaScript Cebu +

Meetup · Nov 2026

+

Ship faster with AI agents

+

01  Building agents with the AI SDK

+

Sat Nov 21 · 2PM · Cebu City · jscebu.org

+
`, + }, + { + id: 'terminal', + name: 'Terminal', + use: 'The website and anything read on screen: slides, docs, dashboards.', + colors: { bg: ink, text: '#E9E9EC', muted: '#9C9CA3', accent: yellow, accentText: ink, cursor: yellow }, + type: { + headline: { font: 'spaceGrotesk', weight: 700 }, + body: { font: 'spaceGrotesk', weight: 400 }, + mono: { font: 'jetbrainsMono', weight: 400 }, + }, + recipe: [ + `Background: ink with a 24px grid of 1px lines in ${terminalGrid}.`, + 'Headline: Space Grotesk 700, yellow, line-height 1, letter-spacing -0.02em. It ends with the yellow cursor, which blinks on the web.', + 'Body: Space Grotesk 400 in the Terminal text color. Labels, metadata and code: JetBrains Mono 400 in the muted color.', + 'Section labels: mono, uppercase, letter-spacing 0.1em. A `// ` prefix is welcome.', + 'Primary button: yellow background, ink text, JetBrains Mono 600, 6px radius. Secondary button: transparent with a 1px light border.', + 'Logo: the dark flat lockup in the nav, the sticker in the hero.', + `Separate sections with 1px hairlines in ${terminalLine}.`, + ], + dont: [ + 'No yellow body text. Yellow is for the headline, links, the primary button and the cursor.', + 'No neon glows, gradients or other accent colors.', + 'No more than one cursor on a screen.', + ], + exampleHtml: `${tokensLink} +${fontLink('spaceGrotesk', 'jetbrainsMono')} +
+ + +

// cebu's javascript community

+

Build things. Ship them. Together

+

Meetups, workshops and Hacktoberfest, run by developers in Cebu.

+

+ Next meetup → + Join us +

+
`, + }, + { + id: 'journal', + name: 'Journal', + use: 'Stories, event recaps and long reads.', + colors: { bg: paper, text: ink, muted: '#6B675F', accent: yellow, accentText: ink, cursor: yellow }, + type: { + headline: { font: 'interTight', weight: 800 }, + body: { font: 'interTight', weight: 400 }, + mono: { font: 'jetbrainsMono', weight: 400 }, + }, + recipe: [ + 'Background: paper. Text: ink.', + 'Headline: Inter Tight 800, sentence case, line-height 1.05, letter-spacing -0.03em. It ends with the yellow cursor.', + 'Key phrase: a yellow highlighter band behind one phrase of the headline or a pull quote, ink text on yellow.', + 'Editorial rules: a 1px ink line above each section label. Labels and captions: JetBrains Mono 400, uppercase, muted color.', + 'Body: Inter Tight 400, 1.6 line-height, about 65 characters per line.', + 'Photos: real and unfiltered, full-bleed or in a simple grid with mono captions.', + 'Logo: the light flat lockup in the header, or the sticker on a photo.', + ], + dont: [ + 'No yellow text on paper. Yellow appears only as the highlighter band, a fill or the cursor.', + 'No uppercase headlines. That is Poster.', + 'No Terminal grid on paper.', + ], + exampleHtml: `${tokensLink} +${fontLink('interTight', 'jetbrainsMono')} +
+

RecapNov 21, 2026

+

What we learned shipping with AI agents

+
+ The JS Cebu community at a meetup +
The November meetup · Photo: JS Cebu
+
+

Notes from the November meetup: what worked, what broke, and what we'd do again.

+
`, + }, + ], + + signatures: [ + { + rank: 1, + id: 'cursor', + name: 'The cursor', + status: 'core', + summary: 'The primary headline ends with a solid block cursor.', + rules: [ + "Size: about 0.5em wide by 0.8em tall at the headline's size, sitting on the baseline.", + 'Color is the contrast color: ink on yellow; yellow on ink or paper.', + 'It never wraps alone: wrap the last word and the cursor together with white-space: nowrap (the jc-nw class).', + 'On the web it blinks (1.05s, steps(1)), stays still under prefers-reduced-motion, and is aria-hidden. In images it does not blink.', + 'One cursor per piece or screen, on the primary headline only.', + ], + }, + { + rank: 2, + id: 'sticker', + name: 'The sticker', + status: 'core', + summary: 'The logo as a tilted die-cut sticker, from laptop-sticker culture at meetups.', + rules: [ + 'Rotate the full-color mark 6–10° in either direction. The default is -8°.', + "A white die-cut edge 5% of the mark's width. A soft shadow is optional.", + 'For expressive moments: graphics, the hero, merch. Nav, footer, sponsor walls and small sizes use the flat lockup.', + 'Use the ready-made sticker files. Do not redraw the edge.', + ], + }, + { + rank: 3, + id: 'bisaya', + name: 'Bisaya, lightly', + status: 'experimental', + summary: 'English first, with a little Bisaya in sign-offs, recaps and calls to action.', + rules: [ + 'English first. Bisaya only in sign-offs, recaps and calls to action.', + 'Never use Bisaya in place of essential information: dates, venues, prices or instructions.', + 'Use phrases from the list. Phrases marked draft are still being tested.', + ], + }, + { + rank: 4, + id: 'photos', + name: 'Real photos', + status: 'core', + summary: 'Real photos of the community, never filtered.', + rules: [ + 'No duotone, yellow tint or filters on photos of people.', + 'On top of a photo you may add only the sticker, a caption, and a dark gradient for legibility.', + 'After every event, update the facebook.com/javascriptcebu cover photo with a real photo from it, using the fb-cover template.', + ], + }, + ], + + must: [ + { id: 'yellow', text: 'JS Cebu yellow `#FFDF00` appears.' }, + { id: 'logo-form', text: 'The logo is the sticker (tilted 6–10°, white die-cut edge), or the flat lockup in nav, footer, sponsor walls or small sizes.' }, + { id: 'logo-on-yellow', text: 'On a yellow background, any flat logo is the one-color version.' }, + { id: 'cursor', text: 'The primary headline ends with a block cursor in the contrast color, and the cursor never wraps onto its own line.' }, + { id: 'one-personality', text: "The piece uses one personality's recipe (Poster, Terminal or Journal), not a mix." }, + { id: 'fonts', text: 'It uses only the approved fonts.' }, + { id: 'photos', text: 'Photos are real and unfiltered.' }, + { id: 'no-hexagons', text: 'No hexagon shapes besides the logo.' }, + { id: 'event-line', text: 'Event graphics include the mono event line with jscebu.org.' }, + { id: 'bisaya', text: 'Any Bisaya is in sign-offs, recaps or CTAs, never in place of essential information.' }, + ], + + logoRules: [ + 'The horizontal "JAVASCRIPT / CEBU" lockup is canonical. The stacked "CEBU.JS" lockup is retired.', + 'Clear space on all sides equals the height of the "C" in CEBU. For the mark alone, it is 25% of the mark\'s height.', + 'Minimum sizes: the mark at 24px tall on screen, the lockup at 120px wide. Below 120px, use the mark alone.', + 'On yellow, use the ink one-color version (white is too faint on yellow). On ink or paper, use the full-color lockup: dark on ink, light on paper. The white one-color version is for dark photos and one-color printing. For expressive placements, use the sticker.', + "The sticker tilts 6–10° in either direction and has a white edge of 5% of the mark's width.", + 'Never recolor, stretch, outline or add effects to the logo, and never place it inside another hexagon.', + ], + + voice: { + principles: [ + 'English first: clear, friendly and direct, like a fellow developer talking.', + 'Lead with what, when and where.', + 'Bisaya lightly, in sign-offs, recaps and calls to action only, never in place of essential information.', + 'Event graphics carry the mono event line: WHEN · WHERE · JSCEBU.ORG, e.g. SAT NOV 21 · 2PM · CEBU CITY · JSCEBU.ORG.', + ], + phrases: [ + { bisaya: 'Salamat', english: 'Thanks', use: 'Sign-offs and thank-you posts', status: 'draft' }, + { bisaya: 'Kita-kits', english: 'See you', use: 'The end of event announcements', status: 'draft' }, + { bisaya: 'Padayon', english: 'Keep going', use: 'Recaps and encouragement', status: 'draft' }, + { bisaya: 'Code ta!', english: "Let's code", use: 'Calls to action for workshops and hack nights', status: 'draft' }, + { bisaya: 'Apil na', english: 'Join now', use: 'Registration calls to action, next to the link', status: 'draft' }, + ], + }, + + assets: [ + ...logo('lockup-light', 'Flat lockup, light', 'On paper and other light backgrounds: nav, footer, sponsor walls.'), + ...logo('lockup-dark', 'Flat lockup, dark', 'On ink and other dark backgrounds.'), + ...logo('lockup-ink', 'One-color lockup, ink', 'On yellow backgrounds, and on light backgrounds when only one color is possible.'), + ...logo('lockup-white', 'One-color lockup, white', 'On dark backgrounds and photos when only one color is possible.'), + ...logo('mark-color', 'Full-color mark', 'Small square spots and avatars on ink or paper.'), + ...logo('mark-ink', 'One-color mark, ink', 'Small square spots on yellow, and on light backgrounds when only one color is possible.'), + ...logo('mark-white', 'One-color mark, white', 'Small square spots on dark backgrounds and photos when only one color is possible.'), + ...logo('sticker', 'Sticker', 'Expressive placements: graphics, the hero, merch. Rotate it 6–10°.'), + { id: 'sticker-tilted', label: 'Sticker, tilted -8° (PNG)', path: '/brand/logo/sticker-tilted.png', use: 'Drop straight into graphics.' }, + { id: 'tokens', label: 'tokens.css', path: '/brand/tokens.css', use: 'CSS custom properties and the cursor, sticker and highlighter classes. Link it from any HTML you make.' }, + { id: 'event-post', label: 'Event post template (Poster, 1080×1080)', path: '/brand/templates/event-post.html', use: 'Copy it, change the text, render it to PNG.' }, + { id: 'fb-cover', label: 'Facebook cover template', path: '/brand/templates/fb-cover.html', use: 'The post-event cover photo ritual.' }, + { id: 'photo', label: 'Sample community photo', path: '/brand/photos/community-meetup.jpg', use: 'Placeholder in the templates. Replace it with a real photo from your event.' }, + ], + + textPairs: [ + { fg: ink, bg: yellow, where: 'Poster headline and body' }, + { fg: '#4A4316', bg: yellow, where: 'Poster labels and metadata (muted)' }, + { fg: yellow, bg: ink, where: 'Poster footer bar event line' }, + { fg: yellow, bg: ink, where: 'Terminal headline and links' }, + { fg: '#E9E9EC', bg: ink, where: 'Terminal body text' }, + { fg: '#9C9CA3', bg: ink, where: 'Terminal labels and metadata (muted)' }, + { fg: ink, bg: yellow, where: 'Terminal primary button' }, + { fg: ink, bg: paper, where: 'Journal headline and body' }, + { fg: '#6B675F', bg: paper, where: 'Journal labels and captions (muted)' }, + { fg: ink, bg: yellow, where: 'Journal highlighter band' }, + ], + + changelog: [ + { + version: '0.1.0', + date: '2026-10-28', + notes: [ + 'First release: yellow, ink and paper; three personalities (Poster, Terminal, Journal); four signatures (cursor, sticker, Bisaya, real photos); the MUST checklist; logos rebuilt as SVG from the original PSD; event post and Facebook cover templates.', + ], + }, + ], +}; diff --git a/packages/brand/src/contrast.ts b/packages/brand/src/contrast.ts new file mode 100644 index 0000000..2e24719 --- /dev/null +++ b/packages/brand/src/contrast.ts @@ -0,0 +1,20 @@ +import type { Hex } from './types.ts'; + +function channel(byte: number): number { + const c = byte / 255; + return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; +} + +/** WCAG 2.x relative luminance of an sRGB `#RRGGBB` color, 0 (black) to 1 (white). */ +export function luminance(hex: Hex): number { + const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex); + if (!m) throw new Error(`Expected #RRGGBB, got ${hex}`); + const [r, g, b] = m.slice(1).map((h) => channel(parseInt(h, 16))); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +/** WCAG 2.x contrast ratio between two colors, 1 to 21. Order doesn't matter. */ +export function contrastRatio(a: Hex, b: Hex): number { + const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); + return (hi + 0.05) / (lo + 0.05); +} diff --git a/packages/brand/src/generate/brand-md.ts b/packages/brand/src/generate/brand-md.ts new file mode 100644 index 0000000..593e544 --- /dev/null +++ b/packages/brand/src/generate/brand-md.ts @@ -0,0 +1,116 @@ +import { contrastRatio } from '../contrast.ts'; +import type { Brand, FontRef, Hex } from '../types.ts'; + +const cell = (s: string) => s.replaceAll('|', '\\|'); +const row = (cells: string[]) => `| ${cells.map(cell).join(' | ')} |`; +const table = (head: string[], rows: string[][]) => + [row(head), `|${head.map(() => '---').join('|')}|`, ...rows.map(row)].join('\n'); +const bullets = (items: string[]) => items.map((i) => `- ${i}`).join('\n'); + +/** The brand rules as one Markdown file for agents. Section order is fixed. */ +export function brandMd(b: Brand): string { + const url = (path: string) => `${b.site}${path}`; + const font = (ref: FontRef) => { + const f = b.fonts.find((x) => x.id === ref.font); + if (!f) throw new Error(`Unknown font ${ref.font}`); + return `${f.family} ${ref.weight}${ref.uppercase ? ', uppercase' : ''}`; + }; + const named = (hex: Hex) => { + const c = b.colors.find((x) => x.hex === hex); + return c ? `\`${hex}\` (${c.id})` : `\`${hex}\``; + }; + const hex = (id: 'yellow' | 'ink' | 'paper') => b.colors.find((c) => c.id === id)!.hex; + const families = b.fonts + .filter((f) => f.id !== 'roboto') + .map((f) => `family=${new URL(f.googleFontsUrl).searchParams.get('family')!.replaceAll(' ', '+')}`); + const allFonts = `https://fonts.googleapis.com/css2?${families.join('&')}&display=swap`; + + const sections: string[] = []; + + sections.push(`# ${b.name} brand · v${b.version} + +> Rules for anyone, person or AI agent, making something for ${b.name}: event graphics, slides, pages, recaps. Follow the MUST checklist exactly and check your work against it before sharing. The same rules, with pictures, are at ${url('/brand/')}. + +Start here: + +${bullets([ + `Link the tokens: \`\``, + `Load the fonts: \`\``, + 'Pick one personality (Poster, Terminal or Journal) and follow its recipe below. Put its class (`jc-poster`, `jc-terminal` or `jc-journal`) on the root element.', + 'Use the logo files linked below. Never redraw the logo.', +])}`); + + sections.push(`## MUST: check every piece + +${b.must.map((r) => `- [ ] ${r.text}`).join('\n')}`); + + const logos = b.assets.filter((a) => a.path.startsWith('/brand/logo/')); + sections.push(`## Logo + +${bullets(b.logoRules)} + +${table(['Version', 'Use', 'URL'], logos.map((a) => [a.label, a.use, url(a.path)]))}`); + + sections.push(`## Colors + +${table(['Token', 'Hex', 'CSS variable', 'Role'], b.colors.map((c) => [c.id, `\`${c.hex}\``, `\`--jc-${c.id}\``, c.role]))} + +Every text/background pair below passes WCAG AA. Yellow is never a text color on paper (${contrastRatio(hex('yellow'), hex('paper')).toFixed(1)}:1). + +${table(['Text', 'Background', 'Where', 'Contrast'], b.textPairs.map((p) => [`\`${p.fg}\``, `\`${p.bg}\``, p.where, `${contrastRatio(p.fg, p.bg).toFixed(1)}:1`]))}`); + + sections.push(`## Fonts + +Use only these. All are free (SIL Open Font License or Apache 2.0). One link loads them all: ${allFonts} + +${table(['Font', 'Weights', 'Role', 'Google Fonts'], b.fonts.map((f) => [f.family, f.weights.join(', '), f.role, f.googleFontsUrl]))}`); + + sections.push(`## Personalities + +Pick one per piece. Never mix them. The signatures and yellow tie them together. + +${b.personalities + .map((p) => { + const c = p.colors; + const parts = [ + `### ${p.id}`, + `**${p.name}.** ${p.use} Put \`class="jc-${p.id}"\` on the root element.`, + bullets([ + `Colors: background ${named(c.bg)}, text ${named(c.text)}, muted ${named(c.muted)}, accent ${named(c.accent)} with ${named(c.accentText)} text on it, cursor ${named(c.cursor)}.`, + `Type: headline ${font(p.type.headline)}; body ${font(p.type.body)}; mono ${font(p.type.mono)}.`, + ]), + `Recipe:\n\n${bullets(p.recipe)}`, + `Don't:\n\n${bullets(p.dont)}`, + ]; + if (p.exampleHtml) parts.push(`Example:\n\n\`\`\`html\n${p.exampleHtml.trim()}\n\`\`\``); + return parts.join('\n\n'); + }) + .join('\n\n')}`); + + sections.push(`## Signatures + +Ranked. These are what make a piece instantly JS Cebu. + +${b.signatures + .map((s) => `### ${s.rank}. ${s.name} (${s.status})\n\n${s.summary}\n\n${bullets(s.rules)}`) + .join('\n\n')}`); + + sections.push(`## Voice + +${bullets(b.voice.principles)} + +${table( + ['Bisaya', 'English', 'Use', 'Status'], + b.voice.phrases.map((p) => [p.bisaya, p.english, p.use, p.status === 'draft' ? 'draft, not yet approved by Dorell' : 'approved']), +)}`); + + sections.push(`## Assets + +${table(['Asset', 'Use', 'URL'], b.assets.map((a) => [a.label, a.use, url(a.path)]))}`); + + sections.push(`## Changelog + +${b.changelog.map((v) => `### ${v.version} (${v.date})\n\n${bullets(v.notes)}`).join('\n\n')}`); + + return sections.join('\n\n') + '\n'; +} diff --git a/packages/brand/src/generate/llms-txt.ts b/packages/brand/src/generate/llms-txt.ts new file mode 100644 index 0000000..828dbe2 --- /dev/null +++ b/packages/brand/src/generate/llms-txt.ts @@ -0,0 +1,15 @@ +import type { Brand } from '../types.ts'; + +/** The site's /llms.txt (llmstxt.org shape), pointing agents at the brand rules. */ +export function llmsTxt(b: Brand): string { + return `# ${b.name} + +> ${b.name} is the JavaScript developer community in Cebu, Philippines: meetups, workshops and community projects. Its brand rules are published for people and for AI agents. + +## Brand + +- [Brand rules for agents](${b.site}/brand.md): the MUST checklist, exact tokens, a recipe per personality, voice and asset URLs. Read this before making anything for ${b.name}. +- [Brand guidelines](${b.site}/brand/): the same rules for people, with logo downloads. +- [tokens.css](${b.site}/brand/tokens.css): CSS custom properties and the cursor, sticker and highlighter classes. +`; +} diff --git a/packages/brand/src/generate/tokens-css.ts b/packages/brand/src/generate/tokens-css.ts new file mode 100644 index 0000000..f63f09d --- /dev/null +++ b/packages/brand/src/generate/tokens-css.ts @@ -0,0 +1,104 @@ +import { terminalGrid, terminalLine } from '../brand.ts'; +import type { Brand, FontId, PersonalityId } from '../types.ts'; + +const kebab = (s: string) => s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`); + +/** Hairline color per personality: ink on the light ones, faint white on Terminal. */ +const lines: Record = { poster: 'var(--jc-ink)', terminal: terminalLine, journal: 'var(--jc-ink)' }; + +const extras: Partial> = { + terminal: [ + 'background-image: linear-gradient(var(--jc-grid) 1px, transparent 1px), linear-gradient(90deg, var(--jc-grid) 1px, transparent 1px);', + 'background-size: 24px 24px;', + ], +}; + +const block = (selector: string, decls: string[]) => `${selector} {\n${decls.map((d) => ` ${d}`).join('\n')}\n}`; + +/** The brand as CSS custom properties plus the signature classes (cursor, sticker, highlighter). */ +export function tokensCss(b: Brand): string { + const stack = (id: FontId) => { + const f = b.fonts.find((x) => x.id === id); + if (!f) throw new Error(`Unknown font ${id}`); + return f.cssStack; + }; + + const root = block(':root', [ + ...b.colors.map((c) => `--jc-${c.id}: ${c.hex};`), + ...b.personalities.map((p) => `--jc-font-${p.id}: ${stack(p.type.headline.font)};`), + `--jc-font-mono: ${stack('jetbrainsMono')};`, + `--jc-font-wordmark: ${stack('roboto')};`, + `--jc-grid: ${terminalGrid};`, + ]); + + const scopes = b.personalities.map((p) => { + const font = (id: FontId) => (id === p.type.headline.font ? `var(--jc-font-${p.id})` : stack(id)); + return block(`.jc-${p.id}`, [ + ...Object.entries(p.colors).map(([k, v]) => `--jc-${kebab(k)}: ${v};`), + `--jc-line: ${lines[p.id]};`, + `--jc-font-headline: ${font(p.type.headline.font)};`, + `--jc-font-body: ${font(p.type.body.font)};`, + `--jc-weight-headline: ${p.type.headline.weight};`, + `--jc-weight-body: ${p.type.body.weight};`, + `--jc-weight-mono: ${p.type.mono.weight};`, + `--jc-headline-case: ${p.type.headline.uppercase ? 'uppercase' : 'none'};`, + 'background-color: var(--jc-bg);', + 'color: var(--jc-text);', + 'font-family: var(--jc-font-body);', + `font-weight: var(--jc-weight-body);`, + ...(extras[p.id] ?? []), + ]); + }); + + return [ + `/* JavaScript Cebu brand tokens v${b.version}. Generated from packages/brand; do not edit. Rules: ${b.site}/brand.md */`, + root, + ...scopes, + block('.jc-headline', [ + 'font-family: var(--jc-font-headline);', + 'font-weight: var(--jc-weight-headline);', + 'text-transform: var(--jc-headline-case);', + ]), + block('.jc-mono', ['font-family: var(--jc-font-mono);', 'font-weight: var(--jc-weight-mono, 400);']), + block('.jc-label', [ + 'font-family: var(--jc-font-mono);', + 'font-weight: var(--jc-weight-mono, 400);', + 'text-transform: uppercase;', + 'letter-spacing: .08em;', + 'color: var(--jc-muted);', + ]), + '/* The event line: WHEN · WHERE · JSCEBU.ORG, on every event graphic. */', + block('.jc-event-line', [ + 'font-family: var(--jc-font-mono);', + 'font-weight: 600;', + 'text-transform: uppercase;', + 'letter-spacing: .08em;', + ]), + '/* Real photos: mark them .jc-photo and never filter them. */', + block('.jc-photo', ['display: block;', 'object-fit: cover;']), + '/* The cursor: wrap the last word and the cursor in .jc-nw so the cursor never wraps alone. */', + block('.jc-nw', ['white-space: nowrap;']), + block('.jc-cursor', [ + 'display: inline-block;', + 'width: .5em;', + 'height: .8em;', + 'margin-left: .08em;', + 'vertical-align: -.05em;', + 'background: var(--jc-cursor, currentColor);', + ]), + block('.jc-cursor--blink', ['animation: jc-blink 1.05s steps(1) infinite;']), + '@keyframes jc-blink {\n 50% { opacity: 0; }\n}', + '@media (prefers-reduced-motion: reduce) {\n .jc-cursor--blink { animation: none; }\n}', + '/* The sticker: use the sticker files, which already carry the white die-cut edge. */', + block('.jc-sticker', ['display: inline-block;', 'transform: rotate(-8deg);']), + block('.jc-sticker--shadow', ['filter: drop-shadow(0 .35em .5em rgba(0, 0, 0, .35));']), + '/* The Journal highlighter: a yellow band behind the key phrase. */', + block('.jc-highlight', [ + 'color: var(--jc-ink);', + 'background: linear-gradient(transparent 40%, var(--jc-yellow) 40%);', + 'padding: 0 .08em;', + '-webkit-box-decoration-break: clone;', + 'box-decoration-break: clone;', + ]), + ].join('\n\n') + '\n'; +} diff --git a/packages/brand/src/index.ts b/packages/brand/src/index.ts new file mode 100644 index 0000000..a6cd4f3 --- /dev/null +++ b/packages/brand/src/index.ts @@ -0,0 +1,8 @@ +export * from './types.ts'; +export { brand } from './brand.ts'; +export { contrastRatio, luminance } from './contrast.ts'; +export { terminalGrid, terminalLine } from './brand.ts'; +export { tokensCss } from './generate/tokens-css.ts'; +export { brandMd } from './generate/brand-md.ts'; +export { llmsTxt } from './generate/llms-txt.ts'; +export { lintHtml, type LintIssue, type LintRule } from './lint.ts'; diff --git a/packages/brand/src/lint.ts b/packages/brand/src/lint.ts new file mode 100644 index 0000000..066dbcc --- /dev/null +++ b/packages/brand/src/lint.ts @@ -0,0 +1,85 @@ +import { parseHTML } from 'linkedom'; + +export type LintRule = 'noColorLiterals' | 'cursorAttached' | 'photosUnfiltered' | 'logoOnYellow' | 'usesTokens'; +export interface LintIssue { rule: LintRule; message: string } + +// Only declaration values are scanned, so `#bad` as a selector is fine; `url(#add)` is a reference, not a color. +const HEX = /(? ({ + selector: m[1].trim(), + body: m[2], + })); +} + +function matchesAny(el: Element, selector: string): boolean { + return selector.split(',').some((s) => { + try { + return el.matches(s.trim()); + } catch { + return false; + } + }); +} + +/** Check HTML against the machine-checkable MUST rules. An empty list means it passes. */ +export function lintHtml(html: string): LintIssue[] { + const { document } = parseHTML(/]/i.test(html) ? html : `${html}`); + const issues: LintIssue[] = []; + const styles = [...document.querySelectorAll('style')].map((s) => s.textContent ?? ''); + const declarations = [ + ...styles.flatMap(cssRules).map((r) => r.body), + ...[...document.querySelectorAll('[style]')].map((el) => el.getAttribute('style') ?? ''), + ]; + + for (const text of declarations) { + const hit = text.match(HEX) ?? text.match(COLOR_FN); + if (hit) { + issues.push({ rule: 'noColorLiterals', message: `Color literal "${hit[0]}". Use the --jc-* variables from tokens.css.` }); + } + } + + for (const cursor of document.querySelectorAll('.jc-cursor')) { + const wrap = cursor.parentElement; + let before = ''; + for (const node of wrap?.childNodes ?? []) { + if (node === cursor) break; + before += node.textContent ?? ''; + } + if (!wrap?.classList.contains('jc-nw') || !/\S/.test(before)) { + issues.push({ + rule: 'cursorAttached', + message: 'Wrap the last word and the cursor together: word.', + }); + } + } + + const filterRules = styles.flatMap(cssRules).filter((r) => FILTER.test(r.body)); + for (const photo of document.querySelectorAll('img.jc-photo')) { + for (let el: Element | null = photo; el && el.tagName !== 'BODY'; el = el.parentElement) { + const filtered = FILTER.test(el.getAttribute('style') ?? '') || filterRules.some((r) => matchesAny(el!, r.selector)); + if (filtered) { + issues.push({ rule: 'photosUnfiltered', message: `Photo ${photo.getAttribute('src')} is filtered. Photos stay real and unfiltered.` }); + break; + } + } + } + + for (const img of document.querySelectorAll('.jc-poster img[src]')) { + const file = img.getAttribute('src')!.match(FULL_COLOR_LOGO); + if (file) { + issues.push({ rule: 'logoOnYellow', message: `${file[1]} on yellow. Use the sticker or the ink one-color logo (lockup-ink, mark-ink).` }); + } + } + + if (!document.querySelector('link[href*="tokens.css"]')) { + issues.push({ rule: 'usesTokens', message: 'Link the brand tokens: .' }); + } + + return issues; +} diff --git a/packages/brand/src/types.ts b/packages/brand/src/types.ts new file mode 100644 index 0000000..5f60153 --- /dev/null +++ b/packages/brand/src/types.ts @@ -0,0 +1,62 @@ +export type Hex = `#${string}`; +export type FontId = 'archivo' | 'spaceGrotesk' | 'interTight' | 'jetbrainsMono' | 'roboto'; +export type PersonalityId = 'poster' | 'terminal' | 'journal'; + +export interface ColorToken { id: 'yellow' | 'ink' | 'paper'; hex: Hex; role: string } + +export interface FontToken { + id: FontId; + family: string; + cssStack: string; + googleFontsUrl: string; + fontsourcePackage: string; + weights: number[]; + role: string; +} + +export interface FontRef { font: FontId; weight: number; uppercase?: boolean } + +export interface Personality { + id: PersonalityId; + name: string; + use: string; + colors: { bg: Hex; text: Hex; muted: Hex; accent: Hex; accentText: Hex; cursor: Hex }; + type: { headline: FontRef; body: FontRef; mono: FontRef }; + recipe: string[]; + dont: string[]; + exampleHtml: string; +} + +export interface Signature { + rank: 1 | 2 | 3 | 4; + id: 'cursor' | 'sticker' | 'bisaya' | 'photos'; + name: string; + status: 'core' | 'experimental'; + summary: string; + rules: string[]; +} + +export interface MustRule { id: string; text: string } + +export interface Phrase { bisaya: string; english: string; use: string; status: 'draft' | 'approved' } + +export interface Asset { id: string; label: string; path: `/brand/${string}`; use: string } + +export interface TextPair { fg: Hex; bg: Hex; where: string; large?: boolean } + +export interface Brand { + name: 'JavaScript Cebu'; + version: string; + site: 'https://www.jscebu.org'; + colors: ColorToken[]; + fonts: FontToken[]; + personalities: Personality[]; + signatures: Signature[]; + must: MustRule[]; + /** Clear space, minimum sizes, which version on which background, sticker tilt and edge. */ + logoRules: string[]; + voice: { principles: string[]; phrases: Phrase[] }; + assets: Asset[]; + textPairs: TextPair[]; + changelog: { version: string; date: string; notes: string[] }[]; +} diff --git a/packages/brand/templates/event-post.html b/packages/brand/templates/event-post.html new file mode 100644 index 0000000..f562a3f --- /dev/null +++ b/packages/brand/templates/event-post.html @@ -0,0 +1,74 @@ + + + + + +JS Cebu event post + + + + + +
+ JavaScript Cebu + + +

Meetup · Nov 2026

+ + +

Ship faster with AI agents

+ + +
    +
  1. 01Building agents with the AI SDK
  2. +
  3. 02Testing what the agent wrote
  4. +
  5. 03Lightning talks and pizza
  6. +
+ +
+ +

Sat Nov 21 · 2PM · Cebu City · jscebu.org

+
+
+ + diff --git a/packages/brand/templates/fb-cover.html b/packages/brand/templates/fb-cover.html new file mode 100644 index 0000000..37bfdae --- /dev/null +++ b/packages/brand/templates/fb-cover.html @@ -0,0 +1,63 @@ + + + + + +JS Cebu Facebook cover + + + + + +
+ + The JS Cebu community at a meetup +
+ JavaScript Cebu +
+ +

Meetup · Nov 21, 2026 · Cebu City

+ +

Thank you, Cebu

+
+
+ + diff --git a/packages/brand/test/brand.test.ts b/packages/brand/test/brand.test.ts new file mode 100644 index 0000000..d0a9fc5 --- /dev/null +++ b/packages/brand/test/brand.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { brand } from '../src/brand.ts'; +import { contrastRatio } from '../src/contrast.ts'; + +describe('brand data', () => { + it('passes WCAG AA for every text/background pair', () => { + for (const p of brand.textPairs) { + expect(contrastRatio(p.fg, p.bg), p.where).toBeGreaterThanOrEqual(p.large ? 3 : 4.5); + } + }); + + it('never uses yellow as text on paper', () => { + expect(brand.textPairs.some((p) => p.fg === '#FFDF00' && p.bg === '#F4F1EA')).toBe(false); + }); + + it('has exactly yellow, ink and paper as system colors', () => { + expect(brand.colors.map((c) => c.hex)).toEqual(['#FFDF00', '#0E0F11', '#F4F1EA']); + }); + + it('has the 10 MUST rules with unique ids', () => { + expect(brand.must).toHaveLength(10); + expect(new Set(brand.must.map((r) => r.id)).size).toBe(10); + }); + + it('ranks the signatures cursor, sticker, bisaya, photos', () => { + expect(brand.signatures.map((s) => s.id)).toEqual(['cursor', 'sticker', 'bisaya', 'photos']); + }); + + it('ships every Bisaya phrase as a draft', () => { + expect(brand.voice.phrases.every((p) => p.status === 'draft')).toBe(true); + }); + + it('gives every personality six uppercase hex colors', () => { + for (const p of brand.personalities) { + for (const k of ['bg', 'text', 'muted', 'accent', 'accentText', 'cursor'] as const) { + expect(p.colors[k], `${p.id}.${k}`).toMatch(/^#[0-9A-F]{6}$/); + } + } + }); +}); diff --git a/packages/brand/test/contrast.test.ts b/packages/brand/test/contrast.test.ts new file mode 100644 index 0000000..950d749 --- /dev/null +++ b/packages/brand/test/contrast.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { contrastRatio, luminance } from '../src/contrast.ts'; + +describe('contrastRatio', () => { + it('is 21:1 for white on black', () => { + expect(contrastRatio('#FFFFFF', '#000000')).toBeCloseTo(21, 1); + }); + + it('is symmetric', () => { + expect(contrastRatio('#0E0F11', '#FFDF00')).toBeCloseTo(contrastRatio('#FFDF00', '#0E0F11'), 10); + }); + + it('gives yellow on ink well over 14:1', () => { + expect(contrastRatio('#FFDF00', '#0E0F11')).toBeGreaterThan(14); + }); + + it('shows why yellow is never text on paper', () => { + expect(contrastRatio('#FFDF00', '#F4F1EA')).toBeLessThan(1.3); + }); +}); + +describe('luminance', () => { + it('is 0 for black and 1 for white', () => { + expect(luminance('#000000')).toBe(0); + expect(luminance('#FFFFFF')).toBeCloseTo(1, 10); + }); +}); diff --git a/packages/brand/test/generate.test.ts b/packages/brand/test/generate.test.ts new file mode 100644 index 0000000..572a4b6 --- /dev/null +++ b/packages/brand/test/generate.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest'; +import { brand, brandMd, llmsTxt, tokensCss } from '../src/index.ts'; + +describe('tokensCss', () => { + const css = tokensCss(brand); + + it('declares the system colors and font stacks', () => { + expect(css).toContain('--jc-yellow: #FFDF00;'); + expect(css).toContain('--jc-font-mono:'); + }); + + it('scopes each personality', () => { + expect(css).toMatch(/\.jc-poster\s*{[^}]*--jc-bg:\s*#FFDF00/); + }); + + it('draws a block cursor that never wraps alone', () => { + expect(css).toMatch(/\.jc-cursor\s*{[^}]*display:\s*inline-block/); + expect(css).toMatch(/\.jc-nw\s*{[^}]*white-space:\s*nowrap/); + }); + + it('stops the cursor blinking under reduced motion', () => { + expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)\s*{[^}]*\.jc-cursor[^}]*animation:\s*none/); + }); + + it('styles the event line, labels and photos', () => { + expect(css).toMatch(/\.jc-event-line\s*{[^}]*font-family:\s*var\(--jc-font-mono\)/); + expect(css).toMatch(/\.jc-label\s*{[^}]*text-transform:\s*uppercase/); + expect(css).toMatch(/\.jc-photo\s*{[^}]*object-fit:\s*cover/); + }); + + it('tilts the sticker -8deg by default', () => { + expect(css).toMatch(/\.jc-sticker\s*{[^}]*rotate\(-8deg\)/); + }); + + it('is deterministic', () => { + expect(tokensCss(brand)).toBe(css); + }); +}); + +describe('brandMd', () => { + const md = brandMd(brand); + + it('starts with the title and version', () => { + expect(md.startsWith('# JavaScript Cebu brand · v0.1.0')).toBe(true); + }); + + it('lists every MUST rule as a checkbox', () => { + for (const r of brand.must) expect(md).toContain(`- [ ] ${r.text}`); + }); + + it('gives every asset as an absolute URL', () => { + for (const a of brand.assets) expect(md).toContain(`https://www.jscebu.org${a.path}`); + expect(md).toContain('https://www.jscebu.org/brand/tokens.css'); + }); + + it('has a recipe heading per personality', () => { + for (const p of brand.personalities) expect(md).toContain(`### ${p.id}`); + }); + + it('includes every logo rule', () => { + for (const r of brand.logoRules) expect(md).toContain(r); + }); + + it('labels draft phrases as not yet approved', () => { + expect(md).toMatch(/draft.*Dorell/i); + }); + + it('keeps the sections in the fixed order', () => { + const order = ['## MUST', '## Logo', '## Colors', '## Fonts', '## Personalities', '## Signatures', '## Voice', '## Assets', '## Changelog']; + const at = order.map((h) => md.indexOf(`\n${h}`)); + expect(at.every((i) => i > 0)).toBe(true); + expect([...at].sort((a, b) => a - b)).toEqual(at); + }); +}); + +describe('links to the /brand page', () => { + it('use the trailing slash the site serves it at, so agents skip a redirect', () => { + for (const text of [brandMd(brand), llmsTxt(brand)]) { + expect(text).toContain('https://www.jscebu.org/brand/'); + expect(text).not.toMatch(/https:\/\/www\.jscebu\.org\/brand(?![/.])/); + } + }); +}); + +describe('llmsTxt', () => { + it('points agents to brand.md', () => { + const txt = llmsTxt(brand); + expect(txt.startsWith('# JavaScript Cebu\n')).toBe(true); + expect(txt).toContain('https://www.jscebu.org/brand.md'); + }); +}); diff --git a/packages/brand/test/helpers/raster.ts b/packages/brand/test/helpers/raster.ts new file mode 100644 index 0000000..bd6c59a --- /dev/null +++ b/packages/brand/test/helpers/raster.ts @@ -0,0 +1,48 @@ +import { readFileSync } from 'node:fs'; +import { Resvg } from '@resvg/resvg-js'; +import { PNG } from 'pngjs'; + +export interface Raster { width: number; height: number; data: Uint8Array | Buffer } + +export function renderSvg(svg: string, width?: number): Raster { + const img = new Resvg(svg, width ? { fitTo: { mode: 'width', value: width } } : {}).render(); + return { width: img.width, height: img.height, data: img.pixels }; +} + +export function readPng(path: string | URL): Raster { + const png = PNG.sync.read(readFileSync(path)); + return { width: png.width, height: png.height, data: png.data }; +} + +/** Intersection over union of two same-size boolean masks. */ +export function iou(a: boolean[], b: boolean[]): number { + if (a.length !== b.length) throw new Error(`Size mismatch: ${a.length} vs ${b.length}`); + let inter = 0; + let union = 0; + for (let i = 0; i < a.length; i++) { + if (a[i] && b[i]) inter++; + if (a[i] || b[i]) union++; + } + return union === 0 ? 1 : inter / union; +} + +export const alphaMask = (r: Raster, min = 128) => Array.from({ length: r.width * r.height }, (_, i) => r.data[i * 4 + 3] >= min); +export const whiteMask = (r: Raster, min = 128) => Array.from({ length: r.width * r.height }, (_, i) => r.data[i * 4] >= min); + +/** Binary erosion with a square of the given radius. */ +export function erode(mask: boolean[], width: number, radius: number): boolean[] { + const height = mask.length / width; + return mask.map((on, i) => { + if (!on) return false; + const x = i % width; + const y = (i - x) / width; + for (let dy = -radius; dy <= radius; dy++) { + for (let dx = -radius; dx <= radius; dx++) { + const nx = x + dx; + const ny = y + dy; + if (nx < 0 || ny < 0 || nx >= width || ny >= height || !mask[ny * width + nx]) return false; + } + } + return true; + }); +} diff --git a/packages/brand/test/lint.test.ts b/packages/brand/test/lint.test.ts new file mode 100644 index 0000000..d8915fb --- /dev/null +++ b/packages/brand/test/lint.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; +import { lintHtml } from '../src/lint.ts'; + +const tokens = ''; +const rules = (html: string) => lintHtml(html).map((i) => i.rule); + +describe('lintHtml', () => { + describe('noColorLiterals', () => { + it.each([ + `

x

`, + `

x

`, + ``, + ``, + ])('flags %s', (html) => { + expect(rules(tokens + html)).toContain('noColorLiterals'); + }); + + it('allows var() colors and id selectors that look like hex', () => { + expect(rules(`${tokens}

x

`)).toEqual([]); + }); + }); + + describe('cursorAttached', () => { + it('flags a cursor outside a .jc-nw wrapper', () => { + expect(rules(`${tokens}

Ship it

`)).toContain('cursorAttached'); + }); + + it('flags a .jc-nw wrapper with no word before the cursor', () => { + expect(rules(`${tokens}

Ship

`)).toContain('cursorAttached'); + }); + + it('accepts the last word and the cursor wrapped together', () => { + expect(rules(`${tokens}

Ship it

`)).toEqual([]); + }); + }); + + describe('photosUnfiltered', () => { + it('flags an inline filter on a photo', () => { + expect(rules(`${tokens}`)).toContain('photosUnfiltered'); + }); + + it('flags a filter on an ancestor set from a style rule', () => { + expect(rules(`${tokens}
`)).toContain('photosUnfiltered'); + }); + + it('accepts an untouched photo', () => { + expect(rules(`${tokens}
`)).toEqual([]); + }); + }); + + describe('logoOnYellow', () => { + it.each(['lockup-light.svg', 'lockup-dark.png', 'mark-color.svg'])('flags %s on a Poster (yellow) piece', (file) => { + expect(rules(`${tokens}
`)).toContain('logoOnYellow'); + }); + + it.each(['sticker.svg', 'lockup-ink.svg'])('accepts %s on a Poster piece', (file) => { + expect(rules(`${tokens}
`)).toEqual([]); + }); + }); + + describe('usesTokens', () => { + it('requires a tokens.css link', () => { + expect(rules('

x

')).toEqual(['usesTokens']); + }); + + it('accepts an absolute tokens.css link', () => { + expect(rules('

x

')).toEqual([]); + }); + }); +}); diff --git a/packages/brand/test/logos.test.ts b/packages/brand/test/logos.test.ts new file mode 100644 index 0000000..b84b68f --- /dev/null +++ b/packages/brand/test/logos.test.ts @@ -0,0 +1,118 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { brand } from '../src/brand.ts'; +import { erode, iou, readPng, renderSvg, whiteMask, type Raster } from './helpers/raster.ts'; + +const source = new URL('../source/', import.meta.url); +const logos = new URL('../assets/logo/', import.meta.url); +const svg = (name: string) => readFileSync(new URL(`${name}.svg`, logos), 'utf8'); +const mask = (id: string) => whiteMask(readPng(new URL(`regions/${id}.png`, source))); + +const marks = ['mark-color', 'mark-ink', 'mark-white', 'sticker']; +const lockups = ['lockup-light', 'lockup-dark', 'lockup-ink', 'lockup-white']; + +describe('logo files', () => { + it.each([...marks, ...lockups])('ships %s as SVG and PNG at the right width', (name) => { + expect(existsSync(new URL(`${name}.svg`, logos))).toBe(true); + const png = readPng(new URL(`${name}.png`, logos)); + expect(png.width).toBe(name.startsWith('lockup') ? 2048 : 1024); + }); + + it('ships the tilted sticker PNG at 1024px', () => { + expect(readPng(new URL('sticker-tilted.png', logos)).width).toBe(1024); + }); +}); + +describe('mark-color', () => { + const rendered = renderSvg(svg('mark-color')); + const flat = readPng(new URL('mark-flattened.png', source)); + + it('renders at the source size', () => { + expect([rendered.width, rendered.height]).toEqual([flat.width, flat.height]); + }); + + it.each(['ring', 'inner', 'blue', 'green', 'red'])('matches the PSD colors within the %s region', (id) => { + const inside = erode(mask(id), flat.width, 2); + const diff = [0, 0, 0]; + let n = 0; + inside.forEach((on, i) => { + if (!on) return; + n++; + for (let c = 0; c < 3; c++) diff[c] += Math.abs(rendered.data[i * 4 + c] - flat.data[i * 4 + c]); + }); + for (const d of diff) expect(d / n).toBeLessThanOrEqual(16); + }); +}); + +describe('one-color marks', () => { + it.each([ + ['mark-ink', '#0E0F11'], + ['mark-white', '#FFFFFF'], + ])('%s uses only %s, with no gradient', (name, color) => { + const text = svg(name); + expect(text).not.toContain('Gradient'); + const fills = [...text.matchAll(/fill="([^"]+)"/g)].map((m) => m[1]); + expect(fills.length).toBeGreaterThan(0); + expect(new Set(fills)).toEqual(new Set([color])); + }); + + it('lets the background show through the white gaps of mark-ink', () => { + const r = renderSvg(svg('mark-ink')); + const [inner, blue, green, red] = ['inner', 'blue', 'green', 'red'].map(mask); + const gaps = erode(inner.map((on, i) => on && !blue[i] && !green[i] && !red[i]), r.width, 2); + const total = gaps.filter(Boolean).length; + const clear = gaps.filter((on, i) => on && r.data[i * 4 + 3] < 8).length; + expect(total).toBeGreaterThan(1000); + expect(clear / total).toBeGreaterThanOrEqual(0.99); + }); +}); + +describe('sticker', () => { + const text = svg('sticker'); + + it('pads the viewBox by 5% of the mark width on each side', () => { + const [x, y, w, h] = text.match(/viewBox="([^"]+)"/)![1].split(/\s+/).map(Number); + const edge = 1005 * 0.05; + expect(x).toBeCloseTo(-edge, 1); + expect(y).toBeCloseTo(-edge, 1); + expect(w).toBeCloseTo(1005 + 2 * edge, 1); + expect(h).toBeCloseTo(1006 + 2 * edge, 1); + }); + + it('draws the white die-cut edge before the mark', () => { + const firstPath = text.indexOf('', firstPath))).toMatch(/fill="#FFFFFF"[^>]*stroke="#FFFFFF"/); + expect(text.indexOf('url(#')).toBeGreaterThan(firstPath); + }); +}); + +describe('lockup-light wordmark', () => { + it('overlays the 2025 horizontal lockup with IoU >= 0.90', () => { + const ref = readPng(new URL('js-horizontal.png', source)); + const ours = renderSvg(svg('lockup-light'), ref.width); + expect([ours.width, ours.height]).toEqual([ref.width, ref.height]); + const markRight = 1300; // the wordmark starts at x 1392 in the reference + const dark = (r: Raster) => + Array.from({ length: r.width * r.height }, (_, i) => { + if (i % r.width < markRight || r.data[i * 4 + 3] < 128) return false; + const [R, G, B] = [r.data[i * 4], r.data[i * 4 + 1], r.data[i * 4 + 2]]; + return 0.299 * R + 0.587 * G + 0.114 * B < 60; + }); + expect(iou(dark(ours), dark(ref))).toBeGreaterThanOrEqual(0.9); + }); +}); + +describe('logo assets in brand data', () => { + const uses = (id: string) => brand.assets.filter((a) => a.id === id || a.id === `${id}-png`).map((a) => a.use); + + it.each(['lockup-ink', 'mark-ink'])('%s is listed for yellow and light backgrounds', (id) => { + expect(uses(id)).toHaveLength(2); + for (const use of uses(id)) expect(use).toMatch(/yellow/i); + for (const use of uses(id)) expect(use).toMatch(/light/i); + }); + + it.each(['lockup-white', 'mark-white'])('%s is listed for dark backgrounds', (id) => { + expect(uses(id)).toHaveLength(2); + for (const use of uses(id)) expect(use).toMatch(/dark/i); + }); +}); diff --git a/packages/brand/test/templates.test.ts b/packages/brand/test/templates.test.ts new file mode 100644 index 0000000..bc23080 --- /dev/null +++ b/packages/brand/test/templates.test.ts @@ -0,0 +1,50 @@ +import { readFileSync } from 'node:fs'; +import { parseHTML } from 'linkedom'; +import { describe, expect, it } from 'vitest'; +import { brand } from '../src/brand.ts'; +import { lintHtml } from '../src/lint.ts'; + +const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8'); + +describe('templates', () => { + it.each(['templates/event-post.html', 'templates/fb-cover.html'])('%s passes lint', (path) => { + expect(lintHtml(read(path))).toEqual([]); + }); + + it('gives the event post a mono event line ending in jscebu.org', () => { + const { document } = parseHTML(read('templates/event-post.html')); + expect(document.querySelector('.jc-event-line')?.textContent).toMatch(/·.*·.*JSCEBU\.ORG/i); + }); + + it.each(['templates/event-post.html', 'templates/fb-cover.html'])('%s links the brand assets by their /brand/ paths', (path) => { + const html = read(path); + expect(html).toContain('href="/brand/tokens.css"'); + expect(html).toMatch(/src="\/brand\/logo\/[a-z-]+\.(svg|png)"/); + }); + + it('puts the real photo on the Facebook cover', () => { + expect(read('templates/fb-cover.html')).toContain('src="/brand/photos/community-meetup.jpg"'); + }); +}); + +describe('personality examples', () => { + it.each(brand.personalities.map((p) => [p.id, p] as const))('%s example passes lint', (_, p) => { + expect(p.exampleHtml).not.toBe(''); + expect(lintHtml(p.exampleHtml)).toEqual([]); + expect(p.exampleHtml).toContain(`class="jc-${p.id}`); + }); +}); + +describe('Bisaya copy', () => { + // Spec §11: Dorell approves all Bisaya copy before it appears anywhere. Agents copy these verbatim. + const drafts = brand.voice.phrases.filter((p) => p.status === 'draft').map((p) => p.bisaya); + const pieces: [string, string][] = [ + ...brand.personalities.map((p) => [`${p.id} example`, p.exampleHtml] as [string, string]), + ['event-post template', read('templates/event-post.html')], + ['fb-cover template', read('templates/fb-cover.html')], + ]; + + it.each(pieces)('keeps draft phrases out of the %s', (_, html) => { + for (const phrase of drafts) expect(html.toLowerCase(), phrase).not.toContain(phrase.toLowerCase()); + }); +}); diff --git a/packages/brand/test/trace.test.ts b/packages/brand/test/trace.test.ts new file mode 100644 index 0000000..748d466 --- /dev/null +++ b/packages/brand/test/trace.test.ts @@ -0,0 +1,33 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { alphaMask, iou, readPng, renderSvg, whiteMask } from './helpers/raster.ts'; + +const source = new URL('../source/', import.meta.url); +const pathsFile = new URL('mark-paths.json', source); +const regions = ['ring', 'inner', 'blue', 'green', 'red', 'outer'] as const; + +const channels = (hex: string) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)); +const near = (hex: string, target: string, tol: number) => + channels(hex).every((v, i) => Math.abs(v - channels(target)[i]) <= tol); + +describe('traced logo mark', () => { + it('has a mark-paths.json', () => { + expect(existsSync(pathsFile)).toBe(true); + }); + + const mark = existsSync(pathsFile) ? JSON.parse(readFileSync(pathsFile, 'utf8')) : null; + + it.each(regions)('traces %s with IoU >= 0.97 against its mask', (id) => { + const [, , w, h] = mark.viewBox; + const svg = ``; + const rendered = alphaMask(renderSvg(svg)); + const expected = whiteMask(readPng(new URL(`regions/${id}.png`, source))); + expect(iou(rendered, expected)).toBeGreaterThanOrEqual(0.97); + }); + + it('runs the ring gradient from orange #FF8F00 to yellow #FFDF00', () => { + const stops = mark.gradients.ring.stops; + expect(near(stops[0].color, '#FF8F00', 6), stops[0].color).toBe(true); + expect(near(stops.at(-1).color, '#FFDF00', 6), stops.at(-1).color).toBe(true); + }); +}); diff --git a/packages/brand/tsconfig.json b/packages/brand/tsconfig.json new file mode 100644 index 0000000..a7c3b63 --- /dev/null +++ b/packages/brand/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "test", "scripts"] +} diff --git a/packages/brand/vitest.config.ts b/packages/brand/vitest.config.ts new file mode 100644 index 0000000..43e56f4 --- /dev/null +++ b/packages/brand/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..e56631f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3675 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^24.19.2 + version: 24.19.2 + typescript: + specifier: ^7.0.2 + version: 7.0.2 + vitest: + specifier: ^5.0.5 + version: 5.0.5(@types/node@24.19.2)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1)) + + apps/website: + dependencies: + '@fontsource-variable/archivo': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/inter-tight': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/jetbrains-mono': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/space-grotesk': + specifier: ^5.3.0 + version: 5.3.0 + '@jscebu/brand': + specifier: workspace:* + version: link:../../packages/brand + astro: + specifier: ~7.4.2 + version: 7.4.2(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1) + devDependencies: + linkedom: + specifier: ~0.18.14 + version: 0.18.14 + tsx: + specifier: ^4.23.15 + version: 4.23.15 + vitest: + specifier: ^5.0.5 + version: 5.0.5(@types/node@26.6.5)(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1)) + wrangler: + specifier: ^4.153.0 + version: 4.153.0(@types/node@26.6.5) + yaml: + specifier: ^2.9.1 + version: 2.9.1 + + packages/brand: + dependencies: + linkedom: + specifier: ~0.18.14 + version: 0.18.14 + devDependencies: + '@fontsource/roboto': + specifier: ^5.3.0 + version: 5.3.0 + '@resvg/resvg-js': + specifier: ^2.6.2 + version: 2.6.2 + '@types/opentype.js': + specifier: ^1.3.10 + version: 1.3.10 + '@types/pngjs': + specifier: ^6.0.5 + version: 6.0.5 + opentype.js: + specifier: ^2.0.0 + version: 2.0.0 + pngjs: + specifier: ^7.0.0 + version: 7.0.0 + tsx: + specifier: ^4.23.15 + version: 4.23.15 + +packages: + + '@acemir/cssom@0.9.32': + resolution: {integrity: sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==} + + '@astrojs/compiler-binding-android-arm64@0.5.2': + resolution: {integrity: sha512-kckIxon6Rm/Bxzh9mGPYpRRqUbToUQoHptCmpMmE+Yt0Xumq8CqrKUidyYhrBsrHVO7bxuRzzWUUqvpf+iYceQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@astrojs/compiler-binding-darwin-arm64@0.5.2': + resolution: {integrity: sha512-1ZG5Ar3dD8HtodX9XH1mRmE0qBXZlvjYXSFfpgFwocqfWnvhFuqtGUNseL8wgfImiRMztIjpQqlogQFQQtgiag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@astrojs/compiler-binding-darwin-x64@0.5.2': + resolution: {integrity: sha512-+kl35S7hiHJ2cb7eujwEsPFYYuSgE11uEE2Z20lAJ+undpgulMcW+weQqFPOSSZccEKHOiLIY+kQHkb4e4092w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@astrojs/compiler-binding-linux-arm64-gnu@0.5.2': + resolution: {integrity: sha512-BqlR0EHNQLPLwgL9+xogjsAc7zGf0cHho2CPdhv0P1yqbVMwGVaGAqrANP6VYAXCqSC43AF4Czcxc2n+oqdesg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-arm64-musl@0.5.2': + resolution: {integrity: sha512-GRGyby3AZWw5d6JQzidyxPyvGgHWsSusrpgKyFVpDTKctABF7L3Pd5V1cRJbV36/rcu+Eh292/wL7pF7MkmGkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-linux-x64-gnu@0.5.2': + resolution: {integrity: sha512-9XAqGTMQAtIslMsjK3xdyY6OI9d59OZqcLBB6pvD/DcoR0daYswfrKx2azdDERDY4AqAnWPC7odVM1BmxYv3Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-x64-musl@0.5.2': + resolution: {integrity: sha512-jOViewGeTh9bSfS8vK72ECxoy2yvmA+Pd/ZcEvBI5c7+7IcfHwZ5qA4L9fRYuOhSrAifvQ2dV4vm1XAtloaWBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-wasm32-wasi@0.5.2': + resolution: {integrity: sha512-Z7p64doar1uvR5C9Xs6j1CV9lGlPVQX7OQ2aAtZ/xKC5aGS6GB13nyc2UfQXTQLvZrvVwsyaflTQy6ziY5Bekw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@astrojs/compiler-binding-win32-arm64-msvc@0.5.2': + resolution: {integrity: sha512-Tr13bh+ahTWsjXjzctK7pFpWV+dUU4FtBfETlR7gO+ueex0F+RDch7eRr0YfKysaRhk9tWm14lsCJ5uWyFPNHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@astrojs/compiler-binding-win32-x64-msvc@0.5.2': + resolution: {integrity: sha512-SfKkI+jOQYLh6wojPvVPV0lFfQnxisweN9B+qCzRvZp7DHAuGif8Y0WcC5PUgN4jb8VKy57LggQDtggR46Du1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@astrojs/compiler-binding@0.5.2': + resolution: {integrity: sha512-lEn76+0JcMQ5RDXU0wSMWJg2ZppiZvSULpghC29cfv4o9bhvWFDbQjsCtvjjXN9iVLFQ7fL3wTR2L2UU13mP8g==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@astrojs/compiler-rs@0.5.2': + resolution: {integrity: sha512-IvwYLB8t1M86rKO4oPdBi9GP4gzXcgrVlNgocjwos1jeeUuEIV8d9Crn1PvYD/5IKteWOxpIHomf7e8iYlkE+Q==} + engines: {node: '>=22.12.0'} + + '@astrojs/internal-helpers@0.13.0': + resolution: {integrity: sha512-20jxQqnSmpskcGyad7N5l6Xf1gz0UCa6lNde44bEBdaXGpncEYfAZJ142r2ZgzBaG4dUyzGmhqZWv1INU2eiHQ==} + + '@astrojs/markdown-satteri@0.4.5': + resolution: {integrity: sha512-0IeGdoFbv0f+442KJhYS44Wg8F3MuHmRsxIEYuFyqkjRubc6dPBFZ7azor5t4AeXLQoT5A5h9HyViKpd70Dl9w==} + + '@astrojs/prism@4.1.1': + resolution: {integrity: sha512-sXlAXJX6t4QfuLg+CuCDzpg7NoxWcyb4paFprgtFppLeisCBmh935h+tJ5CWXfsXEoW2IDy/ihbRMtiu28Chjw==} + engines: {node: '>=22.23.4'} + + '@astrojs/telemetry@3.4.1': + resolution: {integrity: sha512-SMkvMsGOpM0FMAoT5pzhgI3LZC0JSPWf7Evwop+fZ3N6UkusPM7lI+sOJjMNI5OJrVoiei1/AGo1cY7sikTc5A==} + engines: {node: '>=22.23.4'} + + '@babel/helper-string-parser@7.29.11': + resolution: {integrity: sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.11': + resolution: {integrity: sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.11': + resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==} + engines: {node: '>=6.9.0'} + + '@bruits/satteri-darwin-arm64@0.10.5': + resolution: {integrity: sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==} + cpu: [arm64] + os: [darwin] + + '@bruits/satteri-darwin-x64@0.10.5': + resolution: {integrity: sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==} + cpu: [x64] + os: [darwin] + + '@bruits/satteri-linux-arm64-gnu@0.10.5': + resolution: {integrity: sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-linux-arm64-musl@0.10.5': + resolution: {integrity: sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@bruits/satteri-linux-x64-gnu@0.10.5': + resolution: {integrity: sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-linux-x64-musl@0.10.5': + resolution: {integrity: sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@bruits/satteri-wasm32-wasi@0.10.5': + resolution: {integrity: sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@bruits/satteri-win32-arm64-msvc@0.10.5': + resolution: {integrity: sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==} + cpu: [arm64] + os: [win32] + + '@bruits/satteri-win32-x64-msvc@0.10.5': + resolution: {integrity: sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==} + cpu: [x64] + os: [win32] + + '@capsizecss/unpack@4.0.1': + resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} + engines: {node: '>=18'} + + '@clack/core@1.5.1': + resolution: {integrity: sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.8.1': + resolution: {integrity: sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==} + engines: {node: '>= 20.12.0'} + + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.2': + resolution: {integrity: sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20261023.1': + resolution: {integrity: sha512-lsEx0i7nR0PNG/erUFFHmQmiidEhLfDE5uQmAL9421/y4/ycuxDDOSaxu3VGGYFFPK5sduyUeMmjIlX4CwOBFg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20261023.1': + resolution: {integrity: sha512-5e79fHgabdDD8EgmjOhJ1S+xMsf6DbC7fL7IgTBizMnzOp0P2isDn7pYmUxI9AJZE9FVQ4RUilH/I36YNg+GQQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20261023.1': + resolution: {integrity: sha512-VHQ6jKFSihjR2+x1buby8XTdBLLJztVnYiB7OLOTYO8aDXB+c1Rd2410PdgFuFYc4AgFEk6PCJLyCLL5L1KBnw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20261023.1': + resolution: {integrity: sha512-ilpRHzfDN8YFRM5CLRKuFw40x/LJBS89yVdhdQmxehlVDuBqjKpJB++zPFbTgKRRFaNupgnXc8RHktXAIhgCmQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20261023.1': + resolution: {integrity: sha512-279WLVjNsMycAwr0EZNiNEf0l2SY6NUtsN80NwZzbTHzakmiN+WSx4w38muxPm+PAU0oSPPAPXKVD+H6Hya8xw==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fontsource-variable/archivo@5.3.0': + resolution: {integrity: sha512-HogK8FJelrD1o7TlZlkIVtHgc20bO5PZRWE7mUeUTdMN055alznQV6/00J00IBeu8FQAH4s3zW9UJNvKExXf+g==} + + '@fontsource-variable/inter-tight@5.3.0': + resolution: {integrity: sha512-ZhZZ29sNZ15P1K+cUt13GeBRYON0akgmNiftshUeDLos7X7zGcRgPzt434879Xhq1QbwlW3M/apBMk7jWabwvA==} + + '@fontsource-variable/jetbrains-mono@5.3.0': + resolution: {integrity: sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==} + + '@fontsource-variable/space-grotesk@5.3.0': + resolution: {integrity: sha512-2IxmvfB08i9vnGB3Ym/AXvhRE+8XOjWMXIyDum03c+tPwH0FUoMNQfGpU8NXPxjbws0Vvss3AH0Zqt4oJBBAdw==} + + '@fontsource/roboto@5.3.0': + resolution: {integrity: sha512-BapRJOWYP+LZ21zp+wBQjfpPYKRoxc4LspJ/RLuI+HSMBD5u/X4O+ESDrSvEqDSy0rAl7GwBJ+09mdc16cVQ1Q==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@napi-rs/wasm-runtime@1.2.6': + resolution: {integrity: sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || 2.0.0-beta.2 + '@emnapi/runtime': ^1.7.1 || 2.0.0-beta.2 + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@oxc-project/types@0.155.0': + resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@resvg/resvg-js-android-arm-eabi@2.6.2': + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@resvg/resvg-js-android-arm64@2.6.2': + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@resvg/resvg-js-darwin-arm64@2.6.2': + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@resvg/resvg-js-darwin-x64@2.6.2': + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@resvg/resvg-js@2.6.2': + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} + engines: {node: '>= 10'} + + '@rolldown/binding-android-arm-eabi@1.2.15': + resolution: {integrity: sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.15': + resolution: {integrity: sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.15': + resolution: {integrity: sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.15': + resolution: {integrity: sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.15': + resolution: {integrity: sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.15': + resolution: {integrity: sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.15': + resolution: {integrity: sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.15': + resolution: {integrity: sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.15': + resolution: {integrity: sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.15': + resolution: {integrity: sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.15': + resolution: {integrity: sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.15': + resolution: {integrity: sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.15': + resolution: {integrity: sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.15': + resolution: {integrity: sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.15': + resolution: {integrity: sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@shikijs/core@4.5.0': + resolution: {integrity: sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.5.0': + resolution: {integrity: sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.5.0': + resolution: {integrity: sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==} + engines: {node: '>=20'} + + '@shikijs/langs@4.5.0': + resolution: {integrity: sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.5.0': + resolution: {integrity: sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==} + engines: {node: '>=20'} + + '@shikijs/themes@4.5.0': + resolution: {integrity: sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==} + engines: {node: '>=20'} + + '@shikijs/types@4.5.0': + resolution: {integrity: sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.1.0': + resolution: {integrity: sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.24': + resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/node@24.19.2': + resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + + '@types/node@26.6.5': + resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==} + + '@types/opentype.js@1.3.10': + resolution: {integrity: sha512-F67EFyk6j02okHz5JCgata3ZRAcZi9GLnzmkHw/rzJq3OCc8/ZVdoKrxMTYjcQP6IYHGBz2cav1cpzkOkPiPCQ==} + + '@types/pngjs@6.0.5': + resolution: {integrity: sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@ungap/structured-clone@1.4.0': + resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} + + '@vitest/mocker@5.0.5': + resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==} + peerDependencies: + msw: ^2.4.9 || ^3.0.0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.5': + resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==} + + am-i-vibing@0.5.0: + resolution: {integrity: sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==} + hasBin: true + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + astro@7.4.2: + resolution: {integrity: sha512-FAH4cdDlFaVnQGjazLTEIC5vJqGlo5XANc1wIbzQHIqAjJTll/RWTUs+SPNW1JdezG4YGxMZllaE5A41T2kzhA==} + engines: {node: '>=22.23.4', npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + peerDependencies: + '@astrojs/markdown-remark': ^7.3.0 + peerDependenciesMeta: + '@astrojs/markdown-remark': + optional: true + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boolbase@2.0.0: + resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==} + engines: {node: '>=20.19.0'} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + common-ancestor-path@2.0.0: + resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} + engines: {node: '>= 18'} + + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cookie@2.0.1: + resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} + engines: {node: '>=22'} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + css-select@6.0.0: + resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==} + + css-select@7.0.0: + resolution: {integrity: sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==} + engines: {node: '>=20.19.0'} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@7.0.0: + resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==} + engines: {node: '>= 6'} + + css-what@8.0.0: + resolution: {integrity: sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==} + engines: {node: '>=20.19.0'} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@6.0.3: + resolution: {integrity: sha512-3woMEITftG1wKeO/4d/QXH1DVKctTSZg3jNGPpjp9hNr5Hos8MCFF5McTML5ZtgKDObm2R591B0spRp2IVernA==} + engines: {node: '>=18'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + dom-serializer@3.1.1: + resolution: {integrity: sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==} + engines: {node: '>=20.19.0'} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domelementtype@3.0.0: + resolution: {integrity: sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==} + engines: {node: '>=20.19.0'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domhandler@6.0.1: + resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==} + engines: {node: '>=20.19.0'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + domutils@4.0.2: + resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==} + engines: {node: '>=20.19.0'} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + es-module-lexer@3.0.4: + resolution: {integrity: sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fnv1a-64@0.1.2: + resolution: {integrity: sha512-mJbcILTPybAR1jdOwt/lVv8N2cffeJFFP+gVbSAwLEx1ujXH27RpPd8Rokec/KX9c76UYIB6Co+H4lQzxPnJfA==} + + fontace@0.4.1: + resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==} + + fontkitten@1.0.3: + resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} + engines: {node: '>=20'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@5.0.0-beta.6: + resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} + engines: {node: '>=20.20.0'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + http-cache-semantics@4.3.0: + resolution: {integrity: sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-docker@4.0.0: + resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} + engines: {node: '>=20'} + hasBin: true + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + linkedom@0.18.14: + resolution: {integrity: sha512-BRUnkOm4YR5vo+qI3j0EbungB0vZPSCue5AdnTQRaxAGSbsOZiBFRO0rh4zlo8qXUhli8pxvnpEXDklMnlq2Rg==} + engines: {node: '>=16'} + peerDependencies: + canvas: '>= 2' + peerDependenciesMeta: + canvas: + optional: true + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + magic-string@1.4.4: + resolution: {integrity: sha512-jQz9sVhrZuphH4NrAIHSgkDJCluQO7Ioi4KGRLa7aTUjAODQ13hENX0YIytqYE/rIkO485gQG2LeBxJaW53kkQ==} + + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.3: + resolution: {integrity: sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==} + + miniflare@5.20261023.0-beta: + resolution: {integrity: sha512-SHY8A98gCvmrnBLqABHRTSh3duAL1z+tHPRZu/tqxHFqVARrZ4oQis/ALYwp3XvzHvs6B/ff/CdcCafqP+cOFA==} + engines: {node: '>=22.0.0'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neotraverse@1.0.1: + resolution: {integrity: sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==} + engines: {node: '>= 10'} + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-mock-http@1.0.5: + resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nth-check@3.0.1: + resolution: {integrity: sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==} + engines: {node: '>=20.19.0'} + + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + opentype.js@2.0.0: + resolution: {integrity: sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A==} + hasBin: true + + p-limit@7.3.3: + resolution: {integrity: sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==} + engines: {node: '>=20'} + + p-queue@9.3.5: + resolution: {integrity: sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ==} + engines: {node: '>=20'} + + p-timeout@7.0.2: + resolution: {integrity: sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==} + engines: {node: '>=20'} + + package-manager-detector@1.9.0: + resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + piccolore@0.1.3: + resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss@8.5.32: + resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} + engines: {node: ^10 || ^12 || >=14} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + process-ancestry@0.1.0: + resolution: {integrity: sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==} + engines: {node: '>=18.0.0'} + + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + rolldown@1.2.15: + resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + satteri@0.10.5: + resolution: {integrity: sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + shiki@4.5.0: + resolution: {integrity: sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==} + engines: {node: '>=20'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smol-toml@1.9.1: + resolution: {integrity: sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==} + engines: {node: '>= 18'} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + svgo@4.1.0: + resolution: {integrity: sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==} + engines: {node: '>=16'} + hasBin: true + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} + + tinyclip@1.0.3: + resolution: {integrity: sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==} + engines: {node: ^16.14.0 || >= 17.3.0} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyps@0.5.1: + resolution: {integrity: sha512-B6Ykif5HflC3AdDkS4Zst/2muYVfeXoVMy+KorqH80s/1x4vunIORpNySH681X7fjj+dFM9MDKD6/Hsccjb87Q==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.15: + resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + uhyphen@0.2.0: + resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + + ultrahtml@1.7.0: + resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + undici@7.30.1: + resolution: {integrity: sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==} + engines: {node: '>=20.18.1'} + + undici@8.12.3: + resolution: {integrity: sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==} + engines: {node: '>=22.19.0'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@1.0.3: + resolution: {integrity: sha512-XGnEwj5sWdCbiBX01mWLSBncS0yMRjnLygau3IfdOlfjMacNIQvTGW4VH7pWBq+gGWArfSef/CensMLjMmIr/A==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.1.0: + resolution: {integrity: sha512-vKXxz6O38sKDj1mtx/k2cWvkkFBuYGx6uGrlzBkVeWCRF/F8sdI0j3Wbkz/5rVNRRuVc8UrgRdbCxNhmYXgP3w==} + + unist-util-visit@5.2.0: + resolution: {integrity: sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ==} + + unstorage@1.17.6: + resolution: {integrity: sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + verkit@0.5.0: + resolution: {integrity: sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==} + engines: {node: '>=18.12.0'} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@8.3.5: + resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 || ^0.8.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@5.0.5: + resolution: {integrity: sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.5 + '@vitest/browser-preview': 5.0.5 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.5 + '@vitest/coverage-v8': 5.0.5 + '@vitest/ui': 5.0.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} + hasBin: true + + workerd@1.20261023.1: + resolution: {integrity: sha512-NeofJFyaYNpqKpFRJW3ApuonaerPd6/JWlfTtLDYf2A6ygqrDEFnYgEXmc0hhOxqnJsSM+kXJN6VebPojzRrsg==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.153.0: + resolution: {integrity: sha512-Hn1AowKzZao/729rMpdpxBZt/RaKQNJ4GtKtHQcOrwT+J/GIq0iwl680+dhJyO00iSsXaCkxoNcLg3yVIwIvyw==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20261023.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@acemir/cssom@0.9.32': {} + + '@astrojs/compiler-binding-android-arm64@0.5.2': + optional: true + + '@astrojs/compiler-binding-darwin-arm64@0.5.2': + optional: true + + '@astrojs/compiler-binding-darwin-x64@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-arm64-gnu@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-arm64-musl@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-x64-gnu@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-x64-musl@0.5.2': + optional: true + + '@astrojs/compiler-binding-wasm32-wasi@0.5.2': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + optional: true + + '@astrojs/compiler-binding-win32-arm64-msvc@0.5.2': + optional: true + + '@astrojs/compiler-binding-win32-x64-msvc@0.5.2': + optional: true + + '@astrojs/compiler-binding@0.5.2': + optionalDependencies: + '@astrojs/compiler-binding-android-arm64': 0.5.2 + '@astrojs/compiler-binding-darwin-arm64': 0.5.2 + '@astrojs/compiler-binding-darwin-x64': 0.5.2 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.5.2 + '@astrojs/compiler-binding-linux-arm64-musl': 0.5.2 + '@astrojs/compiler-binding-linux-x64-gnu': 0.5.2 + '@astrojs/compiler-binding-linux-x64-musl': 0.5.2 + '@astrojs/compiler-binding-wasm32-wasi': 0.5.2 + '@astrojs/compiler-binding-win32-arm64-msvc': 0.5.2 + '@astrojs/compiler-binding-win32-x64-msvc': 0.5.2 + + '@astrojs/compiler-rs@0.5.2': + dependencies: + '@astrojs/compiler-binding': 0.5.2 + + '@astrojs/internal-helpers@0.13.0': + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + picomatch: 4.0.7 + retext-smartypants: 6.2.0 + shiki: 4.5.0 + smol-toml: 1.9.1 + unified: 11.0.5 + yaml: 2.9.1 + + '@astrojs/markdown-satteri@0.4.5': + dependencies: + '@astrojs/internal-helpers': 0.13.0 + '@astrojs/prism': 4.1.1 + github-slugger: 2.0.0 + satteri: 0.10.5 + + '@astrojs/prism@4.1.1': + dependencies: + prismjs: 1.30.0 + + '@astrojs/telemetry@3.4.1': + dependencies: + ci-info: 4.4.0 + dset: 3.1.4 + is-docker: 4.0.0 + package-manager-detector: 1.9.0 + + '@babel/helper-string-parser@7.29.11': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.11': + dependencies: + '@babel/types': 7.29.11 + + '@babel/types@7.29.11': + dependencies: + '@babel/helper-string-parser': 7.29.11 + '@babel/helper-validator-identifier': 7.29.7 + + '@bruits/satteri-darwin-arm64@0.10.5': + optional: true + + '@bruits/satteri-darwin-x64@0.10.5': + optional: true + + '@bruits/satteri-linux-arm64-gnu@0.10.5': + optional: true + + '@bruits/satteri-linux-arm64-musl@0.10.5': + optional: true + + '@bruits/satteri-linux-x64-gnu@0.10.5': + optional: true + + '@bruits/satteri-linux-x64-musl@0.10.5': + optional: true + + '@bruits/satteri-wasm32-wasi@0.10.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.2.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@bruits/satteri-win32-arm64-msvc@0.10.5': + optional: true + + '@bruits/satteri-win32-x64-msvc@0.10.5': + optional: true + + '@capsizecss/unpack@4.0.1': + dependencies: + fontkitten: 1.0.3 + + '@clack/core@1.5.1': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.8.1': + dependencies: + '@clack/core': 1.5.1 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261023.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20261023.1 + + '@cloudflare/workerd-darwin-64@1.20261023.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20261023.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20261023.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20261023.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20261023.1': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.3': + dependencies: + '@emnapi/wasi-threads': 1.2.3 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@fontsource-variable/archivo@5.3.0': {} + + '@fontsource-variable/inter-tight@5.3.0': {} + + '@fontsource-variable/jetbrains-mono@5.3.0': {} + + '@fontsource-variable/space-grotesk@5.3.0': {} + + '@fontsource/roboto@5.3.0': {} + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/wasm-runtime@1.2.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@napi-rs/wasm-runtime@1.2.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@oslojs/encoding@1.1.0': {} + + '@oxc-project/types@0.155.0': {} + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@resvg/resvg-js-android-arm-eabi@2.6.2': + optional: true + + '@resvg/resvg-js-android-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-x64@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js@2.6.2': + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.2 + '@resvg/resvg-js-android-arm64': 2.6.2 + '@resvg/resvg-js-darwin-arm64': 2.6.2 + '@resvg/resvg-js-darwin-x64': 2.6.2 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + + '@rolldown/binding-android-arm-eabi@1.2.15': + optional: true + + '@rolldown/binding-android-arm64@1.2.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.15': + optional: true + + '@rolldown/binding-darwin-x64@1.2.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.15': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.15': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@shikijs/core@4.5.0': + dependencies: + '@shikijs/primitive': 4.5.0 + '@shikijs/types': 4.5.0 + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.5.0': + dependencies: + '@shikijs/types': 4.5.0 + '@shikijs/vscode-textmate': 10.1.0 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.5.0': + dependencies: + '@shikijs/types': 4.5.0 + '@shikijs/vscode-textmate': 10.1.0 + + '@shikijs/langs@4.5.0': + dependencies: + '@shikijs/types': 4.5.0 + + '@shikijs/primitive@4.5.0': + dependencies: + '@shikijs/types': 4.5.0 + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.5.0': + dependencies: + '@shikijs/types': 4.5.0 + + '@shikijs/types@4.5.0': + dependencies: + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.1.0': {} + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.24': {} + + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + + '@types/estree@1.0.9': {} + + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/node@24.19.2': + dependencies: + undici-types: 7.24.6 + + '@types/node@26.6.5': + dependencies: + undici-types: 8.9.0 + + '@types/opentype.js@1.3.10': {} + + '@types/pngjs@6.0.5': + dependencies: + '@types/node': 26.6.5 + + '@types/unist@3.0.3': {} + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@ungap/structured-clone@1.4.0': {} + + '@vitest/mocker@5.0.5(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.5 + estree-walker: 3.0.3 + magic-string: 1.4.4 + optionalDependencies: + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1) + + '@vitest/mocker@5.0.5(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.5 + estree-walker: 3.0.3 + magic-string: 1.4.4 + optionalDependencies: + vite: 8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1) + + '@vitest/spy@5.0.5': {} + + am-i-vibing@0.5.0: + dependencies: + process-ancestry: 0.1.0 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + astro@7.4.2(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1): + dependencies: + '@astrojs/compiler-rs': 0.5.2 + '@astrojs/internal-helpers': 0.13.0 + '@astrojs/markdown-satteri': 0.4.5 + '@astrojs/telemetry': 3.4.1 + '@capsizecss/unpack': 4.0.1 + '@clack/prompts': 1.8.1 + '@oslojs/encoding': 1.1.0 + am-i-vibing: 0.5.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + ci-info: 4.4.0 + clsx: 2.1.1 + common-ancestor-path: 2.0.0 + cookie: 2.0.1 + devalue: 6.0.3 + diff: 9.0.0 + dset: 3.1.4 + es-module-lexer: 3.0.4 + flattie: 1.1.1 + fontace: 0.4.1 + get-tsconfig: 5.0.0-beta.6 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.3.0 + jsonc-parser: 3.3.1 + magic-string: 1.4.4 + magicast: 0.5.5 + mrmime: 2.0.1 + neotraverse: 1.0.1 + obug: 3.0.0 + p-limit: 7.3.3 + p-queue: 9.3.5 + package-manager-detector: 1.9.0 + piccolore: 0.1.3 + picomatch: 4.0.7 + rolldown: 1.2.15 + shiki: 4.5.0 + smol-toml: 1.9.1 + svgo: 4.1.0 + tinyclip: 1.0.3 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyps: 0.5.1 + ultrahtml: 1.7.0 + unifont: 1.0.3 + unstorage: 1.17.6 + verkit: 0.5.0 + vite: 8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1) + vitefu: 1.1.3(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1)) + xxhash-wasm: 1.1.0 + yargs-parser: 22.0.0 + zod: 4.6.7 + optionalDependencies: + sharp: 0.35.5(@types/node@26.6.5) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools' + - aws4fetch + - db0 + - esbuild + - idb-keyval + - ioredis + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - uploadthing + - yaml + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + blake3-wasm@2.1.5: {} + + boolbase@1.0.0: {} + + boolbase@2.0.0: {} + + ccount@2.0.1: {} + + chai@6.3.0: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + ci-info@4.4.0: {} + + clsx@2.1.1: {} + + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + + common-ancestor-path@2.0.0: {} + + cookie-es@1.2.3: {} + + cookie@1.1.1: {} + + cookie@2.0.1: {} + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + css-select@6.0.0: + dependencies: + boolbase: 1.0.0 + css-what: 7.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-select@7.0.0: + dependencies: + boolbase: 2.0.0 + css-what: 8.0.0 + domhandler: 6.0.1 + domutils: 4.0.2 + nth-check: 3.0.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.2 + + css-what@7.0.0: {} + + css-what@8.0.0: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + defu@6.1.7: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + devalue@6.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@9.0.0: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + dom-serializer@3.1.1: + dependencies: + domelementtype: 3.0.0 + domhandler: 6.0.1 + entities: 8.1.0 + + domelementtype@2.3.0: {} + + domelementtype@3.0.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domhandler@6.0.1: + dependencies: + domelementtype: 3.0.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + domutils@4.0.2: + dependencies: + dom-serializer: 3.1.1 + domelementtype: 3.0.0 + domhandler: 6.0.1 + + dset@3.1.4: {} + + entities@4.5.0: {} + + entities@7.0.1: {} + + entities@8.1.0: {} + + error-stack-parser-es@1.0.5: {} + + es-module-lexer@2.3.2: {} + + es-module-lexer@3.0.4: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + eventemitter3@5.0.4: {} + + expect-type@1.4.0: {} + + extend@3.0.2: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + flattie@1.1.1: {} + + fnv1a-64@0.1.2: {} + + fontace@0.4.1: + dependencies: + fontkitten: 1.0.3 + + fontkitten@1.0.3: + dependencies: + tiny-inflate: 1.0.3 + + fsevents@2.3.3: + optional: true + + get-tsconfig@5.0.0-beta.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-slugger@2.0.0: {} + + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.5 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + http-cache-semantics@4.3.0: {} + + iron-webcrypto@1.2.1: {} + + is-docker@4.0.0: {} + + is-plain-obj@4.1.0: {} + + jsonc-parser@3.3.1: {} + + kleur@4.1.5: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + linkedom@0.18.14: + dependencies: + '@acemir/cssom': 0.9.32 + css-select: 7.0.0 + html-escaper: 3.0.3 + htmlparser2: 10.1.0 + uhyphen: 0.2.0 + + lru-cache@11.5.3: {} + + magic-string@1.4.4: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magicast@0.5.5: + dependencies: + '@babel/parser': 7.29.11 + '@babel/types': 7.29.11 + source-map-js: 1.2.2 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.4.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.2.0 + vfile: 6.0.3 + + mdn-data@2.0.28: {} + + mdn-data@2.27.1: {} + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.3: {} + + miniflare@5.20261023.0-beta(@types/node@26.6.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.5(@types/node@26.6.5) + undici: 7.30.1 + workerd: 1.20261023.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + mrmime@2.0.1: {} + + nanoid@3.3.20: {} + + neotraverse@1.0.1: {} + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + node-fetch-native@1.6.7: {} + + node-mock-http@1.0.5: {} + + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nth-check@3.0.1: + dependencies: + boolbase: 2.0.0 + + obug@3.0.0: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + opentype.js@2.0.0: {} + + p-limit@7.3.3: + dependencies: + yocto-queue: 1.2.2 + + p-queue@9.3.5: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.2 + + p-timeout@7.0.2: {} + + package-manager-detector@1.9.0: {} + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + piccolore@0.1.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + pngjs@7.0.0: {} + + postcss@8.5.32: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + prismjs@1.30.0: {} + + process-ancestry@0.1.0: {} + + property-information@7.2.0: {} + + radix3@1.1.2: {} + + readdirp@5.1.1: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + resolve-pkg-maps@1.0.0: {} + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.2.0 + + rolldown@1.2.15: + dependencies: + '@oxc-project/types': 0.155.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.15 + '@rolldown/binding-android-arm64': 1.2.15 + '@rolldown/binding-darwin-arm64': 1.2.15 + '@rolldown/binding-darwin-x64': 1.2.15 + '@rolldown/binding-freebsd-x64': 1.2.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.15 + '@rolldown/binding-linux-arm64-gnu': 1.2.15 + '@rolldown/binding-linux-arm64-musl': 1.2.15 + '@rolldown/binding-linux-ppc64-gnu': 1.2.15 + '@rolldown/binding-linux-s390x-gnu': 1.2.15 + '@rolldown/binding-linux-x64-gnu': 1.2.15 + '@rolldown/binding-linux-x64-musl': 1.2.15 + '@rolldown/binding-openharmony-arm64': 1.2.15 + '@rolldown/binding-win32-arm64-msvc': 1.2.15 + '@rolldown/binding-win32-x64-msvc': 1.2.15 + + satteri@0.10.5: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + optionalDependencies: + '@bruits/satteri-darwin-arm64': 0.10.5 + '@bruits/satteri-darwin-x64': 0.10.5 + '@bruits/satteri-linux-arm64-gnu': 0.10.5 + '@bruits/satteri-linux-arm64-musl': 0.10.5 + '@bruits/satteri-linux-x64-gnu': 0.10.5 + '@bruits/satteri-linux-x64-musl': 0.10.5 + '@bruits/satteri-wasm32-wasi': 0.10.5 + '@bruits/satteri-win32-arm64-msvc': 0.10.5 + '@bruits/satteri-win32-x64-msvc': 0.10.5 + + sax@1.6.1: {} + + semver@7.8.5: {} + + sharp@0.35.5(@types/node@26.6.5): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 26.6.5 + + shiki@4.5.0: + dependencies: + '@shikijs/core': 4.5.0 + '@shikijs/engine-javascript': 4.5.0 + '@shikijs/engine-oniguruma': 4.5.0 + '@shikijs/langs': 4.5.0 + '@shikijs/themes': 4.5.0 + '@shikijs/types': 4.5.0 + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + + sisteransi@1.0.5: {} + + smol-toml@1.9.1: {} + + source-map-js@1.2.2: {} + + space-separated-tokens@2.0.2: {} + + std-env@4.3.0: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + supports-color@10.2.2: {} + + svgo@4.1.0: + dependencies: + commander: 11.1.0 + css-select: 6.0.0 + css-tree: 3.2.1 + css-what: 7.0.0 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.1 + + tiny-inflate@1.0.3: {} + + tinybench@6.2.2: {} + + tinyclip@1.0.3: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyps@0.5.1: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tslib@2.8.1: + optional: true + + tsx@4.23.15: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + + ufo@1.6.4: {} + + uhyphen@0.2.0: {} + + ultrahtml@1.7.0: {} + + uncrypto@0.1.3: {} + + undici-types@7.24.6: {} + + undici-types@8.9.0: {} + + undici@7.30.1: {} + + undici@8.12.3: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@1.0.3: + dependencies: + css-tree: 3.2.1 + fnv1a-64: 0.1.2 + std-env: 4.3.0 + undici: 8.12.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.2.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.1.0 + + unstorage@1.17.6: + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.11 + lru-cache: 11.5.3 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.4 + + verkit@0.5.0: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.32 + rolldown: 1.2.15 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.19.2 + esbuild: 0.28.2 + fsevents: 2.3.3 + tsx: 4.23.15 + yaml: 2.9.1 + + vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.32 + rolldown: 1.2.15 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.6.5 + esbuild: 0.28.2 + fsevents: 2.3.3 + tsx: 4.23.15 + yaml: 2.9.1 + + vitefu@1.1.3(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1)): + optionalDependencies: + vite: 8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1) + + vitest@5.0.5(@types/node@24.19.2)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.5(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.4 + obug: 3.0.0 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1) + why-is-node-running: 3.2.1 + optionalDependencies: + '@types/node': 24.19.2 + transitivePeerDependencies: + - msw + + vitest@5.0.5(@types/node@26.6.5)(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.5(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.4 + obug: 3.0.0 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(tsx@4.23.15)(yaml@2.9.1) + why-is-node-running: 3.2.1 + optionalDependencies: + '@types/node': 26.6.5 + transitivePeerDependencies: + - msw + + why-is-node-running@3.2.1: {} + + workerd@1.20261023.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20261023.1 + '@cloudflare/workerd-darwin-arm64': 1.20261023.1 + '@cloudflare/workerd-linux-64': 1.20261023.1 + '@cloudflare/workerd-linux-arm64': 1.20261023.1 + '@cloudflare/workerd-windows-64': 1.20261023.1 + + wrangler@4.153.0(@types/node@26.6.5): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261023.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.2 + miniflare: 5.20261023.0-beta(@types/node@26.6.5) + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20261023.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + ws@8.21.0: {} + + xxhash-wasm@1.1.0: {} + + yaml@2.9.1: {} + + yargs-parser@22.0.0: {} + + yocto-queue@1.2.2: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.24 + cookie: 1.1.1 + youch-core: 0.3.3 + + zod@4.6.7: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..4de3ba2 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +packages: + - packages/* + - apps/* +allowBuilds: + esbuild: true + workerd: true + sharp: true diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..eddab06 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true + } +}