mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 21:33:02 +01:00
Custom Colored Labels (#801)
* feat: custom coloured labels * chore: lint * feat: add ColorSelector component for improved color selection in labels and integrate it into CreateModal and Selector components * style: lint * fix: update ColorSelector and Selector components to use empty string instead of null for default color values for types
This commit is contained in:
@@ -3578,6 +3578,9 @@ const docTemplate = `{
|
|||||||
"repo.LabelOut": {
|
"repo.LabelOut": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -3598,6 +3601,9 @@ const docTemplate = `{
|
|||||||
"repo.LabelSummary": {
|
"repo.LabelSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3576,6 +3576,9 @@
|
|||||||
"repo.LabelOut": {
|
"repo.LabelOut": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -3596,6 +3599,9 @@
|
|||||||
"repo.LabelSummary": {
|
"repo.LabelSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -997,6 +997,8 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
repo.LabelOut:
|
repo.LabelOut:
|
||||||
properties:
|
properties:
|
||||||
|
color:
|
||||||
|
type: string
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
@@ -1010,6 +1012,8 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
repo.LabelSummary:
|
repo.LabelSummary:
|
||||||
properties:
|
properties:
|
||||||
|
color:
|
||||||
|
type: string
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type (
|
|||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Color string `json:"color"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,7 @@ func mapLabelSummary(label *ent.Label) LabelSummary {
|
|||||||
ID: label.ID,
|
ID: label.ID,
|
||||||
Name: label.Name,
|
Name: label.Name,
|
||||||
Description: label.Description,
|
Description: label.Description,
|
||||||
|
Color: label.Color,
|
||||||
CreatedAt: label.CreatedAt,
|
CreatedAt: label.CreatedAt,
|
||||||
UpdatedAt: label.UpdatedAt,
|
UpdatedAt: label.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3576,6 +3576,9 @@
|
|||||||
"repo.LabelOut": {
|
"repo.LabelOut": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -3596,6 +3599,9 @@
|
|||||||
"repo.LabelSummary": {
|
"repo.LabelSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -997,6 +997,8 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
repo.LabelOut:
|
repo.LabelOut:
|
||||||
properties:
|
properties:
|
||||||
|
color:
|
||||||
|
type: string
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
@@ -1010,6 +1012,8 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
repo.LabelSummary:
|
repo.LabelSummary:
|
||||||
properties:
|
properties:
|
||||||
|
color:
|
||||||
|
type: string
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
|
|||||||
155
frontend/components/Form/ColorSelector.vue
Normal file
155
frontend/components/Form/ColorSelector.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { Label } from "~/components/ui/label";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import MdiClose from "~icons/mdi/close";
|
||||||
|
import MdiDiceMultiple from "~icons/mdi/dice-multiple";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
inline: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showHex: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 24,
|
||||||
|
},
|
||||||
|
startingColor: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
|
const swatchStyle = computed(() => ({
|
||||||
|
backgroundColor: props.modelValue || "hsl(var(--muted))",
|
||||||
|
width: typeof props.size === "number" ? `${props.size}px` : props.size,
|
||||||
|
height: typeof props.size === "number" ? `${props.size}px` : props.size,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const value = useVModel(props, "modelValue", emits);
|
||||||
|
|
||||||
|
// Initialize with starting color if provided and current value is empty
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.startingColor && (!value.value || value.value === "")) {
|
||||||
|
value.value = props.startingColor;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function clearColor() {
|
||||||
|
value.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomizeColor() {
|
||||||
|
const randomColor =
|
||||||
|
"#" +
|
||||||
|
Math.floor(Math.random() * 16777215)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(6, "0");
|
||||||
|
value.value = randomColor;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="!inline" class="flex w-full flex-col gap-1.5">
|
||||||
|
<Label :for="id" class="flex w-full px-1">
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</Label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
:style="swatchStyle"
|
||||||
|
class="inline-block cursor-pointer rounded-full border ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||||
|
:aria-label="`${t('components.color_selector.color')}: ${modelValue || t('components.color_selector.no_color_selected')}`"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="$refs.colorInput.click()"
|
||||||
|
/>
|
||||||
|
<span v-if="showHex" class="font-mono text-xs text-muted-foreground">{{
|
||||||
|
modelValue || t("components.color_selector.no_color")
|
||||||
|
}}</span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="size-6 p-0"
|
||||||
|
:aria-label="t('components.color_selector.randomize')"
|
||||||
|
@click="randomizeColor"
|
||||||
|
>
|
||||||
|
<MdiDiceMultiple class="size-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="size-6 p-0"
|
||||||
|
:aria-label="t('components.color_selector.clear')"
|
||||||
|
@click="clearColor"
|
||||||
|
>
|
||||||
|
<MdiClose class="size-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<input :id="id" ref="colorInput" v-model="value" type="color" class="sr-only" tabindex="-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
|
||||||
|
<Label class="flex w-full px-1 py-2" :for="id">
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</Label>
|
||||||
|
<div class="col-span-3 mt-2 flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
:style="swatchStyle"
|
||||||
|
class="inline-block cursor-pointer rounded-full border ring-offset-background focus:outline-none focus:outline-primary focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||||
|
:aria-label="`${t('components.color_selector.color')}: ${modelValue || t('components.color_selector.no_color_selected')}`"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="$refs.colorInput.click()"
|
||||||
|
/>
|
||||||
|
<span v-if="showHex" class="font-mono text-xs text-muted-foreground">{{
|
||||||
|
modelValue || t("components.color_selector.no_color")
|
||||||
|
}}</span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="size-6 p-0"
|
||||||
|
:aria-label="t('components.color_selector.randomize')"
|
||||||
|
@click="randomizeColor"
|
||||||
|
>
|
||||||
|
<MdiDiceMultiple class="size-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="size-6 p-0"
|
||||||
|
:aria-label="t('components.color_selector.clear')"
|
||||||
|
@click="clearColor"
|
||||||
|
>
|
||||||
|
<MdiClose class="size-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<input :id="id" ref="colorInput" v-model="value" type="color" class="sr-only" tabindex="-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { LabelOut, LabelSummary } from "~~/lib/api/types/data-contracts";
|
import type { LabelOut, LabelSummary } from "~~/lib/api/types/data-contracts";
|
||||||
import MdiArrowUp from "~icons/mdi/arrow-up";
|
import MdiArrowUp from "~icons/mdi/arrow-up";
|
||||||
import MdiTagOutline from "~icons/mdi/tag-outline";
|
import MdiTagOutline from "~icons/mdi/tag-outline";
|
||||||
|
import { getContrastTextColor } from "~/lib/utils";
|
||||||
|
|
||||||
export type sizes = "sm" | "md" | "lg" | "xl";
|
export type sizes = "sm" | "md" | "lg" | "xl";
|
||||||
defineProps({
|
defineProps({
|
||||||
@@ -18,12 +19,17 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
class="group/label-chip flex gap-2 rounded-full bg-accent text-accent-foreground shadow transition duration-300 hover:bg-accent/50"
|
class="group/label-chip flex gap-2 rounded-full shadow transition duration-300 hover:bg-accent/50"
|
||||||
:class="{
|
:class="{
|
||||||
'p-4 py-1 text-base': size === 'lg',
|
'p-4 py-1 text-base': size === 'lg',
|
||||||
'p-3 py-1 text-sm': size !== 'sm' && size !== 'lg',
|
'p-3 py-1 text-sm': size !== 'sm' && size !== 'lg',
|
||||||
'p-2 py-0.5 text-xs': size === 'sm',
|
'p-2 py-0.5 text-xs': size === 'sm',
|
||||||
}"
|
}"
|
||||||
|
:style="
|
||||||
|
label.color
|
||||||
|
? { backgroundColor: label.color, color: getContrastTextColor(label.color) }
|
||||||
|
: { backgroundColor: 'hsl(var(--accent))' }
|
||||||
|
"
|
||||||
:to="`/label/${label.id}`"
|
:to="`/label/${label.id}`"
|
||||||
>
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
:label="$t('components.label.create_modal.label_description')"
|
:label="$t('components.label.create_modal.label_description')"
|
||||||
:max-length="255"
|
:max-length="255"
|
||||||
/>
|
/>
|
||||||
|
<ColorSelector v-model="form.color" :label="$t('components.label.create_modal.label_color')" :show-hex="true" />
|
||||||
<div class="mt-4 flex flex-row-reverse">
|
<div class="mt-4 flex flex-row-reverse">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button :disabled="loading" type="submit">{{ $t("global.create") }}</Button>
|
<Button :disabled="loading" type="submit">{{ $t("global.create") }}</Button>
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
import { toast } from "@/components/ui/sonner";
|
import { toast } from "@/components/ui/sonner";
|
||||||
import BaseModal from "@/components/App/CreateModal.vue";
|
import BaseModal from "@/components/App/CreateModal.vue";
|
||||||
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
|
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
|
||||||
|
import ColorSelector from "@/components/Form/ColorSelector.vue";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,11 @@
|
|||||||
>
|
>
|
||||||
<div class="flex flex-wrap items-center gap-2 px-3">
|
<div class="flex flex-wrap items-center gap-2 px-3">
|
||||||
<TagsInputItem v-for="item in modelValue" :key="item" :value="item">
|
<TagsInputItem v-for="item in modelValue" :key="item" :value="item">
|
||||||
|
<span
|
||||||
|
v-if="shortenedLabels.find(l => l.id === item)?.color"
|
||||||
|
class="ml-2 inline-block size-4 rounded-full"
|
||||||
|
:style="{ backgroundColor: shortenedLabels.find(l => l.id === item)?.color }"
|
||||||
|
/>
|
||||||
<TagsInputItemText />
|
<TagsInputItemText />
|
||||||
<TagsInputItemDelete />
|
<TagsInputItemDelete />
|
||||||
</TagsInputItem>
|
</TagsInputItem>
|
||||||
@@ -55,6 +60,11 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
class="mr-2 inline-block size-4 rounded-full align-middle"
|
||||||
|
:class="{ border: shortenedLabels.find(l => l.id === label.value)?.color }"
|
||||||
|
:style="{ backgroundColor: shortenedLabels.find(l => l.id === label.value)?.color }"
|
||||||
|
/>
|
||||||
{{ label.label }}
|
{{ label.label }}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
@@ -127,14 +137,14 @@
|
|||||||
return filtered;
|
return filtered;
|
||||||
});
|
});
|
||||||
|
|
||||||
const createAndAdd = async (name: string) => {
|
const createAndAdd = async (name: string, color = "") => {
|
||||||
if (name.length > 50) {
|
if (name.length > 50) {
|
||||||
toast.error(t("components.label.create_modal.toast.label_name_too_long"));
|
toast.error(t("components.label.create_modal.toast.label_name_too_long"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { error, data } = await api.labels.create({
|
const { error, data } = await api.labels.create({
|
||||||
name,
|
name,
|
||||||
color: "", // Future!
|
color,
|
||||||
description: "",
|
description: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -638,6 +638,7 @@ export interface LabelCreate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LabelOut {
|
export interface LabelOut {
|
||||||
|
color: string;
|
||||||
createdAt: Date | string;
|
createdAt: Date | string;
|
||||||
description: string;
|
description: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -646,6 +647,7 @@ export interface LabelOut {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LabelSummary {
|
export interface LabelSummary {
|
||||||
|
color: string;
|
||||||
createdAt: Date | string;
|
createdAt: Date | string;
|
||||||
description: string;
|
description: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -4,3 +4,35 @@ import { twMerge } from "tailwind-merge";
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns either '#000' or '#fff' depending on which has better contrast with the given background color.
|
||||||
|
* Accepts hex (#RRGGBB or #RGB) or rgb(a) strings.
|
||||||
|
*/
|
||||||
|
export function getContrastTextColor(bgColor: string): string {
|
||||||
|
let r = 0;
|
||||||
|
let g = 0;
|
||||||
|
let b = 0;
|
||||||
|
if (bgColor.startsWith("#")) {
|
||||||
|
let hex = bgColor.slice(1);
|
||||||
|
if (hex.length === 3) {
|
||||||
|
hex = hex
|
||||||
|
.split("")
|
||||||
|
.map(x => x + x)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
r = parseInt(hex.slice(0, 2), 16);
|
||||||
|
g = parseInt(hex.slice(2, 4), 16);
|
||||||
|
b = parseInt(hex.slice(4, 6), 16);
|
||||||
|
} else if (bgColor.startsWith("rgb")) {
|
||||||
|
const match = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
r = parseInt(match[1]);
|
||||||
|
g = parseInt(match[2]);
|
||||||
|
b = parseInt(match[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Calculate luminance
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||||
|
return luminance > 0.5 ? "#000" : "#fff";
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,13 @@
|
|||||||
"new_version_available_link": "Click here to view the release notes"
|
"new_version_available_link": "Click here to view the release notes"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"color_selector": {
|
||||||
|
"color": "Color",
|
||||||
|
"clear": "Clear color",
|
||||||
|
"no_color": "No color",
|
||||||
|
"no_color_selected": "No color selected",
|
||||||
|
"randomize": "Randomize color"
|
||||||
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"password": {
|
"password": {
|
||||||
"toggle_show": "Toggle Password Show"
|
"toggle_show": "Toggle Password Show"
|
||||||
@@ -144,7 +151,8 @@
|
|||||||
"create_failed": "Couldn't create label",
|
"create_failed": "Couldn't create label",
|
||||||
"create_success": "Label created",
|
"create_success": "Label created",
|
||||||
"label_name_too_long": "Label name must not be longer than 50 characters"
|
"label_name_too_long": "Label name must not be longer than 50 characters"
|
||||||
}
|
},
|
||||||
|
"label_color": "Label Color"
|
||||||
},
|
},
|
||||||
"selector": {
|
"selector": {
|
||||||
"select_labels": "Select Labels"
|
"select_labels": "Select Labels"
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import ColorSelector from "@/components/Form/ColorSelector.vue";
|
||||||
|
import { getContrastTextColor } from "~/lib/utils";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["auth"],
|
middleware: ["auth"],
|
||||||
@@ -128,7 +130,12 @@
|
|||||||
:label="$t('components.label.create_modal.label_description')"
|
:label="$t('components.label.create_modal.label_description')"
|
||||||
:max-length="255"
|
:max-length="255"
|
||||||
/>
|
/>
|
||||||
<!-- TODO: color -->
|
<ColorSelector
|
||||||
|
v-model="updateData.color"
|
||||||
|
:label="$t('components.label.create_modal.label_color')"
|
||||||
|
:show-hex="true"
|
||||||
|
:starting-color="label.color"
|
||||||
|
/>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit" :loading="updating"> {{ $t("global.update") }} </Button>
|
<Button type="submit" :loading="updating"> {{ $t("global.update") }} </Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -144,7 +151,12 @@
|
|||||||
<header :class="{ 'mb-2': label.description }">
|
<header :class="{ 'mb-2': label.description }">
|
||||||
<div class="flex flex-wrap items-end gap-2">
|
<div class="flex flex-wrap items-end gap-2">
|
||||||
<div
|
<div
|
||||||
class="mb-auto flex size-12 items-center justify-center rounded-full bg-secondary text-secondary-foreground"
|
class="mb-auto flex size-12 items-center justify-center rounded-full"
|
||||||
|
:style="
|
||||||
|
label.color
|
||||||
|
? { backgroundColor: label.color, color: getContrastTextColor(label.color) }
|
||||||
|
: { backgroundColor: 'hsl(var(--secondary))', color: 'hsl(var(--secondary-foreground))' }
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<MdiPackageVariant class="size-7" />
|
<MdiPackageVariant class="size-7" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user