@astryxdesign/cli 0.4.5-canary.ffd05c4 → 0.4.6-canary.cea9ecd

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 (69) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/api/component/_adapter.d.mts +22 -5
  3. package/api/component/_adapter.mjs +51 -3
  4. package/api/docs/_adapter.d.mts +37 -8
  5. package/api/docs/_adapter.mjs +70 -41
  6. package/api/docs/detail/detail.d.mts +2 -0
  7. package/api/docs/detail/detail.mjs +12 -10
  8. package/api/docs/detail/section/section.d.mts +2 -0
  9. package/api/docs/detail/section/section.mjs +1 -0
  10. package/api/docs/docs.d.mts +2 -0
  11. package/api/docs/docs.doc.mjs +11 -2
  12. package/api/docs/docs.mjs +2 -1
  13. package/api/docs/docs.type.d.mts +15 -0
  14. package/api/docs/docs.type.mjs +6 -0
  15. package/api/docs/integrationDocs.test.mjs +208 -0
  16. package/api/docs/list/list.d.mts +5 -1
  17. package/api/docs/list/list.mjs +30 -14
  18. package/api/init/run/run.mjs +9 -4
  19. package/api/integration/validate-integration.mjs +2 -1
  20. package/api/integration/validate-integration.type.d.mts +1 -0
  21. package/api/integration/validate-integration.type.mjs +1 -0
  22. package/api/search/search.mjs +26 -15
  23. package/api/upgrade/_adapter.d.mts +2 -2
  24. package/api/upgrade/_adapter.mjs +7 -3
  25. package/api/upgrade/run/run.mjs +1 -1
  26. package/assets/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +154 -0
  27. package/assets/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +111 -9
  28. package/assets/docs/cli-integrations.doc.mjs +41 -3
  29. package/assets/docs/theme.doc.mjs +3 -3
  30. package/assets/templates/blocks/components/AvatarGroup/AvatarGroupInteractive.doc.mjs +15 -0
  31. package/assets/templates/blocks/components/AvatarGroup/AvatarGroupInteractive.tsx +47 -0
  32. package/assets/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.doc.mjs +1 -1
  33. package/assets/templates/blocks/components/Breadcrumbs/BreadcrumbsCustomSeparator.tsx +18 -5
  34. package/assets/templates/blocks/components/Selector/SelectorOptionDescriptions.doc.mjs +1 -1
  35. package/assets/templates/pages/dashboard-cohort-funnel/page.tsx +914 -0
  36. package/assets/templates/pages/dashboard-cohort-funnel/template.doc.mjs +12 -0
  37. package/assets/templates/pages/dashboard-data/page.tsx +894 -0
  38. package/assets/templates/pages/dashboard-data/template.doc.mjs +13 -0
  39. package/assets/templates/pages/dashboard-executive-summary/page.tsx +874 -0
  40. package/assets/templates/pages/dashboard-executive-summary/template.doc.mjs +13 -0
  41. package/assets/templates/pages/dashboard-project-status/page.tsx +1139 -0
  42. package/assets/templates/pages/dashboard-project-status/template.doc.mjs +13 -0
  43. package/assets/templates/pages/dashboard-service-monitoring/page.tsx +1590 -0
  44. package/assets/templates/pages/dashboard-service-monitoring/template.doc.mjs +12 -0
  45. package/authoring/doctypes/_schema.d.mts +2 -0
  46. package/authoring/doctypes/_schema.mjs +5 -0
  47. package/authoring/doctypes/reference/reference.doc.mjs +14 -0
  48. package/authoring/doctypes/reference/type.ts +12 -0
  49. package/authoring/doctypes/template/type.ts +2 -0
  50. package/authoring/integration/integration.doc.mjs +10 -1
  51. package/authoring/integration/parse.d.mts +1 -0
  52. package/authoring/integration/parse.mjs +1 -0
  53. package/authoring/integration/parse.test.mjs +1 -0
  54. package/authoring/integration/type.ts +5 -0
  55. package/clients/cli/commands/component-ownership.test.mjs +63 -3
  56. package/clients/cli/commands/theme-build.doc.mjs +1 -1
  57. package/foundation/agent-docs/agent-docs.d.mts +19 -3
  58. package/foundation/agent-docs/agent-docs.mjs +30 -11
  59. package/foundation/agent-docs/agent-docs.test.mjs +38 -0
  60. package/foundation/config/project.d.mts +16 -0
  61. package/foundation/config/project.mjs +65 -4
  62. package/foundation/config/project.test.mjs +66 -0
  63. package/foundation/discovery/docs-discovery.d.mts +185 -0
  64. package/foundation/discovery/docs-discovery.mjs +544 -0
  65. package/foundation/discovery/docs-discovery.test.mjs +341 -0
  66. package/foundation/integrations/integrations.d.mts +8 -6
  67. package/foundation/integrations/integrations.mjs +6 -4
  68. package/foundation/integrations/validate-contributions.mjs +30 -2
  69. package/package.json +9 -9
