@firstflow/react 0.0.1

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/README.md ADDED
@@ -0,0 +1,315 @@
1
+ # @cdn/react-issue-reporter
2
+
3
+ Firstflow platform React SDK. Issue reporting is one feature module; the root API is a platform instance with `agentId`, `apiUrl`, and feature modules (e.g. `firstflow.issue`).
4
+
5
+ Three modes for issues: **inline** (form in a container), **modal** (form in a modal), and **programmatic** (submit without UI).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @cdn/react-issue-reporter
11
+ ```
12
+
13
+ Peer dependencies: `react` and `react-dom` (>= 18).
14
+
15
+ ## Quick start
16
+
17
+ ### 1. Create the platform instance
18
+
19
+ ```ts
20
+ import { createFirstflow } from '@cdn/react-issue-reporter';
21
+
22
+ const firstflow = createFirstflow({
23
+ agentId: 'your-agent-id',
24
+ apiUrl: 'https://api.example.com',
25
+ });
26
+ ```
27
+
28
+ The instance has read-only `agentId`, `apiUrl`, a platform **analytics** API (`firstflow.analytics.track()`, etc.), an **issue** module (`open`, `submit`, `getConfig`, `destroy`), and a **feedback** module for per-message ratings (`submit`, `getConfig`, `isEnabled`). Issue and message-feedback submissions are sent via the analytics layer (Jitsu event `issue_submitted` / `feedback_submitted`); no backend POST for those events.
29
+
30
+ To load **issues**, **message feedback**, and **active experiences** (including saved `flow` graphs) from the backend (`GET {apiUrl}/agents/{agentId}/config`), use `useCreateFirstflow(agentId, apiUrl, { fetchConfig: true })`. Pass the returned `firstflow` and **`config`** to `FirstflowProvider` (`config={config}`). Children can call **`useFirstflowConfig()`** to read `config.experiences` (and issues/feedback fields) without a second request.
31
+
32
+ Alternatively pass `feedbackConfig` into `createFirstflow({ ..., feedbackConfig })` if you are not using `fetchConfig` but still want the message feedback UI enabled.
33
+
34
+ ### 2. Use with React (inline or modal)
35
+
36
+ Wrap your app with `FirstflowProvider` and pass the instance:
37
+
38
+ ```tsx
39
+ import { FirstflowProvider, InlineIssueForm, useFirstflow } from '@cdn/react-issue-reporter';
40
+
41
+ <FirstflowProvider firstflow={firstflow}>
42
+ <YourApp />
43
+ <InlineIssueForm />
44
+ </FirstflowProvider>
45
+ ```
46
+
47
+ - **Inline:** `<InlineIssueForm />` renders the form in place.
48
+ - **Modal:** Call `firstflow.issue.open()` or `useIssueReporter().open()` to open the form in a modal (e.g. a “Report issue” button).
49
+
50
+ ### 3. Programmatic submit (no UI)
51
+
52
+ ```ts
53
+ await firstflow.issue.submit({
54
+ name: 'Jane',
55
+ email: 'j@example.com',
56
+ subject: 'Bug',
57
+ description: '…',
58
+ messageId: 'msg_1',
59
+ conversationId: 'conv_1',
60
+ });
61
+ ```
62
+
63
+ Form fields are validated; context metadata (`messageId`, `conversationId`, etc.) is passed through without validation.
64
+
65
+ #### Headless issue form: `useIssueForm()`
66
+
67
+ Inside `IssueReporterProvider` / `FirstflowProvider`, build your own layout (Tailwind, etc.):
68
+
69
+ ```tsx
70
+ import { useIssueForm } from '@cdn/react-issue-reporter';
71
+
72
+ function CustomIssueForm() {
73
+ const { values, setValue, errors, submit, submitting, isEnabled, fields, promptText } = useIssueForm();
74
+ if (!isEnabled) return null;
75
+ return (
76
+ <form
77
+ onSubmit={(e) => {
78
+ e.preventDefault();
79
+ void submit();
80
+ }}
81
+ >
82
+ {promptText && <p>{promptText}</p>}
83
+ {fields.map((f) => (
84
+ <div key={f.id}>
85
+ <label>{f.label}</label>
86
+ <input value={values[f.id] ?? ''} onChange={(e) => setValue(f.id, e.target.value)} />
87
+ {errors[f.id] && <span>{errors[f.id]}</span>}
88
+ </div>
89
+ ))}
90
+ <button type="submit" disabled={submitting}>Send</button>
91
+ </form>
92
+ );
93
+ }
94
+ ```
95
+
96
+ `submit()` merges context from `open({ messageId, conversationId, … })` the same way as `FormEngine` / `InlineIssueForm`.
97
+
98
+ ### 4. Message feedback (per assistant message)
99
+
100
+ Host must pass stable **`conversationId`** and **`messageId`** (the assistant message being rated). **`agent_id`** is added automatically on the Jitsu payload.
101
+
102
+ Two integration styles (similar to Clerk’s ready components vs hooks):
103
+
104
+ | Style | Use when |
105
+ |-------|----------|
106
+ | **`MessageFeedback`** | You want the built-in card / inline thumbs UI. |
107
+ | **`useFeedback({ conversationId, messageId, messagePreview })`** | Headless per-message ratings (Tailwind, custom buttons). |
108
+ | **`useFeedback()`** (no args) | Programmatic feedback only: `submit(payload)`, `getConfig()`, `isEnabled()` (e.g. custom slots). |
109
+
110
+ #### Headless message ratings: `useFeedback({ ... })`
111
+
112
+ Inside `FirstflowProvider`, pass `conversationId`, `messageId`, and `messagePreview`. Returns `rating`, `selectedTags`, `comment`, `setRating`, `toggleTag`, `setComment`, `submit`, `isEnabled`, `config`, `sideConfig`, plus `submitting`, `submitted`, `error`, `clearError`.
113
+
114
+ ```tsx
115
+ import { useFeedback } from '@cdn/react-issue-reporter';
116
+
117
+ function MyFeedbackRow({ conversationId, messageId, preview }: Props) {
118
+ const { rating, setRating, submit, isEnabled, submitting, sideConfig, selectedTags, toggleTag, comment, setComment } =
119
+ useFeedback({
120
+ conversationId,
121
+ messageId,
122
+ messagePreview: preview,
123
+ metadata: { surface: 'chat' },
124
+ });
125
+
126
+ if (!isEnabled) return null;
127
+
128
+ return (
129
+ <div className="flex flex-col gap-2">
130
+ <div className="flex gap-2">
131
+ <button
132
+ type="button"
133
+ className={rating === 'like' ? 'bg-green-600 text-white' : 'bg-gray-200'}
134
+ onClick={() => setRating('like')}
135
+ >
136
+ Helpful
137
+ </button>
138
+ <button
139
+ type="button"
140
+ className={rating === 'dislike' ? 'bg-red-500 text-white' : 'bg-gray-200'}
141
+ onClick={() => setRating('dislike')}
142
+ >
143
+ Not helpful
144
+ </button>
145
+ </div>
146
+ {rating && sideConfig && (
147
+ <>
148
+ <p className="text-xs font-semibold">{sideConfig.heading}</p>
149
+ <div className="flex flex-wrap gap-1">
150
+ {sideConfig.tags.map((tag) => (
151
+ <button key={tag} type="button" onClick={() => toggleTag(tag)} className={selectedTags.includes(tag) ? 'ring-2 ring-blue-500' : ''}>
152
+ {tag}
153
+ </button>
154
+ ))}
155
+ </div>
156
+ <textarea value={comment} onChange={(e) => setComment(e.target.value)} placeholder={sideConfig.placeholder} />
157
+ <button type="button" disabled={submitting} onClick={() => void submit()}>
158
+ {submitting ? 'Sending…' : 'Send'}
159
+ </button>
160
+ </>
161
+ )}
162
+ </div>
163
+ );
164
+ }
165
+ ```
166
+
167
+ #### Ready component: `MessageFeedback`
168
+
169
+ Use **`MessageFeedback`** when `feedback_config.enabled` is true (from agent or from `createFirstflow({ feedbackConfig })`):
170
+
171
+ ```tsx
172
+ // App root: one provider after firstflow is ready
173
+ <FirstflowProvider firstflow={firstflow}>
174
+ <ChatMessage conversationId={cid} messageId={mid} text={assistantText} />
175
+ </FirstflowProvider>
176
+
177
+ function ChatMessage({ conversationId, messageId, text }) {
178
+ return (
179
+ <MessageFeedback
180
+ conversationId={conversationId}
181
+ messageId={messageId}
182
+ messagePreview={text}
183
+ />
184
+ );
185
+ }
186
+ ```
187
+
188
+ - **`variant="default"`** (default): preview box + thumbs + form.
189
+ - **`variant="inline"`**: thumbs only in the first fragment node (place beside the message in a flex row); after the user picks like/dislike, the tag/comment form renders in a second fragment node — use a wrapping flex container with `flex-wrap: wrap` and give the form child `flex: 1 1 100%` so it spans the full width below.
190
+ - **`inlineFormSurface="dark"`**: when using `variant="inline"`, styles the expanded form for dark chat backgrounds.
191
+
192
+ Programmatic (no UI):
193
+
194
+ ```ts
195
+ await firstflow.feedback.submit({
196
+ conversationId: 'conv_1',
197
+ messageId: 'msg_assistant_42',
198
+ rating: 'like', // or 'dislike'
199
+ comment: 'optional',
200
+ messagePreview: 'snippet for analytics',
201
+ metadata: { page: '/chat', locale: 'en' },
202
+ });
203
+ ```
204
+
205
+ **Jitsu event `feedback_submitted`** — properties (snake_case) include:
206
+
207
+ | Property | Required | Notes |
208
+ |----------|----------|--------|
209
+ | `agent_id` | yes | Injected by SDK |
210
+ | `session_id`, `device_type` | yes | Injected by SDK |
211
+ | `anonymousId` | yes | On envelope (Segment-style) |
212
+ | `conversation_id` | yes | From host |
213
+ | `message_id` | yes | From host |
214
+ | `rating` | yes | `like` \| `dislike` |
215
+ | `comment` | no | Free text |
216
+ | `message_preview` | no | Truncated (~2000 chars) |
217
+ | `metadata` | no | Arbitrary JSON object (serializable keys/values; max ~32KB serialized) |
218
+ | `submitted_at` | yes | ISO timestamp |
219
+
220
+ **Analytics:** “Current” feedback per user per message is the **latest** row by timestamp for the same `message_id` and identity (`anonymousId` / `userId`), e.g. `ROW_NUMBER() OVER (PARTITION BY message_id, anonymous_id ORDER BY timestamp DESC) = 1` in your warehouse.
221
+
222
+ ### 5. Experience flow: Message nodes (`useExperienceMessageNode`)
223
+
224
+ Headless hook for a **Message** step in an experience (config matches the dashboard message node: text, carousel, quick replies, CTAs, **`ctaUrl`** / **`ctaPrompt`**). It returns **`blocks`** in fixed order (`text` → `carousel` → `quick_replies` → `cta_primary` → `cta_dismiss`), **`blocksVersion`** (currently **4**; quick-reply options are `{ label, prompt? }` and `quick_reply` actions include optional **`promptText`**; v3 added primary CTA **`promptText`**; v2 added per-card carousel CTA fields), **`ui`** flags, and **`handlers`** for clicks. Integrate navigation in **`onAction`** (map `kind` / `label` / `promptText` / `url` / `index` / `cardId` / `actionId` to your graph — e.g. send **`promptText`** or **`label`** as the user message for button CTAs and quick replies, open **`url`** for link CTAs).
225
+
226
+ | Topic | Behavior |
227
+ |-------|----------|
228
+ | **Shown** | `EXPERIENCE_SHOWN` on mount by default (`trackShownOnMount: true`). |
229
+ | **Dedupe** | Module-scoped Set: same default key (`agentId` + `experienceId` + `nodeId` + `conversationId`) fires **once** per page load. Re-showing the same node in a new run needs a unique **`impressionKey`** (e.g. append `flowInstanceId`) or **`dedupeShown: false`**. |
230
+ | **Virtualized lists** | `trackShownOnMount: false` and call **`reportShown()`** when the row is visible (e.g. IntersectionObserver). |
231
+ | **Clicks** | `EXPERIENCE_CLICKED` with `action` from **`EXPERIENCE_MESSAGE_ANALYTICS_ACTION`** (`cta_primary`, `cta_dismiss`, `quick_reply`, `carousel_cta`). For `cta_primary`, props may include **`cta_url`** (link) and **`cta_prompt`** (button prompt text). For `quick_reply`, props include **`quick_reply_label`**, **`quick_reply_index`**, and optional **`quick_reply_prompt`**. For `carousel_cta`, props include **`card_id`**, **`carousel_action_id`** (`primary` / `secondary`), and when the card is in config: **`carousel_button_action`** (`prompt` / `link`), **`carousel_button_label`**, **`carousel_link_url`**, **`carousel_prompt`**. Use **`buildExperienceCarouselCtaAction(config, cardId, actionId)`** if you build clicks outside **`handlers.onCarouselCta`**. **`message_style`** normalizes legacy `inline` to `message`. |
232
+ | **Errors** | `onAction` / `mapAnalytics` throws are logged; analytics still runs (`onAction` first, then track; if `mapAnalytics` throws, base props are tracked). |
233
+
234
+ Optional **`mapAnalytics(eventName, props)`** transforms payloads before `track` (avoid renaming events unless intentional).
235
+
236
+ **Next step (not in SDK yet):** a thin `<MessageNode />` wrapper around this hook for default markup.
237
+
238
+ ### 6. Imperative mount (optional DOM adapter)
239
+
240
+ ```ts
241
+ import { createFirstflow, mount } from '@cdn/react-issue-reporter';
242
+
243
+ const firstflow = createFirstflow({ agentId, apiUrl });
244
+ const unmount = mount(firstflow, document.getElementById('issue-root'));
245
+ // Later: firstflow.issue.open() opens the modal.
246
+ // Teardown: unmount(); firstflow.issue.destroy();
247
+ ```
248
+
249
+ ## API
250
+
251
+ ### createFirstflow(options)
252
+
253
+ Returns a **FirstflowInstance** (root platform context).
254
+
255
+ - `options.agentId` (string)
256
+ - `options.apiUrl` (string)
257
+ - `options.jitsuHost?` (string) — Jitsu host for analytics; defaults to shared Firstflow Jitsu if omitted
258
+ - `options.jitsuWriteKey?` (string) — Jitsu write key; defaults to shared key if omitted
259
+
260
+ **FirstflowInstance:**
261
+
262
+ - `agentId`, `apiUrl` (read-only)
263
+ - `analytics` — platform analytics API (used by all feature modules):
264
+ - `track(eventName, properties?)` — send a track event to Jitsu (Segment-compatible)
265
+ - `identify(userId?, traits?)` — identify a user (optional)
266
+ - `page(name?, properties?)` — page view (optional)
267
+ - `issue` — public issue module API:
268
+ - `open(options?)` — open the issue UI (modal or inline depending on integration)
269
+ - `submit(data)` — submit an issue (form values + optional context)
270
+ - `getConfig()` — read-only config (enabled, promptText, fields)
271
+ - `destroy()` — tear down handlers
272
+ - `feedback` — message feedback (Jitsu `feedback_submitted`):
273
+ - `submit({ conversationId, messageId, rating, comment?, messagePreview?, metadata? })` — tag pills in the UI are not sent to Jitsu; put anything you need for analytics in `metadata`
274
+ - `getConfig()`, `isEnabled()`
275
+
276
+ Naming: issue uses **open** + **submit**; feedback uses **submit** (and optional **MessageFeedback** UI).
277
+
278
+ ### React exports
279
+
280
+ - **FirstflowProvider** — accepts `firstflow: FirstflowInstance`; provides the platform instance to the tree. Transitional: it delegates to the existing issue provider; the final architecture may use a single provider for multiple modules.
281
+ - **useFirstflow()** — returns the platform instance from context.
282
+ - **useCreateFirstflow(agentId, apiUrl, { fetchConfig? })** — returns `{ firstflow, loading, error }`. With `fetchConfig: true`, loads `issues_config` and `feedback_config` from the agent on `GET .../config` (supports `{ meta, data: { agent } }`).
283
+ - **useFeedback()** — returns `firstflow.feedback` (must be inside `FirstflowProvider`).
284
+ - **MessageFeedback** — props: `conversationId`, `messageId`, `messagePreview`, optional `metadata`; renders nothing when feedback is disabled.
285
+ - **fetchSdkAgentConfig(apiUrl, agentId)** — low-level fetch of `{ issues_config, feedback_config }`.
286
+ - **useIssueReporter()** — returns `{ reporter, open, reportIssue, submit, config, isModalOpen, closeModal }` for issue-specific state.
287
+ - **InlineIssueForm** — renders the form inline (only when `config.enabled`).
288
+ - **IssueModal** — modal wrapper (rendered by the provider).
289
+ - **FormEngine** — headless-friendly form (config + onSubmit + optional onCancel).
290
+
291
+ ### mount(firstflow, target)
292
+
293
+ DOM adapter only: renders `FirstflowProvider` + `InlineIssueForm` into `target` with React 18 `createRoot`. Returns an unmount function.
294
+
295
+ ## Issue types (platform-first exports)
296
+
297
+ Issue-specific types are under the **Issue** namespace so the root package stays platform-first:
298
+
299
+ - `Issue.FormValues`, `Issue.ContextMetadata`, `Issue.Payload`, `Issue.OpenOptions`
300
+ - `Issue.NormalizedConfig`, `Issue.NormalizedFieldConfig`, `Issue.RawIssueFieldConfig`, `Issue.RawIssuesConfig`, `Issue.ValidationResult`
301
+
302
+ Platform types at root: `FirstflowInstance`, `CreateFirstflowOptions`, `IssueModulePublic`, `AnalyticsModulePublic`. Event names: `ISSUE_SUBMITTED`, `FEEDBACK_SUBMITTED`, `SURVEY_COMPLETED`, etc. (see `analytics/events`).
303
+
304
+ ## Analytics and validation
305
+
306
+ - **Analytics:** Issue submissions (and future modules) go through the platform analytics layer. Feature modules call `firstflow.analytics.track(eventName, properties)`; the analytics service sends Segment-compatible events to Jitsu. See [ANALYTICS.md](./ANALYTICS.md).
307
+ - **Validation:** `validatePayload(data, fields)` applies to form field keys only; used by the issue module before calling `analytics.track("issue_submitted", payload)`.
308
+
309
+ ## Architecture
310
+
311
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for the platform instance, analytics layer, transitional provider design, and data flow.
312
+
313
+ ## License
314
+
315
+ Private / Firstflow.
package/dist/index.css ADDED
@@ -0,0 +1,360 @@
1
+ /* src/components/FormEngine.module.css */
2
+ .form {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 1rem;
6
+ }
7
+ .field {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: 0.25rem;
11
+ }
12
+ .label {
13
+ font-size: 0.875rem;
14
+ font-weight: 600;
15
+ color: #334155;
16
+ }
17
+ .input,
18
+ .textarea,
19
+ .select {
20
+ width: 100%;
21
+ padding: 0.5rem 0.75rem;
22
+ font-size: 0.875rem;
23
+ border: 1px solid #e2e8f0;
24
+ border-radius: 0.5rem;
25
+ color: #0f172a;
26
+ background: #fff;
27
+ box-sizing: border-box;
28
+ }
29
+ .textarea {
30
+ min-height: 5rem;
31
+ resize: vertical;
32
+ }
33
+ .input:focus,
34
+ .textarea:focus,
35
+ .select:focus {
36
+ outline: none;
37
+ border-color: #0f766e;
38
+ box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.2);
39
+ }
40
+ .inputError,
41
+ .textareaError,
42
+ .selectError {
43
+ border-color: #dc2626;
44
+ }
45
+ .errorText {
46
+ font-size: 0.75rem;
47
+ color: #dc2626;
48
+ }
49
+ .actions {
50
+ display: flex;
51
+ gap: 0.5rem;
52
+ justify-content: flex-end;
53
+ margin-top: 0.5rem;
54
+ }
55
+ .prompt {
56
+ font-size: 0.875rem;
57
+ color: #64748b;
58
+ margin: 0 0 0.5rem 0;
59
+ }
60
+ .submitError {
61
+ font-size: 0.8125rem;
62
+ color: #dc2626;
63
+ margin: 0;
64
+ }
65
+
66
+ /* src/components/IssueModal.module.css */
67
+ .overlay {
68
+ position: fixed;
69
+ inset: 0;
70
+ background: rgba(0, 0, 0, 0.4);
71
+ z-index: 9998;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ }
76
+ .panel {
77
+ position: relative;
78
+ width: 90%;
79
+ max-width: 400px;
80
+ background: #fff;
81
+ border-radius: 1rem;
82
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
83
+ z-index: 9999;
84
+ padding: 1.5rem;
85
+ }
86
+ .close {
87
+ position: absolute;
88
+ top: 0.5rem;
89
+ right: 0.5rem;
90
+ background: none;
91
+ border: none;
92
+ font-size: 1.5rem;
93
+ line-height: 1;
94
+ cursor: pointer;
95
+ color: #94a3b8;
96
+ padding: 0.25rem;
97
+ }
98
+ .close:hover {
99
+ color: #0f172a;
100
+ }
101
+ .title {
102
+ font-size: 1rem;
103
+ font-weight: 700;
104
+ color: #0f172a;
105
+ margin: 0 0 0.5rem 0;
106
+ }
107
+
108
+ /* src/components/MessageFeedback.module.css */
109
+ .card {
110
+ margin-top: 0.75rem;
111
+ padding: 1rem 1.125rem;
112
+ border-radius: 12px;
113
+ border: 1px solid #e2e8f0;
114
+ background: #f8fafc;
115
+ font-family:
116
+ ui-sans-serif,
117
+ system-ui,
118
+ -apple-system,
119
+ Segoe UI,
120
+ Roboto,
121
+ sans-serif;
122
+ max-width: 100%;
123
+ box-sizing: border-box;
124
+ }
125
+ .previewLabel {
126
+ font-size: 0.6875rem;
127
+ font-weight: 700;
128
+ letter-spacing: 0.06em;
129
+ color: #64748b;
130
+ margin: 0 0 0.5rem 0;
131
+ }
132
+ .previewBox {
133
+ padding: 0.75rem 0.875rem;
134
+ border-radius: 8px;
135
+ background: #fff;
136
+ border: 1px solid #e2e8f0;
137
+ font-size: 0.875rem;
138
+ line-height: 1.45;
139
+ color: #1e293b;
140
+ margin-bottom: 0.875rem;
141
+ }
142
+ .thumbs {
143
+ display: flex;
144
+ gap: 0.75rem;
145
+ align-items: center;
146
+ }
147
+ .thumbBtn {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ width: 44px;
152
+ height: 44px;
153
+ border-radius: 10px;
154
+ border: 2px solid #cbd5e1;
155
+ background: #fff;
156
+ cursor: pointer;
157
+ transition:
158
+ border-color 0.15s,
159
+ background 0.15s,
160
+ color 0.15s;
161
+ color: #64748b;
162
+ }
163
+ .thumbBtn:hover {
164
+ border-color: #94a3b8;
165
+ background: #f1f5f9;
166
+ }
167
+ .thumbBtnLike {
168
+ border-color: #86efac;
169
+ color: #15803d;
170
+ background: #ecfdf5;
171
+ }
172
+ .thumbBtnLike:hover {
173
+ border-color: #4ade80;
174
+ background: #d1fae5;
175
+ color: #166534;
176
+ }
177
+ .thumbBtnActiveLike {
178
+ border-color: #16a34a;
179
+ color: #fff;
180
+ background:
181
+ linear-gradient(
182
+ 180deg,
183
+ #22c55e 0%,
184
+ #16a34a 100%);
185
+ box-shadow: 0 1px 3px rgba(22, 163, 74, 0.35);
186
+ }
187
+ .thumbBtnActiveLike:hover {
188
+ border-color: #15803d;
189
+ background:
190
+ linear-gradient(
191
+ 180deg,
192
+ #16a34a 0%,
193
+ #15803d 100%);
194
+ color: #fff;
195
+ }
196
+ .thumbBtnActiveDislike {
197
+ border-color: #f87171;
198
+ color: #dc2626;
199
+ background: #fef2f2;
200
+ }
201
+ .sectionHeading {
202
+ font-size: 0.6875rem;
203
+ font-weight: 700;
204
+ letter-spacing: 0.06em;
205
+ color: #334155;
206
+ margin: 1.125rem 0 0.625rem 0;
207
+ }
208
+ .tags {
209
+ display: flex;
210
+ flex-wrap: wrap;
211
+ gap: 0.5rem;
212
+ margin-bottom: 0.75rem;
213
+ }
214
+ .tag {
215
+ padding: 0.375rem 0.75rem;
216
+ border-radius: 999px;
217
+ border: 1px solid #e2e8f0;
218
+ background: #fff;
219
+ font-size: 0.8125rem;
220
+ color: #475569;
221
+ cursor: pointer;
222
+ transition: border-color 0.15s, background 0.15s;
223
+ }
224
+ .tag:hover {
225
+ border-color: #cbd5e1;
226
+ }
227
+ .tagSelected {
228
+ border-color: #3b82f6;
229
+ background: #eff6ff;
230
+ color: #1d4ed8;
231
+ }
232
+ .textarea {
233
+ width: 100%;
234
+ min-height: 72px;
235
+ padding: 0.625rem 0.75rem;
236
+ border-radius: 8px;
237
+ border: 1px solid #e2e8f0;
238
+ font-size: 0.8125rem;
239
+ font-family: inherit;
240
+ resize: vertical;
241
+ box-sizing: border-box;
242
+ margin-bottom: 0.75rem;
243
+ }
244
+ .textarea:focus {
245
+ outline: none;
246
+ border-color: #3b82f6;
247
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
248
+ }
249
+ .submitBtn {
250
+ padding: 0.5rem 1rem;
251
+ border-radius: 8px;
252
+ border: none;
253
+ background: #0f172a;
254
+ color: #fff;
255
+ font-size: 0.8125rem;
256
+ font-weight: 600;
257
+ cursor: pointer;
258
+ }
259
+ .submitBtn:hover:not(:disabled) {
260
+ background: #1e293b;
261
+ }
262
+ .submitBtn:disabled {
263
+ opacity: 0.5;
264
+ cursor: not-allowed;
265
+ }
266
+ .hint {
267
+ font-size: 0.75rem;
268
+ color: #64748b;
269
+ margin: 0.5rem 0 0 0;
270
+ }
271
+ .error {
272
+ font-size: 0.75rem;
273
+ color: #dc2626;
274
+ margin: 0.5rem 0 0 0;
275
+ }
276
+ .cardInline {
277
+ margin-top: 0;
278
+ padding: 0.45rem 0.55rem;
279
+ align-self: flex-start;
280
+ width: auto;
281
+ max-width: 100%;
282
+ }
283
+ .cardInline .thumbs {
284
+ gap: 0.35rem;
285
+ }
286
+ .cardInline .thumbBtn {
287
+ width: 34px;
288
+ height: 34px;
289
+ border-radius: 8px;
290
+ }
291
+ .cardInline .thumbBtn svg {
292
+ width: 18px;
293
+ height: 18px;
294
+ }
295
+ .cardInline .sectionHeading {
296
+ margin-top: 0.65rem;
297
+ }
298
+ .inlineFormBelow {
299
+ box-sizing: border-box;
300
+ width: 100%;
301
+ padding: 0.75rem 1rem;
302
+ border-radius: 10px;
303
+ border: 1px solid #e2e8f0;
304
+ background: #fff;
305
+ margin-top: 0.125rem;
306
+ }
307
+ .inlineFormBelow .sectionHeading {
308
+ margin-top: 0;
309
+ }
310
+ .inlineFormBelowDark {
311
+ border-color: rgba(255, 255, 255, 0.12);
312
+ background: rgba(255, 255, 255, 0.06);
313
+ }
314
+ .inlineFormBelowDark .sectionHeading {
315
+ color: rgba(255, 255, 255, 0.65);
316
+ }
317
+ .inlineFormBelowDark .tag {
318
+ border-color: rgba(255, 255, 255, 0.15);
319
+ background: rgba(0, 0, 0, 0.2);
320
+ color: rgba(255, 255, 255, 0.85);
321
+ }
322
+ .inlineFormBelowDark .tag:hover {
323
+ border-color: rgba(255, 255, 255, 0.25);
324
+ background: rgba(0, 0, 0, 0.35);
325
+ }
326
+ .inlineFormBelowDark .tagSelected {
327
+ border-color: #60a5fa;
328
+ background: rgba(59, 130, 246, 0.2);
329
+ color: #93c5fd;
330
+ }
331
+ .inlineFormBelowDark .textarea {
332
+ background: rgba(0, 0, 0, 0.25);
333
+ border-color: rgba(255, 255, 255, 0.12);
334
+ color: #f1f5f9;
335
+ }
336
+ .inlineFormBelowDark .textarea:focus {
337
+ border-color: #60a5fa;
338
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
339
+ }
340
+ .inlineFormBelowDark .hint {
341
+ color: rgba(255, 255, 255, 0.5);
342
+ }
343
+ .inlineFormBelowDark .submitBtn {
344
+ background: #e2e8f0;
345
+ color: #0f172a;
346
+ }
347
+ .inlineFormBelowDark .submitBtn:hover:not(:disabled) {
348
+ background: #f8fafc;
349
+ }
350
+
351
+ /* src/components/InlineIssueForm.module.css */
352
+ .wrapper {
353
+ margin-top: 1rem;
354
+ }
355
+ .title {
356
+ font-size: 1rem;
357
+ font-weight: 700;
358
+ color: #0f172a;
359
+ margin: 0 0 0.5rem 0;
360
+ }