fix(icons): standardize dropdown/select/pagination/expander chevrons

app-select (the shared select used across every admin form) rendered
the browser's native dropdown indicator - inconsistent across
Chrome/Firefox/Safari and outside the icon system entirely. Hid it
(appearance: none) and added a consistent chevronDown via app-icon.

app-pagination used literal HTML entities (« / ») for
prev/next instead of icons. Replaced with chevronLeft/chevronRight.

Every native <details>/<summary> expander (7 call sites across admin
product form, page editor, and the builder's widget advanced-settings
panel) relied on the browser's default disclosure triangle, which
again varies per browser and shares no visual relationship with the
rest of the icon system. Added one global CSS rule (details > summary)
that hides the native marker and draws the same Lucide chevron path
used everywhere else, animated on open/close - covers all 7 without
touching each template.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-20 02:59:22 +04:00
parent e1112bbd50
commit f563d47e8a
6 changed files with 82 additions and 20 deletions

View File

@@ -6,7 +6,7 @@
[attr.aria-label]="previousLabel()"
(click)="goTo(currentPage() - 1)"
>
&laquo;
<app-icon name="chevronLeft" [size]="16" />
</app-button>
@for (page of pages(); track $index) {
@@ -32,6 +32,6 @@
[attr.aria-label]="nextLabel()"
(click)="goTo(currentPage() + 1)"
>
&raquo;
<app-icon name="chevronRight" [size]="16" />
</app-button>
</nav>

View File

@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
import { ButtonComponent } from '../button/button.component';
import { IconComponent } from '../icon/icon.component';
const SIBLING_COUNT = 1;
const ELLIPSIS = '…' as const;
@@ -7,7 +8,7 @@ const ELLIPSIS = '…' as const;
@Component({
selector: 'app-pagination',
standalone: true,
imports: [ButtonComponent],
imports: [ButtonComponent, IconComponent],
templateUrl: './pagination.component.html',
styleUrl: './pagination.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -1,20 +1,23 @@
<select
class="app-select"
[class.app-select--sm]="size() === 'sm'"
[class.app-select--md]="size() === 'md'"
[class.app-select--lg]="size() === 'lg'"
[disabled]="isDisabled || disabled()"
[attr.id]="formField?.fieldId ?? null"
[attr.aria-label]="ariaLabel()"
[attr.aria-describedby]="formField?.describedBy() ?? null"
[value]="value"
(change)="handleChange($event)"
(blur)="handleBlur()"
>
@for (option of options(); track option.value) {
<option [value]="option.value" [title]="option.description ?? ''">{{ option.label }}</option>
}
</select>
<div class="app-select-wrap">
<select
class="app-select"
[class.app-select--sm]="size() === 'sm'"
[class.app-select--md]="size() === 'md'"
[class.app-select--lg]="size() === 'lg'"
[disabled]="isDisabled || disabled()"
[attr.id]="formField?.fieldId ?? null"
[attr.aria-label]="ariaLabel()"
[attr.aria-describedby]="formField?.describedBy() ?? null"
[value]="value"
(change)="handleChange($event)"
(blur)="handleBlur()"
>
@for (option of options(); track option.value) {
<option [value]="option.value" [title]="option.description ?? ''">{{ option.label }}</option>
}
</select>
<app-icon name="chevronDown" [size]="16" class="app-select-arrow" />
</div>
@if (activeDescription(); as description) {
<small class="app-select-description">{{ description }}</small>
}

View File

@@ -7,8 +7,24 @@
width: 100%;
}
.app-select-wrap {
position: relative;
width: 100%;
}
.app-select-arrow {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary, #6b7280);
pointer-events: none;
}
.app-select {
width: 100%;
appearance: none;
padding-right: 36px;
border: 1px solid var(--border-color, #e4e4e7);
border-radius: var(--radius-md, 6px);
background: var(--bg-primary, #fff);

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, forwardRef, inject, input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FormFieldContext } from '../form-field/form-field.component';
import { IconComponent } from '../icon/icon.component';
export type SelectSize = 'sm' | 'md' | 'lg';
@@ -13,6 +14,7 @@ export interface SelectOption {
@Component({
selector: 'app-select',
standalone: true,
imports: [IconComponent],
templateUrl: './select.component.html',
styleUrl: './select.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -149,6 +149,46 @@ a, button, input, textarea, select {
animation: app-icon-spin 1s linear infinite;
}
/* Consistent, animated expander chevron for every native <details>/<summary>
disclosure in the app, replacing the browser's default triangle marker
(which renders differently per browser). Matches the Lucide ChevronDown
path used everywhere else via app-icon, so expanders use the same icon
family as the rest of the UI without touching every call site. */
details > summary {
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
&::-webkit-details-marker {
display: none;
}
&::after {
content: '';
flex-shrink: 0;
width: 16px;
height: 16px;
background-color: currentColor;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
transition: transform 0.2s ease;
}
}
details[open] > summary::after {
transform: rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
details > summary::after {
transition: none;
}
}
/* Respect OS-level reduced-motion preference globally: neutralizes hover
transforms, card-entrance/skeleton-shimmer animations, and smooth-scroll
everywhere in one place rather than requiring every component to opt in