Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 63 additions & 52 deletions src/components/input/ExampleContentMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useId, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { exampleContents, type ExampleContent } from '@/constants/exampleContent'

interface ExampleContentMenuProps {
Expand All @@ -21,10 +22,10 @@ export const ExampleContentMenu = ({

const getCategoryColor = (category: ExampleContent['category']) => {
const colors = {
technical: 'bg-blue-100 text-blue-700',
academic: 'bg-purple-100 text-purple-700',
news: 'bg-green-100 text-green-700',
legal: 'bg-amber-100 text-amber-700',
technical: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-200',
academic: 'bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-200',
news: 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-200',
legal: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-200',
}
return colors[category]
}
Expand Down Expand Up @@ -143,7 +144,7 @@ export const ExampleContentMenu = ({
type="button"
onClick={() => setIsOpen((prev) => !prev)}
disabled={disabled}
className="flex items-center gap-2 rounded-xl border border-neutral-300 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 transition hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 focus-visible:ring-2 focus-visible:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-neutral-300 disabled:hover:bg-white disabled:hover:text-neutral-700"
className="flex items-center gap-2 rounded-xl border border-neutral-300 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 focus-visible:ring-2 focus-visible:ring-primary-500 dark:border-neutral-600 dark:bg-neutral-900/70 dark:text-neutral-200 dark:hover:border-primary-400 dark:hover:bg-primary-500/10 dark:hover:text-primary-200 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:translate-y-0"
aria-haspopup="menu"
aria-expanded={isOpen}
aria-controls={isOpen ? menuId : undefined}
Expand All @@ -164,7 +165,7 @@ export const ExampleContentMenu = ({
</svg>
<span>Load example</span>
<svg
className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
className={`h-4 w-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
Expand All @@ -174,53 +175,63 @@ export const ExampleContentMenu = ({
</svg>
</button>

{isOpen && (
<div
id={menuId}
className="absolute left-0 right-0 z-10 mt-2 min-w-[320px] rounded-xl border border-neutral-200 bg-white shadow-soft"
role="menu"
aria-orientation="vertical"
onKeyDown={handleMenuKeyDown}
>
<div className="border-b border-neutral-200 px-4 py-3">
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">
Example content
</p>
</div>

<div className="max-h-[400px] overflow-y-auto py-2">
{exampleContents.map((example, index) => (
<button
key={example.id}
ref={(el) => {
menuItemsRef.current[index] = el
}}
type="button"
tabIndex={focusedIndex === index ? 0 : -1}
onClick={() => handleSelectExample(example)}
onFocus={() => setFocusedIndex(index)}
className="w-full px-4 py-3 text-left transition hover:bg-neutral-50 focus:bg-neutral-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
role="menuitem"
>
<div className="flex items-start gap-3">
<span
className={`mt-1 rounded-full px-2 py-0.5 text-xs font-semibold uppercase tracking-wide ${getCategoryColor(example.category)}`}
>
{example.category}
</span>

<div className="min-w-0 flex-1">
<p className="truncate font-medium text-neutral-900">{example.title}</p>
<p className="mt-1 line-clamp-2 text-xs text-neutral-600">
{example.description}
</p>
<AnimatePresence>
{isOpen && (
<motion.div
id={menuId}
className="absolute left-0 right-0 z-10 mt-2 min-w-[320px] rounded-xl border border-neutral-200 bg-white/95 shadow-soft backdrop-blur-sm dark:border-neutral-700 dark:bg-neutral-900/95 dark:shadow-[0_20px_45px_-20px_rgba(15,23,42,0.65)]"
role="menu"
aria-orientation="vertical"
onKeyDown={handleMenuKeyDown}
initial={{ opacity: 0, transform: 'translateY(-8px) scale(0.98)' }}
animate={{ opacity: 1, transform: 'translateY(0) scale(1)' }}
transition={{ duration: 0.16, ease: 'easeOut' }}
>
<div className="border-b border-neutral-200 px-4 py-3 dark:border-neutral-700">
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
Example content
</p>
</div>

<div className="max-h-[400px] overflow-y-auto py-2">
{exampleContents.map((example, index) => (
<motion.button
key={example.id}
ref={(el) => {
menuItemsRef.current[index] = el
}}
type="button"
tabIndex={focusedIndex === index ? 0 : -1}
onClick={() => handleSelectExample(example)}
onFocus={() => setFocusedIndex(index)}
className="w-full px-4 py-3 text-left transition-all duration-150 hover:bg-neutral-50 focus:bg-neutral-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:hover:bg-neutral-800/70 dark:focus:bg-neutral-800/70"
role="menuitem"
initial={{ opacity: 0, transform: 'translateY(4px)' }}
animate={{ opacity: 1, transform: 'translateY(0)' }}
transition={{ duration: 0.14, ease: 'easeOut' }}
>
<div className="flex items-start gap-3">
<span
className={`mt-1 rounded-full px-2 py-0.5 text-xs font-semibold uppercase tracking-wide ${getCategoryColor(example.category)}`}
>
{example.category}
</span>

<div className="min-w-0 flex-1">
<p className="truncate font-medium text-neutral-900 dark:text-neutral-100">
{example.title}
</p>
<p className="mt-1 line-clamp-2 text-xs text-neutral-600 dark:text-neutral-400">
{example.description}
</p>
</div>
</div>
</div>
</button>
))}
</div>
</div>
)}
</motion.button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
}
14 changes: 8 additions & 6 deletions src/components/input/TextInputPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,12 @@ export const TextInputPanel = ({
// Get border color based on validation state
const getBorderColor = () => {
if (validation && !validation.isValid && text.length > 0) {
return 'border-red-300 focus:border-red-500 focus:ring-red-500'
return 'border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-500/70 dark:focus:border-red-400 dark:focus:ring-red-400'
}
if (warningMessage) {
return 'border-orange-300 focus:border-orange-500 focus:ring-orange-500'
return 'border-orange-300 focus:border-orange-500 focus:ring-orange-500 dark:border-orange-500/60 dark:focus:border-orange-400 dark:focus:ring-orange-400'
}
return 'border-neutral-300 focus:border-primary-500 focus:ring-primary-500'
return 'border-neutral-300 focus:border-primary-500 focus:ring-primary-500 dark:border-neutral-700 dark:focus:border-primary-400 dark:focus:ring-primary-400'
}

return (
Expand All @@ -298,14 +298,16 @@ export const TextInputPanel = ({
<div className="flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<h2 className="font-display text-2xl font-semibold text-neutral-900">Input text</h2>
<h2 className="font-display text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
Input text
</h2>
<InlineHelp label="What can I do in the text input panel?">
Paste or type text that you want to rewrite or translate. Use ⌘/Ctrl + Enter to
process and ⌘/Ctrl + K to clear. Synapse sanitizes pasted content to remove stray
control characters automatically.
</InlineHelp>
</div>
<p className="mt-1 text-sm text-neutral-600">
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-300">
Paste or type the text you want to process. Maximum 10,000 words.
</p>
</div>
Expand Down Expand Up @@ -388,7 +390,7 @@ export const TextInputPanel = ({
onChange={(e) => setText(e.target.value)}
disabled={isDisabled}
placeholder="Paste or type your text here..."
className={`w-full rounded-xl border bg-white px-4 py-3 text-neutral-900 placeholder-neutral-400 transition focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:bg-neutral-50 disabled:text-neutral-500 ${getBorderColor()}`}
className={`w-full rounded-xl border bg-white px-4 py-3 text-neutral-900 placeholder-neutral-400 transition focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:bg-neutral-50 disabled:text-neutral-500 dark:bg-neutral-900/70 dark:text-neutral-100 dark:placeholder-neutral-500 dark:disabled:bg-neutral-900/40 dark:disabled:text-neutral-500 ${getBorderColor()}`}
rows={12}
aria-label="Text input"
aria-invalid={validation && !validation.isValid ? 'true' : 'false'}
Expand Down
20 changes: 15 additions & 5 deletions src/lib/motion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
useEffect,
useMemo,
useState,
type ButtonHTMLAttributes,
type CSSProperties,
type HTMLAttributes,
type MouseEvent as ReactMouseEvent,
type ReactNode,
} from 'react'
import type { JSX } from 'react'
Expand All @@ -20,7 +22,11 @@ type TransitionConfig = {
type VariantStyles = CSSProperties
type Variants = Record<string, VariantStyles>

type MotionProps<T> = HTMLAttributes<T> & {
type BaseElementProps<T extends HTMLElement> = T extends HTMLButtonElement
? ButtonHTMLAttributes<T>
: HTMLAttributes<T>

type MotionProps<T extends HTMLElement> = BaseElementProps<T> & {
initial?: VariantStyles | string
animate?: VariantStyles | string
whileHover?: VariantStyles
Expand Down Expand Up @@ -97,8 +103,10 @@ const createMotionComponent = <T extends HTMLElement>(element: keyof JSX.Intrins
} as CSSProperties
}, [transition?.duration, transition?.delay, transition?.ease])

const handleMouseEnter: typeof onMouseEnter = (event) => {
onMouseEnter?.(event)
const handleMouseEnter = (event: ReactMouseEvent<T>) => {
if (onMouseEnter) {
onMouseEnter(event as never)
}
if (whileHover) {
setCurrentStyle((prev) => ({
...prev,
Expand All @@ -107,8 +115,10 @@ const createMotionComponent = <T extends HTMLElement>(element: keyof JSX.Intrins
}
}

const handleMouseLeave: typeof onMouseLeave = (event) => {
onMouseLeave?.(event)
const handleMouseLeave = (event: ReactMouseEvent<T>) => {
if (onMouseLeave) {
onMouseLeave(event as never)
}
if (whileHover && resolvedAnimate) {
setCurrentStyle((prev) => ({
...prev,
Expand Down
30 changes: 0 additions & 30 deletions src/pages/DemosPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,6 @@ export const DemosPage = () => {
delay={0.05}
/>

<DemoCard
title="Summarizer API"
description="Generate concise summaries and key points from long-form content."
status="planned"
link="/demos/summarizer"
features={[
'Key points extraction',
'TL;DR summaries',
'Multiple summary formats',
'Adjustable length',
'Context preservation',
]}
delay={0.1}
/>

<DemoCard
title="Language Detector API"
description="Detect the language of text with confidence scores for multiple possibilities."
Expand Down Expand Up @@ -112,21 +97,6 @@ export const DemosPage = () => {
]}
delay={0.2}
/>

<DemoCard
title="Writer & Proofreader APIs"
description="Generate content, check grammar, and improve writing quality with AI assistance."
status="planned"
link="/demos/writer"
features={[
'Content generation',
'Grammar correction',
'Tone adjustment',
'Writing suggestions',
'Style improvements',
]}
delay={0.25}
/>
</div>

<motion.div
Expand Down
Loading