mirror of
https://github.com/modrinth/code.git
synced 2026-09-02 04:56:52 +00:00
feat: app onboarding and library (#6923)
* refactor: remove unused legacy onboarded flag * sqlx prepare * feat: add onboarding checklist app db and add vue provider * feat: add welcome page * feat: add onboarding checklist component * feat: move checklist visibility source of truth in app db * feat: split up import modpack button * feat-small: style tweak * feat: remove discover section from homepage and add library * refactor: move welcome screen to component * refactor: move grid display into /library * refactor: rename existing components * refactor: split up the current library page files * feat: add create group modal * feat: add instance group definitions * feat: update accordian for instance groups * feat: add delete group * feat: implement rename instance groups * refactor: move instance group api out * feat: add context menu for groups * feat: style instance items in library * feat: add selected instance item state * feat: implement action bar for selection * feat: drag and drop to move between groups * feat: show ungrouped with header * feat: improve edit group name * small fix * small style fixes * feat: improve selection behaviour * fix: dont toggle group when context menu open * feat: drag and drop multiple selected * small * feat: add remove from group in action bar * feat: add delay on icon hiding in inline input * feat: improve icon hiding delay * fix: bad routes.js * feat: implement sort and filter options * refactor: dont prefix filenames * feat: change open/collapse group target * feat: improve ungrouped behaviour * feat: add shift to select multiple instances * fix: icon disappear transition * feat: add esc while dragging to cancel * feat: update sorting and moved label * feat: update context menu * fix: multiple context menus can be opened * refactor: instance groups to be identified by ID instead of name, names become display data * feat: creating new group always adds instance * feat: create group button, fix instance selection, and increase group name max length * feat: strokes * fix: show getting started checklist in all sidebar * feat: set size on microsoft login screen * fix: use ButtonStyled in Chips component * feat: creating groups with selected instances and fix some moving selection and duplication bugs * pnpm prepr * feat: add tooltip for instance already in group * feat: create instance with content in creation flow modal - search modpack becomes search projects - creation flow modal context receives prepareProjectInstall and createProjectInstall callbacks, from content-install.ts (shared with ContentInstallModal) * feat: update instance card styles * refactor: separate out instance card content * update styles * refactor: instance card * fix: width of instance * feat: update empty state * feat: update sort and grouping dropdowns * feat: better context menu options * feat: add group action buttons * feat: add/remove favourites group * remove manual sort * feat: improve the "New group" jank * feat: add group by instance type and change group sorting * feat: welcome screen styles match figma * feat: not signed in skins demo * feat: new hosting empty state * feat: new instance card style * feat: add tooltip and spice up animation more * feat: improve the transitions for groups after dropping many instances * feat: set instance group membership for bulk moving * feat: remove fade out * fix: allow dragging from buttons inside instance card * fix: library page load in behaviour * small fix * feat: add group ordering and reordering * feat: add drag to reorder groups * fix: switching page not clearing selected instances * style fixes * style fix * fix spacing * feat: jump in changes * feat: add styled newly created instances * feat: improve jump in cards styles * fix: welcome screen style * fix: empty state styles * feat: max 5 jump in items * feat: max new instances to 3 * fix: search icon * feat: clear selection after move * feat: skins list use check icon styles * feat: list will be deleted instances and default sort last played * fix: stroke * remove: no more group settings in instance general settings * fix: resolving modpack server install * feat: icon editor * feat: add three hue shifted variants * fix: surprise me doesnt pick symbol * feat: add customize icon to create instance modal * feat: skins loading state style * fix: welcome logo * fix: copy * fix: left over usage of ButtonStyled from main's refactor * feat: some leaked merge stuff * feat: server empty state styles * remove: instance helper text * remove: auto link in set up stage * feat: auto focus on search when modal open * fix: delete instance modal style * remove: modpack meta repeating * fix: icon * fix: font weight * fix: dropdown filter preview heights * pnpm prepr * fix: sign into modrinth flashing change in position * feat: add avatar transparent corner gets padding * feat: drag and hold over group opens group * feat: add random icon for custom instance * feat: update loading icon * polish: empty state for search * polish: jump in cards * fix: avatar could skip checking for image if tauri loads it first * feat: add icon customize and randomizer in content install modal for add to new instances * fix: avatar cannot read image pixels due to cors * refactor: popup notifications toast type for instance sharing notifications * feat: apply random icons modal and add popup notification for it * polish: style * fix: set_webview_visible * QA * qa * feat: add inner white 15 opacity stroke around avatar * add to catalog * pnpm prepr * fix: app event import * feat: add new icons and backgrounds for icon editor * feat: recents list generate from randomized icons and save icon in editor + icon editor modal fixes * qa * refactor: recipe -> config * fix: event bus * polish * feat: add safe guard against randomizing empty icons * feat: navigate to instance after creation * fix: icon file names * i18n on welcome screen * i18n pass on PR * fix: inconsistent max group name length * refactor: use sqlx macro * feat: add instance icon background selection drag * feat: update catalog order * fix: remove gradient for welcome screen * remove: frog * fix: min width * feat: update corner size * fix: context menu styles * prepr * fix: instance name trimming * reorder * fix: instance card loading state stayed until hovering * feat: add icon randomizer blacklist * blog * prepr --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
+466
@@ -0,0 +1,466 @@
|
||||
import { defineMessages } from '@modrinth/ui'
|
||||
|
||||
import backpack from '@/assets/instance-icons/backpack.png'
|
||||
import beacon from '@/assets/instance-icons/beacon.png'
|
||||
import blueShark from '@/assets/instance-icons/blue-shark.png'
|
||||
import bookshelf from '@/assets/instance-icons/bookshelf.png'
|
||||
import brownBear from '@/assets/instance-icons/brown-bear.png'
|
||||
import cake from '@/assets/instance-icons/cake.png'
|
||||
import campfire from '@/assets/instance-icons/campfire.png'
|
||||
import chest from '@/assets/instance-icons/chest.png'
|
||||
import cogwheel from '@/assets/instance-icons/cogwheel.png'
|
||||
import commandBlock from '@/assets/instance-icons/command-block.png'
|
||||
import cookingPot from '@/assets/instance-icons/cooking-pot.png'
|
||||
import couch from '@/assets/instance-icons/couch.png'
|
||||
import craftingTable from '@/assets/instance-icons/crafting-table.png'
|
||||
import creeper from '@/assets/instance-icons/creeper.png'
|
||||
import enchantingTable from '@/assets/instance-icons/enchanting-table.png'
|
||||
import enderChest from '@/assets/instance-icons/ender-chest.png'
|
||||
import enderDragon from '@/assets/instance-icons/ender-dragon.png'
|
||||
import engine from '@/assets/instance-icons/engine.png'
|
||||
import furnace from '@/assets/instance-icons/furnace.png'
|
||||
import gizmo from '@/assets/instance-icons/gizmo.png'
|
||||
import globe from '@/assets/instance-icons/globe.png'
|
||||
import grassBlock from '@/assets/instance-icons/grass-block.png'
|
||||
import lantern from '@/assets/instance-icons/lantern.png'
|
||||
import moobloom from '@/assets/instance-icons/moobloom.png'
|
||||
import mrPack from '@/assets/instance-icons/mr-pack.png'
|
||||
import orb from '@/assets/instance-icons/orb.png'
|
||||
import oxygenDistributor from '@/assets/instance-icons/oxygen-distributor.png'
|
||||
import pancakes from '@/assets/instance-icons/pancakes.png'
|
||||
import pickaxe from '@/assets/instance-icons/pickaxe.png'
|
||||
import pokeBall from '@/assets/instance-icons/poke-ball.png'
|
||||
import redstoneBlock from '@/assets/instance-icons/redstone-block.png'
|
||||
import sculkSensor from '@/assets/instance-icons/sculk-sensor.png'
|
||||
import skeleton from '@/assets/instance-icons/skeleton.png'
|
||||
import skillet from '@/assets/instance-icons/skillet.png'
|
||||
import slimeBlock from '@/assets/instance-icons/slime-block.png'
|
||||
import spaceHelmet from '@/assets/instance-icons/space-helmet.png'
|
||||
import stickyPiston from '@/assets/instance-icons/sticky-piston.png'
|
||||
import sword from '@/assets/instance-icons/sword.png'
|
||||
import terminal from '@/assets/instance-icons/terminal.png'
|
||||
import tinyPotato from '@/assets/instance-icons/tiny-potato.png'
|
||||
import tire from '@/assets/instance-icons/tire.png'
|
||||
import tnt from '@/assets/instance-icons/tnt.png'
|
||||
import wrench from '@/assets/instance-icons/wrench.png'
|
||||
import wrenchRinth from '@/assets/instance-icons/wrench-rinth.png'
|
||||
import zombie from '@/assets/instance-icons/zombie.png'
|
||||
|
||||
const names = defineMessages({
|
||||
yellow: {
|
||||
id: 'instance.icon-editor.background.yellow',
|
||||
defaultMessage: 'Yellow',
|
||||
},
|
||||
green: {
|
||||
id: 'instance.icon-editor.background.green',
|
||||
defaultMessage: 'Green',
|
||||
},
|
||||
lime: {
|
||||
id: 'instance.icon-editor.background.lime',
|
||||
defaultMessage: 'Lime',
|
||||
},
|
||||
darkGreen: {
|
||||
id: 'instance.icon-editor.background.dark-green',
|
||||
defaultMessage: 'Dark green',
|
||||
},
|
||||
purple: {
|
||||
id: 'instance.icon-editor.background.purple',
|
||||
defaultMessage: 'Purple',
|
||||
},
|
||||
blue: {
|
||||
id: 'instance.icon-editor.background.blue',
|
||||
defaultMessage: 'Blue',
|
||||
},
|
||||
orange: {
|
||||
id: 'instance.icon-editor.background.orange',
|
||||
defaultMessage: 'Orange',
|
||||
},
|
||||
red: {
|
||||
id: 'instance.icon-editor.background.red',
|
||||
defaultMessage: 'Red',
|
||||
},
|
||||
rose: {
|
||||
id: 'instance.icon-editor.background.rose',
|
||||
defaultMessage: 'Rose',
|
||||
},
|
||||
pink: {
|
||||
id: 'instance.icon-editor.background.pink',
|
||||
defaultMessage: 'Pink',
|
||||
},
|
||||
indigo: {
|
||||
id: 'instance.icon-editor.background.indigo',
|
||||
defaultMessage: 'Indigo',
|
||||
},
|
||||
lavender: {
|
||||
id: 'instance.icon-editor.background.lavender',
|
||||
defaultMessage: 'Lavender',
|
||||
},
|
||||
lightGray: {
|
||||
id: 'instance.icon-editor.background.light-gray',
|
||||
defaultMessage: 'Light gray',
|
||||
},
|
||||
gray: {
|
||||
id: 'instance.icon-editor.background.gray',
|
||||
defaultMessage: 'Gray',
|
||||
},
|
||||
darkGray: {
|
||||
id: 'instance.icon-editor.background.dark-gray',
|
||||
defaultMessage: 'Dark gray',
|
||||
},
|
||||
backpack: { id: 'instance.icon-editor.symbol.backpack', defaultMessage: 'Backpack' },
|
||||
beacon: { id: 'instance.icon-editor.symbol.beacon', defaultMessage: 'Beacon' },
|
||||
blueShark: { id: 'instance.icon-editor.symbol.blue-shark', defaultMessage: 'Blue Shark' },
|
||||
bookshelf: { id: 'instance.icon-editor.symbol.bookshelf', defaultMessage: 'Bookshelf' },
|
||||
brownBear: { id: 'instance.icon-editor.symbol.brown-bear', defaultMessage: 'Brown Bear' },
|
||||
cake: { id: 'instance.icon-editor.symbol.cake', defaultMessage: 'Cake' },
|
||||
campfire: { id: 'instance.icon-editor.symbol.campfire', defaultMessage: 'Campfire' },
|
||||
chest: { id: 'instance.icon-editor.symbol.chest', defaultMessage: 'Chest' },
|
||||
cogwheel: { id: 'instance.icon-editor.symbol.cogwheel', defaultMessage: 'Cogwheel' },
|
||||
commandBlock: {
|
||||
id: 'instance.icon-editor.symbol.command-block',
|
||||
defaultMessage: 'Command Block',
|
||||
},
|
||||
cookingPot: {
|
||||
id: 'instance.icon-editor.symbol.cooking-pot',
|
||||
defaultMessage: 'Cooking Pot',
|
||||
},
|
||||
couch: { id: 'instance.icon-editor.symbol.couch', defaultMessage: 'Couch' },
|
||||
craftingTable: {
|
||||
id: 'instance.icon-editor.symbol.crafting-table',
|
||||
defaultMessage: 'Crafting Table',
|
||||
},
|
||||
creeper: { id: 'instance.icon-editor.symbol.creeper', defaultMessage: 'Creeper' },
|
||||
enchantingTable: {
|
||||
id: 'instance.icon-editor.symbol.enchanting-table',
|
||||
defaultMessage: 'Enchanting Table',
|
||||
},
|
||||
enderChest: {
|
||||
id: 'instance.icon-editor.symbol.ender-chest',
|
||||
defaultMessage: 'Ender Chest',
|
||||
},
|
||||
enderDragon: {
|
||||
id: 'instance.icon-editor.symbol.ender-dragon',
|
||||
defaultMessage: 'Ender Dragon',
|
||||
},
|
||||
engine: { id: 'instance.icon-editor.symbol.engine', defaultMessage: 'Engine' },
|
||||
furnace: { id: 'instance.icon-editor.symbol.furnace', defaultMessage: 'Furnace' },
|
||||
gizmo: { id: 'instance.icon-editor.symbol.gizmo', defaultMessage: 'Gizmo' },
|
||||
globe: { id: 'instance.icon-editor.symbol.globe', defaultMessage: 'Globe' },
|
||||
grassBlock: {
|
||||
id: 'instance.icon-editor.symbol.grass-block',
|
||||
defaultMessage: 'Grass Block',
|
||||
},
|
||||
lantern: { id: 'instance.icon-editor.symbol.lantern', defaultMessage: 'Lantern' },
|
||||
moobloom: { id: 'instance.icon-editor.symbol.moobloom', defaultMessage: 'Moobloom' },
|
||||
mrPack: { id: 'instance.icon-editor.symbol.mr-pack', defaultMessage: 'Mr Pack' },
|
||||
orb: { id: 'instance.icon-editor.symbol.orb', defaultMessage: 'Orb' },
|
||||
oxygenDistributor: {
|
||||
id: 'instance.icon-editor.symbol.oxygen-distributor',
|
||||
defaultMessage: 'Oxygen Distributor',
|
||||
},
|
||||
pancakes: { id: 'instance.icon-editor.symbol.pancakes', defaultMessage: 'Pancakes' },
|
||||
pickaxe: { id: 'instance.icon-editor.symbol.pickaxe', defaultMessage: 'Pickaxe' },
|
||||
pokeBall: { id: 'instance.icon-editor.symbol.poke-ball', defaultMessage: 'Poke Ball' },
|
||||
redstoneBlock: {
|
||||
id: 'instance.icon-editor.symbol.redstone-block',
|
||||
defaultMessage: 'Redstone Block',
|
||||
},
|
||||
sculkSensor: {
|
||||
id: 'instance.icon-editor.symbol.sculk-sensor',
|
||||
defaultMessage: 'Sculk Sensor',
|
||||
},
|
||||
skeleton: { id: 'instance.icon-editor.symbol.skeleton', defaultMessage: 'Skeleton' },
|
||||
skillet: { id: 'instance.icon-editor.symbol.skillet', defaultMessage: 'Skillet' },
|
||||
slimeBlock: {
|
||||
id: 'instance.icon-editor.symbol.slime-block',
|
||||
defaultMessage: 'Slime Block',
|
||||
},
|
||||
spaceHelmet: {
|
||||
id: 'instance.icon-editor.symbol.space-helmet',
|
||||
defaultMessage: 'Space Helmet',
|
||||
},
|
||||
stickyPiston: {
|
||||
id: 'instance.icon-editor.symbol.sticky-piston',
|
||||
defaultMessage: 'Sticky Piston',
|
||||
},
|
||||
sword: { id: 'instance.icon-editor.symbol.sword', defaultMessage: 'Sword' },
|
||||
tnt: { id: 'instance.icon-editor.symbol.tnt', defaultMessage: 'TNT' },
|
||||
terminal: { id: 'instance.icon-editor.symbol.terminal', defaultMessage: 'Terminal' },
|
||||
tinyPotato: {
|
||||
id: 'instance.icon-editor.symbol.tiny-potato',
|
||||
defaultMessage: 'Tiny Potato',
|
||||
},
|
||||
tire: { id: 'instance.icon-editor.symbol.tire', defaultMessage: 'Tire' },
|
||||
wrench: { id: 'instance.icon-editor.symbol.create-wrench', defaultMessage: 'Wrench' },
|
||||
wrenchRinth: {
|
||||
id: 'instance.icon-editor.symbol.wrenth-rinth',
|
||||
defaultMessage: 'Modrinth Wrench',
|
||||
},
|
||||
zombie: { id: 'instance.icon-editor.symbol.zombie', defaultMessage: 'Zombie' },
|
||||
})
|
||||
|
||||
export const backgroundOptions = [
|
||||
{
|
||||
id: 'rose',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#D62E63',
|
||||
bottom_color: '#F95C62',
|
||||
},
|
||||
name: names.rose,
|
||||
},
|
||||
{
|
||||
id: 'orange',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#FF8D29',
|
||||
bottom_color: '#FFB452',
|
||||
},
|
||||
name: names.orange,
|
||||
},
|
||||
{
|
||||
id: 'yellow',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#FFC629',
|
||||
bottom_color: '#FFEE53',
|
||||
},
|
||||
name: names.yellow,
|
||||
},
|
||||
{
|
||||
id: 'lime',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#6FDA1D',
|
||||
bottom_color: '#CBFF50',
|
||||
},
|
||||
name: names.lime,
|
||||
},
|
||||
{
|
||||
id: 'green',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#0B9F21',
|
||||
bottom_color: '#4FD24B',
|
||||
},
|
||||
name: names.green,
|
||||
},
|
||||
// {
|
||||
// id: 'dark_green',
|
||||
// background: {
|
||||
// type: 'linear-top-down-gradient',
|
||||
// top_color: '#084C13',
|
||||
// bottom_color: '#327735',
|
||||
// },
|
||||
// name: names.darkGreen,
|
||||
// },
|
||||
// {
|
||||
// id: 'indigo',
|
||||
// background: {
|
||||
// type: 'linear-top-down-gradient',
|
||||
// top_color: '#3F00D3',
|
||||
// bottom_color: '#2659FE',
|
||||
// },
|
||||
// name: names.indigo,
|
||||
// },
|
||||
{
|
||||
id: 'purple',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#4739FF',
|
||||
bottom_color: '#6670FF',
|
||||
},
|
||||
name: names.purple,
|
||||
},
|
||||
{
|
||||
id: 'blue',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#227EFF',
|
||||
bottom_color: '#5EC1FF',
|
||||
},
|
||||
name: names.blue,
|
||||
},
|
||||
{
|
||||
id: 'lavender',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#C056FD',
|
||||
bottom_color: '#B889FF',
|
||||
},
|
||||
name: names.lavender,
|
||||
},
|
||||
{
|
||||
id: 'pink',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#F640C0',
|
||||
bottom_color: '#FF7BF1',
|
||||
},
|
||||
name: names.pink,
|
||||
},
|
||||
|
||||
// {
|
||||
// id: 'red',
|
||||
// background: {
|
||||
// type: 'linear-top-down-gradient',
|
||||
// top_color: '#F6111C',
|
||||
// bottom_color: '#F94548',
|
||||
// },
|
||||
// name: names.red,
|
||||
// },
|
||||
{
|
||||
id: 'light_gray',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#AEAEAE',
|
||||
bottom_color: '#D9D9D9',
|
||||
},
|
||||
name: names.lightGray,
|
||||
},
|
||||
{
|
||||
id: 'gray',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#373C4C',
|
||||
bottom_color: '#4C4F58',
|
||||
},
|
||||
name: names.gray,
|
||||
},
|
||||
{
|
||||
id: 'dark_gray',
|
||||
background: {
|
||||
type: 'linear-top-down-gradient',
|
||||
top_color: '#1B1D29',
|
||||
bottom_color: '#252731',
|
||||
},
|
||||
name: names.darkGray,
|
||||
},
|
||||
] as const
|
||||
|
||||
export const symbolOptions = [
|
||||
// Cobblemon: Poké Ball
|
||||
{ id: 'poke_ball', name: names.pokeBall, asset: pokeBall, category: 'modded' },
|
||||
|
||||
// Origins: Orb of Origins
|
||||
{ id: 'orb', name: names.orb, asset: orb, category: 'modded' },
|
||||
|
||||
// Farmer's Delight: Cooking Pot, Skillet
|
||||
{ id: 'cooking_pot', name: names.cookingPot, asset: cookingPot, category: 'modded' },
|
||||
{ id: 'skillet', name: names.skillet, asset: skillet, category: 'modded' },
|
||||
|
||||
// Supplementaries: Globe, Pancakes
|
||||
{ id: 'globe', name: names.globe, asset: globe, category: 'modded' },
|
||||
{ id: 'pancakes', name: names.pancakes, asset: pancakes, category: 'modded' },
|
||||
|
||||
// Sophisticated Backpacks: Backpack
|
||||
{ id: 'backpack', name: names.backpack, asset: backpack, category: 'modded' },
|
||||
|
||||
// Chipped: Chair
|
||||
{ id: 'couch', name: names.couch, asset: couch, category: 'modded' },
|
||||
|
||||
// Botania: Tiny Potato
|
||||
{ id: 'tiny_potato', name: names.tinyPotato, asset: tinyPotato, category: 'modded' },
|
||||
|
||||
// Blåhaj: Blue Shark
|
||||
{ id: 'blue_shark', name: names.blueShark, asset: blueShark, category: 'modded' },
|
||||
|
||||
// Other modded symbols: Brown Bear, Moobloom
|
||||
{ id: 'brown_bear', name: names.brownBear, asset: brownBear, category: 'modded' },
|
||||
{ id: 'moobloom', name: names.moobloom, asset: moobloom, category: 'modded' },
|
||||
|
||||
// Create: Wrench, Cogwheel
|
||||
{ id: 'create_wrench', name: names.wrench, asset: wrench, category: 'modded' },
|
||||
{ id: 'cogwheel', name: names.cogwheel, asset: cogwheel, category: 'modded' },
|
||||
|
||||
// Create Aeronautics: Engine, Tire
|
||||
{ id: 'engine', name: names.engine, asset: engine, category: 'modded' },
|
||||
{ id: 'tire', name: names.tire, asset: tire, category: 'modded' },
|
||||
|
||||
// Ad Astra: Oxygen Distributor, Space Helmet
|
||||
{
|
||||
id: 'oxygen_distributor',
|
||||
name: names.oxygenDistributor,
|
||||
asset: oxygenDistributor,
|
||||
category: 'modded',
|
||||
},
|
||||
{ id: 'space_helmet', name: names.spaceHelmet, asset: spaceHelmet, category: 'modded' },
|
||||
|
||||
// Miscellaneous: Gizmo, Terminal
|
||||
{ id: 'gizmo', name: names.gizmo, asset: gizmo, category: 'modded' },
|
||||
{ id: 'terminal', name: names.terminal, asset: terminal, category: 'modded' },
|
||||
|
||||
// Miscellaneous: Modrinth Wrench, Mr Pack
|
||||
{ id: 'wrenth_rinth', name: names.wrenchRinth, asset: wrenchRinth, category: 'modded' },
|
||||
{ id: 'mr_pack', name: names.mrPack, asset: mrPack, category: 'modded' },
|
||||
|
||||
/////////////////////////
|
||||
// vanilla ones
|
||||
/////////////////////////
|
||||
|
||||
{ id: 'grass_block', name: names.grassBlock, asset: grassBlock, category: 'vanilla' },
|
||||
|
||||
{ id: 'crafting_table', name: names.craftingTable, asset: craftingTable, category: 'vanilla' },
|
||||
{ id: 'furnace', name: names.furnace, asset: furnace, category: 'vanilla' },
|
||||
{ id: 'chest', name: names.chest, asset: chest, category: 'vanilla' },
|
||||
|
||||
{ id: 'bookshelf', name: names.bookshelf, asset: bookshelf, category: 'vanilla' },
|
||||
{
|
||||
id: 'redstone_block',
|
||||
name: names.redstoneBlock,
|
||||
asset: redstoneBlock,
|
||||
category: 'vanilla',
|
||||
},
|
||||
{
|
||||
id: 'sticky_piston',
|
||||
name: names.stickyPiston,
|
||||
asset: stickyPiston,
|
||||
category: 'vanilla',
|
||||
},
|
||||
{ id: 'slime_block', name: names.slimeBlock, asset: slimeBlock, category: 'vanilla' },
|
||||
{ id: 'cake', name: names.cake, asset: cake, category: 'vanilla' },
|
||||
{ id: 'campfire', name: names.campfire, asset: campfire, category: 'vanilla' },
|
||||
{ id: 'pickaxe', name: names.pickaxe, asset: pickaxe, category: 'vanilla' },
|
||||
{ id: 'sword', name: names.sword, asset: sword, category: 'vanilla' },
|
||||
{ id: 'zombie', name: names.zombie, asset: zombie, category: 'vanilla' },
|
||||
{ id: 'creeper', name: names.creeper, asset: creeper, category: 'vanilla' },
|
||||
{ id: 'skeleton', name: names.skeleton, asset: skeleton, category: 'vanilla' },
|
||||
{ id: 'ender_dragon', name: names.enderDragon, asset: enderDragon, category: 'vanilla' },
|
||||
{ id: 'ender_chest', name: names.enderChest, asset: enderChest, category: 'vanilla' },
|
||||
{ id: 'sculk_sensor', name: names.sculkSensor, asset: sculkSensor, category: 'vanilla' },
|
||||
{ id: 'beacon', name: names.beacon, asset: beacon, category: 'vanilla' },
|
||||
{
|
||||
id: 'enchanting_table',
|
||||
name: names.enchantingTable,
|
||||
asset: enchantingTable,
|
||||
category: 'vanilla',
|
||||
},
|
||||
{ id: 'lantern', name: names.lantern, asset: lantern, category: 'vanilla' },
|
||||
{ id: 'tnt', name: names.tnt, asset: tnt, category: 'vanilla' },
|
||||
{ id: 'command_block', name: names.commandBlock, asset: commandBlock, category: 'vanilla' },
|
||||
] as const
|
||||
|
||||
export type BackgroundId = (typeof backgroundOptions)[number]['id']
|
||||
export type SymbolId = (typeof symbolOptions)[number]['id']
|
||||
|
||||
export const RANDOM_CONFIG_BLACKLIST = [
|
||||
{ background: 'purple', symbol: 'globe' },
|
||||
{ background: 'blue', symbol: 'globe' },
|
||||
{ background: 'gray', symbol: 'cogwheel' },
|
||||
{ background: 'dark_gray', symbol: 'cogwheel' },
|
||||
{ background: 'rose', symbol: 'poke_ball' },
|
||||
{ background: 'lime', symbol: 'slime_block' },
|
||||
{ background: 'green', symbol: 'slime_block' },
|
||||
{ background: 'rose', symbol: 'redstone_block' },
|
||||
{ background: 'rose', symbol: 'couch' },
|
||||
{ background: 'orange', symbol: 'space_helmet' },
|
||||
{ background: 'rose', symbol: 'tnt' },
|
||||
{ background: 'yellow', symbol: 'moobloom' },
|
||||
{ background: 'green', symbol: 'wrenth_rinth' },
|
||||
{ background: 'lime', symbol: 'mr_pack' },
|
||||
{ background: 'light_gray', symbol: 'skillet' },
|
||||
{ background: 'light_gray', symbol: 'cooking_pot' },
|
||||
] satisfies readonly { background: BackgroundId; symbol: SymbolId }[]
|
||||
|
||||
export const DEFAULT_BACKGROUND_ID = 'purple' satisfies BackgroundId
|
||||
export const DEFAULT_SYMBOL_ID = 'grass_block' satisfies SymbolId
|
||||
@@ -0,0 +1,537 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, InfoIcon, RefreshCwIcon, SaveIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import { toError } from '@/helpers/errors'
|
||||
import {
|
||||
cache_generated_icon,
|
||||
edit_generated_icon,
|
||||
edit_generated_icon_if_empty,
|
||||
get_recent_icon_configs,
|
||||
} from '@/helpers/instance'
|
||||
import type { IconBackground, InstanceIconConfig } from '@/helpers/types'
|
||||
|
||||
import {
|
||||
type BackgroundId,
|
||||
backgroundOptions,
|
||||
DEFAULT_BACKGROUND_ID,
|
||||
DEFAULT_SYMBOL_ID,
|
||||
RANDOM_CONFIG_BLACKLIST,
|
||||
type SymbolId,
|
||||
symbolOptions,
|
||||
} from './editor-catalog'
|
||||
|
||||
const props = defineProps<{
|
||||
instanceId?: string
|
||||
config?: InstanceIconConfig | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
saved: [iconPath: string, config: InstanceIconConfig]
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const modal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const recentConfigs = ref<InstanceIconConfig[]>([])
|
||||
const saving = ref(false)
|
||||
const backgroundScroller = ref<HTMLElement | null>(null)
|
||||
const showLeftBackgroundShadow = ref(false)
|
||||
const showRightBackgroundShadow = ref(false)
|
||||
const draggingBackgrounds = ref(false)
|
||||
|
||||
let backgroundScrollerResizeObserver: ResizeObserver | null = null
|
||||
let backgroundDragPointerId: number | null = null
|
||||
let backgroundDragCaptureTarget: Element | null = null
|
||||
let backgroundDragStartX = 0
|
||||
let backgroundDragStartScrollLeft = 0
|
||||
let suppressBackgroundClick = false
|
||||
let suppressBackgroundClickTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const selectedBackground = ref<BackgroundId>(DEFAULT_BACKGROUND_ID)
|
||||
const selectedSymbol = ref<SymbolId>(DEFAULT_SYMBOL_ID)
|
||||
|
||||
const selectedBackgroundOption = computed(
|
||||
() => backgroundOptions.find((option) => option.id === selectedBackground.value)!,
|
||||
)
|
||||
const selectedSymbolOption = computed(
|
||||
() => symbolOptions.find((option) => option.id === selectedSymbol.value)!,
|
||||
)
|
||||
const vanillaSymbolStartIndex = symbolOptions.findIndex((option) => option.category === 'vanilla')
|
||||
const selectedConfig = computed<InstanceIconConfig>(() => ({
|
||||
background: { ...selectedBackgroundOption.value.background },
|
||||
symbol: selectedSymbol.value,
|
||||
}))
|
||||
const visibleRecentConfigs = computed(() =>
|
||||
recentConfigs.value.filter(
|
||||
(config) => backgroundOption(config.background) && symbolOption(config.symbol),
|
||||
),
|
||||
)
|
||||
|
||||
function updateBackgroundScrollShadows() {
|
||||
const el = backgroundScroller.value
|
||||
if (!el) {
|
||||
showLeftBackgroundShadow.value = false
|
||||
showRightBackgroundShadow.value = false
|
||||
return
|
||||
}
|
||||
|
||||
showLeftBackgroundShadow.value = el.scrollLeft > 0
|
||||
showRightBackgroundShadow.value = el.scrollLeft < el.scrollWidth - el.clientWidth - 1
|
||||
}
|
||||
|
||||
function onBackgroundWheel(event: WheelEvent) {
|
||||
const el = backgroundScroller.value
|
||||
if (!el || el.scrollWidth <= el.clientWidth) return
|
||||
|
||||
const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY
|
||||
el.scrollLeft += delta
|
||||
}
|
||||
|
||||
function onBackgroundPointerDown(event: PointerEvent) {
|
||||
const el = backgroundScroller.value
|
||||
if (!el || event.pointerType === 'touch' || event.button !== 0) return
|
||||
|
||||
backgroundDragPointerId = event.pointerId
|
||||
backgroundDragStartX = event.clientX
|
||||
backgroundDragStartScrollLeft = el.scrollLeft
|
||||
suppressBackgroundClick = false
|
||||
backgroundDragCaptureTarget =
|
||||
event.target instanceof Element ? (event.target.closest('button') ?? el) : el
|
||||
backgroundDragCaptureTarget.setPointerCapture(event.pointerId)
|
||||
}
|
||||
|
||||
function onBackgroundPointerMove(event: PointerEvent) {
|
||||
const el = backgroundScroller.value
|
||||
if (!el || event.pointerId !== backgroundDragPointerId) return
|
||||
|
||||
const distance = event.clientX - backgroundDragStartX
|
||||
if (!draggingBackgrounds.value && Math.abs(distance) < 4) return
|
||||
|
||||
draggingBackgrounds.value = true
|
||||
suppressBackgroundClick = true
|
||||
event.preventDefault()
|
||||
el.scrollLeft = backgroundDragStartScrollLeft - distance
|
||||
}
|
||||
|
||||
function finishBackgroundDrag(event: PointerEvent) {
|
||||
const el = backgroundScroller.value
|
||||
if (!el || event.pointerId !== backgroundDragPointerId) return
|
||||
|
||||
if (backgroundDragCaptureTarget?.hasPointerCapture(event.pointerId)) {
|
||||
backgroundDragCaptureTarget.releasePointerCapture(event.pointerId)
|
||||
}
|
||||
backgroundDragPointerId = null
|
||||
backgroundDragCaptureTarget = null
|
||||
draggingBackgrounds.value = false
|
||||
|
||||
if (suppressBackgroundClick) {
|
||||
if (suppressBackgroundClickTimeout) clearTimeout(suppressBackgroundClickTimeout)
|
||||
suppressBackgroundClickTimeout = setTimeout(() => {
|
||||
suppressBackgroundClick = false
|
||||
suppressBackgroundClickTimeout = null
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function onBackgroundClick(event: MouseEvent) {
|
||||
if (!suppressBackgroundClick) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
suppressBackgroundClick = false
|
||||
if (suppressBackgroundClickTimeout) clearTimeout(suppressBackgroundClickTimeout)
|
||||
suppressBackgroundClickTimeout = null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
backgroundScrollerResizeObserver = new ResizeObserver(updateBackgroundScrollShadows)
|
||||
if (backgroundScroller.value) backgroundScrollerResizeObserver.observe(backgroundScroller.value)
|
||||
nextTick(updateBackgroundScrollShadows)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
backgroundScrollerResizeObserver?.disconnect()
|
||||
if (suppressBackgroundClickTimeout) clearTimeout(suppressBackgroundClickTimeout)
|
||||
})
|
||||
|
||||
function backgroundOption(background?: IconBackground) {
|
||||
if (background?.type !== 'linear-top-down-gradient') return undefined
|
||||
return backgroundOptions.find(
|
||||
(option) =>
|
||||
option.background.top_color === background.top_color &&
|
||||
option.background.bottom_color === background.bottom_color,
|
||||
)
|
||||
}
|
||||
|
||||
function backgroundStyle(background: IconBackground) {
|
||||
if (background.type === 'color') return { backgroundColor: background.value }
|
||||
return {
|
||||
backgroundImage: `linear-gradient(to bottom, ${background.top_color}, ${background.bottom_color})`,
|
||||
}
|
||||
}
|
||||
|
||||
function backgroundKey(background: IconBackground) {
|
||||
if (background.type === 'color') return `${background.type}-${background.value}`
|
||||
return `${background.type}-${background.top_color}-${background.bottom_color}`
|
||||
}
|
||||
|
||||
function symbolOption(symbol: string) {
|
||||
return symbolOptions.find((option) => option.id === symbol)
|
||||
}
|
||||
|
||||
function selectRecent(config: InstanceIconConfig) {
|
||||
const background = backgroundOption(config.background)
|
||||
const symbol = symbolOption(config.symbol)
|
||||
if (!background || !symbol) return
|
||||
|
||||
selectedBackground.value = background.id
|
||||
selectedSymbol.value = symbol.id
|
||||
}
|
||||
|
||||
function surpriseMe() {
|
||||
const configurations = backgroundOptions.flatMap((background) =>
|
||||
symbolOptions
|
||||
.filter(
|
||||
(symbol) =>
|
||||
background.id !== selectedBackground.value &&
|
||||
symbol.id !== selectedSymbol.value &&
|
||||
!RANDOM_CONFIG_BLACKLIST.some(
|
||||
(config) => config.background === background.id && config.symbol === symbol.id,
|
||||
),
|
||||
)
|
||||
.map((symbol) => ({ background: background.id, symbol: symbol.id })),
|
||||
)
|
||||
const configuration = configurations[Math.floor(Math.random() * configurations.length)]
|
||||
|
||||
if (!configuration) return
|
||||
selectedBackground.value = configuration.background
|
||||
selectedSymbol.value = configuration.symbol
|
||||
}
|
||||
|
||||
async function loadRecents() {
|
||||
try {
|
||||
recentConfigs.value = await get_recent_icon_configs()
|
||||
} catch (error) {
|
||||
handleError(toError(error))
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
selectedBackground.value = backgroundOption(props.config?.background)?.id ?? DEFAULT_BACKGROUND_ID
|
||||
selectedSymbol.value = symbolOption(props.config?.symbol ?? '')?.id ?? DEFAULT_SYMBOL_ID
|
||||
modal.value?.show()
|
||||
void loadRecents()
|
||||
nextTick(updateBackgroundScrollShadows)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
async function loadSymbolBytes(asset: string): Promise<number[]> {
|
||||
const response = await fetch(asset)
|
||||
if (!response.ok) throw new Error('Failed to load the icon symbol.')
|
||||
|
||||
return Array.from(new Uint8Array(await response.arrayBuffer()))
|
||||
}
|
||||
|
||||
async function saveIcon() {
|
||||
if (saving.value) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const config = selectedConfig.value
|
||||
const symbolBytes = await loadSymbolBytes(selectedSymbolOption.value.asset)
|
||||
const iconPath = props.instanceId
|
||||
? await edit_generated_icon(props.instanceId, config, symbolBytes)
|
||||
: await cache_generated_icon(config, symbolBytes, true)
|
||||
emit('saved', iconPath, config)
|
||||
saving.value = false
|
||||
await nextTick()
|
||||
hide()
|
||||
} catch (error) {
|
||||
handleError(toError(error))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function randomizeAndSave() {
|
||||
try {
|
||||
surpriseMe()
|
||||
const config = selectedConfig.value
|
||||
const iconPath = await cache_generated_icon(
|
||||
config,
|
||||
await loadSymbolBytes(selectedSymbolOption.value.asset),
|
||||
)
|
||||
return { iconPath, config }
|
||||
} catch (error) {
|
||||
handleError(toError(error))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function applyGeneratedIcon(instanceId: string, config: InstanceIconConfig) {
|
||||
try {
|
||||
const symbol = symbolOption(config.symbol)
|
||||
if (!backgroundOption(config.background) || !symbol) return false
|
||||
|
||||
await edit_generated_icon_if_empty(instanceId, config, await loadSymbolBytes(symbol.asset))
|
||||
return true
|
||||
} catch (error) {
|
||||
handleError(toError(error))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ show, hide, randomize: randomizeAndSave, randomizeAndSave, applyGeneratedIcon })
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'instance.icon-editor.title',
|
||||
defaultMessage: 'Icon editor',
|
||||
},
|
||||
background: {
|
||||
id: 'instance.icon-editor.background',
|
||||
defaultMessage: 'Background',
|
||||
},
|
||||
symbol: {
|
||||
id: 'instance.icon-editor.symbol',
|
||||
defaultMessage: 'Symbol',
|
||||
},
|
||||
surpriseMe: {
|
||||
id: 'instance.icon-editor.surprise-me',
|
||||
defaultMessage: 'Randomize',
|
||||
},
|
||||
recents: {
|
||||
id: 'instance.icon-editor.recents',
|
||||
defaultMessage: 'Recents',
|
||||
},
|
||||
description: {
|
||||
id: 'instance.icon-editor.description',
|
||||
defaultMessage: 'Mix and match elements to create a custom icon.',
|
||||
},
|
||||
saveIcon: {
|
||||
id: 'instance.icon-editor.save',
|
||||
defaultMessage: 'Save icon',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal
|
||||
ref="modal"
|
||||
:header="formatMessage(messages.title)"
|
||||
width="928px"
|
||||
max-width="calc(100vw - 2rem)"
|
||||
no-padding
|
||||
actions-divider
|
||||
:disable-close="saving"
|
||||
>
|
||||
<div class="flex h-[552px] max-h-[calc(100vh-168px)] min-h-0">
|
||||
<aside
|
||||
class="flex w-[244px] shrink-0 flex-col gap-4 overflow-y-auto border-0 border-r border-solid border-surface-5 p-6"
|
||||
>
|
||||
<div
|
||||
class="flex w-full flex-col items-center gap-3 rounded-[20px] border border-solid border-surface-4 bg-surface-2 p-4"
|
||||
>
|
||||
<div
|
||||
class="icon-outline relative size-[132px] overflow-hidden rounded-[20px]"
|
||||
:style="backgroundStyle(selectedBackgroundOption.background)"
|
||||
>
|
||||
<img :src="selectedSymbolOption.asset" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div
|
||||
class="icon-outline relative size-12 overflow-hidden rounded-2xl"
|
||||
:style="backgroundStyle(selectedBackgroundOption.background)"
|
||||
>
|
||||
<img :src="selectedSymbolOption.asset" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
<div
|
||||
class="icon-outline relative size-8 overflow-hidden rounded-[10px]"
|
||||
:style="backgroundStyle(selectedBackgroundOption.background)"
|
||||
>
|
||||
<img :src="selectedSymbolOption.asset" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
<div
|
||||
class="icon-outline relative size-4 overflow-hidden rounded-[5px]"
|
||||
:style="backgroundStyle(selectedBackgroundOption.background)"
|
||||
>
|
||||
<img :src="selectedSymbolOption.asset" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button class="w-full !shadow-none" @click="surpriseMe">
|
||||
<RefreshCwIcon />
|
||||
{{ formatMessage(messages.surpriseMe) }}
|
||||
</Button>
|
||||
|
||||
<div v-if="visibleRecentConfigs.length" class="flex flex-col gap-2.5">
|
||||
<span class="font-semibold text-contrast">{{ formatMessage(messages.recents) }}</span>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<button
|
||||
v-for="(recentConfig, index) in visibleRecentConfigs"
|
||||
:key="`${backgroundKey(recentConfig.background)}-${recentConfig.symbol}`"
|
||||
class="icon-outline relative size-10 cursor-pointer overflow-hidden rounded-xl border-0 p-0 transition-transform hover:scale-105"
|
||||
:style="backgroundStyle(recentConfig.background)"
|
||||
:aria-label="`${formatMessage(messages.recents)} ${index + 1}`"
|
||||
@click="selectRecent(recentConfig)"
|
||||
>
|
||||
<img
|
||||
:src="symbolOption(recentConfig.symbol)?.asset"
|
||||
alt=""
|
||||
class="size-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="min-w-0 flex-1 overflow-y-auto bg-surface-2">
|
||||
<section class="border-0 border-b border-solid border-surface-5 p-4">
|
||||
<h3 class="m-0 mb-3 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.background) }}
|
||||
</h3>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="background-scroll-shadow-left pointer-events-none absolute bottom-0 -left-0.5 top-0 z-10 w-8 bg-surface-2 transition-opacity duration-200"
|
||||
:class="showLeftBackgroundShadow ? 'opacity-100' : 'opacity-0'"
|
||||
/>
|
||||
<div
|
||||
ref="backgroundScroller"
|
||||
class="flex w-full select-none gap-2.5 overflow-x-auto overflow-y-hidden pb-2 pr-6"
|
||||
:class="{ 'cursor-grabbing': draggingBackgrounds }"
|
||||
@pointerdown="onBackgroundPointerDown"
|
||||
@pointermove="onBackgroundPointerMove"
|
||||
@pointerup="finishBackgroundDrag"
|
||||
@pointercancel="finishBackgroundDrag"
|
||||
@click.capture="onBackgroundClick"
|
||||
@wheel.prevent="onBackgroundWheel"
|
||||
@scroll="updateBackgroundScrollShadows"
|
||||
>
|
||||
<button
|
||||
v-for="option in backgroundOptions"
|
||||
:key="option.id"
|
||||
class="icon-option icon-outline relative aspect-square w-[calc((100%_-_3.125rem)/6)] shrink-0 cursor-pointer rounded-[20px] border-0 p-0"
|
||||
:class="{ 'icon-outline-selected': selectedBackground === option.id }"
|
||||
:style="backgroundStyle(option.background)"
|
||||
:aria-label="formatMessage(option.name)"
|
||||
:aria-pressed="selectedBackground === option.id"
|
||||
@click="selectedBackground = option.id"
|
||||
>
|
||||
<span
|
||||
v-if="selectedBackground === option.id"
|
||||
class="absolute right-1.5 top-1.5 flex size-6 items-center justify-center rounded-full bg-white/80 text-black"
|
||||
>
|
||||
<CheckIcon class="size-4" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="background-scroll-shadow-right pointer-events-none absolute bottom-0 right-0 top-0 z-10 w-8 bg-surface-2 transition-opacity duration-200"
|
||||
:class="showRightBackgroundShadow ? 'opacity-100' : 'opacity-0'"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="p-4">
|
||||
<h3 class="m-0 mb-3 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.symbol) }}
|
||||
</h3>
|
||||
<div class="grid grid-cols-6 gap-2.5">
|
||||
<template v-for="(option, index) in symbolOptions" :key="option.id">
|
||||
<hr
|
||||
v-if="index === vanillaSymbolStartIndex"
|
||||
class="col-span-6 my-2.5 mx-1 w-full border-0 border-t border-solid border-surface-5"
|
||||
/>
|
||||
<button
|
||||
v-tooltip="{
|
||||
content: formatMessage(option.name),
|
||||
delay: { show: 500, hide: 0 },
|
||||
}"
|
||||
class="icon-option icon-outline relative aspect-square cursor-pointer overflow-hidden rounded-[20px] border-0 bg-transparent p-0"
|
||||
:class="{ 'icon-outline-selected': selectedSymbol === option.id }"
|
||||
:aria-label="formatMessage(option.name)"
|
||||
:aria-pressed="selectedSymbol === option.id"
|
||||
@click="selectedSymbol = option.id"
|
||||
>
|
||||
<img :src="option.asset" alt="" class="size-full object-cover" />
|
||||
<span
|
||||
v-if="selectedSymbol === option.id"
|
||||
class="absolute right-1.5 top-1.5 flex size-6 items-center justify-center rounded-full bg-white/80 text-black"
|
||||
>
|
||||
<CheckIcon class="size-4" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<div class="flex items-center justify-between gap-4 px-2">
|
||||
<div class="flex min-w-0 items-center gap-2 text-primary">
|
||||
<InfoIcon class="size-6 shrink-0 text-blue" />
|
||||
<span>{{ formatMessage(messages.description) }}</span>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Button :disabled="saving" type="outlined" @click="hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" :disabled="saving" @click="saveIcon">
|
||||
<SpinnerIcon v-if="saving" class="animate-spin" />
|
||||
<SaveIcon v-else />
|
||||
{{ formatMessage(messages.saveIcon) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-outline {
|
||||
outline: 1px solid color-mix(in srgb, var(--color-text-primary) 15%, transparent);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.icon-option:not(.icon-outline-selected):hover {
|
||||
outline-color: color-mix(in srgb, var(--color-text-primary) 30%, transparent);
|
||||
}
|
||||
|
||||
.icon-option {
|
||||
transition: outline-color 150ms ease;
|
||||
}
|
||||
|
||||
.cursor-grabbing,
|
||||
.cursor-grabbing * {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
.icon-outline-selected {
|
||||
outline-color: color-mix(in srgb, var(--color-text-primary) 60%, transparent);
|
||||
}
|
||||
|
||||
.background-scroll-shadow-left {
|
||||
-webkit-mask-image: linear-gradient(to right, black, transparent);
|
||||
mask-image: linear-gradient(to right, black, transparent);
|
||||
}
|
||||
|
||||
.background-scroll-shadow-right {
|
||||
-webkit-mask-image: linear-gradient(to left, black, transparent);
|
||||
mask-image: linear-gradient(to left, black, transparent);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user