rename: Note→Post/Line (dashboard); Recognition→Note (drama); new-post/my-posts to billboard
- dashboard: Note→Post, Item→Line across models, forms, views, API, urls & tests - new-post (9×3) & my-posts (3×3) applets migrate from dashboard→billboard context; billboard view passes form & recent_posts - drama: Recognition→Note, related_name notes; billboard URL /recognition/→/my-notes/, set-palette at /note/<slug>/set-palette - recognition.js→note.js (module Note, data.note key); recognition-page.js→note-page.js; .recog-*→.note-* - _recognition.scss→_note.scss; BillNotes page header; applet slug billboard-recognition→billboard-notes (My Notes) - NoteSpec.js replaces RecognitionSpec.js; test_recognition.py→test_applet_my_notes.py - 4 migrations applied: dashboard 0004, applets 0011+0012, drama 0005; 683 ITs green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,28 @@
|
||||
// ── My Posts applet ────────────────────────────────────────────────────────
|
||||
|
||||
#id_applet_my_posts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.my-posts-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
mask-origin: padding-box;
|
||||
mask-clip: padding-box;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
black 5%,
|
||||
black 85%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared aperture fill for both billboard pages ──────────────────────────
|
||||
|
||||
%billboard-page-base {
|
||||
@@ -87,20 +112,20 @@ body.page-billscroll {
|
||||
}
|
||||
|
||||
// ── Billboard applet placement ─────────────────────────────────────────────
|
||||
// Left column (4-wide): My Scrolls → Contacts → Recognition stacked.
|
||||
// Left column (4-wide): My Scrolls → Contacts → Notes stacked.
|
||||
// Right column (8-wide): Most Recent spans full height.
|
||||
// Portrait override (container query) restores stacked full-width layout.
|
||||
|
||||
#id_billboard_applets_container {
|
||||
#id_applet_billboard_my_scrolls { grid-column: 1 / span 4; grid-row: 1 / span 3; }
|
||||
#id_applet_billboard_my_contacts { grid-column: 1 / span 4; grid-row: 4 / span 3; }
|
||||
#id_applet_billboard_recognition { grid-column: 1 / span 4; grid-row: 7 / span 4; }
|
||||
#id_applet_billboard_notes { grid-column: 1 / span 4; grid-row: 7 / span 4; }
|
||||
#id_applet_billboard_most_recent { grid-column: 5 / span 8; grid-row: 1 / span 10; }
|
||||
|
||||
@container (max-width: 550px) {
|
||||
#id_applet_billboard_my_scrolls,
|
||||
#id_applet_billboard_my_contacts,
|
||||
#id_applet_billboard_recognition,
|
||||
#id_applet_billboard_notes,
|
||||
#id_applet_billboard_most_recent {
|
||||
grid-column: 1 / span 12;
|
||||
grid-row: span var(--applet-rows, 3);
|
||||
@@ -108,9 +133,9 @@ body.page-billscroll {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Recognition applet — vertical title ───────────────────────────────────
|
||||
// ── Notes applet — vertical title ─────────────────────────────────────────
|
||||
|
||||
#id_applet_billboard_recognition {
|
||||
#id_applet_billboard_notes {
|
||||
h2 {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
|
||||
@@ -30,31 +30,6 @@ body.page-dashboard {
|
||||
}
|
||||
|
||||
#id_applets_container {
|
||||
#id_applet_my_notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
.my-notes-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
mask-origin: padding-box;
|
||||
mask-clip: padding-box;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
black 5%,
|
||||
black 85%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#id_applet_wallet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// ── Recognition banner (slides in below page h2 after unlock) ─────────────
|
||||
// ── Note banner (slides in below page h2 after unlock) ─────────────────────
|
||||
|
||||
.recog-banner {
|
||||
.note-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
@@ -9,24 +9,24 @@
|
||||
border-left: 3px solid rgba(var(--priUser), 0.6);
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
.recog-banner__body {
|
||||
.note-banner__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recog-banner__title {
|
||||
.note-banner__title {
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.recog-banner__description,
|
||||
.recog-banner__timestamp {
|
||||
.note-banner__description,
|
||||
.note-banner__timestamp {
|
||||
margin: 0.1rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.recog-banner__image {
|
||||
.note-banner__image {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
flex-shrink: 0;
|
||||
@@ -34,21 +34,21 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.recog-banner__nvm,
|
||||
.recog-banner__fyi {
|
||||
.note-banner__nvm,
|
||||
.note-banner__fyi {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Recognition page ───────────────────────────────────────────────────────
|
||||
// ── Notes page ─────────────────────────────────────────────────────────────
|
||||
|
||||
.recognition-page {
|
||||
.note-page {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.recog-list {
|
||||
.note-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -57,7 +57,7 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.recog-item {
|
||||
.note-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -68,12 +68,12 @@
|
||||
border-radius: 4px;
|
||||
width: 14rem;
|
||||
|
||||
.recog-item__title {
|
||||
.note-item__title {
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.recog-item__description {
|
||||
.note-item__description {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
@@ -81,7 +81,7 @@
|
||||
}
|
||||
|
||||
// Image box — must have a defined size so Selenium can interact with it.
|
||||
.recog-item__image-box {
|
||||
.note-item__image-box {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
display: flex;
|
||||
@@ -97,8 +97,8 @@
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
|
||||
// Unlocked palette swatch inside a recognition item
|
||||
.recog-item__palette {
|
||||
// Unlocked palette swatch inside a note item
|
||||
.note-item__palette {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: 2px;
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
// ── Palette modal ──────────────────────────────────────────────────────────
|
||||
|
||||
.recog-palette-modal {
|
||||
.note-palette-modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -127,7 +127,7 @@
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.recog-swatch-body {
|
||||
.note-swatch-body {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 2px;
|
||||
@@ -137,31 +137,24 @@
|
||||
|
||||
&:hover { border-color: rgba(var(--priUser), 0.8); }
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Palette swatch color fills ─────────────────────────────────────────────
|
||||
// These match the actual palette CSS variables — used both in modal swatches
|
||||
// and as the confirmed .recog-item__palette swatch.
|
||||
|
||||
.palette-bardo .recog-swatch-body,
|
||||
.recog-item__palette.palette-bardo {
|
||||
.palette-bardo .note-swatch-body,
|
||||
.note-item__palette.palette-bardo {
|
||||
background: #2a1a2e;
|
||||
}
|
||||
|
||||
.palette-sheol .recog-swatch-body,
|
||||
.recog-item__palette.palette-sheol {
|
||||
.palette-sheol .note-swatch-body,
|
||||
.note-item__palette.palette-sheol {
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
// ── Confirm submenu ────────────────────────────────────────────────────────
|
||||
|
||||
.recog-palette-confirm {
|
||||
.note-palette-confirm {
|
||||
border-top: 1px solid rgba(var(--priUser), 0.2);
|
||||
padding-top: 0.5rem;
|
||||
gap: 0.4rem;
|
||||
@@ -172,9 +165,4 @@
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
@import 'natus';
|
||||
@import 'tray';
|
||||
@import 'billboard';
|
||||
@import 'recognition';
|
||||
@import 'note';
|
||||
@import 'tooltips';
|
||||
@import 'game-kit';
|
||||
@import 'wallet-tokens';
|
||||
|
||||
@@ -1,143 +1,143 @@
|
||||
// ── RecognitionSpec.js ────────────────────────────────────────────────────────
|
||||
// ── NoteSpec.js ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Unit specs for recognition.js — banner injection from sky/save response.
|
||||
// Unit specs for note.js — banner injection from sky/save response.
|
||||
//
|
||||
// DOM contract assumed by showBanner():
|
||||
// Any <h2> in the document — banner is inserted immediately after it.
|
||||
// If absent, banner is prepended to <body>.
|
||||
//
|
||||
// API under test:
|
||||
// Recognition.showBanner(recognition)
|
||||
// recognition = { slug, title, description, earned_at } → inject .recog-banner
|
||||
// recognition = null → no-op
|
||||
// Note.showBanner(note)
|
||||
// note = { slug, title, description, earned_at } → inject .note-banner
|
||||
// note = null → no-op
|
||||
//
|
||||
// Recognition.handleSaveResponse(data)
|
||||
// data = { saved: true, recognition: {...} } → delegates to showBanner
|
||||
// data = { saved: true, recognition: null } → no-op
|
||||
// Note.handleSaveResponse(data)
|
||||
// data = { saved: true, note: {...} } → delegates to showBanner
|
||||
// data = { saved: true, note: null } → no-op
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const SAMPLE_RECOGNITION = {
|
||||
const SAMPLE_NOTE = {
|
||||
slug: 'stargazer',
|
||||
title: 'Stargazer',
|
||||
description: 'You saved your first personal sky chart.',
|
||||
earned_at: '2026-04-22T02:00:00+00:00',
|
||||
};
|
||||
|
||||
describe('Recognition.showBanner', () => {
|
||||
describe('Note.showBanner', () => {
|
||||
|
||||
let fixture;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = document.createElement('div');
|
||||
fixture.id = 'recognition-fixture';
|
||||
fixture.id = 'note-fixture';
|
||||
fixture.innerHTML = '<h2>Dash</h2>';
|
||||
document.body.appendChild(fixture);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.querySelectorAll('.recog-banner').forEach(b => b.remove());
|
||||
document.querySelectorAll('.note-banner').forEach(b => b.remove());
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
// ── T1 ── null → no banner ────────────────────────────────────────────────
|
||||
|
||||
it('T1: showBanner(null) does not inject a banner', () => {
|
||||
Recognition.showBanner(null);
|
||||
expect(document.querySelector('.recog-banner')).toBeNull();
|
||||
Note.showBanner(null);
|
||||
expect(document.querySelector('.note-banner')).toBeNull();
|
||||
});
|
||||
|
||||
// ── T2 ── recognition present → banner in DOM ─────────────────────────────
|
||||
// ── T2 ── note present → banner in DOM ───────────────────────────────────
|
||||
|
||||
it('T2: showBanner(recognition) injects .recog-banner into the document', () => {
|
||||
Recognition.showBanner(SAMPLE_RECOGNITION);
|
||||
expect(document.querySelector('.recog-banner')).not.toBeNull();
|
||||
it('T2: showBanner(note) injects .note-banner into the document', () => {
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
expect(document.querySelector('.note-banner')).not.toBeNull();
|
||||
});
|
||||
|
||||
// ── T3 ── title ───────────────────────────────────────────────────────────
|
||||
|
||||
it('T3: banner .recog-banner__title contains recognition.title', () => {
|
||||
Recognition.showBanner(SAMPLE_RECOGNITION);
|
||||
const el = document.querySelector('.recog-banner__title');
|
||||
it('T3: banner .note-banner__title contains note.title', () => {
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
const el = document.querySelector('.note-banner__title');
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.textContent).toContain('Stargazer');
|
||||
});
|
||||
|
||||
// ── T4 ── description ─────────────────────────────────────────────────────
|
||||
|
||||
it('T4: banner .recog-banner__description contains recognition.description', () => {
|
||||
Recognition.showBanner(SAMPLE_RECOGNITION);
|
||||
const el = document.querySelector('.recog-banner__description');
|
||||
it('T4: banner .note-banner__description contains note.description', () => {
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
const el = document.querySelector('.note-banner__description');
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.textContent).toContain('You saved your first personal sky chart.');
|
||||
});
|
||||
|
||||
// ── T5 ── timestamp ───────────────────────────────────────────────────────
|
||||
|
||||
it('T5: banner has a .recog-banner__timestamp element', () => {
|
||||
Recognition.showBanner(SAMPLE_RECOGNITION);
|
||||
expect(document.querySelector('.recog-banner__timestamp')).not.toBeNull();
|
||||
it('T5: banner has a .note-banner__timestamp element', () => {
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
expect(document.querySelector('.note-banner__timestamp')).not.toBeNull();
|
||||
});
|
||||
|
||||
// ── T6 ── image area ──────────────────────────────────────────────────────
|
||||
|
||||
it('T6: banner has a .recog-banner__image element', () => {
|
||||
Recognition.showBanner(SAMPLE_RECOGNITION);
|
||||
expect(document.querySelector('.recog-banner__image')).not.toBeNull();
|
||||
it('T6: banner has a .note-banner__image element', () => {
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
expect(document.querySelector('.note-banner__image')).not.toBeNull();
|
||||
});
|
||||
|
||||
// ── T7 ── NVM button ──────────────────────────────────────────────────────
|
||||
|
||||
it('T7: banner has a .btn.btn-danger NVM button', () => {
|
||||
Recognition.showBanner(SAMPLE_RECOGNITION);
|
||||
expect(document.querySelector('.recog-banner .btn.btn-danger')).not.toBeNull();
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
expect(document.querySelector('.note-banner .btn.btn-danger')).not.toBeNull();
|
||||
});
|
||||
|
||||
// ── T8 ── FYI link ────────────────────────────────────────────────────────
|
||||
|
||||
it('T8: banner has a .btn.btn-caution FYI link pointing to /billboard/recognition/', () => {
|
||||
Recognition.showBanner(SAMPLE_RECOGNITION);
|
||||
const fyi = document.querySelector('.recog-banner .btn.btn-caution');
|
||||
it('T8: banner has a .btn.btn-caution FYI link pointing to /billboard/my-notes/', () => {
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
const fyi = document.querySelector('.note-banner .btn.btn-caution');
|
||||
expect(fyi).not.toBeNull();
|
||||
expect(fyi.getAttribute('href')).toBe('/billboard/recognition/');
|
||||
expect(fyi.getAttribute('href')).toBe('/billboard/my-notes/');
|
||||
});
|
||||
|
||||
// ── T9 ── NVM dismissal ───────────────────────────────────────────────────
|
||||
|
||||
it('T9: clicking the NVM button removes the banner from the DOM', () => {
|
||||
Recognition.showBanner(SAMPLE_RECOGNITION);
|
||||
document.querySelector('.recog-banner .btn.btn-danger').click();
|
||||
expect(document.querySelector('.recog-banner')).toBeNull();
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
document.querySelector('.note-banner .btn.btn-danger').click();
|
||||
expect(document.querySelector('.note-banner')).toBeNull();
|
||||
});
|
||||
|
||||
// ── T10 ── placement after h2 ─────────────────────────────────────────────
|
||||
|
||||
it('T10: banner is inserted immediately after the first h2 in the document', () => {
|
||||
Recognition.showBanner(SAMPLE_RECOGNITION);
|
||||
Note.showBanner(SAMPLE_NOTE);
|
||||
const h2 = fixture.querySelector('h2');
|
||||
expect(h2.nextElementSibling.classList.contains('recog-banner')).toBeTrue();
|
||||
expect(h2.nextElementSibling.classList.contains('note-banner')).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Recognition.handleSaveResponse', () => {
|
||||
describe('Note.handleSaveResponse', () => {
|
||||
|
||||
afterEach(() => {
|
||||
document.querySelectorAll('.recog-banner').forEach(b => b.remove());
|
||||
document.querySelectorAll('.note-banner').forEach(b => b.remove());
|
||||
});
|
||||
|
||||
// ── T11 ── delegates when recognition present ─────────────────────────────
|
||||
// ── T11 ── delegates when note present ────────────────────────────────────
|
||||
|
||||
it('T11: handleSaveResponse shows banner when data.recognition is present', () => {
|
||||
Recognition.handleSaveResponse({ saved: true, recognition: SAMPLE_RECOGNITION });
|
||||
expect(document.querySelector('.recog-banner')).not.toBeNull();
|
||||
it('T11: handleSaveResponse shows banner when data.note is present', () => {
|
||||
Note.handleSaveResponse({ saved: true, note: SAMPLE_NOTE });
|
||||
expect(document.querySelector('.note-banner')).not.toBeNull();
|
||||
});
|
||||
|
||||
// ── T12 ── no banner when recognition null ────────────────────────────────
|
||||
// ── T12 ── no banner when note null ───────────────────────────────────────
|
||||
|
||||
it('T12: handleSaveResponse does not show banner when data.recognition is null', () => {
|
||||
Recognition.handleSaveResponse({ saved: true, recognition: null });
|
||||
expect(document.querySelector('.recog-banner')).toBeNull();
|
||||
it('T12: handleSaveResponse does not show banner when data.note is null', () => {
|
||||
Note.handleSaveResponse({ saved: true, note: null });
|
||||
expect(document.querySelector('.note-banner')).toBeNull();
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user