@asd20/ui-next 2.26.0 → 2.27.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ # [2.27.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.26.1...ui-next-v2.27.0) (2026-09-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * support inline public form introduction links ([bea4683](https://github.com/academydistrict20/asd20-ui-next/commit/bea4683f849623abe527ad9b70f6d85db145aa25))
9
+
10
+ ## [2.26.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.26.0...ui-next-v2.26.1) (2026-09-30)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * show public form fallback only after submission failure ([7f17021](https://github.com/academydistrict20/asd20-ui-next/commit/7f17021d2e1f4e04fd193fb0e16d80125efe3ae7))
16
+
3
17
  # [2.26.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.25.3...ui-next-v2.26.0) (2026-09-30)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.26.0",
3
+ "version": "2.27.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -53,9 +53,14 @@
53
53
  v-if="formDefinition.introduction"
54
54
  class="asd20-public-form-modal__introduction"
55
55
  >
56
- {{ formDefinition.introduction }}
56
+ <template v-if="inlineIntroductionParts">
57
+ {{ inlineIntroductionParts.before }}<a :href="formDefinition.introductionLink.href">{{ formDefinition.introductionLink.label }}</a>{{ inlineIntroductionParts.after }}
58
+ </template>
59
+ <template v-else>
60
+ {{ formDefinition.introduction }}
61
+ </template>
57
62
  </p>
58
- <p v-if="formDefinition.introductionLink">
63
+ <p v-if="formDefinition.introductionLink && !formDefinition.introductionLink.inline">
59
64
  <a :href="formDefinition.introductionLink.href">
60
65
  {{ formDefinition.introductionLink.label }}
61
66
  </a>
@@ -114,26 +119,6 @@
114
119
  />
115
120
  </div>
116
121
 
117
- <div
118
- v-if="hasFallbackContact"
119
- class="asd20-public-form-modal__fallback"
120
- >
121
- <p>
122
- {{
123
- formDefinition.fallbackContact.instructions ||
124
- 'If you cannot submit this form, use one of these contact options:'
125
- }}
126
- </p>
127
- <a
128
- v-if="formDefinition.fallbackContact.email"
129
- :href="`mailto:${formDefinition.fallbackContact.email}`"
130
- >{{ formDefinition.fallbackContact.email }}</a>
131
- <a
132
- v-if="formDefinition.fallbackContact.phone"
133
- :href="fallbackPhoneHref"
134
- >{{ formDefinition.fallbackContact.phone }}</a>
135
- </div>
136
-
137
122
  <div
138
123
  class="asd20-public-form-modal__submit"
139
124
  aria-live="polite"
@@ -180,6 +165,26 @@
180
165
  >
181
166
  {{ feedbackMessage }}
182
167
  </div>
168
+
169
+ <div
170
+ v-if="showFallbackContact"
171
+ class="asd20-public-form-modal__fallback"
172
+ >
173
+ <p>
174
+ {{
175
+ formDefinition.fallbackContact.instructions ||
176
+ 'If you cannot submit this form, use one of these contact options:'
177
+ }}
178
+ </p>
179
+ <a
180
+ v-if="formDefinition.fallbackContact.email"
181
+ :href="`mailto:${formDefinition.fallbackContact.email}`"
182
+ >{{ formDefinition.fallbackContact.email }}</a>
183
+ <a
184
+ v-if="formDefinition.fallbackContact.phone"
185
+ :href="fallbackPhoneHref"
186
+ >{{ formDefinition.fallbackContact.phone }}</a>
187
+ </div>
183
188
  </form>
184
189
  </asd20-viewport>
185
190
  </asd20-modal>
@@ -245,6 +250,7 @@ export default {
245
250
  feedbackRole: 'alert',
246
251
  sending: false,
247
252
  submissionSuccess: false,
253
+ submissionFailed: false,
248
254
  loadSequence: 0,
249
255
  }
250
256
  },
