Harden Eidos config and wrappers

active-uix
dev 5 months ago
parent eba7b4522d
commit c939de90c8

@ -2,6 +2,82 @@
Fecha de corte: 2026-05-16. Rama: `active-uix`.
Actualizacion Eidos components audit 2026-05-17:
- Se acometieron los hallazgos autorizados sobre `src/uix/eidos/components`
sin tocar demos ni rutas `web/routes/uix`:
- `Tabs` instala su contexto visual de forma sincronica durante init, no en
`$effect`; los hijos `Tabs.Content` ya no pueden inicializar sin contexto.
- `Tabs.Indicator` deja de observarse a si mismo con `ResizeObserver`, para
evitar loops de medida provocados por sus propias escrituras inline.
- `Avatar.Image` ya no arranca oculto cuando hay `src`; la visibilidad queda
derivada de `src` y de la ultima URL fallida.
- `Accordion.Trigger` ya no contiene `<style>` scoped; la regla de layout del
label vive en `accordion.css`.
- `Toast.Close` emite `data-icon-only` cuando renderiza el icono por defecto,
igual que `Dialog.Close`, `Drawer.Close` y `Popover.Close`.
- `Soma` expone `data-floating-wrapper` como marcador publico del wrapper
flotante; `Popover` y `Tooltip` dejan de depender de
`data-soma-floating-wrapper` en CSS.
- Guardias anadidas/reforzadas:
- `component-api-contract.test.ts` falla si un wrapper Eidos reintroduce
`onMount`, `<style>` scoped o `data-soma-*`.
- `component-visual-attrs.test.ts` cubre ahora `toast-close.svelte` y su
`data-icon-only`.
- Tests de Tooltip/LinkPreview esperan tambien el nuevo
`data-floating-wrapper`.
- Validado:
- `npx vitest run src/uix/eidos/component-api-contract.test.ts src/uix/eidos/component-visual-attrs.test.ts src/uix/eidos/recipe-css-contract.test.ts`
-> 3 archivos, 7 tests OK.
- `npx vitest run src/uix/soma/components/tooltip/tooltip-provider.svelte.test.ts src/uix/soma/components/link-preview/link-preview-provider.svelte.test.ts`
-> 2 archivos, 5 tests OK.
- `npx vitest run src/uix/soma/components/popover/popover-provider.svelte.test.ts`
-> 1 archivo, 2 tests OK.
- `npx vitest run src/uix/eidos` -> 7 archivos, 79 tests OK.
- `npm run check` -> 0 errores, 0 warnings.
- Pendiente deliberado:
- Revisar tokens/recipes antes de cambiar `font-size: 0.9rem/0.85rem` en
`dialog.css` y `popover.css`; no se toco ahora para no mezclarlo con el
problema visual de tipografia observado en `/routes/uix`.
Actualizacion Eidos config audit 2026-05-17:
- Resuelta la parte no-componente de la auditoria consolidada de
`src/uix/eidos`:
- `validateTypography(...)` ya no asume que `sizes`, `families`,
`weights` o `styles` existen y son objetos; ahora produce issues de
validacion.
- `validateSizePrimitive(...)` y `validateTextMetric(...)` degradan campos
ausentes o mal tipados a issues, sin `TypeError` por `.trim()`.
- `validateColorScale(...)` valida escala como objeto plano y rechaza pasos
`null` / `undefined`.
- `primitives.shadow` queda incluido en la validacion de shape.
- El contrato CSS de `size-*-font-line-height` y
`size-*-font-letter-spacing` apunta a
`primitives.typography.sizes.{fontSize}.lineHeight/letterSpacing`, no a
`primitives.size.*.fontSize`.
- `renderThemeCss(config, themeId)` ya no contiene el caso magico
`themeId === 'base-light'`; el renderer puro deriva el selector solo del
`themeId`. `ActiveEidos` sigue siendo quien decide usar `:root` cuando
aplica runtime CSS.
- Tests nuevos en `src/uix/eidos/active-eidos-config.test.ts` cubren
tipografia incompleta, size/text metric incompletos, escalas con `null`,
shape de `shadow`, paths de contrato tipografico y selector raw por
`themeId`.
- Validado:
- `npx vitest run src/uix/eidos` -> 7 archivos, 78 tests OK.
- `npm run check` -> 0 errores, 0 warnings.
- Nota historica: en ese momento quedo pendiente porque se pidio no tocar
componentes aun; queda resuelto en la actualizacion de componentes del
2026-05-17:
- `avatar-image.svelte`: estado `visible` derivable desde `src`.
- `tabs-indicator.svelte`: `ResizeObserver` observa el propio indicador.
- `accordion-trigger.svelte`: unico scoped style dentro de componente.
- `toast-close.svelte`: falta `data-icon-only` consistente con Close de
Dialog/Drawer/Popover.
- `popover.css` y `tooltip.css`: selectores acoplados a
`data-soma-floating-wrapper`.
Actualizacion Eidos ecosystem 2026-05-16:
- Correccion de alcance: no seguir tocando demos/rutas ni

@ -14,6 +14,7 @@ import {
extendEidosConfig,
parseEidosConfigFromJson,
parseEidosConfigDocument,
renderThemeCss,
STATIC_CONTROL_HEIGHT,
STATIC_DENSITY,
STATIC_LAYOUT,
@ -342,6 +343,7 @@ describe('ActiveEidos config', () => {
const malformed = structuredClone(createThemeBaseEidos().toDocument())
;(malformed.options.primitives as unknown as Record<string, unknown>).typography = 'bad'
;(malformed.options.primitives as unknown as Record<string, unknown>).shadow = 'bad'
;(malformed.options.themes!['base-light'] as unknown as Record<string, unknown>).color = 'bad'
;(malformed.options.recipes as unknown as Record<string, unknown>).tooltip = 'bad'
@ -353,6 +355,7 @@ describe('ActiveEidos config', () => {
expect((error as EidosConfigValidationError).issues.map((issue) => issue.path)).toEqual(
expect.arrayContaining([
'primitives.typography',
'primitives.shadow',
'themes.base-light.color',
'recipes.tooltip'
])
@ -360,6 +363,50 @@ describe('ActiveEidos config', () => {
}
})
it('reports malformed typography primitive groups without throwing', () => {
const cfg = structuredClone(THEME_BASE_OPTIONS)
const typography = cfg.primitives.typography as unknown as Record<string, unknown>
delete typography.sizes
typography.families = 'bad'
typography.weights = null
typography.styles = ['bad']
const report = createEidos(cfg).validate()
expect(report.ok).toBe(false)
expect(report.issues.map((issue) => issue.path)).toEqual(
expect.arrayContaining([
'primitives.typography.sizes',
'primitives.typography.families',
'primitives.typography.weights',
'primitives.typography.styles'
])
)
})
it('reports incomplete primitive values without throwing', () => {
const cfg = structuredClone(THEME_BASE_OPTIONS)
delete (cfg.primitives.size!.md as unknown as Record<string, unknown>).paddingInline
delete (cfg.primitives.typography!.sizes.md as unknown as Record<string, unknown>).lineHeight
;(cfg.themes!['base-light'].color!.scales!.purple as unknown as Record<
string,
unknown
>)['9'] = null
const report = createEidos(cfg).validate()
expect(report.ok).toBe(false)
expect(report.issues.map((issue) => issue.path)).toEqual(
expect.arrayContaining([
'primitives.size.md.paddingInline',
'primitives.typography.sizes.md.lineHeight',
'themes.base-light.color.scales.purple.9'
])
)
})
it('ships typography families separately from typography styles', () => {
const typography = createThemeBaseEidos().snapshot().primitives.typography!
@ -769,6 +816,20 @@ describe('ActiveEidos config', () => {
category: 'size',
path: 'primitives.size.md.controlHeight'
})
expect(contract.static).toContainEqual({
name: 'size-md-font-line-height',
cssVar: '--size-md-font-line-height',
scope: 'static',
category: 'size',
path: `primitives.typography.sizes.${STATIC_SIZE.md.fontSize}.lineHeight`
})
expect(contract.static).toContainEqual({
name: 'size-md-font-letter-spacing',
cssVar: '--size-md-font-letter-spacing',
scope: 'static',
category: 'size',
path: `primitives.typography.sizes.${STATIC_SIZE.md.fontSize}.letterSpacing`
})
expect(contract.static).toContainEqual({
name: 'container-width-md',
cssVar: '--container-width-md',
@ -899,6 +960,13 @@ describe('ActiveEidos config', () => {
expect(css).toContain('--shadow-raised: var(--shadow-3);')
})
it('renders raw theme CSS with a selector derived only from the theme id', () => {
const css = renderThemeCss(THEME_BASE_OPTIONS, 'base-light')
expect(css).toContain("[data-theme='base-light'] {")
expect(css).not.toContain(':root')
})
it('renders explicit alpha color scales when a theme provides them', () => {
const eidos = createThemeBaseEidos({
themes: {

@ -94,4 +94,25 @@ describe('Eidos component API contract', () => {
expect(violations).toEqual([])
})
it('keeps component wrappers free of legacy lifecycle and scoped styles', () => {
const violations: string[] = []
for (const component of readComponentDirs()) {
const dir = join(COMPONENTS_DIR, component)
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith('.svelte')) continue
const path = join(dir, entry.name)
const source = readFileSync(path, 'utf8')
if (/\bonMount\b/.test(source)) violations.push(`${component}/${entry.name}: onMount`)
if (/<style(?:\s|>)/.test(source)) violations.push(`${component}/${entry.name}: <style>`)
if (/\bdata-soma-/.test(source)) {
violations.push(`${component}/${entry.name}: data-soma-*`)
}
}
}
expect(violations).toEqual([])
})
})

@ -23,6 +23,7 @@ const VISUAL_ATTRS = [
['tabs/tabs.svelte', ['data-size', 'data-variant', 'data-fitted', 'data-collapsed', 'data-motion']],
['tabs/tabs-content.svelte', ['data-motion']],
['tabs/tabs-indicator.svelte', ['data-ready']],
['toast/toast-close.svelte', ['data-icon-only']],
['toast/toast-viewport.svelte', ['data-position']],
['toggle/toggle.svelte', ['data-size', 'data-variant', 'data-block', 'data-icon-only']],
['tooltip/tooltip-content.svelte', ['data-size', 'data-variant']],

@ -28,17 +28,10 @@
</script>
<Accordion.Trigger {...rest} data-size={resolvedSize} data-indicator-placement={indicatorPlacement}>
<span data-accordion-label class="accordion-trigger-label">{@render children?.()}</span>
<span data-accordion-label>{@render children?.()}</span>
{#if !hideIndicator}
<span data-accordion-indicator aria-hidden="true">
<SvgChevron />
</span>
{/if}
</Accordion.Trigger>
<style>
.accordion-trigger-label {
flex: 1 1 auto;
min-inline-size: 0;
}
</style>

@ -162,6 +162,11 @@
cursor: not-allowed;
}
[data-accordion-label] {
flex: 1 1 auto;
min-inline-size: 0;
}
/* ── Indicator chevron ───────────────────────────────────────────────
* `<SvgChevron>` lives inside the trigger's wrapper, marked
* `data-accordion-indicator`. The chevron points DOWN by default; on

@ -2,18 +2,15 @@
import type { AvatarImageProps } from './types';
let { src, alt = '', ...restProps }: AvatarImageProps = $props();
let visible = $state(false);
$effect(() => {
visible = Boolean(src);
});
let failedSrc = $state<string | undefined>(undefined);
const visible = $derived(Boolean(src) && src !== failedSrc);
function showImage() {
visible = true;
if (failedSrc === src) failedSrc = undefined;
}
function hideImage() {
visible = false;
failedSrc = src ?? undefined;
}
</script>

@ -48,9 +48,9 @@
* z-index unless the content provides one via `getComputedStyle().zIndex`,
* which only works for positioned elements. When modal=true, the overlay
* (z=60) would otherwise paint over the wrapper, hiding the panel and
* making it unclickable. Lift the wrapper above the overlay directly via
* :has() so we don't need to make `[data-popover-content]` positioned. */
[data-soma-floating-wrapper]:has(> [data-popover-content]) {
* making it unclickable. Soma exposes a public floating wrapper marker for
* cross-layer styling, so Eidos does not depend on Soma internals here. */
[data-floating-wrapper]:has(> [data-popover-content]) {
z-index: var(--popover-content-z);
}

@ -71,6 +71,7 @@
const ro = new ResizeObserver(() => measure());
ro.observe(list);
for (const child of Array.from(list.children)) {
if (child === el) continue;
if (child instanceof HTMLElement) ro.observe(child);
}

@ -51,6 +51,27 @@
let isCollapsed = $state(false);
setTabsEidosContext({
get size() {
return resolvedSize;
},
get variant() {
return variant;
},
get fitted() {
return fitted;
},
get collapseAt() {
return collapseAt;
},
get scrollable() {
return scrollable;
},
get motionPreset() {
return motionPreset;
}
});
$effect(() => {
const el = ref;
const px = collapsePx;
@ -66,17 +87,6 @@
ro.observe(el);
return () => ro.disconnect();
});
$effect(() => {
setTabsEidosContext({
size: resolvedSize,
variant,
fitted,
collapseAt,
scrollable,
motionPreset
});
});
</script>
<SomaTabs.Provider

@ -5,7 +5,7 @@
let { children, ...rest }: ToastCloseProps = $props();
</script>
<Toast.Close {...rest}>
<Toast.Close {...rest} data-icon-only={!children ? '' : undefined}>
{#if children}
{@render children?.()}
{:else}

@ -1,6 +1,6 @@
/* ── Content (variant-shared base) ──────────────────────────────────────── */
[data-soma-floating-wrapper]:has(> [data-tooltip-content]) {
[data-floating-wrapper]:has(> [data-tooltip-content]) {
z-index: var(--tooltip-content-z);
}

@ -34,10 +34,7 @@ import {
type EidosConfig,
type EidosConfigSnapshot,
type RecipeTokenMap,
type SizePrimitive,
type TextMetric,
type ThemeDefinition,
type TypographyPrimitiveSet
type ThemeDefinition
} from './config-types'
import { createEidosCssContract } from './contract'
import {
@ -190,7 +187,8 @@ function validateEidosConfigShape(
'motion',
'icon',
'opacity',
'zIndex'
'zIndex',
'shadow'
] as const
for (const key of primitiveKeys) {
@ -490,13 +488,23 @@ function validateColorScale(
scale: ColorScale,
issues: EidosValidationIssue[]
): void {
if (!isPlainRecord(scale)) {
issues.push({
path,
message: 'color scale must be a plain object'
})
return
}
for (const step of COLOR_SCALE_STEPS) {
if (!hasOwn(scale, step) || scale[step] === '') {
if (!hasOwn(scale, step)) {
issues.push({
path: `${path}.${step}`,
message: 'color scales must define every step from 1 to 12'
})
continue
}
validateNonEmptyCssValue(`${path}.${step}`, scale[step], issues)
}
}
@ -591,9 +599,30 @@ function validateColorRoleSlots(
function validateTypography(options: EidosConfig, issues: EidosValidationIssue[]): void {
const typography = options.primitives.typography
if (!typography) return
const sizes = readRequiredPlainRecord(
'primitives.typography.sizes',
typography.sizes,
issues
)
const families = readRequiredPlainRecord(
'primitives.typography.families',
typography.families,
issues
)
const weights = readRequiredPlainRecord(
'primitives.typography.weights',
typography.weights,
issues
)
const styles = readRequiredPlainRecord(
'primitives.typography.styles',
typography.styles,
issues
)
if (sizes) {
for (const size of TEXT_SIZES) {
const metric = typography.sizes[size]
const metric = sizes[size]
if (!metric) {
issues.push({
path: `primitives.typography.sizes.${size}`,
@ -604,8 +633,13 @@ function validateTypography(options: EidosConfig, issues: EidosValidationIssue[]
validateTextMetric(`primitives.typography.sizes.${size}`, metric, issues)
}
for (const name of Object.keys(typography.sizes)) {
validateCssTokenSuffix(`primitives.typography.sizes.${name}`, name, 'typography size', issues)
for (const name of Object.keys(sizes)) {
validateCssTokenSuffix(
`primitives.typography.sizes.${name}`,
name,
'typography size',
issues
)
if (!TEXT_SIZE_SET.has(name)) {
issues.push({
path: `primitives.typography.sizes.${name}`,
@ -613,57 +647,97 @@ function validateTypography(options: EidosConfig, issues: EidosValidationIssue[]
})
}
}
}
for (const [name, family] of Object.entries(typography.families)) {
if (families) {
for (const [name, family] of Object.entries(families)) {
validateCssTokenSuffix(
`primitives.typography.families.${name}`,
name,
'typography family',
issues
)
if (family.family.trim() === '') {
if (!isPlainRecord(family)) {
issues.push({
path: `primitives.typography.families.${name}.family`,
message: 'font family name cannot be empty'
path: `primitives.typography.families.${name}`,
message: 'typography family must be a plain object'
})
continue
}
validateNonEmptyCssValue(
`primitives.typography.families.${name}.family`,
family.family,
issues
)
}
}
for (const name of Object.keys(typography.weights)) {
if (weights) {
for (const name of Object.keys(weights)) {
validateCssTokenSuffix(
`primitives.typography.weights.${name}`,
name,
'typography weight',
issues
)
validateNonEmptyCssValue(`primitives.typography.weights.${name}`, weights[name], issues)
}
}
for (const [name, style] of Object.entries(typography.styles)) {
if (styles) {
for (const [name, style] of Object.entries(styles)) {
const path = `primitives.typography.styles.${name}`
validateCssTokenSuffix(path, name, 'typography style', issues)
if (style.family !== undefined && !hasOwn(typography.families, style.family)) {
if (!isPlainRecord(style)) {
issues.push({
path,
message: 'typography style must be a plain object'
})
continue
}
const familyRef = style.family
if (
familyRef !== undefined &&
(typeof familyRef !== 'string' || !families || !hasOwn(families, familyRef))
) {
issues.push({
path: `${path}.family`,
message: `typography style points to missing family "${style.family}"`
message: `typography style points to missing family "${String(familyRef)}"`
})
}
const weightRef = style.weight
if (
typeof style.weight === 'string' &&
style.weight.trim() !== '' &&
!hasOwn(typography.weights, style.weight)
typeof weightRef === 'string' &&
weightRef.trim() !== '' &&
(!weights || !hasOwn(weights, weightRef))
) {
issues.push({
path: `${path}.weight`,
message: `typography style points to missing weight "${style.weight}"`
message: `typography style points to missing weight "${weightRef}"`
})
}
validateTypographySizeValue(`${path}.size`, style.size, typography, issues)
validateTypographySizeValue(`${path}.size`, style.size, sizes, issues)
}
}
}
function readRequiredPlainRecord(
path: string,
value: unknown,
issues: EidosValidationIssue[]
): Record<string, unknown> | undefined {
if (isPlainRecord(value)) return value
issues.push({
path,
message: 'Eidos config entries must be plain objects'
})
return undefined
}
function validateSizePrimitives(options: EidosConfig, issues: EidosValidationIssue[]): void {
const sizes = options.primitives.size
if (!sizes) return
@ -694,36 +768,64 @@ function validateSizePrimitives(options: EidosConfig, issues: EidosValidationIss
function validateSizePrimitive(
path: string,
size: SizePrimitive,
size: unknown,
options: EidosConfig,
issues: EidosValidationIssue[]
): void {
if (!hasOwn(options.primitives.controlHeight ?? {}, size.controlHeight)) {
if (!isPlainRecord(size)) {
issues.push({
path,
message: 'size primitive must be a plain object'
})
return
}
const controlHeight = size.controlHeight
if (
typeof controlHeight !== 'string' ||
controlHeight.trim() === '' ||
!hasOwn(options.primitives.controlHeight ?? {}, controlHeight)
) {
issues.push({
path: `${path}.controlHeight`,
message: `size primitive points to missing control height "${size.controlHeight}"`
message: `size primitive points to missing control height "${String(controlHeight)}"`
})
}
const typography = options.primitives.typography
if (!TEXT_SIZE_SET.has(size.fontSize) || !hasOwn(typography?.sizes ?? {}, size.fontSize)) {
const fontSize = size.fontSize
if (
typeof fontSize !== 'string' ||
!TEXT_SIZE_SET.has(fontSize) ||
!hasOwn(typography?.sizes ?? {}, fontSize)
) {
issues.push({
path: `${path}.fontSize`,
message: `size primitive points to missing font size "${size.fontSize}"`
message: `size primitive points to missing font size "${String(fontSize)}"`
})
}
if (!hasOwn(options.primitives.icon?.size ?? {}, size.iconSize)) {
const iconSize = size.iconSize
if (
typeof iconSize !== 'string' ||
iconSize.trim() === '' ||
!hasOwn(options.primitives.icon?.size ?? {}, iconSize)
) {
issues.push({
path: `${path}.iconSize`,
message: `size primitive points to missing icon size "${size.iconSize}"`
message: `size primitive points to missing icon size "${String(iconSize)}"`
})
}
if (!hasOwn(options.primitives.radius ?? {}, size.radius)) {
const radius = size.radius
if (
typeof radius !== 'string' ||
radius.trim() === '' ||
!hasOwn(options.primitives.radius ?? {}, radius)
) {
issues.push({
path: `${path}.radius`,
message: `size primitive points to missing radius "${size.radius}"`
message: `size primitive points to missing radius "${String(radius)}"`
})
}
@ -981,7 +1083,7 @@ function validateRecordPrimitive(
})
continue
}
validateNonEmptyCssValue(`${path}.${key}`, String(value), issues)
validateNonEmptyCssValue(`${path}.${key}`, value, issues)
}
}
@ -995,20 +1097,20 @@ function validateOpenScalePrimitive(
for (const [key, value] of Object.entries(record)) {
validateCssTokenSuffix(`${path}.${key}`, key, label, issues)
validateNonEmptyCssValue(`${path}.${key}`, String(value), issues)
validateNonEmptyCssValue(`${path}.${key}`, value, issues)
}
}
function validateTypographySizeValue(
path: string,
value: unknown,
typography: TypographyPrimitiveSet,
sizes: Record<string, unknown> | undefined,
issues: EidosValidationIssue[]
): void {
if (value === undefined) return
if (typeof value === 'string') {
if (!TEXT_SIZE_SET.has(value) || !hasOwn(typography.sizes, value)) {
if (!TEXT_SIZE_SET.has(value) || !sizes || !hasOwn(sizes, value)) {
issues.push({
path,
message: `typography style points to missing text size "${value}"`
@ -1017,7 +1119,7 @@ function validateTypographySizeValue(
return
}
if (isTextMetric(value)) {
if (isPlainRecord(value) && isTextMetricLike(value)) {
validateTextMetric(path, value, issues)
return
}
@ -1030,7 +1132,7 @@ function validateTypographySizeValue(
message: `unknown responsive breakpoint "${key}"`
})
}
validateTypographySizeValue(`${path}.${key}`, item, typography, issues)
validateTypographySizeValue(`${path}.${key}`, item, sizes, issues)
}
return
}
@ -1043,10 +1145,19 @@ function validateTypographySizeValue(
function validateNonEmptyCssValue(
path: string,
value: string,
value: unknown,
issues: EidosValidationIssue[]
): void {
if (value.trim() === '') {
if (typeof value !== 'string' && typeof value !== 'number') {
issues.push({
path,
message: 'CSS value must be a string or number'
})
return
}
const text = String(value)
if (text.trim() === '') {
issues.push({
path,
message: 'CSS value cannot be empty'
@ -1054,7 +1165,7 @@ function validateNonEmptyCssValue(
return
}
if (!/[;{}]/.test(value)) return
if (!/[;{}]/.test(text)) return
issues.push({
path,
message: 'CSS value cannot contain declaration or block delimiters'
@ -1063,36 +1174,24 @@ function validateNonEmptyCssValue(
function validateTextMetric(
path: string,
metric: TextMetric,
metric: unknown,
issues: EidosValidationIssue[]
): void {
if (metric.size.trim() === '') {
if (!isPlainRecord(metric)) {
issues.push({
path: `${path}.size`,
message: 'text metric size cannot be empty'
})
}
if (metric.lineHeight.trim() === '') {
issues.push({
path: `${path}.lineHeight`,
message: 'text metric lineHeight cannot be empty'
})
}
if (metric.letterSpacing.trim() === '') {
issues.push({
path: `${path}.letterSpacing`,
message: 'text metric letterSpacing cannot be empty'
path,
message: 'text metric must be a plain object'
})
return
}
validateNonEmptyCssValue(`${path}.size`, metric.size, issues)
validateNonEmptyCssValue(`${path}.lineHeight`, metric.lineHeight, issues)
validateNonEmptyCssValue(`${path}.letterSpacing`, metric.letterSpacing, issues)
}
function isTextMetric(value: unknown): value is TextMetric {
return (
isPlainRecord(value) &&
typeof value.size === 'string' &&
typeof value.lineHeight === 'string' &&
typeof value.letterSpacing === 'string'
)
function isTextMetricLike(value: Record<string, unknown>): boolean {
return hasOwn(value, 'size') || hasOwn(value, 'lineHeight') || hasOwn(value, 'letterSpacing')
}
function validateCssTokenKey(

@ -462,6 +462,7 @@ function appendTypographyContractTokens(
function appendSizeContractTokens(tokens: EidosCssContractToken[], sizes: SizePrimitiveSet): void {
for (const name of Object.keys(sizes)) {
const size = sizes[name as keyof SizePrimitiveSet]
pushToken(
tokens,
'static',
@ -481,14 +482,14 @@ function appendSizeContractTokens(tokens: EidosCssContractToken[], sizes: SizePr
'static',
'size',
`size-${name}-font-line-height`,
`primitives.size.${name}.fontSize`
`primitives.typography.sizes.${size.fontSize}.lineHeight`
)
pushToken(
tokens,
'static',
'size',
`size-${name}-font-letter-spacing`,
`primitives.size.${name}.fontSize`
`primitives.typography.sizes.${size.fontSize}.letterSpacing`
)
pushToken(
tokens,

@ -603,10 +603,6 @@ function mergeThemeColor(
}
function getThemeSelector(themeId: string): string {
if (themeId === 'base-light') {
return `:root,\n[data-theme='${themeId}']`
}
return `[data-theme='${themeId}']`
}

@ -204,6 +204,7 @@ describe('LinkPreviewProvider', () => {
expect(['top', 'right', 'bottom', 'left']).toContain(result.content.props['data-side']);
expect(result.content.wrapperProps).toMatchObject({
id: 'link-preview-content-wrapper',
'data-floating-wrapper': '',
'data-soma-floating-wrapper': ''
});
expect(result.content.wrapperProps.style).toMatchObject({

@ -249,6 +249,7 @@ describe('TooltipProvider', () => {
});
expect(result.content.wrapperProps).toMatchObject({
id: 'tooltip-content-wrapper',
'data-floating-wrapper': '',
'data-soma-floating-wrapper': '',
dir: 'ltr'
});

@ -184,6 +184,7 @@ export class FloatingContent {
() =>
({
id: this.opts.wrapperId.current,
'data-floating-wrapper': '',
'data-soma-floating-wrapper': '',
style: {
...this.floating.floatingStyles.current,

Loading…
Cancel
Save

Powered by TurnKey Linux.