@@ -346,18 +346,27 @@ export default function transformer(file, api) {
346
346
  }
347
347
  // Don't double-rewrite the import specifier we already handled.
348
348
  if (parent.type === 'ImportSpecifier') return;
349
+ // JSX element tag names are handled by a source-string splice after
350
+ // `toSource()` (see renameJsxElementNamesInSource). Mutating them here
351
+ // would dirty the enclosing JSXElement and make recast collapse whitespace
352
+ // around adjacent `{expressions}`. Under the tsx parser a JSX tag-name
353
+ // JSXIdentifier is a subtype of Identifier, so it surfaces in this
354
+ // `find(j.Identifier)` pass and must be explicitly skipped.
355
+ if (isJsxElementNameIdentifier(path)) return;
349
356
  path.node.name = newName;
350
357
  hasChanges = true;
351
358
  });
352
359
 
353
- // JSX element names: <XDSButton> -> <Button>
354
- root.find(j.JSXIdentifier).forEach((/** @type {any} */ path) => {
355
- const newName = localRenames.get(path.node.name);
356
- if (newName) {
357
- path.node.name = newName;
358
- hasChanges = true;
359
- }
360
- });
360
+ // JSX element names (`<XDSButton>` -> `<Button>`) are intentionally NOT
361
+ // renamed via AST mutation here. Mutating a JSXIdentifier in place dirties the
362
+ // enclosing JSXElement; when that element is the argument of a parenthesized
363
+ // `return ( <JSX/> )` (i.e. every React component body), recast declines a
364
+ // surgical child reprint and generically re-prints the whole return argument.
365
+ // Its generic JSXElement printer drops significant whitespace from JSXText
366
+ // that sits next to a `{expression}` -- so `hello {name} world` collapses to
367
+ // `hello {name}world`. Instead we splice the element-name renames directly
368
+ // into the emitted source string (below), which never re-prints the subtree.
369
+ // See renameJsxElementNamesInSource for the mechanism.
361
370
 
362
371
  // 5. Rename TS type references in generic type-argument positions.
363
372
  //
@@ -413,5 +422,98 @@ export default function transformer(file, api) {
413
422
  };
414
423
  renameTypeReferences(root.get().node);
415
424
 
416
- return hasChanges ? root.toSource() : undefined;
425
+ if (!hasChanges) return undefined;
426
+
427
+ // Emit the AST-based renames, then splice the JSX element-name renames onto
428
+ // the emitted string (see the note in the JSX section above for why element
429
+ // names cannot be renamed through the AST without collapsing whitespace).
430
+ const emitted = root.toSource();
431
+ return renameJsxElementNamesInSource(j, emitted, localRenames);
432
+ }
433
+
434
+ /**
435
+ * Rename JSX element tag names (`<XDSButton>` / `</XDSButton>`) by splicing the
436
+ * new names directly into the already-emitted source, so recast never re-prints
437
+ * the surrounding JSX subtree (which would strip whitespace next to
438
+ * `{expressions}`).
439
+ *
440
+ * Spans are computed by re-parsing `source` itself, so the offsets are always
441
+ * valid against the string we splice into -- no dependency on the original
442
+ * pre-transform offsets. Splices are applied right-to-left (descending start
443
+ * offset) so earlier offsets stay valid as we go.
444
+ *
445
+ * Only the identifier that IS the element's tag name is renamed:
446
+ * - `<Foo attr />` -> the `Foo` opening-name only (not `attr`)
447
+ * - `<Ns.Foo />` / `<Foo.Bar>`-> the specific member-expression identifier that
448
+ * maps in `renames`; the other part of the member name is preserved
449
+ *
450
+ * @param {any} j jscodeshift instance (already bound to the tsx parser)
451
+ * @param {string} source emitted source to rewrite
452
+ * @param {Map<string, string>} renames localName -> newLocalName
453
+ * @returns {string}
454
+ */
455
+ function renameJsxElementNamesInSource(j, source, renames) {
456
+ if (renames.size === 0) return source;
457
+
458
+ const root = j(source);
459
+
460
+ /** @type {Array<{start: number, end: number, name: string}>} */
461
+ const edits = [];
462
+
463
+ root.find(j.JSXIdentifier).forEach((/** @type {any} */ path) => {
464
+ const node = path.node;
465
+ const newName = renames.get(node.name);
466
+ if (!newName || newName === node.name) return;
467
+
468
+ // The identifier must BE an element tag name -- the `.name` of a
469
+ // JSXOpeningElement/JSXClosingElement, or the matching part of a
470
+ // JSXMemberExpression tag name. Attribute names, attribute values, and the
471
+ // non-matching half of a member-expression name must be left alone.
472
+ if (!isJsxElementNameIdentifier(path)) return;
473
+
474
+ if (typeof node.start !== 'number' || typeof node.end !== 'number') return;
475
+ edits.push({start: node.start, end: node.end, name: newName});
476
+ });
477
+
478
+ if (edits.length === 0) return source;
479
+
480
+ edits.sort((a, b) => b.start - a.start);
481
+
482
+ let out = source;
483
+ for (const {start, end, name} of edits) {
484
+ out = out.slice(0, start) + name + out.slice(end);
485
+ }
486
+ return out;
487
+ }
488
+
489
+ /**
490
+ * Is this JSXIdentifier path the tag name of its element (opening or closing),
491
+ * as opposed to an attribute name or a namespace/member-expression segment we
492
+ * should not rename? For member-expression tag names (`<Foo.Bar>`) we accept
493
+ * the identifier regardless of which segment it is -- the caller only reaches
494
+ * here for identifiers whose name is in the rename map, and each renamed
495
+ * identifier maps independently, so `<Foo.Bar>` renames only the segment(s)
496
+ * that actually map.
497
+ *
498
+ * @param {any} path jscodeshift path to a JSXIdentifier
499
+ * @returns {boolean}
500
+ */
501
+ function isJsxElementNameIdentifier(path) {
502
+ const parent = path.parent && path.parent.node;
503
+ if (!parent) return false;
504
+
505
+ if (
506
+ parent.type === 'JSXOpeningElement' ||
507
+ parent.type === 'JSXClosingElement'
508
+ ) {
509
+ return parent.name === path.node;
510
+ }
511
+
512
+ // `<Foo.Bar>` / `<A.B.C>`: the tag name is a JSXMemberExpression whose
513
+ // segments are JSXIdentifiers. Rename the specific matching segment only.
514
+ if (parent.type === 'JSXMemberExpression') {
515
+ return parent.object === path.node || parent.property === path.node;
516
+ }
517
+
518
+ return false;
417
519
  }
