@airmark/quiver 0.32.2 → 0.34.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 (90) hide show
  1. package/README.md +63 -21
  2. package/package.json +9 -7
  3. package/quills/af4141/0.1.0/Quill.yaml +4 -13
  4. package/quills/af4141/0.1.0/example.md +53 -0
  5. package/quills/afmc_moa/0.0.1/Quill.yaml +39 -62
  6. package/quills/afmc_moa/0.0.1/example.md +114 -0
  7. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/config.typ +1 -1
  8. package/quills/classic_resume/0.0.1/Quill.yaml +281 -0
  9. package/quills/classic_resume/0.0.1/example.md +111 -0
  10. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/LICENSE +21 -0
  11. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Bold.ttf +0 -0
  12. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-BoldItalic.ttf +0 -0
  13. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Italic.ttf +0 -0
  14. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Regular.ttf +0 -0
  15. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt +94 -0
  16. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/components.typ +214 -0
  17. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/config.typ +61 -0
  18. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/layout.typ +86 -0
  19. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/lib.typ +7 -0
  20. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +37 -0
  21. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/typst.toml +26 -0
  22. package/quills/classic_resume/0.0.1/plate.typ +143 -0
  23. package/quills/cyber_ribbon_chart/0.0.1/Quill.yaml +327 -0
  24. package/quills/cyber_ribbon_chart/0.0.1/example.md +55 -0
  25. package/quills/cyber_ribbon_chart/0.0.1/plate.typ +609 -0
  26. package/quills/daf1206/0.1.0/Quill.yaml +15 -18
  27. package/quills/daf1206/0.1.0/example.md +55 -0
  28. package/quills/daf4392/0.1.0/Quill.yaml +16 -7
  29. package/quills/daf4392/0.1.0/example.md +51 -0
  30. package/quills/daf4392/0.1.0/plate.typ +2 -2
  31. package/quills/usaf_letter/0.1.0/Quill.yaml +179 -0
  32. package/quills/usaf_letter/0.1.0/assets/dod_seal.png +0 -0
  33. package/quills/usaf_letter/0.1.0/assets/dow_seal.png +0 -0
  34. package/quills/usaf_letter/0.1.0/example.md +35 -0
  35. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/.gitignore +7 -0
  36. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/LICENSE +201 -0
  37. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
  38. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/LICENSE.md +79 -0
  39. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LICENSE +102 -0
  40. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
  41. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  42. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  43. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  44. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  45. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  46. package/quills/{usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE → usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/OFL.txt} +2 -2
  47. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
  48. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Regular.ttf +0 -0
  49. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/backmatter.typ +32 -0
  50. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/config.typ +45 -0
  51. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +226 -0
  52. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/lib.typ +35 -0
  53. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/mainmatter.typ +140 -0
  54. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/primitives.typ +544 -0
  55. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/utils.typ +334 -0
  56. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/typst.toml +22 -0
  57. package/quills/usaf_letter/0.1.0/plate.typ +121 -0
  58. package/quills/usaf_memo/0.2.0/Quill.yaml +8 -41
  59. package/quills/usaf_memo/0.2.0/example.md +45 -0
  60. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
  61. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +1 -1
  62. package/quills/usaf_memo/0.3.0/Quill.yaml +51 -69
  63. package/quills/usaf_memo/0.3.0/example.md +48 -0
  64. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
  65. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/OFL.txt +93 -0
  66. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
  67. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Regular.ttf +0 -0
  68. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -17
  69. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +97 -126
  70. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +12 -45
  71. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +81 -87
  72. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +58 -66
  73. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +14 -36
  74. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +47 -19
  75. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +158 -111
  76. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -141
  77. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +3 -8
  78. package/quills/usaf_memo/0.3.0/plate.typ +78 -85
  79. package/templates/af4141.md +23 -0
  80. package/templates/appointment_letter.md +58 -0
  81. package/templates/daf1206.md +23 -0
  82. package/templates/daf4392.md +34 -0
  83. package/templates/deros_extension.md +40 -0
  84. package/templates/pass_request.md +48 -0
  85. package/templates/rebuttal.md +63 -0
  86. package/templates/templates.json +65 -0
  87. package/templates/usaf_template.md +30 -0
  88. package/templates/ussf_template.md +30 -0
  89. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
  90. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
