@livestorm/ui-vue 2.0.5 → 3.0.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/README.md +6 -3
- package/dist/UiTimelineItem.vue_vue_type_style_index_0_lang-LpdvGf5X.mjs +6093 -0
- package/dist/components.mjs +105 -96
- package/dist/index.mjs +68 -64
- package/dist/layouts.mjs +15 -15
- package/dist/types/index.d.ts +5 -1
- package/dist/types/src/components/UiActionBar/UiActionBar.vue.d.ts +3 -1
- package/dist/types/src/components/UiActionBar/UiActionBarItem.vue.d.ts +18 -3
- package/dist/types/src/components/UiActionBar/types.d.ts +3 -1
- package/dist/types/src/components/UiButton/UiButton.vue.d.ts +17 -13
- package/dist/types/src/components/UiButtonGroup/UiButtonGroupItem.vue.d.ts +3 -0
- package/dist/types/src/components/UiButtonGroup/types.d.ts +1 -1
- package/dist/types/src/components/UiDropdown/UiDropdownItem.vue.d.ts +6 -4
- package/dist/types/src/components/UiInputEmoji/UiInputEmoji.vue.d.ts +45 -0
- package/dist/types/src/components/UiInputEmoji/index.d.ts +1 -0
- package/dist/types/src/components/UiInputRadio/UiInputRadio.vue.d.ts +67 -0
- package/dist/types/src/components/UiInputRadio/UiInputRadioGroup.vue.d.ts +81 -0
- package/dist/types/src/components/UiInputRadio/index.d.ts +3 -0
- package/dist/types/src/components/UiInputRadio/types.d.ts +19 -0
- package/dist/types/src/components/UiLoader/UiLoader.vue.d.ts +6 -5
- package/dist/types/src/components/UiLogo/UiLogo.vue.d.ts +9 -3
- package/dist/types/src/components/UiPage/UiPage.vue.d.ts +34 -0
- package/dist/types/src/components/UiProgress/UiProgress.vue.d.ts +1 -1
- package/dist/types/src/components/UiSwitch/UiSwitch.vue.d.ts +109 -0
- package/dist/types/src/components/UiSwitch/index.d.ts +2 -0
- package/dist/types/src/components/UiSwitch/types.d.ts +3 -0
- package/dist/types/src/components/UiTabs/UiTabs.vue.d.ts +5 -2
- package/dist/types/src/composables/useContentMode.d.ts +25 -0
- package/dist/types/src/composables/useIconSize.d.ts +11 -0
- package/dist/types/src/constants/sizes.d.ts +1 -1
- package/dist/ui-vue.css +1 -1
- package/index.d.ts +4 -0
- package/index.js +8 -0
- package/package.json +6 -3
- package/src/components/Toast/ToastMessage.vue +6 -6
- package/src/components/UiActionBar/UiActionBar.vue +11 -2
- package/src/components/UiActionBar/UiActionBarItem.vue +56 -32
- package/src/components/UiActionBar/types.ts +3 -1
- package/src/components/UiAvatar/UiAvatar.vue +1 -1
- package/src/components/UiBanner/UiBanner.vue +7 -7
- package/src/components/UiButton/UiButton.vue +9 -9
- package/src/components/UiButtonGroup/UiButtonGroup.vue +2 -0
- package/src/components/UiButtonGroup/UiButtonGroupItem.vue +12 -0
- package/src/components/UiButtonGroup/types.ts +1 -1
- package/src/components/UiCarousel/UiCarousel.vue +2 -2
- package/src/components/UiColorPicker/UiColorPicker.vue +5 -5
- package/src/components/UiDialog/MarketplaceHeaderExample.vue +2 -2
- package/src/components/UiDialog/UiDialogClose.vue +1 -1
- package/src/components/UiDropdown/UiDropdownContent.vue +2 -2
- package/src/components/UiDropdown/UiDropdownItem.vue +8 -1
- package/src/components/UiDropdown/UiDropdownTrigger.vue +6 -7
- package/src/components/UiFieldWrapper/UiFieldWrapper.vue +1 -1
- package/src/components/UiInputCheckbox/UiInputCheckbox.vue +2 -2
- package/src/components/UiInputEmoji/UiInputEmoji.vue +173 -0
- package/src/components/UiInputEmoji/index.ts +1 -0
- package/src/components/UiInputRadio/UiInputRadio.vue +374 -0
- package/src/components/UiInputRadio/UiInputRadioGroup.vue +156 -0
- package/src/components/UiInputRadio/index.ts +3 -0
- package/src/components/UiInputRadio/types.ts +25 -0
- package/src/components/UiLoader/UiLoader.vue +8 -10
- package/src/components/UiLogo/UiLogo.vue +41 -7
- package/src/components/UiMenu/UiMenuContent.vue +1 -1
- package/src/components/UiMenu/UiMenuGroup.vue +1 -1
- package/src/components/UiMenu/UiMenuItem.vue +1 -1
- package/src/components/UiMenu/UiMenuSubContent.vue +1 -1
- package/src/components/UiMenu/UiMenuSubTrigger.vue +2 -2
- package/src/components/UiPage/UiPage.vue +102 -34
- package/src/components/UiPageHeader/UiPageHeader.vue +29 -9
- package/src/components/UiRoomButton/UiRoomButton.vue +4 -4
- package/src/components/UiShell/UiShell.vue +1 -1
- package/src/components/UiSideMenu/UiSideMenu.vue +13 -10
- package/src/components/UiSideMenu/UiSideMenuItemContent.vue +2 -2
- package/src/components/UiStepper/UiStepperIndicator.vue +8 -11
- package/src/components/UiSwitch/UiSwitch.vue +359 -0
- package/src/components/UiSwitch/index.ts +2 -0
- package/src/components/UiSwitch/types.ts +5 -0
- package/src/components/UiTabs/UiTabs.vue +7 -4
- package/src/components/UiTextField/UiTextField.vue +6 -11
- package/src/components/UiTimeline/README.md +6 -6
- package/src/components/UiTimeline/UiTimelineItem.vue +3 -3
- package/src/composables/useContentMode.ts +53 -0
- package/src/composables/useIconSize.ts +28 -0
- package/src/constants/sizes.ts +1 -1
- package/src/global.d.ts +8 -0
- package/dist/UiTimelineItem.vue_vue_type_style_index_0_lang-DJFdCn2R.mjs +0 -5456
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
2
|
+
import type { AcceptableValue } from 'reka-ui'
|
|
3
|
+
|
|
4
|
+
export type RadioSize = 'sm' | 'md' | 'lg'
|
|
5
|
+
|
|
6
|
+
export type RadioVariant = 'default' | 'card'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Valid radio option values. `null` is a supported option value;
|
|
10
|
+
* `undefined` means "no selection". Object values must be reference-stable.
|
|
11
|
+
*/
|
|
12
|
+
export type RadioValue = AcceptableValue
|
|
13
|
+
|
|
14
|
+
export interface RadioGroupContext {
|
|
15
|
+
/** Current group value (kept in sync in both controlled and uncontrolled mode) */
|
|
16
|
+
modelValue: Readonly<Ref<RadioValue | undefined>>
|
|
17
|
+
error: Readonly<Ref<boolean>>
|
|
18
|
+
size: Readonly<Ref<RadioSize>>
|
|
19
|
+
disabled: Readonly<Ref<boolean>>
|
|
20
|
+
/** id of the group error message, for aria-describedby on items */
|
|
21
|
+
messageId: Readonly<Ref<string | undefined>>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const RADIO_GROUP_KEY: InjectionKey<RadioGroupContext> =
|
|
25
|
+
Symbol('ui-input-radio-group')
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<svg
|
|
4
4
|
:class="[
|
|
5
5
|
sizeClass,
|
|
6
|
-
'text-
|
|
6
|
+
'text-actions-neutral-idle animate-spin',
|
|
7
7
|
]"
|
|
8
8
|
xmlns="http://www.w3.org/2000/svg"
|
|
9
9
|
fill="none"
|
|
@@ -26,20 +26,18 @@
|
|
|
26
26
|
</div>
|
|
27
27
|
</template>
|
|
28
28
|
<script lang="ts" setup>
|
|
29
|
-
import {
|
|
29
|
+
import { SIZES, type Size } from '../../constants'
|
|
30
|
+
import { useIconSize } from '../../composables/useIconSize'
|
|
30
31
|
|
|
31
32
|
const props = defineProps({
|
|
32
33
|
size: {
|
|
33
|
-
type: String,
|
|
34
|
+
type: String as () => Size,
|
|
34
35
|
default: 'md',
|
|
35
|
-
validator: (value:
|
|
36
|
+
validator: (value: Size) => SIZES.includes(value),
|
|
36
37
|
},
|
|
37
38
|
})
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (props.size === 'lg') return 'size-8'
|
|
43
|
-
return 'size-6'
|
|
44
|
-
})
|
|
40
|
+
// Same ladder as the icons the spinner stands in for, so a button swapping its
|
|
41
|
+
// icon for the loader doesn't resize the content and shift the label.
|
|
42
|
+
const sizeClass = useIconSize(() => props.size)
|
|
45
43
|
</script>
|
|
@@ -5,6 +5,7 @@ import PulseVariant from "./variants/PulseVariant.vue";
|
|
|
5
5
|
|
|
6
6
|
// CSS/svg remake of the lottie tornado logo (UiAnimatedLogo), with themable
|
|
7
7
|
// colors. Animations live in variants/.
|
|
8
|
+
// The viewBox hugs the badge, so height/width size the visible logo.
|
|
8
9
|
// A variant renders the .badge/.mark paths, keeps its keyframes in its own
|
|
9
10
|
// scoped style, and tags animated elements with .motion — then goes in the
|
|
10
11
|
// VARIANTS map.
|
|
@@ -17,10 +18,15 @@ interface Props {
|
|
|
17
18
|
width: number;
|
|
18
19
|
/** Follow the brand palette (--color-brand-800) instead of the Livestorm navy. */
|
|
19
20
|
themed?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Dark badge + white mark, for light backgrounds. `"auto"` inverts
|
|
23
|
+
* whenever the logo sits inside a light `data-colors-semantic` scope.
|
|
24
|
+
*/
|
|
25
|
+
inverted?: boolean | "auto";
|
|
22
26
|
/** When false, renders the static logo (the resting pose). */
|
|
23
27
|
animated?: boolean;
|
|
28
|
+
/** Accessible name announced for the logo. */
|
|
29
|
+
label?: string;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
// vue casts absent boolean props to false, hence the explicit defaults
|
|
@@ -29,6 +35,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
29
35
|
themed: false,
|
|
30
36
|
inverted: false,
|
|
31
37
|
animated: true,
|
|
38
|
+
label: "Livestorm",
|
|
32
39
|
});
|
|
33
40
|
|
|
34
41
|
const VARIANTS = {
|
|
@@ -39,10 +46,17 @@ const VARIANTS = {
|
|
|
39
46
|
const variantComponent = computed(() => VARIANTS[props.variant]);
|
|
40
47
|
|
|
41
48
|
const markColor = computed(() =>
|
|
42
|
-
props.themed ? "var(--color-brand-800, #
|
|
49
|
+
props.themed ? "var(--color-brand-800, #12262B)" : "#12262B",
|
|
43
50
|
);
|
|
44
51
|
|
|
45
52
|
const colorVars = computed(() => {
|
|
53
|
+
if (props.inverted === "auto") {
|
|
54
|
+
// colors live in the scope-aware rules below
|
|
55
|
+
return props.themed
|
|
56
|
+
? { "--ui-logo-auto-mark": "var(--color-brand-800, #12262B)" }
|
|
57
|
+
: {};
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
if (props.inverted) {
|
|
47
61
|
return {
|
|
48
62
|
"--ui-logo-badge": markColor.value,
|
|
@@ -63,15 +77,18 @@ const colorVars = computed(() => {
|
|
|
63
77
|
<div class="ui-logo-loader">
|
|
64
78
|
<svg
|
|
65
79
|
class="ui-logo"
|
|
66
|
-
:class="{
|
|
80
|
+
:class="{
|
|
81
|
+
'ui-logo--static': !props.animated,
|
|
82
|
+
'ui-logo--invert-auto': props.inverted === 'auto',
|
|
83
|
+
}"
|
|
67
84
|
:style="{
|
|
68
85
|
height: props.height + 'rem',
|
|
69
86
|
width: props.width + 'rem',
|
|
70
87
|
...colorVars,
|
|
71
88
|
}"
|
|
72
|
-
viewBox="
|
|
89
|
+
viewBox="67.5 67.5 465 465"
|
|
73
90
|
role="img"
|
|
74
|
-
aria-label="
|
|
91
|
+
:aria-label="props.label"
|
|
75
92
|
>
|
|
76
93
|
<component :is="variantComponent" />
|
|
77
94
|
</svg>
|
|
@@ -84,13 +101,30 @@ const colorVars = computed(() => {
|
|
|
84
101
|
}
|
|
85
102
|
|
|
86
103
|
.ui-logo :deep(.mark) {
|
|
87
|
-
fill: var(--ui-logo-mark, #
|
|
104
|
+
fill: var(--ui-logo-mark, #12262B);
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
.ui-logo--static :deep(.motion) {
|
|
91
108
|
animation: none;
|
|
92
109
|
}
|
|
93
110
|
|
|
111
|
+
/* inverted="auto": the nearest data-colors-semantic scope decides.
|
|
112
|
+
Handles one level of scope nesting — all the app has. */
|
|
113
|
+
.ui-logo--invert-auto {
|
|
114
|
+
--ui-logo-badge: #ffffff;
|
|
115
|
+
--ui-logo-mark: var(--ui-logo-auto-mark, #12262B);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
[data-colors-semantic="light"] .ui-logo--invert-auto {
|
|
119
|
+
--ui-logo-badge: var(--ui-logo-auto-mark, #12262B);
|
|
120
|
+
--ui-logo-mark: #ffffff;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
[data-colors-semantic="light"] [data-colors-semantic="dark"] .ui-logo--invert-auto {
|
|
124
|
+
--ui-logo-badge: #ffffff;
|
|
125
|
+
--ui-logo-mark: var(--ui-logo-auto-mark, #12262B);
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
@media (prefers-reduced-motion: reduce) {
|
|
95
129
|
.ui-logo :deep(.motion) {
|
|
96
130
|
animation: none;
|
|
@@ -16,7 +16,7 @@ const forwarded = useForwardPropsEmits(props, emits)
|
|
|
16
16
|
<DropdownMenuPortal>
|
|
17
17
|
<DropdownMenuContent
|
|
18
18
|
v-bind="{ ...forwarded, ...$attrs }"
|
|
19
|
-
class="bg-surface-neutral-bg-main rounded-xl border border-borders-neutral-light shadow-lg p-1 z-50"
|
|
19
|
+
class="bg-surface-neutral-bg-main rounded-xl border border-borders-neutral-light shadow-lg p-1.5 flex flex-col gap-0.5 z-50"
|
|
20
20
|
data-colors-semantic="light"
|
|
21
21
|
>
|
|
22
22
|
<slot />
|
|
@@ -10,7 +10,7 @@ defineProps<{
|
|
|
10
10
|
<DropdownMenuGroup v-bind="$attrs">
|
|
11
11
|
<DropdownMenuLabel
|
|
12
12
|
v-if="label"
|
|
13
|
-
class="text-content-legends-bold-md text-text-neutral-
|
|
13
|
+
class="text-content-legends-bold-md text-text-neutral-secondary px-3 pt-3 pb-1"
|
|
14
14
|
>
|
|
15
15
|
{{ label }}
|
|
16
16
|
</DropdownMenuLabel>
|
|
@@ -11,7 +11,7 @@ const forwarded = useForwardPropsEmits(props, emits)
|
|
|
11
11
|
<template>
|
|
12
12
|
<DropdownMenuItem
|
|
13
13
|
v-bind="{ ...forwarded, ...$attrs }"
|
|
14
|
-
class="w-full flex items-center gap-2 px-3 py-
|
|
14
|
+
class="w-full flex items-center gap-2 min-h-10 px-3 py-1.5 rounded-lg hover:bg-actions-neutral-hover-overlay data-[highlighted]:bg-actions-neutral-hover-overlay transition-colors text-left outline-none cursor-pointer text-content-text-md-regular text-text-neutral-base"
|
|
15
15
|
>
|
|
16
16
|
<slot />
|
|
17
17
|
</DropdownMenuItem>
|
|
@@ -14,7 +14,7 @@ const forwarded = useForwardPropsEmits(props, emits)
|
|
|
14
14
|
<DropdownMenuPortal>
|
|
15
15
|
<DropdownMenuSubContent
|
|
16
16
|
v-bind="{ ...forwarded, ...$attrs }"
|
|
17
|
-
class="bg-surface-neutral-bg-main rounded-xl border border-borders-neutral-light shadow-lg p-1 z-50"
|
|
17
|
+
class="bg-surface-neutral-bg-main rounded-xl border border-borders-neutral-light shadow-lg p-1.5 flex flex-col gap-0.5 z-50"
|
|
18
18
|
data-colors-semantic="light"
|
|
19
19
|
>
|
|
20
20
|
<slot />
|
|
@@ -9,9 +9,9 @@ const forwarded = useForwardProps(props)
|
|
|
9
9
|
<template>
|
|
10
10
|
<DropdownMenuSubTrigger
|
|
11
11
|
v-bind="{ ...forwarded, ...$attrs }"
|
|
12
|
-
class="w-full flex items-center justify-between gap-3 px-3 py-
|
|
12
|
+
class="w-full flex items-center justify-between gap-3 min-h-10 px-3 py-1.5 rounded-lg hover:bg-actions-neutral-hover-overlay data-[highlighted]:bg-actions-neutral-hover-overlay data-[state=open]:bg-actions-neutral-hover-overlay transition-colors text-left outline-none cursor-pointer"
|
|
13
13
|
>
|
|
14
14
|
<slot />
|
|
15
|
-
<span class="i-
|
|
15
|
+
<span class="i-ph-caret-right w-4 h-4 text-text-neutral-tertiary shrink-0" />
|
|
16
16
|
</DropdownMenuSubTrigger>
|
|
17
17
|
</template>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onBeforeUnmount,
|
|
2
|
+
import { computed, onBeforeUnmount, ref, watch, getCurrentInstance, type Slot } from 'vue'
|
|
3
3
|
import { BREAKPOINT_LG } from '../../utils/breakpoints'
|
|
4
4
|
import { useResponsive } from '../../utils/responsive'
|
|
5
|
-
import
|
|
5
|
+
import { useContentMode } from '../../composables/useContentMode'
|
|
6
6
|
import { useMobileMenu } from '../../composables/useMobileMenu'
|
|
7
7
|
import UiLoader from '../UiLoader/UiLoader.vue'
|
|
8
8
|
|
|
@@ -36,6 +36,24 @@ import UiLoader from '../UiLoader/UiLoader.vue'
|
|
|
36
36
|
// │ │ └─────────────────────────────────────────────────┘ │
|
|
37
37
|
// └─┴─────────────────────────────────────────────────────┘
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Headroom sizes, in px: the gap above the content card that lets the shell
|
|
41
|
+
* background show through. One map so the page frame has a single source of
|
|
42
|
+
* truth — retune here rather than at call sites.
|
|
43
|
+
*/
|
|
44
|
+
const HEADROOM_GAP_PX = {
|
|
45
|
+
none: 0,
|
|
46
|
+
sm: 4,
|
|
47
|
+
md: 10,
|
|
48
|
+
lg: 16,
|
|
49
|
+
xl: 24,
|
|
50
|
+
} as const
|
|
51
|
+
|
|
52
|
+
type HeadroomGap = keyof typeof HEADROOM_GAP_PX
|
|
53
|
+
|
|
54
|
+
/** Used when the card sits flush at the top and no size is given. */
|
|
55
|
+
const DEFAULT_HEADROOM_GAP: HeadroomGap = 'md'
|
|
56
|
+
|
|
39
57
|
const props = withDefaults(defineProps<{
|
|
40
58
|
/**
|
|
41
59
|
* Extra classes for the inner content `<section>`. Use to override the
|
|
@@ -50,9 +68,30 @@ const props = withDefaults(defineProps<{
|
|
|
50
68
|
* menu row — e.g. "Home" — without reaching into useMobileMenu directly.
|
|
51
69
|
*/
|
|
52
70
|
title?: string
|
|
71
|
+
/**
|
|
72
|
+
* The shape of the page inside the content card.
|
|
73
|
+
* - `document` (default): the card scrolls as a single column. The common
|
|
74
|
+
* dashboard page — lists, forms, detail views.
|
|
75
|
+
* - `app`: the card fills the available height and never scrolls itself;
|
|
76
|
+
* the page owns its own scroll regions. For editor-style pages (split
|
|
77
|
+
* panes, canvases) that would otherwise opt out of UiPage entirely.
|
|
78
|
+
*/
|
|
79
|
+
variant?: 'document' | 'app'
|
|
80
|
+
/**
|
|
81
|
+
* Headroom above the content card, letting the shell background show
|
|
82
|
+
* through. Sizes map to px via `HEADROOM_GAP_PX`.
|
|
83
|
+
*
|
|
84
|
+
* Left unset, the card gets `DEFAULT_HEADROOM_GAP` only when it sits flush
|
|
85
|
+
* at the top — i.e. there is no page header or left slot already offsetting
|
|
86
|
+
* it. Naming a size applies it in either case, and `none` removes the gap
|
|
87
|
+
* on a page that would otherwise get the default.
|
|
88
|
+
*/
|
|
89
|
+
headroomGap?: HeadroomGap
|
|
53
90
|
}>(), {
|
|
54
91
|
pageContentClasses: '',
|
|
55
92
|
title: undefined,
|
|
93
|
+
variant: 'document',
|
|
94
|
+
headroomGap: undefined,
|
|
56
95
|
})
|
|
57
96
|
|
|
58
97
|
const slots = defineSlots<{
|
|
@@ -65,6 +104,12 @@ const slots = defineSlots<{
|
|
|
65
104
|
default?(): Slot
|
|
66
105
|
}>()
|
|
67
106
|
|
|
107
|
+
// An `app` page manages its own scrolling, so every level from the page
|
|
108
|
+
// content down to the card must stop scrolling and stop growing past the
|
|
109
|
+
// viewport — otherwise a split-pane editor gets a second, outer scrollbar.
|
|
110
|
+
const isApp = computed(() => props.variant === 'app')
|
|
111
|
+
const scrollClass = computed(() => (isApp.value ? 'overflow-hidden' : 'overflow-y-auto'))
|
|
112
|
+
|
|
68
113
|
// Check for slot presence
|
|
69
114
|
const bannerSlot = computed(() => !!slots.banner)
|
|
70
115
|
const pageHeaderSlot = computed(() => !!slots.pageHeader)
|
|
@@ -73,6 +118,38 @@ const leftSlot = computed(() => !!slots.leftSlot)
|
|
|
73
118
|
const headerSlot = computed(() => !!slots.header)
|
|
74
119
|
const footerSlot = computed(() => !!slots.footer)
|
|
75
120
|
|
|
121
|
+
// Row track for the card grid. An `app` page needs its main row to take the
|
|
122
|
+
// leftover height and be allowed to shrink (minmax(0,1fr)), so nested
|
|
123
|
+
// overflow containers resolve against a real height instead of growing.
|
|
124
|
+
// Spelled out as literals: Tailwind only generates classes it can see as
|
|
125
|
+
// complete strings in the source.
|
|
126
|
+
const cardRowsClass = computed(() => {
|
|
127
|
+
if (!isApp.value) { return headerSlot.value ? 'grid-rows-[min-content]' : '' }
|
|
128
|
+
if (headerSlot.value && footerSlot.value) { return 'grid-rows-[min-content_minmax(0,1fr)_min-content]' }
|
|
129
|
+
if (headerSlot.value) { return 'grid-rows-[min-content_minmax(0,1fr)]' }
|
|
130
|
+
if (footerSlot.value) { return 'grid-rows-[minmax(0,1fr)_min-content]' }
|
|
131
|
+
return 'grid-rows-[minmax(0,1fr)]'
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// A named size always wins; otherwise fall back to the default, and only when
|
|
135
|
+
// the card is flush at the top (a page header or left slot already provides
|
|
136
|
+
// the offset). Applied as an inline style rather than a utility so the value
|
|
137
|
+
// does not depend on Tailwind's `--spacing` fallback resolving to 0.25rem.
|
|
138
|
+
// A zero gap emits no style at all, so `none` and "already offset" produce
|
|
139
|
+
// identical markup.
|
|
140
|
+
//
|
|
141
|
+
// Padding on the clipping wrapper rather than a margin on the card: the card
|
|
142
|
+
// is `h-full`, so a margin would push a full-height box down inside a fixed
|
|
143
|
+
// height, overflowing by exactly the gap and clipping that much off the
|
|
144
|
+
// bottom. As padding it shrinks the box the card measures itself against.
|
|
145
|
+
const headroomGapStyle = computed(() => {
|
|
146
|
+
const size = props.headroomGap ?? (
|
|
147
|
+
!pageHeaderSlot.value && !leftSlot.value ? DEFAULT_HEADROOM_GAP : 'none'
|
|
148
|
+
)
|
|
149
|
+
const px = HEADROOM_GAP_PX[size]
|
|
150
|
+
return px ? { paddingTop: `${px}px` } : undefined
|
|
151
|
+
})
|
|
152
|
+
|
|
76
153
|
// Feed the page title to UiShell's fallback mobile menu topbar. Watched so a
|
|
77
154
|
// reactive title updates live, and cleared on unmount so it doesn't leak to
|
|
78
155
|
// the next route.
|
|
@@ -92,28 +169,9 @@ const displayLeftSlot = computed(() => responsive.value.width >= BREAKPOINT_LG)
|
|
|
92
169
|
const UiPage = ref<HTMLElement | undefined>()
|
|
93
170
|
const PageContent = ref<HTMLElement | undefined>()
|
|
94
171
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// the internal theming tooling is enabled, so the stored content mode
|
|
99
|
-
// (driven by the ThemePicker) can apply; for everyone else the content
|
|
100
|
-
// stays light regardless of what localStorage may hold.
|
|
101
|
-
// Read in onMounted, not at setup: setup also runs during SSR (no
|
|
102
|
-
// localStorage), and server + client-hydration markup must be identical —
|
|
103
|
-
// the pin lifts right after mount for the internal tooling instead.
|
|
104
|
-
const isInternalThemingEnabled = ref(false)
|
|
105
|
-
|
|
106
|
-
onMounted(() => {
|
|
107
|
-
isInternalThemingEnabled.value = localStorage.getItem('ls-internal-theming') === 'true'
|
|
108
|
-
|
|
109
|
-
addElementToWatch(
|
|
110
|
-
'content',
|
|
111
|
-
{
|
|
112
|
-
selector: '[data-name="ui-page-content"]',
|
|
113
|
-
element: PageContent.value!,
|
|
114
|
-
...(isInternalThemingEnabled.value ? {} : { modeOverride: 'light' }),
|
|
115
|
-
})
|
|
116
|
-
})
|
|
172
|
+
// Follow the stored content mode via the shared composable (also used by the
|
|
173
|
+
// settings shell), so the page content's data-colors-semantic stays in sync.
|
|
174
|
+
const { contentMode } = useContentMode(PageContent, '[data-name="ui-page-content"]')
|
|
117
175
|
|
|
118
176
|
</script>
|
|
119
177
|
|
|
@@ -137,7 +195,7 @@ onMounted(() => {
|
|
|
137
195
|
v-if="pageHeaderSlot"
|
|
138
196
|
ref="pageHeader"
|
|
139
197
|
data-name="pageHeader"
|
|
140
|
-
class="page-header p-4 md:
|
|
198
|
+
class="page-header p-4 md:px-8 md:py-6 md:pl-0 relative"
|
|
141
199
|
>
|
|
142
200
|
<slot name="pageHeader" />
|
|
143
201
|
</section>
|
|
@@ -155,8 +213,11 @@ onMounted(() => {
|
|
|
155
213
|
<section
|
|
156
214
|
ref="PageContent"
|
|
157
215
|
data-name="pageContent"
|
|
158
|
-
class="
|
|
159
|
-
|
|
216
|
+
:class="[
|
|
217
|
+
'page-content flex-1 w-full grid has-[>:nth-child(2)]:lg:grid-cols-[1fr_2fr] lg:gap-x-8 min-h-0',
|
|
218
|
+
scrollClass,
|
|
219
|
+
]"
|
|
220
|
+
:data-colors-semantic="contentMode"
|
|
160
221
|
>
|
|
161
222
|
<aside
|
|
162
223
|
v-if="leftSlot && displayLeftSlot"
|
|
@@ -166,14 +227,15 @@ onMounted(() => {
|
|
|
166
227
|
<slot name="leftSlot" />
|
|
167
228
|
</aside>
|
|
168
229
|
|
|
169
|
-
<div
|
|
230
|
+
<div
|
|
231
|
+
class="min-w-0 overflow-hidden rounded-tl-xl"
|
|
232
|
+
:style="headroomGapStyle"
|
|
233
|
+
>
|
|
170
234
|
<section
|
|
171
235
|
:class="[
|
|
172
|
-
'content min-w-0
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
!pageHeaderSlot && !leftSlot ? 'mt-1' : '',
|
|
176
|
-
headerSlot ? 'grid-rows-[min-content]' : '',
|
|
236
|
+
'content min-w-0 h-full bg-surface-neutral-bg-main grid shadow-shadows-drop-shadow-2xl rounded-tl-xl',
|
|
237
|
+
scrollClass,
|
|
238
|
+
cardRowsClass,
|
|
177
239
|
props.pageContentClasses,
|
|
178
240
|
]"
|
|
179
241
|
data-name="ui-shell-content"
|
|
@@ -188,7 +250,13 @@ onMounted(() => {
|
|
|
188
250
|
|
|
189
251
|
<section
|
|
190
252
|
data-name="main-content"
|
|
191
|
-
class="
|
|
253
|
+
:class="[
|
|
254
|
+
'route-content w-full min-w-0 flex flex-col',
|
|
255
|
+
// Let the row shrink so an editor's own overflow containers
|
|
256
|
+
// resolve against a real height. Scoped to `app` so the 15
|
|
257
|
+
// existing document pages keep their current growth behaviour.
|
|
258
|
+
isApp ? 'min-h-0' : '',
|
|
259
|
+
]"
|
|
192
260
|
>
|
|
193
261
|
<slot>
|
|
194
262
|
<UiLoader />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onBeforeUnmount, useSlots, watch, type Slot } from 'vue'
|
|
3
|
-
import LivestormLogo from '@livestorm/ui-tokens/output/vectors/logos/livestorm-white.svg'
|
|
4
3
|
import UiButton from '../UiButton/UiButton.vue'
|
|
4
|
+
import UiLogo from '../UiLogo/UiLogo.vue'
|
|
5
5
|
import { useMobileMenu } from '../../composables/useMobileMenu'
|
|
6
6
|
import type { RouteLocationRaw } from 'vue-router'
|
|
7
7
|
|
|
@@ -65,8 +65,15 @@ onBeforeUnmount(() => syncTrigger(false))
|
|
|
65
65
|
</script>
|
|
66
66
|
|
|
67
67
|
<template>
|
|
68
|
+
<!--
|
|
69
|
+
min-h-10 keeps the header a stable height whether or not a page fills the
|
|
70
|
+
action slots. Without it the row is sized by its tallest child — a bare
|
|
71
|
+
title is 32px but an action button is 40px — so the header, and the content
|
|
72
|
+
card below it, shifted by 8px between pages like Events and People.
|
|
73
|
+
A floor, not a fixed height: taller or wrapped content still grows.
|
|
74
|
+
-->
|
|
68
75
|
<div
|
|
69
|
-
class="flex flex-1 min-w-0 justify-between gap-5 md:gap-8 items-center"
|
|
76
|
+
class="flex flex-1 min-w-0 justify-between gap-5 md:gap-8 items-center min-h-10"
|
|
70
77
|
>
|
|
71
78
|
<div class="flex min-w-0 items-center gap-2">
|
|
72
79
|
<div v-if="showLogo">
|
|
@@ -74,11 +81,13 @@ onBeforeUnmount(() => syncTrigger(false))
|
|
|
74
81
|
:to="logoTo ? logoTo : '#'"
|
|
75
82
|
class="grid items-center w-8 h-8"
|
|
76
83
|
>
|
|
77
|
-
<
|
|
78
|
-
:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
<UiLogo
|
|
85
|
+
:height="1.5"
|
|
86
|
+
:width="1.5"
|
|
87
|
+
:themed="false"
|
|
88
|
+
:animated="false"
|
|
89
|
+
inverted="auto"
|
|
90
|
+
/>
|
|
82
91
|
</router-link>
|
|
83
92
|
</div>
|
|
84
93
|
<div
|
|
@@ -88,7 +97,7 @@ onBeforeUnmount(() => syncTrigger(false))
|
|
|
88
97
|
<UiButton
|
|
89
98
|
color="neutral"
|
|
90
99
|
variant="tertiary"
|
|
91
|
-
icon="i-
|
|
100
|
+
icon="i-ph-list"
|
|
92
101
|
@click="toggle"
|
|
93
102
|
/>
|
|
94
103
|
</div>
|
|
@@ -96,7 +105,7 @@ onBeforeUnmount(() => syncTrigger(false))
|
|
|
96
105
|
<UiButton
|
|
97
106
|
color="neutral"
|
|
98
107
|
variant="tertiary"
|
|
99
|
-
icon="i-
|
|
108
|
+
icon="i-ph-arrow-left"
|
|
100
109
|
/>
|
|
101
110
|
</div>
|
|
102
111
|
<div
|
|
@@ -124,5 +133,16 @@ onBeforeUnmount(() => syncTrigger(false))
|
|
|
124
133
|
>
|
|
125
134
|
<slot name="slotB" />
|
|
126
135
|
</div>
|
|
136
|
+
|
|
137
|
+
<!--
|
|
138
|
+
Teleport target for page-level actions, so a component deep inside the
|
|
139
|
+
page content (e.g. a settings section) can surface its primary action
|
|
140
|
+
here without threading props up. `contents` keeps the empty target out
|
|
141
|
+
of the flex layout, so pages that don't use it are unaffected.
|
|
142
|
+
-->
|
|
143
|
+
<div
|
|
144
|
+
id="ui-page-header-actions-teleport-target"
|
|
145
|
+
class="contents"
|
|
146
|
+
/>
|
|
127
147
|
</div>
|
|
128
148
|
</template>
|
|
@@ -57,7 +57,7 @@ const isIconOnly = computed(() => !slots.default);
|
|
|
57
57
|
>
|
|
58
58
|
<svg
|
|
59
59
|
v-if="loading"
|
|
60
|
-
class="
|
|
60
|
+
class="size-5 shrink-0 animate-spin"
|
|
61
61
|
xmlns="http://www.w3.org/2000/svg"
|
|
62
62
|
fill="none"
|
|
63
63
|
viewBox="0 0 24 24"
|
|
@@ -78,7 +78,7 @@ const isIconOnly = computed(() => !slots.default);
|
|
|
78
78
|
</svg>
|
|
79
79
|
<div
|
|
80
80
|
v-else-if="icon"
|
|
81
|
-
class="
|
|
81
|
+
class="size-5 shrink-0"
|
|
82
82
|
:class="icon"
|
|
83
83
|
/>
|
|
84
84
|
<slot />
|
|
@@ -117,7 +117,7 @@ const isIconOnly = computed(() => !slots.default);
|
|
|
117
117
|
>
|
|
118
118
|
<svg
|
|
119
119
|
v-if="loading"
|
|
120
|
-
class="
|
|
120
|
+
class="size-5 shrink-0 animate-spin"
|
|
121
121
|
xmlns="http://www.w3.org/2000/svg"
|
|
122
122
|
fill="none"
|
|
123
123
|
viewBox="0 0 24 24"
|
|
@@ -138,7 +138,7 @@ const isIconOnly = computed(() => !slots.default);
|
|
|
138
138
|
</svg>
|
|
139
139
|
<div
|
|
140
140
|
v-else-if="icon"
|
|
141
|
-
class="
|
|
141
|
+
class="size-5 shrink-0"
|
|
142
142
|
:class="icon"
|
|
143
143
|
/>
|
|
144
144
|
<slot />
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import LivestormLogo from '@livestorm/ui-tokens/output/vectors/logos/livestorm-white.svg'
|
|
3
2
|
import type { Slot } from 'vue'
|
|
4
3
|
import type { SideMenuCollapsedItem, SideMenuItemProps, SideMenuMoreItem } from './types'
|
|
5
4
|
import UiSideMenuItem from './UiSideMenuItem.vue'
|
|
5
|
+
import UiLogo from '../UiLogo/UiLogo.vue'
|
|
6
6
|
import UiButton from '../UiButton/UiButton.vue'
|
|
7
7
|
import UiPopover from '../UiPopover/UiPopover.vue'
|
|
8
8
|
import UiPopoverTrigger from '../UiPopover/UiPopoverTrigger.vue'
|
|
@@ -189,17 +189,20 @@ function handleLogoClick (event: MouseEvent) {
|
|
|
189
189
|
<!-- Top -->
|
|
190
190
|
<div class="flex flex-col md:items-center gap-8">
|
|
191
191
|
<div class="flex items-center justify-between">
|
|
192
|
-
<
|
|
192
|
+
<UiLogo
|
|
193
193
|
v-if="showLivestormLogo"
|
|
194
|
-
:
|
|
195
|
-
:
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
:height="2.5"
|
|
195
|
+
:width="2.5"
|
|
196
|
+
:themed="false"
|
|
197
|
+
:animated="false"
|
|
198
|
+
inverted="auto"
|
|
199
|
+
:label="logoAlt"
|
|
200
|
+
class="ml-4 md:ml-0"
|
|
198
201
|
@click="handleLogoClick"
|
|
199
|
-
|
|
202
|
+
/>
|
|
200
203
|
<div class="md:hidden">
|
|
201
204
|
<UiButton
|
|
202
|
-
icon="i-
|
|
205
|
+
icon="i-ph-x"
|
|
203
206
|
variant="tertiary"
|
|
204
207
|
color="neutral"
|
|
205
208
|
size="lg"
|
|
@@ -289,7 +292,7 @@ function handleLogoClick (event: MouseEvent) {
|
|
|
289
292
|
<UiPopoverTrigger class="w-full cursor-pointer">
|
|
290
293
|
<UiSideMenuItem
|
|
291
294
|
:label="translate('room_bottom-bar_controls-label_more')"
|
|
292
|
-
icon="i-
|
|
295
|
+
icon="i-ph-dots-three"
|
|
293
296
|
:is-active="false"
|
|
294
297
|
:inside-interactive="true"
|
|
295
298
|
:badge="moreBadge"
|
|
@@ -420,7 +423,7 @@ function handleLogoClick (event: MouseEvent) {
|
|
|
420
423
|
<UiPopoverTrigger class="cursor-pointer">
|
|
421
424
|
<UiSideMenuItem
|
|
422
425
|
:label="translate('room_bottom-bar_controls-label_more')"
|
|
423
|
-
icon="i-
|
|
426
|
+
icon="i-ph-dots-three-vertical"
|
|
424
427
|
:is-active="false"
|
|
425
428
|
:inside-interactive="true"
|
|
426
429
|
/>
|
|
@@ -103,7 +103,7 @@ const textAlignClass = computed(() => {
|
|
|
103
103
|
isActive ? 'bg-actions-neutral-active-overlay text-actions-neutral-idle' : 'text-actions-neutral-idle-alpha-strong',
|
|
104
104
|
// Avatar box is 40px; on mobile `p-1` makes its total height (48px)
|
|
105
105
|
// match icon items (`p-3` + 24px icon). Both are 40px on desktop.
|
|
106
|
-
hasAvatarSlot ? 'p-1 md:p-0' : 'p-
|
|
106
|
+
hasAvatarSlot ? 'p-1 md:p-0' : 'p-2.5',
|
|
107
107
|
]"
|
|
108
108
|
>
|
|
109
109
|
<!-- Icon/avatar wrapper: anchors the notification badge to the icon's
|
|
@@ -119,7 +119,7 @@ const textAlignClass = computed(() => {
|
|
|
119
119
|
<div
|
|
120
120
|
v-if="icon"
|
|
121
121
|
:class="[
|
|
122
|
-
'w-6 h-6 md:w-
|
|
122
|
+
'w-6 h-6 md:w-5 md:h-5',
|
|
123
123
|
icon
|
|
124
124
|
]"
|
|
125
125
|
aria-hidden="true"
|