gravity-dnd 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.eslintignore +3 -0
  2. package/.eslintrc.cjs +21 -0
  3. package/.storybook/main.ts +15 -0
  4. package/.storybook/preview.css +80 -0
  5. package/.storybook/preview.ts +15 -0
  6. package/LICENSE +373 -0
  7. package/README.md +292 -0
  8. package/index.ts +19 -0
  9. package/package.json +64 -0
  10. package/public/.gitkeep +0 -0
  11. package/src/Gravity.stories.ts +207 -0
  12. package/src/components/DragAndDrop/DragAndDrop.scss +4 -0
  13. package/src/components/DragAndDrop/DragAndDrop.stories.ts +787 -0
  14. package/src/components/DragAndDrop/DragAndDrop.visuals.css +1 -0
  15. package/src/components/DragAndDrop/DragAndDrop.vue +23 -0
  16. package/src/components/DragAndDrop.scss +4 -0
  17. package/src/components/DragAndDrop.visuals.css +1 -0
  18. package/src/components/DragAndDrop.vue +23 -0
  19. package/src/components/Draggable/DragDropProvider.scss +4 -0
  20. package/src/components/Draggable/DragDropProvider.visuals.css +1 -0
  21. package/src/components/Draggable/DragDropProvider.vue +11 -0
  22. package/src/components/Draggable/DragPreviewOverlay.scss +21 -0
  23. package/src/components/Draggable/DragPreviewOverlay.visuals.css +3 -0
  24. package/src/components/Draggable/DragPreviewOverlay.vue +41 -0
  25. package/src/components/Draggable/Draggable.scss +86 -0
  26. package/src/components/Draggable/Draggable.stories.ts +232 -0
  27. package/src/components/Draggable/Draggable.visuals.css +8 -0
  28. package/src/components/Draggable/Draggable.vue +292 -0
  29. package/src/components/Draggable/contracts.ts +82 -0
  30. package/src/components/Draggable/internalDropLayer.ts +126 -0
  31. package/src/components/Draggable/useDragDropContext.ts +310 -0
  32. package/src/components/Pool/Pool.scss +107 -0
  33. package/src/components/Pool/Pool.stories.ts +155 -0
  34. package/src/components/Pool/Pool.visuals.css +25 -0
  35. package/src/components/Pool/Pool.vue +198 -0
  36. package/src/components/Slot/Slot.scss +48 -0
  37. package/src/components/Slot/Slot.stories.ts +299 -0
  38. package/src/components/Slot/Slot.visuals.css +15 -0
  39. package/src/components/Slot/Slot.vue +126 -0
  40. package/src/styles.css +15 -0
  41. package/styles.css +1 -0
  42. package/styles.scss +6 -0
  43. package/tsconfig.json +18 -0
  44. package/vite.config.ts +21 -0
@@ -0,0 +1 @@
1
+ /* No visual overrides for this component yet. */
@@ -0,0 +1,23 @@
1
+ <template lang="pug">
2
+ .st-profile-medalpicker2-dnd(@dragstart.prevent)
3
+ slot(:dragState="dragState" :cancelDrag="cancelDrag")
4
+ DragPreviewOverlay(
5
+ :visible="dragState.active && Boolean(dragState.item)"
6
+ :item="dragState.item"
7
+ :x="dragState.clientX"
8
+ :y="dragState.clientY"
9
+ :shiftX="dragState.shiftX"
10
+ :shiftY="dragState.shiftY"
11
+ :width="dragState.previewWidth"
12
+ :height="dragState.previewHeight"
13
+ )
14
+ slot(name="preview" :item="dragState.item")
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import DragPreviewOverlay from '../Draggable/DragPreviewOverlay.vue';
19
+ import { provideDragDropContext } from '../Draggable/useDragDropContext';
20
+ import './DragAndDrop.scss';
21
+
22
+ const { dragState, cancelDrag } = provideDragDropContext();
23
+ </script>
@@ -0,0 +1,4 @@
1
+ .st-profile-medalpicker2-dnd {
2
+ position: relative;
3
+ user-select: none;
4
+ }
@@ -0,0 +1 @@
1
+ /* No visual overrides for this component yet. */
@@ -0,0 +1,23 @@
1
+ <template lang="pug">
2
+ .st-profile-medalpicker2-dnd(@dragstart.prevent)
3
+ slot(:dragState="dragState" :cancelDrag="cancelDrag")
4
+ DragPreviewOverlay(
5
+ :visible="dragState.active && Boolean(dragState.item)"
6
+ :item="dragState.item"
7
+ :x="dragState.clientX"
8
+ :y="dragState.clientY"
9
+ :shiftX="dragState.shiftX"
10
+ :shiftY="dragState.shiftY"
11
+ :width="dragState.previewWidth"
12
+ :height="dragState.previewHeight"
13
+ )
14
+ slot(name="preview" :item="dragState.item")
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import DragPreviewOverlay from '../Draggable/DragPreviewOverlay.vue';
19
+ import { provideDragDropContext } from '../Draggable/useDragDropContext';
20
+ import './DragAndDrop.scss';
21
+
22
+ const { dragState, cancelDrag } = provideDragDropContext();
23
+ </script>
@@ -0,0 +1,4 @@
1
+ .st-gravity-provider {
2
+ position: relative;
3
+ user-select: none;
4
+ }
@@ -0,0 +1 @@
1
+ /* No visual overrides for this component yet. */
@@ -0,0 +1,11 @@
1
+ <template lang="pug">
2
+ .st-gravity-provider(@dragstart.prevent)
3
+ slot(:dragState="dragState" :cancelDrag="cancelDrag")
4
+ </template>
5
+
6
+ <script setup lang="ts">
7
+ import { provideDragDropContext } from './useDragDropContext';
8
+ import './DragDropProvider.scss';
9
+
10
+ const { dragState, cancelDrag } = provideDragDropContext();
11
+ </script>
@@ -0,0 +1,21 @@
1
+ .st-gravity-preview {
2
+ position: fixed;
3
+ left: 0;
4
+ top: 0;
5
+ z-index: 10000;
6
+ box-sizing: border-box;
7
+ pointer-events: none;
8
+ will-change: transform;
9
+ overflow: hidden;
10
+ opacity: 1 !important;
11
+ }
12
+
13
+ .st-gravity-preview__inner {
14
+ opacity: 1 !important;
15
+ width: 100%;
16
+ height: 100%;
17
+ box-sizing: border-box;
18
+ overflow: hidden;
19
+ text-align: center;
20
+ display: block;
21
+ }
@@ -0,0 +1,3 @@
1
+ .st-gravity-preview {
2
+ filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.45));
3
+ }
@@ -0,0 +1,41 @@
1
+ <template lang="pug">
2
+ Teleport(to="body")
3
+ .st-gravity-preview(v-if="visible && cloneReady" :style="previewStyle")
4
+ .st-gravity-preview__inner
5
+ slot(:item="item")
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { computed } from 'vue';
10
+ import './DragPreviewOverlay.scss';
11
+
12
+ const props = defineProps<{
13
+ visible: boolean;
14
+ item: unknown | null;
15
+ x: number;
16
+ y: number;
17
+ shiftX: number;
18
+ shiftY: number;
19
+ width: number;
20
+ height: number;
21
+ }>();
22
+
23
+ const cloneReady = computed(() => props.width > 0 && props.height > 0);
24
+
25
+ const previewStyle = computed(() => {
26
+ const w = props.width;
27
+ const h = props.height;
28
+ const tx = props.x - props.shiftX;
29
+ const ty = props.y - props.shiftY;
30
+ return {
31
+ transform: `translate3d(${tx}px, ${ty}px, 0)`,
32
+ width: `${w}px`,
33
+ height: `${h}px`,
34
+ minWidth: `${w}px`,
35
+ maxWidth: `${w}px`,
36
+ minHeight: `${h}px`,
37
+ maxHeight: `${h}px`,
38
+ boxSizing: 'border-box' as const,
39
+ };
40
+ });
41
+ </script>
@@ -0,0 +1,86 @@
1
+ .st-gravity-draggable {
2
+ display: inline-block;
3
+ cursor: grab;
4
+ }
5
+
6
+ .st-gravity-draggable__content {
7
+ display: block;
8
+ transform-origin: center;
9
+ transition:
10
+ opacity 120ms ease,
11
+ transform 120ms ease,
12
+ box-shadow 140ms ease,
13
+ filter 140ms ease;
14
+ }
15
+
16
+ .st-gravity-draggable--pressed {
17
+ cursor: grabbing;
18
+ }
19
+
20
+ .st-gravity-draggable--pressed .st-gravity-draggable__content {
21
+ transform: scale(0.98);
22
+ }
23
+
24
+ .st-gravity-draggable--dragging {
25
+ cursor: grabbing;
26
+ }
27
+
28
+ .st-gravity-draggable--dragging .st-gravity-draggable__content {
29
+ opacity: 0.35;
30
+ }
31
+
32
+ .st-gravity-draggable--feedback-accepted .st-gravity-draggable__content {
33
+ animation: gravity-draggable-accepted 180ms ease-out;
34
+ }
35
+
36
+ .st-gravity-draggable--feedback-rejected .st-gravity-draggable__content {
37
+ animation: gravity-draggable-rejected 190ms ease-out;
38
+ }
39
+
40
+ .st-gravity-draggable--feedback-returned .st-gravity-draggable__content {
41
+ animation: gravity-draggable-returned 210ms ease-out;
42
+ }
43
+
44
+ .st-gravity-draggable--disabled {
45
+ opacity: 0.45;
46
+ cursor: not-allowed;
47
+ }
48
+
49
+ @keyframes gravity-draggable-accepted {
50
+ 0% {
51
+ transform: scale(1);
52
+ }
53
+ 50% {
54
+ transform: scale(1.06);
55
+ }
56
+ 100% {
57
+ transform: scale(1);
58
+ }
59
+ }
60
+
61
+ @keyframes gravity-draggable-rejected {
62
+ 0% {
63
+ transform: translateX(0);
64
+ }
65
+ 25% {
66
+ transform: translateX(-3px);
67
+ }
68
+ 60% {
69
+ transform: translateX(3px);
70
+ }
71
+ 100% {
72
+ transform: translateX(0);
73
+ }
74
+ }
75
+
76
+ @keyframes gravity-draggable-returned {
77
+ 0% {
78
+ transform: translateY(0) scale(1);
79
+ }
80
+ 55% {
81
+ transform: translateY(-2px) scale(0.99);
82
+ }
83
+ 100% {
84
+ transform: translateY(0) scale(1);
85
+ }
86
+ }
@@ -0,0 +1,232 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { ref } from 'vue';
3
+ import GravityProvider from './DragDropProvider.vue';
4
+ import Slot from '../Slot/Slot.vue';
5
+ import Draggable from './Draggable.vue';
6
+ import type { GravityDraggableDropEvent, GravitySlotDropEvent } from './contracts';
7
+
8
+ interface DemoItem {
9
+ id: string;
10
+ label: string;
11
+ tier?: 'bronze' | 'gold';
12
+ }
13
+
14
+ const meta: Meta<typeof Draggable> = {
15
+ title: 'Gravity/Draggable',
16
+ component: Draggable,
17
+ tags: ['autodocs'],
18
+ parameters: { layout: 'centered' },
19
+ };
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof Draggable>;
23
+
24
+
25
+ export const Basic: Story = {
26
+ render: () => ({
27
+ setup() {
28
+ const item = { id: 'starter', label: 'Starter Item' } satisfies DemoItem;
29
+ const lastDrop = ref<string>('none');
30
+
31
+ function onDragEnd(event: GravityDraggableDropEvent<unknown>) {
32
+ lastDrop.value = `${event.target.kind}${event.target.containerId ? `:${event.target.containerId}` : ''}`;
33
+ }
34
+
35
+ return { item, lastDrop, onDragEnd };
36
+ },
37
+ components: { GravityProvider, Draggable },
38
+ template: `
39
+ <div style="width: 420px; max-width: 96vw; font-family: system-ui, sans-serif;">
40
+ <GravityProvider>
41
+ <Draggable
42
+ draggable-id="draggable-basic"
43
+ :item="item"
44
+ source-id="demo-basic"
45
+ source-kind="custom"
46
+ :source-index="0"
47
+ @drag-end="onDragEnd"
48
+ >
49
+ <template #default="{ dragging }">
50
+ <div
51
+ :style="{
52
+ padding: '10px 12px',
53
+ borderRadius: '8px',
54
+ border: '1px solid #d1d5db',
55
+ background: '#f9fafb',
56
+ opacity: dragging ? .9 : 1
57
+ }"
58
+ >
59
+ {{ item.label }}
60
+ </div>
61
+ </template>
62
+ </Draggable>
63
+ <div style="margin-top: 10px; font-size: 12px; color: #4b5563;">
64
+ Last drop target: {{ lastDrop }}
65
+ </div>
66
+ </GravityProvider>
67
+ </div>
68
+ `,
69
+ }),
70
+ };
71
+
72
+ export const Boundary: Story = {
73
+ render: () => ({
74
+ setup() {
75
+ const item = { id: 'bounded', label: 'Boundary Demo Item' } satisfies DemoItem;
76
+ const lastCancel = ref<string>('none');
77
+
78
+ return { item, lastCancel };
79
+ },
80
+ components: { GravityProvider, Draggable },
81
+ template: `
82
+ <div style="width: 460px; max-width: 96vw; font-family: system-ui, sans-serif;">
83
+ <GravityProvider>
84
+ <div
85
+ class="draggable-boundary-demo"
86
+ style="border: 1px dashed #94a3b8; border-radius: 10px; padding: 16px; min-height: 180px;"
87
+ >
88
+ <div style="font-size: 12px; color: #64748b; margin-bottom: 10px;">
89
+ Drag outside this dashed box to trigger out-of-bounds cancel.
90
+ </div>
91
+ <Draggable
92
+ draggable-id="draggable-boundary"
93
+ :item="item"
94
+ source-id="demo-boundary"
95
+ source-kind="custom"
96
+ :source-index="0"
97
+ drop-mode="floating"
98
+ boundary-selector=".draggable-boundary-demo"
99
+ @cancel="lastCancel = $event.reason"
100
+ >
101
+ <template #default="{ dragging }">
102
+ <div
103
+ :style="{
104
+ display: 'inline-flex',
105
+ padding: '10px 12px',
106
+ borderRadius: '8px',
107
+ border: '1px solid #d1d5db',
108
+ background: '#f9fafb',
109
+ opacity: dragging ? .9 : 1
110
+ }"
111
+ >
112
+ {{ item.label }}
113
+ </div>
114
+ </template>
115
+ </Draggable>
116
+ </div>
117
+ <div style="margin-top: 10px; font-size: 12px; color: #4b5563;">
118
+ Last cancel reason: {{ lastCancel }}
119
+ </div>
120
+ </GravityProvider>
121
+ </div>
122
+ `,
123
+ }),
124
+ };
125
+
126
+ export const DropModes: Story = {
127
+ render: () => ({
128
+ setup() {
129
+ const targetModeItem = { id: 'target-mode', label: 'Target Mode' } satisfies DemoItem;
130
+ const floatingModeItem = { id: 'floating-mode', label: 'Floating Mode' } satisfies DemoItem;
131
+ const slotItem = ref<DemoItem | null>(null);
132
+ const lastTargetModeResult = ref<string>('none');
133
+ const lastFloatingModeResult = ref<string>('none');
134
+
135
+ function onTargetModeDrop(event: GravityDraggableDropEvent<unknown>) {
136
+ lastTargetModeResult.value = `${event.target.kind}${event.target.containerId ? `:${event.target.containerId}` : ''}`;
137
+ }
138
+ function onFloatingModeDrop(event: GravityDraggableDropEvent<unknown>) {
139
+ lastFloatingModeResult.value = `${event.target.kind}${event.target.containerId ? `:${event.target.containerId}` : ''}`;
140
+ }
141
+ function onSlotDrop(event: GravitySlotDropEvent<unknown>) {
142
+ slotItem.value = event.item as DemoItem;
143
+ }
144
+
145
+ return {
146
+ targetModeItem,
147
+ floatingModeItem,
148
+ slotItem,
149
+ lastTargetModeResult,
150
+ lastFloatingModeResult,
151
+ onTargetModeDrop,
152
+ onFloatingModeDrop,
153
+ onSlotDrop,
154
+ };
155
+ },
156
+ components: { GravityProvider, Draggable, Slot },
157
+ template: `
158
+ <div style="width: 520px; max-width: 96vw; font-family: system-ui, sans-serif;">
159
+ <GravityProvider>
160
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; align-items: start;">
161
+ <div style="display: grid; gap: 8px;">
162
+ <Draggable
163
+ draggable-id="draggable-target-mode"
164
+ :item="targetModeItem"
165
+ source-id="target-mode-source"
166
+ source-kind="custom"
167
+ :source-index="0"
168
+ drop-mode="target"
169
+ @drag-end="onTargetModeDrop"
170
+ >
171
+ <template #default="{ dragging }">
172
+ <div
173
+ :style="{
174
+ padding: '10px 12px',
175
+ borderRadius: '8px',
176
+ border: '1px solid #d1d5db',
177
+ background: '#f9fafb',
178
+ opacity: dragging ? .9 : 1
179
+ }"
180
+ >
181
+ {{ targetModeItem.label }}
182
+ </div>
183
+ </template>
184
+ </Draggable>
185
+
186
+ <Draggable
187
+ draggable-id="draggable-floating-mode"
188
+ :item="floatingModeItem"
189
+ source-id="floating-mode-source"
190
+ source-kind="custom"
191
+ :source-index="0"
192
+ drop-mode="floating"
193
+ @drag-end="onFloatingModeDrop"
194
+ >
195
+ <template #default="{ dragging }">
196
+ <div
197
+ :style="{
198
+ padding: '10px 12px',
199
+ borderRadius: '8px',
200
+ border: '1px solid #d1d5db',
201
+ background: '#f9fafb',
202
+ opacity: dragging ? .9 : 1
203
+ }"
204
+ >
205
+ {{ floatingModeItem.label }}
206
+ </div>
207
+ </template>
208
+ </Draggable>
209
+ </div>
210
+
211
+ <Slot slot-id="mode-slot" @drop="onSlotDrop">
212
+ <template #default="{ hovering, accepting }">
213
+ <div
214
+ style="min-height: 132px; border-radius: 8px; border: 1px solid #d1d5db; display: flex; align-items: center; justify-content: center; text-align: center; font-size: 12px;"
215
+ :style="{ background: hovering ? (accepting ? '#ecfeff' : '#fff1f2') : '#ffffff' }"
216
+ >
217
+ {{ slotItem ? 'Dropped: ' + slotItem.label : 'Drop either card here' }}
218
+ </div>
219
+ </template>
220
+ </Slot>
221
+ </div>
222
+
223
+ <div style="margin-top: 10px; font-size: 12px; color: #4b5563;">
224
+ Target mode result: {{ lastTargetModeResult }}
225
+ <br>
226
+ Floating mode result: {{ lastFloatingModeResult }}
227
+ </div>
228
+ </GravityProvider>
229
+ </div>
230
+ `,
231
+ }),
232
+ };
@@ -0,0 +1,8 @@
1
+ .st-gravity-draggable--hover-accept .st-gravity-draggable__content {
2
+ box-shadow: 0 0 0 2px rgb(34 197 94 / 35%);
3
+ }
4
+
5
+ .st-gravity-draggable--hover-reject .st-gravity-draggable__content {
6
+ box-shadow: 0 0 0 2px rgb(239 68 68 / 35%);
7
+ filter: saturate(0.85);
8
+ }