Skip to content

feat(components): forms.button — migrate all buttons + establish the role model#3531

Merged
marcelfolaron merged 13 commits into
masterfrom
feature/componentization
Jun 20, 2026
Merged

feat(components): forms.button — migrate all buttons + establish the role model#3531
marcelfolaron merged 13 commits into
masterfrom
feature/componentization

Conversation

@marcelfolaron

@marcelfolaron marcelfolaron commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Button componentization — <x-global::forms.button>

Establishes the first design-system primitive (forms.button) and routes the app's buttons through it. Scope grew well beyond the original "foundation only" PR (per ongoing direction), so the description below reflects what it actually does now.

Phases (each committed + verified separately)

  1. Component + trackerforms.button (full prop IDL: contentRole/state/scale/variant/tag/leadingVisual…) + COMPONENTS.md (the living tracker/playbook).
  2. No-op migration — ~250 plain buttons across ~90 templates migrated to the component, rendering byte-for-byte today's classes (verified via Playwright before/after + a brace/quote-aware scan). JS-coupled buttons (dropdown-toggles, file-uploads, calendar controls) were deferred.
  3. Role model (deliberate, NOT a no-op) — now that role is an explicit prop, the semantically-wrong cases are corrected:
    • secondary renders outline (role carries its look); tertiary/ghost = transparent.
    • Cancel / Back / Close / Skiptertiary · Save & Closesecondary (outline) · Delete/Removestate="danger" outline (low-chrome) · main actions stay primary.
    • These are intentional visible changes; the role styles are lightly-styled today and get their final look in the design phase.
  4. Bug + review fixes — nested-double-quote-in-attribute bugs repaired; variant="outline" uses the validated $stateClass; doc/ownership fixes.

Reviewer note on "no-op vs visible change"

The migration (phase 2) is a strict no-op. The role normalization (phase 3) is an intentional semantic correction requested during review — it changes some button appearances on purpose (e.g. a "Back" button that was hard-coded btn-primary is now correctly low-emphasis). The earlier "no-op only / no migrations yet" wording is superseded by this description.

Verification

view:cache compiles clean at each step; Pint passes; brace/quote-aware scan reports 0 nested-quote bugs; Playwright no-op spot-checks on /users/showAll, /goalcanvas/showCanvas, /projects/createnew.

🤖 Generated with Claude Code

marcelfolaron and others added 6 commits June 14, 2026 12:16
…tracker

Start the frontend componentization effort with a deliberately conservative,
no-op-first approach (no daisyUI / no tw-prefix churn / no JS rewrite yet):

- COMPONENTS.md: the living tracker + playbook — strategy rules, 6-category
  taxonomy (<x-global::{category}.{name}>), the prop IDL, the no-op mapping
  principle, the component registry (P0/P1/domain), the reference-only branch
  landscape, card-naming resolution, and the per-component process.
- forms/button.blade.php: first no-op component. Renders today's exact
  btn/forms.css classes (zero visual change); call-sites write against the
  canonical prop vocabulary (contentRole/state/scale/variant/tag/leadingVisual)
  so the eventual design swap is a one-file change.

No call-sites migrated yet — that follows after a Playwright before/after pilot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d no-op pilot)

First call-site migration. Verified zero visual change via Playwright before/after on
/auth/login: identical className (btn btn-primary), computed style, and dimensions
(49x22) — only HTML attribute order differs.

- login submit + OIDC anchor -> <x-global::forms.button>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lt color

Two no-op-correctness fixes found while prepping the call-site migration:
- bare <button> now emits NO type attribute (was forcing type="button"), so the
  native default (submit inside a form) is preserved — migrating a form submit
  button no longer silently stops submitting.
- contentRole default is now '' (was 'primary'), so a plain `btn` button isn't
  given an unwanted btn-primary. Callers set the role explicitly from the source.

Re-verified /auth/login still renders byte-identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e templates (no-op)

Batch 1 — form/admin/CRUD domains (Api, Auth, Clients, Comments, Connector, Errors, Help,
Install, Plugins, Projects, Setting, Timesheets, TwoFA, Users). ~65 plain buttons migrated to
<x-global::forms.button> via a 9-agent fan-out on disjoint files.

JS-coupled / ambiguous buttons deferred to preserve the no-op (see COMPONENTS.md backlog):
dropdown-toggles, file-uploads, class="button" (not btn) sets, unstyled submit inputs,
unmapped variants (btn-sm/btn-lg/btn-danger-outline), role+state combos, <a onclick> w/o href.

