Implement radial category selector

This commit is contained in:
openclaw 2026-07-23 23:14:19 +08:00
parent 9e734babbb
commit 00e5e035a9
2 changed files with 309 additions and 324 deletions

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { EntryType } from "@cents/domain"; import type { EntryType } from "@cents/domain";
import { ChevronRight } from "@lucide/vue"; import { ArrowLeft, Check } from "@lucide/vue";
import { computed, onBeforeUnmount, ref, watch } from "vue"; import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { import {
categories, categories,
entryTypes, entryTypes,
@ -10,6 +10,7 @@ import {
} from "../data/categories"; } from "../data/categories";
type SelectorLevel = "type" | "category" | "subcategory"; type SelectorLevel = "type" | "category" | "subcategory";
type SelectorOption = EntryTypeOption | Category;
const props = defineProps<{ const props = defineProps<{
open: boolean; open: boolean;
@ -23,60 +24,93 @@ const emit = defineEmits<{
confirm: []; confirm: [];
}>(); }>();
const FOCUS_ANGLE = 270;
const selectorLevel = ref<SelectorLevel>("type"); const selectorLevel = ref<SelectorLevel>("type");
const axisOffset = ref(0); const wheelStage = ref<HTMLElement | null>(null);
let axisPointerId: number | null = null; const wheelRotation = ref(0);
let axisDragStartY = 0; const wheelPointerId = ref<number | null>(null);
let axisDragStartOffset = 0; let pointerLastAngle = 0;
let axisDragged = false; let pointerLastTime = 0;
let axisLastY = 0; let pointerVelocity = 0;
let axisLastTime = 0; let pointerTravel = 0;
let axisVelocity = 0; let inertiaFrame: number | null = null;
let axisInertiaFrame: number | null = null;
const typeOption = computed(() => entryTypes.find((item) => item.id === props.entryType)!);
const selectedCategory = computed(() => props.categoryPath.at(-1));
const selectedParent = computed(() => props.categoryPath[0]); const selectedParent = computed(() => props.categoryPath[0]);
const selectorOptions = computed<(EntryTypeOption | Category)[]>(() => { const selectorOptions = computed<SelectorOption[]>(() => {
if (selectorLevel.value === "type") { if (selectorLevel.value === "type") return entryTypes;
const current = entryTypes.find((option) => option.id === props.entryType);
return [...entryTypes.filter((option) => option.id !== props.entryType), ...(current ? [current] : [])];
}
if (selectorLevel.value === "category") return categories[props.entryType]; if (selectorLevel.value === "category") return categories[props.entryType];
return selectedParent.value?.children ?? []; return selectedParent.value?.children ?? [];
}); });
const upperSelectorOptions = computed(() => { const segmentAngle = computed(() => 360 / Math.max(1, selectorOptions.value.length));
const count = Math.min(2, Math.floor(selectorOptions.value.length / 2)); const selectedIndex = computed(() => {
return selectorOptions.value.slice(0, count); const selectedId =
selectorLevel.value === "type"
? props.entryType
: selectorLevel.value === "category"
? selectedParent.value?.id
: props.categoryPath[1]?.id;
return selectorOptions.value.findIndex((option) => option.id === selectedId);
}); });
const upperAxisHeight = computed(() => { const levelLabel = computed(() => {
const count = upperSelectorOptions.value.length; if (selectorLevel.value === "type") return "选择收支类型";
return count * 72 + Math.max(0, count - 1) * 10 + 34; if (selectorLevel.value === "category") return "选择大类";
return `选择${selectedParent.value?.label ?? ""}细类`;
}); });
const axisBaseOffset = computed(() => upperSelectorOptions.value.length * 82); const canGoBack = computed(() => selectorLevel.value !== "type");
const axisMaxOffset = computed(() => const ringBackground = computed(() => {
Math.max(0, (selectorOptions.value.length - upperSelectorOptions.value.length - 2) * 82), const options = selectorOptions.value;
if (!options.length) return "transparent";
const step = segmentAngle.value;
const stops = options
.map((option, index) => `${option.color} ${index * step}deg ${(index + 1) * step}deg`)
.join(",");
return `conic-gradient(from ${-step / 2}deg,${stops})`;
});
const rotorStyle = computed(() => ({
"--wheel-rotation": `${wheelRotation.value}deg`,
"--ring-background": ringBackground.value,
}));
watch(
() => props.open,
async (open) => {
stopInertia();
if (!open) return;
selectorLevel.value = "type";
await nextTick();
resetShellScroll();
alignSelectedOption();
},
); );
const selectorPath = computed<(EntryTypeOption | Category)[]>(() => {
if (selectorLevel.value === "type") return []; watch(selectorOptions, async () => {
if (selectorLevel.value === "category") return [typeOption.value]; await nextTick();
return selectedParent.value ? [typeOption.value, selectedParent.value] : [typeOption.value]; resetShellScroll();
alignSelectedOption();
}); });
watch(() => props.open, (open) => { onBeforeUnmount(stopInertia);
if (open) resetAxisScroll();
else stopAxisInertia();
});
onBeforeUnmount(stopAxisInertia); function optionStyle(index: number) {
return {
"--option-angle": `${index * segmentAngle.value}deg`,
};
}
function isOptionSelected(option: SelectorOption) {
if (selectorLevel.value === "type") return option.id === props.entryType;
if (selectorLevel.value === "category") return option.id === selectedParent.value?.id;
return option.id === props.categoryPath[1]?.id;
}
function chooseOption(option: SelectorOption) {
if (pointerTravel > 5) return;
function chooseSelectorOption(option: EntryTypeOption | Category) {
if (selectorLevel.value === "type") { if (selectorLevel.value === "type") {
const nextType = option.id as EntryType; const nextType = option.id as EntryType;
if (nextType !== props.entryType) emit("update:categoryPath", []); if (nextType !== props.entryType) emit("update:categoryPath", []);
emit("update:entryType", nextType); emit("update:entryType", nextType);
selectorLevel.value = "category"; selectorLevel.value = "category";
resetAxisScroll();
return; return;
} }
@ -84,12 +118,8 @@ function chooseSelectorOption(option: EntryTypeOption | Category) {
if (selectorLevel.value === "category") { if (selectorLevel.value === "category") {
const currentChild = selectedParent.value?.id === category.id ? props.categoryPath[1] : undefined; const currentChild = selectedParent.value?.id === category.id ? props.categoryPath[1] : undefined;
emit("update:categoryPath", currentChild ? [category, currentChild] : [category]); emit("update:categoryPath", currentChild ? [category, currentChild] : [category]);
if (category.children?.length) { if (category.children?.length) selectorLevel.value = "subcategory";
selectorLevel.value = "subcategory"; else emit("confirm");
resetAxisScroll();
} else {
emit("confirm");
}
return; return;
} }
@ -98,184 +128,158 @@ function chooseSelectorOption(option: EntryTypeOption | Category) {
emit("confirm"); emit("confirm");
} }
function choosePathLevel(index: number) { function goBack() {
selectorLevel.value = index === 0 ? "type" : "category"; if (selectorLevel.value === "subcategory") {
resetAxisScroll(); selectorLevel.value = "category";
return;
}
if (selectorLevel.value === "category") selectorLevel.value = "type";
} }
function isSelectorOptionSelected(option: EntryTypeOption | Category) { function confirmSelection() {
if (selectorLevel.value === "type") return option.id === props.entryType; if (selectorLevel.value === "type") {
if (selectorLevel.value === "category") return option.id === selectedParent.value?.id; selectorLevel.value = "category";
return props.categoryPath.length > 1 && option.id === selectedCategory.value?.id; return;
}
if (selectedParent.value) emit("confirm");
} }
function resetAxisScroll() { function alignSelectedOption() {
stopAxisInertia(); const index = Math.max(0, selectedIndex.value);
axisOffset.value = 0; wheelRotation.value = FOCUS_ANGLE - index * segmentAngle.value;
} }
function clampAxisOffset(value: number) { function pointerAngle(event: PointerEvent) {
return Math.max(0, Math.min(axisMaxOffset.value, value)); const bounds = wheelStage.value?.getBoundingClientRect();
if (!bounds) return 0;
const centerX = bounds.left + bounds.width / 2;
const centerY = bounds.top + bounds.height / 2;
return (Math.atan2(event.clientY - centerY, event.clientX - centerX) * 180) / Math.PI;
} }
function startAxisDrag(event: PointerEvent) { function startWheelDrag(event: PointerEvent) {
stopAxisInertia(); stopInertia();
axisPointerId = event.pointerId; wheelPointerId.value = event.pointerId;
axisDragStartY = event.clientY; pointerLastAngle = pointerAngle(event);
axisDragStartOffset = axisOffset.value; pointerLastTime = performance.now();
axisDragged = false; pointerVelocity = 0;
axisLastY = event.clientY; pointerTravel = 0;
axisLastTime = performance.now();
axisVelocity = 0;
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
} }
function moveAxisDrag(event: PointerEvent) { function moveWheelDrag(event: PointerEvent) {
if (axisPointerId !== event.pointerId) return; if (wheelPointerId.value !== event.pointerId) return;
const distance = axisDragStartY - event.clientY; const angle = pointerAngle(event);
if (Math.abs(distance) > 5) axisDragged = true; const delta = normalizeAngle(angle - pointerLastAngle);
axisOffset.value = clampAxisOffset(axisDragStartOffset + distance);
const now = performance.now(); const now = performance.now();
const elapsed = Math.max(1, now - axisLastTime); const elapsed = Math.max(1, now - pointerLastTime);
const instantaneousVelocity = (axisLastY - event.clientY) / elapsed; wheelRotation.value += delta;
axisVelocity = axisVelocity * 0.65 + instantaneousVelocity * 0.35; pointerTravel += Math.abs(delta);
axisLastY = event.clientY; if (pointerTravel > 5 && wheelStage.value && !wheelStage.value.hasPointerCapture(event.pointerId)) {
axisLastTime = now; wheelStage.value.setPointerCapture(event.pointerId);
}
pointerVelocity = pointerVelocity * 0.62 + (delta / elapsed) * 0.38;
pointerLastAngle = angle;
pointerLastTime = now;
} }
function endAxisDrag(event: PointerEvent) { function endWheelDrag(event: PointerEvent) {
if (axisPointerId !== event.pointerId) return; if (wheelPointerId.value !== event.pointerId) return;
axisPointerId = null; wheelPointerId.value = null;
const target = event.currentTarget as HTMLElement; if (wheelStage.value?.hasPointerCapture(event.pointerId)) {
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId); wheelStage.value.releasePointerCapture(event.pointerId);
if (axisDragged) startAxisInertia(); }
if (pointerTravel > 5 && Math.abs(pointerVelocity) > 0.015) startInertia();
else snapWheel();
} }
function handleAxisClick(event: MouseEvent) { function cancelOptionClick(event: MouseEvent) {
if (!axisDragged) return; if (pointerTravel <= 5) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
axisDragged = false; pointerTravel = 0;
} }
function scrollAxisBy(delta: number) { function startInertia() {
stopAxisInertia();
axisOffset.value = clampAxisOffset(axisOffset.value + delta);
}
function startAxisInertia() {
if (Math.abs(axisVelocity) < 0.04) return;
let previousTime = performance.now(); let previousTime = performance.now();
const step = (now: number) => { const step = (now: number) => {
const elapsed = Math.min(32, now - previousTime); const elapsed = Math.min(32, now - previousTime);
previousTime = now; previousTime = now;
const current = axisOffset.value; wheelRotation.value += pointerVelocity * elapsed;
const next = clampAxisOffset(current + axisVelocity * elapsed); pointerVelocity *= Math.pow(0.92, elapsed / 16);
axisOffset.value = next; if (Math.abs(pointerVelocity) < 0.012) {
inertiaFrame = null;
if (next === current && (next === 0 || next === axisMaxOffset.value)) { snapWheel();
stopAxisInertia();
return; return;
} }
inertiaFrame = requestAnimationFrame(step);
axisVelocity *= Math.pow(0.91, elapsed / 16);
if (Math.abs(axisVelocity) < 0.025) {
stopAxisInertia();
return;
}
axisInertiaFrame = requestAnimationFrame(step);
}; };
inertiaFrame = requestAnimationFrame(step);
axisInertiaFrame = requestAnimationFrame(step);
} }
function stopAxisInertia() { function snapWheel() {
if (axisInertiaFrame !== null) cancelAnimationFrame(axisInertiaFrame); const step = segmentAngle.value;
axisInertiaFrame = null; const target = Math.round((wheelRotation.value - FOCUS_ANGLE) / step) * step + FOCUS_ANGLE;
axisVelocity = 0; wheelRotation.value = target;
}
function stopInertia() {
if (inertiaFrame !== null) cancelAnimationFrame(inertiaFrame);
inertiaFrame = null;
pointerVelocity = 0;
}
function normalizeAngle(value: number) {
return ((value + 180) % 360 + 360) % 360 - 180;
}
function resetShellScroll() {
const shell = wheelStage.value?.closest<HTMLElement>(".app-shell");
if (shell) shell.scrollLeft = 0;
} }
</script> </script>
<template> <template>
<Transition name="coordinate-selector"> <Transition name="coordinate-selector">
<div v-if="open" class="category-coordinate" aria-label="二维分类选择器" @click.stop> <div v-if="open" class="category-wheel-selector" :aria-label="levelLabel" @click.stop>
<div class="selector-path" aria-label="已选分类路径"> <span class="wheel-level-label">{{ levelLabel }}</span>
<template v-for="(option, index) in selectorPath" :key="option.id">
<ChevronRight v-if="index > 0" :size="18" aria-hidden="true" />
<button
type="button"
:style="{ '--path-color': option.color }"
:aria-label="`返回${option.label}层重新选择`"
@click="choosePathLevel(index)"
>
<component :is="option.icon" :size="22" />
<small>{{ option.label }}</small>
</button>
</template>
<ChevronRight v-if="selectorPath.length" :size="18" aria-hidden="true" />
</div>
<div <div
v-if="upperSelectorOptions.length" ref="wheelStage"
class="candidate-above-axis" class="category-wheel-stage"
:style="{ height: `${upperAxisHeight}px` }" :class="{ dragging: wheelPointerId !== null }"
@pointerdown="startWheelDrag"
@pointermove="moveWheelDrag"
@pointerup="endWheelDrag"
@pointercancel="endWheelDrag"
@click.capture="cancelOptionClick"
> >
<div <div class="category-wheel-rotor" :style="rotorStyle">
class="candidate-axis-viewport" <div class="category-wheel-disc"></div>
@pointerdown="startAxisDrag" <button
@pointermove="moveAxisDrag" v-for="(option, index) in selectorOptions"
@pointerup="endAxisDrag" :key="option.id"
@pointercancel="endAxisDrag" type="button"
@click.capture="handleAxisClick" class="wheel-option"
@wheel.prevent="scrollAxisBy($event.deltaY)" :class="{ selected: isOptionSelected(option) }"
> :style="optionStyle(index)"
<div class="candidate-axis-list" :style="{ transform: `translate3d(0, -${axisOffset}px, 0)` }"> :aria-label="option.label"
<button @click="chooseOption(option)"
v-for="option in selectorOptions" >
:key="option.id" <span>{{ option.label }}</span>
type="button" <component :is="option.icon" :size="21" />
class="coordinate-option" </button>
:class="{ selected: isSelectorOptionSelected(option) }"
:style="{ '--option-color': option.color, '--option-tint': option.tint }"
@click="chooseSelectorOption(option)"
>
<span class="coordinate-icon"><component :is="option.icon" :size="24" /></span>
<span>{{ option.label }}</span>
</button>
</div>
</div> </div>
</div> </div>
<div class="candidate-below-axis"> <div class="wheel-center-controls">
<div <button type="button" class="wheel-back" :disabled="!canGoBack" aria-label="返回上一级" @click="goBack">
class="candidate-axis-viewport" <ArrowLeft :size="16" />
@pointerdown="startAxisDrag" <span>返回</span>
@pointermove="moveAxisDrag" </button>
@pointerup="endAxisDrag" <button type="button" class="wheel-confirm" aria-label="确认当前分类" @click="confirmSelection">
@pointercancel="endAxisDrag" <Check :size="17" />
@click.capture="handleAxisClick" <span>确认</span>
@wheel.prevent="scrollAxisBy($event.deltaY)" </button>
>
<div
class="candidate-axis-list"
:style="{ transform: `translate3d(0, -${axisBaseOffset + axisOffset}px, 0)` }"
>
<button
v-for="option in selectorOptions"
:key="option.id"
type="button"
class="coordinate-option"
:class="{ selected: isSelectorOptionSelected(option) }"
:style="{ '--option-color': option.color, '--option-tint': option.tint }"
@click="chooseSelectorOption(option)"
>
<span class="coordinate-icon"><component :is="option.icon" :size="24" /></span>
<span>{{ option.label }}</span>
</button>
</div>
</div>
</div> </div>
</div> </div>
</Transition> </Transition>

View File

@ -56,6 +56,7 @@ input:focus-visible {
min-height: 0; min-height: 0;
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
overflow: clip;
overscroll-behavior: none; overscroll-behavior: none;
background: #f7faf9; background: #f7faf9;
box-shadow: 0 0 40px rgba(25, 53, 48, 0.14); box-shadow: 0 0 40px rgba(25, 53, 48, 0.14);
@ -924,6 +925,8 @@ input:focus-visible {
background: color-mix(in srgb, var(--category-color) 13%, white); background: color-mix(in srgb, var(--category-color) 13%, white);
box-shadow: 0 10px 24px rgba(29, 57, 50, 0.22); box-shadow: 0 10px 24px rgba(29, 57, 50, 0.22);
isolation: isolate; isolation: isolate;
visibility: hidden;
pointer-events: none;
} }
.meta-category-button.active > * { .meta-category-button.active > * {
@ -965,7 +968,10 @@ input:focus-visible {
-webkit-backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
} }
.category-coordinate { .category-wheel-selector {
--wheel-size: clamp(286px, 88vw, 344px);
--center-size: clamp(122px, 35vw, 140px);
--option-radius: calc((var(--wheel-size) + var(--center-size)) / 4);
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
@ -976,174 +982,149 @@ input:focus-visible {
pointer-events: none; pointer-events: none;
} }
.selector-path { .wheel-level-label {
position: absolute; position: absolute;
top: -2px; width: 1px;
right: 100%; height: 1px;
width: min(392px, calc(100vw - 88px));
height: 76px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
overflow: visible;
border: 0;
padding: 2px 6px;
background: transparent;
pointer-events: auto;
}
.selector-path > button {
width: 72px;
min-width: 72px;
height: 72px;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
overflow: hidden; overflow: hidden;
border: 2px solid color-mix(in srgb, var(--path-color) 72%, #263b36); clip: rect(0 0 0 0);
border-radius: 50%; clip-path: inset(50%);
padding: 0;
background: var(--path-color);
color: #ffffff;
font-weight: 740;
white-space: nowrap; white-space: nowrap;
box-shadow: 0 9px 22px rgba(29, 57, 50, 0.2);
} }
.selector-path > button:active, .category-wheel-stage {
.selector-path > button:focus-visible {
background: color-mix(in srgb, var(--path-color) 82%, #213832);
transform: translateY(1px);
}
.selector-path > button svg {
flex: 0 0 auto;
}
.selector-path small {
max-width: 62px;
overflow: hidden;
font-size: 13px;
line-height: 1.1;
text-overflow: ellipsis;
}
.selector-path > svg {
flex: 0 0 auto;
color: #8ca09b;
}
.candidate-above-axis,
.candidate-below-axis {
position: absolute; position: absolute;
right: -24px; top: 50%;
width: 120px; left: 50%;
background: transparent; width: var(--wheel-size);
pointer-events: auto; height: var(--wheel-size);
} transform: translate(-50%, -50%);
border-radius: 50%;
.candidate-above-axis {
bottom: 60px;
overflow: hidden;
mask-image: linear-gradient(
to bottom,
transparent 0,
#000 30px,
#000 calc(100% - 34px),
transparent calc(100% - 16px),
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0,
#000 30px,
#000 calc(100% - 34px),
transparent calc(100% - 16px),
transparent 100%
);
}
.candidate-below-axis {
top: 70px;
height: 208px;
overflow: hidden;
mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 30px), transparent 100%);
-webkit-mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 30px), transparent 100%);
}
.candidate-axis-viewport {
width: 100%;
height: 100%;
overflow: hidden;
touch-action: none; touch-action: none;
user-select: none; user-select: none;
cursor: grab; cursor: grab;
pointer-events: auto;
} }
.candidate-axis-viewport:active { .category-wheel-stage.dragging {
cursor: grabbing; cursor: grabbing;
} }
.candidate-axis-list { .category-wheel-rotor {
width: 120px; position: absolute;
min-height: max-content; inset: 0;
display: flex; transform: rotate(var(--wheel-rotation));
flex-direction: column; transform-origin: center;
gap: 10px; will-change: transform;
padding: 10px 24px 240px;
} }
.coordinate-option { .category-wheel-disc {
width: 72px; position: absolute;
min-width: 72px; inset: 0;
height: 72px; border-radius: 50%;
min-height: 72px; background: var(--ring-background);
box-shadow: 0 14px 34px rgba(29, 55, 48, 0.24);
-webkit-mask: radial-gradient(
circle,
transparent 0 calc(var(--center-size) / 2),
#000 calc(var(--center-size) / 2 + 1px)
);
mask: radial-gradient(
circle,
transparent 0 calc(var(--center-size) / 2),
#000 calc(var(--center-size) / 2 + 1px)
);
}
.wheel-option {
position: absolute;
top: 50%;
left: 50%;
width: 48px;
height: 54px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0; gap: 3px;
border: 2px solid color-mix(in srgb, var(--option-color) 50%, white); transform:
border-radius: 50%; translate(-50%, -50%)
rotate(var(--option-angle))
translateY(calc(0px - var(--option-radius)));
border: 0;
padding: 0; padding: 0;
background: color-mix(in srgb, var(--option-tint) 74%, white); background: transparent;
color: var(--option-color); color: #ffffff;
font-size: 13px; font-size: 11px;
line-height: 1.05; line-height: 1;
font-weight: 730; font-weight: 760;
text-shadow: 0 1px 3px rgba(19, 35, 31, 0.3);
white-space: nowrap; white-space: nowrap;
box-shadow: 0 9px 22px rgba(29, 57, 50, 0.2); pointer-events: auto;
} }
.coordinate-option.selected { .wheel-option span {
border-color: color-mix(in srgb, var(--option-color) 75%, #263b36); max-width: 48px;
background: var(--option-color);
color: #ffffff;
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.78), 0 11px 25px rgba(29, 57, 50, 0.25);
}
.coordinate-option.selected .coordinate-icon {
color: #ffffff;
}
.coordinate-icon {
width: 28px;
height: 28px;
flex: 0 0 auto;
display: grid;
place-items: center;
color: var(--option-color);
}
.coordinate-option > span:last-child {
max-width: 62px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.wheel-option.selected::after {
position: absolute;
bottom: -5px;
left: 50%;
width: 5px;
height: 5px;
transform: translateX(-50%);
border-radius: 50%;
background: #ffffff;
box-shadow: 0 1px 3px rgba(19, 35, 31, 0.25);
content: "";
}
.wheel-center-controls {
position: absolute;
top: 50%;
left: 50%;
z-index: 2;
width: var(--center-size);
height: var(--center-size);
display: grid;
grid-template-rows: 1fr 1fr;
transform: translate(-50%, -50%);
overflow: hidden;
border-radius: 50%;
box-shadow: 0 8px 24px rgba(29, 55, 48, 0.22);
pointer-events: auto;
}
.wheel-center-controls button {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
border: 0;
padding: 0;
font-size: 12px;
font-weight: 740;
}
.wheel-back {
border-bottom: 1px solid rgba(56, 72, 67, 0.14) !important;
background: #eef3f1;
color: #52635e;
}
.wheel-back:disabled {
color: #a6b0ad;
cursor: default;
}
.wheel-confirm {
background: #6f7f83;
color: #ffffff;
}
.share-sheet-layer { .share-sheet-layer {
position: absolute; position: absolute;
inset: 0; inset: 0;