benivo-ui-library 1.9.75 → 1.9.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +22 -0
  2. package/accordion.less +167 -0
  3. package/activityFeed.less +394 -0
  4. package/alert.less +278 -0
  5. package/alertBanner.less +145 -0
  6. package/avatar.less +280 -0
  7. package/badge.less +2 -2
  8. package/benivo-ui-lib.less +34 -0
  9. package/breadcrumbs.less +92 -0
  10. package/button.less +327 -311
  11. package/checkIcon.less +47 -0
  12. package/checkItem.less +31 -0
  13. package/checkboxGroup.less +198 -0
  14. package/closeButton.less +70 -0
  15. package/copyInput.less +88 -52
  16. package/datePicker.less +505 -129
  17. package/emptyState.less +151 -0
  18. package/featuredIcon.less +13 -1
  19. package/fileCard.less +345 -0
  20. package/fileTypeIcon.less +12 -0
  21. package/fileUpload.less +268 -0
  22. package/flag.less +44 -0
  23. package/flagFeatureIcon.less +41 -0
  24. package/formSection.less +162 -0
  25. package/forms.less +30 -27
  26. package/heroInput.less +322 -0
  27. package/illustration.less +62 -0
  28. package/index.js +1 -1
  29. package/index.js.LICENSE.txt +1 -1
  30. package/input.less +208 -71
  31. package/menu.less +247 -0
  32. package/miButton.less +149 -0
  33. package/modal.less +226 -0
  34. package/notificationCard.less +202 -0
  35. package/notifications.less +13 -4
  36. package/optionList.less +102 -0
  37. package/package.json +10 -4
  38. package/pageIndicator.less +125 -0
  39. package/pagination.less +222 -0
  40. package/phoneNumber.less +56 -16
  41. package/progress.less +242 -0
  42. package/radiobutton.less +29 -0
  43. package/select.less +284 -55
  44. package/slider.less +98 -0
  45. package/starIcon.less +25 -0
  46. package/tabGroup.less +59 -12
  47. package/tabs.less +47 -9
  48. package/tag.less +252 -0
  49. package/tagsSelect.less +164 -76
  50. package/textarea.less +40 -32
  51. package/tileGeometry.less +12 -0
  52. package/tooltip.less +45 -20
  53. package/variables.less +122 -13
  54. package/verificationCode.less +142 -0
package/README.md CHANGED
@@ -4,6 +4,9 @@
4
4
 
5
5
  `benivo-ui-library` is a collection of react hooks and components for faster development.
6
6
 
7
+ Accessibility is tested automatically with jest-axe — run `npm run a11y:audit` and see
8
+ [docs/accessibility-testing.md](docs/accessibility-testing.md) for the enforcement model.
9
+
7
10
  # Installation
8
11
 
9
12
  ```sh
@@ -36,3 +39,22 @@ function MyApp() {
36
39
  );
37
40
  }
38
41
  ```
42
+
43
+ # Publishing
44
+
45
+ Publishing auth comes from the `NPM_TOKEN` environment variable — never from a token
46
+ committed to this repo. Both `.npmrc` files reference it as `${NPM_TOKEN}`, which npm
47
+ expands at run time.
48
+
49
+ ```bash
50
+ export NPM_TOKEN=<your granular npm publish token for benivo-ui-library>
51
+ npm run publish:lib
52
+ ```
53
+
54
+ `publish:lib` runs `check:npm-token` first and stops with a clear message if `NPM_TOKEN`
55
+ is unset, rather than failing part-way through a publish.
56
+
57
+ Use a **granular** npm access token scoped to the `benivo-ui-library` package only, with
58
+ publish rights and an expiry — not a classic automation token on the whole `benivo`
59
+ account. Keep it in your own environment or a CI secret. Do not add it to `.npmrc`,
60
+ `.storybook/.npmrc`, or any other tracked file.
package/accordion.less ADDED
@@ -0,0 +1,167 @@
1
+ //
2
+ // Accordion
3
+ //
4
+ // One row: a chevron, a title, and a panel that opens under it.
5
+ //
6
+ // padding border radius
7
+ // none — — —
8
+ // stroke 24px top 1px top —
9
+ // card 16px all 1px all 12px
10
+ //
11
+ // Only those three lines differ between the looks. Everything else — the 24px
12
+ // gap, the 18/28 title, the 16/24 body, the 8px above the panel — is shared.
13
+ //
14
+ // Figma also carries a Breakpoint property, which is not a variant here: the
15
+ // mobile frames are identical to the desktop ones apart from being narrower, so
16
+ // this reflows rather than switching on a prop.
17
+ //
18
+ //
19
+ // The group holds a set of rows and does nothing else — no border, no padding,
20
+ // no background. It only decides how far apart they sit, and that depends on the
21
+ // divider: a stroke needs clear air either side of its rule, while cards carry
22
+ // their own edges and sit closer.
23
+ //
24
+ // none 24px
25
+ // stroke 24px
26
+ // card 10px
27
+ //
28
+ // `align-items` is left alone so the rows stretch to the group's width. Figma
29
+ // says items-start with w-full on each row, which comes to the same thing without
30
+ // reaching into the children.
31
+ //
32
+ .bnv-accordion-group {
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 1.5rem;
36
+ width: 100%;
37
+
38
+ &--card {
39
+ gap: .625rem;
40
+ }
41
+ }
42
+
43
+ .bnv-accordion {
44
+ box-sizing: border-box;
45
+ min-width: 320px;
46
+
47
+ // Structural only. The row needs to appear in a screen reader's heading list,
48
+ // but its size comes from __title, not from whichever level is used.
49
+ &__heading {
50
+ margin: 0;
51
+ font: inherit;
52
+ }
53
+
54
+ &__trigger {
55
+ display: flex;
56
+ align-items: flex-start;
57
+ gap: 1.5rem;
58
+ width: 100%;
59
+ margin: 0;
60
+ padding: 0;
61
+ border: 0;
62
+ background: none;
63
+ cursor: pointer;
64
+ text-align: left;
65
+ font-family: @font-family;
66
+
67
+ // Keyboard only, so clicking a row does not leave a ring behind. Written
68
+ // out rather than sharing the button restyle's variables, which are not on
69
+ // main yet — worth folding together once that lands.
70
+ &:focus-visible {
71
+ outline: 2px solid var(--brand-400);
72
+ outline-offset: 2px;
73
+ }
74
+ }
75
+
76
+ &__icon {
77
+ display: inline-flex;
78
+ flex-shrink: 0;
79
+ // Figma nudges the 24px chevron 2px down so it reads as centred against
80
+ // the 28px title line instead of sitting flush with its ascender.
81
+ padding-top: 2px;
82
+ color: var(--text-400);
83
+
84
+ svg {
85
+ display: block;
86
+ }
87
+ }
88
+
89
+ // --- hover -----------------------------------------------------------
90
+ //
91
+ // Guarded on a device that genuinely hovers: a touch device latches :hover
92
+ // after a tap, which would leave the last-tapped row looking permanently
93
+ // active.
94
+ //
95
+ // The hover belongs to the trigger, not the whole row — the panel is not
96
+ // clickable, so hovering the answer must not suggest that it is. For the card
97
+ // that means matching on the trigger but colouring the card, hence `:has()`
98
+ // rather than a plain `:hover` on the root.
99
+ @media (hover: hover) {
100
+ .bnv-accordion__trigger:hover .bnv-accordion__icon {
101
+ color: var(--text-600);
102
+ }
103
+
104
+ &--card:has(.bnv-accordion__trigger:hover) {
105
+ background: var(--bg-50);
106
+ }
107
+ }
108
+
109
+ &__title {
110
+ flex: 1 0 0;
111
+ min-width: 0;
112
+ color: var(--black);
113
+ font-size: 1.125rem;
114
+ line-height: 1.75rem;
115
+ font-weight: @font-weight-medium;
116
+ }
117
+
118
+ // No `display` here on purpose: the panel is hidden with the `hidden`
119
+ // attribute, and a display rule would override the browser's own
120
+ // `[hidden] { display: none }` and leave the answer on screen when closed.
121
+ &__panel {
122
+ margin-top: .5rem;
123
+ // 24px chevron + 24px gap, so the body lines up under the title rather
124
+ // than under the icon.
125
+ padding-left: 3rem;
126
+ color: var(--text-600);
127
+ font-size: 1rem;
128
+ line-height: 1.5rem;
129
+ font-weight: @font-weight-normal;
130
+ }
131
+
132
+ // --- icon position ---------------------------------------------------
133
+ // `order` rather than row-reverse: the chevron moves but the DOM does not,
134
+ // so the title stays first for anything reading the markup.
135
+ &--icon-right {
136
+ .bnv-accordion__icon { order: 1; }
137
+
138
+ .bnv-accordion__panel {
139
+ padding-left: 0;
140
+ padding-right: 3rem;
141
+ }
142
+ }
143
+
144
+ // --- dividers --------------------------------------------------------
145
+ //
146
+ // Both rules are drawn with an inset shadow rather than a border, because
147
+ // Figma puts its strokes *inside* the frame while a CSS border adds to it.
148
+ // With a border, stroke came out 1px taller than the design and card 2px
149
+ // taller AND 2px narrower — which moved where the body copy wraps, so it was
150
+ // not only a total-height rounding difference. An inset shadow occupies no
151
+ // space, which is exactly what Figma is doing.
152
+ //
153
+ // The rule sits on top of each row, so stacking rows draws the lines between
154
+ // them without needing a wrapper. The first row gets one too, which is
155
+ // Figma's.
156
+ &--stroke {
157
+ box-shadow: inset 0 1px 0 0 var(--border-200);
158
+ padding-top: 1.5rem;
159
+ }
160
+
161
+ &--card {
162
+ background: var(--white);
163
+ box-shadow: inset 0 0 0 1px var(--border-200);
164
+ border-radius: 12px;
165
+ padding: 1rem;
166
+ }
167
+ }
@@ -0,0 +1,394 @@
1
+ //
2
+ // Activity feed
3
+ //
4
+ // A timeline / message log built from FeedItems, matching the DS "Feed item base"
5
+ // and "Activity feed" (Figma page 253-14231). The container owns spacing and
6
+ // separators; the item owns its anatomy and the connector rail.
7
+ //
8
+ // Colours are tokens only (see scripts/check-tokens.sh): text on --black /
9
+ // --text-400 / --text-600, links and the "new" dot on --brand-700, rail and
10
+ // separators on --border-200, the date band on --bg-100.
11
+ //
12
+
13
+ @feed-bubble-shadow: 0 1px 1px rgba(16, 24, 40, 0.05);
14
+ @feed-gap: 32px; // spacing between items (Figma: 32px in every divider mode)
15
+
16
+ .bnv-feed {
17
+ display: flex;
18
+ flex-direction: column;
19
+ font-family: @font-family;
20
+
21
+ // ---- spacing per divider style ----
22
+ // none: 32px between items.
23
+ &--divider-none {
24
+ gap: @feed-gap;
25
+ }
26
+
27
+ // line: a full-width 1px separator sits in a 16px gap either side (~32px total).
28
+ &--divider-line {
29
+ gap: 16px;
30
+ }
31
+
32
+ // connector: the rail is a flex segment inside the item, so the item carries
33
+ // its own bottom spacing (Figma: content pb 32). No container gap.
34
+ &--divider-connector {
35
+ gap: 0;
36
+
37
+ .bnv-feed-item:last-child .bnv-feed-item__body {
38
+ padding-bottom: 0;
39
+ }
40
+ }
41
+
42
+ // With a 32px container gap, the date's own 12px block margin would make the
43
+ // date↔item spacing 44px, which reads as disconnected. Drop it here so it is a
44
+ // clean 32px, uniform with the item spacing. (Line mode keeps the 12px → 28px.)
45
+ &--divider-none &__date,
46
+ &--divider-connector &__date {
47
+ margin: 0;
48
+ }
49
+
50
+ // ---- separators ----
51
+ &__separator {
52
+ height: 1px;
53
+ background-color: var(--border-200);
54
+ border: 0;
55
+ flex-shrink: 0;
56
+ }
57
+
58
+ // ---- day / date grouping header ----
59
+ // A pill (bg-100) with a calendar glyph and a bold date. The 12px block
60
+ // padding sits on top of the container gap, matching Figma's date frame.
61
+ &__date {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 10px;
65
+ box-sizing: border-box;
66
+ width: 100%;
67
+ margin: 12px 0;
68
+ padding: 8px 12px;
69
+ background-color: var(--bg-100);
70
+ border-radius: 0.5rem;
71
+ }
72
+
73
+ &__date-icon {
74
+ flex-shrink: 0;
75
+ color: var(--text-600);
76
+ }
77
+
78
+ &__date-label {
79
+ font-size: @font-size; // 16px
80
+ line-height: 24px;
81
+ font-weight: @font-weight-semibold;
82
+ color: var(--black);
83
+ white-space: nowrap;
84
+ }
85
+ }
86
+
87
+ // ------------------------------------------------------------------ FeedItem
88
+ .bnv-feed-item {
89
+ --fi-avatar: 48px;
90
+ position: relative;
91
+ display: flex;
92
+ gap: 12px;
93
+ align-items: flex-start;
94
+ box-sizing: border-box;
95
+ font-size: @font-size; // 16px
96
+ line-height: 24px;
97
+ color: var(--text-600);
98
+
99
+ &--sm {
100
+ --fi-avatar: 32px;
101
+ }
102
+
103
+ // ---------- leading column + rail ----------
104
+ &__leading {
105
+ display: flex;
106
+ flex-direction: column;
107
+ align-items: center;
108
+ flex-shrink: 0;
109
+ width: var(--fi-avatar);
110
+ }
111
+
112
+ &--connector &__leading {
113
+ align-self: stretch;
114
+ gap: 4px;
115
+ padding-bottom: 4px;
116
+ }
117
+
118
+ &__avatar {
119
+ display: inline-flex;
120
+ flex-shrink: 0;
121
+ }
122
+
123
+ // Fallback marker when there is no avatar/icon.
124
+ &__marker {
125
+ width: 10px;
126
+ height: 10px;
127
+ margin-top: 7px;
128
+ border-radius: 50%;
129
+ background-color: var(--text-300);
130
+ flex-shrink: 0;
131
+ }
132
+
133
+ // Decorative timeline rail — grows to fill the column below the avatar.
134
+ &__rail {
135
+ flex: 1 0 0;
136
+ width: 2px;
137
+ min-height: 8px;
138
+ border-radius: 2px;
139
+ background-color: var(--border-200);
140
+ }
141
+
142
+ // ---------- body ----------
143
+ &__body {
144
+ flex: 1 1 0;
145
+ min-width: 0;
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 12px;
149
+ }
150
+
151
+ &--connector &__body {
152
+ padding-bottom: @feed-gap;
153
+ }
154
+
155
+ // Header + action line sit tight together; 12px separates them from supporting.
156
+ &__text-block {
157
+ display: flex;
158
+ flex-direction: column;
159
+ }
160
+
161
+ &__heading {
162
+ display: flex;
163
+ flex-direction: column;
164
+ min-width: 0;
165
+ }
166
+
167
+ &__meta {
168
+ display: flex;
169
+ align-items: baseline;
170
+ flex-wrap: wrap;
171
+ gap: 8px;
172
+ min-width: 0;
173
+ }
174
+
175
+ &__actor {
176
+ font-weight: @font-weight-semibold;
177
+ color: var(--black);
178
+ }
179
+
180
+ &__timestamp {
181
+ font-weight: @font-weight-normal;
182
+ color: var(--text-400);
183
+ white-space: nowrap;
184
+ }
185
+
186
+ // @handle sub-line (messages).
187
+ &__handle {
188
+ font-size: @font-size-sm;
189
+ line-height: 20px;
190
+ color: var(--text-400);
191
+ }
192
+
193
+ // Status pill sits below the action line, left-aligned.
194
+ &__status {
195
+ display: flex;
196
+ }
197
+
198
+ // Edit/delete sit top-right, right of the name. Absolute + centred on the name
199
+ // row (24px) so taller buttons never push the description down.
200
+ &__trailing {
201
+ position: absolute;
202
+ top: 0;
203
+ right: 0;
204
+ height: 24px;
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 8px;
208
+ }
209
+
210
+ &__actions {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 2px;
214
+ }
215
+
216
+ // Row actions use the DS Button (icon-only). It carries its own styling — this
217
+ // class is only a hook for context tweaks if ever needed.
218
+ &__action-btn {
219
+ flex-shrink: 0;
220
+ }
221
+
222
+ // Visually-hidden label (e.g. "Unread") — exposed to assistive tech only.
223
+ &__sr-only {
224
+ position: absolute;
225
+ width: 1px;
226
+ height: 1px;
227
+ margin: -1px;
228
+ padding: 0;
229
+ border: 0;
230
+ overflow: hidden;
231
+ clip: rect(0 0 0 0);
232
+ clip-path: inset(50%);
233
+ white-space: nowrap;
234
+ }
235
+
236
+ // Unread indicator.
237
+ &__new-dot {
238
+ width: 10px;
239
+ height: 10px;
240
+ border-radius: 50%;
241
+ background-color: var(--brand-700);
242
+ flex-shrink: 0;
243
+ }
244
+
245
+ // ---------- action line ----------
246
+ // Kept uniformly 16px/24px across every variant so the name→description gap is
247
+ // identical whether the item has a file, labels, text or nothing.
248
+ &__action {
249
+ color: var(--text-600);
250
+
251
+ a,
252
+ .bnv-feed-item__link {
253
+ color: var(--brand-700);
254
+ font-weight: @font-weight-medium;
255
+ text-decoration: none;
256
+
257
+ &:hover { text-decoration: underline; }
258
+
259
+ &:focus-visible {
260
+ outline: 2px solid var(--primary);
261
+ outline-offset: 2px;
262
+ border-radius: 0.125rem;
263
+ }
264
+ }
265
+ }
266
+
267
+ // ---------- supporting: file ----------
268
+ // The file is its own hoverable target — download/delete belong to the document,
269
+ // so they sit on its right and reveal on hover/focus. A "card" outline is drawn
270
+ // just outside the content so nothing shifts when it appears.
271
+ &__file {
272
+ position: relative;
273
+ display: flex;
274
+ gap: 12px;
275
+ align-items: flex-start;
276
+ width: 100%;
277
+ min-width: 0;
278
+
279
+ &::before {
280
+ content: '';
281
+ position: absolute;
282
+ inset: -8px -12px;
283
+ border: 1px solid transparent;
284
+ border-radius: 0.5rem;
285
+ pointer-events: none;
286
+ }
287
+
288
+ &:hover::before,
289
+ &:focus-within::before {
290
+ border-color: var(--border-200);
291
+ background-color: var(--white);
292
+ }
293
+
294
+ > * { position: relative; } // keep content above the ::before card
295
+ }
296
+
297
+ &__file-icon {
298
+ flex-shrink: 0;
299
+ width: 32px;
300
+ height: 40px;
301
+ }
302
+
303
+ &__file-text {
304
+ display: flex;
305
+ flex-direction: column;
306
+ flex: 1 1 auto;
307
+ min-width: 0;
308
+ }
309
+
310
+ &__file-name {
311
+ color: var(--black);
312
+ overflow: hidden;
313
+ text-overflow: ellipsis;
314
+ white-space: nowrap;
315
+ }
316
+
317
+ &__file-size {
318
+ font-size: @font-size-sm;
319
+ line-height: 20px;
320
+ color: var(--text-600);
321
+ }
322
+
323
+ // Actions on the right of the file row — hidden until hover/focus.
324
+ &__file-actions {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 2px;
328
+ flex-shrink: 0;
329
+ align-self: center;
330
+ opacity: 0;
331
+ transition: opacity 0.12s ease;
332
+ }
333
+
334
+ &__file:hover &__file-actions,
335
+ &__file:focus-within &__file-actions {
336
+ opacity: 1;
337
+ }
338
+
339
+ // Touch devices have no hover — keep the actions visible there.
340
+ @media (hover: none) {
341
+ &__file-actions { opacity: 1; }
342
+ }
343
+
344
+ // ---------- supporting: labels ----------
345
+ &__labels {
346
+ display: flex;
347
+ flex-wrap: wrap;
348
+ gap: 4px;
349
+ }
350
+
351
+ // ---------- supporting: text (message bubble) ----------
352
+ &__bubble {
353
+ box-sizing: border-box;
354
+ width: 100%;
355
+ padding: 12px;
356
+ background-color: var(--white);
357
+ border: 1px solid var(--border-200);
358
+ // Speech-bubble: square top-left, rounded elsewhere.
359
+ border-radius: 0 8px 8px 8px;
360
+ box-shadow: @feed-bubble-shadow;
361
+ }
362
+
363
+ &__text {
364
+ color: var(--black);
365
+ white-space: pre-wrap;
366
+ overflow-wrap: anywhere;
367
+
368
+ &.is-clamped {
369
+ display: -webkit-box;
370
+ -webkit-box-orient: vertical;
371
+ -webkit-line-clamp: 4; // overridden inline via clampLines
372
+ overflow: hidden;
373
+ position: relative;
374
+ }
375
+
376
+ // Fade the last visible line while clamped and actually overflowing.
377
+ &.is-clamped.is-overflowing::after {
378
+ content: '';
379
+ position: absolute;
380
+ left: 0;
381
+ right: 0;
382
+ bottom: 0;
383
+ height: 1.4em;
384
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--white));
385
+ pointer-events: none;
386
+ }
387
+ }
388
+
389
+ // The toggle is the DS Button (text/link). Only position it below the text.
390
+ &__toggle {
391
+ align-self: flex-start;
392
+ margin-top: 8px;
393
+ }
394
+ }