Modal Dialog variant with role=alertdialog, forced modal=true, and click-outside disabled. Soma delegates to the Dialog runtime (variant='alertdialog') so DOM markers are Dialog's (data-dialog-*) — eidos reuses Dialog's recipe for chrome and adds two alert-dialog-specific buttons: Action + Cancel. Eidos shape: - Provider, Content, Action, Cancel — alert-dialog-specific wrappers - Trigger, Overlay, Title, Description, Header, Footer, Portal — re-exported from Dialog's eidos barrel (zero duplication) Recipe (alert-dialog.css): - Only styles Action + Cancel - Buttons consume Toggle palette tokens (same "interactive button" vocabulary used by <Toggle> and <ToggleGroup.Item>) - Auto-color cascade: Action picks up the dialog's data-color so a risk/threat-flagged dialog gets a red/amber Action button without manual recipe code - Cancel stays neutral so the safe-choice contrast is preserved Morfo: - scope: ['soma'] → ['soma', 'sema', 'eidos'] - The morfo declares Action + Cancel parts only (Provider is virtual, other parts inherited via Dialog runtime delegation) - Classified passive at this contract level (Dialog owns the events); README documents this in §"Passive justification" Demo: 6-tab canonical template with intent / size / position chips, soma + eidos snippets, references comparison table vs Radix Primitives / Bits UI / Ark UI / Chakra v3 / Radix Themes / shadcn. Checks: svelte-check 0 errors, component:audit 91/91 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>active-uix
parent
61ffbade46
commit
d5630a528f
@ -0,0 +1,143 @@
|
|||||||
|
# `<AlertDialog>` — eidos
|
||||||
|
|
||||||
|
Visual wrapper over `soma/components/alert-dialog`. A modal Dialog with
|
||||||
|
`role="alertdialog"`, forced `modal=true` and click-outside disabled
|
||||||
|
(`interactOutsideBehavior='ignore'`). The user MUST choose
|
||||||
|
`Action` or `Cancel`. Escape still cancels.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog } from '$uix/eidos/components/alert-dialog';
|
||||||
|
let open = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialog bind:open intent="risk">
|
||||||
|
<AlertDialog.Trigger>Delete project…</AlertDialog.Trigger>
|
||||||
|
<AlertDialog.Portal>
|
||||||
|
<AlertDialog.Overlay />
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Header>
|
||||||
|
<AlertDialog.Title>Delete project?</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
This action cannot be undone. All data will be lost.
|
||||||
|
</AlertDialog.Description>
|
||||||
|
</AlertDialog.Header>
|
||||||
|
<AlertDialog.Footer>
|
||||||
|
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action>Delete</AlertDialog.Action>
|
||||||
|
</AlertDialog.Footer>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Portal>
|
||||||
|
</AlertDialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Differences from `<Dialog>`
|
||||||
|
|
||||||
|
| Concern | Dialog | AlertDialog |
|
||||||
|
|---|---|---|
|
||||||
|
| role | `dialog` | `alertdialog` |
|
||||||
|
| modal | optional | **forced** `true` |
|
||||||
|
| click-outside | `'close'` (default) | `'ignore'` (forced) |
|
||||||
|
| Escape | `'close'` | `'close'` (preserved) |
|
||||||
|
| Initial focus | first focusable | **Cancel** (by convention; render it first) |
|
||||||
|
| Footer pattern | open-ended | always `Cancel + Action` for destructive choices |
|
||||||
|
| Doctrinal intent | full set | narrowed: `neutral \| risk \| threat` |
|
||||||
|
|
||||||
|
## Parts
|
||||||
|
|
||||||
|
| Part | Origin | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `AlertDialog` (root) | eidos | Wraps soma's AlertDialog.Provider |
|
||||||
|
| `Trigger` | inherited from Dialog | Identical contract |
|
||||||
|
| `Portal` | inherited from Dialog | Identical contract |
|
||||||
|
| `Overlay` | inherited from Dialog | Identical contract |
|
||||||
|
| `Content` | eidos (alert-dialog) | Wraps soma's AlertDialog.Content (forces `escapeKeydownBehavior='close'`) |
|
||||||
|
| `Title` | inherited from Dialog | Identical contract |
|
||||||
|
| `Description` | inherited from Dialog | Identical contract |
|
||||||
|
| `Header` | inherited from Dialog | Identical contract |
|
||||||
|
| `Footer` | inherited from Dialog | Identical contract |
|
||||||
|
| `Action` | eidos (alert-dialog) | Primary destructive button — auto-colors from dialog's `intent` |
|
||||||
|
| `Cancel` | eidos (alert-dialog) | Secondary safe button — stays neutral |
|
||||||
|
|
||||||
|
## Recipe
|
||||||
|
|
||||||
|
`alert-dialog.css` styles only the Action + Cancel buttons. Chrome
|
||||||
|
(overlay, panel, header, etc.) comes from `dialog.css` — soma's
|
||||||
|
AlertDialog re-uses the Dialog runtime, so the same `data-dialog-*`
|
||||||
|
markers reach the DOM.
|
||||||
|
|
||||||
|
Buttons consume the Toggle palette tokens (`--toggle-color-*`) — same
|
||||||
|
"interactive button" vocabulary used across `<Toggle>`,
|
||||||
|
`<ToggleGroup.Item>` and other action surfaces.
|
||||||
|
|
||||||
|
## Passive justification
|
||||||
|
|
||||||
|
Passive **at the alert-dialog morfo level** — the morfo declares only
|
||||||
|
the Action / Cancel button parts (Provider is virtual). All
|
||||||
|
behavioural events (open/close presence, escape, focus trap, …) come
|
||||||
|
from the Dialog runtime that soma's AlertDialog delegates to. There
|
||||||
|
is no alert-dialog-specific sema event to fire; the
|
||||||
|
`commit-confirm` and `close-cancel` flows live inside the Dialog
|
||||||
|
morfo's vocabulary.
|
||||||
|
|
||||||
|
The component IS interactive from the user's perspective. "Passive"
|
||||||
|
here is a contract-layer classification, not a UX one.
|
||||||
|
|
||||||
|
## Baseline
|
||||||
|
|
||||||
|
WAI-ARIA APG Alert Dialog:
|
||||||
|
<https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/>
|
||||||
|
|
||||||
|
Soma docs at `src/uix/soma/components/alert-dialog/README.md` cover
|
||||||
|
focus management (Cancel-first), nesting, and the `interactOutside`
|
||||||
|
behavioural locking.
|
||||||
|
|
||||||
|
## Comparativa
|
||||||
|
|
||||||
|
| Lib | Role forced | Modal forced | Click-outside | Intent narrowing | Auto-Color Action |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **Radix Primitives** | ✓ | ✓ | ignore (forced) | — | — |
|
||||||
|
| **Bits UI** | ✓ | ✓ | ignore | — | — |
|
||||||
|
| **Ark UI** | ✓ | ✓ | ignore | — | — |
|
||||||
|
| **Chakra v3** | ✓ | ✓ | option | `colorPalette` | by colorPalette |
|
||||||
|
| **Radix Themes** | ✓ | ✓ | ignore | `color` | by `color` |
|
||||||
|
| **shadcn/ui** | ✓ | ✓ | ignore | — | manual `variant="destructive"` |
|
||||||
|
| **Eidos** | ✓ | ✓ | ignore | `neutral\|risk\|threat` | ✓ — cascades from dialog's `data-color` |
|
||||||
|
|
||||||
|
## Decisiones
|
||||||
|
|
||||||
|
- **Intent narrowing.** The morfo restricts to `neutral | risk | threat`
|
||||||
|
(no `affirm` / `fulfill`) because alert dialogs gate destructive or
|
||||||
|
irreversible actions — celebrating them is the wrong affordance.
|
||||||
|
The recipe still picks up `affirm` if a consumer overrides the color
|
||||||
|
by hand (defensive fallback in the cascade).
|
||||||
|
- **Auto-color the Action button** based on the dialog's `data-color`.
|
||||||
|
An `intent="risk"` AlertDialog gets a red Action without manual
|
||||||
|
recipe code. Chakra and Radix Themes do this implicitly via
|
||||||
|
`colorPalette` — Eidos makes it explicit via the intent contract.
|
||||||
|
- **Cancel stays neutral.** Contrast with the Action is the canonical
|
||||||
|
visual cue for "this is the safe choice" (Radix Themes, Material
|
||||||
|
Design, iOS Human Interface Guidelines all agree).
|
||||||
|
- **Re-use Dialog's eidos parts** rather than duplicate Trigger /
|
||||||
|
Overlay / Title / etc. Soma already delegates to Dialog under the
|
||||||
|
hood; the visual layer mirrors that.
|
||||||
|
|
||||||
|
## Gaps
|
||||||
|
|
||||||
|
| Gap | Disposición | Detalle |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Soma's `commit-confirm` / `close-cancel` events not yet wired to a per-component sema cascade | **diferir** | The `commit` family base ships sound; suffices for now. Add cascade if we want different sounds for confirm-destroy vs confirm-affirm. |
|
||||||
|
| No async `onAction` returning Promise to gate close | **diferir** | Soma's wrapper closes synchronously; consumers gate via a parent state machine instead. Open spec question. |
|
||||||
|
| `size='xs'` / `'sm'` shaping for very small confirmations | **diferir** | Inherits Dialog's size scale (`sm/md/lg/xl/full`). Smaller would require Dialog-level changes. |
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- Radix UI Primitives:
|
||||||
|
<https://www.radix-ui.com/primitives/docs/components/alert-dialog>
|
||||||
|
- Bits UI: <https://bits-ui.com/docs/components/alert-dialog>
|
||||||
|
- Ark UI: <https://ark-ui.com/docs/components/alert-dialog>
|
||||||
|
- Chakra v3: <https://chakra-ui.com/docs/components/dialog>
|
||||||
|
- Radix Themes: <https://www.radix-ui.com/themes/docs/components/alert-dialog>
|
||||||
|
- shadcn/ui: <https://ui.shadcn.com/docs/components/alert-dialog>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Eidos `<AlertDialog.Action>` — destructive / primary confirmation
|
||||||
|
* button. Pass-through over soma's `<AlertDialog.Action>`. Visual
|
||||||
|
* treatment lives in `alert-dialog.css` (solid button, defaults to
|
||||||
|
* the dialog's `intent` color so a `risk`-flagged dialog gets a red
|
||||||
|
* Action automatically).
|
||||||
|
*/
|
||||||
|
import * as AlertDialog from '$soma/components/alert-dialog';
|
||||||
|
import type { AlertDialogActionProps } from './types';
|
||||||
|
|
||||||
|
let { children, ...rest }: AlertDialogActionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialog.Action {...rest}>
|
||||||
|
{@render children?.()}
|
||||||
|
</AlertDialog.Action>
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Eidos `<AlertDialog.Cancel>` — secondary / safe escape button.
|
||||||
|
* By WAI-ARIA convention it receives initial focus when the dialog
|
||||||
|
* opens (the safest choice is selected by default for destructive
|
||||||
|
* confirmations). Render `Cancel` BEFORE `Action` in DOM order so
|
||||||
|
* the focus scope lands here first.
|
||||||
|
*
|
||||||
|
* Pass-through over soma's `<AlertDialog.Cancel>`. Visual treatment:
|
||||||
|
* outline button, always neutral palette so it visually contrasts
|
||||||
|
* with the (potentially destructive) Action.
|
||||||
|
*/
|
||||||
|
import * as AlertDialog from '$soma/components/alert-dialog';
|
||||||
|
import type { AlertDialogCancelProps } from './types';
|
||||||
|
|
||||||
|
let { children, ...rest }: AlertDialogCancelProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialog.Cancel {...rest}>
|
||||||
|
{@render children?.()}
|
||||||
|
</AlertDialog.Cancel>
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Eidos `<AlertDialog.Content>`. Mirrors `Dialog.Content`'s recipe
|
||||||
|
* (responsive `size`, auto-sheet on narrow viewports, dimension
|
||||||
|
* overrides) but wraps soma's `AlertDialog.Content` so the
|
||||||
|
* `escapeKeydownBehavior='close'` default from soma's wrapper takes
|
||||||
|
* effect.
|
||||||
|
*
|
||||||
|
* The underlying DOM still emits `data-dialog-content` markers
|
||||||
|
* (soma's AlertDialog re-uses the Dialog runtime), so the existing
|
||||||
|
* `dialog.css` recipe paints chrome.
|
||||||
|
*/
|
||||||
|
import { ActiveEidos } from '$uix/eidos';
|
||||||
|
import { composeInlineStyle } from '$uix/eidos/lib/style';
|
||||||
|
import * as AlertDialog from '$soma/components/alert-dialog';
|
||||||
|
import type { AlertDialogContentProps } from './types';
|
||||||
|
|
||||||
|
let {
|
||||||
|
size = 'md',
|
||||||
|
position = 'middle-center',
|
||||||
|
width,
|
||||||
|
minWidth,
|
||||||
|
maxWidth,
|
||||||
|
height,
|
||||||
|
minHeight,
|
||||||
|
maxHeight,
|
||||||
|
style,
|
||||||
|
children: bodyContent,
|
||||||
|
...rest
|
||||||
|
}: AlertDialogContentProps = $props();
|
||||||
|
|
||||||
|
const eidos = ActiveEidos.require();
|
||||||
|
|
||||||
|
const resolvedSize = $derived(eidos.resolve(size, 'md'));
|
||||||
|
const resolvedPosition = $derived(eidos.resolve(position, 'middle-center'));
|
||||||
|
const isSheet = $derived(resolvedSize !== 'full' && eidos.isBelow('sm'));
|
||||||
|
|
||||||
|
function serializeLength(value: string | number | undefined): string | undefined {
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
if (!Number.isFinite(value)) return undefined;
|
||||||
|
return value === 0 ? '0' : `${value}px`;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dimensionVar(name: string, value: string | number | undefined): string | undefined {
|
||||||
|
const serialized = serializeLength(value);
|
||||||
|
return serialized ? `${name}: ${serialized};` : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dimensionsStyle = $derived(
|
||||||
|
[
|
||||||
|
dimensionVar('--_dialog-content-width-override', serializeLength(eidos.resolve(width))),
|
||||||
|
dimensionVar('--_dialog-content-min-width-override', serializeLength(eidos.resolve(minWidth))),
|
||||||
|
dimensionVar('--_dialog-content-max-width-override', serializeLength(eidos.resolve(maxWidth))),
|
||||||
|
dimensionVar('--_dialog-content-height-override', serializeLength(eidos.resolve(height))),
|
||||||
|
dimensionVar(
|
||||||
|
'--_dialog-content-min-height-override',
|
||||||
|
serializeLength(eidos.resolve(minHeight))
|
||||||
|
),
|
||||||
|
dimensionVar(
|
||||||
|
'--_dialog-content-max-height-override',
|
||||||
|
serializeLength(eidos.resolve(maxHeight))
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
);
|
||||||
|
|
||||||
|
const composedStyle = $derived(composeInlineStyle(style, dimensionsStyle));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialog.Content
|
||||||
|
{...rest}
|
||||||
|
style={composedStyle}
|
||||||
|
data-size={resolvedSize}
|
||||||
|
data-position={resolvedPosition}
|
||||||
|
data-sheet={isSheet ? '' : undefined}
|
||||||
|
>
|
||||||
|
{#snippet children(snippetProps)}
|
||||||
|
{@render bodyContent?.(snippetProps)}
|
||||||
|
{/snippet}
|
||||||
|
</AlertDialog.Content>
|
||||||
@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* AlertDialog recipe — Action + Cancel buttons only.
|
||||||
|
*
|
||||||
|
* The dialog chrome (overlay, content panel, header, title, etc.) is
|
||||||
|
* painted by `dialog.css`. Soma's AlertDialog re-uses the Dialog runtime
|
||||||
|
* (variant='alertdialog', modal=true), so the same `data-dialog-*`
|
||||||
|
* markers reach the DOM and the same recipe applies.
|
||||||
|
*
|
||||||
|
* What's alert-dialog-specific:
|
||||||
|
*
|
||||||
|
* [data-alert-dialog-action] → primary / destructive confirmation
|
||||||
|
* [data-alert-dialog-cancel] → secondary / safe escape
|
||||||
|
*
|
||||||
|
* The buttons consume the Toggle palette tokens (--toggle-*) like
|
||||||
|
* <Toggle> and <ToggleGroup.Item> — same "interactive button" visual
|
||||||
|
* vocabulary throughout the system.
|
||||||
|
*
|
||||||
|
* Intent propagation: the Dialog content carries
|
||||||
|
* `data-color="risk|threat|…"` (resolved from the AlertDialog's
|
||||||
|
* `intent` prop). Selectors below pick the destructive palette only
|
||||||
|
* when the dialog is flagged as such, so a `neutral` confirm dialog
|
||||||
|
* stays primary-colored instead of red.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-alert-dialog-action],
|
||||||
|
[data-alert-dialog-cancel] {
|
||||||
|
--_toggle-height: var(--toggle-height-md);
|
||||||
|
--_toggle-padding-inline: var(--toggle-px-md);
|
||||||
|
--_toggle-radius: var(--toggle-radius-md);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: var(--_toggle-height);
|
||||||
|
padding-inline: var(--_toggle-padding-inline);
|
||||||
|
border: var(--toggle-border-width) solid transparent;
|
||||||
|
border-radius: var(--_toggle-radius);
|
||||||
|
font-family: var(--toggle-font-family);
|
||||||
|
font-size: var(--toggle-font-size-md);
|
||||||
|
font-weight: var(--toggle-font-weight-md);
|
||||||
|
line-height: var(--toggle-line-height);
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background var(--toggle-transition-duration) var(--toggle-transition-ease),
|
||||||
|
border-color var(--toggle-transition-duration) var(--toggle-transition-ease),
|
||||||
|
color var(--toggle-transition-duration) var(--toggle-transition-ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-alert-dialog-action]:focus-visible,
|
||||||
|
[data-alert-dialog-cancel]:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: var(--focus-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Action — primary / destructive confirmation ─────────────────── */
|
||||||
|
/*
|
||||||
|
* Defaults to the dialog's intent palette: a `risk`-flagged
|
||||||
|
* AlertDialog renders a red Action automatically because
|
||||||
|
* `[data-dialog-content][data-color='risk']` cascades the risk palette
|
||||||
|
* to its descendants. The selectors below pin the Action to a SOLID
|
||||||
|
* variant of whichever palette is active.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-alert-dialog-action] {
|
||||||
|
background: var(--toggle-color-primary-solid);
|
||||||
|
color: var(--toggle-color-primary-contrast);
|
||||||
|
border-color: var(--toggle-color-primary-solid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-alert-dialog-action]:hover:not([data-disabled]) {
|
||||||
|
background: var(--toggle-color-primary-solid-hover);
|
||||||
|
border-color: var(--toggle-color-primary-solid-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When the dialog itself signals a destructive intent, Action picks
|
||||||
|
* the matching palette. Looking at the closest ancestor with the
|
||||||
|
* `data-color` projected by the morfo (lives on `data-dialog-content`). */
|
||||||
|
|
||||||
|
[data-dialog-content][data-color='risk'] [data-alert-dialog-action] {
|
||||||
|
background: var(--toggle-color-risk-solid);
|
||||||
|
border-color: var(--toggle-color-risk-solid);
|
||||||
|
color: var(--toggle-color-risk-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-dialog-content][data-color='risk'] [data-alert-dialog-action]:hover:not([data-disabled]) {
|
||||||
|
background: var(--toggle-color-risk-solid-hover);
|
||||||
|
border-color: var(--toggle-color-risk-solid-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-dialog-content][data-color='threat'] [data-alert-dialog-action] {
|
||||||
|
background: var(--toggle-color-threat-solid);
|
||||||
|
border-color: var(--toggle-color-threat-solid);
|
||||||
|
color: var(--toggle-color-threat-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-dialog-content][data-color='threat'] [data-alert-dialog-action]:hover:not([data-disabled]) {
|
||||||
|
background: var(--toggle-color-threat-solid-hover);
|
||||||
|
border-color: var(--toggle-color-threat-solid-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-dialog-content][data-color='affirm'] [data-alert-dialog-action] {
|
||||||
|
background: var(--toggle-color-affirm-solid);
|
||||||
|
border-color: var(--toggle-color-affirm-solid);
|
||||||
|
color: var(--toggle-color-affirm-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-dialog-content][data-color='affirm'] [data-alert-dialog-action]:hover:not([data-disabled]) {
|
||||||
|
background: var(--toggle-color-affirm-solid-hover);
|
||||||
|
border-color: var(--toggle-color-affirm-solid-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Cancel — secondary / safe escape ─────────────────────────────── */
|
||||||
|
/*
|
||||||
|
* Stays neutral-outlined regardless of the dialog's intent. The
|
||||||
|
* contrast between a colored Action and a neutral Cancel is the
|
||||||
|
* canonical visual cue for "this is the safe choice".
|
||||||
|
*/
|
||||||
|
|
||||||
|
[data-alert-dialog-cancel] {
|
||||||
|
background: var(--toggle-color-neutral-track);
|
||||||
|
color: var(--color-content-primary);
|
||||||
|
border-color: var(--toggle-color-neutral-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-alert-dialog-cancel]:hover:not([data-disabled]) {
|
||||||
|
background: var(--toggle-color-neutral-hover);
|
||||||
|
border-color: var(--toggle-color-neutral-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-alert-dialog-action][data-disabled],
|
||||||
|
[data-alert-dialog-cancel][data-disabled] {
|
||||||
|
cursor: default;
|
||||||
|
opacity: var(--toggle-disabled-opacity, 0.55);
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Eidos `<AlertDialog>` — root. Wraps soma's
|
||||||
|
* `<AlertDialog.Provider>` which delegates to the Dialog runtime
|
||||||
|
* with `variant='alertdialog'`, `modal=true` baked in.
|
||||||
|
*
|
||||||
|
* The actual DOM uses Dialog's data-attrs (`data-dialog-content`,
|
||||||
|
* `data-dialog-overlay`, etc.) so the existing Dialog recipe paints
|
||||||
|
* the chrome. Only `Action` and `Cancel` get fresh selectors
|
||||||
|
* (`data-alert-dialog-action`, `data-alert-dialog-cancel`).
|
||||||
|
*/
|
||||||
|
import * as AlertDialog from '$soma/components/alert-dialog';
|
||||||
|
import type { AlertDialogProps } from './types';
|
||||||
|
|
||||||
|
let { open = $bindable(false), children, ...rest }: AlertDialogProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialog.Provider {...rest} bind:open>
|
||||||
|
{@render children?.()}
|
||||||
|
</AlertDialog.Provider>
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
// AlertDialog — eidos compound API.
|
||||||
|
//
|
||||||
|
// import { AlertDialog } from '$uix/eidos/components/alert-dialog';
|
||||||
|
//
|
||||||
|
// <AlertDialog bind:open intent="risk">
|
||||||
|
// <AlertDialog.Trigger>Delete project…</AlertDialog.Trigger>
|
||||||
|
// <AlertDialog.Portal>
|
||||||
|
// <AlertDialog.Overlay />
|
||||||
|
// <AlertDialog.Content>
|
||||||
|
// <AlertDialog.Header>
|
||||||
|
// <AlertDialog.Title>Delete project?</AlertDialog.Title>
|
||||||
|
// <AlertDialog.Description>
|
||||||
|
// This cannot be undone.
|
||||||
|
// </AlertDialog.Description>
|
||||||
|
// </AlertDialog.Header>
|
||||||
|
// <AlertDialog.Footer>
|
||||||
|
// <AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
||||||
|
// <AlertDialog.Action>Delete</AlertDialog.Action>
|
||||||
|
// </AlertDialog.Footer>
|
||||||
|
// </AlertDialog.Content>
|
||||||
|
// </AlertDialog.Portal>
|
||||||
|
// </AlertDialog>
|
||||||
|
//
|
||||||
|
// Architecturally: soma's AlertDialog re-uses Dialog's runtime (variant=
|
||||||
|
// alertdialog, modal=true forced). Eidos parts that map 1:1 to Dialog
|
||||||
|
// (Trigger / Overlay / Title / Description / Header / Footer / Portal)
|
||||||
|
// re-export the Dialog wrappers directly. Provider, Content, Action and
|
||||||
|
// Cancel are alert-dialog-specific.
|
||||||
|
import AlertDialogComponent from './alert-dialog.svelte';
|
||||||
|
import Content from './alert-dialog-content.svelte';
|
||||||
|
import Action from './alert-dialog-action.svelte';
|
||||||
|
import Cancel from './alert-dialog-cancel.svelte';
|
||||||
|
import {
|
||||||
|
default as Dialog
|
||||||
|
} from '$uix/eidos/components/dialog';
|
||||||
|
import { Portal } from '$soma/components/internal';
|
||||||
|
|
||||||
|
const Trigger = Dialog.Trigger;
|
||||||
|
const Overlay = Dialog.Overlay;
|
||||||
|
const Title = Dialog.Title;
|
||||||
|
const Description = Dialog.Description;
|
||||||
|
const Header = Dialog.Header;
|
||||||
|
const Footer = Dialog.Footer;
|
||||||
|
|
||||||
|
type AlertDialogNamespace = typeof AlertDialogComponent & {
|
||||||
|
Trigger: typeof Trigger;
|
||||||
|
Portal: typeof Portal;
|
||||||
|
Overlay: typeof Overlay;
|
||||||
|
Content: typeof Content;
|
||||||
|
Title: typeof Title;
|
||||||
|
Description: typeof Description;
|
||||||
|
Header: typeof Header;
|
||||||
|
Footer: typeof Footer;
|
||||||
|
Action: typeof Action;
|
||||||
|
Cancel: typeof Cancel;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogComponent as AlertDialogNamespace;
|
||||||
|
AlertDialog.Trigger = Trigger;
|
||||||
|
AlertDialog.Portal = Portal;
|
||||||
|
AlertDialog.Overlay = Overlay;
|
||||||
|
AlertDialog.Content = Content;
|
||||||
|
AlertDialog.Title = Title;
|
||||||
|
AlertDialog.Description = Description;
|
||||||
|
AlertDialog.Header = Header;
|
||||||
|
AlertDialog.Footer = Footer;
|
||||||
|
AlertDialog.Action = Action;
|
||||||
|
AlertDialog.Cancel = Cancel;
|
||||||
|
|
||||||
|
export { AlertDialog };
|
||||||
|
|
||||||
|
export default AlertDialog;
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AlertDialogProps,
|
||||||
|
AlertDialogActionProps as ActionProps,
|
||||||
|
AlertDialogCancelProps as CancelProps,
|
||||||
|
AlertDialogTriggerProps as TriggerProps,
|
||||||
|
AlertDialogContentProps as ContentProps,
|
||||||
|
AlertDialogOverlayProps as OverlayProps,
|
||||||
|
AlertDialogTitleProps as TitleProps,
|
||||||
|
AlertDialogDescriptionProps as DescriptionProps,
|
||||||
|
AlertDialogHeaderProps as HeaderProps,
|
||||||
|
AlertDialogFooterProps as FooterProps
|
||||||
|
} from './types';
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import type {
|
||||||
|
ProviderProps,
|
||||||
|
ActionProps,
|
||||||
|
CancelProps
|
||||||
|
} from '$soma/components/alert-dialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eidos `<AlertDialog>` — visual wrapper over `soma/alert-dialog`. A
|
||||||
|
* modal Dialog with `role="alertdialog"`, forced `modal=true` and
|
||||||
|
* `interactOutsideBehavior='ignore'` (Radix-style: the user MUST choose
|
||||||
|
* Action or Cancel — click-outside is not enough). Escape still cancels.
|
||||||
|
*
|
||||||
|
* Compound shape:
|
||||||
|
*
|
||||||
|
* <AlertDialog bind:open intent="risk">
|
||||||
|
* <AlertDialog.Trigger>Delete project…</AlertDialog.Trigger>
|
||||||
|
* <AlertDialog.Portal>
|
||||||
|
* <AlertDialog.Overlay />
|
||||||
|
* <AlertDialog.Content>
|
||||||
|
* <AlertDialog.Header>
|
||||||
|
* <AlertDialog.Title>Delete project?</AlertDialog.Title>
|
||||||
|
* <AlertDialog.Description>
|
||||||
|
* This action cannot be undone. All data will be lost.
|
||||||
|
* </AlertDialog.Description>
|
||||||
|
* </AlertDialog.Header>
|
||||||
|
* <AlertDialog.Footer>
|
||||||
|
* <AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
||||||
|
* <AlertDialog.Action>Delete</AlertDialog.Action>
|
||||||
|
* </AlertDialog.Footer>
|
||||||
|
* </AlertDialog.Content>
|
||||||
|
* </AlertDialog.Portal>
|
||||||
|
* </AlertDialog>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type AlertDialogProps = ProviderProps;
|
||||||
|
export type AlertDialogActionProps = ActionProps;
|
||||||
|
export type AlertDialogCancelProps = CancelProps;
|
||||||
|
|
||||||
|
// Trigger / Overlay / Content / Title / Description re-use Dialog's
|
||||||
|
// eidos wrappers — same recipe applies because soma's AlertDialog
|
||||||
|
// delegates to Dialog under the hood (`variant='alertdialog'`).
|
||||||
|
export type {
|
||||||
|
DialogTriggerProps as AlertDialogTriggerProps,
|
||||||
|
DialogOverlayProps as AlertDialogOverlayProps,
|
||||||
|
DialogContentProps as AlertDialogContentProps,
|
||||||
|
DialogTitleProps as AlertDialogTitleProps,
|
||||||
|
DialogDescriptionProps as AlertDialogDescriptionProps,
|
||||||
|
DialogHeaderProps as AlertDialogHeaderProps,
|
||||||
|
DialogFooterProps as AlertDialogFooterProps
|
||||||
|
} from '$uix/eidos/components/dialog/types';
|
||||||
Loading…
Reference in new issue