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.
svelte-kit-vice/src/uix/morfo/components/float-panel.ts

393 lines
13 KiB

import type { Morfo } from '../types';
import { v } from '../types';
export const floatPanelMorfo = {
name: 'FloatPanel',
kebab: 'float-panel',
scope: ['soma', 'sema', 'eidos'],
expression: 'pack',
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/',
texts: {
label: '#?components.float-panel.label|Floating panel',
'content.roledescription': '#?components.float-panel.content.roledescription|window'
},
events: [
{
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
// Verb per book cap. 26 §5: a floating panel opens "con marco
// propio" — emerge.open (was `present`; C2 verbs pass, 2026-07-07).
name: 'open',
semantic: {
family: 'emerge',
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
verb: 'open',
target: v.partRef('content'),
sequence: 'pre'
},
commits: { part: v.partRef('content'), attr: 'data-state', value: 'open' }
},
{
// Polymorphic close (book §5.3) — mirror of Popover / Dialog / Drawer.
// Provider concretes the cause via `dismissWith(action, opts?)`:
// sets `data-last-action` imperatively + passes `opts.semantic`.
name: 'close',
semantic: {
family: 'emerge',
verb: 'close',
target: v.partRef('content'),
sequence: 'pre',
persistence: 'transient',
allowedFamilies: ['emerge', 'commit', 'signal']
},
regime: 'lock',
commits: { part: v.partRef('content'), attr: 'data-state', value: 'closed' }
},
{
// Pickup cue when a drag (move) gesture starts on the Header handle.
name: 'drag-start',
semantic: {
family: 'handle',
verb: 'pick',
target: v.partRef('content'),
sequence: 'pre',
channels: ['sound', 'haptic']
}
},
{
// Drop cue when the move gesture ends. Continuous movement does NOT
// emit per-frame (would buzz haptics nonstop — virtual-list doctrine).
name: 'drag-end',
semantic: {
family: 'handle',
verb: 'drop',
target: v.partRef('content'),
sequence: 'post',
channels: ['haptic']
}
},
{
name: 'resize-start',
semantic: {
family: 'handle',
verb: 'resize',
target: v.partRef('content'),
sequence: 'pre',
channels: ['haptic']
}
},
{
name: 'resize-end',
semantic: {
family: 'handle',
verb: 'resize',
target: v.partRef('content'),
sequence: 'post',
channels: ['haptic']
}
}
],
focus: {
initial: 'first-focusable',
// Non-modal by default — the provider resolves the effective trap from
// the `modal` prop (modal panels trap, free-floating panels don't).
trap: false,
return: 'trigger',
restore: true
},
parts: [
{
name: 'Provider',
kebab: 'provider',
archetype: 'provider',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') }],
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-haspopup', value: v.literal('dialog') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
}
],
keyboard: [
{ key: 'Enter', action: 'open' },
{ key: ' ', action: 'open' }
]
},
{
name: 'Content',
kebab: 'content',
archetype: 'content',
kind: 'public',
defaultElement: 'div',
role: 'dialog',
optional: false,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') },
{
attr: 'data-stage',
values: ['default', 'minimized', 'maximized'],
value: v.propRef('stage'),
severity: 'optional'
},
{ attr: 'data-dragging', value: v.propRef('dragging'), severity: 'optional' },
{ attr: 'data-resizing', value: v.propRef('resizing'), severity: 'optional' },
fix(contracts): catalogue-guards census — 12 -> 3 failures (knob/menu-dial/onion-menu + unmasked debt) The "3 ajenos" census. What the 12 pre-existing failures really were: a few genuine violations + several guards CRASHING on menu-dial's missing exports.ts (ENOENT) and hiding their true violation lists. All addressed except the two runtime-discipline files (see tail). - knob: soma barrel export + the missing soma README (headless contract — parts/ARIA/absolute angular keyboard/props/published CSS vars/sema table). - menu-dial + onion-menu: joined SOMA_INTERNAL_PRIMITIVES — both are documented provider-less bridges (keyboard dispatch + nav maths + emission; the eidos root owns state and render). The scope guard now subtracts the internal set from BOTH sides so a bridge may truthfully keep 'soma' in its morfo scope without being a public namespace. Self-alias imports fixed; the two morfo docblocks realigned to the implementation ("soma owns open" was drift — the bridge files own dispatch/emission only; onion-menu's stale "family-default, no pack" tail also corrected, S3b shipped its pack). - Public barrels decoupled from provider implementation filenames (the picker `internals.ts` house pattern) for the 6 offenders: calendar, cropper, field-langs, gradient-builder, gradient-picker, image-adjustments. - listSourceFiles now skips ACTIVE_DEV_TRACK paths (words/palabras/chronos) — the track doctrine already said catalogue guards skip them; the file-scanning guards just didn't. - data-archetype added to the known morfo attrs (compiler-emitted from part archetype declarations); the four field-overlay picker triggers (color-picker, date-range-picker, time-picker, time-range-picker) declared archetype 'trigger' while stamping 'field-trigger' at runtime — morfos aligned to the date-picker/ntp precedent (contract now matches the DOM; zero runtime change, the stamp already won). - float-panel: the consumer-placed data-float-panel-no-drag opt-out marker is now declared in the morfo (presence entry with the consumer-marker note) — the NON_DRAG_SELECTOR contract stops being invisible. Remaining tail (3 failures, deliberately deferred to a dedicated runtime-discipline session with browser gesture verification): float-panel-provider direct style writes + raw timer (drag/resize hot path -> dom.apply/uix.timers migration) and media-player media-provider raw listeners (-> dom.listen). Gates: contracts 34/37 (was 25/37) · component-audit 102 PASS · vocab exit 0 · type baseline 57 · eidos-lint invalid 0 on the four touched pickers · compile + eidos contract tests 42/42. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
// CONSUMER-placed opt-out marker (never emitted by the runtime):
// elements carrying it inside a `dragHandle="content"` panel do not
// start a drag — the provider's NON_DRAG_SELECTOR consumes it
// alongside the native interactive tags.
{ attr: 'data-float-panel-no-drag', severity: 'optional' },
// Which element grabs the drag — lets the recipe put the grab cursor
// on the whole surface when `dragHandle="content"`. `values` makes the
// runtime emit the STRING (header/content), not a boolean presence.
{
attr: 'data-drag-handle',
values: ['header', 'content'],
value: v.propRef('dragHandle'),
severity: 'optional'
},
{ attr: 'data-topmost', value: v.propRef('topmost'), severity: 'optional' },
{ attr: 'data-behind', value: v.propRef('behind'), severity: 'optional' },
{
attr: 'data-last-action',
values: ['saved', 'cancelled', 'dismissed', 'dismissed-outside', 'failed'],
severity: 'optional'
},
{
attr: 'data-starting-style',
severity: 'optional',
condition: { when: 'state-equals', state: 'open', value: 'starting' }
},
{
attr: 'data-ending-style',
severity: 'optional',
condition: { when: 'state-equals', state: 'closed', value: 'ending' }
}
],
aria: [
{
attr: 'aria-labelledby',
value: v.propRef('ariaLabelledby'),
condition: { when: 'prop-truthy', prop: 'ariaLabelledby' },
severity: 'recommended'
},
{
attr: 'aria-describedby',
value: v.propRef('ariaDescribedby'),
condition: { when: 'prop-truthy', prop: 'ariaDescribedby' },
severity: 'optional'
},
{
attr: 'aria-modal',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'modal' }
},
{
// SR users hear "… window" rather than just "dialog".
attr: 'aria-roledescription',
value: v.translationRef('content.roledescription', 'window'),
severity: 'recommended'
}
],
keyboard: [
{ key: 'Escape', action: 'close' },
{ key: 'Tab', action: 'focus-next' },
{ key: 'Shift+Tab', action: 'focus-prev' }
]
},
{
// Drag handle. Grabbing it moves the panel. `data-dragging` lets the
// recipe swap the cursor (grab → grabbing).
name: 'Header',
kebab: 'header',
archetype: 'header',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [
{ attr: 'data-dragging', value: v.propRef('dragging'), severity: 'optional' },
// Keyboard "grab" state — eidos styles a grabbed-handle ring.
{ attr: 'data-grabbed', value: v.propRef('moveMode'), severity: 'optional' }
],
aria: [
// Focusable drag-handle: a keyboard user tabs here and presses
// Enter/Space to enter a move "grab mode", then the arrows move it.
{ attr: 'tabindex', value: v.literal('0') },
{ attr: 'aria-keyshortcuts', value: v.literal('Enter Space') }
],
// Move keys are gated on `moveMode` so out-of-mode arrows BUBBLE (the
// runtime skips an entry whose condition is falsy → no preventDefault).
// One `kb-move` handler reads the event (key + Shift) and applies.
keyboard: [
{ key: 'Enter', action: 'kb-move-toggle', condition: { when: 'prop-truthy', prop: 'draggable' } },
{ key: ' ', action: 'kb-move-toggle', condition: { when: 'prop-truthy', prop: 'draggable' } },
{ key: 'Escape', action: 'kb-move-cancel', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'ArrowUp', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'ArrowDown', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'ArrowLeft', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'ArrowRight', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'Shift+ArrowUp', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'Shift+ArrowDown', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'Shift+ArrowLeft', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'Shift+ArrowRight', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'PageUp', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'PageDown', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'Home', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } },
{ key: 'End', action: 'kb-move', condition: { when: 'prop-truthy', prop: 'moveMode' } }
]
},
{
name: 'Title',
kebab: 'title',
archetype: 'title',
kind: 'public',
defaultElement: 'div',
role: 'heading',
optional: true,
data: [],
aria: [{ attr: 'aria-level', value: v.propRef('level'), 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'
}
]
},
{
// Optional button cluster in the Header (right side) — holds Close /
// Action and any custom buttons, grouped + right-aligned. Mirror of
// Ark's `control` group. Purely structural (no state / events).
name: 'Controls',
kebab: 'controls',
archetype: 'group',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{
// Generic Header action button (e.g. a delete icon in a properties
// panel). The panel does NOT own its behaviour — the consumer wires
// `onclick` + `aria-label` via passthrough. Repeatable.
name: 'Action',
kebab: 'action',
archetype: 'action',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [],
aria: [{ attr: 'type', value: v.literal('button') }]
},
{
// Switches the panel's stage (minimize / maximize / restore). The
// target stage is a soma-level prop; clicking it while already in that
// stage returns to 'default' (toggle). Repeatable.
name: 'StageTrigger',
kebab: 'stage-trigger',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [],
aria: [{ attr: 'type', value: v.literal('button') }]
},
{
// Scrollable content region (below the Header).
name: 'Body',
kebab: 'body',
archetype: 'viewport',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{
// Resize affordance. Repeatable; `data-resize-edge` names which
// edge/corner this handle drives (n/s/e/w/ne/nw/se/sw).
name: 'ResizeHandle',
kebab: 'resize-handle',
archetype: 'thumb',
kind: 'public',
defaultElement: 'div',
role: 'presentation',
optional: true,
data: [
{ attr: 'data-resize-edge', severity: 'optional' },
{ attr: 'data-resizing', value: v.propRef('resizing'), severity: 'optional' }
],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
// Resize affordance for the se corner — a focusable button that owns
// BOTH pointer resize (pointerdown → se) and keyboard resize (Enter/Space
// enters a grab mode, arrows grow/shrink se-anchored). The keyboard path
// makes resize fully keyboard-operable (WCAG 2.1.1), which the aria-hidden
// pointer-only ResizeHandle divs do not.
name: 'ResizeGrip',
kebab: 'resize-grip',
archetype: 'thumb',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-grabbed', value: v.propRef('resizeMode'), severity: 'optional' }],
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'tabindex', value: v.literal('0') },
{ attr: 'aria-keyshortcuts', value: v.literal('Enter Space') }
],
keyboard: [
{ key: 'Enter', action: 'kb-resize-toggle', condition: { when: 'prop-truthy', prop: 'resizable' } },
{ key: ' ', action: 'kb-resize-toggle', condition: { when: 'prop-truthy', prop: 'resizable' } },
{ key: 'Escape', action: 'kb-resize-cancel', condition: { when: 'prop-truthy', prop: 'resizeMode' } },
{ key: 'ArrowUp', action: 'kb-resize', condition: { when: 'prop-truthy', prop: 'resizeMode' } },
{ key: 'ArrowDown', action: 'kb-resize', condition: { when: 'prop-truthy', prop: 'resizeMode' } },
{ key: 'ArrowLeft', action: 'kb-resize', condition: { when: 'prop-truthy', prop: 'resizeMode' } },
{ key: 'ArrowRight', action: 'kb-resize', condition: { when: 'prop-truthy', prop: 'resizeMode' } },
{ key: 'Shift+ArrowUp', action: 'kb-resize', condition: { when: 'prop-truthy', prop: 'resizeMode' } },
{ key: 'Shift+ArrowDown', action: 'kb-resize', condition: { when: 'prop-truthy', prop: 'resizeMode' } },
{ key: 'Shift+ArrowLeft', action: 'kb-resize', condition: { when: 'prop-truthy', prop: 'resizeMode' } },
{ key: 'Shift+ArrowRight', action: 'kb-resize', condition: { when: 'prop-truthy', prop: 'resizeMode' } },
{ key: 'Home', action: 'kb-resize', condition: { when: 'prop-truthy', prop: 'resizeMode' } },
{ key: 'End', action: 'kb-resize', condition: { when: 'prop-truthy', prop: 'resizeMode' } }
]
}
]
} as const satisfies Morfo;

Powered by TurnKey Linux.