@airmark/quiver 0.30.0 → 0.32.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.
Files changed (30) hide show
  1. package/README.md +1 -0
  2. package/package.json +4 -4
  3. package/quills/af4141/0.1.0/plate.typ +11 -19
  4. package/quills/afmc_moa/0.0.1/Quill.yaml +289 -0
  5. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  6. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  7. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  8. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  9. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  10. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/attachment.typ +51 -0
  11. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/backmatter.typ +66 -0
  12. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/body.typ +126 -0
  13. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/config.typ +31 -0
  14. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/frontmatter.typ +74 -0
  15. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/lib.typ +41 -0
  16. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/mainmatter.typ +5 -0
  17. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/utils.typ +126 -0
  18. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/typst.toml +19 -0
  19. package/quills/afmc_moa/0.0.1/plate.typ +116 -0
  20. package/quills/daf1206/0.1.0/plate.typ +10 -10
  21. package/quills/daf4392/0.1.0/plate.typ +13 -13
  22. package/quills/usaf_memo/0.2.0/Quill.yaml +0 -2
  23. package/quills/usaf_memo/0.2.0/plate.typ +33 -28
  24. package/quills/usaf_memo/0.3.0/Quill.yaml +25 -44
  25. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +30 -25
  26. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +18 -6
  27. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +1 -0
  28. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +56 -31
  29. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -33
  30. package/quills/usaf_memo/0.3.0/plate.typ +108 -44
@@ -112,7 +112,6 @@ main:
112
112
  classification:
113
113
  type: enum
114
114
  values:
115
- - ""
116
115
  - UNCLASSIFIED
117
116
  - CUI
118
117
  - CONFIDENTIAL
@@ -123,6 +122,28 @@ main:
123
122
  group: classification
124
123
  compact: true
125
124
  description: "Select the classification marking shown in the header and footer banner. Unclassified documents typically omit this banner entirely — leave blank unless the document is CUI or classified. Mark per DAFI 16-1404 (DoDM 5200.48 for CUI) and applicable DoD guidance."
125
+ variants:
126
+ CUI:
127
+ controlled_by:
128
+ type: plaintext
129
+ inline: true
130
+ example: SAF/AA
131
+ description: "Office or organization that designated this information as CUI. Required by DoDM 5200.48."
132
+ poc:
133
+ type: plaintext
134
+ inline: true
135
+ example: "Capt J. Smith, DSN 555-1234, john.smith@us.af.mil"
136
+ description: "Point of contact for CUI questions. Required by DoDM 5200.48."
137
+ category:
138
+ type: plaintext
139
+ inline: true
140
+ default: ""
141
+ description: "CUI category from the DoD CUI Registry (e.g., 'Privacy/MED', 'CTI', 'PRVCY'). Leave blank to omit."
142
+ limited_dissemination:
143
+ type: plaintext
144
+ inline: true
145
+ default: ""
146
+ description: "Limited dissemination control shown in the indicator block (e.g., 'FEDONLY', 'NOFORN'). Independent from the banner suffix."
126
147
 
127
148
  dissemination:
128
149
  type: plaintext
@@ -133,42 +154,6 @@ main:
133
154
  compact: true
134
155
  description: "Banner dissemination suffix appended after double slash (e.g. 'NF' renders as CUI//NF in the header/footer). Leave blank for no suffix."
135
156
 
136
- cui_controlled_by:
137
- type: plaintext
138
- inline: true
139
- default: ""
140
- ui:
141
- group: classification
142
- compact: true
143
- description: "Office or organization that designated this information as CUI (e.g., 'SAF/AA'). Required by DoDM 5200.48 when classification is CUI."
144
-
145
- cui_category:
146
- type: plaintext
147
- inline: true
148
- default: ""
149
- ui:
150
- group: classification
151
- compact: true
152
- description: "CUI category from the DoD CUI Registry (e.g., 'Privacy/MED', 'CTI', 'PRVCY'). Leave blank to omit."
153
-
154
- cui_limited_dissemination:
155
- type: plaintext
156
- inline: true
157
- default: ""
158
- ui:
159
- group: classification
160
- compact: true
161
- description: "Limited dissemination control shown in the indicator block (e.g., 'FEDONLY', 'NOFORN'). Independent from the banner suffix above."
162
-
163
- cui_poc:
164
- type: plaintext
165
- inline: true
166
- default: ""
167
- ui:
168
- group: classification
169
- compact: true
170
- description: "Point of contact for CUI questions (e.g., 'Capt J. Smith, DSN 555-1234, john.smith@us.af.mil'). Required by DoDM 5200.48 when classification is CUI."
171
-
172
157
  references:
173
158
  type: array
174
159
  items:
@@ -291,26 +276,22 @@ card_kinds:
291
276
  ui:
292
277
  group: additional
293
278
  compact: true
294
- description: "'standard' (formal, same page), 'informal' (less formal routing), or 'separate_page' (begins on a new page)."
279
+ description: "'standard' (formal, same page), 'informal' (less formal routing), or 'separate_page' (begins on a new page). 'informal' prints no header, so an informal indorsement with an empty body is nothing but a signature block — it has no text of its own to hold it to the page, and can open a page alone. Give it a body or an action, or use 'standard'."
295
280
  action:
296
281
  type: enum
297
282
  values:
298
- - ""
299
283
  - undecided
300
284
  - approve
301
285
  - disapprove
302
- - undecided_concur
303
- - concur
304
- - nonconcur
305
286
  default: ""
306
287
  ui:
307
288
  group: additional
308
289
  compact: true
309
- description: "Decision taken by the endorser, shown as an either/or line with the choice circled. Leave blank to hide the line entirely. Approval authority: 'approve' or 'disapprove' (use 'undecided' to print Approve/Disapprove for the endorser to circle by hand). Coordinating official: 'concur' or 'nonconcur' (use 'undecided_concur' to print Concur/Nonconcur unmarked)."
290
+ description: "Decision taken by the endorser, shown as an either/or line with the choice underlined and the rejected option struck out. Leave blank to hide the line entirely. The wording is automatic: the last indorsement is the approval authority and reads Approve/Disapprove, while every earlier one is a coordinating official and reads Concur/Nonconcur. Use 'undecided' to print the pair unmarked for the endorser to mark by hand."
310
291
  date:
311
292
  type: date
312
293
  default: ""
313
294
  ui:
314
295
  group: additional
315
296
  compact: true
316
- 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 render a fill-in line for the endorser to date by hand when signing."
297
+ 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 render a fillable PDF text field for the endorser to type the date into when signing."
@@ -308,35 +308,40 @@
308
308
  // `context { for … }` block, where it does not combine with the
309
309
  // preceding header section's block-spacing the same way as a
310
310
  // top-level blank-line() call.
311
- // AFH 33-337 §11 rule applied below: "Avoid dividing a paragraph of
312
- // less than four lines between two pages"
313
311
  if any_emitted { blank-line() }
314
312
  any_emitted = true
