@autobusal/common 1.31.0 → 1.33.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/AiAssist/AiFieldAssist.tsx +3 -3
- package/AiAssist/AiReviewModal.tsx +38 -23
- package/CHANGELOG.md +18 -0
- package/File/File.tsx +44 -1
- package/File/styles.ts +18 -2
- package/Viewer/Data.tsx +23 -7
- package/Viewer/styles.ts +18 -1
- package/package.json +1 -1
|
@@ -39,18 +39,18 @@ const AiFieldAssist = ({ text, sourceText, targetLocale, onInsert, t }: Props):
|
|
|
39
39
|
<>
|
|
40
40
|
<Trigger>
|
|
41
41
|
<button type="button" onClick={ () => setMode('generate') }>
|
|
42
|
-
{ t('ai_assist.trigger.generate') }
|
|
42
|
+
{ t('ai_assist.trigger.generate', { ns: 'common' }) }
|
|
43
43
|
</button>
|
|
44
44
|
|
|
45
45
|
{ hasText && (
|
|
46
46
|
<button type="button" onClick={ () => setMode('improve') }>
|
|
47
|
-
{ t('ai_assist.trigger.improve') }
|
|
47
|
+
{ t('ai_assist.trigger.improve', { ns: 'common' }) }
|
|
48
48
|
</button>
|
|
49
49
|
) }
|
|
50
50
|
|
|
51
51
|
{ canTranslate && (
|
|
52
52
|
<button type="button" onClick={ () => setMode('translate') }>
|
|
53
|
-
{ t('ai_assist.trigger.translate') }
|
|
53
|
+
{ t('ai_assist.trigger.translate', { ns: 'common' }) }
|
|
54
54
|
</button>
|
|
55
55
|
) }
|
|
56
56
|
</Trigger>
|
|
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
3
|
import Modal from '../Modal/Modal';
|
|
4
4
|
import { useAiAvailable, useAiGenerate, useAiTranslate, useAiImprove } from './services';
|
|
5
|
-
import { Row, Field, Compare, Pane, Draft, Actions
|
|
5
|
+
import { Row, Field, Compare, Pane, Draft, Actions } from './styles';
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
mode: 'generate' | 'improve' | 'translate'
|
|
@@ -25,6 +25,17 @@ interface Props {
|
|
|
25
25
|
* Ferjolt Ozuni - Date: 2026-08-08
|
|
26
26
|
*/
|
|
27
27
|
const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClose, t }: Props): JSX.Element => {
|
|
28
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-08
|
|
29
|
+
// Every ai_assist.* key lives in the 'common' namespace (obtapi's
|
|
30
|
+
// public/languages/{locale}/common.json), but the `t` this component
|
|
31
|
+
// receives is whatever namespace the CALLING page's own useTranslation()
|
|
32
|
+
// defaulted to (admin-pages/admin-label both default to 'normal') - a
|
|
33
|
+
// bare t('ai_assist.x') looked the key up in the wrong namespace, found
|
|
34
|
+
// nothing, and i18next's fallback is to render the raw key string. Every
|
|
35
|
+
// OTHER cross-namespace lookup in this codebase passes {ns:'common'}
|
|
36
|
+
// per call; this wrapper does it once instead of thirteen times.
|
|
37
|
+
const tc = (key: string): string => t(key, { ns: 'common' });
|
|
38
|
+
|
|
28
39
|
const { data: available, isLoading: loadingAvailable } = useAiAvailable();
|
|
29
40
|
|
|
30
41
|
const providers = Object.keys(available ?? {});
|
|
@@ -33,7 +44,6 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
33
44
|
const [ model, setModel ] = useState<string>('');
|
|
34
45
|
const [ instruction, setInstruction ] = useState<string>('');
|
|
35
46
|
const [ draft, setDraft ] = useState<string | null>(null);
|
|
36
|
-
const [ error, setError ] = useState<string | null>(null);
|
|
37
47
|
|
|
38
48
|
const activeProvider = provider || providers[0] || '';
|
|
39
49
|
const activeModel = model || available?.[activeProvider]?.default_model || '';
|
|
@@ -44,10 +54,17 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
44
54
|
|
|
45
55
|
const running = generating || translating || improving;
|
|
46
56
|
|
|
57
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-08
|
|
58
|
+
// No local onError/error state here - apiClient's own response
|
|
59
|
+
// interceptor already surfaces the real backend message as a toast on
|
|
60
|
+
// any 422 (obtapi's Ai\AdminController returns provider errors that
|
|
61
|
+
// way). A local onError reading a plain react-query Error's .message
|
|
62
|
+
// would only ever see axios's generic "Request failed with status code
|
|
63
|
+
// 422", never the actual "Incorrect API key provided..." the backend
|
|
64
|
+
// sent - found live, the modal showed exactly that generic text while
|
|
65
|
+
// the real message went to the toast every other form in this app
|
|
66
|
+
// already relies on.
|
|
47
67
|
const onRun = (): void => {
|
|
48
|
-
setError(null);
|
|
49
|
-
|
|
50
|
-
const onError = (err: Error): void => setError(err.message);
|
|
51
68
|
const onSuccess = (result: { content: string }): void => {
|
|
52
69
|
setDraft(result.content);
|
|
53
70
|
setInstruction('');
|
|
@@ -58,13 +75,13 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
58
75
|
// endpoint that takes existing text + an instruction, which is exactly
|
|
59
76
|
// what a revision is.
|
|
60
77
|
if (draft !== null) {
|
|
61
|
-
Improve({ provider: activeProvider, model: activeModel, text: draft, instruction }, { onSuccess
|
|
78
|
+
Improve({ provider: activeProvider, model: activeModel, text: draft, instruction }, { onSuccess });
|
|
62
79
|
|
|
63
80
|
return;
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
if (mode === 'generate') {
|
|
67
|
-
Generate({ provider: activeProvider, model: activeModel, prompt: instruction }, { onSuccess
|
|
84
|
+
Generate({ provider: activeProvider, model: activeModel, prompt: instruction }, { onSuccess });
|
|
68
85
|
|
|
69
86
|
return;
|
|
70
87
|
}
|
|
@@ -76,12 +93,12 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
76
93
|
text: text ?? '',
|
|
77
94
|
target_locale: targetLocale ?? '',
|
|
78
95
|
source_locale: sourceLocale
|
|
79
|
-
}, { onSuccess
|
|
96
|
+
}, { onSuccess });
|
|
80
97
|
|
|
81
98
|
return;
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
Improve({ provider: activeProvider, model: activeModel, text: text ?? '', instruction }, { onSuccess
|
|
101
|
+
Improve({ provider: activeProvider, model: activeModel, text: text ?? '', instruction }, { onSuccess });
|
|
85
102
|
};
|
|
86
103
|
|
|
87
104
|
const onInsertClick = (): void => {
|
|
@@ -94,11 +111,9 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
94
111
|
|
|
95
112
|
const content = (
|
|
96
113
|
<>
|
|
97
|
-
{ error && <Error>{ error }</Error> }
|
|
98
|
-
|
|
99
114
|
<Row>
|
|
100
115
|
<Field>
|
|
101
|
-
{
|
|
116
|
+
{ tc('ai_assist.provider') }
|
|
102
117
|
|
|
103
118
|
<select value={ activeProvider } onChange={ event => { setProvider(event.target.value); setModel(''); } }>
|
|
104
119
|
{ providers.map(key => <option key={ key } value={ key }>{ key }</option>) }
|
|
@@ -106,7 +121,7 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
106
121
|
</Field>
|
|
107
122
|
|
|
108
123
|
<Field>
|
|
109
|
-
{
|
|
124
|
+
{ tc('ai_assist.model') }
|
|
110
125
|
|
|
111
126
|
<select value={ activeModel } onChange={ event => setModel(event.target.value) }>
|
|
112
127
|
{ (available?.[activeProvider]?.models ?? []).map(name => <option key={ name } value={ name }>{ name }</option>) }
|
|
@@ -118,14 +133,14 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
118
133
|
<Row>
|
|
119
134
|
<Field>
|
|
120
135
|
{ draft !== null
|
|
121
|
-
?
|
|
122
|
-
: (mode === 'generate' ?
|
|
136
|
+
? tc('ai_assist.revise_instruction')
|
|
137
|
+
: (mode === 'generate' ? tc('ai_assist.generate_prompt') : tc('ai_assist.improve_instruction')) }
|
|
123
138
|
|
|
124
139
|
<input
|
|
125
140
|
type="text"
|
|
126
141
|
value={ instruction }
|
|
127
142
|
onChange={ event => setInstruction(event.target.value) }
|
|
128
|
-
placeholder={ draft !== null ?
|
|
143
|
+
placeholder={ draft !== null ? tc('ai_assist.revise_placeholder') : undefined }
|
|
129
144
|
/>
|
|
130
145
|
</Field>
|
|
131
146
|
</Row>
|
|
@@ -133,7 +148,7 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
133
148
|
|
|
134
149
|
{ draft === null && mode === 'translate' && (
|
|
135
150
|
<Pane>
|
|
136
|
-
<h4>{
|
|
151
|
+
<h4>{ tc('ai_assist.source') }</h4>
|
|
137
152
|
|
|
138
153
|
<div dangerouslySetInnerHTML={ { __html: text ?? '' } } />
|
|
139
154
|
</Pane>
|
|
@@ -142,13 +157,13 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
142
157
|
{ draft !== null && mode === 'translate' && (
|
|
143
158
|
<Compare>
|
|
144
159
|
<Pane>
|
|
145
|
-
<h4>{
|
|
160
|
+
<h4>{ tc('ai_assist.source') }</h4>
|
|
146
161
|
|
|
147
162
|
<div dangerouslySetInnerHTML={ { __html: text ?? '' } } />
|
|
148
163
|
</Pane>
|
|
149
164
|
|
|
150
165
|
<Pane>
|
|
151
|
-
<h4>{
|
|
166
|
+
<h4>{ tc('ai_assist.draft') }</h4>
|
|
152
167
|
|
|
153
168
|
<div dangerouslySetInnerHTML={ { __html: draft } } />
|
|
154
169
|
</Pane>
|
|
@@ -161,16 +176,16 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
161
176
|
|
|
162
177
|
<Actions>
|
|
163
178
|
<button type="button" onClick={ onClose } disabled={ running }>
|
|
164
|
-
{
|
|
179
|
+
{ tc('ai_assist.cancel') }
|
|
165
180
|
</button>
|
|
166
181
|
|
|
167
182
|
<button type="button" onClick={ onRun } disabled={ running || !activeProvider || (mode !== 'translate' && draft === null && instruction.trim() === '') }>
|
|
168
|
-
{ draft !== null ?
|
|
183
|
+
{ draft !== null ? tc('ai_assist.revise') : tc('ai_assist.run') }
|
|
169
184
|
</button>
|
|
170
185
|
|
|
171
186
|
{ draft !== null && (
|
|
172
187
|
<button type="button" onClick={ onInsertClick } disabled={ running }>
|
|
173
|
-
{
|
|
188
|
+
{ tc('ai_assist.insert') }
|
|
174
189
|
</button>
|
|
175
190
|
) }
|
|
176
191
|
</Actions>
|
|
@@ -181,7 +196,7 @@ const AiReviewModal = ({ mode, text, targetLocale, sourceLocale, onInsert, onClo
|
|
|
181
196
|
<Modal
|
|
182
197
|
loading={ loadingAvailable }
|
|
183
198
|
width={ 720 }
|
|
184
|
-
title={
|
|
199
|
+
title={ tc(`ai_assist.title.${ mode }`) }
|
|
185
200
|
content={ content }
|
|
186
201
|
onClose={ onClose }
|
|
187
202
|
/>
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## 1.33.0
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
- **`Viewer`'s `type: 'component'` rows never actually rendered, anywhere.** `Data.tsx` returned `null` for any item whose `name` was `undefined` before the switch statement that handled `'component'` ever ran - and a component row never HAS a name, it isn't a registered form field. Every SectionHeading divider, every `AiFieldAssist` Generate/Improve/Translate trigger, and every description paragraph moved inside a form's own box (SMS/Flex, this cycle) had silently never rendered, in any tab that used one, despite compiling clean and sitting right there in the bundle. Moved the `'component'` case to an early return above the name guard instead of weakening the guard's condition, which would have broken TypeScript's narrowing of `item.name` to `string` for every other case below it.
|
|
9
|
+
- **AI trigger buttons and the review modal rendered raw i18n keys** (`ai_assist.trigger.generate`) instead of translated text. `ai_assist.*` lives in the `common` namespace, but the `t` passed into `AiFieldAssist`/`AiReviewModal` defaults to whatever namespace the calling page's own `useTranslation()` uses (`normal`/`whitelabel`) - every call now passes `{ ns: 'common' }`.
|
|
10
|
+
- **`AiReviewModal` showed a generic "Request failed with status code 422"** instead of the real provider error on a failed generate/translate/improve. Removed its local error state/display entirely - `apiClient`'s existing global response interceptor already toasts `error.response.data.message` on any 422, the same mechanism every other form in the app already relies on; a local `onError` reading react-query's plain `Error.message` could only ever see Axios's generic text, never the backend's.
|
|
11
|
+
|
|
12
|
+
## 1.32.0
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Drag & drop on `File`**, the shared upload control used everywhere in the app - drop a file directly onto it instead of always going through the native picker. The control's own footprint (a single 38px-tall inline button) is unchanged everywhere it is already used; only a border/background highlight appears while dragging. A dropped file is handed to the same hidden `<input>` react-hook-form already registered, via a real dispatched `change` event, so every existing caller's validation/onChange keeps working with no changes on their end.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **A checkbox row with a long notice no longer stretches sideways.** `.row-checkbox`'s label+input are laid out `flex-direction: row-reverse`, and the row's own notice/error text (a plain div) was sharing that same row - a short notice fit fine, but a longer one (two sentences, easily) stretched the row's WIDTH to fit on one line, visibly pushing the checkbox by however long the text happened to be. Every div child of a checkbox row now gets `flex-basis: 100%`, dropping straight onto its own full-width line; the label and input are unaffected.
|
|
21
|
+
|
|
4
22
|
## 1.31.0
|
|
5
23
|
|
|
6
24
|
### Added
|
package/File/File.tsx
CHANGED
|
@@ -19,6 +19,7 @@ interface Props {
|
|
|
19
19
|
const File = ({ name, value, validation, multiple, t, refs }: Props): JSX.Element => {
|
|
20
20
|
const [ uploaded, setUploaded ] = useState<boolean>(false);
|
|
21
21
|
const [ preview, setPreview ] = useState<boolean>(false);
|
|
22
|
+
const [ dragging, setDragging ] = useState<boolean>(false);
|
|
22
23
|
|
|
23
24
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
24
25
|
|
|
@@ -46,11 +47,53 @@ const File = ({ name, value, validation, multiple, t, refs }: Props): JSX.Elemen
|
|
|
46
47
|
}
|
|
47
48
|
};
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Ferjolt Ozuni - Date: 2026-08-08
|
|
52
|
+
*
|
|
53
|
+
* A dropped file has to land on the actual hidden <input> - it is the
|
|
54
|
+
* one react-hook-form registered via refs(name, rules), and that
|
|
55
|
+
* registration is what onSave ultimately reads. The DataTransfer/
|
|
56
|
+
* dispatchEvent pair is the standard way to hand a File to a native
|
|
57
|
+
* file input programmatically: assigning to `.files` directly is
|
|
58
|
+
* read-only on the input itself, but an input accepts a `FileList`
|
|
59
|
+
* built via DataTransfer, and dispatching a real 'change' event (not
|
|
60
|
+
* calling the onChange prop directly) is what makes React's own
|
|
61
|
+
* synthetic-event system - and therefore RHF's registration - see it,
|
|
62
|
+
* exactly as if the browser's own file picker had fired it.
|
|
63
|
+
*/
|
|
64
|
+
const onDrop = (event: React.DragEvent<HTMLDivElement>): void => {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
setDragging(false);
|
|
67
|
+
|
|
68
|
+
const dropped = event.dataTransfer.files;
|
|
69
|
+
|
|
70
|
+
if (!inputRef.current || dropped.length === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const transfer = new DataTransfer();
|
|
75
|
+
|
|
76
|
+
Array.from(multiple ? dropped : [ dropped[0] ]).forEach(file => transfer.items.add(file));
|
|
77
|
+
|
|
78
|
+
inputRef.current.files = transfer.files;
|
|
79
|
+
inputRef.current.dispatchEvent(new Event('change', { bubbles: true }));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const onDragOver = (event: React.DragEvent<HTMLDivElement>): void => {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
setDragging(true);
|
|
85
|
+
};
|
|
86
|
+
|
|
49
87
|
const type = multiple ? 'multiple' : 'single';
|
|
50
88
|
|
|
51
89
|
return (
|
|
52
90
|
<>
|
|
53
|
-
<Container
|
|
91
|
+
<Container
|
|
92
|
+
$dragging={ dragging }
|
|
93
|
+
onDragOver={ onDragOver }
|
|
94
|
+
onDragLeave={ () => setDragging(false) }
|
|
95
|
+
onDrop={ onDrop }
|
|
96
|
+
>
|
|
54
97
|
<input
|
|
55
98
|
{ ...rest }
|
|
56
99
|
type="file"
|
package/File/styles.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
import styled from 'styled-components';
|
|
1
|
+
import styled, { css } from 'styled-components';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Ferjolt Ozuni - Date: 2026-08-08
|
|
5
|
+
* `$dragging` only changes the border/background while a file is being
|
|
6
|
+
* dragged over it - the box itself stays the same 38px-tall inline
|
|
7
|
+
* control everywhere this component is already used (tables, dense
|
|
8
|
+
* forms), rather than growing into a big dropzone that would only fit
|
|
9
|
+
* some of its callers.
|
|
10
|
+
*/
|
|
11
|
+
export const Container = styled.div<{ $dragging?: boolean }>`
|
|
4
12
|
display: flex;
|
|
5
13
|
align-items: center;
|
|
6
14
|
justify-content: space-between;
|
|
7
15
|
gap: 10px;
|
|
8
16
|
height: 38px;
|
|
17
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
18
|
+
transition: background 0.15s ease;
|
|
19
|
+
|
|
20
|
+
${ props => props.$dragging && css`
|
|
21
|
+
background: ${ props.theme.primary.neutral };
|
|
22
|
+
outline: 2px dashed ${ props.theme.primary.normal };
|
|
23
|
+
outline-offset: 2px;
|
|
24
|
+
` }
|
|
9
25
|
`;
|
|
10
26
|
|
|
11
27
|
export const ImgView = styled.img`
|
package/Viewer/Data.tsx
CHANGED
|
@@ -21,6 +21,29 @@ interface Props {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
|
|
24
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-08
|
|
25
|
+
// A 'component' row is not a registered form field - it never had a
|
|
26
|
+
// name to give, and the name-required guard below (meant to stop every
|
|
27
|
+
// OTHER type from registering with react-hook-form under an undefined
|
|
28
|
+
// key) was silently returning null for every one of them before ever
|
|
29
|
+
// reaching the 'component' case that used to sit in the switch further
|
|
30
|
+
// down. Found live: a whole category of content - SectionHeading
|
|
31
|
+
// dividers, AiFieldAssist's Generate/Improve/Translate triggers,
|
|
32
|
+
// description text moved inside a form's own box - had never actually
|
|
33
|
+
// rendered, in any of the tabs that used it, despite compiling clean
|
|
34
|
+
// and being right there in the production bundle. Handled here, before
|
|
35
|
+
// the name guard, rather than by weakening that guard's condition -
|
|
36
|
+
// TypeScript narrows item.name to a definite string for every case
|
|
37
|
+
// below FROM the guard, and a conditional guard cannot narrow it the
|
|
38
|
+
// same way.
|
|
39
|
+
if (item.type === 'component') {
|
|
40
|
+
return (
|
|
41
|
+
<div className="row-data">
|
|
42
|
+
{ typeof item.value === 'function' ? item.value(onUpdate) : item.value }
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
24
47
|
if (item.name === undefined) {
|
|
25
48
|
return null;
|
|
26
49
|
}
|
|
@@ -65,13 +88,6 @@ const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
|
|
|
65
88
|
/>
|
|
66
89
|
);
|
|
67
90
|
|
|
68
|
-
case 'component':
|
|
69
|
-
return (
|
|
70
|
-
<div className="row-data">
|
|
71
|
-
{ typeof item.value === 'function' ? item.value(onUpdate) : item.value }
|
|
72
|
-
</div>
|
|
73
|
-
);
|
|
74
|
-
|
|
75
91
|
case 'display':
|
|
76
92
|
return <Display className="row-data">{ value.string }</Display>;
|
|
77
93
|
|
package/Viewer/styles.ts
CHANGED
|
@@ -3,8 +3,25 @@ import styled from 'styled-components';
|
|
|
3
3
|
export const Container = styled.div`
|
|
4
4
|
& > div.row-checkbox {
|
|
5
5
|
flex-direction: row-reverse;
|
|
6
|
+
flex-wrap: wrap;
|
|
6
7
|
justify-content: left;
|
|
7
|
-
height: 20px;
|
|
8
|
+
min-height: 20px;
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-08
|
|
12
|
+
* A checkbox row's own notice/error text is a DIV (Notice, Item's
|
|
13
|
+
* error Display) sharing this row's flex layout with the label
|
|
14
|
+
* (a span) and the checkbox input - row-reverse was fine for just
|
|
15
|
+
* those two, but any row with a real notice attached (Construction's
|
|
16
|
+
* "enabled" checkbox is two full sentences) stretched the row's WIDTH
|
|
17
|
+
* to fit the text on one line instead of wrapping it, visibly pushing
|
|
18
|
+
* the checkbox sideways by however long the notice happened to be.
|
|
19
|
+
* Forcing every div child onto its own full-width line - the label
|
|
20
|
+
* and the input stay row-reversed exactly as before.
|
|
21
|
+
*/
|
|
22
|
+
& > div {
|
|
23
|
+
flex-basis: 100%;
|
|
24
|
+
}
|
|
8
25
|
}
|
|
9
26
|
|
|
10
27
|
@media (min-width: 540px) {
|