@@ -16,7 +16,7 @@ export const docs = {
16
16
  content: [
17
17
  {
18
18
  type: 'prose',
19
- text: 'An integration is an npm package that contributes components, templates, and/or upgrade codemods to a consumer\'s design-system workflow. Consumers install the package and add it to their `astryx.config`; from then on the integration\'s contributions show up alongside core\'s in the same CLI commands.',
19
+ text: 'An integration is an npm package that contributes components, templates, doc topics, and/or upgrade codemods to a consumer\'s design-system workflow. Consumers install the package and add it to their `astryx.config`; from then on the integration\'s contributions show up alongside core\'s in the same CLI commands.',
20
20
  },
21
21
  {
22
22
  type: 'prose',
@@ -48,12 +48,12 @@ export const docs = {
48
48
  content: [
49
49
  {
50
50
  type: 'prose',
51
- text: 'To register your package as an integration, add an `astryx.integration.{ts,mjs,js}` file as a sibling of your `package.json`. It tells the CLI where to find your components, templates, and codemods. Identity (name, version) comes from your `package.json`, not this file.',
51
+ text: 'To register your package as an integration, add an `astryx.integration.{ts,mjs,js}` file as a sibling of your `package.json`. It tells the CLI where to find your components, templates, doc topics, and codemods. Identity (name, version) comes from your `package.json`, not this file.',
52
52
  },
53
53
  {
54
54
  type: 'code',
55
55
  lang: 'typescript',
56
- code: "// astryx.integration.ts\nexport default {\n components: './components',\n templates: './templates',\n codemods: './codemods',\n issuesUrl: 'https://github.com/acme/widgets/issues',\n};",
56
+ code: "// astryx.integration.ts\nexport default {\n components: './components',\n templates: './templates',\n codemods: './codemods',\n docs: './docs',\n issuesUrl: 'https://github.com/acme/widgets/issues',\n};",
57
57
  },
58
58
  {
59
59
  type: 'prose',
@@ -109,6 +109,44 @@ export const docs = {
109
109
  },
110
110
  ],
111
111
  },
112
+ {
113
+ title: 'Docs',
114
+ category: 'guide',
115
+ content: [
116
+ {
117
+ type: 'prose',
118
+ text: "Point the integration file's `docs` field at a directory of reference docs and every `{topic}.doc.{ts,mjs,js}` under it becomes a topic the CLI serves — `astryx docs` lists it, `astryx docs <topic>` prints it, `astryx search` indexes it, and `astryx init` names it in the agent block. A topic is a plain object stamped `type: 'generic'`, the same shape core's own topics use.",
119
+ },
120
+ {
121
+ type: 'code',
122
+ lang: 'typescript',
123
+ code: "// docs/deploying.doc.ts\nexport default {\n type: 'generic',\n name: 'deploying',\n title: 'Deploying',\n description: 'Ship an app built with Acme widgets.',\n category: 'guide',\n sections: [\n {title: 'Overview', content: [{type: 'prose', text: '...'}]},\n ],\n};",
124
+ },
125
+ {
126
+ type: 'prose',
127
+ text: "A topic can also speak about one that already exists. `replaces: 'x'` takes over topic x — core's, or another integration's — so a package whose consumers install it differently can serve its own Getting Started instead of the built-in one. Give the replacement a different `name` and the old name keeps resolving to it, so a link or an agent that learned the old topic still lands in the right place.",
128
+ },
129
+ {
130
+ type: 'code',
131
+ lang: 'typescript',
132
+ code: "export default {\n type: 'generic',\n name: 'getting-started',\n replaces: 'getting-started',\n title: 'Getting started',\n description: 'Install Acme widgets and use your first component.',\n sections: [/* ... */],\n};",
133
+ },
134
+ {
135
+ type: 'prose',
136
+ text: "`extends: 'x'` merges onto a topic instead of owning it: a section whose title matches one in the base replaces that section, and a section the base does not have is appended. Reach for it to correct or add to a topic you do not want to fork — a fork of someone else's guide stops receiving their fixes the day you write it.",
137
+ },
138
+ {
139
+ type: 'list',
140
+ style: 'unordered',
141
+ items: [
142
+ 'A topic name is a CLI argument and a docsite path, so it may hold only letters, digits, `_` and `-`.',
143
+ "A name that collides with an existing topic and declares neither `replaces` nor `extends` is an error, not a silent override — the CLI will not guess which one you meant.",
144
+ '`replaces` and `extends` are exclusive: a topic either takes another\'s place or merges onto it.',
145
+ 'Two integrations replacing one topic is a warning, and the one configured later in `astryx.config` wins.',
146
+ ],
147
+ },
148
+ ],
149
+ },
112
150
  {
113
151
  title: 'Codemods',
114
152
  category: 'guide',
@@ -152,7 +152,7 @@ function App() {
152
152
  },
153
153
  {
154
154
  type: 'prose',
155
- text: 'For an annotated map of the whole surface — every defineTheme field, the token families, and the component override syntax, each with the CLI command that prints its reference — run `astryx theme template`. It writes `theme.template.ts` into your project to read and copy from (`astryx init --features theme` writes it as part of project setup).',
155
+ text: 'For an annotated map of the whole surface (every defineTheme field, the token families, and the component override syntax, each with the CLI command that prints its reference), run `astryx theme template`. It writes `theme.template.ts` into your project to read and copy from (`astryx init --features theme` writes it as part of project setup).',
156
156
  },
157
157
  ],
158
158
  },
@@ -261,7 +261,7 @@ const brandTheme = defineTheme({
261
261
  },
262
262
  {
263
263
  type: 'prose',
264
- text: 'Inheritance is resolved when the theme is defined, so an extended theme is flat: `astryx theme build` emits one self-contained stylesheet holding everything the child inherited, and the base theme\'s CSS does not need to be loaded next to it. A base that is not a theme — most often an import that missed — is a build error rather than a theme that silently inherits nothing.',
264
+ text: 'Inheritance is resolved when the theme is defined, so an extended theme is flat: `astryx theme build` emits one self-contained stylesheet holding everything the child inherited, and the base theme\'s CSS does not need to be loaded next to it. A base that is not a theme (most often an import that missed) is a build error rather than a theme that silently inherits nothing.',
265
265
  },
266
266
  ],
267
267
  },
@@ -425,7 +425,7 @@ import './themes/ocean.css';
425
425
  },
426
426
  {
427
427
  type: 'prose',
428
- text: 'The build also warns when the theme names font families it does not load (webfonts like Fraunces) and prints the `<link>`/`@font-face` to add — the built CSS only sets font-family, so loading the font files stays the app\'s job. See `astryx docs typography` for the full recipe.',
428
+ text: 'The build also warns when the theme names font families it does not load (webfonts like Fraunces) and prints the `<link>`/`@font-face` to add. The built CSS only sets font-family, so loading the font files stays the app\'s job. See `astryx docs typography` for the full recipe.',
429
429
  },
430
430
  ],
