@pgcorp/ui-kit 0.1.1 → 0.2.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.
Files changed (27) hide show
  1. package/package.json +6 -2
  2. package/src/components/layout/SSideMenuButton.vue +5 -4
  3. package/src/components/layout/SSideMenuRail.vue +3 -6
  4. package/src/components/shared/_internal/STreeNode.vue +10 -4
  5. package/src/components/shared/_internal/sidebarGroupContext.ts +3 -1
  6. package/src/components/shared/_internal/tabsContext.ts +1 -0
  7. package/src/components/shared/containers/SLeftSidebar.vue +2 -2
  8. package/src/components/shared/containers/SPanel.css +7 -1
  9. package/src/components/shared/containers/SPanel.vue +5 -1
  10. package/src/components/shared/containers/SSidebarGroup.css +1 -1
  11. package/src/components/shared/containers/SSidebarGroup.vue +113 -94
  12. package/src/components/shared/containers/SSidebarSection.css +16 -4
  13. package/src/components/shared/containers/SSidebarSection.vue +15 -16
  14. package/src/components/shared/controls/SButton.css +26 -0
  15. package/src/components/shared/controls/SButton.vue +16 -0
  16. package/src/components/shared/controls/SContextToggleButton.vue +1 -1
  17. package/src/components/shared/controls/SInteractiveSurface.css +4 -0
  18. package/src/components/shared/controls/SInteractiveSurface.vue +4 -0
  19. package/src/components/shared/controls/SRange.css +35 -0
  20. package/src/components/shared/controls/SRange.vue +129 -0
  21. package/src/components/shared/data-display/SCodeBlock.vue +1 -1
  22. package/src/components/shared/data-display/SCodeEditor.css +15 -0
  23. package/src/components/shared/data-display/SCodeEditor.vue +14 -0
  24. package/src/components/shared/navigation/STab.vue +31 -6
  25. package/src/components/shared/navigation/STabs.vue +8 -2
  26. package/src/internal/codeEditorContract.ts +1 -1
  27. package/src/internal/ownedAttrs.ts +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgcorp/ui-kit",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Typed Vue 3 design system with accessible components, semantic themes, and workbench patterns.",
5
5
  "type": "module",
6
6
  "types": "./index.ts",
@@ -252,6 +252,9 @@
252
252
  "shared/controls/SMenuSurface.vue": [
253
253
  "src/components/shared/controls/SMenuSurface.vue"
254
254
  ],
255
+ "shared/controls/SRange.vue": [
256
+ "src/components/shared/controls/SRange.vue"
257
+ ],
255
258
  "shared/controls/SSegmentedControl.vue": [
256
259
  "src/components/shared/controls/SSegmentedControl.vue"
257
260
  ],
@@ -480,6 +483,7 @@
480
483
  "./shared/controls/SListbox.vue": "./src/components/shared/controls/SListbox.vue",
481
484
  "./shared/controls/SMarker.vue": "./src/components/shared/controls/SMarker.vue",
482
485
  "./shared/controls/SMenuSurface.vue": "./src/components/shared/controls/SMenuSurface.vue",
486
+ "./shared/controls/SRange.vue": "./src/components/shared/controls/SRange.vue",
483
487
  "./shared/controls/SSegmentedControl.vue": "./src/components/shared/controls/SSegmentedControl.vue",
484
488
  "./shared/controls/SSelect.vue": "./src/components/shared/controls/SSelect.vue",
485
489
  "./shared/controls/STextarea.vue": "./src/components/shared/controls/STextarea.vue",
@@ -549,7 +553,7 @@
549
553
  "@codemirror/theme-one-dark": "^6.1.3",
550
554
  "@codemirror/view": "^6.39.15",
551
555
  "@uiw/codemirror-extensions-langs": "^4.25.8",
552
- "codemirror": "^6.0.2",
556
+ "codemirror": "6.0.2",
553
557
  "codemirror-lang-makefile": "^0.1.1",
554
558
  "dompurify": "^3.4.12",
555
559
  "graphology": "^0.26.0",
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="{ 'mt-2': offsetTop }">
2
+ <div class="w-full" :class="{ 'mt-2': offsetTop }">
3
3
  <SDropdownMenu
4
4
  v-if="contextMenuItems.length && contextMenuTrigger === 'click'"
5
5
  v-slot="{ triggerAttrs }"
@@ -93,12 +93,13 @@ const buttonProps = computed(() => {
93
93
  'contentAlign': props.showLabel ? 'start' as const : 'center' as const,
94
94
  'disabled': props.disabled,
95
95
  'iconOnly': !props.showLabel,
96
+ 'iconSize': 'large' as const,
96
97
  'label': props.showLabel ? props.title : undefined,
97
98
  'leadingIcon': props.icon,
98
99
  'severity': props.active && !props.disabled ? 'primary' as const : 'secondary' as const,
99
- 'shape': props.showLabel ? 'row' as const : 'circle' as const,
100
- 'size': 'md' as const,
101
- 'width': props.showLabel ? 'full' as const : 'auto' as const,
100
+ 'shape': 'row' as const,
101
+ 'size': props.showLabel ? 'md' as const : 'lg' as const,
102
+ 'width': 'full' as const,
102
103
  }
103
104
  })
104
105
  </script>
@@ -3,16 +3,13 @@
3
3
  v-if="visible"
4
4
  v-bind="ownedAttrs.bindings()"
5
5
  class="flex h-full min-h-0 flex-shrink-0 flex-col gap-1 overflow-x-hidden overflow-y-auto border-r border-surface-200 bg-surface-0 p-1 transition-[width] duration-[var(--s-motion-fast)] dark:border-surface-800 dark:bg-surface-950"
6
- :class="expanded ? 'w-56 items-stretch' : 'w-12 items-center'"
6
+ :class="expanded ? 'w-56 items-stretch' : 'w-14 items-stretch'"
7
7
  >
8
- <div class="flex flex-col gap-1" :class="expanded ? 'items-stretch' : 'items-center'">
8
+ <div class="flex flex-col items-stretch gap-1">
9
9
  <slot name="top" />
10
10
  </div>
11
11
 
12
- <div
13
- class="mt-auto flex flex-col gap-1"
14
- :class="expanded ? 'items-stretch' : 'items-center'"
15
- >
12
+ <div class="mt-auto flex flex-col items-stretch gap-1">
16
13
  <slot name="bottom" />
17
14
  </div>
18
15
  </div>
