Skip to content

Commit 26ef3dd

Browse files
committed
feat: manage categories button/modal
1 parent a4b93cb commit 26ef3dd

9 files changed

Lines changed: 509 additions & 640 deletions

File tree

eslint.config.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module.exports = [
1212
...tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended),
1313
{
1414
rules: {
15-
'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
15+
'no-unused-vars': 'off',
1616
'@typescript-eslint/no-unused-vars': [
1717
'warn',
1818
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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

Comments
 (0)