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.
163 lines
6.2 KiB
163 lines
6.2 KiB
import type { Morfo } from '../types';
|
|
import { v } from '../types';
|
|
|
|
/**
|
|
* FieldLangs — a localized-input wrapper around `Field` that holds one value
|
|
* per language and switches which language is being edited through a compact
|
|
* switcher docked INSIDE the field control (like a trailing unit selector).
|
|
*
|
|
* Surface (composition, not a fork — the picker pattern A27): the root owns the
|
|
* shared `values` map + `activeLang`; it composes the eidos `<Field>` and, at
|
|
* the logical `switcherSide` of the control, an in-control switcher whose form
|
|
* adapts to the language count — a segmented `ToggleGroup` for ≤3 languages,
|
|
* the design-system `Select` (ghost) for more (or forced via `switcher`).
|
|
*
|
|
* <FieldLangs languages={['en','es','fr']} required={['en']} bind:values>
|
|
* <FieldLangs.Label>Product name</FieldLangs.Label>
|
|
* <FieldLangs.Control><FieldLangs.Input /></FieldLangs.Control>
|
|
* <FieldLangs.ErrorText />
|
|
* </FieldLangs>
|
|
*
|
|
* The `Field` and `ToggleGroup` surfaces keep their own morfo contracts; this
|
|
* morfo only declares FieldLangs' own parts (root, switcher, per-language
|
|
* option) plus the language-switch event.
|
|
*/
|
|
export const fieldLangsMorfo = {
|
|
name: 'FieldLangs',
|
|
kebab: 'field-langs',
|
|
scope: ['soma', 'sema', 'eidos'],
|
|
// The language switch fires `shift-navigate`; its perceptual signature is
|
|
// the `shift` family base (a "noticed" visual settle, no sound by default).
|
|
// The switch-press tap comes from the composed ToggleGroup's own pack — no
|
|
// per-component tuning needed here.
|
|
expression: 'family-default',
|
|
apg: 'none — composite CMS pattern with no single APG entry; the composed Field carries textbox and the switcher is a WAI-ARIA radiogroup in both modes',
|
|
texts: {
|
|
label: '#?components.field-langs.label|Localized field',
|
|
'switcher-label': '#?components.field-langs.switcher-label|Select language'
|
|
},
|
|
events: [
|
|
{
|
|
// The active editing language changed — a context/régimen shift
|
|
// between the per-language values (book cap. 27: `shift`, not a state
|
|
// fixation). In segmented mode the composed ToggleGroup also fires its
|
|
// own `commit-toggle` for the switch press; this event marks the
|
|
// higher level "you moved to another language context". `shift` takes
|
|
// no intent (only `commit`/`signal` require one).
|
|
name: 'shift-navigate',
|
|
semantic: {
|
|
family: 'shift',
|
|
verb: 'navigate',
|
|
target: v.partRef('provider'),
|
|
sequence: 'post'
|
|
}
|
|
}
|
|
],
|
|
parts: [
|
|
{
|
|
name: 'Provider',
|
|
kebab: 'provider',
|
|
archetype: 'provider',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: false,
|
|
data: [
|
|
{ attr: 'data-active-lang', value: v.propRef('activeLang'), emit: 'value' },
|
|
{
|
|
attr: 'data-switcher-side',
|
|
values: ['start', 'end'],
|
|
value: v.propRef('switcherSide')
|
|
},
|
|
{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' },
|
|
{ attr: 'data-required', value: v.propRef('required'), severity: 'optional' },
|
|
{ attr: 'data-invalid', value: v.propRef('invalid'), severity: 'optional' }
|
|
],
|
|
aria: []
|
|
},
|
|
{
|
|
// The in-control switcher. Segmented mode = FieldLangs' OWN radiogroup
|
|
// of `LangOption` buttons (NOT the composed ToggleGroup — its
|
|
// `commit-toggle` semantics are wrong for a language-context
|
|
// navigation, which is `shift-navigate`; composing it would also leak
|
|
// its chrome into the control). Select mode = the design-system
|
|
// `<Select>` (ghost). FieldLangs adds its identity marker + `data-side`
|
|
// (which control edge it docks on, logical / RTL-aware) + `data-mode`.
|
|
// Private: the consumer never composes it — the Control injects it.
|
|
name: 'Switcher',
|
|
kebab: 'switcher',
|
|
archetype: 'group',
|
|
kind: 'private',
|
|
defaultElement: 'div',
|
|
optional: false,
|
|
data: [
|
|
{ attr: 'data-side', values: ['start', 'end'], value: v.propRef('side') },
|
|
{ attr: 'data-mode', values: ['segmented', 'select'], value: v.propRef('mode') }
|
|
],
|
|
aria: [
|
|
{
|
|
attr: 'aria-label',
|
|
value: v.translationRef('switcher-label', 'Select language'),
|
|
severity: 'recommended'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
// One radio button per supported language in SEGMENTED mode — a
|
|
// FieldLangs-owned `<button role="radio">`. Carries the per-language
|
|
// identity + completion metadata the recipe styles (the set/unset dot,
|
|
// the solid-primary active pill). Optional — absent in select mode.
|
|
// Private — generated from `languages`, never composed.
|
|
name: 'LangOption',
|
|
kebab: 'lang-option',
|
|
archetype: 'option',
|
|
kind: 'private',
|
|
defaultElement: 'button',
|
|
role: 'radio',
|
|
optional: true,
|
|
data: [
|
|
{ attr: 'data-lang', value: v.propRef('lang'), emit: 'value' },
|
|
{ attr: 'data-active', value: v.propRef('active'), severity: 'optional' },
|
|
{ attr: 'data-required', value: v.propRef('required'), severity: 'optional' },
|
|
{ attr: 'data-complete', value: v.propRef('complete'), severity: 'optional' }
|
|
],
|
|
aria: [{ attr: 'aria-checked', value: v.propRef('active'), ariaBoolean: true }]
|
|
},
|
|
{
|
|
// The multiline control shell — a `position: relative` wrapper around a
|
|
// composed `TextArea` with the switcher docked in its top corner (so
|
|
// the field's default floating label keeps working). Public: the
|
|
// consumer composes `<FieldLangs.Textarea>` instead of
|
|
// `<FieldLangs.Control><FieldLangs.Input/></FieldLangs.Control>` for a
|
|
// multiline localized field. Optional — the single-line path omits it.
|
|
name: 'Textarea',
|
|
kebab: 'textarea',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [],
|
|
aria: []
|
|
},
|
|
{
|
|
// The dropdown list in SELECT mode — a vertical radiogroup of
|
|
// `LangOption` buttons inside a composed `Popover`. FieldLangs-owned,
|
|
// so selecting a language fires only `shift-navigate`; no `Select`
|
|
// commit semantics leak into the field. Optional — absent in segmented
|
|
// mode. Private — generated from `languages`.
|
|
name: 'Menu',
|
|
kebab: 'menu',
|
|
archetype: 'group',
|
|
kind: 'private',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [],
|
|
aria: [
|
|
{
|
|
attr: 'aria-label',
|
|
value: v.translationRef('switcher-label', 'Select language'),
|
|
severity: 'recommended'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
} as const satisfies Morfo;
|