not bad
This commit is contained in:
parent
e2a60e71a3
commit
003f38ea60
36 changed files with 1543 additions and 1287 deletions
|
|
@ -6,6 +6,9 @@ import {
|
|||
signal,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
|
|
@ -19,7 +22,7 @@ import {
|
|||
[class.always-shadow]="alwaysDropShadow()"
|
||||
>
|
||||
<div class="background" [class.active]="open()"></div>
|
||||
<div class="top" (click)="open.update(v => !v)">
|
||||
<div class="top" (click)="toggleOpen($event)">
|
||||
<p>{{ resolvedSelected() ?? placeholder() }}</p>
|
||||
<img class="arrow" [class.upside-down]="open()" src="assets/arrow.svg" alt="" />
|
||||
</div>
|
||||
|
|
@ -33,7 +36,9 @@ import {
|
|||
(blur)="onRename(item, $any($event.target).value)"
|
||||
/>
|
||||
} @else {
|
||||
<p (click)="onSelectItem(item)">{{ item }}</p>
|
||||
<button class="option" type="button" (click)="onSelectItem(item)">
|
||||
{{ item }}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
<div class="add-row">
|
||||
|
|
@ -43,9 +48,20 @@ import {
|
|||
placeholder="Add a value…"
|
||||
(keydown.enter)="onAdd(addInput.value); addInput.value = ''"
|
||||
/>
|
||||
<button (click)="onAdd(addInput.value); addInput.value = ''">Add</button>
|
||||
<button
|
||||
class="add-button"
|
||||
type="button"
|
||||
(click)="onAdd(addInput.value); addInput.value = ''"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
@if (editable()) {
|
||||
<button class="pen" [class.active]="editing()" (click)="editing.update(v => !v)">
|
||||
<button
|
||||
class="pen"
|
||||
type="button"
|
||||
[class.active]="editing()"
|
||||
(click)="editing.update(v => !v)"
|
||||
>
|
||||
<img src="assets/pen.svg" alt="Edit" />
|
||||
</button>
|
||||
}
|
||||
|
|
@ -58,6 +74,7 @@ import {
|
|||
@import '../../../../library/main';
|
||||
|
||||
$inner-padding: var(--medium-padding);
|
||||
$dropdown-shadow: 0 4px 14px rgba($text-color, 0.16), $shadow-border;
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
|
|
@ -80,13 +97,22 @@ import {
|
|||
align-items: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
min-height: 46px;
|
||||
box-sizing: border-box;
|
||||
gap: var(--small-padding);
|
||||
|
||||
p {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
@include sub-title-text();
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
img.arrow {
|
||||
flex: 0 0 auto;
|
||||
@include square(16px);
|
||||
transition: transform $long-animation-time;
|
||||
|
||||
|
|
@ -98,81 +124,129 @@ import {
|
|||
|
||||
.bottom-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: absolute;
|
||||
overflow-y: hidden;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
pointer-events: all;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
padding: $inner-padding;
|
||||
padding-top: 0;
|
||||
@include inner-spacing($inner-padding);
|
||||
padding-top: var(--small-padding);
|
||||
gap: var(--small-padding);
|
||||
// Default (closed) state — also the target of the close transition.
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
transform: translateY(-100%);
|
||||
transform: translateY(-8px);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
// Clip the top edge so the panel's shadow can't bleed back up into
|
||||
// the chip area; sides + bottom get a 10px slack for the shadow.
|
||||
clip-path: inset(0 -10px -10px -10px);
|
||||
// Delay the visibility change until after the slide animation finishes
|
||||
// so the panel stays visible while it animates closed.
|
||||
transition:
|
||||
transform $long-animation-time,
|
||||
opacity $long-animation-time,
|
||||
background-color $long-animation-time,
|
||||
box-shadow $long-animation-time,
|
||||
visibility 0s $long-animation-time;
|
||||
|
||||
&.open {
|
||||
visibility: visible;
|
||||
pointer-events: all;
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
background-color: $light-color;
|
||||
box-shadow: $shadow;
|
||||
// Show shadow on left/right/bottom only; clip the top edge so the
|
||||
// shadow doesn't bleed over the seam where .bottom meets .top.
|
||||
clip-path: inset(0 -6px -6px -6px);
|
||||
box-shadow: $dropdown-shadow;
|
||||
// On open, visibility flips immediately (no delay); transform +
|
||||
// colors + shadow animate over $long-animation-time.
|
||||
transition:
|
||||
transform $long-animation-time,
|
||||
opacity $long-animation-time,
|
||||
background-color $long-animation-time,
|
||||
box-shadow $long-animation-time,
|
||||
visibility 0s 0s;
|
||||
}
|
||||
|
||||
p {
|
||||
.option {
|
||||
@include sub-title-text();
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: $mobile-width) {
|
||||
min-height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
@include sub-title-text();
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba($text-color, 0.72);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: $mobile-width) {
|
||||
min-height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-row {
|
||||
height: 32px;
|
||||
@media (max-width: $mobile-width) { height: 24px; }
|
||||
min-height: 40px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-end;
|
||||
gap: var(--small-padding);
|
||||
|
||||
input[type='text'] {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
border-bottom: solid 2px transparent;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
box-shadow: none;
|
||||
border-bottom-color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
position: static;
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&.add-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
&.pen {
|
||||
opacity: 0.25;
|
||||
|
|
@ -184,6 +258,10 @@ import {
|
|||
background: transparent;
|
||||
position: relative;
|
||||
|
||||
// Kill the global button's hover-grow underline pseudo-element
|
||||
// for the icon-only edit control.
|
||||
&:after { content: none; display: none; }
|
||||
|
||||
img {
|
||||
@include square(16px);
|
||||
}
|
||||
|
|
@ -224,16 +302,18 @@ import {
|
|||
width: 100%;
|
||||
@include card();
|
||||
z-index: 3;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
box-shadow $long-animation-time,
|
||||
height $long-animation-time,
|
||||
border-radius $long-animation-time;
|
||||
|
||||
&.active {
|
||||
box-shadow: $shadow;
|
||||
// Show shadow on top/left/right only; clip the bottom edge so the
|
||||
// shadow doesn't bleed over the seam where .top meets .bottom.
|
||||
clip-path: inset(-6px -6px 0 -6px);
|
||||
// Same shadow recipe as the panel below so the two halves read as
|
||||
// one continuous card. Clip the bottom edge so this shadow doesn't
|
||||
// bleed across the seam where .top meets .bottom.
|
||||
box-shadow: $dropdown-shadow;
|
||||
clip-path: inset(-10px -10px 0 -10px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -251,25 +331,31 @@ import {
|
|||
}
|
||||
}
|
||||
|
||||
// Hover lifts the chip — but only when the dropdown is closed. When
|
||||
// it's open the chip is already showing $dropdown-shadow and a hover
|
||||
// override would make the top heavier than the panel below.
|
||||
&:hover {
|
||||
@media (min-width: $mobile-width) {
|
||||
.background { box-shadow: $shadow; }
|
||||
.background:not(.active) { box-shadow: $shadow; }
|
||||
}
|
||||
}
|
||||
|
||||
&.shadow-border {
|
||||
.background.active {
|
||||
box-shadow: $shadow-border;
|
||||
clip-path: inset(-6px -6px 0 -6px);
|
||||
clip-path: inset(-10px -10px 0 -10px);
|
||||
}
|
||||
.bottom.open {
|
||||
box-shadow: $shadow-border;
|
||||
}
|
||||
}
|
||||
|
||||
&.shadow-border:hover {
|
||||
.background { box-shadow: $shadow-border; }
|
||||
.background:not(.active) { box-shadow: $shadow-border; }
|
||||
}
|
||||
|
||||
&.always-shadow {
|
||||
.background { box-shadow: $shadow; }
|
||||
.background:not(.active) { box-shadow: $shadow; }
|
||||
// When open, clip the bottom so the always-on shadow doesn't bleed
|
||||
// over the seam; restore full shadow when closed.
|
||||
&:has(.bottom.open) .background {
|
||||
|
|
@ -309,6 +395,7 @@ export class SelectAddComponent implements OnChanges {
|
|||
// ── Internal state ─────────────────────────────────────────────────────────
|
||||
readonly open = signal(false);
|
||||
readonly editing = signal(false);
|
||||
private readonly host = inject(ElementRef<HTMLElement>);
|
||||
|
||||
// Resolved values that merge old + new API
|
||||
protected resolvedItems(): string[] {
|
||||
|
|
@ -332,6 +419,20 @@ export class SelectAddComponent implements OnChanges {
|
|||
// Nothing to do — signals handle reactivity
|
||||
}
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
onDocumentClick(event: MouseEvent): void {
|
||||
if (!this.open()) return;
|
||||
if (!this.host.nativeElement.contains(event.target as Node)) {
|
||||
this.open.set(false);
|
||||
this.editing.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
toggleOpen(event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
this.open.update((v) => !v);
|
||||
}
|
||||
|
||||
onSelectItem(item: string): void {
|
||||
this.select.emit(item);
|
||||
// Legacy compat: also emit the index
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue