@kolkrabbi/kol-component 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -3
- package/src/atoms/AnimatedTitle.jsx +108 -0
- package/src/atoms/AssetGrid.jsx +29 -0
- package/src/atoms/Button.jsx +17 -6
- package/src/atoms/CurveOverlay.jsx +180 -0
- package/src/atoms/DocsToc.jsx +48 -0
- package/src/atoms/EmptyState.jsx +22 -0
- package/src/atoms/Figure.jsx +27 -0
- package/src/atoms/HlsVideo.jsx +56 -0
- package/src/atoms/OverlayGlassPanel.jsx +40 -0
- package/src/atoms/PriceDisplay.jsx +34 -0
- package/src/atoms/ProsePreview.jsx +53 -0
- package/src/atoms/QuantityInput.jsx +76 -114
- package/src/atoms/RotaryDial.jsx +150 -0
- package/src/atoms/SearchInput.jsx +108 -0
- package/src/atoms/TextPressure.jsx +331 -0
- package/src/atoms/TiltCard.jsx +127 -0
- package/src/atoms/TypeSample.jsx +50 -0
- package/src/atoms/TypeSpecCard.jsx +42 -0
- package/src/hooks/cssVar.js +53 -0
- package/src/hooks/useAxisAnimation.js +91 -0
- package/src/hooks/usePrefersReducedMotion.js +21 -0
- package/src/hooks/useTilt.js +49 -0
- package/src/index.js +66 -2
- package/src/molecules/AlignmentGrid.jsx +53 -0
- package/src/molecules/ArticleCard.jsx +178 -0
- package/src/molecules/CardFeatureItem.jsx +130 -0
- package/src/molecules/ColorInputRow.jsx +179 -0
- package/src/molecules/ColorRamp.jsx +114 -0
- package/src/molecules/FramedMediaBand.jsx +56 -0
- package/src/molecules/ImageBlock.jsx +34 -0
- package/src/molecules/MenuPopover.jsx +4 -27
- package/src/molecules/SelectionOverlay.jsx +108 -0
- package/src/molecules/ShapeDropdown.jsx +92 -0
- package/src/molecules/ShellDrawer.jsx +169 -0
- package/src/molecules/ShellSearchOverlay.jsx +177 -0
- package/src/molecules/SpecList.jsx +30 -0
- package/src/molecules/SpectrumControls.jsx +504 -0
- package/src/molecules/SwatchControls.jsx +217 -0
- package/src/molecules/TabsRow.jsx +87 -0
- package/src/molecules/VideoBlock.jsx +86 -0
- package/src/molecules/WorkListItem.jsx +83 -0
- package/src/molecules/foundry/SpecimenSectionHeader.jsx +89 -0
- package/src/organisms/ArticleHeader.jsx +95 -0
- package/src/organisms/AsciiCursor.jsx +526 -0
- package/src/organisms/BentoCard.jsx +187 -0
- package/src/organisms/Canvas.jsx +299 -0
- package/src/organisms/ColorLoader.jsx +155 -0
- package/src/organisms/CtaGlobal.jsx +67 -0
- package/src/organisms/DiagonalMarqueeRiver.jsx +138 -0
- package/src/organisms/EditorShell.jsx +111 -0
- package/src/organisms/ErrorBoundary.jsx +70 -0
- package/src/organisms/FeatureSplit.jsx +82 -0
- package/src/organisms/FeaturedCarousel.jsx +258 -0
- package/src/organisms/FeaturesCardSection.jsx +90 -0
- package/src/organisms/FullBleedHero.jsx +111 -0
- package/src/organisms/GalleryCarousel.jsx +83 -0
- package/src/organisms/LoaderOverlay.jsx +30 -0
- package/src/organisms/MediaViewer.jsx +95 -0
- package/src/organisms/NewsletterBand.jsx +122 -0
- package/src/organisms/ParallaxShelf.jsx +141 -0
- package/src/organisms/PortableTextRenderer.jsx +115 -0
- package/src/organisms/ProductDetailLayout.jsx +189 -0
- package/src/organisms/ScrollDriftGallery.jsx +214 -0
- package/src/organisms/SpectrumGrid.jsx +90 -0
- package/src/organisms/StackHero.jsx +83 -0
- package/src/organisms/WorkCard.jsx +120 -0
- package/src/organisms/WorkViewToggle.jsx +170 -0
- package/src/organisms/foundry/FontPreviewSection.jsx +187 -0
- package/src/organisms/foundry/FoundryCharacterSets.jsx +113 -0
- package/src/organisms/foundry/GlyphMetricsGrid.jsx +335 -0
- package/src/organisms/foundry/TypefaceHero.jsx +107 -0
- package/src/organisms/foundry/TypefaceStyleSection.jsx +163 -0
- package/src/organisms/foundry/VariableFontSection.jsx +158 -0
- package/src/organisms/foundry/glyphData.js +30 -0
- package/src/organisms/foundry/index.js +21 -0
- package/src/atoms/QuantityStepper.jsx +0 -149
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import Pill from '../../atoms/Pill.jsx'
|
|
2
|
+
import Button from '../../atoms/Button.jsx'
|
|
3
|
+
|
|
4
|
+
/* taxonomy-ok: organism — nests Pill + Button (relative imports); a full
|
|
5
|
+
* page-hero region rendering live in the specimen's own font-family. */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TypefaceHero — the specimen hero for a typeface page: a centered stack of
|
|
9
|
+
* category pill → giant display name rendered in the typeface's OWN fontFamily
|
|
10
|
+
* → in-family description → download / view-specimen CTAs → an optional
|
|
11
|
+
* licensing caption. Data-driven from one `typeface` object; the giant name is
|
|
12
|
+
* the live font-family preview (arbitrary loaded font, inline `fontFamily`, not
|
|
13
|
+
* a fixed KOL type class — only the size stops are Tailwind).
|
|
14
|
+
*
|
|
15
|
+
* Router-decoupled: the monorepo source called `useNavigate(specimenLink)`;
|
|
16
|
+
* here the "View Specimen" CTA takes an `href` and/or `onSpecimenClick` callback
|
|
17
|
+
* so the DS never depends on a router. CTA copy + the license note are
|
|
18
|
+
* prop-driven (license default off), and `displayName` falls back to
|
|
19
|
+
* `id`/`fontFamily` when the config omits it.
|
|
20
|
+
*
|
|
21
|
+
* Text casing: displayName, category, description and CTA/license strings render
|
|
22
|
+
* verbatim as authored — no text-transform.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} props
|
|
25
|
+
* @param {Object} props.typeface - Config: { displayName?, id?, fontFamily, fontStyle?, category?, description?, specimenLink? }.
|
|
26
|
+
* @param {string} props.downloadHref - Optional href for the "Download font" CTA.
|
|
27
|
+
* @param {string} props.downloadLabel - Download CTA label (default 'Download font').
|
|
28
|
+
* @param {string} props.specimenLabel - View-specimen CTA label (default 'View Specimen').
|
|
29
|
+
* @param {Function} props.onSpecimenClick - Handler for the view-specimen CTA (receives the event).
|
|
30
|
+
* @param {string} props.licenseNote - Licensing caption; omit to hide.
|
|
31
|
+
*/
|
|
32
|
+
const TypefaceHero = ({
|
|
33
|
+
typeface = {},
|
|
34
|
+
downloadHref,
|
|
35
|
+
downloadLabel = 'Download font',
|
|
36
|
+
specimenLabel = 'View Specimen',
|
|
37
|
+
onSpecimenClick,
|
|
38
|
+
licenseNote,
|
|
39
|
+
}) => {
|
|
40
|
+
const { displayName, id, fontFamily, fontStyle, category, description, specimenLink } = typeface
|
|
41
|
+
const name = displayName || id || fontFamily
|
|
42
|
+
const isItalic = fontStyle === 'italic'
|
|
43
|
+
|
|
44
|
+
const handleSpecimenClick = (e) => {
|
|
45
|
+
if (onSpecimenClick) {
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
onSpecimenClick(e)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<section className="py-48 md:py-72 flex flex-col justify-center text-center items-center overflow-hidden">
|
|
53
|
+
<div className="flex flex-col items-center gap-2 max-w-[1400px]">
|
|
54
|
+
{category && (
|
|
55
|
+
<div className="pb-5 flex flex-col items-center gap-2">
|
|
56
|
+
<Pill variant="subtle">{category}</Pill>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
<div className="pb-16 flex flex-col items-center gap-0">
|
|
61
|
+
<h1
|
|
62
|
+
className={`text-[64px] leading-[100%] md:text-[128px] font-semibold ${
|
|
63
|
+
isItalic ? 'italic' : ''
|
|
64
|
+
} text-auto transition-colors duration-300`}
|
|
65
|
+
style={{ fontFamily }}
|
|
66
|
+
>
|
|
67
|
+
{name}
|
|
68
|
+
</h1>
|
|
69
|
+
|
|
70
|
+
{description && (
|
|
71
|
+
<p
|
|
72
|
+
className={`text-xl font-semibold ${
|
|
73
|
+
isItalic ? 'italic' : ''
|
|
74
|
+
} text-auto transition-colors duration-300`}
|
|
75
|
+
style={{ fontFamily }}
|
|
76
|
+
>
|
|
77
|
+
{description}
|
|
78
|
+
</p>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className="flex flex-col items-center gap-2">
|
|
83
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
84
|
+
<Button variant="primary" href={downloadHref}>
|
|
85
|
+
{downloadLabel}
|
|
86
|
+
</Button>
|
|
87
|
+
<Button
|
|
88
|
+
variant="outline"
|
|
89
|
+
href={specimenLink}
|
|
90
|
+
onClick={onSpecimenClick ? handleSpecimenClick : undefined}
|
|
91
|
+
>
|
|
92
|
+
{specimenLabel}
|
|
93
|
+
</Button>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{licenseNote && (
|
|
97
|
+
<p className="kol-mono-12 text-auto pt-4 transition-colors duration-300" style={{ opacity: 0.64 }}>
|
|
98
|
+
{licenseNote}
|
|
99
|
+
</p>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</section>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default TypefaceHero
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import SpecimenSectionHeader from '../../molecules/foundry/SpecimenSectionHeader.jsx'
|
|
3
|
+
|
|
4
|
+
/* taxonomy-ok: organism — nests SpecimenSectionHeader (relative import) plus a
|
|
5
|
+
* same-file StyleCard row; owns the axis/selection state. */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* StyleCard (same-file) — one style row: the label rendered live in its own
|
|
9
|
+
* weight/width/italic on the left, the numeric value on the right, with an
|
|
10
|
+
* active/hover flip. Inlined (the monorepo's `.style-card` CSS classes aren't
|
|
11
|
+
* in the DS theme) and rebuilt Tailwind-first.
|
|
12
|
+
*/
|
|
13
|
+
function StyleCard({ label, weight, width, italic, isActive, onHover, onClick, fontFamily }) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
onMouseEnter={onHover}
|
|
17
|
+
onClick={onClick}
|
|
18
|
+
className={`flex items-center justify-between gap-4 px-4 py-3 rounded cursor-pointer border transition-colors duration-150 ${
|
|
19
|
+
isActive ? 'bg-surface-inverse' : 'border-transparent hover:bg-fg-08'
|
|
20
|
+
}`}
|
|
21
|
+
style={{ borderColor: isActive ? 'transparent' : undefined }}
|
|
22
|
+
>
|
|
23
|
+
<span
|
|
24
|
+
className="text-2xl md:text-3xl leading-none truncate"
|
|
25
|
+
style={{
|
|
26
|
+
fontFamily,
|
|
27
|
+
fontStyle: italic ? 'italic' : 'normal',
|
|
28
|
+
fontWeight: weight || 400,
|
|
29
|
+
fontVariationSettings: width ? `'wdth' ${width}` : undefined,
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{label}
|
|
33
|
+
</span>
|
|
34
|
+
<span className="kol-mono-12 shrink-0 opacity-70">{width || weight}</span>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pickDefault = (list) => list.find((s) => s.isDefault) || list[3] || list[0]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* TypefaceStyleSection — the weight/width/italic style showcase: a sticky
|
|
43
|
+
* inverted preview panel on the left (renders `AaBbCc / 01234567 / {(!@#$?&)}`
|
|
44
|
+
* in the hovered/selected style) beside a grid of every available style on the
|
|
45
|
+
* right. Derives its behavior generically from `typeface.styles`: italic →
|
|
46
|
+
* Roman/Italic dropdown; weight+width → Weight/Width axis dropdown; single-axis
|
|
47
|
+
* / static → no dropdown.
|
|
48
|
+
*
|
|
49
|
+
* Default selection prefers a style flagged `isDefault`, falling back to index
|
|
50
|
+
* (Regular ≈ [3]) — the monorepo's magic indices assumed a fixed ordering; the
|
|
51
|
+
* flag survives reordering. All preview typography stays inline from the
|
|
52
|
+
* selected style (fontFamily / fontWeight / fontStyle / wdth variation) because
|
|
53
|
+
* arbitrary loaded fonts + variable axes can't be fixed KOL type classes.
|
|
54
|
+
*
|
|
55
|
+
* Text casing: badgeText, style labels and specimen strings render verbatim.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} props.typeface - { fontFamily, badgeText, styles:{ hasWeight, hasWidth, hasItalic, defaultStyle?, weights[], widths[] } }.
|
|
58
|
+
* @param {string[]} props.sampleLines - Preview lines (default AaBbCc / 01234567 / {(!@#$?&)}).
|
|
59
|
+
*/
|
|
60
|
+
const TypefaceStyleSection = ({
|
|
61
|
+
typeface = {},
|
|
62
|
+
sampleLines = ['AaBbCc', '01234567', '{(!@#$?&)}'],
|
|
63
|
+
}) => {
|
|
64
|
+
const { fontFamily, badgeText, styles: styleConfig = {} } = typeface
|
|
65
|
+
const {
|
|
66
|
+
hasWeight,
|
|
67
|
+
hasWidth,
|
|
68
|
+
hasItalic,
|
|
69
|
+
defaultStyle = 'weight',
|
|
70
|
+
weights = [],
|
|
71
|
+
widths = [],
|
|
72
|
+
} = styleConfig
|
|
73
|
+
|
|
74
|
+
const showDropdown = hasItalic || (hasWeight && hasWidth)
|
|
75
|
+
const styleOptions = hasItalic
|
|
76
|
+
? [
|
|
77
|
+
{ label: 'Roman', value: 'roman' },
|
|
78
|
+
{ label: 'Italic', value: 'italic' },
|
|
79
|
+
]
|
|
80
|
+
: hasWeight && hasWidth
|
|
81
|
+
? [
|
|
82
|
+
{ label: 'Weight', value: 'weight' },
|
|
83
|
+
{ label: 'Width', value: 'width' },
|
|
84
|
+
]
|
|
85
|
+
: null
|
|
86
|
+
|
|
87
|
+
const [selectedStyleVariant, setSelectedStyleVariant] = useState(
|
|
88
|
+
hasItalic ? 'italic' : defaultStyle,
|
|
89
|
+
)
|
|
90
|
+
const isItalic = selectedStyleVariant === 'italic'
|
|
91
|
+
const activeList = showDropdown && selectedStyleVariant === 'width' ? widths : weights
|
|
92
|
+
|
|
93
|
+
const [currentStyle, setCurrentStyle] = useState(() => pickDefault(activeList) || {})
|
|
94
|
+
|
|
95
|
+
const handleStyleVariantChange = (newVariant) => {
|
|
96
|
+
setSelectedStyleVariant(newVariant)
|
|
97
|
+
if (newVariant === 'width') setCurrentStyle(pickDefault(widths) || {})
|
|
98
|
+
else setCurrentStyle(pickDefault(weights) || {})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const previewStyle = {
|
|
102
|
+
fontFamily,
|
|
103
|
+
fontWeight: currentStyle.weight || 400,
|
|
104
|
+
fontStyle: isItalic ? 'italic' : 'normal',
|
|
105
|
+
...(currentStyle.width ? { fontVariationSettings: `'wdth' ${currentStyle.width}` } : {}),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<section className="w-full py-12 lg:py-16">
|
|
110
|
+
<div className="max-w-[1400px] mx-auto flex flex-col gap-8">
|
|
111
|
+
<SpecimenSectionHeader
|
|
112
|
+
selectedStyle={selectedStyleVariant}
|
|
113
|
+
onStyleChange={handleStyleVariantChange}
|
|
114
|
+
styleOptions={styleOptions || undefined}
|
|
115
|
+
showDropdown={showDropdown}
|
|
116
|
+
badgeText={badgeText}
|
|
117
|
+
icon="foundation"
|
|
118
|
+
size="sm"
|
|
119
|
+
/>
|
|
120
|
+
|
|
121
|
+
<div className="flex flex-row gap-4 md:gap-6 lg:gap-8 items-start w-full">
|
|
122
|
+
{/* Left: sticky inverted preview panel */}
|
|
123
|
+
<div className="w-1/2 aspect-[4/3] p-6 md:p-12 transition-colors duration-300 sticky top-24 bg-surface-inverse rounded">
|
|
124
|
+
<div
|
|
125
|
+
className="text-center transition-colors duration-300 w-full h-full flex flex-col justify-center items-center gap-2"
|
|
126
|
+
style={previewStyle}
|
|
127
|
+
>
|
|
128
|
+
{sampleLines.map((line, i) => (
|
|
129
|
+
<div key={i} className="text-3xl md:text-5xl lg:text-6xl leading-none">
|
|
130
|
+
{line}
|
|
131
|
+
</div>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Right: styles list */}
|
|
137
|
+
<div className="w-1/2 flex flex-col gap-3">
|
|
138
|
+
{activeList.map((style, index) => (
|
|
139
|
+
<StyleCard
|
|
140
|
+
key={`${style.label}-${index}`}
|
|
141
|
+
label={style.label}
|
|
142
|
+
weight={style.weight}
|
|
143
|
+
width={style.width}
|
|
144
|
+
italic={isItalic}
|
|
145
|
+
isActive={
|
|
146
|
+
currentStyle?.label === style.label &&
|
|
147
|
+
(style.weight
|
|
148
|
+
? currentStyle?.weight === style.weight
|
|
149
|
+
: currentStyle?.width === style.width)
|
|
150
|
+
}
|
|
151
|
+
onHover={() => setCurrentStyle(style)}
|
|
152
|
+
onClick={() => setCurrentStyle(style)}
|
|
153
|
+
fontFamily={fontFamily}
|
|
154
|
+
/>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default TypefaceStyleSection
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import Pill from '../../atoms/Pill.jsx'
|
|
3
|
+
import Slider from '../../molecules/Slider.jsx'
|
|
4
|
+
import SpecimenSectionHeader from '../../molecules/foundry/SpecimenSectionHeader.jsx'
|
|
5
|
+
import useAxisAnimation from '../../hooks/useAxisAnimation.js'
|
|
6
|
+
|
|
7
|
+
/* taxonomy-ok: organism — nests Pill, Slider, SpecimenSectionHeader (relative
|
|
8
|
+
* imports) plus a same-file PlayPauseButton; owns the axis-animation state. */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PlayPauseButton (same-file) — CSS-shape play/pause toggle. Two bars when
|
|
12
|
+
* playing, a right-pointing triangle when paused. Pure `currentColor` shapes so
|
|
13
|
+
* it needs no icon registry and inherits the surrounding text color.
|
|
14
|
+
*/
|
|
15
|
+
function PlayPauseButton({ isPlaying = false, onToggle, size = 28 }) {
|
|
16
|
+
const barH = Math.round(size * 0.43)
|
|
17
|
+
const barW = Math.max(2, Math.round(size * 0.07))
|
|
18
|
+
const tri = Math.round(size * 0.21)
|
|
19
|
+
const gap = Math.max(2, Math.round(size * 0.07))
|
|
20
|
+
return (
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={onToggle}
|
|
24
|
+
aria-label={isPlaying ? 'Pause' : 'Play'}
|
|
25
|
+
aria-pressed={isPlaying}
|
|
26
|
+
className="cursor-pointer flex items-center justify-center text-auto shrink-0"
|
|
27
|
+
style={{ width: size, height: size, gap }}
|
|
28
|
+
>
|
|
29
|
+
{isPlaying ? (
|
|
30
|
+
<>
|
|
31
|
+
<span className="bg-current" style={{ width: barW, height: barH }} />
|
|
32
|
+
<span className="bg-current" style={{ width: barW, height: barH }} />
|
|
33
|
+
</>
|
|
34
|
+
) : (
|
|
35
|
+
<span
|
|
36
|
+
className="w-0 h-0"
|
|
37
|
+
style={{
|
|
38
|
+
borderLeft: `${tri}px solid currentColor`,
|
|
39
|
+
borderTop: `${tri}px solid transparent`,
|
|
40
|
+
borderBottom: `${tri}px solid transparent`,
|
|
41
|
+
marginLeft: Math.round(size * 0.07),
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
</button>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* VariableFontSection — the animated variable-axis playground. A big specimen
|
|
51
|
+
* word whose weight auto-oscillates between `minWeight` and `maxWeight` via
|
|
52
|
+
* `useAxisAnimation` (ping-pong rAF, reduced-motion gated), pausing the instant
|
|
53
|
+
* the user scrubs the weight slider. A SpecimenSectionHeader sits above with a
|
|
54
|
+
* Roman/Italic dropdown. "Watch the weight breathe, then grab the slider."
|
|
55
|
+
*
|
|
56
|
+
* The weight is applied as CSS `font-weight` (→ the font's wght axis), so it
|
|
57
|
+
* animates against ANY font with multiple weights — a true variable font makes
|
|
58
|
+
* the morph continuous rather than stepped. See the page's FONT-ASSET CONTRACT.
|
|
59
|
+
*
|
|
60
|
+
* Reconciliations vs the monorepo source: the crude `Date.now()`/`delta>16`
|
|
61
|
+
* throttle is replaced by the hook's `performance.now()` accumulator; the
|
|
62
|
+
* previously unguarded loop now honors `prefers-reduced-motion` (starts paused,
|
|
63
|
+
* static value); brand copy defaults ('Málrómur Aa' / 'Variable' / TGMalromur)
|
|
64
|
+
* are dropped for neutral defaults.
|
|
65
|
+
*
|
|
66
|
+
* Text casing: badgeText, text and dropdown labels render verbatim.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} props
|
|
69
|
+
* @param {string} props.fontFamily - Specimen family (default system stack).
|
|
70
|
+
* @param {string} props.badgeText - Header title (default 'Variable axis').
|
|
71
|
+
* @param {string} props.text - The big specimen word (default 'Variable').
|
|
72
|
+
* @param {number} props.minWeight - Oscillation + slider lower bound (default 300).
|
|
73
|
+
* @param {number} props.maxWeight - Oscillation + slider upper bound (default 900).
|
|
74
|
+
* @param {boolean} props.showDropdown - Show the Roman/Italic dropdown (default true).
|
|
75
|
+
*/
|
|
76
|
+
const VariableFontSection = ({
|
|
77
|
+
fontFamily = 'system-ui, sans-serif',
|
|
78
|
+
badgeText = 'Variable axis',
|
|
79
|
+
text = 'Variable',
|
|
80
|
+
minWeight = 300,
|
|
81
|
+
maxWeight = 900,
|
|
82
|
+
showDropdown = true,
|
|
83
|
+
}) => {
|
|
84
|
+
const [isAnimating, setIsAnimating] = useState(true)
|
|
85
|
+
const [selectedStyle, setSelectedStyle] = useState(showDropdown ? 'italic' : 'roman')
|
|
86
|
+
|
|
87
|
+
const { value: weight, setValue: setWeight, reduced } = useAxisAnimation({
|
|
88
|
+
min: minWeight,
|
|
89
|
+
max: maxWeight,
|
|
90
|
+
running: isAnimating,
|
|
91
|
+
initial: 400,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const handleSliderChange = (value) => {
|
|
95
|
+
setIsAnimating(false)
|
|
96
|
+
setWeight(value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const playing = isAnimating && !reduced
|
|
100
|
+
const fontStyle = selectedStyle === 'italic' ? 'italic' : 'normal'
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<section className="w-full py-12 lg:py-16">
|
|
104
|
+
<div className="max-w-[1400px] mx-auto flex flex-col gap-8">
|
|
105
|
+
<SpecimenSectionHeader
|
|
106
|
+
selectedStyle={selectedStyle}
|
|
107
|
+
onStyleChange={setSelectedStyle}
|
|
108
|
+
showDropdown={showDropdown}
|
|
109
|
+
badgeText={badgeText}
|
|
110
|
+
icon="foundation"
|
|
111
|
+
size="sm"
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
{/* VariableFontDisplay (inlined) — giant text behind, controls in front */}
|
|
115
|
+
<div className="relative w-full rounded border border-fg-16 bg-surface-primary overflow-hidden p-6 md:p-10 h-[40vh] md:h-[60vh]">
|
|
116
|
+
<p
|
|
117
|
+
className="absolute inset-0 flex items-center justify-center text-[80px] md:text-[144px] transition-colors duration-300"
|
|
118
|
+
style={{
|
|
119
|
+
fontFamily,
|
|
120
|
+
fontWeight: Math.round(weight),
|
|
121
|
+
fontStyle,
|
|
122
|
+
color: 'var(--kol-surface-on-primary)',
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{text}
|
|
126
|
+
</p>
|
|
127
|
+
|
|
128
|
+
<div className="relative z-10 h-full flex flex-col justify-between">
|
|
129
|
+
<div className="flex justify-between items-start">
|
|
130
|
+
<div className="flex flex-col gap-1">
|
|
131
|
+
<span className="kol-mono-10 text-fg-32">AXES:</span>
|
|
132
|
+
<span className="kol-helper-12 text-auto">WEIGHT</span>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="flex gap-2">
|
|
135
|
+
<Pill variant="subtle">wght {Math.round(weight)}</Pill>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="flex items-center gap-4">
|
|
140
|
+
<PlayPauseButton isPlaying={playing} onToggle={() => setIsAnimating((a) => !a)} />
|
|
141
|
+
<Slider
|
|
142
|
+
label="Weight"
|
|
143
|
+
min={minWeight}
|
|
144
|
+
max={maxWeight}
|
|
145
|
+
value={Math.round(weight)}
|
|
146
|
+
onChange={handleSliderChange}
|
|
147
|
+
variant="minimal"
|
|
148
|
+
className="w-full"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</section>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export default VariableFontSection
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foundry glyph data — static character collections + category metadata for the
|
|
3
|
+
* type-specimen sections (GlyphMetricsGrid, FoundryCharacterSets). Ported from
|
|
4
|
+
* the monorepo `@kol/ui/data` glyphSets so the DS foundry layer carries its own
|
|
5
|
+
* default coverage instead of reaching into an app data package. Consumers can
|
|
6
|
+
* override any of it via the `uppercaseGlyphs` / `lowercaseGlyphs` / category
|
|
7
|
+
* props.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const glyphSets = {
|
|
11
|
+
uppercase: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
|
|
12
|
+
lowercase: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'],
|
|
13
|
+
numbers: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
14
|
+
punctuation: ['.', ',', '!', '?', ';', ':', '"', "'", '-', '—', '(', ')', '[', ']', '{', '}', '&', '@', '#', '$', '%'],
|
|
15
|
+
latin1: ['À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ø'],
|
|
16
|
+
latinExtended: ['Ā', 'Ă', 'Ą', 'Ć', 'Ĉ', 'Ċ', 'Č', 'Ď', 'Đ', 'Ē', 'Ĕ', 'Ė', 'Ę', 'Ě', 'Ĝ', 'Ğ', 'Ġ', 'Ģ', 'Ĥ', 'Ħ', 'Ĩ', 'Ī', 'Ĭ', 'Į'],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const glyphCategories = [
|
|
20
|
+
{ key: 'uppercase', title: 'Uppercase' },
|
|
21
|
+
{ key: 'lowercase', title: 'Lowercase' },
|
|
22
|
+
{ key: 'numbers', title: 'Numbers' },
|
|
23
|
+
{ key: 'punctuation', title: 'Punctuation & Symbols' },
|
|
24
|
+
{ key: 'latin1', title: 'Latin-1 Supported' },
|
|
25
|
+
{ key: 'latinExtended', title: 'Latin Extended' },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/** Default preview pangram — neutral English, authored in intended case. */
|
|
29
|
+
export const SPECIMEN_SAMPLE_TEXT =
|
|
30
|
+
'The quick brown fox jumps over the lazy dog while five wizards vex the gnomic judge.'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kol/component foundry sub-barrel — the type-specimen layer (the DS answer to
|
|
3
|
+
* the monorepo's `@kol/ui/foundry`). Exposed as the `./foundry` package subpath
|
|
4
|
+
* so the showcase specimen set can import these without touching the root
|
|
5
|
+
* src/index.js barrel. The root barrel export lines are documented in the p9
|
|
6
|
+
* wiring notes for a central merge.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// molecule (shared section header)
|
|
10
|
+
export { default as SpecimenSectionHeader } from '../../molecules/foundry/SpecimenSectionHeader.jsx'
|
|
11
|
+
|
|
12
|
+
// organisms (specimen sections)
|
|
13
|
+
export { default as TypefaceHero } from './TypefaceHero.jsx'
|
|
14
|
+
export { default as VariableFontSection } from './VariableFontSection.jsx'
|
|
15
|
+
export { default as GlyphMetricsGrid } from './GlyphMetricsGrid.jsx'
|
|
16
|
+
export { default as TypefaceStyleSection } from './TypefaceStyleSection.jsx'
|
|
17
|
+
export { default as FontPreviewSection } from './FontPreviewSection.jsx'
|
|
18
|
+
export { default as FoundryCharacterSets } from './FoundryCharacterSets.jsx'
|
|
19
|
+
|
|
20
|
+
// data
|
|
21
|
+
export { glyphSets, glyphCategories, SPECIMEN_SAMPLE_TEXT } from './glyphData.js'
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
// Match Dropdown SIZE_MAP for consistent height
|
|
4
|
-
const SIZE_MAP = {
|
|
5
|
-
sm: { fontSize: 11, paddingY: 12, paddingX: 24, radius: 20 },
|
|
6
|
-
md: { fontSize: 12, paddingY: 14, paddingX: 24, radius: 22 },
|
|
7
|
-
lg: { fontSize: 14, paddingY: 16, paddingX: 24, radius: 24 }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const QuantityStepper = ({
|
|
11
|
-
value = 1,
|
|
12
|
-
onChange,
|
|
13
|
-
min = 1,
|
|
14
|
-
max = 10,
|
|
15
|
-
size,
|
|
16
|
-
className = ''
|
|
17
|
-
}) => {
|
|
18
|
-
const [resolvedSize, setResolvedSize] = useState('md')
|
|
19
|
-
const [componentWidth, setComponentWidth] = useState('180px')
|
|
20
|
-
|
|
21
|
-
// Responsive size
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
const determineSize = () => {
|
|
24
|
-
if (size) {
|
|
25
|
-
setResolvedSize(size)
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (typeof window === 'undefined') {
|
|
30
|
-
setResolvedSize('md')
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (window.innerWidth >= 1024) {
|
|
35
|
-
setResolvedSize('lg')
|
|
36
|
-
} else if (window.innerWidth >= 768) {
|
|
37
|
-
setResolvedSize('md')
|
|
38
|
-
} else {
|
|
39
|
-
setResolvedSize('sm')
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
determineSize()
|
|
44
|
-
window.addEventListener('resize', determineSize)
|
|
45
|
-
return () => window.removeEventListener('resize', determineSize)
|
|
46
|
-
}, [size])
|
|
47
|
-
|
|
48
|
-
// Width management - match Dropdown
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const updateWidth = () => {
|
|
51
|
-
if (typeof window === 'undefined') return
|
|
52
|
-
|
|
53
|
-
if (window.innerWidth >= 1024) {
|
|
54
|
-
setComponentWidth('180px')
|
|
55
|
-
} else if (window.innerWidth >= 768) {
|
|
56
|
-
setComponentWidth('140px')
|
|
57
|
-
} else {
|
|
58
|
-
setComponentWidth('100px')
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
updateWidth()
|
|
62
|
-
window.addEventListener('resize', updateWidth)
|
|
63
|
-
return () => window.removeEventListener('resize', updateWidth)
|
|
64
|
-
}, [])
|
|
65
|
-
|
|
66
|
-
const metrics = SIZE_MAP[resolvedSize] || SIZE_MAP.md
|
|
67
|
-
|
|
68
|
-
const decrement = () => {
|
|
69
|
-
if (value > min) onChange?.(value - 1)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const increment = () => {
|
|
73
|
-
if (value < max) onChange?.(value + 1)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const buttonStyle = {
|
|
77
|
-
padding: `${metrics.paddingY}px 12px`,
|
|
78
|
-
display: 'flex',
|
|
79
|
-
alignItems: 'center',
|
|
80
|
-
justifyContent: 'center',
|
|
81
|
-
backgroundColor: 'transparent',
|
|
82
|
-
border: 'none',
|
|
83
|
-
color: 'var(--kol-surface-on-primary)',
|
|
84
|
-
cursor: 'pointer',
|
|
85
|
-
transition: 'opacity 0.15s',
|
|
86
|
-
fontFamily: 'var(--kol-font-family-mono)',
|
|
87
|
-
fontSize: `${metrics.fontSize}px`,
|
|
88
|
-
lineHeight: '120%'
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const disabledStyle = {
|
|
92
|
-
opacity: 0.3,
|
|
93
|
-
cursor: 'not-allowed'
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<div
|
|
98
|
-
className={`flex items-center justify-center ${className}`}
|
|
99
|
-
style={{
|
|
100
|
-
width: componentWidth,
|
|
101
|
-
minWidth: componentWidth,
|
|
102
|
-
border: '1px solid var(--kol-border-default)',
|
|
103
|
-
borderRadius: `${metrics.radius}px`,
|
|
104
|
-
backgroundColor: 'var(--kol-surface-primary)'
|
|
105
|
-
}}
|
|
106
|
-
>
|
|
107
|
-
<button
|
|
108
|
-
type="button"
|
|
109
|
-
onClick={decrement}
|
|
110
|
-
disabled={value <= min}
|
|
111
|
-
style={{
|
|
112
|
-
...buttonStyle,
|
|
113
|
-
...(value <= min ? disabledStyle : {})
|
|
114
|
-
}}
|
|
115
|
-
aria-label="Decrease quantity"
|
|
116
|
-
>
|
|
117
|
-
−
|
|
118
|
-
</button>
|
|
119
|
-
|
|
120
|
-
<span
|
|
121
|
-
style={{
|
|
122
|
-
minWidth: '24px',
|
|
123
|
-
textAlign: 'center',
|
|
124
|
-
color: 'var(--kol-surface-on-primary)',
|
|
125
|
-
fontFamily: 'var(--kol-font-family-mono)',
|
|
126
|
-
fontSize: `${metrics.fontSize}px`,
|
|
127
|
-
lineHeight: '120%'
|
|
128
|
-
}}
|
|
129
|
-
>
|
|
130
|
-
{value}
|
|
131
|
-
</span>
|
|
132
|
-
|
|
133
|
-
<button
|
|
134
|
-
type="button"
|
|
135
|
-
onClick={increment}
|
|
136
|
-
disabled={value >= max}
|
|
137
|
-
style={{
|
|
138
|
-
...buttonStyle,
|
|
139
|
-
...(value >= max ? disabledStyle : {})
|
|
140
|
-
}}
|
|
141
|
-
aria-label="Increase quantity"
|
|
142
|
-
>
|
|
143
|
-
+
|
|
144
|
-
</button>
|
|
145
|
-
</div>
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export default QuantityStepper
|