@@ -689,24 +689,30 @@ onBeforeUnmount(() => {
689
689
  }
690
690
 
691
691
  .s-tree-node-wrapper--compact .s-tree-node {
692
- min-height: 1.75rem;
692
+ min-height: var(--s-control-hitbox-xs);
693
693
  gap: 0.25rem;
694
694
  padding: 0 0.25rem;
695
695
  }
696
696
 
697
+ .s-tree-node-children {
698
+ margin: 0;
699
+ padding: 0;
700
+ list-style: none;
701
+ }
702
+
697
703
  .s-tree-node-wrapper--overlay-actions .s-tree-node {
698
704
  position: relative;
699
705
  display: grid;
700
706
  overflow: hidden;
701
- grid-template-columns: minmax(0, 1fr) max-content;
702
- gap: 1.25rem;
707
+ grid-template-columns: minmax(0, 1fr) max-content max-content;
708
+ column-gap: 1.25rem;
703
709
  padding-right: 0.78rem;
704
710
  }
705
711
 
706
712
  .s-tree-node-wrapper--overlay-actions .s-tree-node-right {
707
713
  position: relative;
708
714
  z-index: var(--s-layer-sticky);
709
- grid-column: 2;
715
+ grid-column: 3;
710
716
  justify-content: flex-end;
711
717
  }
712
718
 
@@ -1,4 +1,4 @@
1
- import type { ComputedRef, InjectionKey } from 'vue';
1
+ import type { ComputedRef, InjectionKey, Ref } from 'vue';
2
2
 
3
3
  import type { SectionState, SidebarSectionsState } from '../containers/sidebar';
4
4
 
@@ -20,9 +20,11 @@ export type SidebarSectionStyle = Readonly<Record<string, string | number>>;
20
20
  export interface SidebarGroupContext {
21
21
  readonly sectionsState: ComputedRef<SidebarSectionsState>;
22
22
  readonly orderedSections: ComputedRef<readonly RegisteredSidebarSection[]>;
23
+ readonly geometryRevision: Readonly<Ref<number>>;
23
24
  registerSection(registration: SidebarSectionRegistration): () => void;
24
25
  toggleSection(key: string): void;
25
26
  getSectionStyle(key: string): SidebarSectionStyle;
27
+ getSectionSize(key: string): number;
26
28
  beginResize(topKey: string, bottomKey: string): void;
27
29
  resizeSections(topKey: string, bottomKey: string, requestedTopSize: number): void;
28
30
  finishResize(topKey: string, bottomKey: string): void;
@@ -35,6 +35,7 @@ export interface STabsContext {
35
35
  readonly activeKey: ComputedRef<string | null>
36
36
  readonly activationMode: ComputedRef<STabsActivationMode>
37
37
  readonly closeButtonMode: ComputedRef<'reserve' | 'overlay'>
38
+ readonly closeButtonVisibility: ComputedRef<'always' | 'active-hover' | 'hover'>
38
39
  readonly tabDensity: ComputedRef<'compact' | 'default' | 'comfortable'>
39
40
  readonly registerTab: (registration: STabRegistration) => STabRegistrationKey
40
41
  readonly unregisterTab: (key: STabRegistrationKey) => void
@@ -45,7 +45,7 @@ export interface Props {
45
45
  state?: SAsyncStateContract;
46
46
  sectionsState: SidebarSectionsState;
47
47
  layout?: 'desktop' | 'mobile';
48
- scrollBehavior?: 'auto' | 'body' | 'self';
48
+ scrollBehavior?: 'auto' | 'body' | 'self' | 'child';
49
49
  }
50
50
 
51
51
  withDefaults(defineProps<Props>(), {
@@ -53,7 +53,7 @@ withDefaults(defineProps<Props>(), {
53
53
  icon: undefined,
54
54
  state: () => ({ status: 'ready' }),
55
55
  layout: 'desktop',
56
- scrollBehavior: 'auto',
56
+ scrollBehavior: 'child',
57
57
  });
58
58
 
59
59
  const emit = defineEmits<SidebarGroupEmits>();
@@ -105,7 +105,7 @@
105
105
  pointer-events: none;
106
106
  }
107
107
 
108
- .s-panel-content[data-grow='true'] > * {
108
+ .s-panel-content[data-grow='true']:not(.s-panel-content--scroll-child) > * {
109
109
  flex-shrink: 0;
110
110
  }
111
111
 
@@ -121,6 +121,12 @@
121
121
  overflow: hidden;
122
122
  }
123
123
 
124
+ .s-panel-content[data-grow='true'].s-panel-content--scroll-child > :not(.s-panel-scroll-end-spacer) {
125
+ flex: 1 1 0%;
126
+ min-height: 0;
127
+ overflow: hidden;
128
+ }
129
+
124
130
  .s-panel-footer {
125
131
  @apply flex min-h-8 min-w-0 flex-wrap items-center border-t border-surface-200 px-3 py-1.5 text-xs text-surface-500;
126
132
  @apply dark:border-surface-700 dark:text-surface-400;
@@ -65,7 +65,11 @@
65
65
  </template>
66
66
  <slot />
67
67
  </SAsyncState>
68
- <div v-if="grow" class="s-panel-scroll-end-spacer" aria-hidden="true"></div>
68
+ <div
69
+ v-if="grow && scrollBehavior !== 'child'"
70
+ class="s-panel-scroll-end-spacer"
71
+ aria-hidden="true"
72
+ ></div>
69
73
  </div>
70
74
  <div v-if="$slots.footer" :id="validatedFooterId" class="s-panel-footer">
71
75
  <slot name="footer" />
@@ -1,6 +1,7 @@
1
1
  @reference "../../../styles/reference.css";
2
2
 
3
3
  .s-sidebar-group {
4
+ --s-sidebar-collapsed-section-height: 1.75rem;
4
5
  @apply w-full;
5
6
  @apply flex flex-col;
6
7
  @apply min-h-0;
@@ -9,4 +10,3 @@
9
10
  }
10
11
 
11
12
  /* Этот файл определяет базовый flex-контейнер для группы секций. */
12
-
@@ -43,9 +43,10 @@ const emit = defineEmits<SidebarGroupEmits>();
43
43
  const ownedAttrs = useOwnedAttrs({ component: 'SSidebarGroup', owner: 'sidebar group root' });
44
44
  const groupRef = ref<HTMLDivElement | null>(null);
45
45
  const registrations = shallowRef<readonly SidebarSectionRegistration[]>([]);
46
+ const effectiveSizes = new Map<string, number>();
46
47
  let resizeObserver: ResizeObserver | null = null;
47
48
  let mounted = false;
48
- let geometryRevision = 0;
49
+ const geometryRevision = ref(0);
49
50
  let activeResizePair: Readonly<{ topKey: string; bottomKey: string }> | null = null;
50
51
 
51
52
  assertSidebarSectionsState(props.sectionsState);
@@ -122,65 +123,74 @@ function emitTransition(replacements: readonly SidebarSectionReplacement[]): voi
122
123
  }
123
124
 
124
125
  function scheduleGeometryReconciliation(): void {
125
- const revision = ++geometryRevision;
126
+ const revision = ++geometryRevision.value;
126
127
  queueMicrotask(() => {
127
- if (!mounted || revision !== geometryRevision || activeResizePair !== null) return;
128
+ if (!mounted || revision !== geometryRevision.value || activeResizePair !== null) return;
128
129
  reconcileContainerGeometry();
129
130
  });
130
131
  }
131
132
 
133
+ function fitOpenSectionSizes(
134
+ openSections: readonly RegisteredSidebarSection[],
135
+ availableSize: number,
136
+ ): ReadonlyMap<string, number> {
137
+ const minimumTotal = openSections.reduce((sum, section) => sum + section.state.minSize, 0);
138
+ const targetSize = Math.max(minimumTotal, availableSize);
139
+ const flexible = openSections.find((section) => section.state.size === null);
140
+ const sizes = new Map<string, number>();
141
+
142
+ for (const section of openSections) {
143
+ const remembered = effectiveSizes.get(section.key);
144
+ const preferred = remembered ?? section.state.size ?? section.state.minSize;
145
+ sizes.set(section.key, Math.max(section.state.minSize, preferred));
146
+ }
147
+
148
+ let delta = targetSize - [...sizes.values()].reduce((sum, size) => sum + size, 0);
149
+ if (delta > 0) {
150
+ const growTarget = flexible ?? openSections.at(-1);
151
+ if (growTarget) sizes.set(growTarget.key, sizes.get(growTarget.key)! + delta);
152
+ delta = 0;
153
+ }
154
+
155
+ if (delta < 0) {
156
+ const flexibleIndex = flexible
157
+ ? openSections.findIndex((section) => section.key === flexible.key)
158
+ : openSections.length - 1;
159
+ const shrinkOrder = openSections
160
+ .map((section, index) => ({ section, distance: Math.abs(index - flexibleIndex) }))
161
+ .sort((left, right) => left.distance - right.distance);
162
+ let overflow = -delta;
163
+ for (const { section } of shrinkOrder) {
164
+ if (overflow <= 0) break;
165
+ const size = sizes.get(section.key)!;
166
+ const reduction = Math.min(size - section.state.minSize, overflow);
167
+ sizes.set(section.key, size - reduction);
168
+ overflow -= reduction;
169
+ }
170
+ }
171
+
172
+ return sizes;
173
+ }
174
+
132
175
  function reconcileContainerGeometry(): void {
133
176
  const containerHeight = measuredContainerHeight();
134
177
  if (containerHeight === 0) return;
135
178
  const sections = orderedSections.value;
136
179
  const openSections = sections.filter((section) => section.state.isOpen);
137
180
  if (openSections.length === 0) return;
138
- const flexibleIndex = openSections.findIndex((section) => section.state.size === null);
139
- if (flexibleIndex < 0) {
140
- throw new Error('SSidebarGroup: controlled state has no flexible open section');
141
- }
142
-
143
181
  const closedHeight = sections
144
182
  .filter((section) => !section.state.isOpen)
145
183
  .reduce((sum, section) => (
146
184
  sum + measuredHeight(section.element, `sections.${section.key}`)
147
185
  ), 0);
148
- const fixedSections = openSections.filter((_, index) => index !== flexibleIndex);
149
- const fixedHeight = fixedSections.reduce((sum, section) => {
150
- if (section.state.size === null) {
151
- throw new Error(`SSidebarGroup: section '${section.key}' unexpectedly owns flexible size`);
152
- }
153
- return sum + section.state.size;
154
- }, 0);
155
- const flexible = openSections[flexibleIndex]!;
156
- let overflow = closedHeight + fixedHeight + flexible.state.minSize - containerHeight;
157
- if (overflow <= 0) return;
158
-
159
- const orderedCandidates = openSections
160
- .map((section, index) => ({ section, index }))
161
- .filter(({ index }) => index !== flexibleIndex)
162
- .map(({ section, index }) => ({
163
- section,
164
- distance: Math.abs(index - flexibleIndex),
165
- }))
166
- .sort((left, right) => left.distance - right.distance);
167
- const replacements: SidebarSectionReplacement[] = [];
168
- for (const candidate of orderedCandidates) {
169
- if (overflow <= 0) break;
170
- const size = candidate.section.state.size;
171
- if (size === null) {
172
- throw new Error(`SSidebarGroup: section '${candidate.section.key}' unexpectedly owns flexible size`);
173
- }
174
- const available = size - candidate.section.state.minSize;
175
- if (available <= 0) continue;
176
- const reduction = Math.min(available, overflow);
177
- replacements.push({
178
- key: candidate.section.key,
179
- state: { ...candidate.section.state, size: size - reduction },
180
- });
181
- overflow -= reduction;
186
+ const nextSizes = fitOpenSectionSizes(openSections, containerHeight - closedHeight);
187
+ let changed = false;
188
+ for (const section of openSections) {
189
+ const nextSize = nextSizes.get(section.key)!;
190
+ if (effectiveSizes.get(section.key) !== nextSize) changed = true;
191
+ effectiveSizes.set(section.key, nextSize);
182
192
  }
183
- if (replacements.length > 0) emitTransition(replacements);
193
+ if (changed) geometryRevision.value += 1;
184
194
  }
185
195
 
186
196
  function registerSection(registration: SidebarSectionRegistration): () => void {
@@ -284,27 +294,28 @@ function toggleSection(key: string): void {
284
294
  }
285
295
 
286
296
  function getSectionStyle(key: string): SidebarSectionStyle {
297
+ void geometryRevision.value;
287
298
  const state = currentState()[key];
288
299
  if (!state) throw new Error(`SSidebarGroup: cannot style unknown section '${key}'`);
289
300
  if (!state.isOpen) {
290
301
  return Object.freeze({ flexGrow: 0, flexShrink: 0, flexBasis: 'auto', overflow: 'hidden' });
291
302
  }
292
- if (state.size !== null) {
293
- return Object.freeze({
294
- flexBasis: `${state.size}px`,
295
- flexGrow: 0,
296
- flexShrink: 0,
297
- minHeight: `${state.minSize}px`,
298
- });
299
- }
303
+ const effectiveSize = effectiveSizes.get(key) ?? state.size ?? state.minSize;
300
304
  return Object.freeze({
301
- flexGrow: 1,
302
- flexShrink: 1,
303
- flexBasis: '0%',
304
- minHeight: `${state.minSize}px`,
305
+ flexGrow: 0,
306
+ flexShrink: 0,
307
+ flexBasis: `${effectiveSize}px`,
308
+ minHeight: 'var(--s-sidebar-collapsed-section-height)',
305
309
  });
306
310
  }
307
311
 
312
+ function getSectionSize(key: string): number {
313
+ void geometryRevision.value;
314
+ const section = registeredSection(key);
315
+ if (!section.state.isOpen) return measuredHeight(section.element, `sections.${key}`);
316
+ return effectiveSizes.get(key) ?? section.state.size ?? section.state.minSize;
317
+ }
318
+
308
319
  function resizePair(
309
320
  topKey: string,
310
321
  bottomKey: string,
@@ -336,7 +347,7 @@ function beginResize(topKey: string, bottomKey: string): void {
336
347
  );
337
348
  }
338
349
  activeResizePair = Object.freeze({ topKey, bottomKey });
339
- geometryRevision += 1;
350
+ geometryRevision.value += 1;
340
351
  }
341
352
 
342
353
  function resizeSections(topKey: string, bottomKey: string, requestedTopSize: number): void {
@@ -349,45 +360,51 @@ function resizeSections(topKey: string, bottomKey: string, requestedTopSize: num
349
360
  ) {
350
361
  throw new Error('SSidebarGroup: resize update does not match the active section pair');
351
362
  }
352
- const [top, bottom] = resizePair(topKey, bottomKey);
353
- const topHeight = Math.max(
354
- top.state.minSize,
355
- measuredHeight(top.element, `sections.${top.key}`),
356
- );
357
- const bottomHeight = Math.max(
358
- bottom.state.minSize,
359
- measuredHeight(bottom.element, `sections.${bottom.key}`),
363
+ resizePair(topKey, bottomKey);
364
+ const openSections = orderedSections.value.filter((section) => section.state.isOpen);
365
+ const topOpenIndex = openSections.findIndex((section) => section.key === topKey);
366
+ const upper = openSections.slice(0, topOpenIndex + 1);
367
+ const lower = openSections.slice(topOpenIndex + 1);
368
+ const currentUpperSize = upper.reduce(
369
+ (sum, section) => sum + (effectiveSizes.get(section.key) ?? measuredHeight(section.element, `sections.${section.key}`)),
370
+ 0,
360
371
  );
361
- const minimumPairSize = top.state.minSize + bottom.state.minSize;
362
- const measuredPairSize = Math.max(minimumPairSize, topHeight + bottomHeight);
363
- const containerHeight = measuredContainerHeight();
364
- const occupiedByOthers = orderedSections.value.reduce((sum, section) => {
365
- if (section.key === topKey || section.key === bottomKey) return sum;
366
- return sum + measuredHeight(section.element, `sections.${section.key}`);
367
- }, 0);
368
- const availablePairSize = containerHeight > 0
369
- ? Math.max(minimumPairSize, containerHeight - occupiedByOthers)
370
- : measuredPairSize;
371
- const pairSize = Math.max(minimumPairSize, Math.min(measuredPairSize, availablePairSize));
372
- const maximumTopSize = pairSize - bottom.state.minSize;
373
- const nextTopSize = Math.min(maximumTopSize, Math.max(top.state.minSize, requestedTopSize));
374
- const nextBottomSize = pairSize - nextTopSize;
375
- const flexibleOwner = top.state.size === null ? bottom.key : (
376
- bottom.state.size === null ? bottom.key : null
372
+ const delta = requestedTopSize - currentUpperSize;
373
+ const shrinking = delta > 0 ? [...lower] : [...upper].reverse();
374
+ const growing = delta > 0 ? upper.at(-1) : lower[0];
375
+ let transferable = Math.abs(delta);
376
+ transferable = Math.min(
377
+ transferable,
378
+ shrinking.reduce((sum, section) => {
379
+ const size = effectiveSizes.get(section.key) ?? measuredHeight(section.element, `sections.${section.key}`);
380
+ return sum + Math.max(0, size - section.state.minSize);
381
+ }, 0),
377
382
  );
378
- const nextTop: SectionState = { ...top.state, size: nextTopSize };
379
- const nextBottom: SectionState = {
380
- ...bottom.state,
381
- size: flexibleOwner === bottom.key ? null : nextBottomSize,
382
- };
383
- if (
384
- nextTop.size === top.state.size
385
- && nextBottom.size === bottom.state.size
386
- ) return;
387
- emitTransition([
388
- { key: top.key, state: nextTop },
389
- { key: bottom.key, state: nextBottom },
390
- ]);
383
+ if (transferable === 0 || !growing) return;
384
+
385
+ let remaining = transferable;
386
+ for (const section of shrinking) {
387
+ if (remaining <= 0) break;
388
+ const size = effectiveSizes.get(section.key) ?? measuredHeight(section.element, `sections.${section.key}`);
389
+ const reduction = Math.min(size - section.state.minSize, remaining);
390
+ effectiveSizes.set(section.key, size - reduction);
391
+ remaining -= reduction;
392
+ }
393
+ const growSize = effectiveSizes.get(growing.key)
394
+ ?? measuredHeight(growing.element, `sections.${growing.key}`);
395
+ effectiveSizes.set(growing.key, growSize + transferable);
396
+ geometryRevision.value += 1;
397
+
398
+ const replacements = openSections
399
+ .filter((section) => section.state.size !== null)
400
+ .map((section) => ({
401
+ key: section.key,
402
+ state: {
403
+ ...section.state,
404
+ size: effectiveSizes.get(section.key)!,
405
+ } satisfies SectionState,
406
+ }));
407
+ if (replacements.length > 0) emitTransition(replacements);
391
408
  }
392
409
 
393
410
  function finishResize(topKey: string, bottomKey: string): void {
@@ -404,9 +421,11 @@ function finishResize(topKey: string, bottomKey: string): void {
404
421
  provide(sidebarGroupContextKey, {
405
422
  sectionsState: computed(currentState),
406
423
  orderedSections,
424
+ geometryRevision,
407
425
  registerSection,
408
426
  toggleSection,
409
427
  getSectionStyle,
428
+ getSectionSize,
410
429
  beginResize,
411
430
  resizeSections,
412
431
  finishResize,
@@ -432,7 +451,7 @@ onMounted(() => {
432
451
 
433
452
  onBeforeUnmount(() => {
434
453
  mounted = false;
435
- geometryRevision += 1;
454
+ geometryRevision.value += 1;
436
455
  activeResizePair = null;
437
456
  resizeObserver?.disconnect();
438
457
  resizeObserver = null;
@@ -3,11 +3,13 @@
3
3
  .s-sidebar-section-wrapper {
4
4
  @apply flex flex-col;
5
5
  min-height: 0;
6
+ overflow: hidden;
6
7
  }
7
8
 
8
9
  .s-sidebar-section {
9
- @apply flex flex-col;
10
- @apply min-h-0 flex-grow w-full;
10
+ @apply flex flex-col min-h-0 w-full;
11
+ flex: 1 1 0%;
12
+ overflow: hidden;
11
13
  }
12
14
 
13
15
  .s-sidebar-section-wrapper + .s-sidebar-section-wrapper .s-sidebar-section {
@@ -19,9 +21,18 @@
19
21
  }
20
22
 
21
23
  .s-sidebar-section-header {
22
- @apply flex w-full items-center gap-1 px-2 h-7;
24
+ @apply flex w-full items-center gap-1 px-1 h-7;
25
+ @apply bg-surface-50/70 dark:bg-surface-800/55;
23
26
  @apply text-surface-600 dark:text-surface-300;
24
27
  @apply shrink-0;
28
+ transition:
29
+ background-color var(--s-motion-standard) ease,
30
+ color var(--s-motion-standard) ease;
31
+ }
32
+
33
+ .s-sidebar-section-header:has([data-sidebar-section-toggle]:hover),
34
+ .s-sidebar-section-header:has([data-sidebar-section-toggle]:focus-visible) {
35
+ @apply bg-surface-100 text-surface-800 dark:bg-surface-700/70 dark:text-surface-100;
25
36
  }
26
37
  .s-sidebar-section-header--mobile {
27
38
  @apply h-6 px-1.5;
@@ -37,5 +48,6 @@
37
48
 
38
49
  .s-sidebar-section-content {
39
50
  @apply text-sm text-surface-700 dark:text-surface-300;
40
- @apply flex-grow overflow-y-auto min-h-0;
51
+ @apply overflow-y-auto overflow-x-hidden min-h-0;
52
+ flex: 1 1 0%;
41
53
  }
@@ -26,6 +26,8 @@
26
26
  <SButton
27
27
  :id="triggerId"
28
28
  :size="layout === 'mobile' ? 'xs' : 'sm'"
29
+ spacing="tight"
30
+ hover-surface="parent"
29
31
  severity="secondary"
30
32
  appearance="ghost"
31
33
  shape="row"
@@ -208,14 +210,8 @@ const topSectionIndex = computed(() => {
208
210
  return -1;
209
211
  });
210
212
 
211
- function measuredHeight(section: RegisteredSidebarSection): number {
212
- const height = section.element.offsetHeight;
213
- if (!Number.isFinite(height) || height < 0) {
214
- throw new RangeError(
215
- `SSidebarSection: sections.${section.key}.offsetHeight must be a non-negative finite number`,
216
- );
217
- }
218
- return height;
213
+ function effectiveHeight(section: RegisteredSidebarSection): number {
214
+ return Math.max(section.state.minSize, groupContext.getSectionSize(section.key));
219
215
  }
220
216
 
221
217
  function resizePair(): readonly [RegisteredSidebarSection, RegisteredSidebarSection] {
@@ -234,17 +230,20 @@ function resizePair(): readonly [RegisteredSidebarSection, RegisteredSidebarSect
234
230
  }
235
231
 
236
232
  const resizeContract = computed(() => {
233
+ void groupContext.geometryRevision.value;
237
234
  const [top, bottom] = resizePair();
238
- const value = Math.max(top.state.minSize, top.state.size ?? measuredHeight(top));
239
- const bottomSize = Math.max(
240
- bottom.state.minSize,
241
- bottom.state.size ?? measuredHeight(bottom),
242
- );
243
- const pairSize = value + bottomSize;
235
+ const openSections = groupContext.orderedSections.value.filter((section) => section.state.isOpen);
236
+ const topIndex = openSections.findIndex((section) => section.key === top.key);
237
+ const upper = openSections.slice(0, topIndex + 1);
238
+ const lower = openSections.slice(topIndex + 1);
239
+ const value = upper.reduce((sum, section) => sum + effectiveHeight(section), 0);
240
+ const minimumUpper = upper.reduce((sum, section) => sum + section.state.minSize, 0);
241
+ const minimumLower = lower.reduce((sum, section) => sum + section.state.minSize, 0);
242
+ const totalSize = value + lower.reduce((sum, section) => sum + effectiveHeight(section), 0);
244
243
  return {
245
244
  value,
246
- min: top.state.minSize,
247
- max: Math.max(top.state.minSize, pairSize - bottom.state.minSize),
245
+ min: minimumUpper,
246
+ max: Math.max(minimumUpper, totalSize - minimumLower),
248
247
  testId: `sidebar-resize-${top.key}-${bottom.key}`,
249
248
  };
250
249
  });
@@ -33,12 +33,29 @@
33
33
  font-size: 0.75rem;
34
34
  }
35
35
 
36
+ .s-button[data-density='compact'][data-size='xs']:not([data-icon-only='true']) {
37
+ height: var(--s-control-hitbox-xs);
38
+ min-height: var(--s-control-hitbox-xs);
39
+ padding-block: 0;
40
+ }
41
+
36
42
  .s-button[data-size='sm']:not([data-icon-only='true']) {
37
43
  min-height: var(--s-control-hitbox-sm);
38
44
  padding: 0.375rem 0.625rem;
39
45
  font-size: 0.75rem;
40
46
  }
41
47
 
48
+ .s-button[data-spacing='tight'][data-size='sm']:not([data-icon-only='true']) {
49
+ height: var(--s-control-hitbox-sm);
50
+ min-height: var(--s-control-hitbox-sm);
51
+ padding-block: 0;
52
+ padding-inline: 0.25rem;
53
+ }
54
+
55
+ .s-button[data-spacing='tight'] .s-button__content {
56
+ gap: 0.25rem;
57
+ }
58
+
42
59
  .s-button[data-size='md']:not([data-icon-only='true']) {
43
60
  min-height: var(--s-control-hitbox-md);
44
61
  padding: 0.625rem 1.25rem;
@@ -154,6 +171,10 @@
154
171
  background: var(--s-button-hover-background);
155
172
  }
156
173
 
174
+ .s-button[data-hover-surface='parent'][data-appearance='ghost']:hover:not(:disabled) {
175
+ background: transparent;
176
+ }
177
+
157
178
  .s-button[data-appearance='ghost'],
158
179
  .s-button[data-appearance='text'] {
159
180
  background: transparent;
@@ -262,6 +283,11 @@
262
283
  height: var(--s-icon-size-sm);
263
284
  }
264
285
 
286
+ .s-button[data-icon-size='large'] .s-button__icon {
287
+ width: var(--s-icon-size-md);
288
+ height: var(--s-icon-size-md);
289
+ }
290
+
265
291
  .s-button__loader {
266
292
  @apply absolute inline-flex;
267
293
  }
@@ -7,7 +7,11 @@
7
7
  :data-size="size"
8
8
  :data-width="width"
9
9
  :data-content-align="contentAlign"
10
+ :data-density="density"
11
+ :data-spacing="spacing"
12
+ :data-hover-surface="hoverSurface"
10
13
  :data-icon-only="isIconOnly"
14
+ :data-icon-size="iconSize"
11
15
  :data-appearance="appearance"
12
16
  :data-shape="shape"
13
17
  :data-loading="loading || undefined"
@@ -70,6 +74,10 @@ defineOptions({ inheritAttrs: false })
70
74
 
71
75
  export type SButtonSeverity = 'primary' | 'secondary' | 'success' | 'info' | 'warn' | 'danger' | 'contrast'
72
76
  export type SButtonSize = 'xs' | 'sm' | 'md' | 'lg'
77
+ export type SButtonIconSize = 'default' | 'large'
78
+ export type SButtonDensity = 'default' | 'compact'
79
+ export type SButtonSpacing = 'default' | 'tight'
80
+ export type SButtonHoverSurface = 'self' | 'parent'
73
81
  export type SButtonWidth = 'auto' | 'fit' | 'full'
74
82
  export type SButtonContentAlign = 'center' | 'start' | 'between'
75
83
  export type SButtonAppearance = 'solid' | 'soft' | 'outline' | 'ghost' | 'text'
@@ -93,6 +101,10 @@ export interface Props {
93
101
  loadingLabel?: string
94
102
  leadingIcon?: Component
95
103
  trailingIcon?: Component
104
+ iconSize?: SButtonIconSize
105
+ density?: SButtonDensity
106
+ spacing?: SButtonSpacing
107
+ hoverSurface?: SButtonHoverSurface
96
108
  badge?: string | number
97
109
  badgeSeverity?: BadgeSeverity
98
110
  shortcut?: string
@@ -116,6 +128,10 @@ const props = withDefaults(defineProps<Props>(), {
116
128
  loadingLabel: undefined,
117
129
  leadingIcon: undefined,
118
130
  trailingIcon: undefined,
131
+ iconSize: 'default',
132
+ density: 'default',
133
+ spacing: 'default',
134
+ hoverSurface: 'self',
119
135
  badge: undefined,
120
136
  badgeSeverity: 'secondary',
121
137
  shortcut: undefined,
@@ -30,7 +30,7 @@ export interface Props {
30
30
  partial?: boolean
31
31
  addTitle?: string
32
32
  removeTitle?: string
33
- size?: 'sm' | 'md' | 'lg'
33
+ size?: 'xs' | 'sm' | 'md' | 'lg'
34
34
  loading?: boolean
35
35
  }
36
36
 
@@ -14,6 +14,10 @@
14
14
  color var(--s-motion-standard) ease;
15
15
  }
16
16
 
17
+ .s-interactive-surface[data-appearance='plain'] {
18
+ @apply border-transparent bg-transparent dark:border-transparent dark:bg-transparent;
19
+ }
20
+
17
21
  .s-interactive-surface__activation {
18
22
  @apply absolute inset-0 z-10 block h-full w-full cursor-pointer border-0 bg-transparent p-0;
19
23
  border-radius: inherit;
@@ -17,6 +17,7 @@ export type InteractiveSurfaceDensity = 'compact' | 'comfortable'
17
17
  export type InteractiveSurfacePadding = 'none' | 'sm' | 'md'
18
18
  export type InteractiveSurfaceRadius = 'md' | 'lg' | 'xl'
19
19
  export type InteractiveSurfaceFill = 'content' | 'container'
20
+ export type InteractiveSurfaceAppearance = 'card' | 'plain'
20
21
 
21
22
  /**
22
23
  * Единственный semantic-контракт поверхности: passive, action или ровно одна link-target.
@@ -55,6 +56,7 @@ export interface Props {
55
56
  padding?: InteractiveSurfacePadding
56
57
  radius?: InteractiveSurfaceRadius
57
58
  fill?: InteractiveSurfaceFill
59
+ appearance?: InteractiveSurfaceAppearance
58
60
  contentAlign?: 'start' | 'center'
59
61
  /** Полный binding явного STooltip trigger, направляемый на activation owner. / Complete explicit STooltip trigger binding routed to the activation owner. */
60
62
  tooltipTrigger?: STooltipTriggerBinding
@@ -67,6 +69,7 @@ const props = withDefaults(defineProps<Props>(), {
67
69
  padding: 'md',
68
70
  radius: 'lg',
69
71
  fill: 'content',
72
+ appearance: 'card',
70
73
  contentAlign: 'start',
71
74
  tooltipTrigger: undefined,
72
75
  })
@@ -285,6 +288,7 @@ function activateAction(event: MouseEvent): void {
285
288
  :data-padding="padding"
286
289
  :data-radius="radius"
287
290
  :data-fill="fill"
291
+ :data-appearance="appearance"
288
292
  :data-selected="selected || undefined"
289
293
  :data-pressed="surfacePressed"
290
294
  :data-disabled="surfaceDisabled || undefined"
@@ -0,0 +1,35 @@
1
+ @reference "../../../styles/reference.css";
2
+
3
+ .s-range {
4
+ @apply flex min-w-0 items-center gap-3;
5
+ }
6
+
7
+ .s-range__input {
8
+ width: 100%;
9
+ min-height: var(--s-control-hitbox-sm);
10
+ cursor: pointer;
11
+ accent-color: var(--color-primary-500);
12
+ }
13
+
14
+ .s-range__input:focus-visible {
15
+ outline: var(--s-focus-ring-width) solid var(--s-focus-ring-color);
16
+ outline-offset: var(--s-focus-ring-offset);
17
+ }
18
+
19
+ .s-range__input:disabled {
20
+ cursor: not-allowed;
21
+ opacity: 0.6;
22
+ }
23
+
24
+ .s-range__input[aria-readonly="true"] {
25
+ cursor: default;
26
+ opacity: 0.8;
27
+ }
28
+
29
+ .s-range__input[aria-invalid="true"] {
30
+ accent-color: var(--color-danger-500);
31
+ }
32
+
33
+ .s-range__value {
34
+ @apply min-w-12 text-right font-mono text-xs text-surface-600 dark:text-surface-300;
35
+ }
@@ -0,0 +1,129 @@
1
+ <template>
2
+ <SField
3
+ :control-id="inputId"
4
+ :label="label"
5
+ :disabled="disabled"
6
+ :readonly="readonly"
7
+ :required="required"
8
+ :help-text="helpText"
9
+ :error-message="errorMessage"
10
+ :invalid="invalid"
11
+ >
12
+ <template #default="field">
13
+ <div class="s-range">
14
+ <input
15
+ v-bind="ownedAttrs.bindings()"
16
+ :id="field.controlId"
17
+ class="s-range__input"
18
+ type="range"
19
+ :min="validatedMin"
20
+ :max="validatedMax"
21
+ :step="validatedStep"
22
+ :value="validatedValue"
23
+ :disabled="field.disabled"
24
+ :aria-invalid="field.invalid || undefined"
25
+ :aria-readonly="field.readonly || undefined"
26
+ :aria-required="field.required || undefined"
27
+ :aria-describedby="mergeIdReferences(ownedAttrs.string('aria-describedby'), field.describedBy)"
28
+ @input="onInput"
29
+ />
30
+ <output v-if="showValue" class="s-range__value" :for="field.controlId">
31
+ {{ validatedValue }}
32
+ </output>
33
+ </div>
34
+ </template>
35
+ </SField>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import { computed, useId } from 'vue'
40
+ import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs'
41
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
42
+ import { resolveOptionalDomId } from '../../../internal/runtimeContract'
43
+ import SField from './SField.vue'
44
+
45
+ defineOptions({ inheritAttrs: false })
46
+
47
+ export interface Props {
48
+ modelValue: number
49
+ min: number
50
+ max: number
51
+ step?: number
52
+ label: string
53
+ id?: string
54
+ disabled?: boolean
55
+ readonly?: boolean
56
+ required?: boolean
57
+ invalid?: boolean
58
+ helpText?: string
59
+ errorMessage?: string
60
+ showValue?: boolean
61
+ }
62
+
63
+ const props = withDefaults(defineProps<Props>(), {
64
+ step: 1,
65
+ id: undefined,
66
+ disabled: false,
67
+ readonly: false,
68
+ required: false,
69
+ invalid: false,
70
+ helpText: undefined,
71
+ errorMessage: undefined,
72
+ showValue: false,
73
+ })
74
+
75
+ const emit = defineEmits<{
76
+ 'update:modelValue': [value: number]
77
+ }>()
78
+
79
+ function finiteNumber(field: string, value: number): number {
80
+ if (!Number.isFinite(value)) {
81
+ throw new TypeError(`SRange: ${field} должен быть конечным числом / must be a finite number`)
82
+ }
83
+ return value
84
+ }
85
+
86
+ const validatedMin = computed(() => finiteNumber('min', props.min))
87
+ const validatedMax = computed(() => {
88
+ const value = finiteNumber('max', props.max)
89
+ if (value <= validatedMin.value) {
90
+ throw new RangeError('SRange: max должен быть больше min / max must be greater than min')
91
+ }
92
+ return value
93
+ })
94
+ const validatedStep = computed(() => {
95
+ const value = finiteNumber('step', props.step)
96
+ if (value <= 0) {
97
+ throw new RangeError('SRange: step должен быть больше нуля / step must be greater than zero')
98
+ }
99
+ return value
100
+ })
101
+ const validatedValue = computed(() => {
102
+ const value = finiteNumber('modelValue', props.modelValue)
103
+ if (value < validatedMin.value || value > validatedMax.value) {
104
+ throw new RangeError('SRange: modelValue должен находиться внутри [min, max] / must be within [min, max]')
105
+ }
106
+ return value
107
+ })
108
+
109
+ const generatedId = useId()
110
+ const inputId = computed(() => resolveOptionalDomId('SRange', 'id', props.id, `s-range-${generatedId}`))
111
+ const ownedAttrs = useOwnedAttrs({ component: 'SRange', owner: 'native range input' })
112
+ useInteractiveLeafRegistration({ owner: 'SRange' })
113
+
114
+ function onInput(event: Event): void {
115
+ const target = event.currentTarget
116
+ if (!(target instanceof HTMLInputElement)) {
117
+ throw new TypeError('SRange: input event owner must be the native range input')
118
+ }
119
+ if (props.readonly) {
120
+ target.value = String(validatedValue.value)
121
+ return
122
+ }
123
+ const value = Number(target.value)
124
+ finiteNumber('input value', value)
125
+ emit('update:modelValue', value)
126
+ }
127
+ </script>
128
+
129
+ <style lang="postcss" src="./SRange.css" scoped></style>
@@ -6,7 +6,7 @@
6
6
  's-code-block--radius-none': validatedSurfaceRadius === 'none',
7
7
  's-code-block--radius-xl': validatedSurfaceRadius === 'xl',
8
8
  's-code-block--interactive': !validatedReadOnly,
9
- 's-code-block--viewport': validatedViewport !== 'content',
9
+ 's-code-block--viewport': !['content', 'document'].includes(validatedViewport),
10
10
  }"
11
11
  >
12
12
  <div v-if="validatedShowHeader" class="s-code-block-header">
@@ -11,6 +11,21 @@
11
11
  max-height: var(--s-code-editor-viewport-content-max);
12
12
  }
13
13
 
14
+ .s-code-editor[data-viewport='document'] {
15
+ overflow: visible;
16
+ }
17
+
18
+ .s-code-editor[data-viewport='document'] :deep(.cm-editor) {
19
+ height: auto;
20
+ max-height: none;
21
+ overflow: visible;
22
+ }
23
+
24
+ .s-code-editor[data-viewport='document'][data-variant] :deep(.cm-scroller) {
25
+ height: auto;
26
+ overflow: visible;
27
+ }
28
+
14
29
  .s-code-editor[data-viewport='compact'] {
15
30
  height: var(--s-code-editor-viewport-compact);
16
31
  }
@@ -179,6 +179,20 @@ const resolvedExtensions = computed<Extension[]>(() => {
179
179
  const capabilities = resolvedCapabilities.value
180
180
  const language = resolvedLanguage.value
181
181
  const result: Extension[] = [...(trustedChannel?.extensions.value ?? [])]
182
+ if (validatedViewport.value === 'document') {
183
+ result.push(EditorView.theme({
184
+ '&.cm-editor': {
185
+ height: 'auto',
186
+ maxHeight: 'none',
187
+ overflow: 'visible',
188
+ },
189
+ '.cm-scroller': {
190
+ height: 'auto',
191
+ overflow: 'visible',
192
+ },
193
+ }))
194
+ result.push(EditorView.lineWrapping)
195
+ }
182
196
  const languageExtension = getCodeMirrorLanguageExtension(language)
183
197
  if (languageExtension) result.push(languageExtension)
184
198
  if (runtimeTheme.value.mode === 'dark') result.push(oneDark)
@@ -11,6 +11,7 @@
11
11
  class="s-tab-container"
12
12
  :class="{ 's-tab-container--selected': state.active }"
13
13
  :data-close-mode="ctx.closeButtonMode.value"
14
+ :data-close-visibility="ctx.closeButtonVisibility.value"
14
15
  :data-disabled="state.disabled || undefined"
15
16
  :data-editing="editing || undefined"
16
17
  :style="containerStyle"
@@ -192,7 +193,7 @@ const hasContextMenu = computed(() => Boolean(props.contextMenuItems?.length))
192
193
  const containerStyle = computed(() => {
193
194
  const siblingSpace = hasSiblingActions.value ? siblingActionsWidth.value : 0
194
195
  const basePaddingInline = 'var(--s-tab-base-padding-inline)'
195
- const paddingInlineEnd = ctx.closeButtonMode.value === 'overlay' && siblingSpace > 0
196
+ const paddingInlineEnd = ctx.closeButtonMode.value === 'reserve' && siblingSpace > 0
196
197
  ? `calc(${basePaddingInline} + ${siblingSpace}px + var(--s-tab-sibling-actions-gap))`
197
198
  : basePaddingInline
198
199
  return {
@@ -250,13 +251,19 @@ onBeforeUnmount(() => {
250
251
  min-width: var(--s-tab-min-width);
251
252
  max-width: var(--s-tab-max-width);
252
253
  height: var(--s-tab-row-height);
253
- border-bottom: var(--s-tab-indicator-width) solid transparent;
254
254
  border-radius: var(--s-tab-radius);
255
255
  overflow: hidden;
256
256
  }
257
257
 
258
- .s-tab-container--selected {
259
- border-color: var(--s-tab-selected-border-color);
258
+ .s-tab-container::after {
259
+ @apply pointer-events-none absolute inset-x-0 bottom-0;
260
+ height: var(--s-tab-indicator-width);
261
+ background-color: transparent;
262
+ content: '';
263
+ }
264
+
265
+ .s-tab-container--selected::after {
266
+ background-color: var(--s-tab-selected-border-color);
260
267
  }
261
268
 
262
269
  .s-tab__trigger {
@@ -265,8 +272,7 @@ onBeforeUnmount(() => {
265
272
  width: 100%;
266
273
  min-height: var(--s-tab-trigger-min-height);
267
274
  margin: 0;
268
- padding-block-start: calc(var(--s-tab-padding-block) + var(--s-tab-content-offset-y));
269
- padding-block-end: var(--s-tab-padding-block);
275
+ padding-block: var(--s-tab-padding-block);
270
276
  padding-inline-start: var(--s-tab-padding-inline-start);
271
277
  padding-inline-end: var(--s-tab-padding-inline-end);
272
278
  border-radius: inherit;
@@ -348,6 +354,25 @@ onBeforeUnmount(() => {
348
354
  z-index: var(--s-layer-sticky);
349
355
  }
350
356
 
357
+ .s-tab-container[data-close-visibility='active-hover']:not(.s-tab-container--selected):not(:hover):not(:focus-within) .s-tab__sibling-actions {
358
+ visibility: hidden;
359
+ opacity: 0;
360
+ }
361
+
362
+ .s-tab-container[data-close-visibility='hover']:not(:hover):not(:focus-within) .s-tab__sibling-actions {
363
+ visibility: hidden;
364
+ opacity: 0;
365
+ }
366
+
367
+ .s-tab-container:is(
368
+ [data-close-visibility='active-hover'],
369
+ [data-close-visibility='hover']
370
+ ) .s-tab__sibling-actions {
371
+ transition:
372
+ opacity var(--s-motion-fast) ease,
373
+ visibility var(--s-motion-fast) ease;
374
+ }
375
+
351
376
  .s-tab-container[data-close-mode='overlay'] .s-tab__sibling-actions {
352
377
  @apply absolute inset-y-0 right-0;
353
378
  }
@@ -11,6 +11,7 @@
11
11
  :data-list-width="tabListWidth"
12
12
  :data-tone="tone"
13
13
  :data-close-mode="closeButtonMode"
14
+ :data-close-visibility="closeButtonVisibility"
14
15
  :data-radius="surfaceRadius"
15
16
  :data-surface="surface"
16
17
  :data-has-panels="composition === 'custom' ? 'custom' : ($slots.panels ? 'true' : 'false')"
@@ -121,6 +122,8 @@ export interface Props {
121
122
  tabDensity?: 'compact' | 'default' | 'comfortable'
122
123
  /** Резервирует или перекрывает место sibling close action. / Reserves or overlays sibling close-action space. */
123
124
  closeButtonMode?: 'reserve' | 'overlay'
125
+ /** Видимость sibling close/action region. / Visibility policy for the sibling close/action region. */
126
+ closeButtonVisibility?: 'always' | 'active-hover' | 'hover'
124
127
  /** Скругление owner surface. / Radius of the owner surface. */
125
128
  surfaceRadius?: 'default' | 'none'
126
129
  /** Фон owner surface. / Background of the owner surface. */
@@ -156,6 +159,7 @@ const props = withDefaults(defineProps<Props>(), {
156
159
  tabListWidth: 'auto',
157
160
  tabDensity: 'default',
158
161
  closeButtonMode: 'reserve',
162
+ closeButtonVisibility: 'always',
159
163
  surfaceRadius: 'default',
160
164
  surface: 'default',
161
165
  })
@@ -236,6 +240,7 @@ const activeKey = computed<string | null>(() => (
236
240
  ))
237
241
  const activationMode = computed(() => props.activationMode)
238
242
  const closeMode = computed(() => props.closeButtonMode)
243
+ const closeVisibility = computed(() => props.closeButtonVisibility)
239
244
  const density = computed(() => props.tabDensity)
240
245
 
241
246
  function identityIds(tabKey: string): { tabId: string; panelId: string } {
@@ -456,6 +461,7 @@ const context: STabsContext = {
456
461
  activeKey,
457
462
  activationMode,
458
463
  closeButtonMode: closeMode,
464
+ closeButtonVisibility: closeVisibility,
459
465
  tabDensity: density,
460
466
  registerTab,
461
467
  unregisterTab,
@@ -522,7 +528,7 @@ defineExpose({
522
528
  --s-tab-base-padding-inline: 1rem;
523
529
  --s-tab-padding-inline: var(--s-tab-base-padding-inline);
524
530
  --s-tab-padding-block: 0;
525
- --s-tab-content-offset-y: 2px;
531
+ --s-tab-content-offset-y: 0;
526
532
  --s-tab-trigger-min-height: var(--s-control-hitbox-sm);
527
533
  --s-tab-indicator-width: 2px;
528
534
  --s-tab-radius: 0.375rem 0.375rem 0 0;
@@ -575,7 +581,7 @@ defineExpose({
575
581
 
576
582
  .s-tabs[data-density='compact'] { --s-tab-row-height: 2rem; --s-tab-base-padding-inline: 0.5rem; }
577
583
  .s-tabs[data-density='comfortable'] { --s-tab-row-height: 3rem; --s-tab-base-padding-inline: 1.125rem; }
578
- .s-tabs[data-sizing='content'] { --s-tab-min-width: 7rem; --s-tab-max-width: none; }
584
+ .s-tabs[data-sizing='content'] { --s-tab-min-width: 0; --s-tab-max-width: 15rem; }
579
585
  .s-tabs[data-sizing='compact'] { --s-tab-max-width: 10.5rem; }
580
586
  .s-tabs[data-sizing='wide'] { --s-tab-max-width: 20rem; }
581
587
  .s-tabs[data-sizing='equal'] {
@@ -5,7 +5,7 @@ import {
5
5
  validateNonEmptyString,
6
6
  } from './runtimeContract'
7
7
 
8
- export const CODE_EDITOR_VIEWPORTS = ['content', 'compact', 'standard', 'expanded', 'fill'] as const
8
+ export const CODE_EDITOR_VIEWPORTS = ['content', 'document', 'compact', 'standard', 'expanded', 'fill'] as const
9
9
  export const CODE_EDITOR_VARIANTS = ['code', 'sql'] as const
10
10
  export const CODE_EDITOR_KEY_BEHAVIORS = ['indent', 'focus-next'] as const
11
11
  export const CODE_EDITOR_CURSOR_ACTIVITY_MODES = ['editable', 'always'] as const
@@ -175,6 +175,13 @@ export const ownedAttributeSchemas = Object.freeze({
175
175
  step: 'string-or-number',
176
176
  title: 'string',
177
177
  }),
178
+ SRange: defineOwnedAttributeSchema({
179
+ 'aria-describedby': 'string',
180
+ 'aria-label': 'string',
181
+ 'data-testid': 'data',
182
+ onBlur: 'listener',
183
+ onFocus: 'listener',
184
+ }),
178
185
  SInlineTokenEditor: defineOwnedAttributeSchema({
179
186
  'aria-describedby': 'string',
180
187
  'aria-label': 'string',