@pcg/dynamic-components 1.0.0-alpha.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 (60) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/index.d.ts +1816 -0
  3. package/dist/index.js +1564 -0
  4. package/dist/index.js.map +1 -0
  5. package/eslint.config.cjs +14 -0
  6. package/package.json +30 -0
  7. package/src/assertions/basic.ts +58 -0
  8. package/src/assertions/containers.ts +76 -0
  9. package/src/assertions/index.ts +6 -0
  10. package/src/assertions/paths.ts +12 -0
  11. package/src/assertions/rich-text.ts +16 -0
  12. package/src/assertions/yjs.ts +25 -0
  13. package/src/data-objects/data-object.ts +34 -0
  14. package/src/data-objects/index.ts +3 -0
  15. package/src/data-objects/rich-text.ts +38 -0
  16. package/src/dynamic-components/fractional-indexing.ts +321 -0
  17. package/src/dynamic-components/index.ts +6 -0
  18. package/src/dynamic-components/paths.ts +194 -0
  19. package/src/dynamic-components/registry/chats.ts +24 -0
  20. package/src/dynamic-components/registry/content.ts +118 -0
  21. package/src/dynamic-components/registry/forms.ts +525 -0
  22. package/src/dynamic-components/registry/index.ts +6 -0
  23. package/src/dynamic-components/registry/layout.ts +86 -0
  24. package/src/dynamic-components/registry/uikit-dynamic-component.ts +84 -0
  25. package/src/dynamic-components/tools.ts +195 -0
  26. package/src/dynamic-components/types.ts +237 -0
  27. package/src/index.ts +7 -0
  28. package/src/paths/array-keys.ts +164 -0
  29. package/src/paths/array-ops.ts +124 -0
  30. package/src/paths/basic-ops.ts +181 -0
  31. package/src/paths/constants.ts +1 -0
  32. package/src/paths/index.ts +7 -0
  33. package/src/paths/tools.ts +42 -0
  34. package/src/paths/types.ts +133 -0
  35. package/src/y-components/index.ts +3 -0
  36. package/src/y-components/tools.ts +234 -0
  37. package/src/y-components/types.ts +19 -0
  38. package/src/y-tools/array-path-ops.ts +240 -0
  39. package/src/y-tools/basic-path-ops.ts +189 -0
  40. package/src/y-tools/index.ts +6 -0
  41. package/src/y-tools/tools.ts +122 -0
  42. package/src/y-tools/types.ts +32 -0
  43. package/src/y-tools/y-array-keys.ts +47 -0
  44. package/tests/assertions/basic-types.test.ts +78 -0
  45. package/tests/assertions/containers.test.ts +72 -0
  46. package/tests/assertions/paths.test.ts +23 -0
  47. package/tests/assertions/yjs.test.ts +33 -0
  48. package/tests/dynamic-components/paths.test.ts +171 -0
  49. package/tests/dynamic-components/tools.test.ts +121 -0
  50. package/tests/paths/array-keys.test.ts +182 -0
  51. package/tests/paths/array-ops.test.ts +164 -0
  52. package/tests/paths/basic-ops.test.ts +263 -0
  53. package/tests/paths/tools.test.ts +55 -0
  54. package/tests/y-components/tools.test.ts +198 -0
  55. package/tests/y-tools/array-base-ops.test.ts +55 -0
  56. package/tests/y-tools/array-path-ops.test.ts +95 -0
  57. package/tsconfig.json +13 -0
  58. package/tsconfig.lib.json +13 -0
  59. package/tsdown.config.ts +18 -0
  60. package/vitest.config.ts +19 -0
