Skip to content
Merged
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
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ A self-hosted homepage portal for your homelab services.
## Features

- Drag-and-drop sections and widgets
- Visual icon picker (5000+ icons)
- Custom icon uploads
- Labels with color coding
- icon picker (5000+ icons)
- Custom icon uploads or download from known sources
- Labels
- Health check
- Theme customization (colors, card styles)
- Dark/light mode
- Edit mode with right-click context menus
Expand All @@ -31,6 +32,10 @@ services:
- /tmp
```

> [!WARNING]
> Labbit has no built-in authentication. Run it on your LAN, over a VPN, or behind an auth proxy (Authelia, Authentik, Tailscale).


## Development

```bash
Expand Down
16 changes: 8 additions & 8 deletions app/components/board/BoardSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function onDragChange() {
}

function toggleCollapse() {
if (props.section.collapsible === false) return
if (!props.section.collapsible) return
store.updateSection(props.section.id, { collapsed: !props.section.collapsed })
markDirty()
}
Expand All @@ -63,17 +63,17 @@ function toggleCollapse() {
<button
v-if="section.showTitle"
class="flex items-center gap-1.5"
:class="{ 'cursor-pointer': section.collapsible !== false && section.widgets.length > 0 }"
:class="{ 'cursor-pointer': section.collapsible && section.widgets.length > 0 }"
@click="toggleCollapse"
>
<UIcon
v-if="section.collapsible !== false && section.widgets.length > 0"
v-if="section.collapsible && section.widgets.length > 0"
:name="section.collapsed ? 'i-lucide-chevron-right' : 'i-lucide-chevron-down'"
class="size-4 text-dimmed"
/>
<h2 class="text-sm font-semibold text-muted uppercase tracking-wide">
<span class="text-sm font-semibold text-muted uppercase tracking-wide">
{{ section.title }}
</h2>
</span>
</button>
<div
v-if="isEditing"
Expand All @@ -96,7 +96,7 @@ function toggleCollapse() {
</div>
</div>

<div v-show="section.collapsible === false || !section.collapsed">
<div v-show="!section.collapsible || !section.collapsed">
<!-- eslint-disable vue/no-mutating-props -->
<VueDraggable
v-model="section.widgets"
Expand Down Expand Up @@ -132,9 +132,9 @@ function toggleCollapse() {
name="i-lucide-plus"
class="size-6 mb-2"
/>
<p class="text-sm">
<span class="text-sm">
Drag widgets here or add from the widget picker
</p>
</span>
</button>
</div>

Expand Down
5 changes: 2 additions & 3 deletions app/components/editor/EditorSectionSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ watch(open, (val) => {
localColumns.value = props.section.columns
localLayout.value = props.section.layout
localShowTitle.value = props.section.showTitle
localCollapsible.value = props.section.collapsible !== false
localCollapsible.value = props.section.collapsible
localCardVariant.value = props.section.defaults?.cardVariant || 'outline'
}
})
Expand Down Expand Up @@ -57,8 +57,7 @@ const columnOptions = [
const variantOptions: { label: string, value: string, description: string }[] = [
{ label: 'Outline', value: 'outline', description: 'Bordered card' },
{ label: 'Accent', value: 'accent', description: 'Colored border' },
{ label: 'Soft', value: 'soft', description: 'Tinted background' },
{ label: 'Subtle', value: 'subtle', description: 'Light background' },
{ label: 'Tinted', value: 'subtle', description: 'Light tinted background' },
{ label: 'Ghost', value: 'ghost', description: 'No decoration' }
]
</script>
Expand Down
53 changes: 53 additions & 0 deletions app/components/editor/EditorWidgetSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,33 @@ const definition = computed(() => getDefinition(props.widget.kind))

const localOptions = ref<Record<string, unknown>>({})

const { getAllPlugins } = usePluginRegistry()

const enabledPlugins = computed(() => {
return getAllPlugins().filter((plugin) => {
if (plugin.compatibleWith !== '*' && !plugin.compatibleWith.includes(props.widget.kind)) return false
if (!plugin.settingsComponent) return false
const widgetEnabled = props.widget.plugins?.[plugin.id]?.enabled
const sectionEnabled = props.section.defaults?.plugins?.[plugin.id]?.enabled
return widgetEnabled ?? sectionEnabled ?? false
})
})

const pluginConfigs = ref<Record<string, Record<string, unknown>>>({})

watch(open, (val) => {
if (val) {
localOptions.value = JSON.parse(JSON.stringify(props.widget.options))

const configs: Record<string, Record<string, unknown>> = {}
for (const plugin of enabledPlugins.value) {
configs[plugin.id] = {
...plugin.defaultConfig,
...(props.section.defaults?.plugins?.[plugin.id]?.config || {}),
...(props.widget.plugins?.[plugin.id]?.config || {})
}
}
pluginConfigs.value = configs
}
})

Expand Down Expand Up @@ -62,6 +86,13 @@ function createAndAssignLabel() {

function handleSave() {
store.updateWidgetOptions(props.section.id, props.widget.id, localOptions.value)

for (const [pluginId, config] of Object.entries(pluginConfigs.value)) {
store.updateWidgetPlugins(props.section.id, props.widget.id, {
[pluginId]: { enabled: true, config }
})
}

markDirty()
open.value = false
}
Expand Down Expand Up @@ -229,6 +260,28 @@ function handleSave() {
/>
</UFormField>
</template>

<USeparator
v-if="enabledPlugins.length > 0"
class="my-2"
/>

<template
v-for="plugin in enabledPlugins"
:key="plugin.id"
>
<div class="flex items-center gap-2 pb-2">
<UIcon
:name="plugin.icon"
class="size-4 text-dimmed"
/>
<span class="text-sm font-medium text-muted">{{ plugin.label }}</span>
</div>
<component
:is="plugin.settingsComponent"
v-model:config="pluginConfigs[plugin.id]"
/>
</template>
</div>
</template>

Expand Down
Loading
Loading