sprintify-ui 0.12.22 → 0.12.24

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,6 +1,15 @@
1
1
  export default {
2
2
  extend: {
3
3
  spacing: {
4
+ // Measured height of the layout's sticky top header (system
5
+ // alerts + navbar). Kept in sync by the BaseLayout* components
6
+ // through the --app-header-height CSS variable. Use for anything
7
+ // that must clear the header: sticky offsets, scroll margins,
8
+ // 100vh math.
9
+ header: 'var(--app-header-height, 0px)',
10
+ // Header plus the standard content gap, for sticky elements and
11
+ // scroll targets that should sit just below the header.
12
+ 'below-header': 'calc(var(--app-header-height, 0px) + 1.25rem)',
4
13
  'control-xs': '1.65rem',
5
14
  'control-sm': '2rem',
6
15
  'control-md': '2.5rem',
@@ -0,0 +1,33 @@
1
+ import type { Ref, StyleValue } from 'vue';
2
+ /**
3
+ * The layout's sticky top header (system alerts + navbar) overlaps page
4
+ * content when it scrolls, so anything sticky or scroll-anchored inside the
5
+ * layout needs to know its rendered height. That height cannot be hardcoded:
6
+ * the navbar row depends on the layout `size` prop, and system alerts stack
7
+ * above it dynamically.
8
+ *
9
+ * The layout components measure their own header element and publish the
10
+ * height two ways:
11
+ *
12
+ * - The `--app-header-height` CSS variable, bound on the layout's content
13
+ * wrapper — for Tailwind/CSS (`top-header`, `scroll-mt-below-header`, ...,
14
+ * see the tailwindcss plugin's spacing tokens).
15
+ * - A provided reactive ref, read with `useLayoutHeaderHeight()` — for
16
+ * scripts (scrollspy root margins, canvas math, ...).
17
+ */
18
+ export declare const LAYOUT_HEADER_HEIGHT_CSS_VARIABLE = "--app-header-height";
19
+ /**
20
+ * Internal — called once by each layout component (BaseLayoutSidebar,
21
+ * BaseLayoutStacked) with a template ref to its sticky header element.
22
+ * Returns the reactive height and a style binding carrying the CSS variable,
23
+ * to spread on the element that wraps the layout's page content.
24
+ */
25
+ export declare function provideLayoutHeaderHeight(headerElement: Ref<HTMLElement | null>): {
26
+ headerHeight: Readonly<Ref<number>>;
27
+ headerHeightStyle: Readonly<Ref<StyleValue>>;
28
+ };
29
+ /**
30
+ * The rendered height (px) of the layout's sticky top header. Zero when used
31
+ * outside a layout component.
32
+ */
33
+ export declare function useLayoutHeaderHeight(): Readonly<Ref<number>>;
@@ -4,6 +4,7 @@ import { useDialogsStore } from './stores/dialogs';
4
4
  import { useSnackbarsStore } from './stores/snackbars';
5
5
  import { useSystemAlertStore } from './stores/systemAlerts';
6
6
  import { useClickOutside } from './composables/clickOutside';
7
+ import { LAYOUT_HEADER_HEIGHT_CSS_VARIABLE, useLayoutHeaderHeight } from './composables/layoutHeaderHeight';
7
8
  import { useField } from './composables/field';
8
9
  declare const messages: {
9
10
  en: {
@@ -269,5 +270,7 @@ export { useDialogsStore };
269
270
  export { useSnackbarsStore };
270
271
  export { useSystemAlertStore };
271
272
  export { useClickOutside };
273
+ export { useLayoutHeaderHeight };
274
+ export { LAYOUT_HEADER_HEIGHT_CSS_VARIABLE };
272
275
  export { useField };
273
276
  export { SettingsStorage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprintify-ui",
3
- "version": "0.12.22",
3
+ "version": "0.12.24",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "generate-llm-txt": "node scripts/generate-llm-txt.js",
@@ -43,25 +43,20 @@ const props = withDefaults(
43
43
  );
44
44
 
45
45
  const colorStyle = computed((): Record<string, string> => {
46
- try {
47
- const config = getColorConfig(props.color, props.contrast == 'high');
48
-
49
- const styles: Record<string, string> = {
50
- backgroundColor: config.backgroundColor,
51
- color: config.textColor,
52
- };
53
-
54
- if (props.bordered) {
55
- styles.borderColor = config.borderColor;
56
- styles.borderWidth = '1px';
57
- styles.borderStyle = 'solid';
58
- }
59
-
60
- return styles;
61
- } catch {
62
- console.warn(`[BaseBadge] "${props.color}" is not a valid color.`);
63
- return {};
46
+ const config = getColorConfig(props.color, props.contrast == 'high');
47
+
48
+ const styles: Record<string, string> = {
49
+ backgroundColor: config.backgroundColor,
50
+ color: config.textColor,
51
+ };
52
+
53
+ if (props.bordered) {
54
+ styles.borderColor = config.borderColor;
55
+ styles.borderWidth = '1px';
56
+ styles.borderStyle = 'solid';
64
57
  }
58
+
59
+ return styles;
65
60
  });
66
61
 
67
62
  const sizeInternal = computed(() => {
@@ -207,19 +207,15 @@ const classes = computed(() => {
207
207
 
208
208
  const styles = computed(() => {
209
209
  if (props.color && !(props.color in colors)) {
210
- try {
211
- const config = getColorConfig(props.color);
212
-
213
- return {
214
- backgroundColor: config.backgroundColor,
215
- color: config.textColor,
216
- borderColor: config.borderColor,
217
- borderWidth: '1px',
218
- borderStyle: 'solid',
219
- };
220
- } catch {
221
- console.warn(`[BaseButton] "${props.color}" is not a valid color.`);
222
- }
210
+ const config = getColorConfig(props.color);
211
+
212
+ return {
213
+ backgroundColor: config.backgroundColor,
214
+ color: config.textColor,
215
+ borderColor: config.borderColor,
216
+ borderWidth: '1px',
217
+ borderStyle: 'solid',
218
+ };
223
219
  }
224
220
 
225
221
  return {};
@@ -1,5 +1,8 @@
1
1
  <template>
2
- <div class="min-h-full base-layout-sidebar">
2
+ <div
3
+ class="min-h-full base-layout-sidebar"
4
+ :style="headerHeightStyle"
5
+ >
3
6
  <TransitionRoot
4
7
  as="template"
5
8
  :show="showMobileMenu"
@@ -95,7 +98,10 @@
95
98
  class="flex flex-col min-h-full || xl:print:pl-0"
96
99
  :class="[collapsed ? 'xl:pl-14' : 'xl:pl-64']"
97
100
  >
98
- <div class="sticky top-0 left-0 z-10 shadow shrink-0 || print:hidden">
101
+ <div
102
+ ref="headerElement"
103
+ class="sticky top-0 left-0 z-10 shadow shrink-0 || print:hidden"
104
+ >
99
105
  <BaseSystemAlert
100
106
  v-for="systemAlert in systemAlerts"
101
107
  :key="systemAlert.id"
@@ -205,6 +211,7 @@ import {
205
211
  TransitionRoot,
206
212
  } from '@headlessui/vue';
207
213
  import { config } from '@/index';
214
+ import { provideLayoutHeaderHeight } from '../composables/layoutHeaderHeight';
208
215
  import { useSystemAlertStore } from '../stores/systemAlerts';
209
216
  import { Icon as BaseIcon } from '@iconify/vue';
210
217
  import BaseSystemAlert from './BaseSystemAlert.vue';
@@ -243,6 +250,15 @@ const systemAlerts = computed(() => {
243
250
  return systemAlertStore.systemAlerts;
244
251
  });
245
252
 
253
+ /**
254
+ * Header height (sticky system alerts + navbar), published as a CSS variable
255
+ * and via useLayoutHeaderHeight().
256
+ */
257
+
258
+ const headerElement = ref<HTMLElement | null>(null);
259
+
260
+ const { headerHeightStyle } = provideLayoutHeaderHeight(headerElement);
261
+
246
262
  /**
247
263
  * Show/Hide sidebar
248
264
  */
@@ -1,5 +1,8 @@
1
1
  <template>
2
- <div :class="classInternal">
2
+ <div
3
+ ref="headerElement"
4
+ :class="classInternal"
5
+ >
3
6
  <BaseSystemAlert
4
7
  v-for="systemAlert in systemAlerts"
5
8
  :key="systemAlert.id"
@@ -33,13 +36,14 @@
33
36
  </BaseNavbar>
34
37
  </div>
35
38
 
36
- <div>
39
+ <div :style="headerHeightStyle">
37
40
  <slot />
38
41
  </div>
39
42
  </template>
40
43
 
41
44
  <script lang="ts" setup>
42
45
  import { twMerge } from 'tailwind-merge';
46
+ import { provideLayoutHeaderHeight } from '../composables/layoutHeaderHeight';
43
47
  import { useSystemAlertStore } from '../stores/systemAlerts';
44
48
  import BaseNavbar from './BaseNavbar.vue';
45
49
  import BaseSystemAlert from './BaseSystemAlert.vue';
@@ -75,4 +79,13 @@ const systemAlertStore = useSystemAlertStore();
75
79
  const systemAlerts = computed(() => {
76
80
  return systemAlertStore.systemAlerts;
77
81
  });
82
+
83
+ /**
84
+ * Header height (sticky system alerts + navbar), published as a CSS variable
85
+ * and via useLayoutHeaderHeight().
86
+ */
87
+
88
+ const headerElement = ref<HTMLElement | null>(null);
89
+
90
+ const { headerHeightStyle } = provideLayoutHeaderHeight(headerElement);
78
91
  </script>
@@ -0,0 +1,87 @@
1
+ import type { InjectionKey, Ref, StyleValue } from 'vue';
2
+
3
+ /**
4
+ * The layout's sticky top header (system alerts + navbar) overlaps page
5
+ * content when it scrolls, so anything sticky or scroll-anchored inside the
6
+ * layout needs to know its rendered height. That height cannot be hardcoded:
7
+ * the navbar row depends on the layout `size` prop, and system alerts stack
8
+ * above it dynamically.
9
+ *
10
+ * The layout components measure their own header element and publish the
11
+ * height two ways:
12
+ *
13
+ * - The `--app-header-height` CSS variable, bound on the layout's content
14
+ * wrapper — for Tailwind/CSS (`top-header`, `scroll-mt-below-header`, ...,
15
+ * see the tailwindcss plugin's spacing tokens).
16
+ * - A provided reactive ref, read with `useLayoutHeaderHeight()` — for
17
+ * scripts (scrollspy root margins, canvas math, ...).
18
+ */
19
+
20
+ export const LAYOUT_HEADER_HEIGHT_CSS_VARIABLE = '--app-header-height';
21
+
22
+ const layoutHeaderHeightKey: InjectionKey<Readonly<Ref<number>>> = Symbol(
23
+ 'layoutHeaderHeight'
24
+ );
25
+
26
+ /**
27
+ * Internal — called once by each layout component (BaseLayoutSidebar,
28
+ * BaseLayoutStacked) with a template ref to its sticky header element.
29
+ * Returns the reactive height and a style binding carrying the CSS variable,
30
+ * to spread on the element that wraps the layout's page content.
31
+ */
32
+ export function provideLayoutHeaderHeight(
33
+ headerElement: Ref<HTMLElement | null>
34
+ ): {
35
+ headerHeight: Readonly<Ref<number>>;
36
+ headerHeightStyle: Readonly<Ref<StyleValue>>;
37
+ } {
38
+ const height = ref(0);
39
+
40
+ let observer: ResizeObserver | null = null;
41
+
42
+ watch(
43
+ headerElement,
44
+ (element) => {
45
+ observer?.disconnect();
46
+ observer = null;
47
+
48
+ if (!element) {
49
+ height.value = 0;
50
+
51
+ return;
52
+ }
53
+
54
+ observer = new ResizeObserver(() => {
55
+ height.value = element.getBoundingClientRect().height;
56
+ });
57
+
58
+ observer.observe(element);
59
+
60
+ height.value = element.getBoundingClientRect().height;
61
+ },
62
+ { immediate: true }
63
+ );
64
+
65
+ onBeforeUnmount(() => {
66
+ observer?.disconnect();
67
+ observer = null;
68
+ });
69
+
70
+ const headerHeight = readonly(height);
71
+
72
+ provide(layoutHeaderHeightKey, headerHeight);
73
+
74
+ const headerHeightStyle = computed<StyleValue>(() => ({
75
+ [LAYOUT_HEADER_HEIGHT_CSS_VARIABLE]: `${height.value}px`,
76
+ }));
77
+
78
+ return { headerHeight, headerHeightStyle };
79
+ }
80
+
81
+ /**
82
+ * The rendered height (px) of the layout's sticky top header. Zero when used
83
+ * outside a layout component.
84
+ */
85
+ export function useLayoutHeaderHeight(): Readonly<Ref<number>> {
86
+ return inject(layoutHeaderHeightKey, readonly(ref(0)));
87
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,10 @@ import { useDialogsStore } from './stores/dialogs';
8
8
  import { useSnackbarsStore } from './stores/snackbars';
9
9
  import { useSystemAlertStore } from './stores/systemAlerts';
10
10
  import { useClickOutside } from './composables/clickOutside';
11
+ import {
12
+ LAYOUT_HEADER_HEIGHT_CSS_VARIABLE,
13
+ useLayoutHeaderHeight,
14
+ } from './composables/layoutHeaderHeight';
11
15
  import { useField } from './composables/field';
12
16
 
13
17
  const messages = { en, fr };
@@ -134,5 +138,7 @@ export { useDialogsStore };
134
138
  export { useSnackbarsStore };
135
139
  export { useSystemAlertStore };
136
140
  export { useClickOutside };
141
+ export { useLayoutHeaderHeight };
142
+ export { LAYOUT_HEADER_HEIGHT_CSS_VARIABLE };
137
143
  export { useField };
138
144
  export { SettingsStorage };
@@ -211,7 +211,7 @@ const colorNames = {
211
211
  } as Record<string, string>;
212
212
 
213
213
  export function getColorConfig(color: string, contrast = false): ColorConfig {
214
-
214
+ const originalColor = color;
215
215
  color = colorNames[color] || color;
216
216
 
217
217
  const colorConfig = palette[color]?.[contrast ? 'high' : 'low'];
@@ -220,20 +220,25 @@ export function getColorConfig(color: string, contrast = false): ColorConfig {
220
220
  return colorConfig;
221
221
  }
222
222
 
223
- const c = getContrast(color, 'white');
223
+ try {
224
+ const c = getContrast(color, 'white');
224
225
 
225
- let borderColor = 'transparent';
226
- let textColor = 'white';
226
+ let borderColor = 'transparent';
227
+ let textColor = 'white';
227
228
 
228
- if (c < 2) {
229
- textColor = darken(color, 0.5);
230
- borderColor = darken(color, 0.1);
231
- }
229
+ if (c < 2) {
230
+ textColor = darken(color, 0.5);
231
+ borderColor = darken(color, 0.1);
232
+ }
232
233
 
233
- return {
234
- backgroundColor: color,
235
- textColor: textColor,
236
- borderColor: borderColor,
237
- color: color
238
- };
234
+ return {
235
+ backgroundColor: color,
236
+ textColor: textColor,
237
+ borderColor: borderColor,
238
+ color: color,
239
+ };
240
+ } catch {
241
+ console.warn(`[getColorConfig] "${originalColor}" is not a valid color. Falling back to gray.`);
242
+ return palette.gray[contrast ? 'high' : 'low'];
243
+ }
239
244
  }