feat(01-13): layout shell with header, tabs, FAB, toast, data table

- Persistent header: clock countdown, level, blinds, player count (red pulse <10s, PAUSED/BREAK badges)
- Bottom tab bar (mobile): Overview, Players, Tables, Financials, More with 48px touch targets
- Desktop sidebar (>=768px): vertical nav replacing bottom tabs
- FAB: expandable quick actions (Bust, Buy In, Rebuy, Add-On, Pause/Resume) with backdrop
- Toast notification system: success/info/warning/error with auto-dismiss and stacking
- DataTable: sortable columns, sticky header, search/filter, mobile swipe actions, skeleton loading
- Multi-tournament tabs: horizontal scrollable selector when 2+ tournaments active
- Loading components: spinner (sm/md/lg), skeleton rows, full-page overlay
- Root layout: auth guard, responsive shell (mobile bottom tabs / desktop sidebar)
- Route pages: overview, players, tables, financials, more with placeholder content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikkel Georgsen 2026-03-01 04:13:17 +01:00
parent 51153df8dd
commit 7f91301efa
74 changed files with 2598 additions and 76 deletions

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
[data-theme=mocha],:root{--ctp-rosewater: #f5e0dc;--ctp-flamingo: #f2cdcd;--ctp-pink: #f5c2e7;--ctp-mauve: #cba6f7;--ctp-red: #f38ba8;--ctp-maroon: #eba0ac;--ctp-peach: #fab387;--ctp-yellow: #f9e2af;--ctp-green: #a6e3a1;--ctp-teal: #94e2d5;--ctp-sky: #89dceb;--ctp-sapphire: #74c7ec;--ctp-blue: #89b4fa;--ctp-lavender: #b4befe;--ctp-text: #cdd6f4;--ctp-subtext1: #bac2de;--ctp-subtext0: #a6adc8;--ctp-overlay2: #9399b2;--ctp-overlay1: #7f849c;--ctp-overlay0: #6c7086;--ctp-surface2: #585b70;--ctp-surface1: #45475a;--ctp-surface0: #313244;--ctp-base: #1e1e2e;--ctp-mantle: #181825;--ctp-crust: #11111b;--color-bg: var(--ctp-base);--color-bg-elevated: var(--ctp-mantle);--color-bg-sunken: var(--ctp-crust);--color-surface: var(--ctp-surface0);--color-surface-hover: var(--ctp-surface1);--color-surface-active: var(--ctp-surface2);--color-text: var(--ctp-text);--color-text-secondary: var(--ctp-subtext1);--color-text-muted: var(--ctp-subtext0);--color-primary: var(--ctp-blue);--color-success: var(--ctp-green);--color-warning: var(--ctp-yellow);--color-error: var(--ctp-red);--color-accent: var(--ctp-mauve);--color-border: var(--ctp-surface1);--color-overlay: var(--ctp-overlay0);--color-felt: var(--ctp-green);--color-card: var(--ctp-text);--color-bounty: var(--ctp-pink);--color-prize: var(--ctp-yellow);--color-chip: var(--ctp-peach);--color-clock: var(--ctp-sapphire);--color-break: var(--ctp-teal);--color-elimination: var(--ctp-red)}[data-theme=latte]{--ctp-rosewater: #dc8a78;--ctp-flamingo: #dd7878;--ctp-pink: #ea76cb;--ctp-mauve: #8839ef;--ctp-red: #d20f39;--ctp-maroon: #e64553;--ctp-peach: #fe640b;--ctp-yellow: #df8e1d;--ctp-green: #40a02b;--ctp-teal: #179299;--ctp-sky: #04a5e5;--ctp-sapphire: #209fb5;--ctp-blue: #1e66f5;--ctp-lavender: #7287fd;--ctp-text: #4c4f69;--ctp-subtext1: #5c5f77;--ctp-subtext0: #6c6f85;--ctp-overlay2: #7c7f93;--ctp-overlay1: #8c8fa1;--ctp-overlay0: #9ca0b0;--ctp-surface2: #acb0be;--ctp-surface1: #bcc0cc;--ctp-surface0: #ccd0da;--ctp-base: #eff1f5;--ctp-mantle: #e6e9ef;--ctp-crust: #dce0e8}:root{--font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--font-mono: "JetBrains Mono", "Fira Code", ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: 1rem;--text-lg: 1.125rem;--text-xl: 1.25rem;--text-2xl: 1.5rem;--text-3xl: 1.875rem;--text-4xl: 2.25rem;--leading-tight: 1.25;--leading-normal: 1.5;--leading-relaxed: 1.75;--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-6: 1.5rem;--space-8: 2rem;--space-12: 3rem;--radius-sm: .25rem;--radius-md: .5rem;--radius-lg: .75rem;--radius-xl: 1rem;--radius-full: 9999px;--shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);--shadow-md: 0 4px 6px rgba(0, 0, 0, .3);--shadow-lg: 0 10px 15px rgba(0, 0, 0, .3);--transition-fast: .1s ease;--transition-normal: .2s ease;--transition-slow: .3s ease;--touch-target: 48px}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-family:var(--font-body);font-size:16px;line-height:var(--leading-normal);color:var(--color-text);background-color:var(--color-bg);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;scrollbar-gutter:stable}body{min-height:100dvh;color:var(--color-text);background-color:var(--color-bg)}button,a,input,select,textarea,[role=button],[role=tab],[role=menuitem]{touch-action:manipulation}.touch-target,button,[role=button],[role=tab]{min-height:var(--touch-target);min-width:var(--touch-target)}button:active,[role=button]:active,[role=tab]:active,.touch-target:active{transform:scale(.97);opacity:.9;transition:transform var(--transition-fast),opacity var(--transition-fast)}:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}:focus:not(:focus-visible){outline:none}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--color-bg)}::-webkit-scrollbar-thumb{background:var(--color-surface-active);border-radius:var(--radius-full)}::-webkit-scrollbar-thumb:hover{background:var(--color-overlay)}*{scrollbar-width:thin;scrollbar-color:var(--color-surface-active) var(--color-bg)}.font-mono{font-family:var(--font-mono)}.timer,.number,.blinds,.chips,.currency{font-family:var(--font-mono);font-variant-numeric:tabular-nums}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

View file

@ -0,0 +1 @@
.redirect.svelte-1uha8ag{display:flex;align-items:center;justify-content:center;min-height:50dvh;color:var(--color-text-muted)}

View file

@ -1 +0,0 @@
.container.svelte-1uha8ag{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100dvh;padding:1rem;gap:.5rem}h1.svelte-1uha8ag{font-size:2rem;font-weight:700;color:var(--color-primary)}.text-secondary.svelte-1uha8ag{color:var(--color-text-secondary)}

View file

@ -0,0 +1 @@
.page-content.svelte-1ba4c5d{padding:var(--space-4)}h2.svelte-1ba4c5d{font-size:var(--text-2xl);font-weight:700;color:var(--color-text);margin-bottom:var(--space-2)}.text-secondary.svelte-1ba4c5d{color:var(--color-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-6)}.finance-grid.svelte-1ba4c5d{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-3)}@media(min-width:768px){.finance-grid.svelte-1ba4c5d{grid-template-columns:repeat(3,1fr)}}.finance-card.svelte-1ba4c5d{display:flex;flex-direction:column;gap:var(--space-1);padding:var(--space-4);background-color:var(--color-surface);border-radius:var(--radius-lg);border:1px solid var(--color-border)}.finance-card.highlight.svelte-1ba4c5d{border-color:var(--color-prize)}.finance-label.svelte-1ba4c5d{font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted)}.finance-value.svelte-1ba4c5d{font-size:var(--text-xl);font-weight:700;color:var(--color-text)}.finance-value.prize.svelte-1ba4c5d{color:var(--color-prize)}.empty-state.svelte-1ba4c5d{color:var(--color-text-muted);font-style:italic;padding:var(--space-8) 0;text-align:center}

View file

@ -0,0 +1 @@
.page-content.svelte-hq0atu{padding:var(--space-4)}h2.svelte-hq0atu{font-size:var(--text-2xl);font-weight:700;color:var(--color-text);margin-bottom:var(--space-2)}.text-secondary.svelte-hq0atu{color:var(--color-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-6)}.menu-list.svelte-hq0atu{display:flex;flex-direction:column;background-color:var(--color-surface);border-radius:var(--radius-lg);border:1px solid var(--color-border);overflow:hidden}.menu-item.svelte-hq0atu{display:flex;align-items:center;justify-content:space-between;padding:var(--space-4);min-height:var(--touch-target);border-bottom:1px solid var(--color-border)}.menu-item.svelte-hq0atu:last-child{border-bottom:none}.menu-action.svelte-hq0atu{background:none;border:none;border-bottom:1px solid var(--color-border);cursor:pointer;width:100%;text-align:left;font-size:inherit;font-family:inherit}.menu-action.svelte-hq0atu:hover{background-color:var(--color-surface-hover)}.menu-label.svelte-hq0atu{font-size:var(--text-base);color:var(--color-text)}.menu-value.svelte-hq0atu{font-size:var(--text-sm);color:var(--color-text-secondary)}.danger.svelte-hq0atu .menu-label:where(.svelte-hq0atu){color:var(--color-error)}.divider.svelte-hq0atu{border:none;border-top:1px solid var(--color-border);margin:0}

View file

@ -0,0 +1 @@
.page-content.svelte-14qseeg{padding:var(--space-4)}h2.svelte-14qseeg{font-size:var(--text-2xl);font-weight:700;color:var(--color-text);margin-bottom:var(--space-2)}.text-secondary.svelte-14qseeg{color:var(--color-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-6)}.stats-grid.svelte-14qseeg{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-3)}@media(min-width:768px){.stats-grid.svelte-14qseeg{grid-template-columns:repeat(4,1fr)}}.stat-card.svelte-14qseeg{display:flex;flex-direction:column;gap:var(--space-1);padding:var(--space-4);background-color:var(--color-surface);border-radius:var(--radius-lg);border:1px solid var(--color-border)}.stat-label.svelte-14qseeg{font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted)}.stat-value.svelte-14qseeg{font-size:var(--text-2xl);font-weight:700;color:var(--color-text)}.empty-state.svelte-14qseeg{color:var(--color-text-muted);font-style:italic;padding:var(--space-8) 0;text-align:center}

View file

@ -0,0 +1 @@
.page-content.svelte-wtkzqx{padding:var(--space-4)}h2.svelte-wtkzqx{font-size:var(--text-2xl);font-weight:700;color:var(--color-text);margin-bottom:var(--space-2)}.text-secondary.svelte-wtkzqx{color:var(--color-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-4)}

View file

@ -0,0 +1 @@
.page-content.svelte-bf0doe{padding:var(--space-4)}h2.svelte-bf0doe{font-size:var(--text-2xl);font-weight:700;color:var(--color-text);margin-bottom:var(--space-2)}.text-secondary.svelte-bf0doe{color:var(--color-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-4)}

View file

@ -0,0 +1 @@
.data-table-wrapper.svelte-16k18c8{width:100%;overflow:hidden}.table-search.svelte-16k18c8{padding:var(--space-3) 0}.search-input.svelte-16k18c8{width:100%;padding:var(--space-2) var(--space-3);font-size:var(--text-sm);color:var(--color-text);background-color:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);outline:none;transition:border-color var(--transition-fast);min-height:var(--touch-target)}.search-input.svelte-16k18c8:focus{border-color:var(--color-primary)}.search-input.svelte-16k18c8::placeholder{color:var(--color-text-muted)}.table-scroll.svelte-16k18c8{overflow-x:auto;-webkit-overflow-scrolling:touch}.data-table.svelte-16k18c8{width:100%;border-collapse:collapse;font-size:var(--text-sm)}.data-table.svelte-16k18c8 thead:where(.svelte-16k18c8){position:sticky;top:0;z-index:2}.data-table.svelte-16k18c8 th:where(.svelte-16k18c8){padding:var(--space-2) var(--space-3);font-weight:600;font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted);background-color:var(--color-bg-elevated);border-bottom:2px solid var(--color-border);white-space:nowrap;-webkit-user-select:none;user-select:none}.data-table.svelte-16k18c8 th.sortable:where(.svelte-16k18c8){cursor:pointer}.data-table.svelte-16k18c8 th.sortable:where(.svelte-16k18c8):hover{color:var(--color-text)}.th-content.svelte-16k18c8{display:inline-flex;align-items:center;gap:var(--space-1)}.sort-indicator.svelte-16k18c8{font-size:8px;color:var(--color-text-muted);opacity:.3}.sort-indicator.active.svelte-16k18c8{opacity:1;color:var(--color-primary)}.data-table.svelte-16k18c8 td:where(.svelte-16k18c8){padding:var(--space-2) var(--space-3);color:var(--color-text);border-bottom:1px solid var(--color-border);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}.data-row.svelte-16k18c8{min-height:var(--touch-target);transition:background-color var(--transition-fast)}.data-row.svelte-16k18c8:hover{background-color:var(--color-surface)}.data-row.clickable.svelte-16k18c8{cursor:pointer}.data-row.clickable.svelte-16k18c8:active{background-color:var(--color-surface-hover)}.empty-state.svelte-16k18c8{text-align:center;padding:var(--space-12) var(--space-4);color:var(--color-text-muted);font-style:italic}.skeleton-row.svelte-16k18c8 td:where(.svelte-16k18c8){padding:var(--space-3)}.skeleton-cell.svelte-16k18c8{height:16px;background:linear-gradient(90deg,var(--color-surface) 25%,var(--color-surface-hover) 50%,var(--color-surface) 75%);background-size:200% 100%;animation:svelte-16k18c8-skeleton-shimmer 1.5s ease-in-out infinite;border-radius:var(--radius-sm)}@keyframes svelte-16k18c8-skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.swipe-actions-row.svelte-16k18c8 td:where(.svelte-16k18c8){padding:0;border-bottom:none}.swipe-actions.svelte-16k18c8{display:flex;justify-content:flex-end;gap:var(--space-1);padding:var(--space-1);background-color:var(--color-bg-sunken)}.swipe-action-btn.svelte-16k18c8{padding:var(--space-2) var(--space-4);font-size:var(--text-sm);font-weight:600;color:#fff;border:none;border-radius:var(--radius-md);cursor:pointer;white-space:nowrap}.hide-mobile.svelte-16k18c8{display:none}@media(min-width:768px){.hide-mobile.svelte-16k18c8{display:table-cell}.data-table.svelte-16k18c8 td:where(.svelte-16k18c8){max-width:300px}}

View file

@ -1 +0,0 @@
import{N as p,m as u,O as c,P as f,Q as E,T as g,R as w,h as d,j as s,S as N,c as y,U as M,f as x,V as A}from"./Ym0WvvUy.js";var l;const i=((l=globalThis==null?void 0:globalThis.window)==null?void 0:l.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:t=>t});function L(t){return(i==null?void 0:i.createHTML(t))??t}function O(t){var r=p("template");return r.innerHTML=L(t.replaceAll("<!>","<!---->")),r.content}function n(t,r){var e=c;e.nodes===null&&(e.nodes={start:t,end:r,a:null,t:null})}function b(t,r){var e=(r&g)!==0,m=(r&w)!==0,a,v=!t.startsWith("<!>");return()=>{if(d)return n(s,null),s;a===void 0&&(a=O(v?t:"<!>"+t),e||(a=f(a)));var o=m||E?document.importNode(a,!0):a.cloneNode(!0);if(e){var T=f(o),h=o.lastChild;n(T,h)}else n(o,o);return o}}function C(t=""){if(!d){var r=u(t+"");return n(r,r),r}var e=s;return e.nodeType!==M?(e.before(e=u()),x(e)):A(e),n(e,e),e}function I(){if(d)return n(s,null),s;var t=document.createDocumentFragment(),r=document.createComment(""),e=u();return t.append(r,e),n(r,e),t}function S(t,r){if(d){var e=c;((e.f&N)===0||e.nodes.end===null)&&(e.nodes.end=s),y();return}t!==null&&t.before(r)}const P="5";var _;typeof window<"u"&&((_=window.__svelte??(window.__svelte={})).v??(_.v=new Set)).add(P);export{S as a,n as b,I as c,b as f,C as t};

View file

@ -1 +0,0 @@
import{b as T,h as o,c as b,E as v,r as p,H as A,d as E,e as R,f as g,i as l,j as m}from"./Ym0WvvUy.js";import{B as d}from"./Da6yQRl8.js";function H(_,u,c=!1){var i;o&&(i=m,b());var n=new d(_),h=c?v:0;function f(a,s){if(o){var r=p(i),e;if(r===A?e=0:r===E?e=!1:e=parseInt(r.substring(1)),a!==e){var t=R();g(t),n.anchor=t,l(!1),n.ensure(a,s),l(!0);return}}n.ensure(a,s)}T(()=>{var a=!1;u((s,r=0)=>{a=!0,f(r,s)}),a||f(!1,null)},h)}export{H as i};

View file

