Jimin Huang commited on
Commit
a1c02fd
Β·
1 Parent(s): 50c8fe7

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +65 -54
src/views/LiveView.vue CHANGED
@@ -35,53 +35,47 @@
35
  class="card2"
36
  :class="{ 'is-bh': c.kind==='bh', 'is-winner': c.isWinner }"
37
  >
38
- <!-- WINNER RIBBON -->
39
  <div v-if="c.isWinner" class="card2__ribbon" aria-label="Top performer">πŸ‘‘</div>
40
 
41
- <!-- TOP ROW: avatar + titles + primary metric -->
42
- <div class="card2__row card2__row--top">
43
- <div class="info">
44
- <div class="avatar">
45
- <img v-if="c.logo" :src="c.logo" alt="" />
46
- <div v-else class="avatar__fallback" aria-hidden="true"></div>
47
- </div>
48
- <div class="titles">
49
- <div class="title" :title="c.title">{{ c.title }}</div>
50
- <div class="subtitle" :title="c.subtitle">{{ c.subtitle }}</div>
51
- </div>
52
  </div>
 
 
 
 
 
53
 
54
- <div class="metrics">
55
- <div class="primary">
56
- <div class="primary__label">Balance</div>
57
- <div class="primary__value">{{ fmtUSD(c.balance) }}</div>
58
- </div>
59
- <div class="delta" v-if="c.kind==='agent' && c.gapUsd != null">
60
- <span
61
- class="delta__value"
62
- :class="{ neg: c.gapUsd < 0 && !c.isWinner, pos: c.gapUsd >= 0 || c.isWinner }"
63
- >
 
 
 
 
 
 
64
  <template v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</template>
65
  <template v-else>{{ signedPct(c.gapPct) }}</template>
66
- </span>
67
- <span class="delta__label">vs B&H</span>
68
  </div>
69
  </div>
70
  </div>
71
 
72
- <!-- META ROW: type chip -->
73
- <div class="card2__row card2__row--meta">
74
- <div class="chips">
75
- <span v-if="c.kind==='bh'" class="chip chip--neutral">Buy & Hold</span>
76
- <span v-else class="chip chip--outline">Strategy</span>
77
- </div>
78
- </div>
79
-
80
- <!-- FOOTER: date -->
81
- <div class="card2__row card2__row--foot">
82
- <div class="foot__left"></div>
83
- <div class="foot__right">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
84
- </div>
85
  </div>
86
  </div>
87
  </section>
@@ -89,6 +83,8 @@
89
  <section v-else class="panel panel--cards">
90
  <div class="empty">No card data yet for <strong>{{ asset }}</strong>.</div>
91
  </section>
 
 
92
  </div>
93
  </template>
94
 
@@ -259,11 +255,18 @@ watch(
259
  balance: first.perf.bhLast,
260
  date: first.perf.date,
261
  logo: ASSET_ICONS[assetCode] || null,
 
 
 
262
  isWinner: false
263
  }
264
 
265
  // Agent cards (gap vs BH)
