Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
H
hrsystem
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
hrsystem
Commits
64cc390e
Commit
64cc390e
authored
Apr 04, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 8 files via Son of Anton
parent
60c96940
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
726 additions
and
0 deletions
+726
-0
__init__.py
backend/routes/__init__.py
+2
-0
design_routes.py
backend/routes/design_routes.py
+31
-0
design_templates.py
backend/services/design_templates.py
+138
-0
ColorSystemPreview.jsx
frontend/src/components/ColorSystemPreview.jsx
+155
-0
DesignPreview.jsx
frontend/src/components/DesignPreview.jsx
+209
-0
SpacingPreview.jsx
frontend/src/components/SpacingPreview.jsx
+78
-0
TypographyPreview.jsx
frontend/src/components/TypographyPreview.jsx
+47
-0
UIDetector.jsx
frontend/src/components/UIDetector.jsx
+66
-0
No files found.
backend/routes/__init__.py
0 → 100644
View file @
64cc390e
# This file intentionally left mostly empty.
# All route modules are imported directly in main.py.
\ No newline at end of file
backend/routes/design_routes.py
0 → 100644
View file @
64cc390e
"""
Design template routes — serve UI component library.
"""
from
fastapi
import
APIRouter
,
Depends
from
backend.auth
import
get_current_user
from
backend.services.design_templates
import
get_templates
,
get_template
,
list_categories
router
=
APIRouter
()
@
router
.
get
(
"/templates"
)
def
list_templates
(
category
:
str
=
None
,
user
=
Depends
(
get_current_user
)):
templates
=
get_templates
(
category
)
return
[
{
"key"
:
k
,
"name"
:
v
[
"name"
],
"category"
:
v
[
"category"
]}
for
k
,
v
in
templates
.
items
()
]
@
router
.
get
(
"/templates/{key}"
)
def
get_template_detail
(
key
:
str
,
user
=
Depends
(
get_current_user
)):
t
=
get_template
(
key
)
if
not
t
:
return
{
"error"
:
"Template not found"
}
return
t
@
router
.
get
(
"/categories"
)
def
list_template_categories
(
user
=
Depends
(
get_current_user
)):
return
list_categories
()
\ No newline at end of file
backend/services/design_templates.py
0 → 100644
View file @
64cc390e
"""
Design template library — reusable UI patterns for Son of Anton.
Provides pre-built Tailwind HTML component snippets.
"""
COMPONENT_LIBRARY
=
{
"hero_gradient"
:
{
"name"
:
"Hero Section — Gradient"
,
"category"
:
"hero"
,
"code"
:
"""<section class="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950">
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-indigo-900/20 via-transparent to-transparent"></div>
<div class="absolute inset-0 bg-grid-white/[0.02]"></div>
<div class="relative z-10 max-w-5xl mx-auto px-6 text-center">
<div class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-300 text-sm mb-8 animate-fade-in">
<span class="w-2 h-2 rounded-full bg-indigo-400 animate-pulse"></span>
Now Available
</div>
<h1 class="text-5xl md:text-7xl font-extrabold text-white tracking-tight mb-6 animate-fade-in" style="animation-delay:0.1s">
Build something<br/><span class="bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400">extraordinary</span>
</h1>
<p class="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto mb-10 animate-fade-in" style="animation-delay:0.2s">
The modern platform for teams who ship fast. Beautiful by default, powerful under the hood.
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 animate-fade-in" style="animation-delay:0.3s">
<a href="#" class="px-8 py-3.5 bg-indigo-500 hover:bg-indigo-400 text-white font-semibold rounded-xl transition-all hover:shadow-lg hover:shadow-indigo-500/25 hover:-translate-y-0.5">
Get Started Free
</a>
<a href="#" class="px-8 py-3.5 bg-white/5 hover:bg-white/10 text-white font-semibold rounded-xl border border-white/10 transition-all">
See Demo →
</a>
</div>
</div>
</section>"""
,
},
"card_grid"
:
{
"name"
:
"Feature Cards Grid"
,
"category"
:
"features"
,
"code"
:
"""<section class="py-24 bg-gray-950">
<div class="max-w-6xl mx-auto px-6">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">Everything you need</h2>
<p class="text-gray-400 text-lg max-w-xl mx-auto">Powerful features packed into a beautiful interface</p>
</div>
<div class="grid md:grid-cols-3 gap-6">
<div class="group p-6 rounded-2xl bg-gray-900/50 border border-gray-800 hover:border-indigo-500/30 transition-all hover:shadow-lg hover:shadow-indigo-500/5">
<div class="w-12 h-12 rounded-xl bg-indigo-500/10 flex items-center justify-center mb-4 group-hover:bg-indigo-500/20 transition">
<i data-lucide="zap" class="w-6 h-6 text-indigo-400"></i>
</div>
<h3 class="text-lg font-semibold text-white mb-2">Lightning Fast</h3>
<p class="text-gray-400 text-sm leading-relaxed">Built for speed. Every interaction feels instant with our optimized architecture.</p>
</div>
<div class="group p-6 rounded-2xl bg-gray-900/50 border border-gray-800 hover:border-purple-500/30 transition-all hover:shadow-lg hover:shadow-purple-500/5">
<div class="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center mb-4 group-hover:bg-purple-500/20 transition">
<i data-lucide="shield" class="w-6 h-6 text-purple-400"></i>
</div>
<h3 class="text-lg font-semibold text-white mb-2">Enterprise Security</h3>
<p class="text-gray-400 text-sm leading-relaxed">SOC2 compliant with end-to-end encryption. Your data is always safe.</p>
</div>
<div class="group p-6 rounded-2xl bg-gray-900/50 border border-gray-800 hover:border-pink-500/30 transition-all hover:shadow-lg hover:shadow-pink-500/5">
<div class="w-12 h-12 rounded-xl bg-pink-500/10 flex items-center justify-center mb-4 group-hover:bg-pink-500/20 transition">
<i data-lucide="sparkles" class="w-6 h-6 text-pink-400"></i>
</div>
<h3 class="text-lg font-semibold text-white mb-2">AI Powered</h3>
<p class="text-gray-400 text-sm leading-relaxed">Smart suggestions and automation that learn from your workflow.</p>
</div>
</div>
</div>
</section>"""
,
},
"pricing_table"
:
{
"name"
:
"Pricing — 3 Tiers"
,
"category"
:
"pricing"
,
"code"
:
"""<section class="py-24 bg-gray-950">
<div class="max-w-6xl mx-auto px-6">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">Simple pricing</h2>
<p class="text-gray-400 text-lg">No hidden fees. Cancel anytime.</p>
</div>
<div class="grid md:grid-cols-3 gap-6 items-start">
<div class="p-8 rounded-2xl bg-gray-900/50 border border-gray-800">
<h3 class="text-lg font-semibold text-white mb-1">Starter</h3>
<p class="text-gray-400 text-sm mb-6">For individuals</p>
<div class="mb-6"><span class="text-4xl font-bold text-white">$9</span><span class="text-gray-400">/mo</span></div>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> 5 projects</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> 10GB storage</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Community support</li>
</ul>
<button class="w-full py-3 rounded-xl bg-white/5 hover:bg-white/10 text-white font-medium border border-white/10 transition">Get Started</button>
</div>
<div class="p-8 rounded-2xl bg-gradient-to-b from-indigo-500/10 to-gray-900/50 border border-indigo-500/30 relative">
<div class="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-0.5 bg-indigo-500 text-white text-xs font-bold rounded-full">POPULAR</div>
<h3 class="text-lg font-semibold text-white mb-1">Pro</h3>
<p class="text-gray-400 text-sm mb-6">For teams</p>
<div class="mb-6"><span class="text-4xl font-bold text-white">$29</span><span class="text-gray-400">/mo</span></div>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Unlimited projects</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> 100GB storage</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Priority support</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Advanced analytics</li>
</ul>
<button class="w-full py-3 rounded-xl bg-indigo-500 hover:bg-indigo-400 text-white font-semibold transition hover:shadow-lg hover:shadow-indigo-500/25">Get Started</button>
</div>
<div class="p-8 rounded-2xl bg-gray-900/50 border border-gray-800">
<h3 class="text-lg font-semibold text-white mb-1">Enterprise</h3>
<p class="text-gray-400 text-sm mb-6">Custom solutions</p>
<div class="mb-6"><span class="text-4xl font-bold text-white">Custom</span></div>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Everything in Pro</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Unlimited storage</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Dedicated support</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Custom integrations</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> SLA guarantee</li>
</ul>
<button class="w-full py-3 rounded-xl bg-white/5 hover:bg-white/10 text-white font-medium border border-white/10 transition">Contact Sales</button>
</div>
</div>
</div>
</section>"""
,
},
}
def
get_templates
(
category
=
None
):
"""Return all templates, optionally filtered by category."""
if
category
:
return
{
k
:
v
for
k
,
v
in
COMPONENT_LIBRARY
.
items
()
if
v
[
"category"
]
==
category
}
return
COMPONENT_LIBRARY
def
get_template
(
key
):
"""Get a specific template by key."""
return
COMPONENT_LIBRARY
.
get
(
key
)
def
list_categories
():
"""List all available template categories."""
return
list
(
set
(
v
[
"category"
]
for
v
in
COMPONENT_LIBRARY
.
values
()))
\ No newline at end of file
frontend/src/components/ColorSystemPreview.jsx
0 → 100644
View file @
64cc390e
import
React
,
{
useState
,
useMemo
}
from
"react"
;
import
{
Copy
,
Check
,
Palette
,
Sun
,
Moon
}
from
"lucide-react"
;
function
hexToHSL
(
hex
)
{
hex
=
hex
.
replace
(
"#"
,
""
);
const
r
=
parseInt
(
hex
.
substr
(
0
,
2
),
16
)
/
255
;
const
g
=
parseInt
(
hex
.
substr
(
2
,
2
),
16
)
/
255
;
const
b
=
parseInt
(
hex
.
substr
(
4
,
2
),
16
)
/
255
;
const
max
=
Math
.
max
(
r
,
g
,
b
),
min
=
Math
.
min
(
r
,
g
,
b
);
let
h
=
0
,
s
=
0
,
l
=
(
max
+
min
)
/
2
;
if
(
max
!==
min
)
{
const
d
=
max
-
min
;
s
=
l
>
0.5
?
d
/
(
2
-
max
-
min
)
:
d
/
(
max
+
min
);
switch
(
max
)
{
case
r
:
h
=
((
g
-
b
)
/
d
+
(
g
<
b
?
6
:
0
))
/
6
;
break
;
case
g
:
h
=
((
b
-
r
)
/
d
+
2
)
/
6
;
break
;
case
b
:
h
=
((
r
-
g
)
/
d
+
4
)
/
6
;
break
;
}
}
return
{
h
:
Math
.
round
(
h
*
360
),
s
:
Math
.
round
(
s
*
100
),
l
:
Math
.
round
(
l
*
100
)
};
}
function
hslToHex
(
h
,
s
,
l
)
{
s
/=
100
;
l
/=
100
;
const
a
=
s
*
Math
.
min
(
l
,
1
-
l
);
const
f
=
(
n
)
=>
{
const
k
=
(
n
+
h
/
30
)
%
12
;
const
color
=
l
-
a
*
Math
.
max
(
Math
.
min
(
k
-
3
,
9
-
k
,
1
),
-
1
);
return
Math
.
round
(
255
*
color
).
toString
(
16
).
padStart
(
2
,
"0"
);
};
return
`#
${
f
(
0
)}${
f
(
8
)}${
f
(
4
)}
`
;
}
function
generateScale
(
hex
)
{
const
{
h
,
s
}
=
hexToHSL
(
hex
);
return
[
{
step
:
50
,
hex
:
hslToHex
(
h
,
Math
.
min
(
s
+
5
,
100
),
97
)
},
{
step
:
100
,
hex
:
hslToHex
(
h
,
Math
.
min
(
s
+
5
,
100
),
94
)
},
{
step
:
200
,
hex
:
hslToHex
(
h
,
s
,
86
)
},
{
step
:
300
,
hex
:
hslToHex
(
h
,
s
,
76
)
},
{
step
:
400
,
hex
:
hslToHex
(
h
,
s
,
64
)
},
{
step
:
500
,
hex
:
hslToHex
(
h
,
s
,
50
)
},
{
step
:
600
,
hex
:
hslToHex
(
h
,
s
,
40
)
},
{
step
:
700
,
hex
:
hslToHex
(
h
,
s
,
32
)
},
{
step
:
800
,
hex
:
hslToHex
(
h
,
s
,
24
)
},
{
step
:
900
,
hex
:
hslToHex
(
h
,
s
,
16
)
},
{
step
:
950
,
hex
:
hslToHex
(
h
,
Math
.
min
(
s
+
10
,
100
),
10
)
},
];
}
function
contrastRatio
(
hex1
,
hex2
)
{
function
luminance
(
hex
)
{
const
rgb
=
[
parseInt
(
hex
.
slice
(
1
,
3
),
16
),
parseInt
(
hex
.
slice
(
3
,
5
),
16
),
parseInt
(
hex
.
slice
(
5
,
7
),
16
)];
const
[
r
,
g
,
b
]
=
rgb
.
map
((
v
)
=>
{
v
/=
255
;
return
v
<=
0.03928
?
v
/
12.92
:
Math
.
pow
((
v
+
0.055
)
/
1.055
,
2.4
);
});
return
0.2126
*
r
+
0.7152
*
g
+
0.0722
*
b
;
}
const
l1
=
luminance
(
hex1
),
l2
=
luminance
(
hex2
);
const
lighter
=
Math
.
max
(
l1
,
l2
),
darker
=
Math
.
min
(
l1
,
l2
);
return
((
lighter
+
0.05
)
/
(
darker
+
0.05
)).
toFixed
(
2
);
}
function
wcagGrade
(
ratio
)
{
if
(
ratio
>=
7
)
return
{
grade
:
"AAA"
,
color
:
"text-green-400"
};
if
(
ratio
>=
4.5
)
return
{
grade
:
"AA"
,
color
:
"text-green-400"
};
if
(
ratio
>=
3
)
return
{
grade
:
"AA Large"
,
color
:
"text-yellow-400"
};
return
{
grade
:
"Fail"
,
color
:
"text-red-400"
};
}
export
default
function
ColorSystemPreview
({
colors
})
{
const
[
copiedColor
,
setCopiedColor
]
=
useState
(
null
);
// Parse colors — expects array of { name, hex } or object { primary: "#xxx", ... }
const
colorEntries
=
useMemo
(()
=>
{
if
(
Array
.
isArray
(
colors
))
return
colors
;
if
(
typeof
colors
===
"object"
)
{
return
Object
.
entries
(
colors
).
map
(([
name
,
hex
])
=>
({
name
,
hex
}));
}
return
[];
},
[
colors
]);
function
handleCopy
(
hex
)
{
navigator
.
clipboard
.
writeText
(
hex
);
setCopiedColor
(
hex
);
setTimeout
(()
=>
setCopiedColor
(
null
),
1500
);
}
return
(
<
div
className=
"rounded-xl border border-anton-border bg-anton-card overflow-hidden"
>
<
div
className=
"px-4 py-3 border-b border-anton-border flex items-center gap-2"
>
<
Palette
size=
{
14
}
className=
"text-anton-accent"
/>
<
span
className=
"text-xs font-bold text-white uppercase tracking-wider"
>
Color System
</
span
>
</
div
>
<
div
className=
"p-4 space-y-6"
>
{
colorEntries
.
map
(({
name
,
hex
})
=>
{
const
scale
=
generateScale
(
hex
);
const
whiteContrast
=
contrastRatio
(
hex
,
"#ffffff"
);
const
blackContrast
=
contrastRatio
(
hex
,
"#000000"
);
const
wcagWhite
=
wcagGrade
(
whiteContrast
);
const
wcagBlack
=
wcagGrade
(
blackContrast
);
return
(
<
div
key=
{
name
}
>
<
div
className=
"flex items-center justify-between mb-2"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"w-4 h-4 rounded-full border border-white/10"
style=
{
{
backgroundColor
:
hex
}
}
/>
<
span
className=
"text-sm font-semibold text-white capitalize"
>
{
name
}
</
span
>
<
span
className=
"text-[10px] font-mono text-anton-muted"
>
{
hex
}
</
span
>
</
div
>
<
div
className=
"flex items-center gap-3 text-[10px]"
>
<
span
className=
"text-anton-muted"
>
vs white:
{
whiteContrast
}
:1
<
span
className=
{
wcagWhite
.
color
}
>
{
wcagWhite
.
grade
}
</
span
>
</
span
>
<
span
className=
"text-anton-muted"
>
vs black:
{
blackContrast
}
:1
<
span
className=
{
wcagBlack
.
color
}
>
{
wcagBlack
.
grade
}
</
span
>
</
span
>
</
div
>
</
div
>
<
div
className=
"flex rounded-lg overflow-hidden"
>
{
scale
.
map
(({
step
,
hex
:
scaleHex
})
=>
(
<
button
key=
{
step
}
onClick=
{
()
=>
handleCopy
(
scaleHex
)
}
className=
"flex-1 h-12 relative group transition-transform hover:scale-y-110 hover:z-10"
style=
{
{
backgroundColor
:
scaleHex
}
}
title=
{
`${name}-${step}: ${scaleHex}`
}
>
<
span
className=
{
`absolute inset-0 flex flex-col items-center justify-center opacity-0 group-hover:opacity-100 transition text-[9px] font-mono ${step <= 400 ? "text-gray-900" : "text-white"}`
}
>
<
span
className=
"font-bold"
>
{
step
}
</
span
>
<
span
>
{
copiedColor
===
scaleHex
?
"✓"
:
scaleHex
}
</
span
>
</
span
>
</
button
>
))
}
</
div
>
</
div
>
);
})
}
</
div
>
{
/* Tailwind CSS config output */
}
<
div
className=
"px-4 py-3 bg-[#0d0d1a] border-t border-anton-border"
>
<
div
className=
"text-[10px] text-anton-muted mb-1 font-bold uppercase"
>
Tailwind Config
</
div
>
<
pre
className=
"text-[10px] font-mono text-green-300/80 overflow-x-auto whitespace-pre"
>
{
`colors: {\n${colorEntries.map(({ name, hex }) => {
const scale = generateScale(hex);
return `
'${name}'
:
{
\
n$
{
scale
.
map
(({
step
,
hex
:
h
})
=>
` ${step}: '${h}',`
).
join
(
"
\n
"
)}
\
n
},
`;
}).join("\n")}\n}`
}
</
pre
>
</
div
>
</
div
>
);
}
\ No newline at end of file
frontend/src/components/DesignPreview.jsx
0 → 100644
View file @
64cc390e
import
React
,
{
useRef
,
useEffect
,
useState
,
useMemo
}
from
"react"
;
import
{
Monitor
,
Tablet
,
Smartphone
,
Maximize2
,
Minimize2
,
Copy
,
Check
,
Download
,
RefreshCw
,
Sun
,
Moon
,
Code2
,
Eye
}
from
"lucide-react"
;
const
VIEWPORTS
=
[
{
key
:
"desktop"
,
label
:
"Desktop"
,
width
:
"100%"
,
icon
:
Monitor
},
{
key
:
"tablet"
,
label
:
"Tablet"
,
width
:
"768px"
,
icon
:
Tablet
},
{
key
:
"mobile"
,
label
:
"Mobile"
,
width
:
"375px"
,
icon
:
Smartphone
},
];
const
CDN_LIBS
=
{
tailwind
:
'<script src="https://cdn.tailwindcss.com"></script>'
,
alpine
:
'<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>'
,
lucide
:
'<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>'
,
animate
:
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>'
,
googleFonts
:
'<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"/>'
,
};
const
TAILWIND_CONFIG
=
`
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] },
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
'slide-up': 'slideUp 0.5s ease-out',
'slide-in-right': 'slideInRight 0.3s ease-out',
'bounce-in': 'bounceIn 0.6s ease-out',
'pulse-slow': 'pulse 3s ease-in-out infinite',
'float': 'float 6s ease-in-out infinite',
},
keyframes: {
fadeIn: { from: { opacity: 0 }, to: { opacity: 1 } },
slideUp: { from: { opacity: 0, transform: 'translateY(20px)' }, to: { opacity: 1, transform: 'translateY(0)' } },
slideInRight: { from: { transform: 'translateX(100%)' }, to: { transform: 'translateX(0)' } },
bounceIn: { '0%': { opacity: 0, transform: 'scale(0.3)' }, '50%': { transform: 'scale(1.05)' }, '70%': { transform: 'scale(0.9)' }, '100%': { opacity: 1, transform: 'scale(1)' } },
float: { '0%, 100%': { transform: 'translateY(0)' }, '50%': { transform: 'translateY(-10px)' } },
},
},
},
}
</script>`
;
const
BASE_STYLES
=
`
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
body { font-family: 'Inter', system-ui, sans-serif; line-height: 1.6; }
img { max-width: 100%; height: auto; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(155,155,155,0.3); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(155,155,155,0.5); }
.dark { color-scheme: dark; }
</style>`
;
function
buildFullHtml
(
code
,
darkMode
)
{
const
darkClass
=
darkMode
?
' class="dark"'
:
''
;
// Detect if user provided full HTML
if
(
code
.
trim
().
startsWith
(
"<!DOCTYPE"
)
||
code
.
trim
().
startsWith
(
"<html"
))
{
return
code
;
}
// Detect if it has <head> or <body>
if
(
code
.
includes
(
"<head>"
)
||
code
.
includes
(
"<body>"
))
{
return
`<!DOCTYPE html><html lang="en"
${
darkClass
}
>
${
code
}
</html>`
;
}
// Pure component/body content — wrap with full page
return
`<!DOCTYPE html>
<html lang="en"
${
darkClass
}
>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
${
CDN_LIBS
.
googleFonts
}
${
CDN_LIBS
.
tailwind
}
${
TAILWIND_CONFIG
}
${
CDN_LIBS
.
animate
}
${
CDN_LIBS
.
lucide
}
${
BASE_STYLES
}
</head>
<body class="
${
darkMode
?
'bg-gray-950 text-white'
:
'bg-white text-gray-900'
}
">
${
code
}
<script>
try { lucide.createIcons(); } catch(e) {}
</script>
</body>
</html>`
;
}
export
default
function
DesignPreview
({
code
,
title
})
{
const
iframeRef
=
useRef
(
null
);
const
[
viewport
,
setViewport
]
=
useState
(
"desktop"
);
const
[
expanded
,
setExpanded
]
=
useState
(
false
);
const
[
darkMode
,
setDarkMode
]
=
useState
(
false
);
const
[
showCode
,
setShowCode
]
=
useState
(
false
);
const
[
copied
,
setCopied
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
null
);
const
[
iframeKey
,
setIframeKey
]
=
useState
(
0
);
const
fullHtml
=
useMemo
(()
=>
buildFullHtml
(
code
,
darkMode
),
[
code
,
darkMode
]);
useEffect
(()
=>
{
const
iframe
=
iframeRef
.
current
;
if
(
!
iframe
)
return
;
try
{
const
blob
=
new
Blob
([
fullHtml
],
{
type
:
"text/html"
});
const
url
=
URL
.
createObjectURL
(
blob
);
iframe
.
src
=
url
;
setError
(
null
);
return
()
=>
URL
.
revokeObjectURL
(
url
);
}
catch
(
e
)
{
setError
(
e
.
message
);
}
},
[
fullHtml
,
iframeKey
]);
function
handleCopy
()
{
navigator
.
clipboard
.
writeText
(
code
);
setCopied
(
true
);
setTimeout
(()
=>
setCopied
(
false
),
2000
);
}
function
handleDownload
()
{
const
blob
=
new
Blob
([
fullHtml
],
{
type
:
"text/html"
});
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
a
.
download
=
`
${(
title
||
"design"
).
replace
(
/
[^
a-zA-Z0-9
]
/g
,
"-"
)}
.html`
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
const
vp
=
VIEWPORTS
.
find
((
v
)
=>
v
.
key
===
viewport
);
const
containerClass
=
expanded
?
"fixed inset-0 z-50 bg-black/90 flex flex-col p-4"
:
"rounded-xl border border-anton-border overflow-hidden bg-anton-card"
;
return
(
<
div
className=
{
containerClass
}
>
{
/* Toolbar */
}
<
div
className=
"flex items-center justify-between px-3 py-2 bg-anton-surface border-b border-anton-border gap-2"
>
<
div
className=
"flex items-center gap-1"
>
<
span
className=
"text-[10px] font-bold text-anton-accent uppercase tracking-wider mr-2"
>
{
title
||
"UI Preview"
}
</
span
>
{
VIEWPORTS
.
map
((
v
)
=>
{
const
Icon
=
v
.
icon
;
return
(
<
button
key=
{
v
.
key
}
onClick=
{
()
=>
setViewport
(
v
.
key
)
}
className=
{
`p-1.5 rounded-md transition-colors ${viewport === v.key ? "bg-anton-accent/20 text-anton-accent" : "text-anton-muted hover:text-white"}`
}
title=
{
v
.
label
}
>
<
Icon
size=
{
14
}
/>
</
button
>
);
})
}
</
div
>
<
div
className=
"flex items-center gap-1"
>
<
button
onClick=
{
()
=>
setDarkMode
(
!
darkMode
)
}
className=
"p-1.5 rounded-md text-anton-muted hover:text-white transition"
title=
{
darkMode
?
"Light mode"
:
"Dark mode"
}
>
{
darkMode
?
<
Sun
size=
{
14
}
/>
:
<
Moon
size=
{
14
}
/>
}
</
button
>
<
button
onClick=
{
()
=>
setShowCode
(
!
showCode
)
}
className=
{
`p-1.5 rounded-md transition ${showCode ? "bg-anton-accent/20 text-anton-accent" : "text-anton-muted hover:text-white"}`
}
title=
"Toggle code"
>
{
showCode
?
<
Eye
size=
{
14
}
/>
:
<
Code2
size=
{
14
}
/>
}
</
button
>
<
button
onClick=
{
()
=>
setIframeKey
((
k
)
=>
k
+
1
)
}
className=
"p-1.5 rounded-md text-anton-muted hover:text-white transition"
title=
"Refresh"
>
<
RefreshCw
size=
{
14
}
/>
</
button
>
<
button
onClick=
{
handleCopy
}
className=
"p-1.5 rounded-md text-anton-muted hover:text-white transition"
title=
"Copy HTML"
>
{
copied
?
<
Check
size=
{
14
}
className=
"text-green-400"
/>
:
<
Copy
size=
{
14
}
/>
}
</
button
>
<
button
onClick=
{
handleDownload
}
className=
"p-1.5 rounded-md text-anton-muted hover:text-white transition"
title=
"Download HTML"
>
<
Download
size=
{
14
}
/>
</
button
>
<
button
onClick=
{
()
=>
setExpanded
(
!
expanded
)
}
className=
"p-1.5 rounded-md text-anton-muted hover:text-white transition"
title=
{
expanded
?
"Minimize"
:
"Fullscreen"
}
>
{
expanded
?
<
Minimize2
size=
{
14
}
/>
:
<
Maximize2
size=
{
14
}
/>
}
</
button
>
</
div
>
</
div
>
{
/* Preview Area */
}
<
div
className=
{
`flex-1 flex items-start justify-center overflow-auto bg-[#1a1a2e] ${showCode ? "" : "p-4"}`
}
style=
{
{
minHeight
:
expanded
?
"auto"
:
"400px"
}
}
>
{
showCode
?
(
<
pre
className=
"w-full h-full overflow-auto p-4 text-xs text-green-300 font-mono whitespace-pre-wrap"
>
{
code
}
</
pre
>
)
:
error
?
(
<
div
className=
"text-red-400 p-6 text-sm"
>
Preview error:
{
error
}
</
div
>
)
:
(
<
div
className=
"transition-all duration-300 h-full"
style=
{
{
width
:
vp
.
width
,
maxWidth
:
"100%"
}
}
>
<
iframe
key=
{
iframeKey
}
ref=
{
iframeRef
}
className=
"w-full bg-white rounded-lg shadow-2xl"
style=
{
{
height
:
expanded
?
"calc(100vh - 80px)"
:
"500px"
,
border
:
"none"
}
}
sandbox=
"allow-scripts allow-same-origin allow-popups allow-forms"
title=
"Design Preview"
/>
</
div
>
)
}
</
div
>
{
/* Viewport indicator */
}
<
div
className=
"flex items-center justify-center py-1.5 bg-anton-surface border-t border-anton-border"
>
<
span
className=
"text-[10px] text-anton-muted"
>
{
vp
.
label
}
{
vp
.
width
!==
"100%"
&&
`(${vp.width})`
}
•
{
darkMode
?
"Dark"
:
"Light"
}
Mode
</
span
>
</
div
>
</
div
>
);
}
\ No newline at end of file
frontend/src/components/SpacingPreview.jsx
0 → 100644
View file @
64cc390e
import
React
from
"react"
;
const
SPACING_SCALE
=
[
{
name
:
"0.5"
,
px
:
2
},
{
name
:
"1"
,
px
:
4
},
{
name
:
"1.5"
,
px
:
6
},
{
name
:
"2"
,
px
:
8
},
{
name
:
"3"
,
px
:
12
},
{
name
:
"4"
,
px
:
16
},
{
name
:
"5"
,
px
:
20
},
{
name
:
"6"
,
px
:
24
},
{
name
:
"8"
,
px
:
32
},
{
name
:
"10"
,
px
:
40
},
{
name
:
"12"
,
px
:
48
},
{
name
:
"16"
,
px
:
64
},
{
name
:
"20"
,
px
:
80
},
{
name
:
"24"
,
px
:
96
},
];
const
BORDER_RADIUS
=
[
{
name
:
"none"
,
value
:
"0"
},
{
name
:
"sm"
,
value
:
"2px"
},
{
name
:
"DEFAULT"
,
value
:
"4px"
},
{
name
:
"md"
,
value
:
"6px"
},
{
name
:
"lg"
,
value
:
"8px"
},
{
name
:
"xl"
,
value
:
"12px"
},
{
name
:
"2xl"
,
value
:
"16px"
},
{
name
:
"3xl"
,
value
:
"24px"
},
{
name
:
"full"
,
value
:
"9999px"
},
];
const
SHADOWS
=
[
{
name
:
"sm"
,
value
:
"0 1px 2px 0 rgb(0 0 0 / 0.05)"
},
{
name
:
"DEFAULT"
,
value
:
"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"
},
{
name
:
"md"
,
value
:
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
},
{
name
:
"lg"
,
value
:
"0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
},
{
name
:
"xl"
,
value
:
"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
},
{
name
:
"2xl"
,
value
:
"0 25px 50px -12px rgb(0 0 0 / 0.25)"
},
];
export
default
function
SpacingPreview
()
{
return
(
<
div
className=
"rounded-xl border border-anton-border bg-anton-card overflow-hidden"
>
<
div
className=
"px-4 py-3 border-b border-anton-border"
>
<
span
className=
"text-xs font-bold text-white uppercase tracking-wider"
>
Design Tokens
</
span
>
</
div
>
<
div
className=
"p-4 space-y-6"
>
{
/* Spacing */
}
<
div
>
<
h3
className=
"text-[11px] font-bold text-anton-accent uppercase tracking-wider mb-3"
>
Spacing Scale
</
h3
>
<
div
className=
"space-y-1.5"
>
{
SPACING_SCALE
.
map
((
s
)
=>
(
<
div
key=
{
s
.
name
}
className=
"flex items-center gap-3"
>
<
span
className=
"text-[10px] font-mono text-anton-muted w-8 text-right"
>
{
s
.
name
}
</
span
>
<
div
className=
"h-3 bg-anton-accent/30 rounded-sm transition-all"
style=
{
{
width
:
`${s.px}px`
}
}
/>
<
span
className=
"text-[9px] text-anton-muted"
>
{
s
.
px
}
px
</
span
>
</
div
>
))
}
</
div
>
</
div
>
{
/* Border Radius */
}
<
div
>
<
h3
className=
"text-[11px] font-bold text-anton-accent uppercase tracking-wider mb-3"
>
Border Radius
</
h3
>
<
div
className=
"flex flex-wrap gap-3"
>
{
BORDER_RADIUS
.
map
((
r
)
=>
(
<
div
key=
{
r
.
name
}
className=
"flex flex-col items-center gap-1"
>
<
div
className=
"w-12 h-12 bg-anton-accent/20 border border-anton-accent/40"
style=
{
{
borderRadius
:
r
.
value
}
}
/>
<
span
className=
"text-[9px] font-mono text-anton-muted"
>
{
r
.
name
}
</
span
>
</
div
>
))
}
</
div
>
</
div
>
{
/* Shadows */
}
<
div
>
<
h3
className=
"text-[11px] font-bold text-anton-accent uppercase tracking-wider mb-3"
>
Shadows
</
h3
>
<
div
className=
"flex flex-wrap gap-4"
>
{
SHADOWS
.
map
((
s
)
=>
(
<
div
key=
{
s
.
name
}
className=
"flex flex-col items-center gap-2"
>
<
div
className=
"w-16 h-16 bg-white rounded-lg"
style=
{
{
boxShadow
:
s
.
value
}
}
/>
<
span
className=
"text-[9px] font-mono text-anton-muted"
>
{
s
.
name
}
</
span
>
</
div
>
))
}
</
div
>
</
div
>
</
div
>
</
div
>
);
}
\ No newline at end of file
frontend/src/components/TypographyPreview.jsx
0 → 100644
View file @
64cc390e
import
React
from
"react"
;
const
TYPE_SCALE
=
[
{
name
:
"Display"
,
size
:
"4.5rem"
,
weight
:
800
,
lineHeight
:
1.1
,
tracking
:
"-0.02em"
},
{
name
:
"H1"
,
size
:
"3rem"
,
weight
:
700
,
lineHeight
:
1.2
,
tracking
:
"-0.015em"
},
{
name
:
"H2"
,
size
:
"2.25rem"
,
weight
:
700
,
lineHeight
:
1.25
,
tracking
:
"-0.01em"
},
{
name
:
"H3"
,
size
:
"1.875rem"
,
weight
:
600
,
lineHeight
:
1.3
,
tracking
:
"-0.005em"
},
{
name
:
"H4"
,
size
:
"1.5rem"
,
weight
:
600
,
lineHeight
:
1.35
,
tracking
:
"0"
},
{
name
:
"H5"
,
size
:
"1.25rem"
,
weight
:
600
,
lineHeight
:
1.4
,
tracking
:
"0"
},
{
name
:
"Body L"
,
size
:
"1.125rem"
,
weight
:
400
,
lineHeight
:
1.6
,
tracking
:
"0"
},
{
name
:
"Body"
,
size
:
"1rem"
,
weight
:
400
,
lineHeight
:
1.6
,
tracking
:
"0"
},
{
name
:
"Body S"
,
size
:
"0.875rem"
,
weight
:
400
,
lineHeight
:
1.5
,
tracking
:
"0.01em"
},
{
name
:
"Caption"
,
size
:
"0.75rem"
,
weight
:
500
,
lineHeight
:
1.4
,
tracking
:
"0.02em"
},
{
name
:
"Overline"
,
size
:
"0.6875rem"
,
weight
:
600
,
lineHeight
:
1.4
,
tracking
:
"0.08em"
},
];
const
SAMPLE_TEXT
=
"The quick brown fox jumps over the lazy dog"
;
export
default
function
TypographyPreview
({
fontFamily
=
"Inter"
})
{
return
(
<
div
className=
"rounded-xl border border-anton-border bg-anton-card overflow-hidden"
>
<
div
className=
"px-4 py-3 border-b border-anton-border"
>
<
span
className=
"text-xs font-bold text-white uppercase tracking-wider"
>
Type Scale
</
span
>
<
span
className=
"text-[10px] text-anton-muted ml-2"
>
—
{
fontFamily
}
</
span
>
</
div
>
<
div
className=
"divide-y divide-anton-border"
>
{
TYPE_SCALE
.
map
((
t
)
=>
(
<
div
key=
{
t
.
name
}
className=
"px-4 py-3 flex items-baseline gap-4 group hover:bg-white/[0.02] transition"
>
<
div
className=
"w-20 shrink-0"
>
<
div
className=
"text-[10px] font-bold text-anton-accent uppercase"
>
{
t
.
name
}
</
div
>
<
div
className=
"text-[9px] text-anton-muted font-mono"
>
{
t
.
size
}
/
{
t
.
weight
}
</
div
>
</
div
>
<
div
className=
"text-white truncate flex-1 transition-colors"
style=
{
{
fontFamily
,
fontSize
:
t
.
size
,
fontWeight
:
t
.
weight
,
lineHeight
:
t
.
lineHeight
,
letterSpacing
:
t
.
tracking
,
}
}
>
{
t
.
name
===
"Overline"
?
SAMPLE_TEXT
.
toUpperCase
()
:
SAMPLE_TEXT
}
</
div
>
</
div
>
))
}
</
div
>
</
div
>
);
}
\ No newline at end of file
frontend/src/components/UIDetector.jsx
0 → 100644
View file @
64cc390e
import
React
from
"react"
;
import
DesignPreview
from
"./DesignPreview"
;
import
ColorSystemPreview
from
"./ColorSystemPreview"
;
/**
* Detects UI code blocks in assistant messages and renders them
* with the enhanced DesignPreview instead of plain code blocks.
*
* Detection rules:
* - Code blocks with language "html" containing Tailwind classes
* - Code blocks with language "ui" or "design" or "preview"
* - Code blocks with filename ending in .html
*/
const
UI_LANGUAGES
=
new
Set
([
"html"
,
"ui"
,
"design"
,
"preview"
,
"htm"
]);
const
TAILWIND_INDICATORS
=
[
"className="
,
"class=
\"
"
,
"class='"
,
"flex "
,
"grid "
,
"bg-"
,
"text-"
,
"rounded-"
,
"shadow-"
,
"p-"
,
"m-"
,
"w-"
,
"h-"
,
"border-"
,
"hover:"
,
"dark:"
,
"tailwindcss"
,
"tailwind"
,
];
export
function
isUICodeBlock
(
language
,
filename
,
code
)
{
const
lang
=
(
language
||
""
).
toLowerCase
();
const
file
=
(
filename
||
""
).
toLowerCase
();
// Explicit UI language
if
(
UI_LANGUAGES
.
has
(
lang
))
return
true
;
// HTML file with Tailwind
if
((
lang
===
"html"
||
file
.
endsWith
(
".html"
))
&&
code
)
{
const
indicators
=
TAILWIND_INDICATORS
.
filter
((
i
)
=>
code
.
includes
(
i
));
return
indicators
.
length
>=
3
;
}
return
false
;
}
export
function
isColorSystem
(
code
)
{
// Detect JSON color objects
try
{
const
parsed
=
JSON
.
parse
(
code
);
if
(
typeof
parsed
===
"object"
&&
!
Array
.
isArray
(
parsed
))
{
const
values
=
Object
.
values
(
parsed
);
return
values
.
every
((
v
)
=>
typeof
v
===
"string"
&&
/^#
[
0-9a-fA-F
]{6}
$/
.
test
(
v
));
}
if
(
Array
.
isArray
(
parsed
))
{
return
parsed
.
every
((
item
)
=>
item
.
name
&&
item
.
hex
);
}
}
catch
{
}
return
false
;
}
export
function
UICodeBlockRenderer
({
language
,
filename
,
code
})
{
if
(
isColorSystem
(
code
))
{
try
{
return
<
ColorSystemPreview
colors=
{
JSON
.
parse
(
code
)
}
/>;
}
catch
{
}
}
if
(
isUICodeBlock
(
language
,
filename
,
code
))
{
return
<
DesignPreview
code=
{
code
}
title=
{
filename
||
"UI Preview"
}
/>;
}
return
null
;
// Fallback — caller should render normal CodeBlock
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment