feat(eidos): shape Fase 2 — concentric nested-radius harmony

`--shape-nest-gap` token + `[data-shape-nest]` rule: a nested element derives
`border-radius: max(0px, var(--shape-outer-radius) - var(--shape-nest-gap))`, so
its corners stay parallel to the parent (concentric). The parent exposes its
radius via `--shape-outer-radius` (inherited); gap defaults to the nest token.

nestGap is optional on ShapePrimitiveSet (a shape system can omit nesting); the
token + rule are gated on its presence. Only SwiftUI (recent, platform) has
concentric corners — none on the web as a token rule.

Verified: check 0 errors; eidos config+generated 55/55. Regenerated base.css.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 7e26608ee0
commit e04c098787

@ -543,6 +543,15 @@ describe('ActiveEidos config', () => {
expect(css).toContain('corner-shape: squircle;'); expect(css).toContain('corner-shape: squircle;');
}); });
it('emits the Fase 2 concentric nesting helper ([data-shape-nest] + --shape-nest-gap)', () => {
const css = createThemeBaseEidos().renderStaticCss();
expect(css).toContain('--shape-nest-gap: var(--space-2);');
expect(css).toContain('[data-shape-nest] {');
expect(css).toContain(
'border-radius: max(0px, calc(var(--shape-outer-radius, var(--radius-lg)) - var(--shape-nest-gap)));'
);
});
it('emits the Phase 3 typography scales + optical tracking', () => { it('emits the Phase 3 typography scales + optical tracking', () => {
const css = createThemeBaseEidos().renderStaticCss() const css = createThemeBaseEidos().renderStaticCss()
expect(css).toContain('--tracking-tight: -0.02em;') expect(css).toContain('--tracking-tight: -0.02em;')

@ -458,6 +458,7 @@
--depth-recessed-shadow: inset 0 1px 2px color-mix(in srgb, var(--color-neutral-contrast) 12%, transparent); --depth-recessed-shadow: inset 0 1px 2px color-mix(in srgb, var(--color-neutral-contrast) 12%, transparent);
--depth-recessed-z: var(--z-index-base); --depth-recessed-z: var(--z-index-base);
--shape-smoothing: 2; --shape-smoothing: 2;
--shape-nest-gap: var(--space-2);
--control-height-2xs: var(--control-height-xxs); --control-height-2xs: var(--control-height-xxs);
--radius-xs: var(--radius-sm); --radius-xs: var(--radius-sm);
--font-sans: var(--font-family-primary); --font-sans: var(--font-family-primary);
@ -4378,6 +4379,10 @@
corner-shape: scoop; corner-shape: scoop;
} }
[data-shape-nest] {
border-radius: max(0px, calc(var(--shape-outer-radius, var(--radius-lg)) - var(--shape-nest-gap)));
}
[data-density='compact'] { [data-density='compact'] {
--density-space-scale: var(--density-compact-space-scale); --density-space-scale: var(--density-compact-space-scale);
--density-control-scale: var(--density-compact-control-scale); --density-control-scale: var(--density-compact-control-scale);

@ -709,6 +709,13 @@ export interface DepthPrimitiveSet {
export interface ShapePrimitiveSet { export interface ShapePrimitiveSet {
/** Superellipse exponent for the `continuous` family, emitted as `--shape-smoothing`. */ /** Superellipse exponent for the `continuous` family, emitted as `--shape-smoothing`. */
readonly smoothing: string; readonly smoothing: string;
/**
* Concentric nesting gap, emitted as `--shape-nest-gap`. A `[data-shape-nest]` child derives
* `border-radius: max(0px, var(--shape-outer-radius) − gap)` so its corners stay parallel to
* the parent's — the harmonic-radii rule (SHAPE_ENGINE_RFC Fase 2). Optional: omit to ship a
* shape system without the nesting helper.
*/
readonly nestGap?: string;
/** `data-shape='{family}'` → `corner-shape` value (round / superellipse(…) / bevel / scoop). */ /** `data-shape='{family}'` → `corner-shape` value (round / superellipse(…) / bevel / scoop). */
readonly families: Record<string, string>; readonly families: Record<string, string>;
} }

@ -932,6 +932,8 @@ function validateShapePrimitives(options: EidosConfig, issues: EidosValidationIs
const shape = options.primitives.shape const shape = options.primitives.shape
if (shape === undefined) return if (shape === undefined) return
validateNonEmptyCssValue('primitives.shape.smoothing', shape.smoothing, issues) validateNonEmptyCssValue('primitives.shape.smoothing', shape.smoothing, issues)
if (shape.nestGap !== undefined)
validateNonEmptyCssValue('primitives.shape.nestGap', shape.nestGap, issues)
if (!isPlainRecord(shape.families)) { if (!isPlainRecord(shape.families)) {
issues.push({ path: 'primitives.shape.families', message: 'shape families must be a plain object' }) issues.push({ path: 'primitives.shape.families', message: 'shape families must be a plain object' })
return return

@ -250,6 +250,7 @@ export const STATIC_DEPTH: DepthPrimitiveSet = {
// `corner-shape` is unsupported. (SHAPE_ENGINE_RFC) // `corner-shape` is unsupported. (SHAPE_ENGINE_RFC)
export const STATIC_SHAPE: ShapePrimitiveSet = { export const STATIC_SHAPE: ShapePrimitiveSet = {
smoothing: '2', smoothing: '2',
nestGap: 'var(--space-2)',
families: { families: {
rounded: 'round', rounded: 'round',
continuous: 'superellipse(var(--shape-smoothing))', continuous: 'superellipse(var(--shape-smoothing))',

@ -824,21 +824,33 @@ function renderDepthBlocks(depth: DepthPrimitiveSet): string[] {
// ── Shape (SHAPE_ENGINE_RFC) ───────────────────────────────────────────────── // ── Shape (SHAPE_ENGINE_RFC) ─────────────────────────────────────────────────
/** Emit the shape token — `--shape-smoothing` (superellipse exponent of the `continuous` family). */ /** Emit the shape tokens — `--shape-smoothing` (superellipse exponent) + `--shape-nest-gap`. */
function appendShapeDeclarations(declarations: string[], shape: ShapePrimitiveSet): void { function appendShapeDeclarations(declarations: string[], shape: ShapePrimitiveSet): void {
declarations.push(cssVar('shape-smoothing', shape.smoothing)) declarations.push(cssVar('shape-smoothing', shape.smoothing))
if (shape.nestGap !== undefined) declarations.push(cssVar('shape-nest-gap', shape.nestGap))
} }
/** /**
* `[data-shape='{family}']` sets `corner-shape` (continuity / perceptual family) — opt-in, so the * `[data-shape='{family}']` sets `corner-shape` (continuity / perceptual family) — opt-in, so the
* magnitude (`border-radius`) stays universal and the corners degrade to the plain arc where * magnitude (`border-radius`) stays universal and the corners degrade to the plain arc where
* `corner-shape` is unsupported. Magnitude is never touched here. (SHAPE_ENGINE_RFC) * `corner-shape` is unsupported. `[data-shape-nest]` adds the concentric harmony: a child's
* radius = the parent's `--shape-outer-radius` minus the nest gap, so nested corners stay
* parallel. Magnitude is never touched on the family rules. (SHAPE_ENGINE_RFC)
*/ */
function renderShapeBlocks(shape: ShapePrimitiveSet): string[] { function renderShapeBlocks(shape: ShapePrimitiveSet): string[] {
const blocks: string[] = [] const blocks: string[] = []
for (const [family, value] of Object.entries(shape.families)) { for (const [family, value] of Object.entries(shape.families)) {
blocks.push(renderBlock(`[data-shape='${family}']`, [`corner-shape: ${value};`])) blocks.push(renderBlock(`[data-shape='${family}']`, [`corner-shape: ${value};`]))
} }
// Concentric nesting (Fase 2): inner radius = outer − gap, clamped ≥ 0. The parent exposes
// its radius via `--shape-outer-radius` (inherited); the gap defaults to the nest token.
if (shape.nestGap !== undefined) {
blocks.push(
renderBlock(`[data-shape-nest]`, [
`border-radius: max(0px, calc(var(--shape-outer-radius, var(--radius-lg)) - var(--shape-nest-gap)));`
])
)
}
return blocks return blocks
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.