diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2d724..d72c9d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.2.2 — 2026-10-15 + +- Align the separate chip rim with the updated Penpot master: a continuous 1px inner border above every edge insert, using shared design constants on single chips and stack layers. +- Add magnified chip-border previews and document the rim's sizing and paint order. +- Verify that the rim remains visible over all eight inserts across every denomination and custom chips. + ## 0.2.1 — 2026-10-15 - Add the approved two-layer Contact shadow to single chips and each visible chip in a stack, enabled by default and scaled with the chip geometry. diff --git a/README.md b/README.md index 21d377e..4f8215a 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,10 @@ Shadow lengths scale with numeric `width` or `height` props. Override a single c For sizing driven entirely by application CSS, use a shadow value suited to that layout. The standard card's inner panel and the back's decorative frame retain their design strokes. The retired `--card-border` token no longer controls the outer edge. +## Chip borders + +Every chip has a continuous black inner border at 28% opacity, painted above all eight edge inserts. The border is 1px at the 64px design size and scales with the chip geometry. It applies to all denominations, custom chips and every visible stack layer, including when shadows are disabled. + ## Chip shadows `Chip` and `ChipStack` use the Contact shadow by default. At the 64px design size, it combines two independent black layers: offset `0 1`, blur `1`, opacity `35%`; and offset `0 2`, blur `4`, opacity `25%`. The shadow follows the circular artwork and scales with the chip geometry. Each visible chip in a stack gets its own shadow, so overlapping layers remain distinct. diff --git a/apps/playground/e2e/chip-shadows.spec.ts b/apps/playground/e2e/chip-shadows.spec.ts index 2759882..b2f3fd1 100644 --- a/apps/playground/e2e/chip-shadows.spec.ts +++ b/apps/playground/e2e/chip-shadows.spec.ts @@ -68,6 +68,9 @@ test('Contact shadows render outside chips, separate stack layers and support ov ), ).toBe(true); await page.getByLabel('Chip shadow', { exact: true }).selectOption('design'); + await page.locator('.chip-rim-lab').screenshot({ + path: `test-results/chip-rims-${testInfo.project.name}.png`, + }); await page.locator('.chip-shadow-lab').screenshot({ path: `test-results/chip-shadows-${testInfo.project.name}.png`, }); @@ -81,6 +84,9 @@ test('Contact shadows render outside chips, separate stack layers and support ov page.locator('.chip-stage > svg').nth(1).locator('[data-chip-face],filter'), ).toHaveCount(0); await page.setViewportSize({ width: 390, height: 844 }); + await page.locator('.chip-rim-lab').screenshot({ + path: `test-results/chip-rims-mobile-${testInfo.project.name}.png`, + }); await page.locator('.chip-shadow-lab').screenshot({ path: `test-results/chip-shadows-mobile-${testInfo.project.name}.png`, }); diff --git a/apps/playground/src/main.tsx b/apps/playground/src/main.tsx index 9ea5df5..8f044af 100644 --- a/apps/playground/src/main.tsx +++ b/apps/playground/src/main.tsx @@ -705,6 +705,27 @@ function App() { +
+

CHIP BORDERS

+

One continuous edge.

+

+ A thin border follows the whole chip, including every light + insert. Shown at 4× size, without shadows, for a closer look. +

+
+ {[1, 100, 250].map((v) => ( +
+ + + {v === 250 ? 'Custom · 250' : v} · 4× close-up + +
+ ))} +
+
div { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} +.chip-rim-previews span { + font-size: 12px; + color: #74806f; +} .chip-shadow-surfaces { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); diff --git a/docs/api.md b/docs/api.md index 76debef..2f9c97d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -26,6 +26,8 @@ Inline artwork objects are trusted build-time constants, not a sanitization boun Contact consists of independent `0 1 blur 1 / 35%` and `0 2 blur 4 / 25%` black shadows at 64 units. Runtime SVG filters use Gaussian deviations of 0.5 and 2, matching Penpot. Both the geometry and shadows scale together. `shadow={false}` disables them; otherwise `style.filter` overrides `--chip-shadow`, which overrides the default. A direct `style.filter` applies to each chip, rather than filtering the combined stack. Custom filter lengths are in the 64-unit chip coordinate system. Use `none` or a CSS filter value such as `drop-shadow(0 2px 3px rgba(0,0,0,.3))`. The previous `CHIP.shadow` Subtle preset is deprecated; `CHIP.contactShadows` contains the current layers. +`CHIP.rim` defines the continuous inner border: width 1, color `#000000`, opacity 0.28 at 64 units. It paints after Edge Inserts and before Inner Ring on every chip, including stack layers. The SVG stroke uses radius 31.5 and width 1 to remain inside the 32-unit outer radius. It scales with the geometry and remains visible when shadows are disabled. + Colours are configurable defaults, not universal casino denominations. ## Markers diff --git a/docs/design/chip-rim-specification.json b/docs/design/chip-rim-specification.json new file mode 100644 index 0000000..a7c260c --- /dev/null +++ b/docs/design/chip-rim-specification.json @@ -0,0 +1,438 @@ +{ + "file": "Playing Cards", + "master": { + "id": "26c05812-20d7-8042-8008-c688f846feaf", + "name": "Poker / Chip", + "x": 240, + "y": 2100, + "width": 64, + "height": 64, + "children": [ + { + "id": "26c05812-20d7-8042-8008-c688f858d612", + "name": "Outer Edge", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [] + }, + { + "id": "26c05812-20d7-8042-8008-c688f8a7399c", + "name": "Edge Inserts", + "type": "group", + "width": 63.99950408935547, + "height": 63.9996337890625, + "strokes": [], + "children": [] + }, + { + "id": "dcb052a4-ae96-8045-8008-cae9ecb6f7ce", + "name": "Rim", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [ + { + "strokeColor": "#000000", + "strokeColorRefFile": null, + "strokeColorRefId": null, + "strokeOpacity": 0.28, + "strokeStyle": "solid", + "strokeWidth": 1, + "strokeAlignment": "inner", + "strokeCapStart": null, + "strokeCapEnd": null, + "strokeColorGradient": null, + "strokeImage": null + } + ] + }, + { + "id": "26c05812-20d7-8042-8008-c688f8a9ea4c", + "name": "Inner Ring", + "type": "ellipse", + "width": 45, + "height": 45, + "strokes": [] + }, + { + "id": "26c05812-20d7-8042-8008-c688f8b48a09", + "name": "Center Field", + "type": "ellipse", + "width": 41, + "height": 41, + "strokes": [] + }, + { + "id": "26c05812-20d7-8042-8008-c688f8bfa8ac", + "name": "Label", + "type": "text", + "width": 40, + "height": 24, + "strokes": [] + } + ] + }, + "canonicalRims": [ + { + "id": "26c05812-20d7-8042-8008-c68a53bc779a", + "name": "Chip 1", + "width": 64, + "height": 64, + "paintOrder": [ + "Outer Edge", + "Edge Inserts", + "Rim", + "Inner Ring", + "Center Field", + "Label" + ], + "rim": { + "id": "dcb052a4-ae96-8045-8008-cae9ece36429", + "name": "Rim", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [ + { + "strokeColor": "#000000", + "strokeColorRefFile": null, + "strokeColorRefId": null, + "strokeOpacity": 0.28, + "strokeStyle": "solid", + "strokeWidth": 1, + "strokeAlignment": "inner", + "strokeCapStart": null, + "strokeCapEnd": null, + "strokeColorGradient": null, + "strokeImage": null + } + ] + } + }, + { + "id": "26c05812-20d7-8042-8008-c68a53e11da9", + "name": "Chip 500", + "width": 64, + "height": 64, + "paintOrder": [ + "Outer Edge", + "Edge Inserts", + "Rim", + "Inner Ring", + "Center Field", + "Label" + ], + "rim": { + "id": "dcb052a4-ae96-8045-8008-cae9eceb5b04", + "name": "Rim", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [ + { + "strokeColor": "#000000", + "strokeColorRefFile": null, + "strokeColorRefId": null, + "strokeOpacity": 0.28, + "strokeStyle": "solid", + "strokeWidth": 1, + "strokeAlignment": "inner", + "strokeCapStart": null, + "strokeCapEnd": null, + "strokeColorGradient": null, + "strokeImage": null + } + ] + } + }, + { + "id": "26c05812-20d7-8042-8008-c68a53ea2e42", + "name": "Chip 1K", + "width": 64, + "height": 64, + "paintOrder": [ + "Outer Edge", + "Edge Inserts", + "Rim", + "Inner Ring", + "Center Field", + "Label" + ], + "rim": { + "id": "dcb052a4-ae96-8045-8008-cae9ececd992", + "name": "Rim", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [ + { + "strokeColor": "#000000", + "strokeColorRefFile": null, + "strokeColorRefId": null, + "strokeOpacity": 0.28, + "strokeStyle": "solid", + "strokeWidth": 1, + "strokeAlignment": "inner", + "strokeCapStart": null, + "strokeCapEnd": null, + "strokeColorGradient": null, + "strokeImage": null + } + ] + } + }, + { + "id": "26c05812-20d7-8042-8008-c68a53d0113d", + "name": "Chip 25", + "width": 64, + "height": 64, + "paintOrder": [ + "Outer Edge", + "Edge Inserts", + "Rim", + "Inner Ring", + "Center Field", + "Label" + ], + "rim": { + "id": "dcb052a4-ae96-8045-8008-cae9ecee8459", + "name": "Rim", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [ + { + "strokeColor": "#000000", + "strokeColorRefFile": null, + "strokeColorRefId": null, + "strokeOpacity": 0.28, + "strokeStyle": "solid", + "strokeWidth": 1, + "strokeAlignment": "inner", + "strokeCapStart": null, + "strokeCapEnd": null, + "strokeColorGradient": null, + "strokeImage": null + } + ] + } + }, + { + "id": "26c05812-20d7-8042-8008-c68a53c72ecc", + "name": "Chip 5", + "width": 64, + "height": 64, + "paintOrder": [ + "Outer Edge", + "Edge Inserts", + "Rim", + "Inner Ring", + "Center Field", + "Label" + ], + "rim": { + "id": "dcb052a4-ae96-8045-8008-cae9ecf3f72a", + "name": "Rim", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [ + { + "strokeColor": "#000000", + "strokeColorRefFile": null, + "strokeColorRefId": null, + "strokeOpacity": 0.28, + "strokeStyle": "solid", + "strokeWidth": 1, + "strokeAlignment": "inner", + "strokeCapStart": null, + "strokeCapEnd": null, + "strokeColorGradient": null, + "strokeImage": null + } + ] + } + }, + { + "id": "26c05812-20d7-8042-8008-c68a53f510a6", + "name": "Chip 5K", + "width": 64, + "height": 64, + "paintOrder": [ + "Outer Edge", + "Edge Inserts", + "Rim", + "Inner Ring", + "Center Field", + "Label" + ], + "rim": { + "id": "dcb052a4-ae96-8045-8008-cae9ecf524b2", + "name": "Rim", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [ + { + "strokeColor": "#000000", + "strokeColorRefFile": null, + "strokeColorRefId": null, + "strokeOpacity": 0.28, + "strokeStyle": "solid", + "strokeWidth": 1, + "strokeAlignment": "inner", + "strokeCapStart": null, + "strokeCapEnd": null, + "strokeColorGradient": null, + "strokeImage": null + } + ] + } + }, + { + "id": "26c05812-20d7-8042-8008-c68a53fe39a5", + "name": "Chip 250 — Custom", + "width": 64, + "height": 64, + "paintOrder": [ + "Outer Edge", + "Edge Inserts", + "Rim", + "Inner Ring", + "Center Field", + "Label" + ], + "rim": { + "id": "dcb052a4-ae96-8045-8008-cae9ecf959ee", + "name": "Rim", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [ + { + "strokeColor": "#000000", + "strokeColorRefFile": null, + "strokeColorRefId": null, + "strokeOpacity": 0.28, + "strokeStyle": "solid", + "strokeWidth": 1, + "strokeAlignment": "inner", + "strokeCapStart": null, + "strokeCapEnd": null, + "strokeColorGradient": null, + "strokeImage": null + } + ] + } + }, + { + "id": "26c05812-20d7-8042-8008-c68a53d82b98", + "name": "Chip 100", + "width": 64, + "height": 64, + "paintOrder": [ + "Outer Edge", + "Edge Inserts", + "Rim", + "Inner Ring", + "Center Field", + "Label" + ], + "rim": { + "id": "dcb052a4-ae96-8045-8008-cae9ecfbb250", + "name": "Rim", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [ + { + "strokeColor": "#000000", + "strokeColorRefFile": null, + "strokeColorRefId": null, + "strokeOpacity": 0.28, + "strokeStyle": "solid", + "strokeWidth": 1, + "strokeAlignment": "inner", + "strokeCapStart": null, + "strokeCapEnd": null, + "strokeColorGradient": null, + "strokeImage": null + } + ] + } + }, + { + "id": "26c05812-20d7-8042-8008-c68a539d73ec", + "name": "Canonical Chip 64", + "width": 64, + "height": 64, + "paintOrder": [ + "Outer Edge", + "Edge Inserts", + "Rim", + "Inner Ring", + "Center Field", + "Label" + ], + "rim": { + "id": "dcb052a4-ae96-8045-8008-cae9ecfe82b3", + "name": "Rim", + "type": "ellipse", + "width": 64, + "height": 64, + "strokes": [ + { + "strokeColor": "#000000", + "strokeColorRefFile": null, + "strokeColorRefId": null, + "strokeOpacity": 0.28, + "strokeStyle": "solid", + "strokeWidth": 1, + "strokeAlignment": "inner", + "strokeCapStart": null, + "strokeCapEnd": null, + "strokeColorGradient": null, + "strokeImage": null + } + ] + } + } + ], + "reviewBoard": [ + { + "id": "dcb052a4-ae96-8045-8008-cae9e1fc5be7", + "name": "Chip Rim Fix — before / after · AFTER = PRIMARY DESIGN (approved 2026-10-15)", + "x": 100, + "y": 3570, + "width": 1500, + "height": 720 + } + ], + "notes": [ + { + "id": "26c05812-20d7-8042-8008-c68a5ce0e7ea", + "text": "Chip ├ Outer Edge ├ Edge Inserts ├ Rim ├ Inner Ring ├ Center Field └ Label" + }, + { + "id": "dcb052a4-ae96-8045-8008-cae9e2095d06", + "text": "Chip rim — before / after · AFTER approved as the primary design" + }, + { + "id": "dcb052a4-ae96-8045-8008-cae9e216c85a", + "text": "Before: the 1 px rim stroke sat on Outer Edge, UNDER the Edge Inserts, so light inserts hid it and the silhouette looked dented. After: the rim is its own ring ABOVE the inserts — continuous on every chip." + }, + { + "id": "af8256be-c88d-80ed-8008-ca6147067e67", + "text": "0 2 0 · 45% (hard rim = chip thickness) + 0 4 8 · 25%" + }, + { + "id": "dcb052a4-ae96-8045-8008-caea2fbe3a4b", + "text": "3× ZOOM (1 px rim → 3 px)" + }, + { + "id": "dcb052a4-ae96-8045-8008-cae9e22e2774", + "text": "AFTER — PRIMARY DESIGN ✓ (approved)" + } + ] +} diff --git a/docs/design/chip-rims.md b/docs/design/chip-rims.md new file mode 100644 index 0000000..5fdf769 --- /dev/null +++ b/docs/design/chip-rims.md @@ -0,0 +1,9 @@ +# Continuous chip rim + +The current Poker / Chip master uses a separate Rim ellipse immediately above Edge Inserts and below Inner Ring. At 64 × 64 it has a 1px inner stroke, black at 28% opacity, with no fill. The base disc has no stroke. The same Rim appears on all canonical denominations and custom chips. + +SVG represents the inner stroke as a centered stroke at radius 31.5 with width 1, keeping its entire painted area inside the 32-unit outer radius. `CHIP.rim` holds the shared width, color and opacity. The rim scales with the chip geometry and is independent of Contact shadows. The geometry remains procedural and the labels remain live text; no font files or static chip assets are imported. + +The package already drew an equivalent continuous rim above the inserts. This update makes the layer and stroke width explicit, follows the master's paint order exactly, and adds raster coverage for all eight insert positions. It does not add a second border or make the rim thicker. + +The inspected master, canonical instances and approved before/after board are recorded in [chip-rim-specification.json](./chip-rim-specification.json). diff --git a/package.json b/package.json index cc7cadd..27635ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "feltforge-workspace", - "version": "0.2.1", + "version": "0.2.2", "private": true, "type": "module", "packageManager": "pnpm@11.24.0", diff --git a/packages/core/src/chips.ts b/packages/core/src/chips.ts index 5dc53b1..a2cf0e2 100644 --- a/packages/core/src/chips.ts +++ b/packages/core/src/chips.ts @@ -12,6 +12,7 @@ export const CHIP = { minSize: 32, stackOffset: { x: 3, y: -4 }, stackDefaultCap: 5, + rim: { width: 1, color: '#000000', opacity: 0.28 }, /** @deprecated Previous Subtle preset; use contactShadows for the current design. */ shadow: { x: 0, y: 2, blur: 2.5, opacity: 0.18 }, contactShadows: [ diff --git a/packages/react/src/chip.tsx b/packages/react/src/chip.tsx index e9dae9a..eec79c8 100644 --- a/packages/react/src/chip.tsx +++ b/packages/react/src/chip.tsx @@ -55,16 +55,19 @@ export function ChipGeometry({ {inserts.map((d, i) => ( ))} - - + {/* The separate inner rim paints above every edge insert. */} + + { }); }); describe('poker primitives', () => { + it('keeps a continuous inner rim visible over all eight inserts for every chip palette', () => { + for (const value of [1, 5, 25, 100, 500, 1000, 5000, 250]) { + const svg = render( + , + ).replace(']*data-chip-rim=""[^>]*(?:\/>|><\/circle>)/, + '', + ); + expect(withoutRim).not.toBe(svg); + const options = { font: { loadSystemFonts: false } }; + const bordered = PNG.sync.read(new Resvg(svg, options).render().asPng()); + const flat = PNG.sync.read( + new Resvg(withoutRim, options).render().asPng(), + ); + for (let i = 0; i < 8; i++) { + const angle = ((-90 + i * 45) * Math.PI) / 180; + const x = Math.round(128 + 126 * Math.cos(angle)); + const y = Math.round(128 + 126 * Math.sin(angle)); + const offset = (y * bordered.width + x) * 4; + const difference = Math.max( + ...[0, 1, 2].map( + (c) => flat.data[offset + c]! - bordered.data[offset + c]!, + ), + ); + expect( + difference, + `rim over insert ${i} on chip ${value}`, + ).toBeGreaterThan(5); + } + } + }); it('applies independent Contact layers to chips and every visible stack layer', () => { const out = render( <> @@ -127,6 +159,7 @@ describe('poker primitives', () => { const ids = [...out.matchAll(/ m[1]); expect(new Set(ids).size).toBe(2); expect(out.match(/data-chip-face=""/g)).toHaveLength(4); + expect(out.match(/data-chip-rim=""/g)).toHaveLength(4); expect(out.match(/in="SourceAlpha"/g)).toHaveLength(4); expect(out).toContain('stdDeviation="0.5"'); expect(out).toContain('stdDeviation="2"');