{"release_id":"01a1c502-4ad1-0000-ec62-adc54d3c69aa","chunk_id":"01a1c502-4c42-7e40-93ed-8e1e5c72bc92","file":"DvmPmuaT.mjs","mappings":"ggBAOO,MAAMA,EAAoB,CAAC,KAAM,KAAM,KAAM,KAAM,KAAM,KAAM,KAAM,IAAI,EAG9C,IAAI,IAAIA,CAAiB,EAuBpD,SAASC,EACfC,EACAC,EAIsB,CACtB,IAAIC,EAAMF,EAAU,MAAA,EACpB,GAAIC,EAAM,OAAS,UAAYA,EAAM,OAAS,SAAU,CACvD,MAAME,EAAOF,EAAM,OAAS,SAAWA,EAAM,MAAQA,EAAM,cAC3DC,EAAMA,EACJ,IAAKE,GACLA,EAAE,QAAUD,EACT,CAAE,GAAGC,EAAG,MAAOA,EAAE,MAAQ,EAAG,QAASA,EAAE,QAAQ,OAAQC,GAAOA,IAAOJ,EAAM,MAAM,GACjFG,CAAA,EAEH,OAAQA,GAAMA,EAAE,MAAQ,CAAC,CAC5B,CACA,GAAIH,EAAM,OAAS,OAASA,EAAM,OAAS,SAAU,CACpD,MAAMK,EAAWJ,EAAI,KAAME,GAAMA,EAAE,QAAUH,EAAM,KAAK,EACpDK,EACEA,EAAS,QAAQ,SAASL,EAAM,MAAM,IAC1CC,EAAMA,EAAI,IAAKE,GACdA,EAAE,QAAUH,EAAM,MACf,CAAE,GAAGG,EAAG,MAAOA,EAAE,MAAQ,EAAG,QAAS,CAAC,GAAGA,EAAE,QAASH,EAAM,MAAM,GAChEG,CAAA,GAILF,EAAI,KAAK,CAAE,MAAOD,EAAM,MAAO,MAAO,EAAG,QAAS,CAACA,EAAM,MAAM,CAAA,CAAG,CAEpE,CACA,OAAOC,EAAI,KAAK,CAACE,EAAGG,IACnBH,EAAE,QAAUG,EAAE,MAAQA,EAAE,MAAQH,EAAE,MAAQA,EAAE,MAAM,cAAcG,EAAE,KAAK,CAAA,CAEzE,+LCpEA,SAsBsC,IAAAC,gBAAQ,OAAO,kCAInDC,EAAGC,EAAA,EAAHC,EAAAF,qBACoBG,GAAGA,EAAE,SAALA,IAAC,CACZ,MAAAC,qBAA6B,MAAIC,EAAIF,CAAC,EAAC,QAAQ,SAAQG,EAAA,QAAA,CAAA,EAC9D,IAAAC,EAAAC,EAAA,QAOC,IAAAC,EAAIC,EAPLH,CAAA,MAOCE,EAAI,EAAA,IAAJA,CAAI,EACJ,IAAAE,IADAF,EAAI,CAAA,MACJE,EAAI,EAAA,IAAJA,CAAI,EARLC,EAAAL,CAAA,QAAAM,EAAAC,EAAAP,EAAA,EAAA,qBAAA,KAAAM,EAAA,CAAA,OAAAR,EAGcD,CAAa,CAAA,CAAA,EAH3BW,EAAAR,EAAA,aAAAS,CAAA,EAOqBC,EAAAC,EAAAb,EAAAF,CAAC,EAAC,KAAK,EACPc,EAAAE,EAAAd,EAAAF,CAAC,EAAC,KAAK,IAHhB,IAAAiB,EAAE,2BAA0B,CAAI,EAACf,EAAEF,CAAC,EAAC,MAAO,MAAKE,EAAEF,CAAC,EAAC,KAAK,CAAA,cALtEI,EAAA,IAAAD,EAAA,SAAAD,EAIwBF,CAAC,EAAC,KAAK,CAAA,EAJ/BkB,EAAAC,EAAAf,CAAA,MAHFP,CAAG,EAAHuB,EAAA,IAAAR,EAAAf,eAA8BD,EAAK,CAAA,CAAA,MAAnCC,CAAG,WADUM,EAAA,UAAA,OAAS,GAACkB,EAAAC,CAAA,cAFjB","names":["ALLOWED_REACTIONS","applyReactionEvent","aggregate","event","arr","prev","a","id","existing","b","align","div","root_1","$.each","r","viewerReacted","$.get","$$props","button","root_2","span","$.child","span_1","$.reset","classes","$.set_class","$.set_attribute","$0","$.set_text","text","text_1","t","$.append","$$anchor","$.template_effect","$$render","consequent"],"sources":["../../../../../../src/lib/chat/reactions.ts","../../../../../../src/lib/components/chat/ReactionChips.svelte"],"sourcesContent":["/** RAT-350: emoji reaction whitelist + types — client-safe.\n *\n *  Lives outside $lib/server so the UI can render the same picker without\n *  pulling drizzle / db into the client bundle. The server module re-exports\n *  these symbols.\n */\n\nexport const ALLOWED_REACTIONS = ['👍', '❤️', '😂', '🔥', '😮', '😢', '👏', '🙏'] as const;\nexport type Reaction = (typeof ALLOWED_REACTIONS)[number];\n\nconst REACTION_SET: Set<string> = new Set(ALLOWED_REACTIONS);\n\nexport function isAllowedReaction(emoji: string): emoji is Reaction {\n\treturn REACTION_SET.has(emoji);\n}\n\nexport type ReactionAction = 'added' | 'removed' | 'changed';\nexport interface ReactionToggleResult {\n\taction: ReactionAction;\n\temoji: string;\n\tpreviousEmoji: string | null;\n}\n\nexport interface ReactionAggregate {\n\temoji: string;\n\tcount: number;\n\tuserIds: string[];\n}\n\n/** Pure helper for clients applying a realtime row event onto the existing\n *  aggregate array for a message. INSERT bumps the count for `emoji` and\n *  appends the user; DELETE drops the user (and the chip if count hits 0);\n *  UPDATE is a delete-from-old + add-to-new. */\nexport function applyReactionEvent(\n\taggregate: ReactionAggregate[],\n\tevent:\n\t\t| { kind: 'add'; emoji: string; userId: string }\n\t\t| { kind: 'remove'; emoji: string; userId: string }\n\t\t| { kind: 'change'; previousEmoji: string; emoji: string; userId: string }\n): ReactionAggregate[] {\n\tlet arr = aggregate.slice();\n\tif (event.kind === 'remove' || event.kind === 'change') {\n\t\tconst prev = event.kind === 'remove' ? event.emoji : event.previousEmoji;\n\t\tarr = arr\n\t\t\t.map((a) =>\n\t\t\t\ta.emoji === prev\n\t\t\t\t\t? { ...a, count: a.count - 1, userIds: a.userIds.filter((id) => id !== event.userId) }\n\t\t\t\t\t: a\n\t\t\t)\n\t\t\t.filter((a) => a.count > 0);\n\t}\n\tif (event.kind === 'add' || event.kind === 'change') {\n\t\tconst existing = arr.find((a) => a.emoji === event.emoji);\n\t\tif (existing) {\n\t\t\tif (!existing.userIds.includes(event.userId)) {\n\t\t\t\tarr = arr.map((a) =>\n\t\t\t\t\ta.emoji === event.emoji\n\t\t\t\t\t\t? { ...a, count: a.count + 1, userIds: [...a.userIds, event.userId] }\n\t\t\t\t\t\t: a\n\t\t\t\t);\n\t\t\t}\n\t\t} else {\n\t\t\tarr.push({ emoji: event.emoji, count: 1, userIds: [event.userId] });\n\t\t}\n\t}\n\treturn arr.sort((a, b) =>\n\t\ta.count !== b.count ? b.count - a.count : a.emoji.localeCompare(b.emoji)\n\t);\n}\n","<script lang=\"ts\">\n\t/** RAT-350: aggregated emoji-reaction row under a chat bubble.\n\t *\n\t *  Each chip is `{emoji} {count}`; the chip the viewer reacted with is\n\t *  filled (you-already-tapped state). Tapping any chip toggles your\n\t *  reaction — pure server roundtrip + optimistic patch handled by the\n\t *  parent page.\n\t *\n\t *  Empty array → nothing renders (no empty row).\n\t */\n\timport type { ReactionAggregate } from '$lib/chat/reactions';\n\timport { t } from '$lib/i18n';\n\n\ttype Props = {\n\t\treactions: ReactionAggregate[];\n\t\tviewerId: string | null;\n\t\tonToggle: (emoji: string) => void;\n\t\t// Right-aligns the chips when the bubble is a self-message (own\n\t\t// outgoing bubbles sit on the right of the thread).\n\t\talign?: 'start' | 'end';\n\t};\n\n\tlet { reactions, viewerId, onToggle, align = 'start' }: Props = $props();\n</script>\n\n{#if reactions.length > 0}\n\t<div class=\"chips\" data-align={align}>\n\t\t{#each reactions as r (r.emoji)}\n\t\t\t{@const viewerReacted = viewerId !== null && r.userIds.includes(viewerId)}\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"chip\"\n\t\t\t\tclass:active={viewerReacted}\n\t\t\t\tonclick={() => onToggle(r.emoji)}\n\t\t\t\taria-label={t('{n} reacted with {emoji}', { n: r.count, emoji: r.emoji })}\n\t\t\t>\n\t\t\t\t<span class=\"emoji\">{r.emoji}</span>\n\t\t\t\t<span class=\"count\">{r.count}</span>\n\t\t\t</button>\n\t\t{/each}\n\t</div>\n{/if}\n\n<style>\n\t.chips {\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\tgap: 4px;\n\t\tmargin-top: 4px;\n\t}\n\t.chips[data-align='end'] { justify-content: flex-end; }\n\t.chip {\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tgap: 4px;\n\t\tbackground: var(--color-surface);\n\t\tborder: 1px solid var(--color-border);\n\t\tborder-radius: 999px;\n\t\tpadding: 2px 8px;\n\t\tfont-family: var(--font-body);\n\t\tfont-size: 11px;\n\t\tfont-weight: 600;\n\t\tcolor: var(--color-text);\n\t\tcursor: pointer;\n\t\tline-height: 1.4;\n\t\ttransition: background-color 0.12s ease-out, transform 0.12s ease-out;\n\t}\n\t.chip:hover { background: var(--color-bg-elevated, var(--color-surface)); }\n\t.chip:active { transform: scale(0.96); }\n\t.chip.active {\n\t\t/* Viewer already reacted — filled state, no glow (per CLAUDE.md).\n\t\t   The accent tint communicates ownership without crossing into\n\t\t   neon territory. */\n\t\tbackground: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface));\n\t\tborder-color: var(--color-accent);\n\t\tcolor: var(--color-text);\n\t}\n\t.emoji { font-size: 13px; line-height: 1; }\n\t.count { font-variant-numeric: tabular-nums; }\n</style>\n"],"version":3}