@kubex/zinc 1.1.115 → 1.1.116

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.
@@ -3,86 +3,111 @@ import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from
3
3
  import {HasSlotController} from '../../internal/slot';
4
4
  import {ifDefined} from 'lit/directives/if-defined.js';
5
5
  import {property, state} from 'lit/decorators.js';
6
- import {ResizeController} from '@lit-labs/observers/resize-controller.js';
7
- import ZnButton from '../button';
8
- import ZnButtonGroup from '../button-group';
9
- import ZnDropdown from '../dropdown';
6
+ import ZnChip from '../chip';
10
7
  import ZnHeader from '../header';
11
- import ZnMenu from '../menu';
8
+ import ZnOption from '../option';
12
9
  import ZnPanel from '../panel/panel.component';
13
- import type {ZnMenuSelectEvent} from '../../events/zn-menu-select';
10
+ import ZnSelect from '../select';
14
11
  import type ZnTranslations from '../translations/translations.component';
15
12
 
16
13
  import styles from './translation-group.scss';
17
14
 
18
15
  /**
19
- * @summary A panel-styled container that provides a shared language toggle for multiple zn-translations children.
16
+ * @summary Puts several zn-translations fields behind one language select, so a whole form's worth of copy is
17
+ * translated a language at a time.
18
+ * @documentation https://zinc.style/components/translation-group
19
+ * @status experimental
20
+ * @since 1.0
20
21
  *
21
- * @dependency zn-button
22
- * @dependency zn-button-group
23
- * @dependency zn-dropdown
24
- * @dependency zn-menu
22
+ * The select sits at the top right of the header, opposite the caption. Choosing a language switches every child at
23
+ * once, and each child hides its own select while it is in a group — `grouped` is set on them here.
24
+ *
25
+ * Closed, the select carries how many target languages are done — `1/5`. Its options each carry a chip aggregated
26
+ * across the children:
27
+ *
28
+ * - `Translated` — every child has a value for it
29
+ * - `Partial` — only some children do
30
+ * - `English` — none do, so all of them fall back to the English text
31
+ *
32
+ * `Empty` replaces the last of those for English itself, which has nothing to fall back to. English is the source
33
+ * rather than a translation, so it is also left out of the `n of m translated` count beside the label.
34
+ *
35
+ * The children own their values; this component only chooses which language is shown and reports on what they hold.
36
+ * It reads them back on every child `zn-change`, so the chips and the count follow an edit as it is typed.
37
+ *
38
+ * Extends `zn-panel`, so `caption`, `icon`, `flush`, `transparent` and the `footer` slot behave as they do there.
39
+ * Nested inside another panel, add `inline` to drop the chrome and keep the fields aligned with the surrounding form.
40
+ *
41
+ * @dependency zn-chip
42
+ * @dependency zn-header
43
+ * @dependency zn-option
44
+ * @dependency zn-select
25
45
  *
26
46
  * @event zn-language-change - Emitted when the active language changes. Detail: `{ language: string }`.
27
47
  *
28
- * @slot - Default slot for `<zn-translations>` elements.
29
- * @slot actions - Actions displayed in the panel header alongside language buttons.
48
+ * @slot - The `zn-translations` fields the select drives.
49
+ * @slot actions - Buttons for the bottom of the panel, on the white body rather than the grey footer. They sit on
50
+ * the right, as zinc's form action rows do; `align="start"` moves one to the left. Write them in the order they
51
+ * should be read — the sides are set by CSS ordering, so markup order is what a keyboard follows.
52
+ * @slot footer - Content displayed in the grey panel footer. The header belongs to the language select; nothing
53
+ * else is slotted into it.
30
54
  *
31
55
  * @csspart base - The component's base wrapper.
56
+ * @csspart actions - The row of buttons at the bottom of the body.
57
+ * @csspart language-field - The label and select that choose the language every child is editing.
58
+ * @csspart language-select - The select itself.
32
59
  */
