diff --git a/src/components/features/snippet-editor/SplitScreenEditor.tsx b/src/components/features/snippet-editor/SplitScreenEditor.tsx index 82b0fe9..e5d9409 100644 --- a/src/components/features/snippet-editor/SplitScreenEditor.tsx +++ b/src/components/features/snippet-editor/SplitScreenEditor.tsx @@ -3,9 +3,9 @@ import { MonacoEditor } from '@/components/features/snippet-editor/MonacoEditor' import { ReactPreview } from '@/components/features/snippet-editor/ReactPreview' import { PythonOutput } from '@/components/features/python-runner/PythonOutput' import { Button } from '@/components/ui/button' -import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable' import { Code, Eye, SplitHorizontal } from '@phosphor-icons/react' import { InputParameter } from '@/lib/types' +import styles from './split-screen-editor.module.scss' interface SplitScreenEditorProps { value: string @@ -43,43 +43,40 @@ export function SplitScreenEditor({ } return ( -
-
-
+
+
+
-
+
{viewMode === 'code' && ( - +
+
- - - +
+
{isPython ? ( ) : ( @@ -124,8 +120,8 @@ export function SplitScreenEditor({ inputParameters={inputParameters} /> )} - - +
+
)}
diff --git a/src/components/features/snippet-editor/split-screen-editor.module.scss b/src/components/features/snippet-editor/split-screen-editor.module.scss new file mode 100644 index 0000000..e085c94 --- /dev/null +++ b/src/components/features/snippet-editor/split-screen-editor.module.scss @@ -0,0 +1,59 @@ +.container { + display: flex; + flex-direction: column; + gap: 12px; +} + +.toolbar { + display: flex; + align-items: center; + justify-content: flex-end; +} + +.buttonGroup { + display: flex; + align-items: center; + gap: 4px; + padding: 4px; + background-color: var(--mat-sys-surface-variant); + border-radius: var(--mat-sys-corner-small); +} + +.button { + display: flex; + align-items: center; + gap: 8px; + height: 32px; +} + +.buttonIcon { + width: 16px; + height: 16px; +} + +.buttonLabel { + @media (max-width: 640px) { + display: none; + } +} + +.viewport { + border-radius: var(--mat-sys-corner-small); + border: 1px solid var(--mat-sys-outline-variant); + overflow: hidden; + background-color: var(--mat-sys-surface); +} + +.splitView { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1px; + height: 100%; + background-color: var(--mat-sys-outline-variant); +} + +.editorPanel, +.previewPanel { + background-color: var(--mat-sys-surface); + overflow: auto; +} diff --git a/src/components/ui/accordion.module.scss b/src/components/ui/accordion.module.scss new file mode 100644 index 0000000..d3fcfad --- /dev/null +++ b/src/components/ui/accordion.module.scss @@ -0,0 +1,70 @@ +.accordion { + width: 100%; +} + +.item { + border-bottom: 1px solid var(--mat-sys-outline-variant); + + &:last-child { + border-bottom: none; + } +} + +.trigger { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 16px 0; + background: transparent; + border: none; + cursor: pointer; + font-family: var(--mat-sys-body-large-font); + font-size: var(--mat-sys-body-large-size); + font-weight: var(--mat-sys-body-large-weight); + color: var(--mat-sys-on-surface); + text-align: left; + transition: color 200ms; + + &:hover { + color: var(--mat-sys-primary); + } + + &:focus-visible { + outline: 2px solid var(--mat-sys-primary); + outline-offset: 2px; + } + + &:disabled { + opacity: 0.38; + cursor: not-allowed; + } +} + +.icon { + width: 20px; + height: 20px; + transition: transform 200ms; + color: var(--mat-sys-on-surface-variant); +} + +.iconOpen { + transform: rotate(180deg); +} + +.content { + overflow: hidden; + transition: max-height 300ms ease-in-out; + max-height: 1000px; +} + +.contentClosed { + max-height: 0; +} + +.contentInner { + padding: 0 0 16px; + font-family: var(--mat-sys-body-medium-font); + font-size: var(--mat-sys-body-medium-size); + color: var(--mat-sys-on-surface-variant); +} diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index 1fa55cd..cf68077 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -1,64 +1,108 @@ -import { ComponentProps } from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import ChevronDownIcon from "lucide-react/dist/esm/icons/chevron-down" +import { ComponentProps, forwardRef, useState, createContext, useContext } from 'react' +import styles from './accordion.module.scss' +import { cn } from '@/lib/utils' +import { CaretDown } from '@phosphor-icons/react' -import { cn } from "@/lib/utils" - -function Accordion({ - ...props -}: ComponentProps) { - return +interface AccordionContextValue { + openItems: Set + toggleItem: (value: string) => void + type: 'single' | 'multiple' } -function AccordionItem({ +const AccordionContext = createContext(null) + +interface AccordionProps extends ComponentProps<'div'> { + type?: 'single' | 'multiple' + defaultValue?: string | string[] +} + +export function Accordion({ + type = 'single', + defaultValue, + children, className, - ...props -}: ComponentProps) { + ...props +}: AccordionProps) { + const [openItems, setOpenItems] = useState>(() => { + if (!defaultValue) return new Set() + return new Set(Array.isArray(defaultValue) ? defaultValue : [defaultValue]) + }) + + const toggleItem = (value: string) => { + setOpenItems(prev => { + const next = new Set(prev) + if (next.has(value)) { + next.delete(value) + } else { + if (type === 'single') { + next.clear() + } + next.add(value) + } + return next + }) + } + return ( - + +
+ {children} +
+
) } -function AccordionTrigger({ - className, - children, - ...props -}: ComponentProps) { - return ( - - svg]:rotate-180", - className - )} +export const AccordionItem = forwardRef & { value: string }>( + ({ className, value, ...props }, ref) => ( +
+ ) +) +AccordionItem.displayName = 'AccordionItem' + +interface AccordionTriggerProps extends ComponentProps<'button'> { + value?: string +} + +export const AccordionTrigger = forwardRef( + ({ className, children, ...props }, ref) => { + const context = useContext(AccordionContext) + const item = (ref as any)?.current?.closest('[data-value]') + const value = item?.getAttribute('data-value') || '' + const isOpen = context?.openItems.has(value) + + return ( + + ) + } +) +AccordionTrigger.displayName = 'AccordionTrigger' -function AccordionContent({ - className, - children, - ...props -}: ComponentProps) { - return ( - -
{children}
-
- ) -} - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } +export const AccordionContent = forwardRef>( + ({ className, children, ...props }, ref) => { + const context = useContext(AccordionContext) + const item = (ref as any)?.current?.closest('[data-value]') + const value = item?.getAttribute('data-value') || '' + const isOpen = context?.openItems.has(value) + + return ( +
+
+ {children} +
+
+ ) + } +) +AccordionContent.displayName = 'AccordionContent' diff --git a/src/components/ui/checkbox.module.scss b/src/components/ui/checkbox.module.scss new file mode 100644 index 0000000..6d1e5c6 --- /dev/null +++ b/src/components/ui/checkbox.module.scss @@ -0,0 +1,51 @@ +.container { + display: inline-flex; + align-items: center; + cursor: pointer; + position: relative; +} + +.input { + position: absolute; + opacity: 0; + width: 0; + height: 0; + + &:focus-visible + .indicator { + outline: 2px solid var(--mat-sys-primary); + outline-offset: 2px; + } +} + +.indicator { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border: 2px solid var(--mat-sys-on-surface-variant); + border-radius: var(--mat-sys-corner-extra-small); + background-color: transparent; + transition: all 200ms; + + .input:hover + & { + border-color: var(--mat-sys-on-surface); + background-color: color-mix(in srgb, var(--mat-sys-on-surface) 8%, transparent); + } + + .input:checked + & { + border-color: var(--mat-sys-primary); + background-color: var(--mat-sys-primary); + } + + .input:disabled + & { + opacity: 0.38; + cursor: not-allowed; + } +} + +.icon { + width: 14px; + height: 14px; + color: var(--mat-sys-on-primary); +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index 622f154..e9451c1 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -1,30 +1,33 @@ -"use client" +import { ComponentProps, forwardRef } from 'react' +import styles from './checkbox.module.scss' +import { cn } from '@/lib/utils' +import { Check, Minus } from '@phosphor-icons/react' -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import Check from "lucide-react/dist/esm/icons/check" +interface CheckboxProps extends Omit, 'type'> { + checked?: boolean + onCheckedChange?: (checked: boolean) => void + indeterminate?: boolean +} -import { cn } from "@/lib/utils" - -const Checkbox = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - -)) -Checkbox.displayName = CheckboxPrimitive.Root.displayName - -export { Checkbox } +export const Checkbox = forwardRef( + ({ className, checked, onCheckedChange, indeterminate, ...props }, ref) => ( + + ) +) +Checkbox.displayName = 'Checkbox' diff --git a/src/components/ui/radio-group.module.scss b/src/components/ui/radio-group.module.scss new file mode 100644 index 0000000..e006540 --- /dev/null +++ b/src/components/ui/radio-group.module.scss @@ -0,0 +1,55 @@ +.group { + display: grid; + gap: 8px; +} + +.item { + display: inline-flex; + align-items: center; + cursor: pointer; + position: relative; +} + +.input { + position: absolute; + opacity: 0; + width: 0; + height: 0; + + &:focus-visible + .indicator { + outline: 2px solid var(--mat-sys-primary); + outline-offset: 2px; + } +} + +.indicator { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border: 2px solid var(--mat-sys-on-surface-variant); + border-radius: 50%; + background-color: transparent; + transition: all 200ms; + + .input:hover + & { + border-color: var(--mat-sys-on-surface); + background-color: color-mix(in srgb, var(--mat-sys-on-surface) 8%, transparent); + } + + .input:checked + & { + border-color: var(--mat-sys-primary); + } + + .input:disabled + & { + opacity: 0.38; + cursor: not-allowed; + } +} + +.icon { + width: 10px; + height: 10px; + color: var(--mat-sys-primary); +} diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx index 147f414..2e271c6 100644 --- a/src/components/ui/radio-group.tsx +++ b/src/components/ui/radio-group.tsx @@ -1,44 +1,49 @@ -"use client" +import { ComponentProps, forwardRef, createContext, useContext } from 'react' +import styles from './radio-group.module.scss' +import { cn } from '@/lib/utils' +import { Circle } from '@phosphor-icons/react' -import * as React from "react" -import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" -import Circle from "lucide-react/dist/esm/icons/circle" +interface RadioGroupContextValue { + value?: string + onValueChange?: (value: string) => void +} -import { cn } from "@/lib/utils" +const RadioGroupContext = createContext(null) -const RadioGroup = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - return ( - +interface RadioGroupProps extends ComponentProps<'div'> { + value?: string + onValueChange?: (value: string) => void +} + +export const RadioGroup = forwardRef( + ({ className, value, onValueChange, ...props }, ref) => ( + +
+ ) -}) -RadioGroup.displayName = RadioGroupPrimitive.Root.displayName +) +RadioGroup.displayName = 'RadioGroup' -const RadioGroupItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - return ( - - - - - - ) -}) -RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName - -export { RadioGroup, RadioGroupItem } +export const RadioGroupItem = forwardRef & { value: string }>( + ({ className, value, ...props }, ref) => { + const context = useContext(RadioGroupContext) + const isChecked = context?.value === value + + return ( + + ) + } +) +RadioGroupItem.displayName = 'RadioGroupItem' diff --git a/src/components/ui/select.module.scss b/src/components/ui/select.module.scss new file mode 100644 index 0000000..390dd0d --- /dev/null +++ b/src/components/ui/select.module.scss @@ -0,0 +1,127 @@ +.trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-width: 120px; + height: 40px; + padding: 0 12px; + border: 1px solid var(--mat-sys-outline); + border-radius: var(--mat-sys-corner-small); + background-color: var(--mat-sys-surface); + color: var(--mat-sys-on-surface); + font-family: var(--mat-sys-body-large-font); + font-size: var(--mat-sys-body-large-size); + cursor: pointer; + transition: all 200ms; + + &:hover { + border-color: var(--mat-sys-on-surface); + background-color: var(--mat-sys-surface-variant); + } + + &:focus-visible { + outline: 2px solid var(--mat-sys-primary); + border-color: var(--mat-sys-primary); + } + + &:disabled { + opacity: 0.38; + cursor: not-allowed; + } +} + +.triggerSm { + height: 32px; + font-size: var(--mat-sys-body-medium-size); +} + +.icon { + width: 16px; + height: 16px; + color: var(--mat-sys-on-surface-variant); +} + +.overlay { + position: fixed; + inset: 0; + z-index: 1000; + background-color: transparent; +} + +.content { + position: fixed; + z-index: 1001; + min-width: 180px; + max-height: 400px; + overflow-y: auto; + padding: 8px; + background-color: var(--mat-sys-surface-container); + border-radius: var(--mat-sys-corner-small); + box-shadow: var(--mat-sys-level2); + animation: slideIn 200ms; +} + +.item { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 48px; + padding: 12px 16px; + border-radius: var(--mat-sys-corner-extra-small); + cursor: pointer; + font-family: var(--mat-sys-body-large-font); + font-size: var(--mat-sys-body-large-size); + color: var(--mat-sys-on-surface); + transition: background-color 200ms; + + &:hover { + background-color: color-mix(in srgb, var(--mat-sys-on-surface) 8%, transparent); + } + + &:focus-visible { + background-color: color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent); + } +} + +.itemSelected { + background-color: var(--mat-sys-secondary-container); + color: var(--mat-sys-on-secondary-container); +} + +.checkmark { + width: 20px; + height: 20px; + color: var(--mat-sys-primary); +} + +.group { + padding: 8px 0; +} + +.label { + padding: 8px 16px; + font-family: var(--mat-sys-label-small-font); + font-size: var(--mat-sys-label-small-size); + font-weight: var(--mat-sys-label-small-weight); + color: var(--mat-sys-on-surface-variant); + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.separator { + height: 1px; + margin: 8px 0; + background-color: var(--mat-sys-outline-variant); +} + +@keyframes slideIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index 0fb0d44..5fbbfb8 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -1,185 +1,124 @@ -import { ComponentProps } from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import CheckIcon from "lucide-react/dist/esm/icons/check" -import ChevronDownIcon from "lucide-react/dist/esm/icons/chevron-down" -import ChevronUpIcon from "lucide-react/dist/esm/icons/chevron-up" +import { ComponentProps, forwardRef, useState, createContext, useContext } from 'react' +import { createPortal } from 'react-dom' +import styles from './select.module.scss' +import { cn } from '@/lib/utils' +import { CaretDown, Check } from '@phosphor-icons/react' -import { cn } from "@/lib/utils" - -function Select({ - ...props -}: ComponentProps) { - return +interface SelectContextValue { + value?: string + onValueChange?: (value: string) => void + open: boolean + setOpen: (open: boolean) => void } -function SelectGroup({ - ...props -}: ComponentProps) { - return +const SelectContext = createContext(null) + +interface SelectProps { + value?: string + onValueChange?: (value: string) => void + children: React.ReactNode } -function SelectValue({ - ...props -}: ComponentProps) { - return -} - -function SelectTrigger({ - className, - size = "default", - children, - ...props -}: ComponentProps & { - size?: "sm" | "default" -}) { +export function Select({ value, onValueChange, children }: SelectProps) { + const [open, setOpen] = useState(false) + return ( - + {children} - - - - + ) } -function SelectContent({ - className, - children, - position = "popper", - ...props -}: ComponentProps) { - return ( - - & { size?: 'sm' | 'default' }>( + ({ className, children, size = 'default', ...props }, ref) => { + const context = useContext(SelectContext) + + return ( + + ) + } +) +SelectTrigger.displayName = 'SelectTrigger' + +export const SelectValue = ({ placeholder }: { placeholder?: string }) => { + const context = useContext(SelectContext) + return {context?.value || placeholder} +} + +export const SelectContent = forwardRef>( + ({ className, children, position = 'popper', ...props }, ref) => { + const context = useContext(SelectContext) + + if (!context?.open) return null + + return createPortal( +
context.setOpen(false)}> +
e.stopPropagation()} + {...props} > {children} - - - - - ) -} +
+
, + document.body + ) + } +) +SelectContent.displayName = 'SelectContent' -function SelectLabel({ - className, - ...props -}: ComponentProps) { - return ( - - ) -} +export const SelectItem = forwardRef & { value: string }>( + ({ className, children, value, ...props }, ref) => { + const context = useContext(SelectContext) + const isSelected = context?.value === value + + return ( +
{ + context?.onValueChange?.(value) + context?.setOpen(false) + }} + {...props} + > + {children} + {isSelected && } +
+ ) + } +) +SelectItem.displayName = 'SelectItem' -function SelectItem({ - className, - children, - ...props -}: ComponentProps) { - return ( - - - - - - - {children} - +export const SelectGroup = forwardRef>( + ({ className, ...props }, ref) => ( +
) -} +) +SelectGroup.displayName = 'SelectGroup' -function SelectSeparator({ - className, - ...props -}: ComponentProps) { - return ( - +export const SelectLabel = forwardRef>( + ({ className, ...props }, ref) => ( +
) -} +) +SelectLabel.displayName = 'SelectLabel' -function SelectScrollUpButton({ - className, - ...props -}: ComponentProps) { - return ( - - - +export const SelectSeparator = forwardRef>( + ({ className, ...props }, ref) => ( +
) -} +) +SelectSeparator.displayName = 'SelectSeparator' -function SelectScrollDownButton({ - className, - ...props -}: ComponentProps) { - return ( - - - - ) -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -} +export const SelectScrollUpButton = () => null +export const SelectScrollDownButton = () => null diff --git a/src/components/ui/switch.module.scss b/src/components/ui/switch.module.scss new file mode 100644 index 0000000..ead09f1 --- /dev/null +++ b/src/components/ui/switch.module.scss @@ -0,0 +1,58 @@ +.container { + display: inline-flex; + align-items: center; + cursor: pointer; + position: relative; +} + +.input { + position: absolute; + opacity: 0; + width: 0; + height: 0; + + &:focus-visible + .track { + outline: 2px solid var(--mat-sys-primary); + outline-offset: 2px; + } +} + +.track { + position: relative; + display: inline-block; + width: 52px; + height: 32px; + border-radius: var(--mat-sys-corner-full); + background-color: var(--mat-sys-surface-variant); + border: 2px solid var(--mat-sys-outline); + transition: all 200ms; + + .input:checked + & { + background-color: var(--mat-sys-primary); + border-color: var(--mat-sys-primary); + } + + .input:disabled + & { + opacity: 0.38; + cursor: not-allowed; + } +} + +.thumb { + position: absolute; + top: 50%; + left: 8px; + transform: translateY(-50%); + width: 16px; + height: 16px; + border-radius: var(--mat-sys-corner-full); + background-color: var(--mat-sys-outline); + transition: all 200ms; + + .input:checked + .track & { + left: 24px; + width: 24px; + height: 24px; + background-color: var(--mat-sys-on-primary); + } +} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index bc69cf2..45daa20 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -1,29 +1,27 @@ -"use client" +import { ComponentProps, forwardRef } from 'react' +import styles from './switch.module.scss' +import { cn } from '@/lib/utils' -import * as React from "react" -import * as SwitchPrimitives from "@radix-ui/react-switch" +interface SwitchProps extends Omit, 'type'> { + checked?: boolean + onCheckedChange?: (checked: boolean) => void +} -import { cn } from "@/lib/utils" - -const Switch = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -Switch.displayName = SwitchPrimitives.Root.displayName - -export { Switch } +export const Switch = forwardRef( + ({ className, checked, onCheckedChange, ...props }, ref) => ( + + ) +) +Switch.displayName = 'Switch'