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:
Scott Idem
2026-06-17 17:21:17 -04:00
parent 313eca076d
commit 04205e4a63
2 changed files with 255 additions and 152 deletions

View File

@@ -238,10 +238,10 @@ function content_preview(ds: ae_DataStore): string {
}
</script>
<div class="container mx-auto space-y-6 p-4">
<div class="space-y-6">
<!-- ── 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="bg-primary-500/10 rounded-lg p-2">
<Database size={24} class="text-primary-500" />
@@ -535,14 +535,17 @@ function content_preview(ds: ae_DataStore): string {
<tbody>
{#each results as ds (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">
<AE_Field_Editor
object_type="data_store"
object_id={ds_id}
field_name="code"
edit_label="Code"
current_value={ds.code ?? ''}
placeholder="store_code"
display_modal={true}
modal_blocking={false}
on_success={() => do_search(false)}>
<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}
@@ -554,8 +557,10 @@ function content_preview(ds: ae_DataStore): string {
object_type="data_store"
object_id={ds_id}
field_name="name"
edit_label="Name"
current_value={ds.name ?? ''}
placeholder="Display name"
display_modal={true}
on_success={() => do_search(false)} />
</td>
<td class="px-3 py-2">
@@ -563,9 +568,11 @@ function content_preview(ds: ae_DataStore): string {
object_type="data_store"
object_id={ds_id}
field_name="type"
edit_label="Type"
current_value={ds.type ?? 'text'}
field_type="select"
select_options={ds_type_options}
display_modal={true}
on_success={() => do_search(false)}>
<span class="badge {type_badge(ds.type)} font-mono text-[9px] uppercase">{ds.type ?? '?'}</span>
</AE_Field_Editor>