@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.
- package/dist/action-group/action-group.component.js +7 -2
- package/dist/badge/badge-group.component.js +1 -2
- package/dist/badge/badge.component.js +8 -3
- package/dist/button/index.js +3 -2
- package/dist/button-bar/button-bar-item.js +1 -0
- package/dist/button-bar/button-bar.js +0 -1
- package/dist/button-group/button-group-item.js +3 -0
- package/dist/button-group/button-group.js +0 -1
- package/dist/card/card-image.component.js +1 -0
- package/dist/card/card-meta.component.js +1 -0
- package/dist/card/card-title.component.js +1 -0
- package/dist/card/card.component.js +14 -21
- package/dist/checkbox/checkbox-group.component.js +0 -1
- package/dist/dialog/dialog-actions.js +1 -1
- package/dist/dialog/dialog-title.js +1 -0
- package/dist/dialog/dialog.js +0 -1
- package/dist/dropdown-menu/dropdown-menu.component.js +0 -4
- package/dist/dropdown-menu/group.component.js +1 -0
- package/dist/dropdown-menu/item.component.js +1 -0
- package/dist/field/field.stories.d.ts +1 -0
- package/dist/field/field.stories.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/layout.component.js +1 -1
- package/dist/layout/sidebar-menu-item-group.component.js +1 -0
- package/dist/layout/sidebar-menu-item.component.js +2 -0
- package/dist/layout/sidebar.component.js +1 -4
- package/dist/meta/all-meta.test.js +18 -0
- package/dist/meta/index.js.map +1 -1
- package/dist/meta/serialize.d.ts +3 -1
- package/dist/meta/serialize.js +20 -3
- package/dist/meta/serialize.test.js +30 -0
- package/dist/meta/slot-parity.test.d.ts +1 -0
- package/dist/meta/slot-parity.test.js +83 -0
- package/dist/meta.json +155 -58
- package/dist/number-input/index.js +1 -0
- package/dist/option-card/option-card-group.component.js +0 -1
- package/dist/option-card/option-card.component.js +1 -0
- package/dist/panel/panel-group.component.js +0 -1
- package/dist/panel/panel-header.component.js +1 -0
- package/dist/panel/panel.component.js +0 -1
- package/dist/popover/popover-popup.js +1 -0
- package/dist/popover/popover.js +0 -1
- package/dist/quantic-components.js +84 -70
- package/dist/quantic-components.js.map +1 -1
- package/dist/quantic-components.min.js +61 -79
- package/dist/quantic-components.min.js.map +1 -1
- package/dist/radio/radio.component.js +1 -0
- package/dist/register.js.map +1 -1
- package/dist/select/index.js +7 -0
- package/dist/shared/meta.types.d.ts +21 -1
- package/dist/splitter/splitter-group.component.js +0 -1
- package/dist/splitter/splitter-pane.component.js +1 -0
- package/dist/table/table-body.component.js +1 -0
- package/dist/table/table-cell.component.js +1 -0
- package/dist/table/table-controls.component.js +1 -0
- package/dist/table/table-footer.component.js +1 -0
- package/dist/table/table-head.component.js +1 -0
- package/dist/table/table-header.component.js +1 -0
- package/dist/table/table-row.component.js +1 -0
- package/dist/table/table.component.js +0 -9
- package/dist/tabs/button.component.js +1 -0
- package/dist/tabs/panel.component.js +1 -0
- package/dist/tabs/tabs.component.js +0 -1
- package/dist/tag/tag-group.component.js +6 -2
- package/dist/text-input/index.js +6 -8
- package/dist/tooltip/tooltip-popup.js +1 -0
- package/dist/tooltip/tooltip.js +0 -1
- package/dist/types/field/field.stories.d.ts +1 -0
- package/dist/types/meta/serialize.d.ts +3 -1
- package/dist/types/meta/slot-parity.test.d.ts +1 -0
- package/dist/types/shared/meta.types.d.ts +21 -1
- package/package.json +1 -1
package/dist/meta/serialize.d.ts
CHANGED
|
@@ -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;
|
package/dist/meta/serialize.js
CHANGED
|
@@ -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.
|
|
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]
|
|
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
|
+
});
|