fix(eidos): QrCode logo visibility + demo content presets

The logo overlay rendered invisible. Inside a viewBox-scaled foreignObject a
fixed px padding (var(--space-1)) equals that many *modules*, overflowing the
cleared centre and collapsing the content box; inline-size/block-size also
don't size an <svg> element. Fix: proportional inline padding + physical
width/height. The logo now renders and still decodes at ECC H. Dropped the
now-orphan --qr-code-overlay-padding token + regenerated base.css.

Demo: content presets (URL / vCard / WiFi / unicode) prove the encoder takes
any UTF-8 string, not just URLs (each verified decoding via jsQR); logo on by
default; quiet-zone chips fixed to canonical [4, 8, 16] (was [2, …], below the
spec's 4-module minimum). README documents the quiet zone as modules, not
pixels — the canonical unit; for px layout spacing, pad the container in CSS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active-uix
dev 4 months ago
parent e0579f7e24
commit e7a3d9085b

@ -53,6 +53,12 @@ decorative shape.
- **Fixed-tone default colours** (dark-on-light) — a QR can't invert in dark - **Fixed-tone default colours** (dark-on-light) — a QR can't invert in dark
mode and stay scannable, so the default fg/bg are theme-independent; theming mode and stay scannable, so the default fg/bg are theme-independent; theming
is opt-in via `color`/`background` (you own the contrast). is opt-in via `color`/`background` (you own the contrast).
- **Quiet zone is in modules, not pixels** — `margin` measures the quiet zone in
QR *modules* (spec minimum 4 — ISO/IEC 18004), the canonical unit: it scales
with the symbol so the clear ring stays proportional at any rendered size,
which is what guarantees scannability. There is deliberately no pixel margin
prop. For fixed pixel spacing in a layout, pad the container in CSS using the
QR's `background` colour — the symbol's quiet zone stays canonical in modules.
- **Export bakes resolved colours** — a serialized SVG loses the document's CSS - **Export bakes resolved colours** — a serialized SVG loses the document's CSS
custom properties, so `DownloadTrigger` resolves `getComputedStyle` fills into custom properties, so `DownloadTrigger` resolves `getComputedStyle` fills into
the clone before writing PNG/SVG. the clone before writing PNG/SVG.
@ -87,7 +93,7 @@ a `$derived`, rendered to SVG. Download is imperative via `$adom`.
| `color` | `AffirmativeColorRole \| string` | `currentColor`/token | module colour | | `color` | `AffirmativeColorRole \| string` | `currentColor`/token | module colour |
| `background` | `string` | token / transparent | quiet-zone + module background | | `background` | `string` | token / transparent | quiet-zone + module background |
| `cellShape` | `'square' \| 'rounded' \| 'dots'` | `'square'` | module shape | | `cellShape` | `'square' \| 'rounded' \| 'dots'` | `'square'` | module shape |
| `margin` | `number` | `4` | quiet zone in modules (spec minimum 4) | | `margin` | `number` | `4` | quiet zone in modules, not pixels (spec min 4 — see Scannability) |
| `logo` | `Snippet` | — | centred overlay; clears the centre + boosts ECC to `H` | | `logo` | `Snippet` | — | centred overlay; clears the centre + boosts ECC to `H` |
| `alt` | `string` | `value` | accessible name for the Frame | | `alt` | `string` | `value` | accessible name for the Frame |

@ -11,21 +11,26 @@
/* Logo overlay — fills the cleared centre (a foreignObject), centres the /* Logo overlay — fills the cleared centre (a foreignObject), centres the
* media, paints its own light backing so it reads over the modules. */ * media, paints its own light backing so it reads over the modules. */
/* width/height (physical) — not inline-size/block-size: logical props don't
* size an <svg> element (it uses width/height as geometry) nor resolve cleanly
* for HTML inside a viewBox-scaled foreignObject. */
[data-qr-code-overlay] { [data-qr-code-overlay] {
box-sizing: border-box; box-sizing: border-box;
inline-size: 100%; width: 100%;
block-size: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
background: var(--qr-code-overlay-bg); background: var(--qr-code-overlay-bg);
border-radius: var(--qr-code-overlay-radius); border-radius: var(--qr-code-overlay-radius);
padding: var(--qr-code-overlay-padding); /* Padding is set inline (proportional to the cleared centre): a fixed px
* padding equals that many *modules* inside a viewBox-scaled foreignObject
* and would collapse the content box. */
} }
[data-qr-code-overlay] :where(img, svg) { [data-qr-code-overlay] :where(img, svg) {
inline-size: 100%; width: 100%;
block-size: 100%; height: 100%;
object-fit: contain; object-fit: contain;
} }

@ -149,7 +149,13 @@
<path d={path} fill="var(--qr-code-fg)" /> <path d={path} fill="var(--qr-code-fg)" />
{#if logo} {#if logo}
<foreignObject x={overlayPos} y={overlayPos} width={clear} height={clear}> <foreignObject x={overlayPos} y={overlayPos} width={clear} height={clear}>
<div data-qr-code-overlay xmlns="http://www.w3.org/1999/xhtml">{@render logo()}</div> <!-- Explicit px size: inside a foreignObject px == user units == the fo box;
percentages don't resolve against a foreignObject, so the overlay needs
a deterministic size to fill the cleared centre. --><div
data-qr-code-overlay
xmlns="http://www.w3.org/1999/xhtml"
style="width:{clear}px;height:{clear}px;padding:{(clear * 0.1).toFixed(2)}px"
>{@render logo()}</div>
</foreignObject> </foreignObject>
{/if} {/if}
</svg> </svg>

@ -1907,7 +1907,6 @@
--qr-code-size-xl: 256px; --qr-code-size-xl: 256px;
--qr-code-radius: var(--radius-md); --qr-code-radius: var(--radius-md);
--qr-code-overlay-bg: #ffffff; --qr-code-overlay-bg: #ffffff;
--qr-code-overlay-padding: var(--space-1);
--qr-code-overlay-radius: var(--radius-sm); --qr-code-overlay-radius: var(--radius-sm);
--radio-cards-gap: var(--space-3); --radio-cards-gap: var(--space-3);
--radio-cards-horizontal-basis: 12rem; --radio-cards-horizontal-basis: 12rem;

@ -1951,7 +1951,6 @@ export const THEME_BASE_RECIPE_TOKENS = {
'size-xl': '256px', 'size-xl': '256px',
radius: 'var(--radius-md)', radius: 'var(--radius-md)',
'overlay-bg': '#ffffff', 'overlay-bg': '#ffffff',
'overlay-padding': 'var(--space-1)',
'overlay-radius': 'var(--radius-sm)' 'overlay-radius': 'var(--radius-sm)'
}, },
// RadioCards — card-style skin over RadioGroup. Reuses RadioGroup's // RadioCards — card-style skin over RadioGroup. Reuses RadioGroup's

@ -11,13 +11,27 @@
type Tab = 'live' | 'examples' | 'api' | 'morfo' | 'recipe' | 'a11y'; type Tab = 'live' | 'examples' | 'api' | 'morfo' | 'recipe' | 'a11y';
let tab = $state<Tab>('live'); let tab = $state<Tab>('live');
let value = $state('https://uix.dev/qr-code'); // The encoder takes ANY UTF-8 string — not just URLs. The reader decides how
// to interpret it (vCard contact, WiFi join, geo, plain text, …); the QR is
// content-agnostic. Multi-line + accents/CJK go through byte (UTF-8) mode.
const presets: { label: string; value: string }[] = [
{ label: 'URL', value: 'https://uix.dev/qr-code' },
{
label: 'vCard',
value:
'BEGIN:VCARD\nVERSION:3.0\nFN:Juan Vicente\nORG:UIX\nTITLE:Maker\nTEL;TYPE=CELL:+34600123456\nEMAIL:hola@uix.dev\nURL:https://uix.dev\nEND:VCARD'
},
{ label: 'WiFi', value: 'WIFI:T:WPA;S:UIX-Guest;P:s3cret-pass;;' },
{ label: 'Text · unicode', value: 'Diseñando lo que ocurre — açaí · ñoño · 日本語 · 🎯' }
];
let value = $state(presets[0].value);
let size = $state<QrCodeSize>('lg'); let size = $state<QrCodeSize>('lg');
let cellShape = $state<QrCodeCellShape>('square'); let cellShape = $state<QrCodeCellShape>('square');
let errorCorrection = $state<ErrorCorrection>('M'); let errorCorrection = $state<ErrorCorrection>('M');
let margin = $state(4); let margin = $state(4);
let color = $state<string | undefined>(undefined); let color = $state<string | undefined>(undefined);
let withLogo = $state(false); let withLogo = $state(true);
const sizes: QrCodeSize[] = ['xs', 'sm', 'md', 'lg', 'xl']; const sizes: QrCodeSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];
const shapes: QrCodeCellShape[] = ['square', 'rounded', 'dots']; const shapes: QrCodeCellShape[] = ['square', 'rounded', 'dots'];
@ -60,8 +74,16 @@
</svelte:head> </svelte:head>
{#snippet brandLogo()} {#snippet brandLogo()}
<svg viewBox="0 0 24 24" fill={color ?? '#18181b'} aria-hidden="true"> <!-- Test logo: a brand monogram. Its own colour (not the QR fg) so it reads as a logo. -->
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" /> <svg viewBox="0 0 32 32" aria-hidden="true">
<rect width="32" height="32" rx="8" fill="#7c3aed" />
<path
d="M10 9v8a6 6 0 0 0 12 0V9"
fill="none"
stroke="#fff"
stroke-width="3.5"
stroke-linecap="round"
/>
</svg> </svg>
{/snippet} {/snippet}
@ -117,8 +139,16 @@
<p data-uix-section-desc>Pure <span data-uix-layer-badge="eidos">eidos</span> display — no soma. <code>value</code> → matrix → SVG.</p> <p data-uix-section-desc>Pure <span data-uix-layer-badge="eidos">eidos</span> display — no soma. <code>value</code> → matrix → SVG.</p>
<div data-uix-controls> <div data-uix-controls>
<label data-uix-control> <label data-uix-control>
<span data-uix-control-label>value</span> <span data-uix-control-label>content preset</span>
<input type="text" bind:value /> <span data-uix-chips role="radiogroup">
{#each presets as p}
<button data-uix-chip data-active={value === p.value} onclick={() => (value = p.value)}>{p.label}</button>
{/each}
</span>
</label>
<label data-uix-control style="flex-basis: 100%;">
<span data-uix-control-label>value (any UTF-8 string)</span>
<textarea bind:value rows="3" style="resize: vertical; font-family: inherit;"></textarea>
</label> </label>
<label data-uix-control> <label data-uix-control>
<span data-uix-control-label>cellShape</span> <span data-uix-control-label>cellShape</span>
@ -153,9 +183,9 @@
</span> </span>
</label> </label>
<label data-uix-control> <label data-uix-control>
<span data-uix-control-label>margin (quiet zone)</span> <span data-uix-control-label>quiet zone · modules (spec min 4)</span>
<span data-uix-chips role="radiogroup"> <span data-uix-chips role="radiogroup">
{#each [2, 4, 6] as m} {#each [4, 8, 16] as m}
<button data-uix-chip data-active={margin === m} onclick={() => (margin = m)}>{m}</button> <button data-uix-chip data-active={margin === m} onclick={() => (margin = m)}>{m}</button>
{/each} {/each}
</span> </span>
@ -244,7 +274,7 @@
<tr><td class="name">--qr-code-fg / --qr-code-bg</td><td>module colour / quiet-zone (fixed-tone)</td></tr> <tr><td class="name">--qr-code-fg / --qr-code-bg</td><td>module colour / quiet-zone (fixed-tone)</td></tr>
<tr><td class="name">--qr-code-size-{'{'}xs…xl{'}'}</td><td>size presets</td></tr> <tr><td class="name">--qr-code-size-{'{'}xs…xl{'}'}</td><td>size presets</td></tr>
<tr><td class="name">--qr-code-radius</td><td>frame radius</td></tr> <tr><td class="name">--qr-code-radius</td><td>frame radius</td></tr>
<tr><td class="name">--qr-code-overlay-bg / -padding / -radius</td><td>logo backing</td></tr> <tr><td class="name">--qr-code-overlay-bg / -radius</td><td>logo backing (padding is proportional, inline)</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>

Loading…
Cancel
Save

Powered by TurnKey Linux.