@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 +9 -0
- package/README.md +4 -0
- package/components/ux/StandardLayout/StandardLayout.tsx +11 -2
- package/components/ux/StandardLayout/index.ts +14 -0
- package/components/ux/runner/RunnerStatusCard.tsx +57 -4
- package/index.ts +3 -0
- package/lib/layoutTier.test.ts +16 -0
- package/lib/layoutTier.ts +26 -0
- package/lib/useLayoutTier.ts +32 -0
- package/lib/useMediaQuery.ts +29 -0
- package/package.json +4 -1
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
|
|
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
|
|
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
|
-
{
|
|
71
|
+
{slugs.length ? (
|
|
43
72
|
<ul className="text-muted-foreground mt-1 list-inside list-disc text-sm">
|
|
44
|
-
{
|
|
45
|
-
|
|
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.
|
|
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",
|