Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .knowledge/design_system/foundations_and_components.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ result toolbar. Dismissal persists locally and the action applies the existing
`newOnly` filter contract.

Job details use a wordmark/X header, identity-first hierarchy, 32-point title,
`Data confidence` card, and a fixed safe-area action dock. The dock exposes opening
the original listing, sharing, saving, and reporting as four independent 44-point
actions. `Data confidence` shows verification availability, publication time,
source count, ordered provenance fields, and source links without inventing missing
facts. Community and author screens are identity-led destinations, not generic
search results with a label changed.
`Data confidence` card, and a fixed safe-area action dock. The compact dock keeps
opening the original listing as its persistent primary action and exposes sharing,
saving, and reporting through a native secondary-action sheet. Every dock control
and sheet row preserves the minimum interactive target. `Data confidence` shows
verification availability, publication time, source count, ordered provenance
fields, and source links without inventing missing facts. Community and author
screens are identity-led destinations, not generic search results with a label
changed.

Lucide React Native is the application icon language. Use the icon that matches the
responsive web surface, an explicit semantic color, and a consistent 1.8 stroke.
Expand Down
8 changes: 5 additions & 3 deletions .knowledge/patterns/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ modal dismissal, and focus restoration. A full-screen detail experience may requ
a different native presentation from the web dialog while preserving the same
information hierarchy and source-of-truth action.

The job-detail action dock lives outside its `ScrollView`, reports its measured
height, and adds that height to the scroll content inset. This keeps the last source,
tag, or similar job reachable above the bottom safe area.
The job-detail action dock lives outside its `ScrollView` as a compact single row,
reports its measured height, and adds that height to the scroll content inset. Its
native action sheet owns sharing, saving, and reporting without increasing the
persistent dock height. This keeps the last source, tag, or similar job reachable
above the bottom safe area.

Deep links and outbound GitHub links are security boundaries. Internal routes encode
stable IDs, repositories, and handles. External actions accept deliberate HTTPS
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# Compact Job Detail Actions 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:** Replace the four-button job-detail footer with a compact primary action and a native secondary-action sheet.

**Architecture:** `JobDetailActions` continues to own dock presentation and height measurement while adding local modal visibility. `JobDetailsScreen` keeps all product callbacks and receives no new behavioral responsibility; typed localization supplies the two new labels across all six catalogs.

**Tech Stack:** React Native 0.86, Expo SDK 57, TypeScript, NativeWind, React Native Testing Library, Jest, Lucide React Native.

---

## File map

- Modify `src/components/job-detail-actions/index.tsx`: render the compact dock and own the secondary-action modal.
- Modify `src/app/jobs/details/index.tsx`: pass the new localized action and close labels.
- Modify `src/i18n/types.ts`: extend the typed jobs message contract.
- Modify `src/i18n/messages.ts`: add translations for `actions` and `closeActions` in all six locale extensions.
- Modify `tests/components/job-detail-actions.test.tsx`: define compact dock, modal, callback, saved-state, dismissal, and measurement behavior.
- Modify `tests/app/job-details.test.tsx`: verify the screen opens the action sheet before sharing.
- Modify `tests/i18n/messages.test.ts`: require both new labels in every locale.
- Modify `.knowledge/design_system/foundations_and_components.md`: replace the obsolete four-independent-buttons contract.
- Modify `.knowledge/patterns/navigation.md`: document the reduced measured action dock.

### Task 1: Lock the localized contract

**Files:**
- Modify: `tests/i18n/messages.test.ts`
- Modify: `src/i18n/types.ts`
- Modify: `src/i18n/messages.ts`

- [ ] **Step 1: Write the failing locale assertions**

Extend the test-only jobs shape with `actions?: string` and `closeActions?: string`, then assert both values for every supported locale:

```ts
expect(copy?.jobs?.actions).toBeTruthy();
expect(copy?.jobs?.closeActions).toBeTruthy();
```

- [ ] **Step 2: Run the locale test and verify the red state**

Run:

```bash
npx jest tests/i18n/messages.test.ts --runInBand
```

Expected: FAIL because `jobs.actions` and `jobs.closeActions` are absent.

- [ ] **Step 3: Add the typed keys and six translations**

Add these fields beside the other job action labels in `src/i18n/types.ts`:

```ts
actions: string;
closeActions: string;
```

Add locale-specific values to each native jobs extension in `src/i18n/messages.ts`:

```ts
// en
actions: "Actions",
closeActions: "Close job actions",

// pt-BR
actions: "Ações",
closeActions: "Fechar ações da vaga",

// es
actions: "Acciones",
closeActions: "Cerrar acciones del empleo",

// it
actions: "Azioni",
closeActions: "Chiudi azioni del lavoro",

// fr
actions: "Actions",
closeActions: "Fermer les actions du poste",

// de
actions: "Aktionen",
closeActions: "Stellenaktionen schließen",
```

- [ ] **Step 4: Run the locale test and verify green**

Run the same Jest command. Expected: all localization tests PASS.

- [ ] **Step 5: Commit the localization micro-change**

```bash
git add src/i18n/types.ts src/i18n/messages.ts tests/i18n/messages.test.ts
git commit -m "feat(i18n): add compact job action labels"
```

### Task 2: Define the compact action behavior

**Files:**
- Modify: `tests/components/job-detail-actions.test.tsx`
- Modify: `tests/app/job-details.test.tsx`

- [ ] **Step 1: Replace the legacy component expectation with closed-dock behavior**

Render the component with `actions: "Actions"` and `closeActions: "Close job actions"`. Assert that `Open original listing` and `Actions` are visible, while `Share job`, `Save job`, and `Report a problem` are absent before opening the sheet.

Keep the layout event assertion, using a compact sample height:

```ts
await fireEvent(screen.getByTestId("job-detail-actions"), "layout", {
nativeEvent: { layout: { height: 72, width: 390, x: 0, y: 0 } },
});
expect(onHeightChange).toHaveBeenCalledWith(72);
```

- [ ] **Step 2: Add modal interaction expectations**

Open `Actions`, press each secondary row in a fresh render or reopen cycle, and verify only the corresponding callback fires. Assert the sheet disappears after every selection. Also fire `requestClose` on the modal test ID and press the localized close control.

For saved state, rerender with `isSaved` and `save: "Remove saved job"`, then assert:

```ts
expect(screen.getByLabelText("Remove saved job").props.accessibilityState).toEqual({
selected: true,
});
```

- [ ] **Step 3: Update the screen test to open the sheet before sharing**

Change the direct share press to:

```ts
await fireEvent.press(screen.getByText("Actions"));
await fireEvent.press(screen.getByText("Share job"));
expect(Share.share).toHaveBeenCalled();
```

- [ ] **Step 4: Run both tests and verify the red state**

```bash
npx jest tests/components/job-detail-actions.test.tsx tests/app/job-details.test.tsx --runInBand
```

Expected: FAIL because the dock still renders four stacked actions and has no action modal.

### Task 3: Implement the compact dock and action sheet

**Files:**
- Modify: `src/components/job-detail-actions/index.tsx`
- Modify: `src/app/jobs/details/index.tsx`

- [ ] **Step 1: Extend the component labels and add local visibility**

Add `actions` and `closeActions` to `JobDetailActionLabels`, import `MoreHorizontal`, `X`, `Modal`, and `useState`, then own the sheet state:

```ts
const [actionsVisible, setActionsVisible] = useState(false);

const runAction = (action: () => void) => {
setActionsVisible(false);
action();
};
```

- [ ] **Step 2: Replace the stacked buttons with one horizontal row**

Inside the safe-area dock, render a compact row with the existing mint original-listing button using `flex-1`, plus a bordered `Actions` trigger using `MoreHorizontal`. Preserve 44-point minimum height and the existing measured `SafeAreaView`.

- [ ] **Step 3: Render the native modal sheet**

Render a `Modal` with `animationType="slide"`, `presentationStyle="pageSheet"`, `visible={actionsVisible}`, `onRequestClose={() => setActionsVisible(false)}`, and `testID="job-detail-action-sheet"`. Its safe-area content contains:

```tsx
<Pressable onPress={() => runAction(onShare)}>{/* Share2 + labels.share */}</Pressable>
<Pressable
accessibilityState={{ selected: isSaved }}
onPress={() => runAction(onToggleSaved)}
>{/* Bookmark + labels.save */}</Pressable>
<Pressable onPress={() => runAction(onReport)}>{/* CircleAlert + labels.report */}</Pressable>
```

Each row uses a minimum 52-point height. Add a header titled with `labels.actions` and an icon-only close control labelled with `labels.closeActions`.

- [ ] **Step 4: Pass localized labels from the screen**

Add these properties to the existing labels object in `src/app/jobs/details/index.tsx`:

```ts
actions: messages.jobs.actions,
closeActions: messages.jobs.closeActions,
```

- [ ] **Step 5: Run the focused tests and verify green**

Run the two-test Jest command from Task 2. Expected: both suites PASS.

- [ ] **Step 6: Commit the behavior micro-change**

```bash
git add src/components/job-detail-actions/index.tsx src/app/jobs/details/index.tsx tests/components/job-detail-actions.test.tsx tests/app/job-details.test.tsx
git commit -m "feat(ui): compact job detail actions"
```

### Task 4: Align project knowledge and verify the application

**Files:**
- Modify: `.knowledge/design_system/foundations_and_components.md`
- Modify: `.knowledge/patterns/navigation.md`

- [ ] **Step 1: Update the design-system contract**

Replace the statement that the dock exposes four independent actions with a factual description of the primary original-listing button and the secondary native action sheet.

- [ ] **Step 2: Update the navigation contract**

Clarify that the measured dock is a compact single row and that its native sheet owns the secondary actions without changing scroll-inset behavior.

- [ ] **Step 3: Run the complete repository check**

```bash
npm run check
```

Expected: lint, typecheck, all Jest suites, and Expo Doctor PASS. If Expo Doctor alone fails because of local network or unavailable CocoaPods, rerun it with network access and report the native-tooling limitation separately; do not mask application failures.

- [ ] **Step 4: Inspect the final diff and working tree**

```bash
git diff --check
git status --short
```

Expected: no whitespace errors and only the planned knowledge files remain uncommitted.

- [ ] **Step 5: Commit the knowledge micro-change**

```bash
git add .knowledge/design_system/foundations_and_components.md .knowledge/patterns/navigation.md
git commit -m "docs: document compact job action dock"
```

103 changes: 103 additions & 0 deletions docs/superpowers/specs/2026-09-23-compact-job-detail-actions-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Compact Job Detail Actions

## Context

The job-detail screen currently presents opening the original listing, sharing,
saving, and reporting as four full-width buttons stacked inside its fixed action
dock. The layout makes every action appear equally important and consumes a large
part of short mobile viewports. The action dock must remain safe-area aware and
keep the final scroll content reachable, but it should no longer obscure so much
of the listing.

## Product decision

Keep opening the original listing as the only persistent primary action. Replace
the three stacked secondary buttons with one compact `Actions` trigger. The
trigger opens a native bottom sheet containing sharing, saving, and reporting as
separate rows.

This preserves immediate access to the authoritative source while letting the
user deliberately choose a secondary action without presenting four nested or
stacked buttons. Opening the original listing remains one tap; secondary actions
require two taps.

## Dock layout

The fixed dock contains one horizontal row above the bottom safe area:

- a flexible mint primary button for opening the original listing;
- a bounded secondary `Actions` button with an ellipsis icon;
- 44-point minimum interactive height for both controls;
- the existing paper surface and top hairline boundary;
- compact horizontal and vertical spacing from the shared NativeWind scale.

The dock continues to report its measured total height, including safe-area
padding. The job-detail `ScrollView` continues to use that measurement for its
bottom content inset, so the final source, tag, or similar listing remains
reachable above the dock.

## Secondary action sheet

The `Actions` trigger opens a React Native modal presented as a native page sheet.
The sheet owns a safe-area-aware paper surface, a title, an explicit close control,
and three independent rows:

1. share the job;
2. save the job, or remove it from saved jobs when already selected;
3. report a problem.

Each row combines the existing Lucide icon, localized label, and a minimum
52-point row height. The saved row exposes its selected state to assistive
technology and uses both text and filled icon state, rather than color alone.
Reporting keeps muted visual emphasis but remains a normal accessible action, not
a destructive confirmation.

Selecting an action closes the sheet before invoking its existing callback. The
sheet also closes through its close control, Android back behavior, and the native
modal dismissal path. Reopening it reflects the current saved state and label.

## Component boundaries

`JobDetailActions` remains the reusable owner of the dock, sheet visibility, and
presentation. Its callbacks remain narrow and unchanged: the job-details screen
continues to own external URL validation, native sharing, device-local saved
state, and analytics.

The component receives one additional localized `actions` label and one localized
sheet-close label. No service, context, navigation, or persistence boundary
changes. No new dependency is required.

## Localization and accessibility

Add the new visible labels to the typed job message contract and all six supported
catalogs. Existing share, save/unsave, and report strings are reused. The action
sheet title and close control receive explicit localized accessibility labels.

The implementation preserves Dynamic Type, light and dark themes, logical reading
order, 44-point targets, semantic button roles, selected state, bottom safe area,
and Android hardware-back dismissal. Long translations may wrap inside sheet rows
without reducing the touch target.

## Testing

Behavioral component tests will first describe the new contract:

- only the primary action and `Actions` trigger are visible in the closed dock;
- secondary actions are absent until the sheet opens;
- opening and closing the sheet works through visible controls and request-close;
- each secondary row closes the sheet and calls only its matching callback;
- saved and unsaved states expose the correct label and accessibility state;
- dock layout measurement continues to report the compact measured height.

The job-detail screen test will verify that the new localized trigger opens the
sheet and that the existing source and sharing behaviors remain connected. The
complete repository check must pass after the implementation. This is a
JavaScript presentation change and does not require regenerating native projects.

## Out of scope

- changing analytics event contracts;
- changing external URL, sharing, saving, or reporting behavior;
- adding swipe gestures or custom animation systems;
- redesigning the job-detail content or header;
- promoting the compact action pattern to unrelated screens.
2 changes: 2 additions & 0 deletions src/app/jobs/details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ export function JobDetailsScreen({ id }: JobDetailsScreenProps): React.ReactNode
<JobDetailActions
isSaved={candidate.isSaved(item.id)}
labels={{
actions: messages.jobs.actions,
closeActions: messages.jobs.closeActions,
openOriginal: messages.jobs.openOriginal,
report: messages.jobs.reportProblem,
save: candidate.isSaved(item.id)
Expand Down
Loading
Loading