Replaces the @mailman invite Line's inline OK/BYE block w. a dedicated per-bud surface. Three new FTs (test_bill_bud_page, test_bill_my_buds_tooltip, test_bill_mailman_invite_post — landed red 2026-05-27 PM) drive: per-bud landing page rendering 4-btn apparatus + shoptalk textarea + invite-cascade glow handoff; my-buds row tooltip portal w. .tt-title/.tt-description/.tt-email/.tt-shoptalk/.tt-milestone slots; mailman Brief surfacing on any authenticated page-load via context processor + base.html JSON-script.
Models: new `BudshipNote(user, bud, shoptalk[CharField max=160], edited_at)` w. unique_together — per-relation personal note about a bud, never visible to the bud. Lazy-created on first shoptalk save so absence of a row reads as 'never edited' (drives .tt-milestone slot presence).
URLs (billboard): `buds/<uuid:bud_id>/` (bud_page), `buds/<uuid:bud_id>/shoptalk` (save_bud_shoptalk), `buds/<uuid:bud_id>/delete` (delete_bud).
Views: bud_page auto-adds the bud on first visit (mirrors share_post implicit-add); resolves `pending_invite` as non-expired PENDING SeaInvite(owner=bud, invitee=request.user) → drives `sea_btn_active` + `sea_first_draw_pending` flags that _burger.html already reads on my_sea + room. my_buds enriches each bud w. `.shoptalk_text` + `.milestone_dt` so the row template can render data-tt-* attrs without an extra template tag.
mail.py: INVITE_TEMPLATE now interpolates `owner_id` into an `<a class="post-attribution" href="/billboard/buds/{owner_id}/">{handle}</a>` wrapper around the owner's handle. post.html's existing safe-filter branch (gated on author username == 'mailman') passes it through unescaped. Removed the {% if line.sea_invite %} include path — _invite_actions.html left in place for archival.
Templates: new bud.html (header + shoptalk form + apparatus + gear + burger fan + sea_btn nav inline JS); new _bud_gear.html (NVM→my_buds, DEL→guard portal "Delete this bud?" → POST delete_bud); new _bud_tooltip.html (portal w. .tt-* slots); _my_buds_item.html wraps `@handle` in an anchor to bud_page + carries data-tt-* attrs + " the {{ active_title_display }}"; my_buds.html includes the tooltip portal + loads my-buds-tooltip.js.
JS: new my-buds-tooltip.js binds row clicks → .row-locked + populates #id_tooltip_portal from data-tt-* attrs; anchor clicks pass through to navigate; .tt-milestone is removed from DOM (not just emptied) when never-edited so the FT can distinguish absent vs cleared-after-edit.
SCSS: extend landscape gear-btn rule + #id_*_menu rule w. `.bud-page` + `#id_bud_menu` (otherwise gear-btn collided w. bud-btn in landscape on bud.html). Bump active burger sub-btn z-index to 1 so click hit-test picks the active sub-btn during the 0.25s fan arc-out animation (otherwise a later-in-DOM inactive btn obscured the active target during transition).
Cross-page Brief surface: new `mail_brief_payload` context processor injects the user's oldest unread MAIL_ACCEPTANCE Brief into every authenticated response; base.html renders the JSON-script + auto-fires Brief.showBanner. Mark-read still rides view_post's existing GET unread-flip — no new endpoint.
Pre-existing MySeaInvitePostRenderTest (test_sea_invite_views.py) inverted to match the new contract: the .invite-actions sweep is unconditional (PENDING / ACCEPTED / DECLINED all carry prose only); pinned the post-attribution anchor + bud-page href in its place.
1518 ITs green (1475 app ITs + 43 sprint ITs), 23 sprint FTs green (5 my_buds tooltip + 13 bud page + 5 mailman invite post). Jasmine specs from sprint plan deferred — FT coverage of burger-glow / row-lock / portal-populate paths suffices and the textarea blur-POST flow isn't implemented in this sprint (form is server-action only, save-on-blur AJAX is a follow-on).
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
281 lines
7.7 KiB
SCSS
281 lines
7.7 KiB
SCSS
// ── Gear button ────────────────────────────────────────────
|
|
.gear-btn {
|
|
position: absolute;
|
|
bottom: 0.5rem;
|
|
right: 0.5rem;
|
|
z-index: 1;
|
|
font-size: 2rem;
|
|
cursor: pointer;
|
|
color: rgba(var(--secUser), 1);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 3rem;
|
|
height: 3rem;
|
|
border-radius: 50%;
|
|
background-color: rgba(var(--priUser), 1);
|
|
border: 0.15rem solid rgba(var(--secUser), 1);
|
|
|
|
&.active {
|
|
color: rgba(var(--quaUser), 1);
|
|
border-color: rgba(var(--quaUser), 1);
|
|
}
|
|
}
|
|
|
|
// ── Applet menu (shared structure) ─────────────────────────
|
|
%applet-menu {
|
|
position: absolute;
|
|
bottom: 3rem;
|
|
right: 0.5rem;
|
|
z-index: 100;
|
|
background-color: rgba(var(--priUser), 0.95);
|
|
border: 0.15rem solid rgba(var(--secUser), 1);
|
|
box-shadow:
|
|
0 0 0.5rem rgba(var(--secUser), 0.75),
|
|
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
|
;
|
|
border-radius: 0.75rem;
|
|
padding: 1rem;
|
|
|
|
.menu-btns {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
margin-top: 0.75rem;
|
|
}
|
|
|
|
form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
input[type="checkbox"] {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
width: 0.9em;
|
|
height: 0.9em;
|
|
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
|
border-radius: 0.25rem;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
top: 0.1em;
|
|
|
|
&:checked::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0.2em;
|
|
bottom: 0.2em;
|
|
width: 0.55em;
|
|
height: 1em;
|
|
border: 0.12em solid rgba(var(--ninUser), 1);
|
|
border-top: none;
|
|
border-left: none;
|
|
transform: rotate(45deg);
|
|
}
|
|
}
|
|
}
|
|
|
|
#id_dash_applet_menu { @extend %applet-menu; }
|
|
#id_game_applet_menu { @extend %applet-menu; }
|
|
#id_game_kit_menu { @extend %applet-menu; }
|
|
#id_wallet_applet_menu { @extend %applet-menu; }
|
|
#id_room_menu { @extend %applet-menu; }
|
|
#id_post_menu { @extend %applet-menu; }
|
|
#id_billboard_applet_menu { @extend %applet-menu; }
|
|
#id_billscroll_menu { @extend %applet-menu; }
|
|
#id_my_sea_menu { @extend %applet-menu; }
|
|
#id_bud_menu { @extend %applet-menu; }
|
|
|
|
// Page-level gear buttons — fixed to viewport bottom-right
|
|
.gameboard-page,
|
|
.dashboard-page,
|
|
.wallet-page,
|
|
.room-page,
|
|
.post-page,
|
|
.billboard-page,
|
|
.billscroll-page,
|
|
.my-sea-page,
|
|
.bud-page {
|
|
> .gear-btn {
|
|
position: fixed;
|
|
bottom: 4.2rem;
|
|
right: 0.5rem;
|
|
z-index: 314;
|
|
}
|
|
}
|
|
|
|
#id_dash_applet_menu,
|
|
#id_game_applet_menu,
|
|
#id_game_kit_menu,
|
|
#id_wallet_applet_menu,
|
|
#id_room_menu,
|
|
#id_post_menu,
|
|
#id_billboard_applet_menu,
|
|
#id_billscroll_menu,
|
|
#id_my_sea_menu,
|
|
#id_bud_menu {
|
|
position: fixed;
|
|
bottom: 6.6rem;
|
|
right: 1rem;
|
|
z-index: 312;
|
|
}
|
|
|
|
// In landscape: gear-btn relocates to the LEFT of kit_btn at the top of
|
|
// the right sidebar (kit at right:0.5rem, gear at right:4.2rem — same
|
|
// 3.7rem centre-to-centre delta as the portrait gear-above-kit stack,
|
|
// rotated 90deg into the horizontal axis). Each page that hosts a page-
|
|
// level `.gear-btn` gets the same anchor.
|
|
//
|
|
// Applet menus all anchor at top:2.1rem; right:4.2rem (beneath the gear's
|
|
// leftward arc) and extend DOWN-LEFT. Vertical column flow stays the
|
|
// default — the only menu that rotates aspect is #id_room_menu, which
|
|
// carries that override in _room.scss (2-btn menu → row layout).
|
|
@media (orientation: landscape) {
|
|
.gameboard-page > .gear-btn,
|
|
.dashboard-page > .gear-btn,
|
|
.wallet-page > .gear-btn,
|
|
.room-page > .gear-btn,
|
|
.post-page > .gear-btn,
|
|
.billboard-page > .gear-btn,
|
|
.billscroll-page > .gear-btn,
|
|
.my-sea-page > .gear-btn,
|
|
.bud-page > .gear-btn {
|
|
top: 0.5rem;
|
|
bottom: auto;
|
|
right: 4.2rem;
|
|
}
|
|
|
|
#id_dash_applet_menu,
|
|
#id_game_applet_menu,
|
|
#id_game_kit_menu,
|
|
#id_wallet_applet_menu,
|
|
#id_room_menu,
|
|
#id_post_menu,
|
|
#id_billboard_applet_menu,
|
|
#id_billscroll_menu,
|
|
#id_my_sea_menu,
|
|
#id_bud_menu {
|
|
position: fixed;
|
|
top: 2.6rem;
|
|
right: 4.2rem;
|
|
bottom: auto;
|
|
left: auto;
|
|
}
|
|
}
|
|
|
|
// ── Applet box visual shell (reusable outside the grid) ────
|
|
%applet-box {
|
|
border:
|
|
0.2rem solid rgba(var(--secUser), 0.5),
|
|
;
|
|
box-shadow:
|
|
inset -0.125rem -0.125rem 0 rgba(var(--ninUser), 0.125),
|
|
inset 0.125rem 0.125rem 0 rgba(0, 0, 0, 0.8)
|
|
;
|
|
background-color: rgba(0, 0, 0, 0.125);
|
|
border-radius: 0.75rem;
|
|
position: relative;
|
|
padding: 0.75rem 0.75rem 0.75rem 2.5rem;
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
|
|
// Hide any inner scrollbars (e.g. My Sky applet's #id_applet_sky_form_wrap)
|
|
// so they obey the same scrollbar-less treatment as the page apertures
|
|
// (gameboard / billboard / dashboard apertures already use this same pair
|
|
// — keeps the dark theme consistent w.o. the OS-default white track bleeding
|
|
// through inside an applet).
|
|
*::-webkit-scrollbar { display: none; }
|
|
* { scrollbar-width: none; }
|
|
|
|
> h2 {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 2rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
writing-mode: vertical-rl;
|
|
transform: rotate(180deg);
|
|
font-size: 1rem;
|
|
letter-spacing: 0.2em;
|
|
text-transform: uppercase;
|
|
margin: 0;
|
|
padding-right: 0.2rem;
|
|
color: rgba(var(--secUser), 1);
|
|
text-shadow:
|
|
1px 1px 0 rgba(255, 255, 255, 0.06),
|
|
-0.06rem -0.06rem 0 rgba(0, 0, 0, 0.25)
|
|
;
|
|
background-color: rgba(0, 0, 0, 0.125);
|
|
box-shadow:
|
|
0 0 0.5rem rgba(var(--priUser), 0.5),
|
|
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.5),
|
|
;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
z-index: 1;
|
|
|
|
a {
|
|
color: rgba(var(--terUser), 1);
|
|
text-decoration: none;
|
|
|
|
&:hover {
|
|
color: rgba(var(--ninUser), 1);
|
|
text-shadow: 0 0 0.5rem rgba(var(--terUser), 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Applets grid (shared across all boards) ────────────────
|
|
%applets-grid {
|
|
container-type: inline-size;
|
|
--grid-gap: 0.5rem;
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
display: grid;
|
|
grid-template-columns: repeat(12, 1fr);
|
|
grid-auto-rows: 3rem;
|
|
gap: var(--grid-gap);
|
|
padding: 0.75rem;
|
|
-webkit-overflow-scrolling: touch;
|
|
mask-image: linear-gradient(
|
|
to bottom,
|
|
transparent 0%,
|
|
black 2%,
|
|
black 99%,
|
|
transparent 100%
|
|
);
|
|
margin-left: 1rem;
|
|
margin-top: 1rem;
|
|
@media (orientation: landscape) and (min-width: 900px) {
|
|
margin-left: 2rem;
|
|
margin-top: 2rem;
|
|
}
|
|
@media (orientation: landscape) and (min-width: 1800px) {
|
|
margin-left: 4rem;
|
|
margin-top: 4rem;
|
|
}
|
|
|
|
section {
|
|
@extend %applet-box;
|
|
grid-column: span var(--applet-cols, 12);
|
|
grid-row: span var(--applet-rows, 3);
|
|
|
|
@container (max-width: 550px) {
|
|
grid-column: span 12;
|
|
}
|
|
}
|
|
}
|
|
|
|
#id_applets_container { @extend %applets-grid; }
|
|
#id_game_applets_container { @extend %applets-grid; }
|
|
#id_wallet_applets_container { @extend %applets-grid; }
|
|
#id_billboard_applets_container { @extend %applets-grid; }
|
|
#id_gk_sections_container { @extend %applets-grid; }
|