@chesapeake/stack 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to
7
7
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.1.8] - 2026-11-08
10
+
11
+ ### Added
12
+
13
+ - `lib/layoutTier`, `lib/useMediaQuery`, `lib/useLayoutTier`: compact /
14
+ hybrid / comfortable viewport tiers (Tailwind-aligned breakpoints)
15
+ - `StandardLayout` sets `data-layout-tier` and slightly tighter shell padding on
16
+ small viewports
17
+
9
18
  ## [0.1.7] - 2026-10-11
10
19
 
11
20
  ### Added
package/README.md CHANGED
@@ -17,6 +17,10 @@ system consists of:
17
17
  - `styles/index.css` that includes Tailwind CSS, design tokens, base styles, and
18
18
  component styles
19
19
  - `tailwind.preset.ts` for use in `tailwind.config.ts`
20
+ - **Layout tiers** (`compact` / `hybrid` / `comfortable`): `useLayoutTier`,
21
+ `LAYOUT_TIER_*` constants, and `data-layout-tier` on `StandardLayout`. See
22
+ denoise-ui `docs/responsive-layout.md` for product rules; apps own feature
23
+ toolbars.
20
24
 
21
25
  ## Installation
22
26
 
@@ -5,6 +5,7 @@
5
5
  import { useState } from "react";
6
6
  import { Button } from "../../../components/ui/button";
7
7
  import { useAuth } from "../auth";
8
+ import { useLayoutTier } from "../../../lib/useLayoutTier.ts";
8
9
  import { ProfilePage } from "./ProfilePage";
9
10
  import type { AuthUser } from "../auth/context";
10
11
 
