@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.
Files changed (40) hide show
  1. package/README.md +48 -0
  2. package/package.json +50 -0
  3. package/src/a11y/announcer.ts +43 -0
  4. package/src/a11y/apply.ts +83 -0
  5. package/src/a11y/index.ts +31 -0
  6. package/src/a11y/state.ts +40 -0
  7. package/src/a11y/storage.ts +106 -0
  8. package/src/a11y/types.ts +187 -0
  9. package/src/api.ts +89 -0
  10. package/src/assets/softdin-logo.png +0 -0
  11. package/src/auth.ts +134 -0
  12. package/src/components/a11y/A11yLiveRegion.vue +11 -0
  13. package/src/components/a11y/AccessibilityMenuControls.vue +426 -0
  14. package/src/components/a11y/AccessibilityPreferencesPanel.vue +331 -0
  15. package/src/components/a11y/ColorSchemeQuickToggle.vue +105 -0
  16. package/src/components/a11y/SkipLink.vue +41 -0
  17. package/src/components/auth/AuthBrandHeader.vue +129 -0
  18. package/src/components/auth/AuthShell.vue +52 -0
  19. package/src/components/auth/DataProcessingConsentCheckbox.vue +303 -0
  20. package/src/components/auth/DataProcessingConsentForm.vue +111 -0
  21. package/src/components/forms/ErrorAlert.vue +170 -0
  22. package/src/components/forms/FormCancelButton.vue +87 -0
  23. package/src/components/forms/FormCloseButton.vue +114 -0
  24. package/src/components/forms/FormHelpButton.vue +109 -0
  25. package/src/components/forms/FormHelpModal.vue +193 -0
  26. package/src/components/forms/FormMaximizeButton.vue +143 -0
  27. package/src/components/forms/FormRefreshButton.vue +152 -0
  28. package/src/components/forms/FormSaveButton.vue +113 -0
  29. package/src/components/forms/PasswordInput.vue +154 -0
  30. package/src/components/navigation/CatalogListPagination.vue +113 -0
  31. package/src/config/apiBase.ts +37 -0
  32. package/src/lib/catalog-pagination.ts +27 -0
  33. package/src/lib/pagination.ts +86 -0
  34. package/src/lib/softdin-libreria.ts +50 -0
  35. package/src/style.css +278 -0
  36. package/src/types/auth.ts +146 -0
  37. package/src/types/softdin-libreria.d.ts +41 -0
  38. package/src/utils/apiError.ts +55 -0
  39. package/src/utils/connectivity.ts +33 -0
  40. 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