mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
fix(macmini): auto-rebuild stale sveltekit-base before per-app web builds
Per-app web Dockerfiles do `FROM sveltekit-base:local` and do NOT re-COPY
packages/shared-* — those packages are baked into the base image. So a
change to packages/shared-utils, packages/shared-ui, etc. only reaches
the live web app if the base image is also rebuilt.
This bit us THREE times on 2026-04-08 alone:
1. CSP fix in shared-utils ('wasm-unsafe-eval') sat unused in production
for over an hour because every `build-app.sh mana-web` cheerfully
reused the cached base layer that still contained the old shared-utils.
2. Same problem with the BaseListView export in shared-ui after the
ListView consolidation refactor — mana-web's build failed because the
Rollup pass couldn't resolve the new symbol from the stale base.
3. Same shape, different package, repeatedly.
The pattern is identical every time and the manual workaround
(`build-app.sh --base` first) is something you only think to run if you
already know how the layering works. Make the script catch it.
New `is_base_image_stale` helper compares the base image's `Created`
timestamp against the latest git commit touching paths the base image
actually depends on:
- packages/ (all shared-* packages baked in)
- docker/Dockerfile.sveltekit-base
- pnpm-lock.yaml (transitive dep changes)
When building any *-web service, if the image is stale or missing, the
base is rebuilt automatically before the per-app build kicks off, with
the triggering commit's oneline printed for transparency.
Date parsing notes:
- macOS Docker emits the Created field with the local TZ offset
("...+02:00"), not Z. We strip the fractional + offset suffix and
parse the literal local clock time with BSD date (no -u), which is
what the original timestamp meant on this host. GNU date is the
fallback for Linux dev boxes and handles the full ISO directly.
- If parsing fails for any reason we conservatively force a rebuild
rather than risk shipping stale code.
Verified end-to-end against the live Mac Mini's current state earlier
today: image 55s newer than the last packages/ commit at the time →
"fresh, skip" (correct). When the next packages/ commit lands, the
script will see commit_epoch > image_epoch and trigger the base
rebuild automatically.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3dc16bb885
commit
048184bef0
4 changed files with 269 additions and 460 deletions
210
apps/mana/apps/web/src/lib/components/PickerOverlay.svelte
Normal file
210
apps/mana/apps/web/src/lib/components/PickerOverlay.svelte
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
<!--
|
||||
PickerOverlay — shared shell for the workbench page-/app-/option-picker
|
||||
panels (AppPagePicker, ContactPagePicker, todo PagePicker, …).
|
||||
|
||||
Encodes the slide-in cream-paper card with header (title + close), list
|
||||
area with auto-dividers, empty state, and optional footer slot. The
|
||||
per-row content is provided via the `item` snippet.
|
||||
|
||||
All inner classes (`.picker-option`, `.option-icon`, `.option-text`,
|
||||
`.option-title`, `.option-desc`, `.divider`) are exported as `:global`
|
||||
so consumer snippets can use them without redefining the styles.
|
||||
-->
|
||||
<script lang="ts" generics="T">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { X } from '@mana/shared-icons';
|
||||
|
||||
interface Props<TItem> {
|
||||
title: string;
|
||||
items: TItem[];
|
||||
onClose: () => void;
|
||||
item: Snippet<[TItem, number]>;
|
||||
/** Optional snippet rendered after the items (e.g. "create custom" button). */
|
||||
footer?: Snippet;
|
||||
emptyLabel?: string;
|
||||
/** CSS width for the panel; default 320px. */
|
||||
width?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
items,
|
||||
onClose,
|
||||
item,
|
||||
footer,
|
||||
emptyLabel = 'Keine Einträge',
|
||||
width = '320px',
|
||||
}: Props<T> = $props();
|
||||
</script>
|
||||
|
||||
<div class="picker" style="--picker-width: {width};">
|
||||
<div class="picker-header">
|
||||
<h3 class="picker-title">{title}</h3>
|
||||
<button class="close-btn" onclick={onClose} title={$_('common.close')}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="picker-list">
|
||||
{#each items as entry, i}
|
||||
{#if i > 0}<div class="divider"></div>{/if}
|
||||
{@render item(entry, i)}
|
||||
{/each}
|
||||
{#if footer}
|
||||
{#if items.length > 0}<div class="divider"></div>{/if}
|
||||
{@render footer()}
|
||||
{:else if items.length === 0}
|
||||
<div class="empty-state"><p>{emptyLabel}</p></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.picker {
|
||||
flex: 0 0 auto;
|
||||
width: min(var(--picker-width), 85vw);
|
||||
min-height: 60vh;
|
||||
max-height: 80vh;
|
||||
background: #fffef5;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideIn 0.25s ease-out;
|
||||
}
|
||||
:global(.dark) .picker {
|
||||
background-color: #252220;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.picker-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
:global(.dark) .picker-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.picker-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
/* Divider between rows — used by both auto-render above and consumer snippets. */
|
||||
:global(.picker .divider) {
|
||||
height: 1px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
:global(.dark .picker .divider) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
/* Standard option button — consumers use these classes inside the item snippet. */
|
||||
:global(.picker .picker-option) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
transition: background 0.15s;
|
||||
text-align: left;
|
||||
}
|
||||
:global(.picker .picker-option:hover) {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
:global(.dark .picker .picker-option:hover) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
:global(.picker .option-icon) {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 0.5rem;
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
|
||||
:global(.picker .option-text) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
min-width: 0;
|
||||
}
|
||||
:global(.picker .option-title) {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark .picker .option-title) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
:global(.picker .option-desc) {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
:global(.dark .picker .option-desc) {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-state p {
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { X } from '@mana/shared-icons';
|
||||
import PickerOverlay from '$lib/components/PickerOverlay.svelte';
|
||||
import { getAllApps } from '$lib/app-registry';
|
||||
|
||||
function appName(id: string, fallback: string): string {
|
||||
|
|
@ -23,152 +23,34 @@
|
|||
let availableApps = $derived(getAllApps().filter((app) => !activeAppIds.includes(app.id)));
|
||||
</script>
|
||||
|
||||
<div class="app-picker">
|
||||
<div class="picker-header">
|
||||
<h3 class="picker-title">App hinzufügen</h3>
|
||||
<button class="close-btn" onclick={onClose} title={$_('common.close')}><X size={16} /></button>
|
||||
</div>
|
||||
<div class="picker-list">
|
||||
{#each availableApps as app, i (app.id)}
|
||||
{#if i > 0}<div class="divider"></div>{/if}
|
||||
<button class="app-option" onclick={() => onSelect(app.id)}>
|
||||
<div class="app-dot" style="background-color: {app.color}"></div>
|
||||
<span class="app-name">{appName(app.id, app.name)}</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#if availableApps.length === 0}
|
||||
<div class="empty-state"><p>Alle Apps sind bereits geöffnet</p></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<PickerOverlay
|
||||
title="App hinzufügen"
|
||||
items={availableApps}
|
||||
{onClose}
|
||||
width="300px"
|
||||
emptyLabel="Alle Apps sind bereits geöffnet"
|
||||
>
|
||||
{#snippet item(app)}
|
||||
<button class="picker-option" onclick={() => onSelect(app.id)}>
|
||||
<div class="app-dot" style="background-color: {app.color}"></div>
|
||||
<span class="app-name">{appName(app.id, app.name)}</span>
|
||||
</button>
|
||||
{/snippet}
|
||||
</PickerOverlay>
|
||||
|
||||
<style>
|
||||
.app-picker {
|
||||
flex: 0 0 auto;
|
||||
width: min(300px, 85vw);
|
||||
min-height: 60vh;
|
||||
max-height: 80vh;
|
||||
background: #fffef5;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideIn 0.25s ease-out;
|
||||
}
|
||||
:global(.dark) .app-picker {
|
||||
background-color: #252220;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.picker-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
:global(.dark) .picker-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.picker-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 0.5rem 0.75rem;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
:global(.dark) .divider {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.app-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
transition: background 0.15s;
|
||||
text-align: left;
|
||||
}
|
||||
.app-option:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
:global(.dark) .app-option:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.app-dot {
|
||||
:global(.picker .app-dot) {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
:global(.picker .app-name) {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .app-name {
|
||||
:global(.dark .picker .app-name) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-state p {
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import PickerOverlay from '$lib/components/PickerOverlay.svelte';
|
||||
import {
|
||||
Users,
|
||||
User,
|
||||
|
|
@ -10,7 +10,6 @@
|
|||
Briefcase,
|
||||
MapPin,
|
||||
Clock,
|
||||
X,
|
||||
} from '@mana/shared-icons';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -90,162 +89,19 @@
|
|||
let availableOptions = $derived(PAGE_OPTIONS.filter((opt) => !activePageIds.includes(opt.id)));
|
||||
</script>
|
||||
|
||||
<div class="page-picker">
|
||||
<div class="picker-header">
|
||||
<h3 class="picker-title">Neue Seite</h3>
|
||||
<button class="close-btn" onclick={onClose} title={$_('common.close')}><X size={16} /></button>
|
||||
</div>
|
||||
<div class="picker-list">
|
||||
{#each availableOptions as option, i (option.id)}
|
||||
{#if i > 0}<div class="divider"></div>{/if}
|
||||
<button class="page-option" onclick={() => onSelect(option.id)}>
|
||||
<div class="option-icon" style="color: {option.color}"><option.icon size={20} /></div>
|
||||
<div class="option-text">
|
||||
<span class="option-title">{option.title}</span>
|
||||
<span class="option-desc">{option.description}</span>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{#if availableOptions.length === 0}
|
||||
<div class="empty-state"><p>Alle Seiten sind bereits geöffnet</p></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-picker {
|
||||
flex: 0 0 auto;
|
||||
width: min(320px, 85vw);
|
||||
min-height: 60vh;
|
||||
background: #fffef5;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideIn 0.25s ease-out;
|
||||
}
|
||||
:global(.dark) .page-picker {
|
||||
background-color: #252220;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
.picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
.picker-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
:global(.dark) .picker-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.picker-list {
|
||||
flex: 1;
|
||||
padding: 0 0.5rem 0.75rem;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
:global(.dark) .divider {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.page-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
transition: background 0.15s;
|
||||
text-align: left;
|
||||
}
|
||||
.page-option:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
:global(.dark) .page-option:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.option-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 0.5rem;
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
.option-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.option-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .option-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.option-desc {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
:global(.dark) .option-desc {
|
||||
color: #6b7280;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-state p {
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
<PickerOverlay
|
||||
title="Neue Seite"
|
||||
items={availableOptions}
|
||||
{onClose}
|
||||
emptyLabel="Alle Seiten sind bereits geöffnet"
|
||||
>
|
||||
{#snippet item(option)}
|
||||
<button class="picker-option" onclick={() => onSelect(option.id)}>
|
||||
<div class="option-icon" style="color: {option.color}"><option.icon size={20} /></div>
|
||||
<div class="option-text">
|
||||
<span class="option-title">{option.title}</span>
|
||||
<span class="option-desc">{option.description}</span>
|
||||
</div>
|
||||
</button>
|
||||
{/snippet}
|
||||
</PickerOverlay>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import PickerOverlay from '$lib/components/PickerOverlay.svelte';
|
||||
import {
|
||||
Circle,
|
||||
CheckCircle,
|
||||
|
|
@ -9,7 +9,6 @@
|
|||
Flag,
|
||||
Calendar,
|
||||
TagSimple,
|
||||
X,
|
||||
Plus,
|
||||
} from '@mana/shared-icons';
|
||||
|
||||
|
|
@ -78,25 +77,25 @@
|
|||
let availableOptions = $derived(PAGE_OPTIONS.filter((opt) => !activePageIds.includes(opt.id)));
|
||||
</script>
|
||||
|
||||
<div class="page-picker">
|
||||
<div class="picker-header">
|
||||
<h3 class="picker-title">Neue Seite</h3>
|
||||
<button class="close-btn" onclick={onClose} title={$_('common.close')}><X size={16} /></button>
|
||||
</div>
|
||||
<div class="picker-list">
|
||||
{#each availableOptions as option, i (option.id)}
|
||||
{#if i > 0}<div class="divider"></div>{/if}
|
||||
<button class="page-option" onclick={() => onSelect(option.id)}>
|
||||
<div class="option-icon" style="color: {option.color}"><option.icon size={20} /></div>
|
||||
<div class="option-text">
|
||||
<span class="option-title">{option.title}</span>
|
||||
<span class="option-desc">{option.description}</span>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{#if availableOptions.length > 0 && onCreateCustom}<div class="divider"></div>{/if}
|
||||
<PickerOverlay
|
||||
title="Neue Seite"
|
||||
items={availableOptions}
|
||||
{onClose}
|
||||
emptyLabel="Alle Seiten sind bereits geöffnet"
|
||||
>
|
||||
{#snippet item(option)}
|
||||
<button class="picker-option" onclick={() => onSelect(option.id)}>
|
||||
<div class="option-icon" style="color: {option.color}"><option.icon size={20} /></div>
|
||||
<div class="option-text">
|
||||
<span class="option-title">{option.title}</span>
|
||||
<span class="option-desc">{option.description}</span>
|
||||
</div>
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
{#if onCreateCustom}
|
||||
<button class="page-option custom-option" onclick={onCreateCustom}>
|
||||
<button class="picker-option custom-option" onclick={onCreateCustom}>
|
||||
<div class="option-icon custom-icon"><Plus size={20} /></div>
|
||||
<div class="option-text">
|
||||
<span class="option-title">Eigene Seite</span>
|
||||
|
|
@ -104,153 +103,15 @@
|
|||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
{#if availableOptions.length === 0 && !onCreateCustom}
|
||||
<div class="empty-state"><p>Alle Seiten sind bereits geöffnet</p></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</PickerOverlay>
|
||||
|
||||
<style>
|
||||
.page-picker {
|
||||
flex: 0 0 auto;
|
||||
width: min(320px, 85vw);
|
||||
min-height: 60vh;
|
||||
background: #fffef5;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideIn 0.25s ease-out;
|
||||
:global(.picker .custom-option) {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
:global(.dark) .page-picker {
|
||||
background-color: #252220;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
.picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
.picker-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
:global(.dark) .picker-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.picker-list {
|
||||
flex: 1;
|
||||
padding: 0 0.5rem 0.75rem;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
:global(.dark) .divider {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.page-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
transition: background 0.15s;
|
||||
text-align: left;
|
||||
}
|
||||
.page-option:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
:global(.dark) .page-option:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.option-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 0.5rem;
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
.custom-icon {
|
||||
:global(.picker .custom-icon) {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 10%, transparent);
|
||||
}
|
||||
.custom-option {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.option-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.option-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .option-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.option-desc {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
:global(.dark) .option-desc {
|
||||
color: #6b7280;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-state p {
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue