Some checks failed
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 3s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 3s
CI/CD Pipeline / VM Test - performance (push) Successful in 2s
CI/CD Pipeline / VM Test - security (push) Successful in 2s
CI/CD Pipeline / Backend Linting (push) Successful in 2s
CI/CD Pipeline / Frontend Linting (push) Failing after 12s
CI/CD Pipeline / Nix Flake Check (push) Successful in 37s
CI/CD Pipeline / CI Summary (push) Failing after 0s
267 lines
5.4 KiB
Svelte
267 lines
5.4 KiB
Svelte
<script lang="ts">
|
||
import { createEventDispatcher } from 'svelte';
|
||
|
||
export let initialTitle: string = '';
|
||
export let initialDescription: string = '';
|
||
|
||
let title = initialTitle;
|
||
let description = initialDescription;
|
||
let errors: Record<string, string> = {};
|
||
|
||
const dispatch = createEventDispatcher<{
|
||
create: { title: string; description?: string };
|
||
close: void;
|
||
}>();
|
||
|
||
function validate(): boolean {
|
||
errors = {};
|
||
|
||
if (!title.trim()) {
|
||
errors.title = 'Title is required';
|
||
} else if (title.length > 255) {
|
||
errors.title = 'Title must be 255 characters or less';
|
||
}
|
||
|
||
if (description.length > 1000) {
|
||
errors.description = 'Description must be 1000 characters or less';
|
||
}
|
||
|
||
return Object.keys(errors).length === 0;
|
||
}
|
||
|
||
function handleSubmit() {
|
||
if (!validate()) return;
|
||
|
||
dispatch('create', {
|
||
title: title.trim(),
|
||
description: description.trim() || undefined,
|
||
});
|
||
}
|
||
|
||
function handleClose() {
|
||
dispatch('close');
|
||
}
|
||
|
||
function handleBackdropClick(event: MouseEvent) {
|
||
if (event.target === event.currentTarget) {
|
||
handleClose();
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||
<div
|
||
class="modal-backdrop"
|
||
on:click={handleBackdropClick}
|
||
on:keydown={(e) => e.key === 'Escape' && handleClose()}
|
||
role="dialog"
|
||
aria-modal="true"
|
||
tabindex="-1"
|
||
>
|
||
<div class="modal-content">
|
||
<header class="modal-header">
|
||
<h2>Create New Board</h2>
|
||
<button class="close-btn" on:click={handleClose} aria-label="Close">×</button>
|
||
</header>
|
||
|
||
<form on:submit|preventDefault={handleSubmit} class="modal-form">
|
||
<div class="form-group">
|
||
<label for="title">Board Title <span class="required">*</span></label>
|
||
<input
|
||
id="title"
|
||
type="text"
|
||
bind:value={title}
|
||
placeholder="e.g., Character Design References"
|
||
class:error={errors.title}
|
||
maxlength="255"
|
||
required
|
||
/>
|
||
{#if errors.title}
|
||
<span class="error-text">{errors.title}</span>
|
||
{/if}
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="description">Description (optional)</label>
|
||
<textarea
|
||
id="description"
|
||
bind:value={description}
|
||
placeholder="Add a description for this board..."
|
||
rows="3"
|
||
maxlength="1000"
|
||
class:error={errors.description}
|
||
/>
|
||
{#if errors.description}
|
||
<span class="error-text">{errors.description}</span>
|
||
{:else}
|
||
<span class="help-text">{description.length}/1000 characters</span>
|
||
{/if}
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn-secondary" on:click={handleClose}>Cancel</button>
|
||
<button type="submit" class="btn-primary">Create Board</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.modal-backdrop {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
border-radius: 16px;
|
||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||
width: 100%;
|
||
max-width: 500px;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 1.5rem;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.modal-header h2 {
|
||
font-size: 1.5rem;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
margin: 0;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 2rem;
|
||
color: #6b7280;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
line-height: 1;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
color: #1f2937;
|
||
}
|
||
|
||
.modal-form {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.form-group:last-of-type {
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
margin-bottom: 0.5rem;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.required {
|
||
color: #ef4444;
|
||
}
|
||
|
||
input,
|
||
textarea {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 8px;
|
||
font-size: 1rem;
|
||
font-family: inherit;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
input:focus,
|
||
textarea:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
input.error,
|
||
textarea.error {
|
||
border-color: #ef4444;
|
||
}
|
||
|
||
textarea {
|
||
resize: vertical;
|
||
min-height: 80px;
|
||
}
|
||
|
||
.error-text {
|
||
display: block;
|
||
color: #ef4444;
|
||
font-size: 0.875rem;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.help-text {
|
||
display: block;
|
||
color: #6b7280;
|
||
font-size: 0.875rem;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
gap: 1rem;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.btn-primary,
|
||
.btn-secondary {
|
||
padding: 0.75rem 1.5rem;
|
||
border-radius: 8px;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border: none;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: white;
|
||
color: #374151;
|
||
border: 1px solid #d1d5db;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #f9fafb;
|
||
}
|
||
</style>
|