diff --git a/.storybook/custom-icon-pack.ts b/.storybook/custom-icon-pack.ts new file mode 100644 index 000000000..f76d5206b --- /dev/null +++ b/.storybook/custom-icon-pack.ts @@ -0,0 +1,5 @@ +const CUSTOM_ICON_PACK: Record = { + apple: `` +}; + +export default CUSTOM_ICON_PACK; diff --git a/.storybook/preview.ts b/.storybook/preview.ts index cc4df8f09..0629fdb9b 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -3,6 +3,7 @@ import SystemIconPack from "@ollion/flow-system-icon/dist/types/icon-pack"; import ProductIconPack from "@ollion/flow-product-icon/dist/types/icon-pack"; import GcpIconPack from "@ollion/flow-gcp-icon/dist/types/icon-pack"; import AwsIconPack from "@ollion/flow-aws-icon/dist/types/icon-pack"; +import CUSTOM_ICON_PACK from "./custom-icon-pack"; import { ConfigUtil } from "@ollion/flow-core-config"; import { changeRoute } from "./utils"; @@ -99,6 +100,8 @@ export const decorators = [ ...AwsIconPack } }); + + ConfigUtil.setConfig({ iconPack: { ...ConfigUtil.getConfig().iconPack, ...CUSTOM_ICON_PACK } }); return html`
= { + apple: `` +}; + +export default CUSTOM_ICON_PACK; +``` + +- **Step 2**: Import the icon pack in your main.ts or whichever is the startup file of your app: + +```typescript +import CUSTOM_ICON_PACK from "./custom-icon-pack"; +``` + +- **Step 3**: Register the icon pack in your app: + +```typescript +import { ConfigUtil } from "@ollion/flow-core-config"; +ConfigUtil.setConfig({ iconPack: { ...ConfigUtil.getConfig().iconPack, ...CUSTOM_ICON_PACK } }); +``` + +- **Step 4**: Use the icon in your HTML: + +```html + +``` + +- **Final output** + +Screenshot 2024-02-19 at 11 57 27 AM + +- **Note**: We can use url of image as well as an icon like below + +```html + +``` diff --git a/docs/create-icon-picker-categories.md b/docs/create-icon-picker-categories.md new file mode 100644 index 000000000..897fd543d --- /dev/null +++ b/docs/create-icon-picker-categories.md @@ -0,0 +1,48 @@ +# Creating Icon Picker Categories + +- **Prerequisite**: Ensure that `@ollion/flow-core` is installed in your app. If not, you can visit [here](https://github.com/ollionorg/flow-core?tab=readme-ov-file#existing-project) for installation instructions. + +- **Step 1**: Create a TypeScript file (e.g., `icon-picker-categories.ts`) which exports an object with catgeories and icons. + +```typescript +import { FIconPickerCategories } from "@ollion/flow-core"; +const categories: FIconPickerCategories = []; + +categories.push({ + name: "Apple", + /** + * We can provide url as well like this + * categoryIcon : new URL("https://cdn3.iconfinder.com/data/icons/logos-brands-3/24/logo_brand_brands_logos_google-1024.png"); + */ + categoryIcon: ``, + icons: [ + { + name: "apple-music", + /** + * We can provide url as well like this + * source : new URL("https://cdn3.iconfinder.com/data/icons/logos-brands-3/24/logo_brand_brands_logos_google-1024.png"); + */ + source: ``, + keywords: "apple music" + } + ] +}); + +export default categories; +``` + +- **Step 2**: Import the categories in your component file where you want to use it. + +```typescript +import categories from "./icon-picker-categories"; +``` + +- **Step 3** : Use above catgeories in your app. E.g. in you vue app you can use like below + +```html + +``` + +- **Final Output** : + +Screenshot 2024-02-19 at 12 07 07 PM diff --git a/package.json b/package.json index aa0ee1f62..c800e6a03 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,9 @@ ], "devDependencies": { "@changesets/cli": "^2.25.0", + "@faker-js/faker": "^8.3.1", "@ollion/custom-elements-manifest-to-types": "workspace:*", "@ollion/prettier-config": "^2.1.0", - "@faker-js/faker": "^8.3.1", "@storybook/addon-actions": "^7.5.3", "@storybook/addon-essentials": "^7.5.3", "@storybook/addon-links": "^7.5.3", @@ -47,6 +47,7 @@ "@storybook/blocks": "^7.5.3", "@storybook/web-components": "^7.5.3", "@storybook/web-components-vite": "^7.5.3", + "@types/d3": "7.4.3", "@types/eslint": "^8.4.3", "@types/jest": "29.5.5", "@types/prettier": "^3.0.0", @@ -66,13 +67,13 @@ "sass": "^1.52.3", "storybook": "^7.5.3", "typescript": "^5.2.2", - "vite": "^4.4.11", - "@types/d3": "7.4.3" + "vite": "^4.4.11" }, "dependencies": { "@ollion/flow-aws-icon": "latest", "@ollion/flow-code-editor": "workspace:*", "@ollion/flow-core": "workspace:*", + "@ollion/flow-dashboard": "workspace:*", "@ollion/flow-form-builder": "workspace:*", "@ollion/flow-gcp-icon": "latest", "@ollion/flow-lineage": "workspace:*", @@ -81,10 +82,10 @@ "@ollion/flow-product-icon": "1.14.0", "@ollion/flow-system-icon": "latest", "@ollion/flow-table": "workspace:*", - "@ollion/flow-dashboard": "workspace:*", + "d3": "^7.6.1", "jspdf": "^2.5.1", "lit": "^3.1.0", - "d3": "^7.6.1" + "simple-icons": "^11.4.0" }, "loki": { "configurations": { diff --git a/packages/flow-core/package.json b/packages/flow-core/package.json index 20a30cc53..d6f327b94 100644 --- a/packages/flow-core/package.json +++ b/packages/flow-core/package.json @@ -30,6 +30,7 @@ "axios": "^0.27.2", "emoji-mart": "^5.5.2", "flatpickr": "^4.6.13", + "fuse.js": "^7.0.0", "lit": "^3.1.0", "lodash-es": "^4.17.21", "mark.js": "^8.11.1", diff --git a/packages/flow-core/src/components/f-div/f-div-global.scss b/packages/flow-core/src/components/f-div/f-div-global.scss index 3e7d5b7af..4666a5196 100644 --- a/packages/flow-core/src/components/f-div/f-div-global.scss +++ b/packages/flow-core/src/components/f-div/f-div-global.scss @@ -204,6 +204,36 @@ f-div { max-height: 48px; } } + + // if selected state is notch-top, creating pseudo element to create notch + &[selected="notch-top"] { + &::after { + position: absolute; + top: 0; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + left: 4px; + right: 4px; + max-width: 48px; + border-top: 4px solid var(--color-success-default); + content: ""; + } + } + // if selected state is notch-bottom, creating pseudo element to create notch + &[selected="notch-bottom"] { + position: relative; + &::after { + position: absolute; + bottom: 0px; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + right: 4px; + left: 4px; + max-width: 48px; + border-bottom: 4px solid var(--color-success-default); + content: ""; + } + } } } diff --git a/packages/flow-core/src/components/f-div/f-div.ts b/packages/flow-core/src/components/f-div/f-div.ts index e14921433..7156cea75 100644 --- a/packages/flow-core/src/components/f-div/f-div.ts +++ b/packages/flow-core/src/components/f-div/f-div.ts @@ -235,7 +235,14 @@ export class FDiv extends FRoot { * @attribute Sets the f-div to a selected state. Select between border, background, or notch based on your use case. */ @property({ reflect: true, type: String }) - selected?: "none" | "background" | "border" | "notch-right" | "notch-left" = "none"; + selected?: + | "none" + | "background" + | "border" + | "notch-right" + | "notch-left" + | "notch-top" + | "notch-bottom" = "none"; /** * @attribute Sticky property defines a f-div’s position based on the scroll position of the container diff --git a/packages/flow-core/src/components/f-icon-button/f-icon-button.ts b/packages/flow-core/src/components/f-icon-button/f-icon-button.ts index a3d58d7ed..1616bfa81 100644 --- a/packages/flow-core/src/components/f-icon-button/f-icon-button.ts +++ b/packages/flow-core/src/components/f-icon-button/f-icon-button.ts @@ -6,7 +6,7 @@ import { FRoot } from "../../mixins/components/f-root/f-root"; import { classMap } from "lit-html/directives/class-map.js"; import { unsafeSVG } from "lit-html/directives/unsafe-svg.js"; import loader from "../../mixins/svg/loader"; -import { FIcon } from "../f-icon/f-icon"; +import { FIcon, FIconSource } from "../f-icon/f-icon"; import { FCounter } from "../f-counter/f-counter"; import { validateHTMLColorName } from "validate-color"; import { validateHTMLColor } from "validate-color"; @@ -55,18 +55,18 @@ export class FIconButton extends FRoot { /** * @attribute Icon property defines what icon will be displayed on the icon. It can take the icon name from a library , any inline SVG or any URL for the image. */ - @property({ type: String }) - icon!: string; + @property({ type: [String, Object], reflect: true }) + icon!: FIconSource; /** * @attribute Variants are various representations of an icon button. For example an icon button can be round, curved or block. */ - @property({ type: String }) + @property({ type: String, reflect: true }) variant?: FIconButtonVariant = "round"; /** * @attribute Type of f-icon-button */ - @property({ type: String }) + @property({ type: String, reflect: true }) category?: FIconButtonType = "fill"; /** @@ -78,7 +78,7 @@ export class FIconButton extends FRoot { /** * @attribute Size of f-icon-button */ - @property({ type: String }) + @property({ type: String, reflect: true }) state?: FIconButtonState = "primary"; /** diff --git a/packages/flow-core/src/components/f-icon-picker/_f-icon-picker-variables.scss b/packages/flow-core/src/components/f-icon-picker/_f-icon-picker-variables.scss new file mode 100644 index 000000000..c32551e26 --- /dev/null +++ b/packages/flow-core/src/components/f-icon-picker/_f-icon-picker-variables.scss @@ -0,0 +1,30 @@ +$sizes: ( + "medium": 36px, + "small": 28px +); + +$state-colors: ( + "primary": var(--color-primary-default), + "success": var(--color-success-default), + "warning": var(--color-warning-default), + "danger": var(--color-danger-default) +); + +$variants: ( + "curved": 4px, + "round": 22px, + "block": 0px +); + +$categories: ( + "fill": ( + "background": var(--color-input-default), + "hover": var(--color-input-default-hover), + "border": 1px solid var(--color-input-default) + ), + "outline": ( + "background": transparent, + "hover": var(--color-surface-default-hover), + "border": 1px solid var(--color-border-default) + ) +); diff --git a/packages/flow-core/src/components/f-icon-picker/f-icon-picker-global.scss b/packages/flow-core/src/components/f-icon-picker/f-icon-picker-global.scss new file mode 100644 index 000000000..c027ce7f5 --- /dev/null +++ b/packages/flow-core/src/components/f-icon-picker/f-icon-picker-global.scss @@ -0,0 +1,18 @@ +@import "./../../mixins/scss/mixins"; +@import "./f-icon-picker-variables"; + +f-icon-picker { + display: flex; + flex: 1 0 auto; + &[disabled] { + @include disabled(); + } + &[state="default"] { + @include input-color("default"); + } + @each $state, $color in $state-colors { + &[state="#{$state}"] { + @include input-color($state); + } + } +} diff --git a/packages/flow-core/src/components/f-icon-picker/f-icon-picker.scss b/packages/flow-core/src/components/f-icon-picker/f-icon-picker.scss new file mode 100644 index 000000000..dbc26ae34 --- /dev/null +++ b/packages/flow-core/src/components/f-icon-picker/f-icon-picker.scss @@ -0,0 +1,99 @@ +@use "sass:map"; + +// common mixins imported from this file +@import "./../../mixins/scss/mixins"; +@import "./f-icon-picker-variables"; +/** +START : scss maps to hold repsective attribute values +**/ + +:host { + .f-icon-picker { + @include base(); + cursor: pointer; + align-items: center; + display: flex; + justify-content: space-between; + gap: 8px; + max-width: 58px; + > f-div:not([placeholder]) { + font-style: normal; + font-weight: 325; + line-height: 18px; + color: var(--color-text-subtle); + width: fit-content !important; + &[size="small"] { + font-size: 15px; + } + &[size="medium"] { + font-size: 21px; + } + } + padding: 0px 8px; + + @each $variant, $value in $variants { + &[variant="#{$variant}"] { + border-radius: $value; + } + } + @each $size, $value in $sizes { + &[size="#{$size}"] { + height: $value; + } + } + @each $state, $color in $state-colors { + &[state="#{$state}"] { + @each $category, $value in $categories { + &[category="#{$category}"] { + background-color: map.get($value, "background"); + border: 1px solid $color; + &:hover { + background-color: map.get($value, "hover"); + } + } + &[category="transparent"][variant="block"] { + background-color: transparent; + border-top: 0px; + border-bottom: 1px solid $color; + border-left: 0px; + border-right: 0px; + &:hover { + background-color: var(--color-surface-default-hover); + } + } + } + } + } + + &[state="default"] { + @each $category, $value in $categories { + &[category="#{$category}"] { + background-color: map.get($value, "background"); + border: map.get($value, "border"); + &:hover { + background-color: map.get($value, "hover"); + } + } + &[category="transparent"][variant="block"] { + background-color: transparent; + border-top: 0px; + border-bottom: 1px solid var(--color-border-default); + border-left: 0px; + border-right: 0px; + &:hover { + background-color: var(--color-surface-default-hover); + } + } + } + &:focus { + outline: none; + border: 1px solid var(--color-primary-default); + } + } + } + div.f-icon-picker[disabled] { + @include disabled(); + pointer-events: none; + opacity: 1; + } +} diff --git a/packages/flow-core/src/components/f-icon-picker/f-icon-picker.ts b/packages/flow-core/src/components/f-icon-picker/f-icon-picker.ts new file mode 100644 index 000000000..2763e3540 --- /dev/null +++ b/packages/flow-core/src/components/f-icon-picker/f-icon-picker.ts @@ -0,0 +1,475 @@ +import { html, nothing, unsafeCSS } from "lit"; +import { customElement, property, query, queryAssignedElements, state } from "lit/decorators.js"; +import { FRoot } from "./../../mixins/components/f-root/f-root"; +import globalStyle from "./f-icon-picker-global.scss?inline"; +import { injectCss } from "@ollion/flow-core-config"; +import { FDiv } from "../f-div/f-div"; +import { FPopover } from "../f-popover/f-popover"; +import eleStyle from "./f-icon-picker.scss?inline"; +import { FText } from "../f-text/f-text"; +import { FIcon, FIconCustomSource } from "../f-icon/f-icon"; +import { FIconButton } from "../f-icon-button/f-icon-button"; +import { FSearch } from "../f-search/f-search"; +import Fuse from "fuse.js"; + +injectCss("f-icon-picker", globalStyle); + +export type FIconPickerState = "primary" | "default" | "success" | "warning" | "danger"; + +export type FIconPickerCategories = { + name: string; + categoryIcon: string | URL; + icons: FIconCustomSource[]; +}[]; +@customElement("f-icon-picker") +export class FIconPicker extends FRoot { + /** + * css loaded from scss file + */ + static styles = [ + unsafeCSS(globalStyle), + unsafeCSS(eleStyle), + ...FDiv.styles, + ...FText.styles, + ...FPopover.styles, + ...FIcon.styles, + ...FIconButton.styles, + ...FSearch.styles + ]; + + /** + * @attribute Defines the value of f-icon-picker + */ + @property({ reflect: true, type: Object }) + value?: FIconCustomSource; + + /** + * @attribute show/remove clear icon + */ + @property({ reflect: true, type: Boolean }) + clear?: boolean = true; + + /** + * @attribute Defines the size of f-icon-picker. size can be two types - `medium` | `small` + */ + @property({ reflect: true, type: String }) + size?: "medium" | "small" = "medium"; + + /** + * @attribute Defines the placeholder of f-icon-picker + */ + @property({ reflect: true, type: String }) + placeholder?: string; + + /** + * @attribute Variants are various visual representations of icon picker. + */ + @property({ reflect: true, type: String }) + variant?: "curved" | "round" | "block" = "curved"; + + /** + * @attribute Categories are various visual representations of icon picker. + */ + @property({ reflect: true, type: String }) + category?: "fill" | "outline" | "transparent" = "fill"; + + /** + * @attribute States are used to communicate purpose and connotations. + */ + @property({ reflect: true, type: String }) + state?: FIconPickerState = "default"; + + /** + * @attribute Sets the f-icon-picker to disabled state. + */ + @property({ reflect: true, type: Boolean }) + disabled?: boolean = false; + + /** + * @attribute Specify icon set. + */ + @property({ type: Object }) + categories!: FIconPickerCategories; + + /** + * @attribute if true close picker popover on value select + */ + @property({ reflect: true, type: Boolean, attribute: "close-on-select" }) + closeOnSelect?: boolean = false; + + // fix for vue2 + set ["close-on-select"](val: boolean) { + this.closeOnSelect = val; + } + + readonly required = ["categories"]; + + /** + * @attribute assigned elements inside slot label + */ + @queryAssignedElements({ slot: "label" }) + _labelNodes!: NodeListOf; + + /** + * @attribute assigned elements inside slot description + */ + @queryAssignedElements({ slot: "description" }) + _descriptionNodes!: NodeListOf; + + /** + * @attribute assigned elements inside slot help + */ + @queryAssignedElements({ slot: "help" }) + _helpNodes!: NodeListOf; + + @query("#f-emoji-picker-header") + emojiPickerHeader!: FDiv; + + @query("#f-emoji-picker-error") + emojiPickerError!: FDiv; + + @query("#label-slot") + labelSlot!: HTMLElement; + + @query("slot[name='description']") + descriptionSlot!: HTMLElement; + + @query("slot[name='help']") + helpSlot!: HTMLElement; + + @query(".f-icon-picker") + iconPicker!: HTMLDivElement; + + @query(".f-icon-picker-popover") + iconPickerPopover!: FPopover; + + @state() + searchKeyword?: string; + + clearValue() { + /** + * @event input + */ + const event = new CustomEvent("input", { + detail: { + name: undefined, + value: undefined + }, + bubbles: true, + composed: true + }); + this.value = undefined; + this.dispatchEvent(event); + } + handleIconSeletion(value: FIconCustomSource) { + /** + * @event input + */ + const event = new CustomEvent("input", { + detail: value, + bubbles: true, + composed: true + }); + this.value = value; + + if (this.closeOnSelect) { + this.closePopOver(); + } + this.dispatchEvent(event); + } + + get noIconFound() { + return this.filteredCategories?.length === 0 + ? html` + Sorry, no matching icon found for your search. Please try a different search term or + refine your query + ` + : this.categories + ? html`` + : nothing; + } + + get noCategories() { + return !this.filteredCategories + ? html` + No categories and icons configured. + ` + : nothing; + } + + get categoryTabs() { + return this.categories + ? html` + ${this.categories?.map((category, i) => { + return html` this.selectCategory(category.name)} + > + + `; + })} + ` + : nothing; + } + + get searchBar() { + return this.filteredCategories + ? html` + + ` + : nothing; + } + + get allIcons() { + return this.filteredCategories?.map(category => { + return html` + ${category.name} + + + ${category.icons.map(icon => { + return html` this.handleIconSeletion(icon)} + padding="small" + height="hug-content" + gap="x-small" + direction="column" + > + `; + })} + `; + }); + } + closePopOver() { + this.iconPickerPopover.open = false; + } + + getIconPickerPopover() { + return html` + + ${this.categoryTabs} ${this.searchBar} + + + ${this.noCategories} ${this.allIcons} ${this.noIconFound} + + + `; + } + render() { + /** + * clear conditional display + */ + const clearIcon = this.clear + ? html` + ${this.value + ? html` + ` + : ""} + ` + : ""; + + /** + * conditional display of inpu value or plcaeholder + */ + const inputValue = this.value + ? html`` + : html``; + + // render empty string, since there no need of any child element + return html` + + + + + + + + + + + + +
{ + e.stopPropagation(); + this.toggleIconPicker(true); + }} + > + + ${inputValue} + + ${clearIcon} +
+ +
+ ${this.getIconPickerPopover()} + `; + } + + selectCategory(category: string) { + const iconContainer = this.shadowRoot?.querySelector(".icon-container"); + const categorylabel = this.shadowRoot?.querySelector( + `.category-label[data-category="${category}"]` + ); + + if (iconContainer?.getBoundingClientRect().y === categorylabel?.getBoundingClientRect().y) { + const categoryIcons = this.shadowRoot?.querySelector( + `.category-icons[data-category="${category}"]` + ); + + if (categoryIcons) { + categoryIcons.scrollIntoView({ + block: "start" + }); + + if (iconContainer) { + iconContainer.scrollBy({ + top: -46, + behavior: "smooth" + }); + } + } + } else { + if (categorylabel) { + categorylabel.scrollIntoView({ + block: "start", + behavior: "smooth" + }); + } + } + } + + handleCategorySelection(event: Event) { + const container = event.target as HTMLElement; + const allLabels = container.querySelectorAll(".category-label"); + let lastStickylabel: FDiv | undefined = undefined; + allLabels.forEach(labelElement => { + if (labelElement.getBoundingClientRect().top === container.getBoundingClientRect().top) { + lastStickylabel = labelElement; + } + }); + + if (lastStickylabel) { + const allCatTabs = this.shadowRoot?.querySelectorAll(".category-tab"); + + allCatTabs?.forEach(tab => { + if (tab.dataset.category === lastStickylabel?.dataset.category) { + tab.selected = "notch-bottom"; + } else { + tab.selected = "none"; + } + }); + } + } + + get filteredCategories() { + if (this.searchKeyword && this.searchKeyword.length > 0) { + const filtered = this.categories.map(category => { + const fuse = new Fuse(category.icons, { + keys: ["name", "keywords"], + findAllMatches: true, + distance: 3 + }); + return { + ...category, + icons: fuse.search(this.searchKeyword as string).map(r => r.item) + }; + }); + return filtered.filter(cat => cat.icons.length > 0); + } + + return this.categories; + } + + handleSearch(e: CustomEvent) { + this.searchKeyword = e.detail.value as string; + } + + /** + * open/close picker + * @param value boolean + */ + toggleIconPicker(value: boolean) { + this.iconPickerPopover.target = this.iconPicker; + this.iconPickerPopover.open = value; + } +} + +/** + * Required for typescript + */ +declare global { + export interface HTMLElementTagNameMap { + "f-icon-picker": FIconPicker; + } +} diff --git a/packages/flow-core/src/components/f-icon/f-icon.ts b/packages/flow-core/src/components/f-icon/f-icon.ts index e776f186a..eff5ef663 100644 --- a/packages/flow-core/src/components/f-icon/f-icon.ts +++ b/packages/flow-core/src/components/f-icon/f-icon.ts @@ -17,6 +17,14 @@ import { Subscription } from "rxjs"; injectCss("f-icon", globalStyle); +export type FIconCustomSource = { + name: string; + source: string | URL; + keywords?: string; +}; + +export type FIconSource = string | FIconCustomSource; + export type FIconState = | "default" | "secondary" @@ -43,7 +51,7 @@ export class FIcon extends FRoot { fill = ""; private _source!: string; - private _originalSource?: string; + private _originalSource?: FIconSource; /** * @internal @@ -57,26 +65,27 @@ export class FIcon extends FRoot { /** * @attribute The small size is the default. */ - @property({ type: String }) + @property({ type: String, reflect: true }) size?: "x-large" | "large" | "medium" | "small" | "x-small" = "small"; /** * @attribute The state of an Icon helps in indicating the degree of emphasis. The Icon component inherits the state from the parent component. By default it is subtle. */ - @property({ type: String }) + @property({ type: String, reflect: true }) state?: FIconState = "default"; /** * @attribute Source property defines what will be displayed on the icon. For icon variant It can take the icon name from a library , any inline SVG or any URL for the image. For emoji, it takes emoji as inline text. */ @property({ - type: String + type: [String, Object], + reflect: true }) get source(): string { return this._source; } // source computed based on value given by user - set source(value) { + set source(value: FIconSource) { this._originalSource = value; this.computeSource(value); } @@ -90,13 +99,13 @@ export class FIcon extends FRoot { /** * @attribute display loader */ - @property({ type: Boolean }) + @property({ type: Boolean, reflect: true }) loading?: boolean = false; /** * @attribute is clickable */ - @property({ type: Boolean }) + @property({ type: Boolean, reflect: true }) clickable?: boolean = false; readonly required = ["source"]; @@ -151,34 +160,43 @@ export class FIcon extends FRoot { return ""; } - computeSource(value: string) { - const emojiRegex = /\p{Extended_Pictographic}/u; - if (isValidHttpUrl(value)) { - this.isURLSource = true; - this._source = ``; - } else if (emojiRegex.test(value)) { - this._source = value; - } else { - const IconPack = configSubject.value.iconPack; - if (IconPack) { - let svg = IconPack[value]; - const theme = configSubject.value.theme; - if (!svg && theme === "f-dark") { - svg = IconPack[value + "-dark"]; - } - if (!svg && theme === "f-light") { - svg = IconPack[value + "-light"]; - } - if (svg) { - this._source = svg; + computeSource(value: FIconSource) { + if (typeof value === "string") { + const emojiRegex = /\p{Extended_Pictographic}/u; + if (isValidHttpUrl(value)) { + this.isURLSource = true; + this._source = ``; + } else if (emojiRegex.test(value)) { + this._source = value; + } else { + const IconPack = configSubject.value.iconPack; + if (IconPack) { + let svg = IconPack[value]; + const theme = configSubject.value.theme; + if (!svg && theme === "f-dark") { + svg = IconPack[value + "-dark"]; + } + if (!svg && theme === "f-light") { + svg = IconPack[value + "-light"]; + } + if (svg) { + this._source = svg; + } else { + this._source = notFound; + } } else { this._source = notFound; } + } + } else if (typeof value === "object") { + if (typeof value.source === "string") { + this._source = value.source; + } else if (value.source instanceof URL) { + this._source = ``; } else { this._source = notFound; } } - this.requestUpdate(); } render() { diff --git a/packages/flow-core/src/index.ts b/packages/flow-core/src/index.ts index 5f60adc03..db1672f84 100644 --- a/packages/flow-core/src/index.ts +++ b/packages/flow-core/src/index.ts @@ -49,6 +49,7 @@ export * from "./components/f-form-field/f-form-field"; export * from "./components/f-input/f-input-light"; export * from "./components/f-color-picker/f-color-picker"; export * from "./components/f-countdown/f-countdown"; +export * from "./components/f-icon-picker/f-icon-picker"; export { html } from "lit"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab99a9760..7cd5d0db2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: lit: specifier: ^3.1.0 version: 3.1.1 + simple-icons: + specifier: ^11.4.0 + version: 11.4.0 devDependencies: '@changesets/cli': specifier: ^2.25.0 @@ -263,6 +266,9 @@ importers: flatpickr: specifier: ^4.6.13 version: 4.6.13 + fuse.js: + specifier: ^7.0.0 + version: 7.0.0 lit: specifier: ^3.1.0 version: 3.1.1 @@ -9387,6 +9393,11 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true + /fuse.js@7.0.0: + resolution: {integrity: sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==} + engines: {node: '>=10'} + dev: false + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -13323,6 +13334,11 @@ packages: engines: {node: '>=14'} dev: true + /simple-icons@11.4.0: + resolution: {integrity: sha512-f6Y/+qZk8/+qn8NMdOV80MIFYoW0ulaTmtUunZZXB6Ix/01NKoB7yopMzxeZUWtygCr+l75KKSRQo24Db2h0Ug==} + engines: {node: '>=0.12.18'} + dev: false + /simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} diff --git a/stories/flow-core/f-div.stories.ts b/stories/flow-core/f-div.stories.ts index 1fd9eaed7..6cdd55539 100644 --- a/stories/flow-core/f-div.stories.ts +++ b/stories/flow-core/f-div.stories.ts @@ -248,7 +248,15 @@ export const Playground = { selected: { control: "select", - options: ["background", "border", "notch-right", "notch-left", "None"] + options: [ + "background", + "border", + "notch-right", + "notch-left", + "notch-top", + "notch-bottom", + "none" + ] }, highlight: { diff --git a/stories/flow-core/f-icon-picker-apple-categories.ts b/stories/flow-core/f-icon-picker-apple-categories.ts new file mode 100644 index 000000000..a87751ee7 --- /dev/null +++ b/stories/flow-core/f-icon-picker-apple-categories.ts @@ -0,0 +1,16 @@ +import { FIconPickerCategories } from "@ollion/flow-core"; +const appleCategories: FIconPickerCategories = []; + +appleCategories.push({ + name: "Apple", + categoryIcon: ``, + icons: [ + { + name: "apple-music", + source: ``, + keywords: "apple music" + } + ] +}); + +export default appleCategories; diff --git a/stories/flow-core/f-icon-picker-categories.ts b/stories/flow-core/f-icon-picker-categories.ts new file mode 100644 index 000000000..444166ae9 --- /dev/null +++ b/stories/flow-core/f-icon-picker-categories.ts @@ -0,0 +1,77 @@ +import { faker } from "@faker-js/faker"; +import { FIconCustomSource, FIconPickerCategories } from "@ollion/flow-core"; + +import * as icons from "simple-icons"; +const categories: FIconPickerCategories = []; + +const categoryIconLinks = [ + "https://cdn3.iconfinder.com/data/icons/logos-brands-3/24/logo_brand_brands_logos_google-1024.png", + "https://cdn3.iconfinder.com/data/icons/logos-brands-3/24/logo_brand_brands_logos_linux-1024.png", + "https://cdn4.iconfinder.com/data/icons/small-n-flat/24/globe-1024.png", + "https://cdn3.iconfinder.com/data/icons/logos-brands-3/24/logo_brand_brands_logos_safari-1024.png", + "https://cdn3.iconfinder.com/data/icons/social-media-2169/24/social_media_social_media_logo_google_wallet-1024.png" +]; +const categoryNames = ["Google", "Linux", "Earth", "Safari", "Media"]; +export default function getCategories() { + if (categories.length === 0) { + const allIcons = Object.entries(icons); + for (let i = 0; i < 5; i++) { + const icons: FIconCustomSource[] = []; + const noOfIcons = faker.number.int({ min: 10, max: 100 }); + for (let j = 0; j < noOfIcons; j++) { + const randomIcon = faker.helpers.arrayElement(allIcons); + icons.push({ + name: randomIcon[0], + source: randomIcon[1].svg, + keywords: randomIcon[1].title + }); + } + categories.push({ + name: categoryNames[i], //faker.company.buzzNoun(), + categoryIcon: new URL(categoryIconLinks[i]), //faker.helpers.arrayElement(allIcons)[1].svg, + icons + }); + } + } + + return categories; +} + +// const getIconName = () => { +// return `${faker.string.fromCharacters( +// "abcdefghijklmnopqrstuxyz" +// )}-${faker.commerce.product()}`.toLocaleLowerCase(); +// }; + +// const categories: FIconPickerCategories = [ +// { +// name: faker.company.buzzNoun(), +// categoryIcon: `StackHawk`, +// icons: [ +// { +// name: getIconName(), +// source: `99designs` +// }, +// { +// name: getIconName(), +// source: `Adobe InDesign` +// } +// ] +// }, +// { +// name: faker.company.buzzNoun(), +// categoryIcon: `Stellar`, +// icons: [ +// { +// name: getIconName(), +// source: `Stryker` +// }, +// { +// name: getIconName(), +// source: `StackBlitz` +// } +// ] +// } +// ]; + +// export const categories; diff --git a/stories/flow-core/f-icon-picker.stories.ts b/stories/flow-core/f-icon-picker.stories.ts new file mode 100644 index 000000000..7abddde5c --- /dev/null +++ b/stories/flow-core/f-icon-picker.stories.ts @@ -0,0 +1,346 @@ +import getCategories from "./f-icon-picker-categories"; +import { html } from "lit-html"; +import appleCategories from "./f-icon-picker-apple-categories"; + +export default { + title: "@ollion/flow-core/f-icon-picker", + + parameters: { + controls: { + hideNoControlsWarning: true + } + } +}; + +const categories = getCategories(); + +export const Playground = { + render: (args: Record) => { + const handleInput = (e: CustomEvent) => { + console.log("input event", e); + }; + + return html` + + + + Label + Help! + + + + `; + }, + + name: "Playground", + + argTypes: { + value: { + control: "text" + }, + + placeholder: { + control: "text" + }, + + variant: { + control: "select", + options: ["curved", "round", "block"] + }, + + category: { + control: "select", + options: ["fill", "transparent", "outline"] + }, + + state: { + control: "select", + options: ["default", "success", "primary", "warning", "danger"] + }, + + size: { + control: "radio", + options: ["small", "medium"] + }, + + disabled: { + control: "boolean" + }, + + clear: { + control: "boolean" + } + }, + + args: { + value: undefined, + placeholder: undefined, + variant: "round", + category: "fill", + state: "default", + size: "medium", + disabled: false, + clear: true + } +}; + +export const Variant = { + render: () => { + const variants = ["curved", "round", "block"]; + + const value = "i-plus"; + const handleValue = (e: CustomEvent) => { + console.log("input event", e); + }; + + return html` + + ${variants.map( + item => html` + + Label + Help! + ` + )} + + `; + }, + + name: "variant" +}; + +export const Category = { + render: () => { + const categories = ["fill", "outline", "transparent"]; + const value = ""; + + const handleValue = (e: CustomEvent) => { + console.log("input event", e); + }; + + return html` + + ${categories.map( + item => html` + + Label + Help! + ` + )} + + `; + }, + + name: "category" +}; + +export const Value = { + render: () => { + const value = "⌛"; + + const handleValue = (e: CustomEvent) => { + console.log("input event", e); + }; + + return html` + + + + Label + Help! + + `; + }, + + name: "value" +}; + +export const Placeholder = { + render: () => { + const value = ""; + + const handleValue = (e: CustomEvent) => { + console.log("input event", e); + }; + + return html` + + + + Label + Help! + + `; + }, + + name: "placeholder" +}; + +export const Size = { + render: () => { + const sizes = ["small", "medium"]; + const value = ""; + + const handleValue = (e: CustomEvent) => { + console.log("input event", e); + }; + + return html` + + ${sizes.map( + item => html` + + Label + Help! + ` + )} + + `; + }, + + name: "size" +}; + +export const State = { + render: () => { + const states = [ + ["default", "primary", "success"], + ["danger", "warning", "default"] + ]; + const value = ""; + + const handleValue = (e: CustomEvent) => { + console.log("input event", e); + }; + + return html` + + ${states.map( + item => + html` + ${item.map( + state => + html` + Label + Help! + ` + )} + ` + )} + + `; + }, + + name: "state" +}; + +export const Flags = { + render: () => { + const value = ""; + + const handleValue = (e: CustomEvent) => { + console.log("input event", e); + }; + + return html` + + disabled=true + + + Label + Help! + + + + clear=false + + + Label + Help! + + + + close-on-select=true + + + Label + Help! + + + `; + }, + + name: "Flags" +};