@qoretechnologies/reqraft 0.10.13-pr.74.gccefa54 → 0.10.13

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 (125) hide show
  1. package/.claude/CLAUDE.md +8 -11
  2. package/dist/components/form/engine/FormEngine.d.ts +1 -1
  3. package/dist/helpers/common.d.ts +0 -5
  4. package/dist/helpers/common.d.ts.map +1 -1
  5. package/dist/helpers/common.js +1 -20
  6. package/dist/helpers/common.js.map +1 -1
  7. package/dist/index.d.ts +0 -5
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -7
  10. package/dist/index.js.map +1 -1
  11. package/dist/providers/StorageProvider.d.ts +13 -0
  12. package/dist/providers/StorageProvider.d.ts.map +1 -1
  13. package/dist/providers/StorageProvider.js +29 -5
  14. package/dist/providers/StorageProvider.js.map +1 -1
  15. package/package.json +3 -3
  16. package/src/helpers/common.ts +0 -19
  17. package/src/hooks/useStorage/useStorage.stories.tsx +52 -0
  18. package/src/index.tsx +0 -5
  19. package/src/providers/StorageProvider.tsx +30 -4
  20. package/dist/components/composer/Composer.d.ts +0 -108
  21. package/dist/components/composer/Composer.d.ts.map +0 -1
  22. package/dist/components/composer/Composer.js +0 -238
  23. package/dist/components/composer/Composer.js.map +0 -1
  24. package/dist/components/composer/helpers.d.ts +0 -21
  25. package/dist/components/composer/helpers.d.ts.map +0 -1
  26. package/dist/components/composer/helpers.js +0 -75
  27. package/dist/components/composer/helpers.js.map +0 -1
  28. package/dist/components/composer/index.d.ts +0 -3
  29. package/dist/components/composer/index.d.ts.map +0 -1
  30. package/dist/components/composer/index.js +0 -19
  31. package/dist/components/composer/index.js.map +0 -1
  32. package/dist/components/imageLightbox/ImageLightbox.d.ts +0 -17
  33. package/dist/components/imageLightbox/ImageLightbox.d.ts.map +0 -1
  34. package/dist/components/imageLightbox/ImageLightbox.js +0 -129
  35. package/dist/components/imageLightbox/ImageLightbox.js.map +0 -1
  36. package/dist/components/imageLightbox/index.d.ts +0 -2
  37. package/dist/components/imageLightbox/index.d.ts.map +0 -1
  38. package/dist/components/imageLightbox/index.js +0 -18
  39. package/dist/components/imageLightbox/index.js.map +0 -1
  40. package/dist/components/supportTicket/AttachmentChips.d.ts +0 -10
  41. package/dist/components/supportTicket/AttachmentChips.d.ts.map +0 -1
  42. package/dist/components/supportTicket/AttachmentChips.js +0 -119
  43. package/dist/components/supportTicket/AttachmentChips.js.map +0 -1
  44. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +0 -35
  45. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +0 -1
  46. package/dist/components/supportTicket/InterfaceReferenceTags.js +0 -31
  47. package/dist/components/supportTicket/InterfaceReferenceTags.js.map +0 -1
  48. package/dist/components/supportTicket/ReferencePicker.d.ts +0 -41
  49. package/dist/components/supportTicket/ReferencePicker.d.ts.map +0 -1
  50. package/dist/components/supportTicket/ReferencePicker.js +0 -86
  51. package/dist/components/supportTicket/ReferencePicker.js.map +0 -1
  52. package/dist/components/supportTicket/TicketHeader.d.ts +0 -86
  53. package/dist/components/supportTicket/TicketHeader.d.ts.map +0 -1
  54. package/dist/components/supportTicket/TicketHeader.js +0 -149
  55. package/dist/components/supportTicket/TicketHeader.js.map +0 -1
  56. package/dist/components/supportTicket/TicketMetaTags.d.ts +0 -32
  57. package/dist/components/supportTicket/TicketMetaTags.d.ts.map +0 -1
  58. package/dist/components/supportTicket/TicketMetaTags.js +0 -21
  59. package/dist/components/supportTicket/TicketMetaTags.js.map +0 -1
  60. package/dist/components/supportTicket/TicketReplyBox.d.ts +0 -63
  61. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +0 -1
  62. package/dist/components/supportTicket/TicketReplyBox.js +0 -115
  63. package/dist/components/supportTicket/TicketReplyBox.js.map +0 -1
  64. package/dist/components/supportTicket/fileToAttachment.d.ts +0 -9
  65. package/dist/components/supportTicket/fileToAttachment.d.ts.map +0 -1
  66. package/dist/components/supportTicket/fileToAttachment.js +0 -28
  67. package/dist/components/supportTicket/fileToAttachment.js.map +0 -1
  68. package/dist/components/supportTicket/index.d.ts +0 -9
  69. package/dist/components/supportTicket/index.d.ts.map +0 -1
  70. package/dist/components/supportTicket/index.js +0 -25
  71. package/dist/components/supportTicket/index.js.map +0 -1
  72. package/dist/components/supportTicket/meta.d.ts +0 -63
  73. package/dist/components/supportTicket/meta.d.ts.map +0 -1
  74. package/dist/components/supportTicket/meta.js +0 -79
  75. package/dist/components/supportTicket/meta.js.map +0 -1
  76. package/dist/components/ticketReferences/TicketReferences.d.ts +0 -52
  77. package/dist/components/ticketReferences/TicketReferences.d.ts.map +0 -1
  78. package/dist/components/ticketReferences/TicketReferences.js +0 -453
  79. package/dist/components/ticketReferences/TicketReferences.js.map +0 -1
  80. package/dist/components/ticketReferences/index.d.ts +0 -3
  81. package/dist/components/ticketReferences/index.d.ts.map +0 -1
  82. package/dist/components/ticketReferences/index.js +0 -6
  83. package/dist/components/ticketReferences/index.js.map +0 -1
  84. package/dist/components/ticketThread/TicketThread.d.ts +0 -52
  85. package/dist/components/ticketThread/TicketThread.d.ts.map +0 -1
  86. package/dist/components/ticketThread/TicketThread.js +0 -248
  87. package/dist/components/ticketThread/TicketThread.js.map +0 -1
  88. package/dist/components/ticketThread/index.d.ts +0 -3
  89. package/dist/components/ticketThread/index.d.ts.map +0 -1
  90. package/dist/components/ticketThread/index.js +0 -7
  91. package/dist/components/ticketThread/index.js.map +0 -1
  92. package/dist/helpers/scrollbar.d.ts +0 -2
  93. package/dist/helpers/scrollbar.d.ts.map +0 -1
  94. package/dist/helpers/scrollbar.js +0 -25
  95. package/dist/helpers/scrollbar.js.map +0 -1
  96. package/src/components/composer/Composer.stories.tsx +0 -238
  97. package/src/components/composer/Composer.tsx +0 -545
  98. package/src/components/composer/helpers.ts +0 -74
  99. package/src/components/composer/index.ts +0 -2
  100. package/src/components/imageLightbox/ImageLightbox.stories.tsx +0 -156
  101. package/src/components/imageLightbox/ImageLightbox.tsx +0 -169
  102. package/src/components/imageLightbox/index.ts +0 -1
  103. package/src/components/supportTicket/AttachmentChips.stories.tsx +0 -122
  104. package/src/components/supportTicket/AttachmentChips.tsx +0 -142
  105. package/src/components/supportTicket/InterfaceReferenceTags.stories.tsx +0 -134
  106. package/src/components/supportTicket/InterfaceReferenceTags.tsx +0 -85
  107. package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -296
  108. package/src/components/supportTicket/ReferencePicker.tsx +0 -237
  109. package/src/components/supportTicket/TicketConversation.stories.tsx +0 -122
  110. package/src/components/supportTicket/TicketHeader.stories.tsx +0 -183
  111. package/src/components/supportTicket/TicketHeader.tsx +0 -247
  112. package/src/components/supportTicket/TicketMetaTags.stories.tsx +0 -103
  113. package/src/components/supportTicket/TicketMetaTags.tsx +0 -85
  114. package/src/components/supportTicket/TicketReplyBox.stories.tsx +0 -355
  115. package/src/components/supportTicket/TicketReplyBox.tsx +0 -146
  116. package/src/components/supportTicket/fileToAttachment.ts +0 -24
  117. package/src/components/supportTicket/index.ts +0 -8
  118. package/src/components/supportTicket/meta.ts +0 -145
  119. package/src/components/ticketReferences/TicketReferences.stories.tsx +0 -301
  120. package/src/components/ticketReferences/TicketReferences.tsx +0 -899
  121. package/src/components/ticketReferences/index.ts +0 -2
  122. package/src/components/ticketThread/TicketThread.stories.tsx +0 -751
  123. package/src/components/ticketThread/TicketThread.tsx +0 -650
  124. package/src/components/ticketThread/index.ts +0 -6
  125. package/src/helpers/scrollbar.ts +0 -32
