@airmark/quiver 0.32.2 → 0.33.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 +54 -18
- package/package.json +8 -6
- package/quills/af4141/0.1.0/Quill.yaml +1 -1
- package/quills/afmc_moa/0.0.1/Quill.yaml +10 -17
- package/quills/classic_resume/0.0.1/Quill.yaml +361 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/LICENSE +21 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Bold.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-BoldItalic.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Italic.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Regular.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt +94 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/components.typ +181 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/config.typ +53 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/layout.typ +57 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/lib.typ +6 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +32 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/typst.toml +26 -0
- package/quills/classic_resume/0.0.1/plate.typ +143 -0
- package/quills/cyber_ribbon_chart/0.0.1/Quill.yaml +320 -0
- package/quills/cyber_ribbon_chart/0.0.1/plate.typ +428 -0
- package/quills/daf1206/0.1.0/Quill.yaml +1 -2
- package/quills/daf4392/0.1.0/Quill.yaml +15 -1
- package/quills/daf4392/0.1.0/plate.typ +2 -2
- package/quills/usaf_letter/0.1.0/Quill.yaml +214 -0
- package/quills/usaf_letter/0.1.0/assets/dod_seal.png +0 -0
- package/quills/usaf_letter/0.1.0/assets/dow_seal.png +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/.gitignore +7 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/LICENSE +201 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/LICENSE +93 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/LICENSE.md +79 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LICENSE +102 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/backmatter.typ +32 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/config.typ +45 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +226 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/lib.typ +35 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/mainmatter.typ +140 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/primitives.typ +544 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/utils.typ +334 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/typst.toml +22 -0
- package/quills/usaf_letter/0.1.0/plate.typ +124 -0
- package/quills/usaf_memo/0.2.0/Quill.yaml +1 -1
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +1 -1
- package/quills/usaf_memo/0.3.0/Quill.yaml +62 -43
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -17
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +97 -126
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +12 -45
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +81 -87
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +58 -66
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +14 -36
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +47 -19
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +158 -111
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -141
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +3 -8
- package/quills/usaf_memo/0.3.0/plate.typ +81 -85
- package/templates/af4141.md +23 -0
- package/templates/appointment_letter.md +58 -0
- package/templates/daf1206.md +23 -0
- package/templates/daf4392.md +34 -0
- package/templates/pass_request.md +48 -0
- package/templates/rebuttal.md +63 -0
- package/templates/templates.json +58 -0
- package/templates/usaf_template.md +30 -0
- package/templates/ussf_template.md +30 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#import "@local/quillmark-helper:0.1.0": data
|
|
2
|
+
|
|
3
|
+
#let ink = luma(12%)
|
|
4
|
+
#let mute = luma(45%)
|
|
5
|
+
#let faint = luma(62%)
|
|
6
|
+
|
|
7
|
+
#set page(width: 11in, height: 8.5in, margin: 0.45in)
|
|
8
|
+
#set text(font: "Figtree", size: 8pt, fill: ink)
|
|
9
|
+
|
|
10
|
+
#set document(
|
|
11
|
+
title: "Ribbon Chart — " + data.name,
|
|
12
|
+
author: data.name,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// A `plaintext` field lowers to content padded with a space on each side, which
|
|
16
|
+
// would print inside a bold run and defeat an emptiness test.
|
|
17
|
+
#let trim(v) = {
|
|
18
|
+
if type(v) != content { return v }
|
|
19
|
+
let kids = v.at("children", default: none)
|
|
20
|
+
if kids == none { return v }
|
|
21
|
+
let pad(c) = c == [ ] or c.func() == parbreak
|
|
22
|
+
while kids.len() > 0 and pad(kids.first()) { kids = kids.slice(1) }
|
|
23
|
+
while kids.len() > 0 and pad(kids.last()) { kids = kids.slice(0, -1) }
|
|
24
|
+
kids.sum(default: [])
|
|
25
|
+
}
|
|
26
|
+
#let blank(v) = { let t = trim(v); t == [] or t == "" }
|
|
27
|
+
|
|
28
|
+
// The bundled fallback face carries no geometric glyphs — no ✓, ■, □, ●, ▸ — so
|
|
29
|
+
// a mark is drawn rather than set. Held against open is a value contrast, which
|
|
30
|
+
// is what survives the photocopier these are read from.
|
|
31
|
+
#let held-mark = box(baseline: 0.5pt, rect(width: 4.6pt, height: 4.6pt, fill: ink, stroke: 0.5pt + ink))
|
|
32
|
+
#let open-mark = box(baseline: 0.5pt, rect(width: 4.6pt, height: 4.6pt, fill: none, stroke: 0.5pt + faint))
|
|
33
|
+
|
|
34
|
+
#let section-rule(title) = block(spacing: 0pt, below: 5pt)[
|
|
35
|
+
#text(size: 6.6pt, weight: 700, tracking: 0.7pt)[#upper(title)]
|
|
36
|
+
#v(2pt)
|
|
37
|
+
#line(length: 100%, stroke: 0.6pt + ink)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
#let fact(label, value) = block(spacing: 0pt)[
|
|
41
|
+
#text(size: 5.8pt, weight: 600, tracking: 0.4pt, fill: mute)[#upper(label)]
|
|
42
|
+
#v(1.5pt)
|
|
43
|
+
#text(size: 8.5pt, weight: 500)[#if value == none { text(fill: faint)[—] } else { value }]
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
#let show-date(d) = if d == none { none } else { d.display("[day padding:none] [month repr:short] [year]") }
|
|
47
|
+
|
|
48
|
+
// ─── the ladder ──────────────────────────────────────────────────────────────
|
|
49
|
+
// Every 17X board and school gate, as an offset from the year group, counted
|
|
50
|
+
// from the adjusted year group where one is set because that is the year a
|
|
51
|
+
// board counts from. The tier is how heavily it prints: a board is a date you
|
|
52
|
+
// are ranked against, a look is one of a numbered series, a course is neither.
|
|
53
|
+
#let ladder = (
|
|
54
|
+
(3, "Cyber 200", "course"),
|
|
55
|
+
(4, "SOS window", "course"),
|
|
56
|
+
(8, "Maj board", "board"),
|
|
57
|
+
(8, "IDE 1st look", "look"),
|
|
58
|
+
(9, "Cyber 300", "course"),
|
|
59
|
+
(9, "IDE 2nd look", "look"),
|
|
60
|
+
(10, "IDE 3rd look", "look"),
|
|
61
|
+
(11, "IDE final look", "look"),
|
|
62
|
+
(12, "Lt Col board", "board"),
|
|
63
|
+
(14, "SDE 1st look", "look"),
|
|
64
|
+
(15, "SDE 2nd look", "look"),
|
|
65
|
+
(16, "SDE 3rd look", "look"),
|
|
66
|
+
(17, "SDE final look", "look"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// ─── inputs ──────────────────────────────────────────────────────────────────
|
|
70
|
+
#let comm-yg = data.commissioning_yg
|
|
71
|
+
#let adj-yg = data.adjusted_yg
|
|
72
|
+
#let board-yg = if adj-yg != 0 { adj-yg } else { comm-yg }
|
|
73
|
+
#let start-year = data.timeline_start_year
|
|
74
|
+
#let years = calc.max(1, data.timeline_years)
|
|
75
|
+
#let cols = years * 2
|
|
76
|
+
#let narrow = years > 12
|
|
77
|
+
#let band-height = 0.52in
|
|
78
|
+
#let chip-size = if narrow { 5.6pt } else { 6.2pt }
|
|
79
|
+
#let chip-inset = if narrow { 2pt } else { 3.5pt }
|
|
80
|
+
|
|
81
|
+
// Three weights of gate, by what each one is: a board ranks you against a year
|
|
82
|
+
// group on a date, a look is one of a numbered series, a course is a seat.
|
|
83
|
+
#let chip(label, tier) = {
|
|
84
|
+
if tier == "board" {
|
|
85
|
+
box(fill: ink, inset: (x: chip-inset, y: 2pt))[#text(size: chip-size, weight: 700, fill: white)[#label]]
|
|
86
|
+
} else if tier == "look" {
|
|
87
|
+
box(fill: none, stroke: 0.5pt + ink, inset: (x: chip-inset, y: 2pt))[#text(size: chip-size, weight: 600)[#label]]
|
|
88
|
+
} else {
|
|
89
|
+
box(inset: (x: 0.5pt, y: 2pt))[#text(size: chip-size, fill: mute)[#label]]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// One tint per vector, darkest first, so the shade says which row this is.
|
|
94
|
+
// Adjacent tours part by a gap and an edge instead, and at the third rank the
|
|
95
|
+
// edge is doing all the work — which is what a photocopy leaves anyway.
|
|
96
|
+
#let rank-tints = (luma(86%), luma(92%), luma(96%))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// ─── vectors ─────────────────────────────────────────────────────────────────
|
|
100
|
+
#let vectors = data.vectors.enumerate().map(((i, v)) => (
|
|
101
|
+
label: if blank(v.label) { [Vector #(i + 1)] } else { trim(v.label) },
|
|
102
|
+
focus: trim(v.focus),
|
|
103
|
+
tours: v.tours.map(t => (
|
|
104
|
+
title: trim(t.title),
|
|
105
|
+
duration: float(t.duration),
|
|
106
|
+
vml: t.vml,
|
|
107
|
+
school: t.school,
|
|
108
|
+
)),
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
// A half-year column is H1 (Jan–Jun, even) or H2 (Jul–Dec, odd), and a VML
|
|
112
|
+
// cycle picks the phase a tour may start on. Where that means waiting, the wait
|
|
113
|
+
// is added to the tour before it rather than left as a hole: you hold a job
|
|
114
|
+
// until you move, and an empty half-year on a career chart reads as
|
|
115
|
+
// unemployment. Only the first tour can be preceded by nothing, and its indent
|
|
116
|
+
// is the half-year before a summer move.
|
|
117
|
+
//
|
|
118
|
+
// Nothing is dropped. A tour the window cuts keeps a torn edge and says which
|
|
119
|
+
// year it runs past; one starting beyond the window is counted on its row.
|
|
120
|
+
#let place(tours) = {
|
|
121
|
+
let cursor = 0
|
|
122
|
+
let out = ()
|
|
123
|
+
for t in tours {
|
|
124
|
+
let phase = if t.vml == "Winter" { 0 } else { 1 }
|
|
125
|
+
let start = cursor
|
|
126
|
+
if calc.rem(start, 2) != phase { start += 1 }
|
|
127
|
+
if start > cursor and out.len() > 0 {
|
|
128
|
+
let prev = out.last()
|
|
129
|
+
prev.span += start - cursor
|
|
130
|
+
prev.held = true
|
|
131
|
+
out.at(out.len() - 1) = prev
|
|
132
|
+
}
|
|
133
|
+
out.push((
|
|
134
|
+
tour: t,
|
|
135
|
+
start: start,
|
|
136
|
+
span: calc.max(1, int(calc.round(t.duration * 2))),
|
|
137
|
+
held: false,
|
|
138
|
+
))
|
|
139
|
+
cursor = out.last().start + out.last().span
|
|
140
|
+
}
|
|
141
|
+
out.map(p => if p.start >= cols { (..p, beyond: true) } else {
|
|
142
|
+
(..p, beyond: false, drawn: calc.min(p.span, cols - p.start), clipped: p.start + p.span > cols)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The band is ruled to an even height, so a narrow block cannot grow to fit its
|
|
147
|
+
// title: the title sets to the block instead. That is the ceiling `title`
|
|
148
|
+
// warns about in Quill.yaml — about twenty characters per year of length.
|
|
149
|
+
#let title-size(span) = if span <= 1 { 6.4pt } else if span == 2 { 7.2pt } else { 8pt }
|
|
150
|
+
|
|
151
|
+
// Half-columns back to years, so a stretched block prints the length it draws
|
|
152
|
+
// rather than the length it was given.
|
|
153
|
+
#let span-label(span) = {
|
|
154
|
+
let years = span / 2
|
|
155
|
+
if calc.rem(span, 2) == 0 { [#int(years) yr] } else { [#years yr] }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ═══ IDENTITY ════════════════════════════════════════════════════════════════
|
|
159
|
+
#grid(
|
|
160
|
+
columns: (2.7in, 1fr),
|
|
161
|
+
column-gutter: 18pt,
|
|
162
|
+
align: bottom,
|
|
163
|
+
[
|
|
164
|
+
#text(size: 17pt, weight: 800, tracking: -0.25pt)[#data.name]
|
|
165
|
+
#if not blank(data.duty_title) [
|
|
166
|
+
#v(2pt)
|
|
167
|
+
#text(size: 8pt, weight: 500, fill: mute)[#data.duty_title]
|
|
168
|
+
]
|
|
169
|
+
],
|
|
170
|
+
grid(
|
|
171
|
+
columns: (1fr,) * 5,
|
|
172
|
+
column-gutter: 12pt,
|
|
173
|
+
align: top,
|
|
174
|
+
fact("Commissioning YG", [#comm-yg]),
|
|
175
|
+
fact("Adjusted YG", if adj-yg != 0 { [#adj-yg] } else { none }),
|
|
176
|
+
fact("Date of rank", show-date(data.date_of_rank)),
|
|
177
|
+
fact("Arrived station", show-date(data.date_arrived_station)),
|
|
178
|
+
fact("Advanced degree", if blank(data.advanced_degree) { none } else { data.advanced_degree }),
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
#v(6pt)
|
|
183
|
+
#line(length: 100%, stroke: 1.3pt + ink)
|
|
184
|
+
#v(9pt)
|
|
185
|
+
|
|
186
|
+
// ═══ TIMELINE ════════════════════════════════════════════════════════════════
|
|
187
|
+
#let in-window(cy) = cy >= start-year and cy < start-year + years
|
|
188
|
+
#let ahead = ladder.filter(m => board-yg + m.at(0) >= start-year + years)
|
|
189
|
+
|
|
190
|
+
#block(spacing: 0pt)[
|
|
191
|
+
#grid(columns: (1fr, auto), align: (left + bottom, right + bottom),
|
|
192
|
+
text(size: 6.6pt, weight: 700, tracking: 0.7pt)[#upper("Assignment vectors & development timeline")],
|
|
193
|
+
text(size: 6.2pt, fill: mute)[
|
|
194
|
+
CY #start-year–#(start-year + years - 1) · counted from YG #board-yg
|
|
195
|
+
#if ahead.len() > 0 [ · #ahead.len() later milestone#if ahead.len() > 1 [s] beyond #(start-year + years - 1)]
|
|
196
|
+
],
|
|
197
|
+
)
|
|
198
|
+
#v(2pt)
|
|
199
|
+
#line(length: 100%, stroke: 0.6pt + ink)
|
|
200
|
+
]
|
|
201
|
+
#v(5pt)
|
|
202
|
+
|
|
203
|
+
#let rows = ()
|
|
204
|
+
|
|
205
|
+
// year header
|
|
206
|
+
#rows.push(table.cell(stroke: none)[])
|
|
207
|
+
#for i in range(years) {
|
|
208
|
+
rows.push(table.cell(colspan: 2, stroke: (bottom: 0.7pt + ink), inset: (bottom: 2.5pt))[
|
|
209
|
+
#text(size: 8pt, weight: 700)[#(start-year + i)]
|
|
210
|
+
#linebreak()
|
|
211
|
+
#text(size: 5.8pt, fill: mute)[YG+#(start-year + i - board-yg)]
|
|
212
|
+
])
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// milestone row
|
|
216
|
+
#rows.push(table.cell(align: left + horizon, stroke: none)[
|
|
217
|
+
#text(size: 6.4pt, weight: 600, fill: mute)[Eligibility]
|
|
218
|
+
])
|
|
219
|
+
#for i in range(years) {
|
|
220
|
+
let cy = start-year + i
|
|
221
|
+
let hits = ladder.filter(m => board-yg + m.at(0) == cy)
|
|
222
|
+
rows.push(table.cell(colspan: 2, stroke: none, inset: (x: 1.5pt, y: 3pt))[
|
|
223
|
+
#for m in hits [
|
|
224
|
+
#block(spacing: 2pt, breakable: false)[#chip(m.at(1), m.at(2))]
|
|
225
|
+
]
|
|
226
|
+
])
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// one row per vector
|
|
230
|
+
#for (rank, v) in vectors.enumerate() {
|
|
231
|
+
let placed = place(v.tours)
|
|
232
|
+
let beyond = placed.filter(p => p.beyond)
|
|
233
|
+
let top-rule = (top: 0.4pt + luma(80%))
|
|
234
|
+
let tint = rank-tints.at(calc.min(rank, rank-tints.len() - 1))
|
|
235
|
+
|
|
236
|
+
rows.push(table.cell(align: left + horizon, stroke: top-rule, inset: (right: 5pt, y: 3pt))[
|
|
237
|
+
#text(size: 8.5pt, weight: 700)[#v.label]
|
|
238
|
+
#if not blank(v.focus) or beyond.len() > 0 [
|
|
239
|
+
#linebreak()
|
|
240
|
+
#text(size: 6pt, fill: mute)[
|
|
241
|
+
#if not blank(v.focus) [#v.focus]
|
|
242
|
+
#if beyond.len() > 0 [
|
|
243
|
+
#if not blank(v.focus) [ · ]
|
|
244
|
+
+#beyond.len() past #(start-year + years - 1)
|
|
245
|
+
]
|
|
246
|
+
]
|
|
247
|
+
]
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
let at = 0
|
|
251
|
+
let idx = 0
|
|
252
|
+
for p in placed.filter(p => not p.beyond) {
|
|
253
|
+
if p.start > at {
|
|
254
|
+
rows.push(table.cell(colspan: p.start - at, stroke: top-rule)[])
|
|
255
|
+
at = p.start
|
|
256
|
+
}
|
|
257
|
+
rows.push(table.cell(
|
|
258
|
+
colspan: p.drawn,
|
|
259
|
+
stroke: top-rule,
|
|
260
|
+
inset: (x: 1pt, y: 2pt),
|
|
261
|
+
align: left + horizon,
|
|
262
|
+
)[
|
|
263
|
+
// The block is drawn inside its cell rather than as the cell's fill, so
|
|
264
|
+
// adjacent tours are parted by a gap and an edge instead of by a second
|
|
265
|
+
// shade — which is what lets the shade say which vector this is.
|
|
266
|
+
#box(
|
|
267
|
+
width: 100%,
|
|
268
|
+
height: 100%,
|
|
269
|
+
fill: if p.tour.school { none } else { tint },
|
|
270
|
+
stroke: if p.tour.school { (paint: mute, thickness: 0.5pt, dash: (2pt, 1.5pt)) } else { 0.5pt + luma(55%) },
|
|
271
|
+
inset: (x: 3pt, y: 2pt),
|
|
272
|
+
baseline: 0pt,
|
|
273
|
+
)[
|
|
274
|
+
#set align(left + horizon)
|
|
275
|
+
#text(size: title-size(p.drawn), weight: 600, style: if p.tour.school { "italic" } else { "normal" })[
|
|
276
|
+
#p.tour.title
|
|
277
|
+
]
|
|
278
|
+
#linebreak()
|
|
279
|
+
#text(size: 5.8pt, fill: mute)[
|
|
280
|
+
#span-label(p.span)#if p.held [ #sym.dagger] · #p.tour.vml#if p.clipped [ · runs past #(start-year + years - 1)]
|
|
281
|
+
]
|
|
282
|
+
]
|
|
283
|
+
])
|
|
284
|
+
at = p.start + p.drawn
|
|
285
|
+
idx += 1
|
|
286
|
+
}
|
|
287
|
+
if at < cols { rows.push(table.cell(colspan: cols - at, stroke: top-rule)[]) }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#table(
|
|
291
|
+
columns: (1.2in,) + (1fr,) * cols,
|
|
292
|
+
rows: (auto, auto) + (band-height,) * vectors.len(),
|
|
293
|
+
stroke: none,
|
|
294
|
+
inset: 2pt,
|
|
295
|
+
..rows,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
// The legend names the marks this chart actually carries and no others.
|
|
299
|
+
#let drawn-tours = vectors.map(v => place(v.tours).filter(p => not p.beyond)).flatten()
|
|
300
|
+
#let shown-tiers = ladder.filter(m => in-window(board-yg + m.at(0))).map(m => m.at(2)).dedup()
|
|
301
|
+
#let legend = ()
|
|
302
|
+
#if "board" in shown-tiers { legend.push([#chip("board", "board") ranked against your year group]) }
|
|
303
|
+
#if "look" in shown-tiers { legend.push([#chip("look", "look") one of a numbered series]) }
|
|
304
|
+
#if drawn-tours.any(p => not p.tour.school) {
|
|
305
|
+
legend.push([#box(width: 14pt, height: 6pt, fill: rank-tints.at(0), stroke: 0.5pt + luma(55%)) assignment])
|
|
306
|
+
}
|
|
307
|
+
#if drawn-tours.any(p => p.tour.school) {
|
|
308
|
+
legend.push([#box(width: 14pt, height: 6pt, stroke: (paint: mute, thickness: 0.5pt, dash: (2pt, 1.5pt))) school])
|
|
309
|
+
}
|
|
310
|
+
#if drawn-tours.any(p => p.held) { legend.push([#sym.dagger held to the next cycle]) }
|
|
311
|
+
|
|
312
|
+
#if legend.len() > 0 {
|
|
313
|
+
v(3pt)
|
|
314
|
+
text(size: 5.8pt, fill: mute, legend.join(h(6pt)))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#v(9pt)
|
|
318
|
+
|
|
319
|
+
// ═══ RECORD ══════════════════════════════════════════════════════════════════
|
|
320
|
+
// Every member arrives, held or not, in the order Quill.yaml declares it; an
|
|
321
|
+
// unheld member's detail arrives blank whatever the document retains.
|
|
322
|
+
#let quals = data.qualifications
|
|
323
|
+
#let vocabulary = (
|
|
324
|
+
("Leadership & Command", quals.command),
|
|
325
|
+
("Operations", quals.operations),
|
|
326
|
+
("Staff & Functional", quals.staff),
|
|
327
|
+
("Education & PME", quals.education),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
#let remarks = (
|
|
331
|
+
("Awards", data.awards),
|
|
332
|
+
("Certifications", data.certifications),
|
|
333
|
+
("Deployments", data.deployments),
|
|
334
|
+
).filter(r => r.at(1).len() > 0)
|
|
335
|
+
|
|
336
|
+
// The stratifications sit beside the vocabulary; with none, there is no narrow
|
|
337
|
+
// empty column beside it, there is no column.
|
|
338
|
+
#let strats-column = [
|
|
339
|
+
#section-rule("Recent stratifications")
|
|
340
|
+
#for s in data.stratifications {
|
|
341
|
+
block(spacing: 7pt)[
|
|
342
|
+
#grid(columns: (0.44in, 1fr), column-gutter: 7pt, align: (right + top, left + top),
|
|
343
|
+
text(size: 8.5pt, weight: 700)[#s.year],
|
|
344
|
+
[
|
|
345
|
+
#if not blank(s.rater_strat) [#text(size: 8pt)[#s.rater_strat]]
|
|
346
|
+
#if not blank(s.rater_strat) and not blank(s.hlr_strat) [#linebreak()]
|
|
347
|
+
#if not blank(s.hlr_strat) [#text(size: 7.4pt, fill: mute)[#s.hlr_strat]]
|
|
348
|
+
],
|
|
349
|
+
)
|
|
350
|
+
]
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
#let vocabulary-column = [
|
|
355
|
+
#section-rule("Qualifications & experience")
|
|
356
|
+
#grid(
|
|
357
|
+
columns: (1fr,) * vocabulary.len(),
|
|
358
|
+
column-gutter: 14pt,
|
|
359
|
+
..vocabulary.map(group => {
|
|
360
|
+
let (heading, members) = group
|
|
361
|
+
[
|
|
362
|
+
#text(size: 6.2pt, weight: 700, tracking: 0.4pt, fill: mute)[#upper(heading)]
|
|
363
|
+
#v(3pt)
|
|
364
|
+
#set par(leading: 0.42em)
|
|
365
|
+
#for (_, m) in members {
|
|
366
|
+
block(spacing: 0pt, inset: (y: 1.1pt))[
|
|
367
|
+
#grid(columns: (7.6pt, 1fr), align: (left + top, left + top))[
|
|
368
|
+
#if m.held { held-mark } else { open-mark }
|
|
369
|
+
][
|
|
370
|
+
#text(
|
|
371
|
+
size: 7.2pt,
|
|
372
|
+
weight: if m.held { 600 } else { 400 },
|
|
373
|
+
fill: if m.held { ink } else { luma(50%) },
|
|
374
|
+
)[#m.title]
|
|
375
|
+
#if not blank(m.detail) [
|
|
376
|
+
#linebreak()
|
|
377
|
+
#text(size: 6.4pt, style: "italic", fill: mute)[#trim(m.detail)]
|
|
378
|
+
]
|
|
379
|
+
]
|
|
380
|
+
]
|
|
381
|
+
}
|
|
382
|
+
]
|
|
383
|
+
}),
|
|
384
|
+
)
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
#if data.stratifications.len() > 0 {
|
|
388
|
+
grid(columns: (2.45in, 1fr), column-gutter: 24pt, strats-column, vocabulary-column)
|
|
389
|
+
} else {
|
|
390
|
+
vocabulary-column
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Three lists side by side across the full width. Stacked in a narrow column
|
|
394
|
+
// they cost three times the height and read as one long list.
|
|
395
|
+
#if remarks.len() > 0 [
|
|
396
|
+
#v(8pt)
|
|
397
|
+
#section-rule("Awards · certifications · deployments")
|
|
398
|
+
#grid(
|
|
399
|
+
columns: (1fr,) * 3,
|
|
400
|
+
column-gutter: 24pt,
|
|
401
|
+
..remarks.map(((label, lines)) => [
|
|
402
|
+
#text(size: 5.8pt, weight: 600, tracking: 0.4pt, fill: mute)[#upper(label)]
|
|
403
|
+
#v(2pt)
|
|
404
|
+
#for l in lines [#block(spacing: 2.5pt)[#text(size: 7.6pt)[#l]]]
|
|
405
|
+
]),
|
|
406
|
+
)
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
// ═══ NOTES ═══════════════════════════════════════════════════════════════════
|
|
410
|
+
// The one thing the page does that the browser tool it came from could not: a
|
|
411
|
+
// rater writes on it during the discussion, and the sheet becomes the record of
|
|
412
|
+
// the conversation. So the lines print whether or not anything was typed above
|
|
413
|
+
// them.
|
|
414
|
+
#v(9pt)
|
|
415
|
+
#section-rule("Development notes")
|
|
416
|
+
#let typed = data.at("$body", default: "")
|
|
417
|
+
#if type(typed) != str [
|
|
418
|
+
#block(spacing: 6pt)[#text(size: 7.6pt)[#typed]]
|
|
419
|
+
]
|
|
420
|
+
#for _ in range(2) {
|
|
421
|
+
v(11pt)
|
|
422
|
+
line(length: 100%, stroke: 0.4pt + luma(80%))
|
|
423
|
+
}
|
|
424
|
+
#v(8pt)
|
|
425
|
+
#text(size: 6.2pt, fill: mute)[
|
|
426
|
+
Discussed with #box(width: 1.7in, stroke: (bottom: 0.4pt + luma(60%)))[]
|
|
427
|
+
#h(10pt) on #box(width: 1in, stroke: (bottom: 0.4pt + luma(60%)))[]
|
|
428
|
+
]
|
|
@@ -59,9 +59,8 @@ main:
|
|
|
59
59
|
|
|
60
60
|
card_kinds:
|
|
61
61
|
accomplishments_continued:
|
|
62
|
+
title: Specific Accomplishments (Continued — Page 2)
|
|
62
63
|
description: Optional. Add this card only when page 2 overflow is required. Use the card body for continued bullets. Rank/name on page 2 matches the main nominee line automatically.
|
|
63
|
-
ui:
|
|
64
|
-
title: Specific Accomplishments (Continued — Page 2)
|
|
65
64
|
body:
|
|
66
65
|
example: |
|
|
67
66
|
- Continued accomplishment bullet.
|
|
@@ -83,6 +83,20 @@ main:
|
|
|
83
83
|
compact: true
|
|
84
84
|
description: "Put in your Grade."
|
|
85
85
|
|
|
86
|
+
briefer_name:
|
|
87
|
+
type: string
|
|
88
|
+
default: ""
|
|
89
|
+
ui:
|
|
90
|
+
compact: true
|
|
91
|
+
description: "Name of the person giving the briefing (Last, First)."
|
|
92
|
+
|
|
93
|
+
briefer_grade:
|
|
94
|
+
type: string
|
|
95
|
+
default: ""
|
|
96
|
+
ui:
|
|
97
|
+
compact: true
|
|
98
|
+
description: "Grade of the person giving the briefing."
|
|
99
|
+
|
|
86
100
|
emergency_contact_name:
|
|
87
101
|
type: string
|
|
88
102
|
default: ""
|
|
@@ -95,9 +109,9 @@ main:
|
|
|
95
109
|
|
|
96
110
|
card_kinds:
|
|
97
111
|
itinerary:
|
|
112
|
+
title: Proposed Travel Itinerary
|
|
98
113
|
description: "Each card represents one row in the travel itinerary table. Up to 10 rows are supported."
|
|
99
114
|
ui:
|
|
100
|
-
title: Proposed Travel Itinerary
|
|
101
115
|
groups: [itinerary_entry]
|
|
102
116
|
body:
|
|
103
117
|
enabled: false
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
#if data.briefed_date != none { vals.insert("briefed_date", data.briefed_date) }
|
|
48
48
|
#if data.briefee_name != "" { vals.insert("briefee_name", data.briefee_name) }
|
|
49
49
|
#if data.briefee_grade != "" { vals.insert("briefee_grade", data.briefee_grade) }
|
|
50
|
-
#if
|
|
51
|
-
#if
|
|
50
|
+
#if data.briefer_name != "" { vals.insert("briefer_name", data.briefer_name) }
|
|
51
|
+
#if data.briefer_grade != "" { vals.insert("briefer_grade", data.briefer_grade) }
|
|
52
52
|
|
|
53
53
|
// Emergency contact — bold label prefixes a "name: phone" pair.
|
|
54
54
|
#{
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
quill:
|
|
2
|
+
name: usaf_letter
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
backend: typst
|
|
5
|
+
description: U.S. Air Force and Space Force Personal Letter
|
|
6
|
+
|
|
7
|
+
typst:
|
|
8
|
+
plate_file: plate.typ
|
|
9
|
+
|
|
10
|
+
main:
|
|
11
|
+
ui:
|
|
12
|
+
groups: [addressing, letterhead, classification, additional]
|
|
13
|
+
body:
|
|
14
|
+
example: |
|
|
15
|
+
Thank you for asking about the personal letter. The personal letter conveys official information in a personal manner to a limited number of people, which is what makes it the right form for a reply to an inquiry, a welcome, a note of appreciation, or a letter of condolence.
|
|
16
|
+
|
|
17
|
+
Paragraphs are indented and take no number or letter. Leave one blank line between them.
|
|
18
|
+
|
|
19
|
+
Lines that stand as written — a roster of names, an address — go in a block quote, typeset flush with the margin. End a line with a backslash to break it.
|
|
20
|
+
|
|
21
|
+
> MARY E. BROWN, Maj, USAF\
|
|
22
|
+
> ROBERT L. JONES, Capt, USAF
|
|
23
|
+
|
|
24
|
+
The last paragraph may carry your contact information: DSN and commercial telephone numbers and your official email address.
|
|
25
|
+
fields:
|
|
26
|
+
letter_from:
|
|
27
|
+
type: array
|
|
28
|
+
items:
|
|
29
|
+
type: plaintext
|
|
30
|
+
inline: true
|
|
31
|
+
example:
|
|
32
|
+
- Colonel Jane A. Doe
|
|
33
|
+
- Commander
|
|
34
|
+
- 123d Example Squadron
|
|
35
|
+
- 1 Example Way
|
|
36
|
+
- Example AFB ST 12345-6789
|
|
37
|
+
ui:
|
|
38
|
+
group: addressing
|
|
39
|
+
compact: true
|
|
40
|
+
description: "Your full rank and full name, then your duty title and mailing address, one line each. Printed on the second line below the date. Do not put a comma between the city and the state abbreviation."
|
|
41
|
+
|
|
42
|
+
letter_for:
|
|
43
|
+
type: array
|
|
44
|
+
items:
|
|
45
|
+
type: plaintext
|
|
46
|
+
inline: true
|
|
47
|
+
example:
|
|
48
|
+
- Major John B. Smith
|
|
49
|
+
- Chief, Plans and Programs
|
|
50
|
+
- 456th Example Group
|
|
51
|
+
- 2 Sample Boulevard
|
|
52
|
+
- Sample AFB ST 98765-4321
|
|
53
|
+
ui:
|
|
54
|
+
group: addressing
|
|
55
|
+
compact: true
|
|
56
|
+
description: "The receiver's full rank and full name, then their duty title and mailing address, one line each. Printed on the third line below your own address. Do not put a comma between the city and the state abbreviation."
|
|
57
|
+
|
|
58
|
+
salutation:
|
|
59
|
+
type: plaintext
|
|
60
|
+
inline: true
|
|
61
|
+
example: Dear Major Smith
|
|
62
|
+
ui:
|
|
63
|
+
group: addressing
|
|
64
|
+
compact: true
|
|
65
|
+
description: "The form of address for the receiver's rank followed by their last name — see 'Forms of Address, Salutation, and Complimentary Close' in AFH 33-337. Take no punctuation after the last name."
|
|
66
|
+
|
|
67
|
+
date:
|
|
68
|
+
type: date
|
|
69
|
+
default: ""
|
|
70
|
+
ui:
|
|
71
|
+
group: addressing
|
|
72
|
+
compact: true
|
|
73
|
+
description: "YYYY-MM-DD, printed as '15 September 2026' flush with the right margin. Leave blank to print a fillable PDF text field for the signer to type the date into when signing."
|
|
74
|
+
|
|
75
|
+
complimentary_close:
|
|
76
|
+
type: plaintext
|
|
77
|
+
inline: true
|
|
78
|
+
default: Sincerely
|
|
79
|
+
ui:
|
|
80
|
+
group: addressing
|
|
81
|
+
compact: true
|
|
82
|
+
description: "Printed on the second line below the last line of text, above the signature block. AFH 33-337's 'Forms of Address, Salutation, and Complimentary Close' tables give the close each receiver takes; 'Sincerely' is the usual one."
|
|
83
|
+
|
|
84
|
+
signature_block:
|
|
85
|
+
type: array
|
|
86
|
+
items:
|
|
87
|
+
type: plaintext
|
|
88
|
+
inline: true
|
|
89
|
+
example:
|
|
90
|
+
- "JANE A. DOE, Col, USAF"
|
|
91
|
+
ui:
|
|
92
|
+
group: addressing
|
|
93
|
+
compact: true
|
|
94
|
+
description: "Name in UPPERCASE as signed, grade, and service, printed on the fifth line below the complimentary close. A personal letter does not carry the sender's duty title, which the heading has already given."
|
|
95
|
+
|
|
96
|
+
letterhead_title:
|
|
97
|
+
type: array
|
|
98
|
+
items:
|
|
99
|
+
type: plaintext
|
|
100
|
+
inline: true
|
|
101
|
+
default:
|
|
102
|
+
- DEPARTMENT OF THE AIR FORCE
|
|
103
|
+
example:
|
|
104
|
+
- DEPARTMENT OF THE AIR FORCE
|
|
105
|
+
- 123D EXAMPLE SQUADRON
|
|
106
|
+
ui:
|
|
107
|
+
group: letterhead
|
|
108
|
+
description: "Letterhead lines, top to bottom, all rendered in uppercase. The first is the department title, set larger than the rest. Change that line only for a Joint Command or a DoW agency. The lines below it are your unit's full organization name."
|
|
109
|
+
|
|
110
|
+
letterhead_seal:
|
|
111
|
+
type: enum
|
|
112
|
+
values:
|
|
113
|
+
- dow
|
|
114
|
+
- dod
|
|
115
|
+
default: dow
|
|
116
|
+
ui:
|
|
117
|
+
group: letterhead
|
|
118
|
+
compact: true
|
|
119
|
+
description: "Department of War (DoW) or Department of Defense (DoD) seal, shown at the top left of the letterhead."
|
|
120
|
+
|
|
121
|
+
letterhead_seal_subtitle:
|
|
122
|
+
type: plaintext
|
|
123
|
+
inline: true
|
|
124
|
+
default: ""
|
|
125
|
+
ui:
|
|
126
|
+
group: letterhead
|
|
127
|
+
compact: true
|
|
128
|
+
description: "Bold-caps line below the seal. Nothing prints when it is blank."
|
|
129
|
+
|
|
130
|
+
tag_line:
|
|
131
|
+
type: richtext
|
|
132
|
+
inline: true
|
|
133
|
+
default: ""
|
|
134
|
+
ui:
|
|
135
|
+
group: letterhead
|
|
136
|
+
compact: true
|
|
137
|
+
description: "Organizational motto, centered in the footer of every page. Supports Markdown emphasis: *italics*, **bold**."
|
|
138
|
+
|
|
139
|
+
classification:
|
|
140
|
+
type: enum
|
|
141
|
+
values:
|
|
142
|
+
- UNCLASSIFIED
|
|
143
|
+
- CUI
|
|
144
|
+
- CONFIDENTIAL
|
|
145
|
+
- SECRET
|
|
146
|
+
- TOP SECRET
|
|
147
|
+
default: ""
|
|
148
|
+
ui:
|
|
149
|
+
group: classification
|
|
150
|
+
compact: true
|
|
151
|
+
description: "Classification marking, banner-printed at the top and bottom of every page. Leave blank for no banner, which is what an unclassified document normally carries. Mark per DAFI 16-1404, DoDM 5200.48 for CUI, and applicable DoD guidance."
|
|
152
|
+
variants:
|
|
153
|
+
CUI:
|
|
154
|
+
controlled_by:
|
|
155
|
+
type: plaintext
|
|
156
|
+
inline: true
|
|
157
|
+
example: SAF/AA
|
|
158
|
+
description: "Office or organization that designated this information as CUI. Required by DoDM 5200.48."
|
|
159
|
+
poc:
|
|
160
|
+
type: plaintext
|
|
161
|
+
inline: true
|
|
162
|
+
example: "Capt J. Smith, DSN 555-1234, john.smith@us.af.mil"
|
|
163
|
+
description: "Point of contact for CUI questions. Required by DoDM 5200.48."
|
|
164
|
+
category:
|
|
165
|
+
type: plaintext
|
|
166
|
+
inline: true
|
|
167
|
+
default: ""
|
|
168
|
+
description: "CUI category from the DoD CUI Registry, such as 'Privacy/MED', 'CTI', or 'PRVCY'. Leave blank to omit."
|
|
169
|
+
limited_dissemination:
|
|
170
|
+
type: plaintext
|
|
171
|
+
inline: true
|
|
172
|
+
default: ""
|
|
173
|
+
description: "Limited dissemination control shown in the indicator block, such as 'FEDONLY' or 'NOFORN'; rendered in uppercase. The banner's own suffix is the separate Dissemination field."
|
|
174
|
+
|
|
175
|
+
dissemination:
|
|
176
|
+
type: plaintext
|
|
177
|
+
inline: true
|
|
178
|
+
default: ""
|
|
179
|
+
ui:
|
|
180
|
+
group: classification
|
|
181
|
+
compact: true
|
|
182
|
+
description: "Suffix appended to the banner after a double slash, rendered in uppercase: 'NF' prints as CUI//NF. Leave blank for no suffix."
|
|
183
|
+
|
|
184
|
+
cc:
|
|
185
|
+
type: array
|
|
186
|
+
items:
|
|
187
|
+
type: plaintext
|
|
188
|
+
inline: true
|
|
189
|
+
default: []
|
|
190
|
+
example:
|
|
191
|
+
- 456 EXG/CC
|
|
192
|
+
ui:
|
|
193
|
+
group: additional
|
|
194
|
+
description: Offices or people receiving a courtesy copy, one per line.
|
|
195
|
+
|
|
196
|
+
attachments:
|
|
197
|
+
type: array
|
|
198
|
+
items:
|
|
199
|
+
type: richtext
|
|
200
|
+
inline: true
|
|
201
|
+
default: []
|
|
202
|
+
example:
|
|
203
|
+
- Squadron Newcomer Guide, 1 September 2026
|
|
204
|
+
ui:
|
|
205
|
+
group: additional
|
|
206
|
+
description: "Attachments in the order the letter mentions them. Describe each briefly; do not write 'as stated' or abbreviate. A lone attachment prints unnumbered, two or more are numbered. Italicize publication titles with *asterisks*."
|
|
207
|
+
|
|
208
|
+
font_size:
|
|
209
|
+
type: number
|
|
210
|
+
default: 12
|
|
211
|
+
ui:
|
|
212
|
+
group: additional
|
|
213
|
+
compact: true
|
|
214
|
+
description: "Body text size in points; decimals allowed, such as 11.5. AFH 33-337 specifies 12."
|
|
Binary file
|