diff --git a/app/controllers/api/v1/accounts/categories_controller.rb b/app/controllers/api/v1/accounts/categories_controller.rb index 686ffaeec..655b3c890 100644 --- a/app/controllers/api/v1/accounts/categories_controller.rb +++ b/app/controllers/api/v1/accounts/categories_controller.rb @@ -53,7 +53,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle def category_params params.require(:category).permit( - :name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id + :name, :description, :position, :slug, :locale, :icon, :icon_color, :parent_category_id, :associated_category_id ) end diff --git a/app/helpers/portal_helper.rb b/app/helpers/portal_helper.rb index 0c993ec59..31bfe8632 100644 --- a/app/helpers/portal_helper.rb +++ b/app/helpers/portal_helper.rb @@ -97,6 +97,18 @@ module PortalHelper ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text end + # Renders a stored category icon: a bare ri icon name (e.g. `vip-crown-2-fill/line`) saved color, or a plain emoji character. + def render_emoji_or_icon(value, color = nil) + return '' if value.blank? + + # Emojis are non-ascii; bare icon names match this safe charset. + return ERB::Util.html_escape(value) unless value.match?(/\A[a-z][a-z0-9-]*\z/) + + icon_class = value.start_with?('i-') ? value : "i-ri-#{value}" + style = "color: #{color};" if color.to_s.match?(/\A#\h{3,8}\z/) + tag.span(class: icon_class, style: style, 'aria-hidden': true) + end + def thumbnail_bg_color(username) colors = ['#6D95BA', '#A4C3C3', '#E19191'] return colors.sample if username.blank? diff --git a/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue b/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue index 93ff15251..9b1c48a90 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue @@ -13,6 +13,7 @@ import { useMapGetter } from 'dashboard/composables/store.js'; import { useConfig } from 'dashboard/composables/useConfig'; import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import Icon from 'dashboard/components-next/icon/Icon.vue'; +import EmojiIcon from 'dashboard/components-next/emoji-icon-picker/EmojiIcon.vue'; import CardLayout from 'dashboard/components-next/CardLayout.vue'; import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue'; import Button from 'dashboard/components-next/button/Button.vue'; @@ -131,7 +132,7 @@ const statusText = computed(() => { const categoryName = computed(() => { if (props.category?.slug) { - return `${props.category.icon} ${props.category.name}`; + return props.category.name; } return t( 'HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.CATEGORY.UNCATEGORISED' @@ -222,7 +223,15 @@ const handleClick = id => { {{ authorName || '-' }} - + + {{ categoryName }}
{ - return `${props.icon} ${props.title}`; -}); - const description = computed(() => { return props.description ? props.description : 'No description added'; }); @@ -83,10 +84,16 @@ const handleAction = ({ action, value }) => {
- {{ categoryTitleWithIcon }} + + {{ title }} { const categoryList = computed(() => { return ( categories.value - .map(({ name, id, icon }) => ({ + .map(({ name, id, icon, icon_color: iconColor }) => ({ label: name, value: id, emoji: icon, + iconColor, isSelected: isNewArticle.value ? id === (selectedCategoryId.value || selectedCategory.value?.id) : id === props.article?.category?.id, @@ -202,10 +204,6 @@ onMounted(() => {
{ return tabs.value.findIndex(tab => tab.value === tabParam); }); -const activeCategoryName = computed(() => { - const activeCategory = props.categories.find( - category => category.slug === route.params.categorySlug - ); +const activeCategory = computed(() => + props.categories.find(category => category.slug === route.params.categorySlug) +); - if (activeCategory) { - const { icon, name } = activeCategory; - return `${icon} ${name}`; - } - - return t('HELP_CENTER.ARTICLES_PAGE.ARTICLES_HEADER.CATEGORY.ALL'); -}); +const activeCategoryName = computed( + () => + activeCategory.value?.name || + t('HELP_CENTER.ARTICLES_PAGE.ARTICLES_HEADER.CATEGORY.ALL') +); const activeLocaleName = computed(() => { return props.allowedLocales.find( @@ -94,6 +92,7 @@ const categoryMenuItems = computed(() => { value: category.slug, action: 'filter', emoji: category.icon, + iconColor: category.icon_color, })); const hasCategorySlug = !!route.params.categorySlug; @@ -167,14 +166,23 @@ const handleTabChange = value => {
value: category.id, action: 'move', emoji: category.icon, + iconColor: category.icon_color, })) ); diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryDialog.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryDialog.vue index d71c17260..efb6be73f 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryDialog.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryDialog.vue @@ -38,8 +38,8 @@ const { t } = useI18n(); const route = useRoute(); const handleCategory = async formData => { - const { id, name, slug, icon, description, locale } = formData; - const categoryData = { name, icon, slug, description }; + const { id, name, slug, icon, iconColor, description, locale } = formData; + const categoryData = { name, icon, icon_color: iconColor, slug, description }; if (props.mode === 'create') { categoryData.locale = locale; diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryForm.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryForm.vue index d2bf42ef5..a1d5624c9 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryForm.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryForm.vue @@ -18,6 +18,7 @@ import { convertToCategorySlug } from 'dashboard/helper/commons.js'; import Input from 'dashboard/components-next/input/Input.vue'; import TextArea from 'dashboard/components-next/textarea/TextArea.vue'; import Button from 'dashboard/components-next/button/Button.vue'; +import EmojiIcon from 'dashboard/components-next/emoji-icon-picker/EmojiIcon.vue'; const props = defineProps({ mode: { @@ -49,8 +50,9 @@ const props = defineProps({ const emit = defineEmits(['submit', 'cancel']); -const EmojiInput = defineAsyncComponent( - () => import('shared/components/emoji/EmojiInput.vue') +const EmojiIconPicker = defineAsyncComponent( + () => + import('dashboard/components-next/emoji-icon-picker/EmojiIconPicker.vue') ); const { t } = useI18n(); @@ -72,6 +74,7 @@ const state = reactive({ id: '', name: '', icon: '', + iconColor: '', slug: '', description: '', locale: '', @@ -109,8 +112,19 @@ const slugHelpText = computed(() => { }); }); -const onClickInsertEmoji = emoji => { - state.icon = emoji; +const onSelectIcon = ({ type, value, color }) => { + state.icon = value; + state.iconColor = type === 'icon' ? color : ''; + isEmojiPickerOpen.value = false; +}; + +const onColorChange = color => { + state.iconColor = color; +}; + +const onRemoveIcon = () => { + state.icon = ''; + state.iconColor = ''; isEmojiPickerOpen.value = false; }; @@ -139,7 +153,14 @@ watch( newCategory => { if (props.mode === 'edit' && newCategory) { const { id, name, icon, slug, description } = newCategory; - Object.assign(state, { id, name, icon, slug, description }); + Object.assign(state, { + id, + name, + icon, + iconColor: newCategory.icon_color || '', + slug, + description, + }); } }, { immediate: true } @@ -197,19 +218,29 @@ defineExpose({ state, isSubmitDisabled }); diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryHeaderControls.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryHeaderControls.vue index f25ac8027..d134f1168 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryHeaderControls.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryHeaderControls.vue @@ -106,6 +106,7 @@ const breadcrumbItems = computed(() => { } ), emoji: selectedCategoryEmoji.value, + iconColor: selectedCategory.value?.icon_color, }); } return items; diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryList.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryList.vue index 0dc0de895..45e783246 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryList.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryList.vue @@ -68,6 +68,7 @@ watch( :id="element.id" :title="element.name" :icon="element.icon" + :icon-color="element.icon_color" :description="element.description" :articles-count="element.meta?.articles_count || 0" :slug="element.slug" diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/EditCategoryDialog.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/EditCategoryDialog.vue index 5b339e568..2456ce596 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/EditCategoryDialog.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/EditCategoryDialog.vue @@ -52,8 +52,8 @@ const activeLocaleCode = computed(() => activeLocale.value?.code ?? ''); const onUpdateCategory = async () => { if (!categoryFormRef.value) return; const { state } = categoryFormRef.value; - const { id, name, slug, icon, description } = state; - const categoryData = { name, icon, slug, description }; + const { id, name, slug, icon, iconColor, description } = state; + const categoryData = { name, icon, icon_color: iconColor, slug, description }; categoryData.id = id; try { diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue index 22c142322..e488ee107 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue @@ -53,8 +53,9 @@ const generateUid = () => { const uploadAttachment = ref(null); const isEmojiPickerOpen = ref(false); -const EmojiInput = defineAsyncComponent( - () => import('shared/components/emoji/EmojiInput.vue') +const EmojiIconPicker = defineAsyncComponent( + () => + import('dashboard/components-next/emoji-icon-picker/EmojiIconPicker.vue') ); const { @@ -215,10 +216,11 @@ useEventListener(document, 'paste', onPaste); class="!w-10" @click="isEmojiPickerOpen = !isEmojiPickerOpen" /> -
{ - - {{ item.emoji ? item.emoji : '' }} {{ item.label }} + + + {{ item.label }} diff --git a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue index fba446c0d..1849bf685 100644 --- a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue +++ b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue @@ -3,6 +3,7 @@ import { ref, computed, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; import Icon from 'dashboard/components-next/icon/Icon.vue'; +import EmojiIcon from 'dashboard/components-next/emoji-icon-picker/EmojiIcon.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; import Spinner from 'dashboard/components-next/spinner/Spinner.vue'; @@ -207,9 +208,12 @@ onMounted(() => { class="flex-shrink-0 size-3.5" /> - {{ - item.emoji - }} + { class="flex-shrink-0 size-3.5" /> - {{ item.emoji }} + +import { useI18n } from 'vue-i18n'; +import { ICON_COLORS } from './constants'; + +const { t } = useI18n(); +const selectedColor = defineModel({ type: String, default: '' }); + + + diff --git a/app/javascript/dashboard/components-next/emoji-icon-picker/EmojiIcon.vue b/app/javascript/dashboard/components-next/emoji-icon-picker/EmojiIcon.vue new file mode 100644 index 000000000..c4ca21be5 --- /dev/null +++ b/app/javascript/dashboard/components-next/emoji-icon-picker/EmojiIcon.vue @@ -0,0 +1,35 @@ + + + diff --git a/app/javascript/dashboard/components-next/emoji-icon-picker/EmojiIconPicker.vue b/app/javascript/dashboard/components-next/emoji-icon-picker/EmojiIconPicker.vue new file mode 100644 index 000000000..5413f5a48 --- /dev/null +++ b/app/javascript/dashboard/components-next/emoji-icon-picker/EmojiIconPicker.vue @@ -0,0 +1,310 @@ + + + diff --git a/app/javascript/dashboard/components-next/emoji-icon-picker/constants.js b/app/javascript/dashboard/components-next/emoji-icon-picker/constants.js new file mode 100644 index 000000000..d8226db3e --- /dev/null +++ b/app/javascript/dashboard/components-next/emoji-icon-picker/constants.js @@ -0,0 +1,44 @@ +// Prefix that turns a stored icon name (e.g. "rocket-line") into a class. +// Swapping icon libraries means changing this and the curated set only. +export const ICON_PREFIX = 'i-ri-'; + +export const ICON_STYLE = { + LINE: 'line', + FILL: 'fill', +}; + +// Icon values are ascii names (e.g. "rocket-line"); emoji are non-ascii. +export const isIconValue = value => + typeof value === 'string' && /^[a-z][a-z0-9-]*$/.test(value); + +export const iconClassFor = value => + value.startsWith(ICON_PREFIX) ? value : `${ICON_PREFIX}${value}`; + +export const ICON_COLORS = [ + { name: 'SLATE', value: '#64748B' }, + { name: 'RED', value: '#EF4444' }, + { name: 'ORANGE', value: '#F97316' }, + { name: 'AMBER', value: '#F59E0B' }, + { name: 'GREEN', value: '#22C55E' }, + { name: 'TEAL', value: '#14B8A6' }, + { name: 'BLUE', value: '#3B82F6' }, + { name: 'INDIGO', value: '#6366F1' }, + { name: 'VIOLET', value: '#8B5CF6' }, + { name: 'PINK', value: '#EC4899' }, +]; + +export const DEFAULT_ICON_COLOR = '#3B82F6'; + +// Recently used emojis persisted in localStorage. +export const RECENT_EMOJI_KEY = 'emoji-icon-picker.recent-emojis'; +export const MAX_RECENT_EMOJIS = 16; + +export const PICKER_MODE = { + BOTH: 'both', + EMOJI: 'emoji', +}; + +export const PICKER_TAB = { + ICONS: 'icons', + EMOJIS: 'emojis', +}; diff --git a/app/javascript/dashboard/components-next/emoji-icon-picker/helper.js b/app/javascript/dashboard/components-next/emoji-icon-picker/helper.js new file mode 100644 index 000000000..03e8ec5e0 --- /dev/null +++ b/app/javascript/dashboard/components-next/emoji-icon-picker/helper.js @@ -0,0 +1,90 @@ +import emojiGroups from 'shared/components/emoji/emojisGroup.json'; +import { MAX_RECENT_EMOJIS, RECENT_EMOJI_KEY } from './constants'; + +const matchesSearch = (emoji, term) => + emoji.slug.replaceAll('_', ' ').includes(term) || + emoji.name.toLowerCase().includes(term); + +// Emoji sections for the search term; prepends "Frequently used" when idle. +export const buildEmojiSections = (search, recentEmojis, frequentLabel) => { + const term = search.trim().toLowerCase(); + if (term) { + return emojiGroups + .map(({ name, emojis }) => ({ + name, + emojis: emojis.filter(e => matchesSearch(e, term)), + })) + .filter(group => group.emojis.length); + } + return [ + ...(recentEmojis.length + ? [{ name: frequentLabel, emojis: recentEmojis }] + : []), + ...emojiGroups, + ]; +}; + +// Samples an emoji's average color (via canvas) as a translucent hover tint. Cached per emoji. +const emojiTintCache = new Map(); +const NEUTRAL_TINT = 'rgb(var(--slate-9) / 0.12)'; + +const sampleEmojiTint = emoji => { + const canvas = Object.assign(document.createElement('canvas'), { + width: 16, + height: 16, + }); + const ctx = canvas.getContext('2d', { willReadFrequently: true }); + ctx.font = '14px serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(emoji, 8, 9); + + const { data } = ctx.getImageData(0, 0, 16, 16); + const pixels = Array.from( + { length: data.length / 4 }, + (_, i) => i * 4 + ).filter(i => data[i + 3] > 16); + if (!pixels.length) return NEUTRAL_TINT; + + const avg = offset => + Math.round( + pixels.reduce((sum, i) => sum + data[i + offset], 0) / pixels.length + ); + + return `rgba(${avg(0)}, ${avg(1)}, ${avg(2)}, 0.16)`; +}; + +export const getEmojiTint = emoji => { + if (!emojiTintCache.has(emoji)) { + let tint = NEUTRAL_TINT; + try { + tint = sampleEmojiTint(emoji); + } catch { + /* canvas unavailable */ + } + emojiTintCache.set(emoji, tint); + } + return emojiTintCache.get(emoji); +}; + +export const getRecentEmojis = () => { + try { + const stored = JSON.parse(localStorage.getItem(RECENT_EMOJI_KEY) ?? '[]'); + return Array.isArray(stored) ? stored : []; + } catch { + return []; + } +}; + +export const addRecentEmoji = emoji => { + const updated = [ + emoji, + ...getRecentEmojis().filter(item => item.slug !== emoji.slug), + ].slice(0, MAX_RECENT_EMOJIS); + try { + localStorage.setItem(RECENT_EMOJI_KEY, JSON.stringify(updated)); + } catch { + /* private mode; recents are best-effort */ + } + return updated; +}; diff --git a/app/javascript/dashboard/components-next/emoji-icon-picker/icons.js b/app/javascript/dashboard/components-next/emoji-icon-picker/icons.js new file mode 100644 index 000000000..3cf0d7b3d --- /dev/null +++ b/app/javascript/dashboard/components-next/emoji-icon-picker/icons.js @@ -0,0 +1,983 @@ +// Curated Remix (ri) icons, each with a `line` and `fill` class. The class +// literals are kept here so Tailwind generates them. { name, keywords, line, fill } + +export const CURATED_ICONS = [ + { + name: 'home', + keywords: 'home house main', + line: 'i-ri-home-line', + fill: 'i-ri-home-fill', + }, + { + name: 'star', + keywords: 'favorite star', + line: 'i-ri-star-line', + fill: 'i-ri-star-fill', + }, + { + name: 'heart', + keywords: 'like love', + line: 'i-ri-heart-line', + fill: 'i-ri-heart-fill', + }, + { + name: 'bookmark', + keywords: 'save', + line: 'i-ri-bookmark-line', + fill: 'i-ri-bookmark-fill', + }, + { + name: 'flag-2', + keywords: 'flag report', + line: 'i-ri-flag-2-line', + fill: 'i-ri-flag-2-fill', + }, + { + name: 'price-tag-3', + keywords: 'tag label', + line: 'i-ri-price-tag-3-line', + fill: 'i-ri-price-tag-3-fill', + }, + { + name: 'map-pin', + keywords: 'location place', + line: 'i-ri-map-pin-line', + fill: 'i-ri-map-pin-fill', + }, + { + name: 'sparkling-2', + keywords: 'magic ai shine sparkle', + line: 'i-ri-sparkling-2-line', + fill: 'i-ri-sparkling-2-fill', + }, + { + name: 'fire', + keywords: 'flame hot trending', + line: 'i-ri-fire-line', + fill: 'i-ri-fire-fill', + }, + { + name: 'lightbulb', + keywords: 'idea tip', + line: 'i-ri-lightbulb-line', + fill: 'i-ri-lightbulb-fill', + }, + { + name: 'rocket', + keywords: 'launch startup ship', + line: 'i-ri-rocket-line', + fill: 'i-ri-rocket-fill', + }, + { + name: 'focus-3', + keywords: 'target goal aim', + line: 'i-ri-focus-3-line', + fill: 'i-ri-focus-3-fill', + }, + { + name: 'award', + keywords: 'prize badge medal', + line: 'i-ri-award-line', + fill: 'i-ri-award-fill', + }, + { + name: 'trophy', + keywords: 'win achievement', + line: 'i-ri-trophy-line', + fill: 'i-ri-trophy-fill', + }, + { + name: 'vip-crown-2', + keywords: 'crown premium vip', + line: 'i-ri-vip-crown-2-line', + fill: 'i-ri-vip-crown-2-fill', + }, + { + name: 'vip-diamond', + keywords: 'gem diamond premium', + line: 'i-ri-vip-diamond-line', + fill: 'i-ri-vip-diamond-fill', + }, + { + name: 'gift', + keywords: 'present reward', + line: 'i-ri-gift-line', + fill: 'i-ri-gift-fill', + }, + { + name: 'notification-3', + keywords: 'bell alert', + line: 'i-ri-notification-3-line', + fill: 'i-ri-notification-3-fill', + }, + { + name: 'global', + keywords: 'globe world web', + line: 'i-ri-global-line', + fill: 'i-ri-global-fill', + }, + { + name: 'compass-3', + keywords: 'explore discover', + line: 'i-ri-compass-3-line', + fill: 'i-ri-compass-3-fill', + }, + { + name: 'puzzle-2', + keywords: 'extension integration', + line: 'i-ri-puzzle-2-line', + fill: 'i-ri-puzzle-2-fill', + }, + { + name: 'ticket-2', + keywords: 'support coupon', + line: 'i-ri-ticket-2-line', + fill: 'i-ri-ticket-2-fill', + }, + { + name: 'megaphone', + keywords: 'announce broadcast marketing', + line: 'i-ri-megaphone-line', + fill: 'i-ri-megaphone-fill', + }, + { + name: 'briefcase-4', + keywords: 'work job business', + line: 'i-ri-briefcase-4-line', + fill: 'i-ri-briefcase-4-fill', + }, + { + name: 'building-2', + keywords: 'company office org', + line: 'i-ri-building-2-line', + fill: 'i-ri-building-2-fill', + }, + { + name: 'store-2', + keywords: 'shop commerce', + line: 'i-ri-store-2-line', + fill: 'i-ri-store-2-fill', + }, + { + name: 'community', + keywords: 'group team', + line: 'i-ri-community-line', + fill: 'i-ri-community-fill', + }, + { + name: 'folder', + keywords: 'directory', + line: 'i-ri-folder-line', + fill: 'i-ri-folder-fill', + }, + { + name: 'folder-open', + keywords: 'directory open', + line: 'i-ri-folder-open-line', + fill: 'i-ri-folder-open-fill', + }, + { + name: 'file', + keywords: 'document', + line: 'i-ri-file-line', + fill: 'i-ri-file-fill', + }, + { + name: 'file-text', + keywords: 'document doc', + line: 'i-ri-file-text-line', + fill: 'i-ri-file-text-fill', + }, + { + name: 'file-copy', + keywords: 'documents files', + line: 'i-ri-file-copy-line', + fill: 'i-ri-file-copy-fill', + }, + { + name: 'clipboard', + keywords: 'copy tasks', + line: 'i-ri-clipboard-line', + fill: 'i-ri-clipboard-fill', + }, + { + name: 'book-2', + keywords: 'read guide book', + line: 'i-ri-book-2-line', + fill: 'i-ri-book-2-fill', + }, + { + name: 'book-open', + keywords: 'read guide docs', + line: 'i-ri-book-open-line', + fill: 'i-ri-book-open-fill', + }, + { + name: 'graduation-cap', + keywords: 'learn course education', + line: 'i-ri-graduation-cap-line', + fill: 'i-ri-graduation-cap-fill', + }, + { + name: 'newspaper', + keywords: 'news article blog', + line: 'i-ri-newspaper-line', + fill: 'i-ri-newspaper-fill', + }, + { + name: 'sticky-note', + keywords: 'note memo', + line: 'i-ri-sticky-note-line', + fill: 'i-ri-sticky-note-fill', + }, + { + name: 'pencil', + keywords: 'write edit', + line: 'i-ri-pencil-line', + fill: 'i-ri-pencil-fill', + }, + { + name: 'quill-pen', + keywords: 'write author', + line: 'i-ri-quill-pen-line', + fill: 'i-ri-quill-pen-fill', + }, + { + name: 'mark-pen', + keywords: 'highlight', + line: 'i-ri-mark-pen-line', + fill: 'i-ri-mark-pen-fill', + }, + { + name: 'calendar-2', + keywords: 'date schedule calendar', + line: 'i-ri-calendar-2-line', + fill: 'i-ri-calendar-2-fill', + }, + { + name: 'calendar-event', + keywords: 'schedule event', + line: 'i-ri-calendar-event-line', + fill: 'i-ri-calendar-event-fill', + }, + { + name: 'time', + keywords: 'clock time', + line: 'i-ri-time-line', + fill: 'i-ri-time-fill', + }, + { + name: 'timer', + keywords: 'countdown', + line: 'i-ri-timer-line', + fill: 'i-ri-timer-fill', + }, + { + name: 'history', + keywords: 'recent past', + line: 'i-ri-history-line', + fill: 'i-ri-history-fill', + }, + { + name: 'hourglass', + keywords: 'wait time', + line: 'i-ri-hourglass-line', + fill: 'i-ri-hourglass-fill', + }, + { + name: 'shopping-cart-2', + keywords: 'buy ecommerce cart', + line: 'i-ri-shopping-cart-2-line', + fill: 'i-ri-shopping-cart-2-fill', + }, + { + name: 'shopping-bag-3', + keywords: 'buy purchase bag', + line: 'i-ri-shopping-bag-3-line', + fill: 'i-ri-shopping-bag-3-fill', + }, + { + name: 'box-3', + keywords: 'package box shipping', + line: 'i-ri-box-3-line', + fill: 'i-ri-box-3-fill', + }, + { + name: 'archive', + keywords: 'store box archive', + line: 'i-ri-archive-line', + fill: 'i-ri-archive-fill', + }, + { + name: 'bank-card', + keywords: 'pay billing card credit', + line: 'i-ri-bank-card-line', + fill: 'i-ri-bank-card-fill', + }, + { + name: 'wallet-3', + keywords: 'money pay wallet', + line: 'i-ri-wallet-3-line', + fill: 'i-ri-wallet-3-fill', + }, + { + name: 'money-dollar-circle', + keywords: 'money price usd', + line: 'i-ri-money-dollar-circle-line', + fill: 'i-ri-money-dollar-circle-fill', + }, + { + name: 'copper-coin', + keywords: 'coins money', + line: 'i-ri-copper-coin-line', + fill: 'i-ri-copper-coin-fill', + }, + { + name: 'currency', + keywords: 'money exchange', + line: 'i-ri-currency-line', + fill: 'i-ri-currency-fill', + }, + { + name: 'receipt', + keywords: 'invoice bill', + line: 'i-ri-receipt-line', + fill: 'i-ri-receipt-fill', + }, + { + name: 'percent', + keywords: 'discount sale', + line: 'i-ri-percent-line', + fill: 'i-ri-percent-fill', + }, + { + name: 'calculator', + keywords: 'math', + line: 'i-ri-calculator-line', + fill: 'i-ri-calculator-fill', + }, + { + name: 'mail', + keywords: 'email message', + line: 'i-ri-mail-line', + fill: 'i-ri-mail-fill', + }, + { + name: 'send-plane', + keywords: 'send submit', + line: 'i-ri-send-plane-line', + fill: 'i-ri-send-plane-fill', + }, + { + name: 'chat-3', + keywords: 'chat talk message', + line: 'i-ri-chat-3-line', + fill: 'i-ri-chat-3-fill', + }, + { + name: 'message-2', + keywords: 'chat comment', + line: 'i-ri-message-2-line', + fill: 'i-ri-message-2-fill', + }, + { + name: 'question-answer', + keywords: 'chat conversation faq', + line: 'i-ri-question-answer-line', + fill: 'i-ri-question-answer-fill', + }, + { + name: 'phone', + keywords: 'call', + line: 'i-ri-phone-line', + fill: 'i-ri-phone-fill', + }, + { + name: 'vidicon', + keywords: 'video call meeting', + line: 'i-ri-vidicon-line', + fill: 'i-ri-vidicon-fill', + }, + { + name: 'mic', + keywords: 'record voice audio', + line: 'i-ri-mic-line', + fill: 'i-ri-mic-fill', + }, + { + name: 'group', + keywords: 'team people users', + line: 'i-ri-group-line', + fill: 'i-ri-group-fill', + }, + { + name: 'user', + keywords: 'person account', + line: 'i-ri-user-line', + fill: 'i-ri-user-fill', + }, + { + name: 'user-add', + keywords: 'add member', + line: 'i-ri-user-add-line', + fill: 'i-ri-user-add-fill', + }, + { + name: 'contacts', + keywords: 'person card contact', + line: 'i-ri-contacts-line', + fill: 'i-ri-contacts-fill', + }, + { + name: 'emotion-happy', + keywords: 'happy smile emoji', + line: 'i-ri-emotion-happy-line', + fill: 'i-ri-emotion-happy-fill', + }, + { + name: 'thumb-up', + keywords: 'like approve', + line: 'i-ri-thumb-up-line', + fill: 'i-ri-thumb-up-fill', + }, + { + name: 'hand-heart', + keywords: 'care support', + line: 'i-ri-hand-heart-line', + fill: 'i-ri-hand-heart-fill', + }, + { + name: 'at', + keywords: 'mention email at', + line: 'i-ri-at-line', + fill: 'i-ri-at-fill', + }, + { + name: 'image-2', + keywords: 'picture photo image', + line: 'i-ri-image-2-line', + fill: 'i-ri-image-2-fill', + }, + { + name: 'camera-3', + keywords: 'photo camera', + line: 'i-ri-camera-3-line', + fill: 'i-ri-camera-3-fill', + }, + { + name: 'film', + keywords: 'movie video', + line: 'i-ri-film-line', + fill: 'i-ri-film-fill', + }, + { + name: 'music-2', + keywords: 'audio song music', + line: 'i-ri-music-2-line', + fill: 'i-ri-music-2-fill', + }, + { + name: 'headphone', + keywords: 'audio support', + line: 'i-ri-headphone-line', + fill: 'i-ri-headphone-fill', + }, + { + name: 'palette', + keywords: 'color design art', + line: 'i-ri-palette-line', + fill: 'i-ri-palette-fill', + }, + { + name: 'brush-3', + keywords: 'paint design brush', + line: 'i-ri-brush-3-line', + fill: 'i-ri-brush-3-fill', + }, + { + name: 'scissors-2', + keywords: 'cut', + line: 'i-ri-scissors-2-line', + fill: 'i-ri-scissors-2-fill', + }, + { + name: 'code-s-slash', + keywords: 'develop code programming', + line: 'i-ri-code-s-slash-line', + fill: 'i-ri-code-s-slash-fill', + }, + { + name: 'terminal-box', + keywords: 'console cli terminal', + line: 'i-ri-terminal-box-line', + fill: 'i-ri-terminal-box-fill', + }, + { + name: 'git-branch', + keywords: 'version code git', + line: 'i-ri-git-branch-line', + fill: 'i-ri-git-branch-fill', + }, + { + name: 'github', + keywords: 'git code', + line: 'i-ri-github-line', + fill: 'i-ri-github-fill', + }, + { + name: 'cpu', + keywords: 'chip processor', + line: 'i-ri-cpu-line', + fill: 'i-ri-cpu-fill', + }, + { + name: 'database-2', + keywords: 'storage data', + line: 'i-ri-database-2-line', + fill: 'i-ri-database-2-fill', + }, + { + name: 'server', + keywords: 'host backend', + line: 'i-ri-server-line', + fill: 'i-ri-server-fill', + }, + { + name: 'cloud', + keywords: 'storage cloud', + line: 'i-ri-cloud-line', + fill: 'i-ri-cloud-fill', + }, + { + name: 'wifi', + keywords: 'network internet', + line: 'i-ri-wifi-line', + fill: 'i-ri-wifi-fill', + }, + { + name: 'computer', + keywords: 'screen desktop monitor', + line: 'i-ri-computer-line', + fill: 'i-ri-computer-fill', + }, + { + name: 'smartphone', + keywords: 'mobile phone', + line: 'i-ri-smartphone-line', + fill: 'i-ri-smartphone-fill', + }, + { + name: 'macbook', + keywords: 'laptop computer', + line: 'i-ri-macbook-line', + fill: 'i-ri-macbook-fill', + }, + { + name: 'bug', + keywords: 'issue error bug', + line: 'i-ri-bug-line', + fill: 'i-ri-bug-fill', + }, + { + name: 'command', + keywords: 'keyboard shortcut', + line: 'i-ri-command-line', + fill: 'i-ri-command-fill', + }, + { + name: 'settings-3', + keywords: 'config gear options settings', + line: 'i-ri-settings-3-line', + fill: 'i-ri-settings-3-fill', + }, + { + name: 'equalizer', + keywords: 'controls adjust sliders filter', + line: 'i-ri-equalizer-line', + fill: 'i-ri-equalizer-fill', + }, + { + name: 'tools', + keywords: 'fix tools config', + line: 'i-ri-tools-line', + fill: 'i-ri-tools-fill', + }, + { + name: 'hammer', + keywords: 'build fix hammer', + line: 'i-ri-hammer-line', + fill: 'i-ri-hammer-fill', + }, + { + name: 'key-2', + keywords: 'password access key', + line: 'i-ri-key-2-line', + fill: 'i-ri-key-2-fill', + }, + { + name: 'lock', + keywords: 'secure private lock', + line: 'i-ri-lock-line', + fill: 'i-ri-lock-fill', + }, + { + name: 'lock-unlock', + keywords: 'open access', + line: 'i-ri-lock-unlock-line', + fill: 'i-ri-lock-unlock-fill', + }, + { + name: 'shield', + keywords: 'security protect', + line: 'i-ri-shield-line', + fill: 'i-ri-shield-fill', + }, + { + name: 'shield-check', + keywords: 'secure verified', + line: 'i-ri-shield-check-line', + fill: 'i-ri-shield-check-fill', + }, + { + name: 'eye', + keywords: 'view visible preview', + line: 'i-ri-eye-line', + fill: 'i-ri-eye-fill', + }, + { + name: 'search', + keywords: 'find lookup search', + line: 'i-ri-search-line', + fill: 'i-ri-search-fill', + }, + { + name: 'filter-3', + keywords: 'sort refine filter', + line: 'i-ri-filter-3-line', + fill: 'i-ri-filter-3-fill', + }, + { + name: 'links', + keywords: 'url chain link', + line: 'i-ri-links-line', + fill: 'i-ri-links-fill', + }, + { + name: 'share-forward', + keywords: 'share send distribute', + line: 'i-ri-share-forward-line', + fill: 'i-ri-share-forward-fill', + }, + { + name: 'plug', + keywords: 'connect integration', + line: 'i-ri-plug-line', + fill: 'i-ri-plug-fill', + }, + { + name: 'shut-down', + keywords: 'power on off', + line: 'i-ri-shut-down-line', + fill: 'i-ri-shut-down-fill', + }, + { + name: 'qr-scan-2', + keywords: 'qr scan', + line: 'i-ri-qr-scan-2-line', + fill: 'i-ri-qr-scan-2-fill', + }, + { + name: 'fingerprint', + keywords: 'identity auth fingerprint', + line: 'i-ri-fingerprint-line', + fill: 'i-ri-fingerprint-fill', + }, + { + name: 'bar-chart', + keywords: 'analytics stats graph bar', + line: 'i-ri-bar-chart-line', + fill: 'i-ri-bar-chart-fill', + }, + { + name: 'pie-chart', + keywords: 'analytics stats pie', + line: 'i-ri-pie-chart-line', + fill: 'i-ri-pie-chart-fill', + }, + { + name: 'line-chart', + keywords: 'analytics trend graph line', + line: 'i-ri-line-chart-line', + fill: 'i-ri-line-chart-fill', + }, + { + name: 'stock', + keywords: 'growth increase trending', + line: 'i-ri-stock-line', + fill: 'i-ri-stock-fill', + }, + { + name: 'pulse', + keywords: 'activity health monitor', + line: 'i-ri-pulse-line', + fill: 'i-ri-pulse-fill', + }, + { + name: 'dashboard-3', + keywords: 'speed performance dashboard gauge', + line: 'i-ri-dashboard-3-line', + fill: 'i-ri-dashboard-3-fill', + }, + { + name: 'check', + keywords: 'done complete ok check', + line: 'i-ri-check-line', + fill: 'i-ri-check-fill', + }, + { + name: 'checkbox-circle', + keywords: 'done success check', + line: 'i-ri-checkbox-circle-line', + fill: 'i-ri-checkbox-circle-fill', + }, + { + name: 'close-circle', + keywords: 'error fail close', + line: 'i-ri-close-circle-line', + fill: 'i-ri-close-circle-fill', + }, + { + name: 'information', + keywords: 'info information', + line: 'i-ri-information-line', + fill: 'i-ri-information-fill', + }, + { + name: 'error-warning', + keywords: 'warning alert', + line: 'i-ri-error-warning-line', + fill: 'i-ri-error-warning-fill', + }, + { + name: 'alert', + keywords: 'warning caution', + line: 'i-ri-alert-line', + fill: 'i-ri-alert-fill', + }, + { + name: 'question', + keywords: 'support faq help', + line: 'i-ri-question-line', + fill: 'i-ri-question-fill', + }, + { + name: 'layout-grid', + keywords: 'dashboard apps grid', + line: 'i-ri-layout-grid-line', + fill: 'i-ri-layout-grid-fill', + }, + { + name: 'table', + keywords: 'grid data table', + line: 'i-ri-table-line', + fill: 'i-ri-table-fill', + }, + { + name: 'function', + keywords: 'kanban board', + line: 'i-ri-function-line', + fill: 'i-ri-function-fill', + }, + { + name: 'map-2', + keywords: 'location navigate map', + line: 'i-ri-map-2-line', + fill: 'i-ri-map-2-fill', + }, + { + name: 'navigation', + keywords: 'direction gps', + line: 'i-ri-navigation-line', + fill: 'i-ri-navigation-fill', + }, + { + name: 'car', + keywords: 'drive vehicle car', + line: 'i-ri-car-line', + fill: 'i-ri-car-fill', + }, + { + name: 'truck', + keywords: 'delivery shipping', + line: 'i-ri-truck-line', + fill: 'i-ri-truck-fill', + }, + { + name: 'plane', + keywords: 'travel flight plane', + line: 'i-ri-plane-line', + fill: 'i-ri-plane-fill', + }, + { + name: 'bike', + keywords: 'cycle bike', + line: 'i-ri-bike-line', + fill: 'i-ri-bike-fill', + }, + { + name: 'anchor', + keywords: 'ship marine anchor', + line: 'i-ri-anchor-line', + fill: 'i-ri-anchor-fill', + }, + { + name: 'leaf', + keywords: 'eco nature green leaf', + line: 'i-ri-leaf-line', + fill: 'i-ri-leaf-fill', + }, + { + name: 'plant', + keywords: 'nature plant grow', + line: 'i-ri-plant-line', + fill: 'i-ri-plant-fill', + }, + { + name: 'sun', + keywords: 'day light weather sun', + line: 'i-ri-sun-line', + fill: 'i-ri-sun-fill', + }, + { + name: 'moon', + keywords: 'night dark moon', + line: 'i-ri-moon-line', + fill: 'i-ri-moon-fill', + }, + { + name: 'rainy', + keywords: 'weather rain', + line: 'i-ri-rainy-line', + fill: 'i-ri-rainy-fill', + }, + { + name: 'umbrella', + keywords: 'rain protect umbrella', + line: 'i-ri-umbrella-line', + fill: 'i-ri-umbrella-fill', + }, + { + name: 'landscape', + keywords: 'outdoor hike mountain', + line: 'i-ri-landscape-line', + fill: 'i-ri-landscape-fill', + }, + { + name: 'drop', + keywords: 'water drop', + line: 'i-ri-drop-line', + fill: 'i-ri-drop-fill', + }, + { + name: 'cup', + keywords: 'coffee drink cup', + line: 'i-ri-cup-line', + fill: 'i-ri-cup-fill', + }, + { + name: 'restaurant', + keywords: 'food eat restaurant', + line: 'i-ri-restaurant-line', + fill: 'i-ri-restaurant-fill', + }, + { + name: 'cake-3', + keywords: 'birthday celebrate cake', + line: 'i-ri-cake-3-line', + fill: 'i-ri-cake-3-fill', + }, + { + name: 'goblet', + keywords: 'drink wine bar', + line: 'i-ri-goblet-line', + fill: 'i-ri-goblet-fill', + }, + { + name: 'heart-pulse', + keywords: 'health medical', + line: 'i-ri-heart-pulse-line', + fill: 'i-ri-heart-pulse-fill', + }, + { + name: 'stethoscope', + keywords: 'health doctor medical', + line: 'i-ri-stethoscope-line', + fill: 'i-ri-stethoscope-fill', + }, + { + name: 'capsule', + keywords: 'medicine pill health', + line: 'i-ri-capsule-line', + fill: 'i-ri-capsule-fill', + }, + { + name: 'add', + keywords: 'add new create plus', + line: 'i-ri-add-line', + fill: 'i-ri-add-fill', + }, + { + name: 'delete-bin', + keywords: 'delete remove trash', + line: 'i-ri-delete-bin-line', + fill: 'i-ri-delete-bin-fill', + }, + { + name: 'download', + keywords: 'save export download', + line: 'i-ri-download-line', + fill: 'i-ri-download-fill', + }, + { + name: 'upload', + keywords: 'import upload', + line: 'i-ri-upload-line', + fill: 'i-ri-upload-fill', + }, + { + name: 'refresh', + keywords: 'reload sync update refresh', + line: 'i-ri-refresh-line', + fill: 'i-ri-refresh-fill', + }, + { + name: 'external-link', + keywords: 'open new external', + line: 'i-ri-external-link-line', + fill: 'i-ri-external-link-fill', + }, + { + name: 'logout-box', + keywords: 'exit signout logout', + line: 'i-ri-logout-box-line', + fill: 'i-ri-logout-box-fill', + }, + { + name: 'fullscreen', + keywords: 'expand maximize fullscreen', + line: 'i-ri-fullscreen-line', + fill: 'i-ri-fullscreen-fill', + }, + { + name: 'drag-move', + keywords: 'drag reorder move', + line: 'i-ri-drag-move-line', + fill: 'i-ri-drag-move-fill', + }, + { + name: 'paint-brush', + keywords: 'brush paint', + line: 'i-ri-paint-brush-line', + fill: 'i-ri-paint-brush-fill', + }, +]; diff --git a/app/javascript/dashboard/components-next/tabbar/TabBar.vue b/app/javascript/dashboard/components-next/tabbar/TabBar.vue index 6840bc9a9..301f51e07 100644 --- a/app/javascript/dashboard/components-next/tabbar/TabBar.vue +++ b/app/javascript/dashboard/components-next/tabbar/TabBar.vue @@ -81,6 +81,7 @@ const showDivider = index => {