@@ -0,0 +1,334 @@
1
+ // Spacing, normalization, and formatting helpers shared across the template.
2
+
3
+ #import "config.typ": CLASSIFICATION_COLORS, counters, paragraph-config, spacing
4
+
5
+ // Measured height of one wrapped body line, set once in `frontmatter`.
6
+ #let LINE_STRIDE = state("LINE_STRIDE")
7
+
8
+ /// The height one wrapped line of body text occupies.
9
+ ///
10
+ /// Read from `LINE_STRIDE` (cached in `frontmatter`), falling back to an inline
11
+ /// measurement when the cache is unset. Reads state, so callers must already be
12
+ /// in a `context`.
13
+ ///
14
+ /// -> length
15
+ #let line-stride() = {
16
+ let stride = LINE_STRIDE.get()
17
+ if stride == none {
18
+ let one-line = measure(par(spacing: 0pt)[x]).height
19
+ stride = measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
20
+ }
21
+ stride
22
+ }
23
+
24
+ /// Creates vertical spacing equivalent to multiple blank lines.
25
+ ///
26
+ /// Adds `count` wrapped-line strides on top of the natural inter-paragraph
27
+ /// gap, so a blank line occupies exactly the same vertical space as a line
28
+ /// produced by natural paragraph wrapping.
29
+ ///
30
+ /// - count (int): Number of blank lines to create
31
+ /// -> content
32
+ #let blank-lines(count) = {
33
+ if count <= 0 { return }
34
+ context { v(line-stride() * count) }
35
+ }
36
+
37
+ /// Creates vertical spacing equivalent to one blank line.
38
+ ///
39
+ /// -> content
40
+ #let blank-line() = blank-lines(1)
41
+
42
+ /// Checks if a value is "falsey" (none, false, empty array, empty string, or
43
+ /// empty content).
44
+ ///
45
+ /// A caller may hand a field over as a `str` or as content, so both empty
46
+ /// shapes count: `""` and `[]`. Empty content is what an emptied markup block
47
+ /// and `join()`-over-nothing both produce.
48
+ ///
49
+ /// - value (any): The value to check for "falsey" status
50
+ /// -> bool
51
+ #let falsey(value) = {
52
+ (
53
+ value == none
54
+ or value == false
55
+ or (type(value) == array and value.len() == 0)
56
+ or (type(value) == str and value == "")
57
+ or value == []
58
+ )
59
+ }
60
+
61
+ /// Scales content uniformly to fit a fixed-size box, preserving aspect ratio.
62
+ ///
63
+ /// Used for letterhead seals and emblems, whose source images vary in size.
64
+ ///
65
+ /// - width (length): Maximum width for the content (default: 2in)
66
+ /// - height (length): Maximum height for the content (default: 1in)
67
+ /// - alignment (alignment): Content alignment within the box (default: left+horizon)
68
+ /// - body (content): Content to scale and fit
69
+ /// -> content
70
+ #let fit-box(width: 2in, height: 1in, alignment: left + horizon, body) = context {
71
+ let s = measure(body)
72
+ let f = calc.min(width / s.width, height / s.height) * 100% // ratio
73
+ // `reflow: true` so layout respects the scaled size rather than the original.
74
+ box(width: width, height: height, clip: true)[
75
+ #align(alignment)[
76
+ #scale(f, reflow: true)[#body]
77
+ ]
78
+ ]
79
+ }
80
+
81
+ /// Shrinks content to fit within a maximum width, never enlarging it.
82
+ ///
83
+ /// Measures `body` at its current font size and scales it down uniformly only
84
+ /// when it is wider than `width`; content that already fits keeps its set size
85
+ /// (the scale is capped at 100%). Unlike `fit-box`, height flows naturally and
86
+ /// small content is never scaled up. Used for the letterhead caption (unit
87
+ /// designation): a long name is shrunk to the width that clears the seal rather
88
+ /// than running underneath it. Pass a non-wrapping `box` as `body` so the
89
+ /// measured width is the single-line width and the result stays on one line.
90
+ ///
91
+ /// Empty content measures 0pt wide and has no scale to compute, so it is returned
92
+ /// as-is rather than divided by; a non-positive `width` likewise admits no scale
93
+ /// that fits, so the body is left at its set size instead of being flipped or
94
+ /// collapsed.
95
+ ///
96
+ /// - width (length): Maximum width the content may occupy
97
+ /// - alignment (alignment): Horizontal placement within the reserved width
98
+ /// - body (content): Content to fit (typically a non-wrapping `box`)
99
+ /// -> content
100
+ #let fit-to-width(width, alignment: left, body) = context {
101
+ let s = measure(body)
102
+ if s.width <= 0pt or width <= 0pt { return body }
103
+ let f = calc.min(1, width / s.width) * 100% // ratio, capped so it only shrinks
104
+ box(width: width)[
105
+ #align(alignment)[
106
+ #scale(f, reflow: true)[#body]
107
+ ]
108
+ ]
109
+ }
110
+
111
+ /// The date pattern a memo style prints, per AFH 33-337. Exported so a caller
112
+ /// that pre-formats the date matches this package instead of restating it.
113
+ ///
114
+ /// - memo-style (str): `"usaf"` or `"daf"`
115
+ /// -> str
116
+ #let date-pattern(memo-style: "usaf") = if memo-style == "daf" {
117
+ "[month repr:long] [day padding:none], [year]"
118
+ } else {
119
+ "[day padding:none] [month repr:long] [year]"
120
+ }
121
+
122
+ /// Formats a date for the memo heading.
123
+ ///
124
+ /// - str, content: shown as-is (a fixed placeholder, or ink the caller already
125
+ /// formatted through [`date-pattern`]).
126
+ /// - datetime: USAF style `DD Month YYYY`; DAF style `Month DD, YYYY`.
127
+ ///
128
+ /// - date (str | content | datetime): Date to format for display
129
+ /// - memo-style (str): `"usaf"` or `"daf"`
130
+ /// -> content
131
+ #let display-date(date, memo-style: "usaf") = {
132
+ assert(
133
+ memo-style in ("usaf", "daf"),
134
+ message: "memo-style for display-date must be \"usaf\" or \"daf\"",
135
+ )
136
+ // A caller that pre-formatted — to keep a click-to-edit region the package
137
+ // cannot mint itself — passes the finished ink through untouched.
138
+ if type(date) in (str, content) {
139
+ date
140
+ } else {
141
+ date.display(date-pattern(memo-style: memo-style))
142
+ }
143
+ }
144
+
145
+ /// Reserves the space a date occupies, for a date unknown at compile time.
146
+ ///
147
+ /// Used for indorsements whose signing date is not known when the memo is
148
+ /// rendered: the endorser supplies it after the fact. The reserved box is one
149
+ /// line tall and sits on the baseline, so the slot lines up with where a
150
+ /// printed date would sit and takes exactly the space one would take.
151
+ ///
152
+ /// `field` is the caller's fill-in widget, an interactive element such as a
153
+ /// PDF form field. It is anchored at the slot's top-left corner, the corner
154
+ /// such a widget's own rectangle grows right and down from, so one declared at
155
+ /// this slot's dimensions covers it exactly. With no widget to anchor, the slot
156
+ /// rules its baseline instead, for the endorser to date by hand.
157
+ ///
158
+ /// - field (content | none): Fill-in widget to anchor in the slot
159
+ /// - width (length): Width of the slot; defaults to fit a long date such as
160
+ /// "15 September 2026".
161
+ /// -> content
162
+ #let date-placeholder-slot(field: none, width: 1in) = box(
163
+ width: width,
164
+ height: 1em,
165
+ // Keep the slot's bottom edge on the line's baseline, where the printed date
166
+ // would sit; the 1em height then reserves the line above it — the space a
167
+ // handwritten date is written in.
168
+ // (A positive baseline shift would drop the box a full line too low.)
169
+ baseline: 0pt,
170
+ stroke: if field == none { (bottom: 0.5pt + black) },
171
+ if field != none { place(top + left, field) },
172
+ )
173
+
174
+ /// Gets the banner color for a classification marking.
175
+ ///
176
+ /// Matches when `level` (trimmed) starts with a known prefix: TOP SECRET, SECRET,
177
+ /// CONFIDENTIAL, CUI, or UNCLASSIFIED. Otherwise returns black.
178
+ ///
179
+ /// - level (str): Marking string shown in header/footer
180
+ /// -> color
181
+ #let get-classification-level-color(level) = {
182
+ if level == none or type(level) != str {
183
+ return rgb(0, 0, 0)
184
+ }
185
+ let s = level.trim()
186
+ // Longest-prefix-first so e.g. "TOP SECRET" wins over "SECRET".
187
+ let level-order = ("TOP SECRET", "SECRET", "CONFIDENTIAL", "CUI", "UNCLASSIFIED")
188
+ for base-level in level-order {
189
+ if s.starts-with(base-level) {
190
+ return CLASSIFICATION_COLORS.at(base-level)
191
+ }
192
+ }
193
+ rgb(0, 0, 0)
194
+ }
195
+
196
+ /// Arranges one cell or an array of them into a `cols`-wide grid, filled
197
+ /// row-major. Used for the MEMORANDUM FOR recipient list.
198
+ ///
199
+ /// Cells may be content as readily as `str`, so a caller that builds each
200
+ /// recipient as markup — to keep the rendered glyphs traceable to their
201
+ /// source — passes it through unflattened.
202
+ ///
203
+ /// - content (str | content | array): Cell(s) to arrange in grid
204
+ /// - column-gutter (length): Space between columns (default: 0.5em)
205
+ /// - cols (int): Number of columns for the grid (default: 3)
206
+ /// -> grid
207
+ #let create-auto-grid(content, column-gutter: .5em, cols: 3) = {
208
+ let content-type = type(content)
209
+
210
+ if content-type != array {
211
+ content = (content,)
212
+ }
213
+
214
+
215
+ // Each row carries a trailing empty cell, so a row is `cols + 1` wide — the
216
+ // width both this loop and the tail padding below count against.
217
+ let cells = ()
218
+ let i = 0
219
+ for item in content {
220
+ i += 1
221
+ cells.push(item)
222
+ if calc.rem(i, cols) == 0 {
223
+ cells.push([])
224
+ }
225
+ }
226
+
227
+ let remainder = calc.rem(cells.len(), cols + 1)
228
+ if remainder != 0 {
229
+ let padding-needed = (cols + 1) - remainder
230
+ for _ in range(padding-needed) {
231
+ cells.push([])
232
+ }
233
+ }
234
+
235
+ grid(
236
+ columns: calc.max(1, cols) + 1,
237
+ column-gutter: .1fr,
238
+ row-gutter: spacing.line,
239
+ ..cells
240
+ )
241
+ }
242
+
243
+ /// Normalizes a value to an array, wrapping a scalar and mapping `none` to `()`.
244
+ ///
245
+ /// - value: Any value to normalize to array form
246
+ /// -> array
247
+ #let ensure-array(value) = {
248
+ if value == none {
249
+ ()
250
+ } else if type(value) == array {
251
+ value
252
+ } else {
253
+ (value,)
254
+ }
255
+ }
256
+
257
+ /// Normalizes a scalar-or-array field to a single stacked-lines **content**
258
+ /// value, one array element per line.
259
+ ///
260
+ /// A field may arrive as a `str` or as content, and `str(..)` on content is an
261
+ /// error. Joining with `linebreak()` rather than `"\n"` renders the same
262
+ /// stacked lines while accepting either shape.
263
+ ///
264
+ /// A blank field lands as empty content, which stays `falsey` for callers that
265
+ /// test the result.
266
+ ///
267
+ /// - value (any): Scalar or array of scalars/content to stack
268
+ /// -> content
269
+ ///
270
+ /// Examples:
271
+ /// - join-lines(([a], [b])) → [a] + linebreak() + [b]
272
+ /// - join-lines(none) → []
273
+ #let join-lines(value) = {
274
+ if value == none { return [] }
275
+ let items = if type(value) == array { value } else { (value,) }
276
+ // A `str` element is wrapped so the join is content-to-content throughout;
277
+ // mixing `str` and content under `+` is not a legal Typst addition.
278
+ let joined = items.map(item => [#item]).join(linebreak())
279
+ // `().join(..)` is `none`, not `[]` — coerce so an empty array stays `falsey`.
280
+ if joined == none { [] } else { joined }
281
+ }
282
+
283
+ /// The first element of an array, or the value itself when it is not one.
284
+ /// An empty array and `none` both yield `none`.
285
+ ///
286
+ /// - value: Any value to extract from
287
+ /// -> any
288
+ #let first-or-value(value) = {
289
+ if value == none {
290
+ none
291
+ } else if type(value) == array {
292
+ if value.len() > 0 {
293
+ value.at(0)
294
+ } else {
295
+ none
296
+ }
297
+ } else {
298
+ value
299
+ }
300
+ }
301
+
302
+
303
+ /// The ordinal suffix for an indorsement number: 1st, 2d, 3d, 4th, …
304
+ ///
305
+ /// Military correspondence writes "2d" and "3d" where ordinary English writes
306
+ /// "2nd" and "3rd"; 11th through 13th take "th" as usual.
307
+ ///
308
+ /// - number (int): The indorsement number (1, 2, 3, etc.)
309
+ /// -> str
310
+ #let get-ordinal-suffix(number) = {
311
+ let last-digit = calc.rem(number, 10)
312
+ let last-two-digits = calc.rem(number, 100)
313
+
314
+ if last-two-digits >= 11 and last-two-digits <= 13 {
315
+ "th"
316
+ } else if last-digit == 1 {
317
+ "st"
318
+ } else if last-digit == 2 {
319
+ "d"
320
+ } else if last-digit == 3 {
321
+ "d"
322
+ } else {
323
+ "th"
324
+ }
325
+ }
326
+
327
+ /// An indorsement's header label: "1st Ind", "2d Ind", "3d Ind", …
328
+ ///
329
+ /// - number (int): Indorsement sequence number (1, 2, 3, etc.)
330
+ /// -> str
331
+ #let format-indorsement-number(number) = {
332
+ let suffix = get-ordinal-suffix(number)
333
+ str(number) + suffix + " Ind"
334
+ }
@@ -0,0 +1,22 @@
1
+ [package]
2
+ name = "tonguetoquill-usaf-letter"
3
+ version = "0.1.0"
4
+ compiler = "0.15.1"
5
+ entrypoint = "src/lib.typ"
6
+ repository = "https://github.com/tonguetoquill/airmark-quiver"
7
+ authors = ["tonguetoquill"]
8
+ license = "Apache-2.0"
9
+ description = "Typeset USAF personal letters per AFH 33-337 'The Tongue and Quill'."
10
+ keywords = [
11
+ "USAF",
12
+ "Air Force",
13
+ "personal letter",
14
+ "letter",
15
+ "military",
16
+ "AFH 33-337",
17
+ "official",
18
+ "correspondence",
19
+ "formatting",
20
+ ]
21
+ categories = ["office"]
22
+ disciplines = ["politics", "business", "education"]
@@ -0,0 +1,121 @@
1
+ #import "@local/quillmark-helper:0.1.0": (
2
+ data, display, form-field, signature-field,
3
+ )
4
+ #import "@local/tonguetoquill-usaf-letter:0.1.0": (
5
+ backmatter, date-pattern, frontmatter, mainmatter,
6
+ )
7
+
8
+ // The letterhead arrives as one ordered array of lines. Element 0 is the title,
9
+ // set larger; the rest are caption lines beneath it. A single element renders
10
+ // the title alone — the package skips a falsey caption. An empty array renders
11
+ // no letterhead text at all, silently: the block is `place`d, so an empty title
12
+ // leaves no glyphs and takes no space from the flow, and the seal stands alone.
13
+ #let letterhead_lines = data.letterhead_title
14
+
15
+ // Body text size, in points. Also the height of a blank date's fill-in widget.
16
+ #let body_font_size = data.font_size * 1pt
17
+
18
+ // A blank date's fill-in slot: an empty AcroForm text box the signer types the
19
+ // date into, set in the body face and flush right, as the printed date would
20
+ // be. Wide enough for the longest date, "30 September 2026", which runs 8em in
21
+ // the body face.
22
+ //
23
+ // The slot claims a cap-height, the extent of a printed date, and the widget
24
+ // overhangs it about the line's middle, so a blank date takes no more room from
25
+ // the flow than a printed one and the page lays out the same either way.
26
+ #let date_slot_width = body_font_size * 8.5
27
+ #let date_slot(name, field) = context box(
28
+ width: date_slot_width,
29
+ height: measure[0].height,
30
+ place(horizon + left, form-field(
31
+ name,
32
+ type: "text",
33
+ width: date_slot_width,
34
+ height: body_font_size,
35
+ field: field,
36
+ font: "times",
37
+ size: body_font_size,
38
+ align: "right",
39
+ )),
40
+ )
41
+
42
+ #show: frontmatter.with(
43
+ letterhead-title: letterhead_lines.at(0, default: ""),
44
+ letterhead-caption: if letterhead_lines.len() > 1 { letterhead_lines.slice(1) } else { () },
45
+ letterhead-seal-subtitle: data.letterhead_seal_subtitle,
46
+ // A letter has no seal-less state, so the blank takes the default, DoW.
47
+ letterhead-seal: image(
48
+ if data.letterhead_seal == "dod" {
49
+ "assets/dod_seal.png"
50
+ } else {
51
+ "assets/dow_seal.png"
52
+ }
53
+ ),
54
+
55
+ // Date. `data.date` is the native `datetime` and would render identically,
56
+ // but its ink would be born inside the package and carry no schema address.
57
+ // `display` places the field's *content* projection instead: the glyphs are
58
+ // born in the generated helper, so the letter's date stays click-to-edit
59
+ // however deep the package formats it.
60
+ //
61
+ // A blank date is left for the signer: a letter is dated when it is signed,
62
+ // which is generally not when it is rendered, so the slot is fillable rather
63
+ // than stamped with the compile date. Passing it as the date also keeps it
64
+ // from `frontmatter`'s own `datetime.today()` fallback.
65
+ date: {
66
+ let authored = display("date", date-pattern())
67
+ if authored != none { authored } else { date_slot("Date", "date") }
68
+ },
69
+
70
+ letter-from: data.letter_from,
71
+ letter-for: data.letter_for,
72
+ salutation: data.salutation,
73
+
74
+ footer-tag-line: data.tag_line,
75
+
76
+ // The blank reads as no banner, which is what the package's own
77
+ // `classification-level: none` default means.
78
+ classification-level: data.classification.value,
79
+
80
+ dissemination: data.dissemination,
81
+
82
+ // CUI designation indicator block fields (DoDM 5200.48). `classification`
83
+ // declares a `CUI` variant, so these four exist only where the discriminant
84
+ // reads CUI, and the branch is what makes reading them total: inside it every
85
+ // declared field of that world is present, outside it none is. The package's
86
+ // own `cui_*: none` defaults cover the worlds that omit them.
87
+ ..if data.classification.value == "CUI" {
88
+ (
89
+ cui-controlled-by: data.classification.controlled_by,
90
+ cui-category: data.classification.category,
91
+ cui-limited-dissemination: data.classification.limited_dissemination,
92
+ cui-poc: data.classification.poc,
93
+ )
94
+ },
95
+
96
+ font-size: body_font_size,
97
+ )
98
+
99
+ // An empty body reaches the plate as `""`, and `mainmatter` would still set a
100
+ // blank paragraph for it, dropping the close two lines too far.
101
+ #if data.at("$body") != "" {
102
+ mainmatter[#data.at("$body")]
103
+ }
104
+
105
+ #backmatter(
106
+ complimentary-close: data.complimentary_close,
107
+ signature-block: data.signature_block,
108
+ // The widget sits at the bottom of AFH 33-337's four blank lines and is
109
+ // sized to two and a half of them, so the line and a half above it stays
110
+ // clear of the complimentary close without moving the block off the fifth
111
+ // line.
112
+ signing-field: signature-field(
113
+ "Signature",
114
+ field: "signature_block",
115
+ height: body_font_size * 2.5,
116
+ ),
117
+
118
+ ..if data.attachments.len() > 0 { (attachments: data.attachments) },
119
+
120
+ ..if data.cc.len() > 0 { (cc: data.cc) },
121
+ )
@@ -8,21 +8,14 @@ typst:
8
8
  plate_file: plate.typ