@@ -0,0 +1,321 @@
1
+ // License: CC0 (no rights reserved).
2
+
3
+ // This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
4
+
5
+ export const BASE_62_DIGITS =
6
+ '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
7
+
8
+ const SMALLEST_INTEGER = 'A00000000000000000000000000';
9
+
10
+ const INTEGER_ZERO = 'a0';
11
+
12
+ // `a` may be empty string, `b` is null or non-empty string.
13
+ // `a < b` lexicographically if `b` is non-null.
14
+ // no trailing zeros allowed.
15
+ // digits is a string such as '0123456789' for base 10. Digits must be in
16
+ // ascending character code order!
17
+ /**
18
+ * @param {string} a
19
+ * @param {string | null} b
20
+ * @param {string} digits
21
+ * @returns {string}
22
+ */
23
+ const midpoint = (a: string, b: string | null, digits: string): string => {
24
+ if (b !== null && a >= b) {
25
+ throw new Error(a + ' >= ' + b);
26
+ }
27
+ if (a.slice(-1) === '0' || (b && b.slice(-1) === '0')) {
28
+ throw new Error('trailing zero');
29
+ }
30
+ if (b) {
31
+ // remove longest common prefix. pad `a` with 0s as we
32
+ // go. note that we don't need to pad `b`, because it can't
33
+ // end before `a` while traversing the common prefix.
34
+ let n = 0;
35
+ while ((a[n] || '0') === b[n]) {
36
+ n++;
37
+ }
38
+ if (n > 0) {
39
+ return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits);
40
+ }
41
+ }
42
+ // first digits (or lack of digit) are different
43
+ const digitA = a ? digits.indexOf(a[0]) : 0;
44
+ const digitB = b !== null ? digits.indexOf(b[0]) : digits.length;
45
+ if (digitB - digitA > 1) {
46
+ const midDigit = Math.round(0.5 * (digitA + digitB));
47
+
48
+ return digits[midDigit];
49
+ } else {
50
+ // first digits are consecutive
51
+ if (b && b.length > 1) {
52
+ return b.slice(0, 1);
53
+ }
54
+
55
+ // `b` is null or has length 1 (a single digit).
56
+ // the first digit of `a` is the previous digit to `b`,
57
+ // or 9 if `b` is null.
58
+ // given, for example, midpoint('49', '5'), return
59
+ // '4' + midpoint('9', null), which will become
60
+ // '4' + '9' + midpoint('', null), which is '495'
61
+ return digits[digitA] + midpoint(a.slice(1), null, digits);
62
+ }
63
+ };
64
+
65
+ /**
66
+ * @param {string} int
67
+ * @return {void}
68
+ */
69
+
70
+ const validateInteger = (int: string) => {
71
+ if (int.length !== getIntegerLength(int[0])) {
72
+ throw new Error('invalid integer part of order key: ' + int);
73
+ }
74
+ };
75
+
76
+ /**
77
+ * @param {string} head
78
+ * @return {number}
79
+ */
80
+
81
+ const getIntegerLength = (head: string): number => {
82
+ if (head >= 'a' && head <= 'z') {
83
+ return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2;
84
+ } else if (head >= 'A' && head <= 'Z') {
85
+ return 'Z'.charCodeAt(0) - head.charCodeAt(0) + 2;
86
+ } else {
87
+ throw new Error('invalid order key head: ' + head);
88
+ }
89
+ };
90
+
91
+ /**
92
+ * @param {string} key
93
+ * @return {string}
94
+ */
95
+
96
+ const getIntegerPart = (key: string) => {
97
+ const integerPartLength = getIntegerLength(key[0]);
98
+ if (integerPartLength > key.length) {
99
+ throw new Error('invalid order key: ' + key);
100
+ }
101
+
102
+ return key.slice(0, integerPartLength);
103
+ };
104
+
105
+ /**
106
+ * @param {string} key
107
+ * @return {void}
108
+ */
109
+
110
+ const validateOrderKey = (key: string) => {
111
+ if (key === SMALLEST_INTEGER) {
112
+ throw new Error('invalid order key: ' + key);
113
+ }
114
+ // getIntegerPart will throw if the first character is bad,
115
+ // or the key is too short. we'd call it to check these things
116
+ // even if we didn't need the result
117
+ const i = getIntegerPart(key);
118
+ const f = key.slice(i.length);
119
+ if (f.slice(-1) === '0') {
120
+ throw new Error('invalid order key: ' + key);
121
+ }
122
+ };
123
+
124
+ // note that this may return null, as there is a largest integer
125
+ /**
126
+ * @param {string} x
127
+ * @param {string} digits
128
+ * @return {string | null}
129
+ */
130
+ const incrementInteger = (x: string, digits: string) => {
131
+ validateInteger(x);
132
+ const [head, ...digs] = x.split('');
133
+ let carry = true;
134
+ for (let i = digs.length - 1; carry && i >= 0; i--) {
135
+ const d = digits.indexOf(digs[i]) + 1;
136
+ if (d === digits.length) {
137
+ digs[i] = '0';
138
+ } else {
139
+ digs[i] = digits[d];
140
+ carry = false;
141
+ }
142
+ }
143
+ if (carry) {
144
+ if (head === 'Z') {
145
+ return 'a0';
146
+ }
147
+ if (head === 'z') {
148
+ return null;
149
+ }
150
+ const h = String.fromCharCode(head.charCodeAt(0) + 1);
151
+ if (h > 'a') {
152
+ digs.push('0');
153
+ } else {
154
+ digs.pop();
155
+ }
156
+
157
+ return h + digs.join('');
158
+ } else {
159
+ return head + digs.join('');
160
+ }
161
+ };
162
+
163
+ // note that this may return null, as there is a smallest integer
164
+ /**
165
+ * @param {string} x
166
+ * @param {string} digits
167
+ * @return {string | null}
168
+ */
169
+
170
+ const decrementInteger = (x: string, digits: string) => {
171
+ validateInteger(x);
172
+ const [head, ...digs] = x.split('');
173
+ let borrow = true;
174
+ for (let i = digs.length - 1; borrow && i >= 0; i--) {
175
+ const d = digits.indexOf(digs[i]) - 1;
176
+ if (d === -1) {
177
+ digs[i] = digits.slice(-1);
178
+ } else {
179
+ digs[i] = digits[d];
180
+ borrow = false;
181
+ }
182
+ }
183
+ if (borrow) {
184
+ if (head === 'a') {
185
+ return 'Z' + digits.slice(-1);
186
+ }
187
+ if (head === 'A') {
188
+ return null;
189
+ }
190
+ const h = String.fromCharCode(head.charCodeAt(0) - 1);
191
+ if (h < 'Z') {
192
+ digs.push(digits.slice(-1));
193
+ } else {
194
+ digs.pop();
195
+ }
196
+
197
+ return h + digs.join('');
198
+ } else {
199
+ return head + digs.join('');
200
+ }
201
+ };
202
+
203
+ // `a` is an order key or null (START).
204
+ // `b` is an order key or null (END).
205
+ // `a < b` lexicographically if both are non-null.
206
+ // digits is a string such as '0123456789' for base 10. Digits must be in
207
+ // ascending character code order!
208
+ /**
209
+ * @param {string | null} a
210
+ * @param {string | null} b
211
+ * @param {string=} digits
212
+ * @return {string}
213
+ */
214
+ export const generateKeyBetween = (a: string | null, b: string | null, digits = BASE_62_DIGITS) => {
215
+ if (a !== null) {
216
+ validateOrderKey(a);
217
+ }
218
+ if (b !== null) {
219
+ validateOrderKey(b);
220
+ }
221
+ if (a !== null && b !== null && a >= b) {
222
+ throw new Error(a + ' >= ' + b);
223
+ }
224
+ if (a === null) {
225
+ if (b === null) {
226
+ return INTEGER_ZERO;
227
+ }
228
+
229
+ const ib = getIntegerPart(b);
230
+ const fb = b.slice(ib.length);
231
+ if (ib === SMALLEST_INTEGER) {
232
+ return ib + midpoint('', fb, digits);
233
+ }
234
+ if (ib < b) {
235
+ return ib;
236
+ }
237
+ const res = decrementInteger(ib, digits);
238
+ if (res === null) {
239
+ throw new Error('cannot decrement any more');
240
+ }
241
+
242
+ return res;
243
+ }
244
+
245
+ if (b === null) {
246
+ const ia = getIntegerPart(a);
247
+ const fa = a.slice(ia.length);
248
+ const i = incrementInteger(ia, digits);
249
+
250
+ return i === null ? ia + midpoint(fa, null, digits) : i;
251
+ }
252
+
253
+ const ia = getIntegerPart(a);
254
+ const fa = a.slice(ia.length);
255
+ const ib = getIntegerPart(b);
256
+ const fb = b.slice(ib.length);
257
+ if (ia === ib) {
258
+ return ia + midpoint(fa, fb, digits);
259
+ }
260
+ const i = incrementInteger(ia, digits);
261
+ if (i === null) {
262
+ throw new Error('cannot increment any more');
263
+ }
264
+ if (i < b) {
265
+ return i;
266
+ }
267
+
268
+ return ia + midpoint(fa, null, digits);
269
+ };
270
+
271
+ /**
272
+ * same preconditions as generateKeysBetween.
273
+ * n >= 0.
274
+ * Returns an array of n distinct keys in sorted order.
275
+ * If a and b are both null, returns [a0, a1, ...]
276
+ * If one or the other is null, returns consecutive "integer"
277
+ * keys. Otherwise, returns relatively short keys between
278
+ * a and b.
279
+ * @param {string | null} a
280
+ * @param {string | null} b
281
+ * @param {number} n
282
+ * @param {string} digits
283
+ * @return {string[]}
284
+ */
285
+ export const generateNKeysBetween = (a: string | null, b: string | null, n: number, digits = BASE_62_DIGITS): string[] => {
286
+ if (n === 0) {
287
+ return [];
288
+ }
289
+ if (n === 1) {
290
+ return [generateKeyBetween(a, b, digits)];
291
+ }
292
+ if (b === null) {
293
+ let c = generateKeyBetween(a, b, digits);
294
+ const result = [c];
295
+ for (let i = 0; i < n - 1; i++) {
296
+ c = generateKeyBetween(c, b, digits);
297
+ result.push(c);
298
+ }
299
+
300
+ return result;
301
+ }
302
+ if (a === null) {
303
+ let c = generateKeyBetween(a, b, digits);
304
+ const result = [c];
305
+ for (let i = 0; i < n - 1; i++) {
306
+ c = generateKeyBetween(a, c, digits);
307
+ result.push(c);
308
+ }
309
+ result.reverse();
310
+
311
+ return result;
312
+ }
313
+ const mid = Math.floor(n / 2);
314
+ const c = generateKeyBetween(a, b, digits);
315
+
316
+ return [
317
+ ...generateNKeysBetween(a, c, mid, digits),
318
+ c,
319
+ ...generateNKeysBetween(c, b, n - mid - 1, digits),
320
+ ];
321
+ };
@@ -0,0 +1,6 @@
1
+ export { generateKeyBetween } from './fractional-indexing.js';
2
+ export * from './paths.js';
3
+ export * from './registry/index.js';
4
+ export * from './tools.js';
5
+ export * from './types.js';
6
+
@@ -0,0 +1,194 @@
1
+ import { isString } from '@/assertions/index.js';
2
+ import { isArrayKey } from '@/paths/index.js';
3
+
4
+ import {
5
+ isCollectionComponent,
6
+ isColumnComponent, isRepeatableCollectionComponent, isRowComponent,
7
+ } from './registry/index.js';
8
+ import { DynamicComponent } from './types.js';
9
+
10
+ /**
11
+ * Get array with components founded by rpath
12
+ *
13
+ * @example
14
+ * const components = [
15
+ * {
16
+ * id: "xxx",
17
+ * component: "collection",
18
+ * props: {
19
+ * components: [
20
+ * {
21
+ * id: "yyy",
22
+ * component: "text-input",
23
+ * props: {
24
+ * label: "Title"
25
+ * }
26
+ * }
27
+ * ]
28
+ * }
29
+ * }
30
+ * ]
31
+ *
32
+ * const { pathWithComponents } = getComponentsSubtreeByRPath(components, ['xxx', 'yyy']);
33
+ *
34
+ * // pathWithComponents
35
+ * // [
36
+ * // {
37
+ * // id: "xxx",
38
+ * // component: "collection",
39
+ * // ...
40
+ * // },
41
+ * // {
42
+ * // id: "yyy",
43
+ * // component: "text-input",
44
+ * // ...
45
+ * // }
46
+ * // ]
47
+ *
48
+ */
49
+ export const getComponentsSubtreeByRPath = (components: DynamicComponent[], rpath: string[]) => {
50
+ const pathWithComponents: DynamicComponent[] = [];
51
+
52
+ deepGetComponentsSubtreeByRPath(components, rpath, pathWithComponents);
53
+
54
+ return {
55
+ pathWithComponents,
56
+ complete: pathWithComponents.length === rpath.length,
57
+ };
58
+ };
59
+
60
+ const deepGetComponentsSubtreeByRPath = (components: DynamicComponent[], rpath: string[], pathWithComponents: DynamicComponent[]) => {
61
+ const id = rpath[0];
62
+
63
+ const component = components.find((cmp) => cmp.id === id);
64
+
65
+ if (!component) {
66
+ return;
67
+ }
68
+
69
+ pathWithComponents.push(component);
70
+
71
+ if (!Array.isArray(component.props.components)) {
72
+ return;
73
+ }
74
+
75
+ deepGetComponentsSubtreeByRPath(component.props.components, rpath.slice(1), pathWithComponents);
76
+ };
77
+
78
+ /**
79
+ * Get array with components founded by values path.
80
+ *
81
+ * @example
82
+ * const components = [
83
+ * {
84
+ * id: "xxx",
85
+ * component: "collection",
86
+ * props: {
87
+ * name: "seo",
88
+ * components: [
89
+ * {
90
+ * id: "yyy",
91
+ * component: "text-input",
92
+ * props: {
93
+ * label: "Title",
94
+ * name: "title",
95
+ * }
96
+ * }
97
+ * ]
98
+ * }
99
+ * }
100
+ * ]
101
+ *
102
+ * const { pathWithComponents } = getComponentsSubtreeByPath(components, ['seo', 'title']);
103
+ *
104
+ * // pathWithComponents
105
+ * // [
106
+ * // {
107
+ * // id: "xxx",
108
+ * // component: "collection",
109
+ * // ...
110
+ * // },
111
+ * // {
112
+ * // id: "yyy",
113
+ * // component: "text-input",
114
+ * // ...
115
+ * // }
116
+ * // ]
117
+ *
118
+ */
119
+ export const getComponentsSubtreeByPath = (components: DynamicComponent[], path: (string | number)[]) => {
120
+ const pathWithComponents: DynamicComponent[] = [];
121
+ const complete = true;
122
+
123
+ deepGetComponentsSubtreeByPath(components, path, pathWithComponents, complete);
124
+
125
+ return {
126
+ pathWithComponents,
127
+ complete,
128
+ };
129
+ };
130
+
131
+ const deepFindComponentPathKey = (components: DynamicComponent[], key: string): DynamicComponent | null => {
132
+ for (const component of components) {
133
+ if ((component.props.name && component.props.name === key) || component.id === key) {
134
+ return component;
135
+ }
136
+
137
+ if (isRowComponent(component) || isColumnComponent(component)) {
138
+ const result = deepFindComponentPathKey(component.props.components, key);
139
+ if (result) {
140
+ return result;
141
+ }
142
+ }
143
+ }
144
+
145
+ return null;
146
+ };
147
+
148
+ const deepGetComponentsSubtreeByPath = (
149
+ components: DynamicComponent[],
150
+ path: (string | number)[],
151
+ pathWithComponents: DynamicComponent[],
152
+ complete: boolean,
153
+ ) => {
154
+ const key = path[0];
155
+
156
+ if (!isString(key)) {
157
+ complete = false;
158
+
159
+ return;
160
+ }
161
+
162
+ const component = deepFindComponentPathKey(components, key);
163
+
164
+ if (!component) {
165
+ complete = false;
166
+
167
+ return;
168
+ }
169
+
170
+ pathWithComponents.push(component);
171
+
172
+ /**
173
+ * Deep dive if repeatable collection.
174
+ *
175
+ * If next key is array key find in props.components next key after array key
176
+ *
177
+ * @example
178
+ * path = ['fields', 'id:xxx', 'title']
179
+ *
180
+ * cpnst key = 'fields';
181
+ * const nextKey = 'id:xxx', // array key
182
+ * const nextKeyAfterArrayKey = 'title';
183
+ */
184
+ if (isRepeatableCollectionComponent(component)) {
185
+ const nextKey = path[1];
186
+ if (isArrayKey(nextKey) && path[2]) {
187
+ deepGetComponentsSubtreeByPath(component.props.components, path.slice(2), pathWithComponents, complete);
188
+ }
189
+ }
190
+
191
+ if (isCollectionComponent(component)) {
192
+ deepGetComponentsSubtreeByPath(component.props.components, path.slice(1), pathWithComponents, complete);
193
+ }
194
+ };
@@ -0,0 +1,24 @@
1
+ import { RichText } from '../../data-objects/rich-text.js';
2
+ import { DynamicComponent } from '../types.js';
3
+
4
+ /* Text Message */
5
+ export interface DChatTextMessage extends DynamicComponent {
6
+ component: 'chat-text-message';
7
+ props: {
8
+ content: RichText;
9
+ };
10
+ }
11
+
12
+ export const isChatTextMessageComponent = (component: DynamicComponent): component is DChatTextMessage => component.component === 'chat-text-message';
13
+
14
+ /* Message Delay */
15
+ export interface DChatMessageDelay extends DynamicComponent {
16
+ component: 'chat-message-delay';
17
+ props: {
18
+ delay: number;
19
+ typing: boolean;
20
+ };
21
+ }
22
+
23
+ // eslint-disable-next-line max-len
24
+ export const isChatMessageDelayComponent = (component: DynamicComponent): component is DChatMessageDelay => component.component === 'chat-message-delay';
@@ -0,0 +1,118 @@
1
+ import { RichText } from '../../data-objects/rich-text.js';
2
+ import { DynamicComponent } from '../types.js';
3
+
4
+ /* TEXT INPUT */
5
+ export interface DRichText extends DynamicComponent {
6
+ component: 'rich-text';
7
+ props: {
8
+ content: RichText;
9
+ };
10
+ }
11
+
12
+ export const isRichTextComponent = (component: DynamicComponent): component is DRichText => component.component === 'rich-text';
13
+
14
+ /* RowSettings */
15
+ export interface DEmbedCode extends DynamicComponent {
16
+ component: 'embed-code';
17
+ props: {
18
+ js?: string;
19
+ css?: string;
20
+ html?: string;
21
+ visible?: string;
22
+ };
23
+ }
24
+
25
+ export const isEmbedCodeComponent = (component: DynamicComponent): component is DEmbedCode => {
26
+ return component.component === 'embed-code';
27
+ };
28
+
29
+ /* Image */
30
+ export interface DImage extends DynamicComponent {
31
+ component: 'embed-code';
32
+ props: {
33
+ imageId: string;
34
+ };
35
+ }
36
+
37
+ export const isImageComponent = (component: DynamicComponent): component is DImage => component.component === 'image';
38
+
39
+ /* Legacy Content */
40
+ export interface DLegacyContent extends DynamicComponent {
41
+ component: 'legacy-content';
42
+ props: {
43
+ html: string;
44
+ };
45
+ }
46
+
47
+ export const isLegacyContentComponent = (component: DynamicComponent): component is DLegacyContent => component.component === 'legacy-content';
48
+
49
+ /* Bible Quote */
50
+ export interface DBibleQuote extends DynamicComponent {
51
+ component: 'bible-quote';
52
+ props: {
53
+ verseId: string;
54
+ };
55
+ }
56
+
57
+ export const isBibleQuoteComponent = (component: DynamicComponent): component is DBibleQuote => component.component === 'bible-quote';
58
+
59
+ /* Bible Passage */
60
+ export interface DBiblePassage extends DynamicComponent {
61
+ component: 'bible-passage';
62
+ props: {
63
+ passage: string;
64
+ };
65
+ }
66
+
67
+ export const isBiblePassageComponent = (component: DynamicComponent): component is DBiblePassage => component.component === 'bible-passage';
68
+
69
+ /* TESTS */
70
+
71
+ export interface TestItemVariant {
72
+ id: string;
73
+ content: RichText;
74
+ feedback?: RichText;
75
+ correct?: boolean;
76
+ }
77
+
78
+ export interface TestItem {
79
+ id: string;
80
+ content: RichText;
81
+ variants: TestItemVariant[];
82
+ }
83
+
84
+ export interface DTest extends DynamicComponent {
85
+ component: 'test';
86
+ props: {
87
+ title?: string;
88
+ items: TestItem[];
89
+ milestone?: string;
90
+ };
91
+ }
92
+
93
+ export const isTestComponent = (component: DynamicComponent): component is DTest => component.component === 'test';
94
+
95
+ export interface DStatement extends DynamicComponent {
96
+ component: 'statement';
97
+ props: {
98
+ content: RichText;
99
+ };
100
+ }
101
+
102
+ export const isStatementComponent = (component: DynamicComponent): component is DStatement => component.component === 'statement';
103
+
104
+ export interface DPractice extends DynamicComponent {
105
+ component: 'practice';
106
+ props: {
107
+ title?: string;
108
+ content: RichText;
109
+
110
+ /**
111
+ * Date when the practice should be checked in ISO 8601 format
112
+ * @example '2023-10-01T00:08:00Z'
113
+ */
114
+ checkAt?: string;
115
+ };
116
+ }
117
+
118
+ export const isPracticeComponent = (component: DynamicComponent): component is DPractice => component.component === 'practice';