Verified: view:cache compiles clean; audit confirms no JS-coupled class was swallowed by a
forms.button; the forms.button component itself proven byte-identical on /auth/login.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lets <a> buttons that have no href in the source (e.g. <a class="btn" onclick="...">)
migrate without the component injecting href="#". Explicit-link anchors are unchanged
(re-verified on /users/showAll). Unblocks the JS-action buttons common in the dashboard/
canvas/widget domains (Batch 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heavy templates (no-op)

Batch 2 — Tickets, Dashboard, Widgets, Canvas/Blueprints/Goalcanvas/Logicmodel, Ideas, Wiki,
Calendar, Sprints. 93 plain buttons migrated to <x-global::forms.button> (9-agent fan-out, disjoint files).

Deferred to preserve the no-op: dropdown-toggles (-> dropdown-component phase), fc-* calendar
controls, file-uploads, class="button"/no-class submit inputs (design decision), unmapped variants
(btn-outline/btn-sm/btn-danger-outline), role+state combos.

Verified: view:cache compiles clean; audit confirms no deferred class was swallowed by a
forms.button; live no-op spot-check on /goalcanvas/showCanvas (class/href/id/inner all preserved).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
<p>{!! __('text.confirm_key_deletion') !!}</p><br />
<input type="submit" value="{{ __('buttons.yes_delete') }}" name="del" class="button" />
<a class="btn btn-primary" href="{{ BASE_URL }}/setting/editCompanySettings/#apiKeys">{!! __('buttons.back') !!}</a>
<x-global::forms.button tag="a" link="{{ BASE_URL }}/setting/editCompanySettings/#apiKeys" contentRole="primary">{!! __('buttons.back') !!}</x-global::forms.button>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue that a back button is secondary or just tertiary

marcelfolaron and others added 5 commits June 14, 2026 13:54
…ndary

Semantic content-role sanity pass over the migrated forms.button call-sites. 15 Back / Cancel /
"Go Back" buttons that were hard-coded btn-primary in the original markup (now an explicit role)
are demoted to contentRole="secondary" — they're alternative / navigate-away actions, not the
primary CTA. Only the contentRole VALUE changed (diff verified to be role swaps only).

NOTE: this intentionally changes appearance (no longer a no-op). btn-secondary is currently
unstyled, so these render less prominent until the design phase styles secondary buttons.

Left as-is: genuine primaries (Save/Create/Add/Submit/Login/Connect), destructive confirms
(danger-state pass comes later), and 2 Ideas board-submit buttons that are `default` but arguably
should be primary for consistency — flagged for review, not changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mary

Companion to the demotion pass: 5 buttons that are the single main affirmative action of their
form/modal but were styled btn-default (now explicit role) promoted to contentRole="primary" for
consistency with their siblings:
- Ideas board create/save submits (advancedBoards + showBoards, x4) — Blueprints/Canvas board
  dialogs already use primary for the same action.
- Comments/showAll reply submit — generalComment's reply was already primary.

Only the contentRole VALUE changed (diff verified: 5 default -> primary, nothing else). Genuinely
secondary default buttons (Back, Export, Copy, Reset Logo, Resend Invite, Save-and-Close next to a
primary submit, Close, Activate) left as-is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ave & Close"

- forms.button gains variant="outline" -> btn-outline (or btn-{state}-outline, e.g.
  btn-danger-outline). This is the existing outline style used by the edit-ticket save button.
- All "Save & Close" buttons set to variant="outline" for a consistent secondary-emphasis save
  treatment matching the edit-ticket screen: the 5 canvas/idea dialog saveAndClose buttons (were
  default/primary) + the Tickets/Wiki save-and-close inputs (were raw <input class="btn btn-outline">,
  now componentized — a no-op for those two).

The delEvent/delExternalCal "Yes, delete" confirms (which reuse the saveAndClose id) are left
primary — they are delete confirms, not Save & Close.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ttons

~35 Cancel / Back / Close / Delete / Remove actions that were bare <a> text-links (no btn class)
— including the subtask quick-add Cancel — converted to <x-global::forms.button contentRole="secondary">,
preserving onclick + JS-hook classes (delete / formModal / editTimeModal / blueprintsCanvasModal / ...).

Strictly skipped (left as-is): dropdown <li> menu-items (incl. menu delete/edit), accordion + inline
"|"-separated toggles, add/create open-form toggles, navigation, timer links, and already-btn links.
Audit confirms no menu-item was converted.

Still bare (deliberately not converted, flagged): inline per-comment deleteComment links + per-row
table delete actions — those want a smaller/inline treatment, not a full secondary button.

Verified: view:cache compiles, Pint passes (30 files), no forms.button placed inside a <li>.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inks

Component attributes parse more strictly than a raw HTML href: a double-quote inside a {{ }}
expression in an attribute value terminates the attribute early and breaks the tag (fails at
render, not compile — so view:cache didn't catch it). The button migration moved a few such
hrefs into the component's link= attribute:

- Projects/createnew: link="...{{ $projectType["url"] }}" + class="{{ ... ? "disabled" : "" }}"
  -> single-quote the array key + drop the redundant always-empty class ternary.
- Goalcanvas/canvasDialog + delCanvas: link="{{ BASE_URL . "/path/$id" }}" -> Blade {{ }} interpolation.

A brace/quote-aware scan of ALL x- components confirms these were the only instances (now 0).
Documented the gotcha + the scan in COMPONENTS.md so future migrations avoid it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@marcelfolaron marcelfolaron marked this pull request as ready for review June 20, 2026 15:43
@marcelfolaron marcelfolaron requested a review from a team as a code owner June 20, 2026 15:43
@marcelfolaron marcelfolaron requested review from broskees and Copilot and removed request for a team June 20, 2026 15:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces the initial frontend componentization foundation by adding a documented component taxonomy/IDL tracker and a first canonical <x-global::forms.button> component, then applies that component across many existing Blade templates.

Changes:

  • Added app/Views/Templates/components/COMPONENTS.md as a living tracker/playbook defining taxonomy, prop vocabulary (IDL), and migration process.
  • Added app/Views/Templates/components/forms/button.blade.php implementing a canonical button API mapped to today’s btn* classes (plus an outline variant).
  • Migrated many templates to use <x-global::forms.button> in place of raw <a>/<button>/<input> markup.

Reviewed changes

Copilot reviewed 114 out of 114 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
app/Views/Templates/components/forms/button.blade.php New forms.button component mapping canonical props to existing btn* classes.
app/Views/Templates/components/COMPONENTS.md Componentization tracker/playbook and registry/IDL documentation.
app/Domain/Wiki/Templates/show.blade.php Replaces button/link markup with forms.button.
app/Domain/Wiki/Templates/delWiki.blade.php Replaces back link with forms.button.
app/Domain/Wiki/Templates/delArticle.blade.php Replaces back link with forms.button.
app/Domain/Wiki/Templates/articleDialog.blade.php Replaces multiple buttons/links with forms.button (incl. outline usage).
app/Domain/Widgets/Templates/partials/welcome.blade.php Replaces header action links with forms.button.
app/Domain/Widgets/Templates/partials/todoItem.blade.php Replaces task edit/save/cancel controls with forms.button.
app/Domain/Widgets/Templates/partials/myToDos.blade.php Replaces add/save/cancel links/inputs with forms.button.
app/Domain/Users/Templates/showAll.blade.php Replaces “Add user” button with forms.button.
app/Domain/Users/Templates/editUser.blade.php Replaces copy/resend invite actions with forms.button.
app/Domain/Users/Templates/delUser.blade.php Replaces back link with forms.button.
app/Domain/TwoFA/Templates/verify.blade.php Replaces submit input with forms.button.
app/Domain/TwoFA/Templates/edit.blade.php Replaces back link with forms.button.
app/Domain/Timesheets/Templates/editTime.blade.php Replaces delete link with forms.button.
app/Domain/Timesheets/Templates/delTime.blade.php Replaces back link with forms.button.
app/Domain/Tickets/Templates/submodules/ticketFilter.blade.php Replaces filter/search controls with forms.button.
app/Domain/Tickets/Templates/submodules/ticketDetails.blade.php Replaces “Save & Close” input with forms.button outline variant.
app/Domain/Tickets/Templates/submodules/subTasks.blade.php Replaces cancel link with forms.button.
app/Domain/Tickets/Templates/showList.blade.php Replaces quick-add save input with forms.button.
app/Domain/Tickets/Templates/partials/subtasks.blade.php Replaces cancel link with forms.button.
app/Domain/Tickets/Templates/partials/quickadd-form.blade.php Replaces save/cancel buttons with forms.button.
app/Domain/Tickets/Templates/moveTicket.blade.php Replaces back link with forms.button.
app/Domain/Tickets/Templates/milestoneDialog.blade.php Replaces submit input with forms.button.
app/Domain/Tickets/Templates/delTicket.blade.php Replaces back link with forms.button.
app/Domain/Tickets/Templates/delMilestone.blade.php Replaces back link with forms.button.
app/Domain/Sprints/Templates/delSprint.blade.php Replaces back link with forms.button.
app/Domain/Setting/Templates/editCompanySettings.blade.php Replaces links/buttons for logo reset + API key generation with forms.button.
app/Domain/Projects/Templates/showProject.blade.php Replaces duplicate + save actions with forms.button.
app/Domain/Projects/Templates/showAll.blade.php Replaces “New project” link with forms.button.
app/Domain/Projects/Templates/projectHub.blade.php Replaces create/new project links with forms.button.
app/Domain/Projects/Templates/partials/projectHubProjects.blade.php Replaces new project link with forms.button.
app/Domain/Projects/Templates/newProject.blade.php Replaces delete link with forms.button.
app/Domain/Projects/Templates/delProject.blade.php Replaces back link with forms.button.
app/Domain/Projects/Templates/createnew.blade.php Replaces project type buttons/disabled state with forms.button.
app/Domain/Plugins/Templates/myapps.blade.php Replaces activate link with forms.button.
app/Domain/Logicmodelcanvas/Templates/showCanvas.blade.php Replaces “create new board” link with forms.button.
app/Domain/Logicmodelcanvas/Templates/canvasDialog.blade.php Replaces save/save&close/delete actions with forms.button.
app/Domain/Install/Templates/update.blade.php Replaces submit input with forms.button.
app/Domain/Install/Templates/new.blade.php Replaces submit input with forms.button.
app/Domain/Ideas/Templates/showBoards.blade.php Replaces add/create/save/close controls with forms.button.
app/Domain/Ideas/Templates/ideaDialog.blade.php Replaces save/save&close/delete actions with forms.button (incl. outline usage).
app/Domain/Ideas/Templates/delCanvasItem.blade.php Replaces back link with forms.button.
app/Domain/Ideas/Templates/delCanvas.blade.php Replaces back link with forms.button.
app/Domain/Ideas/Templates/advancedBoards.blade.php Replaces add/create/save controls with forms.button.
app/Domain/Help/Templates/support.blade.php Replaces sponsorship/marketplace links with forms.button.
app/Domain/Help/Templates/simpleLeanCanvas.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/showProjects.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/showClients.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/roadmap.blade.php Replaces modal action links with forms.button.
app/Domain/Help/Templates/projectSuccess.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/projectDashboard.blade.php Replaces modal action links with forms.button.
app/Domain/Help/Templates/notfound.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/newProject.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/mytimesheets.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/kanban.blade.php Replaces modal action links with forms.button.
app/Domain/Help/Templates/inviteTeamStep.blade.php Replaces “skip” link with forms.button.
app/Domain/Help/Templates/ideationBoard.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/ideaBoard.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/home.blade.php Replaces modal action links with forms.button.
app/Domain/Help/Templates/goals.blade.php Replaces modal action links with forms.button.
app/Domain/Help/Templates/fullLeanCanvas.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/firstTaskStep.blade.php Replaces submit input with forms.button.
app/Domain/Help/Templates/dashboard.blade.php Replaces CTA link with forms.button.
app/Domain/Help/Templates/blueprints.blade.php Replaces CTA link with forms.button.
app/Domain/Help/Templates/backlog.blade.php Replaces close link with forms.button.
app/Domain/Help/Templates/advancedBoards.blade.php Replaces close links with forms.button.
app/Domain/Goalcanvas/Templates/showCanvas.blade.php Replaces add/create links with forms.button.
app/Domain/Goalcanvas/Templates/delCanvasItem.blade.php Replaces back link with forms.button.
app/Domain/Goalcanvas/Templates/delCanvas.blade.php Replaces back link with forms.button.
app/Domain/Goalcanvas/Templates/canvasDialog.blade.php Replaces save&close/delete/remove actions with forms.button.
app/Domain/Errors/Templates/error501.blade.php Replaces dashboard link with forms.button.
app/Domain/Errors/Templates/error500.blade.php Replaces dashboard link with forms.button.
app/Domain/Errors/Templates/error404.blade.php Replaces dashboard link with forms.button.
app/Domain/Errors/Templates/error403.blade.php Replaces dashboard link with forms.button.
app/Domain/Dashboard/Templates/show.blade.php Replaces copy link + action links with forms.button.
app/Domain/Connector/Templates/show.blade.php Replaces integration CTA links with forms.button.
app/Domain/Connector/Templates/newIntegration.blade.php Replaces connect link with forms.button.
app/Domain/Connector/Templates/integrationImport.blade.php Replaces back/confirm links with forms.button.
app/Domain/Connector/Templates/integrationFields.blade.php Replaces back/next actions with forms.button.
app/Domain/Connector/Templates/integrationEntity.blade.php Replaces back/next actions with forms.button.
app/Domain/Comments/Templates/submodules/generalComment.blade.php Replaces reply/cancel inputs with forms.button.
app/Domain/Comments/Templates/showAll.blade.php Replaces reply input with forms.button.
app/Domain/Clients/Templates/showClient.blade.php Replaces add/remove/save/delete actions with forms.button.
app/Domain/Clients/Templates/showAll.blade.php Replaces “New client” link with forms.button.
app/Domain/Clients/Templates/newClient.blade.php Replaces save input with forms.button.
app/Domain/Clients/Templates/delClient.blade.php Replaces back link with forms.button.
app/Domain/Canvas/Templates/showCanvasTop.blade.php Replaces add-item link with forms.button.
app/Domain/Canvas/Templates/helper.blade.php Replaces close link with forms.button.
app/Domain/Canvas/Templates/delCanvasItem.blade.php Replaces back link with forms.button.
app/Domain/Canvas/Templates/delCanvas.blade.php Replaces back link with forms.button.
app/Domain/Canvas/Templates/canvasDialog.blade.php Replaces save&close/delete/remove actions with forms.button.
app/Domain/Canvas/Templates/boardDialog.blade.php Replaces create/save/close buttons with forms.button.
app/Domain/Calendar/Templates/showMyCalendar.blade.php Replaces add/export links with forms.button.
app/Domain/Calendar/Templates/importGCal.blade.php Replaces save input with forms.button.
app/Domain/Calendar/Templates/export.blade.php Replaces remove link with forms.button.
app/Domain/Calendar/Templates/editExternalCalendar.blade.php Replaces save input with forms.button.
app/Domain/Calendar/Templates/editEvent.blade.php Replaces delete link with forms.button.
app/Domain/Calendar/Templates/delExternalCal.blade.php Replaces yes-delete/back actions with forms.button.
app/Domain/Calendar/Templates/delEvent.blade.php Replaces yes-delete/back actions with forms.button.
app/Domain/Calendar/Templates/connectCalendar.blade.php Replaces import/connect actions with forms.button.
app/Domain/Blueprints/Templates/showCanvasTop.blade.php Replaces add-item link with forms.button.
app/Domain/Blueprints/Templates/showBoards.blade.php Replaces CTA link with forms.button.
app/Domain/Blueprints/Templates/helper.blade.php Replaces close link with forms.button.
app/Domain/Blueprints/Templates/delCanvasItem.blade.php Replaces back link with forms.button.
app/Domain/Blueprints/Templates/delCanvas.blade.php Replaces back link with forms.button.
app/Domain/Blueprints/Templates/canvasDialog.blade.php Replaces save&close/delete/remove actions with forms.button.
app/Domain/Blueprints/Templates/boardDialog.blade.php Replaces create/save/close buttons with forms.button.
app/Domain/Auth/Templates/userInvite4.blade.php Replaces back link with forms.button.
app/Domain/Auth/Templates/userInvite3.blade.php Replaces back link with forms.button.
app/Domain/Auth/Templates/userInvite2.blade.php Replaces back link with forms.button.
app/Domain/Auth/Templates/login.blade.php Replaces login/oidc buttons with forms.button.
app/Domain/Api/Templates/newAPIKey.blade.php Replaces copy button with forms.button.
app/Domain/Api/Templates/delKey.blade.php Replaces back link with forms.button.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +64
if ($variant === 'outline') {
$colorClass = $state !== '' ? 'btn-'.$state.'-outline' : 'btn-outline';
} else {
$colorClass = $stateClass !== '' ? $stateClass : $roleClass;
}
Comment on lines +3 to +5
> **Owner:** maintained by Claude as the single source of truth for the componentization
> effort. Supersedes the "Component Updates Tracker" spreadsheet (whose *status* column is
> stale — the taxonomy, naming, prop vocabulary, and priorities are kept).
Comment on lines +44 to +47
| Prop | Options | Default | Notes |
|---|---|---|---|
| `contentRole` | default · primary · secondary · tertiary(=ghost) · accent · link | primary (actions) | semantic role |
| `state` | default · info · warning · danger · success | default | |
Comment on lines +198 to +213
- _button pilot_: `Auth/login` migrated; Playwright before/after = byte-identical (proven).
- _button batch 1_: ~65 plain buttons migrated across 46 core form/admin/CRUD templates (9-agent
fan-out, disjoint files); ~70 deferred per the backlog above. Verified: view:cache compiles,
audit shows no JS-coupled class swallowed, real before/after on /users/showAll = identical class set.
- _button href tweak_: component emits href only when `link` is set (so `<a onclick>` w/o href migrates).
- _button batch 2_: ~100 plain buttons migrated across 43 JS-heavy templates (Tickets, Dashboard,
Widgets, Canvas/Blueprints/Goalcanvas/Logicmodel, Ideas, Wiki, Calendar, Sprints); the rest deferred
(dropdown-toggles, fc-* calendar, file-uploads, class="button", unmapped variants, role+state).
Verified: compile clean, audit clean, live no-op spot-check on /goalcanvas/showCanvas.
**Core plain-button migration is now essentially complete** — remaining work = the deferral backlog
(dropdowns get migrated in the dropdown-component phase; class="button"/unstyled = design decisions).
- _button role sanity pass_: 15 Back/Cancel/"Go Back" buttons that were hard-coded btn-primary in the
original markup demoted to contentRole="secondary" (alternative/navigate-away actions). Only the role
VALUE changed. This is intentionally NOT a no-op (appearance changes; secondary is unstyled until the
design phase).
- _button role promotions_: 5 main-action submits that were `default` promoted to `primary` for
<p>
</p>
<a href="javascript:void(0);" onclick="jQuery.nmTop().close()">{!! __('links.close') !!}</a><br />
<x-global::forms.button tag="a" link="javascript:void(0);" onclick="jQuery.nmTop().close()" contentRole="secondary">{!! __('links.close') !!}</x-global::forms.button><br />
<p>{!! __('text.confirm_user_deletion') !!}</p><br />
<input type="submit" value="{{ __('buttons.yes_delete') }}" name="del" class="button" />
<a class="btn btn-primary" href="{{ BASE_URL }}/users/showAll">{!! __('buttons.back') !!}</a>
<x-global::forms.button tag="a" contentRole="secondary" link="{{ BASE_URL }}/users/showAll">{!! __('buttons.back') !!}</x-global::forms.button>
<input type="submit" value="{{ $tpl->__('buttons.save') }}" id="primaryCanvasSubmitButton" class="btn btn-primary"/>
<button type="submit" class="btn btn-default" value="closeModal" id="saveAndClose" onclick="leantime.canvasController.setCloseModal();">{!! $tpl->__('buttons.save_and_close') !!}</button>
<x-global::forms.button tag="input" inputType="submit" :labelText="$tpl->__('buttons.save')" id="primaryCanvasSubmitButton" contentRole="primary"/>
<x-global::forms.button inputType="submit" variant="outline" value="closeModal" id="saveAndClose" onclick="leantime.canvasController.setCloseModal();">{!! $tpl->__('buttons.save_and_close') !!}</x-global::forms.button>

<input type="submit" value="{{ __('buttons.save') }}" id="primaryCanvasSubmitButton"/>
<button class="btn btn-primary" type="submit" value="closeModal" id="saveAndClose">{!! __('buttons.save_and_close') !!}</button>
<x-global::forms.button variant="outline" inputType="submit" value="closeModal" id="saveAndClose">{!! __('buttons.save_and_close') !!}</x-global::forms.button>
Comment on lines +18 to +22
<x-global::forms.button tag="a" onclick="leantime.ticketsController.toggleFilterBar();" style="margin-right:5px;"
contentRole="link" data-tippy-content="{{ __('popover.filter') }}">
<i class="fas fa-filter"></i> Filter{!! $numOfFilters > 0 ? " <span class='badge badge-primary'>" . $numOfFilters . '</span> ' : '' !!}
{{-- Please don't change the code formatting below, if not right next to each other it somehow adds a space between the two buttons and increases the distance --}}
</a>@if ($currentRoute !== 'tickets.roadmap' && $currentRoute != 'tickets.showProjectCalendar')<div class="btn-group viewDropDown">
</x-global::forms.button>@if ($currentRoute !== 'tickets.roadmap' && $currentRoute != 'tickets.showProjectCalendar')<div class="btn-group viewDropDown">
…fixes

Role model (role carries emphasis AND its default look):
- secondary now renders btn-outline (was the unstyled btn-secondary) — outline IS secondary.
- Cancel / Back / Close / Skip / "explore on my own" -> contentRole="tertiary" (ghost, low-chrome).
- Delete / Remove triggers -> state="danger" variant="outline" (btn-danger-outline, low-chrome);
  "Yes, delete" primary confirms left loud/filled.
- "Save & Close" -> contentRole="secondary" (dropped the now-redundant variant="outline").

PR review fixes (Copilot):
- forms.button outline uses the validated $stateClass, so state="default" -> btn-outline
  (not the nonexistent btn-default-outline).
- COMPONENTS.md: owner = frontend/UI maintainers (not "Claude"); clarified that forms.button
  defaults contentRole to '' (no color) vs the design-system "primary" intent.

(ticketFilter whitespace revert + a couple re-tiers live in files entangled with concurrent
ticket-redesign/sprint work; handled separately so this commit stays clean.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@marcelfolaron marcelfolaron changed the title feat(components): componentization foundation — no-op forms.button + tracker feat(components): forms.button — migrate all buttons + establish the role model Jun 20, 2026
@marcelfolaron

Copy link
Copy Markdown
Collaborator Author

Thanks for the review — addressed all of these (commit c52134572, plus a PR-description rewrite):

Role model (resolves the "back should be secondary/tertiary" + most "not a no-op" comments)
Established the full design-system role model and re-tiered the call-sites:

  • secondary now renders outline (the role carries its look — no variant needed).
  • Cancel / Back / Close / Skip / "explore on my own" → tertiary (ghost, low-chrome).
  • Save & Close → secondary (outline) — dropped the redundant variant="outline".
  • Delete / Remove → state="danger" outline (low-chrome btn-danger-outline); "Yes, delete" confirms stay loud/primary.

So e.g. the delUser Back is now tertiary, the Logicmodel/ideaDialog "Save & Close" are secondary(outline), and simpleLeanCanvas's close link is a tertiary button.

On "no-op vs visible change" — fair callout; the migration is a strict no-op, but the role normalization is an intentional semantic correction (requested mid-review) that purposely changes some button appearances. I've rewritten the PR title/description to reflect the evolved scope instead of the stale "no-op only / no migrations yet" wording. Role styles are lightly-styled today and get their final look in the design phase.

forms.button outline + $state — fixed: variant="outline" now keys off the validated $stateClass, so state="default"btn-outline (never the nonexistent btn-default-outline).

COMPONENTS.md — owner changed from "Claude" to the frontend/UI maintainers; and the IDL table now notes forms.button defaults contentRole to '' (no color, to preserve plain btn) vs the design-system primary intent.

ticketFilter whitespace — good catch; reverted that button to a raw <a> (whitespace-tight, with an inline comment + a note in COMPONENTS.md's gotchas). That file is concurrently being redesigned (multi-project program boards) by a separate effort, so the revert currently lives in the working tree and will commit with that file's changes to avoid clobbering the in-progress redesign.

…pace-sensitive)

Addresses PR #3531 review: the Filter toggle relies on </a>@if adjacency to avoid an inline-block
gap with the adjacent view dropdown; routing it through forms.button reintroduces surrounding
whitespace. Reverted to a raw <a class="btn btn-link"> with an inline comment (+ COMPONENTS.md
gotcha note); re-migrate once the component guarantees whitespace-tight output.

Staged precisely against HEAD so the concurrent (still-uncommitted) ticket-redesign of this same
file in the working tree is left untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 20, 2026 17:01
@marcelfolaron marcelfolaron merged commit afd1223 into master Jun 20, 2026
12 checks passed
@marcelfolaron marcelfolaron deleted the feature/componentization branch June 20, 2026 17:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 114 out of 114 changed files in this pull request and generated 6 comments.

Comment on lines +14 to +28
{{--
forms.button — NO-OP button.

Renders the exact Bootstrap/forms.css classes the app uses TODAY (btn, btn-primary,
btn-danger, btn-small, …) so there is zero visual change. Call-sites are written against
the canonical prop vocabulary (contentRole/state/scale); at design time ONLY the maps
below + the CSS change, restyling every button from one place. See COMPONENTS.md.

Migration cheatsheet (today's class -> prop):
btn-primary -> contentRole="primary" btn-default -> contentRole="default"
btn-secondary -> contentRole="secondary" btn-transparent -> contentRole="ghost"
btn-link -> contentRole="link" btn-danger/info/success/warning -> state="…"
btn-small/btn-large -> scale="s"/"l" extra classes -> pass as class="…"
JS-coupled buttons (.dropdown-toggle) are migrated in the dropdown phase, not here.
--}}
Comment on lines +63 to +75
## No-op mapping principle (worked example: button)

The canonical vocabulary maps to **today's** classes so output is unchanged:

| canonical | renders today | (at design time →) |
|---|---|---|
| `contentRole="primary"` | `btn btn-primary` | `dui-btn dui-btn-primary` |
| `contentRole="secondary"` | `btn btn-secondary` | … |
| `contentRole="default"` | `btn btn-default` | … |
| `contentRole="tertiary"`/`ghost` | `btn btn-transparent` | … |
| `contentRole="link"` | `btn btn-link` | … |
| `state="danger"` | `btn btn-danger` | … |
| `scale="s"` / `scale="l"` | `btn btn-small` / `btn btn-large` | … |
Comment on lines +65 to 66
<x-global::forms.button inputType="button" contentRole="secondary"
onclick="leantime.kanbanController.toggleQuickAdd(this.closest('.quickaddContainer').querySelector('.quickAddLink'))">
onclick="leantime.commentsController.toggleCommentBoxes(-1);jQuery('.noCommentsMessage').toggle();"
class="tw-leading-[50px]"
>{{ __('links.cancel') }}</a>
contentRole="secondary"
<strong style="margin-bottom:5px; display:block; color:var(--main-titles-color);">Direct Sponsorship through Github</strong>
Fund open source development that benefits everyone<br /><br />
<a href="https://github.com/sponsors/Leantime" class="btn btn-primary" target="_blank" style="background:var(--main-titles-color); color:var(--accent1);">Sponsor Leantime</a>
<x-global::forms.button tag="a" link="https://github.com/sponsors/Leantime" contentRole="primary" target="_blank" style="background:var(--main-titles-color); color:var(--accent1);">Sponsor Leantime</x-global::forms.button>
<strong style="margin-bottom:5px; display:block; color:var(--main-titles-color);">Purchase Plugins</strong>
Get advanced features while supporting development<br /><br />
<a href="{{ BASE_URL }}/plugins/marketplace" class="btn btn-primary" style="background:var(--main-titles-color); color:var(--accent1);" target="_blank">Browse Marketplace</a>
<x-global::forms.button tag="a" link="{{ BASE_URL }}/plugins/marketplace" contentRole="primary" style="background:var(--main-titles-color); color:var(--accent1);" target="_blank">Browse Marketplace</x-global::forms.button>
marcelfolaron added a commit that referenced this pull request Jun 20, 2026
…ts (56 files) (#3558)

* feat(components): add no-op forms.text-input component + tracker scope

Next primitive after the merged button work (#3531), same playbook. Adds a thin no-op
<x-global::forms.text-input>: renders a plain <input> with today's class (default = none) and
passes all attributes through. variant maps the styled cases (headline -> main-title-input,
form -> form-control, large, legacy); type via the inputType prop. The design-system IDL props
(labelText/caption/validation*/leadingVisual) are declared for the contract but NOT rendered in
no-op mode (a label wrapper would change markup — that's the design phase).

COMPONENTS.md: button -> done (merged #3531); text-input -> in progress; records the migration
scope + the JS-coupled do-not-touch rubric (datepickers/time/tags/inline-edit/color) so the
migration never breaks a datepicker.

No call-sites migrated yet — pilot + batched migration follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(components): pilot forms.text-input on newProject headline

First call-site migration: the project-name title input (main-title-input)
becomes <x-global::forms.text-input variant="headline">. Renders byte-identical
(verified via Playwright: same class/type/name/id/style/value/placeholder).

The two .dateFrom/.dateTo datepickers are deliberately left RAW per the
do-not-touch rubric — the component is never applied to JS-coupled inputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(components): migrate 146 inputs to no-op forms.text-input (56 files)

No-op sweep of plain <input> tags to <x-global::forms.text-input> across 56
templates. Renders byte-identical: source class -> variant (main-title-input->
headline, form-control->form, input-large/small, legacy input), type -> inputType,
all other attributes pass through; extra non-variant classes (tw-utilities,
pull-left) pass through class=.

Method: 63-file two-phase workflow (per-file migrate + adversarial diff-verify,
all 63 ok). Verified independently: diff perfectly symmetric (202 ins/202 del =
in-place swaps); static audit of all 146 call-sites = 0 problems (no type/inputType
dup, no variant class left in class=, no JS-coupled signal swallowed, no nested
double-quote, no duplicate attrs); view:cache + Pint clean; live render no-op on
/setting/editCompanySettings (pull-left) and /clients/newClient (bare).

Deliberately left RAW (do-not-touch rubric in COMPONENTS.md):
- datepickers, tags, inline-edit (.secretInput/.asyncInputUpdate), color,
  .hourCell/.sorter grids, dynamic class/id, inline on* handlers
- Auth/userInvite (3 inputs use legacy <?php echo ?> in attrs — new gotcha)
- Tickets/partials/ticketCard + partials/subtasks (HTMX inline-edit/date contexts)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(components): forms.text-input prop type (HTML-native) + drop redundant variant=form

Review feedback on PR #3558.

1) inputType -> type: the prop is now the HTML-native `type` (17 call-sites). It is
   a declared @prop so Blade extracts it from the attribute bag — emits exactly one
   type, no duplication. (forms.button keeps inputType: it is polymorphic and `type`
   is ambiguous across a/button/input.)

2) Drop variant="form": a 3-agent CSS audit showed .form-control is cosmetically
   redundant in Leantime — forms.css element selectors (input[type=text]..., loaded
   after Bootstrap) override its bg/border/radius/shadow/padding/height/color, and the
   only residual effect (desktop width:100%) is already supplied by container rules
   (.regpanelinner input{width:100%}) for the only 7 call-sites (login x2, twoFA/verify,
   install x4 — all entry pages). No JS hooks .form-control on inputs. Collapsed those
   7 to bare; bare IS the form look. Live render on /auth/login confirms bare inputs,
   single type, no form-control.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(components): drop redundant variant=legacy from forms.text-input

Review feedback. A 4-agent CSS audit confirmed .input (the 'legacy' variant's
class) has NO backing CSS rule anywhere — forms.css styles inputs by element
selector, so <input class="input"> is pixel-identical to a bare input. Same
verdict as form-control. Dropped the 'legacy' => 'input' arm and converted the
one call-site (TwoFA/edit twoFACode) to bare.

Surviving variants are all evidence-backed visually-distinct treatments:
  headline -> .main-title-input (large xxxl title font, shadow removed)
  large    -> .input-large      (fixed 210px width — forms.css never sets width)
  small    -> .input-small      (fixed 90px width)
Ghost (.secretInput inline-edit) documented as a planned variant, pending its
async-save JS migration. Taxonomy recorded in COMPONENTS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(components): address Copilot review on forms.text-input PR

- Goalcanvas/canvasDialog: remove a stray, unmatched </textarea> after the
  newMilestone input (pre-existing invalid markup the migration carried over;
  browsers already ignored the orphan end tag, so removal is a no-op). The bogus
  width="50%" is left as-is (inert on a text input; changing it would alter width).
- forms.text-input: drop the undocumented 'title' alias from the variant match —
  no call-site uses variant="title"; only 'headline' remains for .main-title-input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
marcelfolaron added a commit that referenced this pull request Jun 21, 2026
…ent (#3560)

* feat(components): add no-op forms.text-input component + tracker scope

Next primitive after the merged button work (#3531), same playbook. Adds a thin no-op
<x-global::forms.text-input>: renders a plain <input> with today's class (default = none) and
passes all attributes through. variant maps the styled cases (headline -> main-title-input,
form -> form-control, large, legacy); type via the inputType prop. The design-system IDL props
(labelText/caption/validation*/leadingVisual) are declared for the contract but NOT rendered in
no-op mode (a label wrapper would change markup — that's the design phase).

COMPONENTS.md: button -> done (merged #3531); text-input -> in progress; records the migration
scope + the JS-coupled do-not-touch rubric (datepickers/time/tags/inline-edit/color) so the
migration never breaks a datepicker.

No call-sites migrated yet — pilot + batched migration follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(components): pilot forms.text-input on newProject headline

First call-site migration: the project-name title input (main-title-input)
becomes <x-global::forms.text-input variant="headline">. Renders byte-identical
(verified via Playwright: same class/type/name/id/style/value/placeholder).

The two .dateFrom/.dateTo datepickers are deliberately left RAW per the
do-not-touch rubric — the component is never applied to JS-coupled inputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(components): migrate 146 inputs to no-op forms.text-input (56 files)

No-op sweep of plain <input> tags to <x-global::forms.text-input> across 56
templates. Renders byte-identical: source class -> variant (main-title-input->
headline, form-control->form, input-large/small, legacy input), type -> inputType,
all other attributes pass through; extra non-variant classes (tw-utilities,
pull-left) pass through class=.

Method: 63-file two-phase workflow (per-file migrate + adversarial diff-verify,
all 63 ok). Verified independently: diff perfectly symmetric (202 ins/202 del =
in-place swaps); static audit of all 146 call-sites = 0 problems (no type/inputType
dup, no variant class left in class=, no JS-coupled signal swallowed, no nested
double-quote, no duplicate attrs); view:cache + Pint clean; live render no-op on
/setting/editCompanySettings (pull-left) and /clients/newClient (bare).

Deliberately left RAW (do-not-touch rubric in COMPONENTS.md):
- datepickers, tags, inline-edit (.secretInput/.asyncInputUpdate), color,
  .hourCell/.sorter grids, dynamic class/id, inline on* handlers
- Auth/userInvite (3 inputs use legacy <?php echo ?> in attrs — new gotcha)
- Tickets/partials/ticketCard + partials/subtasks (HTMX inline-edit/date contexts)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(components): forms.text-input prop type (HTML-native) + drop redundant variant=form

Review feedback on PR #3558.

1) inputType -> type: the prop is now the HTML-native `type` (17 call-sites). It is
   a declared @prop so Blade extracts it from the attribute bag — emits exactly one
   type, no duplication. (forms.button keeps inputType: it is polymorphic and `type`
   is ambiguous across a/button/input.)

2) Drop variant="form": a 3-agent CSS audit showed .form-control is cosmetically
   redundant in Leantime — forms.css element selectors (input[type=text]..., loaded
   after Bootstrap) override its bg/border/radius/shadow/padding/height/color, and the
   only residual effect (desktop width:100%) is already supplied by container rules
   (.regpanelinner input{width:100%}) for the only 7 call-sites (login x2, twoFA/verify,
   install x4 — all entry pages). No JS hooks .form-control on inputs. Collapsed those
   7 to bare; bare IS the form look. Live render on /auth/login confirms bare inputs,
   single type, no form-control.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(components): drop redundant variant=legacy from forms.text-input

Review feedback. A 4-agent CSS audit confirmed .input (the 'legacy' variant's
class) has NO backing CSS rule anywhere — forms.css styles inputs by element
selector, so <input class="input"> is pixel-identical to a bare input. Same
verdict as form-control. Dropped the 'legacy' => 'input' arm and converted the
one call-site (TwoFA/edit twoFACode) to bare.

Surviving variants are all evidence-backed visually-distinct treatments:
  headline -> .main-title-input (large xxxl title font, shadow removed)
  large    -> .input-large      (fixed 210px width — forms.css never sets width)
  small    -> .input-small      (fixed 90px width)
Ghost (.secretInput inline-edit) documented as a planned variant, pending its
async-save JS migration. Taxonomy recorded in COMPONENTS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(core): extract shared AI/MCP support classes and add PAT management

Move entity formatters, LLM string sanitizer, and markdown helper from
McpServer and Copilot plugins into Core and domain modules so both
plugins (and future AI integrations) share a single implementation.

- Add LLMStringSanitizer and MarkdownHelper to Core/Support (delegate
  to existing Str macros)
- Add EntityFormatterInterface, AbstractEntityFormatter, and
  RoutingFormatter to Core/Support
- Add TicketFormatter to Domain/Tickets/Support
- Add CalendarEventFormatter to Domain/Calendar/Support
- Move Personal Access Token management UI from AdvancedAuth plugin
  to Core (Auth domain) so MCP works without AdvancedAuth installed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(components): address Copilot review on forms.text-input PR

- Goalcanvas/canvasDialog: remove a stray, unmatched </textarea> after the
  newMilestone input (pre-existing invalid markup the migration carried over;
  browsers already ignored the orphan end tag, so removal is a no-op). The bogus
  width="50%" is left as-is (inert on a text input; changing it would alter width).
- forms.text-input: drop the undocumented 'title' alias from the variant match —
  no call-site uses variant="title"; only 'headline' remains for .main-title-input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback and phpstan failure

- PersonalTokens: use $this->tpl->emptyResponse() instead of
  $this->emptyResponse() (not on HtmxController, only on Template)
- LLMStringSanitizer: cast non-string input to string instead of
  returning mixed type from string-typed method
- MarkdownHelper: drop unused $headerLevel param from Str::toMarkdown()
  call (macro closure only accepts data argument)
- AbstractEntityFormatter: guard json_encode returning false
- CalendarEventFormatter: fix fieldPriority to match prepared data keys
  (startDate/endDate not dateFrom/dateTo)
- TicketFormatter: sanitize subtask headlines for LLM safety

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tests): add waitForElementVisible before login form interaction

The loginDeniedForWrongCredentials acceptance test was failing because
Selenium tried to fillField before the DOM was fully ready. The HTML
artifact confirms the form renders correctly -- this is a race condition.
Add waitForElementVisible('#login') to ensure the form is present.

This fixes a pre-existing flaky test also failing on master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
marcelfolaron added a commit that referenced this pull request Jun 21, 2026
Third primitive after button (#3531) and text-input (#3558). A no-op textarea
component plus migration of the 10 plain <textarea> tags to it.

Component body is <textarea {{ $attributes }}>{{ $slot }}</textarea> — attributes
pass through and the field value is the slot (inner content), preserved exactly
(textareas are whitespace-sensitive). No variant arm: plain textareas carry no
distinct style class.

DEFERRED (left RAW) — the 19 Tiptap rich-text editor textareas. JS upgrades exactly
textarea.tiptapSimple / textarea.tiptapComplex (app/.../core/tiptap/index.js) plus the
Wiki .wiki-editor-textarea; routing those through the component would break the editors.

Migrated 10 plain textareas across 6 files (Help projectDefinitionStep x3, Ideas/Wiki
newMilestone, Timesheets add/edit + Tickets timesheet description, Widgets myToDos x2).

Verified: full diff is pure <textarea> <-> <x-global::forms.textarea> tag swaps
(14 ins / 14 del symmetric), view:cache + Pint clean, static audit = 0 plain missed,
19 editors deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
marcelfolaron added a commit that referenced this pull request Jun 21, 2026
…extareas (#3562)

* feat(components): add no-op forms.textarea + migrate 10 plain textareas

Third primitive after button (#3531) and text-input (#3558). A no-op textarea
component plus migration of the 10 plain <textarea> tags to it.

Component body is <textarea {{ $attributes }}>{{ $slot }}</textarea> — attributes
pass through and the field value is the slot (inner content), preserved exactly
(textareas are whitespace-sensitive). No variant arm: plain textareas carry no
distinct style class.

DEFERRED (left RAW) — the 19 Tiptap rich-text editor textareas. JS upgrades exactly
textarea.tiptapSimple / textarea.tiptapComplex (app/.../core/tiptap/index.js) plus the
Wiki .wiki-editor-textarea; routing those through the component would break the editors.

Migrated 10 plain textareas across 6 files (Help projectDefinitionStep x3, Ideas/Wiki
newMilestone, Timesheets add/edit + Tickets timesheet description, Widgets myToDos x2).

Verified: full diff is pure <textarea> <-> <x-global::forms.textarea> tag swaps
(14 ins / 14 del symmetric), view:cache + Pint clean, static audit = 0 plain missed,
19 editors deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(components): clarify forms.textarea attribute passthrough (Copilot review)

Declared @props (state/contentRole/scale/labelText/…) are extracted by Blade and not
emitted as HTML; only non-prop attributes pass through $attributes. Docblock-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
marcelfolaron added a commit that referenced this pull request Jun 21, 2026
…on (#3563)

* feat(components): migrate 44 class=button submit inputs to forms.button

Completes the deferred 'class=button' item from the button PR (#3531). The user
flagged a stray un-componentized submit button in timesheet.

CSS audit: .button has NO rule anywhere; input[type='submit'] is styled by the
.btn-primary element-selector group (forms.css:313), so these 44 submits already
render as primary buttons today. Migrating each to
<x-global::forms.button tag="input" inputType="submit" contentRole="primary"> is a
no-op (same primary look). Also removed a few pre-existing duplicate class="button"
class="button" attributes.

All 44 are input[type=submit] class="button" across 38 files. Verified: 0 class=button
remaining, diff is pure tag swaps (value -> :labelText, type -> inputType,
class -> contentRole=primary, other attrs pass through), view:cache + Pint clean,
audit of all tag=input buttons = 0 problems.

Follow-up: ~16 del* confirmation submits look primary today; candidates for
state="danger" in a later semantic pass (a visual change, not a no-op).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(timesheet): update button selectors after class=button migration

#3563 migrated the timesheet form submits from class="button" to the forms.button
component (renders btn btn-primary), so the acceptance selectors that clicked
'.button' could no longer find them — the actual regression behind the failing
acceptance run (NoSuchElementException on '.stdformbutton .button').

.button is a dead CSS class (no rules) used only as a test hook; no JS depends on
it. Repointed the 6 selectors at the stable submit elements:
- .stdformbutton .button            -> .stdformbutton input[type=submit]
- .formModal .button                -> .formModal input[type=submit][name=saveTimes]
- #allTimesheetsTable_wrapper .button -> #allTimesheetsTable_wrapper input[name=saveInvoice] (x3)
- .nyroModalLink .button            -> .nyroModalLink input[type=submit]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
marcelfolaron added a commit that referenced this pull request Jun 21, 2026
…lements) (#3564)

* feat(components): button + text-input completion (round 2) — 53 missed elements

Audit after #3531/#3558 found a tail of buttons/inputs that were missed or postdate
those PRs. Migrates the cleanly-mappable ones (no-op).

Root cause for the bulk: #3531 deferred 'unstyled <input type=submit> (no class)' as a
design change, but input[type='submit'] is in the .btn-primary element-selector group
(forms.css:313) — so bare submits already render primary, making contentRole=primary a no-op.

Migrated 53 across 38 files:
- 29 bare <input type=submit> -> forms.button primary
- 2 Auth token-UI text inputs (token-created, tokenNew) -> forms.text-input
- plain buttons/links: Errors back x4, support sponsor, Auth token UI (create/copy/close/delete),
  Files cancel x2, widgetManager reset (btn-outline->secondary), Reports chart toggles x6,
  showProject delete (btn-danger-outline -> state=danger variant=outline), 1 comment reply.
- btn-sm/btn-lg/btn-secondary (own CSS, != Leantime small/large/outline) passed through class=
  pending a design-phase scale/role mapping.

Deferred (correct): 3 comment btn-success role+state combos, partials/subtasks quickadd
(nested quote + HTMX), dynamic-class links, ticketFilter (intentional raw <a>), custom non-btn
widget buttons (Wiki/calendar/todoItem), modal data-dismiss/.close, file-upload picSubmit,
dropdown-toggles, <?php echo invite variants.

Verified: view:cache + Pint clean, 0 button-tag problems, diff is tag swaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(help): add rel=noopener noreferrer to sponsor link (Copilot review)

target=_blank link was missing rel=noopener noreferrer (reverse-tabnabbing). No
visual change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(components): soften 'no-op' wording for bare-submit migration (Copilot review)

Copilot correctly noted the bare <input type=submit> -> forms.button migration adds
class="btn", which pulls in the shared .btn base (input.btn { vertical-align: top; … })
that bare submits lacked. It's an intended VISUAL no-op (they already looked primary),
not byte-identical. Reworded the tracker accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants