|
| 1 | +<template> |
| 2 | + <NcDialog |
| 3 | + :name="dialogName" |
| 4 | + :open="open" |
| 5 | + close-on-click-outside |
| 6 | + @update:open="$emit('update:open', $event)" |
| 7 | + > |
| 8 | + <form class="pantry-cat-form" autocomplete="off" @submit.prevent="submit"> |
| 9 | + <NcTextField |
| 10 | + v-model="nameValue" |
| 11 | + :label="strings.nameLabel" |
| 12 | + :placeholder="strings.namePlaceholder" |
| 13 | + autocomplete="off" |
| 14 | + /> |
| 15 | + <div> |
| 16 | + <label class="pantry-cat-form__sub">{{ strings.iconLabel }}</label> |
| 17 | + <div class="pantry-cat-form__icon-grid"> |
| 18 | + <button |
| 19 | + v-for="opt in CATEGORY_ICONS" |
| 20 | + :key="opt.key" |
| 21 | + type="button" |
| 22 | + class="pantry-cat-form__icon-button" |
| 23 | + :class="{ 'pantry-cat-form__icon-button--active': iconValue === opt.key }" |
| 24 | + :title="opt.label" |
| 25 | + :style="{ color: colorValue }" |
| 26 | + @click="iconValue = opt.key" |
| 27 | + > |
| 28 | + <component :is="opt.component" :size="20" /> |
| 29 | + </button> |
| 30 | + </div> |
| 31 | + </div> |
| 32 | + <div> |
| 33 | + <label class="pantry-cat-form__sub">{{ strings.colorLabel }}</label> |
| 34 | + <div class="pantry-cat-form__color-grid"> |
| 35 | + <button |
| 36 | + v-for="c in CATEGORY_COLORS" |
| 37 | + :key="c" |
| 38 | + type="button" |
| 39 | + class="pantry-cat-form__color-swatch" |
| 40 | + :class="{ 'pantry-cat-form__color-swatch--active': colorValue === c }" |
| 41 | + :style="{ backgroundColor: c }" |
| 42 | + :aria-label="c" |
| 43 | + @click="colorValue = c" |
| 44 | + /> |
| 45 | + </div> |
| 46 | + </div> |
| 47 | + <p v-if="error" class="pantry-cat-form__error">{{ error }}</p> |
| 48 | + </form> |
| 49 | + <template #actions> |
| 50 | + <NcButton @click="$emit('update:open', false)">{{ strings.cancel }}</NcButton> |
| 51 | + <NcButton variant="primary" :disabled="saving || !nameValue.trim()" @click="submit"> |
| 52 | + {{ saving ? strings.saving : category ? strings.save : strings.create }} |
| 53 | + </NcButton> |
| 54 | + </template> |
| 55 | + </NcDialog> |
| 56 | +</template> |
| 57 | + |
| 58 | +<script setup lang="ts"> |
| 59 | +import { ref, watch, computed } from 'vue' |
| 60 | +import { t } from '@nextcloud/l10n' |
| 61 | +import NcButton from '@nextcloud/vue/components/NcButton' |
| 62 | +import NcDialog from '@nextcloud/vue/components/NcDialog' |
| 63 | +import NcTextField from '@nextcloud/vue/components/NcTextField' |
| 64 | +import type { Category } from '@/api/types' |
| 65 | +import { |
| 66 | + CATEGORY_COLORS, |
| 67 | + CATEGORY_ICONS, |
| 68 | + DEFAULT_CATEGORY_ICON_KEY, |
| 69 | +} from '@/components/CategoryPicker/categoryIcons' |
| 70 | +
|
| 71 | +const props = defineProps<{ |
| 72 | + open: boolean |
| 73 | + /** Existing category to edit, or null/undefined to create a new one. */ |
| 74 | + category?: Category | null |
| 75 | + saving?: boolean |
| 76 | + error?: string | null |
| 77 | +}>() |
| 78 | +
|
| 79 | +const emit = defineEmits<{ |
| 80 | + 'update:open': [value: boolean] |
| 81 | + save: [data: { name: string; icon: string; color: string }] |
| 82 | +}>() |
| 83 | +
|
| 84 | +const nameValue = ref('') |
| 85 | +const iconValue = ref<string>(DEFAULT_CATEGORY_ICON_KEY) |
| 86 | +const colorValue = ref<string>(CATEGORY_COLORS[3]!) |
| 87 | +
|
| 88 | +watch( |
| 89 | + () => props.open, |
| 90 | + (isOpen) => { |
| 91 | + if (isOpen) { |
| 92 | + if (props.category) { |
| 93 | + nameValue.value = props.category.name |
| 94 | + iconValue.value = props.category.icon |
| 95 | + colorValue.value = props.category.color |
| 96 | + } else { |
| 97 | + nameValue.value = '' |
| 98 | + iconValue.value = DEFAULT_CATEGORY_ICON_KEY |
| 99 | + colorValue.value = CATEGORY_COLORS[3]! |
| 100 | + } |
| 101 | + } |
| 102 | + }, |
| 103 | + { immediate: true }, |
| 104 | +) |
| 105 | +
|
| 106 | +const dialogName = computed(() => (props.category ? strings.editTitle : strings.createTitle)) |
| 107 | +
|
| 108 | +function submit() { |
| 109 | + const name = nameValue.value.trim() |
| 110 | + if (!name) return |
| 111 | + emit('save', { name, icon: iconValue.value, color: colorValue.value }) |
| 112 | +} |
| 113 | +
|
| 114 | +const strings = { |
| 115 | + createTitle: t('pantry', 'New category'), |
| 116 | + editTitle: t('pantry', 'Edit category'), |
| 117 | + nameLabel: t('pantry', 'Name'), |
| 118 | + namePlaceholder: t('pantry', 'e.g. Produce, Dairy'), |
| 119 | + iconLabel: t('pantry', 'Icon:'), |
| 120 | + colorLabel: t('pantry', 'Color:'), |
| 121 | + cancel: t('pantry', 'Cancel'), |
| 122 | + create: t('pantry', 'Create'), |
| 123 | + save: t('pantry', 'Save'), |
| 124 | + saving: t('pantry', 'Saving …'), |
| 125 | +} |
| 126 | +</script> |
| 127 | + |
| 128 | +<style scoped lang="scss"> |
| 129 | +.pantry-cat-form { |
| 130 | + display: flex; |
| 131 | + flex-direction: column; |
| 132 | + gap: 1rem; |
| 133 | + padding: 0.5rem 0; |
| 134 | + min-width: 340px; |
| 135 | +
|
| 136 | + &__sub { |
| 137 | + display: block; |
| 138 | + font-size: 0.85rem; |
| 139 | + font-weight: 600; |
| 140 | + color: var(--color-text-maxcontrast); |
| 141 | + margin-bottom: 0.35rem; |
| 142 | + } |
| 143 | +
|
| 144 | + &__icon-grid { |
| 145 | + display: grid; |
| 146 | + grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); |
| 147 | + gap: 0.35rem; |
| 148 | + } |
| 149 | +
|
| 150 | + &__icon-button { |
| 151 | + aspect-ratio: 1; |
| 152 | + display: flex; |
| 153 | + align-items: center; |
| 154 | + justify-content: center; |
| 155 | + border: 1px solid var(--color-border); |
| 156 | + border-radius: var(--border-radius, 8px); |
| 157 | + background: var(--color-main-background); |
| 158 | + cursor: pointer; |
| 159 | + transition: all 0.15s ease; |
| 160 | +
|
| 161 | + &:hover { |
| 162 | + background: var(--color-background-hover); |
| 163 | + } |
| 164 | +
|
| 165 | + &--active { |
| 166 | + border-color: currentColor; |
| 167 | + box-shadow: 0 0 0 2px currentColor; |
| 168 | + } |
| 169 | + } |
| 170 | +
|
| 171 | + &__color-grid { |
| 172 | + display: flex; |
| 173 | + flex-wrap: wrap; |
| 174 | + gap: 0.35rem; |
| 175 | + } |
| 176 | +
|
| 177 | + &__color-swatch { |
| 178 | + width: 28px; |
| 179 | + height: 28px; |
| 180 | + border-radius: 999px; |
| 181 | + border: 2px solid transparent; |
| 182 | + cursor: pointer; |
| 183 | + transition: transform 0.15s ease; |
| 184 | +
|
| 185 | + &:hover { |
| 186 | + transform: scale(1.08); |
| 187 | + } |
| 188 | +
|
| 189 | + &--active { |
| 190 | + border-color: var(--color-main-text); |
| 191 | + transform: scale(1.1); |
| 192 | + } |
| 193 | + } |
| 194 | +
|
| 195 | + &__error { |
| 196 | + color: var(--color-error); |
| 197 | + margin: 0; |
| 198 | + } |
| 199 | +} |
| 200 | +
|
| 201 | +@media (max-width: 500px) { |
| 202 | + .pantry-cat-form { |
| 203 | + min-width: 0; |
| 204 | + } |
| 205 | +} |
| 206 | +</style> |
0 commit comments