@karyl-chan/ui 0.1.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.
- package/package.json +58 -0
- package/src/components/AppBadge.vue +155 -0
- package/src/components/AppButton.vue +171 -0
- package/src/components/AppConfirmDialog.vue +176 -0
- package/src/components/AppItemCard.vue +216 -0
- package/src/components/AppMenu.vue +197 -0
- package/src/components/AppMenuItem.vue +43 -0
- package/src/components/AppModal.vue +257 -0
- package/src/components/AppPopover.vue +275 -0
- package/src/components/AppSelect.vue +89 -0
- package/src/components/AppSelectField.vue +260 -0
- package/src/components/AppTabs.vue +310 -0
- package/src/components/AppTabsRouted.vue +409 -0
- package/src/components/AppTextArea.vue +128 -0
- package/src/components/AppTextField.vue +165 -0
- package/src/components/AppToast.vue +65 -0
- package/src/components/AppToggle.vue +105 -0
- package/src/components/Draggable.vue +178 -0
- package/src/components/GlobalConfirmDialog.vue +25 -0
- package/src/components/UnreadPill.vue +28 -0
- package/src/components/UserAvatar.vue +114 -0
- package/src/components/UserCard.vue +274 -0
- package/src/components/UserItem.vue +167 -0
- package/src/components/tab-types.ts +9 -0
- package/src/composables/use-app-shell.ts +65 -0
- package/src/composables/use-breakpoint.ts +14 -0
- package/src/composables/use-click-outside-stack.ts +67 -0
- package/src/composables/use-color-scheme.ts +75 -0
- package/src/composables/use-confirm.ts +36 -0
- package/src/composables/use-drawer.css +47 -0
- package/src/composables/use-drawer.ts +58 -0
- package/src/composables/use-escape-stack.ts +80 -0
- package/src/composables/use-file-drop.ts +47 -0
- package/src/composables/use-long-press.ts +62 -0
- package/src/composables/use-popover.css +32 -0
- package/src/composables/use-popover.ts +1324 -0
- package/src/composables/use-shift-key.ts +18 -0
- package/src/index.ts +75 -0
- package/src/lib/avatar.ts +35 -0
- package/src/reset.css +53 -0
- package/src/stores/toastStore.ts +26 -0
- package/src/tokens.css +159 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { Icon } from '@iconify/vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List-item card with an expandable body — the shared chrome that
|
|
7
|
+
* BehaviorCard and PluginCard each re-implemented (and that PluginCard
|
|
8
|
+
* was the uglier of the two, with a hand-rolled menu and no source
|
|
9
|
+
* stripe).
|
|
10
|
+
*
|
|
11
|
+
* The component owns:
|
|
12
|
+
* - Outer card border + radius + background.
|
|
13
|
+
* - Optional left-edge accent stripe (`accentBar` prop).
|
|
14
|
+
* - The header row that holds:
|
|
15
|
+
* - `#leading` slot (drag handle / lock icon, no click semantics)
|
|
16
|
+
* - The expand button — chevron + `#title` slot, click toggles `expanded`
|
|
17
|
+
* - `#trailing` slot (badges / toggles / menus)
|
|
18
|
+
* - The body container that mounts the default slot when expanded.
|
|
19
|
+
* - Disabled-style strikethrough on the title when `disabled` is true.
|
|
20
|
+
*
|
|
21
|
+
* Everything domain-specific (which badge to show, whether to include a
|
|
22
|
+
* toggle, what's in the form) stays at the caller. This deliberately
|
|
23
|
+
* does NOT take an `enabled` prop or render an internal toggle — those
|
|
24
|
+
* are call-site decisions, and forcing them into the API would push
|
|
25
|
+
* BehaviorCard's protected-system rules and PluginCard's per-status
|
|
26
|
+
* delete-only menu logic up into ui.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export type AccentBarTone = 'neutral' | 'accent' | 'success' | 'warn' | 'danger';
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(defineProps<{
|
|
32
|
+
/** Controlled expansion. Pair with `@update:expanded`. */
|
|
33
|
+
expanded?: boolean;
|
|
34
|
+
/** Strike-through + muted title — used by BehaviorCard when off. */
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** Left-edge 3px accent stripe. Use to indicate source / kind. */
|
|
37
|
+
accentBar?: AccentBarTone | null;
|
|
38
|
+
/** Custom CSS colour for `accentBar` — overrides the tone palette. */
|
|
39
|
+
accentBarColor?: string;
|
|
40
|
+
}>(), {
|
|
41
|
+
expanded: false,
|
|
42
|
+
disabled: false,
|
|
43
|
+
accentBar: null,
|
|
44
|
+
accentBarColor: ''
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const emit = defineEmits<{
|
|
48
|
+
(e: 'update:expanded', value: boolean): void;
|
|
49
|
+
/** Fired regardless of who owns the expanded state. */
|
|
50
|
+
(e: 'toggle', next: boolean): void;
|
|
51
|
+
}>();
|
|
52
|
+
|
|
53
|
+
const rootClass = computed(() => [
|
|
54
|
+
'app-item-card',
|
|
55
|
+
{
|
|
56
|
+
'app-item-card--disabled': props.disabled,
|
|
57
|
+
'app-item-card--bar': !!props.accentBar || !!props.accentBarColor
|
|
58
|
+
}
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const barStyle = computed(() => {
|
|
62
|
+
if (props.accentBarColor) return { background: props.accentBarColor };
|
|
63
|
+
if (!props.accentBar) return undefined;
|
|
64
|
+
return { background: `var(--app-item-card-bar-${props.accentBar})` };
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function toggle(): void {
|
|
68
|
+
const next = !props.expanded;
|
|
69
|
+
emit('update:expanded', next);
|
|
70
|
+
emit('toggle', next);
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<article :class="rootClass">
|
|
76
|
+
<div v-if="accentBar || accentBarColor" class="app-item-card__bar" :style="barStyle" aria-hidden="true"></div>
|
|
77
|
+
<div class="app-item-card__inner">
|
|
78
|
+
<header class="app-item-card__head" :class="{ 'app-item-card__head--has-body': expanded }">
|
|
79
|
+
<div v-if="$slots.leading" class="app-item-card__leading">
|
|
80
|
+
<slot name="leading" />
|
|
81
|
+
</div>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
class="app-item-card__expander"
|
|
85
|
+
:aria-expanded="expanded"
|
|
86
|
+
@click="toggle"
|
|
87
|
+
>
|
|
88
|
+
<Icon
|
|
89
|
+
:icon="expanded ? 'material-symbols:expand-less-rounded' : 'material-symbols:expand-more-rounded'"
|
|
90
|
+
width="18"
|
|
91
|
+
height="18"
|
|
92
|
+
class="app-item-card__chevron"
|
|
93
|
+
/>
|
|
94
|
+
<span class="app-item-card__title">
|
|
95
|
+
<slot name="title" />
|
|
96
|
+
</span>
|
|
97
|
+
</button>
|
|
98
|
+
<div v-if="$slots.trailing" class="app-item-card__trailing">
|
|
99
|
+
<slot name="trailing" />
|
|
100
|
+
</div>
|
|
101
|
+
</header>
|
|
102
|
+
<div v-if="expanded" class="app-item-card__body">
|
|
103
|
+
<slot />
|
|
104
|
+
<footer v-if="$slots.footer" class="app-item-card__footer">
|
|
105
|
+
<slot name="footer" />
|
|
106
|
+
</footer>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</article>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<style scoped>
|
|
113
|
+
.app-item-card {
|
|
114
|
+
/* Bar tone palette — sourced from semantic tokens so themes flow through. */
|
|
115
|
+
--app-item-card-bar-neutral: var(--text-muted);
|
|
116
|
+
--app-item-card-bar-accent: var(--accent);
|
|
117
|
+
--app-item-card-bar-success: var(--success-text);
|
|
118
|
+
--app-item-card-bar-warn: var(--warn-text);
|
|
119
|
+
--app-item-card-bar-danger: var(--danger);
|
|
120
|
+
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: row;
|
|
123
|
+
border: 1px solid var(--border);
|
|
124
|
+
border-radius: var(--radius-base);
|
|
125
|
+
background: var(--bg-surface);
|
|
126
|
+
overflow: hidden;
|
|
127
|
+
}
|
|
128
|
+
.app-item-card--disabled .app-item-card__title {
|
|
129
|
+
color: var(--text-muted);
|
|
130
|
+
text-decoration: line-through;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.app-item-card__bar {
|
|
134
|
+
width: 3px;
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
border-radius: var(--radius-base) 0 0 var(--radius-base);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.app-item-card__inner {
|
|
140
|
+
flex: 1;
|
|
141
|
+
min-width: 0;
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-direction: column;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.app-item-card__head {
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: 0.5rem;
|
|
150
|
+
padding: 0.45rem 0.75rem 0.45rem 0.4rem;
|
|
151
|
+
background: var(--bg-page);
|
|
152
|
+
border-bottom: 1px solid transparent;
|
|
153
|
+
}
|
|
154
|
+
.app-item-card__head--has-body {
|
|
155
|
+
border-bottom-color: var(--border);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.app-item-card__leading {
|
|
159
|
+
display: inline-flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
flex-shrink: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.app-item-card__expander {
|
|
165
|
+
flex: 1;
|
|
166
|
+
min-width: 0;
|
|
167
|
+
background: none;
|
|
168
|
+
border: none;
|
|
169
|
+
text-align: left;
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
color: var(--text);
|
|
172
|
+
display: flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
gap: 0.4rem;
|
|
175
|
+
padding: 0.25rem 0.1rem;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
font: inherit;
|
|
178
|
+
}
|
|
179
|
+
.app-item-card__expander:hover { color: var(--text-strong); }
|
|
180
|
+
.app-item-card__expander:focus-visible {
|
|
181
|
+
outline: 2px solid var(--accent);
|
|
182
|
+
outline-offset: -1px;
|
|
183
|
+
border-radius: var(--radius-sm);
|
|
184
|
+
}
|
|
185
|
+
.app-item-card__chevron {
|
|
186
|
+
color: var(--text-muted);
|
|
187
|
+
flex-shrink: 0;
|
|
188
|
+
}
|
|
189
|
+
.app-item-card__title {
|
|
190
|
+
flex: 1;
|
|
191
|
+
min-width: 0;
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 0.4rem;
|
|
195
|
+
overflow: hidden;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.app-item-card__trailing {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 0.4rem;
|
|
202
|
+
flex-shrink: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.app-item-card__body {
|
|
206
|
+
padding: 0.75rem;
|
|
207
|
+
display: flex;
|
|
208
|
+
flex-direction: column;
|
|
209
|
+
gap: 0.6rem;
|
|
210
|
+
}
|
|
211
|
+
.app-item-card__footer {
|
|
212
|
+
display: flex;
|
|
213
|
+
justify-content: flex-end;
|
|
214
|
+
gap: 0.5rem;
|
|
215
|
+
}
|
|
216
|
+
</style>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
import { useBreakpoint } from '../composables/use-breakpoint';
|
|
4
|
+
import { usePopover, type Placement } from '../composables/use-popover';
|
|
5
|
+
import { useDrawer } from '../composables/use-drawer';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Viewport-aware menu:
|
|
9
|
+
* - Desktop → popover anchored to the trigger via usePopover.
|
|
10
|
+
* - Mobile → bottom drawer (mirrors AppPopover so menu surfaces
|
|
11
|
+
* feel consistent with picker / popover surfaces on touch).
|
|
12
|
+
*
|
|
13
|
+
* The trigger slot is wrapped in a click-bubbling `display: contents`
|
|
14
|
+
* span so the wrapper doesn't affect the caller's layout. Clicking
|
|
15
|
+
* any AppMenuItem inside the default slot closes the menu when
|
|
16
|
+
* `closeOnItemClick` is true (the default — same as the original
|
|
17
|
+
* desktop-only AppMenu it replaces).
|
|
18
|
+
*/
|
|
19
|
+
const props = withDefaults(defineProps<{
|
|
20
|
+
placement?: Placement;
|
|
21
|
+
/** Distance [skidding, distance] between trigger and menu. Default: [0, 8]. */
|
|
22
|
+
offset?: [number, number];
|
|
23
|
+
/** Close after the user clicks any item inside the menu. Default: true. */
|
|
24
|
+
closeOnItemClick?: boolean;
|
|
25
|
+
/** Optional heading rendered at the top of the mobile drawer. */
|
|
26
|
+
drawerTitle?: string;
|
|
27
|
+
}>(), {
|
|
28
|
+
placement: 'bottom-end',
|
|
29
|
+
closeOnItemClick: true
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const { isMobile } = useBreakpoint();
|
|
33
|
+
|
|
34
|
+
const isOpen = ref(false);
|
|
35
|
+
function toggle() { isOpen.value = !isOpen.value; }
|
|
36
|
+
function close() { isOpen.value = false; }
|
|
37
|
+
|
|
38
|
+
const triggerWrapRef = ref<HTMLElement | null>(null);
|
|
39
|
+
const contentEl = ref<HTMLElement | null>(null);
|
|
40
|
+
|
|
41
|
+
// Anchor the desktop popover against the slot's first real child —
|
|
42
|
+
// the wrapper itself uses display: contents and has no usable layout
|
|
43
|
+
// rectangle in some browsers. Same trick AppPopover plays.
|
|
44
|
+
const anchorRef = computed<HTMLElement | null>(() => {
|
|
45
|
+
const wrap = triggerWrapRef.value;
|
|
46
|
+
if (!wrap) return null;
|
|
47
|
+
return (wrap.firstElementChild as HTMLElement | null) ?? wrap;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const popoverVisible = computed(() => !isMobile.value && isOpen.value);
|
|
51
|
+
const drawerVisible = computed(() => isMobile.value && isOpen.value);
|
|
52
|
+
|
|
53
|
+
usePopover(anchorRef, contentEl, {
|
|
54
|
+
placement: props.placement,
|
|
55
|
+
trigger: 'manual',
|
|
56
|
+
offset: props.offset ?? [0, 8],
|
|
57
|
+
teleportTo: 'body',
|
|
58
|
+
visible: popoverVisible,
|
|
59
|
+
closeOnClickOutside: true,
|
|
60
|
+
closeOnEscape: true,
|
|
61
|
+
closeOnContentClick: props.closeOnItemClick,
|
|
62
|
+
onHide: () => {
|
|
63
|
+
// Mirror self-close (Escape / click-outside / item-click) back
|
|
64
|
+
// onto the local open ref. Guard against the false branch
|
|
65
|
+
// firing during a mobile/desktop swap, which would otherwise
|
|
66
|
+
// recurse the close.
|
|
67
|
+
if (popoverVisible.value) close();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const { backdropClass, panelClass, backdropTransition, panelTransition } = useDrawer({
|
|
72
|
+
visible: drawerVisible,
|
|
73
|
+
placement: 'bottom',
|
|
74
|
+
closeOnEscape: true,
|
|
75
|
+
onClose: close
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function onContentClick() {
|
|
79
|
+
if (props.closeOnItemClick) close();
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<span
|
|
85
|
+
ref="triggerWrapRef"
|
|
86
|
+
class="app-menu-trigger"
|
|
87
|
+
@click="toggle"
|
|
88
|
+
>
|
|
89
|
+
<slot name="trigger" :is-open="isOpen" :toggle="toggle" :close="close" />
|
|
90
|
+
</span>
|
|
91
|
+
|
|
92
|
+
<!-- Desktop popover content. Always rendered so usePopover binds on
|
|
93
|
+
mount; visibility gated by popoverVisible. -->
|
|
94
|
+
<div
|
|
95
|
+
ref="contentEl"
|
|
96
|
+
class="app-menu app-menu--hidden"
|
|
97
|
+
role="menu"
|
|
98
|
+
@click="onContentClick"
|
|
99
|
+
>
|
|
100
|
+
<slot :close="close" :is-open="isOpen" />
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Mobile drawer branch — slides up from the bottom edge,
|
|
104
|
+
matches AppPopover's drawer chrome. z-index sits above
|
|
105
|
+
AppModal (1500) so a menu opened from inside a modal still
|
|
106
|
+
floats on top. -->
|
|
107
|
+
<Teleport v-if="isMobile" to="body">
|
|
108
|
+
<Transition :name="backdropTransition">
|
|
109
|
+
<div
|
|
110
|
+
v-if="drawerVisible"
|
|
111
|
+
:class="[backdropClass, 'app-menu-drawer-backdrop']"
|
|
112
|
+
:style="{ zIndex: 2000 }"
|
|
113
|
+
@click="close"
|
|
114
|
+
/>
|
|
115
|
+
</Transition>
|
|
116
|
+
<Transition :name="panelTransition">
|
|
117
|
+
<div
|
|
118
|
+
v-if="drawerVisible"
|
|
119
|
+
:class="[panelClass, 'app-menu-drawer-panel']"
|
|
120
|
+
data-placement="bottom"
|
|
121
|
+
:style="{ zIndex: 2001 }"
|
|
122
|
+
role="menu"
|
|
123
|
+
aria-modal="true"
|
|
124
|
+
@click="onContentClick"
|
|
125
|
+
>
|
|
126
|
+
<header v-if="drawerTitle" class="app-menu-drawer-title">{{ drawerTitle }}</header>
|
|
127
|
+
<div class="app-menu-drawer-body">
|
|
128
|
+
<slot :close="close" :is-open="isOpen" />
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</Transition>
|
|
132
|
+
</Teleport>
|
|
133
|
+
</template>
|
|
134
|
+
|
|
135
|
+
<style scoped>
|
|
136
|
+
.app-menu-trigger {
|
|
137
|
+
display: contents;
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
}
|
|
140
|
+
.app-menu {
|
|
141
|
+
min-width: 180px;
|
|
142
|
+
padding: 0.3rem 0;
|
|
143
|
+
background: var(--bg-surface);
|
|
144
|
+
border: 1px solid var(--border);
|
|
145
|
+
border-radius: var(--radius-base);
|
|
146
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
147
|
+
z-index: 1600;
|
|
148
|
+
}
|
|
149
|
+
/* .app-menu--hidden no longer sets display:none — visibility is managed
|
|
150
|
+
exclusively via inline style by use-popover to avoid specificity
|
|
151
|
+
conflicts (inline style wins over class; removing it would let the
|
|
152
|
+
class re-apply display:none and break the popover). */
|
|
153
|
+
.app-menu-drawer-panel {
|
|
154
|
+
max-height: 70vh;
|
|
155
|
+
background: var(--bg-surface);
|
|
156
|
+
border-top: 1px solid var(--border);
|
|
157
|
+
border-radius: 12px 12px 0 0;
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
overflow: hidden;
|
|
161
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
162
|
+
}
|
|
163
|
+
.app-menu-drawer-panel::before {
|
|
164
|
+
content: '';
|
|
165
|
+
display: block;
|
|
166
|
+
width: 36px;
|
|
167
|
+
height: 4px;
|
|
168
|
+
border-radius: 2px;
|
|
169
|
+
background: var(--text-muted);
|
|
170
|
+
opacity: 0.35;
|
|
171
|
+
margin: 0.4rem auto 0;
|
|
172
|
+
flex-shrink: 0;
|
|
173
|
+
}
|
|
174
|
+
.app-menu-drawer-title {
|
|
175
|
+
padding: 0.6rem 1rem 0.5rem;
|
|
176
|
+
font-weight: 600;
|
|
177
|
+
font-size: 0.95rem;
|
|
178
|
+
color: var(--text-strong);
|
|
179
|
+
border-bottom: 1px solid var(--border);
|
|
180
|
+
flex-shrink: 0;
|
|
181
|
+
}
|
|
182
|
+
.app-menu-drawer-body {
|
|
183
|
+
flex: 1;
|
|
184
|
+
min-height: 0;
|
|
185
|
+
overflow-y: auto;
|
|
186
|
+
overflow-x: hidden;
|
|
187
|
+
padding: 0.3rem 0;
|
|
188
|
+
}
|
|
189
|
+
/* Bump menu items inside the mobile drawer to a comfortable touch
|
|
190
|
+
target — the desktop popover already has acceptable spacing for
|
|
191
|
+
pointer use, but cramped rows on a touchscreen are easy to misfire. */
|
|
192
|
+
.app-menu-drawer-body :deep(.app-menu-item) {
|
|
193
|
+
padding: 0.85rem 1rem;
|
|
194
|
+
font-size: 1rem;
|
|
195
|
+
min-height: 48px;
|
|
196
|
+
}
|
|
197
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
/** If true, the item is rendered with danger styling (e.g., sign out). */
|
|
5
|
+
danger?: boolean;
|
|
6
|
+
}>();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<button
|
|
11
|
+
type="button"
|
|
12
|
+
role="menuitem"
|
|
13
|
+
:class="['app-menu-item', { 'is-danger': danger }]"
|
|
14
|
+
:disabled="disabled"
|
|
15
|
+
>
|
|
16
|
+
<slot />
|
|
17
|
+
</button>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<style scoped>
|
|
21
|
+
.app-menu-item {
|
|
22
|
+
width: 100%;
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: 0.5rem;
|
|
26
|
+
padding: 0.5rem 0.85rem;
|
|
27
|
+
background: none;
|
|
28
|
+
border: none;
|
|
29
|
+
color: var(--text);
|
|
30
|
+
font: inherit;
|
|
31
|
+
font-size: 0.9rem;
|
|
32
|
+
text-align: left;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
}
|
|
35
|
+
.app-menu-item:hover:not(:disabled) {
|
|
36
|
+
background: var(--bg-surface-hover);
|
|
37
|
+
}
|
|
38
|
+
.app-menu-item:disabled {
|
|
39
|
+
opacity: 0.5;
|
|
40
|
+
cursor: default;
|
|
41
|
+
}
|
|
42
|
+
.app-menu-item.is-danger { color: var(--danger); }
|
|
43
|
+
</style>
|