diff --git a/app/Actions/Projects/BuildProjectBacklogPayload.php b/app/Actions/Projects/BuildProjectBacklogPayload.php index bff6315..2d2a422 100644 --- a/app/Actions/Projects/BuildProjectBacklogPayload.php +++ b/app/Actions/Projects/BuildProjectBacklogPayload.php @@ -35,6 +35,7 @@ class BuildProjectBacklogPayload )->resolve(), 'canCreateTickets' => $canManageTickets, 'canEditTickets' => $canManageTickets, + 'canManageProject' => $this->permissionService->allows($user, Permission::ProjectAdmin, $project), 'project' => ProjectResource::make($project)->resolve(), 'status' => $status, 'ticketStatusOptions' => TicketStatusResource::collection( diff --git a/app/Actions/Projects/BuildProjectBoardPayload.php b/app/Actions/Projects/BuildProjectBoardPayload.php index 1afb503..db72f5a 100644 --- a/app/Actions/Projects/BuildProjectBoardPayload.php +++ b/app/Actions/Projects/BuildProjectBoardPayload.php @@ -29,6 +29,7 @@ class BuildProjectBoardPayload 'ticketStatuses', 'tickets' => fn ($query) => $query ->with([ + 'assignee', 'ticketPriority', 'ticketStatus.allowedTargetStatuses.boardColumn', 'ticketStatus.boardColumn', @@ -39,6 +40,7 @@ class BuildProjectBoardPayload )->resolve(), 'canCreateTickets' => $canManageTickets, 'canEditTickets' => $canManageTickets, + 'canManageProject' => $this->permissionService->allows($user, Permission::ProjectAdmin, $project), 'project' => ProjectResource::make($project)->resolve(), 'status' => $status, ]; diff --git a/app/Actions/Projects/BuildProjectSettingsPayload.php b/app/Actions/Projects/BuildProjectSettingsPayload.php new file mode 100644 index 0000000..8bc7f11 --- /dev/null +++ b/app/Actions/Projects/BuildProjectSettingsPayload.php @@ -0,0 +1,111 @@ + + */ + public function handle( + Project $project, + User $user, + ProjectSettingsSection $section, + mixed $status = null, + mixed $error = null, + ): array { + return [ + 'activeSection' => $section->value, + 'error' => $error, + 'project' => ProjectResource::make($project)->resolve(), + 'sectionData' => $this->sectionData($project, $user, $section), + 'status' => $status, + ]; + } + + /** + * @return array + */ + private function sectionData( + Project $project, + User $user, + ProjectSettingsSection $section, + ): array { + $payload = match ($section) { + ProjectSettingsSection::General => $this->generalData($project), + ProjectSettingsSection::Board => $this->buildProjectBoardSettingsPayload->handle($project), + ProjectSettingsSection::Metadata => $this->buildProjectTicketMetadataSettingsPayload->handle($project), + ProjectSettingsSection::Repositories => $this->buildProjectRepositorySettingsPayload->handle($project, $user), + ProjectSettingsSection::Notifications => $this->buildProjectNotificationSettingsPayload->handle($project), + ProjectSettingsSection::Access => $this->accessData($project), + }; + + return Arr::except($payload, ['error', 'project', 'status']); + } + + /** + * @return array + */ + private function generalData(Project $project): array + { + return [ + 'sourceTemplates' => ProjectTemplateResource::collection( + ProjectTemplate::query() + ->published() + ->with('sourceProject') + ->orderBy('name') + ->get(), + )->resolve(), + 'templateState' => $this->resolveProjectTemplateState->handle($project), + ]; + } + + /** + * @return array + */ + private function accessData(Project $project): array + { + return [ + 'directPermissions' => $this->permissionService->assignmentMap(User::class, $project), + 'permissionOptions' => PermissionOptionResource::collection( + $this->permissionService->permissionOptions(User::class, Project::class), + )->resolve(), + 'teamPermissions' => $this->permissionService->assignmentMap(Team::class, $project), + 'teams' => TeamResource::collection( + Team::query() + ->with('users') + ->orderBy('name') + ->get(), + )->resolve(), + 'users' => UserResource::collection( + User::query() + ->orderBy('name') + ->get(), + )->resolve(), + ]; + } +} diff --git a/app/Actions/Projects/BuildProjectTicketDetailPayload.php b/app/Actions/Projects/BuildProjectTicketDetailPayload.php index 25257cb..e973fb3 100644 --- a/app/Actions/Projects/BuildProjectTicketDetailPayload.php +++ b/app/Actions/Projects/BuildProjectTicketDetailPayload.php @@ -47,6 +47,7 @@ class BuildProjectTicketDetailPayload 'fieldValues.userValue', 'parentTicket', 'ticketPriority', + 'ticketStatus.allowedTargetStatuses.boardColumn', 'ticketStatus.boardColumn', 'ticketType', 'worklogEntries.actor', diff --git a/app/Actions/Projects/BuildProjectTicketOptionsPayload.php b/app/Actions/Projects/BuildProjectTicketOptionsPayload.php index bd4bfc1..ca1b928 100644 --- a/app/Actions/Projects/BuildProjectTicketOptionsPayload.php +++ b/app/Actions/Projects/BuildProjectTicketOptionsPayload.php @@ -7,9 +7,9 @@ use App\Http\Resources\TicketPriorityResource; use App\Http\Resources\TicketReferenceResource; use App\Http\Resources\TicketStatusResource; use App\Http\Resources\TicketTypeResource; -use App\Models\Ticket; use App\Http\Resources\UserResource; use App\Models\Project; +use App\Models\Ticket; use App\Models\User; class BuildProjectTicketOptionsPayload diff --git a/app/Enums/ProjectSettingsSection.php b/app/Enums/ProjectSettingsSection.php new file mode 100644 index 0000000..e8d0b0f --- /dev/null +++ b/app/Enums/ProjectSettingsSection.php @@ -0,0 +1,13 @@ +user(); abort_unless( @@ -56,13 +58,32 @@ class ProjectBoardController extends Controller : 'projects.board.index'; $result = $this->moveProjectTicket->handle($project, $ticket, $user, $validated); + $statusMessage = $result['moved'] + ? "{$ticket->ticket_key} moved to {$result['destination_label']}." + : "{$ticket->ticket_key} already belongs to {$result['destination_label']}."; + + if ($request->expectsJson()) { + $ticket->refresh()->load([ + 'ticketStatus.allowedTargetStatuses.boardColumn', + 'ticketStatus.boardColumn', + ]); + + return response()->json([ + 'message' => $statusMessage, + 'moved' => $result['moved'], + 'ticket_status' => $ticket->ticketStatus === null + ? null + : TicketStatusResource::make($ticket->ticketStatus)->resolve(), + 'ticket_status_id' => $ticket->ticket_status_id, + ]); + } if (! $result['moved']) { return to_route($redirectRoute, $project) - ->with('status', "{$ticket->ticket_key} already belongs to {$result['destination_label']}."); + ->with('status', $statusMessage); } return to_route($redirectRoute, $project) - ->with('status', "{$ticket->ticket_key} moved to {$result['destination_label']}."); + ->with('status', $statusMessage); } } diff --git a/app/Http/Controllers/ProjectBoardSettingsController.php b/app/Http/Controllers/ProjectBoardSettingsController.php index bd65152..957dbab 100644 --- a/app/Http/Controllers/ProjectBoardSettingsController.php +++ b/app/Http/Controllers/ProjectBoardSettingsController.php @@ -10,8 +10,8 @@ use App\Actions\ProjectBoard\StoreTicketStatus; use App\Actions\ProjectBoard\UpdateBoardColumn; use App\Actions\ProjectBoard\UpdateTicketStatus; use App\Actions\ProjectBoard\UpdateTicketStatusTransitions; -use App\Actions\Projects\BuildProjectBoardSettingsPayload; use App\Enums\Permission; +use App\Enums\ProjectSettingsSection; use App\Http\Requests\StoreBoardColumnRequest; use App\Http\Requests\StoreTicketStatusRequest; use App\Http\Requests\UpdateBoardColumnRequest; @@ -22,13 +22,10 @@ use App\Models\Project; use App\Models\TicketStatus; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Inertia\Inertia; -use Inertia\Response; class ProjectBoardSettingsController extends Controller { public function __construct( - private readonly BuildProjectBoardSettingsPayload $buildProjectBoardSettingsPayload, private readonly DeleteBoardColumn $deleteBoardColumn, private readonly DeleteTicketStatus $deleteTicketStatus, private readonly PermissionService $permissionService, @@ -39,19 +36,6 @@ class ProjectBoardSettingsController extends Controller private readonly UpdateTicketStatusTransitions $updateTicketStatusTransitions, ) {} - public function show(Request $request, Project $project): Response - { - abort_unless( - $this->permissionService->allows($request->user(), Permission::ProjectAdmin, $project), - 403, - ); - - return Inertia::render( - 'projects/settings/Board', - $this->buildProjectBoardSettingsPayload->handle($project, session('status')), - ); - } - public function store( StoreBoardColumnRequest $request, Project $project, @@ -62,7 +46,7 @@ class ProjectBoardSettingsController extends Controller ); $boardColumn = $this->storeBoardColumn->handle($project, $request->validated()); - return to_route('projects.settings.board', $project) + return $this->settingsRedirect($project) ->with('status', "{$boardColumn->name} was added to the board workflow."); } @@ -77,7 +61,7 @@ class ProjectBoardSettingsController extends Controller ); $boardColumn = $this->updateBoardColumn->handle($project, $boardColumn, $request->validated()); - return to_route('projects.settings.board', $project) + return $this->settingsRedirect($project) ->with('status', "{$boardColumn->fresh()->name} was updated."); } @@ -93,7 +77,7 @@ class ProjectBoardSettingsController extends Controller $deletedName = $this->deleteBoardColumn->handle($project, $boardColumn); - return to_route('projects.settings.board', $project) + return $this->settingsRedirect($project) ->with('status', "{$deletedName} was removed from the board workflow."); } @@ -107,7 +91,7 @@ class ProjectBoardSettingsController extends Controller ); $ticketStatus = $this->storeTicketStatus->handle($project, $request->validated()); - return to_route('projects.settings.board', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketStatus->name} was added to {$this->statusContextLabel($ticketStatus)}."); } @@ -122,7 +106,7 @@ class ProjectBoardSettingsController extends Controller ); $ticketStatus = $this->updateTicketStatus->handle($ticketStatus, $request->validated()); - return to_route('projects.settings.board', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketStatus->fresh()->name} was updated."); } @@ -138,7 +122,7 @@ class ProjectBoardSettingsController extends Controller $result = $this->deleteTicketStatus->handle($project, $ticketStatus); - return to_route('projects.settings.board', $project) + return $this->settingsRedirect($project) ->with('status', "{$result['deleted_name']} was removed from {$result['context_label']}."); } @@ -153,7 +137,7 @@ class ProjectBoardSettingsController extends Controller ); $this->updateTicketStatusTransitions->handle($ticketStatus, $request->validated()); - return to_route('projects.settings.board', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketStatus->name} move targets were updated."); } @@ -161,4 +145,12 @@ class ProjectBoardSettingsController extends Controller { return $ticketStatus->boardColumn?->name ?? 'Backlog'; } + + private function settingsRedirect(Project $project): RedirectResponse + { + return to_route('projects.settings.index', [ + 'project' => $project, + 'section' => ProjectSettingsSection::Board->value, + ]); + } } diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 898a9df..d6b17e3 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -8,19 +8,16 @@ use App\Actions\Projects\CreateProject; use App\Actions\Projects\DeleteProject; use App\Actions\Projects\DuplicateProjectConfiguration; use App\Actions\Projects\ListVisibleProjects; -use App\Actions\Projects\ResolveProjectTemplateState; use App\Actions\Projects\UpdateProjectTemplateLink; use App\Enums\Permission; +use App\Enums\ProjectSettingsSection; use App\Http\Requests\DeleteProjectRequest; use App\Http\Requests\ResetProjectTemplateRequest; use App\Http\Requests\StoreProjectRequest; use App\Http\Requests\SwitchProjectTemplateRequest; use App\Http\Requests\SyncProjectPermissionsRequest; -use App\Http\Resources\PermissionOptionResource; use App\Http\Resources\ProjectResource; use App\Http\Resources\ProjectTemplateResource; -use App\Http\Resources\TeamResource; -use App\Http\Resources\UserResource; use App\Models\Project; use App\Models\ProjectTemplate; use App\Models\Team; @@ -39,7 +36,6 @@ class ProjectController extends Controller private readonly DeleteProject $deleteProject, private readonly ListVisibleProjects $listVisibleProjects, private readonly PermissionService $permissionService, - private readonly ResolveProjectTemplateState $resolveProjectTemplateState, private readonly UpdateProjectTemplateLink $updateProjectTemplateLink, ) {} @@ -83,10 +79,10 @@ class ProjectController extends Controller { $project = $this->createProject->handle($request->validated(), $request->user()); - return to_route('projects.show', $project); + return to_route('projects.board.index', $project); } - public function show(Request $request, Project $project): Response + public function show(Request $request, Project $project): RedirectResponse { $user = $request->user(); @@ -95,11 +91,7 @@ class ProjectController extends Controller 403, ); - return Inertia::render('projects/Show', [ - 'canManageAccess' => $this->permissionService->allows($user, Permission::ProjectAdmin, $project), - 'project' => ProjectResource::make($project)->resolve(), - 'templateState' => $this->resolveProjectTemplateState->handle($project), - ]); + return to_route('projects.board.index', $project); } public function backlog(Request $request, Project $project): Response @@ -117,31 +109,19 @@ class ProjectController extends Controller ); } - public function general(Request $request, Project $project): Response - { - return Inertia::render('projects/settings/General', [ - 'project' => ProjectResource::make($project)->resolve(), - 'sourceTemplates' => ProjectTemplateResource::collection( - ProjectTemplate::query() - ->published() - ->with('sourceProject') - ->orderBy('name') - ->get(), - )->resolve(), - 'status' => session('status'), - 'templateState' => $this->resolveProjectTemplateState->handle($project), - ]); - } - public function switchTemplate(SwitchProjectTemplateRequest $request, Project $project): RedirectResponse { + $validated = $request->validated(); $template = ProjectTemplate::query() ->published() - ->findOrFail($request->integer('source_template_id')); + ->findOrFail((int) Arr::get($validated, 'source_template_id')); $this->updateProjectTemplateLink->switch($project, $template); - return to_route('projects.settings.general', $project) + return to_route('projects.settings.index', [ + 'project' => $project, + 'section' => ProjectSettingsSection::General->value, + ]) ->with('status', "{$project->name} now uses the {$template->name} template."); } @@ -155,40 +135,13 @@ class ProjectController extends Controller $this->updateProjectTemplateLink->reset($project->load('projectTemplate')); - return to_route('projects.settings.general', $project) + return to_route('projects.settings.index', [ + 'project' => $project, + 'section' => ProjectSettingsSection::General->value, + ]) ->with('status', "{$project->name} was reset to the {$template->name} template baseline."); } - public function access(Request $request, Project $project): Response - { - $user = $request->user(); - abort_unless( - $this->permissionService->allows($user, Permission::ProjectAdmin, $project), - 403, - ); - - return Inertia::render('projects/Access', [ - 'directPermissions' => $this->permissionService->assignmentMap(User::class, $project), - 'permissionOptions' => PermissionOptionResource::collection( - $this->permissionService->permissionOptions(User::class, Project::class), - )->resolve(), - 'project' => ProjectResource::make($project)->resolve(), - 'status' => session('status'), - 'teamPermissions' => $this->permissionService->assignmentMap(Team::class, $project), - 'teams' => TeamResource::collection( - Team::query() - ->with('users') - ->orderBy('name') - ->get(), - )->resolve(), - 'users' => UserResource::collection( - User::query() - ->orderBy('name') - ->get(), - )->resolve(), - ]); - } - public function updateUserPermissions( SyncProjectPermissionsRequest $request, Project $project, @@ -205,7 +158,10 @@ class ProjectController extends Controller $this->permissionService->sync($user, $project, $permissions); - return to_route('projects.access', $project) + return to_route('projects.settings.index', [ + 'project' => $project, + 'section' => ProjectSettingsSection::Access->value, + ]) ->with('status', "{$user->name} permissions were updated."); } @@ -224,7 +180,10 @@ class ProjectController extends Controller $this->permissionService->sync($team, $project, $permissions); - return to_route('projects.access', $project) + return to_route('projects.settings.index', [ + 'project' => $project, + 'section' => ProjectSettingsSection::Access->value, + ]) ->with('status', "{$team->name} permissions were updated."); } diff --git a/app/Http/Controllers/ProjectNotificationSettingsController.php b/app/Http/Controllers/ProjectNotificationSettingsController.php index 8abb088..e78665d 100644 --- a/app/Http/Controllers/ProjectNotificationSettingsController.php +++ b/app/Http/Controllers/ProjectNotificationSettingsController.php @@ -2,41 +2,28 @@ namespace App\Http\Controllers; -use App\Actions\Notifications\BuildProjectNotificationSettingsPayload; use App\Actions\Notifications\DeleteProjectNotificationAction; use App\Actions\Notifications\StoreProjectNotificationAction; use App\Actions\Notifications\UpdateProjectNotificationAction; use App\Actions\Permissions\PermissionService; use App\Enums\Permission; +use App\Enums\ProjectSettingsSection; use App\Http\Requests\StoreProjectNotificationActionRequest; use App\Http\Requests\UpdateProjectNotificationActionRequest; use App\Models\NotificationAction; use App\Models\Project; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Inertia\Inertia; -use Inertia\Response; class ProjectNotificationSettingsController extends Controller { public function __construct( - private readonly BuildProjectNotificationSettingsPayload $buildProjectNotificationSettingsPayload, private readonly DeleteProjectNotificationAction $deleteProjectNotificationAction, private readonly PermissionService $permissionService, private readonly StoreProjectNotificationAction $storeProjectNotificationAction, private readonly UpdateProjectNotificationAction $updateProjectNotificationAction, ) {} - public function show(Request $request, Project $project): Response - { - $this->abortUnlessCanManage($request, $project); - - return Inertia::render( - 'projects/settings/Notifications', - $this->buildProjectNotificationSettingsPayload->handle($project, session('status')), - ); - } - public function store( StoreProjectNotificationActionRequest $request, Project $project, @@ -45,7 +32,7 @@ class ProjectNotificationSettingsController extends Controller $notificationAction = $this->storeProjectNotificationAction->handle($project, $request->validated()); - return to_route('projects.settings.notifications', $project) + return $this->settingsRedirect($project) ->with('status', "{$notificationAction->name} notification was added."); } @@ -62,7 +49,7 @@ class ProjectNotificationSettingsController extends Controller $request->validated(), ); - return to_route('projects.settings.notifications', $project) + return $this->settingsRedirect($project) ->with('status', "{$notificationAction->name} notification was updated."); } @@ -80,7 +67,7 @@ class ProjectNotificationSettingsController extends Controller $this->deleteProjectNotificationAction->handle($project, $notificationAction); - return to_route('projects.settings.notifications', $project) + return $this->settingsRedirect($project) ->with('status', "{$deletedName} notification was deleted."); } @@ -91,4 +78,12 @@ class ProjectNotificationSettingsController extends Controller 403, ); } + + private function settingsRedirect(Project $project): RedirectResponse + { + return to_route('projects.settings.index', [ + 'project' => $project, + 'section' => ProjectSettingsSection::Notifications->value, + ]); + } } diff --git a/app/Http/Controllers/ProjectRepositorySettingsController.php b/app/Http/Controllers/ProjectRepositorySettingsController.php index c95537c..e58c177 100644 --- a/app/Http/Controllers/ProjectRepositorySettingsController.php +++ b/app/Http/Controllers/ProjectRepositorySettingsController.php @@ -3,44 +3,27 @@ namespace App\Http\Controllers; use App\Actions\Permissions\PermissionService; -use App\Actions\Repositories\BuildProjectRepositorySettingsPayload; use App\Actions\Repositories\DeleteProjectRepositoryConnection; use App\Actions\Repositories\StoreProjectRepositoryConnection; use App\Actions\Repositories\UpdateProjectRepositoryConnection; use App\Enums\Permission; +use App\Enums\ProjectSettingsSection; use App\Http\Requests\StoreRepositoryConnectionRequest; use App\Http\Requests\UpdateRepositoryConnectionRequest; use App\Models\Project; use App\Models\RepositoryConnection; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Inertia\Inertia; -use Inertia\Response; class ProjectRepositorySettingsController extends Controller { public function __construct( - private readonly BuildProjectRepositorySettingsPayload $buildProjectRepositorySettingsPayload, private readonly DeleteProjectRepositoryConnection $deleteProjectRepositoryConnection, private readonly PermissionService $permissionService, private readonly StoreProjectRepositoryConnection $storeProjectRepositoryConnection, private readonly UpdateProjectRepositoryConnection $updateProjectRepositoryConnection, ) {} - public function show(Request $request, Project $project): Response - { - $user = $request->user(); - abort_unless( - $this->permissionService->allows($user, Permission::ProjectAdmin, $project), - 403, - ); - - return Inertia::render( - 'projects/settings/Repositories', - $this->buildProjectRepositorySettingsPayload->handle($project, $user, session('status')), - ); - } - public function store( StoreRepositoryConnectionRequest $request, Project $project, @@ -53,7 +36,7 @@ class ProjectRepositorySettingsController extends Controller $repositoryConnection = $this->storeProjectRepositoryConnection->handle($project, $user, $request->validated()); - return to_route('projects.settings.repositories', $project) + return $this->settingsRedirect($project) ->with('status', "{$repositoryConnection->repository_full_name} was linked to {$project->name}."); } @@ -76,7 +59,7 @@ class ProjectRepositorySettingsController extends Controller $request->validated(), ); - return to_route('projects.settings.repositories', $project) + return $this->settingsRedirect($project) ->with('status', "{$repositoryConnection->fresh()->repository_full_name} connection was updated."); } @@ -92,7 +75,15 @@ class ProjectRepositorySettingsController extends Controller $repositoryLabel = $this->deleteProjectRepositoryConnection->handle($repositoryConnection); - return to_route('projects.settings.repositories', $project) + return $this->settingsRedirect($project) ->with('status', "{$repositoryLabel} was removed from the project."); } + + private function settingsRedirect(Project $project): RedirectResponse + { + return to_route('projects.settings.index', [ + 'project' => $project, + 'section' => ProjectSettingsSection::Repositories->value, + ]); + } } diff --git a/app/Http/Controllers/ProjectSettingsController.php b/app/Http/Controllers/ProjectSettingsController.php new file mode 100644 index 0000000..ca7aa54 --- /dev/null +++ b/app/Http/Controllers/ProjectSettingsController.php @@ -0,0 +1,95 @@ +user(); + + abort_unless( + $this->permissionService->allows($user, Permission::ProjectAdmin, $project), + 403, + ); + + $validated = $request->validated(); + $section = ProjectSettingsSection::from( + (string) Arr::get($validated, 'section', ProjectSettingsSection::General->value), + ); + + return Inertia::render( + 'projects/Settings', + $this->buildProjectSettingsPayload->handle( + $project, + $user, + $section, + session('status'), + session('error'), + ), + ); + } + + public function general(Request $request, Project $project): RedirectResponse + { + return $this->redirectToSection($request, $project, ProjectSettingsSection::General); + } + + public function board(Request $request, Project $project): RedirectResponse + { + return $this->redirectToSection($request, $project, ProjectSettingsSection::Board); + } + + public function metadata(Request $request, Project $project): RedirectResponse + { + return $this->redirectToSection($request, $project, ProjectSettingsSection::Metadata); + } + + public function repositories(Request $request, Project $project): RedirectResponse + { + return $this->redirectToSection($request, $project, ProjectSettingsSection::Repositories); + } + + public function notifications(Request $request, Project $project): RedirectResponse + { + return $this->redirectToSection($request, $project, ProjectSettingsSection::Notifications); + } + + public function access(Request $request, Project $project): RedirectResponse + { + return $this->redirectToSection($request, $project, ProjectSettingsSection::Access); + } + + private function redirectToSection( + Request $request, + Project $project, + ProjectSettingsSection $section, + ): RedirectResponse { + abort_unless( + $this->permissionService->allows($request->user(), Permission::ProjectAdmin, $project), + 403, + ); + + return to_route('projects.settings.index', [ + 'project' => $project, + 'section' => $section->value, + ]); + } +} diff --git a/app/Http/Controllers/ProjectTicketController.php b/app/Http/Controllers/ProjectTicketController.php index 42cfbb9..2d7ca63 100644 --- a/app/Http/Controllers/ProjectTicketController.php +++ b/app/Http/Controllers/ProjectTicketController.php @@ -6,6 +6,7 @@ use App\Actions\Permissions\PermissionService; use App\Actions\Projects\BuildProjectTicketDetailPayload; use App\Actions\Projects\BuildProjectTicketOptionsPayload; use App\Actions\Tickets\CreateProjectTicket; +use App\Actions\Tickets\Support\AcceptanceCriteriaChecklistMapper; use App\Actions\Tickets\UpdateProjectTicket; use App\Enums\Permission; use App\Http\Requests\StoreProjectTicketRequest; @@ -40,11 +41,18 @@ class ProjectTicketController extends Controller return Inertia::render( 'projects/TicketCreate', - $this->buildProjectTicketOptionsPayload->handle( - $project, - Arr::get($request->query(), 'source') === 'board' ? 'board' : 'backlog', - session('status'), - ), + [ + ...$this->buildProjectTicketOptionsPayload->handle( + $project, + Arr::get($request->query(), 'source') === 'board' ? 'board' : 'backlog', + session('status'), + ), + 'canManageProject' => $this->permissionService->allows( + $user, + Permission::ProjectAdmin, + $project, + ), + ], ); } @@ -108,6 +116,9 @@ class ProjectTicketController extends Controller if ($request->expectsJson()) { return response()->json([ + 'acceptance_criteria' => AcceptanceCriteriaChecklistMapper::fromModelsWithIds( + $ticket->acceptanceCriteria()->get(), + ), 'message' => $statusMessage, 'updated' => $wasUpdated, ]); diff --git a/app/Http/Controllers/ProjectTicketMetadataSettingsController.php b/app/Http/Controllers/ProjectTicketMetadataSettingsController.php index fce99ed..c2b4067 100644 --- a/app/Http/Controllers/ProjectTicketMetadataSettingsController.php +++ b/app/Http/Controllers/ProjectTicketMetadataSettingsController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers; use App\Actions\Permissions\PermissionService; -use App\Actions\Projects\BuildProjectTicketMetadataSettingsPayload; use App\Actions\ProjectTicketMetadata\DeleteProjectTicketPriority; use App\Actions\ProjectTicketMetadata\DeleteProjectTicketType; use App\Actions\ProjectTicketMetadata\DeleteTicketFieldDefinition; @@ -18,6 +17,7 @@ use App\Actions\ProjectTicketMetadata\UpdateProjectTicketType; use App\Actions\ProjectTicketMetadata\UpdateTicketFieldDefinition; use App\Actions\ProjectTicketMetadata\UpdateTicketLinkType; use App\Enums\Permission; +use App\Enums\ProjectSettingsSection; use App\Http\Requests\DestroyProjectTicketTypeRequest; use App\Http\Requests\StoreProjectNamedSettingRequest; use App\Http\Requests\StoreProjectTicketTypeRequest; @@ -35,13 +35,10 @@ use App\Models\TicketPriority; use App\Models\TicketType; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Inertia\Inertia; -use Inertia\Response; class ProjectTicketMetadataSettingsController extends Controller { public function __construct( - private readonly BuildProjectTicketMetadataSettingsPayload $buildProjectTicketMetadataSettingsPayload, private readonly DeleteProjectTicketPriority $deleteProjectTicketPriority, private readonly DeleteProjectTicketType $deleteProjectTicketType, private readonly DeleteTicketFieldDefinition $deleteTicketFieldDefinition, @@ -58,23 +55,13 @@ class ProjectTicketMetadataSettingsController extends Controller private readonly UpdateTicketLinkType $updateTicketLinkType, ) {} - public function show(Request $request, Project $project): Response - { - $this->abortUnlessCanManage($request, $project); - - return Inertia::render( - 'projects/settings/Metadata', - $this->buildProjectTicketMetadataSettingsPayload->handle($project, session('status'), session('error')), - ); - } - public function update(UpdateProjectTicketMetadataSettings $request, Project $project): RedirectResponse { $this->abortUnlessCanManage($request, $project); $this->updateProjectTicketMetadataSettings->handle($project, $request->validated()); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', 'Settings have been updated.'); } @@ -84,7 +71,7 @@ class ProjectTicketMetadataSettingsController extends Controller $ticketType = $this->storeProjectTicketType->handle($project, $request->validated()); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketType->name} type was added."); } @@ -97,7 +84,7 @@ class ProjectTicketMetadataSettingsController extends Controller $ticketType = $this->updateProjectTicketType->handle($project, $ticketType, $request->validated()); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketType->fresh()->name} type was updated."); } @@ -116,12 +103,12 @@ class ProjectTicketMetadataSettingsController extends Controller ); if (! $result['deleted']) { - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('error', true) ->with('status', $result['message']); } - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', $result['message']); } @@ -131,7 +118,7 @@ class ProjectTicketMetadataSettingsController extends Controller $ticketPriority = $this->storeProjectTicketPriority->handle($project, $request->validated()); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketPriority->name} priority was added."); } @@ -144,7 +131,7 @@ class ProjectTicketMetadataSettingsController extends Controller $ticketPriority = $this->updateProjectTicketPriority->handle($project, $ticketPriority, $request->validated()); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketPriority->fresh()->name} priority was updated."); } @@ -158,12 +145,12 @@ class ProjectTicketMetadataSettingsController extends Controller $result = $this->deleteProjectTicketPriority->handle($project, $ticketPriority); if (! $result['deleted']) { - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('error', true) ->with('status', $result['message']); } - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', $result['message']); } @@ -175,7 +162,7 @@ class ProjectTicketMetadataSettingsController extends Controller $ticketFieldDefinition = $this->storeTicketFieldDefinition->handle($project, $request->validated()); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketFieldDefinition->label} field was added."); } @@ -188,7 +175,7 @@ class ProjectTicketMetadataSettingsController extends Controller $ticketFieldDefinition = $this->updateTicketFieldDefinition->handle($project, $ticketFieldDefinition, $request->validated()); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketFieldDefinition->fresh()->label} field was updated."); } @@ -201,7 +188,7 @@ class ProjectTicketMetadataSettingsController extends Controller $deletedLabel = $this->deleteTicketFieldDefinition->handle($project, $ticketFieldDefinition); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', "{$deletedLabel} field was removed."); } @@ -211,7 +198,7 @@ class ProjectTicketMetadataSettingsController extends Controller $ticketLinkType = $this->storeTicketLinkType->handle($project, $request->validated()); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketLinkType->label} link type was added."); } @@ -225,7 +212,7 @@ class ProjectTicketMetadataSettingsController extends Controller $ticketLinkType = $this->updateTicketLinkType->handle($project, $ticketLinkType, $request->validated()); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', "{$ticketLinkType->fresh()->label} link type was updated."); } @@ -238,7 +225,7 @@ class ProjectTicketMetadataSettingsController extends Controller $deletedLabel = $this->deleteTicketLinkType->handle($project, $ticketLinkType); - return to_route('projects.settings.metadata', $project) + return $this->settingsRedirect($project) ->with('status', "{$deletedLabel} link type was removed."); } @@ -249,4 +236,12 @@ class ProjectTicketMetadataSettingsController extends Controller 403, ); } + + private function settingsRedirect(Project $project): RedirectResponse + { + return to_route('projects.settings.index', [ + 'project' => $project, + 'section' => ProjectSettingsSection::Metadata->value, + ]); + } } diff --git a/app/Http/Requests/ProjectSettingsRequest.php b/app/Http/Requests/ProjectSettingsRequest.php new file mode 100644 index 0000000..96233de --- /dev/null +++ b/app/Http/Requests/ProjectSettingsRequest.php @@ -0,0 +1,43 @@ +user() !== null; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'section' => ['required', Rule::enum(ProjectSettingsSection::class)], + ]; + } + + protected function prepareForValidation(): void + { + $section = Arr::get($this->query(), 'section'); + + $this->merge([ + 'section' => is_string($section) && ProjectSettingsSection::tryFrom($section) !== null + ? $section + : ProjectSettingsSection::General->value, + ]); + } +} diff --git a/resources/css/app.css b/resources/css/app.css index 6f9d1f5..9543c5b 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,6 +1,6 @@ -@import 'tailwindcss'; +@import "tailwindcss"; -@import 'tw-animate-css'; +@import "tw-animate-css"; @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; @source '../../storage/framework/views/*.php'; @@ -9,13 +9,69 @@ @theme inline { --font-sans: - Instrument Sans, ui-sans-serif, system-ui, sans-serif, - 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; + Inter, Instrument Sans, ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + --font-headline-lg: + Inter, Instrument Sans, ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + --font-headline-lg-mobile: + Inter, Instrument Sans, ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + --font-headline-md: + Inter, Instrument Sans, ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + --font-body-md: + Inter, Instrument Sans, ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + --font-body-sm: + Inter, Instrument Sans, ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + --font-label-uppercase: + Inter, Instrument Sans, ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; - --radius-lg: var(--radius); - --radius-md: calc(var(--radius) - 2px); - --radius-sm: calc(var(--radius) - 4px); + --radius-sm: 0.25rem; + --radius-md: 0.25rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + + --spacing-container-margin: 1.5rem; + --spacing-column-gap: 1rem; + --spacing-card-padding: 0.75rem; + --spacing-stack-gap-sm: 0.5rem; + --spacing-stack-gap-md: 0.75rem; + + --text-headline-lg: 1.5rem; + --text-headline-lg--line-height: 2rem; + --text-headline-lg--font-weight: 600; + --text-headline-lg--letter-spacing: -0.025em; + --text-headline-md: 1.125rem; + --text-headline-md--line-height: 1.5rem; + --text-headline-md--font-weight: 600; + --text-headline-md--letter-spacing: -0.015em; + --text-headline-lg-mobile: 1.25rem; + --text-headline-lg-mobile--line-height: 1.75rem; + --text-headline-lg-mobile--font-weight: 600; + --text-body-md: 0.875rem; + --text-body-md--line-height: 1.25rem; + --text-body-md--font-weight: 400; + --text-body-sm: 0.8125rem; + --text-body-sm--line-height: 1.125rem; + --text-body-sm--font-weight: 400; + --text-label-uppercase: 0.6875rem; + --text-label-uppercase--line-height: 1rem; + --text-label-uppercase--font-weight: 700; + --text-label-uppercase--letter-spacing: 0.05em; + + --shadow-kanban-card: 0 4px 12px rgb(0 0 0 / 0.08); + --shadow-kanban-nav: 0 12px 30px rgb(15 23 42 / 0.06); --color-background: var(--background); --color-foreground: var(--foreground); @@ -59,6 +115,54 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + + --color-surface: var(--surface); + --color-surface-dim: var(--surface-dim); + --color-surface-bright: var(--surface-bright); + --color-surface-container-lowest: var(--surface-container-lowest); + --color-surface-container-low: var(--surface-container-low); + --color-surface-container: var(--surface-container); + --color-surface-container-high: var(--surface-container-high); + --color-surface-container-highest: var(--surface-container-highest); + --color-surface-variant: var(--surface-variant); + --color-surface-tint: var(--surface-tint); + --color-on-surface: var(--on-surface); + --color-on-surface-variant: var(--on-surface-variant); + --color-on-background: var(--on-background); + --color-outline: var(--outline); + --color-outline-variant: var(--outline-variant); + --color-primary-container: var(--primary-container); + --color-on-primary: var(--on-primary); + --color-on-primary-container: var(--on-primary-container); + --color-primary-fixed: var(--primary-fixed); + --color-primary-fixed-dim: var(--primary-fixed-dim); + --color-on-primary-fixed: var(--on-primary-fixed); + --color-on-primary-fixed-variant: var(--on-primary-fixed-variant); + --color-secondary-container: var(--secondary-container); + --color-on-secondary: var(--on-secondary); + --color-on-secondary-container: var(--on-secondary-container); + --color-secondary-fixed: var(--secondary-fixed); + --color-secondary-fixed-dim: var(--secondary-fixed-dim); + --color-on-secondary-fixed: var(--on-secondary-fixed); + --color-on-secondary-fixed-variant: var(--on-secondary-fixed-variant); + --color-tertiary: var(--tertiary); + --color-tertiary-container: var(--tertiary-container); + --color-on-tertiary: var(--on-tertiary); + --color-on-tertiary-container: var(--on-tertiary-container); + --color-tertiary-fixed: var(--tertiary-fixed); + --color-tertiary-fixed-dim: var(--tertiary-fixed-dim); + --color-on-tertiary-fixed: var(--on-tertiary-fixed); + --color-on-tertiary-fixed-variant: var(--on-tertiary-fixed-variant); + --color-error: var(--error); + --color-error-container: var(--error-container); + --color-on-error: var(--on-error); + --color-on-error-container: var(--on-error-container); + --color-inverse-surface: var(--inverse-surface); + --color-inverse-on-surface: var(--inverse-on-surface); + --color-inverse-primary: var(--inverse-primary); + --color-priority-high: #ef4444; + --color-priority-medium: #f59e0b; + --color-priority-low: #3b82f6; } /* @@ -83,83 +187,171 @@ body, html { --font-sans: - 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, - 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; + Inter, "Instrument Sans", ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; } } :root { - --background: hsl(0 0% 100%); - --foreground: hsl(0 0% 3.9%); - --card: hsl(0 0% 100%); - --card-foreground: hsl(0 0% 3.9%); - --popover: hsl(0 0% 100%); - --popover-foreground: hsl(0 0% 3.9%); - --primary: hsl(0 0% 9%); - --primary-foreground: hsl(0 0% 98%); - --secondary: hsl(0 0% 92.1%); - --secondary-foreground: hsl(0 0% 9%); - --muted: hsl(0 0% 96.1%); - --muted-foreground: hsl(0 0% 45.1%); - --accent: hsl(0 0% 96.1%); - --accent-foreground: hsl(0 0% 9%); - --destructive: hsl(0 84.2% 60.2%); - --destructive-foreground: hsl(0 0% 98%); - --border: hsl(0 0% 92.8%); - --input: hsl(0 0% 89.8%); - --ring: hsl(0 0% 3.9%); - --chart-1: hsl(12 76% 61%); - --chart-2: hsl(173 58% 39%); - --chart-3: hsl(197 37% 24%); - --chart-4: hsl(43 74% 66%); - --chart-5: hsl(27 87% 67%); - --radius: 0.5rem; - --sidebar-background: hsl(0 0% 98%); - --sidebar-foreground: hsl(240 5.3% 26.1%); - --sidebar-primary: hsl(0 0% 10%); - --sidebar-primary-foreground: hsl(0 0% 98%); - --sidebar-accent: hsl(0 0% 94%); - --sidebar-accent-foreground: hsl(0 0% 30%); - --sidebar-border: hsl(0 0% 91%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); - --sidebar: hsl(0 0% 98%); + --background: #f7f9fb; + --foreground: #191c1e; + --card: #ffffff; + --card-foreground: #191c1e; + --popover: #ffffff; + --popover-foreground: #191c1e; + --primary: #006e2f; + --primary-foreground: #ffffff; + --secondary: #d0e1fb; + --secondary-foreground: #0b1c30; + --muted: #f2f4f6; + --muted-foreground: #3d4a3d; + --accent: #e6e8ea; + --accent-foreground: #191c1e; + --destructive: #ba1a1a; + --destructive-foreground: #ffffff; + --border: #bccbb9; + --input: #bccbb9; + --ring: #006e2f; + --chart-1: #22c55e; + --chart-2: #3b82f6; + --chart-3: #f59e0b; + --chart-4: #ef4444; + --chart-5: #505f76; + --radius: 0.25rem; + --surface: #f7f9fb; + --surface-dim: #d8dadc; + --surface-bright: #f7f9fb; + --surface-container-lowest: #ffffff; + --surface-container-low: #f2f4f6; + --surface-container: #eceef0; + --surface-container-high: #e6e8ea; + --surface-container-highest: #e0e3e5; + --surface-variant: #e0e3e5; + --surface-tint: #006e2f; + --on-surface: #191c1e; + --on-surface-variant: #3d4a3d; + --on-background: #191c1e; + --outline: #6d7b6c; + --outline-variant: #bccbb9; + --primary-container: #22c55e; + --on-primary: #ffffff; + --on-primary-container: #004b1e; + --primary-fixed: #6bff8f; + --primary-fixed-dim: #4ae176; + --on-primary-fixed: #002109; + --on-primary-fixed-variant: #005321; + --secondary-container: #d0e1fb; + --on-secondary: #ffffff; + --on-secondary-container: #54647a; + --secondary-fixed: #d3e4fe; + --secondary-fixed-dim: #b7c8e1; + --on-secondary-fixed: #0b1c30; + --on-secondary-fixed-variant: #38485d; + --tertiary: #9e4036; + --tertiary-container: #ff8b7c; + --on-tertiary: #ffffff; + --on-tertiary-container: #76231b; + --tertiary-fixed: #ffdad5; + --tertiary-fixed-dim: #ffb4a9; + --on-tertiary-fixed: #410001; + --on-tertiary-fixed-variant: #7f2a21; + --error: #ba1a1a; + --error-container: #ffdad6; + --on-error: #ffffff; + --on-error-container: #93000a; + --inverse-surface: #2d3133; + --inverse-on-surface: #eff1f3; + --inverse-primary: #4ae176; + --sidebar-background: #f2f4f6; + --sidebar-foreground: #191c1e; + --sidebar-primary: #006e2f; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #e6e8ea; + --sidebar-accent-foreground: #191c1e; + --sidebar-border: #bccbb9; + --sidebar-ring: #006e2f; + --sidebar: #f2f4f6; } .dark { - --background: hsl(0 0% 3.9%); - --foreground: hsl(0 0% 98%); - --card: hsl(0 0% 3.9%); - --card-foreground: hsl(0 0% 98%); - --popover: hsl(0 0% 3.9%); - --popover-foreground: hsl(0 0% 98%); - --primary: hsl(0 0% 98%); - --primary-foreground: hsl(0 0% 9%); - --secondary: hsl(0 0% 14.9%); - --secondary-foreground: hsl(0 0% 98%); - --muted: hsl(0 0% 16.08%); - --muted-foreground: hsl(0 0% 63.9%); - --accent: hsl(0 0% 14.9%); - --accent-foreground: hsl(0 0% 98%); - --destructive: hsl(0 84% 60%); - --destructive-foreground: hsl(0 0% 98%); - --border: hsl(0 0% 14.9%); - --input: hsl(0 0% 14.9%); - --ring: hsl(0 0% 83.1%); - --chart-1: hsl(220 70% 50%); - --chart-2: hsl(160 60% 45%); - --chart-3: hsl(30 80% 55%); - --chart-4: hsl(280 65% 60%); - --chart-5: hsl(340 75% 55%); - --sidebar-background: hsl(0 0% 7%); - --sidebar-foreground: hsl(0 0% 95.9%); - --sidebar-primary: hsl(360, 100%, 100%); - --sidebar-primary-foreground: hsl(0 0% 100%); - --sidebar-accent: hsl(0 0% 15.9%); - --sidebar-accent-foreground: hsl(240 4.8% 95.9%); - --sidebar-border: hsl(0 0% 15.9%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); - --sidebar: hsl(240 5.9% 10%); + --background: #0b1326; + --foreground: #e5eaf5; + --card: #131b2e; + --card-foreground: #e5eaf5; + --popover: #171f33; + --popover-foreground: #e5eaf5; + --primary: #4be277; + --primary-foreground: #002109; + --secondary: #222a3d; + --secondary-foreground: #e5eaf5; + --muted: #171f33; + --muted-foreground: #aeb8cd; + --accent: #222a3d; + --accent-foreground: #f3f6fc; + --destructive: #ffb4ab; + --destructive-foreground: #690005; + --border: #3b4358; + --input: #4a5268; + --ring: #4be277; + --chart-1: #4be277; + --chart-2: #60a5fa; + --chart-3: #fbbf24; + --chart-4: #f87171; + --chart-5: #b7c8e1; + --surface: #0b1326; + --surface-dim: #060e20; + --surface-bright: #2d3449; + --surface-container-lowest: #060e20; + --surface-container-low: #131b2e; + --surface-container: #171f33; + --surface-container-high: #222a3d; + --surface-container-highest: #2d3449; + --surface-variant: #2d3449; + --surface-tint: #4be277; + --on-surface: #e5eaf5; + --on-surface-variant: #aeb8cd; + --on-background: #e5eaf5; + --outline: #8993aa; + --outline-variant: #3b4358; + --primary-container: #136c35; + --on-primary: #002109; + --on-primary-container: #bbf7d0; + --primary-fixed: #6bff8f; + --primary-fixed-dim: #4be277; + --on-primary-fixed: #002109; + --on-primary-fixed-variant: #005321; + --secondary-container: #1e3a5f; + --on-secondary: #ffffff; + --on-secondary-container: #d0e1fb; + --secondary-fixed: #d3e4fe; + --secondary-fixed-dim: #b7c8e1; + --on-secondary-fixed: #0b1c30; + --on-secondary-fixed-variant: #38485d; + --tertiary: #ffb4a9; + --tertiary-container: #7f2a21; + --on-tertiary: #410001; + --on-tertiary-container: #ffdad5; + --tertiary-fixed: #ffdad5; + --tertiary-fixed-dim: #ffb4a9; + --on-tertiary-fixed: #410001; + --on-tertiary-fixed-variant: #ffb4a9; + --error: #f87171; + --error-container: #7f1d1d; + --on-error: #ffffff; + --on-error-container: #fee2e2; + --inverse-surface: #eff1f3; + --inverse-on-surface: #191c1e; + --inverse-primary: #006e2f; + --sidebar-background: #060e20; + --sidebar-foreground: #e5eaf5; + --sidebar-primary: #4be277; + --sidebar-primary-foreground: #002109; + --sidebar-accent: #171f33; + --sidebar-accent-foreground: #f3f6fc; + --sidebar-border: #2d3449; + --sidebar-ring: #4be277; + --sidebar: #060e20; } @layer base { @@ -167,6 +359,74 @@ @apply border-border outline-ring/50; } body { - @apply bg-background text-foreground; + @apply min-h-screen bg-background text-foreground antialiased; + letter-spacing: -0.006em; + } + + ::selection { + background: color-mix(in srgb, var(--primary) 24%, transparent); + color: var(--foreground); + } + + textarea, + select { + @apply border-input bg-surface-container-lowest text-foreground rounded-md border px-3 py-2 text-sm shadow-xs outline-none transition-[border-color,box-shadow,background-color]; + background-color: var(--surface-container-lowest) !important; + } + + textarea:focus-visible, + select:focus-visible { + @apply border-ring ring-ring/25 ring-3; + } + + textarea:disabled, + select:disabled { + @apply cursor-not-allowed opacity-55; + } + + table { + @apply w-full border-separate border-spacing-0; + } + + thead { + @apply bg-surface-container-low text-on-surface-variant; + } + + tbody tr { + @apply transition-colors hover:bg-primary/3; + } + + th { + @apply text-label-uppercase font-bold tracking-[0.08em]; + } +} + +@layer components { + .app-page { + @apply mx-auto flex w-full max-w-[96rem] flex-1 flex-col gap-6 px-4 py-5 sm:px-6 sm:py-7 lg:px-8; + } + + .app-page-narrow { + @apply mx-auto flex w-full max-w-5xl flex-1 flex-col gap-6 px-4 py-5 sm:px-6 sm:py-7 lg:px-8; + } + + .page-actions { + @apply flex flex-wrap items-center gap-2; + } + + .status-success { + @apply border-primary/25 bg-primary/8 text-on-primary-container rounded-lg border px-4 py-3 text-sm dark:text-primary-fixed; + } + + .empty-state { + @apply border-outline-variant bg-surface-container-low text-on-surface-variant rounded-lg border border-dashed px-5 py-8 text-center text-sm; + } + + .table-shell { + @apply border-outline-variant bg-surface-container-lowest overflow-hidden rounded-xl border; + } + + .eyebrow { + @apply text-label-uppercase text-primary font-bold uppercase tracking-[0.12em]; } } diff --git a/resources/js/app.ts b/resources/js/app.ts index c098682..e4a6cf4 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -20,7 +20,7 @@ createInertiaApp({ .mount(el); }, progress: { - color: '#4B5563', + color: '#22c55e', }, }); diff --git a/resources/js/components/AppContent.vue b/resources/js/components/AppContent.vue index 555a1a6..1598df3 100644 --- a/resources/js/components/AppContent.vue +++ b/resources/js/components/AppContent.vue @@ -1,14 +1,9 @@ diff --git a/resources/js/components/AppLogo.vue b/resources/js/components/AppLogo.vue index 2de902d..f7e20d5 100644 --- a/resources/js/components/AppLogo.vue +++ b/resources/js/components/AppLogo.vue @@ -4,13 +4,13 @@ import AppLogoIcon from '@/components/AppLogoIcon.vue'; diff --git a/resources/js/components/AppLogoIcon.vue b/resources/js/components/AppLogoIcon.vue index 9a26231..152cd3f 100644 --- a/resources/js/components/AppLogoIcon.vue +++ b/resources/js/components/AppLogoIcon.vue @@ -15,15 +15,15 @@ defineProps(); diff --git a/resources/js/components/AppShell.vue b/resources/js/components/AppShell.vue index e76af0b..f691bac 100644 --- a/resources/js/components/AppShell.vue +++ b/resources/js/components/AppShell.vue @@ -1,13 +1,9 @@