From 12971283d0a077268f580e0bc5b81964a870f133 Mon Sep 17 00:00:00 2001 From: Christian Milocanovich Date: Tue, 11 Nov 2025 14:40:06 -0300 Subject: [PATCH 1/2] refactor action-list - replace action-list-group by action-list-item --- .../action-list/action-list-render.tsx | 55 ++--- .../action-list-item/action-list-item.scss | 68 ++++++ .../action-list-item/action-list-item.tsx | 225 ++++++++++++++---- src/components/action-list/types.ts | 36 ++- src/components/action-list/utils.ts | 2 +- .../action-list/action-list.showcase.tsx | 4 +- .../assets/components/action-list/models.tsx | 119 ++++++++- 7 files changed, 401 insertions(+), 108 deletions(-) diff --git a/src/components/action-list/action-list-render.tsx b/src/components/action-list/action-list-render.tsx index d3ad6ec5..df196042 100644 --- a/src/components/action-list/action-list-render.tsx +++ b/src/components/action-list/action-list-render.tsx @@ -71,8 +71,8 @@ const renderMapping: { itemModel, actionListRenderState, disabled: boolean, - nested = false, - nestedExpandable = false + nested: boolean, + nestedExpandable: boolean = false ) => ( - ), - group: (itemModel, actionListRenderState) => ( - {itemModel.items?.map(item => actionListRenderState.renderItem( @@ -116,7 +107,7 @@ const renderMapping: { itemModel.expandable ) )} - + ), separator: () => (
- itemModel.type === "actionable" - ? renderMapping.actionable( - itemModel as any, // THIS IS A WA - actionListRenderState, - disabled, - nested, - nestedExpandable - ) - : renderMapping[itemModel.type]( - itemModel as any, // THIS IS A WA - actionListRenderState - ); + nested?: boolean +) => { + if (itemModel.type === "separator") { + return renderMapping.separator(itemModel as any, actionListRenderState); + } + + return renderMapping.actionable( + itemModel as any, // THIS IS A WA + actionListRenderState, + disabled, + nested + ); +}; const FIRST_ITEM_GREATER_THAN_SECOND = -1; const SECOND_ITEM_GREATER_THAN_FIRST = 0; @@ -713,7 +702,7 @@ export class ChActionListRender { const itemInfo = this.#getItemOrGroupInfo(actionListItemOrGroup.id); this.#checkIfMustExpandCollapseGroup(itemInfo); - if (itemInfo.type === "group" && !itemInfo.expandable) { + if (!itemInfo.expandable) { return; } this.itemClick.emit(this.#flattenedModel.get(itemInfo.id)); @@ -723,11 +712,7 @@ export class ChActionListRender { itemInfo: ActionListItemActionable | ActionListItemGroup ) => { // Toggle the expanded/collapsed in the group on click - if ( - itemInfo.type === "group" && - itemInfo.expandable && - !itemInfo.disabled - ) { + if (itemInfo.expandable && !itemInfo.disabled) { itemInfo.expanded = !itemInfo.expanded; forceUpdate(this); } diff --git a/src/components/action-list/internal/action-list-item/action-list-item.scss b/src/components/action-list/internal/action-list-item/action-list-item.scss index 701b6dca..2806f98e 100644 --- a/src/components/action-list/internal/action-list-item/action-list-item.scss +++ b/src/components/action-list/internal/action-list-item/action-list-item.scss @@ -17,6 +17,14 @@ "stretch-start block-end stretch-end" max-content / max-content 1fr max-content; } +.action-with-children { + display: grid; + grid-template: + "expandable-start stretch-start block-start stretch-end expandable-end" max-content + "expandable-start stretch-start inline-caption stretch-end expandable-end" max-content + "expandable-start stretch-start block-end stretch-end expandable-end" max-content / min-content max-content 1fr max-content min-content; +} + // TODO: Fix "inline-caption: start" items support .align-container { display: grid; @@ -181,3 +189,63 @@ --ch-start-start: var(--ch-start-img--disabled); } } + +// - - - - - - - - - - - - - - - - +// Expandable button +// - - - - - - - - - - - - - - - - +.expandable-button { + grid-area: expandable-start; + overflow: hidden; + text-indent: -9999px; + white-space: nowrap; + + &::before { + content: ""; + inline-size: var(--ch-action-list-group__expandable-button-size); + block-size: var(--ch-action-list-group__expandable-button-size); + background-color: currentColor; + -webkit-mask: no-repeat center / + var(--ch-action-list-group__expandable-button-image-size) $expandable-icon; + } + + &--collapsed::before { + transform: rotate(-90deg); // TODO: Add RTL support + } +} + +.expandable-button--start { + grid-area: expandable-start; +} + +.expandable-button--end { + grid-area: expandable-end; + + &::before { + transform: rotate(90deg); + } + + &--collapsed::before { + transform: rotate(-90deg); // TODO: Add RTL support + } +} + +// - - - - - - - - - - - - - - - - +// Expandable content +// - - - - - - - - - - - - - - - - +.expandable { + display: grid; + grid-auto-rows: min-content; + position: relative; + padding: 0; + margin: 0; + + &--lazy-loaded { + content-visibility: auto; + contain-intrinsic-size: auto 100px; + } +} + +.expandable--collapsed { + display: none; + overflow: hidden; +} diff --git a/src/components/action-list/internal/action-list-item/action-list-item.tsx b/src/components/action-list/internal/action-list-item/action-list-item.tsx index c082632b..e5cd58c8 100644 --- a/src/components/action-list/internal/action-list-item/action-list-item.tsx +++ b/src/components/action-list/internal/action-list-item/action-list-item.tsx @@ -3,24 +3,20 @@ import { Element, Event, EventEmitter, + h, Host, + Method, Prop, - Watch, - h + Watch } from "@stencil/core"; import { - ActionListImagePathCallback, - ActionListItemAdditionalAction, - ActionListItemAdditionalBase, - ActionListItemAdditionalCustom, - ActionListItemAdditionalInformation, - ActionListItemAdditionalInformationSection, - ActionListItemAdditionalItem, - ActionListItemAdditionalItemActionType, - ActionListItemAdditionalModel -} from "../../types"; + DEFAULT_GET_IMAGE_PATH_CALLBACK, + getControlRegisterProperty +} from "../../../../common/registry-properties"; import { renderImg } from "../../../../common/renders"; import { + ACTION_LIST_GROUP_EXPORT_PARTS, + ACTION_LIST_GROUP_PARTS_DICTIONARY, ACTION_LIST_ITEM_EXPORT_PARTS, ACTION_LIST_ITEM_PARTS_DICTIONARY, ACTION_LIST_PARTS_DICTIONARY, @@ -29,25 +25,33 @@ import { startPseudoImageTypeDictionary } from "../../../../common/reserved-names"; import { - ActionListCaptionChangeEventDetail, - ActionListFixedChangeEventDetail, - ActionListItemActionTypeBlockInfo -} from "./types"; + GxImageMultiState, + GxImageMultiStateStart +} from "../../../../common/types"; import { tokenMap, updateDirectionInImageCustomVar } from "../../../../common/utils"; -import { computeExportParts } from "./compute-exportparts"; -import { computeActionTypeBlocks } from "./compute-editing-sections"; import { ActionListTranslations } from "../../translations"; import { - GxImageMultiState, - GxImageMultiStateStart -} from "../../../../common/types"; + ActionListImagePathCallback, + ActionListItemAdditionalAction, + ActionListItemAdditionalBase, + ActionListItemAdditionalCustom, + ActionListItemAdditionalInformation, + ActionListItemAdditionalInformationSection, + ActionListItemAdditionalItem, + ActionListItemAdditionalItemActionType, + ActionListItemAdditionalMenu, + ActionListItemAdditionalModel +} from "../../types"; +import { computeActionTypeBlocks } from "./compute-editing-sections"; +import { computeExportParts } from "./compute-exportparts"; import { - DEFAULT_GET_IMAGE_PATH_CALLBACK, - getControlRegisterProperty -} from "../../../../common/registry-properties"; + ActionListCaptionChangeEventDetail, + ActionListFixedChangeEventDetail, + ActionListItemActionTypeBlockInfo +} from "./types"; const ACTION_TYPE_PARTS = { fix: ACTION_LIST_ITEM_PARTS_DICTIONARY.ACTION_FIX, @@ -56,12 +60,16 @@ const ACTION_TYPE_PARTS = { custom: ACTION_LIST_ITEM_PARTS_DICTIONARY.ACTION_CUSTOM } as const; +const EXPANDABLE_ID = "expandable"; + @Component({ tag: "ch-action-list-item", styleUrl: "action-list-item.scss", shadow: { delegatesFocus: true } }) export class ChActionListItem { + #buttonRef: HTMLButtonElement; + #additionalItemListenerDictionary = { fix: () => this.fixedChange.emit({ itemId: this.el.id, value: !this.fixed }), @@ -172,6 +180,24 @@ export class ChActionListItem { */ @Prop({ mutable: true }) editing = false; + /** + * If the item has a sub-tree, this attribute determines if the subtree is + * displayed. + */ + @Prop() readonly expandable?: boolean; + + /** + * If the item has a sub-tree, this attribute determines if the subtree is + * displayed. + */ + @Prop() readonly expanded?: boolean; + @Watch("expanded") + expandedChanged(isExpanded: boolean) { + this.#getDirectActionListItems().map(item => { + item.nestedExpandable = isExpanded; + }); + } + /** * */ @@ -190,6 +216,12 @@ export class ChActionListItem { */ @Prop({ mutable: true }) indeterminate = false; + /** + * Determine if the items are lazy loaded when opening the first time the + * control. + */ + @Prop({ mutable: true }) lazyLoad = false; + /** * This attribute represents additional info for the control that is included * when dragging the item. @@ -270,6 +302,16 @@ export class ChActionListItem { */ @Event() itemDragEnd: EventEmitter; + /** + * Set the focus in the control if `expandable === true`. + */ + @Method() + async setFocus() { + if (this.expandable && this.#buttonRef) { + this.#buttonRef.focus(); + } + } + #removeEditMode = (shouldFocusHeader: boolean, commitEdition = false) => () => { @@ -311,16 +353,51 @@ export class ChActionListItem { this.#removeEditMode(true, commitEdition)(); }; + #getDirectActionListItems = (): HTMLChActionListItemElement[] => + Array.from( + this.el.querySelectorAll(":scope > ch-action-list-item") + ) as HTMLChActionListItemElement[]; + #renderAdditionalItems = (additionalItems: ActionListItemAdditionalItem[]) => - additionalItems.map(item => - (item as ActionListItemAdditionalCustom).jsx - ? (item as ActionListItemAdditionalCustom).jsx() - : this.#renderAdditionalItem( - item as - | ActionListItemAdditionalBase - | ActionListItemAdditionalAction - ) - ); + additionalItems.map(item => { + if ((item as ActionListItemAdditionalCustom).jsx) { + return (item as ActionListItemAdditionalCustom).jsx(); + } + + if ((item as ActionListItemAdditionalMenu)?.menu) { + return ( + + Menu + + ); + } + + return this.#renderAdditionalItem( + item as ActionListItemAdditionalBase | ActionListItemAdditionalAction + ); + }); + + #renderExpandableButton = (hasContent, expanded) => ( + + ); #renderAdditionalItem = ( item: ActionListItemAdditionalBase | ActionListItemAdditionalAction @@ -503,7 +580,7 @@ export class ChActionListItem { // Additional parts if (parts.size > 0) { - exportParts = `${ACTION_LIST_ITEM_EXPORT_PARTS},${Array.from( + exportParts = `${ACTION_LIST_ITEM_EXPORT_PARTS},${ACTION_LIST_GROUP_EXPORT_PARTS},${Array.from( parts ).join(",")}`; } @@ -511,7 +588,8 @@ export class ChActionListItem { this.el.setAttribute( "exportparts", - exportParts ?? ACTION_LIST_ITEM_EXPORT_PARTS + exportParts ?? + `${ACTION_LIST_ITEM_EXPORT_PARTS},${ACTION_LIST_GROUP_EXPORT_PARTS}` ); }; @@ -706,11 +784,15 @@ export class ChActionListItem { : null; }; + #getExpandedValue = (): boolean => + this.expandable ? this.expanded ?? false : true; + connectedCallback() { this.el.setAttribute("role", "listitem"); this.el.setAttribute("part", ACTION_LIST_PARTS_DICTIONARY.ITEM); this.#setExportParts(); this.#setActionTypeBlocks(); + this.expandedChanged(this.expandable); } render() { @@ -723,30 +805,51 @@ export class ChActionListItem { const blockEnd = hasAdditionalInfo && additionalInfo["block-end"]; const stretchEnd = hasAdditionalInfo && additionalInfo["stretch-end"]; + const hasChildren = this.el.hasChildNodes(); + const hasContent = !this.lazyLoad; + const expanded = hasContent && this.#getExpandedValue(); const hasParts = !!this.parts; return ( + + {hasChildren && ( +
    + +
+ )}
); } diff --git a/src/components/action-list/types.ts b/src/components/action-list/types.ts index 519f3908..11211da5 100644 --- a/src/components/action-list/types.ts +++ b/src/components/action-list/types.ts @@ -1,4 +1,5 @@ import { GxImageMultiState, ImageRender } from "../../common/types"; +import { ActionMenuModel } from "../action-menu/types"; // import { ChActionListRender } from "./action-list-render"; export type ActionListModel = ActionListItemModel[]; @@ -8,7 +9,6 @@ export type ActionListModel = ActionListItemModel[]; // - - - - - - - - - - - - - - - - - - - - export type ActionListItemType = | ActionListItemTypeActionable - | ActionListItemTypeGroup | ActionListItemTypeSeparator; export type ActionListItemTypeActionable = "actionable"; @@ -43,7 +43,7 @@ export interface ActionListItemModelMap { // - - - - - - - - - - - - - - - - - - - - // List Item Actionable // - - - - - - - - - - - - - - - - - - - - -export type ActionListItemActionable = { +export type ActionListItem = { id: string; additionalInformation?: ActionListItemAdditionalInformation; @@ -65,7 +65,13 @@ export type ActionListItemActionable = { selected?: boolean; - // TODO: Add support to avoid setting this property + expandable?: boolean; + expanded?: boolean; + + items?: ActionListItemActionable[]; +}; + +export type ActionListItemActionable = ActionListItem & { type: ActionListItemTypeActionable; }; @@ -94,7 +100,8 @@ export type ActionListItemAdditionalItem = | ActionListItemAdditionalBase | ActionListItemAdditionalAction | ActionListItemAdditionalCustom - | ActionListItemAdditionalSlot; + | ActionListItemAdditionalSlot + | ActionListItemAdditionalMenu; export type ActionListItemAdditionalBase = { id?: string; @@ -117,6 +124,10 @@ export type ActionListItemAdditionalCustom = { part?: string; }; +export type ActionListItemAdditionalMenu = { + menu: ActionMenuModel; +}; + export type ActionListItemAdditionalAction = ActionListItemAdditionalBase & { id: string; accessibleName: string; @@ -142,22 +153,7 @@ export type ActionListItemAdditionalItemActionType = // - - - - - - - - - - - - - - - - - - - - // List Item Heading // - - - - - - - - - - - - - - - - - - - - -export type ActionListItemGroup = { - id: string; - caption: string; - disabled?: boolean; - expandable?: boolean; - expanded?: boolean; - - items: ActionListItemActionable[]; - - /** - * Establish the order at which the item will be placed in its parent. - * Multiple items can have the same `order` value. - */ - order?: number; - part?: string; - selected?: boolean; // TODO: This property does not make much sense if expandable: false +export type ActionListItemGroup = ActionListItem & { type: ActionListItemTypeGroup; }; diff --git a/src/components/action-list/utils.ts b/src/components/action-list/utils.ts index 13116111..16a2fc44 100644 --- a/src/components/action-list/utils.ts +++ b/src/components/action-list/utils.ts @@ -9,7 +9,7 @@ import { // Tags export const ACTION_LIST_ITEM_TAG = "ch-action-list-item"; -export const ACTION_LIST_GROUP_TAG = "ch-action-list-group"; +export const ACTION_LIST_GROUP_TAG = "ch-action-list-item"; // TODO: Deprecate "ch-action-list-group" // Selectors export const ACTION_LIST_ITEM_SELECTOR = (id: string) => diff --git a/src/showcase/assets/components/action-list/action-list.showcase.tsx b/src/showcase/assets/components/action-list/action-list.showcase.tsx index dd2308a5..e7652119 100644 --- a/src/showcase/assets/components/action-list/action-list.showcase.tsx +++ b/src/showcase/assets/components/action-list/action-list.showcase.tsx @@ -1,6 +1,7 @@ import { h } from "@stencil/core"; import { ShowcaseRenderProperties, ShowcaseStory } from "../types"; import { + CustomActions, GitHubChangesModel, GitHubHistoryModel, GxEAINotifications, @@ -48,7 +49,8 @@ const showcaseRenderProperties: ShowcaseRenderProperties ( Date: Tue, 11 Nov 2025 15:03:24 -0300 Subject: [PATCH 2/2] Remove action-list-group --- .../action-list-group/action-list-group.scss | 47 ----- .../action-list-group/action-list-group.tsx | 191 ------------------ .../internal/action-list-group/readme.md | 59 ------ 3 files changed, 297 deletions(-) delete mode 100644 src/components/action-list/internal/action-list-group/action-list-group.scss delete mode 100644 src/components/action-list/internal/action-list-group/action-list-group.tsx delete mode 100644 src/components/action-list/internal/action-list-group/readme.md diff --git a/src/components/action-list/internal/action-list-group/action-list-group.scss b/src/components/action-list/internal/action-list-group/action-list-group.scss deleted file mode 100644 index b17944de..00000000 --- a/src/components/action-list/internal/action-list-group/action-list-group.scss +++ /dev/null @@ -1,47 +0,0 @@ -@import "../../../../common/base"; -@import "../../../../common/icons"; - -@include button-reset(); -@include box-sizing(); - -:host, -.group { - display: grid; - grid-template-rows: min-content; -} - -.action { - &::before { - content: ""; - inline-size: var(--ch-action-list-group__expandable-button-size); - block-size: var(--ch-action-list-group__expandable-button-size); - background-color: currentColor; - -webkit-mask: no-repeat center / - var(--ch-action-list-group__expandable-button-image-size) $expandable-icon; - } - - &--collapsed::before { - transform: rotate(-90deg); // TODO: Add RTL support - } -} - -// - - - - - - - - - - - - - - - - -// Expandable content -// - - - - - - - - - - - - - - - - -.expandable { - display: grid; - grid-auto-rows: min-content; - position: relative; - padding: 0; - margin: 0; - - &--lazy-loaded { - content-visibility: auto; - contain-intrinsic-size: auto 100px; - } -} - -.expandable--collapsed { - display: none; - overflow: hidden; -} diff --git a/src/components/action-list/internal/action-list-group/action-list-group.tsx b/src/components/action-list/internal/action-list-group/action-list-group.tsx deleted file mode 100644 index 977deec4..00000000 --- a/src/components/action-list/internal/action-list-group/action-list-group.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { - Component, - Element, - Event, - EventEmitter, - Host, - Method, - Prop, - h -} from "@stencil/core"; -import { tokenMap } from "../../../../common/utils"; -import { - ACTION_LIST_GROUP_EXPORT_PARTS, - ACTION_LIST_GROUP_PARTS_DICTIONARY, - ACTION_LIST_PARTS_DICTIONARY -} from "../../../../common/reserved-names"; - -const EXPANDABLE_ID = "expandable"; - -@Component({ - tag: "ch-action-list-group", - styleUrl: "action-list-group.scss", - shadow: { delegatesFocus: true } -}) -export class ChActionListGroup { - #buttonRef: HTMLButtonElement; - - @Element() el: HTMLChActionListGroupElement; - - /** - * This attributes specifies the caption of the control - */ - @Prop() readonly caption: string; - - /** - * This attribute lets you specify if the element is disabled. - * If disabled, it will not fire any user interaction related event - * (for example, click event). - */ - @Prop({ reflect: true }) readonly disabled: boolean = false; - - /** - * This attribute lets you specify when items are being lazy loaded in the - * control. - */ - @Prop({ mutable: true }) downloading = false; - - /** - * If the item has a sub-tree, this attribute determines if the subtree is - * displayed. - */ - @Prop() readonly expandable?: boolean; - - /** - * If the item has a sub-tree, this attribute determines if the subtree is - * displayed. - */ - @Prop() readonly expanded?: boolean; - // @Watch("expanded") - // expandedChanged(isExpanded: boolean) { - // // Wait until all properties are updated before lazy loading. Otherwise, the - // // lazyLoad property could be updated just after the executing of the function - // setTimeout(() => { - // this.#lazyLoadItems(isExpanded); - // }); - // } - - /** - * Determine if the items are lazy loaded when opening the first time the - * control. - */ - @Prop({ mutable: true }) lazyLoad = false; - - /** - * This attribute represents additional info for the control that is included - * when dragging the item. - */ - @Prop() readonly metadata: string; - - /** - * Specifies a set of parts to use in every DOM element of the control. - */ - @Prop() readonly parts?: string; - // @Watch("parts") - // partsChanged(newParts: string) { - // this.#setExportParts(newParts); - // } - - /** - * This attribute lets you specify if the item is selected - */ - @Prop() readonly selected: boolean = false; - - /** - * `true` to show the downloading spinner when lazy loading the sub items of - * the control. - */ - @Prop() readonly showDownloadingSpinner: boolean = true; - - // /** - // * Fired when the item is being dragged. - // */ - // @Event() itemDragStart: EventEmitter; - - /** - * Fired when the lazy control is expanded an its content must be loaded. - */ - @Event() loadLazyContent: EventEmitter; - - /** - * Set the focus in the control if `expandable === true`. - */ - @Method() - async setFocus() { - if (this.expandable && this.#buttonRef) { - this.#buttonRef.focus(); - } - } - - #getExpandedValue = (): boolean => - this.expandable ? this.expanded ?? false : true; - - connectedCallback() { - this.el.setAttribute("role", "listitem"); - this.el.setAttribute("part", ACTION_LIST_PARTS_DICTIONARY.GROUP); - this.el.setAttribute("exportparts", ACTION_LIST_GROUP_EXPORT_PARTS); - } - - render() { - const hasContent = !this.lazyLoad; - const expanded = hasContent && this.#getExpandedValue(); - - return ( - - {this.expandable ? ( - - ) : ( - - {this.caption} - - )} - - {hasContent && ( -
    - -
