+
+
-
+
{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 (
+
+ )
+ }
+)
+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'