highsoft-ui 1.0.117 → 1.0.118
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/.cursorrules +845 -0
- package/CLAUDE.md +845 -0
- package/dist/Box-Bi-n_ZxQ.js +69 -0
- package/dist/Button-CJp__tle.js +63 -0
- package/dist/ButtonGroup-M9-A5Xva.js +53 -0
- package/dist/{Container-LAkF4ShX.js → Container-DI2smQRq.js} +1 -1
- package/dist/{CookiePolicyModal-B2mvebvV.js → CookiePolicyModal-BZkVbILl.js} +9 -43
- package/dist/CopyCode-BLXf0o74.js +40 -0
- package/dist/DiscordInvite-Mkr81Pib.js +92 -0
- package/dist/Footer-bUIou6Hl.js +83 -0
- package/dist/FooterBottom-_AezmdZB.js +133 -0
- package/dist/FooterHelp-CdeMRh1G.js +112 -0
- package/dist/FooterLinks-BUKcScC4.js +287 -0
- package/dist/{GPTInput-DRFGKJ2u.js → GPTInput-DKxJOGYt.js} +1 -1
- package/dist/{Header-BW8ilZxt.js → Header-6RIZc0v3.js} +90 -85
- package/dist/Heading-DuolDa27.js +19 -0
- package/dist/Icon-Dwo9ZDNX.js +31 -0
- package/dist/InputGrouped-CiA86XL7.js +221 -0
- package/dist/InputSolo-cLqKBh1c.js +122 -0
- package/dist/Label-BH3YxfNt.js +29 -0
- package/dist/Link-DGMiLJES.js +21 -0
- package/dist/{Logo-CZ5LYqub.js → Logo-h9bQL-cT.js} +1 -1
- package/dist/NewsletterSignup-eGx4jwiM.js +137 -0
- package/dist/{Notification-DHcQtd1k.js → Notification-CJ3iQv3Z.js} +6 -6
- package/dist/{PrefPanel-BTX6RAo4.js → PrefPanel-25erkPRB.js} +8 -8
- package/dist/{Search-DAPCZo13.js → Search-CDHkDXCm.js} +15 -15
- package/dist/Typography-nzBx3Yxn.js +49 -0
- package/dist/YoutubeModal-DNaiM6RL.js +89 -0
- package/dist/components/Box/index.js +2 -0
- package/dist/components/Button/index.js +1 -1
- package/dist/components/ButtonGroup/index.js +2 -0
- package/dist/components/Container/index.js +1 -1
- package/dist/components/CopyCode/index.js +2 -0
- package/dist/components/CopyCommand/index.js +2 -0
- package/dist/components/DiscordInvite/index.js +2 -0
- package/dist/components/Footer/CookiePolicyModal.js +1 -1
- package/dist/components/Footer/FooterBottom.js +2 -0
- package/dist/components/Footer/FooterHelp.js +2 -0
- package/dist/components/Footer/FooterLinks.js +2 -0
- package/dist/components/Footer/YoutubeModal.js +2 -0
- package/dist/components/Footer/index.js +1 -1
- package/dist/components/GPTInput/index.js +1 -1
- package/dist/components/Header/index.js +1 -1
- package/dist/components/Heading/index.js +2 -0
- package/dist/components/Icon/index.js +2 -0
- package/dist/components/InputGrouped/index.js +2 -0
- package/dist/components/InputSolo/index.js +2 -0
- package/dist/components/Label/index.js +2 -0
- package/dist/components/Link/index.js +2 -0
- package/dist/components/Logo/index.js +1 -1
- package/dist/components/NewsletterSignup/index.js +2 -0
- package/dist/components/Notification/index.js +1 -1
- package/dist/components/PrefPanel/index.js +1 -1
- package/dist/components/Search/index.js +1 -1
- package/dist/components/Typography/index.js +2 -0
- package/dist/{dist-CjrKJb_1.js → dist-GNbC83fc.js} +1 -1
- package/dist/{dist-0f0-S8_q.js → dist-fJmWSp8_.js} +1 -1
- package/dist/{esm-C1TDRARf.js → esm-T3Xn_ZHF.js} +208 -104
- package/dist/fa-ByhY-7YY.js +187 -0
- package/dist/highsoft-ui.css +1 -1
- package/dist/lib/components/Box/index.d.ts +28 -0
- package/dist/lib/components/Button/index.d.ts +9 -1
- package/dist/lib/components/ButtonGroup/index.d.ts +29 -0
- package/dist/lib/components/CopyCode/index.d.ts +12 -0
- package/dist/lib/components/CopyCommand/index.d.ts +6 -0
- package/dist/lib/components/DiscordInvite/index.d.ts +8 -0
- package/dist/lib/components/Footer/FooterBottom.d.ts +7 -0
- package/dist/lib/components/Footer/FooterHelp.d.ts +7 -0
- package/dist/lib/components/Footer/FooterLinks.d.ts +7 -0
- package/dist/lib/components/Footer/YoutubeModal.d.ts +1 -0
- package/dist/lib/components/Header/index.d.ts +14 -9
- package/dist/lib/components/Heading/index.d.ts +8 -0
- package/dist/lib/components/Icon/index.d.ts +26 -0
- package/dist/lib/components/InputGrouped/index.d.ts +60 -0
- package/dist/lib/components/InputSolo/index.d.ts +23 -0
- package/dist/lib/components/Label/index.d.ts +25 -0
- package/dist/lib/components/Link/index.d.ts +15 -0
- package/dist/lib/components/NewsletterSignup/index.d.ts +33 -0
- package/dist/lib/components/Typography/index.d.ts +13 -0
- package/dist/lib/main.d.ts +30 -3
- package/dist/lib/utils.d.ts +28 -0
- package/dist/main.js +23 -12
- package/dist/scss/mixins.scss +36 -0
- package/dist/scss/variables.scss +107 -0
- package/dist/{utils-C4QGLbxr.js → utils-BCI5dixg.js} +35 -15
- package/dist/utils.js +2 -2
- package/package.json +14 -6
- package/dist/Button-9RIB8ZR0.js +0 -47
- package/dist/Footer-5yMo5akY.js +0 -784
- package/dist/NewTag-C-3mRyvV.js +0 -26
- package/dist/components/NewTag/index.js +0 -2
- package/dist/lib/components/NewTag/index.d.ts +0 -8
- /package/dist/{clsx-ROFf4vs9.js → clsx-D7D_XZ_-.js} +0 -0
- /package/dist/{es2015-CzBBLWyD.js → es2015-C6iv4436.js} +0 -0
package/.cursorrules
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
# Highsoft Design System — AI Guide
|
|
2
|
+
|
|
3
|
+
This is the **highsoft-ui** component library and design token system for Highsoft products.
|
|
4
|
+
|
|
5
|
+
## Quick setup
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import 'highsoft-ui/css'
|
|
9
|
+
import 'highsoft-design-tokens/dist/css/tokens.css'
|
|
10
|
+
|
|
11
|
+
import { Button, Heading, Typography, Link, Container } from 'highsoft-ui'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Core rules
|
|
15
|
+
|
|
16
|
+
1. **NEVER use hardcoded colors.** Always use semantic CSS custom properties (`--HS-*`).
|
|
17
|
+
2. **NEVER use primitives in UI.** Primitives (`--primitives-*`) are only for token definitions — use semantic tokens so dark mode works automatically.
|
|
18
|
+
3. **Always use the component library** when a component exists. Don't recreate buttons, inputs, headings, etc.
|
|
19
|
+
4. **Use design token animations** for all transitions — never hardcode easing or durations.
|
|
20
|
+
|
|
21
|
+
## Semantic color tokens
|
|
22
|
+
|
|
23
|
+
Use these for all UI. They auto-switch between light and dark mode.
|
|
24
|
+
|
|
25
|
+
### Backgrounds
|
|
26
|
+
| Token | Use |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `--HS-surface-ui-background-0` | Card/panel background (white in light) |
|
|
29
|
+
| `--HS-surface-ui-background-50` | Subtle background (rows, hover states) |
|
|
30
|
+
| `--HS-surface-ui-background-100` | Slightly darker background (code blocks) |
|
|
31
|
+
| `--HS-common-background` | Page background |
|
|
32
|
+
|
|
33
|
+
### Borders
|
|
34
|
+
| Token | Use |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `--HS-surface-ui-border-100` | Default border (cards, dividers) |
|
|
37
|
+
| `--HS-surface-ui-border-200` | Stronger border (inputs, interactive) |
|
|
38
|
+
| `--HS-surface-ui-border-300` | Emphasis border |
|
|
39
|
+
|
|
40
|
+
### Text
|
|
41
|
+
| Token | Use |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `--HS-text-primary-default` | Headings, body text |
|
|
44
|
+
| `--HS-text-secondary-default` | Subtitles, descriptions |
|
|
45
|
+
| `--HS-text-tertiary-default` | Captions, hints, muted text |
|
|
46
|
+
| `--HS-text-brand-default-alt` | Links, accents, interactive |
|
|
47
|
+
|
|
48
|
+
### Brand surfaces
|
|
49
|
+
| Token | Use |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `--HS-surface-brand-background-50` | Subtle brand tint (badges, tags) |
|
|
52
|
+
| `--HS-surface-brand-background-100` | Brand background (selected state) |
|
|
53
|
+
| `--HS-surface-brand-border-200` | Brand border |
|
|
54
|
+
|
|
55
|
+
### Status colors
|
|
56
|
+
Each status (success, danger, attention) has: `background`, `background-boxed`, `border`, `border-boxed` for surfaces, and text tokens like `--HS-text-success-Primary`, `--HS-text-danger-default`, `--HS-text-attention-default`.
|
|
57
|
+
|
|
58
|
+
## Animation tokens
|
|
59
|
+
|
|
60
|
+
Never hardcode transitions. Use these CSS custom properties:
|
|
61
|
+
|
|
62
|
+
```css
|
|
63
|
+
/* Bounce — for interactive elements (buttons, toggles, inputs) */
|
|
64
|
+
--animations-bounce-instant: 0.1s cubic-bezier(0.6, 1, 0.2, 1.2);
|
|
65
|
+
--animations-bounce-fast: 0.2s cubic-bezier(0.6, 1, 0.2, 1.2);
|
|
66
|
+
--animations-bounce-default: 0.3s cubic-bezier(0.6, 1, 0.2, 1.2);
|
|
67
|
+
--animations-bounce-slow: 0.4s cubic-bezier(0.6, 1, 0.2, 1.2);
|
|
68
|
+
|
|
69
|
+
/* Fade — for color changes, opacity, border transitions */
|
|
70
|
+
--animations-fade-instant: 0.1s cubic-bezier(0.85, 0, 0.15, 1);
|
|
71
|
+
--animations-fade-fast: 0.2s cubic-bezier(0.85, 0, 0.15, 1);
|
|
72
|
+
--animations-fade-default: 0.3s cubic-bezier(0.85, 0, 0.15, 1);
|
|
73
|
+
|
|
74
|
+
/* Classic — general purpose */
|
|
75
|
+
--animations-classic-default: 0.3s cubic-bezier(0.65, 0, 0.35, 1);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
```css
|
|
80
|
+
transition: border-color var(--animations-fade-fast),
|
|
81
|
+
box-shadow var(--animations-bounce-default);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Shadow tokens
|
|
85
|
+
|
|
86
|
+
```css
|
|
87
|
+
--shadow-size-50 /* Subtle */
|
|
88
|
+
--shadow-size-100 /* Cards */
|
|
89
|
+
--shadow-size-200 /* Elevated (dropdowns, popovers) */
|
|
90
|
+
--shadow-size-300 /* Modals */
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Typography tokens
|
|
94
|
+
|
|
95
|
+
```css
|
|
96
|
+
--typography-font-family /* IBM Plex Sans Variable */
|
|
97
|
+
--typography-size-75 /* 0.75rem — caption */
|
|
98
|
+
--typography-size-100 /* 0.875rem — small body */
|
|
99
|
+
--typography-size-200 /* 1rem — body */
|
|
100
|
+
--typography-size-300 /* 1.125rem — large body */
|
|
101
|
+
--typography-weight-normal: 400;
|
|
102
|
+
--typography-weight-medium: 500;
|
|
103
|
+
--typography-weight-semi: 600;
|
|
104
|
+
--typography-lineHeight-ui: 100%;
|
|
105
|
+
--typography-lineHeight-text: 130%;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Components
|
|
109
|
+
|
|
110
|
+
### Box
|
|
111
|
+
The foundational layout component. Use it instead of raw `<div>` with inline styles.
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
// Default card (background 0, border, radius 8, padding 24px)
|
|
115
|
+
<Box>
|
|
116
|
+
<Heading level={4}>Title</Heading>
|
|
117
|
+
<Typography size={200}>Content</Typography>
|
|
118
|
+
</Box>
|
|
119
|
+
|
|
120
|
+
// Larger card
|
|
121
|
+
<Box padding={700}>
|
|
122
|
+
Padding 32px
|
|
123
|
+
</Box>
|
|
124
|
+
|
|
125
|
+
// Subtle row
|
|
126
|
+
<Box background={50} padding={400}>
|
|
127
|
+
Row content
|
|
128
|
+
</Box>
|
|
129
|
+
|
|
130
|
+
// Interactive card with hover effect
|
|
131
|
+
<Box padding={700} radius={16} shadow={100} hoverable>
|
|
132
|
+
Hover me
|
|
133
|
+
</Box>
|
|
134
|
+
|
|
135
|
+
// Flex column with gap
|
|
136
|
+
<Box gap={700}>
|
|
137
|
+
<div>Item 1</div>
|
|
138
|
+
<div>Item 2</div>
|
|
139
|
+
</Box>
|
|
140
|
+
|
|
141
|
+
// No border, darker background
|
|
142
|
+
<Box border={false} background={100} padding={500}>
|
|
143
|
+
Muted section
|
|
144
|
+
</Box>
|
|
145
|
+
|
|
146
|
+
// Polymorphic — render as <section>, <a>, etc.
|
|
147
|
+
<Box as="section" background={100} padding={900}>
|
|
148
|
+
Full-width section
|
|
149
|
+
</Box>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **`background`:** `0` (card/white, default), `50` (subtle), `100` (darker)
|
|
153
|
+
- **`border`:** `true` (default), `200`, `300` for stronger, `false` to disable
|
|
154
|
+
- **`radius`:** `0`, `4`, `8` (default), `16`, `32`
|
|
155
|
+
- **`padding`:** `0`, `100`–`1000` (maps to `--containers-inner-padding-*`). Default `600` (24px). Use `700` (32px) for larger cards
|
|
156
|
+
- **`gap`:** `0`, `200`, `400`, `600`, `700`, `800`, `900`, `1000` (maps to `--item-spacing-*`, sets `display: flex; flex-direction: column`)
|
|
157
|
+
- **`shadow`:** `50`, `100`, `200`, `300`
|
|
158
|
+
- **`hoverable`:** adds animated border-highlight + shadow-lift on hover using `--animations-fade-fast` and `--animations-bounce-default`
|
|
159
|
+
- **`as`:** polymorphic — render as any HTML element
|
|
160
|
+
|
|
161
|
+
### Button
|
|
162
|
+
```tsx
|
|
163
|
+
<Button variant="brand" size={300}>Primary action</Button>
|
|
164
|
+
<Button variant="neutral" size={200}>Secondary</Button>
|
|
165
|
+
<Button variant="soft" size={200}>Soft</Button>
|
|
166
|
+
<Button variant="transparent" size={200}>Ghost</Button>
|
|
167
|
+
<Button variant="danger" size={200}>Destructive</Button>
|
|
168
|
+
|
|
169
|
+
// With icons (from @untitled-ui/icons-react)
|
|
170
|
+
<Button variant="brand" iconLeft={<Plus />}>Create</Button>
|
|
171
|
+
<Button variant="neutral" iconRight={<ArrowRight />}>Continue</Button>
|
|
172
|
+
<Button variant="soft" iconLeft={<Copy06 />} iconRight={<Check />}>Both sides</Button>
|
|
173
|
+
```
|
|
174
|
+
- **Variants:** `brand`, `neutral`, `soft`, `transparent`, `plain`, `success`, `attention`, `danger`
|
|
175
|
+
- **Sizes:** `50`, `100`, `200`, `300`, `400`, `500`
|
|
176
|
+
- **`iconLeft`:** `ReactNode` — icon placed before the label
|
|
177
|
+
- **`iconRight`:** `ReactNode` — icon placed after the label
|
|
178
|
+
- **`as`:** polymorphic — auto-detects `<a>` when `href` is provided
|
|
179
|
+
|
|
180
|
+
### Heading
|
|
181
|
+
```tsx
|
|
182
|
+
<Heading level={1}>Page title</Heading>
|
|
183
|
+
<Heading level={2}>Section title</Heading>
|
|
184
|
+
<Heading level={4}>Card title</Heading>
|
|
185
|
+
```
|
|
186
|
+
- **Levels:** `1`–`6`
|
|
187
|
+
|
|
188
|
+
### Typography
|
|
189
|
+
```tsx
|
|
190
|
+
<Typography size={200}>Body text</Typography>
|
|
191
|
+
<Typography size={75} style={{ color: 'var(--HS-text-tertiary-default)' }}>Caption</Typography>
|
|
192
|
+
<Typography size={300} weight="semi">Large semi-bold</Typography>
|
|
193
|
+
```
|
|
194
|
+
- **Sizes:** `50`, `75`, `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900`, `1000`
|
|
195
|
+
- **Weights:** `"normal"`, `"medium"`, `"semi"`
|
|
196
|
+
|
|
197
|
+
### Link
|
|
198
|
+
```tsx
|
|
199
|
+
<Link href="/page">Default link</Link>
|
|
200
|
+
<Link href="/page" variant="fancy">Animated link</Link>
|
|
201
|
+
```
|
|
202
|
+
- **Variants:** `native` (subtle underline, default), `fancy` (animated slide-in underline)
|
|
203
|
+
- Polymorphic: `<Link as={RouterLink} to="/page">` for SPA navigation
|
|
204
|
+
|
|
205
|
+
### Icon
|
|
206
|
+
```tsx
|
|
207
|
+
<Icon name="chevron-right" size={200} />
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### InputSolo
|
|
211
|
+
```tsx
|
|
212
|
+
<InputSolo label="Email" placeholder="you@example.com" />
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### InputGrouped / SelectGrouped / FormGroup / FormRow
|
|
216
|
+
```tsx
|
|
217
|
+
<FormGroup>
|
|
218
|
+
<FormRow>
|
|
219
|
+
<InputGrouped label="First name" />
|
|
220
|
+
<InputGrouped label="Last name" />
|
|
221
|
+
</FormRow>
|
|
222
|
+
<SelectGrouped label="Country" options={[...]} />
|
|
223
|
+
</FormGroup>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Label (formerly NewTag)
|
|
227
|
+
```tsx
|
|
228
|
+
<Label variant="brand">New</Label> // Default with Stars02 icon
|
|
229
|
+
<Label variant="success" iconLeft={false}>Live</Label> // No icon
|
|
230
|
+
<Label variant="attention" iconLeft={<MyIcon />}>Warning</Label> // Custom icon
|
|
231
|
+
<Label variant="neutral" iconLeft={false}>v12.1</Label> // Plain text label
|
|
232
|
+
<Label variant="danger" border>Breaking</Label> // With border
|
|
233
|
+
```
|
|
234
|
+
- **Variants:** `success` (default), `brand`, `attention`, `danger`, `neutral`
|
|
235
|
+
- **`iconLeft`:** `true` (Stars02, default), `false` (none), or any `ReactNode` — aligned with Button's `iconLeft` pattern
|
|
236
|
+
- **`icon`:** deprecated alias for `iconLeft` — still works but use `iconLeft` in new code
|
|
237
|
+
- **`border`:** adds a visible border
|
|
238
|
+
- **`children`:** custom text (defaults to "New")
|
|
239
|
+
- `NewTag` is a deprecated alias — use `Label` in new code
|
|
240
|
+
|
|
241
|
+
### CopyCode
|
|
242
|
+
Pill-shaped inline code display with integrated copy button and success state.
|
|
243
|
+
```tsx
|
|
244
|
+
<CopyCode>npm i highsoft-ui highsoft-design-tokens</CopyCode>
|
|
245
|
+
<CopyCode label="Copy" copiedLabel="Copied" duration={2000}>yarn add highsoft-ui</CopyCode>
|
|
246
|
+
```
|
|
247
|
+
- Uses `Button` (soft → success variant) with `Copy06` / `Check` icons internally
|
|
248
|
+
- **`children`:** `string` — the code text to display and copy
|
|
249
|
+
- **`duration`:** ms for success state (default: 2000)
|
|
250
|
+
|
|
251
|
+
### Layout
|
|
252
|
+
```tsx
|
|
253
|
+
<Header />
|
|
254
|
+
<SubHeader />
|
|
255
|
+
<Container>{children}</Container>
|
|
256
|
+
<Footer />
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Notification
|
|
260
|
+
```tsx
|
|
261
|
+
<Notification variant="success" title="Saved" />
|
|
262
|
+
<Notification variant="danger" title="Error" message="Something went wrong" />
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Hooks
|
|
266
|
+
|
|
267
|
+
### useSuccessFlash
|
|
268
|
+
Triggers a temporary success state for any component. Use with Button variant switching for click confirmations (copy, save, submit, etc.).
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
import { useSuccessFlash } from 'highsoft-ui'
|
|
272
|
+
|
|
273
|
+
const [flash, flashing] = useSuccessFlash() // default 2s
|
|
274
|
+
const [flash, flashing] = useSuccessFlash(3000) // custom duration
|
|
275
|
+
|
|
276
|
+
<Button
|
|
277
|
+
variant={flashing ? 'success' : 'neutral'}
|
|
278
|
+
onClick={() => { doSomething(); flash(); }}
|
|
279
|
+
>
|
|
280
|
+
{flashing ? 'Done!' : 'Save'}
|
|
281
|
+
</Button>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### useClipboard
|
|
285
|
+
Copy text to clipboard with a built-in success state.
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
import { useClipboard } from 'highsoft-ui'
|
|
289
|
+
|
|
290
|
+
const [copy, copied] = useClipboard()
|
|
291
|
+
|
|
292
|
+
<Button
|
|
293
|
+
variant={copied ? 'success' : 'neutral'}
|
|
294
|
+
onClick={() => copy('some text')}
|
|
295
|
+
>
|
|
296
|
+
{copied ? 'Copied!' : 'Copy'}
|
|
297
|
+
</Button>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Combining both for copy buttons
|
|
301
|
+
```tsx
|
|
302
|
+
const [copy, copied] = useClipboard()
|
|
303
|
+
const [flash, flashing] = useSuccessFlash()
|
|
304
|
+
const active = copied || flashing
|
|
305
|
+
|
|
306
|
+
<button onClick={() => { copy(text); flash(); }}>
|
|
307
|
+
{active ? '✓ Copied!' : 'Copy'}
|
|
308
|
+
</button>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Link mixin (SCSS only, no React)
|
|
312
|
+
|
|
313
|
+
For projects using only design tokens without the React library:
|
|
314
|
+
|
|
315
|
+
```scss
|
|
316
|
+
@use 'highsoft-design-tokens/dist/scss/mixins/links' as *;
|
|
317
|
+
|
|
318
|
+
a { @include link; } // native — subtle underline
|
|
319
|
+
a.fancy { @include link(fancy); } // animated slide-in underline
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Building custom UI
|
|
323
|
+
|
|
324
|
+
Use the `<Box>` component for all layout containers. Prefer `<Box>` over raw `<div>` with inline styles.
|
|
325
|
+
|
|
326
|
+
### Card
|
|
327
|
+
```tsx
|
|
328
|
+
<Box background={0} border padding={700} radius={16}>
|
|
329
|
+
<Heading level={4}>Title</Heading>
|
|
330
|
+
<Typography size={200} style={{ color: 'var(--HS-text-secondary-default)' }}>
|
|
331
|
+
Description text
|
|
332
|
+
</Typography>
|
|
333
|
+
</Box>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Row / list item
|
|
337
|
+
```tsx
|
|
338
|
+
<Box background={50} border padding={400} radius={8} style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
|
339
|
+
{/* content */}
|
|
340
|
+
</Box>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Interactive card
|
|
344
|
+
```tsx
|
|
345
|
+
<Box background={0} border padding={700} radius={16} shadow={100} hoverable>
|
|
346
|
+
Hover lifts and highlights automatically
|
|
347
|
+
</Box>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Stacked layout with gap
|
|
351
|
+
```tsx
|
|
352
|
+
<Box background={0} border padding={700} radius={16} gap={700}>
|
|
353
|
+
<Heading level={4}>Section</Heading>
|
|
354
|
+
<Typography size={200}>Spaced with design token gap</Typography>
|
|
355
|
+
<Button variant="brand" size={300}>Action</Button>
|
|
356
|
+
</Box>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Badge / tag pattern
|
|
360
|
+
```tsx
|
|
361
|
+
<span style={{
|
|
362
|
+
display: 'inline-flex',
|
|
363
|
+
padding: '4px 10px',
|
|
364
|
+
borderRadius: 20,
|
|
365
|
+
fontSize: 11,
|
|
366
|
+
fontWeight: 600,
|
|
367
|
+
background: 'var(--HS-surface-brand-background-50)',
|
|
368
|
+
color: 'var(--HS-text-brand-default-alt)',
|
|
369
|
+
border: '1px solid var(--HS-surface-brand-border-200)',
|
|
370
|
+
}}>
|
|
371
|
+
Label
|
|
372
|
+
</span>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Dark mode
|
|
376
|
+
|
|
377
|
+
Dark mode is automatic when using semantic tokens. Toggle with:
|
|
378
|
+
```html
|
|
379
|
+
<html data-theme="dark">
|
|
380
|
+
```
|
|
381
|
+
No code changes needed — all `--HS-*` tokens remap automatically.
|
|
382
|
+
|
|
383
|
+
## Icon library
|
|
384
|
+
|
|
385
|
+
All icons come from **`@untitled-ui/icons-react`** (Untitled UI icon set). Never use emoji, ASCII characters (✓, ✗, →), or other icon libraries.
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
import { Plus, ArrowRight, Check, Copy06, Trash01, Settings04, Star01, Share07, Download01, Send01 } from '@untitled-ui/icons-react'
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Icons auto-size inside Button and Label based on their `size` prop:
|
|
392
|
+
| Button size | Icon size |
|
|
393
|
+
|---|---|
|
|
394
|
+
| `50` | 12×12 |
|
|
395
|
+
| `100` | 12×12 |
|
|
396
|
+
| `200` | 16×16 |
|
|
397
|
+
| `300` | 16×16 |
|
|
398
|
+
| `400` | 20×20 |
|
|
399
|
+
| `500` | 24×24 |
|
|
400
|
+
|
|
401
|
+
### Icon prop pattern (consistent across components)
|
|
402
|
+
|
|
403
|
+
All components use the same `iconLeft` / `iconRight` naming:
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
// Button — ReactNode | undefined
|
|
407
|
+
<Button iconLeft={<Plus />}>Create</Button>
|
|
408
|
+
<Button iconRight={<ArrowRight />}>Next</Button>
|
|
409
|
+
<Button iconLeft={<Copy06 />} iconRight={<Check />}>Both</Button>
|
|
410
|
+
|
|
411
|
+
// Label — boolean | ReactNode (true = default Stars02 icon)
|
|
412
|
+
<Label iconLeft={true}>New</Label> // Stars02 (default)
|
|
413
|
+
<Label iconLeft={false}>v12.1</Label> // no icon
|
|
414
|
+
<Label iconLeft={<Check />}>Passed</Label> // custom icon
|
|
415
|
+
|
|
416
|
+
// CopyCode — icons handled internally (Copy06 → Check on success)
|
|
417
|
+
<CopyCode>npm i highsoft-ui</CopyCode>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Component Do's and Don'ts
|
|
421
|
+
|
|
422
|
+
### Button
|
|
423
|
+
- **Do** use `brand` for the single primary action per section
|
|
424
|
+
- **Don't** use multiple `brand` buttons side by side — use `neutral` or `soft` for secondary
|
|
425
|
+
- **Do** keep grouped buttons at the same `size`
|
|
426
|
+
- **Don't** mix sizes like `size={300}` next to `size={100}`
|
|
427
|
+
- **Do** use `iconLeft`/`iconRight` on standard-width buttons
|
|
428
|
+
- **Don't** add icons to `fullWidth` buttons — the wide gap looks disconnected
|
|
429
|
+
- **Do** use `href` for navigation so it renders as `<a>` (a11y, cmd-click, right-click)
|
|
430
|
+
- **Don't** use `<button>` with `onClick` + router.push for navigation
|
|
431
|
+
- **Do** use `useSuccessFlash` with `iconLeft={<Check />}` for success states
|
|
432
|
+
- **Don't** use ASCII characters like ✓ or ✗ — always use icon components
|
|
433
|
+
- **Do** use `danger` only for destructive actions (delete, remove, revoke)
|
|
434
|
+
- **Don't** use `danger` for "Cancel" or "Go back" — use `neutral` or `transparent`
|
|
435
|
+
|
|
436
|
+
### Label
|
|
437
|
+
- **Do** use `iconLeft={false}` for plain text labels like version numbers
|
|
438
|
+
- **Don't** use Label for long text — it's designed for short status/category tags
|
|
439
|
+
|
|
440
|
+
## WordPress blocks team guide
|
|
441
|
+
|
|
442
|
+
This section is for the team building WordPress Gutenberg blocks that match the Highsoft design system.
|
|
443
|
+
|
|
444
|
+
Gutenberg blocks use React, so there are **two contexts** with different approaches:
|
|
445
|
+
|
|
446
|
+
| Context | Technology | Approach |
|
|
447
|
+
|---|---|---|
|
|
448
|
+
| **Block editor** (`edit.js`) | React | Import and use `highsoft-ui` components directly |
|
|
449
|
+
| **Frontend output** (`save.js` / PHP render) | Static HTML | Use design token CSS custom properties with matching class names |
|
|
450
|
+
|
|
451
|
+
### Block editor — use the React components directly
|
|
452
|
+
|
|
453
|
+
The block editor is React. Import components from `highsoft-ui` and map block attributes to props:
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
// edit.js — the editor preview uses real components
|
|
457
|
+
import 'highsoft-ui/css'
|
|
458
|
+
import 'highsoft-design-tokens/dist/css/tokens.css'
|
|
459
|
+
import { Button, Label, Heading, Typography, Box } from 'highsoft-ui'
|
|
460
|
+
|
|
461
|
+
export default function Edit({ attributes, setAttributes }) {
|
|
462
|
+
const { variant, size, label, href, iconName } = attributes
|
|
463
|
+
|
|
464
|
+
return (
|
|
465
|
+
<Box padding={700} radius={16}>
|
|
466
|
+
<Heading level={4}>{attributes.title}</Heading>
|
|
467
|
+
<Typography size={200} style={{ color: 'var(--HS-text-secondary-default)' }}>
|
|
468
|
+
{attributes.description}
|
|
469
|
+
</Typography>
|
|
470
|
+
<div style={{ display: 'flex', gap: 12, alignItems: 'center', marginTop: 16 }}>
|
|
471
|
+
<Button variant={variant} size={size} href={href}>
|
|
472
|
+
{label}
|
|
473
|
+
</Button>
|
|
474
|
+
<Label variant="brand" iconLeft={false}>Recommended</Label>
|
|
475
|
+
</div>
|
|
476
|
+
</Box>
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
```js
|
|
482
|
+
// block.json — map attributes to component props
|
|
483
|
+
{
|
|
484
|
+
"attributes": {
|
|
485
|
+
"variant": { "type": "string", "default": "brand" },
|
|
486
|
+
"size": { "type": "number", "default": 300 },
|
|
487
|
+
"label": { "type": "string", "default": "Get started" },
|
|
488
|
+
"href": { "type": "string" }
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**Block attribute names should match component prop names** (`variant`, `size`, `iconLeft`, `fullWidth`, etc.) so the mapping is 1:1.
|
|
494
|
+
|
|
495
|
+
### Frontend output — use PHP dynamic render (recommended)
|
|
496
|
+
|
|
497
|
+
Use **`render.php`** instead of `save.js` for frontend output. The block stores only attributes (JSON), and PHP renders the HTML on every page load. This gives total control — update the template once and every block instance updates automatically, with no deprecation migrations.
|
|
498
|
+
|
|
499
|
+
```json
|
|
500
|
+
// block.json — use render instead of save
|
|
501
|
+
{
|
|
502
|
+
"apiVersion": 3,
|
|
503
|
+
"name": "hs/button",
|
|
504
|
+
"title": "HS Button",
|
|
505
|
+
"render": "file:./render.php",
|
|
506
|
+
"attributes": {
|
|
507
|
+
"variant": { "type": "string", "default": "brand" },
|
|
508
|
+
"size": { "type": "number", "default": 300 },
|
|
509
|
+
"label": { "type": "string", "default": "Get started" },
|
|
510
|
+
"href": { "type": "string" },
|
|
511
|
+
"iconLeft": { "type": "string" }
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
```php
|
|
517
|
+
<?php
|
|
518
|
+
// render.php — generates the frontend HTML from attributes
|
|
519
|
+
$variant = $attributes['variant'] ?? 'brand';
|
|
520
|
+
$size = $attributes['size'] ?? 300;
|
|
521
|
+
$label = $attributes['label'] ?? 'Get started';
|
|
522
|
+
$href = $attributes['href'] ?? '#';
|
|
523
|
+
$icon = $attributes['iconLeft'] ?? '';
|
|
524
|
+
|
|
525
|
+
$classes = "hs-button hs-button--{$variant} hs-button--{$size}";
|
|
526
|
+
?>
|
|
527
|
+
<a class="<?php echo esc_attr($classes); ?>" href="<?php echo esc_url($href); ?>">
|
|
528
|
+
<?php if ($icon) : ?>
|
|
529
|
+
<?php echo hs_get_icon_svg($icon, $size); // helper that returns inline SVG ?>
|
|
530
|
+
<?php endif; ?>
|
|
531
|
+
<span><?php echo esc_html($label); ?></span>
|
|
532
|
+
</a>
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
```js
|
|
536
|
+
// save.js — returns null for dynamic blocks
|
|
537
|
+
export default function Save() {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Why `render.php` over `save.js`:**
|
|
543
|
+
- Change a border-radius or token → every block updates instantly
|
|
544
|
+
- No "This block contains unexpected content" errors when evolving the design
|
|
545
|
+
- HTML stays in sync with the component library — single source of truth
|
|
546
|
+
- Full server-side control (conditional logic, user roles, A/B testing)
|
|
547
|
+
|
|
548
|
+
### Frontend CSS — style the PHP output with tokens
|
|
549
|
+
|
|
550
|
+
```php
|
|
551
|
+
// Enqueue the token CSS in your block or theme
|
|
552
|
+
wp_enqueue_style('hs-tokens', 'path/to/highsoft-design-tokens/dist/css/tokens.css');
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
#### Button styles
|
|
556
|
+
```css
|
|
557
|
+
/* style.css — frontend styles using the same tokens as the React components */
|
|
558
|
+
.hs-button {
|
|
559
|
+
display: inline-flex;
|
|
560
|
+
align-items: center;
|
|
561
|
+
justify-content: center;
|
|
562
|
+
box-sizing: border-box;
|
|
563
|
+
border: 1px solid transparent;
|
|
564
|
+
cursor: pointer;
|
|
565
|
+
white-space: nowrap;
|
|
566
|
+
text-decoration: none;
|
|
567
|
+
font-family: var(--typography-font-family);
|
|
568
|
+
transition: transform 0.2s cubic-bezier(0.075, 0.82, 0.165, 1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* Size 200 (default) */
|
|
572
|
+
.hs-button--200 {
|
|
573
|
+
height: 32px;
|
|
574
|
+
padding: 0 7px;
|
|
575
|
+
border-radius: 8px;
|
|
576
|
+
font-size: var(--typography-size-100);
|
|
577
|
+
font-weight: var(--typography-weight-medium);
|
|
578
|
+
gap: 8px;
|
|
579
|
+
}
|
|
580
|
+
.hs-button--200 svg { width: 16px; height: 16px; }
|
|
581
|
+
|
|
582
|
+
/* Size 300 */
|
|
583
|
+
.hs-button--300 {
|
|
584
|
+
height: 36px;
|
|
585
|
+
padding: 0 9px;
|
|
586
|
+
border-radius: 8px;
|
|
587
|
+
font-size: var(--typography-size-200);
|
|
588
|
+
font-weight: var(--typography-weight-medium);
|
|
589
|
+
gap: 10px;
|
|
590
|
+
}
|
|
591
|
+
.hs-button--300 svg { width: 16px; height: 16px; }
|
|
592
|
+
|
|
593
|
+
/* Variants — use button token CSS custom properties */
|
|
594
|
+
.hs-button--brand {
|
|
595
|
+
background: var(--button-colors-brand-background-default);
|
|
596
|
+
color: var(--button-colors-brand-text-default);
|
|
597
|
+
border-color: var(--button-colors-brand-stroke-outer-default);
|
|
598
|
+
}
|
|
599
|
+
.hs-button--brand:hover {
|
|
600
|
+
background: var(--button-colors-brand-background-hover);
|
|
601
|
+
}
|
|
602
|
+
.hs-button--neutral {
|
|
603
|
+
background: var(--button-colors-neutral-background-default);
|
|
604
|
+
color: var(--button-colors-neutral-text-default);
|
|
605
|
+
border-color: var(--button-colors-neutral-stroke-outer-default);
|
|
606
|
+
}
|
|
607
|
+
.hs-button--soft {
|
|
608
|
+
background: var(--button-colors-soft-background-default);
|
|
609
|
+
color: var(--button-colors-soft-text-default);
|
|
610
|
+
border-color: var(--button-colors-soft-stroke-outer-default);
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
#### Label styles
|
|
615
|
+
```css
|
|
616
|
+
.hs-label {
|
|
617
|
+
display: inline-flex;
|
|
618
|
+
align-items: center;
|
|
619
|
+
gap: 5px;
|
|
620
|
+
height: 18px;
|
|
621
|
+
padding: 0 6px;
|
|
622
|
+
border-radius: 4px;
|
|
623
|
+
font-size: 0.75rem;
|
|
624
|
+
font-weight: var(--typography-weight-medium);
|
|
625
|
+
text-transform: uppercase;
|
|
626
|
+
}
|
|
627
|
+
.hs-label svg { width: 12px; height: 12px; }
|
|
628
|
+
.hs-label--with-icon { height: 24px; padding: 0 9px; border-radius: 6px; }
|
|
629
|
+
|
|
630
|
+
/* Variants — use tag token CSS custom properties */
|
|
631
|
+
.hs-label--brand { background: var(--tags-brand-background); color: var(--tags-brand-text); }
|
|
632
|
+
.hs-label--success { background: var(--tags-success-background); color: var(--tags-success-text); }
|
|
633
|
+
.hs-label--attention { background: var(--tags-attention-background); color: var(--tags-attention-text); }
|
|
634
|
+
.hs-label--danger { background: var(--tags-danger-background); color: var(--tags-danger-text); }
|
|
635
|
+
.hs-label--neutral { background: var(--tags-neutral-background); color: var(--tags-neutral-text); }
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
#### Link styles (SCSS mixin)
|
|
639
|
+
```scss
|
|
640
|
+
@use 'highsoft-design-tokens/dist/scss/mixins/links' as *;
|
|
641
|
+
|
|
642
|
+
.hs-link { @include link; } // native — subtle underline
|
|
643
|
+
.hs-link--fancy { @include link(fancy); } // animated slide-in underline
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Token naming conventions for blocks
|
|
647
|
+
|
|
648
|
+
Use the `hs-` prefix and mirror the React component API:
|
|
649
|
+
|
|
650
|
+
| React prop | Block attribute | CSS class | Example |
|
|
651
|
+
|---|---|---|---|
|
|
652
|
+
| `variant="brand"` | `variant: "brand"` | `hs-{component}--brand` | `hs-button--brand` |
|
|
653
|
+
| `size={300}` | `size: 300` | `hs-{component}--300` | `hs-button--300` |
|
|
654
|
+
| `iconLeft={<X />}` | `iconLeft: "x"` | `hs-{component}--with-icon` | `hs-label--with-icon` |
|
|
655
|
+
| `fullWidth` | `fullWidth: true` | `hs-{component}--full` | `hs-button--full` |
|
|
656
|
+
| `border` | `border: true` | `hs-{component}--border` | `hs-label--border` |
|
|
657
|
+
|
|
658
|
+
### Dark mode in WordPress
|
|
659
|
+
|
|
660
|
+
Add `data-theme="dark"` to the `<html>` element. All `--HS-*` tokens remap automatically — no additional CSS needed.
|
|
661
|
+
|
|
662
|
+
```php
|
|
663
|
+
<html data-theme="<?php echo get_theme_mod('hs_dark_mode') ? 'dark' : 'light'; ?>">
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Common block patterns (frontend CSS)
|
|
667
|
+
|
|
668
|
+
#### Pricing card
|
|
669
|
+
```css
|
|
670
|
+
.wp-block-hs-pricing {
|
|
671
|
+
background: var(--HS-surface-ui-background-0);
|
|
672
|
+
border: 1px solid var(--HS-surface-ui-border-100);
|
|
673
|
+
border-radius: 16px;
|
|
674
|
+
padding: 32px;
|
|
675
|
+
}
|
|
676
|
+
.wp-block-hs-pricing__title {
|
|
677
|
+
color: var(--HS-text-primary-default);
|
|
678
|
+
font-family: var(--typography-font-family);
|
|
679
|
+
font-size: var(--typography-size-300);
|
|
680
|
+
font-weight: var(--typography-weight-semi);
|
|
681
|
+
}
|
|
682
|
+
.wp-block-hs-pricing__description {
|
|
683
|
+
color: var(--HS-text-secondary-default);
|
|
684
|
+
font-size: var(--typography-size-200);
|
|
685
|
+
line-height: var(--typography-lineHeight-text);
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
#### Feature grid item with hover
|
|
690
|
+
```css
|
|
691
|
+
.wp-block-hs-feature {
|
|
692
|
+
background: var(--HS-surface-ui-background-0);
|
|
693
|
+
border: 1px solid var(--HS-surface-ui-border-100);
|
|
694
|
+
border-radius: 8px;
|
|
695
|
+
padding: 24px;
|
|
696
|
+
transition: border-color var(--animations-fade-fast),
|
|
697
|
+
box-shadow var(--animations-bounce-default);
|
|
698
|
+
}
|
|
699
|
+
.wp-block-hs-feature:hover {
|
|
700
|
+
border-color: var(--HS-surface-brand-border-200);
|
|
701
|
+
box-shadow: var(--shadow-size-100);
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Block development rules
|
|
706
|
+
|
|
707
|
+
1. **Use the React components in `edit.js`** — import from `highsoft-ui` directly, don't rebuild components
|
|
708
|
+
2. **Use `render.php` for frontend output** — dynamic blocks store only attributes, PHP renders the HTML. No `save.js` needed (return `null`)
|
|
709
|
+
3. **Name block attributes after component props** — `variant`, `size`, `iconLeft`, `fullWidth` — so the editor↔frontend mapping is obvious
|
|
710
|
+
4. **Always use token CSS custom properties** — never hardcode colors, shadows, radii, or animation values
|
|
711
|
+
5. **Icons must be inline SVGs** on the frontend — copy the SVG source from `@untitled-ui/icons-react`. In the editor, use the React icon components directly
|
|
712
|
+
6. **Use `box-sizing: border-box`** on all interactive elements to prevent height mismatches
|
|
713
|
+
7. **Test in both light and dark mode** — if you only use `--HS-*` tokens, dark mode works automatically
|
|
714
|
+
8. **Use the SCSS link mixin** for all anchor styling — don't write custom link hover/underline CSS
|
|
715
|
+
|
|
716
|
+
## Headless CMS / static site guide
|
|
717
|
+
|
|
718
|
+
If the site uses a **headless CMS** (git-based, API-based, or any visual editor) with a React or Astro frontend, the components can be used **directly** — no CSS recreation needed. This is the preferred approach when developers control the frontend and a non-technical editor only needs to change content.
|
|
719
|
+
|
|
720
|
+
### Architecture
|
|
721
|
+
|
|
722
|
+
```
|
|
723
|
+
┌─────────────────┐ ┌──────────────┐ ┌───────────────┐
|
|
724
|
+
│ CMS / Editor │────▶│ Content API │────▶│ React / Astro │
|
|
725
|
+
│ (visual editor │ │ or JSON/MD │ │ uses highsoft-ui│
|
|
726
|
+
│ for content) │ │ files │ │ components │
|
|
727
|
+
└─────────────────┘ └──────────────┘ └───────────────┘
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
The CMS manages **content** (text, images, links). The frontend uses the **real components** from `highsoft-ui`. Design tokens, variants, sizes — all handled by the component library. One implementation, not two.
|
|
731
|
+
|
|
732
|
+
### Content schema → component props
|
|
733
|
+
|
|
734
|
+
Define content fields that map directly to component props. Keep the CMS schema minimal — only expose what the editor needs to change:
|
|
735
|
+
|
|
736
|
+
```ts
|
|
737
|
+
// Example: a CTA block schema
|
|
738
|
+
interface CTABlock {
|
|
739
|
+
// Editor-controlled content
|
|
740
|
+
title: string
|
|
741
|
+
description: string
|
|
742
|
+
buttonLabel: string
|
|
743
|
+
buttonHref: string
|
|
744
|
+
// Design props (can be editor-controlled or hardcoded per template)
|
|
745
|
+
buttonVariant?: 'brand' | 'neutral' | 'soft' // default: 'brand'
|
|
746
|
+
buttonSize?: 200 | 300 | 400 // default: 300
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
```tsx
|
|
751
|
+
// The component uses highsoft-ui directly — no CSS duplication
|
|
752
|
+
import { Button, Heading, Typography, Box } from 'highsoft-ui'
|
|
753
|
+
|
|
754
|
+
export function CTABlock({ title, description, buttonLabel, buttonHref, buttonVariant = 'brand', buttonSize = 300 }: CTABlock) {
|
|
755
|
+
return (
|
|
756
|
+
<Box padding={700} radius={16}>
|
|
757
|
+
<Heading level={3}>{title}</Heading>
|
|
758
|
+
<Typography size={200} style={{ color: 'var(--HS-text-secondary-default)' }}>
|
|
759
|
+
{description}
|
|
760
|
+
</Typography>
|
|
761
|
+
<Button variant={buttonVariant} size={buttonSize} href={buttonHref}>
|
|
762
|
+
{buttonLabel}
|
|
763
|
+
</Button>
|
|
764
|
+
</Box>
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### Page templates with editable regions
|
|
770
|
+
|
|
771
|
+
For mostly-static pages, define templates where only the content is CMS-editable. The layout and design stay in code:
|
|
772
|
+
|
|
773
|
+
```tsx
|
|
774
|
+
// A page template — layout is code, content comes from CMS
|
|
775
|
+
import { Button, Heading, Typography, Box, Label, CopyCode } from 'highsoft-ui'
|
|
776
|
+
|
|
777
|
+
interface PricingPageContent {
|
|
778
|
+
hero: { title: string; subtitle: string }
|
|
779
|
+
plans: Array<{
|
|
780
|
+
name: string
|
|
781
|
+
price: string
|
|
782
|
+
description: string
|
|
783
|
+
features: string[]
|
|
784
|
+
cta: string
|
|
785
|
+
href: string
|
|
786
|
+
recommended?: boolean
|
|
787
|
+
}>
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export function PricingPage({ content }: { content: PricingPageContent }) {
|
|
791
|
+
return (
|
|
792
|
+
<>
|
|
793
|
+
<Heading level={1}>{content.hero.title}</Heading>
|
|
794
|
+
<Typography size={300} style={{ color: 'var(--HS-text-secondary-default)' }}>
|
|
795
|
+
{content.hero.subtitle}
|
|
796
|
+
</Typography>
|
|
797
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24 }}>
|
|
798
|
+
{content.plans.map((plan) => (
|
|
799
|
+
<Box key={plan.name} padding={700} radius={16} shadow={plan.recommended ? 100 : undefined} hoverable>
|
|
800
|
+
{plan.recommended && <Label variant="brand" iconLeft={false}>Recommended</Label>}
|
|
801
|
+
<Heading level={3}>{plan.name}</Heading>
|
|
802
|
+
<Heading level={2}>{plan.price}</Heading>
|
|
803
|
+
<Typography size={200}>{plan.description}</Typography>
|
|
804
|
+
<Button variant={plan.recommended ? 'brand' : 'neutral'} size={300} href={plan.href} fullWidth>
|
|
805
|
+
{plan.cta}
|
|
806
|
+
</Button>
|
|
807
|
+
</Box>
|
|
808
|
+
))}
|
|
809
|
+
</div>
|
|
810
|
+
</>
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
### What goes in the CMS vs. what stays in code
|
|
816
|
+
|
|
817
|
+
| In the CMS (editor can change) | In code (developer controls) |
|
|
818
|
+
|---|---|
|
|
819
|
+
| Page titles, headings, body text | Component selection and layout |
|
|
820
|
+
| Button labels and link URLs | Variant, size, design props |
|
|
821
|
+
| Image URLs and alt text | Spacing, grid structure |
|
|
822
|
+
| Feature lists, pricing data | Animation tokens, hover states |
|
|
823
|
+
| Meta descriptions, SEO fields | Dark mode, responsive breakpoints |
|
|
824
|
+
|
|
825
|
+
The editor should never need to pick a `variant` or `size` — those are design decisions that belong in the template. Only expose design props to the CMS when there's a genuine editorial need (e.g. "highlighted" toggle on a pricing card).
|
|
826
|
+
|
|
827
|
+
### Why this approach works
|
|
828
|
+
|
|
829
|
+
- **One implementation** — the real React components, not a CSS recreation
|
|
830
|
+
- **Design updates propagate instantly** — change a token or component prop, rebuild, done
|
|
831
|
+
- **Editor gets a simple interface** — text fields and toggles, not CSS classes
|
|
832
|
+
- **Type safety** — content schema is TypeScript, caught at build time
|
|
833
|
+
- **Static output** — pages pre-render to HTML at build time, no runtime cost
|
|
834
|
+
- **Dark mode works automatically** — components use semantic tokens
|
|
835
|
+
|
|
836
|
+
## File structure
|
|
837
|
+
|
|
838
|
+
```
|
|
839
|
+
lib/
|
|
840
|
+
components/ — React components (Button, Heading, Typography, Link, Label, Box, CopyCode, etc.)
|
|
841
|
+
scss/ — SCSS variables and semantic token definitions
|
|
842
|
+
utils.ts — Shared hooks (useSuccessFlash, useClipboard, useTheme)
|
|
843
|
+
main.ts — Package entry point and exports
|
|
844
|
+
stories/ — Storybook documentation pages
|
|
845
|
+
```
|