Skip to content

Commit

Permalink
V0.3.7 - Color Picker & Use Auto Scroll (#62)
Browse files Browse the repository at this point in the history
* 🚀 add pre-build script to package.json and turbo.json

* 🚀 add color picker components and category to application UI

* 🚀 add use-auto-scroll hook and preview components to enhance chat functionality

* 📝 add changelog for Color Picker category and useAutoScroll hook
  • Loading branch information
damien-schneider authored Nov 28, 2024
1 parent b98fe32 commit 2ac92f1
Show file tree
Hide file tree
Showing 16 changed files with 586 additions and 2 deletions.
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

0 comments on commit 2ac92f1

Please sign in to comment.