feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5 → d28cf7b). **Microtooltip extraction**: .tt-microbutton-portal (Chunk 4's wrap-inside-.tt) replaced w. a sibling .tt-micro div on each .shop-tile. wallet.js's initWalletTooltips clones BOTH into separate portals on hover — .tt → #id_tooltip_portal (main card), .tt-micro → #id_mini_tooltip_portal (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: WalletTooltips.pin() / .unpin() exposed on window; wallet-shop.js's BUY click calls pin() before showGuard() + both onConfirm / onDismiss callbacks call unpin() → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new Applet.display_order field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets wallet-shop.display_order=10 so Shop renders atop Balances/Tokens/Payment. applet_context() updated to .order_by("display_order", "pk"). New WalletAppletOrderTest (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot .tt-title / .tt-description / .tt-shoptalk / .tt-expiry classes as the Tokens row. New ShopItem.shoptalk field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New ShopItem.tooltip_expiry() method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: .tt-micro hidden in source DOM (portal-only); #id_mini_tooltip_portal mostly mirrors gameboard's mini at _gameboard.scss:140 but allows BUY-btn label to wrap onto multiple lines (white-space: normal on .tt-buy-btn); .tt-already-owned styled w. --secUser italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: lyric/0010_repricing_tithe_writs (writs + description), lyric/0011_shopitem_shoptalk (schema), lyric/0012_seed_shop_shoptalk (band split), applets/0012_applet_display_order (schema), applets/0013_wallet_shop_display_order (Shop atop). All idempotent. **TDD** — 5 new ITs across test_shop_models.py (shoptalk default + per-item assertions, tooltip_expiry method, updated tithe writs values, WalletAppletOrderTest), 1 new FT (test_shop_buy_guard_portal_pins_item_tooltip — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to .tt-micro (no .tt-buy-btn present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line {# #} comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) spyOn(window, 'fetch') Jasmine double-spy collision (cf trapped previously); (c) async pollution where afterEach restores window.Stripe=undefined before _doBuy's continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
18
src/apps/applets/migrations/0012_applet_display_order.py
Normal file
18
src/apps/applets/migrations/0012_applet_display_order.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0 on 2026-05-22 05:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applets', '0011_seed_wallet_shop_applet'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='applet',
|
||||
name='display_order',
|
||||
field=models.PositiveSmallIntegerField(default=100),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Pin the wallet Shop applet atop the wallet row.
|
||||
|
||||
The 4-applet wallet layout (per [[project-wallet-shop-expansion]]) wants
|
||||
Shop first; the other 3 (Balances, Tokens, Payment) keep their historical
|
||||
order via the default `display_order=100` + PK tie-break.
|
||||
|
||||
Idempotent — `update_or_create(slug=…, defaults={display_order: 10})`
|
||||
also covers fresh DBs where `0011_seed_wallet_shop_applet` already ran.
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
Applet = apps.get_model("applets", "Applet")
|
||||
Applet.objects.filter(slug="wallet-shop").update(display_order=10)
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
Applet = apps.get_model("applets", "Applet")
|
||||
Applet.objects.filter(slug="wallet-shop").update(display_order=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("applets", "0012_applet_display_order"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward, reverse),
|
||||
]
|
||||
@@ -18,6 +18,11 @@ class Applet(models.Model):
|
||||
default_visible = models.BooleanField(default=True)
|
||||
grid_cols = models.PositiveSmallIntegerField(default=12)
|
||||
grid_rows = models.PositiveSmallIntegerField(default=3)
|
||||
# Render-time sort key. Lower = earlier in the applets row. Default 100
|
||||
# gives every existing applet a tied position → falls back to PK insertion
|
||||
# order (the historical behavior), so this field is backwards-compatible.
|
||||
# Set to <100 to pin an applet ABOVE the rest (eg. wallet-shop = 10).
|
||||
display_order = models.PositiveSmallIntegerField(default=100)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -13,9 +13,12 @@ def apply_applet_toggle(user, context, checked_slugs):
|
||||
|
||||
def applet_context(user, context):
|
||||
ua_map = {ua.applet_id: ua.visible for ua in user.user_applets.all()}
|
||||
applets = {a.slug: a for a in Applet.objects.filter(context=context)}
|
||||
# `display_order` (lower = earlier) is the primary sort key; `pk` tie-breaks
|
||||
# so applets at the default order=100 keep their historical insertion-order
|
||||
# rendering. New applets that want pinned positions set order < 100 in
|
||||
# their seed migration (eg. wallet-shop = 10 to render atop the wallet row).
|
||||
applets_qs = Applet.objects.filter(context=context).order_by("display_order", "pk")
|
||||
return [
|
||||
{"applet": applets[slug], "visible": ua_map.get(applets[slug].pk, applets[slug].default_visible)}
|
||||
for slug in applets
|
||||
if slug in applets
|
||||
{"applet": a, "visible": ua_map.get(a.pk, a.default_visible)}
|
||||
for a in applets_qs
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user