@globalbrain/sefirot 4.23.1 → 4.25.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.
package/config/vite.js CHANGED
@@ -46,8 +46,10 @@ export const baseConfig = {
46
46
  'sefirot/': fileURLToPath(new URL('../lib/', import.meta.url))
47
47
  },
48
48
 
49
+ // list the client-side direct dependencies/peerDependencies which get bundled
49
50
  dedupe: [
50
51
  '@sentry/browser',
52
+ '@sentry/vue',
51
53
  '@tanstack/vue-virtual',
52
54
  '@tinyhttp/content-disposition',
53
55
  '@tinyhttp/cookie',
@@ -56,10 +58,12 @@ export const baseConfig = {
56
58
  '@vuelidate/validators',
57
59
  '@vueuse/core',
58
60
  'body-scroll-lock',
59
- 'dayjs',
60
61
  'd3',
62
+ 'dayjs',
63
+ 'dompurify',
61
64
  'file-saver',
62
65
  'fuse.js',
66
+ 'html2canvas',
63
67
  'lodash-es',
64
68
  'markdown-it',
65
69
  'normalize.css',
@@ -67,8 +71,8 @@ export const baseConfig = {
67
71
  'pinia',
68
72
  'qs',
69
73
  'v-calendar',
70
- 'vue-router',
71
- 'vue'
74
+ 'vue',
75
+ 'vue-router'
72
76
  ]
73
77
  },
74
78
 
@@ -8,7 +8,7 @@ import SDropdown from './SDropdown.vue'
8
8
  export type { Mode, Size, Tooltip, Type }
9
9
 
10
10
  const props = defineProps<{
11
- tag?: string
11
+ tag?: Component | string
12
12
  size?: Size
13
13
  type?: Type
14
14
  mode?: Mode
@@ -22,8 +22,8 @@ export type Mode =
22
22
  | 'danger'
23
23
 
24
24
  export interface Tooltip {
25
- tag?: string
26
- triggerTag?: string
25
+ tag?: Component | string
26
+ triggerTag?: Component | string
27
27
  text?: MaybeRef<string | null>
28
28
  position?: Position
29
29
  display?: 'inline' | 'inline-block' | 'block'
@@ -32,7 +32,7 @@ export interface Tooltip {
32
32
  }
33
33
 
34
34
  const props = defineProps<{
35
- tag?: string
35
+ tag?: Component | string
36
36
  size?: Size
37
37
  type?: Type
38
38
  mode?: Mode
@@ -5,7 +5,7 @@ import { type DropdownSection } from '../composables/Dropdown'
5
5
  import SActionMenu, { type Mode, type Tooltip, type Type } from './SActionMenu.vue'
6
6
 
7
7
  defineProps<{
8
- tag?: string
8
+ tag?: Component | string
9
9
  type?: Type
10
10
  mode?: Mode
11
11
  icon?: Component
@@ -4,7 +4,7 @@ import { useControlSize } from '../composables/Control'
4
4
  import SButton, { type Mode, type Tooltip, type Type } from './SButton.vue'
5
5
 
6
6
  defineProps<{
7
- tag?: string
7
+ tag?: Component | string
8
8
  type?: Type
9
9
  mode?: Mode
10
10
  icon?: Component
@@ -3,7 +3,7 @@ import { type Component } from 'vue'
3
3
  import SButton, { type Mode, type Tooltip, type Type } from './SButton.vue'
4
4
 
5
5
  export interface Action {
6
- tag?: string
6
+ tag?: Component | string
7
7
  type?: Type
8
8
  mode?: Mode
9
9
  icon?: Component
@@ -29,6 +29,7 @@ defineProps<{
29
29
  <div v-for="action, i in actions" :key="i" class="action">
30
30
  <SButton
31
31
  size="small"
32
+ :tag="action.tag"
32
33
  :type="action.type"
33
34
  :mode="action.mode"
34
35
  :icon="action.icon"
@@ -1,52 +1,24 @@
1
1
  <script setup lang="ts">
2
- import { computed, nextTick, shallowRef, watch } from 'vue'
3
- import { type LinkCallback, type LinkSubscriberPayload, useLink, useMarkdown } from '../composables/Markdown'
2
+ import { type Component, computed } from 'vue'
3
+ import { useMarkdown } from '../composables/Markdown'
4
4
 
5
- const props = defineProps<{
6
- tag?: string
5
+ const props = withDefaults(defineProps<{
6
+ tag?: Component | string
7
7
  content: string
8
- callbacks?: LinkCallback[]
8
+ html?: boolean
9
9
  inline?: boolean
10
- }>()
11
-
12
- const emit = defineEmits<{
13
- clicked: [payload: LinkSubscriberPayload]
14
- }>()
15
-
16
- const container = shallowRef<Element | null>(null)
17
-
18
- const { addListeners, subscribe } = useLink({
19
- container,
20
- callbacks: props.callbacks
10
+ }>(), {
11
+ tag: 'div',
12
+ html: true
21
13
  })
22
14
 
23
- const markdown = useMarkdown()
24
- const rendered = computed(() => markdown(props.content, props.inline ?? false))
25
-
26
- watch(
27
- rendered,
28
- () => nextTick(() => addListeners()),
29
- { immediate: true }
30
- )
31
-
32
- subscribe((payload) => emit('clicked', payload))
15
+ const markdown = useMarkdown({ html: props.html, inline: props.inline })
16
+ const rendered = computed(() => markdown(props.content))
33
17
  </script>
34
18
 
35
19
  <template>
36
- <component
37
- v-if="$slots.default"
38
- :is="tag ?? 'div'"
39
- class="SMarkdown-container"
40
- ref="container"
41
- >
20
+ <component v-if="$slots.default" :is="tag" class="SMarkdown-container">
42
21
  <slot v-bind="{ rendered }" />
43
22
  </component>
44
-
45
- <component
46
- v-else
47
- :is="tag ?? 'div'"
48
- class="SMarkdown-container"
49
- ref="container"
50
- v-html="rendered"
51
- />
23
+ <component v-else :is="tag" class="SMarkdown-container" v-html="rendered" />
52
24
  </template>
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
- import { onMounted, ref } from 'vue'
2
+ import { type Component, onMounted, ref } from 'vue'
3
3
 
4
4
  defineProps<{
5
- tag?: string
5
+ tag?: Component | string
6
6
  }>()
7
7
 
8
8
  const mount = ref(true)
@@ -1,11 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import { onClickOutside, onKeyStroke, useElementHover, useFocusWithin } from '@vueuse/core'
3
- import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
3
+ import { type Component, computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
4
4
  import { type Position, useTooltip } from '../composables/Tooltip'
5
5
 
6
6
  const props = withDefaults(defineProps<{
7
- tag?: string
8
- triggerTag?: string
7
+ tag?: Component | string
8
+ triggerTag?: Component | string
9
9
  text?: string
10
10
  position?: Position
11
11
  display?: 'inline' | 'inline-block' | 'block'
@@ -1,34 +1,54 @@
1
+ import DOMPurify, { type Config } from 'dompurify'
1
2
  import MarkdownIt, { type Options as MarkdownItOptions } from 'markdown-it'
2
- import { type Ref, onUnmounted } from 'vue'
3
- import { useRouter } from 'vue-router'
4
- import { type LinkAttrs, isCallbackUrl, isExternalUrl, linkPlugin } from './markdown/LinkPlugin'
5
3
 
6
- export type UseMarkdown = (source: string, inline: boolean) => string
4
+ export type UseMarkdown = (source: string, inline?: boolean) => string
7
5
 
8
6
  export interface UseMarkdownOptions extends MarkdownItOptions {
9
- linkAttrs?: LinkAttrs
10
7
  config?: (md: MarkdownIt) => void
8
+ /** @default false */
9
+ inline?: boolean
10
+ domPurifyOptions?: Config
11
11
  }
12
12
 
13
- export function useMarkdown(options: UseMarkdownOptions = {}): UseMarkdown {
14
- const md = new MarkdownIt({
15
- html: true,
16
- linkify: true,
17
- ...options
18
- })
13
+ const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i
19
14
 
20
- md.use(linkPlugin, {
21
- target: '_blank',
22
- rel: 'noopener noreferrer',
23
- ...options.linkAttrs
24
- })
15
+ if (typeof document !== 'undefined') {
16
+ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
17
+ if (node.tagName === 'A') {
18
+ const target = node.getAttribute('target')
19
+ if (target && target !== '_blank' && target !== '_self') {
20
+ node.removeAttribute('target')
21
+ }
25
22
 
26
- if (options.config) {
27
- options.config(md)
28
- }
23
+ const href = node.getAttribute('href')
24
+ if (href && EXTERNAL_URL_RE.test(href)) {
25
+ node.setAttribute('target', '_blank')
26
+ node.setAttribute('rel', 'noreferrer')
27
+ }
29
28
 
30
- return (source, inline) => {
31
- return inline ? md.renderInline(source) : md.render(source)
29
+ node.classList.add('SMarkdown-link')
30
+ }
31
+ })
32
+ }
33
+
34
+ export function useMarkdown({
35
+ config,
36
+ inline: _inline,
37
+ domPurifyOptions,
38
+ ...options
39
+ }: UseMarkdownOptions = {}): UseMarkdown {
40
+ //
41
+
42
+ const md = new MarkdownIt({ html: true, linkify: true, ...options })
43
+ config?.(md)
44
+
45
+ return (source, inline = _inline) => {
46
+ const html = inline ? md.renderInline(source) : md.render(source)
47
+ return DOMPurify.sanitize(html, {
48
+ USE_PROFILES: { html: true },
49
+ ADD_ATTR: ['target'],
50
+ ...domPurifyOptions
51
+ })
32
52
  }
33
53
  }
34
54
 
@@ -45,109 +65,3 @@ export function useLinkifyIt() {
45
65
 
46
66
  return (source: string) => md.renderInline(source)
47
67
  }
48
-
49
- export interface UseLink {
50
- addListeners(): void
51
- removeListeners(): void
52
- subscribe(cb: LinkSubscriber): () => void
53
- }
54
-
55
- export interface UseLinkOptions {
56
- container: Ref<Element | null>
57
- callbacks?: LinkCallback[]
58
- }
59
-
60
- export interface LinkSubscriberPayload {
61
- event: Event
62
- target: HTMLAnchorElement
63
- isExternal: boolean
64
- isCallback: boolean
65
- }
66
-
67
- export type LinkSubscriber = (payload: LinkSubscriberPayload) => void
68
-
69
- export type LinkCallback = () => void
70
-
71
- export function useLink({ container, callbacks }: UseLinkOptions): UseLink {
72
- const router = useRouter()
73
- const subscribers: LinkSubscriber[] = []
74
-
75
- onUnmounted(() => removeListeners())
76
-
77
- function handler(event: Event): void {
78
- const target = event.target as HTMLAnchorElement
79
- const href = target.getAttribute('href')!
80
-
81
- if (!href) {
82
- return
83
- }
84
-
85
- const isExternal = isExternalUrl(href)
86
- const isCallback = isCallbackUrl(href)
87
-
88
- subscribers.forEach((sub) => sub({
89
- event,
90
- target,
91
- isExternal,
92
- isCallback
93
- }))
94
-
95
- if (isExternal) {
96
- return
97
- }
98
-
99
- if (!event.defaultPrevented) {
100
- event.preventDefault()
101
- }
102
-
103
- if (isCallback) {
104
- const idx = Number.parseInt(target.dataset.callbackId || '')
105
- const callback = (callbacks ?? [])[idx]
106
-
107
- if (!callback) {
108
- throw new Error(`Callback not found at index: ${idx}`)
109
- }
110
-
111
- return callback()
112
- }
113
-
114
- router.push(href)
115
- }
116
-
117
- function addListeners(): void {
118
- removeListeners()
119
-
120
- if (container.value) {
121
- findLinks(container.value).forEach((element) => {
122
- element.addEventListener('click', handler)
123
- })
124
- }
125
- }
126
-
127
- function removeListeners(): void {
128
- if (container.value) {
129
- findLinks(container.value).forEach((element) => {
130
- element.removeEventListener('click', handler)
131
- })
132
- }
133
- }
134
-
135
- function subscribe(fn: LinkSubscriber): () => void {
136
- subscribers.push(fn)
137
-
138
- return () => {
139
- const idx = subscribers.indexOf(fn)
140
- idx > -1 && subscribers.splice(idx, 1)
141
- }
142
- }
143
-
144
- return {
145
- addListeners,
146
- removeListeners,
147
- subscribe
148
- }
149
- }
150
-
151
- function findLinks(target: Element) {
152
- return target.querySelectorAll('a.SMarkdown-link')
153
- }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
3
  "type": "module",
4
- "version": "4.23.1",
4
+ "version": "4.25.0",
5
5
  "packageManager": "pnpm@9.15.4",
6
6
  "description": "Vue Components for Global Brain Design System.",
7
7
  "author": "Kia Ishii <ka.ishii@globalbrains.com>",
@@ -48,7 +48,7 @@
48
48
  "@types/body-scroll-lock": "^3.1.2",
49
49
  "@types/lodash-es": "^4.17.12",
50
50
  "@types/markdown-it": "^14.1.2",
51
- "@vue/reactivity": "^3.5.13",
51
+ "@vue/reactivity": "^3.5.16",
52
52
  "@vuelidate/core": "^2.0.3",
53
53
  "@vuelidate/validators": "^2.0.4",
54
54
  "@vueuse/core": "^12 || ^13",
@@ -58,23 +58,24 @@
58
58
  "lodash-es": "^4.17.21",
59
59
  "markdown-it": "^14.1.0",
60
60
  "normalize.css": "^8.0.1",
61
- "pinia": "^3.0.2",
62
- "postcss": "^8.5.3",
61
+ "pinia": "^3.0.3",
62
+ "postcss": "^8.5.5",
63
63
  "postcss-nested": "^7.0.2",
64
64
  "v-calendar": "3.0.1",
65
- "vue": "^3.5.13",
66
- "vue-router": "^4.5.0"
65
+ "vue": "^3.5.16",
66
+ "vue-router": "^4.5.1"
67
67
  },
68
68
  "dependencies": {
69
- "@sentry/browser": "^9.14.0",
70
- "@sentry/vue": "^9.14.0",
69
+ "@sentry/browser": "^9.29.0",
70
+ "@sentry/vue": "^9.29.0",
71
71
  "@tanstack/vue-virtual": "3.0.0-beta.62",
72
72
  "@tinyhttp/content-disposition": "^2.2.2",
73
73
  "@tinyhttp/cookie": "^2.1.1",
74
74
  "@types/d3": "^7.4.3",
75
75
  "@types/file-saver": "^2.0.7",
76
- "@types/qs": "^6.9.18",
76
+ "@types/qs": "^6.14.0",
77
77
  "d3": "^7.9.0",
78
+ "dompurify": "^3.2.6",
78
79
  "file-saver": "^2.0.5",
79
80
  "html2canvas": "^1.4.1",
80
81
  "magic-string": "^0.30.17",
@@ -91,10 +92,10 @@
91
92
  "@types/body-scroll-lock": "^3.1.2",
92
93
  "@types/lodash-es": "^4.17.12",
93
94
  "@types/markdown-it": "^14.1.2",
94
- "@types/node": "^22.14.1",
95
- "@vitejs/plugin-vue": "^5.2.3",
96
- "@vitest/coverage-v8": "^3.1.2",
97
- "@vue/reactivity": "^3.5.13",
95
+ "@types/node": "^24.0.1",
96
+ "@vitejs/plugin-vue": "^5.2.4",
97
+ "@vitest/coverage-v8": "^3.2.3",
98
+ "@vue/reactivity": "^3.5.16",
98
99
  "@vue/test-utils": "^2.4.6",
99
100
  "@vuelidate/core": "^2.0.3",
100
101
  "@vuelidate/validators": "^2.0.4",
@@ -103,23 +104,23 @@
103
104
  "dayjs": "^1.11.13",
104
105
  "eslint": "8.57.0",
105
106
  "fuse.js": "^7.1.0",
106
- "happy-dom": "^17.4.4",
107
+ "happy-dom": "^18.0.1",
107
108
  "histoire": "0.16.5",
108
109
  "lodash-es": "^4.17.21",
109
110
  "markdown-it": "^14.1.0",
110
111
  "normalize.css": "^8.0.1",
111
- "pinia": "^3.0.2",
112
- "postcss": "^8.5.3",
112
+ "pinia": "^3.0.3",
113
+ "postcss": "^8.5.5",
113
114
  "postcss-nested": "^7.0.2",
114
115
  "punycode": "^2.3.1",
115
- "release-it": "^19.0.1",
116
+ "release-it": "^19.0.3",
116
117
  "typescript": "~5.8.3",
117
118
  "v-calendar": "3.0.1",
118
- "vite": "^6.3.3",
119
- "vitepress": ">=2.0.0-alpha.3",
120
- "vitest": "^3.1.2",
121
- "vue": "^3.5.13",
122
- "vue-router": "^4.5.0",
119
+ "vite": "^6.3.5",
120
+ "vitepress": "^2.0.0-alpha.6",
121
+ "vitest": "^3.2.3",
122
+ "vue": "^3.5.16",
123
+ "vue-router": "^4.5.1",
123
124
  "vue-tsc": "^2.2.10"
124
125
  }
125
126
  }
@@ -1,45 +0,0 @@
1
- import type MarkdownIt from 'markdown-it'
2
-
3
- export type LinkAttrs = Record<string, string>
4
-
5
- const EXTERNAL_REGEX = /^https?:/
6
- const CALLBACK_REGEX = /\{([\d}]+)\}/
7
- const CALLBACK_HREF = '#callback'
8
-
9
- export function linkPlugin(md: MarkdownIt, linkAttrs: LinkAttrs = {}): void {
10
- md.renderer.rules.link_open = (tokens, idx, options, _env, self) => {
11
- const token = tokens[idx]
12
- const hrefIndex = token.attrIndex('href')
13
-
14
- if (hrefIndex >= 0) {
15
- const hrefAttr = token.attrs![hrefIndex]
16
- const url = decodeURIComponent(hrefAttr[1])
17
-
18
- if (isExternalUrl(url)) {
19
- Object.entries(linkAttrs).forEach(([key, val]) => {
20
- token.attrSet(key, val)
21
- })
22
- }
23
-
24
- if (isCallbackUrl(url)) {
25
- const matched = url.match(CALLBACK_REGEX)![1]
26
-
27
- token.attrSet('data-callback-id', matched)
28
-
29
- hrefAttr[1] = CALLBACK_HREF
30
- }
31
-
32
- token.attrSet('class', 'SMarkdown-link')
33
- }
34
-
35
- return self.renderToken(tokens, idx, options)
36
- }
37
- }
38
-
39
- export function isExternalUrl(url: string): boolean {
40
- return EXTERNAL_REGEX.test(url)
41
- }
42
-
43
- export function isCallbackUrl(url: string): boolean {
44
- return url === CALLBACK_HREF || CALLBACK_REGEX.test(decodeURIComponent(url))
45
- }