@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.
- 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/14892813664845064572/dist/libs/africanies-ui/README.md +68 -0
- package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
- package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
- package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +5 -0
- package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/assets/carriers/dhl.svg +7 -0
- package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +20013 -0
- package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
- package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/package.json +35 -0
- package/.nx/cache/14892813664845064572/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +5617 -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 +18 -18
- package/.nx/cache/terminalOutputs/1149535958171016935 +21 -0
- package/.nx/cache/terminalOutputs/14892813664845064572 +21 -0
- package/.nx/cache/terminalOutputs/17909810599991457481 +40 -0
- package/.nx/cache/terminalOutputs/2927637324428060203 +42 -0
- package/.nx/cache/terminalOutputs/3735952477975556635 +21 -0
- package/.nx/cache/terminalOutputs/4787481949164239892 +8 -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 +3807 -0
- package/.nx/workspace-data/d/server-process.json +2 -2
- package/.nx/workspace-data/file-map.json +3254 -3260
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/project-graph.json +2 -2
- package/README.md +0 -17
- 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 +986 -473
- 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 -39
- 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.spec.ts +15 -0
- package/libs/africanies-ui/src/lib/navigation/shipping-mode-switch/shipping-mode-switch.component.ts +195 -64
- 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 +254 -109
- package/package.json +1 -1
- package/tools/build-icon-sprite.mjs +1 -1
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* text-export-strong bg-export-strong hover:bg-export-hover border-export-strong
|
|
13
13
|
* text-export bg-export bg-export-subtle bg-export-light border-export
|
|
14
14
|
* hover:bg-export-light hover:bg-export-subtle dark:bg-export/15 dark:hover:bg-export/15
|
|
15
|
-
* dark:bg-[color-mix(in_srgb,#
|
|
15
|
+
* dark:bg-[color-mix(in_srgb,#26bd5d_15%,#212529)] dark:bg-export dark:text-export dark:border-export
|
|
16
16
|
*
|
|
17
17
|
* STN (import / orange):
|
|
18
18
|
* text-import-strong bg-import-strong hover:bg-import-hover border-import-strong
|
|
@@ -35,7 +35,7 @@ export const MODE_COLOR_SAFELIST = [
|
|
|
35
35
|
'dark:bg-export/15',
|
|
36
36
|
'dark:hover:bg-export/15',
|
|
37
37
|
'dark:hover:bg-export-light',
|
|
38
|
-
'dark:bg-[color-mix(in_srgb,#
|
|
38
|
+
'dark:bg-[color-mix(in_srgb,#26bd5d_15%,#212529)]',
|
|
39
39
|
'dark:bg-export',
|
|
40
40
|
'dark:text-export',
|
|
41
41
|
'dark:border-export',
|
|
@@ -26,14 +26,16 @@ describe('ModeColorService', () => {
|
|
|
26
26
|
service = TestBed.inject(ModeColorService);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it('uses
|
|
29
|
+
it('uses export brand fills and text for SFN in light and dark', () => {
|
|
30
30
|
const classes = service.classes();
|
|
31
|
-
|
|
32
|
-
expect(classes.
|
|
33
|
-
expect(
|
|
34
|
-
expect(
|
|
35
|
-
expect(classes.
|
|
36
|
-
expect(classes.
|
|
31
|
+
const primaryTokens = classes.primary.split(/\s+/);
|
|
32
|
+
expect(classes.text.split(/\s+/)).toContain('text-export');
|
|
33
|
+
expect(primaryTokens).toContain('bg-export');
|
|
34
|
+
expect(primaryTokens).not.toContain('bg-export-strong');
|
|
35
|
+
expect(classes.ghostPrimary.split(/\s+/)).toContain('text-export');
|
|
36
|
+
expect(classes.activeFill.split(/\s+/)).toContain('bg-export');
|
|
37
|
+
expect(classes.stroked.split(/\s+/)).toContain('text-export');
|
|
38
|
+
expect(classes.stroked.split(/\s+/)).toContain('border-export');
|
|
37
39
|
});
|
|
38
40
|
|
|
39
41
|
it('keeps bright export accents for dark-mode companions in SFN', () => {
|
|
@@ -9,7 +9,9 @@ import { ShippingModeService } from '@africanies/africanies-core';
|
|
|
9
9
|
* and accent text. Semantic colors (danger / warning) stay mode-independent.
|
|
10
10
|
*/
|
|
11
11
|
export interface ModeColorClasses {
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Accessible accent text — mode brand color in light and dark.
|
|
14
|
+
*/
|
|
13
15
|
text: string;
|
|
14
16
|
/** Solid background utility, e.g. `bg-export`. */
|
|
15
17
|
bg: string;
|
|
@@ -80,22 +82,22 @@ export class ModeColorService {
|
|
|
80
82
|
// Literal strings required for Tailwind content detection — do not interpolate.
|
|
81
83
|
if (this.shippingMode.mode() === 'sfn') {
|
|
82
84
|
return {
|
|
83
|
-
text: 'text-export
|
|
85
|
+
text: 'text-export dark:text-export',
|
|
84
86
|
bg: 'bg-export',
|
|
85
87
|
bgSubtle: 'bg-export-subtle',
|
|
86
88
|
border: 'border-export',
|
|
87
89
|
primary:
|
|
88
|
-
'bg-export
|
|
90
|
+
'bg-export text-white border-transparent hover:bg-export-strong dark:bg-export dark:hover:bg-export-light',
|
|
89
91
|
ghostPrimary:
|
|
90
|
-
'bg-transparent text-export
|
|
92
|
+
'bg-transparent text-export border-transparent hover:bg-export-subtle dark:text-export dark:hover:bg-export/15',
|
|
91
93
|
soft: 'bg-export-subtle dark:bg-export/15',
|
|
92
94
|
softSolid:
|
|
93
|
-
'bg-export-subtle dark:bg-[color-mix(in_srgb,#
|
|
95
|
+
'bg-export-subtle dark:bg-[color-mix(in_srgb,#26bd5d_15%,#212529)]',
|
|
94
96
|
softHover: 'hover:bg-export-subtle dark:hover:bg-export/15',
|
|
95
97
|
activeFill:
|
|
96
|
-
'bg-export
|
|
98
|
+
'bg-export text-white border-transparent hover:bg-export-strong dark:bg-export dark:text-white dark:hover:bg-export-light',
|
|
97
99
|
stroked:
|
|
98
|
-
'bg-surface text-export
|
|
100
|
+
'bg-surface text-export border-export hover:bg-export-subtle dark:bg-ink-950 dark:text-export dark:border-export dark:hover:bg-export/15',
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
103
|
return {
|
|
@@ -270,7 +270,7 @@ module.exports = {
|
|
|
270
270
|
hover: '#e9eef5',
|
|
271
271
|
},
|
|
272
272
|
export: {
|
|
273
|
-
DEFAULT: '#
|
|
273
|
+
DEFAULT: '#26bd5d',
|
|
274
274
|
light: '#24dc6d',
|
|
275
275
|
subtle: '#e4fff3',
|
|
276
276
|
tint: '#f2fff8',
|
|
@@ -282,8 +282,8 @@ module.exports = {
|
|
|
282
282
|
DEFAULT: '#f08829',
|
|
283
283
|
light: '#ffa95b',
|
|
284
284
|
subtle: '#fffcef',
|
|
285
|
-
//
|
|
286
|
-
strong: '#
|
|
285
|
+
// Brand orange (same as DEFAULT) — portal STN primary.
|
|
286
|
+
strong: '#f08829',
|
|
287
287
|
hover: '#7f3e09',
|
|
288
288
|
},
|
|
289
289
|
danger: {
|
|
@@ -37,28 +37,32 @@ import { AvatarComponent, type AvatarSize } from './avatar.component';
|
|
|
37
37
|
AfricaniesIconComponent,
|
|
38
38
|
],
|
|
39
39
|
template: `
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<button
|
|
46
|
-
type="button"
|
|
47
|
-
africaniesActionMenuTrigger
|
|
48
|
-
class="inline-flex cursor-pointer items-center gap-1 rounded-lg p-0.5 pr-1 transition-colors hover:bg-background-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-white/10"
|
|
49
|
-
[attr.aria-label]="ariaLabel()"
|
|
40
|
+
@if (menuItems().length > 0) {
|
|
41
|
+
<africanies-action-menu
|
|
42
|
+
[items]="menuItems()"
|
|
43
|
+
[ariaLabel]="ariaLabel()"
|
|
44
|
+
[disabled]="disabled()"
|
|
50
45
|
>
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
africaniesActionMenuTrigger
|
|
49
|
+
class="inline-flex cursor-pointer items-center gap-1 rounded-lg p-0.5 pr-1 transition-colors hover:bg-background-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-white/10"
|
|
50
|
+
[attr.aria-label]="ariaLabel()"
|
|
51
|
+
>
|
|
52
|
+
<africanies-avatar [name]="name()" [src]="src()" [size]="size()" />
|
|
53
|
+
@if (showCaret()) {
|
|
54
|
+
<africanies-icon
|
|
55
|
+
name="chevron-down"
|
|
56
|
+
[size]="14"
|
|
57
|
+
class="shrink-0 text-neutral-600 dark:text-neutral-400"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
/>
|
|
60
|
+
}
|
|
61
|
+
</button>
|
|
62
|
+
</africanies-action-menu>
|
|
63
|
+
} @else {
|
|
64
|
+
<africanies-avatar [name]="name()" [src]="src()" [size]="size()" />
|
|
65
|
+
}
|
|
62
66
|
`,
|
|
63
67
|
})
|
|
64
68
|
export class AvatarMenuComponent {
|
|
@@ -65,29 +65,34 @@ describe('ButtonComponent', () => {
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
it('should apply primary variant classes by default (SFN / export)', () => {
|
|
68
|
-
|
|
69
|
-
expect(
|
|
68
|
+
const tokens = button.className.split(/\s+/);
|
|
69
|
+
expect(tokens).toContain('bg-export');
|
|
70
|
+
expect(tokens).toContain('text-white');
|
|
71
|
+
expect(tokens).not.toContain('bg-export-strong');
|
|
70
72
|
});
|
|
71
73
|
|
|
72
74
|
it('should switch primary to import orange when mode is STN', () => {
|
|
73
75
|
mode.set('stn');
|
|
74
76
|
fixture.detectChanges();
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
expect(
|
|
78
|
+
const tokens = button.className.split(/\s+/);
|
|
79
|
+
expect(tokens).toContain('bg-import-strong');
|
|
80
|
+
expect(tokens).not.toContain('bg-export');
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
it('should switch ghost-primary text accent with shipping mode', () => {
|
|
81
84
|
host.variant.set('ghost-primary');
|
|
82
85
|
fixture.detectChanges();
|
|
83
|
-
|
|
84
|
-
expect(
|
|
86
|
+
let tokens = button.className.split(/\s+/);
|
|
87
|
+
expect(tokens).toContain('text-export');
|
|
88
|
+
expect(tokens).toContain('bg-transparent');
|
|
85
89
|
|
|
86
90
|
mode.set('stn');
|
|
87
91
|
fixture.detectChanges();
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
expect(
|
|
93
|
+
tokens = button.className.split(/\s+/);
|
|
94
|
+
expect(tokens).toContain('text-import-strong');
|
|
95
|
+
expect(tokens).not.toContain('text-export');
|
|
91
96
|
});
|
|
92
97
|
|
|
93
98
|
it('should switch variant and size classes', () => {
|
|
@@ -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"
|
|
@@ -119,7 +124,9 @@ export class AppShellHeaderEndDirective {}
|
|
|
119
124
|
<span
|
|
120
125
|
class="hidden text-caption text-neutral-600 md:inline dark:text-neutral-400"
|
|
121
126
|
>
|
|
122
|
-
{{ weatherCondition() }}
|
|
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
|
}
|
|
@@ -285,6 +300,11 @@ export class AppShellHeaderComponent {
|
|
|
285
300
|
return headerWeatherLabel(forecast.kind, this.now().getHours());
|
|
286
301
|
});
|
|
287
302
|
|
|
303
|
+
protected readonly weatherCity = computed(() => {
|
|
304
|
+
const city = this.weather()?.city?.trim();
|
|
305
|
+
return city || null;
|
|
306
|
+
});
|
|
307
|
+
|
|
288
308
|
protected readonly weatherAriaLabel = computed(() => {
|
|
289
309
|
const forecast = this.weather();
|
|
290
310
|
if (!forecast) {
|
|
@@ -292,9 +312,12 @@ export class AppShellHeaderComponent {
|
|
|
292
312
|
}
|
|
293
313
|
const condition = this.weatherCondition().toLowerCase();
|
|
294
314
|
const temp = this.weatherTemp();
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
315
|
+
const city = this.weatherCity();
|
|
316
|
+
const degrees =
|
|
317
|
+
temp === null
|
|
318
|
+
? condition
|
|
319
|
+
: `${temp} degrees Celsius, ${condition}`;
|
|
320
|
+
return city ? `Weather in ${city}: ${degrees}` : `Weather: ${degrees}`;
|
|
298
321
|
});
|
|
299
322
|
|
|
300
323
|
protected readonly unreadCount = computed(
|
|
@@ -33,6 +33,8 @@ export interface HeaderWeather {
|
|
|
33
33
|
kind: HeaderWeatherKind;
|
|
34
34
|
/** Celsius, when the forecast includes it. */
|
|
35
35
|
temperatureC?: number;
|
|
36
|
+
/** City from reverse-geocode / IP geolocation, when known. */
|
|
37
|
+
city?: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const PERIOD_KICKERS: Record<HeaderGreetingPeriod, readonly string[]> = {
|
|
@@ -66,7 +66,7 @@ describe('loadHeaderWeather', () => {
|
|
|
66
66
|
).resolves.toBeNull();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it('reads Open-Meteo after IP
|
|
69
|
+
it('reads Open-Meteo after IP geolocation and reverse-geocodes the city', async () => {
|
|
70
70
|
const fetchFn = jest.fn(async (url: string) => {
|
|
71
71
|
if (String(url).includes('geojs')) {
|
|
72
72
|
return jsonResponse({
|
|
@@ -75,6 +75,9 @@ describe('loadHeaderWeather', () => {
|
|
|
75
75
|
city: 'Lagos',
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
+
if (String(url).includes('bigdatacloud')) {
|
|
79
|
+
return jsonResponse({ city: 'Abuja', locality: 'Abuja' });
|
|
80
|
+
}
|
|
78
81
|
return jsonResponse({
|
|
79
82
|
current: { weather_code: 61, temperature_2m: 27.4 },
|
|
80
83
|
});
|
|
@@ -85,8 +88,29 @@ describe('loadHeaderWeather', () => {
|
|
|
85
88
|
).resolves.toEqual({
|
|
86
89
|
kind: 'rain',
|
|
87
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,
|
|
112
|
+
city: 'Lagos',
|
|
88
113
|
});
|
|
89
|
-
expect(fetchFn).toHaveBeenCalledTimes(2);
|
|
90
114
|
});
|
|
91
115
|
|
|
92
116
|
it('reuses the same-hour session cache', async () => {
|
|
@@ -94,6 +118,9 @@ describe('loadHeaderWeather', () => {
|
|
|
94
118
|
if (String(url).includes('geojs')) {
|
|
95
119
|
return jsonResponse({ latitude: 1, longitude: 2 });
|
|
96
120
|
}
|
|
121
|
+
if (String(url).includes('bigdatacloud')) {
|
|
122
|
+
return jsonResponse({ city: 'Accra' });
|
|
123
|
+
}
|
|
97
124
|
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30 } });
|
|
98
125
|
});
|
|
99
126
|
|
|
@@ -101,13 +128,16 @@ describe('loadHeaderWeather', () => {
|
|
|
101
128
|
const second = await loadHeaderWeather(fetchFn as unknown as typeof fetch);
|
|
102
129
|
|
|
103
130
|
expect(first).toEqual(second);
|
|
104
|
-
expect(fetchFn).toHaveBeenCalledTimes(
|
|
131
|
+
expect(fetchFn).toHaveBeenCalledTimes(3);
|
|
105
132
|
});
|
|
106
133
|
|
|
107
134
|
it('fetches again when the cached hour no longer matches', async () => {
|
|
108
135
|
const fetchFn = jest.fn(async (url: string) => {
|
|
109
136
|
if (String(url).includes('geojs')) {
|
|
110
|
-
return jsonResponse({ latitude: 1, longitude: 2 });
|
|
137
|
+
return jsonResponse({ latitude: 1, longitude: 2, city: 'Lagos' });
|
|
138
|
+
}
|
|
139
|
+
if (String(url).includes('bigdatacloud')) {
|
|
140
|
+
return jsonResponse({ city: 'Lagos' });
|
|
111
141
|
}
|
|
112
142
|
return jsonResponse({ current: { weather_code: 0, temperature_2m: 30.2 } });
|
|
113
143
|
});
|
|
@@ -124,7 +154,7 @@ describe('loadHeaderWeather', () => {
|
|
|
124
154
|
sessionStorage.setItem(cacheKey!, JSON.stringify(stored));
|
|
125
155
|
|
|
126
156
|
await loadHeaderWeather(fetchFn as unknown as typeof fetch);
|
|
127
|
-
expect(fetchFn).toHaveBeenCalledTimes(
|
|
157
|
+
expect(fetchFn).toHaveBeenCalledTimes(6);
|
|
128
158
|
});
|
|
129
159
|
|
|
130
160
|
it('uses browser coordinates when geolocation succeeds', async () => {
|
|
@@ -151,8 +181,61 @@ describe('loadHeaderWeather', () => {
|
|
|
151
181
|
});
|
|
152
182
|
|
|
153
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
|
+
return jsonResponse({
|
|
189
|
+
latitude: 6.5,
|
|
190
|
+
longitude: 3.4,
|
|
191
|
+
city: 'Lagos',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return jsonResponse({ current: { weather_code: 0, temperature_2m: 31 } });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await expect(
|
|
198
|
+
loadHeaderWeather(fetchFn as unknown as typeof fetch),
|
|
199
|
+
).resolves.toEqual({
|
|
200
|
+
kind: 'clear',
|
|
201
|
+
temperatureC: 31,
|
|
202
|
+
city: 'Abuja',
|
|
203
|
+
});
|
|
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
|
+
}
|
|
154
233
|
if (String(url).includes('geojs')) {
|
|
155
|
-
|
|
234
|
+
return jsonResponse({
|
|
235
|
+
latitude: 6.5,
|
|
236
|
+
longitude: 3.4,
|
|
237
|
+
city: 'Lagos',
|
|
238
|
+
});
|
|
156
239
|
}
|
|
157
240
|
return jsonResponse({ current: { weather_code: 0, temperature_2m: 31 } });
|
|
158
241
|
});
|
|
@@ -162,10 +245,38 @@ describe('loadHeaderWeather', () => {
|
|
|
162
245
|
).resolves.toEqual({
|
|
163
246
|
kind: 'clear',
|
|
164
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',
|
|
165
278
|
});
|
|
166
|
-
expect(
|
|
167
|
-
fetchFn.mock.calls.some((call) => String(call[0]).includes('geojs')),
|
|
168
|
-
).toBe(false);
|
|
279
|
+
expect(fetchFn).toHaveBeenCalled();
|
|
169
280
|
});
|
|
170
281
|
});
|
|
171
282
|
|