diff --git a/src/uix/active-uix/id/create-id.ts b/src/uix/active-uix/id/create-id.ts
index b687e4d01..ba4b44389 100644
--- a/src/uix/active-uix/id/create-id.ts
+++ b/src/uix/active-uix/id/create-id.ts
@@ -2,8 +2,8 @@
* Create an id from a Svelte `$props.id()` uid, with optional component context.
*
* ```ts
- * createId(uid) // "soma-c12"
- * createId(uid, 'dialog-trigger') // "soma-dialog-trigger-c12"
+ * createId(uid) // "uix-c12"
+ * createId(uid, 'dialog-trigger') // "uix-dialog-trigger-c12"
* ```
*
* The `uid` comes from the consuming component's `$props.id()`, so the output
@@ -12,9 +12,10 @@
*
* Lives in active-uix (imported by subpath `$active-uix/id`) so every UIX layer
* — soma, eidos, demos, apps — shares one id authority without cross-layer
- * coupling. The `soma-` prefix is the UIX id convention, kept for output
- * stability across the migration.
+ * coupling. The prefix is `uix-` (the framework), NOT a layer name: the same
+ * doctrine that bans `data-soma-*` / `--soma-*` keeps a layer name out of
+ * element ids too.
*/
export function createId(uid: string, component?: string): string {
- return component ? `soma-${component}-${uid}` : `soma-${uid}`
+ return component ? `uix-${component}-${uid}` : `uix-${uid}`
}
diff --git a/src/uix/active-uix/id/use-id.ts b/src/uix/active-uix/id/use-id.ts
index b321a5884..9071cc603 100644
--- a/src/uix/active-uix/id/use-id.ts
+++ b/src/uix/active-uix/id/use-id.ts
@@ -8,11 +8,14 @@
* NOT for SSR / ARIA element ids derived from the component tree — those use
* `createId` (same module), which wraps Svelte's `$props.id()` for
* hydration-stable output. A global counter would desync server / client.
+ *
+ * Prefix is `uix-` (framework-neutral), never a layer name — same doctrine as
+ * the `data-soma-*` / `--soma-*` bans.
*/
let counter = 0
export function useId(component = ''): string {
const id = ++counter
- return component ? `soma-${component}-${id}` : `soma-${id}`
+ return component ? `uix-${component}-${id}` : `uix-${id}`
}
diff --git a/src/uix/eidos/components/color-field/color-field-segment.svelte b/src/uix/eidos/components/color-field/color-field-segment.svelte
index 640d68aac..e11a4c3b6 100644
--- a/src/uix/eidos/components/color-field/color-field-segment.svelte
+++ b/src/uix/eidos/components/color-field/color-field-segment.svelte
@@ -1,10 +1,11 @@
-
+
{@render children?.()}
diff --git a/src/uix/eidos/components/color-field/color-field.css b/src/uix/eidos/components/color-field/color-field.css
index 90dd0037e..822b84d44 100644
--- a/src/uix/eidos/components/color-field/color-field.css
+++ b/src/uix/eidos/components/color-field/color-field.css
@@ -178,12 +178,8 @@
color: var(--color-field-placeholder-color);
}
-[data-color-field-segment]:focus-visible {
- /* Active highlight: shared `--field-segment-active-*` foundation tokens. */
- background: var(--field-segment-active-bg);
- color: var(--field-segment-active-text);
- font-weight: 600;
-}
+/* Segment hover + active states are shared — see field/field-segment-state.css
+ (selects `[data-field-segment]`, the eidos marker on every field segment). */
[data-color-field-segment][data-invalid] {
color: var(--color-field-segment-invalid-color);
diff --git a/src/uix/eidos/components/date-field/date-field-segment.svelte b/src/uix/eidos/components/date-field/date-field-segment.svelte
index 2a5297bf6..e9282adae 100644
--- a/src/uix/eidos/components/date-field/date-field-segment.svelte
+++ b/src/uix/eidos/components/date-field/date-field-segment.svelte
@@ -1,10 +1,11 @@
-
+
{@render children?.()}
diff --git a/src/uix/eidos/components/date-field/date-field.css b/src/uix/eidos/components/date-field/date-field.css
index b085c7c66..4936ea014 100644
--- a/src/uix/eidos/components/date-field/date-field.css
+++ b/src/uix/eidos/components/date-field/date-field.css
@@ -157,7 +157,10 @@
display: inline-flex;
align-items: center;
justify-content: center;
- min-inline-size: var(--date-field-segment-min-width);
+ /* Fixed 2-digit width (font-relative) + tabular figures (shared layer) so the
+ segment never reflows when the value changes 1 → 2 digits or between glyphs.
+ border-box: hold 2ch of content + the inline padding. */
+ min-inline-size: calc(2ch + 2 * var(--_date-field-segment-px));
block-size: var(--date-field-segment-height);
padding-inline: var(--_date-field-segment-px);
border-radius: var(--date-field-segment-radius);
@@ -191,32 +194,8 @@
cursor: text;
}
-[data-date-field-segment]:not([data-segment='literal']):not([data-readonly]):not([data-disabled]):hover {
- background: var(--date-field-segment-hover-bg, color-mix(in srgb, var(--color-content-primary) 6%, transparent));
-}
-
-/*
- * Active segment — the one the user is editing (focused via click,
- * tap, arrow-key navigation or paste). Catch BOTH `:focus` and
- * `:focus-visible` so the affordance shows regardless of input
- * modality — segments are direct-manipulation surfaces where the
- * user is about to type, so always advertising the active target
- * is correct (vs the global `:focus-visible` convention which
- * suppresses focus rings on mouse-down).
- *
- * Visual: accent element bg + accent text + bold weight. NO border/ring —
- * the active segment is highlighted by background only (homogeneous across
- * the date/time/color field family).
- */
-[data-date-field-segment]:focus,
-[data-date-field-segment]:focus-visible {
- /* Active highlight: theme-owned via the shared `--field-segment-active-*`
- foundation tokens (the bg carries its own transparency). */
- background: var(--field-segment-active-bg);
- color: var(--field-segment-active-text);
- font-weight: 600;
- outline: none;
-}
+/* Segment hover + active states are shared — see field/field-segment-state.css
+ (selects `[data-field-segment]`, the eidos marker on every field segment). */
[data-date-field-segment][data-invalid] {
color: var(--date-field-segment-invalid-color);
diff --git a/src/uix/eidos/components/date-picker/date-picker-trigger.svelte b/src/uix/eidos/components/date-picker/date-picker-trigger.svelte
index 91ca34d21..3775de82b 100644
--- a/src/uix/eidos/components/date-picker/date-picker-trigger.svelte
+++ b/src/uix/eidos/components/date-picker/date-picker-trigger.svelte
@@ -2,6 +2,7 @@
import { ActiveEidos } from '$uix/eidos';
import * as DatePicker from '$soma/components/date-picker';
import CalendarIcon from '../icon/lib/calendar.svelte';
+ import '../field/field-control-trigger.css';
import { getDatePickerVisualContext } from './context.svelte';
import type { DatePickerTriggerProps } from './types';
@@ -16,6 +17,7 @@
as a descendant of
- * , the icon becomes a flush affordance at the input's
- * trailing edge: no border, no separate background, smaller hit area sized
- * to the segment height, and a subtle hover tint that doesn't compete with
- * the field's focus ring. The standalone treatment above stays available
- * for consumers who place the trigger as a sibling. */
-
-[data-date-field-input] [data-date-picker-trigger] {
- flex: 0 0 auto;
- /* `--date-field-segment-height` is scoped to `[data-date-field]`, which the
- picker doesn't render — so it's undefined here and the box collapsed to the
- icon. Inline the same calc off the picker-local `--_date-field-height` (which
- scales per size). `font: inherit` so the `1em` icon tracks the field font. */
- inline-size: calc(var(--_date-field-height) - var(--space-2));
- block-size: calc(var(--_date-field-height) - var(--space-2));
- margin-inline-start: auto;
- padding: 0;
- border: none;
- border-radius: var(--date-field-segment-radius);
- background: transparent;
- color: var(--date-field-segment-color);
- box-shadow: none;
- font: inherit;
-}
-
-[data-date-field-input] [data-date-picker-trigger]:hover {
- border: none;
- background: var(--_date-field-accent-track);
- color: var(--_date-field-accent-text);
-}
-
-[data-date-field-input] [data-date-picker-trigger]:focus-visible {
- outline: none;
- border: none;
- background: var(--field-segment-active-bg);
- color: var(--field-segment-active-text);
-}
+/* Trigger chrome (flush affordance, hover / focus states, size) is shared —
+ see field/field-control-trigger.css (`[data-field-control-trigger]`, the
+ eidos marker on the trigger). */
[data-popover-content]:has([data-date-picker-calendar]) {
--_popover-content-width-override: max-content;
@@ -216,9 +143,3 @@
inline-size: max-content;
min-inline-size: max-content;
}
-
-@media (prefers-reduced-motion: reduce) {
- [data-date-picker-trigger] {
- transition: none;
- }
-}
diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker-trigger.svelte b/src/uix/eidos/components/date-range-picker/date-range-picker-trigger.svelte
index 36bd7680f..005f0407f 100644
--- a/src/uix/eidos/components/date-range-picker/date-range-picker-trigger.svelte
+++ b/src/uix/eidos/components/date-range-picker/date-range-picker-trigger.svelte
@@ -1,7 +1,7 @@
-
+
{@render children?.()}
diff --git a/src/uix/eidos/components/time-field/time-field.css b/src/uix/eidos/components/time-field/time-field.css
index f418301e3..17d79b015 100644
--- a/src/uix/eidos/components/time-field/time-field.css
+++ b/src/uix/eidos/components/time-field/time-field.css
@@ -173,12 +173,8 @@
color: var(--time-field-placeholder-color);
}
-[data-time-field-segment]:focus-visible {
- /* Active highlight: shared `--field-segment-active-*` foundation tokens. */
- background: var(--field-segment-active-bg);
- color: var(--field-segment-active-text);
- font-weight: 600;
-}
+/* Segment hover + active states are shared — see field/field-segment-state.css
+ (selects `[data-field-segment]`, the eidos marker on every field segment). */
[data-time-field-segment][data-invalid] {
color: var(--time-field-segment-invalid-color);
diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css
index 87f437051..7a551fb87 100644
--- a/src/uix/eidos/generated/base.css
+++ b/src/uix/eidos/generated/base.css
@@ -180,6 +180,8 @@
--focus-ring-error: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color-error), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color-error);
--field-segment-active-bg: color-mix(in srgb, var(--color-primary-border) 40%, transparent);
--field-segment-active-text: var(--color-primary-text);
+ --field-segment-hover-bg: color-mix(in srgb, var(--color-content-primary) 6%, transparent);
+ --field-control-trigger-hover-bg: color-mix(in srgb, var(--color-content-primary) 8%, transparent);
--container-width-sm: 448px;
--container-width-md: 688px;
--container-width-lg: 880px;
@@ -1425,7 +1427,6 @@
--date-field-segment-readonly-bg: var(--color-surface-overlay);
--date-field-segment-readonly-color: var(--color-content-secondary);
--date-field-segment-readonly-decoration: underline dotted var(--color-content-muted);
- --date-field-segment-hover-bg: color-mix(in srgb, var(--color-content-primary) 6%, transparent);
--date-field-transition-duration: var(--duration-fast);
--date-field-transition-ease: var(--ease-default);
--date-field-disabled-opacity: var(--opacity-disabled);
@@ -2117,14 +2118,9 @@
--password-field-color: var(--color-content-primary);
--password-field-placeholder-color: var(--color-content-muted);
--password-field-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
- --password-field-trigger-radius: var(--radius-full);
- --password-field-trigger-border-width: var(--border-width);
- --password-field-trigger-border: transparent;
- --password-field-trigger-border-hover: var(--color-border-subtle);
+ --password-field-trigger-radius: var(--radius-sm);
--password-field-trigger-bg: transparent;
- --password-field-trigger-bg-hover: var(--color-surface-raised);
--password-field-trigger-color: var(--color-content-secondary);
- --password-field-trigger-color-hover: var(--color-content-primary);
--password-field-trigger-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--password-field-icon-size-xs: var(--control-height-xs);
--password-field-icon-size-sm: var(--control-height-xs);
diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts
index 14ad2b301..3ff5e0924 100644
--- a/src/uix/eidos/lib/recipes/base.ts
+++ b/src/uix/eidos/lib/recipes/base.ts
@@ -1249,7 +1249,6 @@ export const THEME_BASE_RECIPE_TOKENS = {
'segment-readonly-bg': 'var(--color-surface-overlay)',
'segment-readonly-color': 'var(--color-content-secondary)',
'segment-readonly-decoration': 'underline dotted var(--color-content-muted)',
- 'segment-hover-bg': 'color-mix(in srgb, var(--color-content-primary) 6%, transparent)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
@@ -2156,14 +2155,9 @@ export const THEME_BASE_RECIPE_TOKENS = {
'placeholder-color': 'var(--color-content-muted)',
'focus-shadow':
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
- 'trigger-radius': 'var(--radius-full)',
- 'trigger-border-width': 'var(--border-width)',
- 'trigger-border': 'transparent',
- 'trigger-border-hover': 'var(--color-border-subtle)',
+ 'trigger-radius': 'var(--radius-sm)',
'trigger-bg': 'transparent',
- 'trigger-bg-hover': 'var(--color-surface-raised)',
'trigger-color': 'var(--color-content-secondary)',
- 'trigger-color-hover': 'var(--color-content-primary)',
'trigger-focus-shadow':
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'icon-size-xs': 'var(--control-height-xs)',
diff --git a/src/uix/eidos/lib/render-css.ts b/src/uix/eidos/lib/render-css.ts
index bb8744143..c65b7c5b9 100644
--- a/src/uix/eidos/lib/render-css.ts
+++ b/src/uix/eidos/lib/render-css.ts
@@ -212,6 +212,25 @@ export function renderStaticCss(options: EidosConfig): string {
)
declarations.push(cssVar('field-segment-active-text', 'var(--color-primary-text)'))
+ // Hover affordance for segments — the faint "you can edit here" tint, shared
+ // by every date / time / color field segment (distinct from the active fill).
+ declarations.push(
+ cssVar(
+ 'field-segment-hover-bg',
+ 'color-mix(in srgb, var(--color-content-primary) 6%, transparent)'
+ )
+ )
+
+ // Control-trigger hover — uniform neutral tint for the affordance that opens a
+ // field overlay (calendar / clock / swatch) or toggles password visibility. NOT
+ // the field's accent: every field's control reads the same on hover.
+ declarations.push(
+ cssVar(
+ 'field-control-trigger-hover-bg',
+ 'color-mix(in srgb, var(--color-content-primary) 8%, transparent)'
+ )
+ )
+
if (primitives.layout) {
appendLayoutDeclarations(declarations, primitives.layout)
}
diff --git a/src/uix/soma/components/announce/global.svelte.ts b/src/uix/soma/components/announce/global.svelte.ts
index d151f90a8..f38f7313d 100644
--- a/src/uix/soma/components/announce/global.svelte.ts
+++ b/src/uix/soma/components/announce/global.svelte.ts
@@ -23,8 +23,8 @@ import type { AnnouncePriority, AnnounceApi } from './types';
* resolve a document.
*/
-const POLITE_ID = 'soma-announce-global-polite';
-const ASSERTIVE_ID = 'soma-announce-global-assertive';
+const POLITE_ID = 'uix-announce-global-polite';
+const ASSERTIVE_ID = 'uix-announce-global-assertive';
const VISUALLY_HIDDEN_STYLE =
'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0';
diff --git a/src/uix/soma/components/menubar/menubar-provider.svelte.ts b/src/uix/soma/components/menubar/menubar-provider.svelte.ts
index 019012b03..09e256046 100644
--- a/src/uix/soma/components/menubar/menubar-provider.svelte.ts
+++ b/src/uix/soma/components/menubar/menubar-provider.svelte.ts
@@ -202,7 +202,7 @@ export class MenubarMenuProvider {
/** Nested DropdownMenu provider — supplies Content/Item/Sub with context. */
readonly menu: DropdownMenuProvider;
- private readonly menuId = state(`soma-menubar-menu-${Math.random().toString(36).slice(2, 8)}`);
+ private readonly menuId = state(`uix-menubar-menu-${Math.random().toString(36).slice(2, 8)}`);
private constructor(opts: MenubarMenuOpts) {
this.opts = opts;
diff --git a/src/uix/soma/datetime/announcer.ts b/src/uix/soma/datetime/announcer.ts
index 24f754c83..1961e36b6 100644
--- a/src/uix/soma/datetime/announcer.ts
+++ b/src/uix/soma/datetime/announcer.ts
@@ -18,7 +18,7 @@ function initAnnouncer(dom: ActiveDom | null, target?: Node | null) {
if (!isHTMLElement(el)) {
el =
- dom.writeNode('soma-sr-announcer', {
+ dom.writeNode('uix-sr-announcer', {
host: doc.body,
position: 'prepend',
attrs: {
@@ -29,11 +29,11 @@ function initAnnouncer(dom: ActiveDom | null, target?: Node | null) {
}
if (isHTMLElement(el)) {
- dom.writeNode('soma-sr-announcer-assertive', {
+ dom.writeNode('uix-sr-announcer-assertive', {
host: el,
attrs: { role: 'log', 'aria-live': 'assertive', 'aria-relevant': 'additions' }
});
- dom.writeNode('soma-sr-announcer-polite', {
+ dom.writeNode('uix-sr-announcer-polite', {
host: el,
attrs: { role: 'log', 'aria-live': 'polite', 'aria-relevant': 'additions' }
});