@bpjs/design-system 0.0.0-stage → 0.0.1
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 +708 -2
- package/dist/bpjs.css +3275 -0
- package/dist/bpjs.d.ts +20 -0
- package/dist/bpjs.iife.js +1 -0
- package/dist/bpjs.js +157 -0
- package/dist/bpjs.min.css +2 -0
- package/dist/bpjs.nofont.css +3257 -0
- package/dist/bpjs.nofont.min.css +2 -0
- package/dist/fonts/OFL.txt +93 -0
- package/dist/fonts/plus-jakarta-sans-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/plus-jakarta-sans-latin-wght-normal.woff2 +0 -0
- package/dist/icons-map.d.ts +159 -0
- package/dist/icons-map.js +166 -0
- package/dist/icons.d.ts +155 -0
- package/dist/icons.js +157 -0
- package/dist/icons.svg +157 -0
- package/dist/tokens.css +432 -0
- package/dist/tokens.dark.json +359 -0
- package/dist/tokens.light.json +359 -0
- package/package.json +82 -4
package/README.md
CHANGED
|
@@ -1,3 +1,709 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bpjs/design-system
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Framework-agnostic design system for BPJS Kesehatan. It ships design tokens, CSS components and a small plain-JavaScript module. Everything is generated from the Figma file "BPJS Design System 1.1 (1110)", so the code follows the design file.
|
|
4
|
+
|
|
5
|
+
Works with React, Vue, Angular, Laravel Blade or plain HTML. Components are plain CSS classes; behavior is plain ES modules with no dependencies.
|
|
6
|
+
|
|
7
|
+
**License:** UNLICENSED for now. No use rights are granted until a license is added. See [License](#license).
|
|
8
|
+
|
|
9
|
+
- **Components built:** Button, Modal, Icons, Texts, Text Field, Form Item, Textarea, Checkbox, Radio, Radio Card, Switch, Pills, Avatar, Badge, Swiper, Stepper, Tooltip, Dropdown, Select, Segmented, Pagination, Table, Empty State and File Upload.
|
|
10
|
+
- **Not built yet:** Date Picker and Time Picker. Their Figma pages are still empty, so they wait for the designer.
|
|
11
|
+
- **Foundation:** tokens (colors, spacing, radius, typography, effects, layout grids) and the bundled Plus Jakarta Sans font are in place.
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
- [Install](#install)
|
|
16
|
+
- [Quick start](#quick-start)
|
|
17
|
+
- [Fonts](#fonts)
|
|
18
|
+
- [Theming](#theming)
|
|
19
|
+
- [Tokens](#tokens)
|
|
20
|
+
- [Components](#components)
|
|
21
|
+
- [Icons](#icons)
|
|
22
|
+
- [JavaScript API](#javascript-api)
|
|
23
|
+
- [Browser support](#browser-support)
|
|
24
|
+
- [Accessibility](#accessibility)
|
|
25
|
+
- [Known gaps](#known-gaps)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @bpjs/design-system
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
With a bundler (Vite, webpack, esbuild):
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import "@bpjs/design-system/css";
|
|
40
|
+
import { init } from "@bpjs/design-system";
|
|
41
|
+
|
|
42
|
+
init();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Plain HTML from a CDN:
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<link rel="stylesheet" href="https://unpkg.com/@bpjs/design-system/dist/bpjs.css" />
|
|
49
|
+
<script src="https://unpkg.com/@bpjs/design-system/dist/bpjs.iife.js" defer></script>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The IIFE build calls `init()` on load and exposes `window.BPJS`.
|
|
53
|
+
|
|
54
|
+
## Fonts
|
|
55
|
+
|
|
56
|
+
Plus Jakarta Sans is bundled with `bpjs.css`: latin and latin-ext subsets, variable weight 200 to 800, `font-display: swap`. The font is licensed under the SIL Open Font License 1.1, and the license ships as `dist/fonts/OFL.txt`.
|
|
57
|
+
|
|
58
|
+
If your app already loads Plus Jakarta Sans, use the build without the font to avoid downloading it twice:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import "@bpjs/design-system/css/nofont";
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<link rel="stylesheet" href="https://unpkg.com/@bpjs/design-system/dist/bpjs.nofont.css" />
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Components set the font through the text tokens, so their text uses Plus Jakarta Sans. Text outside a component (your own headings and paragraphs) uses your page's font. To match the design, set `font-family: var(--bpjs-font-family)` on your page root, or add the `bpjs-theme` class to it.
|
|
69
|
+
|
|
70
|
+
## Theming
|
|
71
|
+
|
|
72
|
+
Light is the default. Set `data-theme="dark"` on any element to switch its subtree:
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
<div data-theme="dark">...</div>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
To change a brand value for part of a page, override the semantic token on a parent. Components read semantic tokens, so the override reaches them:
|
|
79
|
+
|
|
80
|
+
```css
|
|
81
|
+
.my-app {
|
|
82
|
+
--bpjs-background-brand-primary: #0a7a4a;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Tokens
|
|
87
|
+
|
|
88
|
+
Every token is a CSS custom property named after its Figma path, prefixed with `--bpjs-`:
|
|
89
|
+
|
|
90
|
+
| Figma | CSS variable |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `Modes/Background/background-brand-primary` | `--bpjs-background-brand-primary` |
|
|
93
|
+
| `Spacing/Standard/spacing-md` | `--bpjs-spacing-md` |
|
|
94
|
+
| `Radius/radius-md` | `--bpjs-radius-md` |
|
|
95
|
+
| `Effects/Focus Normal` | `--bpjs-focus-normal` |
|
|
96
|
+
| Text style `Text/SM/Semibold` | `--bpjs-text-sm-semibold` (a `font` shorthand) |
|
|
97
|
+
| Layout grid `Dekstop` | `--bpjs-layout-grid-desktop-columns` |
|
|
98
|
+
|
|
99
|
+
Text styles also have utility classes, such as `.bpjs-type-text-sm-semibold`.
|
|
100
|
+
|
|
101
|
+
Two token files are available for use outside CSS:
|
|
102
|
+
|
|
103
|
+
- `@bpjs/design-system/tokens/light.json`
|
|
104
|
+
- `@bpjs/design-system/tokens/dark.json`
|
|
105
|
+
|
|
106
|
+
They hold fully resolved values, with one token per key.
|
|
107
|
+
|
|
108
|
+
Use `@bpjs/design-system/tokens.css` on its own if you only need the variables.
|
|
109
|
+
|
|
110
|
+
## Components
|
|
111
|
+
|
|
112
|
+
Optional `__label` spans inherit the control's color and font, so you can leave them out or add your own styling.
|
|
113
|
+
|
|
114
|
+
### Button
|
|
115
|
+
|
|
116
|
+
```html
|
|
117
|
+
<button type="button" class="bpjs-button bpjs-button--primary bpjs-button--md">Save</button>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- Variants: `--primary` (default), `--secondary`, `--tertiary`, `--ghost`, `--text`, `--danger`
|
|
121
|
+
- Sizes: `--sm`, `--md` (default), `--lg`, `--xl`
|
|
122
|
+
- Modifiers: `--gradient` (the Figma Gradient set), `--block`, `--icon-only` (needs an `aria-label`)
|
|
123
|
+
- Disabled: the `disabled` attribute on `<button>`, or `aria-disabled="true"` on a link
|
|
124
|
+
- Loading: `aria-busy="true"` shows a spinner and keeps the width
|
|
125
|
+
|
|
126
|
+
### Modal
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<button type="button" data-bpjs-modal-open="confirm-delete">Delete item</button>
|
|
130
|
+
|
|
131
|
+
<dialog class="bpjs-modal bpjs-modal--sm" id="confirm-delete" aria-labelledby="confirm-title"
|
|
132
|
+
aria-describedby="confirm-desc">
|
|
133
|
+
<header class="bpjs-modal__header">
|
|
134
|
+
<div class="bpjs-modal__titles">
|
|
135
|
+
<h2 class="bpjs-modal__title" id="confirm-title">Delete item?</h2>
|
|
136
|
+
<p class="bpjs-modal__description" id="confirm-desc">This cannot be undone.</p>
|
|
137
|
+
</div>
|
|
138
|
+
<button type="button" class="bpjs-modal__close" aria-label="Close" data-bpjs-modal-close>
|
|
139
|
+
<!-- close icon svg -->
|
|
140
|
+
</button>
|
|
141
|
+
</header>
|
|
142
|
+
<div class="bpjs-modal__body">Content goes here.</div>
|
|
143
|
+
<footer class="bpjs-modal__footer bpjs-modal__footer--end">
|
|
144
|
+
<button type="button" class="bpjs-button bpjs-button--secondary" data-bpjs-modal-close="cancel">Cancel</button>
|
|
145
|
+
<button type="button" class="bpjs-button bpjs-button--danger" data-bpjs-modal-close="confirm">Delete</button>
|
|
146
|
+
</footer>
|
|
147
|
+
</dialog>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- Sizes: `--sm` (480px, default), `--md` (600px), `--lg` (720px)
|
|
151
|
+
- The native `<dialog>` handles focus trapping, the Esc key and the backdrop.
|
|
152
|
+
- Click outside the box to close. Add `data-bpjs-backdrop-close="false"` to turn this off.
|
|
153
|
+
- Closing with `data-bpjs-modal-close="value"` sets `dialog.returnValue` to `value`.
|
|
154
|
+
- Focus returns to the button that opened the modal.
|
|
155
|
+
|
|
156
|
+
### Texts
|
|
157
|
+
|
|
158
|
+
Four blocks match Figma's Texts page. Use whatever heading level suits the page, since the classes work on any element:
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<h2 class="bpjs-heading bpjs-heading--lg bpjs-heading--bold">Judul halaman</h2>
|
|
162
|
+
<p class="bpjs-subtitle bpjs-subtitle--lg">Subjudul</p>
|
|
163
|
+
<p class="bpjs-body">Body Small, Regular, High (default)</p>
|
|
164
|
+
<p class="bpjs-body bpjs-body--lg bpjs-body--level-medium">Body Large, Medium</p>
|
|
165
|
+
<small class="bpjs-note bpjs-note--level-low">Catatan</small>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- Heading sizes: `--2xl`, `--xl`, `--lg`, `--md` (default), `--sm`, `--xs`
|
|
169
|
+
- Subtitle and body sizes: `--lg`, `--sm` (default). Note has one size.
|
|
170
|
+
- Weights: `--medium`, `--semibold`, `--bold`, `--extra-bold` (default is Regular)
|
|
171
|
+
- Levels (subtitle, body and note): `--level-medium`, `--level-low`, `--level-very-low` (default is High). Heading has no level in Figma.
|
|
172
|
+
|
|
173
|
+
### Text Field
|
|
174
|
+
|
|
175
|
+
A Text Field needs a visible label, so wrap it in a Form Item (below). Every field carries the `bpjs-control-field` class, which holds the shared focus, danger and disabled styles (`form-control.css`). The state is read from the input, so there are no state classes:
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<div class="bpjs-text-field bpjs-control-field">
|
|
179
|
+
<span class="bpjs-text-field__addon bpjs-text-field__addon--prefix">Rp</span>
|
|
180
|
+
<input class="bpjs-text-field__control" id="iuran" type="text" inputmode="numeric" />
|
|
181
|
+
<span class="bpjs-text-field__addon bpjs-text-field__addon--suffix">/bulan</span>
|
|
182
|
+
</div>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- Focus: the wrapper shows the focus ring when the input has focus
|
|
186
|
+
- Danger: `aria-invalid="true"` on the input
|
|
187
|
+
- Disabled: `disabled` on the input
|
|
188
|
+
- Icons: `<svg class="bpjs-icon bpjs-text-field__icon">` (20px, inherits text color)
|
|
189
|
+
|
|
190
|
+
### Textarea
|
|
191
|
+
|
|
192
|
+
Figma: 100px tall including the border, with the native resize handle. Wrap it in a Form Item.
|
|
193
|
+
|
|
194
|
+
```html
|
|
195
|
+
<div class="bpjs-textarea bpjs-control-field">
|
|
196
|
+
<textarea class="bpjs-textarea__control" id="catatan" rows="4"></textarea>
|
|
197
|
+
</div>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The same states as Text Field apply: `aria-invalid="true"` for danger and `disabled` for disabled.
|
|
201
|
+
|
|
202
|
+
### Form Item
|
|
203
|
+
|
|
204
|
+
Form Item groups a label, a control and an optional caption. Label, control and caption are 6px apart:
|
|
205
|
+
|
|
206
|
+
```html
|
|
207
|
+
<div class="bpjs-form-item">
|
|
208
|
+
<label class="bpjs-form-item__label" for="nik">NIK</label>
|
|
209
|
+
<div class="bpjs-text-field bpjs-control-field">
|
|
210
|
+
<input class="bpjs-text-field__control" id="nik" type="text" aria-describedby="nik-caption" />
|
|
211
|
+
</div>
|
|
212
|
+
<p class="bpjs-form-item__caption" id="nik-caption">
|
|
213
|
+
<svg class="bpjs-icon bpjs-icon--sm" aria-hidden="true"><use href="icons.svg#info"></use></svg>
|
|
214
|
+
<span>Masukkan 16 digit NIK sesuai KTP</span>
|
|
215
|
+
</p>
|
|
216
|
+
</div>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- Danger: set `aria-invalid="true"` on the control. The caption turns red and uses the alert icon.
|
|
220
|
+
- Disabled: set `disabled` on the control. The label and caption turn gray.
|
|
221
|
+
|
|
222
|
+
### Checkbox
|
|
223
|
+
|
|
224
|
+
Figma: 16px by default (Small) and 20px for Large (`bpjs-checkbox--lg`). Checked, indeterminate and disabled states come from the input.
|
|
225
|
+
|
|
226
|
+
```html
|
|
227
|
+
<label class="bpjs-checkbox">
|
|
228
|
+
<input class="bpjs-checkbox__control" type="checkbox" name="setuju" />
|
|
229
|
+
<span class="bpjs-checkbox__box" aria-hidden="true">
|
|
230
|
+
<svg class="bpjs-icon bpjs-checkbox__mark bpjs-checkbox__mark--check"><use href="icons.svg#check"></use></svg>
|
|
231
|
+
<svg class="bpjs-icon bpjs-checkbox__mark bpjs-checkbox__mark--minus"><use href="icons.svg#minus"></use></svg>
|
|
232
|
+
</span>
|
|
233
|
+
<span class="bpjs-checkbox__label">Saya setuju dengan syarat dan ketentuan</span>
|
|
234
|
+
</label>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The mixed ("indeterminate") state is set from JavaScript, since HTML has no attribute for it. It clears when the user toggles the box:
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
import { setIndeterminate } from "@bpjs/design-system";
|
|
241
|
+
|
|
242
|
+
setIndeterminate(selectAll, true); // some, but not all, items are selected
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Radio
|
|
246
|
+
|
|
247
|
+
Figma: 16px (Small) or 20px (`bpjs-radio--lg`). Options that share a `name` form a group. Arrow keys move between enabled options and skip disabled ones, which `init()` wires up.
|
|
248
|
+
|
|
249
|
+
```html
|
|
250
|
+
<fieldset>
|
|
251
|
+
<legend>Pilih paket</legend>
|
|
252
|
+
<label class="bpjs-radio">
|
|
253
|
+
<input class="bpjs-radio__control" type="radio" name="paket" value="a" checked />
|
|
254
|
+
<span class="bpjs-radio__dot" aria-hidden="true"></span>
|
|
255
|
+
<span class="bpjs-radio__label">Paket A</span>
|
|
256
|
+
</label>
|
|
257
|
+
<label class="bpjs-radio">
|
|
258
|
+
<input class="bpjs-radio__control" type="radio" name="paket" value="b" />
|
|
259
|
+
<span class="bpjs-radio__dot" aria-hidden="true"></span>
|
|
260
|
+
<span class="bpjs-radio__label">Paket B</span>
|
|
261
|
+
</label>
|
|
262
|
+
</fieldset>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Radio Card
|
|
266
|
+
|
|
267
|
+
A bordered option card around a radio: an icon, a value and a description, with the radio on the right. The card is 10px rounded with 16px padding. It is checked and unchecked the same way as Radio, and it groups with arrow keys in the same way.
|
|
268
|
+
|
|
269
|
+
```html
|
|
270
|
+
<label class="bpjs-radio-card">
|
|
271
|
+
<input class="bpjs-radio-card__control bpjs-radio__control" type="radio" name="paket" value="a" checked />
|
|
272
|
+
<span class="bpjs-radio__dot" aria-hidden="true"></span>
|
|
273
|
+
<span class="bpjs-radio-card__icon" aria-hidden="true">
|
|
274
|
+
<svg class="bpjs-icon"><use href="icons.svg#info"></use></svg>
|
|
275
|
+
</span>
|
|
276
|
+
<span class="bpjs-radio-card__content">
|
|
277
|
+
<span class="bpjs-radio-card__value">Paket A</span>
|
|
278
|
+
<span class="bpjs-radio-card__description">Rawat jalan dan rawat inap kelas III</span>
|
|
279
|
+
</span>
|
|
280
|
+
</label>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Switch
|
|
284
|
+
|
|
285
|
+
Figma: 36×20 (Small) or 44×24 (`bpjs-switch-field--lg`). A switch needs an accessible name, so it always has a label. If the design shows no text, hide the label visually with `bpjs-sr-only`; it is still read by screen readers.
|
|
286
|
+
|
|
287
|
+
```html
|
|
288
|
+
<label class="bpjs-switch-field">
|
|
289
|
+
<input class="bpjs-switch" type="checkbox" role="switch" name="notif" />
|
|
290
|
+
<span class="bpjs-switch__track" aria-hidden="true"><span class="bpjs-switch__thumb"></span></span>
|
|
291
|
+
<span class="bpjs-switch__label">Notifikasi</span>
|
|
292
|
+
</label>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Enter toggles a switch as well as Space.
|
|
296
|
+
|
|
297
|
+
### Pills
|
|
298
|
+
|
|
299
|
+
A compact toggle chip, 32px tall. It can have a leading icon and a count badge.
|
|
300
|
+
|
|
301
|
+
```html
|
|
302
|
+
<button type="button" class="bpjs-pill bpjs-pill--active" aria-pressed="true">
|
|
303
|
+
<svg class="bpjs-icon bpjs-pill__icon" aria-hidden="true"><use href="icons.svg#check"></use></svg>
|
|
304
|
+
<span class="bpjs-pill__label">Label</span>
|
|
305
|
+
<span class="bpjs-pill__badge"><span class="bpjs-pill__dot" aria-hidden="true"></span>2</span>
|
|
306
|
+
</button>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
For single choice, use a Radio Pill. Its radio is part of the pill, and arrow keys move between options that share a `name`:
|
|
310
|
+
|
|
311
|
+
```html
|
|
312
|
+
<label class="bpjs-pill bpjs-pill--radio">
|
|
313
|
+
<input class="bpjs-radio__control" type="radio" name="filter" value="a" checked />
|
|
314
|
+
<span class="bpjs-radio__dot" aria-hidden="true"></span>
|
|
315
|
+
<span class="bpjs-pill__label">Label</span>
|
|
316
|
+
</label>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Active and checked pills use the brand border and text. Disabled pills use `disabled` on the button or input.
|
|
320
|
+
|
|
321
|
+
The leading icon is not final. See [Known gaps](#known-gaps).
|
|
322
|
+
|
|
323
|
+
### Avatar
|
|
324
|
+
|
|
325
|
+
Figma has six sizes, from 24px to 64px. An avatar shows a photo, an icon or initials, and can carry an online dot.
|
|
326
|
+
|
|
327
|
+
```html
|
|
328
|
+
<span class="bpjs-avatar bpjs-avatar--md">
|
|
329
|
+
<img class="bpjs-avatar__image" src="foto.jpg" alt="Budi Santoso" />
|
|
330
|
+
</span>
|
|
331
|
+
<span class="bpjs-avatar bpjs-avatar--md" role="img" aria-label="Ani Wijaya">
|
|
332
|
+
<span class="bpjs-avatar__text" aria-hidden="true">AW</span>
|
|
333
|
+
<span class="bpjs-avatar__status" aria-hidden="true"></span>
|
|
334
|
+
</span>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
- Sizes: `--xs` (24px), `--sm` (32px), `--md` (40px, default), `--lg` (48px), `--xl` (56px), `--2xl` (64px)
|
|
338
|
+
- Content: a photo (`bpjs-avatar__image`), an icon (`<svg class="bpjs-icon bpjs-avatar__icon">`, 60% of the size), or initials (`bpjs-avatar__text`)
|
|
339
|
+
- Status: add `bpjs-avatar__status` for the green Active dot. Figma's Status set has four types, and each one is a modifier:
|
|
340
|
+
- `bpjs-avatar__status` (no modifier): Active, green
|
|
341
|
+
- `bpjs-avatar__status--inactive`: gray dot
|
|
342
|
+
- `bpjs-avatar__status--badge`: red circle with a count inside, e.g. `<span class="bpjs-avatar__status bpjs-avatar__status--badge">8</span>`
|
|
343
|
+
- `bpjs-avatar__status--icon`: white circle with an icon inside, e.g. `<span class="bpjs-avatar__status bpjs-avatar__status--icon"><svg class="bpjs-icon">…</svg></span>`
|
|
344
|
+
- The status scales with the avatar size. Hide it from assistive tech with `aria-hidden="true"` unless the status is announced elsewhere.
|
|
345
|
+
- A photo needs `alt`. An icon or initials avatar needs `role="img"` and an `aria-label`.
|
|
346
|
+
|
|
347
|
+
### Badge
|
|
348
|
+
|
|
349
|
+
Figma has five types, two weights and three sizes.
|
|
350
|
+
|
|
351
|
+
```html
|
|
352
|
+
<span class="bpjs-badge bpjs-badge--success bpjs-badge--hard">
|
|
353
|
+
<span class="bpjs-badge__dot" aria-hidden="true"></span>
|
|
354
|
+
<span class="bpjs-badge__label">Aktif</span>
|
|
355
|
+
</span>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- Types: `--neutral` (default), `--info`, `--success`, `--warning`, `--danger`
|
|
359
|
+
- Weights: Soft is the default. Add `--hard` for a tinted fill with a colored border. A hard badge also colors its label by type.
|
|
360
|
+
- Sizes: 22px by default, `--md` (24px) and `--lg` (28px)
|
|
361
|
+
- Icons: `<svg class="bpjs-icon bpjs-badge__icon">` before or after the label
|
|
362
|
+
- Icon only: add `--icon-only`, put one icon inside, and set `role="img"` with an `aria-label` on the badge.
|
|
363
|
+
|
|
364
|
+
### Swiper
|
|
365
|
+
|
|
366
|
+
Figma's Swiper is the indicator only: dots or a progress bar that show the position in a carousel. The carousel itself is not part of this library.
|
|
367
|
+
|
|
368
|
+
```html
|
|
369
|
+
<div class="bpjs-swiper-dots" aria-hidden="true">
|
|
370
|
+
<span class="bpjs-swiper-dot bpjs-swiper-dot--active"></span>
|
|
371
|
+
<span class="bpjs-swiper-dot"></span>
|
|
372
|
+
<span class="bpjs-swiper-dot"></span>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div class="bpjs-swiper-progress" style="--bpjs-swiper-progress: 33%" role="progressbar"
|
|
376
|
+
aria-valuenow="1" aria-valuemin="1" aria-valuemax="3" aria-label="Langkah 1 dari 3">
|
|
377
|
+
<span class="bpjs-swiper-progress__bar"></span>
|
|
378
|
+
</div>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
- Dots are 8px, 4px apart. The active dot uses the brand color.
|
|
382
|
+
- The progress bar is 72 by 6px. Set `--bpjs-swiper-progress` to a percentage.
|
|
383
|
+
- Hide the dots from assistive tech when the carousel already announces its position.
|
|
384
|
+
|
|
385
|
+
### Stepper
|
|
386
|
+
|
|
387
|
+
Figma has a vertical stepper with titles and notes, and a timeline with markers only. Each step is idle, active or done.
|
|
388
|
+
|
|
389
|
+
```html
|
|
390
|
+
<ol class="bpjs-stepper bpjs-stepper--vertical">
|
|
391
|
+
<li class="bpjs-step bpjs-step--done">
|
|
392
|
+
<span class="bpjs-step__marker">
|
|
393
|
+
<svg class="bpjs-icon bpjs-step__icon" aria-hidden="true"><use href="icons.svg#check"></use></svg>
|
|
394
|
+
</span>
|
|
395
|
+
<span class="bpjs-step__content">
|
|
396
|
+
<span class="bpjs-step__title">Isi data peserta</span>
|
|
397
|
+
<span class="bpjs-step__note">Nama, NIK, dan tanggal lahir</span>
|
|
398
|
+
</span>
|
|
399
|
+
</li>
|
|
400
|
+
<li class="bpjs-step bpjs-step--active" aria-current="step">
|
|
401
|
+
<span class="bpjs-step__marker"><span class="bpjs-step__number" aria-hidden="true">2</span></span>
|
|
402
|
+
<span class="bpjs-step__content">
|
|
403
|
+
<span class="bpjs-step__title">Verifikasi</span>
|
|
404
|
+
</span>
|
|
405
|
+
</li>
|
|
406
|
+
</ol>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- Orientation: `bpjs-stepper--vertical` (titles and notes) or `bpjs-stepper--timeline` (markers and lines only, no titles or notes)
|
|
410
|
+
- States: `bpjs-step--idle`, `bpjs-step--active` (add `aria-current="step"`) and `bpjs-step--done` (filled, with a check icon)
|
|
411
|
+
- Use an ordered list, so screen readers announce the step count.
|
|
412
|
+
|
|
413
|
+
### Tooltip
|
|
414
|
+
|
|
415
|
+
A dark box with a title, an optional description and a pointer. It shows on hover or keyboard focus of its trigger.
|
|
416
|
+
|
|
417
|
+
```html
|
|
418
|
+
<span class="bpjs-tooltip-wrap">
|
|
419
|
+
<button type="button" class="bpjs-button bpjs-button--secondary bpjs-button--sm" aria-describedby="tip-simpan">Simpan</button>
|
|
420
|
+
<span role="tooltip" id="tip-simpan" class="bpjs-tooltip bpjs-tooltip--top">
|
|
421
|
+
<span class="bpjs-tooltip__title">Simpan perubahan</span>
|
|
422
|
+
<span class="bpjs-tooltip__description">Data akan tersimpan ke server</span>
|
|
423
|
+
</span>
|
|
424
|
+
</span>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
- Placement: `--top`, `--bottom`, `--left` or `--right`. For top and bottom, add `--start` or `--end` to align with an edge.
|
|
428
|
+
- Add `--open` to keep it visible, for static pages.
|
|
429
|
+
- Put essential information in the page, not only in a tooltip. The tooltip is shown with CSS and has no Esc key handling.
|
|
430
|
+
|
|
431
|
+
### Dropdown
|
|
432
|
+
|
|
433
|
+
A menu panel with an optional header, items and dividers. Your code shows and positions it.
|
|
434
|
+
|
|
435
|
+
```html
|
|
436
|
+
<div class="bpjs-dropdown">
|
|
437
|
+
<div class="bpjs-dropdown-header">
|
|
438
|
+
<span class="bpjs-dropdown-header__title">Akun saya</span>
|
|
439
|
+
<span class="bpjs-dropdown-header__description">user@example.com</span>
|
|
440
|
+
</div>
|
|
441
|
+
<div class="bpjs-dropdown__divider" role="separator"></div>
|
|
442
|
+
<button type="button" class="bpjs-dropdown-item">
|
|
443
|
+
<svg class="bpjs-icon bpjs-dropdown-item__leading" aria-hidden="true"><use href="icons.svg#user"></use></svg>
|
|
444
|
+
<span class="bpjs-dropdown-item__label">Profil saya</span>
|
|
445
|
+
</button>
|
|
446
|
+
</div>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
- The panel is 240px wide. Each item is 40px tall.
|
|
450
|
+
- Items are buttons, so Enter and Space work. Disable an item with `disabled`.
|
|
451
|
+
- For a user header, add `bpjs-dropdown-header--user` and put an avatar before the text.
|
|
452
|
+
- Items have an optional leading icon. Figma's sample items have no trailing icon. For a submenu, add a trailing icon with `bpjs-dropdown-item__trailing`, such as a chevron.
|
|
453
|
+
|
|
454
|
+
### Select
|
|
455
|
+
|
|
456
|
+
A native `<select>` styled to match Figma. The browser handles the menu, the keyboard and screen readers. Wrap it in a Form Item.
|
|
457
|
+
|
|
458
|
+
```html
|
|
459
|
+
<div class="bpjs-form-item">
|
|
460
|
+
<label class="bpjs-form-item__label" for="kota">Kota</label>
|
|
461
|
+
<div class="bpjs-select bpjs-control-field">
|
|
462
|
+
<select class="bpjs-select__control" id="kota" name="kota">
|
|
463
|
+
<option value="" disabled selected hidden>Pilih kota</option>
|
|
464
|
+
<option value="jkt">Jakarta</option>
|
|
465
|
+
</select>
|
|
466
|
+
<svg class="bpjs-icon bpjs-select__icon" aria-hidden="true"><use href="icons.svg#chevron-down"></use></svg>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
- Placeholder: the first option, with `value=""`.
|
|
472
|
+
- Prefix: put `<span class="bpjs-select__addon bpjs-select__addon--prefix">` before the select.
|
|
473
|
+
- Danger: `aria-invalid="true"` on the select. Disabled: `disabled` on the select.
|
|
474
|
+
|
|
475
|
+
### Segmented
|
|
476
|
+
|
|
477
|
+
A gray track with one selected item. Use it to switch a view, such as list and grid, or to filter.
|
|
478
|
+
|
|
479
|
+
```html
|
|
480
|
+
<div class="bpjs-segmented" role="group" aria-label="Filter">
|
|
481
|
+
<button type="button" class="bpjs-segmented-item bpjs-segmented-item--active" aria-pressed="true">Semua</button>
|
|
482
|
+
<button type="button" class="bpjs-segmented-item" aria-pressed="false">Aktif</button>
|
|
483
|
+
</div>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
- The selected item is marked with `bpjs-segmented-item--active` and `aria-pressed="true"`. Update both when the selection changes.
|
|
487
|
+
- Disable an item with `disabled`.
|
|
488
|
+
- Items are 36px tall inside a 40px track.
|
|
489
|
+
|
|
490
|
+
### Pagination
|
|
491
|
+
|
|
492
|
+
Page numbers with previous and next buttons. Add a summary and a per-page select when needed.
|
|
493
|
+
|
|
494
|
+
```html
|
|
495
|
+
<nav class="bpjs-pagination" aria-label="Halaman">
|
|
496
|
+
<button type="button" class="bpjs-pagination__nav" aria-label="Sebelumnya" disabled>
|
|
497
|
+
<svg class="bpjs-icon" aria-hidden="true"><use href="icons.svg#chevron-left"></use></svg>
|
|
498
|
+
</button>
|
|
499
|
+
<ol class="bpjs-pagination__pages">
|
|
500
|
+
<li><button type="button" class="bpjs-pagination__page bpjs-pagination__page--active" aria-current="page">1</button></li>
|
|
501
|
+
<li><button type="button" class="bpjs-pagination__page">2</button></li>
|
|
502
|
+
<li><span class="bpjs-pagination__ellipsis" aria-hidden="true">…</span></li>
|
|
503
|
+
<li><button type="button" class="bpjs-pagination__page">10</button></li>
|
|
504
|
+
</ol>
|
|
505
|
+
<button type="button" class="bpjs-pagination__nav" aria-label="Berikutnya">
|
|
506
|
+
<svg class="bpjs-icon" aria-hidden="true"><use href="icons.svg#chevron-right"></use></svg>
|
|
507
|
+
</button>
|
|
508
|
+
</nav>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
- Page numbers are 32px squares. The current page has `aria-current="page"` and the brand fill.
|
|
512
|
+
- Skipped pages are shown with `…`, which is not a button.
|
|
513
|
+
- The previous and next buttons need an `aria-label`, since they show only an icon.
|
|
514
|
+
|
|
515
|
+
### Table
|
|
516
|
+
|
|
517
|
+
A card with a title, a toolbar, a table and a footer. The toolbar, footer and empty state use the parts above.
|
|
518
|
+
|
|
519
|
+
```html
|
|
520
|
+
<section class="bpjs-table-card" aria-label="Daftar peserta">
|
|
521
|
+
<header class="bpjs-table-title">
|
|
522
|
+
<div>
|
|
523
|
+
<h2 class="bpjs-table-title__heading">Daftar peserta</h2>
|
|
524
|
+
<p class="bpjs-table-title__description">Data peserta aktif</p>
|
|
525
|
+
</div>
|
|
526
|
+
</header>
|
|
527
|
+
<div class="bpjs-table-scroll">
|
|
528
|
+
<table class="bpjs-table">
|
|
529
|
+
<thead>
|
|
530
|
+
<tr><th scope="col">Nama</th><th scope="col">Kelas</th></tr>
|
|
531
|
+
</thead>
|
|
532
|
+
<tbody>
|
|
533
|
+
<tr><td>Budi Santoso</td><td>Kelas I</td></tr>
|
|
534
|
+
</tbody>
|
|
535
|
+
</table>
|
|
536
|
+
</div>
|
|
537
|
+
</section>
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
- Use real `<th scope="col">` and `<td>` cells, so screen readers can read the table.
|
|
541
|
+
- Checkbox column: put `class="bpjs-table__check"` on the header and cell, and an `<input type="checkbox">` with an `aria-label`.
|
|
542
|
+
- The table scrolls sideways inside `.bpjs-table-scroll` on narrow screens.
|
|
543
|
+
- Empty state: replace the rows with one row that holds `.bpjs-empty-state`, with a title and a description.
|
|
544
|
+
- Sort: use a `<button class="bpjs-table__sort">` in the header cell.
|
|
545
|
+
|
|
546
|
+
### File Upload
|
|
547
|
+
|
|
548
|
+
A drop zone for choosing files, and a row that shows each file's upload progress.
|
|
549
|
+
|
|
550
|
+
```html
|
|
551
|
+
<div class="bpjs-form-item">
|
|
552
|
+
<label class="bpjs-form-item__label" for="dokumen">Dokumen</label>
|
|
553
|
+
<div class="bpjs-file-upload">
|
|
554
|
+
<label class="bpjs-file-upload__zone">
|
|
555
|
+
<input class="bpjs-file-upload__input" id="dokumen" type="file" accept=".pdf,.png,.jpg,.jpeg" />
|
|
556
|
+
<span class="bpjs-file-upload__icon" aria-hidden="true"><svg class="bpjs-icon"><use href="icons.svg#upload"></use></svg></span>
|
|
557
|
+
<span class="bpjs-file-upload__title"><span class="bpjs-file-upload__action">Klik untuk mengupload</span> atau drag file</span>
|
|
558
|
+
<span class="bpjs-file-upload__hint">PDF, PNG atau JPG file (Max. 2MB)</span>
|
|
559
|
+
</label>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
|
|
563
|
+
<div class="bpjs-file-row bpjs-file-row--uploading" aria-live="polite">
|
|
564
|
+
<span class="bpjs-file-row__name">surat.pdf</span>
|
|
565
|
+
<span class="bpjs-file-row__meta">
|
|
566
|
+
<span>12 KB of 20 KB</span>
|
|
567
|
+
<span aria-hidden="true">|</span>
|
|
568
|
+
<span class="bpjs-file-row__status">Uploading</span>
|
|
569
|
+
<span class="bpjs-file-row__percent">50%</span>
|
|
570
|
+
</span>
|
|
571
|
+
<span class="bpjs-file-row__bar" role="progressbar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" aria-label="Progres unggah surat.pdf">
|
|
572
|
+
<span style="--bpjs-file-progress: 50%"></span>
|
|
573
|
+
</span>
|
|
574
|
+
</div>
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
- The drop zone's input is visually hidden but still focusable. The label is the visible target, so clicking it opens the file picker.
|
|
578
|
+
- Disabled: `disabled` on the input.
|
|
579
|
+
- File row states: `bpjs-file-row--uploading`, `bpjs-file-row--completed` and `bpjs-file-row--failed`. A failed row shows a `bpjs-file-row__retry` button ("Mulai ulang").
|
|
580
|
+
- Your code must set `--bpjs-file-progress` and `aria-valuenow` as the upload moves, and switch the state class when it finishes or fails. The rows are not wired to uploads.
|
|
581
|
+
|
|
582
|
+
None of the new components ship JavaScript. Your code must update the selected Segmented item (`aria-pressed` and the active class) and the current Pagination page (`aria-current`) when the user changes them.
|
|
583
|
+
|
|
584
|
+
### Date Picker and Time Picker
|
|
585
|
+
|
|
586
|
+
Not built yet. Figma's Date Picker and Time Picker pages are empty, so there is no layout to match. The calendar components on other Figma pages may be parts of the Date Picker, but this is not confirmed.
|
|
587
|
+
|
|
588
|
+
## Icons
|
|
589
|
+
|
|
590
|
+
There are 154 monochrome icons from Figma's Assets / Icons. Brands, Miscs and Layanan are not included yet. Each icon uses `currentColor`, so it takes the text color around it.
|
|
591
|
+
|
|
592
|
+
Import only the icons you use. Bundlers drop the rest:
|
|
593
|
+
|
|
594
|
+
```js
|
|
595
|
+
import { arrowRight, close } from "@bpjs/design-system/icons";
|
|
596
|
+
|
|
597
|
+
button.innerHTML = `${close}<span>Tutup</span>`;
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
To look up an icon by its Figma name, use the map entry. It includes every icon:
|
|
601
|
+
|
|
602
|
+
```js
|
|
603
|
+
import { icon } from "@bpjs/design-system/icons/map";
|
|
604
|
+
|
|
605
|
+
icon("arrow-right"); // inline SVG markup
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Without a bundler, use the sprite. Serve `icons.svg` from the same origin as the page:
|
|
609
|
+
|
|
610
|
+
```html
|
|
611
|
+
<svg class="bpjs-icon bpjs-icon--lg" aria-hidden="true"><use href="icons.svg#close"></use></svg>
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Sizes: `bpjs-icon--xs` (12px), `--sm` (16px), `--md` (20px, default), `--lg` (24px), `--xl` (32px).
|
|
615
|
+
|
|
616
|
+
Icons are decorative by default. For an icon-only control, put `aria-label` on the button.
|
|
617
|
+
|
|
618
|
+
## JavaScript API
|
|
619
|
+
|
|
620
|
+
```js
|
|
621
|
+
import {
|
|
622
|
+
init,
|
|
623
|
+
initModals,
|
|
624
|
+
openModal,
|
|
625
|
+
closeModal,
|
|
626
|
+
initControls,
|
|
627
|
+
setIndeterminate,
|
|
628
|
+
} from "@bpjs/design-system";
|
|
629
|
+
|
|
630
|
+
const teardown = init(document); // wires every data-bpjs-* trigger and the form controls; returns a cleanup function
|
|
631
|
+
openModal("confirm-delete", { trigger: deleteButton });
|
|
632
|
+
closeModal("confirm-delete", "confirm");
|
|
633
|
+
setIndeterminate(selectAll, true);
|
|
634
|
+
teardown();
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
- `init(root)` calls `initModals` and `initControls`. Use the individual functions if you need only one.
|
|
638
|
+
- `setIndeterminate(input, value)` sets the mixed state on a checkbox. It only accepts `<input type="checkbox">`.
|
|
639
|
+
|
|
640
|
+
The package has no runtime dependencies and no network calls. Nothing is sent anywhere.
|
|
641
|
+
|
|
642
|
+
## Browser support
|
|
643
|
+
|
|
644
|
+
Chrome and Edge 111+, Firefox 121+, Safari 16.4+. Firefox 121 is the minimum because the form controls use `:has()` to read their state from the input.
|
|
645
|
+
|
|
646
|
+
## Accessibility
|
|
647
|
+
|
|
648
|
+
- Components use native elements (`button`, `dialog`, `label`, `input`) where possible.
|
|
649
|
+
- Icon-only buttons need an `aria-label`. Decorative icons are hidden from assistive tech.
|
|
650
|
+
- Switches need a label. Use `bpjs-sr-only` for a label the design does not show.
|
|
651
|
+
- Automated checks catch only part of WCAG 2.1 AA. Full conformance still needs manual testing with assistive technology and an expert review.
|
|
652
|
+
|
|
653
|
+
## Known gaps
|
|
654
|
+
|
|
655
|
+
These come from the Figma file. The tokens and icons are unchanged until the design file is updated.
|
|
656
|
+
|
|
657
|
+
### Font weight
|
|
658
|
+
|
|
659
|
+
Figma defines a Black (900) weight, but Plus Jakarta Sans stops at 800, so 900 renders as 800.
|
|
660
|
+
|
|
661
|
+
### Native controls
|
|
662
|
+
|
|
663
|
+
Components give their buttons, inputs, selects and textareas the design font. Native controls outside the components keep the browser font unless you set `font: inherit` on them. A custom control you build yourself needs `font: inherit` or the design font.
|
|
664
|
+
|
|
665
|
+
### Table data
|
|
666
|
+
|
|
667
|
+
The Table is a native `<table>` with the layout and styles only. Sorting and row selection are not wired to data, so your code must handle them. The Table scrolls sideways on narrow screens.
|
|
668
|
+
|
|
669
|
+
### Swiper
|
|
670
|
+
|
|
671
|
+
The Swiper is only the position indicator. The carousel itself is not part of this library.
|
|
672
|
+
|
|
673
|
+
### Pill icon
|
|
674
|
+
|
|
675
|
+
Figma's Pills have a 16px leading icon. The export for that glyph failed, so the examples use `check` as a placeholder. The active pill with its count badge is about 99px wide, against Figma's 90px. Most of the difference is the count badge, which is about 27px wide here and 20px in Figma. Inactive pills show a count badge too, as in Figma. The Radio Pill is 72px in Figma and 72px here.
|
|
676
|
+
|
|
677
|
+
### Avatar status
|
|
678
|
+
|
|
679
|
+
At the XS and SM sizes, the badge count is 9px inside a 12px circle, and the icon status uses a generic user glyph. Both follow Figma's sizes. The glyph may change in a later design update.
|
|
680
|
+
|
|
681
|
+
### File Upload
|
|
682
|
+
|
|
683
|
+
The drop zone and file row are both built, but the file row is not tied to a real upload. Your code must update its progress and state.
|
|
684
|
+
|
|
685
|
+
### Badge widths
|
|
686
|
+
|
|
687
|
+
Badges size to their content. In Figma, labeled badges range from 44px to 102px wide, and icon-only badges are 22, 24 or 28px. Figma's labeled badges with the same text are not all the same width, and the design file does not explain why. Our badges size to their text, so they may not match Figma's widths exactly.
|
|
688
|
+
|
|
689
|
+
### Contrast
|
|
690
|
+
|
|
691
|
+
Some Figma text colors fail WCAG AA (4.5:1 for normal text) on white or on tinted backgrounds, such as pills, badges and the gray track. The tokens are unchanged because they come from the design file:
|
|
692
|
+
|
|
693
|
+
| Token | Value | Contrast | Where it appears |
|
|
694
|
+
| --- | --- | --- | --- |
|
|
695
|
+
| `--bpjs-text-very-low` | `#adadad` on white | 2.24:1 | Disabled placeholders and Subtitle, Body and Note levels |
|
|
696
|
+
| `--bpjs-text-low` | `#909090` on white | 3.19:1 | Disabled labels and captions, Note and Body levels |
|
|
697
|
+
| `--bpjs-text-warning` | `#c88629` on white | 3.04:1 | Warning text |
|
|
698
|
+
| `--bpjs-text-success` | `#009736` on white | 3.83:1 | Success text |
|
|
699
|
+
| `--bpjs-text-medium` on `--bpjs-background-medium` | `#737373` on `#f4f4f4` | 4.31:1 | Inactive Pill labels, Avatar text, Table avatar initials, inactive Segmented label |
|
|
700
|
+
| `--bpjs-text-success` on `--bpjs-background-success-soft` | `#009736` on `#eaf7ef` | 3.47:1 | Hard Success badge label |
|
|
701
|
+
| `--bpjs-text-warning` on `--bpjs-background-warning-soft` | `#c88629` on `#fcecd6` | 2.62:1 | Hard Warning badge label |
|
|
702
|
+
| `--bpjs-text-danger` on `--bpjs-background-danger-soft` | `#e60400` on `#fdeded` | 4.22:1 | Hard Danger badge label |
|
|
703
|
+
| `--bpjs-text-success` on white | `#009736` on `#fdfdfd` | 3.83:1 | File Upload "Selesai" status |
|
|
704
|
+
|
|
705
|
+
`text-medium` (4.74:1) and `text-danger` (4.79:1) on white pass. Until the design file darkens the failing colors, avoid them for text people need to read. They are fine for decoration, or for disabled controls, which WCAG exempts.
|
|
706
|
+
|
|
707
|
+
## License
|
|
708
|
+
|
|
709
|
+
UNLICENSED for now, so no use rights are granted until a license is chosen. The bundled Plus Jakarta Sans font is licensed under the SIL Open Font License 1.1 (`dist/fonts/OFL.txt`).
|