@globalbrain/sefirot 3.29.2 → 3.31.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,9 @@
1
1
  <script setup lang="ts">
2
2
  import { type IconifyIcon } from '@iconify/vue/dist/offline'
3
- import { computed } from 'vue'
3
+ import { computed, ref } from 'vue'
4
4
  import { type Validatable } from '../composables/V'
5
5
  import SInputBase from './SInputBase.vue'
6
+ import SInputSegments from './SInputSegments.vue'
6
7
 
7
8
  export type Size = 'mini' | 'small' | 'medium'
8
9
  export type Color = 'neutral' | 'mute' | 'info' | 'success' | 'warning' | 'danger'
@@ -24,6 +25,9 @@ const props = defineProps<{
24
25
  modelValue?: string | null
25
26
  hideError?: boolean
26
27
  validation?: Validatable
28
+ preview?: (value: string | null) => string
29
+ previewLabel?: string
30
+ writeLabel?: string
27
31
  }>()
28
32
 
29
33
  const emit = defineEmits<{
@@ -57,6 +61,8 @@ function emitBlur(e: FocusEvent): void {
57
61
  emit('update:model-value', v)
58
62
  emit('blur', v)
59
63
  }
64
+
65
+ const isPreview = ref(false)
60
66
  </script>
61
67
 
62
68
  <template>
@@ -74,24 +80,96 @@ function emitBlur(e: FocusEvent): void {
74
80
  :hide-error="hideError"
75
81
  :validation="validation"
76
82
  >
77
- <textarea
78
- :id="name"
79
- class="input"
80
- :class="{ fill: rows === 'fill' }"
81
- :placeholder="placeholder"
82
- :rows="rows ?? 3"
83
- :disabled="disabled"
84
- :value="_value ?? undefined"
85
- @input="emitInput"
86
- @blur="emitBlur"
87
- />
83
+ <div class="box">
84
+ <div v-if="preview !== undefined" class="control">
85
+ <SInputSegments
86
+ :options="[
87
+ { label: writeLabel ?? 'Write', value: false },
88
+ { label: previewLabel ?? 'Preview', value: true }
89
+ ]"
90
+ v-model="isPreview"
91
+ size="mini"
92
+ />
93
+ </div>
94
+ <textarea
95
+ v-show="!isPreview"
96
+ :id="name"
97
+ class="input"
98
+ :placeholder="placeholder"
99
+ :rows="rows ?? 3"
100
+ :disabled="disabled"
101
+ :value="_value ?? undefined"
102
+ @input="emitInput"
103
+ @blur="emitBlur"
104
+ />
105
+ <div
106
+ v-if="preview !== undefined && isPreview"
107
+ class="prose"
108
+ :class="!_value && 'empty'"
109
+ v-html="preview(_value)"
110
+ />
111
+ </div>
88
112
  <template v-if="$slots.info" #info><slot name="info" /></template>
89
113
  </SInputBase>
90
114
  </template>
91
115
 
92
116
  <style scoped lang="postcss">
117
+ .box {
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: 1px;
121
+ flex-grow: 1;
122
+ border: 1px solid var(--input-border-color);
123
+ border-radius: 6px;
124
+ width: 100%;
125
+ background-color: var(--c-gutter);
126
+ overflow: hidden;
127
+ transition: border-color 0.25s;
128
+
129
+ &:has(.input:hover) {
130
+ border-color: var(--input-hover-border-color);
131
+ }
132
+
133
+ &:has(.input:focus) {
134
+ border-color: var(--input-focus-border-color);
135
+ }
136
+ }
137
+
138
+ .control {
139
+ display: flex;
140
+ align-items: center;
141
+ padding: 0 8px;
142
+ height: 48px;
143
+ background-color: var(--c-bg-elv-3);
144
+ }
145
+
146
+ .input,
147
+ .prose {
148
+ display: block;
149
+ flex-grow: 1;
150
+ width: 100%;
151
+ font-family: var(--input-value-font-family);
152
+ font-weight: 400;
153
+ background-color: var(--input-bg-color);
154
+ }
155
+
156
+ .input {
157
+ &::placeholder {
158
+ color: var(--input-placeholder-color);
159
+ }
160
+ }
161
+
162
+ .prose {
163
+ background-color: var(--c-bg-elv-3);
164
+
165
+ &.empty {
166
+ color: var(--input-placeholder-color);
167
+ }
168
+ }
169
+
93
170
  .SInputTextarea.mini {
94
- .input {
171
+ .input,
172
+ .prose {
95
173
  padding: 6px 10px;
96
174
  width: 100%;
97
175
  min-height: 80px;
@@ -101,7 +179,8 @@ function emitBlur(e: FocusEvent): void {
101
179
  }
102
180
 
103
181
  .SInputTextarea.small {
104
- .input {
182
+ .input,
183
+ .prose {
105
184
  padding: 7px 12px;
106
185
  width: 100%;
107
186
  min-height: 96px;
@@ -111,7 +190,8 @@ function emitBlur(e: FocusEvent): void {
111
190
  }
112
191
 
113
192
  .SInputTextarea.medium {
114
- .input {
193
+ .input,
194
+ .prose {
115
195
  padding: 11px 16px;
116
196
  width: 100%;
117
197
  min-height: 96px;
@@ -120,10 +200,24 @@ function emitBlur(e: FocusEvent): void {
120
200
  }
121
201
  }
122
202
 
123
- .SInputTextarea.disabled {
203
+ .SInputTextarea.fill {
204
+ display: flex;
205
+ flex-direction: column;
206
+ flex-grow: 1;
207
+ height: 100%;
208
+
124
209
  .input,
125
- .input:hover {
210
+ .prose {
211
+ height: 100%;
212
+ }
213
+ }
214
+
215
+ .SInputTextarea.disabled {
216
+ .box {
126
217
  border-color: var(--input-disabled-border-color);
218
+ }
219
+
220
+ .input {
127
221
  color: var(--input-disabled-value-color);
128
222
  background-color: var(--input-disabled-bg-color);
129
223
  cursor: not-allowed;
@@ -131,42 +225,8 @@ function emitBlur(e: FocusEvent): void {
131
225
  }
132
226
 
133
227
  .SInputTextarea.has-error {
134
- .input {
228
+ .box {
135
229
  border-color: var(--input-error-border-color);
136
-
137
- &:hover,
138
- &:focus {
139
- border-color: var(--input-error-border-color);
140
- }
141
- }
142
- }
143
-
144
- .input {
145
- display: block;
146
- flex-grow: 1;
147
- border: 1px solid var(--input-border-color);
148
- border-radius: 6px;
149
- width: 100%;
150
- font-family: var(--input-value-font-family);
151
- font-weight: 400;
152
- background-color: var(--input-bg-color);
153
- transition: border-color 0.25s, background-color 0.25s;
154
-
155
- &::placeholder {
156
- color: var(--input-placeholder-color);
157
- }
158
-
159
- &:hover {
160
- border-color: var(--input-hover-border-color);
161
- }
162
-
163
- &:focus,
164
- &:hover:focus {
165
- border-color: var(--input-focus-border-color);
166
- }
167
-
168
- &.fill {
169
- height: 100%;
170
230
  }
171
231
  }
172
232
  </style>
@@ -12,6 +12,7 @@ export interface UseMarkdownOptions extends MarkdownIt.Options {
12
12
 
13
13
  export function useMarkdown(options: UseMarkdownOptions = {}): UseMarkdown {
14
14
  const md = new MarkdownIt({
15
+ html: true,
15
16
  linkify: true,
16
17
  ...options
17
18
  })
@@ -1,128 +1,141 @@
1
1
  import isEqual from 'lodash-es/isEqual'
2
- import isPlainObject from 'lodash-es/isPlainObject'
3
- import { type MaybeRef, unref, watch } from 'vue'
4
- import { useRoute, useRouter } from 'vue-router'
2
+ import { type MaybeRef, nextTick, unref, watch } from 'vue'
3
+ import { type LocationQuery, useRoute, useRouter } from 'vue-router'
5
4
 
6
5
  export interface UseUrlQuerySyncOptions {
7
6
  casts?: Record<string, (value: any) => any>
8
7
  exclude?: string[]
9
8
  }
10
9
 
10
+ /**
11
+ * Sync between the given state and the URL query params.
12
+ *
13
+ * Caveats:
14
+ * - Vulnerable to prototype pollution.
15
+ * - Does not support objects inside arrays.
16
+ */
11
17
  export function useUrlQuerySync(
12
18
  state: MaybeRef<Record<string, any>>,
13
- { casts = {}, exclude }: UseUrlQuerySyncOptions = {}
19
+ { casts = {}, exclude = [] }: UseUrlQuerySyncOptions = {}
14
20
  ): void {
15
- const router = useRouter()
16
21
  const route = useRoute()
22
+ const router = useRouter()
17
23
 
18
- const flattenInitialState = flattenObject(
19
- JSON.parse(JSON.stringify(unref(state)))
20
- )
21
-
22
- setStateFromQuery()
23
-
24
- watch(() => unref(state), setQueryFromState, {
25
- deep: true,
26
- immediate: true
27
- })
24
+ const flattenedDefaultState = flattenObject(unref(state))
28
25
 
29
- function setStateFromQuery() {
30
- const flattenState = flattenObject(unref(state))
31
- const flattenQuery = flattenObject(route.query)
26
+ let isSyncing = false
32
27
 
33
- Object.keys(flattenQuery).forEach((key) => {
34
- if (exclude?.includes(key)) {
35
- return
28
+ watch(
29
+ () => route.query,
30
+ async () => {
31
+ if (!isSyncing) {
32
+ isSyncing = true
33
+ await setState()
34
+ isSyncing = false
36
35
  }
36
+ },
37
+ { deep: true, immediate: true }
38
+ )
37
39
 
38
- const value = flattenQuery[key]
39
- if (value === undefined) {
40
- return
40
+ watch(
41
+ () => unref(state),
42
+ async () => {
43
+ if (!isSyncing) {
44
+ isSyncing = true
45
+ await setQuery()
46
+ isSyncing = false
41
47
  }
48
+ },
49
+ { deep: true }
50
+ )
42
51
 
43
- const cast = casts[key]
44
- flattenState[key] = cast ? cast(value) : value
45
- })
52
+ async function setState() {
53
+ const newState = unflattenObject({ ...flattenedDefaultState, ...normalizeQuery(route.query) })
54
+ deepAssign(unref(state), newState)
46
55
 
47
- deepAssign(unref(state), unflattenObject(flattenState))
56
+ await nextTick()
57
+ await setQuery()
48
58
  }
49
59
 
50
- async function setQueryFromState() {
51
- const flattenState = flattenObject(unref(state))
52
- const flattenQuery = flattenObject(route.query)
60
+ async function setQuery() {
61
+ const flattenedState = flattenObject(unref(state))
62
+ const newQuery: Record<string, any> = {}
53
63
 
54
- Object.keys(flattenState).forEach((key) => {
55
- if (exclude?.includes(key)) {
56
- return
64
+ for (const key in flattenedState) {
65
+ if (!exclude.includes(key) && flattenedDefaultState[key] !== flattenedState[key]) {
66
+ newQuery[key] = flattenedState[key]
57
67
  }
68
+ }
58
69
 
59
- const value = flattenState[key]
60
- const initialValue = flattenInitialState[key]
70
+ const currentQuery = normalizeQuery(route.query)
61
71
 
62
- if (isEqual(value, initialValue)) {
63
- delete flattenQuery[key]
64
- } else {
65
- flattenQuery[key] = value
66
- }
72
+ if (!isEqual(newQuery, currentQuery)) {
73
+ await router.replace({ query: unflattenObject(newQuery) })
74
+ }
75
+ }
76
+
77
+ function normalizeQuery(query: LocationQuery): Record<string, any> {
78
+ const flattenedQuery = flattenObject(query)
79
+ const result: Record<string, any> = {}
67
80
 
68
- if (flattenQuery[key] === undefined) {
69
- delete flattenQuery[key]
81
+ for (const key in flattenedQuery) {
82
+ if (!exclude.includes(key)) {
83
+ result[key] = casts[key] ? casts[key](flattenedQuery[key]) : flattenedQuery[key]
70
84
  }
71
- })
85
+ }
72
86
 
73
- await router.replace({ query: unflattenObject(flattenQuery) })
87
+ return result
74
88
  }
75
89
  }
76
90
 
77
- function flattenObject(obj: Record<string, any>, prefix = '') {
78
- return Object.keys(obj).reduce((acc, k) => {
79
- const pre = prefix.length ? `${prefix}.` : ''
80
- if (isPlainObject(obj[k])) {
81
- Object.assign(acc, flattenObject(obj[k], pre + k))
91
+ function flattenObject(obj: Record<string, any>, path: string[] = []): Record<string, any> {
92
+ const result: Record<string, any> = {}
93
+
94
+ for (const key in obj) {
95
+ const value = obj[key]
96
+
97
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
98
+ Object.assign(result, flattenObject(value, [...path, key]))
82
99
  } else {
83
- acc[pre + k] = obj[k]
100
+ result[path.concat(key).join('.')] = value
84
101
  }
85
- return acc
86
- }, {} as Record<string, any>)
87
- }
102
+ }
88
103
 
89
- function unflattenObject(obj: Record<string, any>) {
90
- return Object.keys(obj).reduce((acc, k) => {
91
- const keys = k.split('.')
92
- keys.reduce((a, c, i) => {
93
- if (i === keys.length - 1) {
94
- a[c] = obj[k]
95
- } else {
96
- a[c] = a[c] || {}
97
- }
98
- return a[c]
99
- }, acc)
100
- return acc
101
- }, {} as Record<string, any>)
104
+ return result
102
105
  }
103
106
 
104
- function deepAssign(target: Record<string, any>, source: Record<string, any>) {
105
- const dest = target
106
- const src = source
107
-
108
- if (isPlainObject(src)) {
109
- Object.keys(src).forEach((key) => deepAssignBase(dest, src, key))
110
- } else if (Array.isArray(src)) {
111
- dest.length = src.length
112
- src.forEach((_, key) => deepAssignBase(dest, src, key))
113
- } else {
114
- throw new TypeError('[deepAssign] src must be an object or array')
107
+ function unflattenObject(obj: Record<string, any>): Record<string, any> {
108
+ const result: Record<string, any> = {}
109
+
110
+ for (const key in obj) {
111
+ const value = obj[key]
112
+
113
+ let target = result
114
+ const keys = key.split('.')
115
+
116
+ for (let i = 0; i < keys.length - 1; i++) {
117
+ const k = keys[i]
118
+ target = target[k] = target[k] || {}
119
+ }
120
+
121
+ target[keys[keys.length - 1]] = value
115
122
  }
123
+
124
+ return result
116
125
  }
117
126
 
118
- function deepAssignBase(
119
- dest: Record<string, any>,
120
- src: Record<string, any>,
121
- key: string | number
122
- ) {
123
- if (typeof src[key] === 'object' && src[key] !== null) {
124
- deepAssign(dest[key], src[key])
125
- } else {
126
- dest[key] = src[key]
127
+ function deepAssign(target: Record<string, any>, source: Record<string, any>) {
128
+ for (const key in source) {
129
+ const value = source[key]
130
+
131
+ if (Array.isArray(value)) {
132
+ target[key].splice(0, target[key].length, ...value)
133
+ } else if (value && typeof value === 'object') {
134
+ target[key] = deepAssign(target[key] || {}, value)
135
+ } else {
136
+ target[key] = value
137
+ }
127
138
  }
139
+
140
+ return target
128
141
  }
@@ -1,6 +1,6 @@
1
1
  @import "normalize.css";
2
2
  @import "v-calendar/dist/style.css";
3
- @import "./variables-deprecated";
4
- @import "./variables";
5
- @import "./base";
6
- @import "./utilities";
3
+ @import "./variables-deprecated.css";
4
+ @import "./variables.css";
5
+ @import "./base.css";
6
+ @import "./utilities.css";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "3.29.2",
4
- "packageManager": "pnpm@8.15.1",
3
+ "version": "3.31.0",
4
+ "packageManager": "pnpm@8.15.3",
5
5
  "description": "Vue Components for Global Brain Design System.",
6
6
  "author": "Kia Ishii <ka.ishii@globalbrains.com>",
7
7
  "license": "MIT",
@@ -53,10 +53,10 @@
53
53
  "markdown-it": "^14.0.0",
54
54
  "normalize.css": "^8.0.1",
55
55
  "pinia": "^2.1.7",
56
- "postcss": "^8.4.34",
56
+ "postcss": "^8.4.35",
57
57
  "postcss-nested": "^6.0.1",
58
58
  "v-calendar": "^3.1.2",
59
- "vue": "^3.4.15",
59
+ "vue": "^3.4.19",
60
60
  "vue-router": "^4.2.5"
61
61
  },
62
62
  "dependencies": {
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "devDependencies": {
74
74
  "@globalbrain/eslint-config": "^1.5.2",
75
- "@histoire/plugin-vue": "^0.17.9",
75
+ "@histoire/plugin-vue": "0.17.9",
76
76
  "@iconify-icons/ph": "^1.2.5",
77
77
  "@iconify-icons/ri": "^1.2.10",
78
78
  "@iconify/vue": "^4.1.1",
@@ -80,9 +80,9 @@
80
80
  "@types/body-scroll-lock": "^3.1.2",
81
81
  "@types/lodash-es": "^4.17.12",
82
82
  "@types/markdown-it": "^13.0.7",
83
- "@types/node": "^20.11.16",
84
- "@vitejs/plugin-vue": "^5.0.3",
85
- "@vitest/coverage-v8": "^1.2.2",
83
+ "@types/node": "^20.11.19",
84
+ "@vitejs/plugin-vue": "^5.0.4",
85
+ "@vitest/coverage-v8": "^1.3.0",
86
86
  "@vue/test-utils": "^2.4.4",
87
87
  "@vuelidate/core": "^2.0.3",
88
88
  "@vuelidate/validators": "^2.0.4",
@@ -91,21 +91,21 @@
91
91
  "eslint": "^8.56.0",
92
92
  "fuse.js": "^7.0.0",
93
93
  "happy-dom": "^13.3.8",
94
- "histoire": "^0.17.9",
94
+ "histoire": "0.17.9",
95
95
  "lodash-es": "^4.17.21",
96
96
  "markdown-it": "^14.0.0",
97
97
  "normalize.css": "^8.0.1",
98
98
  "pinia": "^2.1.7",
99
- "postcss": "^8.4.34",
99
+ "postcss": "^8.4.35",
100
100
  "postcss-nested": "^6.0.1",
101
101
  "punycode": "^2.3.1",
102
- "release-it": "^17.0.3",
102
+ "release-it": "^17.1.1",
103
103
  "typescript": "~5.3.3",
104
104
  "v-calendar": "^3.1.2",
105
- "vite": "^5.0.12",
106
- "vitepress": "1.0.0-rc.41",
107
- "vitest": "^1.2.2",
108
- "vue": "^3.4.15",
105
+ "vite": "^5.1.3",
106
+ "vitepress": "1.0.0-rc.44",
107
+ "vitest": "^1.3.0",
108
+ "vue": "^3.4.19",
109
109
  "vue-router": "^4.2.5",
110
110
  "vue-tsc": "^1.8.27"
111
111
  }