@ -0,0 +1 @@
import{t as b}from"./BeLKMLqR.js";import{h as c}from"./C4An0dnW.js";function A(i,u={},r,f){for(var a in r){var o=r[a];u[a]!==o&&(r[a]==null?i.style.removeProperty(a):i.style.setProperty(a,o,f))}}function t(i,u,r,f){var a=i.__style;if(c||a!==u){var o=b(u,f);(!c||o!==i.getAttribute("style"))&&(o==null?i.removeAttribute("style"):i.style.cssText=o),i.__style=u}else f&&(Array.isArray(f)?(A(i,r==null?void 0:r[0],f[0]),A(i,r==null?void 0:r[1],f[1],"important")):A(i,r,f));return f}export{t as s};

View file

@ -0,0 +1 @@
import{j as g,m as d,u as c,k as m,n as i,o as b,g as p,q as v,v as k,w as h}from"./C4An0dnW.js";function x(t=!1){const s=g,e=s.l.u;if(!e)return;let f=()=>v(s.s);if(t){let o=0,n={};const _=k(()=>{let l=!1;const r=s.s;for(const a in r)r[a]!==n[a]&&(n[a]=r[a],l=!0);return l&&o++,o});f=()=>p(_)}e.b.length&&d(()=>{u(s,f),i(e.b)}),c(()=>{const o=m(()=>e.m.map(b));return()=>{for(const n of o)typeof n=="function"&&n()}}),e.a.length&&c(()=>{u(s,f),i(e.a)})}function u(t,s){if(t.l.s)for(const e of t.l.s)p(e);s()}h();export{x as i};

View file

@ -0,0 +1,2 @@
import{K as U,T as tr,a5 as sr,h as A,_ as Y,a6 as vr,U as cr,g as j,W as dr,Y as gr,Z as x,$ as q,O as z,a7 as hr,a8 as pr,a9 as y,N as _r,aa as I,M as m,ab as Er,R as Ar,i as Tr,ac as Nr,ad as V,ae as Sr,af as Ir,ag as br,ah as rr,ai as Cr,H as ur,J as lr,aj as B,ak as or,al as Mr,am as Or,an as Lr,I as wr,ao as Hr,ap as Rr,aq as kr,ar as Dr,as as Fr,at as zr,au as Ur}from"./C4An0dnW.js";function Wr(r,e){return e}function Yr(r,e,f){for(var a=[],u=e.length,n,s=e.length,c=0;c<u;c++){let g=e[c];lr(g,()=>{if(n){if(n.pending.delete(g),n.done.add(g),n.pending.size===0){var t=r.outrogroups;G(V(n.done)),t.delete(n),t.size===0&&(r.outrogroups=null)}}else s-=1},!1)}if(s===0){var l=a.length===0&&f!==null;if(l){var d=f,o=d.parentNode;Lr(o),o.append(d),r.items.clear()}G(e,!l)}else n={pending:new Set(e),done:new Set},(r.outrogroups??(r.outrogroups=new Set)).add(n)}function G(r,e=!0){for(var f=0;f<r.length;f++)wr(r[f],e)}var er;function Zr(r,e,f,a,u,n=null){var s=r,c=new Map,l=(e&sr)!==0;if(l){var d=r;s=A?Y(vr(d)):d.appendChild(U())}A&&cr();var o=null,g=Tr(()=>{var v=f();return Nr(v)?v:v==null?[]:V(v)}),t,h=!0;function T(){i.fallback=o,qr(i,t,s,e,a),o!==null&&(t.length===0?(o.f&I)===0?ur(o):(o.f^=I,k(o,null,s)):lr(o,()=>{o=null}))}var N=tr(()=>{t=j(g);var v=t.length;let O=!1;if(A){var L=dr(s)===gr;L!==(v===0)&&(s=x(),Y(s),q(!1),O=!0)}for(var _=new Set,C=_r,w=Ar(),p=0;p<v;p+=1){A&&z.nodeType===hr&&z.data===pr&&(s=z,O=!0,q(!1));var M=t[p],H=a(M,p),E=h?null:c.get(H);E?(E.v&&y(E.v,M),E.i&&y(E.i,p),w&&C.unskip_effect(E.e)):(E=Br(c,h?s:er??(er=U()),M,H,p,u,e,f),h||(E.e.f|=I),c.set(H,E)),_.add(H)}if(v===0&&n&&!o&&(h?o=m(()=>n(s)):(o=m(()=>n(er??(er=U()))),o.f|=I)),v>_.size&&Er(),A&&v>0&&Y(x()),!h)if(w){for(const[D,F]of c)_.has(D)||C.skip_effect(F.e);C.oncommit(T),C.ondiscard(()=>{})}else T();O&&q(!0),j(g)}),i={effect:N,items:c,outrogroups:null,fallback:o};h=!1,A&&(s=z)}function R(r){for(;r!==null&&(r.f&Mr)===0;)r=r.next;return r}function qr(r,e,f,a,u){var E,D,F,X,J,P,W,Z,$;var n=(a&Or)!==0,s=e.length,c=r.items,l=R(r.effect.first),d,o=null,g,t=[],h=[],T,N,i,v;if(n)for(v=0;v<s;v+=1)T=e[v],N=u(T,v),i=c.get(N).e,(i.f&I)===0&&((D=(E=i.nodes)==null?void 0:E.a)==null||D.measure(),(g??(g=new Set)).add(i));for(v=0;v<s;v+=1){if(T=e[v],N=u(T,v),i=c.get(N).e,r.outrogroups!==null)for(const S of r.outrogroups)S.pending.delete(i),S.done.delete(i);if((i.f&I)!==0)if(i.f^=I,i===l)k(i,null,f);else{var O=o?o.next:l;i===r.effect.last&&(r.effect.last=i.prev),i.prev&&(i.prev.next=i.next),i.next&&(i.next.prev=i.prev),b(r,o,i),b(r,i,O),k(i,O,f),o=i,t=[],h=[],l=R(o.next);continue}if((i.f&B)!==0&&(ur(i),n&&((X=(F=i.nodes)==null?void 0:F.a)==null||X.unfix(),(g??(g=new Set)).delete(i))),i!==l){if(d!==void 0&&d.has(i)){if(t.length<h.length){var L=h[0],_;o=L.prev;var C=t[0],w=t[t.length-1];for(_=0;_<t.length;_+=1)k(t[_],L,f);for(_=0;_<h.length;_+=1)d.delete(h[_]);b(r,C.prev,w.next),b(r,o,C),b(r,w,L),l=L,o=w,v-=1,t=[],h=[]}else d.delete(i),k(i,l,f),b(r,i.prev,i.next),b(r,i,o===null?r.effect.first:o.next),b(r,o,i),o=i;continue}for(t=[],h=[];l!==null&&l!==i;)(d??(d=new Set)).add(l),h.push(l),l=R(l.next);if(l===null)continue}(i.f&I)===0&&t.push(i),o=i,l=R(i.next)}if(r.outrogroups!==null){for(const S of r.outrogroups)S.pending.size===0&&(G(V(S.done)),(J=r.outrogroups)==null||J.delete(S));r.outrogroups.size===0&&(r.outrogroups=null)}if(l!==null||d!==void 0){var p=[];if(d!==void 0)for(i of d)(i.f&B)===0&&p.push(i);for(;l!==null;)(l.f&B)===0&&l!==r.fallback&&p.push(l),l=R(l.next);var M=p.length;if(M>0){var H=(a&sr)!==0&&s===0?f:null;if(n){for(v=0;v<M;v+=1)(W=(P=p[v].nodes)==null?void 0:P.a)==null||W.measure();for(v=0;v<M;v+=1)($=(Z=p[v].nodes)==null?void 0:Z.a)==null||$.fix()}Yr(r,p,H)}}n&&or(()=>{var S,Q;if(g!==void 0)for(i of g)(Q=(S=i.nodes)==null?void 0:S.a)==null||Q.apply()})}function Br(r,e,f,a,u,n,s,c){var l=(s&Sr)!==0?(s&Ir)===0?br(f,!1,!1):rr(f):null,d=(s&Cr)!==0?rr(u):null;return{v:l,i:d,e:m(()=>(n(e,l??f,d??u,c),()=>{r.delete(a)}))}}function k(r,e,f){if(r.nodes)for(var a=r.nodes.start,u=r.nodes.end,n=e&&(e.f&I)===0?e.nodes.start:f;a!==null;){var s=Hr(a);if(n.before(a),a===u)return;a=s}}function b(r,e,f){e===null?r.effect.first=f:e.next=f,f===null?r.effect.last=e:f.prev=e}const fr=[...`
\r\f \v\uFEFF`];function Kr(r,e,f){var a=r==null?"":""+r;if(e&&(a=a?a+" "+e:e),f){for(var u of Object.keys(f))if(f[u])a=a?a+" "+u:u;else if(a.length)for(var n=u.length,s=0;(s=a.indexOf(u,s))>=0;){var c=s+n;(s===0||fr.includes(a[s-1]))&&(c===a.length||fr.includes(a[c]))?a=(s===0?"":a.substring(0,s))+a.substring(c+1):s=c}}return a===""?null:a}function ar(r,e=!1){var f=e?" !important;":";",a="";for(var u of Object.keys(r)){var n=r[u];n!=null&&n!==""&&(a+=" "+u+": "+n+f)}return a}function K(r){return r[0]!=="-"||r[1]!=="-"?r.toLowerCase():r}function $r(r,e){if(e){var f="",a,u;if(Array.isArray(e)?(a=e[0],u=e[1]):a=e,r){r=String(r).replaceAll(/\s*\/\*.*?\*\/\s*/g,"").trim();var n=!1,s=0,c=!1,l=[];a&&l.push(...Object.keys(a).map(K)),u&&l.push(...Object.keys(u).map(K));var d=0,o=-1;const N=r.length;for(var g=0;g<N;g++){var t=r[g];if(c?t==="/"&&r[g-1]==="*"&&(c=!1):n?n===t&&(n=!1):t==="/"&&r[g+1]==="*"?c=!0:t==='"'||t==="'"?n=t:t==="("?s++:t===")"&&s--,!c&&n===!1&&s===0){if(t===":"&&o===-1)o=g;else if(t===";"||g===N-1){if(o!==-1){var h=K(r.substring(d,o).trim());if(!l.includes(h)){t!==";"&&g++;var T=r.substring(d,g).trim();f+=" "+T+";"}}d=g+1,o=-1}}}}return a&&(f+=ar(a)),u&&(f+=ar(u,!0)),f=f.trim(),f===""?null:f}return r==null?null:String(r)}function Qr(r,e,f,a,u,n){var s=r.__className;if(A||s!==f||s===void 0){var c=Kr(f,a,n);(!A||c!==r.getAttribute("class"))&&(c==null?r.removeAttribute("class"):r.className=c),r.__className=f}else if(n&&u!==n)for(var l in n){var d=!!n[l];(u==null||d!==!!u[l])&&r.classList.toggle(l,d)}return n}const mr=Symbol("is custom element"),Gr=Symbol("is html"),Vr=Ur?"link":"LINK";function jr(r){if(A){var e=!1,f=()=>{if(!e){if(e=!0,r.hasAttribute("value")){var a=r.value;ir(r,"value",null),r.value=a}if(r.hasAttribute("checked")){var u=r.checked;ir(r,"checked",null),r.checked=u}}};r.__on_r=f,or(f),Fr()}}function ir(r,e,f,a){var u=Xr(r);A&&(u[e]=r.getAttribute(e),e==="src"||e==="srcset"||e==="href"&&r.nodeName===Vr)||u[e]!==(u[e]=f)&&(e==="loading"&&(r[zr]=f),f==null?r.removeAttribute(e):typeof f!="string"&&Jr(r).includes(e)?r[e]=f:r.setAttribute(e,f))}function Xr(r){return r.__attributes??(r.__attributes={[mr]:r.nodeName.includes("-"),[Gr]:r.namespaceURI===Rr})}var nr=new Map;function Jr(r){var e=r.getAttribute("is")||r.nodeName,f=nr.get(e);if(f)return f;nr.set(e,f=[]);for(var a,u=r,n=Element.prototype;n!==u;){a=Dr(u);for(var s in a)a[s].set&&f.push(s);u=kr(u)}return f}export{Qr as a,Zr as e,Wr as i,jr as r,ir as s,$r as t};

View file

