@infonomic/uikit 6.7.6 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/components/button/combo-button.d.ts +11 -4
  2. package/dist/components/button/combo-button.d.ts.map +1 -1
  3. package/dist/components/button/combo-button.js +20 -3
  4. package/dist/components/button/combo-button.module.css +20 -0
  5. package/dist/components/button/combo-button.module.js +7 -1
  6. package/dist/components/button/combo-button_module.css +20 -0
  7. package/dist/components/dropdown/dropdown.d.ts +9 -1
  8. package/dist/components/dropdown/dropdown.d.ts.map +1 -1
  9. package/dist/components/dropdown/dropdown.js +2 -1
  10. package/dist/components/inputs/checkbox.module.css +8 -0
  11. package/dist/components/inputs/checkbox_module.css +4 -0
  12. package/dist/components/inputs/label.d.ts.map +1 -1
  13. package/dist/components/inputs/label.js +1 -0
  14. package/dist/components/notifications/alert.module.css +2 -1
  15. package/dist/components/notifications/alert_module.css +2 -1
  16. package/dist/icons/index.d.ts +1 -0
  17. package/dist/icons/index.d.ts.map +1 -1
  18. package/dist/icons/index.js +1 -0
  19. package/dist/icons/markdown-icon.d.ts +11 -0
  20. package/dist/icons/markdown-icon.d.ts.map +1 -0
  21. package/dist/icons/markdown-icon.js +34 -0
  22. package/dist/react.d.ts +1 -0
  23. package/dist/react.d.ts.map +1 -1
  24. package/dist/react.js +1 -0
  25. package/dist/styles/styles.css +1 -1
  26. package/dist/widgets/modal/modal.d.ts.map +1 -1
  27. package/dist/widgets/modal/modal.js +14 -1
  28. package/dist/widgets/search/search.d.ts +2 -1
  29. package/dist/widgets/search/search.d.ts.map +1 -1
  30. package/dist/widgets/search/search.js +4 -11
  31. package/package.json +1 -1
  32. package/src/components/button/combo-button.module.css +20 -0
  33. package/src/components/button/combo-button.tsx +46 -2
  34. package/src/components/dropdown/dropdown.tsx +10 -0
  35. package/src/components/inputs/checkbox.module.css +8 -0
  36. package/src/components/inputs/label.tsx +8 -1
  37. package/src/components/notifications/alert.module.css +2 -1
  38. package/src/icons/index.ts +1 -0
  39. package/src/icons/markdown-icon.tsx +42 -0
  40. package/src/react.ts +1 -0
  41. package/src/styles/functional/colors.css +9 -3
  42. package/src/styles/local-fonts.css +3 -2
  43. package/src/widgets/modal/modal.tsx +33 -2
  44. package/src/widgets/search/search.tsx +7 -8
@@ -1,3 +1,5 @@
1
+ import { useRef } from 'react'
2
+
1
3
  import cx from 'classnames'
2
4
 
3
5
  import { ChevronDownIcon } from '../../icons/chevron-down-icon'
@@ -5,8 +7,19 @@ import { Dropdown as DropdownComponent } from '../dropdown/dropdown'
5
7
  import { Button, type ButtonProps } from './button'
6
8
  import styles from './combo-button.module.css'
7
9
 
10
+ export interface ComboButtonOption {
11
+ label: string
12
+ value: string
13
+ /**
14
+ * Optional leading icon for the menu item. Rendered before the label; items
15
+ * without one still align with their icon-bearing siblings, so a menu may
16
+ * mix the two.
17
+ */
18
+ icon?: React.ReactNode
19
+ }
20
+
8
21
  export type ComboButtonProps = ButtonProps & {
9
- options: { label: string; value: string }[]
22
+ options: ComboButtonOption[]
10
23
  onButtonClick?: () => void
11
24
  onOptionSelect?: (value: string) => void
12
25
  disabled?: boolean
@@ -38,8 +51,20 @@ export const ComboButton = ({
38
51
  intent = 'primary',
39
52
  ...rest
40
53
  }: ComboButtonProps) => {
54
+ // Reserve the icon column for every item as soon as one option carries an
55
+ // icon, so labels stay on a common left edge in a mixed menu.
56
+ const anyOptionHasIcon = options.some((option) => option.icon != null)
57
+
58
+ // The menu is positioned against the whole control, not against the dropdown
59
+ // half that triggers it. Anchoring to the trigger would measure `align`
60
+ // from the narrow chevron, so an `end`-aligned menu hangs off the right of
61
+ // the button and a `start`-aligned one starts at the chevron rather than at
62
+ // the button's left edge.
63
+ const wrapperRef = useRef<HTMLDivElement>(null)
64
+
41
65
  return (
42
66
  <div
67
+ ref={wrapperRef}
43
68
  className={cx('combo-button-wrapper', styles['combo-button-wrapper'], className)}
44
69
  style={{ '--ring-color': `var(--ring-${intent})` } as React.CSSProperties}
45
70
  >
@@ -65,6 +90,7 @@ export const ComboButton = ({
65
90
  <DropdownComponent.Content
66
91
  className={cx('combo-button-options', styles['combo-button-options'])}
67
92
  align={align}
93
+ anchor={wrapperRef}
68
94
  data-side={dataSide}
69
95
  sideOffset={sideOffset}
70
96
  >
@@ -76,7 +102,25 @@ export const ComboButton = ({
76
102
  <div
77
103
  className={cx('combo-button-options-item', styles['combo-button-options-item'])}
78
104
  >
79
- {option.label}
105
+ {anyOptionHasIcon && (
106
+ <span
107
+ className={cx(
108
+ 'combo-button-options-item-icon',
109
+ styles['combo-button-options-item-icon']
110
+ )}
111
+ aria-hidden="true"
112
+ >
113
+ {option.icon}
114
+ </span>
115
+ )}
116
+ <span
117
+ className={cx(
118
+ 'combo-button-options-item-label',
119
+ styles['combo-button-options-item-label']
120
+ )}
121
+ >
122
+ {option.label}
123
+ </span>
80
124
  </div>
81
125
  </DropdownComponent.Item>
82
126
  ))}
@@ -43,6 +43,7 @@ const Content = ({
43
43
  align,
44
44
  alignOffset,
45
45
  collisionPadding,
46
+ anchor,
46
47
  ...rest
47
48
  }: {
48
49
  ref?: React.RefObject<React.ComponentRef<'div'>>
@@ -53,6 +54,14 @@ const Content = ({
53
54
  align?: React.ComponentProps<typeof Menu.Positioner>['align']
54
55
  alignOffset?: React.ComponentProps<typeof Menu.Positioner>['alignOffset']
55
56
  collisionPadding?: React.ComponentProps<typeof Menu.Positioner>['collisionPadding']
57
+ /**
58
+ * Element the popup is positioned against. Defaults to the trigger, which is
59
+ * the right answer when the trigger is the whole control. Pass a wider
60
+ * element when the trigger is only part of it — a combo button's dropdown
61
+ * half, say — so `align` measures against the control the user sees rather
62
+ * than against the narrow trigger.
63
+ */
64
+ anchor?: React.ComponentProps<typeof Menu.Positioner>['anchor']
56
65
  } & Omit<React.ComponentProps<typeof Menu.Popup>, 'className'>): React.JSX.Element => {
57
66
  return (
58
67
  <Menu.Positioner
@@ -61,6 +70,7 @@ const Content = ({
61
70
  align={align}
62
71
  alignOffset={alignOffset}
63
72
  collisionPadding={collisionPadding}
73
+ anchor={anchor}
64
74
  >
65
75
  <Menu.Popup
66
76
  ref={ref}
@@ -42,6 +42,14 @@
42
42
  outline-color: var(--ring-color);
43
43
  }
44
44
 
45
+ /* a11y (WCAG 2.4.7): show a focus ring on keyboard focus regardless of
46
+ checked state. The intent-level ring color is always defined, whereas
47
+ --ring-color is only set on [data-checked], so use it directly here. */
48
+ .checkbox:focus-visible,
49
+ :global(.infonomic-checkbox):focus-visible {
50
+ outline-color: var(--checkbox-variant-outline-ring-color);
51
+ }
52
+
45
53
  .checkbox[data-disabled],
46
54
  :global(.infonomic-checkbox)[data-disabled] {
47
55
  pointer-events: none;
@@ -20,7 +20,14 @@ export function Label({ className, id, htmlFor, label, required }: LabelProps):
20
20
  className={cx('label', styles.label, className)}
21
21
  >
22
22
  {label}
23
- {required && <span className={styles.required}>&nbsp;*</span>}
23
+ {/* a11y: the input carries aria-required, so the visual asterisk is
24
+ decorative — hide it from the accessible name (it would otherwise be
25
+ read as part of the label via aria-labelledby). */}
26
+ {required && (
27
+ <span aria-hidden="true" className={styles.required}>
28
+ &nbsp;*
29
+ </span>
30
+ )}
24
31
  </label>
25
32
  )
26
33
  }
@@ -22,7 +22,8 @@
22
22
  transparency, without altering the --fill-<intent>-weak color primitives. */
23
23
  --alert-background-opacity: 0.5;
24
24
  background-color: oklch(
25
- from var(--alert-background) l c h / calc(alpha * var(--alert-background-opacity))
25
+ from var(--alert-background) l c h /
26
+ calc(alpha * var(--alert-background-opacity))
26
27
  );
27
28
  color: var(--foreground);
28
29
  margin-bottom: 1rem;
@@ -32,6 +32,7 @@ export * from './info-icon.js'
32
32
  export * from './infonomic-icon.js'
33
33
  export * from './light-icon.js'
34
34
  export * from './location-pin-icon.js'
35
+ export * from './markdown-icon.js'
35
36
  export * from './moon-icon.js'
36
37
  export * from './plus-icon.js'
37
38
  export * from './primary-icon.js'
@@ -0,0 +1,42 @@
1
+ import type React from 'react'
2
+
3
+ import cx from 'classnames'
4
+
5
+ import { IconElement } from './icon-element.js'
6
+ import styles from './icons.module.css'
7
+ import type { IconProps } from './types/icon.js'
8
+
9
+ /**
10
+ * The CommonMark mark — the conventional glyph for "this content is available
11
+ * as markdown". Used by surfaces that expose a document's `.md` representation.
12
+ */
13
+ export const MarkdownIcon = ({
14
+ className,
15
+ svgClassName,
16
+ ...rest
17
+ }: IconProps): React.JSX.Element => {
18
+ const applied = cx(styles['fill-current'], svgClassName)
19
+
20
+ return (
21
+ <IconElement className={cx('markdown-icon', className)} {...rest}>
22
+ <svg
23
+ className={applied}
24
+ xmlns="http://www.w3.org/2000/svg"
25
+ focusable="false"
26
+ aria-hidden="true"
27
+ viewBox="0 0 16 16"
28
+ strokeWidth="0"
29
+ >
30
+ <path
31
+ d="M14.5 3H1.5C0.671573 3 0 3.67157 0 4.5V11.5C0 12.3284 0.671573 13 1.5 13H14.5C15.3284 13 16 12.3284 16 11.5V4.5C16 3.67157 15.3284 3 14.5 3ZM1.5 4H14.5C14.7761 4 15 4.22386 15 4.5V11.5C15 11.7761 14.7761 12 14.5 12H1.5C1.22386 12 1 11.7761 1 11.5V4.5C1 4.22386 1.22386 4 1.5 4Z"
32
+ fillRule="evenodd"
33
+ clipRule="evenodd"
34
+ />
35
+ <path d="M2.5 10.5V5.5H4L5.5 7.5L7 5.5H8.5V10.5H7V7.75L5.5 9.75L4 7.75V10.5H2.5Z" />
36
+ <path d="M11.75 10.5L9.5 8H11V5.5H12.5V8H14L11.75 10.5Z" />
37
+ </svg>
38
+ </IconElement>
39
+ )
40
+ }
41
+
42
+ MarkdownIcon.displayName = 'MarkdownIcon'
package/src/react.ts CHANGED
@@ -81,6 +81,7 @@ export * from './icons/info-icon.js'
81
81
  export * from './icons/infonomic-icon.js'
82
82
  export * from './icons/light-icon.js'
83
83
  export * from './icons/location-pin-icon.js'
84
+ export * from './icons/markdown-icon.js'
84
85
  export * from './icons/moon-icon.js'
85
86
  export * from './icons/plus-icon.js'
86
87
  export * from './icons/primary-icon.js'
@@ -210,7 +210,9 @@
210
210
  --stroke-noeffect-hover: oklch(from var(--gray-500) calc(l * 0.75) c h);
211
211
  --stroke-noeffect-disabled: oklch(from var(--gray-500) calc(l * 1.5) calc(c * 0.8) h);
212
212
 
213
- --ring-noeffect: var(--gray-200);
213
+ /* a11y: gray-200 (#C0C0C0) is only 1.8:1 on white; gray-500 (#666666) is
214
+ 5.78:1, clearing the WCAG 1.4.11 / 2.4.7 3:1 non-text contrast minimum. */
215
+ --ring-noeffect: var(--gray-500);
214
216
 
215
217
  --gradient-noeffect-start: var(--gray-100);
216
218
  --gradient-noeffect-end: var(--gray-200);
@@ -637,7 +639,9 @@
637
639
  --stroke-noeffect-hover: oklch(from var(--gray-500) calc(l * 1.2) c h);
638
640
  --stroke-noeffect-disabled: oklch(from var(--gray-500) calc(l * 0.8) calc(c * 0.8) h);
639
641
 
640
- --ring-noeffect: var(--gray-900);
642
+ /* a11y: a dark ring (gray-900) is ~1.1:1 on the dark canvas — effectively
643
+ invisible. A light ring (gray-200) gives 5.5–10:1 on dark surfaces. */
644
+ --ring-noeffect: var(--gray-200);
641
645
 
642
646
  --gradient-noeffect-start: var(--gray-700);
643
647
  --gradient-noeffect-end: var(--gray-800);
@@ -1051,7 +1055,9 @@
1051
1055
  --stroke-noeffect-hover: oklch(from var(--gray-500) calc(l * 0.75) c h);
1052
1056
  --stroke-noeffect-disabled: oklch(from var(--gray-500) calc(l * 1.5) calc(c * 0.8) h);
1053
1057
 
1054
- --ring-noeffect: var(--gray-200);
1058
+ /* a11y: gray-200 (#C0C0C0) is only 1.8:1 on white; gray-500 (#666666) is
1059
+ 5.78:1, clearing the WCAG 1.4.11 / 2.4.7 3:1 non-text contrast minimum. */
1060
+ --ring-noeffect: var(--gray-500);
1055
1061
 
1056
1062
  --gradient-noeffect-start: var(--gray-100);
1057
1063
  --gradient-noeffect-end: var(--gray-200);
@@ -74,7 +74,8 @@
74
74
 
75
75
  @font-face {
76
76
  font-family: "Merriweather";
77
- src: url("/fonts/Merriweather/Merriweather-Italic-VariableFont_opsz,wdth,wght.woff2") format("woff2");
77
+ src: url("/fonts/Merriweather/Merriweather-Italic-VariableFont_opsz,wdth,wght.woff2")
78
+ format("woff2");
78
79
  font-weight: 100 900;
79
80
  font-style: italic;
80
81
  font-display: swap;
@@ -94,4 +95,4 @@
94
95
  font-weight: 100 900;
95
96
  font-style: italic;
96
97
  font-display: swap;
97
- }
98
+ }
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import type React from 'react'
4
- import { createContext, useCallback, useState } from 'react'
4
+ import { createContext, useCallback, useRef, useState } from 'react'
5
5
 
6
6
  import { Dialog } from '@base-ui/react/dialog'
7
7
  import cx from 'classnames'
@@ -55,6 +55,33 @@ function Modal({
55
55
  closeOnOverlayClick,
56
56
  children,
57
57
  }: ModalProps): React.JSX.Element {
58
+ // Overlay dismissal is handled here rather than by Base UI's outside-press
59
+ // detection. `Dialog.Popup` below is the full-viewport flex box that centres
60
+ // the dialog, not the dialog box itself, so a click on the empty space around
61
+ // the dialog still lands *inside* the popup — Base UI sees no outside press
62
+ // and `disablePointerDismissal` never comes into play. Comparing the event
63
+ // target against the popup identifies those clicks: they hit the centring box
64
+ // directly, whereas a click on the dialog hits `Modal.Container` or a
65
+ // descendant of it.
66
+ //
67
+ // The press must both start and end on the popup. Without the pointerdown
68
+ // check, selecting text in the dialog and releasing outside it emits a click
69
+ // on their common ancestor — the popup — and would dismiss the dialog
70
+ // mid-drag.
71
+ const pressStartedOnOverlay = useRef(false)
72
+
73
+ const handleOverlayPointerDown = (event: React.PointerEvent<HTMLDivElement>): void => {
74
+ pressStartedOnOverlay.current = event.target === event.currentTarget
75
+ }
76
+
77
+ const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>): void => {
78
+ const startedOnOverlay = pressStartedOnOverlay.current
79
+ pressStartedOnOverlay.current = false
80
+ if (closeOnOverlayClick !== true) return
81
+ if (!startedOnOverlay || event.target !== event.currentTarget) return
82
+ onDismiss?.()
83
+ }
84
+
58
85
  return (
59
86
  <ModalContext.Provider value={{ onDismiss }}>
60
87
  <Dialog.Root
@@ -69,7 +96,11 @@ function Modal({
69
96
  >
70
97
  <Dialog.Portal>
71
98
  <Dialog.Backdrop className={cx('infonomic-modal-backdrop', styles.backdrop)} />
72
- <Dialog.Popup className={cx('infonomic-modal-wrapper', styles['modal-wrapper'])}>
99
+ <Dialog.Popup
100
+ className={cx('infonomic-modal-wrapper', styles['modal-wrapper'])}
101
+ onPointerDown={handleOverlayPointerDown}
102
+ onClick={handleOverlayClick}
103
+ >
73
104
  {children}
74
105
  </Dialog.Popup>
75
106
  </Dialog.Portal>
@@ -15,6 +15,7 @@ export interface SearchProps extends React.InputHTMLAttributes<HTMLInputElement>
15
15
  inputClassName?: ClassName
16
16
  intent?: Intent
17
17
  className?: ClassName
18
+ ariaLabel?: string
18
19
  ariaLabelForSearch?: string
19
20
  ariaLabelForClear?: string
20
21
  onClear?: () => void
@@ -52,8 +53,9 @@ export function Search({
52
53
  onSearch,
53
54
  validatorFn,
54
55
  placeHolderText = 'Search',
55
- ariaLabelForSearch = 'search',
56
- ariaLabelForClear = 'clear',
56
+ ariaLabel,
57
+ ariaLabelForSearch = 'Search',
58
+ ariaLabelForClear = 'Clear search',
57
59
  ...rest
58
60
  }: SearchProps): React.JSX.Element {
59
61
  const [search, setSearch] = useState<null | string>(null)
@@ -144,18 +146,17 @@ export function Search({
144
146
  id="search"
145
147
  defaultValue={search ?? ''}
146
148
  name="search"
149
+ aria-label={ariaLabel ?? placeHolderText}
147
150
  placeHolder={placeHolderText}
148
151
  disabled={false}
149
152
  error={false}
150
153
  startAdornment={
151
154
  <InputAdornment position="start">
152
155
  <IconButton
153
- role="button"
154
156
  intent="noeffect"
155
157
  variant="text"
156
- style={{ outline: 'none' }}
157
158
  ripple={false}
158
- arial-label={ariaLabelForSearch}
159
+ aria-label={ariaLabelForSearch}
159
160
  size="xs"
160
161
  onClick={() => {
161
162
  handleSearch()
@@ -168,12 +169,10 @@ export function Search({
168
169
  endAdornment={
169
170
  <InputAdornment position="end">
170
171
  <IconButton
171
- role="button"
172
172
  intent="noeffect"
173
173
  variant="text"
174
- style={{ outline: 'none' }}
175
174
  ripple={false}
176
- arial-label={ariaLabelForClear}
175
+ aria-label={ariaLabelForClear}
177
176
  size="xs"
178
177
  onClick={() => {
179
178
  handleClear()