diff --git a/apps/website/src/changelogs/2024-11-28.mdx b/apps/website/src/changelogs/2024-11-28.mdx new file mode 100644 index 0000000..0699faa --- /dev/null +++ b/apps/website/src/changelogs/2024-11-28.mdx @@ -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`. \ No newline at end of file diff --git a/apps/website/src/changelogs/last-changelog-date.ts b/apps/website/src/changelogs/last-changelog-date.ts index 3251886..6dfe4ca 100644 --- a/apps/website/src/changelogs/last-changelog-date.ts +++ b/apps/website/src/changelogs/last-changelog-date.ts @@ -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"); diff --git a/package.json b/package.json index 3bc8d1f..dd77f36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "pre-build": "turbo pre-build", "build": "turbo build", "dev": "turbo dev", "lint": "turbo lint", @@ -24,4 +25,4 @@ "node": ">=18" }, "name": "cuicui" -} +} \ No newline at end of file diff --git a/packages/ui/categories-previews-list.ts b/packages/ui/categories-previews-list.ts index f9dcc68..1b8a40f 100644 --- a/packages/ui/categories-previews-list.ts +++ b/packages/ui/categories-previews-list.ts @@ -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'; @@ -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'; @@ -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, @@ -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, diff --git a/packages/ui/cuicui/application-ui/application-ui.section.ts b/packages/ui/cuicui/application-ui/application-ui.section.ts index b70e93b..41cbfe6 100644 --- a/packages/ui/cuicui/application-ui/application-ui.section.ts +++ b/packages/ui/cuicui/application-ui/application-ui.section.ts @@ -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", @@ -31,6 +32,7 @@ export const applicationUiSection: MultiComponentSectionType = { authenticationCategory, batteryCategory, codeCategory, + colorPickerCategory, contextMenuCategory, cookieBannerCategory, dropdownMenuCategory, diff --git a/packages/ui/cuicui/application-ui/color-picker/arc-color-picker/arc-color-picker.tsx b/packages/ui/cuicui/application-ui/color-picker/arc-color-picker/arc-color-picker.tsx new file mode 100644 index 0000000..a307623 --- /dev/null +++ b/packages/ui/cuicui/application-ui/color-picker/arc-color-picker/arc-color-picker.tsx @@ -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(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 ( +
+ + + {/* 2-axis slider */} +
{ + 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; + } + }} + > +
+ + + +
+
+ + + +
+ ); +}; +export const PreviewColor = ({ + selectedColor, + intensity, +}: { selectedColor: string; intensity: number }) => { + return ( +
+
+
+ +
+
+ ); +}; + +const ColorSwatches = ({ + handleColorSelect, + selectedColor, +}: { handleColorSelect: (color: Color) => void; selectedColor: string }) => { + return ( +
+
+ {COLORS.map((color, index) => ( +
+ + +
+ ); +}; + +const GrainSlider = ({ + intensity, + handleIntensityChange, +}: { + intensity: number; + handleIntensityChange: (intensity: number) => void; +}) => { + return ( +
+ + Intensity + + + { + handleIntensityChange(Number(e.target.value)); + }} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> +
+
+ ); +}; diff --git a/packages/ui/cuicui/application-ui/color-picker/arc-color-picker/component.ts b/packages/ui/cuicui/application-ui/color-picker/arc-color-picker/component.ts new file mode 100644 index 0000000..2f80140 --- /dev/null +++ b/packages/ui/cuicui/application-ui/color-picker/arc-color-picker/component.ts @@ -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; diff --git a/packages/ui/cuicui/application-ui/color-picker/arc-color-picker/preview.arc-color-picker.tsx b/packages/ui/cuicui/application-ui/color-picker/arc-color-picker/preview.arc-color-picker.tsx new file mode 100644 index 0000000..857a45c --- /dev/null +++ b/packages/ui/cuicui/application-ui/color-picker/arc-color-picker/preview.arc-color-picker.tsx @@ -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 ( + + ); +} diff --git a/packages/ui/cuicui/application-ui/color-picker/category.ts b/packages/ui/cuicui/application-ui/color-picker/category.ts new file mode 100644 index 0000000..67516af --- /dev/null +++ b/packages/ui/cuicui/application-ui/color-picker/category.ts @@ -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], +}; diff --git a/packages/ui/cuicui/application-ui/color-picker/preview.tsx b/packages/ui/cuicui/application-ui/color-picker/preview.tsx new file mode 100644 index 0000000..e7fd751 --- /dev/null +++ b/packages/ui/cuicui/application-ui/color-picker/preview.tsx @@ -0,0 +1,28 @@ +import { PipetteIcon } from "lucide-react"; + +export default function ColorPickerCategoryPreview() { + return ( +
+ {/* Color Swatch 1 */} +
+ + {/* Floating Pipette */} + + + {/* Color Swatch 2 (Selected) */} +
+ {/* Selected Indicator */} +
+
+ + {/* Color Swatch 3 */} +
+ + {/* Color Swatch 4 */} +
+ + {/* Color Swatch 5 */} +
+
+ ); +} diff --git a/packages/ui/cuicui/hooks/hooks.section.ts b/packages/ui/cuicui/hooks/hooks.section.ts index c052c3f..f3ff1dd 100644 --- a/packages/ui/cuicui/hooks/hooks.section.ts +++ b/packages/ui/cuicui/hooks/hooks.section.ts @@ -31,6 +31,7 @@ import { useFirstVisitCategory } from "@/cuicui/hooks/use-first-visit/category.u import { useRerenderCategory } from "@/cuicui/hooks/use-rerender/component.use-rerender"; import { useMeasureCategory } from "@/cuicui/hooks/use-measure/category.use-measure"; import { useClickOutsideCategory } from "@/cuicui/hooks/use-click-outside/category.use-click-outside"; +import { useAutoScrollCategory } from "@/cuicui/hooks/use-auto-scroll/category"; export const hooksSection: SectionType = { type: "single-component", @@ -39,6 +40,7 @@ export const hooksSection: SectionType = { description: "A collection of React hooks for building modern applications.", icon: ToyBrickIcon, categoriesList: [ + useAutoScrollCategory, useBatteryCategory, useClickOutsideCategory, useCopyToClipboardCategory, diff --git a/packages/ui/cuicui/hooks/use-auto-scroll/category.ts b/packages/ui/cuicui/hooks/use-auto-scroll/category.ts new file mode 100644 index 0000000..a3fc2da --- /dev/null +++ b/packages/ui/cuicui/hooks/use-auto-scroll/category.ts @@ -0,0 +1,23 @@ +import { Scroll } from "lucide-react"; +import type { SingleComponentCategoryType } from "@/lib/types/component"; +import PreviewUseAutoScroll from "@/cuicui/hooks/use-auto-scroll/preview.use-auto-scroll"; + +export const useAutoScrollCategory: SingleComponentCategoryType = { + slug: "use-auto-scroll", + name: "Use Auto Scroll", + description: "A hook to automatically scroll a list", + releaseDateCategory: new Date("2024-09-16"), + icon: Scroll, + component: { + rerenderButton: true, + sizePreview: "lg", + variantList: [ + { + name: "variant 1", + component: PreviewUseAutoScroll, + slugComponentFile: "use-auto-scroll", + slugPreviewFile: "preview-use-auto-scroll", + }, + ], + }, +}; diff --git a/packages/ui/cuicui/hooks/use-auto-scroll/preview.tsx b/packages/ui/cuicui/hooks/use-auto-scroll/preview.tsx new file mode 100644 index 0000000..9acac2a --- /dev/null +++ b/packages/ui/cuicui/hooks/use-auto-scroll/preview.tsx @@ -0,0 +1,18 @@ +import { ArrowDown } from "lucide-react"; + +export default function AutoScrollChatCategoryPreview() { + return ( +
+ {/* Incoming Message */} +
+ + {/* Outgoing Message */} +
+ + {/* Auto Scroll Indicator */} +
+ +
+
+ ); +} diff --git a/packages/ui/cuicui/hooks/use-auto-scroll/preview.use-auto-scroll.tsx b/packages/ui/cuicui/hooks/use-auto-scroll/preview.use-auto-scroll.tsx new file mode 100644 index 0000000..78b41be --- /dev/null +++ b/packages/ui/cuicui/hooks/use-auto-scroll/preview.use-auto-scroll.tsx @@ -0,0 +1,99 @@ +// src/ChatListSimplified.tsx + +"use client"; + +import { useState, useEffect, useRef, type KeyboardEvent } from "react"; + +const AI_RESPONSES = [ + "I'm here to help with any questions you have.", + "Sint nisi eu cillum nulla officia incididunt irure laboris enim cillum cupidatat occaecat. Duis adipisicing veniam exercitation quis anim. Exercitation consectetur tempor et consectetur dolor. Cupidatat culpa eiusmod ex enim occaecat dolor sunt. Et et commodo qui ipsum nostrud ut et incididunt est cupidatat excepteur laborum. Anim ullamco aliqua ad sit sint cupidatat esse esse.", +]; + +interface Message { + sender: "user" | "ai"; + text: string; +} + +const PreviewUseAutoScroll = () => { + const [messages, setMessages] = useState([ + { sender: "ai", text: "Welcome to the chat!" }, + { sender: "ai", text: "Feel free to add new messages." }, + ]); + const [input, setInput] = useState(""); + const listRef = useRef(null); + + // Auto-scroll to the bottom when messages change + useEffect(() => { + if (listRef.current && messages.length > 0) { + listRef.current.scrollTop = listRef.current.scrollHeight; + } + }, [messages]); + + const sendMessage = () => { + const trimmedInput = input.trim(); + if (trimmedInput === "") { + return; + } + + const userMessage: Message = { sender: "user", text: trimmedInput }; + setMessages((prev) => [...prev, userMessage]); + setInput(""); + + // Simulate AI response + const aiResponse = + AI_RESPONSES[Math.floor(Math.random() * AI_RESPONSES.length)]; + setTimeout(() => { + setMessages((prev) => [...prev, { sender: "ai", text: aiResponse }]); + }, 500); + }; + + const handleKeyPress = (e: KeyboardEvent) => { + if (e.key === "Enter") { + sendMessage(); + } + }; + + return ( +
+

+ Chat Interface +

+
    + {messages.map((msg, index) => ( +
  • + {msg.text} +
  • + ))} +
+
+ setInput(e.target.value)} + onKeyDown={handleKeyPress} + placeholder="Type your message..." + className="flex-1 px-4 py-2 border border-neutral-400/20 rounded focus:outline-none focus:ring-2 focus:ring-blue-400" + /> + +
+
+ ); +}; + +export default PreviewUseAutoScroll; diff --git a/packages/ui/cuicui/hooks/use-auto-scroll/use-auto-scroll.ts b/packages/ui/cuicui/hooks/use-auto-scroll/use-auto-scroll.ts new file mode 100644 index 0000000..b16dd59 --- /dev/null +++ b/packages/ui/cuicui/hooks/use-auto-scroll/use-auto-scroll.ts @@ -0,0 +1,85 @@ +"use client"; +import { useEffect, useRef } from "react"; + +// biome-ignore lint/suspicious/noExplicitAny: +const useAutoScroll = (enabled: boolean, deps: any[]) => { + const listRef = useRef(null); + + useEffect(() => { + if (enabled && listRef.current) { + return autoScrollListRef(listRef.current); + } + }, [enabled, ...deps]); + + return listRef; +}; + +export default useAutoScroll; + +export function autoScrollListRef(list: HTMLUListElement) { + let shouldAutoScroll = true; + let touchStartY = 0; + let lastScrollTop = 0; + + const checkScrollPosition = () => { + const { scrollHeight, clientHeight, scrollTop } = list; + const maxScrollHeight = scrollHeight - clientHeight; + const scrollThreshold = maxScrollHeight / 2; + + if (scrollTop < lastScrollTop) { + shouldAutoScroll = false; + } else if (maxScrollHeight - scrollTop <= scrollThreshold) { + shouldAutoScroll = true; + } + + lastScrollTop = scrollTop; + }; + + const handleWheel = (e: WheelEvent) => { + if (e.deltaY < 0) { + shouldAutoScroll = false; + } else { + checkScrollPosition(); + } + }; + + const handleTouchStart = (e: TouchEvent) => { + touchStartY = e.touches[0].clientY; + }; + + const handleTouchMove = (e: TouchEvent) => { + const touchEndY = e.touches[0].clientY; + const deltaY = touchStartY - touchEndY; + + if (deltaY < 0) { + shouldAutoScroll = false; + } else { + checkScrollPosition(); + } + + touchStartY = touchEndY; + }; + + list.addEventListener("wheel", handleWheel); + list.addEventListener("touchstart", handleTouchStart); + list.addEventListener("touchmove", handleTouchMove); + + const observer = new MutationObserver(() => { + if (shouldAutoScroll) { + list.scrollTo({ top: list.scrollHeight }); + } + }); + + observer.observe(list, { + childList: true, + subtree: true, + characterData: true, + }); + + return () => { + observer.disconnect(); + list.removeEventListener("wheel", handleWheel); + list.removeEventListener("touchstart", handleTouchStart); + list.removeEventListener("touchmove", handleTouchMove); + }; +} diff --git a/turbo.json b/turbo.json index b338bd0..ed3c4cc 100644 --- a/turbo.json +++ b/turbo.json @@ -34,6 +34,11 @@ "dependsOn": [ "^start" ] + }, + "pre-build": { + "dependsOn": [ + "^pre-build" + ] } } } \ No newline at end of file