9
9
 
10
10
  main:
11
+ description: Top-level body paragraphs are numbered and nested bullets lettered automatically; do not number them by hand.
11
12
  ui:
12
13
  groups: [addressing, letterhead, classification, additional]
13
- body:
14
- example: |
15
- The first paragraph. Top-level paragraphs are auto-numbered; do not add manual numbering.
16
-
17
- - Nested bullets are automatically lettered.
18
14
  fields:
19
15
  memo_for:
20
16
  type: array
21
17
  items:
22
18
  type: string
23
- example:
24
- - ORG1/SYMBOL
25
- - ORG2/SYMBOL
26
19
  ui:
27
20
  group: addressing
28
21
  compact: true
@@ -33,19 +26,13 @@ main:
33
26
  items:
34
27
  type: string
35
28
  default: []
36
- example:
37
- - ORG/SYMBOL
38
- - Organization Name
39
- - 123 Street Ave
40
- - City ST 12345-6789
41
29
  ui:
42
30
  group: addressing
43
31
  compact: true
44
- description: "If recipients are on the same installation, use only the office symbol. For recipients on other installations, include the full mailing address to enable return correspondence. Omit entirely to produce a Memorandum for Record (no MEMO FROM line)."
32
+ description: "If recipients are on the same installation, use only the office symbol. For recipients on other installations, include the full mailing address, one line each, ending 'City ST 12345-6789', to enable return correspondence. Omit entirely to produce a Memorandum for Record (no MEMO FROM line)."
45
33
 
