跳到主要内容

CHAPTER 2 Store Showcase Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Rebuild CHAPTER 2 as a complete-image desktop case study beside an accessible “main stage + market navigation” mobile Store showcase.

Architecture: Keep the data and selected Store state in PortfolioHomepage, but extract the CHAPTER 2 desktop and mobile render units into small local components. Use a one-shot IntersectionObserver for section entrance state, keyed React content for Store-switch animation, and CSS Modules for device framing, responsive layout, reduced-motion behavior, and complete-image rendering.

Tech Stack: React 19, TypeScript, Docusaurus 3.10, CSS Modules, Node test runner, in-app Browser verification.

Global Constraints

  • Preserve the existing editorial portfolio language: paper background, serif headings, mono labels, fine rules, terracotta accent, and restrained motion.
  • Do not add animation, carousel, or UI dependencies.
  • Desktop and phone screenshots must use object-fit: contain and remain fully visible.
  • At 390px, all three Store controls must be visible without horizontal scrolling.
  • Support pointer, keyboard focus, semantic tabs, and prefers-reduced-motion: reduce.
  • Do not reset or overwrite the pre-existing staged Chapter 1 changes in styles.module.css and homepage-structure.test.mjs.
  • Because the target CSS and test files already contain staged user changes, record diff checkpoints after each task and defer feature commits rather than accidentally committing unrelated staged work.
  • If Shopify Theme Check is used, check only files modified by this task; do not run a full-theme check.

File Structure

  • Modify src/components/PortfolioHomepage/index.tsx: Store data typing, one-shot visibility state, desktop stage component, mobile Store stage, semantic navigation, and CHAPTER 2 composition.
  • Modify src/components/PortfolioHomepage/styles.module.css: full-image device frames, two-module hierarchy, market navigation, switch/entrance animation, and responsive rules.
  • Modify tests/homepage-structure.test.mjs: replace the obsolete horizontal-overflow assertion with CHAPTER 2 structure, image-fit, accessibility, motion, and mobile-layout contracts.

No new runtime source file is required: the extracted components are page-specific and remain small enough to live beside PortfolioHomepage without creating an unnecessary public component API.


Task 1: Lock the CHAPTER 2 semantic contract

Files:

  • Modify: tests/homepage-structure.test.mjs:54-109
  • Test: tests/homepage-structure.test.mjs

Interfaces:

  • Consumes: the existing stores labels and image paths.

  • Produces: source-level contracts for DesktopStoreStage, MobileStoreShowcase, semantic tab attributes, stable panel id mobile-store-panel, and motion-state markup.

  • Step 1: Replace the obsolete overflow test and extend the CHAPTER 2 test

Add the following assertions to chapter 02 uses three named production storefronts after the existing Store label checks:

for (const contract of [
'DesktopStoreStage',
'MobileStoreShowcase',
'DESKTOP EXPERIENCE',
'MOBILE MARKETS · 03',
'role="tablist"',
'role="tab"',
'role="tabpanel"',
'aria-selected',
'aria-controls="mobile-store-panel"',
'data-motion-state',
]) {
assert.ok(homepage.includes(contract));
}

Replace responsive gallery exposes horizontal overflow before tablet breakpoint with:

test('chapter 02 shows complete images without a horizontal mobile gallery', () => {
const styles = readFileSync(
'src/components/PortfolioHomepage/styles.module.css',
'utf8',
);

for (const className of [
'desktopStoreImage',
'storePhoneImage',
'storeMarketNav',
'workShowcase',
]) {
assert.ok(styles.includes(`.${className}`));
}

assert.match(
styles,
/\.desktopStoreImage\s*\{[\s\S]*?object-fit: contain;/,
);
assert.match(
styles,
/\.storePhoneImage\s*\{[\s\S]*?object-fit: contain;/,
);
assert.doesNotMatch(styles, /\.storeTabs\s*\{[\s\S]*?min-width: 520px;/);
assert.match(
styles,
/@media \(max-width: 700px\)[\s\S]*?\.storeMarketNav\s*\{[\s\S]*?grid-template-columns: repeat\(3, minmax\(0, 1fr\)\);/,
);
});
  • Step 2: Run the homepage test and verify the new contract fails

Run:

pnpm test:homepage

Expected: FAIL because the source does not yet contain DesktopStoreStage, MobileStoreShowcase, role="tabpanel", or the new CSS class names.

  • Step 3: Record the failing-test checkpoint

Run:

git diff -- tests/homepage-structure.test.mjs
git status --short

Expected: only the new unstaged CHAPTER 2 test additions appear on top of the existing staged test change; do not reset or commit the existing staged lines.


Task 2: Implement the accessible Store stage and entrance state

Files:

  • Modify: src/components/PortfolioHomepage/index.tsx:1-41,125-184,251-304
  • Test: tests/homepage-structure.test.mjs

Interfaces:

  • Consumes: stores, StoreId, selectedStoreId, and setSelectedStoreId.

  • Produces: Store, DesktopStoreStage, MobileStoreShowcase, stable panel id mobile-store-panel, and workMotionState: 'idle' | 'ready' | 'visible'.

  • Step 1: Add React hooks and Store typing

Replace the React import and extend the Store types with:

import type {KeyboardEvent, ReactNode} from 'react';
import {useEffect, useRef, useState} from 'react';

type Store = (typeof stores)[number];
type StoreId = Store['id'];
type WorkMotionState = 'idle' | 'ready' | 'visible';
  • Step 2: Add the desktop case-study render unit

Insert before PortfolioHomepage:

function DesktopStoreStage(): ReactNode {
return (
<article className={styles.desktopExperience}>
<div className={styles.moduleHeading}>
<span>DESKTOP EXPERIENCE</span>
<span>01 / RESPONSIVE</span>
</div>
<div className={styles.desktopBrowserFrame}>
<div className={styles.desktopBrowserChrome} aria-hidden="true">
<span className={styles.browserDots}><i /><i /><i /></span>
<span>shokz.co.kr</span>
<span></span>
</div>
<img
className={styles.desktopStoreImage}
src="/assets/home/韶音韩国官网.png"
alt="SHOKZ 韩国 Shopify 官网桌面端首页"
loading="lazy"
width={3818}
height={1864}
/>
</div>
<div className={styles.desktopStoreMeta}>
<span>SHOKZ 韩国官网 · 桌面端</span>
<span>RESPONSIVE · LOCALIZATION</span>
</div>
</article>
);
}
  • Step 3: Add the mobile Store showcase render unit

Insert after DesktopStoreStage:

function MobileStoreShowcase({
selectedStore,
onSelect,
}: {
selectedStore: Store;
onSelect: (storeId: StoreId) => void;
}): ReactNode {
const handleTabKeyDown = (
event: KeyboardEvent<HTMLButtonElement>,
storeIndex: number,
) => {
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
return;
}

event.preventDefault();
const direction = ['ArrowRight', 'ArrowDown'].includes(event.key) ? 1 : -1;
const nextIndex = (storeIndex + direction + stores.length) % stores.length;
const nextStore = stores[nextIndex];
onSelect(nextStore.id);
document.getElementById(`store-tab-${nextStore.id}`)?.focus();
};

return (
<article className={styles.mobileMarkets}>
<div className={styles.moduleHeading}>
<span>MOBILE MARKETS · 03</span>
<span>SELECT A MARKET</span>
</div>
<div className={styles.mobileMarketsLayout}>
<div
id="mobile-store-panel"
key={selectedStore.id}
className={styles.storeStage}
role="tabpanel"
aria-labelledby={`store-tab-${selectedStore.id}`}>
<div className={styles.storePhoneFrame}>
<span className={styles.storePhoneSpeaker} aria-hidden="true" />
<div className={styles.storePhoneViewport}>
<img
className={styles.storePhoneImage}
src={selectedStore.image}
alt={selectedStore.alt}
loading={selectedStore.id === stores[0].id ? 'eager' : 'lazy'}
width={selectedStore.width}
height={selectedStore.height}
/>
</div>
<span className={styles.storePhoneHome} aria-hidden="true" />
</div>
<div className={styles.storeCaption} aria-live="polite">
<span>{selectedStore.label}</span>
<strong>{selectedStore.title}</strong>
<p>{selectedStore.summary}</p>
</div>
</div>
<ul
className={styles.storeMarketNav}
role="tablist"
aria-label="负责开发的三个 Shopify 官网移动端">
{stores.map((store, index) => {
const isSelected = selectedStore.id === store.id;
return (
<li key={store.id}>
<button
id={`store-tab-${store.id}`}
type="button"
role="tab"
aria-selected={isSelected}
aria-controls="mobile-store-panel"
tabIndex={isSelected ? 0 : -1}
className={isSelected ? styles.activeStore : undefined}
onClick={() => onSelect(store.id)}
onKeyDown={(event) => handleTabKeyDown(event, index)}>
<span className={styles.storeIndex}>{String(index + 1).padStart(2, '0')}</span>
<span className={styles.storeNavCopy}>
<strong>{store.label}</strong>
<small>{store.title}</small>
</span>
<img
className={styles.storeThumb}
src={store.image}
alt=""
aria-hidden="true"
loading="lazy"
width={store.width}
height={store.height}
/>
</button>
</li>
);
})}
</ul>
</div>
</article>
);
}
  • Step 4: Add the one-shot CHAPTER 2 visibility state

At the start of PortfolioHomepage, add:

const workSectionRef = useRef<HTMLElement>(null);
const [workMotionState, setWorkMotionState] = useState<WorkMotionState>('idle');

useEffect(() => {
const section = workSectionRef.current;
if (!section) return;

if (!('IntersectionObserver' in window)) {
setWorkMotionState('visible');
return;
}

setWorkMotionState('ready');
const observer = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return;
setWorkMotionState('visible');
observer.disconnect();
}, {threshold: 0.18});

observer.observe(section);
return () => observer.disconnect();
}, []);
  • Step 5: Replace the old CHAPTER 2 gallery markup

Add ref={workSectionRef} and data-motion-state={workMotionState} to the #work section. Replace the old .featuredStore and .storeGallery blocks with:

<div className={styles.workShowcase}>
<DesktopStoreStage />
<MobileStoreShowcase
selectedStore={selectedStore}
onSelect={setSelectedStoreId}
/>
</div>
  • Step 6: Run the homepage test

Run:

pnpm test:homepage

Expected: the semantic assertions PASS; the CSS contract still FAILS because Task 3 has not added the new classes.

  • Step 7: Run TypeScript checking

Run:

pnpm typecheck

Expected: PASS with no React, ref, keyboard-event, or image-loading type errors.

  • Step 8: Record the component checkpoint

Run:

git diff -- src/components/PortfolioHomepage/index.tsx tests/homepage-structure.test.mjs
git status --short

Expected: CHAPTER 2 component changes are unstaged; the two pre-existing staged files remain staged and preserved.


Task 3: Build the full-image editorial layout and motion system

Files:

  • Modify: src/components/PortfolioHomepage/styles.module.css:378-566,1125-1475
  • Test: tests/homepage-structure.test.mjs

Interfaces:

  • Consumes: every CSS Module class introduced by DesktopStoreStage and MobileStoreShowcase, plus data-motion-state values.

  • Produces: two-module desktop layout, complete-image device frames, market tab states, responsive navigation, entrance animation, switch animation, and reduced-motion overrides.

  • Step 1: Replace the old featured/gallery styles with the new desktop module styles

Remove the .featuredStore, .featuredStoreImage, .storeGallery, and .storeTabs rules. Add:

.workShowcase {
display: grid;
min-width: 0;
grid-column: 1 / -1;
grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
gap: 56px;
align-items: start;
}

.desktopExperience,
.mobileMarkets {
min-width: 0;
}

.moduleHeading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding-bottom: 11px;
border-bottom: 1px solid var(--soft-rule);
color: var(--muted);
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
font-size: 0.58rem;
letter-spacing: 0.06em;
}

