diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 91021367..e4a33db7 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -36,6 +36,7 @@ module.exports = { "calendar", "table", "split-button", + "tag", "datepicker", ], ], diff --git a/scripts/build.js b/scripts/build.js index bd13ba52..ca3cf8f4 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,34 +1,46 @@ -import { context, build } from 'esbuild'; -import parseArgs from 'minimist'; -import CleanCSS from 'clean-css'; -import del from 'del'; -import { litCssPlugin } from 'esbuild-plugin-lit-css'; +import del from "del"; +import { context, build } from "esbuild"; +import parseArgs from "minimist"; +import CleanCSS from "clean-css"; +import { litCssPlugin } from "esbuild-plugin-lit-css"; const args = parseArgs(process.argv.slice(2), { boolean: true, }); (async () => { - const { globby } = await import('globby'); - const destinationPath = 'dist'; + const { globby } = await import("globby"); + const destinationPath = "dist"; const isRelease = process.env.RELEASE || false; /* This is for using inside Storybook for demonstration purposes. */ - const cssHoverClassAdder = (content) => content.replace(/.*:hover[^{]*/g, matched => { - // Replace :hover with special class. (There will be additional classes for focus, etc. Should be implemented in here.) - const replacedWithNewClass = matched.replace(/:hover/, '.__ONLY_FOR_STORYBOOK_DEMONSTRATION_HOVER__') - // Concat strings - return replacedWithNewClass.concat(', ', matched); - }); - - const cssCleaner = (content) => { - const { styles, errors, warnings } = new CleanCSS({ level: 0 }).minify(content); + const cssHoverClassAdder = content => + content.replace(/.*:hover[^{]*/g, matched => { + // Replace :hover with special class. (There will be additional classes for focus, etc. Should be implemented in here.) + const replacedWithNewClass = matched.replace( + /:hover/, + ".__ONLY_FOR_STORYBOOK_DEMONSTRATION_HOVER__" + ); + // Concat strings + return replacedWithNewClass.concat(", ", matched); + }); + + const cssCleaner = content => { + const { styles, errors, warnings } = new CleanCSS({ level: 2 }).minify(content); if (errors.length) { - console.error(errors); + console.error({ + errors, + styles: JSON.stringify(styles), + }); } + if (warnings.length) { - console.warn(warnings); + console.warn({ + warnings, + styles: JSON.stringify(styles), + }); } + return styles; }; @@ -43,57 +55,53 @@ const args = parseArgs(process.argv.slice(2), { const cssPluginOptions = { filter: /components\/.*\.css$/, - transform: (content) => cssTransformers.reduce((result, transformer) => transformer(result), content) + transform: content => + cssTransformers.reduce((result, transformer) => transformer(result), content), }; try { const buildOptions = { entryPoints: [ - 'src/baklava.ts', - 'src/baklava-react.ts', - 'src/localization.ts', + "src/baklava.ts", + "src/baklava-react.ts", + "src/localization.ts", ...(await globby([ - 'src/generated/**/*.ts', - 'src/components/**/!(*.(test|d)).ts', - 'src/themes/*.css', - 'src/components/**/*.svg', + "src/generated/**/*.ts", + "src/components/**/!(*.(test|d)).ts", + "src/themes/*.css", + "src/components/**/*.svg", ])), ], loader: { - '.woff': 'file', - '.woff2': 'file', - '.svg': 'file', + ".woff": "file", + ".woff2": "file", + ".svg": "file", }, outdir: destinationPath, - assetNames: 'assets/[name]', + assetNames: "assets/[name]", bundle: true, sourcemap: true, - format: 'esm', - target: ['es2020', 'chrome73', 'edge79', 'firefox63', 'safari12'], + format: "esm", + target: ["es2020", "chrome73", "edge79", "firefox63", "safari12"], splitting: true, metafile: true, minify: true, - external: ['react'], - plugins: [ - litCssPlugin(cssPluginOptions), - ], + external: ["react"], + plugins: [litCssPlugin(cssPluginOptions)], }; - if (args.serve) { - const servedir = 'playground'; + const servedir = "playground"; let ctx = await context({ ...buildOptions, - outdir: `${servedir}/dist` + outdir: `${servedir}/dist`, }); - const { host, port } = await ctx.serve( - { - servedir, - host: 'localhost', - } - ); + const { host, port } = await ctx.serve({ + servedir, + host: "localhost", + }); console.log(`Playground is served on http://${host}:${port}`); @@ -104,12 +112,12 @@ const args = parseArgs(process.argv.slice(2), { if (errors.length > 0) { console.table(errors); - console.error('Build Failed!'); + console.error("Build Failed!"); return; } if (warnings.length > 0) { - console.warn('Warnings:'); + console.warn("Warnings:"); console.table(warnings); } @@ -122,19 +130,19 @@ const args = parseArgs(process.argv.slice(2), { .filter( ({ fileName }) => !/icon\/icons\/.*\.js/.test(fileName) && - (fileName.endsWith('.js') || fileName.endsWith('.css')) + (fileName.endsWith(".js") || fileName.endsWith(".css")) ); analyzeResult.push({ - fileName: 'TOTAL', + fileName: "TOTAL", size: `${(analyzeResult.reduce((acc, { bytes }) => acc + bytes, 0) / 1024).toFixed(2)} KB`, - }) + }); del(`${destinationPath}/components/icon/icons`); - console.table(analyzeResult, ['fileName', 'size']); + console.table(analyzeResult, ["fileName", "size"]); - console.info('Build Done!'); + console.info("Build Done!"); } catch (error) { console.error(error); process.exit(1); diff --git a/src/baklava.ts b/src/baklava.ts index bbaddfb5..54d137c2 100644 --- a/src/baklava.ts +++ b/src/baklava.ts @@ -36,5 +36,6 @@ export { default as BlTableHeaderCell } from "./components/table/table-header-ce export { default as BlTableCell } from "./components/table/table-cell/bl-table-cell"; export { default as BlSplitButton } from "./components/split-button/bl-split-button"; export { default as BlCalendar } from "./components/calendar/bl-calendar"; +export { default as BlTag } from "./components/tag/bl-tag"; export { default as BlDatePicker } from "./components/datepicker/bl-datepicker"; export { getIconPath, setIconPath } from "./utilities/asset-paths"; diff --git a/src/components/tag/bl-tag.css b/src/components/tag/bl-tag.css new file mode 100644 index 00000000..87f0a70c --- /dev/null +++ b/src/components/tag/bl-tag.css @@ -0,0 +1,97 @@ +:host { + display: inline-block; + max-width: 100%; +} + +.tag { + --bg-color: var(--bl-color-neutral-full); + --color: var(--bl-color-neutral-darker); + --font: var(--bl-font-title-4-medium); + --padding-vertical: var(--bl-size-2xs); + --padding-horizontal: var(--bl-size-m); + --margin-icon: var(--bl-size-2xs); + --icon-size: var(--bl-size-m); + --height: var(--bl-size-2xl); + --border-radius: var(--bl-size-m); + --remove-icon-size: var(--bl-size-s); + + display: flex; + gap: var(--margin-icon); + justify-content: center; + align-items: center; + box-sizing: border-box; + width: 100%; + border: 1px solid var(--bl-color-neutral-lighter); + border-radius: var(--border-radius); + padding-block: var(--padding-vertical); + padding-inline: var(--padding-horizontal); + background-color: var(--bg-color); + color: var(--color, white); + font: var(--font); + font-kerning: none; + height: var(--height); +} + +:host([variant="selectable"]) .tag { + cursor: pointer; + user-select: none; +} + +:host([variant="selectable"]) .tag:hover { + background-color: var(--bl-color-neutral-lightest); +} + +:host([variant="selectable"][selected]) .tag { + border-color: var(--bl-color-neutral-darker); + background-color: var(--bl-color-neutral-darker); + + --color: var(--bl-color-neutral-full); +} + + +:host([variant="selectable"][disabled]) .tag { + background-color: var(--bl-color-neutral-lightest); + border-color: var(--bl-color-neutral-lightest); + color: var(--bl-color-neutral-light); + cursor: not-allowed; +} + +:host([variant="removable"]) .remove-button { + --bl-border-radius-m: var(--bl-border-radius-circle); +} + +:host([size="small"]) .tag { + --font: var(--bl-font-title-4-medium); + --height: var(--bl-size-xl); + --icon-size: var(--bl-size-s); + --border-radius: var(--bl-size-xs); + --padding-vertical: 0px; + --padding-horizontal: var(--bl-size-2xs); + --remove-icon-size: var(--bl-size-xs); +} + +:host([size="large"]) .tag { + --font: var(--bl-font-title-3-medium); + --padding-vertical: var(--bl-size-2xs); + --padding-horizontal: var(--bl-size-l); + --height: var(--bl-size-3xl); + --icon-size: var(--bl-size-m); + --border-radius: var(--bl-size-l); +} + +:host([variant="removable"][size="small"]) .tag { + --padding-horizontal: var(--bl-size-2xs) 0px; +} + +:host([variant="removable"]) .tag { + --padding-horizontal: var(--bl-size-m) var(--bl-size-3xs); +} + +:host([variant="removable"][size="large"]) .tag { + --padding-horizontal: var(--bl-size-l) var(--bl-size-2xs); +} + +slot[name="icon"] bl-icon { + font-size: var(--icon-size); + color: var(--color); +} diff --git a/src/components/tag/bl-tag.stories.mdx b/src/components/tag/bl-tag.stories.mdx new file mode 100644 index 00000000..4f0cdec0 --- /dev/null +++ b/src/components/tag/bl-tag.stories.mdx @@ -0,0 +1,122 @@ +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'; +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; + + + +export const Template = (args) => html` + + ${args.content} + +`; + +# Tag + +[ADR](https://github.com/Trendyol/baklava/pull/955) +[Figma](https://www.figma.com/file/RrcLH0mWpuIy4vwuTlDeKN/Baklava-Design-Guide) + +Tags are compact elements that represent an input, attribute, or action. + +## Basic Usage + +Tags can be used to label, categorize, or organize items using keywords. + + + + {Template.bind({})} + + + +## Variants + +Tags come in two variants: selectable and removable. + + + + {html` + Selectable Tag + Removable Tag + `} + + + +## Sizes + +Tags are available in three sizes: small, medium (default), and large. + + + + {html` + Small + Medium + Large + Small Removable + Medium Removable + Large Removable + `} + + + +## With Icons + +Tags can include icons to provide additional visual context. + + + + {html` + Info Tag + Removable Tag Icon + `} + + + +## States + +Tags can be selected or disabled. + + + + {html` + Selected Tag + Disabled Tag + Selected Disabled Tag + Disabled Removable Tag + `} + + + +## Reference + + diff --git a/src/components/tag/bl-tag.ts b/src/components/tag/bl-tag.ts new file mode 100644 index 00000000..d5c4f33c --- /dev/null +++ b/src/components/tag/bl-tag.ts @@ -0,0 +1,106 @@ +import { CSSResultGroup, html, LitElement, nothing, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators.js"; +import { event, EventDispatcher } from "../../utilities/event"; +import "../button/bl-button"; +import "../icon/bl-icon"; +import { BaklavaIcon } from "../icon/icon-list"; +import style from "./bl-tag.css"; + +export type TagSize = "small" | "medium" | "large"; +type TagVariant = "selectable" | "removable"; + +/** + * @tag bl-tag + * @summary Baklava Tag component + */ + +@customElement("bl-tag") +export default class BlTag extends LitElement { + static get styles(): CSSResultGroup { + return [style]; + } + + @query(".remove-button") removeButton!: HTMLButtonElement; + + /** + * Sets the tag size + */ + @property({ type: String, reflect: true }) + size: TagSize = "medium"; + + @property({ type: String, reflect: true }) + variant: TagVariant = "selectable"; + + @property({ type: Boolean, reflect: true }) + selected = false; + + @property({ type: Boolean, reflect: true }) + disabled = false; + + @property({ type: String, reflect: true }) + value: string | null = null; + + @event("bl-tag-click") private _onBlTagClick: EventDispatcher<{ + value: string | null; + selected: boolean; + }>; + + private handleClick = () => { + if (this.variant === "selectable") this.selected = !this.selected; + this._onBlTagClick({ selected: this.selected, value: this.value }); + }; + + /** + * Sets the name of the icon + */ + @property({ type: String }) + icon?: BaklavaIcon; + + render(): TemplateResult { + const removeIconSize = this.size === "large" ? "medium" : "small"; + const icon = this.icon + ? html`` + : nothing; + + const removeButton = + this.variant === "removable" + ? html` + + ` + : nothing; + + const selectableVariant = html``; + + const removableVariant = html`
+ ${icon} + + ${removeButton} +
`; + + return this.variant === "selectable" ? selectableVariant : removableVariant; + } +} + +declare global { + interface HTMLElementTagNameMap { + "bl-tag": BlTag; + } +} diff --git a/src/components/tag/doc/ADR.md b/src/components/tag/doc/ADR.md new file mode 100644 index 00000000..064f152e --- /dev/null +++ b/src/components/tag/doc/ADR.md @@ -0,0 +1,66 @@ +## Figma Design Document + +https://www.figma.com/design/lSvX6Qe0jc8b4CaIK7egXR/Baklava-Component-Library?node-id=21476-4839&node-type=frame&t=PriuJR3qmpVaFIdy-0 + +## Implementation + +General usage example: + +```html +In Progress +``` + +### Usage Examples +Selectable variant usage: +```html +Selectable tag +``` +The removable variant can be set like this: + +```js + +const handleTagClick=(event)=>{ + tags.filter((tag)=>tag.value!==event.value) +} +Removable tag +``` + +The icon can be set like this: + +```html +Default +``` + +The size and disabled attributes can be set like this: + +```html +In Progress +``` + +## API Reference: + +#### Slots + +| Name | Description | Default Content | +|-------------|-----------------| --------------- | +| `icon` slot | Icon of the tag | - | + +#### Attributes + +| Attribute | Description | Default Value | +|----------------------|-----------------------------------------------|---------------| +| size (`string`) | Size of tag(`small`,`medium`,`large`) | medium | +| icon (`bl-icon`) | Name of the icon that will be shown in tag | - | +| variant (`string`) | Variants of the tag(`selectable`,`removable`) | selectable | +| disabled (`boolean`) | Makes tag disabled | false | +| selected (`boolean`) | Makes tag selected | false | +| value (`string`) | Sets tags value | - | + + + +### Events + +| Name | Description | Payload | +|----------------|----------------------------|-----------------------------------| +| `bl-tag-click` | Fires when tag is clicked | `{value:string,selected:boolean}` | +