46
34
  subject:
47
35
  type: string
48
- example: Subject of the Memorandum
49
36
  ui:
50
37
  group: addressing
51
38
  description: Be brief and clear. Capitalize the first letter of each word except articles, prepositions, and conjunctions. Include suspense dates in parentheses if applicable.
@@ -54,13 +41,10 @@ main:
54
41
  type: array
55
42
  items:
56
43
  type: string
57
- example:
58
- - "FIRST M. LAST, Rank, USAF"
59
- - Duty Title
60
44
  ui:
61
45
  group: addressing
62
46
  compact: true
63
- description: "Line 1: Name in UPPERCASE as signed, grade, and service. Line 2: Duty title."
47
+ description: "Line 1: Name in UPPERCASE as signed, grade, and service, as in 'FIRST M. LAST, Capt, USAF'. Line 2: Duty title."
64
48
 
65
49
  letterhead_title:
66
50
  type: string
@@ -73,11 +57,9 @@ main:
73
57
  type: array
74
58
  items:
75
59
  type: string
76
- example:
77
- - HEADQUARTERS [UNIT NAME]
78
60
  ui:
79
61
  group: letterhead
80
- description: The full organization name of your unit.
62
+ description: The full organization name of your unit, as in HEADQUARTERS 123D EXAMPLE WING.
81
63
 
