@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.
@@ -1,4 +1,4 @@
1
- #import "@local/quillmark-helper:0.1.0": data, roster
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 plain-mark(body, ..style) = box(inset: (x: 0.5pt, y: 2pt), text(size: mark-size, ..style, body))
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) = text(size: s, weight: 600, ..style, 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, officer)),
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 { value }),
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
- #text(weight: 700)[#(start-year + i)]
388
+ #year
374
389
  #linebreak()
375
390
  #text(size: micro, fill: mute, offset)
376
- ] else { text(weight: 700)[#(start-year + i)] }
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.move_cycle #move-year], [to #data.move_cycle #move-year]),
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", roster(quals, "command")),
559
- ("Operations", roster(quals, "operations")),
560
- ("Staff & functional", roster(quals, "staff")),
561
- ("Education & PME", roster(quals, "education")),
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(v) = if blank(v) { dash } else { trim(v) }
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)[#s.year] },
590
- strat-cell(s.rater),
591
- ..if any-hlr { (strat-cell(s.hlr),) },
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
- #let listed(name, lines) = if lines.len() > 0 { (([#name], lines.map(trim).join[ · ]),) }
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", data.awards),
601
- ..vocabulary.map(((name, members)) => ([#name], members-row(members))),
602
- ..listed("Certifications", data.certifications),
603
- ..listed("Deployments", data.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.0": data
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
- fields:
114
- date:
115
- type: date
116
- default: ""
117
- ui:
118
- group: itinerary_entry
119
- compact: true
120
- description: "The date of this leg."
121
- departure_point:
122
- type: string
123
- default: ""
124
- ui:
125
- group: itinerary_entry
126
- compact: true
127
- description: "Where this leg starts."
128
- arrival_point:
129
- type: string
130
- default: ""
131
- ui:
132
- group: itinerary_entry
133
- compact: true
134
- description: "Where this leg ends."
135
- rest_length:
136
- type: string
137
- default: ""
138
- ui:
139
- group: itinerary_entry
140
- compact: true
141
- description: "How long you'll stay."
142
- mileage:
143
- type: string
144
- default: ""
145
- ui:
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.0": data
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])