Skip to content

Commit f6b790f

Browse files
authored
Improve the UI/UX of the "icons" page (#660)
* Improve the UI/UX of the "icons" page * Update icons.scss * Improve layout * Improve the style of the "icons" page * Improve the initial render perf by lazy loading the icons * Add "Copied" message when clicking a icon variant * Update lock file * Use "hoisted" installs, because the default has been changed in bun 1.3
1 parent b4fca5d commit f6b790f

File tree

10 files changed

+328
-113
lines changed

10 files changed

+328
-113
lines changed

.github/workflows/install-and-deploy.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
${{ runner.os }}-bun-
6868
6969
- name: Install dependencies
70-
run: bun install
70+
run: bun i --linker hoisted
7171
working-directory: ${{ inputs.package }}
7272

7373
# Validate the common package, only for Mercury.
@@ -86,3 +86,4 @@ jobs:
8686
env:
8787
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
8888
working-directory: ${{ inputs.package }}
89+

bun.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
},
3737
"packages/mercury": {
3838
"name": "@genexus/mercury",
39-
"version": "0.31.1",
39+
"version": "0.33.0",
4040
"bin": {
4141
"mercury": "./dist/cli/mercury.js",
4242
},
4343
"devDependencies": {
44-
"@eslint/js": "*",
44+
"@eslint/js": "~9.35.0",
4545
"@genexus/chameleon-controls-library": "6.22.1",
4646
"@genexus/svg-sass-generator": "1.1.24",
4747
"@jackolope/ts-lit-plugin": "^3.1.4",

packages/mercury/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
"@genexus/chameleon-controls-library": ">= 6.22.1"
8282
},
8383
"devDependencies": {
84-
"@eslint/js": "*",
84+
"@eslint/js": "~9.35.0",
8585
"@genexus/chameleon-controls-library": "6.22.1",
8686
"@genexus/svg-sass-generator": "1.1.24",
8787
"@jackolope/ts-lit-plugin": "^3.1.4",

packages/showcase/src/app/app.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
[bundles]="[
33
'components/button',
44
'components/checkbox',
5-
'components/chat',
65
'components/code',
6+
'components/dialog',
77
'components/edit',
88
'components/icon',
99
'components/navigation-list',
@@ -40,7 +40,7 @@
4040
<ch-edit
4141
[accessibleName]="isTokensPage ? 'Token' : 'Icon'"
4242
class="input app-header__filters"
43-
[debounce]="300"
43+
[debounce]="100"
4444
[placeholder]="isTokensPage ? 'Search token...' : 'Search icon...'"
4545
[type]="'search'"
4646
(input)="handleFilterChange($event)"

packages/showcase/src/app/core-concepts/icons/icons.component.html

Lines changed: 119 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,6 @@
22
<h1 class="heading-1">Icons</h1>
33
</header>
44

5-
@if (getIconPathCodeBlock() !== undefined) {
6-
<aside class="icons-code-block-selected">
7-
<ch-code
8-
class="code get-icon-path-code-block"
9-
language="ts"
10-
[value]="getIconPathCodeBlock()"
11-
></ch-code>
12-
</aside>
13-
}
14-
155
@for (
166
category of filteredAssets();
177
track category.categoryName;
@@ -32,68 +22,133 @@ <h2 [id]="categoryName" class="heading-2">
3222
{{ categoryExplanation()[categoryName] }}
3323
</p>
3424

35-
@for (
36-
iconWithColorType of category.icons;
37-
track iconWithColorType.iconName;
38-
let iconIndex = $index
39-
) {
40-
@let iconName = iconWithColorType.iconName;
41-
@let iconTitlePrefix = categoryTitlePrefix + (iconIndex + 1) + ".";
25+
<ol class="section-icons__table">
26+
@for (
27+
iconWithColorType of category.icons;
28+
track iconWithColorType.iconName;
29+
let iconIndex = $index
30+
) {
31+
@let iconName = iconWithColorType.iconName;
32+
@let colorType = iconWithColorType.colorType.colorType;
33+
@let states = iconWithColorType.colorType.states;
34+
@let monochromeIcon = colorType?.[0]?.states?.[0]?.img;
35+
@let iconPath = monochromeIcon ?? states![0].img;
36+
@let categoryAndIconName = `${categoryName}/${iconName}`;
37+
@let ariaSelected =
38+
selectedIconName() === categoryAndIconName ? "true" : null;
39+
@let dialogIsOpened = open() === "true" && ariaSelected === "true";
40+
41+
@if (dialogIsOpened && open() === "true") {
42+
<ch-dialog
43+
class="dialog"
44+
[style.--ch-dialog-max-block-size]="'min(800px, calc(100dvh - 32px))'"
45+
[caption]="categoryName + ' / ' + iconName"
46+
[show]="open() === 'true'"
47+
show-header
48+
(dialogClosed)="closeDialog()"
49+
>
50+
<div class="section-icons__dialog" (click)="onStateClick($event)">
51+
<div class="section-icons__dialog-states">
52+
@if (colorType) {
53+
@for (
54+
colorTypeWithStates of colorType;
55+
track $index;
56+
let typeIndex = $index
57+
) {
58+
@let type = colorTypeWithStates.type;
59+
@let buttonValue = categoryName + "/" + iconName + "/" + type;
60+
61+
<button
62+
[attr.aria-labelledby]="type"
63+
[class.section-icons__group]="true"
64+
[class.section-icons__group--selected]="
65+
selectedStates() === buttonValue
66+
"
67+
value="{{ buttonValue }}"
68+
>
69+
<h3 [id]="type" class="subtitle-semi-bold-m">
70+
{{ type }}
71+
</h3>
72+
73+
<div class="states">
74+
@for (
75+
onlyStates of colorTypeWithStates.states;
76+
track $index
77+
) {
78+
<span
79+
class="state subtitle-semi-bold-xs"
80+
[style.--icon-path]="onlyStates.img"
81+
>{{ onlyStates.state }}</span
82+
>
83+
}
84+
</div>
85+
</button>
86+
}
87+
} @else {
88+
@let buttonValue = categoryName + "/" + iconName;
4289

43-
<section
44-
[attr.aria-labelledby]="iconName"
45-
[class.section-icons__icon-name]="true"
46-
>
47-
<h3 [id]="iconName" class="heading-4">
48-
<a routerLink="." [fragment]="categoryName + '-' + iconName">
49-
{{ iconTitlePrefix + " " + iconName }}
50-
</a>
51-
</h3>
90+
<button
91+
[attr.aria-labelledby]="iconName"
92+
[class.section-icons__group]="true"
93+
[class.section-icons__group--selected]="
94+
selectedStates() === buttonValue
95+
"
96+
value="{{ buttonValue }}"
97+
>
98+
<h3 class="subtitle-semi-bold-m">Default</h3>
5299

53-
@if (iconWithColorType.colorType.colorType) {
54-
@for (
55-
colorTypeWithStates of iconWithColorType.colorType.colorType;
56-
track $index;
57-
let typeIndex = $index
58-
) {
59-
@let type = colorTypeWithStates.type;
60-
@let buttonValue = categoryName + "/" + iconName + "/" + type;
100+
<div class="states">
101+
@for (
102+
onlyStates of iconWithColorType.colorType.states;
103+
track $index
104+
) {
105+
<span
106+
class="state subtitle-semi-bold-xs"
107+
[style.--icon-path]="onlyStates.img"
108+
>{{ onlyStates.state }}</span
109+
>
110+
}
111+
</div>
112+
</button>
113+
}
114+
</div>
61115

62-
<button
63-
[attr.aria-labelledby]="type"
64-
[class.section-icons__group]="true"
65-
[class.section-icons__group--selected]="selected() === buttonValue"
66-
value="{{ buttonValue }}"
67-
>
68-
<h4 [id]="type" class="heading-5">
69-
{{ type }}
70-
</h4>
116+
<div class="section-icons__dialog-code-container">
117+
@if (getIconPathCodeBlock() !== undefined) {
118+
<span
119+
aria-hidden="true"
120+
#copied
121+
class="section-icons__dialog-code-copied elevation-1"
122+
>Copied!</span
123+
>
124+
}
71125

72-
@for (onlyStates of colorTypeWithStates.states; track $index) {
73-
<span class="state" [style.--icon-path]="onlyStates.img">{{
74-
onlyStates.state
75-
}}</span>
76-
}
77-
</button>
78-
}
79-
} @else {
80-
@let buttonValue = categoryName + "/" + iconName;
126+
<ch-code
127+
class="code elevation-2"
128+
language="ts"
129+
[value]="getIconPathCodeBlock() ?? '<-- Select a variant'"
130+
></ch-code>
131+
</div>
132+
</div>
133+
</ch-dialog>
134+
}
81135

136+
<li class="section-icons__list-item">
82137
<button
83-
[attr.aria-labelledby]="iconName"
84-
[class.section-icons__group]="true"
85-
[class.section-icons__group--selected]="selected() === buttonValue"
86-
value="{{ buttonValue }}"
138+
attr.aria-selected="{{ ariaSelected }}"
139+
[class.section-icons__icon-name]="true"
140+
[class.section-icons__icon-name-monochrome]="!!monochromeIcon"
141+
[class.section-icons__icon-name-multicolor]="!monochromeIcon"
142+
[style.--icon-path]="iconPath"
143+
value="{{ categoryAndIconName }}"
87144
>
88-
@for (onlyStates of iconWithColorType.colorType.states; track $index) {
89-
<span class="state" [style.--icon-path]="onlyStates.img">{{
90-
onlyStates.state
91-
}}</span>
92-
}
145+
<p class="subtitle-semi-bold-xs">
146+
{{ iconName }}
147+
</p>
93148
</button>
94-
}
95-
</section>
96-
}
149+
</li>
150+
}
151+
</ol>
97152
</section>
98153
}
99154
}

packages/showcase/src/app/core-concepts/icons/icons.component.ts

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
import { isPlatformBrowser } from "@angular/common";
1+
import { isPlatformBrowser, Location } from "@angular/common";
22
import {
33
ChangeDetectionStrategy,
44
Component,
55
computed,
66
CUSTOM_ELEMENTS_SCHEMA,
77
effect,
8+
ElementRef,
89
HostListener,
910
inject,
1011
input,
1112
model,
1213
PLATFORM_ID,
1314
signal,
15+
viewChild,
1416
ViewEncapsulation
1517
} from "@angular/core";
16-
import { Router, RouterLink, RouterModule } from "@angular/router";
18+
import { RouterLink, RouterModule } from "@angular/router";
1719
import { MERCURY_ASSETS } from "@genexus/mercury/MERCURY_ASSETS.js";
1820

1921
import { PageFiltersService } from "../../../services/page-filters.service";
@@ -46,9 +48,9 @@ export type MercuryCategory = keyof (typeof MERCURY_ASSETS)["icons"];
4648
encapsulation: ViewEncapsulation.None
4749
})
4850
export class IconsComponent {
49-
router = inject(Router);
5051
platform = inject(PLATFORM_ID);
5152
pageFilters = inject(PageFiltersService);
53+
location = inject(Location);
5254
hiddenFields = model<string>("");
5355

5456
assets = signal<
@@ -67,13 +69,16 @@ export class IconsComponent {
6769
])
6870
);
6971

