Skip to content

Commit fb91c50

Browse files
committed
feat: add Spacing settings type for margin/padding control
1 parent 4d2071c commit fb91c50

File tree

7 files changed

+222
-0
lines changed

7 files changed

+222
-0
lines changed

resources/assets/editor/components.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ declare module 'vue' {
2929
IBiTags: typeof import('~icons/bi/tags')['default']
3030
IconPicker: typeof import('./components/IconPicker.vue')['default']
3131
IHeroiconsArrowLeft: typeof import('~icons/heroicons/arrow-left')['default']
32+
IHeroiconsArrowLongUp: typeof import('~icons/heroicons/arrow-long-up')['default']
3233
IHeroiconsArrowTopRightOnSquareSolid: typeof import('~icons/heroicons/arrow-top-right-on-square-solid')['default']
34+
IHeroiconsArrowUp: typeof import('~icons/heroicons/arrow-up')['default']
3335
IHeroiconsBold: typeof import('~icons/heroicons/bold')['default']
3436
IHeroiconsBuildingStorefront: typeof import('~icons/heroicons/building-storefront')['default']
3537
IHeroiconsCheckCircleSolid: typeof import('~icons/heroicons/check-circle-solid')['default']
3638
IHeroiconsChevronDown: typeof import('~icons/heroicons/chevron-down')['default']
39+
IHeroiconsChevronUp: typeof import('~icons/heroicons/chevron-up')['default']
3740
IHeroiconsChevronUpDown: typeof import('~icons/heroicons/chevron-up-down')['default']
3841
IHeroiconsEye: typeof import('~icons/heroicons/eye')['default']
3942
IHeroiconsEyeDropper: typeof import('~icons/heroicons/eye-dropper')['default']
@@ -61,6 +64,7 @@ declare module 'vue' {
6164
ProductPicker: typeof import('./components/ProductPicker.vue')['default']
6265
PublishAction: typeof import('./components/PublishAction.vue')['default']
6366
RichtextEditor: typeof import('./components/RichtextEditor.vue')['default']
67+
SpacingField: typeof import('./components/SpacingField.vue')['default']
6468
Spinner: typeof import('./components/Spinner.vue')['default']
6569
TemplateSelector: typeof import('./components/TemplateSelector.vue')['default']
6670
ThemeSettingsPanel: typeof import('./components/ThemeSettingsPanel.vue')['default']
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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>

resources/assets/editor/plugin.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import IconPicker from './components/IconPicker.vue';
2525
import ImagePicker from './components/ImagePicker.vue';
2626
import RichtextEditor from './components/RichtextEditor.vue';
2727
import GradientPicker from './components/GradientPicker.vue';
28+
import SpacingField from './components/SpacingField.vue';
2829
import PublishAction from './components/PublishAction.vue';
2930
import PreviewAction from './components/PreviewAction.vue';
3031
import useI18n from './composables/i18n';
@@ -143,6 +144,11 @@ function registerPropertyFields(ui: PluginContext['editor']['ui']) {
143144
type: 'gradient',
144145
render: GradientPicker,
145146
});
147+
148+
ui.registerPropertyField({
149+
type: 'spacing',
150+
render: SpacingField,
151+
});
146152
}
147153

148154
function mergeUpdates(updates: UpdatesEvent[]): UpdatesEvent {

src/Providers/CoreServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ protected function registerPropertyTransformers(): void
138138
'color-scheme' => SettingTransformers\ColorSchemeTransformer::class,
139139
'color-scheme-group' => SettingTransformers\ColorSchemeGroupTransformer::class,
140140
'gradient' => SettingTransformers\GradientTransformer::class,
141+
'spacing' => SettingTransformers\SpacingTransformer::class,
141142
];
142143

143144
foreach ($transformers as $type => $transformerClass) {

src/Settings/Spacing.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace BagistoPlus\Visual\Settings;
4+
5+
class Spacing extends Base
6+
{
7+
protected static string $type = 'spacing';
8+
9+
public function min(int|float $min): self
10+
{
11+
$this->meta['min'] = $min;
12+
13+
return $this;
14+
}
15+
16+
public function max(int|float $max): self
17+
{
18+
$this->meta['max'] = $max;
19+
20+
return $this;
21+
}
22+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace BagistoPlus\Visual\Settings\Support;
4+
5+
use BagistoPlus\Visual\Contracts\SettingTransformerInterface;
6+
7+
class SpacingTransformer implements SettingTransformerInterface
8+
{
9+
public function transform(mixed $value): mixed
10+
{
11+
if (! $value || ! is_array($value)) {
12+
$value = $this->getDefault();
13+
}
14+
15+
return new SpacingValue($value);
16+
}
17+
18+
private function getDefault(): array
19+
{
20+
return [
21+
'top' => 0,
22+
'right' => 0,
23+
'bottom' => 0,
24+
'left' => 0,
25+
];
26+
}
27+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace BagistoPlus\Visual\Settings\Support;
4+
5+
class SpacingValue
6+
{
7+
public readonly int $top;
8+
public readonly int $right;
9+
public readonly int $bottom;
10+
public readonly int $left;
11+
12+
public function __construct(array $data)
13+
{
14+
$this->top = $data['top'] ?? 0;
15+
$this->right = $data['right'] ?? 0;
16+
$this->bottom = $data['bottom'] ?? 0;
17+
$this->left = $data['left'] ?? 0;
18+
}
19+
}

0 commit comments

Comments
 (0)