@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.
- package/README.md +8 -0
- package/package.json +2 -3
- package/src/atoms/RotaryDial.jsx +35 -21
- package/src/index.js +6 -18
- package/src/molecules/ButtonGroup.jsx +45 -0
- package/src/molecules/ColorInputRow.jsx +146 -126
- package/src/molecules/Slider.jsx +29 -14
- package/src/molecules/SplitToolButton.jsx +133 -0
- package/src/organisms/ContentFilters.jsx +2 -2
- package/src/organisms/GalleryCarousel.jsx +2 -1
- package/src/organisms/LoaderOverlay.jsx +11 -14
- package/src/organisms/MediaTileGallery.jsx +57 -0
- package/src/organisms/MediaViewer.jsx +89 -64
- package/src/atoms/PriceDisplay.jsx +0 -34
- package/src/atoms/TextPressure.jsx +0 -331
- package/src/atoms/TypeSample.jsx +0 -50
- package/src/atoms/TypeSpecCard.jsx +0 -42
- package/src/molecules/ArticleCard.jsx +0 -178
- package/src/molecules/WorkListItem.jsx +0 -83
- package/src/molecules/foundry/SpecimenSectionHeader.jsx +0 -89
- package/src/organisms/ArticleHeader.jsx +0 -95
- package/src/organisms/ColorLoader.jsx +0 -155
- package/src/organisms/DiagonalMarqueeRiver.jsx +0 -138
- package/src/organisms/ParallaxShelf.jsx +0 -141
- package/src/organisms/PortableTextRenderer.jsx +0 -115
- package/src/organisms/ProductDetailLayout.jsx +0 -189
- package/src/organisms/ScrollDriftGallery.jsx +0 -214
- package/src/organisms/StackHero.jsx +0 -83
- package/src/organisms/WorkCard.jsx +0 -120
- package/src/organisms/WorkViewToggle.jsx +0 -170
- package/src/organisms/foundry/FontPreviewSection.jsx +0 -187
- package/src/organisms/foundry/FoundryCharacterSets.jsx +0 -113
- package/src/organisms/foundry/GlyphMetricsGrid.jsx +0 -335
- package/src/organisms/foundry/TypefaceHero.jsx +0 -107
- package/src/organisms/foundry/TypefaceStyleSection.jsx +0 -163
- package/src/organisms/foundry/VariableFontSection.jsx +0 -158
- package/src/organisms/foundry/glyphData.js +0 -30
- 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
|