@airmark/quiver 0.10.0 → 0.11.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airmark/quiver",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Airmark Quiver — Air Force and DAF document Quills for Quillmark.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -244,12 +244,13 @@ card_types:
244
244
  title: Action Decision
245
245
  type: string
246
246
  enum:
247
+ - none
247
248
  - undecided
248
249
  - approve
249
250
  - disapprove
250
251
  ui:
251
252
  group: Addressing
252
- description: "Action taken by the endorser. Use 'undecided' to display the Approve/Disapprove line with neither option circled, 'approve' or 'disapprove' to circle the selected option."
253
+ description: "Action taken by the endorser. Use 'none' to hide the Approve/Disapprove line entirely, 'undecided' to display the line with neither option circled, 'approve' or 'disapprove' to circle the selected option."
253
254
  attachments:
254
255
  title: Attachments for this endorsement
255
256
  type: array
@@ -263,8 +264,8 @@ card_types:
263
264
  group: Additional
264
265
  description: List of office symbols to receive copies of this endorsement.
265
266
  date:
266
- title: Date of endorsement (YYYY-MM-DD)
267
+ title: Date of endorsement (YYYY-MM-DD); defaults to today
267
268
  type: date
268
269
  ui:
269
270
  group: Additional
270
- description: Date of the endorsement. Leave blank to omit.
271
+ description: "YYYY-MM-DD. Date the endorser signs/forwards the memo (per AFH 33-337 Ch. 14, this is distinct from the originating memo's date). Leave blank to use today's date."
@@ -19,7 +19,7 @@
19
19
  // =============================================================================
20
20
  // AFH 33-337 §5: "Use 12 point Times New Roman font for text"
21
21
 
22
- #let DEFAULT_LETTERHEAD_FONTS = ("NimbusRomNo9L", "times new roman")
22
+ #let DEFAULT_LETTERHEAD_FONTS = ("Copperplate CC", "NimbusRomNo9L", "times new roman")
23
23
  #let DEFAULT_BODY_FONTS = ("NimbusRomNo9L", "times new roman") // AFH 33-337 §5: Times New Roman required
24
24
  #let LETTERHEAD_COLOR = rgb("#204093") // Faded USAF blue for letterhead
25
25
 
@@ -45,6 +45,18 @@
45
45
  let ind_from = first-or-value(from)
46
46
  let ind_for = to
47
47
 
48
+ // An empty body (e.g. a CARD with only an action selected and no markdown
49
+ // body) collapses to zero rendered layout via render-body's filter. To
50
+ // make the "empty body takes no layout space" guarantee end-to-end, also
51
+ // suppress the spacing the surrounding code reserves *for* the body:
52
+ // the header→body gap (when no action is present) and the action→body
53
+ // trailing gap. Without this, an empty body still leaves an extra
54
+ // blank-line stride above the signature, pushing it off the AFH 33-337
55
+ // "fifth line below the last line of text" anchor.
56
+ // The plate calls indorsement with `[]` when the markdown body is empty
57
+ // or whitespace-only; comparing against `[]` reliably detects that.
58
+ let body_empty = content == []
59
+
48
60
  if format != "informal" {
49
61
  // Step the counter BEFORE the context block to avoid read-then-update loop
50
62
  counters.indorsement.step()
@@ -89,20 +101,26 @@
89
101
  )
90
102
  }
91
103
  }
92
- blank-line()
104
+ // Header→content gap. Skipped when there is neither an action line nor
105
+ // body to follow — render-signature-block supplies its own 4-line gap.
106
+ if (action != none and action != "none") or not body_empty {
107
+ blank-line()
108
+ }
93
109
  }
94
110
 
95
111
  // Show action line only when an action decision is set (not `none`)
96
- if action != none {
97
- render-action-line(action)
112
+ if action != none and action != "none" {
113
+ render-action-line(action, trailing-blank-line: not body_empty)
98
114
  }
99
115
 
100
- context {
101
- let memo-style = {
102
- let items = query(metadata)
103
- if items.len() > 0 { items.last().value.at("memo_style", default: "usaf") } else { "usaf" }
116
+ if not body_empty {
117
+ context {
118
+ let memo-style = {
119
+ let items = query(metadata)
120
+ if items.len() > 0 { items.last().value.at("memo_style", default: "usaf") } else { "usaf" }
121
+ }
122
+ render-body(content, memo-style: memo-style)
104
123
  }
105
- render-body(content, memo-style: memo-style)
106
124
  }
107
125
 
108
126
  render-signature-block(signature_block, signature-blank-lines: signature_blank_lines)
@@ -199,9 +199,10 @@
199
199
  // action: "none" = no action line displayed (hidden), "undecided" = both options
200
200
  // rendered plain (no circle), "approve" = Approve circled,
201
201
  // "disapprove" = Disapprove circled. The action line is rendered when
202
- // action is "undecided", "approve", or "disapprove".
202
+ // action is "undecided", "approve", or "disapprove". "none" suppresses
203
+ // the line entirely (handled by the caller).
203
204
 
204
- #let render-action-line(action) = {
205
+ #let render-action-line(action, trailing-blank-line: true) = {
205
206
  assert(
206
207
  action in ("none", "undecided", "approve", "disapprove"),
207
208
  message: "action must be \"none\", \"undecided\", \"approve\", or \"disapprove\"",
@@ -230,10 +231,13 @@
230
231
  // using the same sticky-block pattern that body.typ applies to the last
231
232
  // paragraph, per AFH 33-337 §11 orphan-prevention rules.
232
233
  block(sticky: true)[#approve-text / #disapprove-text]
233
- // Trailing blank-line so the body's first paragraph (or signature block,
234
- // when the body is empty) sits one line below the action line, mirroring
235
- // the gap above it.
236
- blank-line()
234
+ // Trailing blank-line places the body's first paragraph one line below
235
+ // the action, mirroring the gap above it. Suppressed when the body is
236
+ // empty so the signature block's own 4-line gap lands on AFH 33-337's
237
+ // "fifth line below the last line of text" anchor.
238
+ if trailing-blank-line {
239
+ blank-line()
240
+ }
237
241
  }
238
242
 
239
243
  // =============================================================================
@@ -71,6 +71,21 @@
71
71
  // Indorsements - iterate through CARDS array and filter by CARD type
72
72
  #for card in data.CARDS {
73
73
  if card.CARD == "indorsement" {
74
+ // The quillmark helper leaves an unset/whitespace-only markdown body as
75
+ // the empty string `""`; only non-empty bodies are eval'd into content.
76
+ // Pass truly empty content (`[]`) in the empty case so indorsement can
77
+ // collapse the body's surrounding spacing.
78
+ let body = card.at("BODY", default: "")
79
+ let body_content = if type(body) == str { [] } else { body }
80
+ // Per AFH 33-337 Ch. 14, an indorsement is dated when the endorser signs
81
+ // it (distinct from the originating memo's date). Default to today when
82
+ // the card omits or leaves the date blank.
83
+ let card_date = card.at("date", default: none)
84
+ let resolved_date = if card_date == none or card_date == "" {
85
+ datetime.today()
86
+ } else {
87
+ card_date
88
+ }
74
89
  indorsement(
75
90
  from: card.at("from", default: ""),
76
91
  to: card.at("for", default: ""),
@@ -78,10 +93,9 @@
78
93
  ..if "attachments" in card { (attachments: card.attachments) },
79
94
  ..if "cc" in card { (cc: card.cc) },
80
95
  format: card.at("format", default: "standard"),
81
- ..if "date" in card { (date: card.date) },
96
+ date: resolved_date,
82
97
  ..if "action" in card { (action: card.action) },
83
- )[
84
- #card.BODY
85
- ]
98
+ body_content,
99
+ )
86
100
  }
87
101
  }