Skip to content
Draft
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
101 changes: 101 additions & 0 deletions src/components/PrototypeChromeMenuPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<script setup lang="ts">
import { CdxButton, CdxSelect } from '@wikimedia/codex'

import {
MICROTASK_SOURCE_MENU_ITEMS,
MORELIKE_STRATEGY_MENU_ITEMS,
usePrototypeDevSettings,
} from '@/composables/usePrototypeDevSettings'
import {
clearAllInterests,
useInterestsSettings,
} from '@/prototypes/template-homepage/suggested-edits/data/useInterestsSettings'

const { morelikeStrategy, microtaskSource } = usePrototypeDevSettings()
const interestsSettings = useInterestsSettings()

function onClearInterests(): void {
clearAllInterests(interestsSettings.value)
}
</script>

<template>
<div class="prototype-chrome-menu-panel">
<p class="prototype-chrome-menu-panel__title">Prototype settings</p>
<p class="prototype-chrome-menu-panel__hint">
Dev-only controls for suggested-edits morelike search and microtask assignment.
</p>

<label class="prototype-chrome-menu-panel__field">
<span class="prototype-chrome-menu-panel__label">Morelike strategy</span>
<CdxSelect
v-model:selected="morelikeStrategy"
:menu-items="MORELIKE_STRATEGY_MENU_ITEMS"
default-label="Serial multi-call"
/>
</label>

<label class="prototype-chrome-menu-panel__field">
<span class="prototype-chrome-menu-panel__label">Microtask source</span>
<CdxSelect
v-model:selected="microtaskSource"
:menu-items="MICROTASK_SOURCE_MENU_ITEMS"
default-label="Microtask Generator API"
/>
</label>

<div class="prototype-chrome-menu-panel__section">
<CdxButton @click="onClearInterests">Clear interests</CdxButton>
</div>
</div>
</template>

<style scoped>
.prototype-chrome-menu-panel {
display: flex;
flex-direction: column;
gap: var(--spacing-100, 16px);
min-width: 16rem;
max-width: 20rem;
padding: var(--spacing-100, 16px);
}

.prototype-chrome-menu-panel__title {
margin: 0;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-medium);
line-height: var(--line-height-medium);
}

.prototype-chrome-menu-panel__hint {
margin: 0;
font-size: var(--font-size-small);
line-height: var(--line-height-small);
color: var(--color-base--subtle, #54595d);
}

.prototype-chrome-menu-panel__field {
display: flex;
flex-direction: column;
gap: var(--spacing-50, 8px);
}

.prototype-chrome-menu-panel__label {
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
}

.prototype-chrome-menu-panel__section {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-50, 8px);
}
</style>

<!-- Teleported popover: allow the select menu to extend past the scrollable body. -->
<style>
.prototype-chrome-menu-popover__overlay .cdx-popover__body {
overflow: visible;
}
</style>
41 changes: 41 additions & 0 deletions src/components/PrototypeChromeMenuPopover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue'

import { CdxPopover } from '@wikimedia/codex'

import PrototypeChromeMenuPanel from './PrototypeChromeMenuPanel.vue'

const open = ref(false)
const anchor = ref<HTMLElement | null>(null)

function toggle(): void {
open.value = !open.value
}
</script>

<template>
<div class="prototype-chrome-menu-popover">
<span ref="anchor" class="prototype-chrome-menu-popover__trigger">
<slot :open="open" :toggle="toggle" />
</span>
<CdxPopover
v-model:open="open"
:anchor="anchor"
placement="bottom-start"
class="prototype-chrome-menu-popover__overlay"
>
<PrototypeChromeMenuPanel @click.stop />
</CdxPopover>
</div>
</template>

<style scoped>
.prototype-chrome-menu-popover {
display: inline-flex;
flex-shrink: 0;
}

.prototype-chrome-menu-popover__trigger {
display: inline-flex;
}
</style>
30 changes: 23 additions & 7 deletions src/components/chrome/ChromeHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { DEFAULT_CHROME_NAV_TOOLS, type ChromeNavTool } from './headerNavTools'
import { globalSkin, globalTheme } from '@/theme'
import type { Skin, Theme } from '@/theme'
import PrototypeUserSettingsPopover from '../PrototypeUserSettingsPopover.vue'
import PrototypeChromeMenuPopover from '../PrototypeChromeMenuPopover.vue'
import SearchBar from '../SearchBar/SearchBar.vue'

const { user } = useConfig()
Expand Down Expand Up @@ -97,10 +98,17 @@ function navHas(tool: ChromeNavTool): boolean {
<nav v-if="isDesktop" class="chrome-header__nav-desktop" aria-label="Site">
<div class="chrome-header__desktop-start">
<slot name="menu">
<!-- Mock only — not interactive (FakeMediaWiki uses bare chrome / icon affordances). -->
<span class="chrome-header__menu-icon" aria-hidden="true">
<CdxIcon :icon="cdxIconMenu" />
</span>
<PrototypeChromeMenuPopover v-slot="{ toggle, open }">
<CdxButton
class="chrome-header__menu-btn"
weight="quiet"
aria-label="Main menu"
:aria-expanded="open"
@click="toggle"
>
<CdxIcon :icon="cdxIconMenu" />
</CdxButton>
</PrototypeChromeMenuPopover>
</slot>

<RouterLink class="chrome-header__brand-link" to="/" aria-label="Visit the main page">
Expand Down Expand Up @@ -226,9 +234,17 @@ function navHas(tool: ChromeNavTool): boolean {
<!-- Minerva-style chrome (mobile skin) -->
<nav v-else class="chrome-header__nav-mobile" aria-label="Site">
<slot name="menu">
<CdxButton weight="quiet" size="large" aria-label="Main menu">
<CdxIcon :icon="cdxIconMenu" />
</CdxButton>
<PrototypeChromeMenuPopover v-slot="{ toggle, open }">
<CdxButton
weight="quiet"
size="large"
aria-label="Main menu"
:aria-expanded="open"
@click="toggle"
>
<CdxIcon :icon="cdxIconMenu" />
</CdxButton>
</PrototypeChromeMenuPopover>
</slot>

<RouterLink class="chrome-header__mobile-brand" to="/" aria-label="Visit the main page">
Expand Down
130 changes: 130 additions & 0 deletions src/composables/usePrototypeDevSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { computed, ref, watch, type ComputedRef, type Ref } from 'vue'

export type MorelikeStrategy = 'minimal' | 'opening_text' | 'serial'

export type MicrotaskSource = 'random' | 'quality-check'

export interface PrototypeDevSettings {
morelikeStrategy: MorelikeStrategy
microtaskSource: MicrotaskSource
}

export const DEFAULT_PROTOTYPE_DEV_SETTINGS: PrototypeDevSettings = {
morelikeStrategy: 'serial',
microtaskSource: 'quality-check',
}

export const MORELIKE_STRATEGY_MENU_ITEMS: { value: MorelikeStrategy; label: string }[] = [
{ value: 'minimal', label: 'Minimal (wiki default)' },
{ value: 'opening_text', label: 'Opening text only' },
{ value: 'serial', label: 'Serial multi-call' },
]

export const MICROTASK_SOURCE_MENU_ITEMS: { value: MicrotaskSource; label: string }[] = [
{ value: 'random', label: 'Random from catalog' },
{ value: 'quality-check', label: 'Microtask Generator API' },
]

const STORAGE_KEY = 'protowiki-prototype-dev-settings'

const VALID_STRATEGIES: MorelikeStrategy[] = ['minimal', 'opening_text', 'serial']
const VALID_MICROTASK_SOURCES: MicrotaskSource[] = ['random', 'quality-check']

function isMorelikeStrategy(value: unknown): value is MorelikeStrategy {
return typeof value === 'string' && VALID_STRATEGIES.includes(value as MorelikeStrategy)
}

function isMicrotaskSource(value: unknown): value is MicrotaskSource {
return typeof value === 'string' && VALID_MICROTASK_SOURCES.includes(value as MicrotaskSource)
}

export function normalizePrototypeDevSettings(input: unknown): PrototypeDevSettings {
if (typeof input !== 'object' || input === null) {
return { ...DEFAULT_PROTOTYPE_DEV_SETTINGS }
}

const record = input as Record<string, unknown>

return {
morelikeStrategy: isMorelikeStrategy(record.morelikeStrategy)
? record.morelikeStrategy
: DEFAULT_PROTOTYPE_DEV_SETTINGS.morelikeStrategy,
microtaskSource: isMicrotaskSource(record.microtaskSource)
? record.microtaskSource
: DEFAULT_PROTOTYPE_DEV_SETTINGS.microtaskSource,
}
}

export function loadPrototypeDevSettings(): PrototypeDevSettings {
if (typeof window === 'undefined') {
return { ...DEFAULT_PROTOTYPE_DEV_SETTINGS }
}

try {
const stored = window.localStorage.getItem(STORAGE_KEY)
if (!stored) return { ...DEFAULT_PROTOTYPE_DEV_SETTINGS }

const parsed = JSON.parse(stored)
return normalizePrototypeDevSettings(parsed)
} catch {
return { ...DEFAULT_PROTOTYPE_DEV_SETTINGS }
}
}

export function savePrototypeDevSettings(settings: PrototypeDevSettings): void {
if (typeof window === 'undefined') return

try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
} catch {
// Quota or private-mode failures — ignore.
}
}

/** Module-level singleton so homepage and drill-down share dev settings. */
const devSettings = ref<PrototypeDevSettings>(loadPrototypeDevSettings())

let persistenceInitialized = false

function ensurePersistenceWatch(): void {
if (persistenceInitialized) return
persistenceInitialized = true

watch(
devSettings,
(value) => {
savePrototypeDevSettings(value)
},
{ deep: true },
)
}

export function usePrototypeDevSettings(): {
morelikeStrategy: ComputedRef<MorelikeStrategy>
microtaskSource: ComputedRef<MicrotaskSource>
settings: Ref<PrototypeDevSettings>
} {
ensurePersistenceWatch()

const morelikeStrategy = computed({
get: () => devSettings.value.morelikeStrategy,
set: (value: MorelikeStrategy | null | undefined) => {
if (!isMorelikeStrategy(value)) return
devSettings.value = { ...devSettings.value, morelikeStrategy: value }
},
})

const microtaskSource = computed({
get: () => devSettings.value.microtaskSource,
set: (value: MicrotaskSource | null | undefined) => {
if (!isMicrotaskSource(value)) return
devSettings.value = { ...devSettings.value, microtaskSource: value }
},
})

return {
morelikeStrategy,
microtaskSource,
settings: devSettings,
}
}
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ export function wikiBaseUrlFromLang(lang: string): string {
return `https://${wikiHostFromLang(lang)}/`
}

/** VisualEditor / wikitext edit URL for an article title. */
export function wikiEditUrlFromLang(lang: string, title: string): string {
const trimmed = title.trim()
const pageTitle = trimmed.replace(/ /g, '_')
return `https://${wikiHostFromLang(lang)}/w/index.php?title=${encodeURIComponent(pageTitle)}&action=edit`
}

export function langForUser(
user: ConfigUser,
userPageLists: Record<ConfigUser, UserPageLists>,
Expand Down
Loading
Loading