@design.estate/wcctools 5.1.0 → 6.1.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/.smartconfig.json +7 -3
- package/changelog.md +28 -0
- package/cli.js +3 -0
- package/dist_shell/bundle.js +2970 -0
- package/dist_shell/bundle.js.map +1 -0
- package/dist_shell/bundle.js.third-party-notices.json +1722 -0
- package/dist_shell/bundle.js.third-party-notices.txt +4195 -0
- package/dist_shell/index.html +21 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.devapi.d.ts +11 -0
- package/dist_ts/classes.devapi.js +21 -0
- package/dist_ts/classes.devserver.d.ts +49 -0
- package/dist_ts/classes.devserver.js +142 -0
- package/dist_ts/cli.d.ts +6 -0
- package/dist_ts/cli.js +51 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -0
- package/dist_ts/paths.d.ts +4 -0
- package/dist_ts/paths.js +6 -0
- package/dist_ts/plugins.d.ts +20 -0
- package/dist_ts/plugins.js +27 -0
- package/dist_ts/shellcontent.d.ts +9 -0
- package/dist_ts/shellcontent.js +42 -0
- package/dist_ts/standards/baselines.d.ts +27 -0
- package/dist_ts/standards/baselines.js +83 -0
- package/dist_ts/standards/checker.d.ts +21 -0
- package/dist_ts/standards/checker.js +88 -0
- package/dist_ts/standards/classes.gitrepo.d.ts +13 -0
- package/dist_ts/standards/classes.gitrepo.js +43 -0
- package/dist_ts/standards/classes.sourceedits.d.ts +15 -0
- package/dist_ts/standards/classes.sourceedits.js +54 -0
- package/dist_ts/standards/classes.standardfixer.d.ts +40 -0
- package/dist_ts/standards/classes.standardfixer.js +210 -0
- package/dist_ts/standards/classes.standardproject.d.ts +80 -0
- package/dist_ts/standards/classes.standardproject.js +195 -0
- package/dist_ts/standards/classes.standardtree.d.ts +56 -0
- package/dist_ts/standards/classes.standardtree.js +207 -0
- package/dist_ts/standards/cli.d.ts +7 -0
- package/dist_ts/standards/cli.js +89 -0
- package/dist_ts/standards/config.d.ts +33 -0
- package/dist_ts/standards/config.js +139 -0
- package/dist_ts/standards/errors.d.ts +9 -0
- package/dist_ts/standards/errors.js +17 -0
- package/dist_ts/standards/helpers.ast.d.ts +56 -0
- package/dist_ts/standards/helpers.ast.js +172 -0
- package/dist_ts/standards/helpers.paths.d.ts +36 -0
- package/dist_ts/standards/helpers.paths.js +86 -0
- package/dist_ts/standards/index.d.ts +14 -0
- package/dist_ts/standards/index.js +15 -0
- package/dist_ts/standards/output.d.ts +7 -0
- package/dist_ts/standards/output.js +57 -0
- package/dist_ts/standards/ratchet.d.ts +15 -0
- package/dist_ts/standards/ratchet.js +45 -0
- package/dist_ts/standards/rule.d.ts +50 -0
- package/dist_ts/standards/rule.js +9 -0
- package/dist_ts/standards/rules/barrel.element-index.d.ts +7 -0
- package/dist_ts/standards/rules/barrel.element-index.js +108 -0
- package/dist_ts/standards/rules/barrel.no-demo-export.d.ts +3 -0
- package/dist_ts/standards/rules/barrel.no-demo-export.js +29 -0
- package/dist_ts/standards/rules/demo.fixture-not-exported.d.ts +3 -0
- package/dist_ts/standards/rules/demo.fixture-not-exported.js +29 -0
- package/dist_ts/standards/rules/demo.groups.d.ts +6 -0
- package/dist_ts/standards/rules/demo.groups.js +72 -0
- package/dist_ts/standards/rules/demo.required.d.ts +3 -0
- package/dist_ts/standards/rules/demo.required.js +48 -0
- package/dist_ts/standards/rules/demo.separate-file.d.ts +3 -0
- package/dist_ts/standards/rules/demo.separate-file.js +38 -0
- package/dist_ts/standards/rules/demo.shape.d.ts +3 -0
- package/dist_ts/standards/rules/demo.shape.js +38 -0
- package/dist_ts/standards/rules/docs.component-index.d.ts +8 -0
- package/dist_ts/standards/rules/docs.component-index.js +84 -0
- package/dist_ts/standards/rules/element.exported.d.ts +3 -0
- package/dist_ts/standards/rules/element.exported.js +33 -0
- package/dist_ts/standards/rules/element.literal-tag.d.ts +3 -0
- package/dist_ts/standards/rules/element.literal-tag.js +30 -0
- package/dist_ts/standards/rules/element.reachable.d.ts +3 -0
- package/dist_ts/standards/rules/element.reachable.js +48 -0
- package/dist_ts/standards/rules/element.tag-map.d.ts +3 -0
- package/dist_ts/standards/rules/element.tag-map.js +83 -0
- package/dist_ts/standards/rules/i18n.literals.d.ts +3 -0
- package/dist_ts/standards/rules/i18n.literals.js +52 -0
- package/dist_ts/standards/rules/index.d.ts +12 -0
- package/dist_ts/standards/rules/index.js +83 -0
- package/dist_ts/standards/rules/layout.element-folder.d.ts +3 -0
- package/dist_ts/standards/rules/layout.element-folder.js +47 -0
- package/dist_ts/standards/rules/layout.entry-file-name.d.ts +3 -0
- package/dist_ts/standards/rules/layout.entry-file-name.js +71 -0
- package/dist_ts/standards/rules/layout.entry.d.ts +3 -0
- package/dist_ts/standards/rules/layout.entry.js +38 -0
- package/dist_ts/standards/rules/layout.group-folder.d.ts +3 -0
- package/dist_ts/standards/rules/layout.group-folder.js +22 -0
- package/dist_ts/standards/rules/layout.shared-00.d.ts +3 -0
- package/dist_ts/standards/rules/layout.shared-00.js +36 -0
- package/dist_ts/standards/rules/naming.class-matches-tag.d.ts +3 -0
- package/dist_ts/standards/rules/naming.class-matches-tag.js +26 -0
- package/dist_ts/standards/rules/naming.lowercase-files.d.ts +3 -0
- package/dist_ts/standards/rules/naming.lowercase-files.js +26 -0
- package/dist_ts/standards/rules/naming.tag-prefix.d.ts +3 -0
- package/dist_ts/standards/rules/naming.tag-prefix.js +23 -0
- package/dist_ts/standards/rules/props.accessor.d.ts +3 -0
- package/dist_ts/standards/rules/props.accessor.js +35 -0
- package/dist_ts/standards/rules/props.object-no-attribute.d.ts +3 -0
- package/dist_ts/standards/rules/props.object-no-attribute.js +42 -0
- package/dist_ts/standards/rules/props.reflect-primitive.d.ts +5 -0
- package/dist_ts/standards/rules/props.reflect-primitive.js +41 -0
- package/dist_ts/standards/rules/props.typed.d.ts +6 -0
- package/dist_ts/standards/rules/props.typed.js +64 -0
- package/dist_ts/standards/rules/shared.d.ts +38 -0
- package/dist_ts/standards/rules/shared.js +103 -0
- package/dist_ts/standards/rules/style.bdtheme.d.ts +5 -0
- package/dist_ts/standards/rules/style.bdtheme.js +28 -0
- package/dist_ts_interfaces/bridge.d.ts +88 -0
- package/dist_ts_interfaces/bridge.js +2 -0
- package/dist_ts_interfaces/catalog.d.ts +32 -0
- package/dist_ts_interfaces/catalog.js +6 -0
- package/dist_ts_interfaces/index.d.ts +4 -0
- package/dist_ts_interfaces/index.js +5 -0
- package/dist_ts_interfaces/plugins.d.ts +2 -0
- package/dist_ts_interfaces/plugins.js +3 -0
- package/dist_ts_interfaces/requests.d.ts +15 -0
- package/dist_ts_interfaces/requests.js +2 -0
- package/dist_ts_interfaces/standard.d.ts +93 -0
- package/dist_ts_interfaces/standard.js +6 -0
- package/dist_ts_shared/index.d.ts +1 -0
- package/dist_ts_shared/index.js +2 -0
- package/dist_ts_shared/plugins.d.ts +2 -0
- package/dist_ts_shared/plugins.js +3 -0
- package/dist_ts_shared/previewroute.d.ts +18 -0
- package/dist_ts_shared/previewroute.js +47 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-contextmenu.js +1 -1
- package/dist_ts_shell/elements/wcc-preview-frame.d.ts +23 -0
- package/dist_ts_shell/elements/wcc-preview-frame.js +197 -0
- package/dist_ts_shell/elements/wcc-properties.d.ts +49 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-properties.js +149 -216
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-record-button.js +1 -1
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-recording-panel.d.ts +2 -2
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-recording-panel.js +12 -16
- package/dist_ts_shell/elements/wcc-shell.d.ts +87 -0
- package/dist_ts_shell/elements/wcc-shell.js +524 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-sidebar.d.ts +12 -10
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-sidebar.js +137 -183
- package/dist_ts_shell/index.d.ts +4 -0
- package/dist_ts_shell/index.js +5 -0
- package/dist_ts_shell/plugins.d.ts +4 -0
- package/dist_ts_shell/plugins.js +5 -0
- package/dist_ts_shell/previewconnection.d.ts +30 -0
- package/dist_ts_shell/previewconnection.js +86 -0
- package/dist_ts_shell/services/recorder.service.js +353 -0
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/dist_ts_web/catalog.d.ts +45 -0
- package/dist_ts_web/catalog.js +153 -0
- package/dist_ts_web/elementproperties.d.ts +15 -0
- package/dist_ts_web/elementproperties.js +89 -0
- package/dist_ts_web/elements/wcc-preview.d.ts +19 -0
- package/dist_ts_web/elements/wcc-preview.js +164 -0
- package/dist_ts_web/elements/wcc-standalone-notice.d.ts +14 -0
- package/dist_ts_web/elements/wcc-standalone-notice.js +110 -0
- package/dist_ts_web/elements/wcctools.helpers.d.ts +2 -6
- package/dist_ts_web/elements/wcctools.helpers.js +1 -7
- package/dist_ts_web/index.d.ts +2 -5
- package/dist_ts_web/index.js +38 -17
- package/dist_ts_web/previewbridge.d.ts +39 -0
- package/dist_ts_web/previewbridge.js +153 -0
- package/dist_ts_web/renderedtree.d.ts +5 -0
- package/dist_ts_web/renderedtree.js +37 -0
- package/dist_ts_web/wcctools.interfaces.d.ts +3 -6
- package/dist_ts_web/wcctools.interfaces.js +1 -1
- package/dist_ts_web/wcctools.plugins.d.ts +3 -1
- package/dist_ts_web/wcctools.plugins.js +4 -2
- package/package.json +17 -7
- package/readme.md +93 -28
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.devapi.ts +25 -0
- package/ts/classes.devserver.ts +161 -0
- package/ts/cli.ts +53 -0
- package/ts/index.ts +3 -0
- package/ts/paths.ts +7 -0
- package/ts/plugins.ts +37 -0
- package/ts/shellcontent.ts +42 -0
- package/ts/standards/baselines.ts +100 -0
- package/ts/standards/checker.ts +114 -0
- package/ts/standards/classes.gitrepo.ts +48 -0
- package/ts/standards/classes.sourceedits.ts +58 -0
- package/ts/standards/classes.standardfixer.ts +234 -0
- package/ts/standards/classes.standardproject.ts +250 -0
- package/ts/standards/classes.standardtree.ts +220 -0
- package/ts/standards/cli.ts +86 -0
- package/ts/standards/config.ts +160 -0
- package/ts/standards/errors.ts +18 -0
- package/ts/standards/helpers.ast.ts +208 -0
- package/ts/standards/helpers.paths.ts +119 -0
- package/ts/standards/index.ts +22 -0
- package/ts/standards/output.ts +65 -0
- package/ts/standards/ratchet.ts +63 -0
- package/ts/standards/rule.ts +58 -0
- package/ts/standards/rules/barrel.element-index.ts +112 -0
- package/ts/standards/rules/barrel.no-demo-export.ts +30 -0
- package/ts/standards/rules/demo.fixture-not-exported.ts +30 -0
- package/ts/standards/rules/demo.groups.ts +74 -0
- package/ts/standards/rules/demo.required.ts +49 -0
- package/ts/standards/rules/demo.separate-file.ts +40 -0
- package/ts/standards/rules/demo.shape.ts +42 -0
- package/ts/standards/rules/docs.component-index.ts +88 -0
- package/ts/standards/rules/element.exported.ts +35 -0
- package/ts/standards/rules/element.literal-tag.ts +30 -0
- package/ts/standards/rules/element.reachable.ts +49 -0
- package/ts/standards/rules/element.tag-map.ts +87 -0
- package/ts/standards/rules/i18n.literals.ts +58 -0
- package/ts/standards/rules/index.ts +87 -0
- package/ts/standards/rules/layout.element-folder.ts +49 -0
- package/ts/standards/rules/layout.entry-file-name.ts +73 -0
- package/ts/standards/rules/layout.entry.ts +40 -0
- package/ts/standards/rules/layout.group-folder.ts +23 -0
- package/ts/standards/rules/layout.shared-00.ts +37 -0
- package/ts/standards/rules/naming.class-matches-tag.ts +27 -0
- package/ts/standards/rules/naming.lowercase-files.ts +27 -0
- package/ts/standards/rules/naming.tag-prefix.ts +24 -0
- package/ts/standards/rules/props.accessor.ts +37 -0
- package/ts/standards/rules/props.object-no-attribute.ts +45 -0
- package/ts/standards/rules/props.reflect-primitive.ts +44 -0
- package/ts/standards/rules/props.typed.ts +70 -0
- package/ts/standards/rules/shared.ts +127 -0
- package/ts/standards/rules/style.bdtheme.ts +31 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/catalog.ts +194 -0
- package/ts_web/elementproperties.ts +96 -0
- package/ts_web/elements/wcc-preview.ts +111 -0
- package/ts_web/elements/wcc-standalone-notice.ts +65 -0
- package/ts_web/elements/wcctools.helpers.ts +2 -10
- package/ts_web/index.ts +38 -19
- package/ts_web/previewbridge.ts +189 -0
- package/ts_web/readme.md +26 -85
- package/ts_web/renderedtree.ts +43 -0
- package/ts_web/wcctools.interfaces.ts +4 -7
- package/ts_web/wcctools.plugins.ts +6 -1
- package/dist_bundle/bundle.js +0 -92419
- package/dist_bundle/bundle.js.map +0 -1
- package/dist_ts_web/elements/wcc-dashboard.d.ts +0 -119
- package/dist_ts_web/elements/wcc-dashboard.js +0 -700
- package/dist_ts_web/elements/wcc-frame.d.ts +0 -22
- package/dist_ts_web/elements/wcc-frame.js +0 -256
- package/dist_ts_web/elements/wcc-properties.d.ts +0 -43
- package/dist_ts_web/pages/index.d.ts +0 -1
- package/dist_ts_web/pages/index.js +0 -2
- package/dist_ts_web/services/recorder.service.js +0 -353
- package/ts_web/elements/wcc-contextmenu.ts +0 -291
- package/ts_web/elements/wcc-dashboard.ts +0 -672
- package/ts_web/elements/wcc-frame.ts +0 -189
- package/ts_web/elements/wcc-properties.ts +0 -1089
- package/ts_web/elements/wcc-record-button.ts +0 -108
- package/ts_web/elements/wcc-recording-panel.ts +0 -1010
- package/ts_web/elements/wcc-sidebar.ts +0 -1470
- package/ts_web/pages/index.ts +0 -1
- package/ts_web/services/recorder.service.ts +0 -451
- package/ts_web/types/dom-mediacapture-stub/index.d.ts +0 -12
- package/ts_web/types/dom-mediacapture-stub/package.json +0 -6
- package/ts_web/types/dom-webcodecs-stub/index.d.ts +0 -2
- package/ts_web/types/dom-webcodecs-stub/package.json +0 -6
- /package/{dist_ts_web → dist_ts_shell}/elements/wcc-contextmenu.d.ts +0 -0
- /package/{dist_ts_web → dist_ts_shell}/elements/wcc-record-button.d.ts +0 -0
- /package/{dist_ts_web → dist_ts_shell}/services/recorder.service.d.ts +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as ast from '../helpers.ast.js';
|
|
2
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
3
|
+
import { addOptionFix } from './props.typed.js';
|
|
4
|
+
import { propertyKey, publicProperties } from './shared.js';
|
|
5
|
+
|
|
6
|
+
/** Global attributes with native behaviour that stay unreflected without an allowlist entry. */
|
|
7
|
+
export const reflectExemptNames: readonly string[] = ['title', 'hidden', 'dir', 'lang', 'role', 'inert', 'draggable'];
|
|
8
|
+
|
|
9
|
+
const primitiveTypes = ['String', 'Number', 'Boolean'];
|
|
10
|
+
|
|
11
|
+
/** A primitive public property declares `reflect: true` or `attribute: false`. */
|
|
12
|
+
export const propsReflectPrimitiveRule: IStandardRule = {
|
|
13
|
+
id: 'props/reflect-primitive',
|
|
14
|
+
severity: { base: 'error' },
|
|
15
|
+
fixMode: 'opt-in',
|
|
16
|
+
allowlistCategories: ['user-data', 'object', 'large-text', 'global-attribute'],
|
|
17
|
+
detect: ({ project }) => {
|
|
18
|
+
const findings: IRuleFinding[] = [];
|
|
19
|
+
for (const property of publicProperties(project)) {
|
|
20
|
+
if (property.dynamicOptions || reflectExemptNames.includes(property.name)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const options = property.options;
|
|
24
|
+
const type = options ? ast.objectPropertyText(options, 'type') : null;
|
|
25
|
+
if (type !== null && !primitiveTypes.includes(type)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (options && (ast.objectPropertyText(options, 'attribute') === 'false' || ast.objectPropertyText(options, 'reflect') === 'true')) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const explicitReflect = options ? ast.objectProperty(options, 'reflect') : null;
|
|
32
|
+
findings.push({
|
|
33
|
+
file: property.file,
|
|
34
|
+
position: property.call.getStart(),
|
|
35
|
+
key: propertyKey(property),
|
|
36
|
+
message: `public primitive @property ${property.name} declares neither reflect: true nor attribute: false`,
|
|
37
|
+
fix: explicitReflect
|
|
38
|
+
? (builderArg) => builderArg.edit(property.file, explicitReflect.initializer.getStart(), explicitReflect.initializer.end, 'true')
|
|
39
|
+
: addOptionFix(property, 'reflect: true', 'last'),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return findings;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import * as ast from '../helpers.ast.js';
|
|
3
|
+
import type { IStandardProperty } from '../classes.standardproject.js';
|
|
4
|
+
import type { IRuleFinding, IStandardRule, TFixAction } from '../rule.js';
|
|
5
|
+
import { coreTypeOf, isArrayTypeNode, propertyKey, publicProperties } from './shared.js';
|
|
6
|
+
|
|
7
|
+
const ts = plugins.typescript;
|
|
8
|
+
|
|
9
|
+
/** The Lit `type` that the property's annotation maps to, or null when the annotation does not decide it. */
|
|
10
|
+
const litTypeFor = (propertyArg: IStandardProperty): string | null => {
|
|
11
|
+
const typeNode = coreTypeOf(propertyArg);
|
|
12
|
+
if (!typeNode) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
switch (typeNode.kind) {
|
|
16
|
+
case ts.SyntaxKind.StringKeyword:
|
|
17
|
+
return 'String';
|
|
18
|
+
case ts.SyntaxKind.NumberKeyword:
|
|
19
|
+
return 'Number';
|
|
20
|
+
case ts.SyntaxKind.BooleanKeyword:
|
|
21
|
+
return 'Boolean';
|
|
22
|
+
}
|
|
23
|
+
if (ts.isUnionTypeNode(typeNode) && typeNode.types.every((typeArg) =>
|
|
24
|
+
(ts.isLiteralTypeNode(typeArg) && ts.isStringLiteral(typeArg.literal))
|
|
25
|
+
|| typeArg.kind === ts.SyntaxKind.UndefinedKeyword
|
|
26
|
+
|| (ts.isLiteralTypeNode(typeArg) && typeArg.literal.kind === ts.SyntaxKind.NullKeyword))) {
|
|
27
|
+
return 'String';
|
|
28
|
+
}
|
|
29
|
+
if (ts.isLiteralTypeNode(typeNode) && ts.isStringLiteral(typeNode.literal)) {
|
|
30
|
+
return 'String';
|
|
31
|
+
}
|
|
32
|
+
return isArrayTypeNode(typeNode) ? 'Array' : null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The edit that adds `propertyTextArg` to a property decorator's options, creating the options literal when absent. */
|
|
36
|
+
export const addOptionFix = (propertyArg: IStandardProperty, propertyTextArg: string, placementArg: 'first' | 'last'): TFixAction => (builderArg) => {
|
|
37
|
+
if (propertyArg.options) {
|
|
38
|
+
const edit = placementArg === 'first'
|
|
39
|
+
? ast.prependObjectPropertyEdit(propertyArg.options, propertyTextArg)
|
|
40
|
+
: ast.appendObjectPropertyEdit(propertyArg.options, propertyTextArg);
|
|
41
|
+
builderArg.edit(propertyArg.file, edit.start, edit.end, edit.text);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const at = propertyArg.call.arguments.pos;
|
|
45
|
+
builderArg.edit(propertyArg.file, at, at, `{ ${propertyTextArg} }`);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** A public property declares `type`. */
|
|
49
|
+
export const propsTypedRule: IStandardRule = {
|
|
50
|
+
id: 'props/typed',
|
|
51
|
+
severity: { base: 'warn' },
|
|
52
|
+
fixMode: 'auto',
|
|
53
|
+
detect: ({ project }) => {
|
|
54
|
+
const findings: IRuleFinding[] = [];
|
|
55
|
+
for (const property of publicProperties(project)) {
|
|
56
|
+
if (property.dynamicOptions || (property.options && ast.objectProperty(property.options, 'type'))) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const litType = litTypeFor(property);
|
|
60
|
+
findings.push({
|
|
61
|
+
file: property.file,
|
|
62
|
+
position: property.call.getStart(),
|
|
63
|
+
key: propertyKey(property),
|
|
64
|
+
message: `@property ${property.name} declares no type`,
|
|
65
|
+
fix: litType ? addOptionFix(property, `type: ${litType}`, 'first') : undefined,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return findings;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import * as ast from '../helpers.ast.js';
|
|
3
|
+
import { baseOf, dirOf, specifierFor } from '../helpers.paths.js';
|
|
4
|
+
import type { IStandardElement, IStandardProperty, StandardProject } from '../classes.standardproject.js';
|
|
5
|
+
import type { IFixBuilder } from '../rule.js';
|
|
6
|
+
|
|
7
|
+
const ts = plugins.typescript;
|
|
8
|
+
|
|
9
|
+
export const elementsDir = 'ts_web/elements';
|
|
10
|
+
|
|
11
|
+
/** `ts_web/elements/00group-<area>` or `ts_web/elements` itself. */
|
|
12
|
+
export const isElementParentDir = (dirArg: string): boolean =>
|
|
13
|
+
dirArg === elementsDir || /^ts_web\/elements\/00group-[^/]+$/.test(dirArg);
|
|
14
|
+
|
|
15
|
+
/** Names the HTML specification reserves; `customElements.define` rejects them. */
|
|
16
|
+
const reservedElementNames = ['annotation-xml', 'color-profile', 'font-face', 'font-face-src', 'font-face-uri', 'font-face-format', 'font-face-name', 'missing-glyph'];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A valid custom element name in the ASCII subset the standard allows: a lowercase letter first,
|
|
20
|
+
* then lowercase letters, digits, `-`, `.` and `_`, at least one `-`, and not a reserved name. Such
|
|
21
|
+
* a tag is also a safe file and folder name, so only these tags feed fixes that derive paths or code.
|
|
22
|
+
*/
|
|
23
|
+
export const isCustomElementName = (tagArg: string): boolean =>
|
|
24
|
+
/^[a-z][a-z0-9._-]*$/.test(tagArg) && tagArg.includes('-') && !reservedElementNames.includes(tagArg);
|
|
25
|
+
|
|
26
|
+
/** True when an element's entry file sits in its own element folder `…/<tag>/`. */
|
|
27
|
+
export const isInElementFolder = (elementArg: IStandardElement): boolean => {
|
|
28
|
+
const folder = dirOf(elementArg.file);
|
|
29
|
+
return !!elementArg.tag && baseOf(folder) === elementArg.tag && isElementParentDir(dirOf(folder));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The `<area>` of `ts_web/elements/00group-<area>/…`, or null. */
|
|
33
|
+
export const groupAreaOf = (fileArg: string): string | null => /^ts_web\/elements\/00group-([^/]+)\//.exec(fileArg)?.[1] ?? null;
|
|
34
|
+
|
|
35
|
+
/** Elements outside demo files: the catalog's own elements. */
|
|
36
|
+
export const catalogElements = (projectArg: StandardProject): IStandardElement[] =>
|
|
37
|
+
projectArg.elements.filter((elementArg) => !elementArg.inDemoFile);
|
|
38
|
+
|
|
39
|
+
/** Public `@property` members outside demo files. */
|
|
40
|
+
export const publicProperties = (projectArg: StandardProject): IStandardProperty[] =>
|
|
41
|
+
projectArg.properties.filter((propertyArg) => propertyArg.decorator === 'property' && propertyArg.isPublic && !propertyArg.inDemoFile);
|
|
42
|
+
|
|
43
|
+
export const elementKey = (elementArg: IStandardElement): string => `${elementArg.file}#${elementArg.className ?? elementArg.tag ?? '?'}`;
|
|
44
|
+
|
|
45
|
+
export const propertyKey = (propertyArg: IStandardProperty): string => `${propertyArg.file}#${propertyArg.name}`;
|
|
46
|
+
|
|
47
|
+
/** The type annotation of a property member without `undefined` and `null` union members. */
|
|
48
|
+
export const coreTypeOf = (propertyArg: IStandardProperty): ast.TTypeNode | null => {
|
|
49
|
+
const member = propertyArg.member;
|
|
50
|
+
const typeNode = ts.isPropertyDeclaration(member) ? member.type : undefined;
|
|
51
|
+
if (!typeNode) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (ts.isUnionTypeNode(typeNode)) {
|
|
55
|
+
const members = typeNode.types.filter((typeArg) =>
|
|
56
|
+
typeArg.kind !== ts.SyntaxKind.UndefinedKeyword
|
|
57
|
+
&& !(ts.isLiteralTypeNode(typeArg) && typeArg.literal.kind === ts.SyntaxKind.NullKeyword));
|
|
58
|
+
if (members.length === 1) {
|
|
59
|
+
return members[0];
|
|
60
|
+
}
|
|
61
|
+
return typeNode;
|
|
62
|
+
}
|
|
63
|
+
return typeNode;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const isArrayTypeNode = (typeArg: ast.TTypeNode): boolean => {
|
|
67
|
+
if (ts.isArrayTypeNode(typeArg)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (ts.isTypeOperatorNode(typeArg) && typeArg.operator === ts.SyntaxKind.ReadonlyKeyword) {
|
|
71
|
+
return ts.isArrayTypeNode(typeArg.type);
|
|
72
|
+
}
|
|
73
|
+
return ts.isTypeReferenceNode(typeArg) && ts.isIdentifier(typeArg.typeName) && ['Array', 'ReadonlyArray'].includes(typeArg.typeName.text);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** The position at the start of the line after the last `export … from` statement, or at the end of the file. */
|
|
77
|
+
export const exportInsertionPoint = (sourceFileArg: ast.TSourceFile): { position: number; prefix: string } => {
|
|
78
|
+
let lastEnd = -1;
|
|
79
|
+
for (const statement of sourceFileArg.statements) {
|
|
80
|
+
if (ts.isExportDeclaration(statement)) {
|
|
81
|
+
lastEnd = statement.end;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const text = sourceFileArg.text;
|
|
85
|
+
if (lastEnd === -1) {
|
|
86
|
+
return { position: text.length, prefix: text.length > 0 && !text.endsWith('\n') ? '\n' : '' };
|
|
87
|
+
}
|
|
88
|
+
const newline = text.indexOf('\n', lastEnd);
|
|
89
|
+
return newline === -1 ? { position: text.length, prefix: '\n' } : { position: newline + 1, prefix: '' };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Adds `export * from '<target>';` to a barrel. */
|
|
93
|
+
export const addReexport = (builderArg: IFixBuilder, projectArg: StandardProject, barrelArg: string, targetArg: string): void => {
|
|
94
|
+
const point = exportInsertionPoint(projectArg.sourceFile(barrelArg));
|
|
95
|
+
builderArg.edit(barrelArg, point.position, point.position, `${point.prefix}export * from '${specifierFor(barrelArg, targetArg, 'js')}';\n`);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Removes a whole statement including its line break; from the line start when only indentation precedes it. */
|
|
99
|
+
export const removeStatement = (builderArg: IFixBuilder, fileArg: string, statementArg: ast.TNode): void => {
|
|
100
|
+
const text = statementArg.getSourceFile().text;
|
|
101
|
+
let start = statementArg.getStart();
|
|
102
|
+
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
103
|
+
if (/^[ \t]*$/.test(text.slice(lineStart, start))) {
|
|
104
|
+
start = lineStart;
|
|
105
|
+
}
|
|
106
|
+
let end = statementArg.end;
|
|
107
|
+
const rest = /^[ \t]*\r?\n/.exec(text.slice(end));
|
|
108
|
+
if (rest) {
|
|
109
|
+
end += rest[0].length;
|
|
110
|
+
}
|
|
111
|
+
builderArg.edit(fileArg, start, end, '');
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** The identifier a `static demo` value refers to at its root, e.g. `demoFunc` or `demos` in `demos.main`. */
|
|
115
|
+
export const rootIdentifier = (expressionArg: ast.TExpression): string | null => {
|
|
116
|
+
let current: ast.TExpression = expressionArg;
|
|
117
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
118
|
+
current = current.expression;
|
|
119
|
+
}
|
|
120
|
+
return ts.isIdentifier(current) ? current.text : null;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** The start of the line following the line a position is on. */
|
|
124
|
+
export const nextLineStart = (textArg: string, positionArg: number): number => {
|
|
125
|
+
const newline = textArg.indexOf('\n', positionArg);
|
|
126
|
+
return newline === -1 ? textArg.length : newline + 1;
|
|
127
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import * as ast from '../helpers.ast.js';
|
|
3
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
4
|
+
|
|
5
|
+
const ts = plugins.typescript;
|
|
6
|
+
|
|
7
|
+
/** The one module that may call `bdTheme()`: the token bridge. */
|
|
8
|
+
export const bdThemeBridge = 'ts_web/elements/00theme.ts';
|
|
9
|
+
|
|
10
|
+
/** dees: no new `cssManager.bdTheme()` call outside `ts_web/elements/00theme.ts`. A ratchet: baselines only. */
|
|
11
|
+
export const styleBdThemeRule: IStandardRule = {
|
|
12
|
+
id: 'style/bdtheme',
|
|
13
|
+
severity: { dees: 'error' },
|
|
14
|
+
fixMode: 'none',
|
|
15
|
+
detect: ({ project }) => {
|
|
16
|
+
const findings: IRuleFinding[] = [];
|
|
17
|
+
for (const file of project.webFiles) {
|
|
18
|
+
if (file === bdThemeBridge) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const visit = (nodeArg: ast.TNode): void => {
|
|
22
|
+
if (ts.isCallExpression(nodeArg) && ast.calleeName(nodeArg.expression) === 'bdTheme') {
|
|
23
|
+
findings.push({ file, position: nodeArg.getStart(), key: file, message: 'bdTheme() call; use --dees-* tokens' });
|
|
24
|
+
}
|
|
25
|
+
ts.forEachChild(nodeArg, visit);
|
|
26
|
+
};
|
|
27
|
+
visit(project.sourceFile(file));
|
|
28
|
+
}
|
|
29
|
+
return findings;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@design.estate/wcctools',
|
|
6
|
-
version: '
|
|
6
|
+
version: '6.1.0',
|
|
7
7
|
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
|
8
8
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as plugins from './wcctools.plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
classifyCatalogEntry,
|
|
4
|
+
getDemoAtIndex,
|
|
5
|
+
getDemoCount,
|
|
6
|
+
getElementDemoGroups,
|
|
7
|
+
getElementTagName,
|
|
8
|
+
type ICatalogElementConstructor,
|
|
9
|
+
type TDemoDefinition,
|
|
10
|
+
type TTemplateFactory,
|
|
11
|
+
} from './elements/wcctools.helpers.js';
|
|
12
|
+
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
|
13
|
+
|
|
14
|
+
type TSectionEntry = [string, any];
|
|
15
|
+
|
|
16
|
+
const applySectionFilterAndSort = (
|
|
17
|
+
section: IWccSection,
|
|
18
|
+
entries: TSectionEntry[],
|
|
19
|
+
fallbackSort?: (a: TSectionEntry, b: TSectionEntry) => number
|
|
20
|
+
): TSectionEntry[] => {
|
|
21
|
+
let result = entries;
|
|
22
|
+
if (section.filter) {
|
|
23
|
+
const filterFn = section.filter;
|
|
24
|
+
result = result.filter(([name, item]) => filterFn(name, item));
|
|
25
|
+
}
|
|
26
|
+
const sortFn = section.sort ?? fallbackSort;
|
|
27
|
+
if (sortFn) {
|
|
28
|
+
result = [...result].sort(sortFn);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The navigable entries of a section, after classification and the section's own filter and sort.
|
|
35
|
+
* Elements sections keep only custom element classes with a usable static `demo`;
|
|
36
|
+
* pages sections keep only template factories.
|
|
37
|
+
*/
|
|
38
|
+
export const getSectionItems = (section: IWccSection): TSectionEntry[] => {
|
|
39
|
+
const entries = Object.entries(section.items).filter(([, item]) =>
|
|
40
|
+
section.type === 'elements'
|
|
41
|
+
? classifyCatalogEntry(item) === 'demo'
|
|
42
|
+
: typeof item === 'function'
|
|
43
|
+
);
|
|
44
|
+
return applySectionFilterAndSort(section, entries);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Registered custom elements of an elements section that have no usable demo.
|
|
49
|
+
* They are listed (not navigable) in the sidebar's "Without demo" group.
|
|
50
|
+
*/
|
|
51
|
+
export const getSectionItemsWithoutDemo = (section: IWccSection): TSectionEntry[] => {
|
|
52
|
+
if (section.type !== 'elements') {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const entries = Object.entries(section.items).filter(([, item]) => classifyCatalogEntry(item) === 'no-demo');
|
|
56
|
+
return applySectionFilterAndSort(section, entries, ([a], [b]) => a.localeCompare(b));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** What a selection resolves to: something to render, or the reason there is nothing. */
|
|
60
|
+
export type TWccCatalogTarget =
|
|
61
|
+
| { kind: 'element'; factory: TTemplateFactory; elementClass: ICatalogElementConstructor }
|
|
62
|
+
| { kind: 'page'; factory: TTemplateFactory }
|
|
63
|
+
| { kind: 'empty'; reason: plugins.interfaces.TWccEmptyReason; emptyState: plugins.interfaces.IWccEmptyState };
|
|
64
|
+
|
|
65
|
+
type TResolvedEntry =
|
|
66
|
+
| { kind: 'listed'; section: IWccSection; item: unknown }
|
|
67
|
+
| { kind: 'no-demo' | 'ignored' | 'missing'; section: IWccSection | null };
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The catalog of one `setupWccTools` call: classifies its sections, describes them as a
|
|
71
|
+
* data-only manifest and resolves selections to template factories.
|
|
72
|
+
*/
|
|
73
|
+
export class WccCatalog {
|
|
74
|
+
private readonly sections: IWccSection[];
|
|
75
|
+
|
|
76
|
+
constructor(configArg: IWccConfig) {
|
|
77
|
+
this.sections = configArg.sections;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public getManifest(): plugins.interfaces.IWccCatalogManifest {
|
|
81
|
+
return {
|
|
82
|
+
sections: this.sections.map((section) => ({
|
|
83
|
+
name: section.name,
|
|
84
|
+
type: section.type,
|
|
85
|
+
...(section.icon ? { icon: section.icon } : {}),
|
|
86
|
+
...(section.collapsed ? { collapsed: true } : {}),
|
|
87
|
+
entries: getSectionItems(section).map(([name, item]) => ({
|
|
88
|
+
name,
|
|
89
|
+
tagName: getElementTagName(item),
|
|
90
|
+
demoCount: section.type === 'elements' ? getDemoCount(item.demo as TDemoDefinition) : 1,
|
|
91
|
+
demoGroups: getElementDemoGroups(item),
|
|
92
|
+
})),
|
|
93
|
+
entriesWithoutDemo: getSectionItemsWithoutDemo(section).map(([name, item]) => ({
|
|
94
|
+
name,
|
|
95
|
+
tagName: getElementTagName(item),
|
|
96
|
+
})),
|
|
97
|
+
})),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public resolve(selectionArg: plugins.interfaces.IWccSelection | null): TWccCatalogTarget {
|
|
102
|
+
if (!selectionArg) {
|
|
103
|
+
return {
|
|
104
|
+
kind: 'empty',
|
|
105
|
+
reason: 'nothing-selected',
|
|
106
|
+
emptyState: {
|
|
107
|
+
title: 'Nothing selected',
|
|
108
|
+
detail: 'Choose an element or page in the sidebar to preview it here.',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const { sectionName, itemName, demoIndex } = selectionArg;
|
|
113
|
+
const resolved = this.resolveEntry(sectionName, itemName);
|
|
114
|
+
if (resolved.kind !== 'listed') {
|
|
115
|
+
return { kind: 'empty', reason: resolved.kind, emptyState: this.getUnavailableEmptyState(resolved, sectionName, itemName) };
|
|
116
|
+
}
|
|
117
|
+
if (resolved.section.type === 'pages') {
|
|
118
|
+
return { kind: 'page', factory: resolved.item as TTemplateFactory };
|
|
119
|
+
}
|
|
120
|
+
const elementClass = resolved.item as ICatalogElementConstructor;
|
|
121
|
+
const demo = elementClass.demo as TDemoDefinition;
|
|
122
|
+
const factory = getDemoAtIndex(demo, demoIndex);
|
|
123
|
+
if (!factory) {
|
|
124
|
+
const count = getDemoCount(demo);
|
|
125
|
+
return {
|
|
126
|
+
kind: 'empty',
|
|
127
|
+
reason: 'no-such-demo',
|
|
128
|
+
emptyState: {
|
|
129
|
+
title: `${itemName} has no demo ${demoIndex + 1}`,
|
|
130
|
+
detail: `This element defines ${count} ${count === 1 ? 'demo' : 'demos'}.`,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { kind: 'element', factory, elementClass };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a section/item pair to a listed entry, or to the reason it cannot be previewed.
|
|
139
|
+
* The section names `element` and `page` of links from before sections existed search every section.
|
|
140
|
+
*/
|
|
141
|
+
private resolveEntry(sectionNameArg: string, itemNameArg: string): TResolvedEntry {
|
|
142
|
+
const section = this.sections.find((sectionArg) => sectionArg.name === sectionNameArg) ?? null;
|
|
143
|
+
const isLegacyName = !section && (sectionNameArg === 'element' || sectionNameArg === 'page');
|
|
144
|
+
const candidates = section ? [section] : isLegacyName ? this.sections : [];
|
|
145
|
+
|
|
146
|
+
for (const candidate of candidates) {
|
|
147
|
+
const listed = getSectionItems(candidate).find(([name]) => name === itemNameArg);
|
|
148
|
+
if (listed) {
|
|
149
|
+
return { kind: 'listed', section: candidate, item: listed[1] };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const candidate of candidates) {
|
|
153
|
+
if (getSectionItemsWithoutDemo(candidate).some(([name]) => name === itemNameArg)) {
|
|
154
|
+
return { kind: 'no-demo', section: candidate };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const candidate of candidates) {
|
|
158
|
+
if (Object.prototype.hasOwnProperty.call(candidate.items, itemNameArg)) {
|
|
159
|
+
const item = candidate.items[itemNameArg];
|
|
160
|
+
const notDemoable = candidate.type === 'elements'
|
|
161
|
+
? classifyCatalogEntry(item) === 'ignored'
|
|
162
|
+
: typeof item !== 'function';
|
|
163
|
+
return { kind: notDemoable ? 'ignored' : 'missing', section: candidate };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { kind: 'missing', section };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private getUnavailableEmptyState(
|
|
170
|
+
resolvedArg: Exclude<TResolvedEntry, { kind: 'listed' }>,
|
|
171
|
+
sectionNameArg: string,
|
|
172
|
+
itemNameArg: string,
|
|
173
|
+
): plugins.interfaces.IWccEmptyState {
|
|
174
|
+
switch (resolvedArg.kind) {
|
|
175
|
+
case 'no-demo': {
|
|
176
|
+
const tagName = getElementTagName(resolvedArg.section?.items[itemNameArg]);
|
|
177
|
+
return {
|
|
178
|
+
title: `${itemNameArg} has no demo`,
|
|
179
|
+
detail: `${tagName ? `<${tagName}>` : 'This element'} is a registered custom element without a static demo. It is listed under "Without demo".`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
case 'ignored':
|
|
183
|
+
return {
|
|
184
|
+
title: `${itemNameArg} cannot be previewed`,
|
|
185
|
+
detail: 'This export is not a custom element with a demo, so the catalog does not list it.',
|
|
186
|
+
};
|
|
187
|
+
case 'missing':
|
|
188
|
+
return {
|
|
189
|
+
title: `${itemNameArg} was not found`,
|
|
190
|
+
detail: `No entry with this name exists in "${sectionNameArg}".`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as plugins from './wcctools.plugins.js';
|
|
2
|
+
import { determinePropertyType, type IReactivePropertyDeclaration } from './elements/wcctools.helpers.js';
|
|
3
|
+
|
|
4
|
+
/** The reactive property declarations Lit keeps on an element class. */
|
|
5
|
+
type TPropertyDeclarations = Map<PropertyKey, IReactivePropertyDeclaration>;
|
|
6
|
+
|
|
7
|
+
const skippedPropertyNames = new Set(['goBright', 'domtools']);
|
|
8
|
+
|
|
9
|
+
const scalarKinds = new Set<plugins.interfaces.TWccPropertyKind>(['String', 'Number', 'Boolean', 'Enum']);
|
|
10
|
+
|
|
11
|
+
const getDeclarations = (elementClassArg: unknown): TPropertyDeclarations | null => {
|
|
12
|
+
const declarations = (elementClassArg as { elementProperties?: unknown }).elementProperties;
|
|
13
|
+
return declarations instanceof Map ? declarations as TPropertyDeclarations : null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const toScalar = (valueArg: unknown): plugins.interfaces.TWccPropertyScalar => {
|
|
17
|
+
if (typeof valueArg === 'string' || typeof valueArg === 'number' || typeof valueArg === 'boolean') {
|
|
18
|
+
return valueArg;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const toJson = (valueArg: unknown): string | undefined => {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(valueArg ?? null, null, 2);
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The editable properties an element class declares and the values its instance holds.
|
|
33
|
+
* It runs in the realm that defined the class, so Lit's `type: String` declarations compare
|
|
34
|
+
* against the same `String` and the values never cross into another document's realm.
|
|
35
|
+
*/
|
|
36
|
+
export class WccElementProperties {
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly elementClass: unknown,
|
|
39
|
+
private readonly instance: HTMLElement,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
public describe(): plugins.interfaces.IWccPropertyDescriptor[] | null {
|
|
43
|
+
const declarations = getDeclarations(this.elementClass);
|
|
44
|
+
if (!declarations) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const descriptors: plugins.interfaces.IWccPropertyDescriptor[] = [];
|
|
48
|
+
for (const [key, declaration] of declarations) {
|
|
49
|
+
if (typeof key !== 'string' || key.startsWith('#') || skippedPropertyNames.has(key)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const kind = determinePropertyType(declaration);
|
|
53
|
+
if (!kind) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const value = (this.instance as unknown as Record<string, unknown>)[key];
|
|
57
|
+
if (scalarKinds.has(kind)) {
|
|
58
|
+
descriptors.push(kind === 'Enum'
|
|
59
|
+
? { name: key, kind, value: toScalar(value), enumValues: Object.keys(declaration.type as object) }
|
|
60
|
+
: { name: key, kind, value: toScalar(value) });
|
|
61
|
+
} else {
|
|
62
|
+
descriptors.push({ name: key, kind, json: toJson(value) });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return descriptors;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public set(nameArg: string, valueArg: plugins.interfaces.TWccPropertyScalar): plugins.interfaces.TWccPropertyEditResult {
|
|
69
|
+
const kind = this.getKind(nameArg);
|
|
70
|
+
if (!kind || !scalarKinds.has(kind)) {
|
|
71
|
+
return { ok: false, error: `${nameArg} is not an editable scalar property` };
|
|
72
|
+
}
|
|
73
|
+
(this.instance as unknown as Record<string, unknown>)[nameArg] = valueArg;
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public setJson(nameArg: string, jsonArg: string): plugins.interfaces.TWccPropertyEditResult {
|
|
78
|
+
const kind = this.getKind(nameArg);
|
|
79
|
+
if (kind !== 'Object' && kind !== 'Array') {
|
|
80
|
+
return { ok: false, error: `${nameArg} is not an Object or Array property` };
|
|
81
|
+
}
|
|
82
|
+
let value: unknown;
|
|
83
|
+
try {
|
|
84
|
+
value = JSON.parse(jsonArg);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return { ok: false, error: `Invalid JSON: ${(error as Error).message}` };
|
|
87
|
+
}
|
|
88
|
+
(this.instance as unknown as Record<string, unknown>)[nameArg] = value;
|
|
89
|
+
return { ok: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private getKind(nameArg: string): plugins.interfaces.TWccPropertyKind | null {
|
|
93
|
+
const declaration = getDeclarations(this.elementClass)?.get(nameArg);
|
|
94
|
+
return declaration && !skippedPropertyNames.has(nameArg) ? determinePropertyType(declaration) : null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { DeesElement, property, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
|
|
2
|
+
import * as plugins from '../wcctools.plugins.js';
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
interface HTMLElementTagNameMap {
|
|
6
|
+
'wcc-preview': WccPreview;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The surface a preview document renders demos and pages into. The preview document is as
|
|
12
|
+
* wide as the selected viewport, so width media queries and the `wccToolsViewport` container
|
|
13
|
+
* queries of the rendered elements both follow it.
|
|
14
|
+
*/
|
|
15
|
+
@customElement('wcc-preview')
|
|
16
|
+
export class WccPreview extends DeesElement {
|
|
17
|
+
@property({ attribute: false })
|
|
18
|
+
accessor emptyState: plugins.interfaces.IWccEmptyState | null = null;
|
|
19
|
+
|
|
20
|
+
public static styles = [
|
|
21
|
+
css`
|
|
22
|
+
:host {
|
|
23
|
+
display: block;
|
|
24
|
+
position: relative;
|
|
25
|
+
min-height: 100vh;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.viewport {
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
container-type: inline-size;
|
|
31
|
+
container-name: wccToolsViewport;
|
|
32
|
+
position: relative;
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.empty-state {
|
|
37
|
+
position: absolute;
|
|
38
|
+
inset: 0;
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
padding: 24px;
|
|
43
|
+
pointer-events: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.empty-state-card {
|
|
47
|
+
pointer-events: auto;
|
|
48
|
+
max-width: 420px;
|
|
49
|
+
padding: 20px 24px;
|
|
50
|
+
border-radius: 8px;
|
|
51
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
52
|
+
background: rgba(10, 10, 10, 0.88);
|
|
53
|
+
backdrop-filter: blur(8px);
|
|
54
|
+
color: #e5e5e5;
|
|
55
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
56
|
+
text-align: center;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.empty-state-title {
|
|
60
|
+
margin: 0 0 6px;
|
|
61
|
+
font-size: 15px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
overflow-wrap: anywhere;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.empty-state-detail {
|
|
67
|
+
margin: 0;
|
|
68
|
+
font-size: 13px;
|
|
69
|
+
line-height: 1.5;
|
|
70
|
+
color: #a3a3a3;
|
|
71
|
+
overflow-wrap: anywhere;
|
|
72
|
+
}
|
|
73
|
+
`,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
public render(): TemplateResult {
|
|
77
|
+
const pattern = this.goBright
|
|
78
|
+
? { dot: '#CCCCCC', base: '#eeeeee' }
|
|
79
|
+
: { dot: '#444444', base: '#222222' };
|
|
80
|
+
return html`
|
|
81
|
+
<style>
|
|
82
|
+
.viewport {
|
|
83
|
+
background:
|
|
84
|
+
radial-gradient(${pattern.dot} 3px, transparent 4px),
|
|
85
|
+
radial-gradient(${pattern.dot} 3px, transparent 4px),
|
|
86
|
+
linear-gradient(${pattern.base} 4px, transparent 0),
|
|
87
|
+
linear-gradient(45deg, transparent 74px, transparent 75px, ${pattern.dot} 75px, ${pattern.dot} 76px, transparent 77px, transparent 109px),
|
|
88
|
+
linear-gradient(-45deg, transparent 75px, transparent 76px, ${pattern.dot} 76px, ${pattern.dot} 77px, transparent 78px, transparent 109px),
|
|
89
|
+
${pattern.base};
|
|
90
|
+
background-size: 109px 109px, 109px 109px, 100% 6px, 109px 109px, 109px 109px;
|
|
91
|
+
background-position: 54px 55px, 0px 0px, 0px 0px, 0px 0px, 0px 0px;
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
94
|
+
<div class="viewport"></div>
|
|
95
|
+
${this.emptyState ? html`
|
|
96
|
+
<div class="empty-state">
|
|
97
|
+
<div class="empty-state-card" role="status">
|
|
98
|
+
<h2 class="empty-state-title">${this.emptyState.title}</h2>
|
|
99
|
+
<p class="empty-state-detail">${this.emptyState.detail}</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
` : null}
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The element demos and pages render into; available once the first update completed. */
|
|
107
|
+
public async getViewportElement(): Promise<HTMLElement> {
|
|
108
|
+
await this.updateComplete;
|
|
109
|
+
return this.shadowRoot.querySelector('.viewport') as HTMLElement;
|
|
110
|
+
}
|
|
111
|
+
}
|