@airmark/quiver 0.27.0 → 0.28.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @airmark/quiver
2
2
 
3
- A [Quillmark](https://github.com/nibsbin/quillmark) Source Quiver of Air Force
4
- and DAF official-document Quills, sourced from
3
+ A [Quillmark](https://github.com/nibsbin/quillmark) source quiver of Air Force
4
+ and DAF official-document quills, sourced from
5
5
  [tonguetoquill-collection](https://github.com/nibsbin/tonguetoquill-collection)
6
6
  and aligned to the current Quillmark spec.
7
7
 
@@ -20,28 +20,31 @@ and aligned to the current Quillmark spec.
20
20
  npm install @airmark/quiver @quillmark/quiver @quillmark/wasm
21
21
  ```
22
22
 
23
- This package ships only the Source Quiver assets (`Quiver.yaml` + `quills/`)
24
- and exposes no JavaScript API. All loading is performed by `@quillmark/quiver`,
25
- either from the on-disk package directory or from a packed archive fetched
26
- over HTTP.
23
+ This package ships only the source-quiver assets (`Quiver.yaml` + `quills/`) and
24
+ exposes no JavaScript API. Loading is `@quillmark/quiver`'s, rendering is
25
+ `@quillmark/wasm`'s.
27
26
 
28
27
  ## Usage
29
28
 
30
- ### Load from the exported directory
29
+ ### Load from the installed directory (Node)
31
30
 
32
- Resolve the package's `Quiver.yaml` via `import.meta.resolve` and hand it to
33
- `@quillmark/quiver`:
31
+ Resolve this package's `Quiver.yaml` from your own module — your dependencies are
32
+ reachable from you, not from the loader's install location — and hand its
33
+ directory to `fromDir`:
34
34
 
35
35
  ```ts
36
- import { fileURLToPath } from 'node:url';
37
- import path from 'node:path';
38
- import { Quillmark, Document } from '@quillmark/wasm';
39
- import { Quiver } from '@quillmark/quiver/node';
36
+ import { createRequire } from 'node:module';
37
+ import { dirname } from 'node:path';
38
+ import { Engine, init } from '@quillmark/wasm';
39
+ import { fromDir } from '@quillmark/quiver/node';
40
40
 
41
- const quiverYaml = fileURLToPath(import.meta.resolve('@airmark/quiver/Quiver.yaml'));
42
- const quiver = await Quiver.fromDir(path.dirname(quiverYaml));
41
+ // `init` is the only door to `Document`: there is no static export to reach early.
42
+ const { Document } = await init();
43
+
44
+ const root = dirname(createRequire(import.meta.url).resolve('@airmark/quiver/Quiver.yaml'));
45
+ const quiver = await fromDir(root);
46
+ const engine = new Engine();
43
47
 
44
- const engine = new Quillmark();
45
48
  const doc = Document.fromMarkdown(`~~~
46
49
  $quill: usaf_memo@0.2
47
50
  $kind: main
@@ -53,19 +56,31 @@ signature_block: ["FIRST M. LAST, Rank, USAF", "Duty Title"]
53
56
 
54
57
  Body of the memo.`);
55
58
 
56
- const quill = await quiver.getQuill(doc.quillRef, { engine });
57
- const { artifacts } = quill.render(doc, { format: 'pdf' });
59
+ const quill = await quiver.getQuill(doc.quillRef);
60
+ const { artifacts } = await engine.render(quill, doc, { format: 'pdf' });
58
61
  ```
59
62
 
60
- ### Load a packed Quiver via HTTP
63
+ A quill from `getQuill` is borrowed: every caller asking for that ref gets the
64
+ same instance, so it is not yours to `free()`.
65
+
66
+ ### Load over HTTP (browser)
67
+
68
+ A browser cannot read the source layout, so pack it at deploy time and serve the
69
+ output as static files:
70
+
71
+ ```ts
72
+ import { build } from '@quillmark/quiver/node'; // build step
73
+ await build(root, './public/quivers/airmark');
74
+ ```
61
75
 
62
- Publish a packed Quiver archive (e.g. as a CDN or GitHub release asset) and
63
- load it in the browser or any fetch-capable runtime through `@quillmark/quiver`.
64
- See the upstream `@quillmark/quiver` docs for the packed-format and HTTP API.
76
+ ```ts
77
+ import { Quiver } from '@quillmark/quiver'; // browser runtime
78
+ const quiver = await Quiver.fromBuiltUrl('/quivers/airmark/');
79
+ ```
65
80
 
66
81
  ## Layout
67
82
 
68
- This package is a [Source Quiver](https://www.npmjs.com/package/@quillmark/quiver)
83
+ This package is a [source quiver](https://www.npmjs.com/package/@quillmark/quiver)
69
84
  conforming to the canonical layout:
70
85
 
71
86
  ```
@@ -79,20 +94,53 @@ quills/
79
94
  packages/
80
95
  ```
81
96
 
82
- ## Testing
97
+ ## Working on a quill
83
98
 
84
- `quiver.test.js` at the repo root runs the
85
- [`@quillmark/quiver/testing`](https://www.npmjs.com/package/@quillmark/quiver)
86
- suite under the built-in [`node:test`](https://nodejs.org/api/test.html) runner,
87
- validating each `(quill, version)` pair through the full load +
88
- `engine.quill(tree)` compile pipeline:
99
+ Two verbs, and they answer different questions.
100
+
101
+ Both verbs are [`quillkit`](https://www.npmjs.com/package/quillkit)'s, the quill
102
+ author's toolchain. It resolves `@quillmark/quiver` and `@quillmark/wasm` out of
103
+ this collection's own `node_modules`, so the versions pinned here are the format
104
+ the quiver is packed in and the wasm the gate renders through.
105
+
106
+ **Does it work?** `quillkit test` loads the collection with `fromDir`, compiles
107
+ every quill, and renders each one's example document — the blueprint seeded from
108
+ the `example:` values in `Quill.yaml`. It is the gate CI runs, so a validation
109
+ failure surfaces here rather than on a consumer's build:
89
110
 
90
111
  ```bash
91
112
  npm install
92
113
  npm test
93
114
  ```
94
115
 
116
+ The gate renders nothing the schema did not write, so the coverage is the
117
+ `example:` block: a field with no example is a field no render exercises.
118
+
119
+ **What is it like to use?** `npm run dev` is `quillkit studio`: it packs this
120
+ quiver, serves the studio client over it, and repacks on every save — pick a
121
+ quill, edit the seeded document, watch it paint, read the diagnostics. The
122
+ client is quillkit's own, so there is nothing to install for it and nothing to
123
+ keep in step.
124
+
125
+ Reload to pick up a repack. Editing a quill into an invalid state is not a
126
+ failure of the loop: it packs, and the client says what is wrong with that quill
127
+ and where, while the others stay usable.
128
+
129
+ The two verbs answer to different authorities. `quillkit test` renders through
130
+ the wasm pinned here and is what CI blocks on; the client renders through the
131
+ wasm it was built against, and nothing at runtime reconciles the two. The gate
132
+ is authoritative, studio is advisory.
133
+
134
+ `npm run site` writes the arrangement a deploy serves into `site/` — the client
135
+ at the root, a built quiver at `quiver/` beneath it — without serving it. CI
136
+ uploads that on every run, so a pull request — a fork's included — is reviewed
137
+ by downloading the artifact and serving the directory.
138
+
139
+ `main` is deployed to GitHub Pages by `.github/workflows/studio.yml`, which runs
140
+ that same command and keeps the deploy here: nothing outside this repository
141
+ holds `pages: write`.
142
+
95
143
  ## License
96
144
 
97
- Apache-2.0. Individual Quills carry their own licensing terms; see the
98
- `packages/` directory inside each Quill for upstream font and template licenses.
145
+ Apache-2.0. Individual quills carry their own licensing terms; see the
146
+ `packages/` directory inside each quill for upstream font and template licenses.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airmark/quiver",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Airmark Quiver — Air Force and DAF document Quills for Quillmark.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -16,12 +16,14 @@
16
16
  "node": ">=24"
17
17
  },
18
18
  "devDependencies": {
19
- "@quillmark/quiver": "0.15.0",
20
- "@quillmark/wasm": "^0.91.0"
19
+ "@quillmark/quiver": "0.21.0",
20
+ "@quillmark/wasm": "^0.103.0",
21
+ "quillkit": "0.2.0"
21
22
  },
22
23
  "scripts": {
23
- "test": "quiver test",
24
- "preview": "quiver preview"
24
+ "test": "quillkit test",
25
+ "site": "quillkit site --out site",
26
+ "dev": "quillkit studio"
25
27
  },
26
28
  "keywords": [
27
29
  "quillmark",
@@ -2,9 +2,11 @@ quill:
2
2
  name: af4141
3
3
  version: 0.1.0
4
4
  backend: typst
5
- plate_file: plate.typ
6
5
  description: "AF Form 4141 — Individual's Record of Duties and Experience, Ground Environment Personnel"
7
6
 
7
+ typst:
8
+ plate_file: plate.typ
9
+
8
10
  main:
9
11
  body:
10
12
  enabled: false
@@ -36,58 +38,66 @@ card_kinds:
36
38
  description: Each card represents one row in the Record of Experience table. Rows fill page 1 (up to 16 rows) then page 2 (up to 21 rows), for a maximum of 37 rows.
37
39
  ui:
38
40
  title: Record of Experience
41
+ groups: [record]
39
42
  body:
40
43
  enabled: false
41
44
  fields:
42
45
  date:
43
- type: datetime
46
+ type: date
44
47
  default: ""
48
+ example: "2026-03-14"
45
49
  ui:
46
- group: Record
50
+ group: record
47
51
  description: Date of the action (column A).
48
52
 
49
53
  action:
50
54
  type: string
51
55
  default: ""
56
+ example: Upgraded to 5-level; assumed ADO duties
52
57
  ui:
53
- group: Record
58
+ group: record
54
59
  description: One type of action per line (column B).
55
60
 
56
61
  written_grade:
57
62
  type: string
58
63
  default: ""
64
+ example: "3"
59
65
  ui:
60
- group: Record
66
+ group: record
61
67
  compact: true
62
68
  description: Written grade, if applicable (column C).
63
69
 
64
70
  written_grade_date:
65
- type: datetime
71
+ type: date
66
72
  default: ""
73
+ example: "2026-02-28"
67
74
  ui:
68
- group: Record
75
+ group: record
69
76
  compact: true
70
77
  description: Date of written grade (column D). Omit or leave blank when there is no written grade.
71
78
 
72
79
  positional_grade:
73
80
  type: string
74
81
  default: ""
82
+ example: "2"
75
83
  ui:
76
- group: Record
84
+ group: record
77
85
  compact: true
78
86
  description: Positional grade, if applicable (column E).
79
87
 
80
88
  positional_grade_date:
81
- type: datetime
89
+ type: date
82
90
  default: ""
91
+ example: "2026-03-14"
83
92
  ui:
84
- group: Record
93
+ group: record
85
94
  compact: true
86
95
  description: Date of positional grade (column F). Omit or leave blank when there is no positional grade.
87
96
 
88
97
  auth_or_remarks:
89
98
  type: string
90
99
  default: ""
100
+ example: "J.A.D."
91
101
  ui:
92
- group: Record
102
+ group: record
93
103
  description: Authentication or remarks entry (column G).
@@ -11,7 +11,15 @@
11
11
  /// True for short/narrow fields with brief content (grades, ranks, dates).
12
12
  #let should-shrink-to-fit(display, width, height) = {
13
13
  let aspect = width / height
14
- let char-count = display.len()
14
+ // A date field's content is a single `text` element, and the string inside it
15
+ // is what decides the shrink, as a plain value's own does. Any other content
16
+ // (a styled label, a sequence) has no such string and counts as long, so only
17
+ // the dimension conditions apply.
18
+ let char-count = if type(display) == str {
19
+ display.len()
20
+ } else {
21
+ display.at("text", default: "0" * 99).len()
22
+ }
15
23
  char-count <= 10 or height < 20pt or aspect > 4.0
16
24
  }
17
25
 
@@ -81,8 +89,14 @@
81
89
  /// - field (dictionary): raw field entry from the schema
82
90
  #let render-field(field-type, value, width, height, field) = {
83
91
  if field-type == "text" {
84
- if value != none and str(value) != "" {
85
- render-text-field(str(value), width, height, 1.5pt, 1pt)
92
+ if value != none {
93
+ // A date field arrives as content carrying its click-to-edit region, so it
94
+ // is placed as it stands; a plain string is placed only when non-empty.
95
+ if type(value) == content {
96
+ render-text-field(value, width, height, 1.5pt, 1pt)
97
+ } else if str(value) != "" {
98
+ render-text-field(str(value), width, height, 1.5pt, 1pt)
99
+ }
86
100
  }
87
101
  } else if field-type == "checkbox" {
88
102
  if value == true {
@@ -3,13 +3,20 @@
3
3
 
4
4
  #set text(font: "NimbusRomNo9L")
5
5
 
6
- // `type: date` fields arrive as Typst `datetime`; PDF overlay expects strings.
7
- // Missing / blank dates: helper uses `none`; empty strings may also appear from input.
6
+ // A `type: date` field lowers to a click-to-edit value object
7
+ // `(value: datetime, display: closure)`; `display` is a stored closure on a dict
8
+ // rather than a method, so it is called through parentheses, and the content it
9
+ // returns carries the region that makes the glyphs clickable. A native datetime
10
+ // is still accepted. Missing / blank dates arrive as `none`; empty strings may
11
+ // also appear from input.
8
12
  #let form-cell(v) = {
13
+ let pattern = "[month padding:none]/[day padding:none]/[year]"
9
14
  if v == none { "" }
10
15
  else if type(v) == str and v == "" { "" }
11
- else if type(v) == datetime {
12
- v.display("[month padding:none]/[day padding:none]/[year]")
16
+ else if type(v) == dictionary {
17
+ (v.display)(pattern)
18
+ } else if type(v) == datetime {
19
+ v.display(pattern)
13
20
  } else {
14
21
  str(v)
15
22
  }
@@ -2,9 +2,11 @@ quill:
2
2
  name: daf1206
3
3
  version: 0.1.0
4
4
  backend: typst
5
- plate_file: plate.typ
6
5
  description: "DAF Form 1206 — Nomination for Award"
7
6
 
7
+ typst:
8
+ plate_file: plate.typ
9
+
8
10
  main:
9
11
  body:
10
12
  example: |
@@ -2,9 +2,11 @@ quill:
2
2
  name: daf4392
3
3
  version: 0.1.0
4
4
  backend: typst
5
- plate_file: plate.typ
6
5
  description: "DAF Form 4392 - Pre-Departure Safety Briefing (Page 2)"
7
6
 
7
+ typst:
8
+ plate_file: plate.typ
9
+
8
10
  main:
9
11
  body:
10
12
  enabled: false
@@ -16,7 +18,7 @@ main:
16
18
  description: "Matches the check box."
17
19
 
18
20
  departure_date:
19
- type: datetime
21
+ type: date
20
22
  default: ""
21
23
  example: "2026-06-01"
22
24
  ui:
@@ -59,7 +61,7 @@ main:
59
61
  description: "Unit / Organization."
60
62
 
61
63
  briefed_date:
62
- type: datetime
64
+ type: date
63
65
  default: ""
64
66
  example: "2026-05-22"
65
67
  ui:
@@ -96,41 +98,42 @@ card_kinds:
96
98
  description: "Each card represents one row in the travel itinerary table. Up to 10 rows are supported."
97
99
  ui:
98
100
  title: Proposed Travel Itinerary
101
+ groups: [itinerary_entry]
99
102
  body:
100
103
  enabled: false
101
104
  fields:
102
105
  date:
103
- type: datetime
106
+ type: date
104
107
  default: ""
105
108
  ui:
106
- group: Itinerary Entry
109
+ group: itinerary_entry
107
110
  compact: true
108
111
  description: "Date for the itinerary row."
109
112
  departure_point:
110
113
  type: string
111
114
  default: ""
112
115
  ui:
113
- group: Itinerary Entry
116
+ group: itinerary_entry
114
117
  compact: true
115
118
  description: "Point of departure."
116
119
  arrival_point:
117
120
  type: string
118
121
  default: ""
119
122
  ui:
120
- group: Itinerary Entry
123
+ group: itinerary_entry
121
124
  compact: true
122
125
  description: "Point of arrival."
123
126
  rest_length:
124
127
  type: string
125
128
  default: ""
126
129
  ui:
127
- group: Itinerary Entry
130
+ group: itinerary_entry
128
131
  compact: true
129
132
  description: "Duration of stay at arrival point"
130
133
  mileage:
131
134
  type: string
132
135
  default: ""
133
136
  ui:
134
- group: Itinerary Entry
137
+ group: itinerary_entry
135
138
  compact: true
136
139
  description: "Approximate mileage for this leg (leave blank if not applicable, e.g., flights)."
@@ -31,11 +31,22 @@
31
31
  }
32
32
 
33
33
  /// Format a date value for display as `DD Mon YYYY`.
34
+ ///
35
+ /// Dispatches on the shape a date can arrive in:
36
+ /// - dict: a Quillmark `date` field lowers to a click-to-edit value object
37
+ /// `(value: datetime, display: closure)`. `display` is a stored closure on a
38
+ /// dict rather than a method, so it is called through parentheses, and the
39
+ /// content it returns carries the region that makes the glyphs clickable.
40
+ /// - datetime: a native Typst datetime, from a plate building its own value.
41
+ /// - str: shown as-is.
34
42
  #let format-date(value) = {
43
+ let pattern = "[day padding:zero] [month repr:short] [year]"
35
44
  if value == none {
36
45
  none
46
+ } else if type(value) == dictionary {
47
+ (value.display)(pattern)
37
48
  } else if type(value) == datetime {
38
- value.display("[day padding:zero] [month repr:short] [year]")
49
+ value.display(pattern)
39
50
  } else if type(value) == str and value == "" {
40
51
  ""
41
52
  } else {
@@ -45,11 +56,17 @@
45
56
 
46
57
  /// Should this field shrink text to a single line rather than word-wrap?
47
58
  /// True for short/narrow fields with brief content (grades, ranks, dates).
48
- /// `display` may be a string or content; char-count is skipped for content.
49
59
  #let should-shrink-to-fit(display, width, height) = {
50
60
  let aspect = width / height
51
- // content has no .len(); treat as "long" so only the dimension conditions apply
52
- let char-count = if type(display) == str { display.len() } else { 99 }
61
+ // A date field's content is a single `text` element, and the string inside it
62
+ // is what decides the shrink, as a plain value's own does. Any other content
63
+ // (a styled label, a sequence) has no such string and counts as long, so only
64
+ // the dimension conditions apply.
65
+ let char-count = if type(display) == str {
66
+ display.len()
67
+ } else {
68
+ display.at("text", default: "0" * 99).len()
69
+ }
53
70
  char-count <= 10 or height < 20pt or aspect > 4.0
54
71
  }
55
72
 
@@ -2,10 +2,14 @@ quill:
2
2
  name: usaf_memo
3
3
  version: 0.2.0
4
4
  backend: typst
5
- plate_file: plate.typ
6
5
  description: U.S. Air Force and Space Force Official Memorandum
7
6
 
7
+ typst:
8
+ plate_file: plate.typ
9
+
8
10
  main:
11
+ ui:
12
+ groups: [addressing, letterhead, classification, additional]
9
13
  body:
10
14
  example: |
11
15
  The first paragraph. Top-level paragraphs are auto-numbered; do not add manual numbering.
@@ -20,7 +24,8 @@ main:
20
24
  - ORG1/SYMBOL
21
25
  - ORG2/SYMBOL
22
26
  ui:
23
- group: Addressing
27
+ group: addressing
28
+ compact: true
24
29
  description: "Organization/office symbol in UPPERCASE. To address a specific person, add their rank and name in parentheses (e.g., 'ORG/SYMBOL (LT COL JANE DOE)'). For many recipients, enter 'SEE DISTRIBUTION' and list them in the Distribution field."
25
30
 
26
31
  memo_from:
@@ -34,14 +39,15 @@ main:
34
39
  - 123 Street Ave
35
40
  - City ST 12345-6789
36
41
  ui:
37
- group: Addressing
42
+ group: addressing
43
+ compact: true
38
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)."
39
45
 
40
46
  subject:
41
47
  type: string
42
48
  example: Subject of the Memorandum
43
49
  ui:
44
- group: Addressing
50
+ group: addressing
45
51
  description: Be brief and clear. Capitalize the first letter of each word except articles, prepositions, and conjunctions. Include suspense dates in parentheses if applicable.
46
52
 
47
53
  signature_block:
@@ -52,14 +58,15 @@ main:
52
58
  - "FIRST M. LAST, Rank, USAF"
53
59
  - Duty Title
54
60
  ui:
55
- group: Addressing
61
+ group: addressing
62
+ compact: true
56
63
  description: "Line 1: Name in UPPERCASE as signed, grade, and service. Line 2: Duty title."
57
64
 
58
65
  letterhead_title:
59
66
  type: string
60
67
  default: DEPARTMENT OF THE AIR FORCE
61
68
  ui:
62
- group: Letterhead
69
+ group: letterhead
63
70
  description: Change only for Joint Commands or DoW agencies.
64
71
 
65
72
  letterhead_caption:
@@ -69,7 +76,7 @@ main:
69
76
  example:
70
77
  - HEADQUARTERS [UNIT NAME]
71
78
  ui:
72
- group: Letterhead
79
+ group: letterhead
73
80
  description: The full organization name of your unit.
74
81
 
75
82
  letterhead_seal:
@@ -79,7 +86,7 @@ main:
79
86
  - dod
80
87
  default: dow
81
88
  ui:
82
- group: Letterhead
89
+ group: letterhead
83
90
  compact: true
84
91
  description: Department of War (DoW) or Department of Defense (DoD) seal shown in the letterhead.
85
92
 
@@ -87,15 +94,16 @@ main:
87
94
  type: string
88
95
  default: ""
89
96
  ui:
90
- group: Letterhead
97
+ group: letterhead
91
98
  compact: true
92
99
  description: Bold-caps line below the seal; blank to omit.
93
100
 
94
101
  tag_line:
95
- type: markdown
102
+ type: richtext
103
+ inline: true
96
104
  default: ""
97
105
  ui:
98
- group: Letterhead
106
+ group: letterhead
99
107
  compact: true
100
108
  description: "Organizational motto at the bottom of the page. Supports Markdown emphasis (e.g. *italics*, **bold**)."
101
109
 
@@ -110,7 +118,7 @@ main:
110
118
  - TOP SECRET
111
119
  default: ""
112
120
  ui:
113
- group: Classification
121
+ group: classification
114
122
  compact: true
115
123
  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."
116
124
 
@@ -118,7 +126,7 @@ main:
118
126
  type: string
119
127
  default: ""
120
128
  ui:
121
- group: Classification
129
+ group: classification
122
130
  compact: true
123
131
  description: "Banner dissemination suffix appended after double slash (e.g. 'NF' renders as CUI//NF in the header/footer). Leave blank for no suffix."
124
132
 
@@ -126,7 +134,7 @@ main:
126
134
  type: string
127
135
  default: ""
128
136
  ui:
129
- group: Classification
137
+ group: classification
130
138
  compact: true
131
139
  description: "Office or organization that designated this information as CUI (e.g., 'SAF/AA'). Required by DoDM 5200.48 when classification is CUI."
132
140
 
@@ -134,7 +142,7 @@ main:
134
142
  type: string
135
143
  default: ""
136
144
  ui:
137
- group: Classification
145
+ group: classification
138
146
  compact: true
139
147
  description: "CUI category from the DoD CUI Registry (e.g., 'Privacy/MED', 'CTI', 'PRVCY'). Leave blank to omit."
140
148
 
@@ -142,7 +150,7 @@ main:
142
150
  type: string
143
151
  default: ""
144
152
  ui:
145
- group: Classification
153
+ group: classification
146
154
  compact: true
147
155
  description: "Limited dissemination control shown in the indicator block (e.g., 'FEDONLY', 'NOFORN'). Independent from the banner suffix above."
148
156
 
@@ -150,20 +158,21 @@ main:
150
158
  type: string
151
159
  default: ""
152
160
  ui:
153
- group: Classification
161
+ group: classification
154
162
  compact: true
155
163
  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."
156
164
 
157
165
  references:
158
166
  type: array
159
167
  items:
160
- type: markdown
168
+ type: richtext
169
+ inline: true
161
170
  default: []
162
171
  example:
163
172
  - "AFMAN 33-326, 25 November 2011, *Preparing Official Communications*"
164
173
  - "AFI 33-360, *Publications and Forms Management*"
165
174
  ui:
166
- group: Additional
175
+ group: additional
167
176
  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)."
168
177
 
169
178
  cc:
@@ -174,7 +183,7 @@ main:
174
183
  example:
175
184
  - Rank and Name, ORG/SYMBOL
176
185
  ui:
177
- group: Additional
186
+ group: additional
178
187
  description: List office symbols of recipients to receive copies.
179
188
 
180
189
  distribution:
@@ -186,7 +195,7 @@ main:
186
195
  - ORG1/SYMBOL
187
196
  - ORG2/SYMBOL
188
197
  ui:
189
- group: Additional
198
+ group: additional
190
199
  description: Complete list of recipients if 'SEE DISTRIBUTION' is used in the 'Memo For' field.
191
200
 
192
201
  attachments:
@@ -197,7 +206,7 @@ main:
197
206
  example:
198
207
  - Attachment description, YYYY MMM DD
199
208
  ui:
200
- group: Additional
209
+ group: additional
201
210
  description: List attachments in the order they are mentioned in the memo. Briefly describe each; do not use 'as stated' or abbreviations.
202
211
 
203
212
  memo_style:
@@ -207,7 +216,7 @@ main:
207
216
  - daf
208
217
  default: usaf
209
218
  ui:
210
- group: Additional
219
+ group: additional
211
220
  compact: true
212
221
  description: "USAF memorandum or DAF headquarters memorandum; DAF changes date format and body paragraph layout per AFH 33-337."
213
222
 
@@ -215,15 +224,15 @@ main:
215
224
  type: number
216
225
  default: 12
217
226
  ui:
218
- group: Additional
227
+ group: additional
219
228
  compact: true
220
229
  description: Body text size in points; decimals allowed (e.g. 11.5).
221
230
 
222
231
  date:
223
- type: datetime
232
+ type: date
224
233
  default: ""
225
234
  ui:
226
- group: Additional
235
+ group: additional
227
236
  compact: true
228
237
  description: YYYY-MM-DD. Leave blank to use today's date.
229
238
 
@@ -232,18 +241,21 @@ card_kinds:
232
241
  description: Chain of routing endorsements. Each endorsement block adds an official response or forwarding action to the original memo.
233
242
  ui:
234
243
  title: Routing indorsement
244
+ groups: [addressing, additional]
235
245
  fields:
236
246
  from:
237
247
  type: string
238
248
  example: ORG/SYMBOL
239
249
  ui:
240
- group: Addressing
250
+ group: addressing
251
+ compact: true
241
252
  description: Office symbol or Rank Name of the endorsing official.
242
253
  for:
243
254
  type: string
244
255
  example: ORG/SYMBOL
245
256
  ui:
246
- group: Addressing
257
+ group: addressing
258
+ compact: true
247
259
  description: Office symbol or organization receiving the endorsed memo.
248
260
  signature_block:
249
261
  type: array
@@ -253,7 +265,8 @@ card_kinds:
253
265
  - "FIRST M. LAST, Rank, USAF"
254
266
  - Duty Title
255
267
  ui:
256
- group: Addressing
268
+ group: addressing
269
+ compact: true
257
270
  description: "Line 1: Name in UPPERCASE as signed, grade, and service. Line 2: Duty title."
258
271
  format:
259
272
  type: string
@@ -263,7 +276,7 @@ card_kinds:
263
276
  - separate_page
264
277
  default: standard
265
278
  ui:
266
- group: Additional
279
+ group: additional
267
280
  compact: true
268
281
  description: "'standard' (formal, same page), 'informal' (less formal routing), or 'separate_page' (begins on a new page)."
269
282
  action:
@@ -275,13 +288,13 @@ card_kinds:
275
288
  - disapprove
276
289
  default: ""
277
290
  ui:
278
- group: Additional
291
+ group: additional
279
292
  compact: true
280
293
  description: "Action taken by the endorser. Leave blank to hide the Approve/Disapprove line entirely, 'undecided' to display the line with neither option circled, 'approve' or 'disapprove' to circle the selected option."
281
294
  date:
282
- type: datetime
295
+ type: date
283
296
  default: ""
284
297
  ui:
285
- group: Additional
298
+ group: additional
286
299
  compact: true
287
300
  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."
@@ -89,10 +89,15 @@
89
89
 
90
90
  /// Formats a date for the memo heading.
91
91
  ///
92
- /// - String: shown as-is (use for fixed text like placeholders).
93
- /// - datetime: USAF style `DD Month YYYY`; DAF style `Month DD, YYYY`.
94
- ///
95
- /// - date (str|datetime): Date to format for display
92
+ /// Dispatches on the shape a date field can arrive in:
93
+ /// - str: shown as-is (fixed text like a placeholder).
94
+ /// - datetime: a native Typst datetime (e.g. `datetime.today()`) — USAF style
95
+ /// `DD Month YYYY`, DAF style `Month DD, YYYY`.
96
+ /// - dict: a Quillmark `date`/`datetime` field lowers to a click-to-edit value
97
+ /// object `{ value: datetime, display: closure }`; `display` is a stored closure
98
+ /// over `.display`, so call it through parentheses — a dict has no `.display` method.
99
+ ///
100
+ /// - date (str | datetime | dict): Date to format for display
96
101
  /// - memo-style (str): `"usaf"` or `"daf"`
97
102
  /// -> content
98
103
  #let display-date(date, memo-style: "usaf") = {
@@ -108,7 +113,11 @@
108
113
  } else {
109
114
  "[day padding:none] [month repr:long] [year]"
110
115
  }
111
- date.display(pattern)
116
+ if type(date) == datetime {
117
+ date.display(pattern)
118
+ } else {
119
+ (date.display)(pattern)
120
+ }
112
121
  }
113
122
  }
114
123
 
@@ -54,7 +54,10 @@
54
54
  memo_for_cols: 1,
55
55
  )
56
56
 
57
- // Mainmatter configuration
57
+ // Mainmatter. The body's region needs no recovery step here: the package's
58
+ // render-body rebuilds paragraphs through a state buffer (AFH 33-337
59
+ // auto-numbering), but the rebuilt glyphs keep their spans, which is what
60
+ // the backend reads regions from.
58
61
  #mainmatter[
59
62
  #data.at("$body")
60
63
  ]
@@ -63,7 +66,7 @@
63
66
  #backmatter(
64
67
  // Signature block
65
68
  signature_block: data.signature_block,
66
- signing_field: signature-field("Signature"),
69
+ signing_field: signature-field("Signature", field: "signature_block"),
67
70
 
68
71
  // Optional cc
69
72
  ..if "cc" in data { (cc: data.cc) },
@@ -94,11 +97,20 @@
94
97
  } else {
95
98
  card_date
96
99
  }
100
+ // The card's `$path` prefix composes its canonical schema addresses
101
+ // (`$cards.indorsement.<n>.…`, per-kind ordinal) — the absolute loop
102
+ // index `i` is NOT that ordinal once kinds interleave, so it stays a
103
+ // widget-name suffix only. The card body's region rides its own glyph
104
+ // spans through the package rebuild, per-card because each card's body
105
+ // has its own backend-generated eval site.
97
106
  indorsement(
98
107
  from: card.at("from", default: ""),
99
108
  to: card.at("for", default: ""),
100
109
  signature_block: card.signature_block,
101
- signing_field: signature-field("Ind_" + str(i) + "_Signature"),
110
+ signing_field: signature-field(
111
+ "Ind_" + str(i) + "_Signature",
112
+ field: card.at("$path") + "signature_block",
113
+ ),
102
114
  format: card.at("format", default: "standard"),
103
115
  date: resolved_date,
104
116
  ..if "action" in card { (action: card.action) },
@@ -1,20 +0,0 @@
1
- I visually mapped out a bunch of fields on the form to the typst field names. The intent is to write a plate and field schema that interfaces with the Typst form and exposes an ergonomic API to the Quill consumer. See [Creating Quills](https://quillmark.readthedocs.io/en/latest/guides/creating-quills/) to learn how to write the field schema.
2
-
3
- Note that the following field names are not authortative; use your discretion to name them ergonomically.
4
-
5
- ## Admin Fields
6
-
7
- Name: `commonforms_text_p1_1`
8
- Unit: `commonforms_text_p1_2`
9
- Grade/CCC Level: `commonforms_text_p1_3`
10
-
11
- ## Record of Experience table fields
12
-
13
- Date: `commonforms_text_p1_{4,7,18,...,109}` and `commonforms_text_p2_{1,8,15,...141}`
14
- Mandatory Actions to be Reported: `commonforms_text_p1_{5,12,19,...,110}` and `commonforms_text_p2_{2,9,16,...,142}`
15
- written_grade: `commonforms_text_p1_{6,13,...111}` and `commonforms_text_p2_{3,10,...,143}`
16
- writen_grade_date: `commonforms_text_p1_{7,14,...,112}` and `commonforms_text_p2_{4,11,144}`
17
- positional_grade: `commonforms_text_p1_{8,15,...,113}` and `commonforms_text_p2_{5,12,...,145}`
18
- positional_grade_date: `commonforms_text_p1_{9,...,114}` and
19
- `commonforms_text_p2_{6,...,145}`
20
- auth_or_remarks: `commonforms_text_p1_{10,...,115}` and `commonforms_text_p2_{7,...,146}`
@@ -1,31 +0,0 @@
1
- # Example .quillignore for usaf_memo template
2
- # This demonstrates the .quillignore functionality
3
-
4
- # Ignore temporary and backup files
5
- *.tmp
6
- *.bak
7
- *~
8
- *.swp
9
-
10
- # Ignore build artifacts
11
- target/
12
- dist/
13
- build/
14
-
15
- # Ignore OS-specific files
16
- .DS_Store
17
- Thumbs.db
18
- desktop.ini
19
-
20
- # Ignore version control
21
- .git/
22
- .svn/
23
-
24
- # Ignore editor files
25
- .vscode/
26
- .idea/
27
- *.sublime-*
28
-
29
- # Ignore log files
30
- *.log
31
- logs/