@jskit-ai/shell-web 0.1.148 → 0.1.150

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.
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  computed,
4
4
  inject,
5
+ nextTick,
5
6
  onBeforeUnmount,
6
7
  onMounted,
7
8
  ref,
@@ -9,6 +10,18 @@ import {
9
10
  } from "vue";
10
11
  import { useDisplay } from "vuetify";
11
12
  import { useShellLayoutState } from "../composables/useShellLayoutState.js";
13
+ import {
14
+ resolveShellDrawerPresentation,
15
+ resolveShellDrawerToggleLabel
16
+ } from "../support/drawerPresentation.js";
17
+ import {
18
+ DEFAULT_SHELL_DRAWER_WIDTH,
19
+ DEFAULT_SHELL_RAIL_WIDTH,
20
+ SHELL_DRAWER_LABEL_END_GAP,
21
+ normalizeShellDrawerWidth,
22
+ normalizeShellRailWidth,
23
+ resolveContentAwareDrawerWidth
24
+ } from "../support/drawerWidth.js";
12
25
  import ShellOutlet from "./ShellOutlet.vue";
13
26
  import ShellRouteTransition from "./ShellRouteTransition.vue";
14
27
 
@@ -28,6 +41,18 @@ const props = defineProps({
28
41
  subtitle: {
29
42
  type: String,
30
43
  default: ""
44
+ },
45
+ desktopDrawerClosedMode: {
46
+ type: String,
47
+ default: "rail"
48
+ },
49
+ drawerWidth: {
50
+ type: Number,
51
+ default: null
52
+ },
53
+ railWidth: {
54
+ type: Number,
55
+ default: DEFAULT_SHELL_RAIL_WIDTH
31
56
  }
32
57
  });
33
58
 
@@ -47,7 +72,14 @@ const display = useDisplay();
47
72
  const refreshRuntime = inject("jskit.shell-web.runtime.web-refresh.client", null);
48
73
  const pullDistance = ref(0);
49
74
  const pullRefreshing = ref(false);
75
+ const wideDrawerOpen = ref(Boolean(drawerDefaultOpen.value));
76
+ const navigationToggle = ref(null);
77
+ const navigationDrawer = ref(null);
78
+ const measuredDrawerWidth = ref(DEFAULT_SHELL_DRAWER_WIDTH);
50
79
  let activePull = null;
80
+ let drawerContentObserver = null;
81
+ let drawerMeasurementFrame = null;
82
+ let shellMounted = false;
51
83
 
52
84
  const PULL_REFRESH_TRIGGER_DISTANCE = 72;
53
85
  const PULL_REFRESH_MAX_DISTANCE = 112;
@@ -63,6 +95,22 @@ const layoutClass = computed(() => {
63
95
  return "expanded";
64
96
  });
65
97
  const isCompactLayout = computed(() => layoutClass.value === "compact");
98
+ const drawerPresentation = computed(() => resolveShellDrawerPresentation({
99
+ compact: isCompactLayout.value,
100
+ open: drawerOpen.value,
101
+ desktopClosedMode: props.desktopDrawerClosedMode
102
+ }));
103
+ const drawerToggleLabel = computed(() => resolveShellDrawerToggleLabel({
104
+ compact: isCompactLayout.value,
105
+ open: drawerOpen.value
106
+ }));
107
+ const resolvedDrawerWidth = computed(() => {
108
+ if (props.drawerWidth !== null && props.drawerWidth !== undefined) {
109
+ return normalizeShellDrawerWidth(props.drawerWidth);
110
+ }
111
+ return normalizeShellDrawerWidth(measuredDrawerWidth.value);
112
+ });
113
+ const resolvedRailWidth = computed(() => normalizeShellRailWidth(props.railWidth));
66
114
  const pullProgress = computed(() =>
67
115
  Math.min(100, Math.round((pullDistance.value / PULL_REFRESH_TRIGGER_DISTANCE) * 100))
68
116
  );
@@ -79,19 +127,31 @@ const pullRefreshStyle = computed(() => ({
79
127
  "--shell-pull-refresh-distance": `${Math.round(Math.min(pullDistance.value, PULL_REFRESH_MAX_DISTANCE))}px`
80
128
  }));
81
129
 
130
+ watch(
131
+ drawerOpen,
132
+ handleDrawerOpenChange
133
+ );
134
+
82
135
  watch(
83
136
  isCompactLayout,
84
- (compact) => {
85
- setDrawerOpen(compact ? false : drawerDefaultOpen.value);
86
- },
137
+ handleLayoutClassChange,
87
138
  { immediate: true }
88
139
  );
89
140
 
90
- onMounted(() => {
141
+ watch(drawerPresentation, scheduleDrawerWidthMeasurement, { flush: "post" });
142
+ watch(resolvedSurface, scheduleDrawerWidthMeasurement, { flush: "post" });
143
+
144
+ onMounted(attachShellListeners);
145
+ onBeforeUnmount(detachShellListeners);
146
+
147
+ function attachShellListeners() {
148
+ shellMounted = true;
91
149
  if (typeof window !== "object") {
92
150
  return;
93
151
  }
94
152
 
153
+ window.addEventListener("keydown", handleShellKeydown);
154
+ window.addEventListener("resize", scheduleDrawerWidthMeasurement, { passive: true });
95
155
  window.addEventListener("pointerdown", handlePullPointerDown, { capture: true, passive: true });
96
156
  window.addEventListener("pointermove", handlePullPointerMove, { capture: true, passive: false });
97
157
  window.addEventListener("pointerup", handlePullPointerEnd, { capture: true, passive: true });
@@ -100,13 +160,22 @@ onMounted(() => {
100
160
  window.addEventListener("touchmove", handlePullTouchMove, { capture: true, passive: false });
101
161
  window.addEventListener("touchend", handlePullTouchEnd, { capture: true, passive: true });
102
162
  window.addEventListener("touchcancel", handlePullTouchCancel, { capture: true, passive: true });
103
- });
104
163
 
105
- onBeforeUnmount(() => {
164
+ if (document?.fonts) {
165
+ void document.fonts.ready.then(scheduleDrawerWidthMeasurement);
166
+ document.fonts.addEventListener?.("loadingdone", scheduleDrawerWidthMeasurement);
167
+ }
168
+ void nextTick().then(initializeDrawerMeasurement);
169
+ }
170
+
171
+ function detachShellListeners() {
172
+ shellMounted = false;
106
173
  if (typeof window !== "object") {
107
174
  return;
108
175
  }
109
176
 
177
+ window.removeEventListener("keydown", handleShellKeydown);
178
+ window.removeEventListener("resize", scheduleDrawerWidthMeasurement);
110
179
  window.removeEventListener("pointerdown", handlePullPointerDown, { capture: true });
111
180
  window.removeEventListener("pointermove", handlePullPointerMove, { capture: true });
112
181
  window.removeEventListener("pointerup", handlePullPointerEnd, { capture: true });
@@ -115,7 +184,135 @@ onBeforeUnmount(() => {
115
184
  window.removeEventListener("touchmove", handlePullTouchMove, { capture: true });
116
185
  window.removeEventListener("touchend", handlePullTouchEnd, { capture: true });
117
186
  window.removeEventListener("touchcancel", handlePullTouchCancel, { capture: true });
118
- });
187
+ document?.fonts?.removeEventListener?.("loadingdone", scheduleDrawerWidthMeasurement);
188
+ drawerContentObserver?.disconnect();
189
+ drawerContentObserver = null;
190
+ if (drawerMeasurementFrame !== null) {
191
+ window.cancelAnimationFrame(drawerMeasurementFrame);
192
+ drawerMeasurementFrame = null;
193
+ }
194
+ }
195
+
196
+ function handleDrawerOpenChange(open) {
197
+ if (!isCompactLayout.value) {
198
+ wideDrawerOpen.value = Boolean(open);
199
+ }
200
+ }
201
+
202
+ function handleLayoutClassChange(compact) {
203
+ setDrawerOpen(compact ? false : wideDrawerOpen.value);
204
+ }
205
+
206
+ function handleShellKeydown(event) {
207
+ if (
208
+ event?.defaultPrevented ||
209
+ event?.key !== "Escape" ||
210
+ !isCompactLayout.value ||
211
+ !drawerOpen.value
212
+ ) {
213
+ return;
214
+ }
215
+
216
+ event.preventDefault();
217
+ setDrawerOpen(false);
218
+ void nextTick().then(focusNavigationToggle);
219
+ }
220
+
221
+ function focusNavigationToggle() {
222
+ const toggleElement = navigationToggle.value?.$el || navigationToggle.value;
223
+ toggleElement?.focus?.({ preventScroll: true });
224
+ }
225
+
226
+ function initializeDrawerMeasurement() {
227
+ const drawerElement = resolveNavigationDrawerElement();
228
+ const drawerContent = drawerElement?.querySelector?.(".v-navigation-drawer__content") || drawerElement;
229
+ if (typeof MutationObserver === "function" && drawerContent) {
230
+ drawerContentObserver?.disconnect();
231
+ drawerContentObserver = new MutationObserver(scheduleDrawerWidthMeasurement);
232
+ drawerContentObserver.observe(drawerContent, {
233
+ childList: true,
234
+ characterData: true,
235
+ subtree: true
236
+ });
237
+ }
238
+ scheduleDrawerWidthMeasurement();
239
+ }
240
+
241
+ function resolveNavigationDrawerElement() {
242
+ const exposedElement = navigationDrawer.value?.$el || navigationDrawer.value || null;
243
+ if (typeof exposedElement?.getBoundingClientRect === "function") {
244
+ return exposedElement;
245
+ }
246
+ if (typeof document === "object") {
247
+ return document.getElementById("jskit-shell-drawer");
248
+ }
249
+ return null;
250
+ }
251
+
252
+ function scheduleDrawerWidthMeasurement() {
253
+ if (
254
+ !shellMounted ||
255
+ props.drawerWidth !== null && props.drawerWidth !== undefined ||
256
+ !drawerPresentation.value.visible ||
257
+ drawerPresentation.value.rail ||
258
+ typeof window !== "object"
259
+ ) {
260
+ return;
261
+ }
262
+
263
+ if (drawerMeasurementFrame !== null) {
264
+ window.cancelAnimationFrame(drawerMeasurementFrame);
265
+ }
266
+ drawerMeasurementFrame = window.requestAnimationFrame(measureDrawerContentWidth);
267
+ }
268
+
269
+ function measureDrawerContentWidth() {
270
+ drawerMeasurementFrame = null;
271
+ const drawerElement = resolveNavigationDrawerElement();
272
+ if (!drawerElement || typeof document !== "object") {
273
+ return;
274
+ }
275
+
276
+ const drawerRect = drawerElement.getBoundingClientRect();
277
+ const drawerStyle = window.getComputedStyle(drawerElement);
278
+ const rightToLeft = drawerStyle.direction === "rtl";
279
+ const endBorderWidth = Number.parseFloat(
280
+ rightToLeft ? drawerStyle.borderLeftWidth : drawerStyle.borderRightWidth
281
+ ) || 0;
282
+ const measurements = [];
283
+
284
+ for (const label of drawerElement.querySelectorAll(".v-list-item-title")) {
285
+ const labelStyle = window.getComputedStyle(label);
286
+ if (labelStyle.display === "none" || labelStyle.visibility === "hidden") {
287
+ continue;
288
+ }
289
+ const textRect = measureRenderedText(label);
290
+ if (!textRect || textRect.width <= 0) {
291
+ continue;
292
+ }
293
+ measurements.push({
294
+ logicalStart: rightToLeft
295
+ ? drawerRect.right - textRect.right
296
+ : textRect.left - drawerRect.left,
297
+ textWidth: textRect.width
298
+ });
299
+ }
300
+
301
+ const nextWidth = resolveContentAwareDrawerWidth(measurements, {
302
+ endGap: SHELL_DRAWER_LABEL_END_GAP + endBorderWidth
303
+ });
304
+ if (Math.abs(nextWidth - measuredDrawerWidth.value) >= 0.25) {
305
+ measuredDrawerWidth.value = nextWidth;
306
+ }
307
+ }
308
+
309
+ function measureRenderedText(element) {
310
+ const range = document.createRange();
311
+ range.selectNodeContents(element);
312
+ const rect = range.getBoundingClientRect();
313
+ range.detach?.();
314
+ return rect;
315
+ }
119
316
 
120
317
  function handlePullPointerDown(event) {
121
318
  if (!canStartPullRefresh(event)) {
@@ -132,6 +329,12 @@ function handlePullPointerDown(event) {
132
329
  };
133
330
  }
134
331
 
332
+ function handleDrawerVisibilityChange(open) {
333
+ if (isCompactLayout.value || drawerPresentation.value.closedMode === "hidden" || open === false) {
334
+ setDrawerOpen(open);
335
+ }
336
+ }
337
+
135
338
  function handlePullPointerMove(event) {
136
339
  if (!activePull || event.pointerId !== activePull.pointerId) {
137
340
  return;
@@ -358,8 +561,12 @@ function touchListIncludesActiveTouch(touchList) {
358
561
  data-testid="jskit-shell-app-bar"
359
562
  >
360
563
  <v-app-bar-nav-icon
564
+ ref="navigationToggle"
361
565
  class="shell-layout__nav-toggle"
362
- aria-label="Toggle navigation menu"
566
+ data-testid="jskit-shell-nav-toggle"
567
+ :aria-label="drawerToggleLabel"
568
+ :aria-expanded="drawerOpen"
569
+ aria-controls="jskit-shell-drawer"
363
570
  @click="toggleDrawer"
364
571
  />
365
572
 
@@ -400,17 +607,30 @@ function touchListIncludesActiveTouch(touchList) {
400
607
  </div>
401
608
 
402
609
  <v-navigation-drawer
403
- v-model="drawerOpen"
610
+ ref="navigationDrawer"
611
+ id="jskit-shell-drawer"
612
+ :model-value="drawerPresentation.visible"
404
613
  border
405
- class="bg-surface"
614
+ class="shell-layout__drawer bg-surface"
406
615
  data-testid="jskit-shell-drawer"
616
+ :data-layout="layoutClass"
617
+ :data-presentation="drawerPresentation.kind"
618
+ :data-drawer-width-mode="props.drawerWidth === null || props.drawerWidth === undefined ? 'content' : 'fixed'"
619
+ :data-drawer-width="resolvedDrawerWidth"
620
+ :data-rail-width="resolvedRailWidth"
621
+ :style="{ '--shell-drawer-label-end-gap': `${SHELL_DRAWER_LABEL_END_GAP}px` }"
407
622
  :temporary="isCompactLayout"
408
623
  :permanent="!isCompactLayout"
409
- :width="248"
624
+ :width="resolvedDrawerWidth"
625
+ :rail="drawerPresentation.rail"
626
+ :rail-width="resolvedRailWidth"
627
+ @update:model-value="handleDrawerVisibilityChange"
410
628
  >
411
- <slot name="menu" :surface="resolvedSurface">
629
+ <slot name="menu" :surface="resolvedSurface" :rail="drawerPresentation.rail">
412
630
  <v-list nav density="comfortable" class="pt-2">
413
- <v-list-subheader class="text-uppercase text-caption">{{ resolvedSurfaceLabel }}</v-list-subheader>
631
+ <v-list-subheader v-if="!drawerPresentation.rail" class="text-uppercase text-caption">
632
+ {{ resolvedSurfaceLabel }}
633
+ </v-list-subheader>
414
634
  <ShellOutlet
415
635
  target="shell-layout:primary-menu"
416
636
  default
@@ -495,6 +715,56 @@ function touchListIncludesActiveTouch(touchList) {
495
715
  min-width: 48px;
496
716
  }
497
717
 
718
+ .shell-layout__drawer :deep(.v-list-item-title) {
719
+ white-space: nowrap;
720
+ }
721
+
722
+ .shell-layout__drawer[data-presentation="drawer"] :deep(.v-list--nav),
723
+ .shell-layout__drawer[data-presentation="drawer"] :deep(.v-list-item) {
724
+ padding-inline-end: calc(var(--shell-drawer-label-end-gap) / 2);
725
+ }
726
+
727
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item) {
728
+ grid-template-areas: "prepend";
729
+ grid-template-columns: minmax(0, 1fr);
730
+ inline-size: 100%;
731
+ min-height: 48px;
732
+ min-width: 48px;
733
+ padding-inline: 0;
734
+ }
735
+
736
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__prepend) {
737
+ inline-size: 100%;
738
+ justify-content: center;
739
+ min-width: 0;
740
+ }
741
+
742
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__prepend .v-list-item__spacer),
743
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__content),
744
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__append) {
745
+ display: none;
746
+ }
747
+
748
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__overlay) {
749
+ border-radius: 999px;
750
+ inset-block: 8px;
751
+ inset-inline: 4px;
752
+ }
753
+
754
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item--active > .v-list-item__overlay) {
755
+ background: color-mix(
756
+ in srgb,
757
+ rgb(var(--v-theme-primary)) 18%,
758
+ rgb(var(--v-theme-surface))
759
+ );
760
+ opacity: 1;
761
+ }
762
+
763
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item--active .v-icon) {
764
+ color: rgb(var(--v-theme-primary));
765
+ opacity: 1;
766
+ }
767
+
498
768
  .shell-layout__top-right {
499
769
  max-width: min(45vw, 18rem);
500
770
  overflow: hidden;
@@ -5,6 +5,8 @@ import {
5
5
  useWebPlacementContext
6
6
  } from "../placement/index.js";
7
7
  import { resolveMenuLinkIcon } from "../lib/menuIcons.js";
8
+ import { activateShellNavigationLinkOnSpace } from "../support/navigationLinkKeyboard.js";
9
+ import ShellNavigationTooltip from "./ShellNavigationTooltip.vue";
8
10
 
9
11
  const props = defineProps({
10
12
  label: {
@@ -59,16 +61,22 @@ const resolvedIcon = computed(() =>
59
61
  </script>
60
62
 
61
63
  <template>
62
- <v-list-item
63
- v-if="resolvedTarget.href"
64
- class="shell-menu-link-item"
65
- :title="props.label"
66
- :to="resolvedTarget.sameOrigin ? resolvedTarget.href : undefined"
67
- :href="resolvedTarget.sameOrigin ? undefined : resolvedTarget.href"
68
- :prepend-icon="resolvedIcon || undefined"
69
- :disabled="props.disabled"
70
- :exact="props.exact"
71
- />
64
+ <ShellNavigationTooltip v-if="resolvedTarget.href" :label="props.label">
65
+ <template #default="{ activatorProps }">
66
+ <v-list-item
67
+ v-bind="activatorProps"
68
+ class="shell-menu-link-item"
69
+ :title="props.label"
70
+ :to="resolvedTarget.sameOrigin ? resolvedTarget.href : undefined"
71
+ :href="resolvedTarget.sameOrigin ? undefined : resolvedTarget.href"
72
+ :prepend-icon="resolvedIcon || undefined"
73
+ :disabled="props.disabled"
74
+ :exact="props.exact"
75
+ :aria-label="props.label"
76
+ @keydown.space="activateShellNavigationLinkOnSpace"
77
+ />
78
+ </template>
79
+ </ShellNavigationTooltip>
72
80
  </template>
73
81
 
74
82
  <style scoped>
@@ -0,0 +1,37 @@
1
+ <script setup>
2
+ import { useTheme } from "vuetify";
3
+
4
+ const props = defineProps({
5
+ label: {
6
+ type: String,
7
+ default: ""
8
+ }
9
+ });
10
+
11
+ const theme = useTheme();
12
+ </script>
13
+
14
+ <template>
15
+ <v-tooltip
16
+ :text="props.label"
17
+ :theme="theme.name.value"
18
+ location="end"
19
+ content-class="shell-navigation-tooltip"
20
+ :open-on-focus="true"
21
+ :open-on-hover="true"
22
+ >
23
+ <template #activator="{ props: activatorProps }">
24
+ <slot :activator-props="activatorProps" />
25
+ </template>
26
+ </v-tooltip>
27
+ </template>
28
+
29
+ <style>
30
+ .shell-navigation-tooltip {
31
+ background-color:
32
+ rgb(var(--v-theme-inverse-surface, var(--v-theme-on-surface))) !important;
33
+ color:
34
+ rgb(var(--v-theme-inverse-on-surface, var(--v-theme-surface))) !important;
35
+ opacity: 1 !important;
36
+ }
37
+ </style>
@@ -9,6 +9,8 @@ import {
9
9
  } from "../placement/index.js";
10
10
  import { resolveMenuLinkIcon } from "../lib/menuIcons.js";
11
11
  import { resolveMenuLinkTarget } from "../support/menuLinkTarget.js";
12
+ import { activateShellNavigationLinkOnSpace } from "../support/navigationLinkKeyboard.js";
13
+ import ShellNavigationTooltip from "./ShellNavigationTooltip.vue";
12
14
 
13
15
  const props = defineProps({
14
16
  label: {
@@ -104,16 +106,22 @@ const resolvedIcon = computed(() =>
104
106
  </script>
105
107
 
106
108
  <template>
107
- <v-list-item
108
- v-if="resolvedTarget.href"
109
- class="shell-menu-link-item"
110
- :title="props.label"
111
- :to="resolvedTarget.sameOrigin ? resolvedTarget.href : undefined"
112
- :href="resolvedTarget.sameOrigin ? undefined : resolvedTarget.href"
113
- :prepend-icon="resolvedIcon || undefined"
114
- :disabled="props.disabled"
115
- :exact="props.exact"
116
- />
109
+ <ShellNavigationTooltip v-if="resolvedTarget.href" :label="props.label">
110
+ <template #default="{ activatorProps }">
111
+ <v-list-item
112
+ v-bind="activatorProps"
113
+ class="shell-menu-link-item"
114
+ :title="props.label"
115
+ :to="resolvedTarget.sameOrigin ? resolvedTarget.href : undefined"
116
+ :href="resolvedTarget.sameOrigin ? undefined : resolvedTarget.href"
117
+ :prepend-icon="resolvedIcon || undefined"
118
+ :disabled="props.disabled"
119
+ :exact="props.exact"
120
+ :aria-label="props.label"
121
+ @keydown.space="activateShellNavigationLinkOnSpace"
122
+ />
123
+ </template>
124
+ </ShellNavigationTooltip>
117
125
  </template>
118
126
 
119
127
  <style scoped>
@@ -0,0 +1,58 @@
1
+ const DESKTOP_DRAWER_CLOSED_MODES = Object.freeze(["rail", "hidden"]);
2
+
3
+ function normalizeDesktopDrawerClosedMode(value = "rail") {
4
+ const normalized = String(value || "rail").trim().toLowerCase();
5
+ return DESKTOP_DRAWER_CLOSED_MODES.includes(normalized) ? normalized : "rail";
6
+ }
7
+
8
+ function resolveShellDrawerPresentation({
9
+ compact = false,
10
+ open = false,
11
+ desktopClosedMode = "rail"
12
+ } = {}) {
13
+ const closedMode = normalizeDesktopDrawerClosedMode(desktopClosedMode);
14
+ if (compact) {
15
+ return Object.freeze({
16
+ visible: Boolean(open),
17
+ rail: false,
18
+ kind: "modal",
19
+ closedMode
20
+ });
21
+ }
22
+ if (open) {
23
+ return Object.freeze({
24
+ visible: true,
25
+ rail: false,
26
+ kind: "drawer",
27
+ closedMode
28
+ });
29
+ }
30
+ if (closedMode === "hidden") {
31
+ return Object.freeze({
32
+ visible: false,
33
+ rail: false,
34
+ kind: "hidden",
35
+ closedMode
36
+ });
37
+ }
38
+ return Object.freeze({
39
+ visible: true,
40
+ rail: true,
41
+ kind: "rail",
42
+ closedMode
43
+ });
44
+ }
45
+
46
+ function resolveShellDrawerToggleLabel({ compact = false, open = false } = {}) {
47
+ if (compact) {
48
+ return open ? "Close navigation menu" : "Open navigation menu";
49
+ }
50
+ return open ? "Collapse navigation drawer" : "Expand navigation drawer";
51
+ }
52
+
53
+ export {
54
+ DESKTOP_DRAWER_CLOSED_MODES,
55
+ normalizeDesktopDrawerClosedMode,
56
+ resolveShellDrawerPresentation,
57
+ resolveShellDrawerToggleLabel
58
+ };
@@ -0,0 +1,74 @@
1
+ const DEFAULT_SHELL_DRAWER_WIDTH = 248;
2
+ const DEFAULT_SHELL_RAIL_WIDTH = 80;
3
+ const MINIMUM_SHELL_DRAWER_WIDTH = 120;
4
+ const MAXIMUM_SHELL_DRAWER_WIDTH = 360;
5
+ const MINIMUM_SHELL_RAIL_WIDTH = 48;
6
+ const MAXIMUM_SHELL_RAIL_WIDTH = 160;
7
+ const SHELL_DRAWER_LABEL_END_GAP = 10;
8
+
9
+ function clampNumber(value, minimum, maximum) {
10
+ return Math.min(maximum, Math.max(minimum, value));
11
+ }
12
+
13
+ function normalizeShellDrawerWidth(value, fallback = DEFAULT_SHELL_DRAWER_WIDTH) {
14
+ const parsed = Number(value);
15
+ if (!Number.isFinite(parsed) || parsed <= 0) {
16
+ return fallback;
17
+ }
18
+ return clampNumber(
19
+ parsed,
20
+ MINIMUM_SHELL_DRAWER_WIDTH,
21
+ MAXIMUM_SHELL_DRAWER_WIDTH
22
+ );
23
+ }
24
+
25
+ function normalizeShellRailWidth(value, fallback = DEFAULT_SHELL_RAIL_WIDTH) {
26
+ const parsed = Number(value);
27
+ if (!Number.isFinite(parsed) || parsed <= 0) {
28
+ return fallback;
29
+ }
30
+ return clampNumber(
31
+ Math.round(parsed),
32
+ MINIMUM_SHELL_RAIL_WIDTH,
33
+ MAXIMUM_SHELL_RAIL_WIDTH
34
+ );
35
+ }
36
+
37
+ function resolveContentAwareDrawerWidth(
38
+ measurements = [],
39
+ {
40
+ endGap = SHELL_DRAWER_LABEL_END_GAP,
41
+ minimum = MINIMUM_SHELL_DRAWER_WIDTH,
42
+ maximum = MAXIMUM_SHELL_DRAWER_WIDTH,
43
+ fallback = DEFAULT_SHELL_DRAWER_WIDTH
44
+ } = {}
45
+ ) {
46
+ let requiredWidth = 0;
47
+ for (const measurement of measurements) {
48
+ const logicalStart = Number(measurement?.logicalStart);
49
+ const textWidth = Number(measurement?.textWidth);
50
+ if (!Number.isFinite(logicalStart) || logicalStart < 0 || !Number.isFinite(textWidth) || textWidth <= 0) {
51
+ continue;
52
+ }
53
+ requiredWidth = Math.max(requiredWidth, logicalStart + textWidth + Number(endGap || 0));
54
+ }
55
+
56
+ if (requiredWidth <= 0) {
57
+ return normalizeShellDrawerWidth(fallback);
58
+ }
59
+ // Layout boxes expose integer client widths even when the rendered glyph
60
+ // range is fractional. Round outward so the final glyph is never ellipsized
61
+ // at the exact end-gap boundary.
62
+ return clampNumber(Math.ceil(requiredWidth), minimum, maximum);
63
+ }
64
+
65
+ export {
66
+ DEFAULT_SHELL_DRAWER_WIDTH,
67
+ DEFAULT_SHELL_RAIL_WIDTH,
68
+ MAXIMUM_SHELL_DRAWER_WIDTH,
69
+ MINIMUM_SHELL_DRAWER_WIDTH,
70
+ SHELL_DRAWER_LABEL_END_GAP,
71
+ normalizeShellDrawerWidth,
72
+ normalizeShellRailWidth,
73
+ resolveContentAwareDrawerWidth
74
+ };
@@ -0,0 +1,10 @@
1
+ function activateShellNavigationLinkOnSpace(event) {
2
+ if (event?.key !== " " || typeof event.currentTarget?.click !== "function") {
3
+ return;
4
+ }
5
+ event.preventDefault();
6
+ event.stopPropagation();
7
+ event.currentTarget.click();
8
+ }
9
+
10
+ export { activateShellNavigationLinkOnSpace };