82
64
  letterhead_seal:
83
65
  type: enum
@@ -167,9 +149,6 @@ main:
167
149
  type: richtext
168
150
  inline: true
169
151
  default: []
170
- example:
171
- - "AFMAN 33-326, 25 November 2011, *Preparing Official Communications*"
172
- - "AFI 33-360, *Publications and Forms Management*"
173
152
  ui:
174
153
  group: additional
175
154
  description: "References, auto-lettered (a), (b), (c) per AFH 33-337. Cite each by organization, type, date, and title; italicize publication titles with *asterisks* (standard Markdown emphasis)."
@@ -179,20 +158,15 @@ main:
179
158
  items:
180
159
  type: string
181
160
  default: []
182
- example:
183
- - Rank and Name, ORG/SYMBOL
184
161
  ui:
185
162
  group: additional
186
- description: List office symbols of recipients to receive copies.
163
+ description: List office symbols of recipients to receive copies; for a person, 'Rank and Name, ORG/SYMBOL'.
187
164
 
188
165
  distribution:
189
166
  type: array
190
167
  items:
191
168
  type: string
192
169
  default: []
193
- example:
194
- - ORG1/SYMBOL
195
- - ORG2/SYMBOL
196
170
  ui:
197
171
  group: additional
198
172
  description: Complete list of recipients if 'SEE DISTRIBUTION' is used in the 'Memo For' field.
