feat(saju-ui-v2): OrnamentBloom + TopRibbon + OrnateFrame + TitleBlock

This commit is contained in:
2026-05-27 02:03:59 +09:00
parent fd84e17f0b
commit 78e7e68bb0
4 changed files with 107 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
import React from 'react';
export default function OrnamentBloom({ size = 18, color = '#D4AF37' }) {
return (
<svg width={size} height={size} viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="2.4" fill={color} />
{[0, 60, 120, 180, 240, 300].map((angle) => (
<ellipse key={angle} cx="9" cy="4" rx="1.6" ry="3" fill={color} opacity="0.7"
transform={`rotate(${angle} 9 9)`} />
))}
</svg>
);
}

View File

@@ -0,0 +1,36 @@
import React from 'react';
import hexA from './helpers/hexA';
export default function OrnateFrame({
children, color = '#D4AF37', bg = 'transparent', radius = 14, padding = '20px',
style = {}, double = false,
}) {
return (
<div style={{
position: 'relative', borderRadius: radius,
background: bg, padding,
border: `1px solid ${hexA(color, 0.45)}`,
boxShadow: 'var(--shadow-card)',
...style,
}}>
{double && (
<div style={{
position: 'absolute', inset: 4, borderRadius: radius - 4,
border: `1px solid ${hexA(color, 0.3)}`, pointerEvents: 'none',
}} />
)}
{[[0,0,0],[0,1,90],[1,1,180],[1,0,270]].map(([x,y,r], i) => (
<svg key={i} width="12" height="12" viewBox="0 0 12 12" style={{
position: 'absolute',
[x ? 'right' : 'left']: 6,
[y ? 'bottom' : 'top']: 6,
transform: `rotate(${r}deg)`,
pointerEvents: 'none',
}}>
<path d="M0 4 L0 0 L4 0" stroke={color} strokeWidth="1.2" fill="none" strokeLinecap="round" />
</svg>
))}
<div style={{ position: 'relative', zIndex: 1 }}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,37 @@
import React from 'react';
import OrnamentBloom from './OrnamentBloom';
export default function TitleBlock({
title, subtitle, color = '#1F2A44', subColor = '#6B6B6B',
center = true, withBloom = true, gold = '#D4AF37',
}) {
return (
<div style={{ textAlign: center ? 'center' : 'left' }}>
{withBloom && center && (
<div style={{
display: 'flex', justifyContent: 'center', gap: 12,
alignItems: 'center', marginBottom: 10, color: gold,
}}>
<svg width="40" height="6" viewBox="0 0 40 6">
<path d="M0 3 L36 3" stroke={gold} strokeWidth="1" />
<circle cx="38" cy="3" r="1.5" fill={gold} />
</svg>
<OrnamentBloom size={18} color={gold} />
<svg width="40" height="6" viewBox="0 0 40 6">
<circle cx="2" cy="3" r="1.5" fill={gold} />
<path d="M4 3 L40 3" stroke={gold} strokeWidth="1" />
</svg>
</div>
)}
<h1 className="font-title" style={{
margin: 0, fontSize: 30, color, letterSpacing: '-0.02em',
}}>{title}</h1>
{subtitle && (
<div style={{
marginTop: 6, fontSize: 13, color: subColor, lineHeight: 1.55,
letterSpacing: '-0.01em',
}}>{subtitle}</div>
)}
</div>
);
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
function CloudOrnament({ width = 90, color = '#D4AF37', opacity = 0.85 }) {
return (
<svg width={width} height={width / 3.5} viewBox="0 0 90 26" fill="none" opacity={opacity}>
<path d="M5 18 Q12 6 24 12 Q36 4 48 14 Q60 6 72 14 Q82 8 88 18"
stroke={color} strokeWidth="1" fill="none" />
<circle cx="24" cy="12" r="1.4" fill={color} />
<circle cx="48" cy="14" r="1.4" fill={color} />
<circle cx="72" cy="14" r="1.4" fill={color} />
</svg>
);
}
export default function TopRibbon({ color = '#D4AF37', opacity = 0.5 }) {
return (
<div style={{ display: 'flex', justifyContent: 'center', padding: '4px 0 0', opacity }}>
<CloudOrnament width={90} color={color} opacity={0.85} />
</div>
);
}