@xy-planning-network/trees 0.4.0-rc-7 → 0.4.2

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 (61) hide show
  1. package/README.md +236 -53
  2. package/dist/trees.es.js +1069 -330
  3. package/dist/trees.umd.js +6 -6
  4. package/package.json +6 -4
  5. package/src/lib-components/forms/BaseInput.vue +83 -0
  6. package/src/lib-components/forms/Checkbox.vue +46 -0
  7. package/src/lib-components/forms/DateRangePicker.vue +65 -0
  8. package/src/lib-components/forms/InputHelp.vue +24 -0
  9. package/src/lib-components/forms/InputLabel.vue +23 -0
  10. package/src/lib-components/forms/MultiCheckboxes.vue +55 -0
  11. package/src/lib-components/forms/Radio.vue +58 -0
  12. package/src/lib-components/forms/Select.vue +65 -0
  13. package/src/lib-components/forms/TextArea.vue +50 -0
  14. package/src/lib-components/forms/Toggle.vue +25 -0
  15. package/src/lib-components/forms/YesOrNoRadio.vue +70 -0
  16. package/src/lib-components/layout/DateFilter.vue +54 -0
  17. package/src/lib-components/layout/SidebarLayout.vue +239 -0
  18. package/src/lib-components/layout/StackedLayout.vue +172 -0
  19. package/src/lib-components/lists/Cards.vue +33 -0
  20. package/src/lib-components/lists/DetailList.vue +114 -0
  21. package/src/lib-components/lists/DownloadCell.vue +12 -0
  22. package/src/lib-components/lists/StaticTable.vue +83 -0
  23. package/src/lib-components/lists/Table.vue +291 -0
  24. package/src/lib-components/navigation/ActionsDropdown.vue +78 -0
  25. package/src/lib-components/navigation/Paginator.vue +111 -0
  26. package/src/lib-components/navigation/Steps.vue +83 -0
  27. package/src/lib-components/navigation/Tabs.vue +92 -0
  28. package/src/lib-components/overlays/ContentModal.vue +95 -0
  29. package/src/lib-components/overlays/Flash.vue +131 -0
  30. package/src/lib-components/overlays/Modal.vue +133 -0
  31. package/src/lib-components/overlays/Popover/Popover.vue +229 -0
  32. package/src/lib-components/overlays/Popover/PopoverContent.vue +8 -0
  33. package/src/lib-components/overlays/Slideover.vue +87 -0
  34. package/src/lib-components/overlays/Spinner.vue +149 -0
  35. package/src/lib-components/overlays/Tooltip.vue +34 -0
  36. package/types/components.d.ts +6 -2
  37. package/types/composables/date.d.ts +4 -0
  38. package/types/composables/nav.d.ts +13 -0
  39. package/types/composables/overlay.d.ts +4 -0
  40. package/types/composables/table.d.ts +32 -0
  41. package/types/composables/user.d.ts +6 -0
  42. package/types/global.d.ts +5 -2
  43. package/types/helpers/Debounce.d.ts +1 -0
  44. package/types/helpers/Throttle.d.ts +1 -0
  45. package/types/lib-components/forms/Select.vue.d.ts +2 -2
  46. package/types/lib-components/index.d.ts +9 -9
  47. package/types/lib-components/layout/DateFilter.vue.d.ts +1 -4
  48. package/types/lib-components/layout/SidebarLayout.vue.d.ts +1 -1
  49. package/types/lib-components/layout/StackedLayout.vue.d.ts +2 -2
  50. package/types/lib-components/lists/StaticTable.vue.d.ts +1 -1
  51. package/types/lib-components/lists/Table.vue.d.ts +1 -1
  52. package/types/lib-components/navigation/ActionsDropdown.vue.d.ts +2 -2
  53. package/types/lib-components/navigation/Paginator.vue.d.ts +1 -6
  54. package/types/lib-components/overlays/Flash.vue.d.ts +0 -4
  55. package/types/lib-components/overlays/Popover/Popover.vue.d.ts +23 -0
  56. package/types/lib-components/overlays/Popover/PopoverContent.vue.d.ts +2 -0
  57. package/types/lib-components/overlays/Tooltip.vue.d.ts +23 -0
  58. package/types/index.d.ts +0 -3
  59. package/types/nav.d.ts +0 -8
  60. package/types/table.d.ts +0 -36
  61. package/types/users.d.ts +0 -10
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ Dialog,
4
+ DialogOverlay,
5
+ DialogTitle,
6
+ TransitionChild,
7
+ TransitionRoot,
8
+ } from "@headlessui/vue"
9
+
10
+ withDefaults(
11
+ defineProps<{
12
+ modelValue: boolean
13
+ title?: string
14
+ }>(),
15
+ {
16
+ title: "",
17
+ }
18
+ )
19
+
20
+ const emit = defineEmits<{
21
+ (e: "update:modelValue", val: boolean): void
22
+ }>()
23
+
24
+ const updateModelValue = (value: boolean) => {
25
+ emit("update:modelValue", value)
26
+ }
27
+ </script>
28
+ <template>
29
+ <TransitionRoot as="template" :show="modelValue">
30
+ <Dialog
31
+ as="div"
32
+ static
33
+ class="fixed z-10 inset-0 overflow-y-auto"
34
+ @close="updateModelValue(false)"
35
+ :open="modelValue"
36
+ >
37
+ <div
38
+ class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
39
+ >
40
+ <TransitionChild
41
+ as="template"
42
+ enter="ease-out duration-300"
43
+ enter-from="opacity-0"
44
+ enter-to="opacity-100"
45
+ leave="ease-in duration-200"
46
+ leave-from="opacity-100"
47
+ leave-to="opacity-0"
48
+ >
49
+ <DialogOverlay
50
+ class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
51
+ />
52
+ </TransitionChild>
53
+
54
+ <!-- This element is to trick the browser into centering the modal contents. -->
55
+ <span
56
+ class="hidden sm:inline-block sm:align-middle sm:h-screen"
57
+ aria-hidden="true"
58
+ >&#8203;</span
59
+ >
60
+ <TransitionChild
61
+ as="template"
62
+ enter="ease-out duration-300"
63
+ enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
64
+ enter-to="opacity-100 translate-y-0 sm:scale-100"
65
+ leave="ease-in duration-200"
66
+ leave-from="opacity-100 translate-y-0 sm:scale-100"
67
+ leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
68
+ >
69
+ <div
70
+ class="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6"
71
+ >
72
+ <div>
73
+ <slot name="icon"></slot>
74
+ <div class="mt-3 text-center sm:mt-5">
75
+ <DialogTitle as="h3" v-text="title"></DialogTitle>
76
+ <div class="mt-2">
77
+ <slot></slot>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ <div class="mt-5 sm:mt-6">
82
+ <button
83
+ type="button"
84
+ class="inline-flex justify-center w-full xy-btn"
85
+ @click="updateModelValue(false)"
86
+ >
87
+ Go back
88
+ </button>
89
+ </div>
90
+ </div>
91
+ </TransitionChild>
92
+ </div>
93
+ </Dialog>
94
+ </TransitionRoot>
95
+ </template>
@@ -0,0 +1,131 @@
1
+ <script setup lang="ts">
2
+ import { Flash } from "@/composables/overlay"
3
+ import { onMounted, ref } from "vue"
4
+
5
+ // TODO: spk this might benefit from the composition api to avoid race conditions where a flash is requested before the component is mounted.
6
+
7
+ const flashes = ref<Flash[]>([])
8
+ const flashTypeBorderClass = {
9
+ warning: "border-orange-500",
10
+ error: "border-red-500",
11
+ info: "border-blue-500",
12
+ success: "border-green-500",
13
+ }
14
+
15
+ const getFlashClass = (flash: Flash): string => {
16
+ const availableTypes = Object.keys(flashTypeBorderClass)
17
+ // default the flash type to "info" if no flash type or an unsupported flash.type is found
18
+ const type =
19
+ flash?.type && availableTypes.includes(flash.type)
20
+ ? (flash.type as "warning" | "error" | "info" | "success")
21
+ : "info"
22
+ return flashTypeBorderClass[type]
23
+ }
24
+
25
+ const remove = (flash: Flash): void => {
26
+ let index = 0
27
+ for (const f of flashes.value) {
28
+ if (flash.message === f.message) {
29
+ flashes.value.splice(index, 1)
30
+ return
31
+ }
32
+ index++
33
+ }
34
+ }
35
+ const renderFlash = (flash: Flash): void => {
36
+ flashes.value.push(flash)
37
+ // Super simple flash implementation. This could get "smarter" by adding an
38
+ // id to the flash object, and then searching for the specific flash in the
39
+ // array and splicing, instead of simply doing a pop().
40
+ setTimeout(
41
+ (flashes: Flash[]) => {
42
+ flashes.pop()
43
+ },
44
+ 10000,
45
+ flashes.value
46
+ )
47
+ }
48
+
49
+ const renderGenericError = (email: string): void => {
50
+ renderFlash({
51
+ type: "error",
52
+ message:
53
+ "Whoops! Something went wrong, please reach out to " +
54
+ `<a class="underline text-xy-blue" href="mailto:${email}">${email}</a>` +
55
+ " if the issue persists.",
56
+ })
57
+ }
58
+
59
+ onMounted(() => {
60
+ window.VueBus.on("Flash-show-message", (flash) => {
61
+ renderFlash(flash)
62
+ })
63
+
64
+ window.VueBus.on("Flash-show-generic-error", (email) => {
65
+ renderGenericError(email)
66
+ })
67
+
68
+ if (window.Flashes) {
69
+ for (const flash of window.Flashes) {
70
+ if (typeof flash.type === "undefined") {
71
+ const values: string[] = flash.message.split(": ")
72
+ renderFlash({ type: values[0], message: values[1] })
73
+ return
74
+ }
75
+ renderFlash({ type: flash.type, message: flash.message })
76
+ }
77
+ }
78
+ })
79
+ </script>
80
+ <template>
81
+ <div
82
+ class="fixed inset-0 flex flex-col items-end justify-end px-4 py-6 pointer-events-none sm:p-6 z-40"
83
+ >
84
+ <transition-group
85
+ tag="div"
86
+ class="max-w-sm w-full"
87
+ enter-active-class="ease-out duration-300"
88
+ enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
89
+ enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
90
+ leave-active-class="ease-in duration-100"
91
+ leave-from-class="opacity-100"
92
+ leave-to-class="opacity-0"
93
+ >
94
+ <div
95
+ v-for="(flash, idx) in flashes"
96
+ :key="flash.message"
97
+ class="bg-white shadow-lg rounded-lg pointer-events-auto border-t-4 transform"
98
+ :class="[{ 'mt-2': idx > 0 }, getFlashClass(flash)]"
99
+ >
100
+ <div
101
+ class="rounded-lg ring-1 ring-black ring-opacity-5 overflow-hidden"
102
+ >
103
+ <div class="p-4">
104
+ <div class="flex items-center">
105
+ <div class="w-0 flex-1 flex justify-between">
106
+ <p
107
+ class="w-0 flex-1 text-sm leading-5 font-medium text-gray-900"
108
+ v-html="flash.message"
109
+ ></p>
110
+ </div>
111
+ <div class="ml-4 flex-shrink-0 flex">
112
+ <button
113
+ @click="remove(flash)"
114
+ class="inline-flex text-gray-400 focus:outline-none focus:text-gray-500 transition ease-in-out duration-150"
115
+ >
116
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
117
+ <path
118
+ fill-rule="evenodd"
119
+ d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
120
+ clip-rule="evenodd"
121
+ />
122
+ </svg>
123
+ </button>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </transition-group>
130
+ </div>
131
+ </template>
@@ -0,0 +1,133 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ Dialog,
4
+ DialogOverlay,
5
+ DialogTitle,
6
+ TransitionChild,
7
+ TransitionRoot,
8
+ } from "@headlessui/vue"
9
+ import { XIcon } from "@heroicons/vue/outline"
10
+
11
+ withDefaults(
12
+ defineProps<{
13
+ destructive?: boolean
14
+ disabled?: boolean
15
+ modelValue: boolean
16
+ submitText?: string
17
+ title?: string
18
+ }>(),
19
+ {
20
+ destructive: false,
21
+ disabled: false,
22
+ submitText: "",
23
+ title: "",
24
+ }
25
+ )
26
+
27
+ const emit = defineEmits<{
28
+ (e: "submit"): void
29
+ (e: "update:modelValue", val: boolean): void
30
+ }>()
31
+
32
+ const submit = () => {
33
+ emit("submit")
34
+ }
35
+
36
+ const updateModelValue = (value: boolean) => {
37
+ emit("update:modelValue", value)
38
+ }
39
+ </script>
40
+ <template>
41
+ <TransitionRoot as="template" :show="modelValue">
42
+ <Dialog
43
+ as="div"
44
+ static
45
+ class="fixed z-10 inset-0 overflow-y-auto"
46
+ @close="updateModelValue(false)"
47
+ :open="modelValue"
48
+ >
49
+ <div
50
+ class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
51
+ >
52
+ <TransitionChild
53
+ as="template"
54
+ enter="ease-out duration-300"
55
+ enter-from="opacity-0"
56
+ enter-to="opacity-100"
57
+ leave="ease-in duration-200"
58
+ leave-from="opacity-100"
59
+ leave-to="opacity-0"
60
+ >
61
+ <DialogOverlay
62
+ class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
63
+ />
64
+ </TransitionChild>
65
+
66
+ <!-- This element is to trick the browser into centering the modal contents. -->
67
+ <span
68
+ class="hidden sm:inline-block sm:align-middle sm:h-screen"
69
+ aria-hidden="true"
70
+ >&#8203;</span
71
+ >
72
+ <TransitionChild
73
+ as="template"
74
+ enter="ease-out duration-300"
75
+ enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
76
+ enter-to="opacity-100 translate-y-0 sm:scale-100"
77
+ leave="ease-in duration-200"
78
+ leave-from="opacity-100 translate-y-0 sm:scale-100"
79
+ leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
80
+ >
81
+ <div
82
+ class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl w-full"
83
+ >
84
+ <div class="block absolute top-0 right-0 pt-4 pr-4">
85
+ <button
86
+ type="button"
87
+ class="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
88
+ @click="updateModelValue(false)"
89
+ >
90
+ <span class="sr-only">Close</span>
91
+ <XIcon class="h-6 w-6" aria-hidden="true" />
92
+ </button>
93
+ </div>
94
+ <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
95
+ <div class="mt-3 sm:mt-0 sm:text-left">
96
+ <DialogTitle
97
+ as="h3"
98
+ class="text-center text-lg leading-6 font-medium text-gray-900"
99
+ v-text="title"
100
+ ></DialogTitle>
101
+ <div class="mt-2">
102
+ <slot></slot>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ <div
107
+ class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"
108
+ v-if="submitText"
109
+ >
110
+ <button
111
+ type="button"
112
+ class="xy-btn w-full sm:ml-3 sm:w-auto sm:text-sm"
113
+ @click="submit()"
114
+ v-text="submitText"
115
+ :class="[destructive ? 'xy-btn-red' : 'xy-btn']"
116
+ :disabled="disabled"
117
+ ></button>
118
+ <button
119
+ type="button"
120
+ class="xy-btn-white mt-3 w-full sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
121
+ @click="updateModelValue(false)"
122
+ ref="cancelButtonRef"
123
+ >
124
+ Cancel
125
+ </button>
126
+ </div>
127
+ <slot name="buttons"></slot>
128
+ </div>
129
+ </TransitionChild>
130
+ </div>
131
+ </Dialog>
132
+ </TransitionRoot>
133
+ </template>
@@ -0,0 +1,229 @@
1
+ <script lang="ts">
2
+ export type PopoverPosition =
3
+ | "top-left"
4
+ | "top-center"
5
+ | "top-right"
6
+ | "bottom-left"
7
+ | "bottom-center"
8
+ | "bottom-right"
9
+ | "left"
10
+ | "right"
11
+ | "auto"
12
+ | "none"
13
+ </script>
14
+ <script lang="ts" setup>
15
+ import { throttle } from "@/helpers/Throttle"
16
+ import {
17
+ Popover as HeadlessPopover,
18
+ PopoverButton as HeadlessPopoverButton,
19
+ PopoverPanel as HeadlessPopoverPanel,
20
+ } from "@headlessui/vue"
21
+ import { computed, onMounted, onUnmounted, ref } from "vue"
22
+
23
+ const props = withDefaults(
24
+ defineProps<{
25
+ as?: string
26
+ position?: PopoverPosition
27
+ }>(),
28
+ {
29
+ as: "div",
30
+ position: "auto",
31
+ }
32
+ )
33
+
34
+ const getViewportDimensions = () => {
35
+ return {
36
+ vw: document.documentElement.clientWidth,
37
+ vh: document.documentElement.clientHeight,
38
+ }
39
+ }
40
+
41
+ const trigger = ref<typeof HeadlessPopoverButton>()
42
+ const wrapper = ref<typeof HeadlessPopoverPanel>()
43
+ const viewport = ref<{ vw: number; vh: number }>(getViewportDimensions())
44
+
45
+ const classes = computed(() => {
46
+ const classes = {
47
+ wrapper: "",
48
+ content: "",
49
+ }
50
+
51
+ if (props.position === "none") {
52
+ return classes
53
+ }
54
+
55
+ // defaults classes when positioning
56
+ classes.wrapper = "h-0 flex w-screen"
57
+ classes.content = "absolute"
58
+
59
+ // merge static positioning classes
60
+ if (props.position !== "auto") {
61
+ classes.wrapper += ` ${staticPosition.value.wrapper}`
62
+ classes.content += ` ${staticPosition.value.content}`
63
+ }
64
+
65
+ return classes
66
+ })
67
+
68
+ const staticPosition = computed(() => {
69
+ let wrapperClasses = ""
70
+ let contentClasses = ""
71
+
72
+ switch (props.position) {
73
+ case "top-left":
74
+ wrapperClasses = "top-0 right-0 -translate-y-full justify-end"
75
+ contentClasses = "bottom-full"
76
+ break
77
+ case "top-center":
78
+ wrapperClasses =
79
+ "top-0 -translate-y-full -translate-x-full left-1/2 justify-end"
80
+ contentClasses = "bottom-full translate-x-1/2"
81
+ break
82
+ case "top-right":
83
+ wrapperClasses =
84
+ "top-0 -translate-y-full left-0 -translate-x-full justify-end"
85
+ contentClasses = "bottom-full translate-x-full"
86
+ break
87
+ case "bottom-left":
88
+ wrapperClasses = "top-full right-0 justify-end"
89
+ contentClasses = "top-full"
90
+ break
91
+ case "bottom-center":
92
+ wrapperClasses = "top-full -translate-x-full left-1/2 justify-end"
93
+ contentClasses = "top-full translate-x-1/2"
94
+ break
95
+ case "bottom-right":
96
+ wrapperClasses = "top-full left-0 -translate-x-full justify-end"
97
+ contentClasses = "top-full translate-x-full"
98
+ break
99
+ case "left":
100
+ wrapperClasses =
101
+ "top-1/2 left-0 -translate-y-1/2 -translate-x-full justify-end"
102
+ contentClasses = "-translate-y-1/2"
103
+ break
104
+ case "right":
105
+ wrapperClasses = "top-1/2 -translate-y-1/2 right-0 justify-end"
106
+ contentClasses = "translate-x-full -translate-y-1/2"
107
+ break
108
+ }
109
+
110
+ return {
111
+ wrapper: wrapperClasses,
112
+ content: contentClasses,
113
+ }
114
+ })
115
+
116
+ const autoPosition = computed(() => {
117
+ if (!wrapper?.value?.el || !trigger?.value?.el) {
118
+ return {}
119
+ }
120
+
121
+ const { vw, vh } = viewport.value
122
+
123
+ // avoid bumping up against the edge of the browser when possible
124
+ const offset = 10
125
+
126
+ // base the anchor rectangle off of the entire trigger dom element to move around it
127
+ const anchorRect: DOMRect = trigger.value.el.getBoundingClientRect()
128
+ // the content rectangle is best calculated by our first child (content) element inside the wrapper
129
+ const contentRect: DOMRect =
130
+ wrapper.value.el.firstChild.getBoundingClientRect()
131
+ const distToBottom = vh - anchorRect.bottom
132
+ // NOTE: edge case - there may be more space below in the viewport
133
+ // but less document space for display
134
+ // the inverse could also be true - but will be very rare
135
+ // occurring with unreasonably large popover content
136
+ const positionAbove = anchorRect.top > distToBottom
137
+ const distToRight = vw - anchorRect.left
138
+ const flowLeft = anchorRect.left > distToRight
139
+
140
+ // translate the content container on the x axis to the correct position
141
+ // considering the flow the content should take
142
+ let xPos = 0
143
+ if (flowLeft) {
144
+ if (contentRect.width > anchorRect.right) {
145
+ xPos =
146
+ anchorRect.right -
147
+ contentRect.width +
148
+ (contentRect.width - anchorRect.right)
149
+ } else {
150
+ xPos = anchorRect.right - contentRect.width
151
+ }
152
+
153
+ if (vw > contentRect.width + offset) {
154
+ xPos = xPos + offset
155
+ }
156
+ } else {
157
+ if (contentRect.width > distToRight) {
158
+ xPos = anchorRect.left - (contentRect.width - distToRight)
159
+ } else {
160
+ xPos = anchorRect.left
161
+ }
162
+
163
+ if (vw > contentRect.width + offset) {
164
+ xPos = xPos - offset
165
+ }
166
+ }
167
+
168
+ return {
169
+ wrapper: {
170
+ top: positionAbove ? "auto" : `100%`,
171
+ bottom: positionAbove ? "100%" : `auto`,
172
+ transform: `translate(${anchorRect.left * -1}px, 0)`, // pin to left of window
173
+ width: `${vw}px`,
174
+ },
175
+ content: {
176
+ top: positionAbove ? "auto" : `100%`,
177
+ bottom: positionAbove ? "100%" : `auto`,
178
+ transform: `translate(${xPos}px, 0)`,
179
+ },
180
+ }
181
+ })
182
+
183
+ if (props.position === "auto") {
184
+ const throttledSetPositions = throttle(() => {
185
+ viewport.value = getViewportDimensions()
186
+ })
187
+
188
+ onMounted(() => {
189
+ window.addEventListener("resize", throttledSetPositions)
190
+ window.addEventListener("scroll", throttledSetPositions)
191
+ })
192
+
193
+ onUnmounted(() => {
194
+ window.removeEventListener("resize", throttledSetPositions)
195
+ window.removeEventListener("scroll", throttledSetPositions)
196
+ })
197
+ }
198
+ </script>
199
+
200
+ <template>
201
+ <HeadlessPopover v-slot="{ open, close }" class="relative" :as="as">
202
+ <HeadlessPopoverButton ref="trigger">
203
+ <slot name="button" :open="open" :close="close"></slot>
204
+ </HeadlessPopoverButton>
205
+
206
+ <transition
207
+ enter-active-class="transition-opacity transition-faster ease-out-quad"
208
+ leave-active-class="transition-opacity transition-fastest ease-in-quad"
209
+ enter-from-class="opacity-0"
210
+ enter-to-class="opacity-100"
211
+ leave-from-class="opacity-100"
212
+ leave-to-class="opacity-0"
213
+ >
214
+ <HeadlessPopoverPanel
215
+ ref="wrapper"
216
+ class="absolute z-10"
217
+ :class="classes.wrapper"
218
+ :style="position === 'auto' ? autoPosition.wrapper : {}"
219
+ >
220
+ <div
221
+ :class="classes.content"
222
+ :style="position === 'auto' ? autoPosition.content : {}"
223
+ >
224
+ <slot :open="open" :close="close"></slot>
225
+ </div>
226
+ </HeadlessPopoverPanel>
227
+ </transition>
228
+ </HeadlessPopover>
229
+ </template>
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <!--styling wrapper - top level element will merge attrs for class overrides -->
3
+ <div
4
+ class="w-full max-w-xs bg-white rounded-md p-2 border border-gray-100 shadow-md"
5
+ >
6
+ <slot></slot>
7
+ </div>
8
+ </template>
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ Dialog,
4
+ DialogOverlay,
5
+ DialogTitle,
6
+ TransitionChild,
7
+ TransitionRoot,
8
+ } from "@headlessui/vue"
9
+ import { XIcon } from "@heroicons/vue/outline"
10
+ import { ref } from "vue"
11
+
12
+ const props = defineProps<{
13
+ header: string
14
+ description: string
15
+ modelValue: boolean
16
+ }>()
17
+
18
+ const open = ref(props.modelValue)
19
+
20
+ const emit = defineEmits<{
21
+ (e: "close", val: boolean): void
22
+ (e: "update:modelValue", val: boolean): void
23
+ }>()
24
+
25
+ const close = () => {
26
+ open.value = false
27
+ emit("close", open.value)
28
+ emit("update:modelValue", open.value)
29
+ }
30
+ </script>
31
+ <template>
32
+ <TransitionRoot as="template" :show="modelValue">
33
+ <Dialog
34
+ as="div"
35
+ static
36
+ class="fixed inset-0 z-40 overflow-hidden bg-black bg-opacity-50"
37
+ @close="close()"
38
+ :open="modelValue"
39
+ >
40
+ <div class="absolute inset-0 overflow-hidden">
41
+ <DialogOverlay class="absolute inset-0" />
42
+
43
+ <div class="fixed inset-y-0 right-0 pl-10 max-w-full flex">
44
+ <TransitionChild
45
+ as="template"
46
+ enter="transform transition ease-in-out duration-500 sm:duration-700"
47
+ enter-from="translate-x-full"
48
+ enter-to="translate-x-0"
49
+ leave="transform transition ease-in-out duration-500 sm:duration-700"
50
+ leave-from="translate-x-0"
51
+ leave-to="translate-x-full"
52
+ >
53
+ <div class="w-screen max-w-md">
54
+ <div
55
+ class="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
56
+ >
57
+ <div class="py-6 px-4 bg-blue-700 sm:px-6">
58
+ <div class="flex items-center justify-between">
59
+ <DialogTitle as="h3" class="text-white" v-text="header">
60
+ </DialogTitle>
61
+ <div class="ml-3 h-7 flex items-center">
62
+ <button
63
+ class="bg-blue-700 rounded-md text-blue-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-white"
64
+ @click="close()"
65
+ >
66
+ <span class="sr-only">Close panel</span>
67
+ <XIcon class="h-6 w-6" aria-hidden="true" />
68
+ </button>
69
+ </div>
70
+ </div>
71
+ <div class="mt-1">
72
+ <p class="text-blue-300" v-text="description"></p>
73
+ </div>
74
+ </div>
75
+ <div class="relative flex-1 py-6 px-4 sm:px-6">
76
+ <slot></slot>
77
+ </div>
78
+
79
+ <slot name="footer"></slot>
80
+ </div>
81
+ </div>
82
+ </TransitionChild>
83
+ </div>
84
+ </div>
85
+ </Dialog>
86
+ </TransitionRoot>
87
+ </template>