33
60
  export default class ZnTranslationGroup extends ZnPanel {
34
61
  static styles: CSSResultGroup = [ZnPanel.styles, unsafeCSS(styles)];
35
62
  static dependencies = {
36
- 'zn-button': ZnButton,
37
- 'zn-button-group': ZnButtonGroup,
38
- 'zn-dropdown': ZnDropdown,
63
+ 'zn-chip': ZnChip,
39
64
  'zn-header': ZnHeader,
40
- 'zn-menu': ZnMenu
65
+ 'zn-option': ZnOption,
66
+ 'zn-select': ZnSelect
41
67
  };
42
68
 
43
69
  private readonly _slotController = new HasSlotController(this, 'actions', 'footer');
44
70
 
45
- /** The group label displayed in the panel header. */
71
+ /** The caption shown in the panel header. An alias for the inherited `caption`, which wins where both are set. */
46
72
  @property() label = '';
47
73
 
48
- /** The available languages for the group. */
74
+ /**
75
+ * Drops the panel chrome — border, background and padding — so the group reads as a section of the form around it
76
+ * rather than a panel of its own. For groups nested inside another panel, where the fields would otherwise sit
77
+ * indented behind a second border.
78
+ */
79
+ @property({type: Boolean, reflect: true}) inline = false;
80
+
81
+ /**
82
+ * The select's accessible name. Not shown — the caption is what names the section on screen — but read out by a
83
+ * screen reader, which has nothing else to go on once the visible label is gone.
84
+ */
85
+ @property({attribute: 'language-label'}) languageLabel = 'Edit Languages';
86
+
87
+ /**
88
+ * The languages on offer, as language code to display name — `{"en": "English", "fr": "French"}`. Writing the code
89
+ * as the name (`{"en": "EN"}`) is also accepted. `en` is the language every other one falls back to. Set on every
90
+ * child, so they do not need their own copy.
91
+ */
49
92
  @property({type: Object}) languages: Record<string, string> = {
50
93
  'en': 'EN'
51
94
  };
52
95
 
96
+ /** The language every child is currently editing. */
53
97
  @state() private _activeLanguage = 'en';
54
98
 
55
- /** Tracks all language codes that have been activated across children. */
56
- @state() private _activatedLanguages: string[] = ['en'];
57
-
58
- @state() private _overflowIndex = -1;
59
-
60
- private _lastObservedWidth = 0;
61
- private _measureRafId = 0;
62
-
63
- constructor() {
64
- super();
65
- // eslint-disable-next-line no-new
66
- new ResizeController(this, {
67
- callback: entries => {
68
- const width = entries[0]?.contentRect.width ?? 0;
69
- if (Math.abs(width - this._lastObservedWidth) < 1) return;
70
- this._lastObservedWidth = width;
71
- if (this._overflowIndex !== -1) {
72
- this._overflowIndex = -1;
73
- } else {
74
- this._scheduleLangOverflow();
75
- }
76
- },
77
- });
99
+ private _form: HTMLFormElement | null = null;
100
+
101
+ connectedCallback() {
102
+ super.connectedCallback();
103
+ this._form = this.closest('form');
104
+ this._form?.addEventListener('reset', this.handleFormReset);
78
105
  }
79
106
 
80
107
  disconnectedCallback() {
81
108
  super.disconnectedCallback();
82
- if (this._measureRafId) {
83
- cancelAnimationFrame(this._measureRafId);
84
- this._measureRafId = 0;
85
- }
109
+ this._form?.removeEventListener('reset', this.handleFormReset);
110
+ this._form = null;
86
111
  }
87
112
 
88
113
  protected firstUpdated(_changedProperties: PropertyValues) {
@@ -95,81 +120,43 @@ export default class ZnTranslationGroup extends ZnPanel {
95
120
  if (changedProperties.has('languages')) {
96
121
  this.syncChildLanguages();
97
122
  }
98
- this._scheduleLangOverflow();
99
- }
100
-
101
- private _scheduleLangOverflow() {
102
- if (this._measureRafId) return;
103
- this._measureRafId = requestAnimationFrame(() => {
104
- this._measureRafId = 0;
105
- this._computeLangOverflow();
106
- });
107
- }
108
-
109
- private _computeLangOverflow() {
110
- const group = this.shadowRoot?.querySelector<HTMLElement>('.translation-group__languages zn-button-group');
111
- if (!group) return;
112
-
113
- const buttons = Array.from(group.querySelectorAll<HTMLElement>('zn-button[data-lang-btn]'));
114
- if (buttons.length < 2) {
115
- if (this._overflowIndex !== -1) this._overflowIndex = -1;
116
- return;
117
- }
118
-
119
- const firstTop = buttons[0].getBoundingClientRect().top;
120
- let wrapAt = -1;
121
- for (let i = 1; i < buttons.length; i++) {
122
- if (buttons[i].getBoundingClientRect().top > firstTop + 1) {
123
- wrapAt = i;
124
- break;
125
- }
126
- }
127
-
128
- if (wrapAt === -1) {
129
- const trailing = group.querySelector<HTMLElement>('[data-lang-overflow], [data-lang-add]');
130
- if (trailing && trailing.getBoundingClientRect().top > firstTop + 1) {
131
- wrapAt = buttons.length;
132
- }
133
- }
134
-
135
- if (wrapAt === -1) return;
136
-
137
- const newIndex = this._overflowIndex === -1
138
- ? wrapAt
139
- : Math.max(1, Math.min(wrapAt, this._overflowIndex - 1));
140
-
141
- if (newIndex !== this._overflowIndex) {
142
- this._overflowIndex = newIndex;
143
- }
144
123
  }
145
124
 
125
+ /** The children the select drives. Read live rather than cached, so markup added later is picked up. */
146
126
  private getAllTranslations(): ZnTranslations[] {
147
127
  return [...this.querySelectorAll<ZnTranslations>('zn-translations')];
148
128
  }
149
129
 
150
130
  /** Sync grouped state, languages, and active language to all children. */
151
131
  private syncChildren() {
152
- const children = this.getAllTranslations();
153
-
154
- // Collect all language codes from children's existing values
155
- const allLanguageCodes = new Set<string>(this._activatedLanguages);
156
- children.forEach(child => {
157
- child.getValueLanguages().forEach(code => allLanguageCodes.add(code));
158
- });
159
- this._activatedLanguages = [...allLanguageCodes];
160
-
161
- children.forEach(child => {
132
+ this.getAllTranslations().forEach(child => {
162
133
  child.grouped = true;
163
134
  child.languages = this.languages;
164
- // Ensure every activated language exists in each child's values.
165
- // This handles the case where one child has a language that another doesn't.
166
- for (const code of this._activatedLanguages) {
167
- child.addLanguageKey(code);
168
- }
169
135
  child.setActiveLanguage(this._activeLanguage);
170
136
  });
171
137
  }
172
138
 
139
+ /**
140
+ * A language is translated once every child carries a value for it, partial while only some do. The chips and the
141
+ * count are read off the children, so a child's edit has to bring the group back round.
142
+ */
143
+ private languageState(language: string): { type: 'success' | 'warning' | 'error'; label: string } {
144
+ const children = this.getAllTranslations();
145
+ const translated = children.filter(child => child.hasTranslation(language)).length;
146
+
147
+ if (children.length > 0 && translated === children.length) return {type: 'success', label: 'Translated'};
148
+ if (translated > 0) return {type: 'warning', label: 'Partial'};
149
+ return {type: 'error', label: language === 'en' ? 'Empty' : 'English'};
150
+ }
151
+
152
+ /** `English (EN)`, or the code alone where the configured name already is the code. */
153
+ private displayName(language: string): string {
154
+ const name = this.languages[language] ?? language.toUpperCase();
155
+ const code = language.toUpperCase();
156
+ return name.toUpperCase() === code ? name : `${name} (${code})`;
157
+ }
158
+
159
+ /** Children take their language list from the group, so a change to `languages` has to reach them. */
173
160
  private syncChildLanguages() {
174
161
  this.getAllTranslations().forEach(child => {
175
162
  child.languages = this.languages;
@@ -180,74 +167,75 @@ export default class ZnTranslationGroup extends ZnPanel {
180
167
  this.syncChildren();
181
168
  };
182
169
 
170
+ /** Moves every child onto `lang` and announces it. Does not touch their values. */
183
171
  private switchLanguage(lang: string) {
184
172
  this._activeLanguage = lang;
185
173
  this.getAllTranslations().forEach(child => child.setActiveLanguage(lang));
186
174
  this.emit('zn-language-change', {detail: {language: lang}});
187
175
  }
188
176
 
189
- private handleLanguageAdd = (e: ZnMenuSelectEvent) => {
177
+ /**
178
+ * The select's own change and input events describe the language being browsed, not a translation being edited, so
179
+ * they are stopped rather than allowed to reach a consumer listening for a child's value change.
180
+ */
181
+ private handleLanguageSelect = (e: Event) => {
190
182
  e.stopPropagation();
191
- const element = e.detail.element as HTMLElement;
192
- const languageCode = element.getAttribute('data-path');
193
- if (languageCode) {
194
- if (!this._activatedLanguages.includes(languageCode)) {
195
- this._activatedLanguages = [...this._activatedLanguages, languageCode];
196
- }
197
-
198
- // Add language key to all children first so the key exists in values
199
- this.getAllTranslations().forEach(child => child.addLanguageKey(languageCode));
200
- this.switchLanguage(languageCode);
183
+ const language = (e.target as ZnSelect).value;
184
+ if (typeof language === 'string' && language && language !== this._activeLanguage) {
185
+ this.switchLanguage(language);
201
186
  }
202
187
  };
203
188
 
204
- private handleOverflowSelect = (e: ZnMenuSelectEvent) => {
189
+ private handleLanguageInput = (e: Event) => {
205
190
  e.stopPropagation();
206
- const element = e.detail.element as HTMLElement;
207
- const languageCode = element.getAttribute('data-path');
208
- if (languageCode) {
209
- this.switchLanguage(languageCode);
210
- }
191
+ };
192
+
193
+ /** A child's edit changes which chips the select shows, and the translated count above it. */
194
+ private handleChildChange = () => {
195
+ this.requestUpdate();
196
+ };
197
+
198
+ /**
199
+ * The children restore their own values on the form's reset event without announcing it, and the chips and the
200
+ * count are read off them — so re-read once every listener on that event has run.
201
+ */
202
+ private handleFormReset = () => {
203
+ requestAnimationFrame(() => this.requestUpdate());
211
204
  };
212
205
 
213
206
  render() {
214
- const hasActionSlot = this._slotController.test('actions');
207
+ const hasActionsSlot = this._slotController.test('actions');
215
208
  const hasFooterSlot = this._slotController.test('footer');
216
209
  const headerCaption = this.caption || this.label;
217
210
 
218
- const availableLanguages = Object.entries(this.languages)
219
- .filter(([code]) => code !== 'en' && !this._activatedLanguages.includes(code))
220
- .map(([code, name]) => ({
221
- title: name,
222
- type: 'dropdown',
223
- path: code
224
- }));
225
-
226
- const visibleTabs = [...this._activatedLanguages];
227
- if (!visibleTabs.includes('en')) {
228
- visibleTabs.unshift('en');
229
- }
230
-
231
- const overflowCutoff = this._overflowIndex === -1 ? visibleTabs.length : this._overflowIndex;
232
- const visibleLangTabs = visibleTabs.slice(0, overflowCutoff);
233
- const overflowLangTabs = visibleTabs.slice(overflowCutoff);
234
- const overflowActions = overflowLangTabs.map(code => ({
235
- title: code.toUpperCase(),
236
- type: 'dropdown',
237
- path: code,
238
- icon: code === this._activeLanguage ? 'check' : ''
239
- }));
240
-
241
- const hasMultipleLanguages = Object.keys(this.languages).length > 1;
242
- const hasHeader = headerCaption || hasMultipleLanguages || hasActionSlot;
211
+ // A child's value can carry a language `languages` does not list — server-rendered content outliving a config
212
+ // change. Offer those too, or the translation is stranded in the value with no way to reach it.
213
+ const extra = new Set<string>();
214
+ this.getAllTranslations().forEach(child => child.getValueLanguages()
215
+ .filter(code => !Object.prototype.hasOwnProperty.call(this.languages, code))
216
+ .forEach(code => extra.add(code)));
217
+ const languageCodes = [...Object.keys(this.languages), ...extra];
218
+ const hasMultipleLanguages = languageCodes.length > 1;
219
+ const hasHeader = Boolean(headerCaption) || hasMultipleLanguages;
220
+
221
+ // English is the source every other language falls back to, so it is not itself one of the translations counted.
222
+ const targets = languageCodes.filter(code => code !== 'en');
223
+ const translated = targets.filter(code => this.languageState(code).type === 'success').length;
224
+ // Closed, the select answers "how much is left to do" rather than the state of the one language on show — that
225
+ // is what the options are for.
226
+ const summary = {
227
+ label: `${translated}/${targets.length}`,
228
+ type: translated === targets.length ? 'success' : translated > 0 ? 'warning' : 'error'
229
+ };
243
230
 
244
231
  return html`
245
232
  <div class="${classMap({
246
233
  panel: true,
247
- 'panel--flush': this.flush,
248
- 'panel--transparent': this.transparent,
234
+ 'panel--flush': this.flush || this.inline,
235
+ 'panel--transparent': this.transparent || this.inline,
236
+ 'translation-group--inline': this.inline,
249
237
  'panel--has-header': hasHeader,
250
- 'panel--has-actions': hasMultipleLanguages || hasActionSlot,
238
+ 'panel--has-actions': hasActionsSlot,
251
239
  'panel--has-footer': hasFooterSlot,
252
240
  })}">
253
241
 
@@ -257,55 +245,40 @@ export default class ZnTranslationGroup extends ZnPanel {
257
245
  caption="${ifDefined(headerCaption || undefined)}"
258
246
  transparent>
259
247
  ${hasMultipleLanguages ? html`
260
- <div slot="actions" class="translation-group__languages">
261
- <zn-button-group>
262
- ${visibleLangTabs.map(code => html`
263
- <zn-button
264
- data-lang-btn
265
- color="default"
266
- ?outline="${code !== this._activeLanguage}"
267
- @click="${() => this.switchLanguage(code)}"
268
- >${code.toUpperCase()}
269
- </zn-button>
270
- `)}
271
- ${overflowActions.length > 0 ? html`
272
- <zn-dropdown placement="bottom-end" data-lang-overflow>
273
- <zn-button
274
- slot="trigger"
275
- color="default"
276
- icon="keyboard_arrow_down"
277
- ?outline="${!overflowLangTabs.includes(this._activeLanguage)}"
278
- ></zn-button>
279
- <zn-menu
280
- .actions=${overflowActions}
281
- @zn-menu-select="${this.handleOverflowSelect}"
282
- ></zn-menu>
283
- </zn-dropdown>
284
- ` : nothing}
285
- ${availableLanguages.length > 0 ? html`
286
- <zn-dropdown placement="bottom-end" data-lang-add>
287
- <zn-button
288
- slot="trigger"
289
- color="default"
290
- outline
291
- >+
292
- </zn-button>
293
- <zn-menu
294
- .actions=${availableLanguages}
295
- @zn-menu-select="${this.handleLanguageAdd}"
296
- ></zn-menu>
297
- </zn-dropdown>
298
- ` : nothing}
299
- </zn-button-group>
300
- </div>
301
- ` : nothing}
302
- ${hasActionSlot ? html`
303
- <slot name="actions" slot="actions"></slot>` : null}
248
+ <div slot="actions" class="translation-group__language-field" part="language-field">
249
+ <zn-select
250
+ label="${this.languageLabel}"
251
+ class="translation-group__language-select"
252
+ part="language-select"
253
+ hoist
254
+ .value="${this._activeLanguage}"
255
+ @zn-change="${this.handleLanguageSelect}"
256
+ @zn-input="${this.handleLanguageInput}">
257
+ <zn-chip slot="suffix" type="${summary.type}">${summary.label}</zn-chip>
258
+ ${languageCodes.map(code => {
259
+ const optionState = this.languageState(code);
260
+ return html`
261
+ <zn-option value="${code}">
262
+ ${this.displayName(code)}
263
+ <zn-chip slot="suffix" type="${optionState.type}">${optionState.label}</zn-chip>
264
+ </zn-option>`;
265
+ })}
266
+ </zn-select>
267
+ </div>` : nothing}
304
268
  </zn-header>` : null}
305
269
 
306
270
  <div class="panel__content">
307
271
  <div class="panel__body">
308
- <slot @slotchange="${this.handleSlotChange}"></slot>
272
+ <slot
273
+ @slotchange="${this.handleSlotChange}"
274
+ @zn-change="${this.handleChildChange}"></slot>
275
+
276
+ ${hasActionsSlot ? html`
277
+ <div class="translation-group__actions" part="actions">
278
+ <slot name="actions"></slot>
279
+ <span class="translation-group__actions-spacer"></span>
280
+ </div>` : nothing}
281
+
309
282
  </div>
310
283
  </div>
311
284
 
@@ -2,7 +2,130 @@
2
2
  display: block;
3
3
  }
4
4
 
5
- .translation-group__languages {
5
+ // The body stacks the fields the select drives, so they take the row gap zn-form-group sets between stacked
6
+ // controls. The slotted children are `display: contents` through the slot, putting them in this same flex flow.
7
+ .panel__body {
8
+ gap: var(--zn-spacing-medium);
9
+ }
10
+
11
+ // The header sets its own gap down to the first field; the panel's body padding on top of it would make that one gap
12
+ // the odd one out.
13
+ .panel--has-header .panel__body {
14
+ padding-top: 0;
15
+ }
16
+
17
+ .panel__header {
18
+ padding-bottom: var(--zn-spacing-medium);
19
+ }
20
+
21
+ .translation-group--inline .panel__header {
22
+ padding: 0 0 var(--zn-spacing-medium);
23
+ }
24
+
25
+ // zn-header holds its content row at 36px so a caption sits level with action buttons. The select is taller than
26
+ // that and sets the height on its own, so the row hugs whichever of the two is bigger.
27
+ .panel__header::part(content) {
28
+ min-height: 0;
29
+ }
30
+
31
+ // The actions container is a plain block that clips horizontally: its content would sit at the top rather than level
32
+ // with the caption, and a select flush against its right edge would lose the focus ring.
33
+ .panel__header::part(header-right) {
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: flex-end;
37
+ overflow: visible;
38
+ }
39
+
40
+ .translation-group__language-field {
41
+ display: flex;
42
+ align-items: center;
43
+ }
44
+
45
+ // The trigger shows one language, so it sizes to that.
46
+ .translation-group__language-select {
47
+ width: max-content;
48
+ max-width: 100%;
49
+ }
50
+
51
+ // Left alone the display input hands the trigger its default 20-character intrinsic width, however short the language
52
+ // name is. Where field-sizing is unsupported that width is what is left, which only makes the trigger a little wider.
53
+ .translation-group__language-select::part(display-input) {
54
+ width: auto;
55
+ min-width: 0;
56
+ field-sizing: content;
57
+ }
58
+
59
+ // The popup syncs its width to the trigger, which is now sized to the current language rather than the longest one in
60
+ // the list. zn-select runs the popup with auto-size="vertical", so --auto-size-available-width is never set and the
61
+ // listbox's own max-width is inert — this is the only thing keeping a long list on screen.
62
+ .translation-group__language-select::part(listbox) {
63
+ min-width: 320px;
64
+ max-width: calc(100vw - var(--zn-spacing-large));
65
+ }
66
+
67
+ // The select is chrome in the panel header rather than a field to fill in, so it drops the input's fill and shadow.
68
+ // The border stays, marking it out as something to open.
69
+ .translation-group__language-select::part(combobox) {
70
+ background-color: transparent;
71
+ box-shadow: none;
72
+ }
73
+
74
+ .translation-group__language-select:hover::part(combobox) {
75
+ background-color: var(--zn-input-background-color-hover);
76
+ }
77
+
78
+ .translation-group__language-select:focus-within::part(combobox) {
79
+ box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
80
+ }
81
+
82
+ // The select's label is its accessible name only; the caption names the section on screen.
83
+ .translation-group__language-select::part(form-control-label) {
84
+ position: absolute;
85
+ width: 1px;
86
+ height: 1px;
87
+ padding: 0;
88
+ margin: -1px;
89
+ overflow: hidden;
90
+ clip-path: inset(50%);
91
+ white-space: nowrap;
92
+ border: 0;
93
+ }
94
+
95
+ // Matches zinc's form action rows (.form-actions, zn-form-actions): right-aligned, 16px apart.
96
+ .translation-group__actions {
6
97
  display: flex;
7
98
  align-items: center;
99
+ gap: var(--zn-spacing-small);
100
+ }
101
+
102
+ // The spacer, not an auto margin, is what splits the two sides: an auto margin on every right-hand button would open
103
+ // a gap between each of them, where one flexing element in the middle holds any number on either side together.
104
+ .translation-group__actions-spacer {
105
+ order: 1;
106
+ flex: 1 1 auto;
107
+ }
108
+
109
+ ::slotted([slot="actions"]) {
110
+ order: 2;
111
+ }
112
+
113
+ ::slotted([slot="actions"][align="start"]) {
114
+ order: 0;
115
+ }
116
+
117
+ // The panel draws a border between any two body children, and the actions row is the first real element to follow
118
+ // the fields' slot. Matched on .panel--has-actions to out-specify that rule.
119
+ .panel--has-actions .panel__body > .translation-group__actions {
120
+ border: 0;
121
+ }
122
+
123
+ // The panel clips its content to its rounded corners, and the body scrolls. Neither applies with the chrome gone, and
124
+ // both would cut the focus ring off a field now that no padding stands between it and the body's edge.
125
+ .translation-group--inline {
126
+ .panel__inner,
127
+ .panel__content,
128
+ .panel__body {
129
+ overflow: visible;
130
+ }
8
131
  }