@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
|
@@ -8,10 +8,16 @@ export { default as DomDialog } from '../../components/dialog/DomDialog.vue';
|
|
|
8
8
|
export { default as DomDialogStack } from '../../components/dialog/DomDialogStack.vue';
|
|
9
9
|
export { default as DomDialogSchemaForm } from '../../components/dialog/DomDialogSchemaForm.vue';
|
|
10
10
|
export { default as DomLightbox } from '../../components/lightbox/DomLightbox.vue';
|
|
11
|
+
export { default as DomVideoFeed } from '../../components/video-feed/DomVideoFeed.vue';
|
|
12
|
+
export { default as DomAudioControls } from '../../components/audio-controls/DomAudioControls.vue';
|
|
13
|
+
export { default as DomAudioInput } from '../../components/audio-input/DomAudioInput.vue';
|
|
14
|
+
export { default as DomCameraInput } from '../../components/camera-input/DomCameraInput.vue';
|
|
15
|
+
export { default as DomAudioVisualizer } from '../../visual/audio-visualizer/DomAudioVisualizer.vue';
|
|
11
16
|
export { default as DomTabs } from '../../components/tabs/DomTabs.vue';
|
|
12
17
|
export { default as DomTooltip } from '../../components/tooltip/DomTooltip.vue';
|
|
13
18
|
export { vDomTooltip } from '../../components/tooltip/vDomTooltip.js';
|
|
14
19
|
export { default as DomAccordion } from '../../components/accordion/DomAccordion.vue';
|
|
20
|
+
export { default as DomMenuStack } from '../../components/menu-stack/DomMenuStack.vue';
|
|
15
21
|
export { default as DomMenu } from '../../components/menu/DomMenu.vue';
|
|
16
22
|
export { default as DomMenuItem } from '../../components/menu/DomMenuItem.vue';
|
|
17
23
|
export { default as MenuItem } from '../../components/menu/DomMenuItem.vue';
|
|
@@ -43,6 +49,7 @@ export { default as DomPopover } from '../../components/popover/DomPopover.vue';
|
|
|
43
49
|
export { default as DomDrawer } from '../../components/drawer/DomDrawer.vue';
|
|
44
50
|
export { default as DomToastStack } from '../../components/toast/DomToastStack.vue';
|
|
45
51
|
export { default as DomToastItem } from '../../components/toast/DomToastItem.vue';
|
|
52
|
+
export { default as DomNotificationInbox } from '../../data/notification-inbox/DomNotificationInbox.vue';
|
|
46
53
|
export { default as DomCommandPalette } from '../../components/command-palette/DomCommandPalette.vue';
|
|
47
54
|
export { default as DomContextMenu } from '../../components/context-menu/DomContextMenu.vue';
|
|
48
55
|
export { default as DomDataGrid } from '../../data/data-grid/DomDataGrid.vue';
|
|
@@ -51,6 +58,8 @@ export { default as DomActivityTimeline } from '../../data/activity-timeline/Dom
|
|
|
51
58
|
export { default as DomKanban } from '../../data/kanban/DomKanban.vue';
|
|
52
59
|
export { default as DomMonthCalendar } from '../../data/month-calendar/DomMonthCalendar.vue';
|
|
53
60
|
export { default as DomWeekPlanner } from '../../data/week-planner/DomWeekPlanner.vue';
|
|
61
|
+
export { default as DomGantt } from '../../data/gantt/DomGantt.vue';
|
|
62
|
+
export { ganttProps } from '../../data/gantt/DomGantt.vue';
|
|
54
63
|
export { default as DomMediaBrowser } from '../../data/media-browser/DomMediaBrowser.vue';
|
|
55
64
|
export { default as DomDocumentEditor } from '../../data/document-editor/DomDocumentEditor.vue';
|
|
56
65
|
export { default as DomJsonViewer } from '../../dev/json-viewer/DomJsonViewer.vue';
|
|
@@ -76,6 +85,7 @@ export { monthCalendarProps } from '../../data/month-calendar/DomMonthCalendar.v
|
|
|
76
85
|
export { weekPlannerProps } from '../../data/week-planner/DomWeekPlanner.vue';
|
|
77
86
|
export { popoverProps } from '../../components/popover/DomPopover.vue';
|
|
78
87
|
export { useToasts } from '../../components/toast/useToasts.js';
|
|
88
|
+
export { notificationTone, notificationToast } from '../../data/notification-inbox/notificationPresentation.js';
|
|
79
89
|
export { useDialogs } from '../../components/dialog/useDialogs.js';
|
|
80
90
|
export {
|
|
81
91
|
THEME_MODE_STORAGE_KEY,
|
|
@@ -207,12 +217,45 @@ export {
|
|
|
207
217
|
// Mobile primitives
|
|
208
218
|
export { default as DomAppBottomNav } from '../../mobile/bottom-nav/DomAppBottomNav.vue';
|
|
209
219
|
export { default as DomAppListItem } from '../../mobile/list-item/DomAppListItem.vue';
|
|
220
|
+
export { default as DomAppProfile } from '../../mobile/profile/DomAppProfile.vue';
|
|
210
221
|
export { default as DomAppShell } from '../../mobile/shell/DomAppShell.vue';
|
|
211
222
|
export { default as DomAppTopBar } from '../../mobile/top-bar/DomAppTopBar.vue';
|
|
212
223
|
export { default as DomActionSheet } from '../../mobile/action-sheet/DomActionSheet.vue';
|
|
224
|
+
export { default as DomBottomSheet } from '../../mobile/bottom-sheet/DomBottomSheet.vue';
|
|
225
|
+
export { default as DomSwipeActions } from '../../mobile/swipe-actions/DomSwipeActions.vue';
|
|
213
226
|
export { default as DomSafeArea } from '../../mobile/safe-area/DomSafeArea.vue';
|
|
214
227
|
export { default as DomAppStack } from '../../mobile/stack/DomAppStack.vue';
|
|
215
228
|
export { default as DomAppStackScreen } from '../../mobile/stack/AppStackScreen.vue';
|
|
216
229
|
|
|
217
230
|
// Rehydration
|
|
218
231
|
export { default as DomRenderer } from '../../components/playground/DomRenderer.vue';
|
|
232
|
+
|
|
233
|
+
// Data visualisation
|
|
234
|
+
export { default as DomLineChart } from '../../charts/line-chart/DomLineChart.vue';
|
|
235
|
+
export { default as DomAreaChart } from '../../charts/area-chart/DomAreaChart.vue';
|
|
236
|
+
export { default as DomBarChart } from '../../charts/bar-chart/DomBarChart.vue';
|
|
237
|
+
export { default as DomDonutChart } from '../../charts/donut-chart/DomDonutChart.vue';
|
|
238
|
+
export { default as DomSparkline } from '../../charts/sparkline/DomSparkline.vue';
|
|
239
|
+
export { default as DomMetricCard } from '../../charts/metric-card/DomMetricCard.vue';
|
|
240
|
+
export { default as DomDateRangePicker } from '../../charts/date-range-picker/DomDateRangePicker.vue';
|
|
241
|
+
export { default as DomChartContainer } from '../../charts/_shared/ChartContainer.vue';
|
|
242
|
+
export { default as DomChartLegend } from '../../charts/_shared/ChartLegend.vue';
|
|
243
|
+
export { default as DomChartTooltip } from '../../charts/_shared/ChartTooltip.vue';
|
|
244
|
+
|
|
245
|
+
// Beta UI designer primitives.
|
|
246
|
+
export { default as DomDesignerFrame } from '../../designer/frame/DomDesignerFrame.vue';
|
|
247
|
+
export { default as DomFrameLayoutEditor } from '../../designer/frame/DomFrameLayoutEditor.vue';
|
|
248
|
+
export { createFrameLayout, frameLayoutStyle } from '../../designer/frame/frameLayout.js';
|
|
249
|
+
export { default as DomColorSwatch } from '../../designer/color-swatch/DomColorSwatch.vue';
|
|
250
|
+
export { default as DomShadowEditor } from '../../designer/shadow-editor/DomShadowEditor.vue';
|
|
251
|
+
export { default as DomDesignerLayers } from '../../designer/layers/DomDesignerLayers.vue';
|
|
252
|
+
export { default as DomDesignerStage } from '../../designer/stage/DomDesignerStage.vue';
|
|
253
|
+
export { createShadow, shadowListToCss } from '../../designer/shadow-editor/shadowModel.js';
|
|
254
|
+
|
|
255
|
+
export { default as DomBorderEditor } from '../../designer/border-editor/DomBorderEditor.vue';
|
|
256
|
+
export { createDesignerBorder, borderToCss } from '../../designer/border-editor/borderModel.js';
|
|
257
|
+
|
|
258
|
+
export { default as DomBackgroundEditor } from '../../designer/background-editor/DomBackgroundEditor.vue';
|
|
259
|
+
export { createBackgroundState, createFill, backgroundImageToCss } from '../../designer/background-editor/backgroundModel.js';
|
|
260
|
+
export { default as DomBackdropEditor } from '../../designer/backdrop-editor/DomBackdropEditor.vue';
|
|
261
|
+
export { createBackdrop, backdropToCss } from '../../designer/backdrop-editor/backdropModel.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns sorted, unique sheet fractions, retaining zero only as a dismiss stop.
|
|
3
|
+
* Invalid input falls back to a usable half-height and expanded sheet.
|
|
4
|
+
* @param {number[]} values Requested fractions of the available height.
|
|
5
|
+
* @returns {number[]} Normalized stops in the inclusive range zero to one.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeSnapPoints(values) {
|
|
8
|
+
const points = [...new Set((Array.isArray(values) ? values : []).filter((value) => Number.isFinite(value) && value >= 0 && value <= 1))].sort((a, b) => a - b);
|
|
9
|
+
return points.some((point) => point > 0) ? points : [0, 0.5, 0.9];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Chooses the nearest sheet stop after a short projection of release velocity.
|
|
14
|
+
* @param {number[]} points Normalized stops.
|
|
15
|
+
* @param {number} position Current height as a fraction of the available height.
|
|
16
|
+
* @param {number} velocity Height change per millisecond, in fractional units.
|
|
17
|
+
* @returns {number} Destination stop.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveSnapPoint(points, position, velocity = 0) {
|
|
20
|
+
const projected = Math.max(0, Math.min(1, position + velocity * 160));
|
|
21
|
+
return points.reduce((nearest, point) => Math.abs(point - projected) < Math.abs(nearest - projected) ? point : nearest, points[0]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Determines whether a released swipe closes, reveals buttons, or commits an action.
|
|
26
|
+
* Full swipes require explicit opt-in and deliberate travel, then project release
|
|
27
|
+
* velocity over 180ms so a shorter outward flick can cross the commit threshold.
|
|
28
|
+
* @param {object} options Measured gesture and action availability.
|
|
29
|
+
* @returns {'close'|'reveal'|'commit'} Release outcome.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveSwipeRelease({ distance, velocity = 0, actionWidth, rowWidth, fullSwipe = false, cancelled = false }) {
|
|
32
|
+
if (cancelled || !actionWidth) return 'close';
|
|
33
|
+
const projected = distance + velocity * 180;
|
|
34
|
+
if (fullSwipe && distance >= Math.max(48, rowWidth * 0.16) && projected >= Math.max(actionWidth + 48, rowWidth * 0.72)) return 'commit';
|
|
35
|
+
return distance + velocity * 120 >= Math.min(48, actionWidth * 0.5) ? 'reveal' : 'close';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Separates a likely software keyboard from browser chrome and pinch zoom.
|
|
40
|
+
* @param {object} options Current viewport and editable-focus measurements.
|
|
41
|
+
* @returns {number} Estimated keyboard height, or zero when not detected.
|
|
42
|
+
*/
|
|
43
|
+
export function keyboardHeight({ baseline, viewportHeight, scale = 1, editing = false }) {
|
|
44
|
+
const difference = Math.max(0, baseline - viewportHeight);
|
|
45
|
+
return editing && Math.abs(scale - 1) < 0.05 && difference > 120 ? Math.round(difference) : 0;
|
|
46
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { onBeforeUnmount, onMounted, ref, toValue, watch } from 'vue';
|
|
2
|
+
import { keyboardHeight } from './interactionMath.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Observes the owning window's keyboard-sized viewport changes without treating
|
|
6
|
+
* pinch zoom as a keyboard. An optional app-supplied inset supports native
|
|
7
|
+
* keyboards that overlay the webview without resizing its visual viewport.
|
|
8
|
+
* @param {import('vue').Ref<HTMLElement|null>} target Surface containing editable controls.
|
|
9
|
+
* @param {Function} enabled Returns whether the surface should adapt.
|
|
10
|
+
* @param {Function} inset Returns a native keyboard inset, or null for detection.
|
|
11
|
+
* @returns {{ open: import('vue').Ref<boolean>, height: import('vue').Ref<number>, available: import('vue').Ref<number>, top: import('vue').Ref<number> }} Reactive geometry.
|
|
12
|
+
*/
|
|
13
|
+
export function useKeyboardViewport(target, enabled, inset = () => null) {
|
|
14
|
+
const open = ref(false);
|
|
15
|
+
const height = ref(0);
|
|
16
|
+
const available = ref(0);
|
|
17
|
+
const top = ref(0);
|
|
18
|
+
let view;
|
|
19
|
+
let doc;
|
|
20
|
+
let baseline = 0;
|
|
21
|
+
let baselineWidth = 0;
|
|
22
|
+
let frame = 0;
|
|
23
|
+
let blurTimer = 0;
|
|
24
|
+
|
|
25
|
+
/** Measures only the viewport of the document containing this surface. */
|
|
26
|
+
function measure() {
|
|
27
|
+
frame = 0;
|
|
28
|
+
if (!view || !target.value) return;
|
|
29
|
+
const viewport = view.visualViewport;
|
|
30
|
+
const visible = viewport?.height || view.innerHeight;
|
|
31
|
+
const layout = Math.max(view.innerHeight, doc.documentElement.clientHeight);
|
|
32
|
+
const active = doc.activeElement;
|
|
33
|
+
const editable = active?.matches('textarea, input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="range"]):not([type="file"]), [contenteditable="true"], [contenteditable=""]');
|
|
34
|
+
const editing = Boolean(editable && target.value.contains(active) && !active.readOnly && !active.disabled);
|
|
35
|
+
if (Math.abs(view.innerWidth - baselineWidth) > 80) {
|
|
36
|
+
baseline = layout;
|
|
37
|
+
baselineWidth = view.innerWidth;
|
|
38
|
+
}
|
|
39
|
+
if (!editing && !open.value) baseline = layout;
|
|
40
|
+
baseline = Math.max(baseline, layout);
|
|
41
|
+
const supplied = toValue(inset);
|
|
42
|
+
const detected = keyboardHeight({ baseline, viewportHeight: visible, scale: viewport?.scale || 1, editing });
|
|
43
|
+
height.value = toValue(enabled) ? (supplied == null ? detected : Math.max(0, Number(supplied) || 0)) : 0;
|
|
44
|
+
open.value = height.value > 0;
|
|
45
|
+
top.value = viewport?.offsetTop || 0;
|
|
46
|
+
const origin = Math.max(0, target.value.getBoundingClientRect().top - top.value);
|
|
47
|
+
available.value = Math.max(0, Math.min(visible, baseline - height.value) - origin);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Coalesces focus, scroll, and resize notifications into a single frame. */
|
|
51
|
+
function schedule() {
|
|
52
|
+
if (view && !frame) frame = view.requestAnimationFrame(measure);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Waits for the next focus target before deciding that editing ended. */
|
|
56
|
+
function onBlur() {
|
|
57
|
+
view.clearTimeout(blurTimer);
|
|
58
|
+
blurTimer = view.setTimeout(schedule, 0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Binds after mount so adopted iframe nodes use their actual owner window. */
|
|
62
|
+
function mount() {
|
|
63
|
+
doc = target.value?.ownerDocument;
|
|
64
|
+
view = doc?.defaultView;
|
|
65
|
+
if (!view) return;
|
|
66
|
+
baseline = Math.max(view.innerHeight, doc.documentElement.clientHeight);
|
|
67
|
+
baselineWidth = view.innerWidth;
|
|
68
|
+
view.addEventListener('resize', schedule);
|
|
69
|
+
view.visualViewport?.addEventListener('resize', schedule);
|
|
70
|
+
view.visualViewport?.addEventListener('scroll', schedule);
|
|
71
|
+
doc.addEventListener('focusin', schedule);
|
|
72
|
+
doc.addEventListener('focusout', onBlur);
|
|
73
|
+
measure();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Releases listeners and pending callbacks when a surface is removed. */
|
|
77
|
+
function unmount() {
|
|
78
|
+
view?.cancelAnimationFrame(frame);
|
|
79
|
+
view?.clearTimeout(blurTimer);
|
|
80
|
+
view?.removeEventListener('resize', schedule);
|
|
81
|
+
view?.visualViewport?.removeEventListener('resize', schedule);
|
|
82
|
+
view?.visualViewport?.removeEventListener('scroll', schedule);
|
|
83
|
+
doc?.removeEventListener('focusin', schedule);
|
|
84
|
+
doc?.removeEventListener('focusout', onBlur);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
watch([enabled, inset], schedule);
|
|
88
|
+
onMounted(mount);
|
|
89
|
+
onBeforeUnmount(unmount);
|
|
90
|
+
return { open, height, available, top };
|
|
91
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, onMounted, ref, useId, watch } from 'vue';
|
|
2
|
+
import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue';
|
|
3
3
|
|
|
4
4
|
defineOptions({
|
|
5
5
|
__doc: {
|
|
6
6
|
name: 'Action sheet',
|
|
7
7
|
tag: '<DomActionSheet>',
|
|
8
|
-
description: 'A bottom action sheet for
|
|
8
|
+
description: 'A bottom action sheet for mobile and desktop, with bounded scrolling, keyboard dismissal, focus return, and drag dismissal. Works standalone as a viewport overlay. In DomAppShell, place it in the overlay slot and it stays inside that app.',
|
|
9
9
|
events: [
|
|
10
10
|
{ name: 'update:modelValue', payload: 'boolean', description: 'Fired when the sheet opens or closes.' },
|
|
11
11
|
{ name: 'select', payload: 'object', description: 'Fired with the selected action.' },
|
|
@@ -17,9 +17,9 @@ defineOptions({
|
|
|
17
17
|
title: 'Project actions',
|
|
18
18
|
description: 'Use for app commands that should stay near the thumb.',
|
|
19
19
|
actions: [
|
|
20
|
-
{ label: 'Share project', description: 'Open the native share sheet', value: 'share' },
|
|
21
|
-
{ label: 'Duplicate screen', value: 'duplicate' },
|
|
22
|
-
{ label: 'Delete draft', value: 'delete', variant: 'danger' },
|
|
20
|
+
{ label: 'Share project', description: 'Open the native share sheet', value: 'share', variant: '', disabled: false },
|
|
21
|
+
{ label: 'Duplicate screen', description: '', value: 'duplicate', variant: '', disabled: false },
|
|
22
|
+
{ label: 'Delete draft', description: '', value: 'delete', variant: 'danger', disabled: false },
|
|
23
23
|
],
|
|
24
24
|
},
|
|
25
25
|
},
|
|
@@ -27,6 +27,11 @@ defineOptions({
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
const props = defineProps({
|
|
30
|
+
contained: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: undefined,
|
|
33
|
+
_edit: { description: 'Defaults to contained inside DomAppShell, viewport otherwise. Explicit true requires a positioned, sized parent; prefer the shell overlay slot.' },
|
|
34
|
+
},
|
|
30
35
|
modelValue: {
|
|
31
36
|
type: Boolean,
|
|
32
37
|
default: false,
|
|
@@ -55,6 +60,7 @@ const props = defineProps({
|
|
|
55
60
|
items: {
|
|
56
61
|
type: 'DomForm',
|
|
57
62
|
properties: {
|
|
63
|
+
disabled: { type: 'boolean', label: 'Disabled', default: false },
|
|
58
64
|
label: {
|
|
59
65
|
type: 'string',
|
|
60
66
|
label: 'Label',
|
|
@@ -83,7 +89,7 @@ const props = defineProps({
|
|
|
83
89
|
},
|
|
84
90
|
},
|
|
85
91
|
},
|
|
86
|
-
description: 'Action rows: { label, description, value, variant }.',
|
|
92
|
+
description: 'Action rows: { label, description, value, variant, disabled }.',
|
|
87
93
|
},
|
|
88
94
|
},
|
|
89
95
|
cancelLabel: {
|
|
@@ -95,92 +101,123 @@ const props = defineProps({
|
|
|
95
101
|
const emit = defineEmits(['update:modelValue', 'select']);
|
|
96
102
|
|
|
97
103
|
const drawer = ref(null);
|
|
104
|
+
const shell = inject('dom-app-shell', null);
|
|
105
|
+
const isContained = computed(() => props.contained ?? Boolean(shell));
|
|
106
|
+
const teleportTarget = ref(null);
|
|
107
|
+
let ready = false;
|
|
108
|
+
let removeListeners = null;
|
|
98
109
|
const uniqueId = useId();
|
|
99
110
|
const titleId = computed(() => (props.title ? `dom-action-sheet-${uniqueId}-title` : ''));
|
|
111
|
+
const descriptionId = `dom-action-sheet-${uniqueId}-description`;
|
|
100
112
|
|
|
113
|
+
/** Requests dismissal of the controlled action sheet. */
|
|
101
114
|
function close() {
|
|
102
115
|
emit('update:modelValue', false);
|
|
103
116
|
}
|
|
104
117
|
|
|
118
|
+
/** Emits an enabled action and then requests sheet dismissal. */
|
|
105
119
|
function selectAction(action) {
|
|
120
|
+
if (action.disabled) return;
|
|
106
121
|
emit('select', action);
|
|
107
122
|
close();
|
|
108
123
|
}
|
|
109
124
|
|
|
125
|
+
/** Synchronizes Vue state after the headless drawer has upgraded. */
|
|
110
126
|
function setOpen(value) {
|
|
111
|
-
if (drawer.value) drawer.value.open = value;
|
|
127
|
+
if (ready && drawer.value) drawer.value.open = value;
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
onMounted(async () => {
|
|
115
131
|
await import('../../lib/headless/drawer.js');
|
|
116
|
-
drawer.value
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
if (!drawer.value) return;
|
|
133
|
+
const element = drawer.value;
|
|
134
|
+
const onOpen = () => emit('update:modelValue', true);
|
|
135
|
+
const onClose = () => emit('update:modelValue', false);
|
|
136
|
+
const onState = (event) => emit('update:modelValue', event.detail.open);
|
|
137
|
+
element.addEventListener('dom:open', onOpen);
|
|
138
|
+
element.addEventListener('dom:close', onClose);
|
|
139
|
+
element.addEventListener('dom:state', onState);
|
|
140
|
+
removeListeners = () => {
|
|
141
|
+
element.removeEventListener('dom:open', onOpen);
|
|
142
|
+
element.removeEventListener('dom:close', onClose);
|
|
143
|
+
element.removeEventListener('dom:state', onState);
|
|
144
|
+
};
|
|
145
|
+
ready = true;
|
|
146
|
+
teleportTarget.value = element.ownerDocument.body;
|
|
147
|
+
await nextTick();
|
|
148
|
+
setOpen(props.modelValue);
|
|
123
149
|
});
|
|
124
150
|
|
|
125
151
|
watch(() => props.modelValue, setOpen);
|
|
152
|
+
onBeforeUnmount(() => {
|
|
153
|
+
ready = false;
|
|
154
|
+
removeListeners?.();
|
|
155
|
+
});
|
|
126
156
|
</script>
|
|
127
157
|
|
|
128
158
|
<template>
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
class="absolute inset-0 pointer-events-none data-[state=open]:pointer-events-auto"
|
|
159
|
+
<Teleport :to="teleportTarget || 'body'" :disabled="isContained || !teleportTarget">
|
|
160
|
+
<dom-drawer
|
|
161
|
+
ref="drawer"
|
|
162
|
+
side="bottom"
|
|
163
|
+
class="inset-0 z-50 block pointer-events-none data-[state=open]:pointer-events-auto"
|
|
164
|
+
:class="isContained ? 'absolute' : 'fixed'"
|
|
165
|
+
:aria-labelledby="titleId || null"
|
|
166
|
+
:aria-label="title ? null : 'Actions'"
|
|
167
|
+
:aria-describedby="description ? descriptionId : null"
|
|
139
168
|
>
|
|
140
169
|
<div
|
|
141
|
-
data-
|
|
170
|
+
data-overlay
|
|
142
171
|
data-no-defaults
|
|
143
|
-
class="absolute inset-0
|
|
144
|
-
|
|
145
|
-
<section
|
|
146
|
-
data-panel
|
|
147
|
-
data-no-defaults
|
|
148
|
-
class="absolute inset-x-3 bottom-3 translate-y-full outline-none transition-transform duration-300 ease-out data-[state=open]:translate-y-0"
|
|
149
|
-
:class="'pb-[env(safe-area-inset-bottom,0px)]'"
|
|
172
|
+
class="absolute inset-0 overflow-hidden pointer-events-none data-[state=open]:pointer-events-auto"
|
|
173
|
+
hidden
|
|
150
174
|
>
|
|
151
|
-
<
|
|
152
|
-
data-
|
|
153
|
-
|
|
154
|
-
class="
|
|
155
|
-
|
|
175
|
+
<div
|
|
176
|
+
data-backdrop
|
|
177
|
+
data-no-defaults
|
|
178
|
+
class="absolute inset-0 bg-black/40 opacity-0 backdrop-blur-sm transition-opacity duration-300 data-[state=open]:opacity-100 motion-reduce:transition-none"
|
|
179
|
+
></div>
|
|
180
|
+
<section
|
|
181
|
+
data-panel
|
|
182
|
+
data-no-defaults
|
|
183
|
+
class="absolute left-[max(0.75rem,var(--dom-safe-area-left,env(safe-area-inset-left,0px)))] right-[max(0.75rem,var(--dom-safe-area-right,env(safe-area-inset-right,0px)))] bottom-0 mx-auto flex max-h-[calc(100%-var(--dom-safe-area-top,env(safe-area-inset-top,0px))-0.75rem)] max-w-md flex-col pb-[max(0.75rem,var(--dom-safe-area-bottom,env(safe-area-inset-bottom,0px)))] translate-y-full outline-none transition-transform duration-300 ease-out data-[state=open]:translate-y-0 motion-reduce:transition-none"
|
|
156
184
|
>
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
185
|
+
<button
|
|
186
|
+
data-drag-handle
|
|
187
|
+
type="button"
|
|
188
|
+
class="group mx-auto flex h-11 w-full shrink-0 touch-none items-center justify-center focus-visible:outline-2 focus-visible:outline-ring"
|
|
189
|
+
aria-label="Close action sheet"
|
|
190
|
+
@click="close"
|
|
191
|
+
>
|
|
192
|
+
<span class="block h-1.5 w-12 rounded-full bg-white/70 shadow-sm transition group-hover:bg-white"></span>
|
|
193
|
+
</button>
|
|
194
|
+
<div class="skin-overlay min-h-0 overflow-y-auto overscroll-contain rounded-2xl">
|
|
195
|
+
<div v-if="title || description" class="border-b border-border px-5 py-4 text-center">
|
|
196
|
+
<h2 v-if="title" :id="titleId" class="text-sm font-semibold text-canvas-fg">{{ title }}</h2>
|
|
197
|
+
<p v-if="description" :id="descriptionId" class="mt-1 text-xs leading-5 text-muted-fg">{{ description }}</p>
|
|
198
|
+
</div>
|
|
199
|
+
<div>
|
|
200
|
+
<button
|
|
201
|
+
v-for="action in actions"
|
|
202
|
+
:key="action.value || action.label"
|
|
203
|
+
type="button"
|
|
204
|
+
class="block min-h-14 w-full border-b border-border px-5 py-3 text-center transition last:border-b-0 hover:bg-secondary/70 active:bg-secondary focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring disabled:opacity-40 disabled:pointer-events-none"
|
|
205
|
+
:disabled="action.disabled"
|
|
206
|
+
:class="action.variant === 'danger' ? 'text-destructive' : 'text-canvas-fg'"
|
|
207
|
+
@click="selectAction(action)"
|
|
208
|
+
>
|
|
209
|
+
<span class="block text-sm font-medium">{{ action.label }}</span>
|
|
210
|
+
<span v-if="action.description" class="mt-0.5 block text-xs text-muted-fg">{{ action.description }}</span>
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
176
213
|
</div>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
</
|
|
184
|
-
</
|
|
185
|
-
</
|
|
214
|
+
<button
|
|
215
|
+
data-close
|
|
216
|
+
type="button"
|
|
217
|
+
class="skin-control mt-2 min-h-14 w-full shrink-0 rounded-2xl px-5 text-sm font-semibold transition hover:brightness-[0.98] active:brightness-[0.96] focus-visible:outline-2 focus-visible:outline-ring"
|
|
218
|
+
>{{ cancelLabel }}</button>
|
|
219
|
+
</section>
|
|
220
|
+
</div>
|
|
221
|
+
</dom-drawer>
|
|
222
|
+
</Teleport>
|
|
186
223
|
</template>
|
|
@@ -50,19 +50,21 @@ const activeValue = computed(() => props.modelValue || props.items[0]?.value ||
|
|
|
50
50
|
* @returns {void}
|
|
51
51
|
*/
|
|
52
52
|
function selectItem(item) {
|
|
53
|
+
if (item.disabled) return;
|
|
53
54
|
emit('update:modelValue', item.value);
|
|
54
55
|
emit('change', item);
|
|
55
56
|
}
|
|
56
57
|
</script>
|
|
57
58
|
|
|
58
59
|
<template>
|
|
59
|
-
<nav class="
|
|
60
|
+
<nav class="shrink-0 border-t border-border bg-canvas/95 pl-[max(0.5rem,var(--dom-safe-area-left,env(safe-area-inset-left,0px)))] pr-[max(0.5rem,var(--dom-safe-area-right,env(safe-area-inset-right,0px)))] pt-1 pb-[max(0.25rem,var(--dom-safe-area-bottom,env(safe-area-inset-bottom,0px)))]" aria-label="Primary">
|
|
60
61
|
<div class="grid auto-cols-fr grid-flow-col gap-1">
|
|
61
62
|
<button
|
|
62
63
|
v-for="item in items"
|
|
63
64
|
:key="item.value"
|
|
64
65
|
type="button"
|
|
65
|
-
class="relative flex min-h-14 min-w-0 flex-col items-center justify-center gap-1 rounded-
|
|
66
|
+
class="relative flex min-h-14 min-w-0 flex-col items-center justify-center gap-1 rounded-xl px-2 text-xs font-medium text-muted-fg transition active:scale-95 focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50 disabled:pointer-events-none"
|
|
67
|
+
:disabled="item.disabled"
|
|
66
68
|
:class="item.value === activeValue && 'bg-secondary text-canvas-fg'"
|
|
67
69
|
:aria-current="item.value === activeValue ? 'page' : undefined"
|
|
68
70
|
@click="selectItem(item)"
|