feat(components): forms.button — migrate all buttons + establish the role model#3531
Conversation
…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> |
There was a problem hiding this comment.
I'd argue that a back button is secondary or just tertiary
…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>
There was a problem hiding this comment.
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.mdas a living tracker/playbook defining taxonomy, prop vocabulary (IDL), and migration process. - Added
app/Views/Templates/components/forms/button.blade.phpimplementing a canonical button API mapped to today’sbtn*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.
| if ($variant === 'outline') { | ||
| $colorClass = $state !== '' ? 'btn-'.$state.'-outline' : 'btn-outline'; | ||
| } else { | ||
| $colorClass = $stateClass !== '' ? $stateClass : $roleClass; | ||
| } |
| > **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). |
| | Prop | Options | Default | Notes | | ||
| |---|---|---|---| | ||
| | `contentRole` | default · primary · secondary · tertiary(=ghost) · accent · link | primary (actions) | semantic role | | ||
| | `state` | default · info · warning · danger · success | default | | |
| - _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> |
| <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>
|
Thanks for the review — addressed all of these (commit Role model (resolves the "back should be secondary/tertiary" + most "not a no-op" comments)
So e.g. the 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.
COMPONENTS.md — owner changed from "Claude" to the frontend/UI maintainers; and the IDL table now notes
|
…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>
| {{-- | ||
| 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. | ||
| --}} |
| ## 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` | … | |
| <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> |
…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>
…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>
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>
…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>
…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>
…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>
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)
forms.button(full prop IDL:contentRole/state/scale/variant/tag/leadingVisual…) +COMPONENTS.md(the living tracker/playbook).secondaryrenders outline (role carries its look);tertiary/ghost = transparent.tertiary· Save & Close →secondary(outline) · Delete/Remove →state="danger"outline (low-chrome) · main actions stayprimary.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-primaryis now correctly low-emphasis). The earlier "no-op only / no migrations yet" wording is superseded by this description.Verification
view:cachecompiles 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