@qoretechnologies/reqraft 0.10.65 → 0.10.66-pr.145.g2d1b4c8

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 (101) hide show
  1. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  2. package/dist/components/form/engine/CompactRow.js +128 -24
  3. package/dist/components/form/engine/CompactRow.js.map +1 -1
  4. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  5. package/dist/components/form/engine/CompactToolbar.js +78 -15
  6. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  7. package/dist/components/form/engine/FormEngine.d.ts +48 -1
  8. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  9. package/dist/components/form/engine/FormEngine.js +243 -85
  10. package/dist/components/form/engine/FormEngine.js.map +1 -1
  11. package/dist/components/form/engine/OptionFieldMessages.d.ts +18 -2
  12. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  13. package/dist/components/form/engine/OptionFieldMessages.js +22 -3
  14. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  15. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  16. package/dist/components/form/engine/_structuredData/SchemaDataView.js +27 -1
  17. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  18. package/dist/components/form/engine/compactRowContext.d.ts +5 -0
  19. package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
  20. package/dist/components/form/engine/compactRowContext.js.map +1 -1
  21. package/dist/components/form/engine/compactRowStyles.d.ts +12 -0
  22. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  23. package/dist/components/form/engine/compactRowStyles.js +72 -39
  24. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  25. package/dist/components/form/engine/compactToolbarContext.d.ts +3 -0
  26. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  27. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  28. package/dist/components/form/engine/formAppearance.d.ts +48 -0
  29. package/dist/components/form/engine/formAppearance.d.ts.map +1 -0
  30. package/dist/components/form/engine/formAppearance.js +50 -0
  31. package/dist/components/form/engine/formAppearance.js.map +1 -0
  32. package/dist/components/form/engine/readFirst.d.ts +14 -2
  33. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  34. package/dist/components/form/engine/readFirst.js +6 -2
  35. package/dist/components/form/engine/readFirst.js.map +1 -1
  36. package/dist/components/form/engine/rowFocus.d.ts +9 -1
  37. package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
  38. package/dist/components/form/engine/rowFocus.js +17 -5
  39. package/dist/components/form/engine/rowFocus.js.map +1 -1
  40. package/dist/components/form/fields/ChoiceGroupHeading.d.ts +21 -0
  41. package/dist/components/form/fields/ChoiceGroupHeading.d.ts.map +1 -0
  42. package/dist/components/form/fields/ChoiceGroupHeading.js +33 -0
  43. package/dist/components/form/fields/ChoiceGroupHeading.js.map +1 -0
  44. package/dist/components/form/fields/Field.d.ts.map +1 -1
  45. package/dist/components/form/fields/Field.js +5 -3
  46. package/dist/components/form/fields/Field.js.map +1 -1
  47. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  48. package/dist/components/form/fields/allowed-values/AllowedValues.js +4 -2
  49. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  50. package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
  51. package/dist/components/form/fields/radio-group/RadioGroup.js +13 -7
  52. package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
  53. package/dist/components/form/index.d.ts +1 -0
  54. package/dist/components/form/index.d.ts.map +1 -1
  55. package/dist/components/form/index.js +1 -0
  56. package/dist/components/form/index.js.map +1 -1
  57. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +3 -2
  58. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +1 -1
  59. package/dist/components/supportTicket/InterfaceReferenceTags.js.map +1 -1
  60. package/dist/components/supportTicket/ReferencePicker.d.ts +2 -1
  61. package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -1
  62. package/dist/components/supportTicket/ReferencePicker.js +19 -13
  63. package/dist/components/supportTicket/ReferencePicker.js.map +1 -1
  64. package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
  65. package/dist/components/supportTicket/TicketHeader.js +4 -1
  66. package/dist/components/supportTicket/TicketHeader.js.map +1 -1
  67. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
  68. package/dist/components/supportTicket/TicketReplyBox.js +29 -10
  69. package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
  70. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  71. package/dist/components/ticketThread/TicketThread.js +8 -16
  72. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  73. package/package.json +1 -1
  74. package/src/components/form/engine/CompactRow.tsx +141 -22
  75. package/src/components/form/engine/CompactToolbar.tsx +85 -16
  76. package/src/components/form/engine/FormEngine.stories.tsx +25 -13
  77. package/src/components/form/engine/FormEngine.tsx +358 -77
  78. package/src/components/form/engine/FormEngineAppearance.stories.tsx +540 -0
  79. package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +5 -1
  80. package/src/components/form/engine/OptionFieldMessages.tsx +73 -2
  81. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +17 -4
  82. package/src/components/form/engine/compactRowContext.ts +5 -0
  83. package/src/components/form/engine/compactRowStyles.ts +218 -38
  84. package/src/components/form/engine/compactToolbarContext.ts +3 -0
  85. package/src/components/form/engine/formAppearance.ts +78 -0
  86. package/src/components/form/engine/readFirst.ts +16 -3
  87. package/src/components/form/engine/rowFocus.ts +17 -6
  88. package/src/components/form/fields/ChoiceGroupHeading.tsx +38 -0
  89. package/src/components/form/fields/Field.tsx +6 -5
  90. package/src/components/form/fields/allowed-values/AllowedValues.tsx +7 -4
  91. package/src/components/form/fields/radio-group/RadioGroup.tsx +13 -7
  92. package/src/components/form/index.tsx +1 -0
  93. package/src/components/supportTicket/InterfaceReferenceTags.tsx +3 -2
  94. package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -4
  95. package/src/components/supportTicket/ReferencePicker.tsx +22 -12
  96. package/src/components/supportTicket/TicketConversation.stories.tsx +0 -1
  97. package/src/components/supportTicket/TicketHeader.tsx +2 -1
  98. package/src/components/supportTicket/TicketReplyBox.stories.tsx +24 -21
  99. package/src/components/supportTicket/TicketReplyBox.tsx +27 -5
  100. package/src/components/ticketThread/TicketThread.stories.tsx +31 -2
  101. package/src/components/ticketThread/TicketThread.tsx +13 -28