- )} - - ); - } -} diff --git a/src/components/action-list/internal/action-list-group/readme.md b/src/components/action-list/internal/action-list-group/readme.md deleted file mode 100644 index 31dcb2ae..00000000 --- a/src/components/action-list/internal/action-list-group/readme.md +++ /dev/null @@ -1,59 +0,0 @@ -# ch-action-list-group - - - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ------------------------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | -| `caption` | `caption` | This attributes specifies the caption of the control | `string` | `undefined` | -| `disabled` | `disabled` | This attribute lets you specify if the element is disabled. If disabled, it will not fire any user interaction related event (for example, click event). | `boolean` | `false` | -| `downloading` | `downloading` | This attribute lets you specify when items are being lazy loaded in the control. | `boolean` | `false` | -| `expandable` | `expandable` | If the item has a sub-tree, this attribute determines if the subtree is displayed. | `boolean` | `undefined` | -| `expanded` | `expanded` | If the item has a sub-tree, this attribute determines if the subtree is displayed. | `boolean` | `undefined` | -| `lazyLoad` | `lazy-load` | Determine if the items are lazy loaded when opening the first time the control. | `boolean` | `false` | -| `metadata` | `metadata` | This attribute represents additional info for the control that is included when dragging the item. | `string` | `undefined` | -| `parts` | `parts` | Specifies a set of parts to use in every DOM element of the control. | `string` | `undefined` | -| `selected` | `selected` | This attribute lets you specify if the item is selected | `boolean` | `false` | -| `showDownloadingSpinner` | `show-downloading-spinner` | `true` to show the downloading spinner when lazy loading the sub items of the control. | `boolean` | `true` | - - -## Events - -| Event | Description | Type | -| ----------------- | ---------------------------------------------------------------------- | --------------------- | -| `loadLazyContent` | Fired when the lazy control is expanded an its content must be loaded. | `CustomEvent` | - - -## Methods - -### `setFocus() => Promise` - -Set the focus in the control if `expandable === true`. - -#### Returns - -Type: `Promise` - - - - -## Dependencies - -### Used by - - - [ch-action-list-render](../..) - -### Graph -```mermaid -graph TD; - ch-action-list-render --> ch-action-list-group - style ch-action-list-group fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - -*Built with [StencilJS](https://stenciljs.com/)*