@ctrliq/quantic-components 1.86.0 → 1.87.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 (72) hide show
  1. package/dist/action-group/action-group.component.js +7 -2
  2. package/dist/badge/badge-group.component.js +1 -2
  3. package/dist/badge/badge.component.js +8 -3
  4. package/dist/button/index.js +3 -2
  5. package/dist/button-bar/button-bar-item.js +1 -0
  6. package/dist/button-bar/button-bar.js +0 -1
  7. package/dist/button-group/button-group-item.js +3 -0
  8. package/dist/button-group/button-group.js +0 -1
  9. package/dist/card/card-image.component.js +1 -0
  10. package/dist/card/card-meta.component.js +1 -0
  11. package/dist/card/card-title.component.js +1 -0
  12. package/dist/card/card.component.js +14 -21
  13. package/dist/checkbox/checkbox-group.component.js +0 -1
  14. package/dist/dialog/dialog-actions.js +1 -1
  15. package/dist/dialog/dialog-title.js +1 -0
  16. package/dist/dialog/dialog.js +0 -1
  17. package/dist/dropdown-menu/dropdown-menu.component.js +0 -4
  18. package/dist/dropdown-menu/group.component.js +1 -0
  19. package/dist/dropdown-menu/item.component.js +1 -0
  20. package/dist/field/field.stories.d.ts +1 -0
  21. package/dist/field/field.stories.js +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/layout/layout.component.js +1 -1
  24. package/dist/layout/sidebar-menu-item-group.component.js +1 -0
  25. package/dist/layout/sidebar-menu-item.component.js +2 -0
  26. package/dist/layout/sidebar.component.js +1 -4
  27. package/dist/meta/all-meta.test.js +18 -0
  28. package/dist/meta/index.js.map +1 -1
  29. package/dist/meta/serialize.d.ts +3 -1
  30. package/dist/meta/serialize.js +20 -3
  31. package/dist/meta/serialize.test.js +30 -0
  32. package/dist/meta/slot-parity.test.d.ts +1 -0
  33. package/dist/meta/slot-parity.test.js +83 -0
  34. package/dist/meta.json +155 -58
  35. package/dist/number-input/index.js +1 -0
  36. package/dist/option-card/option-card-group.component.js +0 -1
  37. package/dist/option-card/option-card.component.js +1 -0
  38. package/dist/panel/panel-group.component.js +0 -1
  39. package/dist/panel/panel-header.component.js +1 -0
  40. package/dist/panel/panel.component.js +0 -1
  41. package/dist/popover/popover-popup.js +1 -0
  42. package/dist/popover/popover.js +0 -1
  43. package/dist/quantic-components.js +84 -70
  44. package/dist/quantic-components.js.map +1 -1
  45. package/dist/quantic-components.min.js +61 -79
  46. package/dist/quantic-components.min.js.map +1 -1
  47. package/dist/radio/radio.component.js +1 -0
  48. package/dist/register.js.map +1 -1
  49. package/dist/select/index.js +7 -0
  50. package/dist/shared/meta.types.d.ts +21 -1
  51. package/dist/splitter/splitter-group.component.js +0 -1
  52. package/dist/splitter/splitter-pane.component.js +1 -0
  53. package/dist/table/table-body.component.js +1 -0
  54. package/dist/table/table-cell.component.js +1 -0
  55. package/dist/table/table-controls.component.js +1 -0
  56. package/dist/table/table-footer.component.js +1 -0
  57. package/dist/table/table-head.component.js +1 -0
  58. package/dist/table/table-header.component.js +1 -0
  59. package/dist/table/table-row.component.js +1 -0
  60. package/dist/table/table.component.js +0 -9
  61. package/dist/tabs/button.component.js +1 -0
  62. package/dist/tabs/panel.component.js +1 -0
  63. package/dist/tabs/tabs.component.js +0 -1
  64. package/dist/tag/tag-group.component.js +6 -2
  65. package/dist/text-input/index.js +6 -8
  66. package/dist/tooltip/tooltip-popup.js +1 -0
  67. package/dist/tooltip/tooltip.js +0 -1
  68. package/dist/types/field/field.stories.d.ts +1 -0
  69. package/dist/types/meta/serialize.d.ts +3 -1
  70. package/dist/types/meta/slot-parity.test.d.ts +1 -0
  71. package/dist/types/shared/meta.types.d.ts +21 -1
  72. package/package.json +1 -1
@@ -13,11 +13,13 @@ export interface ManifestComponent extends Omit<ComponentMeta, 'props' | 'slots'
13
13
  props?: ManifestProp[];
14
14
  slots?: ManifestSlot[];
15
15
  events?: ManifestEvent[];
16
+ /** Derived from the children's `subComponentOf`, never hand-authored. */
17
+ subComponents?: string[];
16
18
  }
17
19
  export interface ComponentManifest {
18
20
  schemaVersion: number;
19
21
  generated: string;
20
22
  components: ManifestComponent[];
21
23
  }
22
- export declare function serializeComponent(component: ComponentMeta): ManifestComponent;
24
+ export declare function serializeComponent(component: ComponentMeta, subComponents?: string[]): ManifestComponent;
23
25
  export declare function buildManifest(components: ComponentMeta[], generated?: string): ComponentManifest;
@@ -3,7 +3,7 @@ function toNamedArray(record) {
3
3
  const entries = Object.entries(record ?? {});
4
4
  return entries.length ? entries.map(([name, value]) => ({ name, ...value })) : undefined;
5
5
  }
6
- export function serializeComponent(component) {
6
+ export function serializeComponent(component, subComponents) {
7
7
  const props = toNamedArray(component.props);
8
8
  const events = toNamedArray(component.events);
9
9
  const slots = toNamedArray(component.slots);
@@ -12,19 +12,36 @@ export function serializeComponent(component) {
12
12
  description: component.description,
13
13
  category: component.category,
14
14
  ...(component.ascii ? { ascii: component.ascii } : {}),
15
+ ...(component.experimental ? { experimental: true } : {}),
15
16
  ...(component.deprecated ? { deprecated: component.deprecated } : {}),
16
17
  ...(props ? { props } : {}),
17
18
  ...(events ? { events } : {}),
18
19
  ...(slots ? { slots } : {}),
19
20
  ...(component.examples?.length ? { examples: component.examples } : {}),
20
21
  ...(component.relatedTo?.length ? { relatedTo: component.relatedTo } : {}),
21
- ...(component.subComponents?.length ? { subComponents: component.subComponents } : {}),
22
+ ...(component.subComponentOf ? { subComponentOf: component.subComponentOf } : {}),
23
+ ...(subComponents?.length ? { subComponents } : {}),
22
24
  };
23
25
  }
26
+ /** Inverts the children's `subComponentOf` into each parent's anatomy list. */
27
+ function collectSubComponents(components) {
28
+ const byParent = new Map();
29
+ for (const component of [...components].sort((a, b) => a.tag.localeCompare(b.tag))) {
30
+ if (!component.subComponentOf)
31
+ continue;
32
+ const children = byParent.get(component.subComponentOf) ?? [];
33
+ children.push(component.tag);
34
+ byParent.set(component.subComponentOf, children);
35
+ }
36
+ return byParent;
37
+ }
24
38
  export function buildManifest(components, generated = new Date().toISOString()) {
39
+ const subComponents = collectSubComponents(components);
25
40
  return {
26
41
  schemaVersion: MANIFEST_SCHEMA_VERSION,
27
42
  generated,
28
- components: [...components].sort((a, b) => a.tag.localeCompare(b.tag)).map(serializeComponent),
43
+ components: [...components]
44
+ .sort((a, b) => a.tag.localeCompare(b.tag))
45
+ .map((component) => serializeComponent(component, subComponents.get(component.tag))),
29
46
  };
30
47
  }
@@ -36,6 +36,16 @@ describe('serializeComponent', () => {
36
36
  { name: 'click', description: 'Native click', forwarded: true },
37
37
  ]);
38
38
  });
39
+ it('keeps experimental, and omits it when unset or false', () => {
40
+ expect(serializeComponent(makeComponent({ experimental: true })).experimental).toBe(true);
41
+ expect(serializeComponent(makeComponent({ experimental: false })).experimental).toBeUndefined();
42
+ expect(serializeComponent(makeComponent()).experimental).toBeUndefined();
43
+ });
44
+ it('carries experimental and deprecated together', () => {
45
+ const component = serializeComponent(makeComponent({ experimental: true, deprecated: { use: 'quantic-bar' } }));
46
+ expect(component.experimental).toBe(true);
47
+ expect(component.deprecated).toEqual({ use: 'quantic-bar' });
48
+ });
39
49
  it('keeps ascii', () => {
40
50
  expect(serializeComponent(makeComponent({ ascii: '[ foo ]' })).ascii).toBe('[ foo ]');
41
51
  });
@@ -67,6 +77,26 @@ describe('buildManifest', () => {
67
77
  ]);
68
78
  expect(components.map((component) => component.tag)).toEqual(['quantic-a', 'quantic-z']);
69
79
  });
80
+ it('derives each parent list from the children that name it', () => {
81
+ const { components } = buildManifest([
82
+ makeComponent({ tag: 'quantic-card' }),
83
+ makeComponent({ tag: 'quantic-card-title', subComponentOf: 'quantic-card' }),
84
+ makeComponent({ tag: 'quantic-card-image', subComponentOf: 'quantic-card' }),
85
+ ]);
86
+ const byTag = new Map(components.map((component) => [component.tag, component]));
87
+ expect(byTag.get('quantic-card')?.subComponents).toEqual([
88
+ 'quantic-card-image',
89
+ 'quantic-card-title',
90
+ ]);
91
+ expect(byTag.get('quantic-card-title')?.subComponentOf).toBe('quantic-card');
92
+ });
93
+ it('leaves a parent with no anatomy without a list', () => {
94
+ const { components } = buildManifest([
95
+ makeComponent({ tag: 'quantic-action-group', relatedTo: ['quantic-button'] }),
96
+ makeComponent({ tag: 'quantic-button' }),
97
+ ]);
98
+ expect(components.every((component) => component.subComponents === undefined)).toBe(true);
99
+ });
70
100
  it('round-trips through JSON', () => {
71
101
  const manifest = buildManifest([makeComponent({ slots: { '': { description: 'Content' } } })]);
72
102
  expect(JSON.parse(JSON.stringify(manifest))).toEqual(manifest);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,83 @@
1
+ import { globSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'vitest';
5
+ const SRC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
6
+ const TAG = /@quanticElement\(\s*['"`]([^'"`]+)['"`]\s*\)/;
7
+ const SLOT_TAG = /<slot\b([^>]*)>/g;
8
+ const SLOT_NAME = /name=(?:"([^"]*)"|([^\s>]+))/;
9
+ const SLOT_KEY = /(?:'([^']*)'|([A-Za-z_$][\w$]*))\s*:\s*\{/g;
10
+ // Read from source rather than importing allMeta: that would pull in every component
11
+ // module, and several import generated icon files that only exist after a build.
12
+ function componentSources() {
13
+ return globSync('**/*.ts', { cwd: SRC_DIR })
14
+ .filter((file) => !/\.(stories|test|d)\.ts$/.test(file))
15
+ .map((file) => ({ file, source: readFileSync(join(SRC_DIR, file), 'utf-8') }));
16
+ }
17
+ /** Top-level keys of the `slots: { ... }` object, by brace matching from its opening. */
18
+ function declaredSlots(source) {
19
+ const start = source.indexOf('slots: {');
20
+ if (start === -1)
21
+ return new Set();
22
+ const open = source.indexOf('{', start);
23
+ let depth = 0;
24
+ let end = open;
25
+ for (; end < source.length; end++) {
26
+ if (source[end] === '{')
27
+ depth++;
28
+ else if (source[end] === '}' && --depth === 0)
29
+ break;
30
+ }
31
+ const body = source.slice(open + 1, end);
32
+ const names = new Set();
33
+ let nesting = 0;
34
+ for (const match of body.matchAll(SLOT_KEY)) {
35
+ // Only keys sitting directly in the slots object name a slot; `description` and
36
+ // friends live one level down.
37
+ nesting = [...body.slice(0, match.index)].reduce((depth, character) => depth + (character === '{' ? 1 : character === '}' ? -1 : 0), 0);
38
+ if (nesting === 0)
39
+ names.add(match[1] ?? match[2]);
40
+ }
41
+ return names;
42
+ }
43
+ /** Rendered slot names per tag, skipping names built at runtime. */
44
+ function renderedSlots() {
45
+ return componentSources().flatMap(({ file, source }) => {
46
+ const tag = source.match(TAG)?.[1];
47
+ // render-as-field.hoc.ts registers `quantic-${name}-field` for five field wrappers,
48
+ // so there is no static tag here to attribute slots to.
49
+ if (!tag || tag.includes('${'))
50
+ return [];
51
+ const names = [...source.matchAll(SLOT_TAG)]
52
+ .map(([, attributes]) => {
53
+ const name = attributes.match(SLOT_NAME);
54
+ // No name attribute at all is the default slot. An unquoted one is built at
55
+ // runtime, and has to stay distinguishable from the default so the filter below
56
+ // can drop it rather than miscount it.
57
+ return name ? (name[1] ?? name[2]) : '';
58
+ })
59
+ // `quantic-tabs` renders `name=${buttonsSlot}` and `splitter-group` renders
60
+ // `pane-${i}`, neither of which a manifest can enumerate.
61
+ .filter((name) => !name.includes('${'));
62
+ return names.length
63
+ ? [{ tag, file, names: [...new Set(names)], declared: declaredSlots(source) }]
64
+ : [];
65
+ });
66
+ }
67
+ describe('slot parity', () => {
68
+ // The manifest is the only thing anything reads. A slot that render() serves but meta
69
+ // does not declare is invisible to generated docs, to meta.json, and to the
70
+ // valid-slot-name lint rule, which skips a component that declares no slots.
71
+ it('declares every slot that render serves', () => {
72
+ const undeclared = renderedSlots().flatMap(({ tag, file, names, declared }) => {
73
+ const missing = names.filter((name) => !declared.has(name));
74
+ return missing.length
75
+ ? [`${tag} (${file}) is missing: ${missing.map((n) => n || '(default)').join(', ')}`]
76
+ : [];
77
+ });
78
+ expect(undeclared).toEqual([]);
79
+ });
80
+ it('found slots to check', () => {
81
+ expect(renderedSlots().length).toBeGreaterThan(40);
82
+ });
83
+ });