@africanies/angular-web-sdk 0.1.8 → 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/.angular/cache/22.0.9/playground/.tsbuildinfo +1 -1
- 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/17289624717336768573/dist/libs/africanies-core/README.md +85 -0
- package/.nx/cache/17289624717336768573/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs +4639 -0
- package/.nx/cache/17289624717336768573/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +1 -0
- package/.nx/cache/17289624717336768573/dist/libs/africanies-core/package.json +31 -0
- package/.nx/cache/17289624717336768573/dist/libs/africanies-core/types/africanies-africanies-core.d.ts +2748 -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/3953970051755345887/dist/libs/africanies-ui/README.md +68 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +5 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/assets/carriers/dhl.svg +7 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +19500 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/package.json +35 -0
- package/.nx/cache/3953970051755345887/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +5521 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/README.md +46 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +243 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +1 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/package.json +35 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/tailwind-preset.cjs +330 -0
- package/.nx/cache/4225324930345532464/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +176 -0
- package/.nx/cache/run.json +26 -26
- package/.nx/cache/terminalOutputs/1149535958171016935 +21 -0
- package/.nx/cache/terminalOutputs/12565224028063674529 +37 -0
- package/.nx/cache/terminalOutputs/13478445034325959094 +176 -0
- package/.nx/cache/terminalOutputs/15835338512221847842 +184 -182
- package/.nx/cache/terminalOutputs/17289624717336768573 +21 -0
- package/.nx/cache/terminalOutputs/17571479482482478257 +35 -0
- package/.nx/cache/terminalOutputs/3088264223473143358 +8 -0
- package/.nx/cache/terminalOutputs/3735952477975556635 +21 -0
- package/.nx/cache/terminalOutputs/3953970051755345887 +21 -0
- package/.nx/cache/terminalOutputs/4225324930345532464 +21 -0
- package/.nx/cache/terminalOutputs/4363587767476846758 +176 -0
- package/.nx/cache/terminalOutputs/8193462679279018087 +39 -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 +5890 -0
- package/.nx/workspace-data/d/server-process.json +2 -2
- package/.nx/workspace-data/file-map.json +3610 -3620
- 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-core/fesm2022/africanies-africanies-core.mjs +6 -1
- package/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +1 -1
- package/dist/libs/africanies-core/types/africanies-africanies-core.d.ts +6 -1
- 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 +848 -375
- package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -1
- package/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +123 -30
- package/libs/africanies-core/src/index.ts +1 -1
- package/libs/africanies-core/src/lib/notification/notification-link.ts +6 -1
- 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 +39 -16
- package/libs/africanies-ui/src/lib/layout/header-greeting.util.ts +1 -1
- package/libs/africanies-ui/src/lib/layout/header-weather.spec.ts +87 -5
- package/libs/africanies-ui/src/lib/layout/header-weather.ts +35 -15
- 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
|
@@ -13,10 +13,15 @@ import { AfricaniesIconComponent } from '@africanies/africanies-icons';
|
|
|
13
13
|
/**
|
|
14
14
|
* Semantic tone for {@link ChipComponent}.
|
|
15
15
|
*
|
|
16
|
+
* Prefer these for common statuses. For one-off accents, pass {@link ChipComponent.color}
|
|
17
|
+
* instead — any CSS color with a faded background.
|
|
18
|
+
*
|
|
16
19
|
* - `neutral` — generic label / count
|
|
17
20
|
* - `success` — positive outcome (same green family as export)
|
|
18
21
|
* - `warning` — caution / pending
|
|
19
22
|
* - `danger` — error / exception
|
|
23
|
+
* - `info` — informational (theme blue)
|
|
24
|
+
* - `violet` / `teal` / `rose` / `cyan` — extra categorical tones
|
|
20
25
|
* - `export` — SFN / outbound accent
|
|
21
26
|
* - `import` — STN / inbound accent
|
|
22
27
|
*/
|
|
@@ -25,6 +30,11 @@ export type ChipVariant =
|
|
|
25
30
|
| 'success'
|
|
26
31
|
| 'warning'
|
|
27
32
|
| 'danger'
|
|
33
|
+
| 'info'
|
|
34
|
+
| 'violet'
|
|
35
|
+
| 'teal'
|
|
36
|
+
| 'rose'
|
|
37
|
+
| 'cyan'
|
|
28
38
|
| 'export'
|
|
29
39
|
| 'import';
|
|
30
40
|
|
|
@@ -37,12 +47,14 @@ export type ChipSize = 'sm' | 'md';
|
|
|
37
47
|
* Compact status / category label.
|
|
38
48
|
*
|
|
39
49
|
* Soft fills stay readable in dark mode (translucent accent tints, not
|
|
40
|
-
* light-only subtle backgrounds).
|
|
50
|
+
* light-only subtle backgrounds). Pass {@link color} for a custom accent —
|
|
51
|
+
* text uses that color and the background is a faded mix of the same.
|
|
41
52
|
*
|
|
42
53
|
* @example
|
|
43
54
|
* ```html
|
|
44
55
|
* <africanies-chip variant="success">Delivered</africanies-chip>
|
|
45
|
-
* <africanies-chip variant="
|
|
56
|
+
* <africanies-chip variant="violet">Priority</africanies-chip>
|
|
57
|
+
* <africanies-chip color="#0f766e">Pay Later</africanies-chip>
|
|
46
58
|
* <africanies-chip variant="warning" [removable]="true" (removed)="clear()">Pending</africanies-chip>
|
|
47
59
|
* ```
|
|
48
60
|
*/
|
|
@@ -53,8 +65,38 @@ export type ChipSize = 'sm' | 'md';
|
|
|
53
65
|
imports: [AfricaniesIconComponent],
|
|
54
66
|
host: {
|
|
55
67
|
'[class]': 'hostClass()',
|
|
68
|
+
'[style.--africanies-chip-accent]': 'customAccent()',
|
|
56
69
|
role: 'status',
|
|
57
70
|
},
|
|
71
|
+
styles: `
|
|
72
|
+
:host.africanies-chip--custom {
|
|
73
|
+
color: var(--africanies-chip-accent);
|
|
74
|
+
background-color: color-mix(
|
|
75
|
+
in srgb,
|
|
76
|
+
var(--africanies-chip-accent) 14%,
|
|
77
|
+
transparent
|
|
78
|
+
);
|
|
79
|
+
border-color: color-mix(
|
|
80
|
+
in srgb,
|
|
81
|
+
var(--africanies-chip-accent) 28%,
|
|
82
|
+
transparent
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
:host-context(.dark):host.africanies-chip--custom {
|
|
86
|
+
/* Lighten the accent so labels stay readable on dark surfaces. */
|
|
87
|
+
color: color-mix(in srgb, var(--africanies-chip-accent) 68%, white);
|
|
88
|
+
background-color: color-mix(
|
|
89
|
+
in srgb,
|
|
90
|
+
var(--africanies-chip-accent) 22%,
|
|
91
|
+
transparent
|
|
92
|
+
);
|
|
93
|
+
border-color: color-mix(
|
|
94
|
+
in srgb,
|
|
95
|
+
var(--africanies-chip-accent) 40%,
|
|
96
|
+
transparent
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
`,
|
|
58
100
|
template: `
|
|
59
101
|
@if (icon(); as iconName) {
|
|
60
102
|
<africanies-icon [name]="iconName" [size]="iconSize()" class="shrink-0" />
|
|
@@ -73,9 +115,15 @@ export type ChipSize = 'sm' | 'md';
|
|
|
73
115
|
`,
|
|
74
116
|
})
|
|
75
117
|
export class ChipComponent {
|
|
76
|
-
/** Semantic tone. Defaults to `neutral`. */
|
|
118
|
+
/** Semantic tone. Defaults to `neutral`. Ignored when {@link color} is set. */
|
|
77
119
|
readonly variant = input<ChipVariant>('neutral');
|
|
78
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Optional CSS color (`#7c3aed`, `rgb(…)`, `oklch(…)`, …). When set, overrides
|
|
123
|
+
* {@link variant}: label uses the color and the fill is a faded mix of it.
|
|
124
|
+
*/
|
|
125
|
+
readonly color = input<string | null>(null);
|
|
126
|
+
|
|
79
127
|
/** Padding / type scale. Defaults to `sm` (table / dense UI). */
|
|
80
128
|
readonly size = input<ChipSize>('sm');
|
|
81
129
|
|
|
@@ -96,6 +144,12 @@ export class ChipComponent {
|
|
|
96
144
|
|
|
97
145
|
protected readonly iconSize = computed(() => (this.size() === 'md' ? 14 : 12));
|
|
98
146
|
|
|
147
|
+
/** Trimmed custom accent, or `null` when using a named {@link variant}. */
|
|
148
|
+
protected readonly customAccent = computed(() => {
|
|
149
|
+
const value = this.color()?.trim() ?? '';
|
|
150
|
+
return value.length > 0 ? value : null;
|
|
151
|
+
});
|
|
152
|
+
|
|
99
153
|
protected readonly hostClass = computed(() => {
|
|
100
154
|
const base =
|
|
101
155
|
'inline-flex max-w-full items-center gap-1 rounded-md font-sans font-medium ' +
|
|
@@ -110,11 +164,15 @@ export class ChipComponent {
|
|
|
110
164
|
});
|
|
111
165
|
|
|
112
166
|
/**
|
|
113
|
-
* Soft background + text per variant
|
|
114
|
-
* chips never wash out as near-white
|
|
115
|
-
*
|
|
167
|
+
* Soft background + text per variant, or the custom-color host class.
|
|
168
|
+
* Dark mode uses translucent accents so chips never wash out as near-white
|
|
169
|
+
* blobs.
|
|
170
|
+
* @returns Tailwind / host class string for the active tone.
|
|
116
171
|
*/
|
|
117
172
|
private toneClass(): string {
|
|
173
|
+
if (this.customAccent()) {
|
|
174
|
+
return 'africanies-chip--custom';
|
|
175
|
+
}
|
|
118
176
|
switch (this.variant()) {
|
|
119
177
|
case 'success':
|
|
120
178
|
case 'export':
|
|
@@ -125,6 +183,16 @@ export class ChipComponent {
|
|
|
125
183
|
return 'bg-warning-subtle text-warning-dark dark:bg-warning/15 dark:text-warning';
|
|
126
184
|
case 'danger':
|
|
127
185
|
return 'bg-danger-subtle text-danger-dark dark:bg-danger/15 dark:text-danger';
|
|
186
|
+
case 'info':
|
|
187
|
+
return 'bg-info-subtle text-info dark:bg-info/20 dark:text-[#93c5fd]';
|
|
188
|
+
case 'violet':
|
|
189
|
+
return 'bg-violet-100 text-violet-800 dark:bg-violet-400/15 dark:text-violet-300';
|
|
190
|
+
case 'teal':
|
|
191
|
+
return 'bg-teal-100 text-teal-800 dark:bg-teal-400/15 dark:text-teal-300';
|
|
192
|
+
case 'rose':
|
|
193
|
+
return 'bg-rose-100 text-rose-800 dark:bg-rose-400/15 dark:text-rose-300';
|
|
194
|
+
case 'cyan':
|
|
195
|
+
return 'bg-cyan-100 text-cyan-800 dark:bg-cyan-400/15 dark:text-cyan-300';
|
|
128
196
|
default:
|
|
129
197
|
return 'bg-border/60 text-ink dark:bg-white/10 dark:text-neutral-200';
|
|
130
198
|
}
|
|
@@ -17,7 +17,7 @@ import type { Observable } from 'rxjs';
|
|
|
17
17
|
import { AfricaniesIconComponent } from '@africanies/africanies-icons';
|
|
18
18
|
|
|
19
19
|
import type { AfricaniesMenuItem } from '../action-menu/menu-item';
|
|
20
|
-
import { AvatarMenuComponent } from '../avatar';
|
|
20
|
+
import { AvatarComponent, AvatarMenuComponent } from '../avatar';
|
|
21
21
|
import type { AfricaniesNotification, NotificationPageResult } from '../notifications';
|
|
22
22
|
import { NotificationDrawerService } from '../notifications';
|
|
23
23
|
import type { HeaderWeather } from './header-greeting.util';
|
|
@@ -70,7 +70,12 @@ export class AppShellHeaderEndDirective {}
|
|
|
70
70
|
selector: 'africanies-app-shell-header',
|
|
71
71
|
standalone: true,
|
|
72
72
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
73
|
-
imports: [
|
|
73
|
+
imports: [
|
|
74
|
+
AfricaniesIconComponent,
|
|
75
|
+
AvatarComponent,
|
|
76
|
+
AvatarMenuComponent,
|
|
77
|
+
DatePipe,
|
|
78
|
+
],
|
|
74
79
|
template: `
|
|
75
80
|
<div
|
|
76
81
|
class="flex min-w-0 items-center gap-2 px-3 py-2.5 sm:gap-4 sm:px-6 sm:py-3"
|
|
@@ -113,13 +118,15 @@ export class AppShellHeaderEndDirective {}
|
|
|
113
118
|
<span
|
|
114
119
|
class="text-body-sm font-medium tabular-nums tracking-tight text-ink dark:text-white"
|
|
115
120
|
>
|
|
116
|
-
{{ temp }}°
|
|
121
|
+
{{ temp }}°C
|
|
117
122
|
</span>
|
|
118
123
|
}
|
|
119
124
|
<span
|
|
120
125
|
class="hidden text-caption text-neutral-600 md:inline dark:text-neutral-400"
|
|
121
126
|
>
|
|
122
|
-
{{
|
|
127
|
+
{{ weatherCondition() }}@if (weatherCity(); as city) {
|
|
128
|
+
· {{ city }}
|
|
129
|
+
}
|
|
123
130
|
</span>
|
|
124
131
|
</div>
|
|
125
132
|
}
|
|
@@ -178,12 +185,20 @@ export class AppShellHeaderEndDirective {}
|
|
|
178
185
|
}
|
|
179
186
|
|
|
180
187
|
@if (userName()) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
188
|
+
@if (userMenuItems().length > 0) {
|
|
189
|
+
<africanies-avatar-menu
|
|
190
|
+
[name]="userName()!"
|
|
191
|
+
[src]="userAvatarSrc()"
|
|
192
|
+
[menuItems]="userMenuItems()"
|
|
193
|
+
[size]="'md'"
|
|
194
|
+
/>
|
|
195
|
+
} @else {
|
|
196
|
+
<africanies-avatar
|
|
197
|
+
[name]="userName()!"
|
|
198
|
+
[src]="userAvatarSrc()"
|
|
199
|
+
size="md"
|
|
200
|
+
/>
|
|
201
|
+
}
|
|
187
202
|
}
|
|
188
203
|
</div>
|
|
189
204
|
}
|
|
@@ -274,15 +289,20 @@ export class AppShellHeaderComponent {
|
|
|
274
289
|
if (temp === undefined || !Number.isFinite(temp)) {
|
|
275
290
|
return null;
|
|
276
291
|
}
|
|
277
|
-
return
|
|
292
|
+
return String(Math.round(temp));
|
|
278
293
|
});
|
|
279
294
|
|
|
280
|
-
protected readonly
|
|
295
|
+
protected readonly weatherCondition = computed(() => {
|
|
281
296
|
const forecast = this.weather();
|
|
282
297
|
if (!forecast) {
|
|
283
298
|
return '';
|
|
284
299
|
}
|
|
285
|
-
return
|
|
300
|
+
return headerWeatherLabel(forecast.kind, this.now().getHours());
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
protected readonly weatherCity = computed(() => {
|
|
304
|
+
const city = this.weather()?.city?.trim();
|
|
305
|
+
return city || null;
|
|
286
306
|
});
|
|
287
307
|
|
|
288
308
|
protected readonly weatherAriaLabel = computed(() => {
|
|
@@ -290,10 +310,13 @@ export class AppShellHeaderComponent {
|
|
|
290
310
|
if (!forecast) {
|
|
291
311
|
return 'Weather';
|
|
292
312
|
}
|
|
293
|
-
const condition =
|
|
313
|
+
const condition = this.weatherCondition().toLowerCase();
|
|
294
314
|
const temp = this.weatherTemp();
|
|
295
|
-
const city =
|
|
296
|
-
const degrees =
|
|
315
|
+
const city = this.weatherCity();
|
|
316
|
+
const degrees =
|
|
317
|
+
temp === null
|
|
318
|
+
? condition
|
|
319
|
+
: `${temp} degrees Celsius, ${condition}`;
|
|
297
320
|
return city ? `Weather in ${city}: ${degrees}` : `Weather: ${degrees}`;
|
|
298
321
|
});
|
|
299
322
|
|
|
@@ -33,7 +33,7 @@ export interface HeaderWeather {
|
|
|
33
33
|
kind: HeaderWeatherKind;
|
|
34
34
|
/** Celsius, when the forecast includes it. */
|
|
35
35
|
temperatureC?: number;
|
|
36
|
-
/** City from IP geolocation, when known. */
|
|
36
|
+
/** City from reverse-geocode / IP geolocation, when known. */
|
|
37
37
|
city?: string;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -185,7 +185,11 @@ describe('loadHeaderWeather', () => {
|
|
|
185
185
|
return jsonResponse({ city: 'Abuja' });
|
|
186
186
|
}
|
|
187
187
|
if (String(url).includes('geojs')) {
|
|
188
|
-
|
|
188
|
+
return jsonResponse({
|
|
189
|
+
latitude: 6.5,
|
|
190
|
+
longitude: 3.4,
|
|
191
|
+
city: 'Lagos',
|
|
192
|
+
});
|
|
189
193
|
}
|
|
190
194
|
return jsonResponse({ current: { weather_code: 0, temperature_2m: 31 } });
|
|
191
195
|
});
|
|
@@ -197,16 +201,94 @@ describe('loadHeaderWeather', () => {
|
|
|
197
201
|
temperatureC: 31,
|
|
198
202
|
city: 'Abuja',
|
|
199
203
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('uses the IP city when browser geo succeeds but reverse geocode is empty', async () => {
|
|
207
|
+
Object.defineProperty(navigator, 'geolocation', {
|
|
208
|
+
configurable: true,
|
|
209
|
+
value: {
|
|
210
|
+
getCurrentPosition: (success: PositionCallback) => {
|
|
211
|
+
success({
|
|
212
|
+
coords: {
|
|
213
|
+
latitude: 9.0765,
|
|
214
|
+
longitude: 7.3986,
|
|
215
|
+
accuracy: 10,
|
|
216
|
+
altitude: null,
|
|
217
|
+
altitudeAccuracy: null,
|
|
218
|
+
heading: null,
|
|
219
|
+
speed: null,
|
|
220
|
+
toJSON: () => ({}),
|
|
221
|
+
},
|
|
222
|
+
timestamp: Date.now(),
|
|
223
|
+
toJSON: () => ({}),
|
|
224
|
+
} as GeolocationPosition);
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const fetchFn = jest.fn(async (url: string) => {
|
|
230
|
+
if (String(url).includes('bigdatacloud')) {
|
|
231
|
+
return jsonResponse({ city: '' });
|
|
232
|
+
}
|
|
233
|
+
if (String(url).includes('geojs')) {
|
|
234
|
+
return jsonResponse({
|
|
235
|
+
latitude: 6.5,
|
|
236
|
+
longitude: 3.4,
|
|
237
|
+
city: 'Lagos',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return jsonResponse({ current: { weather_code: 0, temperature_2m: 31 } });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await expect(
|
|
244
|
+
loadHeaderWeather(fetchFn as unknown as typeof fetch),
|
|
245
|
+
).resolves.toEqual({
|
|
246
|
+
kind: 'clear',
|
|
247
|
+
temperatureC: 31,
|
|
248
|
+
city: 'Lagos',
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('refetches when the hour cache has weather but no city', async () => {
|
|
253
|
+
sessionStorage.setItem(
|
|
254
|
+
'africanies-header-weather-v7',
|
|
255
|
+
JSON.stringify({
|
|
256
|
+
hour: `${new Date().getFullYear()}-${new Date().getMonth()}-${new Date().getDate()}-${new Date().getHours()}`,
|
|
257
|
+
kind: 'cloudy',
|
|
258
|
+
temperatureC: 25,
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const fetchFn = jest.fn(async (url: string) => {
|
|
263
|
+
if (String(url).includes('geojs')) {
|
|
264
|
+
return jsonResponse({ latitude: 1, longitude: 2, city: 'Lagos' });
|
|
265
|
+
}
|
|
266
|
+
if (String(url).includes('bigdatacloud')) {
|
|
267
|
+
return jsonResponse({ city: 'Lagos' });
|
|
268
|
+
}
|
|
269
|
+
return jsonResponse({ current: { weather_code: 3, temperature_2m: 25 } });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await expect(
|
|
273
|
+
loadHeaderWeather(fetchFn as unknown as typeof fetch),
|
|
274
|
+
).resolves.toEqual({
|
|
275
|
+
kind: 'cloudy',
|
|
276
|
+
temperatureC: 25,
|
|
277
|
+
city: 'Lagos',
|
|
278
|
+
});
|
|
279
|
+
expect(fetchFn).toHaveBeenCalled();
|
|
203
280
|
});
|
|
204
281
|
});
|
|
205
282
|
|
|
206
283
|
describe('headerWeatherLabel', () => {
|
|
207
284
|
it('returns a short condition', () => {
|
|
208
285
|
expect(headerWeatherLabel('rain')).toBe('Rain');
|
|
209
|
-
expect(headerWeatherLabel('
|
|
286
|
+
expect(headerWeatherLabel('cloudy')).toBe('Cloudy');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('uses Sunny by day and Clear at night', () => {
|
|
290
|
+
expect(headerWeatherLabel('clear', 10)).toBe('Sunny');
|
|
291
|
+
expect(headerWeatherLabel('clear', 22)).toBe('Clear');
|
|
210
292
|
});
|
|
211
293
|
});
|
|
212
294
|
|
|
@@ -3,10 +3,10 @@ import type { IconName } from '@africanies/africanies-icons';
|
|
|
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-
|
|
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 — city string is
|
|
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
11
|
/** Client reverse-geocode so the label matches the forecast coordinates. */
|
|
12
12
|
const REVERSE_GEO_URL =
|
|
@@ -88,9 +88,16 @@ export function mapWmoWeatherCode(code: number): HeaderWeatherKind | null {
|
|
|
88
88
|
* Short condition label for the header weather chip.
|
|
89
89
|
*
|
|
90
90
|
* @param kind - Coarse forecast kind.
|
|
91
|
-
* @
|
|
91
|
+
* @param hour - Local hour; clear days show as Sunny, nights as Clear.
|
|
92
|
+
* @returns Display label such as `Rain` or `Sunny`.
|
|
92
93
|
*/
|
|
93
|
-
export function headerWeatherLabel(
|
|
94
|
+
export function headerWeatherLabel(
|
|
95
|
+
kind: HeaderWeatherKind,
|
|
96
|
+
hour = new Date().getHours(),
|
|
97
|
+
): string {
|
|
98
|
+
if (kind === 'clear') {
|
|
99
|
+
return hour >= 19 || hour < 6 ? 'Clear' : 'Sunny';
|
|
100
|
+
}
|
|
94
101
|
return WEATHER_LABELS[kind];
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -116,8 +123,7 @@ export function headerWeatherIcon(kind: HeaderWeatherKind, hour: number): IconNa
|
|
|
116
123
|
*
|
|
117
124
|
* Coordinates prefer the browser Geolocation API (accurate for travellers;
|
|
118
125
|
* may prompt once). Otherwise IP geo is used. The city label is reverse-
|
|
119
|
-
* geocoded from those coordinates
|
|
120
|
-
* Nigeria (many networks resolve as Lagos).
|
|
126
|
+
* geocoded from those coordinates, with the IP city as a fallback.
|
|
121
127
|
*
|
|
122
128
|
* Fails closed: missing browser APIs, timeouts, and HTTP errors all return
|
|
123
129
|
* `null` so the greeting can stay time-of-day only. Cached per local hour
|
|
@@ -191,10 +197,16 @@ function readCache(hour: string): HeaderWeather | null {
|
|
|
191
197
|
if (parsed.hour !== hour || !parsed.kind) {
|
|
192
198
|
return null;
|
|
193
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
|
+
}
|
|
194
206
|
return {
|
|
195
207
|
kind: parsed.kind,
|
|
196
208
|
temperatureC: parsed.temperatureC,
|
|
197
|
-
city
|
|
209
|
+
city,
|
|
198
210
|
};
|
|
199
211
|
} catch {
|
|
200
212
|
return null;
|
|
@@ -211,30 +223,38 @@ function writeCache(value: CachedWeather): void {
|
|
|
211
223
|
|
|
212
224
|
/**
|
|
213
225
|
* Prefer device coordinates when available (may prompt once); otherwise fall
|
|
214
|
-
* back to IP geo. IP
|
|
215
|
-
*
|
|
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).
|
|
216
228
|
*/
|
|
217
229
|
async function resolveCoordinates(
|
|
218
230
|
fetchFn: typeof fetch,
|
|
219
231
|
): Promise<(Coordinates & { fallbackCity?: string }) | null> {
|
|
220
|
-
const browser = await
|
|
232
|
+
const [browser, ipGeo] = await Promise.all([
|
|
233
|
+
tryBrowserGeolocation(),
|
|
234
|
+
fetchIpGeo(fetchFn),
|
|
235
|
+
]);
|
|
236
|
+
const ipCity = asCity(ipGeo?.city);
|
|
237
|
+
|
|
221
238
|
if (browser) {
|
|
222
|
-
return browser;
|
|
239
|
+
return { ...browser, fallbackCity: ipCity };
|
|
223
240
|
}
|
|
224
241
|
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
const longitude = asNumber(geo?.longitude);
|
|
242
|
+
const latitude = asNumber(ipGeo?.latitude);
|
|
243
|
+
const longitude = asNumber(ipGeo?.longitude);
|
|
228
244
|
if (latitude === null || longitude === null) {
|
|
229
245
|
return null;
|
|
230
246
|
}
|
|
231
247
|
return {
|
|
232
248
|
latitude,
|
|
233
249
|
longitude,
|
|
234
|
-
fallbackCity:
|
|
250
|
+
fallbackCity: ipCity,
|
|
235
251
|
};
|
|
236
252
|
}
|
|
237
253
|
|
|
254
|
+
async function fetchIpGeo(fetchFn: typeof fetch): Promise<GeoJsPayload | null> {
|
|
255
|
+
return (await fetchJson(fetchFn, GEO_URL)) as GeoJsPayload | null;
|
|
256
|
+
}
|
|
257
|
+
|
|
238
258
|
async function tryBrowserGeolocation(): Promise<Coordinates | null> {
|
|
239
259
|
if (
|
|
240
260
|
typeof navigator === 'undefined' ||
|
|
@@ -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;
|