From 13ff72e5e3ced3810f754ff2a02c50d345053f5f Mon Sep 17 00:00:00 2001
From: Mark Janiczak
Date: Mon, 14 Jul 2025 12:52:35 +1000
Subject: [PATCH 1/9] feat: add color swatches
---
docs/.vitepress/config.ts | 10 ++
.../components/demo/ColorSwatch/css/index.vue | 11 ++
.../demo/ColorSwatch/css/styles.css | 11 ++
.../demo/ColorSwatch/tailwind/index.vue | 10 ++
.../ColorSwatch/tailwind/tailwind.config.js | 6 +
.../demo/ColorSwatchPicker/css/index.vue | 31 ++++
.../demo/ColorSwatchPicker/css/styles.css | 32 +++++
.../demo/ColorSwatchPicker/tailwind/index.vue | 30 ++++
.../tailwind/tailwind.config.js | 6 +
.../docs/components/color-swatch-picker.md | 90 ++++++++++++
docs/content/docs/components/color-swatch.md | 58 ++++++++
docs/content/meta/ColorSwatch.md | 25 ++++
docs/content/meta/ColorSwatchPickerItem.md | 37 +++++
.../meta/ColorSwatchPickerItemIndicator.md | 17 +++
docs/content/meta/ColorSwatchPickerRoot.md | 112 +++++++++++++++
packages/core/constant/components.ts | 11 ++
.../core/src/ColorSwatch/ColorSwatch.test.ts | 25 ++++
packages/core/src/ColorSwatch/ColorSwatch.vue | 61 ++++++++
packages/core/src/ColorSwatch/index.ts | 4 +
.../ColorSwatchPicker.test.ts | 29 ++++
.../ColorSwatchPickerItem.vue | 44 ++++++
.../ColorSwatchPickerItemIndicator.vue | 17 +++
.../ColorSwatchPickerItemSwatch.vue | 21 +++
.../ColorSwatchPickerRoot.vue | 49 +++++++
packages/core/src/ColorSwatchPicker/index.ts | 17 +++
packages/core/src/color/index.ts | 1 +
packages/core/src/color/utils.ts | 132 ++++++++++++++++++
packages/core/src/index.ts | 2 +
28 files changed, 899 insertions(+)
create mode 100644 docs/components/demo/ColorSwatch/css/index.vue
create mode 100644 docs/components/demo/ColorSwatch/css/styles.css
create mode 100644 docs/components/demo/ColorSwatch/tailwind/index.vue
create mode 100644 docs/components/demo/ColorSwatch/tailwind/tailwind.config.js
create mode 100644 docs/components/demo/ColorSwatchPicker/css/index.vue
create mode 100644 docs/components/demo/ColorSwatchPicker/css/styles.css
create mode 100644 docs/components/demo/ColorSwatchPicker/tailwind/index.vue
create mode 100644 docs/components/demo/ColorSwatchPicker/tailwind/tailwind.config.js
create mode 100644 docs/content/docs/components/color-swatch-picker.md
create mode 100644 docs/content/docs/components/color-swatch.md
create mode 100644 docs/content/meta/ColorSwatch.md
create mode 100644 docs/content/meta/ColorSwatchPickerItem.md
create mode 100644 docs/content/meta/ColorSwatchPickerItemIndicator.md
create mode 100644 docs/content/meta/ColorSwatchPickerRoot.md
create mode 100644 packages/core/src/ColorSwatch/ColorSwatch.test.ts
create mode 100644 packages/core/src/ColorSwatch/ColorSwatch.vue
create mode 100644 packages/core/src/ColorSwatch/index.ts
create mode 100644 packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts
create mode 100644 packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue
create mode 100644 packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemIndicator.vue
create mode 100644 packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemSwatch.vue
create mode 100644 packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue
create mode 100644 packages/core/src/ColorSwatchPicker/index.ts
create mode 100644 packages/core/src/color/index.ts
create mode 100644 packages/core/src/color/utils.ts
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 6a01653e49..328ed332e5 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -157,6 +157,16 @@ export default defineConfig({
{ text: 'Toggle Group', link: '/docs/components/toggle-group' },
],
},
+ {
+ text: 'Color',
+ items: [
+ { text: 'Color Swatch', link: '/docs/components/color-swatch' },
+ {
+ text: 'Color Swatch Picker',
+ link: '/docs/components/color-swatch-picker',
+ },
+ ],
+ },
{
text: 'Dates',
items: [
diff --git a/docs/components/demo/ColorSwatch/css/index.vue b/docs/components/demo/ColorSwatch/css/index.vue
new file mode 100644
index 0000000000..613b1f12ba
--- /dev/null
+++ b/docs/components/demo/ColorSwatch/css/index.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/docs/components/demo/ColorSwatch/css/styles.css b/docs/components/demo/ColorSwatch/css/styles.css
new file mode 100644
index 0000000000..1cf37ca441
--- /dev/null
+++ b/docs/components/demo/ColorSwatch/css/styles.css
@@ -0,0 +1,11 @@
+.swatch {
+ width: 32px;
+ height: 32px;
+ border-radius: 4px;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
+ flex-shrink: 0;
+}
+
+.dark .swatch {
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
+}
\ No newline at end of file
diff --git a/docs/components/demo/ColorSwatch/tailwind/index.vue b/docs/components/demo/ColorSwatch/tailwind/index.vue
new file mode 100644
index 0000000000..6895143cd5
--- /dev/null
+++ b/docs/components/demo/ColorSwatch/tailwind/index.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/docs/components/demo/ColorSwatch/tailwind/tailwind.config.js b/docs/components/demo/ColorSwatch/tailwind/tailwind.config.js
new file mode 100644
index 0000000000..fe691bdd5f
--- /dev/null
+++ b/docs/components/demo/ColorSwatch/tailwind/tailwind.config.js
@@ -0,0 +1,6 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./**/*.vue'],
+ theme: {},
+ plugins: [],
+}
diff --git a/docs/components/demo/ColorSwatchPicker/css/index.vue b/docs/components/demo/ColorSwatchPicker/css/index.vue
new file mode 100644
index 0000000000..ed6af2c068
--- /dev/null
+++ b/docs/components/demo/ColorSwatchPicker/css/index.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/components/demo/ColorSwatchPicker/css/styles.css b/docs/components/demo/ColorSwatchPicker/css/styles.css
new file mode 100644
index 0000000000..c927379b69
--- /dev/null
+++ b/docs/components/demo/ColorSwatchPicker/css/styles.css
@@ -0,0 +1,32 @@
+.root {
+ display: flex;
+ gap: 8px;
+}
+
+.item {
+ position: relative;
+}
+
+.swatch {
+ height: 32px;
+ width: 32px;
+ border-radius: 4px;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
+ flex-shrink: 0;
+ cursor: pointer;
+}
+
+.dark .swatch {
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
+}
+
+.indicator {
+ position: absolute;
+ bottom: 4px;
+ right: 4px;
+ color: white;
+}
+
+.swatch[data-color-contrast='dark'] ~ .indicator {
+ color: black;
+}
\ No newline at end of file
diff --git a/docs/components/demo/ColorSwatchPicker/tailwind/index.vue b/docs/components/demo/ColorSwatchPicker/tailwind/index.vue
new file mode 100644
index 0000000000..5f69b3f81b
--- /dev/null
+++ b/docs/components/demo/ColorSwatchPicker/tailwind/index.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/components/demo/ColorSwatchPicker/tailwind/tailwind.config.js b/docs/components/demo/ColorSwatchPicker/tailwind/tailwind.config.js
new file mode 100644
index 0000000000..fe691bdd5f
--- /dev/null
+++ b/docs/components/demo/ColorSwatchPicker/tailwind/tailwind.config.js
@@ -0,0 +1,6 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./**/*.vue'],
+ theme: {},
+ plugins: [],
+}
diff --git a/docs/content/docs/components/color-swatch-picker.md b/docs/content/docs/components/color-swatch-picker.md
new file mode 100644
index 0000000000..5711006717
--- /dev/null
+++ b/docs/content/docs/components/color-swatch-picker.md
@@ -0,0 +1,90 @@
+---
+
+title: Color Swatch Picker
+description: A component that allows users to select from a set of predefined colors, often used for themes or branding.
+name: color-swatch-picker
+---
+
+# Color Swatch Picker
+
+
+A component that allows users to select from a set of predefined colors, often used for themes or branding.
+
+
+
+
+## Features
+
+
+
+## Installation
+
+Install the component from your command line.
+
+
+
+## Anatomy
+
+Import all parts and piece them together.
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## API Reference
+
+### ColorSwatchPickerRoot
+
+The main component that displays a color swatch picker.
+
+
+
+### ColorSwatchPickerItem
+
+The item that represents a selectable color swatch.
+
+
+
+### ColorSwatchPickerItemSwatch
+
+The component that displays the color swatch within an item.
+
+
+
+### ColorSwatchPickerItemIndicator
+
+The component that indicates the selected color swatch within an item.
+
+
diff --git a/docs/content/docs/components/color-swatch.md b/docs/content/docs/components/color-swatch.md
new file mode 100644
index 0000000000..7842d3303b
--- /dev/null
+++ b/docs/content/docs/components/color-swatch.md
@@ -0,0 +1,58 @@
+---
+
+title: Color Swatch
+description: A predefined color block that allows users to quickly select commonly used or brand-specific colors.
+name: color-swatch
+---
+
+# Color Swatch
+
+
+Displays a color swatch, which can be used to represent colors in a UI.
+
+
+
+
+## Features
+
+
+
+## Installation
+
+Install the component from your command line.
+
+
+
+## Anatomy
+
+Import all parts and piece them together.
+
+```vue
+
+
+
+
+
+```
+
+## API Reference
+
+### ColorSwatch
+
+The main component that displays a color swatch.
+
+
diff --git a/docs/content/meta/ColorSwatch.md b/docs/content/meta/ColorSwatch.md
new file mode 100644
index 0000000000..afaf643915
--- /dev/null
+++ b/docs/content/meta/ColorSwatch.md
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/docs/content/meta/ColorSwatchPickerItem.md b/docs/content/meta/ColorSwatchPickerItem.md
new file mode 100644
index 0000000000..25f4d859b2
--- /dev/null
+++ b/docs/content/meta/ColorSwatchPickerItem.md
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/docs/content/meta/ColorSwatchPickerItemIndicator.md b/docs/content/meta/ColorSwatchPickerItemIndicator.md
new file mode 100644
index 0000000000..5df63e84f6
--- /dev/null
+++ b/docs/content/meta/ColorSwatchPickerItemIndicator.md
@@ -0,0 +1,17 @@
+
+
+
diff --git a/docs/content/meta/ColorSwatchPickerRoot.md b/docs/content/meta/ColorSwatchPickerRoot.md
new file mode 100644
index 0000000000..55c54316ae
--- /dev/null
+++ b/docs/content/meta/ColorSwatchPickerRoot.md
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
diff --git a/packages/core/constant/components.ts b/packages/core/constant/components.ts
index e1f1ac71a8..80fa341e35 100644
--- a/packages/core/constant/components.ts
+++ b/packages/core/constant/components.ts
@@ -56,6 +56,17 @@ export const components = {
'CollapsibleContent',
] as const,
+ colorSwatch: [
+ 'ColorSwatch',
+ ] as const,
+
+ colorSwatchPicker: [
+ 'ColorSwatchPickerRoot',
+ 'ColorSwatchPickerItem',
+ 'ColorSwatchPickerItemSwatch',
+ 'ColorSwatchPickerItemIndicator',
+ ] as const,
+
combobox: [
'ComboboxRoot',
'ComboboxInput',
diff --git a/packages/core/src/ColorSwatch/ColorSwatch.test.ts b/packages/core/src/ColorSwatch/ColorSwatch.test.ts
new file mode 100644
index 0000000000..687415ad4f
--- /dev/null
+++ b/packages/core/src/ColorSwatch/ColorSwatch.test.ts
@@ -0,0 +1,25 @@
+import { render } from '@testing-library/vue'
+import { describe, expect, it } from 'vitest'
+import ColorSwatch from './ColorSwatch.vue'
+
+const TEST_COLOR = '#123abc'
+
+describe('colorSwatch.vue', () => {
+ it('applies the correct style variable for color', () => {
+ const { getByRole } = render(ColorSwatch, { props: { color: TEST_COLOR } })
+ const swatch = getByRole('img')
+ expect(swatch.style.getPropertyValue('--reka-color-swatch-color')).toBe(TEST_COLOR)
+ })
+
+ it('sets the correct aria-label', () => {
+ const { getByRole } = render(ColorSwatch, { props: { color: TEST_COLOR } })
+ const swatch = getByRole('img')
+ expect(swatch.hasAttribute('aria-label')).toBe(true)
+ })
+
+ it('sets aria-roledescription to "color swatch"', () => {
+ const { getByRole } = render(ColorSwatch, { props: { color: TEST_COLOR } })
+ const swatch = getByRole('img')
+ expect(swatch.getAttribute('aria-roledescription')).toBe('color swatch')
+ })
+})
diff --git a/packages/core/src/ColorSwatch/ColorSwatch.vue b/packages/core/src/ColorSwatch/ColorSwatch.vue
new file mode 100644
index 0000000000..2cfcd76167
--- /dev/null
+++ b/packages/core/src/ColorSwatch/ColorSwatch.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/ColorSwatch/index.ts b/packages/core/src/ColorSwatch/index.ts
new file mode 100644
index 0000000000..db424fa21e
--- /dev/null
+++ b/packages/core/src/ColorSwatch/index.ts
@@ -0,0 +1,4 @@
+export {
+ default as ColorSwatch,
+ type ColorSwatchProps,
+} from './ColorSwatch.vue'
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts b/packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts
new file mode 100644
index 0000000000..8f4bfff217
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts
@@ -0,0 +1,29 @@
+import { render } from '@testing-library/vue'
+import { describe, expect, it } from 'vitest'
+import ColorSwatchPickerItem from './ColorSwatchPickerItem.vue'
+import ColorSwatchPickerRoot from './ColorSwatchPickerRoot.vue'
+
+const TEST_COLOR = '#ff00aa'
+
+describe('colorSwatchPickerRoot.vue', () => {
+ it('renders with the correct model value', () => {
+ const { getByRole } = render(ColorSwatchPickerRoot, {
+ props: { modelValue: TEST_COLOR },
+ slots: {
+ default: ``,
+ },
+ })
+ expect(getByRole('listbox')).toBeInTheDocument()
+ })
+})
+
+describe('colorSwatchPickerItem.vue', () => {
+ it('applies the correct style variable for color', () => {
+ const { getByRole } = render(ColorSwatchPickerItem, {
+ props: { value: TEST_COLOR },
+ slots: { default: 'item' },
+ })
+ const item = getByRole('option')
+ expect(item.style.getPropertyValue('--reka-color-swatch-picker-item-color')).toBe(TEST_COLOR)
+ })
+})
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue
new file mode 100644
index 0000000000..27b3f660ae
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemIndicator.vue b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemIndicator.vue
new file mode 100644
index 0000000000..7294b6935e
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemIndicator.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemSwatch.vue b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemSwatch.vue
new file mode 100644
index 0000000000..189daffa4d
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemSwatch.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue
new file mode 100644
index 0000000000..81aeb9db40
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/ColorSwatchPicker/index.ts b/packages/core/src/ColorSwatchPicker/index.ts
new file mode 100644
index 0000000000..9320ca4703
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/index.ts
@@ -0,0 +1,17 @@
+export {
+ default as ColorSwatchPickerItem,
+ type ColorSwatchPickerItemProps,
+} from './ColorSwatchPickerItem.vue'
+export {
+ default as ColorSwatchPickerItemIndicator,
+ type ColorSwatchPickerItemIndicatorProps,
+} from './ColorSwatchPickerItemIndicator.vue'
+export {
+ default as ColorSwatchPickerItemSwatch,
+ type ColorSwatchPickerItemSwatchProps,
+} from './ColorSwatchPickerItemSwatch.vue'
+export {
+ default as ColorSwatchPickerRoot,
+ type ColorSwatchPickerRootEmits,
+ type ColorSwatchPickerRootProps,
+} from './ColorSwatchPickerRoot.vue'
diff --git a/packages/core/src/color/index.ts b/packages/core/src/color/index.ts
new file mode 100644
index 0000000000..9c56149efa
--- /dev/null
+++ b/packages/core/src/color/index.ts
@@ -0,0 +1 @@
+export * from './utils'
diff --git a/packages/core/src/color/utils.ts b/packages/core/src/color/utils.ts
new file mode 100644
index 0000000000..a475a6650a
--- /dev/null
+++ b/packages/core/src/color/utils.ts
@@ -0,0 +1,132 @@
+/**
+ * Converts a hex color string to HSL (Hue, Saturation, Lightness).
+ * @param hex Hex color string (e.g., "#ff5733")
+ * @returns An object containing hue, saturation, and lightness values.
+ */
+export function hexToHSL(hex: string): { h: number, s: number, l: number } {
+ // Remove the hash at the start if it's there
+ hex = hex.replace(/^#/, '')
+
+ // Parse the r, g, b values from the hex string
+ const bigint = parseInt(hex, 16)
+ let r = (bigint >> 16) & 255
+ let g = (bigint >> 8) & 255
+ let b = bigint & 255
+
+ // Convert to HSL
+ r /= 255
+ g /= 255
+ b /= 255
+
+ const max = Math.max(r, g, b)
+ const min = Math.min(r, g, b)
+ let h: number
+ let s: number
+ let l: number = (max + min) / 2
+
+ if (max === min) {
+ h = s = 0 // achromatic
+ }
+ else {
+ const d = max - min
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
+ if (max === r) {
+ h = (g - b) / d + (g < b ? 6 : 0)
+ }
+ else if (max === g) {
+ h = (b - r) / d + 2
+ }
+ else {
+ h = (r - g) / d + 4
+ }
+ h /= 6
+ h *= 360
+ s *= 100
+ l *= 100
+ }
+
+ return { h, s, l }
+}
+
+/**
+ * Converts a hex color string to a human-readable color name.
+ * @param hex Hex color string (e.g., "#ff5733")
+ * @returns A human-readable color name based on the hue, saturation, and lightness.
+ */
+export function getColorName(hex: string) {
+ // Convert RGB to HSL
+ const { h, s, l } = hexToHSL(hex)
+
+ // Handle achromatic colors (low saturation)
+ if (s < 0.1) {
+ if (l < 0.1)
+ return 'black'
+ if (l > 0.95)
+ return 'white'
+ if (l < 0.2)
+ return 'very dark gray'
+ if (l < 0.35)
+ return 'dark gray'
+ if (l < 0.65)
+ return 'gray'
+ if (l < 0.8)
+ return 'light gray'
+ return 'very light gray'
+ }
+
+ // Determine base color by hue
+ let baseName
+ if (h < 15 || h >= 345)
+ baseName = 'red'
+ else if (h < 45)
+ baseName = 'orange'
+ else if (h < 75)
+ baseName = 'yellow'
+ else if (h < 105)
+ baseName = 'yellow-green'
+ else if (h < 135)
+ baseName = 'green'
+ else if (h < 165)
+ baseName = 'green-cyan'
+ else if (h < 195)
+ baseName = 'cyan'
+ else if (h < 225)
+ baseName = 'blue'
+ else if (h < 255)
+ baseName = 'blue-violet'
+ else if (h < 285)
+ baseName = 'violet'
+ else if (h < 315)
+ baseName = 'magenta'
+ else baseName = 'red-magenta'
+
+ // Add descriptors based on saturation and lightness
+ const descriptors = []
+ if (s > 0.8)
+ descriptors.push('vibrant')
+ else if (s < 0.3)
+ descriptors.push('muted')
+
+ if (l > 0.8)
+ descriptors.push('light')
+ else if (l < 0.3)
+ descriptors.push('dark')
+
+ return descriptors.length > 0
+ ? `${descriptors.join(' ')} ${baseName}`
+ : baseName
+}
+
+export function getColorContrast(hex: string): 'light' | 'dark' {
+ // Convert hex to RGB
+ const bigint = parseInt(hex.replace(/^#/, ''), 16)
+ const r = (bigint >> 16) & 255
+ const g = (bigint >> 8) & 255
+ const b = bigint & 255
+
+ // Calculate luminance
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
+
+ // Return 'light' or 'dark' based on luminance
+ return luminance > 0.5 ? 'dark' : 'light'
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 348691136c..5546c9b3f7 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -5,6 +5,8 @@ export * from './Avatar'
export * from './Calendar'
export * from './Checkbox'
export * from './Collapsible'
+export * from './ColorSwatch'
+export * from './ColorSwatchPicker'
export * from './Combobox'
// utilities
export * from './ConfigProvider'
From 939adf2366ff46ae81977ed510c170db1fc649ef Mon Sep 17 00:00:00 2001
From: Mark Janiczak
Date: Mon, 14 Jul 2025 13:07:03 +1000
Subject: [PATCH 2/9] feat: use radix colors
---
.../components/demo/ColorSwatch/css/index.vue | 3 ++-
.../demo/ColorSwatch/tailwind/index.vue | 3 ++-
.../demo/ColorSwatchPicker/css/index.vue | 21 ++++++++++++++-----
.../demo/ColorSwatchPicker/tailwind/index.vue | 21 ++++++++++++++-----
docs/content/docs/components/color-swatch.md | 8 ++-----
5 files changed, 38 insertions(+), 18 deletions(-)
diff --git a/docs/components/demo/ColorSwatch/css/index.vue b/docs/components/demo/ColorSwatch/css/index.vue
index 613b1f12ba..b0413f57b7 100644
--- a/docs/components/demo/ColorSwatch/css/index.vue
+++ b/docs/components/demo/ColorSwatch/css/index.vue
@@ -1,11 +1,12 @@
diff --git a/docs/components/demo/ColorSwatch/tailwind/index.vue b/docs/components/demo/ColorSwatch/tailwind/index.vue
index 6895143cd5..89f9048993 100644
--- a/docs/components/demo/ColorSwatch/tailwind/index.vue
+++ b/docs/components/demo/ColorSwatch/tailwind/index.vue
@@ -1,10 +1,11 @@
diff --git a/docs/components/demo/ColorSwatchPicker/css/index.vue b/docs/components/demo/ColorSwatchPicker/css/index.vue
index ed6af2c068..67a8f1d194 100644
--- a/docs/components/demo/ColorSwatchPicker/css/index.vue
+++ b/docs/components/demo/ColorSwatchPicker/css/index.vue
@@ -1,14 +1,25 @@
diff --git a/docs/components/demo/ColorSwatchPicker/tailwind/index.vue b/docs/components/demo/ColorSwatchPicker/tailwind/index.vue
index 5f69b3f81b..04e26a4d25 100644
--- a/docs/components/demo/ColorSwatchPicker/tailwind/index.vue
+++ b/docs/components/demo/ColorSwatchPicker/tailwind/index.vue
@@ -1,13 +1,24 @@
diff --git a/docs/content/docs/components/color-swatch.md b/docs/content/docs/components/color-swatch.md
index 7842d3303b..e91c869807 100644
--- a/docs/content/docs/components/color-swatch.md
+++ b/docs/content/docs/components/color-swatch.md
@@ -17,13 +17,9 @@ Displays a color swatch, which can be used to represent colors in a UI.
From bd0be40f0c9c569143f5d9a5f7429e76ae4b802b Mon Sep 17 00:00:00 2001
From: Mark Janiczak
Date: Mon, 14 Jul 2025 13:08:49 +1000
Subject: [PATCH 3/9] feat: update docs
---
.../docs/components/color-swatch-picker.md | 2 +-
docs/content/meta/ColorSwatch.md | 13 +++++++++++
.../meta/ColorSwatchPickerItemSwatch.md | 23 +++++++++++++++++++
3 files changed, 37 insertions(+), 1 deletion(-)
create mode 100644 docs/content/meta/ColorSwatchPickerItemSwatch.md
diff --git a/docs/content/docs/components/color-swatch-picker.md b/docs/content/docs/components/color-swatch-picker.md
index 5711006717..9bcce6b227 100644
--- a/docs/content/docs/components/color-swatch-picker.md
+++ b/docs/content/docs/components/color-swatch-picker.md
@@ -40,8 +40,8 @@ Import all parts and piece them together.
import {
ColorSwatchPickerItem,
ColorSwatchPickerItemIndicator,
+ ColorSwatchPickerItemSwatch,
ColorSwatchPickerRoot,
- ColorSwatchPickerSwatch
} from 'reka-ui'
diff --git a/docs/content/meta/ColorSwatch.md b/docs/content/meta/ColorSwatch.md
index afaf643915..cecff1f6da 100644
--- a/docs/content/meta/ColorSwatch.md
+++ b/docs/content/meta/ColorSwatch.md
@@ -1,6 +1,19 @@
The element or component this component should render as. Can be overwritten by asChild.
\n',
+ 'type': 'AsTag | Component',
+ 'required': false,
+ 'default': '\'div\''
+ },
+ {
+ 'name': 'asChild',
+ 'description': 'Change the default rendered element for the one passed as a child, merging their props and behavior.
\nRead our Composition guide for more details.
\n',
+ 'type': 'boolean',
+ 'required': false
+ },
+ {
+ 'name': 'label',
+ 'description': '',
+ 'type': 'string',
+ 'required': false
+ }
+]" />
From 10f991273579f7945195ecbe91719a2b41fa919e Mon Sep 17 00:00:00 2001
From: Mark Janiczak
Date: Mon, 14 Jul 2025 13:11:50 +1000
Subject: [PATCH 4/9] feat: docs
---
docs/content/meta/ColorSwatch.md | 4 ++--
docs/content/meta/ColorSwatchPickerItem.md | 2 +-
docs/content/meta/ColorSwatchPickerItemSwatch.md | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/content/meta/ColorSwatch.md b/docs/content/meta/ColorSwatch.md
index cecff1f6da..6ff30b9e57 100644
--- a/docs/content/meta/ColorSwatch.md
+++ b/docs/content/meta/ColorSwatch.md
@@ -16,14 +16,14 @@
},
{
'name': 'color',
- 'description': '',
+ 'description': 'The color to display in the swatch as a hex string.\nExample: #16a372 or #ff5733.
\n',
'type': 'string',
'required': false,
'default': '\'\''
},
{
'name': 'label',
- 'description': '',
+ 'description': 'Optional accessible label for the color. If omitted, the color name will be derived from the color value.
\n',
'type': 'string',
'required': false
}
diff --git a/docs/content/meta/ColorSwatchPickerItem.md b/docs/content/meta/ColorSwatchPickerItem.md
index 25f4d859b2..965b0b595c 100644
--- a/docs/content/meta/ColorSwatchPickerItem.md
+++ b/docs/content/meta/ColorSwatchPickerItem.md
@@ -22,7 +22,7 @@
},
{
'name': 'value',
- 'description': 'The value given as data when submitted with a name.
\n',
+ 'description': 'The color to display in the swatch as a hex string.\nExample: #16a372 or #ff5733.
\n',
'type': 'string',
'required': true
}
diff --git a/docs/content/meta/ColorSwatchPickerItemSwatch.md b/docs/content/meta/ColorSwatchPickerItemSwatch.md
index 3dbd2c3504..701ee1d389 100644
--- a/docs/content/meta/ColorSwatchPickerItemSwatch.md
+++ b/docs/content/meta/ColorSwatchPickerItemSwatch.md
@@ -16,7 +16,7 @@
},
{
'name': 'label',
- 'description': '',
+ 'description': 'Optional accessible label for the color. If omitted, the color name will be derived from the color value.
\n',
'type': 'string',
'required': false
}
From fdd294a98275ec1830a7bb5cb07104a0a7fb12dc Mon Sep 17 00:00:00 2001
From: Mark Janiczak
Date: Mon, 14 Jul 2025 13:11:59 +1000
Subject: [PATCH 5/9] feat: comments
---
packages/core/src/ColorSwatch/ColorSwatch.vue | 7 +++++++
.../core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue | 4 ++++
2 files changed, 11 insertions(+)
diff --git a/packages/core/src/ColorSwatch/ColorSwatch.vue b/packages/core/src/ColorSwatch/ColorSwatch.vue
index 2cfcd76167..4d465c8ae9 100644
--- a/packages/core/src/ColorSwatch/ColorSwatch.vue
+++ b/packages/core/src/ColorSwatch/ColorSwatch.vue
@@ -2,7 +2,14 @@
import type { PrimitiveProps } from '@/Primitive'
export interface ColorSwatchProps extends PrimitiveProps {
+ /**
+ * The color to display in the swatch as a hex string.
+ * Example: `#16a372` or `#ff5733`.
+ */
color?: string
+ /**
+ * Optional accessible label for the color. If omitted, the color name will be derived from the color value.
+ */
label?: string
}
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue
index 27b3f660ae..53ed0e3092 100644
--- a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue
@@ -4,6 +4,10 @@ import type { ListboxItemEmits, ListboxItemProps } from '@/Listbox'
import { createContext, useForwardPropsEmits } from '@/shared'
export interface ColorSwatchPickerItemProps extends ListboxItemProps {
+/**
+ * The color to display in the swatch as a hex string.
+ * Example: `#16a372` or `#ff5733`.
+ */
value: string
}
From 7495c928950483139a0f0cef9965565aa25df0f1 Mon Sep 17 00:00:00 2001
From: Mark Janiczak
Date: Mon, 14 Jul 2025 13:35:08 +1000
Subject: [PATCH 6/9] feat: tests
---
.../core/src/ColorSwatch/ColorSwatch.test.ts | 29 ++++--------
.../src/ColorSwatch/story/_ColorSwatch.vue | 16 +++++++
.../ColorSwatchPicker.test.ts | 46 +++++++++----------
.../ColorSwatchPickerRoot.vue | 1 +
.../story/_ColorSwatchPicker.vue | 37 +++++++++++++++
5 files changed, 87 insertions(+), 42 deletions(-)
create mode 100644 packages/core/src/ColorSwatch/story/_ColorSwatch.vue
create mode 100644 packages/core/src/ColorSwatchPicker/story/_ColorSwatchPicker.vue
diff --git a/packages/core/src/ColorSwatch/ColorSwatch.test.ts b/packages/core/src/ColorSwatch/ColorSwatch.test.ts
index 687415ad4f..4a24d77de3 100644
--- a/packages/core/src/ColorSwatch/ColorSwatch.test.ts
+++ b/packages/core/src/ColorSwatch/ColorSwatch.test.ts
@@ -1,25 +1,16 @@
-import { render } from '@testing-library/vue'
+import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
-import ColorSwatch from './ColorSwatch.vue'
+import { axe } from 'vitest-axe'
+import ColorSwatch from './story/_ColorSwatch.vue'
-const TEST_COLOR = '#123abc'
-
-describe('colorSwatch.vue', () => {
- it('applies the correct style variable for color', () => {
- const { getByRole } = render(ColorSwatch, { props: { color: TEST_COLOR } })
- const swatch = getByRole('img')
- expect(swatch.style.getPropertyValue('--reka-color-swatch-color')).toBe(TEST_COLOR)
- })
-
- it('sets the correct aria-label', () => {
- const { getByRole } = render(ColorSwatch, { props: { color: TEST_COLOR } })
- const swatch = getByRole('img')
- expect(swatch.hasAttribute('aria-label')).toBe(true)
+describe('given a default ColorSwatch', () => {
+ it('should pass axe accessibility tests', async () => {
+ const wrapper = mount(ColorSwatch, { attachTo: document.body })
+ expect(await axe(wrapper.element)).toHaveNoViolations()
})
- it('sets aria-roledescription to "color swatch"', () => {
- const { getByRole } = render(ColorSwatch, { props: { color: TEST_COLOR } })
- const swatch = getByRole('img')
- expect(swatch.getAttribute('aria-roledescription')).toBe('color swatch')
+ it('should render ColorSwatch', () => {
+ const wrapper = mount(ColorSwatch)
+ expect(wrapper.element).toBeTruthy()
})
})
diff --git a/packages/core/src/ColorSwatch/story/_ColorSwatch.vue b/packages/core/src/ColorSwatch/story/_ColorSwatch.vue
new file mode 100644
index 0000000000..b9eb415a17
--- /dev/null
+++ b/packages/core/src/ColorSwatch/story/_ColorSwatch.vue
@@ -0,0 +1,16 @@
+
+
+
+ />
+
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts b/packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts
index 8f4bfff217..6798ee7ee5 100644
--- a/packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts
@@ -1,29 +1,29 @@
-import { render } from '@testing-library/vue'
-import { describe, expect, it } from 'vitest'
-import ColorSwatchPickerItem from './ColorSwatchPickerItem.vue'
-import ColorSwatchPickerRoot from './ColorSwatchPickerRoot.vue'
+import type { DOMWrapper, VueWrapper } from '@vue/test-utils'
+import { mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, it } from 'vitest'
+import { axe } from 'vitest-axe'
+import ColorSwatchPicker from './story/_ColorSwatchPicker.vue'
-const TEST_COLOR = '#ff00aa'
+describe('given default ColorSwatchPicker', () => {
+ let wrapper: VueWrapper>
+ let content: DOMWrapper
+ let items: DOMWrapper[]
-describe('colorSwatchPickerRoot.vue', () => {
- it('renders with the correct model value', () => {
- const { getByRole } = render(ColorSwatchPickerRoot, {
- props: { modelValue: TEST_COLOR },
- slots: {
- default: ``,
- },
- })
- expect(getByRole('listbox')).toBeInTheDocument()
+ beforeEach(() => {
+ document.body.innerHTML = ''
+ wrapper = mount(ColorSwatchPicker, { attachTo: document.body })
+ content = wrapper.find('[role=listbox]')
+ items = wrapper.findAll('[role=option]')
+ })
+
+ it('should render the component', () => {
+ expect(wrapper.exists()).toBe(true)
+ expect(content.exists()).toBe(true)
+ expect(items.length).toBeGreaterThan(0)
})
-})
-describe('colorSwatchPickerItem.vue', () => {
- it('applies the correct style variable for color', () => {
- const { getByRole } = render(ColorSwatchPickerItem, {
- props: { value: TEST_COLOR },
- slots: { default: 'item' },
- })
- const item = getByRole('option')
- expect(item.style.getPropertyValue('--reka-color-swatch-picker-item-color')).toBe(TEST_COLOR)
+ it('should have no accessibility violations', async () => {
+ const results = await axe(wrapper.element)
+ expect(results).toHaveNoViolations()
})
})
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue
index 81aeb9db40..42d3423560 100644
--- a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue
@@ -40,6 +40,7 @@ const forwarded = useForwardPropsEmits(props, emits)
as-child
>
diff --git a/packages/core/src/ColorSwatchPicker/story/_ColorSwatchPicker.vue b/packages/core/src/ColorSwatchPicker/story/_ColorSwatchPicker.vue
new file mode 100644
index 0000000000..65f562fc6f
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/story/_ColorSwatchPicker.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
From a547d277bca189bd6f9aacec478abeb843ef7a45 Mon Sep 17 00:00:00 2001
From: Mark Janiczak
Date: Mon, 14 Jul 2025 13:39:00 +1000
Subject: [PATCH 7/9] feat: change color picker colors
---
.../demo/ColorSwatchPicker/tailwind/index.vue | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/components/demo/ColorSwatchPicker/tailwind/index.vue b/docs/components/demo/ColorSwatchPicker/tailwind/index.vue
index 04e26a4d25..cccced8106 100644
--- a/docs/components/demo/ColorSwatchPicker/tailwind/index.vue
+++ b/docs/components/demo/ColorSwatchPicker/tailwind/index.vue
@@ -3,8 +3,8 @@ import { Icon } from '@iconify/vue'
import {
blue,
green,
+ indigo,
orange,
- pink,
red,
violet,
yellow,
@@ -13,12 +13,12 @@ import { ColorSwatchPickerItem, ColorSwatchPickerItemIndicator, ColorSwatchPicke
const colors = [
red.red9,
- pink.pink9,
- violet.violet9,
- blue.blue9,
- green.green9,
orange.orange9,
yellow.yellow9,
+ green.green9,
+ blue.blue9,
+ indigo.indigo9,
+ violet.violet9,
]
From b4c081facc135f68d99ed81ec46122dfcbb9d907 Mon Sep 17 00:00:00 2001
From: Mark Janiczak
Date: Mon, 14 Jul 2025 13:41:56 +1000
Subject: [PATCH 8/9] feat: docs
---
docs/content/docs/components/color-swatch-picker.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/content/docs/components/color-swatch-picker.md b/docs/content/docs/components/color-swatch-picker.md
index 9bcce6b227..092b1da05f 100644
--- a/docs/content/docs/components/color-swatch-picker.md
+++ b/docs/content/docs/components/color-swatch-picker.md
@@ -48,15 +48,15 @@ import {
-
+
-
+
-
+
From b31767509a7a0ddf31d202d9ae3f2b85c5ae3d7a Mon Sep 17 00:00:00 2001
From: Mark Janiczak
Date: Mon, 14 Jul 2025 13:48:17 +1000
Subject: [PATCH 9/9] feat: utils
---
packages/core/src/color/utils.ts | 35 +++++++++++++++-----------------
1 file changed, 16 insertions(+), 19 deletions(-)
diff --git a/packages/core/src/color/utils.ts b/packages/core/src/color/utils.ts
index a475a6650a..51ee2265dc 100644
--- a/packages/core/src/color/utils.ts
+++ b/packages/core/src/color/utils.ts
@@ -1,19 +1,25 @@
+/**
+ * Converts a hex color string to RGB (Red, Green, Blue).
+ * @param hex
+ * @returns
+ */
+export function hexToRGB(hex: string): { r: number, g: number, b: number } {
+ hex = hex.replace(/^#/, '')
+ const bigint = parseInt(hex, 16)
+ const r = (bigint >> 16) & 255
+ const g = (bigint >> 8) & 255
+ const b = bigint & 255
+ return { r, g, b }
+}
+
/**
* Converts a hex color string to HSL (Hue, Saturation, Lightness).
* @param hex Hex color string (e.g., "#ff5733")
* @returns An object containing hue, saturation, and lightness values.
*/
export function hexToHSL(hex: string): { h: number, s: number, l: number } {
- // Remove the hash at the start if it's there
- hex = hex.replace(/^#/, '')
-
- // Parse the r, g, b values from the hex string
- const bigint = parseInt(hex, 16)
- let r = (bigint >> 16) & 255
- let g = (bigint >> 8) & 255
- let b = bigint & 255
+ let { r, g, b } = hexToRGB(hex)
- // Convert to HSL
r /= 255
g /= 255
b /= 255
@@ -54,7 +60,6 @@ export function hexToHSL(hex: string): { h: number, s: number, l: number } {
* @returns A human-readable color name based on the hue, saturation, and lightness.
*/
export function getColorName(hex: string) {
- // Convert RGB to HSL
const { h, s, l } = hexToHSL(hex)
// Handle achromatic colors (low saturation)
@@ -118,15 +123,7 @@ export function getColorName(hex: string) {
}
export function getColorContrast(hex: string): 'light' | 'dark' {
- // Convert hex to RGB
- const bigint = parseInt(hex.replace(/^#/, ''), 16)
- const r = (bigint >> 16) & 255
- const g = (bigint >> 8) & 255
- const b = bigint & 255
-
- // Calculate luminance
+ const { r, g, b } = hexToRGB(hex)
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
-
- // Return 'light' or 'dark' based on luminance
return luminance > 0.5 ? 'dark' : 'light'
}