@pgcorp/ui-kit 0.8.1 → 0.10.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 (113) hide show
  1. package/README.md +332 -9
  2. package/docs/getting-started.md +135 -0
  3. package/docs/public-api.md +53 -4
  4. package/docs/rich-document.md +216 -0
  5. package/docs/scroll-viewport.md +167 -0
  6. package/docs/theming.md +17 -0
  7. package/package.json +108 -50
  8. package/src/components/layout/SAppShell.vue +3 -1
  9. package/src/components/layout/SDockRegion.vue +5 -3
  10. package/src/components/layout/SResizeHandle.vue +45 -5
  11. package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
  12. package/src/components/shared/_internal/SDialogSurface.css +23 -0
  13. package/src/components/shared/_internal/SDialogSurface.vue +9 -2
  14. package/src/components/shared/_internal/SToastItem.css +4 -0
  15. package/src/components/shared/_internal/SToastItem.vue +44 -8
  16. package/src/components/shared/_internal/STreeNode.vue +24 -11
  17. package/src/components/shared/_internal/dialogSurface.ts +7 -1
  18. package/src/components/shared/complex/SKanbanLane.vue +4 -2
  19. package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
  20. package/src/components/shared/containers/SDrawer.css +11 -2
  21. package/src/components/shared/containers/SDrawer.vue +48 -9
  22. package/src/components/shared/containers/SModal.vue +5 -2
  23. package/src/components/shared/containers/SPanel.vue +4 -2
  24. package/src/components/shared/containers/SPopover.vue +7 -3
  25. package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
  26. package/src/components/shared/controls/SButton.css +14 -10
  27. package/src/components/shared/controls/SCombobox.vue +8 -6
  28. package/src/components/shared/controls/SComboboxTrigger.css +7 -4
  29. package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
  30. package/src/components/shared/controls/SContextToggleButton.vue +6 -4
  31. package/src/components/shared/controls/SDragHandle.vue +8 -4
  32. package/src/components/shared/controls/SDropdownMenu.vue +7 -4
  33. package/src/components/shared/controls/SInputText.css +5 -4
  34. package/src/components/shared/controls/SListbox.vue +4 -2
  35. package/src/components/shared/controls/SSelect.vue +4 -2
  36. package/src/components/shared/controls/STextarea.ts +13 -0
  37. package/src/components/shared/controls/STextarea.vue +391 -37
  38. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
  39. package/src/components/shared/data-display/SChart.vue +7 -5
  40. package/src/components/shared/data-display/SChip.vue +3 -1
  41. package/src/components/shared/data-display/SCodeBlock.css +4 -0
  42. package/src/components/shared/data-display/SCodeBlock.vue +46 -22
  43. package/src/components/shared/data-display/SCodeEditor.css +17 -0
  44. package/src/components/shared/data-display/SCodeEditor.vue +173 -8
  45. package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
  46. package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
  47. package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
  48. package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
  49. package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
  50. package/src/components/shared/data-display/SCopyField.vue +12 -10
  51. package/src/components/shared/data-display/SDiffViewer.css +122 -0
  52. package/src/components/shared/data-display/SDiffViewer.vue +499 -0
  53. package/src/components/shared/data-display/SDocBlock.vue +99 -71
  54. package/src/components/shared/data-display/SExpandableText.vue +10 -8
  55. package/src/components/shared/data-display/SJsonTree.vue +4 -2
  56. package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
  57. package/src/components/shared/data-display/SMessage.vue +6 -6
  58. package/src/components/shared/data-display/SRichDocument.css +8 -0
  59. package/src/components/shared/data-display/SRichDocument.vue +65 -0
  60. package/src/components/shared/data-display/SScrollViewport.css +53 -0
  61. package/src/components/shared/data-display/SScrollViewport.vue +470 -0
  62. package/src/components/shared/data-display/STable.vue +38 -19
  63. package/src/components/shared/data-display/SToastContainer.vue +22 -17
  64. package/src/components/shared/data-display/SVirtualList.ts +33 -0
  65. package/src/components/shared/data-display/SVirtualList.vue +321 -36
  66. package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
  67. package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
  68. package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
  69. package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
  70. package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
  71. package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
  72. package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
  73. package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
  74. package/src/components/shared/data-display/codeEditor.ts +200 -0
  75. package/src/components/shared/data-display/codeWorkspace.ts +233 -0
  76. package/src/components/shared/data-display/diff.ts +541 -0
  77. package/src/components/shared/data-display/richDocument.ts +876 -0
  78. package/src/components/shared/data-display/scrollViewport.ts +47 -0
  79. package/src/components/shared/data-display/toast.ts +194 -0
  80. package/src/components/shared/database/SDataGrid.vue +7 -4
  81. package/src/components/shared/database/SSqlEditor.vue +39 -39
  82. package/src/components/shared/feedback/SAsyncState.vue +3 -1
  83. package/src/components/shared/graph/SGraphViewport.vue +11 -9
  84. package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
  85. package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
  86. package/src/components/shared/navigation/STabList.vue +10 -6
  87. package/src/components/shared/navigation/STabs.vue +15 -11
  88. package/src/components/shared/navigation/SWizardSteps.vue +4 -2
  89. package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
  90. package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
  91. package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
  92. package/src/components/shared/persona/_internal/labels.ts +66 -57
  93. package/src/components/shared/persona/presentation.ts +140 -110
  94. package/src/composables/useFloatingPosition.ts +39 -2
  95. package/src/composables/useUiKitLocale.ts +17 -0
  96. package/src/internal/codeHighlight.ts +110 -0
  97. package/src/internal/diffEngine.ts +464 -0
  98. package/src/internal/diffRuntime.ts +189 -0
  99. package/src/internal/feedbackPresentation.ts +33 -14
  100. package/src/internal/localeContext.ts +22 -0
  101. package/src/internal/ownedAttrs.ts +8 -0
  102. package/src/internal/runtimeContract.ts +11 -9
  103. package/src/internal/semanticSizing.ts +13 -0
  104. package/src/locales/catalog.ts +634 -0
  105. package/src/locales/locale.ts +472 -0
  106. package/src/locales/messages.generated.ts +1826 -0
  107. package/src/locales/messages.ts +150 -0
  108. package/src/stores/useNotifierStore.ts +61 -44
  109. package/src/styles/tokens.css +23 -2
  110. package/src/theme.ts +59 -32
  111. package/src/workers/diff.worker.ts +73 -0
  112. package/src/workers/diffProtocol.ts +155 -0
  113. package/src/components/shared/data-display/SToastContainer.css +0 -53
