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
3 changes: 2 additions & 1 deletion cws/CHROMEWEBSTORE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ English
1. **Main card** — the primary new-tab experience: a cat card front and center (name, meta, fee, distance, "Updated" freshness, rescue link, View profile button).
2. **Explore another area** — the explore banner active, showing the "browse other cities" feature and its "Show another cat" / "Back to my area" links.
3. **First run** — the "Find cats near you" panel (Use my location / enter ZIP), so users see how onboarding works before installing.
4. **Settings** — the redesigned settings page (stacked "Use my location" / ZIP layout from the recent redesign).
4. **Settings** — the redesigned settings page: "Use my location," a floating-label ZIP field joined with Save into a single bordered group (#30).
5. **Status chips & fee** — a card showing the "Adoption pending" / "Special needs" chips and a normalized adoption fee, to demonstrate the extension surfaces real, practical adoption details.
6. **Share menu** — the "Share via" popover open on a card, showing the explicit channel list (WhatsApp, Email, X/Twitter, Facebook, Reddit, Pinterest, Nextdoor, Copy link) added in v2.1.0.

Expand Down Expand Up @@ -191,6 +191,7 @@ https://github.com/BrandonML/tabby
|---------|------|---------|--------|
| 0.1.0 | 2026-08-19 | Initial Chrome Web Store submission: location/ZIP search with radius escalation, explore-another-area, adoption fee display, redesigned card layout | Draft |
| 2.1.0 | 2026-09-14 | Custom "Share via" menu (WhatsApp, Email, X/Twitter, Facebook, Reddit, Pinterest, Nextdoor, Copy link) replacing the single native-share-only button | Submitted |
| 2.2.0 | 2026-09-18 | Design polish: settings panel spacing + floating-label ZIP/Save input group (#30), tagline moved beside the wordmark (#25), dropped the un-loaded Inter font (#42), focus-visible rings and hover transitions, ZIP field now filters non-digit input as you type | Draft |


## Review Notes
Expand Down
Binary file modified cws/screenshot-1-main-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cws/screenshot-2-explore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cws/screenshot-3-first-run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cws/screenshot-4-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cws/screenshot-5-fee-tags.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cws/screenshot-6-share.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 10 additions & 6 deletions cws/screenshot-frame-options.source.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,21 @@
</header>
<section class="location-panel">
<h1>Your Location</h1>
<p class="panel-sub">Tabby uses this to find adoptable cats nearest to you.</p>
<form id="settings-form">
<div class="location-actions">
<button id="use-location" class="primary" type="button">Use my location</button>
</div>
<p class="or-divider">or</p>
<div class="zip-field">
<div class="zip-input-wrap">
<label for="zip">Zip Code <span class="label-optional">(optional)</span></label>
<input id="zip" inputmode="numeric" maxlength="5" pattern="[0-9]{5}" placeholder="90210">
<p class="or-divider"><span></span>or<span></span></p>
<div class="zip-field-block">
<div class="zip-field">
<div class="form-floating">
<input id="zip" type="text" inputmode="numeric" maxlength="5" pattern="[0-9]{5}" placeholder=" ">
<label for="zip">Zip code</label>
</div>
<button type="submit">Save</button>
</div>
<button type="submit">Save</button>
<p class="field-hint">Optional — skip this if you use your location.</p>
</div>
</form>
<p id="saved" class="notice" role="status" hidden></p>
Expand Down
7 changes: 5 additions & 2 deletions cws/screenshot-frame.source.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@
<div class="stage">
<main class="shell">
<header>
<p class="wordmark">Tabby</p>
<div class="brand">
<p class="wordmark">Tabby</p>
<span class="brand-rule" aria-hidden="true"></span>
<p class="tagline">One cat at a time.</p>
</div>
<div class="header-actions">
<button id="explore" class="quiet" type="button">Explore another area</button>
<button id="settings" class="quiet">Settings</button>
Expand All @@ -119,7 +123,6 @@ <h1>Find cats near you</h1>
<p id="explore-banner" class="notice" role="status" hidden><span class="explore-text">Exploring near <span id="explore-label"></span>.</span><span class="explore-links"><button id="show-another-explore-cat" class="notice-link" type="button">Show another cat</button> · <button id="back-to-my-area" class="notice-link" type="button">Back to my area</button></span></p>
<p id="notice" class="notice" role="status" hidden></p>
<footer class="footer">
<p class="tagline">One cat at a time.</p>
<p class="credit">Built by <a href="https://linke.ro/brandon" target="_blank" rel="noopener noreferrer">Brandon</a></p>
<p class="links">
<a href="https://github.com/BrandonML/tabby" target="_blank" rel="noopener noreferrer">GitHub</a>
Expand Down
132 changes: 125 additions & 7 deletions extension/newtab.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
background: #e4e1d3;
color: #2a2620;

Expand All @@ -29,7 +29,13 @@ body { margin: 0; min-width: 320px; }
.shell { width: min(620px, calc(100vw - 32px)); margin: 8vh auto; }

header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
.brand { display: flex; align-items: baseline; gap: 10px; }
.brand-rule { width: 1px; height: 13px; background: var(--card-edge); align-self: center; }
.wordmark { font-family: var(--font-display); font-weight: 600; font-size: 0.95rem; letter-spacing: 0.01em; color: var(--ink); margin: 0; }
.tagline { font-family: var(--font-display); font-style: italic; font-size: 14px; color: var(--ink-soft); margin: 0; }
@media (max-width: 460px) {
.brand-rule, .tagline { display: none; }
}
.eyebrow { letter-spacing: 0.14em; text-transform: uppercase; font-size: 12px; font-weight: 700; color: var(--ink-soft); }
.header-actions { display: flex; align-items: center; gap: 4px; }

Expand Down Expand Up @@ -197,14 +203,25 @@ h1 { margin: 0; font-weight: 700; color: var(--ink); }
}

.location-panel { margin-top: 18px; padding: 22px; border-radius: 14px; background: var(--paper); border: 1px solid var(--card-edge); }
.location-panel h1 { font-size: 1.5rem; margin: 0 0 12px; }
.location-panel h1 { font-size: 1.5rem; margin: 0 0 6px; }
.location-panel p { color: var(--ink-soft); margin: 0 0 18px; }
/* .location-panel p's specificity (a class + a type selector) beats a
same-context single-class rule, so .panel-sub/.field-hint/.or-divider's
own margins below are scoped through .location-panel too -- otherwise
this blanket rule (still needed for #saved, which has no other class)
would silently win and the spacing fixes below would never apply. */
.location-panel .panel-sub {
margin: 0 0 24px;
font-size: 14px;
line-height: 1.5;
}
.location-actions { display: flex; flex-direction: column; gap: 10px; }
.location-actions .primary, .location-actions .secondary { width: 100%; }
.location-actions .secondary { background: transparent; color: var(--moss-dark); padding: 0; font-weight: 600; text-align: center; }
.location-actions .secondary:hover { background: transparent; text-decoration: underline; }

form { display: flex; flex-direction: column; gap: 14px; margin-top: 18px; }
.location-panel form { margin-top: 0; }
.or-divider {
display: flex;
align-items: center;
Expand All @@ -217,13 +234,91 @@ form { display: flex; flex-direction: column; gap: 14px; margin-top: 18px; }
color: var(--ink-soft);
margin: 8px 0;
}
.or-divider::before, .or-divider::after { content: ""; flex: 1; height: 1px; background: var(--card-edge); }
.zip-field { display: flex; gap: 8px; align-items: end; }
.zip-input-wrap { flex: 1; }
.location-panel .or-divider { margin: 18px 0; }
.or-divider span { flex: 1; height: 1px; background: var(--card-edge); }
label { display: block; font-size: 13px; font-weight: 700; }
.label-optional { font-weight: 400; color: var(--ink-soft); }
input { display: block; width: 100%; padding: 10px; margin-top: 4px; border: 1px solid var(--card-edge); border-radius: 8px; font: inherit; background: var(--paper); color: var(--ink); }

.zip-field {
display: flex;
align-items: stretch;
gap: 0;
overflow: hidden;
border: 1px solid var(--card-edge);
border-radius: 8px;
background: var(--paper);
}
.zip-field:focus-within { box-shadow: 0 0 0 3px rgba(63, 93, 66, .22); }
.zip-field .form-floating { flex: 1; }
.zip-field input {
height: 58px;
margin-top: 0;
border: 0;
background: transparent;
outline: none;
}
.zip-field button {
border: 0;
border-left: 1px solid var(--card-edge);
border-radius: 0;
padding: 0 22px;
background: #eae3cf;
color: var(--moss-dark);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
cursor: pointer;
}
.zip-field button:hover { background: #ddd2b6; }
.zip-field button:focus-visible {
outline: none;
box-shadow: inset 0 0 0 2px var(--moss);
}
.location-panel .field-hint { margin: 8px 2px 0; font-size: 12.5px; }

.form-floating { position: relative; }

.form-floating > input {
padding: 1rem .75rem;
font: inherit;
font-size: 16px;
line-height: 1.25;
color: var(--ink);
}

.form-floating > label {
position: absolute;
top: 0;
left: 0;
z-index: 2;
height: 100%;
padding: 1rem .75rem;
font-size: 13px;
font-weight: 700;
color: var(--ink-soft);
pointer-events: none;
border: 1px solid transparent;
transform-origin: 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: opacity .1s ease-in-out, transform .1s ease-in-out;
}

.form-floating > input:focus,
.form-floating > input:not(:placeholder-shown) {
padding-top: 1.625rem;
padding-bottom: .625rem;
}

.form-floating > input:focus ~ label,
.form-floating > input:not(:placeholder-shown) ~ label {
opacity: .65;
transform: scale(.85) translateY(-.5rem) translateX(.15rem);
}

.notice {
min-height: 1.3em;
font-family: var(--font-mono);
Expand Down Expand Up @@ -257,11 +352,34 @@ input { display: block; width: 100%; padding: 10px; margin-top: 4px; border: 1px
border-top: 1px solid var(--card-edge);
text-align: center;
}
.footer .tagline { font-family: var(--font-display); font-style: italic; font-size: 13px; color: var(--ink-soft); margin: 0 0 8px; }
.footer .credit { font-size: 12px; color: var(--ink-soft); margin: 0 0 6px; }
.footer .credit a { color: var(--ink); font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
.footer .links { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.02em; display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; color: var(--ink-soft); margin: 0; }
.footer .links a { color: var(--moss-dark); text-decoration: underline; text-underline-offset: 2px; }
.footer .links .sep { color: var(--card-edge); }

[hidden] { display: none !important; }

/* transitions */
button,
.profile,
a {
transition: background-color .12s ease, color .12s ease, box-shadow .12s ease;
}

@media (prefers-reduced-motion: reduce) {
button, .profile, a { transition: none; }
}

/* focus rings */
:focus-visible {
outline: 2px solid var(--moss);
outline-offset: 2px;
border-radius: 3px;
}

.share-menu-item:focus-visible {
outline: none;
background: var(--wall);
box-shadow: inset 0 0 0 2px var(--moss);
}
7 changes: 5 additions & 2 deletions extension/newtab.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
<body>
<main class="shell">
<header>
<p class="wordmark">Tabby</p>
<div class="brand">
<p class="wordmark">Tabby</p>
<span class="brand-rule" aria-hidden="true"></span>
<p class="tagline">One cat at a time.</p>
</div>
<div class="header-actions">
<button id="explore" class="quiet" type="button">Explore another area</button>
<button id="settings" class="quiet">Settings</button>
Expand All @@ -22,7 +26,6 @@ <h1>Find cats near you</h1>
<p id="explore-banner" class="notice" role="status" hidden><span class="explore-text">Exploring near <span id="explore-label"></span>.</span><span class="explore-links"><button id="show-another-explore-cat" class="notice-link" type="button">Show another cat</button> · <button id="back-to-my-area" class="notice-link" type="button">Back to my area</button></span></p>
<p id="notice" class="notice" role="status" hidden></p>
<footer class="footer">
<p class="tagline">One cat at a time.</p>
<p class="credit">Built by <a href="https://linke.ro/brandon" target="_blank" rel="noopener noreferrer">Brandon</a></p>
<p class="links">
<a href="https://github.com/BrandonML/tabby" target="_blank" rel="noopener noreferrer">GitHub</a>
Expand Down
2 changes: 1 addition & 1 deletion extension/options.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Tabby settings</title><link rel="icon" type="image/png" sizes="16x16" href="icons/icon16.png"><link rel="icon" type="image/png" sizes="32x32" href="icons/icon32.png"><link rel="stylesheet" href="newtab.css"></head><body><main class="shell"><header><p class="eyebrow">TABBY SETTINGS</p><button id="close-settings" class="quiet" type="button" aria-label="Close settings">Close</button></header><section class="location-panel"><h1>Your Location</h1><form id="settings-form"><div class="location-actions"><button id="use-location" class="primary" type="button">Use my location</button></div><p class="or-divider">or</p><div class="zip-field"><div class="zip-input-wrap"><label for="zip">Zip Code <span class="label-optional">(optional)</span></label><input id="zip" inputmode="numeric" maxlength="5" pattern="[0-9]{5}" placeholder="90210"></div><button type="submit">Save</button></div></form><p id="saved" class="notice" role="status" hidden></p></section></main><script type="module" src="options.js"></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Tabby settings</title><link rel="icon" type="image/png" sizes="16x16" href="icons/icon16.png"><link rel="icon" type="image/png" sizes="32x32" href="icons/icon32.png"><link rel="stylesheet" href="newtab.css"></head><body><main class="shell"><header><p class="eyebrow">TABBY SETTINGS</p><button id="close-settings" class="quiet" type="button" aria-label="Close settings">Close</button></header><section class="location-panel"><h1>Your Location</h1><p class="panel-sub">Tabby uses this to find adoptable cats nearest to you.</p><form id="settings-form"><div class="location-actions"><button id="use-location" class="primary" type="button">Use my location</button></div><p class="or-divider"><span></span>or<span></span></p><div class="zip-field-block"><div class="zip-field"><div class="form-floating"><input id="zip" type="text" inputmode="numeric" maxlength="5" pattern="[0-9]{5}" placeholder=" "><label for="zip">Zip code</label></div><button type="submit">Save</button></div><p class="field-hint">Optional — skip this if you use your location.</p></div></form><p id="saved" class="notice" role="status" hidden></p></section></main><script type="module" src="options.js"></script></body></html>
8 changes: 8 additions & 0 deletions extension/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ const useLocation = $("use-location");
const saved = $("saved");
const closeSettings = $("close-settings");

// maxlength/pattern only catch a bad ZIP at validation time; strip
// non-digits as they're typed (or pasted) so it's never possible to
// enter one in the first place.
zip.addEventListener("input", () => {
const digitsOnly = zip.value.replace(/\D/g, "");
if (digitsOnly !== zip.value) zip.value = digitsOnly;
});

// Same reasoning as newtab.js's showNotice(): #saved reserves visible box
// space (min-height + flex, via the shared .notice class) even with no
// text, so hide the element itself rather than leaving a blank gap.
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Tabby: New Tab for Adoptable Cats",
"version": "2.1.0",
"version": "2.2.0",
"description": "See one real, nearby adoptable cat on every new tab.",
"permissions": [
"storage",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tabby",
"version": "2.1.0",
"version": "2.2.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
10 changes: 10 additions & 0 deletions test/newtab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1641,4 +1641,14 @@ describe('newtab.js DOM manipulation', () => {
assert.equal(document.getElementById('card').querySelector('h1').textContent, 'HomeCat');
});
});

describe('header brand + tagline (#25)', () => {
it('wordmark and tagline share a .brand container in the header, not the footer', () => {
const brand = document.querySelector('header .brand');
assert.ok(brand, '.brand must be a direct part of the header');
assert.equal(brand.querySelector('.wordmark').textContent, 'Tabby');
assert.equal(brand.querySelector('.tagline').textContent, 'One cat at a time.');
assert.ok(!document.querySelector('.footer .tagline'), 'the tagline must not still be duplicated in the footer');
});
});
});
23 changes: 23 additions & 0 deletions test/options.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,29 @@ describe('options.js settings logic', () => {
assert.equal(saved.textContent, '');
});

it('zip field keeps the floating-label structure the CSS depends on (#30)', () => {
const input = document.getElementById('zip');
const label = document.querySelector('label[for="zip"]');

assert.equal(input.getAttribute('placeholder'), ' ', 'a non-empty placeholder is required for :placeholder-shown to work');
assert.equal(input.parentElement.className, 'form-floating', '.form-floating must wrap only the input + label');
assert.equal(input.nextElementSibling, label, 'the label must follow the input for the ~ sibling selector to match');
assert.equal(input.getAttribute('maxlength'), '5');
assert.equal(input.getAttribute('pattern'), '[0-9]{5}');
});

it('strips non-digit characters from the ZIP field as they are typed or pasted', () => {
const zip = document.getElementById('zip');

zip.value = 'a1b2c3';
zip.dispatchEvent(new window.Event('input'));
assert.equal(zip.value, '123');

zip.value = '9-0210';
zip.dispatchEvent(new window.Event('input'));
assert.equal(zip.value, '90210');
});

it('validates ZIP format on submit', async () => {
const zip = document.getElementById('zip');
const form = document.getElementById('settings-form');
Expand Down
Loading