@softdin/shared 0.1.3
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/README.md +48 -0
- package/package.json +50 -0
- package/src/a11y/announcer.ts +43 -0
- package/src/a11y/apply.ts +83 -0
- package/src/a11y/index.ts +31 -0
- package/src/a11y/state.ts +40 -0
- package/src/a11y/storage.ts +106 -0
- package/src/a11y/types.ts +187 -0
- package/src/api.ts +89 -0
- package/src/assets/softdin-logo.png +0 -0
- package/src/auth.ts +134 -0
- package/src/components/a11y/A11yLiveRegion.vue +11 -0
- package/src/components/a11y/AccessibilityMenuControls.vue +426 -0
- package/src/components/a11y/AccessibilityPreferencesPanel.vue +331 -0
- package/src/components/a11y/ColorSchemeQuickToggle.vue +105 -0
- package/src/components/a11y/SkipLink.vue +41 -0
- package/src/components/auth/AuthBrandHeader.vue +129 -0
- package/src/components/auth/AuthShell.vue +52 -0
- package/src/components/auth/DataProcessingConsentCheckbox.vue +303 -0
- package/src/components/auth/DataProcessingConsentForm.vue +111 -0
- package/src/components/forms/ErrorAlert.vue +170 -0
- package/src/components/forms/FormCancelButton.vue +87 -0
- package/src/components/forms/FormCloseButton.vue +114 -0
- package/src/components/forms/FormHelpButton.vue +109 -0
- package/src/components/forms/FormHelpModal.vue +193 -0
- package/src/components/forms/FormMaximizeButton.vue +143 -0
- package/src/components/forms/FormRefreshButton.vue +152 -0
- package/src/components/forms/FormSaveButton.vue +113 -0
- package/src/components/forms/PasswordInput.vue +154 -0
- package/src/components/navigation/CatalogListPagination.vue +113 -0
- package/src/config/apiBase.ts +37 -0
- package/src/lib/catalog-pagination.ts +27 -0
- package/src/lib/pagination.ts +86 -0
- package/src/lib/softdin-libreria.ts +50 -0
- package/src/style.css +278 -0
- package/src/types/auth.ts +146 -0
- package/src/types/softdin-libreria.d.ts +41 -0
- package/src/utils/apiError.ts +55 -0
- package/src/utils/connectivity.ts +33 -0
- package/src/utils/tenantLogo.ts +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @softdin/shared
|
|
2
|
+
|
|
3
|
+
Librería interna compartida entre **softdin-tenant** y **softdin-central**.
|
|
4
|
+
|
|
5
|
+
Repositorio: [github.com/DinSoftdin/softdin-shared](https://github.com/DinSoftdin/softdin-shared)
|
|
6
|
+
|
|
7
|
+
## Contenido
|
|
8
|
+
|
|
9
|
+
- Cliente HTTP (`api`, interceptores)
|
|
10
|
+
- Autenticación (`authService`, tipos)
|
|
11
|
+
- Componentes de login (`AuthShell`, `AuthBrandHeader`, `PasswordInput`)
|
|
12
|
+
- Estilos base Tailwind (`style.css`)
|
|
13
|
+
- Utilidades (`pagination`, `softdin-libreria`, `tenantLogo`)
|
|
14
|
+
|
|
15
|
+
Incluye `softdinlibreriajs` como dependencia (central aún no la consume).
|
|
16
|
+
|
|
17
|
+
## Publicar cambios en GitHub
|
|
18
|
+
|
|
19
|
+
Este directorio es la **raíz del repositorio** `softdin-shared` (no subir la carpeta `packages/`).
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd packages/softdin-shared
|
|
23
|
+
git init
|
|
24
|
+
git add .
|
|
25
|
+
git commit -m "feat: versión inicial de @softdin/shared"
|
|
26
|
+
git branch -M main
|
|
27
|
+
git remote add origin https://github.com/DinSoftdin/softdin-shared.git
|
|
28
|
+
git push -u origin main
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Tras publicar, etiquete versiones para que tenant y central fijen la dependencia:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git tag v0.1.0
|
|
35
|
+
git push origin v0.1.0
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Consumo desde las apps
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
"@softdin/shared": "github:DinSoftdin/softdin-shared#v0.1.0"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Desarrollo local en monorepo SoftDIN:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
"@softdin/shared": "file:../packages/softdin-shared"
|
|
48
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@softdin/shared",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"src",
|
|
7
|
+
"src/style.css"
|
|
8
|
+
],
|
|
9
|
+
"exports": {
|
|
10
|
+
"./api": "./src/api.ts",
|
|
11
|
+
"./auth": "./src/auth.ts",
|
|
12
|
+
"./a11y": "./src/a11y/index.ts",
|
|
13
|
+
"./config/apiBase": "./src/config/apiBase.ts",
|
|
14
|
+
"./lib/pagination": "./src/lib/pagination.ts",
|
|
15
|
+
"./lib/catalog-pagination": "./src/lib/catalog-pagination.ts",
|
|
16
|
+
"./lib/softdin-libreria": "./src/lib/softdin-libreria.ts",
|
|
17
|
+
"./types/auth": "./src/types/auth.ts",
|
|
18
|
+
"./utils/apiError": "./src/utils/apiError.ts",
|
|
19
|
+
"./utils/tenantLogo": "./src/utils/tenantLogo.ts",
|
|
20
|
+
"./components/forms/PasswordInput.vue": "./src/components/forms/PasswordInput.vue",
|
|
21
|
+
"./components/forms/FormCloseButton.vue": "./src/components/forms/FormCloseButton.vue",
|
|
22
|
+
"./components/forms/FormHelpButton.vue": "./src/components/forms/FormHelpButton.vue",
|
|
23
|
+
"./components/forms/FormMaximizeButton.vue": "./src/components/forms/FormMaximizeButton.vue",
|
|
24
|
+
"./components/forms/FormRefreshButton.vue": "./src/components/forms/FormRefreshButton.vue",
|
|
25
|
+
"./components/forms/FormSaveButton.vue": "./src/components/forms/FormSaveButton.vue",
|
|
26
|
+
"./components/forms/FormCancelButton.vue": "./src/components/forms/FormCancelButton.vue",
|
|
27
|
+
"./components/forms/FormHelpModal.vue": "./src/components/forms/FormHelpModal.vue",
|
|
28
|
+
"./components/forms/ErrorAlert.vue": "./src/components/forms/ErrorAlert.vue",
|
|
29
|
+
"./components/navigation/CatalogListPagination.vue": "./src/components/navigation/CatalogListPagination.vue",
|
|
30
|
+
"./components/auth/AuthBrandHeader.vue": "./src/components/auth/AuthBrandHeader.vue",
|
|
31
|
+
"./components/auth/AuthShell.vue": "./src/components/auth/AuthShell.vue",
|
|
32
|
+
"./components/auth/DataProcessingConsentForm.vue": "./src/components/auth/DataProcessingConsentForm.vue",
|
|
33
|
+
"./components/auth/DataProcessingConsentCheckbox.vue": "./src/components/auth/DataProcessingConsentCheckbox.vue",
|
|
34
|
+
"./components/a11y/AccessibilityPreferencesPanel.vue": "./src/components/a11y/AccessibilityPreferencesPanel.vue",
|
|
35
|
+
"./components/a11y/SkipLink.vue": "./src/components/a11y/SkipLink.vue",
|
|
36
|
+
"./components/a11y/A11yLiveRegion.vue": "./src/components/a11y/A11yLiveRegion.vue",
|
|
37
|
+
"./components/a11y/ColorSchemeQuickToggle.vue": "./src/components/a11y/ColorSchemeQuickToggle.vue",
|
|
38
|
+
"./components/a11y/AccessibilityMenuControls.vue": "./src/components/a11y/AccessibilityMenuControls.vue",
|
|
39
|
+
"./style.css": "./src/style.css",
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"axios": "^1.18.1",
|
|
44
|
+
"softdinlibreriajs": "^12.0.43"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/DinSoftdin/softdin-shared.git"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type A11yAnnouncementPoliteness = 'polite' | 'assertive'
|
|
4
|
+
|
|
5
|
+
const announcementMessage = ref('')
|
|
6
|
+
const announcementPoliteness = ref<A11yAnnouncementPoliteness>('polite')
|
|
7
|
+
let clearTimer: ReturnType<typeof setTimeout> | null = null
|
|
8
|
+
|
|
9
|
+
export function useA11yAnnouncer() {
|
|
10
|
+
return {
|
|
11
|
+
message: announcementMessage,
|
|
12
|
+
politeness: announcementPoliteness,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Anuncia un mensaje a lectores de pantalla vía la región aria-live montada en el layout.
|
|
18
|
+
*/
|
|
19
|
+
export function announce(
|
|
20
|
+
message: string,
|
|
21
|
+
politeness: A11yAnnouncementPoliteness = 'polite',
|
|
22
|
+
): void {
|
|
23
|
+
const text = message.trim()
|
|
24
|
+
if (!text || typeof window === 'undefined') {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (clearTimer) {
|
|
29
|
+
clearTimeout(clearTimer)
|
|
30
|
+
clearTimer = null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
announcementPoliteness.value = politeness
|
|
34
|
+
// Vaciar y reasignar fuerza un nuevo anuncio aunque el texto sea igual.
|
|
35
|
+
announcementMessage.value = ''
|
|
36
|
+
window.requestAnimationFrame(() => {
|
|
37
|
+
announcementMessage.value = text
|
|
38
|
+
clearTimer = setTimeout(() => {
|
|
39
|
+
announcementMessage.value = ''
|
|
40
|
+
clearTimer = null
|
|
41
|
+
}, 4000)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
A11Y_BRAND_ACCENT_PALETTES,
|
|
3
|
+
A11Y_FONT_FAMILY_VALUES,
|
|
4
|
+
A11Y_LINE_SPACING_VALUES,
|
|
5
|
+
A11Y_TEXT_SCALE_VALUES,
|
|
6
|
+
type AccessibilityPreferences,
|
|
7
|
+
type A11yColorScheme,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
let systemColorSchemeListener: ((event: MediaQueryListEvent) => void) | null = null
|
|
11
|
+
let systemColorSchemeMedia: MediaQueryList | null = null
|
|
12
|
+
|
|
13
|
+
export function resolveColorScheme(colorScheme: A11yColorScheme): 'light' | 'dark' {
|
|
14
|
+
if (colorScheme !== 'system') {
|
|
15
|
+
return colorScheme
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
19
|
+
return 'light'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function clearSystemColorSchemeListener(): void {
|
|
26
|
+
if (systemColorSchemeMedia && systemColorSchemeListener) {
|
|
27
|
+
systemColorSchemeMedia.removeEventListener('change', systemColorSchemeListener)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
systemColorSchemeMedia = null
|
|
31
|
+
systemColorSchemeListener = null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function watchSystemColorScheme(prefs: AccessibilityPreferences): void {
|
|
35
|
+
clearSystemColorSchemeListener()
|
|
36
|
+
|
|
37
|
+
if (prefs.colorScheme !== 'system' || typeof window === 'undefined') {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
systemColorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)')
|
|
42
|
+
systemColorSchemeListener = () => {
|
|
43
|
+
applyAccessibilityPreferences(prefs)
|
|
44
|
+
}
|
|
45
|
+
systemColorSchemeMedia.addEventListener('change', systemColorSchemeListener)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function applyAccessibilityPreferences(prefs: AccessibilityPreferences): void {
|
|
49
|
+
if (typeof document === 'undefined') {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const root = document.documentElement
|
|
54
|
+
const spacing = A11Y_LINE_SPACING_VALUES[prefs.lineSpacing]
|
|
55
|
+
const resolvedTheme = resolveColorScheme(prefs.colorScheme)
|
|
56
|
+
const brand = A11Y_BRAND_ACCENT_PALETTES[prefs.brandAccent][resolvedTheme]
|
|
57
|
+
|
|
58
|
+
root.style.setProperty('--a11y-text-scale', String(A11Y_TEXT_SCALE_VALUES[prefs.textScale]))
|
|
59
|
+
root.style.setProperty('--a11y-font-family', A11Y_FONT_FAMILY_VALUES[prefs.fontFamily])
|
|
60
|
+
root.style.setProperty('--a11y-line-height', spacing.lineHeight)
|
|
61
|
+
root.style.setProperty('--a11y-letter-spacing', spacing.letterSpacing)
|
|
62
|
+
|
|
63
|
+
root.style.setProperty('--color-brand-50', brand.brand50)
|
|
64
|
+
root.style.setProperty('--color-brand-100', brand.brand100)
|
|
65
|
+
root.style.setProperty('--color-brand-500', brand.brand500)
|
|
66
|
+
root.style.setProperty('--color-brand-600', brand.brand600)
|
|
67
|
+
root.style.setProperty('--color-brand-700', brand.brand700)
|
|
68
|
+
root.style.setProperty('--color-brand-800', brand.brand800)
|
|
69
|
+
root.style.setProperty('--color-sidebar', brand.sidebar)
|
|
70
|
+
|
|
71
|
+
root.dataset.a11yScale = prefs.textScale
|
|
72
|
+
root.dataset.a11yFont = prefs.fontFamily
|
|
73
|
+
root.dataset.a11ySpacing = prefs.lineSpacing
|
|
74
|
+
root.dataset.a11yContrast = prefs.contrast
|
|
75
|
+
root.dataset.a11yTheme = resolvedTheme
|
|
76
|
+
root.dataset.a11yColorScheme = prefs.colorScheme
|
|
77
|
+
root.dataset.a11yMotion = prefs.motion
|
|
78
|
+
root.dataset.a11yBrand = prefs.brandAccent
|
|
79
|
+
|
|
80
|
+
root.style.colorScheme = resolvedTheme
|
|
81
|
+
|
|
82
|
+
watchSystemColorScheme(prefs)
|
|
83
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export {
|
|
2
|
+
initAccessibilityPreferences,
|
|
3
|
+
patchAccessibilityPreferences,
|
|
4
|
+
resetAccessibilityPreferences,
|
|
5
|
+
updateAccessibilityPreferences,
|
|
6
|
+
accessibilityPreferencesState,
|
|
7
|
+
} from './state'
|
|
8
|
+
export { announce, useA11yAnnouncer } from './announcer'
|
|
9
|
+
export { applyAccessibilityPreferences, resolveColorScheme } from './apply'
|
|
10
|
+
export {
|
|
11
|
+
loadAccessibilityPreferences,
|
|
12
|
+
normalizeAccessibilityPreferences,
|
|
13
|
+
saveAccessibilityPreferences,
|
|
14
|
+
} from './storage'
|
|
15
|
+
export {
|
|
16
|
+
A11Y_BRAND_ACCENT_PALETTES,
|
|
17
|
+
A11Y_FONT_FAMILY_VALUES,
|
|
18
|
+
A11Y_LINE_SPACING_VALUES,
|
|
19
|
+
A11Y_STORAGE_KEY,
|
|
20
|
+
A11Y_TEXT_SCALE_VALUES,
|
|
21
|
+
DEFAULT_ACCESSIBILITY_PREFERENCES,
|
|
22
|
+
type A11yBrandAccent,
|
|
23
|
+
type A11yColorScheme,
|
|
24
|
+
type A11yContrast,
|
|
25
|
+
type A11yFontFamily,
|
|
26
|
+
type A11yLineSpacing,
|
|
27
|
+
type A11yMotion,
|
|
28
|
+
type A11yTextScale,
|
|
29
|
+
type AccessibilityPreferences,
|
|
30
|
+
type BrandAccentPalette,
|
|
31
|
+
} from './types'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { reactive } from 'vue'
|
|
2
|
+
import { applyAccessibilityPreferences } from './apply'
|
|
3
|
+
import { loadAccessibilityPreferences, saveAccessibilityPreferences } from './storage'
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_ACCESSIBILITY_PREFERENCES,
|
|
6
|
+
type AccessibilityPreferences,
|
|
7
|
+
} from './types'
|
|
8
|
+
|
|
9
|
+
/** Estado reactivo compartido entre panel, toggle y bootstrap. */
|
|
10
|
+
export const accessibilityPreferencesState = reactive<AccessibilityPreferences>(
|
|
11
|
+
loadAccessibilityPreferences(),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export function initAccessibilityPreferences(): AccessibilityPreferences {
|
|
15
|
+
Object.assign(accessibilityPreferencesState, loadAccessibilityPreferences())
|
|
16
|
+
applyAccessibilityPreferences({ ...accessibilityPreferencesState })
|
|
17
|
+
return { ...accessibilityPreferencesState }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function updateAccessibilityPreferences(
|
|
21
|
+
next: AccessibilityPreferences,
|
|
22
|
+
): AccessibilityPreferences {
|
|
23
|
+
Object.assign(accessibilityPreferencesState, next)
|
|
24
|
+
applyAccessibilityPreferences({ ...accessibilityPreferencesState })
|
|
25
|
+
saveAccessibilityPreferences({ ...accessibilityPreferencesState })
|
|
26
|
+
return { ...accessibilityPreferencesState }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resetAccessibilityPreferences(): AccessibilityPreferences {
|
|
30
|
+
return updateAccessibilityPreferences({ ...DEFAULT_ACCESSIBILITY_PREFERENCES })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function patchAccessibilityPreferences(
|
|
34
|
+
patch: Partial<AccessibilityPreferences>,
|
|
35
|
+
): AccessibilityPreferences {
|
|
36
|
+
return updateAccessibilityPreferences({
|
|
37
|
+
...accessibilityPreferencesState,
|
|
38
|
+
...patch,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
A11Y_STORAGE_KEY,
|
|
3
|
+
DEFAULT_ACCESSIBILITY_PREFERENCES,
|
|
4
|
+
type A11yBrandAccent,
|
|
5
|
+
type A11yColorScheme,
|
|
6
|
+
type A11yContrast,
|
|
7
|
+
type A11yFontFamily,
|
|
8
|
+
type A11yLineSpacing,
|
|
9
|
+
type A11yMotion,
|
|
10
|
+
type A11yTextScale,
|
|
11
|
+
type AccessibilityPreferences,
|
|
12
|
+
} from './types'
|
|
13
|
+
|
|
14
|
+
function isTextScale(value: unknown): value is A11yTextScale {
|
|
15
|
+
return value === 'xs' || value === 'sm' || value === 'md' || value === 'lg' || value === 'xl'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isFontFamily(value: unknown): value is A11yFontFamily {
|
|
19
|
+
return value === 'system' || value === 'serif' || value === 'dyslexia'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isLineSpacing(value: unknown): value is A11yLineSpacing {
|
|
23
|
+
return value === 'normal' || value === 'relaxed'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isContrast(value: unknown): value is A11yContrast {
|
|
27
|
+
return value === 'default' || value === 'high'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isColorScheme(value: unknown): value is A11yColorScheme {
|
|
31
|
+
return value === 'light' || value === 'dark' || value === 'system'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isMotion(value: unknown): value is A11yMotion {
|
|
35
|
+
return value === 'system' || value === 'reduce'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isBrandAccent(value: unknown): value is A11yBrandAccent {
|
|
39
|
+
return (
|
|
40
|
+
value === 'green' ||
|
|
41
|
+
value === 'blue' ||
|
|
42
|
+
value === 'red' ||
|
|
43
|
+
value === 'purple' ||
|
|
44
|
+
value === 'orange' ||
|
|
45
|
+
value === 'teal'
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function normalizeAccessibilityPreferences(
|
|
50
|
+
raw: Partial<AccessibilityPreferences> | null | undefined,
|
|
51
|
+
): AccessibilityPreferences {
|
|
52
|
+
const source = raw ?? {}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
textScale: isTextScale(source.textScale)
|
|
56
|
+
? source.textScale
|
|
57
|
+
: DEFAULT_ACCESSIBILITY_PREFERENCES.textScale,
|
|
58
|
+
fontFamily: isFontFamily(source.fontFamily)
|
|
59
|
+
? source.fontFamily
|
|
60
|
+
: DEFAULT_ACCESSIBILITY_PREFERENCES.fontFamily,
|
|
61
|
+
lineSpacing: isLineSpacing(source.lineSpacing)
|
|
62
|
+
? source.lineSpacing
|
|
63
|
+
: DEFAULT_ACCESSIBILITY_PREFERENCES.lineSpacing,
|
|
64
|
+
contrast: isContrast(source.contrast)
|
|
65
|
+
? source.contrast
|
|
66
|
+
: DEFAULT_ACCESSIBILITY_PREFERENCES.contrast,
|
|
67
|
+
colorScheme: isColorScheme(source.colorScheme)
|
|
68
|
+
? source.colorScheme
|
|
69
|
+
: DEFAULT_ACCESSIBILITY_PREFERENCES.colorScheme,
|
|
70
|
+
motion: isMotion(source.motion)
|
|
71
|
+
? source.motion
|
|
72
|
+
: DEFAULT_ACCESSIBILITY_PREFERENCES.motion,
|
|
73
|
+
brandAccent: isBrandAccent(source.brandAccent)
|
|
74
|
+
? source.brandAccent
|
|
75
|
+
: DEFAULT_ACCESSIBILITY_PREFERENCES.brandAccent,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function loadAccessibilityPreferences(): AccessibilityPreferences {
|
|
80
|
+
if (typeof window === 'undefined') {
|
|
81
|
+
return { ...DEFAULT_ACCESSIBILITY_PREFERENCES }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const raw = window.localStorage.getItem(A11Y_STORAGE_KEY)
|
|
86
|
+
if (!raw) {
|
|
87
|
+
return { ...DEFAULT_ACCESSIBILITY_PREFERENCES }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return normalizeAccessibilityPreferences(JSON.parse(raw) as Partial<AccessibilityPreferences>)
|
|
91
|
+
} catch {
|
|
92
|
+
return { ...DEFAULT_ACCESSIBILITY_PREFERENCES }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function saveAccessibilityPreferences(prefs: AccessibilityPreferences): void {
|
|
97
|
+
if (typeof window === 'undefined') {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
window.localStorage.setItem(A11Y_STORAGE_KEY, JSON.stringify(normalizeAccessibilityPreferences(prefs)))
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore quota / private mode errors.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
export type A11yTextScale = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
2
|
+
export type A11yFontFamily = 'system' | 'serif' | 'dyslexia'
|
|
3
|
+
export type A11yLineSpacing = 'normal' | 'relaxed'
|
|
4
|
+
export type A11yContrast = 'default' | 'high'
|
|
5
|
+
export type A11yColorScheme = 'light' | 'dark' | 'system'
|
|
6
|
+
export type A11yMotion = 'system' | 'reduce'
|
|
7
|
+
export type A11yBrandAccent = 'green' | 'blue' | 'red' | 'purple' | 'orange' | 'teal'
|
|
8
|
+
|
|
9
|
+
export interface BrandAccentPalette {
|
|
10
|
+
brand50: string
|
|
11
|
+
brand100: string
|
|
12
|
+
brand500: string
|
|
13
|
+
brand600: string
|
|
14
|
+
brand700: string
|
|
15
|
+
brand800: string
|
|
16
|
+
sidebar: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AccessibilityPreferences {
|
|
20
|
+
textScale: A11yTextScale
|
|
21
|
+
fontFamily: A11yFontFamily
|
|
22
|
+
lineSpacing: A11yLineSpacing
|
|
23
|
+
contrast: A11yContrast
|
|
24
|
+
colorScheme: A11yColorScheme
|
|
25
|
+
motion: A11yMotion
|
|
26
|
+
brandAccent: A11yBrandAccent
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const A11Y_STORAGE_KEY = 'softdin_a11y_prefs'
|
|
30
|
+
|
|
31
|
+
export const A11Y_TEXT_SCALE_VALUES: Record<A11yTextScale, number> = {
|
|
32
|
+
xs: 0.75,
|
|
33
|
+
sm: 0.875,
|
|
34
|
+
md: 1,
|
|
35
|
+
lg: 1.125,
|
|
36
|
+
xl: 1.25,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const A11Y_FONT_FAMILY_VALUES: Record<A11yFontFamily, string> = {
|
|
40
|
+
system: "system-ui, 'Segoe UI', Roboto, sans-serif",
|
|
41
|
+
serif: "Georgia, 'Times New Roman', 'Liberation Serif', serif",
|
|
42
|
+
dyslexia: "Verdana, 'Comic Sans MS', 'Trebuchet MS', Arial, sans-serif",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const A11Y_LINE_SPACING_VALUES: Record<
|
|
46
|
+
A11yLineSpacing,
|
|
47
|
+
{ lineHeight: string; letterSpacing: string }
|
|
48
|
+
> = {
|
|
49
|
+
normal: { lineHeight: '1.5', letterSpacing: '0' },
|
|
50
|
+
relaxed: { lineHeight: '1.75', letterSpacing: '0.04em' },
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const DEFAULT_ACCESSIBILITY_PREFERENCES: AccessibilityPreferences = {
|
|
54
|
+
textScale: 'md',
|
|
55
|
+
fontFamily: 'system',
|
|
56
|
+
lineSpacing: 'normal',
|
|
57
|
+
contrast: 'default',
|
|
58
|
+
colorScheme: 'light',
|
|
59
|
+
motion: 'system',
|
|
60
|
+
brandAccent: 'green',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const A11Y_BRAND_ACCENT_PALETTES: Record<
|
|
64
|
+
A11yBrandAccent,
|
|
65
|
+
{ light: BrandAccentPalette; dark: BrandAccentPalette }
|
|
66
|
+
> = {
|
|
67
|
+
green: {
|
|
68
|
+
light: {
|
|
69
|
+
brand50: '#f0fdf4',
|
|
70
|
+
brand100: '#dcfce7',
|
|
71
|
+
brand500: '#22c55e',
|
|
72
|
+
brand600: '#16a34a',
|
|
73
|
+
brand700: '#15803d',
|
|
74
|
+
brand800: '#166534',
|
|
75
|
+
sidebar: '#14532d',
|
|
76
|
+
},
|
|
77
|
+
dark: {
|
|
78
|
+
brand50: '#052e16',
|
|
79
|
+
brand100: '#14532d',
|
|
80
|
+
brand500: '#4ade80',
|
|
81
|
+
brand600: '#22c55e',
|
|
82
|
+
brand700: '#16a34a',
|
|
83
|
+
brand800: '#15803d',
|
|
84
|
+
sidebar: '#052e16',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
blue: {
|
|
88
|
+
light: {
|
|
89
|
+
brand50: '#eff6ff',
|
|
90
|
+
brand100: '#dbeafe',
|
|
91
|
+
brand500: '#3b82f6',
|
|
92
|
+
brand600: '#2563eb',
|
|
93
|
+
brand700: '#1d4ed8',
|
|
94
|
+
brand800: '#1e40af',
|
|
95
|
+
sidebar: '#1e3a8a',
|
|
96
|
+
},
|
|
97
|
+
dark: {
|
|
98
|
+
brand50: '#172554',
|
|
99
|
+
brand100: '#1e3a8a',
|
|
100
|
+
brand500: '#60a5fa',
|
|
101
|
+
brand600: '#3b82f6',
|
|
102
|
+
brand700: '#2563eb',
|
|
103
|
+
brand800: '#1d4ed8',
|
|
104
|
+
sidebar: '#172554',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
red: {
|
|
108
|
+
light: {
|
|
109
|
+
brand50: '#fef2f2',
|
|
110
|
+
brand100: '#fee2e2',
|
|
111
|
+
brand500: '#ef4444',
|
|
112
|
+
brand600: '#dc2626',
|
|
113
|
+
brand700: '#b91c1c',
|
|
114
|
+
brand800: '#991b1b',
|
|
115
|
+
sidebar: '#7f1d1d',
|
|
116
|
+
},
|
|
117
|
+
dark: {
|
|
118
|
+
brand50: '#450a0a',
|
|
119
|
+
brand100: '#7f1d1d',
|
|
120
|
+
brand500: '#f87171',
|
|
121
|
+
brand600: '#ef4444',
|
|
122
|
+
brand700: '#dc2626',
|
|
123
|
+
brand800: '#b91c1c',
|
|
124
|
+
sidebar: '#450a0a',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
purple: {
|
|
128
|
+
light: {
|
|
129
|
+
brand50: '#faf5ff',
|
|
130
|
+
brand100: '#f3e8ff',
|
|
131
|
+
brand500: '#a855f7',
|
|
132
|
+
brand600: '#9333ea',
|
|
133
|
+
brand700: '#7e22ce',
|
|
134
|
+
brand800: '#6b21a8',
|
|
135
|
+
sidebar: '#581c87',
|
|
136
|
+
},
|
|
137
|
+
dark: {
|
|
138
|
+
brand50: '#3b0764',
|
|
139
|
+
brand100: '#581c87',
|
|
140
|
+
brand500: '#c084fc',
|
|
141
|
+
brand600: '#a855f7',
|
|
142
|
+
brand700: '#9333ea',
|
|
143
|
+
brand800: '#7e22ce',
|
|
144
|
+
sidebar: '#3b0764',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
orange: {
|
|
148
|
+
light: {
|
|
149
|
+
brand50: '#fff7ed',
|
|
150
|
+
brand100: '#ffedd5',
|
|
151
|
+
brand500: '#f97316',
|
|
152
|
+
brand600: '#ea580c',
|
|
153
|
+
brand700: '#c2410c',
|
|
154
|
+
brand800: '#9a3412',
|
|
155
|
+
sidebar: '#7c2d12',
|
|
156
|
+
},
|
|
157
|
+
dark: {
|
|
158
|
+
brand50: '#431407',
|
|
159
|
+
brand100: '#7c2d12',
|
|
160
|
+
brand500: '#fb923c',
|
|
161
|
+
brand600: '#f97316',
|
|
162
|
+
brand700: '#ea580c',
|
|
163
|
+
brand800: '#c2410c',
|
|
164
|
+
sidebar: '#431407',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
teal: {
|
|
168
|
+
light: {
|
|
169
|
+
brand50: '#f0fdfa',
|
|
170
|
+
brand100: '#ccfbf1',
|
|
171
|
+
brand500: '#14b8a6',
|
|
172
|
+
brand600: '#0d9488',
|
|
173
|
+
brand700: '#0f766e',
|
|
174
|
+
brand800: '#115e59',
|
|
175
|
+
sidebar: '#134e4a',
|
|
176
|
+
},
|
|
177
|
+
dark: {
|
|
178
|
+
brand50: '#042f2e',
|
|
179
|
+
brand100: '#134e4a',
|
|
180
|
+
brand500: '#2dd4bf',
|
|
181
|
+
brand600: '#14b8a6',
|
|
182
|
+
brand700: '#0d9488',
|
|
183
|
+
brand800: '#0f766e',
|
|
184
|
+
sidebar: '#042f2e',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import { API_BASE_URL } from './config/apiBase'
|
|
3
|
+
import { markBackendReachable } from './utils/connectivity'
|
|
4
|
+
|
|
5
|
+
export const api = axios.create({
|
|
6
|
+
baseURL: API_BASE_URL,
|
|
7
|
+
headers: {
|
|
8
|
+
Accept: 'application/json',
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
function isPublicAuthRequest(config: { method?: string; url?: string }): boolean {
|
|
14
|
+
if (config.method?.toLowerCase() !== 'post') {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const path = config.url ?? ''
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
path.includes('/auth/login')
|
|
22
|
+
|| path.includes('/auth/central-login')
|
|
23
|
+
|| path.includes('/auth/central-admin-login')
|
|
24
|
+
|| path.includes('/auth/register')
|
|
25
|
+
|| path.includes('/auth/forgot-password')
|
|
26
|
+
|| path.includes('/auth/reset-password')
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setupApiInterceptors(
|
|
31
|
+
getToken: () => string | null,
|
|
32
|
+
getTenantSlug: () => string | null,
|
|
33
|
+
onUnauthorized: () => void,
|
|
34
|
+
onTenantMismatch?: () => void,
|
|
35
|
+
): void {
|
|
36
|
+
api.interceptors.request.use((config) => {
|
|
37
|
+
const token = getToken()
|
|
38
|
+
if (token && !isPublicAuthRequest(config)) {
|
|
39
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
40
|
+
} else {
|
|
41
|
+
delete config.headers.Authorization
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const slug = getTenantSlug()
|
|
45
|
+
if (slug && !isPublicAuthRequest(config)) {
|
|
46
|
+
config.headers['X-Tenant'] = slug
|
|
47
|
+
} else {
|
|
48
|
+
delete config.headers['X-Tenant']
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (config.data instanceof FormData) {
|
|
52
|
+
delete config.headers['Content-Type']
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return config
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
api.interceptors.response.use(
|
|
59
|
+
(response) => {
|
|
60
|
+
markBackendReachable()
|
|
61
|
+
return response
|
|
62
|
+
},
|
|
63
|
+
(error) => {
|
|
64
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
65
|
+
// El backend respondió (aunque con error HTTP): la conectividad de red está OK.
|
|
66
|
+
markBackendReachable()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const status = error.response?.status
|
|
70
|
+
const message = error.response?.data?.message
|
|
71
|
+
|
|
72
|
+
if (status === 401 && !isPublicAuthRequest(error.config ?? {})) {
|
|
73
|
+
onUnauthorized()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
status === 403
|
|
78
|
+
&& typeof message === 'string'
|
|
79
|
+
&& message.toLowerCase().includes('token')
|
|
80
|
+
&& message.toLowerCase().includes('tenant')
|
|
81
|
+
&& onTenantMismatch
|
|
82
|
+
) {
|
|
83
|
+
onTenantMismatch()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return Promise.reject(error)
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
}
|
|
Binary file
|