.desktopBrowserFrame {
overflow: hidden;
margin-top: 18px;
border: 1px solid rgba(22, 22, 19, 0.52);
border-radius: 6px 6px 2px 2px;
background: #f8f6f0;
box-shadow: 0 18px 38px rgba(65, 51, 35, 0.11);
transition: transform 300ms cubic-bezier(0.2, 0.7, 0.2, 1), box-shadow 300ms ease;
}

.desktopExperience:hover .desktopBrowserFrame {
transform: translateY(-3px);
box-shadow: 0 24px 46px rgba(65, 51, 35, 0.15);
}

.desktopBrowserChrome {
display: grid;
height: 30px;
grid-template-columns: 70px 1fr 70px;
align-items: center;
padding: 0 10px;
color: rgba(22, 22, 19, 0.55);
border-bottom: 1px solid rgba(22, 22, 19, 0.18);
background: #f2efe8;
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
font-size: 0.44rem;
}

.desktopBrowserChrome > span:nth-child(2) { justify-self: center; }
.desktopBrowserChrome > span:last-child { justify-self: end; }

.desktopStoreImage {
display: block;
width: 100%;
height: auto;
object-fit: contain;
background: #ede8de;
}

.desktopStoreMeta {
display: flex;
justify-content: space-between;
gap: 16px;
margin-top: 11px;
color: var(--muted);
font-size: 0.61rem;
letter-spacing: 0.035em;
}
  • Step 2: Add the mobile stage and market-navigation styles
.mobileMarkets {
padding-left: 48px;
border-left: 1px solid var(--soft-rule);
}

.mobileMarketsLayout {
display: grid;
min-width: 0;
grid-template-columns: minmax(180px, 1.05fr) minmax(170px, 0.95fr);
gap: 24px;
align-items: start;
margin-top: 18px;
}

.storeStage {
min-width: 0;
animation: storeStageEnter 360ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}

.storePhoneFrame {
position: relative;
width: min(100%, 230px);
margin-inline: auto;
padding: 11px 7px 14px;
border: 2px solid #25241f;
border-radius: 24px;
background: #f8f6f0;
box-shadow: 0 18px 34px rgba(45, 36, 26, 0.18);
}

.storePhoneViewport {
display: flex;
height: clamp(330px, 34vw, 420px);
align-items: flex-start;
justify-content: center;
overflow: hidden;
border-radius: 17px;
background: #ebe5da;
}

.storePhoneImage {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
object-position: center top;
background: #f8f6f0;
}

.storePhoneSpeaker,
.storePhoneHome {
position: absolute;
z-index: 2;
left: 50%;
border-radius: 999px;
background: #25241f;
transform: translateX(-50%);
}

.storePhoneSpeaker { top: 5px; width: 22%; height: 3px; }
.storePhoneHome { bottom: 5px; width: 27%; height: 2px; }

.storeMarketNav {
display: grid;
min-width: 0;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}

.storeMarketNav button {
position: relative;
display: grid;
width: 100%;
min-height: 92px;
grid-template-columns: 28px 1fr 42px;
gap: 10px;
align-items: center;
padding: 11px 10px;
color: var(--ink);
border: 1px solid transparent;
background: transparent;
cursor: pointer;
text-align: left;
transition: transform 220ms ease, border-color 220ms ease, background 220ms ease;
}

.storeMarketNav button::after {
position: absolute;
right: 10px;
bottom: 7px;
left: 48px;
height: 2px;
background: linear-gradient(90deg, var(--terracotta) 0 0) left / 0 100% no-repeat;
content: "";
transition: background-size 250ms ease;
}

.storeMarketNav button:hover {
border-color: rgba(22, 22, 19, 0.2);
transform: translateY(-2px);
}

.storeMarketNav button:hover::after { background-size: 30% 100%; }

.storeMarketNav button.activeStore {
border-color: rgba(185, 70, 45, 0.34);
background: rgba(255, 255, 255, 0.34);
}

.storeMarketNav button.activeStore::after { background-size: 100% 100%; }

.storeMarketNav button:focus-visible {
outline: 2px solid var(--cobalt);
outline-offset: 3px;
}

