From ddea73cb4ef30597e807b19f4ddcbe54a7a2e050 Mon Sep 17 00:00:00 2001 From: Creeperkatze Date: Thu, 27 Aug 2026 21:22:57 +0200 Subject: [PATCH 1/2] Initial color picker impl --- packages/ui/src/components/base/index.ts | 10 +- .../components/base/inputs/ColorPicker.vue | 380 ++++++++++++++++++ .../ui/src/components/base/inputs/index.ts | 1 + .../image-viewer-editor/controls.vue | 32 +- .../src/stories/inputs/ColorPicker.stories.ts | 79 ++++ standards/frontend/INPUTS.md | 163 +++----- 6 files changed, 532 insertions(+), 133 deletions(-) create mode 100644 packages/ui/src/components/base/inputs/ColorPicker.vue create mode 100644 packages/ui/src/stories/inputs/ColorPicker.stories.ts diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts index c789f4e7d8..ca06c2867f 100644 --- a/packages/ui/src/components/base/index.ts +++ b/packages/ui/src/components/base/index.ts @@ -71,7 +71,15 @@ export { default as I18nDebugPanel } from './I18nDebugPanel.vue' export { default as IconSelect } from './IconSelect.vue' export { default as InlineEditableText } from './InlineEditableText.vue' export type { InputAppearance, InputSize } from './inputs' -export { DateInput, DatePicker, Input, InputClearButton, InputFrame, Textarea } from './inputs' +export { + ColorPicker, + DateInput, + DatePicker, + Input, + InputClearButton, + InputFrame, + Textarea, +} from './inputs' export { default as IntlFormatted } from './IntlFormatted.vue' export { default as LoadingBar } from './LoadingBar.vue' export { default as LoadingIndicator } from './LoadingIndicator.vue' diff --git a/packages/ui/src/components/base/inputs/ColorPicker.vue b/packages/ui/src/components/base/inputs/ColorPicker.vue new file mode 100644 index 0000000000..1057259c6b --- /dev/null +++ b/packages/ui/src/components/base/inputs/ColorPicker.vue @@ -0,0 +1,380 @@ + + + + + diff --git a/packages/ui/src/components/base/inputs/index.ts b/packages/ui/src/components/base/inputs/index.ts index e396a26cd2..fce0c36a7f 100644 --- a/packages/ui/src/components/base/inputs/index.ts +++ b/packages/ui/src/components/base/inputs/index.ts @@ -1,3 +1,4 @@ +export { default as ColorPicker } from './ColorPicker.vue' export { default as DateInput } from './DateInput.vue' export { default as DatePicker } from './DatePicker.vue' export { default as Input } from './Input.vue' diff --git a/packages/ui/src/components/image-viewer-editor/controls.vue b/packages/ui/src/components/image-viewer-editor/controls.vue index 96527033a5..761593306f 100644 --- a/packages/ui/src/components/image-viewer-editor/controls.vue +++ b/packages/ui/src/components/image-viewer-editor/controls.vue @@ -15,6 +15,7 @@ import IconButton from '#ui/components/base/buttons/IconButton.vue' import SplitButton from '#ui/components/base/buttons/SplitButton.vue' import type { OverflowMenuOption } from '#ui/components/base/buttons/types' import Chips from '#ui/components/base/Chips.vue' +import ColorPicker from '#ui/components/base/inputs/ColorPicker.vue' import { useVIntl } from '#ui/composables/i18n' import { commonMessages } from '#ui/utils/common-messages' @@ -108,10 +109,6 @@ function handlePropertyInput(event: Event) { updatePropertyValue(Number((event.target as HTMLInputElement).value)) } -function handleColorInput(event: Event) { - updateColor((event.target as HTMLInputElement).value) -} - function formatCensorMode(value: ScreenshotCensorMode) { return formatMessage(value === 'blur' ? messages.blur : messages.solid) } @@ -153,27 +150,14 @@ function formatEraserMode(value: ScreenshotEraserMode) { size="small" hide-checkmark-icon /> - + :model-value="color" + :label="formatMessage(messages.colour)" + @update:model-value="updateColor" + @focus="beginPropertyEdit" + @change="commitPropertyEdit" + />
+ +export default meta +type Story = StoryObj + +export const Default: Story = { + render: (args) => ({ + components: { ColorPicker }, + setup() { + const value = ref('#ff6b6b') + return { args, value } + }, + template: /* html */ ` +
+ +

Selected value: {{ value }}

+
+ `, + }), + args: { + label: 'Colour', + }, +} + +export const Small: Story = { + render: () => ({ + components: { ColorPicker }, + setup() { + const value = ref('#3b82f6') + return { value } + }, + template: /* html */ ` +
+ +

Selected value: {{ value }}

+
+ `, + }), +} + +export const Disabled: Story = { + args: { + label: 'Colour', + modelValue: '#22c55e', + disabled: true, + }, +} + +export const FocusAndChangeEvents: Story = { + render: () => ({ + components: { ColorPicker }, + setup() { + const value = ref('#a855f7') + const log = ref([]) + return { value, log } + }, + template: /* html */ ` +
+ +
    +
  • {{ entry }}
  • +
+
+ `, + }), +} diff --git a/standards/frontend/INPUTS.md b/standards/frontend/INPUTS.md index fa5c48fb91..959b063d91 100644 --- a/standards/frontend/INPUTS.md +++ b/standards/frontend/INPUTS.md @@ -12,12 +12,13 @@ into application code. ## Select a component -| Component | Use | -| ------------ | -------------------------------------------------------------------------- | -| `Input` | Single-line text, email, password, number, URL, or search entry | -| `Textarea` | Multi-line text entry | -| `DateInput` | A native browser control for a date or a local date and time | -| `DatePicker` | A styled calendar for ranges, multiple dates, time, or inline presentation | +| Component | Use | +| ------------- | -------------------------------------------------------------------------- | +| `Input` | Single-line text, email, password, number, URL, or search entry | +| `Textarea` | Multi-line text entry | +| `DateInput` | A native browser control for a date or a local date and time | +| `DatePicker` | A styled calendar for ranges, multiple dates, time, or inline presentation | +| `ColorPicker` | A swatch button that opens a hue and saturation picker with a hex field | Use a dedicated component for each other type of control. Do not use `Input` instead of a select, checkbox, radio group, toggle, or file picker. @@ -26,7 +27,7 @@ Import the public input components from `@modrinth/ui`. In `packages/ui`, import from `#ui/components/base/inputs`: ```ts -import { DateInput, DatePicker, Input, Textarea } from '@modrinth/ui' +import { ColorPicker, DateInput, DatePicker, Input, Textarea } from '@modrinth/ui' ``` ## Labels and field descriptions @@ -38,12 +39,7 @@ For a visible label, set the same value for the label `for` and the control `id` ```vue - + ``` An input does not have a `label` prop. Do not use a placeholder as the only label. The @@ -52,13 +48,7 @@ placeholder is not visible after the user enters a value. If a visible label repeats other content, use an accessible name such as `aria-label`: ```vue - + ``` Connect descriptions and validation messages to the control with `aria-describedby`. @@ -66,12 +56,7 @@ The component sends additional attributes to the native control: ```vue - +

Use lowercase letters, numbers, and dashes.

{{ slugError }} @@ -87,22 +72,9 @@ Bind the field value with `v-model`. For text types, the `Input` model is a stri `type="number"`, the model is a number or `undefined`: ```vue - - - + + + ``` An empty number input sets the model to `undefined`. The `clamp` prop keeps the number @@ -117,14 +89,7 @@ the native input type are different. Use `Textarea` for multi-line content: ```vue -