diff --git a/.eslintrc.json b/.eslintrc.json index 2f40f09..0c7ed59 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -40,6 +40,13 @@ "new-cap": ["error", {"capIsNewExceptionPattern": "Mixin$"}], "brace-style": ["error", "stroustrup", {"allowSingleLine": true}], "indent": "off", + "@typescript-eslint/consistent-type-imports": [ + "error", + { + "fixStyle": "separate-type-imports", + "prefer": "type-imports" + } + ], "@typescript-eslint/indent": [ "error", 2, diff --git a/README.md b/README.md index f4fe2ef..2ae4afb 100644 --- a/README.md +++ b/README.md @@ -20,4 +20,4 @@ Hybrid UI is a cutting-edge web front-end framework that empowers developers to ## About the Package: -Hybrid UI is an open-source package developed by a team of experienced web engineers. It is not directly associated with the gecut company, but aligns with the company's mission of providing high-quality tools to developers. \ No newline at end of file +Hybrid UI is an open-source package developed by a team of experienced web engineers. It is not directly associated with the gecut company, but aligns with the company's mission of providing high-quality tools to developers. diff --git a/demo/assets/placeholder.webp b/demo/assets/placeholder.webp new file mode 100644 index 0000000..1533bd1 Binary files /dev/null and b/demo/assets/placeholder.webp differ diff --git a/demo/button/scripts.ts b/demo/button/scripts.ts index a00a687..01dc55b 100644 --- a/demo/button/scripts.ts +++ b/demo/button/scripts.ts @@ -1,33 +1,43 @@ /* eslint-disable max-len */ -import {ButtonContent, gecutButton} from '@gecut/components'; +import {gecutButton} from '@gecut/components'; import {map} from '@gecut/lit-helper'; import {html, render} from 'lit/html.js'; +import type {ButtonContent} from '@gecut/components'; + const buttonTypes: ButtonContent['type'][] = ['elevated', 'filled', 'filledTonal', 'outlined', 'text']; -const buttonsContents: Record = { +const buttonsContents: Record< + ButtonContent['type'], + {name: string; iconSVG?: string; loaderSVG?: string} & Partial +> = { elevated: { - name: 'Elevated Button', + name: 'Open In New Tag', + href: document.URL, + target: '_blank', }, filled: { name: 'Buy me a coffee', - loader: + loaderSVG: '', - icon: '', + iconSVG: + '', }, filledTonal: { name: 'Download', - loader: + loaderSVG: '', - icon: '', + iconSVG: + '', }, outlined: { name: 'Add to Cart', }, text: { name: 'Upload', - loader: + loaderSVG: '', - icon: '', + iconSVG: + '', }, }; @@ -38,15 +48,15 @@ render( ${map(this, buttonTypes, (type) => gecutButton({ type, - loader: buttonsContents[type].loader + loader: buttonsContents[type].loaderSVG ? { - svg: buttonsContents[type].loader!, + svg: buttonsContents[type].loaderSVG!, } : undefined, - icon: buttonsContents[type].icon + icon: buttonsContents[type].iconSVG ? { // eslint-disable-next-line max-len - svg: buttonsContents[type].icon!, + svg: buttonsContents[type].iconSVG!, } : undefined, onClick: (event) => { @@ -59,6 +69,8 @@ render( }, 5120); }, label: buttonsContents[type].name, + + ...buttonsContents[type], }), )} diff --git a/demo/index.html b/demo/index.html index c0b2031..e885452 100644 --- a/demo/index.html +++ b/demo/index.html @@ -8,9 +8,9 @@ - +
-
+

Hybrid UI

diff --git a/demo/lists/index.html b/demo/lists/index.html new file mode 100644 index 0000000..a8dc160 --- /dev/null +++ b/demo/lists/index.html @@ -0,0 +1,11 @@ + + + + + + Gecut Lists + + + + + diff --git a/demo/lists/preview.png b/demo/lists/preview.png new file mode 100644 index 0000000..265c642 Binary files /dev/null and b/demo/lists/preview.png differ diff --git a/demo/lists/scripts.ts b/demo/lists/scripts.ts new file mode 100644 index 0000000..7f3a43a --- /dev/null +++ b/demo/lists/scripts.ts @@ -0,0 +1,67 @@ +/* eslint-disable max-len */ +import {gecutItem} from '@gecut/components'; +import {map} from 'lit/directives/map.js'; +import {range} from 'lit/directives/range.js'; +import {html, render} from 'lit/html.js'; + +import placeHolderImage from '../assets/placeholder.webp'; + +render( + html` +

+
+ ${map(range(3), (i) => + gecutItem({ + leading: { + type: 'icon', + svg: '', + }, + headline: 'List Item: ' + i, + }), + )} + ${map(range(3), (i) => + gecutItem({ + onClick: console.log, + leading: {type: 'avatar:character', character: 'C'}, + headline: 'List Item: ' + i, + supportingText: + 'Hybrid UI is a cutting-edge web front-end framework that empowers developers to create high-performance, memory-safe, and visually stunning applications. It provides a comprehensive set of tools and features to streamline development and deliver exceptional user experiences.', + }), + )} + ${map(range(3), (i) => + gecutItem({ + headline: 'List Item: ' + i, + leading: { + type: 'icon', + svg: '', + }, + supportingText: + 'Hybrid UI is a cutting-edge web front-end framework that empowers developers to create high-performance, memory-safe, and visually stunning applications. It provides a comprehensive set of tools and features to streamline development and deliver exceptional user experiences.', + supportingTextTwoLine: true, + trailingSupportingText: { + type: 'number', + value: '1000', + maximum: 99, + }, + }), + )} + ${map(range(10), (i) => + gecutItem({ + href: '#' + i, + leading: {type: 'image', placeholder: placeHolderImage, source: 'https://picsum.photos/' + (i + 1) * 100}, + headline: 'List Item: ' + i, + divider: true, + trailing: { + type: 'icon-button', + onClick: console.log, + svg: '', + }, + supportingText: + 'Hybrid UI is a cutting-edge web front-end framework that empowers developers to create high-performance, memory-safe, and visually stunning applications. It provides a comprehensive set of tools and features to streamline development and deliver exceptional user experiences.', + }), + )} +
+
+ `, + document.body, +); diff --git a/demo/main/scripts.ts b/demo/main/scripts.ts index ebd2881..3e7fb8c 100644 --- a/demo/main/scripts.ts +++ b/demo/main/scripts.ts @@ -27,6 +27,11 @@ const demos: Demo[] = [ href: '/top-bar/', align: 'top', }, + { + title: 'Lists', + href: '/lists/', + align: 'top', + }, ]; if (container) diff --git a/demo/postcss.config.js b/demo/postcss.config.js index 2e7af2b..2aa7205 100644 --- a/demo/postcss.config.js +++ b/demo/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/demo/vite.config.mts b/demo/vite.config.mts index 4aebbc8..bb83260 100644 --- a/demo/vite.config.mts +++ b/demo/vite.config.mts @@ -2,7 +2,7 @@ import {defineConfig} from 'vite'; import Unfonts from 'unplugin-fonts/vite'; import tsconfigPaths from 'vite-tsconfig-paths'; -const entrys = ['dialog', 'top-bar', 'button']; +const entrys = ['dialog', 'top-bar', 'button', 'lists']; const DIST_PATH = './dist/'; const pages = entrys.reduce((result, name) => { result[name] = `./${name}/index.html`; @@ -38,7 +38,7 @@ export default defineConfig(() => { families: [ { name: 'Roboto', - styles: 'wght@500', + styles: 'wght@400', defer: true, }, ], diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index cbe4012..36f00bd 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -7,38 +7,38 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -* **components/button:** loader icon ([9626862](https://github.com/gecut/hybrid-ui/commit/962686228324711d5a8e0b6e672e6290bba68184)) by @MM25Zamanian -* **components/dialog/helper:** add content to open method ([37ef94a](https://github.com/gecut/hybrid-ui/commit/37ef94a34a1c99a88636126c9d420a79a943f69d)) by @MM25Zamanian +- **components/button:** loader icon ([9626862](https://github.com/gecut/hybrid-ui/commit/962686228324711d5a8e0b6e672e6290bba68184)) by @MM25Zamanian +- **components/dialog/helper:** add content to open method ([37ef94a](https://github.com/gecut/hybrid-ui/commit/37ef94a34a1c99a88636126c9d420a79a943f69d)) by @MM25Zamanian # [2.1.0-alpha.1](https://github.com/gecut/hybrid-ui/compare/@gecut/components@2.1.0-alpha.0...@gecut/components@2.1.0-alpha.1) (2024-3-31) ### Bug Fixes -* **components/dialog:** content style issue ([a73f96d](https://github.com/gecut/hybrid-ui/commit/a73f96d472fdc72a5c5be5b87cc12e02553728d3)) by @MM25Zamanian +- **components/dialog:** content style issue ([a73f96d](https://github.com/gecut/hybrid-ui/commit/a73f96d472fdc72a5c5be5b87cc12e02553728d3)) by @MM25Zamanian ### Features -* **components/button:** loading ([b19eaba](https://github.com/gecut/hybrid-ui/commit/b19eabad22a07e53a9e69836f839afdce2f2ed22)) by @MM25Zamanian +- **components/button:** loading ([b19eaba](https://github.com/gecut/hybrid-ui/commit/b19eabad22a07e53a9e69836f839afdce2f2ed22)) by @MM25Zamanian # [2.1.0-alpha.0](https://github.com/gecut/hybrid-ui/compare/@gecut/components@2.0.1-alpha.0...@gecut/components@2.1.0-alpha.0) (2024-3-20) ### Features -* **components:** create dialog components ([#26](https://github.com/gecut/hybrid-ui/issues/26)) ([db91c35](https://github.com/gecut/hybrid-ui/commit/db91c352417257d8f516e2104209597eeeb26647)) by @MM25Zamanian +- **components:** create dialog components ([#26](https://github.com/gecut/hybrid-ui/issues/26)) ([db91c35](https://github.com/gecut/hybrid-ui/commit/db91c352417257d8f516e2104209597eeeb26647)) by @MM25Zamanian ## [2.0.1-alpha.0](https://github.com/gecut/hybrid-ui/compare/@gecut/components@2.0.0...@gecut/components@2.0.1-alpha.0) (2024-3-10) ### Bug Fixes -* **components:** button ([0766241](https://github.com/gecut/hybrid-ui/commit/07662418a4d984b39ac4600b7b5bd5a0d592e085)) by @MM25Zamanian -* **components:** button ([85be9b2](https://github.com/gecut/hybrid-ui/commit/85be9b22d16e44480b9c28e9d905de7adcd036c9)) by @MM25Zamanian +- **components:** button ([0766241](https://github.com/gecut/hybrid-ui/commit/07662418a4d984b39ac4600b7b5bd5a0d592e085)) by @MM25Zamanian +- **components:** button ([85be9b2](https://github.com/gecut/hybrid-ui/commit/85be9b22d16e44480b9c28e9d905de7adcd036c9)) by @MM25Zamanian # 2.0.0 (2024-03-06) ### Bug Fixes -* **components:** eslint issues ([d839b84](https://github.com/gecut/hybrid-ui/commit/d839b8487ae815e8bf46bff3e93bfa7f4b4d71c8)) by @MM25Zamanian +- **components:** eslint issues ([d839b84](https://github.com/gecut/hybrid-ui/commit/d839b8487ae815e8bf46bff3e93bfa7f4b4d71c8)) by @MM25Zamanian ### Features -* **components:** new packages ([07f7233](https://github.com/gecut/hybrid-ui/commit/07f72331da17e4a01299477d0f2bed923e4ca1bb)) by @MM25Zamanian +- **components:** new packages ([07f7233](https://github.com/gecut/hybrid-ui/commit/07f72331da17e4a01299477d0f2bed923e4ca1bb)) by @MM25Zamanian diff --git a/packages/components/src/button/button.ts b/packages/components/src/button/button.ts index df5f959..d67714e 100644 --- a/packages/components/src/button/button.ts +++ b/packages/components/src/button/button.ts @@ -1,15 +1,19 @@ /* eslint-disable max-len */ import {GecutDirective} from '@gecut/lit-helper/directives/directive.js'; import {directive, type PartInfo} from 'lit/directive.js'; +import {classMap} from 'lit/directives/class-map.js'; import {ifDefined} from 'lit/directives/if-defined.js'; import {when} from 'lit/directives/when.js'; -import {html, noChange} from 'lit/html.js'; +import {html, noChange, nothing} from 'lit/html.js'; +import {literal, html as staticHtml} from 'lit/static-html.js'; import {icon} from '../icon/icon.js'; import type {IconContent} from '../icon/icon.js'; +import type {ClassInfo} from 'lit/directives/class-map.js'; +import type {StaticValue} from 'lit/static-html.js'; -export type ButtonContent = { +export interface ButtonContent { /** * @default elevated */ @@ -21,102 +25,127 @@ export type ButtonContent = { icon?: IconContent; trailingIcon?: IconContent; + href?: string; + target?: '_blank' | '_parent' | '_self' | '_top'; + + onClick?: (event: MouseEvent) => void; + onDblClick?: (event: MouseEvent) => void; + label?: string; -} & ( - | {href?: string; target?: '_blank' | '_parent' | '_self' | '_top'} - | { - onClick: (event: MouseEvent) => void; - } - | { - onDblClick: (event: MouseEvent) => void; - } - | Record -); +} export class GecutButtonDirective extends GecutDirective { constructor(partInfo: PartInfo) { super(partInfo, 'gecut-button'); } - private static baseStyleClass = - 'relative group rounded-full h-10 px-6 cursor-pointer focus-ring disabled:cursor-default disabled:pointer-events-none [&[loading]]:cursor-default [&[loading]]:pointer-events-none'; - private static uiTypeStylesClasses = { - elevated: - 'text-primary bg-surfaceContainerLow elevation-1 hover:elevation-2 hover:stateHover-primary focus:elevation-1 active:stateActive-primary disabled:opacity-60', - filled: - 'text-onPrimary bg-primary elevation-0 hover:elevation-2 hover:stateHover-onPrimary focus:elevation-1 active:stateActive-onPrimary disabled:opacity-40', - filledTonal: - 'text-onSecondaryContainer bg-secondaryContainer elevation-0 hover:elevation-2 hover:stateHover-onSecondaryContainer focus:elevation-1 active:stateActive-onSecondaryContainer disabled:opacity-60', - outlined: - 'text-primary bg-transparent border border-outline hover:stateHover-primary active:stateActive-primary disabled:opacity-60', - text: 'text-primary bg-transparent hover:stateHover-primary active:stateActive-primary disabled:opacity-60', - }; + protected content?: ButtonContent; + protected type: 'link' | 'button' = 'button'; + + protected $rootClassName = + 'relative group rounded-full h-10 px-6 cursor-pointer focus-ring disabled:cursor-default disabled:pointer-events-none'; + protected $loaderClassName = + 'absolute inset-0 flex justify-center items-center transition-opacity duration-300 opacity-0 group-[[loading]]:opacity-100 [&[loading]]:cursor-default [&[loading]]:pointer-events-none'; + protected $bodyClassName = + 'flex items-center justify-center h-full w-full gap-2 transition-opacity duration-300 opacity-100 group-[[loading]]:opacity-0'; render(content?: ButtonContent): unknown { this.log.methodArgs?.('render', content); if (content === undefined) return noChange; - if (GecutButtonDirective.isLink(content)) { - return GecutButtonDirective.renderLink(content); - } + this.content = content; - return GecutButtonDirective.renderButton(content); - } + if (this.content.href) this.type = 'link'; - protected static isLink(content: ButtonContent) { - return 'href' in content; + return this.renderButton(); } - protected static renderButton(content: ButtonContent): unknown { - const onClick = 'onClick' in content ? (event: MouseEvent) => content.onClick(event) : undefined; - const onDblClick = 'onDblClick' in content ? (event: MouseEvent) => content.onDblClick(event) : undefined; + protected renderButton() { + if (!this.content) return nothing; - return html` - + this.log.method?.('renderItem'); + + let tag: StaticValue; + + switch (this.type) { + case 'link': + tag = literal`a`; + break; + case 'button': + tag = literal`button`; + break; + } + + return staticHtml` + <${tag} + class=${classMap({[this.$rootClassName]: true, ...this.getRenderClasses()})} + role="button" + href=${ifDefined(this.content.href)} + target=${ifDefined(this.content.target)} + tabindex="${this.content.disabled ? -1 : 0}" + ?disabled=${this.content.disabled ?? false} + ?loading=${this.content.loading ?? false} + @click=${this.content.onClick} + @dblclick=${this.content.onDblClick} + >${this.renderLoader()}${this.renderBody()} `; } - protected static renderLink(content: ButtonContent): unknown { - return html` - ${this.renderContent(content)} - `; - } - protected static renderContent(content: ButtonContent): unknown { + protected renderBody(): unknown { + if (!this.content) return nothing; + + this.log.method?.('renderContent'); + return html` -
+
${icon( - content.loader ?? { + this.content.loader ?? { svg: '', }, )}
-
- ${when(content.icon?.svg, () => icon({svg: content.icon?.svg as string}))} +
+ ${when(this.content.icon?.svg, () => icon({svg: this.content?.icon?.svg as string}))} + + ${this.content.label} + + ${when(this.content.trailingIcon?.svg, () => icon({svg: this.content?.trailingIcon?.svg as string}))} +
+ `; + } + protected renderLoader(): unknown { + if (!this.content) return nothing; - ${content.label} + this.log.method?.('renderLoader'); - ${when(content.trailingIcon?.svg, () => icon({svg: content.trailingIcon?.svg as string}))} + return html` +
+ ${icon( + this.content.loader ?? { + svg: '', + }, + )}
`; } + + protected override getRenderClasses(): ClassInfo { + return { + ...super.getRenderClasses(), + + 'text-primary bg-surfaceContainerLow elevation-1 hover:elevation-2 hover:stateHover-primary focus:elevation-1 active:stateActive-primary disabled:opacity-60': + this.content?.type === 'elevated', + 'text-onPrimary bg-primary elevation-0 hover:elevation-2 hover:stateHover-onPrimary focus:elevation-1 active:stateActive-onPrimary disabled:opacity-40': + this.content?.type === 'filled', + 'text-onSecondaryContainer bg-secondaryContainer elevation-0 hover:elevation-2 hover:stateHover-onSecondaryContainer focus:elevation-1 active:stateActive-onSecondaryContainer disabled:opacity-60': + this.content?.type === 'filledTonal', + 'text-primary bg-transparent border border-outline hover:stateHover-primary active:stateActive-primary disabled:opacity-60': + this.content?.type === 'outlined', + 'text-primary bg-transparent hover:stateHover-primary active:stateActive-primary disabled:opacity-60': + this.content?.type === 'text', + }; + } } export const gecutButton = directive(GecutButtonDirective); diff --git a/packages/components/src/components.ts b/packages/components/src/components.ts index dc63027..a4e6d36 100644 --- a/packages/components/src/components.ts +++ b/packages/components/src/components.ts @@ -4,6 +4,8 @@ export * from './dialog/helper.js'; export * from './divider/divider.js'; export * from './icon/icon.js'; export * from './icon-button/icon-button.js'; +export * from './list/list.js'; +export * from './list/item.js'; export * from './navigation-bar/navigation-bar.js'; export * from './navigation-drawer/navigation-drawer.js'; export * from './top-bar/center-top-bar.js'; diff --git a/packages/components/src/dialog/_type.ts b/packages/components/src/dialog/_type.ts index 52295d7..67196b4 100644 --- a/packages/components/src/dialog/_type.ts +++ b/packages/components/src/dialog/_type.ts @@ -1,8 +1,8 @@ -import {ContextSignal} from '@gecut/signal'; import type {ButtonContent} from '../button/button.js'; import type {IconContent} from '../icon/icon.js'; import type {TopBarContent} from '../top-bar/_type.js'; +import type {ContextSignal} from '@gecut/signal'; import type {RenderResult} from '@gecut/types'; type CssSize = `${number}${string}`; diff --git a/packages/components/src/divider/divider.ts b/packages/components/src/divider/divider.ts index 1f7818b..9c1fc9d 100644 --- a/packages/components/src/divider/divider.ts +++ b/packages/components/src/divider/divider.ts @@ -1,22 +1,39 @@ -import { html } from 'lit/html.js'; +import {classMap} from 'lit/directives/class-map.js'; +import {styleMap} from 'lit/directives/style-map.js'; +import {html} from 'lit/html.js'; export interface DividerContent { - inset?: boolean; - insetStart?: boolean; - insetEnd?: boolean; + inset?: boolean | string; + insetStart?: boolean | string; + insetEnd?: boolean | string; - gapTop?: boolean; - gapBottom?: boolean; + gap?: boolean | string; + gapTop?: boolean | string; + gapBottom?: boolean | string; } export const divider = (content: DividerContent) => { - const inset = content.inset ? 'mx-4' : ''; - const insetStart = content.insetStart ? 'ms-4' : ''; - const insetEnd = content.insetEnd ? 'me-4' : ''; - const gapTop = content.gapTop ? 'mt-2' : ''; - const gapBottom = content.gapBottom ? 'mb-2' : ''; + if (content.inset) { + content.insetStart ??= content.inset; + content.insetEnd ??= content.inset; + } + if (content.gap) { + content.gapTop ??= content.gap; + content.gapBottom ??= content.gap; + } return html`
`; }; diff --git a/packages/components/src/icon-button/icon-button.ts b/packages/components/src/icon-button/icon-button.ts index e3d3dd6..0de1e96 100644 --- a/packages/components/src/icon-button/icon-button.ts +++ b/packages/components/src/icon-button/icon-button.ts @@ -1,6 +1,6 @@ -import { html } from 'lit/html.js'; +import {html} from 'lit/html.js'; -import { icon, type IconContent } from '../icon/icon.js'; +import {icon, type IconContent} from '../icon/icon.js'; export interface IconButtonContent extends IconContent { disabled?: boolean; @@ -8,13 +8,12 @@ export interface IconButtonContent extends IconContent { onClick(event: MouseEvent): void; } -export const iconButton = (content: IconButtonContent) => - html` - - `; + > + ${icon(content)} + +`; diff --git a/packages/components/src/icon/icon.ts b/packages/components/src/icon/icon.ts index 81368cd..577665f 100644 --- a/packages/components/src/icon/icon.ts +++ b/packages/components/src/icon/icon.ts @@ -1,10 +1,10 @@ -import { GecutAsyncDirective } from '@gecut/lit-helper/directives/async-directive.js'; -import { directive } from 'lit/directive.js'; -import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; -import { html, nothing } from 'lit/html.js'; +import {GecutAsyncDirective} from '@gecut/lit-helper/directives/async-directive.js'; +import {directive} from 'lit/directive.js'; +import {unsafeSVG} from 'lit/directives/unsafe-svg.js'; +import {html, nothing} from 'lit/html.js'; -import type { MaybePromise } from '@gecut/types'; -import type { PartInfo } from 'lit/directive.js'; +import type {MaybePromise} from '@gecut/types'; +import type {PartInfo} from 'lit/directive.js'; export type SvgContent = MaybePromise; @@ -28,15 +28,12 @@ export class IconDirective extends GecutAsyncDirective { return this._renderSvg(); } else { - return this._renderSvg( - content.svg, - content.flipIconInRtl ? 'rtl:-scale-x-100' : '' - ); + return this._renderSvg(content.svg, content.flipIconInRtl ? 'rtl:-scale-x-100' : ''); } } protected _renderSvg(svg?: string, customClass = ''): unknown { - this.log.methodArgs?.('_renderSvg', { svg, customClass }); + this.log.methodArgs?.('_renderSvg', {svg, customClass}); return html`
void; + onDblClick?: (event: MouseEvent) => void; + + disabled?: boolean; + divider?: boolean; + + leading?: ItemSlutContent; + trailing?: ItemSlutContent; +} + +export class GecutItemDirective extends GecutDirective { + constructor(partInfo: PartInfo) { + super(partInfo, 'gecut-button'); + } + + protected content?: ItemContent; + protected type: 'link' | 'text' | 'button' = 'text'; + + protected $rootClassName = + 'relative flex flex-col list-none group w-full bg-surface text-onSurface overflow-hidden [&[interactive]]:focus-ring-inner rounded-lg disabled:cursor-default disabled:pointer-events-none select-none [&[interactive]]:hover:stateHover-onSurface [&[interactive]]:active:stateActive-onSurface'; + + render(content?: ItemContent): unknown { + this.log.methodArgs?.('render', content); + + if (content === undefined) return noChange; + + this.content = content; + + if (content.onClick || content.onDblClick) this.type = 'button'; + if (content.href) this.type = 'link'; + + return this.renderItem(); + } + + protected renderItem() { + if (!this.content) return nothing; + + this.log.method?.('renderItem'); + + let tag: StaticValue; + + switch (this.type) { + case 'link': + tag = literal`a`; + break; + case 'button': + tag = literal`button`; + break; + case 'text': + tag = literal`label`; + break; + } + + const isInteractive = this.type !== 'text'; + + return staticHtml` + <${tag} + class=${classMap({[this.$rootClassName]: true, ...this.getRenderClasses()})} + tabindex="${this.content.disabled || !isInteractive ? -1 : 0}" + role="listitem" + href=${ifDefined(this.content.href)} + target=${ifDefined(this.content.target)} + ?disabled=${this.content.disabled} + ?sttl=${this.content.supportingTextTwoLine} + ?interactive=${isInteractive} + ?multiline=${this.content.headline && this.content.supportingText} + ?divider=${this.content.divider} + @click=${this.content.onClick} + @dblclick=${this.content.onDblClick} + >${this.renderBody()} + `; + } + protected renderBody() { + if (!this.content) return nothing; + + return html` +
+
+ ${this.renderSlot('leading')} +
+ +
+

${this.content.headline}

+

+ ${this.content.supportingText} +

+
+ +
+ ${when( + this.content.trailingSupportingText, + () => html` +

+ ${this.renderItemTrailingSupportingText()} +

+ `, + )} + ${this.renderSlot('trailing')} +
+
+ + ${when(this.content.divider, () => + divider({ + insetStart: !!this.content?.leading, + insetEnd: this.content?.trailing ? '1.5rem' : undefined, + }), + )} + `; + } + protected renderSlot(slotName: 'trailing' | 'leading'): RenderResult { + const content = this.content?.[slotName]; + + if (!this.content || !content) return nothing; + + switch (content.type) { + case 'avatar:character': + return html` +
+ ${content.character} +
+ `; + case 'avatar:image': + return nothing; + case 'image': { + const image = new Image(); + const lazyLoadImage = new Image(); + + lazyLoadImage.addEventListener( + 'load', + () => { + image.src = lazyLoadImage.src; + + lazyLoadImage.remove(); + }, + {once: true}, + ); + + image.src = content.placeholder; + image.className = 'size-14 rounded elevation-1'; + image.alt = this.content.headline.toString() ?? ''; + + lazyLoadImage.src = content.source; + + return html`${image}`; + } + case 'icon': + return icon(content); + case 'icon-button': + return iconButton(content); + case 'template': + return content.template; + } + } + protected renderItemTrailingSupportingText(): RenderResult { + if (!this.content || !this.content.trailingSupportingText) return nothing; + + switch (this.content.trailingSupportingText.type) { + case 'string': + return this.content.trailingSupportingText.value; + case 'number': { + if (!numberUtils.is(this.content.trailingSupportingText.value)) + return this.content.trailingSupportingText.value; + + const valueAsNumber = Number(this.content.trailingSupportingText.value); + const value = Math.min(valueAsNumber, this.content.trailingSupportingText.maximum); + const valuePrefix = valueAsNumber > this.content.trailingSupportingText.maximum ? '+' : ''; + + return value.toLocaleString() + valuePrefix; + } + } + } +} + +export const gecutItem = directive(GecutItemDirective); diff --git a/packages/components/src/list/list.ts b/packages/components/src/list/list.ts new file mode 100644 index 0000000..a185a25 --- /dev/null +++ b/packages/components/src/list/list.ts @@ -0,0 +1,29 @@ +/* eslint-disable max-len */ +import {GecutDirective} from '@gecut/lit-helper/directives/directive.js'; +import {directive} from 'lit/directive.js'; +import {html, noChange} from 'lit/html.js'; + +import type {ItemContent} from './item'; +import type {PartInfo} from 'lit/directive.js'; + +export interface ListContent { + scrollable?: boolean; + box?: 'elevated' | 'filled' | 'outlined'; + items: ItemContent[]; +} + +export class GecutListDirective extends GecutDirective { + constructor(partInfo: PartInfo) { + super(partInfo, 'gecut-button'); + } + + render(content?: ListContent): unknown { + this.log.methodArgs?.('render', content); + + if (content === undefined) return noChange; + + return html``; + } +} + +export const gecutList = directive(GecutListDirective); diff --git a/packages/components/src/navigation-drawer/navigation-drawer.ts b/packages/components/src/navigation-drawer/navigation-drawer.ts index 8b39f29..6e301d8 100644 --- a/packages/components/src/navigation-drawer/navigation-drawer.ts +++ b/packages/components/src/navigation-drawer/navigation-drawer.ts @@ -1,16 +1,16 @@ /* eslint-disable max-len */ -import { GecutDirective } from '@gecut/lit-helper/directives/directive.js'; -import { mapObject } from '@gecut/lit-helper/utilities/map-object.js'; -import { map } from '@gecut/lit-helper/utilities/map.js'; -import { directive, type PartInfo } from 'lit/directive.js'; -import { ifDefined } from 'lit/directives/if-defined.js'; -import { when } from 'lit/directives/when.js'; -import { html, noChange } from 'lit/html.js'; +import {GecutDirective} from '@gecut/lit-helper/directives/directive.js'; +import {mapObject} from '@gecut/lit-helper/utilities/map-object.js'; +import {map} from '@gecut/lit-helper/utilities/map.js'; +import {directive, type PartInfo} from 'lit/directive.js'; +import {ifDefined} from 'lit/directives/if-defined.js'; +import {when} from 'lit/directives/when.js'; +import {html, noChange} from 'lit/html.js'; -import { divider } from '../divider/divider.js'; -import { icon } from '../icon/icon.js'; +import {divider} from '../divider/divider.js'; +import {icon} from '../icon/icon.js'; -import type { NavigationItemContent } from '../navigation-bar/navigation-bar.js'; +import type {NavigationItemContent} from '../navigation-bar/navigation-bar.js'; export interface NavigationDrawerGroupContent { title?: string; @@ -41,9 +41,7 @@ export class GecutNavigationDrawerDirective extends GecutDirective { id="navigationDrawer" class="absolute bottom-0 start-0 top-0 z-modal w-[22.5rem] max-w-[88vw] translate-x-[-105%] rtl:translate-x-[105%] transform-gpu overflow-clip rounded-e-3xl md:rounded-3xl md:my-3 bg-surfaceContainerHigh transition-transform duration-300 ease-in will-change-transform elevation-1 [&.opened]:translate-x-0 [&.opened]:ease-out [&.opened]:shadow-[0_0_100vw_100vw_rgba(0,0,0,0.3)] md:[&.opened]:-translate-x-2 md:[&.opened]:elevation-3 border-0 border-surfaceContainerHighest dark:md:border-2 md:my-5 ${openAttr}" > -