315
313
  if i == total_count {
314
+ // AFH 33-337 "Signature Block": "Do not place the signature element on a
315
+ // continuation page by itself." The signature block that follows has no
316
+ // keep-with-previous of its own — Typst has no such property — so the
317
+ // anchor comes from this side: a sticky last element is carried onto the
318
+ // next page along with the signature instead of breaking away from it.
319
+ //
320
+ // Bounded, because a sticky block does not *split* at a page boundary,
321
+ // it relocates whole; applied to a half-page block that turns an
322
+ // ordinary mid-paragraph break into a wholesale jump. A third of the
323
+ // text block covers what a memorandum actually ends on — a closing
324
+ // paragraph, a short table — while leaving the long block to be divided
325
+ // by the break as before, which leaves its own tail on the continuation
326
+ // page for the signature to sit under.
327
+ //
328
+ // The budget is a fixed fraction rather than the space actually left
329
+ // below this element, which is the question one would rather ask. A
330
+ // position-dependent test cannot be answered honestly here: on the first
331
+ // layout pass `here()` resolves against an empty introspector and reports
332
+ // the top of the page, so every element measures as fitting and
333
+ // relocates — and from the top of its new page the same test agrees, so
334
+ // the wrong answer is a fixed point that later passes never revisit.
335
+ //
336
+ // (The superseded rule was sticky under four lines, reading §11 — "avoid
337
+ // dividing a paragraph of less than four lines between two pages" — as
338
+ // if it governed the signature. It does not; it governs how a paragraph
339
+ // may be divided. All the four-line gate did was leave every body ending
340
+ // in a longer paragraph, or in a table, free to strand its signature.)
316
341
  let available_width = page.width - spacing.margin * 2
317
-
318
- // Use the shared measured line stride used by blank-line spacing.
319
- let line_height = {
320
- let cached = LINE_STRIDE.get()
321
- if cached != none {
322
- cached
323
- } else {
324
- let one-line = measure(par(spacing: 0pt)[x]).height
325
- measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
326
- }
327
- }
328
- // Calculate last item's height
329
- let par_height = measure(final_par, width: available_width).height
330
-
331
- let estimated_lines = calc.ceil(par_height / line_height)
332
-
333
- if estimated_lines < 4 {
334
- // Short content (< 4 lines): make sticky to keep with signature
335
- block(sticky: true)[#final_par]
336
- } else {
337
- // Longer content (≥ 4 lines): use default breaking behavior
338
- block(breakable: true)[#final_par]
339
- }
342
+ let relocation_budget = (page.height - spacing.margin * 2) / 3
343
+ let element_height = measure(final_par, width: available_width).height
344
+ block(sticky: element_height <= relocation_budget)[#final_par]
340
345
  } else {
341
346
  // Wrap every non-last emission in a plain block so the document-wide
342
347
  // `set block(above: spacing.line)` rule contributes the same 0.5em
@@ -20,14 +20,22 @@
20
20
  signature_blank_lines: 4,
21
21
  signing_field: none,
22
22
  date: none,
23
+ // Fill-in widget for an omitted `date`, anchored in the date slot of the
24
+ // indorsement header (see `date-placeholder-slot`). `none` leaves the slot
25
+ // blank.
26
+ date_field: none,
23
27
  // Format of indorsement: "standard" (same page), "informal" (no header), or "separate_page" (starts on new page)
24
28
  format: "standard",
25
29
  // Decision action. `none` (default) displays no action line at all.
26
- // Approval authority pair: "approve" or "disapprove" circles the selected
27
- // option; "undecided" displays the pair with neither circled.
28
- // Coordination pair: "concur" or "nonconcur" circles the selected option;
29
- // "undecided_concur" displays the pair with neither circled.
30
+ // "approve" or "disapprove" underlines the selected option and strikes the
31
+ // other; "undecided" displays the pair with neither marked.
30
32
  action: none,
33
+ // Role this indorsement plays in the coordination chain, which selects the
34
+ // wording of the action line: the approval authority (the last indorsement
35
+ // in the chain) Approves / Disapproves, every coordinating official before
36
+ // it Concurs / Nonconcurs. The caller owns this because only it can see
37
+ // whether further indorsements follow.
38
+ approval_authority: false,
31
39
  content,
32
40
  ) = {
33
41
  // Validate format parameter
@@ -79,7 +87,7 @@
79
87
  let indorsement_number = counters.indorsement.get().at(0, default: 1)
80
88
  let indorsement_label = format-indorsement-number(indorsement_number)
81
89
 
82
- let ind_date = align(right)[#if actual_date != none { display-date(actual_date, memo-style: memo-style) } else { date-placeholder-line() }]
90
+ let ind_date = align(right)[#if actual_date != none { display-date(actual_date, memo-style: memo-style) } else { date-placeholder-slot(date_field) }]
83
91
 
84
92
  // Separate-page header body: restates the original memo's identity (FROM,
85
93
  // date, subject) on its own line, since the indorsement no longer shares a
@@ -156,7 +164,11 @@
156
164
 
157
165
  // Show action line only when an action decision is set (not `none`)
158
166
  if effective_action != none {
159
- render-action-line(effective_action, trailing-blank-line: not body_empty)
167
+ render-action-line(
168
+ effective_action,
169
+ approval-authority: approval_authority,
170
+ trailing-blank-line: not body_empty,
171
+ )
160
172
  }
161
173
 
162
174
  if not body_empty {
@@ -55,3 +55,4 @@
55
55
  #import "mainmatter.typ": mainmatter
56
56
  #import "backmatter.typ": backmatter
57
57
  #import "indorsement.typ": indorsement
58
+ #import "utils.typ": date-pattern
@@ -224,9 +224,6 @@
224
224
 
225
225
  #let render-signature-block(signature-lines, signature-blank-lines: 4, signing-field: none) = {
226
226
  signature-lines = ensure-array(signature-lines)
227
- // AFH 33-337: "fifth line below" = 4 blank lines between text and signature block.
228
- // breakable: false discourages orphaning the signature block onto a page by itself.
229
- blank-lines(signature-blank-lines)
230
227
  // AFH 33-337 allows two equivalent anchors: 4.5in from the left edge, or three
231
228
  // spaces right of page center. On 8.5in stock these coincide (page center =
232
229
  // 4.25in; three TNR-12pt spaces ≈ 0.25in), so we use 4.5in as the canonical
@@ -240,6 +237,13 @@
240
237
  let w = measure(text(hyphenate: false, line)).width
241
238
  if w > widest { widest = w }
242
239
  }
240
+ let stride = {
241
+ let s = LINE_STRIDE.get()
242
+ if s == none {
243
+ let one-line = measure(par(spacing: 0pt)[x]).height
244
+ measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
245
+ } else { s }
246
+ }
243
247
  // If the widest line would overflow the right margin at the standard
244
248
  // anchor, shift the block left just enough to fit. Clamp at 0 so the
245
249
  // block never crosses the left margin.
@@ -251,18 +255,30 @@
251
255
  default-pad
252
256
  }
253
257
  block(breakable: false)[
258
+ #let gap = stride * signature-blank-lines
259
+ // AFH 33-337: "fifth line below the last line of text" = four blank lines
260
+ // between the text and the signature block. Carried INSIDE the
261
+ // unbreakable block rather than emitted ahead of it, which keeps the gap
262
+ // and the block one indivisible unit. The total space above the block is
263
+ // identical either way — `block.above` still contributes the same 0.5em —
264
+ // but a gap left outside can be consumed at the foot of one page while
265
+ // the block starts at the top margin of the next, which is also what put
266
+ // the signing field off the page (see below).
267
+ #v(gap)
254
268
  #if signing-field != none {
255
- let stride = {
256
- let s = LINE_STRIDE.get()
257
- if s == none {
258
- let one-line = measure(par(spacing: 0pt)[x]).height
259
- measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
260
- } else { s }
261
- }
269
+ // The signing field covers those blank lines — where a signature is
270
+ // actually written — so it is placed over the gap. With the gap inside
271
+ // the block that is a downward offset from the block's own origin, and
272
+ // the field therefore travels with the block onto whatever page it
273
+ // lands on. The superseded form placed it at a NEGATIVE dy, reaching
274
+ // above the block into space that belongs to the previous page: when
275
+ // the block started at the top margin, the field was painted into the
276
+ // header band over the page number and the classification banner, or
277
+ // off the sheet entirely.
262
278
  place(
263
279
  dx: left-pad,
264
- dy: -(stride * signature-blank-lines),
265
- box(width: body-width - left-pad, height: stride * signature-blank-lines, signing-field),
280
+ dy: 0pt,
281
+ box(width: body-width - left-pad, height: gap, signing-field),
266
282
  )
267
283
  }
268
284
  #align(left)[
@@ -290,37 +306,46 @@
290
306
  // plays in a coordination chain: coordinating officials Concur / Nonconcur,
291
307
  // while the final approval authority Approves / Disapproves.
292
308
  //
293
- // Each pair also has an "undecided" form that renders both options plain, for
294
- // printing a memo the endorser circles by hand when signing.
309
+ // Which pair prints is not the endorser's to choose — it follows from the
310
+ // role, so the caller supplies the role via `approval-authority` and the same
311
+ // three `action` values (affirm, reject, or leave open) carry over both pairs.
312
+ //
313
+ // The "undecided" form renders both options plain, for printing a memo the
314
+ // endorser marks by hand when signing.
295
315
  //
296
316
  // Empty/none suppression is handled by the caller before this is invoked.
297
317
 
298
- // Maps each `action` value to the option pair it renders and the index of the
299
- // selected option within that pair (`none` selects neither).
300
- #let ACTION_FORMS = (
301
- "undecided": (("Approve", "Disapprove"), none),
302
- "approve": (("Approve", "Disapprove"), 0),
303
- "disapprove": (("Approve", "Disapprove"), 1),
304
- "undecided_concur": (("Concur", "Nonconcur"), none),
305
- "concur": (("Concur", "Nonconcur"), 0),
306
- "nonconcur": (("Concur", "Nonconcur"), 1),
318
+ // The option pair for each role, ordered (affirmative, negative).
319
+ #let APPROVAL_OPTIONS = ("Approve", "Disapprove")
320
+ #let COORDINATION_OPTIONS = ("Concur", "Nonconcur")
321
+
322
+ // Maps each `action` value to the index of the option it selects within
323
+ // whichever pair is rendered (`none` selects neither).
324
+ #let ACTION_SELECTIONS = (
325
+ "undecided": none,
326
+ "approve": 0,
327
+ "disapprove": 1,
307
328
  )
308
329
 
309
- #let render-action-line(action, trailing-blank-line: true) = {
330
+ #let render-action-line(action, approval-authority: false, trailing-blank-line: true) = {
310
331
  assert(
311
- action in ACTION_FORMS,
312
- message: "action must be one of " + ACTION_FORMS.keys().map(k => "\"" + k + "\"").join(", "),
332
+ action in ACTION_SELECTIONS,
333
+ message: "action must be one of " + ACTION_SELECTIONS.keys().map(k => "\"" + k + "\"").join(", "),
313
334
  )
314
- let (options, selected) = ACTION_FORMS.at(action)
315
- // Circle the selected option using a box with rounded corners; strike the
316
- // one it was chosen over. When neither is selected both render plain.
317
- // Use baseline parameter to maintain vertical text alignment.
335
+ let options = if approval-authority { APPROVAL_OPTIONS } else { COORDINATION_OPTIONS }
336
+ let selected = ACTION_SELECTIONS.at(action)
337
+ // Underline the selected option; strike the one it was chosen over. When
338
+ // neither is selected both render plain. An underline keeps the option on
339
+ // the line's own baseline — the box it replaces had to be nudged with
340
+ // `baseline` to sit straight — and reads as a mark made on the page, where a
341
+ // ruled rectangle in a PDF carrying real AcroForm widgets reads as one more
342
+ // fillable field.
318
343
  let render-option(index) = {
319
344
  let option = options.at(index)
320
345
  if selected == none {
321
346
  option
322
347
  } else if selected == index {
323
- box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[#option]
348
+ underline(option)
324
349
  } else {
325
350
  strike(option)
326
351
  }
@@ -129,17 +129,24 @@
129
129
  ]
130
130
  }
131
131
 
132
+ /// The date pattern a memo style prints, per AFH 33-337. Exported so a caller
133
+ /// that pre-formats the date matches this package instead of restating it.
134
+ ///
135
+ /// - memo-style (str): `"usaf"` or `"daf"`
136
+ /// -> str
137
+ #let date-pattern(memo-style: "usaf") = if memo-style == "daf" {
138
+ "[month repr:long] [day padding:none], [year]"
139
+ } else {
140
+ "[day padding:none] [month repr:long] [year]"
141
+ }
142
+
132
143
  /// Formats a date for the memo heading.
133
144
  ///
134
- /// Dispatches on the shape a date field can arrive in:
135
- /// - str: shown as-is (fixed text like a placeholder).
136
- /// - datetime: a native Typst datetime (e.g. `datetime.today()`) — USAF style
137
- /// `DD Month YYYY`, DAF style `Month DD, YYYY`.
138
- /// - dict: a Quillmark `date`/`datetime` field lowers to a click-to-edit value
139
- /// object `{ value: datetime, display: closure }`; `display` is a stored closure
140
- /// over `.display`, so call it through parentheses — a dict has no `.display` method.
145
+ /// - str, content: shown as-is (a fixed placeholder, or ink the caller already
146
+ /// formatted through [`date-pattern`]).
147
+ /// - datetime: USAF style `DD Month YYYY`; DAF style `Month DD, YYYY`.
141
148
  ///
142
- /// - date (str | datetime | dict): Date to format for display
149
+ /// - date (str | content | datetime): Date to format for display
143
150
  /// - memo-style (str): `"usaf"` or `"daf"`
144
151
  /// -> content
145
152
  #let display-date(date, memo-style: "usaf") = {
@@ -147,40 +154,41 @@
147
154
  memo-style in ("usaf", "daf"),
148
155
  message: "memo-style for display-date must be \"usaf\" or \"daf\"",
149
156
  )
150
- if type(date) == str {
157
+ // A caller that pre-formatted — to keep a click-to-edit region the package
158
+ // cannot mint itself — passes the finished ink through untouched.
159
+ if type(date) in (str, content) {
151
160
  date
152
161
  } else {
153
- let pattern = if memo-style == "daf" {
154
- "[month repr:long] [day padding:none], [year]"
155
- } else {
156
- "[day padding:none] [month repr:long] [year]"
157
- }
158
- if type(date) == datetime {
159
- date.display(pattern)
160
- } else {
161
- (date.display)(pattern)
162
- }
162
+ date.display(date-pattern(memo-style: memo-style))
163
163
  }
164
164
  }
165
165
 
166
- /// Renders a horizontal rule sized to fit a handwritten date.
167
- ///
168
- /// Used for indorsements whose signing date is unknown at compile time: the
169
- /// endorser writes the date on the line by hand when signing. The rule sits at
170
- /// the baseline with one line of height above it so handwritten text can be
171
- /// written on the line without colliding with surrounding header text.
172
- ///
173
- /// - width (length): Length of the fill-in rule; defaults to fit a long date
174
- /// such as "15 September 2026".
166
+ /// Reserves the space a date occupies, for a date unknown at compile time.
167
+ ///
168
+ /// Used for indorsements whose signing date is not known when the memo is
169
+ /// rendered: the endorser supplies it after the fact. The reserved box is one
170
+ /// line tall and sits on the baseline, so the slot lines up with where a
171
+ /// printed date would sit and takes exactly the space one would take.
172
+ ///
173
+ /// `field` is the caller's fill-in widget — the Quillmark helper's
174
+ /// `form-field(.., type: "text")`, which lowers to a fillable AcroForm text box
175
+ /// in PDF output. It is anchored at the slot's top-left corner, the corner the
176
+ /// widget's own rectangle grows right and down from, so a widget declared at
177
+ /// this slot's dimensions covers it exactly. `none` (a caller with no widget to
178
+ /// give, e.g. the package used outside Quillmark) leaves the slot blank.
179
+ ///
180
+ /// - field (content | none): Fill-in widget to anchor in the slot
181
+ /// - width (length): Width of the slot; defaults to fit a long date such as
182
+ /// "15 September 2026".
175
183
  /// -> content
176
- #let date-placeholder-line(width: 1in) = box(
184
+ #let date-placeholder-slot(field, width: 1in) = box(
177
185
  width: width,
178
186
  height: 1em,
179
- // Keep the rule on the line's baseline so it aligns with where the printed
180
- // date would sit. The 1em box height reserves the writing space above it.
181
- // (A positive baseline shift would drop the rule a full line too low.)
187
+ // Keep the slot's bottom edge on the line's baseline, where the printed date
188
+ // would sit; the 1em height then reserves the line above it.
189
+ // (A positive baseline shift would drop the box a full line too low.)
182
190
  baseline: 0pt,
183
- stroke: (bottom: 0.5pt + black),
191
+ if field != none { place(top + left, field) },
184
192
  )
185
193
 
186
194
  /// Gets the banner color for a classification marking.