@africanies/angular-web-sdk 0.1.9 → 0.1.11

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 (70) hide show
  1. package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/README.md +46 -0
  2. package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +243 -0
  3. package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -0
  4. package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/package.json +35 -0
  5. package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/tailwind-preset.cjs +330 -0
  6. package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +178 -0
  7. package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/README.md +68 -0
  8. package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
  9. package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
  10. package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +5 -0
  11. package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/assets/carriers/dhl.svg +7 -0
  12. package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +20013 -0
  13. package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
  14. package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/package.json +35 -0
  15. package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +5617 -0
  16. package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/README.md +68 -0
  17. package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
  18. package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
  19. package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +5 -0
  20. package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/assets/carriers/dhl.svg +7 -0
  21. package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +19998 -0
  22. package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
  23. package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/package.json +35 -0
  24. package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +5616 -0
  25. package/.nx/cache/run.json +18 -18
  26. package/.nx/cache/terminalOutputs/1149535958171016935 +21 -0
  27. package/.nx/cache/terminalOutputs/14892813664845064572 +21 -0
  28. package/.nx/cache/terminalOutputs/17909810599991457481 +40 -0
  29. package/.nx/cache/terminalOutputs/2927637324428060203 +42 -0
  30. package/.nx/cache/terminalOutputs/3735952477975556635 +21 -0
  31. package/.nx/cache/terminalOutputs/4787481949164239892 +8 -0
  32. package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db +0 -0
  33. package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-shm +0 -0
  34. package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-wal +0 -0
  35. package/.nx/workspace-data/d/daemon.log +3807 -0
  36. package/.nx/workspace-data/d/server-process.json +2 -2
  37. package/.nx/workspace-data/file-map.json +3254 -3260
  38. package/.nx/workspace-data/nx_files.nxt +0 -0
  39. package/.nx/workspace-data/project-graph.json +2 -2
  40. package/README.md +0 -17
  41. package/apps/playground/src/app/pages/chip-page.ts +11 -1
  42. package/apps/playground/src/app/pages/tokens-page.ts +2 -2
  43. package/apps/playground/src/app/snippets/chip.snippets.ts +5 -0
  44. package/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +8 -8
  45. package/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -1
  46. package/dist/libs/africanies-theme/tailwind-preset.cjs +3 -3
  47. package/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +5 -3
  48. package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +986 -473
  49. package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -1
  50. package/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +135 -39
  51. package/libs/africanies-theme/src/lib/mode-color.safelist.ts +2 -2
  52. package/libs/africanies-theme/src/lib/mode-color.service.spec.ts +9 -7
  53. package/libs/africanies-theme/src/lib/mode-color.service.ts +9 -7
  54. package/libs/africanies-theme/tailwind-preset.cjs +3 -3
  55. package/libs/africanies-ui/src/lib/avatar/avatar-menu.component.ts +25 -21
  56. package/libs/africanies-ui/src/lib/button/button.component.spec.ts +13 -8
  57. package/libs/africanies-ui/src/lib/chip/chip.component.ts +74 -6
  58. package/libs/africanies-ui/src/lib/layout/app-shell-header.component.ts +35 -12
  59. package/libs/africanies-ui/src/lib/layout/header-greeting.util.ts +2 -0
  60. package/libs/africanies-ui/src/lib/layout/header-weather.spec.ts +120 -9
  61. package/libs/africanies-ui/src/lib/layout/header-weather.ts +79 -18
  62. package/libs/africanies-ui/src/lib/navigation/segment/segment.component.ts +98 -22
  63. package/libs/africanies-ui/src/lib/navigation/shipping-mode-switch/shipping-mode-switch.component.spec.ts +15 -0
  64. package/libs/africanies-ui/src/lib/navigation/shipping-mode-switch/shipping-mode-switch.component.ts +195 -64
  65. package/libs/africanies-ui/src/lib/navigation/side-nav/africanies-side-nav.component.html +4 -2
  66. package/libs/africanies-ui/src/lib/navigation/side-nav/africanies-side-nav.component.ts +3 -1
  67. package/libs/africanies-ui/src/lib/table/table.component.spec.ts +3 -0
  68. package/libs/africanies-ui/src/lib/table/table.component.ts +254 -109
  69. package/package.json +1 -1
  70. package/tools/build-icon-sprite.mjs +1 -1