.storeIndex {
color: var(--terracotta);
font-family: Georgia, "Times New Roman", serif;
font-size: 1.15rem;
}

.storeNavCopy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 5px;
}

.storeNavCopy strong {
overflow: hidden;
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
font-size: 0.6rem;
text-overflow: ellipsis;
white-space: nowrap;
}

.storeNavCopy small {
color: var(--muted);
font-family: "Songti SC", "STSong", serif;
font-size: 0.72rem;
}

.storeThumb {
display: block;
width: 42px;
height: 64px;
border: 1px solid rgba(22, 22, 19, 0.18);
background: #ebe5da;
object-fit: contain;
object-position: center top;
}

.storeCaption {
display: grid;
grid-template-columns: auto 1fr;
gap: 5px 12px;
margin-top: 14px;
padding-top: 13px;
border-top: 1px dashed rgba(22, 22, 19, 0.28);
}

.storeCaption span { font-family: "IBM Plex Mono", "SFMono-Regular", monospace; font-size: 0.62rem; }
.storeCaption strong { font-family: "Songti SC", "STSong", serif; }
.storeCaption p { grid-column: 1 / -1; margin-bottom: 0; color: var(--muted); font-size: 0.72rem; line-height: 1.6; }
  • Step 3: Add entrance and switch keyframes
.workSection[data-motion-state="ready"] :is(.workIntro, .companyMeta, .desktopExperience, .mobileMarkets) {
opacity: 0;
transform: translateY(16px);
}

.workSection[data-motion-state="visible"] .workIntro,
.workSection[data-motion-state="visible"] .companyMeta,
.workSection[data-motion-state="visible"] .desktopExperience,
.workSection[data-motion-state="visible"] .mobileMarkets {
animation: workModuleEnter 520ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}

