@africanies/angular-web-sdk 0.1.9 → 0.1.10
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/1149535958171016935/dist/libs/africanies-theme/README.md +46 -0
- package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +243 -0
- package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -0
- package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/package.json +35 -0
- package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/tailwind-preset.cjs +330 -0
- package/.nx/cache/1149535958171016935/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +178 -0
- package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/README.md +68 -0
- package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
- package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
- package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +5 -0
- package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/assets/carriers/dhl.svg +7 -0
- package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +19998 -0
- package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
- package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/package.json +35 -0
- package/.nx/cache/3735952477975556635/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +5616 -0
- package/.nx/cache/run.json +17 -17
- package/.nx/cache/terminalOutputs/1149535958171016935 +21 -0
- package/.nx/cache/terminalOutputs/3735952477975556635 +21 -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 +1430 -0
- package/.nx/workspace-data/d/server-process.json +2 -2
- package/.nx/workspace-data/file-map.json +3332 -3334
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/project-graph.json +2 -2
- package/apps/playground/src/app/pages/chip-page.ts +11 -1
- package/apps/playground/src/app/pages/tokens-page.ts +2 -2
- package/apps/playground/src/app/snippets/chip.snippets.ts +5 -0
- package/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +8 -8
- package/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -1
- package/dist/libs/africanies-theme/tailwind-preset.cjs +3 -3
- package/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +5 -3
- package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +869 -371
- package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -1
- package/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +135 -40
- package/libs/africanies-theme/src/lib/mode-color.safelist.ts +2 -2
- package/libs/africanies-theme/src/lib/mode-color.service.spec.ts +9 -7
- package/libs/africanies-theme/src/lib/mode-color.service.ts +9 -7
- package/libs/africanies-theme/tailwind-preset.cjs +3 -3
- package/libs/africanies-ui/src/lib/avatar/avatar-menu.component.ts +25 -21
- package/libs/africanies-ui/src/lib/button/button.component.spec.ts +13 -8
- package/libs/africanies-ui/src/lib/chip/chip.component.ts +74 -6
- package/libs/africanies-ui/src/lib/layout/app-shell-header.component.ts +35 -12
- package/libs/africanies-ui/src/lib/layout/header-greeting.util.ts +2 -0
- package/libs/africanies-ui/src/lib/layout/header-weather.spec.ts +120 -9
- package/libs/africanies-ui/src/lib/layout/header-weather.ts +79 -18
- package/libs/africanies-ui/src/lib/navigation/segment/segment.component.ts +98 -22
- package/libs/africanies-ui/src/lib/navigation/shipping-mode-switch/shipping-mode-switch.component.ts +193 -65
- package/libs/africanies-ui/src/lib/navigation/side-nav/africanies-side-nav.component.html +4 -2
- package/libs/africanies-ui/src/lib/navigation/side-nav/africanies-side-nav.component.ts +3 -1
- package/libs/africanies-ui/src/lib/table/table.component.spec.ts +3 -0
- package/libs/africanies-ui/src/lib/table/table.component.ts +197 -57
- package/package.json +1 -1
- 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
|
|
6
|
-
const CACHE_KEY = 'africanies-header-weather-
|
|
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
|
|
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
|
-
*
|
|
122
|
+
* City-level forecast via browser / IP geolocation + Open-Meteo (no API key).
|
|
112
123
|
*
|
|
113
|
-
* Coordinates prefer the browser Geolocation API (
|
|
114
|
-
* Otherwise IP geo is used
|
|
115
|
-
*
|
|
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
|
|
162
|
-
|
|
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
|
|
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
|
|
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
|
|
217
|
-
const
|
|
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 {
|
|
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-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
175
|
+
/** Two equal options → portal-style sliding brand pill. */
|
|
176
|
+
protected readonly useSlidingThumb = computed(
|
|
177
|
+
() => this.items().length === 2,
|
|
160
178
|
);
|
|
161
179
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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;
|
package/libs/africanies-ui/src/lib/navigation/shipping-mode-switch/shipping-mode-switch.component.ts
CHANGED
|
@@ -3,13 +3,46 @@ import {
|
|
|
3
3
|
ChangeDetectionStrategy,
|
|
4
4
|
Component,
|
|
5
5
|
computed,
|
|
6
|
+
effect,
|
|
6
7
|
inject,
|
|
7
8
|
input,
|
|
9
|
+
model,
|
|
8
10
|
} from '@angular/core';
|
|
9
11
|
|
|
10
12
|
import { ShippingModeService } from '@africanies/africanies-core';
|
|
13
|
+
import { AfricaniesIconComponent } from '@africanies/africanies-icons';
|
|
11
14
|
import type { ShippingMode } from '@africanies/africanies-models';
|
|
12
15
|
|
|
16
|
+
import { ChipComponent, type ChipVariant } from '../../chip/chip.component';
|
|
17
|
+
|
|
18
|
+
const PANEL_OPEN_STORAGE_KEY = 'africanies-shipping-mode-panel-open';
|
|
19
|
+
|
|
20
|
+
function readStoredPanelOpen(): boolean {
|
|
21
|
+
if (typeof localStorage === 'undefined') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const raw = localStorage.getItem(PANEL_OPEN_STORAGE_KEY);
|
|
26
|
+
if (raw === null) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return raw !== '0' && raw !== 'false';
|
|
30
|
+
} catch {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeStoredPanelOpen(open: boolean): void {
|
|
36
|
+
if (typeof localStorage === 'undefined') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
localStorage.setItem(PANEL_OPEN_STORAGE_KEY, open ? '1' : '0');
|
|
41
|
+
} catch {
|
|
42
|
+
// Private mode / quota — panel still toggles in-session.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
13
46
|
/**
|
|
14
47
|
* Shared globe + plane glyph for STN / SFN. STN paints it rotated 180°.
|
|
15
48
|
*
|
|
@@ -25,12 +58,13 @@ const SHIPPING_MODE_GLYPH = `M88.42,68.75a48.8,48.8,0,0,1-4.74,11.1h9.89a7.67,7.
|
|
|
25
58
|
* import orange; **from Nigeria** uses export green. Both tiles keep equal
|
|
26
59
|
* chrome so unselected does not look like floating text.
|
|
27
60
|
*
|
|
28
|
-
* Project into `<africanies-side-nav>` with the `
|
|
61
|
+
* Project into `<africanies-side-nav>` with the `pre-nav` attribute (above the
|
|
62
|
+
* nav list). `footer` still works for legacy placement under the list.
|
|
29
63
|
*
|
|
30
64
|
* @example
|
|
31
65
|
* ```html
|
|
32
66
|
* <africanies-side-nav [items]="nav" [(collapsed)]="collapsed">
|
|
33
|
-
* <africanies-shipping-mode-switch
|
|
67
|
+
* <africanies-shipping-mode-switch pre-nav [collapsed]="collapsed()" />
|
|
34
68
|
* </africanies-side-nav>
|
|
35
69
|
* ```
|
|
36
70
|
*/
|
|
@@ -38,71 +72,140 @@ const SHIPPING_MODE_GLYPH = `M88.42,68.75a48.8,48.8,0,0,1-4.74,11.1h9.89a7.67,7.
|
|
|
38
72
|
selector: 'africanies-shipping-mode-switch',
|
|
39
73
|
standalone: true,
|
|
40
74
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
75
|
+
imports: [AfricaniesIconComponent, ChipComponent],
|
|
41
76
|
host: {
|
|
42
77
|
'[class]': 'hostClass()',
|
|
43
78
|
},
|
|
44
79
|
template: `
|
|
45
|
-
@if (
|
|
46
|
-
|
|
47
|
-
|
|
80
|
+
@if (collapsed()) {
|
|
81
|
+
<!-- Narrow rail: icon tiles only — chip + chevron reads as cramped. -->
|
|
82
|
+
<div
|
|
83
|
+
class="grid grid-cols-1 gap-1.5"
|
|
84
|
+
role="radiogroup"
|
|
85
|
+
aria-label="Shipping mode"
|
|
48
86
|
>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
aria-label="Shipping mode"
|
|
58
|
-
>
|
|
59
|
-
<button
|
|
60
|
-
type="button"
|
|
61
|
-
role="radio"
|
|
62
|
-
[attr.aria-checked]="mode() === 'stn'"
|
|
63
|
-
aria-label="Shipping to Nigeria"
|
|
64
|
-
[class]="cardClass('stn')"
|
|
65
|
-
(click)="select('stn')"
|
|
66
|
-
>
|
|
67
|
-
<svg
|
|
68
|
-
viewBox="0 0 122.88 107.54"
|
|
69
|
-
class="h-6 w-6 shrink-0 rotate-180"
|
|
70
|
-
aria-hidden="true"
|
|
71
|
-
focusable="false"
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
role="radio"
|
|
90
|
+
[attr.aria-checked]="mode() === 'stn'"
|
|
91
|
+
aria-label="Shipping to Nigeria"
|
|
92
|
+
title="Shipping to Nigeria"
|
|
93
|
+
[class]="cardClass('stn')"
|
|
94
|
+
(click)="select('stn')"
|
|
72
95
|
>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
<svg
|
|
97
|
+
viewBox="0 0 122.88 107.54"
|
|
98
|
+
class="h-5 w-5 shrink-0 rotate-180"
|
|
99
|
+
aria-hidden="true"
|
|
100
|
+
focusable="false"
|
|
101
|
+
>
|
|
102
|
+
<path [attr.d]="glyphPath()" fill="currentColor" />
|
|
103
|
+
</svg>
|
|
104
|
+
</button>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
role="radio"
|
|
108
|
+
[attr.aria-checked]="mode() === 'sfn'"
|
|
109
|
+
aria-label="Shipping from Nigeria"
|
|
110
|
+
title="Shipping from Nigeria"
|
|
111
|
+
[class]="cardClass('sfn')"
|
|
112
|
+
(click)="select('sfn')"
|
|
113
|
+
>
|
|
114
|
+
<svg
|
|
115
|
+
viewBox="0 0 122.88 107.54"
|
|
116
|
+
class="h-5 w-5 shrink-0"
|
|
117
|
+
aria-hidden="true"
|
|
118
|
+
focusable="false"
|
|
119
|
+
>
|
|
120
|
+
<path [attr.d]="glyphPath()" fill="currentColor" />
|
|
121
|
+
</svg>
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
} @else {
|
|
125
|
+
<div class="mb-2 flex items-center gap-1.5">
|
|
126
|
+
<p
|
|
127
|
+
class="m-0 shrink-0 px-1 text-caption font-semibold uppercase tracking-wide text-neutral-600 dark:text-neutral-400"
|
|
95
128
|
>
|
|
96
|
-
|
|
97
|
-
</
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
129
|
+
Shipping mode
|
|
130
|
+
</p>
|
|
131
|
+
<africanies-chip [variant]="modeChipVariant()" size="sm">
|
|
132
|
+
{{ modeChipLabel() }}
|
|
133
|
+
</africanies-chip>
|
|
134
|
+
<span class="min-w-0 flex-1" aria-hidden="true"></span>
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
class="inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-neutral-600 transition-colors hover:bg-background-hover hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus dark:text-neutral-400 dark:hover:bg-white/10 dark:hover:text-white"
|
|
138
|
+
[attr.aria-label]="
|
|
139
|
+
panelOpen() ? 'Hide shipping modes' : 'Show shipping modes'
|
|
140
|
+
"
|
|
141
|
+
[attr.aria-expanded]="panelOpen()"
|
|
142
|
+
(click)="togglePanel()"
|
|
143
|
+
>
|
|
144
|
+
<africanies-icon
|
|
145
|
+
[name]="panelOpen() ? 'chevron-up' : 'chevron-down'"
|
|
146
|
+
[size]="14"
|
|
147
|
+
/>
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div
|
|
152
|
+
class="grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
|
|
153
|
+
[style.grid-template-rows]="panelOpen() ? '1fr' : '0fr'"
|
|
154
|
+
[attr.aria-hidden]="!panelOpen()"
|
|
155
|
+
[class.pointer-events-none]="!panelOpen()"
|
|
156
|
+
>
|
|
157
|
+
<div class="min-h-0 overflow-hidden">
|
|
158
|
+
<div
|
|
159
|
+
class="grid grid-cols-2 gap-1.5"
|
|
160
|
+
role="radiogroup"
|
|
161
|
+
aria-label="Shipping mode"
|
|
162
|
+
>
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
role="radio"
|
|
166
|
+
[attr.aria-checked]="mode() === 'stn'"
|
|
167
|
+
aria-label="Shipping to Nigeria"
|
|
168
|
+
[class]="cardClass('stn')"
|
|
169
|
+
(click)="select('stn')"
|
|
170
|
+
>
|
|
171
|
+
<svg
|
|
172
|
+
viewBox="0 0 122.88 107.54"
|
|
173
|
+
class="h-6 w-6 shrink-0 rotate-180"
|
|
174
|
+
aria-hidden="true"
|
|
175
|
+
focusable="false"
|
|
176
|
+
>
|
|
177
|
+
<path [attr.d]="glyphPath()" fill="currentColor" />
|
|
178
|
+
</svg>
|
|
179
|
+
<span class="flex flex-col leading-tight">
|
|
180
|
+
<span>Shipping</span>
|
|
181
|
+
<span>to Nigeria</span>
|
|
182
|
+
</span>
|
|
183
|
+
</button>
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
role="radio"
|
|
187
|
+
[attr.aria-checked]="mode() === 'sfn'"
|
|
188
|
+
aria-label="Shipping from Nigeria"
|
|
189
|
+
[class]="cardClass('sfn')"
|
|
190
|
+
(click)="select('sfn')"
|
|
191
|
+
>
|
|
192
|
+
<svg
|
|
193
|
+
viewBox="0 0 122.88 107.54"
|
|
194
|
+
class="h-6 w-6 shrink-0"
|
|
195
|
+
aria-hidden="true"
|
|
196
|
+
focusable="false"
|
|
197
|
+
>
|
|
198
|
+
<path [attr.d]="glyphPath()" fill="currentColor" />
|
|
199
|
+
</svg>
|
|
200
|
+
<span class="flex flex-col leading-tight">
|
|
201
|
+
<span>Shipping</span>
|
|
202
|
+
<span>from Nigeria</span>
|
|
203
|
+
</span>
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
}
|
|
106
209
|
`,
|
|
107
210
|
})
|
|
108
211
|
export class ShippingModeSwitchComponent {
|
|
@@ -125,16 +228,41 @@ export class ShippingModeSwitchComponent {
|
|
|
125
228
|
*/
|
|
126
229
|
readonly collapsed = input(false, { transform: booleanAttribute });
|
|
127
230
|
|
|
231
|
+
/**
|
|
232
|
+
* When true, the mode tiles are visible. Persisted in `localStorage`.
|
|
233
|
+
* Two-way bindable for hosts that need to force-open (e.g. onboarding).
|
|
234
|
+
*/
|
|
235
|
+
readonly panelOpen = model(readStoredPanelOpen());
|
|
236
|
+
|
|
128
237
|
/** Active mode from {@link ShippingModeService}. */
|
|
129
238
|
protected readonly mode = this.shipping.mode;
|
|
130
239
|
|
|
131
|
-
|
|
240
|
+
protected readonly modeChipLabel = computed(() =>
|
|
241
|
+
this.mode() === 'stn' ? 'STN' : 'SFN',
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
protected readonly modeChipVariant = computed(
|
|
245
|
+
(): ChipVariant => (this.mode() === 'stn' ? 'import' : 'export'),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
/** Host padding / divider — sits above the nav list (`border-b`). */
|
|
132
249
|
protected readonly hostClass = computed(() =>
|
|
133
250
|
this.collapsed()
|
|
134
|
-
? 'block shrink-0 border-
|
|
135
|
-
: 'block shrink-0 border-
|
|
251
|
+
? 'block shrink-0 border-b border-border px-1.5 py-2 dark:border-white/10'
|
|
252
|
+
: 'block shrink-0 border-b border-border px-2 py-2.5 dark:border-white/10',
|
|
136
253
|
);
|
|
137
254
|
|
|
255
|
+
constructor() {
|
|
256
|
+
effect(() => {
|
|
257
|
+
writeStoredPanelOpen(this.panelOpen());
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Show or hide the mode tiles. */
|
|
262
|
+
protected togglePanel(): void {
|
|
263
|
+
this.panelOpen.update((open) => !open);
|
|
264
|
+
}
|
|
265
|
+
|
|
138
266
|
/**
|
|
139
267
|
* Requests a mode change through {@link ShippingModeService.requestModeChange}
|
|
140
268
|
* so feature guards (e.g. Create Shipment confirm) can run first.
|
|
@@ -160,7 +288,7 @@ export class ShippingModeSwitchComponent {
|
|
|
160
288
|
const base =
|
|
161
289
|
'flex w-full cursor-pointer flex-col items-center justify-center gap-1 rounded-lg border text-center text-caption font-medium transition-colors ' +
|
|
162
290
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus ' +
|
|
163
|
-
(compact ? 'min-h-
|
|
291
|
+
(compact ? 'size-9 min-h-0 px-0 py-0' : 'min-h-[4.5rem] px-1.5 py-2');
|
|
164
292
|
const idle =
|
|
165
293
|
'border-control bg-surface text-ink hover:bg-background-hover dark:border-white/15 dark:bg-ink-950 dark:text-white dark:hover:bg-white/10';
|
|
166
294
|
|
|
@@ -170,7 +298,7 @@ export class ShippingModeSwitchComponent {
|
|
|
170
298
|
: `${base} ${idle}`;
|
|
171
299
|
}
|
|
172
300
|
return selected
|
|
173
|
-
? `${base} border-export
|
|
301
|
+
? `${base} border-export bg-export text-white dark:border-export dark:bg-export`
|
|
174
302
|
: `${base} ${idle}`;
|
|
175
303
|
}
|
|
176
304
|
}
|