@mindlogic-ai/logician-ui 4.0.0-alpha.21 → 4.0.0-alpha.22

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 (77) hide show
  1. package/dist/components/ErrorFallback/ErrorFallback.d.ts.map +1 -1
  2. package/dist/components/ErrorFallback/ErrorFallback.js +13 -2
  3. package/dist/components/ErrorFallback/ErrorFallback.js.map +1 -1
  4. package/dist/components/ErrorFallback/ErrorFallback.mjs +13 -2
  5. package/dist/components/ErrorFallback/ErrorFallback.mjs.map +1 -1
  6. package/dist/components/FileInput/FileInput.d.ts.map +1 -1
  7. package/dist/components/FileInput/FileInput.js +7 -1
  8. package/dist/components/FileInput/FileInput.js.map +1 -1
  9. package/dist/components/FileInput/FileInput.mjs +7 -1
  10. package/dist/components/FileInput/FileInput.mjs.map +1 -1
  11. package/dist/components/Markdown/Markdown.d.ts.map +1 -1
  12. package/dist/components/Markdown/Markdown.js +6 -1
  13. package/dist/components/Markdown/Markdown.js.map +1 -1
  14. package/dist/components/Markdown/Markdown.mjs +6 -1
  15. package/dist/components/Markdown/Markdown.mjs.map +1 -1
  16. package/dist/components/Pagination/Pagination.d.ts.map +1 -1
  17. package/dist/components/Pagination/Pagination.js +6 -1
  18. package/dist/components/Pagination/Pagination.js.map +1 -1
  19. package/dist/components/Pagination/Pagination.mjs +6 -1
  20. package/dist/components/Pagination/Pagination.mjs.map +1 -1
  21. package/dist/components/Radio/RadioControl.d.ts.map +1 -1
  22. package/dist/components/Radio/RadioControl.js +14 -1
  23. package/dist/components/Radio/RadioControl.js.map +1 -1
  24. package/dist/components/Radio/RadioControl.mjs +15 -2
  25. package/dist/components/Radio/RadioControl.mjs.map +1 -1
  26. package/dist/components/Select/ComboboxField.d.ts +1 -1
  27. package/dist/components/Select/ComboboxField.d.ts.map +1 -1
  28. package/dist/components/Select/ComboboxField.js +2 -2
  29. package/dist/components/Select/ComboboxField.js.map +1 -1
  30. package/dist/components/Select/ComboboxField.mjs +2 -2
  31. package/dist/components/Select/ComboboxField.mjs.map +1 -1
  32. package/dist/components/Select/Select.types.d.ts +16 -0
  33. package/dist/components/Select/Select.types.d.ts.map +1 -1
  34. package/dist/components/Select/SelectField.d.ts +1 -1
  35. package/dist/components/Select/SelectField.d.ts.map +1 -1
  36. package/dist/components/Select/SelectField.js +2 -2
  37. package/dist/components/Select/SelectField.js.map +1 -1
  38. package/dist/components/Select/SelectField.mjs +2 -2
  39. package/dist/components/Select/SelectField.mjs.map +1 -1
  40. package/dist/components/Slider/Slider.d.ts.map +1 -1
  41. package/dist/components/Slider/Slider.js +2 -2
  42. package/dist/components/Slider/Slider.js.map +1 -1
  43. package/dist/components/Slider/Slider.mjs +3 -3
  44. package/dist/components/Slider/Slider.mjs.map +1 -1
  45. package/dist/components/Slider/Slider.types.d.ts +15 -0
  46. package/dist/components/Slider/Slider.types.d.ts.map +1 -1
  47. package/dist/components/Typography/Link.d.ts.map +1 -1
  48. package/dist/components/Typography/Link.js +17 -7
  49. package/dist/components/Typography/Link.js.map +1 -1
  50. package/dist/components/Typography/Link.mjs +17 -7
  51. package/dist/components/Typography/Link.mjs.map +1 -1
  52. package/dist/components/Typography/Subtitle.d.ts +17 -0
  53. package/dist/components/Typography/Subtitle.d.ts.map +1 -1
  54. package/dist/components/Typography/Subtitle.js +18 -1
  55. package/dist/components/Typography/Subtitle.js.map +1 -1
  56. package/dist/components/Typography/Subtitle.mjs +18 -1
  57. package/dist/components/Typography/Subtitle.mjs.map +1 -1
  58. package/dist/components/Workflow/canvas/NodePalette/NodePalette.d.ts.map +1 -1
  59. package/dist/components/Workflow/canvas/NodePalette/NodePalette.js +19 -2
  60. package/dist/components/Workflow/canvas/NodePalette/NodePalette.js.map +1 -1
  61. package/dist/components/Workflow/canvas/NodePalette/NodePalette.mjs +19 -2
  62. package/dist/components/Workflow/canvas/NodePalette/NodePalette.mjs.map +1 -1
  63. package/package.json +2 -1
  64. package/src/components/ErrorFallback/ErrorFallback.tsx +30 -3
  65. package/src/components/FileInput/FileInput.tsx +11 -2
  66. package/src/components/Markdown/Markdown.tsx +6 -1
  67. package/src/components/Pagination/Pagination.tsx +5 -0
  68. package/src/components/Radio/RadioControl.tsx +20 -6
  69. package/src/components/Select/ComboboxField.tsx +11 -1
  70. package/src/components/Select/Select.types.ts +16 -0
  71. package/src/components/Select/SelectField.tsx +14 -1
  72. package/src/components/Slider/Slider.tsx +16 -1
  73. package/src/components/Slider/Slider.types.ts +15 -0
  74. package/src/components/Typography/Link.tsx +17 -7
  75. package/src/components/Typography/Subtitle.tsx +17 -1
  76. package/src/components/Workflow/canvas/NodePalette/NodePalette.tsx +12 -2
  77. package/src/components/a11y.kwcag.test.tsx +295 -0
@@ -1,3 +1,4 @@
1
+ import { useId } from 'react';
1
2
  import { Box, Flex, VStack } from '@chakra-ui/react';
2
3
 
3
4
  import { useTranslate } from '@/hooks/useTranslate';
@@ -20,6 +21,8 @@ export function ErrorFallback({
20
21
  }: ErrorFallbackProps) {
21
22
  const translate = useTranslate();
22
23
  const showToast = useToast();
24
+ // Names the landmark below with the same words the card shows as its title.
25
+ const titleId = useId();
23
26
 
24
27
  const errorInfo = {
25
28
  errorId:
@@ -61,7 +64,22 @@ User Agent: ${errorInfo.userAgent}`;
61
64
  };
62
65
 
63
66
  return (
64
- <Flex minH="100vh" align="center" justify="center" p={8}>
67
+ // A landmark, and a named one. This card replaces whatever failed — often
68
+ // the whole page, so the host's `<main>` goes with it — and a screen-reader
69
+ // user then has nothing to navigate by: axe reports every node under
70
+ // `region`, and there is no landmark to jump to (KWCAG 2.1 5.2.4.1 반복
71
+ // 영역 건너뛰기). A `<section>` labelled by the error title is a region
72
+ // landmark wherever it renders, which `<main>` would not be — a segment-level
73
+ // boundary leaves the host's `<main>` in place, and two of those is its own
74
+ // failure.
75
+ <Flex
76
+ as="section"
77
+ aria-labelledby={titleId}
78
+ minH="100vh"
79
+ align="center"
80
+ justify="center"
81
+ p={8}
82
+ >
65
83
  <Container disableResponsive maxW="container.sm">
66
84
  <Card
67
85
  p={12}
@@ -83,7 +101,12 @@ User Agent: ${errorInfo.userAgent}`;
83
101
 
84
102
  {/* Error Title */}
85
103
  <VStack gap={3} align="center" w="full">
86
- <H1 color="fg.default" textStyle="h2" fontWeight="bold">
104
+ <H1
105
+ id={titleId}
106
+ color="fg.default"
107
+ textStyle="h2"
108
+ fontWeight="bold"
109
+ >
87
110
  {translate('error_boundary_title')}
88
111
  </H1>
89
112
  <Subtitle color="fg.muted" textStyle="h5" maxW="md">
@@ -115,7 +138,11 @@ User Agent: ${errorInfo.userAgent}`;
115
138
  p={6}
116
139
  w="full"
117
140
  >
118
- <H4 color="primary.dark" mb={4} textAlign="center">
141
+ {/* `as="h2"`: `H4` is the type size wanted here, but this block
142
+ sits directly under the card's `<h1>` — an `<h4>` there skips
143
+ two levels and tells a screen-reader user there are sections
144
+ they missed (KWCAG 5.2.4.2 제목 제공). */}
145
+ <H4 as="h2" color="primary.dark" mb={4} textAlign="center">
119
146
  {translate('support_information')}
120
147
  </H4>
121
148
  <VStack gap={2} align="stretch" mb={4}>
@@ -27,8 +27,12 @@ export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
27
27
  };
28
28
 
29
29
  return (
30
+ // No `role="button"` on this wrapper. It is not a button — the `<label>`
31
+ // inside is what activates the file input — and claiming the role put an
32
+ // interactive element inside another (axe `nested-interactive`), which is
33
+ // a state screen readers have no sensible way to announce
34
+ // (KWCAG 2.1 5.4.2.1 웹 애플리케이션 접근성 준수).
30
35
  <Flex
31
- role="button"
32
36
  w="100%"
33
37
  h="100%"
34
38
  maxH={32}
@@ -42,8 +46,13 @@ export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
42
46
  borderRadius="md"
43
47
  {...containerStyle}
44
48
  >
49
+ {/* `className="group"`, not `role="group"`. The role was doing nothing for
50
+ assistive tech (a `<label>` may not be a group — axe
51
+ `aria-allowed-role`) and nothing for the hover effect either: Chakra's
52
+ `_groupHover` compiles to `.group:hover &`, so it needs the CLASS.
53
+ The reveal-on-hover below has therefore never fired. */}
45
54
  <label
46
- role="group"
55
+ className="group"
47
56
  style={{ width: '100%', height: '100%', cursor: 'pointer' }}
48
57
  >
49
58
  {bgImage && (
@@ -22,7 +22,12 @@ const H2Wrapper = (props: any) => <H2 {...props} />;
22
22
  const H3Wrapper = (props: any) => <H3 {...props} />;
23
23
  const H4Wrapper = (props: any) => <H4 {...props} />;
24
24
  const H5Wrapper = (props: any) => <H5 {...props} />;
25
- const SubtitleWrapper = (props: any) => <Subtitle {...props} />;
25
+ // `as="h6"` explicitly. `Subtitle` renders a paragraph by default now (its call
26
+ // sites are overwhelmingly helper copy that was never a heading), but this
27
+ // wrapper is markdown's `h6` — a heading the author actually wrote. Inheriting
28
+ // the new default here would silently drop `###### 제목` out of the document
29
+ // outline, which is the same KWCAG 5.2.4.2 (제목 제공) defect inverted.
30
+ const SubtitleWrapper = (props: any) => <Subtitle as="h6" {...props} />;
26
31
  const TheadWrapper = (props: any) => <Thead {...props} />;
27
32
  const TbodyWrapper = (props: any) => <Tbody {...props} />;
28
33
  const TrWrapper = (props: any) => <Tr {...props} />;
@@ -81,6 +81,11 @@ export const Pagination = ({
81
81
  <SelectField<number>
82
82
  size="sm"
83
83
  width="5.5rem"
84
+ // The visible label for this control is the "개씩 보기" text beside
85
+ // it, which the select itself cannot claim — so it carries the
86
+ // same words as its own hidden label rather than shipping
87
+ // nameless (KWCAG 5.3.4.1 레이블 제공).
88
+ ariaLabel={translate('pagination_items_per_page') as string}
84
89
  options={itemsPerPageOptions.map((option) => ({
85
90
  label: option.toString(),
86
91
  value: option,
@@ -1,4 +1,4 @@
1
- import { forwardRef } from 'react';
1
+ import { forwardRef, useId } from 'react';
2
2
  import { RadioGroup, RadioGroupItemControlProps } from '@chakra-ui/react';
3
3
 
4
4
  import { focusRing } from '@/utils/focusRing';
@@ -6,9 +6,23 @@ import { focusRing } from '@/utils/focusRing';
6
6
  export const RadioControl = forwardRef<
7
7
  HTMLDivElement,
8
8
  RadioGroupItemControlProps
9
- >((props, ref) => (
10
- <RadioGroup.ItemControl ref={ref} {...focusRing} {...props}>
11
- <RadioGroup.ItemIndicator />
12
- </RadioGroup.ItemControl>
13
- ));
9
+ >((props, ref) => {
10
+ // Chakra derives the indicator's id from the same `ids.itemControl` it gives
11
+ // the control it sits inside, so every radio on the page shipped TWO elements
12
+ // with one id — a markup error under KWCAG 2.1 5.4.1.1 마크업 오류 방지, which
13
+ // Korean evaluators still count even though WCAG 2.2 dropped SC 4.1.1. It also
14
+ // breaks anything that resolves the control by id, since `getElementById`
15
+ // returns whichever came first.
16
+ //
17
+ // Nothing references the indicator — it is the dot inside the control — so any
18
+ // unique value fixes it, and `useId` is the one that stays stable across
19
+ // server and client render.
20
+ const indicatorId = useId();
21
+
22
+ return (
23
+ <RadioGroup.ItemControl ref={ref} {...focusRing} {...props}>
24
+ <RadioGroup.ItemIndicator id={indicatorId} />
25
+ </RadioGroup.ItemControl>
26
+ );
27
+ });
14
28
  RadioControl.displayName = 'Radio.Control';
@@ -19,6 +19,7 @@ export function ComboboxField<T = string>({
19
19
  onChange,
20
20
  placeholder,
21
21
  label,
22
+ ariaLabel,
22
23
  size = 'md',
23
24
  invalid,
24
25
  disabled,
@@ -60,7 +61,16 @@ export function ComboboxField<T = string>({
60
61
  }}
61
62
  onInputValueChange={(details) => filter(details.inputValue)}
62
63
  >
63
- {label != null && <Combobox.Label>{label}</Combobox.Label>}
64
+ {/* Same as `SelectField`: the label part is the target of the input's
65
+ `aria-labelledby`, so omitting it leaves the control unnamed rather
66
+ than unlabelled (KWCAG 5.3.4.1 레이블 제공). */}
67
+ {label != null ? (
68
+ <Combobox.Label>{label}</Combobox.Label>
69
+ ) : (
70
+ (ariaLabel ?? placeholder) && (
71
+ <Combobox.Label srOnly>{ariaLabel ?? placeholder}</Combobox.Label>
72
+ )
73
+ )}
64
74
  <Combobox.Control>
65
75
  <Combobox.Input placeholder={placeholder} />
66
76
  <Combobox.IndicatorGroup>
@@ -20,6 +20,22 @@ interface FieldBaseProps<T> {
20
20
  placeholder?: string;
21
21
  /** Optional label rendered above the control. */
22
22
  label?: ReactNode;
23
+ /**
24
+ * Accessible name for the trigger when no visible `label` is rendered — a
25
+ * field labelled by a table column header, a `FieldRow`, or the sentence
26
+ * around it.
27
+ *
28
+ * Without one of `label` / `ariaLabel` / `placeholder` the trigger has **no
29
+ * accessible name at all**: the composition points `aria-labelledby` at a
30
+ * label part that is not rendered, the reference dangles, and the control
31
+ * announces as a bare "button" however much text it shows (KWCAG 2.1
32
+ * 5.3.4.1 레이블 제공, axe `button-name`).
33
+ *
34
+ * Rendered as a visually hidden label rather than an `aria-label` so the name
35
+ * is in the server-rendered markup — an attribute applied after hydration
36
+ * leaves a window in which the control is nameless.
37
+ */
38
+ ariaLabel?: string;
23
39
  size?: SelectSize;
24
40
  invalid?: boolean;
25
41
  disabled?: boolean;
@@ -19,6 +19,7 @@ export function SelectField<T = string>({
19
19
  onChange,
20
20
  placeholder,
21
21
  label,
22
+ ariaLabel,
22
23
  size = 'md',
23
24
  invalid,
24
25
  disabled,
@@ -61,7 +62,19 @@ export function SelectField<T = string>({
61
62
  }}
62
63
  >
63
64
  <Select.HiddenSelect />
64
- {label != null && <Select.Label>{label}</Select.Label>}
65
+ {/* The label part is what the trigger's `aria-labelledby` points at, and
66
+ Chakra emits that reference whether or not the part is rendered. Leave
67
+ it out and the reference dangles, so the trigger resolves to no name —
68
+ a "button" to a screen reader, whatever the selected option says
69
+ (KWCAG 5.3.4.1 레이블 제공). So it is always rendered: visibly when the
70
+ field has a label, hidden when the name lives elsewhere on screen. */}
71
+ {label != null ? (
72
+ <Select.Label>{label}</Select.Label>
73
+ ) : (
74
+ (ariaLabel ?? placeholder) && (
75
+ <Select.Label srOnly>{ariaLabel ?? placeholder}</Select.Label>
76
+ )
77
+ )}
65
78
  <Select.Control>
66
79
  <Select.Trigger>
67
80
  <Select.ValueText placeholder={placeholder} />
@@ -15,7 +15,14 @@ import { SliderControl } from './SliderControl';
15
15
  */
16
16
  export const Slider = forwardRef(
17
17
  (
18
- { value, defaultValue, onValueChange, children, ...rest }: SliderProps,
18
+ {
19
+ value,
20
+ defaultValue,
21
+ onValueChange,
22
+ ariaLabel,
23
+ children,
24
+ ...rest
25
+ }: SliderProps,
19
26
  ref?: ForwardedRef<HTMLDivElement>
20
27
  ) => {
21
28
  // Check if children already contains SliderControl
@@ -42,6 +49,14 @@ export const Slider = forwardRef(
42
49
  onValueChange={onValueChange}
43
50
  {...rest}
44
51
  >
52
+ {/* The label part is what each thumb's `aria-labelledby` points at, and
53
+ the reference is emitted whether or not the part is rendered — so a
54
+ slider composed without one is not merely unlabelled, it is unnamed
55
+ (KWCAG 5.3.4.1 레이블 제공). Hidden, because these call sites carry
56
+ their label as an adjacent icon or time display. */}
57
+ {ariaLabel && (
58
+ <ChakraSlider.Label srOnly>{ariaLabel}</ChakraSlider.Label>
59
+ )}
45
60
  {wrappedChildren}
46
61
  </ChakraSlider.Root>
47
62
  );
@@ -11,5 +11,20 @@ export interface SliderProps extends Omit<
11
11
  defaultValue?: number[];
12
12
  /** Callback when slider value changes */
13
13
  onValueChange?: Slider.RootProps['onValueChange'];
14
+ /**
15
+ * Accessible name for the slider, when no visible `Slider.Label` is composed
16
+ * in — a seek bar next to a time display, a volume control beside a mute
17
+ * button.
18
+ *
19
+ * Without it the thumb has **no accessible name**: Zag points each thumb's
20
+ * `aria-labelledby` at a label part, and a composition that does not render
21
+ * one leaves that reference dangling — the control announces as a bare
22
+ * "slider" (KWCAG 2.1 5.3.4.1 레이블 제공, axe `aria-input-field-name`).
23
+ *
24
+ * Rendered as a visually hidden label rather than an `aria-label` array so it
25
+ * works the same whether the slider has one thumb or several, and so the name
26
+ * is in the server-rendered markup.
27
+ */
28
+ ariaLabel?: string;
14
29
  children?: ReactNode;
15
30
  }
@@ -7,15 +7,25 @@ export interface LinkCustomProps extends Omit<LinkProps, 'variant'> {
7
7
 
8
8
  export const Link = forwardRef<HTMLAnchorElement, LinkCustomProps>(
9
9
  ({ color, variant, ...rest }, ref) => {
10
- const defaultColor = 'primary.main';
11
- const errorColor = 'danger.main';
12
- const defaultHoverColor = 'primary.dark';
13
- const errorHoverColor = 'danger.dark';
10
+ // Theme-aware, because `.main` is tuned against a WHITE page. On the dark
11
+ // canvas (#181A20) `primary.main` measures 4.19:1 and `danger.main` 4.26:1
12
+ // — both under the 4.5:1 that KWCAG 2.1 5.1.3.3 (명도 대비) asks of body
13
+ // text, and every link in a consuming app inherits it. The `.dark` end of
14
+ // each ramp is the far side from the PAGE rather than a fixed lightness, so
15
+ // in dark mode it resolves lighter: 6.68:1 and 6.38:1.
16
+ const defaultColor = { base: 'primary.main', _dark: 'primary.dark' };
17
+ const errorColor = { base: 'danger.main', _dark: 'danger.dark' };
18
+ const defaultHoverColor = { base: 'primary.dark', _dark: 'primary.main' };
19
+ const errorHoverColor = { base: 'danger.dark', _dark: 'danger.main' };
14
20
 
21
+ // `color ?? defaultColor`, not a `typeof color === 'string'` narrowing. The
22
+ // old form silently DISCARDED any non-string value, so a call site passing
23
+ // a per-theme `{ base, _dark }` — the documented Chakra way to fix exactly
24
+ // the contrast problem above — got the default back and no error, in the
25
+ // types or at runtime. A prop the type accepts and the component throws
26
+ // away is worse than one it rejects.
15
27
  const linkColor =
16
- variant === 'error'
17
- ? errorColor
18
- : (typeof color === 'string' ? color : undefined) || defaultColor;
28
+ variant === 'error' ? errorColor : (color ?? defaultColor);
19
29
 
20
30
  const hoverColor =
21
31
  variant === 'error' ? errorHoverColor : defaultHoverColor;
@@ -1,6 +1,23 @@
1
1
  import { forwardRef } from 'react';
2
2
  import { Text, TextProps } from '@chakra-ui/react';
3
3
 
4
+ /**
5
+ * Supporting text under a heading — a subtitle by role in the page, not by
6
+ * document structure.
7
+ *
8
+ * Renders a paragraph. It used to render `<h6>`, which made every call site a
9
+ * level-6 heading in the document outline: a sentence of helper copy under an
10
+ * `<h1>` announced as a heading to a screen reader, and a jump from level 1 to
11
+ * level 6 in the heading list (KWCAG 2.1 5.2.4.2 제목 제공 / axe `heading-order`).
12
+ * A component chosen for its type scale should not decide the outline.
13
+ *
14
+ * A subtitle that genuinely IS the next heading level still says so at the call
15
+ * site, which is where that is known:
16
+ *
17
+ * ```tsx
18
+ * <Subtitle as="h2">{sectionTitle}</Subtitle>
19
+ * ```
20
+ */
4
21
  export const Subtitle = forwardRef<HTMLParagraphElement, TextProps>(
5
22
  (props, ref) => {
6
23
  const { fontSize, ...rest } = props;
@@ -8,7 +25,6 @@ export const Subtitle = forwardRef<HTMLParagraphElement, TextProps>(
8
25
  return (
9
26
  <Text
10
27
  ref={ref}
11
- as="h6"
12
28
  textStyle={fontSize ? undefined : 'subtitle'}
13
29
  fontSize={fontSize}
14
30
  color="fg.muted"
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { DragEvent } from 'react';
3
+ import { DragEvent, useId } from 'react';
4
4
  import { Box, HStack, Stack } from '@chakra-ui/react';
5
5
 
6
6
  import { Card } from '@/components/Card';
@@ -95,6 +95,8 @@ function PaletteItem({ def }: { def: NodeTypeDef }) {
95
95
  export function NodePalette({ onClose }: { onClose: () => void }) {
96
96
  const { nodeTypes, readOnly } = useWorkflow();
97
97
  const translate = useWorkflowTranslate();
98
+ // Names the region below with the same words the header shows.
99
+ const titleId = useId();
98
100
  if (readOnly) return null;
99
101
 
100
102
  // Group by category, preserving registration order within each group.
@@ -115,6 +117,14 @@ export function NodePalette({ onClose }: { onClose: () => void }) {
115
117
 
116
118
  return (
117
119
  <FloatingCard
120
+ // A named region, so the palette is something a screen-reader user can
121
+ // navigate TO. Its title used to be a `Subtitle`, which rendered `<h6>`
122
+ // and put a level-6 heading under the host page's `<h1>` (axe
123
+ // `heading-order`); `Subtitle` is a paragraph now, and a labelled region
124
+ // is what this panel actually is — a landmark, not an outline entry
125
+ // (KWCAG 2.1 5.2.4.1 반복 영역 건너뛰기 / 5.2.4.2 제목 제공).
126
+ role="region"
127
+ aria-labelledby={titleId}
118
128
  width={PALETTE_WIDTH}
119
129
  mt={4}
120
130
  mb={4}
@@ -140,7 +150,7 @@ export function NodePalette({ onClose }: { onClose: () => void }) {
140
150
  borderBottom="1px solid"
141
151
  borderColor="slate.200"
142
152
  >
143
- <Subtitle color="slate.1300">
153
+ <Subtitle id={titleId} color="slate.1300">
144
154
  {translate('workflow_palette_title')}
145
155
  </Subtitle>
146
156
  <IconButton