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

V0.3.7 - Color Picker & Use Auto Scroll #62

Merged
merged 4 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/website/src/changelogs/2024-11-28.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Color Picker & Use Auto Scroll
---

#### New Category

- Added the Color Picker category with a new component: the Arc `Color Picker`.

#### New hook

- Added a new hook which allows you to easily create auto scroll chat. The hook is called `useAutoScroll`.
2 changes: 1 addition & 1 deletion apps/website/src/changelogs/last-changelog-date.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// This file is generated by the generate-latest-changelog-date script
export const lastChangelogDate = new Date("2024-11-26T23:00:00.000Z");
export const lastChangelogDate = new Date("2024-11-27T23:00:00.000Z");
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"private": true,
"scripts": {
"pre-build": "turbo pre-build",
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
Expand All @@ -24,4 +25,4 @@
"node": ">=18"
},
"name": "cuicui"
}
}
4 changes: 4 additions & 0 deletions packages/ui/categories-previews-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import application_ui_alert_preview from './cuicui/application-ui/alert/preview'
import application_ui_authentication_preview from './cuicui/application-ui/authentication/preview';
import application_ui_battery_preview from './cuicui/application-ui/battery/preview';
import application_ui_code_preview from './cuicui/application-ui/code/preview';
import application_ui_color_picker_preview from './cuicui/application-ui/color-picker/preview';
import application_ui_context_menu_preview from './cuicui/application-ui/context-menu/preview';
import application_ui_cookie_banner_preview from './cuicui/application-ui/cookie-banner/preview';
import application_ui_dropdown_menu_preview from './cuicui/application-ui/dropdown-menu/preview';
Expand All @@ -27,6 +28,7 @@ import common_ui_loaders_preview from './cuicui/common-ui/loaders/preview';
import common_ui_navigation_preview from './cuicui/common-ui/navigation/preview';
import common_ui_skeletons_preview from './cuicui/common-ui/skeletons/preview';
import common_ui_toggle_preview from './cuicui/common-ui/toggle/preview';
import hooks_use_auto_scroll_preview from './cuicui/hooks/use-auto-scroll/preview';
import hooks_use_battery_preview from './cuicui/hooks/use-battery/preview';
import hooks_use_click_outside_preview from './cuicui/hooks/use-click-outside/preview';
import hooks_use_copy_to_clipboard_preview from './cuicui/hooks/use-copy-to-clipboard/preview';
Expand Down Expand Up @@ -81,6 +83,7 @@ export const categoriesPreviewsList = {
'authentication': application_ui_authentication_preview,
'battery': application_ui_battery_preview,
'code': application_ui_code_preview,
'color-picker': application_ui_color_picker_preview,
'context-menu': application_ui_context_menu_preview,
'cookie-banner': application_ui_cookie_banner_preview,
'dropdown-menu': application_ui_dropdown_menu_preview,
Expand All @@ -105,6 +108,7 @@ export const categoriesPreviewsList = {
'navigation': common_ui_navigation_preview,
'skeletons': common_ui_skeletons_preview,
'toggle': common_ui_toggle_preview,
'use-auto-scroll': hooks_use_auto_scroll_preview,
'use-battery': hooks_use_battery_preview,
'use-click-outside': hooks_use_click_outside_preview,
'use-copy-to-clipboard': hooks_use_copy_to_clipboard_preview,
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/cuicui/application-ui/application-ui.section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { notificationCategory } from "@/cuicui/application-ui/notification/categ
import { dropdownMenuCategory } from "@/cuicui/application-ui/dropdown-menu/category";
import { tableOfContentCategory } from "@/cuicui/application-ui/table-of-contents/table-of-contents.category";
import { contextMenuCategory } from "@/cuicui/application-ui/context-menu/category";
import { colorPickerCategory } from "@/cuicui/application-ui/color-picker/category";

export const applicationUiSection: MultiComponentSectionType = {
type: "multiple-component",
Expand All @@ -31,6 +32,7 @@ export const applicationUiSection: MultiComponentSectionType = {
authenticationCategory,
batteryCategory,
codeCategory,
colorPickerCategory,
contextMenuCategory,
cookieBannerCategory,
dropdownMenuCategory,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
"use client";

import { useState, useEffect, useRef } from "react";
import {} from "lucide-react";
import { cn } from "@/cuicui/utils/cn/cn";
import { StaticNoise } from "@/cuicui/other/creative-effects/animated-noise/static-noise";
import { DotsPattern } from "@/cuicui/other/patterns/dots-pattern/dots-pattern";

type Color = { value: string; gradient?: boolean };

const COLORS: Color[] = [
{ value: "linear-gradient(45deg, #ff9a9e, #fad0c4)", gradient: true },
{ value: "#6B6E8D" },
{
value: "linear-gradient(45deg, #ff9a9e, #fad0c4, #ffd1ff)",
gradient: true,
},
{ value: "linear-gradient(45deg, #f6d365, #fda085)", gradient: true },
{ value: "linear-gradient(45deg, #84fab0, #8fd3f4)", gradient: true },
{ value: "#79E7D0" },
{ value: "#7AA2F7" },
];

export const ArcColorPicker = ({
selectedColor,
setSelectedColor,
grainIntensity,
setGrainIntensity,
}: {
selectedColor: string;
setSelectedColor: (color: string) => void;
grainIntensity: number;
setGrainIntensity: (intensity: number) => void;
}) => {
const [hue, setHue] = useState(0);
const [opacity, setOpacity] = useState(100);
const sliderRef = useRef<HTMLDivElement>(null);

// Haptic feedback function
const vibrate = () => {
if (navigator.vibrate) {
navigator.vibrate(100);
}
};

// Handle color selection
const handleColorSelect = (color: Color) => {
setSelectedColor(color.value);
vibrate();
};
const handleIntensityChange = (intensity: number) => {
setGrainIntensity(intensity);
vibrate();
};

// Handle 2-axis slider movement
const handleSliderMove = (event: MouseEvent | TouchEvent) => {
if (sliderRef.current) {
const rect = sliderRef.current.getBoundingClientRect();
const clientX =
"touches" in event ? event.touches[0].clientX : event.clientX;
const clientY =
"touches" in event ? event.touches[0].clientY : event.clientY;
const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const y = Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));
setHue(Math.round(x * 360));
setOpacity(Math.round((1 - y) * 100));
vibrate();
}
};

// Update selected color based on hue and opacity
useEffect(() => {
setSelectedColor(`hsla(${hue}, 100%, 50%, ${opacity / 100})`);
}, [hue, opacity, setSelectedColor]);

return (
<div className="w-[400px] bg-white/80 dark:bg-neutral-900 backdrop-blur-xl rounded-3xl p-8 space-y-8 shadow-lg">
<PreviewColor selectedColor={selectedColor} intensity={grainIntensity} />

{/* 2-axis slider */}
<div
ref={sliderRef}
className="h-48 relative cursor-crosshair rounded-md overflow-hidden"
onMouseDown={(e) => {
handleSliderMove(e.nativeEvent);
const handleMouseMove = (e: MouseEvent) => handleSliderMove(e);
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}}
onTouchStart={(e) => {
handleSliderMove(e.nativeEvent);
const handleTouchMove = (e: TouchEvent) => handleSliderMove(e);
const handleTouchEnd = () => {
document.removeEventListener("touchmove", handleTouchMove);
document.removeEventListener("touchend", handleTouchEnd);
};
document.addEventListener("touchmove", handleTouchMove);
document.addEventListener("touchend", handleTouchEnd);
}}
// role="slider"
aria-valuetext={`Hue: ${hue}, Opacity: ${opacity}%`}
aria-valuemin={0}
aria-valuemax={360}
// tabIndex={0}
onKeyDown={(e) => {
const step = 5;
switch (e.key) {
case "ArrowLeft":
setHue((h) => Math.max(0, h - step));
break;
case "ArrowRight":
setHue((h) => Math.min(360, h + step));
break;
case "ArrowUp":
setOpacity((o) => Math.min(100, o + step));
break;
case "ArrowDown":
setOpacity((o) => Math.max(0, o - step));
break;
default:
break;
}
}}
>
<div
className="absolute inset-0 border-4 border-white/50 dark:border-black/50"
style={{
backgroundImage:
"linear-gradient(90deg, oklch(90% 0.10 0), oklch(90% 0.10 60), oklch(90% 0.10 120), oklch(90% 0.10 180), oklch(90% 0.10 240), oklch(90% 0.10 300), oklch(90% 0.10 360))",
maskImage: "linear-gradient(to bottom, white, transparent)",
}}
/>

<DotsPattern
className="absolute inset-0 fill-neutral-400/20"
width={8}
height={8}
/>
<StaticNoise
opacity={grainIntensity / 100 / 2}
backgroundSize="200px"
className="inset-0 absolute mix-blend-screen dark:mix-blend-multiply z-20"
/>
<div
className="absolute w-4 h-4 border-2 border-white dark:border-black rounded-full shadow-lg transform -translate-x-1/2 -translate-y-1/2"
style={{ left: `${(hue / 360) * 100}%`, top: `${100 - opacity}%` }}
/>
</div>

<ColorSwatches
handleColorSelect={handleColorSelect}
selectedColor={selectedColor}
/>
<GrainSlider
intensity={grainIntensity}
handleIntensityChange={handleIntensityChange}
/>
</div>
);
};
export const PreviewColor = ({
selectedColor,
intensity,
}: { selectedColor: string; intensity: number }) => {
return (
<div className="flex justify-center">
<div className="relative size-16 rounded-full border-white dark:border-neutral-950 shadow-neutral-400/50 dark:shadow-none shadow-xl overflow-hidden border-4">
<div
className="size-full opacity-50 z-10"
style={{ background: selectedColor }}
/>
<StaticNoise
opacity={intensity / 100}
backgroundSize="150px"
className="inset-0 absolute mix-blend-screen dark:mix-blend-multiply z-20"
/>
</div>
</div>
);
};

const ColorSwatches = ({
handleColorSelect,
selectedColor,
}: { handleColorSelect: (color: Color) => void; selectedColor: string }) => {
return (
<div className="relative">
<div className="flex justify-evenly w-full gap-2">
{COLORS.map((color, index) => (
<button
type="button"
key={`${index}-${color}`}
onClick={() => handleColorSelect(color)}
className={cn(
"size-8 rounded-full border-2 transition-transform hover:scale-110",
selectedColor === color.value
? "border-white dark:border-black shadow-lg"
: "border-transparent",
)}
style={{ background: color.value }}
/>
))}
</div>
<button
type="button"
className="absolute left-0 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600"
>
<span className="sr-only">Previous colors</span>‹
</button>
<button
type="button"
className="absolute right-0 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600"
>
<span className="sr-only">Next colors</span>›
</button>
</div>
);
};

const GrainSlider = ({
intensity,
handleIntensityChange,
}: {
intensity: number;
handleIntensityChange: (intensity: number) => void;
}) => {
return (
<div className="relative h-12 flex items-center">
<svg className="w-full h-8" viewBox="0 0 200 20">
<title>Intensity</title>
<path
d="M0 10 Q 20 20, 40 10 T 80 10 T 120 10 T 160 10 T 200 10"
fill="none"
stroke="currentColor"
className="text-neutral-300 dark:text-neutral-700"
strokeWidth="2"
/>
</svg>
<input
type="range"
min="0"
max="100"
value={intensity}
onChange={(e) => {
handleIntensityChange(Number(e.target.value));
}}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
<div
className="absolute top-1/2 -translate-y-1/2 w-7 h-12 border-2 border-neutral-400/20 bg-white dark:bg-neutral-700 rounded-full shadow-lg transition-all pointer-events-none"
style={{ left: `calc(${intensity}% - 16px)` }}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import PreviewArcColorPicker from "@/cuicui/application-ui/color-picker/arc-color-picker/preview.arc-color-picker";
import type { ComponentType } from "@/lib/types/component";

export const arcColorPickerComponent: ComponentType = {
name: "Arc Color Picker",
slug: "arc-color-picker",
description: "A color picker with an arc shape.",
variantList: [
{
component: PreviewArcColorPicker,
name: "Default",
slugPreviewFile: "preview.arc-color-picker",
slugComponentFile: "arc-color-picker",
},
],
};
export default arcColorPickerComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use client";
import { ArcColorPicker } from "@/cuicui/application-ui/color-picker/arc-color-picker/arc-color-picker";
import { useState } from "react";

export default function PreviewArcColorPicker() {
const [selectedColor, setSelectedColor] = useState("#f6d365");
const [grainIntensity, setGrainIntensity] = useState(50);

return (
<ArcColorPicker
selectedColor={selectedColor}
setSelectedColor={setSelectedColor}
grainIntensity={grainIntensity}
setGrainIntensity={setGrainIntensity}
/>
);
}
12 changes: 12 additions & 0 deletions packages/ui/cuicui/application-ui/color-picker/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import arcColorPickerComponent from "@/cuicui/application-ui/color-picker/arc-color-picker/component";
import type { CategoryType } from "@/lib/types/component";
import { PipetteIcon } from "lucide-react";

export const colorPickerCategory: CategoryType = {
slug: "color-picker",
name: "Color Picker",
description: "A collection of color pickers.",
icon: PipetteIcon,
releaseDateCategory: new Date("2021-11-28"),
componentList: [arcColorPickerComponent],
};
Loading