@kolkrabbi/kol-component 0.6.0 → 0.8.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 (38) hide show
  1. package/README.md +8 -0
  2. package/package.json +2 -3
  3. package/src/atoms/RotaryDial.jsx +35 -21
  4. package/src/index.js +6 -18
  5. package/src/molecules/ButtonGroup.jsx +45 -0
  6. package/src/molecules/ColorInputRow.jsx +146 -126
  7. package/src/molecules/Slider.jsx +29 -14
  8. package/src/molecules/SplitToolButton.jsx +133 -0
  9. package/src/organisms/ContentFilters.jsx +2 -2
  10. package/src/organisms/GalleryCarousel.jsx +2 -1
  11. package/src/organisms/LoaderOverlay.jsx +11 -14
  12. package/src/organisms/MediaTileGallery.jsx +57 -0
  13. package/src/organisms/MediaViewer.jsx +89 -64
  14. package/src/atoms/PriceDisplay.jsx +0 -34
  15. package/src/atoms/TextPressure.jsx +0 -331
  16. package/src/atoms/TypeSample.jsx +0 -50
  17. package/src/atoms/TypeSpecCard.jsx +0 -42
  18. package/src/molecules/ArticleCard.jsx +0 -178
  19. package/src/molecules/WorkListItem.jsx +0 -83
  20. package/src/molecules/foundry/SpecimenSectionHeader.jsx +0 -89
  21. package/src/organisms/ArticleHeader.jsx +0 -95
  22. package/src/organisms/ColorLoader.jsx +0 -155
  23. package/src/organisms/DiagonalMarqueeRiver.jsx +0 -138
  24. package/src/organisms/ParallaxShelf.jsx +0 -141
  25. package/src/organisms/PortableTextRenderer.jsx +0 -115
  26. package/src/organisms/ProductDetailLayout.jsx +0 -189
  27. package/src/organisms/ScrollDriftGallery.jsx +0 -214
  28. package/src/organisms/StackHero.jsx +0 -83
  29. package/src/organisms/WorkCard.jsx +0 -120
  30. package/src/organisms/WorkViewToggle.jsx +0 -170
  31. package/src/organisms/foundry/FontPreviewSection.jsx +0 -187
  32. package/src/organisms/foundry/FoundryCharacterSets.jsx +0 -113
  33. package/src/organisms/foundry/GlyphMetricsGrid.jsx +0 -335
  34. package/src/organisms/foundry/TypefaceHero.jsx +0 -107
  35. package/src/organisms/foundry/TypefaceStyleSection.jsx +0 -163
  36. package/src/organisms/foundry/VariableFontSection.jsx +0 -158
  37. package/src/organisms/foundry/glyphData.js +0 -30
  38. package/src/organisms/foundry/index.js +0 -21
