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:
@@ -6,7 +6,7 @@
|
|||||||
[attr.aria-label]="previousLabel()"
|
[attr.aria-label]="previousLabel()"
|
||||||
(click)="goTo(currentPage() - 1)"
|
(click)="goTo(currentPage() - 1)"
|
||||||
>
|
>
|
||||||
«
|
<app-icon name="chevronLeft" [size]="16" />
|
||||||
</app-button>
|
</app-button>
|
||||||
|
|
||||||
@for (page of pages(); track $index) {
|
@for (page of pages(); track $index) {
|
||||||
@@ -32,6 +32,6 @@
|
|||||||
[attr.aria-label]="nextLabel()"
|
[attr.aria-label]="nextLabel()"
|
||||||
(click)="goTo(currentPage() + 1)"
|
(click)="goTo(currentPage() + 1)"
|
||||||
>
|
>
|
||||||
»
|
<app-icon name="chevronRight" [size]="16" />
|
||||||
</app-button>
|
</app-button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
||||||
import { ButtonComponent } from '../button/button.component';
|
import { ButtonComponent } from '../button/button.component';
|
||||||
|
import { IconComponent } from '../icon/icon.component';
|
||||||
|
|
||||||
const SIBLING_COUNT = 1;
|
const SIBLING_COUNT = 1;
|
||||||
const ELLIPSIS = '…' as const;
|
const ELLIPSIS = '…' as const;
|
||||||
@@ -7,7 +8,7 @@ const ELLIPSIS = '…' as const;
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-pagination',
|
selector: 'app-pagination',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ButtonComponent],
|
imports: [ButtonComponent, IconComponent],
|
||||||
templateUrl: './pagination.component.html',
|
templateUrl: './pagination.component.html',
|
||||||
styleUrl: './pagination.component.scss',
|
styleUrl: './pagination.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<div class="app-select-wrap">
|
||||||
<select
|
<select
|
||||||
class="app-select"
|
class="app-select"
|
||||||
[class.app-select--sm]="size() === 'sm'"
|
[class.app-select--sm]="size() === 'sm'"
|
||||||
@@ -15,6 +16,8 @@
|
|||||||
<option [value]="option.value" [title]="option.description ?? ''">{{ option.label }}</option>
|
<option [value]="option.value" [title]="option.description ?? ''">{{ option.label }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
|
<app-icon name="chevronDown" [size]="16" class="app-select-arrow" />
|
||||||
|
</div>
|
||||||
@if (activeDescription(); as description) {
|
@if (activeDescription(); as description) {
|
||||||
<small class="app-select-description">{{ description }}</small>
|
<small class="app-select-description">{{ description }}</small>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,24 @@
|
|||||||
width: 100%;
|
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 {
|
.app-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
appearance: none;
|
||||||
|
padding-right: 36px;
|
||||||
border: 1px solid var(--border-color, #e4e4e7);
|
border: 1px solid var(--border-color, #e4e4e7);
|
||||||
border-radius: var(--radius-md, 6px);
|
border-radius: var(--radius-md, 6px);
|
||||||
background: var(--bg-primary, #fff);
|
background: var(--bg-primary, #fff);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, forwardRef, inject, input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, forwardRef, inject, input } from '@angular/core';
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
import { FormFieldContext } from '../form-field/form-field.component';
|
import { FormFieldContext } from '../form-field/form-field.component';
|
||||||
|
import { IconComponent } from '../icon/icon.component';
|
||||||
|
|
||||||
export type SelectSize = 'sm' | 'md' | 'lg';
|
export type SelectSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ export interface SelectOption {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-select',
|
selector: 'app-select',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
imports: [IconComponent],
|
||||||
templateUrl: './select.component.html',
|
templateUrl: './select.component.html',
|
||||||
styleUrl: './select.component.scss',
|
styleUrl: './select.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|||||||
@@ -149,6 +149,46 @@ a, button, input, textarea, select {
|
|||||||
animation: app-icon-spin 1s linear infinite;
|
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
|
/* Respect OS-level reduced-motion preference globally: neutralizes hover
|
||||||
transforms, card-entrance/skeleton-shimmer animations, and smooth-scroll
|
transforms, card-entrance/skeleton-shimmer animations, and smooth-scroll
|
||||||
everywhere in one place rather than requiring every component to opt in
|
everywhere in one place rather than requiring every component to opt in
|
||||||
|
|||||||
Reference in New Issue
Block a user