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
1 change: 0 additions & 1 deletion alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ export const alias = {
'@devframes/plugin-messages/client': p('messages/src/client/index.ts'),
'@devframes/plugin-messages/node': p('messages/src/node/index.ts'),
'@devframes/plugin-messages/constants': p('messages/src/constants.ts'),
'@devframes/plugin-messages/types': p('messages/src/types.ts'),
'@devframes/plugin-messages/rpc': p('messages/src/rpc/index.ts'),
'@devframes/plugin-messages/cli': p('messages/src/cli.ts'),
'@devframes/plugin-messages/vite': p('messages/src/vite.ts'),
Expand Down
2 changes: 1 addition & 1 deletion plugins/messages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"./constants": "./dist/constants.mjs",
"./node": "./dist/node/index.mjs",
"./rpc": "./dist/rpc/index.mjs",
"./types": "./dist/types.mjs",
"./vite": "./dist/vite.mjs",
"./package.json": "./package.json"
},
Expand Down Expand Up @@ -75,6 +74,7 @@
"@vueuse/core": "catalog:frontend",
"colorjs.io": "catalog:frontend",
"devframe": "workspace:*",
"floating-vue": "catalog:frontend",
"get-port-please": "catalog:deps",
"h3": "catalog:deps",
"storybook": "catalog:storybook",
Expand Down
47 changes: 36 additions & 11 deletions plugins/messages/src/client/App.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { DevframeMessageEntry } from '@devframes/hub/types'
import type { DevframeConnectionStatus, DevframeRpcClient } from 'devframe/client'
import type { DevframeMessageEntry } from '../types'
import DisplayBadge from '@antfu/design/components/Display/DisplayBadge.vue'
import FormSearchField from '@antfu/design/components/Form/FormSearchField.vue'
import LayoutToolbar from '@antfu/design/components/Layout/LayoutToolbar.vue'
import { computed, onBeforeUnmount, ref } from 'vue'
import {
Expand All @@ -13,6 +15,8 @@ import {
connectionTitle,
} from '../../../../design/design'
import MessagesView from './components/MessagesView.vue'
import MessageToolbarActions from './components/MessageToolbarActions.vue'
import { useMessageFilters } from './composables/useMessageFilters'
import { useMessages } from './state/messages'

const props = defineProps<{
Expand All @@ -21,6 +25,10 @@ const props = defineProps<{

const state = useMessages(props.rpc)

// Search / sort / filter state lives here (not in the view) so the nav bar's
// search field + actions and the view's filter bar share one source of truth.
const filters = useMessageFilters(() => state.entries)

// The live feed rides on shared-state over the socket, so a dropped socket or
// refused auth is surfaced instead of silently freezing the list. The client
// doesn't auto-reconnect; a reload re-runs the whole handshake.
Expand Down Expand Up @@ -50,9 +58,9 @@ async function onDismiss(id: string): Promise<void> {
await props.rpc.call('devframes:plugin:messages:remove', id)
}

async function onDismissMany(ids: string[]): Promise<void> {
for (const id of ids)
await props.rpc.call('devframes:plugin:messages:remove', id)
async function onDismissFiltered(): Promise<void> {
for (const entry of filters.filteredEntries)
await props.rpc.call('devframes:plugin:messages:remove', entry.id)
}

async function onClear(): Promise<void> {
Expand Down Expand Up @@ -85,17 +93,36 @@ async function onOpenFile(entry: DevframeMessageEntry): Promise<void> {
</div>

<template #search>
<div />
<div class="flex gap-2 items-center">
<FormSearchField
v-if="!connState"
v-model="filters.search"
size="sm"
placeholder="Search messages…"
class="max-w-64"
/>
<DisplayBadge v-if="filters.totalCount > 0" :color="false" class="text-xs font-mono">
<template v-if="filters.filteredCount !== filters.totalCount">
{{ filters.filteredCount }}/{{ filters.totalCount }}
</template>
<template v-else>
{{ filters.totalCount }}
</template>
</DisplayBadge>
</div>
</template>

<template #end>
<MessageToolbarActions
v-if="!connState"
:filters
@dismiss-filtered="onDismissFiltered"
@clear="onClear"
/>
<span v-if="conn" :class="conn.class">
<span :class="conn.dot" />
{{ conn.label }}
</span>
<span v-if="state.entries.length > 0" class="badge-muted font-mono">
{{ state.entries.length }}
</span>
</template>
</LayoutToolbar>

Expand All @@ -122,11 +149,9 @@ async function onOpenFile(entry: DevframeMessageEntry): Promise<void> {
</div>
<MessagesView
v-else
:entries="state.entries"
:filters
:can-open-file="canOpenFile"
@dismiss="onDismiss"
@dismiss-many="onDismissMany"
@clear="onClear"
@persist="onPersist"
@open-file="onOpenFile"
/>
Expand Down
15 changes: 12 additions & 3 deletions plugins/messages/src/client/components/FilterToggles.stories.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import FilterToggles from './FilterToggles.vue'
import { getHashColorFromString, levels } from './MessageItemConstants'
import { levels } from './message-styles'

const meta = {
title: 'Messages/FilterToggles',
Expand Down Expand Up @@ -32,11 +32,20 @@ export const LevelsFiltered: Story = {
},
}

export const HashColored: Story = {
export const CategoryTags: Story = {
args: {
label: 'Category',
items: ['a11y', 'lint', 'runtime', 'build'],
active: new Set<string>(),
hashColor: getHashColorFromString,
tag: 'category',
},
}

export const LabelTags: Story = {
args: {
label: 'Labels',
items: ['axe', 'eslint', 'vite', 'hmr'],
active: new Set<string>(),
tag: 'label',
},
}
36 changes: 22 additions & 14 deletions plugins/messages/src/client/components/FilterToggles.vue
Original file line number Diff line number Diff line change
@@ -1,39 +1,47 @@
<script setup lang="ts">
import MessageTag from './MessageTag.vue'

defineProps<{
label: string
items: string[]
active: Set<string>
/** Map item key → { icon, color, label } for styled items */
/** Map item key → { icon, color, label } for styled (level/source) items. */
styles?: Record<string, { icon?: string, color?: string, label?: string }>
/** Compute inline color via hash for items without predefined styles */
hashColor?: (item: string, opacity: number) => string
/** Render items as hash-colored `MessageTag` chips of this kind (category/label). */
tag?: 'category' | 'label'
}>()

defineEmits<{
toggle: [item: string]
}>()

function isDimmed(active: Set<string>, item: string): boolean {
return active.size > 0 && !active.has(item)
}
</script>

<template>
<span class="text-xs op40">{{ label }}</span>
<span class="text-xs op-fade">{{ label }}</span>
<div class="flex flex-wrap items-center gap-0.5">
<button
v-for="item of items"
:key="item"
class="px-1.5 py-0.5 rounded text-xs flex items-center gap-0.5 hover:bg-active transition"
type="button"
class="rounded flex items-center transition"
:class="[
active.size === 0 || active.has(item)
? (styles?.[item]?.color || '')
: 'op30',
tag ? 'p-0.5' : 'px-1.5 py-0.5 gap-0.5 text-xs',
!tag && !isDimmed(active, item) ? (styles?.[item]?.color || '') : '',
isDimmed(active, item) ? 'op50 saturate-10 hover:op85 hover:saturate-50' : 'op85 hover:op100',
]"
:style="!styles?.[item]?.color && hashColor ? {
color: active.size === 0 || active.has(item) ? hashColor(item, 1) : undefined,
backgroundColor: active.size === 0 || active.has(item) ? hashColor(item, 0.1) : undefined,
} : undefined"
@click="$emit('toggle', item)"
>
<div v-if="styles?.[item]?.icon" :class="styles[item]!.icon" class="w-3.5 h-3.5" />
<span>{{ styles?.[item]?.label || item }}</span>
<template v-if="tag">
<MessageTag :text="item" :kind="tag" />
</template>
<template v-else>
<div v-if="styles?.[item]?.icon" :class="styles[item]!.icon" class="w-3.5 h-3.5" />
<span>{{ styles?.[item]?.label || item }}</span>
</template>
</button>
</div>
</template>
17 changes: 0 additions & 17 deletions plugins/messages/src/client/components/HashBadge.vue

This file was deleted.

Loading
Loading