Compare commits

..

2 Commits

Author SHA1 Message Date
4969fb3f6d fix(web): settings and bell use accessible checkboxes, not slide toggles
@pi/ui's slide toggle hides its input with display: none and gives it
no name: neither the Tab key nor a screen reader could reach the
switches in settings (visibility, the public's proposals, the
definition of done, the link for clients) or the bell's (e-mail copy,
notices on this device). They are pi-checkbox now, each with a name
saying what it turns on: reachable by keyboard, toggled with the space
bar and announced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:20:06 +02:00
e10c9b84bf refactor(web): settings switches are @pi/ui's slide toggle
Visibility, the public's proposals, the definition of done and the link
for clients each had a hand-made switch; they use pi-slide-toggle now,
like the bell's, with their state written beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:12:13 +02:00
6 changed files with 45 additions and 179 deletions

View File

@@ -291,7 +291,7 @@ visualización de la plataforma:
lectura, y en uno privado (o inexistente), el enlace para iniciar sesión volviendo a esa página (`returnUrl`). lectura, y en uno privado (o inexistente), el enlace para iniciar sesión volviendo a esa página (`returnUrl`).
- El tiempo real necesita sesión: `GraphqlRealtimeGateway` no abre subscriptions sin token (si lo hiciera, la API - El tiempo real necesita sesión: `GraphqlRealtimeGateway` no abre subscriptions sin token (si lo hiciera, la API
cerraría el WebSocket y el cliente reintentaría sin fin). Los visitantes anónimos ven los datos al cargar la página. cerraría el WebSocket y el cliente reintentaría sin fin). Los visitantes anónimos ven los datos al cargar la página.
- Configuración tiene en General el interruptor «Público» (propietario y admins) y, en Zona peligrosa y solo para el propietario, «Eliminar el - Configuración tiene en General la casilla «Proyecto público» (propietario y admins) y, en Zona peligrosa y solo para el propietario, «Eliminar el
proyecto», que exige escribir la clave. Al borrarlo se vuelve a la portada; si otro lo borra mientras lo tienes proyecto», que exige escribir la clave. Al borrarlo se vuelve a la portada; si otro lo borra mientras lo tienes
abierto, la subscription avisa (`ProjectChanged.deleted`) y también se vuelve a la portada. abierto, la subscription avisa (`ProjectChanged.deleted`) y también se vuelve a la portada.

Binary file not shown.

Binary file not shown.

View File

@@ -209,17 +209,13 @@ function idFrom(name: string): string {
} }
</p> </p>
</div> </div>
<label class="ts-switch"> <pi-checkbox
<input class="ts-toggle"
type="checkbox" label="Proyecto público"
role="switch"
[checked]="project.isPublic" [checked]="project.isPublic"
[disabled]="!facade.canManage()" [disabled]="!facade.canManage()"
(change)="facade.setPublic($any($event.target).checked)" (checkedChange)="facade.setPublic($event)"
/> />
<span class="ts-switch__track" aria-hidden="true"></span>
<span>{{ project.isPublic ? 'Público' : 'Privado' }}</span>
</label>
</section> </section>
<section <section
@@ -245,19 +241,13 @@ function idFrom(name: string): string {
} }
</p> </p>
</div> </div>
<label class="ts-switch"> <pi-checkbox
<input class="ts-toggle"
type="checkbox" label="Formulario abierto"
role="switch"
[checked]="project.isPublic && project.acceptsProposals" [checked]="project.isPublic && project.acceptsProposals"
[disabled]="!facade.canManage() || !project.isPublic" [disabled]="!facade.canManage() || !project.isPublic"
(change)="facade.setAcceptsProposals($any($event.target).checked)" (checkedChange)="facade.setAcceptsProposals($event)"
/> />
<span class="ts-switch__track" aria-hidden="true"></span>
<span>{{
project.isPublic && project.acceptsProposals ? 'Abierto' : 'Cerrado'
}}</span>
</label>
</section> </section>
@if (facade.canManage()) { @if (facade.canManage()) {
@@ -464,20 +454,12 @@ function idFrom(name: string): string {
medias. El propietario y los admins sí pueden cerrarlo. medias. El propietario y los admins sí pueden cerrarlo.
</p> </p>
</div> </div>
<label class="ts-switch"> <pi-checkbox
<input class="ts-toggle"
type="checkbox" label="Exigir la checklist completa"
role="switch"
[checked]="!!d.checklistBeforeDone" [checked]="!!d.checklistBeforeDone"
(change)=" (checkedChange)="setChecklistBeforeDone($event)"
setChecklistBeforeDone($any($event.target).checked)
"
/> />
<span class="ts-switch__track" aria-hidden="true"></span>
<span>{{
d.checklistBeforeDone ? 'Activada' : 'Desactivada'
}}</span>
</label>
</header> </header>
</section> </section>
@@ -1108,59 +1090,9 @@ function idFrom(name: string): string {
font-size: 0.8125rem; font-size: 0.8125rem;
} }
} }
.ts-switch { .ts-toggle {
display: inline-flex; flex: none;
align-items: center;
gap: 0.625rem;
font-size: 0.875rem;
font-weight: 600; font-weight: 600;
cursor: pointer;
input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
input:focus-visible + .ts-switch__track {
outline: 2px solid var(--ts-brand);
outline-offset: 2px;
}
input:disabled ~ * {
opacity: 0.6;
cursor: default;
}
}
.ts-switch__track {
position: relative;
width: 2.5rem;
height: 1.375rem;
border-radius: 999px;
background: var(--ts-line-strong);
transition: background-color 0.15s ease;
&::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 1rem;
height: 1rem;
border-radius: 50%;
background: var(--ts-surface);
box-shadow: var(--ts-shadow);
transition: transform 0.15s ease;
}
}
.ts-switch input:checked + .ts-switch__track {
background: var(--ts-brand);
&::after {
transform: translateX(1.125rem);
}
}
.ts-danger {
border: 1px solid color-mix(in srgb, var(--ts-c-red) 45%, transparent);
h3 {
color: var(--ts-c-red);
}
} }
.ts-danger__confirm { .ts-danger__confirm {
display: flex; display: flex;
@@ -1453,12 +1385,6 @@ function idFrom(name: string): string {
display: grid; display: grid;
place-items: center; place-items: center;
} }
input[type='checkbox'] {
width: 1rem;
height: 1rem;
accent-color: var(--ts-brand);
cursor: pointer;
}
.ts-type-icon { .ts-type-icon {
display: inline-grid; display: inline-grid;
place-items: center; place-items: center;

View File

@@ -49,17 +49,13 @@ const DEFAULT_BLOCKS: SharedBlock[] = ['flow', 'sprint', 'releases'];
enseña personas ni tickets sueltos (salvo las épicas). enseña personas ni tickets sueltos (salvo las épicas).
</p> </p>
</div> </div>
<label class="ts-switch"> <pi-checkbox
<input class="ts-toggle"
type="checkbox" label="Compartir con clientes"
role="switch"
[checked]="!!sharing.share()" [checked]="!!sharing.share()"
[disabled]="!sharing.loaded() || sharing.busy()" [disabled]="!sharing.loaded() || sharing.busy()"
(change)="toggle($any($event.target).checked)" (checkedChange)="toggle($event)"
/> />
<span class="ts-switch__track" aria-hidden="true"></span>
<span>{{ sharing.share() ? 'Compartido' : 'Sin compartir' }}</span>
</label>
</div> </div>
@if (sharing.share(); as share) { @if (sharing.share(); as share) {
@@ -186,53 +182,9 @@ const DEFAULT_BLOCKS: SharedBlock[] = ['flow', 'sprint', 'releases'];
color: var(--ts-c-red); color: var(--ts-c-red);
font-size: 0.875rem; font-size: 0.875rem;
} }
.ts-switch { .ts-toggle {
display: inline-flex; flex: none;
align-items: center;
gap: 0.625rem;
font-size: 0.875rem;
font-weight: 600; font-weight: 600;
cursor: pointer;
input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
input:focus-visible + .ts-switch__track {
outline: 2px solid var(--ts-brand);
outline-offset: 2px;
}
input:disabled ~ * {
opacity: 0.6;
cursor: default;
}
}
.ts-switch__track {
position: relative;
width: 2.5rem;
height: 1.375rem;
border-radius: 999px;
background: var(--ts-line-strong);
transition: background-color 0.15s ease;
&::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 1rem;
height: 1rem;
border-radius: 50%;
background: var(--ts-surface);
box-shadow: var(--ts-shadow);
transition: transform 0.15s ease;
}
}
.ts-switch input:checked + .ts-switch__track {
background: var(--ts-brand);
&::after {
transform: translateX(1.125rem);
}
} }
`, `,
}) })

View File

@@ -11,7 +11,7 @@ import {
PiBadgeComponent, PiBadgeComponent,
PiIconButtonComponent, PiIconButtonComponent,
PiIconComponent, PiIconComponent,
PiSlideToggleComponent, PiCheckboxComponent,
} from '@pi/ui'; } from '@pi/ui';
import { DrawerService } from '@task-stream/web/shared/ui'; import { DrawerService } from '@task-stream/web/shared/ui';
import { InboxFacade, PushFacade } from '@task-stream/web/tracker/application'; import { InboxFacade, PushFacade } from '@task-stream/web/tracker/application';
@@ -46,7 +46,7 @@ const ICON: Record<NotificationKind, string> = {
PiBadgeComponent, PiBadgeComponent,
PiIconButtonComponent, PiIconButtonComponent,
PiIconComponent, PiIconComponent,
PiSlideToggleComponent, PiCheckboxComponent,
], ],
template: ` template: `
<pi-badge <pi-badge
@@ -81,25 +81,19 @@ const ICON: Record<NotificationKind, string> = {
(execute)="inbox.markRead()" (execute)="inbox.markRead()"
/> />
@if (inbox.settings()?.emailAvailable) { @if (inbox.settings()?.emailAvailable) {
<div class="ts-inbox__switch"> <pi-checkbox
<pi-slide-toggle
label="Copia por correo" label="Copia por correo"
[checked]="inbox.settings()?.email ?? false" [checked]="inbox.settings()?.email ?? false"
(checkedChange)="inbox.setEmail($event)" (checkedChange)="inbox.setEmail($event)"
/> />
<span aria-hidden="true">Copia por correo</span>
</div>
} }
@if (inbox.settings()?.pushPublicKey && push.supported) { @if (inbox.settings()?.pushPublicKey && push.supported) {
<div class="ts-inbox__switch"> <pi-checkbox
<pi-slide-toggle
label="Avisos en este dispositivo" label="Avisos en este dispositivo"
[checked]="push.on()" [checked]="push.on()"
[disabled]="push.busy()" [disabled]="push.busy()"
(checkedChange)="togglePush($event)" (checkedChange)="togglePush($event)"
/> />
<span aria-hidden="true">Avisos en este dispositivo</span>
</div>
} }
</div> </div>
@if (push.error(); as error) { @if (push.error(); as error) {
@@ -156,12 +150,6 @@ const ICON: Record<NotificationKind, string> = {
justify-content: space-between; justify-content: space-between;
gap: 0.75rem; gap: 0.75rem;
} }
.ts-inbox__switch {
display: inline-flex;
gap: 0.5rem;
align-items: center;
font-size: 0.875rem;
}
.ts-inbox__error { .ts-inbox__error {
margin: 0; margin: 0;
color: var(--ts-c-red, var(--ts-ink)); color: var(--ts-c-red, var(--ts-ink));