Commitf1dc7c7unknown_key
polish: mobile responsive sweep + loading skeleton primitives
polish: mobile responsive sweep + loading skeleton primitives Picked up from two parallel polish agents' worktrees: MOBILE RESPONSIVE — top 10 surfaces audited for 375px viewport: - @media (max-width: 720px) blocks added to /admin, /dashboard, /explore, /:owner/:repo (web.tsx), /:owner/:repo/issues, /:owner/:repo/pulls - Multi-column grids collapse to 1-column - Filter pill rows become horizontal-scroll on narrow viewports - Tap targets bumped to 40-44px min-height - Hero padding tightened - Tables/lists wrapped in overflow-x: auto LOADING SKELETONS — flag-gated structural preview: - Skeleton SSR rendered when ?skeleton=1 query param is present - Pattern: linear-gradient shimmer with 1.4s loop - Applied to /dashboard, /explore, /:owner/:repo, /:owner/:repo/issues, /:owner/:repo/pulls - Behind a flag so it can't flash before real content lands All scoped CSS. Form actions / data fetches preserved exactly.
6 files changed+306−1f1dc7c7d8456346b541173a0476621413f63d14f
6 changed files+306−1
Modifiedsrc/routes/admin.tsx+39−0View fileUnifiedSplit
@@ -236,6 +236,15 @@ const adminStyles = `
236236 }
237237 (max-width: 720px) {
238238 .admin-stat-grid { grid-template-columns: 1fr; }
239 .admin-hero { padding: var(--space-4); }
240 .admin-actions { grid-template-columns: 1fr; }
241 .admin-action { min-height: 44px; padding: 14px; }
242 .admin-list-row { flex-direction: column; align-items: stretch; padding: 14px; }
243 .admin-search { flex-direction: column; align-items: stretch; }
244 .admin-search .admin-input { width: 100%; }
245 .admin-card-body { padding: var(--space-4); }
246 .admin-card-foot { padding: var(--space-3) var(--space-4); justify-content: flex-start; }
247 .admin-ap-table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
239248 }
240249 .admin-stat {
241250 position: relative;
@@ -1062,6 +1071,12 @@ const admUsersStyles = `
10621071 border-radius: 4px;
10631072 color: var(--text);
10641073 }
1074
1075 (max-width: 720px) {
1076 .adm-users-wrap { padding: var(--space-4) var(--space-3); }
1077 .adm-users-hero { padding: var(--space-4); }
1078 .adm-users-grid { grid-template-columns: 1fr; }
1079 }
10651080`;
10661081
10671082const admReposStyles = `
@@ -1360,6 +1375,12 @@ const admReposStyles = `
13601375 font-size: 13.5px;
13611376 line-height: 1.5;
13621377 }
1378
1379 (max-width: 720px) {
1380 .adm-repos-wrap { padding: var(--space-4) var(--space-3); }
1381 .adm-repos-hero { padding: var(--space-4); }
1382 .adm-repos-grid { grid-template-columns: 1fr; }
1383 }
13631384`;
13641385
13651386const admFlagsStyles = `
@@ -1589,6 +1610,11 @@ const admFlagsStyles = `
15891610 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.45), inset 0 1px 0 rgba(255,255,255,0.16);
15901611 }
15911612 .adm-flags-btn-primary:hover { transform: translateY(-1px); box-shadow: 0 10px 24px -8px rgba(140,109,255,0.55); }
1613
1614 (max-width: 720px) {
1615 .adm-flags-wrap { padding: var(--space-4) var(--space-3); }
1616 .adm-flags-hero { padding: var(--space-4); }
1617 }
15921618`;
15931619
15941620const admDigestsStyles = `
@@ -1953,6 +1979,12 @@ const admDigestsStyles = `
19531979 font-size: 13.5px;
19541980 line-height: 1.5;
19551981 }
1982
1983 (max-width: 720px) {
1984 .adm-digests-wrap { padding: var(--space-4) var(--space-3); }
1985 .adm-digests-hero { padding: var(--space-4); }
1986 .adm-digests-grid { grid-template-columns: 1fr; }
1987 }
19561988`;
19571989
19581990const admAutopilotStyles = `
@@ -2379,6 +2411,13 @@ const admAutopilotStyles = `
23792411 border-radius: 4px;
23802412 color: var(--text);
23812413 }
2414
2415 (max-width: 720px) {
2416 .adm-autopilot-wrap { padding: var(--space-4) var(--space-3); }
2417 .adm-autopilot-hero { padding: var(--space-4); }
2418 .adm-autopilot-statgrid { grid-template-columns: 1fr 1fr; }
2419 .adm-autopilot-tasks { grid-template-columns: 1fr; }
2420 }
23822421`;
23832422
23842423/** Inline-SVG icons (no external deps). Stroke-based, currentColor. */
Modifiedsrc/routes/dashboard.tsx+66−1View fileUnifiedSplit
@@ -66,6 +66,19 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
6666 return c.redirect("/dashboard");
6767 }
6868
69 // ── Loading skeleton (flag-gated) ──
70 // Render an SSR'd structural preview when `?skeleton=1` is present.
71 // Keeps the user oriented on first paint while DB warms up. Behind a
72 // flag until we wire it to streamed/replaced content — we don't ship
73 // a flash before the real markup lands.
74 if (c.req.query("skeleton") === "1") {
75 return c.html(
76 <Layout title="Command Center" user={user}>
77 <DashboardSkeleton />
78 </Layout>
79 );
80 }
81
6982 // Get all user's repos
7083 const repos = await db
7184 .select()
@@ -365,7 +378,11 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
365378 (max-width: 720px) {
366379 .dash-hero-inner { flex-direction: column; align-items: flex-start; }
367380 .dash-hero-actions { width: 100%; }
368 .dash-hero-actions .btn { flex: 1; min-width: 0; }
381 .dash-hero-actions .btn { flex: 1; min-width: 0; min-height: 44px; }
382 .dash-hero { padding: var(--space-4); }
383 .dash-hero-text { min-width: 0; }
384 .dash-hero-bg { width: 220px; height: 220px; inset: -10% -20% auto auto; }
385 .ai-hours-saved-tabs { flex-wrap: wrap; }
369386 }
370387 `,
371388 }}
@@ -1132,4 +1149,52 @@ function formatRelative(date: Date | string): string {
11321149 });
11331150}
11341151
1152// ─── Loading skeleton (flag-gated; renders when ?skeleton=1) ──────────
1153const DashboardSkeleton = () => (
1154 <>
1155 <style
1156 dangerouslySetInnerHTML={{
1157 __html: `
1158 .dash-skel { background: linear-gradient(90deg, var(--bg-secondary) 0%, var(--bg-elevated) 50%, var(--bg-secondary) 100%); background-size: 200% 100%; animation: dashSkelShimmer 1.4s infinite; border-radius: 6px; display: block; }
1159 dashSkelShimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
1160 (prefers-reduced-motion: reduce) { .dash-skel { animation: none; } }
1161 .dash-skel-hero { height: 168px; border-radius: 16px; margin-bottom: var(--space-6); }
1162 .dash-skel-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-3); margin-bottom: var(--space-8); }
1163 .dash-skel-stat { height: 86px; border-radius: var(--radius); }
1164 .dash-skel-h { height: 18px; width: 180px; margin: 0 0 16px; border-radius: 5px; }
1165 .dash-skel-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: var(--space-4); margin-bottom: var(--space-8); }
1166 .dash-skel-card { height: 168px; border-radius: var(--radius); }
1167 .dash-skel-feed { display: flex; flex-direction: column; gap: 8px; }
1168 .dash-skel-feed-row { height: 52px; border-radius: var(--radius); }
1169 `,
1170 }}
1171 />
1172 <div class="dash-skel dash-skel-hero" aria-hidden="true" />
1173 <div class="dash-skel-stats" aria-hidden="true">
1174 <div class="dash-skel dash-skel-stat" />
1175 <div class="dash-skel dash-skel-stat" />
1176 <div class="dash-skel dash-skel-stat" />
1177 <div class="dash-skel dash-skel-stat" />
1178 </div>
1179 <div class="dash-skel dash-skel-h" aria-hidden="true" />
1180 <div class="dash-skel-grid" aria-hidden="true">
1181 <div class="dash-skel dash-skel-card" />
1182 <div class="dash-skel dash-skel-card" />
1183 <div class="dash-skel dash-skel-card" />
1184 <div class="dash-skel dash-skel-card" />
1185 </div>
1186 <div class="dash-skel dash-skel-h" aria-hidden="true" />
1187 <div class="dash-skel-feed" aria-hidden="true">
1188 <div class="dash-skel dash-skel-feed-row" />
1189 <div class="dash-skel dash-skel-feed-row" />
1190 <div class="dash-skel dash-skel-feed-row" />
1191 <div class="dash-skel dash-skel-feed-row" />
1192 <div class="dash-skel dash-skel-feed-row" />
1193 </div>
1194 <span style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0" role="status" aria-live="polite">
1195 Loading your command center…
1196 </span>
1197 </>
1198);
1199
11351200export default dashboard;
Modifiedsrc/routes/explore.tsx+66−0View fileUnifiedSplit
@@ -589,6 +589,23 @@ const ExploreStyle = () => (
589589 gap: 8px;
590590 flex-wrap: wrap;
591591 }
592
593 /* ─── Mobile (≤720px) ─── */
594 (max-width: 720px) {
595 .explore-wrap { padding: 0 2px; }
596 .explore-hero-search-field { min-width: 0; width: 100%; }
597 .explore-hero-search input[type="search"] { padding: 14px 14px 14px 38px; min-height: 44px; }
598 .explore-toolbar { flex-direction: column; align-items: stretch; }
599 .explore-filters { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
600 .explore-filter { min-height: 40px; padding: 10px 14px; }
601 .explore-grid { grid-template-columns: 1fr; }
602 .explore-skel-grid { grid-template-columns: 1fr; }
603 .explore-card { padding: 14px 16px; }
604 .explore-empty { padding: 40px 20px; }
605 .explore-foot { flex-direction: column; align-items: stretch; padding: 16px 18px; }
606 .explore-foot-actions { width: 100%; }
607 .explore-foot-actions .btn { flex: 1; min-width: 0; }
608 }
592609 `,
593610 }}
594611 />
@@ -624,6 +641,55 @@ explore.get("/explore", async (c) => {
624641 const sort = c.req.query("sort") || "recent";
625642 const topic = c.req.query("topic") || "";
626643
644 // ── Loading skeleton (flag-gated) ──
645 // Renders the explore shell + card-grid skeleton when `?skeleton=1`
646 // is set. Re-uses the existing `.explore-skel-*` styles from
647 // ExploreStyle. Behind a flag so we don't flash empty placeholders
648 // before the real list lands.
649 if (c.req.query("skeleton") === "1") {
650 return c.html(
651 <Layout title="Explore" user={user}>
652 <ExploreStyle />
653 <div class="explore-wrap">
654 <section class="explore-hero" aria-hidden="true">
655 <div class="explore-hero-bg">
656 <div class="explore-hero-orb" />
657 </div>
658 <div class="explore-hero-orb-2" />
659 <div class="explore-hero-inner">
660 <div class="explore-hero-text">
661 <div class="explore-hero-eyebrow">
662 <span class="explore-hero-eyebrow-dot" />
663 Discover
664 </div>
665 <h1 class="explore-hero-title">
666 What’s{" "}
667 <span class="explore-hero-title-grad">shipping</span>.
668 </h1>
669 <p class="explore-hero-sub">
670 Loading public repositories…
671 </p>
672 </div>
673 </div>
674 </section>
675 <div class="explore-skel-grid" aria-hidden="true">
676 {Array.from({ length: 9 }).map(() => (
677 <div class="explore-skel">
678 <div class="explore-skel-row is-title" />
679 <div class="explore-skel-row is-line" />
680 <div class="explore-skel-row is-line" />
681 <div class="explore-skel-row is-short" />
682 </div>
683 ))}
684 </div>
685 <span style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0" role="status" aria-live="polite">
686 Loading public repositories…
687 </span>
688 </div>
689 </Layout>
690 );
691 }
692
627693 let repoList: Array<{
628694 repo: typeof repositories.$inferSelect;
629695 ownerName: string;
Modifiedsrc/routes/issues.tsx+51−0View fileUnifiedSplit
@@ -147,6 +147,20 @@ const issuesStyles = `
147147 .issues-hero-actions .btn { flex: 1; min-width: 0; }
148148 }
149149
150 /* Mobile rules — added in the 720px sweep. Kept additive only. */
151 (max-width: 720px) {
152 .issues-toolbar { flex-direction: column; align-items: stretch; }
153 .issues-filters { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
154 .issues-filter { min-height: 40px; padding: 10px 14px; }
155 .issues-row { padding: 12px 14px; gap: 10px; }
156 .issues-row-side { flex-wrap: wrap; gap: 8px; }
157 .issues-detail-hero { padding: 18px; }
158 .issues-detail-attr { font-size: 13px; }
159 .issues-composer-actions { gap: 8px; }
160 .issues-composer-actions .btn { flex: 1; min-width: 0; min-height: 44px; }
161 .issues-empty { padding: 40px 20px; }
162 }
163
150164 /* Count chip + filter pills */
151165 .issues-toolbar {
152166 display: flex;
@@ -614,6 +628,43 @@ issueRoutes.get("/:owner/:repo/issues", softAuth, requireRepoAccess("read"), asy
614628 const page = Math.max(1, Number(c.req.query("page")) || 1);
615629 const offset = (page - 1) * perPage;
616630
631 // ── Loading skeleton (flag-gated) ──
632 // Renders an SSR'd row skeleton when `?skeleton=1` is set. Lets the
633 // user see the shape of the issue list before the DB count + select
634 // resolve. Behind a flag — we don't ship flashes.
635 if (c.req.query("skeleton") === "1") {
636 return c.html(
637 <Layout title={`Issues — ${ownerName}/${repoName}`} user={user}>
638 <IssuesStyle />
639 <RepoHeader owner={ownerName} repo={repoName} />
640 <IssueNav owner={ownerName} repo={repoName} active="issues" />
641 <style
642 dangerouslySetInnerHTML={{
643 __html: `
644 .issues-skel { background: linear-gradient(90deg, var(--bg-secondary) 0%, var(--bg-elevated) 50%, var(--bg-secondary) 100%); background-size: 200% 100%; animation: issuesSkelShimmer 1.4s infinite; border-radius: 6px; display: block; }
645 @keyframes issuesSkelShimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
646 @media (prefers-reduced-motion: reduce) { .issues-skel { animation: none; } }
647 .issues-skel-hero { height: 168px; border-radius: 16px; margin: 4px 0 24px; }
648 .issues-skel-toolbar { height: 44px; width: 240px; border-radius: 9999px; margin-bottom: 16px; }
649 .issues-skel-list { display: flex; flex-direction: column; gap: 8px; }
650 .issues-skel-row { height: 62px; border-radius: 10px; }
651 `,
652 }}
653 />
654 <div class="issues-skel issues-skel-hero" aria-hidden="true" />
655 <div class="issues-skel issues-skel-toolbar" aria-hidden="true" />
656 <div class="issues-skel-list" aria-hidden="true">
657 {Array.from({ length: 8 }).map(() => (
658 <div class="issues-skel issues-skel-row" />
659 ))}
660 </div>
661 <span style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0" role="status" aria-live="polite">
662 Loading issues for {ownerName}/{repoName}…
663 </span>
664 </Layout>
665 );
666 }
667
617668 const resolved = await resolveRepo(ownerName, repoName);
618669 if (!resolved) {
619670 return c.html(
Modifiedsrc/routes/pulls.tsx+28−0View fileUnifiedSplit
@@ -345,6 +345,16 @@ const PRS_LIST_STYLES = `
345345 .prs-hero-actions { width: 100%; }
346346 .prs-row-tags { margin-left: 0; }
347347 }
348
349 /* Additional mobile rules. Additive only. */
350 (max-width: 720px) {
351 .prs-hero { padding: 18px 18px 20px; }
352 .prs-hero-actions .prs-cta { flex: 1; min-width: 0; justify-content: center; min-height: 44px; }
353 .prs-tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; flex-wrap: nowrap; }
354 .prs-tab { min-height: 40px; padding: 9px 14px; white-space: nowrap; }
355 .prs-row { padding: 12px 14px; gap: 10px; }
356 .prs-row-icon { width: 24px; height: 24px; }
357 }
348358`;
349359
350360/* ──────────────────────────────────────────────────────────────────────
@@ -720,6 +730,24 @@ const PRS_DETAIL_STYLES = `
720730 .prs-merge-actions { width: 100%; }
721731 .prs-merge-actions > * { flex: 1; min-width: 0; }
722732 }
733
734 /* Additional mobile rules. Additive only. */
735 @media (max-width: 720px) {
736 .prs-detail-hero { padding: 18px; }
737 .prs-detail-meta { gap: 8px 12px; font-size: 12.5px; }
738 .prs-detail-tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; flex-wrap: nowrap; }
739 .prs-detail-tab { white-space: nowrap; min-height: 44px; padding: 12px 14px; }
740 .prs-gate-row { flex-wrap: wrap; padding: 12px 14px; }
741 .prs-gate-name { min-width: 0; }
742 .prs-gate-head { padding: 12px 14px; flex-wrap: wrap; }
743 .prs-gate-summary { margin-left: 0; }
744 .prs-merge-btn,
745 .prs-merge-ready-btn,
746 .prs-merge-back-draft { min-height: 44px; }
747 .prs-comment-body { padding: 12px 14px; }
748 .prs-comment-head { padding: 10px 12px; }
749 .prs-files-card { padding: 12px 14px; }
750 }
723751`;
724752
725753/**
Modifiedsrc/routes/web.tsx+56−0View fileUnifiedSplit
@@ -1515,6 +1515,57 @@ web.get("/:owner/:repo", async (c) => {
15151515 const { owner, repo } = c.req.param();
15161516 const user = c.get("user");
15171517
1518 // ── Loading skeleton (flag-gated) ──
1519 // Renders an SSR'd shell with file-tree + README placeholders when
1520 // `?skeleton=1` is set. Lets the user see the page structure before
1521 // git ops finish. Behind a flag for now so we never flash before the
1522 // real content lands.
1523 if (c.req.query("skeleton") === "1") {
1524 return c.html(
1525 <Layout title={`${owner}/${repo}`} user={user}>
1526 <style
1527 dangerouslySetInnerHTML={{
1528 __html: `
1529 .repo-skel { background: linear-gradient(90deg, var(--bg-secondary) 0%, var(--bg-elevated) 50%, var(--bg-secondary) 100%); background-size: 200% 100%; animation: repoSkelShimmer 1.4s infinite; border-radius: 6px; display: block; }
1530 @keyframes repoSkelShimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
1531 @media (prefers-reduced-motion: reduce) { .repo-skel { animation: none; } }
1532 .repo-skel-hero { height: 132px; border-radius: 16px; margin-bottom: var(--space-4); }
1533 .repo-skel-nav { height: 36px; border-radius: 8px; margin-bottom: var(--space-4); }
1534 .repo-skel-grid { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: var(--space-5); align-items: start; }
1535 @media (max-width: 960px) { .repo-skel-grid { grid-template-columns: minmax(0, 1fr); } }
1536 .repo-skel-branch { height: 32px; width: 200px; border-radius: 8px; margin-bottom: 12px; }
1537 .repo-skel-tree { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--space-5); }
1538 .repo-skel-tree-row { height: 36px; border-radius: 8px; }
1539 .repo-skel-readme { height: 320px; border-radius: 12px; }
1540 .repo-skel-side { display: flex; flex-direction: column; gap: var(--space-4); }
1541 .repo-skel-side-card { height: 180px; border-radius: 12px; }
1542 `,
1543 }}
1544 />
1545 <div class="repo-skel repo-skel-hero" aria-hidden="true" />
1546 <div class="repo-skel repo-skel-nav" aria-hidden="true" />
1547 <div class="repo-skel-grid" aria-hidden="true">
1548 <div>
1549 <div class="repo-skel repo-skel-branch" />
1550 <div class="repo-skel-tree">
1551 {Array.from({ length: 8 }).map(() => (
1552 <div class="repo-skel repo-skel-tree-row" />
1553 ))}
1554 </div>
1555 <div class="repo-skel repo-skel-readme" />
1556 </div>
1557 <aside class="repo-skel-side">
1558 <div class="repo-skel repo-skel-side-card" />
1559 <div class="repo-skel repo-skel-side-card" />
1560 </aside>
1561 </div>
1562 <span style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0" role="status" aria-live="polite">
1563 Loading {owner}/{repo}…
1564 </span>
1565 </Layout>
1566 );
1567 }
1568
15181569 // F1 — fire-and-forget traffic tracking. Never awaits; never throws.
15191570 trackByName(owner, repo, "view", {
15201571 userId: user?.id || null,
@@ -1961,6 +2012,11 @@ web.get("/:owner/:repo", async (c) => {
19612012 .repo-home-hero { padding: var(--space-4) var(--space-4); }
19622013 .repo-home-clone-body { flex-direction: column; align-items: stretch; }
19632014 .repo-home-clone-copy { width: 100%; }
2015 .repo-home-stat-row { gap: var(--space-2) var(--space-3); font-size: 12.5px; }
2016 .repo-home-side-val { max-width: 55%; }
2017 .repo-home-clone-tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; }
2018 .repo-home-clone-tab { min-height: 44px; padding: 11px 14px; }
2019 .repo-home-side-card { padding: var(--space-3); }
19642020 }
19652021 `;
19662022 const cloneHttpsUrl = `${config.appBaseUrl}/${owner}/${repo}.git`;
19672023