70-
selected = model<string | undefined>(undefined);
72+
selectedIconName = model<string | undefined>(undefined);
73+
selectedStates = model<string | undefined>(undefined);
74+
open = model<"true" | "false">("false");
75+
7176
getIconPathCodeBlock = computed(() => {
72-
if (!this.selected()) {
77+
if (!this.selectedStates()) {
7378
return undefined;
7479
}
7580

76-
const iconMetadata = this.selected()!.split("/");
81+
const iconMetadata = this.selectedStates()!.split("/");
7782

7883
return `getIconPath({
7984
category: "${iconMetadata[0]}",
@@ -124,6 +129,8 @@ export class IconsComponent {
124129

125130
categoryExplanation = signal(mercuryCategoryExplanation);
126131

132+
protected copiedRef = viewChild<ElementRef<HTMLSpanElement>>("copied");
133+
127134
constructor() {
128135
const newAssets = [];
129136
const categories = Object.entries(MERCURY_ASSETS.icons);
@@ -199,6 +206,16 @@ export class IconsComponent {
199206
if (isPlatformBrowser(this.platform)) {
200207
if (this.getIconPathCodeBlock()) {
201208
navigator.clipboard.writeText(this.getIconPathCodeBlock()!);
209+
210+
const copiedRef = this.copiedRef()?.nativeElement;
211+
212+
// TODO: Improve the accessibility of this message
213+
if (copiedRef) {
214+
copiedRef.style.animation = "none";
215+
216+
// Wait one frame to properly restart the animation
217+
requestAnimationFrame(() => copiedRef.style.removeProperty("animation"));
218+
}
202219
}
203220
}
204221
});
@@ -208,13 +225,45 @@ export class IconsComponent {
208225
onClick(target: HTMLElement) {
209226
const buttonRef = target.closest("button");
210227

211-
if (buttonRef && buttonRef.value) {
212-
this.selected.set(buttonRef.value);
228+
if (buttonRef && buttonRef.value && buttonRef.value.split("/").length === 2) {
229+
this.selectedIconName.set(buttonRef.value);
230+
this.open.set("true");
231+
232+
const currentUrl = new URL(window.location.href);
233+
currentUrl.searchParams.set("selectedIconName", buttonRef.value);
234+
currentUrl.searchParams.set("open", "true");
213235

214-
this.router.navigate([], {
215-
queryParams: { selected: buttonRef.value },
216-
queryParamsHandling: "merge" // Conserve other query parameters
217-
});
236+
this.location.replaceState(currentUrl.pathname, currentUrl.search);
218237
}
219238
}
239+
240+
onStateClick = (event: MouseEvent) => {
241+
if (!event.target) {
242+
return;
243+
}
244+
245+
const buttonRef = (event.target as HTMLElement).closest("button");
246+
247+
if (buttonRef && buttonRef.value) {
248+
this.selectedStates.set(buttonRef.value);
249+
250+
const currentUrl = new URL(window.location.href);
251+
currentUrl.searchParams.set("selectedStates", buttonRef.value);
252+
253+
this.location.replaceState(currentUrl.pathname, currentUrl.search);
254+
}
255+
};
256+
257+
closeDialog = () => {
258+
this.open.set("false");
259+
this.selectedIconName.set(undefined);
260+
this.selectedStates.set(undefined);
261+
262+
const currentUrl = new URL(window.location.href);
263+
currentUrl.searchParams.delete("open");
264+
currentUrl.searchParams.delete("selectedIconName");
265+
currentUrl.searchParams.delete("selectedStates");
266+
267+
this.location.replaceState(currentUrl.pathname, currentUrl.search);
268+
};
220269
}

0 commit comments

Comments
 (0)