Skip to content

Commit 93782e9

Browse files
committed
fix(app): unify panel header CTA size with WorkbenchPanelAction
1 parent 4a198e3 commit 93782e9

4 files changed

Lines changed: 80 additions & 38 deletions

File tree

‎AGENTS.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
- Announces failure with `role="alert"`; uses shared state layout and subtle danger badge background.
2323
- Keep field validation, inline action feedback, and compact canvas error overlays in their specialized forms. Preserve usable content after partial failures.
2424

25+
- Primary call-to-action in panel/tab headers (Run, Stop, ...) must use `WorkbenchPanelAction` from `packages/histoire-app/src/app/components/shell/WorkbenchPanelAction.vue` so every tab CTA shares same size (follows `--histoire-control-height`).
26+
- `icon`: offline Carbon glyph name. `label`: short visible label. `disabled`: disables action and options trigger.
27+
- Optional `menu` slot (scoped `{ close }`) adds split chevron with options popover; `menuLabel` overrides its accessible name.
28+
- Example: `<WorkbenchPanelAction icon="play-filled-alt" label="Run" @click="run"><template #menu="{ close }"><MenuItem label="Run all" @click="close()" /></template></WorkbenchPanelAction>`.
29+
2530
- Every spinner, busy dot, or "running"/"working" indicator must use `HstPulsatingDisc` from `@histoire/controls/vue` (source: `packages/histoire-controls/src/components/HstPulsatingDisc.vue`). Applies to app (`histoire-app`) and native components (`histoire-vue`).
2631
- `size`: number (px) or CSS length; default `16`. Color follows `currentColor`.
2732
- Never add custom spin/rotate keyframes or animate status icons (e.g. `in-progress`) for loading.

‎packages/histoire-app/src/app/components/panes/tests/FunctionalTestsPanel.vue‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { computed, ref, useId } from 'vue'
99
import WorkbenchEmptyState from '../../shell/WorkbenchEmptyState.vue'
1010
import WorkbenchErrorState from '../../shell/WorkbenchErrorState.vue'
1111
import WorkbenchLoadingState from '../../shell/WorkbenchLoadingState.vue'
12+
import WorkbenchPanelAction from '../../shell/WorkbenchPanelAction.vue'
1213
1314
import { useWorkbenchTestsModel } from './model.js'
1415
import TestsFilter from './TestsFilter.vue'
@@ -51,12 +52,8 @@ async function select(row: WorkbenchTestRow) {
5152
<HstButton color="flat" type="button" class="refresh-tests" aria-label="Refresh tests" title="Refresh tests" :disabled="!model?.canRefresh.value" @click="model?.retryCollection()">
5253
<BuiltinIcon icon="renew" :size="16" />
5354
</HstButton>
54-
<HstButton v-if="model?.running.value" color="primary" type="button" class="run-tests" @click="model.cancel()">
55-
<BuiltinIcon icon="stop-filled-alt" :size="16" />Stop
56-
</HstButton>
57-
<HstButton v-else color="primary" type="button" class="run-tests" :disabled="!model?.canRun.value" @click="model?.runAll()">
58-
<BuiltinIcon icon="play-filled-alt" :size="16" />Run all
59-
</HstButton>
55+
<WorkbenchPanelAction v-if="model?.running.value" icon="stop-filled-alt" label="Stop" @click="model.cancel()" />
56+
<WorkbenchPanelAction v-else icon="play-filled-alt" label="Run all" :disabled="!model?.canRun.value" @click="model?.runAll()" />
6057
</header>
6158
<template v-if="model">
6259
<TestsSummary :summary="model.summary.value" :running="model.running.value" :completed="model.completed.value" :total="model.total.value" />
@@ -98,7 +95,6 @@ async function select(row: WorkbenchTestRow) {
9895
header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px var(--histoire-pane-gutter, 16px); }
9996
h2 { margin: 0; font-size: 14px; font-weight: 800; }
10097
.refresh-tests { display: inline-flex; align-items: center; justify-content: center; padding: 8px; margin-left: auto; }
101-
.run-tests { display: inline-flex; align-items: center; gap: 6px; padding: 8px 10px; font-weight: 750; }
10298
.test-issue { margin: 10px var(--histoire-pane-gutter, 16px); color: var(--histoire-danger-text); font-size: 12px; overflow-wrap: anywhere; white-space: pre-line; }
10399
.retry-collection { margin: 10px var(--histoire-pane-gutter, 16px); align-self: flex-start; }
104100
.collection-progress { display: flex; align-items: center; gap: 8px; margin: 10px var(--histoire-pane-gutter, 16px); color: var(--histoire-muted); font-size: 12px; }
Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,25 @@
11
<script setup lang="ts">
2-
import { BuiltinIcon, HstButton } from '@histoire/controls/vue'
3-
import { useHistoireContext } from '@histoire/vue/internal'
4-
import { computed, ref, shallowRef } from 'vue'
2+
import { computed } from 'vue'
53
import { useWorkbenchVisual } from '../../../stores/visual.js'
6-
import BasePopover from '../../base/popover/BasePopover.vue'
74
import MenuItem from '../../menu/MenuItem.vue'
5+
import WorkbenchPanelAction from '../../shell/WorkbenchPanelAction.vue'
86
97
const model = useWorkbenchVisual()!
10-
const context = useHistoireContext()
11-
const open = ref(false)
12-
const chevron = shallowRef<InstanceType<typeof HstButton> | null>(null)
13-
const anchor = computed(() => (chevron.value?.element as HTMLElement | undefined) ?? null)
148
const running = computed(() => !!model.requestId.value)
159
const disabled = computed(() => model.inventoryLoading.value || model.saving.value || model.approving.value || !model.cases.value.length)
1610
1711
/** Recapture every selected comparison, ignoring retained evidence. */
18-
function runAll(): void {
19-
open.value = false
12+
function runAll(close: () => void): void {
13+
close()
2014
model.start()
2115
}
2216
</script>
2317

2418
<template>
25-
<HstButton v-if="running" color="primary" type="button" class="run-visual" @click="model.cancel()">
26-
<BuiltinIcon icon="stop-filled-alt" :size="16" />Stop
27-
</HstButton>
28-
<div v-else class="run-visual-group">
29-
<HstButton color="primary" type="button" class="run-visual" :disabled="disabled" @click="model.start({ incremental: true })">
30-
<BuiltinIcon icon="play-filled-alt" :size="16" />Run
31-
</HstButton>
32-
<HstButton ref="chevron" color="primary" type="button" class="run-visual-options" icon-only aria-label="Run options" aria-haspopup="menu" :aria-expanded="open" :disabled="disabled" @click="open = !open">
33-
<BuiltinIcon icon="chevron-down" :size="16" />
34-
</HstButton>
35-
<BasePopover v-if="open" v-model:open="open" :anchor="anchor" label="Run options" role="menu" :width="160" :overlay-target="context.overlay.value ?? context.root.value">
36-
<MenuItem label="Run all" icon="renew" @click="runAll" />
37-
</BasePopover>
38-
</div>
19+
<WorkbenchPanelAction v-if="running" icon="stop-filled-alt" label="Stop" @click="model.cancel()" />
20+
<WorkbenchPanelAction v-else icon="play-filled-alt" label="Run" :disabled="disabled" @click="model.start({ incremental: true })">
21+
<template #menu="{ close }">
22+
<MenuItem label="Run all" icon="renew" @click="runAll(close)" />
23+
</template>
24+
</WorkbenchPanelAction>
3925
</template>
40-
41-
<style scoped>
42-
.run-visual-group { display: inline-flex; gap: 1px; }
43-
.run-visual { display: inline-flex; align-items: center; gap: 6px; padding: 8px 10px; font-weight: 750; }
44-
.run-visual-group .run-visual { border-start-end-radius: 0; border-end-end-radius: 0; }
45-
.run-visual-options { display: inline-flex; align-items: center; justify-content: center; padding: 8px 6px; border-start-start-radius: 0; border-end-start-radius: 0; }
46-
</style>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script setup lang="ts">
2+
import { BuiltinIcon, HstButton } from '@histoire/controls/vue'
3+
import { useHistoireContext } from '@histoire/vue/internal'
4+
import { computed, ref, shallowRef } from 'vue'
5+
import BasePopover from '../base/popover/BasePopover.vue'
6+
7+
const props = defineProps<{
8+
/** Offline Carbon glyph name shown before label. */
9+
icon: string
10+
/** Short visible action label. */
11+
label: string
12+
/** Disables main action and options trigger. */
13+
disabled?: boolean
14+
/** Accessible name of options trigger and its menu. */
15+
menuLabel?: string
16+
/** Options menu width in pixels. */
17+
menuWidth?: number
18+
}>()
19+
const emit = defineEmits<{
20+
/** Main action activated. */
21+
click: [event: MouseEvent]
22+
}>()
23+
defineSlots<{
24+
/** Optional secondary options; renders split chevron trigger when present. */
25+
menu?: (props: { close: () => void }) => any
26+
}>()
27+
28+
const context = useHistoireContext()
29+
const open = ref(false)
30+
const chevron = shallowRef<InstanceType<typeof HstButton> | null>(null)
31+
const anchor = computed(() => (chevron.value?.element as HTMLElement | undefined) ?? null)
32+
const resolvedMenuLabel = computed(() => props.menuLabel ?? `${props.label} options`)
33+
34+
/** Dismiss options menu after an item runs. */
35+
function close(): void {
36+
open.value = false
37+
}
38+
</script>
39+
40+
<template>
41+
<div class="workbench-panel-action" :class="{ split: $slots.menu }">
42+
<HstButton color="primary" type="button" class="workbench-panel-action-main" :disabled="disabled" @click="emit('click', $event)">
43+
<BuiltinIcon :icon="icon" :size="16" />{{ label }}
44+
</HstButton>
45+
<template v-if="$slots.menu">
46+
<HstButton ref="chevron" color="primary" type="button" class="workbench-panel-action-options" icon-only :aria-label="resolvedMenuLabel" aria-haspopup="menu" :aria-expanded="open" :disabled="disabled" @click="open = !open">
47+
<BuiltinIcon icon="chevron-down" :size="16" />
48+
</HstButton>
49+
<BasePopover v-if="open" v-model:open="open" :anchor="anchor" :label="resolvedMenuLabel" role="menu" :width="menuWidth ?? 160" :overlay-target="context.overlay.value ?? context.root.value">
50+
<slot name="menu" :close="close" />
51+
</BasePopover>
52+
</template>
53+
</div>
54+
</template>
55+
56+
<style scoped>
57+
/* Pane header CTA: height follows shared control height so every tab's primary action matches across density modes. */
58+
.workbench-panel-action { display: inline-flex; flex: none; gap: 1px; --histoire-icon-button-size: var(--histoire-control-height, 34px); }
59+
.workbench-panel-action-main { box-sizing: border-box; height: var(--histoire-control-height, 34px); padding-block: 0; padding-inline: 10px; font-weight: 750; white-space: nowrap; }
60+
.split .workbench-panel-action-main { border-start-end-radius: 0; border-end-end-radius: 0; }
61+
.workbench-panel-action-options { border-start-start-radius: 0; border-end-start-radius: 0; }
62+
</style>

0 commit comments

Comments
 (0)