add context menu ui components and i18n

This commit is contained in:
Justin Edmund 2025-09-30 03:42:14 -07:00
parent f325836bf6
commit 030c5916c7
5 changed files with 310 additions and 25 deletions

View file

@ -20,5 +20,9 @@
"skill_selection_title": "Select Skill",
"skill_selection_search_placeholder": "Search skills...",
"extra_weapons": "Additional Weapons"
"extra_weapons": "Additional Weapons",
"context_view_details": "View Details",
"context_replace": "Replace",
"context_remove": "Remove"
}

View file

@ -20,5 +20,9 @@
"skill_selection_title": "スキル選択",
"skill_selection_search_placeholder": "スキルを検索...",
"extra_weapons": "追加武器"
"extra_weapons": "追加武器",
"context_view_details": "詳細を見る",
"context_replace": "交換",
"context_remove": "削除"
}

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.82709 1.22031C6.6779 0.923646 7.25659 0.929271 8.10834 1.22226C8.28529 1.28333 8.40794 1.44526 8.42767 1.63144L8.50887 2.40014C8.51296 2.43877 8.53908 2.47146 8.57565 2.4846C9.12387 2.68153 9.62546 2.97629 10.0594 3.34777C10.0894 3.3734 10.1314 3.37969 10.1673 3.36333L10.9042 3.02695C11.075 2.94891 11.2766 2.97187 11.4189 3.09433C12.101 3.68273 12.3854 4.18691 12.5576 5.07089C12.5932 5.25466 12.5146 5.44117 12.3632 5.55136L11.6928 6.03856C11.6615 6.06128 11.6462 6.09998 11.6529 6.13806C11.6999 6.40606 11.7255 6.68212 11.7255 6.96347C11.7255 7.24471 11.7 7.52031 11.653 7.78815C11.6463 7.82661 11.6619 7.86566 11.6937 7.88828L12.4355 8.41562C12.5998 8.53263 12.6803 8.73841 12.6279 8.9332C12.4111 9.73632 12.1217 10.2374 11.5185 10.8492C11.3772 10.9925 11.1605 11.0283 10.9765 10.9469L10.146 10.5782C10.1105 10.5624 10.0692 10.5687 10.0395 10.5938C9.61438 10.9539 9.12567 11.2404 8.59192 11.4344C8.55553 11.4476 8.52957 11.4802 8.5255 11.5187L8.4306 12.4166C8.40927 12.6166 8.26965 12.7865 8.07513 12.8375C7.24375 13.054 6.66499 13.0541 5.86127 12.8404C5.66623 12.7885 5.528 12.6165 5.50873 12.4156L5.42401 11.5256C5.42029 11.4866 5.39407 11.4534 5.35717 11.4401C4.82678 11.2493 4.34036 10.9667 3.91641 10.6119C3.88675 10.587 3.84557 10.5809 3.81024 10.5967L3.02435 10.9469C2.84037 11.0286 2.62383 10.9924 2.48236 10.8492C1.87907 10.2374 1.58981 9.73627 1.37299 8.9332C1.32052 8.73823 1.40077 8.5326 1.56537 8.41562L2.24705 7.93064C2.27912 7.90783 2.29473 7.86835 2.28761 7.82964C2.23596 7.54886 2.20897 7.25912 2.20892 6.96347C2.20892 6.66765 2.23608 6.37751 2.28776 6.09655C2.29481 6.05823 2.27958 6.01911 2.24807 5.99619L1.63666 5.55136C1.48547 5.44111 1.40668 5.25458 1.44232 5.07089C1.61447 4.18667 1.89855 3.68289 2.58099 3.09433C2.72342 2.97152 2.92562 2.94866 3.09662 3.02695L3.79001 3.34393C3.82567 3.36023 3.86752 3.35411 3.89746 3.32879C4.32988 2.96314 4.82825 2.67348 5.37244 2.47985C5.40953 2.46666 5.43592 2.43338 5.43963 2.3942L5.51166 1.63339C5.52935 1.44623 5.64962 1.28229 5.82709 1.22031ZM6.96771 5.05917C5.91663 5.05917 5.06452 5.91144 5.06439 6.96249C5.06439 8.01366 5.91655 8.86581 6.96771 8.86581C8.01866 8.86556 8.87103 8.0135 8.87103 6.96249C8.8709 5.91159 8.01858 5.05943 6.96771 5.05917Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,30 +1,61 @@
<script lang="ts">
import { ContextMenu as ContextMenuBase } from 'bits-ui'
import { ContextMenu as ContextMenuBase, DropdownMenu as DropdownMenuBase } from 'bits-ui'
import type { Snippet } from 'svelte'
import gearIcon from '$src/assets/icons/gear.svg'
interface ContextMenuProps {
children: Snippet
menu: Snippet
contextMenu: Snippet
dropdownMenu: Snippet
showGearButton?: boolean
}
let { children, menu }: ContextMenuProps = $props()
let { children, contextMenu, dropdownMenu, showGearButton = false }: ContextMenuProps = $props()
let gearMenuOpen = $state(false)
let contextMenuOpen = $state(false)
</script>
<ContextMenuBase.Root>
<ContextMenuBase.Trigger>
{#snippet child({ props })}
<div {...props}>
{@render children()}
</div>
{/snippet}
</ContextMenuBase.Trigger>
<div class="context-menu-container">
<ContextMenuBase.Root bind:open={contextMenuOpen}>
<ContextMenuBase.Trigger>
{#snippet child({ props })}
<div class="context-trigger" {...props}>
{@render children()}
</div>
{/snippet}
</ContextMenuBase.Trigger>
<ContextMenuBase.Portal>
<ContextMenuBase.Content class="context-menu">
{@render menu()}
</ContextMenuBase.Content>
</ContextMenuBase.Portal>
</ContextMenuBase.Root>
<ContextMenuBase.Portal>
<ContextMenuBase.Content class="context-menu">
{@render contextMenu()}
</ContextMenuBase.Content>
</ContextMenuBase.Portal>
</ContextMenuBase.Root>
{#if showGearButton}
<DropdownMenuBase.Root bind:open={gearMenuOpen}>
<DropdownMenuBase.Trigger class="gear-button-trigger">
{#snippet child({ props })}
<button
{...props}
class="gear-button"
oncontextmenu={(e) => e.preventDefault()}
type="button"
>
<img src={gearIcon} alt="Options" />
</button>
{/snippet}
</DropdownMenuBase.Trigger>
<DropdownMenuBase.Portal>
<DropdownMenuBase.Content class="dropdown-menu" side="bottom" align="start" sideOffset={4}>
{@render dropdownMenu()}
</DropdownMenuBase.Content>
</DropdownMenuBase.Portal>
</DropdownMenuBase.Root>
{/if}
</div>
<style lang="scss">
@use '$src/themes/spacing' as *;
@ -32,6 +63,77 @@
@use '$src/themes/typography' as *;
@use '$src/themes/effects' as *;
.context-menu-container {
position: relative;
display: inline-block;
}
.context-menu-container:hover :global(.gear-button) {
opacity: 1;
pointer-events: auto;
}
.context-trigger {
display: block;
}
:global(.gear-button-trigger) {
width: 32px;
height: 32px;
z-index: 10;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
&[data-state='open'] {
opacity: 1;
pointer-events: auto;
}
}
.gear-button {
position: absolute;
top: $unit;
left: $unit;
z-index: 99;
width: 32px;
height: 32px;
padding: 0;
opacity: 0;
pointer-events: none;
border: none;
background: rgba(0, 0, 0, 0.6);
border-radius: $item-corner;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
&[data-state='open'] {
opacity: 1;
pointer-events: auto;
}
&:hover {
background: rgba(0, 0, 0, 0.8);
transform: scale(1.05);
}
img {
width: 16px;
height: 16px;
display: block;
color: white;
filter: brightness(0) invert(1);
}
}
.context-menu-container:hover :global(.gear-button-trigger) {
opacity: 1;
pointer-events: auto;
}
:global(.context-menu) {
background: var(--app-bg, white);
border: 1px solid var(--border-color, #ddd);
@ -44,7 +146,7 @@
}
:global(.context-menu-item) {
padding: $unit-half $unit;
padding: $unit $unit-2x;
border-radius: $item-corner-small;
cursor: pointer;
font-size: $font-regular;
@ -58,8 +160,14 @@
background: var(--button-contained-bg-hover, #f5f5f5);
}
&.danger {
color: var(--danger, #dc3545);
&:first-child {
border-top-left-radius: $item-corner;
border-top-right-radius: $item-corner;
}
&:last-child {
border-bottom-left-radius: $item-corner;
border-bottom-right-radius: $item-corner;
}
&:disabled {
@ -68,10 +176,77 @@
}
}
:global(.context-menu-item.danger) {
color: var(--danger);
&:hover {
background: var(--danger-bg);
}
}
:global(.context-menu-separator) {
height: 1px;
background: var(--border-color, #ddd);
margin: $unit-half 0;
border-radius: $full-corner;
height: 2px;
background: var(--menu-separator);
margin: $unit-half ($unit * 0.75);
}
// Dropdown menu styles (same as context menu)
:global(.dropdown-menu) {
background: var(--app-bg, white);
border: 1px solid var(--border-color, #ddd);
border-radius: $card-corner;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: $unit-half;
min-width: calc($unit * 22.5);
z-index: 200;
animation: slideIn $duration-quick ease-out;
}
:global(.dropdown-menu-item) {
padding: $unit $unit-2x;
border-radius: $item-corner-small;
cursor: pointer;
font-size: $font-regular;
color: var(--text-primary);
display: flex;
align-items: center;
gap: $unit;
@include smooth-transition($duration-standard, background);
&:hover {
background: var(--button-contained-bg-hover, #f5f5f5);
}
&:first-child {
border-top-left-radius: $item-corner;
border-top-right-radius: $item-corner;
}
&:last-child {
border-bottom-left-radius: $item-corner;
border-bottom-right-radius: $item-corner;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
:global(.dropdown-menu-item.danger) {
color: var(--danger);
&:hover {
background: var(--danger-bg);
}
}
:global(.dropdown-menu-separator) {
border-radius: $full-corner;
height: 2px;
background: var(--menu-separator);
margin: $unit-half ($unit * 0.75);
}
@keyframes slideIn {

View file

@ -0,0 +1,99 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuBase } from 'bits-ui'
import type { Snippet } from 'svelte'
interface DropdownMenuProps {
trigger: Snippet
menu: Snippet
open?: boolean
}
let { trigger, menu, open = $bindable(false) }: DropdownMenuProps = $props()
</script>
<DropdownMenuBase.Root bind:open>
<DropdownMenuBase.Trigger>
{@render trigger()}
</DropdownMenuBase.Trigger>
<DropdownMenuBase.Portal>
<DropdownMenuBase.Content class="dropdown-menu" side="bottom" align="start" sideOffset={4}>
{@render menu()}
</DropdownMenuBase.Content>
</DropdownMenuBase.Portal>
</DropdownMenuBase.Root>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/layout' as *;
@use '$src/themes/typography' as *;
@use '$src/themes/effects' as *;
:global(.dropdown-menu) {
background: var(--app-bg, white);
border: 1px solid var(--border-color, #ddd);
border-radius: $card-corner;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: $unit-half;
min-width: calc($unit * 22.5);
z-index: 200;
animation: slideIn $duration-quick ease-out;
}
:global(.dropdown-menu-item) {
padding: $unit $unit-2x;
border-radius: $item-corner-small;
cursor: pointer;
font-size: $font-regular;
color: var(--text-primary);
display: flex;
align-items: center;
gap: $unit;
@include smooth-transition($duration-standard, background);
&:hover {
background: var(--button-contained-bg-hover, #f5f5f5);
}
&:first-child {
border-top-left-radius: $item-corner;
border-top-right-radius: $item-corner;
}
&:last-child {
border-bottom-left-radius: $item-corner;
border-bottom-right-radius: $item-corner;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
:global(.dropdown-menu-item.danger) {
color: var(--danger);
&:hover {
background: var(--danger-bg);
}
}
:global(.dropdown-menu-separator) {
border-radius: $full-corner;
height: 2px;
background: var(--menu-separator);
margin: $unit-half ($unit * 0.75);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-$unit-fourth);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>