.workSection[data-motion-state="visible"] .companyMeta { animation-delay: 40ms; }
.workSection[data-motion-state="visible"] .desktopExperience { animation-delay: 80ms; }
.workSection[data-motion-state="visible"] .mobileMarkets { animation-delay: 160ms; }
.workSection[data-motion-state="visible"] .storeMarketNav li { animation: workModuleEnter 420ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
.workSection[data-motion-state="visible"] .storeMarketNav li:nth-child(1) { animation-delay: 220ms; }
.workSection[data-motion-state="visible"] .storeMarketNav li:nth-child(2) { animation-delay: 280ms; }
.workSection[data-motion-state="visible"] .storeMarketNav li:nth-child(3) { animation-delay: 340ms; }

@keyframes workModuleEnter {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}

@keyframes storeStageEnter {
from { opacity: 0; transform: translateX(14px) scale(1.01); }
to { opacity: 1; transform: translateX(0) scale(1); }
}
  • Step 4: Replace old responsive gallery overrides

At max-width: 1100px, use:

.workShowcase { grid-template-columns: 1fr; gap: 48px; }
.mobileMarkets { padding-top: 38px; padding-left: 0; border-top: 1px solid var(--soft-rule); border-left: 0; }
.mobileMarketsLayout { grid-template-columns: minmax(260px, 0.85fr) minmax(300px, 1.15fr); }
.storePhoneViewport { height: 410px; }

Add an 819px breakpoint:

@media (max-width: 819px) {
.mobileMarketsLayout { grid-template-columns: 1fr; gap: 24px; }
.storeMarketNav { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.storeMarketNav button { min-height: 104px; grid-template-columns: 24px 1fr; }
.storeThumb { display: none; }
}

At max-width: 700px, remove old .featuredStoreImage, .storeGallery, .storeTabs, and sticky .storeCaption overrides, then use:

.workShowcase { gap: 38px; }
.moduleHeading { font-size: 0.52rem; }
.desktopStoreMeta { flex-direction: column; gap: 5px; }
.mobileMarkets { padding-top: 30px; }
.storePhoneFrame { width: min(78vw, 250px); }
.storePhoneViewport { height: min(124vw, 430px); }
.storeMarketNav { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
.storeMarketNav button { min-height: 94px; grid-template-columns: 1fr; gap: 4px; padding: 9px 6px; text-align: center; }
.storeMarketNav button::after { right: 8px; left: 8px; }
.storeIndex { font-size: 1rem; }
.storeNavCopy strong { font-size: 0.5rem; }
.storeNavCopy small { font-size: 0.66rem; }
  • Step 5: Extend the existing reduced-motion rule

The existing global reduced-motion rule already shortens all animation and transition durations. Add explicit transform reset so pending entrance state cannot hide content:

.workSection[data-motion-state] :is(.workIntro, .companyMeta, .desktopExperience, .mobileMarkets, .storeMarketNav li) {
opacity: 1 !important;
transform: none !important;
}
  • Step 6: Run the homepage tests

Run:

pnpm test:homepage

Expected: all homepage tests PASS, including the new semantic and complete-image contracts and the pre-existing Chapter 1 offset test.

  • Step 7: Run type checking and production build

Run:

pnpm typecheck
pnpm build

Expected: both commands exit 0; Docusaurus generates the production build without broken links or CSS Module errors.

  • Step 8: Record the layout checkpoint

Run:

git diff --check
git diff -- src/components/PortfolioHomepage/index.tsx src/components/PortfolioHomepage/styles.module.css tests/homepage-structure.test.mjs
git status --short

Expected: no whitespace errors; CHAPTER 2 changes are visible and the pre-existing staged Chapter 1 changes remain intact.


Task 4: Verify visual fidelity and interaction in Browser

Files:

  • Modify only if Browser finds a defect: src/components/PortfolioHomepage/index.tsx, src/components/PortfolioHomepage/styles.module.css, tests/homepage-structure.test.mjs
  • Test: live page at http://localhost:3000/#work

Interfaces:

  • Consumes: the built CHAPTER 2 UI and existing local Docusaurus server.

  • Produces: verified desktop/mobile layout, Store selection behavior, keyboard focus behavior, reduced-motion fallback, and console cleanliness.

  • Step 1: Reload and inspect desktop CHAPTER 2 at 1280×720

Use the in-app Browser viewport capability and navigate to http://localhost:3000/#work. Verify:

  • no horizontal document overflow;

  • desktop screenshot natural ratio is approximately 3818/1864 and no left/right crop occurs;

  • phone screenshot uses object-fit: contain and the complete page is visible;

  • the 56px module gap and vertical rule distinguish Desktop Experience from Mobile Markets;

  • all three market controls are visible.

  • Step 2: Test Store selection and keyboard navigation

Activate SHOKZ US and NOVILLA and verify the active tab, main image, heading, and description update together. Focus the selected tab and use ArrowLeft/ArrowRight to cycle; verify focus and aria-selected move to the new Store.

  • Step 3: Inspect mobile CHAPTER 2 at 390×844

Verify:

  • document scrollWidth equals clientWidth;

  • Desktop Experience and Mobile Markets are stacked;

  • the phone image is complete;

  • all three market buttons fit in one row without a native horizontal scrollbar;

  • Store switching does not create layout overflow or hide the active state.

  • Step 4: Inspect reduced motion and console logs

Emulate prefers-reduced-motion: reduce through the Browser page context if supported; otherwise inspect the matching CSS rule. Confirm content stays visible and selection remains immediate. Read Browser console logs and confirm no new error-level entries from CHAPTER 2.

  • Step 5: Fix any observed defect with a focused test-first loop

For each defect, add or tighten one assertion in homepage-structure.test.mjs, run pnpm test:homepage to observe failure, apply the smallest TSX/CSS correction, then rerun the targeted test and Browser check.

  • Step 6: Run final verification

Run:

pnpm test:homepage
pnpm typecheck
pnpm build
git diff --check

Expected: all commands exit 0. Do not claim completion until the fresh outputs are inspected.

  • Step 7: Present the final diff without committing overlapping staged work

Run:

git status --short
git diff --stat
git diff --cached --stat

Expected: clearly separate the pre-existing staged Chapter 1 changes from the unstaged CHAPTER 2 implementation. Offer the user a scoped staging/commit choice instead of committing the shared files automatically.