@africanies/angular-web-sdk 0.1.2 → 0.1.4
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/.nx/cache/12276756491762927325/dist/libs/africanies-theme/README.md +46 -0
- package/.nx/cache/12276756491762927325/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +215 -0
- package/.nx/cache/12276756491762927325/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -0
- package/.nx/cache/12276756491762927325/dist/libs/africanies-theme/package.json +35 -0
- package/.nx/cache/12276756491762927325/dist/libs/africanies-theme/tailwind-preset.cjs +245 -0
- package/.nx/cache/12276756491762927325/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +157 -0
- package/.nx/cache/17491371155400585077/dist/libs/africanies-core/README.md +85 -0
- package/.nx/cache/17491371155400585077/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs +4546 -0
- package/.nx/cache/17491371155400585077/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +1 -0
- package/.nx/cache/17491371155400585077/dist/libs/africanies-core/package.json +31 -0
- package/.nx/cache/17491371155400585077/dist/libs/africanies-core/types/africanies-africanies-core.d.ts +2740 -0
- package/.nx/cache/17499961385238663835/dist/libs/africanies-ui/README.md +68 -0
- package/.nx/cache/17499961385238663835/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
- package/.nx/cache/17499961385238663835/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
- package/.nx/cache/17499961385238663835/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +5 -0
- package/.nx/cache/17499961385238663835/dist/libs/africanies-ui/assets/carriers/dhl.svg +7 -0
- package/.nx/cache/17499961385238663835/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +19375 -0
- package/.nx/cache/17499961385238663835/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
- package/.nx/cache/17499961385238663835/dist/libs/africanies-ui/package.json +35 -0
- package/.nx/cache/17499961385238663835/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +5490 -0
- package/.nx/cache/3314955264390318442/dist/libs/africanies-ui/README.md +68 -0
- package/.nx/cache/3314955264390318442/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
- package/.nx/cache/3314955264390318442/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
- package/.nx/cache/3314955264390318442/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +5 -0
- package/.nx/cache/3314955264390318442/dist/libs/africanies-ui/assets/carriers/dhl.svg +7 -0
- package/.nx/cache/3314955264390318442/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +19291 -0
- package/.nx/cache/3314955264390318442/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
- package/.nx/cache/3314955264390318442/dist/libs/africanies-ui/package.json +35 -0
- package/.nx/cache/3314955264390318442/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +5483 -0
- package/.nx/cache/4082520855419352840/dist/libs/africanies-theme/README.md +46 -0
- package/.nx/cache/4082520855419352840/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +221 -0
- package/.nx/cache/4082520855419352840/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -0
- package/.nx/cache/4082520855419352840/dist/libs/africanies-theme/package.json +35 -0
- package/.nx/cache/4082520855419352840/dist/libs/africanies-theme/tailwind-preset.cjs +253 -0
- package/.nx/cache/4082520855419352840/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +166 -0
- package/.nx/cache/run.json +19 -19
- package/.nx/cache/terminalOutputs/12276756491762927325 +21 -0
- package/.nx/cache/terminalOutputs/16279119293212733518 +6 -0
- package/.nx/cache/terminalOutputs/17491371155400585077 +21 -0
- package/.nx/cache/terminalOutputs/17499961385238663835 +21 -0
- package/.nx/cache/terminalOutputs/3017748574492080228 +28 -0
- package/.nx/cache/terminalOutputs/3314955264390318442 +21 -0
- package/.nx/cache/terminalOutputs/4082520855419352840 +21 -0
- package/.nx/cache/terminalOutputs/7126777490381568487 +32 -0
- package/.nx/cache/terminalOutputs/8648260256613557249 +173 -0
- package/.nx/cache/terminalOutputs/9408529484792607601 +6 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db +0 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-shm +0 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-wal +0 -0
- package/.nx/workspace-data/d/daemon.log +3296 -0
- package/.nx/workspace-data/d/server-process.json +2 -2
- package/.nx/workspace-data/file-map.json +3422 -3408
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/project-graph.json +5 -5
- package/.nx/workspace-data/source-maps.json +0 -80
- package/apps/playground/src/app/snippets/action-menu.snippets.ts +1 -0
- package/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs +1 -1
- package/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +1 -1
- package/dist/libs/africanies-core/package.json +3 -3
- package/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +6 -0
- package/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -1
- package/dist/libs/africanies-theme/package.json +3 -3
- package/dist/libs/africanies-theme/tailwind-preset.cjs +15 -7
- package/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +10 -1
- package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +456 -169
- package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -1
- package/dist/libs/africanies-ui/package.json +5 -5
- package/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +111 -31
- package/libs/africanies-core/package.json +3 -3
- package/libs/africanies-core/src/lib/payment-method/payment-method.mapper.spec.ts +15 -0
- package/libs/africanies-core/src/lib/payment-method/payment-method.mapper.ts +1 -2
- package/libs/africanies-theme/package.json +3 -3
- package/libs/africanies-theme/src/lib/mode-color.safelist.ts +4 -0
- package/libs/africanies-theme/src/lib/mode-color.service.ts +11 -0
- package/libs/africanies-theme/tailwind-preset.cjs +15 -7
- package/libs/africanies-ui/package.json +5 -5
- package/libs/africanies-ui/src/index.ts +6 -0
- package/libs/africanies-ui/src/lib/accordion/accordion.component.ts +135 -0
- package/libs/africanies-ui/src/lib/accordion/index.ts +7 -0
- package/libs/africanies-ui/src/lib/action-menu/action-menu.component.ts +84 -17
- package/libs/africanies-ui/src/lib/action-menu/menu-item.ts +19 -2
- package/libs/africanies-ui/src/lib/layout/header-weather.spec.ts +111 -6
- package/libs/africanies-ui/src/lib/layout/header-weather.ts +104 -8
- package/libs/africanies-ui/src/lib/modules/africanies-feedback.module.ts +3 -1
- package/libs/africanies-ui/src/lib/table/table.component.ts +67 -44
- package/package.json +1 -1
|
@@ -24,9 +24,34 @@ describe('mapWmoWeatherCode', () => {
|
|
|
24
24
|
|
|
25
25
|
describe('loadHeaderWeather', () => {
|
|
26
26
|
const originalFetch = globalThis.fetch;
|
|
27
|
+
const originalGeolocation = navigator.geolocation;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
Object.defineProperty(navigator, 'geolocation', {
|
|
31
|
+
configurable: true,
|
|
32
|
+
value: {
|
|
33
|
+
getCurrentPosition: (
|
|
34
|
+
_success: PositionCallback,
|
|
35
|
+
error?: PositionErrorCallback,
|
|
36
|
+
) => {
|
|
37
|
+
error?.({
|
|
38
|
+
code: 1,
|
|
39
|
+
message: 'denied',
|
|
40
|
+
PERMISSION_DENIED: 1,
|
|
41
|
+
POSITION_UNAVAILABLE: 2,
|
|
42
|
+
TIMEOUT: 3,
|
|
43
|
+
} as GeolocationPositionError);
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
27
48
|
|
|
28
49
|
afterEach(() => {
|
|
29
50
|
globalThis.fetch = originalFetch;
|
|
51
|
+
Object.defineProperty(navigator, 'geolocation', {
|
|
52
|
+
configurable: true,
|
|
53
|
+
value: originalGeolocation,
|
|
54
|
+
});
|
|
30
55
|
sessionStorage.clear();
|
|
31
56
|
});
|
|
32
57
|
|
|
@@ -41,22 +66,51 @@ describe('loadHeaderWeather', () => {
|
|
|
41
66
|
).resolves.toBeNull();
|
|
42
67
|
});
|
|
43
68
|
|
|
44
|
-
it('reads Open-Meteo after IP geolocation', async () => {
|
|
69
|
+
it('reads Open-Meteo after IP geolocation and reverse-geocodes the city', async () => {
|
|
45
70
|
const fetchFn = jest.fn(async (url: string) => {
|
|
46
71
|
if (String(url).includes('geojs')) {
|
|
47
|
-
return jsonResponse({
|
|
72
|
+
return jsonResponse({
|
|
73
|
+
latitude: '9.0765',
|
|
74
|
+
longitude: '7.3986',
|
|
75
|
+
city: 'Lagos',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (String(url).includes('bigdatacloud')) {
|
|
79
|
+
return jsonResponse({ city: 'Abuja', locality: 'Abuja' });
|
|
48
80
|
}
|
|
49
81
|
return jsonResponse({
|
|
50
82
|
current: { weather_code: 61, temperature_2m: 27.4 },
|
|
51
83
|
});
|
|
52
84
|
});
|
|
53
85
|
|
|
54
|
-
await expect(
|
|
86
|
+
await expect(
|
|
87
|
+
loadHeaderWeather(fetchFn as unknown as typeof fetch),
|
|
88
|
+
).resolves.toEqual({
|
|
55
89
|
kind: 'rain',
|
|
56
90
|
temperatureC: 27.4,
|
|
91
|
+
city: 'Abuja',
|
|
92
|
+
});
|
|
93
|
+
expect(fetchFn).toHaveBeenCalledTimes(3);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('falls back to the IP city when reverse geocode is empty', async () => {
|
|
97
|
+
const fetchFn = jest.fn(async (url: string) => {
|
|
98
|
+
if (String(url).includes('geojs')) {
|
|
99
|
+
return jsonResponse({ latitude: 1, longitude: 2, city: 'Lagos' });
|
|
100
|
+
}
|
|
101
|
+
if (String(url).includes('bigdatacloud')) {
|
|
102
|
+
return jsonResponse({});
|
|
103
|
+
}
|
|
104
|
+
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30 } });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await expect(
|
|
108
|
+
loadHeaderWeather(fetchFn as unknown as typeof fetch),
|
|
109
|
+
).resolves.toEqual({
|
|
110
|
+
kind: 'clear',
|
|
111
|
+
temperatureC: 30,
|
|
57
112
|
city: 'Lagos',
|
|
58
113
|
});
|
|
59
|
-
expect(fetchFn).toHaveBeenCalledTimes(2);
|
|
60
114
|
});
|
|
61
115
|
|
|
62
116
|
it('reuses the same-hour session cache', async () => {
|
|
@@ -64,6 +118,9 @@ describe('loadHeaderWeather', () => {
|
|
|
64
118
|
if (String(url).includes('geojs')) {
|
|
65
119
|
return jsonResponse({ latitude: 1, longitude: 2 });
|
|
66
120
|
}
|
|
121
|
+
if (String(url).includes('bigdatacloud')) {
|
|
122
|
+
return jsonResponse({ city: 'Accra' });
|
|
123
|
+
}
|
|
67
124
|
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30 } });
|
|
68
125
|
});
|
|
69
126
|
|
|
@@ -71,7 +128,7 @@ describe('loadHeaderWeather', () => {
|
|
|
71
128
|
const second = await loadHeaderWeather(fetchFn as unknown as typeof fetch);
|
|
72
129
|
|
|
73
130
|
expect(first).toEqual(second);
|
|
74
|
-
expect(fetchFn).toHaveBeenCalledTimes(
|
|
131
|
+
expect(fetchFn).toHaveBeenCalledTimes(3);
|
|
75
132
|
});
|
|
76
133
|
|
|
77
134
|
it('fetches again when the cached hour no longer matches', async () => {
|
|
@@ -79,6 +136,9 @@ describe('loadHeaderWeather', () => {
|
|
|
79
136
|
if (String(url).includes('geojs')) {
|
|
80
137
|
return jsonResponse({ latitude: 1, longitude: 2, city: 'Lagos' });
|
|
81
138
|
}
|
|
139
|
+
if (String(url).includes('bigdatacloud')) {
|
|
140
|
+
return jsonResponse({ city: 'Lagos' });
|
|
141
|
+
}
|
|
82
142
|
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30.2 } });
|
|
83
143
|
});
|
|
84
144
|
|
|
@@ -94,7 +154,52 @@ describe('loadHeaderWeather', () => {
|
|
|
94
154
|
sessionStorage.setItem(cacheKey!, JSON.stringify(stored));
|
|
95
155
|
|
|
96
156
|
await loadHeaderWeather(fetchFn as unknown as typeof fetch);
|
|
97
|
-
expect(fetchFn).toHaveBeenCalledTimes(
|
|
157
|
+
expect(fetchFn).toHaveBeenCalledTimes(6);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('uses browser coordinates when geolocation succeeds', async () => {
|
|
161
|
+
Object.defineProperty(navigator, 'geolocation', {
|
|
162
|
+
configurable: true,
|
|
163
|
+
value: {
|
|
164
|
+
getCurrentPosition: (success: PositionCallback) => {
|
|
165
|
+
success({
|
|
166
|
+
coords: {
|
|
167
|
+
latitude: 9.0765,
|
|
168
|
+
longitude: 7.3986,
|
|
169
|
+
accuracy: 10,
|
|
170
|
+
altitude: null,
|
|
171
|
+
altitudeAccuracy: null,
|
|
172
|
+
heading: null,
|
|
173
|
+
speed: null,
|
|
174
|
+
toJSON: () => ({}),
|
|
175
|
+
},
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
toJSON: () => ({}),
|
|
178
|
+
} as GeolocationPosition);
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const fetchFn = jest.fn(async (url: string) => {
|
|
184
|
+
if (String(url).includes('bigdatacloud')) {
|
|
185
|
+
return jsonResponse({ city: 'Abuja' });
|
|
186
|
+
}
|
|
187
|
+
if (String(url).includes('geojs')) {
|
|
188
|
+
throw new Error('should not call IP geo');
|
|
189
|
+
}
|
|
190
|
+
return jsonResponse({ current: { weather_code: 0, temperature_2m: 31 } });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await expect(
|
|
194
|
+
loadHeaderWeather(fetchFn as unknown as typeof fetch),
|
|
195
|
+
).resolves.toEqual({
|
|
196
|
+
kind: 'clear',
|
|
197
|
+
temperatureC: 31,
|
|
198
|
+
city: 'Abuja',
|
|
199
|
+
});
|
|
200
|
+
expect(
|
|
201
|
+
fetchFn.mock.calls.some((call) => String(call[0]).includes('geojs')),
|
|
202
|
+
).toBe(false);
|
|
98
203
|
});
|
|
99
204
|
});
|
|
100
205
|
|
|
@@ -2,9 +2,15 @@ import type { IconName } from '@africanies/africanies-icons';
|
|
|
2
2
|
|
|
3
3
|
import type { HeaderWeather, HeaderWeatherKind } from './header-greeting.util';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Bump when cache shape or city resolution strategy changes. */
|
|
6
|
+
const CACHE_KEY = 'africanies-header-weather-v4';
|
|
6
7
|
const FETCH_MS = 4_000;
|
|
8
|
+
const BROWSER_GEO_MS = 3_500;
|
|
9
|
+
/** IP fallback — city string is often wrong (e.g. NG IPs labelled Lagos). */
|
|
7
10
|
const GEO_URL = 'https://get.geojs.io/v1/ip/geo.json';
|
|
11
|
+
/** Client reverse-geocode so the label matches the forecast coordinates. */
|
|
12
|
+
const REVERSE_GEO_URL =
|
|
13
|
+
'https://api.bigdatacloud.net/data/reverse-geocode-client';
|
|
8
14
|
|
|
9
15
|
const WEATHER_LABELS: Record<HeaderWeatherKind, string> = {
|
|
10
16
|
clear: 'Clear',
|
|
@@ -29,6 +35,12 @@ interface GeoJsPayload {
|
|
|
29
35
|
longitude?: string | number;
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
interface ReverseGeoPayload {
|
|
39
|
+
city?: string;
|
|
40
|
+
locality?: string;
|
|
41
|
+
principalSubdivision?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
interface OpenMeteoPayload {
|
|
33
45
|
current?: {
|
|
34
46
|
weather_code?: number;
|
|
@@ -36,6 +48,11 @@ interface OpenMeteoPayload {
|
|
|
36
48
|
};
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
interface Coordinates {
|
|
52
|
+
latitude: number;
|
|
53
|
+
longitude: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
/**
|
|
40
57
|
* Map Open-Meteo / WMO weather codes to a greeting flavor.
|
|
41
58
|
*
|
|
@@ -95,7 +112,12 @@ export function headerWeatherIcon(kind: HeaderWeatherKind, hour: number): IconNa
|
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
/**
|
|
98
|
-
* City-level forecast via IP geolocation + Open-Meteo (no API key).
|
|
115
|
+
* City-level forecast via browser / IP geolocation + Open-Meteo (no API key).
|
|
116
|
+
*
|
|
117
|
+
* Coordinates prefer the browser Geolocation API (accurate for travellers;
|
|
118
|
+
* may prompt once). Otherwise IP geo is used. The city label is reverse-
|
|
119
|
+
* geocoded from those coordinates — IP `city` strings are often wrong for
|
|
120
|
+
* Nigeria (many networks resolve as Lagos).
|
|
99
121
|
*
|
|
100
122
|
* Fails closed: missing browser APIs, timeouts, and HTTP errors all return
|
|
101
123
|
* `null` so the greeting can stay time-of-day only. Cached per local hour
|
|
@@ -119,16 +141,14 @@ export async function loadHeaderWeather(
|
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
try {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
const longitude = asNumber(geo?.longitude);
|
|
125
|
-
if (latitude === null || longitude === null) {
|
|
144
|
+
const coords = await resolveCoordinates(run);
|
|
145
|
+
if (!coords) {
|
|
126
146
|
return null;
|
|
127
147
|
}
|
|
128
148
|
|
|
129
149
|
const forecastUrl =
|
|
130
150
|
`https://api.open-meteo.com/v1/forecast` +
|
|
131
|
-
`?latitude=${latitude}&longitude=${longitude}` +
|
|
151
|
+
`?latitude=${coords.latitude}&longitude=${coords.longitude}` +
|
|
132
152
|
`¤t=weather_code,temperature_2m`;
|
|
133
153
|
const forecast = (await fetchJson(run, forecastUrl)) as OpenMeteoPayload | null;
|
|
134
154
|
const code = asNumber(forecast?.current?.weather_code);
|
|
@@ -142,7 +162,9 @@ export async function loadHeaderWeather(
|
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
const temperatureC = asNumber(forecast?.current?.temperature_2m) ?? undefined;
|
|
145
|
-
const city =
|
|
165
|
+
const city =
|
|
166
|
+
(await reverseGeocodeCity(run, coords.latitude, coords.longitude)) ??
|
|
167
|
+
coords.fallbackCity;
|
|
146
168
|
const weather: HeaderWeather = { kind, temperatureC, city };
|
|
147
169
|
writeCache({ hour, kind, temperatureC, city });
|
|
148
170
|
return weather;
|
|
@@ -187,6 +209,80 @@ function writeCache(value: CachedWeather): void {
|
|
|
187
209
|
}
|
|
188
210
|
}
|
|
189
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Prefer device coordinates when available (may prompt once); otherwise fall
|
|
214
|
+
* back to IP geo. IP city labels for Nigeria are often wrong — reverse
|
|
215
|
+
* geocoding from these coordinates still depends on accurate lat/lon.
|
|
216
|
+
*/
|
|
217
|
+
async function resolveCoordinates(
|
|
218
|
+
fetchFn: typeof fetch,
|
|
219
|
+
): Promise<(Coordinates & { fallbackCity?: string }) | null> {
|
|
220
|
+
const browser = await tryBrowserGeolocation();
|
|
221
|
+
if (browser) {
|
|
222
|
+
return browser;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const geo = (await fetchJson(fetchFn, GEO_URL)) as GeoJsPayload | null;
|
|
226
|
+
const latitude = asNumber(geo?.latitude);
|
|
227
|
+
const longitude = asNumber(geo?.longitude);
|
|
228
|
+
if (latitude === null || longitude === null) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
latitude,
|
|
233
|
+
longitude,
|
|
234
|
+
fallbackCity: asCity(geo?.city),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function tryBrowserGeolocation(): Promise<Coordinates | null> {
|
|
239
|
+
if (
|
|
240
|
+
typeof navigator === 'undefined' ||
|
|
241
|
+
typeof navigator.geolocation?.getCurrentPosition !== 'function'
|
|
242
|
+
) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return await new Promise((resolve) => {
|
|
247
|
+
const timer = setTimeout(() => resolve(null), BROWSER_GEO_MS);
|
|
248
|
+
navigator.geolocation.getCurrentPosition(
|
|
249
|
+
(position) => {
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
resolve({
|
|
252
|
+
latitude: position.coords.latitude,
|
|
253
|
+
longitude: position.coords.longitude,
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
() => {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
resolve(null);
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
enableHighAccuracy: false,
|
|
262
|
+
maximumAge: 600_000,
|
|
263
|
+
timeout: BROWSER_GEO_MS,
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function reverseGeocodeCity(
|
|
270
|
+
fetchFn: typeof fetch,
|
|
271
|
+
latitude: number,
|
|
272
|
+
longitude: number,
|
|
273
|
+
): Promise<string | undefined> {
|
|
274
|
+
const url =
|
|
275
|
+
`${REVERSE_GEO_URL}?latitude=${encodeURIComponent(String(latitude))}` +
|
|
276
|
+
`&longitude=${encodeURIComponent(String(longitude))}` +
|
|
277
|
+
`&localityLanguage=en`;
|
|
278
|
+
const payload = (await fetchJson(fetchFn, url)) as ReverseGeoPayload | null;
|
|
279
|
+
return (
|
|
280
|
+
asCity(payload?.city) ??
|
|
281
|
+
asCity(payload?.locality) ??
|
|
282
|
+
asCity(payload?.principalSubdivision)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
190
286
|
function asNumber(value: unknown): number | null {
|
|
191
287
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
192
288
|
return value;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NgModule } from '@angular/core';
|
|
2
2
|
|
|
3
|
+
import { AccordionComponent } from '../accordion';
|
|
3
4
|
import { AlertComponent } from '../alert';
|
|
4
5
|
import { ChipComponent } from '../chip';
|
|
5
6
|
import {
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
} from '../feedback';
|
|
12
13
|
|
|
13
14
|
const FEEDBACK = [
|
|
15
|
+
AccordionComponent,
|
|
14
16
|
LoadingStateComponent,
|
|
15
17
|
ErrorStateComponent,
|
|
16
18
|
ErrorIndicatorComponent,
|
|
@@ -21,7 +23,7 @@ const FEEDBACK = [
|
|
|
21
23
|
] as const;
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
* Async feedback states, inline alert, and status chips.
|
|
26
|
+
* Async feedback states, accordion panels, inline alert, and status chips.
|
|
25
27
|
*
|
|
26
28
|
* Toast stays out of this module — register {@link provideAfricaniesToasts} instead.
|
|
27
29
|
*
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Component,
|
|
6
6
|
computed,
|
|
7
7
|
contentChildren,
|
|
8
|
+
inject,
|
|
8
9
|
input,
|
|
9
10
|
numberAttribute,
|
|
10
11
|
output,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
|
|
15
16
|
import { AfricaniesIconComponent } from '@africanies/africanies-icons';
|
|
16
17
|
import type { PaginationMetaModel } from '@africanies/africanies-models';
|
|
18
|
+
import { ModeColorService } from '@africanies/africanies-theme';
|
|
17
19
|
|
|
18
20
|
import { ButtonComponent } from '../button/button.component';
|
|
19
21
|
import { EmptyStateComponent } from '../feedback/empty-state.component';
|
|
@@ -98,7 +100,8 @@ import { TableColumn, TableSortChange } from './table-column';
|
|
|
98
100
|
* **Sticky columns:** a column with `key: 'actions'` stays pinned to the
|
|
99
101
|
* right while the grid scrolls horizontally. The expand trigger column pins
|
|
100
102
|
* to the left when rows are expandable. Override with
|
|
101
|
-
* {@link TableColumn.sticky}.
|
|
103
|
+
* {@link TableColumn.sticky}. Sticky columns use a slightly thicker bottom
|
|
104
|
+
* border (and opaque fill) so dividers stay visible while scrolling.
|
|
102
105
|
*/
|
|
103
106
|
@Component({
|
|
104
107
|
selector: 'africanies-table',
|
|
@@ -210,9 +213,7 @@ import { TableColumn, TableSortChange } from './table-column';
|
|
|
210
213
|
<table
|
|
211
214
|
class="w-max min-w-full table-auto border-separate border-spacing-0 bg-inherit text-left text-body text-ink dark:text-white"
|
|
212
215
|
>
|
|
213
|
-
<thead
|
|
214
|
-
class="border-b border-border bg-background-welcome dark:border-white/10 dark:bg-ink-950"
|
|
215
|
-
>
|
|
216
|
+
<thead [class]="headerRowClass()">
|
|
216
217
|
<tr>
|
|
217
218
|
@if (isExpandable()) {
|
|
218
219
|
<th
|
|
@@ -276,14 +277,14 @@ import { TableColumn, TableSortChange } from './table-column';
|
|
|
276
277
|
@switch (bodyKind()) {
|
|
277
278
|
@case ('loading') {
|
|
278
279
|
<tr>
|
|
279
|
-
<td class="px-
|
|
280
|
+
<td class="px-[1.125rem] py-6" [attr.colspan]="colSpan()">
|
|
280
281
|
<africanies-loading-state [message]="loadingLabel()" />
|
|
281
282
|
</td>
|
|
282
283
|
</tr>
|
|
283
284
|
}
|
|
284
285
|
@case ('error') {
|
|
285
286
|
<tr>
|
|
286
|
-
<td class="px-
|
|
287
|
+
<td class="px-[1.125rem] py-6" [attr.colspan]="colSpan()">
|
|
287
288
|
<africanies-error-state
|
|
288
289
|
[message]="errorMessage()"
|
|
289
290
|
[refreshing]="refreshing()"
|
|
@@ -294,7 +295,7 @@ import { TableColumn, TableSortChange } from './table-column';
|
|
|
294
295
|
}
|
|
295
296
|
@case ('empty') {
|
|
296
297
|
<tr>
|
|
297
|
-
<td class="px-
|
|
298
|
+
<td class="px-[1.125rem] py-6" [attr.colspan]="colSpan()">
|
|
298
299
|
<africanies-empty-state
|
|
299
300
|
[message]="emptyMessage()"
|
|
300
301
|
[refreshing]="refreshing()"
|
|
@@ -362,7 +363,7 @@ import { TableColumn, TableSortChange } from './table-column';
|
|
|
362
363
|
class="min-h-0 overflow-hidden border-t border-border/70 bg-neutral-50 dark:border-white/10 dark:bg-white/[0.05]"
|
|
363
364
|
>
|
|
364
365
|
<dl
|
|
365
|
-
class="m-0 grid gap-x-6 gap-y-3 px-
|
|
366
|
+
class="m-0 grid gap-x-6 gap-y-3 px-[1.125rem] py-4 transition-opacity duration-200 ease-out motion-reduce:transition-none sm:grid-cols-2 lg:grid-cols-3"
|
|
366
367
|
[class.opacity-0]="!isRowExpanded(row, i)"
|
|
367
368
|
[class.opacity-100]="isRowExpanded(row, i)"
|
|
368
369
|
>
|
|
@@ -409,6 +410,8 @@ import { TableColumn, TableSortChange } from './table-column';
|
|
|
409
410
|
`,
|
|
410
411
|
})
|
|
411
412
|
export class TableComponent<T = unknown> {
|
|
413
|
+
protected readonly modeColor = inject(ModeColorService);
|
|
414
|
+
|
|
412
415
|
/**
|
|
413
416
|
* When false, hides row expansion even if {@link RowDetailDefDirective}
|
|
414
417
|
* templates are projected. Defaults to true.
|
|
@@ -652,6 +655,23 @@ export class TableComponent<T = unknown> {
|
|
|
652
655
|
return { $implicit: row };
|
|
653
656
|
}
|
|
654
657
|
|
|
658
|
+
/**
|
|
659
|
+
* Header row chrome. Soft fill lives on each `th` ({@link headerSoftClass})
|
|
660
|
+
* so sticky columns do not double-tint and paint over neighbor labels.
|
|
661
|
+
* @returns Header row class list.
|
|
662
|
+
*/
|
|
663
|
+
protected headerRowClass(): string {
|
|
664
|
+
return 'border-b border-border dark:border-white/10';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Opaque mode soft fill for header cells (sticky-safe in dark mode).
|
|
669
|
+
* @returns Soft solid background utilities.
|
|
670
|
+
*/
|
|
671
|
+
protected headerSoftClass(): string {
|
|
672
|
+
return this.modeColor.classes().softSolid;
|
|
673
|
+
}
|
|
674
|
+
|
|
655
675
|
/**
|
|
656
676
|
* Body row classes — row marker for hover styles, plus hide cell borders on
|
|
657
677
|
* the last / expanded row.
|
|
@@ -662,7 +682,7 @@ export class TableComponent<T = unknown> {
|
|
|
662
682
|
protected bodyRowClass(row: T, index: number): string {
|
|
663
683
|
if (this.isExpandable() && this.isRowExpanded(row, index)) {
|
|
664
684
|
// Only the bottom edge — keep the top divider so the row still
|
|
665
|
-
// separates from the row above.
|
|
685
|
+
// separates from the row above. Detail panel owns the separator.
|
|
666
686
|
return 'africanies-table-row group [&>td]:border-b-0';
|
|
667
687
|
}
|
|
668
688
|
return 'africanies-table-row group last:[&>td]:border-b-0';
|
|
@@ -691,25 +711,31 @@ export class TableComponent<T = unknown> {
|
|
|
691
711
|
*/
|
|
692
712
|
protected expandHeaderClass(): string {
|
|
693
713
|
return (
|
|
694
|
-
'w-10 border-b border-border px-2 py-
|
|
714
|
+
'w-10 border-b-[1.5px] border-border px-2 py-4 dark:border-white/10 ' +
|
|
715
|
+
this.headerSoftClass() +
|
|
716
|
+
' ' +
|
|
695
717
|
this.stickyPinClass('left', 'head')
|
|
696
718
|
);
|
|
697
719
|
}
|
|
698
720
|
|
|
699
721
|
/**
|
|
700
722
|
* Body classes for the expand/collapse trigger column (pinned left).
|
|
701
|
-
*
|
|
702
|
-
*
|
|
723
|
+
* Uses a slightly thicker bottom border than scrolling cells so the
|
|
724
|
+
* sticky fill still reads a clear divider (no top-shadow fallback).
|
|
703
725
|
* @param index - Index within {@link rows}.
|
|
704
726
|
* @returns Body cell class list.
|
|
705
727
|
*/
|
|
706
728
|
protected expandBodyClass(index: number): string {
|
|
707
|
-
const
|
|
708
|
-
const
|
|
709
|
-
|
|
729
|
+
const row = this.rowList()[index];
|
|
730
|
+
const expanded =
|
|
731
|
+
row !== undefined && this.isRowExpanded(row, index);
|
|
732
|
+
const border = expanded
|
|
733
|
+
? ''
|
|
734
|
+
: 'border-b-[1.5px] border-border dark:border-white/10 ';
|
|
710
735
|
return (
|
|
711
|
-
'w-10 px-2 py-
|
|
712
|
-
|
|
736
|
+
'w-10 px-2 py-4 align-middle ' +
|
|
737
|
+
border +
|
|
738
|
+
this.stickyPinClass('left', 'body')
|
|
713
739
|
);
|
|
714
740
|
}
|
|
715
741
|
|
|
@@ -719,37 +745,46 @@ export class TableComponent<T = unknown> {
|
|
|
719
745
|
* @returns Header class list.
|
|
720
746
|
*/
|
|
721
747
|
protected headerCellClass(col: TableColumn<T>): string {
|
|
748
|
+
const sticky = this.stickyEdge(col);
|
|
749
|
+
const border = sticky
|
|
750
|
+
? 'border-b-[1.5px] '
|
|
751
|
+
: 'border-b ';
|
|
722
752
|
return (
|
|
723
|
-
|
|
753
|
+
border +
|
|
754
|
+
'border-border whitespace-normal wrap-break-word px-[1.125rem] py-4 font-medium text-neutral-600 dark:border-white/10 dark:text-neutral-400 ' +
|
|
755
|
+
this.headerSoftClass() +
|
|
756
|
+
' ' +
|
|
724
757
|
this.stickySurfaceClass(col, 'head')
|
|
725
758
|
);
|
|
726
759
|
}
|
|
727
760
|
|
|
728
761
|
/**
|
|
729
762
|
* Body cell classes, including sticky pin when set.
|
|
730
|
-
* Sticky
|
|
731
|
-
*
|
|
732
|
-
* same pixel and read brighter (especially in dark mode).
|
|
763
|
+
* Sticky expand/actions columns use a 1.5px bottom border; scrolling cells
|
|
764
|
+
* stay at 1px. Expanded rows drop the bottom border (see {@link bodyRowClass}).
|
|
733
765
|
* @param col - Column definition.
|
|
734
766
|
* @param index - Index within {@link rows}.
|
|
735
767
|
* @returns Body cell class list.
|
|
736
768
|
*/
|
|
737
769
|
protected bodyCellClass(col: TableColumn<T>, index: number): string {
|
|
738
|
-
const
|
|
770
|
+
const row = this.rowList()[index];
|
|
771
|
+
const expanded =
|
|
772
|
+
row !== undefined && this.isRowExpanded(row, index);
|
|
773
|
+
const sticky = this.stickyEdge(col);
|
|
774
|
+
const rowLine = expanded
|
|
739
775
|
? ''
|
|
740
|
-
:
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
prev !== undefined && this.isRowExpanded(prev, index - 1);
|
|
776
|
+
: sticky
|
|
777
|
+
? 'border-b-[1.5px] border-border dark:border-white/10 '
|
|
778
|
+
: 'border-b border-border dark:border-white/10 ';
|
|
744
779
|
const wrap =
|
|
745
780
|
col.key === 'actions'
|
|
746
781
|
? 'whitespace-nowrap '
|
|
747
782
|
: 'whitespace-normal wrap-break-word ';
|
|
748
783
|
return (
|
|
749
784
|
rowLine +
|
|
750
|
-
'px-
|
|
785
|
+
'px-[1.125rem] py-4 align-middle ' +
|
|
751
786
|
wrap +
|
|
752
|
-
this.stickySurfaceClass(col, 'body'
|
|
787
|
+
this.stickySurfaceClass(col, 'body')
|
|
753
788
|
);
|
|
754
789
|
}
|
|
755
790
|
|
|
@@ -757,30 +792,22 @@ export class TableComponent<T = unknown> {
|
|
|
757
792
|
* Sticky pin utilities for a known edge (expand column or data column).
|
|
758
793
|
* @param edge - Left or right pin.
|
|
759
794
|
* @param surface - Header vs body fill tokens.
|
|
760
|
-
* @param index - Body row index; unused for the header.
|
|
761
|
-
* @param afterExpanded - Previous row is expanded — skip the top shadow
|
|
762
|
-
* divider so the sticky column stays flush under the detail panel.
|
|
763
795
|
* @returns Sticky utilities.
|
|
764
796
|
*/
|
|
765
797
|
protected stickyPinClass(
|
|
766
798
|
edge: 'left' | 'right',
|
|
767
799
|
surface: 'head' | 'body',
|
|
768
|
-
index = 0,
|
|
769
|
-
afterExpanded = false,
|
|
770
800
|
): string {
|
|
771
801
|
// Expand (left) sits above scrolling cells; actions (right) match.
|
|
772
802
|
// Header needs a higher z so it stays above sticky body cells.
|
|
773
803
|
const side =
|
|
774
804
|
edge === 'right' ? 'sticky right-0 z-[1]' : 'sticky left-0 z-[1]';
|
|
775
805
|
if (surface === 'head') {
|
|
776
|
-
|
|
806
|
+
// Fill comes from {@link headerSoftClass} on the cell — do not re-tint.
|
|
807
|
+
return `${side} z-[2]`;
|
|
777
808
|
}
|
|
778
|
-
const rowDivider =
|
|
779
|
-
!afterExpanded && index > 0
|
|
780
|
-
? 'shadow-[0_-1px_0_0_#c9d5e1] dark:shadow-[0_-1px_0_0_rgba(255,255,255,0.1)]'
|
|
781
|
-
: '';
|
|
782
809
|
const fill = 'bg-white dark:bg-ink';
|
|
783
|
-
return `${side} ${fill}
|
|
810
|
+
return `${side} ${fill}`;
|
|
784
811
|
}
|
|
785
812
|
|
|
786
813
|
/**
|
|
@@ -788,21 +815,17 @@ export class TableComponent<T = unknown> {
|
|
|
788
815
|
* cells do not show through.
|
|
789
816
|
* @param col - Column definition.
|
|
790
817
|
* @param surface - Header vs body fill tokens.
|
|
791
|
-
* @param index - Body row index; unused for the header.
|
|
792
|
-
* @param afterExpanded - Previous row is expanded — skip the top shadow.
|
|
793
818
|
* @returns Sticky utilities, or empty when the column scrolls.
|
|
794
819
|
*/
|
|
795
820
|
protected stickySurfaceClass(
|
|
796
821
|
col: TableColumn<T>,
|
|
797
822
|
surface: 'head' | 'body',
|
|
798
|
-
index = 0,
|
|
799
|
-
afterExpanded = false,
|
|
800
823
|
): string {
|
|
801
824
|
const edge = this.stickyEdge(col);
|
|
802
825
|
if (!edge) {
|
|
803
826
|
return '';
|
|
804
827
|
}
|
|
805
|
-
return this.stickyPinClass(edge, surface
|
|
828
|
+
return this.stickyPinClass(edge, surface);
|
|
806
829
|
}
|
|
807
830
|
|
|
808
831
|
/**
|