frappe-ui 1.0.0-beta.76 → 1.0.0-beta.77
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 +1 -1
- package/src/components/KeyboardShortcut/KeyboardShortcut.md +5 -5
- package/src/components/Sidebar/Sidebar.api.md +8 -1
- package/src/components/Sidebar/Sidebar.md +27 -2
- package/src/components/Sidebar/Sidebar.vue +8 -2
- package/src/components/Sidebar/SidebarHeader.vue +67 -47
- package/src/components/Sidebar/SidebarItem.vue +50 -6
- package/src/components/Sidebar/SidebarSection.vue +12 -5
- package/src/components/Sidebar/types.ts +10 -2
- package/src/components/SidebarRail/SidebarRail.md +4 -0
- package/src/components/SidebarRail/SidebarRailItem.vue +12 -1
- package/src/molecules/editor/extensions/shared/media-node-ops.ts +2 -2
- package/src/molecules/editor/extensions/shared/media-upload-engine.ts +3 -3
- package/src/molecules/editor/extensions/shared/media-upload-types.ts +3 -3
- package/src/molecules/editor/extensions/shared/upload-types.ts +2 -2
- package/src/molecules/editor/index.ts +1 -1
- package/src/molecules/editor/useEditor.ts +2 -2
- package/src/utils/useFileUpload.ts +1 -1
- package/src/components/Sidebar/SidebarItemIcon.vue +0 -25
package/package.json
CHANGED
|
@@ -37,11 +37,11 @@ and Alt as icons, `bg` mode draws them as text.
|
|
|
37
37
|
|
|
38
38
|
## Styling hooks
|
|
39
39
|
|
|
40
|
-
The root carries `data-slot="keyboard-shortcut"`, and `data-bg
|
|
41
|
-
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
The root carries `data-slot="keyboard-shortcut"`, and `data-bg` when `bg` is
|
|
41
|
+
set. Each key carries `data-slot="key"` with a `data-key-type` of `cmd`, `ctrl`,
|
|
42
|
+
`shift`, `alt` or `key`. The `+` separators carry `data-slot="separator"`, and
|
|
43
|
+
alternative combos sit in `data-slot="alt-combos"` inside the root. Style
|
|
44
|
+
through those instead of a class prop (P10):
|
|
45
45
|
|
|
46
46
|
```css
|
|
47
47
|
[data-slot='keyboard-shortcut'][data-bg] [data-slot='key'] {
|
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
type: 'string',
|
|
27
27
|
default: '"3rem"'
|
|
28
28
|
},
|
|
29
|
+
{
|
|
30
|
+
name: 'ariaLabel',
|
|
31
|
+
description: 'Accessible name of the `<nav>` landmark. Default: `\'Main\'`. Give a second\nsidebar on the same page its own name, and translate it in a localised\napp — the library ships no translation layer.',
|
|
32
|
+
required: false,
|
|
33
|
+
type: 'string',
|
|
34
|
+
default: '"Main"'
|
|
35
|
+
},
|
|
29
36
|
{
|
|
30
37
|
name: 'collapsed',
|
|
31
38
|
description: 'v-model. Whether the sidebar is collapsed. Left unset, it collapses automatically below the `sm` breakpoint.',
|
|
@@ -66,7 +73,7 @@
|
|
|
66
73
|
},
|
|
67
74
|
{
|
|
68
75
|
name: 'icon',
|
|
69
|
-
description: 'Leading icon: a
|
|
76
|
+
description: 'Leading icon: a `lucide-*` class, an emoji, or a component. Any other\nstring renders nothing and warns in dev — put initials in the `#prefix`\nslot instead. Ignored when the `#prefix` slot is used.',
|
|
70
77
|
required: false,
|
|
71
78
|
type: 'string | Component'
|
|
72
79
|
},
|
|
@@ -12,6 +12,18 @@ There are no layout slots and no built-in scrolling in composition mode. Put a
|
|
|
12
12
|
header as a direct child, wrap the middle list in your own `overflow-y-auto`
|
|
13
13
|
container, and push a footer down with `mt-auto`.
|
|
14
14
|
|
|
15
|
+
## Accessibility
|
|
16
|
+
|
|
17
|
+
`Sidebar` renders the `<nav>` landmark, named `Main` through `aria-label`. Set
|
|
18
|
+
`ariaLabel` to rename it — do that when a page holds a second sidebar, and to
|
|
19
|
+
translate the name in a localised app. It is the only landmark the family
|
|
20
|
+
emits: keep your own wrappers inside the slot as plain `div`s, or the page
|
|
21
|
+
reports two navigations where it has one.
|
|
22
|
+
|
|
23
|
+
`SidebarSection` gives its body `role="group"`, named by the section's `<h3>`
|
|
24
|
+
whenever `label` is set, collapsible or not. A collapsible section's trigger
|
|
25
|
+
keeps `aria-expanded` and `aria-controls` on the body.
|
|
26
|
+
|
|
15
27
|
## Collapse
|
|
16
28
|
|
|
17
29
|
`Sidebar` owns collapse. Bind `v-model:collapsed` to control it, or leave it
|
|
@@ -29,8 +41,10 @@ A single row. It renders a container with a navigable main area and a **sibling*
|
|
|
29
41
|
trailing zone, so an options menu in `#suffix` isn't nested inside the link
|
|
30
42
|
(which anchors and buttons disallow).
|
|
31
43
|
|
|
32
|
-
- `#prefix` — a leading icon or avatar (falls back to the `icon` prop: a
|
|
33
|
-
class,
|
|
44
|
+
- `#prefix` — a leading icon or avatar (falls back to the `icon` prop: a
|
|
45
|
+
`lucide-*` class, an emoji, or a component — the same values `Icon` takes).
|
|
46
|
+
Any other string renders nothing and warns in dev, so put initials or a text
|
|
47
|
+
glyph in `#prefix`.
|
|
34
48
|
- default slot — the label region (falls back to the `label` prop). Put inline
|
|
35
49
|
adornments like a lock icon here next to the text.
|
|
36
50
|
- `#suffix` — the trailing zone: an unread count, an options `…` menu, etc.
|
|
@@ -41,6 +55,13 @@ Set `route` to render a router link or `href` for a native same-tab anchor;
|
|
|
41
55
|
`route` against the current route. A click invokes `onClick` (bound from
|
|
42
56
|
`@click`) in every case.
|
|
43
57
|
|
|
58
|
+
Attributes you write on `SidebarItem` split in two. `class`, `style` and event
|
|
59
|
+
listeners stay on the row container, so a row background or a drag-and-drop
|
|
60
|
+
ring covers the `#suffix` zone and a `@dragover` / `@drop` there still fires.
|
|
61
|
+
Every other attribute (`target`, `rel`, `id`, `title`, `data-*`, `aria-*`)
|
|
62
|
+
lands on the link or the button inside, the element it describes. Your
|
|
63
|
+
`aria-label` replaces the one derived from the label text.
|
|
64
|
+
|
|
44
65
|
## SidebarHeader
|
|
45
66
|
|
|
46
67
|
The app-switcher / workspace-identity row. A fixed 48px region that lines up
|
|
@@ -51,6 +72,10 @@ or the title's first letter; `showLogo: false` drops the box entirely for a
|
|
|
51
72
|
flush-left title. `menuItems` renders inside the trigger's
|
|
52
73
|
dropdown — the same structured-options shape `Dropdown` itself takes.
|
|
53
74
|
|
|
75
|
+
Without `menuItems` the header is a plain static block: no button, no chevron,
|
|
76
|
+
no hover background, no tab stop. The logo and the title keep the same
|
|
77
|
+
position, so adding a menu later does not move them.
|
|
78
|
+
|
|
54
79
|
## SidebarSection
|
|
55
80
|
|
|
56
81
|
A collapsible group. It owns only the label row and the collapse chrome —
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<!-- A <nav> landmark, so screen-reader users can jump to the app's
|
|
3
|
+
navigation. It is the only one the sidebar family emits: SidebarSection
|
|
4
|
+
bodies are `role="group"`, because nested landmarks add noise instead of
|
|
5
|
+
structure. Keep app-written wrappers inside the slot as plain divs. -->
|
|
6
|
+
<nav
|
|
3
7
|
data-slot="sidebar"
|
|
4
8
|
:data-state="shouldCollapse ? 'collapsed' : 'expanded'"
|
|
9
|
+
:aria-label="ariaLabel"
|
|
5
10
|
class="flex h-full flex-shrink-0 flex-col overflow-x-hidden bg-surface-sidebar transition-[width] duration-300 ease-in-out"
|
|
6
11
|
:style="{ width: shouldCollapse ? collapsedWidth : width }"
|
|
7
12
|
>
|
|
@@ -9,7 +14,7 @@
|
|
|
9
14
|
it from SidebarHeader / SidebarSection / SidebarLabel / SidebarItem
|
|
10
15
|
and plain markup. -->
|
|
11
16
|
<slot />
|
|
12
|
-
</
|
|
17
|
+
</nav>
|
|
13
18
|
</template>
|
|
14
19
|
|
|
15
20
|
<script setup lang="ts">
|
|
@@ -21,6 +26,7 @@ const props = withDefaults(defineProps<SidebarProps>(), {
|
|
|
21
26
|
collapsible: true,
|
|
22
27
|
width: '15rem',
|
|
23
28
|
collapsedWidth: '3rem',
|
|
29
|
+
ariaLabel: 'Main',
|
|
24
30
|
})
|
|
25
31
|
|
|
26
32
|
defineSlots<{
|
|
@@ -4,7 +4,55 @@
|
|
|
4
4
|
the sidebar gutter (px-2) itself — so consumers drop it straight into
|
|
5
5
|
<Sidebar> without a wrapping padding div. -->
|
|
6
6
|
<div data-slot="sidebar-header" class="flex h-12 shrink-0 items-center px-1">
|
|
7
|
-
|
|
7
|
+
<!-- The logo + title block. Declared once (this renders nothing) and
|
|
8
|
+
reused by both branches below, so the interactive and the static
|
|
9
|
+
header cannot drift apart. -->
|
|
10
|
+
<DefineIdentity>
|
|
11
|
+
<div
|
|
12
|
+
v-if="showLogo"
|
|
13
|
+
class="size-7 shrink-0 rounded-[6px] overflow-hidden"
|
|
14
|
+
>
|
|
15
|
+
<slot name="prefix">
|
|
16
|
+
<img
|
|
17
|
+
v-if="typeof props.logo === 'string'"
|
|
18
|
+
:src="props.logo"
|
|
19
|
+
class="w-full h-full object-cover"
|
|
20
|
+
alt="Logo"
|
|
21
|
+
/>
|
|
22
|
+
<div
|
|
23
|
+
v-else-if="!props.logo"
|
|
24
|
+
class="w-full h-full bg-surface-gray-4 flex items-center justify-center text-ink-gray-7"
|
|
25
|
+
>
|
|
26
|
+
{{ props.title.charAt(0).toUpperCase() }}
|
|
27
|
+
</div>
|
|
28
|
+
<component v-else :is="props.logo" class="w-full h-full" />
|
|
29
|
+
</slot>
|
|
30
|
+
</div>
|
|
31
|
+
<div
|
|
32
|
+
class="flex flex-1 flex-col text-left duration-300 ease-in-out truncate"
|
|
33
|
+
:class="
|
|
34
|
+
isCollapsed
|
|
35
|
+
? 'ml-0 w-0 overflow-hidden opacity-0'
|
|
36
|
+
: showLogo
|
|
37
|
+
? 'ml-2 w-auto opacity-100'
|
|
38
|
+
: 'ml-0 w-auto opacity-100'
|
|
39
|
+
"
|
|
40
|
+
>
|
|
41
|
+
<!-- The size sits on the line itself, not on a span inside a
|
|
42
|
+
leading-none div: a line box built from a strut at the
|
|
43
|
+
inherited size and text at another puts the baseline at a
|
|
44
|
+
different offset in an app whose layout sets a smaller base
|
|
45
|
+
size, and the header text sat a px lower there. -->
|
|
46
|
+
<div class="truncate text-base-medium leading-tighter text-ink-gray-8">
|
|
47
|
+
{{ props.title }}
|
|
48
|
+
</div>
|
|
49
|
+
<div class="mt-0.5 truncate text-sm leading-tighter text-ink-gray-6">
|
|
50
|
+
{{ props.subtitle }}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</DefineIdentity>
|
|
54
|
+
|
|
55
|
+
<Dropdown v-if="hasMenu" :options="props.menuItems" match-trigger-width>
|
|
8
56
|
<template v-slot="{ open }">
|
|
9
57
|
<button
|
|
10
58
|
class="flex h-10 items-center rounded-4 px-1.5 duration-300 ease-in-out"
|
|
@@ -16,52 +64,7 @@
|
|
|
16
64
|
: 'w-full hover:bg-surface-gray-3'
|
|
17
65
|
"
|
|
18
66
|
>
|
|
19
|
-
<
|
|
20
|
-
v-if="showLogo"
|
|
21
|
-
class="size-7 shrink-0 rounded-[6px] overflow-hidden"
|
|
22
|
-
>
|
|
23
|
-
<slot name="prefix">
|
|
24
|
-
<img
|
|
25
|
-
v-if="typeof props.logo === 'string'"
|
|
26
|
-
:src="props.logo"
|
|
27
|
-
class="w-full h-full object-cover"
|
|
28
|
-
alt="Logo"
|
|
29
|
-
/>
|
|
30
|
-
<div
|
|
31
|
-
v-else-if="!props.logo"
|
|
32
|
-
class="w-full h-full bg-surface-gray-4 flex items-center justify-center text-ink-gray-7"
|
|
33
|
-
>
|
|
34
|
-
{{ props.title.charAt(0).toUpperCase() }}
|
|
35
|
-
</div>
|
|
36
|
-
<component v-else :is="props.logo" class="w-full h-full" />
|
|
37
|
-
</slot>
|
|
38
|
-
</div>
|
|
39
|
-
<div
|
|
40
|
-
class="flex flex-1 flex-col text-left duration-300 ease-in-out truncate"
|
|
41
|
-
:class="
|
|
42
|
-
isCollapsed
|
|
43
|
-
? 'ml-0 w-0 overflow-hidden opacity-0'
|
|
44
|
-
: showLogo
|
|
45
|
-
? 'ml-2 w-auto opacity-100'
|
|
46
|
-
: 'ml-0 w-auto opacity-100'
|
|
47
|
-
"
|
|
48
|
-
>
|
|
49
|
-
<!-- The size sits on the line itself, not on a span inside a
|
|
50
|
-
leading-none div: a line box built from a strut at the
|
|
51
|
-
inherited size and text at another puts the baseline at a
|
|
52
|
-
different offset in an app whose layout sets a smaller base
|
|
53
|
-
size, and the header text sat a px lower there. -->
|
|
54
|
-
<div
|
|
55
|
-
class="truncate text-base-medium leading-tighter text-ink-gray-8"
|
|
56
|
-
>
|
|
57
|
-
{{ props.title }}
|
|
58
|
-
</div>
|
|
59
|
-
<div
|
|
60
|
-
class="mt-0.5 truncate text-sm leading-tighter text-ink-gray-6"
|
|
61
|
-
>
|
|
62
|
-
{{ props.subtitle }}
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
67
|
+
<ReuseIdentity />
|
|
65
68
|
<div
|
|
66
69
|
class="duration-300 ease-in-out"
|
|
67
70
|
:class="
|
|
@@ -75,10 +78,24 @@
|
|
|
75
78
|
</button>
|
|
76
79
|
</template>
|
|
77
80
|
</Dropdown>
|
|
81
|
+
|
|
82
|
+
<!--
|
|
83
|
+
No menu items, nothing to open: a plain div, with the trigger's box but
|
|
84
|
+
no chevron, no hover background and no tab stop. The identity block keeps
|
|
85
|
+
the same x/y either way, so adding menu items later does not move it.
|
|
86
|
+
-->
|
|
87
|
+
<div
|
|
88
|
+
v-else
|
|
89
|
+
class="flex h-10 items-center rounded-4 px-1.5 duration-300 ease-in-out"
|
|
90
|
+
:class="isCollapsed ? 'w-auto' : 'w-full'"
|
|
91
|
+
>
|
|
92
|
+
<ReuseIdentity />
|
|
93
|
+
</div>
|
|
78
94
|
</div>
|
|
79
95
|
</template>
|
|
80
96
|
|
|
81
97
|
<script setup lang="ts">
|
|
98
|
+
import { createReusableTemplate } from '@vueuse/core'
|
|
82
99
|
import { computed, inject } from 'vue'
|
|
83
100
|
import Dropdown from '../Dropdown/Dropdown.vue'
|
|
84
101
|
import { SidebarHeaderProps, sidebarCollapsedKey } from './types'
|
|
@@ -95,9 +112,12 @@ defineSlots<{
|
|
|
95
112
|
prefix?: () => any
|
|
96
113
|
}>()
|
|
97
114
|
|
|
115
|
+
const [DefineIdentity, ReuseIdentity] = createReusableTemplate()
|
|
116
|
+
|
|
98
117
|
const isCollapsed = inject(
|
|
99
118
|
sidebarCollapsedKey,
|
|
100
119
|
computed(() => false),
|
|
101
120
|
)
|
|
102
121
|
const showLogo = computed(() => props.showLogo !== false)
|
|
122
|
+
const hasMenu = computed(() => Boolean(props.menuItems?.length))
|
|
103
123
|
</script>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<div
|
|
3
3
|
data-slot="sidebar-item"
|
|
4
4
|
:data-state="resolvedActive ? 'active' : 'inactive'"
|
|
5
|
+
v-bind="rootAttrs"
|
|
5
6
|
class="group/sidebar-item flex h-7 items-center rounded-4 transition"
|
|
6
7
|
:class="
|
|
7
8
|
resolvedActive
|
|
@@ -16,13 +17,18 @@
|
|
|
16
17
|
RouterLink component object or the 'a' string is safe; only a raw 'button'
|
|
17
18
|
string collides with the globally-registered <Button>, so that stays a
|
|
18
19
|
literal <button v-else>. The inner content is identical in both branches.
|
|
20
|
+
|
|
21
|
+
`controlAttrs` is spread before `linkAttrs` and before the component's own
|
|
22
|
+
attributes, so a caller cannot overwrite `href` / `to`, `aria-current` or
|
|
23
|
+
the classes. `aria-label` is the exception: `ariaLabel` returns the
|
|
24
|
+
caller's value when there is one.
|
|
19
25
|
-->
|
|
20
26
|
<component
|
|
21
27
|
:is="linkComponent"
|
|
22
28
|
v-if="route || href"
|
|
23
|
-
v-bind="linkAttrs"
|
|
29
|
+
v-bind="{ ...controlAttrs, ...linkAttrs }"
|
|
24
30
|
:accesskey="accessKey"
|
|
25
|
-
:aria-label="
|
|
31
|
+
:aria-label="ariaLabel"
|
|
26
32
|
:aria-current="resolvedActive ? 'page' : undefined"
|
|
27
33
|
class="flex h-full min-w-0 flex-1 items-center rounded-4 pl-2 focus-visible:ring-0 focus-visible:focus-ring"
|
|
28
34
|
@click="handleClick"
|
|
@@ -38,7 +44,7 @@
|
|
|
38
44
|
square and back (the whole row is already the hit target). -->
|
|
39
45
|
<span class="grid shrink-0 place-items-center">
|
|
40
46
|
<slot name="prefix">
|
|
41
|
-
<
|
|
47
|
+
<Icon :icon="icon" class="size-4 text-ink-gray-6" />
|
|
42
48
|
</slot>
|
|
43
49
|
</span>
|
|
44
50
|
</Tooltip>
|
|
@@ -63,8 +69,9 @@
|
|
|
63
69
|
<button
|
|
64
70
|
v-else
|
|
65
71
|
type="button"
|
|
72
|
+
v-bind="controlAttrs"
|
|
66
73
|
:accesskey="accessKey"
|
|
67
|
-
:aria-label="
|
|
74
|
+
:aria-label="ariaLabel"
|
|
68
75
|
class="flex h-full text-left min-w-0 flex-1 items-center rounded-4 pl-2 focus-visible:ring-0 focus-visible:focus-ring"
|
|
69
76
|
@click="handleClick"
|
|
70
77
|
>
|
|
@@ -79,7 +86,7 @@
|
|
|
79
86
|
square and back (the whole row is already the hit target). -->
|
|
80
87
|
<span class="grid shrink-0 place-items-center">
|
|
81
88
|
<slot name="prefix">
|
|
82
|
-
<
|
|
89
|
+
<Icon :icon="icon" class="size-4 text-ink-gray-6" />
|
|
83
90
|
</slot>
|
|
84
91
|
</span>
|
|
85
92
|
</Tooltip>
|
|
@@ -127,13 +134,41 @@ import {
|
|
|
127
134
|
inject,
|
|
128
135
|
onMounted,
|
|
129
136
|
ref,
|
|
137
|
+
useAttrs,
|
|
130
138
|
useTemplateRef,
|
|
131
139
|
} from 'vue'
|
|
132
140
|
import { RouterLink } from 'vue-router'
|
|
141
|
+
import Icon from '../Icon/Icon.vue'
|
|
133
142
|
import Tooltip from '../Tooltip/Tooltip.vue'
|
|
134
|
-
import SidebarItemIcon from './SidebarItemIcon.vue'
|
|
135
143
|
import { SidebarItemProps, sidebarCollapsedKey } from './types'
|
|
136
144
|
|
|
145
|
+
// The root <div> is a wrapper, not the control: the link/button and the
|
|
146
|
+
// #suffix zone are siblings inside it. Without this, every fallthrough
|
|
147
|
+
// attribute stopped at the wrapper, so `target`, `rel`, `data-*` and `aria-*`
|
|
148
|
+
// never reached the element they describe.
|
|
149
|
+
defineOptions({ inheritAttrs: false })
|
|
150
|
+
|
|
151
|
+
const attrs = useAttrs()
|
|
152
|
+
|
|
153
|
+
// The split. `class`, `style` and event listeners belong to the whole row, so
|
|
154
|
+
// they stay on the wrapper: a row background or a drag-and-drop ring has to
|
|
155
|
+
// cover the #suffix zone, and so does a listener. Mail and Drive make a folder
|
|
156
|
+
// row a drop target with `@dragover` / `@drop` while the suffix holds the
|
|
157
|
+
// unread count and the options menu. On the inner control those events would
|
|
158
|
+
// miss that strip. Everything else describes the control and goes there.
|
|
159
|
+
// `onClick` is a declared prop, so it is never in `attrs`; it stays bound to
|
|
160
|
+
// the control through `handleClick`.
|
|
161
|
+
const isRootAttr = (key: string) =>
|
|
162
|
+
key === 'class' || key === 'style' || /^on[A-Z]/.test(key)
|
|
163
|
+
|
|
164
|
+
const rootAttrs = computed(() =>
|
|
165
|
+
Object.fromEntries(Object.entries(attrs).filter(([key]) => isRootAttr(key))),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const controlAttrs = computed(() =>
|
|
169
|
+
Object.fromEntries(Object.entries(attrs).filter(([key]) => !isRootAttr(key))),
|
|
170
|
+
)
|
|
171
|
+
|
|
137
172
|
// `active` must default to `undefined`, not Vue's implicit boolean `false` —
|
|
138
173
|
// "not passed" and "passed false" are different states here: absence falls
|
|
139
174
|
// through to route inference.
|
|
@@ -166,6 +201,15 @@ onMounted(() => {
|
|
|
166
201
|
})
|
|
167
202
|
const tooltipText = computed(() => props.label || slotLabel.value)
|
|
168
203
|
|
|
204
|
+
// A caller's `aria-label` wins over the derived one. It is the more specific
|
|
205
|
+
// name, and it is the only way to name a row whose visible text is not its
|
|
206
|
+
// full name (a folder row that also announces its unread count). Every other
|
|
207
|
+
// attribute the component computes is bound after `controlAttrs`, so a caller
|
|
208
|
+
// cannot overwrite it.
|
|
209
|
+
const ariaLabel = computed(
|
|
210
|
+
() => (attrs['aria-label'] as string) || tooltipText.value || undefined,
|
|
211
|
+
)
|
|
212
|
+
|
|
169
213
|
// Read the router/route off global properties instead of useRouter()/useRoute()
|
|
170
214
|
// so this component works — without warnings or crashes — when mounted outside a
|
|
171
215
|
// vue-router app (docs, tests, embedded use). vue-router installs $router/$route
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
<h3> inside it strips the heading from the accessibility tree.
|
|
17
17
|
-->
|
|
18
18
|
<h3
|
|
19
|
+
:id="headingId"
|
|
19
20
|
class="h-4 text-sm leading-tighter text-ink-gray-5 transition-all duration-300 ease-in-out"
|
|
20
21
|
:class="
|
|
21
22
|
isSidebarCollapsed
|
|
@@ -25,7 +26,6 @@
|
|
|
25
26
|
>
|
|
26
27
|
<button
|
|
27
28
|
v-if="collapsible"
|
|
28
|
-
:id="triggerId"
|
|
29
29
|
type="button"
|
|
30
30
|
class="flex items-center gap-1 rounded-4 focus-visible:ring-0 focus-visible:focus-ring"
|
|
31
31
|
:aria-expanded="!isSectionCollapsed"
|
|
@@ -64,15 +64,20 @@
|
|
|
64
64
|
resolve to a real element at all times, including the instant
|
|
65
65
|
aria-expanded flips to "false" — an id that only exists while
|
|
66
66
|
expanded breaks the reference exactly when it's read.
|
|
67
|
+
|
|
68
|
+
role="group", not a <nav>: Sidebar is already the navigation landmark,
|
|
69
|
+
and a landmark inside a landmark buys nothing. The group takes its
|
|
70
|
+
name from the <h3>, so a non-collapsible section is named too.
|
|
67
71
|
-->
|
|
68
|
-
<
|
|
72
|
+
<div
|
|
69
73
|
v-show="!isSectionCollapsed"
|
|
70
74
|
:id="bodyId"
|
|
71
|
-
|
|
75
|
+
role="group"
|
|
76
|
+
:aria-labelledby="label ? headingId : undefined"
|
|
72
77
|
class="flex flex-col gap-0.5"
|
|
73
78
|
>
|
|
74
79
|
<slot />
|
|
75
|
-
</
|
|
80
|
+
</div>
|
|
76
81
|
</transition>
|
|
77
82
|
</div>
|
|
78
83
|
</template>
|
|
@@ -109,6 +114,8 @@ defineEmits<{
|
|
|
109
114
|
/** v-model. Whether the section is collapsed. Bind it to own the state (start a section collapsed, persist the choice); left unbound the section manages it internally, starting expanded. */
|
|
110
115
|
const isSectionCollapsed = defineModel<boolean>('collapsed', { default: false })
|
|
111
116
|
|
|
112
|
-
|
|
117
|
+
// The heading carries the id, not the trigger button: a non-collapsible
|
|
118
|
+
// section has no button, and the group still has to be named.
|
|
119
|
+
const headingId = useId()
|
|
113
120
|
const bodyId = useId()
|
|
114
121
|
</script>
|
|
@@ -28,6 +28,13 @@ export type SidebarProps = {
|
|
|
28
28
|
|
|
29
29
|
/** Collapsed width as a CSS length. */
|
|
30
30
|
collapsedWidth?: string
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Accessible name of the `<nav>` landmark. Default: `'Main'`. Give a second
|
|
34
|
+
* sidebar on the same page its own name, and translate it in a localised
|
|
35
|
+
* app — the library ships no translation layer.
|
|
36
|
+
*/
|
|
37
|
+
ariaLabel?: string
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
export interface SidebarItemProps {
|
|
@@ -38,8 +45,9 @@ export interface SidebarItemProps {
|
|
|
38
45
|
accessKey?: string
|
|
39
46
|
|
|
40
47
|
/**
|
|
41
|
-
* Leading icon: a
|
|
42
|
-
*
|
|
48
|
+
* Leading icon: a `lucide-*` class, an emoji, or a component. Any other
|
|
49
|
+
* string renders nothing and warns in dev — put initials in the `#prefix`
|
|
50
|
+
* slot instead. Ignored when the `#prefix` slot is used.
|
|
43
51
|
*/
|
|
44
52
|
icon?: string | Component
|
|
45
53
|
|
|
@@ -36,4 +36,8 @@ The `badge` count shows as a pill (`badgeStyle="count"`) or a dot
|
|
|
36
36
|
a dot surfaces the real number in the tooltip. The badge pill teleports to
|
|
37
37
|
`<body>` so an `overflow-hidden` scroll container can't clip it.
|
|
38
38
|
|
|
39
|
+
Attributes you write on `SidebarRailItem` land on the clickable cell — the
|
|
40
|
+
`<button>` or the link — not on the tooltip wrapper, and a `class` adds to the
|
|
41
|
+
cell's own classes.
|
|
42
|
+
|
|
39
43
|
<!-- @include: ./SidebarRail.api.md -->
|
|
@@ -10,11 +10,16 @@
|
|
|
10
10
|
literal v-else because a raw 'button' string can resolve to the globally
|
|
11
11
|
registered <Button> in consumer apps. The inner content is identical in
|
|
12
12
|
both branches.
|
|
13
|
+
|
|
14
|
+
$attrs is spread before the component's own attributes, so a caller
|
|
15
|
+
cannot overwrite data-slot / data-state, which tests and app CSS select
|
|
16
|
+
on. `class` is exempt: Vue merges class and style whatever the order, so
|
|
17
|
+
a caller's class adds to `cellClasses` instead of replacing it.
|
|
13
18
|
-->
|
|
14
19
|
<component
|
|
15
20
|
:is="linkComponent"
|
|
16
21
|
v-if="route || href"
|
|
17
|
-
v-bind="linkAttrs"
|
|
22
|
+
v-bind="{ ...linkAttrs, ...$attrs }"
|
|
18
23
|
data-slot="sidebar-rail-item"
|
|
19
24
|
:data-variant="variant"
|
|
20
25
|
:data-state="resolvedActive ? 'active' : 'inactive'"
|
|
@@ -36,6 +41,7 @@
|
|
|
36
41
|
<button
|
|
37
42
|
v-else
|
|
38
43
|
type="button"
|
|
44
|
+
v-bind="$attrs"
|
|
39
45
|
data-slot="sidebar-rail-item"
|
|
40
46
|
:data-variant="variant"
|
|
41
47
|
:data-state="resolvedActive ? 'active' : 'inactive'"
|
|
@@ -73,6 +79,11 @@ import Tooltip from '../Tooltip/Tooltip.vue'
|
|
|
73
79
|
import SidebarRailItemBadge from './SidebarRailItemBadge.vue'
|
|
74
80
|
import type { SidebarRailItemProps } from './types'
|
|
75
81
|
|
|
82
|
+
// The root is <Tooltip>, which drops what it receives. Without this, every
|
|
83
|
+
// fallthrough attribute a caller writes on <SidebarRailItem> is lost; the two
|
|
84
|
+
// branches below re-bind them onto the real clickable cell instead.
|
|
85
|
+
defineOptions({ inheritAttrs: false })
|
|
86
|
+
|
|
76
87
|
const props = withDefaults(defineProps<SidebarRailItemProps>(), {
|
|
77
88
|
variant: 'subtle',
|
|
78
89
|
active: undefined,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import type { MediaDimensions } from '#molecules/editor/extensions/shared/media-dimensions'
|
|
16
16
|
import type {
|
|
17
17
|
InsertMode,
|
|
18
|
-
|
|
18
|
+
UploadedMedia,
|
|
19
19
|
} from '#molecules/editor/extensions/shared/media-upload-types'
|
|
20
20
|
|
|
21
21
|
/** Optional intrinsic dimensions (null when the probe failed). */
|
|
@@ -120,7 +120,7 @@ export function applyUploadSuccess(
|
|
|
120
120
|
view: EditorView,
|
|
121
121
|
nodeName: string,
|
|
122
122
|
uploadId: string,
|
|
123
|
-
uploaded:
|
|
123
|
+
uploaded: UploadedMedia,
|
|
124
124
|
): void {
|
|
125
125
|
const pos = findNodeByUploadId(view, nodeName, uploadId)
|
|
126
126
|
if (pos === null) return
|
|
@@ -48,7 +48,7 @@ import type {
|
|
|
48
48
|
MediaUploadEngine,
|
|
49
49
|
MediaUploadOptions,
|
|
50
50
|
MediaUploadRequestOptions,
|
|
51
|
-
|
|
51
|
+
UploadedMedia,
|
|
52
52
|
UploadResult,
|
|
53
53
|
} from '#molecules/editor/extensions/shared/media-upload-types'
|
|
54
54
|
|
|
@@ -58,7 +58,7 @@ export type {
|
|
|
58
58
|
MediaUploadOptions,
|
|
59
59
|
MediaUploadRequestOptions,
|
|
60
60
|
UploadFunction,
|
|
61
|
-
|
|
61
|
+
UploadedMedia,
|
|
62
62
|
UploadResult,
|
|
63
63
|
} from '#molecules/editor/extensions/shared/media-upload-types'
|
|
64
64
|
|
|
@@ -97,7 +97,7 @@ export async function uploadFile(
|
|
|
97
97
|
file: File,
|
|
98
98
|
options: MediaUploadOptions,
|
|
99
99
|
requestOptions?: MediaUploadRequestOptions,
|
|
100
|
-
): Promise<
|
|
100
|
+
): Promise<UploadedMedia> {
|
|
101
101
|
if (!options.uploadFunction) {
|
|
102
102
|
throw new Error('uploadFunction option is not provided')
|
|
103
103
|
}
|
|
@@ -46,7 +46,7 @@ declare module '@tiptap/core' {
|
|
|
46
46
|
* The index signature keeps server fields the editor does not know about, so
|
|
47
47
|
* an upload handler can return the raw File document.
|
|
48
48
|
*/
|
|
49
|
-
export interface
|
|
49
|
+
export interface UploadedMedia extends Partial<FrappeUploadedFile> {
|
|
50
50
|
file_url: string
|
|
51
51
|
width?: number | null
|
|
52
52
|
height?: number | null
|
|
@@ -72,7 +72,7 @@ export interface MediaUploadRequestOptions {
|
|
|
72
72
|
export type UploadFunction = (
|
|
73
73
|
file: File,
|
|
74
74
|
options?: MediaUploadRequestOptions,
|
|
75
|
-
) => Promise<
|
|
75
|
+
) => Promise<UploadedMedia>
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Effective upload options. Mirrors the legacy extension option shape: the only
|
|
@@ -88,7 +88,7 @@ export interface MediaUploadOptions {
|
|
|
88
88
|
/** Outcome of a no-doc-mutation single-file upload. */
|
|
89
89
|
export interface UploadResult {
|
|
90
90
|
success: boolean
|
|
91
|
-
file?:
|
|
91
|
+
file?: UploadedMedia
|
|
92
92
|
error?: Error
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* content-paste path stop each declaring their own `UploadResult` /
|
|
4
4
|
* `MediaUploadOptions` interfaces.
|
|
5
5
|
*
|
|
6
|
-
* `UploadResult`, `MediaUploadOptions` and `
|
|
6
|
+
* `UploadResult`, `MediaUploadOptions` and `UploadedMedia` are OWNED by the image
|
|
7
7
|
* cluster's `media-upload-types.ts` (the engine's canonical type surface); they
|
|
8
8
|
* are re-exported here so consumers have one stable import for the whole upload
|
|
9
9
|
* value-type vocabulary. The only type added here is {@link ExistingImage},
|
|
@@ -13,7 +13,7 @@ export type {
|
|
|
13
13
|
MediaUploadOptions,
|
|
14
14
|
UploadFunction,
|
|
15
15
|
UploadResult,
|
|
16
|
-
|
|
16
|
+
UploadedMedia,
|
|
17
17
|
} from '#molecules/editor/extensions/shared/media-upload-types'
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -43,11 +43,11 @@ export type UseEditorOptions = {
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* The upload result contract, re-exported so `frappe-ui/editor` keeps one
|
|
46
|
-
* `
|
|
46
|
+
* `UploadedMedia` name. The canonical declaration lives next to the media
|
|
47
47
|
* upload engine.
|
|
48
48
|
*/
|
|
49
49
|
export type {
|
|
50
|
-
|
|
50
|
+
UploadedMedia,
|
|
51
51
|
UploadFunction,
|
|
52
52
|
MediaUploadProgress,
|
|
53
53
|
MediaUploadRequestOptions,
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!-- Icon fallback used when no #prefix slot is supplied: a lucide CSS class,
|
|
3
|
-
plain text, or a component. -->
|
|
4
|
-
<span
|
|
5
|
-
v-if="isLucideClass"
|
|
6
|
-
class="size-4 text-ink-gray-6"
|
|
7
|
-
:class="icon as string"
|
|
8
|
-
aria-hidden="true"
|
|
9
|
-
/>
|
|
10
|
-
<span v-else-if="typeof icon === 'string'" class="size-4 text-ink-gray-6">
|
|
11
|
-
{{ icon }}
|
|
12
|
-
</span>
|
|
13
|
-
<component v-else-if="icon" :is="icon" class="size-4 text-ink-gray-6" />
|
|
14
|
-
</template>
|
|
15
|
-
|
|
16
|
-
<script setup lang="ts">
|
|
17
|
-
import { computed } from 'vue'
|
|
18
|
-
import type { Component } from 'vue'
|
|
19
|
-
|
|
20
|
-
const props = defineProps<{ icon?: string | Component }>()
|
|
21
|
-
|
|
22
|
-
const isLucideClass = computed(
|
|
23
|
-
() => typeof props.icon === 'string' && props.icon.startsWith('lucide-'),
|
|
24
|
-
)
|
|
25
|
-
</script>
|