@chelone/vue 0.9.0 → 0.10.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.
@@ -1,19 +1,26 @@
1
1
  import { useTranslate } from "../../../i18n/use-translate.js";
2
2
  import { cn } from "../../../lib/utils.js";
3
3
  import button_default from "../../ui/button.js";
4
+ import { buttonVariants } from "../../ui/button-variants.js";
4
5
  import data_provider_devtools_entry_default from "./data-provider-devtools-entry.js";
5
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, renderList, shallowRef, toDisplayString, unref, watch, withCtx } from "vue";
6
+ import { ActivityIcon } from "./icons.js";
7
+ import { Fragment, computed, createBlock, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, renderList, shallowRef, toDisplayString, unref, watch, withCtx } from "vue";
6
8
  import { shouldHandleHotkey } from "@chelone/core";
7
9
  // src/components/admin/layout/data-provider-devtools.vue?vue&type=script&setup=true&lang.ts
8
- var _hoisted_1 = ["aria-label", "data-open"];
9
- var _hoisted_2 = ["data-open"];
10
- var _hoisted_3 = { class: "font-semibold" };
11
- var _hoisted_4 = { class: "text-muted-foreground tabular-nums" };
12
- var _hoisted_5 = {
10
+ var _hoisted_1 = ["aria-label"];
11
+ var _hoisted_2 = {
12
+ "data-slot": "data-provider-devtools-badge",
13
+ class: "absolute top-0 right-0 min-w-4 -translate-y-1/2 translate-x-1/2 rounded-full bg-primary px-1 text-center text-[10px] leading-4 text-primary-foreground tabular-nums"
14
+ };
15
+ var _hoisted_3 = ["aria-label"];
16
+ var _hoisted_4 = { class: "flex items-center gap-2 border-b px-3 py-1.5" };
17
+ var _hoisted_5 = { class: "font-semibold" };
18
+ var _hoisted_6 = { class: "text-muted-foreground tabular-nums" };
19
+ var _hoisted_7 = {
13
20
  key: 0,
14
21
  class: "px-3 py-2 text-muted-foreground"
15
22
  };
16
- var _hoisted_6 = {
23
+ var _hoisted_8 = {
17
24
  key: 1,
18
25
  class: "min-h-0 flex-1 overflow-y-auto"
19
26
  };
@@ -30,11 +37,13 @@ var data_provider_devtools_vue_vue_type_script_setup_true_lang_default = /*@__PU
30
37
  },
31
38
  setup(__props) {
32
39
  /**
33
- * `<DataProviderDevtools>` — a floating panel at the bottom right that lists
34
- * the calls a `createDataProviderLog` log has recorded: method, resource,
35
- * duration and, for a failed call, the error. The Vue twin of
40
+ * `<DataProviderDevtools>` — a round button at the bottom right carrying a
41
+ * badge with the number of calls a `createDataProviderLog` log has recorded.
42
+ * Activating it opens a panel listing those calls: method, resource, duration
43
+ * and, for a failed call, the error. The panel's Hide button, or the
44
+ * `shortcut`, closes it back to the button. The Vue twin of
36
45
  * `@chelone/react`'s `layout/data-provider-devtools.tsx`. Wrap the data
37
- * provider with `log.wrap`, then mount the panel once, in the layout.
46
+ * provider with `log.wrap`, then mount the component once, in the layout.
38
47
  */
39
48
  const props = __props;
40
49
  const translate = useTranslate();
@@ -48,6 +57,7 @@ var data_provider_devtools_vue_vue_type_script_setup_true_lang_default = /*@__PU
48
57
  }));
49
58
  }, { immediate: true });
50
59
  const entries = computed(() => [...snapshot.value.entries].reverse());
60
+ const title = () => translate("chelone.devtools.title", { _: "Data provider" });
51
61
  function onKeydown(event) {
52
62
  const target = event.target instanceof HTMLElement ? event.target : null;
53
63
  if (!shouldHandleHotkey(event, props.shortcut, target)) return;
@@ -56,24 +66,48 @@ var data_provider_devtools_vue_vue_type_script_setup_true_lang_default = /*@__PU
56
66
  }
57
67
  onMounted(() => window.addEventListener("keydown", onKeydown));
58
68
  onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
69
+ const triggerRef = ref(null);
70
+ const panelRef = ref(null);
71
+ watch(open, (opened) => {
72
+ nextTick(() => {
73
+ const focused = document.activeElement;
74
+ if (focused !== null && focused !== document.body && focused.isConnected) return;
75
+ (opened ? panelRef : triggerRef).value?.focus();
76
+ });
77
+ });
59
78
  return (_ctx, _cache) => {
60
- return openBlock(), createElementBlock("section", {
61
- "aria-label": unref(translate)("chelone.devtools.title", { _: "Data provider" }),
79
+ return !open.value ? (openBlock(), createElementBlock("button", {
80
+ key: 0,
81
+ ref_key: "triggerRef",
82
+ ref: triggerRef,
83
+ type: "button",
84
+ "aria-expanded": false,
85
+ "aria-label": `${title()} (${snapshot.value.entries.length})`,
86
+ "data-slot": "data-provider-devtools",
87
+ "data-open": false,
88
+ class: normalizeClass(unref(cn)(unref(buttonVariants)({
89
+ variant: "outline",
90
+ size: "icon"
91
+ }), "fixed right-4 bottom-4 z-50 rounded-full shadow-lg", props.class)),
92
+ onClick: _cache[0] || (_cache[0] = ($event) => open.value = true)
93
+ }, [createVNode(unref(ActivityIcon)), createElementVNode("span", _hoisted_2, toDisplayString(snapshot.value.entries.length), 1)], 10, _hoisted_1)) : (openBlock(), createElementBlock("section", {
94
+ key: 1,
95
+ ref_key: "panelRef",
96
+ ref: panelRef,
97
+ tabindex: "-1",
98
+ "aria-label": title(),
62
99
  "data-slot": "data-provider-devtools",
63
- "data-open": open.value,
100
+ "data-open": true,
64
101
  class: normalizeClass(unref(cn)("fixed right-4 bottom-4 z-50 flex max-h-[70vh] w-96 max-w-[calc(100vw-2rem)] flex-col rounded-lg border bg-popover text-xs text-popover-foreground shadow-lg", props.class))
65
- }, [createElementVNode("header", {
66
- class: "flex items-center gap-2 border-b px-3 py-1.5 data-[open=false]:border-b-0",
67
- "data-open": open.value
68
- }, [
69
- createElementVNode("span", _hoisted_3, toDisplayString(unref(translate)("chelone.devtools.title", { _: "Data provider" })), 1),
70
- createElementVNode("span", _hoisted_4, toDisplayString(snapshot.value.entries.length), 1),
102
+ }, [createElementVNode("header", _hoisted_4, [
103
+ createElementVNode("span", _hoisted_5, toDisplayString(title()), 1),
104
+ createElementVNode("span", _hoisted_6, toDisplayString(snapshot.value.entries.length), 1),
71
105
  createVNode(button_default, {
72
106
  type: "button",
73
107
  variant: "ghost",
74
108
  size: "sm",
75
109
  class: "ml-auto h-6 px-2",
76
- onClick: _cache[0] || (_cache[0] = ($event) => props.log.clear())
110
+ onClick: _cache[1] || (_cache[1] = ($event) => props.log.clear())
77
111
  }, {
78
112
  default: withCtx(() => [createTextVNode(toDisplayString(unref(translate)("chelone.devtools.clear", { _: "Clear" })), 1)]),
79
113
  _: 1
@@ -83,18 +117,18 @@ var data_provider_devtools_vue_vue_type_script_setup_true_lang_default = /*@__PU
83
117
  variant: "ghost",
84
118
  size: "sm",
85
119
  class: "h-6 px-2",
86
- "aria-expanded": open.value,
87
- onClick: _cache[1] || (_cache[1] = ($event) => open.value = !open.value)
120
+ "aria-expanded": true,
121
+ onClick: _cache[2] || (_cache[2] = ($event) => open.value = false)
88
122
  }, {
89
- default: withCtx(() => [createTextVNode(toDisplayString(open.value ? unref(translate)("chelone.devtools.hide", { _: "Hide" }) : unref(translate)("chelone.devtools.show", { _: "Show" })), 1)]),
123
+ default: withCtx(() => [createTextVNode(toDisplayString(unref(translate)("chelone.devtools.hide", { _: "Hide" })), 1)]),
90
124
  _: 1
91
- }, 8, ["aria-expanded"])
92
- ], 8, _hoisted_2), open.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [entries.value.length === 0 ? (openBlock(), createElementBlock("p", _hoisted_5, toDisplayString(unref(translate)("chelone.devtools.empty", { _: "No calls yet" })), 1)) : (openBlock(), createElementBlock("ul", _hoisted_6, [(openBlock(true), createElementBlock(Fragment, null, renderList(entries.value, (entry) => {
125
+ })
126
+ ]), entries.value.length === 0 ? (openBlock(), createElementBlock("p", _hoisted_7, toDisplayString(unref(translate)("chelone.devtools.empty", { _: "No calls yet" })), 1)) : (openBlock(), createElementBlock("ul", _hoisted_8, [(openBlock(true), createElementBlock(Fragment, null, renderList(entries.value, (entry) => {
93
127
  return openBlock(), createBlock(data_provider_devtools_entry_default, {
94
128
  key: entry.id,
95
129
  entry
96
130
  }, null, 8, ["entry"]);
97
- }), 128))]))], 64)) : createCommentVNode("", true)], 10, _hoisted_1);
131
+ }), 128))]))], 10, _hoisted_3));
98
132
  };
99
133
  }
100
134
  });
@@ -20,3 +20,5 @@ export declare const SendIcon: FunctionalComponent;
20
20
  export declare const SquareIcon: FunctionalComponent;
21
21
  /** The assistant close glyph (a cross). */
22
22
  export declare const XIcon: FunctionalComponent;
23
+ /** The data provider devtools trigger glyph (a pulse line). */
24
+ export declare const ActivityIcon: FunctionalComponent;
@@ -38,4 +38,6 @@ var SendIcon = glyph(["M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0
38
38
  var SquareIcon = glyph(["M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"]);
39
39
  /** The assistant close glyph (a cross). */
40
40
  var XIcon = glyph(["M18 6 6 18", "m6 6 12 12"]);
41
- export { HouseIcon, ListIcon, LogOutIcon, MessageCircleIcon, SendIcon, SquareIcon, XIcon };
41
+ /** The data provider devtools trigger glyph (a pulse line). */
42
+ var ActivityIcon = glyph(["M22 12h-4l-3 9L9 3l-3 9H2"]);
43
+ export { ActivityIcon, HouseIcon, ListIcon, LogOutIcon, MessageCircleIcon, SendIcon, SquareIcon, XIcon };
@@ -22,7 +22,9 @@ export interface MenuLiveProps {
22
22
  /**
23
23
  * A menu link with a badge counting the resource's new records. The count
24
24
  * lives in the store, so it survives a remount; it clears while the link's
25
- * route is active. Divergence from the React twin: the render props are slots.
25
+ * route is active. Inside a `<SidebarProvider>` it is a sidebar menu button,
26
+ * with the label as a `title` while the desktop rail is collapsed; elsewhere it is
27
+ * a plain link. Divergence from the React twin: the render props are slots.
26
28
  * Declared as a render component so the file can also hold {@link MenuLive}.
27
29
  */
28
30
  export declare const MenuLiveItemLink: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
@@ -2,11 +2,17 @@ import { useCheloneContext, useCheloneStore } from "../../../context.js";
2
2
  import { useTranslate } from "../../../i18n/use-translate.js";
3
3
  import { useMatch } from "../../../router/nav-composables.js";
4
4
  import { Badge } from "../../ui/badge.js";
5
- import { Fragment, computed, createBlock, createElementBlock, defineComponent, h, mergeProps, openBlock, renderList, shallowRef, unref, watchEffect } from "vue";
5
+ import { SidebarContextKey } from "../../ui/sidebar/sidebar-context.js";
6
+ import sidebar_menu_button_default from "../../ui/sidebar/sidebar-menu-button.js";
7
+ import { SidebarMenuItem } from "../../ui/sidebar/sidebar-parts.js";
8
+ import { Fragment, computed, createBlock, createElementBlock, defineComponent, h, inject, mergeProps, openBlock, renderList, shallowRef, unref, watchEffect } from "vue";
6
9
  import { assertRealtimeDataProvider, clearUnread, subscribeUnreadCount, unreadKey } from "@chelone/core";
7
10
  import { RouterLink } from "vue-router";
8
11
  // src/components/admin/realtime/menu-live.vue?vue&type=script&setup=true&lang.ts
9
- var _hoisted_1 = { class: "flex flex-col gap-1" };
12
+ var _hoisted_1 = {
13
+ key: 1,
14
+ class: "flex flex-col gap-1"
15
+ };
10
16
  var subscriptions = /* @__PURE__ */ new WeakMap();
11
17
  var toEventsKey = (events) => [...new Set(events)].sort().join(",");
12
18
  var fromEventsKey = (eventsKey) => eventsKey.split(",").filter(Boolean);
@@ -51,7 +57,9 @@ function acquire(dataProvider, store, resource, eventsKey) {
51
57
  /**
52
58
  * A menu link with a badge counting the resource's new records. The count
53
59
  * lives in the store, so it survives a remount; it clears while the link's
54
- * route is active. Divergence from the React twin: the render props are slots.
60
+ * route is active. Inside a `<SidebarProvider>` it is a sidebar menu button,
61
+ * with the label as a `title` while the desktop rail is collapsed; elsewhere it is
62
+ * a plain link. Divergence from the React twin: the render props are slots.
55
63
  * Declared as a render component so the file can also hold {@link MenuLive}.
56
64
  */
57
65
  var MenuLiveItemLink = defineComponent({
@@ -86,6 +94,7 @@ var MenuLiveItemLink = defineComponent({
86
94
  const { dataProvider } = useCheloneContext();
87
95
  const store = useCheloneStore();
88
96
  const translate = useTranslate();
97
+ const sidebar = inject(SidebarContextKey, null);
89
98
  const match = useMatch({
90
99
  path: props.to,
91
100
  end: false
@@ -111,15 +120,30 @@ var MenuLiveItemLink = defineComponent({
111
120
  return () => {
112
121
  const n = count.value ?? 0;
113
122
  const text = translate(props.label, { _: props.label });
114
- return h(RouterLink, {
115
- to: props.to,
116
- class: "flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent"
117
- }, () => [
123
+ const badge = n > 0 ? h(Badge, {
124
+ "data-testid": "menu-live-badge",
125
+ class: sidebar ? "group-data-[collapsible=icon]:absolute group-data-[collapsible=icon]:top-0 group-data-[collapsible=icon]:right-0 group-data-[collapsible=icon]:h-4 group-data-[collapsible=icon]:min-w-4 group-data-[collapsible=icon]:px-1 group-data-[collapsible=icon]:text-[10px]" : void 0
126
+ }, () => props.badgeLabel?.(n) ?? (n > 99 ? "99+" : String(n))) : null;
127
+ const content = () => [
118
128
  props.icon ? h(props.icon) : null,
119
129
  slots.icon?.(),
120
130
  h("span", { class: "flex-1" }, text),
121
- n > 0 ? h(Badge, { "data-testid": "menu-live-badge" }, () => props.badgeLabel?.(n) ?? (n > 99 ? "99+" : String(n))) : null
122
- ]);
131
+ badge
132
+ ];
133
+ if (sidebar) return h(SidebarMenuItem, null, () => h(sidebar_menu_button_default, {
134
+ as: RouterLink,
135
+ to: props.to,
136
+ isActive: match.value != null,
137
+ class: "relative",
138
+ title: sidebar.state.value === "collapsed" && !sidebar.isMobile.value ? text : void 0,
139
+ onClick: () => {
140
+ if (sidebar.openMobile.value) sidebar.setOpenMobile(false);
141
+ }
142
+ }, content));
143
+ return h(RouterLink, {
144
+ to: props.to,
145
+ class: "flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent"
146
+ }, content);
123
147
  };
124
148
  }
125
149
  });
@@ -128,12 +152,18 @@ var menu_live_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
128
152
  props: { items: {} },
129
153
  setup(__props) {
130
154
  /**
131
- * `<MenuLive>` — a list of {@link MenuLiveItemLink}s, one per item.
155
+ * `<MenuLive>` — a list of {@link MenuLiveItemLink}s, one per item. Inside a
156
+ * sidebar it renders the items as sidebar menu entries with no wrapper, so
157
+ * place it in a `<SidebarMenu>`, such as a `<ResourceMenuItemGroup>`'s default
158
+ * slot.
132
159
  */
160
+ const sidebar = inject(SidebarContextKey, null);
133
161
  return (_ctx, _cache) => {
134
- return openBlock(), createElementBlock("nav", _hoisted_1, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item) => {
162
+ return unref(sidebar) ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(__props.items, (item) => {
163
+ return openBlock(), createBlock(unref(MenuLiveItemLink), mergeProps({ key: item.to }, { ref_for: true }, item), null, 16);
164
+ }), 128)) : (openBlock(), createElementBlock("nav", _hoisted_1, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item) => {
135
165
  return openBlock(), createBlock(unref(MenuLiveItemLink), mergeProps({ key: item.to }, { ref_for: true }, item), null, 16);
136
- }), 128))]);
166
+ }), 128))]));
137
167
  };
