@seasonkoh/webaz 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +2 -2
  2. package/NOTICE +24 -3
  3. package/README.md +74 -330
  4. package/README.zh-CN.md +419 -0
  5. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
  6. package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
  7. package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +69 -0
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +270 -82
  10. package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
  11. package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
  12. package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
  13. package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
  14. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
  15. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
  16. package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
  20. package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
  21. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
  22. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
  23. package/dist/ledger.js +1 -1
  24. package/dist/pwa/admin-audit.js +38 -0
  25. package/dist/pwa/anti-abuse-thresholds.js +135 -0
  26. package/dist/pwa/cf-origin-guard.js +33 -0
  27. package/dist/pwa/contract-fingerprint.js +1 -0
  28. package/dist/pwa/data/onboarding-cases.js +2 -2
  29. package/dist/pwa/data/onboarding-quiz.js +1 -1
  30. package/dist/pwa/economic-participation.js +2 -2
  31. package/dist/pwa/integration-contract.js +46 -4
  32. package/dist/pwa/internal/pv-settlement.js +12 -0
  33. package/dist/pwa/internal/wallet-signer.js +26 -0
  34. package/dist/pwa/public/app-account.js +977 -0
  35. package/dist/pwa/public/app-admin.js +608 -0
  36. package/dist/pwa/public/app-agents.js +63 -0
  37. package/dist/pwa/public/app-ai.js +2162 -0
  38. package/dist/pwa/public/app-contribution.js +836 -0
  39. package/dist/pwa/public/app-discover.js +1296 -0
  40. package/dist/pwa/public/app-listings.js +226 -0
  41. package/dist/pwa/public/app-profile.js +1692 -0
  42. package/dist/pwa/public/app-seller.js +199 -0
  43. package/dist/pwa/public/app-shop.js +1145 -0
  44. package/dist/pwa/public/app.js +15075 -23960
  45. package/dist/pwa/public/i18n.js +31 -28
  46. package/dist/pwa/public/index.html +11 -1
  47. package/dist/pwa/public/openapi.json +4851 -2776
  48. package/dist/pwa/pv-kill-switch.js +31 -0
  49. package/dist/pwa/routes/admin-admins.js +48 -1
  50. package/dist/pwa/routes/admin-analytics.js +1 -10
  51. package/dist/pwa/routes/admin-atomic.js +4 -17
  52. package/dist/pwa/routes/admin-operator-claims.js +280 -0
  53. package/dist/pwa/routes/admin-reports.js +4 -26
  54. package/dist/pwa/routes/admin-tokenomics.js +2 -76
  55. package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
  56. package/dist/pwa/routes/admin-users-query.js +23 -1
  57. package/dist/pwa/routes/admin-wallet-ops.js +1 -1
  58. package/dist/pwa/routes/agent-grants.js +255 -0
  59. package/dist/pwa/routes/auth-read.js +1 -5
  60. package/dist/pwa/routes/auth-register.js +3 -13
  61. package/dist/pwa/routes/build-task-quota.js +113 -0
  62. package/dist/pwa/routes/claim-verify.js +15 -11
  63. package/dist/pwa/routes/contribution-facts.js +18 -0
  64. package/dist/pwa/routes/dispute-cases.js +5 -4
  65. package/dist/pwa/routes/growth.js +3 -3
  66. package/dist/pwa/routes/orders-action.js +27 -10
  67. package/dist/pwa/routes/orders-create.js +1 -1
  68. package/dist/pwa/routes/products-meta.js +19 -6
  69. package/dist/pwa/routes/profile-placement.js +1 -1
  70. package/dist/pwa/routes/promoter.js +10 -29
  71. package/dist/pwa/routes/public-build-tasks.js +5 -1
  72. package/dist/pwa/routes/public-utils.js +9 -12
  73. package/dist/pwa/routes/referral.js +5 -26
  74. package/dist/pwa/routes/rewards-apply.js +3 -2
  75. package/dist/pwa/routes/share-redirects.js +1 -1
  76. package/dist/pwa/routes/shareables-interactions.js +2 -1
  77. package/dist/pwa/routes/task-proposals.js +85 -9
  78. package/dist/pwa/routes/users-public.js +1 -4
  79. package/dist/pwa/routes/wallet-read.js +2 -14
  80. package/dist/pwa/routes/webauthn.js +7 -2
  81. package/dist/pwa/server-schema.js +9 -0
  82. package/dist/pwa/server.js +319 -2034
  83. package/dist/runtime/agent-grant-scopes.js +128 -0
  84. package/dist/runtime/agent-grant-verifier.js +67 -0
  85. package/dist/runtime/agent-pairing.js +60 -0
  86. package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
  87. package/dist/runtime/webaz-schema-helpers.js +1848 -0
  88. package/dist/settlement-math.js +3 -3
  89. package/dist/version.js +6 -4
  90. package/package.json +43 -8
  91. package/dist/index.js +0 -182
  92. package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
  93. package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
  94. package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
  95. package/dist/test-dispute.js +0 -153
  96. package/dist/test-manifest.js +0 -61
  97. package/dist/test-mcp-tools.js +0 -135
  98. package/dist/test-reputation.js +0 -116
  99. package/dist/test-skill-market.js +0 -101