@@ -8,7 +8,7 @@
8
8
  // - `ConnectionManagement` per-item actions — dropped (actions omitted).
9
9
  // - `Select` → reqraft `SelectFormField` (`items` instead of `defaultItems`,
10
10
  // single-arg `onChange`; `type`/`closeCollectionOnUnMount` not forwarded).
11
- import { ReqoreCheckbox, ReqoreControlGroup, ReqoreSpan } from '@qoretechnologies/reqore';
11
+ import { ReqoreCheckbox, ReqoreControlGroup } from '@qoretechnologies/reqore';
12
12
  import { IReqoreButtonProps } from '@qoretechnologies/reqore/dist/components/Button';
13
13
  import { IQorusAllowedValue, TQorusType } from '@qoretechnologies/ts-toolkit';
14
14
  import { size as count, isEqual } from 'lodash';
@@ -20,6 +20,7 @@ import {
20
20
  } from '../select/SelectCollection';
21
21
  import { ISelectFormFieldItem, SelectFormField as Select } from '../select/Select';
22
22
  import { useAllowedValueAvailability } from '../../engine/OptionFieldMessages';
23
+ import { ChoiceGroupHeading, useSelectedChoiceIntent } from '../ChoiceGroupHeading';
23
24
  import type { IReqraftAllowedValue } from '../Field';
24
25
 
25
26
  export interface IFieldAllowedValuesProps extends Pick<
