@json-layout/vocabulary 0.14.0 → 0.15.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 (46) hide show
  1. package/package.json +5 -5
  2. package/src/components/compile-types.js +61 -0
  3. package/src/components/index.js +226 -0
  4. package/src/components/types.ts +378 -0
  5. package/src/index.js +3 -0
  6. package/src/layout-keyword/index.js +9 -2
  7. package/src/layout-keyword/schema.js +67 -62
  8. package/src/layout-keyword/schema.json +35 -50
  9. package/src/layout-keyword/types.ts +111 -42
  10. package/src/normalize.js +92 -53
  11. package/src/normalized-layout/index.js +18 -64
  12. package/src/normalized-layout/schema.js +235 -762
  13. package/src/normalized-layout/schema.json +118 -324
  14. package/src/normalized-layout/types.ts +163 -233
  15. package/src/types.ts +11 -0
  16. package/src/validate.js +42 -0
  17. package/types/components/compile-types.d.ts +13 -0
  18. package/types/components/compile-types.d.ts.map +1 -0
  19. package/types/components/index.d.ts +52 -0
  20. package/types/components/index.d.ts.map +1 -0
  21. package/types/components/types.d.ts +329 -0
  22. package/types/components/types.d.ts.map +1 -0
  23. package/types/index.d.ts +2 -0
  24. package/types/index.d.ts.map +1 -1
  25. package/types/layout-keyword/index.d.ts +6 -1
  26. package/types/layout-keyword/index.d.ts.map +1 -1
  27. package/types/layout-keyword/schema.d.ts +43 -26
  28. package/types/layout-keyword/types.d.ts +111 -18
  29. package/types/layout-keyword/types.d.ts.map +1 -1
  30. package/types/normalize.d.ts +3 -2
  31. package/types/normalize.d.ts.map +1 -1
  32. package/types/normalized-layout/index.d.ts +19 -65
  33. package/types/normalized-layout/index.d.ts.map +1 -1
  34. package/types/normalized-layout/schema.d.ts +171 -526
  35. package/types/normalized-layout/types.d.ts +163 -208
  36. package/types/normalized-layout/types.d.ts.map +1 -1
  37. package/types/types.d.ts +10 -0
  38. package/types/types.d.ts.map +1 -1
  39. package/types/validate.d.ts +8 -0
  40. package/types/validate.d.ts.map +1 -0
  41. package/src/layout-keyword/validate.js +0 -2
  42. package/src/normalized-layout/validate.js +0 -3
  43. package/types/layout-keyword/validate.d.ts +0 -23
  44. package/types/layout-keyword/validate.d.ts.map +0 -1
  45. package/types/normalized-layout/validate.d.ts +0 -23
  46. package/types/normalized-layout/validate.d.ts.map +0 -1
package/src/normalize.js CHANGED
@@ -1,10 +1,11 @@
1
- import { validateLayoutKeyword, isComponentName, isPartialCompObject, isPartialChildren, isPartialSwitch, isPartialGetItemsExpr, isPartialGetItemsObj, isPartialSlotMarkdown, isPartialGetItemsFetch } from './layout-keyword/index.js'
2
- import { validateNormalizedLayout, compositeCompNames } from './normalized-layout/index.js'
1
+ import { validateLayoutKeyword, isComponentName, isPartialCompObject, isPartialChildren, isPartialSwitch, isPartialGetItemsExpr, isPartialGetItemsObj, isPartialSlotMarkdown, isPartialGetItemsFetch, isPartialChildComposite } from './layout-keyword/index.js'
2
+ import { validateNormalizedLayout } from './normalized-layout/index.js'
3
+ import { getComponentValidate } from './validate.js'
3
4
 
4
5
  /**
5
6
  * @typedef {import('./index.js').Child} Child
6
7
  * @typedef {import('./index.js').Children} Children
7
- * @typedef {import('./index.js').CompObject} CompObject
8
+ * @typedef {import('./index.js').BaseCompObject} BaseCompObject
8
9
  * @typedef {import('./index.js').Expression} Expression
9
10
  * @typedef {import('./index.js').NormalizedLayout} NormalizedLayout
10
11
  * @typedef {import('./index.js').LayoutKeyword} LayoutKeyword
@@ -55,20 +56,20 @@ function getChildren (defaultChildren, partialChildren) {
55
56
  return partialChildren.map(partialChild => {
56
57
  if (typeof partialChild === 'string') { // simple string/key referencing a known child
57
58
  const matchingDefaultChild = defaultChildren.find(c => c.key === partialChild)
58
- if (!matchingDefaultChild) throw new Error(`child unknown ${partialChild}`)
59
+ if (!matchingDefaultChild) throw new Error(`unknown child "${partialChild}"`)
59
60
  return matchingDefaultChild
60
61
  } else {
61
62
  if (typeof partialChild.cols === 'number') partialChild.cols = { sm: partialChild.cols }
62
63
  if (typeof partialChild.cols === 'object' && partialChild.cols.xs === undefined) partialChild.cols.xs = 12
63
64
  if (partialChild.key) { // object referencing known child and overwriting cols
64
65
  const matchingDefaultChild = defaultChildren.find(c => c.key === partialChild.key)
65
- if (!matchingDefaultChild) throw new Error(`child unknown ${partialChild.key}`)
66
+ if (!matchingDefaultChild) throw new Error(`unknown child "${partialChild.key}"`)
66
67
  return /** @type {Child} */ (partialChild)
67
68
  } else { // a composite component definition, not directly related to a known child
68
69
  const child = partialChild
69
- if (partialChild.children) {
70
- if (!partialChild.comp) child.comp = 'section'
71
- child.children = getChildren(defaultChildren, partialChild.children)
70
+ if (isPartialChildComposite(child)) {
71
+ if (!child.comp) child.comp = 'section'
72
+ child.children = getChildren(defaultChildren, child.children)
72
73
  }
73
74
  if (!('key' in partialChild)) {
74
75
  child.key = `$comp-${compI}`
@@ -232,42 +233,39 @@ export const getSchemaFragmentType = (schemaFragment) => {
232
233
  * @param {LayoutKeyword} layoutKeyword
233
234
  * @param {SchemaFragment} schemaFragment
234
235
  * @param {string} schemaPath
236
+ * @param {Record<string, import('./types.js').ComponentInfo>} components
235
237
  * @param {(text: string) => string} markdown
236
238
  * @param {string[]} optionsKeys
237
239
  * @param {'oneOf'} [arrayChild]
238
- * @returns {{normalized: CompObject, errors: string[]}}
240
+ * @returns {BaseCompObject}
239
241
  */
240
- function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild) {
241
- /** @type {string[]} */
242
- const errors = []
242
+ function getCompObject (layoutKeyword, schemaFragment, schemaPath, components, markdown, optionsKeys, arrayChild) {
243
243
  const key = schemaPath.slice(schemaPath.lastIndexOf('/') + 1)
244
244
 
245
245
  const { type, nullable } = getSchemaFragmentType(schemaFragment)
246
246
 
247
- if ('const' in schemaFragment) return { normalized: { comp: 'none' }, errors }
248
- if (!type) return { normalized: { comp: 'none' }, errors }
247
+ if ('const' in schemaFragment) return { comp: 'none' }
248
+ if (!type) return { comp: 'none' }
249
249
 
250
250
  const partial = getPartialCompObject(layoutKeyword)
251
251
 
252
252
  if (type === 'array' && !schemaFragment.items && partial.comp !== 'file-input') {
253
- return { normalized: { comp: 'none' }, errors }
253
+ return { comp: 'none' }
254
254
  }
255
255
 
256
256
  // chose the default component for a schema fragment
257
257
  if (!partial.comp) {
258
- try {
259
- partial.comp = getDefaultComp(partial, schemaFragment, arrayChild)
260
- } catch (/** @type {any} */err) {
261
- errors.push(err.message)
262
- partial.comp = 'none'
263
- }
258
+ partial.comp = getDefaultComp(partial, schemaFragment, arrayChild)
264
259
  }
265
- if (partial.comp === 'none') return { normalized: { comp: 'none' }, errors }
260
+ const component = components[partial.comp]
261
+ if (!component) {
262
+ throw new Error(`unknown component "${partial.comp}"`)
263
+ }
264
+ if (partial.comp === 'none') return { comp: 'none' }
266
265
 
267
266
  if (nullable) partial.nullable = nullable
268
267
 
269
- // @ts-ignore
270
- if (compositeCompNames.includes(partial.comp)) {
268
+ if (component.composite) {
271
269
  if (!('title' in partial)) partial.title = schemaFragment.title ?? null
272
270
  partial.children = getChildren(getDefaultChildren(schemaFragment), partial.children)
273
271
  } else if (partial.comp === 'list') {
@@ -278,7 +276,7 @@ function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, opt
278
276
  if (!('label' in partial)) partial.label = schemaFragment.title ?? key
279
277
  }
280
278
 
281
- if (['select', 'autocomplete', 'combobox'].includes(partial.comp) && !partial.items) {
279
+ if (component.itemsBased && !partial.items) {
282
280
  let items
283
281
  if (type === 'array') {
284
282
  items = getItemsFromSchema(schemaFragment.items)
@@ -294,7 +292,7 @@ function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, opt
294
292
  }
295
293
  }
296
294
 
297
- if (['select', 'autocomplete', 'combobox', 'number-combobox', 'file-input'].includes(partial.comp)) {
295
+ if (component.multipleCompat) {
298
296
  if (type === 'array' || partial.separator) {
299
297
  partial.multiple = true
300
298
  }
@@ -401,36 +399,42 @@ function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, opt
401
399
  if (typeof partial.cols === 'number') partial.cols = { xs: partial.cols }
402
400
  if (typeof partial.cols === 'object' && partial.cols.xs === undefined) partial.cols.xs = 12
403
401
 
404
- return { normalized: /** @type {CompObject} */(partial), errors }
402
+ const validateComponent = getComponentValidate(component)
403
+ if (!validateComponent(partial)) {
404
+ const error = new Error(`component "${component.name}" validation errors`)
405
+ error.cause = lighterValidationErrors(validateComponent.errors)
406
+ throw error
407
+ }
408
+
409
+ return /** @type {BaseCompObject} */(partial)
405
410
  }
406
411
 
407
412
  /**
408
413
  * @param {LayoutKeyword} layoutKeyword
409
414
  * @param {SchemaFragment} schemaFragment
410
415
  * @param {string} schemaPath
416
+ * @param {Record<string, import('./types.js').ComponentInfo>} components
411
417
  * @param {(text: string) => string} markdown
412
418
  * @param {string[]} optionsKeys
413
419
  * @param {'oneOf'} [arrayChild]
414
- * @returns {{normalized: NormalizedLayout, errors: string[]}}
420
+ * @returns {NormalizedLayout}}
415
421
  */
416
- function getNormalizedLayout (layoutKeyword, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild) {
422
+ function getNormalizedLayout (layoutKeyword, schemaFragment, schemaPath, components, markdown, optionsKeys, arrayChild) {
417
423
  if (isPartialSwitch(layoutKeyword)) {
418
- /** @type {CompObject[]} */
424
+ /** @type {BaseCompObject[]} */
419
425
  const normalizedSwitchCases = []
420
- const errors = []
421
426
  const switchCases = [...layoutKeyword.switch]
422
427
  if (!switchCases.find(s => !s.if)) {
423
428
  switchCases.push({})
424
429
  }
425
430
  for (let i = 0; i < switchCases.length; i++) {
426
431
  const switchCase = switchCases[i]
427
- const compObjectResult = getCompObject(switchCase, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild)
428
- normalizedSwitchCases.push(compObjectResult.normalized)
429
- for (const error of compObjectResult.errors) errors.push(`switch ${i} - ${error}`)
432
+ const compObjectResult = getCompObject(switchCase, schemaFragment, schemaPath, components, markdown, optionsKeys, arrayChild)
433
+ normalizedSwitchCases.push(compObjectResult)
430
434
  }
431
- return { normalized: { switch: normalizedSwitchCases }, errors: [] }
435
+ return { switch: normalizedSwitchCases }
432
436
  } else {
433
- return getCompObject(layoutKeyword, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild)
437
+ return getCompObject(layoutKeyword, schemaFragment, schemaPath, components, markdown, optionsKeys, arrayChild)
434
438
  }
435
439
  }
436
440
 
@@ -447,10 +451,11 @@ function matchValidationError (error, fn) {
447
451
  }
448
452
 
449
453
  /**
450
- * @param {import('ajv').ErrorObject[]} errors
454
+ * @param {import('ajv').ErrorObject[] | null | undefined} errors
451
455
  * @returns {string[]}
452
456
  */
453
457
  function lighterValidationErrors (errors) {
458
+ if (!errors) return []
454
459
  const compositeErrors = errors.filter(e => matchValidationError(e, (e) => e.keyword === 'anyOf' || e.keyword === 'oneOf'))
455
460
  // in case of a anyOf/oneOf error there are some subschemas errors that mostly prevent readability
456
461
  for (const compositeError of compositeErrors) {
@@ -459,7 +464,13 @@ function lighterValidationErrors (errors) {
459
464
  errors = errors.filter(e => matchValidationError(e, (e) => e.instancePath !== compositeError.instancePath || e.keyword !== 'type'))
460
465
  }
461
466
  }
462
- return errors.map(e => e.message ?? e.keyword)
467
+ const messages = []
468
+ for (const error of errors) {
469
+ let message = error.message ?? error.keyword
470
+ if (error.params) message += ' ' + JSON.stringify(error.params)
471
+ messages.push(message)
472
+ }
473
+ return messages
463
474
  }
464
475
 
465
476
  const defaultOptionsKeys = ['readOnly', 'summary', 'titleDepth', 'density', 'removeAdditional', 'validateOn', 'updateOne', 'debounceInputMs', 'initialValidation', 'defaultOn', 'readOnlyPropertiesMode']
@@ -467,12 +478,13 @@ const defaultOptionsKeys = ['readOnly', 'summary', 'titleDepth', 'density', 'rem
467
478
  /**
468
479
  * @param {SchemaFragment} schemaFragment
469
480
  * @param {string} schemaPath
481
+ * @param {Record<string, import('./types.js').ComponentInfo>} components
470
482
  * @param {(text: string) => string} markdown
471
483
  * @param {string[]} [optionsKeys]
472
484
  * @param {'oneOf'} [arrayChild]
473
- * @returns {{layout: NormalizedLayout, errors: string[]}}
485
+ * @returns {NormalizedLayout}
474
486
  */
475
- export function normalizeLayoutFragment (schemaFragment, schemaPath, markdown = (src) => src, optionsKeys, arrayChild) {
487
+ function normalizeValidLayoutFragment (schemaFragment, schemaPath, components, markdown, optionsKeys, arrayChild) {
476
488
  optionsKeys = optionsKeys ? optionsKeys.concat(defaultOptionsKeys) : defaultOptionsKeys
477
489
  let layoutKeyword
478
490
  if (arrayChild === 'oneOf') {
@@ -481,20 +493,47 @@ export function normalizeLayoutFragment (schemaFragment, schemaPath, markdown =
481
493
  layoutKeyword = schemaFragment.layout ?? {}
482
494
  }
483
495
  if (!validateLayoutKeyword(layoutKeyword)) {
484
- console.error(`layout keyword validation errors at path ${schemaPath}`, layoutKeyword, validateLayoutKeyword.errors)
485
- return {
486
- layout: getNormalizedLayout({}, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild).normalized,
487
- errors: lighterValidationErrors(validateLayoutKeyword.errors)
488
- }
496
+ const error = new Error('layout keyword validation errors at path')
497
+ error.cause = lighterValidationErrors(validateLayoutKeyword.errors)
498
+ throw error
489
499
  }
490
- const normalizedLayout = getNormalizedLayout(layoutKeyword, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild)
491
- if (!validateNormalizedLayout(normalizedLayout.normalized)) {
492
- console.error(`normalized layout validation errors at path ${schemaPath}`, normalizedLayout, validateNormalizedLayout.errors)
493
- return {
494
- layout: getNormalizedLayout({}, schemaFragment, schemaPath, markdown, optionsKeys, arrayChild).normalized,
495
- errors: lighterValidationErrors(validateNormalizedLayout.errors)
500
+ const normalizedLayout = getNormalizedLayout(layoutKeyword, schemaFragment, schemaPath, components, markdown, optionsKeys, arrayChild)
501
+
502
+ if (!validateNormalizedLayout(normalizedLayout)) {
503
+ const error = new Error('normalized layout validation errors at path')
504
+ error.cause = lighterValidationErrors(validateNormalizedLayout.errors)
505
+ throw error
506
+ }
507
+ return normalizedLayout
508
+ }
509
+
510
+ /**
511
+ * @param {SchemaFragment} schemaFragment
512
+ * @param {string} schemaPath
513
+ * @param {Record<string, import('./types.js').ComponentInfo>} components
514
+ * @param {(text: string) => string} markdown
515
+ * @param {string[]} [optionsKeys]
516
+ * @param {'oneOf'} [arrayChild]
517
+ * @returns {{layout: NormalizedLayout, errors: string[]}}
518
+ */
519
+ export function normalizeLayoutFragment (schemaFragment, schemaPath, components, markdown = (src) => src, optionsKeys, arrayChild) {
520
+ /** @type {string[]} */
521
+ const errors = []
522
+ try {
523
+ const layout = normalizeValidLayoutFragment(schemaFragment, schemaPath, components, markdown, optionsKeys, arrayChild)
524
+ return { layout, errors }
525
+ } catch (/** @type {any} */err) {
526
+ try {
527
+ errors.push(err.message)
528
+ if (err.cause && Array.isArray(err.cause)) errors.push(...err.cause)
529
+ errors.push('failed to normalize layout, use default component')
530
+ const layout = normalizeValidLayoutFragment({ ...schemaFragment, layout: {} }, schemaPath, components, markdown, optionsKeys, arrayChild)
531
+ return { layout, errors }
532
+ } catch (/** @type {any} */err) {
533
+ errors.push(err.message)
534
+ if (err.cause && Array.isArray(err.cause)) errors.push(...err.cause)
535
+ errors.push('failed to produce default layout, hide this fragment')
536
+ return { layout: { comp: 'none' }, errors }
496
537
  }
497
- // throw new Error(`invalid layout at path ${schemaPath}`, { cause: validateNormalizedLayout.errors })
498
538
  }
499
- return { layout: normalizedLayout.normalized, errors: [] }
500
539
  }
@@ -1,40 +1,20 @@
1
- import validate from './validate.js'
2
1
  import schema from './schema.js'
2
+ import {ajv} from '../validate.js'
3
3
 
4
4
  /**
5
5
  * @typedef {import('./types.js').NormalizedLayout} NormalizedLayout
6
6
  * @typedef {import('./types.js').SwitchStruct} SwitchStruct
7
- * @typedef {import('./types.js').CompObject} CompObject
8
- * @typedef {import('./types.js').Section} Section
9
- * @typedef {import('./types.js').GetItems} GetItems
10
- * @typedef {import('./types.js').Expression} Expression
11
- * @typedef {import('./types.js').Select} Select
12
- * @typedef {import('./types.js').Autocomplete} Autocomplete
7
+ * @typedef {import('./types.js').BaseCompObject} BaseCompObject
13
8
  * @typedef {import('./types.js').SelectItem} SelectItem
14
9
  * @typedef {import('./types.js').SelectItems} SelectItems
15
10
  * @typedef {import('./types.js').GetItemsFetch} GetItemsFetch
16
- * @typedef {import('./types.js').TextField} TextField
17
- * @typedef {import('./types.js').Textarea} Textarea
18
- * @typedef {import('./types.js').NumberField} NumberField
19
- * @typedef {import('./types.js').Slider} Slider
20
- * @typedef {import('./types.js').Checkbox} Checkbox
21
- * @typedef {import('./types.js').Switch} Switch
22
- * @typedef {import('./types.js').DatePicker} DatePicker
23
- * @typedef {import('./types.js').TimePicker} TimePicker
24
- * @typedef {import('./types.js').DateTimePicker} DateTimePicker
25
- * @typedef {import('./types.js').ColorPicker} ColorPicker
26
- * @typedef {import('./types.js').OneOfSelect} OneOfSelect
27
11
  * @typedef {import('./types.js').Child} Child
28
12
  * @typedef {import('./types.js').Children} Children
29
13
  * @typedef {import('./types.js').CompositeCompObject} CompositeCompObject
30
- * @typedef {import('./types.js').Tabs} Tabs
31
- * @typedef {import('./types.js').VerticalTabs} VerticalTabs
32
- * @typedef {import('./types.js').ExpansionPanels} ExpansionPanels
33
- * @typedef {import('./types.js').Stepper} Stepper
34
- * @typedef {import('./types.js').List} List
35
- * @typedef {import('./types.js').Combobox} Combobox
36
- * @typedef {import('./types.js').Markdown} Markdown
37
- * @typedef {import('./types.js').FileInput} FileInput
14
+ * @typedef {import('./types.js').ItemsBasedCompObject} ItemsBasedCompObject
15
+ * @typedef {import('./types.js').FocusableCompObject} FocusableCompObject
16
+ * @typedef {import('./types.js').GetItems} GetItems
17
+ * @typedef {import('./types.js').Expression} Expression
38
18
  * @typedef {import('./types.js').Cols} Cols
39
19
  * @typedef {import('./types.js').ColsObj} ColsObj
40
20
  * @typedef {import('./types.js').StateNodeOptionsBase} StateNodeOptionsBase
@@ -43,7 +23,7 @@ import schema from './schema.js'
43
23
  * @typedef {{ errors: any, (layoutKeyword: any): layoutKeyword is NormalizedLayout }} ValidateNormalizedLayout
44
24
  */
45
25
 
46
- export const /** @type {ValidateNormalizedLayout} */ validateNormalizedLayout = /** @type {any} */ (validate)
26
+ export const /** @type {ValidateNormalizedLayout} */ validateNormalizedLayout = /** @type {any} */ (ajv.getSchema(schema.$id))
47
27
 
48
28
  export const normalizedLayoutSchema = /** @type {any} */ (schema)
49
29
 
@@ -52,55 +32,29 @@ export function isSwitchStruct (layout) {
52
32
  return typeof layout === 'object' && 'switch' in layout
53
33
  }
54
34
 
55
- /** @type {(layout: NormalizedLayout) => layout is CompObject} */
35
+ /** @type {(layout: NormalizedLayout) => layout is BaseCompObject} */
56
36
  export function isCompObject (layout) {
57
37
  return !isSwitchStruct(layout)
58
38
  }
59
39
 
60
40
  /** @type {(child: Child) => child is Child & CompositeCompObject} */
61
41
  export function childIsCompObject (child) {
62
- return !!child.comp
63
- }
64
-
65
- /** @type {(layout: CompObject) => layout is Section} */
66
- export function isSectionLayout (layout) {
67
- return layout.comp === 'section'
68
- }
69
-
70
- export const compositeCompNames = ['section', 'tabs', 'vertical-tabs', 'expansion-panels', 'stepper']
71
-
72
- // these components can received keybord inputs and emit blur events
73
- // they will be debounced when validateOn=blur is applicable and data binding will be updated on blur
74
- export const editableCompNames = ['text-field', 'number-field', 'textarea', 'markdown']
75
-
76
- /** @type {(layout: CompObject) => layout is CompositeCompObject} */
77
- export function isCompositeLayout (layout) {
78
- return compositeCompNames.includes(layout.comp)
79
- }
80
-
81
- /** @type {(layout: CompObject) => layout is TextField} */
82
- export function isTextFieldLayout (layout) {
83
- return layout.comp === 'text-field'
84
- }
85
-
86
- /** @type {(layout: CompObject) => layout is Select} */
87
- export function isSelectLayout (layout) {
88
- return layout.comp === 'select'
42
+ return 'comp' in child
89
43
  }
90
44
 
91
- /** @type {(layout: CompObject) => layout is FileInput} */
92
- export function isFileLayout (layout) {
93
- return layout.comp === 'file-input'
45
+ /** @type {(layout: BaseCompObject, components: Record<string, import('../types.js').ComponentInfo>) => layout is CompositeCompObject} */
46
+ export function isCompositeLayout (layout, components) {
47
+ return !!components[layout.comp]?.composite
94
48
  }
95
49
 
96
- /** @type {(layout: CompObject) => layout is CompObject & {autofocus: boolean}} */
97
- export function isFocusableLayout (layout) {
98
- return ['text-field', 'number-field', 'textarea', 'select', 'combobox', 'number-combobox', 'autocomplete', 'markdown'].includes(layout.comp)
50
+ /** @type {(layout: BaseCompObject, components: Record<string, import('../types.js').ComponentInfo>) => layout is FocusableCompObject} */
51
+ export function isFocusableLayout (layout, components) {
52
+ return !!components[layout.comp]?.focusable
99
53
  }
100
54
 
101
- /** @type {(layout: CompObject) => layout is Select | Combobox | Autocomplete} */
102
- export function isItemsLayout (layout) {
103
- return layout.comp === 'select' || layout.comp === 'combobox' || layout.comp === 'autocomplete'
55
+ /** @type {(layout: BaseCompObject, components: Record<string, import('../types.js').ComponentInfo>) => layout is ItemsBasedCompObject} */
56
+ export function isItemsLayout (layout, components) {
57
+ return !!components[layout.comp]?.itemsBased
104
58
  }
105
59
 
106
60
  /** @type {(getItems: GetItems) => getItems is Expression} */