@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.
- package/dist/components/button/combo-button.d.ts +11 -4
- package/dist/components/button/combo-button.d.ts.map +1 -1
- package/dist/components/button/combo-button.js +20 -3
- package/dist/components/button/combo-button.module.css +20 -0
- package/dist/components/button/combo-button.module.js +7 -1
- package/dist/components/button/combo-button_module.css +20 -0
- package/dist/components/dropdown/dropdown.d.ts +9 -1
- package/dist/components/dropdown/dropdown.d.ts.map +1 -1
- package/dist/components/dropdown/dropdown.js +2 -1
- package/dist/components/inputs/checkbox.module.css +8 -0
- package/dist/components/inputs/checkbox_module.css +4 -0
- package/dist/components/inputs/label.d.ts.map +1 -1
- package/dist/components/inputs/label.js +1 -0
- package/dist/components/notifications/alert.module.css +2 -1
- package/dist/components/notifications/alert_module.css +2 -1
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.d.ts.map +1 -1
- package/dist/icons/index.js +1 -0
- package/dist/icons/markdown-icon.d.ts +11 -0
- package/dist/icons/markdown-icon.d.ts.map +1 -0
- package/dist/icons/markdown-icon.js +34 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +1 -0
- package/dist/styles/styles.css +1 -1
- package/dist/widgets/modal/modal.d.ts.map +1 -1
- package/dist/widgets/modal/modal.js +14 -1
- package/dist/widgets/search/search.d.ts +2 -1
- package/dist/widgets/search/search.d.ts.map +1 -1
- package/dist/widgets/search/search.js +4 -11
- package/package.json +1 -1
- package/src/components/button/combo-button.module.css +20 -0
- package/src/components/button/combo-button.tsx +46 -2
- package/src/components/dropdown/dropdown.tsx +10 -0
- package/src/components/inputs/checkbox.module.css +8 -0
- package/src/components/inputs/label.tsx +8 -1
- package/src/components/notifications/alert.module.css +2 -1
- package/src/icons/index.ts +1 -0
- package/src/icons/markdown-icon.tsx +42 -0
- package/src/react.ts +1 -0
- package/src/styles/functional/colors.css +9 -3
- package/src/styles/local-fonts.css +3 -2
- package/src/widgets/modal/modal.tsx +33 -2
- 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:
|
|
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
|
-
{
|
|
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
|
|
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
|
+
*
|
|
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 /
|
|
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;
|
package/src/icons/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
aria-label={ariaLabelForClear}
|
|
177
176
|
size="xs"
|
|
178
177
|
onClick={() => {
|
|
179
178
|
handleClear()
|