eliminato ui-kit
This commit is contained in:
parent
a5dda58555
commit
675264f26a
|
|
@ -1,15 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>UI Kit Dev</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>UI Kit Dev</h1>
|
|
||||||
<ui-data-table-shell endpoint="/users/table" target="#target" page-size="10"></ui-data-table-shell>
|
|
||||||
<div id="target"></div>
|
|
||||||
|
|
||||||
<script type="module" src="/src/index.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"name": "trustcontact-ui-kit",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"css:build": "tailwindcss -c tailwind.config.cjs -i ./styles/tailwind.css -o ../web/static/css/app.css --minify",
|
|
||||||
"css:dev": "tailwindcss -c tailwind.config.cjs -i ./styles/tailwind.css -o ../web/static/css/app.css --watch"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"postcss": "^8.4.47",
|
|
||||||
"svelte": "^5.0.0",
|
|
||||||
"tailwindcss": "^3.4.13",
|
|
||||||
"typescript": "^5.6.0",
|
|
||||||
"vite": "^5.4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
:root {
|
|
||||||
--ui-bg: #ffffff;
|
|
||||||
--ui-fg: #111827;
|
|
||||||
--ui-muted: #6b7280;
|
|
||||||
--ui-border: #d1d5db;
|
|
||||||
--ui-overlay: rgba(17, 24, 39, 0.56);
|
|
||||||
--ui-panel: #ffffff;
|
|
||||||
--ui-radius: 10px;
|
|
||||||
--ui-shadow: 0 10px 35px rgba(15, 23, 42, 0.2);
|
|
||||||
--ui-primary: #111827;
|
|
||||||
--ui-primary-contrast: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui-modal,
|
|
||||||
ui-drop-down,
|
|
||||||
ui-data-table-shell {
|
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
||||||
color: var(--ui-fg);
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
<svelte:options customElement="ui-data-table-shell" />
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export let endpoint = '';
|
|
||||||
export let target = '';
|
|
||||||
export let pageSize = 10;
|
|
||||||
|
|
||||||
let query = '';
|
|
||||||
|
|
||||||
function submit(page = 1) {
|
|
||||||
if (!endpoint || !target) return;
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (query.trim()) params.set('q', query.trim());
|
|
||||||
params.set('page', String(page));
|
|
||||||
params.set('pageSize', String(pageSize));
|
|
||||||
|
|
||||||
const url = `${endpoint}${endpoint.includes('?') ? '&' : '?'}${params.toString()}`;
|
|
||||||
|
|
||||||
const htmxApi = (window as any).htmx;
|
|
||||||
if (htmxApi && typeof htmxApi.ajax === 'function') {
|
|
||||||
htmxApi.ajax('GET', url, { target });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetEl = document.querySelector(target);
|
|
||||||
if (!targetEl) return;
|
|
||||||
|
|
||||||
fetch(url)
|
|
||||||
.then((res) => res.text())
|
|
||||||
.then((html) => {
|
|
||||||
targetEl.innerHTML = html;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// no-op fallback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<form class="toolbar" on:submit|preventDefault={() => submit(1)}>
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
placeholder="Search..."
|
|
||||||
bind:value={query}
|
|
||||||
on:keydown={(e) => e.key === 'Enter' && submit(1)}
|
|
||||||
/>
|
|
||||||
<button type="submit">Search</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='search'] {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 180px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid var(--ui-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border: 0;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--ui-primary);
|
|
||||||
color: var(--ui-primary-contrast);
|
|
||||||
padding: 10px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
<svelte:options customElement="ui-drop-down" />
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
export let value = '';
|
|
||||||
export let name = '';
|
|
||||||
export let placeholder = 'Select...';
|
|
||||||
export let disabled = false;
|
|
||||||
|
|
||||||
let rootEl: HTMLElement;
|
|
||||||
let host: HTMLElement;
|
|
||||||
let hiddenInput: HTMLInputElement;
|
|
||||||
let open = false;
|
|
||||||
|
|
||||||
type OptionItem = { value: string; label: string };
|
|
||||||
let options: OptionItem[] = [];
|
|
||||||
|
|
||||||
function loadOptions() {
|
|
||||||
options = Array.from(host.querySelectorAll('option')).map((opt) => ({
|
|
||||||
value: opt.value,
|
|
||||||
label: opt.textContent?.trim() || opt.value
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
const selected = host.querySelector('option[selected]') as HTMLOptionElement | null;
|
|
||||||
if (selected) value = selected.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectedLabel() {
|
|
||||||
if (!value) return placeholder;
|
|
||||||
return options.find((o) => o.value === value)?.label || placeholder;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectOption(nextValue: string) {
|
|
||||||
value = nextValue;
|
|
||||||
open = false;
|
|
||||||
syncHiddenInput();
|
|
||||||
|
|
||||||
host.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
host.dispatchEvent(
|
|
||||||
new CustomEvent('ui:change', {
|
|
||||||
detail: { value },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncHiddenInput() {
|
|
||||||
if (!hiddenInput) return;
|
|
||||||
hiddenInput.name = name;
|
|
||||||
hiddenInput.value = value;
|
|
||||||
hiddenInput.disabled = disabled || !name;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
host = (rootEl.getRootNode() as ShadowRoot).host as HTMLElement;
|
|
||||||
if (!host) return;
|
|
||||||
|
|
||||||
hiddenInput = host.querySelector('input[data-ui-dropdown-hidden="true"]') as HTMLInputElement;
|
|
||||||
if (!hiddenInput) {
|
|
||||||
hiddenInput = document.createElement('input');
|
|
||||||
hiddenInput.type = 'hidden';
|
|
||||||
hiddenInput.setAttribute('data-ui-dropdown-hidden', 'true');
|
|
||||||
host.appendChild(hiddenInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadOptions();
|
|
||||||
syncHiddenInput();
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
loadOptions();
|
|
||||||
syncHiddenInput();
|
|
||||||
});
|
|
||||||
observer.observe(host, { childList: true, subtree: true, attributes: true });
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="dropdown" bind:this={rootEl}>
|
|
||||||
<button type="button" class="trigger" disabled={disabled} on:click={() => (open = !open)}>
|
|
||||||
<span>{selectedLabel()}</span>
|
|
||||||
<span aria-hidden="true">▾</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if open}
|
|
||||||
<div class="menu" role="listbox">
|
|
||||||
{#if options.length === 0}
|
|
||||||
<div class="empty">No options</div>
|
|
||||||
{:else}
|
|
||||||
{#each options as opt}
|
|
||||||
<button type="button" class="item" on:click={() => selectOption(opt.value)}>
|
|
||||||
{opt.label}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.dropdown {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid var(--ui-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--ui-bg);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 30;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin-top: 6px;
|
|
||||||
border: 1px solid var(--ui-border);
|
|
||||||
background: var(--ui-bg);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: var(--ui-shadow);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
padding: 10px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:hover {
|
|
||||||
background: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
padding: 10px 12px;
|
|
||||||
color: var(--ui-muted);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
<svelte:options customElement="ui-modal" />
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
export let title = '';
|
|
||||||
|
|
||||||
let rootEl: HTMLElement;
|
|
||||||
let panelEl: HTMLElement;
|
|
||||||
let hostEl: HTMLElement;
|
|
||||||
|
|
||||||
function isOpen(): boolean {
|
|
||||||
return !!hostEl?.hasAttribute('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
hostEl?.removeAttribute('open');
|
|
||||||
hostEl?.dispatchEvent(
|
|
||||||
new CustomEvent('ui:close', { bubbles: true, composed: true })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKeyDown(event: KeyboardEvent) {
|
|
||||||
if (!isOpen()) return;
|
|
||||||
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
closeModal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Tab') {
|
|
||||||
trapFocus(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function trapFocus(event: KeyboardEvent) {
|
|
||||||
const focusables = panelEl?.querySelectorAll<HTMLElement>(
|
|
||||||
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
|
|
||||||
);
|
|
||||||
if (!focusables || focusables.length === 0) {
|
|
||||||
event.preventDefault();
|
|
||||||
panelEl?.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const first = focusables[0];
|
|
||||||
const last = focusables[focusables.length - 1];
|
|
||||||
const active = document.activeElement as HTMLElement | null;
|
|
||||||
|
|
||||||
if (event.shiftKey && active === first) {
|
|
||||||
event.preventDefault();
|
|
||||||
last.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!event.shiftKey && active === last) {
|
|
||||||
event.preventDefault();
|
|
||||||
first.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBackdropClick(event: MouseEvent) {
|
|
||||||
if (event.target === event.currentTarget) {
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
hostEl = (rootEl.getRootNode() as ShadowRoot).host as HTMLElement;
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
if (isOpen()) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const autofocus = panelEl?.querySelector<HTMLElement>('[autofocus]');
|
|
||||||
(autofocus || panelEl)?.focus();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(hostEl, { attributes: true, attributeFilter: ['open'] });
|
|
||||||
|
|
||||||
if (isOpen()) {
|
|
||||||
setTimeout(() => {
|
|
||||||
(panelEl as HTMLElement | undefined)?.focus();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="overlay" on:click={onBackdropClick} on:keydown={onKeyDown} role="presentation" bind:this={rootEl}>
|
|
||||||
<div class="panel" role="dialog" aria-modal="true" tabindex="-1" bind:this={panelEl}>
|
|
||||||
<header class="header">
|
|
||||||
<h3>{title}</h3>
|
|
||||||
<button type="button" class="close" on:click={closeModal} aria-label="Close">×</button>
|
|
||||||
</header>
|
|
||||||
<section class="body">
|
|
||||||
<slot />
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: var(--ui-overlay);
|
|
||||||
display: none;
|
|
||||||
place-items: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([open]) .overlay {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
width: min(640px, calc(100vw - 32px));
|
|
||||||
max-height: calc(100vh - 48px);
|
|
||||||
overflow: auto;
|
|
||||||
background: var(--ui-panel);
|
|
||||||
border-radius: var(--ui-radius);
|
|
||||||
box-shadow: var(--ui-shadow);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-bottom: 1px solid var(--ui-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
padding: 12px 16px 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import './base.css';
|
|
||||||
import './components/UiModal.svelte';
|
|
||||||
import './components/UiDropDown.svelte';
|
|
||||||
import './components/UiDataTableShell.svelte';
|
|
||||||
|
|
||||||
export {};
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
body {
|
|
||||||
@apply m-0 bg-slate-100 text-slate-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
@apply text-slate-800;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
|
||||||
.row {
|
|
||||||
@apply flex flex-wrap gap-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.muted {
|
|
||||||
@apply text-sm text-slate-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
@apply rounded-lg bg-slate-900 px-4 py-2 text-white hover:bg-slate-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-base {
|
|
||||||
@apply rounded-lg border border-slate-300 px-3 py-2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
'./src/**/*.{svelte,ts,js}',
|
|
||||||
'../web/templates/**/*.html'
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {}
|
|
||||||
},
|
|
||||||
plugins: []
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"types": ["vite/client"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*", "vite.config.ts"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
svelte({
|
|
||||||
compilerOptions: {
|
|
||||||
customElement: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
build: {
|
|
||||||
outDir: '../web/static/ui',
|
|
||||||
emptyOutDir: true,
|
|
||||||
cssCodeSplit: false,
|
|
||||||
lib: {
|
|
||||||
entry: 'src/index.ts',
|
|
||||||
formats: ['es'],
|
|
||||||
fileName: () => 'ui.esm.js'
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
assetFileNames: (assetInfo) => {
|
|
||||||
if (assetInfo.name === 'style.css') {
|
|
||||||
return 'ui.css';
|
|
||||||
}
|
|
||||||
return '[name][extname]';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue