@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.
Files changed (77) hide show
  1. package/package.json +11 -3
  2. package/src/atoms/AnimatedTitle.jsx +108 -0
  3. package/src/atoms/AssetGrid.jsx +29 -0
  4. package/src/atoms/Button.jsx +17 -6
  5. package/src/atoms/CurveOverlay.jsx +180 -0
  6. package/src/atoms/DocsToc.jsx +48 -0
  7. package/src/atoms/EmptyState.jsx +22 -0
  8. package/src/atoms/Figure.jsx +27 -0
  9. package/src/atoms/HlsVideo.jsx +56 -0
  10. package/src/atoms/OverlayGlassPanel.jsx +40 -0
  11. package/src/atoms/PriceDisplay.jsx +34 -0
  12. package/src/atoms/ProsePreview.jsx +53 -0
  13. package/src/atoms/QuantityInput.jsx +76 -114
  14. package/src/atoms/RotaryDial.jsx +150 -0
  15. package/src/atoms/SearchInput.jsx +108 -0
  16. package/src/atoms/TextPressure.jsx +331 -0
  17. package/src/atoms/TiltCard.jsx +127 -0
  18. package/src/atoms/TypeSample.jsx +50 -0
  19. package/src/atoms/TypeSpecCard.jsx +42 -0
  20. package/src/hooks/cssVar.js +53 -0
  21. package/src/hooks/useAxisAnimation.js +91 -0
  22. package/src/hooks/usePrefersReducedMotion.js +21 -0
  23. package/src/hooks/useTilt.js +49 -0
  24. package/src/index.js +66 -2
  25. package/src/molecules/AlignmentGrid.jsx +53 -0
  26. package/src/molecules/ArticleCard.jsx +178 -0
  27. package/src/molecules/CardFeatureItem.jsx +130 -0
  28. package/src/molecules/ColorInputRow.jsx +179 -0
  29. package/src/molecules/ColorRamp.jsx +114 -0
  30. package/src/molecules/FramedMediaBand.jsx +56 -0
  31. package/src/molecules/ImageBlock.jsx +34 -0
  32. package/src/molecules/MenuPopover.jsx +4 -27
  33. package/src/molecules/SelectionOverlay.jsx +108 -0
  34. package/src/molecules/ShapeDropdown.jsx +92 -0
  35. package/src/molecules/ShellDrawer.jsx +169 -0
  36. package/src/molecules/ShellSearchOverlay.jsx +177 -0
  37. package/src/molecules/SpecList.jsx +30 -0
  38. package/src/molecules/SpectrumControls.jsx +504 -0
  39. package/src/molecules/SwatchControls.jsx +217 -0
  40. package/src/molecules/TabsRow.jsx +87 -0
  41. package/src/molecules/VideoBlock.jsx +86 -0
  42. package/src/molecules/WorkListItem.jsx +83 -0
  43. package/src/molecules/foundry/SpecimenSectionHeader.jsx +89 -0
  44. package/src/organisms/ArticleHeader.jsx +95 -0
  45. package/src/organisms/AsciiCursor.jsx +526 -0
  46. package/src/organisms/BentoCard.jsx +187 -0
  47. package/src/organisms/Canvas.jsx +299 -0
  48. package/src/organisms/ColorLoader.jsx +155 -0
  49. package/src/organisms/CtaGlobal.jsx +67 -0
  50. package/src/organisms/DiagonalMarqueeRiver.jsx +138 -0
  51. package/src/organisms/EditorShell.jsx +111 -0
  52. package/src/organisms/ErrorBoundary.jsx +70 -0
  53. package/src/organisms/FeatureSplit.jsx +82 -0
  54. package/src/organisms/FeaturedCarousel.jsx +258 -0
  55. package/src/organisms/FeaturesCardSection.jsx +90 -0
  56. package/src/organisms/FullBleedHero.jsx +111 -0
  57. package/src/organisms/GalleryCarousel.jsx +83 -0
  58. package/src/organisms/LoaderOverlay.jsx +30 -0
  59. package/src/organisms/MediaViewer.jsx +95 -0
  60. package/src/organisms/NewsletterBand.jsx +122 -0
  61. package/src/organisms/ParallaxShelf.jsx +141 -0
  62. package/src/organisms/PortableTextRenderer.jsx +115 -0
  63. package/src/organisms/ProductDetailLayout.jsx +189 -0
  64. package/src/organisms/ScrollDriftGallery.jsx +214 -0
  65. package/src/organisms/SpectrumGrid.jsx +90 -0
  66. package/src/organisms/StackHero.jsx +83 -0
  67. package/src/organisms/WorkCard.jsx +120 -0
  68. package/src/organisms/WorkViewToggle.jsx +170 -0
  69. package/src/organisms/foundry/FontPreviewSection.jsx +187 -0
  70. package/src/organisms/foundry/FoundryCharacterSets.jsx +113 -0
  71. package/src/organisms/foundry/GlyphMetricsGrid.jsx +335 -0
  72. package/src/organisms/foundry/TypefaceHero.jsx +107 -0
  73. package/src/organisms/foundry/TypefaceStyleSection.jsx +163 -0
  74. package/src/organisms/foundry/VariableFontSection.jsx +158 -0
  75. package/src/organisms/foundry/glyphData.js +30 -0
  76. package/src/organisms/foundry/index.js +21 -0
  77. 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