Design language tweaks, improvements

This commit is contained in:
2026-05-13 20:44:01 +12:00
parent de8b60b9c3
commit baafafabdb
22 changed files with 1302 additions and 998 deletions
+233 -31
View File
@@ -5,6 +5,8 @@
export let values: IconCard[];
let valuesScroller: HTMLDivElement | undefined;
let activeIndex = 0;
let mobileCardsObserver: IntersectionObserver | null = null;
$: orderedValues = values
.map((value, index) => ({ value, index }))
@@ -24,30 +26,102 @@
return typeof window !== 'undefined' && window.innerWidth <= 768;
}
function cardScrollLeft(card: HTMLElement) {
return Math.max(0, card.offsetLeft - 8);
}
async function scrollValues(direction: 1 | -1) {
if (!valuesScroller || !isMobileViewport()) {
return;
}
const firstCard = valuesScroller.querySelector<HTMLElement>('.value-card');
if (!firstCard) {
return;
}
const cardStyle = window.getComputedStyle(valuesScroller);
const gap = Number.parseFloat(cardStyle.columnGap || cardStyle.gap || '0') || 0;
const step = firstCard.offsetWidth + gap;
await tick();
valuesScroller.scrollBy({ left: direction * step, behavior: 'smooth' });
const nextIndex = Math.max(0, Math.min(activeIndex + direction, orderedValues.length - 1));
await scrollToValue(nextIndex, 'smooth');
}
onMount(() => {
async function scrollToValue(index: number, behavior: ScrollBehavior = 'smooth') {
if (!valuesScroller || !isMobileViewport()) {
return;
}
valuesScroller.scrollTo({ left: 0, behavior: 'auto' });
const cards = valuesScroller.querySelectorAll<HTMLElement>('.value-card');
const targetCard = cards[index];
if (!targetCard) {
return;
}
activeIndex = index;
await tick();
valuesScroller.scrollTo({
left: cardScrollLeft(targetCard),
behavior
});
}
function bindMobileCardObserver() {
mobileCardsObserver?.disconnect();
if (!valuesScroller || !isMobileViewport()) {
return;
}
const cards = valuesScroller.querySelectorAll<HTMLElement>('.value-card');
if (!cards.length) {
return;
}
mobileCardsObserver = new IntersectionObserver(
(entries) => {
const visibleEntry = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (!visibleEntry) {
return;
}
const nextIndex = cards.length ? [...cards].indexOf(visibleEntry.target as HTMLElement) : -1;
if (nextIndex >= 0) {
activeIndex = nextIndex;
}
},
{
root: valuesScroller,
threshold: [0.6, 0.75, 0.9]
}
);
cards.forEach((card) => mobileCardsObserver?.observe(card));
}
onMount(() => {
const handleResize = () => {
if (!valuesScroller) {
return;
}
if (isMobileViewport()) {
if (activeIndex === 0) {
valuesScroller.scrollTo({ left: 0, behavior: 'auto' });
}
bindMobileCardObserver();
void scrollToValue(activeIndex, 'auto');
} else {
mobileCardsObserver?.disconnect();
}
};
if (valuesScroller && isMobileViewport()) {
valuesScroller.scrollTo({ left: 0, behavior: 'auto' });
bindMobileCardObserver();
}
window.addEventListener('resize', handleResize);
return () => {
mobileCardsObserver?.disconnect();
window.removeEventListener('resize', handleResize);
};
});
</script>
@@ -61,8 +135,8 @@
<div class="values-shell">
<div bind:this={valuesScroller} class="values-grid">
{#each orderedValues as value}
<div class="value-card">
{#each orderedValues as value, index}
<div class:active={index === activeIndex} class="value-card">
<div class="value-icon-wrap">
<Icon name={value.icon} className="value-card-icon" />
</div>
@@ -75,10 +149,34 @@
</div>
<div class="values-mobile-controls" aria-label="Value cards navigation">
<button type="button" class="values-mobile-button" aria-label="Previous value" on:click={() => scrollValues(-1)}>
<button
type="button"
class="values-mobile-button"
aria-label="Previous value"
disabled={activeIndex === 0}
on:click={() => scrollValues(-1)}
>
<Icon name="fas fa-chevron-left" />
</button>
<button type="button" class="values-mobile-button" aria-label="Next value" on:click={() => scrollValues(1)}>
<div class="values-mobile-pager" aria-label="Current value">
{#each orderedValues as _, index}
<button
type="button"
class:active={index === activeIndex}
class="values-mobile-dot"
aria-label={`Go to value ${index + 1}`}
aria-pressed={index === activeIndex}
on:click={() => scrollToValue(index)}
/>
{/each}
</div>
<button
type="button"
class="values-mobile-button"
aria-label="Next value"
disabled={activeIndex === orderedValues.length - 1}
on:click={() => scrollValues(1)}
>
<Icon name="fas fa-chevron-right" />
</button>
</div>
@@ -117,21 +215,27 @@
@media (max-width: 768px) {
.values-shell {
margin-top: 32px;
margin-top: 24px;
overflow: hidden;
}
.values-grid {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(272px, 84vw);
grid-auto-columns: calc(100% - 64px);
grid-template-columns: none;
gap: 14px;
align-items: stretch;
gap: 10px;
margin-top: 0;
border-top: none;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x proximity;
scroll-padding-left: 24px;
padding: 0 24px 8px 0;
scroll-snap-type: x mandatory;
scroll-padding-left: 8px;
padding: 0 14px 8px 8px;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
touch-action: pan-x pinch-zoom;
}
.values-grid::-webkit-scrollbar {
@@ -140,9 +244,10 @@
.values-mobile-controls {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-top: 12px;
}
.values-mobile-button {
@@ -158,19 +263,116 @@
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
transition:
background 0.18s ease,
opacity 0.18s ease,
transform 0.18s ease;
}
.values-mobile-button:disabled {
opacity: 0.35;
}
.values-mobile-pager {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-width: 0;
flex: 1;
}
.values-mobile-dot {
width: 9px;
height: 9px;
padding: 0;
border: none;
border-radius: 999px;
background: rgba(255, 255, 255, 0.28);
transition:
width 0.22s ease,
background 0.22s ease,
transform 0.22s ease;
}
.values-mobile-dot.active {
width: 28px;
background: var(--yellow);
}
.value-card {
min-height: 100%;
padding: 24px 22px;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
min-height: clamp(230px, 42svh, 320px);
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 20px 18px 22px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 24px;
background: rgba(255, 255, 255, 0.05);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.07));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 6px 16px rgba(0, 0, 0, 0.06);
scroll-snap-align: start;
scroll-snap-stop: always;
flex-direction: column;
justify-content: flex-start;
gap: 14px;
transition:
background 0.24s ease,
box-shadow 0.24s ease,
border-color 0.24s ease;
touch-action: pan-x;
user-select: none;
-webkit-user-select: none;
}
.value-card.active {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.09));
border-color: rgba(255, 255, 255, 0.16);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 10px 22px rgba(0, 0, 0, 0.08);
}
.value-card:nth-child(odd) {
border-right: 1px solid rgba(255, 255, 255, 0.1);
border-right: none;
}
.value-card:last-child {
margin-right: 2px;
}
.value-icon-wrap {
width: 56px;
height: 56px;
border-radius: 18px;
margin-top: 0;
background: rgba(255, 255, 255, 0.1);
}
.value-card .value-card-icon {
font-size: 23px;
}
.value-text {
max-width: 30ch;
min-width: 0;
margin-top: 0;
}
.value-text h3 {
margin-bottom: 8px;
font-size: 21px;
line-height: 1.08;
}
.value-card p {
font-size: 14px;
line-height: 1.55;
opacity: 0.9;
}
.values-eyebrow {