431
431
  },
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'AvatarGroup',
7
+ name: 'AvatarGroup — Interactive',
8
+ displayName: 'Avatar Group — Interactive',
9
+ description:
10
+ 'A facepile whose avatars link to profiles, with an overflow indicator that opens the full member list. The whole group is one Tab stop; arrow keys move between the avatars and the overflow button.',
11
+ isReady: true,
12
+ isShowcase: false,
13
+ aspectRatio: 16 / 9,
14
+ componentsUsed: ['AvatarGroup', 'Avatar', 'Layout', 'Text'],
15
+ };
@@ -0,0 +1,47 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ 'use client';
3
+
4
+ import {AvatarGroup, AvatarGroupOverflow} from '@astryxdesign/core/AvatarGroup';
5
+ import {Avatar} from '@astryxdesign/core/Avatar';
6
+ import {Stack} from '@astryxdesign/core/Layout';
7
+ import {Text} from '@astryxdesign/core/Text';
8
+
9
+ const MEMBERS = [
10
+ {name: 'Alex Daniels', key: 'alex', href: '/team/alex'},
11
+ {name: 'Ann Smith', key: 'ann', href: '/team/ann'},
12
+ {name: 'Carol Davis', key: 'carol', href: '/team/carol'},
13
+ ];
14
+
15
+ const TOTAL_MEMBERS = 18;
16
+
17
+ export default function AvatarGroupInteractive() {
18
+ return (
19
+ <Stack direction="vertical" gap={8}>
20
+ <Stack direction="vertical" gap={3}>
21
+ <Text type="supporting" color="secondary">
22
+ Reviewers
23
+ </Text>
24
+ <AvatarGroup size="lg" aria-label="Reviewers">
25
+ {MEMBERS.map(m => (
26
+ <Avatar key={m.key} name={m.name} href={m.href} />
27
+ ))}
28
+ <AvatarGroupOverflow
29
+ count={TOTAL_MEMBERS - MEMBERS.length}
30
+ onClick={() => {}}
31
+ />
32
+ </AvatarGroup>
33
+ </Stack>
34
+ <Stack direction="vertical" gap={3}>
35
+ <Text type="supporting" color="secondary">
36
+ Compact, static
37
+ </Text>
38
+ <AvatarGroup size="sm" aria-label="Attendees">
39
+ {MEMBERS.map(m => (
40
+ <Avatar key={m.key} name={m.name} />
41
+ ))}
42
+ <AvatarGroupOverflow count={TOTAL_MEMBERS - MEMBERS.length} />
43
+ </AvatarGroup>
44
+ </Stack>
45
+ </Stack>
46
+ );
47
+ }
@@ -6,7 +6,7 @@ export const doc = {
6
6
  name: 'AvatarGroup',
7
7
  displayName: 'Avatar Group',
8
8
  description:
9
- 'Overlapping avatar rows with max limit and server-side overflow count. Shows team members in a compact facepile layout.',
9
+ 'Overlapping avatar rows with a sliced visible set and a server-side overflow count. Shows team members in a compact facepile layout.',
10
10
  isReady: true,
11
11
  isShowcase: true,
12
12
  aspectRatio: 16 / 9,
@@ -2,25 +2,38 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import * as stylex from '@stylexjs/stylex';
5
6
  import {Breadcrumbs, BreadcrumbItem} from '@astryxdesign/core/Breadcrumbs';
6
7
  import {Stack} from '@astryxdesign/core/Layout';
7
8
  import {Text} from '@astryxdesign/core/Text';
9
+ import {rtlStyles} from '@astryxdesign/core/utils';
8
10
 
11
+ // Only a glyph the bidi algorithm does not already flip needs rtlStyles.mirror.
12
+ // U+203A has Unicode Bidi_Mirrored=Yes, so the browser mirrors it under RTL on
13
+ // its own and a second flip would point it back the wrong way; U+2192 has
14
+ // Bidi_Mirrored=No and stays pointing left-to-right unless we mirror it.
9
15
  const SEPARATORS = [
10
- {char: '›', label: 'Chevron'},
11
- {char: '→', label: 'Arrow'},
12
- {char: '·', label: 'Dot'},
16
+ {char: '›', label: 'Chevron', needsRtlMirror: false},
17
+ {char: '→', label: 'Arrow', needsRtlMirror: true},
18
+ {char: '·', label: 'Dot', needsRtlMirror: false},
13
19
  ];
14
20
 
15
21
  export default function BreadcrumbsCustomSeparator() {
16
22
  return (
17
23
  <Stack direction="vertical" gap={4}>
18
- {SEPARATORS.map(({char, label}) => (
24
+ {SEPARATORS.map(({char, label, needsRtlMirror}) => (
19
25
  <Stack key={label} direction="vertical" gap={1}>
20
26
  <Text type="supporting" color="secondary">
21
27
  {label}
22
28
  </Text>
23
- <Breadcrumbs separator={char}>
29
+ <Breadcrumbs
30
+ separator={
31
+ needsRtlMirror ? (
32
+ <span {...stylex.props(rtlStyles.mirror)}>{char}</span>
33
+ ) : (
34
+ char
35
+ )
36
+ }>
24
37
  <BreadcrumbItem href="/">Home</BreadcrumbItem>
25
38
  <BreadcrumbItem href="/docs">Docs</BreadcrumbItem>
26
39
  <BreadcrumbItem isCurrent>API Reference</BreadcrumbItem>
@@ -7,7 +7,7 @@ export const doc = {
7
7
  name: 'Selector — Option descriptions',
8
8
  displayName: 'Selector — Option descriptions',
9
9
  description:
10
- 'Options carry a description, so the dropdown draws a two-line row. The closed trigger is sized by padding, so it is the size token for a one-line value and exactly one line taller for a two-line one — both on the 4px rhythm. An InputGroup pins the row, so the value folds back onto one line there.',
10
+ 'Options carry a description, so the dropdown draws a two-line row. The closed trigger is sized by padding, so it is the size token for a one-line value and exactly one line taller for a two-line one; both on the 4px rhythm. An InputGroup pins the row, so the value folds back onto one line there.',
11
11
  isReady: true,
12
12
  aspectRatio: 16 / 9,
13
13
  componentsUsed: ['Selector', 'SelectorOption', 'InputGroup', 'Button', 'Stack'],