1+ <script setup lang="ts">
2+ import type { PropertyField } from ' @craftile/types' ;
3+ import { NumberInput } from ' @ark-ui/vue/number-input' ;
4+ import HeroiconsLink from ' ~icons/heroicons/link' ;
5+ import HeroiconsLinkSlash from ' ~icons/heroicons/link-slash' ;
6+ import ArrowLongUp from ' ~icons/heroicons/arrow-long-up' ;
7+ import ArrowLongDown from ' ~icons/heroicons/arrow-long-down' ;
8+ import ArrowLongLeft from ' ~icons/heroicons/arrow-long-left' ;
9+ import ArrowLongRight from ' ~icons/heroicons/arrow-long-right' ;
10+
11+
12+ const sides = [' top' , ' right' , ' bottom' , ' left' ] as const
13+ const icons = {
14+ top: ArrowLongUp ,
15+ right: ArrowLongRight ,
16+ bottom: ArrowLongDown ,
17+ left: ArrowLongLeft ,
18+ };
19+
20+ interface SpacingValue {
21+ top: number ;
22+ right: number ;
23+ bottom: number ;
24+ left: number ;
25+ }
26+
27+ interface Props {
28+ field: PropertyField ;
29+ }
30+
31+ defineProps <Props >();
32+
33+ const modelValue = defineModel <SpacingValue | null >();
34+
35+ const defaultSpacing: SpacingValue = {
36+ top: 0 ,
37+ right: 0 ,
38+ bottom: 0 ,
39+ left: 0 ,
40+ };
41+
42+ const model = computed ({
43+ get : () => modelValue .value || defaultSpacing ,
44+ set : (value : SpacingValue ) => {
45+ modelValue .value = value ;
46+ },
47+ });
48+
49+ const isLinked = ref (true );
50+
51+ function updateValue(side : keyof SpacingValue , value : number ) {
52+ if (isLinked .value ) {
53+ model .value = {
54+ top: value ,
55+ right: value ,
56+ bottom: value ,
57+ left: value ,
58+ };
59+ } else {
60+ model .value = {
61+ ... model .value ,
62+ [side ]: value ,
63+ };
64+ }
65+ }
66+
67+ function toggleLink() {
68+ isLinked .value = ! isLinked .value ;
69+
70+ if (isLinked .value ) {
71+ const topValue = model .value .top ;
72+ model .value = {
73+ top: topValue ,
74+ right: topValue ,
75+ bottom: topValue ,
76+ left: topValue ,
77+ };
78+ }
79+ }
80+
81+ onMounted (() => {
82+ if (model .value ) {
83+ isLinked .value = model .value .top === model .value .right
84+ && model .value .top === model .value .bottom
85+ && model .value .top === model .value .left
86+ }
87+ });
88+ </script >
89+
90+ <template >
91+ <div class =" flex flex-col gap-1" >
92+ <div
93+ v-if =" field.label"
94+ class =" flex items-center justify-between"
95+ >
96+ <label class =" text-sm font-medium text-gray-700" >
97+ {{ field.label }}
98+ </label >
99+ </div >
100+
101+ <div class =" flex border border-gray-300 rounded overflow-hidden divide-x" >
102+ <div class =" grid grid-cols-4 divide-x " >
103+ <NumberInput .Root
104+ class =" flex w-full relative group items-center gap-0.5 px-0.5"
105+ v-for =" side in sides"
106+ :model-value =" String(model[side])"
107+ :min =" field.min"
108+ :max =" field.max"
109+ @update:model-value =" updateValue(side, Number($event))"
110+ >
111+ <component
112+ :is =" icons[side]"
113+ class =" w-4 h-4 text-gray-600 flex-none"
114+ />
115+ <NumberInput .Input class =" flex-1 w-full border-none outline-none text-sm py-1.5" />
116+ <NumberInput .Control class =" absolute top-0 right-1 bottom-0 flex flex-col hidden group-hover:flex" >
117+ <NumberInput .IncrementTrigger class =" flex-1 flex items-center" >
118+ <i-heroicons-chevron-up class =" w-3 h-3 text-gray-600" />
119+ </NumberInput .IncrementTrigger >
120+ <NumberInput .DecrementTrigger class =" flex-1 flex items-center" >
121+ <i-heroicons-chevron-down class =" w-3 h-3 text-gray-600" />
122+ </NumberInput .DecrementTrigger >
123+ </NumberInput .Control >
124+ </NumberInput .Root >
125+ </div >
126+ <button
127+ type =" button"
128+ class =" flex-none p-1 rounded hover:bg-gray-100 transition-colors"
129+ :class =" { 'text-blue-600': isLinked, 'text-gray-400': !isLinked }"
130+ @click =" toggleLink"
131+ >
132+ <HeroiconsLink
133+ v-if =" isLinked"
134+ class =" w-4 h-4"
135+ />
136+ <HeroiconsLinkSlash
137+ v-else
138+ class =" w-4 h-4"
139+ />
140+ </button >
141+ </div >
142+ </div >
143+ </template >
0 commit comments