From 82e1c7c36905df3cb606fcb169dbb80438c51ddb Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Thu, 20 Jun 2024 19:44:51 +0300 Subject: [PATCH 1/8] feat(tools): knobs elements wip dev server knobs --- .../dev-server/plugins/pfe-dev-server.ts | 2 +- .../dev-server/plugins/templates/index.html | 8 +- .../dev-server/plugins/templates/knobs.html | 16 ++ tools/pfe-tools/elements/pft-element-knobs.ts | 161 ++++++++++++++++++ tools/pfe-tools/package.json | 1 + 5 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 tools/pfe-tools/dev-server/plugins/templates/knobs.html create mode 100644 tools/pfe-tools/elements/pft-element-knobs.ts diff --git a/tools/pfe-tools/dev-server/plugins/pfe-dev-server.ts b/tools/pfe-tools/dev-server/plugins/pfe-dev-server.ts index df7aa48800..3569345fff 100644 --- a/tools/pfe-tools/dev-server/plugins/pfe-dev-server.ts +++ b/tools/pfe-tools/dev-server/plugins/pfe-dev-server.ts @@ -5,7 +5,7 @@ import type { SiteOptions } from '../../config.js'; import { pfeDevServerRouterMiddleware } from './dev-server-router.js'; import { pfeDevServerTemplateMiddleware } from './dev-server-templates.js'; -export type PfeDevServerInternalConfig = Required & { +type PfeDevServerInternalConfig = Required & { site: Required; }; diff --git a/tools/pfe-tools/dev-server/plugins/templates/index.html b/tools/pfe-tools/dev-server/plugins/templates/index.html index df77100625..f0ae097248 100644 --- a/tools/pfe-tools/dev-server/plugins/templates/index.html +++ b/tools/pfe-tools/dev-server/plugins/templates/index.html @@ -105,7 +105,10 @@
  • {{ first.title }} -
      {% for d in group %} +
        +
      • + Knobs +
      • {% for d in group %}
      • {{ d.title }}
      • {% endfor %} @@ -128,7 +131,8 @@

        {{ first.title }}

        {{ primary }} -
          {% for d in group %}{% if not loop.first %} +
            +
          • Knobs
          • {% for d in group %}{% if not loop.first %}
          • {{ d.title }}
          • {% endif %}{% endfor %} diff --git a/tools/pfe-tools/dev-server/plugins/templates/knobs.html b/tools/pfe-tools/dev-server/plugins/templates/knobs.html new file mode 100644 index 0000000000..8342139026 --- /dev/null +++ b/tools/pfe-tools/dev-server/plugins/templates/knobs.html @@ -0,0 +1,16 @@ + + + + + + diff --git a/tools/pfe-tools/elements/pft-element-knobs.ts b/tools/pfe-tools/elements/pft-element-knobs.ts new file mode 100644 index 0000000000..b98e439935 --- /dev/null +++ b/tools/pfe-tools/elements/pft-element-knobs.ts @@ -0,0 +1,161 @@ +import type { + Attribute, + CustomElementDeclaration, + Declaration, + Package, +} from 'custom-elements-manifest'; + +import { LitElement, css, html, type PropertyValues } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { property } from 'lit/decorators/property.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; + +type KnobRenderer = ( + this: PftElementKnobs, + attribute: T, + index: number, + array: T[], +) => unknown; + +export type AttributeRenderer = KnobRenderer; + +const isCustomElementDecl = (decl: Declaration): decl is CustomElementDeclaration => + 'customElement' in decl; + +@customElement('pft-element-knobs') +export class PftElementKnobs extends LitElement { + static styles = [ + css` + #element { + padding: 1em; + } + fieldset { + display: grid; + gap: 4px; + grid-template-columns: max-content 1fr; + align-items: center; + } + `, + ]; + + @property() tag?: string; + + @property({ attribute: false }) manifest?: Package; + + @property({ attribute: false }) element: T | null = null; + + @property({ attribute: false }) renderAttribute: AttributeRenderer = this.#renderAttribute; + + #mo = new MutationObserver(this.#loadTemplate); + + #node: DocumentFragment | null = null; + + #elementDecl: CustomElementDeclaration | null = null; + + override connectedCallback(): void { + super.connectedCallback(); + this.#mo.observe(this, { childList: true }); + this.#loadTemplate(); + } + + get #template() { + return this.querySelector('template'); + } + + protected willUpdate(changed: PropertyValues): void { + if (changed.has('manifest') || changed.has('tag')) { + for (const mod of this.manifest?.modules ?? []) { + for (const decl of mod.declarations ?? []) { + if (isCustomElementDecl(decl) && decl.tagName === this.tag) { + this.#elementDecl = decl; + } + } + } + } + } + + #loadTemplate() { + const script = this.querySelector('script[type="application/json"]'); + if (script) { + try { + this.manifest = JSON.parse(script.textContent ?? ''); + } catch { + null; + } + } + if (this.#template && this.tag) { + this.#node = this.#template.content.cloneNode(true) as DocumentFragment; + this.element = this.#node.querySelector(this.tag); + } + } + + #renderAttribute(attribute: Attribute) { + const QUOTE_RE = /^['"](.*)['"]$/; + // TODO: non-typescript types? + const isBoolean = attribute?.type?.text === 'boolean'; + const isUnion = !!attribute?.type?.text?.includes?.('|'); + let isEnum = false; + let values: string[]; + if (isUnion) { + values = attribute?.type?.text + .split('|') + .map(x => x.trim()) + .filter(x => x !== 'undefined' && x !== 'null') ?? []; + if (values.length > 1) { + isEnum = true; + } + } + const id = `knob-attribute-${attribute.name}`; + return html` + ${isBoolean ? html` + ` : isEnum ? html` + ${values!.map(x => html` + ${x.trim().replace(QUOTE_RE, '$1')}`)} + + ` : html` + `} + `; + } + + #renderKnobs() { + const decl = this.#elementDecl; + const { element, tag, manifest } = this; + if (element && decl && tag && manifest) { + const { attributes } = decl; + + const onChange = (e: Event & { target: HTMLInputElement }) => { + if (e.target instanceof HTMLInputElement && e.target.type === 'checkbox') { + this.element?.toggleAttribute(e.target.dataset.attribute!, e.target.checked); + } else { + this.element?.setAttribute(e.target.dataset.attribute!, e.target.value); + } + }; + + return html` +
            + ${!attributes ? '' : html` +
            + Attributes + ${attributes.map(this.renderAttribute, this)} +
            `} +
            + `; + } + } + + protected override render(): unknown { + return html` +
            ${this.#node ?? ''}
            + ${this.#renderKnobs() ?? ''} + `; + } +} diff --git a/tools/pfe-tools/package.json b/tools/pfe-tools/package.json index 638714c63f..af88ad6d52 100644 --- a/tools/pfe-tools/package.json +++ b/tools/pfe-tools/package.json @@ -26,6 +26,7 @@ "./dev-server/config.js": "./dev-server/config.js", "./dev-server/demo.js": "./dev-server/demo.js", "./environment.js": "./environment.js", + "./elements/": "./elements/", "./11ty/DocsPage.js": "./11ty/DocsPage.js", "./11ty/plugins/anchors.cjs": "./11ty/plugins/anchors.cjs", "./11ty/plugins/custom-elements-manifest.cjs": "./11ty/plugins/custom-elements-manifest.cjs", From 05e5dde455d0714c854b6cfbbe1e79450a472db2 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Tue, 25 Jun 2024 09:58:30 +0300 Subject: [PATCH 2/8] refactor: attribute renderer --- tools/pfe-tools/elements/pft-element-knobs.ts | 69 ++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/tools/pfe-tools/elements/pft-element-knobs.ts b/tools/pfe-tools/elements/pft-element-knobs.ts index b98e439935..d665d1beca 100644 --- a/tools/pfe-tools/elements/pft-element-knobs.ts +++ b/tools/pfe-tools/elements/pft-element-knobs.ts @@ -12,11 +12,23 @@ import { ifDefined } from 'lit/directives/if-defined.js'; type KnobRenderer = ( this: PftElementKnobs, - attribute: T, - index: number, - array: T[], + member: T, + info: T extends Attribute ? AttributeKnobInfo : KnobInfo, ) => unknown; +interface KnobInfo { + knobId: string; +} + +interface AttributeKnobInfo extends KnobInfo { + isBoolean: boolean; + isEnum: boolean; + isNullable: boolean; + isNumber: boolean; + isOptional: boolean; + values: string[]; +} + export type AttributeRenderer = KnobRenderer; const isCustomElementDecl = (decl: Declaration): decl is CustomElementDeclaration => @@ -89,37 +101,46 @@ export class PftElementKnobs extends LitElement { } } - #renderAttribute(attribute: Attribute) { + #getAttributeInfo(attribute: Attribute): AttributeKnobInfo { + // NOTE: we assume typescript types + const type = attribute?.type?.text ?? ''; + const isUnion = !!type.includes?.('|'); + const types = type.split('|').map(x => x.trim()); + const isNullable = types.includes('null'); + const isOptional = types.includes('undefined'); + const isNumber = types.includes('number'); + const isBoolean = types.every(type => type.match(/null|undefined|boolean/)); + const values = isUnion ? types.filter(x => x !== 'undefined' && x !== 'null') : []; + const isEnum = isUnion && values.length > 1; + const knobId = `knob-attribute-${attribute.name}`; + return { + knobId, + isBoolean, + isEnum, + isNullable, + isNumber, + isOptional, + values, + }; + } + + #renderAttribute(attribute: Attribute, info: AttributeKnobInfo) { + const { knobId, isEnum, isBoolean, values } = info; const QUOTE_RE = /^['"](.*)['"]$/; - // TODO: non-typescript types? - const isBoolean = attribute?.type?.text === 'boolean'; - const isUnion = !!attribute?.type?.text?.includes?.('|'); - let isEnum = false; - let values: string[]; - if (isUnion) { - values = attribute?.type?.text - .split('|') - .map(x => x.trim()) - .filter(x => x !== 'undefined' && x !== 'null') ?? []; - if (values.length > 1) { - isEnum = true; - } - } - const id = `knob-attribute-${attribute.name}`; return html` - ${isBoolean ? html` - ${attribute.name}${isBoolean ? html` + ` : isEnum ? html` - ${values!.map(x => html` ${x.trim().replace(QUOTE_RE, '$1')}`)} ` : html` - `} @@ -145,7 +166,7 @@ export class PftElementKnobs extends LitElement { ${!attributes ? '' : html`
            Attributes - ${attributes.map(this.renderAttribute, this)} + ${attributes.map(attr => this.renderAttribute(attr, this.#getAttributeInfo(attr)))}
            `} `; From 65cb9f9687cc040957946a32c90b13501e59a610 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Tue, 25 Jun 2024 14:29:36 +0300 Subject: [PATCH 3/8] fix: wip knobs --- elements/pf-banner/demo/pf-banner.html | 10 -- elements/pf-button/pf-button.ts | 4 +- elements/pf-card/pf-card.ts | 2 +- package.json | 14 +- tools/pfe-tools/dev-server/config.ts | 1 + .../dev-server/plugins/templates/index.html | 25 ++- .../dev-server/plugins/templates/knobs.html | 1 + tools/pfe-tools/elements/pft-element-knobs.ts | 145 +++++++++++++----- 8 files changed, 147 insertions(+), 55 deletions(-) diff --git a/elements/pf-banner/demo/pf-banner.html b/elements/pf-banner/demo/pf-banner.html index 6d00374a8d..780ecbc842 100644 --- a/elements/pf-banner/demo/pf-banner.html +++ b/elements/pf-banner/demo/pf-banner.html @@ -1,15 +1,5 @@ Default Banner -Blue Banner -Red Banner -Green Banner -Gold Banner - - diff --git a/elements/pf-button/pf-button.ts b/elements/pf-button/pf-button.ts index 067de822c7..9cf5c63f18 100644 --- a/elements/pf-button/pf-button.ts +++ b/elements/pf-button/pf-button.ts @@ -29,7 +29,9 @@ export type ButtonVariant = ( * actions a user can take in an application, like submitting a form, canceling a * process, or creating a new object. Buttons can also be used to take a user to a * new location, like another page inside of a web application, or an external site - * such as help or documentation.. + * such as help or documentation. + * @slot - Button text label + * @slot icon - Button Icon, overrides `icon` attribute * @summary Allows users to perform an action when triggered * @cssprop {} [--pf-c-button--FontSize=1rem] * @cssprop [--pf-c-button--FontWeight=400] diff --git a/elements/pf-card/pf-card.ts b/elements/pf-card/pf-card.ts index 72a29419a6..8215121ee3 100644 --- a/elements/pf-card/pf-card.ts +++ b/elements/pf-card/pf-card.ts @@ -94,7 +94,7 @@ export class PfCard extends LitElement {
            - + this.requestUpdate()}>