-
- {([
- { name: 'Developer', desc: 'Monospace, dark code, minimal', font: 'jetbrains-mono', code_theme: 'onedark', layout: 'minimal', compactness: 'compact', accent_color: '#10b981' },
- { name: 'Writer', desc: 'Serif, light code, spacious', font: 'merriweather', code_theme: 'github', layout: 'default', compactness: 'spacious', accent_color: '#6366f1' },
- { name: 'Magazine', desc: 'Sans-serif, grid layout', font: 'inter', code_theme: 'nord', layout: 'magazine', compactness: 'cozy', accent_color: '#f59e0b' },
- ] as const).map(preset => (
-
- ))}
-
-
Accent Color
diff --git a/studio/src/pages/DomainPage.tsx b/frontends/studio/src/pages/DomainPage.tsx
similarity index 100%
rename from studio/src/pages/DomainPage.tsx
rename to frontends/studio/src/pages/DomainPage.tsx
diff --git a/studio/src/pages/EngagementPage.tsx b/frontends/studio/src/pages/EngagementPage.tsx
similarity index 100%
rename from studio/src/pages/EngagementPage.tsx
rename to frontends/studio/src/pages/EngagementPage.tsx
diff --git a/studio/src/pages/GeneralPage.tsx b/frontends/studio/src/pages/GeneralPage.tsx
similarity index 100%
rename from studio/src/pages/GeneralPage.tsx
rename to frontends/studio/src/pages/GeneralPage.tsx
diff --git a/studio/src/pages/HomePage.tsx b/frontends/studio/src/pages/HomePage.tsx
similarity index 100%
rename from studio/src/pages/HomePage.tsx
rename to frontends/studio/src/pages/HomePage.tsx
diff --git a/studio/src/pages/MonetizationPage.tsx b/frontends/studio/src/pages/MonetizationPage.tsx
similarity index 100%
rename from studio/src/pages/MonetizationPage.tsx
rename to frontends/studio/src/pages/MonetizationPage.tsx
diff --git a/studio/src/pages/PluginsPage.tsx b/frontends/studio/src/pages/PluginsPage.tsx
similarity index 100%
rename from studio/src/pages/PluginsPage.tsx
rename to frontends/studio/src/pages/PluginsPage.tsx
diff --git a/studio/src/pages/PostEditorPage.tsx b/frontends/studio/src/pages/PostEditorPage.tsx
similarity index 100%
rename from studio/src/pages/PostEditorPage.tsx
rename to frontends/studio/src/pages/PostEditorPage.tsx
diff --git a/studio/src/pages/PostsPage.tsx b/frontends/studio/src/pages/PostsPage.tsx
similarity index 100%
rename from studio/src/pages/PostsPage.tsx
rename to frontends/studio/src/pages/PostsPage.tsx
diff --git a/studio/src/pages/index.ts b/frontends/studio/src/pages/index.ts
similarity index 100%
rename from studio/src/pages/index.ts
rename to frontends/studio/src/pages/index.ts
diff --git a/studio/src/stores/analytics.ts b/frontends/studio/src/stores/analytics.ts
similarity index 100%
rename from studio/src/stores/analytics.ts
rename to frontends/studio/src/stores/analytics.ts
diff --git a/studio/src/stores/apiKeys.ts b/frontends/studio/src/stores/apiKeys.ts
similarity index 100%
rename from studio/src/stores/apiKeys.ts
rename to frontends/studio/src/stores/apiKeys.ts
diff --git a/studio/src/stores/app.ts b/frontends/studio/src/stores/app.ts
similarity index 100%
rename from studio/src/stores/app.ts
rename to frontends/studio/src/stores/app.ts
diff --git a/studio/src/stores/assets.ts b/frontends/studio/src/stores/assets.ts
similarity index 100%
rename from studio/src/stores/assets.ts
rename to frontends/studio/src/stores/assets.ts
diff --git a/studio/src/stores/billing.ts b/frontends/studio/src/stores/billing.ts
similarity index 100%
rename from studio/src/stores/billing.ts
rename to frontends/studio/src/stores/billing.ts
diff --git a/studio/src/stores/editor.ts b/frontends/studio/src/stores/editor.ts
similarity index 100%
rename from studio/src/stores/editor.ts
rename to frontends/studio/src/stores/editor.ts
diff --git a/studio/src/stores/fetcher.ts b/frontends/studio/src/stores/fetcher.ts
similarity index 100%
rename from studio/src/stores/fetcher.ts
rename to frontends/studio/src/stores/fetcher.ts
diff --git a/studio/src/stores/hooks.ts b/frontends/studio/src/stores/hooks.ts
similarity index 100%
rename from studio/src/stores/hooks.ts
rename to frontends/studio/src/stores/hooks.ts
diff --git a/studio/src/stores/index.ts b/frontends/studio/src/stores/index.ts
similarity index 100%
rename from studio/src/stores/index.ts
rename to frontends/studio/src/stores/index.ts
diff --git a/studio/src/stores/interactions.ts b/frontends/studio/src/stores/interactions.ts
similarity index 100%
rename from studio/src/stores/interactions.ts
rename to frontends/studio/src/stores/interactions.ts
diff --git a/studio/src/stores/plugins.ts b/frontends/studio/src/stores/plugins.ts
similarity index 100%
rename from studio/src/stores/plugins.ts
rename to frontends/studio/src/stores/plugins.ts
diff --git a/studio/src/stores/posts.ts b/frontends/studio/src/stores/posts.ts
similarity index 100%
rename from studio/src/stores/posts.ts
rename to frontends/studio/src/stores/posts.ts
diff --git a/studio/src/stores/router.ts b/frontends/studio/src/stores/router.ts
similarity index 100%
rename from studio/src/stores/router.ts
rename to frontends/studio/src/stores/router.ts
diff --git a/studio/src/stores/secrets.ts b/frontends/studio/src/stores/secrets.ts
similarity index 100%
rename from studio/src/stores/secrets.ts
rename to frontends/studio/src/stores/secrets.ts
diff --git a/studio/src/stores/settings.ts b/frontends/studio/src/stores/settings.ts
similarity index 100%
rename from studio/src/stores/settings.ts
rename to frontends/studio/src/stores/settings.ts
diff --git a/studio/src/stores/webhooks.ts b/frontends/studio/src/stores/webhooks.ts
similarity index 100%
rename from studio/src/stores/webhooks.ts
rename to frontends/studio/src/stores/webhooks.ts
diff --git a/studio/src/types.ts b/frontends/studio/src/types.ts
similarity index 100%
rename from studio/src/types.ts
rename to frontends/studio/src/types.ts
diff --git a/studio/tsconfig.json b/frontends/studio/tsconfig.json
similarity index 100%
rename from studio/tsconfig.json
rename to frontends/studio/tsconfig.json
diff --git a/studio/uno.config.ts b/frontends/studio/uno.config.ts
similarity index 69%
rename from studio/uno.config.ts
rename to frontends/studio/uno.config.ts
index bb2fa30..a90faff 100644
--- a/studio/uno.config.ts
+++ b/frontends/studio/uno.config.ts
@@ -1,4 +1,5 @@
import { defineConfig, presetWind4, presetIcons, presetTypography } from 'unocss'
+import { theme, shortcuts } from '@writekit/ui/uno.config'
export default defineConfig({
presets: [
@@ -206,54 +207,6 @@ export default defineConfig({
`,
},
],
- theme: {
- colors: {
- bg: '#fafafa',
- surface: '#ffffff',
- text: '#0a0a0a',
- muted: '#737373',
- border: '#e5e5e5',
- accent: '#10b981',
- success: '#10b981',
- warning: '#f59e0b',
- danger: '#ef4444',
- },
- fontFamily: {
- sans: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
- mono: '"SF Mono", "JetBrains Mono", "Fira Code", Consolas, monospace',
- },
- },
- shortcuts: {
- // Buttons - dark bg with light text for primary
- 'btn': 'inline-flex items-center justify-center gap-2 px-4 py-2 text-xs font-medium border border-border transition-all duration-150 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed',
- 'btn-primary': 'btn bg-text text-bg border-text hover:bg-[#1a1a1a]',
- 'btn-secondary': 'btn bg-bg text-text hover:border-muted',
- 'btn-danger': 'btn text-danger border-danger hover:bg-danger hover:text-white hover:border-danger',
- 'btn-ghost': 'btn border-transparent hover:bg-border',
-
- // Inputs - clean borders, minimal styling
- 'input': 'w-full px-3 py-2 text-sm bg-bg border border-border font-sans focus:outline-none focus:border-muted transition-colors placeholder:text-muted/60',
- 'textarea': 'input resize-none',
-
- // Labels - small, muted
- 'label': 'block text-xs text-muted font-medium mb-1',
-
- // Cards - minimal borders, no shadows
- 'card': 'bg-surface border border-border p-6',
- 'section': 'card',
-
- // Navigation - matching old dashboard
- 'nav-item': 'relative flex items-center gap-2.5 px-2.5 py-2 text-[13px] font-[450] text-muted transition-all duration-150 hover:text-text hover:bg-black/4',
- 'nav-item-active': 'relative flex items-center gap-2.5 px-2.5 py-2 text-[13px] font-[450] text-text bg-black/4 before:content-[""] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-[3px] before:h-4 before:bg-accent before:rounded-r-sm',
- 'nav-section': 'text-[10px] uppercase tracking-[0.06em] text-muted font-semibold px-2.5 pt-3 pb-1.5',
-
- // Badges
- 'badge': 'inline-flex items-center text-xs px-2 py-0.5 border',
- 'badge-draft': 'badge text-warning border-warning/40 bg-warning/10',
- 'badge-published': 'badge text-success border-success/40 bg-success/10',
-
- // Page structure
- 'page-header': 'flex items-center justify-between mb-6',
- 'page-title': 'text-base font-medium text-text',
- },
+ theme,
+ shortcuts,
})
diff --git a/studio/vite.config.ts b/frontends/studio/vite.config.ts
similarity index 100%
rename from studio/vite.config.ts
rename to frontends/studio/vite.config.ts
diff --git a/frontends/ui/package.json b/frontends/ui/package.json
new file mode 100644
index 0000000..a2e4530
--- /dev/null
+++ b/frontends/ui/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@writekit/ui",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts",
+ "./uno.config": "./uno.config.ts",
+ "./styles": "./src/styles.ts"
+ },
+ "scripts": {
+ "build": "echo 'no build needed - consumed as source'",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "unocss": "^66.5.12"
+ },
+ "devDependencies": {
+ "@iconify-json/lucide": "^1.2.82",
+ "@types/react": "^19.0.0",
+ "react": "^19.0.0",
+ "typescript": "^5.7.0"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+}
diff --git a/frontends/ui/src/ActionMenu.tsx b/frontends/ui/src/ActionMenu.tsx
new file mode 100644
index 0000000..81de3c1
--- /dev/null
+++ b/frontends/ui/src/ActionMenu.tsx
@@ -0,0 +1,92 @@
+import { useState, useRef, useEffect } from 'react'
+import type { IconComponent } from './Icons'
+import { Icons } from './Icons'
+
+export interface ActionMenuItem {
+ label: string
+ Icon?: IconComponent
+ onClick?: () => void
+ href?: string
+ external?: boolean
+ variant?: 'default' | 'danger'
+}
+
+interface ActionMenuProps {
+ items: ActionMenuItem[]
+ className?: string
+}
+
+export function ActionMenu({ items, className = '' }: ActionMenuProps) {
+ const [open, setOpen] = useState(false)
+ const containerRef = useRef
(null)
+
+ useEffect(() => {
+ function handleClickOutside(e: MouseEvent) {
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
+ setOpen(false)
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => document.removeEventListener('mousedown', handleClickOutside)
+ }, [])
+
+ return (
+
+
+
+ {open && (
+
+ {items.map((item, i) => {
+ const itemClass = `w-full flex items-center gap-2.5 px-3 py-2 text-sm transition-colors ${
+ item.variant === 'danger'
+ ? 'text-danger hover:bg-danger/5'
+ : 'text-text hover:bg-bg'
+ }`
+
+ if (item.href) {
+ return (
+
setOpen(false)}
+ >
+ {item.Icon && }
+ {item.label}
+
+ )
+ }
+
+ return (
+
+ )
+ })}
+
+ )}
+
+ )
+}
diff --git a/frontends/ui/src/Badge.tsx b/frontends/ui/src/Badge.tsx
new file mode 100644
index 0000000..f110645
--- /dev/null
+++ b/frontends/ui/src/Badge.tsx
@@ -0,0 +1,20 @@
+type BadgeVariant = 'draft' | 'published' | 'default'
+
+interface BadgeProps {
+ variant?: BadgeVariant
+ children: string
+}
+
+const variantClasses: Record = {
+ draft: 'badge-draft',
+ published: 'badge-published',
+ default: 'badge text-muted border-border',
+}
+
+export function Badge({ variant = 'default', children }: BadgeProps) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/frontends/ui/src/Button.tsx b/frontends/ui/src/Button.tsx
new file mode 100644
index 0000000..e0bc6c4
--- /dev/null
+++ b/frontends/ui/src/Button.tsx
@@ -0,0 +1,64 @@
+import type { ButtonHTMLAttributes, AnchorHTMLAttributes, ReactNode } from 'react'
+import { Icons, type IconComponent } from './Icons'
+
+type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost' | 'accent'
+
+type ButtonBaseProps = {
+ variant?: ButtonVariant
+ loading?: boolean
+ Icon?: IconComponent
+ children: ReactNode
+}
+
+type ButtonAsButton = ButtonBaseProps & ButtonHTMLAttributes & { href?: never }
+type ButtonAsLink = ButtonBaseProps & AnchorHTMLAttributes & { href: string }
+
+type ButtonProps = ButtonAsButton | ButtonAsLink
+
+const variantClasses: Record = {
+ primary: 'btn-primary',
+ secondary: 'btn-secondary',
+ danger: 'btn-danger',
+ ghost: 'btn-ghost',
+ accent: 'btn-accent',
+}
+
+export function Button({
+ variant = 'secondary',
+ loading = false,
+ Icon,
+ children,
+ className = '',
+ ...props
+}: ButtonProps) {
+ const content = (
+ <>
+ {loading ? (
+
+ ) : Icon ? (
+
+ ) : null}
+ {children}
+ >
+ )
+
+ if ('href' in props && props.href) {
+ const { href, ...linkProps } = props
+ return (
+
+ {content}
+
+ )
+ }
+
+ const { disabled, ...buttonProps } = props as ButtonAsButton
+ return (
+
+ )
+}
diff --git a/frontends/ui/src/Dropdown.tsx b/frontends/ui/src/Dropdown.tsx
new file mode 100644
index 0000000..315e323
--- /dev/null
+++ b/frontends/ui/src/Dropdown.tsx
@@ -0,0 +1,86 @@
+import { useState, useRef, useEffect } from 'react'
+import type { IconComponent } from './Icons'
+import { Icons } from './Icons'
+
+export interface DropdownOption {
+ value: T
+ label: string
+ Icon?: IconComponent
+ description?: string
+}
+
+interface DropdownProps {
+ value: T
+ onChange: (value: T) => void
+ options: DropdownOption[]
+ placeholder?: string
+ className?: string
+}
+
+export function Dropdown({
+ value,
+ onChange,
+ options,
+ placeholder = 'Select...',
+ className = '',
+}: DropdownProps) {
+ const [open, setOpen] = useState(false)
+ const containerRef = useRef(null)
+
+ const selected = options.find(opt => opt.value === value)
+
+ useEffect(() => {
+ function handleClickOutside(e: MouseEvent) {
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
+ setOpen(false)
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => document.removeEventListener('mousedown', handleClickOutside)
+ }, [])
+
+ return (
+
+
+
+ {open && (
+
+ {options.map((opt) => (
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/frontends/ui/src/Icons.tsx b/frontends/ui/src/Icons.tsx
new file mode 100644
index 0000000..cf363c8
--- /dev/null
+++ b/frontends/ui/src/Icons.tsx
@@ -0,0 +1,50 @@
+import type { ComponentProps, ReactElement } from 'react'
+
+export type IconProps = ComponentProps<'span'>
+export type IconComponent = (props: IconProps) => ReactElement
+
+function createIcon(className: string): IconComponent {
+ return function Icon(props: IconProps) {
+ return
+ }
+}
+
+export const Icons = {
+ // Navigation
+ Home: createIcon('i-lucide-home'),
+ Settings: createIcon('i-lucide-settings'),
+ ExternalLink: createIcon('i-lucide-external-link'),
+
+ // Actions
+ Plus: createIcon('i-lucide-plus'),
+ Edit: createIcon('i-lucide-pencil'),
+ Trash: createIcon('i-lucide-trash-2'),
+ Copy: createIcon('i-lucide-copy'),
+ Search: createIcon('i-lucide-search'),
+ Close: createIcon('i-lucide-x'),
+ Check: createIcon('i-lucide-check'),
+ Loader: createIcon('i-lucide-loader-2'),
+ Save: createIcon('i-lucide-save'),
+
+ // UI
+ ChevronDown: createIcon('i-lucide-chevron-down'),
+ ChevronUp: createIcon('i-lucide-chevron-up'),
+ ChevronRight: createIcon('i-lucide-chevron-right'),
+ ArrowLeft: createIcon('i-lucide-arrow-left'),
+ ArrowRight: createIcon('i-lucide-arrow-right'),
+ MoreHorizontal: createIcon('i-lucide-more-horizontal'),
+
+ // Status
+ CheckCircle: createIcon('i-lucide-check-circle'),
+ AlertCircle: createIcon('i-lucide-alert-circle'),
+ AlertTriangle: createIcon('i-lucide-alert-triangle'),
+ Info: createIcon('i-lucide-info'),
+
+ // Misc
+ Eye: createIcon('i-lucide-eye'),
+ EyeOff: createIcon('i-lucide-eye-off'),
+ Link: createIcon('i-lucide-link'),
+ Globe: createIcon('i-lucide-globe'),
+} as const
+
+export { createIcon }
diff --git a/frontends/ui/src/Input.tsx b/frontends/ui/src/Input.tsx
new file mode 100644
index 0000000..dc2c90e
--- /dev/null
+++ b/frontends/ui/src/Input.tsx
@@ -0,0 +1,50 @@
+import type { InputHTMLAttributes, TextareaHTMLAttributes } from 'react'
+import type { IconComponent } from './Icons'
+
+interface InputProps extends Omit, 'onChange'> {
+ value: string
+ onChange: (value: string) => void
+ Icon?: IconComponent
+}
+
+interface TextareaProps extends Omit, 'onChange'> {
+ value: string
+ onChange: (value: string) => void
+ rows?: number
+}
+
+export function Input({ value, onChange, Icon, className = '', ...props }: InputProps) {
+ if (Icon) {
+ return (
+
+
+ onChange(e.target.value)}
+ {...props}
+ />
+
+ )
+ }
+ return (
+ onChange(e.target.value)}
+ {...props}
+ />
+ )
+}
+
+export function Textarea({ value, onChange, rows = 4, className = '', ...props }: TextareaProps) {
+ return (
+