@@ -0,0 +1,199 @@
1
+ // WebAZ — Seller read-only analytics / store-reviews display (classic split, slice K / app-seller.js)
2
+ //
3
+ // Loaded as a CLASSIC script in this order (index.html):
4
+ // i18n → app-admin → app-contribution → app-ai → app-discover → app-profile → app-account → app-shop → app-listings → app-seller → app.js (source of truth: index.html)
5
+ // Top-level functions are global; pages run on route/click (after app.js loads),
6
+ // so cross-file globals (GET/state/shell/escHtml/t/fmtTime/submitSellerReviewReply/
7
+ // ...) resolve at call time. No import/export.
8
+ //
9
+ // READ-ONLY only: renderSellerAnalytics (GET /sellers/me/analytics) + its review
10
+ // hydration hydrateSellerReviews (GET /sellers/me/ratings). The reply WRITE handler
11
+ // submitSellerReviewReply (POST /orders/:id/rating/reply) stays in app.js and is
12
+ // reached cross-file via the onclick these read-only views render.
13
+ //
14
+ // INTENTIONALLY LEFT in app.js (money/order/status/product-mutation — never moved):
15
+ // renderSeller (the full seller workbench), renderSellerTrials (trial-campaign
16
+ // create/delete + /charity/fund/donate), renderSellerFlashSales (flash-sale config),
17
+ // sellerDeclineContestPanel (order decline/status), and all order/shipping/refund/
18
+ // dispute/wallet/withdraw/settlement/escrow handlers. No UI/behavior change.
19
+
20
+ // Wave C-5: 卖家销售分析
21
+ async function renderSellerAnalytics(app) {
22
+ if (!state.user || state.user.role !== 'seller') { app.innerHTML = shell(`<div class="empty">${t('仅卖家可访问')}</div>`, 'me'); return }
23
+ app.innerHTML = shell(loading$(), 'me')
24
+ const win = state._analyticsWindow || 30
25
+ const r = await GET(`/sellers/me/analytics?window=${win}`)
26
+ if (r.error) { app.innerHTML = shell(alert$('error', r.error), 'me'); return }
27
+ const fmt = (v) => Number(v || 0).toFixed(0)
28
+ const fmt2 = (v) => Number(v || 0).toFixed(2)
29
+ const pct = (v) => (Number(v || 0) * 100).toFixed(1) + '%'
30
+ // S1 增量箭头 — prev_window 对比
31
+ const delta = (cur, prev) => {
32
+ if (!prev || prev === 0) return cur > 0 ? `<span style="color:#16a34a;font-size:10px">${t('新')}</span>` : ''
33
+ const d = (cur - prev) / prev
34
+ const arrow = d > 0.01 ? '↑' : d < -0.01 ? '↓' : '·'
35
+ const color = d > 0.01 ? '#16a34a' : d < -0.01 ? '#dc2626' : '#9ca3af'
36
+ return `<span style="color:${color};font-size:10px;font-weight:600;margin-left:4px" title="${t('对比上一')}${win}${t('天')}">${arrow} ${Math.abs(d * 100).toFixed(0)}%</span>`
37
+ }
38
+
39
+ // 简易柱状图(按 day_trend.gmv 缩放)
40
+ const trend = r.daily_trend || []
41
+ const maxGmv = Math.max(1, ...trend.map(d => Number(d.gmv)))
42
+ const trendBars = trend.length === 0
43
+ ? `<div style="font-size:11px;color:#9ca3af;text-align:center;padding:14px">${t('暂无数据')}</div>`
44
+ : `<div style="display:flex;gap:3px;align-items:flex-end;height:80px">
45
+ ${trend.map(d => {
46
+ const h = Math.max(3, (Number(d.gmv) / maxGmv) * 70)
47
+ return `<div title="${d.date}: ${Number(d.gmv).toFixed(0)} WAZ · ${d.orders} ${t('单')}" style="flex:1;background:linear-gradient(180deg,#6366f1,#a5b4fc);height:${h}px;border-radius:2px 2px 0 0;cursor:help"></div>`
48
+ }).join('')}
49
+ </div>
50
+ <div style="display:flex;justify-content:space-between;font-size:9px;color:#9ca3af;margin-top:4px">
51
+ <span>${trend[0]?.date || ''}</span>
52
+ <span>${trend[trend.length - 1]?.date || ''}</span>
53
+ </div>`
54
+
55
+ const topRows = (r.top_products || []).length === 0
56
+ ? `<div style="font-size:11px;color:#9ca3af;text-align:center;padding:14px">${t('暂无完成订单')}</div>`
57
+ : (r.top_products || []).map((p, i) => `
58
+ <div style="display:flex;gap:8px;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6">
59
+ <div style="font-size:13px;font-weight:700;color:${i === 0 ? '#f59e0b' : i < 3 ? '#6366f1' : '#9ca3af'};width:22px;text-align:center">${i + 1}</div>
60
+ <div style="flex:1;min-width:0">
61
+ <div style="font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(p.title)}</div>
62
+ <div style="font-size:10px;color:#9ca3af">${p.price} WAZ</div>
63
+ </div>
64
+ <div style="text-align:right">
65
+ <div style="font-size:13px;font-weight:600;color:#16a34a">${p.sales} ${t('单')}</div>
66
+ <div style="font-size:10px;color:#9ca3af">${fmt2(p.revenue)} WAZ</div>
67
+ </div>
68
+ </div>`).join('')
69
+
70
+ const winSelector = [7, 30, 90, 180].map(w => `
71
+ <button class="btn btn-sm" style="font-size:11px;padding:4px 10px;${win === w ? 'background:#4f46e5;color:#fff' : 'background:#fff;color:#374151;border:1px solid #d1d5db'}" onclick="switchAnalyticsWindow(${w})">${w}${t('天')}</button>
72
+ `).join('')
73
+
74
+ app.innerHTML = shell(`
75
+ <h1 class="page-title">📊 ${t('销售分析')}</h1>
76
+ <div style="display:flex;gap:6px;margin-bottom:12px">${winSelector}</div>
77
+
78
+ <div class="card" style="background:linear-gradient(135deg,#eef2ff,#fff);padding:14px;margin-bottom:10px">
79
+ <div style="font-size:14px;font-weight:600;margin-bottom:8px">💰 ${t('核心指标')}</div>
80
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center">
81
+ <div>
82
+ <div style="font-size:18px;font-weight:700;color:#4f46e5">${fmt2(r.orders.gmv)}${delta(Number(r.orders.gmv), Number(r.prev_window?.gmv))}</div>
83
+ <div style="font-size:10px;color:#6b7280">GMV (WAZ)</div>
84
+ </div>
85
+ <div>
86
+ <div style="font-size:18px;font-weight:700;color:#16a34a">${fmt(r.orders.completed_orders)}${delta(Number(r.orders.completed_orders), Number(r.prev_window?.completed_orders))}</div>
87
+ <div style="font-size:10px;color:#6b7280">${t('完成订单')}</div>
88
+ </div>
89
+ <div>
90
+ <div style="font-size:18px;font-weight:700;color:#f59e0b">${fmt2(r.orders.aov)}</div>
91
+ <div style="font-size:10px;color:#6b7280">${t('客单价')} (WAZ)</div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- S1 新增:履约 & 质量 -->
97
+ <div class="card" style="padding:14px;margin-bottom:10px">
98
+ <div style="font-size:13px;font-weight:600;margin-bottom:8px">⚙️ ${t('履约 & 质量')}</div>
99
+ <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px;text-align:center;font-size:12px">
100
+ <div title="${t('paid → shipped 平均耗时')}">
101
+ <div style="font-size:18px;font-weight:700;color:${Number(r.fulfillment?.avg_handling_hours||0)<=24?'#16a34a':Number(r.fulfillment?.avg_handling_hours||0)<=72?'#d97706':'#dc2626'}">${r.fulfillment?.sample_n > 0 ? Number(r.fulfillment.avg_handling_hours).toFixed(1) + 'h' : '—'}</div>
102
+ <div style="font-size:10px;color:#9ca3af">${t('平均备货时长')}${r.fulfillment?.sample_n > 0 ? ` (n=${r.fulfillment.sample_n})` : ''}</div>
103
+ </div>
104
+ <div title="${t('refunded / completed')}">
105
+ <div style="font-size:18px;font-weight:700;color:${Number(r.quality?.return_rate||0)<=0.05?'#16a34a':Number(r.quality?.return_rate||0)<=0.15?'#d97706':'#dc2626'}">${r.quality?.completed > 0 ? pct(r.quality.return_rate) : '—'}</div>
106
+ <div style="font-size:10px;color:#9ca3af">${t('退货率')}${r.quality?.completed > 0 ? ` (${r.quality.refunds}/${r.quality.completed})` : ''}</div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="card" style="padding:14px;margin-bottom:10px">
112
+ <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:8px">
113
+ <div style="font-size:13px;font-weight:600">📈 ${t('每日 GMV 趋势')}</div>
114
+ <div style="font-size:10px;color:#9ca3af">${t('最近')} ${Math.min(win, 30)} ${t('天(按日)')}</div>
115
+ </div>
116
+ ${trendBars}
117
+ ${win > 30 ? `<div style="font-size:10px;color:#9ca3af;margin-top:6px;text-align:center">${t('日粒度仅展示最近 30 天;汇总指标按完整窗口计算')}</div>` : ''}
118
+ </div>
119
+
120
+ <div class="card" style="padding:14px;margin-bottom:10px">
121
+ <div style="font-size:13px;font-weight:600;margin-bottom:8px">🔄 ${t('客户结构')}</div>
122
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center;font-size:12px">
123
+ <div><div style="font-weight:700;color:#374151">${r.buyers.unique}</div><div style="font-size:10px;color:#9ca3af">${t('独立客户')}</div></div>
124
+ <div><div style="font-weight:700;color:#16a34a">${r.buyers.repeat}</div><div style="font-size:10px;color:#9ca3af">${t('复购客户')}</div></div>
125
+ <div><div style="font-weight:700;color:#4f46e5">${pct(r.buyers.repeat_rate)}</div><div style="font-size:10px;color:#9ca3af">${t('复购率')}</div></div>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="card" style="padding:14px;margin-bottom:10px">
130
+ <div style="font-size:13px;font-weight:600;margin-bottom:8px">🎯 ${t('意向转化')}</div>
131
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center;font-size:12px">
132
+ <div><div style="font-weight:700;color:#dc2626">${r.funnel.wishlist_adds}</div><div style="font-size:10px;color:#9ca3af">❤ ${t('心愿单加入')}</div></div>
133
+ <div><div style="font-weight:700;color:#f59e0b">${r.funnel.orders}</div><div style="font-size:10px;color:#9ca3af">📦 ${t('总下单')}</div></div>
134
+ <div><div style="font-weight:700;color:#16a34a">${r.funnel.completed}</div><div style="font-size:10px;color:#9ca3af">✓ ${t('完成')}</div></div>
135
+ </div>
136
+ <div style="margin-top:6px;font-size:11px;color:#9ca3af;text-align:center">${t('心愿单 → 下单转化率')}: ${r.funnel.wishlist_adds > 0 ? pct(r.funnel.orders / r.funnel.wishlist_adds) : '—'}</div>
137
+ </div>
138
+
139
+ <div class="card" style="padding:14px;margin-bottom:10px">
140
+ <div style="font-size:13px;font-weight:600;margin-bottom:8px">🏆 ${t('热销 Top 10')}</div>
141
+ ${topRows}
142
+ </div>
143
+
144
+ <div class="card" style="padding:14px;margin-bottom:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px">
145
+ <div style="text-align:center">
146
+ <div style="font-size:18px;font-weight:700;color:#f59e0b">${Number(r.ratings.avg_stars || 0).toFixed(1)} ⭐</div>
147
+ <div style="font-size:10px;color:#9ca3af">${t('评价')} (${r.ratings.cnt})</div>
148
+ </div>
149
+ <div style="text-align:center">
150
+ <div style="font-size:18px;font-weight:700;color:#dc2626">${r.refunds}</div>
151
+ <div style="font-size:10px;color:#9ca3af">${t('退款')}</div>
152
+ </div>
153
+ </div>
154
+
155
+ <div class="card" style="padding:14px;margin-bottom:10px">
156
+ <div style="font-size:13px;font-weight:600;margin-bottom:8px">⭐ ${t('店铺评价')} <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(买家评价 · 每条可回应一次)')}</span></div>
157
+ <div id="seller-reviews-area" style="font-size:12px;color:#6b7280">${loading$()}</div>
158
+ </div>
159
+ `, 'me')
160
+ hydrateSellerReviews()
161
+ }
162
+
163
+ // 店铺评价汇总 + 逐条回应(P2)。复用既有 POST /orders/:order_id/rating/reply(卖家一回一限);
164
+ // 读 /sellers/me/ratings(authed,含 order_id)。不改评价 / 资金逻辑。
165
+ async function hydrateSellerReviews() {
166
+ const area = document.getElementById('seller-reviews-area')
167
+ if (!area) return
168
+ const r = await GET('/sellers/me/ratings?limit=50').catch(() => null)
169
+ const items = Array.isArray(r?.items) ? r.items : []
170
+ if (items.length === 0) { area.innerHTML = `<div style="color:#9ca3af;text-align:center;padding:12px">${t('暂无评价')}</div>`; return }
171
+ const unreplied = Number(r?.agg?.unreplied || 0)
172
+ const starStr = (n) => '★'.repeat(Math.max(0, Math.min(5, Number(n) || 0))) + '☆'.repeat(5 - Math.max(0, Math.min(5, Number(n) || 0)))
173
+ area.innerHTML = `
174
+ ${unreplied > 0 ? `<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-bottom:8px">📝 ${unreplied} ${t('条评价待回应')}</div>` : ''}
175
+ ${items.map(it => it.masked ? `
176
+ <div style="border:1px solid #f3f4f6;border-radius:8px;padding:10px;margin-bottom:8px;background:#fafafa">
177
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:6px;margin-bottom:4px">
178
+ <span style="font-size:12px;color:#6b7280">🔒 ${t('评价双盲遮蔽中')}</span>
179
+ <span style="font-size:10px;color:#9ca3af">${fmtTime(it.created_at)}</span>
180
+ </div>
181
+ <div style="font-size:11px;color:#9ca3af;margin-bottom:4px">📦 ${escHtml(it.product_title || '')}</div>
182
+ <div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px">${t('买家已评价,但需你先评价买家,或盲评期结束后才能查看与回应(防互相影响打分)。')}</div>
183
+ </div>` : `
184
+ <div style="border:1px solid #f3f4f6;border-radius:8px;padding:10px;margin-bottom:8px">
185
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:6px;margin-bottom:4px">
186
+ <span style="color:#f59e0b;font-size:13px">${starStr(it.stars)}</span>
187
+ <span style="font-size:10px;color:#9ca3af">${fmtTime(it.created_at)}</span>
188
+ </div>
189
+ <div style="font-size:11px;color:#6b7280;margin-bottom:4px">@${escHtml(it.buyer_handle || it.buyer_name || '')} · 📦 ${escHtml(it.product_title || '')}</div>
190
+ ${it.comment ? `<div style="font-size:12px;color:#374151;margin-bottom:6px">${escHtml(it.comment)}</div>` : `<div style="font-size:12px;color:#9ca3af;margin-bottom:6px">${t('买家未留言')}</div>`}
191
+ ${it.reply ? `<div style="background:#f0f9ff;border-radius:6px;padding:6px 8px;font-size:12px;color:#0369a1"><strong>${t('你的回应')}:</strong>${escHtml(it.reply)}</div>${it.buyer_followup ? `<div style="background:#fafafa;border-radius:6px;padding:6px 8px;font-size:12px;color:#374151;margin-top:4px"><strong>${t('买家追问')}:</strong>${escHtml(it.buyer_followup)}</div>` : ''}` : `
192
+ <div style="display:flex;gap:6px;align-items:flex-end">
193
+ <textarea id="rev-reply-${it.order_id}" rows="1" maxlength="500" placeholder="${t('回应这条评价(最多 500 字 · 仅一次)')}" style="flex:1;padding:6px 8px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;resize:none"></textarea>
194
+ <button class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:11px" onclick="submitSellerReviewReply('${it.order_id}')">${t('回应')}</button>
195
+ </div>
196
+ <div id="rev-reply-err-${it.order_id}" style="font-size:11px;color:#dc2626;margin-top:4px"></div>`}
197
+ </div>`).join('')}
198
+ `
199
+ }