shadcn-glass-ui 2.0.11 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +109 -5
- package/README.md +132 -43
- package/context7.json +2 -1
- package/dist/cli/index.cjs +1 -1
- package/dist/components.cjs +4 -4
- package/dist/components.d.ts +103 -29
- package/dist/components.js +1 -1
- package/dist/demo-screenshot-aurora.png +0 -0
- package/dist/demo-screenshot.png +0 -0
- package/dist/demo-screenshot.png.zip +0 -0
- package/dist/hooks.cjs +2 -2
- package/dist/index.cjs +5 -5
- package/dist/index.js +28 -28
- package/dist/index.js.map +1 -1
- package/dist/r/ai-card-glass.json +1 -1
- package/dist/r/avatar-glass.json +1 -1
- package/dist/r/badge-glass.json +1 -1
- package/dist/r/button-glass.json +1 -1
- package/dist/r/combobox-glass.json +1 -1
- package/dist/r/registry.json +2 -2
- package/dist/r/repository-card-glass.json +2 -1
- package/dist/r/slider-glass.json +4 -5
- package/dist/r/toggle-glass.json +2 -2
- package/dist/r/year-card-glass.json +1 -1
- package/dist/shadcn-glass-ui.css +1 -1
- package/dist/{theme-context-DNe_2vWJ.cjs → theme-context-BHXYJ4RE.cjs} +2 -2
- package/dist/{theme-context-DNe_2vWJ.cjs.map → theme-context-BHXYJ4RE.cjs.map} +1 -1
- package/dist/themes.cjs +1 -1
- package/dist/{trust-score-card-glass-Dgu46oWI.cjs → trust-score-card-glass-CGXmOIfq.cjs} +850 -150
- package/dist/trust-score-card-glass-CGXmOIfq.cjs.map +1 -0
- package/dist/{trust-score-card-glass-A7kas5OS.js → trust-score-card-glass-L9g0qamo.js} +1182 -482
- package/dist/trust-score-card-glass-L9g0qamo.js.map +1 -0
- package/dist/{use-focus-BRkQtQCj.cjs → use-focus-CeNHOiBa.cjs} +2 -2
- package/dist/{use-focus-BRkQtQCj.cjs.map → use-focus-CeNHOiBa.cjs.map} +1 -1
- package/dist/{use-wallpaper-tint-CfShPBo2.cjs → use-wallpaper-tint-Bt2G3g1v.cjs} +2 -2
- package/dist/{use-wallpaper-tint-CfShPBo2.cjs.map → use-wallpaper-tint-Bt2G3g1v.cjs.map} +1 -1
- package/dist/{utils-BXN7AcRu.cjs → utils-LYxxWvUn.cjs} +2 -2
- package/dist/{utils-BXN7AcRu.cjs.map → utils-LYxxWvUn.cjs.map} +1 -1
- package/dist/utils.cjs +1 -1
- package/docs/ADVANCED_PATTERNS.md +584 -0
- package/docs/AI_USAGE.md +135 -611
- package/docs/BEST_PRACTICES.md +2 -2
- package/docs/BREAKING_CHANGES.md +239 -0
- package/docs/COMPONENTS_CATALOG.md +8 -8
- package/docs/EXPORTS_STRUCTURE.md +3 -3
- package/docs/GETTING_STARTED.md +13 -8
- package/docs/PUBLISHING.md +1 -1
- package/docs/REGISTRY_SUMMARY.md +2 -2
- package/docs/REGISTRY_USAGE.md +1 -1
- package/docs/api/README.md +11 -11
- package/docs/api/interfaces/BadgeGlassProps.md +21 -14
- package/docs/api/interfaces/ButtonGlassProps.md +37 -30
- package/package.json +4 -3
- package/dist/trust-score-card-glass-A7kas5OS.js.map +0 -1
- package/dist/trust-score-card-glass-Dgu46oWI.cjs.map +0 -1
- package/dist/vite.svg +0 -1
- package/docs/migration/modal-glass-compound-api.md +0 -458
- package/docs/migration/select-to-combobox.md +0 -386
- package/docs/migration/tabs-glass-compound-api.md +0 -579
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{
|
|
16
16
|
"path": "components/glass/composite/ai-card-glass.tsx",
|
|
17
17
|
"type": "registry:component",
|
|
18
|
-
"content": "// ========================================\n// AI CARD GLASS COMPONENT\n// AI summary card with feature list\n// ========================================\n\nimport { forwardRef } from 'react';\nimport { Sparkles, Check, Zap, Clock } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { ButtonGlass } from '../ui/button-glass';\nimport { InteractiveCard } from '../primitives';\nimport '@/glass-theme.css';\n\nexport interface AICardGlassProps extends React.HTMLAttributes<HTMLDivElement> {\n readonly onGenerate?: () => void;\n readonly features?: readonly string[];\n readonly estimatedTime?: string;\n}\n\nconst defaultFeatures: readonly string[] = [\n 'Code quality assessment',\n 'Architecture patterns',\n 'Best practices',\n];\n\nexport const AICardGlass = forwardRef<HTMLDivElement, AICardGlassProps>(\n (\n { onGenerate, features = defaultFeatures, estimatedTime = '~30 seconds', className, ...props },\n ref\n ) => {\n return (\n <InteractiveCard\n ref={ref}\n baseBg=\"var(--ai-card-bg)\"\n borderColor=\"var(--ai-card-border)\"\n hoverGlow=\"var(--ai-card-hover-glow)\"\n hoverLift\n blur=\"sm\"\n rounded=\"rounded-xl\"\n className={cn('w-full sm:w-56 md:w-64 p-3 md:p-4', className)}\n {...props}\n >\n <div\n className=\"flex items-center gap-1.5 md:gap-2 font-semibold text-xs md:text-sm mb-1.5 md:mb-2\"\n style={{ color: 'var(--text-accent)' }}\n >\n <Sparkles className=\"w-3.5 h-3.5 md:w-4 md:h-4\" />\n AI Summary\n </div>\n <p className=\"text-(length:--font-size-2xs) md:text-xs mb-1.5 md:mb-2 text-(--text-secondary)\">\n Get comprehensive analysis:\n </p>\n <ul className=\"text-(length:--font-size-2xs) md:text-xs space-y-0.5 md:space-y-1 mb-2 md:mb-3\">\n {features.map((feature, i) => (\n <li\n key={`feature-${i}`}\n className=\"flex items-center gap-1\"\n style={{ color: 'var(--text-muted)' }}\n >\n <Check\n className=\"w-2.5 h-2.5 md:w-3 md:h-3\"\n style={{ color: 'var(--status-online)' }}\n />\n {feature}\n </li>\n ))}\n </ul>\n <ButtonGlass variant=\"
|
|
18
|
+
"content": "// ========================================\n// AI CARD GLASS COMPONENT\n// AI summary card with feature list\n// ========================================\n\nimport { forwardRef } from 'react';\nimport { Sparkles, Check, Zap, Clock } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { ButtonGlass } from '../ui/button-glass';\nimport { InteractiveCard } from '../primitives';\nimport '@/glass-theme.css';\n\nexport interface AICardGlassProps extends React.HTMLAttributes<HTMLDivElement> {\n readonly onGenerate?: () => void;\n readonly features?: readonly string[];\n readonly estimatedTime?: string;\n}\n\nconst defaultFeatures: readonly string[] = [\n 'Code quality assessment',\n 'Architecture patterns',\n 'Best practices',\n];\n\nexport const AICardGlass = forwardRef<HTMLDivElement, AICardGlassProps>(\n (\n { onGenerate, features = defaultFeatures, estimatedTime = '~30 seconds', className, ...props },\n ref\n ) => {\n return (\n <InteractiveCard\n ref={ref}\n baseBg=\"var(--ai-card-bg)\"\n borderColor=\"var(--ai-card-border)\"\n hoverGlow=\"var(--ai-card-hover-glow)\"\n hoverLift\n blur=\"sm\"\n rounded=\"rounded-xl\"\n className={cn('w-full sm:w-56 md:w-64 p-3 md:p-4', className)}\n {...props}\n >\n <div\n className=\"flex items-center gap-1.5 md:gap-2 font-semibold text-xs md:text-sm mb-1.5 md:mb-2\"\n style={{ color: 'var(--text-accent)' }}\n >\n <Sparkles className=\"w-3.5 h-3.5 md:w-4 md:h-4\" />\n AI Summary\n </div>\n <p className=\"text-(length:--font-size-2xs) md:text-xs mb-1.5 md:mb-2 text-(--text-secondary)\">\n Get comprehensive analysis:\n </p>\n <ul className=\"text-(length:--font-size-2xs) md:text-xs space-y-0.5 md:space-y-1 mb-2 md:mb-3\">\n {features.map((feature, i) => (\n <li\n key={`feature-${i}`}\n className=\"flex items-center gap-1\"\n style={{ color: 'var(--text-muted)' }}\n >\n <Check\n className=\"w-2.5 h-2.5 md:w-3 md:h-3\"\n style={{ color: 'var(--status-online)' }}\n />\n {feature}\n </li>\n ))}\n </ul>\n <ButtonGlass variant=\"default\" size=\"sm\" icon={Zap} onClick={onGenerate} className=\"w-full\">\n Generate Report\n </ButtonGlass>\n <p className=\"text-(length:--font-size-2xs) md:text-xs mt-1.5 md:mt-2 text-center flex items-center justify-center gap-1 text-(--text-muted)\">\n <Clock className=\"w-2.5 h-2.5 md:w-3 md:h-3\" />\n {estimatedTime}\n </p>\n </InteractiveCard>\n );\n }\n);\n\nAICardGlass.displayName = 'AICardGlass';\n"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"categories": [
|
package/dist/r/avatar-glass.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
{
|
|
18
18
|
"path": "components/glass/ui/avatar-glass.tsx",
|
|
19
19
|
"type": "registry:component",
|
|
20
|
-
"content": "/**\n * AvatarGlass Component\n *\n * Glass-themed avatar with:\n * - Theme-aware styling (glass/light/aurora)\n * - Glow effect on hover\n * - Optional glow-pulse animation\n * - Status indicator with glow\n * - Size variants\n * - Built on Radix UI primitives\n *\n * @example Compound API (recommended)\n * ```tsx\n * <AvatarGlass size=\"
|
|
20
|
+
"content": "/**\n * AvatarGlass Component\n *\n * Glass-themed avatar with:\n * - Theme-aware styling (glass/light/aurora)\n * - Glow effect on hover\n * - Optional glow-pulse animation\n * - Status indicator with glow\n * - Size variants\n * - Built on Radix UI primitives\n *\n * @example Compound API (recommended)\n * ```tsx\n * <AvatarGlass size=\"default\">\n * <AvatarGlassImage src=\"/avatar.jpg\" alt=\"User\" />\n * <AvatarGlassFallback>JD</AvatarGlassFallback>\n * </AvatarGlass>\n * ```\n *\n * @example With status indicator\n * ```tsx\n * <AvatarGlass size=\"lg\" status=\"online\">\n * <AvatarGlassImage src=\"/avatar.jpg\" alt=\"User\" />\n * <AvatarGlassFallback>JD</AvatarGlassFallback>\n * </AvatarGlass>\n * ```\n *\n * @example With glow animation\n * ```tsx\n * <AvatarGlass size=\"xl\" glowing>\n * <AvatarGlassImage src=\"/avatar.jpg\" alt=\"User\" />\n * <AvatarGlassFallback>JD</AvatarGlassFallback>\n * </AvatarGlass>\n * ```\n */\n\n'use client';\n\nimport * as React from 'react';\nimport * as AvatarPrimitive from '@radix-ui/react-avatar';\nimport { cn } from '@/lib/utils';\nimport { useHover } from '@/lib/hooks/use-hover';\nimport { avatarSizes, statusSizes } from '@/lib/variants/avatar-glass-variants';\nimport '@/glass-theme.css';\n\n// ========================================\n// TYPES\n// ========================================\n\nexport type AvatarStatus = 'online' | 'offline' | 'busy' | 'away';\nexport type AvatarSize = 'sm' | 'md' | 'lg' | 'xl';\n\n// ========================================\n// HELPERS\n// ========================================\n\nconst getStatusVars = (statusType: AvatarStatus): { bg: string; glow: string } => {\n const statusVars: Record<AvatarStatus, { bg: string; glow: string }> = {\n online: { bg: 'var(--status-online)', glow: 'var(--status-online-glow)' },\n offline: { bg: 'var(--status-offline)', glow: 'none' },\n busy: { bg: 'var(--status-busy)', glow: 'var(--status-busy-glow)' },\n away: { bg: 'var(--status-away)', glow: 'var(--status-away-glow)' },\n };\n return statusVars[statusType];\n};\n\n// ========================================\n// CONTEXT\n// ========================================\n\ninterface AvatarGlassContextValue {\n size: AvatarSize;\n status?: AvatarStatus;\n glowing?: boolean;\n}\n\nconst AvatarGlassContext = React.createContext<AvatarGlassContextValue>({\n size: 'md',\n});\n\n// ========================================\n// COMPOUND COMPONENT: ROOT\n// ========================================\n\ninterface AvatarGlassRootProps extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> {\n size?: AvatarSize;\n status?: AvatarStatus;\n glowing?: boolean;\n}\n\nconst AvatarGlassRoot = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Root>,\n AvatarGlassRootProps\n>(({ className, size = 'md', status, glowing = false, children, ...props }, ref) => {\n const { isHovered, hoverProps } = useHover();\n\n const avatarStyles: React.CSSProperties = {\n background: 'var(--avatar-bg)',\n border: '3px solid var(--avatar-border)',\n boxShadow: isHovered ? 'var(--avatar-hover-glow)' : 'var(--avatar-shadow)',\n color: 'var(--text-inverse)',\n };\n\n return (\n <AvatarGlassContext.Provider value={{ size, status, glowing }}>\n <div\n className={cn('relative inline-flex', className)}\n onMouseEnter={hoverProps.onMouseEnter}\n onMouseLeave={hoverProps.onMouseLeave}\n >\n <AvatarPrimitive.Root\n ref={ref}\n className={cn(\n avatarSizes({ size }),\n glowing && 'animate-[glow-pulse_2s_ease-in-out_infinite]'\n )}\n style={avatarStyles}\n {...props}\n >\n {children}\n </AvatarPrimitive.Root>\n\n {/* Status indicator */}\n {status && (\n <span\n className={cn(statusSizes({ size }))}\n style={{\n background: getStatusVars(status).bg,\n boxShadow: getStatusVars(status).glow,\n }}\n role=\"status\"\n aria-label={`Status: ${status}`}\n />\n )}\n </div>\n </AvatarGlassContext.Provider>\n );\n});\n\nAvatarGlassRoot.displayName = 'AvatarGlass';\n\n// ========================================\n// COMPOUND COMPONENT: IMAGE\n// ========================================\n\ntype AvatarGlassImageProps = React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>;\n\nconst AvatarGlassImage = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Image>,\n AvatarGlassImageProps\n>(({ className, ...props }, ref) => {\n return (\n <AvatarPrimitive.Image\n ref={ref}\n className={cn('aspect-square h-full w-full object-cover', className)}\n {...props}\n />\n );\n});\n\nAvatarGlassImage.displayName = 'AvatarGlassImage';\n\n// ========================================\n// COMPOUND COMPONENT: FALLBACK\n// ========================================\n\ntype AvatarGlassFallbackProps = React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>;\n\nconst AvatarGlassFallback = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Fallback>,\n AvatarGlassFallbackProps\n>(({ className, ...props }, ref) => {\n return (\n <AvatarPrimitive.Fallback\n ref={ref}\n className={cn(\n 'flex h-full w-full items-center justify-center font-semibold uppercase',\n className\n )}\n {...props}\n />\n );\n});\n\nAvatarGlassFallback.displayName = 'AvatarGlassFallback';\n\n// ========================================\n// HELPER FUNCTION (for simple use cases)\n// ========================================\n\nconst getInitials = (name: string): string => {\n if (!name || name.trim().length === 0) return '?';\n return name\n .split(' ')\n .map((part) => part[0])\n .join('')\n .toUpperCase()\n .slice(0, 2);\n};\n\n// ========================================\n// SIMPLE WRAPPER (backward compatibility)\n// ========================================\n\ninterface AvatarGlassSimpleProps {\n name: string;\n size?: AvatarSize;\n status?: AvatarStatus;\n glowing?: boolean;\n className?: string;\n}\n\nconst AvatarGlassSimple: React.FC<AvatarGlassSimpleProps> = ({\n name,\n size = 'md',\n status,\n glowing,\n className,\n}) => {\n return (\n <AvatarGlassRoot size={size} status={status} glowing={glowing} className={className}>\n <AvatarGlassFallback>{getInitials(name)}</AvatarGlassFallback>\n </AvatarGlassRoot>\n );\n};\n\n// ========================================\n// EXPORTS\n// ========================================\n\n// Compound API (shadcn/ui pattern)\nexport const AvatarGlass = AvatarGlassRoot;\nexport { AvatarGlassImage, AvatarGlassFallback };\n\n// Simple wrapper (backward compatibility)\nexport { AvatarGlassSimple };\n"
|
|
21
21
|
}
|
|
22
22
|
],
|
|
23
23
|
"categories": [
|
package/dist/r/badge-glass.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"path": "components/glass/ui/badge-glass.tsx",
|
|
18
18
|
"type": "registry:component",
|
|
19
|
-
"content": "/**\n * BadgeGlass Component\n *\n * Glass-themed badge with:\n * - Theme-aware styling via CSS variables (glass/light/aurora)\n * - shadcn/ui compatible variants (default, secondary, destructive, outline)\n * - Extended Glass UI variants (success, warning, info)\n * - Size options\n * - Optional animated dot\n */\n\nimport { forwardRef, type ReactNode, type CSSProperties } from 'react';\nimport { type VariantProps } from 'class-variance-authority';\nimport { cn } from '@/lib/utils';\nimport { badgeVariants, type BadgeVariant } from '@/lib/variants/badge-glass-variants';\nimport '@/glass-theme.css';\n\n// ========================================\n// CSS VARIABLE HELPERS\n// ========================================\n\ntype BadgeStyleVars = { bg: string; text: string; border: string };\n\nconst variantStyles: Record<BadgeVariant, BadgeStyleVars> = {\n // shadcn/ui compatible variants\n default: {\n bg: 'var(--badge-default-bg)',\n text: 'var(--badge-default-text)',\n border: 'var(--badge-default-border)',\n },\n secondary: {\n bg: 'var(--badge-secondary-bg)',\n text: 'var(--badge-secondary-text)',\n border: 'var(--badge-secondary-border)',\n },\n destructive: {\n bg: 'var(--badge-destructive-bg)',\n text: 'var(--badge-destructive-text)',\n border: 'var(--badge-destructive-border)',\n },\n outline: {\n bg: 'var(--badge-outline-bg)',\n text: 'var(--badge-outline-text)',\n border: 'var(--badge-outline-border)',\n },\n // Glass UI extended variants\n success: {\n bg: 'var(--badge-success-bg)',\n text: 'var(--badge-success-text)',\n border: 'var(--badge-success-border)',\n },\n warning: {\n bg: 'var(--badge-warning-bg)',\n text: 'var(--badge-warning-text)',\n border: 'var(--badge-warning-border)',\n },\n info: {\n bg: 'var(--badge-info-bg)',\n text: 'var(--badge-info-text)',\n border: 'var(--badge-info-border)',\n },\n};\n\nconst getBadgeStyles = (variant: BadgeVariant): CSSProperties => {\n const v = variantStyles[variant] || variantStyles.default;\n return {\n background: v.bg,\n color: v.text,\n border: `1px solid ${v.border}`,\n };\n};\n\n// ========================================\n// PROPS INTERFACE\n// ========================================\n\n/**\n * Props for the BadgeGlass component\n *\n * A glass-themed badge with semantic variants and optional animated status dot.\n * Features shadcn/ui compatible variants plus extended Glass UI variants.\n *\n * @accessibility\n * - **Keyboard Navigation:** Badges are non-interactive by default (display-only)\n * - **Focus Management:** N/A - badges do not receive focus unless wrapped in interactive elements\n * - **Screen Readers:** Semantic `<span>` element, content announced naturally\n * - **Status Indicators:** Use `aria-label` to provide context for status badges (e.g., \"Status: Active\")\n * - **Animated Dot:** Pulse animation respects `prefers-reduced-motion` settings\n * - **Touch Targets:** N/A for display badges, ensure 44x44px if wrapping in button/link (WCAG 2.5.5)\n * - **Color Contrast:** All variant text meets WCAG AA contrast ratio 4.5:1 against badge background\n * - **Motion:** Dot pulse animation can be disabled for users with motion sensitivity\n *\n * @example\n * ```tsx\n * // Basic badge with variant\n * <BadgeGlass variant=\"default\">New</BadgeGlass>\n *\n * // Status badge with aria-label for screen readers\n * <BadgeGlass variant=\"success\" aria-label=\"Status: Active\">\n * Active\n * </BadgeGlass>\n *\n * // Different variants (shadcn/ui compatible)\n * <BadgeGlass variant=\"default\">Default</BadgeGlass>\n * <BadgeGlass variant=\"secondary\">Secondary</BadgeGlass>\n * <BadgeGlass variant=\"destructive\">Error</BadgeGlass>\n * <BadgeGlass variant=\"outline\">Outline</BadgeGlass>\n *\n * // Extended Glass UI variants\n * <BadgeGlass variant=\"success\">Success</BadgeGlass>\n * <BadgeGlass variant=\"warning\">Warning</BadgeGlass>\n * <BadgeGlass variant=\"info\">Info</BadgeGlass>\n *\n * // With animated status dot\n * <BadgeGlass variant=\"success\" dot aria-label=\"Status: Online\">\n * Online\n * </BadgeGlass>\n * <BadgeGlass variant=\"destructive\" dot aria-label=\"Status: Offline\">\n * Offline\n * </BadgeGlass>\n *\n * // Size variants\n * <BadgeGlass size=\"sm\">Small</BadgeGlass>\n * <BadgeGlass size=\"
|
|
19
|
+
"content": "/**\n * BadgeGlass Component\n *\n * Glass-themed badge with:\n * - Theme-aware styling via CSS variables (glass/light/aurora)\n * - shadcn/ui compatible variants (default, secondary, destructive, outline)\n * - Extended Glass UI variants (success, warning, info)\n * - Size options\n * - Optional animated dot\n */\n\nimport { forwardRef, type ReactNode, type CSSProperties } from 'react';\nimport { type VariantProps } from 'class-variance-authority';\nimport { cn } from '@/lib/utils';\nimport { badgeVariants, type BadgeVariant } from '@/lib/variants/badge-glass-variants';\nimport '@/glass-theme.css';\n\n// ========================================\n// CSS VARIABLE HELPERS\n// ========================================\n\ntype BadgeStyleVars = { bg: string; text: string; border: string };\n\nconst variantStyles: Record<BadgeVariant, BadgeStyleVars> = {\n // shadcn/ui compatible variants\n default: {\n bg: 'var(--badge-default-bg)',\n text: 'var(--badge-default-text)',\n border: 'var(--badge-default-border)',\n },\n secondary: {\n bg: 'var(--badge-secondary-bg)',\n text: 'var(--badge-secondary-text)',\n border: 'var(--badge-secondary-border)',\n },\n destructive: {\n bg: 'var(--badge-destructive-bg)',\n text: 'var(--badge-destructive-text)',\n border: 'var(--badge-destructive-border)',\n },\n outline: {\n bg: 'var(--badge-outline-bg)',\n text: 'var(--badge-outline-text)',\n border: 'var(--badge-outline-border)',\n },\n // Glass UI extended variants\n success: {\n bg: 'var(--badge-success-bg)',\n text: 'var(--badge-success-text)',\n border: 'var(--badge-success-border)',\n },\n warning: {\n bg: 'var(--badge-warning-bg)',\n text: 'var(--badge-warning-text)',\n border: 'var(--badge-warning-border)',\n },\n info: {\n bg: 'var(--badge-info-bg)',\n text: 'var(--badge-info-text)',\n border: 'var(--badge-info-border)',\n },\n};\n\nconst getBadgeStyles = (variant: BadgeVariant): CSSProperties => {\n const v = variantStyles[variant] || variantStyles.default;\n return {\n background: v.bg,\n color: v.text,\n border: `1px solid ${v.border}`,\n };\n};\n\n// ========================================\n// PROPS INTERFACE\n// ========================================\n\n/**\n * Props for the BadgeGlass component\n *\n * A glass-themed badge with semantic variants and optional animated status dot.\n * Features shadcn/ui compatible variants plus extended Glass UI variants.\n *\n * @accessibility\n * - **Keyboard Navigation:** Badges are non-interactive by default (display-only)\n * - **Focus Management:** N/A - badges do not receive focus unless wrapped in interactive elements\n * - **Screen Readers:** Semantic `<span>` element, content announced naturally\n * - **Status Indicators:** Use `aria-label` to provide context for status badges (e.g., \"Status: Active\")\n * - **Animated Dot:** Pulse animation respects `prefers-reduced-motion` settings\n * - **Touch Targets:** N/A for display badges, ensure 44x44px if wrapping in button/link (WCAG 2.5.5)\n * - **Color Contrast:** All variant text meets WCAG AA contrast ratio 4.5:1 against badge background\n * - **Motion:** Dot pulse animation can be disabled for users with motion sensitivity\n *\n * @example\n * ```tsx\n * // Basic badge with variant\n * <BadgeGlass variant=\"default\">New</BadgeGlass>\n *\n * // Status badge with aria-label for screen readers\n * <BadgeGlass variant=\"success\" aria-label=\"Status: Active\">\n * Active\n * </BadgeGlass>\n *\n * // Different variants (shadcn/ui compatible)\n * <BadgeGlass variant=\"default\">Default</BadgeGlass>\n * <BadgeGlass variant=\"secondary\">Secondary</BadgeGlass>\n * <BadgeGlass variant=\"destructive\">Error</BadgeGlass>\n * <BadgeGlass variant=\"outline\">Outline</BadgeGlass>\n *\n * // Extended Glass UI variants\n * <BadgeGlass variant=\"success\">Success</BadgeGlass>\n * <BadgeGlass variant=\"warning\">Warning</BadgeGlass>\n * <BadgeGlass variant=\"info\">Info</BadgeGlass>\n *\n * // With animated status dot\n * <BadgeGlass variant=\"success\" dot aria-label=\"Status: Online\">\n * Online\n * </BadgeGlass>\n * <BadgeGlass variant=\"destructive\" dot aria-label=\"Status: Offline\">\n * Offline\n * </BadgeGlass>\n *\n * // Size variants\n * <BadgeGlass size=\"sm\">Small</BadgeGlass>\n * <BadgeGlass size=\"default\">Medium</BadgeGlass>\n * <BadgeGlass size=\"lg\">Large</BadgeGlass>\n *\n * // Inside interactive elements (ensure accessible labels)\n * <button aria-label=\"Filter by active status\">\n * Filter: <BadgeGlass variant=\"success\">Active</BadgeGlass>\n * </button>\n *\n * // Count badge with semantic meaning\n * <div>\n * <span>Notifications</span>\n * <BadgeGlass variant=\"destructive\" aria-label=\"3 unread notifications\">\n * 3\n * </BadgeGlass>\n * </div>\n * ```\n */\nexport interface BadgeGlassProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'style'>, VariantProps<typeof badgeVariants> {\n readonly children: ReactNode;\n readonly variant?: BadgeVariant;\n readonly dot?: boolean;\n}\n\n// ========================================\n// COMPONENT\n// ========================================\n\nexport const BadgeGlass = forwardRef<HTMLSpanElement, BadgeGlassProps>(\n ({ children, className, variant = 'default', size = 'md', dot, ...props }, ref) => {\n const v = variantStyles[variant];\n\n return (\n <span\n ref={ref}\n className={cn(badgeVariants({ size }), className)}\n style={getBadgeStyles(variant)}\n {...props}\n >\n {dot && (\n <span\n className=\"w-1 h-1 md:w-1.5 md:h-1.5 rounded-full animate-pulse\"\n style={{ background: v.text }}\n />\n )}\n {children}\n </span>\n );\n }\n);\n\nBadgeGlass.displayName = 'BadgeGlass';\n"
|
|
20
20
|
}
|
|
21
21
|
],
|
|
22
22
|
"categories": [
|
package/dist/r/button-glass.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
{
|
|
21
21
|
"path": "components/glass/ui/button-glass.tsx",
|
|
22
22
|
"type": "registry:component",
|
|
23
|
-
"content": "/**\n * ButtonGlass Component\n *\n * Glass-themed button with:\n * - Theme-aware styling via CSS variables (glass/light/aurora)\n * - Glow effects on hover\n * - Ripple effect on click (JS)\n * - Shine animation for primary variant (JS)\n * - Loading state with spinner\n * - Icon support (left/right position)\n */\n\nimport {\n forwardRef,\n useState,\n useCallback,\n useEffect,\n useRef,\n type MouseEvent,\n type CSSProperties,\n} from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { type VariantProps } from 'class-variance-authority';\nimport { RefreshCw, type LucideIcon } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { useHover } from '@/lib/hooks/use-hover';\nimport { useFocus } from '@/lib/hooks/use-focus';\nimport { buttonGlassVariants, type ButtonGlassVariant } from '@/lib/variants/button-glass-variants';\nimport { ICON_SIZES } from '@/components/glass/primitives';\nimport '@/glass-theme.css';\n\n// ========================================\n// CSS VARIABLE STYLE MAPS\n// ========================================\n\nconst getVariantStyles = (\n variant: ButtonGlassVariant,\n isHovered: boolean,\n isFocusVisible: boolean\n): CSSProperties => {\n const baseStyles: Record<ButtonGlassVariant, CSSProperties> = {\n primary: {\n background: isHovered ? 'var(--btn-primary-hover-bg)' : 'var(--btn-primary-bg)',\n color: 'var(--btn-primary-text)',\n border: 'none',\n boxShadow: isFocusVisible\n ? 'var(--focus-glow)'\n : isHovered\n ? 'var(--btn-primary-glow)'\n : 'var(--btn-primary-shadow)',\n },\n secondary: {\n background: isHovered ? 'var(--btn-secondary-hover-bg)' : 'var(--btn-secondary-bg)',\n color: 'var(--btn-secondary-text)',\n border: '1px solid var(--btn-secondary-border)',\n boxShadow: isFocusVisible\n ? 'var(--focus-glow)'\n : isHovered\n ? 'var(--btn-secondary-glow)'\n : 'none',\n },\n ghost: {\n background: isHovered ? 'var(--btn-ghost-hover-bg)' : 'var(--btn-ghost-bg)',\n color: 'var(--btn-ghost-text)',\n border: 'none',\n boxShadow: isFocusVisible ? 'var(--focus-glow)' : 'none',\n },\n destructive: {\n background: 'var(--btn-destructive-bg)',\n color: 'var(--btn-destructive-text)',\n border: 'none',\n boxShadow: isFocusVisible\n ? 'var(--focus-glow)'\n : isHovered\n ? 'var(--btn-destructive-glow)'\n : 'var(--btn-destructive-shadow)',\n },\n success: {\n background: 'var(--btn-success-bg)',\n color: 'var(--btn-success-text)',\n border: 'none',\n boxShadow: isFocusVisible\n ? 'var(--focus-glow)'\n : isHovered\n ? 'var(--btn-success-glow)'\n : 'var(--btn-success-shadow)',\n },\n text: {\n background: 'transparent',\n color: 'var(--text-secondary)',\n border: 'none',\n boxShadow: isFocusVisible ? 'var(--focus-glow)' : 'none',\n },\n };\n\n return baseStyles[variant];\n};\n\n// ========================================\n// PROPS INTERFACE\n// ========================================\n\n/**\n * Props for the ButtonGlass component\n *\n * A glass-themed button with ripple effects, loading states, and icon support.\n * Features theme-aware styling and hover animations.\n *\n * @accessibility\n * - **Keyboard Navigation:** Fully keyboard accessible with native `<button>` element\n * - **Focus Management:** Visible focus ring using `--focus-glow` CSS variable (WCAG 2.4.7)\n * - **Screen Readers:** Semantic `<button>` element, disabled state announced automatically\n * - **Loading State:** When loading=true, button is disabled and loading spinner is visible\n * - **Touch Targets:** Minimum 44x44px touch target (WCAG 2.5.5) via size variants\n * - **Color Contrast:** All variants meet WCAG AA contrast ratio 4.5:1 minimum\n * - **Motion:** Respects `prefers-reduced-motion` for ripple/shine animations\n *\n * @example\n * ```tsx\n * // Basic button\n * <ButtonGlass variant=\"primary\">Click me</ButtonGlass>\n *\n * // With icon and aria-label for icon-only buttons\n * <ButtonGlass icon={Check} iconPosition=\"left\">Save</ButtonGlass>\n * <ButtonGlass icon={X} size=\"icon\" aria-label=\"Close dialog\" />\n *\n * // Loading state (automatically disables and shows spinner)\n * <ButtonGlass loading aria-live=\"polite\">Processing...</ButtonGlass>\n *\n * // Different variants\n * <ButtonGlass variant=\"ghost\">Cancel</ButtonGlass>\n * <ButtonGlass variant=\"success\">Confirm</ButtonGlass>\n * <ButtonGlass variant=\"destructive\">Delete</ButtonGlass>\n *\n * // As a link (asChild pattern) - maintains semantic HTML\n * <ButtonGlass asChild variant=\"primary\">\n * <a href=\"/dashboard\">Go to Dashboard</a>\n * </ButtonGlass>\n *\n * // With Next.js Link\n * <ButtonGlass asChild variant=\"ghost\">\n * <Link href=\"/settings\">Settings</Link>\n * </ButtonGlass>\n *\n * // Form submit button\n * <ButtonGlass type=\"submit\" variant=\"primary\">\n * Submit Form\n * </ButtonGlass>\n * ```\n */\nexport interface ButtonGlassProps\n extends\n Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'style'>,\n VariantProps<typeof buttonGlassVariants> {\n /**\n * Render as child element instead of button (polymorphic rendering).\n * Useful for rendering buttons as links or other interactive elements.\n *\n * **Note:** When using `asChild`, decorative effects (ripple, shine, glow)\n * are disabled to maintain compatibility with Radix UI Slot.\n * Only styles and event handlers are passed to the child element.\n *\n * @default false\n * @example\n * ```tsx\n * <ButtonGlass asChild>\n * <a href=\"/about\">About Us</a>\n * </ButtonGlass>\n * ```\n */\n readonly asChild?: boolean;\n\n /**\n * Visual style variant of the button\n * @default \"primary\"\n */\n readonly variant?: ButtonGlassVariant;\n\n /**\n * Show loading spinner and disable interactions\n * @default false\n */\n readonly loading?: boolean;\n\n /**\n * Icon component from lucide-react to display\n * @example icon={Check}\n */\n readonly icon?: LucideIcon;\n\n /**\n * Position of the icon relative to button text\n * @default \"left\"\n */\n readonly iconPosition?: 'left' | 'right';\n\n /**\n * Size variant of the button\n * @default \"md\"\n */\n readonly size?: 'sm' | 'md' | 'lg' | 'icon';\n}\n\n// ========================================\n// COMPONENT\n// ========================================\n\nexport const ButtonGlass = forwardRef<HTMLButtonElement, ButtonGlassProps>(\n (\n {\n asChild = false,\n className,\n variant = 'primary',\n size = 'md',\n children,\n loading = false,\n disabled,\n icon: Icon,\n iconPosition = 'left',\n onClick,\n ...props\n },\n ref\n ) => {\n const { isHovered, hoverProps } = useHover();\n const { isFocusVisible, focusProps } = useFocus({ focusVisible: true });\n const [ripple, setRipple] = useState<{ x: number; y: number } | null>(null);\n\n const isDisabled = disabled || loading;\n const rippleTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n // Cleanup ripple timeout on unmount\n useEffect(() => {\n return () => {\n if (rippleTimeoutRef.current) {\n clearTimeout(rippleTimeoutRef.current);\n }\n };\n }, []);\n\n // Ripple effect handler\n const handleClick = useCallback(\n (e: MouseEvent<HTMLButtonElement>) => {\n if (isDisabled) return;\n\n // Create ripple effect\n const rect = e.currentTarget.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n setRipple({ x, y });\n\n // Clear previous timeout if exists\n if (rippleTimeoutRef.current) {\n clearTimeout(rippleTimeoutRef.current);\n }\n\n rippleTimeoutRef.current = setTimeout(() => {\n setRipple(null);\n rippleTimeoutRef.current = null;\n }, 600);\n\n onClick?.(e);\n },\n [isDisabled, onClick]\n );\n\n // Polymorphic component - render as Slot when asChild is true\n const Comp = asChild ? Slot : 'button';\n\n return (\n <Comp\n ref={ref}\n className={cn(\n buttonGlassVariants({ variant, size }),\n isHovered && !isDisabled && 'scale-[1.02]',\n className\n )}\n style={{\n ...getVariantStyles(variant, isHovered && !isDisabled, isFocusVisible && !isDisabled),\n outline: 'none',\n }}\n type={asChild ? undefined : 'button'}\n disabled={isDisabled}\n onClick={handleClick}\n onMouseEnter={hoverProps.onMouseEnter}\n onMouseLeave={hoverProps.onMouseLeave}\n onFocus={focusProps.onFocus}\n onBlur={focusProps.onBlur}\n {...props}\n >\n {/* When asChild is true, only render children (Slot expects a single child) */}\n {asChild ? (\n children\n ) : (\n <>\n {/* Shine effect on hover for primary */}\n {isHovered && variant === 'primary' && !isDisabled && (\n <div\n className=\"absolute inset-0 overflow-hidden pointer-events-none\"\n style={{ borderRadius: 'inherit' }}\n >\n <div\n className=\"absolute top-0 h-full w-1/3 bg-linear-to-r from-transparent via-white/20 to-transparent\"\n style={{ animation: 'btn-shine 1.5s ease-in-out infinite' }}\n />\n </div>\n )}\n\n {/* Ripple effect */}\n {ripple && (\n <span\n className=\"absolute rounded-full bg-white/30 pointer-events-none\"\n style={{\n left: ripple.x,\n top: ripple.y,\n width: 10,\n height: 10,\n transform: 'translate(-50%, -50%)',\n animation: 'ripple 0.6s ease-out',\n }}\n />\n )}\n\n {/* Pulsing glow on hover */}\n {isHovered && variant === 'primary' && !isDisabled && (\n <div\n className=\"absolute inset-0 rounded-xl animate-glow-pulse pointer-events-none\"\n style={{\n background: 'var(--btn-glow-radial)',\n }}\n />\n )}\n\n {/* Loading spinner */}\n {loading && <RefreshCw className={cn(ICON_SIZES.md, 'animate-spin')} />}\n\n {/* Icon left */}\n {!loading && Icon && iconPosition === 'left' && <Icon className={ICON_SIZES.md} />}\n\n {/* Content */}\n {!loading && children}\n\n {/* Icon right */}\n {!loading && Icon && iconPosition === 'right' && <Icon className={ICON_SIZES.md} />}\n </>\n )}\n </Comp>\n );\n }\n);\n\nButtonGlass.displayName = 'ButtonGlass';\n"
|
|
23
|
+
"content": "/**\n * ButtonGlass Component\n *\n * Glass-themed button with:\n * - Theme-aware styling via CSS variables (glass/light/aurora)\n * - Glow effects on hover\n * - Ripple effect on click (JS)\n * - Shine animation for primary variant (JS)\n * - Loading state with spinner\n * - Icon support (left/right position)\n */\n\nimport {\n forwardRef,\n useState,\n useCallback,\n useEffect,\n useRef,\n type MouseEvent,\n type CSSProperties,\n} from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { type VariantProps } from 'class-variance-authority';\nimport { RefreshCw, type LucideIcon } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { useHover } from '@/lib/hooks/use-hover';\nimport { useFocus } from '@/lib/hooks/use-focus';\nimport { buttonGlassVariants, type ButtonGlassVariant } from '@/lib/variants/button-glass-variants';\nimport { ICON_SIZES } from '@/components/glass/primitives';\nimport '@/glass-theme.css';\n\n// ========================================\n// CSS VARIABLE STYLE MAPS\n// ========================================\n\nconst getVariantStyles = (\n variant: ButtonGlassVariant,\n isHovered: boolean,\n isFocusVisible: boolean\n): CSSProperties => {\n const baseStyles: Record<ButtonGlassVariant, CSSProperties> = {\n default: {\n background: isHovered ? 'var(--btn-primary-hover-bg)' : 'var(--btn-primary-bg)',\n color: 'var(--btn-primary-text)',\n border: 'none',\n boxShadow: isFocusVisible\n ? 'var(--focus-glow)'\n : isHovered\n ? 'var(--btn-primary-glow)'\n : 'var(--btn-primary-shadow)',\n },\n secondary: {\n background: isHovered ? 'var(--btn-secondary-hover-bg)' : 'var(--btn-secondary-bg)',\n color: 'var(--btn-secondary-text)',\n border: '1px solid var(--btn-secondary-border)',\n boxShadow: isFocusVisible\n ? 'var(--focus-glow)'\n : isHovered\n ? 'var(--btn-secondary-glow)'\n : 'none',\n },\n ghost: {\n background: isHovered ? 'var(--btn-ghost-hover-bg)' : 'var(--btn-ghost-bg)',\n color: 'var(--btn-ghost-text)',\n border: 'none',\n boxShadow: isFocusVisible ? 'var(--focus-glow)' : 'none',\n },\n destructive: {\n background: 'var(--btn-destructive-bg)',\n color: 'var(--btn-destructive-text)',\n border: 'none',\n boxShadow: isFocusVisible\n ? 'var(--focus-glow)'\n : isHovered\n ? 'var(--btn-destructive-glow)'\n : 'var(--btn-destructive-shadow)',\n },\n outline: {\n background: isHovered ? 'var(--btn-outline-hover-bg)' : 'transparent',\n color: 'var(--btn-outline-text)',\n border: '1px solid var(--btn-outline-border)',\n boxShadow: isFocusVisible\n ? 'var(--focus-glow)'\n : isHovered\n ? 'var(--btn-outline-glow)'\n : 'none',\n },\n success: {\n background: 'var(--btn-success-bg)',\n color: 'var(--btn-success-text)',\n border: 'none',\n boxShadow: isFocusVisible\n ? 'var(--focus-glow)'\n : isHovered\n ? 'var(--btn-success-glow)'\n : 'var(--btn-success-shadow)',\n },\n link: {\n background: 'transparent',\n color: 'var(--btn-link-text)',\n border: 'none',\n boxShadow: isFocusVisible ? 'var(--focus-glow)' : 'none',\n textDecoration: isHovered ? 'underline' : 'none',\n },\n };\n\n return baseStyles[variant];\n};\n\n// ========================================\n// PROPS INTERFACE\n// ========================================\n\n/**\n * Props for the ButtonGlass component\n *\n * A glass-themed button with ripple effects, loading states, and icon support.\n * Features theme-aware styling and hover animations.\n *\n * **shadcn/ui compatible variants:**\n * - `default` - Primary action button with glow effects\n * - `secondary` - Secondary action with border\n * - `ghost` - Minimal visual presence\n * - `destructive` - Dangerous/delete actions (red)\n * - `outline` - Border with transparent background\n * - `link` - Text-only button with underline on hover\n * - `success` - Positive feedback (glass-ui extension)\n *\n * @accessibility\n * - **Keyboard Navigation:** Fully keyboard accessible with native `<button>` element\n * - **Focus Management:** Visible focus ring using `--focus-glow` CSS variable (WCAG 2.4.7)\n * - **Screen Readers:** Semantic `<button>` element, disabled state announced automatically\n * - **Loading State:** When loading=true, button is disabled and loading spinner is visible\n * - **Touch Targets:** Minimum 44x44px touch target (WCAG 2.5.5) via size variants\n * - **Color Contrast:** All variants meet WCAG AA contrast ratio 4.5:1 minimum\n * - **Motion:** Respects `prefers-reduced-motion` for ripple/shine animations\n *\n * @example\n * ```tsx\n * // Basic button (default variant)\n * <ButtonGlass>Click me</ButtonGlass>\n * <ButtonGlass variant=\"default\">Primary action</ButtonGlass>\n *\n * // With icon and aria-label for icon-only buttons\n * <ButtonGlass icon={Check} iconPosition=\"left\">Save</ButtonGlass>\n * <ButtonGlass icon={X} size=\"icon\" aria-label=\"Close dialog\" />\n *\n * // Loading state (automatically disables and shows spinner)\n * <ButtonGlass loading aria-live=\"polite\">Processing...</ButtonGlass>\n *\n * // Different variants (shadcn/ui compatible)\n * <ButtonGlass variant=\"secondary\">Secondary</ButtonGlass>\n * <ButtonGlass variant=\"ghost\">Cancel</ButtonGlass>\n * <ButtonGlass variant=\"outline\">Outline</ButtonGlass>\n * <ButtonGlass variant=\"destructive\">Delete</ButtonGlass>\n * <ButtonGlass variant=\"link\">Learn more</ButtonGlass>\n * <ButtonGlass variant=\"success\">Confirm</ButtonGlass>\n *\n * // As a link (asChild pattern) - maintains semantic HTML\n * <ButtonGlass asChild>\n * <a href=\"/dashboard\">Go to Dashboard</a>\n * </ButtonGlass>\n *\n * // With Next.js Link\n * <ButtonGlass asChild variant=\"ghost\">\n * <Link href=\"/settings\">Settings</Link>\n * </ButtonGlass>\n *\n * // Form submit button\n * <ButtonGlass type=\"submit\">\n * Submit Form\n * </ButtonGlass>\n * ```\n */\nexport interface ButtonGlassProps\n extends\n Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'style'>,\n VariantProps<typeof buttonGlassVariants> {\n /**\n * Render as child element instead of button (polymorphic rendering).\n * Useful for rendering buttons as links or other interactive elements.\n *\n * **Note:** When using `asChild`, decorative effects (ripple, shine, glow)\n * are disabled to maintain compatibility with Radix UI Slot.\n * Only styles and event handlers are passed to the child element.\n *\n * @default false\n * @example\n * ```tsx\n * <ButtonGlass asChild>\n * <a href=\"/about\">About Us</a>\n * </ButtonGlass>\n * ```\n */\n readonly asChild?: boolean;\n\n /**\n * Visual style variant of the button (shadcn/ui compatible)\n * @default \"default\"\n */\n readonly variant?: ButtonGlassVariant;\n\n /**\n * Show loading spinner and disable interactions\n * @default false\n */\n readonly loading?: boolean;\n\n /**\n * Icon component from lucide-react to display\n * @example icon={Check}\n */\n readonly icon?: LucideIcon;\n\n /**\n * Position of the icon relative to button text\n * @default \"left\"\n */\n readonly iconPosition?: 'left' | 'right';\n\n /**\n * Size variant of the button (shadcn/ui compatible)\n * @default \"default\"\n */\n readonly size?: 'default' | 'sm' | 'lg' | 'xl' | 'icon';\n}\n\n// ========================================\n// COMPONENT\n// ========================================\n\nexport const ButtonGlass = forwardRef<HTMLButtonElement, ButtonGlassProps>(\n (\n {\n asChild = false,\n className,\n variant = 'default',\n size = 'default',\n children,\n loading = false,\n disabled,\n icon: Icon,\n iconPosition = 'left',\n onClick,\n ...props\n },\n ref\n ) => {\n const { isHovered, hoverProps } = useHover();\n const { isFocusVisible, focusProps } = useFocus({ focusVisible: true });\n const [ripple, setRipple] = useState<{ x: number; y: number } | null>(null);\n\n const isDisabled = disabled || loading;\n const rippleTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n // Cleanup ripple timeout on unmount\n useEffect(() => {\n return () => {\n if (rippleTimeoutRef.current) {\n clearTimeout(rippleTimeoutRef.current);\n }\n };\n }, []);\n\n // Ripple effect handler\n const handleClick = useCallback(\n (e: MouseEvent<HTMLButtonElement>) => {\n if (isDisabled) return;\n\n // Create ripple effect\n const rect = e.currentTarget.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n setRipple({ x, y });\n\n // Clear previous timeout if exists\n if (rippleTimeoutRef.current) {\n clearTimeout(rippleTimeoutRef.current);\n }\n\n rippleTimeoutRef.current = setTimeout(() => {\n setRipple(null);\n rippleTimeoutRef.current = null;\n }, 600);\n\n onClick?.(e);\n },\n [isDisabled, onClick]\n );\n\n // Polymorphic component - render as Slot when asChild is true\n const Comp = asChild ? Slot : 'button';\n\n return (\n <Comp\n ref={ref}\n className={cn(\n buttonGlassVariants({ variant, size }),\n isHovered && !isDisabled && 'scale-[1.02]',\n className\n )}\n style={{\n ...getVariantStyles(variant, isHovered && !isDisabled, isFocusVisible && !isDisabled),\n outline: 'none',\n }}\n type={asChild ? undefined : 'button'}\n disabled={isDisabled}\n onClick={handleClick}\n onMouseEnter={hoverProps.onMouseEnter}\n onMouseLeave={hoverProps.onMouseLeave}\n onFocus={focusProps.onFocus}\n onBlur={focusProps.onBlur}\n {...props}\n >\n {/* When asChild is true, only render children (Slot expects a single child) */}\n {asChild ? (\n children\n ) : (\n <>\n {/* Shine effect on hover for default variant */}\n {isHovered && variant === 'default' && !isDisabled && (\n <div\n className=\"absolute inset-0 overflow-hidden pointer-events-none\"\n style={{ borderRadius: 'inherit' }}\n >\n <div\n className=\"absolute top-0 h-full w-1/3 bg-linear-to-r from-transparent via-white/20 to-transparent\"\n style={{ animation: 'btn-shine 1.5s ease-in-out infinite' }}\n />\n </div>\n )}\n\n {/* Ripple effect */}\n {ripple && (\n <span\n className=\"absolute rounded-full bg-white/30 pointer-events-none\"\n style={{\n left: ripple.x,\n top: ripple.y,\n width: 10,\n height: 10,\n transform: 'translate(-50%, -50%)',\n animation: 'ripple 0.6s ease-out',\n }}\n />\n )}\n\n {/* Pulsing glow on hover */}\n {isHovered && variant === 'default' && !isDisabled && (\n <div\n className=\"absolute inset-0 rounded-xl animate-glow-pulse pointer-events-none\"\n style={{\n background: 'var(--btn-glow-radial)',\n }}\n />\n )}\n\n {/* Loading spinner */}\n {loading && <RefreshCw className={cn(ICON_SIZES.md, 'animate-spin')} />}\n\n {/* Icon left */}\n {!loading && Icon && iconPosition === 'left' && <Icon className={ICON_SIZES.md} />}\n\n {/* Content */}\n {!loading && children}\n\n {/* Icon right */}\n {!loading && Icon && iconPosition === 'right' && <Icon className={ICON_SIZES.md} />}\n </>\n )}\n </Comp>\n );\n }\n);\n\nButtonGlass.displayName = 'ButtonGlass';\n"
|
|
24
24
|
}
|
|
25
25
|
],
|
|
26
26
|
"categories": [
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"path": "components/glass/ui/combobox-glass.tsx",
|
|
18
18
|
"type": "registry:component",
|
|
19
|
-
"content": "/**\n * ComboBoxGlass Component\n *\n * Glass-themed combobox (searchable select) with:\n * - Keyboard navigation support\n * - Search/filter functionality\n * - Custom rendering options\n * - Async data loading support\n * - Glass styling with theme support\n * - Accessibility features\n * - Form field wrapper support (label, error, success)\n * - Size variants (sm, md, lg, xl)\n * - Optional searchable mode\n * - Icon support for trigger and options\n */\n\n'use client';\n\nimport { useState, useMemo, useCallback, forwardRef, useId } from 'react';\nimport { CheckIcon, ChevronsUpDownIcon, type LucideIcon } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { ButtonGlass } from './button-glass';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from '@/components/ui/command';\nimport { FormFieldWrapper } from '../primitives/form-field-wrapper';\nimport { inputVariants, type InputGlassSize } from '@/lib/variants/input-glass-variants';\nimport {\n getDropdownContentStyles,\n dropdownContentClasses,\n getDropdownItemClasses,\n} from '@/lib/variants/dropdown-content-styles';\nimport { ICON_SIZES } from '../primitives/style-utils';\nimport '@/glass-theme.css';\n\n// ========================================\n// TYPES\n// ========================================\n\nexport type GlassVariant = 'glass' | 'frosted' | 'fluted' | 'crystal';\n\nexport interface ComboBoxOption<T = string> {\n readonly value: T;\n readonly label: string;\n readonly disabled?: boolean;\n /** Optional icon component for the option */\n readonly icon?: LucideIcon;\n}\n\nexport interface ComboBoxGlassProps<T = string> {\n /** Available options */\n readonly options: readonly ComboBoxOption<T>[];\n /** Currently selected value */\n readonly value?: T;\n /** Callback when value changes */\n readonly
|
|
19
|
+
"content": "/**\n * ComboBoxGlass Component\n *\n * Glass-themed combobox (searchable select) with:\n * - Keyboard navigation support\n * - Search/filter functionality\n * - Custom rendering options\n * - Async data loading support\n * - Glass styling with theme support\n * - Accessibility features\n * - Form field wrapper support (label, error, success)\n * - Size variants (sm, md, lg, xl)\n * - Optional searchable mode\n * - Icon support for trigger and options\n */\n\n'use client';\n\nimport { useState, useMemo, useCallback, forwardRef, useId } from 'react';\nimport { CheckIcon, ChevronsUpDownIcon, type LucideIcon } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { ButtonGlass } from './button-glass';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from '@/components/ui/command';\nimport { FormFieldWrapper } from '../primitives/form-field-wrapper';\nimport { inputVariants, type InputGlassSize } from '@/lib/variants/input-glass-variants';\nimport {\n getDropdownContentStyles,\n dropdownContentClasses,\n getDropdownItemClasses,\n} from '@/lib/variants/dropdown-content-styles';\nimport { ICON_SIZES } from '../primitives/style-utils';\nimport '@/glass-theme.css';\n\n// ========================================\n// TYPES\n// ========================================\n\nexport type GlassVariant = 'glass' | 'frosted' | 'fluted' | 'crystal';\n\nexport interface ComboBoxOption<T = string> {\n readonly value: T;\n readonly label: string;\n readonly disabled?: boolean;\n /** Optional icon component for the option */\n readonly icon?: LucideIcon;\n}\n\nexport interface ComboBoxGlassProps<T = string> {\n /** Available options */\n readonly options: readonly ComboBoxOption<T>[];\n /** Currently selected value */\n readonly value?: T;\n /** Callback when value changes */\n readonly onValueChange?: (value: T | undefined) => void;\n /** Placeholder text for trigger button */\n readonly placeholder?: string;\n /** Text shown when no results found */\n readonly emptyText?: string;\n /** Placeholder for search input */\n readonly searchPlaceholder?: string;\n /** Glass variant style */\n readonly glassVariant?: GlassVariant;\n /** Disabled state */\n readonly disabled?: boolean;\n /** Custom className for container */\n readonly className?: string;\n /** Custom className for popover content */\n readonly popoverClassName?: string;\n /** Allow clearing selection */\n readonly clearable?: boolean;\n /** Popover side */\n readonly side?: 'top' | 'right' | 'bottom' | 'left';\n /** Popover alignment */\n readonly align?: 'start' | 'center' | 'end';\n\n // ========================================\n // NEW PROPS (Week 3 Enhancement)\n // ========================================\n\n /** Label text displayed above the field */\n readonly label?: string;\n /** Error message - displays in red below the field */\n readonly error?: string;\n /** Success message - displays in green if no error */\n readonly success?: string;\n /** Shows required asterisk (*) next to label */\n readonly required?: boolean;\n /** Size variant (affects trigger button height and padding) */\n readonly size?: InputGlassSize;\n /** Enable/disable search functionality */\n readonly searchable?: boolean;\n /** Optional icon for trigger button (displayed before text) */\n readonly icon?: LucideIcon;\n}\n\n// ========================================\n// COMPONENT\n// ========================================\n\nfunction ComboBoxGlassInner<T = string>(\n {\n options,\n value,\n onValueChange,\n placeholder = 'Select option...',\n emptyText = 'No results found.',\n searchPlaceholder = 'Search...',\n glassVariant = 'glass',\n disabled = false,\n className,\n popoverClassName,\n clearable = false,\n side = 'bottom',\n align = 'start',\n // NEW PROPS\n label,\n error,\n success,\n required = false,\n size = 'md',\n searchable = true,\n icon: TriggerIcon,\n }: ComboBoxGlassProps<T>,\n ref: React.ForwardedRef<HTMLButtonElement>\n) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const fieldId = useId();\n\n // Find selected option\n const selectedOption = useMemo(\n () => options.find((opt) => opt.value === value),\n [options, value]\n );\n\n // Filter options based on search\n const filteredOptions = useMemo(() => {\n if (!search) return options;\n const searchLower = search.toLowerCase();\n return options.filter((opt) => opt.label.toLowerCase().includes(searchLower));\n }, [options, search]);\n\n // Handle option selection\n const handleSelect = useCallback(\n (optionValue: T) => {\n if (clearable && value === optionValue) {\n onValueChange?.(undefined);\n } else {\n onValueChange?.(optionValue);\n }\n setOpen(false);\n setSearch('');\n },\n [value, onValueChange, clearable]\n );\n\n // Get glass variant class\n const getGlassClass = () => {\n const variants: Record<GlassVariant, string> = {\n glass: 'glass',\n frosted: 'frosted',\n fluted: 'fluted',\n crystal: 'crystal',\n };\n return variants[glassVariant];\n };\n\n // Render content\n const comboboxContent = (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <ButtonGlass\n ref={ref}\n variant=\"secondary\"\n role=\"combobox\"\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n aria-label={selectedOption?.label || placeholder}\n aria-describedby={error ? `${fieldId}-error` : success ? `${fieldId}-success` : undefined}\n disabled={disabled}\n className={cn(\n 'w-full justify-between',\n // Apply size variant via inputVariants\n inputVariants({ size }),\n !selectedOption && 'text-muted-foreground',\n className\n )}\n >\n <span className=\"flex items-center gap-2 truncate\">\n {TriggerIcon && <TriggerIcon className={ICON_SIZES.md} />}\n {selectedOption ? selectedOption.label : placeholder}\n </span>\n <ChevronsUpDownIcon className={cn(ICON_SIZES.md, 'shrink-0 opacity-50')} />\n </ButtonGlass>\n </PopoverTrigger>\n <PopoverContent\n side={side}\n align={align}\n className={cn(\n dropdownContentClasses,\n 'w-[--radix-popover-trigger-width] p-0 border-0',\n getGlassClass(),\n popoverClassName\n )}\n style={getDropdownContentStyles()}\n >\n <Command\n shouldFilter={false}\n className={cn(\n 'bg-transparent',\n // Hide default border, use subtle divider instead\n '[&_[data-slot=command-input-wrapper]]:border-b-0',\n // Fix search icon - use glass theme color with better visibility\n '[&_[data-slot=command-input-wrapper]_svg]:text-[var(--text-muted)]',\n '[&_[data-slot=command-input-wrapper]_svg]:opacity-80'\n )}\n >\n {searchable && (\n <CommandInput\n placeholder={searchPlaceholder}\n value={search}\n onValueChange={setSearch}\n className=\"text-[var(--text-primary)] placeholder:text-[var(--text-muted)] h-10 font-medium\"\n />\n )}\n <CommandList className=\"p-1.5 scrollbar-hide\">\n <CommandEmpty className=\"text-[var(--text-muted)] py-4\">{emptyText}</CommandEmpty>\n <CommandGroup className=\"text-[var(--text-primary)] p-0\">\n {filteredOptions.map((option) => {\n const OptionIcon = option.icon;\n const isSelected = value === option.value;\n\n return (\n <CommandItem\n key={String(option.value)}\n value={String(option.value)}\n disabled={option.disabled}\n onSelect={() => handleSelect(option.value)}\n className={cn(\n getDropdownItemClasses({ selected: isSelected }),\n option.disabled && 'cursor-not-allowed opacity-50'\n )}\n >\n <CheckIcon\n className={cn(\n ICON_SIZES.md,\n 'shrink-0',\n isSelected ? 'opacity-100 text-[var(--text-accent)]' : 'opacity-0'\n )}\n />\n {OptionIcon && (\n <OptionIcon\n className={cn(ICON_SIZES.md, 'shrink-0 text-[var(--dropdown-icon)]')}\n />\n )}\n <span className=\"truncate\">{option.label}</span>\n </CommandItem>\n );\n })}\n </CommandGroup>\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n );\n\n // Wrap with FormFieldWrapper if label/error/success provided\n if (label || error || success) {\n return (\n <FormFieldWrapper\n label={label}\n error={error}\n success={success}\n required={required}\n htmlFor={fieldId}\n >\n {comboboxContent}\n </FormFieldWrapper>\n );\n }\n\n return comboboxContent;\n}\n\n// Export with generic type support\nexport const ComboBoxGlass = forwardRef(ComboBoxGlassInner) as <T = string>(\n props: ComboBoxGlassProps<T> & { ref?: React.ForwardedRef<HTMLButtonElement> }\n) => ReturnType<typeof ComboBoxGlassInner>;\n\n// Add display name for debugging\nComboBoxGlassInner.displayName = 'ComboBoxGlass';\n"
|
|
20
20
|
}
|
|
21
21
|
],
|
|
22
22
|
"categories": [
|
package/dist/r/registry.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"name": "toggle-glass",
|
|
14
14
|
"type": "registry:ui",
|
|
15
15
|
"title": "Toggle Glass",
|
|
16
|
-
"description": "Glass-themed toggle switch with:"
|
|
16
|
+
"description": "Glass-themed toggle switch with shadcn/ui compatible API:"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "tabs-glass",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"name": "slider-glass",
|
|
32
32
|
"type": "registry:ui",
|
|
33
33
|
"title": "Slider Glass",
|
|
34
|
-
"description": "Glass-themed range slider with:"
|
|
34
|
+
"description": "Glass-themed range slider built on Radix UI with shadcn/ui compatible API:"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"name": "skeleton-glass",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"title": "Repository Card Glass",
|
|
6
6
|
"description": "Repository Card Glass component with glass effects",
|
|
7
7
|
"dependencies": [
|
|
8
|
+
"lucide-react",
|
|
8
9
|
"react"
|
|
9
10
|
],
|
|
10
11
|
"registryDependencies": [
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
{
|
|
15
16
|
"path": "components/glass/composite/repository-card-glass.tsx",
|
|
16
17
|
"type": "registry:component",
|
|
17
|
-
"content": "// ========================================\n// REPOSITORY CARD GLASS COMPONENT\n// Expandable repository card with metrics\n// ========================================\n\nimport { forwardRef, type CSSProperties } from
|
|
18
|
+
"content": "// ========================================\n// REPOSITORY CARD GLASS COMPONENT\n// Expandable repository card with metrics\n// ========================================\n\nimport { forwardRef, type CSSProperties } from 'react';\nimport { ChevronDown, ChevronUp, Star, ExternalLink, Sparkles, AlertTriangle } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { StatusIndicatorGlass } from '../specialized/status-indicator-glass';\nimport { ButtonGlass } from '../ui/button-glass';\nimport { InteractiveCard } from '../primitives';\nimport '@/glass-theme.css';\n\nexport type RepositoryFlagType = 'green' | 'yellow' | 'red';\n\nexport interface RepositoryCardGlassProps extends React.HTMLAttributes<HTMLDivElement> {\n readonly name: string;\n readonly languages: string;\n readonly commits: number;\n readonly contribution: number;\n readonly stars?: number;\n readonly flagType?: RepositoryFlagType;\n readonly issues?: readonly string[];\n readonly expanded?: boolean;\n readonly onToggle?: () => void;\n readonly onGitHubClick?: () => void;\n readonly onAIAnalysisClick?: () => void;\n}\n\nexport const RepositoryCardGlass = forwardRef<HTMLDivElement, RepositoryCardGlassProps>(\n (\n {\n name,\n languages,\n commits,\n contribution,\n stars = 0,\n flagType = 'green',\n issues = [],\n expanded = false,\n onToggle,\n onGitHubClick,\n onAIAnalysisClick,\n className,\n ...props\n },\n ref\n ) => {\n // Calculate total project commits from contribution percentage\n const totalProjectCommits =\n contribution > 0 ? Math.round(commits / (contribution / 100)) : commits;\n const estimatedLines = Math.round(commits * 12);\n\n const expandedStyles: CSSProperties = {\n background: 'var(--expanded-bg)',\n borderColor: 'var(--card-border)',\n };\n\n const metricCardStyles: CSSProperties = {\n background: 'var(--card-bg)',\n borderColor: 'var(--card-border)',\n };\n\n return (\n <InteractiveCard\n ref={ref}\n baseBg=\"var(--card-bg)\"\n hoverBg=\"var(--card-hover-bg)\"\n borderColor=\"var(--card-border)\"\n hoverLift={false}\n blur=\"sm\"\n rounded=\"rounded-xl\"\n className={cn('overflow-hidden', className)}\n {...props}\n >\n {/* Main Card Content */}\n <div\n className=\"p-3 md:p-3.5 cursor-pointer\"\n onClick={onToggle}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onToggle?.();\n }\n }}\n aria-expanded={expanded}\n >\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2.5\">\n <span className=\"font-medium text-sm\" style={{ color: 'var(--text-primary)' }}>\n {name}\n </span>\n <StatusIndicatorGlass type={flagType} />\n </div>\n <div className=\"flex items-center gap-2\">\n {stars > 0 && (\n <span\n className=\"flex items-center gap-1 text-xs\"\n style={{ color: 'var(--status-away)' }}\n >\n <Star className=\"w-3 h-3\" />\n {stars}\n </span>\n )}\n {expanded ? (\n <ChevronUp className=\"w-4 h-4\" style={{ color: 'var(--text-muted)' }} />\n ) : (\n <ChevronDown className=\"w-4 h-4\" style={{ color: 'var(--text-muted)' }} />\n )}\n </div>\n </div>\n <div className=\"text-xs mt-1.5\" style={{ color: 'var(--text-muted)' }}>\n {languages}\n </div>\n <div className=\"text-xs mt-0.5\" style={{ color: 'var(--text-secondary)' }}>\n {commits} commits · {contribution}%\n </div>\n </div>\n\n {/* Expanded Section */}\n {expanded && (\n <div className=\"border-t p-3.5 space-y-3\" style={expandedStyles}>\n {/* Issues Alert */}\n {issues.length > 0 && (\n <div\n className=\"p-3 rounded-xl border\"\n style={{\n background: 'var(--alert-danger-bg)',\n borderColor: 'var(--alert-danger-border)',\n }}\n >\n <div\n className=\"text-xs font-semibold flex items-center gap-1.5 mb-1.5\"\n style={{ color: 'var(--alert-danger-text)' }}\n >\n <AlertTriangle className=\"w-3.5 h-3.5\" />\n Issues\n </div>\n {issues.map((issue, index) => (\n <div\n key={index}\n className=\"text-xs opacity-70\"\n style={{ color: 'var(--alert-danger-text)' }}\n >\n • {issue}\n </div>\n ))}\n </div>\n )}\n\n {/* Contribution Metrics */}\n <div className=\"grid grid-cols-2 gap-2\">\n <div className=\"p-2.5 rounded-lg border\" style={metricCardStyles}>\n <div className=\"text-xs\" style={{ color: 'var(--text-muted)' }}>\n Your Contribution\n </div>\n <div className=\"font-semibold\" style={{ color: 'var(--text-primary)' }}>\n {commits} commits\n </div>\n <div className=\"text-xs\" style={{ color: 'var(--text-muted)' }}>\n {contribution}%\n </div>\n </div>\n <div className=\"p-2.5 rounded-lg border\" style={metricCardStyles}>\n <div className=\"text-xs\" style={{ color: 'var(--text-muted)' }}>\n Full Project\n </div>\n <div className=\"font-semibold\" style={{ color: 'var(--text-primary)' }}>\n {totalProjectCommits} commits\n </div>\n <div className=\"text-xs\" style={{ color: 'var(--text-muted)' }}>\n ~{estimatedLines} lines\n </div>\n </div>\n </div>\n\n {/* Action Buttons */}\n <div className=\"flex flex-col sm:flex-row gap-2\">\n <ButtonGlass\n variant=\"secondary\"\n size=\"sm\"\n icon={ExternalLink}\n onClick={(e) => {\n e.stopPropagation();\n onGitHubClick?.();\n }}\n className=\"flex-1\"\n >\n GitHub\n </ButtonGlass>\n <ButtonGlass\n variant=\"default\"\n size=\"sm\"\n icon={Sparkles}\n onClick={(e) => {\n e.stopPropagation();\n onAIAnalysisClick?.();\n }}\n className=\"flex-1\"\n >\n AI Analysis\n </ButtonGlass>\n </div>\n </div>\n )}\n </InteractiveCard>\n );\n }\n);\n\nRepositoryCardGlass.displayName = 'RepositoryCardGlass';\n"
|
|
18
19
|
}
|
|
19
20
|
],
|
|
20
21
|
"categories": [
|
package/dist/r/slider-glass.json
CHANGED
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
"name": "slider-glass",
|
|
4
4
|
"type": "registry:ui",
|
|
5
5
|
"title": "Slider Glass",
|
|
6
|
-
"description": "Glass-themed range slider with:",
|
|
6
|
+
"description": "Glass-themed range slider built on Radix UI with shadcn/ui compatible API:",
|
|
7
7
|
"dependencies": [
|
|
8
|
+
"@radix-ui/react-slider",
|
|
8
9
|
"react"
|
|
9
10
|
],
|
|
10
11
|
"registryDependencies": [
|
|
11
12
|
"cn",
|
|
12
|
-
"primitives"
|
|
13
|
-
"use-focus",
|
|
14
|
-
"use-hover"
|
|
13
|
+
"primitives"
|
|
15
14
|
],
|
|
16
15
|
"files": [
|
|
17
16
|
{
|
|
18
17
|
"path": "components/glass/ui/slider-glass.tsx",
|
|
19
18
|
"type": "registry:component",
|
|
20
|
-
"content": "/**\n * SliderGlass Component\n *\n * Glass-themed range slider with:\n * - Theme-aware styling (glass/light/aurora)\n * - Glow effect on hover/drag\n * - Gradient fill (glass theme)\n * - Optional label and value display\n
|
|
19
|
+
"content": "/**\n * SliderGlass Component\n *\n * Glass-themed range slider built on Radix UI with shadcn/ui compatible API:\n * - Theme-aware styling (glass/light/aurora)\n * - Glow effect on hover/drag\n * - Gradient fill (glass theme)\n * - Range slider support (multiple thumbs)\n * - Optional label and value display\n *\n * **shadcn/ui compatible props:**\n * - `value` / `defaultValue` - Array of values (supports range)\n * - `onValueChange` - Callback when values change\n * - `onValueCommit` - Callback when interaction ends\n *\n * @example\n * ```tsx\n * // Single value (controlled)\n * <SliderGlass value={[50]} onValueChange={setValue} />\n *\n * // Range slider\n * <SliderGlass defaultValue={[25, 75]} />\n *\n * // With label and value display\n * <SliderGlass value={[50]} onValueChange={setValue} label=\"Volume\" showValue />\n * ```\n */\n\nimport { forwardRef, type CSSProperties, type ComponentPropsWithoutRef } from 'react';\nimport * as SliderPrimitive from '@radix-ui/react-slider';\nimport { cn } from '@/lib/utils';\nimport { FormFieldWrapper } from '@/components/glass/primitives';\nimport '@/glass-theme.css';\n\n// ========================================\n// PROPS INTERFACE\n// ========================================\n\nexport interface SliderGlassProps extends Omit<\n ComponentPropsWithoutRef<typeof SliderPrimitive.Root>,\n 'value' | 'defaultValue' | 'onValueChange'\n> {\n /**\n * Controlled value (shadcn/ui compatible - array for range support)\n */\n readonly value?: number[];\n /**\n * Default value for uncontrolled usage (array for range support)\n */\n readonly defaultValue?: number[];\n /**\n * Callback when value changes (shadcn/ui compatible)\n */\n readonly onValueChange?: (value: number[]) => void;\n /**\n * Callback when interaction ends (mouse up / touch end)\n */\n readonly onValueCommit?: (value: number[]) => void;\n /**\n * Show current value(s) next to label\n */\n readonly showValue?: boolean;\n /**\n * Optional label text\n */\n readonly label?: string;\n /**\n * Error message to display\n */\n readonly error?: string;\n /**\n * Success message to display\n */\n readonly success?: string;\n}\n\n// ========================================\n// COMPONENT\n// ========================================\n\nexport const SliderGlass = forwardRef<\n React.ComponentRef<typeof SliderPrimitive.Root>,\n SliderGlassProps\n>(\n (\n {\n className,\n value,\n defaultValue,\n onValueChange,\n onValueCommit,\n min = 0,\n max = 100,\n step = 1,\n showValue,\n label,\n error,\n success,\n disabled,\n orientation = 'horizontal',\n ...props\n },\n ref\n ) => {\n // Determine current values for rendering thumbs and display\n const currentValue = value ?? defaultValue ?? [min];\n\n // Format value display\n const formatValueDisplay = (values: number[]) => {\n if (values.length === 1) {\n return `${values[0]}`;\n }\n return `${values[0]} - ${values[values.length - 1]}`;\n };\n\n // Track styles via CSS variables\n const trackStyles: CSSProperties = {\n background: 'var(--slider-track)',\n };\n\n // Range (fill) styles via CSS variables\n const rangeStyles: CSSProperties = {\n background: 'var(--slider-fill)',\n };\n\n // Thumb styles via CSS variables\n const thumbStyles: CSSProperties = {\n background: 'var(--slider-thumb)',\n border: '2px solid var(--slider-thumb-border)',\n };\n\n // Custom label with value display - only used when showValue is true\n const customLabel =\n (label && showValue) || (!label && showValue) ? (\n <div className=\"flex justify-between mb-1.5 md:mb-2\">\n {label && (\n <label\n className=\"text-xs md:text-sm font-medium\"\n style={{ color: 'var(--text-secondary)' }}\n >\n {label}\n </label>\n )}\n <span\n className=\"text-xs md:text-sm font-medium tabular-nums\"\n style={{ color: 'var(--text-secondary)' }}\n >\n {formatValueDisplay(currentValue)}\n </span>\n </div>\n ) : undefined;\n\n return (\n <FormFieldWrapper\n label={showValue ? undefined : label}\n error={error}\n success={success}\n className={cn('w-full', className)}\n >\n {customLabel}\n <SliderPrimitive.Root\n ref={ref}\n value={value}\n defaultValue={defaultValue}\n onValueChange={onValueChange}\n onValueCommit={onValueCommit}\n min={min}\n max={max}\n step={step}\n disabled={disabled}\n orientation={orientation}\n className={cn(\n 'relative flex touch-none select-none',\n orientation === 'horizontal'\n ? 'w-full h-8 md:h-6 items-center'\n : 'flex-col h-full w-8 md:w-6 justify-center',\n disabled && 'opacity-50 cursor-not-allowed',\n 'group'\n )}\n {...props}\n >\n <SliderPrimitive.Track\n className={cn(\n 'relative grow rounded-full',\n orientation === 'horizontal' ? 'h-2.5 md:h-2 w-full' : 'w-2.5 md:w-2 h-full'\n )}\n style={trackStyles}\n >\n <SliderPrimitive.Range\n className={cn(\n 'absolute rounded-full transition-shadow duration-150',\n orientation === 'horizontal' ? 'h-full' : 'w-full',\n 'group-hover:shadow-(--slider-fill-glow)',\n 'group-active:shadow-(--slider-fill-glow)'\n )}\n style={rangeStyles}\n />\n </SliderPrimitive.Track>\n {currentValue.map((_, index) => (\n <SliderPrimitive.Thumb\n key={index}\n className={cn(\n 'block rounded-full shadow-md transition-all duration-150',\n 'w-6 h-6 md:w-5 md:h-5',\n 'hover:scale-105',\n 'focus-visible:outline-none focus-visible:shadow-(--focus-glow)',\n 'active:scale-110',\n disabled && 'pointer-events-none'\n )}\n style={thumbStyles}\n aria-label={\n label\n ? currentValue.length > 1\n ? `${label} thumb ${index + 1}`\n : label\n : `Slider thumb ${index + 1}`\n }\n />\n ))}\n </SliderPrimitive.Root>\n </FormFieldWrapper>\n );\n }\n);\n\nSliderGlass.displayName = 'SliderGlass';\n"
|
|
21
20
|
}
|
|
22
21
|
],
|
|
23
22
|
"categories": [
|
package/dist/r/toggle-glass.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "toggle-glass",
|
|
4
4
|
"type": "registry:ui",
|
|
5
5
|
"title": "Toggle Glass",
|
|
6
|
-
"description": "Glass-themed toggle switch with:",
|
|
6
|
+
"description": "Glass-themed toggle switch with shadcn/ui compatible API:",
|
|
7
7
|
"dependencies": [
|
|
8
8
|
"class-variance-authority",
|
|
9
9
|
"react"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
{
|
|
18
18
|
"path": "components/glass/ui/toggle-glass.tsx",
|
|
19
19
|
"type": "registry:component",
|
|
20
|
-
"content": "/**\n * ToggleGlass Component\n *\n * Glass-themed toggle switch with:\n * - Theme-aware styling (glass/light/aurora)\n * - Glow effect when active\n * - Size variants\n * - Optional label\n */\n\nimport { forwardRef, type CSSProperties } from 'react';\nimport { type VariantProps } from 'class-variance-authority';\nimport { cn } from '@/lib/utils';\nimport { useFocus } from '@/lib/hooks/use-focus';\nimport { toggleSizes } from '@/lib/variants/toggle-glass-variants';\nimport '@/glass-theme.css';\n\n// ========================================\n// SIZE CONFIG\n// ========================================\n\nconst sizesConfig = {\n sm: { track: 'w-8 h-4', knob: 'w-3 h-3', translate: 'translate-x-4' },\n
|
|
20
|
+
"content": "/**\n * ToggleGlass Component\n *\n * Glass-themed toggle switch with shadcn/ui compatible API:\n * - Theme-aware styling (glass/light/aurora)\n * - Glow effect when active\n * - Size variants (default, sm, lg)\n * - Variant styles (default, outline)\n * - Optional label\n *\n * **shadcn/ui compatible props:**\n * - `pressed` / `defaultPressed` - Control pressed state\n * - `onPressedChange` - Callback when state changes\n * - `variant` - 'default' | 'outline'\n * - `size` - 'default' | 'sm' | 'lg'\n *\n * @example\n * ```tsx\n * // Controlled\n * <ToggleGlass pressed={isOn} onPressedChange={setIsOn} />\n *\n * // Uncontrolled\n * <ToggleGlass defaultPressed={true} />\n *\n * // With variant\n * <ToggleGlass variant=\"outline\" pressed={isOn} onPressedChange={setIsOn} />\n * ```\n */\n\nimport { forwardRef, useState, type CSSProperties } from 'react';\nimport { type VariantProps } from 'class-variance-authority';\nimport { cn } from '@/lib/utils';\nimport { useFocus } from '@/lib/hooks/use-focus';\nimport { toggleSizes, type ToggleGlassVariant } from '@/lib/variants/toggle-glass-variants';\nimport '@/glass-theme.css';\n\n// ========================================\n// SIZE CONFIG\n// ========================================\n\nconst sizesConfig = {\n sm: { track: 'w-8 h-4', knob: 'w-3 h-3', translate: 'translate-x-4' },\n default: { track: 'w-11 h-6', knob: 'w-5 h-5', translate: 'translate-x-5' },\n lg: { track: 'w-14 h-7', knob: 'w-6 h-6', translate: 'translate-x-7' },\n} as const;\n\n// ========================================\n// PROPS INTERFACE\n// ========================================\n\nexport interface ToggleGlassProps\n extends\n Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange' | 'defaultValue'>,\n VariantProps<typeof toggleSizes> {\n /**\n * Controlled pressed state (shadcn/ui compatible)\n */\n readonly pressed?: boolean;\n /**\n * Default pressed state for uncontrolled usage\n */\n readonly defaultPressed?: boolean;\n /**\n * Callback when pressed state changes (shadcn/ui compatible)\n */\n readonly onPressedChange?: (pressed: boolean) => void;\n /**\n * Visual variant (shadcn/ui compatible)\n * @default \"default\"\n */\n readonly variant?: ToggleGlassVariant;\n /**\n * Optional label text\n */\n readonly label?: string;\n}\n\n// ========================================\n// COMPONENT\n// ========================================\n\nexport const ToggleGlass = forwardRef<HTMLButtonElement, ToggleGlassProps>(\n (\n {\n className,\n size = 'default',\n variant = 'default',\n pressed: controlledPressed,\n defaultPressed = false,\n onPressedChange,\n disabled,\n label,\n ...props\n },\n ref\n ) => {\n // Support both controlled and uncontrolled modes\n const [uncontrolledPressed, setUncontrolledPressed] = useState(defaultPressed);\n const isControlled = controlledPressed !== undefined;\n const isPressed = isControlled ? controlledPressed : uncontrolledPressed;\n\n const { isFocusVisible, focusProps } = useFocus({ focusVisible: true });\n const s = sizesConfig[size ?? 'default'];\n\n const handleToggle = () => {\n if (disabled) return;\n const newValue = !isPressed;\n if (!isControlled) {\n setUncontrolledPressed(newValue);\n }\n onPressedChange?.(newValue);\n };\n\n const getTrackStyles = (): CSSProperties => {\n if (variant === 'outline') {\n return {\n background: isPressed ? 'var(--toggle-outline-active-bg)' : 'transparent',\n borderColor: isPressed\n ? 'var(--toggle-outline-active-border)'\n : 'var(--toggle-outline-border)',\n boxShadow: isFocusVisible && !disabled ? 'var(--focus-glow)' : 'none',\n };\n }\n return {\n background: isPressed ? 'var(--toggle-active-bg)' : 'var(--toggle-bg)',\n boxShadow:\n isFocusVisible && !disabled\n ? 'var(--focus-glow)'\n : isPressed\n ? 'var(--toggle-glow)'\n : 'none',\n };\n };\n\n const knobStyles: CSSProperties = {\n background: 'var(--toggle-knob)',\n };\n\n // Touch area wrapper ensures 44px minimum touch target (Apple HIG)\n const toggle = (\n <span className=\"inline-flex items-center justify-center min-h-11\">\n <button\n ref={ref}\n type=\"button\"\n role=\"switch\"\n aria-pressed={isPressed}\n aria-label={label || 'Toggle switch'}\n disabled={disabled}\n className={cn(\n toggleSizes({ size, variant }),\n disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',\n !label && className\n )}\n style={getTrackStyles()}\n onClick={handleToggle}\n onFocus={focusProps.onFocus}\n onBlur={focusProps.onBlur}\n {...props}\n >\n <div\n className={cn(\n 'absolute top-0.5 left-0.5 rounded-full shadow-md transition-all duration-300',\n s.knob,\n isPressed && s.translate\n )}\n style={knobStyles}\n />\n </button>\n </span>\n );\n\n if (label) {\n return (\n <label\n className={cn(\n 'inline-flex items-center gap-2 md:gap-2.5',\n disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',\n className\n )}\n >\n {toggle}\n <span className=\"text-xs md:text-sm\" style={{ color: 'var(--text-secondary)' }}>\n {label}\n </span>\n </label>\n );\n }\n\n return toggle;\n }\n);\n\nToggleGlass.displayName = 'ToggleGlass';\n"
|
|
21
21
|
}
|
|
22
22
|
],
|
|
23
23
|
"categories": [
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"path": "components/glass/composite/year-card-glass.tsx",
|
|
18
18
|
"type": "registry:component",
|
|
19
|
-
"content": "// ========================================\n// YEAR CARD GLASS COMPONENT\n// Year card for career timeline / analytics\n// Domain-specific composite component following shadcn/ui patterns\n// ========================================\n\nimport { forwardRef, type CSSProperties, type ReactNode } from 'react';\nimport { ChevronDown, ChevronUp } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { BadgeGlass } from '../ui/badge-glass';\nimport { ProgressGlass } from '../specialized/progress-glass';\nimport { SparklineGlass } from '../specialized/sparkline-glass';\nimport { ButtonGlass } from '../ui/button-glass';\nimport { InteractiveCard } from '../primitives';\nimport { InsightCardGlass } from '../atomic/insight-card-glass';\nimport type { ProgressGradient } from '@/lib/variants/progress-glass-variants';\nimport '@/glass-theme.css';\n\n// ========================================\n// TYPES\n// ========================================\n\nexport interface YearCardGlassInsight {\n readonly variant?: 'default' | 'tip' | 'highlight' | 'warning' | 'stat' | 'growth' | 'decline';\n readonly emoji?: string;\n readonly text: string;\n readonly detail?: string;\n}\n\nexport interface YearCardGlassStat {\n readonly label: string;\n readonly value: string | number;\n readonly icon?: ReactNode;\n}\n\nexport interface YearCardGlassProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Year to display (string or number) */\n readonly year: string | number;\n /** Emoji for the badge */\n readonly emoji: string;\n /** Label for the badge (e.g., \"Peak Year\") */\n readonly label: string;\n /** Primary metric value (e.g., \"629 commits\") */\n readonly commits: string;\n /** Progress percentage (0-100) */\n readonly progress: number;\n /** Whether the card is expanded to show details */\n readonly isExpanded?: boolean;\n /** Progress bar gradient color */\n readonly gradient?: ProgressGradient;\n /** Number of PRs (shown in expanded view) */\n readonly prs?: number;\n /** Number of repos (shown in expanded view) */\n readonly repos?: number;\n /** Callback when \"Show repos\" button is clicked */\n readonly onShowYear?: () => void;\n /** Monthly activity data for sparkline */\n readonly sparklineData?: readonly number[];\n /** Labels for sparkline (e.g., month names) */\n readonly sparklineLabels?: readonly string[];\n /** Insights to display in expanded view */\n readonly insights?: readonly YearCardGlassInsight[];\n /** Custom stats for expanded view (replaces default commits/prs/repos grid) */\n readonly stats?: readonly YearCardGlassStat[];\n /** Custom action button text */\n readonly actionLabel?: string;\n /** Show sparkline in collapsed view */\n readonly showSparklineCollapsed?: boolean;\n /** Custom value formatter for commits display */\n readonly valueFormatter?: (commits: string) => string;\n /** Additional content for expanded section */\n readonly children?: ReactNode;\n}\n\n// ========================================\n// COMPONENT\n// ========================================\n\nexport const YearCardGlass = forwardRef<HTMLDivElement, YearCardGlassProps>(\n (\n {\n year,\n emoji,\n label,\n commits,\n progress,\n isExpanded = false,\n gradient = 'blue',\n prs = 0,\n repos = 0,\n onShowYear,\n sparklineData,\n sparklineLabels,\n insights,\n stats,\n actionLabel,\n showSparklineCollapsed = true,\n valueFormatter,\n children,\n className,\n onClick,\n ...props\n },\n ref\n ) => {\n const expandedStyles: CSSProperties = {\n background: 'var(--expanded-bg)',\n borderColor: 'var(--expanded-border)',\n };\n\n const metricCardStyles: CSSProperties = {\n background: 'var(--card-bg)',\n borderColor: 'var(--card-border)',\n };\n\n // Format commits display\n const displayCommits = valueFormatter ? valueFormatter(commits) : commits;\n\n // Default stats if none provided\n const displayStats = stats || [\n { label: 'Commits', value: commits },\n { label: 'PRs', value: prs },\n { label: 'Repos', value: repos },\n ];\n\n // Default action label\n const buttonLabel = actionLabel || `Show repos from ${year}`;\n\n return (\n <InteractiveCard\n ref={ref}\n baseBg=\"var(--year-card-bg)\"\n hoverBg=\"var(--card-hover-bg)\"\n borderColor=\"var(--year-card-border)\"\n hoverGlow=\"var(--year-card-hover-glow)\"\n hoverLift\n rounded=\"rounded-xl\"\n className={cn('w-full max-w-2xl p-2.5 md:p-3 cursor-pointer', className)}\n onClick={onClick}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.(e as unknown as React.MouseEvent<HTMLDivElement>);\n }\n }}\n aria-expanded={isExpanded}\n aria-label={`${year} year: ${label}, ${commits} commits. ${isExpanded ? 'Collapse' : 'Expand'} details`}\n {...props}\n >\n {/* Header */}\n <div className=\"flex items-center justify-between mb-1.5 md:mb-2\">\n <div className=\"flex items-center gap-1.5 md:gap-2\">\n <span\n className=\"font-semibold text-sm md:text-base\"\n style={{ color: 'var(--text-primary)' }}\n >\n {year}\n </span>\n <BadgeGlass>\n {emoji} {label}\n </BadgeGlass>\n </div>\n <span\n className=\"text-xs md:text-sm flex items-center gap-0.5 md:gap-1\"\n style={{ color: 'var(--text-secondary)' }}\n >\n {displayCommits}\n {isExpanded ? (\n <ChevronUp className=\"w-3.5 h-3.5 md:w-4 md:h-4\" aria-hidden=\"true\" />\n ) : (\n <ChevronDown className=\"w-3.5 h-3.5 md:w-4 md:h-4\" aria-hidden=\"true\" />\n )}\n </span>\n </div>\n\n {/* Progress + Sparkline (collapsed view) */}\n <div className=\"flex items-center gap-2\">\n <div className=\"flex-1\">\n <ProgressGlass value={progress} gradient={gradient} size=\"sm\" />\n </div>\n {showSparklineCollapsed && sparklineData && sparklineData.length > 0 && (\n <SparklineGlass\n data={sparklineData}\n labels={sparklineLabels}\n height=\"sm\"\n gap=\"sm\"\n className=\"w-16 md:w-20\"\n aria-label={`Activity trend for ${year}`}\n />\n )}\n </div>\n\n {/* Expanded Section */}\n {isExpanded && (\n <div className=\"mt-3 pt-3 border-t space-y-3\" style={expandedStyles}>\n {/* Stats Grid */}\n <div\n className=\"grid gap-2 md:gap-3\"\n style={{ gridTemplateColumns: `repeat(${Math.min(displayStats.length, 4)}, 1fr)` }}\n >\n {displayStats.map((stat, index) => (\n <div\n key={index}\n className=\"p-2 md:p-2.5 rounded-lg border text-center\"\n style={metricCardStyles}\n >\n {stat.icon && (\n <div className=\"mb-1 flex justify-center text-[var(--text-muted)]\">\n {stat.icon}\n </div>\n )}\n <div\n className=\"text-base md:text-xl font-bold\"\n style={{ color: 'var(--text-primary)' }}\n >\n {stat.value}\n </div>\n <div className=\"text-(length:--font-size-2xs) md:text-xs text-(--text-muted)\">\n {stat.label}\n </div>\n </div>\n ))}\n </div>\n\n {/* Expanded Sparkline with labels (if not shown in collapsed) */}\n {!showSparklineCollapsed && sparklineData && sparklineData.length > 0 && (\n <SparklineGlass\n data={sparklineData}\n labels={sparklineLabels}\n showLabels={!!sparklineLabels}\n height=\"md\"\n gap=\"sm\"\n highlightMax\n className=\"w-full\"\n aria-label={`Monthly activity for ${year}`}\n />\n )}\n\n {/* Insights */}\n {insights && insights.length > 0 && (\n <div className=\"space-y-2\">\n {insights.map((insight, index) => (\n <InsightCardGlass\n key={index}\n variant={insight.variant}\n emoji={insight.emoji}\n text={insight.text}\n detail={insight.detail}\n inline={false}\n />\n ))}\n </div>\n )}\n\n {/* Custom children content */}\n {children}\n\n {/* Show Year Button */}\n {onShowYear && (\n <ButtonGlass\n variant=\"
|
|
19
|
+
"content": "// ========================================\n// YEAR CARD GLASS COMPONENT\n// Year card for career timeline / analytics\n// Domain-specific composite component following shadcn/ui patterns\n// ========================================\n\nimport { forwardRef, type CSSProperties, type ReactNode } from 'react';\nimport { ChevronDown, ChevronUp } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { BadgeGlass } from '../ui/badge-glass';\nimport { ProgressGlass } from '../specialized/progress-glass';\nimport { SparklineGlass } from '../specialized/sparkline-glass';\nimport { ButtonGlass } from '../ui/button-glass';\nimport { InteractiveCard } from '../primitives';\nimport { InsightCardGlass } from '../atomic/insight-card-glass';\nimport type { ProgressGradient } from '@/lib/variants/progress-glass-variants';\nimport '@/glass-theme.css';\n\n// ========================================\n// TYPES\n// ========================================\n\nexport interface YearCardGlassInsight {\n readonly variant?: 'default' | 'tip' | 'highlight' | 'warning' | 'stat' | 'growth' | 'decline';\n readonly emoji?: string;\n readonly text: string;\n readonly detail?: string;\n}\n\nexport interface YearCardGlassStat {\n readonly label: string;\n readonly value: string | number;\n readonly icon?: ReactNode;\n}\n\nexport interface YearCardGlassProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Year to display (string or number) */\n readonly year: string | number;\n /** Emoji for the badge */\n readonly emoji: string;\n /** Label for the badge (e.g., \"Peak Year\") */\n readonly label: string;\n /** Primary metric value (e.g., \"629 commits\") */\n readonly commits: string;\n /** Progress percentage (0-100) */\n readonly progress: number;\n /** Whether the card is expanded to show details */\n readonly isExpanded?: boolean;\n /** Progress bar gradient color */\n readonly gradient?: ProgressGradient;\n /** Number of PRs (shown in expanded view) */\n readonly prs?: number;\n /** Number of repos (shown in expanded view) */\n readonly repos?: number;\n /** Callback when \"Show repos\" button is clicked */\n readonly onShowYear?: () => void;\n /** Monthly activity data for sparkline */\n readonly sparklineData?: readonly number[];\n /** Labels for sparkline (e.g., month names) */\n readonly sparklineLabels?: readonly string[];\n /** Insights to display in expanded view */\n readonly insights?: readonly YearCardGlassInsight[];\n /** Custom stats for expanded view (replaces default commits/prs/repos grid) */\n readonly stats?: readonly YearCardGlassStat[];\n /** Custom action button text */\n readonly actionLabel?: string;\n /** Show sparkline in collapsed view */\n readonly showSparklineCollapsed?: boolean;\n /** Custom value formatter for commits display */\n readonly valueFormatter?: (commits: string) => string;\n /** Additional content for expanded section */\n readonly children?: ReactNode;\n}\n\n// ========================================\n// COMPONENT\n// ========================================\n\nexport const YearCardGlass = forwardRef<HTMLDivElement, YearCardGlassProps>(\n (\n {\n year,\n emoji,\n label,\n commits,\n progress,\n isExpanded = false,\n gradient = 'blue',\n prs = 0,\n repos = 0,\n onShowYear,\n sparklineData,\n sparklineLabels,\n insights,\n stats,\n actionLabel,\n showSparklineCollapsed = true,\n valueFormatter,\n children,\n className,\n onClick,\n ...props\n },\n ref\n ) => {\n const expandedStyles: CSSProperties = {\n background: 'var(--expanded-bg)',\n borderColor: 'var(--expanded-border)',\n };\n\n const metricCardStyles: CSSProperties = {\n background: 'var(--card-bg)',\n borderColor: 'var(--card-border)',\n };\n\n // Format commits display\n const displayCommits = valueFormatter ? valueFormatter(commits) : commits;\n\n // Default stats if none provided\n const displayStats = stats || [\n { label: 'Commits', value: commits },\n { label: 'PRs', value: prs },\n { label: 'Repos', value: repos },\n ];\n\n // Default action label\n const buttonLabel = actionLabel || `Show repos from ${year}`;\n\n return (\n <InteractiveCard\n ref={ref}\n baseBg=\"var(--year-card-bg)\"\n hoverBg=\"var(--card-hover-bg)\"\n borderColor=\"var(--year-card-border)\"\n hoverGlow=\"var(--year-card-hover-glow)\"\n hoverLift\n rounded=\"rounded-xl\"\n className={cn('w-full max-w-2xl p-2.5 md:p-3 cursor-pointer', className)}\n onClick={onClick}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.(e as unknown as React.MouseEvent<HTMLDivElement>);\n }\n }}\n aria-expanded={isExpanded}\n aria-label={`${year} year: ${label}, ${commits} commits. ${isExpanded ? 'Collapse' : 'Expand'} details`}\n {...props}\n >\n {/* Header */}\n <div className=\"flex items-center justify-between mb-1.5 md:mb-2\">\n <div className=\"flex items-center gap-1.5 md:gap-2\">\n <span\n className=\"font-semibold text-sm md:text-base\"\n style={{ color: 'var(--text-primary)' }}\n >\n {year}\n </span>\n <BadgeGlass>\n {emoji} {label}\n </BadgeGlass>\n </div>\n <span\n className=\"text-xs md:text-sm flex items-center gap-0.5 md:gap-1\"\n style={{ color: 'var(--text-secondary)' }}\n >\n {displayCommits}\n {isExpanded ? (\n <ChevronUp className=\"w-3.5 h-3.5 md:w-4 md:h-4\" aria-hidden=\"true\" />\n ) : (\n <ChevronDown className=\"w-3.5 h-3.5 md:w-4 md:h-4\" aria-hidden=\"true\" />\n )}\n </span>\n </div>\n\n {/* Progress + Sparkline (collapsed view) */}\n <div className=\"flex items-center gap-2\">\n <div className=\"flex-1\">\n <ProgressGlass value={progress} gradient={gradient} size=\"sm\" />\n </div>\n {showSparklineCollapsed && sparklineData && sparklineData.length > 0 && (\n <SparklineGlass\n data={sparklineData}\n labels={sparklineLabels}\n height=\"sm\"\n gap=\"sm\"\n className=\"w-16 md:w-20\"\n aria-label={`Activity trend for ${year}`}\n />\n )}\n </div>\n\n {/* Expanded Section */}\n {isExpanded && (\n <div className=\"mt-3 pt-3 border-t space-y-3\" style={expandedStyles}>\n {/* Stats Grid */}\n <div\n className=\"grid gap-2 md:gap-3\"\n style={{ gridTemplateColumns: `repeat(${Math.min(displayStats.length, 4)}, 1fr)` }}\n >\n {displayStats.map((stat, index) => (\n <div\n key={index}\n className=\"p-2 md:p-2.5 rounded-lg border text-center\"\n style={metricCardStyles}\n >\n {stat.icon && (\n <div className=\"mb-1 flex justify-center text-[var(--text-muted)]\">\n {stat.icon}\n </div>\n )}\n <div\n className=\"text-base md:text-xl font-bold\"\n style={{ color: 'var(--text-primary)' }}\n >\n {stat.value}\n </div>\n <div className=\"text-(length:--font-size-2xs) md:text-xs text-(--text-muted)\">\n {stat.label}\n </div>\n </div>\n ))}\n </div>\n\n {/* Expanded Sparkline with labels (if not shown in collapsed) */}\n {!showSparklineCollapsed && sparklineData && sparklineData.length > 0 && (\n <SparklineGlass\n data={sparklineData}\n labels={sparklineLabels}\n showLabels={!!sparklineLabels}\n height=\"md\"\n gap=\"sm\"\n highlightMax\n className=\"w-full\"\n aria-label={`Monthly activity for ${year}`}\n />\n )}\n\n {/* Insights */}\n {insights && insights.length > 0 && (\n <div className=\"space-y-2\">\n {insights.map((insight, index) => (\n <InsightCardGlass\n key={index}\n variant={insight.variant}\n emoji={insight.emoji}\n text={insight.text}\n detail={insight.detail}\n inline={false}\n />\n ))}\n </div>\n )}\n\n {/* Custom children content */}\n {children}\n\n {/* Show Year Button */}\n {onShowYear && (\n <ButtonGlass\n variant=\"default\"\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation();\n onShowYear();\n }}\n className=\"w-full\"\n >\n {buttonLabel}\n </ButtonGlass>\n )}\n </div>\n )}\n </InteractiveCard>\n );\n }\n);\n\nYearCardGlass.displayName = 'YearCardGlass';\n"
|
|
20
20
|
}
|
|
21
21
|
],
|
|
22
22
|
"categories": [
|