@@ -46,6 +47,8 @@ export function StandardLayout({
46
47
  maxWidth,
47
48
  }: StandardLayoutProps) {
48
49
  const [showProfile, setShowProfile] = useState(false);
50
+ const layoutTier = useLayoutTier();
51
+ const shellPadding = "p-3 md:p-4";
49
52
  const { user: authUser } = useAuth();
50
53
 
51
54
  // Determine if user is logged in based on authUser
@@ -94,11 +97,14 @@ export function StandardLayout({
94
97
  return (
95
98
  <div
96
99
  className={`h-screen flex flex-col overflow-hidden ${className ?? ""}`}
100
+ data-layout-tier={layoutTier}
97
101
  >
98
102
  {/* Toolbar/Header */}
99
103
  <div className="border-b bg-background flex-shrink-0">
100
104
  <div className="flex justify-center">
101
- <div className={`w-full ${getMaxWidthClass(maxWidth)} p-4`}>
105
+ <div
106
+ className={`w-full ${getMaxWidthClass(maxWidth)} ${shellPadding}`}
107
+ >
102
108
  <div className="flex justify-between items-center">
103
109
  {headerContent ? (
104
110
  <div className="flex items-center gap-2 w-full">
@@ -242,11 +248,14 @@ export function StandardLayout({
242
248
  return (
243
249
  <div
244
250
  className={`h-screen flex flex-col overflow-hidden ${className ?? ""}`}
251
+ data-layout-tier={layoutTier}
245
252
  >
246
253
  {/* Toolbar/Header */}
247
254
  <div className="border-b bg-background flex-shrink-0">
248
255
  <div className="flex justify-center">
249
- <div className={`w-full ${getMaxWidthClass(maxWidth)} p-4`}>
256
+ <div
257
+ className={`w-full ${getMaxWidthClass(maxWidth)} ${shellPadding}`}
258
+ >
250
259
  <div className="flex justify-between items-center">
251
260
  {headerContent ? (
252
261
  <div className="flex items-center gap-2 w-full">
@@ -14,6 +14,20 @@ export {
14
14
  PageHeader,
15
15
  PageLayout,
16
16
  } from "./PageLayout.tsx";
17
+
18
+ export {
19
+ LAYOUT_TIER_COMPACT_MAX_PX,
20
+ LAYOUT_TIER_COMFORTABLE_MIN_PX,
21
+ LAYOUT_TIER_DIALOG_MIN_MEDIA,
22
+ LAYOUT_TIER_MEDIA,
23
+ resolveLayoutTier,
24
+ type LayoutTier,
25
+ } from "../../../lib/layoutTier.ts";
26
+ export {
27
+ useLayoutTier,
28
+ useLayoutTierWidth,
29
+ } from "../../../lib/useLayoutTier.ts";
30
+ export { useMediaQuery } from "../../../lib/useMediaQuery.ts";
17
31
  export type {
18
32
  PageContainerProps,
19
33
  PageContentProps,
@@ -1,23 +1,52 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { Card } from "../../ui/card.tsx";
3
3
 
4
+ /** Per-checkout readiness reported by the paired runner. */
5
+ export interface RepositoryReadinessEntry {
6
+ repository: string;
7
+ ready: boolean;
8
+ reason?: string;
9
+ }
10
+
4
11
  export interface RunnerStatusCardProps {
5
12
  name: string;
6
13
  state: string;
7
14
  details?: string;
8
15
  repositories?: string[];
16
+ /** Slug highlighted as the kickstart target for this page. */
17
+ targetRepository?: string;
18
+ /** Optional readiness per registered slug. */
19
+ repositoryReadiness?: RepositoryReadinessEntry[];
9
20
  children?: ReactNode;
10
21
  jobs?: ReactNode;
11
22
  }
12
23
 
24
+ function readinessForSlug(
25
+ slug: string,
26
+ entries: RepositoryReadinessEntry[] | undefined
27
+ ): RepositoryReadinessEntry | undefined {
28
+ if (!entries?.length) return undefined;
29
+ const lower = slug.toLowerCase();
30
+ return entries.find((entry) => entry.repository.toLowerCase() === lower);
31
+ }
32
+
13
33
  export function RunnerStatusCard({
14
34
  name,
15
35
  state,
16
36
  details,
17
37
  repositories = [],
38
+ targetRepository,
39
+ repositoryReadiness,
18
40
  children,
19
41
  jobs,
20
42
  }: RunnerStatusCardProps) {
43
+ const targetLower = targetRepository?.trim().toLowerCase();
44
+ const slugs = repositories.length
45
+ ? repositories
46
+ : targetRepository
47
+ ? [targetRepository]
48
+ : [];
49
+
21
50
  return (
22
51
  <Card
23
52
  className="space-y-3 rounded-md border p-4"
@@ -39,11 +68,35 @@ export function RunnerStatusCard({
39
68
  </div>
40
69
  <div>
41
70
  <p className="text-sm font-medium">Repository access</p>
42
- {repositories.length ? (
71
+ {slugs.length ? (
43
72
  <ul className="text-muted-foreground mt-1 list-inside list-disc text-sm">
44
- {repositories.map((repository) => (
45
- <li key={repository}>{repository}</li>
46
- ))}
73
+ {slugs.map((repository) => {
74
+ const readiness = readinessForSlug(
75
+ repository,
76
+ repositoryReadiness
77
+ );
78
+ const isTarget =
79
+ targetLower != null && repository.toLowerCase() === targetLower;
80
+ return (
81
+ <li
82
+ key={repository}
83
+ className={
84
+ isTarget ? "text-foreground font-medium" : undefined
85
+ }
86
+ title={readiness?.reason}
87
+ >
88
+ {repository}
89
+ {isTarget ? " · kickstart target" : ""}
90
+ {readiness != null
91
+ ? readiness.ready
92
+ ? " · ready"
93
+ : ` · not ready${
94
+ readiness.reason ? `: ${readiness.reason}` : ""
95
+ }`
96
+ : ""}
97
+ </li>
98
+ );
99
+ })}
47
100
  </ul>
48
101
  ) : (
49
102
  <p className="text-muted-foreground mt-1 text-sm">
package/index.ts CHANGED
@@ -29,4 +29,7 @@ export * from "./components/retail/index.ts";
29
29
  export * from "./types/retail-location.ts";
30
30
  export * from "./lib/utils.ts";
31
31
  export * from "./lib/githubIssue.ts";
32
+ export * from "./lib/layoutTier.ts";
33
+ export * from "./lib/useMediaQuery.ts";
34
+ export * from "./lib/useLayoutTier.ts";
32
35
  export { default as tailwindPreset } from "./tailwind.preset.ts";
@@ -0,0 +1,16 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import assert from "node:assert/strict";
5
+ import test from "node:test";
6
+ import { resolveLayoutTier } from "./layoutTier.ts";
7
+
8
+ await test("resolveLayoutTier boundaries", () => {
9
+ assert.equal(resolveLayoutTier(390), "compact");
10
+ assert.equal(resolveLayoutTier(767), "compact");
11
+ assert.equal(resolveLayoutTier(768), "hybrid");
12
+ assert.equal(resolveLayoutTier(683), "compact");
13
+ assert.equal(resolveLayoutTier(1023), "hybrid");
14
+ assert.equal(resolveLayoutTier(1024), "comfortable");
15
+ assert.equal(resolveLayoutTier(1920), "comfortable");
16
+ });
@@ -0,0 +1,26 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** Three-tier layout names aligned with Tailwind default breakpoints. */
5
+ export type LayoutTier = "compact" | "hybrid" | "comfortable";
6
+
7
+ /** Max width (px) for compact tier (below md). */
8
+ export const LAYOUT_TIER_COMPACT_MAX_PX = 767;
9
+
10
+ /** Min width (px) for comfortable tier (lg and up). */
11
+ export const LAYOUT_TIER_COMFORTABLE_MIN_PX = 1024;
12
+
13
+ export const LAYOUT_TIER_MEDIA = {
14
+ compact: `(max-width: ${String(LAYOUT_TIER_COMPACT_MAX_PX)}px)`,
15
+ hybrid: `(min-width: 768px) and (max-width: ${String(LAYOUT_TIER_COMFORTABLE_MIN_PX - 1)}px)`,
16
+ comfortable: `(min-width: ${String(LAYOUT_TIER_COMFORTABLE_MIN_PX)}px)`,
17
+ } as const;
18
+
19
+ /** Dialog vs drawer: at md and above use dialog shell. */
20
+ export const LAYOUT_TIER_DIALOG_MIN_MEDIA = "(min-width: 768px)";
21
+
22
+ export function resolveLayoutTier(viewportWidthPx: number): LayoutTier {
23
+ if (viewportWidthPx <= LAYOUT_TIER_COMPACT_MAX_PX) return "compact";
24
+ if (viewportWidthPx < LAYOUT_TIER_COMFORTABLE_MIN_PX) return "hybrid";
25
+ return "comfortable";
26
+ }
@@ -0,0 +1,32 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import {
6
+ LAYOUT_TIER_MEDIA,
7
+ type LayoutTier,
8
+ resolveLayoutTier,
9
+ } from "./layoutTier.ts";
10
+ import { useMediaQuery } from "./useMediaQuery.ts";
11
+ import { useMemo } from "react";
12
+
13
+ /**
14
+ * Current viewport layout tier (compact / hybrid / comfortable).
15
+ */
16
+ export function useLayoutTier(): LayoutTier {
17
+ const compact = useMediaQuery(LAYOUT_TIER_MEDIA.compact);
18
+ const comfortable = useMediaQuery(LAYOUT_TIER_MEDIA.comfortable);
19
+
20
+ return useMemo(() => {
21
+ if (compact) return "compact";
22
+ if (comfortable) return "comfortable";
23
+ return "hybrid";
24
+ }, [compact, comfortable]);
25
+ }
26
+
27
+ export function useLayoutTierWidth(): number {
28
+ if (typeof window === "undefined") return 1280;
29
+ return window.innerWidth;
30
+ }
31
+
32
+ export { resolveLayoutTier };
@@ -0,0 +1,29 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import { useEffect, useState } from "react";
6
+
7
+ /**
8
+ * Subscribe to a CSS media query; returns whether it currently matches.
9
+ */
10
+ export function useMediaQuery(query: string): boolean {
11
+ const [matches, setMatches] = useState(() => {
12
+ if (typeof window === "undefined") return false;
13
+ return window.matchMedia(query).matches;
14
+ });
15
+
16
+ useEffect(() => {
17
+ const media = window.matchMedia(query);
18
+ const onChange = () => {
19
+ setMatches(media.matches);
20
+ };
21
+ onChange();
22
+ media.addEventListener("change", onChange);
23
+ return () => {
24
+ media.removeEventListener("change", onChange);
25
+ };
26
+ }, [query]);
27
+
28
+ return matches;
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chesapeake/stack",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "A simple, reusable React component library and design system built on shadcn/ui with Tailwind CSS",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.2.0",
@@ -134,6 +134,9 @@
134
134
  "./components/ux/auth/github/types": "./components/ux/auth/github/types.ts",
135
135
  "./lib/utils": "./lib/utils.ts",
136
136
  "./lib/githubIssue": "./lib/githubIssue.ts",
137
+ "./lib/layoutTier": "./lib/layoutTier.ts",
138
+ "./lib/useMediaQuery": "./lib/useMediaQuery.ts",
139
+ "./lib/useLayoutTier": "./lib/useLayoutTier.ts",
137
140
  "./types/user": "./types/user.ts",
138
141
  "./types/retail-location": "./types/retail-location.ts",
139
142
  "./styles": "./styles/index.css",