@@ -202,11 +176,9 @@ main:
202
176
  items:
203
177
  type: string
204
178
  default: []
205
- example:
206
- - Attachment description, YYYY MMM DD
207
179
  ui:
208
180
  group: additional
209
- description: List attachments in the order they are mentioned in the memo. Briefly describe each; do not use 'as stated' or abbreviations.
181
+ description: List attachments in the order they are mentioned in the memo. Briefly describe each and give its date; do not use 'as stated' or abbreviations.
210
182
 
211
183
  memo_style:
212
184
  type: enum
@@ -237,21 +209,19 @@ main:
237
209
 
238
210
  card_kinds:
239
211
  indorsement:
212
+ title: Routing indorsement
240
213
  description: Chain of routing endorsements. Each endorsement block adds an official response or forwarding action to the original memo.
241
214
  ui:
242
- title: Routing indorsement
243
215
  groups: [addressing, additional]
244
216
  fields:
245
217
  from:
246
218
  type: string
247
- example: ORG/SYMBOL
248
219
  ui:
249
220
  group: addressing
250
221
  compact: true
251
222
  description: Office symbol or Rank Name of the endorsing official.
252
223
  for:
253
224
  type: string
254
- example: ORG/SYMBOL
255
225
  ui:
256
226
  group: addressing
