@airmark/quiver 0.28.0 → 0.29.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 +3 -2
- package/package.json +3 -3
- package/quills/daf4392/0.1.0/Quill.yaml +2 -2
- package/quills/usaf_memo/0.2.0/Quill.yaml +14 -11
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +37 -15
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +5 -6
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +5 -3
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +87 -35
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +34 -1
- package/quills/usaf_memo/0.3.0/Quill.yaml +321 -0
- package/quills/usaf_memo/0.3.0/assets/dod_seal.png +0 -0
- package/quills/usaf_memo/0.3.0/assets/dow_seal.png +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/.gitignore +7 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/LiberationMono/LICENSE +102 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +364 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +78 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +242 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +170 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +57 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +27 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +462 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +419 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -0
- package/quills/usaf_memo/0.3.0/plate.typ +120 -0
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ and aligned to the current Quillmark spec.
|
|
|
9
9
|
|
|
10
10
|
| Quill | Version | Description |
|
|
11
11
|
|--------------|---------|-----------------------------------------------------------------------------------|
|
|
12
|
-
| `usaf_memo` | 0.
|
|
12
|
+
| `usaf_memo` | 0.3.0 | USAF / DAF Official Memorandum (AFH 33-337) |
|
|
13
|
+
| `usaf_memo` | 0.2.0 | Previous release, retained so documents pinned to `@0.2` keep resolving |
|
|
13
14
|
| `af4141` | 0.1.0 | AF Form 4141 — Individual's Record of Duties and Experience (Ground Environment) |
|
|
14
15
|
| `daf1206` | 0.1.0 | DAF Form 1206 — Nomination for Award |
|
|
15
16
|
| `daf4392` | 0.1.0 | DAF Form 4392 — Pre-Departure Safety Briefing (Page 2) |
|
|
@@ -46,7 +47,7 @@ const quiver = await fromDir(root);
|
|
|
46
47
|
const engine = new Engine();
|
|
47
48
|
|
|
48
49
|
const doc = Document.fromMarkdown(`~~~
|
|
49
|
-
$quill: usaf_memo@0.
|
|
50
|
+
$quill: usaf_memo@0.3
|
|
50
51
|
$kind: main
|
|
51
52
|
memo_for: ["ORG/SYMBOL"]
|
|
52
53
|
memo_from: ["ORG/SYMBOL", "Organization Name", "123 Street Ave", "City ST 12345-6789"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@airmark/quiver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Airmark Quiver — Air Force and DAF document Quills for Quillmark.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@quillmark/quiver": "0.21.0",
|
|
20
|
-
"@quillmark/wasm": "^0.
|
|
21
|
-
"quillkit": "0.2.
|
|
20
|
+
"@quillmark/wasm": "^0.104.0",
|
|
21
|
+
"quillkit": "0.2.2"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "quillkit test",
|
|
@@ -12,8 +12,8 @@ main:
|
|
|
12
12
|
enabled: false
|
|
13
13
|
fields:
|
|
14
14
|
transportation_mode:
|
|
15
|
-
type:
|
|
16
|
-
|
|
15
|
+
type: enum
|
|
16
|
+
values: [pmv, airplane, bus, train, motorcycle, other]
|
|
17
17
|
default: pmv
|
|
18
18
|
description: "Matches the check box."
|
|
19
19
|
|
|
@@ -80,8 +80,8 @@ main:
|
|
|
80
80
|
description: The full organization name of your unit.
|
|
81
81
|
|
|
82
82
|
letterhead_seal:
|
|
83
|
-
type:
|
|
84
|
-
|
|
83
|
+
type: enum
|
|
84
|
+
values:
|
|
85
85
|
- dow
|
|
86
86
|
- dod
|
|
87
87
|
default: dow
|
|
@@ -108,8 +108,8 @@ main:
|
|
|
108
108
|
description: "Organizational motto at the bottom of the page. Supports Markdown emphasis (e.g. *italics*, **bold**)."
|
|
109
109
|
|
|
110
110
|
classification:
|
|
111
|
-
type:
|
|
112
|
-
|
|
111
|
+
type: enum
|
|
112
|
+
values:
|
|
113
113
|
- ""
|
|
114
114
|
- UNCLASSIFIED
|
|
115
115
|
- CUI
|
|
@@ -210,8 +210,8 @@ main:
|
|
|
210
210
|
description: List attachments in the order they are mentioned in the memo. Briefly describe each; do not use 'as stated' or abbreviations.
|
|
211
211
|
|
|
212
212
|
memo_style:
|
|
213
|
-
type:
|
|
214
|
-
|
|
213
|
+
type: enum
|
|
214
|
+
values:
|
|
215
215
|
- usaf
|
|
216
216
|
- daf
|
|
217
217
|
default: usaf
|
|
@@ -269,8 +269,8 @@ card_kinds:
|
|
|
269
269
|
compact: true
|
|
270
270
|
description: "Line 1: Name in UPPERCASE as signed, grade, and service. Line 2: Duty title."
|
|
271
271
|
format:
|
|
272
|
-
type:
|
|
273
|
-
|
|
272
|
+
type: enum
|
|
273
|
+
values:
|
|
274
274
|
- standard
|
|
275
275
|
- informal
|
|
276
276
|
- separate_page
|
|
@@ -280,17 +280,20 @@ card_kinds:
|
|
|
280
280
|
compact: true
|
|
281
281
|
description: "'standard' (formal, same page), 'informal' (less formal routing), or 'separate_page' (begins on a new page)."
|
|
282
282
|
action:
|
|
283
|
-
type:
|
|
284
|
-
|
|
283
|
+
type: enum
|
|
284
|
+
values:
|
|
285
285
|
- ""
|
|
286
286
|
- undecided
|
|
287
287
|
- approve
|
|
288
288
|
- disapprove
|
|
289
|
+
- undecided_concur
|
|
290
|
+
- concur
|
|
291
|
+
- nonconcur
|
|
289
292
|
default: ""
|
|
290
293
|
ui:
|
|
291
294
|
group: additional
|
|
292
295
|
compact: true
|
|
293
|
-
description: "
|
|
296
|
+
description: "Decision taken by the endorser, shown as an either/or line with the choice circled. Leave blank to hide the line entirely. Approval authority: 'approve' or 'disapprove' (use 'undecided' to print Approve/Disapprove for the endorser to circle by hand). Coordinating official: 'concur' or 'nonconcur' (use 'undecided_concur' to print Concur/Nonconcur unmarked)."
|
|
294
297
|
date:
|
|
295
298
|
type: date
|
|
296
299
|
default: ""
|
|
@@ -199,6 +199,7 @@
|
|
|
199
199
|
// item.kind — "par", "heading", "table", or "continuation"
|
|
200
200
|
context {
|
|
201
201
|
let heading_buffer = none
|
|
202
|
+
let heading_level = 0
|
|
202
203
|
// Filter out zero-width paragraphs so that an empty body (e.g. empty
|
|
203
204
|
// string from a KIND with no text) emits nothing and collapses to zero
|
|
204
205
|
// vertical space. Tables are always kept regardless of measured width.
|
|
@@ -226,24 +227,36 @@
|
|
|
226
227
|
let kind = item.kind
|
|
227
228
|
let item_content = item.content
|
|
228
229
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
//
|
|
230
|
+
// A buffered heading runs into this element only when the two belong to
|
|
231
|
+
// the same item: a later block of this list item ("continuation"), or,
|
|
232
|
+
// at top level, the next paragraph. The first block of the *next* item,
|
|
233
|
+
// a table (nest_level −1, so the level test alone excludes it), and
|
|
234
|
+
// another heading all fail the test, and each would otherwise carry the
|
|
235
|
+
// heading's text somewhere it was not authored. Those emit the heading
|
|
236
|
+
// on its own line, the treatment a heading before a table takes.
|
|
236
237
|
if heading_buffer != none {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
} else {
|
|
238
|
+
let runs_in = (
|
|
239
|
+
kind != "heading"
|
|
240
|
+
and item.nest_level == heading_level
|
|
241
|
+
and (heading_level == 0 or kind == "continuation")
|
|
242
|
+
)
|
|
243
|
+
if runs_in {
|
|
244
244
|
item_content = [#strong[#heading_buffer.] #item_content]
|
|
245
|
-
|
|
245
|
+
} else {
|
|
246
|
+
if any_emitted { blank-line() }
|
|
247
|
+
strong[#heading_buffer.]
|
|
248
|
+
any_emitted = true
|
|
246
249
|
}
|
|
250
|
+
heading_buffer = none
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Buffer this heading for the next element to run into. The level it was
|
|
254
|
+
// captured at rides along: the run-in test needs it, and the buffer is
|
|
255
|
+
// read one iteration later, by which time `item` is the *next* element.
|
|
256
|
+
if kind == "heading" {
|
|
257
|
+
heading_buffer = item_content
|
|
258
|
+
heading_level = item.nest_level
|
|
259
|
+
continue
|
|
247
260
|
}
|
|
248
261
|
|
|
249
262
|
// Format based on element kind
|
|
@@ -336,6 +349,15 @@
|
|
|
336
349
|
block[#final_par]
|
|
337
350
|
}
|
|
338
351
|
}
|
|
352
|
+
|
|
353
|
+
// A heading with nothing after it never ran into anything, and the buffer
|
|
354
|
+
// dies with the loop unless it is drained here. Sticky for the same reason
|
|
355
|
+
// the last item is: a standalone heading is one line, so it keeps with the
|
|
356
|
+
// signature block rather than opening a page alone.
|
|
357
|
+
if heading_buffer != none {
|
|
358
|
+
if any_emitted { blank-line() }
|
|
359
|
+
block(sticky: true)[#strong[#heading_buffer.]]
|
|
360
|
+
}
|
|
339
361
|
}
|
|
340
362
|
}
|
|
341
363
|
|
|
@@ -221,13 +221,12 @@
|
|
|
221
221
|
render-date-section(actual_date, memo-style: memo_style)
|
|
222
222
|
render-for-section(memo_for, memo_for_cols)
|
|
223
223
|
if not falsey(memo_from) { render-from-section(memo_from) }
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
224
|
+
// Blank entries are dropped first so a stub `- ` left under `references:`
|
|
225
|
+
// cannot pass as the lone reference and render an empty `()` after the subject.
|
|
226
|
+
let refs = compact-references(references)
|
|
227
|
+
let single-ref = if refs.len() == 1 { refs.at(0) } else { none }
|
|
229
228
|
render-subject-section(subject, inline-reference: single-ref)
|
|
230
|
-
render-references-section(
|
|
229
|
+
render-references-section(refs)
|
|
231
230
|
|
|
232
231
|
// AFH 33-337: "Begin text on second line below subject/references".
|
|
233
232
|
// Emitted here (not inside body.typ) so the v() lands at the same lexical
|
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
date: none,
|
|
23
23
|
// Format of indorsement: "standard" (same page), "informal" (no header), or "separate_page" (starts on new page)
|
|
24
24
|
format: "standard",
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// Decision action. `none` (default) displays no action line at all.
|
|
26
|
+
// Approval authority pair: "approve" or "disapprove" circles the selected
|
|
27
|
+
// option; "undecided" displays the pair with neither circled.
|
|
28
|
+
// Coordination pair: "concur" or "nonconcur" circles the selected option;
|
|
29
|
+
// "undecided_concur" displays the pair with neither circled.
|
|
28
30
|
action: none,
|
|
29
31
|
content,
|
|
30
32
|
) = {
|
|
@@ -39,23 +39,36 @@
|
|
|
39
39
|
let band-top = -band-height / 2 // puts the band center at dy 0
|
|
40
40
|
let band-center = band-top + band-height / 2
|
|
41
41
|
|
|
42
|
+
// The centered title/caption is placed content (not width-constrained), so a
|
|
43
|
+
// long caption would otherwise render on one line and run underneath the seal.
|
|
44
|
+
// Bound the caption to the width that stays clear of the seal on both sides
|
|
45
|
+
// and auto-shrink it to fit: the seal reaches `band-height - corner-overhang`
|
|
46
|
+
// into the text area, so reserving that plus a gutter on each side keeps the
|
|
47
|
+
// centered caption from touching the seal. `layout` resolves the text-area
|
|
48
|
+
// width so the bound tracks the page/margins.
|
|
49
|
+
let caption-gutter = 0.25in
|
|
50
|
+
let caption-clear = (band-height - corner-overhang) + caption-gutter
|
|
42
51
|
place(
|
|
43
52
|
dy: 0.625in - spacing.margin,
|
|
44
53
|
box(
|
|
45
54
|
width: 100%,
|
|
46
55
|
fill: none,
|
|
47
56
|
stroke: none,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
layout(size => place(
|
|
58
|
+
center + top,
|
|
59
|
+
align(center)[
|
|
60
|
+
#set text(12pt, font: font, fill: LETTERHEAD_COLOR, weight: "bold")
|
|
61
|
+
#title\
|
|
62
|
+
#v(1pt)
|
|
63
|
+
#if not falsey(caption) {
|
|
64
|
+
fit-to-width(
|
|
65
|
+
size.width - 2 * caption-clear,
|
|
66
|
+
alignment: center,
|
|
67
|
+
box(text(10.5pt)[#caption]),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
)),
|
|
59
72
|
),
|
|
60
73
|
)
|
|
61
74
|
|
|
@@ -145,11 +158,32 @@
|
|
|
145
158
|
)
|
|
146
159
|
}
|
|
147
160
|
|
|
161
|
+
/// Whether a `references` entry carries no citation text.
|
|
162
|
+
///
|
|
163
|
+
/// Blank entries reach the template routinely: a template author leaves a
|
|
164
|
+
/// stub `- ` under `references:` for the user to fill in, and the quillmark
|
|
165
|
+
/// helper hands back `""` for an unset or whitespace-only markdown field.
|
|
166
|
+
/// Such an entry must not count as a reference — otherwise a lone blank one
|
|
167
|
+
/// satisfies the "exactly one reference" test below and renders as an empty
|
|
168
|
+
/// `()` after the subject.
|
|
169
|
+
///
|
|
170
|
+
/// - entry (any): A single `references` element
|
|
171
|
+
/// -> bool
|
|
172
|
+
#let blank-reference(entry) = falsey(entry) or entry == []
|
|
173
|
+
|
|
174
|
+
/// Drops `references` entries that carry no citation text and normalizes the
|
|
175
|
+
/// result to an array, so blank placeholder entries neither render on their
|
|
176
|
+
/// own nor affect the inline-vs-block decision.
|
|
177
|
+
///
|
|
178
|
+
/// - references (array | none): Raw reference entries
|
|
179
|
+
/// -> array
|
|
180
|
+
#let compact-references(references) = ensure-array(references).filter(entry => not blank-reference(entry))
|
|
181
|
+
|
|
148
182
|
// AFH 33-337 "SUBJECT:": "In all uppercase letters place 'SUBJECT:', flush with the
|
|
149
183
|
// left margin, on the second line below the last line of the FROM element"
|
|
150
184
|
#let render-subject-section(subject-text, inline-reference: none) = {
|
|
151
185
|
blank-line()
|
|
152
|
-
let content = if inline-reference
|
|
186
|
+
let content = if not blank-reference(inline-reference) {
|
|
153
187
|
[#subject-text (#box(inline-reference))]
|
|
154
188
|
} else {
|
|
155
189
|
[#subject-text]
|
|
@@ -163,7 +197,8 @@
|
|
|
163
197
|
// AFH 33-337: only render References block for two or more references.
|
|
164
198
|
// A single reference is rendered inline after the SUBJECT text instead.
|
|
165
199
|
#let render-references-section(references) = {
|
|
166
|
-
|
|
200
|
+
let references = compact-references(references)
|
|
201
|
+
if references.len() >= 2 {
|
|
167
202
|
blank-line()
|
|
168
203
|
grid(
|
|
169
204
|
columns: (auto, auto, 1fr),
|
|
@@ -245,40 +280,57 @@
|
|
|
245
280
|
// =============================================================================
|
|
246
281
|
// ACTION LINE RENDERING
|
|
247
282
|
// =============================================================================
|
|
248
|
-
// Renders the
|
|
249
|
-
//
|
|
250
|
-
//
|
|
283
|
+
// Renders the decision line for indorsement memos — an "either / or" pair
|
|
284
|
+
// where the endorser's choice is circled and the rejected option struck out.
|
|
285
|
+
//
|
|
286
|
+
// Two option pairs are supported, reflecting the two roles an indorsement
|
|
287
|
+
// plays in a coordination chain: coordinating officials Concur / Nonconcur,
|
|
288
|
+
// while the final approval authority Approves / Disapproves.
|
|
289
|
+
//
|
|
290
|
+
// Each pair also has an "undecided" form that renders both options plain, for
|
|
291
|
+
// printing a memo the endorser circles by hand when signing.
|
|
292
|
+
//
|
|
251
293
|
// Empty/none suppression is handled by the caller before this is invoked.
|
|
252
294
|
|
|
295
|
+
// Maps each `action` value to the option pair it renders and the index of the
|
|
296
|
+
// selected option within that pair (`none` selects neither).
|
|
297
|
+
#let ACTION_FORMS = (
|
|
298
|
+
"undecided": (("Approve", "Disapprove"), none),
|
|
299
|
+
"approve": (("Approve", "Disapprove"), 0),
|
|
300
|
+
"disapprove": (("Approve", "Disapprove"), 1),
|
|
301
|
+
"undecided_concur": (("Concur", "Nonconcur"), none),
|
|
302
|
+
"concur": (("Concur", "Nonconcur"), 0),
|
|
303
|
+
"nonconcur": (("Concur", "Nonconcur"), 1),
|
|
304
|
+
)
|
|
305
|
+
|
|
253
306
|
#let render-action-line(action, trailing-blank-line: true) = {
|
|
254
307
|
assert(
|
|
255
|
-
action in
|
|
256
|
-
message: "action must be
|
|
308
|
+
action in ACTION_FORMS,
|
|
309
|
+
message: "action must be one of " + ACTION_FORMS.keys().map(k => "\"" + k + "\"").join(", "),
|
|
257
310
|
)
|
|
311
|
+
let (options, selected) = ACTION_FORMS.at(action)
|
|
312
|
+
// Circle the selected option using a box with rounded corners; strike the
|
|
313
|
+
// one it was chosen over. When neither is selected both render plain.
|
|
314
|
+
// Use baseline parameter to maintain vertical text alignment.
|
|
315
|
+
let render-option(index) = {
|
|
316
|
+
let option = options.at(index)
|
|
317
|
+
if selected == none {
|
|
318
|
+
option
|
|
319
|
+
} else if selected == index {
|
|
320
|
+
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[#option]
|
|
321
|
+
} else {
|
|
322
|
+
strike(option)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
258
325
|
// No leading blank-line: the caller (indorsement.typ) already emits the
|
|
259
326
|
// header→content gap once. The action line's `block(sticky: true)`
|
|
260
327
|
// additionally inherits `block.above: spacing.line` so the visual gap
|
|
261
328
|
// above matches the gap above a body's first paragraph.
|
|
262
|
-
//
|
|
263
|
-
// Use baseline parameter to maintain vertical text alignment
|
|
264
|
-
let approve-text = if action == "approve" {
|
|
265
|
-
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Approve]
|
|
266
|
-
} else if action == "disapprove" {
|
|
267
|
-
strike[Approve]
|
|
268
|
-
} else {
|
|
269
|
-
[Approve]
|
|
270
|
-
}
|
|
271
|
-
let disapprove-text = if action == "disapprove" {
|
|
272
|
-
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Disapprove]
|
|
273
|
-
} else if action == "approve" {
|
|
274
|
-
strike[Disapprove]
|
|
275
|
-
} else {
|
|
276
|
-
[Disapprove]
|
|
277
|
-
}
|
|
329
|
+
//
|
|
278
330
|
// Keep the action line with the following content (body or signature block)
|
|
279
331
|
// using the same sticky-block pattern that body.typ applies to the last
|
|
280
332
|
// paragraph, per AFH 33-337 §11 orphan-prevention rules.
|
|
281
|
-
block(sticky: true)[#
|
|
333
|
+
block(sticky: true)[#render-option(0) / #render-option(1)]
|
|
282
334
|
// Trailing blank-line places the body's first paragraph one line below
|
|
283
335
|
// the action, mirroring the gap above it. Suppressed when the body is
|
|
284
336
|
// empty so the signature block's own 4-line gap lands on AFH 33-337's
|
|
@@ -87,6 +87,36 @@
|
|
|
87
87
|
]
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/// Shrinks content to fit within a maximum width, never enlarging it.
|
|
91
|
+
///
|
|
92
|
+
/// Measures `body` at its current font size and scales it down uniformly only
|
|
93
|
+
/// when it is wider than `width`; content that already fits keeps its set size
|
|
94
|
+
/// (the scale is capped at 100%). Unlike `fit-box`, height flows naturally and
|
|
95
|
+
/// small content is never scaled up. Used for the letterhead caption (unit
|
|
96
|
+
/// designation): a long name is shrunk to the width that clears the seal rather
|
|
97
|
+
/// than running underneath it. Pass a non-wrapping `box` as `body` so the
|
|
98
|
+
/// measured width is the single-line width and the result stays on one line.
|
|
99
|
+
///
|
|
100
|
+
/// Empty content measures 0pt wide and has no scale to compute, so it is returned
|
|
101
|
+
/// as-is rather than divided by; a non-positive `width` likewise admits no scale
|
|
102
|
+
/// that fits, so the body is left at its set size instead of being flipped or
|
|
103
|
+
/// collapsed.
|
|
104
|
+
///
|
|
105
|
+
/// - width (length): Maximum width the content may occupy
|
|
106
|
+
/// - alignment (alignment): Horizontal placement within the reserved width
|
|
107
|
+
/// - body (content): Content to fit (typically a non-wrapping `box`)
|
|
108
|
+
/// -> content
|
|
109
|
+
#let fit-to-width(width, alignment: left, body) = context {
|
|
110
|
+
let s = measure(body)
|
|
111
|
+
if s.width <= 0pt or width <= 0pt { return body }
|
|
112
|
+
let f = calc.min(1, width / s.width) * 100% // ratio, capped so it only shrinks
|
|
113
|
+
box(width: width)[
|
|
114
|
+
#align(alignment)[
|
|
115
|
+
#scale(f, reflow: true)[#body]
|
|
116
|
+
]
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
/// Formats a date for the memo heading.
|
|
91
121
|
///
|
|
92
122
|
/// Dispatches on the shape a date field can arrive in:
|
|
@@ -275,7 +305,10 @@
|
|
|
275
305
|
if value == none {
|
|
276
306
|
""
|
|
277
307
|
} else if type(value) == array {
|
|
278
|
-
|
|
308
|
+
// `().join(sep)` is `none`, not `""` — coerce so an empty array keeps the
|
|
309
|
+
// documented contract and stays `falsey` for callers that test the result.
|
|
310
|
+
let joined = value.join(separator)
|
|
311
|
+
if joined == none { "" } else { joined }
|
|
279
312
|
} else {
|
|
280
313
|
str(value)
|
|
281
314
|
}
|