@getdom/studio 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/CHANGELOG.md +31 -0
- package/README.md +31 -0
- package/SKILL.md +21 -1
- package/package.json +4 -2
- package/public/llms.txt +13 -2
- package/src/pages/_layout/componentCatalog.generated.js +1185 -44
- package/src/pages/_layout/componentLoaders.js +41 -0
- package/src/pages/_layout/componentManager.js +38 -27
- package/src/pages/_layout/componentRuntimeRecords.js +3 -1
- package/src/pages/charts/_shared/ChartContainer.vue +67 -0
- package/src/pages/charts/_shared/ChartLegend.vue +22 -0
- package/src/pages/charts/_shared/ChartTooltip.vue +18 -0
- package/src/pages/charts/_shared/DonutRenderer.vue +33 -0
- package/src/pages/charts/_shared/XYChart.vue +140 -0
- package/src/pages/charts/_shared/XYRenderer.vue +90 -0
- package/src/pages/charts/_shared/chartProps.js +23 -0
- package/src/pages/charts/_shared/chartUtils.js +95 -0
- package/src/pages/charts/area-chart/DomAreaChart.vue +27 -0
- package/src/pages/charts/bar-chart/DomBarChart.vue +29 -0
- package/src/pages/charts/date-range-picker/DomDateRangePicker.vue +104 -0
- package/src/pages/charts/date-range-picker/dateRange.js +69 -0
- package/src/pages/charts/donut-chart/DomDonutChart.vue +117 -0
- package/src/pages/charts/line-chart/DomLineChart.vue +26 -0
- package/src/pages/charts/metric-card/DomMetricCard.vue +49 -0
- package/src/pages/charts/sparkline/DomSparkline.vue +30 -0
- package/src/pages/components/_shared/media/MediaDeviceSelect.vue +28 -0
- package/src/pages/components/_shared/media/useMediaInput.js +200 -0
- package/src/pages/components/audio-controls/DomAudioControls.vue +91 -0
- package/src/pages/components/audio-input/DomAudioInput.vue +58 -0
- package/src/pages/components/button/DomButton.vue +10 -4
- package/src/pages/components/button/buttonStyles.js +5 -0
- package/src/pages/components/camera-input/DomCameraInput.vue +75 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +12 -2
- package/src/pages/components/dialog/DomDialog.vue +338 -36
- package/src/pages/components/icon-button/DomIconButton.vue +8 -2
- package/src/pages/components/menu-stack/DomMenuStack.vue +111 -0
- package/src/pages/components/menu-stack/menuTree.js +35 -0
- package/src/pages/components/popover/DomPopover.vue +1 -53
- package/src/pages/components/popover/popover.css +50 -0
- package/src/pages/components/video-feed/DomVideoFeed.vue +551 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +1 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +1 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +1 -0
- package/src/pages/data/gantt/DomGantt.vue +788 -0
- package/src/pages/data/gantt/ganttTime.js +223 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +84 -9
- package/src/pages/data/notification-inbox/DomNotificationInbox.vue +505 -0
- package/src/pages/data/notification-inbox/notificationPresentation.js +34 -0
- package/src/pages/designer/_shared/ControlIconButton.vue +62 -0
- package/src/pages/designer/_shared/StyleControlSection.vue +59 -0
- package/src/pages/designer/_shared/cssEditorContext.js +19 -0
- package/src/pages/designer/backdrop-editor/BackdropControl.vue +57 -0
- package/src/pages/designer/backdrop-editor/DomBackdropEditor.vue +33 -0
- package/src/pages/designer/backdrop-editor/backdropModel.js +39 -0
- package/src/pages/designer/background-editor/BackgroundLayers.vue +210 -0
- package/src/pages/designer/background-editor/DomBackgroundEditor.vue +33 -0
- package/src/pages/designer/background-editor/backgroundModel.js +102 -0
- package/src/pages/designer/border-editor/BorderControl.vue +72 -0
- package/src/pages/designer/border-editor/DomBorderEditor.vue +34 -0
- package/src/pages/designer/border-editor/borderModel.js +21 -0
- package/src/pages/designer/color-swatch/DomColorSwatch.vue +95 -0
- package/src/pages/designer/frame/DomDesignerFrame.vue +16 -0
- package/src/pages/designer/frame/DomFrameLayoutEditor.vue +41 -0
- package/src/pages/designer/frame/frameLayout.js +93 -0
- package/src/pages/designer/layers/DomDesignerLayers.vue +11 -0
- package/src/pages/designer/shadow-editor/DomShadowEditor.vue +32 -0
- package/src/pages/designer/shadow-editor/ShadowLayers.vue +185 -0
- package/src/pages/designer/shadow-editor/shadowModel.js +73 -0
- package/src/pages/designer/stage/DesignerBounds.vue +31 -0
- package/src/pages/designer/stage/DomDesignerStage.vue +237 -0
- package/src/pages/designer/stage/SpacingGuides.vue +33 -0
- package/src/pages/designer/stage/resizeGeometry.js +15 -0
- package/src/pages/designer/stage/snapGeometry.js +69 -0
- package/src/pages/designer/stage/snapPipeline.js +17 -0
- package/src/pages/designer/stage/spacingGeometry.js +129 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +8 -3
- package/src/pages/forms/number-input/DomNumberInput.vue +39 -3
- package/src/pages/forms/range-input/DomRangeInput.vue +75 -23
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +91 -9
- package/src/pages/forms/toggle/DomToggle.vue +1 -1
- package/src/pages/lib/headless/base.js +10 -5
- package/src/pages/lib/headless/dialog.js +25 -6
- package/src/pages/lib/headless/drawer.js +49 -21
- package/src/pages/lib/vue/index.d.ts +1161 -14
- package/src/pages/lib/vue/index.js +43 -0
- package/src/pages/mobile/_shared/interactionMath.js +46 -0
- package/src/pages/mobile/_shared/useKeyboardViewport.js +91 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +102 -65
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +4 -2
- package/src/pages/mobile/bottom-sheet/DomBottomSheet.vue +221 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +6 -6
- package/src/pages/mobile/profile/DomAppProfile.vue +42 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +6 -6
- package/src/pages/mobile/shell/DomAppShell.vue +60 -9
- package/src/pages/mobile/stack/DomAppStack.vue +214 -55
- package/src/pages/mobile/swipe-actions/DomSwipeActions.vue +346 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +3 -3
- package/src/pages/visual/audio-visualizer/DomAudioVisualizer.vue +143 -0
- package/src/theme.css +13 -0
|
@@ -11,6 +11,10 @@ defineOptions({
|
|
|
11
11
|
description: 'A compact slider input for numeric values with a stylable track and handle.',
|
|
12
12
|
icon: 'M5 12h14M8 12a2 2 0 1 0 0.01 0M16 12a2 2 0 1 0 0.01 0',
|
|
13
13
|
slots: [],
|
|
14
|
+
events: [
|
|
15
|
+
{ name: 'update:modelValue', payload: 'number', description: 'The value changed through pointer or keyboard input.' },
|
|
16
|
+
{ name: 'update:dragging', payload: 'boolean', description: 'Pointer scrubbing started or finished, including cancellation.' },
|
|
17
|
+
],
|
|
14
18
|
playground: {
|
|
15
19
|
initial: {
|
|
16
20
|
label: 'Confidence',
|
|
@@ -60,6 +64,11 @@ const props = defineProps({
|
|
|
60
64
|
default: '',
|
|
61
65
|
_edit: { description: 'Classes applied to the visual range track.' },
|
|
62
66
|
},
|
|
67
|
+
controlClass: {
|
|
68
|
+
type: [String, Array, Object],
|
|
69
|
+
default: '',
|
|
70
|
+
_edit: { description: 'Classes applied to the interactive track area, including its touch target height.' },
|
|
71
|
+
},
|
|
63
72
|
progressClass: {
|
|
64
73
|
type: [String, Array, Object],
|
|
65
74
|
default: '',
|
|
@@ -76,11 +85,14 @@ const props = defineProps({
|
|
|
76
85
|
_edit: { description: 'Additional classes applied to the handle while it is being dragged.' },
|
|
77
86
|
},
|
|
78
87
|
});
|
|
79
|
-
const emit = defineEmits(['update:modelValue']);
|
|
88
|
+
const emit = defineEmits(['update:modelValue', 'update:dragging', 'focus', 'blur']);
|
|
80
89
|
const field = useField(props, emit, { idPrefix: 'dom-range-input' });
|
|
81
90
|
const control = ref(null);
|
|
82
91
|
const rangeInput = ref(null);
|
|
83
92
|
const dragging = ref(false);
|
|
93
|
+
const keyboardFocused = ref(false);
|
|
94
|
+
let dragWindow;
|
|
95
|
+
let dragPointerId;
|
|
84
96
|
|
|
85
97
|
const displayValue = computed(() => {
|
|
86
98
|
const value = field.value.value === '' ? props.min : Number(field.value.value);
|
|
@@ -110,10 +122,11 @@ const handleStyle = computed(() => ({
|
|
|
110
122
|
left: `${progress.value}%`,
|
|
111
123
|
}));
|
|
112
124
|
|
|
113
|
-
const
|
|
114
|
-
'dom-range-input__control group relative min-h-7 w-full rounded-full outline-none',
|
|
125
|
+
const controlClassName = computed(() => [
|
|
126
|
+
'dom-range-input__control group group/range relative min-h-7 w-full touch-none rounded-full outline-none',
|
|
115
127
|
field.disabled.value || field.readOnly.value ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
116
128
|
(field.disabled.value || field.readOnly.value) && 'opacity-50',
|
|
129
|
+
props.controlClass,
|
|
117
130
|
]);
|
|
118
131
|
|
|
119
132
|
const inputClass = computed(() => [
|
|
@@ -126,12 +139,12 @@ const rangeInputAttrs = computed(() => ({
|
|
|
126
139
|
}));
|
|
127
140
|
|
|
128
141
|
const trackClassName = computed(() => [
|
|
129
|
-
'
|
|
142
|
+
'pointer-events-none absolute inset-x-0 top-1/2 h-2 -translate-y-1/2 rounded-full bg-secondary shadow-[inset_0_1px_2px_color-mix(in_oklch,var(--border)_70%,transparent)]',
|
|
130
143
|
props.trackClass,
|
|
131
144
|
]);
|
|
132
145
|
|
|
133
146
|
const progressClassName = computed(() => [
|
|
134
|
-
'
|
|
147
|
+
'pointer-events-none absolute left-0 top-1/2 h-2 -translate-y-1/2 rounded-full bg-primary shadow-[inset_0_1px_2px_color-mix(in_oklch,var(--border)_70%,transparent)]',
|
|
135
148
|
props.progressClass,
|
|
136
149
|
]);
|
|
137
150
|
|
|
@@ -141,11 +154,13 @@ const handleClassName = computed(() => [
|
|
|
141
154
|
dragging.value && props.activeHandleClass,
|
|
142
155
|
]);
|
|
143
156
|
|
|
157
|
+
/** Converts native range values to numbers without discarding unparseable input. */
|
|
144
158
|
function toNumber(value) {
|
|
145
159
|
const number = Number(value);
|
|
146
160
|
return Number.isNaN(number) ? value : number;
|
|
147
161
|
}
|
|
148
162
|
|
|
163
|
+
/** Commits native input changes while preserving read-only field values. */
|
|
149
164
|
function onInput(value, event) {
|
|
150
165
|
if (field.readOnly.value) {
|
|
151
166
|
event.target.value = displayValue.value;
|
|
@@ -155,51 +170,75 @@ function onInput(value, event) {
|
|
|
155
170
|
field.onInput(toNumber(value));
|
|
156
171
|
}
|
|
157
172
|
|
|
173
|
+
/** Starts one primary-pointer drag in the control's owning window, including iframe previews. */
|
|
158
174
|
function beginDrag(event) {
|
|
175
|
+
if (event.button !== 0 || !event.isPrimary || dragging.value) return;
|
|
159
176
|
if (field.disabled.value || field.readOnly.value) {
|
|
160
177
|
event.preventDefault();
|
|
161
178
|
return;
|
|
162
179
|
}
|
|
163
180
|
|
|
164
181
|
event.preventDefault();
|
|
165
|
-
|
|
182
|
+
keyboardFocused.value = false;
|
|
166
183
|
dragging.value = true;
|
|
184
|
+
rangeInput.value?.focus({ preventScroll: true });
|
|
185
|
+
dragPointerId = event.pointerId;
|
|
186
|
+
dragWindow = control.value.ownerDocument.defaultView;
|
|
187
|
+
emit('update:dragging', true);
|
|
167
188
|
updateFromPointer(event);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
189
|
+
dragWindow.addEventListener('pointermove', moveDrag);
|
|
190
|
+
dragWindow.addEventListener('pointerup', endDrag);
|
|
191
|
+
dragWindow.addEventListener('pointercancel', endDrag);
|
|
192
|
+
dragWindow.addEventListener('blur', endDrag);
|
|
171
193
|
}
|
|
172
194
|
|
|
195
|
+
/** Follows only the pointer that started the current drag. */
|
|
173
196
|
function moveDrag(event) {
|
|
197
|
+
if (event.pointerId !== dragPointerId) return;
|
|
198
|
+
if (field.disabled.value || field.readOnly.value) {
|
|
199
|
+
endDrag();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
174
202
|
event.preventDefault();
|
|
175
203
|
updateFromPointer(event);
|
|
176
204
|
}
|
|
177
205
|
|
|
178
|
-
|
|
206
|
+
/** Releases drag listeners on completion, cancellation, window blur, or unmount. */
|
|
207
|
+
function endDrag(event) {
|
|
208
|
+
if (event?.pointerId != null && event.pointerId !== dragPointerId) return;
|
|
209
|
+
const wasDragging = dragging.value;
|
|
179
210
|
dragging.value = false;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
dragWindow?.removeEventListener('pointermove', moveDrag);
|
|
212
|
+
dragWindow?.removeEventListener('pointerup', endDrag);
|
|
213
|
+
dragWindow?.removeEventListener('pointercancel', endDrag);
|
|
214
|
+
dragWindow?.removeEventListener('blur', endDrag);
|
|
215
|
+
dragWindow = undefined;
|
|
216
|
+
dragPointerId = undefined;
|
|
217
|
+
if (wasDragging) emit('update:dragging', false);
|
|
183
218
|
}
|
|
184
219
|
|
|
220
|
+
/** Writes the pointer's clamped and step-aligned value to the shared field state. */
|
|
185
221
|
function updateFromPointer(event) {
|
|
186
222
|
field.onInput(valueFromPointer(event));
|
|
187
223
|
}
|
|
188
224
|
|
|
225
|
+
/** Maps horizontal pointer position onto the finite numeric range. */
|
|
189
226
|
function valueFromPointer(event) {
|
|
190
227
|
const rect = control.value?.getBoundingClientRect();
|
|
191
228
|
const min = Number(props.min);
|
|
192
229
|
const max = Number(props.max);
|
|
193
230
|
const range = max - min;
|
|
194
231
|
|
|
195
|
-
if (!rect || !Number.isFinite(min) || !Number.isFinite(max) || range <= 0) return displayValue.value;
|
|
232
|
+
if (!rect?.width || !Number.isFinite(min) || !Number.isFinite(max) || range <= 0) return displayValue.value;
|
|
196
233
|
|
|
197
234
|
const ratio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
|
|
198
235
|
return snapValue(min + ratio * range);
|
|
199
236
|
}
|
|
200
237
|
|
|
238
|
+
/** Implements native slider keyboard navigation with the same bounds and stepping as pointer input. */
|
|
201
239
|
function onKeydown(event) {
|
|
202
240
|
if (field.disabled.value || field.readOnly.value) return;
|
|
241
|
+
keyboardFocused.value = true;
|
|
203
242
|
|
|
204
243
|
const step = stepSize();
|
|
205
244
|
const current = displayValue.value;
|
|
@@ -219,6 +258,19 @@ function onKeydown(event) {
|
|
|
219
258
|
field.onInput(snapValue(next));
|
|
220
259
|
}
|
|
221
260
|
|
|
261
|
+
/** Reveals keyboard focus while excluding the programmatic focus used during pointer scrubbing. */
|
|
262
|
+
function onFocus(event) {
|
|
263
|
+
keyboardFocused.value = !dragging.value;
|
|
264
|
+
field.onFocus(event);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Clears the keyboard indicator and preserves shared field blur and validation behavior. */
|
|
268
|
+
function onBlur(event) {
|
|
269
|
+
keyboardFocused.value = false;
|
|
270
|
+
field.onBlur(event);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Clamps a requested value and snaps it to the nearest increment relative to the minimum. */
|
|
222
274
|
function snapValue(value) {
|
|
223
275
|
const min = Number(props.min);
|
|
224
276
|
const max = Number(props.max);
|
|
@@ -234,16 +286,19 @@ function snapValue(value) {
|
|
|
234
286
|
return roundToStep(next, step);
|
|
235
287
|
}
|
|
236
288
|
|
|
289
|
+
/** Resolves a positive step, falling back to the native range default. */
|
|
237
290
|
function stepSize() {
|
|
238
291
|
const step = Number(props.step);
|
|
239
292
|
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
240
293
|
}
|
|
241
294
|
|
|
295
|
+
/** Removes floating-point noise without losing precision required by the configured step. */
|
|
242
296
|
function roundToStep(value, step) {
|
|
243
297
|
const decimals = decimalPlaces(step);
|
|
244
298
|
return Number(value.toFixed(Math.min(12, decimals + 2)));
|
|
245
299
|
}
|
|
246
300
|
|
|
301
|
+
/** Counts decimal precision for ordinary and negative-exponent step values. */
|
|
247
302
|
function decimalPlaces(value) {
|
|
248
303
|
const text = String(value);
|
|
249
304
|
if (text.includes('e-')) return Number(text.split('e-')[1]) || 0;
|
|
@@ -259,9 +314,10 @@ onBeforeUnmount(endDrag);
|
|
|
259
314
|
<div class="flex w-full items-center gap-3">
|
|
260
315
|
<div
|
|
261
316
|
ref="control"
|
|
262
|
-
:class="
|
|
317
|
+
:class="controlClassName"
|
|
263
318
|
:style="rangeStyle"
|
|
264
319
|
:data-dragging="dragging ? 'true' : undefined"
|
|
320
|
+
:data-keyboard-focus="keyboardFocused ? '' : undefined"
|
|
265
321
|
@pointerdown="beginDrag"
|
|
266
322
|
>
|
|
267
323
|
<span aria-hidden="true" :class="trackClassName"></span>
|
|
@@ -270,6 +326,8 @@ onBeforeUnmount(endDrag);
|
|
|
270
326
|
<input
|
|
271
327
|
ref="rangeInput"
|
|
272
328
|
v-bind="rangeInputAttrs"
|
|
329
|
+
:aria-label="$attrs['aria-label']"
|
|
330
|
+
:aria-valuetext="$attrs['aria-valuetext']"
|
|
273
331
|
type="range"
|
|
274
332
|
:min="min"
|
|
275
333
|
:max="max"
|
|
@@ -278,8 +336,8 @@ onBeforeUnmount(endDrag);
|
|
|
278
336
|
:class="inputClass"
|
|
279
337
|
@input="onInput($event.target.value, $event)"
|
|
280
338
|
@keydown="onKeydown"
|
|
281
|
-
@focus="
|
|
282
|
-
@blur="
|
|
339
|
+
@focus="onFocus"
|
|
340
|
+
@blur="onBlur"
|
|
283
341
|
/>
|
|
284
342
|
</div>
|
|
285
343
|
<output v-if="showValue" class="min-w-10 text-right font-mono text-xs text-muted-fg">
|
|
@@ -288,9 +346,3 @@ onBeforeUnmount(endDrag);
|
|
|
288
346
|
</div>
|
|
289
347
|
</DomField>
|
|
290
348
|
</template>
|
|
291
|
-
|
|
292
|
-
<style scoped>
|
|
293
|
-
.dom-range-input__bar {
|
|
294
|
-
box-shadow: inset 0 1px 2px color-mix(in oklch, var(--border) 70%, transparent);
|
|
295
|
-
}
|
|
296
|
-
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed } from 'vue';
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
3
3
|
import DomIconButton from '../../components/icon-button/DomIconButton.vue';
|
|
4
4
|
import DomField from '../field/DomField.vue';
|
|
5
5
|
import { fieldProps } from '../field/fieldProps.js';
|
|
@@ -23,11 +23,11 @@ defineOptions({
|
|
|
23
23
|
{ name: 'attachments', payload: '{ value, disabled, readOnly }', description: 'File, image, reference, or context previews shown above the text.' },
|
|
24
24
|
{ name: 'leading', payload: '{ value, submit, disabled }', description: 'Controls at the start of the bottom action row, such as add or tool buttons.' },
|
|
25
25
|
{ name: 'actions', payload: '{ value, submit, disabled }', description: 'Additional controls before the built-in submit action.' },
|
|
26
|
-
{ name: 'submit', payload: '{ submit, disabled, submitting, label }', description: 'Replace the built-in circular submit button.' },
|
|
26
|
+
{ name: 'submit', payload: '{ submit, disabled, submitting, label, pressed }', description: 'Replace the built-in circular submit button. Bind pressed to DomButton or DomIconButton to retain keyboard submit feedback.' },
|
|
27
27
|
],
|
|
28
28
|
keyboard: [
|
|
29
|
-
{ keys: 'Enter', action: 'Submit when submitOnEnter is enabled.' },
|
|
30
|
-
{ keys: 'Shift + Enter', action: 'Insert a new line.' },
|
|
29
|
+
{ keys: 'Enter', action: 'Submit with button press feedback when submitOnEnter is enabled; otherwise insert a new line.' },
|
|
30
|
+
{ keys: 'Shift + Enter', action: 'Insert a new line when submitOnEnter is enabled; otherwise submit with button press feedback.' },
|
|
31
31
|
],
|
|
32
32
|
},
|
|
33
33
|
});
|
|
@@ -77,7 +77,7 @@ const props = defineProps({
|
|
|
77
77
|
submitOnEnter: {
|
|
78
78
|
type: Boolean,
|
|
79
79
|
default: true,
|
|
80
|
-
_edit: { group: 'Submit props', description: '
|
|
80
|
+
_edit: { group: 'Submit props', description: 'When enabled, Enter submits and Shift + Enter adds a new line. When disabled, Enter adds a new line and Shift + Enter submits.' },
|
|
81
81
|
},
|
|
82
82
|
allowEmptySubmit: {
|
|
83
83
|
type: Boolean,
|
|
@@ -107,6 +107,85 @@ const { textarea, resize } = useTextareaAutosize({
|
|
|
107
107
|
rows,
|
|
108
108
|
maxRows,
|
|
109
109
|
});
|
|
110
|
+
const submitPressed = ref(false);
|
|
111
|
+
let enterHeld = false;
|
|
112
|
+
let pressTimer;
|
|
113
|
+
let ownerWindow;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Keeps quick submit shortcuts visible while retaining feedback for a held key.
|
|
117
|
+
*
|
|
118
|
+
* @returns {void}
|
|
119
|
+
*/
|
|
120
|
+
function pressSubmit() {
|
|
121
|
+
clearTimeout(pressTimer);
|
|
122
|
+
enterHeld = true;
|
|
123
|
+
submitPressed.value = true;
|
|
124
|
+
pressTimer = setTimeout(() => {
|
|
125
|
+
pressTimer = undefined;
|
|
126
|
+
if (!enterHeld) submitPressed.value = false;
|
|
127
|
+
}, 150);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Releases Enter feedback even if submission moved focus or disabled the field.
|
|
132
|
+
*
|
|
133
|
+
* @param {KeyboardEvent} event Native window keyup event.
|
|
134
|
+
* @returns {void}
|
|
135
|
+
*/
|
|
136
|
+
function onKeyup(event) {
|
|
137
|
+
if (event.key !== 'Enter') return;
|
|
138
|
+
enterHeld = false;
|
|
139
|
+
if (pressTimer === undefined) submitPressed.value = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Clears pending feedback when focus leaves the composer or its window.
|
|
144
|
+
*
|
|
145
|
+
* @returns {void}
|
|
146
|
+
*/
|
|
147
|
+
function resetSubmitPress() {
|
|
148
|
+
clearTimeout(pressTimer);
|
|
149
|
+
pressTimer = undefined;
|
|
150
|
+
enterHeld = false;
|
|
151
|
+
submitPressed.value = false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Preserves field blur handling and cancels any outstanding Enter press.
|
|
156
|
+
*
|
|
157
|
+
* @param {FocusEvent} event Native textarea blur event.
|
|
158
|
+
* @returns {void}
|
|
159
|
+
*/
|
|
160
|
+
function onBlur(event) {
|
|
161
|
+
resetSubmitPress();
|
|
162
|
+
field.onBlur(event);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Observes key release and window blur in the textarea's owning document.
|
|
167
|
+
*
|
|
168
|
+
* @returns {void}
|
|
169
|
+
*/
|
|
170
|
+
function mountPressListeners() {
|
|
171
|
+
ownerWindow = textarea.value?.ownerDocument.defaultView ?? window;
|
|
172
|
+
ownerWindow?.addEventListener('keyup', onKeyup, true);
|
|
173
|
+
ownerWindow?.addEventListener('blur', resetSubmitPress);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Removes native listeners and timers when the composer is unmounted.
|
|
178
|
+
*
|
|
179
|
+
* @returns {void}
|
|
180
|
+
*/
|
|
181
|
+
function unmountPressListeners() {
|
|
182
|
+
ownerWindow?.removeEventListener('keyup', onKeyup, true);
|
|
183
|
+
ownerWindow?.removeEventListener('blur', resetSubmitPress);
|
|
184
|
+
resetSubmitPress();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
onMounted(mountPressListeners);
|
|
188
|
+
onBeforeUnmount(unmountPressListeners);
|
|
110
189
|
|
|
111
190
|
/**
|
|
112
191
|
* Writes native composer input through the shared form-field contract.
|
|
@@ -131,7 +210,7 @@ function submit(event) {
|
|
|
131
210
|
}
|
|
132
211
|
|
|
133
212
|
/**
|
|
134
|
-
* Exposes keyboard activity and applies the
|
|
213
|
+
* Exposes keyboard activity and applies the selected submit/newline shortcut pair.
|
|
135
214
|
*
|
|
136
215
|
* @param {KeyboardEvent} event Native keydown event.
|
|
137
216
|
* @returns {void}
|
|
@@ -140,12 +219,13 @@ function onKeydown(event) {
|
|
|
140
219
|
emit('keydown', event);
|
|
141
220
|
if (
|
|
142
221
|
event.defaultPrevented
|
|
143
|
-
|| !props.submitOnEnter
|
|
144
222
|
|| event.isComposing
|
|
145
223
|
|| event.key !== 'Enter'
|
|
146
|
-
|| event.shiftKey
|
|
224
|
+
|| event.shiftKey === props.submitOnEnter
|
|
147
225
|
) return;
|
|
148
226
|
event.preventDefault();
|
|
227
|
+
if (event.repeat || enterHeld || !canSubmit.value) return;
|
|
228
|
+
pressSubmit();
|
|
149
229
|
submit(event);
|
|
150
230
|
}
|
|
151
231
|
|
|
@@ -198,7 +278,7 @@ defineExpose({ focus, resize });
|
|
|
198
278
|
@input="onInput"
|
|
199
279
|
@keydown="onKeydown"
|
|
200
280
|
@focus="field.onFocus"
|
|
201
|
-
@blur="
|
|
281
|
+
@blur="onBlur"
|
|
202
282
|
></textarea>
|
|
203
283
|
|
|
204
284
|
<div
|
|
@@ -217,11 +297,13 @@ defineExpose({ focus, resize });
|
|
|
217
297
|
:disabled="!canSubmit"
|
|
218
298
|
:submitting="submitting"
|
|
219
299
|
:label="submitLabel"
|
|
300
|
+
:pressed="submitPressed"
|
|
220
301
|
>
|
|
221
302
|
<DomIconButton
|
|
222
303
|
:label="submitLabel"
|
|
223
304
|
:disabled="!canSubmit"
|
|
224
305
|
:loading="submitting"
|
|
306
|
+
:pressed="submitPressed"
|
|
225
307
|
variant="primary"
|
|
226
308
|
icon="M12 19V5m0 0-6 6m6-6 6 6"
|
|
227
309
|
@click="submit"
|
|
@@ -33,7 +33,7 @@ const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
|
|
|
33
33
|
|
|
34
34
|
const root = ref(null);
|
|
35
35
|
const field = useField(props, emit, { idPrefix: 'dom-toggle' });
|
|
36
|
-
const labelId = computed(() => (props.label ? `${field.id.value}-label` : ''));
|
|
36
|
+
const labelId = computed(() => (props.label && props.chrome !== 'none' ? `${field.id.value}-label` : ''));
|
|
37
37
|
const sizeConfig = computed(() => ({
|
|
38
38
|
sm: {
|
|
39
39
|
wrapper: 'gap-2',
|
|
@@ -60,7 +60,7 @@ export class DomElementBase extends (isBrowser ? HTMLElement : class {}) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
/** Keeps Tab navigation inside a panel, including panels adopted into another document. */
|
|
64
64
|
export function focusTrap(container) {
|
|
65
65
|
const sel = [
|
|
66
66
|
'a[href]', 'button:not([disabled])', 'input:not([disabled])',
|
|
@@ -69,14 +69,19 @@ export function focusTrap(container) {
|
|
|
69
69
|
].join(',');
|
|
70
70
|
const onKey = (e) => {
|
|
71
71
|
if (e.key !== 'Tab') return;
|
|
72
|
-
const nodes = container.querySelectorAll(sel);
|
|
73
|
-
if (!nodes.length)
|
|
72
|
+
const nodes = [...container.querySelectorAll(sel)].filter((node) => !node.closest('[inert], [hidden]') && node.getClientRects().length > 0);
|
|
73
|
+
if (!nodes.length) {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
container.focus({ preventScroll: true });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
74
78
|
const first = nodes[0];
|
|
75
79
|
const last = nodes[nodes.length - 1];
|
|
76
|
-
|
|
80
|
+
const active = container.ownerDocument.activeElement;
|
|
81
|
+
if (e.shiftKey && (active === first || active === container)) {
|
|
77
82
|
e.preventDefault();
|
|
78
83
|
last.focus();
|
|
79
|
-
} else if (!e.shiftKey &&
|
|
84
|
+
} else if (!e.shiftKey && (active === last || active === container)) {
|
|
80
85
|
e.preventDefault();
|
|
81
86
|
first.focus();
|
|
82
87
|
}
|
|
@@ -51,9 +51,9 @@ function getTemplate() {
|
|
|
51
51
|
-webkit-backdrop-filter: blur(4px);
|
|
52
52
|
opacity: 0;
|
|
53
53
|
transition:
|
|
54
|
-
opacity 140ms ease,
|
|
55
|
-
overlay 140ms ease allow-discrete,
|
|
56
|
-
display 140ms ease allow-discrete;
|
|
54
|
+
opacity var(--dom-dialog-backdrop-duration, 140ms) ease,
|
|
55
|
+
overlay var(--dom-dialog-backdrop-duration, 140ms) ease allow-discrete,
|
|
56
|
+
display var(--dom-dialog-backdrop-duration, 140ms) ease allow-discrete;
|
|
57
57
|
}
|
|
58
58
|
dialog[open]::backdrop { opacity: 1; }
|
|
59
59
|
:host([no-backdrop]) dialog::backdrop {
|
|
@@ -153,6 +153,7 @@ export class DomDialogElement extends DomElementBase {
|
|
|
153
153
|
events: [
|
|
154
154
|
{ name: 'dom:before-open', description: 'Fired immediately before the dialog opens so callers can prepare render state.' },
|
|
155
155
|
{ name: 'dom:open', description: 'Fired when the dialog opens.' },
|
|
156
|
+
{ name: 'dom:request-close', description: 'Cancelable dismissal request with { reason: "escape" | "backdrop" | "control" }. Prevent the event to keep the dialog open.' },
|
|
156
157
|
{ name: 'dom:close', description: 'Fired when the dialog closes (Esc, .close(), backdrop, or data-close).' },
|
|
157
158
|
{ name: 'dom:close-complete', description: 'Fired after the close transition completes so callers can release retained data.' },
|
|
158
159
|
],
|
|
@@ -207,16 +208,17 @@ export class DomDialogElement extends DomElementBase {
|
|
|
207
208
|
|
|
208
209
|
this.on(this, 'click', (ev) => {
|
|
209
210
|
const closer = ev.target.closest?.('[data-close]');
|
|
210
|
-
if (closer && this.contains(closer)) this.
|
|
211
|
+
if (closer && this.contains(closer)) this._requestClose('control');
|
|
211
212
|
});
|
|
212
213
|
|
|
213
214
|
this.on(this._dialog, 'click', (e) => {
|
|
214
215
|
if (this.hasAttribute('static')) return;
|
|
215
|
-
if (e.target === this._dialog) this.
|
|
216
|
+
if (e.target === this._dialog) this._requestClose('backdrop');
|
|
216
217
|
});
|
|
217
218
|
|
|
218
219
|
this.on(this._dialog, 'cancel', (e) => {
|
|
219
|
-
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
this._requestClose('escape');
|
|
220
222
|
});
|
|
221
223
|
this.on(this._dialog, 'keydown', (e) => {
|
|
222
224
|
if (this.hasAttribute('static') && e.key === 'Escape') e.preventDefault();
|
|
@@ -251,6 +253,23 @@ export class DomDialogElement extends DomElementBase {
|
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Ask consumers whether a user-initiated dismissal may close the dialog.
|
|
258
|
+
*
|
|
259
|
+
* @param {'escape'|'backdrop'|'control'} reason Dismissal source.
|
|
260
|
+
* @returns {void}
|
|
261
|
+
*/
|
|
262
|
+
_requestClose(reason) {
|
|
263
|
+
const request = new CustomEvent('dom:request-close', {
|
|
264
|
+
detail: { reason },
|
|
265
|
+
bubbles: true,
|
|
266
|
+
composed: true,
|
|
267
|
+
cancelable: true,
|
|
268
|
+
});
|
|
269
|
+
const blockedByStatic = this.hasAttribute('static') && reason !== 'control';
|
|
270
|
+
if (this.dispatchEvent(request) && !blockedByStatic) this._setOpen(false);
|
|
271
|
+
}
|
|
272
|
+
|
|
254
273
|
/** Fallback when Invoker Commands are unavailable — wire [commandfor] buttons by id. */
|
|
255
274
|
_wireCommandForButtons() {
|
|
256
275
|
const id = this.id;
|