You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
5.6 KiB
243 lines
5.6 KiB
import type { Morfo } from '../types';
|
|
import { v } from '../types';
|
|
import { INTENTS } from '../../intent';
|
|
|
|
export const toastMorfo = {
|
|
name: 'Toast',
|
|
kebab: 'toast',
|
|
scope: ['soma', 'sema'],
|
|
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/alert/',
|
|
texts: {
|
|
label: '#?components.toast.label|Toast'
|
|
},
|
|
events: [
|
|
{
|
|
name: 'present',
|
|
semantic: {
|
|
family: 'emerge',
|
|
verb: 'present',
|
|
target: v.partRef('item'),
|
|
sequence: 'pre'
|
|
}
|
|
},
|
|
{
|
|
// Persistence: explicit `transient` even though the book §6.2
|
|
// canon would imply `untilFix` / `untilAction` for `signal +
|
|
// risk` / `signal + threat`. The Toast component owns its own
|
|
// lifecycle via `Toast.Provider`'s auto-dismiss timer + user
|
|
// dismiss; the `data-event-*` projection is a one-shot pulse
|
|
// when the toast enters. Persistence of the toast surface
|
|
// itself lives in `data-state` (open/closed) on the Item, not
|
|
// in the signal projection.
|
|
name: 'announce',
|
|
semantic: {
|
|
family: 'signal',
|
|
verb: 'announce',
|
|
target: v.partRef('item'),
|
|
sequence: 'coincident',
|
|
intent: {
|
|
fromProp: 'intent',
|
|
default: 'neutral',
|
|
supported: INTENTS
|
|
},
|
|
persistence: 'transient'
|
|
}
|
|
},
|
|
{
|
|
name: 'dismiss',
|
|
semantic: {
|
|
family: 'emerge',
|
|
verb: 'dismiss',
|
|
target: v.partRef('item'),
|
|
sequence: 'pre'
|
|
}
|
|
}
|
|
],
|
|
parts: [
|
|
{
|
|
name: 'Provider',
|
|
kebab: 'provider',
|
|
archetype: 'provider',
|
|
kind: 'virtual',
|
|
defaultElement: 'none',
|
|
optional: false,
|
|
data: [],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'Viewport',
|
|
kebab: 'viewport',
|
|
archetype: 'viewport',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
role: 'region',
|
|
optional: false,
|
|
data: [],
|
|
aria: [
|
|
{
|
|
attr: 'aria-label',
|
|
value: v.propRef('label'),
|
|
severity: 'recommended'
|
|
},
|
|
{ attr: 'aria-live', value: v.literal('polite') }
|
|
]
|
|
},
|
|
{
|
|
name: 'Item',
|
|
kebab: 'item',
|
|
archetype: 'item',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: false,
|
|
states: ['open', 'closed'],
|
|
data: [
|
|
{ attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') },
|
|
{ attr: 'data-starting-style', severity: 'optional' },
|
|
{ attr: 'data-ending-style', severity: 'optional' },
|
|
{
|
|
attr: 'data-intent',
|
|
values: INTENTS,
|
|
value: v.propRef('intent')
|
|
},
|
|
{ attr: 'data-loading', severity: 'optional', value: v.propRef('loading') },
|
|
{
|
|
attr: 'data-swipe',
|
|
values: ['start', 'move', 'cancel', 'end'],
|
|
severity: 'optional',
|
|
value: v.propRef('swipeState')
|
|
},
|
|
{
|
|
attr: 'data-swipe-direction',
|
|
values: ['up', 'down', 'left', 'right'],
|
|
value: v.propRef('swipeDirection')
|
|
}
|
|
],
|
|
aria: [
|
|
{
|
|
attr: 'role',
|
|
value: v.mapRef(v.propRef('intent'), {
|
|
neutral: 'status',
|
|
affirm: 'status',
|
|
fulfill: 'status',
|
|
risk: 'alert',
|
|
threat: 'alert',
|
|
// `loss` is a consummated negative consequence (no urgency
|
|
// to interrupt) — `status` is the appropriate ARIA role.
|
|
loss: 'status'
|
|
})
|
|
},
|
|
{
|
|
attr: 'aria-live',
|
|
value: v.mapRef(v.propRef('intent'), {
|
|
neutral: 'polite',
|
|
affirm: 'polite',
|
|
fulfill: 'polite',
|
|
risk: 'assertive',
|
|
threat: 'assertive',
|
|
loss: 'polite'
|
|
}),
|
|
severity: 'recommended'
|
|
},
|
|
{ attr: 'aria-atomic', value: v.literal('true') },
|
|
{
|
|
attr: 'aria-labelledby',
|
|
value: v.partRef('title'),
|
|
condition: { when: 'part-present', part: 'title' },
|
|
severity: 'recommended'
|
|
},
|
|
{
|
|
attr: 'aria-describedby',
|
|
value: v.partRef('description'),
|
|
condition: { when: 'part-present', part: 'description' },
|
|
severity: 'optional'
|
|
}
|
|
],
|
|
keyboard: [{ key: 'Escape', action: 'close' }]
|
|
},
|
|
{
|
|
name: 'Title',
|
|
kebab: 'title',
|
|
archetype: 'title',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'Description',
|
|
kebab: 'description',
|
|
archetype: 'description',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [],
|
|
aria: []
|
|
},
|
|
{
|
|
/**
|
|
* Status disc — the leading-edge icon area whose color follows
|
|
* the Item's `data-intent`. Optional visual structural part:
|
|
* eidos's flat template ships one by default, but a consumer
|
|
* compounding the parts can omit it (the recipe's grid auto-
|
|
* collapses) or replace its contents.
|
|
*/
|
|
name: 'Status',
|
|
kebab: 'status',
|
|
archetype: 'indicator',
|
|
kind: 'public',
|
|
defaultElement: 'span',
|
|
optional: true,
|
|
data: [],
|
|
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
|
|
},
|
|
{
|
|
/**
|
|
* Main column — stack container for title + description + action.
|
|
* Pure layout part (no semantics, no states); declared so the
|
|
* recipe's `[data-toast-main]` selector is morfo-backed instead
|
|
* of an off-contract data-attr.
|
|
*/
|
|
name: 'Main',
|
|
kebab: 'main',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'Action',
|
|
kebab: 'action',
|
|
archetype: 'action',
|
|
kind: 'public',
|
|
defaultElement: 'button',
|
|
role: 'button',
|
|
optional: true,
|
|
data: [],
|
|
aria: [
|
|
{ attr: 'type', value: v.literal('button') },
|
|
{ attr: 'aria-label', value: v.propRef('altText'), severity: 'recommended' }
|
|
]
|
|
},
|
|
{
|
|
name: 'Close',
|
|
kebab: 'close',
|
|
archetype: 'close',
|
|
kind: 'public',
|
|
defaultElement: 'button',
|
|
role: 'button',
|
|
optional: true,
|
|
data: [],
|
|
aria: [
|
|
{ attr: 'type', value: v.literal('button') },
|
|
{
|
|
attr: 'aria-label',
|
|
value: v.commonRef('buttons.close', 'Close'),
|
|
severity: 'recommended'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
} as const satisfies Morfo;
|