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
16 changes: 15 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,26 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stellar Developer Dashboard</title>
<script>
// Blocking script to accurately set the theme dataset attribute before initial paint
// Blocking script to restore the theme before initial paint (FOUC prevention)
(function () {
try {
var saved = localStorage.getItem('stellar-dashboard-theme');
if (saved === 'light' || saved === 'dark') {
document.documentElement.setAttribute('data-theme', saved);
} else if (saved === 'custom') {
// Restore custom theme CSS vars from localStorage
var varsRaw = localStorage.getItem('stellar-theme-vars');
if (varsRaw) {
try {
var vars = JSON.parse(varsRaw);
for (var key in vars) {
if (vars.hasOwnProperty(key)) {
document.documentElement.style.setProperty(key, vars[key]);
}
}
} catch (e) {}
}
document.documentElement.setAttribute('data-theme', 'custom');
} else {
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
Expand Down
59 changes: 59 additions & 0 deletions src/components/theme-builder/ColorPicker.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react'

const COLOR_LABELS = {
background: 'Background',
surface: 'Surface',
primary: 'Primary',
secondary: 'Secondary',
text: 'Text',
}

export default function ColorPicker({ colors, onChange }) {
return (
<div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '10px' }}>
Colors
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{Object.entries(COLOR_LABELS).map(([key, label]) => (
<div key={key} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '12px', color: 'var(--text-secondary)', fontFamily: 'var(--font-mono)', minWidth: '80px' }}>
{label}
</span>
<input
type="color"
value={colors[key]}
onChange={(e) => onChange('colors', { ...colors, [key]: e.target.value })}
style={{
width: '32px',
height: '32px',
padding: '2px',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
background: 'none',
flexShrink: 0,
}}
/>
<input
type="text"
value={colors[key]}
onChange={(e) => onChange('colors', { ...colors, [key]: e.target.value })}
style={{
flex: 1,
padding: '6px 8px',
background: 'var(--bg-elevated)',
border: '1px solid var(--border-bright)',
borderRadius: 'var(--radius-sm)',
color: 'var(--text-primary)',
fontSize: '12px',
fontFamily: 'var(--font-mono)',
outline: 'none',
}}
/>
</div>
))}
</div>
</div>
)
}
69 changes: 69 additions & 0 deletions src/components/theme-builder/FontSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react'

const FONTS = [
{ value: "'Syne', sans-serif", label: 'Syne' },
{ value: "'Space Mono', monospace", label: 'Space Mono' },
]

export default function FontSelector({ fontFamily, fontScale, onChange }) {
return (
<div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '10px' }}>
Typography
</div>

<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginBottom: '6px' }}>
Font Family
</div>
<div style={{ display: 'flex', gap: '8px' }}>
{FONTS.map((f) => (
<button
key={f.value}
onClick={() => onChange('typography', { fontFamily: f.value, fontScale })}
style={{
flex: 1,
padding: '10px 16px',
background: fontFamily === f.value ? 'var(--cyan-glow)' : 'var(--bg-elevated)',
border: `1px solid ${fontFamily === f.value ? 'var(--cyan-dim)' : 'var(--border)'}`,
borderRadius: 'var(--radius-sm)',
color: fontFamily === f.value ? 'var(--cyan)' : 'var(--text-secondary)',
fontSize: fontFamily === f.value ? '14px' : '12px',
fontFamily: f.value,
cursor: 'pointer',
textAlign: 'center',
transition: 'var(--transition)',
}}
>
{f.label}
</button>
))}
</div>
</div>

<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '6px' }}>
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>Font Scale</span>
<span style={{ fontSize: '11px', color: 'var(--text-secondary)', fontFamily: 'var(--font-mono)' }}>
{fontScale.toFixed(1)}x
</span>
</div>
<input
type="range"
min="0.75"
max="1.5"
step="0.05"
value={fontScale}
onChange={(e) => onChange('typography', { fontFamily, fontScale: parseFloat(e.target.value) })}
style={{ width: '100%', cursor: 'pointer', accentColor: 'var(--cyan)' }}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '10px', color: 'var(--text-muted)' }}>
<span>0.75×</span>
<span>1.5×</span>
</div>
</div>
</div>
</div>
)
}
68 changes: 68 additions & 0 deletions src/components/theme-builder/LivePreview.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react'
import Card from '../dashboard/Card'

export default function LivePreview({ colors, typography, spacing }) {
const borderColor = colors.primary + '33'

return (
<div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '10px' }}>
Preview
</div>
<Card
title="Theme Preview"
subtitle={`${spacing.baseUnit}px base · ${typography.fontScale.toFixed(1)}x scale`}
style={{
background: colors.surface,
borderColor: borderColor,
color: colors.text,
fontFamily: typography.fontFamily,
fontSize: `${14 * typography.fontScale}px`,
}}
>
<div style={{ padding: `calc(${spacing.baseUnit}px * 2)` }}>
<p style={{
color: colors.text,
opacity: 0.85,
fontSize: 'inherit',
lineHeight: 1.6,
marginBottom: `${spacing.baseUnit * 2}px`,
}}>
Sample body text rendered with the selected theme colors and typography.
</p>

<div style={{
background: colors.background,
border: `1px solid ${borderColor}`,
borderRadius: `${Math.max(4, spacing.baseUnit)}px`,
padding: `${spacing.baseUnit * 1.5}px`,
marginBottom: `${spacing.baseUnit * 2}px`,
}}>
<code style={{
fontFamily: "'Space Mono', monospace",
color: colors.primary,
fontSize: `${12 * typography.fontScale}px`,
}}>
const theme = {'{'} colors: {'{'}&quot;{colors.background}&quot; ... {'}'}{'}'}
</code>
</div>

<button style={{
background: colors.primary,
color: colors.background,
border: 'none',
borderRadius: `${Math.max(4, spacing.baseUnit / 2)}px`,
padding: `${spacing.baseUnit}px ${spacing.baseUnit * 2}px`,
fontFamily: typography.fontFamily,
fontSize: `${13 * typography.fontScale}px`,
fontWeight: 600,
cursor: 'pointer',
transition: 'opacity 150ms ease',
}}>
Action Button
</button>
</div>
</Card>
</div>
)
}
118 changes: 118 additions & 0 deletions src/components/theme-builder/MarketplacePanel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React, { useEffect, useState } from 'react'
import { fetchMarketplaceThemes, installMarketplaceTheme } from '../../lib/marketplace'
import { THEME_COLOR_KEYS } from '../../styles/themeTypes'

const swatchStyle = (color) => ({
width: '100%',
height: '6px',
background: color,
borderRadius: '2px',
})

export default function MarketplacePanel({ onInstall }) {
const [themes, setThemes] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)

useEffect(() => {
let cancelled = false
setLoading(true)
setError(null)
fetchMarketplaceThemes()
.then((data) => {
if (!cancelled) setThemes(data)
})
.catch((err) => {
if (!cancelled) setError(err.message || 'Failed to load marketplace')
})
.finally(() => {
if (!cancelled) setLoading(false)
})
return () => { cancelled = true }
}, [])

const handleInstall = async (themeId) => {
const theme = await installMarketplaceTheme(themeId)
if (theme) onInstall(theme)
}

return (
<div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '10px' }}>
Marketplace
</div>

{loading && (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 0' }}>
<div className="spinner" />
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>Loading themes...</span>
</div>
)}

{error && (
<div style={{ fontSize: '11px', color: 'var(--red)', padding: '8px 0' }}>
{error}
</div>
)}

{!loading && !error && themes.length === 0 && (
<div style={{ fontSize: '11px', color: 'var(--text-muted)', padding: '8px 0' }}>
No themes available.
</div>
)}

{!loading && themes.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{themes.map((theme) => (
<div
key={theme.id}
style={{
background: 'var(--bg-elevated)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)',
overflow: 'hidden',
}}
>
{/* Color swatch strip */}
<div style={{ display: 'flex', gap: '2px', padding: '6px 8px 4px' }}>
{THEME_COLOR_KEYS.map((key) => (
<div key={key} style={swatchStyle(theme.colors[key])} />
))}
</div>

{/* Info row */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 8px 6px' }}>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: '11px', fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{theme.name}
</div>
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>
{theme.author} · {theme.downloads} downloads
</div>
</div>
<button
onClick={() => handleInstall(theme.id)}
aria-label={`Install ${theme.name} theme`}
style={{
padding: '4px 10px',
background: 'var(--cyan-glow)',
border: '1px solid var(--cyan-dim)',
borderRadius: 'var(--radius-sm)',
color: 'var(--cyan)',
fontSize: '10px',
fontFamily: 'var(--font-mono)',
cursor: 'pointer',
whiteSpace: 'nowrap',
flexShrink: 0,
}}
>
Install
</button>
</div>
</div>
))}
</div>
)}
</div>
)
}
Loading
Loading