@globalbrain/sefirot 2.32.0 → 2.33.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.
@@ -1,8 +1,12 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue'
2
+ import type { MaybeRef } from '@vueuse/core'
3
+ import { computed, unref, useSlots } from 'vue'
4
+ import type { Position } from '../composables/Tooltip'
5
+ import SFragment from './SFragment.vue'
3
6
  import SIcon from './SIcon.vue'
4
7
  import SLink from './SLink.vue'
5
8
  import SSpinner from './SSpinner.vue'
9
+ import STooltip from './STooltip.vue'
6
10
 
7
11
  export type Size = 'mini' | 'small' | 'medium' | 'large' | 'jumbo'
8
12
 
@@ -18,6 +22,14 @@ export type Mode =
18
22
  | 'warning'
19
23
  | 'danger'
20
24
 
25
+ export interface Tooltip {
26
+ tag?: string
27
+ text?: MaybeRef<string>
28
+ position?: Position
29
+ trigger?: 'hover' | 'focus' | 'both'
30
+ timeout?: number
31
+ }
32
+
21
33
  const props = defineProps<{
22
34
  tag?: string
23
35
  size?: Size
@@ -32,6 +44,7 @@ const props = defineProps<{
32
44
  block?: boolean
33
45
  loading?: boolean
34
46
  disabled?: boolean
47
+ tooltip?: Tooltip
35
48
  }>()
36
49
 
37
50
  const emit = defineEmits<{
@@ -57,6 +70,12 @@ const computedTag = computed(() => {
57
70
  : props.href ? SLink : 'button'
58
71
  })
59
72
 
73
+ const slots = useSlots()
74
+
75
+ const hasTooltip = computed(() => {
76
+ return slots['tooltip-text'] || unref(props.tooltip?.text)
77
+ })
78
+
60
79
  function handleClick(): void {
61
80
  if (!props.loading) {
62
81
  props.disabled ? emit('disabled-click') : emit('click')
@@ -65,29 +84,41 @@ function handleClick(): void {
65
84
  </script>
66
85
 
67
86
  <template>
68
- <Component
69
- :is="computedTag"
70
- class="SButton"
71
- :class="classes"
72
- :href="href"
73
- role="button"
74
- @click="handleClick"
87
+ <SFragment
88
+ :is="hasTooltip && STooltip"
89
+ :tag="tooltip?.tag"
90
+ :text="unref(tooltip?.text)"
91
+ :position="tooltip?.position"
92
+ display="inline-block"
93
+ :trigger="tooltip?.trigger ?? 'both'"
94
+ :timeout="tooltip?.timeout"
95
+ :tabindex="-1"
75
96
  >
76
- <span class="content">
77
- <span v-if="icon" class="icon" :class="iconMode">
78
- <SIcon :icon="icon" class="icon-svg" />
97
+ <template v-if="$slots['tooltip-text']" #text><slot name="tooltip-text" /></template>
98
+ <component
99
+ :is="computedTag"
100
+ class="SButton"
101
+ :class="classes"
102
+ :href="href"
103
+ role="button"
104
+ @click="handleClick"
105
+ >
106
+ <span class="content">
107
+ <span v-if="icon" class="icon" :class="iconMode">
108
+ <SIcon :icon="icon" class="icon-svg" />
109
+ </span>
110
+ <span v-if="label" class="label" :class="labelMode">
111
+ {{ label }}
112
+ </span>
79
113
  </span>
80
- <span v-if="label" class="label" :class="labelMode">
81
- {{ label }}
82
- </span>
83
- </span>
84
114
 
85
- <Transition name="fade">
86
- <span v-if="loading" key="loading" class="loader">
87
- <SSpinner class="loader-icon" />
88
- </span>
89
- </Transition>
90
- </Component>
115
+ <transition name="fade">
116
+ <span v-if="loading" key="loading" class="loader">
117
+ <SSpinner class="loader-icon" />
118
+ </span>
119
+ </transition>
120
+ </component>
121
+ </SFragment>
91
122
  </template>
92
123
 
93
124
  <style scoped lang="postcss">
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ export default { inheritAttrs: false }
3
+ </script>
4
+
5
+ <script setup lang="ts">
6
+ defineProps<{ is?: any }>()
7
+ </script>
8
+
9
+ <template>
10
+ <component v-if="is" :is="is" v-bind="$attrs">
11
+ <template v-for="(_, slot) of $slots" #[slot]="scope">
12
+ <slot :name="slot" v-bind="scope" />
13
+ </template>
14
+ </component>
15
+ <slot v-else />
16
+ </template>
@@ -15,9 +15,9 @@ export interface Value {
15
15
  }
16
16
 
17
17
  export interface Placeholder {
18
- hour: string
19
- minute: string
20
- second: string
18
+ hour?: string
19
+ minute?: string
20
+ second?: string
21
21
  }
22
22
 
23
23
  export type ValueType = 'hour' | 'minute' | 'second'
@@ -63,9 +63,9 @@ const padValue = computed(() => {
63
63
 
64
64
  const padPlaceholder = computed(() => {
65
65
  return {
66
- hour: props.placeholder?.hour.toString().padStart(2, '0') ?? '00',
67
- minute: props.placeholder?.minute.toString().padStart(2, '0') ?? '00',
68
- second: props.placeholder?.second.toString().padStart(2, '0') ?? '00'
66
+ hour: props.placeholder?.hour?.toString().padStart(2, '0') ?? '00',
67
+ minute: props.placeholder?.minute?.toString().padStart(2, '0') ?? '00',
68
+ second: props.placeholder?.second?.toString().padStart(2, '0') ?? '00'
69
69
  }
70
70
  })
71
71
 
@@ -17,9 +17,9 @@ export interface Value {
17
17
  export type ValueType = 'year' | 'month' | 'date'
18
18
 
19
19
  export interface Placeholder {
20
- year: number
21
- month: number
22
- date: number
20
+ year?: number
21
+ month?: number
22
+ date?: number
23
23
  }
24
24
 
25
25
  const props = defineProps<{
@@ -64,9 +64,9 @@ const padValue = computed(() => {
64
64
 
65
65
  const padPlaceholder = computed(() => {
66
66
  return {
67
- year: props.placeholder?.year.toString().padStart(4, '0') ?? '1998',
68
- month: props.placeholder?.month.toString().padStart(2, '0') ?? '01',
69
- date: props.placeholder?.date.toString().padStart(2, '0') ?? '14'
67
+ year: props.placeholder?.year?.toString().padStart(4, '0') ?? '1998',
68
+ month: props.placeholder?.month?.toString().padStart(2, '0') ?? '01',
69
+ date: props.placeholder?.date?.toString().padStart(2, '0') ?? '14'
70
70
  }
71
71
  })
72
72
 
@@ -1,34 +1,111 @@
1
1
  <script setup lang="ts">
2
- import { computed, shallowRef } from 'vue'
3
- import type { Position } from '../composables/Tooltip'
4
- import { useTooltip } from '../composables/Tooltip'
2
+ import { onKeyStroke } from '@vueuse/core'
3
+ import { computed, ref, shallowRef } from 'vue'
4
+ import { type Position, useTooltip } from '../composables/Tooltip'
5
5
  import SMarkdown from './SMarkdown.vue'
6
6
 
7
- const props = defineProps<{
7
+ const props = withDefaults(defineProps<{
8
8
  tag?: string
9
9
  text?: string
10
10
  position?: Position
11
- }>()
12
-
11
+ display?: 'inline' | 'inline-block' | 'block'
12
+ trigger?: 'hover' | 'focus' | 'both'
13
+ timeout?: number
14
+ tabindex?: number
15
+ }>(), {
16
+ tag: 'span',
17
+ position: 'top',
18
+ trigger: 'hover'
19
+ })
20
+
21
+ const el = shallowRef<HTMLElement | null>(null)
13
22
  const tip = shallowRef<HTMLElement | null>(null)
14
23
  const content = shallowRef<HTMLElement | null>(null)
15
- const classes = computed(() => [props.position ?? 'top'])
24
+
25
+ const rootClasses = computed(() => [
26
+ props.display,
27
+ props.tabindex && (props.tabindex > -1) && 'focusable'
28
+ ])
29
+
30
+ const containerClasses = computed(() => [props.position])
31
+
32
+ const timeoutId = ref<number | null>(null)
33
+
34
+ const tabindex = computed(() => {
35
+ if (props.trigger === 'focus' || props.trigger === 'both') {
36
+ return props.tabindex ?? 0
37
+ }
38
+ return -1
39
+ })
16
40
 
17
41
  const { on, show, hide } = useTooltip(
42
+ el,
18
43
  content,
19
44
  tip,
20
- computed(() => props.position ?? 'top')
45
+ computed(() => props.position),
46
+ timeoutId
21
47
  )
48
+
49
+ onKeyStroke('Escape', (e) => {
50
+ if (on.value && el.value?.matches(':focus-within')) {
51
+ e.preventDefault()
52
+ e.stopPropagation()
53
+ hide()
54
+ }
55
+ })
56
+
57
+ function onMouseEnter() {
58
+ if (props.trigger === 'hover' || props.trigger === 'both') {
59
+ show()
60
+ }
61
+ }
62
+
63
+ function onMouseLeave() {
64
+ if (
65
+ props.trigger === 'hover'
66
+ || (props.trigger === 'both' && !el.value?.matches(':focus-within'))
67
+ ) {
68
+ hide()
69
+ }
70
+ }
71
+
72
+ function onFocus() {
73
+ if (props.trigger === 'focus' || props.trigger === 'both') {
74
+ show()
75
+ if (props.timeout) {
76
+ timeoutId.value = setTimeout(hide, props.timeout) as any
77
+ }
78
+ }
79
+ }
80
+
81
+ function onBlur() {
82
+ if (
83
+ props.trigger === 'focus'
84
+ || (props.trigger === 'both' && !el.value?.matches(':hover'))
85
+ ) {
86
+ hide()
87
+ }
88
+ }
22
89
  </script>
23
90
 
24
91
  <template>
25
- <component :is="tag ?? 'span'" class="STooltip" @mouseenter="show" @mouseleave="hide">
92
+ <component
93
+ ref="el"
94
+ :is="tag"
95
+ class="STooltip"
96
+ :class="rootClasses"
97
+ :tabindex="tabindex"
98
+ @mouseenter="onMouseEnter"
99
+ @mouseleave="onMouseLeave"
100
+ @focusin="onFocus"
101
+ @focusout="onBlur"
102
+ >
26
103
  <span class="content" ref="content">
27
104
  <slot />
28
105
  </span>
29
106
 
30
107
  <transition name="fade">
31
- <span v-show="on" class="container" :class="classes" ref="tip">
108
+ <span v-show="on" class="container" :class="containerClasses" ref="tip">
32
109
  <span v-if="$slots.text" class="tip"><slot name="text" /></span>
33
110
  <SMarkdown v-else-if="text" tag="span" class="tip" :content="text" inline />
34
111
  </span>
@@ -39,6 +116,18 @@ const { on, show, hide } = useTooltip(
39
116
  <style scoped lang="postcss">
40
117
  .STooltip {
41
118
  position: relative;
119
+
120
+ &:focus {
121
+ outline: none;
122
+ }
123
+
124
+ &.focusable {
125
+ cursor: pointer;
126
+ }
127
+
128
+ &.inline { display: inline; }
129
+ &.inline-block { display: inline-block; }
130
+ &.block { display: block; }
42
131
  }
43
132
 
44
133
  .content {
@@ -58,7 +147,7 @@ const { on, show, hide } = useTooltip(
58
147
  &.top .tip { transform: translateY(8px); }
59
148
  &.right .tip { transform: translateX(-8px); }
60
149
  &.bottom .tip { transform: translateY(-8px); }
61
- &.left .tip { transform: translateX(8px); }
150
+ &.left .tip { transform: translateX(8px); }
62
151
  }
63
152
 
64
153
  .container.top {
@@ -91,12 +180,11 @@ const { on, show, hide } = useTooltip(
91
180
  display: block;
92
181
  border: 1px solid var(--tooltip-border-color);
93
182
  border-radius: 6px;
94
- padding: 12px;
183
+ padding: 10px 12px;
95
184
  width: max-content;
96
185
  max-width: var(--tooltip-max-width);
97
- line-height: 18px;
186
+ line-height: 20px;
98
187
  font-size: 12px;
99
- font-weight: 500;
100
188
  color: var(--tooltip-text-color);
101
189
  background-color: var(--tooltip-bg-color);
102
190
  transition: transform 0.25s;
@@ -13,32 +13,32 @@ export interface UseGridOptions {
13
13
 
14
14
  type CssStyles = Partial<Record<keyof CSSStyleDeclaration, string>>
15
15
 
16
- export function useGrid(options: UseGridOptions): Grid {
16
+ export function useGrid(options: UseGridOptions = {}): Grid {
17
17
  const container: Ref<HTMLElement | null> = ref(null)
18
18
 
19
19
  const spacerClass = options.class ? toClassName(options.class) : 'spacer'
20
20
  const spacerTag = options.tag ?? 'div'
21
21
  const type = options.type ?? 'fit'
22
22
 
23
- const observer = new MutationObserver((_, observer) => {
24
- observer.disconnect()
25
-
26
- adjustSpacer()
27
-
28
- observer.observe(container.value!, { childList: true })
29
- })
23
+ let observer: MutationObserver | null = null
30
24
 
31
25
  watchEffect(() => {
32
- observer.disconnect()
26
+ observer?.disconnect()
33
27
 
34
28
  if (container.value) {
35
29
  adjustSpacer()
36
- observer.observe(container.value, { childList: true })
30
+ observer?.observe(container.value, { childList: true })
37
31
  }
38
32
  })
39
33
 
40
34
  onMounted(() => {
41
35
  window.addEventListener('resize', adjustSpacer)
36
+
37
+ observer = new MutationObserver((_, observer) => {
38
+ observer.disconnect()
39
+ adjustSpacer()
40
+ observer.observe(container.value!, { childList: true })
41
+ })
42
42
  })
43
43
 
44
44
  onUnmounted(() => {
@@ -1,29 +1,51 @@
1
- import type { Ref } from 'vue'
2
- import { ref } from 'vue'
1
+ import { type Ref, ref } from 'vue'
2
+
3
+ export interface Tooltip {
4
+ on: Ref<boolean>
5
+ show: () => void
6
+ hide: () => void
7
+ }
3
8
 
4
9
  export type Position = 'top' | 'right' | 'bottom' | 'left'
5
10
 
6
11
  const SCREEN_PADDING = 16
7
12
 
13
+ const globalHide = ref<() => void>()
14
+
8
15
  /**
9
16
  * Prevent tooltip going off-screen by adjusting the position depending on
10
17
  * the current window size. This only applies to position `top` and
11
18
  * `bottom` since we only care about left and right of the screen.
12
19
  */
13
20
  export function useTooltip(
21
+ el: Ref<HTMLElement | null>,
14
22
  content: Ref<HTMLElement | null>,
15
23
  tip: Ref<HTMLElement | null>,
16
- position: Ref<Position>
17
- ) {
24
+ position: Ref<Position>,
25
+ timeoutId: Ref<number | null>
26
+ ): Tooltip {
18
27
  const on = ref(false)
19
28
 
20
29
  function show(): void {
30
+ if (on.value) { return }
31
+ globalHide.value?.()
21
32
  setPosition()
22
33
  setTimeout(() => { on.value = true })
34
+ globalHide.value = hide
23
35
  }
24
36
 
25
37
  function hide(): void {
26
- setTimeout(() => { on.value = false })
38
+ if (!on.value) { return }
39
+ setTimeout(() => {
40
+ if (timeoutId.value) {
41
+ clearTimeout(timeoutId.value)
42
+ timeoutId.value = null
43
+ }
44
+ on.value = false
45
+ if (el.value?.matches(':focus-within')) {
46
+ (document.activeElement as HTMLElement)?.blur?.()
47
+ }
48
+ })
27
49
  }
28
50
 
29
51
  function setPosition(): void {
@@ -7,6 +7,22 @@
7
7
  * -------------------------------------------------------------------------- */
8
8
 
9
9
  :root {
10
+ /**
11
+ * DEPRECATED: These values are no longer used. Equivalent colors are defined
12
+ * directly in theme-aware color definitions.
13
+ */
14
+ --c-white-soft: #fafafa;
15
+ --c-white-mute: #f2f2f2;
16
+ --c-white-elv: #fafafa;
17
+ --c-white-elv-up: #ffffff;
18
+ --c-white-elv-down: #f2f2f2;
19
+ --c-black: #000000;
20
+ --c-black-soft: #171717;
21
+ --c-black-mute: #1c1c1e;
22
+ --c-black-elv: #171717;
23
+ --c-black-elv-up: #1c1c1e;
24
+ --c-black-elv-down: #000000;
25
+
10
26
  --c-white: #ffffff;
11
27
  --c-black: #000000;
12
28
 
@@ -135,22 +151,6 @@
135
151
  --c-danger-bg-lighter: var(--c-danger-lighter);
136
152
  --c-danger-bg-dark: var(--c-danger-dark);
137
153
  --c-danger-bg-darker: var(--c-danger-darker);
138
-
139
- /**
140
- * DEPRECATED: These values are no longer used. Equivalent colors are defined
141
- * directly in theme-aware color definitions.
142
- */
143
- --c-white-soft: #fafafa;
144
- --c-white-mute: #f2f2f2;
145
- --c-white-elv: #fafafa;
146
- --c-white-elv-up: #ffffff;
147
- --c-white-elv-down: #f2f2f2;
148
- --c-black: #000000;
149
- --c-black-soft: #171717;
150
- --c-black-mute: #1c1c1e;
151
- --c-black-elv: #171717;
152
- --c-black-elv-up: #1c1c1e;
153
- --c-black-elv-down: #000000;
154
154
  }
155
155
 
156
156
  /**
@@ -158,6 +158,23 @@
158
158
  * -------------------------------------------------------------------------- */
159
159
 
160
160
  :root {
161
+ /* DEPRECATED: Use `--c-bg-elv-{number}`. */
162
+ --c-bg: var(--c-white);
163
+ --c-bg-soft: var(--c-white-soft);
164
+ --c-bg-mute: var(--c-white-mute);
165
+ --c-bg-elv: var(--c-white-elv);
166
+ --c-bg-elv-up: var(--c-white-elv-up);
167
+ --c-bg-elv-down: var(--c-white-elv-down);
168
+ --c-bg-lift-1: var(--c-white-soft);
169
+ --c-bg-lift-2: var(--c-white-mute);
170
+
171
+ /* DEPRECATED: Use `--c-divider-x`. */
172
+ --c-divider: var(--c-divider-light-1);
173
+ --c-divider-light: var(--c-divider-light-2);
174
+
175
+ /* DEPRECATED: Use `--c-bg-soft`. */
176
+ --c-soft: var(--c-white-soft);
177
+
161
178
  --c-bg-elv-1: #ffffff;
162
179
  --c-bg-elv-2: #f9f9f9;
163
180
  --c-bg-elv-3: #ffffff;
@@ -193,26 +210,26 @@
193
210
  --c-mute-darker: #d1d1d1;
194
211
  --c-mute-dimm-1: #f1f1f1;
195
212
  --c-mute-dimm-2: #e3e3e3;
213
+ }
196
214
 
197
- /* DEPRECATED: Use `--c-bg-elv-{number}`. */
198
- --c-bg: var(--c-white);
199
- --c-bg-soft: var(--c-white-soft);
200
- --c-bg-mute: var(--c-white-mute);
201
- --c-bg-elv: var(--c-white-elv);
202
- --c-bg-elv-up: var(--c-white-elv-up);
203
- --c-bg-elv-down: var(--c-white-elv-down);
204
- --c-bg-lift-1: var(--c-white-soft);
205
- --c-bg-lift-2: var(--c-white-mute);
215
+ .dark {
216
+ /* DEPRECATED: Use `--c-bg-elv-x`. */
217
+ --c-bg-elv: var(--c-black-elv);
218
+ --c-bg-elv-up: var(--c-black-elv-up);
219
+ --c-bg-elv-down: var(--c-black-elv-down);
220
+ --c-bg: var(--c-black);
221
+ --c-bg-soft: var(--c-black-soft);
222
+ --c-bg-mute: var(--c-black-mute);
223
+ --c-bg-lift-1: #222226;
224
+ --c-bg-lift-2: #2c2c2e;
206
225
 
207
- /* DEPRECATED: Use `--c-divider-x`. */
208
- --c-divider: var(--c-divider-light-1);
209
- --c-divider-light: var(--c-divider-light-2);
226
+ /* DEPRECATED: Use `--c-divider-1` and `--c-divider-2` instead. */
227
+ --c-divider: var(--c-divider-dark-1);
228
+ --c-divider-light: var(--c-divider-dark-2);
210
229
 
211
230
  /* DEPRECATED: Use `--c-bg-soft`. */
212
- --c-soft: var(--c-white-soft);
213
- }
231
+ --c-soft: #222226;
214
232
 
215
- .dark {
216
233
  --c-bg-elv-1: #000000;
217
234
  --c-bg-elv-2: #171717;
218
235
  --c-bg-elv-3: #1c1c1e;
@@ -248,23 +265,6 @@
248
265
  --c-mute-darker: #1c1c1e;
249
266
  --c-mute-dimm-1: #222226;
250
267
  --c-mute-dimm-2: #2c2c2e;
251
-
252
- /* DEPRECATED: Use `--c-bg-elv-x`. */
253
- --c-bg-elv: var(--c-black-elv);
254
- --c-bg-elv-up: var(--c-black-elv-up);
255
- --c-bg-elv-down: var(--c-black-elv-down);
256
- --c-bg: var(--c-black);
257
- --c-bg-soft: var(--c-black-soft);
258
- --c-bg-mute: var(--c-black-mute);
259
- --c-bg-lift-1: #222226;
260
- --c-bg-lift-2: #2c2c2e;
261
-
262
- /* DEPRECATED: Use `--c-divider-1` and `--c-divider-2` instead. */
263
- --c-divider: var(--c-divider-dark-1);
264
- --c-divider-light: var(--c-divider-dark-2);
265
-
266
- /* DEPRECATED: Use `--c-bg-soft`. */
267
- --c-soft: #222226;
268
268
  }
269
269
 
270
270
  /**
@@ -658,7 +658,7 @@
658
658
  --tooltip-max-width: 288px;
659
659
  --tooltip-border-color: var(--c-divider-2);
660
660
  --tooltip-text-color: var(--c-text-1);
661
- --tooltip-bg-color: var(--c-bg-elv-3);
661
+ --tooltip-bg-color: var(--c-bg-soft);
662
662
  }
663
663
 
664
664
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "2.32.0",
4
- "packageManager": "pnpm@7.26.2",
3
+ "version": "2.33.0",
4
+ "packageManager": "pnpm@8.3.1",
5
5
  "description": "Vue Components for Global Brain Design System.",
6
6
  "author": "Kia Ishii <ka.ishii@globalbrains.com>",
7
7
  "license": "MIT",
@@ -68,8 +68,8 @@
68
68
  "eslint": "^8.35.0",
69
69
  "execa": "^5.1.1",
70
70
  "fuse.js": "^6.6.2",
71
+ "happy-dom": "^9.9.2",
71
72
  "histoire": "^0.15.8",
72
- "jsdom": "^21.1.1",
73
73
  "lodash-es": "^4.17.21",
74
74
  "markdown-it": "^13.0.1",
75
75
  "normalize.css": "^8.0.1",