frappe-ui 1.0.0-beta.21 → 1.0.0-beta.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.
- package/package.json +2 -1
- package/src/components/Avatar/Avatar.api.md +7 -1
- package/src/components/Avatar/Avatar.cy.ts +21 -0
- package/src/components/Avatar/Avatar.md +5 -2
- package/src/components/Avatar/Avatar.vue +20 -6
- package/src/components/Avatar/index.ts +1 -1
- package/src/components/Avatar/stories/Themes.vue +23 -0
- package/src/components/Avatar/types.ts +11 -0
- package/src/components/Calendar/CalendarMonthEvent.vue +2 -2
- package/src/components/Calendar/CalendarMonthly.vue +10 -9
- package/src/components/Calendar/CalendarWeekly.vue +1 -1
- package/src/components/Calendar/calendarUtils.ts +71 -149
- package/src/components/Calendar/stories/CustomHeader.vue +8 -6
- package/src/components/Calendar/stories/Examples.vue +7 -5
- package/src/components/Calendar/style.css +1 -1
- package/src/components/Calendar/useEventBase.ts +12 -0
- package/src/molecules/editor/Editor.vue +6 -2
- package/src/molecules/editor/extensions.ts +5 -0
- package/src/molecules/editor/useEditor.test.ts +72 -0
- package/src/molecules/editor/useEditor.ts +31 -4
- package/vitepress/components/Docs/Sidebar.vue +3 -3
- package/vitepress/components/Navbar.vue +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.24",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -172,6 +172,7 @@
|
|
|
172
172
|
"@tiptap/extension-typography": "^3.26.0",
|
|
173
173
|
"@tiptap/extension-underline": "^3.26.0",
|
|
174
174
|
"@tiptap/extensions": "^3.26.0",
|
|
175
|
+
"@tiptap/markdown": "^3.26.0",
|
|
175
176
|
"@tiptap/pm": "^3.26.0",
|
|
176
177
|
"@tiptap/starter-kit": "^3.26.0",
|
|
177
178
|
"@tiptap/suggestion": "^3.26.0",
|
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
required: false,
|
|
31
31
|
type: '"circle" | "square"',
|
|
32
32
|
default: '"circle"'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'theme',
|
|
36
|
+
description: 'Visual color theme used for the fallback avatar',
|
|
37
|
+
required: false,
|
|
38
|
+
type: '"gray" | "blue" | "green" | "amber" | "red" | "violet"',
|
|
39
|
+
default: '"gray"'
|
|
33
40
|
}
|
|
34
41
|
]
|
|
35
42
|
|
|
@@ -51,4 +58,3 @@
|
|
|
51
58
|
<PropsTable name="Avatar" :data="propsData"/>
|
|
52
59
|
|
|
53
60
|
<SlotsTable :data="slotsData"/>
|
|
54
|
-
|
|
@@ -44,4 +44,25 @@ describe('Avatar', () => {
|
|
|
44
44
|
|
|
45
45
|
cy.get('[data-cy="avatar"]').should('have.text', 'A')
|
|
46
46
|
})
|
|
47
|
+
|
|
48
|
+
it('Uses gray fallback theme by default', () => {
|
|
49
|
+
cy.mount(Avatar, {
|
|
50
|
+
props: { 'data-cy': 'avatar', label: 'Abc' },
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
cy.get('[data-cy="avatar"] > div')
|
|
54
|
+
.should('have.class', 'bg-surface-gray-2')
|
|
55
|
+
.and('have.class', 'text-ink-gray-5')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('Supports colorful fallback themes', () => {
|
|
59
|
+
cy.mount(Avatar, {
|
|
60
|
+
props: { 'data-cy': 'avatar', label: 'Abc', theme: 'blue' },
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
cy.get('[data-cy="avatar"] > div')
|
|
64
|
+
.should('have.class', 'bg-surface-blue-2')
|
|
65
|
+
.and('have.class', 'text-ink-blue-8')
|
|
66
|
+
})
|
|
67
|
+
|
|
47
68
|
})
|
|
@@ -6,10 +6,13 @@ A visual representation of a user, typically shown as an image, initials, or an
|
|
|
6
6
|
|
|
7
7
|
<ClientOnly><AvatarBuilder /></ClientOnly>
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Shapes
|
|
10
10
|
<ComponentPreview name="Avatar-Shapes" />
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Sizes
|
|
13
13
|
<ComponentPreview name="Avatar-Sizes" />
|
|
14
14
|
|
|
15
|
+
## Themes
|
|
16
|
+
<ComponentPreview name="Avatar-Themes" />
|
|
17
|
+
|
|
15
18
|
<!-- @include: ./Avatar.api.md -->
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
/>
|
|
13
13
|
<div
|
|
14
14
|
v-else
|
|
15
|
-
class="flex h-full w-full items-center justify-center
|
|
16
|
-
:class="[labelClasses, shapeClasses]"
|
|
15
|
+
class="flex h-full w-full items-center justify-center uppercase select-none"
|
|
16
|
+
:class="[labelClasses, fallbackThemeClasses, shapeClasses]"
|
|
17
17
|
>
|
|
18
18
|
<div :class="iconClasses" v-if="$slots.default">
|
|
19
19
|
<slot></slot>
|
|
@@ -38,13 +38,26 @@
|
|
|
38
38
|
|
|
39
39
|
<script setup lang="ts">
|
|
40
40
|
import { ref, computed, useAttrs } from 'vue'
|
|
41
|
-
import type { AvatarProps } from './types'
|
|
41
|
+
import type { AvatarProps, AvatarTheme } from './types'
|
|
42
42
|
|
|
43
43
|
const imgFetchError = ref(false)
|
|
44
44
|
|
|
45
45
|
const props = withDefaults(defineProps<AvatarProps>(), {
|
|
46
46
|
size: 'md',
|
|
47
47
|
shape: 'circle',
|
|
48
|
+
theme: 'gray',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const fallbackThemeClasses = computed(() => {
|
|
52
|
+
const classes: Record<AvatarTheme, string> = {
|
|
53
|
+
gray: 'bg-surface-gray-2 text-ink-gray-5',
|
|
54
|
+
blue: 'bg-surface-blue-2 text-ink-blue-8',
|
|
55
|
+
green: 'bg-surface-green-2 text-ink-green-8',
|
|
56
|
+
amber: 'bg-surface-amber-2 text-ink-amber-8',
|
|
57
|
+
red: 'bg-surface-red-2 text-ink-red-8',
|
|
58
|
+
violet: 'bg-surface-violet-2 text-ink-violet-8',
|
|
59
|
+
}
|
|
60
|
+
return classes[props.theme]
|
|
48
61
|
})
|
|
49
62
|
|
|
50
63
|
// Let a consumer size the avatar with a Tailwind utility (`class="size-16"`)
|
|
@@ -60,7 +73,9 @@ const hasSizeOverride = computed(() => {
|
|
|
60
73
|
? attrs.class
|
|
61
74
|
: ''
|
|
62
75
|
return cls.split(/\s+/).some((token) => {
|
|
63
|
-
const base = token.includes(':')
|
|
76
|
+
const base = token.includes(':')
|
|
77
|
+
? token.slice(token.lastIndexOf(':') + 1)
|
|
78
|
+
: token
|
|
64
79
|
return /^-?(size|w|h|min-w|max-w|min-h|max-h)-/.test(base)
|
|
65
80
|
})
|
|
66
81
|
})
|
|
@@ -142,7 +157,7 @@ const iconClasses = computed(() => {
|
|
|
142
157
|
}[props.size]
|
|
143
158
|
})
|
|
144
159
|
|
|
145
|
-
function handleImageError(err) {
|
|
160
|
+
function handleImageError(err: Event) {
|
|
146
161
|
if (err.type) {
|
|
147
162
|
imgFetchError.value = true
|
|
148
163
|
}
|
|
@@ -155,5 +170,4 @@ defineSlots<{
|
|
|
155
170
|
/** Small indicator shown at the bottom-right of the avatar */
|
|
156
171
|
indicator?: () => any
|
|
157
172
|
}>()
|
|
158
|
-
|
|
159
173
|
</script>
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Avatar } from './Avatar.vue'
|
|
2
|
-
export type { AvatarProps } from './types'
|
|
2
|
+
export type { AvatarProps, AvatarTheme } from './types'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Avatar } from 'frappe-ui'
|
|
3
|
+
import type { AvatarTheme } from 'frappe-ui'
|
|
4
|
+
|
|
5
|
+
const themes: AvatarTheme[] = [
|
|
6
|
+
'gray',
|
|
7
|
+
'blue',
|
|
8
|
+
'green',
|
|
9
|
+
'amber',
|
|
10
|
+
'red',
|
|
11
|
+
'violet',
|
|
12
|
+
]
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<Avatar
|
|
17
|
+
v-for="theme in themes"
|
|
18
|
+
:key="theme"
|
|
19
|
+
:label="theme"
|
|
20
|
+
:theme="theme"
|
|
21
|
+
size="lg"
|
|
22
|
+
/>
|
|
23
|
+
</template>
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export type AvatarTheme =
|
|
2
|
+
| 'gray'
|
|
3
|
+
| 'blue'
|
|
4
|
+
| 'green'
|
|
5
|
+
| 'amber'
|
|
6
|
+
| 'red'
|
|
7
|
+
| 'violet'
|
|
8
|
+
|
|
1
9
|
export interface AvatarProps {
|
|
2
10
|
/** Image URL used for the avatar */
|
|
3
11
|
image?: string
|
|
@@ -10,4 +18,7 @@ export interface AvatarProps {
|
|
|
10
18
|
|
|
11
19
|
/** Defines the avatar shape */
|
|
12
20
|
shape?: 'circle' | 'square'
|
|
21
|
+
|
|
22
|
+
/** Visual color theme used for the fallback avatar */
|
|
23
|
+
theme?: AvatarTheme
|
|
13
24
|
}
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
class="relative flex h-full select-none items-start gap-2 overflow-hidden"
|
|
26
26
|
>
|
|
27
27
|
<div v-if="config.showIcon && eventIcon">
|
|
28
|
-
<component :is="eventIcon" class="h-4 w-4 text-
|
|
28
|
+
<component :is="eventIcon" class="h-4 w-4 text-ink-gray-8" />
|
|
29
29
|
</div>
|
|
30
30
|
<p
|
|
31
|
-
class="text-sm-medium truncate"
|
|
31
|
+
class="text-sm-medium truncate text-ink-gray-8"
|
|
32
32
|
:class="{ italic: !props.event.title }"
|
|
33
33
|
>
|
|
34
34
|
{{ props.event.title || '[No title]' }}
|
|
@@ -37,20 +37,17 @@
|
|
|
37
37
|
>
|
|
38
38
|
<span
|
|
39
39
|
class="w-full flex justify-between items-center"
|
|
40
|
-
:class="[
|
|
41
|
-
date.toDateString() === new Date().toDateString()
|
|
42
|
-
? 'p-[3px] pb-0.5'
|
|
43
|
-
: 'p-2',
|
|
44
|
-
]"
|
|
40
|
+
:class="[isToday(date) ? 'p-[3px] pb-0.5' : 'p-2']"
|
|
45
41
|
>
|
|
46
42
|
<div></div>
|
|
47
43
|
<div
|
|
48
44
|
class="cursor-pointer"
|
|
49
45
|
:class="[
|
|
50
|
-
date
|
|
51
|
-
? '
|
|
52
|
-
:
|
|
53
|
-
|
|
46
|
+
!isCurrentMonth(date)
|
|
47
|
+
? 'bg-surface-base text-ink-gray-4'
|
|
48
|
+
: isToday(date)
|
|
49
|
+
? 'flex items-center justify-center bg-surface-gray-10 text-ink-gray-2 rounded size-[25px]'
|
|
50
|
+
: 'bg-surface-base text-ink-gray-8',
|
|
54
51
|
]"
|
|
55
52
|
@click.stop="
|
|
56
53
|
isCurrentMonth(date)
|
|
@@ -145,6 +142,10 @@ const maxEventsInCell = computed(() =>
|
|
|
145
142
|
props.currentMonthDates.length > 35 ? 1 : 2,
|
|
146
143
|
)
|
|
147
144
|
|
|
145
|
+
function isToday(date: Date) {
|
|
146
|
+
return date.toDateString() === new Date().toDateString()
|
|
147
|
+
}
|
|
148
|
+
|
|
148
149
|
function isCurrentMonth(date: Date) {
|
|
149
150
|
return date.getMonth() === props.currentMonth
|
|
150
151
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{{ isToday(date) ? daysList[date.getDay()] : parseDateWithDay(date) }}
|
|
13
13
|
<span
|
|
14
14
|
v-if="isToday(date)"
|
|
15
|
-
class="inline-flex items-center justify-center bg-surface-gray-10 text-ink-
|
|
15
|
+
class="inline-flex items-center justify-center bg-surface-gray-10 text-ink-gray-1 rounded size-[25px]"
|
|
16
16
|
>
|
|
17
17
|
{{ date.getDate() }}
|
|
18
18
|
</span>
|
|
@@ -304,170 +304,92 @@ export function formatTime(time: string, format: CalendarTimeFormat): string {
|
|
|
304
304
|
|
|
305
305
|
export const colorMap: Record<string, CalendarColor> = {
|
|
306
306
|
amber: {
|
|
307
|
-
color: '
|
|
308
|
-
border: '
|
|
309
|
-
borderActive: '
|
|
310
|
-
text: '
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
307
|
+
color: 'var(--ink-amber-7)',
|
|
308
|
+
border: 'var(--ink-amber-7)',
|
|
309
|
+
borderActive: 'var(--outline-amber-2)',
|
|
310
|
+
text: 'var(--ink-amber-7)',
|
|
311
|
+
textActive: 'var(--ink-amber-1)',
|
|
312
|
+
subtext: 'var(--ink-gray-6)',
|
|
313
|
+
subtextActive: 'var(--ink-amber-1)',
|
|
314
|
+
bg: 'var(--surface-amber-1)',
|
|
315
|
+
bgHover: 'var(--surface-amber-1)',
|
|
316
|
+
bgActive: 'var(--surface-amber-2)',
|
|
316
317
|
},
|
|
317
318
|
violet: {
|
|
318
|
-
color: '
|
|
319
|
-
border: '
|
|
320
|
-
borderActive: '
|
|
321
|
-
text: '
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
319
|
+
color: 'var(--ink-violet-7)',
|
|
320
|
+
border: 'var(--ink-violet-7)',
|
|
321
|
+
borderActive: 'var(--outline-gray-4)',
|
|
322
|
+
text: 'var(--ink-violet-7)',
|
|
323
|
+
textActive: 'var(--ink-violet-1)',
|
|
324
|
+
subtext: 'var(--ink-gray-6)',
|
|
325
|
+
subtextActive: 'var(--ink-base)',
|
|
326
|
+
bg: 'var(--surface-violet-1)',
|
|
327
|
+
bgHover: 'var(--surface-violet-1)',
|
|
328
|
+
bgActive: 'var(--surface-violet-7)',
|
|
327
329
|
},
|
|
328
330
|
pink: {
|
|
329
|
-
color: '
|
|
330
|
-
border: '
|
|
331
|
-
borderActive: '
|
|
332
|
-
text: '
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
331
|
+
color: 'var(--ink-pink-7)',
|
|
332
|
+
border: 'var(--ink-pink-7)',
|
|
333
|
+
borderActive: 'var(--outline-gray-4)',
|
|
334
|
+
text: 'var(--ink-pink-7)',
|
|
335
|
+
textActive: 'var(--ink-pink-1)',
|
|
336
|
+
subtext: 'var(--ink-gray-6)',
|
|
337
|
+
subtextActive: 'var(--ink-base)',
|
|
338
|
+
bg: 'var(--surface-pink-1)',
|
|
339
|
+
bgHover: 'var(--surface-pink-1)',
|
|
340
|
+
bgActive: 'var(--surface-pink-7)',
|
|
338
341
|
},
|
|
339
342
|
cyan: {
|
|
340
|
-
color: '
|
|
341
|
-
border: '
|
|
342
|
-
borderActive: '
|
|
343
|
-
text: '
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
343
|
+
color: 'var(--ink-cyan-7)',
|
|
344
|
+
border: 'var(--ink-cyan-7)',
|
|
345
|
+
borderActive: 'var(--ink-cyan-7)',
|
|
346
|
+
text: 'var(--ink-cyan-7)',
|
|
347
|
+
textActive: 'var(--ink-cyan-1)',
|
|
348
|
+
subtext: 'var(--ink-gray-6)',
|
|
349
|
+
subtextActive: 'var(--ink-base)',
|
|
350
|
+
bg: 'var(--surface-cyan-1)',
|
|
351
|
+
bgHover: 'var(--surface-cyan-1)',
|
|
352
|
+
bgActive: 'var(--surface-cyan-7)',
|
|
349
353
|
},
|
|
350
354
|
blue: {
|
|
351
|
-
color: '
|
|
352
|
-
border: '
|
|
353
|
-
borderActive: '
|
|
354
|
-
text: '
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
355
|
+
color: 'var(--ink-blue-7)',
|
|
356
|
+
border: 'var(--ink-blue-7)',
|
|
357
|
+
borderActive: 'var(--outline-blue-2)',
|
|
358
|
+
text: 'var(--ink-blue-7)',
|
|
359
|
+
textActive: 'var(--ink-blue-1)',
|
|
360
|
+
subtext: 'var(--ink-gray-6)',
|
|
361
|
+
subtextActive: 'var(--ink-blue-1)',
|
|
362
|
+
bg: 'var(--surface-blue-1)',
|
|
363
|
+
bgHover: 'var(--surface-blue-1)',
|
|
364
|
+
bgActive: 'var(--surface-blue-2)',
|
|
360
365
|
},
|
|
361
366
|
orange: {
|
|
362
|
-
color: '
|
|
363
|
-
border: '
|
|
364
|
-
borderActive: '
|
|
365
|
-
text: '
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
color: 'var(--ink-orange-7)',
|
|
368
|
+
border: 'var(--outline-orange-1)',
|
|
369
|
+
borderActive: 'var(--outline-orange-1)',
|
|
370
|
+
text: 'var(--ink-orange-7)',
|
|
371
|
+
textActive: 'var(--ink-orange-1)',
|
|
372
|
+
subtext: 'var(--ink-gray-6)',
|
|
373
|
+
subtextActive: 'var(--ink-base)',
|
|
374
|
+
bg: 'var(--surface-orange-1)',
|
|
375
|
+
bgHover: 'var(--surface-orange-1)',
|
|
376
|
+
bgActive: 'var(--outline-orange-1)',
|
|
371
377
|
},
|
|
372
378
|
green: {
|
|
373
|
-
color: '
|
|
374
|
-
border: '
|
|
375
|
-
borderActive: '
|
|
376
|
-
text: '
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
color: 'var(--ink-green-7)',
|
|
380
|
+
border: 'var(--ink-green-7)',
|
|
381
|
+
borderActive: 'var(--outline-green-2)',
|
|
382
|
+
text: 'var(--ink-green-7)',
|
|
383
|
+
textActive: 'var(--ink-green-1)',
|
|
384
|
+
subtext: 'var(--ink-gray-6)',
|
|
385
|
+
subtextActive: 'var(--ink-green-1)',
|
|
386
|
+
bg: 'var(--surface-green-1)',
|
|
387
|
+
bgHover: 'var(--surface-green-1)',
|
|
388
|
+
bgActive: 'var(--surface-green-3)',
|
|
382
389
|
},
|
|
383
390
|
}
|
|
384
391
|
|
|
385
|
-
export const colorMapDark: Record<string, CalendarColor> =
|
|
386
|
-
amber: {
|
|
387
|
-
color: '#DB7706',
|
|
388
|
-
border: '#C57411',
|
|
389
|
-
borderActive: '#C57411',
|
|
390
|
-
text: '#C57411',
|
|
391
|
-
textActive: '#824108',
|
|
392
|
-
subtext: '#988356',
|
|
393
|
-
subtextActive: '#8E6026',
|
|
394
|
-
bg: '#371E06',
|
|
395
|
-
bgHover: '#4B2606',
|
|
396
|
-
bgActive: '#F8D16E',
|
|
397
|
-
},
|
|
398
|
-
violet: {
|
|
399
|
-
color: '#6846E3',
|
|
400
|
-
border: '#A384EC',
|
|
401
|
-
borderActive: '#8867E8',
|
|
402
|
-
text: '#A384EC',
|
|
403
|
-
textActive: '#4639A6',
|
|
404
|
-
subtext: '#9389AE',
|
|
405
|
-
subtextActive: '#332978',
|
|
406
|
-
bg: '#221C42',
|
|
407
|
-
bgHover: '#281E5D',
|
|
408
|
-
bgActive: '#C4AFEE',
|
|
409
|
-
},
|
|
410
|
-
pink: {
|
|
411
|
-
color: '#E34AA6',
|
|
412
|
-
border: '#CB4394',
|
|
413
|
-
borderActive: '#CB4394',
|
|
414
|
-
text: '#E359AB',
|
|
415
|
-
textActive: '#822A5F',
|
|
416
|
-
subtext: '#B07E99',
|
|
417
|
-
subtextActive: '#935277',
|
|
418
|
-
bg: '#471432',
|
|
419
|
-
bgHover: '#68204B',
|
|
420
|
-
bgActive: '#F6C5DE',
|
|
421
|
-
},
|
|
422
|
-
cyan: {
|
|
423
|
-
color: '#3BBDE5',
|
|
424
|
-
border: '#2B8DAB',
|
|
425
|
-
borderActive: '#2B8DAB',
|
|
426
|
-
text: '#3CB8DC',
|
|
427
|
-
textActive: '#155266',
|
|
428
|
-
subtext: '#819FA8',
|
|
429
|
-
subtextActive: '#3A6E7D',
|
|
430
|
-
bg: '#0B252D',
|
|
431
|
-
bgHover: '#0E3B49',
|
|
432
|
-
bgActive: '#A0E6F7',
|
|
433
|
-
},
|
|
434
|
-
blue: {
|
|
435
|
-
color: '#0289F7',
|
|
436
|
-
border: '#3294E3',
|
|
437
|
-
borderActive: '#1580D8',
|
|
438
|
-
text: '#5AAEF2',
|
|
439
|
-
textActive: '#155999',
|
|
440
|
-
subtext: '#7F95AC',
|
|
441
|
-
subtextActive: '#386A99',
|
|
442
|
-
bg: '#10243E',
|
|
443
|
-
bgHover: '#052B53',
|
|
444
|
-
bgActive: '#ADD2F5',
|
|
445
|
-
},
|
|
446
|
-
orange: {
|
|
447
|
-
color: '#E86C13',
|
|
448
|
-
border: '#C45A0E',
|
|
449
|
-
borderActive: '#C45A0E',
|
|
450
|
-
text: '#DE6D1B',
|
|
451
|
-
textActive: '#823906',
|
|
452
|
-
subtext: '#B3876B',
|
|
453
|
-
subtextActive: '#955528',
|
|
454
|
-
bg: '#401F07',
|
|
455
|
-
bgHover: '#532707',
|
|
456
|
-
bgActive: '#FFA873',
|
|
457
|
-
},
|
|
458
|
-
green: {
|
|
459
|
-
color: '#30A66D',
|
|
460
|
-
border: '#35AE74',
|
|
461
|
-
borderActive: '#35AE74',
|
|
462
|
-
text: '#58C08E',
|
|
463
|
-
textActive: '#0B6139',
|
|
464
|
-
subtext: '#7CA490',
|
|
465
|
-
subtextActive: '#0B6139',
|
|
466
|
-
bg: '#0B2E1C',
|
|
467
|
-
bgHover: '#0A3F27',
|
|
468
|
-
bgActive: '#9BE6C1',
|
|
469
|
-
},
|
|
470
|
-
}
|
|
392
|
+
export const colorMapDark: Record<string, CalendarColor> = colorMap
|
|
471
393
|
|
|
472
394
|
// config.weekends can be array of numbers [0-6] (0=Sun) or weekday names (e.g., 'Saturday').
|
|
473
395
|
// Falls back to [0] (Sunday) if not provided / invalid.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
-
import { Button, Calendar, DatePicker, Select } from
|
|
3
|
+
import { Button, Calendar, DatePicker, Select } from 'frappe-ui'
|
|
4
4
|
|
|
5
5
|
const config = {
|
|
6
6
|
defaultMode: 'Month',
|
|
@@ -65,10 +65,10 @@ const events = ref([
|
|
|
65
65
|
color: 'red',
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
|
-
title: '
|
|
68
|
+
title: 'Frappe Meet with John',
|
|
69
69
|
participant: 'John',
|
|
70
70
|
id: '#htrht41',
|
|
71
|
-
venue: '
|
|
71
|
+
venue: 'Frappe Meet',
|
|
72
72
|
fromDate: currentMonthYear + '-11',
|
|
73
73
|
toDate: currentMonthYear + '-11',
|
|
74
74
|
fromTime: '00:00',
|
|
@@ -77,10 +77,10 @@ const events = ref([
|
|
|
77
77
|
isFullDay: true,
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
|
-
title: '
|
|
80
|
+
title: 'Frappe Meet with Sheldon',
|
|
81
81
|
participant: 'Sheldon',
|
|
82
82
|
id: '#htrht42',
|
|
83
|
-
venue: '
|
|
83
|
+
venue: 'Frappe Meet',
|
|
84
84
|
fromDate: currentMonthYear + '-07',
|
|
85
85
|
toDate: currentMonthYear + '-07',
|
|
86
86
|
fromTime: '00:00',
|
|
@@ -92,7 +92,9 @@ const events = ref([
|
|
|
92
92
|
</script>
|
|
93
93
|
|
|
94
94
|
<template>
|
|
95
|
-
<div
|
|
95
|
+
<div
|
|
96
|
+
class="w-full flex flex-wrap gap-3 items-center h-screen overflow-hidden"
|
|
97
|
+
>
|
|
96
98
|
<Calendar
|
|
97
99
|
:config="config"
|
|
98
100
|
:events="events"
|
|
@@ -65,10 +65,10 @@ const events = ref([
|
|
|
65
65
|
color: 'red',
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
|
-
title: '
|
|
68
|
+
title: 'Frappe Meet with John',
|
|
69
69
|
participant: 'John',
|
|
70
70
|
id: '#htrht41',
|
|
71
|
-
venue: '
|
|
71
|
+
venue: 'Frappe Meet',
|
|
72
72
|
fromDate: currentMonthYear + '-11',
|
|
73
73
|
toDate: currentMonthYear + '-11',
|
|
74
74
|
fromTime: '00:00',
|
|
@@ -77,10 +77,10 @@ const events = ref([
|
|
|
77
77
|
isFullDay: true,
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
|
-
title: '
|
|
80
|
+
title: 'Frappe Meet with Sheldon',
|
|
81
81
|
participant: 'Sheldon',
|
|
82
82
|
id: '#htrht42',
|
|
83
|
-
venue: '
|
|
83
|
+
venue: 'Frappe Meet',
|
|
84
84
|
fromDate: currentMonthYear + '-07',
|
|
85
85
|
toDate: currentMonthYear + '-07',
|
|
86
86
|
fromTime: '00:00',
|
|
@@ -92,7 +92,9 @@ const events = ref([
|
|
|
92
92
|
</script>
|
|
93
93
|
|
|
94
94
|
<template>
|
|
95
|
-
<div
|
|
95
|
+
<div
|
|
96
|
+
class="w-full flex flex-wrap gap-3 items-center h-screen overflow-hidden"
|
|
97
|
+
>
|
|
96
98
|
<Calendar
|
|
97
99
|
:config="config"
|
|
98
100
|
:events="events"
|
|
@@ -8,6 +8,16 @@ import {
|
|
|
8
8
|
type CalendarEvent,
|
|
9
9
|
} from './types'
|
|
10
10
|
|
|
11
|
+
const legacyColorNamesByHex: Record<string, keyof typeof colorMap> = {
|
|
12
|
+
'#db7706': 'amber',
|
|
13
|
+
'#6846e3': 'violet',
|
|
14
|
+
'#e34aa6': 'pink',
|
|
15
|
+
'#3bbde5': 'cyan',
|
|
16
|
+
'#0289f7': 'blue',
|
|
17
|
+
'#e86c13': 'orange',
|
|
18
|
+
'#30a66d': 'green',
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
export const isAnyPopoverOpen = ref(false)
|
|
12
22
|
|
|
13
23
|
export function useEventBase(props: { event: CalendarEvent; date: Date }) {
|
|
@@ -51,6 +61,8 @@ export function useEventBase(props: { event: CalendarEvent; date: Date }) {
|
|
|
51
61
|
const map = getTheme() === 'dark' ? colorMapDark : colorMap
|
|
52
62
|
if (!colorValue?.startsWith('#'))
|
|
53
63
|
return map[colorValue || 'green'] || map['green']!
|
|
64
|
+
const legacyColorName = legacyColorNamesByHex[colorValue.toLowerCase()]
|
|
65
|
+
if (legacyColorName) return map[legacyColorName]!
|
|
54
66
|
for (const value of Object.values(map)) {
|
|
55
67
|
if (value.color === colorValue) return value
|
|
56
68
|
}
|
|
@@ -16,7 +16,7 @@ const props = withDefaults(
|
|
|
16
16
|
extensions: Extension[]
|
|
17
17
|
|
|
18
18
|
// content / behavior knobs (universal, reactive where noted)
|
|
19
|
-
format?: 'html' | 'json'
|
|
19
|
+
format?: 'html' | 'json' | 'markdown'
|
|
20
20
|
placeholder?: string
|
|
21
21
|
editable?: boolean
|
|
22
22
|
autofocus?: boolean
|
|
@@ -60,7 +60,11 @@ const editor = useEditor({
|
|
|
60
60
|
// independent of v-model timing).
|
|
61
61
|
emit(
|
|
62
62
|
'change',
|
|
63
|
-
props.format === 'json'
|
|
63
|
+
props.format === 'json'
|
|
64
|
+
? editor.getJSON()
|
|
65
|
+
: props.format === 'markdown'
|
|
66
|
+
? editor.getMarkdown()
|
|
67
|
+
: editor.getHTML(),
|
|
64
68
|
)
|
|
65
69
|
},
|
|
66
70
|
onFocus(_editor, event) {
|
|
@@ -311,3 +311,8 @@ export const Toc = TocNodeExtension
|
|
|
311
311
|
export const ContentPaste = ContentPasteExtension
|
|
312
312
|
export const StyleClipboard = StyleClipboardExtension
|
|
313
313
|
export type { MediaUploadRequestOptions } from './extensions/shared/media-upload-engine'
|
|
314
|
+
|
|
315
|
+
// Required in `extensions` for `format: 'markdown'`. A plain re-export (no
|
|
316
|
+
// use inside useEditor) so bundlers drop the markdown/marked payload for
|
|
317
|
+
// editors that never import it.
|
|
318
|
+
export { Markdown, type MarkdownExtensionOptions } from '@tiptap/markdown'
|
|
@@ -54,6 +54,10 @@ vi.mock('@tiptap/core', () => {
|
|
|
54
54
|
return typeof this.content === 'object' ? this.content : { type: 'doc', content: [] }
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
getMarkdown() {
|
|
58
|
+
return typeof this.content === 'string' ? this.content : '# json'
|
|
59
|
+
}
|
|
60
|
+
|
|
57
61
|
setEditable(value: boolean) {
|
|
58
62
|
this.editable = value
|
|
59
63
|
}
|
|
@@ -66,6 +70,17 @@ vi.mock('@tiptap/core', () => {
|
|
|
66
70
|
return { Editor, Extension, Node, Mark, mergeAttributes, nodeInputRule, markInputRule }
|
|
67
71
|
})
|
|
68
72
|
|
|
73
|
+
vi.mock('@tiptap/markdown', () => ({
|
|
74
|
+
Markdown: {
|
|
75
|
+
name: 'markdown',
|
|
76
|
+
configure: vi.fn((options: any) => ({
|
|
77
|
+
name: 'markdown',
|
|
78
|
+
options,
|
|
79
|
+
configured: true,
|
|
80
|
+
})),
|
|
81
|
+
},
|
|
82
|
+
}))
|
|
83
|
+
|
|
69
84
|
vi.mock('@tiptap/vue-3', () => ({
|
|
70
85
|
EditorContent: defineComponent({
|
|
71
86
|
props: ['editor'],
|
|
@@ -196,6 +211,63 @@ describe('frappe-ui/editor minimal primitives', () => {
|
|
|
196
211
|
expect(editor.commands.setContent).toHaveBeenCalledTimes(calls)
|
|
197
212
|
})
|
|
198
213
|
|
|
214
|
+
it('sets markdown contentType and warns when the Markdown extension is missing', async () => {
|
|
215
|
+
const { useEditor } = await import('./index')
|
|
216
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
217
|
+
|
|
218
|
+
createApp(
|
|
219
|
+
defineComponent({
|
|
220
|
+
setup() {
|
|
221
|
+
useEditor({ content: ref('# Hi'), format: 'markdown', extensions: [] })
|
|
222
|
+
return () => null
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
).mount(document.createElement('div'))
|
|
226
|
+
|
|
227
|
+
expect(editors[0].options.contentType).toBe('markdown')
|
|
228
|
+
expect(warn).toHaveBeenCalledWith(
|
|
229
|
+
expect.stringContaining("format: 'markdown' needs the Markdown extension"),
|
|
230
|
+
)
|
|
231
|
+
warn.mockRestore()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('binds markdown content in both directions without rewriting equal external markdown', async () => {
|
|
235
|
+
const { useEditor, Markdown } = await import('./index')
|
|
236
|
+
const content = ref('# Hello')
|
|
237
|
+
|
|
238
|
+
createApp(
|
|
239
|
+
defineComponent({
|
|
240
|
+
setup() {
|
|
241
|
+
useEditor({ content, format: 'markdown', extensions: [Markdown as any] })
|
|
242
|
+
return () => null
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
).mount(document.createElement('div'))
|
|
246
|
+
|
|
247
|
+
const editor = editors[0]
|
|
248
|
+
expect(editor.options.content).toBe('# Hello')
|
|
249
|
+
|
|
250
|
+
// Internal edit flows out through getMarkdown() and must not bounce back.
|
|
251
|
+
editor.content = '# Internal'
|
|
252
|
+
editor.options.onUpdate({ editor })
|
|
253
|
+
expect(content.value).toBe('# Internal')
|
|
254
|
+
await nextTick()
|
|
255
|
+
expect(editor.commands.setContent).not.toHaveBeenCalled()
|
|
256
|
+
|
|
257
|
+
// External write equal to the current document is a no-op.
|
|
258
|
+
content.value = '# Internal'
|
|
259
|
+
await nextTick()
|
|
260
|
+
expect(editor.commands.setContent).not.toHaveBeenCalled()
|
|
261
|
+
|
|
262
|
+
// A genuinely new external value is parsed as markdown.
|
|
263
|
+
content.value = '# External'
|
|
264
|
+
await nextTick()
|
|
265
|
+
expect(editor.commands.setContent).toHaveBeenCalledWith('# External', {
|
|
266
|
+
emitUpdate: false,
|
|
267
|
+
contentType: 'markdown',
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
199
271
|
it('keeps editable reactive', async () => {
|
|
200
272
|
const { useEditor } = await import('./index')
|
|
201
273
|
const editable = ref(true)
|
|
@@ -19,7 +19,7 @@ type Editor = TiptapEditor
|
|
|
19
19
|
|
|
20
20
|
export type UseEditorOptions = {
|
|
21
21
|
content?: Ref<string | JSONContent | null | undefined>
|
|
22
|
-
format?: 'html' | 'json'
|
|
22
|
+
format?: 'html' | 'json' | 'markdown'
|
|
23
23
|
editable?: MaybeRefOrGetter<boolean>
|
|
24
24
|
autofocus?: boolean
|
|
25
25
|
uploadFunction?: (file: File) => Promise<UploadedFile>
|
|
@@ -63,6 +63,23 @@ export function useEditor(
|
|
|
63
63
|
|
|
64
64
|
const extensions = [UploadStorage, ...options.extensions]
|
|
65
65
|
|
|
66
|
+
if (
|
|
67
|
+
import.meta.env?.DEV &&
|
|
68
|
+
format === 'markdown' &&
|
|
69
|
+
!extensions.some((extension) => extension.name === 'markdown')
|
|
70
|
+
) {
|
|
71
|
+
console.warn(
|
|
72
|
+
"[frappe-ui] format: 'markdown' needs the Markdown extension in `extensions` — import { Markdown } from 'frappe-ui/editor'.",
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const serialize = (tiptapEditor: Editor) =>
|
|
77
|
+
format === 'json'
|
|
78
|
+
? tiptapEditor.getJSON()
|
|
79
|
+
: format === 'markdown'
|
|
80
|
+
? tiptapEditor.getMarkdown()
|
|
81
|
+
: tiptapEditor.getHTML()
|
|
82
|
+
|
|
66
83
|
const editorOptions: Partial<EditorOptions> = {
|
|
67
84
|
element: null,
|
|
68
85
|
extensions,
|
|
@@ -70,8 +87,7 @@ export function useEditor(
|
|
|
70
87
|
autofocus: options.autofocus,
|
|
71
88
|
onUpdate: ({ editor: tiptapEditor }) => {
|
|
72
89
|
if (!isCollaborationMode && options.content && !applyingExternalUpdate) {
|
|
73
|
-
const value =
|
|
74
|
-
format === 'json' ? tiptapEditor.getJSON() : tiptapEditor.getHTML()
|
|
90
|
+
const value = serialize(tiptapEditor)
|
|
75
91
|
lastEmitted = value
|
|
76
92
|
options.content.value = value
|
|
77
93
|
}
|
|
@@ -88,6 +104,10 @@ export function useEditor(
|
|
|
88
104
|
},
|
|
89
105
|
}
|
|
90
106
|
|
|
107
|
+
if (format === 'markdown') {
|
|
108
|
+
editorOptions.contentType = 'markdown'
|
|
109
|
+
}
|
|
110
|
+
|
|
91
111
|
if (!isCollaborationMode && options.content?.value != null) {
|
|
92
112
|
editorOptions.content = options.content.value
|
|
93
113
|
}
|
|
@@ -109,10 +129,17 @@ export function useEditor(
|
|
|
109
129
|
// whose fresh-object identity defeats the HTML string check below).
|
|
110
130
|
if (toRaw(content) === lastEmitted) return
|
|
111
131
|
if (format === 'html' && editor.value.getHTML() === content) return
|
|
132
|
+
if (format === 'markdown' && editor.value.getMarkdown() === content)
|
|
133
|
+
return
|
|
112
134
|
|
|
113
135
|
applyingExternalUpdate = true
|
|
114
136
|
try {
|
|
115
|
-
editor.value.commands.setContent(
|
|
137
|
+
editor.value.commands.setContent(
|
|
138
|
+
content ?? '',
|
|
139
|
+
format === 'markdown'
|
|
140
|
+
? { emitUpdate: false, contentType: 'markdown' }
|
|
141
|
+
: { emitUpdate: false },
|
|
142
|
+
)
|
|
116
143
|
} finally {
|
|
117
144
|
applyingExternalUpdate = false
|
|
118
145
|
}
|
|
@@ -32,12 +32,12 @@ const isActive = (link: string) =>
|
|
|
32
32
|
>
|
|
33
33
|
<div class="px-1">
|
|
34
34
|
<a
|
|
35
|
-
class="hidden items-center gap-
|
|
35
|
+
class="hidden items-center gap-1.5 px-2 py-2 lg:flex hover:bg-surface-gray-2 rounded transition-colors"
|
|
36
36
|
:href="withBase('/')"
|
|
37
37
|
>
|
|
38
|
-
<img v-if="logo" :src="withBase(logo)" class="w-
|
|
38
|
+
<img v-if="logo" :src="withBase(logo)" class="w-6" />
|
|
39
39
|
<div class="flex flex-col gap-1 *:leading-none">
|
|
40
|
-
<span class="text-
|
|
40
|
+
<span class="text-lg-bold text-ink-gray-8 tracking-[-0.01em]">{{
|
|
41
41
|
siteName
|
|
42
42
|
}}</span>
|
|
43
43
|
<span v-if="curVersion" class="text-sm text-ink-gray-5"
|
|
@@ -58,7 +58,7 @@ const toggleTheme = () =>
|
|
|
58
58
|
<a
|
|
59
59
|
v-if="logo || name"
|
|
60
60
|
:href="withBase('/')"
|
|
61
|
-
class="flex items-center gap-
|
|
61
|
+
class="flex items-center gap-1.5 min-w-0"
|
|
62
62
|
:class="{ 'lg:hidden': isDocs }"
|
|
63
63
|
>
|
|
64
64
|
<img
|
|
@@ -67,7 +67,10 @@ const toggleTheme = () =>
|
|
|
67
67
|
class="w-6 shrink-0"
|
|
68
68
|
:alt="name"
|
|
69
69
|
/>
|
|
70
|
-
<span
|
|
70
|
+
<span
|
|
71
|
+
v-if="name"
|
|
72
|
+
class="text-lg-bold text-ink-gray-8 truncate tracking-[-0.01em]"
|
|
73
|
+
>
|
|
71
74
|
{{ name }}
|
|
72
75
|
</span>
|
|
73
76
|
</a>
|