feat(field-editor): add display_modal mode with placement, blocking toggle, and unsaved-changes guard
- New `display_modal` prop opens the edit panel as a native <dialog> anchored
near the pencil trigger instead of shifting inline content
- `modal_placement` (center|above|below|left|right, default center) positions
the dialog relative to the trigger via getBoundingClientRect + CSS transform
- `modal_blocking` (default true) toggles showModal() vs show(); non-modal
mode adds a document pointerdown listener to close on outside click
- `cancel_edit()` now warns "Discard unsaved changes?" when draft differs from
saved value (matches data store form behaviour); skips warning after a
successful save
- Dialog background uses theme CSS vars directly (--color-surface-50/900) via
:global CSS — Skeleton tonal presets are intentionally semi-transparent and
rendered behind table content without explicit position:fixed + z-index
- Extracted edit panel to {#snippet edit_panel()} — shared by inline and
dialog paths with no duplication
- data-stores table: all three inline field editors switched to display_modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,9 @@ interface Props {
|
|||||||
edit_label?: string;
|
edit_label?: string;
|
||||||
display_block?: boolean;
|
display_block?: boolean;
|
||||||
display_absolute_edit?: boolean;
|
display_absolute_edit?: boolean;
|
||||||
|
display_modal?: boolean;
|
||||||
|
modal_blocking?: boolean;
|
||||||
|
modal_placement?: 'center' | 'above' | 'below' | 'left' | 'right';
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
class_li?: string;
|
class_li?: string;
|
||||||
textarea_rows?: number;
|
textarea_rows?: number;
|
||||||
@@ -92,6 +95,9 @@ let {
|
|||||||
edit_label = 'Edit Field',
|
edit_label = 'Edit Field',
|
||||||
display_block = false,
|
display_block = false,
|
||||||
display_absolute_edit = false,
|
display_absolute_edit = false,
|
||||||
|
display_modal = false,
|
||||||
|
modal_blocking = true,
|
||||||
|
modal_placement = 'center' as 'center' | 'above' | 'below' | 'left' | 'right',
|
||||||
placeholder = 'Enter value...',
|
placeholder = 'Enter value...',
|
||||||
class_li = '',
|
class_li = '',
|
||||||
textarea_rows = 4,
|
textarea_rows = 4,
|
||||||
@@ -159,6 +165,9 @@ let patch_status = $state<'idle' | 'processing' | 'success' | 'error'>('idle');
|
|||||||
let error_message = $state('');
|
let error_message = $state('');
|
||||||
let draft_value = $state(to_input_value(current_value, field_type));
|
let draft_value = $state(to_input_value(current_value, field_type));
|
||||||
let input_ref = $state<HTMLElement | null>(null);
|
let input_ref = $state<HTMLElement | null>(null);
|
||||||
|
let dialog_ref = $state<HTMLDialogElement | null>(null);
|
||||||
|
let trigger_ref = $state<HTMLElement | null>(null);
|
||||||
|
let dialog_style = $state('');
|
||||||
|
|
||||||
// Optimistic display state machine
|
// Optimistic display state machine
|
||||||
let has_optimistic = $state(false);
|
let has_optimistic = $state(false);
|
||||||
@@ -190,6 +199,31 @@ $effect(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Open/close the <dialog> in modal mode
|
||||||
|
$effect(() => {
|
||||||
|
if (!display_modal || !dialog_ref) return;
|
||||||
|
if (is_editing) {
|
||||||
|
untrack(() => {
|
||||||
|
if (!dialog_ref!.open) {
|
||||||
|
if (modal_blocking) dialog_ref!.showModal();
|
||||||
|
else dialog_ref!.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
untrack(() => { if (dialog_ref!.open) dialog_ref!.close(); });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Non-modal: close when clicking outside the dialog (no backdrop to click)
|
||||||
|
$effect(() => {
|
||||||
|
if (!display_modal || modal_blocking || !is_editing) return;
|
||||||
|
function on_outside(e: PointerEvent) {
|
||||||
|
if (dialog_ref && !dialog_ref.contains(e.target as Node)) cancel_edit();
|
||||||
|
}
|
||||||
|
document.addEventListener('pointerdown', on_outside);
|
||||||
|
return () => document.removeEventListener('pointerdown', on_outside);
|
||||||
|
});
|
||||||
|
|
||||||
async function handle_patch() {
|
async function handle_patch() {
|
||||||
if (log_lvl) console.log(`AE Field Editor (new): Patching ${object_type}.${field_name}...`);
|
if (log_lvl) console.log(`AE Field Editor (new): Patching ${object_type}.${field_name}...`);
|
||||||
|
|
||||||
@@ -230,6 +264,8 @@ async function handle_patch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cancel_edit() {
|
function cancel_edit() {
|
||||||
|
const value_changed = draft_value !== to_input_value(current_value, field_type);
|
||||||
|
if (value_changed && patch_status !== 'success' && !confirm('Discard unsaved changes?')) return;
|
||||||
has_optimistic = false;
|
has_optimistic = false;
|
||||||
draft_value = to_input_value(current_value, field_type);
|
draft_value = to_input_value(current_value, field_type);
|
||||||
is_editing = false;
|
is_editing = false;
|
||||||
@@ -237,11 +273,36 @@ function cancel_edit() {
|
|||||||
error_message = '';
|
error_message = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calc_dialog_pos() {
|
||||||
|
if (!trigger_ref) return;
|
||||||
|
const rect = trigger_ref.getBoundingClientRect();
|
||||||
|
const gap = 8;
|
||||||
|
const cx = rect.left + rect.width / 2;
|
||||||
|
const cy = rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
let top: number, left: number, tx: string, ty: string;
|
||||||
|
switch (modal_placement) {
|
||||||
|
case 'above':
|
||||||
|
top = rect.top - gap; left = cx; tx = '-50%'; ty = '-100%'; break;
|
||||||
|
case 'below':
|
||||||
|
top = rect.bottom + gap; left = cx; tx = '-50%'; ty = '0'; break;
|
||||||
|
case 'left':
|
||||||
|
top = cy; left = rect.left - gap; tx = '-100%'; ty = '-50%'; break;
|
||||||
|
case 'right':
|
||||||
|
top = cy; left = rect.right + gap; tx = '0'; ty = '-50%'; break;
|
||||||
|
default: // center
|
||||||
|
top = cy; left = cx; tx = '-50%'; ty = '-50%'; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog_style = `margin:0;top:${top}px;left:${left}px;transform:translate(${tx},${ty});`;
|
||||||
|
}
|
||||||
|
|
||||||
function toggle_edit() {
|
function toggle_edit() {
|
||||||
if (is_editing) cancel_edit();
|
if (is_editing) cancel_edit();
|
||||||
else {
|
else {
|
||||||
has_optimistic = false;
|
has_optimistic = false;
|
||||||
draft_value = to_input_value(current_value, field_type);
|
draft_value = to_input_value(current_value, field_type);
|
||||||
|
if (display_modal) calc_dialog_pos();
|
||||||
is_editing = true;
|
is_editing = true;
|
||||||
if (on_open) on_open();
|
if (on_open) on_open();
|
||||||
}
|
}
|
||||||
@@ -252,15 +313,160 @@ function handle_keydown(e: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet edit_panel()}
|
||||||
|
<header class="mb-2 flex items-center justify-between">
|
||||||
|
<span class="text-xs font-bold tracking-wider uppercase opacity-60">
|
||||||
|
{edit_label || field_name}
|
||||||
|
</span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-icon btn-icon-sm preset-tonal-surface"
|
||||||
|
onclick={cancel_edit}
|
||||||
|
aria-label="Cancel editing"
|
||||||
|
title="Cancel editing"
|
||||||
|
disabled={patch_status === 'processing'}>
|
||||||
|
<X size="14" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="input_container mb-3">
|
||||||
|
{#if field_type === 'textarea'}
|
||||||
|
<textarea
|
||||||
|
{id}
|
||||||
|
bind:this={input_ref}
|
||||||
|
bind:value={draft_value}
|
||||||
|
rows={textarea_rows}
|
||||||
|
class="textarea"
|
||||||
|
{placeholder}></textarea>
|
||||||
|
{:else if field_type === 'select'}
|
||||||
|
<div class="relative">
|
||||||
|
<select
|
||||||
|
{id}
|
||||||
|
bind:this={input_ref}
|
||||||
|
value={draft_value}
|
||||||
|
onchange={(e) => draft_value = coerce_select_value(e.currentTarget.value, current_value)}
|
||||||
|
class="select pr-10">
|
||||||
|
{#if allow_null}
|
||||||
|
<option value={null}>-- None --</option>
|
||||||
|
{/if}
|
||||||
|
{#if Object.keys(select_options).length === 0}
|
||||||
|
<option value="" disabled>Loading options...</option>
|
||||||
|
{/if}
|
||||||
|
{#each Object.entries(select_options) as [val, label] (val)}
|
||||||
|
<option value={val}>{label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if Object.keys(select_options).length === 0}
|
||||||
|
<div class="absolute inset-y-0 right-8 flex items-center">
|
||||||
|
<LoaderCircle size="14" class="animate-spin opacity-50" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if field_type === 'checkbox'}
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
type="checkbox"
|
||||||
|
bind:this={input_ref}
|
||||||
|
bind:checked={draft_value}
|
||||||
|
class="checkbox" />
|
||||||
|
<span>{draft_value ? 'True' : 'False'}</span>
|
||||||
|
</label>
|
||||||
|
{:else if field_type === 'tiptap'}
|
||||||
|
<AE_Comp_Editor_TipTap
|
||||||
|
bind:content={draft_value}
|
||||||
|
{placeholder} />
|
||||||
|
{:else if field_type === 'codemirror'}
|
||||||
|
<AE_Comp_Editor_CodeMirror
|
||||||
|
bind:content={draft_value}
|
||||||
|
{placeholder} />
|
||||||
|
{:else if field_type === 'date'}
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
type="date"
|
||||||
|
bind:this={input_ref}
|
||||||
|
bind:value={draft_value}
|
||||||
|
class="input" />
|
||||||
|
{:else if field_type === 'datetime'}
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
type="datetime-local"
|
||||||
|
bind:this={input_ref}
|
||||||
|
bind:value={draft_value}
|
||||||
|
class="input" />
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
type={field_type === 'number' ? 'number' : field_type === 'email' ? 'email' : field_type === 'url' ? 'url' : field_type === 'tel' ? 'tel' : 'text'}
|
||||||
|
bind:this={input_ref}
|
||||||
|
bind:value={draft_value}
|
||||||
|
class="input"
|
||||||
|
{placeholder}
|
||||||
|
onkeydown={(e) => e.key === 'Enter' && handle_patch()} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="flex items-center justify-between">
|
||||||
|
<div class="status_indicator text-xs">
|
||||||
|
{#if patch_status === 'processing'}
|
||||||
|
<span class="text-primary-500 flex items-center gap-1">
|
||||||
|
<LoaderCircle size="12" class="animate-spin" /> Saving...
|
||||||
|
</span>
|
||||||
|
{:else if patch_status === 'success'}
|
||||||
|
<span class="text-success-500 flex items-center gap-1">
|
||||||
|
<Check size="12" /> Saved
|
||||||
|
</span>
|
||||||
|
{:else if patch_status === 'error'}
|
||||||
|
<div class="text-error-500 flex flex-col gap-0.5">
|
||||||
|
<span class="flex items-center gap-1 font-bold">
|
||||||
|
<CircleAlert size="12" /> Error
|
||||||
|
</span>
|
||||||
|
<span class="opacity-80">{error_message}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions flex gap-2">
|
||||||
|
{#if allow_null && draft_value !== null}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm preset-tonal-error"
|
||||||
|
onclick={() => (draft_value = null)}
|
||||||
|
aria-label="Clear value"
|
||||||
|
title="Clear value">
|
||||||
|
<Eraser size="14" class="mr-1" />
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm preset-filled-primary-500"
|
||||||
|
onclick={handle_patch}
|
||||||
|
aria-label="Save changes"
|
||||||
|
title="Save changes"
|
||||||
|
disabled={patch_status === 'processing' || draft_value === to_input_value(current_value, field_type)}>
|
||||||
|
{#if patch_status === 'processing'}
|
||||||
|
<LoaderCircle size="14" class="mr-1 animate-spin" />
|
||||||
|
{:else}
|
||||||
|
<Save size="14" class="mr-1" />
|
||||||
|
{/if}
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="ae_field_editor group relative {class_li}"
|
class="ae_field_editor group relative {class_li}"
|
||||||
class:block={display_block}
|
class:block={display_block}
|
||||||
class:inline-block={!display_block}
|
class:inline-block={!display_block}
|
||||||
role="none"
|
role="none"
|
||||||
onkeydown={handle_keydown}>
|
onkeydown={handle_keydown}>
|
||||||
|
|
||||||
<!-- VIEW MODE -->
|
<!-- VIEW MODE: stays visible in modal mode so layout doesn't shift -->
|
||||||
<div class="view_wrapper flex items-center gap-2" class:hidden={is_editing}>
|
<div class="view_wrapper flex items-center gap-2" class:hidden={is_editing && !display_modal}>
|
||||||
<div class="content_render grow">
|
<div class="content_render grow">
|
||||||
{#if children}
|
{#if children}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
@@ -281,6 +487,7 @@ function handle_keydown(e: KeyboardEvent) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
bind:this={trigger_ref}
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-icon btn-icon-sm preset-tonal-warning opacity-20 transition-opacity hover:opacity-100"
|
class="btn-icon btn-icon-sm preset-tonal-warning opacity-20 transition-opacity hover:opacity-100"
|
||||||
class:invisible={!$ae_loc.edit_mode}
|
class:invisible={!$ae_loc.edit_mode}
|
||||||
@@ -293,162 +500,32 @@ function handle_keydown(e: KeyboardEvent) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- EDIT MODE -->
|
<!-- EDIT MODE — inline or absolute (not used in modal mode) -->
|
||||||
{#if is_editing}
|
{#if is_editing && !display_modal}
|
||||||
<div
|
<div
|
||||||
class="edit_wrapper border-warning-500/50 bg-surface-50-950 z-50 rounded-lg border-2 border-dashed p-3 shadow-xl"
|
class="edit_wrapper border-warning-500/50 bg-surface-50-950 z-50 rounded-lg border-2 border-dashed p-3 shadow-xl"
|
||||||
class:absolute={display_absolute_edit}
|
class:absolute={display_absolute_edit}
|
||||||
class:top-0={display_absolute_edit}
|
class:top-0={display_absolute_edit}
|
||||||
class:left-0={display_absolute_edit}
|
class:left-0={display_absolute_edit}
|
||||||
class:w-full={display_absolute_edit}>
|
class:w-full={display_absolute_edit}>
|
||||||
|
{@render edit_panel()}
|
||||||
<header class="mb-2 flex items-center justify-between">
|
|
||||||
<span class="text-xs font-bold tracking-wider uppercase opacity-60">
|
|
||||||
{edit_label || field_name}
|
|
||||||
</span>
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-icon btn-icon-sm preset-tonal-surface"
|
|
||||||
onclick={cancel_edit}
|
|
||||||
aria-label="Cancel editing"
|
|
||||||
title="Cancel editing"
|
|
||||||
disabled={patch_status === 'processing'}>
|
|
||||||
<X size="14" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="input_container mb-3">
|
|
||||||
{#if field_type === 'textarea'}
|
|
||||||
<textarea
|
|
||||||
{id}
|
|
||||||
bind:this={input_ref}
|
|
||||||
bind:value={draft_value}
|
|
||||||
rows={textarea_rows}
|
|
||||||
class="textarea"
|
|
||||||
{placeholder}></textarea>
|
|
||||||
{:else if field_type === 'select'}
|
|
||||||
<div class="relative">
|
|
||||||
<select
|
|
||||||
{id}
|
|
||||||
bind:this={input_ref}
|
|
||||||
value={draft_value}
|
|
||||||
onchange={(e) => draft_value = coerce_select_value(e.currentTarget.value, current_value)}
|
|
||||||
class="select pr-10">
|
|
||||||
{#if allow_null}
|
|
||||||
<option value={null}>-- None --</option>
|
|
||||||
{/if}
|
|
||||||
{#if Object.keys(select_options).length === 0}
|
|
||||||
<option value="" disabled>Loading options...</option>
|
|
||||||
{/if}
|
|
||||||
{#each Object.entries(select_options) as [val, label] (val)}
|
|
||||||
<option value={val}>{label}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
{#if Object.keys(select_options).length === 0}
|
|
||||||
<div class="absolute inset-y-0 right-8 flex items-center">
|
|
||||||
<LoaderCircle size="14" class="animate-spin opacity-50" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else if field_type === 'checkbox'}
|
|
||||||
<label class="flex items-center space-x-2">
|
|
||||||
<input
|
|
||||||
{id}
|
|
||||||
type="checkbox"
|
|
||||||
bind:this={input_ref}
|
|
||||||
bind:checked={draft_value}
|
|
||||||
class="checkbox" />
|
|
||||||
<span>{draft_value ? 'True' : 'False'}</span>
|
|
||||||
</label>
|
|
||||||
{:else if field_type === 'tiptap'}
|
|
||||||
<AE_Comp_Editor_TipTap
|
|
||||||
bind:content={draft_value}
|
|
||||||
{placeholder} />
|
|
||||||
{:else if field_type === 'codemirror'}
|
|
||||||
<AE_Comp_Editor_CodeMirror
|
|
||||||
bind:content={draft_value}
|
|
||||||
{placeholder} />
|
|
||||||
{:else if field_type === 'date'}
|
|
||||||
<input
|
|
||||||
{id}
|
|
||||||
type="date"
|
|
||||||
bind:this={input_ref}
|
|
||||||
bind:value={draft_value}
|
|
||||||
class="input" />
|
|
||||||
{:else if field_type === 'datetime'}
|
|
||||||
<input
|
|
||||||
{id}
|
|
||||||
type="datetime-local"
|
|
||||||
bind:this={input_ref}
|
|
||||||
bind:value={draft_value}
|
|
||||||
class="input" />
|
|
||||||
{:else}
|
|
||||||
<input
|
|
||||||
{id}
|
|
||||||
type={field_type === 'number' ? 'number' : field_type === 'email' ? 'email' : field_type === 'url' ? 'url' : field_type === 'tel' ? 'tel' : 'text'}
|
|
||||||
bind:this={input_ref}
|
|
||||||
bind:value={draft_value}
|
|
||||||
class="input"
|
|
||||||
{placeholder}
|
|
||||||
onkeydown={(e) => e.key === 'Enter' && handle_patch()} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<footer class="flex items-center justify-between">
|
|
||||||
<div class="status_indicator text-xs">
|
|
||||||
{#if patch_status === 'processing'}
|
|
||||||
<span class="text-primary-500 flex items-center gap-1">
|
|
||||||
<LoaderCircle size="12" class="animate-spin" /> Saving...
|
|
||||||
</span>
|
|
||||||
{:else if patch_status === 'success'}
|
|
||||||
<span class="text-success-500 flex items-center gap-1">
|
|
||||||
<Check size="12" /> Saved
|
|
||||||
</span>
|
|
||||||
{:else if patch_status === 'error'}
|
|
||||||
<div class="text-error-500 flex flex-col gap-0.5">
|
|
||||||
<span class="flex items-center gap-1 font-bold">
|
|
||||||
<CircleAlert size="12" /> Error
|
|
||||||
</span>
|
|
||||||
<span class="opacity-80">{error_message}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions flex gap-2">
|
|
||||||
{#if allow_null && draft_value !== null}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm preset-tonal-error"
|
|
||||||
onclick={() => (draft_value = null)}
|
|
||||||
aria-label="Clear value"
|
|
||||||
title="Clear value">
|
|
||||||
<Eraser size="14" class="mr-1" />
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm preset-filled-primary-500"
|
|
||||||
onclick={handle_patch}
|
|
||||||
aria-label="Save changes"
|
|
||||||
title="Save changes"
|
|
||||||
disabled={patch_status === 'processing' || draft_value === to_input_value(current_value, field_type)}>
|
|
||||||
{#if patch_status === 'processing'}
|
|
||||||
<LoaderCircle size="14" class="mr-1 animate-spin" />
|
|
||||||
{:else}
|
|
||||||
<Save size="14" class="mr-1" />
|
|
||||||
{/if}
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL MODE — <dialog> renders in the browser top layer, never clipped by table/overflow -->
|
||||||
|
{#if display_modal}
|
||||||
|
<dialog
|
||||||
|
bind:this={dialog_ref}
|
||||||
|
style={dialog_style}
|
||||||
|
class="ae_field_editor_dialog border-surface-200-800 w-full max-w-sm rounded-xl border p-4 shadow-2xl"
|
||||||
|
oncancel={(e) => { e.preventDefault(); cancel_edit(); }}
|
||||||
|
onclick={(e) => { if (e.target === dialog_ref) cancel_edit(); }}
|
||||||
|
onkeydown={(e) => { if (e.key === 'Escape') cancel_edit(); }}>
|
||||||
|
{@render edit_panel()}
|
||||||
|
</dialog>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.ae_field_editor :global(.btn-icon-sm) {
|
.ae_field_editor :global(.btn-icon-sm) {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@@ -458,4 +535,23 @@ function handle_keydown(e: KeyboardEvent) {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Solid card-face background using theme variables directly —
|
||||||
|
Skeleton tonal classes are semi-transparent by design, unusable on dialogs.
|
||||||
|
position:fixed + z-index required for non-modal show() which doesn't get
|
||||||
|
the browser top-layer; redundant but harmless for showModal(). */
|
||||||
|
:global(.ae_field_editor_dialog) {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
background-color: var(--color-surface-50);
|
||||||
|
color: var(--color-surface-950);
|
||||||
|
}
|
||||||
|
:global(.dark .ae_field_editor_dialog) {
|
||||||
|
background-color: var(--color-surface-900);
|
||||||
|
color: var(--color-surface-50);
|
||||||
|
}
|
||||||
|
:global(.ae_field_editor_dialog::backdrop) {
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -238,10 +238,10 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto space-y-6 p-4">
|
<div class="space-y-6">
|
||||||
|
|
||||||
<!-- ── Header ──────────────────────────────────────────────────────────── -->
|
<!-- ── Header ──────────────────────────────────────────────────────────── -->
|
||||||
<header class="bg-surface-50-900-token border-surface-500/10 flex flex-wrap items-center justify-between gap-4 rounded-xl border p-4 shadow-lg">
|
<header class="bg-surface-100-900 border-surface-500/10 flex flex-wrap items-center justify-between gap-4 rounded-xl border p-4 shadow-lg">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="bg-primary-500/10 rounded-lg p-2">
|
<div class="bg-primary-500/10 rounded-lg p-2">
|
||||||
<Database size={24} class="text-primary-500" />
|
<Database size={24} class="text-primary-500" />
|
||||||
@@ -535,14 +535,17 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{#each results as ds (ds.id ?? ds.data_store_id)}
|
{#each results as ds (ds.id ?? ds.data_store_id)}
|
||||||
{@const ds_id = ds.id ?? ds.data_store_id}
|
{@const ds_id = ds.id ?? ds.data_store_id}
|
||||||
<tr class="border-surface-500/10 hover:bg-surface-500/5 border-b transition-colors">
|
<tr class="border-surface-500/10 hover:bg-surface-500/5 border-b transition-colors duration-200">
|
||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
<AE_Field_Editor
|
<AE_Field_Editor
|
||||||
object_type="data_store"
|
object_type="data_store"
|
||||||
object_id={ds_id}
|
object_id={ds_id}
|
||||||
field_name="code"
|
field_name="code"
|
||||||
|
edit_label="Code"
|
||||||
current_value={ds.code ?? ''}
|
current_value={ds.code ?? ''}
|
||||||
placeholder="store_code"
|
placeholder="store_code"
|
||||||
|
display_modal={true}
|
||||||
|
modal_blocking={false}
|
||||||
on_success={() => do_search(false)}>
|
on_success={() => do_search(false)}>
|
||||||
<span class="font-mono" class:opacity-40={!ds.enable}>{ds.code}</span>
|
<span class="font-mono" class:opacity-40={!ds.enable}>{ds.code}</span>
|
||||||
{#if !ds.enable}<span class="badge preset-tonal-error ml-1 text-[9px]">off</span>{/if}
|
{#if !ds.enable}<span class="badge preset-tonal-error ml-1 text-[9px]">off</span>{/if}
|
||||||
@@ -554,8 +557,10 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
object_type="data_store"
|
object_type="data_store"
|
||||||
object_id={ds_id}
|
object_id={ds_id}
|
||||||
field_name="name"
|
field_name="name"
|
||||||
|
edit_label="Name"
|
||||||
current_value={ds.name ?? ''}
|
current_value={ds.name ?? ''}
|
||||||
placeholder="Display name"
|
placeholder="Display name"
|
||||||
|
display_modal={true}
|
||||||
on_success={() => do_search(false)} />
|
on_success={() => do_search(false)} />
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
@@ -563,9 +568,11 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
object_type="data_store"
|
object_type="data_store"
|
||||||
object_id={ds_id}
|
object_id={ds_id}
|
||||||
field_name="type"
|
field_name="type"
|
||||||
|
edit_label="Type"
|
||||||
current_value={ds.type ?? 'text'}
|
current_value={ds.type ?? 'text'}
|
||||||
field_type="select"
|
field_type="select"
|
||||||
select_options={ds_type_options}
|
select_options={ds_type_options}
|
||||||
|
display_modal={true}
|
||||||
on_success={() => do_search(false)}>
|
on_success={() => do_search(false)}>
|
||||||
<span class="badge {type_badge(ds.type)} font-mono text-[9px] uppercase">{ds.type ?? '?'}</span>
|
<span class="badge {type_badge(ds.type)} font-mono text-[9px] uppercase">{ds.type ?? '?'}</span>
|
||||||
</AE_Field_Editor>
|
</AE_Field_Editor>
|
||||||
|
|||||||
Reference in New Issue
Block a user