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
6 changes: 3 additions & 3 deletions dashboard/src/components/ProposalEditDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ import { computed, ref, watch } from "vue"
import PhoneInput from "@/components/PhoneInput.vue"
import type { FrappeError } from "@/types"
import {
proposalEditorExtensions as editorExtensions,
proposalEditorToolbar as editorToolbar,
} from "@/utils/proposalEditor"
richTextExtensions as editorExtensions,
richTextToolbar as editorToolbar,
} from "@/utils/richTextEditor"

const props = defineProps({
open: {
Expand Down
4 changes: 1 addition & 3 deletions dashboard/src/components/UserMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
import {
Avatar,
Dropdown,
KeyboardShortcut,
Tooltip,
sidebarCollapsedKey,
useColorScheme,
type DropdownOptions,
} from "frappe-ui"
import { computed, h, inject, ref } from "vue"
import { computed, inject, ref } from "vue"

import UserSettingsDialog from "@/components/UserSettingsDialog.vue"
import { session } from "@/data/session"
Expand Down Expand Up @@ -52,7 +51,6 @@ const menu = computed<DropdownOptions>(() => [
label: __("Settings"),
icon: "lucide-settings",
onClick: () => (settingsOpen.value = true),
slots: { suffix: () => h(KeyboardShortcut, { combo: "G+S", bg: true }) },
},
{
label: __("Theme"),
Expand Down
3 changes: 1 addition & 2 deletions dashboard/src/components/UserSettingsDialog.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
import {
Avatar,
Button,
ErrorMessage,
FormControl,
Expand Down Expand Up @@ -131,7 +130,7 @@ async function save(fields: Partial<Profile> = { ...form }) {
<SettingsNavGroup :label="__('Account')">
<SettingsNavItem value="profile">
<template #prefix>
<Avatar size="xs" :image="form.user_image ?? undefined" :label="fullName" />
<span class="lucide-circle-user-round size-4" />
</template>
{{ __("Profile") }}
</SettingsNavItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { computed, ref } from "vue"

import type { CommunicationDraft } from "@/types"
import { audienceOptions, hasText } from "@/utils/communicationText"
import { proposalEditorExtensions, proposalEditorToolbar } from "@/utils/proposalEditor"
import { richTextExtensions, richTextToolbar } from "@/utils/richTextEditor"

const props = defineProps<{ canWrite: boolean; sending: boolean }>()
const draft = defineModel<CommunicationDraft>({ required: true })
Expand Down Expand Up @@ -36,7 +36,7 @@ const canSend = computed(() => props.canWrite && hasText(draft.value.message) &&

<Editor
v-model="draft.message"
:extensions="proposalEditorExtensions"
:extensions="richTextExtensions"
:editable="canWrite"
placeholder="Write your message…"
@focus="touched = true"
Expand All @@ -48,7 +48,7 @@ const canSend = computed(() => props.canWrite && hasText(draft.value.message) &&
v-if="showActions"
class="composer-actions flex items-center gap-2 border-t border-outline-gray-1 p-2"
>
<EditorFixedMenu :items="proposalEditorToolbar" class="min-w-0 flex-1 overflow-x-auto" />
<EditorFixedMenu :items="richTextToolbar" class="min-w-0 flex-1 overflow-x-auto" />
<Button label="Advanced" icon-left="lucide-sliders-horizontal" @click="emit('advanced')" />
<Button
variant="solid"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { useRecipientCount } from "@/data/communications"
import type { CommunicationDraft, CommunicationItem, EventCommunications } from "@/types"
import { audienceOptions, hasText } from "@/utils/communicationText"
import { proposalEditorExtensions, proposalEditorToolbar } from "@/utils/proposalEditor"
import { richTextExtensions, richTextToolbar } from "@/utils/richTextEditor"

const props = defineProps<{
event: string
Expand Down Expand Up @@ -114,11 +114,7 @@ const viewedWhen = computed(() =>

<!-- The same editor, read-only, rather than v-html: tiptap parses the stored
HTML into its own schema, so only what the composer can produce is rendered. -->
<Editor
:model-value="viewing.message"
:extensions="proposalEditorExtensions"
:editable="false"
>
<Editor :model-value="viewing.message" :extensions="richTextExtensions" :editable="false">
<EditorContent
class="prose prose-sm max-w-none text-base leading-[1.6] text-ink-gray-7"
/>
Expand Down Expand Up @@ -177,13 +173,9 @@ const viewedWhen = computed(() =>

<div class="space-y-2">
<label class="block text-xs text-ink-gray-5">Message</label>
<Editor
v-model="draft.message"
:extensions="proposalEditorExtensions"
:editable="canWrite"
>
<Editor v-model="draft.message" :extensions="richTextExtensions" :editable="canWrite">
<EditorFixedMenu
:items="proposalEditorToolbar"
:items="richTextToolbar"
class="rounded-t-5 border border-b-0 border-outline-gray-2 px-2 py-1"
/>
<EditorContent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { session } from "@/data/session"
import { userResource } from "@/data/user"
import type { ProposalListItem, ProposalSpeaker, TalkProposal } from "@/types"
import { proposalActions } from "@/utils/proposalActions"
import { proposalEditorExtensions, proposalEditorToolbar } from "@/utils/proposalEditor"
import { richTextExtensions, richTextToolbar } from "@/utils/richTextEditor"
import { isReader, speakerName } from "@/utils/speakerByline"

const props = defineProps<{ proposal: ProposalListItem | null }>()
Expand Down Expand Up @@ -240,11 +240,11 @@ const fields = computed(() => [
<label class="block text-xs text-ink-gray-5">Description</label>
<Editor
v-model="form.description"
:extensions="proposalEditorExtensions"
:extensions="richTextExtensions"
placeholder="What is the talk about?"
>
<EditorFixedMenu
:items="proposalEditorToolbar"
:items="richTextToolbar"
class="rounded-t-5 border border-b-0 border-outline-gray-2 px-2 py-1"
/>
<EditorContent
Expand Down
4 changes: 2 additions & 2 deletions dashboard/src/components/dashboard/tickets/PrintedTicket.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const time = computed(() => {
<span class="flex min-w-0 flex-1 flex-col justify-between gap-4 p-4">
<span class="block">
<span class="block text-2xs-medium uppercase tracking-widest text-ink-gray-5">Event</span>
<span class="block text-4xl-bold uppercase line-clamp-2">
<span class="text-3xl-bold uppercase line-clamp-2">
{{ ticket.event_title }}
</span>
</span>
Expand All @@ -65,7 +65,7 @@ const time = computed(() => {
<span class="block text-2xs-medium uppercase tracking-widest text-ink-gray-5"
>Venue</span
>
<span class="block line-clamp-3">{{ ticket.venue || "To be announced" }}</span>
<span class="line-clamp-3">{{ ticket.venue || "To be announced" }}</span>
</span>
</span>
</span>
Expand Down
27 changes: 21 additions & 6 deletions dashboard/src/pages/manage/events/CreateEvent.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useTextareaAutosize } from "@vueuse/core"
import { Alert, Button, ErrorMessage, toast } from "frappe-ui"
import { Editor, EditorContent, RichTextKit } from "frappe-ui/editor"
import { Editor, EditorContent, EditorFixedMenu } from "frappe-ui/editor"
import { computed, onBeforeUnmount, onMounted, ref } from "vue"
import { onBeforeRouteLeave, useRouter } from "vue-router"

Expand All @@ -13,6 +14,7 @@ import type { FrappeError } from "@/types"
import { defaultSchedule } from "@/utils/eventDates"
import type { ChecklistItem } from "@/utils/eventValidation"
import { eventDraftChecklist, isDraftComplete } from "@/utils/eventValidation"
import { richTextExtensions, richTextToolbar } from "@/utils/richTextEditor"
import { canCreateEvents } from "@/utils/teamRoles"
import { currentTimeZone } from "@/utils/timeZones"

Expand All @@ -25,6 +27,10 @@ const router = useRouter()
const canCreate = computed(() => canCreateEvents(currentTeam.value?.team_role))

const title = ref("")

// A title wraps rather than scrolling out of sight, so the box grows with it.
const titleField = ref<HTMLTextAreaElement>()
useTextareaAutosize({ element: titleField, watch: title })
const about = ref("")
const bannerImage = ref("")

Expand Down Expand Up @@ -191,14 +197,19 @@ async function save() {
<EventBanner v-model="bannerImage" :seed="title" :disabled="!canCreate" />

<!-- Plain input on purpose: this is the page's headline, not a labelled field. -->
<input
<!-- A textarea rather than an input so a long name wraps; Enter is swallowed
since a title has no second line of its own. -->
<textarea
id="event-title"
ref="titleField"
v-model="title"
rows="1"
aria-label="Event title"
placeholder="Name your event"
:disabled="!canCreate"
:aria-invalid="saveAttempted && !title.trim()"
class="w-full bg-transparent text-4xl font-semibold text-ink-gray-9 placeholder:text-ink-gray-4 focus:outline-none disabled:text-ink-gray-5 aria-invalid:placeholder:text-ink-red-4"
class="w-full resize-none overflow-hidden border-0 bg-transparent p-0 text-4xl font-semibold text-ink-gray-9 placeholder:text-ink-gray-4 focus:outline-none disabled:text-ink-gray-5 aria-invalid:placeholder:text-ink-red-4"
@keydown.enter.prevent
/>

<div class="grid gap-8 md:grid-cols-5">
Expand All @@ -207,15 +218,19 @@ async function save() {
<!-- Editor is renderless, so EditorContent's root is the ProseMirror element
itself: the height and scrolling land on the editable area rather than on a
wrapper, and the whole box takes a click. -->
<div class="rounded-6 border border-outline-gray-2 p-3">
<div class="overflow-hidden rounded-6 border border-outline-gray-2">
<Editor
v-model="about"
:extensions="[RichTextKit]"
:extensions="richTextExtensions"
placeholder="What is this event about?"
:editable="canCreate"
>
<EditorFixedMenu
:items="richTextToolbar"
class="overflow-x-auto border-b border-outline-gray-2 px-2 py-1"
/>
<EditorContent
class="prose-sm h-48 max-w-none overflow-y-auto text-ink-gray-8 focus:outline-none"
class="prose-sm h-48 max-w-none overflow-y-auto p-3 text-ink-gray-8 focus:outline-none"
/>
</Editor>
</div>
Expand Down
38 changes: 28 additions & 10 deletions dashboard/src/pages/manage/events/EventDetails.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useEventListener } from "@vueuse/core"
import { useEventListener, useTextareaAutosize } from "@vueuse/core"
import { Button, ErrorMessage, Textarea, toast } from "frappe-ui"
import { Editor, EditorContent, RichTextKit } from "frappe-ui/editor"
import { Editor, EditorContent, EditorFixedMenu } from "frappe-ui/editor"
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue"
import { useRoute } from "vue-router"

Expand All @@ -18,6 +18,7 @@ import { session } from "@/data/session"
import type { EventDetail, FrappeError } from "@/types"
import { isEndBeforeStart } from "@/utils/eventDates"
import { matches } from "@/utils/formDraft"
import { richTextExtensions, richTextToolbar } from "@/utils/richTextEditor"

const route = useRoute()
const eventId = route.params.eventId as string
Expand All @@ -30,6 +31,10 @@ type EventForm = ReturnType<typeof blank>
const form = reactive(blank())
const saved = ref<EventForm>(blank())

// A title wraps rather than scrolling out of sight, so the box grows with it.
const titleField = ref<HTMLTextAreaElement>()
useTextareaAutosize({ element: titleField, watch: () => form.title })

// Unsaved edits outlive the page: the section tabs unmount it, and losing a half-written
// description to a look at the guest list is not a fair trade.
const draft = useFormDraft(
Expand Down Expand Up @@ -194,12 +199,17 @@ async function save() {
<div class="grid gap-8 md:grid-cols-5">
<div class="space-y-8 md:col-span-3">
<div class="space-y-2">
<!-- Plain input on purpose: this is the page's headline, not a labelled field. -->
<input
<!-- Plain field on purpose: this is the page's headline, not a labelled one.
A textarea rather than an input so a long name wraps; Enter is swallowed
since a title has no second line of its own. -->
<textarea
ref="titleField"
v-model="form.title"
rows="1"
aria-label="Event title"
placeholder="Name your event"
class="-mx-1 w-full rounded-4 bg-transparent px-1 text-4xl font-semibold text-ink-gray-9 placeholder:text-ink-gray-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-outline-gray-3"
class="w-full resize-none overflow-hidden border-0 bg-transparent p-0 text-4xl font-semibold text-ink-gray-9 placeholder:text-ink-gray-4 focus:outline-none"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing keyboard focus

The new textarea removes the previous focus-visible:ring-2 styling while also suppressing the browser's default outline. Keyboard users therefore receive no visible focus indicator when editing the event title. Please preserve an explicit focus-visible style.

Prompt To Fix With AI
This is a comment left during a code review.
Path: dashboard/src/pages/manage/events/EventDetails.vue
Line: 211

Comment:
**Missing keyboard focus**

The new textarea removes the previous `focus-visible:ring-2` styling while also suppressing the browser's default outline. Keyboard users therefore receive no visible focus indicator when editing the event title. Please preserve an explicit focus-visible style.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

@keydown.enter.prevent
/>
<!-- Ghost variant: no border, so it reads as a subtitle under the name. -->
<Textarea
Expand All @@ -218,23 +228,30 @@ async function save() {
itself: the height and scrolling land on the editable area rather than on a
wrapper, and the whole box takes a click. -->
<div
class="rounded-6 border border-outline-gray-2 p-3 transition-colors duration-150 ease-out focus-within:border-outline-gray-4 motion-reduce:transition-none"
class="overflow-hidden rounded-6 border border-outline-gray-2 transition-colors duration-150 ease-out focus-within:border-outline-gray-4 motion-reduce:transition-none"
>
<Editor
v-model="form.about"
:extensions="[RichTextKit]"
:extensions="richTextExtensions"
placeholder="What is this event about?"
>
<EditorFixedMenu
:items="richTextToolbar"
class="overflow-x-auto border-b border-outline-gray-2 px-2 py-1"
/>
<EditorContent
class="prose-sm h-48 max-w-none overflow-y-auto text-ink-gray-8 focus:outline-none"
class="prose-sm h-48 max-w-none overflow-y-auto p-3 text-ink-gray-8 focus:outline-none"
/>
</Editor>
</div>
</section>
</div>

<div class="space-y-8 md:col-span-2">
<div class="space-y-4 md:col-span-2">
<!-- Every section in this column carries the same padding, so their labels
share one left edge. -->
<EventRoute
class="rounded-6 p-4"
v-model="form.route"
v-model:taken="routeTaken"
:event="eventId"
Expand All @@ -249,7 +266,7 @@ async function save() {
v-model:time-zone="form.time_zone"
/>

<section class="space-y-3">
<section class="space-y-3 rounded-6 p-4">
<h2 class="text-sm font-medium uppercase tracking-wide text-ink-gray-5">Where</h2>
<EventMedium
v-model:medium="form.medium"
Expand All @@ -261,6 +278,7 @@ async function save() {
</section>

<EventHosts
class="rounded-6 p-4"
:event="eventId"
:primary-host="event.data.primary_host"
:co-hosts="event.data.co_hosts"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
Strike,
} from "frappe-ui/editor"

// No upload handler is wired for proposals, so the media extensions are off —
// No upload handler is wired anywhere this is used, so the media extensions are off —
// otherwise the drop/paste paths would silently fail.
export const proposalEditorExtensions = [
export const richTextExtensions = [
RichTextKit.configure({
heading: { levels: [2, 3, 4, 5, 6] },
image: false,
Expand All @@ -25,7 +25,7 @@ export const proposalEditorExtensions = [
}),
]

export const proposalEditorToolbar = [
export const richTextToolbar = [
HeadingGroup,
Separator,
Bold,
Expand Down
Loading