@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.
- package/package.json +6 -2
- package/src/components/layout/SSideMenuButton.vue +5 -4
- package/src/components/layout/SSideMenuRail.vue +3 -6
- package/src/components/shared/_internal/STreeNode.vue +10 -4
- package/src/components/shared/_internal/sidebarGroupContext.ts +3 -1
- package/src/components/shared/_internal/tabsContext.ts +1 -0
- package/src/components/shared/containers/SLeftSidebar.vue +2 -2
- package/src/components/shared/containers/SPanel.css +7 -1
- package/src/components/shared/containers/SPanel.vue +5 -1
- package/src/components/shared/containers/SSidebarGroup.css +1 -1
- package/src/components/shared/containers/SSidebarGroup.vue +113 -94
- package/src/components/shared/containers/SSidebarSection.css +16 -4
- package/src/components/shared/containers/SSidebarSection.vue +15 -16
- package/src/components/shared/controls/SButton.css +26 -0
- package/src/components/shared/controls/SButton.vue +16 -0
- package/src/components/shared/controls/SContextToggleButton.vue +1 -1
- package/src/components/shared/controls/SInteractiveSurface.css +4 -0
- package/src/components/shared/controls/SInteractiveSurface.vue +4 -0
- package/src/components/shared/controls/SRange.css +35 -0
- package/src/components/shared/controls/SRange.vue +129 -0
- package/src/components/shared/data-display/SCodeBlock.vue +1 -1
- package/src/components/shared/data-display/SCodeEditor.css +15 -0
- package/src/components/shared/data-display/SCodeEditor.vue +14 -0
- package/src/components/shared/navigation/STab.vue +31 -6
- package/src/components/shared/navigation/STabs.vue +8 -2
- package/src/internal/codeEditorContract.ts +1 -1
- 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.
|
|
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": "
|
|
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':
|
|
100
|
-
'size': 'md' as const,
|
|
101
|
-
'width':
|
|
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-
|
|
6
|
+
:class="expanded ? 'w-56 items-stretch' : 'w-14 items-stretch'"
|
|
7
7
|
>
|
|
8
|
-
<div class="flex flex-col
|
|
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:
|
|
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:
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
302
|
-
flexShrink:
|
|
303
|
-
flexBasis:
|
|
304
|
-
minHeight:
|
|
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
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
);
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
|
212
|
-
|
|
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
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
243
|
-
const
|
|
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:
|
|
247
|
-
max: Math.max(
|
|
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,
|
|
@@ -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':
|
|
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 === '
|
|
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
|
|
259
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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',
|