Skip to content

Commit 6fce38b

Browse files
committed
feat: add inline rename support for typography presets
1 parent df048ea commit 6fce38b

File tree

8 files changed

+142
-6
lines changed

8 files changed

+142
-6
lines changed

resources/assets/editor/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ declare module 'vue' {
4242
IHeroiconsItalic: typeof import('~icons/heroicons/italic')['default']
4343
IHeroiconsLink: typeof import('~icons/heroicons/link')['default']
4444
IHeroiconsMagnifyingGlass: typeof import('~icons/heroicons/magnifying-glass')['default']
45+
IHeroiconsPencil: typeof import('~icons/heroicons/pencil')['default']
4546
IHeroiconsPlus: typeof import('~icons/heroicons/plus')['default']
4647
IHeroiconsQuestionMarkCircle: typeof import('~icons/heroicons/question-mark-circle')['default']
4748
IHeroiconsTrash: typeof import('~icons/heroicons/trash')['default']

resources/assets/editor/components/TypographyPicker.vue

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const typographyPresets = computed(() => {
1818
return {};
1919
}
2020
21-
// Find the TypographyPresets group setting
2221
const settingId = theme.value.settingsSchema
2322
.flatMap((obj) => obj.settings)
2423
.find((setting) => setting.type === 'typography_presets')?.id;
@@ -27,7 +26,6 @@ const typographyPresets = computed(() => {
2726
return {};
2827
}
2928
30-
// Return the presets from the group
3129
return theme.value.settings[settingId] || {};
3230
});
3331
@@ -45,11 +43,11 @@ const selectedPreset = computed(() => {
4543
});
4644
4745
const selectedLabel = computed(() => {
48-
if (!model.value) {
46+
if (!model.value || !selectedPreset.value) {
4947
return null;
5048
}
5149
52-
return toTitleCase(model.value);
50+
return selectedPreset.value.name || toTitleCase(model.value);
5351
});
5452
5553
function onSelectPreset(id: string) {

resources/assets/editor/components/TypographyPresetEditor.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface Font {
2020
}
2121
2222
interface TypographyPresetValue {
23+
name?: string;
2324
fontFamily: Font | null;
2425
fontStyle: string;
2526
fontWeight: number;

resources/assets/editor/components/TypographyPresetPreview.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const displayLabel = computed(() => {
2828
return '';
2929
}
3030
31-
return toTitleCase(props.label);
31+
return props.preset?.name || toTitleCase(props.label);
3232
});
3333
3434
const summary = computed(() => {

resources/assets/editor/components/TypographyPresets.vue

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { PropertyField } from '@craftile/types';
33
import { Dialog } from '@ark-ui/vue/dialog';
44
import useI18n from '../composables/i18n';
5+
import { toTitleCase } from '../utils/strings';
56
import TypographyPresetEditor from './TypographyPresetEditor.vue';
67
import TypographyPresetPreview from './TypographyPresetPreview.vue';
78
@@ -13,6 +14,7 @@ interface Font {
1314
}
1415
1516
interface TypographyPresetData {
17+
name?: string;
1618
fontFamily: Font | null;
1719
fontStyle: string;
1820
fontWeight: string;
@@ -33,6 +35,8 @@ const { t } = useI18n();
3335
3436
const editingPreset = ref<string | null>(null);
3537
const editModalOpen = ref(false);
38+
const isEditingName = ref(false);
39+
const editingNameValue = ref('');
3640
3741
const themePresetIds = computed(() => {
3842
return Object.keys(props.field.presets || {});
@@ -51,6 +55,18 @@ const editingPresetValue = computed({
5155
}
5256
});
5357
58+
const editingPresetDisplayName = computed(() => {
59+
if (!editingPreset.value || !editingPresetValue.value) {
60+
return '';
61+
}
62+
return editingPresetValue.value.name || toTitleCase(editingPreset.value);
63+
});
64+
65+
const canRenameCurrentPreset = computed(() => {
66+
if (!editingPreset.value) return false;
67+
return !themePresetIds.value.includes(editingPreset.value);
68+
});
69+
5470
function onEditPreset(id: string) {
5571
editingPreset.value = id;
5672
editModalOpen.value = true;
@@ -85,6 +101,47 @@ function onDeletePreset(id: string) {
85101
model.value = rest;
86102
}
87103
104+
function startEditingName() {
105+
if (!canRenameCurrentPreset.value) {
106+
return;
107+
}
108+
109+
isEditingName.value = true;
110+
editingNameValue.value = editingPresetValue.value?.name || editingPresetDisplayName.value;
111+
112+
nextTick(() => {
113+
const input = document.querySelector('.preset-name-input') as HTMLInputElement;
114+
input?.focus();
115+
input?.select();
116+
});
117+
}
118+
119+
function saveEditingName() {
120+
if (!editingPresetValue.value) {
121+
return;
122+
}
123+
124+
editingPresetValue.value = {
125+
...editingPresetValue.value,
126+
name: editingNameValue.value?.trim(),
127+
};
128+
129+
isEditingName.value = false;
130+
}
131+
132+
function cancelEditingName() {
133+
isEditingName.value = false;
134+
editingNameValue.value = '';
135+
}
136+
137+
function handleNameKeydown(e: KeyboardEvent) {
138+
if (e.key === 'Enter') {
139+
saveEditingName();
140+
} else if (e.key === 'Escape') {
141+
cancelEditingName();
142+
}
143+
}
144+
88145
function handleEditorDelete() {
89146
if (!editingPreset.value) {
90147
return;
@@ -148,7 +205,35 @@ const canDeleteCurrentPreset = computed(() => {
148205
<Dialog.Positioner class="flex fixed z-50 top-14 left-14 bottom-0 w-75 items-center justify-center">
149206
<Dialog.Content class="bg-white shadow flex flex-col w-full h-full overflow-hidden">
150207
<header class="flex-none h-12 border-b border-neutral-200 flex gap-3 px-4 items-center justify-between">
151-
<Dialog.Title class="capitalize">{{ t('Editing') }} {{ editingPreset?.replace('-', ' ') }}</Dialog.Title>
208+
<div class="flex items-center gap-2 flex-1 min-w-0">
209+
<span class="text-sm text-gray-600 flex-none">{{ t('Editing') }}</span>
210+
211+
<input
212+
v-if="isEditingName"
213+
v-model="editingNameValue"
214+
type="text"
215+
maxlength="50"
216+
class="preset-name-input flex-1 min-w-0 px-2 py-1 text-sm font-medium border border-blue-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
217+
@blur="saveEditingName"
218+
@keydown="handleNameKeydown"
219+
/>
220+
221+
<button
222+
v-else
223+
type="button"
224+
class="flex-1 min-w-0 text-left px-2 py-1 text-sm font-medium rounded truncate"
225+
:class="canRenameCurrentPreset ? 'hover:bg-gray-100 cursor-pointer' : 'cursor-default'"
226+
:title="canRenameCurrentPreset ? t('Click to rename') : t('Theme preset (cannot be renamed)')"
227+
@click="startEditingName"
228+
>
229+
{{ editingPresetDisplayName }}
230+
<i-heroicons-pencil
231+
v-if="canRenameCurrentPreset"
232+
class="inline-block w-3 h-3 ml-1 text-gray-400"
233+
/>
234+
</button>
235+
</div>
236+
152237
<Dialog.CloseTrigger class="cursor-pointer rounded-lg p-0.5 text-neutral-700 hover:bg-neutral-300">
153238
<i-heroicons-x-mark class="w-5 h-5" />
154239
</Dialog.CloseTrigger>

resources/lang/en/theme-editor.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@
8888
'Letter Spacing' => 'Letter Spacing',
8989
'Text Transform' => 'Text Transform',
9090

91+
// Typography preset rename (inline editing)
92+
'Click to rename' => 'Click to rename',
93+
'Theme preset (cannot be renamed)' => 'Theme preset (cannot be renamed)',
94+
9195
// Font size options
9296
'Extra Small' => 'Extra Small',
9397
'Small' => 'Small',

src/Settings/Support/TypographyValue.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
class TypographyValue
99
{
10+
public readonly ?string $name;
11+
1012
public readonly ?FontValue $fontFamily;
1113

1214
public readonly string $fontStyle;
@@ -27,6 +29,8 @@ public function __construct(array $data, string $id)
2729
{
2830
$this->id = $id;
2931

32+
$this->name = $data['name'] ?? null;
33+
3034
$this->fontFamily = $this->transformFontFamily($data['fontFamily'] ?? null);
3135
$this->fontStyle = $data['fontStyle'] ?? 'normal';
3236
$this->fontWeight = $data['fontWeight'] ?? '400';
@@ -82,6 +86,7 @@ public function __toString(): string
8286
public function toArray(): array
8387
{
8488
return [
89+
'name' => $this->name,
8590
'fontFamily' => $this->fontFamily?->toArray(),
8691
'fontStyle' => $this->fontStyle,
8792
'fontWeight' => $this->fontWeight,

tests/Settings/Support/TypographyValueTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,45 @@
395395
expect($value->toCss()->toHtml())
396396
->toContain('[data-typography="heading1"]');
397397
});
398+
399+
it('constructs with name', function () {
400+
$data = [
401+
'name' => 'My Custom Heading',
402+
'fontSize' => 'xl',
403+
];
404+
405+
$value = new TypographyValue($data, 'heading');
406+
407+
expect($value->name)->toBe('My Custom Heading');
408+
});
409+
410+
it('handles missing name', function () {
411+
$value = new TypographyValue([], 'test');
412+
413+
expect($value->name)->toBeNull();
414+
});
415+
416+
it('includes name in toArray', function () {
417+
$data = [
418+
'name' => 'Test Preset Name',
419+
'fontSize' => 'base',
420+
];
421+
422+
$value = new TypographyValue($data, 'test');
423+
$array = $value->toArray();
424+
425+
expect($array)
426+
->toHaveKey('name')
427+
->and($array['name'])->toBe('Test Preset Name');
428+
});
429+
430+
it('includes null name in toArray when not provided', function () {
431+
$data = ['fontSize' => 'base'];
432+
433+
$value = new TypographyValue($data, 'test');
434+
$array = $value->toArray();
435+
436+
expect($array)
437+
->toHaveKey('name')
438+
->and($array['name'])->toBeNull();
439+
});

0 commit comments

Comments
 (0)