uix(color): abre `color` en color-picker (cross-portal resuelto)

La trampa: el accent vivia en DOS scopes — cascada per-rol en el root y una
re-declaracion fija `primary-border` en el contenido portalado. El forward del
root nunca alcanzaba el portal: el prop `color` solo tintaba el ring del
trigger; los rings del portal (area-thumb, slider-thumb, eye-dropper) eran
SIEMPRE primary.

Solucion (espejo de select): el forward apunta a las DOS partes que estampan —
recipe `_palette-border` con `parts: ['trigger', 'content']` (fusionado en la
recipe existente). El trigger consume el token directo en su ring; el content
re-deriva su `--_color-picker-accent` del token EN SU scope (sus descendientes
lo heredan como antes). Root: fuera la decl base + la cascada de 7 roles; el
provider solo estampa el marcador canonico y el color RAW viaja por contexto
(Trigger/Content/ChannelInput lo resuelven cada uno via resolveComponentColor,
con seed `--color-custom` para valores raw). El marker div del ChannelInput
enruta ademas el forward de color-field, como un ColorField standalone.

Tipos: `ColorPickerColor = ComponentColorProp` (era ColorRole).

Verificado en Chrome con el popover abierto: content portalado estampa su
propio data-color; default primary = probe exacto en trigger y content;
risk/teal en el scope del portal = token compartido; custom → color-mix del
seed; el area-thumb hereda el accent. Contract test: mis cambios no añaden
fallos (los 2 de chat-composer son WIP de otra sesion con recipe fuera del
tree; 2 de proof-of-human pre-existentes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-sec-dom
dev 3 months ago
parent 6b37beed69
commit c9db0a8d26

@ -1,4 +1,6 @@
<script lang="ts">
import { composeInlineStyle } from '$uix/eidos/lib/style';
import { resolveComponentColor } from '$uix/eidos/lib/component-color';
import * as ColorPicker from '$soma/components/color-picker';
import { getColorPickerVisualContext } from './context.svelte';
import type {
@ -17,10 +19,20 @@
// flex stays exactly as before).
const visual = getColorPickerVisualContext();
const size = $derived(visual?.getSize() ?? 'md');
const color = $derived(visual?.getColor() ?? 'primary');
// `color` accepts the full system — canonical stamps `data-color`, raw values
// stamp `data-color-custom` + the `--color-custom` seed, so the color-field
// forward derives its palette here exactly like a standalone `<ColorField>`.
const colorAttrs = $derived(resolveComponentColor(visual?.getColor() ?? 'primary'));
const mergedStyle = $derived(composeInlineStyle('display: contents;', colorAttrs.customStyle));
</script>
<div data-color-field data-size={size} data-color={color} style="display: contents;">
<div
data-color-field
data-size={size}
data-color={colorAttrs.dataColor}
data-color-custom={colorAttrs.isCustom ? '' : undefined}
style={mergedStyle}
>
<ColorPicker.ChannelInput {...rest}>
{#snippet children(args: ColorPickerChannelInputSnippetProps)}
{@render bodyContent?.(args)}

@ -1,5 +1,7 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import { composeInlineStyle } from '$uix/eidos/lib/style';
import { resolveComponentColor } from '$uix/eidos/lib/component-color';
import PopoverContent from '../popover/popover-content.svelte';
import PickerShellRoot from '../picker-shell/picker-shell.svelte';
import { getColorPickerVisualContext } from './context.svelte';
@ -11,12 +13,19 @@
sideOffset,
collisionPadding = 12,
children: bodyContent,
style,
...rest
}: ColorPickerContentProps = $props();
const eidos = ActiveEidos.require();
const visual = getColorPickerVisualContext();
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize() ?? 'md', 'md'));
// The content portals OUTSIDE the root subtree, so the root's palette can't
// inherit here — it stamps its own `data-color` (+ custom seed) and the
// recipe forward targets the `content` part directly.
const resolvedColor = $derived(visual?.getColor() ?? 'primary');
const colorAttrs = $derived(resolveComponentColor(resolvedColor));
const mergedStyle = $derived(composeInlineStyle(style, colorAttrs.customStyle));
</script>
<PopoverContent
@ -27,6 +36,9 @@
size={resolvedSize}
data-picker-size={resolvedSize}
data-color-picker-content=""
data-color={colorAttrs.dataColor}
data-color-custom={colorAttrs.isCustom ? '' : undefined}
style={mergedStyle}
>
{#snippet children(snippetProps)}
<PickerShellRoot>

@ -6,6 +6,8 @@
* var to tint the dot.
*/
import { ActiveEidos } from '$uix/eidos';
import { composeInlineStyle } from '$uix/eidos/lib/style';
import { resolveComponentColor } from '$uix/eidos/lib/component-color';
import * as ColorPicker from '$soma/components/color-picker';
import ColorSwatch from '../color-swatch';
import { getColorPickerVisualContext } from './context.svelte';
@ -27,16 +29,21 @@
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize() ?? 'md', 'md'));
const resolvedVariant = $derived(variant ?? visual?.getVariant() ?? 'surface');
const resolvedColor = $derived(color ?? visual?.getColor() ?? 'primary');
// `color` accepts the full system: canonical → `data-color`, raw value →
// `data-color-custom` + the `--color-custom` seed (shared derivation).
const colorAttrs = $derived(resolveComponentColor(resolvedColor));
const mergedStyle = $derived(composeInlineStyle(style, colorAttrs.customStyle));
</script>
<ColorPicker.Trigger
{...rest}
data-size={resolvedSize}
data-variant={resolvedVariant}
data-color={resolvedColor}
data-color={colorAttrs.dataColor}
data-color-custom={colorAttrs.isCustom ? '' : undefined}
data-shape={shape}
data-show-swatch={showSwatch ? '' : undefined}
{style}
style={mergedStyle}
>
{#if showSwatch}
<!-- Composes the shared <ColorSwatch> primitive: checker + colour overlay,

@ -25,7 +25,6 @@
--_color-picker-trigger-swatch-size: var(--color-picker-trigger-swatch-size-md);
--_color-picker-trigger-font-size: var(--font-size-md);
--_color-picker-swatch-size: var(--color-picker-swatch-size-md);
--_color-picker-accent: var(--color-primary-border);
/* Transparency checker — reusable on any descendant element. */
--cp-checker-cell: 6px;
@ -60,13 +59,10 @@
--_color-picker-swatch-size: var(--color-picker-swatch-size-lg);
}
[data-color-picker][data-color='secondary'] { --_color-picker-accent: var(--color-secondary-border); }
[data-color-picker][data-color='neutral'] { --_color-picker-accent: var(--color-neutral-border); }
[data-color-picker][data-color='affirm'] { --_color-picker-accent: var(--color-affirm-border); }
[data-color-picker][data-color='fulfill'] { --_color-picker-accent: var(--color-fulfill-border); }
[data-color-picker][data-color='risk'] { --_color-picker-accent: var(--color-risk-border); }
[data-color-picker][data-color='threat'] { --_color-picker-accent: var(--color-threat-border); }
[data-color-picker][data-color='loss'] { --_color-picker-accent: var(--color-loss-border); }
/* Accent colour comes from the shared palette forward
(`--_color-picker-palette-border`): the trigger + the portaled content each
stamp `data-color` (the portal can't inherit the root's cascade), and any
role / 33 scale / custom value routes through. */
[data-color-picker-label] {
color: var(--color-content-primary);
@ -96,7 +92,9 @@
linear-gradient(-45deg, var(--color-picker-transparency-cell) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--color-picker-transparency-cell) 75%),
linear-gradient(-45deg, transparent 75%, var(--color-picker-transparency-cell) 75%);
--_color-picker-accent: var(--color-primary-border);
/* The accent re-derives from the palette forward ON THIS scope — the
content stamps its own `data-color`, so custom/scales resolve here. */
--_color-picker-accent: var(--_color-picker-palette-border);
--_color-picker-swatch-size: var(--color-picker-swatch-size-md);
/* +2rem over the base so the CANONICAL ColorField value row (6ch hex + 3ch
@ -166,7 +164,7 @@
}
[data-color-picker-trigger]:focus-visible {
border-color: var(--_color-picker-accent);
border-color: var(--_color-picker-palette-border);
/* Focus ring as a flush outline (no offset → no gap) — survives forced-colors. */
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: 0;

@ -9,6 +9,7 @@
import '$uix/eidos/components/color-field/color-field.css';
import './color-picker.css';
import { ActiveEidos } from '$uix/eidos';
import { resolveComponentColor } from '$uix/eidos/lib/component-color';
import * as ColorPicker from '$soma/components/color-picker';
import { setColorPickerVisualContext } from './context.svelte';
import { setPickerShellVisual } from '$uix/eidos/components/picker-shell/visual-context';
@ -29,6 +30,10 @@
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
// The RAW `color` travels through context so Trigger + Content (the forward
// targets, Content portals outside this subtree) each resolve it; the root
// only stamps the canonical marker.
const colorAttrs = $derived(resolveComponentColor(color));
setColorPickerVisualContext({
getSize: () => resolvedSize,
@ -48,7 +53,8 @@
bind:savedColors
data-size={resolvedSize}
data-variant={variant}
data-color={color}
data-color={colorAttrs.dataColor}
data-color-custom={colorAttrs.isCustom ? '' : undefined}
>
{@render children?.()}
</ColorPicker.Provider>

@ -29,18 +29,21 @@ import type {
ValueTextProps as SomaValueTextProps
} from '$soma/components/color-picker'
import type { PopoverContentProps } from '../popover/types'
import type { ColorRole, ControlVariant, ResponsiveProp, ShapeFamily, Size } from '$uix/eidos/lib/types'
import type { ComponentColorProp, ControlVariant, ResponsiveProp, ShapeFamily, Size } from '$uix/eidos/lib/types'
export type ColorPickerSize = Extract<Size, 'sm' | 'md' | 'lg'>
export type ColorPickerVariant = ControlVariant
export type ColorPickerColor = ColorRole
export type ColorPickerColor = ComponentColorProp
export type ColorPickerProps = ProviderProps & {
/** Visual size for the trigger, popover and child controls. @default 'md' */
size?: ResponsiveProp<ColorPickerSize>
/** Visual treatment of the trigger. @default 'surface' */
variant?: ColorPickerVariant
/** Intent palette for focus and accent affordances. @default 'primary' */
/**
* Accent for focus affordances (trigger border + content rings) — any
* role / intent / 33 donor scales / raw CSS color. @default 'primary'
*/
color?: ColorPickerColor
}

@ -4316,6 +4316,14 @@
--_image-palette-track: var(--palette-track, var(--color-primary-track));
}
[data-color-picker-trigger], [data-color-picker-content] {
--_color-picker-palette-border: var(--color-primary-border);
}
[data-color-picker-trigger][data-color], [data-color-picker-trigger][data-color-custom], [data-color-picker-content][data-color], [data-color-picker-content][data-color-custom] {
--_color-picker-palette-border: var(--palette-border, var(--color-primary-border));
}
[data-button] {
--button-palette-track: var(--button-primary-track);
--button-palette-element: var(--button-primary-element);

@ -1482,7 +1482,17 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// Misc
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': 'var(--opacity-disabled)'
'disabled-opacity': 'var(--opacity-disabled)',
// Accent palette forward (THM-2) — `color` tints the trigger focus
// border and the portaled content's focus rings. The content portals
// OUTSIDE the root, so both parts stamp `data-color` themselves and the
// forward targets them; roles + 33 scales + custom route through the
// shared layer. Host = primary (the previous hard-coded accent).
'_palette-border': {
parts: ['trigger', 'content'],
declarations: [{ value: 'var(--color-primary-border)', scope: 'host' }]
}
},
button: {
// Size tokens

Loading…
Cancel
Save

Powered by TurnKey Linux.