@zap-wunschlachen/wl-shared-components 1.0.35 → 1.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/App.vue +2 -7
- package/package.json +1 -1
- package/src/components/Audio/Waveform.vue +1 -0
- package/src/components/Button/Button.vue +39 -1
- package/src/components/CheckBox/CheckBox.css +29 -0
- package/src/components/CheckBox/Checkbox.vue +10 -2
- package/src/components/Dialog/Dialog.vue +14 -5
- package/src/components/Input/Input.vue +8 -2
- package/src/components/Loader/Loader.css +20 -0
- package/src/components/Loader/Loader.vue +1 -0
- package/src/components/OtpInput/OtpInput.vue +1 -1
- package/src/components/Select/Select.vue +1 -0
- package/src/components/StagingBanner/StagingBanner.css +19 -0
- package/src/components/StagingBanner/StagingBanner.vue +82 -0
- package/src/components/accessibility.css +218 -0
- package/src/components/index.ts +1 -0
- package/tests/unit/accessibility/component-a11y.spec.ts +469 -0
- package/tests/unit/components/Accordion/AccordionGroup.spec.ts +228 -0
- package/tests/unit/components/Accordion/AccordionItem.spec.ts +292 -0
- package/tests/unit/components/Appointment/AnamneseNotification.spec.ts +176 -0
- package/tests/unit/components/Background/Background.spec.ts +177 -0
- package/tests/unit/components/ErrorPage/ErrorPage.spec.ts +313 -0
- package/tests/unit/components/ErrorPage/ErrorPageLogo.spec.ts +153 -0
- package/tests/unit/components/Icons/AdvanceAppointments.spec.ts +61 -0
- package/tests/unit/components/Icons/Logo.spec.ts +228 -0
- package/tests/unit/components/Icons/MiniLogo.spec.ts +38 -0
- package/tests/unit/components/Icons/SolidArrowRight.spec.ts +49 -0
- package/tests/unit/components/Loader/Loader.spec.ts +197 -0
- package/tests/unit/components/MaintenanceBanner/MaintenanceBanner.spec.ts +302 -0
- package/tests/unit/utils/accessibility.spec.ts +318 -0
package/App.vue
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="app">
|
|
3
3
|
<div class="element-container">
|
|
4
|
-
<
|
|
5
|
-
:color="siteColors['slot-bg']"
|
|
6
|
-
:label="'Some Text'"
|
|
7
|
-
class="!font-bold mx-2" :text-color="siteColors['slot-text']"
|
|
8
|
-
>
|
|
9
|
-
</Button>
|
|
4
|
+
<StagingBanner show/>
|
|
10
5
|
</div>
|
|
11
6
|
</div>
|
|
12
7
|
</template>
|
|
13
8
|
|
|
14
9
|
<script setup lang="ts">
|
|
15
|
-
import
|
|
10
|
+
import StagingBanner from "@/components/StagingBanner/StagingBanner.vue";
|
|
16
11
|
import { siteColors } from "@/utils/index";
|
|
17
12
|
</script>
|
|
18
13
|
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
:variant="variant"
|
|
12
12
|
:size="size"
|
|
13
13
|
:readonly="readonly"
|
|
14
|
-
:style="
|
|
14
|
+
:style="rootStyle"
|
|
15
15
|
data-testid="root"
|
|
16
16
|
>
|
|
17
17
|
<slot>{{ label }}</slot>
|
|
@@ -127,10 +127,48 @@ const buttonStyle = computed(() => {
|
|
|
127
127
|
}
|
|
128
128
|
return {};
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
// Compute a CSS variable for focus color to avoid hardcoded values.
|
|
132
|
+
// Use provided color prop, fall back to siteColors.btn_bg, then to a sensible default.
|
|
133
|
+
const focusColor = computed(() => {
|
|
134
|
+
const col = props.color || siteColors['btn_bg'] || '#172774';
|
|
135
|
+
return col;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Expose a style object that includes the focus color custom property.
|
|
139
|
+
const rootStyle = computed(() => ({
|
|
140
|
+
...buttonStyle.value,
|
|
141
|
+
'--wl-button-focus-color': focusColor.value
|
|
142
|
+
}));
|
|
130
143
|
</script>
|
|
131
144
|
|
|
132
145
|
<style>
|
|
133
146
|
.wl-button.v-btn--variant-outlined {
|
|
134
147
|
border-width: 1.5px;
|
|
135
148
|
}
|
|
149
|
+
|
|
150
|
+
/* Accessibility: Focus Indicators (WCAG 2.4.7) */
|
|
151
|
+
.wl-button:focus-visible {
|
|
152
|
+
outline: 2px solid currentColor;
|
|
153
|
+
outline-offset: 2px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.wl-button.v-btn--variant-outlined:focus-visible {
|
|
157
|
+
/* Use component-provided focus color with fallback */
|
|
158
|
+
outline-color: var(--wl-button-focus-color, var(--Dental-Blue-0, #172774));
|
|
159
|
+
outline-width: 3px;
|
|
160
|
+
box-shadow: 0 0 0 4px rgba(0,0,0,0.08), 0 0 0 6px rgba(255,255,255,0.06) inset;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.wl-button.v-btn--variant-flat:focus-visible {
|
|
164
|
+
outline-color: white;
|
|
165
|
+
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.3);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* High contrast mode support */
|
|
169
|
+
@media (forced-colors: active) {
|
|
170
|
+
.wl-button {
|
|
171
|
+
border: 2px solid currentColor;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
136
174
|
</style>
|
|
@@ -183,3 +183,32 @@
|
|
|
183
183
|
.label-text {
|
|
184
184
|
font-weight: 300;
|
|
185
185
|
}
|
|
186
|
+
|
|
187
|
+
/* =====================================================
|
|
188
|
+
ACCESSIBILITY: Focus Indicators
|
|
189
|
+
WCAG 2.4.7: Focus Visible (Level AA)
|
|
190
|
+
===================================================== */
|
|
191
|
+
|
|
192
|
+
.input-checkbox:focus-visible {
|
|
193
|
+
outline: 2px solid var(--Dental-Blue-0);
|
|
194
|
+
outline-offset: 2px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.input-success:focus-visible {
|
|
198
|
+
outline-color: var(--Success-Green-0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.input-error:focus-visible {
|
|
202
|
+
outline-color: var(--Error-Red-0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* High contrast mode support */
|
|
206
|
+
@media (forced-colors: active) {
|
|
207
|
+
.input-checkbox {
|
|
208
|
+
border: 2px solid currentColor;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.input-checkbox:checked::before {
|
|
212
|
+
background-color: currentColor;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<label :class="labelClasses" data-testid="root">
|
|
2
|
+
<label :class="labelClasses" data-testid="root" :for="checkboxId">
|
|
3
3
|
<span class="checkbox-container">
|
|
4
4
|
<input
|
|
5
5
|
type="checkbox"
|
|
6
|
+
:id="checkboxId"
|
|
6
7
|
:name="value"
|
|
7
8
|
:checked="isChecked"
|
|
8
9
|
@change="updateValue"
|
|
9
10
|
:class="inputClasses"
|
|
10
11
|
:disabled="disabled"
|
|
12
|
+
:aria-checked="isChecked"
|
|
13
|
+
:aria-disabled="disabled"
|
|
14
|
+
:aria-invalid="error"
|
|
15
|
+
:aria-describedby="error ? `${checkboxId}-error` : undefined"
|
|
11
16
|
/>
|
|
12
17
|
</span>
|
|
13
|
-
{{ label }}
|
|
18
|
+
<span class="label-text">{{ label }}</span>
|
|
14
19
|
</label>
|
|
15
20
|
</template>
|
|
16
21
|
|
|
@@ -82,6 +87,9 @@ const props = defineProps({
|
|
|
82
87
|
// Define events that can be emitted from the component
|
|
83
88
|
const emit = defineEmits(['update:modelValue', 'input-error']);
|
|
84
89
|
|
|
90
|
+
// Generate stable unique ID for accessibility
|
|
91
|
+
const checkboxId = `checkbox-${props.value}-${Math.random().toString(36).substr(2, 9)}`;
|
|
92
|
+
|
|
85
93
|
// Computed property to check if the checkbox is selected
|
|
86
94
|
const isChecked = computed(() => {
|
|
87
95
|
if (Array.isArray(props.modelValue)) {
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
<!-- Scoped slot for the activator with click handler to open dialog -->
|
|
4
4
|
<slot name="activator" :openDialog="openDialog" />
|
|
5
5
|
|
|
6
|
-
<!-- Dialog component with
|
|
7
|
-
<v-dialog
|
|
8
|
-
|
|
6
|
+
<!-- Dialog component with accessibility attributes -->
|
|
7
|
+
<v-dialog
|
|
8
|
+
v-model="dialog"
|
|
9
|
+
width="auto"
|
|
10
|
+
role="dialog"
|
|
11
|
+
aria-modal="true"
|
|
12
|
+
:aria-labelledby="dialogTitleId"
|
|
13
|
+
@keydown.escape="closeDialog"
|
|
14
|
+
>
|
|
15
|
+
<slot :closeDialog="closeDialog" :titleId="dialogTitleId" name="content"></slot>
|
|
9
16
|
</v-dialog>
|
|
10
17
|
</div>
|
|
11
18
|
</template>
|
|
@@ -17,13 +24,15 @@ import './Dialog.css';
|
|
|
17
24
|
// Reactive dialog state
|
|
18
25
|
const dialog = ref(false);
|
|
19
26
|
|
|
27
|
+
// Generate unique ID for accessibility (stable across re-renders)
|
|
28
|
+
const dialogTitleId = `dialog-title-${Math.random().toString(36).slice(2, 9)}`;
|
|
29
|
+
|
|
20
30
|
// Methods to open and close the dialog
|
|
21
31
|
const openDialog = () => {
|
|
22
|
-
console.log('entereed 1');
|
|
23
32
|
dialog.value = true;
|
|
24
33
|
};
|
|
34
|
+
|
|
25
35
|
const closeDialog = () => {
|
|
26
|
-
console.log('entereed 2');
|
|
27
36
|
dialog.value = false;
|
|
28
37
|
};
|
|
29
38
|
</script>
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
@click:prepend-inner="onPrependInnerClick"
|
|
26
26
|
@click:append="onAppendClick"
|
|
27
27
|
@click:append-inner="onAppendInnerClick"
|
|
28
|
+
:error="error"
|
|
29
|
+
:aria-invalid="error || undefined"
|
|
30
|
+
:aria-describedby="hint ? inputHintId : undefined"
|
|
28
31
|
data-testid="root"
|
|
29
32
|
>
|
|
30
33
|
<!-- Prepend Slot (before v-field) -->
|
|
@@ -49,8 +52,8 @@
|
|
|
49
52
|
|
|
50
53
|
<!-- Details Slot (for messages, hints, etc.) -->
|
|
51
54
|
<template #details>
|
|
52
|
-
<div class="d-flex flex-row ga-1 align-center">
|
|
53
|
-
<v-icon icon="heroicons:exclamation-circle"></v-icon>
|
|
55
|
+
<div v-if="hint" class="d-flex flex-row ga-1 align-center" :id="inputHintId" role="alert" aria-hidden="true">
|
|
56
|
+
<v-icon icon="heroicons:exclamation-circle" aria-hidden="true"></v-icon>
|
|
54
57
|
<span>{{ hint }}</span>
|
|
55
58
|
</div>
|
|
56
59
|
</template>
|
|
@@ -76,6 +79,9 @@ const emit = defineEmits([
|
|
|
76
79
|
return props.placeholder || t('wl.input.placeholder');
|
|
77
80
|
});
|
|
78
81
|
|
|
82
|
+
// Generate unique ID for accessibility - connects input to hint/error message
|
|
83
|
+
const inputHintId = `input-hint-${Math.random().toString(36).slice(2, 11)}`;
|
|
84
|
+
|
|
79
85
|
// Define the component props
|
|
80
86
|
const props = defineProps({
|
|
81
87
|
/**
|
|
@@ -49,3 +49,23 @@ div:focus-visible {
|
|
|
49
49
|
outline: 2px solid;
|
|
50
50
|
outline-offset: 2px;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/* Screen reader only text - visually hidden but accessible */
|
|
54
|
+
.sr-only {
|
|
55
|
+
position: absolute;
|
|
56
|
+
width: 1px;
|
|
57
|
+
height: 1px;
|
|
58
|
+
padding: 0;
|
|
59
|
+
margin: -1px;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
clip: rect(0, 0, 0, 0);
|
|
62
|
+
white-space: nowrap;
|
|
63
|
+
border: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Reduced motion support */
|
|
67
|
+
@media (prefers-reduced-motion: reduce) {
|
|
68
|
+
.loader-spinner-arc {
|
|
69
|
+
animation: none;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.staging-banner {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
right: 0;
|
|
6
|
+
background: #ff6b35;
|
|
7
|
+
color: white;
|
|
8
|
+
text-align: center;
|
|
9
|
+
padding: 10px;
|
|
10
|
+
z-index: 99999;
|
|
11
|
+
font-weight: bold;
|
|
12
|
+
font-size: 14px;
|
|
13
|
+
font-family: inherit;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.staging-banner--hidden {
|
|
18
|
+
display: none;
|
|
19
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="isVisible" class="staging-banner">
|
|
3
|
+
{{ displayText }}
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { computed, onMounted, ref } from 'vue';
|
|
9
|
+
import './StagingBanner.css';
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
/**
|
|
13
|
+
* Custom text to display in the banner.
|
|
14
|
+
* Default: "⚠️ STAGING - Testumgebung (keine echten Daten)"
|
|
15
|
+
*/
|
|
16
|
+
text: {
|
|
17
|
+
type: String,
|
|
18
|
+
default: ''
|
|
19
|
+
},
|
|
20
|
+
/**
|
|
21
|
+
* Force the banner to show regardless of hostname.
|
|
22
|
+
* Useful for testing or manual control.
|
|
23
|
+
*/
|
|
24
|
+
show: {
|
|
25
|
+
type: Boolean,
|
|
26
|
+
default: undefined
|
|
27
|
+
},
|
|
28
|
+
/**
|
|
29
|
+
* Whether to auto-detect staging based on hostname.
|
|
30
|
+
* Looks for "staging." or "staging-" in the hostname.
|
|
31
|
+
* Default: true
|
|
32
|
+
*/
|
|
33
|
+
autoDetect: {
|
|
34
|
+
type: Boolean,
|
|
35
|
+
default: true
|
|
36
|
+
},
|
|
37
|
+
/**
|
|
38
|
+
* Whether to add padding to the body to account for the banner.
|
|
39
|
+
* Default: true
|
|
40
|
+
*/
|
|
41
|
+
addBodyPadding: {
|
|
42
|
+
type: Boolean,
|
|
43
|
+
default: true
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* Custom padding value for the body (in pixels).
|
|
47
|
+
* Default: 40
|
|
48
|
+
*/
|
|
49
|
+
bodyPaddingPx: {
|
|
50
|
+
type: Number,
|
|
51
|
+
default: 40
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const isStaging = ref(false);
|
|
56
|
+
|
|
57
|
+
const displayText = computed(() => {
|
|
58
|
+
return props.text || '⚠️ STAGING - Testumgebung (keine echten Daten)';
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const isVisible = computed(() => {
|
|
62
|
+
// If show prop is explicitly set, use that
|
|
63
|
+
if (props.show !== undefined) {
|
|
64
|
+
return props.show;
|
|
65
|
+
}
|
|
66
|
+
// Otherwise use auto-detection result
|
|
67
|
+
return props.autoDetect && isStaging.value;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
onMounted(() => {
|
|
71
|
+
// Check if running in browser
|
|
72
|
+
if (typeof window !== 'undefined') {
|
|
73
|
+
const hostname = window.location.hostname;
|
|
74
|
+
isStaging.value = hostname.includes('staging.') || hostname.includes('staging-');
|
|
75
|
+
|
|
76
|
+
// Add body padding if visible and option is enabled
|
|
77
|
+
if (isVisible.value && props.addBodyPadding) {
|
|
78
|
+
document.body.style.paddingTop = `${props.bodyPaddingPx}px`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Accessibility Styles
|
|
3
|
+
*
|
|
4
|
+
* These styles ensure WCAG 2.1 AA compliance for all components.
|
|
5
|
+
* Required for German accessibility requirements (BITV 2.0).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* =====================================================
|
|
9
|
+
FOCUS INDICATORS
|
|
10
|
+
WCAG 2.4.7: Focus Visible (Level AA)
|
|
11
|
+
===================================================== */
|
|
12
|
+
|
|
13
|
+
/* Universal focus-visible style for all interactive elements */
|
|
14
|
+
.wl-button:focus-visible,
|
|
15
|
+
button:focus-visible,
|
|
16
|
+
a:focus-visible,
|
|
17
|
+
input:focus-visible,
|
|
18
|
+
select:focus-visible,
|
|
19
|
+
textarea:focus-visible,
|
|
20
|
+
[tabindex="0"]:focus-visible {
|
|
21
|
+
outline: 2px solid var(--Dental-Blue-0, #172774);
|
|
22
|
+
outline-offset: 2px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* High contrast focus for dark backgrounds */
|
|
26
|
+
.wl-button.v-btn--variant-flat:focus-visible {
|
|
27
|
+
outline-color: white;
|
|
28
|
+
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.5);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Checkbox focus indicator */
|
|
32
|
+
.input-checkbox:focus-visible {
|
|
33
|
+
outline: 2px solid var(--Dental-Blue-0, #172774);
|
|
34
|
+
outline-offset: 2px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Button focus styles */
|
|
38
|
+
.wl-button:focus-visible {
|
|
39
|
+
outline: 2px solid currentColor;
|
|
40
|
+
outline-offset: 2px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.wl-button.v-btn--variant-outlined:focus-visible {
|
|
44
|
+
outline: 2px solid var(--Dental-Blue-0, #172774);
|
|
45
|
+
outline-offset: 2px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Input focus styles */
|
|
49
|
+
.v-text-field input:focus-visible,
|
|
50
|
+
.v-textarea textarea:focus-visible {
|
|
51
|
+
outline: none; /* Vuetify handles this via border */
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.v-text-field:focus-within {
|
|
55
|
+
border-color: var(--Dental-Blue-0, #172774) !important;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Select/Combobox focus */
|
|
59
|
+
.wl-select .v-field:focus-within {
|
|
60
|
+
border-color: var(--Dental-Blue-0, #172774) !important;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* =====================================================
|
|
64
|
+
REDUCED MOTION
|
|
65
|
+
WCAG 2.3.3: Animation from Interactions (Level AAA)
|
|
66
|
+
===================================================== */
|
|
67
|
+
|
|
68
|
+
@media (prefers-reduced-motion: reduce) {
|
|
69
|
+
*,
|
|
70
|
+
*::before,
|
|
71
|
+
*::after {
|
|
72
|
+
animation-duration: 0.01ms !important;
|
|
73
|
+
animation-iteration-count: 1 !important;
|
|
74
|
+
transition-duration: 0.01ms !important;
|
|
75
|
+
scroll-behavior: auto !important;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* =====================================================
|
|
80
|
+
HIGH CONTRAST MODE
|
|
81
|
+
Windows High Contrast Mode Support
|
|
82
|
+
===================================================== */
|
|
83
|
+
|
|
84
|
+
@media (forced-colors: active) {
|
|
85
|
+
.wl-button {
|
|
86
|
+
border: 2px solid currentColor;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.input-checkbox {
|
|
90
|
+
border: 2px solid currentColor;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.input-checkbox:checked::before {
|
|
94
|
+
background-color: currentColor;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* =====================================================
|
|
99
|
+
DISABLED STATES
|
|
100
|
+
Ensure disabled elements maintain contrast
|
|
101
|
+
WCAG 1.4.3: Contrast (Minimum) - Note: Disabled elements are exempt
|
|
102
|
+
but we still aim for visibility
|
|
103
|
+
===================================================== */
|
|
104
|
+
|
|
105
|
+
.wl-button:disabled,
|
|
106
|
+
.wl-button[aria-disabled="true"] {
|
|
107
|
+
opacity: 0.6;
|
|
108
|
+
cursor: not-allowed;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.input-checkbox:disabled {
|
|
112
|
+
opacity: 0.6;
|
|
113
|
+
cursor: not-allowed;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* =====================================================
|
|
117
|
+
SKIP LINKS
|
|
118
|
+
WCAG 2.4.1: Bypass Blocks (Level A)
|
|
119
|
+
===================================================== */
|
|
120
|
+
|
|
121
|
+
.skip-link {
|
|
122
|
+
position: absolute;
|
|
123
|
+
top: -40px;
|
|
124
|
+
left: 0;
|
|
125
|
+
background: var(--Dental-Blue-0, #172774);
|
|
126
|
+
color: white;
|
|
127
|
+
padding: 8px 16px;
|
|
128
|
+
z-index: 10000;
|
|
129
|
+
text-decoration: none;
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.skip-link:focus {
|
|
134
|
+
top: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* =====================================================
|
|
138
|
+
SCREEN READER ONLY
|
|
139
|
+
Visually hidden but accessible to screen readers
|
|
140
|
+
===================================================== */
|
|
141
|
+
|
|
142
|
+
.sr-only {
|
|
143
|
+
position: absolute;
|
|
144
|
+
width: 1px;
|
|
145
|
+
height: 1px;
|
|
146
|
+
padding: 0;
|
|
147
|
+
margin: -1px;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
clip: rect(0, 0, 0, 0);
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
border: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* =====================================================
|
|
155
|
+
ICON BUTTONS
|
|
156
|
+
Ensure icon-only buttons are accessible
|
|
157
|
+
===================================================== */
|
|
158
|
+
|
|
159
|
+
.icon-button {
|
|
160
|
+
position: relative;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Show in development environments */
|
|
164
|
+
body.dev-mode .icon-button:not([aria-label]):not([aria-labelledby])::after {
|
|
165
|
+
content: "Warning: Missing accessible name";
|
|
166
|
+
position: absolute;
|
|
167
|
+
background: red;
|
|
168
|
+
color: white;
|
|
169
|
+
font-size: 10px;
|
|
170
|
+
padding: 2px;
|
|
171
|
+
display: block;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* =====================================================
|
|
175
|
+
LOADING STATES
|
|
176
|
+
WCAG 4.1.3: Status Messages (Level AA)
|
|
177
|
+
===================================================== */
|
|
178
|
+
|
|
179
|
+
.loading-indicator {
|
|
180
|
+
/* Announce loading state to screen readers */
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
[aria-busy="true"] {
|
|
184
|
+
cursor: wait;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* =====================================================
|
|
188
|
+
ERROR STATES
|
|
189
|
+
Ensure error messages are visible and announced
|
|
190
|
+
===================================================== */
|
|
191
|
+
|
|
192
|
+
.error-message {
|
|
193
|
+
color: var(--Error-Red-0, #d32f2f);
|
|
194
|
+
font-weight: 500;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
[aria-invalid="true"] {
|
|
198
|
+
border-color: var(--Error-Red-0, #d32f2f) !important;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* =====================================================
|
|
202
|
+
MINIMUM TOUCH TARGET SIZE
|
|
203
|
+
WCAG 2.5.5: Target Size (Level AAA) - 44x44px minimum
|
|
204
|
+
===================================================== */
|
|
205
|
+
|
|
206
|
+
.touch-target-min {
|
|
207
|
+
min-width: 44px;
|
|
208
|
+
min-height: 44px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* =====================================================
|
|
212
|
+
TEXT SPACING
|
|
213
|
+
WCAG 1.4.12: Text Spacing (Level AA)
|
|
214
|
+
Content should support user adjustments
|
|
215
|
+
===================================================== */
|
|
216
|
+
|
|
217
|
+
/* These rules ensure content adapts when users adjust text spacing */
|
|
218
|
+
|
package/src/components/index.ts
CHANGED
|
@@ -24,5 +24,6 @@ export { default as Loader } from './Loader/Loader.vue';
|
|
|
24
24
|
export { default as AnamneseNotification } from './Appointment/Card/AnamneseNotification.vue';
|
|
25
25
|
export { default as ErrorPage } from './ErrorPage/ErrorPage.vue';
|
|
26
26
|
export { default as MaintenanceBanner } from './MaintenanceBanner/MaintenanceBanner.vue';
|
|
27
|
+
export { default as StagingBanner } from './StagingBanner/StagingBanner.vue';
|
|
27
28
|
export { default as Logo } from './Icons/Logo.vue';
|
|
28
29
|
export { default as MiniLogo } from './Icons/MiniLogo.vue';
|