@jskit-ai/shell-web 0.1.149 → 0.1.151

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,
@@ -13,6 +14,15 @@ import {
13
14
  resolveShellDrawerPresentation,
14
15
  resolveShellDrawerToggleLabel
15
16
  } from "../support/drawerPresentation.js";
17
+ import {
18
+ DEFAULT_SHELL_DRAWER_WIDTH,
19
+ DEFAULT_SHELL_NAVIGATION_ITEM_SPACING,
20
+ DEFAULT_SHELL_RAIL_WIDTH,
21
+ normalizeShellDrawerWidth,
22
+ normalizeShellNavigationItemSpacing,
23
+ normalizeShellRailWidth,
24
+ resolveContentAwareDrawerWidth
25
+ } from "../support/drawerWidth.js";
16
26
  import ShellOutlet from "./ShellOutlet.vue";
17
27
  import ShellRouteTransition from "./ShellRouteTransition.vue";
18
28
 
@@ -36,6 +46,18 @@ const props = defineProps({
36
46
  desktopDrawerClosedMode: {
37
47
  type: String,
38
48
  default: "rail"
49
+ },
50
+ drawerWidth: {
51
+ type: Number,
52
+ default: null
53
+ },
54
+ railWidth: {
55
+ type: Number,
56
+ default: DEFAULT_SHELL_RAIL_WIDTH
57
+ },
58
+ navigationItemSpacing: {
59
+ type: Number,
60
+ default: DEFAULT_SHELL_NAVIGATION_ITEM_SPACING
39
61
  }
40
62
  });
41
63
 
@@ -56,7 +78,13 @@ const refreshRuntime = inject("jskit.shell-web.runtime.web-refresh.client", null
56
78
  const pullDistance = ref(0);
57
79
  const pullRefreshing = ref(false);
58
80
  const wideDrawerOpen = ref(Boolean(drawerDefaultOpen.value));
81
+ const navigationToggle = ref(null);
82
+ const navigationDrawer = ref(null);
83
+ const measuredDrawerWidth = ref(DEFAULT_SHELL_DRAWER_WIDTH);
59
84
  let activePull = null;
85
+ let drawerContentObserver = null;
86
+ let drawerMeasurementFrame = null;
87
+ let shellMounted = false;
60
88
 
61
89
  const PULL_REFRESH_TRIGGER_DISTANCE = 72;
62
90
  const PULL_REFRESH_MAX_DISTANCE = 112;
@@ -81,6 +109,16 @@ const drawerToggleLabel = computed(() => resolveShellDrawerToggleLabel({
81
109
  compact: isCompactLayout.value,
82
110
  open: drawerOpen.value
83
111
  }));
112
+ const resolvedDrawerWidth = computed(() => {
113
+ if (props.drawerWidth !== null && props.drawerWidth !== undefined) {
114
+ return normalizeShellDrawerWidth(props.drawerWidth);
115
+ }
116
+ return normalizeShellDrawerWidth(measuredDrawerWidth.value);
117
+ });
118
+ const resolvedRailWidth = computed(() => normalizeShellRailWidth(props.railWidth));
119
+ const resolvedNavigationItemSpacing = computed(() =>
120
+ normalizeShellNavigationItemSpacing(props.navigationItemSpacing)
121
+ );
84
122
  const pullProgress = computed(() =>
85
123
  Math.min(100, Math.round((pullDistance.value / PULL_REFRESH_TRIGGER_DISTANCE) * 100))
86
124
  );
@@ -99,26 +137,30 @@ const pullRefreshStyle = computed(() => ({
99
137
 
100
138
  watch(
101
139
  drawerOpen,
102
- (open) => {
103
- if (!isCompactLayout.value) {
104
- wideDrawerOpen.value = Boolean(open);
105
- }
106
- }
140
+ handleDrawerOpenChange
107
141
  );
108
142
 
109
143
  watch(
110
144
  isCompactLayout,
111
- (compact) => {
112
- setDrawerOpen(compact ? false : wideDrawerOpen.value);
113
- },
145
+ handleLayoutClassChange,
114
146
  { immediate: true }
115
147
  );
116
148
 
117
- onMounted(() => {
149
+ watch(drawerPresentation, scheduleDrawerWidthMeasurement, { flush: "post" });
150
+ watch(resolvedSurface, scheduleDrawerWidthMeasurement, { flush: "post" });
151
+ watch(resolvedNavigationItemSpacing, scheduleDrawerWidthMeasurement, { flush: "post" });
152
+
153
+ onMounted(attachShellListeners);
154
+ onBeforeUnmount(detachShellListeners);
155
+
156
+ function attachShellListeners() {
157
+ shellMounted = true;
118
158
  if (typeof window !== "object") {
119
159
  return;
120
160
  }
121
161
 
162
+ window.addEventListener("keydown", handleShellKeydown);
163
+ window.addEventListener("resize", scheduleDrawerWidthMeasurement, { passive: true });
122
164
  window.addEventListener("pointerdown", handlePullPointerDown, { capture: true, passive: true });
123
165
  window.addEventListener("pointermove", handlePullPointerMove, { capture: true, passive: false });
124
166
  window.addEventListener("pointerup", handlePullPointerEnd, { capture: true, passive: true });
@@ -127,13 +169,22 @@ onMounted(() => {
127
169
  window.addEventListener("touchmove", handlePullTouchMove, { capture: true, passive: false });
128
170
  window.addEventListener("touchend", handlePullTouchEnd, { capture: true, passive: true });
129
171
  window.addEventListener("touchcancel", handlePullTouchCancel, { capture: true, passive: true });
130
- });
131
172
 
132
- onBeforeUnmount(() => {
173
+ if (document?.fonts) {
174
+ void document.fonts.ready.then(scheduleDrawerWidthMeasurement);
175
+ document.fonts.addEventListener?.("loadingdone", scheduleDrawerWidthMeasurement);
176
+ }
177
+ void nextTick().then(initializeDrawerMeasurement);
178
+ }
179
+
180
+ function detachShellListeners() {
181
+ shellMounted = false;
133
182
  if (typeof window !== "object") {
134
183
  return;
135
184
  }
136
185
 
186
+ window.removeEventListener("keydown", handleShellKeydown);
187
+ window.removeEventListener("resize", scheduleDrawerWidthMeasurement);
137
188
  window.removeEventListener("pointerdown", handlePullPointerDown, { capture: true });
138
189
  window.removeEventListener("pointermove", handlePullPointerMove, { capture: true });
139
190
  window.removeEventListener("pointerup", handlePullPointerEnd, { capture: true });
@@ -142,7 +193,135 @@ onBeforeUnmount(() => {
142
193
  window.removeEventListener("touchmove", handlePullTouchMove, { capture: true });
143
194
  window.removeEventListener("touchend", handlePullTouchEnd, { capture: true });
144
195
  window.removeEventListener("touchcancel", handlePullTouchCancel, { capture: true });
145
- });
196
+ document?.fonts?.removeEventListener?.("loadingdone", scheduleDrawerWidthMeasurement);
197
+ drawerContentObserver?.disconnect();
198
+ drawerContentObserver = null;
199
+ if (drawerMeasurementFrame !== null) {
200
+ window.cancelAnimationFrame(drawerMeasurementFrame);
201
+ drawerMeasurementFrame = null;
202
+ }
203
+ }
204
+
205
+ function handleDrawerOpenChange(open) {
206
+ if (!isCompactLayout.value) {
207
+ wideDrawerOpen.value = Boolean(open);
208
+ }
209
+ }
210
+
211
+ function handleLayoutClassChange(compact) {
212
+ setDrawerOpen(compact ? false : wideDrawerOpen.value);
213
+ }
214
+
215
+ function handleShellKeydown(event) {
216
+ if (
217
+ event?.defaultPrevented ||
218
+ event?.key !== "Escape" ||
219
+ !isCompactLayout.value ||
220
+ !drawerOpen.value
221
+ ) {
222
+ return;
223
+ }
224
+
225
+ event.preventDefault();
226
+ setDrawerOpen(false);
227
+ void nextTick().then(focusNavigationToggle);
228
+ }
229
+
230
+ function focusNavigationToggle() {
231
+ const toggleElement = navigationToggle.value?.$el || navigationToggle.value;
232
+ toggleElement?.focus?.({ preventScroll: true });
233
+ }
234
+
235
+ function initializeDrawerMeasurement() {
236
+ const drawerElement = resolveNavigationDrawerElement();
237
+ const drawerContent = drawerElement?.querySelector?.(".v-navigation-drawer__content") || drawerElement;
238
+ if (typeof MutationObserver === "function" && drawerContent) {
239
+ drawerContentObserver?.disconnect();
240
+ drawerContentObserver = new MutationObserver(scheduleDrawerWidthMeasurement);
241
+ drawerContentObserver.observe(drawerContent, {
242
+ childList: true,
243
+ characterData: true,
244
+ subtree: true
245
+ });
246
+ }
247
+ scheduleDrawerWidthMeasurement();
248
+ }
249
+
250
+ function resolveNavigationDrawerElement() {
251
+ const exposedElement = navigationDrawer.value?.$el || navigationDrawer.value || null;
252
+ if (typeof exposedElement?.getBoundingClientRect === "function") {
253
+ return exposedElement;
254
+ }
255
+ if (typeof document === "object") {
256
+ return document.getElementById("jskit-shell-drawer");
257
+ }
258
+ return null;
259
+ }
260
+
261
+ function scheduleDrawerWidthMeasurement() {
262
+ if (
263
+ !shellMounted ||
264
+ props.drawerWidth !== null && props.drawerWidth !== undefined ||
265
+ !drawerPresentation.value.visible ||
266
+ drawerPresentation.value.rail ||
267
+ typeof window !== "object"
268
+ ) {
269
+ return;
270
+ }
271
+
272
+ if (drawerMeasurementFrame !== null) {
273
+ window.cancelAnimationFrame(drawerMeasurementFrame);
274
+ }
275
+ drawerMeasurementFrame = window.requestAnimationFrame(measureDrawerContentWidth);
276
+ }
277
+
278
+ function measureDrawerContentWidth() {
279
+ drawerMeasurementFrame = null;
280
+ const drawerElement = resolveNavigationDrawerElement();
281
+ if (!drawerElement || typeof document !== "object") {
282
+ return;
283
+ }
284
+
285
+ const drawerRect = drawerElement.getBoundingClientRect();
286
+ const drawerStyle = window.getComputedStyle(drawerElement);
287
+ const rightToLeft = drawerStyle.direction === "rtl";
288
+ const endBorderWidth = Number.parseFloat(
289
+ rightToLeft ? drawerStyle.borderLeftWidth : drawerStyle.borderRightWidth
290
+ ) || 0;
291
+ const measurements = [];
292
+
293
+ for (const label of drawerElement.querySelectorAll(".v-list-item-title")) {
294
+ const labelStyle = window.getComputedStyle(label);
295
+ if (labelStyle.display === "none" || labelStyle.visibility === "hidden") {
296
+ continue;
297
+ }
298
+ const textRect = measureRenderedText(label);
299
+ if (!textRect || textRect.width <= 0) {
300
+ continue;
301
+ }
302
+ measurements.push({
303
+ logicalStart: rightToLeft
304
+ ? drawerRect.right - textRect.right
305
+ : textRect.left - drawerRect.left,
306
+ textWidth: textRect.width
307
+ });
308
+ }
309
+
310
+ const nextWidth = resolveContentAwareDrawerWidth(measurements, {
311
+ endGap: resolvedNavigationItemSpacing.value + endBorderWidth
312
+ });
313
+ if (Math.abs(nextWidth - measuredDrawerWidth.value) >= 0.25) {
314
+ measuredDrawerWidth.value = nextWidth;
315
+ }
316
+ }
317
+
318
+ function measureRenderedText(element) {
319
+ const range = document.createRange();
320
+ range.selectNodeContents(element);
321
+ const rect = range.getBoundingClientRect();
322
+ range.detach?.();
323
+ return rect;
324
+ }
146
325
 
147
326
  function handlePullPointerDown(event) {
148
327
  if (!canStartPullRefresh(event)) {
@@ -391,6 +570,7 @@ function touchListIncludesActiveTouch(touchList) {
391
570
  data-testid="jskit-shell-app-bar"
392
571
  >
393
572
  <v-app-bar-nav-icon
573
+ ref="navigationToggle"
394
574
  class="shell-layout__nav-toggle"
395
575
  data-testid="jskit-shell-nav-toggle"
396
576
  :aria-label="drawerToggleLabel"
@@ -436,21 +616,33 @@ function touchListIncludesActiveTouch(touchList) {
436
616
  </div>
437
617
 
438
618
  <v-navigation-drawer
619
+ ref="navigationDrawer"
439
620
  id="jskit-shell-drawer"
440
621
  :model-value="drawerPresentation.visible"
441
622
  border
442
- class="bg-surface"
623
+ class="shell-layout__drawer bg-surface"
443
624
  data-testid="jskit-shell-drawer"
625
+ :data-layout="layoutClass"
444
626
  :data-presentation="drawerPresentation.kind"
627
+ :data-drawer-width-mode="props.drawerWidth === null || props.drawerWidth === undefined ? 'content' : 'fixed'"
628
+ :data-drawer-width="resolvedDrawerWidth"
629
+ :data-navigation-item-spacing="resolvedNavigationItemSpacing"
630
+ :data-rail-width="resolvedRailWidth"
631
+ :style="{ '--shell-navigation-item-spacing': `${resolvedNavigationItemSpacing}px` }"
445
632
  :temporary="isCompactLayout"
446
633
  :permanent="!isCompactLayout"
447
- :width="248"
634
+ :width="resolvedDrawerWidth"
448
635
  :rail="drawerPresentation.rail"
449
- :rail-width="80"
636
+ :rail-width="resolvedRailWidth"
450
637
  @update:model-value="handleDrawerVisibilityChange"
451
638
  >
452
639
  <slot name="menu" :surface="resolvedSurface" :rail="drawerPresentation.rail">
453
- <v-list nav density="comfortable" class="pt-2">
640
+ <v-list
641
+ nav
642
+ density="comfortable"
643
+ class="pt-2"
644
+ :prepend-gap="resolvedNavigationItemSpacing"
645
+ >
454
646
  <v-list-subheader v-if="!drawerPresentation.rail" class="text-uppercase text-caption">
455
647
  {{ resolvedSurfaceLabel }}
456
648
  </v-list-subheader>
@@ -538,6 +730,56 @@ function touchListIncludesActiveTouch(touchList) {
538
730
  min-width: 48px;
539
731
  }
540
732
 
733
+ .shell-layout__drawer :deep(.v-list-item-title) {
734
+ white-space: nowrap;
735
+ }
736
+
737
+ .shell-layout__drawer[data-presentation="drawer"] :deep(.v-list--nav),
738
+ .shell-layout__drawer[data-presentation="drawer"] :deep(.v-list-item) {
739
+ padding-inline-end: calc(var(--shell-navigation-item-spacing) / 2);
740
+ }
741
+
742
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item) {
743
+ grid-template-areas: "prepend";
744
+ grid-template-columns: minmax(0, 1fr);
745
+ inline-size: 100%;
746
+ min-height: 48px;
747
+ min-width: 48px;
748
+ padding-inline: 0;
749
+ }
750
+
751
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__prepend) {
752
+ inline-size: 100%;
753
+ justify-content: center;
754
+ min-width: 0;
755
+ }
756
+
757
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__prepend .v-list-item__spacer),
758
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__content),
759
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__append) {
760
+ display: none;
761
+ }
762
+
763
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__overlay) {
764
+ border-radius: 999px;
765
+ inset-block: 8px;
766
+ inset-inline: 4px;
767
+ }
768
+
769
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item--active > .v-list-item__overlay) {
770
+ background: color-mix(
771
+ in srgb,
772
+ rgb(var(--v-theme-primary)) 18%,
773
+ rgb(var(--v-theme-surface))
774
+ );
775
+ opacity: 1;
776
+ }
777
+
778
+ .shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item--active .v-icon) {
779
+ color: rgb(var(--v-theme-primary));
780
+ opacity: 1;
781
+ }
782
+
541
783
  .shell-layout__top-right {
542
784
  max-width: min(45vw, 18rem);
543
785
  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,18 +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
- :aria-label="props.label"
72
- v-tooltip="props.label"
73
- />
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>
74
80
  </template>
75
81
 
76
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,18 +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
- :aria-label="props.label"
117
- v-tooltip="props.label"
118
- />
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>
119
125
  </template>
120
126
 
121
127
  <style scoped>
@@ -0,0 +1,91 @@
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 DEFAULT_SHELL_NAVIGATION_ITEM_SPACING = 12;
8
+ const MINIMUM_SHELL_NAVIGATION_ITEM_SPACING = 8;
9
+ const MAXIMUM_SHELL_NAVIGATION_ITEM_SPACING = 24;
10
+
11
+ function clampNumber(value, minimum, maximum) {
12
+ return Math.min(maximum, Math.max(minimum, value));
13
+ }
14
+
15
+ function normalizeShellDrawerWidth(value, fallback = DEFAULT_SHELL_DRAWER_WIDTH) {
16
+ const parsed = Number(value);
17
+ if (!Number.isFinite(parsed) || parsed <= 0) {
18
+ return fallback;
19
+ }
20
+ return clampNumber(
21
+ parsed,
22
+ MINIMUM_SHELL_DRAWER_WIDTH,
23
+ MAXIMUM_SHELL_DRAWER_WIDTH
24
+ );
25
+ }
26
+
27
+ function normalizeShellRailWidth(value, fallback = DEFAULT_SHELL_RAIL_WIDTH) {
28
+ const parsed = Number(value);
29
+ if (!Number.isFinite(parsed) || parsed <= 0) {
30
+ return fallback;
31
+ }
32
+ return clampNumber(
33
+ Math.round(parsed),
34
+ MINIMUM_SHELL_RAIL_WIDTH,
35
+ MAXIMUM_SHELL_RAIL_WIDTH
36
+ );
37
+ }
38
+
39
+ function normalizeShellNavigationItemSpacing(
40
+ value,
41
+ fallback = DEFAULT_SHELL_NAVIGATION_ITEM_SPACING
42
+ ) {
43
+ const parsed = Number(value);
44
+ if (!Number.isFinite(parsed) || parsed <= 0) {
45
+ return fallback;
46
+ }
47
+ return clampNumber(
48
+ Math.round(parsed),
49
+ MINIMUM_SHELL_NAVIGATION_ITEM_SPACING,
50
+ MAXIMUM_SHELL_NAVIGATION_ITEM_SPACING
51
+ );
52
+ }
53
+
54
+ function resolveContentAwareDrawerWidth(
55
+ measurements = [],
56
+ {
57
+ endGap = DEFAULT_SHELL_NAVIGATION_ITEM_SPACING,
58
+ minimum = MINIMUM_SHELL_DRAWER_WIDTH,
59
+ maximum = MAXIMUM_SHELL_DRAWER_WIDTH,
60
+ fallback = DEFAULT_SHELL_DRAWER_WIDTH
61
+ } = {}
62
+ ) {
63
+ let requiredWidth = 0;
64
+ for (const measurement of measurements) {
65
+ const logicalStart = Number(measurement?.logicalStart);
66
+ const textWidth = Number(measurement?.textWidth);
67
+ if (!Number.isFinite(logicalStart) || logicalStart < 0 || !Number.isFinite(textWidth) || textWidth <= 0) {
68
+ continue;
69
+ }
70
+ requiredWidth = Math.max(requiredWidth, logicalStart + textWidth + Number(endGap || 0));
71
+ }
72
+
73
+ if (requiredWidth <= 0) {
74
+ return normalizeShellDrawerWidth(fallback);
75
+ }
76
+ // Preserve subpixel font metrics while rounding outward enough to avoid
77
+ // clipping the final glyph at the exact spacing boundary.
78
+ return clampNumber(Math.ceil(requiredWidth * 4) / 4, minimum, maximum);
79
+ }
80
+
81
+ export {
82
+ DEFAULT_SHELL_DRAWER_WIDTH,
83
+ DEFAULT_SHELL_NAVIGATION_ITEM_SPACING,
84
+ DEFAULT_SHELL_RAIL_WIDTH,
85
+ MAXIMUM_SHELL_DRAWER_WIDTH,
86
+ MINIMUM_SHELL_DRAWER_WIDTH,
87
+ normalizeShellDrawerWidth,
88
+ normalizeShellNavigationItemSpacing,
89
+ normalizeShellRailWidth,
90
+ resolveContentAwareDrawerWidth
91
+ };
@@ -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 };