add ui components

This commit is contained in:
Justin Edmund 2025-09-15 21:26:14 -07:00
parent ef30e57eba
commit 2fbb181078
2 changed files with 250 additions and 0 deletions

View 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>

View 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>