diff --git a/app/share/[id]/page.tsx b/app/share/[id]/page.tsx
index 393af46..5c41430 100644
--- a/app/share/[id]/page.tsx
+++ b/app/share/[id]/page.tsx
@@ -11,9 +11,8 @@ import TopReferrers from '@/components/dashboard/TopReferrers'
import Locations from '@/components/dashboard/Locations'
import TechSpecs from '@/components/dashboard/TechSpecs'
import PerformanceStats from '@/components/dashboard/PerformanceStats'
-import Select from '@/components/ui/Select'
+import { Select, DatePicker as DatePickerModal } from '@ciphera-net/ui'
import { LightningBoltIcon } from '@radix-ui/react-icons'
-import DatePickerModal from '@/components/ui/DatePicker'
// Helper to get date ranges
const getDateRange = (days: number) => {
diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx
index 1732a0d..bfe9c12 100644
--- a/app/sites/[id]/page.tsx
+++ b/app/sites/[id]/page.tsx
@@ -8,8 +8,7 @@ import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, get
import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format'
import { toast } from 'sonner'
import { LoadingOverlay } from '@ciphera-net/ui'
-import Select from '@/components/ui/Select'
-import DatePicker from '@/components/ui/DatePicker'
+import { Select, DatePicker } from '@ciphera-net/ui'
import ContentStats from '@/components/dashboard/ContentStats'
import TopReferrers from '@/components/dashboard/TopReferrers'
import Locations from '@/components/dashboard/Locations'
diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx
index c4b2d7b..86246c1 100644
--- a/app/sites/[id]/settings/page.tsx
+++ b/app/sites/[id]/settings/page.tsx
@@ -7,7 +7,7 @@ import { toast } from 'sonner'
import { LoadingOverlay } from '@ciphera-net/ui'
import VerificationModal from '@/components/sites/VerificationModal'
import { PasswordInput } from '@ciphera-net/ui'
-import Select from '@/components/ui/Select'
+import { Select } from '@ciphera-net/ui'
import { APP_URL, API_URL } from '@/lib/api/client'
import { generatePrivacySnippet } from '@/lib/utils/privacySnippet'
import { motion, AnimatePresence } from 'framer-motion'
diff --git a/components/dashboard/Chart.tsx b/components/dashboard/Chart.tsx
index 7020802..05bf02b 100644
--- a/components/dashboard/Chart.tsx
+++ b/components/dashboard/Chart.tsx
@@ -16,7 +16,7 @@ import type { TooltipProps } from 'recharts'
import { formatNumber, formatDuration } from '@/lib/utils/format'
import { ArrowTopRightIcon, ArrowBottomRightIcon, DownloadIcon, BarChartIcon } from '@radix-ui/react-icons'
import { Button } from '@ciphera-net/ui'
-import { Checkbox } from '@/components/ui/Checkbox'
+import { Checkbox } from '@ciphera-net/ui'
const COLORS = {
brand: '#FD5E0F',
diff --git a/components/dashboard/PerformanceStats.tsx b/components/dashboard/PerformanceStats.tsx
index c8e5fff..61eb397 100644
--- a/components/dashboard/PerformanceStats.tsx
+++ b/components/dashboard/PerformanceStats.tsx
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import { ChevronDownIcon } from '@radix-ui/react-icons'
import { PerformanceStats as Stats, PerformanceByPageStat, getPerformanceByPage } from '@/lib/api/stats'
-import Select from '@/components/ui/Select'
+import { Select } from '@ciphera-net/ui'
interface Props {
stats: Stats
diff --git a/components/dashboard/StatsCard.tsx b/components/dashboard/StatsCard.tsx
deleted file mode 100644
index 63cb8bc..0000000
--- a/components/dashboard/StatsCard.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-'use client'
-
-interface StatsCardProps {
- title: string
- value: string
-}
-
-export default function StatsCard({ title, value }: StatsCardProps) {
- return (
-
-
- {title}
-
-
- {value}
-
-
- )
-}
diff --git a/components/ui/Checkbox.tsx b/components/ui/Checkbox.tsx
deleted file mode 100644
index 2ee5fc3..0000000
--- a/components/ui/Checkbox.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import { CheckIcon } from '@radix-ui/react-icons';
-
-export interface CheckboxProps extends Omit, 'onChange'> {
- checked?: boolean;
- onCheckedChange?: (checked: boolean) => void;
- label?: React.ReactNode;
-}
-
-export const Checkbox = React.forwardRef(
- ({ className = '', checked, onCheckedChange, label, disabled, ...props }, ref) => {
- return (
-
- );
- }
-);
-
-Checkbox.displayName = 'Checkbox';
diff --git a/components/ui/DatePicker.tsx b/components/ui/DatePicker.tsx
deleted file mode 100644
index ee551a7..0000000
--- a/components/ui/DatePicker.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-import { ChevronLeftIcon, ChevronRightIcon, Cross2Icon } from '@radix-ui/react-icons'
-
-interface DateRange {
- start: string
- end: string
-}
-
-interface DatePickerProps {
- isOpen: boolean
- onClose: () => void
- onApply: (range: DateRange) => void
- initialRange: DateRange
-}
-
-export default function DatePicker({ isOpen, onClose, onApply, initialRange }: DatePickerProps) {
- const [startDate, setStartDate] = useState(new Date(initialRange.start))
- const [endDate, setEndDate] = useState(new Date(initialRange.end))
- const [currentMonth, setCurrentMonth] = useState(new Date(initialRange.end))
- const [selectingStart, setSelectingStart] = useState(true)
-
- useEffect(() => {
- if (isOpen) {
- setStartDate(new Date(initialRange.start))
- setEndDate(new Date(initialRange.end))
- setCurrentMonth(new Date(initialRange.end))
- }
- }, [isOpen, initialRange])
-
- if (!isOpen) return null
-
- const getDaysInMonth = (date: Date) => {
- const year = date.getFullYear()
- const month = date.getMonth()
- const days = new Date(year, month + 1, 0).getDate()
- const firstDay = new Date(year, month, 1).getDay()
- return { days, firstDay }
- }
-
- const { days, firstDay } = getDaysInMonth(currentMonth)
-
- const handleDateClick = (day: number) => {
- const clickedDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day)
-
- if (selectingStart) {
- setStartDate(clickedDate)
- // If clicked date is after current end date, reset end date
- if (clickedDate > endDate) {
- setEndDate(clickedDate)
- }
- setSelectingStart(false)
- } else {
- if (clickedDate < startDate) {
- setStartDate(clickedDate)
- setSelectingStart(false) // Keep selecting start effectively if they clicked before start
- } else {
- setEndDate(clickedDate)
- setSelectingStart(true) // Reset to start for next interaction or just done
- }
- }
- }
-
- const handleMonthChange = (increment: number) => {
- const newMonth = new Date(currentMonth)
- newMonth.setMonth(newMonth.getMonth() + increment)
- setCurrentMonth(newMonth)
- }
-
- const formatDate = (date: Date) => date.toISOString().split('T')[0]
-
- const isSelected = (day: number) => {
- const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day)
- return (
- date.getTime() === startDate.getTime() ||
- date.getTime() === endDate.getTime()
- )
- }
-
- const isInRange = (day: number) => {
- const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day)
- return date > startDate && date < endDate
- }
-
- const isToday = (day: number) => {
- const today = new Date()
- const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day)
- return (
- date.getDate() === today.getDate() &&
- date.getMonth() === today.getMonth() &&
- date.getFullYear() === today.getFullYear()
- )
- }
-
- return (
-
-
-
-
Select Date Range
-
-
-
-
-
-
- {currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
-
-
-
-
-
- {['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(day => (
-
{day}
- ))}
-
-
-
- {Array.from({ length: firstDay }).map((_, i) => (
-
- ))}
- {Array.from({ length: days }).map((_, i) => {
- const day = i + 1
- const selected = isSelected(day)
- const inRange = isInRange(day)
- const today = isToday(day)
-
- return (
-
- )
- })}
-
-
-
-
-
- {startDate.toLocaleDateString()}
-
- {' - '}
-
- {endDate.toLocaleDateString()}
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/components/ui/Select.tsx b/components/ui/Select.tsx
deleted file mode 100644
index 18bb326..0000000
--- a/components/ui/Select.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-'use client'
-
-import { useState, useRef, useEffect } from 'react'
-
-interface Option {
- value: string
- label: string
-}
-
-interface SelectProps {
- value: string
- onChange: (value: string) => void
- options: Option[]
- className?: string
- /** Form-field style (input-like). Default: toolbar style (btn-secondary). */
- variant?: 'default' | 'input'
- /** Shown when value is empty or does not match any option. */
- placeholder?: string
- /** Full-width trigger and panel. Use in form layouts. */
- fullWidth?: boolean
- /** Id for the trigger (e.g. for label htmlFor). */
- id?: string
- /** Alignment of the dropdown panel. */
- align?: 'left' | 'right'
-}
-
-export default function Select({
- value,
- onChange,
- options,
- className = '',
- variant = 'default',
- placeholder,
- fullWidth = false,
- id,
- align = 'right',
-}: SelectProps) {
- const [isOpen, setIsOpen] = useState(false)
- const ref = useRef(null)
-
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (ref.current && !ref.current.contains(event.target as Node)) {
- setIsOpen(false)
- }
- }
- document.addEventListener('mousedown', handleClickOutside)
- return () => document.removeEventListener('mousedown', handleClickOutside)
- }, [])
-
- const selectedOption = options.find((o) => o.value === value)
- const displayLabel = selectedOption?.label ?? placeholder ?? value ?? ''
-
- const triggerBase =
- variant === 'input'
- ? 'px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-neutral-50/50 dark:bg-neutral-900/50 text-neutral-900 dark:text-white text-left text-sm ' +
- 'focus:outline-none focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 transition-all duration-200 ' +
- (isOpen ? 'ring-4 ring-brand-orange/10 border-brand-orange' : '')
- : 'btn-secondary min-w-[140px]'
-
- const triggerLayout = fullWidth ? 'w-full ' : ''
- const alignClass = align === 'left' ? 'left-0' : 'right-0'
- const panelMinW = fullWidth ? 'w-full' : 'min-w-[140px] w-full'
-
- return (
-
-
-
- {isOpen && (
-
- {options.map((option) => (
-
- ))}
-
- )}
-
- )
-}
diff --git a/package-lock.json b/package-lock.json
index 0eeaedf..e8c7489 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"name": "pulse-frontend",
"version": "0.1.0",
"dependencies": {
- "@ciphera-net/ui": "^0.0.13",
+ "@ciphera-net/ui": "^0.0.14",
"@radix-ui/react-icons": "^1.3.2",
"axios": "^1.13.2",
"country-flag-icons": "^1.6.4",
@@ -268,9 +268,9 @@
}
},
"node_modules/@ciphera-net/ui": {
- "version": "0.0.13",
- "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.13/0bcb7f064f6212cf701f647bc07596530e9db9c9",
- "integrity": "sha512-edp2igwPpZDdwHol+jkqdRU3oDFxijyBQtLwjjuoZc2/hnGmHJRsnpKzd4Eo0tY1PuWrHv/QP4nDgYPG2819CQ==",
+ "version": "0.0.14",
+ "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.14/fb56b1bbd138eddc5a16d26c26d58524821f78c8",
+ "integrity": "sha512-IcQnp8pr7qsCU1QLKCUad7i+H0l/MykwHiu7pvbEON31PeFEJj8pdkXYnp+0ihRunWQ73G5Jik44AZtqHgNyFg==",
"dependencies": {
"@radix-ui/react-icons": "^1.3.0",
"clsx": "^2.1.0",
diff --git a/package.json b/package.json
index dc91ceb..2ac6828 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
- "@ciphera-net/ui": "^0.0.13",
+ "@ciphera-net/ui": "^0.0.14",
"@radix-ui/react-icons": "^1.3.2",
"axios": "^1.13.2",
"country-flag-icons": "^1.6.4",
diff --git a/styles/globals.css b/styles/globals.css
index 94264cb..4c3ece6 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -8,54 +8,13 @@
--color-success: #10B981;
--color-warning: #F59E0B;
--color-error: #EF4444;
-
- /* * Brand colors - Orange used as accent only */
- --color-brand-orange: #FD5E0F;
-
- /* * Neutral greys for UI */
- --color-neutral-50: #fafafa;
- --color-neutral-100: #f5f5f5;
- --color-neutral-200: #e5e5e5;
- --color-neutral-300: #d4d4d4;
- --color-neutral-400: #a3a3a3;
- --color-neutral-500: #737373;
- --color-neutral-600: #525252;
- --color-neutral-700: #404040;
- --color-neutral-800: #262626;
- --color-neutral-900: #171717;
-
- /* * Dark mode support */
- --color-bg: #ffffff;
- --color-text: #171717;
- }
-
- .dark {
- --color-bg: #0a0a0a;
- --color-text: #fafafa;
- }
-
- * {
- @apply border-neutral-200 dark:border-neutral-800 transition-colors duration-300 ease-in-out;
}
body {
- @apply bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-50 transition-colors duration-300 ease-in-out;
@apply bg-ciphera-gradient bg-fixed;
- font-family: var(--font-plus-jakarta-sans), system-ui, sans-serif;
}
.dark body {
@apply bg-ciphera-gradient-dark;
}
}
-
-@layer components {
- /* * Reusable component styles */
- .btn-primary {
- @apply bg-brand-orange text-white px-5 py-2.5 rounded-xl font-semibold shadow-sm shadow-orange-200 dark:shadow-none hover:shadow-orange-300 dark:hover:shadow-brand-orange/20 hover:-translate-y-0.5 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2 dark:focus:ring-offset-neutral-900;
- }
-
- .btn-secondary {
- @apply bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 text-neutral-900 dark:text-white px-5 py-2.5 rounded-xl font-medium hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all duration-200 shadow-sm hover:shadow-md dark:shadow-none focus:outline-none focus:ring-2 focus:ring-neutral-200 dark:focus:ring-neutral-700 focus:ring-offset-2 dark:focus:ring-offset-neutral-900;
- }
-}