@@ -97,13 +98,15 @@ export const FieldAllowedValuesCheckGroup = memo(
97
98
  */
98
99
  label?: string;
99
100
  }) => {
101
+ const selectedChoiceIntent = useSelectedChoiceIntent();
102
+
100
103
  return (
101
104
  <ReqoreControlGroup vertical size={rest.size || 'small'} gapSize='tiny'>
102
- <ReqoreSpan effect={{ opacity: 0.6, uppercase: true, weight: 'bold' }} size='tiny'>
105
+ <ChoiceGroupHeading>
103
106
  {label ? `${label}:`
104
107
  : multiSelect ? 'Select one or more:'
105
108
  : 'Select one:'}
106
- </ReqoreSpan>
109
+ </ChoiceGroupHeading>
107
110
  {items?.map((item) => {
108
111
  const unavailable = getSelectItemUnavailability(item);
109
112
 
@@ -134,7 +137,7 @@ export const FieldAllowedValuesCheckGroup = memo(
134
137
  which keeps the pointer events the tooltip is read through.
135
138
  It does not withhold `onClick` — the handler below does. */
136
139
  readOnly={rest.readOnly || !!unavailable}
137
- intent={item.value === value ? 'info' : undefined}
140
+ intent={selectedChoiceIntent(item.value === value)}
138
141
  // `isEqual`, and only once there is a value to match. The single
139
142
  // -select branch compared JSON.stringify(value) with
140
143
  // JSON.stringify(item.value), and JSON.stringify(undefined) is
@@ -1,6 +1,7 @@
1
1
  import { ReqoreRadioGroup } from '@qoretechnologies/reqore';
2
2
  import { IReqoreRadioGroupProps } from '@qoretechnologies/reqore/dist/components/RadioGroup';
3
3
  import { useMemo } from 'react';
4
+ import { useIsCalmForm } from '../../engine/formAppearance';
4
5
 
5
6
  export interface IRadioGroupFormFieldProps
6
7
  extends Omit<IReqoreRadioGroupProps, 'onChange' | 'onSelectClick' | 'selected'> {
@@ -16,20 +17,25 @@ export const RadioGroupFormField = ({
16
17
  value,
17
18
  ...rest
18
19
  }: IRadioGroupFormFieldProps) => {
20
+ const calm = useIsCalmForm();
19
21
  const items: IReqoreRadioGroupProps['items'] = useMemo(
20
22
  () =>
21
23
  _items.map((item) => ({
22
24
  margin: 'right',
23
- labelEffect: {
24
- spaced: 1,
25
- weight: 'bold',
26
- uppercase: true,
27
- textSize: 'small',
28
- },
25
+ // Calm: an option's name as written — a choice is read, not shouted.
26
+ labelEffect:
27
+ calm ?
28
+ { textSize: 'small' }
29
+ : {
30
+ spaced: 1,
31
+ weight: 'bold',
32
+ uppercase: true,
33
+ textSize: 'small',
34
+ },
29
35
  ...item,
30
36
  disabled: disabled || item.disabled,
31
37
  })),
32
- [_items]
38
+ [_items, calm]
33
39
  );
34
40
 
35
41
  return (
@@ -24,6 +24,7 @@ export * from './fields/string/String';
24
24
  export * from './fields/template/ExampleValueModal';
25
25
  export * from './fields/template/TemplateField';
26
26
  export * from './engine/FormEngine';
27
+ export * from './engine/formAppearance';
27
28
  export * from './engine/FormFieldsSkeleton';
28
29
  export * from './engine/optionActions';
29
30
  export * from './engine/rendererTypes';
@@ -31,8 +31,9 @@ export interface IInterfaceReferenceTagsProps {
31
31
  */
32
32
  onRemove?: (reference: IInterfaceReference) => void;
33
33
  size?: TSizes;
34
- /** Chip styling — pass `intent`/`customTheme` (e.g. `{ main: 'custom1' }`) to tint
35
- * the reference chips to a surface's accent (the Qonsole/helpdesk composer does). */
34
+ /** Chip styling. Left out, a reference is a quiet tag with its kind's icon, as the
35
+ * support surfaces draw it; tint it only where the colour has a job (Qonsole's purple
36
+ * is Qonsole's alone — instruction-files decision 002). */
36
37
  intent?: ComponentProps<typeof ReqoreTag>['intent'];
37
38
  customTheme?: ComponentProps<typeof ReqoreTag>['customTheme'];
38
39
  /** Resolve a per-reference image — e.g. a qog's trigger-app logo, the way the
@@ -29,8 +29,6 @@ const meta = {
29
29
  export default meta;
30
30
  type Story = StoryObj<typeof meta>;
31
31
 
32
- const ACCENT = { main: 'custom1' } as const;
33
-
34
32
  /* The kind ids a host passes — wire values, not labels. `qog` is the customer-facing
35
33
  * kind for what the instance still lists as `fsm`; hosts map that at the fetch
36
34
  * boundary, so `fsm` never reaches the picker. */
@@ -143,7 +141,6 @@ const Hosted = ({
143
141
  their own remove — and read a size up from the thread's, where they're
144
142
  a record rather than a control. */
145
143
  <InterfaceReferenceTags
146
- customTheme={ACCENT}
147
144
  references={picked}
148
145
  onRemove={(reference) =>
149
146
  setPicked((current) =>
@@ -167,7 +164,6 @@ const Hosted = ({
167
164
  flat
168
165
  badge={picked.length || undefined}
169
166
  active={open}
170
- customTheme={ACCENT}
171
167
  tooltip={open ? 'Hide references' : 'Reference interfaces'}
172
168
  onClick={() => setOpen((shown) => !shown)}
173
169
  />
@@ -4,8 +4,11 @@ import {
4
4
  ReqoreMenuItem,
5
5
  ReqoreModal,
6
6
  ReqoreSpinner,
7
+ useReqoreTheme,
7
8
  } from '@qoretechnologies/reqore';
8
9
  import { TReqoreBadge } from '@qoretechnologies/reqore/dist/components/Button';
10
+ import { getReadableColor } from '@qoretechnologies/reqore/dist/helpers/colors';
11
+ import { rgba } from 'polished';
9
12
  import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
10
13
  import styled from 'styled-components';
11
14
  import { thinScrollbar } from '../../helpers/scrollbar';
@@ -128,21 +131,22 @@ const Panes = styled.div`
128
131
  * instead keeps a focus cue without the box. The selector is scoped under this wrapper,
129
132
  * so it outranks Reqore's without `!important`.
130
133
  *
131
- * `$active` (the field has a query) holds the underline at full accent, so a filtered
132
- * list is legible as filtered at a glance — the same signal `intent='info'` carries on
133
- * a plain filter control, spoken in this surface's own accent.
134
+ * The line is the theme's neutral hairline, a step brighter on focus. Only `$active` (the
135
+ * field has a query) colours it, in `$accent` — blue by default, the same signal
136
+ * `intent='info'` carries on a plain filter control: blue says a filter is narrowing the
137
+ * list, and nothing else (instruction-files decision 001). It is never the product's
138
+ * purple, which a support surface does not wear (decision 002).
134
139
  */
135
- const Search = styled.div<{ $accent: string; $active?: boolean }>`
140
+ const Search = styled.div<{ $accent: string; $line: string; $focus: string; $active?: boolean }>`
136
141
  padding: 6px 4px 4px;
137
- border-bottom: 1px solid
138
- ${({ $accent, $active }) => ($active ? $accent : `${$accent}80`)};
142
+ border-bottom: 1px solid ${({ $accent, $line, $active }) => ($active ? $accent : $line)};
139
143
  transition: border-color 0.15s ease-out;
140
144
 
141
145
  .reqore-control-wrapper:focus-within {
142
146
  outline: none;
143
147
  }
144
148
  &:focus-within {
145
- border-bottom-color: ${({ $accent }) => $accent};
149
+ border-bottom-color: ${({ $accent, $focus, $active }) => ($active ? $accent : $focus)};
146
150
  }
147
151
  `;
148
152
 
@@ -152,8 +156,6 @@ const Note = styled.div`
152
156
  font-size: 12px;
153
157
  `;
154
158
 
155
- const DEFAULT_ACCENT = '#762f7e';
156
-
157
159
  /*
158
160
  * How a picked row reads: `info`, tinted rather than filled.
159
161
  *
@@ -241,7 +243,8 @@ export interface IReferencePickerProps {
241
243
  * vocabulary.
242
244
  */
243
245
  resolveKindLabel?: (kind: string) => string;
244
- /** the underline / selected-kind colour. Defaults to the support surfaces' violet. */
246
+ /** the search underline's colour while the list is filtered. Defaults to the theme's
247
+ * `info` blue — the colour that says a filter is narrowing a list. */
245
248
  accent?: string;
246
249
  /** shown above the panes — the composer's picked-reference chips, typically */
247
250
  header?: ReactNode;
@@ -303,13 +306,15 @@ export const ReferencePicker = ({
303
306
  disabled,
304
307
  resolveInterfaceIcon = defaultInterfaceIcon,
305
308
  resolveKindLabel = defaultInterfaceKindLabel,
306
- accent = DEFAULT_ACCENT,
309
+ accent,
307
310
  header,
308
311
  onClose,
309
312
  title = 'Reference interfaces',
310
313
  layout = 'auto',
311
314
  presentation = 'auto',
312
315
  }: IReferencePickerProps) => {
316
+ const theme = useReqoreTheme();
317
+ const readable = getReadableColor(theme);
313
318
  const [query, setQuery] = useState<string>('');
314
319
  const needle = query.trim().toLowerCase();
315
320
 
@@ -510,7 +515,12 @@ export const ReferencePicker = ({
510
515
  const body = (
511
516
  <Frame ref={frameRef} $bare={asModal}>
512
517
  {header}
513
- <Search $accent={accent} $active={!!needle}>
518
+ <Search
519
+ $accent={accent ?? theme.intents?.info ?? readable}
520
+ $line={rgba(readable, 0.2)}
521
+ $focus={rgba(readable, 0.5)}
522
+ $active={!!needle}
523
+ >
514
524
  <ReqoreInput
515
525
  value={query}
516
526
  onChange={(event) => setQuery((event.target as HTMLInputElement).value)}
@@ -53,7 +53,6 @@ const themed = (node: ReactNode) => (
53
53
 
54
54
  const refs = (
55
55
  <InterfaceReferenceTags
56
- customTheme={{ main: 'custom1' }}
57
56
  references={[{ interface_kind: 'connection', interface_name: 'sftp-partner' }]}
58
57
  />
59
58
  );
@@ -201,7 +201,8 @@ export const TicketHeader = ({
201
201
  and the strip would no longer line up with them. */
202
202
  rowPadding='tiny'
203
203
  labelWidth={LABEL_WIDTH}
204
- uppercaseLabels
204
+ /* The labels' case is the theme's (`theme.uppercaseLabels`): an app that sets its
205
+ labels in sentence case gets "Opened by", not "OPENED BY". */
205
206
  flat
206
207
  transparent
207
208
  padded={false}
@@ -16,6 +16,19 @@ const meta = {
16
16
  export default meta;
17
17
  type Story = StoryObj<typeof meta>;
18
18
 
19
+ /**
20
+ * The reply is sent with the white primary, never the Qonsole purple: a ticket reply is not a
21
+ * hand-off to Qonsole (instruction-files decision 002). Read from the drawn button, so a theme
22
+ * or a default that brings the purple back fails here.
23
+ */
24
+ const expectWhiteSend = async (canvasElement: HTMLElement) => {
25
+ const send = within(canvasElement).getByText('Send reply').closest('button')!;
26
+ await waitFor(() => {
27
+ const [r, g, b] = getComputedStyle(send).backgroundColor.match(/\d+/g)!.map(Number);
28
+ expect(Math.min(r, g, b)).toBeGreaterThan(200);
29
+ });
30
+ };
31
+
19
32
  /**
20
33
  * The customer composer, plain-editor variant (used here for a deterministic
21
34
  * type→send test): an attach button (no extra menu → a plain button, not a
@@ -27,7 +40,7 @@ export const Default: Story = {
27
40
  docs: {
28
41
  description: {
29
42
  story:
30
- "Renders the customer composer's plain-editor variant — an attach button and Send, no internal-note toggle; typing a reply enables Send, which calls onSend with the body and internal: false.",
43
+ "Renders the customer composer's plain-editor variant — an attach button and Send, no internal-note toggle; typing a reply enables Send — the white primary, not the Qonsole purple — which calls onSend with the body and internal: false.",
31
44
  },
32
45
  },
33
46
  },
@@ -39,6 +52,7 @@ export const Default: Story = {
39
52
  await expect(canvas.queryByText('Internal note')).not.toBeInTheDocument();
40
53
 
41
54
  await userEvent.type(textarea!, 'Here is my reply');
55
+ await expectWhiteSend(canvasElement);
42
56
  await userEvent.click(canvas.getByText('Send reply'));
43
57
  await waitFor(() =>
44
58
  expect(args.onSend).toHaveBeenCalledWith(
@@ -59,7 +73,7 @@ export const StaffWithInternalNote: Story = {
59
73
  docs: {
60
74
  description: {
61
75
  story:
62
- "Renders the staff composer with a split send button — the primary action posts a public reply, while the caret opens 'Add internal note', which sends with internal: true.",
76
+ "Renders the staff composer with a split send button — the primary action (white, not the Qonsole purple) posts a public reply, while the caret opens 'Add internal note', which sends with internal: true.",
63
77
  },
64
78
  },
65
79
  },
@@ -72,6 +86,7 @@ export const StaffWithInternalNote: Story = {
72
86
 
73
87
  const textarea = canvasElement.querySelector('textarea');
74
88
  await userEvent.type(textarea!, 'internal context');
89
+ await expectWhiteSend(canvasElement);
75
90
 
76
91
  // "Add internal note" lives behind the split-send caret
77
92
  const caret = canvasElement.querySelector('.ticket-send-more');
@@ -122,8 +137,7 @@ export const RichHelpdeskReply: Story = {
122
137
  },
123
138
  },
124
139
  },
125
- // Rendered under the Qorus theme (as in qorus-ide / admin-portal) so the
126
- // `custom1` accent shows as the Qorus purple — a bare reqore theme greys it out.
140
+ // Rendered under the Qorus theme, as in qorus-ide / admin-portal.
127
141
  decorators: [
128
142
  (Story) => (
129
143
  <ReqoreUIProvider theme={{ main: '#121212', intents: { custom1: '#762f7e' } }}>
@@ -141,23 +155,15 @@ export const RichHelpdeskReply: Story = {
141
155
  { label: 'Take screenshot', icon: 'CameraAiLine', onClick: fn() },
142
156
  { label: 'Capture area', icon: 'CropLine', onClick: fn() },
143
157
  ],
144
- // the reference toggle, styled to the same custom1 accent as the send
158
+ // the reference toggle: a quiet icon button, like the attach button beside it
145
159
  footerActions: (
146
- <ReqoreButton
147
- icon='LinksLine'
148
- minimal
149
- flat
150
- badge={1}
151
- customTheme={{ main: 'custom1' }}
152
- tooltip='Reference interfaces'
153
- />
160
+ <ReqoreButton icon='LinksLine' minimal flat badge={1} tooltip='Reference interfaces' />
154
161
  ),
155
- // references on top of the editor (inside the bar), tinted to the accent — no
156
- // "References" label, the chips read as the referenced interfaces on their own
162
+ // references on top of the editor (inside the bar), as quiet tags with the kind's
163
+ // icon — no "References" label, the chips read as the referenced interfaces on their own
157
164
  aboveInput: (
158
165
  <InterfaceReferenceTags
159
166
  references={[{ interface_kind: 'connection', interface_name: 'sftp-partner' }]}
160
- customTheme={{ main: 'custom1' }}
161
167
  />
162
168
  ),
163
169
  },
@@ -176,7 +182,7 @@ export const RichHelpdeskReply: Story = {
176
182
 
177
183
  /**
178
184
  * Every referenceable interface kind on one message, so the per-kind icons +
179
- * accent chip styling can be reviewed together: workflow, service, job, fsm,
185
+ * chip styling can be reviewed together: workflow, service, job, fsm,
180
186
  * connection, mapper, class, value-map, qog, ai-collection, ai-endpoint,
181
187
  * ai-guardrail. (`qog` has no built-in icon → the generic fallback.)
182
188
  */
@@ -185,7 +191,7 @@ export const AllReferenceTypes: Story = {
185
191
  docs: {
186
192
  description: {
187
193
  story:
188
- 'Renders a reply carrying every referenceable interface kind at once, so the per-kind icons and accent chip styling can be reviewed together (qog falls back to the generic glyph).',
194
+ 'Renders a reply carrying every referenceable interface kind at once, so the per-kind icons and chip styling can be reviewed together (qog falls back to the generic glyph).',
189
195
  },
190
196
  },
191
197
  },
@@ -202,7 +208,6 @@ export const AllReferenceTypes: Story = {
202
208
  defaultText: 'This one references every interface kind.',
203
209
  aboveInput: (
204
210
  <InterfaceReferenceTags
205
- customTheme={{ main: 'custom1' }}
206
211
  references={[
207
212
  { interface_kind: 'workflow', interface_name: 'order-sync:1.2' },
208
213
  { interface_kind: 'service', interface_name: 'notifier' },
@@ -267,7 +272,6 @@ export const QogTriggerAppLogo: Story = {
267
272
  defaultText: 'These qogs are referenced by their trigger apps.',
268
273
  aboveInput: (
269
274
  <InterfaceReferenceTags
270
- customTheme={{ main: 'custom1' }}
271
275
  references={[
272
276
  // resolved live from trigger_app (the customer/IDE path)
273
277
  { interface_kind: 'qog', interface_name: 'telegram-intake', trigger_app: 'Telegram' },
@@ -337,7 +341,6 @@ export const WithScreenshotAndReferences: Story = {
337
341
  defaultFiles: [screenshotFile()],
338
342
  aboveInput: (
339
343
  <InterfaceReferenceTags
340
- customTheme={{ main: 'custom1' }}
341
344
  references={[
342
345
  { interface_kind: 'connection', interface_name: 'sftp-partner' },
343
346
  { interface_kind: 'qog', interface_name: 'telegram-intake', logo: appLogo('#2AABEE', 'T') },
@@ -1,9 +1,29 @@
1
1
  import type { TReqoreDropdownItems } from '@qoretechnologies/reqore/dist/components/Dropdown/list';
2
2
  import { ReactNode } from 'react';
3
- import { Composer, IComposerSendAction, TComposerEditor } from '../composer/Composer';
3
+ import {
4
+ Composer,
5
+ IComposerProps,
6
+ IComposerSendAction,
7
+ TComposerEditor,
8
+ } from '../composer/Composer';
4
9
  import { fileToAttachment as defaultFileToAttachment } from './fileToAttachment';
5
10
  import { IAttachmentUpload } from './meta';
6
11
 
12
+ /**
13
+ * A ticket reply is not a hand-off to Qonsole, so it does not wear Qonsole's look: no purple
14
+ * bar, no purple send (instruction-files decision 002 — a purple button means "this hands the
15
+ * work to Qonsole / AI" and nothing else). The bar keeps the composer's shape with the theme's
16
+ * own neutral edge (its `main`, a step lighter — how Reqore draws its dividers), and sending
17
+ * the reply is the surface's one primary action: white.
18
+ */
19
+ const REPLY_BOX_EFFECT: IComposerProps['effect'] = {
20
+ gradient: { direction: 'to bottom right', colors: 'main:lighten:3' },
21
+ };
22
+ const REPLY_SEND_STYLE: Pick<IComposerSendAction, 'intent' | 'minimal'> = {
23
+ intent: 'white',
24
+ minimal: false,
25
+ };
26
+
7
27
  /**
8
28
  * The `items` shape ReqoreDropdown accepts. Named, not derived: `ReqoreDropdown`
9
29
  * is a generic component, and `ComponentProps` of a generic resolves to `{}`
@@ -96,16 +116,15 @@ export const TicketReplyBox = ({
96
116
  : 'Send reply';
97
117
 
98
118
  // Staff surface splits the send button; customer surface is a single "Send reply".
99
- // Both use the Qonsole-style minimal `custom1` accent (the Composer default); the
100
- // internal note stays amber (`warning`) so it can't be mistaken for a public reply.
119
+ // Both send the reply as the white primary (`REPLY_SEND_STYLE`); the internal note
120
+ // stays amber (`warning`) so it can't be mistaken for a public reply.
101
121
  const sendActions: IComposerSendAction[] | undefined = allowInternalNote
102
122
  ? [
103
123
  {
104
124
  id: 'reply',
105
125
  label: labelFor(false),
106
126
  icon: 'SendPlane2Line',
107
- minimal: true,
108
- customTheme: { main: 'custom1' },
127
+ ...REPLY_SEND_STYLE,
109
128
  },
110
129
  {
111
130
  id: 'internal',
@@ -139,7 +158,10 @@ export const TicketReplyBox = ({
139
158
  disabledReason={disabledReason}
140
159
  onSend={handleSend}
141
160
  sendLabel={labelFor(false)}
161
+ sendIntent={REPLY_SEND_STYLE.intent}
162
+ sendMinimal={REPLY_SEND_STYLE.minimal}
142
163
  sendActions={sendActions}
164
+ effect={REPLY_BOX_EFFECT}
143
165
  sendMenuClassName='ticket-send-more'
144
166
  attachMenuItems={attachMenuItems}
145
167
  footerActions={footerActions}
@@ -159,6 +159,30 @@ const meta = {
159
159
  export default meta;
160
160
  type Story = StoryObj<typeof meta>;
161
161
 
162
+ /**
163
+ * How coloured a message's card is: the saturation (0–1) of its border, the one place
164
+ * ReqoreBubble draws its tint. A neutral card is near 0; amber or purple is well above.
165
+ */
166
+ const cardSaturation = (messageId: string): number => {
167
+ const bubble = document.getElementById(`ticket-message-${messageId}`);
168
+ if (!bubble) throw new Error(`message ${messageId} not rendered`);
169
+ const bordered = [bubble, ...Array.from(bubble.querySelectorAll<HTMLElement>('*'))].find((el) => {
170
+ const style = getComputedStyle(el);
171
+ return parseFloat(style.borderTopWidth) > 0 && style.borderTopStyle !== 'none';
172
+ });
173
+ if (!bordered) throw new Error(`message ${messageId} has no card border`);
174
+ const [r, g, b] = getComputedStyle(bordered).borderTopColor.match(/[\d.]+/g)!.map(Number);
175
+ const max = Math.max(r, g, b);
176
+ return max === 0 ? 0 : (max - Math.min(r, g, b)) / max;
177
+ };
178
+
179
+ /** Neither side of a conversation is tinted (no purple, no blue); only an internal note is. */
180
+ const expectNeutralCards = async (ids: string[]) => {
181
+ for (const id of ids) {
182
+ await expect(cardSaturation(id)).toBeLessThan(0.2);
183
+ }
184
+ };
185
+
162
186
  /**
163
187
  * The customer viewpoint: the customer's own messages sit on the right under
164
188
  * "You", staff appear under a masked "Qorus Support" label, and internal notes
@@ -170,7 +194,7 @@ export const CustomerView: Story = {
170
194
  docs: {
171
195
  description: {
172
196
  story:
173
- "Renders the thread from the customer's viewpoint: the customer's own messages sit on the right under 'You', staff appear under the masked 'Qorus Support' label, internal notes are filtered out, and clicking an attachment chip calls onDownloadAttachment with its id and filename.",
197
+ "Renders the thread from the customer's viewpoint: the customer's own messages sit on the right under 'You', staff appear under the masked 'Qorus Support' label, both as neutral cards (the side and the label say who spoke), internal notes are filtered out, and clicking an attachment chip calls onDownloadAttachment with its id and filename.",
174
198
  },
175
199
  },
176
200
  },
@@ -186,6 +210,8 @@ export const CustomerView: Story = {
186
210
  await expect(canvas.getByText('Qorus Support')).toBeInTheDocument();
187
211
  // internal notes are filtered out for the customer
188
212
  await expect(canvas.queryByText('Internal note')).not.toBeInTheDocument();
213
+ // the side, the avatar and the label say who spoke; no card is tinted for it
214
+ await expectNeutralCards(['m1', 'm2']);
189
215
 
190
216
  await userEvent.click(canvas.getAllByText('deploy-trace.log')[0]);
191
217
  await waitFor(() =>
@@ -306,7 +332,7 @@ export const StaffView: Story = {
306
332
  docs: {
307
333
  description: {
308
334
  story:
309
- "Renders the thread from the staff viewpoint: staff messages sit on the right, the customer on the left, and staff-only internal notes render amber and bordered under an 'Internal note' label.",
335
+ "Renders the thread from the staff viewpoint: staff messages sit on the right, the customer on the left, both as neutral cards, and staff-only internal notes render amber and bordered under an 'Internal note' label.",
310
336
  },
311
337
  },
312
338
  },
@@ -320,6 +346,9 @@ export const StaffView: Story = {
320
346
  await expect(canvas.getByText('Internal note')).toBeInTheDocument();
321
347
  await expect(canvas.getByText(/Support · nick/)).toBeInTheDocument();
322
348
  await expect(canvas.getByText(/Customer · acme/)).toBeInTheDocument();
349
+ // both sides are neutral cards; the internal note alone is amber
350
+ await expectNeutralCards(['m1', 'm2']);
351
+ await expect(cardSaturation('m3')).toBeGreaterThan(0.3);
323
352
  // the reference chip renders with the built-in default icon (no resolver passed)
324
353
  await expect(canvas.getByText('nightly-deploy:1.2')).toBeInTheDocument();
325
354
  },
@@ -13,7 +13,7 @@ import {
13
13
  import { getReadableColor } from '@qoretechnologies/reqore/dist/helpers/colors';
14
14
  import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
15
15
  import { rgba } from 'polished';
16
- import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
16
+ import { useEffect, useMemo, useRef, useState } from 'react';
17
17
  import ReactMarkdown from 'react-markdown';
18
18
  import styled from 'styled-components';
19
19
  import { formatBytes, isImage } from '../../helpers/common';
@@ -32,12 +32,11 @@ import {
32
32
  * the Qonsole `support-ticket` renderer. Presentational — data in, callbacks out
33
33
  * — so each consumer keeps its own fetch/websocket wiring.
34
34
  *
35
- * Rendered as a comment feed: an avatar beside a bordered, accent-tinted card per
35
+ * Rendered as a comment feed: an avatar beside a bordered, neutral card per
36
36
  * message, each hugging its own side. Built on Reqore's `ReqoreBubble` (which
37
- * supplies the avatar, the bold author label, the alignment and the tint), so the
38
- * accent follows whatever the host app themes `custom1` to rather than a colour
39
- * baked in here. Interface references render via the shared
40
- * `InterfaceReferenceTags`; the status/priority/category vocabularies live in
37
+ * supplies the avatar, the bold author label and the alignment). Interface
38
+ * references render via the shared `InterfaceReferenceTags`, as quiet tags with
39
+ * the kind's icon; the status/priority/category vocabularies live in
41
40
  * `../supportTicket/meta`.
42
41
  *
43
42
  * Runs of consecutive messages are wrapped in a `ReqoreBubbleGroup`, which prints
@@ -114,10 +113,11 @@ const AUTHOR_META: Record<TTicketMessageAuthor, IAuthorMeta> = {
114
113
  system: { icon: 'InformationLine' },
115
114
  };
116
115
 
117
- // Both sides share one accent: the side, the avatar and the author label already
118
- // carry who is speaking, which frees colour to mean "internal note" and nothing
119
- // else. `custom1` is a theme slot, so each host app supplies its own hue.
120
- const ACCENT: ComponentProps<typeof ReqoreBubble>['customTheme'] = { main: 'custom1' };
116
+ // No side is tinted: the side, the avatar and the author label ("You", "Qorus
117
+ // Support") already carry who is speaking, which leaves colour one job here —
118
+ // amber for an internal note. Never the product's purple, which a support thread
119
+ // does not wear (instruction-files decision 002: purple marks where you are, a
120
+ // chart's accent, and Qonsole).
121
121
 
122
122
  /** Avatar glyph; an internal note carries the "not visible to the customer" eye. */
123
123
  const authorIcon = (message: ITicketThreadMessage): IReqoreIconName =>
@@ -666,23 +666,9 @@ export const TicketThread = ({
666
666
  avatar={{ icon: authorIcon(message) }}
667
667
  label={authorLabel(message, viewerRole)}
668
668
  timestamp={message.created ? <ReqoreTimeAgo time={message.created} /> : undefined}
669
- // Own messages read as info-blue, the other party's as the purple
670
- // accent — so the two sides of the conversation are told apart by
671
- // colour as well as by side, and "which of these did I write?" is
672
- // answerable at a glance. Deliberately keyed off `viewerRole`, so it
673
- // stays correct on the staff surface too (staff replies are the blue
674
- // ones there). Internal notes keep their warning intent and are not
675
- // re-tinted — they are neither side of the conversation.
676
- intent={
677
- message.internal
678
- ? 'warning'
679
- : message.author_type === viewerRole
680
- ? 'info'
681
- : undefined
682
- }
683
- customTheme={
684
- message.internal || message.author_type === viewerRole ? undefined : ACCENT
685
- }
669
+ // Both sides are neutral cards; an internal note is amber, so it can
670
+ // never be mistaken for a reply the customer sees.
671
+ intent={message.internal ? 'warning' : undefined}
686
672
  flat={false}
687
673
  minimal
688
674
  >
@@ -698,7 +684,6 @@ export const TicketThread = ({
698
684
  <div style={{ marginTop: 8 }}>
699
685
  <InterfaceReferenceTags
700
686
  size='tiny'
701
- customTheme={ACCENT}
702
687
  references={refsFor(message)}
703
688
  resolveInterfaceIcon={resolveInterfaceIcon}
704
689
  preferKindImage={preferKindImage}