@@ -252,6 +258,16 @@ export default {
252
258
  modalTitle() {
253
259
  return this.formDefinition?.title || 'Public Form'
254
260
  },
261
+ inlineIntroductionParts() {
262
+ const introduction = this.formDefinition?.introduction || ''
263
+ const link = this.formDefinition?.introductionLink
264
+ if (!link?.inline) return null
265
+ const index = introduction.indexOf(link.label)
266
+ return {
267
+ before: introduction.slice(0, index),
268
+ after: introduction.slice(index + link.label.length),
269
+ }
270
+ },
255
271
  usesRecordsFormSpacing() {
256
272
  return [
257
273
  'student-records-request',
@@ -272,7 +288,10 @@ export default {
272
288
  },
273
289
  hasFallbackContact() {
274
290
  const fallback = this.formDefinition?.fallbackContact
275
- return Boolean(fallback?.email || fallback?.phone)
291
+ return Boolean(fallback?.instructions || fallback?.email || fallback?.phone)
292
+ },
293
+ showFallbackContact() {
294
+ return this.submissionFailed && this.hasFallbackContact
276
295
  },
277
296
  fallbackPhoneHref() {
278
297
  const phone = this.formDefinition?.fallbackContact?.phone || ''
@@ -318,6 +337,7 @@ export default {
318
337
  this.definitionError = ''
319
338
  this.feedbackMessage = ''
320
339
  this.submissionSuccess = false
340
+ this.submissionFailed = false
321
341
  this.validationErrors = {}
322
342
  this.validationAttempted = false
323
343
  this.honeypot = ''
@@ -386,7 +406,7 @@ export default {
386
406
  },
387
407
  captchaFailed() {
388
408
  this.showFeedback(
389
- 'The verification service could not start. Please try again or use the contact options listed.'
409
+ 'The verification service could not start. Please try again or use another contact option on this page.'
390
410
  )
391
411
  },
392
412
  captchaExpired() {
@@ -426,6 +446,7 @@ export default {
426
446
 
427
447
  this.sending = true
428
448
  this.feedbackMessage = ''
449
+ this.submissionFailed = false
429
450
  try {
430
451
  const payload = {
431
452
  ...this.values,
@@ -443,6 +464,7 @@ export default {
443
464
  definitionVersion: this.formDefinition.version,
444
465
  })
445
466
  } catch (error) {
467
+ this.submissionFailed = true
446
468
  this.showFeedback(this.failureMessage(error))
447
469
  } finally {
448
470
  this.sending = false
@@ -171,6 +171,12 @@ export function normalizePublicFormDefinition(definition) {
171
171
  const id = boundedText(definition.id, 'Form ID', 100, true)
172
172
  if (!FORM_ID_PATTERN.test(id)) throw new Error('The public form ID is invalid.')
173
173
 
174
+ const introduction = boundedText(
175
+ definition.introduction,
176
+ 'Form introduction',
177
+ 2000
178
+ )
179
+
174
180
  let introductionLink
175
181
  if (definition.introductionLink != null) {
176
182
  const label = boundedText(
@@ -187,18 +193,25 @@ export function normalizePublicFormDefinition(definition) {
187
193
  )
188
194
  if (!/^\/[a-z0-9/-]+\/?$/.test(href) || href.startsWith('//'))
189
195
  throw new Error('The introduction link must be a local page path.')
190
- introductionLink = { label, href }
196
+ const inline = optionalBoolean(
197
+ definition.introductionLink.inline,
198
+ 'Inline introduction link'
199
+ )
200
+ if (
201
+ inline &&
202
+ (!introduction.includes(label) ||
203
+ introduction.indexOf(label) !== introduction.lastIndexOf(label))
204
+ ) {
205
+ throw new Error('The inline introduction link label must appear once in the introduction.')
206
+ }
207
+ introductionLink = inline ? { label, href, inline: true } : { label, href }
191
208
  }
192
209
 
193
210
  return {
194
211
  id,
195
212
  version,
196
213
  title: boundedText(definition.title, 'Form title', 200, true),
197
- introduction: boundedText(
198
- definition.introduction,
199
- 'Form introduction',
200
- 2000
201
- ),
214
+ introduction,
202
215
  introductionLink,
203
216
  submitLabel: boundedText(definition.submitLabel, 'Submit label', 100, true),
204
217
  successMessage: boundedText(