add ui components
This commit is contained in:
parent
ef30e57eba
commit
2fbb181078
2 changed files with 250 additions and 0 deletions
86
src/lib/components/ui/ContextMenu.svelte
Normal file
86
src/lib/components/ui/ContextMenu.svelte
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||||
|
import type { Snippet } from 'svelte'
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
children: Snippet
|
||||||
|
menu: Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
let { children, menu }: ContextMenuProps = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenuBase.Root>
|
||||||
|
<ContextMenuBase.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<div {...props}>
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</ContextMenuBase.Trigger>
|
||||||
|
|
||||||
|
<ContextMenuBase.Portal>
|
||||||
|
<ContextMenuBase.Content class="context-menu">
|
||||||
|
{@render menu()}
|
||||||
|
</ContextMenuBase.Content>
|
||||||
|
</ContextMenuBase.Portal>
|
||||||
|
</ContextMenuBase.Root>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as *;
|
||||||
|
@use '$src/themes/layout' as *;
|
||||||
|
@use '$src/themes/typography' as *;
|
||||||
|
|
||||||
|
:global(.context-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: 180px;
|
||||||
|
z-index: 200;
|
||||||
|
animation: slideIn 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.context-menu-item) {
|
||||||
|
padding: $unit-half $unit;
|
||||||
|
border-radius: $item-corner-small;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: $font-regular;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--button-contained-bg-hover, #f5f5f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
color: var(--danger, #dc3545);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.context-menu-separator) {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-color, #ddd);
|
||||||
|
margin: $unit-half 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
164
src/lib/components/ui/Dialog.svelte
Normal file
164
src/lib/components/ui/Dialog.svelte
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogBase } from 'bits-ui'
|
||||||
|
import type { Snippet } from 'svelte'
|
||||||
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
children: Snippet
|
||||||
|
footer?: Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
open = $bindable(false),
|
||||||
|
onOpenChange,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
footer
|
||||||
|
}: DialogProps = $props()
|
||||||
|
|
||||||
|
function handleOpenChange(newOpen: boolean) {
|
||||||
|
open = newOpen
|
||||||
|
onOpenChange?.(newOpen)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogBase.Root bind:open onOpenChange={handleOpenChange}>
|
||||||
|
<DialogBase.Portal>
|
||||||
|
<DialogBase.Overlay class="dialog-overlay" />
|
||||||
|
<DialogBase.Content class="dialog-content">
|
||||||
|
{#if title}
|
||||||
|
<DialogBase.Title class="dialog-title">{title}</DialogBase.Title>
|
||||||
|
{/if}
|
||||||
|
{#if description}
|
||||||
|
<DialogBase.Description class="dialog-description">{description}</DialogBase.Description>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<DialogBase.Close class="dialog-close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</DialogBase.Close>
|
||||||
|
|
||||||
|
<div class="dialog-body">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if footer}
|
||||||
|
<div class="dialog-footer">
|
||||||
|
{@render footer()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</DialogBase.Content>
|
||||||
|
</DialogBase.Portal>
|
||||||
|
</DialogBase.Root>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as *;
|
||||||
|
@use '$src/themes/layout' as *;
|
||||||
|
@use '$src/themes/typography' as *;
|
||||||
|
|
||||||
|
:global(.dialog-overlay) {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 100;
|
||||||
|
animation: fade-in 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dialog-content) {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: var(--app-bg, white);
|
||||||
|
border-radius: $card-corner;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
width: 500px;
|
||||||
|
z-index: 101;
|
||||||
|
animation: slide-up 0.2s ease-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dialog-title) {
|
||||||
|
font-size: $font-large;
|
||||||
|
font-weight: $bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: $unit-2x;
|
||||||
|
padding-bottom: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dialog-description) {
|
||||||
|
font-size: $font-regular;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 0 $unit-2x;
|
||||||
|
margin: $unit 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dialog-close) {
|
||||||
|
position: absolute;
|
||||||
|
right: $unit;
|
||||||
|
top: $unit;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: $item-corner-small;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--button-contained-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dialog-body) {
|
||||||
|
padding: $unit-2x;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dialog-footer) {
|
||||||
|
padding: $unit-2x;
|
||||||
|
padding-top: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: $unit;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -48%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue