@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.
Files changed (85) hide show
  1. package/README.md +6 -3
  2. package/dist/UiTimelineItem.vue_vue_type_style_index_0_lang-LpdvGf5X.mjs +6093 -0
  3. package/dist/components.mjs +105 -96
  4. package/dist/index.mjs +68 -64
  5. package/dist/layouts.mjs +15 -15
  6. package/dist/types/index.d.ts +5 -1
  7. package/dist/types/src/components/UiActionBar/UiActionBar.vue.d.ts +3 -1
  8. package/dist/types/src/components/UiActionBar/UiActionBarItem.vue.d.ts +18 -3
  9. package/dist/types/src/components/UiActionBar/types.d.ts +3 -1
  10. package/dist/types/src/components/UiButton/UiButton.vue.d.ts +17 -13
  11. package/dist/types/src/components/UiButtonGroup/UiButtonGroupItem.vue.d.ts +3 -0
  12. package/dist/types/src/components/UiButtonGroup/types.d.ts +1 -1
  13. package/dist/types/src/components/UiDropdown/UiDropdownItem.vue.d.ts +6 -4
  14. package/dist/types/src/components/UiInputEmoji/UiInputEmoji.vue.d.ts +45 -0
  15. package/dist/types/src/components/UiInputEmoji/index.d.ts +1 -0
  16. package/dist/types/src/components/UiInputRadio/UiInputRadio.vue.d.ts +67 -0
  17. package/dist/types/src/components/UiInputRadio/UiInputRadioGroup.vue.d.ts +81 -0
  18. package/dist/types/src/components/UiInputRadio/index.d.ts +3 -0
  19. package/dist/types/src/components/UiInputRadio/types.d.ts +19 -0
  20. package/dist/types/src/components/UiLoader/UiLoader.vue.d.ts +6 -5
  21. package/dist/types/src/components/UiLogo/UiLogo.vue.d.ts +9 -3
  22. package/dist/types/src/components/UiPage/UiPage.vue.d.ts +34 -0
  23. package/dist/types/src/components/UiProgress/UiProgress.vue.d.ts +1 -1
  24. package/dist/types/src/components/UiSwitch/UiSwitch.vue.d.ts +109 -0
  25. package/dist/types/src/components/UiSwitch/index.d.ts +2 -0
  26. package/dist/types/src/components/UiSwitch/types.d.ts +3 -0
  27. package/dist/types/src/components/UiTabs/UiTabs.vue.d.ts +5 -2
  28. package/dist/types/src/composables/useContentMode.d.ts +25 -0
  29. package/dist/types/src/composables/useIconSize.d.ts +11 -0
  30. package/dist/types/src/constants/sizes.d.ts +1 -1
  31. package/dist/ui-vue.css +1 -1
  32. package/index.d.ts +4 -0
  33. package/index.js +8 -0
  34. package/package.json +6 -3
  35. package/src/components/Toast/ToastMessage.vue +6 -6
  36. package/src/components/UiActionBar/UiActionBar.vue +11 -2
  37. package/src/components/UiActionBar/UiActionBarItem.vue +56 -32
  38. package/src/components/UiActionBar/types.ts +3 -1
  39. package/src/components/UiAvatar/UiAvatar.vue +1 -1
  40. package/src/components/UiBanner/UiBanner.vue +7 -7
  41. package/src/components/UiButton/UiButton.vue +9 -9
  42. package/src/components/UiButtonGroup/UiButtonGroup.vue +2 -0
  43. package/src/components/UiButtonGroup/UiButtonGroupItem.vue +12 -0
  44. package/src/components/UiButtonGroup/types.ts +1 -1
  45. package/src/components/UiCarousel/UiCarousel.vue +2 -2
  46. package/src/components/UiColorPicker/UiColorPicker.vue +5 -5
  47. package/src/components/UiDialog/MarketplaceHeaderExample.vue +2 -2
  48. package/src/components/UiDialog/UiDialogClose.vue +1 -1
  49. package/src/components/UiDropdown/UiDropdownContent.vue +2 -2
  50. package/src/components/UiDropdown/UiDropdownItem.vue +8 -1
  51. package/src/components/UiDropdown/UiDropdownTrigger.vue +6 -7
  52. package/src/components/UiFieldWrapper/UiFieldWrapper.vue +1 -1
  53. package/src/components/UiInputCheckbox/UiInputCheckbox.vue +2 -2
  54. package/src/components/UiInputEmoji/UiInputEmoji.vue +173 -0
  55. package/src/components/UiInputEmoji/index.ts +1 -0
  56. package/src/components/UiInputRadio/UiInputRadio.vue +374 -0
  57. package/src/components/UiInputRadio/UiInputRadioGroup.vue +156 -0
  58. package/src/components/UiInputRadio/index.ts +3 -0
  59. package/src/components/UiInputRadio/types.ts +25 -0
  60. package/src/components/UiLoader/UiLoader.vue +8 -10
  61. package/src/components/UiLogo/UiLogo.vue +41 -7
  62. package/src/components/UiMenu/UiMenuContent.vue +1 -1
  63. package/src/components/UiMenu/UiMenuGroup.vue +1 -1
  64. package/src/components/UiMenu/UiMenuItem.vue +1 -1
  65. package/src/components/UiMenu/UiMenuSubContent.vue +1 -1
  66. package/src/components/UiMenu/UiMenuSubTrigger.vue +2 -2
  67. package/src/components/UiPage/UiPage.vue +102 -34
  68. package/src/components/UiPageHeader/UiPageHeader.vue +29 -9
  69. package/src/components/UiRoomButton/UiRoomButton.vue +4 -4
  70. package/src/components/UiShell/UiShell.vue +1 -1
  71. package/src/components/UiSideMenu/UiSideMenu.vue +13 -10
  72. package/src/components/UiSideMenu/UiSideMenuItemContent.vue +2 -2
  73. package/src/components/UiStepper/UiStepperIndicator.vue +8 -11
  74. package/src/components/UiSwitch/UiSwitch.vue +359 -0
  75. package/src/components/UiSwitch/index.ts +2 -0
  76. package/src/components/UiSwitch/types.ts +5 -0
  77. package/src/components/UiTabs/UiTabs.vue +7 -4
  78. package/src/components/UiTextField/UiTextField.vue +6 -11
  79. package/src/components/UiTimeline/README.md +6 -6
  80. package/src/components/UiTimeline/UiTimelineItem.vue +3 -3
  81. package/src/composables/useContentMode.ts +53 -0
  82. package/src/composables/useIconSize.ts +28 -0
  83. package/src/constants/sizes.ts +1 -1
  84. package/src/global.d.ts +8 -0
  85. package/dist/UiTimelineItem.vue_vue_type_style_index_0_lang-DJFdCn2R.mjs +0 -5456
