@astratra/saas-kit-ui 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +69 -42
- package/package.json +2 -2
- package/src/components/FeedbackBar.js +70 -0
- package/src/index.jsx +11 -0
- package/src/lib/feedbackBar.js +61 -0
- package/src/styles/global.css +120 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# @astratra/saas-kit-ui
|
|
2
2
|
|
|
3
|
-
Interface React
|
|
4
|
-
Astratra. Le package expose une application dashboard complete et quelques
|
|
5
|
-
primitives (`AuthProvider`, `useAuth`, `apiFetch`) pour composer une interface
|
|
6
|
-
plus specifique.
|
|
3
|
+
Interface React réutilisable pour démarrer rapidement un tableau de bord SaaS. Le paquet expose une application complète (connexion, vue d'ensemble, utilisateurs, réglages) et quelques primitives (`AuthProvider`, `useAuth`, `apiFetch`) pour composer une interface plus spécifique. Il est prévu pour un backend `@astratra/saas-kit`.
|
|
7
4
|
|
|
8
5
|
## Installation
|
|
9
6
|
|
|
@@ -11,10 +8,9 @@ plus specifique.
|
|
|
11
8
|
npm install @astratra/saas-kit-ui react react-dom
|
|
12
9
|
```
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
backend `@astratra/saas-kit`.
|
|
11
|
+
React 18 ou plus récent. Le dépôt contient `examples/dashboard-ui`, qui montre l'intégration avec un backend `@astratra/saas-kit`.
|
|
16
12
|
|
|
17
|
-
##
|
|
13
|
+
## Exemple
|
|
18
14
|
|
|
19
15
|
```jsx
|
|
20
16
|
import React from 'react';
|
|
@@ -28,42 +24,73 @@ createRoot(document.getElementById('root')).render(
|
|
|
28
24
|
);
|
|
29
25
|
```
|
|
30
26
|
|
|
31
|
-
Par
|
|
32
|
-
`VITE_API_URL` pour pointer vers ton backend :
|
|
27
|
+
Le paquet importe sa feuille de styles avec l'application ; elle est aussi disponible séparément via `@astratra/saas-kit-ui/styles.css`. Par défaut, l'interface appelle `http://localhost:4000`. Avec Vite, définissez `VITE_API_URL` pour pointer vers votre backend :
|
|
33
28
|
|
|
34
29
|
```bash
|
|
35
|
-
VITE_API_URL=
|
|
30
|
+
VITE_API_URL=https://api.exemple.fr npm run dev
|
|
36
31
|
```
|
|
37
32
|
|
|
38
|
-
##
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
33
|
+
## Ce que le tableau de bord attend du backend
|
|
34
|
+
|
|
35
|
+
| Écran | Appels |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| Connexion | `POST /auth/login` avec `{ email, password }` ; réponse `{ token, user }`. |
|
|
38
|
+
| Vue d'ensemble | `GET /dashboard/summary` |
|
|
39
|
+
| Utilisateurs | `GET /users?limit=50&offset=0`, `POST /users` |
|
|
40
|
+
| Réglages | `GET /settings`, `PATCH /settings/<clé>` avec `{ value }` |
|
|
41
|
+
|
|
42
|
+
Les réponses suivent le format `apiResponse` d'`@astratra/core` : `{ success, data, message }`.
|
|
43
|
+
|
|
44
|
+
## Exports
|
|
45
|
+
|
|
46
|
+
| Export | Rôle |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `AstratraDashboardApp` (aussi export par défaut) | Tableau de bord complet. |
|
|
49
|
+
| `AuthProvider`, `useAuth` | Session par jeton Bearer gardé en mémoire ; `useAuth()` rend `isAuthenticated`, `token`, `user`, `login`, `logout` et `request`. |
|
|
50
|
+
| `apiFetch(chemin, options)`, `ApiError` | Client minimal. Options : `apiUrl`, `token`, `method`, `body`, `fetcher`, `onUnauthorized`. |
|
|
51
|
+
| `Layout`, `Notice` | Composants de mise en page et de message. |
|
|
52
|
+
| `DashboardScreen`, `LoginScreen`, `SettingsScreen`, `UsersScreen` | Les écrans, réutilisables séparément. |
|
|
53
|
+
|
|
54
|
+
## Ce que le paquet ne fait pas
|
|
55
|
+
|
|
56
|
+
- Il ne contient pas de serveur et ne fixe aucun domaine métier ; le backend est fourni par `@astratra/saas-kit` ou par votre application.
|
|
57
|
+
- Le jeton n'est gardé qu'en mémoire : un rechargement de la page déconnecte l'utilisateur.
|
|
58
|
+
- Il n'est pas conçu pour être branché sur une interface existante qui utilise des sessions par cookie `HttpOnly` : voir `@astratra/react`.
|
|
59
|
+
|
|
60
|
+
## `AuthProvider`/`useAuth` et `@astratra/react`
|
|
61
|
+
|
|
62
|
+
- **`AuthProvider`/`useAuth` (ce paquet)** : garde le JWT en mémoire et l'envoie en `Authorization: Bearer` via `apiFetch`. Couplé à `AstratraDashboardApp`.
|
|
63
|
+
- **`SessionProvider`/`useSession` (`@astratra/react`)** : ne stocke aucun jeton côté client ; la session vit dans un cookie `HttpOnly` géré par le backend. Pas de tableau de bord imposé.
|
|
64
|
+
|
|
65
|
+
Utilisez ce paquet si `AstratraDashboardApp` vous convient tel quel ; utilisez `@astratra/react` si vous construisez votre propre interface sur des sessions par cookie.
|
|
66
|
+
|
|
67
|
+
## Licence
|
|
68
|
+
|
|
69
|
+
MIT.
|
|
70
|
+
|
|
71
|
+
## FeedbackBar : 👍 / 👎 sous une réponse d'IA
|
|
72
|
+
|
|
73
|
+
Le même composant que dans `@astratra/native-ui`, pour le web. Contrôlé :
|
|
74
|
+
l'application garde `value` et parle à son serveur (par exemple
|
|
75
|
+
`createFeedbackRouter` de `@astratra/agent`).
|
|
76
|
+
|
|
77
|
+
```jsx
|
|
78
|
+
import { FeedbackBar } from '@astratra/saas-kit-ui';
|
|
79
|
+
|
|
80
|
+
<FeedbackBar
|
|
81
|
+
value={feedback?.rating ?? null}
|
|
82
|
+
onSubmit={(payload) => api.giveFeedback(runId, payload)} // { rating, reason?, comment? }
|
|
83
|
+
onRemove={() => api.removeFeedback(runId)}
|
|
84
|
+
locale="fr"
|
|
85
|
+
/>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Le pouce haut part aussitôt ; le pouce bas ouvre un panneau (raison,
|
|
89
|
+
commentaire facultatif) ; appuyer sur le pouce choisi le retire. Libellés par
|
|
90
|
+
défaut en anglais et en français (`locale`), remplaçables un par un (`labels`) ;
|
|
91
|
+
raisons ajoutables (`reasons`) ; `icons` remplace les émojis ; `haptics` est une
|
|
92
|
+
fonction facultative (rien par défaut). Accessibilité : groupe nommé, boutons à
|
|
93
|
+
`aria-pressed`, puces `radio`, panneau `dialog`, Échap le ferme. Le style vient
|
|
94
|
+
des classes `.astratra-feedback*` de `styles.css` et des variables CSS du kit.
|
|
95
|
+
Les règles pures (`feedbackPress`, `buildFeedbackPayload`, `resolveFeedbackLabels`,
|
|
96
|
+
`feedbackReasonOptions`, `clampComment`) sont exportées.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astratra/saas-kit-ui",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Reusable React dashboard shell for Astratra SaaS applications.",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Reusable React dashboard shell for Astratra SaaS applications, plus a thumbs up/down feedback bar for AI answers.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20"
|
|
7
7
|
},
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createElement as h, useId, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
FEEDBACK_COMMENT_MAX,
|
|
4
|
+
FEEDBACK_REASONS,
|
|
5
|
+
buildFeedbackPayload,
|
|
6
|
+
feedbackPress,
|
|
7
|
+
feedbackReasonOptions,
|
|
8
|
+
resolveFeedbackLabels
|
|
9
|
+
} from '../lib/feedbackBar.js';
|
|
10
|
+
|
|
11
|
+
const THUMBS = { up: '\u{1F44D}', down: '\u{1F44E}' };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Thumbs up / thumbs down under an AI answer. Controlled: the app keeps
|
|
15
|
+
* `value` and talks to its server in `onSubmit` / `onRemove`. Up is sent at
|
|
16
|
+
* once; down opens a small panel for a reason and an optional comment;
|
|
17
|
+
* pressing the chosen thumb again withdraws it. Words come from `labels` and
|
|
18
|
+
* `locale`. Colours come from the kit's CSS variables, with fallbacks.
|
|
19
|
+
*/
|
|
20
|
+
export default function FeedbackBar({
|
|
21
|
+
value = null, onSubmit, onRemove, disabled = false, reasons = FEEDBACK_REASONS, maxComment = FEEDBACK_COMMENT_MAX,
|
|
22
|
+
labels, locale, icons, haptics, defaultOpen = false, className, style, id
|
|
23
|
+
}) {
|
|
24
|
+
const words = resolveFeedbackLabels({ locale, labels });
|
|
25
|
+
const [asking, setAsking] = useState(Boolean(defaultOpen));
|
|
26
|
+
const [reason, setReason] = useState(null);
|
|
27
|
+
const [comment, setComment] = useState('');
|
|
28
|
+
const titleId = useId();
|
|
29
|
+
const tick = () => { if (typeof haptics === 'function') { try { haptics(); } catch (_error) { /* never blocks a rating */ } } };
|
|
30
|
+
const close = () => { setAsking(false); setReason(null); setComment(''); };
|
|
31
|
+
const press = (rating) => {
|
|
32
|
+
if (disabled) return;
|
|
33
|
+
tick();
|
|
34
|
+
const action = feedbackPress(value, rating);
|
|
35
|
+
if (action.type === 'remove') onRemove?.();
|
|
36
|
+
else if (action.type === 'ask') setAsking(true);
|
|
37
|
+
else if (action.type === 'submit') onSubmit?.(action.payload);
|
|
38
|
+
};
|
|
39
|
+
const send = () => {
|
|
40
|
+
tick();
|
|
41
|
+
const payload = buildFeedbackPayload({ rating: 'down', reason, comment, reasons, maxComment });
|
|
42
|
+
close();
|
|
43
|
+
onSubmit?.(payload);
|
|
44
|
+
};
|
|
45
|
+
const thumb = (rating) => h('button', {
|
|
46
|
+
key: rating, type: 'button', disabled, 'aria-pressed': value === rating, 'aria-label': words[rating], title: words[rating],
|
|
47
|
+
className: 'astratra-feedback__thumb', 'data-rating': rating, onClick: () => press(rating)
|
|
48
|
+
}, icons?.[rating] ?? h('span', { 'aria-hidden': 'true' }, THUMBS[rating]));
|
|
49
|
+
|
|
50
|
+
const options = feedbackReasonOptions({ reasons, labels: words, selected: reason });
|
|
51
|
+
return h('div', { id, role: 'group', 'aria-label': words.group, className: ['astratra-feedback', className].filter(Boolean).join(' '), style },
|
|
52
|
+
thumb('up'), thumb('down'),
|
|
53
|
+
asking && h('div', {
|
|
54
|
+
role: 'dialog', 'aria-modal': 'false', 'aria-labelledby': titleId, className: 'astratra-feedback__sheet',
|
|
55
|
+
onKeyDown: (event) => { if (event.key === 'Escape') close(); }
|
|
56
|
+
},
|
|
57
|
+
h('p', { id: titleId, className: 'astratra-feedback__title' }, words.sheetTitle),
|
|
58
|
+
h('div', { role: 'radiogroup', 'aria-labelledby': titleId, className: 'astratra-feedback__reasons' },
|
|
59
|
+
options.map((option) => h('button', {
|
|
60
|
+
key: option.id, type: 'button', role: 'radio', 'aria-checked': option.selected, className: 'astratra-feedback__chip',
|
|
61
|
+
onClick: () => setReason(option.selected ? null : option.id)
|
|
62
|
+
}, option.label))),
|
|
63
|
+
h('label', { className: 'astratra-feedback__comment' },
|
|
64
|
+
h('span', { className: 'astratra-feedback__label' }, words.commentLabel),
|
|
65
|
+
h('textarea', { value: comment, maxLength: maxComment, placeholder: words.commentPlaceholder, rows: 3, onChange: (event) => setComment(event.target.value) }),
|
|
66
|
+
h('span', { className: 'astratra-feedback__counter', 'aria-hidden': 'true' }, words.counter(Array.from(comment).length, maxComment))),
|
|
67
|
+
h('div', { className: 'astratra-feedback__actions' },
|
|
68
|
+
h('button', { type: 'button', className: 'astratra-feedback__cancel', onClick: close }, words.cancel),
|
|
69
|
+
h('button', { type: 'button', className: 'astratra-feedback__send', onClick: send }, words.submit))));
|
|
70
|
+
}
|
package/src/index.jsx
CHANGED
|
@@ -7,3 +7,14 @@ export { default as DashboardScreen } from './screens/Dashboard.jsx';
|
|
|
7
7
|
export { default as LoginScreen } from './screens/Login.jsx';
|
|
8
8
|
export { default as SettingsScreen } from './screens/Settings.jsx';
|
|
9
9
|
export { default as UsersScreen } from './screens/Users.jsx';
|
|
10
|
+
export { default as FeedbackBar } from './components/FeedbackBar.js';
|
|
11
|
+
export {
|
|
12
|
+
FEEDBACK_COMMENT_MAX,
|
|
13
|
+
FEEDBACK_LABELS,
|
|
14
|
+
FEEDBACK_REASONS,
|
|
15
|
+
buildFeedbackPayload,
|
|
16
|
+
clampComment,
|
|
17
|
+
feedbackPress,
|
|
18
|
+
feedbackReasonOptions,
|
|
19
|
+
resolveFeedbackLabels
|
|
20
|
+
} from './lib/feedbackBar.js';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Pure rules of the feedback bar (thumbs up / down on an AI answer).
|
|
2
|
+
// Same rules as the mobile bar in @astratra/native-ui; no React here.
|
|
3
|
+
|
|
4
|
+
export const FEEDBACK_REASONS = Object.freeze(['inaccurate', 'incomplete', 'off_topic', 'wrong_source', 'too_long', 'unsafe', 'other']);
|
|
5
|
+
export const FEEDBACK_COMMENT_MAX = 500;
|
|
6
|
+
|
|
7
|
+
export const FEEDBACK_LABELS = Object.freeze({
|
|
8
|
+
en: Object.freeze({
|
|
9
|
+
group: 'Rate this answer', up: 'Good answer', down: 'Bad answer', sheetTitle: 'What went wrong?',
|
|
10
|
+
commentLabel: 'Comment (optional)', commentPlaceholder: 'Tell us more (optional)', submit: 'Send', cancel: 'Cancel',
|
|
11
|
+
reasons: Object.freeze({ inaccurate: 'Inaccurate', incomplete: 'Incomplete', off_topic: 'Off topic', wrong_source: 'Wrong source', too_long: 'Too long', unsafe: 'Unsafe', other: 'Other' }),
|
|
12
|
+
counter: (used, max) => `${used}/${max}`
|
|
13
|
+
}),
|
|
14
|
+
fr: Object.freeze({
|
|
15
|
+
group: 'Évaluer cette réponse', up: 'Bonne réponse', down: 'Mauvaise réponse', sheetTitle: "Qu'est-ce qui n'allait pas ?",
|
|
16
|
+
commentLabel: 'Commentaire (facultatif)', commentPlaceholder: 'Dites-nous en plus (facultatif)', submit: 'Envoyer', cancel: 'Annuler',
|
|
17
|
+
reasons: Object.freeze({ inaccurate: 'Inexacte', incomplete: 'Incomplète', off_topic: 'Hors sujet', wrong_source: 'Mauvaise source', too_long: 'Trop longue', unsafe: 'Dangereuse', other: 'Autre' }),
|
|
18
|
+
counter: (used, max) => `${used}/${max}`
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const TEXT_KEYS = ['group', 'up', 'down', 'sheetTitle', 'commentLabel', 'commentPlaceholder', 'submit', 'cancel'];
|
|
23
|
+
|
|
24
|
+
/** Labels for a locale ('fr…' is French, anything else English), with the app's overrides on top. */
|
|
25
|
+
export function resolveFeedbackLabels({ locale, labels } = {}) {
|
|
26
|
+
const language = typeof locale === 'string' ? locale.toLowerCase().split(/[-_]/)[0] : 'en';
|
|
27
|
+
const base = FEEDBACK_LABELS[language] || FEEDBACK_LABELS.en;
|
|
28
|
+
const given = labels && typeof labels === 'object' ? labels : {};
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const key of TEXT_KEYS) out[key] = typeof given[key] === 'string' && given[key] ? given[key] : base[key];
|
|
31
|
+
out.reasons = { ...base.reasons, ...(given.reasons && typeof given.reasons === 'object' ? given.reasons : {}) };
|
|
32
|
+
out.counter = typeof given.counter === 'function' ? given.counter : base.counter;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What pressing a thumb does: withdraw the chosen one, send up at once, ask why for down. */
|
|
37
|
+
export function feedbackPress(value, rating) {
|
|
38
|
+
if (rating !== 'up' && rating !== 'down') return { type: 'none' };
|
|
39
|
+
if (value === rating) return { type: 'remove' };
|
|
40
|
+
return rating === 'up' ? { type: 'submit', payload: { rating: 'up' } } : { type: 'ask' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function clampComment(text, max = FEEDBACK_COMMENT_MAX) {
|
|
44
|
+
if (typeof text !== 'string') return '';
|
|
45
|
+
const chars = Array.from(text);
|
|
46
|
+
return chars.length <= max ? text : chars.slice(0, max).join('');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The body to send: a reason only with 'down' and only if listed; blank comment dropped; trimmed and cut. */
|
|
50
|
+
export function buildFeedbackPayload({ rating, reason, comment, reasons = FEEDBACK_REASONS, maxComment = FEEDBACK_COMMENT_MAX } = {}) {
|
|
51
|
+
if (rating !== 'up' && rating !== 'down') throw new Error("rating must be 'up' or 'down'.");
|
|
52
|
+
const payload = { rating };
|
|
53
|
+
if (rating === 'down' && typeof reason === 'string' && reasons.includes(reason)) payload.reason = reason;
|
|
54
|
+
const text = clampComment(typeof comment === 'string' ? comment.trim() : '', maxComment).trim();
|
|
55
|
+
if (text) payload.comment = text;
|
|
56
|
+
return payload;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function feedbackReasonOptions({ reasons = FEEDBACK_REASONS, labels, selected }) {
|
|
60
|
+
return reasons.map((id) => ({ id, label: labels.reasons[id] || id, selected: id === selected }));
|
|
61
|
+
}
|
package/src/styles/global.css
CHANGED
|
@@ -96,3 +96,123 @@ h3 {
|
|
|
96
96
|
transition-duration: 1ms !important;
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
/* FeedbackBar: thumbs up / down under an AI answer. */
|
|
101
|
+
.astratra-feedback {
|
|
102
|
+
position: relative;
|
|
103
|
+
display: inline-flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 6px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.astratra-feedback__thumb {
|
|
109
|
+
width: 32px;
|
|
110
|
+
height: 32px;
|
|
111
|
+
display: inline-grid;
|
|
112
|
+
place-items: center;
|
|
113
|
+
border: 1px solid var(--line, #2e3a31);
|
|
114
|
+
border-radius: 999px;
|
|
115
|
+
background: transparent;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
opacity: 0.7;
|
|
118
|
+
transition: opacity 120ms, background 120ms;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.astratra-feedback__thumb:hover:not(:disabled),
|
|
122
|
+
.astratra-feedback__thumb:focus-visible {
|
|
123
|
+
opacity: 1;
|
|
124
|
+
background: var(--panel-raised, #1a231d);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.astratra-feedback__thumb[aria-pressed="true"] {
|
|
128
|
+
opacity: 1;
|
|
129
|
+
border-color: var(--lime, #d9ff66);
|
|
130
|
+
background: var(--panel-raised, #1a231d);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.astratra-feedback__thumb:disabled {
|
|
134
|
+
cursor: default;
|
|
135
|
+
opacity: 0.4;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.astratra-feedback__sheet {
|
|
139
|
+
position: absolute;
|
|
140
|
+
top: calc(100% + 8px);
|
|
141
|
+
left: 0;
|
|
142
|
+
z-index: 10;
|
|
143
|
+
width: min(360px, 90vw);
|
|
144
|
+
display: grid;
|
|
145
|
+
gap: 10px;
|
|
146
|
+
padding: 14px;
|
|
147
|
+
border: 1px solid var(--line-bright, #46584c);
|
|
148
|
+
border-radius: 12px;
|
|
149
|
+
background: var(--panel, #141b17);
|
|
150
|
+
color: var(--cream, #f4f1e6);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.astratra-feedback__title {
|
|
154
|
+
margin: 0;
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.astratra-feedback__reasons {
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-wrap: wrap;
|
|
161
|
+
gap: 6px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.astratra-feedback__chip {
|
|
165
|
+
padding: 5px 10px;
|
|
166
|
+
border: 1px solid var(--line, #2e3a31);
|
|
167
|
+
border-radius: 999px;
|
|
168
|
+
background: transparent;
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.astratra-feedback__chip[aria-checked="true"] {
|
|
173
|
+
border-color: var(--lime, #d9ff66);
|
|
174
|
+
background: var(--panel-raised, #1a231d);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.astratra-feedback__comment {
|
|
178
|
+
display: grid;
|
|
179
|
+
gap: 4px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.astratra-feedback__label,
|
|
183
|
+
.astratra-feedback__counter {
|
|
184
|
+
font-size: 0.8em;
|
|
185
|
+
color: var(--muted, #7c8a7a);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.astratra-feedback__counter {
|
|
189
|
+
text-align: right;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.astratra-feedback__comment textarea {
|
|
193
|
+
resize: vertical;
|
|
194
|
+
padding: 8px;
|
|
195
|
+
border: 1px solid var(--line, #2e3a31);
|
|
196
|
+
border-radius: 8px;
|
|
197
|
+
background: var(--bg, #0d1310);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.astratra-feedback__actions {
|
|
201
|
+
display: flex;
|
|
202
|
+
justify-content: flex-end;
|
|
203
|
+
gap: 8px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.astratra-feedback__cancel,
|
|
207
|
+
.astratra-feedback__send {
|
|
208
|
+
padding: 6px 14px;
|
|
209
|
+
border: 1px solid var(--line, #2e3a31);
|
|
210
|
+
border-radius: 8px;
|
|
211
|
+
background: transparent;
|
|
212
|
+
cursor: pointer;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.astratra-feedback__send {
|
|
216
|
+
border-color: var(--lime, #d9ff66);
|
|
217
|
+
color: var(--lime, #d9ff66);
|
|
218
|
+
}
|