Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app/src/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
@import "@nuxt/ui";
@plugin "@tailwindcss/typography";

@source "../../../../module/src/**/*.ts";

@source "../../**";

@source inline('ring-orange-200 hover:ring-orange-300 hover:dark:ring-orange-700');
Expand Down
21 changes: 11 additions & 10 deletions src/app/src/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStudio } from '../composables/useStudio'
import { useStudioState } from '../composables/useStudioState'
import { useStudioUI } from '../composables/useStudioUI'
import type { DropdownMenuItem } from '@nuxt/ui/runtime/components/DropdownMenu.vue.d.ts'

const { ui, host, gitProvider } = useStudio()
const { ui: studioUI, host, gitProvider } = useStudio()
const { devMode, preferences, updatePreference, unsetActiveLocation } = useStudioState()
const user = host.user.get()
const { t } = useI18n()
Expand Down Expand Up @@ -55,17 +56,17 @@ const syncTooltipText = computed(() => {

function closeStudio() {
unsetActiveLocation()
ui.close()
studioUI.close()
}

const { ui } = useStudioUI('footer')
</script>

<template>
<div
class="bg-muted/50 border-default border-t-[0.5px] flex items-center justify-between gap-1.5 px-2 py-2"
>
<div :class="ui.root">
<span
v-if="devMode"
class="ml-2 text-xs text-muted"
:class="ui.text"
>
{{ $t('studio.footer.localFilesystem') }}
</span>
Expand All @@ -90,14 +91,14 @@ function closeStudio() {
</template> -->
<template #debug-mode>
<div
class="w-full"
:class="ui.debugSwitchContainer"
@click.stop
>
<USwitch
:model-value="preferences.debug"
:label="$t('studio.footer.debugMode')"
size="xs"
:ui="{ root: 'w-full flex-row-reverse justify-between', wrapper: 'ms-0' }"
:ui="ui.debugSwitch"
@update:model-value="updatePreference('debug', $event)"
/>
</div>
Expand All @@ -107,12 +108,12 @@ function closeStudio() {
variant="ghost"
size="sm"
:avatar="{ src: user?.avatar, alt: user?.name, size: '2xs' }"
class="px-2 py-1 font-medium"
:class="ui.userMenuButton"
:label="user?.name"
/>
</UDropdownMenu>

<div class="flex items-center">
<div :class="ui.actions">
<UTooltip
:text="syncTooltipText"
:delay-duration="0"
Expand Down
5 changes: 4 additions & 1 deletion src/app/src/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { computed } from 'vue'
import HeaderMain from './header/HeaderMain.vue'
import HeaderReview from './header/HeaderReview.vue'
import HeaderSuccess from './header/HeaderSuccess.vue'
import { useStudioUI } from '../composables/useStudioUI'

const route = useRoute()

Expand All @@ -16,10 +17,12 @@ const currentHeader = computed(() => {
}
return HeaderMain
})

const { ui } = useStudioUI('header')
</script>

<template>
<div class="bg-muted/50 border-default border-b-[0.5px] pr-4 gap-1.5 flex items-center justify-between px-4 h-[45px]">
<div :class="ui.root">
<component :is="currentHeader" />
</div>
</template>
9 changes: 6 additions & 3 deletions src/app/src/components/AppLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import AppHeader from './AppHeader.vue'
import AppFooter from './AppFooter.vue'
import { useSidebar } from '../composables/useSidebar'
import { useStudioUI } from '../composables/useStudioUI'

defineProps<{
title?: string
Expand All @@ -10,6 +11,8 @@ defineProps<{

const { sidebarStyle } = useSidebar()

const { ui } = useStudioUI('layout')

function onBeforeEnter(el: Element) {
const element = el as HTMLElement
element.style.transform = 'translateX(-100%)'
Expand Down Expand Up @@ -49,17 +52,17 @@ function onLeave(el: Element, done: () => void) {
>
<div
v-if="open"
class="fixed top-0 bottom-0 left-0 border-r border-default flex flex-col max-w-full bg-default"
:class="ui.sidebar"
:style="sidebarStyle"
>
<!-- This is needed for the Monaco editor to be able to position the portal correctly -->
<div class="monaco-editor">
<div :class="ui.monaco">
<div id="monaco-portal" />
</div>

<AppHeader />

<div class="flex-1 overflow-y-auto relative">
<div :class="ui.body">
<slot />
</div>

Expand Down
13 changes: 13 additions & 0 deletions src/app/src/composables/useStudioUI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { computed } from 'vue'
import { useStudio } from './useStudio'
import type { StudioUI } from '../types'

export function useStudioUI<K extends keyof StudioUI>(key: K) {
const { host } = useStudio()

const ui = computed(() => {
return host.config.studio.ui[key]
})

return { ui }
}
36 changes: 36 additions & 0 deletions src/app/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,42 @@ if (typeof window !== 'undefined' && 'customElements' in window) {
},
) as VueElementConstructor

const originalConnectedCallback = NuxtStudio.prototype.connectedCallback
NuxtStudio.prototype.connectedCallback = function () {
originalConnectedCallback.call(this)
const shadowRoot = this.shadowRoot
if (!shadowRoot) return

const syncStyles = () => {
const headStyles = document.head.querySelectorAll('style, link[rel="stylesheet"]')
headStyles.forEach((node) => {
const clonedNode = node.cloneNode(true) as HTMLElement
if (clonedNode.hasAttribute('data-vite-dev-id') && clonedNode.getAttribute('data-vite-dev-id')?.includes('nuxt-studio')) {
return
}

shadowRoot.appendChild(clonedNode)
})
}
syncStyles()
const observer = new MutationObserver((mutations) => {
let shouldSync = false
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeName === 'STYLE' || (node.nodeName === 'LINK' && (node as HTMLLinkElement).rel === 'stylesheet')) {
shouldSync = true
break
}
}
}
if (shouldSync) {
syncStyles()
}
})

observer.observe(document.head, { childList: true, subtree: true })
}

customElements.define('nuxt-studio', NuxtStudio)
}

Expand Down
22 changes: 22 additions & 0 deletions src/app/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,25 @@ export interface StudioLocation {
feature: StudioFeature
fsPath: string
}

export interface StudioUI {
header: {
root: string
}
footer: {
root: string
text: string
userMenuButton: string
debugSwitchContainer: string
debugSwitch: {
root: string
wrapper: string
}
actions: string
}
layout: {
sidebar: string
monaco: string
body: string
}
}
6 changes: 6 additions & 0 deletions src/app/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Repository } from './git'
import type { ComponentMeta } from './component'
import type { MarkdownParsingOptions, SyntaxHighlightTheme } from './content'
import type { CollectionInfo } from '@nuxt/content'
import type { StudioUI } from './config'

export * from './file'
export * from './item'
Expand All @@ -28,6 +29,11 @@ export interface StudioHost {
defaultLocale: string
getHighlightTheme: () => SyntaxHighlightTheme
}
config: {
studio: {
ui: StudioUI
}
}
on: {
routeChange: (fn: (to: RouteLocationNormalized, from: RouteLocationNormalized) => void) => void
mounted: (fn: () => void) => void
Expand Down
55 changes: 54 additions & 1 deletion src/module/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNuxtModule, createResolver, addPlugin, extendViteConfig, addServerHandler, addTemplate, addServerImports } from '@nuxt/kit'
import { defineNuxtModule, createResolver, addPlugin, extendViteConfig, addServerHandler, addTemplate, addServerImports, addTypeTemplate } from '@nuxt/kit'
import { createHash } from 'node:crypto'
import { defu } from 'defu'
import { resolve } from 'node:path'
Expand All @@ -9,6 +9,28 @@ import { version } from '../../../package.json'
import { setupDevMode } from './dev'
import { validateAuthConfig } from './auth'

const studioUI = {
header: {
root: 'bg-muted/50 border-default border-b-[0.5px] pr-4 gap-1.5 flex items-center justify-between px-4 h-[45px]',
},
footer: {
root: 'bg-muted/50 border-default border-t-[0.5px] flex items-center justify-between gap-1.5 px-2 py-2',
text: 'ml-2 text-xs text-muted',
userMenuButton: 'px-2 py-1 font-medium',
debugSwitchContainer: 'w-full',
debugSwitch: {
root: 'w-full flex-row-reverse justify-between',
wrapper: 'ms-0',
},
actions: 'flex items-center',
},
layout: {
sidebar: 'fixed top-0 bottom-0 left-0 border-r border-default flex flex-col max-w-full bg-default',
monaco: 'monaco-editor',
body: 'flex-1 overflow-y-auto relative',
},
}

interface BaseRepository {
/**
* The owner of the git repository.
Expand Down Expand Up @@ -198,6 +220,37 @@ export default defineNuxtModule<ModuleOptions>({
validateAuthConfig(options)
}

// Initialize AppConfig with Studio defaults
nuxt.options.appConfig.studio = defu(nuxt.options.appConfig.studio || {}, {
ui: studioUI,
})

// Register type definitions
addTypeTemplate({
filename: 'types/studio-ui.d.ts',
getContents: () => `
import type { StudioUI } from 'nuxt-studio/app'

type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;

declare module '@nuxt/schema' {
interface AppConfigInput {
studio?: {
ui?: DeepPartial<StudioUI>
}
}

interface AppConfig {
studio: {
ui: StudioUI
}
}
}

export {}
`,
})

// Enable checkoutOutdatedBuildInterval to detect new deployments
nuxt.options.experimental = nuxt.options.experimental || {}
nuxt.options.experimental.checkOutdatedBuildInterval = 1000 * 30
Expand Down
6 changes: 5 additions & 1 deletion src/module/src/runtime/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getHostStyles, getSidebarWidth, adjustFixedElements } from './utils/sid
import type { StudioHost, StudioUser, DatabaseItem, MediaItem, Repository } from 'nuxt-studio/app'
import type { RouteLocationNormalized, Router } from 'vue-router'
// @ts-expect-error queryCollection is not defined in .nuxt/imports.d.ts
import { clearError, getAppManifest, queryCollection, queryCollectionItemSurroundings, queryCollectionNavigation, queryCollectionSearchSections, useRuntimeConfig } from '#imports'
import { clearError, getAppManifest, queryCollection, queryCollectionItemSurroundings, queryCollectionNavigation, queryCollectionSearchSections, useRuntimeConfig, useAppConfig } from '#imports'
import { collections } from '#content/preview'
import { publicAssetsStorage } from '#build/studio-public-assets'
import { useHostMeta } from './composables/useMeta'
Expand All @@ -28,6 +28,7 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH

const isMounted = ref(false)
const meta = useHostMeta()
const appConfig = useAppConfig()

function useNuxtApp() {
return window.useNuxtApp!()
Expand Down Expand Up @@ -75,6 +76,9 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH
defaultLocale: useRuntimeConfig().public.studio.i18n?.defaultLocale || 'en',
getHighlightTheme: () => meta.highlightTheme.value!,
},
config: {
studio: appConfig.studio || {},
},
on: {
routeChange: (fn: (to: RouteLocationNormalized, from: RouteLocationNormalized) => void) => {
const router = useRouter()
Expand Down
Loading