@astratra/native-ui 0.2.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Astratra
3
+ Copyright (c) 2026 Astratra contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -488,6 +488,46 @@ Aucun texte, aucune icône, aucun chargement en dur : tout vient des props.
488
488
 
489
489
  ---
490
490
 
491
+ # Les retours
492
+
493
+ ## FeedbackBar : 👍 / 👎 sous une réponse
494
+
495
+ Deux boutons en verre discrets sous la réponse d'une IA. Le pouce haut part
496
+ tout de suite ; le pouce bas ouvre une feuille (raison, commentaire facultatif)
497
+ avant d'envoyer ; appuyer de nouveau sur le pouce choisi le retire. Le composant
498
+ est **contrôlé** : l'application garde `value` et parle à son serveur (par
499
+ exemple `createFeedbackRouter` de `@astratra/agent`).
500
+
501
+ ```js
502
+ import { FeedbackBar } from '@astratra/native-ui';
503
+
504
+ <FeedbackBar
505
+ value={feedback?.rating ?? null}
506
+ onSubmit={(payload) => api.giveFeedback(runId, payload)} // { rating, reason?, comment? }
507
+ onRemove={() => api.removeFeedback(runId)}
508
+ locale="fr"
509
+ labels={{ sheetTitle: 'Dites-nous pourquoi' }}
510
+ />
511
+ ```
512
+
513
+ - **Textes** : aucun en dur qui ne se surcharge pas. `locale` choisit les
514
+ libellés par défaut (anglais, ou français pour `fr…`), `labels` remplace
515
+ chacun (`up`, `down`, `group`, `sheetTitle`, `commentLabel`,
516
+ `commentPlaceholder`, `submit`, `cancel`, `reasons`, `counter`). Une raison
517
+ ajoutée par l'application (`reasons={['slow']}`) a son libellé dans
518
+ `labels.reasons`.
519
+ - **Accessibilité** : une barre `toolbar` nommée, deux `button` avec libellé et
520
+ état `selected`/`disabled`, puces `radio`, feuille modale.
521
+ - **Haptique** : `expo-haptics` (pair facultatif) donne un léger tic ; `haptics={false}`
522
+ le coupe, `haptics={() => …}` le remplace. Sans le module, la barre est muette.
523
+ - **Style** : `accent`, `size`, `scheme`, `pageBackground`, `ink`, `icons`
524
+ (nœuds à la place des émojis).
525
+ - **Règles pures** (entrée `@astratra/native-ui/logic`, testées en Node) :
526
+ `FEEDBACK_REASONS`, `FEEDBACK_LABELS`, `resolveFeedbackLabels`,
527
+ `feedbackPress`, `buildFeedbackPayload` (raison seulement avec `down` et si
528
+ listée, commentaire rogné et coupé), `clampComment`, `feedbackButtonState`,
529
+ `feedbackReasonOptions`.
530
+
491
531
  # Tests
492
532
 
493
533
  ```sh
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astratra/native-ui",
3
- "version": "0.2.0",
4
- "description": "Mobile UI kit for React Native / Expo: Apple's Liquid Glass on iOS and a calibrated visible surface on Android, glass buttons, pale cards, bars that fold on scroll, an Instagram-style tab bar, a collapsible header, a Markdown renderer for AI answers, and pictures: natural-ratio images revealed gently, a shimmer placeholder, and a full-screen viewer with pinch, double-tap and pull-to-close. Pure rules exported separately and tested in plain Node.",
3
+ "version": "1.1.0",
4
+ "description": "Mobile UI kit for React Native / Expo: Apple's Liquid Glass on iOS and a calibrated visible surface on Android, glass buttons, pale cards, bars that fold on scroll, an Instagram-style tab bar, a collapsible header, a Markdown renderer for AI answers, pictures (natural-ratio images revealed gently, a shimmer placeholder, a full-screen viewer with pinch, double-tap and pull-to-close), and a thumbs up/down feedback bar for AI answers. Pure rules exported separately and tested in plain Node.",
5
5
  "engines": {
6
6
  "node": ">=20"
7
7
  },
@@ -47,16 +47,22 @@
47
47
  "access": "public"
48
48
  },
49
49
  "dependencies": {
50
- "@astratra/native": "^0.3.0"
50
+ "@astratra/native": "^1.0.0"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "expo-blur": ">=14",
54
54
  "expo-glass-effect": ">=0.1",
55
+ "expo-haptics": ">=14",
55
56
  "expo-linear-gradient": ">=14",
56
57
  "react": ">=18",
57
58
  "react-native": ">=0.76",
58
59
  "react-native-reanimated": ">=3.16"
59
60
  },
61
+ "peerDependenciesMeta": {
62
+ "expo-haptics": {
63
+ "optional": true
64
+ }
65
+ },
60
66
  "devDependencies": {
61
67
  "@testing-library/react": "^16.3.2",
62
68
  "jest": "30.4.2",
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Thumbs up / thumbs down under an AI answer.
3
+ *
4
+ * Two discreet glass buttons. Up is sent at once; down opens a sheet for the
5
+ * reason and an optional comment. Pressing the chosen thumb again withdraws
6
+ * it. The bar is controlled: the app keeps `value` and talks to its server in
7
+ * `onSubmit` / `onRemove` (see `createFeedbackRouter` in @astratra/agent).
8
+ *
9
+ * Words come from the app (`labels`, `locale`); the haptic tick is optional
10
+ * (expo-haptics when installed, or the function the app passes).
11
+ */
12
+ const { React, h, RN, useScheme } = require('./runtime');
13
+ const { GlassButton } = require('./GlassButton');
14
+ const { usePaleCardColors } = require('./PaleCard');
15
+ const {
16
+ FEEDBACK_REASONS,
17
+ FEEDBACK_COMMENT_MAX,
18
+ resolveFeedbackLabels,
19
+ feedbackPress,
20
+ buildFeedbackPayload,
21
+ feedbackButtonState,
22
+ feedbackReasonOptions
23
+ } = require('../logic/feedbackBar');
24
+
25
+ const DEFAULT_ACCENT = '#3b6cf0';
26
+
27
+ /* expo-haptics is an optional peer: absent, the bar is silent. */
28
+ let cachedHaptics;
29
+ function loadHaptics() {
30
+ if (cachedHaptics !== undefined) return cachedHaptics;
31
+ try {
32
+ const mod = require('expo-haptics');
33
+ cachedHaptics = mod && (mod.default || mod);
34
+ } catch (_error) {
35
+ cachedHaptics = null;
36
+ }
37
+ return cachedHaptics;
38
+ }
39
+
40
+ function tick(haptics) {
41
+ if (haptics === false) return;
42
+ try {
43
+ if (typeof haptics === 'function') return void haptics();
44
+ const lib = loadHaptics();
45
+ if (lib && typeof lib.selectionAsync === 'function') Promise.resolve(lib.selectionAsync()).catch(() => {});
46
+ } catch (_error) {
47
+ /* A missing tick never blocks a rating. */
48
+ }
49
+ }
50
+
51
+ function FeedbackBar({
52
+ value = null,
53
+ onSubmit,
54
+ onRemove,
55
+ disabled = false,
56
+ reasons = FEEDBACK_REASONS,
57
+ maxComment = FEEDBACK_COMMENT_MAX,
58
+ labels,
59
+ locale,
60
+ icons,
61
+ haptics,
62
+ accent = DEFAULT_ACCENT,
63
+ size = 34,
64
+ scheme: schemeOverride,
65
+ pageBackground,
66
+ ink,
67
+ style,
68
+ testID
69
+ }) {
70
+ const scheme = useScheme(schemeOverride);
71
+ const words = resolveFeedbackLabels({ locale, labels });
72
+ const [asking, setAsking] = React.useState(false);
73
+ const [reason, setReason] = React.useState(null);
74
+ const [comment, setComment] = React.useState('');
75
+ const card = usePaleCardColors({ tint: accent, pageBackground, ink, scheme });
76
+ const textColor = ink || (scheme === 'dark' ? '#ffffff' : '#0d1235');
77
+
78
+ const close = () => {
79
+ setAsking(false);
80
+ setReason(null);
81
+ setComment('');
82
+ };
83
+ const press = (rating) => {
84
+ if (disabled) return;
85
+ tick(haptics);
86
+ const action = feedbackPress(value, rating);
87
+ if (action.type === 'remove') {
88
+ if (onRemove) onRemove();
89
+ } else if (action.type === 'ask') {
90
+ setAsking(true);
91
+ } else if (action.type === 'submit' && onSubmit) {
92
+ onSubmit(action.payload);
93
+ }
94
+ };
95
+ const send = () => {
96
+ tick(haptics);
97
+ const payload = buildFeedbackPayload({ rating: 'down', reason, comment, reasons, maxComment });
98
+ close();
99
+ if (onSubmit) onSubmit(payload);
100
+ };
101
+
102
+ const thumb = (rating, glyph) => {
103
+ const state = feedbackButtonState({ value, rating, labels: words, disabled });
104
+ return h(
105
+ GlassButton,
106
+ {
107
+ key: rating,
108
+ size,
109
+ scheme,
110
+ disabled,
111
+ selected: state.selected,
112
+ tint: state.selected ? accent : undefined,
113
+ accessibilityLabel: state.accessibilityLabel,
114
+ testID: testID ? `${testID}-${rating}` : undefined,
115
+ onPress: () => press(rating)
116
+ },
117
+ icons && icons[rating] ? icons[rating] : h(RN.Text, { style: { fontSize: Math.round(size * 0.46) } }, glyph)
118
+ );
119
+ };
120
+
121
+ const chip = (option) =>
122
+ h(
123
+ RN.Pressable,
124
+ {
125
+ key: option.id,
126
+ testID: testID ? `${testID}-reason-${option.id}` : undefined,
127
+ accessibilityRole: 'radio',
128
+ accessibilityLabel: option.label,
129
+ accessibilityState: { selected: option.selected },
130
+ onPress: () => setReason(option.selected ? null : option.id),
131
+ style: {
132
+ paddingHorizontal: 12,
133
+ paddingVertical: 8,
134
+ borderRadius: 16,
135
+ backgroundColor: option.selected ? accent : card.fill
136
+ }
137
+ },
138
+ h(RN.Text, { style: { color: option.selected ? '#ffffff' : textColor, fontSize: 14 } }, option.label)
139
+ );
140
+
141
+ return h(
142
+ RN.View,
143
+ { testID, accessibilityRole: 'toolbar', accessibilityLabel: words.group, style: [{ flexDirection: 'row', alignItems: 'center', gap: 8 }, style] },
144
+ thumb('up', '\u{1F44D}'),
145
+ thumb('down', '\u{1F44E}'),
146
+ h(
147
+ RN.Modal,
148
+ { visible: asking, transparent: true, animationType: 'slide', onRequestClose: close },
149
+ h(
150
+ RN.View,
151
+ { style: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.35)' } },
152
+ h(
153
+ RN.View,
154
+ {
155
+ accessibilityViewIsModal: true,
156
+ style: { padding: 20, gap: 14, borderTopLeftRadius: 24, borderTopRightRadius: 24, backgroundColor: pageBackground || (scheme === 'dark' ? '#1c1c1e' : '#ffffff') }
157
+ },
158
+ h(RN.Text, { accessibilityRole: 'header', style: { color: textColor, fontSize: 17, fontWeight: '600' } }, words.sheetTitle),
159
+ h(RN.View, { accessibilityRole: 'radiogroup', style: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 } }, feedbackReasonOptions({ reasons, labels: words, selected: reason }).map(chip)),
160
+ h(RN.TextInput, {
161
+ testID: testID ? `${testID}-comment` : undefined,
162
+ accessibilityLabel: words.commentLabel,
163
+ placeholder: words.commentPlaceholder,
164
+ value: comment,
165
+ maxLength: maxComment,
166
+ multiline: true,
167
+ onChangeText: (text) => setComment(text),
168
+ style: { minHeight: 72, padding: 12, borderRadius: 14, color: textColor, backgroundColor: card.fill, textAlignVertical: 'top' }
169
+ }),
170
+ h(RN.Text, { style: { color: textColor, opacity: 0.55, fontSize: 12, textAlign: 'right' } }, words.counter(Array.from(comment).length, maxComment)),
171
+ h(
172
+ RN.View,
173
+ { style: { flexDirection: 'row', justifyContent: 'flex-end', gap: 12 } },
174
+ h(RN.Pressable, { testID: testID ? `${testID}-cancel` : undefined, accessibilityRole: 'button', accessibilityLabel: words.cancel, onPress: close, style: { paddingHorizontal: 16, paddingVertical: 10 } }, h(RN.Text, { style: { color: textColor } }, words.cancel)),
175
+ h(RN.Pressable, { testID: testID ? `${testID}-send` : undefined, accessibilityRole: 'button', accessibilityLabel: words.submit, onPress: send, style: { paddingHorizontal: 18, paddingVertical: 10, borderRadius: 20, backgroundColor: accent } }, h(RN.Text, { style: { color: '#ffffff', fontWeight: '600' } }, words.submit))
176
+ )
177
+ )
178
+ )
179
+ )
180
+ );
181
+ }
182
+
183
+ module.exports = { FeedbackBar };
package/src/index.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * through untouched.
8
8
  */
9
9
  import type { ComponentType, ReactElement, ReactNode } from 'react';
10
- import type { ColorScheme, GlassMode, PictureSize } from './logic';
10
+ import type { ColorScheme, FeedbackLabels, FeedbackPayload, FeedbackRating, GlassMode, PictureSize } from './logic';
11
11
 
12
12
  export * from './logic';
13
13
 
@@ -343,3 +343,31 @@ export interface ImageViewerProps<P extends ViewerPicture = ViewerPicture> {
343
343
  testID?: string;
344
344
  }
345
345
  export function ImageViewer<P extends ViewerPicture>(props: ImageViewerProps<P>): ReactElement;
346
+
347
+ export interface FeedbackBarProps {
348
+ /** The person's current choice; the bar is controlled. */
349
+ value?: FeedbackRating | null;
350
+ /** A rating to send: `{ rating: 'up' }`, or `{ rating: 'down', reason?, comment? }`. */
351
+ onSubmit?: (payload: FeedbackPayload) => void;
352
+ /** The chosen thumb was pressed again: withdraw the feedback. */
353
+ onRemove?: () => void;
354
+ disabled?: boolean;
355
+ /** Reason ids offered on the sheet. Default: FEEDBACK_REASONS. */
356
+ reasons?: readonly string[];
357
+ maxComment?: number;
358
+ /** Overrides of the default words. */
359
+ labels?: Partial<FeedbackLabels>;
360
+ /** 'fr…' picks the French defaults, anything else English. */
361
+ locale?: string;
362
+ icons?: { up?: ReactNode; down?: ReactNode };
363
+ /** false: silent. A function: your own tick. Default: expo-haptics when installed. */
364
+ haptics?: false | (() => void);
365
+ accent?: string;
366
+ size?: number;
367
+ scheme?: ColorScheme;
368
+ pageBackground?: string;
369
+ ink?: string;
370
+ style?: NativeStyle;
371
+ testID?: string;
372
+ }
373
+ export function FeedbackBar(props: FeedbackBarProps): ReactElement;
package/src/index.js CHANGED
@@ -19,5 +19,6 @@ module.exports = {
19
19
  ...require('./components/ImageShimmer'),
20
20
  ...require('./components/AutoRatioImage'),
21
21
  ...require('./components/ImageViewer'),
22
+ ...require('./components/FeedbackBar'),
22
23
  getGlassMode: require('./components/runtime').getGlassMode
23
24
  };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The rules of the feedback bar (thumbs up / down on an AI answer), dry:
3
+ * what a press does, what is sent, what the screen reader says, which words
4
+ * appear. No react-native here — importable and testable from plain Node.
5
+ *
6
+ * The words are the app's. The defaults (English, French) only keep the bar
7
+ * usable out of the box; every label can be overridden.
8
+ */
9
+
10
+ /** The reasons the server accepts by default, in the order the sheet lists them. */
11
+ const FEEDBACK_REASONS = Object.freeze(['inaccurate', 'incomplete', 'off_topic', 'wrong_source', 'too_long', 'unsafe', 'other']);
12
+
13
+ /** Default comment limit; the server's own limit is the one that counts. */
14
+ const FEEDBACK_COMMENT_MAX = 500;
15
+
16
+ const FEEDBACK_LABELS = Object.freeze({
17
+ en: Object.freeze({
18
+ group: 'Rate this answer',
19
+ up: 'Good answer',
20
+ down: 'Bad answer',
21
+ sheetTitle: 'What went wrong?',
22
+ commentLabel: 'Comment (optional)',
23
+ commentPlaceholder: 'Tell us more (optional)',
24
+ submit: 'Send',
25
+ cancel: 'Cancel',
26
+ reasons: Object.freeze({
27
+ inaccurate: 'Inaccurate',
28
+ incomplete: 'Incomplete',
29
+ off_topic: 'Off topic',
30
+ wrong_source: 'Wrong source',
31
+ too_long: 'Too long',
32
+ unsafe: 'Unsafe',
33
+ other: 'Other'
34
+ }),
35
+ counter: (used, max) => `${used}/${max}`
36
+ }),
37
+ fr: Object.freeze({
38
+ group: 'Évaluer cette réponse',
39
+ up: 'Bonne réponse',
40
+ down: 'Mauvaise réponse',
41
+ sheetTitle: "Qu'est-ce qui n'allait pas ?",
42
+ commentLabel: 'Commentaire (facultatif)',
43
+ commentPlaceholder: 'Dites-nous en plus (facultatif)',
44
+ submit: 'Envoyer',
45
+ cancel: 'Annuler',
46
+ reasons: Object.freeze({
47
+ inaccurate: 'Inexacte',
48
+ incomplete: 'Incomplète',
49
+ off_topic: 'Hors sujet',
50
+ wrong_source: 'Mauvaise source',
51
+ too_long: 'Trop longue',
52
+ unsafe: 'Dangereuse',
53
+ other: 'Autre'
54
+ }),
55
+ counter: (used, max) => `${used}/${max}`
56
+ })
57
+ });
58
+
59
+ /**
60
+ * The labels for a locale (`'fr'`, `'fr-CA'`… → French, anything else →
61
+ * English), with the app's overrides on top. A reason the app adds needs its
62
+ * own label; without one, the id itself is shown.
63
+ */
64
+ function resolveFeedbackLabels({ locale, labels } = {}) {
65
+ const language = typeof locale === 'string' ? locale.toLowerCase().split(/[-_]/)[0] : 'en';
66
+ const base = FEEDBACK_LABELS[language] || FEEDBACK_LABELS.en;
67
+ const given = labels && typeof labels === 'object' ? labels : {};
68
+ const pick = (key) => (typeof given[key] === 'string' && given[key] ? given[key] : base[key]);
69
+ return {
70
+ group: pick('group'),
71
+ up: pick('up'),
72
+ down: pick('down'),
73
+ sheetTitle: pick('sheetTitle'),
74
+ commentLabel: pick('commentLabel'),
75
+ commentPlaceholder: pick('commentPlaceholder'),
76
+ submit: pick('submit'),
77
+ cancel: pick('cancel'),
78
+ reasons: { ...base.reasons, ...(given.reasons && typeof given.reasons === 'object' ? given.reasons : {}) },
79
+ counter: typeof given.counter === 'function' ? given.counter : base.counter
80
+ };
81
+ }
82
+
83
+ /**
84
+ * What pressing a thumb does, given the current choice:
85
+ * the thumb already chosen → withdraw it;
86
+ * thumbs up → send it at once;
87
+ * thumbs down → ask why first.
88
+ */
89
+ function feedbackPress(value, rating) {
90
+ if (rating !== 'up' && rating !== 'down') return { type: 'none' };
91
+ if (value === rating) return { type: 'remove' };
92
+ return rating === 'up' ? { type: 'submit', payload: { rating: 'up' } } : { type: 'ask' };
93
+ }
94
+
95
+ /** The comment cut to the limit, never sliced inside a surrogate pair. */
96
+ function clampComment(text, max = FEEDBACK_COMMENT_MAX) {
97
+ if (typeof text !== 'string') return '';
98
+ const chars = Array.from(text);
99
+ return chars.length <= max ? text : chars.slice(0, max).join('');
100
+ }
101
+
102
+ /**
103
+ * The body sent for a rating. A reason only goes with 'down' and only if the
104
+ * list allows it; a blank comment is dropped; the comment is trimmed and cut.
105
+ */
106
+ function buildFeedbackPayload({ rating, reason, comment, reasons = FEEDBACK_REASONS, maxComment = FEEDBACK_COMMENT_MAX } = {}) {
107
+ if (rating !== 'up' && rating !== 'down') throw new Error("rating must be 'up' or 'down'.");
108
+ const payload = { rating };
109
+ if (rating === 'down' && typeof reason === 'string' && reasons.includes(reason)) payload.reason = reason;
110
+ const text = clampComment(typeof comment === 'string' ? comment.trim() : '', maxComment).trim();
111
+ if (text) payload.comment = text;
112
+ return payload;
113
+ }
114
+
115
+ /** What each thumb tells the screen reader. */
116
+ function feedbackButtonState({ value, rating, labels, disabled = false }) {
117
+ const selected = value === rating;
118
+ return {
119
+ selected,
120
+ accessibilityRole: 'button',
121
+ accessibilityLabel: rating === 'up' ? labels.up : labels.down,
122
+ accessibilityState: { selected, disabled: Boolean(disabled) }
123
+ };
124
+ }
125
+
126
+ /** The sheet's chips: id, visible label, selected or not. */
127
+ function feedbackReasonOptions({ reasons = FEEDBACK_REASONS, labels, selected }) {
128
+ return reasons.map((id) => ({ id, label: labels.reasons[id] || id, selected: id === selected }));
129
+ }
130
+
131
+ module.exports = {
132
+ FEEDBACK_REASONS,
133
+ FEEDBACK_COMMENT_MAX,
134
+ FEEDBACK_LABELS,
135
+ resolveFeedbackLabels,
136
+ feedbackPress,
137
+ clampComment,
138
+ buildFeedbackPayload,
139
+ feedbackButtonState,
140
+ feedbackReasonOptions
141
+ };
@@ -305,3 +305,61 @@ export function countRunningSteps(
305
305
  steps: ReadonlyArray<{ tool: string; state: string } | null> | null | undefined,
306
306
  tool: string | readonly string[]
307
307
  ): number;
308
+
309
+ /* ───────────────────────────── Feedback bar ─────────────────────────── */
310
+
311
+ export type FeedbackRating = 'up' | 'down';
312
+ export const FEEDBACK_REASONS: readonly string[];
313
+ export const FEEDBACK_COMMENT_MAX: number;
314
+ export interface FeedbackLabels {
315
+ group: string;
316
+ up: string;
317
+ down: string;
318
+ sheetTitle: string;
319
+ commentLabel: string;
320
+ commentPlaceholder: string;
321
+ submit: string;
322
+ cancel: string;
323
+ /** Visible name of each reason id; an id without a label is shown as is. */
324
+ reasons: Record<string, string>;
325
+ /** "12/500". */
326
+ counter: (used: number, max: number) => string;
327
+ }
328
+ export interface FeedbackPayload {
329
+ rating: FeedbackRating;
330
+ reason?: string;
331
+ comment?: string;
332
+ }
333
+ /** Default labels, English and French. */
334
+ export const FEEDBACK_LABELS: Readonly<Record<'en' | 'fr', Readonly<FeedbackLabels>>>;
335
+ export function resolveFeedbackLabels(options?: { locale?: string; labels?: Partial<FeedbackLabels> }): FeedbackLabels;
336
+ export type FeedbackPressAction =
337
+ | { type: 'none' }
338
+ | { type: 'remove' }
339
+ | { type: 'ask' }
340
+ | { type: 'submit'; payload: FeedbackPayload };
341
+ export function feedbackPress(value: FeedbackRating | null | undefined, rating: FeedbackRating): FeedbackPressAction;
342
+ export function clampComment(text: unknown, max?: number): string;
343
+ export function buildFeedbackPayload(options: {
344
+ rating: FeedbackRating;
345
+ reason?: string | null;
346
+ comment?: string | null;
347
+ reasons?: readonly string[];
348
+ maxComment?: number;
349
+ }): FeedbackPayload;
350
+ export function feedbackButtonState(options: {
351
+ value: FeedbackRating | null | undefined;
352
+ rating: FeedbackRating;
353
+ labels: FeedbackLabels;
354
+ disabled?: boolean;
355
+ }): {
356
+ selected: boolean;
357
+ accessibilityRole: 'button';
358
+ accessibilityLabel: string;
359
+ accessibilityState: { selected: boolean; disabled: boolean };
360
+ };
361
+ export function feedbackReasonOptions(options: {
362
+ reasons?: readonly string[];
363
+ labels: FeedbackLabels;
364
+ selected?: string | null;
365
+ }): Array<{ id: string; label: string; selected: boolean }>;
@@ -13,5 +13,6 @@ module.exports = {
13
13
  ...require('./markdown'),
14
14
  ...require('./tableColumns'),
15
15
  ...require('./anchorQuestion'),
16
- ...require('./picture')
16
+ ...require('./picture'),
17
+ ...require('./feedbackBar')
17
18
  };