266
  const agentCards = perfs.map(({ sel, perf }) => {
 
 
 
 
267
  const gapUsd = perf.stratLast - perf.bhLast
268
  const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
269
  return {
@@ -318,17 +321,18 @@ watch(
318
  .empty { padding: 14px; border: 1px dashed #D6DAE1; border-radius: 12px; color: #6B7280; font-size: .92rem; }
319
 
320
  /* NEW GRID: fluid, auto-fit tiles */
321
- .cards-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
322
 
323
  /* CARD v2 */
324
  .card2 {
325
  position: relative;
326
  display: grid;
327
  grid-template-rows: auto auto auto; /* rows: top, meta, foot */
328
- gap: 12px;
329
- padding: 14px;
 
330
  border: 1px solid #EEF1F6;
331
- border-radius: 14px;
332
  background: #fff;
333
  box-shadow: 0 1px 2px rgba(0,0,0,.04);
334
  transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;
@@ -343,33 +347,30 @@ watch(
343
  /* row layout */
344
  .card2__row { display: grid; align-items: center; }
345
  .card2__row--top {
346
- grid-template-columns: 48px minmax(0,1fr) auto; /* avatar | titles | metrics */
347
  column-gap: 12px;
348
  }
349
  .card2__row--meta { grid-template-columns: 1fr; }
350
  .card2__row--foot { grid-template-columns: 1fr auto; font-size: 12px; color: #5B6476; opacity: .9; margin-top: -4px; }
351
 
352
  /* avatar */
353
- .avatar { width: 44px; height: 44px; border-radius: 999px; background: #F3F4F6; display: grid; place-items: center; overflow: hidden; }
354
  .avatar img { width: 100%; height: 100%; object-fit: contain; }
355
  .avatar__fallback { width: 60%; height: 60%; border-radius: 999px; background: #E5E7EB; }
356
 
357
  /* titles */
358
- .titles { min-width: 0; display: grid; gap: 2px; }
359
- .title {
360
- font-weight: 800; color: #0F172A; line-height: 1.15;
361
- display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
362
- overflow: hidden; font-size: clamp(16px, 1.4vw, 18px);
363
- }
364
- .subtitle { font-size: 12px; color: #5B6476; opacity: .9; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
365
 
366
  /* primary metric */
367
  .primary { text-align: right; }
368
- .primary__label { font-size: 11px; letter-spacing: .02em; color: #6B7280; }
369
- .primary__value { font-weight: 900; color: #0F172A; font-size: clamp(18px, 1.8vw, 22px); white-space: nowrap; }
370
 
371
- /* metrics wrapper (balance + delta in same row) */
372
- .metrics { display: flex; align-items: baseline; gap: 12px; justify-content: flex-end; }
373
 
374
  /* chips & delta */
375
  .chips { display: flex; align-items: center; gap: 8px; min-width: 0; }
@@ -387,6 +388,16 @@ watch(
387
  .chip--neutral { background: #EEF2F7; color: #0F172A; }
388
  .chip--outline { background: transparent; color: #475569; border: 1px solid #E5E7EB; padding: 3px 8px; }
389
 
 
 
 
 
 
 
 
 
 
 
390
  .delta { display: flex; align-items: baseline; gap: 8px; }
391
  .delta__value { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
392
  .delta__value.pos { background: #DCFCE7; color: #166534; }
 
35
  class="card2"
36
  :class="{ 'is-bh': c.kind==='bh', 'is-winner': c.isWinner }"
37
  >
38
+ <!-- Crown -->
39
  <div v-if="c.isWinner" class="card2__ribbon" aria-label="Top performer">πŸ‘‘</div>
40
 
41
+ <!-- Header: LOGO + title -->
42
+ <div class="card2__header">
43
+ <div class="avatar">
44
+ <img v-if="c.logo" :src="c.logo" alt="" />
45
+ <div v-else class="avatar__fallback" aria-hidden="true"></div>
 
 
 
 
 
 
46
  </div>
47
+ <div class="titleblock">
48
+ <div class="title" :title="c.kind==='bh' ? 'Buy & Hold' : c.title">{{ c.kind==='bh' ? 'Buy & Hold' : c.title }}</div>
49
+ <div class="subtitle" :title="c.subtitle">{{ c.subtitle }}</div>
50
+ </div>
51
+ </div>
52
 
53
+ <!-- KPIs: Net value, Profit, Delta Profit -->
54
+ <div class="card2__kpis">
55
+ <div class="kpi kpi--net">
56
+ <div class="kpi__label">Net value</div>
57
+ <div class="kpi__value">{{ fmtUSD(c.balance) }}</div>
58
+ </div>
59
+
60
+ <div class="kpi kpi--profit">
61
+ <div class="kpi__label">Profit</div>
62
+ <div class="pill" :class="{ pos: c.profitUsd >= 0, neg: c.profitUsd < 0 }">{{ signedMoney(c.profitUsd) }}</div>
63
+ </div>
64
+
65
+ <div class="kpi kpi--delta">
66
+ <div class="kpi__label">Delta profit</div>
67
+ <div class="pill" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
68
+ <template v-if="c.kind==='agent'">
69
  <template v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</template>
70
  <template v-else>{{ signedPct(c.gapPct) }}</template>
71
+ </template>
72
+ <template v-else>β€”</template>
73
  </div>
74
  </div>
75
  </div>
76
 
77
+ <!-- EOD bottom-right -->
78
+ <div class="card2__eod">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
 
 
 
 
 
 
 
 
 
 
 
79
  </div>
80
  </div>
81
  </section>
 
83
  <section v-else class="panel panel--cards">
84
  <div class="empty">No card data yet for <strong>{{ asset }}</strong>.</div>
85
  </section>
86
+ <div class="empty">No card data yet for <strong>{{ asset }}</strong>.</div>
87
+ </section>
88
  </div>
89
  </template>
90
 
 
255
  balance: first.perf.bhLast,
256
  date: first.perf.date,
257
  logo: ASSET_ICONS[assetCode] || null,
258
+ profitUsd: (first.perf.bhLast ?? 0) - 100000,
259
+ gapUsd: 0,
260
+ gapPct: 0,
261
  isWinner: false
262
  }
263
 
264
  // Agent cards (gap vs BH)
265
  const agentCards = perfs.map(({ sel, perf }) => {
266
+ const gapUsd = perf.stratLast - perf.bhLast
267
+ const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
268
+ const profitUsd = (perf.stratLast ?? 0) - 100000
269
+ return { => {
270
  const gapUsd = perf.stratLast - perf.bhLast
271
  const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
272
  return {
 
321
  .empty { padding: 14px; border: 1px dashed #D6DAE1; border-radius: 12px; color: #6B7280; font-size: .92rem; }
322
 
323
  /* NEW GRID: fluid, auto-fit tiles */
324
+ .cards-grid { display: grid; gap: 16px; grid-template-columns: repeat(5, minmax(0, 1fr)); }
325
 
326
  /* CARD v2 */
327
  .card2 {
328
  position: relative;
329
  display: grid;
330
  grid-template-rows: auto auto auto; /* rows: top, meta, foot */
331
+ gap: 14px;
332
+ padding: 16px;
333
+ min-height: 200px;
334
  border: 1px solid #EEF1F6;
335
+ border-radius: 16px;
336
  background: #fff;
337
  box-shadow: 0 1px 2px rgba(0,0,0,.04);
338
  transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;
 
347
  /* row layout */
348
  .card2__row { display: grid; align-items: center; }
349
  .card2__row--top {
350
+ grid-template-columns: 1fr auto; /* info | metrics */
351
  column-gap: 12px;
352
  }
353
  .card2__row--meta { grid-template-columns: 1fr; }
354
  .card2__row--foot { grid-template-columns: 1fr auto; font-size: 12px; color: #5B6476; opacity: .9; margin-top: -4px; }
355
 
356
  /* avatar */
357
+ .avatar { width: 56px; height: 56px; border-radius: 999px; background: #F3F4F6; display: grid; place-items: center; overflow: hidden; }
358
  .avatar img { width: 100%; height: 100%; object-fit: contain; }
359
  .avatar__fallback { width: 60%; height: 60%; border-radius: 999px; background: #E5E7EB; }
360
 
361
  /* titles */
362
+ .info { display: grid; grid-template-columns: 56px minmax(0,1fr); align-items: center; gap: 12px; min-width: 0; }
363
+ .titles { min-width: 0; display: grid; gap: 4px; }
364
+ .title { font-weight: 900; color: #0F172A; line-height: 1.1; font-size: clamp(18px, 1.6vw, 22px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
365
+ .subtitle { font-size: 13px; color: #64748B; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
 
 
 
366
 
367
  /* primary metric */
368
  .primary { text-align: right; }
369
+ .primary__label { font-size: 12px; letter-spacing: .02em; color: #6B7280; }
370
+ .primary__value { font-weight: 900; color: #0F172A; font-size: clamp(22px, 2.2vw, 28px); white-space: nowrap; }
371
 
372
+ /* metrics wrapper */
373
+ .metrics { display: flex; align-items: center; gap: 12px; justify-content: flex-end; }
374
 
375
  /* chips & delta */
376
  .chips { display: flex; align-items: center; gap: 8px; min-width: 0; }
 
388
  .chip--neutral { background: #EEF2F7; color: #0F172A; }
389
  .chip--outline { background: transparent; color: #475569; border: 1px solid #E5E7EB; padding: 3px 8px; }
390
 
391
+ .delta { display: flex; align-items: baseline; gap: 8px; }
392
+ .delta__value { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
393
+ .delta__value.pos { background: #DCFCE7; color: #166534; }
394
+ .delta__value.neg { background: #FEE2E2; color: #B91C1C; }
395
+ .delta__label { font-size: 11px; color: #64748B; }
396
+ .chips { display: flex; align-items: center; gap: 8px; min-width: 0; }
397
+ .chip { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
398
+ .chip--neutral { background: #EEF2F7; color: #0F172A; }
399
+ .chip--outline { background: transparent; color: #475569; border: 1px solid #E5E7EB; padding: 3px 8px; }
400
+
401
  .delta { display: flex; align-items: baseline; gap: 8px; }
402
  .delta__value { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
403
  .delta__value.pos { background: #DCFCE7; color: #166534; }