@casomoltd/tooling 0.21.2 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/bin/build-report.mjs +137 -0
- package/bin/render-report.mjs +2 -6
- package/bin/utils.mjs +13 -0
- package/bin/verify-pack.mjs +1 -0
- package/package.json +3 -1
- package/report/casomo-arrow-white.svg +14 -0
- package/report/casomo-template.typ +66 -0
- package/report/components.typ +56 -0
- package/report/cover.typ +38 -0
- package/report/example.typ +88 -0
- package/report/footer.typ +15 -0
- package/report/mark.typ +13 -0
- package/report/styles.typ +56 -0
- package/report/table.typ +17 -0
- package/report/tokens.typ +17 -0
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Shared linting, formatting, commit config, and CLI tools for Casomo Ltd's repos.
|
|
|
26
26
|
| `commit-msg` | Husky commit-msg hook — runs commitlint |
|
|
27
27
|
| `readability` | Measure reading difficulty of page content |
|
|
28
28
|
| `screenshot` | Capture a dev server page via Puppeteer |
|
|
29
|
+
| `build-report` | Compile a Typst client report to PDF with the house template |
|
|
29
30
|
|
|
30
31
|
## Install
|
|
31
32
|
|
|
@@ -131,6 +132,56 @@ Add these scripts to your `package.json`:
|
|
|
131
132
|
Screenshots are saved to `.claude/screenshots/`. Set
|
|
132
133
|
`SCREENSHOT_URL` to override the default `http://localhost:3000`.
|
|
133
134
|
|
|
135
|
+
## Client reports (Typst)
|
|
136
|
+
|
|
137
|
+
`report/` ships the Casomo house style for client-facing PDF reports —
|
|
138
|
+
a [Typst](https://typst.app) template (`casomo-template.typ`, with the
|
|
139
|
+
brand mark beside it) plus the `build-report` bin that compiles a
|
|
140
|
+
report to PDF:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx build-report path/to/report.typ # → scratch (see below)
|
|
144
|
+
npx build-report report.typ -o public/report.pdf # explicit output path
|
|
145
|
+
npx build-report report.typ --watch # recompile on save
|
|
146
|
+
npx build-report report.typ --open # open the PDF when done
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The compiled PDF is a build artefact, so it defaults to the same scratch
|
|
150
|
+
resolution as the agent reports (see *Report output & `SCRATCH_DIR`*
|
|
151
|
+
under Agents): `$SCRATCH_DIR/reports/<name>.pdf`, or
|
|
152
|
+
`<repo-root>/scratch/reports/` when `SCRATCH_DIR` is unset — add
|
|
153
|
+
`scratch/` to the consumer repo's `.gitignore` (this repo does). Pass
|
|
154
|
+
`--out` only when the PDF is a deliverable the repo actually keeps
|
|
155
|
+
(e.g. a site's `public/`).
|
|
156
|
+
|
|
157
|
+
A report imports the template and applies it as a show rule; everything
|
|
158
|
+
after is the body. From a consumer repo the import goes through
|
|
159
|
+
`node_modules` (the bin sets the Typst root to the nearest
|
|
160
|
+
`package.json`/`.git` ancestor so the path resolves):
|
|
161
|
+
|
|
162
|
+
```typst
|
|
163
|
+
#import "/node_modules/@casomoltd/tooling/report/casomo-template.typ": casomo-report, band
|
|
164
|
+
|
|
165
|
+
#show: casomo-report.with(
|
|
166
|
+
kicker: "Delivery Report & Findings",
|
|
167
|
+
title: "The Report Title",
|
|
168
|
+
subtitle: "One-line summary.", // optional
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
= Executive summary
|
|
172
|
+
…
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`report/example.typ` is a lorem-ipsum reference report exercising every
|
|
176
|
+
feature the template styles — compile it to see the house style.
|
|
177
|
+
|
|
178
|
+
Two prerequisites are external to npm (like puppeteer for
|
|
179
|
+
`screenshot`, they are deliberately not dependencies): the **`typst`
|
|
180
|
+
CLI** on PATH (`snap install typst` / `cargo install typst-cli`) and
|
|
181
|
+
the **IBM Plex Sans / IBM Plex Mono** fonts
|
|
182
|
+
([github.com/IBM/plex](https://github.com/IBM/plex)) — `build-report`
|
|
183
|
+
fails loud on the former and warns on the latter.
|
|
184
|
+
|
|
134
185
|
## Quality gates
|
|
135
186
|
|
|
136
187
|
There is no CI — all quality gates run locally via git hooks.
|
|
@@ -208,6 +259,7 @@ decision table) that keeps these units composing without overlap.
|
|
|
208
259
|
| `/casomoltd:design-pass` | Map → review → refactor a package (drives `design-xray` + `code-review`) |
|
|
209
260
|
| `/casomoltd:draft-design-spec` | Author a browser-reviewable HTML design spec from a brief and iterate on it before writing code (drives `design-xray`) |
|
|
210
261
|
| `/casomoltd:page-design` | Structure a content/explainer page for trust — above-the-fold answer, disclosure, palette-only colour (rubric a page-design reviewer preloads) |
|
|
262
|
+
| `/casomoltd:generate-report` | Scaffold a Typst client report from the house template, compile via `build-report`, verify the PDF |
|
|
211
263
|
|
|
212
264
|
Enable the plugin by adding this repo as a marketplace and installing it:
|
|
213
265
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compile a Typst client report to PDF with the Casomo house template.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* build-report <report.typ> [--out <file.pdf>] [--watch] [--open]
|
|
8
|
+
*
|
|
9
|
+
* Arguments:
|
|
10
|
+
* report.typ The report source. It imports the house template
|
|
11
|
+
* (report/casomo-template.typ in this package).
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* --out, -o <file> Output PDF path. Default follows the README's report
|
|
15
|
+
* output rule: $SCRATCH_DIR/reports/<name>.pdf, or
|
|
16
|
+
* <repo-root>/scratch/reports/ when unset (gitignore
|
|
17
|
+
* scratch/ in the consumer repo).
|
|
18
|
+
* --root <dir> Typst compile root (default: auto-detected, see below)
|
|
19
|
+
* --watch Recompile on save (typst watch)
|
|
20
|
+
* --open Open the PDF after a successful compile (ignored with
|
|
21
|
+
* --watch)
|
|
22
|
+
*
|
|
23
|
+
* Requirements (not npm-managed):
|
|
24
|
+
* - the `typst` CLI on PATH (https://typst.app)
|
|
25
|
+
* - the IBM Plex Sans / IBM Plex Mono fonts installed
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {parseArgs} from "node:util";
|
|
29
|
+
import {spawnSync} from "node:child_process";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import {openPath} from "./utils.mjs";
|
|
33
|
+
|
|
34
|
+
const {values, positionals} = parseArgs({
|
|
35
|
+
options: {
|
|
36
|
+
out: {type: "string", short: "o"},
|
|
37
|
+
root: {type: "string"},
|
|
38
|
+
watch: {type: "boolean", default: false},
|
|
39
|
+
open: {type: "boolean", default: false},
|
|
40
|
+
},
|
|
41
|
+
allowPositionals: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function fail(message) {
|
|
45
|
+
console.error(`build-report: ${message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!positionals[0]) {
|
|
50
|
+
fail("usage: build-report <report.typ> [--out <file.pdf>] [--watch] [--open]");
|
|
51
|
+
}
|
|
52
|
+
const input = path.resolve(positionals[0]);
|
|
53
|
+
if (!fs.existsSync(input)) fail(`no such file: ${input}`);
|
|
54
|
+
if (path.extname(input) !== ".typ") fail(`expected a .typ source, got: ${input}`);
|
|
55
|
+
|
|
56
|
+
// Typst refuses to read files outside its --root, and a report imports the
|
|
57
|
+
// template as "/node_modules/@casomoltd/tooling/report/casomo-template.typ".
|
|
58
|
+
// Prefer the ancestor whose node_modules actually holds this package — in a
|
|
59
|
+
// hoisted/workspace layout that is higher than the report's own package.json —
|
|
60
|
+
// falling back to the nearest package.json/.git ancestor (this repo itself,
|
|
61
|
+
// where the example imports the template relative). No marker at all is an
|
|
62
|
+
// error: compiling on with a wrong root would only surface later as a
|
|
63
|
+
// confusing Typst import failure.
|
|
64
|
+
function findRoot(from) {
|
|
65
|
+
const packageDir = path.join("node_modules", "@casomoltd", "tooling", "report");
|
|
66
|
+
let marker = null;
|
|
67
|
+
let dir = path.dirname(from);
|
|
68
|
+
for (;;) {
|
|
69
|
+
if (fs.existsSync(path.join(dir, packageDir))) return dir;
|
|
70
|
+
if (
|
|
71
|
+
marker === null &&
|
|
72
|
+
(fs.existsSync(path.join(dir, "package.json")) ||
|
|
73
|
+
fs.existsSync(path.join(dir, ".git")))
|
|
74
|
+
) {
|
|
75
|
+
marker = dir;
|
|
76
|
+
}
|
|
77
|
+
const parent = path.dirname(dir);
|
|
78
|
+
if (parent === dir) break;
|
|
79
|
+
dir = parent;
|
|
80
|
+
}
|
|
81
|
+
if (marker) return marker;
|
|
82
|
+
fail("no repo root found above the input (package.json/.git) — pass --root <dir>");
|
|
83
|
+
}
|
|
84
|
+
const root = values.root ? path.resolve(values.root) : findRoot(input);
|
|
85
|
+
|
|
86
|
+
// typst is an external native binary (like puppeteer for screenshot, it is
|
|
87
|
+
// deliberately not an npm dependency). One probe does double duty: ENOENT
|
|
88
|
+
// means typst is missing; otherwise the output feeds the font check. Missing
|
|
89
|
+
// fonts don't fail a Typst compile — it silently falls back and the PDF comes
|
|
90
|
+
// out off-brand — so warn loud up front.
|
|
91
|
+
const fonts = spawnSync("typst", ["fonts"], {encoding: "utf8"});
|
|
92
|
+
if (fonts.error) {
|
|
93
|
+
fail(
|
|
94
|
+
"the `typst` CLI is not on PATH — install it first " +
|
|
95
|
+
"(https://typst.app, e.g. `snap install typst` or `cargo install typst-cli`).",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (fonts.status === 0) {
|
|
99
|
+
const families = new Set(fonts.stdout.split(/\r?\n/));
|
|
100
|
+
// Keep in sync with the faces report/tokens.typ names.
|
|
101
|
+
for (const family of ["IBM Plex Sans", "IBM Plex Mono"]) {
|
|
102
|
+
if (!families.has(family)) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`build-report: font "${family}" not found — the PDF will use a ` +
|
|
105
|
+
"fallback font. Install IBM Plex: https://github.com/IBM/plex",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
console.warn("build-report: could not verify fonts (`typst fonts` failed)");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// A compiled PDF is a throwaway build artefact, not a source — default it to
|
|
114
|
+
// the shared scratch pool, same resolution order as the agent reports (README
|
|
115
|
+
// "Report output & SCRATCH_DIR"): $SCRATCH_DIR/reports/, else a repo-local
|
|
116
|
+
// scratch/reports/. --out opts out for deliverables a repo serves.
|
|
117
|
+
const output = values.out
|
|
118
|
+
? path.resolve(values.out)
|
|
119
|
+
: path.join(
|
|
120
|
+
path.resolve(process.env.SCRATCH_DIR || path.join(root, "scratch")),
|
|
121
|
+
"reports",
|
|
122
|
+
path.basename(input, ".typ") + ".pdf",
|
|
123
|
+
);
|
|
124
|
+
fs.mkdirSync(path.dirname(output), {recursive: true});
|
|
125
|
+
|
|
126
|
+
const mode = values.watch ? "watch" : "compile";
|
|
127
|
+
const result = spawnSync("typst", [mode, "--root", root, input, output], {
|
|
128
|
+
stdio: "inherit",
|
|
129
|
+
});
|
|
130
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
131
|
+
|
|
132
|
+
// typst watch narrates its own recompiles and only returns on interrupt, so
|
|
133
|
+
// the post-compile tail is for single compiles only.
|
|
134
|
+
if (!values.watch) {
|
|
135
|
+
console.log(`Report compiled: ${output}`);
|
|
136
|
+
if (values.open) openPath(output);
|
|
137
|
+
}
|
package/bin/render-report.mjs
CHANGED
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
* 0 HTML written (and opened unless --no-open)
|
|
23
23
|
* 1 Bad arguments or the input file is missing
|
|
24
24
|
*/
|
|
25
|
-
import {spawn} from "node:child_process";
|
|
26
25
|
import {existsSync, readFileSync, writeFileSync} from "node:fs";
|
|
27
26
|
import {resolve} from "node:path";
|
|
27
|
+
import {openPath} from "./utils.mjs";
|
|
28
28
|
|
|
29
29
|
const args = process.argv.slice(2);
|
|
30
30
|
const noOpen = args.includes("--no-open");
|
|
@@ -110,9 +110,5 @@ const url = `file://${resolve(htmlPath)}`;
|
|
|
110
110
|
console.log(url);
|
|
111
111
|
|
|
112
112
|
if (!noOpen) {
|
|
113
|
-
|
|
114
|
-
// opener mis-routes on Linux, run e.g.
|
|
115
|
-
// xdg-mime default firefox_firefox.desktop text/html
|
|
116
|
-
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
117
|
-
spawn(opener, [url], {detached: true, stdio: "ignore"}).unref();
|
|
113
|
+
openPath(url);
|
|
118
114
|
}
|
package/bin/utils.mjs
CHANGED
|
@@ -2,10 +2,23 @@
|
|
|
2
2
|
* Shared helpers for CLI bin scripts.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {spawn} from "node:child_process";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import fs from "node:fs";
|
|
7
8
|
import {pathToFileURL} from "node:url";
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Open a file or URL with the platform opener, detached so the CLI can exit.
|
|
12
|
+
*
|
|
13
|
+
* text/html must be associated with a browser, not a mail client; if the
|
|
14
|
+
* opener mis-routes on Linux, run e.g.
|
|
15
|
+
* xdg-mime default firefox_firefox.desktop text/html
|
|
16
|
+
*/
|
|
17
|
+
export function openPath(target) {
|
|
18
|
+
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
19
|
+
spawn(opener, [target], {detached: true, stdio: "ignore"}).unref();
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
/**
|
|
10
23
|
* Load an optional per-repo config file from cwd.
|
|
11
24
|
*
|
package/bin/verify-pack.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@casomoltd/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Casomo's shared dev config: ESLint, Prettier, commitlint, knip & jscpd presets and dev CLIs (git hooks, readability, screenshots)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"knip/",
|
|
16
16
|
"jscpd/",
|
|
17
17
|
"readability/",
|
|
18
|
+
"report/",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"./readability": "./readability/index.mjs"
|
|
34
35
|
},
|
|
35
36
|
"bin": {
|
|
37
|
+
"build-report": "./bin/build-report.mjs",
|
|
36
38
|
"check-tags": "./bin/check-tags.sh",
|
|
37
39
|
"check-version": "./bin/check-version.sh",
|
|
38
40
|
"commit-msg": "./bin/commit-msg.sh",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg viewBox="0 0 135.467 135.467" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path
|
|
3
|
+
fill="none"
|
|
4
|
+
stroke="#fff"
|
|
5
|
+
stroke-width="10.583"
|
|
6
|
+
stroke-linecap="round"
|
|
7
|
+
stroke-linejoin="round"
|
|
8
|
+
d="M 27.752,77.13
|
|
9
|
+
118.744,77.565
|
|
10
|
+
66.064,36.496
|
|
11
|
+
67.733,58.482
|
|
12
|
+
27.68,58.337 Z"
|
|
13
|
+
/>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// casomo-template.typ — shared house style for Casomo client reports.
|
|
2
|
+
//
|
|
3
|
+
// The single import point. Internals live in the sibling component files —
|
|
4
|
+
// tokens.typ (fonts, rule weights, legal identity) · styles.typ (document
|
|
5
|
+
// type rules) · table.typ (flat tables) · components.typ (band, callout,
|
|
6
|
+
// keyfig, num) · mark.typ (brand mark) · footer.typ · cover.typ — and this
|
|
7
|
+
// file re-exports the author-facing API, so a report needs only:
|
|
8
|
+
//
|
|
9
|
+
// #import "casomo-template.typ": casomo-report, band, callout, keyfig, num
|
|
10
|
+
// #show: casomo-report.with(
|
|
11
|
+
// kicker: "Delivery Report & Findings",
|
|
12
|
+
// title: "The Report Title",
|
|
13
|
+
// subtitle: "One-line summary of what was delivered.", // optional
|
|
14
|
+
// )
|
|
15
|
+
//
|
|
16
|
+
// = Executive summary
|
|
17
|
+
// …
|
|
18
|
+
//
|
|
19
|
+
// Components follow the house minimal direction — flat, hairline dividers,
|
|
20
|
+
// one separation device per element — translated to the report's own
|
|
21
|
+
// identity: monochrome, IBM Plex. Conventions transfer; the site's palette
|
|
22
|
+
// and web faces do not.
|
|
23
|
+
//
|
|
24
|
+
// Asset paths resolve relative to the file using them, so the report/ files
|
|
25
|
+
// stay together as one directory. Importers only need this directory inside
|
|
26
|
+
// the compile root (`typst compile --root …` — the `build-report` bin sets
|
|
27
|
+
// this). Compile with `build-report <report.typ>`; needs the `typst` CLI
|
|
28
|
+
// plus the IBM Plex Sans / IBM Plex Mono fonts installed.
|
|
29
|
+
|
|
30
|
+
#import "components.typ": band, callout, keyfig, num
|
|
31
|
+
#import "styles.typ": document-styles
|
|
32
|
+
#import "table.typ": table-styles
|
|
33
|
+
#import "footer.typ": report-footer
|
|
34
|
+
#import "cover.typ": cover-page
|
|
35
|
+
|
|
36
|
+
#let casomo-report(
|
|
37
|
+
kicker: none,
|
|
38
|
+
title: none,
|
|
39
|
+
subtitle: none,
|
|
40
|
+
author: "David Mohamad",
|
|
41
|
+
email: "info@casomoltd.com",
|
|
42
|
+
body,
|
|
43
|
+
) = {
|
|
44
|
+
// Copyright year tracks the compile date — a report is (re)issued when it is
|
|
45
|
+
// (re)compiled, so the year is never stale.
|
|
46
|
+
let year = datetime.today().display("[year]")
|
|
47
|
+
|
|
48
|
+
show: document-styles
|
|
49
|
+
show: table-styles
|
|
50
|
+
|
|
51
|
+
set page(
|
|
52
|
+
paper: "a4",
|
|
53
|
+
margin: (top: 2.5cm, bottom: 2.5cm, left: 2.5cm, right: 2.5cm),
|
|
54
|
+
header: none,
|
|
55
|
+
footer: report-footer(year),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
cover-page(kicker, title, subtitle, author, email, year)
|
|
59
|
+
|
|
60
|
+
// --- Table of contents ---
|
|
61
|
+
page[
|
|
62
|
+
#outline(title: "Contents", indent: auto, depth: 2)
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
body
|
|
66
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// components.typ — the author-facing body components: band dividers, mono
|
|
2
|
+
// figures, callouts, and key figures. (How a band heading *renders* lives
|
|
3
|
+
// with the other heading rules in styles.typ; this file owns the semantics.)
|
|
4
|
+
#import "tokens.typ": mono-font, intent-bar
|
|
5
|
+
|
|
6
|
+
// Band dividers — unnumbered group headers that organise the contents and the
|
|
7
|
+
// body into sections (e.g. Delivery / Advisories / Decisions / Hand-over)
|
|
8
|
+
// without numbers and without disturbing the 1..N section numbering. Optional:
|
|
9
|
+
// reports that don't want grouped sections simply never call band().
|
|
10
|
+
#let band(title, note: none) = {
|
|
11
|
+
heading(level: 1, numbering: none, outlined: true)[#title]
|
|
12
|
+
if note != none {
|
|
13
|
+
text(size: 9.5pt, style: "italic", fill: luma(110))[#note]
|
|
14
|
+
v(0.1em)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Figures set in IBM Plex Mono so numerals read as data and align
|
|
19
|
+
// digit-for-digit down a column; labels and prose stay in Plex Sans. Sized
|
|
20
|
+
// down slightly to match the sans x-height. Use in numeric table cells and
|
|
21
|
+
// for inline figures: num[£29,970].
|
|
22
|
+
#let num(value) = text(font: mono-font, size: 0.92em, value)
|
|
23
|
+
|
|
24
|
+
// A note the reader must not miss — a thick left bar carries the emphasis,
|
|
25
|
+
// no box, no fill. `label` bolds the intent: callout(label: "Warning:")[…].
|
|
26
|
+
#let callout(body, label: none) = block(
|
|
27
|
+
above: 1.3em,
|
|
28
|
+
below: 1.3em,
|
|
29
|
+
width: 100%,
|
|
30
|
+
stroke: (left: intent-bar),
|
|
31
|
+
inset: (left: 12pt, top: 3pt, bottom: 3pt),
|
|
32
|
+
{
|
|
33
|
+
set text(size: 10pt)
|
|
34
|
+
set par(justify: false)
|
|
35
|
+
if label != none {
|
|
36
|
+
strong(label)
|
|
37
|
+
h(0.4em)
|
|
38
|
+
}
|
|
39
|
+
body
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// A lifted headline metric — big mono figure over a muted caption, carried
|
|
44
|
+
// by the same left bar: keyfig("£30,639", [Band 5, top of scale]).
|
|
45
|
+
#let keyfig(value, caption) = block(
|
|
46
|
+
above: 1.4em,
|
|
47
|
+
below: 1.4em,
|
|
48
|
+
stroke: (left: intent-bar),
|
|
49
|
+
inset: (left: 12pt, top: 2pt, bottom: 2pt),
|
|
50
|
+
{
|
|
51
|
+
text(font: mono-font, weight: 700, size: 20pt, value)
|
|
52
|
+
linebreak()
|
|
53
|
+
v(1pt)
|
|
54
|
+
text(size: 9.5pt, fill: luma(110), caption)
|
|
55
|
+
},
|
|
56
|
+
)
|
package/report/cover.typ
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// cover.typ — the cover page: kicker over the title, the author block, and
|
|
2
|
+
// the brand badge with the legal line. Suppresses the running footer.
|
|
3
|
+
#import "tokens.typ": sans-font, company-name, company-registration
|
|
4
|
+
#import "mark.typ": casomo-mark
|
|
5
|
+
|
|
6
|
+
#let cover-page(kicker, title, subtitle, author, email, year) = page(footer: [])[
|
|
7
|
+
#set text(font: sans-font)
|
|
8
|
+
|
|
9
|
+
#v(1fr)
|
|
10
|
+
|
|
11
|
+
#text(size: 12pt, fill: luma(100))[#kicker]
|
|
12
|
+
#v(8pt)
|
|
13
|
+
#text(size: 28pt, weight: "bold")[#title]
|
|
14
|
+
#if subtitle != none {
|
|
15
|
+
v(10pt)
|
|
16
|
+
text(size: 12pt, fill: luma(100))[#subtitle]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#v(24pt)
|
|
20
|
+
#text(size: 11pt, fill: luma(80))[
|
|
21
|
+
Author: #author \
|
|
22
|
+
Email: #link("mailto:" + email)[#email] \
|
|
23
|
+
Prepared on #datetime.today().display("[day] [month repr:long] [year]")
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
#v(1fr)
|
|
27
|
+
|
|
28
|
+
#grid(
|
|
29
|
+
columns: (28pt, 1fr),
|
|
30
|
+
column-gutter: 12pt,
|
|
31
|
+
align: (horizon, horizon),
|
|
32
|
+
casomo-mark(size: 28pt, arrow: 20pt, radius: 5pt),
|
|
33
|
+
text(size: 10pt)[
|
|
34
|
+
#sym.copyright #year #company-name \
|
|
35
|
+
#company-registration
|
|
36
|
+
],
|
|
37
|
+
)
|
|
38
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// example.typ — lorem-ipsum reference report for the Casomo house template.
|
|
2
|
+
//
|
|
3
|
+
// Exercises every structural feature the template styles (cover, contents,
|
|
4
|
+
// band dividers, numbered headings to depth 3, lists, flat tables with mono
|
|
5
|
+
// numerals, callouts, key figures, code, figures, links, cross-references)
|
|
6
|
+
// so a single compile verifies the whole style.
|
|
7
|
+
//
|
|
8
|
+
// build-report report/example.typ
|
|
9
|
+
|
|
10
|
+
#import "casomo-template.typ": casomo-report, band, callout, keyfig, num
|
|
11
|
+
|
|
12
|
+
#show: casomo-report.with(
|
|
13
|
+
kicker: "Example Report — Template Reference",
|
|
14
|
+
title: "Lorem Ipsum Dolor Sit Amet",
|
|
15
|
+
subtitle: "A placeholder report exercising every feature of the Casomo house style.",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
= Executive summary
|
|
19
|
+
|
|
20
|
+
#lorem(60)
|
|
21
|
+
|
|
22
|
+
- *First finding* — #lorem(18)
|
|
23
|
+
- *Second finding* — #lorem(22)
|
|
24
|
+
- *Third finding* — #lorem(15)
|
|
25
|
+
|
|
26
|
+
#keyfig("£30,639", [Lorem ipsum dolor sit amet — the headline metric, lifted.])
|
|
27
|
+
|
|
28
|
+
#callout(label: "Note:")[#lorem(28)]
|
|
29
|
+
|
|
30
|
+
#band("Part One", note: "Band dividers group sections without disturbing numbering.")
|
|
31
|
+
|
|
32
|
+
= Background
|
|
33
|
+
|
|
34
|
+
#lorem(70)
|
|
35
|
+
|
|
36
|
+
== Prior work
|
|
37
|
+
|
|
38
|
+
#lorem(45) See @approach for how this shaped the approach, or the
|
|
39
|
+
#link("https://www.casomoltd.com")[Casomo website] for context.
|
|
40
|
+
|
|
41
|
+
== Constraints
|
|
42
|
+
|
|
43
|
+
+ #lorem(14)
|
|
44
|
+
+ #lorem(16)
|
|
45
|
+
+ #lorem(12)
|
|
46
|
+
|
|
47
|
+
= Approach <approach>
|
|
48
|
+
|
|
49
|
+
#lorem(50)
|
|
50
|
+
|
|
51
|
+
== Method detail
|
|
52
|
+
|
|
53
|
+
#lorem(30)
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
export function loremIpsum(dolor: string): string {
|
|
57
|
+
return `sit amet, ${dolor} adipiscing elit`;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#band("Part Two")
|
|
62
|
+
|
|
63
|
+
= Results
|
|
64
|
+
|
|
65
|
+
#lorem(40)
|
|
66
|
+
|
|
67
|
+
#figure(
|
|
68
|
+
table(
|
|
69
|
+
columns: (auto, 1fr, auto, auto),
|
|
70
|
+
align: (left, left, right, right),
|
|
71
|
+
table.header([Item], [Description], [Annual], [Monthly]),
|
|
72
|
+
[Alpha], [#lorem(8)], num[£29,970], num[£2,497.50],
|
|
73
|
+
[Beta], [#lorem(9)], num[£31,876], num[£2,656.33],
|
|
74
|
+
[Gamma], [#lorem(7)], num[£36,483], num[£3,040.25],
|
|
75
|
+
),
|
|
76
|
+
caption: [#lorem(16)],
|
|
77
|
+
) <results-table>
|
|
78
|
+
|
|
79
|
+
#lorem(35) The headline figures sit in @results-table.
|
|
80
|
+
|
|
81
|
+
#callout(label: "Warning:")[#lorem(20)]
|
|
82
|
+
|
|
83
|
+
= Recommendations
|
|
84
|
+
|
|
85
|
+
#lorem(30)
|
|
86
|
+
|
|
87
|
+
- #lorem(20)
|
|
88
|
+
- #lorem(17)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// footer.typ — the running page footer: brand mark, legal line, page X / N.
|
|
2
|
+
#import "tokens.typ": sans-font, company-name, company-registration
|
|
3
|
+
#import "mark.typ": casomo-mark
|
|
4
|
+
|
|
5
|
+
#let report-footer(year) = context {
|
|
6
|
+
set text(size: 9pt, font: sans-font)
|
|
7
|
+
grid(
|
|
8
|
+
columns: (20pt, 1fr, auto),
|
|
9
|
+
column-gutter: 8pt,
|
|
10
|
+
align: (horizon, horizon, horizon),
|
|
11
|
+
casomo-mark(),
|
|
12
|
+
[#sym.copyright #year #company-name, #company-registration],
|
|
13
|
+
[#counter(page).display() / #counter(page).final().first()],
|
|
14
|
+
)
|
|
15
|
+
}
|
package/report/mark.typ
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// mark.typ — the brand mark: the white arrow clipped in a black rounded
|
|
2
|
+
// square. One definition renders both placements (footer small, cover large).
|
|
3
|
+
// The SVG path resolves relative to this file, so both stay in report/.
|
|
4
|
+
#let casomo-mark(size: 20pt, arrow: 14pt, radius: 4pt) = box(
|
|
5
|
+
width: size,
|
|
6
|
+
height: size,
|
|
7
|
+
radius: radius,
|
|
8
|
+
fill: black,
|
|
9
|
+
clip: true,
|
|
10
|
+
align(center + horizon)[
|
|
11
|
+
#image("casomo-arrow-white.svg", width: arrow)
|
|
12
|
+
],
|
|
13
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// styles.typ — the document-wide type rules: base faces, paragraph rhythm,
|
|
2
|
+
// the heading scale (including how a band() divider renders), list spacing,
|
|
3
|
+
// link/ref colour, and figure captions. Table rules live in table.typ.
|
|
4
|
+
#import "tokens.typ": sans-font, mono-font
|
|
5
|
+
|
|
6
|
+
#let document-styles(body) = {
|
|
7
|
+
// Faces — tokens.typ owns the names; build-report.mjs preflights them.
|
|
8
|
+
set text(font: sans-font, size: 11.5pt)
|
|
9
|
+
set par(justify: true, leading: 0.95em)
|
|
10
|
+
set heading(numbering: "1.1")
|
|
11
|
+
show raw: set text(font: mono-font)
|
|
12
|
+
|
|
13
|
+
// Lists: a broader marker and a clear gap between items, so multi-line
|
|
14
|
+
// bullet items read as distinct points rather than blending into one block.
|
|
15
|
+
// The inter-item spacing is deliberately wider than the in-item leading.
|
|
16
|
+
set list(marker: text(size: 1.15em, fill: luma(55))[•], spacing: 1.15em)
|
|
17
|
+
set enum(spacing: 1.15em)
|
|
18
|
+
|
|
19
|
+
// Numbered headings get a size by level; an unnumbered level-1 heading is a
|
|
20
|
+
// band() group divider (components.typ), rendered as a smallcaps label with
|
|
21
|
+
// a rule across the page. Reports that never call band() only ever hit the
|
|
22
|
+
// numbered branch.
|
|
23
|
+
show heading: it => {
|
|
24
|
+
if it.numbering == none {
|
|
25
|
+
block(above: 1.6em, below: 0.7em, width: 100%, breakable: false)[
|
|
26
|
+
#set text(size: 14pt, weight: "bold", fill: luma(70))
|
|
27
|
+
#grid(
|
|
28
|
+
columns: (auto, 1fr),
|
|
29
|
+
align: (left + horizon, horizon),
|
|
30
|
+
column-gutter: 10pt,
|
|
31
|
+
smallcaps(it.body),
|
|
32
|
+
line(length: 100%, stroke: 0.6pt + luma(180)),
|
|
33
|
+
)
|
|
34
|
+
]
|
|
35
|
+
} else {
|
|
36
|
+
let size = if it.level == 1 { 16pt } else if it.level == 2 { 13pt } else { 11pt }
|
|
37
|
+
set text(size: size)
|
|
38
|
+
it
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
show outline.entry.where(level: 1): it => {
|
|
42
|
+
if it.element.numbering == none {
|
|
43
|
+
v(11pt, weak: true)
|
|
44
|
+
strong(smallcaps(it.element.body))
|
|
45
|
+
} else {
|
|
46
|
+
it
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
show link: set text(fill: rgb("#0000cc"))
|
|
50
|
+
show ref: set text(fill: rgb("#0000cc"))
|
|
51
|
+
show figure.caption: set text(size: 9.5pt, style: "italic", fill: luma(110))
|
|
52
|
+
show figure.caption: set par(justify: true)
|
|
53
|
+
show figure.caption: set align(left)
|
|
54
|
+
|
|
55
|
+
body
|
|
56
|
+
}
|
package/report/table.typ
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// table.typ — flat table rules: a strong rule under the header row, hairlines
|
|
2
|
+
// between body rows, no vertical strokes, no outer box, no zebra, no fills.
|
|
3
|
+
// The first row is styled as the header, so open every table with
|
|
4
|
+
// table.header(). Right-align numeric columns via the table's `align` and set
|
|
5
|
+
// the cells with num().
|
|
6
|
+
#import "tokens.typ": strong-rule, hairline
|
|
7
|
+
|
|
8
|
+
#let table-styles(body) = {
|
|
9
|
+
set table(
|
|
10
|
+
stroke: (x, y) => if y == 0 { (bottom: strong-rule) } else { (bottom: hairline) },
|
|
11
|
+
inset: (x: 10pt, y: 7pt),
|
|
12
|
+
)
|
|
13
|
+
show table.cell.where(y: 0): set text(weight: 600, size: 10pt)
|
|
14
|
+
// Body-par justification stretches badly in narrow cells — tables rag right.
|
|
15
|
+
show table.cell: set par(justify: false)
|
|
16
|
+
body
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// tokens.typ — the shared constants every report component reads.
|
|
2
|
+
|
|
3
|
+
// Faces — checked by bin/build-report.mjs's font preflight; keep in sync.
|
|
4
|
+
#let sans-font = "IBM Plex Sans"
|
|
5
|
+
#let mono-font = "IBM Plex Mono"
|
|
6
|
+
|
|
7
|
+
// The whole style runs on three separation weights: a strong rule (table
|
|
8
|
+
// headers), a hairline (row separators), and an intent bar (callouts / key
|
|
9
|
+
// figures). Anything heavier — boxes, fills, zebra striping — is deliberately
|
|
10
|
+
// absent: the house minimal direction, one separation device per element.
|
|
11
|
+
#let strong-rule = 1pt + black
|
|
12
|
+
#let hairline = 0.5pt + luma(200)
|
|
13
|
+
#let intent-bar = 2.5pt + black
|
|
14
|
+
|
|
15
|
+
// The legal identity, printed by both the footer and the cover.
|
|
16
|
+
#let company-name = "Casomo Ltd"
|
|
17
|
+
#let company-registration = "Registered in the UK, 15030496"
|