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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "1.0.0-beta.76",
3
+ "version": "1.0.0-beta.77",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -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="true"` when `bg`
41
- is set. Each key carries `data-slot="key"` with a `data-key-type` of `cmd`,
42
- `ctrl`, `shift`, `alt` or `key`. The `+` separators carry
43
- `data-slot="separator"`, and alternative combos sit in `data-slot="alt-combos"`
44
- inside the root. Style through those instead of a class prop (P10):
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 CSS class (e.g. `lucide-box`), plain text, or a component.\nIgnored when the `#prefix` slot is used.',
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 lucide
33
- class, text, or a component).
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
- <div
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
- </div>
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
- <Dropdown :options="props.menuItems" match-trigger-width>
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
- <div
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="tooltipText || undefined"
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
- <SidebarItemIcon :icon="icon" />
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="tooltipText || undefined"
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
- <SidebarItemIcon :icon="icon" />
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
- <nav
72
+ <div
69
73
  v-show="!isSectionCollapsed"
70
74
  :id="bodyId"
71
- :aria-labelledby="collapsible && label ? triggerId : undefined"
75
+ role="group"
76
+ :aria-labelledby="label ? headingId : undefined"
72
77
  class="flex flex-col gap-0.5"
73
78
  >
74
79
  <slot />
75
- </nav>
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
- const triggerId = useId()
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 CSS class (e.g. `lucide-box`), plain text, or a component.
42
- * Ignored when the `#prefix` slot is used.
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
- UploadedFile,
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: UploadedFile,
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
- UploadedFile,
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
- UploadedFile,
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<UploadedFile> {
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 UploadedFile extends Partial<FrappeUploadedFile> {
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<UploadedFile>
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?: UploadedFile
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 `UploadedFile` are OWNED by the image
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
- UploadedFile,
16
+ UploadedMedia,
17
17
  } from '#molecules/editor/extensions/shared/media-upload-types'
18
18
 
19
19
  /**
@@ -8,7 +8,7 @@ export {
8
8
  useEditor,
9
9
  type Editor as TiptapEditor,
10
10
  type JSONContent,
11
- type UploadedFile,
11
+ type UploadedMedia,
12
12
  type UploadFunction,
13
13
  type MediaUploadProgress,
14
14
  } from './useEditor'
@@ -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
- * `UploadedFile` name. The canonical declaration lives next to the media
46
+ * `UploadedMedia` name. The canonical declaration lives next to the media
47
47
  * upload engine.
48
48
  */
49
49
  export type {
50
- UploadedFile,
50
+ UploadedMedia,
51
51
  UploadFunction,
52
52
  MediaUploadProgress,
53
53
  MediaUploadRequestOptions,
@@ -83,7 +83,7 @@ export interface UploadState {
83
83
  result: UploadedFile | null
84
84
  }
85
85
 
86
- export interface UploadedFile {
86
+ export type UploadedFile = {
87
87
  file_name: string
88
88
  file_size: number
89
89
  file_url: string
@@ -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>