257
227
  compact: true
@@ -260,13 +230,10 @@ card_kinds:
260
230
  type: array
261
231
  items:
262
232
  type: string
263
- example:
264
- - "FIRST M. LAST, Rank, USAF"
265
- - Duty Title
266
233
  ui:
267
234
  group: addressing
268
235
  compact: true
269
- description: "Line 1: Name in UPPERCASE as signed, grade, and service. Line 2: Duty title."
236
+ description: "Line 1: Name in UPPERCASE as signed, grade, and service, as in 'FIRST M. LAST, Capt, USAF'. Line 2: Duty title."
270
237
  format:
271
238
  type: enum
272
239
  values:
@@ -0,0 +1,45 @@
1
+ ~~~
2
+ $quill: usaf_memo
3
+ memo_for:
4
+ - SEE DISTRIBUTION
5
+ memo_from:
6
+ - 123 CS/SCX
7
+ subject: "Annual Records Management Review (Suspense: 30 October 2026)"
8
+ date: 2026-09-15
9
+ signature_block:
10
+ - JOHN B. SMITH, Maj, USAF
11
+ - Director of Operations
12
+ letterhead_caption:
13
+ - 123D COMMUNICATIONS SQUADRON
14
+ references:
15
+ - "AFMAN 33-326, 25 November 2011, *Preparing Official Communications*"
16
+ - "AFI 33-360, *Publications and Forms Management*"
17
+ cc:
18
+ - 123 WG/CCE
19
+ distribution:
20
+ - 123 OG/CC
21
+ - 123 MXG/CC
22
+ - 123 MSG/CC
23
+ - 123 MDG/CC
24
+ attachments:
25
+ - Records Management Self-Inspection Checklist, 1 September 2026
26
+ ~~~
27
+
28
+ Each group reviews every office of record it owns against references (a) and (b) by the suspense in the subject line. Group records custodians confirm two things for each office:
29
+
30
+ - The file plan lists every series the office keeps, with its disposition authority.
31
+ - Files past cutoff are staged for retirement or destruction.
32
+
33
+ Return the checklist at the attachment, signed by the group commander, to 123 CS/SCX. Direct questions to 123 CS/SCX at DSN 555-0142.
34
+
35
+ ~~~
36
+ $kind: indorsement
37
+ from: 123 OG/CC
38
+ for: 123 CS/SCX
39
+ date: 2026-10-22
40
+ signature_block:
41
+ - RICHARD C. ROE, Col, USAF
42
+ - Commander
43
+ ~~~
44
+
45
+ The 123 OG completed its review on 21 October 2026. All four squadron file plans are current, and 123 OSS retires its two cutoff series by 15 November 2026.