@@ -0,0 +1,3 @@
1
+ export { default as UiInputRadio } from './UiInputRadio.vue'
2
+ export { default as UiInputRadioGroup } from './UiInputRadioGroup.vue'
3
+ export type { RadioSize, RadioValue, RadioVariant } from './types'
@@ -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-action-neutral-idle animate-spin',
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 { computed } from 'vue'
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: string) => ['sm', 'md', 'lg'].includes(value),
36
+ validator: (value: Size) => SIZES.includes(value),
36
37
  },
37
38
  })
38
39
 
39
- const sizeClass = computed(() => {
40
- if (props.size === 'sm') return 'size-4'
41
- if (props.size === 'md') return 'size-6'
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
- /** Dark badge + white mark, for light backgrounds. */
21
- inverted?: boolean;
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, #112660)" : "#112660",
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="{ 'ui-logo--static': !props.animated }"
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="0 0 600 600"
89
+ viewBox="67.5 67.5 465 465"
73
90
  role="img"
74
- aria-label="Livestorm"
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, #112660);
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-tertiary px-3 pt-3 pb-1"
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-2 rounded-lg hover:bg-actions-primary-hover-overlay data-[highlighted]:bg-actions-primary-hover-overlay transition-colors text-left outline-none cursor-pointer text-content-text-md-regular text-text-neutral-base"
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-2 rounded-lg hover:bg-actions-primary-hover-overlay data-[highlighted]:bg-actions-primary-hover-overlay data-[state=open]:bg-actions-primary-hover-overlay transition-colors text-left outline-none cursor-pointer"
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-ls-chevron-right w-4 h-4 text-text-neutral-tertiary shrink-0" />
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, onMounted, ref, watch, getCurrentInstance, type Slot } from 'vue'
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 useThemeAndModes from '../../composables/useThemeAndModes'
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
- const { addElementToWatch } = useThemeAndModes()
96
-
97
- // The page content historically forces light mode. Only lift that pin when
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:p-8 md:pl-0 relative"
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="page-content flex-1 overflow-y-auto w-full grid has-[>:nth-child(2)]:lg:grid-cols-[1fr_2fr] lg:gap-x-8 min-h-0"
159
- :data-colors-semantic="isInternalThemingEnabled ? undefined : 'light'"
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 class="min-w-0 overflow-hidden rounded-tl-xl">
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 overflow-y-auto h-full bg-surface-neutral-bg-main grid shadow-shadows-drop-shadow-2xl rounded-tl-xl',
173
- // Nudge the content down only when it sits flush at the top — i.e.
174
- // no page header and no left slot to align against.
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="route-content w-full min-w-0 flex flex-col"
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
- <img
78
- :src="LivestormLogo"
79
- alt="Livestorm logo"
80
- class="w-6 h-6"
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-ls-menu"
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-ls-back"
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="h-4 w-4 shrink-0 animate-spin"
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="h-4 w-4 shrink-0"
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="h-4 w-4 shrink-0 animate-spin"
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="h-4 w-4 shrink-0"
141
+ class="size-5 shrink-0"
142
142
  :class="icon"
143
143
  />
144
144
  <slot />
@@ -69,7 +69,7 @@ const fallbackLabel = computed(() => menuLabel.value ?? props.openMenuLabel)
69
69
  <UiButton
70
70
  color="neutral"
71
71
  variant="tertiary"
72
- icon="i-ls-menu"
72
+ icon="i-ph-list"
73
73
  :aria-label="fallbackLabel"
74
74
  @click="toggle"
75
75
  />
@@ -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
- <img
192
+ <UiLogo
193
193
  v-if="showLivestormLogo"
194
- :src="LivestormLogo"
195
- :alt="logoAlt"
196
- class="w-10 h-10 ml-4 md:ml-0"
197
- role="img"
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-ls-x-stream"
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-ls-more-horizontal"
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-ls-more-vertical"
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-3',
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-4 md:h-4',
122
+ 'w-6 h-6 md:w-5 md:h-5',
123
123
  icon
124
124
  ]"
125
125
  aria-hidden="true"