@the-bearded-bear/claude-craft 8.1.0 → 8.2.0
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/Dev/i18n/base/Common/hooks/templates/settings-hooks.json +1 -1
- package/Dev/i18n/de/Common/commands/getting-started.md +348 -0
- package/Dev/i18n/de/Common/templates/settings.json.template +32 -39
- package/Dev/i18n/en/Common/commands/getting-started.md +348 -0
- package/Dev/i18n/en/Common/templates/settings.json.template +10 -80
- package/Dev/i18n/es/Common/commands/getting-started.md +348 -0
- package/Dev/i18n/es/Common/templates/settings.json.template +32 -39
- package/Dev/i18n/fr/Common/commands/getting-started.md +348 -0
- package/Dev/i18n/fr/Common/templates/settings.json.template +32 -39
- package/Dev/i18n/pt/Common/commands/getting-started.md +348 -0
- package/Dev/i18n/pt/Common/templates/settings.json.template +32 -39
- package/Dev/scripts/check-prerequisites.sh +4 -3
- package/Dev/scripts/install-common-rules.sh +10 -2
- package/Dev/scripts/lib/install-tech-common.sh +3 -0
- package/Dev/scripts/lib/shell-ui.sh +2 -0
- package/Dev/scripts/tcl-common.sh +2 -0
- package/Infra/install-ansible-rules.sh +2 -0
- package/Infra/install-coolify-rules.sh +2 -0
- package/Infra/install-docker-rules.sh +2 -0
- package/Infra/install-frankenphp-rules.sh +2 -0
- package/Infra/install-hcloud-rules.sh +2 -0
- package/Infra/install-kubernetes-rules.sh +2 -0
- package/Infra/install-opentofu-rules.sh +2 -0
- package/Infra/install-pgbouncer-rules.sh +2 -0
- package/README.md +12 -3
- package/Tools/AgentTeams/lib/ralph-teams-adapter.sh +45 -8
- package/Tools/MultiAccount/claude-accounts.sh +7 -5
- package/Tools/PluginExport/export-plugin.sh +2 -0
- package/Tools/ProjectConfig/claude-projects.sh +2 -0
- package/Tools/RTK/install-rtk.sh +55 -2
- package/Tools/Ralph/lib/parallel-manager.sh +21 -3
- package/Tools/Recette/lib/browser-executor.sh +2 -0
- package/Tools/Recette/lib/chrome-check.sh +2 -0
- package/Tools/Recette/lib/plan-generator.sh +2 -0
- package/Tools/Recette/lib/regression-detector.sh +2 -0
- package/Tools/Recette/lib/report-generator.sh +2 -0
- package/Tools/Recette/lib/session.sh +2 -0
- package/Tools/Recette/lib/test-generator.sh +2 -0
- package/Tools/StatusLine/statusline.sh +3 -1
- package/Tools/lib/tools-ui.sh +2 -0
- package/cli/kanban/client/src/views/KanbanView.svelte +229 -1
- package/cli/lib/banner.js +2 -1
- package/cli/lib/check.js +10 -9
- package/cli/lib/symbols.js +88 -0
- package/package.json +11 -8
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
IFS=$'\n\t'
|
|
2
4
|
# =============================================================================
|
|
3
5
|
# Ralph Teams Adapter - Agent Teams Abstraction Layer
|
|
4
6
|
# Encapsulates Agent Teams API behind an interface compatible with
|
|
@@ -153,8 +155,13 @@ teams_create_team() {
|
|
|
153
155
|
_teams_log "Registered teammate: $dev_name (agent: dev)"
|
|
154
156
|
done
|
|
155
157
|
|
|
158
|
+
local teammate_count=0
|
|
159
|
+
if [[ -v _TEAMS_TEAMMATES ]]; then
|
|
160
|
+
teammate_count="${#_TEAMS_TEAMMATES[@]}"
|
|
161
|
+
fi
|
|
162
|
+
|
|
156
163
|
_teams_write_state "team_created"
|
|
157
|
-
_teams_log "Team created: $_TEAMS_TEAM_ID ($
|
|
164
|
+
_teams_log "Team created: $_TEAMS_TEAM_ID ($teammate_count teammates)"
|
|
158
165
|
|
|
159
166
|
echo "$_TEAMS_TEAM_ID"
|
|
160
167
|
}
|
|
@@ -286,8 +293,13 @@ teams_wait_completion() {
|
|
|
286
293
|
|
|
287
294
|
_teams_require_init
|
|
288
295
|
|
|
296
|
+
local story_count=0
|
|
297
|
+
if [[ -v _TEAMS_STORY_ASSIGNMENT ]]; then
|
|
298
|
+
story_count="${#_TEAMS_STORY_ASSIGNMENT[@]}"
|
|
299
|
+
fi
|
|
300
|
+
|
|
289
301
|
if [[ "$TEAMS_DRY_RUN" == "true" ]]; then
|
|
290
|
-
_teams_log "[DRY-RUN] Would wait for $
|
|
302
|
+
_teams_log "[DRY-RUN] Would wait for $story_count active stories"
|
|
291
303
|
# Simulate instant completion in dry-run
|
|
292
304
|
for story_id in "${!_TEAMS_STORY_ASSIGNMENT[@]}"; do
|
|
293
305
|
teams_mark_completed "$story_id"
|
|
@@ -295,9 +307,9 @@ teams_wait_completion() {
|
|
|
295
307
|
return 0
|
|
296
308
|
fi
|
|
297
309
|
|
|
298
|
-
_teams_log "Waiting for $
|
|
310
|
+
_teams_log "Waiting for $story_count active stories..."
|
|
299
311
|
|
|
300
|
-
while [[ $
|
|
312
|
+
while [[ $story_count -gt 0 ]]; do
|
|
301
313
|
# Check timeout
|
|
302
314
|
if [[ $timeout -gt 0 ]]; then
|
|
303
315
|
local elapsed=$(( $(date +%s) - start_time ))
|
|
@@ -311,6 +323,12 @@ teams_wait_completion() {
|
|
|
311
323
|
teams_watchdog
|
|
312
324
|
|
|
313
325
|
sleep "$TEAMS_WATCHDOG_INTERVAL"
|
|
326
|
+
|
|
327
|
+
# Update story count
|
|
328
|
+
story_count=0
|
|
329
|
+
if [[ -v _TEAMS_STORY_ASSIGNMENT ]]; then
|
|
330
|
+
story_count="${#_TEAMS_STORY_ASSIGNMENT[@]}"
|
|
331
|
+
fi
|
|
314
332
|
done
|
|
315
333
|
|
|
316
334
|
_teams_log "All stories completed"
|
|
@@ -352,8 +370,13 @@ teams_watchdog() {
|
|
|
352
370
|
local now
|
|
353
371
|
now=$(date +%s)
|
|
354
372
|
|
|
373
|
+
local story_count=0
|
|
374
|
+
if [[ -v _TEAMS_STORY_ASSIGNMENT ]]; then
|
|
375
|
+
story_count="${#_TEAMS_STORY_ASSIGNMENT[@]}"
|
|
376
|
+
fi
|
|
377
|
+
|
|
355
378
|
if [[ "$TEAMS_DRY_RUN" == "true" ]]; then
|
|
356
|
-
_teams_log "[DRY-RUN] Watchdog check: $
|
|
379
|
+
_teams_log "[DRY-RUN] Watchdog check: $story_count active assignments"
|
|
357
380
|
return 0
|
|
358
381
|
fi
|
|
359
382
|
|
|
@@ -518,8 +541,13 @@ teams_get_status() {
|
|
|
518
541
|
local active_count
|
|
519
542
|
active_count=$(teams_get_active_count 2>/dev/null || echo "0")
|
|
520
543
|
|
|
544
|
+
local teammate_count=0
|
|
545
|
+
if [[ -v _TEAMS_TEAMMATES ]]; then
|
|
546
|
+
teammate_count="${#_TEAMS_TEAMMATES[@]}"
|
|
547
|
+
fi
|
|
548
|
+
|
|
521
549
|
local teammate_list="[]"
|
|
522
|
-
if [[ $
|
|
550
|
+
if [[ $teammate_count -gt 0 ]]; then
|
|
523
551
|
teammate_list="["
|
|
524
552
|
local first=true
|
|
525
553
|
for name in "${!_TEAMS_TEAMMATES[@]}"; do
|
|
@@ -530,6 +558,11 @@ teams_get_status() {
|
|
|
530
558
|
teammate_list+="]"
|
|
531
559
|
fi
|
|
532
560
|
|
|
561
|
+
local story_count=0
|
|
562
|
+
if [[ -v _TEAMS_STORY_ASSIGNMENT ]]; then
|
|
563
|
+
story_count="${#_TEAMS_STORY_ASSIGNMENT[@]}"
|
|
564
|
+
fi
|
|
565
|
+
|
|
533
566
|
cat << EOF
|
|
534
567
|
{
|
|
535
568
|
"version": "$TEAMS_ADAPTER_VERSION",
|
|
@@ -543,7 +576,7 @@ teams_get_status() {
|
|
|
543
576
|
"assigned": $_TEAMS_STORIES_ASSIGNED,
|
|
544
577
|
"completed": $_TEAMS_STORIES_COMPLETED,
|
|
545
578
|
"failed": $_TEAMS_STORIES_FAILED,
|
|
546
|
-
"active": $
|
|
579
|
+
"active": $story_count
|
|
547
580
|
},
|
|
548
581
|
"watchdog": {
|
|
549
582
|
"interval_seconds": $TEAMS_WATCHDOG_INTERVAL,
|
|
@@ -595,6 +628,10 @@ _teams_write_state() {
|
|
|
595
628
|
fi
|
|
596
629
|
|
|
597
630
|
local state_file="$_TEAMS_SESSION_DIR/state.yaml"
|
|
631
|
+
local active_count=0
|
|
632
|
+
if [[ -v _TEAMS_STORY_ASSIGNMENT ]]; then
|
|
633
|
+
active_count="${#_TEAMS_STORY_ASSIGNMENT[@]}"
|
|
634
|
+
fi
|
|
598
635
|
|
|
599
636
|
cat > "$state_file" << EOF
|
|
600
637
|
adapter_version: "$TEAMS_ADAPTER_VERSION"
|
|
@@ -608,7 +645,7 @@ stories:
|
|
|
608
645
|
assigned: $_TEAMS_STORIES_ASSIGNED
|
|
609
646
|
completed: $_TEAMS_STORIES_COMPLETED
|
|
610
647
|
failed: $_TEAMS_STORIES_FAILED
|
|
611
|
-
active: $
|
|
648
|
+
active: $active_count
|
|
612
649
|
watchdog:
|
|
613
650
|
interval: $TEAMS_WATCHDOG_INTERVAL
|
|
614
651
|
timeout: $TEAMS_WATCHDOG_TIMEOUT
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
# shellcheck disable=SC2310
|
|
3
|
+
IFS=$'\n\t'
|
|
2
4
|
# =============================================================================
|
|
3
5
|
# Claude Code Multi-Account Manager
|
|
4
6
|
# Manage multiple Claude Code accounts easily
|
|
@@ -116,9 +118,9 @@ print_header() {
|
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
detect_shell_rc() {
|
|
119
|
-
if [[ -n "$ZSH_VERSION" ]] || [[ "$SHELL" == *"zsh"* ]]; then
|
|
121
|
+
if [[ -n "${ZSH_VERSION:-}" ]] || [[ "$SHELL" == *"zsh"* ]]; then
|
|
120
122
|
SHELL_RC="$HOME/.zshrc"
|
|
121
|
-
elif [[ -n "$BASH_VERSION" ]] || [[ "$SHELL" == *"bash"* ]]; then
|
|
123
|
+
elif [[ -n "${BASH_VERSION:-}" ]] || [[ "$SHELL" == *"bash"* ]]; then
|
|
122
124
|
SHELL_RC="$HOME/.bashrc"
|
|
123
125
|
else
|
|
124
126
|
SHELL_RC="$HOME/.profile"
|
|
@@ -472,7 +474,7 @@ migrate_profile() {
|
|
|
472
474
|
|
|
473
475
|
if [[ ! -d "$CLAUDE_PROFILES_DIR" ]] || [[ -z "$(ls -A "$CLAUDE_PROFILES_DIR" 2>/dev/null)" ]]; then
|
|
474
476
|
print_warning "${MSG_NO_PROFILE}"
|
|
475
|
-
return
|
|
477
|
+
return 0
|
|
476
478
|
fi
|
|
477
479
|
|
|
478
480
|
# List legacy profiles (without .mode file)
|
|
@@ -708,7 +710,7 @@ cmd_sync() {
|
|
|
708
710
|
|
|
709
711
|
if [[ ! -d "$CLAUDE_PROFILES_DIR" ]] || [[ -z "$(ls -A "$CLAUDE_PROFILES_DIR" 2>/dev/null)" ]]; then
|
|
710
712
|
print_warning "${MSG_NO_PROFILE}"
|
|
711
|
-
return
|
|
713
|
+
return 0
|
|
712
714
|
fi
|
|
713
715
|
|
|
714
716
|
local synced=0
|
|
@@ -787,7 +789,7 @@ cmd_doctor() {
|
|
|
787
789
|
|
|
788
790
|
if [[ ! -d "$CLAUDE_PROFILES_DIR" ]] || [[ -z "$(ls -A "$CLAUDE_PROFILES_DIR" 2>/dev/null)" ]]; then
|
|
789
791
|
print_warning "${MSG_NO_PROFILE}"
|
|
790
|
-
return
|
|
792
|
+
return 0
|
|
791
793
|
fi
|
|
792
794
|
|
|
793
795
|
# Check shell RC for orphan aliases
|
package/Tools/RTK/install-rtk.sh
CHANGED
|
@@ -80,6 +80,11 @@ RTK_MD="$CLAUDE_DIR/RTK.md"
|
|
|
80
80
|
RTK_HOOK_MATCHER="Bash"
|
|
81
81
|
RTK_HOOK_COMMAND="~/.claude/hooks/rtk-rewrite.sh"
|
|
82
82
|
|
|
83
|
+
# RTK official installer checksum (rtk-ai/rtk @ master/install.sh)
|
|
84
|
+
# To update: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sha256sum
|
|
85
|
+
# Last updated: 2026-04-15
|
|
86
|
+
RTK_INSTALL_SHA256="9989e60e33a353e9e6802fab1fd410b96d1dd228b34e52402c32f3c8c2dd8c66"
|
|
87
|
+
|
|
83
88
|
# ---------------------------------------------------------------------------
|
|
84
89
|
# check_prerequisites — verify jq and curl are available
|
|
85
90
|
# ---------------------------------------------------------------------------
|
|
@@ -117,7 +122,7 @@ check_rtk_installed() {
|
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
# ---------------------------------------------------------------------------
|
|
120
|
-
# install_rtk_binary — install RTK via official installer
|
|
125
|
+
# install_rtk_binary — install RTK via official installer (with checksum verification)
|
|
121
126
|
# ---------------------------------------------------------------------------
|
|
122
127
|
install_rtk_binary() {
|
|
123
128
|
print_info "$MSG_RTK_CHECK"
|
|
@@ -132,7 +137,55 @@ install_rtk_binary() {
|
|
|
132
137
|
print_warning "$MSG_RTK_NOT_FOUND"
|
|
133
138
|
print_info "$MSG_RTK_INSTALL_START"
|
|
134
139
|
|
|
135
|
-
|
|
140
|
+
# Setup cleanup trap
|
|
141
|
+
local tmp_install=""
|
|
142
|
+
cleanup_install() {
|
|
143
|
+
if [[ -n "$tmp_install" ]] && [[ -f "$tmp_install" ]]; then
|
|
144
|
+
rm -f "$tmp_install"
|
|
145
|
+
fi
|
|
146
|
+
}
|
|
147
|
+
trap cleanup_install EXIT
|
|
148
|
+
|
|
149
|
+
# Create temporary file in $HOME (not /tmp per CLAUDE.md)
|
|
150
|
+
tmp_install=$(mktemp "$HOME/.rtk-install-XXXXXX.sh")
|
|
151
|
+
|
|
152
|
+
# Download installer
|
|
153
|
+
print_info "Downloading RTK installer..."
|
|
154
|
+
if ! curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh -o "$tmp_install"; then
|
|
155
|
+
print_error "Failed to download RTK installer"
|
|
156
|
+
exit 1
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# Verify checksum (unless --skip-checksum flag is set)
|
|
160
|
+
if [[ "${RTK_SKIP_CHECKSUM:-}" != "1" ]]; then
|
|
161
|
+
print_info "Verifying installer checksum..."
|
|
162
|
+
local computed_hash
|
|
163
|
+
computed_hash=$(sha256sum "$tmp_install" | awk '{print $1}')
|
|
164
|
+
|
|
165
|
+
if [[ "$computed_hash" != "$RTK_INSTALL_SHA256" ]]; then
|
|
166
|
+
print_error "CHECKSUM MISMATCH!"
|
|
167
|
+
print_error "Expected: $RTK_INSTALL_SHA256"
|
|
168
|
+
print_error "Got: $computed_hash"
|
|
169
|
+
print_error ""
|
|
170
|
+
print_error "This could indicate:"
|
|
171
|
+
print_error " - The RTK installer was updated (verify manually)"
|
|
172
|
+
print_error " - A man-in-the-middle attack"
|
|
173
|
+
print_error ""
|
|
174
|
+
print_error "To update the checksum after manual verification:"
|
|
175
|
+
print_error " 1. Inspect the downloaded script: less $tmp_install"
|
|
176
|
+
print_error " 2. Update RTK_INSTALL_SHA256 in $0"
|
|
177
|
+
print_error ""
|
|
178
|
+
print_warning "To bypass this check (NOT recommended):"
|
|
179
|
+
print_warning " RTK_SKIP_CHECKSUM=1 bash $0"
|
|
180
|
+
exit 1
|
|
181
|
+
fi
|
|
182
|
+
print_success "Checksum verified"
|
|
183
|
+
else
|
|
184
|
+
print_warning "⚠ CHECKSUM VERIFICATION SKIPPED (RTK_SKIP_CHECKSUM=1)"
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
# Execute installer
|
|
188
|
+
if sh "$tmp_install"; then
|
|
136
189
|
# Re-check after install (binary might be in a new path)
|
|
137
190
|
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
|
138
191
|
if check_rtk_installed; then
|
|
@@ -231,7 +231,10 @@ check_resource_availability() {
|
|
|
231
231
|
|
|
232
232
|
# Get number of available slots
|
|
233
233
|
get_available_slots() {
|
|
234
|
-
local active_count
|
|
234
|
+
local active_count=0
|
|
235
|
+
if [[ -v PARALLEL_PIDS ]]; then
|
|
236
|
+
active_count=${#PARALLEL_PIDS[@]}
|
|
237
|
+
fi
|
|
235
238
|
local available=$((PARALLEL_MAX_CONCURRENT - active_count))
|
|
236
239
|
|
|
237
240
|
# Further limit if resources are constrained
|
|
@@ -391,7 +394,12 @@ wait_all_sessions() {
|
|
|
391
394
|
local start_time
|
|
392
395
|
start_time=$(date +%s)
|
|
393
396
|
|
|
394
|
-
|
|
397
|
+
local active_count=0
|
|
398
|
+
if [[ -v PARALLEL_PIDS ]]; then
|
|
399
|
+
active_count=${#PARALLEL_PIDS[@]}
|
|
400
|
+
fi
|
|
401
|
+
|
|
402
|
+
while [[ $active_count -gt 0 ]]; do
|
|
395
403
|
collect_completed_sessions > /dev/null
|
|
396
404
|
|
|
397
405
|
# Check timeout
|
|
@@ -404,6 +412,12 @@ wait_all_sessions() {
|
|
|
404
412
|
fi
|
|
405
413
|
|
|
406
414
|
sleep 5
|
|
415
|
+
|
|
416
|
+
# Update active count
|
|
417
|
+
active_count=0
|
|
418
|
+
if [[ -v PARALLEL_PIDS ]]; then
|
|
419
|
+
active_count=${#PARALLEL_PIDS[@]}
|
|
420
|
+
fi
|
|
407
421
|
done
|
|
408
422
|
|
|
409
423
|
return 0
|
|
@@ -411,7 +425,11 @@ wait_all_sessions() {
|
|
|
411
425
|
|
|
412
426
|
# Get active session count
|
|
413
427
|
get_active_count() {
|
|
414
|
-
|
|
428
|
+
local count=0
|
|
429
|
+
if [[ -v PARALLEL_PIDS ]]; then
|
|
430
|
+
count=${#PARALLEL_PIDS[@]}
|
|
431
|
+
fi
|
|
432
|
+
echo "$count"
|
|
415
433
|
}
|
|
416
434
|
|
|
417
435
|
# =============================================================================
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
IFS=$'\n\t'
|
|
2
4
|
# =============================================================================
|
|
3
5
|
# Claude Code Status Line v2.0.0
|
|
4
6
|
#
|
|
@@ -171,7 +173,7 @@ done
|
|
|
171
173
|
# 1. PROFILE
|
|
172
174
|
# -----------------------------------------------------------------------------
|
|
173
175
|
get_profile() {
|
|
174
|
-
if [[ -n "$CLAUDE_CONFIG_DIR" ]]; then
|
|
176
|
+
if [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
|
|
175
177
|
local profile_name="${CLAUDE_CONFIG_DIR##*/}"
|
|
176
178
|
profile_name="${profile_name#.claude-}"
|
|
177
179
|
profile_name="${profile_name#claude-}"
|
package/Tools/lib/tools-ui.sh
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# =============================================================================
|
|
3
3
|
# tools-ui.sh — Shared UI helpers for Claude Craft CLI tools
|
|
4
4
|
# Source this file from any tool script for consistent colors and helpers.
|
|
5
|
+
# NOTE: No set -euo pipefail here — this is a sourced library. The caller
|
|
6
|
+
# decides its own shell strictness.
|
|
5
7
|
# =============================================================================
|
|
6
8
|
|
|
7
9
|
# Guard against double-sourcing
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { store, patchStatus } from '../lib/store.svelte.js';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
3
4
|
|
|
4
5
|
const COLUMNS = [
|
|
5
6
|
{ key: 'backlog', label: 'Backlog' },
|
|
@@ -10,6 +11,12 @@
|
|
|
10
11
|
{ key: 'blocked', label: 'Blocked' },
|
|
11
12
|
];
|
|
12
13
|
|
|
14
|
+
// Accessibility: keyboard navigation state
|
|
15
|
+
let focusedCardIndex = $state(-1);
|
|
16
|
+
let focusedColumnIndex = $state(0);
|
|
17
|
+
let showMoveMenu = $state(false);
|
|
18
|
+
let liveRegion = $state('');
|
|
19
|
+
|
|
13
20
|
const storiesByStatus = $derived.by(() => {
|
|
14
21
|
const map = Object.fromEntries(COLUMNS.map((c) => [c.key, []]));
|
|
15
22
|
for (const s of store.stories) (map[s.status] ??= []).push(s);
|
|
@@ -51,7 +58,127 @@
|
|
|
51
58
|
if (!reason) return;
|
|
52
59
|
body.blocked_reason = reason;
|
|
53
60
|
}
|
|
54
|
-
try {
|
|
61
|
+
try {
|
|
62
|
+
await patchStatus(id, body);
|
|
63
|
+
announceMove(story.id, targetStatus);
|
|
64
|
+
} catch { /* toast already shown */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Accessibility: keyboard navigation
|
|
68
|
+
onMount(() => {
|
|
69
|
+
const enabled = import.meta.env.CC_A11Y_KANBAN !== '0';
|
|
70
|
+
if (!enabled) return;
|
|
71
|
+
|
|
72
|
+
function handleKeyboard(e) {
|
|
73
|
+
// Arrow navigation between cards
|
|
74
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
const currentColumn = COLUMNS[focusedColumnIndex];
|
|
77
|
+
const cards = storiesByStatus[currentColumn.key];
|
|
78
|
+
if (cards.length === 0) return;
|
|
79
|
+
|
|
80
|
+
if (e.key === 'ArrowDown') {
|
|
81
|
+
focusedCardIndex = Math.min(focusedCardIndex + 1, cards.length - 1);
|
|
82
|
+
} else {
|
|
83
|
+
focusedCardIndex = Math.max(0, focusedCardIndex - 1);
|
|
84
|
+
}
|
|
85
|
+
focusCard(cards[focusedCardIndex]?.id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Arrow navigation between columns
|
|
89
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
if (e.key === 'ArrowRight') {
|
|
92
|
+
focusedColumnIndex = Math.min(focusedColumnIndex + 1, COLUMNS.length - 1);
|
|
93
|
+
} else {
|
|
94
|
+
focusedColumnIndex = Math.max(0, focusedColumnIndex - 1);
|
|
95
|
+
}
|
|
96
|
+
focusedCardIndex = 0;
|
|
97
|
+
const newColumn = COLUMNS[focusedColumnIndex];
|
|
98
|
+
const cards = storiesByStatus[newColumn.key];
|
|
99
|
+
if (cards.length > 0) {
|
|
100
|
+
focusCard(cards[0].id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Open move menu with Alt+M
|
|
105
|
+
if (e.altKey && e.key === 'm') {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
const currentColumn = COLUMNS[focusedColumnIndex];
|
|
108
|
+
const cards = storiesByStatus[currentColumn.key];
|
|
109
|
+
if (focusedCardIndex >= 0 && cards[focusedCardIndex]) {
|
|
110
|
+
showMoveMenu = !showMoveMenu;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Move card with number keys when menu is open
|
|
115
|
+
if (showMoveMenu && /^[1-6]$/.test(e.key)) {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
const targetColumn = COLUMNS[parseInt(e.key) - 1];
|
|
118
|
+
if (targetColumn) {
|
|
119
|
+
const currentColumn = COLUMNS[focusedColumnIndex];
|
|
120
|
+
const cards = storiesByStatus[currentColumn.key];
|
|
121
|
+
const card = cards[focusedCardIndex];
|
|
122
|
+
if (card) {
|
|
123
|
+
moveCard(card, targetColumn.key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Close move menu with Escape
|
|
129
|
+
if (e.key === 'Escape') {
|
|
130
|
+
showMoveMenu = false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Enter to focus card details (could be extended)
|
|
134
|
+
if (e.key === 'Enter' && !showMoveMenu) {
|
|
135
|
+
const currentColumn = COLUMNS[focusedColumnIndex];
|
|
136
|
+
const cards = storiesByStatus[currentColumn.key];
|
|
137
|
+
const card = cards[focusedCardIndex];
|
|
138
|
+
if (card) {
|
|
139
|
+
announceCardDetails(card);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
document.addEventListener('keydown', handleKeyboard);
|
|
145
|
+
return () => document.removeEventListener('keydown', handleKeyboard);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
function focusCard(cardId) {
|
|
149
|
+
if (!cardId) return;
|
|
150
|
+
const el = document.querySelector(`[data-card-id="${cardId}"]`);
|
|
151
|
+
if (el) el.focus();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function moveCard(card, targetStatus) {
|
|
155
|
+
const body = { status: targetStatus };
|
|
156
|
+
if (targetStatus === 'blocked') {
|
|
157
|
+
const reason = window.prompt('Blocked reason?');
|
|
158
|
+
if (!reason) {
|
|
159
|
+
showMoveMenu = false;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
body.blocked_reason = reason;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await patchStatus(card.id, body);
|
|
166
|
+
showMoveMenu = false;
|
|
167
|
+
announceMove(card.id, targetStatus);
|
|
168
|
+
} catch {
|
|
169
|
+
showMoveMenu = false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function announceMove(cardId, targetStatus) {
|
|
174
|
+
const targetCol = COLUMNS.find(c => c.key === targetStatus);
|
|
175
|
+
liveRegion = `Card ${cardId} moved to ${targetCol?.label || targetStatus}`;
|
|
176
|
+
setTimeout(() => liveRegion = '', 2000);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function announceCardDetails(card) {
|
|
180
|
+
liveRegion = `Card ${card.id}: ${card.title}. Status: ${card.status}. Priority: ${card.priority}. ${card.story_points} story points.`;
|
|
181
|
+
setTimeout(() => liveRegion = '', 3000);
|
|
55
182
|
}
|
|
56
183
|
</script>
|
|
57
184
|
|
|
@@ -75,6 +202,10 @@
|
|
|
75
202
|
draggable="true"
|
|
76
203
|
ondragstart={(e) => onDragStart(e, s.id)}
|
|
77
204
|
aria-label="{s.id} {s.title}"
|
|
205
|
+
data-card-id={s.id}
|
|
206
|
+
tabindex="0"
|
|
207
|
+
role="button"
|
|
208
|
+
aria-describedby="keyboard-help"
|
|
78
209
|
>
|
|
79
210
|
<header class="card-top">
|
|
80
211
|
<span class="id">{s.id}</span>
|
|
@@ -108,6 +239,34 @@
|
|
|
108
239
|
{/each}
|
|
109
240
|
</div>
|
|
110
241
|
|
|
242
|
+
<!-- Accessibility: keyboard help & live region -->
|
|
243
|
+
<div id="keyboard-help" class="sr-only">
|
|
244
|
+
Use arrow keys to navigate. Alt+M to open move menu. Numbers 1-6 to move to column. Escape to close menu. Enter for details.
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div aria-live="polite" aria-atomic="true" class="sr-only">
|
|
248
|
+
{liveRegion}
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{#if showMoveMenu}
|
|
252
|
+
<div class="move-menu" role="dialog" aria-label="Move card to column">
|
|
253
|
+
<div class="move-menu-content">
|
|
254
|
+
<h3>Move card to:</h3>
|
|
255
|
+
{#each COLUMNS as col, idx}
|
|
256
|
+
<button onclick={() => {
|
|
257
|
+
const currentColumn = COLUMNS[focusedColumnIndex];
|
|
258
|
+
const cards = storiesByStatus[currentColumn.key];
|
|
259
|
+
const card = cards[focusedCardIndex];
|
|
260
|
+
if (card) moveCard(card, col.key);
|
|
261
|
+
}}>
|
|
262
|
+
{idx + 1}. {col.label}
|
|
263
|
+
</button>
|
|
264
|
+
{/each}
|
|
265
|
+
<button onclick={() => showMoveMenu = false}>Cancel (Esc)</button>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
{/if}
|
|
269
|
+
|
|
111
270
|
<style>
|
|
112
271
|
.board {
|
|
113
272
|
display: flex;
|
|
@@ -224,4 +383,73 @@
|
|
|
224
383
|
font-weight: 600;
|
|
225
384
|
margin-left: auto;
|
|
226
385
|
}
|
|
386
|
+
|
|
387
|
+
/* Accessibility: screen reader only */
|
|
388
|
+
.sr-only {
|
|
389
|
+
position: absolute;
|
|
390
|
+
width: 1px;
|
|
391
|
+
height: 1px;
|
|
392
|
+
padding: 0;
|
|
393
|
+
margin: -1px;
|
|
394
|
+
overflow: hidden;
|
|
395
|
+
clip: rect(0, 0, 0, 0);
|
|
396
|
+
white-space: nowrap;
|
|
397
|
+
border-width: 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* Accessibility: move menu */
|
|
401
|
+
.move-menu {
|
|
402
|
+
position: fixed;
|
|
403
|
+
top: 0;
|
|
404
|
+
left: 0;
|
|
405
|
+
right: 0;
|
|
406
|
+
bottom: 0;
|
|
407
|
+
background: rgba(0, 0, 0, 0.5);
|
|
408
|
+
display: flex;
|
|
409
|
+
align-items: center;
|
|
410
|
+
justify-content: center;
|
|
411
|
+
z-index: 1000;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.move-menu-content {
|
|
415
|
+
background: var(--bg-elev);
|
|
416
|
+
border: 1px solid var(--border);
|
|
417
|
+
border-radius: var(--radius);
|
|
418
|
+
padding: 20px;
|
|
419
|
+
min-width: 300px;
|
|
420
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.move-menu-content h3 {
|
|
424
|
+
margin: 0 0 12px;
|
|
425
|
+
font-size: 16px;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.move-menu-content button {
|
|
429
|
+
display: block;
|
|
430
|
+
width: 100%;
|
|
431
|
+
padding: 10px;
|
|
432
|
+
margin: 4px 0;
|
|
433
|
+
background: var(--bg-sidebar);
|
|
434
|
+
border: 1px solid var(--border);
|
|
435
|
+
border-radius: var(--radius);
|
|
436
|
+
color: var(--fg);
|
|
437
|
+
cursor: pointer;
|
|
438
|
+
text-align: left;
|
|
439
|
+
font-size: 14px;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.move-menu-content button:hover,
|
|
443
|
+
.move-menu-content button:focus {
|
|
444
|
+
background: var(--accent);
|
|
445
|
+
color: var(--accent-fg);
|
|
446
|
+
outline: 2px solid var(--accent);
|
|
447
|
+
outline-offset: 1px;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* Focus visible styles */
|
|
451
|
+
.card:focus-visible {
|
|
452
|
+
outline: 2px solid var(--accent);
|
|
453
|
+
outline-offset: 2px;
|
|
454
|
+
}
|
|
227
455
|
</style>
|
package/cli/lib/banner.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import c from './colors.js';
|
|
7
|
+
import { success as successSymbol } from './symbols.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Print the ASCII art banner with version information.
|
|
@@ -42,7 +43,7 @@ export function printSuccess(targetPath) {
|
|
|
42
43
|
console.log(`
|
|
43
44
|
${c.green}${c.bold}╔═══════════════════════════════════════════════════════════════╗${c.reset}
|
|
44
45
|
${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}
|
|
45
|
-
${c.green}${c.bold}║${c.reset} ${
|
|
46
|
+
${c.green}${c.bold}║${c.reset} ${successSymbol('Installation Complete!')} ${c.green}${c.bold}║${c.reset}
|
|
46
47
|
${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}
|
|
47
48
|
${c.green}${c.bold}╚═══════════════════════════════════════════════════════════════╝${c.reset}
|
|
48
49
|
|