Implement accessibility plan: Sprints 5, 6, and 7 (WCAG 2.1 AA)

Sprint 5 – Critical fixes:
- 5-A: Add sr-only label + aria-label to search input and clear/submit buttons
- 5-B: Add role/aria-live/aria-atomic to flash messages; dismiss button label
- 5-C: Add role=dialog aria-modal aria-labelledby to _html_modal
- 5-D: Change file-dropzone inputs from display:none to sr-only; add region/label to dropzone element
- 5-E: Convert all <a href="#"> acting as buttons to <button type="button"> in signature_step, initials_step, phone_step

Sprint 6 – High priority:
- 6-A: Wrap pagination in <nav aria-label>; add aria-current="page" to current page
- 6-B: Wrap settings nav in <nav aria-label>; add aria-labels to icon-only social links
- 6-B/7-J: menu_active.js sets aria-current="page" on active links
- 6-C/D: Progress dots → <button> with aria-label/aria-current; form container gets aria-hidden; expand button gets aria-expanded/aria-controls
- 6-E: Add aria-label to folder card link
- 6-F: Wrap breadcrumb back-link in <nav aria-label="Breadcrumb">
- 6-G: scroll_to.js adds keydown (Enter/Space) handler and focuses target after scroll
- 6-H: Add aria-label to template builder option remove (×) button
- 6-I: fetch_form.js announces success via announcePolite when data-success-message set
- 6-J: Convert turbo_drawer close anchors to <button> with aria-label

Sprint 7 – Medium priority:
- 7-A: aria-errormessage + aria-invalid on signature/initials canvas linked to error divs
- 7-B: aria-busy on payment step processing/checkout buttons
- 7-D: Add id/aria-label/aria-controls to API settings DaisyUI collapse checkboxes
- 7-E: Add table caption, scope="col" on headers, sr-only Actions column header
- 7-F: Wrap SMTP security radio buttons in <fieldset><legend>
- 7-G: Add aria-pressed to dashboard toggle view buttons
- 7-I: Add aria-label="Country code" to phone step select
- i18n: Add dismiss, step, form_progress, breadcrumb, actions keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pull/599/head
Marcelo Paiva 3 weeks ago
parent 76f0fa7426
commit cf20940023

@ -1,4 +1,4 @@
import { announceError } from './aria_announce'
import { announceError, announcePolite } from './aria_announce'
export default class extends HTMLElement {
connectedCallback () {
@ -28,6 +28,8 @@ export default class extends HTMLElement {
} catch (err) {
console.error(err)
}
} else if (this.dataset.successMessage) {
announcePolite(this.dataset.successMessage)
}
})
}

