diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcbb116..534a575 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,3 +101,5 @@ jobs: $B tinymodel "turn a model into a dict" -C crates/lockdocs-core/tests/fixture | grep -q to_dict - name: Docs build run: bun run docs:build + - name: Brand home (hashes and surface copies agree) + run: python3 brand/build.py --check diff --git a/PROJECT.md b/PROJECT.md index bd29682..8c10456 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -20,6 +20,9 @@ MIT licensed. - `packages/lockdocs`: the npm launcher; `packages/npm/*`: native binaries - `bench/`: version-sensitive questions, project pins, and the runner - `docs/`: the VitePress site; `scripts/`: version sync +- `brand/`: the brand home — vector masters, generated favicons and app icons, + colour and type tokens. The site's logo, favicon and theme colours are copies + of it. See `brand/README.md` ## Release diff --git a/brand/README.md b/brand/README.md new file mode 100644 index 0000000..5ef1a6f --- /dev/null +++ b/brand/README.md @@ -0,0 +1,161 @@ +# lockdocs brand + +This folder is the source of truth for the lockdocs mark, its small sizes, its +app icon, its colours and its type. Every surface copies from it — `brand.json` +lists which file each surface is a copy of — and nothing draws its own. +`build.py` rebuilds every derived file from the masters in `svg/`: + +```bash +pip install pillow numpy resvg-py +python3 brand/build.py # regenerate (favicon, app icons, tokens.css, provenance) +python3 brand/build.py --resnap # also redraw the 16/32 px grids from the master +python3 brand/build.py --check # verify hashes and surface copies (CI runs this; stdlib only) +``` + +## Name + +Running text is `lockdocs`: lower case, one word, everywhere — the README, the +docs site, the npm package `@sylphx/lockdocs`, the registry entry +`io.github.SylphxAI/lockdocs`, and the `lockdocs` binary. No document in this +repository writes `Lockdocs` or `LockDocs`. The only upper-case form is the +`LOCKDOCS_*` environment-variable prefix, which is not the name. + +lockdocs is operated by Sylphx Limited. The operator line is never part of the +brand: it belongs to the licence, legal pages and the site footer, and the mark +never carries it. + +There are no local-script names: the product ships in English. + +The mark is a padlock with no lettering and there is no wordmark, so the name +never appears inside the logo and has no capitalised form anywhere. + +## Files + +| Need | File | +|---|---| +| The mark, anywhere it is placed (site header, README, slides) | `svg/lockdocs-symbol.svg` | +| One-colour printing, engraving, embroidery, stamps | `svg/lockdocs-symbol-black.svg`, `svg/lockdocs-symbol-white.svg` | +| App icon master (store listing, launcher) | `svg/lockdocs-app-icon.svg` | +| Full-bleed square for masks (Android maskable, Apple touch, vendors that crop) | `svg/lockdocs-maskable.svg` | +| Browser tab | `favicon/favicon.svg` (32 px pixel grid), `favicon/favicon.ico` (16, 32, 48), `favicon/favicon-16.png`, `favicon/favicon-32.png`, `favicon/favicon-48.png` | +| Home screen, store listing, future web-app manifest | `app-icon/icon-192.png`, `app-icon/icon-512.png`, `app-icon/icon-1024.png`, `app-icon/icon-maskable-192.png`, `app-icon/icon-maskable-512.png`, `app-icon/apple-touch-icon-180.png` | +| The website's own logo file (served at `/lockdocs/logo.svg`) | `docs/public/logo.svg` | +| Colour and type tokens | `tokens.json` (source), `tokens.css` (generated) | +| Where every file came from, with its SHA-256 | `provenance.json` | + +## Colours + +| Token | Hex | Used for | +|---|---|---| +| `--brand-color-bg` | `#06080C` | Page ground; the app-icon ground; the document lines cut out of the lock body | +| `--brand-color-bg-raised` | `#0B0E14` | Alternating background: code blocks, sidebar, footer | +| `--brand-color-bg-elevated` | `#0E1118` | Elevated surfaces: menus, dialogs, hover cards | +| `--brand-color-bg-soft` | `#11151D` | Soft fills and panels | +| `--brand-color-accent` | `#7C9CFF` | The lock body; links, badges and primary buttons | +| `--brand-color-accent-hover` | `#9DB4FF` | Links, badges and primary buttons on hover | +| `--brand-color-accent-active` | `#6480E8` | Links and buttons when pressed | +| `--brand-color-mint` | `#42D6A4` | The shackle; second stop of the hero gradient | +| `--brand-color-gold` | `#FFD166` | Third stop of the hero gradient | + +The docs site gets its theme colours from these tokens: `custom.css` imports +`brand/tokens.css` and assigns `--vp-c-*` from `var(--brand-color-*)`, and +`docs/.vitepress/config.ts` reads the `theme-color` meta tag from +`tokens.json`. The values are unchanged from the site that shipped before the +brand home existed. + +## Type + +Body and interface are set in Inter (`--brand-font-sans`), served by VitePress +with the docs theme from the built site's own `/assets` — self-hosted, no font +CDN — under the SIL Open Font License 1.1. The theme already defaults +`--vp-font-family-base` to it, so no surface declares its own face. No type +face is part of the mark, because the mark has no lettering. + +## Small sizes + +16 px and 32 px are not scaled renders. `build.py` renders the app icon at 8x, +quantises every sample to the three colours in `brand.json` (`palette`), and +writes the result as a character grid: `favicon/grid-16.txt` and +`favicon/grid-32.txt`. A pixel takes the colour most of its samples agree on, +and stays empty when fewer than `snap_threshold` (0.5) of its samples are +filled. + +The grid files are meant to be hand-edited — a later `build.py` run draws from +the grid as it stands; only `--resnap` redraws it from the master. +`favicon/favicon.svg` is the 32 px grid as one path per colour, and +`favicon/favicon.ico` holds 16 and 32 from the grids plus 48 from a direct +render. `palette` must list exactly the colours the master uses, or the grids +pick up colours that are not in the brand. + +## Clear space and minimum size + +Not yet specified. This repository has no design document, so no clear space or +minimum size has been recorded. The symbol's viewBox is tight to the artwork: +add clearance where the mark is placed, not inside the file. + +## Do / Don't + +Not yet specified beyond what the files themselves fix: the mark is never +recoloured, so the three accent colours above are the only ones (the one-colour +files are the exception, for print); and the two document lines stay visible in +every version, which in the one-colour files means they are cut out of the lock +body rather than painted over it. The rounded square belongs to the app icon +alone — for anything that crops or masks, use the full-bleed `lockdocs-maskable`. + +## Surfaces + +| Surface | Is a copy of | +|---|---| +| `docs/public/logo.svg` (site header) | `svg/lockdocs-symbol.svg` | +| `docs/public/favicon.svg` | `favicon/favicon.svg` | +| `docs/public/favicon.ico` | `favicon/favicon.ico` | + +`python3 brand/build.py --check` fails if any of these stops being a +byte-for-byte copy. The website files are plain copies under `docs/public/`, so +a regenerated icon is picked up by the next docs build and deploy; the deploy +workflow runs on changes under `docs/`. + +### Surfaces still to move + +- `docs/.vitepress/theme/custom.css` keeps four brand tints as literal + `rgba()`: `--vp-c-brand-soft` (`rgba(124, 156, 255, 0.14)`), the hero + radial gradient (`rgba(124,156,255,.35)`, `rgba(66,214,164,.2)`) and the + hero-shot border and shadow (`rgba(124,156,255,.08)`). They are alpha tints + of `--brand-color-accent` and `--brand-color-mint`; writing them as tokens + needs `color-mix()`, which drops the whole declaration on browsers older + than 2023, so they stay literals. +- `README.md` badges: the shields.io URLs carry `color=7c9cff` (npm badge) and + `42d6a4` (MCP Registry badge). shields.io renders from its own servers and + cannot read a local token. +- Social preview: `docs/.vitepress/config.ts` sets `og:image` to + `docs/public/img/demo.gif`, a 1400×780 terminal recording, not a 1200×630 + still. There is no `og/` master. The audit of 2026-09-28 recorded this as a + gap to fill later rather than draw one now. + +## Provenance + +- `svg/lockdocs-symbol.svg` is `docs/public/logo.svg` moved here; git history + for that path starts at `d4c4d54` (2026-09-25, "feat: lockdocs 0.1 — + exact-version library docs from your lockfile, offline (#1)") and the drawing + has not changed since. The website file is now generated back as a copy. +- `svg/lockdocs-symbol-black.svg` and `-white.svg` are that drawing in one + colour: every fill and stroke set to `#000000` / `#FFFFFF`, with the two + document lines cut out of the lock body by `fill-rule="evenodd"` so they stay + visible on a black or white ground. +- `svg/lockdocs-app-icon.svg` was specified by the brand lead in the audit of + 2026-09-28: the symbol on a `#06080C` rounded square (`rx` 22% of the side), + 1024 viewBox, symbol at 70% width, centred. `svg/lockdocs-maskable.svg` is the + same construction without the corner radius, so Apple's and Android's masks + have a full-bleed square to work with. +- `tokens.json` records the colours and the type face the product already + shipped: the logo's three colours, the docs theme's background and accent + roles, and the theme's Inter stack. +- Every file's SHA-256 is in `provenance.json`; the entry for each file says + where it came from, and `build.py` refreshes the hashes. + +## Trademark + +Not registered. Owner decision owner#781: no trademark filings before the +product earns money. Use ™ at most, never ®. + +Checked 2026-09-28. **Same name, different category:** LockDocs (lockdocs.com) is a client-document and smart-forms platform for wealth and fund managers, listed on Microsoft AppSource. Not a developer tool. Noted once, not blocking. Mark (a padlock over document lines): padlock marks are common; no close match found among AI docs tools (Context7). diff --git a/brand/app-icon/apple-touch-icon-180.png b/brand/app-icon/apple-touch-icon-180.png new file mode 100644 index 0000000..895bb67 Binary files /dev/null and b/brand/app-icon/apple-touch-icon-180.png differ diff --git a/brand/app-icon/icon-1024.png b/brand/app-icon/icon-1024.png new file mode 100644 index 0000000..067663a Binary files /dev/null and b/brand/app-icon/icon-1024.png differ diff --git a/brand/app-icon/icon-192.png b/brand/app-icon/icon-192.png new file mode 100644 index 0000000..040aff1 Binary files /dev/null and b/brand/app-icon/icon-192.png differ diff --git a/brand/app-icon/icon-512.png b/brand/app-icon/icon-512.png new file mode 100644 index 0000000..acbba0d Binary files /dev/null and b/brand/app-icon/icon-512.png differ diff --git a/brand/app-icon/icon-maskable-192.png b/brand/app-icon/icon-maskable-192.png new file mode 100644 index 0000000..b022d48 Binary files /dev/null and b/brand/app-icon/icon-maskable-192.png differ diff --git a/brand/app-icon/icon-maskable-512.png b/brand/app-icon/icon-maskable-512.png new file mode 100644 index 0000000..80706bc Binary files /dev/null and b/brand/app-icon/icon-maskable-512.png differ diff --git a/brand/brand.json b/brand/brand.json new file mode 100644 index 0000000..0de6f27 --- /dev/null +++ b/brand/brand.json @@ -0,0 +1,18 @@ +{ + "name": "lockdocs", + "icon": { + "master": "svg/lockdocs-app-icon.svg", + "small": "svg/lockdocs-app-icon.svg", + "apple_touch": "svg/lockdocs-maskable.svg", + "maskable": "svg/lockdocs-maskable.svg", + "palette": ["#06080C", "#7C9CFF", "#42D6A4"], + "snap_threshold": 0.5, + "sizes": [180, 192, 512, 1024], + "maskable_sizes": [192, 512] + }, + "outputs": { + "docs/public/logo.svg": "svg/lockdocs-symbol.svg", + "docs/public/favicon.svg": "favicon/favicon.svg", + "docs/public/favicon.ico": "favicon/favicon.ico" + } +} diff --git a/brand/build.py b/brand/build.py new file mode 100644 index 0000000..3b92f91 --- /dev/null +++ b/brand/build.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +"""Brand home generator: rebuilds every derived file in brand/ from the SVG masters. + + python3 brand/build.py # regenerate (needs: pip install pillow resvg-py numpy) + python3 brand/build.py --resnap # also redraw the 16/32 px grids from the master + python3 brand/build.py --check # verify hashes and surface copies (stdlib only; for CI) + +brand/brand.json says which master feeds which output and which surface files +are copies of brand files. brand/README.md is the usage sheet. + +Small sizes are pixel-snapped: the master is rendered at 8x and every sample +takes the nearest brand colour from tokens.json (or empty). A pixel takes the +colour most of its samples agree on, and stays empty when fewer than the +threshold of its samples are filled. The result +is stored as a character grid (favicon/grid-16.txt, grid-32.txt) that a +person may hand-edit; later runs draw from the grid until --resnap. +""" +import hashlib, io, json, os, shutil, struct, sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +SPEC = json.load(open(os.path.join(HERE, 'brand.json'))) +TOKENS = json.load(open(os.path.join(HERE, 'tokens.json'))) +GRID_CHARS = '.ABCDEFGHIJKLMNOPQRSTUVWXYZ' + + +def p(rel): + return os.path.join(HERE, rel) + + +def sha(path): + return hashlib.sha256(open(path, 'rb').read()).hexdigest() + + +def colour_tokens(): + out = {} + for name, tok in TOKENS.get('color', {}).items(): + out[name] = tok['$value'] if isinstance(tok, dict) else tok + return out + + +# ---------------------------------------------------------------- tokens.css +def write_tokens_css(): + lines = ['/* Generated by brand/build.py from brand/tokens.json. Do not edit. */', ':root {'] + for group, items in TOKENS.items(): + if group.startswith('$') or not isinstance(items, dict): + continue + for name, tok in items.items(): + v = tok['$value'] if isinstance(tok, dict) else tok + if isinstance(v, list): + v = ', '.join(v) + lines.append(f' --brand-{group}-{name}: {v};') + lines.append('}') + open(p('tokens.css'), 'w').write('\n'.join(lines) + '\n') + + +# ---------------------------------------------------------------- rendering +def render(svg_rel, w, h=None): + import resvg_py + from PIL import Image + h = h or w + png = resvg_py.svg_to_bytes(svg_path=p(svg_rel), width=w, height=h) + img = Image.open(io.BytesIO(bytes(png))).convert('RGBA') + if img.size != (w, h): # a non-square master keeps its aspect ratio: centre it on the canvas + img.thumbnail((w, h), Image.LANCZOS) + canvas = Image.new('RGBA', (w, h), (0, 0, 0, 0)) + canvas.paste(img, ((w - img.width) // 2, (h - img.height) // 2)) + img = canvas + return img + + +def save_png(img, rel): + """Save by extension: .png (optimised), .jpg/.jpeg (quality 85), .webp.""" + os.makedirs(os.path.dirname(p(rel)), exist_ok=True) + if rel.lower().endswith(('.jpg', '.jpeg')): + img.convert('RGB').save(p(rel), quality=85, optimize=True, progressive=True) + else: + img.save(p(rel), optimize=True) + + +def hex_rgb(h): + h = h.lstrip('#') + return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) + + +def snap(svg_rel, size, threshold, palette): + """Render at 8x and quantise every sample to the nearest palette colour (or empty when + under half opaque). A pixel takes the colour most of its samples agree on, and stays + empty when fewer than `threshold` of its samples are filled.""" + import numpy as np + ss = 8 + a = np.asarray(render(svg_rel, size * ss), dtype=np.float64) / 255.0 + pal = np.array([hex_rgb(c) for c in palette], dtype=np.float64) / 255.0 + d = ((a[..., None, :3] - pal[None, None]) ** 2).sum(axis=-1) + idx = d.argmin(axis=-1) + 1 + idx[a[..., 3] < 0.5] = 0 + idx = idx.reshape(size, ss, size, ss).transpose(0, 2, 1, 3).reshape(size, size, ss * ss) + rows = [] + for y in range(size): + row = '' + for x in range(size): + counts = np.bincount(idx[y, x], minlength=len(palette) + 1) + filled = counts[1:].sum() / (ss * ss) + row += '.' if filled < threshold else GRID_CHARS[1 + int(counts[1:].argmax())] + rows.append(row) + return rows + + +def grid_path(size): + return f'favicon/grid-{size}.txt' + + +def write_grid(size, rows, palette): + header = [f'# {size}x{size} pixel grid for the favicon. "." is empty; letters index the palette below.', + '# Hand edits are kept: build.py draws from this file until --resnap.'] + header += [f'# {GRID_CHARS[i + 1]} = {c}' for i, c in enumerate(palette)] + open(p(grid_path(size)), 'w').write('\n'.join(header + rows) + '\n') + + +def read_grid(size): + rows, pal = [], {} + for line in open(p(grid_path(size))): + line = line.rstrip('\n') + if line.startswith('# ') and ' = #' in line: + k, v = line[2:].split(' = ') + pal[k.strip()] = v.strip() + elif line and not line.startswith('#'): + rows.append(line) + assert len(rows) == size and all(len(r) == size for r in rows), f'grid-{size} must be {size}x{size}' + return rows, pal + + +def grid_png(rows, pal, size): + from PIL import Image + img = Image.new('RGBA', (size, size), (0, 0, 0, 0)) + for y, r in enumerate(rows): + for x, ch in enumerate(r): + if ch != '.': + img.putpixel((x, y), hex_rgb(pal[ch]) + (255,)) + return img + + +def grid_svg(rows, pal, size, dark_map=None): + """Pixel grid as one path per colour, run-length merged along rows. dark_map swaps + colours when the browser is in a dark theme.""" + paths = {} + for y, r in enumerate(rows): + x = 0 + while x < size: + ch = r[x] + if ch == '.': + x += 1 + continue + x0 = x + while x < size and r[x] == ch: + x += 1 + paths.setdefault(ch, []).append(f'M{x0} {y}h{x - x0}v1h-{x - x0}z') + dark = {ch: dark_map[c] for ch, c in pal.items() if dark_map and c in dark_map} + style = '' + if dark: + rules = ''.join(f'.{ch}{{fill:{c}}}' for ch, c in sorted(dark.items())) + style = f'' + body = ''.join(f'' + for ch, d in sorted(paths.items())) + return (f'{style}{body}\n') + + +def write_ico(images, rel): + """ICO holding PNG entries (one per size, drawn separately).""" + blobs = [] + for img in images: + b = io.BytesIO() + img.save(b, format='PNG', optimize=True) + blobs.append((img.size[0], b.getvalue())) + out = struct.pack(' diff --git a/brand/favicon/grid-16.txt b/brand/favicon/grid-16.txt new file mode 100644 index 0000000..a23df71 --- /dev/null +++ b/brand/favicon/grid-16.txt @@ -0,0 +1,21 @@ +# 16x16 pixel grid for the favicon. "." is empty; letters index the palette below. +# Hand edits are kept: build.py draws from this file until --resnap. +# A = #06080C +# B = #7C9CFF +# C = #42D6A4 +..AAAAAAAAAAAA.. +.AAAAAAAAAAAAAA. +AAAAAAAAAAAAAAAA +AAAAAAACCAAAAAAA +AAAAAACAACAAAAAA +AAAAACAAAACAAAAA +AAAAACAAAACAAAAA +AAAAABBBBBBAAAAA +AAAAABAAAABAAAAA +AAAAABBBBBBAAAAA +AAAAABAAABBAAAAA +AAAAABBBBBBAAAAA +AAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAA +.AAAAAAAAAAAAAA. +..AAAAAAAAAAAA.. diff --git a/brand/favicon/grid-32.txt b/brand/favicon/grid-32.txt new file mode 100644 index 0000000..9c86a26 --- /dev/null +++ b/brand/favicon/grid-32.txt @@ -0,0 +1,37 @@ +# 32x32 pixel grid for the favicon. "." is empty; letters index the palette below. +# Hand edits are kept: build.py draws from this file until --resnap. +# A = #06080C +# B = #7C9CFF +# C = #42D6A4 +....AAAAAAAAAAAAAAAAAAAAAAAA.... +...AAAAAAAAAAAAAAAAAAAAAAAAAA... +..AAAAAAAAAAAAAAAAAAAAAAAAAAAA.. +.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA. +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAACCCCCCAAAAAAAAAAAAA +AAAAAAAAAAAACCCCCCCCAAAAAAAAAAAA +AAAAAAAAAAACCCAAAACCCAAAAAAAAAAA +AAAAAAAAAAACCAAAAAACCAAAAAAAAAAA +AAAAAAAAAAACCAAAAAACCAAAAAAAAAAA +AAAAAAAAAAACCAAAAAACCAAAAAAAAAAA +AAAAAAAAAAACCAAAAAACCAAAAAAAAAAA +AAAAAAAAAAACCAAAAAACCAAAAAAAAAAA +AAAAAAAAABBBBBBBBBBBBBBAAAAAAAAA +AAAAAAAAABBBBBBBBBBBBBBAAAAAAAAA +AAAAAAAAABBBBAAAAAABBBBAAAAAAAAA +AAAAAAAAABBBBAAAAAABBBBAAAAAAAAA +AAAAAAAAABBBBBBBBBBBBBBAAAAAAAAA +AAAAAAAAABBBBAAAABBBBBBAAAAAAAAA +AAAAAAAAABBBBAAAABBBBBBAAAAAAAAA +AAAAAAAAABBBBBBBBBBBBBBAAAAAAAAA +AAAAAAAAABBBBBBBBBBBBBBAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA. +..AAAAAAAAAAAAAAAAAAAAAAAAAAAA.. +...AAAAAAAAAAAAAAAAAAAAAAAAAA... +....AAAAAAAAAAAAAAAAAAAAAAAA.... diff --git a/brand/provenance.json b/brand/provenance.json new file mode 100644 index 0000000..b903435 --- /dev/null +++ b/brand/provenance.json @@ -0,0 +1,102 @@ +{ + "about": "Where every file in brand/ came from, with its SHA-256. build.py refreshes the hashes.", + "files": { + "README.md": { + "source": "the usage sheet for this brand home, written for the audit of 2026-09-28", + "sha256": "16ec0dd668209e524aa872750e84e3b5a4a4015066681156fa678fe7149b1efb" + }, + "app-icon/apple-touch-icon-180.png": { + "source": "generated by build.py", + "sha256": "d6fc00b6744c5b9586f685cf22bd7442e8ae6e3628c970bb4a6786d7164c8100" + }, + "app-icon/icon-1024.png": { + "source": "generated by build.py", + "sha256": "b33a02acd9c0a0bf0b99bf6bf3423be21c89d4400dfcee88b807e447448eeb9a" + }, + "app-icon/icon-192.png": { + "source": "generated by build.py", + "sha256": "49a272fafe59281a8b7263a90721e92f270f00fd09d27b3fd0593bb646c31bae" + }, + "app-icon/icon-512.png": { + "source": "generated by build.py", + "sha256": "5e885a3260b5a307cc0045abbd1add2eeb0ae529986b3fdd62c81e4abafc1af1" + }, + "app-icon/icon-maskable-192.png": { + "source": "generated by build.py", + "sha256": "ce140eb07785347537d1c84bb804ad28d02f45fd3b4ec0e367c1aaf5545d8368" + }, + "app-icon/icon-maskable-512.png": { + "source": "generated by build.py", + "sha256": "9ec9f90aa165231ee95d7ecbe58cc368e6e7d66a8f7b083c91e92510031d3ec9" + }, + "brand.json": { + "source": "written 2026-09-28 for this brand home: names the masters, the icon palette for the 16/32 px grids, and the surface files that are copies of brand files", + "sha256": "b3365e3feb4b87fb388e42c80bedea9fdfb7cd8bbf4cd715763a12f11a469e7f" + }, + "build.py": { + "source": "copy of the shared brand-home generator, unmodified", + "sha256": "406f811f5e85e5969fa6905cc8f8503788b7d70d0998426922e2ba97caa1bdcf" + }, + "favicon/favicon-16.png": { + "source": "generated by build.py", + "sha256": "aaf5e4a3665fd7e3578156604c4fe2f7704969a958f3593e003f1ab182edfdab" + }, + "favicon/favicon-32.png": { + "source": "generated by build.py", + "sha256": "de69897a8e5ac2883b3f686cdd02365e29fc97be5ce413b144d0642001e370bd" + }, + "favicon/favicon-48.png": { + "source": "generated by build.py", + "sha256": "65f5fa3987a95624088de6c0d5e626b5879793e698824e6d99cec0e3e65ccb88" + }, + "favicon/favicon.ico": { + "source": "generated by build.py", + "sha256": "e020fcdb5849785fe457f6a7c98b94c42b2b556b074d1cc11c7ef2b5aa2d872e" + }, + "favicon/favicon.svg": { + "source": "generated by build.py", + "sha256": "ce6571a05cdedb61b9d6db573b6ef3da06d1456ee0638fce41f37304cbb23ed1" + }, + "favicon/grid-16.txt": { + "source": "generated by build.py", + "sha256": "1bbfd78ae12ec900a53e28309885563ccd64076005c02d73168c19176214b6d0" + }, + "favicon/grid-32.txt": { + "source": "generated by build.py", + "sha256": "144c21fa1bc84370c3ddf07cbbf6a4a626b5d386d0d88005a32a73328c899a41" + }, + "svg/lockdocs-app-icon.svg": { + "source": "specified by the brand lead in the audit of 2026-09-28: the symbol on a #06080C rounded square (rx 22% of the 1024 side), symbol at 70% width, centred", + "sha256": "a5058f927de47ed94eb9f58d44ec146cc514165a431c81d930f8b3e11be2215e" + }, + "svg/lockdocs-maskable.svg": { + "source": "svg/lockdocs-app-icon.svg without the corner radius, so masks that crop (Android maskable, Apple touch) have a full-bleed square", + "sha256": "dd55b9d3e7f7912d6fe9f4f0eca1dbf0cee8d85445bd57dbc01369690d9d5b71" + }, + "svg/lockdocs-symbol-black.svg": { + "source": "one-colour version of svg/lockdocs-symbol.svg: every fill and stroke set to #000000, with the two document lines cut out of the lock body using fill-rule=\"evenodd\" so they stay visible", + "sha256": "dd65b1813ff2287a8783e086998076745754014803b16e898b67a557a4377c29" + }, + "svg/lockdocs-symbol-white.svg": { + "source": "one-colour version of svg/lockdocs-symbol.svg: every fill and stroke set to #FFFFFF, with the two document lines cut out of the lock body using fill-rule=\"evenodd\" so they stay visible", + "sha256": "4df58371e47dc511ccc08f2b2a696349afe8ac9fa3601e5c306000c3d8d984fb" + }, + "svg/lockdocs-symbol.svg": { + "source": "docs/public/logo.svg moved here 2026-09-28; git history for that path starts at d4c4d54 (2026-09-25, 'feat: lockdocs 0.1 - exact-version library docs from your lockfile, offline (#1)') and the drawing is unchanged. build.py copies it back to docs/public/logo.svg.", + "sha256": "8d553fc7585fb61d4e788582ab250ff5af155bf14f8647258e90fbc895653721" + }, + "tokens.css": { + "source": "generated by build.py", + "sha256": "760eb9d8cf89aa8b27e6089f0b2f48829a19a9cfa8132c063196ef1a4202939e" + }, + "tokens.json": { + "source": "the colours and the type face the product already shipped: the logo's three colours, the docs theme's background and accent roles (docs/.vitepress/theme/custom.css) and the theme's Inter stack", + "sha256": "8bd583de4e27f9bbc5bf34f1ef0d944e1e653c6746531d5feae4264d3150d851" + } + }, + "surfaces": { + "docs/public/favicon.ico": "favicon/favicon.ico", + "docs/public/favicon.svg": "favicon/favicon.svg", + "docs/public/logo.svg": "svg/lockdocs-symbol.svg" + } +} diff --git a/brand/svg/lockdocs-app-icon.svg b/brand/svg/lockdocs-app-icon.svg new file mode 100644 index 0000000..2fb44c5 --- /dev/null +++ b/brand/svg/lockdocs-app-icon.svg @@ -0,0 +1 @@ + diff --git a/brand/svg/lockdocs-maskable.svg b/brand/svg/lockdocs-maskable.svg new file mode 100644 index 0000000..02d1a8d --- /dev/null +++ b/brand/svg/lockdocs-maskable.svg @@ -0,0 +1 @@ + diff --git a/brand/svg/lockdocs-symbol-black.svg b/brand/svg/lockdocs-symbol-black.svg new file mode 100644 index 0000000..7ceb873 --- /dev/null +++ b/brand/svg/lockdocs-symbol-black.svg @@ -0,0 +1 @@ + diff --git a/brand/svg/lockdocs-symbol-white.svg b/brand/svg/lockdocs-symbol-white.svg new file mode 100644 index 0000000..91ccd8c --- /dev/null +++ b/brand/svg/lockdocs-symbol-white.svg @@ -0,0 +1 @@ + diff --git a/brand/svg/lockdocs-symbol.svg b/brand/svg/lockdocs-symbol.svg new file mode 100644 index 0000000..9fddf84 --- /dev/null +++ b/brand/svg/lockdocs-symbol.svg @@ -0,0 +1 @@ + diff --git a/brand/tokens.css b/brand/tokens.css new file mode 100644 index 0000000..85aad04 --- /dev/null +++ b/brand/tokens.css @@ -0,0 +1,13 @@ +/* Generated by brand/build.py from brand/tokens.json. Do not edit. */ +:root { + --brand-color-bg: #06080C; + --brand-color-bg-raised: #0B0E14; + --brand-color-bg-elevated: #0E1118; + --brand-color-bg-soft: #11151D; + --brand-color-accent: #7C9CFF; + --brand-color-accent-hover: #9DB4FF; + --brand-color-accent-active: #6480E8; + --brand-color-mint: #42D6A4; + --brand-color-gold: #FFD166; + --brand-font-sans: Inter, ui-sans-serif, system-ui, sans-serif; +} diff --git a/brand/tokens.json b/brand/tokens.json new file mode 100644 index 0000000..78f66d1 --- /dev/null +++ b/brand/tokens.json @@ -0,0 +1,16 @@ +{ + "color": { + "bg": {"$value": "#06080C", "$type": "color", "$description": "Page ground; the app-icon ground; the document lines cut out of the logo's lock body."}, + "bg-raised": {"$value": "#0B0E14", "$type": "color", "$description": "Alternating background: code blocks, sidebar, footer."}, + "bg-elevated": {"$value": "#0E1118", "$type": "color", "$description": "Elevated surfaces: menus, dialogs, hover cards."}, + "bg-soft": {"$value": "#11151D", "$type": "color", "$description": "Soft fills and panels."}, + "accent": {"$value": "#7C9CFF", "$type": "color", "$description": "The lock body; links, badges and primary buttons (docs --vp-c-brand-1)."}, + "accent-hover": {"$value": "#9DB4FF", "$type": "color", "$description": "Links, badges and primary buttons on hover (docs --vp-c-brand-2)."}, + "accent-active": {"$value": "#6480E8", "$type": "color", "$description": "Links and buttons when pressed (docs --vp-c-brand-3)."}, + "mint": {"$value": "#42D6A4", "$type": "color", "$description": "The shackle; second stop of the hero gradient."}, + "gold": {"$value": "#FFD166", "$type": "color", "$description": "Third stop of the hero gradient."} + }, + "font": { + "sans": {"$value": ["Inter", "ui-sans-serif", "system-ui", "sans-serif"], "$type": "fontFamily", "$description": "Body and interface, Latin and CJK; VitePress serves Inter with the docs theme, self-hosted from the built site."} + } +} diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index f4d45e1..809c775 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,11 +1,12 @@ import { defineConfig } from 'vitepress' +import tokens from '../../brand/tokens.json' +const base = '/lockdocs/' const url = 'https://sylphxai.github.io/lockdocs/' const desc = 'Exact-version library docs from your lockfile — local, offline, no rate limits.' -const icon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='6' y='14' width='20' height='14' rx='3' fill='%237c9cff'/%3E%3Cpath d='M10 14V10a6 6 0 0 1 12 0v4' fill='none' stroke='%2342d6a4' stroke-width='3'/%3E%3Crect x='11' y='18' width='10' height='2' rx='1' fill='%2306080c'/%3E%3Crect x='11' y='22' width='7' height='2' rx='1' fill='%2306080c'/%3E%3C/svg%3E" export default defineConfig({ - base: '/lockdocs/', + base, title: 'lockdocs', description: desc, appearance: 'force-dark', @@ -15,8 +16,10 @@ export default defineConfig({ lastUpdated: true, sitemap: { hostname: url }, head: [ - ['link', { rel: 'icon', href: icon }], - ['meta', { name: 'theme-color', content: '#06080c' }], + // The icons are copies of the brand home's files (brand/svg, brand/favicon). + ['link', { rel: 'icon', href: `${base}favicon.ico`, sizes: '48x48' }], + ['link', { rel: 'icon', type: 'image/svg+xml', href: `${base}favicon.svg` }], + ['meta', { name: 'theme-color', content: tokens.color.bg.$value }], ['meta', { property: 'og:type', content: 'website' }], ['meta', { property: 'og:site_name', content: 'lockdocs' }], ['meta', { property: 'og:image', content: `${url}img/demo.gif` }], diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index e8cc156..09189a0 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -1,15 +1,17 @@ +@import '../../../brand/tokens.css'; + :root, .dark { - --vp-c-bg: #06080c; - --vp-c-bg-alt: #0b0e14; - --vp-c-bg-elv: #0e1118; - --vp-c-bg-soft: #11151d; + --vp-c-bg: var(--brand-color-bg); + --vp-c-bg-alt: var(--brand-color-bg-raised); + --vp-c-bg-elv: var(--brand-color-bg-elevated); + --vp-c-bg-soft: var(--brand-color-bg-soft); --vp-c-divider: rgba(255, 255, 255, 0.08); - --vp-c-brand-1: #7c9cff; - --vp-c-brand-2: #9db4ff; - --vp-c-brand-3: #6480e8; + --vp-c-brand-1: var(--brand-color-accent); + --vp-c-brand-2: var(--brand-color-accent-hover); + --vp-c-brand-3: var(--brand-color-accent-active); --vp-c-brand-soft: rgba(124, 156, 255, 0.14); --vp-home-hero-name-color: transparent; - --vp-home-hero-name-background: linear-gradient(120deg, #7c9cff 10%, #42d6a4 60%, #ffd166 95%); + --vp-home-hero-name-background: linear-gradient(120deg, var(--brand-color-accent) 10%, var(--brand-color-mint) 60%, var(--brand-color-gold) 95%); --vp-home-hero-image-background-image: radial-gradient(circle, rgba(124,156,255,.35), rgba(66,214,164,.2) 50%, transparent 70%); --vp-home-hero-image-filter: blur(56px); } diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico new file mode 100644 index 0000000..55453a7 Binary files /dev/null and b/docs/public/favicon.ico differ diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg new file mode 100644 index 0000000..70b5975 --- /dev/null +++ b/docs/public/favicon.svg @@ -0,0 +1 @@ +