@asd20/ui-next 2.31.4 → 2.31.5
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/CHANGELOG.md +7 -0
- package/helpers.js +1 -0
- package/package.json +1 -1
- package/src/components/templates/Asd20HandbookDocument/index.vue +42 -11
- package/src/components/templates/Asd20HandbookTemplate/index.vue +1 -1
- package/src/helpers/handbookDocument.js +11 -3
- package/src/helpers/handbookPresentation.js +66 -0
- package/src/helpers/publicationIntroduction.js +5 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.31.5](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.31.4...ui-next-v2.31.5) (2026-10-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* add specific cover sheet for each handbook ([6c1f423](https://github.com/academydistrict20/asd20-ui-next/commit/6c1f423c06be9caa2443eee0c6d74d975416cc5a))
|
|
9
|
+
|
|
3
10
|
## [2.31.4](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.31.3...ui-next-v2.31.4) (2026-10-09)
|
|
4
11
|
|
|
5
12
|
|
package/helpers.js
CHANGED
|
@@ -60,3 +60,4 @@ export { default as queryPeople } from './src/helpers/queryPeople.js'
|
|
|
60
60
|
export { default as sendEmail } from './src/helpers/sendEmail.js'
|
|
61
61
|
export { default as sortOrgsByLevel } from './src/helpers/sortOrgsByLevel.js'
|
|
62
62
|
export { buildHandbookDocument, documentImageUrls } from './src/helpers/handbookDocument.js'
|
|
63
|
+
export { handbookSchoolYear, isDistrictHandbook, handbookCoverTitle, handbookDownloadFilename } from './src/helpers/handbookPresentation.js'
|
package/package.json
CHANGED
|
@@ -3,19 +3,31 @@
|
|
|
3
3
|
class="asd20-handbook-document"
|
|
4
4
|
:data-asd20-document-root="documentId"
|
|
5
5
|
>
|
|
6
|
-
<header>
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
<header class="asd20-handbook-document__cover">
|
|
7
|
+
<div class="asd20-handbook-document__cover-heading">
|
|
8
|
+
<h1>
|
|
9
|
+
<span class="asd20-handbook-document__organization">{{ coverTitle.organizationTitle }}</span>{{ ' ' }}
|
|
10
|
+
<span class="asd20-handbook-document__title">{{ coverTitle.handbookTitle }}</span>
|
|
11
|
+
</h1>
|
|
12
|
+
<img
|
|
13
|
+
v-if="organization.logoImageUrl"
|
|
14
|
+
class="asd20-handbook-document__logo"
|
|
15
|
+
:src="organization.logoImageUrl"
|
|
16
|
+
:alt="`${coverTitle.organizationTitle} logo`"
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="asd20-handbook-document__cover-body">
|
|
20
|
+
<asd20-messaging
|
|
21
|
+
:body-content="messages[0]?.bodyContent || ''"
|
|
22
|
+
:body-json="messages[0]?.bodyJson || ''"
|
|
23
|
+
:heading-id-prefix="document.coverKey"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
<asd20-print-header
|
|
27
|
+
:canonical-url="documentUrl"
|
|
28
|
+
v-bind="printMetadata"
|
|
12
29
|
/>
|
|
13
30
|
</header>
|
|
14
|
-
<asd20-print-header
|
|
15
|
-
:organization-title="organization.title || ''"
|
|
16
|
-
:canonical-url="documentUrl"
|
|
17
|
-
v-bind="printMetadata"
|
|
18
|
-
/>
|
|
19
31
|
<asd20-print-contents :headings="document.headings" />
|
|
20
32
|
<main>
|
|
21
33
|
<article
|
|
@@ -51,6 +63,7 @@ import Asd20Messaging from '../../atoms/Asd20Messaging'
|
|
|
51
63
|
import Asd20PrintHeader from '../../molecules/Asd20PrintHeader'
|
|
52
64
|
import Asd20PrintContents from '../../molecules/Asd20PrintContents'
|
|
53
65
|
import { buildHandbookDocument } from '../../../helpers/handbookDocument'
|
|
66
|
+
import { handbookCoverTitle } from '../../../helpers/handbookPresentation'
|
|
54
67
|
import { publicationHeadingPlanKey } from '../../atoms/Asd20RichBodyContent/headingContext'
|
|
55
68
|
|
|
56
69
|
export default {
|
|
@@ -65,9 +78,14 @@ export default {
|
|
|
65
78
|
documentId: { type: String, required: true },
|
|
66
79
|
documentUrl: { type: String, required: true },
|
|
67
80
|
printMetadata: { type: Object, default: () => ({}) },
|
|
81
|
+
includeIntroductionBody: { type: Boolean, default: true },
|
|
82
|
+
schoolYear: { type: String, default: '' },
|
|
68
83
|
},
|
|
69
84
|
computed: {
|
|
70
85
|
document() { return buildHandbookDocument(this.$props) },
|
|
86
|
+
coverTitle() {
|
|
87
|
+
return handbookCoverTitle(this.organization, this.document.introduction.heading || this.title, this.printMetadata.generatedAt || new Date().toISOString())
|
|
88
|
+
},
|
|
71
89
|
},
|
|
72
90
|
}
|
|
73
91
|
</script>
|
|
@@ -81,5 +99,18 @@ export default {
|
|
|
81
99
|
h3 { font-size: 13pt; }
|
|
82
100
|
h4, h5, h6 { font-size: 11pt; }
|
|
83
101
|
a { color: #111; }
|
|
102
|
+
&__cover { break-after: page; padding-top: 0.35in; }
|
|
103
|
+
&__cover-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.3in; }
|
|
104
|
+
&__cover h1 { flex: 1; font-size: 24pt; line-height: 1.3; margin: 0; color: #1e477d; }
|
|
105
|
+
&__organization, &__title { display: block; }
|
|
106
|
+
&__title { margin-top: 0.15in; font-size: 19pt; }
|
|
107
|
+
&__cover &__logo { width: 1.45in; max-width: 1.45in; max-height: 1.65in; object-fit: contain; }
|
|
108
|
+
&__cover-body { margin-top: 0.75in; }
|
|
109
|
+
&__cover-body .asd20-messaging { padding: 0; margin: 0; }
|
|
110
|
+
&__cover-body .asd20-messaging__body-content { font-size: 10pt; line-height: 1.5; }
|
|
111
|
+
&__cover-body p { margin: 0 0 0.18in; }
|
|
112
|
+
&__cover-body img { margin-top: 0.2in; }
|
|
113
|
+
&__cover .asd20-print-header { margin-top: 0.35in; font-size: 8pt; color: #555; }
|
|
114
|
+
&__cover .asd20-print-header p { margin: 0.05in 0; }
|
|
84
115
|
}
|
|
85
116
|
</style>
|
|
@@ -18,7 +18,7 @@ export function documentImageUrls(html, baseUrl) {
|
|
|
18
18
|
|
|
19
19
|
// The online handbook and the isolated PDF view share ordering, exclusions and
|
|
20
20
|
// heading planning. Rendering a document never changes the authored messages.
|
|
21
|
-
export function buildHandbookDocument({ messages = [], metaDescription = '', documentId = 'handbook', documentUrl = '', reservedIds = [] } = {}) {
|
|
21
|
+
export function buildHandbookDocument({ messages = [], metaDescription = '', documentId = 'handbook', documentUrl = '', reservedIds = [], includeIntroductionBody = false } = {}) {
|
|
22
22
|
const counts = new Map()
|
|
23
23
|
const entries = messages.slice(1).map((message, index) => {
|
|
24
24
|
const identity = String(message.id ?? `position-${index}`)
|
|
@@ -27,18 +27,26 @@ export function buildHandbookDocument({ messages = [], metaDescription = '', doc
|
|
|
27
27
|
const key = createPublicationHeadingPrefix(documentId, JSON.stringify([identity, ordinal]))
|
|
28
28
|
return { message, key, titleId: `${key}-title`, subtopicsId: `${key}-subtopics` }
|
|
29
29
|
})
|
|
30
|
+
const coverKey = createPublicationHeadingPrefix(documentId, 'cover')
|
|
31
|
+
const bodies = entries.map(entry => ({ bodyContent: entry.message.bodyContent, bodyJson: entry.message.bodyJson, headingLevelOffset: 1, headingIdPrefix: entry.key }))
|
|
32
|
+
if (includeIntroductionBody) bodies.push({ bodyContent: messages[0]?.bodyContent, bodyJson: messages[0]?.bodyJson, headingIdPrefix: coverKey })
|
|
30
33
|
const headingModels = buildPublicationHeadingModels({
|
|
31
34
|
documentUrl,
|
|
32
|
-
bodies
|
|
35
|
+
bodies,
|
|
33
36
|
reservedIds: ['main', 'handbook-content', 'handbook-topics', ...reservedIds, ...entries.flatMap(entry => [entry.titleId, entry.subtopicsId])],
|
|
34
37
|
})
|
|
35
38
|
const introduction = buildPublicationIntroduction(messages[0], metaDescription)
|
|
39
|
+
const coverBody = includeIntroductionBody ? headingModels.models[entries.length] : null
|
|
36
40
|
return {
|
|
37
41
|
entries, introduction, headingModels,
|
|
42
|
+
coverKey, coverBody,
|
|
38
43
|
headings: entries.flatMap((entry, index) => [
|
|
39
44
|
{ id: entry.titleId, label: entry.message.heading || entry.message.title, authoredLevel: 2, renderedLevel: 2 },
|
|
40
45
|
...(headingModels.models[index]?.headings || []),
|
|
41
46
|
]),
|
|
42
|
-
diagnostics: [...introduction.diagnostics, ...headingModels.headingDiagnostics
|
|
47
|
+
diagnostics: [...introduction.diagnostics, ...headingModels.headingDiagnostics,
|
|
48
|
+
...(coverBody?.status === 'unsupported' ? [{ code: 'introduction-body-unsupported' }] : []),
|
|
49
|
+
...(coverBody?.headings.some(heading => heading.renderedLevel === 1) ? [{ code: 'introduction-body-h1' }] : []),
|
|
50
|
+
],
|
|
43
51
|
}
|
|
44
52
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Shared website/PDF presentation rules. The renderer copy is generated by
|
|
2
|
+
// scripts/sync-handbook-presentation.mjs.
|
|
3
|
+
const districtId = '26eaf390-d8ab-11e9-a3a8-5de5bba4f125'
|
|
4
|
+
|
|
5
|
+
function generationDateParts(generatedAt) {
|
|
6
|
+
if (!Number.isFinite(Date.parse(generatedAt)))
|
|
7
|
+
throw new Error('Invalid PDF generation date.')
|
|
8
|
+
return Object.fromEntries(
|
|
9
|
+
new Intl.DateTimeFormat('en-US', {
|
|
10
|
+
timeZone: 'America/Denver',
|
|
11
|
+
year: 'numeric',
|
|
12
|
+
month: '2-digit',
|
|
13
|
+
day: '2-digit'
|
|
14
|
+
})
|
|
15
|
+
.formatToParts(new Date(generatedAt))
|
|
16
|
+
.map((part) => [part.type, part.value])
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function handbookSchoolYear(generatedAt) {
|
|
21
|
+
const { year, month } = generationDateParts(generatedAt)
|
|
22
|
+
const start = Number(year) - (Number(month) < 7 ? 1 : 0)
|
|
23
|
+
return `${start}-${String(start + 1).slice(-2)}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isDistrictHandbook(organization = {}) {
|
|
27
|
+
return (
|
|
28
|
+
organization.id === districtId ||
|
|
29
|
+
(!organization.id && organization.abbreviation === 'ASD20')
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function handbookCoverTitle(
|
|
34
|
+
organization = {},
|
|
35
|
+
firstMessageTitle = '',
|
|
36
|
+
generatedAt
|
|
37
|
+
) {
|
|
38
|
+
const organizationTitle = isDistrictHandbook(organization)
|
|
39
|
+
? 'Academy District 20'
|
|
40
|
+
: organization.title || organization.name || 'School'
|
|
41
|
+
const handbookTitle = isDistrictHandbook(organization)
|
|
42
|
+
? firstMessageTitle || 'School Handbook'
|
|
43
|
+
: 'School Handbook'
|
|
44
|
+
return {
|
|
45
|
+
organizationTitle,
|
|
46
|
+
handbookTitle: `${handbookSchoolYear(generatedAt)} ${handbookTitle}`
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function handbookDownloadFilename(
|
|
51
|
+
organization = {},
|
|
52
|
+
generatedAt,
|
|
53
|
+
firstMessageTitle = ''
|
|
54
|
+
) {
|
|
55
|
+
const safe = (value) =>
|
|
56
|
+
String(value || '')
|
|
57
|
+
.replace(/[^a-zA-Z0-9_. ()-]/g, '')
|
|
58
|
+
.trim()
|
|
59
|
+
.slice(0, 160)
|
|
60
|
+
const abbreviation = safe(organization.abbreviation).slice(0, 80)
|
|
61
|
+
const { year, month, day } = generationDateParts(generatedAt)
|
|
62
|
+
const title = isDistrictHandbook(organization)
|
|
63
|
+
? `ASD20 ${safe(firstMessageTitle) || 'School Handbook'}`
|
|
64
|
+
: `${abbreviation ? abbreviation + ' ' : ''}School Handbook`
|
|
65
|
+
return `${title} - ${year}-${month}-${day}.pdf`
|
|
66
|
+
}
|
|
@@ -11,31 +11,16 @@ function nonemptyHtml(html) {
|
|
|
11
11
|
return (parseFragment(String(html || '')).childNodes || []).some(visit)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function nonemptyJson(value) {
|
|
15
|
-
if (!value) return false
|
|
16
|
-
let doc = value
|
|
17
|
-
if (typeof value === 'string') {
|
|
18
|
-
try { doc = JSON.parse(value) } catch { return true }
|
|
19
|
-
}
|
|
20
|
-
if (doc?.version === 1 && Array.isArray(doc.blocks)) return doc.blocks.length > 0
|
|
21
|
-
const visit = node => {
|
|
22
|
-
if (node?.type === 'text') return Boolean(node.text?.trim())
|
|
23
|
-
if (!['doc', 'paragraph'].includes(node?.type)) return true
|
|
24
|
-
return (node.content || []).some(visit)
|
|
25
|
-
}
|
|
26
|
-
return visit(doc)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
14
|
export function buildPublicationIntroduction(firstMessage, metaDescription = '') {
|
|
30
15
|
const message = firstMessage || {}
|
|
31
16
|
const lead = message.shortDescription || message.summary || metaDescription || ''
|
|
32
|
-
|
|
17
|
+
// First-message body/media belong to the PDF cover, never the online header.
|
|
18
|
+
const excludedFields = ['bodyContent', 'bodyJson', 'longDescription', 'images', 'callsToAction', 'detailLink', 'detailLinkLabel']
|
|
33
19
|
const unmappedFields = []
|
|
34
|
-
for (const field of ['
|
|
20
|
+
for (const field of ['longDescription']) {
|
|
35
21
|
if (nonemptyHtml(message[field])) unmappedFields.push(field)
|
|
36
22
|
}
|
|
37
|
-
|
|
38
|
-
for (const field of ['images', 'callsToAction']) {
|
|
23
|
+
for (const field of ['callsToAction']) {
|
|
39
24
|
if (message[field]?.length) unmappedFields.push(field)
|
|
40
25
|
}
|
|
41
26
|
// Detail links direct readers from wayfinding messages to detailed pages.
|
|
@@ -44,7 +29,7 @@ export function buildPublicationIntroduction(firstMessage, metaDescription = '')
|
|
|
44
29
|
if (message.links?.some(item => (item?.link ?? item)?.type !== 'Detail Link')) unmappedFields.push('links')
|
|
45
30
|
if (message.shortDescription && nonemptyHtml(message.summary) && message.summary !== lead) unmappedFields.push('summary')
|
|
46
31
|
return {
|
|
47
|
-
messageId: message.id ?? null, mode: 'first-
|
|
32
|
+
messageId: message.id ?? null, mode: 'first-message-header', heading: message.heading || message.title || '', lead,
|
|
48
33
|
detailLinkPolicy: 'omit-wayfinding',
|
|
49
34
|
leadSource: message.shortDescription || message.summary ? 'summary' : metaDescription ? 'metaDescription' : 'none',
|
|
50
35
|
excludedFields: [...excludedFields, 'links', ...(message.shortDescription ? ['summary'] : [])],
|