@ -0,0 +1 @@
import{y as D,z as T,P as B,g,e as m,d as Y,A as y,B as M,D as N,C as U,k as h,l as x,E as z,F as C,v as G,i as $,G as q,S as w,L as F}from"./C4An0dnW.js";let S=!1;function Z(r){var n=S;try{return S=!1,[r(),S]}finally{S=n}}function H(r,n,t,d){var E;var f=!x||(t&z)!==0,v=(t&U)!==0,O=(t&q)!==0,a=d,c=!0,o=()=>(c&&(c=!1,a=O?h(d):d),a),u;if(v){var R=w in r||F in r;u=((E=D(r,n))==null?void 0:E.set)??(R&&n in r?e=>r[n]=e:void 0)}var _,I=!1;v?[_,I]=Z(()=>r[n]):_=r[n],_===void 0&&d!==void 0&&(_=o(),u&&(f&&T(),u(_)));var i;if(f?i=()=>{var e=r[n];return e===void 0?o():(c=!0,e)}:i=()=>{var e=r[n];return e!==void 0&&(a=void 0),e===void 0?a:e},f&&(t&B)===0)return i;if(u){var L=r.$$legacy;return(function(e,l){return arguments.length>0?((!f||!l||L||I)&&u(l?i():e),e):i()})}var P=!1,s=((t&C)!==0?G:$)(()=>(P=!1,i()));v&&g(s);var b=M;return(function(e,l){if(arguments.length>0){const A=l?g(s):f&&v?m(e):e;return Y(s,A),P=!0,a!==void 0&&(a=A),e}return y&&P||(b.f&N)!==0?s.v:g(s)})}export{H as p};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
var S=Object.defineProperty;var v=e=>{throw TypeError(e)};var O=(e,t,a)=>t in e?S(e,t,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[t]=a;var g=(e,t,a)=>O(e,typeof t!="symbol"?t+"":t,a),U=(e,t,a)=>t.has(e)||v("Cannot "+a);var s=(e,t,a)=>(U(e,t,"read from private field"),a?a.call(e):t.get(e)),i=(e,t,a)=>t.has(e)?v("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,a);import{b as l,g as r,d as c,e as p}from"./C4An0dnW.js";var n,d,h,u,b,k,y,o;class x{constructor(){i(this,n,l(null));i(this,d,l(null));i(this,h,l(p([])));i(this,u,l(p([])));i(this,b,l(null));i(this,k,l(p([])));i(this,y,l(p([])));i(this,o,l(null));g(this,"maxActivityEntries",100)}get id(){return r(s(this,n))}set id(t){c(s(this,n),t,!0)}get clock(){return r(s(this,d))}set clock(t){c(s(this,d),t,!0)}get players(){return r(s(this,h))}set players(t){c(s(this,h),t,!0)}get tables(){return r(s(this,u))}set tables(t){c(s(this,u),t,!0)}get financials(){return r(s(this,b))}set financials(t){c(s(this,b),t,!0)}get activity(){return r(s(this,k))}set activity(t){c(s(this,k),t,!0)}get rankings(){return r(s(this,y))}set rankings(t){c(s(this,y),t,!0)}get balanceStatus(){return r(s(this,o))}set balanceStatus(t){c(s(this,o),t,!0)}get remainingPlayers(){return this.players.filter(t=>t.status==="active").length}get totalPlayers(){return this.players.length}get activeTables(){return this.tables.filter(t=>t.players.length>0).length}get isBalanced(){var t;return((t=this.balanceStatus)==null?void 0:t.is_balanced)??!0}handleMessage(t){switch(t.type){case"clock.tick":this.clock=t.data;break;case"clock.level_change":this.clock=t.data;break;case"clock.paused":this.clock&&(this.clock.is_paused=!0);break;case"clock.resumed":this.clock&&(this.clock.is_paused=!1);break;case"state.snapshot":this.loadFullState(t.data);break;case"player.registered":this.addOrUpdatePlayer(t.data);break;case"player.seated":this.addOrUpdatePlayer(t.data);break;case"player.bust":case"player.eliminated":this.addOrUpdatePlayer(t.data);break;case"player.rebuy":case"player.addon":this.addOrUpdatePlayer(t.data);break;case"player.moved":this.addOrUpdatePlayer(t.data);break;case"table.created":this.addOrUpdateTable(t.data);break;case"table.broken":this.removeTable(t.data.id);break;case"table.updated":this.addOrUpdateTable(t.data);break;case"financial.updated":this.financials=t.data;break;case"rankings.updated":this.rankings=t.data;break;case"balance.updated":this.balanceStatus=t.data;break;case"activity.new":this.addActivity(t.data);break;case"connected":console.log("tournament: connected to server");break;default:console.warn(`tournament: unknown message type: ${t.type}`)}}reset(){this.id=null,this.clock=null,this.players=[],this.tables=[],this.financials=null,this.activity=[],this.rankings=[],this.balanceStatus=null}loadFullState(t){this.id=t.id??this.id,this.clock=t.clock??null,this.players=t.players??[],this.tables=t.tables??[],this.financials=t.financials??null,this.activity=t.activity??[],this.rankings=t.rankings??[],this.balanceStatus=t.balance_status??null}addOrUpdatePlayer(t){const a=this.players.findIndex(f=>f.id===t.id);a>=0?this.players[a]=t:this.players.push(t)}addOrUpdateTable(t){const a=this.tables.findIndex(f=>f.id===t.id);a>=0?this.tables[a]=t:this.tables.push(t)}removeTable(t){this.tables=this.tables.filter(a=>a.id!==t)}addActivity(t){this.activity=[t,...this.activity].slice(0,this.maxActivityEntries)}}n=new WeakMap,d=new WeakMap,h=new WeakMap,u=new WeakMap,b=new WeakMap,k=new WeakMap,y=new WeakMap,o=new WeakMap;const w=new x;export{w as t};

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
var h=e=>{throw TypeError(e)};var m=(e,t,o)=>t.has(e)||h("Cannot "+o);var r=(e,t,o)=>(m(e,t,"read from private field"),o?o.call(e):t.get(e)),l=(e,t,o)=>t.has(e)?h("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,o);import{s as c,g,a as u}from"./Ym0WvvUy.js";const n="felt_token",i="felt_operator";var a,s;class p{constructor(){l(this,a,c(null));l(this,s,c(null));typeof window<"u"&&this.loadFromStorage()}get token(){return g(r(this,a))}set token(t){u(r(this,a),t,!0)}get operator(){return g(r(this,s))}set operator(t){u(r(this,s),t,!0)}get isAuthenticated(){return this.token!==null}get isAdmin(){var t;return((t=this.operator)==null?void 0:t.role)==="admin"}get isFloor(){var t;return["admin","floor"].includes(((t=this.operator)==null?void 0:t.role)??"")}login(t,o){this.token=t,this.operator=o,this.saveToStorage()}logout(){this.token=null,this.operator=null,this.clearStorage()}loadFromStorage(){try{const t=localStorage.getItem(n),o=localStorage.getItem(i);t&&o&&(this.token=t,this.operator=JSON.parse(o))}catch(t){console.warn("auth: failed to load from storage:",t),this.clearStorage()}}saveToStorage(){try{this.token&&this.operator&&(localStorage.setItem(n,this.token),localStorage.setItem(i,JSON.stringify(this.operator)))}catch(t){console.warn("auth: failed to save to storage:",t)}}clearStorage(){try{localStorage.removeItem(n),localStorage.removeItem(i)}catch(t){console.warn("auth: failed to clear storage:",t)}}}a=new WeakMap,s=new WeakMap;const f=new p;export{f as a};
var h=e=>{throw TypeError(e)};var m=(e,t,o)=>t.has(e)||h("Cannot "+o);var r=(e,t,o)=>(m(e,t,"read from private field"),o?o.call(e):t.get(e)),l=(e,t,o)=>t.has(e)?h("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,o);import{b as c,g,d as u}from"./C4An0dnW.js";const n="felt_token",i="felt_operator";var a,s;class p{constructor(){l(this,a,c(null));l(this,s,c(null));typeof window<"u"&&this.loadFromStorage()}get token(){return g(r(this,a))}set token(t){u(r(this,a),t,!0)}get operator(){return g(r(this,s))}set operator(t){u(r(this,s),t,!0)}get isAuthenticated(){return this.token!==null}get isAdmin(){var t;return((t=this.operator)==null?void 0:t.role)==="admin"}get isFloor(){var t;return["admin","floor"].includes(((t=this.operator)==null?void 0:t.role)??"")}login(t,o){this.token=t,this.operator=o,this.saveToStorage()}logout(){this.token=null,this.operator=null,this.clearStorage()}loadFromStorage(){try{const t=localStorage.getItem(n),o=localStorage.getItem(i);t&&o&&(this.token=t,this.operator=JSON.parse(o))}catch(t){console.warn("auth: failed to load from storage:",t),this.clearStorage()}}saveToStorage(){try{this.token&&this.operator&&(localStorage.setItem(n,this.token),localStorage.setItem(i,JSON.stringify(this.operator)))}catch(t){console.warn("auth: failed to save to storage:",t)}}clearStorage(){try{localStorage.removeItem(n),localStorage.removeItem(i)}catch(t){console.warn("auth: failed to clear storage:",t)}}}a=new WeakMap,s=new WeakMap;const f=new p;export{f as a};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
var I=Object.defineProperty;var R=a=>{throw TypeError(a)};var x=(a,e,t)=>e in a?I(a,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[e]=t;var w=(a,e,t)=>x(a,typeof e!="symbol"?e+"":e,t),E=(a,e,t)=>e.has(a)||R("Cannot "+t);var s=(a,e,t)=>(E(a,e,"read from private field"),t?t.call(a):e.get(a)),_=(a,e,t)=>e.has(a)?R("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(a):e.set(a,t),M=(a,e,t,i)=>(E(a,e,"write to private field"),i?i.call(a,t):e.set(a,t),t);import{H,I as T,J as O,K as N,M as S,N as Y,h as A,O as F,Q as B,R as C,T as J,U as K,V as L,W as P,X as Q,Y as U,Z as V,_ as W,$ as D}from"./C4An0dnW.js";var l,u,h,p,v,m,g;class X{constructor(e,t=!0){w(this,"anchor");_(this,l,new Map);_(this,u,new Map);_(this,h,new Map);_(this,p,new Set);_(this,v,!0);_(this,m,e=>{if(s(this,l).has(e)){var t=s(this,l).get(e),i=s(this,u).get(t);if(i)H(i),s(this,p).delete(t);else{var n=s(this,h).get(t);n&&(s(this,u).set(t,n.effect),s(this,h).delete(t),n.fragment.lastChild.remove(),this.anchor.before(n.fragment),i=n.effect)}for(const[f,c]of s(this,l)){if(s(this,l).delete(f),f===e)break;const r=s(this,h).get(c);r&&(T(r.effect),s(this,h).delete(c))}for(const[f,c]of s(this,u)){if(f===t||s(this,p).has(f))continue;const r=()=>{if(Array.from(s(this,l).values()).includes(f)){var d=document.createDocumentFragment();B(c,d),d.append(N()),s(this,h).set(f,{effect:c,fragment:d})}else T(c);s(this,p).delete(f),s(this,u).delete(f)};s(this,v)||!i?(s(this,p).add(f),O(c,r,!1)):r()}}});_(this,g,e=>{s(this,l).delete(e);const t=Array.from(s(this,l).values());for(const[i,n]of s(this,h))t.includes(i)||(T(n.effect),s(this,h).delete(i))});this.anchor=e,M(this,v,t)}ensure(e,t){var i=Y,n=C();if(t&&!s(this,u).has(e)&&!s(this,h).has(e))if(n){var f=document.createDocumentFragment(),c=N();f.append(c),s(this,h).set(e,{effect:S(()=>t(c)),fragment:f})}else s(this,u).set(e,S(()=>t(this.anchor)));if(s(this,l).set(i,e),n){for(const[r,o]of s(this,u))r===e?i.unskip_effect(o):i.skip_effect(o);for(const[r,o]of s(this,h))r===e?i.unskip_effect(o.effect):i.skip_effect(o.effect);i.oncommit(s(this,m)),i.ondiscard(s(this,g))}else A&&(this.anchor=F),s(this,m).call(this,i)}}l=new WeakMap,u=new WeakMap,h=new WeakMap,p=new WeakMap,v=new WeakMap,m=new WeakMap,g=new WeakMap;function j(a,e,t=!1){var i;A&&(i=F,K());var n=new X(a),f=t?L:0;function c(r,o){if(A){var d=P(i),b;if(d===Q?b=0:d===U?b=!1:b=parseInt(d.substring(1)),r!==b){var k=V();W(k),n.anchor=k,D(!1),n.ensure(r,o),D(!0);return}}n.ensure(r,o)}J(()=>{var r=!1;e((o,d=0)=>{r=!0,c(d,o)}),r||c(!1,null)},f)}export{X as B,j as i};

View file

@ -1 +0,0 @@
var B=Object.defineProperty;var g=i=>{throw TypeError(i)};var D=(i,e,s)=>e in i?B(i,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):i[e]=s;var w=(i,e,s)=>D(i,typeof e!="symbol"?e+"":e,s),y=(i,e,s)=>e.has(i)||g("Cannot "+s);var t=(i,e,s)=>(y(i,e,"read from private field"),s?s.call(i):e.get(i)),l=(i,e,s)=>e.has(i)?g("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,s),M=(i,e,s,a)=>(y(i,e,"write to private field"),a?a.call(i,s):e.set(i,s),s);import{k as F,l as b,p as j,m as x,n as A,o as q,h as C,j as S,q as z,t as E}from"./Ym0WvvUy.js";var h,n,r,u,p,_,v;class I{constructor(e,s=!0){w(this,"anchor");l(this,h,new Map);l(this,n,new Map);l(this,r,new Map);l(this,u,new Set);l(this,p,!0);l(this,_,e=>{if(t(this,h).has(e)){var s=t(this,h).get(e),a=t(this,n).get(s);if(a)F(a),t(this,u).delete(s);else{var c=t(this,r).get(s);c&&(t(this,n).set(s,c.effect),t(this,r).delete(s),c.fragment.lastChild.remove(),this.anchor.before(c.fragment),a=c.effect)}for(const[f,o]of t(this,h)){if(t(this,h).delete(f),f===e)break;const d=t(this,r).get(o);d&&(b(d.effect),t(this,r).delete(o))}for(const[f,o]of t(this,n)){if(f===s||t(this,u).has(f))continue;const d=()=>{if(Array.from(t(this,h).values()).includes(f)){var k=document.createDocumentFragment();z(o,k),k.append(x()),t(this,r).set(f,{effect:o,fragment:k})}else b(o);t(this,u).delete(f),t(this,n).delete(f)};t(this,p)||!a?(t(this,u).add(f),j(o,d,!1)):d()}}});l(this,v,e=>{t(this,h).delete(e);const s=Array.from(t(this,h).values());for(const[a,c]of t(this,r))s.includes(a)||(b(c.effect),t(this,r).delete(a))});this.anchor=e,M(this,p,s)}ensure(e,s){var a=q,c=E();if(s&&!t(this,n).has(e)&&!t(this,r).has(e))if(c){var f=document.createDocumentFragment(),o=x();f.append(o),t(this,r).set(e,{effect:A(()=>s(o)),fragment:f})}else t(this,n).set(e,A(()=>s(this.anchor)));if(t(this,h).set(a,e),c){for(const[d,m]of t(this,n))d===e?a.unskip_effect(m):a.skip_effect(m);for(const[d,m]of t(this,r))d===e?a.unskip_effect(m.effect):a.skip_effect(m.effect);a.oncommit(t(this,_)),a.ondiscard(t(this,v))}else C&&(this.anchor=S),t(this,_).call(this,a)}}h=new WeakMap,n=new WeakMap,r=new WeakMap,u=new WeakMap,p=new WeakMap,_=new WeakMap,v=new WeakMap;export{I as B};

View file

@ -1 +0,0 @@
import{B as g,C as d,D as c,F as m,G as i,I as b,g as p,J as v,K as h,L as k}from"./Ym0WvvUy.js";function x(n=!1){const s=g,e=s.l.u;if(!e)return;let f=()=>v(s.s);if(n){let a=0,t={};const _=h(()=>{let l=!1;const r=s.s;for(const o in r)r[o]!==t[o]&&(t[o]=r[o],l=!0);return l&&a++,a});f=()=>p(_)}e.b.length&&d(()=>{u(s,f),i(e.b)}),c(()=>{const a=m(()=>e.m.map(b));return()=>{for(const t of a)typeof t=="function"&&t()}}),e.a.length&&c(()=>{u(s,f),i(e.a)})}function u(n,s){if(n.l.s)for(const e of n.l.s)p(e);s()}k();export{x as i};

View file

@ -0,0 +1 @@
import{s as e,p as r}from"./DQNCp18R.js";const t={get error(){return r.error},get status(){return r.status},get url(){return r.url}};e.updated.check;const a=t;export{a as p};

View file

@ -0,0 +1 @@
import{av as p,K as u,a6 as l,aw as E,B as c,ax as w,ay as g,h as d,O as s,az as y,U as N,aA as x,_ as A,aB as M}from"./C4An0dnW.js";var f;const i=((f=globalThis==null?void 0:globalThis.window)==null?void 0:f.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:t=>t});function L(t){return(i==null?void 0:i.createHTML(t))??t}function O(t){var r=p("template");return r.innerHTML=L(t.replaceAll("<!>","<!---->")),r.content}function n(t,r){var e=c;e.nodes===null&&(e.nodes={start:t,end:r,a:null,t:null})}function R(t,r){var e=(r&w)!==0,m=(r&g)!==0,a,v=!t.startsWith("<!>");return()=>{if(d)return n(s,null),s;a===void 0&&(a=O(v?t:"<!>"+t),e||(a=l(a)));var o=m||E?document.importNode(a,!0):a.cloneNode(!0);if(e){var T=l(o),h=o.lastChild;n(T,h)}else n(o,o);return o}}function C(t=""){if(!d){var r=u(t+"");return n(r,r),r}var e=s;return e.nodeType!==x?(e.before(e=u()),A(e)):M(e),n(e,e),e}function I(){if(d)return n(s,null),s;var t=document.createDocumentFragment(),r=document.createComment(""),e=u();return t.append(r,e),n(r,e),t}function B(t,r){if(d){var e=c;((e.f&y)===0||e.nodes.end===null)&&(e.nodes.end=s),N();return}t!==null&&t.before(r)}const b="5";var _;typeof window<"u"&&((_=window.__svelte??(window.__svelte={})).v??(_.v=new Set)).add(b);export{B as a,n as b,I as c,R as f,C as t};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
import{D as o,B as t,M as c,F as u}from"./Ym0WvvUy.js";function l(n){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function r(n){t===null&&l(),c&&t.l!==null?a(t).m.push(n):o(()=>{const e=u(n);if(typeof e=="function")return e})}function a(n){var e=n.l;return e.u??(e.u={a:[],b:[],m:[]})}export{r as o};
import{u as o,j as t,l as c,k as u}from"./C4An0dnW.js";function l(n){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function r(n){t===null&&l(),c&&t.l!==null?a(t).m.push(n):o(()=>{const e=u(n);if(typeof e=="function")return e})}function a(n){var e=n.l;return e.u??(e.u={a:[],b:[],m:[]})}export{r as o};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
import{l as o,a as r}from"../chunks/giww_vF6.js";export{o as load_css,r as start};

View file

@ -0,0 +1 @@
import{l as o,a as r}from"../chunks/DQNCp18R.js";export{o as load_css,r as start};

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
import{c as s,a as c}from"../chunks/B6M6q2Zo.js";import{b as l,E as p,v as i}from"../chunks/Ym0WvvUy.js";import{B as m}from"../chunks/Da6yQRl8.js";function u(n,r,...e){var o=new m(n);l(()=>{const t=r()??null;o.ensure(t,t&&(a=>t(a,...e)))},p)}const f=!0,_=!1,g=Object.freeze(Object.defineProperty({__proto__:null,prerender:f,ssr:_},Symbol.toStringTag,{value:"Module"}));function h(n,r){var e=s(),o=i(e);u(o,()=>r.children),c(n,e)}export{h as component,g as universal};

View file

@ -0,0 +1 @@
import{a as c,f as l}from"../chunks/Q5CB4WY5.js";import{i as v}from"../chunks/BViIIwgj.js";import{p as u,f as _,t as g,a as x,c as e,r as o,s as d}from"../chunks/C4An0dnW.js";import{s as p}from"../chunks/CQQh_IlD.js";import{p as m}from"../chunks/DyXP65qD.js";var b=l("<h1> </h1> <p> </p>",1);function y(f,i){u(i,!1),v();var t=b(),r=_(t),n=e(r,!0);o(r);var a=d(r,2),h=e(a,!0);o(a),g(()=>{var s;p(n,m.status),p(h,(s=m.error)==null?void 0:s.message)}),c(f,t),x()}export{y as component};

View file

@ -1 +0,0 @@
import{a as h,f as g}from"../chunks/B6M6q2Zo.js";import{i as v}from"../chunks/De6rLmuB.js";import{u as l,v as d,A as x,w as _,y as e,z as o,x as $}from"../chunks/Ym0WvvUy.js";import{s as p}from"../chunks/dTRRgeF-.js";import{s as k,p as m}from"../chunks/giww_vF6.js";const b={get error(){return m.error},get status(){return m.status}};k.updated.check;const i=b;var w=g("<h1> </h1> <p> </p>",1);function q(f,n){l(n,!1),v();var t=w(),r=d(t),c=e(r,!0);o(r);var s=$(r,2),u=e(s,!0);o(s),x(()=>{var a;p(c,i.status),p(u,(a=i.error)==null?void 0:a.message)}),h(f,t),_()}export{q as component};

View file

@ -0,0 +1 @@
import{a as t,f as p}from"../chunks/Q5CB4WY5.js";import{i as e}from"../chunks/BViIIwgj.js";import{o as i}from"../chunks/nIaoZoCo.js";import{p as m,a as s}from"../chunks/C4An0dnW.js";import{g as f}from"../chunks/DQNCp18R.js";var n=p('<div class="redirect svelte-1uha8ag"><p>Loading...</p></div>');function u(o,a){m(a,!1),i(()=>{f("/overview",{replaceState:!0})}),e();var r=n();t(o,r),s()}export{u as component};

View file

@ -1 +0,0 @@
import{c as u,a as e,f as p}from"../chunks/B6M6q2Zo.js";import{i as _}from"../chunks/De6rLmuB.js";import{o as d}from"../chunks/Bfwrz3i4.js";import{u as x,v as y,w as A,x as b,y as m,z as n,A as k}from"../chunks/Ym0WvvUy.js";import{s as w}from"../chunks/dTRRgeF-.js";import{i as z}from"../chunks/B9dvBo0E.js";import{a as o}from"../chunks/DMqvp7vx.js";import{g as F}from"../chunks/giww_vF6.js";var M=p('<main class="container svelte-1uha8ag"><h1 class="svelte-1uha8ag">Felt</h1> <p class="text-secondary svelte-1uha8ag">Tournament management system</p> <p> </p></main>'),O=p('<main class="container svelte-1uha8ag"><p>Redirecting to login...</p></main>');function E(l,f){x(f,!1),d(()=>{o.isAuthenticated||F("/login")}),_();var s=u(),c=y(s);{var v=a=>{var t=M(),r=b(m(t),4),h=m(r);n(r),n(t),k(()=>{var i;return w(h,`Welcome, ${((i=o.operator)==null?void 0:i.name)??"Operator"??""}`)}),e(a,t)},g=a=>{var t=O();e(a,t)};z(c,a=>{o.isAuthenticated?a(v):a(g,!1)})}e(l,s),A()}export{E as component};

View file

@ -0,0 +1 @@
import{a as b,f as _}from"../chunks/Q5CB4WY5.js";import{i as G}from"../chunks/BViIIwgj.js";import{p as I,a as J,s,c as a,r as e,t as K,g as c,i as M}from"../chunks/C4An0dnW.js";import{s as l}from"../chunks/CQQh_IlD.js";import{i as O}from"../chunks/D__6P984.js";import{t as P}from"../chunks/C5aWxL5p.js";var Q=_('<div class="finance-grid svelte-1ba4c5d"><div class="finance-card svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">Total Buy-ins</span> <span class="finance-value currency svelte-1ba4c5d"> </span></div> <div class="finance-card svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">Total Rebuys</span> <span class="finance-value currency svelte-1ba4c5d"> </span></div> <div class="finance-card svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">Total Add-ons</span> <span class="finance-value currency svelte-1ba4c5d"> </span></div> <div class="finance-card highlight svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">Prize Pool</span> <span class="finance-value currency prize svelte-1ba4c5d"> </span></div> <div class="finance-card svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">House Fee</span> <span class="finance-value currency svelte-1ba4c5d"> </span></div> <div class="finance-card svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">Paid Positions</span> <span class="finance-value number svelte-1ba4c5d"> </span></div></div>'),U=_('<p class="empty-state svelte-1ba4c5d">No financial data available yet.</p>'),V=_('<div class="page-content svelte-1ba4c5d"><h2 class="svelte-1ba4c5d">Financials</h2> <p class="text-secondary svelte-1ba4c5d">Prize pool and payout information.</p> <!></div>');function ea(S,z){I(z,!1),G();var i=V(),T=s(a(i),4);{var F=t=>{const n=M(()=>P.financials);var r=Q(),v=a(r),u=s(a(v),2),q=a(u,!0);e(u),e(v);var d=s(v,2),m=s(a(d),2),A=a(m,!0);e(m),e(d);var o=s(d,2),g=s(a(o),2),B=a(g,!0);e(g),e(o);var p=s(o,2),y=s(a(p),2),H=a(y,!0);e(y),e(p);var f=s(p,2),h=s(a(f),2),N=a(h,!0);e(h),e(f);var x=s(f,2),L=s(a(x),2),R=a(L,!0);e(L),e(x),e(r),K((j,w,C,D,E)=>{l(q,j),l(A,w),l(B,C),l(H,D),l(N,E),l(R,c(n).paid_positions)},[()=>c(n).total_buyin.toLocaleString(),()=>c(n).total_rebuys.toLocaleString(),()=>c(n).total_addons.toLocaleString(),()=>c(n).prize_pool.toLocaleString(),()=>c(n).house_fee.toLocaleString()]),b(t,r)},k=t=>{var n=U();b(t,n)};O(T,t=>{P.financials?t(F):t(k,!1)})}e(i),b(S,i),J()}export{ea as component};

View file

@ -1,2 +0,0 @@
import{a as P,f as K,t as ue}from"../chunks/B6M6q2Zo.js";import{o as ke}from"../chunks/Bfwrz3i4.js";import{m as V,b as Ne,av as we,h as I,f as X,P as ze,c as Ce,g as h,r as Se,d as Ie,e as ce,i as j,j as G,am as ye,at as Me,ac as de,o as De,aw as y,n as Z,ax as He,t as Le,ay as Oe,az as Pe,as as ae,aA as Re,aB as Fe,aC as Be,Y as ve,aD as Ue,k as Ae,p as Te,aE as Q,_ as $e,aF as qe,aG as Ge,aq as Ye,l as Je,an as Ke,aH as Ve,aI as Xe,aJ as je,u as Qe,A as Y,w as We,aK as Ze,y as R,x as L,z as O,s as W,a as w}from"../chunks/Ym0WvvUy.js";import{d as er,e as rr,a as B,s as pe}from"../chunks/dTRRgeF-.js";import{i as ge}from"../chunks/B9dvBo0E.js";import{a as J}from"../chunks/DMqvp7vx.js";import{g as ee}from"../chunks/giww_vF6.js";function he(e,t){return t}function tr(e,t,r){for(var a=[],i=t.length,l,s=t.length,d=0;d<i;d++){let m=t[d];Te(m,()=>{if(l){if(l.pending.delete(m),l.done.add(m),l.pending.size===0){var g=e.outrogroups;re(ae(l.done)),g.delete(l),g.size===0&&(e.outrogroups=null)}}else s-=1},!1)}if(s===0){var f=a.length===0&&r!==null;if(f){var p=r,c=p.parentNode;Ye(c),c.append(p),e.items.clear()}re(t,!f)}else l={pending:new Set(t),done:new Set},(e.outrogroups??(e.outrogroups=new Set)).add(l)}function re(e,t=!0){for(var r=0;r<e.length;r++)Je(e[r],t)}var _e;function me(e,t,r,a,i,l=null){var s=e,d=new Map,f=(t&we)!==0;if(f){var p=e;s=I?X(ze(p)):p.appendChild(V())}I&&Ce();var c=null,m=Oe(()=>{var u=r();return Pe(u)?u:u==null?[]:ae(u)}),g,_=!0;function T(){n.fallback=c,ar(n,g,s,t,a),c!==null&&(g.length===0?(c.f&y)===0?Ae(c):(c.f^=y,q(c,null,s)):Te(c,()=>{c=null}))}var M=Ne(()=>{g=h(m);var u=g.length;let k=!1;if(I){var N=Se(s)===Ie;N!==(u===0)&&(s=ce(),X(s),j(!1),k=!0)}for(var b=new Set,A=De,D=Le(),E=0;E<u;E+=1){I&&G.nodeType===ye&&G.data===Me&&(s=G,k=!0,j(!1));var z=g[E],o=a(z,E),v=_?null:d.get(o);v?(v.v&&de(v.v,z),v.i&&de(v.i,E),D&&A.unskip_effect(v.e)):(v=nr(d,_?s:_e??(_e=V()),z,o,E,i,t,r),_||(v.e.f|=y),d.set(o,v)),b.add(o)}if(u===0&&l&&!c&&(_?c=Z(()=>l(s)):(c=Z(()=>l(_e??(_e=V()))),c.f|=y)),u>b.size&&He(),I&&u>0&&X(ce()),!_)if(D){for(const[x,C]of d)b.has(x)||A.skip_effect(C.e);A.oncommit(T),A.ondiscard(()=>{})}else T();k&&j(!0),h(m)}),n={effect:M,items:d,outrogroups:null,fallback:c};_=!1,I&&(s=G)}function U(e){for(;e!==null&&(e.f&qe)===0;)e=e.next;return e}function ar(e,t,r,a,i){var v,x,C,F,ne,ie,se,oe,le;var l=(a&Ge)!==0,s=t.length,d=e.items,f=U(e.effect.first),p,c=null,m,g=[],_=[],T,M,n,u;if(l)for(u=0;u<s;u+=1)T=t[u],M=i(T,u),n=d.get(M).e,(n.f&y)===0&&((x=(v=n.nodes)==null?void 0:v.a)==null||x.measure(),(m??(m=new Set)).add(n));for(u=0;u<s;u+=1){if(T=t[u],M=i(T,u),n=d.get(M).e,e.outrogroups!==null)for(const S of e.outrogroups)S.pending.delete(n),S.done.delete(n);if((n.f&y)!==0)if(n.f^=y,n===f)q(n,null,r);else{var k=c?c.next:f;n===e.effect.last&&(e.effect.last=n.prev),n.prev&&(n.prev.next=n.next),n.next&&(n.next.prev=n.prev),H(e,c,n),H(e,n,k),q(n,k,r),c=n,g=[],_=[],f=U(c.next);continue}if((n.f&Q)!==0&&(Ae(n),l&&((F=(C=n.nodes)==null?void 0:C.a)==null||F.unfix(),(m??(m=new Set)).delete(n))),n!==f){if(p!==void 0&&p.has(n)){if(g.length<_.length){var N=_[0],b;c=N.prev;var A=g[0],D=g[g.length-1];for(b=0;b<g.length;b+=1)q(g[b],N,r);for(b=0;b<_.length;b+=1)p.delete(_[b]);H(e,A.prev,D.next),H(e,c,A),H(e,D,N),f=N,c=D,u-=1,g=[],_=[]}else p.delete(n),q(n,f,r),H(e,n.prev,n.next),H(e,n,c===null?e.effect.first:c.next),H(e,c,n),c=n;continue}for(g=[],_=[];f!==null&&f!==n;)(p??(p=new Set)).add(f),_.push(f),f=U(f.next);if(f===null)continue}(n.f&y)===0&&g.push(n),c=n,f=U(n.next)}if(e.outrogroups!==null){for(const S of e.outrogroups)S.pending.size===0&&(re(ae(S.done)),(ne=e.outrogroups)==null||ne.delete(S));e.outrogroups.size===0&&(e.outrogroups=null)}if(f!==null||p!==void 0){var E=[];if(p!==void 0)for(n of p)(n.f&Q)===0&&E.push(n);for(;f!==null;)(f.f&Q)===0&&f!==e.fallback&&E.push(f),f=U(f.next);var z=E.length;if(z>0){var o=(a&we)!==0&&s===0?r:null;if(l){for(u=0;u<z;u+=1)(se=(ie=E[u].nodes)==null?void 0:ie.a)==null||se.measure();for(u=0;u<z;u+=1)(le=(oe=E[u].nodes)==null?void 0:oe.a)==null||le.fix()}tr(e,E,o)}}l&&$e(()=>{var S,fe;if(m!==void 0)for(n of m)(fe=(S=n.nodes)==null?void 0:S.a)==null||fe.apply()})}function nr(e,t,r,a,i,l,s,d){var f=(s&Re)!==0?(s&Fe)===0?Be(r,!1,!1):ve(r):null,p=(s&Ue)!==0?ve(i):null;return{v:f,i:p,e:Z(()=>(l(t,f??r,p??i,d),()=>{e.delete(a)}))}}function q(e,t,r){if(e.nodes)for(var a=e.nodes.start,i=e.nodes.end,l=t&&(t.f&y)===0?t.nodes.start:r;a!==null;){var s=Ke(a);if(l.before(a),a===i)return;a=s}}function H(e,t,r){t===null?e.effect.first=r:t.next=r,r===null?e.effect.last=t:r.prev=t}const be=[...`
\r\f \v\uFEFF`];function ir(e,t,r){var a=""+e;if(r){for(var i of Object.keys(r))if(r[i])a=a?a+" "+i:i;else if(a.length)for(var l=i.length,s=0;(s=a.indexOf(i,s))>=0;){var d=s+l;(s===0||be.includes(a[s-1]))&&(d===a.length||be.includes(a[d]))?a=(s===0?"":a.substring(0,s))+a.substring(d+1):s=d}}return a===""?null:a}function sr(e,t,r,a,i,l){var s=e.__className;if(I||s!==r||s===void 0){var d=ir(r,a,l);(!I||d!==e.getAttribute("class"))&&(d==null?e.removeAttribute("class"):e.className=d),e.__className=r}else if(l&&i!==l)for(var f in l){var p=!!l[f];(i==null||p!==!!i[f])&&e.classList.toggle(f,p)}return l}const or=Symbol("is custom element"),lr=Symbol("is html");function Ee(e,t,r,a){var i=fr(e);I&&(i[t]=e.getAttribute(t)),i[t]!==(i[t]=r)&&(r==null?e.removeAttribute(t):typeof r!="string"&&ur(e).includes(t)?e[t]=r:e.setAttribute(t,r))}function fr(e){return e.__attributes??(e.__attributes={[or]:e.nodeName.includes("-"),[lr]:e.namespaceURI===Ve})}var xe=new Map;function ur(e){var t=e.getAttribute("is")||e.nodeName,r=xe.get(t);if(r)return r;xe.set(t,r=[]);for(var a,i=e,l=Element.prototype;l!==i;){a=je(i);for(var s in a)a[s].set&&r.push(s);i=Xe(i)}return r}class te extends Error{constructor(t,r,a){const i=typeof a=="object"&&a!==null&&"error"in a?a.error:r;super(i),this.status=t,this.statusText=r,this.body=a,this.name="ApiError"}}function cr(){return`${window.location.origin}/api/v1`}function dr(e){const t={Accept:"application/json"};e&&(t["Content-Type"]="application/json");const r=J.token;return r&&(t.Authorization=`Bearer ${r}`),t}async function vr(e){if(e.status===401)throw J.logout(),await ee("/login"),new te(401,"Unauthorized",{error:"Session expired"});if(!e.ok){let t;try{t=await e.json()}catch{t={error:e.statusText}}throw new te(e.status,e.statusText,t)}if(e.status!==204)return e.json()}async function $(e,t,r){const a=`${cr()}${t}`,i={method:e,headers:dr(r!==void 0),credentials:"same-origin"};r!==void 0&&(i.body=JSON.stringify(r));const l=await fetch(a,i);return vr(l)}const pr={get(e){return $("GET",e)},post(e,t){return $("POST",e,t)},put(e,t){return $("PUT",e,t)},patch(e,t){return $("PATCH",e,t)},delete(e){return $("DELETE",e)}};var gr=K("<div></div>"),hr=K('<div class="error-message svelte-1x05zx6" role="alert"> </div>'),_r=K('<button class="numpad-btn touch-target svelte-1x05zx6"> </button>'),mr=K('<main class="login-container svelte-1x05zx6"><div class="login-card svelte-1x05zx6"><div class="logo svelte-1x05zx6"><h1 class="svelte-1x05zx6">Felt</h1> <p class="subtitle svelte-1x05zx6">Tournament Manager</p></div> <div class="pin-display svelte-1x05zx6" role="status"></div> <!> <div class="numpad svelte-1x05zx6"><!> <button class="numpad-btn numpad-fn touch-target svelte-1x05zx6" aria-label="Clear PIN">CLR</button> <button class="numpad-btn touch-target svelte-1x05zx6" aria-label="Digit 0">0</button> <button class="numpad-btn numpad-fn touch-target svelte-1x05zx6" aria-label="Delete last digit">DEL</button></div> <button class="submit-btn touch-target svelte-1x05zx6"><!></button></div></main>');function Nr(e,t){Qe(t,!0);let r=W(""),a=W(""),i=W(!1);const l=6;ke(()=>{J.isAuthenticated&&ee("/")});function s(o){h(r).length>=l||(w(r,h(r)+o),w(a,""))}function d(){w(r,h(r).slice(0,-1),!0),w(a,"")}function f(){w(r,""),w(a,"")}async function p(){if(!(h(r).length<4||h(i))){w(i,!0),w(a,"");try{const o=await pr.post("/auth/login",{pin:h(r)});J.login(o.token,{id:o.operator.id,name:o.operator.name,role:o.operator.role}),await ee("/")}catch(o){o instanceof te?o.status===429?w(a,"Too many attempts. Please wait."):o.status===401?w(a,"Invalid PIN. Try again."):w(a,o.message,!0):w(a,"Connection error. Check your network."),w(r,"")}finally{w(i,!1)}}}function c(o){o.key>="0"&&o.key<="9"?s(o.key):o.key==="Backspace"?d():o.key==="Enter"?p():o.key==="Escape"&&f()}var m=mr();rr("keydown",Ze,c);var g=R(m),_=L(R(g),2);me(_,21,()=>Array(l),he,(o,v,x)=>{var C=gr();let F;Y(()=>F=sr(C,1,"pin-dot svelte-1x05zx6",null,F,{filled:x<h(r).length})),P(o,C)}),O(_);var T=L(_,2);{var M=o=>{var v=hr(),x=R(v,!0);O(v),Y(()=>pe(x,h(a))),P(o,v)};ge(T,o=>{h(a)&&o(M)})}var n=L(T,2),u=R(n);me(u,16,()=>["1","2","3","4","5","6","7","8","9"],he,(o,v)=>{var x=_r(),C=R(x,!0);O(x),Y(()=>{x.disabled=h(i)||h(r).length>=l,Ee(x,"aria-label",`Digit ${v??""}`),pe(C,v)}),B("click",x,()=>s(v)),P(o,x)});var k=L(u,2),N=L(k,2),b=L(N,2);O(n);var A=L(n,2),D=R(A);{var E=o=>{var v=ue("Signing in...");P(o,v)},z=o=>{var v=ue("Sign In");P(o,v)};ge(D,o=>{h(i)?o(E):o(z,!1)})}O(A),O(g),O(m),Y(()=>{Ee(_,"aria-label",`PIN entered: ${h(r).length??""} digits`),k.disabled=h(i),N.disabled=h(i)||h(r).length>=l,b.disabled=h(i)||h(r).length===0,A.disabled=h(r).length<4||h(i)}),B("click",k,f),B("click",N,()=>s("0")),B("click",b,d),B("click",A,p),P(e,m),We()}er(["click"]);export{Nr as component};

View file

@ -0,0 +1 @@
import{a as f,f as y,t as M}from"../chunks/Q5CB4WY5.js";import{o as V}from"../chunks/nIaoZoCo.js";import{p as W,t as _,a as Y,a0 as Z,c as p,s as u,r as d,g as r,b as A,d as o}from"../chunks/C4An0dnW.js";import{d as tt,e as et,a as g,s as U}from"../chunks/CQQh_IlD.js";import{i as G}from"../chunks/D__6P984.js";import{e as O,i as R,s as q,a as at}from"../chunks/BeLKMLqR.js";import{a as k}from"../chunks/D3f6eoxz.js";import{g as D}from"../chunks/DQNCp18R.js";class C extends Error{constructor(s,a,i){const n=typeof i=="object"&&i!==null&&"error"in i?i.error:a;super(n),this.status=s,this.statusText=a,this.body=i,this.name="ApiError"}}function rt(){return`${window.location.origin}/api/v1`}function st(e){const s={Accept:"application/json"};e&&(s["Content-Type"]="application/json");const a=k.token;return a&&(s.Authorization=`Bearer ${a}`),s}async function it(e){if(e.status===401)throw k.logout(),await D("/login"),new C(401,"Unauthorized",{error:"Session expired"});if(!e.ok){let s;try{s=await e.json()}catch{s={error:e.statusText}}throw new C(e.status,e.statusText,s)}if(e.status!==204)return e.json()}async function m(e,s,a){const i=`${rt()}${s}`,n={method:e,headers:st(a!==void 0),credentials:"same-origin"};a!==void 0&&(n.body=JSON.stringify(a));const v=await fetch(i,n);return it(v)}const nt={get(e){return m("GET",e)},post(e,s){return m("POST",e,s)},put(e,s){return m("PUT",e,s)},patch(e,s){return m("PATCH",e,s)},delete(e){return m("DELETE",e)}};var ot=y("<div></div>"),lt=y('<div class="error-message svelte-1x05zx6" role="alert"> </div>'),ct=y('<button class="numpad-btn touch-target svelte-1x05zx6"> </button>'),ut=y('<main class="login-container svelte-1x05zx6"><div class="login-card svelte-1x05zx6"><div class="logo svelte-1x05zx6"><h1 class="svelte-1x05zx6">Felt</h1> <p class="subtitle svelte-1x05zx6">Tournament Manager</p></div> <div class="pin-display svelte-1x05zx6" role="status"></div> <!> <div class="numpad svelte-1x05zx6"><!> <button class="numpad-btn numpad-fn touch-target svelte-1x05zx6" aria-label="Clear PIN">CLR</button> <button class="numpad-btn touch-target svelte-1x05zx6" aria-label="Digit 0">0</button> <button class="numpad-btn numpad-fn touch-target svelte-1x05zx6" aria-label="Delete last digit">DEL</button></div> <button class="submit-btn touch-target svelte-1x05zx6"><!></button></div></main>');function bt(e,s){W(s,!0);let a=A(""),i=A(""),n=A(!1);const v=6;V(()=>{k.isAuthenticated&&D("/")});function w(t){r(a).length>=v||(o(a,r(a)+t),o(i,""))}function I(){o(a,r(a).slice(0,-1),!0),o(i,"")}function N(){o(a,""),o(i,"")}async function j(){if(!(r(a).length<4||r(n))){o(n,!0),o(i,"");try{const t=await nt.post("/auth/login",{pin:r(a)});k.login(t.token,{id:t.operator.id,name:t.operator.name,role:t.operator.role}),await D("/")}catch(t){t instanceof C?t.status===429?o(i,"Too many attempts. Please wait."):t.status===401?o(i,"Invalid PIN. Try again."):o(i,t.message,!0):o(i,"Connection error. Check your network."),o(a,"")}finally{o(n,!1)}}}function F(t){t.key>="0"&&t.key<="9"?w(t.key):t.key==="Backspace"?I():t.key==="Enter"?j():t.key==="Escape"&&N()}var z=ut();et("keydown",Z,F);var S=p(z),x=u(p(S),2);O(x,21,()=>Array(v),R,(t,l,c)=>{var b=ot();let H;_(()=>H=at(b,1,"pin-dot svelte-1x05zx6",null,H,{filled:c<r(a).length})),f(t,b)}),d(x);var $=u(x,2);{var J=t=>{var l=lt(),c=p(l,!0);d(l),_(()=>U(c,r(i))),f(t,l)};G($,t=>{r(i)&&t(J)})}var T=u($,2),L=p(T);O(L,16,()=>["1","2","3","4","5","6","7","8","9"],R,(t,l)=>{var c=ct(),b=p(c,!0);d(c),_(()=>{c.disabled=r(n)||r(a).length>=v,q(c,"aria-label",`Digit ${l??""}`),U(b,l)}),g("click",c,()=>w(l)),f(t,c)});var E=u(L,2),P=u(E,2),B=u(P,2);d(T);var h=u(T,2),K=p(h);{var X=t=>{var l=M("Signing in...");f(t,l)},Q=t=>{var l=M("Sign In");f(t,l)};G(K,t=>{r(n)?t(X):t(Q,!1)})}d(h),d(S),d(z),_(()=>{q(x,"aria-label",`PIN entered: ${r(a).length??""} digits`),E.disabled=r(n),P.disabled=r(n)||r(a).length>=v,B.disabled=r(n)||r(a).length===0,h.disabled=r(a).length<4||r(n)}),g("click",E,N),g("click",P,()=>w("0")),g("click",B,I),g("click",h,j),f(e,z),Y()}tt(["click"]);export{bt as component};

View file

@ -0,0 +1 @@
import{a as _,f as b}from"../chunks/Q5CB4WY5.js";import{i as x}from"../chunks/BViIIwgj.js";import{p as k,t as w,a as O,s as e,c as a,r as t}from"../chunks/C4An0dnW.js";import{d as S,a as U,s as m}from"../chunks/CQQh_IlD.js";import{a as o}from"../chunks/D3f6eoxz.js";import{g as y}from"../chunks/DQNCp18R.js";var L=b('<div class="page-content svelte-hq0atu"><h2 class="svelte-hq0atu">More</h2> <p class="text-secondary svelte-hq0atu">Settings and additional options.</p> <div class="menu-list svelte-hq0atu"><div class="menu-item svelte-hq0atu"><span class="menu-label svelte-hq0atu">Operator</span> <span class="menu-value svelte-hq0atu"> </span></div> <div class="menu-item svelte-hq0atu"><span class="menu-label svelte-hq0atu">Role</span> <span class="menu-value svelte-hq0atu"> </span></div> <hr class="divider svelte-hq0atu"/> <button class="menu-item menu-action danger touch-target svelte-hq0atu"><span class="menu-label svelte-hq0atu">Sign Out</span></button></div></div>');function C(c,d){k(d,!1);function h(){o.logout(),y("/login")}x();var s=L(),r=e(a(s),4),n=a(r),i=e(a(n),2),g=a(i,!0);t(i),t(n);var l=e(n,2),u=e(a(l),2),f=a(u,!0);t(u),t(l);var q=e(l,4);t(r),t(s),w(()=>{var v,p;m(g,((v=o.operator)==null?void 0:v.name)??"Unknown"),m(f,((p=o.operator)==null?void 0:p.role)??"Unknown")}),U("click",q,h),_(c,s),O()}S(["click"]);export{C as component};

View file

@ -0,0 +1 @@
import{a as c,f as d}from"../chunks/Q5CB4WY5.js";import{i as N}from"../chunks/BViIIwgj.js";import{p as $,a as j,s as a,c as s,r as e,t as B}from"../chunks/C4An0dnW.js";import{s as r}from"../chunks/CQQh_IlD.js";import{i as L}from"../chunks/D__6P984.js";import{t}from"../chunks/C5aWxL5p.js";var O=d('<div class="stats-grid svelte-14qseeg"><div class="stat-card svelte-14qseeg"><span class="stat-label svelte-14qseeg">Players</span> <span class="stat-value number svelte-14qseeg"> </span></div> <div class="stat-card svelte-14qseeg"><span class="stat-label svelte-14qseeg">Tables</span> <span class="stat-value number svelte-14qseeg"> </span></div> <div class="stat-card svelte-14qseeg"><span class="stat-label svelte-14qseeg">Level</span> <span class="stat-value number svelte-14qseeg"> </span></div> <div class="stat-card svelte-14qseeg"><span class="stat-label svelte-14qseeg">Blinds</span> <span class="stat-value blinds svelte-14qseeg"> </span></div></div>'),S=d('<p class="empty-state svelte-14qseeg">No active tournament. Start or join a tournament to see the overview.</p>'),z=d('<div class="page-content svelte-14qseeg"><h2 class="svelte-14qseeg">Overview</h2> <p class="text-secondary svelte-14qseeg">Tournament dashboard — detailed views coming in Plan N.</p> <!></div>');function H(f,u){$(u,!1),N();var i=z(),h=a(s(i),4);{var x=l=>{var v=O(),n=s(v),m=a(s(n),2),y=s(m);e(m),e(n);var o=a(n,2),g=a(s(o),2),P=s(g,!0);e(g),e(o);var p=a(o,2),_=a(s(p),2),w=s(_,!0);e(_),e(p);var q=a(p,2),b=a(s(q),2),T=s(b);e(b),e(q),e(v),B(()=>{r(y,`${t.remainingPlayers??""}/${t.totalPlayers??""}`),r(P,t.activeTables),r(w,t.clock.level),r(T,`${t.clock.small_blind??""}/${t.clock.big_blind??""}`)}),c(l,v)},k=l=>{var v=S();c(l,v)};L(h,l=>{t.clock?l(x):l(k,!1)})}e(i),c(f,i),j()}export{H as component};

View file

@ -0,0 +1 @@
import{a as o,f as i}from"../chunks/Q5CB4WY5.js";import{i as n}from"../chunks/BViIIwgj.js";import{p as b,a as p,s as u,c,r as d}from"../chunks/C4An0dnW.js";import{t as m}from"../chunks/C5aWxL5p.js";import{D as y}from"../chunks/WPMya0VZ.js";var h=i('<div class="page-content svelte-wtkzqx"><h2 class="svelte-wtkzqx">Players</h2> <p class="text-secondary svelte-wtkzqx">Registered players and chip counts.</p> <!></div>');function x(a,t){b(t,!1);const s=[{key:"name",label:"Name",sortable:!0},{key:"status",label:"Status",sortable:!0},{key:"chips",label:"Chips",sortable:!0,align:"right",render:r=>r.chips.toLocaleString()},{key:"table_id",label:"Table",hideMobile:!0,sortable:!0},{key:"seat",label:"Seat",hideMobile:!0,sortable:!0,align:"center"},{key:"rebuys",label:"Rebuys",hideMobile:!0,sortable:!0,align:"center"}];n();var e=h(),l=u(c(e),4);y(l,{get columns(){return s},get data(){return m.players},sortable:!0,searchable:!0,loading:!1,emptyMessage:"No players registered yet",rowKey:r=>String(r.id),swipeActions:[{id:"bust",label:"Bust",color:"var(--color-error)",handler:()=>{}},{id:"rebuy",label:"Rebuy",color:"var(--color-primary)",handler:()=>{}}]}),d(e),o(a,e),p()}export{x as component};

View file

@ -0,0 +1 @@
import{a as o,f as i}from"../chunks/Q5CB4WY5.js";import{p as b,a as p,g as c,x as u,s as d,c as m,r as g}from"../chunks/C4An0dnW.js";import{t as f}from"../chunks/C5aWxL5p.js";import{D as y}from"../chunks/WPMya0VZ.js";var v=i('<div class="page-content svelte-bf0doe"><h2 class="svelte-bf0doe">Tables</h2> <p class="text-secondary svelte-bf0doe">Active tables and seating.</p> <!></div>');function D(t,s){b(s,!0);const l=[{key:"number",label:"Table #",sortable:!0,align:"center"},{key:"seats",label:"Seats",sortable:!0,align:"center"},{key:"player_count",label:"Players",sortable:!0,align:"center"},{key:"is_final_table",label:"Final",hideMobile:!0,sortable:!0,align:"center",render:e=>e.is_final_table?"Yes":""}];let r=u(()=>f.tables.map(e=>({...e,player_count:e.players.length})));var a=v(),n=d(m(a),4);y(n,{get columns(){return l},get data(){return c(r)},sortable:!0,searchable:!1,loading:!1,emptyMessage:"No tables set up yet",rowKey:e=>String(e.id)}),g(a),o(t,a),p()}export{D as component};

View file

@ -1 +1 @@
{"version":"1772333625386"}
{"version":"1772334772507"}

View file

@ -0,0 +1,39 @@
<!doctype html>
<html lang="en" data-theme="mocha">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<link rel="icon" href="./favicon.png" />
<title>Felt</title>
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
</head>
<body>
<div style="display: contents">
<script>
{
__sveltekit_og1wdu = {
base: new URL(".", location).pathname.slice(0, -1)
};
const element = document.currentScript.parentElement;
Promise.all([
import("./_app/immutable/entry/start.Do4A91T6.js"),
import("./_app/immutable/entry/app.Dwn0pdp1.js")
]).then(([kit, app]) => {
kit.start(app, element);
});
}
</script>
</div>
</body>
</html>

View file

@ -5,30 +5,30 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<link rel="icon" href="/favicon.png" />
<title>Felt</title>
<link href="/_app/immutable/entry/start.Cw5np0_P.js" rel="modulepreload">
<link href="/_app/immutable/chunks/giww_vF6.js" rel="modulepreload">
<link href="/_app/immutable/chunks/Ym0WvvUy.js" rel="modulepreload">
<link href="/_app/immutable/chunks/Bfwrz3i4.js" rel="modulepreload">
<link href="/_app/immutable/entry/app.DWnDWHgs.js" rel="modulepreload">
<link href="/_app/immutable/chunks/dTRRgeF-.js" rel="modulepreload">
<link href="/_app/immutable/chunks/B6M6q2Zo.js" rel="modulepreload">
<link href="/_app/immutable/chunks/B9dvBo0E.js" rel="modulepreload">
<link href="/_app/immutable/chunks/Da6yQRl8.js" rel="modulepreload">
<link href="/_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
<link href="/_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
<link href="/_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
<link href="/_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
<link href="/_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
<link href="/_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
<link href="/_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
<link href="/_app/immutable/chunks/D__6P984.js" rel="modulepreload">
<link href="/_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
</head>
<body>
<div style="display: contents">
<script>
{
__sveltekit_1rgg0vt = {
__sveltekit_og1wdu = {
base: ""
};
const element = document.currentScript.parentElement;
Promise.all([
import("/_app/immutable/entry/start.Cw5np0_P.js"),
import("/_app/immutable/entry/app.DWnDWHgs.js")
import("/_app/immutable/entry/start.Do4A91T6.js"),
import("/_app/immutable/entry/app.Dwn0pdp1.js")
]).then(([kit, app]) => {
kit.start(app, element);
});

View file

@ -5,30 +5,30 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<link rel="icon" href="./favicon.png" />
<title>Felt</title>
<link href="./_app/immutable/entry/start.Cw5np0_P.js" rel="modulepreload">
<link href="./_app/immutable/chunks/giww_vF6.js" rel="modulepreload">
<link href="./_app/immutable/chunks/Ym0WvvUy.js" rel="modulepreload">
<link href="./_app/immutable/chunks/Bfwrz3i4.js" rel="modulepreload">
<link href="./_app/immutable/entry/app.DWnDWHgs.js" rel="modulepreload">
<link href="./_app/immutable/chunks/dTRRgeF-.js" rel="modulepreload">
<link href="./_app/immutable/chunks/B6M6q2Zo.js" rel="modulepreload">
<link href="./_app/immutable/chunks/B9dvBo0E.js" rel="modulepreload">
<link href="./_app/immutable/chunks/Da6yQRl8.js" rel="modulepreload">
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
</head>
<body>
<div style="display: contents">
<script>
{
__sveltekit_1rgg0vt = {
__sveltekit_og1wdu = {
base: new URL(".", location).pathname.slice(0, -1)
};
const element = document.currentScript.parentElement;
Promise.all([
import("./_app/immutable/entry/start.Cw5np0_P.js"),
import("./_app/immutable/entry/app.DWnDWHgs.js")
import("./_app/immutable/entry/start.Do4A91T6.js"),
import("./_app/immutable/entry/app.Dwn0pdp1.js")
]).then(([kit, app]) => {
kit.start(app, element);
});

39
frontend/build/more.html Normal file
View file

@ -0,0 +1,39 @@
<!doctype html>
<html lang="en" data-theme="mocha">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<link rel="icon" href="./favicon.png" />
<title>Felt</title>
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
</head>
<body>
<div style="display: contents">
<script>
{
__sveltekit_og1wdu = {
base: new URL(".", location).pathname.slice(0, -1)
};
const element = document.currentScript.parentElement;
Promise.all([
import("./_app/immutable/entry/start.Do4A91T6.js"),
import("./_app/immutable/entry/app.Dwn0pdp1.js")
]).then(([kit, app]) => {
kit.start(app, element);
});
}
</script>
</div>
</body>
</html>

View file

@ -0,0 +1,39 @@
<!doctype html>
<html lang="en" data-theme="mocha">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<link rel="icon" href="./favicon.png" />
<title>Felt</title>
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
</head>
<body>
<div style="display: contents">
<script>
{
__sveltekit_og1wdu = {
base: new URL(".", location).pathname.slice(0, -1)
};
const element = document.currentScript.parentElement;
Promise.all([
import("./_app/immutable/entry/start.Do4A91T6.js"),
import("./_app/immutable/entry/app.Dwn0pdp1.js")
]).then(([kit, app]) => {
kit.start(app, element);
});
}
</script>
</div>
</body>
</html>

View file

@ -0,0 +1,39 @@
<!doctype html>
<html lang="en" data-theme="mocha">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<link rel="icon" href="./favicon.png" />
<title>Felt</title>
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
</head>
<body>
<div style="display: contents">
<script>
{
__sveltekit_og1wdu = {
base: new URL(".", location).pathname.slice(0, -1)
};
const element = document.currentScript.parentElement;
Promise.all([
import("./_app/immutable/entry/start.Do4A91T6.js"),
import("./_app/immutable/entry/app.Dwn0pdp1.js")
]).then(([kit, app]) => {
kit.start(app, element);
});
}
</script>
</div>
</body>
</html>

View file

@ -0,0 +1,39 @@
<!doctype html>
<html lang="en" data-theme="mocha">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<link rel="icon" href="./favicon.png" />
<title>Felt</title>
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
</head>
<body>
<div style="display: contents">
<script>
{
__sveltekit_og1wdu = {
base: new URL(".", location).pathname.slice(0, -1)
};
const element = document.currentScript.parentElement;
Promise.all([
import("./_app/immutable/entry/start.Do4A91T6.js"),
import("./_app/immutable/entry/app.Dwn0pdp1.js")
]).then(([kit, app]) => {
kit.start(app, element);
});
}
</script>
</div>
</body>
</html>

View file

@ -0,0 +1,108 @@
<script lang="ts">
import { page } from '$app/state';
/**
* Bottom tab bar for mobile navigation.
* 5 tabs: Overview, Players, Tables, Financials, More.
* Hidden on desktop (>= 768px) where sidebar shows instead.
* 48px touch targets for poker room environment.
*/
interface Tab {
label: string;
href: string;
icon: string;
}
const tabs: Tab[] = [
{ label: 'Overview', href: '/overview', icon: '\u{1F3E0}' },
{ label: 'Players', href: '/players', icon: '\u{1F465}' },
{ label: 'Tables', href: '/tables', icon: '\u{1FA91}' },
{ label: 'Financials', href: '/financials', icon: '\u{1F4B0}' },
{ label: 'More', href: '/more', icon: '\u{2699}' }
];
function isActive(tabHref: string): boolean {
const path = page.url?.pathname ?? '/';
if (tabHref === '/overview') {
return path === '/' || path === '/overview' || path.startsWith('/overview/');
}
return path === tabHref || path.startsWith(tabHref + '/');
}
</script>
<div class="bottom-tabs" role="tablist" aria-label="Main navigation">
{#each tabs as tab}
<a
href={tab.href}
class="tab-item"
class:active={isActive(tab.href)}
role="tab"
aria-selected={isActive(tab.href)}
aria-label={tab.label}
>
<span class="tab-icon" aria-hidden="true">{tab.icon}</span>
<span class="tab-label">{tab.label}</span>
</a>
{/each}
</div>
<style>
.bottom-tabs {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 90;
display: flex;
align-items: stretch;
justify-content: space-around;
background-color: var(--color-bg-elevated);
border-top: 1px solid var(--color-border);
/* Safe area for phones with home indicator */
padding-bottom: env(safe-area-inset-bottom, 0);
}
/* Hidden on desktop */
@media (min-width: 768px) {
.bottom-tabs {
display: none;
}
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
flex: 1;
min-height: var(--touch-target);
padding: var(--space-1) var(--space-2);
text-decoration: none;
color: var(--color-text-muted);
transition: color var(--transition-fast);
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.tab-item.active {
color: var(--color-primary);
}
.tab-item:hover:not(.active) {
color: var(--color-text-secondary);
}
.tab-icon {
font-size: var(--text-xl);
line-height: 1;
}
.tab-label {
font-size: 10px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
}
</style>

View file

@ -0,0 +1,466 @@
<script lang="ts">
/**
* Reusable data table component.
*
* Features: sort by column, sticky header, search/filter, row click,
* mobile swipe actions, loading skeleton, empty state.
* Responsive: configurable column visibility per breakpoint.
* 48px row height for touch targets.
*/
interface Column<T> {
/** Unique key matching a property on data items. */
key: string;
/** Display label for column header. */
label: string;
/** Whether this column is sortable. Default: true if sortable prop is true. */
sortable?: boolean;
/** Hide on mobile (< 768px). */
hideMobile?: boolean;
/** Custom render function (returns string). */
render?: (item: T) => string;
/** Text alignment. */
align?: 'left' | 'center' | 'right';
/** Column width (CSS value). */
width?: string;
}
interface SwipeAction<T> {
id: string;
label: string;
color: string;
handler: (item: T) => void;
}
interface Props<T> {
columns: Column<T>[];
data: T[];
sortable?: boolean;
searchable?: boolean;
loading?: boolean;
emptyMessage?: string;
rowKey?: (item: T) => string;
onrowclick?: (item: T) => void;
swipeActions?: SwipeAction<T>[];
}
let {
columns,
data,
sortable = false,
searchable = false,
loading = false,
emptyMessage = 'No data',
rowKey = (item: Record<string, unknown>) => String(item['id'] ?? ''),
onrowclick,
swipeActions = []
}: Props<Record<string, unknown>> = $props();
// Sort state
let sortKey = $state<string | null>(null);
let sortDir = $state<'asc' | 'desc'>('asc');
// Search state
let searchQuery = $state('');
// Swipe state
let swipedRowId = $state<string | null>(null);
let touchStartX = 0;
let touchCurrentX = 0;
const SWIPE_THRESHOLD = 60;
// Filtered and sorted data
let processedData = $derived.by(() => {
let result = [...data];
// Filter by search query
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
result = result.filter((item) => {
return columns.some((col) => {
const value = col.render ? col.render(item) : String(item[col.key] ?? '');
return value.toLowerCase().includes(query);
});
});
}
// Sort
if (sortKey) {
const key = sortKey;
const dir = sortDir;
result.sort((a, b) => {
const aVal = a[key];
const bVal = b[key];
if (aVal == null && bVal == null) return 0;
if (aVal == null) return 1;
if (bVal == null) return -1;
let cmp: number;
if (typeof aVal === 'number' && typeof bVal === 'number') {
cmp = aVal - bVal;
} else {
cmp = String(aVal).localeCompare(String(bVal));
}
return dir === 'asc' ? cmp : -cmp;
});
}
return result;
});
function toggleSort(key: string): void {
if (!sortable) return;
if (sortKey === key) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
} else {
sortKey = key;
sortDir = 'asc';
}
}
function getCellValue(item: Record<string, unknown>, col: Column<Record<string, unknown>>): string {
if (col.render) return col.render(item);
const val = item[col.key];
if (val == null) return '';
return String(val);
}
// Swipe handlers for mobile
function handleTouchStart(event: TouchEvent, id: string): void {
if (swipeActions.length === 0) return;
touchStartX = event.touches[0].clientX;
touchCurrentX = touchStartX;
// Close other swiped rows
if (swipedRowId && swipedRowId !== id) {
swipedRowId = null;
}
}
function handleTouchMove(event: TouchEvent): void {
if (swipeActions.length === 0) return;
touchCurrentX = event.touches[0].clientX;
}
function handleTouchEnd(id: string): void {
if (swipeActions.length === 0) return;
const diff = touchStartX - touchCurrentX;
if (diff > SWIPE_THRESHOLD) {
// Swipe left: reveal actions
swipedRowId = id;
} else if (diff < -SWIPE_THRESHOLD) {
// Swipe right: hide actions
swipedRowId = null;
}
}
</script>
<div class="data-table-wrapper">
<!-- Search input -->
{#if searchable}
<div class="table-search">
<input
type="search"
placeholder="Search..."
bind:value={searchQuery}
class="search-input"
aria-label="Search table"
/>
</div>
{/if}
<!-- Table -->
<div class="table-scroll">
<table class="data-table" role="grid">
<thead>
<tr>
{#each columns as col}
<th
class:hide-mobile={col.hideMobile}
class:sortable={sortable && col.sortable !== false}
style:text-align={col.align ?? 'left'}
style:width={col.width}
onclick={() => {
if (sortable && col.sortable !== false) toggleSort(col.key);
}}
aria-sort={sortKey === col.key ? sortDir === 'asc' ? 'ascending' : 'descending' : undefined}
>
<span class="th-content">
{col.label}
{#if sortable && col.sortable !== false}
<span class="sort-indicator" class:active={sortKey === col.key}>
{#if sortKey === col.key}
{sortDir === 'asc' ? '\u25B2' : '\u25BC'}
{:else}
\u25B4
{/if}
</span>
{/if}
</span>
</th>
{/each}
</tr>
</thead>
<tbody>
{#if loading}
<!-- Skeleton loading rows -->
{#each Array(5) as _}
<tr class="skeleton-row">
{#each columns as col}
<td class:hide-mobile={col.hideMobile}>
<div class="skeleton-cell"></div>
</td>
{/each}
</tr>
{/each}
{:else if processedData.length === 0}
<!-- Empty state -->
<tr>
<td colspan={columns.length} class="empty-state">
{emptyMessage}
</td>
</tr>
{:else}
<!-- Data rows -->
{#each processedData as item (rowKey(item))}
{@const id = rowKey(item)}
<tr
class="data-row"
class:clickable={!!onrowclick}
class:swiped={swipedRowId === id}
onclick={() => onrowclick?.(item)}
onkeydown={(e) => e.key === 'Enter' && onrowclick?.(item)}
ontouchstart={(e) => handleTouchStart(e, id)}
ontouchmove={handleTouchMove}
ontouchend={() => handleTouchEnd(id)}
role={onrowclick ? 'button' : undefined}
tabindex={onrowclick ? 0 : undefined}
>
{#each columns as col}
<td
class:hide-mobile={col.hideMobile}
style:text-align={col.align ?? 'left'}
>
{getCellValue(item, col)}
</td>
{/each}
</tr>
<!-- Swipe actions overlay -->
{#if swipedRowId === id && swipeActions.length > 0}
<tr class="swipe-actions-row">
<td colspan={columns.length}>
<div class="swipe-actions">
{#each swipeActions as action}
<button
class="swipe-action-btn touch-target"
style="background-color: {action.color}"
onclick={(e) => {
e.stopPropagation();
action.handler(item);
swipedRowId = null;
}}
>
{action.label}
</button>
{/each}
</div>
</td>
</tr>
{/if}
{/each}
{/if}
</tbody>
</table>
</div>
</div>
<style>
.data-table-wrapper {
width: 100%;
overflow: hidden;
}
/* Search */
.table-search {
padding: var(--space-3) 0;
}
.search-input {
width: 100%;
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
color: var(--color-text);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
outline: none;
transition: border-color var(--transition-fast);
min-height: var(--touch-target);
}
.search-input:focus {
border-color: var(--color-primary);
}
.search-input::placeholder {
color: var(--color-text-muted);
}
/* Table scroll wrapper */
.table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Table */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-sm);
}
/* Sticky header */
.data-table thead {
position: sticky;
top: 0;
z-index: 2;
}
.data-table th {
padding: var(--space-2) var(--space-3);
font-weight: 600;
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
background-color: var(--color-bg-elevated);
border-bottom: 2px solid var(--color-border);
white-space: nowrap;
user-select: none;
}
.data-table th.sortable {
cursor: pointer;
}
.data-table th.sortable:hover {
color: var(--color-text);
}
.th-content {
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
.sort-indicator {
font-size: 8px;
color: var(--color-text-muted);
opacity: 0.3;
}
.sort-indicator.active {
opacity: 1;
color: var(--color-primary);
}
/* Data rows */
.data-table td {
padding: var(--space-2) var(--space-3);
color: var(--color-text);
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.data-row {
min-height: var(--touch-target);
transition: background-color var(--transition-fast);
}
.data-row:hover {
background-color: var(--color-surface);
}
.data-row.clickable {
cursor: pointer;
}
.data-row.clickable:active {
background-color: var(--color-surface-hover);
}
/* Empty state */
.empty-state {
text-align: center;
padding: var(--space-12) var(--space-4);
color: var(--color-text-muted);
font-style: italic;
}
/* Skeleton loading */
.skeleton-row td {
padding: var(--space-3);
}
.skeleton-cell {
height: 16px;
background: linear-gradient(
90deg,
var(--color-surface) 25%,
var(--color-surface-hover) 50%,
var(--color-surface) 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: var(--radius-sm);
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Swipe actions */
.swipe-actions-row td {
padding: 0;
border-bottom: none;
}
.swipe-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-1);
padding: var(--space-1);
background-color: var(--color-bg-sunken);
}
.swipe-action-btn {
padding: var(--space-2) var(--space-4);
font-size: var(--text-sm);
font-weight: 600;
color: white;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
white-space: nowrap;
}
/* Hide on mobile */
.hide-mobile {
display: none;
}
@media (min-width: 768px) {
.hide-mobile {
display: table-cell;
}
.data-table td {
max-width: 300px;
}
}
</style>

View file

@ -0,0 +1,257 @@
<script lang="ts">
import { tournament } from '$lib/stores/tournament.svelte';
/**
* Floating Action Button for quick tournament actions.
* Positioned bottom-right, above the tab bar.
* Expands to show action buttons: Bust, Buy In, Rebuy, Add-On, Pause/Resume.
* Context-aware: only shows relevant actions.
*/
interface FABAction {
id: string;
label: string;
icon: string;
color: string;
visible: () => boolean;
}
let expanded = $state(false);
/** Event handler passed from parent. */
interface Props {
onaction?: (actionId: string) => void;
}
let { onaction }: Props = $props();
const actions: FABAction[] = [
{
id: 'bust',
label: 'Bust',
icon: '\u{274C}',
color: 'var(--color-error)',
visible: () => tournament.remainingPlayers > 0
},
{
id: 'buyin',
label: 'Buy In',
icon: '\u{2795}',
color: 'var(--color-success)',
visible: () => true
},
{
id: 'rebuy',
label: 'Rebuy',
icon: '\u{1F504}',
color: 'var(--color-primary)',
visible: () => tournament.remainingPlayers > 0
},
{
id: 'addon',
label: 'Add-On',
icon: '\u{2B06}',
color: 'var(--color-warning)',
visible: () => {
// Show add-on only when relevant (simplified; real logic depends on tournament phase)
return tournament.clock !== null;
}
},
{
id: 'pause-resume',
label: tournament.clock?.is_paused ? 'Resume' : 'Pause',
icon: tournament.clock?.is_paused ? '\u{25B6}' : '\u{23F8}',
color: 'var(--ctp-peach)',
visible: () => tournament.clock !== null
}
];
function toggle(): void {
expanded = !expanded;
}
function handleAction(actionId: string): void {
expanded = false;
onaction?.(actionId);
}
function handleBackdrop(): void {
expanded = false;
}
function handleKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape' && expanded) {
expanded = false;
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if expanded}
<!-- Backdrop -->
<div
class="fab-backdrop"
onclick={handleBackdrop}
onkeydown={(e) => e.key === 'Enter' && handleBackdrop()}
role="presentation"
></div>
{/if}
<div class="fab-container" class:expanded>
<!-- Action buttons (visible when expanded) -->
{#if expanded}
<div class="fab-actions">
{#each actions.filter((a) => a.visible()) as action, i}
<button
class="fab-action touch-target"
style="--action-color: {action.color}; --action-delay: {i * 40}ms"
onclick={() => handleAction(action.id)}
aria-label={action.label}
>
<span class="fab-action-icon" aria-hidden="true">{action.icon}</span>
<span class="fab-action-label">{action.label}</span>
</button>
{/each}
</div>
{/if}
<!-- Main FAB button -->
<button
class="fab-main touch-target"
class:expanded
onclick={toggle}
aria-label={expanded ? 'Close actions' : 'Open quick actions'}
aria-expanded={expanded}
>
<span class="fab-icon" aria-hidden="true">{expanded ? '\u{2715}' : '\u{002B}'}</span>
</button>
</div>
<style>
.fab-backdrop {
position: fixed;
inset: 0;
z-index: 94;
background-color: rgba(0, 0, 0, 0.4);
animation: fade-in 150ms ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.fab-container {
position: fixed;
/* Above tab bar on mobile (tab bar ~56px + safe area) */
bottom: calc(var(--touch-target) + var(--space-6) + env(safe-area-inset-bottom, 0px));
right: var(--space-4);
z-index: 95;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-3);
}
/* Desktop: no tab bar offset needed */
@media (min-width: 768px) {
.fab-container {
bottom: var(--space-6);
right: var(--space-6);
}
}
/* Action buttons container */
.fab-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-2);
}
.fab-action {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) var(--space-4);
background-color: var(--color-surface);
border: 1px solid var(--action-color);
border-radius: var(--radius-full);
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
box-shadow: var(--shadow-md);
animation: fab-action-in 200ms ease-out backwards;
animation-delay: var(--action-delay);
transition:
transform var(--transition-fast),
background-color var(--transition-fast);
}
.fab-action:hover {
background-color: var(--color-surface-hover);
}
@keyframes fab-action-in {
from {
opacity: 0;
transform: translateY(10px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.fab-action-icon {
font-size: var(--text-lg);
color: var(--action-color);
width: 1.5em;
text-align: center;
}
.fab-action-label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text);
white-space: nowrap;
}
/* Main FAB button */
.fab-main {
width: 56px;
height: 56px;
min-height: 56px;
min-width: 56px;
border-radius: var(--radius-full);
background-color: var(--color-primary);
color: var(--color-bg);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow-lg);
-webkit-tap-highlight-color: transparent;
user-select: none;
transition:
transform var(--transition-normal),
background-color var(--transition-fast);
}
.fab-main:hover {
background-color: color-mix(in srgb, var(--color-primary) 90%, white);
}
.fab-main.expanded {
background-color: var(--color-surface-active);
transform: rotate(45deg);
}
.fab-icon {
font-size: var(--text-2xl);
font-weight: 300;
line-height: 1;
}
</style>

View file

@ -0,0 +1,258 @@
<script lang="ts">
import { tournament } from '$lib/stores/tournament.svelte';
/**
* Persistent header showing tournament clock, level, blinds, player count.
* Fixed at top, always visible. Auto-updates from tournament state store.
* Compact on mobile, expanded on desktop.
*/
/** Format seconds into MM:SS. */
function formatTime(seconds: number): string {
if (seconds < 0) seconds = 0;
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
/** Format blind values (e.g., "100/200"). */
function formatBlinds(sb: number, bb: number): string {
return `${sb.toLocaleString()}/${bb.toLocaleString()}`;
}
</script>
<header class="header">
<div class="header-inner">
{#if tournament.clock}
{@const clock = tournament.clock}
{@const isUrgent = clock.remaining_seconds <= 10 && !clock.is_break && !clock.is_paused}
<!-- Clock / Timer -->
<div class="header-clock" class:urgent={isUrgent} class:paused={clock.is_paused} class:on-break={clock.is_break}>
{#if clock.is_paused}
<span class="status-badge paused-badge">PAUSED</span>
{:else if clock.is_break}
<span class="status-badge break-badge">BREAK</span>
{/if}
<span class="timer clock-time">{formatTime(clock.remaining_seconds)}</span>
</div>
<!-- Level info -->
<div class="header-level">
<span class="level-number">L{clock.level}</span>
<span class="level-name hide-mobile">{clock.name}</span>
</div>
<!-- Blinds -->
<div class="header-blinds">
<span class="blinds-label hide-mobile">Blinds</span>
<span class="blinds blinds-value">{formatBlinds(clock.small_blind, clock.big_blind)}</span>
{#if clock.ante > 0}
<span class="blinds ante-value hide-mobile">Ante {clock.ante.toLocaleString()}</span>
{/if}
</div>
<!-- Player count -->
<div class="header-players">
<span class="number player-count">{tournament.remainingPlayers}/{tournament.totalPlayers}</span>
<span class="player-label hide-mobile">remaining</span>
</div>
{:else}
<!-- No tournament data yet -->
<div class="header-empty">
<span class="brand">Felt</span>
<span class="no-data">No active tournament</span>
</div>
{/if}
</div>
</header>
<style>
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-2) var(--space-4);
max-width: 1400px;
margin: 0 auto;
min-height: var(--touch-target);
}
/* Clock section */
.header-clock {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
}
.clock-time {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-clock);
letter-spacing: 0.02em;
}
.header-clock.urgent .clock-time {
color: var(--color-error);
animation: pulse-urgent 1s ease-in-out infinite;
}
.header-clock.paused .clock-time {
opacity: 0.6;
}
.header-clock.on-break .clock-time {
color: var(--color-break);
}
@keyframes pulse-urgent {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-badge {
font-size: var(--text-xs);
font-weight: 700;
padding: 2px var(--space-2);
border-radius: var(--radius-sm);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.paused-badge {
background-color: color-mix(in srgb, var(--ctp-peach) 20%, transparent);
color: var(--ctp-peach);
animation: pulse-paused 2s ease-in-out infinite;
}
@keyframes pulse-paused {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.break-badge {
background-color: color-mix(in srgb, var(--color-break) 20%, transparent);
color: var(--color-break);
}
/* Level section */
.header-level {
display: flex;
align-items: baseline;
gap: var(--space-2);
min-width: 0;
}
.level-number {
font-size: var(--text-lg);
font-weight: 700;
color: var(--color-text);
flex-shrink: 0;
}
.level-name {
font-size: var(--text-sm);
color: var(--color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Blinds section */
.header-blinds {
display: flex;
align-items: baseline;
gap: var(--space-2);
flex-shrink: 0;
}
.blinds-label {
font-size: var(--text-xs);
color: var(--color-text-muted);
text-transform: uppercase;
}
.blinds-value {
font-size: var(--text-lg);
font-weight: 600;
color: var(--color-text);
}
.ante-value {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
/* Player count section */
.header-players {
display: flex;
align-items: baseline;
gap: var(--space-1);
flex-shrink: 0;
}
.player-count {
font-size: var(--text-lg);
font-weight: 600;
color: var(--color-success);
}
.player-label {
font-size: var(--text-xs);
color: var(--color-text-muted);
}
/* Empty state */
.header-empty {
display: flex;
align-items: center;
gap: var(--space-3);
width: 100%;
}
.brand {
font-size: var(--text-xl);
font-weight: 700;
color: var(--color-primary);
}
.no-data {
font-size: var(--text-sm);
color: var(--color-text-muted);
}
/* Hide on mobile */
.hide-mobile {
display: none;
}
/* Desktop: show hidden elements, bigger clock */
@media (min-width: 768px) {
.hide-mobile {
display: inline;
}
.clock-time {
font-size: var(--text-3xl);
}
.header-inner {
padding: var(--space-3) var(--space-6);
gap: var(--space-6);
}
}
</style>

View file

@ -0,0 +1,145 @@
<script lang="ts">
/**
* Loading state components.
*
* - Skeleton: animated placeholder matching content shape
* - Spinner: circular spinner for buttons and page load
* - FullPage: full-page loading overlay for initial app load
*/
interface Props {
/** Loading variant to render. */
variant?: 'spinner' | 'skeleton' | 'full-page';
/** Number of skeleton rows (for skeleton variant). */
rows?: number;
/** Text to show below spinner (for full-page and spinner variants). */
text?: string;
/** Size of the spinner. */
size?: 'sm' | 'md' | 'lg';
}
let {
variant = 'spinner',
rows = 3,
text = '',
size = 'md'
}: Props = $props();
</script>
{#if variant === 'full-page'}
<div class="full-page-loading" role="status" aria-label="Loading">
<div class="spinner spinner-lg"></div>
{#if text}
<p class="loading-text">{text}</p>
{/if}
</div>
{:else if variant === 'skeleton'}
<div class="skeleton-container" role="status" aria-label="Loading content">
{#each Array(rows) as _}
<div class="skeleton-row">
<div class="skeleton-line skeleton-short"></div>
<div class="skeleton-line skeleton-long"></div>
<div class="skeleton-line skeleton-medium"></div>
</div>
{/each}
<span class="visually-hidden">Loading...</span>
</div>
{:else}
<span class="spinner spinner-{size}" role="status" aria-label="Loading">
<span class="visually-hidden">Loading...</span>
</span>
{/if}
<style>
/* Spinner */
.spinner {
display: inline-block;
border-radius: var(--radius-full);
border-style: solid;
border-color: var(--color-surface-active);
border-top-color: var(--color-primary);
animation: spin 0.6s linear infinite;
}
.spinner-sm {
width: 16px;
height: 16px;
border-width: 2px;
}
.spinner-md {
width: 24px;
height: 24px;
border-width: 3px;
}
.spinner-lg {
width: 40px;
height: 40px;
border-width: 4px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Full-page loading */
.full-page-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-4);
min-height: 100dvh;
background-color: var(--color-bg);
}
.loading-text {
font-size: var(--text-sm);
color: var(--color-text-muted);
}
/* Skeleton */
.skeleton-container {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-4);
}
.skeleton-row {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.skeleton-line {
height: 14px;
background: linear-gradient(
90deg,
var(--color-surface) 25%,
var(--color-surface-hover) 50%,
var(--color-surface) 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: var(--radius-sm);
}
.skeleton-short {
width: 40%;
}
.skeleton-medium {
width: 70%;
}
.skeleton-long {
width: 90%;
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>

View file

@ -0,0 +1,135 @@
<script lang="ts">
import { page } from '$app/state';
/**
* Desktop sidebar navigation.
* Same 5 navigation items as bottom tabs but in vertical layout.
* Renders only on desktop (>= 768px). Hidden on mobile.
*/
interface NavItem {
label: string;
href: string;
icon: string;
}
const navItems: NavItem[] = [
{ label: 'Overview', href: '/overview', icon: '\u{1F3E0}' },
{ label: 'Players', href: '/players', icon: '\u{1F465}' },
{ label: 'Tables', href: '/tables', icon: '\u{1FA91}' },
{ label: 'Financials', href: '/financials', icon: '\u{1F4B0}' },
{ label: 'More', href: '/more', icon: '\u{2699}' }
];
function isActive(itemHref: string): boolean {
const path = page.url?.pathname ?? '/';
if (itemHref === '/overview') {
return path === '/' || path === '/overview' || path.startsWith('/overview/');
}
return path === itemHref || path.startsWith(itemHref + '/');
}
</script>
<nav class="sidebar" aria-label="Main navigation">
<div class="sidebar-brand">
<span class="brand-name">Felt</span>
</div>
<ul class="sidebar-nav" role="list">
{#each navItems as item}
<li>
<a
href={item.href}
class="nav-item"
class:active={isActive(item.href)}
aria-current={isActive(item.href) ? 'page' : undefined}
>
<span class="nav-icon" aria-hidden="true">{item.icon}</span>
<span class="nav-label">{item.label}</span>
</a>
</li>
{/each}
</ul>
</nav>
<style>
.sidebar {
display: none;
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 220px;
background-color: var(--color-bg-elevated);
border-right: 1px solid var(--color-border);
z-index: 80;
padding-top: var(--space-4);
overflow-y: auto;
}
/* Only show on desktop */
@media (min-width: 768px) {
.sidebar {
display: flex;
flex-direction: column;
}
}
.sidebar-brand {
padding: var(--space-4) var(--space-6);
margin-bottom: var(--space-4);
}
.brand-name {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-primary);
letter-spacing: -0.02em;
}
.sidebar-nav {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.nav-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-6);
min-height: var(--touch-target);
text-decoration: none;
color: var(--color-text-secondary);
border-radius: 0;
transition:
background-color var(--transition-fast),
color var(--transition-fast);
}
.nav-item:hover:not(.active) {
background-color: var(--color-surface);
color: var(--color-text);
}
.nav-item.active {
color: var(--color-primary);
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
border-right: 3px solid var(--color-primary);
}
.nav-icon {
font-size: var(--text-xl);
width: 1.5em;
text-align: center;
flex-shrink: 0;
}
.nav-label {
font-size: var(--text-base);
font-weight: 500;
}
</style>

View file

@ -0,0 +1,156 @@
<script lang="ts">
import { toast } from '$lib/stores/toast.svelte';
/**
* Toast notification container.
* Renders all active toasts with slide-in/fade-out animations.
* Positioned fixed at top-right on desktop, bottom-center on mobile.
*/
function typeColor(type: string): string {
switch (type) {
case 'success':
return 'var(--color-success)';
case 'info':
return 'var(--color-primary)';
case 'warning':
return 'var(--color-warning)';
case 'error':
return 'var(--color-error)';
default:
return 'var(--color-text)';
}
}
function typeIcon(type: string): string {
switch (type) {
case 'success':
return '\u2713'; // checkmark
case 'info':
return '\u2139'; // info
case 'warning':
return '\u26A0'; // warning
case 'error':
return '\u2717'; // X
default:
return '';
}
}
</script>
{#if toast.toasts.length > 0}
<div class="toast-container" role="region" aria-label="Notifications" aria-live="polite">
{#each toast.toasts as t (t.id)}
<div
class="toast toast-{t.type}"
role="alert"
style="--toast-color: {typeColor(t.type)}"
>
<span class="toast-icon" aria-hidden="true">{typeIcon(t.type)}</span>
<span class="toast-message">{t.message}</span>
{#if t.dismissible}
<button
class="toast-dismiss"
onclick={() => toast.dismiss(t.id)}
aria-label="Dismiss notification"
>
&times;
</button>
{/if}
</div>
{/each}
</div>
{/if}
<style>
.toast-container {
position: fixed;
z-index: 9999;
display: flex;
flex-direction: column;
gap: var(--space-2);
pointer-events: none;
/* Mobile: bottom center */
bottom: calc(var(--touch-target) + var(--space-8) + var(--space-4));
left: var(--space-4);
right: var(--space-4);
align-items: center;
}
/* Desktop: top-right */
@media (min-width: 768px) {
.toast-container {
top: var(--space-4);
right: var(--space-4);
bottom: auto;
left: auto;
align-items: flex-end;
max-width: 400px;
}
}
.toast {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background-color: var(--color-surface);
border: 1px solid var(--toast-color);
border-left: 4px solid var(--toast-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
pointer-events: auto;
width: 100%;
max-width: 400px;
animation: toast-slide-in 200ms ease-out;
}
@keyframes toast-slide-in {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.toast-icon {
flex-shrink: 0;
font-size: var(--text-lg);
color: var(--toast-color);
width: 1.5em;
text-align: center;
}
.toast-message {
flex: 1;
font-size: var(--text-sm);
color: var(--color-text);
line-height: var(--leading-normal);
}
.toast-dismiss {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
min-height: 28px;
min-width: 28px;
padding: 0;
font-size: var(--text-lg);
color: var(--color-text-muted);
background: none;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: color var(--transition-fast);
}
.toast-dismiss:hover {
color: var(--color-text);
}
</style>

View file

@ -0,0 +1,110 @@
<script lang="ts">
import { multiTournament } from '$lib/stores/multi-tournament.svelte';
/**
* Multi-tournament tab selector.
* Shows when 2+ tournaments are active.
* Horizontal scrollable tabs on mobile.
* Each tab: tournament name + status indicator.
*/
function statusColor(status: string): string {
switch (status) {
case 'active':
return 'var(--color-success)';
case 'paused':
return 'var(--ctp-peach)';
case 'break':
return 'var(--color-break)';
case 'completed':
return 'var(--color-text-muted)';
default:
return 'var(--color-text-secondary)';
}
}
</script>
{#if multiTournament.isMulti}
<div class="tournament-tabs" role="tablist" aria-label="Tournament selection">
{#each multiTournament.tournaments as t}
<button
class="tournament-tab touch-target"
class:active={t.id === multiTournament.activeId}
role="tab"
aria-selected={t.id === multiTournament.activeId}
onclick={() => multiTournament.switchTo(t.id)}
>
<span
class="status-dot"
style="background-color: {statusColor(t.status)}"
aria-hidden="true"
></span>
<span class="tab-name">{t.name}</span>
</button>
{/each}
</div>
{/if}
<style>
.tournament-tabs {
display: flex;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
gap: var(--space-1);
padding: var(--space-2) var(--space-4);
background-color: var(--color-bg-sunken);
border-bottom: 1px solid var(--color-border);
}
.tournament-tabs::-webkit-scrollbar {
display: none;
}
.tournament-tab {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
background: none;
border: 1px solid transparent;
border-radius: var(--radius-md);
cursor: pointer;
white-space: nowrap;
color: var(--color-text-secondary);
font-size: var(--text-sm);
font-weight: 500;
transition:
background-color var(--transition-fast),
border-color var(--transition-fast),
color var(--transition-fast);
flex-shrink: 0;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.tournament-tab:hover:not(.active) {
background-color: var(--color-surface);
color: var(--color-text);
}
.tournament-tab.active {
background-color: var(--color-surface);
border-color: var(--color-primary);
color: var(--color-text);
font-weight: 600;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
flex-shrink: 0;
}
.tab-name {
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
</style>

View file

@ -0,0 +1,89 @@
/**
* Multi-tournament state for managing multiple active tournaments.
*
* Keeps all active tournament states in memory keyed by tournament ID
* for fast switching. WebSocket subscribes to all active tournaments;
* messages route to the correct state by tournament ID.
*/
import { tournament, type WSMessage } from './tournament.svelte';
export interface TournamentInfo {
id: string;
name: string;
status: 'active' | 'paused' | 'break' | 'completed';
}
class MultiTournamentState {
/** List of active tournaments. */
tournaments = $state<TournamentInfo[]>([]);
/** Currently selected tournament ID. */
activeId = $state<string | null>(null);
/** Whether multi-tournament mode is active (2+ tournaments). */
get isMulti(): boolean {
return this.tournaments.length >= 2;
}
/** The currently active tournament info. */
get activeTournament(): TournamentInfo | null {
return this.tournaments.find((t) => t.id === this.activeId) ?? null;
}
/**
* Set the list of active tournaments.
* If only one, auto-select it. If current selection is gone, select first.
*/
setTournaments(list: TournamentInfo[]): void {
this.tournaments = list;
if (list.length === 0) {
this.activeId = null;
} else if (list.length === 1) {
this.activeId = list[0].id;
} else if (!this.activeId || !list.find((t) => t.id === this.activeId)) {
this.activeId = list[0].id;
}
}
/** Switch to a different tournament. */
switchTo(tournamentId: string): void {
if (this.tournaments.find((t) => t.id === tournamentId)) {
this.activeId = tournamentId;
tournament.id = tournamentId;
}
}
/**
* Route a WebSocket message to the correct tournament state.
* Only updates the singleton state if the message is for the active tournament.
*/
routeMessage(msg: WSMessage): void {
const targetId = msg.tournament_id;
// Update tournament info from status messages
if (msg.type === 'tournament.status' && targetId) {
const data = msg.data as { name?: string; status?: string };
const existing = this.tournaments.find((t) => t.id === targetId);
if (existing) {
if (data.name) existing.name = data.name;
if (data.status) existing.status = data.status as TournamentInfo['status'];
}
}
// Route to singleton state only if it's for the active tournament
if (!targetId || targetId === this.activeId) {
tournament.handleMessage(msg);
}
}
/** Reset state. */
reset(): void {
this.tournaments = [];
this.activeId = null;
}
}
/** Singleton multi-tournament state. */
export const multiTournament = new MultiTournamentState();

View file

@ -0,0 +1,92 @@
/**
* Toast notification state using Svelte 5 runes.
*
* Provides success/info/warning/error notifications with auto-dismiss.
* Toasts stack vertically and animate in/out.
*/
export type ToastType = 'success' | 'info' | 'warning' | 'error';
export interface Toast {
id: string;
type: ToastType;
message: string;
duration: number;
dismissible: boolean;
timer?: ReturnType<typeof setTimeout>;
}
/** Default auto-dismiss durations by type (milliseconds). */
const DEFAULT_DURATIONS: Record<ToastType, number> = {
success: 3000,
info: 4000,
warning: 5000,
error: 8000
};
let nextId = 0;
class ToastState {
toasts = $state<Toast[]>([]);
/** Add a success toast (green, 3s auto-dismiss). */
success(message: string, duration?: number): string {
return this.add('success', message, duration);
}
/** Add an info toast (blue, 4s auto-dismiss). */
info(message: string, duration?: number): string {
return this.add('info', message, duration);
}
/** Add a warning toast (yellow, 5s auto-dismiss). */
warning(message: string, duration?: number): string {
return this.add('warning', message, duration);
}
/** Add an error toast (red, 8s auto-dismiss). */
error(message: string, duration?: number): string {
return this.add('error', message, duration);
}
/** Dismiss a toast by ID. */
dismiss(id: string): void {
const toast = this.toasts.find((t) => t.id === id);
if (toast?.timer) {
clearTimeout(toast.timer);
}
this.toasts = this.toasts.filter((t) => t.id !== id);
}
/** Dismiss all toasts. */
dismissAll(): void {
for (const t of this.toasts) {
if (t.timer) clearTimeout(t.timer);
}
this.toasts = [];
}
private add(type: ToastType, message: string, duration?: number): string {
const id = `toast-${++nextId}`;
const dur = duration ?? DEFAULT_DURATIONS[type];
const toast: Toast = {
id,
type,
message,
duration: dur,
dismissible: type === 'error'
};
// Auto-dismiss after duration
toast.timer = setTimeout(() => {
this.dismiss(id);
}, dur);
this.toasts = [...this.toasts, toast];
return id;
}
}
/** Singleton toast state instance. */
export const toast = new ToastState();

View file

@ -1,7 +1,110 @@
<script lang="ts">
import '../app.css';
import { auth } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { onMount } from 'svelte';
import Header from '$lib/components/Header.svelte';
import BottomTabs from '$lib/components/BottomTabs.svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
import FAB from '$lib/components/FAB.svelte';
import Toast from '$lib/components/Toast.svelte';
import TournamentTabs from '$lib/components/TournamentTabs.svelte';
import { toast } from '$lib/stores/toast.svelte';
let { children } = $props();
/** Whether current route is the login page (no shell shown). */
let isLoginPage = $derived((page.url?.pathname ?? '') === '/login');
/** Auth guard: redirect unauthenticated users to /login. */
onMount(() => {
if (!auth.isAuthenticated && !isLoginPage) {
goto('/login');
}
});
/** Handle FAB action dispatches. */
function handleFABAction(actionId: string): void {
switch (actionId) {
case 'bust':
toast.info('Bust flow: coming in Plan N');
break;
case 'buyin':
toast.info('Buy-in flow: coming in Plan N');
break;
case 'rebuy':
toast.info('Rebuy flow: coming in Plan N');
break;
case 'addon':
toast.info('Add-on flow: coming in Plan N');
break;
case 'pause-resume':
toast.info('Pause/Resume: coming in Plan N');
break;
default:
console.warn(`Unknown FAB action: ${actionId}`);
}
}
</script>
{#if isLoginPage}
<!-- Login page: no layout shell -->
{@render children()}
{:else if auth.isAuthenticated}
<!-- Authenticated layout shell -->
<div class="app-shell">
<Header />
<Sidebar />
<TournamentTabs />
<main class="main-content">
{@render children()}
</main>
<BottomTabs />
<FAB onaction={handleFABAction} />
</div>
{:else}
<!-- Redirecting to login -->
<div class="redirect-screen">
<p>Redirecting to login...</p>
</div>
{/if}
<!-- Toast notifications always visible -->
<Toast />
<style>
.app-shell {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
.main-content {
flex: 1;
/* Offset for fixed header */
padding-top: calc(var(--touch-target) + var(--space-2));
/* Offset for bottom tabs on mobile */
padding-bottom: calc(var(--touch-target) + var(--space-4) + env(safe-area-inset-bottom, 0px));
overflow-y: auto;
}
/* Desktop: offset for sidebar */
@media (min-width: 768px) {
.main-content {
margin-left: 220px;
/* No bottom padding on desktop (no bottom tabs) */
padding-bottom: var(--space-4);
}
}
.redirect-screen {
display: flex;
align-items: center;
justify-content: center;
min-height: 100dvh;
color: var(--color-text-muted);
}
</style>

View file

@ -1,45 +1,26 @@
<script lang="ts">
import { auth } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
/**
* Root page redirects to /overview.
* The layout shell handles auth guards.
*/
onMount(() => {
if (!auth.isAuthenticated) {
goto('/login');
}
goto('/overview', { replaceState: true });
});
</script>
{#if auth.isAuthenticated}
<main class="container">
<h1>Felt</h1>
<p class="text-secondary">Tournament management system</p>
<p>Welcome, {auth.operator?.name ?? 'Operator'}</p>
</main>
{:else}
<main class="container">
<p>Redirecting to login...</p>
</main>
{/if}
<div class="redirect">
<p>Loading...</p>
</div>
<style>
.container {
.redirect {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100dvh;
padding: 1rem;
gap: 0.5rem;
}
h1 {
font-size: 2rem;
font-weight: 700;
color: var(--color-primary);
}
.text-secondary {
color: var(--color-text-secondary);
min-height: 50dvh;
color: var(--color-text-muted);
}
</style>

View file

@ -0,0 +1,109 @@
<script lang="ts">
import { tournament } from '$lib/stores/tournament.svelte';
</script>
<div class="page-content">
<h2>Financials</h2>
<p class="text-secondary">Prize pool and payout information.</p>
{#if tournament.financials}
{@const fin = tournament.financials}
<div class="finance-grid">
<div class="finance-card">
<span class="finance-label">Total Buy-ins</span>
<span class="finance-value currency">{fin.total_buyin.toLocaleString()}</span>
</div>
<div class="finance-card">
<span class="finance-label">Total Rebuys</span>
<span class="finance-value currency">{fin.total_rebuys.toLocaleString()}</span>
</div>
<div class="finance-card">
<span class="finance-label">Total Add-ons</span>
<span class="finance-value currency">{fin.total_addons.toLocaleString()}</span>
</div>
<div class="finance-card highlight">
<span class="finance-label">Prize Pool</span>
<span class="finance-value currency prize">{fin.prize_pool.toLocaleString()}</span>
</div>
<div class="finance-card">
<span class="finance-label">House Fee</span>
<span class="finance-value currency">{fin.house_fee.toLocaleString()}</span>
</div>
<div class="finance-card">
<span class="finance-label">Paid Positions</span>
<span class="finance-value number">{fin.paid_positions}</span>
</div>
</div>
{:else}
<p class="empty-state">No financial data available yet.</p>
{/if}
</div>
<style>
.page-content {
padding: var(--space-4);
}
h2 {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text);
margin-bottom: var(--space-2);
}
.text-secondary {
color: var(--color-text-secondary);
font-size: var(--text-sm);
margin-bottom: var(--space-6);
}
.finance-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-3);
}
@media (min-width: 768px) {
.finance-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.finance-card {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--space-4);
background-color: var(--color-surface);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
}
.finance-card.highlight {
border-color: var(--color-prize);
}
.finance-label {
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.finance-value {
font-size: var(--text-xl);
font-weight: 700;
color: var(--color-text);
}
.finance-value.prize {
color: var(--color-prize);
}
.empty-state {
color: var(--color-text-muted);
font-style: italic;
padding: var(--space-8) 0;
text-align: center;
}
</style>

View file

@ -0,0 +1,107 @@
<script lang="ts">
import { auth } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
function handleLogout(): void {
auth.logout();
goto('/login');
}
</script>
<div class="page-content">
<h2>More</h2>
<p class="text-secondary">Settings and additional options.</p>
<div class="menu-list">
<div class="menu-item">
<span class="menu-label">Operator</span>
<span class="menu-value">{auth.operator?.name ?? 'Unknown'}</span>
</div>
<div class="menu-item">
<span class="menu-label">Role</span>
<span class="menu-value">{auth.operator?.role ?? 'Unknown'}</span>
</div>
<hr class="divider" />
<button class="menu-item menu-action danger touch-target" onclick={handleLogout}>
<span class="menu-label">Sign Out</span>
</button>
</div>
</div>
<style>
.page-content {
padding: var(--space-4);
}
h2 {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text);
margin-bottom: var(--space-2);
}
.text-secondary {
color: var(--color-text-secondary);
font-size: var(--text-sm);
margin-bottom: var(--space-6);
}
.menu-list {
display: flex;
flex-direction: column;
background-color: var(--color-surface);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
overflow: hidden;
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
min-height: var(--touch-target);
border-bottom: 1px solid var(--color-border);
}
.menu-item:last-child {
border-bottom: none;
}
.menu-action {
background: none;
border: none;
border-bottom: 1px solid var(--color-border);
cursor: pointer;
width: 100%;
text-align: left;
font-size: inherit;
font-family: inherit;
}
.menu-action:hover {
background-color: var(--color-surface-hover);
}
.menu-label {
font-size: var(--text-base);
color: var(--color-text);
}
.menu-value {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.danger .menu-label {
color: var(--color-error);
}
.divider {
border: none;
border-top: 1px solid var(--color-border);
margin: 0;
}
</style>

View file

@ -0,0 +1,92 @@
<script lang="ts">
import { tournament } from '$lib/stores/tournament.svelte';
</script>
<div class="page-content">
<h2>Overview</h2>
<p class="text-secondary">Tournament dashboard — detailed views coming in Plan N.</p>
{#if tournament.clock}
<div class="stats-grid">
<div class="stat-card">
<span class="stat-label">Players</span>
<span class="stat-value number">{tournament.remainingPlayers}/{tournament.totalPlayers}</span>
</div>
<div class="stat-card">
<span class="stat-label">Tables</span>
<span class="stat-value number">{tournament.activeTables}</span>
</div>
<div class="stat-card">
<span class="stat-label">Level</span>
<span class="stat-value number">{tournament.clock.level}</span>
</div>
<div class="stat-card">
<span class="stat-label">Blinds</span>
<span class="stat-value blinds">{tournament.clock.small_blind}/{tournament.clock.big_blind}</span>
</div>
</div>
{:else}
<p class="empty-state">No active tournament. Start or join a tournament to see the overview.</p>
{/if}
</div>
<style>
.page-content {
padding: var(--space-4);
}
h2 {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text);
margin-bottom: var(--space-2);
}
.text-secondary {
color: var(--color-text-secondary);
font-size: var(--text-sm);
margin-bottom: var(--space-6);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-3);
}
@media (min-width: 768px) {
.stats-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.stat-card {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--space-4);
background-color: var(--color-surface);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
}
.stat-label {
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.stat-value {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text);
}
.empty-state {
color: var(--color-text-muted);
font-style: italic;
padding: var(--space-8) 0;
text-align: center;
}
</style>

View file

@ -0,0 +1,51 @@
<script lang="ts">
import { tournament } from '$lib/stores/tournament.svelte';
import DataTable from '$lib/components/DataTable.svelte';
const columns = [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'status', label: 'Status', sortable: true },
{ key: 'chips', label: 'Chips', sortable: true, align: 'right' as const, render: (p: Record<string, unknown>) => (p['chips'] as number).toLocaleString() },
{ key: 'table_id', label: 'Table', hideMobile: true, sortable: true },
{ key: 'seat', label: 'Seat', hideMobile: true, sortable: true, align: 'center' as const },
{ key: 'rebuys', label: 'Rebuys', hideMobile: true, sortable: true, align: 'center' as const }
];
</script>
<div class="page-content">
<h2>Players</h2>
<p class="text-secondary">Registered players and chip counts.</p>
<DataTable
{columns}
data={tournament.players}
sortable={true}
searchable={true}
loading={false}
emptyMessage="No players registered yet"
rowKey={(item) => String(item['id'])}
swipeActions={[
{ id: 'bust', label: 'Bust', color: 'var(--color-error)', handler: () => {} },
{ id: 'rebuy', label: 'Rebuy', color: 'var(--color-primary)', handler: () => {} }
]}
/>
</div>
<style>
.page-content {
padding: var(--space-4);
}
h2 {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text);
margin-bottom: var(--space-2);
}
.text-secondary {
color: var(--color-text-secondary);
font-size: var(--text-sm);
margin-bottom: var(--space-4);
}
</style>

View file

@ -0,0 +1,52 @@
<script lang="ts">
import { tournament } from '$lib/stores/tournament.svelte';
import DataTable from '$lib/components/DataTable.svelte';
const columns = [
{ key: 'number', label: 'Table #', sortable: true, align: 'center' as const },
{ key: 'seats', label: 'Seats', sortable: true, align: 'center' as const },
{ key: 'player_count', label: 'Players', sortable: true, align: 'center' as const },
{ key: 'is_final_table', label: 'Final', hideMobile: true, sortable: true, align: 'center' as const, render: (t: Record<string, unknown>) => t['is_final_table'] ? 'Yes' : '' }
];
let tableData = $derived(
tournament.tables.map((t) => ({
...t,
player_count: t.players.length
}))
);
</script>
<div class="page-content">
<h2>Tables</h2>
<p class="text-secondary">Active tables and seating.</p>
<DataTable
{columns}
data={tableData}
sortable={true}
searchable={false}
loading={false}
emptyMessage="No tables set up yet"
rowKey={(item) => String(item['id'])}
/>
</div>
<style>
.page-content {
padding: var(--space-4);
}
h2 {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text);
margin-bottom: var(--space-2);
}
.text-secondary {
color: var(--color-text-secondary);
font-size: var(--text-sm);
margin-bottom: var(--space-4);
}
</style>