sprintify-ui 0.12.23 → 0.12.25

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>>;
@@ -0,0 +1,4 @@
1
+ import type { Config } from 'dompurify';
2
+ import type { Directive } from 'vue';
3
+ export declare const SAFE_HTML_CONFIG: Config;
4
+ export declare const vSafeHtml: Directive<HTMLElement, string | null | undefined>;
@@ -4,7 +4,9 @@ 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';
9
+ import { vSafeHtml, SAFE_HTML_CONFIG } from './directives/safeHtml';
8
10
  declare const messages: {
9
11
  en: {
10
12
  cancel: string;
@@ -269,5 +271,9 @@ export { useDialogsStore };
269
271
  export { useSnackbarsStore };
270
272
  export { useSystemAlertStore };
271
273
  export { useClickOutside };
274
+ export { useLayoutHeaderHeight };
275
+ export { LAYOUT_HEADER_HEIGHT_CSS_VARIABLE };
272
276
  export { useField };
273
277
  export { SettingsStorage };
278
+ export { vSafeHtml };
279
+ export { SAFE_HTML_CONFIG };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprintify-ui",
3
- "version": "0.12.23",
3
+ "version": "0.12.25",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "generate-llm-txt": "node scripts/generate-llm-txt.js",
@@ -44,6 +44,7 @@
44
44
  "@tiptap/extension-subscript": "^3.20.4",
45
45
  "@tiptap/extension-superscript": "^3.20.4",
46
46
  "color2k": "^2.0.3",
47
+ "dompurify": "^3.4.14",
47
48
  "tailwind-merge": "^2.4.0"
48
49
  },
49
50
  "devDependencies": {
@@ -69,8 +69,8 @@
69
69
  name="tooltip"
70
70
  >
71
71
  <div
72
+ v-safe-html="tooltip"
72
73
  class="text-xs max-w-xs leading-snug rounded-md pt-1.5 pb-2 px-3 bg-slate-900 text-white"
73
- v-html="tooltip"
74
74
  />
75
75
  </slot>
76
76
  </transition>
@@ -79,6 +79,7 @@
79
79
  </template>
80
80
 
81
81
  <script lang="ts" setup>
82
+ import { vSafeHtml } from '@/directives/safeHtml';
82
83
  import { ClassNameValue, twMerge } from 'tailwind-merge';
83
84
  import { RouteLocationRaw } from 'vue-router';
84
85
  import { BaseIcon } from '.';
@@ -43,9 +43,9 @@
43
43
  @change="onVisibleColumnChange($event, element.newKey)"
44
44
  >
45
45
  <div
46
+ v-safe-html="element.label"
46
47
  class="text-xs grow leading-tight"
47
48
  :class="[element.toggle ? 'text-slate-700' : 'text-slate-400']"
48
- v-html="element.label"
49
49
  >
50
50
  </div>
51
51
  </label>
@@ -57,6 +57,7 @@
57
57
  </template>
58
58
 
59
59
  <script lang="ts" setup>
60
+ import { vSafeHtml } from '@/directives/safeHtml';
60
61
  import { cloneDeep } from 'lodash';
61
62
  import { PropType } from 'vue';
62
63
  import BaseDataTableTemplate from './BaseDataTableTemplate.vue';
@@ -58,12 +58,12 @@
58
58
  @click="sort(column, undefined, $event as any)"
59
59
  >
60
60
  <div
61
+ v-safe-html="column.label"
61
62
  class="whitespace-nowrap text-slate-600"
62
63
  :class="{
63
64
  'text-[12px]': size == 'sm',
64
65
  'text-xs': size == 'md',
65
66
  }"
66
- v-html="column.label"
67
67
  />
68
68
  <!-- h-4 is used to make sure the tooltip icon is always smaller than label and avoid alignment issues when icon is not present -->
69
69
  <div
@@ -197,6 +197,7 @@
197
197
  </template>
198
198
 
199
199
  <script lang="ts" setup>
200
+ import { vSafeHtml } from '@/directives/safeHtml';
200
201
  import { PropType, ref } from 'vue';
201
202
  import { BaseTableColumnData, CollectionItem, Row } from '@/types';
202
203
  import SlotComponent from './SlotComponent';
@@ -47,8 +47,8 @@
47
47
  <div class="mt-2">
48
48
  <div
49
49
  v-if="html"
50
+ v-safe-html="message"
50
51
  class="text-slate-500 prose-sm prose-p:leading-snug prose-a:text-blue-600 prose-a:underline"
51
- v-html="message"
52
52
  />
53
53
  <p
54
54
  v-else
@@ -118,6 +118,7 @@
118
118
  </template>
119
119
 
120
120
  <script lang="ts" setup>
121
+ import { vSafeHtml } from '@/directives/safeHtml';
121
122
  import { PropType } from 'vue';
122
123
  import { Icon as BaseIcon } from '@iconify/vue';
123
124
  import { t } from '@/i18n';
@@ -10,9 +10,9 @@
10
10
  />
11
11
  <div class="min-w-0 flex-1">
12
12
  <div
13
+ v-safe-html="notification.text"
13
14
  class="text-sm leading-tight"
14
15
  :class="textClasses"
15
- v-html="notification.text"
16
16
  />
17
17
  <BaseDisplayRelativeTime
18
18
  v-if="notification.created_at"
@@ -32,6 +32,7 @@
32
32
  </template>
33
33
 
34
34
  <script lang="ts" setup>
35
+ import { vSafeHtml } from '@/directives/safeHtml';
35
36
  import { Notification } from '@/types/Notification';
36
37
  import BaseDisplayRelativeTime from './BaseDisplayRelativeTime.vue';
37
38
 
@@ -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>
@@ -32,8 +32,8 @@
32
32
  name="tooltip"
33
33
  >
34
34
  <div
35
+ v-safe-html="tooltip"
35
36
  class="text-xs max-w-xs leading-snug rounded-md pt-1.5 pb-2 px-3 bg-slate-900 text-white"
36
- v-html="tooltip"
37
37
  />
38
38
  </slot>
39
39
  </transition>
@@ -42,6 +42,7 @@
42
42
  </template>
43
43
 
44
44
  <script lang="ts" setup>
45
+ import { vSafeHtml } from '@/directives/safeHtml';
45
46
  import { useTooltip } from '@/composables/tooltip';
46
47
  import { cellClasses } from '@/services/table/classes';
47
48
  import { CellConfig, TableProps } from '@/services/table/types';
@@ -28,11 +28,11 @@
28
28
  >
29
29
  <div class="relative filter drop-shadow-[0_1px_2px_rgba(0,0,0,0.1)]">
30
30
  <div
31
+ v-safe-html="text"
31
32
  class="text-xs max-w-xs leading-snug rounded-md pt-1.5 pb-2 px-3"
32
33
  :class="[
33
34
  dark ? 'bg-slate-900 text-white' : 'bg-white text-slate-900',
34
35
  ]"
35
- v-html="text"
36
36
  />
37
37
  <div
38
38
  ref="arrowRef"
@@ -51,6 +51,7 @@
51
51
  </template>
52
52
 
53
53
  <script lang="ts" setup>
54
+ import { vSafeHtml } from '@/directives/safeHtml';
54
55
  import { useTooltip } from '@/composables/tooltip';
55
56
  import { unrefElement } from '@vueuse/core';
56
57
  import { UseFloatingOptions } from '@floating-ui/vue';
@@ -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
+ }
@@ -0,0 +1,28 @@
1
+ import DOMPurify from 'dompurify';
2
+ import type { Config } from 'dompurify';
3
+ import type { Directive } from 'vue';
4
+
5
+ // Must keep BaseTipTap output intact: links (href/target), images,
6
+ // highlights (<mark style="background-color">), sub/superscript.
7
+ export const SAFE_HTML_CONFIG: Config = {
8
+ USE_PROFILES: { html: true },
9
+ ADD_ATTR: ['target'],
10
+ FORBID_TAGS: ['style', 'form', 'input', 'textarea', 'select', 'button'],
11
+ };
12
+
13
+ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
14
+ if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {
15
+ node.setAttribute('rel', 'noopener noreferrer');
16
+ }
17
+ });
18
+
19
+ export const vSafeHtml: Directive<HTMLElement, string | null | undefined> = {
20
+ mounted: (el, { value }) => {
21
+ el.innerHTML = DOMPurify.sanitize(value ?? '', SAFE_HTML_CONFIG);
22
+ },
23
+ updated: (el, { value, oldValue }) => {
24
+ if (value !== oldValue) {
25
+ el.innerHTML = DOMPurify.sanitize(value ?? '', SAFE_HTML_CONFIG);
26
+ }
27
+ },
28
+ };
package/src/index.ts CHANGED
@@ -8,7 +8,12 @@ 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';
16
+ import { vSafeHtml, SAFE_HTML_CONFIG } from './directives/safeHtml';
12
17
 
13
18
  const messages = { en, fr };
14
19
 
@@ -72,6 +77,8 @@ function install(app: App, options?: Options) {
72
77
  app.component(key, components[key]);
73
78
  }
74
79
 
80
+ app.directive('safe-html', vSafeHtml);
81
+
75
82
  if (options?.http) {
76
83
  config.http = options.http;
77
84
  }
@@ -134,5 +141,9 @@ export { useDialogsStore };
134
141
  export { useSnackbarsStore };
135
142
  export { useSystemAlertStore };
136
143
  export { useClickOutside };
144
+ export { useLayoutHeaderHeight };
145
+ export { LAYOUT_HEADER_HEIGHT_CSS_VARIABLE };
137
146
  export { useField };
138
147
  export { SettingsStorage };
148
+ export { vSafeHtml };
149
+ export { SAFE_HTML_CONFIG };