Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add field type fields.color() #1279

Open
tordans opened this issue Aug 29, 2024 · 4 comments
Open

Add field type fields.color() #1279

tordans opened this issue Aug 29, 2024 · 4 comments

Comments

@tordans
Copy link
Contributor

tordans commented Aug 29, 2024

It would be great to have a fields.color field that shows the native color picker of the browser and returns a hex value.

@tordans
Copy link
Contributor Author

tordans commented Sep 14, 2024

@zanhk I can create and add custom field components? That sounds great! Do you know of an example on this or some docs?

@zanhk
Copy link

zanhk commented Sep 14, 2024

@tordans yes you can create custom fields components, I don't think there is docs about it.

for usage you can simply use like this

import { ColorPicker } from "keystatic-components";

....

primary: ColorPicker({
  label: "Primary Color",
  description: "Main color of the site, the selected color will be used to create a color palette based on the chosen color",
}),

@zanhk
Copy link

zanhk commented Sep 14, 2024

If people need I made an icon picker as well

import { Icon } from "@iconify/react";
import { FieldPrimitive } from "@keystar/ui/field";
import type { BasicFormField } from "@keystatic/core";
import { useEffect, useState } from "react";
import AsyncSelect from "react-select/async";
import packageJson from "../package.json";

const customStyles = {
	control: (provided: any, state: { isFocused: boolean }) => ({
		...provided,
		fontFamily: "var(--kui-typography-font-family-base)",
		paddingBlock: "var(--kui-size-space-small)",
		paddingInline: "var(--kui-size-space-medium)",
		paddingLeft: "6px",
		paddingRight: "0",
		maxWidth: "20rem",
		minHeight: "32px",
		height: "32px",
	}),
	valueContainer: (provided: any) => ({
		...provided,
		height: "30px",
		paddingLeft: "2px",
		paddingTop: "0",
		paddingBottom: "8px",
	}),
	input: (provided: any) => ({
		...provided,
		fontFamily: "var(--kui-typography-font-family-base)",
		fontWeight: "var(--kui-typography-font-weight-regular)",
		fontSize: "var(--kui-typography-text-regular-size)",
		margin: "0px",
	}),
	singleValue: (provided: any) => ({
		...provided,
		fontFamily: "var(--kui-typography-font-family-base)",
		fontWeight: "var(--kui-typography-font-weight-regular)",
		fontSize: "var(--kui-typography-text-regular-size)",
		display: "flex",
		alignItems: "center",
	}),
	menu: (provided: any) => ({
		...provided,
		fontFamily: "var(--kui-typography-font-family-base)",
		maxWidth: "20rem",
	}),
	option: (provided: any, state: { isFocused: boolean }) => ({
		...provided,
		fontFamily: "var(--kui-typography-font-family-base)",
		display: "flex",
		alignItems: "center",
	}),
	indicatorSeparator: () => ({
		display: "none",
	}),
	indicatorsContainer: (provided: any) => ({
		...provided,
		height: "30px",
		paddingBottom: "6px",
		paddingRight: "0",
	}),
	container: (provided: any) => ({
		...provided,
		maxWidth: "20rem",
	}),
};

function getIconifySets() {
	const iconifySets = Object.entries(packageJson.devDependencies || {})
		.filter(([key]) => key.startsWith("@iconify-json/"))
		.map(([key, version]) => {
			const setName = key.split("/")[1];
			return {
				key: setName,
				prefix: `${setName}:`,
				url: `https://cdn.jsdelivr.net/npm/${key}@${version}/icons.json`,
			};
		});
	return iconifySets;
}

const iconSets = getIconifySets();

async function fetchIconOptions(inputValue: string) {
	const options = [];
	for (const set of iconSets) {
		try {
			const response = await fetch(set.url);
			const data = await response.json();
			const icons = Object.keys(data.icons)
				.filter((icon) => icon.toLowerCase().includes(inputValue.toLowerCase()))
				.slice(0, 50);
			options.push(
				...icons.map((icon) => ({
					label: `${set.key}:${icon}`,
					value: `${set.prefix}${icon}`,
					icon: `${set.prefix}${icon}`,
				})),
			);
		} catch (error) {
			console.error(`Error fetching icons for ${set.key}:`, error);
		}
	}
	return options;
}

export function IconPicker({
	label,
	defaultValue = "mdi:home",
	description,
}: {
	label: string;
	defaultValue?: string;
	description?: string;
}): BasicFormField<string> {
	return {
		kind: "form",
		Input({ value, onChange, autoFocus, forceValidation }) {
			const [selectedOption, setSelectedOption] = useState<{ value: string; label: string; icon: string } | null>(null);

			useEffect(() => {
				if (value) {
					const [prefix, iconName] = value.split(":");
					setSelectedOption({ value, label: `${prefix}:${iconName}`, icon: value });
				}
			}, [value]);

			const handleChange = (option: { value: string; label: string; icon: string } | null) => {
				setSelectedOption(option);
				onChange(option?.value ?? "");
			};

			const loadOptions = (inputValue: string) =>
				new Promise<{ label: string; value: string; icon: string }[]>((resolve) => {
					setTimeout(() => {
						fetchIconOptions(inputValue).then(resolve);
					}, 300);
				});

			const customOption = ({ data, innerProps, isFocused, isSelected }) => (
				<div
					{...innerProps}
					style={{
						display: "flex",
						alignItems: "center",
						padding: "8px",
						color: isSelected ? "white" : "var(--kui-color-foreground-neutral)",
						background: isSelected ? "#3a5ccc" : isFocused ? "#f9f1fe" : "transparent",
					}}
				>
					<Icon icon={data.icon} style={{ marginRight: "8px" }} />
					{data.label}
				</div>
			);

			return (
				<FieldPrimitive label={label} description={description}>
					<>
						<AsyncSelect
							cacheOptions
							defaultOptions
							loadOptions={loadOptions}
							value={selectedOption}
							onChange={handleChange}
							placeholder="Search for an icon..."
							autoFocus={autoFocus}
							styles={customStyles}
							components={{ Option: customOption }}
							menuPortalTarget={document.body}
							formatOptionLabel={(option) => (
								<div style={{ display: "flex", alignItems: "center" }}>
									<Icon icon={option.icon} style={{ marginRight: "8px" }} />
									<span>{option.label}</span>
								</div>
							)}
						/>
						<small
							style={{
								fontFamily: "var(--kui-typography-font-family-base)",
								color: "var(--kui-color-content-subtle)",
								paddingTop: 0,
								paddingLeft: "2px",
								paddingBottom: 0,
								marginTop: "-2px",
								marginBottom: 0,
							}}
						>
							Find all available icons at{" "}
							<a href="https://icon-sets.iconify.design/" target="_blank" rel="noopener noreferrer">
								Iconify Icon Sets
							</a>{" "}
							(available sets: {iconSets.map((set) => set.key).join(", ")})
						</small>
					</>
				</FieldPrimitive>
			);
		},
		defaultValue() {
			return defaultValue;
		},
		parse(value) {
			return value as string;
		},
		serialize(value) {
			return { value };
		},
		validate(value) {
			return value;
		},
		reader: {
			parse(value) {
				return value as string;
			},
		},
	};
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants