@inherent.design/brand 0.3.1 → 0.4.1

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.
Files changed (49) hide show
  1. package/LICENSE +202 -0
  2. package/fonts/dist/en/279406bfba1f0020d1fce5ec6fd2a735.woff2 +0 -0
  3. package/fonts/dist/en/41252d539ae6f30ba7b2f91398c8f31d.woff2 +0 -0
  4. package/fonts/dist/en/5228f68218619155166c83a4ee524afe.woff2 +0 -0
  5. package/fonts/dist/en/98773bdef5c210140ab004df6c5a8331.woff2 +0 -0
  6. package/fonts/dist/en/c5b99cd7f72716cad8b79f5452bc2f37.woff2 +0 -0
  7. package/fonts/dist/en/d88a44b1521929f0cedd8f54f815b90e.woff2 +0 -0
  8. package/fonts/dist/en/ffb5f092abefd125a087b64efb40c9b0.woff2 +0 -0
  9. package/fonts/dist/en/index.css +9 -9
  10. package/fonts/dist/hi/index.css +3 -3
  11. package/fonts/dist/zh/16144bae757cf69e1303657f4666167b.woff2 +0 -0
  12. package/fonts/dist/zh/da5d9643f9cd74d319fb3c3fcf47b73c.woff2 +0 -0
  13. package/fonts/dist/zh/e00f6f95a6a747bfbd32af5df1fe1f1c.woff2 +0 -0
  14. package/fonts/dist/zh/index.css +4 -4
  15. package/package.json +8 -4
  16. package/typst/inherent.typ +22 -0
  17. package/typst/lib/code.typ +24 -0
  18. package/typst/lib/colors.typ +22 -0
  19. package/typst/lib/components-content.typ +161 -0
  20. package/typst/lib/components-document.typ +275 -0
  21. package/typst/lib/components-layout.typ +104 -0
  22. package/typst/lib/components.typ +55 -0
  23. package/typst/lib/layout.typ +35 -0
  24. package/typst/lib/tables.typ +19 -0
  25. package/typst/lib/tokens.typ +54 -0
  26. package/typst/lib/typography.typ +58 -0
  27. package/typst/lib/utils.typ +55 -0
  28. package/typst/prelude.typ +6 -0
  29. package/typst/typst.toml +7 -0
  30. package/fonts/dist/en/1c6a77e998e54efef01b7caf56bc42a3.woff2 +0 -0
  31. package/fonts/dist/en/4db369d2bd069bba9eecb25938906a84.woff2 +0 -0
  32. package/fonts/dist/en/50be83d8450ee117be0e081216cb6c61.woff2 +0 -0
  33. package/fonts/dist/en/6543456df4504eb8eec3cb90d97fd006.woff2 +0 -0
  34. package/fonts/dist/en/91424d2ed0640f376b7ab256afefe512.woff2 +0 -0
  35. package/fonts/dist/en/bd7f480f2bcf174edd41ec205c345a62.woff2 +0 -0
  36. package/fonts/dist/en/e5fe10dc949fa944db6496bcd735f594.woff2 +0 -0
  37. package/fonts/dist/zh/67cdd35800ba1cfd8e98b3943bd77834.woff2 +0 -0
  38. package/fonts/dist/zh/6ee817625aeeadd71f081cc671e0c26e.woff2 +0 -0
  39. package/fonts/dist/zh/7ff3a53513ebbbc16b0f1baf70713e17.woff2 +0 -0
  40. /package/{license → fonts/licenses}/LICENSE-Charter.txt +0 -0
  41. /package/{license → fonts/licenses}/LICENSE-CommitMono.txt +0 -0
  42. /package/{license → fonts/licenses}/LICENSE-CormorantGaramond.txt +0 -0
  43. /package/{license → fonts/licenses}/LICENSE-Inter.txt +0 -0
  44. /package/{license → fonts/licenses}/LICENSE-LXGWWenKai.txt +0 -0
  45. /package/{license → fonts/licenses}/LICENSE-NotoSansDevanagari.txt +0 -0
  46. /package/{license → fonts/licenses}/LICENSE-NotoSansMono.txt +0 -0
  47. /package/{license → fonts/licenses}/LICENSE-NotoSansSC.txt +0 -0
  48. /package/{license → fonts/licenses}/LICENSE-SarasaMonoSC.txt +0 -0
  49. /package/{license → fonts/licenses}/LICENSE-TiroDevanagari.txt +0 -0
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@inherent.design/brand",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "Design tokens, web fonts, and Starwind components for inherent.design projects.",
6
- "license": "OFL-1.1",
6
+ "license": "Apache-2.0",
7
7
  "author": "inherent.design",
8
8
  "exports": {
9
9
  "./fonts/vars": {
@@ -33,14 +33,18 @@
33
33
  "./components/button": "./components/button/index.ts",
34
34
  "./components/image": "./components/image/index.ts",
35
35
  "./components/prose": "./components/prose/index.ts",
36
- "./components/separator": "./components/separator/index.ts"
36
+ "./components/separator": "./components/separator/index.ts",
37
+ "./typst": "./typst/inherent.typ",
38
+ "./typst/prelude": "./typst/prelude.typ"
37
39
  },
38
40
  "files": [
39
41
  "fonts/vars.css",
40
42
  "fonts/dist/",
43
+ "fonts/src/typst/",
44
+ "fonts/licenses/LICENSE*",
41
45
  "styles/",
42
46
  "components/",
43
- "license/LICENSE*"
47
+ "typst/"
44
48
  ],
45
49
  "scripts": {
46
50
  "fonts:download": "node fonts/scripts/download.js",
@@ -0,0 +1,22 @@
1
+ // brand/typst/inherent.typ: Base template for inherent.design documents
2
+ //
3
+ // Pure Typst entry point. Templates use: #show: base-template.with(title: ..., date: ...)
4
+
5
+ #import "lib/colors.typ": *
6
+ #import "lib/tokens.typ": *
7
+ #import "lib/typography.typ": apply-typography
8
+ #import "lib/layout.typ": apply-layout
9
+ #import "lib/tables.typ": apply-tables
10
+ #import "lib/code.typ": apply-code
11
+
12
+ #let base-template(
13
+ title: "",
14
+ date: none,
15
+ body,
16
+ ) = {
17
+ show: apply-typography
18
+ show: apply-layout.with(title: title, date: date)
19
+ show: apply-tables
20
+ show: apply-code
21
+ body
22
+ }
@@ -0,0 +1,24 @@
1
+ // Code/raw block styling
2
+
3
+ #import "colors.typ": accent, text-gray, bg-subtle
4
+ #import "typography.typ": font-code
5
+ #import "tokens.typ": border-thin, radius-md
6
+
7
+ #let apply-code(doc) = {
8
+ show raw: set text(font: font-code, size: 0.9em)
9
+
10
+ show raw.where(block: true): it => {
11
+ block(
12
+ width: 100%,
13
+ fill: bg-subtle,
14
+ stroke: border-thin + text-gray,
15
+ inset: 10pt,
16
+ radius: radius-md,
17
+ it,
18
+ )
19
+ }
20
+
21
+ show raw.where(block: false): set text(fill: accent)
22
+
23
+ doc
24
+ }
@@ -0,0 +1,22 @@
1
+ // Brand color tokens for inherent.design
2
+ // All document modules import from this file.
3
+
4
+ #let text-dark = rgb("#1a1a1a") // CSS: oklch(21.78% 0 0)
5
+ #let text-gray = rgb("#6b7280") // CSS: oklch(55.10% 0.0234 264.4)
6
+ #let accent = rgb("#2563eb") // CSS: oklch(54.61% 0.2152 262.9)
7
+
8
+ #let accent-light = accent.lighten(80%)
9
+ #let accent-dark = accent.darken(20%)
10
+
11
+ // Semantic tokens
12
+ #let success = rgb("#2E7D32")
13
+ #let warning = rgb("#F57F17")
14
+ #let error = rgb("#C62828")
15
+ #let info = rgb("#1565C0")
16
+
17
+ // Background variants
18
+ #let bg-subtle = rgb("#F8F9FA")
19
+ #let bg-card = rgb("#FFFFFF")
20
+
21
+ // Border
22
+ #let border = rgb("#DEE2E6")
@@ -0,0 +1,161 @@
1
+ // Content components: metric-card, service-card, competency-block, callout-box, pull-quote
2
+
3
+ #import "tokens.typ": sp-xs, sp-sm, sp-md, sp-lg, sp-xl, ts-h1, ts-h2, ts-h3, ts-h4, ts-body, ts-caption, border-thin, radius-md
4
+ #import "colors.typ": accent, text-dark, text-gray, bg-subtle, success, warning, error, info
5
+
6
+ #let metric-card(
7
+ value,
8
+ label,
9
+ description: none,
10
+ fill: accent.lighten(92%),
11
+ value-color: accent,
12
+ ) = {
13
+ block(
14
+ width: 100%,
15
+ fill: fill,
16
+ radius: radius-md,
17
+ inset: sp-md,
18
+ )[
19
+ #text(size: ts-h1, weight: "bold", fill: value-color)[#value]
20
+ #v(sp-xs)
21
+ #text(size: ts-h4, weight: "semibold", fill: text-dark)[#label]
22
+ #if description != none {
23
+ v(sp-xs)
24
+ text(size: ts-caption, fill: text-gray)[#description]
25
+ }
26
+ ]
27
+ }
28
+
29
+ #let service-card(
30
+ name,
31
+ description,
32
+ deliverables: (),
33
+ accent-color: accent,
34
+ ) = {
35
+ block(
36
+ width: 100%,
37
+ stroke: (left: 3pt + accent-color, rest: border-thin + text-gray.lighten(70%)),
38
+ radius: (right: radius-md),
39
+ inset: sp-md,
40
+ below: sp-md,
41
+ )[
42
+ #text(size: ts-h3, weight: "bold", fill: accent-color)[#name]
43
+ #v(sp-sm)
44
+ #text(fill: text-dark)[#description]
45
+ #if deliverables.len() > 0 {
46
+ v(sp-sm)
47
+ text(size: ts-caption, weight: "bold", fill: text-gray)[Deliverables:]
48
+ for item in deliverables {
49
+ v(sp-xs)
50
+ grid(
51
+ columns: (12pt, 1fr),
52
+ column-gutter: sp-xs,
53
+ text(fill: accent-color)[--],
54
+ text(size: ts-caption)[#item],
55
+ )
56
+ }
57
+ }
58
+ ]
59
+ }
60
+
61
+ #let competency-block(
62
+ title,
63
+ items,
64
+ accent-color: accent,
65
+ columns: 2,
66
+ ) = {
67
+ block(below: sp-lg)[
68
+ #text(size: ts-h3, weight: "bold", fill: accent-color)[#title]
69
+ #v(sp-sm)
70
+ #grid(
71
+ columns: (1fr,) * columns,
72
+ column-gutter: sp-lg,
73
+ row-gutter: sp-sm,
74
+ ..items.map(item => {
75
+ grid(
76
+ columns: (8pt, 1fr),
77
+ column-gutter: sp-xs,
78
+ align(horizon, circle(radius: 3pt, fill: accent-color)),
79
+ text(size: ts-body, fill: text-dark)[#item],
80
+ )
81
+ }),
82
+ )
83
+ ]
84
+ }
85
+
86
+ #let callout-box(
87
+ body,
88
+ title: none,
89
+ variant: "info",
90
+ icon: none,
91
+ ) = {
92
+ let (fill, stroke-color, title-color) = if variant == "info" {
93
+ (info.lighten(90%), info, info)
94
+ } else if variant == "warning" {
95
+ (warning.lighten(88%), warning, warning)
96
+ } else if variant == "success" {
97
+ (success.lighten(90%), success, success)
98
+ } else if variant == "error" {
99
+ (error.lighten(90%), error, error)
100
+ } else {
101
+ (bg-subtle, text-gray, text-dark)
102
+ }
103
+
104
+ block(
105
+ width: 100%,
106
+ fill: fill,
107
+ stroke: (left: 3pt + stroke-color, rest: none),
108
+ radius: (right: radius-md),
109
+ inset: sp-md,
110
+ above: sp-md,
111
+ below: sp-md,
112
+ )[
113
+ #if title != none {
114
+ text(weight: "bold", fill: title-color, size: ts-body)[
115
+ #if icon != none [#icon ] #title
116
+ ]
117
+ v(sp-xs)
118
+ }
119
+ #body
120
+ ]
121
+ }
122
+
123
+ // Preset variants via .with()
124
+ #let info-box = callout-box.with(variant: "info")
125
+ #let warning-box = callout-box.with(variant: "warning")
126
+ #let success-box = callout-box.with(variant: "success")
127
+ #let error-box = callout-box.with(variant: "error")
128
+ #let note-box = callout-box.with(variant: "note", title: "Note")
129
+ #let tip-box = callout-box.with(variant: "info", title: "Tip")
130
+ #let important-box = callout-box.with(variant: "warning", title: "Important")
131
+
132
+ #let pull-quote(
133
+ body,
134
+ attribution: none,
135
+ accent-color: accent,
136
+ ) = {
137
+ block(
138
+ width: 100%,
139
+ inset: (left: sp-xl, right: sp-lg, y: sp-lg),
140
+ above: sp-lg,
141
+ below: sp-lg,
142
+ )[
143
+ #block(
144
+ stroke: (left: 3pt + accent-color),
145
+ inset: (left: sp-md),
146
+ )[
147
+ #text(
148
+ size: ts-h2,
149
+ style: "italic",
150
+ fill: text-dark,
151
+ weight: "regular",
152
+ )[#body]
153
+ ]
154
+ #if attribution != none {
155
+ v(sp-sm)
156
+ align(right)[
157
+ #text(size: ts-body, fill: text-gray)[--- #attribution]
158
+ ]
159
+ }
160
+ ]
161
+ }
@@ -0,0 +1,275 @@
1
+ // Document components: cover-page, contact-strip, registration-block, invoice-header, line-items-table
2
+
3
+ #import "tokens.typ": sp-xs, sp-sm, sp-md, sp-lg, sp-xl, sp-2xl, sp-3xl, ts-display, ts-h1, ts-h2, ts-body, ts-small, ts-caption, border-thin, border-medium, border-thick, radius-md
4
+ #import "colors.typ": accent, text-dark, text-gray, bg-subtle
5
+ #import "typography.typ": font-display
6
+
7
+ #let cover-page(
8
+ title: [],
9
+ subtitle: none,
10
+ date: none,
11
+ company: none,
12
+ fill: white,
13
+ accent-color: accent,
14
+ ) = {
15
+ page(
16
+ margin: (x: 1.5in, top: 2.5in, bottom: 1.5in),
17
+ header: none,
18
+ footer: none,
19
+ fill: fill,
20
+ )[
21
+ // Accent bar at top
22
+ #place(top + left, dx: -1.5in, dy: -2.5in)[
23
+ #rect(width: 100% + 3in, height: 6pt, fill: accent-color)
24
+ ]
25
+
26
+ // Title block
27
+ #text(
28
+ font: font-display,
29
+ size: ts-display + 8pt,
30
+ weight: "bold",
31
+ fill: text-dark,
32
+ )[#title]
33
+
34
+ #if subtitle != none {
35
+ v(sp-md)
36
+ text(size: ts-h2, fill: text-gray)[#subtitle]
37
+ }
38
+
39
+ #v(sp-xl)
40
+ #line(length: 100pt, stroke: border-thick + accent-color)
41
+
42
+ #if date != none {
43
+ v(sp-xl)
44
+ text(size: ts-body, fill: text-gray)[#date]
45
+ }
46
+
47
+ // Company name at bottom
48
+ #if company != none {
49
+ align(bottom + left)[
50
+ #text(size: 12pt, fill: text-gray)[#company]
51
+ ]
52
+ }
53
+ ]
54
+ }
55
+
56
+ #let contact-strip(
57
+ name: none,
58
+ title: none,
59
+ email: none,
60
+ phone: none,
61
+ address: none,
62
+ separator: [ | ],
63
+ ) = {
64
+ block(
65
+ width: 100%,
66
+ inset: (y: sp-sm),
67
+ )[
68
+ #set text(size: ts-small, fill: text-gray)
69
+ #{
70
+ let parts = ()
71
+ if name != none { parts.push(text(weight: "bold", fill: text-dark)[#name]) }
72
+ if title != none { parts.push(title) }
73
+ if email != none { parts.push(link("mailto:" + email)[#email]) }
74
+ if phone != none { parts.push(phone) }
75
+ if address != none { parts.push(address) }
76
+ parts.join(separator)
77
+ }
78
+ ]
79
+ }
80
+
81
+ #let registration-block(
82
+ uei: none,
83
+ cage: none,
84
+ sam: none,
85
+ naics-primary: none,
86
+ naics-secondary: none,
87
+ certifications: none,
88
+ ein-last4: none,
89
+ ) = {
90
+ block(
91
+ width: 100%,
92
+ fill: bg-subtle,
93
+ radius: radius-md,
94
+ inset: sp-md,
95
+ below: sp-md,
96
+ )[
97
+ #text(size: 11pt, weight: "bold", fill: accent)[Federal Registration]
98
+ #v(sp-sm)
99
+ #grid(
100
+ columns: (auto, 1fr),
101
+ row-gutter: sp-sm,
102
+ column-gutter: sp-md,
103
+ ..{
104
+ let pairs = ()
105
+ if uei != none { pairs.push((strong[UEI:], uei)) }
106
+ if cage != none { pairs.push((strong[CAGE Code:], cage)) }
107
+ if sam != none { pairs.push((strong[SAM.gov:], sam)) }
108
+ if naics-primary != none { pairs.push((strong[NAICS (Primary):], naics-primary)) }
109
+ if naics-secondary != none { pairs.push((strong[NAICS (Secondary):], naics-secondary)) }
110
+ if certifications != none { pairs.push((strong[Certifications:], certifications)) }
111
+ if ein-last4 != none { pairs.push((strong[EIN (last 4):], ein-last4)) }
112
+ pairs.flatten()
113
+ },
114
+ )
115
+ ]
116
+ }
117
+
118
+ #let invoice-header(
119
+ company: none,
120
+ client: none,
121
+ number: none,
122
+ date: none,
123
+ due: none,
124
+ po-number: none,
125
+ ) = {
126
+ grid(
127
+ columns: (1fr, auto),
128
+ column-gutter: sp-xl,
129
+
130
+ // Left: company identity
131
+ {
132
+ if company != none {
133
+ text(size: 14pt, weight: "bold", fill: accent)[#company.at("name", default: "")]
134
+ v(sp-xs)
135
+ if "address" in company {
136
+ text(size: ts-small, fill: text-gray)[#company.address]
137
+ }
138
+ if "email" in company {
139
+ v(sp-xs)
140
+ text(size: ts-small, fill: text-gray)[#company.email]
141
+ }
142
+ if "phone" in company {
143
+ v(sp-xs)
144
+ text(size: ts-small, fill: text-gray)[#company.phone]
145
+ }
146
+ }
147
+ },
148
+
149
+ // Right: invoice metadata
150
+ {
151
+ text(size: ts-h1, weight: "bold", fill: accent)[INVOICE]
152
+ v(sp-sm)
153
+ grid(
154
+ columns: (auto, auto),
155
+ row-gutter: sp-xs,
156
+ column-gutter: sp-md,
157
+ text(size: ts-small, weight: "bold")[Invoice \#:], text(size: ts-small)[#number],
158
+ text(size: ts-small, weight: "bold")[Date:], text(size: ts-small)[#date],
159
+ text(size: ts-small, weight: "bold")[Due:], text(size: ts-small)[#due],
160
+ ..if po-number != none {
161
+ (text(size: ts-small, weight: "bold")[PO \#:], text(size: ts-small)[#po-number])
162
+ } else { () },
163
+ )
164
+ },
165
+ )
166
+
167
+ v(sp-lg)
168
+
169
+ // Client: "Bill To" section
170
+ if client != none {
171
+ text(size: ts-small, weight: "bold", fill: text-gray)[BILL TO]
172
+ v(sp-xs)
173
+ text(size: ts-body)[#client.at("name", default: "")]
174
+ if "address" in client {
175
+ v(sp-xs)
176
+ text(size: ts-small, fill: text-gray)[#client.address]
177
+ }
178
+ }
179
+
180
+ v(sp-lg)
181
+ line(length: 100%, stroke: border-thin + text-gray.lighten(40%))
182
+ v(sp-md)
183
+ }
184
+
185
+ #let line-items-table(
186
+ items,
187
+ tax-rate: none,
188
+ tax-label: "Tax",
189
+ currency: "$",
190
+ show-quantity: true,
191
+ ) = {
192
+ // Calculate totals
193
+ let subtotal = items.fold(0, (acc, item) => acc + item.at("amount", default: 0))
194
+ let tax-amount = if tax-rate != none { subtotal * tax-rate } else { 0 }
195
+ let total = subtotal + tax-amount
196
+
197
+ // Format currency
198
+ let fmt(n) = {
199
+ currency + str(calc.round(n, digits: 2))
200
+ }
201
+
202
+ // Header + items
203
+ table(
204
+ columns: if show-quantity {
205
+ (1fr, auto, auto, auto)
206
+ } else {
207
+ (1fr, auto)
208
+ },
209
+ inset: sp-sm,
210
+ stroke: (x, y) => if y == 0 {
211
+ (bottom: border-medium + accent)
212
+ } else {
213
+ (bottom: border-thin + text-gray.lighten(70%))
214
+ },
215
+
216
+ // Header row
217
+ ..if show-quantity {
218
+ (
219
+ table.cell(text(weight: "bold", size: ts-small)[Description]),
220
+ table.cell(text(weight: "bold", size: ts-small)[Qty]),
221
+ table.cell(text(weight: "bold", size: ts-small)[Rate]),
222
+ table.cell(text(weight: "bold", size: ts-small, fill: text-dark)[Amount]),
223
+ )
224
+ } else {
225
+ (
226
+ table.cell(text(weight: "bold", size: ts-small)[Description]),
227
+ table.cell(text(weight: "bold", size: ts-small, fill: text-dark)[Amount]),
228
+ )
229
+ },
230
+
231
+ // Item rows
232
+ ..items.map(item => {
233
+ if show-quantity {
234
+ (
235
+ text(size: ts-body)[#item.description],
236
+ align(right, text(size: ts-small)[#item.at("quantity", default: 1)]),
237
+ align(right, text(size: ts-small)[#fmt(item.at("rate", default: 0))]),
238
+ align(right, text(size: ts-body, weight: "medium")[#fmt(item.amount)]),
239
+ )
240
+ } else {
241
+ (
242
+ text(size: ts-body)[#item.description],
243
+ align(right, text(size: ts-body, weight: "medium")[#fmt(item.amount)]),
244
+ )
245
+ }
246
+ }).flatten(),
247
+ )
248
+
249
+ // Totals section
250
+ v(sp-sm)
251
+ align(right)[
252
+ #grid(
253
+ columns: (auto, 6em),
254
+ row-gutter: sp-xs,
255
+ column-gutter: sp-lg,
256
+ text(size: ts-small, fill: text-gray)[Subtotal:],
257
+ align(right, text(size: ts-body)[#fmt(subtotal)]),
258
+ ..if tax-rate != none {
259
+ (
260
+ text(size: ts-small, fill: text-gray)[#tax-label (#str(calc.round(tax-rate * 100, digits: 2))%):],
261
+ align(right, text(size: ts-body)[#fmt(tax-amount)]),
262
+ )
263
+ } else { () },
264
+ )
265
+ #v(sp-xs)
266
+ #line(length: 10em, stroke: border-thin + text-gray.lighten(40%))
267
+ #v(sp-xs)
268
+ #grid(
269
+ columns: (auto, 6em),
270
+ column-gutter: sp-lg,
271
+ text(size: 12pt, weight: "bold")[Total:],
272
+ align(right, text(size: 12pt, weight: "bold", fill: accent)[#fmt(total)]),
273
+ )
274
+ ]
275
+ }
@@ -0,0 +1,104 @@
1
+ // Layout components: hero-section, two-column, sidebar-layout, card, card-grid
2
+
3
+ #import "tokens.typ": sp-xs, sp-sm, sp-md, sp-lg, sp-xl, sp-2xl, sp-3xl, ts-display, ts-body, border-thin, border-thick, radius-md
4
+ #import "colors.typ": accent, text-dark, text-gray, bg-card, border
5
+ #import "typography.typ": font-display
6
+
7
+ #let hero-section(
8
+ title: [],
9
+ subtitle: none,
10
+ accent-line: true,
11
+ fill: accent.lighten(92%),
12
+ text-fill: text-dark,
13
+ ) = {
14
+ block(
15
+ width: 100%,
16
+ fill: fill,
17
+ inset: (x: sp-2xl, top: sp-3xl, bottom: sp-2xl),
18
+ below: sp-xl,
19
+ )[
20
+ #text(
21
+ font: font-display,
22
+ size: ts-display,
23
+ fill: text-fill,
24
+ weight: "bold",
25
+ )[#title]
26
+ #if subtitle != none {
27
+ v(sp-sm)
28
+ text(size: ts-body + 2pt, fill: text-gray)[#subtitle]
29
+ }
30
+ #if accent-line {
31
+ v(sp-md)
32
+ line(length: 80pt, stroke: border-thick + accent)
33
+ }
34
+ ]
35
+ }
36
+
37
+ #let two-column(
38
+ left,
39
+ right,
40
+ ratio: (2fr, 1fr),
41
+ gutter: sp-xl,
42
+ ) = {
43
+ grid(
44
+ columns: ratio,
45
+ column-gutter: gutter,
46
+ left,
47
+ right,
48
+ )
49
+ }
50
+
51
+ #let sidebar-layout(
52
+ main,
53
+ sidebar,
54
+ sidebar-width: 2.2in,
55
+ gutter: sp-xl,
56
+ sidebar-position: "right",
57
+ ) = {
58
+ let cols = if sidebar-position == "left" {
59
+ (sidebar-width, 1fr)
60
+ } else {
61
+ (1fr, sidebar-width)
62
+ }
63
+ let cells = if sidebar-position == "left" {
64
+ (sidebar, main)
65
+ } else {
66
+ (main, sidebar)
67
+ }
68
+ grid(
69
+ columns: cols,
70
+ column-gutter: gutter,
71
+ ..cells,
72
+ )
73
+ }
74
+
75
+ #let card(body, title: none, fill: bg-card, stroke: border-thin + border) = {
76
+ block(
77
+ width: 100%,
78
+ fill: fill,
79
+ stroke: stroke,
80
+ radius: radius-md,
81
+ inset: sp-md,
82
+ above: sp-sm,
83
+ below: sp-sm,
84
+ )[
85
+ #if title != none {
86
+ text(weight: "bold", size: 11pt)[#title]
87
+ v(sp-xs)
88
+ }
89
+ #body
90
+ ]
91
+ }
92
+
93
+ #let card-grid(
94
+ cards,
95
+ columns: 3,
96
+ gutter: sp-md,
97
+ ) = {
98
+ grid(
99
+ columns: (1fr,) * columns,
100
+ column-gutter: gutter,
101
+ row-gutter: gutter,
102
+ ..cards,
103
+ )
104
+ }
@@ -0,0 +1,55 @@
1
+ // Title block, info-table, signature-block, separator
2
+
3
+ #import "colors.typ": accent, text-gray
4
+ #import "typography.typ": font-display
5
+ #import "tokens.typ": ts-h1, ts-body, sp-xs, sp-sm, sp-md, border-thin
6
+
7
+ #let title-block(title: [], date: none) = {
8
+ align(left)[
9
+ #text(font: font-display, size: ts-h1, fill: accent, weight: "bold")[#title]
10
+ #v(sp-sm)
11
+ #if date != none [
12
+ #text(size: ts-body, fill: text-gray)[#date]
13
+ #v(sp-xs)
14
+ ]
15
+ ]
16
+ v(sp-sm)
17
+ pdf.artifact(line(length: 100%, stroke: border-thin + text-gray.lighten(40%)))
18
+ v(sp-md)
19
+ }
20
+
21
+ #let info-table(..pairs) = {
22
+ grid(
23
+ columns: (auto, 1fr),
24
+ row-gutter: sp-sm,
25
+ column-gutter: sp-md,
26
+ ..pairs.pos().map(((label, value)) => {
27
+ (strong(label), value)
28
+ }).flatten()
29
+ )
30
+ }
31
+
32
+ #let signature-block(party-name, include-title: true) = {
33
+ v(1em)
34
+ strong(party-name)
35
+ v(2em)
36
+ grid(
37
+ columns: (1fr, auto),
38
+ column-gutter: 2em,
39
+ [Name: #box(width: 12em, stroke: (bottom: border-thin))],
40
+ [Date: #box(width: 6em, stroke: (bottom: border-thin))],
41
+ )
42
+ if include-title {
43
+ v(1em)
44
+ [Title: #box(width: 12em, stroke: (bottom: border-thin))]
45
+ }
46
+ }
47
+
48
+ #let separator() = {
49
+ pdf.artifact(line(length: 100%, stroke: border-thin + text-gray.lighten(40%)))
50
+ }
51
+
52
+ // Re-export component modules (barrel file)
53
+ #import "components-layout.typ": *
54
+ #import "components-content.typ": *
55
+ #import "components-document.typ": *