diff --git a/AGENTS.md b/AGENTS.md index 6e1503c..d99a6ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ - **Breaking changes:** if work would break what people have saved or exported beyond what the saved-format rule covers, stop and report what breaks and the options before building a migration. - **Shared code:** keep behaviour in its app until a second app needs it, then move it to `packages/` rather than copying it. - **Budgets:** before raising an app's `jsBudget`, measure what grew and say why in the commit. -- **Docs:** reference docs describe what is built; intentions go in `docs/plan/`. Markdown links resolve from the folder of the file holding them; `bun run links` (part of `check`) fails on a missing target. +- **Docs:** each app is described in `docs/apps/`. An app that is not built yet says so at the top, and that file is its design. On a built app, ideas that are not built go under Later; when one ships, write it into the description and remove it from Later. When a description and the code disagree, the code is right and the description is fixed. The shared editor is `docs/editor.md`. Markdown links resolve from the folder of the file holding them; `bun run links` (part of `check`) fails on a missing target. ## Project conventions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a30b8f1..3682831 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,4 +44,4 @@ An app that is not released yet has no saved documents to carry forward. When it ## Docs -Reference docs describe the code as it is. When a doc and the code disagree, fix the doc. Intent that is not built yet goes in [`docs/plan`](./docs/plan). +Each app is described in [`docs/apps`](./docs/apps). When a description and the code disagree, fix the description. An app that is not built yet says so at the top. On a built app, an idea that is not built yet goes under Later in that file. diff --git a/README.md b/README.md index 925616e..acd9606 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ send anonymous page-view analytics to Cloudflare when you are online. The [site](https://peterhewat.github.io/Tools/) lists the tools and their capabilities. The [catalog](packages/catalog/src/index.ts) is the source for that list. +The [roadmap](docs/README.md) shows each app, its place in line, and a link to its description. ## Running it @@ -65,7 +66,7 @@ packages/ json-core/ source-preserving JSON parser and formatter shared by JSON and JWT tsconfig/ shared TypeScript config scripts/ build and scaffold scripts -docs/ decisions, reference and plans +docs/ decisions, and a description of each app ``` Each app is TypeScript built by Vite into plain static files, with **no runtime framework**. diff --git a/docs/README.md b/docs/README.md index ea4e7d0..18185c3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,27 +1,58 @@ -# Docs - -Four kinds of document. The kind tells you how much to trust it, and when it gets deleted. - -| Kind | Meaning | -| ------------- | --------------------------------------------------------------------- | -| **Decision** | Settled once accepted; change it with a new ADR. | -| **Reference** | How the code works today. If it disagrees with the code, it is wrong. | -| **Plan** | Intent. Nothing here is built yet. | -| **Work log** | An open investigation. Deleted when the work lands. | - -| Document | Kind | Read it when | -| -------------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------- | -| [adr/001-static-apps-no-framework.md](adr/001-static-apps-no-framework.md) | Decision | Tempted to add a framework or a runtime dependency | -| [adr/002-catalog-drives-the-site.md](adr/002-catalog-drives-the-site.md) | Decision | Adding an app, or changing the deploy path | -| [adr/003-codemirror-for-code-editing.md](adr/003-codemirror-for-code-editing.md) | Decision | Editing or showing code in an app, or adding a CodeMirror package | -| [svg-spec.md](svg-spec.md) | Reference | Working on SVG | -| [plan/app-ideas.md](plan/app-ideas.md) | Plan | Browsing or prioritizing future Tools apps | -| [plan/code-editor.md](plan/code-editor.md) | Plan | YAML in, a Diff app on the shared editor | - -## Conventions - -- **Reference docs describe the present tense only.** Aspirations belong in a plan. -- **No history in docs.** Say what is and what is intended, not what was tried, renamed or - discarded along the way — that is what git history is for. Scope a plan with "out of scope", - not with a list of abandoned ideas. -- **When a doc and the code disagree, the code wins** — then fix the doc. +# Roadmap + +One overview of the apps. Each name links to a description: what the app does, or the design when +it is not built yet. On a built app, ideas that are not built are appended under Later. Status in +this table is the release or the proposed place in line, not a promise that a Later item will ship. + +## Apps + +**Stable**, **Beta** and **Experiment** are catalog release statuses; an unlisted experiment +may not appear on the public index. **Next** is the next proposed new app, not work already +under way; **Planned** is a later proposal. Future rows are ordered by build priority. + +| App | Status | Purpose | +| -------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [SVG](apps/svg.md) | Stable | Trace images with a Bézier pen and shapes; combine, align, group and fill with gradients. A searchable library of drawings, exported as clean SVG or PNG. | +| [JSON](apps/json.md) | Stable | Format, minify, fold and validate JSON, and fix almost-JSON. Convert it to YAML, CSV, TypeScript types or a JSON Schema. | +| [JWT](apps/jwt.md) | Stable | Decode, encode and verify JSON Web Tokens with HMAC, RSA, ECDSA and Ed25519. | +| [Codes](apps/codes.md) | Stable | QR codes for links, Wi-Fi, contacts, events and more, with your colours, shapes, logo and frame; plus Code 128, EAN-13 and UPC-A barcodes. | +| [Codec](apps/codec.md) | Stable | Convert UTF-8 text, Base64, Base64url, URL components and hex, with every byte visible. | +| [Digests](apps/digests.md) | Stable | Hash text and files with every SHA at once, check an expected digest, and compute HMAC. | +| [Time](apps/time.md) | Next | Convert timestamps, compare availability and measure durations. | +| [Diff](apps/diff.md) | Planned | Compare two texts or JSON documents, with clear changes and navigation. | +| [Certs](apps/certs.md) | Planned | Inspect X.509 certificates, chains and related data. | +| [Notes](apps/notes.md) | Planned | Write Markdown and organise a small local collection of notes. | +| [Magnet](apps/magnet.md) | Planned | Inspect torrent metadata and convert infohashes and magnet links. | + +## Shared groundwork and boundaries + +- `@tools/ui` supplies themes, headers, panels, menus, editor cards, copy/download helpers + and app build wiring. +- `@tools/editor` supplies CodeMirror behind one API; see the + [editor description](editor.md) and [ADR 003](adr/003-codemirror-for-code-editing.md). +- `@tools/bytes` supplies strict UTF-8, hex, Base64/Base64url and Web Crypto helpers. +- `@tools/json-core` supplies source-preserving JSON parsing and formatting. + +Core features run locally, offline and from static hosting, under +[ADR 001](adr/001-static-apps-no-framework.md). Certs' optional revocation relay requires +a separate design decision. Keep a feature when it helps complete the same task with the +same input; give each app an explicit scope boundary. Drawing features belong in SVG. +Cutting and plotting tools serve a different workflow; palettes, gradients and sprite +slicing are outside the roadmap until a concrete need appears. + +Names stay short and tool-like; slugs are lowercase kebab-case. New apps are scaffolded +with `bun run new-app`. Shared logic moves to `packages/` when a second app needs it. +Use existing shared controls, respect saved-format rules and measure bundle growth before +raising a budget. + +## Maintaining this roadmap + +Edit this table when priorities or release statuses change. Future row order is the proposed +build order. The [catalog](../packages/catalog/src/index.ts) controls which apps are built and +their release statuses; this page is the human-readable overview. Do not repeat status or +priority in an app file. + +An app file describes what is built. When it and the code disagree, the code is right and the +file is fixed. Append an idea that is not built under Later; when it ships, write it into the +description and remove it from Later. An app that is not built yet says so at the top, and the +file is its design: append further ideas there. diff --git a/docs/adr/003-codemirror-for-code-editing.md b/docs/adr/003-codemirror-for-code-editing.md index 4c87428..91ccecc 100644 --- a/docs/adr/003-codemirror-for-code-editing.md +++ b/docs/adr/003-codemirror-for-code-editing.md @@ -8,7 +8,7 @@ the shipped files keep working for years whatever happens to the toolchain. Several apps edit or show code: the JSON app's text and its converted views, the SVG app's live -source panel, and planned apps such as Diff, JWT and Icon Check. What they need is an IDE's +source panel, JWT's decoded JSON, and planned apps such as Diff. What they need is an IDE's editor: folding, bracket matching, auto-closing, indent on Enter, go to line, selection by syntax, errors in place, large documents drawn only where they are on screen, plus the parts that make an editor work for everyone: IME composition, phone keyboards and selection handles, diff --git a/docs/apps/certs.md b/docs/apps/certs.md new file mode 100644 index 0000000..395e80a --- /dev/null +++ b/docs/apps/certs.md @@ -0,0 +1,92 @@ +# Certs + +Not built yet. Status and build order are on the [roadmap](../README.md). + +Name: **Certs**. Slug: `certs`. Descriptive title: **X.509 Certificate Inspector**. + +The short name fits the developer audience and the site's other tool names. The descriptive +title makes the app's purpose clear; related certificate material can fit as focused extensions. + +Purpose: paste a PEM or drop a DER certificate and see readable details beside it, without +sending it to a server for basic inspection. + +Initial scope: + +- Subject, issuer, serial number and validity dates. +- Subject alternative names, key type and size, signature algorithm and fingerprints. +- Extensions, including constraints, key usages, OCSP locations and CRL distribution points. +- Multiple certificate PEM blocks, with issuer/subject relationships shown as a potential + chain. A displayed relationship is not itself proof of a verified chain. +- Clear parsing errors and individually labelled checks, rather than one ambiguous + "valid" badge. + +Useful extensions: compare certificates, check a hostname against the certificate's names, +check validity at a chosen time, compare expected fingerprints, and verify signatures +using supplied issuer certificates where supported. + +Keep parsing, time checks, hostname matching, signature verification, chain trust and +revocation as separate results. A successful check does not imply all the others passed. +Building a complete trust engine equivalent to a browser, with a maintained root store +and its policies, is beyond the proposed inspector's scope. + +## Related inputs and optional CSR inspection + +- **Certificate chains:** a certificate and its issuing intermediate certificates, connecting + it towards a root. Display supplied relationships in the initial inspector; verifying + signatures and establishing trust are separate checks, and a supplied root is not + automatically trusted. +- **Certificate signing requests (CSRs):** a public key and requested identity information, + signed by the applicant and submitted to an issuer before a certificate is issued. + Optional later inspection could accept PEM or DER requests, show the subject, public key + and requested extensions such as alternative names, and check the request's signature. + A verified request signature is not an issuer's endorsement of the requested identity; + the issuer may change or reject requested fields. See [PKCS #10](https://www.rfc-editor.org/rfc/rfc2986.html). +- **Revocation data:** signed OCSP responses and certificate revocation lists (CRLs), used + to check revocation within their stated scope and time. Inspection and verification of + imported data can work locally, as described below. + +CSR inspection is an optional extension, not a launch requirement. CSR generation, +private-key management and certificate issuance are outside this inspector's scope. + +## Revocation: OCSP and CRLs + +An OCSP URL in a certificate identifies a responder; it does not make a complete check +possible from the certificate alone. Constructing the request needs issuer name and key +information. A usable result requires verification of the response signature, authorised +signer, certificate identifier and freshness. OCSP returns good, revoked or unknown; +**good is a revocation status, not proof of overall certificate validity or trust**. +See [RFC 6960](https://www.rfc-editor.org/rfc/rfc6960.html). + +There are three possible access paths: + +| Path | What it enables | Constraint | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| Direct browser request | Online checks without a service operated by Tools. | Requires a responder reachable over HTTPS that permits the browser's cross-origin request; issuer retrieval can face the same restrictions. | +| Optional controlled relay | A server forwards the request; the browser verifies the signed response. | Adds hosting, maintenance and a service dependency; it is an architectural choice, not part of the offline core. | +| Imported response | Generate a ready-to-copy OpenSSL command and let the person import its response for inspection and verification. | Keeps the app static, with a manual external retrieval step and supplied issuer material. | + +There is no general webpage-only bypass for a responder that blocks access. `fetch` with +`no-cors` gives an opaque response that JavaScript cannot read. HTTP requests from an HTTPS +page can also be blocked as mixed content. See +[MDN's CORS explanation and proxy alternative](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors) +and [mixed-content rules](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Mixed_content). + +If a relay is pursued, restrict it to the intended certificate-checking operations, validate +destinations including redirects, prevent access to private networks, and bound request and +response sizes and timeouts. It must not become an arbitrary URL-fetching proxy. Make online +requests an explicit action; offline inspection remains useful when the relay is unavailable. + +Frame the feature as revocation checking, rather than OCSP alone. Let's Encrypt ended its +OCSP service on 6 August 2025 and publishes revocation information through CRLs instead +([announcement](https://letsencrypt.org/2025/08/06/ocsp-service-has-reached-end-of-life)). +CRL retrieval encounters the same browser access limits. Importing a CRL is a possible local +extension, with explicit signature, freshness and coverage checks before making a claim. + +Suggested progression: offline inspection first, then verification using supplied materials +and imported revocation data; decide separately whether reliable online checks justify a relay. + +## Decisions before implementation + +- Supported parsing and signature algorithms in the first release. +- Whether imported revocation checks are enough or an optional controlled relay is justified. +- Whether and when to add CSR inspection. diff --git a/docs/apps/codec.md b/docs/apps/codec.md new file mode 100644 index 0000000..954359e --- /dev/null +++ b/docs/apps/codec.md @@ -0,0 +1,18 @@ +# Codec + +Convert UTF-8 text, Base64, Base64url, URL components and hex, with every byte visible. + +Every field holds the same bytes. Editing one rewrites the others; the field being edited is left +as written. While the text cannot be read yet, the error shows beside its name and the other fields +keep their last value. + +UTF-8 is kept byte for byte, including Windows line breaks. Invisible characters are drawn. Bytes +that are not valid UTF-8 leave the text field empty. URL component is percent-encoding; a `+` is a +plus, not a space. Base64 accepts white space and optional padding; Base64url uses the unpadded +URL-safe alphabet. Hex accepts white space between complete byte pairs. + +Digests, beside the text, opens the Digests app on the same bytes. + +## Later + +None yet. diff --git a/docs/apps/codes.md b/docs/apps/codes.md new file mode 100644 index 0000000..ae6e2fd --- /dev/null +++ b/docs/apps/codes.md @@ -0,0 +1,22 @@ +# Codes + +QR codes for links, Wi-Fi, contacts, events and more, with your colours, shapes, logo and frame, +plus Code 128, EAN-13 and UPC-A barcodes. + +Codes are made in the browser and never sent anywhere. They are static: once printed, what they +hold does not change. + +A code can hold a link (`https://` is added when it is missing), plain text, an email, a call, an +SMS, a WhatsApp chat, Wi-Fi credentials, a vCard contact or a calendar event in the local time +zone. A barcode is Code 128 (printable ASCII, up to 80 characters), EAN-13 or UPC-A. Enter one +digit less and the check digit is calculated; enter all of them and it is checked. A Wi-Fi password +is readable by anyone who scans the code. + +Colours, shapes, a logo and a frame change how a QR code looks. A barcode takes the colours only. +Every colour has an opacity; a background at 0% is transparent. A logo can cover the middle, with +error correction raised so the code can still scan. Download is SVG or PNG at about 512, 1024 or +2048 pixels wide, with the clear border scanners need. + +## Later + +None yet. diff --git a/docs/apps/diff.md b/docs/apps/diff.md new file mode 100644 index 0000000..e5f0b20 --- /dev/null +++ b/docs/apps/diff.md @@ -0,0 +1,30 @@ +# Diff + +Not built yet. Status and build order are on the [roadmap](../README.md). + +Purpose: compare two pasted or dropped texts, including source code, SVG exports and JSON documents. Its value extends across the existing tools. + +## Initial scope + +- Two text inputs with side-by-side or unified differences. +- Navigation between changes and clear added, removed and modified content. +- Editable inputs, with unchanged stretches collapsible. +- Copy and download each input without changing its original text. +- A structural JSON mode that ignores object-key ordering while retaining array order, distinguishing missing values from null and preserving exact numbers. Invalid JSON gets an explicit error rather than a misleading structural comparison. + +Use `@codemirror/merge` only through `@tools/editor`, following [ADR 003](../adr/003-codemirror-for-code-editing.md). The shared package supplies rendering and editor integration; JSON comparison rules belong in Diff. Use existing `@tools/json-core` parsing where applicable. + +## Optional extensions + +- Explicit whitespace comparison options, with their effect visible. +- Hand off existing app output through the site's per-tab draft pattern. + +## Scope boundaries + +Compare local documents. Git repository management, hosted reviews, collaboration and a full merge-conflict editor are outside the initial scope. Text comparison and structural JSON comparison are different modes; neither should silently rewrite the inputs. + +## Decisions before implementation + +- Whether structural JSON comparison ships with text comparison or in a following release. +- How structural differences map to source locations and how results display on narrow screens. +- Bundle cost of merge support and whether it should load on demand. diff --git a/docs/apps/digests.md b/docs/apps/digests.md new file mode 100644 index 0000000..e100207 --- /dev/null +++ b/docs/apps/digests.md @@ -0,0 +1,17 @@ +# Digests + +Hash text and files with every SHA at once, check an expected digest, and compute HMAC. + +Text is hashed as its exact UTF-8 bytes. Invisible characters are drawn, and the byte count is +shown. A file is hashed in the browser, up to 64 MiB, and has to be chosen again after a reload. +Codec opens the same text, or a file up to 1 MiB as Base64. + +SHA-256, SHA-384, SHA-512 and SHA-1 are computed together, in hex or Base64. SHA-1 is legacy: do not +rely on it for security. Pasting an expected digest names the algorithm it matches. With HMAC on, +each result is the HMAC of the input keyed by a non-empty key in UTF-8, hex, Base64 or Base64url. + +A digest is not encryption. Web Crypto needs HTTPS or localhost. + +## Later + +None yet. diff --git a/docs/apps/json.md b/docs/apps/json.md new file mode 100644 index 0000000..33cbead --- /dev/null +++ b/docs/apps/json.md @@ -0,0 +1,62 @@ +# JSON + +Format, minify, fold and validate JSON, and fix almost-JSON. Convert it to YAML, CSV, TypeScript +types or a JSON Schema. + +Nothing you open or paste leaves the page. The draft is kept for the tab, up to 2 MB. + +The JSON view is editable, with colours, line numbers and folding. YAML, CSV, Types and Schema +follow the edits; Copy and Export take the view shown. Exact values, above Types and Schema, types +a string as the few values it holds. CSV turns each array into a table, one row per item. A `.csv` +or `.tsv` file opens as JSON, one object per row, and pasted CSV offers the same conversion. +Numbers, true, false and null become values; everything else stays text. + +Format and Minify keep numbers and strings as written. Fix appears when the text is almost JSON — +comments, trailing commas, single quotes, unquoted keys, NaN, Python's True / None — and corrects +those places. An error is underlined where it is. Find searches the view shown. Settings hold the +indent, key sorting, syntax colours, the status-bar path, and whether a string that holds JSON is +turned into real JSON. + +## Later + +### YAML in + +The JSON app opens CSV (a file, or pasted CSV with **Convert CSV to JSON**). YAML is the other +format people hold JSON-shaped data in — config files above all — so it should open the same way: +a `.yaml` / `.yml` file opens as JSON, and pasted YAML offers **Convert YAML to JSON**. + +That needs a YAML reader, written here (no dependencies), for the subset real files use: + +- block maps and lists, nested by indentation; +- the inline forms `[a, b]` and `{a: 1}`; +- plain, single- and double-quoted scalars, with YAML's escapes; +- `|` and `>` block strings, with their chomping indicators; +- comments, and a leading `---`. + +Scalars map to JSON as YAML 1.2's core schema does (`true`, `null`, numbers; `yes` stays a string). +Out of scope: anchors and aliases, tags, several documents in one file, complex keys — a file using +them gets a clear "not supported" message rather than a wrong result. The app's own YAML view is +the test: every document it writes must read back to the same JSON. + +### Editing + +- **Replace** in the find bar, with a regex option: the one everyday editor feature the app lacks. + CodeMirror's search query does the matching; the app's own bar stays. +- **Check against a JSON Schema**: pick or paste a schema (the app already writes them) and see + every problem underlined in place and listed, through `@codemirror/lint`. +- **Complete keys** as you type, from a schema, or from the keys the document already uses at that + place. +- **Hover a value** for its path, its type and, for an object or array, how much it holds. +- **YAML view** folding and indentation from `@codemirror/lang-yaml`; the same grammar helps YAML + in. + +CodeMirror capabilities stay behind `@tools/editor`, following +[ADR 003](../adr/003-codemirror-for-code-editing.md). Shared settings are in the +[editor description](../editor.md). + +### Scope boundaries + +Keep validation and conversion semantics in JSON and share them only when another app needs them. +Define supported JSON Schema vocabulary before implementation; unsupported features must be +explicit. Preserve existing preferences and exact source values under the repository's saved-format +rules. diff --git a/docs/apps/jwt.md b/docs/apps/jwt.md new file mode 100644 index 0000000..9215ff8 --- /dev/null +++ b/docs/apps/jwt.md @@ -0,0 +1,27 @@ +# JWT + +Decode, encode and verify JSON Web Tokens with HMAC, RSA, ECDSA and Ed25519. + +Decoding, signing and verifying run in the page with Web Crypto. The token and keys stay in the +tab, up to 2 MB. + +Editing the encoded token updates the header and payload. Editing the header or payload rebuilds +the token and signs it again when a key that can sign is present. The algorithm is the header's +`alg`; choosing another rewrites the header and signs again. Known header parameters and claims are +explained at the end of their line, with `exp`, `nbf` and `iat` as local dates. Those notes are not +part of the JSON. A token pasted with a `Bearer` prefix, or wrapped over several lines, is made +compact as it is pasted. + +HS256, HS384 and HS512 use a shared secret, as UTF-8 text, Base64url, Base64, hex or an oct JWK. +Signing needs at least 32, 48 or 64 bytes. RS, PS, ES and EdDSA verify with the public key and sign +with the private key, each as PEM or JWK. Generate key makes a fresh secret or key pair for the +algorithm. + +A valid token is three Base64url parts, with a strict JSON header and payload and an `alg`. A +verified signature means the key matches; it does not mean the claims should be trusted. `alg` +`none` is an unsigned token. An encrypted token (JWE, five parts) shows only its header. `exp`, +`nbf` and `iat` are Unix seconds. Duplicate member names, `crit` and unencoded payloads are refused. + +## Later + +None yet. diff --git a/docs/apps/magnet.md b/docs/apps/magnet.md new file mode 100644 index 0000000..5b90988 --- /dev/null +++ b/docs/apps/magnet.md @@ -0,0 +1,29 @@ +# Magnet + +Not built yet. Status and build order are on the [roadmap](../README.md). + +Purpose: parse, inspect and build magnet links for torrent infohashes, entirely locally. + +Initial scope: + +- Infohash to a minimal magnet link and magnet link to its constituent fields. +- v1 hashes in hex or Base32, with validation and normalisation. +- v2 and hybrid magnet forms, respecting their different hash encodings. +- Optional display name and editable tracker list, correctly URL-encoded. +- Copyable results and clear malformed-input errors. + +The input is a torrent **infohash**, not an arbitrary hash of a downloaded file. A hash can +generate a minimal magnet but cannot recover an original display name or tracker list. +Magnet links can carry both v1 and v2 identifiers for a hybrid torrent. The +[BitTorrent magnet specification](https://www.bittorrent.org/beps/bep_0009.html) defines +these forms. This distinct meaning warrants an app separate from Digests. + +Useful extensions: inspect a `.torrent` file, extract its metadata and generate a magnet; +batch parsing/conversion. Infohash calculation must follow the torrent format's exact +hashing rules rather than hash the whole `.torrent` file. + +Out of scope: torrent downloading, peer discovery, tracker monitoring and a torrent client. + +## Decisions before implementation + +- Whether `.torrent` inspection belongs in the first release or follows link conversion. diff --git a/docs/apps/notes.md b/docs/apps/notes.md new file mode 100644 index 0000000..66b7704 --- /dev/null +++ b/docs/apps/notes.md @@ -0,0 +1,32 @@ +# Notes + +Not built yet. Status and build order are on the [roadmap](../README.md). + +Purpose: a small Markdown editor with a local collection of notes. The initial product should +be clearly scoped as local notes, rather than expanding into a general knowledge-management app. + +Initial scope: + +- Markdown source and live preview, with safe handling of embedded HTML and links. +- Create, rename and delete notes; autosave; a searchable note list. +- Import and export `.md` files, plus an easy export-all backup. +- Store note content in IndexedDB and preferences in localStorage, following the site's + existing separation between documents and settings. +- Visible save failures and clear recovery behaviour; browser storage is not a backup. + +Useful extensions: word count, search within the current note, and simple folders if the +collection needs them. A fuller virtual filesystem, attachments and access to an actual +device directory should be considered separately rather than assumed by "mini filesystem". + +The Markdown dialect and supported preview syntax need to be specified before implementation. +Respect the runtime dependency rules; the existing editor does not itself provide a rendered +Markdown preview. + +Out of scope: cloud sync, collaboration, accounts, plugins, rich-text editing and a large +attachment library. + +## Decisions before implementation + +- Supported Markdown dialect and safe preview syntax. +- Whether simple folders are needed in the first release. +- Export-all format and storage failure/recovery behaviour. diff --git a/docs/svg-spec.md b/docs/apps/svg.md similarity index 98% rename from docs/svg-spec.md rename to docs/apps/svg.md index c9bf432..0c915ee 100644 --- a/docs/svg-spec.md +++ b/docs/apps/svg.md @@ -1,7 +1,7 @@ -# SVG — Reference +# SVG What the SVG app does and how, as built. When this and the code disagree, the code is right and this -is fixed. +is fixed. Status is on the [roadmap](../README.md). ## 1. Purpose @@ -490,3 +490,16 @@ Documents move between browsers as files (`document-files.ts`): - A transform stored on a group (see §8 for what is done instead). - Creating guides on a phone, where the rulers are hidden. - Dragging gradient stops along the gradient on the canvas; they are typed as percentages. + +## Later + +Source editing, on top of the live SVG panel: + +- **Colour swatches** beside `fill` and `stroke` values, opening the app's colour picker. +- **Complete element and attribute names** from a small SVG schema (`lang-xml` takes one). +- **Hover a line** to outline that shape on the canvas. + +Use the shared colour picker and keep CodeMirror behind `@tools/editor` +([ADR 003](../adr/003-codemirror-for-code-editing.md)). Shared settings are in the +[editor description](../editor.md). Keep drawing features in SVG. Preserve exported SVG +compatibility and the byte-for-byte export → import → export invariant. diff --git a/docs/apps/time.md b/docs/apps/time.md new file mode 100644 index 0000000..04d4d4d --- /dev/null +++ b/docs/apps/time.md @@ -0,0 +1,96 @@ +# Time + +Not built yet. Status and build order are on the [roadmap](../README.md). + +Purpose: convert times, compare availability and measure durations. Three views share +the same date controls and time-zone picker: **Convert**, **Find a time**, and **Duration**. + +## Convert + +Initial scope: + +- Unix timestamps in seconds or milliseconds, with explicit units. Detection may suggest + a unit, but people can see and override it. +- ISO 8601 input/output, UTC, the browser's local zone and a few pinned named zones. +- Conversion in both directions, with copy buttons and a visible UTC offset for the date. +- Relative display such as "in 3 hours" as a supplement to the exact result. + +Use named zones such as `Europe/Paris`, not a fixed list of 24 offsets. Zones can have +fractional-hour offsets, and their offsets depend on the date and daylight-saving rules. +The [IANA time-zone database](https://www.iana.org/time-zones) describes these rules. + +A local time can occur twice or never occur when the clocks change. Explain that result +and require a choice for an ambiguous time rather than silently picking an instant. +Date-only and date-time inputs need visible assumptions about their zone and time. +Any "now" display uses the device clock; it should not promise an independently +synchronised exact clock like [Time.is](https://time.is/). + +Small extension: batch timestamp conversion. A handoff from JWT's time claims would +also fit the existing pattern for opening another app on the same input. + +## Find a time + +Use case: one person enters their own and their friends' availability to find a convenient +call time across several zones, or the least inconvenient compromise when preferences +do not overlap. It works for two people or a group without accounts. + +Inputs: + +- A name and named time zone for each person. +- Multiple availability windows in each person's local time, including windows that + cross midnight. +- Three levels: **preferred**, **acceptable**, and **unavailable**. Define the availability + model so those levels are unambiguous; outside the entered windows is unavailable. +- A call duration and a date range to search. +- Reusable weekly patterns, with exceptions for individual dates. + +Resolve weekly windows for each actual date before comparing them. A recurring local +evening window should follow that person's daylight-saving changes, not a frozen UTC offset. +The call's entire duration must fit, not just its start. Show the calendar date as well +as the clock time for each person when a candidate crosses a date boundary. + +Results: + +- A timeline with one row per person, aligned to the same instants, showing local times, + preferences and shared availability. +- Ranked suggestions, with fully preferred overlaps first and acceptable compromises next. +- A plain explanation of each compromise, for example "Alice: 30 minutes past preferred + hours; everyone else: preferred." + +For compromise ranking, minimise the worst inconvenience for any one person first, then +use total inconvenience to break ties. This avoids making one person bear a large penalty +just to improve the group's total. The exact scoring rule remains to be defined; it should +be deterministic and understandable, and account for the whole call duration. + +Unavailable periods remain excluded unless the person explicitly relaxes them. If no +candidate fits everyone's acceptable windows, show that result and identify which windows +prevent a match; offer relaxation as a deliberate action. Do not quietly schedule a call +during someone's excluded sleep or work hours. + +Save people and patterns locally. Useful later additions are a shareable snapshot and a +copyable summary such as "Saturday: Paris 20:00 / New York 14:00". A snapshot represents +entered information, not live answers from friends; choose its sharing format separately. + +Out of scope: accounts, collecting live availability from participants, calendar integrations, +invitations and an ongoing scheduling service. + +## Duration and age + +Make "How old am I?" a preset of a general duration calculator: + +- A start date/time and optional end date/time, defaulting to now. +- Calendar age in years, months and days, or a total expressed in **days only**, **hours + only**, **minutes only**, or **seconds only**. +- The same controls for "how long since we met?" or "how long until my holiday?" +- An optional live counter when the end is now. + +Calendar age and elapsed duration are separate measurements. An elapsed day is 24 hours; +calendar days can span daylight-saving changes. Define and expose rounding for totals. +A birth date alone does not establish an exact age in seconds: allow a birth time and +zone, or visibly state the assumed time and zone. Resolve ambiguous inputs as in Convert. + +## Decisions before implementation + +- First-release scope for conversion, duration and basic availability overlap. +- Compromise scoring, duration rounding and calendar-age conventions. +- Portable file, URL snapshot or copied text for sharing availability. diff --git a/docs/editor.md b/docs/editor.md new file mode 100644 index 0000000..d4c815f --- /dev/null +++ b/docs/editor.md @@ -0,0 +1,26 @@ +# Shared editor + +`@tools/editor` supplies CodeMirror 6 behind one API +([ADR 003](adr/003-codemirror-for-code-editing.md)). Apps import that package and never +`@codemirror/*` or `@lezer/*`. + +The package exposes text, selection, marks, folds, errors, JSON and XML structure, read-only mode +and a theme taken from the site's colours. What a document means stays in the app: the JSON app's +parser decides what is valid, and the editor shows it. + +## Later + +- Line wrapping. +- Showing whitespace. + +Each consuming app exposes applicable preferences through its Settings menu. The package supplies +the editor behaviour. The app keeps its own preferences and their saved-format handling. + +## Boundaries + +- [Diff](apps/diff.md) may need a shared wrapper around `@codemirror/merge`. Diff keeps its own + comparison rules. +- [JSON](apps/json.md) and [SVG](apps/svg.md) keep their own validation, completion and + source-editing ideas under Later. Add a shared API only when one of those needs it. +- Vim keymaps, minimaps and unrelated third-party editor plugins stay out. +- Measure bundle growth before changing an app's budget. diff --git a/docs/plan/app-ideas.md b/docs/plan/app-ideas.md deleted file mode 100644 index 7b6f151..0000000 --- a/docs/plan/app-ideas.md +++ /dev/null @@ -1,37 +0,0 @@ -# App ideas (plan) - -Intent only — nothing here is built unless it appears in the catalog. Slugs are suggestions for -`bun run new-app`. - -Tools fit: client-only, offline, static output. No "save online", no fetching arbitrary URLs. - -## Shared groundwork (done) - -- `@tools/ui` — `base.css` (tokens, header with a link back to the index, buttons, inputs, - code areas), DOM helpers (`copyText`, `downloadText`, `pickFiles`, `onFileDrop`), and - `toolsApp(slug)` for the Vite config. -- `@tools/editor` — the code editor (CodeMirror 6): folding, errors in place, find marks, the - site's colours. JSON, SVG and JWT use it; Diff and Icon Check would too. -- `@tools/bytes` — strict UTF-8, hex, Base64 and Base64url, plus Web Crypto digest and HMAC helpers shared by JWT, Codec and Digests. -- `@tools/json-core` — JSON parsing and formatting that preserve source numbers and strings, shared by JSON and JWT. - -## Build order - -| # | Slug | Name | What it does | MVP notes | -| --- | ------------ | ---------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| 4 | `icon-check` | Icon Check | See an SVG icon at the sizes it will actually be used at. | Paste or drop an SVG; render at 16/24/32/48/64 px on light and dark, with a pixel grid option to catch half-pixel strokes. Later: PNG / favicon export via canvas. | -| 6 | `diff` | Diff | Compare two texts, or two JSON documents structurally. | Line diff side-by-side or unified; JSON mode compares parsed values (key order ignored). Handy for two SVG exports too. | -| 7 | `time` | Time | Unix timestamps ⇄ dates, across time zones. | Seconds or milliseconds auto-detected; ISO 8601; relative ("in 3 h"). Small, and pairs with JWT's claims. | - -## Out of scope - -- **Separate SVG tools around the SVG app.** What drawing needs belongs in that app itself. Tools for - cutting and plotting (nesting parts on a sheet, stroke-to-outline, toolpath previews) serve a - different workflow. -- **Design utilities** (palettes, gradients, sprite slicing) until a real need shows up. - -## Naming - -- Slugs are lowercase kebab-case and match the folder under `apps/`. -- Short, tool-like names (`json`, `jwt`, `codes`, `svg`); no repeated suffix. -- Shared code moves into `packages/` only once two apps need it. diff --git a/docs/plan/code-editor.md b/docs/plan/code-editor.md deleted file mode 100644 index dc9f186..0000000 --- a/docs/plan/code-editor.md +++ /dev/null @@ -1,61 +0,0 @@ -# Plan: after the shared code editor - -**Kind:** Plan — nothing here is built yet. - -The JSON app (its text and its converted views), the SVG app's source panel and JWT's decoded JSON use -`@tools/editor`: CodeMirror 6 behind a small API -([ADR 003](../adr/003-codemirror-for-code-editing.md)). The JSON text folds, shows its parse -error in place, and is one document however long it is. What follows builds on that. - -## YAML in - -The JSON app opens CSV (a file, or pasted CSV with **Convert CSV to JSON**). YAML is the other -format people hold JSON-shaped data in — config files above all — so it should open the same -way: a `.yaml` / `.yml` file opens as JSON, and pasted YAML offers **Convert YAML to JSON**. - -That needs a YAML reader, written here (no dependencies), for the subset real files use: - -- block maps and lists, nested by indentation; -- the inline forms `[a, b]` and `{a: 1}`; -- plain, single- and double-quoted scalars, with YAML's escapes; -- `|` and `>` block strings, with their chomping indicators; -- comments, and a leading `---`. - -Scalars map to JSON as YAML 1.2's core schema does (`true`, `null`, numbers; `yes` stays a -string). Out of scope: anchors and aliases, tags, several documents in one file, complex keys — -a file using them gets a clear "not supported" message rather than a wrong result. The app's -own YAML view is the test: every document it writes must read back to the same JSON. - -## Later - -- **Diff** (app-ideas #6) on `@codemirror/merge`, added to `@tools/editor`: side by side or - unified, editable, unchanged stretches collapsed. -- Icon Check's pasted SVG in the editor. - -## Optional extras - -What CodeMirror makes cheap to add. None is needed; each is worth doing when someone misses it. -Roughly most useful first. Anything from outside the CodeMirror project (a Vim keymap, a -minimap) is ruled out by ADR 003. - -### JSON - -- **Replace** in the find bar, with a regex option: the one everyday editor feature the app - lacks. CodeMirror's search query does the matching; the app's own bar stays. -- **Check against a JSON Schema**: pick or paste a schema (the app already writes them) and see - every problem underlined in place and listed, through `@codemirror/lint`. -- **Complete keys** as you type, from a schema, or from the keys the document already uses at - that place. -- **Hover a value** for its path, its type and, for an object or array, how much it holds. -- **YAML view** folding and indentation from `@codemirror/lang-yaml`; the same grammar helps - YAML in. - -### SVG source - -- **Colour swatches** beside `fill` and `stroke` values, opening the app's colour picker. -- **Complete element and attribute names** from a small SVG schema (`lang-xml` takes one). -- **Hover a line** to outline that shape on the canvas. - -### Both - -- Settings for **line wrapping** and **showing whitespace**. diff --git a/scripts/docs-links.ts b/scripts/docs-links.ts index 98f7d4c..bd71e82 100644 --- a/scripts/docs-links.ts +++ b/scripts/docs-links.ts @@ -1,8 +1,9 @@ #!/usr/bin/env bun /** - * Fails when a tracked Markdown file links to a path that does not exist. Only relative links are + * Fails when a working-tree Markdown file links to a path that does not exist. Only relative links are * checked (external URLs and `#anchor` links are not), each from the folder of the file holding - * it, as GitHub and editors follow them. Tracked files only, from `git ls-files`. + * it, as GitHub and editors follow them. Includes tracked and non-ignored new files, + * excluding files deleted from the working tree, so checking does not require staging. */ import { existsSync, readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; @@ -38,9 +39,15 @@ export function brokenLinks(root: string, files: readonly string[]): string[] { if (import.meta.main) { const root = resolve(import.meta.dir, ".."); - const git = Bun.spawnSync(["git", "ls-files", "-z", "*.md"], { cwd: root }); + const git = Bun.spawnSync( + ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "*.md"], + { cwd: root } + ); if (git.exitCode !== 0) throw new Error("git ls-files failed: " + git.stderr.toString()); - const broken = brokenLinks(root, git.stdout.toString().split("\0").filter(Boolean)); + const files = [...new Set(git.stdout.toString().split("\0").filter(Boolean))].filter((file) => + existsSync(join(root, file)) + ); + const broken = brokenLinks(root, files); if (broken.length) { console.error(`Broken Markdown links (${broken.length}):\n ${broken.join("\n ")}`); console.error("A link resolves from the folder of the file holding it, not the repo root.");