Add CI and Release workflows; enhance loading skeletons and splash screen

- Introduced CI workflow for automated testing and validation on push and pull request events.
- Added Release workflow for packaging and distributing the application on version tag pushes.
- Enhanced loading skeletons in ChatHistory and ProjectsGrid components for improved visual feedback.
- Updated splash screen with new animations and styles for better user experience.
- Refined CSS variables for skeleton loading states to ensure consistency across themes.
This commit is contained in:
matt 2026-02-11 20:31:27 +09:00
parent 5a0e4c474f
commit e8f65cf492
7 changed files with 286 additions and 73 deletions

45
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,45 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
validate:
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.25.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Typecheck
run: pnpm typecheck
- name: Lint
run: pnpm lint
- name: Test
run: pnpm test
- name: Build
run: pnpm build

48
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,48 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
package:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.25.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Build app
run: pnpm build
- name: Package macOS
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: pnpm dist:mac
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release-macos
path: release/**
if-no-files-found: error

View file

@ -1,20 +1,40 @@
/**
* Loading skeleton for ChatHistory while conversation is loading.
* Industrial shimmer with organic line widths no generic pulse.
*/
export const ChatHistoryLoadingState = (): JSX.Element => {
const rows = [
{ user: ['85%', '60%'], ai: ['92%', '70%', '82%', '45%'] },
{ user: ['75%', '92%', '40%'], ai: ['88%', '65%', '78%'] },
{ user: ['95%', '55%'], ai: ['72%', '85%', '60%', '92%', '35%'] },
];
return (
<div className="flex flex-1 items-center justify-center overflow-hidden bg-[#141416]">
<div className="flex flex-1 items-center justify-center overflow-hidden bg-surface">
<div className="w-full max-w-5xl space-y-8 px-6">
{/* Loading skeleton */}
{[1, 2, 3].map((i) => (
<div key={i} className="animate-pulse space-y-6">
{/* User message skeleton - right aligned */}
{rows.map((row, i) => (
<div key={i} className="space-y-6">
{/* User message skeleton — right aligned */}
<div className="flex justify-end">
<div className="h-16 w-2/3 rounded-2xl rounded-br-sm border border-white/5 bg-[#27272A]/50" />
<div className="w-2/3 space-y-2">
{row.user.map((width, j) => (
<div
key={j}
className="skeleton-shimmer ml-auto h-3 rounded-sm"
style={{ width, backgroundColor: 'var(--skeleton-base)' }}
/>
))}
</div>
</div>
{/* AI response skeleton - left aligned with border accent */}
<div className="border-l-2 border-white/5 pl-3">
<div className="h-24 w-full rounded-lg bg-[#27272A]/30" />
{/* AI response skeleton — left aligned with border accent */}
<div className="space-y-2.5 border-l-2 border-border pl-3">
{row.ai.map((width, j) => (
<div
key={j}
className="skeleton-shimmer h-3 rounded-sm"
style={{ width, backgroundColor: 'var(--skeleton-base)' }}
/>
))}
</div>
</div>
))}

View file

@ -54,7 +54,7 @@ const CommandSearch = ({ value, onChange }: Readonly<CommandSearchProps>): React
<div className="relative mx-auto w-full max-w-xl">
{/* Search container with glow effect on focus */}
<div
className={`relative flex items-center gap-3 rounded-lg border bg-surface-raised px-4 py-3 transition-all duration-200 ${
className={`relative flex items-center gap-3 rounded-sm border bg-surface-raised px-4 py-3 transition-all duration-200 ${
isFocused
? 'border-zinc-500 shadow-[0_0_20px_rgba(255,255,255,0.04)] ring-1 ring-zinc-600/30'
: 'border-border hover:border-zinc-600'
@ -154,14 +154,14 @@ const RepositoryCard = ({
return (
<button
onClick={onClick}
className={`group relative flex min-h-[120px] flex-col overflow-hidden rounded-xl border p-4 text-left transition-all duration-300 ${
className={`group relative flex min-h-[120px] flex-col overflow-hidden rounded-sm border p-4 text-left transition-all duration-300 ${
isHighlighted
? 'border-border-emphasis bg-surface-raised'
: 'bg-surface/50 border-border hover:border-border-emphasis hover:bg-surface-raised'
} `}
>
{/* Icon with subtle border */}
<div className="mb-3 flex size-8 items-center justify-center rounded-lg border border-border bg-surface-overlay transition-colors duration-300 group-hover:border-border-emphasis">
<div className="mb-3 flex size-8 items-center justify-center rounded-sm border border-border bg-surface-overlay transition-colors duration-300 group-hover:border-border-emphasis">
<FolderGit2 className="size-4 text-text-secondary transition-colors group-hover:text-text" />
</div>
@ -232,11 +232,11 @@ const NewProjectCard = (): React.JSX.Element => {
return (
<button
className="hover:bg-surface/30 group relative flex min-h-[120px] flex-col items-center justify-center rounded-xl border border-dashed border-border bg-transparent p-4 transition-all duration-300 hover:border-border-emphasis"
className="hover:bg-surface/30 group relative flex min-h-[120px] flex-col items-center justify-center rounded-sm border border-dashed border-border bg-transparent p-4 transition-all duration-300 hover:border-border-emphasis"
onClick={handleClick}
title="Select a project folder"
>
<div className="mb-2 flex size-8 items-center justify-center rounded-lg border border-dashed border-border transition-colors duration-300 group-hover:border-border-emphasis">
<div className="mb-2 flex size-8 items-center justify-center rounded-sm border border-dashed border-border transition-colors duration-300 group-hover:border-border-emphasis">
<FolderOpen className="size-4 text-text-muted transition-colors group-hover:text-text-secondary" />
</div>
<span className="text-xs text-text-muted transition-colors group-hover:text-text-secondary">
@ -295,24 +295,52 @@ const ProjectsGrid = ({
}, [repositoryGroups, searchQuery, maxProjects]);
if (repositoryGroupsLoading) {
// Organic widths per card — no repeating stamp
const titleWidths = [60, 66, 50, 55, 75, 45, 40, 65];
const pathWidths = [80, 75, 85, 66, 70, 80, 60, 72];
return (
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-4">
{Array.from({ length: 8 }).map((_, i) => (
<div
key={i}
className="skeleton-card bg-surface/50 flex min-h-[120px] flex-col rounded-xl border border-border p-4"
style={{ animationDelay: `${i * 80}ms` }}
className="skeleton-card flex min-h-[120px] flex-col rounded-sm border border-border p-4"
style={{
animationDelay: `${i * 80}ms`,
backgroundColor: 'var(--skeleton-base)',
}}
>
{/* Icon placeholder */}
<div className="mb-3 size-8 rounded-lg bg-surface-raised" />
<div
className="mb-3 size-8 rounded-sm"
style={{ backgroundColor: 'var(--skeleton-base-light)' }}
/>
{/* Title placeholder */}
<div className="mb-2 h-3.5 w-3/5 rounded bg-surface-raised" />
<div
className="mb-2 h-3.5 rounded-sm"
style={{
width: `${titleWidths[i]}%`,
backgroundColor: 'var(--skeleton-base-light)',
}}
/>
{/* Path placeholder */}
<div className="bg-surface-raised/60 mb-auto h-2.5 w-4/5 rounded" />
<div
className="mb-auto h-2.5 rounded-sm"
style={{
width: `${pathWidths[i]}%`,
backgroundColor: 'var(--skeleton-base-dim)',
}}
/>
{/* Meta row placeholder */}
<div className="mt-3 flex gap-2">
<div className="bg-surface-raised/40 h-2.5 w-16 rounded" />
<div className="bg-surface-raised/40 h-2.5 w-12 rounded" />
<div
className="h-2.5 w-16 rounded-sm"
style={{ backgroundColor: 'var(--skeleton-base-dim)' }}
/>
<div
className="h-2.5 w-12 rounded-sm"
style={{ backgroundColor: 'var(--skeleton-base-dim)' }}
/>
</div>
</div>
))}
@ -322,8 +350,8 @@ const ProjectsGrid = ({
if (filteredRepos.length === 0 && searchQuery.trim()) {
return (
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border px-8 py-16">
<div className="mb-4 flex size-12 items-center justify-center rounded-xl border border-border bg-surface-raised">
<div className="flex flex-col items-center justify-center rounded-sm border border-dashed border-border px-8 py-16">
<div className="mb-4 flex size-12 items-center justify-center rounded-sm border border-border bg-surface-raised">
<Search className="size-6 text-text-muted" />
</div>
<p className="mb-1 text-sm text-text-secondary">No projects found</p>
@ -334,8 +362,8 @@ const ProjectsGrid = ({
if (repositoryGroups.length === 0) {
return (
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border px-8 py-16">
<div className="mb-4 flex size-12 items-center justify-center rounded-xl border border-border bg-surface-raised">
<div className="flex flex-col items-center justify-center rounded-sm border border-dashed border-border px-8 py-16">
<div className="mb-4 flex size-12 items-center justify-center rounded-sm border border-border bg-surface-raised">
<FolderGit2 className="size-6 text-text-muted" />
</div>
<p className="mb-1 text-sm text-text-secondary">No projects found</p>

View file

@ -203,22 +203,28 @@ export const DateGroupedSessions = (): React.JSX.Element => {
}
if (sessionsLoading && sessions.length === 0) {
const widths = [
{ header: '30%', title: '75%', sub: '90%' },
{ header: '22%', title: '60%', sub: '80%' },
{ header: '26%', title: '85%', sub: '65%' },
];
return (
<div className="p-4">
<div className="space-y-3">
{[...Array<undefined>(3)].map((_, i) => (
<div key={i} className="animate-pulse">
{widths.map((w, i) => (
<div key={i} className="space-y-2">
<div
className="mb-3 h-3 w-1/4 rounded"
style={{ backgroundColor: 'var(--color-surface-raised)' }}
className="skeleton-shimmer h-3 rounded-sm"
style={{ backgroundColor: 'var(--skeleton-base-dim)', width: w.header }}
/>
<div
className="mb-2 h-4 w-2/3 rounded"
style={{ backgroundColor: 'var(--color-surface-raised)' }}
className="skeleton-shimmer h-4 rounded-sm"
style={{ backgroundColor: 'var(--skeleton-base)', width: w.title }}
/>
<div
className="h-3 w-full rounded"
style={{ backgroundColor: 'var(--color-surface-raised)', opacity: 0.5 }}
className="skeleton-shimmer h-3 rounded-sm"
style={{ backgroundColor: 'var(--skeleton-base-dim)', width: w.sub }}
/>
</div>
))}

View file

@ -186,6 +186,11 @@
--context-btn-bg-hover: rgba(255, 255, 255, 0.14);
--context-btn-active-bg: rgba(99, 102, 241, 0.45);
--context-btn-active-text: #e0e7ff;
/* Skeleton — tinted deep charcoal (2% cool shift) */
--skeleton-base: #24262c;
--skeleton-base-light: #2c2e35;
--skeleton-base-dim: rgba(36, 38, 44, 0.6);
}
/* Light theme overrides - Warm neutral palette for eye comfort */
@ -371,6 +376,11 @@
--context-btn-bg-hover: rgba(0, 0, 0, 0.1);
--context-btn-active-bg: rgba(99, 102, 241, 0.35);
--context-btn-active-text: #3730a3;
/* Skeleton — tinted cool gray */
--skeleton-base: #d6d8de;
--skeleton-base-light: #cdd0d7;
--skeleton-base-dim: rgba(205, 208, 215, 0.6);
}
* {
@ -485,21 +495,55 @@ body {
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.04) 40%,
rgba(255, 255, 255, 0.06) 50%,
rgba(255, 255, 255, 0.04) 60%,
rgba(255, 255, 255, 0.06) 40%,
rgba(255, 255, 255, 0.1) 50%,
rgba(255, 255, 255, 0.06) 60%,
transparent 100%
);
animation: shimmer 1.8s ease-in-out infinite;
animation: shimmer 1.2s ease-in-out infinite;
}
:root.light .skeleton-card::after {
background: linear-gradient(
90deg,
transparent 0%,
rgba(0, 0, 0, 0.03) 40%,
rgba(0, 0, 0, 0.05) 50%,
rgba(0, 0, 0, 0.03) 60%,
rgba(0, 0, 0, 0.04) 40%,
rgba(0, 0, 0, 0.07) 50%,
rgba(0, 0, 0, 0.04) 60%,
transparent 100%
);
}
/* Per-element shimmer — fast, high-contrast sweep */
.skeleton-shimmer {
position: relative;
overflow: hidden;
}
.skeleton-shimmer::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
transform: translateX(-100%);
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.06) 40%,
rgba(255, 255, 255, 0.1) 50%,
rgba(255, 255, 255, 0.06) 60%,
transparent 100%
);
animation: shimmer 1.2s ease-in-out infinite;
}
:root.light .skeleton-shimmer::after {
background: linear-gradient(
90deg,
transparent 0%,
rgba(0, 0, 0, 0.04) 40%,
rgba(0, 0, 0, 0.07) 50%,
rgba(0, 0, 0, 0.04) 60%,
transparent 100%
);
}

View file

@ -6,18 +6,43 @@
<link rel="icon" type="image/png" href="./favicon.png" />
<title>Claude Code Context</title>
<style>
@keyframes splash-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(350%); }
/* Splash: spotlight gradient + noise overlay */
#splash {
position: fixed; inset: 0; z-index: 9999;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
background: radial-gradient(ellipse 60% 50% at 50% 45%, #1c1c20 0%, #0e0e10 100%);
transition: opacity 0.3s ease-out;
}
#splash-bar {
animation: splash-slide 1.2s ease-in-out infinite;
#splash-noise {
position: absolute; inset: 0; width: 100%; height: 100%;
opacity: 0.03; pointer-events: none;
}
#splash-logo { margin-bottom: 18px; }
#splash-text {
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
font-size: 15px; font-weight: 500; letter-spacing: 0.05em;
color: #a1a1aa;
}
/* Logo quadrant breathing — clockwise: TL → TR → BR → BL */
@keyframes splash-q {
0%, 100% { opacity: 0.12; }
10%, 25% { opacity: 0.95; }
40% { opacity: 0.12; }
}
.splash-q {
animation: splash-q 3.2s cubic-bezier(0.4, 0, 0.2, 1) infinite both;
}
/* Light theme splash overrides */
:root.light #splash { background: #f4f4f5; }
:root.light #splash-text { color: #18181b; }
:root.light #splash-track { background: rgba(0,0,0,0.06); }
:root.light #splash-bar { background: rgba(0,0,0,0.15); }
:root.light #splash {
background: radial-gradient(ellipse 60% 50% at 50% 45%, #fafafa 0%, #e4e4e7 100%);
}
:root.light #splash-text { color: #52525b; }
:root.light #splash-noise { opacity: 0.02; }
:root.light .splash-logo-bg { fill: #e4e4e7; }
:root.light .splash-logo-shape { fill: #52525b; }
</style>
<script>
// Flash prevention: Apply cached theme before React loads
@ -32,30 +57,27 @@
</script>
</head>
<body>
<div id="splash" style="
position: fixed; inset: 0; z-index: 9999;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
background: #141416;
transition: opacity 0.3s ease-out;
">
<img src="./favicon.png" width="56" height="56" alt=""
style="border-radius: 14px; margin-bottom: 18px;" />
<div id="splash-text" style="
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 17px; font-weight: 500; letter-spacing: 0.02em;
color: #fafafa;
">Claude Code Context</div>
<div id="splash-track" style="
margin-top: 24px; width: 120px; height: 2px;
background: rgba(255,255,255,0.08); border-radius: 1px;
overflow: hidden;
">
<div id="splash-bar" style="
width: 40%; height: 100%;
background: rgba(255,255,255,0.25); border-radius: 1px;
"></div>
</div>
<div id="splash">
<!-- SVG noise texture — matte paper grain -->
<svg id="splash-noise" xmlns="http://www.w3.org/2000/svg">
<filter id="noiseFilter">
<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/>
</filter>
<rect width="100%" height="100%" filter="url(#noiseFilter)"/>
</svg>
<!-- Logo with animated quadrants -->
<svg id="splash-logo" viewBox="0 0 56 56" width="56" height="56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect class="splash-logo-bg" width="56" height="56" rx="14" fill="#1c1c20"/>
<!-- TL: small circle -->
<circle class="splash-q splash-logo-shape" style="animation-delay:0s" cx="19" cy="19" r="5" fill="#d4d4d8"/>
<!-- TR: right-facing semicircle -->
<path class="splash-q splash-logo-shape" style="animation-delay:0.8s" d="M34,12 A7,7 0 0,1 34,26 Z" fill="#d4d4d8"/>
<!-- BR: large circle -->
<circle class="splash-q splash-logo-shape" style="animation-delay:1.6s" cx="37" cy="37" r="6.5" fill="#d4d4d8"/>
<!-- BL: down-facing semicircle -->
<path class="splash-q splash-logo-shape" style="animation-delay:2.4s" d="M12,34 A7,7 0 0,0 26,34 Z" fill="#d4d4d8"/>
</svg>
<div id="splash-text">Claude Code Context</div>
</div>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>