aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorskjnldsv <skjnldsv@protonmail.com>2024-11-15 11:07:37 +0100
committerskjnldsv <skjnldsv@protonmail.com>2024-12-06 10:19:42 +0100
commitcb472bebfe73f91f4f3fabc99f569083f28e00ad (patch)
treee7cafce5b76bfb334f64d1244767e1c1035f8762
parentadf8a454dd934b076bc907ff7202cd9e85b67525 (diff)
downloadnextcloud-server-cb472bebfe73f91f4f3fabc99f569083f28e00ad.tar.gz
nextcloud-server-cb472bebfe73f91f4f3fabc99f569083f28e00ad.zip
feat(systemtags): add colors in bulk tagging action
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
-rw-r--r--apps/systemtags/appinfo/info.xml2
-rw-r--r--apps/systemtags/src/components/SystemTagPicker.vue169
-rw-r--r--apps/systemtags/src/css/fileEntryInlineSystemTags.scss10
-rw-r--r--apps/systemtags/src/event-bus.d.ts4
-rw-r--r--apps/systemtags/src/files_actions/inlineSystemTagsAction.spec.ts17
-rw-r--r--apps/systemtags/src/files_actions/inlineSystemTagsAction.ts57
-rw-r--r--apps/systemtags/src/services/api.ts10
-rw-r--r--apps/systemtags/src/types.ts2
-rw-r--r--apps/systemtags/src/utils/colorUtils.ts193
-rw-r--r--package-lock.json342
-rw-r--r--package.json1
11 files changed, 487 insertions, 320 deletions
diff --git a/apps/systemtags/appinfo/info.xml b/apps/systemtags/appinfo/info.xml
index bfc33c6ff66..e2e84cce1c8 100644
--- a/apps/systemtags/appinfo/info.xml
+++ b/apps/systemtags/appinfo/info.xml
@@ -11,7 +11,7 @@
<summary>Collaborative tagging functionality which shares tags among people.</summary>
<description>Collaborative tagging functionality which shares tags among people. Great for teams.
(If you are a provider with a multi-tenancy installation, it is advised to deactivate this app as tags are shared.)</description>
- <version>1.21.0</version>
+ <version>1.21.1</version>
<licence>agpl</licence>
<author>Vincent Petry</author>
<author>Joas Schilling</author>
diff --git a/apps/systemtags/src/components/SystemTagPicker.vue b/apps/systemtags/src/components/SystemTagPicker.vue
index 8ed26ce9cb3..ada4e31643d 100644
--- a/apps/systemtags/src/components/SystemTagPicker.vue
+++ b/apps/systemtags/src/components/SystemTagPicker.vue
@@ -31,34 +31,57 @@
</div>
<!-- Tags list -->
- <div class="systemtags-picker__tags"
+ <ul class="systemtags-picker__tags"
data-cy-systemtags-picker-tags>
- <NcCheckboxRadioSwitch v-for="tag in filteredTags"
+ <li v-for="tag in filteredTags"
:key="tag.id"
- :label="tag.displayName"
- :checked="isChecked(tag)"
- :indeterminate="isIndeterminate(tag)"
- :disabled="!tag.canAssign"
:data-cy-systemtags-picker-tag="tag.id"
- class="systemtags-picker__tag"
- @update:checked="onCheckUpdate(tag, $event)">
- {{ formatTagName(tag) }}
- </NcCheckboxRadioSwitch>
- <NcButton v-if="canCreateTag"
- :disabled="status === Status.CREATING_TAG"
- alignment="start"
- class="systemtags-picker__tag-create"
- native-type="submit"
- type="tertiary"
- data-cy-systemtags-picker-button-create
- @click="onNewTag">
- {{ input.trim() }}<br>
- <span class="systemtags-picker__tag-create-subline">{{ t('systemtags', 'Create new tag') }}</span>
- <template #icon>
- <PlusIcon />
- </template>
- </NcButton>
- </div>
+ :style="tagListStyle(tag)"
+ class="systemtags-picker__tag">
+ <NcCheckboxRadioSwitch :checked="isChecked(tag)"
+ :disabled="!tag.canAssign"
+ :indeterminate="isIndeterminate(tag)"
+ :label="tag.displayName"
+ class="systemtags-picker__tag-checkbox"
+ @update:checked="onCheckUpdate(tag, $event)">
+ {{ formatTagName(tag) }}
+ </NcCheckboxRadioSwitch>
+
+ <!-- Color picker -->
+ <NcColorPicker :data-cy-systemtags-picker-tag-color="tag.id"
+ :value="`#${tag.color}`"
+ :shown.sync="openedPicker"
+ class="systemtags-picker__tag-color"
+ @update:value="onColorChange(tag, $event)"
+ @submit="openedPicker = false">
+ <NcButton :aria-label="t('systemtags', 'Change tag color')" type="tertiary">
+ <template #icon>
+ <CircleIcon v-if="tag.color" :size="24" fill-color="var(--color-circle-icon)" />
+ <CircleOutlineIcon v-else :size="24" fill-color="var(--color-circle-icon)" />
+ <PencilIcon />
+ </template>
+ </NcButton>
+ </NcColorPicker>
+ </li>
+
+ <!-- Create new tag -->
+ <li>
+ <NcButton v-if="canCreateTag"
+ :disabled="status === Status.CREATING_TAG"
+ alignment="start"
+ class="systemtags-picker__tag-create"
+ native-type="submit"
+ type="tertiary"
+ data-cy-systemtags-picker-button-create
+ @click="onNewTag">
+ {{ input.trim() }}<br>
+ <span class="systemtags-picker__tag-create-subline">{{ t('systemtags', 'Create new tag') }}</span>
+ <template #icon>
+ <PlusIcon />
+ </template>
+ </NcButton>
+ </li>
+ </ul>
<!-- Note -->
<div class="systemtags-picker__note">
@@ -110,19 +133,28 @@ import escapeHTML from 'escape-html'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcChip from '@nextcloud/vue/dist/Components/NcChip.js'
+import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
-import TagIcon from 'vue-material-design-icons/Tag.vue'
import CheckIcon from 'vue-material-design-icons/CheckCircle.vue'
+import CircleIcon from 'vue-material-design-icons/Circle.vue'
+import CircleOutlineIcon from 'vue-material-design-icons/CircleOutline.vue'
+import PencilIcon from 'vue-material-design-icons/Pencil.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue'
+import TagIcon from 'vue-material-design-icons/Tag.vue'
+import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects, updateTag } from '../services/api'
import { getNodeSystemTags, setNodeSystemTags } from '../utils'
-import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects } from '../services/api'
+import { elementColor, invertTextColor, isDarkModeEnabled } from '../utils/colorUtils'
import logger from '../services/logger'
+const mainBackgroundColor = getComputedStyle(document.body)
+ .getPropertyValue('--color-main-background')
+ .replace('#', '') || (isDarkModeEnabled() ? '000000' : 'ffffff')
+
type TagListCount = {
string: number
}
@@ -139,15 +171,19 @@ export default defineComponent({
components: {
CheckIcon,
+ CircleIcon,
+ CircleOutlineIcon,
NcButton,
NcCheckboxRadioSwitch,
// eslint-disable-next-line vue/no-unused-components
NcChip,
+ NcColorPicker,
NcDialog,
NcEmptyContent,
NcLoadingIcon,
NcNoteCard,
NcTextField,
+ PencilIcon,
PlusIcon,
TagIcon,
},
@@ -171,6 +207,7 @@ export default defineComponent({
return {
status: Status.BASE,
opened: true,
+ openedPicker: false,
input: '',
tags: [] as TagWithId[],
@@ -329,7 +366,14 @@ export default defineComponent({
// Format & sanitize a tag chip for v-html tag rendering
formatTagChip(tag: TagWithId): string {
const chip = this.$refs.chip as NcChip
- const chipHtml = chip.$el.outerHTML
+ const chipCloneEl = chip.$el.cloneNode(true) as HTMLElement
+ if (tag.color) {
+ const style = this.tagListStyle(tag)
+ Object.entries(style).forEach(([key, value]) => {
+ chipCloneEl.style.setProperty(key, value)
+ })
+ }
+ const chipHtml = chipCloneEl.outerHTML
return chipHtml.replace('%s', escapeHTML(sanitize(tag.displayName)))
},
@@ -345,6 +389,11 @@ export default defineComponent({
return tag.displayName
},
+ onColorChange(tag: TagWithId, color: string) {
+ tag.color = color.replace('#', '')
+ updateTag(tag)
+ },
+
isChecked(tag: TagWithId): boolean {
return tag.displayName in this.tagList
&& this.tagList[tag.displayName] === this.nodes.length
@@ -480,6 +529,28 @@ export default defineComponent({
showInfo(t('systemtags', 'File tags modification canceled'))
this.$emit('close', null)
},
+
+ tagListStyle(tag: TagWithId): Record<string, string> {
+ // No color, no style
+ if (!tag.color) {
+ return {
+ // See inline system tag color
+ '--color-circle-icon': 'var(--color-text-maxcontrast)',
+ }
+ }
+
+ // Make the checkbox color the same as the tag color
+ // as well as the circle icon color picker
+ const primaryElement = elementColor(`#${tag.color}`, `#${mainBackgroundColor}`)
+ const textColor = invertTextColor(primaryElement) ? '#000000' : '#ffffff'
+ return {
+ '--color-circle-icon': 'var(--color-primary-element)',
+ '--color-primary': primaryElement,
+ '--color-primary-text': textColor,
+ '--color-primary-element': primaryElement,
+ '--color-primary-element-text': textColor,
+ }
+ },
},
})
</script>
@@ -506,6 +577,48 @@ export default defineComponent({
gap: var(--default-grid-baseline);
display: flex;
flex-direction: column;
+
+ li {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+
+ // Make switch full width
+ :deep(.checkbox-radio-switch) {
+ width: 100%;
+
+ .checkbox-content {
+ // adjust width
+ max-width: none;
+ // recalculate padding
+ box-sizing: border-box;
+ min-height: calc(var(--default-grid-baseline) * 2 + var(--default-clickable-area));
+ }
+ }
+ }
+
+ .systemtags-picker__tag-color button {
+ margin-inline-start: calc(var(--default-grid-baseline) * 2);
+
+ span.pencil-icon {
+ display: none;
+ color: var(--color-main-text);
+ }
+
+ &:focus,
+ &:hover,
+ &[aria-expanded='true'] {
+ .pencil-icon {
+ display: block;
+ }
+ .circle-icon,
+ .circle-outline-icon {
+ display: none;
+ }
+ }
+ }
+
.systemtags-picker__tag-create {
:deep(span) {
text-align: start;
diff --git a/apps/systemtags/src/css/fileEntryInlineSystemTags.scss b/apps/systemtags/src/css/fileEntryInlineSystemTags.scss
index 4cf72ed429f..d6b6d3a28bd 100644
--- a/apps/systemtags/src/css/fileEntryInlineSystemTags.scss
+++ b/apps/systemtags/src/css/fileEntryInlineSystemTags.scss
@@ -22,7 +22,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- line-height: 22px; // min-size - 2 * 5px padding
+ line-height: 20px; // min-size - 2 * 5px padding - 2 * 1px border
text-align: center;
&--more {
@@ -34,6 +34,14 @@
& + .files-list__system-tag {
margin-inline-start: 5px;
}
+
+ // With color
+ &[data-systemtag-color] {
+ border-color: var(--systemtag-color);
+ color: var(--systemtag-color);
+ border-width: 2px;
+ line-height: 18px; // min-size - 2 * 5px padding - 2 * 2px border
+ }
}
@media (min-width: 512px) {
diff --git a/apps/systemtags/src/event-bus.d.ts b/apps/systemtags/src/event-bus.d.ts
index 4009f3f372b..f17be3dca53 100644
--- a/apps/systemtags/src/event-bus.d.ts
+++ b/apps/systemtags/src/event-bus.d.ts
@@ -3,10 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
+import type { TagWithId } from './types'
declare module '@nextcloud/event-bus' {
interface NextcloudEvents {
'systemtags:node:updated': Node
+ 'systemtags:tag:deleted': TagWithId
+ 'systemtags:tag:updated': TagWithId
+ 'systemtags:tag:created': TagWithId
}
}
diff --git a/apps/systemtags/src/files_actions/inlineSystemTagsAction.spec.ts b/apps/systemtags/src/files_actions/inlineSystemTagsAction.spec.ts
index 6861b5015a1..354837b91f6 100644
--- a/apps/systemtags/src/files_actions/inlineSystemTagsAction.spec.ts
+++ b/apps/systemtags/src/files_actions/inlineSystemTagsAction.spec.ts
@@ -3,10 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { action } from './inlineSystemTagsAction'
-import { describe, expect, test } from 'vitest'
-import { File, Permission, View, FileAction } from '@nextcloud/files'
+import { beforeEach, describe, expect, test, vi } from 'vitest'
import { emit, subscribe } from '@nextcloud/event-bus'
+import { File, Permission, View, FileAction } from '@nextcloud/files'
import { setNodeSystemTags } from '../utils'
+import * as serviceTagApi from '../services/api'
+import { set } from 'lodash'
const view = {
id: 'files',
@@ -53,6 +55,13 @@ describe('Inline system tags action conditions tests', () => {
})
describe('Inline system tags action render tests', () => {
+
+ beforeEach(() => {
+ vi.spyOn(serviceTagApi, 'fetchTags').mockImplementation(async () => {
+ return []
+ })
+ })
+
test('Render something even when Node does not have system tags', async () => {
const file = new File({
id: 1,
@@ -165,7 +174,9 @@ describe('Inline system tags action render tests', () => {
// Subscribe to the event
const eventPromise = new Promise((resolve) => {
- subscribe('systemtags:node:updated', resolve)
+ subscribe('systemtags:node:updated', () => {
+ setTimeout(resolve, 100)
+ })
})
// Change tags
diff --git a/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts b/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts
index afd54a4f7de..cc6cb55c632 100644
--- a/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts
+++ b/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts
@@ -3,18 +3,38 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
+import type { TagWithId } from '../types'
import { FileAction } from '@nextcloud/files'
import { subscribe } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'
import '../css/fileEntryInlineSystemTags.scss'
+import { elementColor, isDarkModeEnabled } from '../utils/colorUtils'
+import { fetchTags } from '../services/api'
import { getNodeSystemTags } from '../utils'
+import logger from '../services/logger'
+
+// Init tag cache
+const cache: TagWithId[] = []
const renderTag = function(tag: string, isMore = false): HTMLElement {
const tagElement = document.createElement('li')
tagElement.classList.add('files-list__system-tag')
+ tagElement.setAttribute('data-systemtag-name', tag)
tagElement.textContent = tag
+ // Set the color if it exists
+ const cachedTag = cache.find((t) => t.displayName === tag)
+ if (cachedTag?.color) {
+ // Make sure contrast is good and follow WCAG guidelines
+ const mainBackgroundColor = getComputedStyle(document.body)
+ .getPropertyValue('--color-main-background')
+ .replace('#', '') || (isDarkModeEnabled() ? '000000' : 'ffffff')
+ const primaryElement = elementColor(`#${cachedTag.color}`, `#${mainBackgroundColor}`)
+ tagElement.style.setProperty('--systemtag-color', primaryElement)
+ tagElement.setAttribute('data-systemtag-color', 'true')
+ }
+
if (isMore) {
tagElement.classList.add('files-list__system-tag--more')
}
@@ -35,6 +55,17 @@ const renderInline = async function(node: Node): Promise<HTMLElement> {
return systemTagsElement
}
+ // Fetch the tags if the cache is empty
+ if (cache.length === 0) {
+ try {
+ // Best would be to support attributes from webdav,
+ // but currently the library does not support it
+ cache.push(...await fetchTags())
+ } catch (error) {
+ logger.error('Failed to fetch tags', { error })
+ }
+ }
+
systemTagsElement.append(renderTag(tags[0]))
if (tags.length === 2) {
// Special case only two tags:
@@ -84,6 +115,7 @@ export const action = new FileAction({
order: 0,
})
+// Update the system tags html when the node is updated
const updateSystemTagsHtml = function(node: Node) {
renderInline(node).then((systemTagsHtml) => {
document.querySelectorAll(`[data-systemtags-fileid="${node.fileid}"]`).forEach((element) => {
@@ -92,4 +124,29 @@ const updateSystemTagsHtml = function(node: Node) {
})
}
+// Add and remove tags from the cache
+const addTag = function(tag: TagWithId) {
+ cache.push(tag)
+}
+const removeTag = function(tag: TagWithId) {
+ cache.splice(cache.findIndex((t) => t.id === tag.id), 1)
+}
+const updateTag = function(tag: TagWithId) {
+ const index = cache.findIndex((t) => t.id === tag.id)
+ if (index !== -1) {
+ cache[index] = tag
+ }
+ updateSystemTagsColorAttribute(tag)
+}
+// Update the color attribute of the system tags
+const updateSystemTagsColorAttribute = function(tag: TagWithId) {
+ document.querySelectorAll(`[data-systemtag-name="${tag.displayName}"]`).forEach((element) => {
+ (element as HTMLElement).style.setProperty('--systemtag-color', `#${tag.color}`)
+ })
+}
+
+// Subscribe to the events
subscribe('systemtags:node:updated', updateSystemTagsHtml)
+subscribe('systemtags:tag:created', addTag)
+subscribe('systemtags:tag:deleted', removeTag)
+subscribe('systemtags:tag:updated', updateTag)
diff --git a/apps/systemtags/src/services/api.ts b/apps/systemtags/src/services/api.ts
index 3262ccd3a87..ae99b2292b6 100644
--- a/apps/systemtags/src/services/api.ts
+++ b/apps/systemtags/src/services/api.ts
@@ -13,9 +13,10 @@ import { t } from '@nextcloud/l10n'
import { davClient } from './davClient.js'
import { formatTag, parseIdFromLocation, parseTags } from '../utils'
import { logger } from '../logger.js'
+import { emit } from '@nextcloud/event-bus'
export const fetchTagsPayload = `<?xml version="1.0"?>
-<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
+<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop>
<oc:id />
<oc:display-name />
@@ -23,6 +24,7 @@ export const fetchTagsPayload = `<?xml version="1.0"?>
<oc:user-assignable />
<oc:can-assign />
<d:getetag />
+ <nc:color />
</d:prop>
</d:propfind>`
@@ -81,6 +83,7 @@ export const createTag = async (tag: Tag | ServerTag): Promise<number> => {
})
const contentLocation = headers.get('content-location')
if (contentLocation) {
+ emit('systemtags:tag:created', tag)
return parseIdFromLocation(contentLocation)
}
logger.error(t('systemtags', 'Missing "Content-Location" header'))
@@ -98,12 +101,13 @@ export const createTag = async (tag: Tag | ServerTag): Promise<number> => {
export const updateTag = async (tag: TagWithId): Promise<void> => {
const path = '/systemtags/' + tag.id
const data = `<?xml version="1.0"?>
- <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
+ <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:set>
<d:prop>
<oc:display-name>${tag.displayName}</oc:display-name>
<oc:user-visible>${tag.userVisible}</oc:user-visible>
<oc:user-assignable>${tag.userAssignable}</oc:user-assignable>
+ <nc:color>${tag.color}</nc:color>
</d:prop>
</d:set>
</d:propertyupdate>`
@@ -113,6 +117,7 @@ export const updateTag = async (tag: TagWithId): Promise<void> => {
method: 'PROPPATCH',
data,
})
+ emit('systemtags:tag:updated', tag)
} catch (error) {
logger.error(t('systemtags', 'Failed to update tag'), { error })
throw new Error(t('systemtags', 'Failed to update tag'))
@@ -123,6 +128,7 @@ export const deleteTag = async (tag: TagWithId): Promise<void> => {
const path = '/systemtags/' + tag.id
try {
await davClient.deleteFile(path)
+ emit('systemtags:tag:deleted', tag)
} catch (error) {
logger.error(t('systemtags', 'Failed to delete tag'), { error })
throw new Error(t('systemtags', 'Failed to delete tag'))
diff --git a/apps/systemtags/src/types.ts b/apps/systemtags/src/types.ts
index 161e4d74247..6e4f03227e0 100644
--- a/apps/systemtags/src/types.ts
+++ b/apps/systemtags/src/types.ts
@@ -8,6 +8,8 @@ export interface BaseTag {
userVisible: boolean
userAssignable: boolean
readonly canAssign: boolean // Computed server-side
+ etag?: string
+ color?: string
}
export type Tag = BaseTag & {
diff --git a/apps/systemtags/src/utils/colorUtils.ts b/apps/systemtags/src/utils/colorUtils.ts
new file mode 100644
index 00000000000..9bc0ca8c19d
--- /dev/null
+++ b/apps/systemtags/src/utils/colorUtils.ts
@@ -0,0 +1,193 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import Color from 'color'
+
+type hexColor = `#${string & (
+ `${string}${string}${string}` |
+ `${string}${string}${string}${string}${string}${string}`
+)}`;
+
+/**
+ * Is the current theme dark?
+ */
+export function isDarkModeEnabled() {
+ const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches
+ const darkModeSetting = document.body.getAttribute('data-themes')?.includes('dark')
+ return darkModeSetting || darkModePreference || false
+}
+
+/**
+ * Is the current theme high contrast?
+ */
+export function isHighContrastModeEnabled() {
+ const highContrastPreference = window.matchMedia('(forced-colors: active)').matches
+ const highContrastSetting = document.body.getAttribute('data-themes')?.includes('highcontrast')
+ return highContrastSetting || highContrastPreference || false
+}
+
+/**
+ * Should we invert the text on this background color?
+ * @param color RGB color value as a hex string
+ * @return boolean
+ */
+export function invertTextColor(color: hexColor): boolean {
+ return colorContrast(color, '#ffffff') < 4.5
+}
+
+/**
+ * Is this color too bright?
+ * @param color RGB color value as a hex string
+ * @return boolean
+ */
+export function isBrightColor(color: hexColor): boolean {
+ return calculateLuma(color) > 0.6
+}
+
+/**
+ * Get color for on-page elements
+ * theme color by default, grey if theme color is too bright.
+ * @param color the color to contrast against, e.g. #ffffff
+ * @param backgroundColor the background color to contrast against, e.g. #000000
+ */
+export function elementColor(
+ color: hexColor,
+ backgroundColor: hexColor,
+): hexColor {
+ const brightBackground = isBrightColor(backgroundColor)
+ const blurredBackground = mix(
+ backgroundColor,
+ brightBackground ? color : '#ffffff',
+ 66,
+ )
+
+ let contrast = colorContrast(color, blurredBackground)
+ const minContrast = isHighContrastModeEnabled() ? 5.6 : 3.2
+
+ let iteration = 0
+ let result = color
+ const epsilon = 1.0 / 255.0
+ while (contrast < minContrast && iteration++ < 100) {
+ const hsl = hexToHSL(result)
+ const l = Math.max(
+ 0,
+ Math.min(255, hsl.l + (brightBackground ? -epsilon : epsilon)),
+ )
+ result = hslToHex({ h: hsl.h, s: hsl.s, l })
+ contrast = colorContrast(result, blurredBackground)
+ }
+
+ return result
+}
+
+/**
+ * Get color for on-page text:
+ * black if background is bright, white if background is dark.
+ * @param color1 the color to contrast against, e.g. #ffffff
+ * @param color2 the background color to contrast against, e.g. #000000
+ * @param factor the factor to mix the colors between -100 and 100, e.g. 66
+ */
+export function mix(color1: hexColor, color2: hexColor, factor: number): hexColor {
+ if (factor < -100 || factor > 100) {
+ throw new RangeError('Factor must be between -100 and 100')
+ }
+ return new Color(color2).mix(new Color(color1), (factor + 100) / 200).hex()
+}
+
+/**
+ * Lighten a color by a factor
+ * @param color the color to lighten, e.g. #000000
+ * @param factor the factor to lighten the color by between -100 and 100, e.g. -41
+ */
+export function lighten(color: hexColor, factor: number): hexColor {
+ if (factor < -100 || factor > 100) {
+ throw new RangeError('Factor must be between -100 and 100')
+ }
+ return new Color(color).lighten((factor + 100) / 200).hex()
+}
+
+/**
+ * Darken a color by a factor
+ * @param color the color to darken, e.g. #ffffff
+ * @param factor the factor to darken the color by between -100 and 100, e.g. 32
+ */
+export function darken(color: hexColor, factor: number): hexColor {
+ if (factor < -100 || factor > 100) {
+ throw new RangeError('Factor must be between -100 and 100')
+ }
+ return new Color(color).darken((factor + 100) / 200).hex()
+}
+
+/**
+ * Calculate the luminance of a color
+ * @param color the color to calculate the luminance of, e.g. #ffffff
+ */
+export function calculateLuminance(color: hexColor): number {
+ return hexToHSL(color).l
+}
+
+/**
+ * Calculate the luma of a color
+ * @param color the color to calculate the luma of, e.g. #ffffff
+ */
+export function calculateLuma(color: hexColor): number {
+ const rgb = hexToRGB(color).map((value) => {
+ value /= 255
+ return value <= 0.03928
+ ? value / 12.92
+ : Math.pow((value + 0.055) / 1.055, 2.4)
+ })
+ const [red, green, blue] = rgb
+ return 0.2126 * red + 0.7152 * green + 0.0722 * blue
+}
+
+/**
+ * Calculate the contrast between two colors
+ * @param color1 the first color to calculate the contrast of, e.g. #ffffff
+ * @param color2 the second color to calculate the contrast of, e.g. #000000
+ */
+export function colorContrast(color1: hexColor, color2: hexColor): number {
+ const luminance1 = calculateLuma(color1) + 0.05
+ const luminance2 = calculateLuma(color2) + 0.05
+ return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2)
+}
+
+/**
+ * Convert hex color to RGB
+ * @param color RGB color value as a hex string
+ */
+export function hexToRGB(color: hexColor): [number, number, number] {
+ return new Color(color).rgb().array()
+}
+
+/**
+ * Convert RGB color to hex
+ * @param color RGB color value as a hex string
+ */
+export function hexToHSL(color: hexColor): { h: number; s: number; l: number } {
+ const hsl = new Color(color).hsl()
+ return { h: hsl.color[0], s: hsl.color[1], l: hsl.color[2] }
+}
+
+/**
+ * Convert HSL color to hex
+ * @param hsl HSL color value as an object
+ * @param hsl.h hue
+ * @param hsl.s saturation
+ * @param hsl.l lightness
+ */
+export function hslToHex(hsl: { h: number; s: number; l: number }): hexColor {
+ return new Color(hsl).hex()
+}
+
+/**
+ * Convert RGB color to hex
+ * @param r red
+ * @param g green
+ * @param b blue
+ */
+export function rgbToHex(r: number, g: number, b: number): hexColor {
+ const hex = ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1)
+ return `#${hex}`
+}
diff --git a/package-lock.json b/package-lock.json
index 3c80c781711..aef3aab8322 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,6 +42,7 @@
"camelcase": "^8.0.0",
"cancelable-promise": "^4.3.1",
"clipboard": "^2.0.11",
+ "color": "^4.2.3",
"core-js": "^3.38.1",
"davclient.js": "github:owncloud/davclient.js.git#0.2.2",
"debounce": "^2.1.0",
@@ -5378,26 +5379,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/@testing-library/dom/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@testing-library/dom/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@testing-library/dom/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -5471,26 +5452,6 @@
"node": ">=8"
}
},
- "node_modules/@testing-library/jest-dom/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@testing-library/jest-dom/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
@@ -5617,26 +5578,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/@testing-library/vue/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@testing-library/vue/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@testing-library/vue/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -9179,6 +9120,47 @@
"node": ">=6"
}
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
@@ -10141,26 +10123,6 @@
"node": ">=8"
}
},
- "node_modules/cypress/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/cypress/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/cypress/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -12444,28 +12406,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/eslint/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/eslint/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT",
- "peer": true
- },
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -16125,24 +16065,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/jake/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/jake/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
- },
"node_modules/jake/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -17173,26 +17095,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/log-symbols/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/log-symbols/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/log-symbols/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -17251,26 +17153,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/log-update/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/log-update/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/log-update/node_modules/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
@@ -20468,13 +20350,6 @@
"postcss": "^8.2.9"
}
},
- "node_modules/postcss-values-parser/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/precinct": {
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/precinct/-/precinct-12.1.2.tgz",
@@ -20915,24 +20790,6 @@
"wrap-ansi": "^6.2.0"
}
},
- "node_modules/qrcode/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/qrcode/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "license": "MIT"
- },
"node_modules/qrcode/node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
@@ -22765,6 +22622,21 @@
"integrity": "sha512-+OmPgi01yHK/bRNQDoehUcV8fqs9nNJkG2DoWCnnLvj0lmowab7BH3v9776BG0y7dGEOLh0F7mfd37k+ht26Yw==",
"license": "MIT"
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/simple-swizzle/node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+ "license": "MIT"
+ },
"node_modules/sinon": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-5.0.7.tgz",
@@ -22823,26 +22695,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/slice-ansi/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/slice-ansi/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@@ -24250,26 +24102,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/table/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/table/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/table/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -24979,26 +24811,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/ts-loader/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/ts-loader/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/ts-loader/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -27496,26 +27308,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/wrap-ansi-cjs/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -27532,26 +27324,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/wrap-ansi/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/wrap-ansi/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/package.json b/package.json
index a585edb5a7b..d58d2dfa5c5 100644
--- a/package.json
+++ b/package.json
@@ -73,6 +73,7 @@
"camelcase": "^8.0.0",
"cancelable-promise": "^4.3.1",
"clipboard": "^2.0.11",
+ "color": "^4.2.3",
"core-js": "^3.38.1",
"davclient.js": "github:owncloud/davclient.js.git#0.2.2",
"debounce": "^2.1.0",