anvilsign in

collin/mahjong · 29a832cf

Party QR: a giant face-up tile in the middle of the felt

Collin Richards · 2026-08-20 17:09 UTC · 29a832cf0613e51d3b03b94fff33ff00b5adc993 · parent 822e6726 · browse files

modifiedsrc/App.tsx+3 −4
⋯ 375 unchanged lines
376376 {/* The tiles in the middle. Not on a phone: the compact layout has no
377377 centre square for them to be thrown into. */}
378378 {!compact && <Pool game={ctl} pool={pool} viewSeat={viewSeat} />}
379- {/* Party: one QR for the whole table, in the corner of the felt — the
380- corners are the one part of the table nothing else wants, and the
381- strips clip their own contents. Scanning it lands a phone on the seat
379+ {/* Party: one QR for the whole table, lying in the middle of the felt
380+ as a giant face-up tile. Scanning it lands a phone on the seat
382381 picker. Once every hand is held it steps aside, and comes back while
383382 any seat's 📱 tag is held down — for a player who lost theirs. */}
384383 {netMode === 'party' &&
385384 !compact &&
386385 (qrPeek || session!.seats.some((s) => s.ids.length === 0)) && (
387386 <div className="qr-dock">
388- <SeatQr url={roomLink(session!.room.code)} />
387+ <SeatQr url={roomLink(session!.room.code, session!.room.publicBase)} />
389388 <span className="qr-hint">
390389 掃碼入座,用手機拿牌
391390 <em>scan to join from your phone</em>
⋯ 27 unchanged lines
modifiedsrc/styles.css+36 −21
⋯ 1991 unchanged lines
19921992 }
19931993
19941994 /* ---------- the party table's QR ---------- */
1995-/* One join code for the whole table, in the bottom-right corner — the felt
1996- the four strips leave over. It must not cover any hand: an unclaimed seat
1997- is still played right here. */
1995+/* One join code for the whole table, lying in the middle of the felt like a
1996+ tile someone left face up. It may cover the pool while it shows — it steps
1997+ aside the moment every hand is held, and comes back while a 📱 tag is held
1998+ down. Below the result panel (z-index 2, later in the DOM), above the pool
1999+ and the centre readout (1): scannable while it matters, never over the
2000+ scores. */
19982001 .qr-dock {
19992002 position: absolute;
2000- right: 10px;
2001- bottom: 10px;
2002- z-index: 3;
2003+ left: 50%;
2004+ top: 50%;
2005+ transform: translate(-50%, -50%);
2006+ z-index: 2;
20032007 display: flex;
20042008 flex-direction: column;
20052009 align-items: center;
2006- gap: 4px;
2007- padding: 6px;
2008- border-radius: 10px;
2009- background: rgba(10, 49, 38, 0.92);
2010- border: 1px solid rgba(217, 180, 92, 0.45);
2010+ gap: 8px;
20112011 /* Purely something to look at: taps fall through to the table under it. */
20122012 pointer-events: none;
20132013 }
2014+/* The QR wears the face of a giant tile: ivory, softly bevelled, the same
2015+ rounded corners and resting shadow the sprite gives the real ones, with the
2016+ code engraved in the deep green the dragons are cut in. */
20142017 .seat-qr {
2015- width: clamp(72px, calc(var(--depth) * 0.44), 132px);
2016- aspect-ratio: 1;
2017- border-radius: 6px;
2018- overflow: hidden;
2019- background: #fff;
2020- padding: 3px;
2018+ --qw: clamp(96px, calc(var(--depth) * 0.56), 168px);
2019+ width: var(--qw);
2020+ height: calc(var(--qw) * 1.2);
2021+ display: grid;
2022+ place-items: center;
2023+ padding: calc(var(--qw) * 0.09);
2024+ border-radius: calc(var(--qw) * 0.1);
2025+ background: linear-gradient(165deg, #fffdf3, #f4edd9 58%, #e6dcbf);
2026+ box-shadow:
2027+ inset 0 2px 1px rgba(255, 255, 255, 0.85),
2028+ inset 0 calc(var(--qw) * -0.05) calc(var(--qw) * 0.04) rgba(148, 130, 90, 0.4),
2029+ inset calc(var(--qw) * -0.02) 0 calc(var(--qw) * 0.03) rgba(148, 130, 90, 0.22),
2030+ 0 6px 12px rgba(0, 0, 0, 0.5);
20212031 }
20222032 .seat-qr svg {
20232033 display: block;
20242034 width: 100%;
2025- height: 100%;
2035+ height: auto;
2036+ aspect-ratio: 1;
20262037 }
20272038 .qr-hint {
2028- font-size: 10px;
2029- line-height: 1.3;
2039+ font-size: 11px;
2040+ line-height: 1.35;
20302041 color: var(--ivory);
20312042 text-align: center;
2032- max-width: 140px;
2043+ max-width: 190px;
2044+ padding: 5px 12px;
2045+ border-radius: 999px;
2046+ background: rgba(10, 49, 38, 0.88);
2047+ border: 1px solid rgba(217, 180, 92, 0.4);
20332048 }
20342049 .qr-hint em {
20352050 display: block;
⋯ 197 unchanged lines
modifiedsrc/ui/SeatQr.tsx+6 −1
⋯ 5 unchanged lines
66 * way it faces is its seat's business, and it reads at any angle anyway.
77 */
88 export function SeatQr({ url }: { url: string }) {
9- const svg = useMemo(() => renderSVG(url, { border: 1 }), [url]);
9+ // Transparent ground, dark-green ink: the code is engraved into whatever
10+ // face the CSS gives the div, the way a dragon tile is cut.
11+ const svg = useMemo(
12+ () => renderSVG(url, { border: 1, whiteColor: 'transparent', blackColor: '#14402f' }),
13+ [url],
14+ );
1015 return <div className="seat-qr" dangerouslySetInnerHTML={{ __html: svg }} />;
1116 }