Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions frontend/src/__tests__/css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import path from 'path';

describe('CSS Styles', () => {
let css: string;
let responsiveCss: string;
let componentsCss: string;

beforeAll(() => {
// Read all CSS files from the styles directory
Expand All @@ -15,6 +17,8 @@ describe('CSS Styles', () => {
.filter(file => file.endsWith('.css'))
.map(file => fs.readFileSync(path.join(stylesDir, file), 'utf8'));
css = cssFiles.join('\n');
responsiveCss = fs.readFileSync(path.join(stylesDir, 'responsive.css'), 'utf8');
componentsCss = fs.readFileSync(path.join(stylesDir, 'components.css'), 'utf8');
});

describe('Required CSS Rules', () => {
Expand Down Expand Up @@ -301,6 +305,37 @@ describe('CSS Styles', () => {
test('has modal-wide variant', () => {
expect(css).toMatch(/\.modal-wide\s*\{/);
});

test('has a vw cap on modal-confirm', () => {
expect(css).toMatch(/\.modal-confirm\s*\{[^}]*max-width:\s*min\(480px,\s*calc\(100vw - 2rem\)\)/);
});

test('has a 44px minimum tap target on modal-confirm-close', () => {
expect(css).toMatch(/\.modal-confirm-close\s*\{[^}]*min-width:\s*44px[^}]*min-height:\s*44px/);
});

test('flips modals to a bottom sheet at 480px, winning the cascade', () => {
// The bottom-sheet rule must live in responsive.css (imported last in
// index.css), not components.css (imported 3rd). components.css is
// imported before modals.css and responsive.css, so an equal-specificity
// .modal-content rule placed there would always lose to the
// .modal-content{width:95%} 768px rule in responsive.css -- only
// .modal-confirm would actually flip, leaving every real modal
// (#user-modal, #purchase-modal, etc.) centered instead of bottom-sheet.
const bottomSheetQuery = /@media \(max-width: 480px\)\s*\{[^]*?\.modal,\s*\.modal-confirm-backdrop\s*\{[^}]*align-items:\s*flex-end[^}]*\}[^]*?\.modal-content,\s*\.modal-confirm\s*\{[^}]*width:\s*100%[^}]*\}/;

expect(responsiveCss).toMatch(bottomSheetQuery);
expect(componentsCss).not.toMatch(bottomSheetQuery);

// Source-order check: the 480px bottom-sheet block must appear after
// the existing 768px block within responsive.css, so at <=480px it
// wins the cascade over the 768px .modal-content{width:95%} rule
// (equal selector specificity -> later declaration wins).
const idx768 = responsiveCss.search(/@media \(max-width: 768px\)/);
const idx480 = responsiveCss.search(/@media \(max-width: 480px\)/);
expect(idx768).toBeGreaterThan(-1);
expect(idx480).toBeGreaterThan(idx768);
});
});

describe('CLI Command Styles', () => {
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/styles/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,7 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
background: white;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
max-width: 480px;
max-width: min(480px, calc(100vw - 2rem));
min-width: 320px;
padding: 1.5rem;
}
Expand All @@ -995,13 +995,18 @@ tr.rec-variant-row:hover td:not(.checkbox-col) {
position: absolute;
top: 0.5rem;
right: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: var(--cudly-text-muted);
padding: 0 0.25rem;
padding: 0;
}

.modal-confirm-close:hover {
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/styles/responsive.css
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,27 @@ body.sidebar-open {
}
}

/* Bottom-sheet layout on narrow viewports (issue #985): modals slide up
* from the bottom edge instead of floating centered, so there's no
* side margin to lose on small screens. Deliberately placed here (after
* the 768px block above) rather than in components.css: modals.css and
* this file are imported after components.css in index.css, so with
* equal selector specificity, a .modal-content rule in components.css
* loses the cascade to the .modal-content{width:95%} rule from the
* 768px block above and would never take effect at <=480px. */
@media (max-width: 480px) {
.modal, .modal-confirm-backdrop {
align-items: flex-end;
}

.modal-content, .modal-confirm {
border-radius: 12px 12px 0 0;
width: 100%;
max-width: 100%;
margin: 0;
}
}

@media (max-width: 1200px) {
.planned-purchases-table {
display: block;
Expand Down
Loading