@ -10,6 +10,15 @@ export default actionable(targetable(class extends HTMLElement {
]
connectedCallback () {
this.setAttribute('role', 'region')
if (!this.hasAttribute('aria-label')) {
this.setAttribute('aria-label', this.dataset.name || 'File upload')
}
if (this.input && !this.input.getAttribute('aria-label')) {
this.input.setAttribute('aria-label', this.dataset.name || 'Upload file')
}
this.addEventListener('dragover', (e) => e.preventDefault())
this.addEventListener('drop', this.onDrop)
document.addEventListener('turbo:submit-end', this.toggleLoading)

@ -3,6 +3,7 @@ export default class extends HTMLElement {
this.querySelectorAll('a').forEach((link) => {
if (document.location.pathname.startsWith(link.pathname) && !link.getAttribute('href').startsWith('http')) {
link.classList.add('bg-base-300')
link.setAttribute('aria-current', 'page')
}
})
}

@ -2,9 +2,23 @@ export default class extends HTMLElement {
connectedCallback () {
this.selector = document.getElementById(this.dataset.selectorId)
this.addEventListener('click', () => {
const scrollToTarget = () => {
this.selector.scrollIntoView({ behavior: 'smooth', block: 'start' })
history.replaceState(null, null, `#${this.dataset.selectorId}`)
if (this.selector.tabIndex < 0) {
this.selector.tabIndex = -1
}
this.selector.focus({ preventScroll: true })
}
this.addEventListener('click', scrollToTarget)
this.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
scrollToTarget()
}
})
}
}

@ -69,7 +69,9 @@
id="expand_form_button"
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
style="width: 96%; margin-left: 2%"
@click.prevent="[isFormVisible = true, $nextTick(() => scrollIntoField(currentField))]"
aria-expanded="false"
aria-controls="form_container"
@click="[isFormVisible = true, $nextTick(() => scrollIntoField(currentField))]"
>
<template v-if="['initials', 'signature'].includes(currentField.type)">
<IconWritingSign stroke-width="1.5" />
@ -96,6 +98,7 @@
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
:class="{ 'md:bottom-4': isBreakpointMd }"
:style="{ backgroundColor: backgroundColor }"
:aria-hidden="!isFormVisible"
>
<button
v-if="!isCompleted"
@ -553,14 +556,20 @@
v-if="stepFields.length < 80"
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
>
<div class="flex items-center flex-wrap steps-progress">
<a
<div
role="group"
:aria-label="t('form_progress')"
class="flex items-center flex-wrap steps-progress"
>
<button
v-for="(step, index) in stepFields"
:key="step[0].uuid"
href="#"
type="button"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
:aria-label="`${t('step')} ${index + 1} ${t('of')} ${stepFields.length}`"
:aria-current="index === currentStep ? 'step' : undefined"
@click="isCompleted ? undefined : [saveStep(), goToStep(index, true)]"
/>
</div>
</div>

@ -102,7 +102,11 @@ const en = {
initials_drawing_pad: 'Initials drawing pad. Use the tools above to draw or type your initials.',
qr_code_for_mobile_signature: 'QR code for signing on a mobile device.',
show_qr_code: 'Show QR code for mobile signing',
close_qr_code: 'Close QR code'
close_qr_code: 'Close QR code',
step: 'Step',
of: 'of',
form_progress: 'Form progress',
country_code: 'Country code'
}
const es = {

@ -22,34 +22,34 @@
class="md:tooltip"
:data-tip="t('type_initial')"
>
<a
<button
id="type_text_button"
href="#"
type="button"
class="btn btn-outline font-medium btn-sm type-text-button"
@click.prevent="toggleTextInput"
@click="toggleTextInput"
>
<IconTextSize :width="16" />
<span class="hidden sm:inline">
{{ t('type') }}
</span>
</a>
</button>
</span>
<span
v-else
class="md:tooltip ml-2"
:data-tip="t('draw_initials')"
>
<a
<button
id="type_text_button"
href="#"
type="button"
class="btn btn-outline font-medium btn-sm type-text-button"
@click.prevent="toggleTextInput"
@click="toggleTextInput"
>
<IconSignature :width="16" />
<span class="hidden sm:inline">
{{ t('draw') }}
</span>
</a>
</button>
</span>
<span
class="md:tooltip"
@ -69,36 +69,36 @@
</span>
</label>
</span>
<a
<button
v-if="modelValue || computedPreviousValue"
href="#"
type="button"
class="btn font-medium btn-outline btn-sm clear-canvas-button"
@click.prevent="remove"
@click="remove"
>
<IconReload :width="16" />
{{ t('clear') }}
</a>
<a
</button>
<button
v-else
href="#"
type="button"
class="btn font-medium btn-outline btn-sm clear-canvas-button"
@click.prevent="clear"
@click="clear"
>
<IconReload :width="16" />
{{ t('clear') }}
</a>
<a
</button>
<button
type="button"
:title="t('minimize')"
:aria-label="t('minimize')"
href="#"
class="py-1.5 inline md:hidden"
@click.prevent="$emit('minimize')"
@click="$emit('minimize')"
>
<IconArrowsDiagonalMinimize2
:width="20"
:height="20"
/>
</a>
</button>
</div>
</div>
<div
@ -135,6 +135,8 @@
ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
:aria-label="t('initials_drawing_pad')"
:aria-invalid="initialsError ? 'true' : undefined"
:aria-errormessage="initialsError ? 'initials-error' : undefined"
>{{ t('initials_drawing_pad') }}</canvas>
</div>
<input
@ -150,6 +152,7 @@
>
<div
v-if="initialsError"
id="initials-error"
role="alert"
aria-live="assertive"
class="text-error text-sm mt-2 px-1"

@ -34,11 +34,13 @@
<button
v-if="sessionId"
disabled
aria-busy="true"
class="base-button w-full modal-save-button"
>
<IconLoader
width="22"
class="animate-spin"
aria-hidden="true"
/>
<span>
{{ t('processing') }}...
@ -50,12 +52,14 @@
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
:class="{ disabled: isCreatingCheckout }"
:disabled="isCreatingCheckout"
:aria-busy="isCreatingCheckout"
@click.prevent="postCheckout"
>
<IconInnerShadowTop
v-if="isCreatingCheckout"
width="22"
class="animate-spin"
aria-hidden="true"
/>
<IconBrandStripe
v-else

@ -42,28 +42,28 @@
@input="onInputCode"
>
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
<a
<button
v-if="!defaultValue"
href="#"
type="button"
class="link change-phone-number-link"
@click.prevent="isCodeSent = false"
@click="isCodeSent = false"
>
{{ t('change_phone_number') }}
</a>
</button>
<span
v-if="resendCodeCountdown > 0"
class="link"
>
{{ t('wait_countdown_seconds').replace('{countdown}', resendCodeCountdown) }}
</span>
<a
<button
v-else
href="#"
type="button"
class="link resend-code-link"
@click.prevent="resendCode"
@click="resendCode"
>
{{ isResendLoading ? t('sending') : t('resend_code') }}
</a>
</button>
</div>
</div>
<div
@ -80,6 +80,7 @@
<select
id="country_code_select"
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:aria-label="t('country_code')"
:disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
>

@ -25,17 +25,17 @@
class="md:tooltip"
:data-tip="t('draw_signature')"
>
<a
<button
id="type_text_button"
href="#"
type="button"
class="btn btn-outline btn-sm font-medium type-text-button"
@click.prevent="[toggleTextInput(), hideQr()]"
@click="[toggleTextInput(), hideQr()]"
>
<IconSignature :width="16" />
<span class="hidden sm:inline">
{{ t('draw') }}
</span>
</a>
</button>
</span>
<span
v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
@ -43,17 +43,17 @@
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('type_text')"
>
<a
<button
id="type_text_button"
href="#"
type="button"
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap type-text-button"
@click.prevent="[toggleTextInput(), hideQr()]"
@click="[toggleTextInput(), hideQr()]"
>
<IconTextSize :width="16" />
<span class="hidden sm:inline">
{{ t('type') }}
</span>
</a>
</button>
</span>
<span
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
@ -75,47 +75,47 @@
</span>
</label>
</span>
<a
<button
v-if="modelValue || computedPreviousValue"
href="#"
type="button"
class="btn btn-outline btn-sm font-medium reupload-button"
@click.prevent="remove"
@click="remove"
>
<IconReload :width="16" />
{{ t(format === 'upload' ? 'reupload' : 'redraw') }}
</a>
</button>
<span
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
class="md:tooltip before:translate-x-[-90%]"
:data-tip="t('sign_on_the_touchscreen')"
>
<a
href="#"
<button
type="button"
class="btn btn-sm btn-neutral font-medium hidden md:flex"
:class="{ 'btn-outline': !isShowQr, 'text-white': isShowQr }"
:aria-label="isShowQr ? t('close_qr_code') : t('show_qr_code')"
:aria-pressed="isShowQr ? 'true' : 'false'"
@click.prevent="isShowQr ? hideQr() : [isTextSignature = false, showQr()]"
@click="isShowQr ? hideQr() : [isTextSignature = false, showQr()]"
>
<IconQrcode
:width="19"
:height="19"
/>
</a>
</button>
</span>
<a
href="#"
<button
type="button"
:title="t('minimize')"
:aria-label="t('minimize')"
class="py-1.5 inline md:hidden"
@click.prevent="$emit('minimize')"
@click="$emit('minimize')"
>
<IconArrowsDiagonalMinimize2
aria-hidden="true"
:width="20"
:height="20"
/>
</a>
</button>
</div>
</div>
<div
@ -159,14 +159,14 @@
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
class="absolute top-0.5 right-0.5"
>
<a
href="#"
<button
type="button"
class="btn btn-ghost font-medium btn-xs md:btn-sm"
@click.prevent="[clear(), hideQr()]"
@click="[clear(), hideQr()]"
>
<IconReload :width="16" />
{{ t('clear') }}
</a>
</button>
</div>
<div
v-if="isTextSignature"
@ -178,6 +178,8 @@
style="padding: 1px; 0"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
:aria-label="t('signature_drawing_pad')"
:aria-invalid="signatureError ? 'true' : undefined"
:aria-errormessage="signatureError ? 'signature-error' : undefined"
>{{ t('signature_drawing_pad') }}</canvas>
<div
v-if="isShowQr"
@ -188,14 +190,14 @@
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
>
<div class="absolute top-1.5 right-1.5">
<a
href="#"
<button
type="button"
class="btn btn-sm btn-circle btn-normal btn-outline"
:aria-label="t('close_qr_code')"
@click.prevent="hideQr"
@click="hideQr"
>
<IconX />
</a>
</button>
</div>
<div class="flex items-center justify-center w-full h-full p-4">
<div
@ -315,6 +317,7 @@
/>
<div
v-if="signatureError"
id="signature-error"
role="alert"
aria-live="assertive"
class="text-error text-sm mt-2 px-1"

@ -230,6 +230,7 @@
v-if="editable && !defaultField"
class="text-sm w-3.5"
tabindex="-1"
:aria-label="`${t('remove_option')} ${index + 1}`"
@click="removeOption(option)"
>
&times;

@ -104,6 +104,7 @@ const en = {
copy_to_all_pages: 'Copy to all pages',
more: 'More',
add_option: 'Add option',
remove_option: 'Remove option',
option: 'Option',
options: 'Options',
condition: 'Condition',

@ -33,8 +33,8 @@
</div>
<div class="space-y-4 mt-4">
<div class="collapse collapse-plus bg-base-200 px-1">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<input type="checkbox" id="api-collapse-1" aria-label="<%= t('request_signature_multiple_submitters_with_default_values') %>" aria-controls="api-collapse-1-content">
<div id="api-collapse-1-content" class="collapse-title text-xl font-medium">
<div>
<%= t('request_signature_multiple_submitters_with_default_values') %>
</div>
@ -69,8 +69,8 @@
</div>
</div>
<div class="collapse collapse-plus bg-base-200 px-1">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<input type="checkbox" id="api-collapse-2" aria-label="<%= t('request_signature_single_submitter') %>" aria-controls="api-collapse-2-content">
<div id="api-collapse-2-content" class="collapse-title text-xl font-medium">
<div>
<%= t('request_signature_single_submitter') %>
</div>
@ -95,8 +95,8 @@
</div>
</div>
<div class="collapse collapse-plus bg-base-200 px-1">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<input type="checkbox" id="api-collapse-3" aria-label="<%= t('template_details') %>" aria-controls="api-collapse-3-content">
<div id="api-collapse-3-content" class="collapse-title text-xl font-medium">
<div>
<%= t('template_details') %>
</div>

@ -1,12 +1,12 @@
<form action="<%= root_path %>" method="get" id="templates_submissions_toggle" class="bg-base-200 px-1.5 rounded-xl py-1 whitespace-nowrap">
<toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="<%= t('templates') %>">
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>" aria-label="<%= t('templates') %>">
<%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %>
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>" aria-label="<%= t('templates') %>" aria-pressed="<%= local_assigns[:selected] != 'submissions' %>">
<%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2', aria_hidden: true) %>
</button>
</toggle-cookies>
<toggle-cookies data-value="submissions" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="<%= t('submissions') %>">
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9' : 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>" aria-label="<%= t('submissions') %>">
<%= svg_icon('layout_list', class: 'w-6 h-6 stroke-2') %>
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9' : 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>" aria-label="<%= t('submissions') %>" aria-pressed="<%= local_assigns[:selected] == 'submissions' %>">
<%= svg_icon('layout_list', class: 'w-6 h-6 stroke-2', aria_hidden: true) %>
</button>
</toggle-cookies>
</form>

@ -37,7 +37,8 @@
</div>
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
<fieldset>
<legend class="label">SMTP Security</legend>
<div class="flex items-center space-x-6">
<% [%w[STARTTLS none], %w[TLS tls], %w[SSL ssl], %w[Noverify noverify]].each do |(label, val)| %>
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
@ -46,6 +47,7 @@
<% end %>
<% end %>
</div>
</fieldset>
</div>
<% end %>
<div class="form-control">

@ -34,7 +34,7 @@
<%= t('click_to_upload_or_drag_and_drop_files_html') %>
</div>
</div>
<input id="file" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
<input id="file" name="files[]" class="sr-only" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
</div>
</label>
</file-dropzone>
@ -54,18 +54,20 @@
<%= render 'alert' %>
<div class="overflow-x-auto">
<table class="table w-full table-lg rounded-b-none overflow-hidden">
<caption class="sr-only"><%= t('signing_certificates') %></caption>
<thead class="bg-base-200">
<tr class="text-neutral uppercase">
<th>
<th scope="col">
<%= t('name') %>
</th>
<th>
<th scope="col">
<%= t('valid_to') %>
</th>
<th>
<th scope="col">
<%= t('status') %>
</th>
<th class="text-right" width="1px">
<th scope="col" class="text-right sr-only" width="1px">
<%= t('actions') %>
</th>
</tr>
</thead>

@ -1,17 +1,17 @@
<div id="flash" class="absolute top-0 w-full h-0 z-20">
<div class="max-w-xl mx-auto mt-1.5">
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between mx-4 md:mx-0">
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between mx-4 md:mx-0" <% if flash[:notice] %>role="status" aria-live="polite" aria-atomic="true"<% else %>role="alert" aria-live="assertive" aria-atomic="true"<% end %>>
<div class="flex items-center space-x-3">
<% if flash[:notice] %>
<%= svg_icon('info_circle', class: 'stroke-info flex-none w-6 h-6') %>
<%= svg_icon('info_circle', class: 'stroke-info flex-none w-6 h-6', aria_hidden: true) %>
<% else %>
<%= svg_icon('exclamation_circle', class: 'stroke-error flex-none w-6 h-6') %>
<%= svg_icon('exclamation_circle', class: 'stroke-error flex-none w-6 h-6', aria_hidden: true) %>
<% end %>
<div>
<span><%= flash[:notice] || flash[:alert] %></span>
</div>
</div>
<remove-on-event data-event-type="click" data-selector-id="flash" class="mr-1 cursor-pointer">&times;</remove-on-event>
<remove-on-event data-event-type="click" data-selector-id="flash" class="mr-1 cursor-pointer" role="button" tabindex="0" aria-label="<%= t('dismiss') %>">&times;</remove-on-event>
</div>
</div>
</div>

@ -1,10 +1,10 @@
<% uuid = local_assigns[:uuid] || SecureRandom.uuid %>
<input type="checkbox" id="<%= uuid %>" class="modal-toggle">
<div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto">
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none" role="dialog" aria-modal="true"<% if local_assigns[:title] %> aria-labelledby="modal-title-<%= uuid %>"<% end %>>
<% if local_assigns[:title] %>
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
<span id="modal-title-<%= uuid %>">
<%= local_assigns[:title] %>
</span>
<label for="<%= uuid %>" class="text-xl" role="button" tabindex="0" aria-label="<%= t('close') %>">&times;</label>

@ -1,5 +1,5 @@
<% if pagy.pages > 1 %>
<div class="flex my-6 justify-center md:justify-between">
<nav aria-label="<%= t('pagination') %>" class="flex my-6 justify-center md:justify-between">
<div class="hidden md:block text-sm">
<% if pagy.count.nil? %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to)) %>
@ -16,7 +16,7 @@
<% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
<% end %>
<span class="join-item btn font-medium uppercase min-h-full h-10">
<span class="join-item btn font-medium uppercase min-h-full h-10" aria-current="page">
<%= t('page_number', number: pagy.page) %>
</span>
<% if local_assigns[:next_page_path].present? %>
@ -28,5 +28,5 @@
<% end %>
</div>
</div>
</div>
</nav>
<% end %>

@ -6,14 +6,15 @@
<% end %>
<% if params[:q].present? %>
<div class="relative">
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" aria-label="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
&times;
</a>
</div>
<% end %>
<search-input data-title="<%= local_assigns[:title_selector] || 'h1' %>" class="flex items-center">
<label for="search" class="sr-only"><%= t('search') %></label>
<input id="search" name="q" value="<%= params[:q] %>" class="input text-lg pr-10 -mr-12 w-0 md:w-60 <%= 'pl-8 input-outlined w-60' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>">
<button type="submit" title="<%= t('search') %>" class="btn btn-ghost btn-circle">
<button type="submit" aria-label="<%= t('search') %>" class="btn btn-ghost btn-circle">
<span class="enabled">
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
</span>

@ -1,4 +1,5 @@
<div class="block w-full md:w-52 flex-none">
<nav aria-label="<%= t('settings') %>">
<menu-active>
<ul id="account_settings_menu" class="menu px-0">
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium flex items-center space-x-0.5"><%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %><span><%= t('back') %></span></a></li>
@ -108,6 +109,7 @@
<% end %>
</ul>
</menu-active>
</nav>
<% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
<div id="support_channels" class="mx-4 border-t border-base-300 hidden md:block">
<div class="text-sm mt-3">
@ -115,19 +117,19 @@
</div>
<div class="flex mt-3 space-x-3">
<div class="tooltip" data-tip="GitHub">
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_github', class: 'w-8 h-8') %>
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" aria-label="GitHub" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_github', class: 'w-8 h-8', aria_hidden: true) %>
</a>
</div>
<div class="tooltip" data-tip="<%= t('discord_community') %>">
<a href="<%= Docuseal::DISCORD_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_discord', class: 'w-8 h-8') %>
<a href="<%= Docuseal::DISCORD_URL %>" target="_blank" aria-label="<%= t('discord_community') %>" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_discord', class: 'w-8 h-8', aria_hidden: true) %>
</a>
</div>
<%= capture do %>
<div class="tooltip" data-tip="<%= t('ai_assistant') %>">
<a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_openai', class: 'w-8 h-8') %>
<a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" aria-label="<%= t('ai_assistant') %>" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_openai', class: 'w-8 h-8', aria_hidden: true) %>
</a>
</div>
<% end %>

@ -7,12 +7,12 @@
<span>
<%= local_assigns[:title] %>
</span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close">&times;</a>
<button type="button" class="text-xl" aria-label="<%= t('close') %>" data-action="click:turbo-modal#close">&times;</button>
</div>
<% else %>
<span data-action="click:turbo-modal#close" class="absolute btn border border-base-100 bg-base-100 hover:bg-base-300 hover:border-base-300 btn-primary !text-base btn-sm btn-circle" style="left: -40px; top: 6px">
<button type="button" data-action="click:turbo-modal#close" aria-label="<%= t('close') %>" class="absolute btn border border-base-100 bg-base-100 hover:bg-base-300 hover:border-base-300 btn-primary !text-base btn-sm btn-circle" style="left: -40px; top: 6px">
&times;
</span>
</button>
<% end %>
<div class="w-screen md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= local_assigns[:title] ? '45px' : '0px' %>)">
<%= yield %>

@ -1,5 +1,5 @@
<% is_long = folder.name.size > 32 %>
<a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl relative" data-targets="dashboard-dropzone.folderCards" data-full-name="<%= folder.full_name %>">
<a href="<%= folder_path(folder) %>" aria-label="<%= folder.name %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl relative" data-targets="dashboard-dropzone.folderCards" data-full-name="<%= folder.full_name %>">
<% if !is_long %>
<%= svg_icon('folder', class: 'w-6 h-6') %>
<% end %>

@ -1,9 +1,10 @@
<div>
<nav aria-label="<%= t('breadcrumb') %>">
<%= link_to @template_folder.parent_folder ? folder_path(@template_folder.parent_folder) : root_path, class: 'flex items-center' do %>
<%= svg_icon('chevron_left', class: 'w-5 h-5') %>
<%= svg_icon('chevron_left', class: 'w-5 h-5', aria_hidden: true) %>
<span style="margin-left: 3px"><%= @template_folder.parent_folder&.name || t('home') %></span>
<% end %>
</div>
<span class="sr-only" aria-current="page"><%= @template_folder.name %></span>
</nav>
<dashboard-dropzone>
<div class="relative flex justify-between items-center w-full mb-4">
<%= form_for '', url: '', id: form_id = SecureRandom.uuid, method: :post, class: 'hidden', data: { target: 'dashboard-dropzone.form' }, html: { enctype: 'multipart/form-data' } do %>

@ -30,7 +30,7 @@
</div>
</span>
</div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
<input id="file_dropzone_input" name="files[]" class="sr-only" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
</div>
</label>
</file-dropzone>

@ -43,7 +43,7 @@
<%= t('click_to_upload_or_drag_and_drop_html') %>
</div>
</div>
<input id="file" name="file" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/png,image/jpeg,image/jpg">
<input id="file" name="file" class="sr-only" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/png,image/jpeg,image/jpg">
</div>
</label>
</file-dropzone>

@ -43,7 +43,7 @@
<%= t('click_to_upload_or_drag_and_drop_html') %>
</div>
</div>
<input id="file" name="file" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/png,image/jpeg,image/jpg">
<input id="file" name="file" class="sr-only" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/png,image/jpeg,image/jpg">
</div>
</label>
</file-dropzone>

@ -365,6 +365,11 @@ en: &en
page_number: 'Page %{number}'
page: Page
of: of
dismiss: Dismiss
step: Step
form_progress: Form progress
breadcrumb: Breadcrumb navigation
actions: Actions
text_content: text content
pdf_view: "PDF View"
text_view: "Text View"

Loading…
Cancel
Save