@@ -1,237 +0,0 @@
1
- import { ReqoreInput, ReqoreMenu, ReqoreMenuItem, ReqoreSpinner } from '@qoretechnologies/reqore';
2
- import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
3
- import { ReactNode, useMemo, useState } from 'react';
4
- import styled from 'styled-components';
5
- import { thinScrollbar } from '../../helpers/scrollbar';
6
- import { defaultInterfaceIcon, IInterfaceReference } from './meta';
7
-
8
- /*
9
- * The two-pane interface browser: kinds down the left, the selected kind's interfaces
10
- * on the right, and one search field that scopes both. Sits above a ticket composer,
11
- * toggled by the composer's "Reference interfaces" action.
12
- *
13
- * The host owns the data. Which kinds exist and what lives under one are questions only
14
- * the surrounding app can answer — the IDE lists them off the customer's live instance,
15
- * a story hands over fixtures — so this component takes `kinds` and the selected kind's
16
- * `items` and does the rest: selection, search, and turning a click into a reference.
17
- */
18
-
19
- /** How many interfaces the right pane shows before it scrolls. */
20
- const PANE_MAX_HEIGHT = 180;
21
- const KIND_PANE_WIDTH = 150;
22
-
23
- /*
24
- * A scroll pane with the thin, unobtrusive scrollbar the Qonsole surfaces use
25
- * (transparent track, translucent rounded thumb) rather than the raw browser one.
26
- * webkit-only, like those surfaces: setting `scrollbar-width` as well would let
27
- * Chromium override this custom 6px with its own, wider, "thin".
28
- */
29
- const Pane = styled.div<{ $right?: boolean }>`
30
- ${({ $right }) =>
31
- $right
32
- ? 'flex: 1; min-width: 0;'
33
- : `width: ${KIND_PANE_WIDTH}px; flex: none; border-right: 1px solid rgba(123, 104, 238, 0.25);`}
34
- max-height: ${PANE_MAX_HEIGHT}px;
35
- overflow-y: auto;
36
- padding: 4px;
37
-
38
- ${thinScrollbar}
39
- `;
40
-
41
- const Frame = styled.div`
42
- border: 1px solid rgba(123, 104, 238, 0.25);
43
- border-radius: 8px;
44
- overflow: hidden;
45
- `;
46
-
47
- const Panes = styled.div`
48
- display: flex;
49
- `;
50
-
51
- /*
52
- * An underline search field. The input itself is borderless (`flat transparent`) and
53
- * this wrapper carries the single bottom border, so the search reads as a line rather
54
- * than a full grey box — including on focus, where Reqore's default is a 2px outline
55
- * around the whole control. Suppressing that outline and brightening the underline
56
- * instead keeps a focus cue without the box. The selector is scoped under this wrapper,
57
- * so it outranks Reqore's without `!important`.
58
- */
59
- const Search = styled.div<{ $accent: string }>`
60
- padding: 6px 4px 4px;
61
- border-bottom: 1px solid ${({ $accent }) => `${$accent}80`};
62
- transition: border-color 0.15s ease-out;
63
-
64
- .reqore-control-wrapper:focus-within {
65
- outline: none;
66
- }
67
- &:focus-within {
68
- border-bottom-color: ${({ $accent }) => $accent};
69
- }
70
- `;
71
-
72
- const Note = styled.div`
73
- opacity: 0.4;
74
- padding: 8px 6px;
75
- font-size: 12px;
76
- `;
77
-
78
- const DEFAULT_ACCENT = '#762f7e';
79
-
80
- export interface IReferencePickerProps {
81
- /** the interface kinds listed down the left pane */
82
- kinds: string[];
83
- /** the kind whose interfaces `items` holds */
84
- kind: string;
85
- onKindChange: (kind: string) => void;
86
- /**
87
- * The selected kind's interfaces, by name. Fetching is the host's job — it knows
88
- * where they come from and how to cache them.
89
- */
90
- items: string[];
91
- /** `items` is still being fetched, so the right pane shows a spinner rather than
92
- * claiming the kind is empty */
93
- loading?: boolean;
94
- /**
95
- * The host's fetch for this kind failed. Shown in place of the list, because an
96
- * empty pane would otherwise read as "this instance has no workflows" — which is a
97
- * different and much more alarming statement than "we couldn't ask".
98
- */
99
- error?: string;
100
- /** Retry the failed fetch. The error state is pressable when given. */
101
- onRetry?: () => void;
102
- /** Already-picked references. Anything of the current kind that is already picked is
103
- * dropped from the right pane, so a reference can't be added twice. */
104
- picked?: IInterfaceReference[];
105
- onAdd: (reference: IInterfaceReference) => void;
106
- /** the whole picker is inert (e.g. a reply is in flight) */
107
- disabled?: boolean;
108
- /** Per-kind icon. Consumers with their own icon vocabulary (the IDE) pass one; the
109
- * built-in per-kind default is used when omitted. */
110
- resolveInterfaceIcon?: (kind: string) => IReqoreIconName;
111
- /** the underline / selected-kind colour. Defaults to the support surfaces' violet. */
112
- accent?: string;
113
- /** shown above the panes — the composer's picked-reference chips, typically */
114
- header?: ReactNode;
115
- }
116
-
117
- export const ReferencePicker = ({
118
- kinds,
119
- kind,
120
- onKindChange,
121
- items,
122
- loading,
123
- error,
124
- onRetry,
125
- picked,
126
- onAdd,
127
- disabled,
128
- resolveInterfaceIcon = defaultInterfaceIcon,
129
- accent = DEFAULT_ACCENT,
130
- header,
131
- }: IReferencePickerProps) => {
132
- const [query, setQuery] = useState<string>('');
133
- const needle = query.trim().toLowerCase();
134
-
135
- const visibleKinds = useMemo(
136
- () => (needle ? kinds.filter((k) => k.toLowerCase().includes(needle)) : kinds),
137
- [kinds, needle]
138
- );
139
-
140
- /*
141
- * The right pane drops what's already been picked and then applies the search. When
142
- * the KIND itself matches the search ("workflow"), its interfaces all stay — you
143
- * searched for the kind, so you want to see what's in it.
144
- */
145
- const visibleItems = useMemo(() => {
146
- const already = new Set(
147
- (picked ?? [])
148
- .filter((reference) => reference.interface_kind === kind)
149
- .map((reference) => reference.interface_name)
150
- );
151
- const kindMatches = kind.toLowerCase().includes(needle);
152
- return items.filter(
153
- (name) =>
154
- !already.has(name) && (!needle || kindMatches || name.toLowerCase().includes(needle))
155
- );
156
- }, [items, picked, kind, needle]);
157
-
158
- return (
159
- <Frame>
160
- {header}
161
- <Search $accent={accent}>
162
- <ReqoreInput
163
- value={query}
164
- onChange={(event) => setQuery((event.target as HTMLInputElement).value)}
165
- placeholder='Search kinds and interfaces…'
166
- icon='SearchLine'
167
- minimal
168
- flat
169
- transparent
170
- fluid
171
- disabled={disabled}
172
- onClearClick={query ? () => setQuery('') : undefined}
173
- />
174
- </Search>
175
- <Panes>
176
- {/* Both panes are `ReqoreMenu`s of `ReqoreMenuItem`s — a vertical selectable
177
- list is exactly what Menu is, so the selection (`selected`), the left-icon +
178
- right-icon, and the item chrome come from reqore rather than hand-styled
179
- buttons. The Pane wrapper still owns the scroll (fixed/flex width +
180
- thinScrollbar); the menus render transparent inside it. */}
181
- <Pane>
182
- {visibleKinds.length ? (
183
- <ReqoreMenu transparent flat padded={false} width='100%' size='small'>
184
- {visibleKinds.map((option) => (
185
- <ReqoreMenuItem
186
- key={option}
187
- icon={resolveInterfaceIcon(option)}
188
- label={option}
189
- selected={option === kind}
190
- disabled={disabled}
191
- onClick={() => onKindChange(option)}
192
- />
193
- ))}
194
- </ReqoreMenu>
195
- ) : (
196
- <Note>No kinds</Note>
197
- )}
198
- </Pane>
199
- <Pane $right>
200
- {error ? (
201
- <ReqoreMenu transparent flat padded={false} width='100%' size='small'>
202
- <ReqoreMenuItem
203
- icon='ErrorWarningLine'
204
- rightIcon={onRetry ? 'RefreshLine' : undefined}
205
- intent='danger'
206
- label={`Couldn't load ${kind}s`}
207
- /* the specific failure is the tooltip — the pane states the outcome */
208
- tooltip={error}
209
- onClick={onRetry}
210
- />
211
- </ReqoreMenu>
212
- ) : loading ? (
213
- <ReqoreSpinner iconColor='info' size='small' centered>
214
- Loading {kind}s…
215
- </ReqoreSpinner>
216
- ) : visibleItems.length ? (
217
- <ReqoreMenu transparent flat padded={false} width='100%' size='small'>
218
- {visibleItems.map((name) => (
219
- <ReqoreMenuItem
220
- key={name}
221
- icon={resolveInterfaceIcon(kind)}
222
- rightIcon='AddLine'
223
- label={name}
224
- disabled={disabled}
225
- tooltip={`Reference ${kind} ${name}`}
226
- onClick={() => onAdd({ interface_kind: kind, interface_name: name })}
227
- />
228
- ))}
229
- </ReqoreMenu>
230
- ) : (
231
- <Note>No matching {kind}</Note>
232
- )}
233
- </Pane>
234
- </Panes>
235
- </Frame>
236
- );
237
- };
@@ -1,122 +0,0 @@
1
- import { ReqoreUIProvider } from '@qoretechnologies/reqore';
2
- import { StoryObj } from '@storybook/react-vite';
3
- import { expect, fn } from 'storybook/test';
4
- import { ReactNode } from 'react';
5
- import { StoryMeta } from '../../types';
6
- import { ITicketThreadMessage, TicketThread } from '../ticketThread/TicketThread';
7
- import { InterfaceReferenceTags } from './InterfaceReferenceTags';
8
- import { TicketReplyBox } from './TicketReplyBox';
9
-
10
- /**
11
- * The full ticket surface — the shared `<TicketThread>` conversation above the
12
- * `<TicketReplyBox>` composer, the way a ticket detail (customer) or the staff
13
- * drawer stacks them. Shown under the Qorus theme so the `custom1` accent renders.
14
- */
15
- const meta = {
16
- component: TicketReplyBox,
17
- title: 'Components/TicketConversation',
18
- args: { onSend: fn() },
19
- } as StoryMeta<typeof TicketReplyBox>;
20
-
21
- export default meta;
22
- type Story = StoryObj<typeof meta>;
23
-
24
- const CUSTOMER: ITicketThreadMessage = {
25
- message_id: 'm1',
26
- author_type: 'customer',
27
- author_id: 'ops@acme.io',
28
- body: 'Since upgrading to 3.2 our nightly SFTP transfer to the partner endpoint times out after about 30 seconds. It ran fine on 3.1 with the same connection config.',
29
- created: '2026-07-16T09:12:00Z',
30
- referenced_interfaces: [{ interface_kind: 'connection', interface_name: 'sftp-partner' }],
31
- };
32
- const STAFF: ITicketThreadMessage = {
33
- message_id: 'm2',
34
- author_type: 'staff',
35
- author_id: 'nick',
36
- body: "Thanks for the detailed report — we've reproduced the timeout and a fix is in review. In the meantime, can you confirm the datasource pool size configured for the sftp-partner connection?",
37
- created: '2026-07-16T10:03:00Z',
38
- };
39
- const INTERNAL: ITicketThreadMessage = {
40
- message_id: 'm3',
41
- author_type: 'staff',
42
- author_id: 'nick',
43
- internal: true,
44
- body: 'Root cause: the connection-pool default silently dropped from 4 to 1 in the 3.2 migration. Patch is in review against develop.',
45
- created: '2026-07-16T10:05:00Z',
46
- };
47
-
48
- const themed = (node: ReactNode) => (
49
- <ReqoreUIProvider theme={{ main: '#121212', intents: { custom1: '#762f7e' } }}>
50
- <div style={{ padding: 20, background: '#121212', maxWidth: 780 }}>{node}</div>
51
- </ReqoreUIProvider>
52
- );
53
-
54
- const refs = (
55
- <InterfaceReferenceTags
56
- size='tiny'
57
- customTheme={{ main: 'custom1' }}
58
- references={[{ interface_kind: 'connection', interface_name: 'sftp-partner' }]}
59
- />
60
- );
61
-
62
- /**
63
- * Staff view — the thread with `viewerRole='staff'` (internal notes show, amber)
64
- * above the staff composer with the split send.
65
- */
66
- export const StaffConversation: Story = {
67
- parameters: {
68
- docs: {
69
- description: {
70
- story:
71
- "Renders the staff view — the thread with viewerRole='staff' (internal notes visible, amber) stacked above the staff composer with its split send.",
72
- },
73
- },
74
- },
75
- render: () =>
76
- themed(
77
- <>
78
- <TicketThread viewerRole='staff' messages={[CUSTOMER, STAFF, INTERNAL]} />
79
- <div style={{ marginTop: 16 }}>
80
- <TicketReplyBox allowInternalNote onSend={fn()} aboveInput={refs} />
81
- </div>
82
- </>
83
- ),
84
- async play({ canvasElement }) {
85
- await expect(canvasElement.textContent).toContain('reproduced the timeout');
86
- // the internal note is visible to staff
87
- await expect(canvasElement.textContent).toContain('Root cause');
88
- // the composer (rich editor) renders below the thread
89
- await expect(canvasElement.querySelector('[contenteditable="true"]')).not.toBeNull();
90
- },
91
- };
92
-
93
- /**
94
- * Customer view — the same thread as the customer sees it (`viewerRole='customer'`,
95
- * staff shown as "Qorus Support", no internal notes) above their reply composer.
96
- */
97
- export const CustomerConversation: Story = {
98
- parameters: {
99
- docs: {
100
- description: {
101
- story:
102
- "Renders the customer view — the same thread with viewerRole='customer' (staff shown as 'Qorus Support', no internal notes) stacked above the customer's reply composer.",
103
- },
104
- },
105
- },
106
- render: () =>
107
- themed(
108
- <>
109
- <TicketThread viewerRole='customer' messages={[CUSTOMER, STAFF]} />
110
- <div style={{ marginTop: 16 }}>
111
- <TicketReplyBox onSend={fn()} aboveInput={refs} />
112
- </div>
113
- </>
114
- ),
115
- async play({ canvasElement }) {
116
- await expect(canvasElement.textContent).toContain('reproduced the timeout');
117
- // the internal note is not in the customer's messages
118
- await expect(canvasElement.textContent).not.toContain('Root cause');
119
- // the composer (rich editor) renders below the thread
120
- await expect(canvasElement.querySelector('[contenteditable="true"]')).not.toBeNull();
121
- },
122
- };
@@ -1,183 +0,0 @@
1
- import { ReqoreButton, ReqoreDropdown } from '@qoretechnologies/reqore';
2
- import { StoryObj } from '@storybook/react-vite';
3
- import { expect, within } from 'storybook/test';
4
- import { StoryMeta } from '../../types';
5
- import { TicketMetaTags } from './TicketMetaTags';
6
- import {
7
- TicketAvatar,
8
- TicketHeader,
9
- TicketHeaderText,
10
- TicketPersonValue,
11
- ticketInitials,
12
- } from './TicketHeader';
13
-
14
- const meta = {
15
- component: TicketHeader,
16
- title: 'Components/TicketHeader',
17
- args: {
18
- subject: 'Workflow deploy fails at the mapper step',
19
- reference: '#1001',
20
- },
21
- } as StoryMeta<typeof TicketHeader>;
22
-
23
- export default meta;
24
- type Story = StoryObj<typeof meta>;
25
-
26
- const person = (name: string) => (
27
- <TicketPersonValue>
28
- <TicketAvatar>{ticketInitials(name)}</TicketAvatar>
29
- <TicketHeaderText>{name}</TicketHeaderText>
30
- </TicketPersonValue>
31
- );
32
-
33
- /**
34
- * What a customer sees: the same rail, every value read-only. The properties are the
35
- * caller's — this is the shorter of the two lists a real surface passes.
36
- */
37
- export const Customer: Story = {
38
- parameters: {
39
- docs: {
40
- description: {
41
- story:
42
- "Renders the customer's header rail — subject, reference, and a short list of read-only property rows, with the status shown from the customer's viewpoint as 'Awaiting your reply'.",
43
- },
44
- },
45
- },
46
- args: {
47
- properties: [
48
- {
49
- label: 'Status',
50
- value: (
51
- <TicketMetaTags
52
- ticket={{ status: 'awaiting_customer', priority: 'high', category: 'bug' }}
53
- viewerRole='customer'
54
- />
55
- ),
56
- },
57
- { label: 'Opened by', value: person('ide-demo@local.dev') },
58
- { label: 'References', value: <TicketHeaderText muted>None</TicketHeaderText> },
59
- ],
60
- },
61
- async play({ canvasElement }) {
62
- const canvas = within(canvasElement);
63
- await expect(canvas.getByText('Workflow deploy fails at the mapper step')).toBeInTheDocument();
64
- await expect(canvas.getByText('#1001')).toBeInTheDocument();
65
- await expect(canvas.getByText('Awaiting your reply')).toBeInTheDocument();
66
- },
67
- };
68
-
69
- /**
70
- * What staff see: the same frame with pressable values. The row is as tall as a
71
- * control whether or not it holds one, so a rail that mixes text and dropdowns still
72
- * reads as a table — that invariant is what the alignment test below pins.
73
- */
74
- export const Staff: Story = {
75
- parameters: {
76
- docs: {
77
- description: {
78
- story:
79
- 'Renders the staff header rail — the same frame with pressable values (a status dropdown, assignee and customer people, a references button) alongside plain text.',
80
- },
81
- },
82
- },
83
- args: {
84
- properties: [
85
- {
86
- label: 'Status',
87
- value: (
88
- <ReqoreDropdown
89
- size='small'
90
- minimal
91
- flat
92
- label='Open'
93
- showCaret={false}
94
- rightIcon='ArrowDownSLine'
95
- items={[{ label: 'Open', selected: true }, { label: 'Resolved' }]}
96
- />
97
- ),
98
- },
99
- { label: 'Assignee', value: person('nick.m@acme.io') },
100
- { label: 'Customer', value: person('ide-demo@local.dev') },
101
- {
102
- label: 'References',
103
- value: (
104
- <ReqoreButton size='small' minimal flat icon='GitBranchLine'>
105
- 5 references
106
- </ReqoreButton>
107
- ),
108
- },
109
- ],
110
- },
111
- async play({ canvasElement }) {
112
- const canvas = within(canvasElement);
113
- await expect(canvas.getByText('nick.m@acme.io')).toBeInTheDocument();
114
- await expect(canvas.getByText('NM')).toBeInTheDocument();
115
- },
116
- };
117
-
118
- /**
119
- * The rail's two layout invariants, measured rather than eyeballed — both have
120
- * regressed by hand more than once.
121
- *
122
- * 1. Every value starts on the same vertical edge, whether it is a control (which
123
- * insets its own content by its horizontal padding) or bare text (which does not).
124
- * 2. Every row is the same height, whether it holds a 32px control or a line of text.
125
- */
126
- export const RowsAlign: Story = {
127
- parameters: {
128
- docs: {
129
- description: {
130
- story:
131
- "Renders the staff rail again to measure its two layout invariants — every value's content starts on the same vertical edge, and every row is the same height, whether the row holds a control or bare text.",
132
- },
133
- },
134
- },
135
- args: Staff.args,
136
- async play({ canvasElement }) {
137
- const rows = Array.from(
138
- canvasElement.querySelectorAll<HTMLElement>('.reqore-description-list-row')
139
- );
140
- await expect(rows.length).toBe(4);
141
-
142
- /* Where the value's CONTENT starts, not where its box does. The inset is padding
143
- and it lands on the value itself — the button on a control row, the person span
144
- on a text row — so measure that child's padded edge. */
145
- const contentLeft = (row: HTMLElement) => {
146
- const column = row.querySelector<HTMLElement>('.reqore-description-list-content')!;
147
- const value = (column.firstElementChild as HTMLElement | null) ?? column;
148
- return Math.round(
149
- value.getBoundingClientRect().left + parseFloat(getComputedStyle(value).paddingLeft)
150
- );
151
- };
152
-
153
- const lefts = rows.map(contentLeft);
154
- await expect(new Set(lefts).size, `value content starts at: ${lefts.join(', ')}`).toBe(1);
155
-
156
- const heights = rows.map((row) => Math.round(row.getBoundingClientRect().height));
157
- await expect(new Set(heights).size, `row heights: ${heights.join(', ')}`).toBe(1);
158
- },
159
- };
160
-
161
- /** `when: false` drops a row, so one list can serve both viewer roles. */
162
- export const HiddenRow: Story = {
163
- parameters: {
164
- docs: {
165
- description: {
166
- story:
167
- 'Renders a property list where one row is flagged when: false, showing that the flagged Assignee row drops out so a single list can serve both viewer roles.',
168
- },
169
- },
170
- },
171
- args: {
172
- properties: [
173
- { label: 'Status', value: <TicketHeaderText>Open</TicketHeaderText> },
174
- { label: 'Assignee', value: <TicketHeaderText>nick.m@acme.io</TicketHeaderText>, when: false },
175
- { label: 'Customer', value: <TicketHeaderText>ide-demo@local.dev</TicketHeaderText> },
176
- ],
177
- },
178
- async play({ canvasElement }) {
179
- const canvas = within(canvasElement);
180
- await expect(canvas.queryByText('Assignee')).not.toBeInTheDocument();
181
- await expect(canvas.getByText('Customer')).toBeInTheDocument();
182
- },
183
- };