@@ -2,12 +2,15 @@ import type { IconName } from '@africanies/africanies-icons';
2
2
 
3
3
  import type { HeaderWeather, HeaderWeatherKind } from './header-greeting.util';
4
4
 
5
- /** Bump when cache shape or lookup strategy changes. */
6
- const CACHE_KEY = 'africanies-header-weather-v5';
5
+ /** Bump when cache shape or city resolution strategy changes. */
6
+ const CACHE_KEY = 'africanies-header-weather-v7';
7
7
  const FETCH_MS = 4_000;
8
8
  const BROWSER_GEO_MS = 3_500;
9
- /** IP fallback for coordinates only city strings are often wrong. */
9
+ /** IP fallback city string is used only when reverse-geocode is empty. */
10
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';
11
14
 
12
15
  const WEATHER_LABELS: Record<HeaderWeatherKind, string> = {
13
16
  clear: 'Clear',
@@ -23,13 +26,21 @@ interface CachedWeather {
23
26
  hour: string;
24
27
  kind: HeaderWeatherKind;
25
28
  temperatureC?: number;
29
+ city?: string;
26
30
  }
27
31
 
28
32
  interface GeoJsPayload {
33
+ city?: string;
29
34
  latitude?: string | number;
30
35
  longitude?: string | number;
31
36
  }
32
37
 
38
+ interface ReverseGeoPayload {
39
+ city?: string;
40
+ locality?: string;
41
+ principalSubdivision?: string;
42
+ }
43
+
33
44
  interface OpenMeteoPayload {
34
45
  current?: {
35
46
  weather_code?: number;
@@ -108,12 +119,11 @@ export function headerWeatherIcon(kind: HeaderWeatherKind, hour: number): IconNa
108
119
  }
109
120
 
110
121
  /**
111
- * Local forecast via browser / IP coordinates + Open-Meteo (no API key).
122
+ * City-level forecast via browser / IP geolocation + Open-Meteo (no API key).
112
123
  *
113
- * Coordinates prefer the browser Geolocation API (may prompt once).
114
- * Otherwise IP geo is used for lat/lon only — city labels are not shown
115
- * because IP and reverse-geocode cities are often wrong (e.g. NG IPs
116
- * labelled Lagos).
124
+ * Coordinates prefer the browser Geolocation API (accurate for travellers;
125
+ * may prompt once). Otherwise IP geo is used. The city label is reverse-
126
+ * geocoded from those coordinates, with the IP city as a fallback.
117
127
  *
118
128
  * Fails closed: missing browser APIs, timeouts, and HTTP errors all return
119
129
  * `null` so the greeting can stay time-of-day only. Cached per local hour
@@ -158,8 +168,11 @@ export async function loadHeaderWeather(
158
168
  }
159
169
 
160
170
  const temperatureC = asNumber(forecast?.current?.temperature_2m) ?? undefined;
161
- const weather: HeaderWeather = { kind, temperatureC };
162
- writeCache({ hour, kind, temperatureC });
171
+ const city =
172
+ (await reverseGeocodeCity(run, coords.latitude, coords.longitude)) ??
173
+ coords.fallbackCity;
174
+ const weather: HeaderWeather = { kind, temperatureC, city };
175
+ writeCache({ hour, kind, temperatureC, city });
163
176
  return weather;
164
177
  } catch {
165
178
  return null;
@@ -184,9 +197,16 @@ function readCache(hour: string): HeaderWeather | null {
184
197
  if (parsed.hour !== hour || !parsed.kind) {
185
198
  return null;
186
199
  }
200
+ // City-less entries (e.g. browser geo + failed reverse) used to stick for
201
+ // the hour — treat them as a miss so we can recover the location label.
202
+ const city = asCity(parsed.city);
203
+ if (!city) {
204
+ return null;
205
+ }
187
206
  return {
188
207
  kind: parsed.kind,
189
208
  temperatureC: parsed.temperatureC,
209
+ city,
190
210
  };
191
211
  } catch {
192
212
  return null;
@@ -203,23 +223,36 @@ function writeCache(value: CachedWeather): void {
203
223
 
204
224
  /**
205
225
  * Prefer device coordinates when available (may prompt once); otherwise fall
206
- * back to IP lat/lon for the forecast only.
226
+ * back to IP geo. IP is always fetched in parallel so its city can backfill
227
+ * the label when reverse-geocode is empty (browser geo has no city string).
207
228
  */
208
229
  async function resolveCoordinates(
209
230
  fetchFn: typeof fetch,
210
- ): Promise<Coordinates | null> {
211
- const browser = await tryBrowserGeolocation();
231
+ ): Promise<(Coordinates & { fallbackCity?: string }) | null> {
232
+ const [browser, ipGeo] = await Promise.all([
233
+ tryBrowserGeolocation(),
234
+ fetchIpGeo(fetchFn),
235
+ ]);
236
+ const ipCity = asCity(ipGeo?.city);
237
+
212
238
  if (browser) {
213
- return browser;
239
+ return { ...browser, fallbackCity: ipCity };
214
240
  }
215
241
 
216
- const geo = (await fetchJson(fetchFn, GEO_URL)) as GeoJsPayload | null;
217
- const latitude = asNumber(geo?.latitude);
218
- const longitude = asNumber(geo?.longitude);
242
+ const latitude = asNumber(ipGeo?.latitude);
243
+ const longitude = asNumber(ipGeo?.longitude);
219
244
  if (latitude === null || longitude === null) {
220
245
  return null;
221
246
  }
222
- return { latitude, longitude };
247
+ return {
248
+ latitude,
249
+ longitude,
250
+ fallbackCity: ipCity,
251
+ };
252
+ }
253
+
254
+ async function fetchIpGeo(fetchFn: typeof fetch): Promise<GeoJsPayload | null> {
255
+ return (await fetchJson(fetchFn, GEO_URL)) as GeoJsPayload | null;
223
256
  }
224
257
 
225
258
  async function tryBrowserGeolocation(): Promise<Coordinates | null> {
@@ -253,6 +286,23 @@ async function tryBrowserGeolocation(): Promise<Coordinates | null> {
253
286
  });
254
287
  }
255
288
 
289
+ async function reverseGeocodeCity(
290
+ fetchFn: typeof fetch,
291
+ latitude: number,
292
+ longitude: number,
293
+ ): Promise<string | undefined> {
294
+ const url =
295
+ `${REVERSE_GEO_URL}?latitude=${encodeURIComponent(String(latitude))}` +
296
+ `&longitude=${encodeURIComponent(String(longitude))}` +
297
+ `&localityLanguage=en`;
298
+ const payload = (await fetchJson(fetchFn, url)) as ReverseGeoPayload | null;
299
+ return (
300
+ asCity(payload?.city) ??
301
+ asCity(payload?.locality) ??
302
+ asCity(payload?.principalSubdivision)
303
+ );
304
+ }
305
+
256
306
  function asNumber(value: unknown): number | null {
257
307
  if (typeof value === 'number' && Number.isFinite(value)) {
258
308
  return value;
@@ -264,6 +314,17 @@ function asNumber(value: unknown): number | null {
264
314
  return null;
265
315
  }
266
316
 
317
+ function asCity(value: unknown): string | undefined {
318
+ if (typeof value !== 'string') {
319
+ return undefined;
320
+ }
321
+ const trimmed = value.trim();
322
+ if (!trimmed || trimmed.toLowerCase() === 'n/a') {
323
+ return undefined;
324
+ }
325
+ return trimmed;
326
+ }
327
+
267
328
  async function fetchJson(
268
329
  fetchFn: typeof fetch,
269
330
  url: string,
@@ -38,6 +38,10 @@ const DEFAULT_LINK_ACTIVE: IsActiveMatchOptions = {
38
38
  /**
39
39
  * Compact segmented control for view modes and filters.
40
40
  *
41
+ * Selected option uses a **solid brand pill** (white label) on a neutral track —
42
+ * same pattern as the portal shipping-mode toggle. With exactly two options the
43
+ * pill slides; with more options each selected chip fills in place.
44
+ *
41
45
  * ## Router mode (URL is source of truth)
42
46
  *
43
47
  * Set `routerLink` (and optional `queryParams`) on items. The selected pill
@@ -65,20 +69,38 @@ const DEFAULT_LINK_ACTIVE: IsActiveMatchOptions = {
65
69
  standalone: true,
66
70
  changeDetection: ChangeDetectionStrategy.OnPush,
67
71
  imports: [AfricaniesIconComponent],
72
+ styles: `
73
+ :host .africanies-segment-thumb {
74
+ width: calc((100% - 0.5rem) / 2);
75
+ }
76
+ `,
68
77
  template: `
69
78
  <div
70
- class="inline-flex max-w-full flex-wrap gap-1 rounded-lg bg-background-welcome p-1 dark:bg-ink-950"
79
+ class="relative inline-grid max-w-full rounded-lg bg-[#f0f2f5] p-1 dark:bg-[#2a2c31]"
71
80
  role="group"
72
81
  [attr.aria-label]="ariaLabel()"
82
+ [class.grid-cols-2]="useSlidingThumb()"
83
+ [class.inline-flex]="!useSlidingThumb()"
84
+ [class.flex-wrap]="!useSlidingThumb()"
85
+ [class.gap-1]="!useSlidingThumb()"
73
86
  >
87
+ @if (useSlidingThumb()) {
88
+ <div
89
+ class="africanies-segment-thumb pointer-events-none absolute top-1 bottom-1 left-1 rounded-md shadow-sm transition-transform duration-200 ease-out motion-reduce:transition-none"
90
+ [class]="thumbFillClass()"
91
+ [style.transform]="
92
+ activeIndex() === 1
93
+ ? 'translateX(calc(100% + 0.25rem))'
94
+ : 'translateX(0)'
95
+ "
96
+ aria-hidden="true"
97
+ ></div>
98
+ }
99
+
74
100
  @for (item of items(); track item.id) {
75
101
  @if (item.routerLink !== null && item.routerLink !== undefined) {
76
102
  <a
77
- [class]="
78
- segmentChrome() +
79
- ' ' +
80
- (isItemActive(item) ? selectedClass() : idleClass())
81
- "
103
+ [class]="optionChrome(item)"
82
104
  [class.cursor-pointer]="!item.disabled"
83
105
  [class.cursor-not-allowed]="item.disabled"
84
106
  [class.opacity-50]="item.disabled"
@@ -95,11 +117,7 @@ const DEFAULT_LINK_ACTIVE: IsActiveMatchOptions = {
95
117
  } @else {
96
118
  <button
97
119
  type="button"
98
- [class]="
99
- segmentChrome() +
100
- ' ' +
101
- (isItemActive(item) ? selectedClass() : idleClass())
102
- "
120
+ [class]="optionChrome(item)"
103
121
  [disabled]="!!item.disabled"
104
122
  [class.cursor-pointer]="!item.disabled"
105
123
  [class.cursor-not-allowed]="item.disabled"
@@ -154,21 +172,18 @@ export class SegmentComponent {
154
172
  { initialValue: this.router.url },
155
173
  );
156
174
 
157
- protected readonly segmentChrome = computed(
158
- () =>
159
- 'inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-body-sm transition-colors duration-150 no-underline',
175
+ /** Two equal options → portal-style sliding brand pill. */
176
+ protected readonly useSlidingThumb = computed(
177
+ () => this.items().length === 2,
160
178
  );
161
179
 
162
- protected readonly selectedClass = computed(() => {
163
- const colors = this.modeColor.classes();
164
- return `${colors.soft} ${colors.text} shadow-sm font-semibold`;
180
+ /** Index of the active option (0 / 1 for the sliding thumb). */
181
+ protected readonly activeIndex = computed(() => {
182
+ const list = this.items();
183
+ const idx = list.findIndex((item) => this.isItemActive(item));
184
+ return idx < 0 ? 0 : idx;
165
185
  });
166
186
 
167
- protected readonly idleClass = computed(
168
- () =>
169
- 'font-medium text-neutral-600 hover:text-ink dark:text-neutral-400 dark:hover:text-white',
170
- );
171
-
172
187
  constructor() {
173
188
  effect(() => {
174
189
  this.url();
@@ -198,6 +213,67 @@ export class SegmentComponent {
198
213
  return this.activeId() === item.id;
199
214
  }
200
215
 
216
+ /**
217
+ * Sliding thumb fill. STN / SFN options keep their own brand color so the
218
+ * pill matches mode identity; other pairs follow the active theme primary.
219
+ */
220
+ protected thumbFillClass(): string {
221
+ const active = this.items()[this.activeIndex()];
222
+ return this.brandFillFor(active?.id ?? null);
223
+ }
224
+
225
+ /**
226
+ * Option chrome: sliding mode keeps labels transparent over the thumb;
227
+ * multi-option mode paints a solid selected chip in place.
228
+ *
229
+ * @param item - Option being rendered.
230
+ * @returns Class string for the control.
231
+ */
232
+ protected optionChrome(item: AfricaniesNavItem): string {
233
+ const base =
234
+ 'relative z-10 inline-flex min-w-0 items-center justify-center gap-1.5 rounded-md px-3 py-1.5 text-center text-body-sm no-underline transition-colors duration-150';
235
+ const active = this.isItemActive(item);
236
+
237
+ if (this.useSlidingThumb()) {
238
+ return (
239
+ base +
240
+ ' w-full ' +
241
+ (active
242
+ ? 'font-semibold text-white'
243
+ : 'font-medium text-ink hover:text-ink dark:text-neutral-300 dark:hover:text-white')
244
+ );
245
+ }
246
+
247
+ if (active) {
248
+ return `${base} font-semibold text-white shadow-sm ${this.brandFillFor(item.id)}`;
249
+ }
250
+
251
+ return (
252
+ base +
253
+ ' font-medium text-ink hover:bg-white/70 hover:text-ink dark:text-neutral-300 dark:hover:bg-white/10 dark:hover:text-white'
254
+ );
255
+ }
256
+
257
+ /**
258
+ * Solid brand fill for a selected option / sliding thumb.
259
+ *
260
+ * Light + dark use the same SFN green / STN orange primaries so the control
261
+ * stays mode-identifiable on both surfaces (portal primary, not soft tint).
262
+ * Class strings are full literals so Tailwind keeps them.
263
+ *
264
+ * @param id - Option id (`stn` / `sfn` get fixed brands; else theme primary).
265
+ * @returns Background utility classes.
266
+ */
267
+ protected brandFillFor(id: string | null): string {
268
+ if (id === 'stn') {
269
+ return 'bg-import dark:bg-import';
270
+ }
271
+ if (id === 'sfn') {
272
+ return 'bg-export dark:bg-export';
273
+ }
274
+ return this.modeColor.classes().activeFill;
275
+ }
276
+
201
277
  protected selectLocal(item: AfricaniesNavItem): void {
202
278
  if (item.disabled || item.routerLink != null) {
203
279
  return;
@@ -59,4 +59,19 @@ describe('ShippingModeSwitchComponent', () => {
59
59
  expect(shipping.mode()).toBe('stn');
60
60
  expect(toNigeria.getAttribute('aria-checked')).toBe('true');
61
61
  });
62
+
63
+ it('should show the STN/SFN chip only when the mode panel is collapsed', () => {
64
+ fixture.componentRef.setInput('panelOpen', true);
65
+ fixture.detectChanges();
66
+ expect(fixture.nativeElement.textContent).not.toContain('SFN');
67
+
68
+ fixture.componentRef.setInput('panelOpen', false);
69
+ fixture.detectChanges();
70
+ expect(fixture.nativeElement.textContent).toContain('SFN');
71
+
72
+ shipping.setMode('stn');
73
+ fixture.detectChanges();
74
+ expect(fixture.nativeElement.textContent).toContain('STN');
75
+ expect(fixture.nativeElement.textContent).not.toContain('SFN');
76
+ });
62
77
  });