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