138
168
  }
139
169
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chelone/vue",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -76,14 +76,14 @@
76
76
  "access": "public"
77
77
  },
78
78
  "dependencies": {
79
- "@chelone/core": "0.9.0",
80
- "@chelone/utils": "0.9.0",
79
+ "@chelone/core": "0.10.0",
80
+ "@chelone/utils": "0.10.0",
81
81
  "@types/geojson": "^7946.0.16",
82
82
  "cronstrue": "^3.38.0",
83
83
  "libphonenumber-js": "^1.13.15"
84
84
  },
85
85
  "peerDependencies": {
86
- "@chelone/supabase": "0.9.0",
86
+ "@chelone/supabase": "0.10.0",
87
87
  "@dnd-kit/dom": "^0.5.3",
88
88
  "@dnd-kit/vue": "^0.5.3",
89
89
  "@guolao/vue-monaco-editor": "^1.6.0",
@@ -219,7 +219,7 @@
219
219
  },
220
220
  "devDependencies": {
221
221
  "@arethetypeswrong/cli": "^0.18.5",
222
- "@chelone/supabase": "0.9.0",
222
+ "@chelone/supabase": "0.10.0",
223
223
  "@chelone/test-utils": "0.0.0",
224
224
  "@dnd-kit/dom": "^0.5.3",
225
225
  "@dnd-kit/vue": "^0.5.3",