@@ -0,0 +1,85 @@
1
+ @reference "../../../../styles/reference.css";
2
+
3
+ .s-rich-document__paragraph {
4
+ min-width: 0;
5
+ margin: 0;
6
+ }
7
+
8
+ .s-rich-document__heading {
9
+ @apply font-semibold text-surface-950 dark:text-surface-50;
10
+ min-width: 0;
11
+ margin: 0;
12
+ line-height: 1.25;
13
+ }
14
+
15
+ h1.s-rich-document__heading { @apply text-3xl; }
16
+ h2.s-rich-document__heading { @apply text-2xl; }
17
+ h3.s-rich-document__heading { @apply text-xl; }
18
+ h4.s-rich-document__heading { @apply text-lg; }
19
+ h5.s-rich-document__heading,
20
+ h6.s-rich-document__heading { @apply text-base; }
21
+
22
+ .s-rich-document__list {
23
+ display: grid;
24
+ gap: var(--s-space-content-gap);
25
+ margin: 0;
26
+ padding-inline-start: 1.5rem;
27
+ }
28
+
29
+ ul.s-rich-document__list { list-style: disc; }
30
+ ol.s-rich-document__list { list-style: decimal; }
31
+
32
+ .s-rich-document__quote {
33
+ @apply border-l-4 border-primary-300 text-surface-600 dark:border-primary-700 dark:text-surface-300;
34
+ display: grid;
35
+ gap: var(--s-space-content-gap);
36
+ margin: 0;
37
+ padding-inline-start: var(--s-space-section-gap);
38
+ }
39
+
40
+ .s-rich-document__divider {
41
+ @apply border-0 border-t border-surface-200 dark:border-surface-700;
42
+ width: 100%;
43
+ margin: 0;
44
+ }
45
+
46
+ .s-rich-document__figure {
47
+ display: grid;
48
+ gap: var(--s-space-content-gap);
49
+ min-width: 0;
50
+ margin: 0;
51
+ }
52
+
53
+ .s-rich-document__image {
54
+ @apply rounded-lg border border-surface-200 bg-surface-50 dark:border-surface-700 dark:bg-surface-900;
55
+ display: block;
56
+ width: auto;
57
+ max-width: 100%;
58
+ height: auto;
59
+ margin-inline: auto;
60
+ }
61
+
62
+ .s-rich-document__caption,
63
+ .s-rich-document__resource-description {
64
+ @apply text-sm text-surface-500 dark:text-surface-400;
65
+ }
66
+
67
+ .s-rich-document__caption {
68
+ text-align: center;
69
+ }
70
+
71
+ .s-rich-document__resource {
72
+ @apply flex min-w-0 flex-wrap items-center;
73
+ gap: var(--s-space-content-gap);
74
+ }
75
+
76
+ .s-rich-document__callout-content {
77
+ display: grid;
78
+ gap: var(--s-space-content-gap);
79
+ min-width: 0;
80
+ }
81
+
82
+ .s-rich-document__actions {
83
+ @apply flex min-w-0 flex-wrap items-center;
84
+ gap: var(--s-space-content-gap);
85
+ }
@@ -0,0 +1,282 @@
1
+ <template>
2
+ <p v-if="node.type === 'paragraph'" class="s-rich-document__paragraph">
3
+ <SRichInlineNode
4
+ v-for="(child, index) in node.children"
5
+ :key="index"
6
+ :node="child"
7
+ :document-id="documentId"
8
+ :path="`${path}.children[${index}]`"
9
+ @link-activate="emit('link-activate', $event)"
10
+ />
11
+ </p>
12
+ <component :is="`h${node.level}`" v-else-if="node.type === 'heading'" class="s-rich-document__heading">
13
+ <SRichInlineNode
14
+ v-for="(child, index) in node.children"
15
+ :key="index"
16
+ :node="child"
17
+ :document-id="documentId"
18
+ :path="`${path}.children[${index}]`"
19
+ @link-activate="emit('link-activate', $event)"
20
+ />
21
+ </component>
22
+ <component
23
+ :is="node.ordered ? 'ol' : 'ul'"
24
+ v-else-if="node.type === 'list'"
25
+ class="s-rich-document__list"
26
+ :start="node.ordered ? node.start : undefined"
27
+ >
28
+ <li v-for="(item, itemIndex) in node.children" :key="itemIndex">
29
+ <SRichBlockNode
30
+ v-for="(child, childIndex) in item.children"
31
+ :key="childIndex"
32
+ :node="child"
33
+ :resources="resources"
34
+ :document-id="documentId"
35
+ :path="`${path}.children[${itemIndex}].children[${childIndex}]`"
36
+ @link-activate="emit('link-activate', $event)"
37
+ @resource-activate="emit('resource-activate', $event)"
38
+ @action="emit('action', $event)"
39
+ />
40
+ </li>
41
+ </component>
42
+ <blockquote v-else-if="node.type === 'quote'" class="s-rich-document__quote">
43
+ <SRichBlockNode
44
+ v-for="(child, index) in node.children"
45
+ :key="index"
46
+ :node="child"
47
+ :resources="resources"
48
+ :document-id="documentId"
49
+ :path="`${path}.children[${index}]`"
50
+ @link-activate="emit('link-activate', $event)"
51
+ @resource-activate="emit('resource-activate', $event)"
52
+ @action="emit('action', $event)"
53
+ />
54
+ </blockquote>
55
+ <hr v-else-if="node.type === 'divider'" class="s-rich-document__divider" />
56
+ <figure v-else-if="node.type === 'image'" class="s-rich-document__figure">
57
+ <img
58
+ class="s-rich-document__image"
59
+ :src="imageResource(node.resourceId).url"
60
+ :alt="imageResource(node.resourceId).alt"
61
+ :title="imageResource(node.resourceId).title"
62
+ loading="lazy"
63
+ decoding="async"
64
+ />
65
+ <figcaption v-if="node.caption" class="s-rich-document__caption">
66
+ <SRichInlineNode
67
+ v-for="(child, index) in node.caption"
68
+ :key="index"
69
+ :node="child"
70
+ :document-id="documentId"
71
+ :path="`${path}.caption[${index}]`"
72
+ @link-activate="emit('link-activate', $event)"
73
+ />
74
+ </figcaption>
75
+ </figure>
76
+ <div v-else-if="node.type === 'resource'" class="s-rich-document__resource">
77
+ <SButton
78
+ :label="node.label"
79
+ severity="secondary"
80
+ appearance="soft"
81
+ size="sm"
82
+ @click="activateResource(node.resourceId)"
83
+ />
84
+ <span v-if="node.description" class="s-rich-document__resource-description">{{ node.description }}</span>
85
+ </div>
86
+ <AsyncSCodeBlock
87
+ v-else-if="node.type === 'codeBlock'"
88
+ :code="node.code"
89
+ :language="{ mode: 'explicit', id: node.language ?? 'text' }"
90
+ presentation="highlighted"
91
+ :show-header="node.language !== undefined || node.label !== undefined"
92
+ :show-line-numbers="node.showLineNumbers ?? true"
93
+ read-only
94
+ viewport="document"
95
+ :aria-label="node.label ?? t('editor.codeBlock')"
96
+ />
97
+ <AsyncSRichMath
98
+ v-else-if="node.type === 'mathBlock'"
99
+ :source="node.source"
100
+ :label="node.label"
101
+ display="block"
102
+ />
103
+ <STable
104
+ v-else-if="node.type === 'table'"
105
+ :columns="tableColumns"
106
+ :data="node.rows"
107
+ :get-row-key="rowKey"
108
+ :aria-label="node.label"
109
+ surface="plain"
110
+ density="compact"
111
+ min-width="content"
112
+ >
113
+ <template #cell="{ row, column }">
114
+ <SRichInlineNode
115
+ v-for="(child, index) in tableCell(row, column.field).children"
116
+ :key="index"
117
+ :node="child"
118
+ :document-id="documentId"
119
+ :path="`${path}.rows[${tableRowIndex(row)}].cells[${tableColumnIndex(column.field)}].children[${index}]`"
120
+ @link-activate="emit('link-activate', $event)"
121
+ />
122
+ </template>
123
+ </STable>
124
+ <SPanel
125
+ v-else-if="node.type === 'callout' && node.tone === 'neutral'"
126
+ :title="node.title"
127
+ :heading-level="3"
128
+ surface-tone="subtle"
129
+ content-layout="stack"
130
+ content-gap="content"
131
+ >
132
+ <SRichBlockNode
133
+ v-for="(child, index) in node.children"
134
+ :key="index"
135
+ :node="child"
136
+ :resources="resources"
137
+ :document-id="documentId"
138
+ :path="`${path}.children[${index}]`"
139
+ @link-activate="emit('link-activate', $event)"
140
+ @resource-activate="emit('resource-activate', $event)"
141
+ @action="emit('action', $event)"
142
+ />
143
+ </SPanel>
144
+ <SMessage
145
+ v-else-if="node.type === 'callout'"
146
+ :severity="calloutSeverity(node.tone)"
147
+ :title="node.title"
148
+ live="off"
149
+ >
150
+ <div class="s-rich-document__callout-content">
151
+ <SRichBlockNode
152
+ v-for="(child, index) in node.children"
153
+ :key="index"
154
+ :node="child"
155
+ :resources="resources"
156
+ :document-id="documentId"
157
+ :path="`${path}.children[${index}]`"
158
+ @link-activate="emit('link-activate', $event)"
159
+ @resource-activate="emit('resource-activate', $event)"
160
+ @action="emit('action', $event)"
161
+ />
162
+ </div>
163
+ </SMessage>
164
+ <div v-else-if="node.type === 'actionGroup'" class="s-rich-document__actions" role="group" :aria-label="node.label">
165
+ <SButton
166
+ v-for="action in node.actions"
167
+ :key="action.id"
168
+ :label="action.label"
169
+ :severity="actionSeverity(action.appearance)"
170
+ :appearance="action.appearance === 'primary' ? 'solid' : 'soft'"
171
+ :disabled="action.disabled"
172
+ size="sm"
173
+ @click="activateAction(action.id)"
174
+ />
175
+ </div>
176
+ </template>
177
+
178
+ <script setup lang="ts">
179
+ import { computed, defineAsyncComponent } from 'vue'
180
+
181
+ import { useUiKitLocale } from '../../../../composables/useUiKitLocale'
182
+ import SPanel from '../../containers/SPanel.vue'
183
+ import SButton, { type SButtonSeverity } from '../../controls/SButton.vue'
184
+ import SMessage, { type Severity as SMessageSeverity } from '../SMessage.vue'
185
+ import STable from '../STable.vue'
186
+ import type { STableColumn } from '../table'
187
+ import type {
188
+ SRichActionAppearance,
189
+ SRichBlockNode,
190
+ SRichCalloutTone,
191
+ SRichDocumentActionIntent,
192
+ SRichDocumentImageResource,
193
+ SRichDocumentLinkActivate,
194
+ SRichDocumentResource,
195
+ SRichDocumentResourceActivate,
196
+ SRichTableCell,
197
+ SRichTableRow,
198
+ } from '../richDocument'
199
+ import SRichInlineNode from './SRichInlineNode.vue'
200
+
201
+ defineOptions({ inheritAttrs: false })
202
+
203
+ const props = defineProps<{
204
+ node: SRichBlockNode
205
+ resources: readonly SRichDocumentResource[]
206
+ documentId: string
207
+ path: string
208
+ }>()
209
+
210
+ const emit = defineEmits<{
211
+ 'link-activate': [payload: SRichDocumentLinkActivate]
212
+ 'resource-activate': [payload: SRichDocumentResourceActivate]
213
+ action: [payload: SRichDocumentActionIntent]
214
+ }>()
215
+
216
+ const { t } = useUiKitLocale()
217
+ const AsyncSCodeBlock = defineAsyncComponent(() => import('../SCodeBlock.vue'))
218
+ const AsyncSRichMath = defineAsyncComponent(() => import('./SRichMath.vue'))
219
+ const tableColumns = computed<readonly STableColumn<SRichTableRow, string>[]>(() => {
220
+ if (props.node.type !== 'table') return []
221
+ return Object.freeze(props.node.columns.map((column, index) => Object.freeze({
222
+ field: column.id,
223
+ header: column.header,
224
+ headerPresentation: column.headerPresentation,
225
+ align: column.align,
226
+ content: 'value' as const,
227
+ value: (row: SRichTableRow) => row.cells[index],
228
+ })))
229
+ })
230
+
231
+ function rowKey(row: SRichTableRow): string {
232
+ return row.id
233
+ }
234
+
235
+ function tableColumnIndex(field: string): number {
236
+ if (props.node.type !== 'table') throw new Error('SRichBlockNode: table column outside table node.')
237
+ const index = props.node.columns.findIndex((column) => column.id === field)
238
+ if (index < 0) throw new Error(`SRichBlockNode: unknown table column ${JSON.stringify(field)}.`)
239
+ return index
240
+ }
241
+
242
+ function tableRowIndex(row: SRichTableRow): number {
243
+ if (props.node.type !== 'table') throw new Error('SRichBlockNode: table row outside table node.')
244
+ const index = props.node.rows.findIndex((candidate) => candidate.id === row.id)
245
+ if (index < 0) throw new Error(`SRichBlockNode: unknown table row ${JSON.stringify(row.id)}.`)
246
+ return index
247
+ }
248
+
249
+ function tableCell(row: SRichTableRow, field: string): SRichTableCell {
250
+ const cell = row.cells[tableColumnIndex(field)]
251
+ if (!cell) throw new Error(`SRichBlockNode: missing table cell for ${JSON.stringify(field)}.`)
252
+ return cell
253
+ }
254
+
255
+ function imageResource(id: string): SRichDocumentImageResource {
256
+ const resource = props.resources.find((candidate) => candidate.id === id)
257
+ if (resource?.kind !== 'image') throw new Error(`SRichBlockNode: image resource ${JSON.stringify(id)} is unavailable.`)
258
+ return resource
259
+ }
260
+
261
+ function activateResource(resourceId: string): void {
262
+ emit('resource-activate', Object.freeze({ documentId: props.documentId, path: props.path, resourceId }))
263
+ }
264
+
265
+ function activateAction(actionId: string): void {
266
+ emit('action', Object.freeze({ documentId: props.documentId, path: props.path, actionId }))
267
+ }
268
+
269
+ function calloutSeverity(tone: SRichCalloutTone): SMessageSeverity {
270
+ if (tone === 'information' || tone === 'neutral') return 'info'
271
+ if (tone === 'warning') return 'warn'
272
+ return tone
273
+ }
274
+
275
+ function actionSeverity(appearance: SRichActionAppearance | undefined): SButtonSeverity {
276
+ if (appearance === 'danger') return 'danger'
277
+ if (appearance === 'secondary') return 'secondary'
278
+ return 'primary'
279
+ }
280
+ </script>
281
+
282
+ <style lang="postcss" src="./SRichBlockNode.css" scoped></style>
@@ -0,0 +1,6 @@
1
+ @reference "../../../../styles/reference.css";
2
+
3
+ .s-rich-document__inline-code {
4
+ @apply rounded bg-surface-100 px-1 py-0.5 font-mono text-sm text-surface-800;
5
+ @apply dark:bg-surface-800 dark:text-surface-100;
6
+ }
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <template v-if="node.type === 'text'">{{ node.value }}</template>
3
+ <strong v-else-if="node.type === 'strong'">
4
+ <SRichInlineNode
5
+ v-for="(child, index) in node.children"
6
+ :key="index"
7
+ :node="child"
8
+ :document-id="documentId"
9
+ :path="`${path}.children[${index}]`"
10
+ @link-activate="emit('link-activate', $event)"
11
+ />
12
+ </strong>
13
+ <em v-else-if="node.type === 'emphasis'">
14
+ <SRichInlineNode
15
+ v-for="(child, index) in node.children"
16
+ :key="index"
17
+ :node="child"
18
+ :document-id="documentId"
19
+ :path="`${path}.children[${index}]`"
20
+ @link-activate="emit('link-activate', $event)"
21
+ />
22
+ </em>
23
+ <code v-else-if="node.type === 'inlineCode'" class="s-rich-document__inline-code">{{ node.code }}</code>
24
+ <SLink
25
+ v-else-if="node.type === 'link'"
26
+ :href="node.href"
27
+ :title="node.title"
28
+ :active="node.current"
29
+ variant="document"
30
+ wrap="anywhere"
31
+ @click.prevent="activateLink"
32
+ >
33
+ <SRichInlineNode
34
+ v-for="(child, index) in node.children"
35
+ :key="index"
36
+ :node="child"
37
+ :document-id="documentId"
38
+ :path="`${path}.children[${index}]`"
39
+ @link-activate="emit('link-activate', $event)"
40
+ />
41
+ </SLink>
42
+ <AsyncSRichMath v-else-if="node.type === 'mathInline'" :source="node.source" display="inline" />
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import { defineAsyncComponent } from 'vue'
47
+
48
+ import SLink from '../../controls/SLink.vue'
49
+ import type { SRichDocumentLinkActivate, SRichInlineNode } from '../richDocument'
50
+
51
+ defineOptions({ inheritAttrs: false })
52
+
53
+ const props = defineProps<{
54
+ node: SRichInlineNode
55
+ documentId: string
56
+ path: string
57
+ }>()
58
+
59
+ const emit = defineEmits<{
60
+ 'link-activate': [payload: SRichDocumentLinkActivate]
61
+ }>()
62
+
63
+ const AsyncSRichMath = defineAsyncComponent(() => import('./SRichMath.vue'))
64
+
65
+ function activateLink(): void {
66
+ if (props.node.type !== 'link') return
67
+ emit('link-activate', Object.freeze({
68
+ documentId: props.documentId,
69
+ path: props.path,
70
+ href: props.node.href,
71
+ ...(props.node.title === undefined ? {} : { title: props.node.title }),
72
+ }))
73
+ }
74
+ </script>
75
+
76
+ <style lang="postcss" src="./SRichInlineNode.css" scoped></style>
@@ -0,0 +1,42 @@
1
+ @reference "../../../../styles/reference.css";
2
+
3
+ .s-rich-math {
4
+ min-width: 0;
5
+ }
6
+
7
+ .s-rich-math[data-display='block'] {
8
+ max-width: 100%;
9
+ overflow-x: auto;
10
+ padding-block: var(--s-space-content-gap);
11
+ text-align: center;
12
+ }
13
+
14
+ .s-rich-math__surface:empty {
15
+ display: none;
16
+ }
17
+
18
+ .s-rich-math__fallback,
19
+ .s-rich-math__error code {
20
+ @apply rounded bg-surface-100 px-1 py-0.5 font-mono text-sm text-surface-700;
21
+ @apply dark:bg-surface-800 dark:text-surface-200;
22
+ }
23
+
24
+ .s-rich-math__error {
25
+ @apply inline-flex min-w-0 flex-wrap items-baseline gap-2 text-danger-700 dark:text-danger-300;
26
+ }
27
+
28
+ .s-rich-math__assistive {
29
+ position: absolute;
30
+ width: 1px;
31
+ height: 1px;
32
+ padding: 0;
33
+ margin: -1px;
34
+ overflow: hidden;
35
+ clip: rect(0, 0, 0, 0);
36
+ white-space: nowrap;
37
+ border: 0;
38
+ }
39
+
40
+ .s-rich-math .katex-display {
41
+ margin: 0;
42
+ }
@@ -0,0 +1,102 @@
1
+ <template>
2
+ <component
3
+ :is="validatedDisplay === 'block' ? 'div' : 'span'"
4
+ class="s-rich-math"
5
+ :data-display="validatedDisplay"
6
+ :data-state="state"
7
+ >
8
+ <span ref="surface" class="s-rich-math__surface" />
9
+ <code v-if="state === 'loading'" class="s-rich-math__fallback">{{ validatedSource }}</code>
10
+ <span v-if="state === 'loading'" class="s-rich-math__assistive" role="status">{{ t('editor.mathLoading') }}</span>
11
+ <span v-if="state === 'error'" class="s-rich-math__error" role="alert">
12
+ <span>{{ t('editor.mathError') }}</span>
13
+ <code>{{ validatedSource }}</code>
14
+ </span>
15
+ </component>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
20
+ import 'katex/dist/katex.min.css'
21
+
22
+ import { useUiKitLocale } from '../../../../composables/useUiKitLocale'
23
+ import { validateExactString, validateNonEmptyString } from '../../../../internal/runtimeContract'
24
+
25
+ defineOptions({ inheritAttrs: false })
26
+
27
+ const props = withDefaults(defineProps<{
28
+ source: string
29
+ display?: 'inline' | 'block'
30
+ label?: string
31
+ }>(), {
32
+ display: 'inline',
33
+ label: undefined,
34
+ })
35
+
36
+ const { t } = useUiKitLocale()
37
+ const surface = ref<HTMLElement | null>(null)
38
+ const state = ref<'loading' | 'ready' | 'error'>('loading')
39
+ const validatedSource = computed(() => validateNonEmptyString('SRichMath', 'source', props.source))
40
+ const validatedDisplay = computed(() => validateExactString(
41
+ 'SRichMath',
42
+ 'display',
43
+ props.display,
44
+ ['inline', 'block'] as const,
45
+ ))
46
+ const validatedLabel = computed(() => props.label === undefined
47
+ ? validatedSource.value
48
+ : validateNonEmptyString('SRichMath', 'label', props.label))
49
+ let mounted = false
50
+ let generation = 0
51
+
52
+ async function renderMath(): Promise<void> {
53
+ if (!mounted) return
54
+ const target = surface.value
55
+ if (!target) throw new Error('SRichMath: render surface is unavailable.')
56
+ const currentGeneration = generation += 1
57
+ state.value = 'loading'
58
+ target.replaceChildren()
59
+ try {
60
+ const katex = await import('katex')
61
+ if (currentGeneration !== generation || !mounted) return
62
+ katex.default.render(validatedSource.value, target, {
63
+ displayMode: validatedDisplay.value === 'block',
64
+ output: 'htmlAndMathml',
65
+ strict: 'error',
66
+ throwOnError: true,
67
+ trust: false,
68
+ maxExpand: 1_000,
69
+ maxSize: 20,
70
+ })
71
+ target.setAttribute('role', 'math')
72
+ target.setAttribute('aria-label', validatedLabel.value)
73
+ state.value = 'ready'
74
+ } catch {
75
+ if (currentGeneration !== generation || !mounted) return
76
+ target.replaceChildren()
77
+ target.removeAttribute('role')
78
+ target.removeAttribute('aria-label')
79
+ state.value = 'error'
80
+ }
81
+ }
82
+
83
+ onMounted(() => {
84
+ mounted = true
85
+ void renderMath()
86
+ })
87
+
88
+ watch(
89
+ () => [props.source, props.display, props.label] as const,
90
+ () => { void renderMath() },
91
+ )
92
+
93
+ onBeforeUnmount(() => {
94
+ mounted = false
95
+ generation += 1
96
+ surface.value?.replaceChildren()
97
+ surface.value?.removeAttribute('role')
98
+ surface.value?.removeAttribute('aria-label')
99
+ })
100
+ </script>
101
+
102
+ <style lang="postcss" src="./SRichMath.css"></style>