fix(eidos/knob): render bugs + donut face, value in centre, per-size font, needle toggle

The relocated Knob didn't render (only the number showed). Root causes, all
found by inspecting the loaded stylesheet (not guessing):

- A stray `*/` inside the header comment (`--scale-*/--primitive-*`) closed the
  comment early and corrupted the FIRST rule, so the base `[data-knob]` rule
  (which sets `--_knob-diameter`) never loaded → the control collapsed to height 0.
- Phantom tokens inherited from the Fable draft that don't exist in the theme:
  `--color-neutral-content` (→ `--color-neutral-text`), `--state-hover`
  (→ a `filter: brightness` hover); the pointer was transparent because its
  fallback token didn't exist.

Design (per the user's reference image + directives):
- Value readout centred in the dial's hole (grid place-items).
- Donut arc: replaced an improvised hard-edged radial mask (aliased inner edge)
  with an opaque face plate over the conic pie — a border-radius circle is
  GPU-antialiased, so the inner ring is clean. Colours use theme-adaptive
  tokens (surface-raised face, neutral-track ring, neutral-text value,
  neutral-text-strong pointer).
- Value + label fonts scale with the knob via the same size coordinate as the
  diameter (`--size-{k}-*` bundle), not an invented "one step below" rule.

Demo: value composed inside the Control (centre), plus a `needle` control that
conditionally composes `<Knob.Indicator>` (the compositional way to show/hide it).

Verified structurally (sizes, tokens, toggle) via DOM inspection; the screenshot
tool timed out all session, so the final pixel check is the user's. Recipe
contract green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 9a993a2a87
commit 627bf00da0

@ -1,7 +1,7 @@
/* src/uix/eidos/components/knob/knob.css
*
* Recipe for Knob. Token discipline per completion-checklist C:
* - no raw colors (R-2.1), no --scale-*/--primitive-* direct (R-4.6)
* - no raw colors (R-2.1), no direct --scale- or --primitive- tokens (R-4.6)
* - elevation via var(--shadow-*) (R-4.1)
* - opacity via var(--opacity-*) (R-4.2)
* - hover via the state layer (R-4.3)
@ -13,14 +13,14 @@
*/
[data-knob] {
/* Private size coordinate: `data-size` selects the global --size-{k}-*
* bundle. The public --knob-* aliases below still override per-instance;
* the private var is only the size-driven default. Value readout sits one
* step below the knob's own size. */
/* Private size coordinate: data-size selects the global per-size bundle, so
* diameter AND fonts scale together from the same size k (--size-{k}-*). The
* public --knob-* aliases below still override per-instance; the private var
* is only the size-driven default. */
--_knob-gap: var(--size-md-gap);
--_knob-diameter: calc(var(--size-md-control-height) * 2);
--_knob-label-font: var(--size-md-font-size);
--_knob-value-font: var(--size-sm-font-size);
--_knob-value-font: var(--size-md-font-size);
display: inline-flex;
flex-direction: column;
@ -32,54 +32,73 @@
--_knob-gap: var(--size-sm-gap);
--_knob-diameter: calc(var(--size-sm-control-height) * 2);
--_knob-label-font: var(--size-sm-font-size);
--_knob-value-font: var(--size-xs-font-size);
--_knob-value-font: var(--size-sm-font-size);
}
[data-knob][data-size='lg'] {
--_knob-gap: var(--size-lg-gap);
--_knob-diameter: calc(var(--size-lg-control-height) * 2);
--_knob-label-font: var(--size-lg-font-size);
--_knob-value-font: var(--size-md-font-size);
--_knob-value-font: var(--size-lg-font-size);
}
[data-knob][data-size='xl'] {
--_knob-gap: var(--size-xl-gap);
--_knob-diameter: calc(var(--size-xl-control-height) * 2);
--_knob-label-font: var(--size-xl-font-size);
--_knob-value-font: var(--size-lg-font-size);
--_knob-value-font: var(--size-xl-font-size);
}
[data-knob-control] {
position: relative;
/* Center the value readout in the dial's hole (known-knob convention). */
display: grid;
place-items: center;
inline-size: var(--knob-diameter, var(--_knob-diameter));
block-size: var(--knob-diameter, var(--_knob-diameter));
border-radius: var(--radius-full, 9999px); /* literal: fallback for circle */
background:
/* value arc: conic gradient driven by the provider-published progress */
conic-gradient(
from calc(var(--knob-start-angle, -135deg)),
var(--color-primary-solid) calc(var(--knob-progress, 0) * var(--knob-sweep, 270deg)),
var(--knob-track-bg, var(--color-neutral-element)) 0
);
/* Value arc (conic pie), driven by the provider-published progress. The face
* plate below carves it into a donut ring. */
background: conic-gradient(
from calc(var(--knob-start-angle, -135deg)),
var(--color-primary-solid) calc(var(--knob-progress, 0) * var(--knob-sweep, 270deg)),
var(--knob-track-bg, var(--color-neutral-track)) 0
);
box-shadow: var(--shadow-2);
cursor: grab;
touch-action: none;
user-select: none;
}
/* Face plate above the arc */
/* Face plate: an OPAQUE raised surface covering the centre so the arc reads as
* a donut ring, not a filled pie. A border-radius circle is GPU-antialiased, so
* the inner edge is clean (unlike a hard-edged radial mask). Layer 0: below the
* pointer and the centred value. */
[data-knob-control]::before {
content: '';
position: absolute;
inset: var(--knob-arc-width, var(--space-2));
inset: var(--knob-arc-width, var(--space-3));
border-radius: inherit;
background: var(--knob-face-bg, var(--color-neutral-surface));
background: var(--knob-face-bg, var(--color-surface-raised));
z-index: 0;
}
/* Value readout centered in the dial's hole (grid item, above the face). */
[data-knob-value-text] {
position: relative;
z-index: 2;
font-size: var(--_knob-value-font);
line-height: 1;
color: var(--color-neutral-text);
font-variant-numeric: tabular-nums;
}
/* Pointer, rotated by the provider-published angle */
/* Pointer, rotated by the provider-published angle. Layer 1: above the face,
* below the value. */
[data-knob-indicator] {
position: absolute;
inset: 0;
z-index: 1;
rotate: var(--knob-angle, -135deg);
pointer-events: none;
}
@ -91,13 +110,15 @@
inline-size: var(--knob-pointer-width, 3px); /* literal: hairline pointer */
block-size: 28%; /* literal: pointer proportion of face */
border-radius: var(--radius-sm);
background: var(--knob-pointer-bg, var(--color-neutral-content));
background: var(--knob-pointer-bg, var(--color-neutral-text-strong));
}
/* ---- states (R-1.x coverage) -------------------------------------------- */
/* Hover: subtly brighten the whole dial (the arc lives on ::before, so a
* background-color on the control wouldn't show). */
[data-knob-control]:hover {
background-color: var(--state-hover);
filter: brightness(1.08);
}
[data-knob-control]:focus-visible {
@ -148,12 +169,5 @@
[data-knob-label] {
font-size: var(--_knob-label-font);
line-height: var(--leading-ui);
color: var(--color-neutral-content);
}
[data-knob-value-text] {
font-size: var(--_knob-value-font);
line-height: var(--leading-ui);
color: var(--color-neutral-content-subtle);
font-variant-numeric: tabular-nums;
color: var(--color-neutral-text);
}

@ -23,6 +23,7 @@
let invalid = $state(false);
let intent = $state<Intent>('neutral');
let size = $state<KnobSize>('md');
let showNeedle = $state(true);
const compiled = compileMorfo(knobMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
@ -128,9 +129,11 @@
aria-label="Gain"
>
<Knob.Control>
<Knob.Indicator />
{#if showNeedle}
<Knob.Indicator />
{/if}
<Knob.ValueText>{value}</Knob.ValueText>
</Knob.Control>
<Knob.ValueText>{value}</Knob.ValueText>
</Knob>
</div>
<div data-uix-stage-trace>
@ -250,6 +253,14 @@
></span
>
</label>
<label data-uix-control>
<span data-uix-control-label>needle</span>
<span data-uix-switch
><input type="checkbox" bind:checked={showNeedle} /><span data-uix-switch-label
>{showNeedle ? 'on' : 'off'}</span
></span
>
</label>
</div>
<div data-uix-subsection-head>

Loading…
Cancel
Save

Powered by TurnKey Linux.