@asd20/ui-next 2.15.1 → 2.17.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.17.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.16.0...ui-next-v2.17.0) (2026-08-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add canonical video rendering ([295fbfd](https://github.com/academydistrict20/asd20-ui-next/commit/295fbfd38d3d66c2bc1e0070fb98a15ba6e51d02))
|
|
9
|
+
|
|
10
|
+
# [2.16.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.15.1...ui-next-v2.16.0) (2026-08-04)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* support canonical TipTap tables ([179eca3](https://github.com/academydistrict20/asd20-ui-next/commit/179eca3f7c0e0f2f226c8d660d84e3564fbf28c2))
|
|
16
|
+
|
|
3
17
|
## [2.15.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.15.0...ui-next-v2.15.1) (2026-07-31)
|
|
4
18
|
|
|
5
19
|
# [2.15.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.14.1...ui-next-v2.15.0) (2026-07-31)
|
package/package.json
CHANGED
|
@@ -114,5 +114,73 @@ export default {
|
|
|
114
114
|
&__html {
|
|
115
115
|
display: block;
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
:deep(.tableWrapper) {
|
|
119
|
+
max-width: 100%;
|
|
120
|
+
margin: 0.75rem 0;
|
|
121
|
+
overflow-x: auto;
|
|
122
|
+
overscroll-behavior-inline: contain;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
:deep(.asd20-message-table) {
|
|
126
|
+
border-collapse: collapse;
|
|
127
|
+
font-size: 0.875rem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
:deep(.asd20-message-table th),
|
|
131
|
+
:deep(.asd20-message-table td) {
|
|
132
|
+
min-width: 3rem;
|
|
133
|
+
padding: 0.5rem;
|
|
134
|
+
border-bottom: 1px solid #cfd8e3;
|
|
135
|
+
vertical-align: top;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
:deep(.asd20-message-table th) {
|
|
139
|
+
color: white;
|
|
140
|
+
background: #003b71;
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
:deep(.asd20-message-table caption) {
|
|
145
|
+
padding: 0 0 0.5rem;
|
|
146
|
+
color: #263238;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
text-align: left;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:deep(.asd20-message-table th > :first-child),
|
|
152
|
+
:deep(.asd20-message-table td > :first-child) {
|
|
153
|
+
margin-top: 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
:deep(.asd20-message-table th > :last-child),
|
|
157
|
+
:deep(.asd20-message-table td > :last-child) {
|
|
158
|
+
margin-bottom: 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
:deep(.asd20-message-video) {
|
|
162
|
+
max-width: 100%;
|
|
163
|
+
margin: 0.75rem 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
:deep(.asd20-message-video__description) {
|
|
167
|
+
margin: 0 0 0.5rem;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
:deep(.asd20-message-video__frame) {
|
|
171
|
+
position: relative;
|
|
172
|
+
width: 100%;
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
aspect-ratio: 16 / 9;
|
|
175
|
+
background: #000;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
:deep(.asd20-message-video__frame iframe) {
|
|
179
|
+
position: absolute;
|
|
180
|
+
inset: 0;
|
|
181
|
+
width: 100%;
|
|
182
|
+
height: 100%;
|
|
183
|
+
border: 0;
|
|
184
|
+
}
|
|
117
185
|
}
|
|
118
186
|
</style>
|
|
@@ -55,6 +55,23 @@ const MARK_TYPES = new Set([
|
|
|
55
55
|
'subscript',
|
|
56
56
|
'superscript',
|
|
57
57
|
])
|
|
58
|
+
const TABLE_HEADER_SCOPES = new Set(['col', 'row', 'colgroup', 'rowgroup'])
|
|
59
|
+
const VIDEO_TITLE_MAX_LENGTH = 200
|
|
60
|
+
const VIDEO_DESCRIPTION_MAX_LENGTH = 1000
|
|
61
|
+
const VIDEO_PROVIDER_RULES = Object.freeze([
|
|
62
|
+
{
|
|
63
|
+
host: 'www.youtube.com',
|
|
64
|
+
pathPattern: /^\/embed\/([A-Za-z0-9_-]{11})$/,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
host: 'www.youtube-nocookie.com',
|
|
68
|
+
pathPattern: /^\/embed\/([A-Za-z0-9_-]{11})$/,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
host: 'player.vimeo.com',
|
|
72
|
+
pathPattern: /^\/video\/([0-9]+)$/,
|
|
73
|
+
},
|
|
74
|
+
])
|
|
58
75
|
const MARK_RENDER_ORDER = Object.freeze({
|
|
59
76
|
link: 0,
|
|
60
77
|
bold: 1,
|
|
@@ -78,10 +95,10 @@ const NODE_ATTRIBUTE_NAMES = Object.freeze({
|
|
|
78
95
|
listItem: [],
|
|
79
96
|
image: ['src', 'alt', 'title', 'width', 'height'],
|
|
80
97
|
videoEmbed: ['src', 'title', 'descriptiveText'],
|
|
81
|
-
table: [],
|
|
98
|
+
table: ['caption'],
|
|
82
99
|
tableRow: [],
|
|
83
100
|
tableCell: ['colspan', 'rowspan', 'colwidth', 'align'],
|
|
84
|
-
tableHeader: ['colspan', 'rowspan', 'colwidth', 'align'],
|
|
101
|
+
tableHeader: ['colspan', 'rowspan', 'colwidth', 'align', 'scope'],
|
|
85
102
|
tableauBlock: [
|
|
86
103
|
'blockId',
|
|
87
104
|
'src',
|
|
@@ -195,6 +212,45 @@ function isSafeResource(value, allowHash = false) {
|
|
|
195
212
|
}
|
|
196
213
|
}
|
|
197
214
|
|
|
215
|
+
function isCanonicalVideoEmbedSource(value) {
|
|
216
|
+
if (typeof value !== 'string' || !value) return false
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const url = new URL(value)
|
|
220
|
+
if (
|
|
221
|
+
url.protocol !== 'https:' ||
|
|
222
|
+
url.username ||
|
|
223
|
+
url.password ||
|
|
224
|
+
url.port ||
|
|
225
|
+
url.search ||
|
|
226
|
+
url.hash
|
|
227
|
+
) {
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const rule = VIDEO_PROVIDER_RULES.find(
|
|
232
|
+
candidate => candidate.host === url.hostname.toLowerCase()
|
|
233
|
+
)
|
|
234
|
+
return Boolean(
|
|
235
|
+
rule &&
|
|
236
|
+
rule.pathPattern.test(url.pathname) &&
|
|
237
|
+
value === `https://${rule.host}${url.pathname}`
|
|
238
|
+
)
|
|
239
|
+
} catch (_error) {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function validateRequiredCanonicalText(node, name, path, maximumLength) {
|
|
245
|
+
const attrIssue = validateStringAttribute(node, name, path, true)
|
|
246
|
+
if (attrIssue) return attrIssue
|
|
247
|
+
|
|
248
|
+
const value = node.attrs[name]
|
|
249
|
+
return !value || value !== value.trim() || value.length > maximumLength
|
|
250
|
+
? issue('malformed-attribute', path, name)
|
|
251
|
+
: null
|
|
252
|
+
}
|
|
253
|
+
|
|
198
254
|
function validateMarks(marks, path) {
|
|
199
255
|
if (marks === undefined) return null
|
|
200
256
|
if (!Array.isArray(marks)) return issue('malformed-marks', path)
|
|
@@ -395,17 +451,36 @@ function validateNode(node, path) {
|
|
|
395
451
|
return validateAtom(node, path)
|
|
396
452
|
}
|
|
397
453
|
if (node.type === 'videoEmbed') {
|
|
398
|
-
const srcIssue = validateStringAttribute(node, 'src', path)
|
|
399
|
-
if (srcIssue || (node.attrs?.src
|
|
454
|
+
const srcIssue = validateStringAttribute(node, 'src', path, true)
|
|
455
|
+
if (srcIssue || !isCanonicalVideoEmbedSource(node.attrs?.src)) {
|
|
400
456
|
return srcIssue || issue('unsafe-resource', path, 'src')
|
|
401
457
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
458
|
+
const titleIssue = validateRequiredCanonicalText(
|
|
459
|
+
node,
|
|
460
|
+
'title',
|
|
461
|
+
path,
|
|
462
|
+
VIDEO_TITLE_MAX_LENGTH
|
|
463
|
+
)
|
|
464
|
+
if (titleIssue) return titleIssue
|
|
465
|
+
const descriptionIssue = validateRequiredCanonicalText(
|
|
466
|
+
node,
|
|
467
|
+
'descriptiveText',
|
|
468
|
+
path,
|
|
469
|
+
VIDEO_DESCRIPTION_MAX_LENGTH
|
|
470
|
+
)
|
|
471
|
+
if (descriptionIssue) return descriptionIssue
|
|
406
472
|
return validateAtom(node, path)
|
|
407
473
|
}
|
|
408
474
|
if (node.type === 'table') {
|
|
475
|
+
const captionIssue = validateStringAttribute(node, 'caption', path)
|
|
476
|
+
if (captionIssue) return captionIssue
|
|
477
|
+
if (
|
|
478
|
+
node.attrs?.caption !== undefined &&
|
|
479
|
+
node.attrs?.caption !== null &&
|
|
480
|
+
!node.attrs.caption.trim()
|
|
481
|
+
) {
|
|
482
|
+
return issue('malformed-attribute', path, 'caption')
|
|
483
|
+
}
|
|
409
484
|
return validateChildren(node, path, new Set(['tableRow']), true)
|
|
410
485
|
}
|
|
411
486
|
if (node.type === 'tableRow') {
|
|
@@ -421,6 +496,7 @@ function validateNode(node, path) {
|
|
|
421
496
|
const rowspan = node.attrs?.rowspan ?? 1
|
|
422
497
|
const colwidth = node.attrs?.colwidth
|
|
423
498
|
const align = node.attrs?.align
|
|
499
|
+
const scope = node.attrs?.scope
|
|
424
500
|
if (!Number.isInteger(colspan) || colspan < 1) {
|
|
425
501
|
return issue('malformed-attribute', path, 'colspan')
|
|
426
502
|
}
|
|
@@ -443,6 +519,14 @@ function validateNode(node, path) {
|
|
|
443
519
|
) {
|
|
444
520
|
return issue('malformed-attribute', path, 'align')
|
|
445
521
|
}
|
|
522
|
+
if (
|
|
523
|
+
node.type === 'tableHeader' &&
|
|
524
|
+
scope !== undefined &&
|
|
525
|
+
scope !== null &&
|
|
526
|
+
!TABLE_HEADER_SCOPES.has(scope)
|
|
527
|
+
) {
|
|
528
|
+
return issue('malformed-attribute', path, 'scope')
|
|
529
|
+
}
|
|
446
530
|
return validateChildren(node, path, STANDARD_BLOCK_NODE_TYPES, true)
|
|
447
531
|
}
|
|
448
532
|
if (node.type === 'tableauBlock') {
|
|
@@ -563,14 +647,15 @@ function renderTableCell(node, tagName) {
|
|
|
563
647
|
const rowspan = node.attrs?.rowspan ?? 1
|
|
564
648
|
const colwidth = node.attrs?.colwidth
|
|
565
649
|
const align = node.attrs?.align
|
|
650
|
+
const scope = node.attrs?.scope
|
|
566
651
|
return `<${tagName}${attribute('colspan', colspan)}${attribute(
|
|
567
652
|
'rowspan',
|
|
568
653
|
rowspan
|
|
569
654
|
)}${
|
|
570
655
|
Array.isArray(colwidth) ? attribute('colwidth', colwidth.join(',')) : ''
|
|
571
|
-
}${align ? attribute('style', `text-align: ${align}`) : ''}
|
|
572
|
-
|
|
573
|
-
)}</${tagName}>`
|
|
656
|
+
}${align ? attribute('style', `text-align: ${align}`) : ''}${
|
|
657
|
+
scope ? attribute('scope', scope) : ''
|
|
658
|
+
}>${renderChildren(node)}</${tagName}>`
|
|
574
659
|
}
|
|
575
660
|
|
|
576
661
|
function renderTable(node) {
|
|
@@ -596,12 +681,15 @@ function renderTable(node) {
|
|
|
596
681
|
: `<col style="min-width: ${minimumWidth}px;">`
|
|
597
682
|
)
|
|
598
683
|
.join('')
|
|
599
|
-
|
|
684
|
+
const caption = node.attrs?.caption
|
|
685
|
+
? `<caption>${escapeHtml(node.attrs.caption)}</caption>`
|
|
686
|
+
: ''
|
|
687
|
+
return `<div class="tableWrapper"><table class="asd20-message-table"${attribute(
|
|
600
688
|
'style',
|
|
601
689
|
tableStyle
|
|
602
|
-
)}
|
|
690
|
+
)}>${caption}<colgroup>${colgroup}</colgroup><tbody>${renderChildren(
|
|
603
691
|
node
|
|
604
|
-
)}</tbody></table>`
|
|
692
|
+
)}</tbody></table></div>`
|
|
605
693
|
}
|
|
606
694
|
|
|
607
695
|
function renderStandardNode(node) {
|
|
@@ -643,16 +731,25 @@ function renderStandardNode(node) {
|
|
|
643
731
|
)}${attribute('height', node.attrs.height)}>`
|
|
644
732
|
}
|
|
645
733
|
if (node.type === 'videoEmbed') {
|
|
646
|
-
return `<div data-cc-node="video"${attribute(
|
|
734
|
+
return `<div class="asd20-message-video" data-cc-node="video"${attribute(
|
|
647
735
|
'data-src',
|
|
648
736
|
node.attrs.src
|
|
649
737
|
)}${attribute('data-title', node.attrs.title)}${attribute(
|
|
650
738
|
'data-descriptive-text',
|
|
651
739
|
node.attrs.descriptiveText
|
|
652
|
-
)}><
|
|
740
|
+
)}><p class="asd20-message-video__description">${escapeHtml(
|
|
741
|
+
node.attrs.descriptiveText
|
|
742
|
+
)}</p><div class="asd20-message-video__frame"><iframe${attribute(
|
|
743
|
+
'src',
|
|
744
|
+
node.attrs.src
|
|
745
|
+
)}${attribute(
|
|
653
746
|
'title',
|
|
654
|
-
node.attrs.title
|
|
655
|
-
)}
|
|
747
|
+
node.attrs.title
|
|
748
|
+
)} loading="lazy" referrerpolicy="strict-origin-when-cross-origin"${attribute(
|
|
749
|
+
'allowfullscreen',
|
|
750
|
+
'',
|
|
751
|
+
true
|
|
752
|
+
)}></iframe></div></div>`
|
|
656
753
|
}
|
|
657
754
|
if (node.type === 'table') return renderTable(node)
|
|
658
755
|
if (node.type === 'tableRow') return `<tr>${renderChildren(node)}</tr>`
|