Modernise & deploy #1
3 changed files with 199 additions and 71 deletions
|
|
@ -61,7 +61,10 @@ const FIXED_L = 0.55;
|
||||||
display: block;
|
display: block;
|
||||||
padding: var(--medium-padding);
|
padding: var(--medium-padding);
|
||||||
@include card();
|
@include card();
|
||||||
box-shadow: $shadow-border;
|
border: 1px solid rgba($text-color, 0.14);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba($light-color, 0.7);
|
||||||
|
background-color: rgba($text-color, 0.025);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker {
|
.picker {
|
||||||
|
|
@ -76,35 +79,40 @@ const FIXED_L = 0.55;
|
||||||
grid-template-columns: repeat(12, 1fr);
|
grid-template-columns: repeat(12, 1fr);
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-width) {
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: var(--small-padding);
|
||||||
|
}
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
all: unset;
|
all: unset;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: $shadow-border;
|
box-shadow: 0 0 0 1px rgba($text-color, 0.18);
|
||||||
transition: transform $short-animation-time, box-shadow $long-animation-time;
|
transition: transform $short-animation-time, box-shadow $long-animation-time;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
box-shadow: $shadow;
|
box-shadow: 0 0 0 2px $light-color, 0 0 0 4px rgba($text-color, 0.5);
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
box-shadow: $shadow;
|
box-shadow: 0 0 0 2px $light-color, 0 0 0 4px rgba($text-color, 0.5);
|
||||||
transform: scale(1.15);
|
transform: scale(1.15);
|
||||||
outline: 2px solid $light-color;
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hue-slider {
|
.hue-slider {
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
input[type='range'] {
|
input[type='range'] {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 12px;
|
height: 16px;
|
||||||
border-radius: 1000px;
|
border-radius: 1000px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
|
|
@ -119,11 +127,15 @@ const FIXED_L = 0.55;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
box-shadow: 0 0 0 3px rgba($text-color, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
&::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
height: 24px;
|
height: 32px;
|
||||||
width: 24px;
|
width: 32px;
|
||||||
border-radius: 1000px;
|
border-radius: 1000px;
|
||||||
background-color: var(--thumb-color, #{$light-color});
|
background-color: var(--thumb-color, #{$light-color});
|
||||||
box-shadow: 0 0 0 2px #{$light-color}, #{$shadow};
|
box-shadow: 0 0 0 2px #{$light-color}, #{$shadow};
|
||||||
|
|
@ -135,8 +147,8 @@ const FIXED_L = 0.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-moz-range-thumb {
|
&::-moz-range-thumb {
|
||||||
height: 24px;
|
height: 32px;
|
||||||
width: 24px;
|
width: 32px;
|
||||||
border-radius: 1000px;
|
border-radius: 1000px;
|
||||||
background-color: var(--thumb-color, white);
|
background-color: var(--thumb-color, white);
|
||||||
border: 2px solid white;
|
border: 2px solid white;
|
||||||
|
|
@ -153,7 +165,7 @@ const FIXED_L = 0.55;
|
||||||
.preview {
|
.preview {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-shadow: $shadow-border;
|
box-shadow: 0 0 0 1px rgba($text-color, 0.18);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
|
|
@ -173,9 +185,8 @@ export class ColorPickerComponent {
|
||||||
return `hsl(${h}, 70%, 55%)`;
|
return `hsl(${h}, 70%, 55%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
toCss(c: HslColor): string {
|
/** Re-exported so the template can call the utility directly. */
|
||||||
return toCss(c);
|
readonly toCss = toCss;
|
||||||
}
|
|
||||||
|
|
||||||
pickHue(h: number): void {
|
pickHue(h: number): void {
|
||||||
this.colorChange.emit({ h: h / 360, s: FIXED_S, l: FIXED_L });
|
this.colorChange.emit({ h: h / 360, s: FIXED_S, l: FIXED_L });
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export interface DoubleSliderRange<T> {
|
||||||
* Two-thumb range slider — legacy "double-slider".
|
* Two-thumb range slider — legacy "double-slider".
|
||||||
* Hands an indexed range over an arbitrary values array; emits the
|
* Hands an indexed range over an arbitrary values array; emits the
|
||||||
* underlying values on each change. Labels magnetically lift as a thumb
|
* underlying values on each change. Labels magnetically lift as a thumb
|
||||||
* approaches them (rotated -45°), per the legacy.
|
* approaches them (rotated -30°), per the legacy.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'lt-double-slider',
|
selector: 'lt-double-slider',
|
||||||
|
|
@ -32,7 +32,7 @@ export interface DoubleSliderRange<T> {
|
||||||
id="ds-1"
|
id="ds-1"
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
[max]="MAX - 1"
|
[max]="maxIndex()"
|
||||||
[value]="oneValue()"
|
[value]="oneValue()"
|
||||||
(input)="oneValue.set(+$any($event.target).value)"
|
(input)="oneValue.set(+$any($event.target).value)"
|
||||||
/>
|
/>
|
||||||
|
|
@ -40,7 +40,7 @@ export interface DoubleSliderRange<T> {
|
||||||
id="ds-2"
|
id="ds-2"
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
[max]="MAX - 1"
|
[max]="maxIndex()"
|
||||||
[value]="otherValue()"
|
[value]="otherValue()"
|
||||||
(input)="otherValue.set(+$any($event.target).value)"
|
(input)="otherValue.set(+$any($event.target).value)"
|
||||||
/>
|
/>
|
||||||
|
|
@ -65,6 +65,11 @@ export interface DoubleSliderRange<T> {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: calc(#{$slider-size} / 2) auto 0 auto;
|
margin: calc(#{$slider-size} / 2) auto 0 auto;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-width) {
|
||||||
|
max-width: 90vw;
|
||||||
|
margin-top: calc(#{$slider-size} / 2);
|
||||||
|
}
|
||||||
|
|
||||||
label { display: none; }
|
label { display: none; }
|
||||||
|
|
||||||
input[type='range'] {
|
input[type='range'] {
|
||||||
|
|
@ -145,6 +150,12 @@ export interface DoubleSliderRange<T> {
|
||||||
transition: transform $long-animation-time;
|
transition: transform $long-animation-time;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-width) {
|
||||||
|
font-size: var(--small-font-size);
|
||||||
|
margin-top: $slider-size;
|
||||||
|
span { margin-top: 10px; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
@ -157,10 +168,9 @@ export class DoubleSliderComponent {
|
||||||
|
|
||||||
readonly rangeChange = output<DoubleSliderRange<unknown>>();
|
readonly rangeChange = output<DoubleSliderRange<unknown>>();
|
||||||
|
|
||||||
readonly MAX = 100;
|
|
||||||
|
|
||||||
readonly oneValue = signal(0);
|
readonly oneValue = signal(0);
|
||||||
readonly otherValue = signal(this.MAX - 1);
|
readonly otherValue = signal(0);
|
||||||
|
readonly maxIndex = computed(() => Math.max(0, this.values().length - 1));
|
||||||
|
|
||||||
private prevValuesLength = 0;
|
private prevValuesLength = 0;
|
||||||
|
|
||||||
|
|
@ -187,42 +197,44 @@ export class DoubleSliderComponent {
|
||||||
const hi = Math.max(a, b);
|
const hi = Math.max(a, b);
|
||||||
untracked(() => {
|
untracked(() => {
|
||||||
this.rangeChange.emit({
|
this.rangeChange.emit({
|
||||||
from: vs[this.indexFromValue(lo)],
|
from: vs[this.clampIndex(lo)],
|
||||||
to: vs[this.indexFromValue(hi)],
|
to: vs[this.clampIndex(hi)],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Snap the higher thumb to MAX - 1 when a new entry is appended.
|
// Snap the higher thumb to the newest value when a new entry is appended.
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const len = this.values().length;
|
const len = this.values().length;
|
||||||
untracked(() => {
|
untracked(() => {
|
||||||
|
const max = Math.max(0, len - 1);
|
||||||
if (len > this.prevValuesLength) {
|
if (len > this.prevValuesLength) {
|
||||||
const a = this.oneValue();
|
const a = this.oneValue();
|
||||||
const b = this.otherValue();
|
const b = this.otherValue();
|
||||||
if (a > b) this.oneValue.set(this.MAX - 1);
|
if (a > b) this.oneValue.set(max);
|
||||||
else this.otherValue.set(this.MAX - 1);
|
else this.otherValue.set(max);
|
||||||
|
} else {
|
||||||
|
if (this.oneValue() > max) this.oneValue.set(max);
|
||||||
|
if (this.otherValue() > max) this.otherValue.set(max);
|
||||||
}
|
}
|
||||||
this.prevValuesLength = len;
|
this.prevValuesLength = len;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private indexFromValue(value: number): number {
|
private clampIndex(value: number): number {
|
||||||
return Math.min(
|
return Math.max(0, Math.min(this.values().length - 1, Math.round(value)));
|
||||||
this.values().length - 1,
|
|
||||||
Math.floor((value / this.MAX) * this.values().length),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Magnetic label position: returns a CSS `transform` that lifts the label
|
* Magnetic label position: returns a CSS `transform` that lifts the label
|
||||||
* upward and rotates -45° as a thumb approaches.
|
* upward and rotates -30° as a thumb approaches.
|
||||||
*/
|
*/
|
||||||
getOffset(index: number): string {
|
getOffset(index: number): string {
|
||||||
const labelIndex = index / Math.max(1, this.drawnLabels().length);
|
const labelIndex = index / Math.max(1, this.drawnLabels().length - 1);
|
||||||
const a = this.oneValue() / this.MAX - 0.1;
|
const max = Math.max(1, this.maxIndex());
|
||||||
const b = this.otherValue() / this.MAX - 0.1;
|
const a = this.oneValue() / max - 0.1;
|
||||||
|
const b = this.otherValue() / max - 0.1;
|
||||||
const dist = Math.min(Math.abs(labelIndex - a), Math.abs(labelIndex - b));
|
const dist = Math.min(Math.abs(labelIndex - a), Math.abs(labelIndex - b));
|
||||||
const ACTIVE_ZONE = 0.2;
|
const ACTIVE_ZONE = 0.2;
|
||||||
const base = 'translateX(-50%) rotate(-30deg) translateY(100%)';
|
const base = 'translateX(-50%) rotate(-30deg) translateY(100%)';
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import {
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
signal,
|
signal,
|
||||||
OnChanges,
|
ElementRef,
|
||||||
SimpleChanges,
|
HostListener,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
@ -19,7 +20,15 @@ import {
|
||||||
[class.always-shadow]="alwaysDropShadow()"
|
[class.always-shadow]="alwaysDropShadow()"
|
||||||
>
|
>
|
||||||
<div class="background" [class.active]="open()"></div>
|
<div class="background" [class.active]="open()"></div>
|
||||||
<div class="top" (click)="open.update(v => !v)">
|
<div
|
||||||
|
class="top"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
[attr.aria-expanded]="open()"
|
||||||
|
(click)="toggleOpen($event)"
|
||||||
|
(keydown.enter)="toggleOpen($event)"
|
||||||
|
(keydown.space)="$event.preventDefault(); toggleOpen($event)"
|
||||||
|
>
|
||||||
<p>{{ resolvedSelected() ?? placeholder() }}</p>
|
<p>{{ resolvedSelected() ?? placeholder() }}</p>
|
||||||
<img class="arrow" [class.upside-down]="open()" src="assets/arrow.svg" alt="" />
|
<img class="arrow" [class.upside-down]="open()" src="assets/arrow.svg" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -30,10 +39,13 @@ import {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
[value]="item"
|
[value]="item"
|
||||||
|
maxlength="200"
|
||||||
(blur)="onRename(item, $any($event.target).value)"
|
(blur)="onRename(item, $any($event.target).value)"
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<p (click)="onSelectItem(item)">{{ item }}</p>
|
<button class="option" type="button" (click)="onSelectItem(item)">
|
||||||
|
{{ item }}
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<div class="add-row">
|
<div class="add-row">
|
||||||
|
|
@ -41,11 +53,23 @@ import {
|
||||||
type="text"
|
type="text"
|
||||||
#addInput
|
#addInput
|
||||||
placeholder="Add a value…"
|
placeholder="Add a value…"
|
||||||
|
maxlength="200"
|
||||||
(keydown.enter)="onAdd(addInput.value); addInput.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()) {
|
@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" />
|
<img src="assets/pen.svg" alt="Edit" />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +82,7 @@ import {
|
||||||
@import '../../../../library/main';
|
@import '../../../../library/main';
|
||||||
|
|
||||||
$inner-padding: var(--medium-padding);
|
$inner-padding: var(--medium-padding);
|
||||||
|
$dropdown-shadow: 0 4px 14px rgba($text-color, 0.16), $shadow-border;
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -80,13 +105,22 @@ import {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
min-height: 46px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
gap: var(--small-padding);
|
||||||
|
|
||||||
p {
|
p {
|
||||||
display: inline-block;
|
display: block;
|
||||||
@include sub-title-text();
|
@include sub-title-text();
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.arrow {
|
img.arrow {
|
||||||
|
flex: 0 0 auto;
|
||||||
@include square(16px);
|
@include square(16px);
|
||||||
transition: transform $long-animation-time;
|
transition: transform $long-animation-time;
|
||||||
|
|
||||||
|
|
@ -98,81 +132,129 @@ import {
|
||||||
|
|
||||||
.bottom-container {
|
.bottom-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow-y: hidden;
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
overflow: visible;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
position: absolute;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
pointer-events: all;
|
pointer-events: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||||
padding: $inner-padding;
|
padding: $inner-padding;
|
||||||
padding-top: 0;
|
padding-top: var(--small-padding);
|
||||||
@include inner-spacing($inner-padding);
|
gap: var(--small-padding);
|
||||||
// Default (closed) state — also the target of the close transition.
|
// Default (closed) state — also the target of the close transition.
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transform: translateY(-100%);
|
transform: translateY(-8px);
|
||||||
|
opacity: 0;
|
||||||
visibility: hidden;
|
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
|
// Delay the visibility change until after the slide animation finishes
|
||||||
// so the panel stays visible while it animates closed.
|
// so the panel stays visible while it animates closed.
|
||||||
transition:
|
transition:
|
||||||
transform $long-animation-time,
|
transform $long-animation-time,
|
||||||
|
opacity $long-animation-time,
|
||||||
background-color $long-animation-time,
|
background-color $long-animation-time,
|
||||||
box-shadow $long-animation-time,
|
box-shadow $long-animation-time,
|
||||||
visibility 0s $long-animation-time;
|
visibility 0s $long-animation-time;
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
pointer-events: all;
|
||||||
transform: none;
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
background-color: $light-color;
|
background-color: $light-color;
|
||||||
box-shadow: $shadow;
|
box-shadow: $dropdown-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);
|
|
||||||
// On open, visibility flips immediately (no delay); transform +
|
// On open, visibility flips immediately (no delay); transform +
|
||||||
// colors + shadow animate over $long-animation-time.
|
// colors + shadow animate over $long-animation-time.
|
||||||
transition:
|
transition:
|
||||||
transform $long-animation-time,
|
transform $long-animation-time,
|
||||||
|
opacity $long-animation-time,
|
||||||
background-color $long-animation-time,
|
background-color $long-animation-time,
|
||||||
box-shadow $long-animation-time,
|
box-shadow $long-animation-time,
|
||||||
visibility 0s 0s;
|
visibility 0s 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.option {
|
||||||
@include sub-title-text();
|
@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;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-width) {
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='text'] {
|
input[type='text'] {
|
||||||
@include sub-title-text();
|
@include sub-title-text();
|
||||||
width: 100%;
|
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 {
|
.add-row {
|
||||||
height: 32px;
|
min-height: 40px;
|
||||||
@media (max-width: $mobile-width) { height: 24px; }
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
gap: var(--small-padding);
|
gap: var(--small-padding);
|
||||||
|
|
||||||
input[type='text'] {
|
input[type='text'] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: solid 2px transparent;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
box-shadow: none;
|
||||||
|
border-bottom-color: $text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: static;
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
&.add-button {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
&.pen {
|
&.pen {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
|
|
@ -184,6 +266,10 @@ import {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
// Kill the global button's hover-grow underline pseudo-element
|
||||||
|
// for the icon-only edit control.
|
||||||
|
&:after { content: none; display: none; }
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@include square(16px);
|
@include square(16px);
|
||||||
}
|
}
|
||||||
|
|
@ -224,16 +310,18 @@ import {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@include card();
|
@include card();
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
box-sizing: border-box;
|
||||||
transition:
|
transition:
|
||||||
box-shadow $long-animation-time,
|
box-shadow $long-animation-time,
|
||||||
height $long-animation-time,
|
height $long-animation-time,
|
||||||
border-radius $long-animation-time;
|
border-radius $long-animation-time;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
box-shadow: $shadow;
|
// Same shadow recipe as the panel below so the two halves read as
|
||||||
// Show shadow on top/left/right only; clip the bottom edge so the
|
// one continuous card. Clip the bottom edge so this shadow doesn't
|
||||||
// shadow doesn't bleed over the seam where .top meets .bottom.
|
// bleed across the seam where .top meets .bottom.
|
||||||
clip-path: inset(-6px -6px 0 -6px);
|
box-shadow: $dropdown-shadow;
|
||||||
|
clip-path: inset(-10px -10px 0 -10px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,25 +339,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 {
|
&:hover {
|
||||||
@media (min-width: $mobile-width) {
|
@media (min-width: $mobile-width) {
|
||||||
.background { box-shadow: $shadow; }
|
.background:not(.active) { box-shadow: $shadow; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.shadow-border {
|
&.shadow-border {
|
||||||
.background.active {
|
.background.active {
|
||||||
box-shadow: $shadow-border;
|
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 {
|
&.shadow-border:hover {
|
||||||
.background { box-shadow: $shadow-border; }
|
.background:not(.active) { box-shadow: $shadow-border; }
|
||||||
}
|
}
|
||||||
|
|
||||||
&.always-shadow {
|
&.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
|
// When open, clip the bottom so the always-on shadow doesn't bleed
|
||||||
// over the seam; restore full shadow when closed.
|
// over the seam; restore full shadow when closed.
|
||||||
&:has(.bottom.open) .background {
|
&:has(.bottom.open) .background {
|
||||||
|
|
@ -279,7 +373,7 @@ import {
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
export class SelectAddComponent implements OnChanges {
|
export class SelectAddComponent {
|
||||||
// ── New API (spec) ─────────────────────────────────────────────────────────
|
// ── New API (spec) ─────────────────────────────────────────────────────────
|
||||||
/** List of string options */
|
/** List of string options */
|
||||||
readonly items = input<string[]>([]);
|
readonly items = input<string[]>([]);
|
||||||
|
|
@ -309,6 +403,7 @@ export class SelectAddComponent implements OnChanges {
|
||||||
// ── Internal state ─────────────────────────────────────────────────────────
|
// ── Internal state ─────────────────────────────────────────────────────────
|
||||||
readonly open = signal(false);
|
readonly open = signal(false);
|
||||||
readonly editing = signal(false);
|
readonly editing = signal(false);
|
||||||
|
private readonly host = inject(ElementRef<HTMLElement>);
|
||||||
|
|
||||||
// Resolved values that merge old + new API
|
// Resolved values that merge old + new API
|
||||||
protected resolvedItems(): string[] {
|
protected resolvedItems(): string[] {
|
||||||
|
|
@ -320,7 +415,7 @@ export class SelectAddComponent implements OnChanges {
|
||||||
protected resolvedSelected(): string | null {
|
protected resolvedSelected(): string | null {
|
||||||
// New API: string
|
// New API: string
|
||||||
const s = this.selected();
|
const s = this.selected();
|
||||||
if (s != null) return s;
|
if (s != null && s.trim()) return s;
|
||||||
// Legacy API: index into options
|
// Legacy API: index into options
|
||||||
const idx = this.selectedIndex();
|
const idx = this.selectedIndex();
|
||||||
const opts = this.resolvedItems();
|
const opts = this.resolvedItems();
|
||||||
|
|
@ -328,8 +423,18 @@ export class SelectAddComponent implements OnChanges {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(_changes: SimpleChanges): void {
|
@HostListener('document:click', ['$event'])
|
||||||
// Nothing to do — signals handle reactivity
|
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: Event): void {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.open.update((v) => !v);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectItem(item: string): void {
|
onSelectItem(item: string): void {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue