@airmark/quiver 0.35.7 → 0.36.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/package.json +4 -4
- package/quills/af4141/0.1.0/Quill.yaml +39 -49
- package/quills/af4141/0.1.0/plate.typ +7 -7
- package/quills/afmc_moa/0.0.1/Quill.yaml +126 -153
- package/quills/afmc_moa/0.0.1/plate.typ +11 -11
- package/quills/classic_resume/0.1.0/Quill.yaml +34 -40
- package/quills/classic_resume/0.1.0/plate.typ +11 -11
- package/quills/cyber_career_plan/0.1.0/Quill.yaml +258 -287
- package/quills/cyber_career_plan/0.1.0/plate.typ +92 -70
- package/quills/daf1206/0.1.0/plate.typ +18 -18
- package/quills/daf4392/0.1.0/Quill.yaml +33 -38
- package/quills/daf4392/0.1.0/plate.typ +16 -16
- package/quills/usaf_letter/0.1.0/Quill.yaml +143 -158
- package/quills/usaf_letter/0.1.0/plate.typ +26 -28
- package/quills/usaf_memo/0.2.0/Quill.yaml +217 -247
- package/quills/usaf_memo/0.2.0/plate.typ +28 -28
- package/quills/usaf_memo/0.3.0/Quill.yaml +219 -245
- package/quills/usaf_memo/0.3.0/plate.typ +35 -36
- package/templates/cyber_career_plan.md +56 -0
- package/templates/templates.json +7 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#import "@local/quillmark-helper:0.
|
|
1
|
+
#import "@local/quillmark-helper:0.2.0" as qm
|
|
2
2
|
|
|
3
3
|
// ─── tokens ──────────────────────────────────────────────────────────────────
|
|
4
4
|
// Grays only, and none lighter than a copier holds: the chart is read from a
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
#set block(spacing: 0pt)
|
|
30
30
|
#set par(spacing: 0pt)
|
|
31
31
|
|
|
32
|
-
#let officer = data.at("name", default: "").trim()
|
|
32
|
+
#let officer = qm.data.at("name", default: "").trim()
|
|
33
33
|
#set document(
|
|
34
34
|
title: if officer == "" { "Career Plan" } else { "Career Plan — " + officer },
|
|
35
35
|
author: officer,
|
|
@@ -68,24 +68,25 @@
|
|
|
68
68
|
#let show-date(d) = if d == none { none } else { d.display("[day padding:none] [month repr:short] [year]") }
|
|
69
69
|
|
|
70
70
|
// ─── inputs ──────────────────────────────────────────────────────────────────
|
|
71
|
-
#let quals = data.qualifications
|
|
72
|
-
#let comm-yg = data.commissioning_yg
|
|
71
|
+
#let quals = qm.data.qualifications
|
|
72
|
+
#let comm-yg = qm.data.commissioning_yg
|
|
73
73
|
// 0 is read as blank: it was this field's blank before the field took `?`.
|
|
74
|
-
#let adj-yg = { let v = data.at("adjusted_yg", default: none); if v == 0 { none } else { v } }
|
|
74
|
+
#let adj-yg = { let v = qm.data.at("adjusted_yg", default: none); if v == 0 { none } else { v } }
|
|
75
75
|
#let adjusted = adj-yg != none and adj-yg != comm-yg
|
|
76
76
|
#let board-yg = if adjusted { adj-yg } else { comm-yg }
|
|
77
|
+
#let yg-field = if adjusted { "adjusted_yg" } else { "commissioning_yg" }
|
|
77
78
|
// A required year not yet filled in arrives as 0. With no start year the window
|
|
78
79
|
// opens on the year group; with neither, as on a new chart, the axis counts
|
|
79
80
|
// from YG+0 and prints no calendar years rather than the years 0 to 11. With a
|
|
80
81
|
// start year but no year group there is nothing to count the ladder from, so
|
|
81
82
|
// the axis prints calendar years alone and no milestone is drawn.
|
|
82
|
-
#let start-year = if data.timeline_start_year == 0 { board-yg } else { data.timeline_start_year }
|
|
83
|
+
#let start-year = if qm.data.timeline_start_year == 0 { board-yg } else { qm.data.timeline_start_year }
|
|
83
84
|
#let dated = start-year != 0
|
|
84
85
|
#let laddered = board-yg != 0 or not dated
|
|
85
86
|
#let yg-offset(k) = if k < 0 [YG#k] else [YG+#k]
|
|
86
87
|
// Eighteen is the widest the marks hold their columns at, and the schema cannot
|
|
87
88
|
// bound an integer, so a wider window draws at eighteen.
|
|
88
|
-
#let years = calc.clamp(data.timeline_years, 1, 18)
|
|
89
|
+
#let years = calc.clamp(qm.data.timeline_years, 1, 18)
|
|
89
90
|
#let cols = years * 2
|
|
90
91
|
#let last-year = start-year + years - 1
|
|
91
92
|
#let window-end = if dated [#last-year] else { yg-offset(last-year - board-yg) }
|
|
@@ -101,17 +102,17 @@
|
|
|
101
102
|
#let cycle-of(col) = if calc.rem(col, 2) == 0 { "Winter" } else { "Summer" }
|
|
102
103
|
#let col-of(year, cycle) = (year - start-year) * 2 + if cycle == "Winter" { 0 } else { 1 }
|
|
103
104
|
|
|
104
|
-
#let afsc = or-none(data.afsc)
|
|
105
|
-
#let duty = or-none(data.duty_title)
|
|
106
|
-
#let unit = or-none(data.unit)
|
|
107
|
-
#let move-year = data.at("move_year", default: none)
|
|
105
|
+
#let afsc = or-none(qm.data.afsc)
|
|
106
|
+
#let duty = or-none(qm.data.duty_title)
|
|
107
|
+
#let unit = or-none(qm.data.unit)
|
|
108
|
+
#let move-year = qm.data.at("move_year", default: none)
|
|
108
109
|
|
|
109
110
|
// Where the vectors branch. Everything left of it is the assignment you hold
|
|
110
111
|
// now, which is the same on every row, so it is drawn once across all of them.
|
|
111
112
|
// A move before the window is kept where it falls, so the tours after it are
|
|
112
113
|
// laid from their real start and cut at the left edge rather than shifted.
|
|
113
|
-
#let move-col = if move-year == none { 0 } else { col-of(move-year, data.move_cycle) }
|
|
114
|
-
#let current-span = if data.vectors.len() == 0 { 0 } else { calc.clamp(move-col, 0, cols) }
|
|
114
|
+
#let move-col = if move-year == none { 0 } else { col-of(move-year, qm.data.move_cycle) }
|
|
115
|
+
#let current-span = if qm.data.vectors.len() == 0 { 0 } else { calc.clamp(move-col, 0, cols) }
|
|
115
116
|
|
|
116
117
|
// ─── the ladder ──────────────────────────────────────────────────────────────
|
|
117
118
|
// Every 17X board and school gate, as an offset from the year group, counted
|
|
@@ -149,14 +150,18 @@
|
|
|
149
150
|
// A board prints heaviest, since it is a date you are ranked against; a course
|
|
150
151
|
// is a seat, and prints plain. A board holds to one line: at eighteen years
|
|
151
152
|
// "Lt Col board" runs a little into YG+13, which the ladder leaves empty.
|
|
153
|
+
// A click on a gate opens the tick that marks it done, or, on a board, the
|
|
154
|
+
// year group it counts from.
|
|
155
|
+
#let gate-field(by) = if by.len() > 0 { quals.at("$path") + "education." + by.first() } else { yg-field }
|
|
152
156
|
#let gate-mark(g) = {
|
|
153
|
-
let
|
|
157
|
+
let claim(body) = qm.region(gate-field(g.by), body)
|
|
158
|
+
let plain-mark(body, ..style) = box(inset: (x: 0.5pt, y: 2pt), text(size: mark-size, ..style, claim(body)))
|
|
154
159
|
if g.overdue {
|
|
155
160
|
plain-mark([#g.name, due #g.year], weight: 600, style: "italic")
|
|
156
161
|
} else if g.done {
|
|
157
162
|
plain-mark(strike(stroke: 0.5pt + mute, g.name), fill: mute)
|
|
158
163
|
} else if g.board {
|
|
159
|
-
box(fill: ink, inset: (x: mark-inset, y: 2pt), text(size: mark-size, weight: 700, fill: white, g.name.replace(" ", "\u{a0}")))
|
|
164
|
+
claim(box(fill: ink, inset: (x: mark-inset, y: 2pt), text(size: mark-size, weight: 700, fill: white, g.name.replace(" ", "\u{a0}"))))
|
|
160
165
|
} else {
|
|
161
166
|
plain-mark(g.name, fill: mute)
|
|
162
167
|
}
|
|
@@ -165,7 +170,7 @@
|
|
|
165
170
|
// A window is one span over its looks in the chart, split by year and named
|
|
166
171
|
// once. Looks already past are history and the span opens at the left edge;
|
|
167
172
|
// looks past the window run it open into the right.
|
|
168
|
-
#let window-mark(w, shown) = {
|
|
173
|
+
#let window-mark(w, shown) = qm.region(gate-field(w.by), {
|
|
169
174
|
let paint = if w.done { mute } else { ink }
|
|
170
175
|
let edge = 0.5pt + paint
|
|
171
176
|
box(
|
|
@@ -186,7 +191,7 @@
|
|
|
186
191
|
}),
|
|
187
192
|
),
|
|
188
193
|
)
|
|
189
|
-
}
|
|
194
|
+
})
|
|
190
195
|
|
|
191
196
|
// ─── vectors ─────────────────────────────────────────────────────────────────
|
|
192
197
|
#let ranks = ("Primary", "Alternate", "Backup")
|
|
@@ -224,8 +229,9 @@
|
|
|
224
229
|
})
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
#let vectors = data.vectors.enumerate().map(((i, v)) => (
|
|
232
|
+
#let vectors = qm.data.vectors.enumerate().map(((i, v)) => (
|
|
228
233
|
rank: ranks.at(i, default: "Vector " + str(i + 1)),
|
|
234
|
+
path: v.at("$path"),
|
|
229
235
|
track: or-none(v.track),
|
|
230
236
|
// A length is rounded to the half-year first; one that rounds outside 0.5 to
|
|
231
237
|
// 4 draws at the nearer end under a dagger the legend explains, since the
|
|
@@ -233,6 +239,7 @@
|
|
|
233
239
|
placed: lay(v.tours.map(t => {
|
|
234
240
|
let halves = int(calc.round(float(t.years) * 2))
|
|
235
241
|
(
|
|
242
|
+
path: t.at("$path"),
|
|
236
243
|
title: trim(t.title),
|
|
237
244
|
span: calc.clamp(halves, 1, 8),
|
|
238
245
|
out-of-range: halves < 1 or halves > 8,
|
|
@@ -252,10 +259,13 @@
|
|
|
252
259
|
// each line as full as it will go, so "JFHQ-C" never strands its "C" and a word
|
|
253
260
|
// too long for the block rules the setting out rather than running past its
|
|
254
261
|
// edge. A note holds to one line, and turned up the side nothing breaks.
|
|
255
|
-
#let block-text(title, notes, ..style) = layout(size => {
|
|
262
|
+
#let block-text(title, notes, field: none, ..style) = layout(size => {
|
|
256
263
|
let (w, h) = (size.width, size.height)
|
|
257
264
|
let words = plain(title).split(" ").filter(x => x != "")
|
|
258
|
-
let face(s, body) =
|
|
265
|
+
let face(s, body) = {
|
|
266
|
+
let run = text(size: s, weight: 600, ..style, body)
|
|
267
|
+
if field == none { run } else { qm.region(field, run) }
|
|
268
|
+
}
|
|
259
269
|
// The title in lines no wider than `room`, or in one line given none.
|
|
260
270
|
let lines(s, room) = words.fold((), (out, x) => {
|
|
261
271
|
if out.len() > 0 and (room == none or measure(face(s, out.last() + " " + x)).width <= room) {
|
|
@@ -263,7 +273,7 @@
|
|
|
263
273
|
} else { out + (x,) }
|
|
264
274
|
})
|
|
265
275
|
let typeset((s, n), room) = (
|
|
266
|
-
..if words.len() > 0 { (face(s, lines(s, room).join(linebreak())),) },
|
|
276
|
+
..if words.len() > 0 { (face(s, lines(s, room).map(l => [#l]).join(linebreak())),) },
|
|
267
277
|
..if n != none { (text(size: micro, fill: mute, n),) },
|
|
268
278
|
).join(linebreak(), default: [])
|
|
269
279
|
let within(c, room, w, h) = { let m = measure(typeset(c, room)); m.width <= w and m.height <= h }
|
|
@@ -297,7 +307,8 @@
|
|
|
297
307
|
if calc.rem(span, 2) == 0 { [#int(years) yr] } else { [#years yr] }
|
|
298
308
|
}
|
|
299
309
|
|
|
300
|
-
#let constraints = data.constraints.map(c => (
|
|
310
|
+
#let constraints = qm.data.constraints.map(c => (
|
|
311
|
+
path: c.at("$path"),
|
|
301
312
|
note: trim(c.note),
|
|
302
313
|
through: c.through,
|
|
303
314
|
end: calc.min(cols, (c.through - start-year + 1) * 2),
|
|
@@ -327,18 +338,18 @@
|
|
|
327
338
|
}
|
|
328
339
|
})
|
|
329
340
|
#let facts = (
|
|
330
|
-
("Year group", if board-yg != 0 [#board-yg], if adjusted [adjusted from #comm-yg]),
|
|
331
|
-
("Date of rank", show-date(data.date_of_rank), none),
|
|
332
|
-
("Arrived station", show-date(data.date_arrived_station), none),
|
|
341
|
+
("Year group", yg-field, if board-yg != 0 [#board-yg], if adjusted { qm.region("commissioning_yg")[adjusted from #comm-yg] }),
|
|
342
|
+
("Date of rank", "date_of_rank", show-date(qm.data.date_of_rank), none),
|
|
343
|
+
("Arrived station", "date_arrived_station", show-date(qm.data.date_arrived_station), none),
|
|
333
344
|
// A degree marked "(in progress)" says so in a note under it, as the year
|
|
334
345
|
// group's adjustment does, rather than breaking inside the parenthesis.
|
|
335
346
|
{
|
|
336
|
-
let degree = or-none(data.advanced_degree)
|
|
347
|
+
let degree = or-none(qm.data.advanced_degree)
|
|
337
348
|
let flat = plain(degree).trim()
|
|
338
349
|
let mark = "(in progress)"
|
|
339
350
|
if degree != none and flat.ends-with(mark) and flat.len() > mark.len() {
|
|
340
|
-
("Advanced degree", flat.slice(0, -mark.len()).trim(), [in progress])
|
|
341
|
-
} else { ("Advanced degree", degree, none) }
|
|
351
|
+
("Advanced degree", "advanced_degree", flat.slice(0, -mark.len()).trim(), [in progress])
|
|
352
|
+
} else { ("Advanced degree", "advanced_degree", degree, none) }
|
|
342
353
|
},
|
|
343
354
|
)
|
|
344
355
|
#grid(
|
|
@@ -347,14 +358,14 @@
|
|
|
347
358
|
row-gutter: 3pt,
|
|
348
359
|
align: (_, y) => if y == 0 { bottom } else { top },
|
|
349
360
|
// Raised clear of the line under it by the depth of its descenders.
|
|
350
|
-
pad(bottom: 3pt, text(size: display, weight: 800, tracking: -0.3pt
|
|
351
|
-
..facts.map(((name, ..)) => eyebrow(name)),
|
|
361
|
+
qm.region("name", pad(bottom: 3pt, text(size: display, weight: 800, tracking: -0.3pt)[#officer])),
|
|
362
|
+
..facts.map(((name, field, ..)) => qm.region(field, eyebrow(name))),
|
|
352
363
|
identity,
|
|
353
|
-
..facts.map(((_, value, note)) => stack(
|
|
364
|
+
..facts.map(((_, field, value, note)) => qm.region(field, stack(
|
|
354
365
|
spacing: 3pt,
|
|
355
|
-
text(size: lead, weight: 500, top-edge: hang, if value == none { dash } else
|
|
366
|
+
text(size: lead, weight: 500, top-edge: hang, if value == none { dash } else [#value]),
|
|
356
367
|
..if note != none { (text(size: micro, fill: mute, note),) },
|
|
357
|
-
)),
|
|
368
|
+
))),
|
|
358
369
|
)
|
|
359
370
|
|
|
360
371
|
// ═══ PLAN ════════════════════════════════════════════════════════════════════
|
|
@@ -367,13 +378,17 @@
|
|
|
367
378
|
// the axis
|
|
368
379
|
#rows.push(table.cell[])
|
|
369
380
|
#for i in range(years) {
|
|
370
|
-
let offset = yg-offset(start-year + i - board-yg)
|
|
381
|
+
let offset = qm.region(yg-field, yg-offset(start-year + i - board-yg))
|
|
382
|
+
let year = qm.region(
|
|
383
|
+
if i == years - 1 { "timeline_years" } else { "timeline_start_year" },
|
|
384
|
+
text(weight: 700)[#(start-year + i)],
|
|
385
|
+
)
|
|
371
386
|
rows.push(table.cell(colspan: 2, stroke: (bottom: 0.7pt + ink), inset: (x: 3pt, bottom: 3pt))[
|
|
372
387
|
#if not dated { text(weight: 700, offset) } else if laddered [
|
|
373
|
-
#
|
|
388
|
+
#year
|
|
374
389
|
#linebreak()
|
|
375
390
|
#text(size: micro, fill: mute, offset)
|
|
376
|
-
] else {
|
|
391
|
+
] else { year }
|
|
377
392
|
])
|
|
378
393
|
}
|
|
379
394
|
|
|
@@ -383,11 +398,11 @@
|
|
|
383
398
|
// on past its end, on one line, over nothing; one too long for that as well is
|
|
384
399
|
// cut at whichever edge leaves more of it, the bar's end or the page's.
|
|
385
400
|
#for (i, c) in constraints.enumerate() {
|
|
386
|
-
rows.push(if i == 0 { row-label[Constraints] } else { table.cell[] })
|
|
387
|
-
let note = [#text(size: micro, weight: 600)[#c.note]#text(size: micro, fill: mute)[#(" · through " + str(c.through))]]
|
|
401
|
+
rows.push(if i == 0 { row-label(qm.region("constraints")[Constraints]) } else { table.cell[] })
|
|
402
|
+
let note = [#text(size: micro, weight: 600)[#c.note]#qm.region(c.path + "through", text(size: micro, fill: mute)[#(" · through " + str(c.through))])]
|
|
388
403
|
// The first stands clear of the axis, which would otherwise close its top.
|
|
389
404
|
rows.push(table.cell(colspan: c.end, inset: (x: 1pt, top: if i == 0 { 3pt } else { 0.5pt }, bottom: 0.5pt))[
|
|
390
|
-
#layout(size => {
|
|
405
|
+
#qm.region(c.path.slice(0, -1), layout(size => {
|
|
391
406
|
// A bar that runs past the window is left open at its end, since the
|
|
392
407
|
// window's edge is not where it stops.
|
|
393
408
|
let open = c.through > last-year
|
|
@@ -399,7 +414,7 @@
|
|
|
399
414
|
box(width: 100%, height: 8.5pt, stroke: (bottom: constraint-stroke, right: if open { none } else { constraint-stroke }))
|
|
400
415
|
// Clipped across only: the inset keeps the ascenders and descenders.
|
|
401
416
|
place(left + bottom, dx: dx, box(width: calc.max(0pt, room), inset: (y: 2pt), clip: true, box(width: 10in, note)))
|
|
402
|
-
})
|
|
417
|
+
}))
|
|
403
418
|
])
|
|
404
419
|
rows += empty(cols - c.end)
|
|
405
420
|
}
|
|
@@ -410,7 +425,7 @@
|
|
|
410
425
|
#let lanes = if spans.len() > 0 { 2 } else { 1 }
|
|
411
426
|
#rows.push(table.cell(rowspan: lanes, align: left + horizon, stack(
|
|
412
427
|
spacing: 3pt,
|
|
413
|
-
rail-label[Eligibility],
|
|
428
|
+
qm.region(yg-field, rail-label[Eligibility]),
|
|
414
429
|
..gates.filter(g => g.overdue).map(gate-mark),
|
|
415
430
|
)))
|
|
416
431
|
#for i in range(years) {
|
|
@@ -433,7 +448,7 @@
|
|
|
433
448
|
// hold the way one does; with nothing left, the header still names the job. The
|
|
434
449
|
// unit is left to the header throughout. A move past the window leaves the
|
|
435
450
|
// block open at the window's edge, as a tour the window cuts is.
|
|
436
|
-
#let current-block = block(
|
|
451
|
+
#let current-block = qm.region("move_year", block(
|
|
437
452
|
width: 100%,
|
|
438
453
|
height: 100%,
|
|
439
454
|
stroke: (
|
|
@@ -445,9 +460,10 @@
|
|
|
445
460
|
inset: (x: 3pt, y: 2pt),
|
|
446
461
|
block-text(
|
|
447
462
|
if duty != none { duty } else { [Current assignment] },
|
|
448
|
-
([Current · to #data
|
|
463
|
+
([Current · to #qm.ink(qm.data, "move_cycle") #move-year], [to #qm.ink(qm.data, "move_cycle") #move-year]),
|
|
464
|
+
field: "duty_title",
|
|
449
465
|
),
|
|
450
|
-
)
|
|
466
|
+
))
|
|
451
467
|
|
|
452
468
|
// One row per vector. Its label says which it is, so every duty tour takes the
|
|
453
469
|
// one shade, and a school goes unshaded: a year in a classroom does not read as
|
|
@@ -460,10 +476,10 @@
|
|
|
460
476
|
if beyond.len() > 0 { sub.push([+#beyond.len() past #window-end]) }
|
|
461
477
|
|
|
462
478
|
rows.push(table.cell(stroke: top-rule, inset: (right: 5pt, y: 3pt))[
|
|
463
|
-
#block(width: 100%, height: 100%, track-label(
|
|
479
|
+
#qm.region(v.path + "track", block(width: 100%, height: 100%, track-label(
|
|
464
480
|
if v.track != none { v.track } else { [#v.rank] },
|
|
465
481
|
if sub.len() > 0 { sub.join[ · ] },
|
|
466
|
-
))
|
|
482
|
+
)))
|
|
467
483
|
])
|
|
468
484
|
|
|
469
485
|
if rank == 0 and current-span > 0 {
|
|
@@ -486,7 +502,7 @@
|
|
|
486
502
|
// The block is drawn inside its cell rather than as the cell's fill, so
|
|
487
503
|
// adjacent tours part by a gap and an edge. A tour the window cuts runs
|
|
488
504
|
// to the window's edge with that end open.
|
|
489
|
-
#block(
|
|
505
|
+
#qm.region(p.tour.path.slice(0, -1), block(
|
|
490
506
|
width: 100%,
|
|
491
507
|
height: 100%,
|
|
492
508
|
fill: if p.tour.school { none } else { shade },
|
|
@@ -499,10 +515,11 @@
|
|
|
499
515
|
inset: (x: 3pt, y: 2pt),
|
|
500
516
|
block-text(
|
|
501
517
|
p.tour.title,
|
|
502
|
-
range(notes.len(), 0, step: -1).map(n => notes.slice(0, n).join[ · ]),
|
|
518
|
+
range(notes.len(), 0, step: -1).map(n => qm.region(p.tour.path + "years", notes.slice(0, n).join[ · ])),
|
|
519
|
+
field: p.tour.path + "title",
|
|
503
520
|
style: if p.tour.school { "italic" } else { "normal" },
|
|
504
521
|
),
|
|
505
|
-
)
|
|
522
|
+
))
|
|
506
523
|
])
|
|
507
524
|
at = p.from + p.drawn
|
|
508
525
|
}
|
|
@@ -539,7 +556,7 @@
|
|
|
539
556
|
block(above: 5pt, pad(left: rail, grid(
|
|
540
557
|
columns: (1fr, auto),
|
|
541
558
|
text(size: micro, fill: mute, legend.join(h(12pt), default: [])),
|
|
542
|
-
if ahead > 0 { text(size: micro, fill: mute)[#ahead more milestone#if ahead > 1 [s] past #window-end] },
|
|
559
|
+
if ahead > 0 { qm.region(yg-field, text(size: micro, fill: mute)[#ahead more milestone#if ahead > 1 [s] past #window-end]) },
|
|
543
560
|
)))
|
|
544
561
|
}
|
|
545
562
|
|
|
@@ -555,19 +572,19 @@
|
|
|
555
572
|
// open boxes say what has not been done yet as plainly as the filled ones say
|
|
556
573
|
// what has. What is held leads its row, and a member holds together on its line.
|
|
557
574
|
#let vocabulary = (
|
|
558
|
-
("Leadership & command",
|
|
559
|
-
("Operations",
|
|
560
|
-
("Staff & functional",
|
|
561
|
-
("Education & PME",
|
|
575
|
+
("Leadership & command", "command"),
|
|
576
|
+
("Operations", "operations"),
|
|
577
|
+
("Staff & functional", "staff"),
|
|
578
|
+
("Education & PME", "education"),
|
|
562
579
|
)
|
|
563
|
-
#let member(m) = box[
|
|
580
|
+
#let member(m) = qm.region(m.path, box[
|
|
564
581
|
#let detail = if m.held { m.value.detail } else { "" }
|
|
565
582
|
#if m.held { held-mark } else { open-mark }#h(3pt)#text(
|
|
566
583
|
weight: if m.held { 600 } else { 400 },
|
|
567
584
|
fill: if m.held { ink } else { mute },
|
|
568
585
|
m.title,
|
|
569
586
|
)#if not blank(detail) [#h(2pt)#text(size: micro, style: "italic", fill: mute, trim(detail))]
|
|
570
|
-
]
|
|
587
|
+
])
|
|
571
588
|
#let members-row(rows) = {
|
|
572
589
|
(rows.filter(m => m.held) + rows.filter(m => not m.held)).map(member).join([#h(10pt) ])
|
|
573
590
|
}
|
|
@@ -576,9 +593,9 @@
|
|
|
576
593
|
// Reviewer strat on any row, that column goes rather than printing empty. The
|
|
577
594
|
// rows are set at the record's size, so the table reads as one of its rows. A
|
|
578
595
|
// year not yet filled in arrives as 0, sorts last and prints a dash.
|
|
579
|
-
#let strats = data.stratifications.sorted(key: s => -s.year)
|
|
596
|
+
#let strats = qm.data.stratifications.sorted(key: s => -s.year)
|
|
580
597
|
#let any-hlr = strats.any(s => not blank(s.hlr))
|
|
581
|
-
#let strat-cell(
|
|
598
|
+
#let strat-cell(s, key) = qm.region(s.at("$path") + key, if blank(s.at(key)) { dash } else { trim(s.at(key)) })
|
|
582
599
|
#let strats-table = table(
|
|
583
600
|
columns: (auto,) * (if any-hlr { 3 } else { 2 }),
|
|
584
601
|
column-gutter: 16pt,
|
|
@@ -586,21 +603,26 @@
|
|
|
586
603
|
stroke: none,
|
|
587
604
|
table.header([], eyebrow[Rater], ..if any-hlr { (eyebrow[HLR],) }),
|
|
588
605
|
..strats.map(s => (
|
|
589
|
-
if s.year == 0 { dash } else { text(weight: 700
|
|
590
|
-
strat-cell(s
|
|
591
|
-
..if any-hlr { (strat-cell(s
|
|
606
|
+
qm.region(s.at("$path") + "year", if s.year == 0 { dash } else { text(weight: 700, qm.ink(s, "year")) }),
|
|
607
|
+
strat-cell(s, "rater"),
|
|
608
|
+
..if any-hlr { (strat-cell(s, "hlr"),) },
|
|
592
609
|
)).flatten(),
|
|
593
610
|
)
|
|
594
611
|
|
|
595
612
|
// The two things a board reads first, the stratifications and the awards,
|
|
596
613
|
// lead; the experience follows.
|
|
597
|
-
|
|
614
|
+
// Each row's label claims the field the row prints, so a click on it opens that
|
|
615
|
+
// field as a click on the row does.
|
|
616
|
+
#let listed(name, field) = {
|
|
617
|
+
let lines = qm.data.at(field)
|
|
618
|
+
if lines.len() > 0 { (([#name], field, lines.map(trim).join[ · ]),) }
|
|
619
|
+
}
|
|
598
620
|
#let record = (
|
|
599
|
-
..if strats.len() > 0 { (([Stratifications], strats-table),) },
|
|
600
|
-
..listed("Awards",
|
|
601
|
-
..vocabulary.map(((name,
|
|
602
|
-
..listed("Certifications",
|
|
603
|
-
..listed("Deployments",
|
|
621
|
+
..if strats.len() > 0 { (([Stratifications], "stratifications", qm.region("stratifications", strats-table)),) },
|
|
622
|
+
..listed("Awards", "awards"),
|
|
623
|
+
..vocabulary.map(((name, key)) => ([#name], quals.at("$path") + key, members-row(qm.roster(quals, key)))),
|
|
624
|
+
..listed("Certifications", "certifications"),
|
|
625
|
+
..listed("Deployments", "deployments"),
|
|
604
626
|
)
|
|
605
627
|
|
|
606
628
|
#divider(14pt)
|
|
@@ -609,7 +631,7 @@
|
|
|
609
631
|
grid(
|
|
610
632
|
columns: (rail, 1fr),
|
|
611
633
|
row-gutter: 7pt,
|
|
612
|
-
..record.map(((name, body)) => (rail-label(name), body)).flatten(),
|
|
634
|
+
..record.map(((name, field, body)) => (rail-label(qm.region(field, name)), body)).flatten(),
|
|
613
635
|
)
|
|
614
636
|
}
|
|
615
637
|
|
|
@@ -632,7 +654,7 @@
|
|
|
632
654
|
// Every line box is exactly one pitch tall with its baseline at the same drop,
|
|
633
655
|
// so the typed notes land on the rules however many lines they run to, and the
|
|
634
656
|
// rail's label sits on the first.
|
|
635
|
-
#let typed = data.at("$body", default: "")
|
|
657
|
+
#let typed = qm.data.at("$body", default: "")
|
|
636
658
|
#let on-ruling(body) = {
|
|
637
659
|
set text(top-edge: baseline-drop, bottom-edge: baseline-drop - pitch)
|
|
638
660
|
set par(leading: 0pt, spacing: 0pt)
|
|
@@ -673,7 +695,7 @@
|
|
|
673
695
|
block(breakable: false, width: 100%, height: calc.max(room, need), {
|
|
674
696
|
grid(
|
|
675
697
|
columns: (rail, 1fr),
|
|
676
|
-
on-ruling(rail-label[Notes]),
|
|
698
|
+
on-ruling(qm.region("$body", rail-label[Notes])),
|
|
677
699
|
block(width: 100%, { ruling(rules); notes }),
|
|
678
700
|
)
|
|
679
701
|
place(bottom + left, signature)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#import "@local/quillmark-helper:0.
|
|
1
|
+
#import "@local/quillmark-helper:0.2.0" as qm
|
|
2
2
|
#import "@local/typst-daf1206:0.1.0": form
|
|
3
3
|
|
|
4
4
|
#set text(font: "NimbusRomNo9L")
|
|
@@ -6,39 +6,39 @@
|
|
|
6
6
|
// Map snake_case Quill `data` keys to generated `form.typ` parameter names.
|
|
7
7
|
#let vals = (:)
|
|
8
8
|
|
|
9
|
-
#if data.award != "" { vals.insert("AWARD", data.award) }
|
|
10
|
-
#if data.category_if_applicable != "" { vals.insert("CATEGORY_If_Applicable", data.category_if_applicable) }
|
|
11
|
-
#if data.award_period != "" { vals.insert("AWARD_PERIOD", data.award_period) }
|
|
12
|
-
#if data.rankname_of_nominee != "" {
|
|
13
|
-
vals.insert("RANKNAME_OF_NOMINEE_First_Middle_Initial_Last", data.rankname_of_nominee)
|
|
14
|
-
vals.insert("RANKNAME_OF_NOMINEE_First_Middle_Initial_Last_2", data.rankname_of_nominee)
|
|
9
|
+
#if qm.data.award != "" { vals.insert("AWARD", qm.data.award) }
|
|
10
|
+
#if qm.data.category_if_applicable != "" { vals.insert("CATEGORY_If_Applicable", qm.data.category_if_applicable) }
|
|
11
|
+
#if qm.data.award_period != "" { vals.insert("AWARD_PERIOD", qm.data.award_period) }
|
|
12
|
+
#if qm.data.rankname_of_nominee != "" {
|
|
13
|
+
vals.insert("RANKNAME_OF_NOMINEE_First_Middle_Initial_Last", qm.data.rankname_of_nominee)
|
|
14
|
+
vals.insert("RANKNAME_OF_NOMINEE_First_Middle_Initial_Last_2", qm.data.rankname_of_nominee)
|
|
15
15
|
}
|
|
16
|
-
#if data.majcom_fldcom_foa_or_dru != "" { vals.insert("MAJCOM_FLDCOM_FOA_OR_DRU", data.majcom_fldcom_foa_or_dru) }
|
|
17
|
-
#if data.dafsc_duty_title != "" { vals.insert("DAFSCDUTY_TITLE", data.dafsc_duty_title) }
|
|
18
|
-
#if data.nominees_telephone_dsn_commercial != "" {
|
|
19
|
-
vals.insert("NOMINEES_TELEPHONE__DSN__Commercial", data.nominees_telephone_dsn_commercial)
|
|
16
|
+
#if qm.data.majcom_fldcom_foa_or_dru != "" { vals.insert("MAJCOM_FLDCOM_FOA_OR_DRU", qm.data.majcom_fldcom_foa_or_dru) }
|
|
17
|
+
#if qm.data.dafsc_duty_title != "" { vals.insert("DAFSCDUTY_TITLE", qm.data.dafsc_duty_title) }
|
|
18
|
+
#if qm.data.nominees_telephone_dsn_commercial != "" {
|
|
19
|
+
vals.insert("NOMINEES_TELEPHONE__DSN__Commercial", qm.data.nominees_telephone_dsn_commercial)
|
|
20
20
|
}
|
|
21
|
-
#if data.unit_office_symbol_street_address_base_state_zip != "" {
|
|
21
|
+
#if qm.data.unit_office_symbol_street_address_base_state_zip != "" {
|
|
22
22
|
vals.insert(
|
|
23
23
|
"UNITOFFICE_SYMBOLSTREET_ADDRESSBASESTATEZIP_CODE",
|
|
24
|
-
data.unit_office_symbol_street_address_base_state_zip,
|
|
24
|
+
qm.data.unit_office_symbol_street_address_base_state_zip,
|
|
25
25
|
)
|
|
26
26
|
}
|
|
27
|
-
#if data.unit_commander_rank_name_and_telephone != "" {
|
|
27
|
+
#if qm.data.unit_commander_rank_name_and_telephone != "" {
|
|
28
28
|
vals.insert(
|
|
29
29
|
"RANKNAME_OF_UNIT_COMMANDER_First_Middle_Initial_LastCOMMANDERS_TELEPHONE_DSN__Commercial",
|
|
30
|
-
data.unit_commander_rank_name_and_telephone,
|
|
30
|
+
qm.data.unit_commander_rank_name_and_telephone,
|
|
31
31
|
)
|
|
32
32
|
}
|
|
33
|
-
#if "$body" in data {
|
|
33
|
+
#if "$body" in qm.data {
|
|
34
34
|
vals.insert(
|
|
35
35
|
"SPECIFIC_ACCOMPLISHMENTS_Use_Performance_Statements_IAW_DAFMAN_362806",
|
|
36
|
-
data.at("$body"),
|
|
36
|
+
qm.data.at("$body"),
|
|
37
37
|
)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
#let continued-key = "SPECIFIC_ACCOMPLISHMENTS_Use_Performance_Statements_IAW_DAFMAN_362806_Continued"
|
|
41
|
-
#for card in data.at("$cards") {
|
|
41
|
+
#for card in qm.data.at("$cards") {
|
|
42
42
|
if card.at("$kind", default: none) == "accomplishments_continued" and continued-key not in vals and "$body" in card {
|
|
43
43
|
vals.insert(continued-key, card.at("$body"))
|
|
44
44
|
}
|
|
@@ -106,43 +106,38 @@ card_kinds:
|
|
|
106
106
|
itinerary:
|
|
107
107
|
title: Proposed Travel Itinerary
|
|
108
108
|
description: "One leg of your trip. The form holds up to 10."
|
|
109
|
-
ui:
|
|
110
|
-
groups: [itinerary_entry]
|
|
111
109
|
body:
|
|
112
110
|
enabled: false
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
group: itinerary_entry
|
|
147
|
-
compact: true
|
|
148
|
-
description: "Approximate miles for this leg. Leave blank for flights."
|
|
111
|
+
sections:
|
|
112
|
+
itinerary_entry:
|
|
113
|
+
fields:
|
|
114
|
+
date:
|
|
115
|
+
type: date
|
|
116
|
+
default: ""
|
|
117
|
+
ui:
|
|
118
|
+
compact: true
|
|
119
|
+
description: "The date of this leg."
|
|
120
|
+
departure_point:
|
|
121
|
+
type: string
|
|
122
|
+
default: ""
|
|
123
|
+
ui:
|
|
124
|
+
compact: true
|
|
125
|
+
description: "Where this leg starts."
|
|
126
|
+
arrival_point:
|
|
127
|
+
type: string
|
|
128
|
+
default: ""
|
|
129
|
+
ui:
|
|
130
|
+
compact: true
|
|
131
|
+
description: "Where this leg ends."
|
|
132
|
+
rest_length:
|
|
133
|
+
type: string
|
|
134
|
+
default: ""
|
|
135
|
+
ui:
|
|
136
|
+
compact: true
|
|
137
|
+
description: "How long you'll stay."
|
|
138
|
+
mileage:
|
|
139
|
+
type: string
|
|
140
|
+
default: ""
|
|
141
|
+
ui:
|
|
142
|
+
compact: true
|
|
143
|
+
description: "Approximate miles for this leg. Leave blank for flights."
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#import "@local/quillmark-helper:0.
|
|
1
|
+
#import "@local/quillmark-helper:0.2.0" as qm
|
|
2
2
|
#import "@local/daf4392page2-pkg:0.1.0": form
|
|
3
3
|
|
|
4
4
|
#set text(font: "NimbusRomNo9L")
|
|
@@ -7,18 +7,18 @@
|
|
|
7
7
|
#let vals = (:)
|
|
8
8
|
|
|
9
9
|
// Mode of Transportation — enum string drives one of six checkboxes.
|
|
10
|
-
#let mode = data.transportation_mode
|
|
10
|
+
#let mode = qm.data.transportation_mode
|
|
11
11
|
#if mode != "" { vals.insert("mode_" + mode, true) }
|
|
12
12
|
|
|
13
13
|
// Header row
|
|
14
|
-
#if data.departure_date != none { vals.insert("departure_date", data.departure_date) }
|
|
15
|
-
#if data.final_destination != "" { vals.insert("final_destination", data.final_destination) }
|
|
14
|
+
#if qm.data.departure_date != none { vals.insert("departure_date", qm.data.departure_date) }
|
|
15
|
+
#if qm.data.final_destination != "" { vals.insert("final_destination", qm.data.final_destination) }
|
|
16
16
|
|
|
17
17
|
// Proposed Travel Itinerary — one card per row, up to 10 rows.
|
|
18
18
|
#let itin-cols = ("date", "departure_point", "arrival_point", "rest_length", "mileage")
|
|
19
19
|
#{
|
|
20
20
|
let row = 0
|
|
21
|
-
for card in data.at("$cards", default: ()) {
|
|
21
|
+
for card in qm.data.at("$cards", default: ()) {
|
|
22
22
|
if card.at("$kind", default: none) == "itinerary" and row < 10 {
|
|
23
23
|
let n = str(row + 1)
|
|
24
24
|
for col in itin-cols {
|
|
@@ -33,27 +33,27 @@
|
|
|
33
33
|
|
|
34
34
|
// Flight info — bold label prefixes a tentative/confirmed flight number.
|
|
35
35
|
#{
|
|
36
|
-
let d = str(data.dept_flight_num)
|
|
36
|
+
let d = str(qm.data.dept_flight_num)
|
|
37
37
|
if d != "" { vals.insert("dept_flight", [#text(weight: "bold")[Dept Flight:] #d]) }
|
|
38
|
-
let a = str(data.arrival_flight_num)
|
|
38
|
+
let a = str(qm.data.arrival_flight_num)
|
|
39
39
|
if a != "" { vals.insert("arrival_flight", [#text(weight: "bold")[Arr Flight:] #a]) }
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Notes
|
|
43
|
-
#if data.notes != "" { vals.insert("notes", data.notes) }
|
|
43
|
+
#if qm.data.notes != "" { vals.insert("notes", qm.data.notes) }
|
|
44
44
|
|
|
45
45
|
// Acknowledgements
|
|
46
|
-
#if data.organization != "" { vals.insert("organization", data.organization) }
|
|
47
|
-
#if data.briefed_date != none { vals.insert("briefed_date", data.briefed_date) }
|
|
48
|
-
#if data.briefee_name != "" { vals.insert("briefee_name", data.briefee_name) }
|
|
49
|
-
#if data.briefee_grade != "" { vals.insert("briefee_grade", data.briefee_grade) }
|
|
50
|
-
#if data.briefer_name != "" { vals.insert("briefer_name", data.briefer_name) }
|
|
51
|
-
#if data.briefer_grade != "" { vals.insert("briefer_grade", data.briefer_grade) }
|
|
46
|
+
#if qm.data.organization != "" { vals.insert("organization", qm.data.organization) }
|
|
47
|
+
#if qm.data.briefed_date != none { vals.insert("briefed_date", qm.data.briefed_date) }
|
|
48
|
+
#if qm.data.briefee_name != "" { vals.insert("briefee_name", qm.data.briefee_name) }
|
|
49
|
+
#if qm.data.briefee_grade != "" { vals.insert("briefee_grade", qm.data.briefee_grade) }
|
|
50
|
+
#if qm.data.briefer_name != "" { vals.insert("briefer_name", qm.data.briefer_name) }
|
|
51
|
+
#if qm.data.briefer_grade != "" { vals.insert("briefer_grade", qm.data.briefer_grade) }
|
|
52
52
|
|
|
53
53
|
// Emergency contact — bold label prefixes a "name: phone" pair.
|
|
54
54
|
#{
|
|
55
|
-
let en = str(data.emergency_contact_name)
|
|
56
|
-
let ep = str(data.emergency_contact_phone)
|
|
55
|
+
let en = str(qm.data.emergency_contact_name)
|
|
56
|
+
let ep = str(qm.data.emergency_contact_phone)
|
|
57
57
|
let contact = if en != "" and ep != "" { en + ": " + ep } else if en != "" { en } else { ep }
|
|
58
58
|
if contact != "" {
|
|
59
59
|
vals.insert("emergency_contact", [#text(weight: "bold")[EMERGENCY CONTACT:] #contact])
|