@foxeltech/angular-ui 0.7.108 → 0.7.124
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/components.css +19 -1
- package/fesm2022/foxeltech-angular-ui-chart.mjs +25 -1
- package/fesm2022/foxeltech-angular-ui-chart.mjs.map +1 -1
- package/fesm2022/foxeltech-angular-ui.mjs +73 -21
- package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
- package/package.json +3 -2
- package/src/lib/styles/components.css +19 -1
- package/src/lib/styles/tokens.css +3 -0
- package/src/lib/ui/components/bar-list/bar-list.component.html +19 -18
- package/src/lib/ui/components/bar-list/bar-list.component.scss +63 -0
- package/src/lib/ui/components/breadcrumb/breadcrumb.component.css +31 -0
- package/src/lib/ui/components/breadcrumb/breadcrumb.component.html +6 -1
- package/src/lib/ui/components/drawer/drawer.component.html +32 -7
- package/src/lib/ui/components/drawer/drawer.component.scss +114 -17
- package/src/lib/ui/components/filter-pills/filter-pills.component.html +4 -1
- package/src/lib/ui/components/filter-pills/filter-pills.component.scss +23 -0
- package/src/lib/ui/components/section-card/section-card.component.html +10 -5
- package/src/lib/ui/components/section-card/section-card.component.scss +28 -0
- package/src/lib/ui/components/stat-cards/stat-cards.component.css +48 -0
- package/src/lib/ui/components/stat-cards/stat-cards.component.html +16 -7
- package/tokens.css +3 -0
- package/types/foxeltech-angular-ui.d.ts +70 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foxeltech/angular-ui",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.124",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"@microsoft/signalr": "^10.0.0",
|
|
27
27
|
"d3-drag": "^3.0.0",
|
|
28
28
|
"d3-selection": "^3.0.0",
|
|
29
|
-
"d3-zoom": "^3.0.0"
|
|
29
|
+
"d3-zoom": "^3.0.0",
|
|
30
|
+
"lucide-angular": "^1.0.0"
|
|
30
31
|
},
|
|
31
32
|
"exports": {
|
|
32
33
|
"./tailwind.css": "./tailwind.css",
|
|
@@ -631,6 +631,10 @@ input[type='password'] {
|
|
|
631
631
|
0 1px 2px rgb(0 0 0 / 0.15);
|
|
632
632
|
}
|
|
633
633
|
.btn-cta:hover {
|
|
634
|
+
/* The edge darkens a step past the fill — reference `.bi-newbtn:hover`
|
|
635
|
+
(measured 16/09). Without it the button only changed fill and shadow, so
|
|
636
|
+
the hover read softer than the design's. */
|
|
637
|
+
border-color: var(--color-cta-hover-border);
|
|
634
638
|
box-shadow:
|
|
635
639
|
inset 0 1px 0 rgb(255 255 255 / 0.18),
|
|
636
640
|
0 2px 6px rgb(0 0 0 / 0.2);
|
|
@@ -971,7 +975,9 @@ input[type='password'] {
|
|
|
971
975
|
font-size: var(--og-fs-xs);
|
|
972
976
|
line-height: 1.5;
|
|
973
977
|
white-space: nowrap;
|
|
974
|
-
|
|
978
|
+
/* 400, per the reference's risk badge ("High · 82", measured 16/09). At 500
|
|
979
|
+
it sat a step heavier than every other 11.5px label around it. */
|
|
980
|
+
font-weight: 400;
|
|
975
981
|
}
|
|
976
982
|
.fx-badge--negative {
|
|
977
983
|
background: rgb(var(--og-negative-soft-bg));
|
|
@@ -1032,3 +1038,15 @@ input[type='password'] {
|
|
|
1032
1038
|
.fx-statusdot--accent::before {
|
|
1033
1039
|
background: rgb(var(--og-accent));
|
|
1034
1040
|
}
|
|
1041
|
+
|
|
1042
|
+
/* A tooltip must never take the pointer. Material gives its overlay pane
|
|
1043
|
+
`pointer-events: auto`, and with the drawer COLLAPSED a nav item's tooltip
|
|
1044
|
+
opens at x≈70..110 while the collapse/expand button sits at 58..82 — the
|
|
1045
|
+
tooltip covered the button's right half and swallowed the click, which is
|
|
1046
|
+
why the toggle "sometimes" did nothing (MEASURED 16/09). Nothing in a
|
|
1047
|
+
tooltip is interactive, so this is safe everywhere. */
|
|
1048
|
+
.mat-mdc-tooltip-panel,
|
|
1049
|
+
.mat-mdc-tooltip,
|
|
1050
|
+
.mat-mdc-tooltip-panel > * {
|
|
1051
|
+
pointer-events: none !important;
|
|
1052
|
+
}
|
|
@@ -53,6 +53,9 @@
|
|
|
53
53
|
|
|
54
54
|
--color-cta: #c2582a;
|
|
55
55
|
--color-cta-hover: #b34e24;
|
|
56
|
+
/* Border of the CTA while hovered — the reference's `.bi-newbtn:hover`
|
|
57
|
+
darkens the edge one more step than the fill (16/09). */
|
|
58
|
+
--color-cta-hover-border: #a3461f;
|
|
56
59
|
|
|
57
60
|
/* ---- Legacy colour names (@deprecated — remove after the V2 campaign;
|
|
58
61
|
they resolve through the alias layer in theme.css, so every existing
|
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<!-- Ranked list, to the reference's "Top projects by risk" (measured in the
|
|
2
|
+
browser 16/09): the SCORE leads, right-aligned in a 70px column, then the
|
|
3
|
+
label in a 150px column, then a 3px track taking the rest. It used to be
|
|
4
|
+
label-left / score-right with a 5px full-width track underneath, which is a
|
|
5
|
+
different object — two lines per row instead of one. -->
|
|
6
|
+
<div class="fx-barlist flex flex-col">
|
|
7
|
+
@for (item of items(); track item.label; let first = $first) {
|
|
3
8
|
<button
|
|
4
9
|
type="button"
|
|
5
|
-
class="text-left
|
|
10
|
+
class="fx-barlist__row text-left flex items-center"
|
|
6
11
|
[class.hover:bg-surface-sunken]="interactive()"
|
|
7
12
|
[class.cursor-default]="!interactive()"
|
|
8
13
|
[tabindex]="interactive() ? 0 : -1"
|
|
9
14
|
(click)="onSelect(item)"
|
|
10
15
|
>
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@if (item.meta) {
|
|
18
|
-
<div class="txt-utility-description truncate">{{ item.meta }}</div>
|
|
19
|
-
}
|
|
20
|
-
<div class="mt-xs h-[var(--og-track-h)] rounded-pill bg-track overflow-hidden">
|
|
21
|
-
<div
|
|
22
|
-
class="h-full rounded-pill transition-all duration-500"
|
|
23
|
-
[ngClass]="barClass(item)"
|
|
16
|
+
<span class="fx-barlist__score shrink-0">{{ item.valueLabel ?? item.value }}</span>
|
|
17
|
+
<span class="fx-barlist__label shrink-0 truncate">{{ item.label }}</span>
|
|
18
|
+
<span class="fx-barlist__track flex-1">
|
|
19
|
+
<span
|
|
20
|
+
class="fx-barlist__fill block"
|
|
21
|
+
[class.fx-barlist__fill--lead]="first"
|
|
24
22
|
[style.width.%]="widthOf(item)"
|
|
25
|
-
></
|
|
26
|
-
</
|
|
23
|
+
></span>
|
|
24
|
+
</span>
|
|
27
25
|
</button>
|
|
26
|
+
@if (item.meta) {
|
|
27
|
+
<div class="fx-barlist__meta truncate">{{ item.meta }}</div>
|
|
28
|
+
}
|
|
28
29
|
}
|
|
29
30
|
</div>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
@reference '../../../styles/tailwind.css';
|
|
2
|
+
|
|
3
|
+
/* Metrics from the reference (measured 16/09): rows 8px apart, each 18px tall
|
|
4
|
+
with a 10px gap between its three cells. */
|
|
5
|
+
.fx-barlist {
|
|
6
|
+
gap: 8px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.fx-barlist__row {
|
|
10
|
+
gap: 10px;
|
|
11
|
+
min-height: 18px;
|
|
12
|
+
/* A right gutter so the bar stops short of the panel edge (anh Tú 16/09).
|
|
13
|
+
The reference's row is 387px wide and its 147px track happens to end level
|
|
14
|
+
with the card's padding; this panel is twice that, so a track that fills
|
|
15
|
+
the remainder runs right up to the edge and reads as an overflow. */
|
|
16
|
+
padding: 0 24px 0 0;
|
|
17
|
+
background: transparent;
|
|
18
|
+
border: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Score first, right-aligned in its own column — that alignment is what makes
|
|
22
|
+
the numbers read as a ranked column rather than trailing labels. */
|
|
23
|
+
.fx-barlist__score {
|
|
24
|
+
width: 70px;
|
|
25
|
+
text-align: right;
|
|
26
|
+
font-family: var(--og-font-mono);
|
|
27
|
+
font-size: var(--og-fs-sm);
|
|
28
|
+
font-weight: 400;
|
|
29
|
+
color: rgb(var(--og-text));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.fx-barlist__label {
|
|
33
|
+
width: 150px;
|
|
34
|
+
font-size: var(--og-fs-sm);
|
|
35
|
+
font-weight: 400;
|
|
36
|
+
color: rgb(var(--og-text-muted));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* A 3px hairline track, square ends — not the 5px pill it was. */
|
|
40
|
+
.fx-barlist__track {
|
|
41
|
+
height: 3px;
|
|
42
|
+
background: rgb(var(--og-track));
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Every bar is muted EXCEPT the leader, which takes the accent. The reference
|
|
47
|
+
ranks by one colour and highlights the top row; tone-per-row turned the
|
|
48
|
+
panel into a severity chart, which is the donut's job, not this one. */
|
|
49
|
+
.fx-barlist__fill {
|
|
50
|
+
height: 100%;
|
|
51
|
+
background: rgb(var(--og-text-muted));
|
|
52
|
+
transition: width 500ms ease;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.fx-barlist__fill--lead {
|
|
56
|
+
background: rgb(var(--og-accent));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.fx-barlist__meta {
|
|
60
|
+
font-size: var(--og-fs-2xs);
|
|
61
|
+
color: rgb(var(--og-text-faint));
|
|
62
|
+
padding-left: 80px;
|
|
63
|
+
}
|
|
@@ -260,3 +260,34 @@
|
|
|
260
260
|
.text-faint { color: rgb(var(--og-text-faint)); }
|
|
261
261
|
.text-strong { color: rgb(var(--og-text)); }
|
|
262
262
|
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
263
|
+
|
|
264
|
+
/* CTA crumb — the reference's "+ Create project" at the end of the Project
|
|
265
|
+
Management trail (measured 16/09: 24px tall, 4px 10px 4px 8px, gap 5,
|
|
266
|
+
10.5px/600, radius 7, a light accent fill with an accent border).
|
|
267
|
+
The exact demo colours are #eaf1fa / #b9cde6 / #2a4f7c; the accent-soft
|
|
268
|
+
TOKENS are used instead so the pill follows the theme in all six — they sit
|
|
269
|
+
within ~3 of those values in light mode, and the border is mixed from the
|
|
270
|
+
accent rather than hard-coded because no token matches it. */
|
|
271
|
+
.fx-crumb-cta {
|
|
272
|
+
appearance: none;
|
|
273
|
+
display: inline-flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
gap: 5px;
|
|
276
|
+
min-width: 0;
|
|
277
|
+
padding: 4px 10px 4px 8px;
|
|
278
|
+
line-height: 1.5;
|
|
279
|
+
border: 1px solid color-mix(in srgb, rgb(var(--og-accent)) 35%, transparent);
|
|
280
|
+
border-radius: var(--og-radius-control);
|
|
281
|
+
background: rgb(var(--og-accent-soft-bg));
|
|
282
|
+
color: rgb(var(--og-accent-soft-fg));
|
|
283
|
+
font-size: var(--og-fs-2xs);
|
|
284
|
+
font-weight: 600;
|
|
285
|
+
white-space: nowrap;
|
|
286
|
+
cursor: pointer;
|
|
287
|
+
transition: background-color 130ms ease, border-color 130ms ease;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.fx-crumb-cta:hover {
|
|
291
|
+
background: color-mix(in srgb, rgb(var(--og-accent)) 16%, rgb(var(--og-accent-soft-bg)));
|
|
292
|
+
border-color: color-mix(in srgb, rgb(var(--og-accent)) 55%, transparent);
|
|
293
|
+
}
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
@if (i > 0) {
|
|
4
4
|
<span aria-hidden="true" class="fx-crumb-sep">/</span>
|
|
5
5
|
}
|
|
6
|
-
@if (
|
|
6
|
+
@if (crumb.cta) {
|
|
7
|
+
<button type="button" class="fx-crumb-cta" (click)="go($event, crumb.url || '')">
|
|
8
|
+
<fx-ui-hero-icon [icon]="crumb.icon || 'plus'" [size]="13" class="flex shrink-0" />
|
|
9
|
+
<span class="truncate">{{ crumb.label }}</span>
|
|
10
|
+
</button>
|
|
11
|
+
} @else if (isChip(crumb)) {
|
|
7
12
|
<span class="relative inline-flex min-w-0">
|
|
8
13
|
<button
|
|
9
14
|
type="button"
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
<div class="drawer-host-wrapper">
|
|
2
2
|
<a href="/" class="drawer-brand" [class.is-collapsed]="!isExpanded">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
<!-- Logo sits in its own sunken tile (reference `.sb-logo`, measured 16/09):
|
|
4
|
+
the tile is what carries the hover border, so the mark keeps its own
|
|
5
|
+
size and nothing shifts by a pixel when that border appears. -->
|
|
6
|
+
<span class="drawer-brand-logo">
|
|
7
|
+
@if (logo) {
|
|
8
|
+
<img [src]="logo" alt="Home" class="drawer-brand-mark" />
|
|
9
|
+
} @else {
|
|
10
|
+
<span class="drawer-brand-mark"></span>
|
|
11
|
+
}
|
|
12
|
+
</span>
|
|
8
13
|
@if (isExpanded && (logoText || logoSubtext)) {
|
|
9
14
|
<span class="drawer-brand-copy">
|
|
10
15
|
@if (logoText) {
|
|
@@ -87,6 +92,7 @@
|
|
|
87
92
|
<a
|
|
88
93
|
[routerLink]="item.route"
|
|
89
94
|
class="drawer-item"
|
|
95
|
+
[class.is-collapsed]="!isExpanded"
|
|
90
96
|
[class.active]="isActiveRoute(item.route)"
|
|
91
97
|
[class.has-children]="item.children && item.children.length > 0"
|
|
92
98
|
[style.paddingLeft.px]="isExpanded ? 8 + level * 16 : 16"
|
|
@@ -96,7 +102,16 @@
|
|
|
96
102
|
(click)="onItemClick(item, $event)"
|
|
97
103
|
>
|
|
98
104
|
<div class="item-content">
|
|
99
|
-
|
|
105
|
+
@if (item.iconData) {
|
|
106
|
+
<lucide-icon
|
|
107
|
+
[img]="item.iconData"
|
|
108
|
+
[size]="isExpanded ? 13 : 18"
|
|
109
|
+
class="item-icon"
|
|
110
|
+
[class.item-icon--solid]="item.iconSolid"
|
|
111
|
+
/>
|
|
112
|
+
} @else {
|
|
113
|
+
<fx-ui-hero-icon [icon]="item.icon" [size]="isExpanded ? 13 : 18" class="item-icon"></fx-ui-hero-icon>
|
|
114
|
+
}
|
|
100
115
|
@if (isExpanded) {
|
|
101
116
|
<span class="item-label">{{ item.label }}</span>
|
|
102
117
|
}
|
|
@@ -108,6 +123,7 @@
|
|
|
108
123
|
<button
|
|
109
124
|
type="button"
|
|
110
125
|
class="drawer-item"
|
|
126
|
+
[class.is-collapsed]="!isExpanded"
|
|
111
127
|
[class.active]="isActiveRoute(item.route)"
|
|
112
128
|
[class.has-children]="item.children && item.children.length > 0"
|
|
113
129
|
[style.paddingLeft.px]="isExpanded ? 8 + level * 16 : 16"
|
|
@@ -117,7 +133,16 @@
|
|
|
117
133
|
(click)="onItemClick(item, $event)"
|
|
118
134
|
>
|
|
119
135
|
<div class="item-content">
|
|
120
|
-
|
|
136
|
+
@if (item.iconData) {
|
|
137
|
+
<lucide-icon
|
|
138
|
+
[img]="item.iconData"
|
|
139
|
+
[size]="isExpanded ? 13 : 18"
|
|
140
|
+
class="item-icon"
|
|
141
|
+
[class.item-icon--solid]="item.iconSolid"
|
|
142
|
+
/>
|
|
143
|
+
} @else {
|
|
144
|
+
<fx-ui-hero-icon [icon]="item.icon" [size]="isExpanded ? 13 : 18" class="item-icon"></fx-ui-hero-icon>
|
|
145
|
+
}
|
|
121
146
|
@if (isExpanded) {
|
|
122
147
|
<span class="item-label">{{ item.label }}</span>
|
|
123
148
|
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
trước đó là 16px, tức 24px tổng). Brand nằm NGOÀI container nên phải tự
|
|
31
31
|
cộng đủ 16px. */
|
|
32
32
|
.drawer-brand {
|
|
33
|
-
@apply h-[60px] shrink-0 flex items-center gap-
|
|
33
|
+
@apply h-[60px] shrink-0 flex items-center gap-[10px] no-underline overflow-hidden;
|
|
34
34
|
padding-left: 16px;
|
|
35
35
|
padding-right: 8px;
|
|
36
36
|
transition: padding 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
@@ -51,8 +51,38 @@
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/* Sunken tile behind the mark (reference `.sb-logo`, measured 16/09: 6px pad,
|
|
55
|
+
radius-control, --og-surface-sunken). The border is declared TRANSPARENT and
|
|
56
|
+
only takes a colour on hover — declaring it here rather than adding one on
|
|
57
|
+
:hover is what keeps the row from shifting 1px when the pointer arrives. */
|
|
58
|
+
.drawer-brand-logo {
|
|
59
|
+
@apply inline-flex items-center justify-center shrink-0;
|
|
60
|
+
padding: 6px;
|
|
61
|
+
border-radius: var(--og-radius-control);
|
|
62
|
+
background: rgb(var(--og-surface-sunken));
|
|
63
|
+
border: 1px solid transparent;
|
|
64
|
+
transition:
|
|
65
|
+
padding 220ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
66
|
+
border-color 150ms ease;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* The whole brand row is one link, so the tile lights up for a hover anywhere
|
|
70
|
+
on it — the reference's `.sb-logo:hover` sits on a non-clickable span, where
|
|
71
|
+
pointer-over-the-mark is the only hover there is. */
|
|
72
|
+
.drawer-brand:hover .drawer-brand-logo {
|
|
73
|
+
border-color: rgb(var(--og-border));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.drawer-brand.is-collapsed .drawer-brand-logo {
|
|
77
|
+
padding: 4px;
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
.drawer-brand-mark {
|
|
55
|
-
@apply h-[
|
|
81
|
+
@apply h-[21px] w-auto shrink-0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.drawer-brand.is-collapsed .drawer-brand-mark {
|
|
85
|
+
@apply h-[19px];
|
|
56
86
|
}
|
|
57
87
|
|
|
58
88
|
/* Cột chữ: title + subtitle xếp dọc. min-w-0 để truncate hoạt động khi
|
|
@@ -63,21 +93,26 @@
|
|
|
63
93
|
|
|
64
94
|
/* Wordmark: dùng CHỮ THẬT chứ không phải ảnh text_logo.png — ảnh đó là
|
|
65
95
|
navy đặc nên chìm hẳn trên theme dark; chữ ăn --og-text nên đúng cả 6. */
|
|
96
|
+
/* Reference: --og-fs-lg / weight 500 / -0.015em / line-height 1 (measured 16/09).
|
|
97
|
+
It was 600 at -0.01em, which read a notch heavier and wider than the design. */
|
|
66
98
|
.drawer-brand-text {
|
|
67
|
-
@apply truncate text-[
|
|
99
|
+
@apply truncate text-[length:var(--og-fs-lg)] font-medium text-text-primary select-none;
|
|
68
100
|
font-family: var(--og-font-display);
|
|
69
|
-
letter-spacing: -0.
|
|
101
|
+
letter-spacing: -0.015em;
|
|
102
|
+
line-height: 1;
|
|
70
103
|
}
|
|
71
104
|
|
|
72
|
-
/* Subtitle:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Intelligence Platform"
|
|
76
|
-
|
|
77
|
-
|
|
105
|
+
/* Subtitle: 9px / weight 400 / +0.02em / line-height 1.2, --og-text-muted
|
|
106
|
+
(reference, measured 16/09).
|
|
107
|
+
MỘT DÒNG truncate chứ không còn line-clamp-2: ở 11px thì "Binary Exploit
|
|
108
|
+
Intelligence Platform" không lọt cột chữ ~188px nên phải cho xuống hai dòng;
|
|
109
|
+
ở 9px nó lọt (~157px), và một dòng mới đúng bản gốc. Đổi cỡ mà quên đổi chỗ
|
|
110
|
+
này thì subtitle lại tự ngắt dòng. */
|
|
78
111
|
.drawer-brand-subtext {
|
|
79
|
-
@apply
|
|
80
|
-
|
|
112
|
+
@apply truncate text-[9px] font-normal select-none mt-[2px];
|
|
113
|
+
color: rgb(var(--og-text-muted));
|
|
114
|
+
letter-spacing: 0.02em;
|
|
115
|
+
line-height: 1.2;
|
|
81
116
|
}
|
|
82
117
|
|
|
83
118
|
.drawer-container {
|
|
@@ -90,14 +125,30 @@
|
|
|
90
125
|
.toggle-btn {
|
|
91
126
|
@apply absolute top-1/2 -translate-y-1/2 z-50 flex items-center justify-center w-6 h-6 rounded-full bg-bg-primary border border-border-default cursor-pointer shadow-sm;
|
|
92
127
|
right: -12px; /* half of button width to straddle the border */
|
|
93
|
-
transition:
|
|
128
|
+
transition: background-color 200ms ease, box-shadow 200ms ease;
|
|
129
|
+
|
|
130
|
+
/* An invisible 40px hit area over the 24px circle. The visible button is a
|
|
131
|
+
circle whose CORNERS are dead, and half of it hangs over the content pane,
|
|
132
|
+
so the reliable target was ~20px across — small enough that an off-centre
|
|
133
|
+
press missed (anh Tú: "đôi lúc không hoạt động"). The look does not
|
|
134
|
+
change; only what the pointer can hit. */
|
|
135
|
+
&::before {
|
|
136
|
+
content: '';
|
|
137
|
+
position: absolute;
|
|
138
|
+
inset: -8px;
|
|
139
|
+
border-radius: 9999px;
|
|
140
|
+
}
|
|
94
141
|
|
|
95
142
|
&:hover {
|
|
96
143
|
@apply bg-bg-hover shadow-md;
|
|
97
144
|
}
|
|
98
145
|
|
|
146
|
+
/* NO scale on :active. Shrinking the button under a pressed pointer moves its
|
|
147
|
+
edge inward mid-press; a press that started near the rim then released
|
|
148
|
+
outside the element, and the browser fires no `click` at all for a
|
|
149
|
+
mousedown/mouseup pair on different targets. Press feedback is the fill. */
|
|
99
150
|
&:active {
|
|
100
|
-
|
|
151
|
+
@apply bg-bg-hover;
|
|
101
152
|
}
|
|
102
153
|
|
|
103
154
|
&:focus-visible {
|
|
@@ -203,18 +254,64 @@
|
|
|
203
254
|
@apply outline-none;
|
|
204
255
|
}
|
|
205
256
|
|
|
257
|
+
/* 9px icon-to-label, per the reference's nav row (measured 16/09). It was
|
|
258
|
+
`gap-3` = 12px. */
|
|
206
259
|
.item-content {
|
|
207
|
-
@apply flex items-center gap-
|
|
260
|
+
@apply flex items-center gap-[9px] w-full overflow-hidden;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* 18px, in an 18px box — the reference's glyph is 12.5px inside an 18px span.
|
|
264
|
+
At 20px the icon alone made the row 32px tall, so the `min-height: 30px` on
|
|
265
|
+
.drawer-item was never the height that actually shipped. The gap lives on
|
|
266
|
+
.item-content; do NOT add a margin here as well or the two compound. */
|
|
267
|
+
/* Collapsed the label is gone and the icon is the only thing in the row, so it
|
|
268
|
+
carries the row on its own and goes back up to 18px (anh Tú 16/09). Expanded
|
|
269
|
+
it stays at the reference's 13px mark. */
|
|
270
|
+
.drawer-item.is-collapsed .item-icon {
|
|
271
|
+
width: 18px;
|
|
272
|
+
height: 18px;
|
|
273
|
+
margin-right: 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* A solid mark: lucide draws outlines, and filling the shape turns the diamond
|
|
277
|
+
into the reference's filled lozenge.
|
|
278
|
+
The descendant selector is REQUIRED: lucide renders an <svg> INSIDE the
|
|
279
|
+
element carrying `.item-icon`, and it is that inner svg that holds
|
|
280
|
+
`fill="none"` — styling only the outer one left the mark hollow (MEASURED
|
|
281
|
+
16/09). Paths are named too, for the nesting lucide may skip. */
|
|
282
|
+
.item-icon--solid {
|
|
283
|
+
fill: currentColor;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* ::ng-deep is REQUIRED for the descendants, not a shortcut: lucide builds its
|
|
287
|
+
<svg> (and the `fill="none"` on it) at runtime with Renderer2, so those nodes
|
|
288
|
+
never carry this component's [_ngcontent] attribute and a scoped selector
|
|
289
|
+
matches nothing. The host element above is in the template and does get it —
|
|
290
|
+
which is why the outer fill applied while the mark stayed hollow. */
|
|
291
|
+
:host ::ng-deep .item-icon--solid svg,
|
|
292
|
+
:host ::ng-deep .item-icon--solid path,
|
|
293
|
+
:host ::ng-deep .item-icon--solid polygon {
|
|
294
|
+
fill: currentColor;
|
|
208
295
|
}
|
|
209
296
|
|
|
210
297
|
.item-icon {
|
|
211
298
|
@apply flex-shrink-0;
|
|
212
299
|
color: inherit;
|
|
300
|
+
/* The reference's mark is a 12.5px glyph sitting in an 18px slot, so the
|
|
301
|
+
ICON is 13px and the 5px below restores the slot: 13 + 5 + the container's
|
|
302
|
+
9px gap = 27px from the icon's left edge to the label, exactly as in the
|
|
303
|
+
design. Sizing the icon itself 18px drew it half again too large. */
|
|
304
|
+
width: 13px;
|
|
305
|
+
height: 13px;
|
|
306
|
+
margin-right: 5px;
|
|
213
307
|
}
|
|
214
308
|
|
|
215
309
|
.item-label {
|
|
216
|
-
/* Size comes from .drawer-item (12.5px, 0.7.107) — no second scale here.
|
|
217
|
-
|
|
310
|
+
/* Size comes from .drawer-item (12.5px, 0.7.107) — no second scale here.
|
|
311
|
+
Leading is 18px, not `leading-5`'s 20px: the label is the tallest thing in
|
|
312
|
+
the row, so 20 + 6 + 6 padding made every row 32px and the declared
|
|
313
|
+
`min-height: 30px` never applied. The reference's row is 30. */
|
|
314
|
+
@apply flex-1 whitespace-nowrap overflow-hidden text-ellipsis leading-[18px] text-left;
|
|
218
315
|
font-size: inherit;
|
|
219
316
|
}
|
|
220
317
|
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
<button
|
|
4
4
|
type="button"
|
|
5
5
|
[attr.aria-pressed]="isActive(option)"
|
|
6
|
-
class="flex items-center
|
|
6
|
+
class="fx-pill flex items-center rounded-full border cursor-pointer transition-colors"
|
|
7
7
|
[ngClass]="pillClass(option)"
|
|
8
8
|
(click)="valueChange.emit(option.value)"
|
|
9
9
|
>
|
|
10
|
+
@if (option.icon) {
|
|
11
|
+
<lucide-icon [img]="option.icon" [size]="14" class="flex shrink-0" />
|
|
12
|
+
}
|
|
10
13
|
{{ option.label }}
|
|
11
14
|
@if (option.count !== null && option.count !== undefined) {
|
|
12
15
|
<span [ngClass]="countClass(option)" style="font-family: var(--og-font-figure)">{{ option.count }}</span>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@reference '../../../styles/tailwind.css';
|
|
2
|
+
|
|
3
|
+
/* Pill metrics to the reference (measured in the browser 16/09): 29px tall,
|
|
4
|
+
6px/12px padding, --og-fs-xs at weight 600, 7px between label and count.
|
|
5
|
+
It was 26px at 4px/11px, 12px/700 and a 4px gap — a step smaller, a step
|
|
6
|
+
heavier and a step tighter than the design at every point. */
|
|
7
|
+
.fx-pill {
|
|
8
|
+
padding: 6px 12px;
|
|
9
|
+
gap: 7px;
|
|
10
|
+
font-size: var(--og-fs-xs);
|
|
11
|
+
font-weight: 600;
|
|
12
|
+
/* 15px, not a ratio: 6+6 padding + 2 border + 15 = the reference's 29px.
|
|
13
|
+
`1.4` on 11.5px renders 16.1 and the pill came out 30. */
|
|
14
|
+
line-height: 15px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Hover lift, matching the reference's `.bi-filter-pill:hover` — the same
|
|
18
|
+
0 2px 7px rgba(0,0,0,.12) every other button in that design gets
|
|
19
|
+
(`.og-button:hover`, which `.btn-alternative` already carries here).
|
|
20
|
+
Measured 16/09; the pill had no hover affordance at all before. */
|
|
21
|
+
.fx-pill:hover {
|
|
22
|
+
box-shadow: 0 2px 7px rgb(0 0 0 / 0.12);
|
|
23
|
+
}
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
<!-- flex column filling the host: cards in one grid row share a height and a
|
|
2
2
|
projected footer can sit at the bottom with `mt-auto` (design 01). -->
|
|
3
|
-
|
|
3
|
+
<!-- The card itself carries NO padding: the reference pads the header and the
|
|
4
|
+
body separately (10px 12px 4px / 10px 12px, measured 16/09), which is what
|
|
5
|
+
lets the header rule sit flush and the body start tight under it. -->
|
|
6
|
+
<div class="card-common fx-section-card bg-surface flex flex-col flex-1 min-h-0">
|
|
4
7
|
@if (titleStyle() === 'eyebrow') {
|
|
5
|
-
<div class="flex items-baseline justify-between gap-normal
|
|
8
|
+
<div class="fx-section-card__head flex items-baseline justify-between gap-normal">
|
|
6
9
|
<span class="txt-eyebrow">{{ title() }}</span>
|
|
7
10
|
@if (aside()) {
|
|
8
|
-
<span class="
|
|
11
|
+
<span class="fx-section-card__aside text-right">{{ aside() }}</span>
|
|
9
12
|
}
|
|
10
13
|
</div>
|
|
11
14
|
} @else {
|
|
12
|
-
<div class="flex items-center gap-normal
|
|
15
|
+
<div class="fx-section-card__head flex items-center gap-normal">
|
|
13
16
|
@if (icon) {
|
|
14
17
|
<fx-ui-hero-icon [ngClass]="iconClass" [icon]="icon" [size]="18"></fx-ui-hero-icon>
|
|
15
18
|
}
|
|
16
19
|
<span class="txt-heading-06">{{ title() }}</span>
|
|
17
20
|
</div>
|
|
18
21
|
}
|
|
19
|
-
<
|
|
22
|
+
<div class="fx-section-card__body flex flex-col flex-1 min-h-0">
|
|
23
|
+
<ng-content></ng-content>
|
|
24
|
+
</div>
|
|
20
25
|
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@reference '../../../styles/tailwind.css';
|
|
2
|
+
|
|
3
|
+
/* Card padding, to the reference (measured in the browser 16/09).
|
|
4
|
+
`card-common` pads 20px/16px uniformly, which the design never does: there
|
|
5
|
+
the card has none, the header carries 10px 12px 4px and the body 10px 12px.
|
|
6
|
+
The uniform 16/20 is what made every panel on the page read roomier than the
|
|
7
|
+
design and pushed each section's first row 6px further from its title. */
|
|
8
|
+
.fx-section-card {
|
|
9
|
+
padding: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.fx-section-card__head {
|
|
13
|
+
padding: 10px 12px 4px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.fx-section-card__body {
|
|
17
|
+
padding: 10px 12px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Section caption ("ranked by risk · across all projects"): 11.5px/400 in
|
|
21
|
+
--og-text-faint. `txt-utility-description` is 12px/300 in --og-text-secondary
|
|
22
|
+
— a size too large, a weight too light and a shade too dark. */
|
|
23
|
+
.fx-section-card__aside {
|
|
24
|
+
font-size: var(--og-fs-xs);
|
|
25
|
+
font-weight: 400;
|
|
26
|
+
line-height: 1.4;
|
|
27
|
+
color: rgb(var(--og-text-faint));
|
|
28
|
+
}
|
|
@@ -16,3 +16,51 @@
|
|
|
16
16
|
grid-template-columns: minmax(0, 1fr) !important;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
/* The ruled KPI strip, to the reference (`.og-statgrid--ruled`, measured in the
|
|
21
|
+
browser 16/09): one bordered, rounded, softly-shadowed panel with NO gap,
|
|
22
|
+
its cells padded 10px 12px and separated by a hairline on every cell but the
|
|
23
|
+
last. Same treatment the Exploit Intelligence page already hand-rolled. */
|
|
24
|
+
.fx-stat-grid--ruled {
|
|
25
|
+
gap: 0;
|
|
26
|
+
border: 1px solid rgb(var(--og-border));
|
|
27
|
+
border-radius: var(--og-radius-control);
|
|
28
|
+
background: rgb(var(--og-surface));
|
|
29
|
+
box-shadow:
|
|
30
|
+
0 1px 2px rgb(0 0 0 / 0.04),
|
|
31
|
+
0 3px 8px rgb(0 0 0 / 0.05);
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.fx-stat-grid--ruled .fx-stat-cell {
|
|
36
|
+
padding: 10px 12px;
|
|
37
|
+
border-right: 1px solid rgb(var(--og-border));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.fx-stat-grid--ruled .fx-stat-cell--last {
|
|
41
|
+
border-right: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Cell type: 10.5px faint label, a 32px mono figure 6px below it, an 11.5px
|
|
45
|
+
muted sub 4px below that. */
|
|
46
|
+
.fx-stat-cell__label {
|
|
47
|
+
font-size: var(--og-fs-2xs);
|
|
48
|
+
font-weight: 400;
|
|
49
|
+
color: rgb(var(--og-text-faint));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.fx-stat-cell__figure {
|
|
53
|
+
font-family: var(--og-font-figure);
|
|
54
|
+
font-size: 32px;
|
|
55
|
+
font-weight: 400;
|
|
56
|
+
line-height: 1.1;
|
|
57
|
+
color: rgb(var(--og-text));
|
|
58
|
+
margin-top: 6px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.fx-stat-cell__sub {
|
|
62
|
+
font-size: var(--og-fs-xs);
|
|
63
|
+
font-weight: 400;
|
|
64
|
+
color: rgb(var(--og-text-muted));
|
|
65
|
+
margin-top: 4px;
|
|
66
|
+
}
|