@@ -1,120 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
- import TiltCard from '../atoms/TiltCard.jsx'
3
- import AssetPlaceholder from '../atoms/AssetPlaceholder.jsx'
4
-
5
- /* Ragged-skyline heights, chosen by `index % 3` — a shelf of these reads as an
6
- * uneven horizon. Overridable via the `heights` prop. */
7
- const HEIGHTS = ['h-[408px] md:h-[560px]', 'h-[372px] md:h-[520px]', 'h-[336px] md:h-[480px]']
8
- const EASE = 'cubic-bezier(0.16, 1, 0.3, 1)'
9
- const TYPE_LABELS = { client: 'Client', collection: 'Collection', typeface: 'Typeface', tool: 'Tool', system: 'System' }
10
-
11
- /**
12
- * WorkCard — the grid/shelf project tile for a `/work` listing: a fixed-size
13
- * media card (the DS **TiltCard** in its `grounded` variant) with a
14
- * hover-revealed bottom drawer carrying the project title + a meta line
15
- * (client-or-type · year). The whole card is an anchor to the project detail;
16
- * it preloads its own thumbnail and plays a staggered rise-and-untilt entrance
17
- * keyed off its `index` in the shelf.
18
- *
19
- * Composes TiltCard (owns the `<img>` + pointer tilt); this organism adds the
20
- * entrance choreography, the caption drawer, and the link. Router-agnostic:
21
- * renders a plain `<a href>` plus an `onNavigate(href, event)` seam — call
22
- * `preventDefault` inside it for SPA navigation. Flat project props (no Sanity):
23
- * the meta line is composed internally so callers pass raw fields.
24
- *
25
- * Text casing: no text-transform (KOL rule) — the source's presentational
26
- * `uppercase` is dropped; `title`, `client`, `year` render verbatim as authored.
27
- *
28
- * @param {string} title overlay heading (display type)
29
- * @param {string} thumbnail image URL (preloaded, passed to TiltCard `src`); AssetPlaceholder shows when absent
30
- * @param {string} href link target
31
- * @param {string} client first meta segment; falls back to the type label
32
- * @param {string} type 'client'|'collection'|'typeface'|'tool'|'system' — mapped via TYPE_LABELS when no client
33
- * @param {string|number} year second meta segment
34
- * @param {number} index drives stagger height + entrance timing (default 0)
35
- * @param {number} perspective CSS perspective in px on fine pointers (default 800)
36
- * @param {string[]} heights override the ragged-height ramp (chosen by index % heights.length)
37
- * @param {Function} onNavigate (href, event) => void — anchor click seam for SPA routing
38
- */
39
- export default function WorkCard({
40
- title,
41
- thumbnail,
42
- href,
43
- client,
44
- type,
45
- year,
46
- index = 0,
47
- perspective = 800,
48
- heights = HEIGHTS,
49
- onNavigate,
50
- }) {
51
- const [ready, setReady] = useState(false)
52
- // Evaluated once at mount (SSR-safe): drop perspective on coarse/small screens.
53
- const [isMobile] = useState(
54
- () => typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches,
55
- )
56
-
57
- useEffect(() => {
58
- if (!thumbnail) {
59
- setReady(true)
60
- return
61
- }
62
- let cancelled = false
63
- const img = new window.Image()
64
- const done = () => { if (!cancelled) setReady(true) }
65
- img.onload = done
66
- img.onerror = done
67
- img.src = thumbnail
68
- return () => { cancelled = true }
69
- }, [thumbnail])
70
-
71
- const height = heights[index % heights.length]
72
- const meta = [client || TYPE_LABELS[type] || type, year].filter(Boolean).join(' · ')
73
-
74
- const drawer = (
75
- <div className="absolute inset-x-0 bottom-0 z-10 p-4 md:p-6 bg-surface-inverse opacity-0 group-hover:opacity-100 transition-opacity duration-300">
76
- <p className="kol-sans-display-02 text-fg-inverse leading-tight">{title}</p>
77
- {meta && (
78
- <p className="kol-mono-12 text-fg-inverse opacity-60 tracking-widest mt-2">{meta}</p>
79
- )}
80
- </div>
81
- )
82
-
83
- return (
84
- <a
85
- href={href}
86
- onClick={onNavigate ? (e) => onNavigate(href, e) : undefined}
87
- className={`flex-none w-[280px] md:w-[400px] ${height} group`}
88
- style={isMobile ? undefined : { perspective }}
89
- >
90
- <div
91
- className="w-full h-full"
92
- style={{
93
- transformOrigin: 'bottom center',
94
- opacity: ready ? 1 : 0,
95
- transform: ready
96
- ? 'rotateX(0deg) translateY(0px)'
97
- : `rotateX(${20 + (index % 3) * 8}deg) translateY(${30 + (index % 4) * 10}px)`,
98
- transition: `opacity ${0.7 + (index % 3) * 0.15}s ${EASE} ${index * 0.07}s, transform ${0.7 + (index % 3) * 0.15}s ${EASE} ${index * 0.07}s`,
99
- }}
100
- >
101
- {thumbnail ? (
102
- <TiltCard
103
- src={thumbnail}
104
- alt={title}
105
- variant="grounded"
106
- perspective={perspective}
107
- className="w-full h-full rounded-[4px] border border-fg-04"
108
- >
109
- {drawer}
110
- </TiltCard>
111
- ) : (
112
- <div className="relative w-full h-full rounded-[4px] border border-fg-04 overflow-hidden">
113
- <AssetPlaceholder category="work" name={title} aspectRatio="auto" className="w-full h-full" />
114
- {drawer}
115
- </div>
116
- )}
117
- </div>
118
- </a>
119
- )
120
- }
@@ -1,170 +0,0 @@
1
- import { useEffect, useRef, useState } from 'react'
2
- import { Icon } from '@kolkrabbi/kol-icons'
3
-
4
- /* taxonomy-ok: composites a segmented toggle + an inline search into one
5
- * space-trading control (organism); nests kol-icons's Icon (infrastructure). */
6
-
7
- const CUBIC_EASE = 'cubic-bezier(0.16, 1, 0.3, 1)'
8
- /* The pill uses a springier overshoot ease, distinct from CUBIC_EASE. */
9
- const PILL_EASE = 'cubic-bezier(0.34, 1.2, 0.64, 1)'
10
-
11
- /**
12
- * WorkViewToggle — the `/work` header control: a pill-shaped segmented
13
- * **Shelf ⟷ List** view switch with a sliding-pill highlight, beside an
14
- * expandable inline search. Resting: the two-option toggle + a collapsed
15
- * search icon-button. Opening search collapses the toggle to zero width (fades
16
- * out) and expands the field to full width, revealing a round close button on
17
- * the left — the three siblings trade horizontal space via animated
18
- * width/margin/opacity.
19
- *
20
- * Its own component, not a variant of DS ViewToggle / SegmentedToggle: the
21
- * animated sliding pill, the expanding active-option icon, and the embedded
22
- * search choreography are materially different. All animated numerics are
23
- * inline `style` (width/left/margin/opacity + the two eases).
24
- *
25
- * Controlled seam replacing the app's WorkViewContext: `view`/`onView` +
26
- * `query`/`onQuery`. `searchOpen` is internal state (open on the search icon,
27
- * close on the × / Escape); closing clears the controlled query via
28
- * `onQuery('')`. No router, no context.
29
- *
30
- * Text casing: labels are authored data (`shelfLabel`/`listLabel`), no
31
- * text-transform.
32
- *
33
- * @param {string} view 'shelf' | 'list' — active view; positions the pill + swaps icon/color
34
- * @param {Function} onView (mode) => void — fires on Shelf/List click
35
- * @param {string} query controlled search value (default '')
36
- * @param {Function} onQuery (value) => void — fires on input change; called with '' on close
37
- * @param {string} shelfLabel first option label (default 'Shelf')
38
- * @param {string} listLabel second option label (default 'List')
39
- * @param {string} shelfIcon first option icon name (default 'library')
40
- * @param {string} listIcon second option icon name (default 'list')
41
- * @param {string} placeholder search input placeholder (default '')
42
- * @param {string} className extra classes on the root
43
- */
44
- export default function WorkViewToggle({
45
- view = 'shelf',
46
- onView,
47
- query = '',
48
- onQuery,
49
- shelfLabel = 'Shelf',
50
- listLabel = 'List',
51
- shelfIcon = 'library',
52
- listIcon = 'list',
53
- placeholder = '',
54
- className = '',
55
- }) {
56
- const [open, setOpen] = useState(false)
57
- const inputRef = useRef(null)
58
-
59
- useEffect(() => {
60
- if (open) inputRef.current?.focus()
61
- }, [open])
62
-
63
- const closeSearch = () => {
64
- setOpen(false)
65
- onQuery?.('')
66
- }
67
-
68
- const isShelf = view === 'shelf'
69
- const isList = view === 'list'
70
-
71
- const collapse = `width 600ms ${CUBIC_EASE}, margin 600ms ${CUBIC_EASE}, opacity 300ms ${CUBIC_EASE}`
72
- const inactiveColor = 'color-mix(in srgb, var(--kol-surface-on-primary) 80%, transparent)'
73
-
74
- const optionIcon = (name, on) => (
75
- <span
76
- className="inline-flex overflow-hidden flex-shrink-0"
77
- style={{ width: on ? 20 : 0, marginRight: on ? 8 : 0, opacity: on ? 1 : 0, transition: collapse }}
78
- >
79
- <Icon name={name} size={20} />
80
- </span>
81
- )
82
-
83
- return (
84
- <div className={`flex items-center ${className}`.trim()}>
85
- {/* Close button — appears when search is open */}
86
- <span
87
- className="inline-flex overflow-hidden flex-shrink-0"
88
- style={{ width: open ? 36 : 0, marginRight: open ? 12 : 0, opacity: open ? 1 : 0, transition: collapse }}
89
- >
90
- <button
91
- type="button"
92
- className="flex items-center justify-center w-9 h-9 rounded-full bg-fg-96 transition-colors hover:bg-fg-88"
93
- style={{ color: 'var(--kol-surface-primary)' }}
94
- onClick={closeSearch}
95
- aria-label="Close search"
96
- >
97
- <Icon name="cross" size={20} />
98
- </button>
99
- </span>
100
-
101
- {/* Toggle — collapses when search is open */}
102
- <div
103
- className="relative flex items-center rounded-full bg-fg-04 h-9 overflow-hidden"
104
- style={{ width: open ? 0 : 176, marginRight: open ? 0 : 12, opacity: open ? 0 : 1, transition: collapse }}
105
- >
106
- <div
107
- className="absolute top-0 h-9 rounded-full bg-fg-96"
108
- style={{ width: 96, left: isShelf ? 0 : 80, transition: `left 600ms ${PILL_EASE}` }}
109
- />
110
-
111
- <button
112
- type="button"
113
- className="relative z-10 flex items-center justify-center rounded-full h-9 kol-helper-14 transition-colors duration-300"
114
- style={{
115
- width: 96,
116
- letterSpacing: 0,
117
- color: isShelf ? 'var(--kol-surface-primary)' : inactiveColor,
118
- paddingRight: isShelf ? undefined : 8,
119
- }}
120
- onClick={() => onView?.('shelf')}
121
- aria-pressed={isShelf}
122
- >
123
- {optionIcon(shelfIcon, isShelf)}
124
- {shelfLabel}
125
- </button>
126
- <button
127
- type="button"
128
- className="relative z-10 flex items-center justify-center rounded-full h-9 -ml-4 kol-helper-14 transition-colors duration-300"
129
- style={{
130
- width: 96,
131
- letterSpacing: 0,
132
- color: isList ? 'var(--kol-surface-primary)' : inactiveColor,
133
- paddingLeft: isList ? undefined : 8,
134
- }}
135
- onClick={() => onView?.('list')}
136
- aria-pressed={isList}
137
- >
138
- {optionIcon(listIcon, isList)}
139
- {listLabel}
140
- </button>
141
- </div>
142
-
143
- {/* Search — icon button expands to a search field */}
144
- <div
145
- className="flex items-center bg-fg-04 rounded-full h-9"
146
- style={{ width: open ? 280 : 36, transition: `width 600ms ${CUBIC_EASE}` }}
147
- >
148
- <button
149
- type="button"
150
- className={`flex items-center justify-center w-9 h-9 rounded-full text-auto flex-shrink-0 border border-transparent ${open ? '' : 'transition-colors hover:border-fg-12'}`}
151
- onClick={() => !open && setOpen(true)}
152
- aria-label="Search projects"
153
- >
154
- <Icon name="search" size={16} className="text-fg-80" />
155
- </button>
156
- {open && (
157
- <input
158
- ref={inputRef}
159
- type="text"
160
- value={query}
161
- onChange={(e) => onQuery?.(e.target.value)}
162
- placeholder={placeholder}
163
- className="bg-transparent outline-none kol-helper-14 flex-1 text-fg-80 caret-current pr-4 min-w-0"
164
- onKeyDown={(e) => { if (e.key === 'Escape') closeSearch() }}
165
- />
166
- )}
167
- </div>
168
- </div>
169
- )
170
- }
@@ -1,187 +0,0 @@
1
- import { useEffect, useRef, useState } from 'react'
2
- import Dropdown from '../../molecules/Dropdown.jsx'
3
- import Slider from '../../molecules/Slider.jsx'
4
- import SpecimenSectionHeader from '../../molecules/foundry/SpecimenSectionHeader.jsx'
5
- import { SPECIMEN_SAMPLE_TEXT } from './glyphData.js'
6
-
7
- /* taxonomy-ok: organism — nests Dropdown, Slider, SpecimenSectionHeader
8
- * (relative imports) plus a same-file FontPreviewItem. */
9
-
10
- const WEIGHT_VALUES = {
11
- Thin: 100,
12
- Extralight: 200,
13
- Light: 300,
14
- Regular: 400,
15
- Medium: 500,
16
- Semibold: 600,
17
- Bold: 700,
18
- Extrabold: 800,
19
- Black: 900,
20
- }
21
-
22
- /**
23
- * FontPreviewItem (same-file) — one preview block: a weight dropdown + Size /
24
- * Leading / Spacing sliders over an editable, in-family sample. Weight is
25
- * seeded from the section header (re-syncs when the parent's selection changes);
26
- * size/leading/spacing are local. Leading maps 0–50 → 90–140% line-height.
27
- */
28
- function FontPreviewItem({
29
- fontFamily,
30
- fontStyle = 'normal',
31
- text = SPECIMEN_SAMPLE_TEXT,
32
- initialWeight = 'Regular',
33
- initialSize = 64,
34
- initialLeading = 10,
35
- lineClamp = null,
36
- availableWeights,
37
- }) {
38
- const [size, setSize] = useState(initialSize)
39
- const [leading, setLeading] = useState(initialLeading)
40
- const [spacing, setSpacing] = useState(0)
41
- const [selectedWeight, setSelectedWeight] = useState(initialWeight)
42
- const editableRef = useRef(null)
43
-
44
- useEffect(() => setSelectedWeight(initialWeight), [initialWeight])
45
- useEffect(() => {
46
- if (editableRef.current && editableRef.current.textContent !== text) {
47
- editableRef.current.textContent = text
48
- }
49
- }, [text])
50
-
51
- const weightOptions = availableWeights.map((label) => ({ label, value: label }))
52
- const numericWeight = WEIGHT_VALUES[selectedWeight] || 400
53
-
54
- return (
55
- <div className="self-stretch min-h-40 p-6 rounded border border-fg-16 flex flex-col justify-start items-start gap-6 bg-surface-primary">
56
- <div className="self-stretch flex justify-start items-start gap-8">
57
- <div className="flex-shrink-0">
58
- <Dropdown
59
- options={weightOptions}
60
- value={selectedWeight}
61
- onChange={setSelectedWeight}
62
- variant="minimal"
63
- />
64
- </div>
65
- <div className="flex-1 flex justify-start items-start gap-8">
66
- <Slider label="Size" min={12} max={200} value={size} onChange={setSize} variant="minimal" className="flex-1" />
67
- <Slider
68
- label="Leading"
69
- min={0}
70
- max={50}
71
- value={leading}
72
- onChange={setLeading}
73
- variant="minimal"
74
- className="hidden md:flex flex-1"
75
- formatValue={(val) => 90 + val}
76
- />
77
- <Slider label="Spacing" min={-50} max={50} value={spacing} onChange={setSpacing} variant="minimal" className="hidden md:flex flex-1" />
78
- </div>
79
- </div>
80
-
81
- <div className="self-stretch rounded flex justify-start items-start overflow-hidden">
82
- <div
83
- ref={editableRef}
84
- contentEditable
85
- suppressContentEditableWarning
86
- className="flex-1 bg-transparent outline-none border-none text-auto"
87
- style={{
88
- fontFamily,
89
- fontStyle,
90
- fontWeight: numericWeight,
91
- fontSize: `${size}px`,
92
- lineHeight: `${90 + leading}%`,
93
- letterSpacing: `${spacing}px`,
94
- wordWrap: 'break-word',
95
- overflowWrap: 'break-word',
96
- whiteSpace: 'normal',
97
- hyphens: 'none',
98
- maxWidth: '100%',
99
- ...(lineClamp && {
100
- display: '-webkit-box',
101
- WebkitLineClamp: lineClamp,
102
- WebkitBoxOrient: 'vertical',
103
- overflow: 'hidden',
104
- }),
105
- }}
106
- />
107
- </div>
108
- </div>
109
- )
110
- }
111
-
112
- const DEFAULT_WEIGHTS = [
113
- 'Thin', 'Extralight', 'Light', 'Regular', 'Medium', 'Semibold', 'Bold', 'Extrabold', 'Black',
114
- ]
115
- const SIZE_LADDER = [
116
- { size: 96, lines: 1 },
117
- { size: 64, lines: 3 },
118
- { size: 48, lines: 4 },
119
- { size: 24, lines: 5, leading: 50 },
120
- ]
121
-
122
- /**
123
- * FontPreviewSection — a stacked multi-size preview: the shared header (weight +
124
- * Roman/Italic dropdowns) above a ladder of preview blocks at 96 / 64 / 48 / 24
125
- * px, each pre-seeded with the section's selected weight + italic state. Read a
126
- * family from display down to body size at a chosen weight.
127
- *
128
- * Font-agnostic: the monorepo's TGMalromur/Málrómur defaults are dropped —
129
- * `fontFamily` + `badgeText` are prop-driven. The size ladder + weight ramp are
130
- * sensible defaults, overridable via `sizes` / `availableWeights`.
131
- *
132
- * Text casing: badgeText, weight labels and style labels render verbatim.
133
- *
134
- * @param {string} props.fontFamily - Family rendered in every preview item.
135
- * @param {string} props.badgeText - Header title.
136
- * @param {boolean} props.showDropdown - Render the style dropdown; seeds italic (default true).
137
- * @param {string[]} props.availableWeights - Weight dropdown options (default 9-stop ramp).
138
- * @param {string} props.initialWeight - Initially selected weight (default 'Regular').
139
- * @param {Array<{size,lines,leading?}>} props.sizes - Size ladder (default 96/64/48/24).
140
- */
141
- const FontPreviewSection = ({
142
- fontFamily = 'system-ui, sans-serif',
143
- badgeText = 'Preview',
144
- showDropdown = true,
145
- availableWeights = DEFAULT_WEIGHTS,
146
- initialWeight = 'Regular',
147
- sizes = SIZE_LADDER,
148
- }) => {
149
- const [selectedStyleVariant, setSelectedStyleVariant] = useState(showDropdown ? 'italic' : 'roman')
150
- const [selectedWeight, setSelectedWeight] = useState(initialWeight)
151
- const isItalic = selectedStyleVariant === 'italic'
152
- const weightOptions = availableWeights.map((label) => ({ label, value: label }))
153
-
154
- return (
155
- <section className="w-full py-12 lg:py-16">
156
- <div className="max-w-[1400px] mx-auto flex flex-col gap-8">
157
- <SpecimenSectionHeader
158
- selectedStyle={selectedStyleVariant}
159
- onStyleChange={setSelectedStyleVariant}
160
- showDropdown={showDropdown}
161
- badgeText={badgeText}
162
- icon="foundation"
163
- size="sm"
164
- selectedWeight={selectedWeight}
165
- onWeightChange={setSelectedWeight}
166
- showWeightDropdown={availableWeights.length > 0}
167
- weightOptions={weightOptions}
168
- />
169
-
170
- {sizes.map(({ size, lines, leading }) => (
171
- <FontPreviewItem
172
- key={size}
173
- fontFamily={fontFamily}
174
- fontStyle={isItalic ? 'italic' : 'normal'}
175
- initialWeight={selectedWeight}
176
- availableWeights={availableWeights}
177
- initialSize={size}
178
- initialLeading={leading}
179
- lineClamp={lines}
180
- />
181
- ))}
182
- </div>
183
- </section>
184
- )
185
- }
186
-
187
- export default FontPreviewSection
@@ -1,113 +0,0 @@
1
- import { useState } from 'react'
2
- import Button from '../../atoms/Button.jsx'
3
- import SpecimenSectionHeader from '../../molecules/foundry/SpecimenSectionHeader.jsx'
4
- import { glyphSets, glyphCategories } from './glyphData.js'
5
-
6
- /* taxonomy-ok: organism — nests Button + SpecimenSectionHeader (relative
7
- * imports) plus same-file GlyphCategory / GlyphItem. */
8
-
9
- /** GlyphItem (same-file) — one bordered glyph cell that flips to the inverse
10
- * surface on hover. Display-only (character sets aren't selectable). */
11
- function GlyphItem({ glyph, fontFamily, fontStyle }) {
12
- return (
13
- <div
14
- className="aspect-square border rounded-lg flex items-center justify-center text-3xl leading-none transition-colors duration-300 w-16 h-16 hover:bg-surface-inverse"
15
- style={{ borderColor: 'var(--kol-border-default)', fontFamily, fontStyle }}
16
- >
17
- {glyph}
18
- </div>
19
- )
20
- }
21
-
22
- /** GlyphCategory (same-file) — a labeled category title over a wrap grid of
23
- * GlyphItems, all rendered in the chosen family/style. */
24
- function GlyphCategory({ title, glyphs, fontFamily, fontStyle, className = '' }) {
25
- return (
26
- <div className={`w-full flex flex-col gap-4 ${className}`.trim()}>
27
- <h3 className="kol-mono-12">{title}</h3>
28
- <div className="flex flex-wrap gap-4 w-full">
29
- {glyphs?.map((glyph, i) => (
30
- <GlyphItem key={i} glyph={glyph} fontFamily={fontFamily} fontStyle={fontStyle} />
31
- ))}
32
- </div>
33
- </div>
34
- )
35
- }
36
-
37
- /**
38
- * FoundryCharacterSets — the character-set browser: the shared header ("Character
39
- * Set" + Roman/Italic dropdown) above a stack of glyph categories rendered in
40
- * the chosen family/style. Collapsed by default to the first two categories
41
- * under a fade with a "Show All Glyphs" reveal; clicking expands to every
42
- * category.
43
- *
44
- * Light-theme fix ([source-bug]): the monorepo faded with a hardcoded
45
- * `rgba(18,18,21,…)` — the app's literal dark background baked in, which broke
46
- * in light theme. Here the mask runs `transparent → var(--kol-surface-primary)`
47
- * so it matches whatever surface it sits on, in either theme. `fontFamily` is
48
- * prop-driven (no brand default).
49
- *
50
- * Text casing: label, category titles and the reveal button render verbatim.
51
- *
52
- * @param {string} props.fontFamily - Family every glyph renders in.
53
- * @param {boolean} props.showDropdown - Render the Roman/Italic dropdown (default true).
54
- * @param {number} props.collapsedCount - Categories shown when collapsed (default 2).
55
- */
56
- const FoundryCharacterSets = ({
57
- fontFamily = 'system-ui, sans-serif',
58
- showDropdown = true,
59
- collapsedCount = 2,
60
- }) => {
61
- const [selectedStyle, setSelectedStyle] = useState('italic')
62
- const [showAll, setShowAll] = useState(false)
63
-
64
- const visibleCategories = showAll ? glyphCategories : glyphCategories.slice(0, collapsedCount)
65
- const fontStyle = selectedStyle === 'italic' ? 'italic' : 'normal'
66
-
67
- return (
68
- <section className="w-full py-12 lg:py-16">
69
- <div className="max-w-[1400px] mx-auto flex flex-col gap-8">
70
- <SpecimenSectionHeader
71
- label="Character Set"
72
- size="sm"
73
- selectedStyle={selectedStyle}
74
- onStyleChange={setSelectedStyle}
75
- showDropdown={showDropdown}
76
- />
77
-
78
- <div className="w-full flex flex-col gap-12 relative">
79
- {visibleCategories.map((category, index) => (
80
- <GlyphCategory
81
- key={category.key}
82
- title={category.title}
83
- glyphs={glyphSets[category.key]}
84
- fontFamily={fontFamily}
85
- fontStyle={fontStyle}
86
- className={!showAll && index === collapsedCount - 1 ? 'relative' : ''}
87
- />
88
- ))}
89
-
90
- {!showAll && (
91
- <>
92
- {/* Theme-aware fade (fixes the hardcoded rgba(18,18,21) dark mask). */}
93
- <div
94
- className="absolute bottom-0 left-0 right-0 h-64 pointer-events-none"
95
- style={{
96
- background:
97
- 'linear-gradient(to bottom, transparent 13%, var(--kol-surface-primary) 86%)',
98
- }}
99
- />
100
- <div className="absolute bottom-0 left-0 right-0 flex justify-center">
101
- <Button onClick={() => setShowAll(true)} variant="outline">
102
- Show All Glyphs
103
- </Button>
104
- </div>
105
- </>
106
- )}
107
- </div>
108
- </div>
109
- </section>
110
- )
111
- }
112
-
113
- export default FoundryCharacterSets