@webx-ui/module-recipes 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +502 -488
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -4
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/api.ts","../src/editor.ts","../src/messages.ts","../src/i18n.ts","../src/RecipeEditorPage.vue","../src/RecipeHistory.vue","../src/minutes.ts","../src/RecipeCreateDialog.vue","../src/RecipesPage.vue","../src/module.ts"],"sourcesContent":["import { reorderItems, type AdminContext } from '@webx-ui/module-admin'\nimport type {\n RecipeDetail,\n RecipeInput,\n RecipeQuery,\n RecipeRow,\n RecipeSave,\n RecipesList,\n RecipeVersion,\n} from './types'\n\nexport interface RecipesApi {\n /** Every recipe, with what the list can be narrowed by. */\n list(query?: RecipeQuery): Promise<RecipesList>\n /** One recipe as its editor opens it: the record, the screen's values and the revision. */\n get(id: number): Promise<RecipeDetail>\n /** A new recipe, made in one transaction: a refused address leaves no bare row behind. */\n create(input: RecipeInput): Promise<Pick<RecipeDetail, 'recipe' | 'values'>>\n /**\n * Save the draft. Refused with a 409 when the revision is stale — the body is a\n * {@link RecipeConflict} with the recipe as it now is.\n */\n save(id: number, input: RecipeSave): Promise<RecipeDetail>\n /** Throw away what is waiting and keep what the site is showing. */\n discard(id: number): Promise<RecipeDetail>\n publish(id: number): Promise<RecipeRow>\n unpublish(id: number): Promise<RecipeRow>\n remove(id: number): Promise<void>\n restore(id: number): Promise<RecipeRow>\n /** The publications, newest first. */\n versions(id: number): Promise<RecipeVersion[]>\n /** An old publication becomes the draft; putting it on the site is a separate step. */\n restoreVersion(id: number, number: number): Promise<RecipeDetail>\n /** The one order recipes have — never a category's (decision 4). */\n reorder(ids: number[]): Promise<void>\n}\n\n/** The path of the list under the panel's API. */\nexport const RECIPES_API = 'recipes'\n\n/**\n * Everything under `/recipes`, below the panel's API path — except the categories and the\n * nutrients, which are the panel's shared category API under `recipes/categories` and\n * `recipes/nutrients`.\n */\nexport function createRecipesApi(admin: AdminContext): RecipesApi {\n const base = `${admin.apiPath}/${RECIPES_API}`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n list: (query = {}) => {\n const search = new URLSearchParams()\n\n // Only what was asked for: `category=` would ask for the recipes of no category.\n if (query.q) search.set('q', query.q)\n if (query.category != null) search.set('category', String(query.category))\n if (query.nutrient != null) search.set('nutrient', String(query.nutrient))\n if (query.service != null) search.set('service', String(query.service))\n if (query.status) search.set('status', query.status)\n if (query.trashed) search.set('trashed', '1')\n\n return admin.http.get<RecipesList>(search.size > 0 ? `${base}?${search}` : base)\n },\n get: (id) => admin.http.get<{ data: RecipeDetail }>(`${base}/${id}`).then(data),\n create: (input) =>\n admin.http.post<{ data: Pick<RecipeDetail, 'recipe' | 'values'> }>(base, input).then(data),\n save: (id, input) => admin.http.put<{ data: RecipeDetail }>(`${base}/${id}`, input).then(data),\n discard: (id) =>\n admin.http.post<{ data: RecipeDetail }>(`${base}/${id}/discard`, {}).then(data),\n publish: (id) => admin.http.post<{ data: RecipeRow }>(`${base}/${id}/publish`, {}).then(data),\n unpublish: (id) =>\n admin.http.post<{ data: RecipeRow }>(`${base}/${id}/unpublish`, {}).then(data),\n remove: (id) => admin.http.delete<void>(`${base}/${id}`).then(() => undefined),\n restore: (id) => admin.http.post<{ data: RecipeRow }>(`${base}/${id}/restore`, {}).then(data),\n versions: (id) =>\n admin.http.get<{ data: RecipeVersion[] }>(`${base}/${id}/versions`).then(data),\n restoreVersion: (id, number) =>\n admin.http\n .post<{ data: RecipeDetail }>(`${base}/${id}/versions/${number}/restore`, {})\n .then(data),\n reorder: (ids) => reorderItems(admin, RECIPES_API, ids, null),\n }\n}\n","import { inject, provide, type InjectionKey, type Ref } from 'vue'\nimport type { RecipeRow } from './types'\n\n/**\n * What the editor knows and the nodes of `recipes.form` do not: the recipe being edited, and a\n * way to read it again. The history needs both — its list is keyed by the recipe, and a restored\n * version has to reach the fields the form is bound to.\n */\nexport interface RecipeEditorContext {\n /** The recipe as the server last answered it, or `null` while the first request is out. */\n recipe: Ref<RecipeRow | null>\n /** Whether this administrator may write at all. */\n canManage: boolean\n /** Ask the server for the recipe again — after a restore, a publication, a discard. */\n reload(): Promise<void>\n}\n\nexport const recipeEditorKey: InjectionKey<RecipeEditorContext> = Symbol('wx-recipe-editor')\n\nexport function provideRecipeEditor(editor: RecipeEditorContext): void {\n provide(recipeEditorKey, editor)\n}\n\n/** The editor above this node, or `null` outside one — a demo or a test draws nothing. */\nexport function useRecipeEditor(): RecipeEditorContext | null {\n return inject(recipeEditorKey, null)\n}\n","import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * The English this package speaks on its own, before the server's dictionary arrives, or without\n * one. The server ships the same lines in ten languages under `webx-recipes::` and overrides\n * these; `messages.test.ts` keeps the two sets of keys equal.\n */\nexport const recipesMessages: Record<string, Messages> = {\n module: {\n group: 'Recipes',\n recipes: 'Recipes',\n categories: 'Categories',\n nutrients: 'Rich in',\n },\n panel: {\n new: 'New recipe',\n 'new-title': 'New recipe',\n 'field-title': 'Title',\n create: 'Create',\n cancel: 'Cancel',\n search: 'Search by title or address',\n empty: 'No recipes yet.',\n 'empty-help':\n 'A recipe is a page of the site with a gallery, ingredients, a method and nutrition, filed under categories.',\n 'empty-search': 'Nothing matches that.',\n 'empty-bin': 'The bin is empty.',\n\n 'status-draft': 'Draft',\n 'status-published': 'Live',\n 'status-modified': 'Live',\n 'status-unpublished': 'Off the site',\n edits: 'edits',\n 'no-address': 'No address in this language',\n 'main-category': 'The main category: it goes in the breadcrumbs',\n minutes: ':count min',\n 'hours-minutes': ':hours h :minutes min',\n hours: ':hours h',\n\n 'view-all': 'All',\n 'view-published': 'Live',\n 'view-draft': 'Drafts',\n 'view-unpublished': 'Off the site',\n bin: 'Bin',\n\n 'filter-category': 'Category',\n 'any-category': 'Any category',\n 'filter-nutrient': 'Rich in',\n 'any-nutrient': 'Rich in anything',\n 'filter-service': 'Service',\n 'any-service': 'Any service',\n\n 'order-all': 'Drag to change the order on the site. It is the one order recipes have.',\n 'order-locked':\n 'Recipes have one order, the same in every category and on every page of the site. Clear the search and the filters to drag it.',\n\n open: 'Open',\n 'open-on-site': 'Open on the site',\n 'copy-address': 'Copy the address',\n 'address-copied': 'The address is on the clipboard.',\n publish: 'Publish',\n unpublish: 'Take off the site',\n delete: 'Delete',\n restore: 'Restore',\n 'delete-title': 'Delete “:title”?',\n 'delete-text': 'It goes to the bin and comes off the site, and its address is free again.',\n deleted: 'The recipe is in the bin.',\n restored: 'The recipe is back.',\n published: 'The recipe is on the site.',\n 'unpublished-done': 'The recipe is off the site.',\n 'reorder-failed': 'The new order was not saved.',\n },\n recipe: {\n trail: 'Where this recipe sits',\n untitled: 'Untitled',\n save: 'Save',\n 'save-failed': 'The recipe was not saved.',\n 'publish-title': 'Put “:title” on the site?',\n 'publish-text':\n 'It answers at :address from the moment you do, for everyone — with its categories, services and similar recipes as they are chosen now.',\n 'publish-nowhere': 'It has no address in this language yet, so nothing will answer.',\n preview: 'Preview',\n discard: 'Discard changes',\n 'discard-title': 'Discard what is waiting?',\n 'discard-text':\n 'The recipe goes back to what the site is showing. What was written since is not listed anywhere.',\n discarded: 'The recipe is back to what is published.',\n 'conflict-title': 'The recipe changed while you were editing',\n 'conflict-mine': 'Keep mine',\n 'conflict-theirs': 'Take the newer version',\n 'conflict-theirs-title': 'Give up what you wrote?',\n 'conflict-theirs-text':\n 'The recipe is read again as it now is, and what you have typed since goes.',\n leave: 'Leave',\n 'leave-title': 'Leave without saving?',\n 'leave-text': 'The recipe could not be saved, and what you wrote is not on the server.',\n 'live-since': 'On the site since :date',\n 'address-moving':\n 'The address is changing. The old one keeps working and leads to the new one.',\n 'history-empty': 'This recipe has never been published.',\n version: '#:number',\n 'version-live': 'On the site',\n 'source-panel': 'From the panel',\n 'source-mcp': 'By an agent',\n 'source-import': 'Imported',\n 'restore-title': 'Restore version :number?',\n 'restore-text':\n 'It becomes the draft. The site keeps showing what is published until you publish this.',\n 'restore-version': 'Restore',\n 'restored-version': 'Version :number is now the draft.',\n },\n category: {\n new: 'New category',\n empty: 'No categories yet.',\n 'empty-help': 'A category is a section of the recipes with a page of its own.',\n order: 'Drag to change the order of the categories on the site.',\n hidden: 'Hidden from the site',\n 'no-address': 'No address in this language',\n recipes: 'Recipes: :count',\n 'show-recipes': 'Show its recipes',\n 'delete-blocked': 'This category still holds recipes. Move them first.',\n 'delete-text': 'It goes to the bin and its page comes off the site. Its recipes stay.',\n deleted: 'The category is in the bin.',\n saved: 'The category is saved.',\n 'field-title': 'Title',\n 'field-slug': 'Address',\n 'address-moving':\n 'The address is changing. The old one keeps working and leads to the new one.',\n },\n nutrient: {\n new: 'New nutrient',\n empty: 'Nothing here yet.',\n 'empty-help':\n 'What a recipe is rich in — iron, fibre, protein. A chip on the recipe and a filter of the catalogue, with no page of its own.',\n order: 'Drag to change the order of the chips and of the filter on the site.',\n hidden: 'Hidden from the site',\n recipes: 'Recipes: :count',\n 'show-recipes': 'Show its recipes',\n 'delete-blocked': 'Recipes are still marked with this. Take it off them first.',\n 'delete-text': 'It goes to the bin and drops out of the filter. The recipes stay.',\n deleted: 'It is in the bin.',\n saved: 'Saved.',\n 'field-title': 'Title',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { recipesMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/** Puts this package's English under the panel's dictionary, once per panel. */\nexport function useRecipesMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-recipes', recipesMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'\nimport {\n provideRecordAddress,\n provideRelationOwner,\n useAdmin,\n useDates,\n useErrorText,\n useTranslate,\n WxSaveState,\n WxScreen,\n WxScreenHead,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport {\n confirm,\n localizedValue,\n toast,\n useLocales,\n WxActionBar,\n WxAlert,\n WxBadge,\n WxBreadcrumb,\n WxBreadcrumbItem,\n WxButton,\n WxCard,\n WxSkeleton,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport { createRecipesApi } from './api'\nimport { provideRecipeEditor } from './editor'\nimport { useRecipesMessages } from './i18n'\nimport type { RecipeConflict, RecipeDetail, RecipeRow } from './types'\n\n/**\n * The editor of one recipe: a head that stays put, the described screen under it, and the bar\n * along the bottom where it is saved and published (§5.9).\n *\n * Everything between the head and the bar is `recipes.form` — the recipe, its settings, the SEO\n * card `module-seo` patches on and the history — so a project adds its own field (an author's\n * note) with a patch rather than a fork of this file. Saving is by autosave into the draft, and\n * the draft carries the categories, the services and the similar recipes too: the site changes,\n * all of it at once, only on \"Publish\". Edits are guarded by a revision: a save over somebody\n * else's is refused with a 409 and the question of which version the site gets.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/recipes' })\n\nconst context = useAdmin()\nconst api = createRecipesApi(context)\nconst route = useRoute()\nconst router = useRouter()\nconst locales = useLocales()\nconst dates = useDates()\nuseRecipesMessages()\n\nconst t = useTranslate('webx-recipes')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\n/** How long after the last keystroke the draft goes to the server. */\nconst PAUSE = 1200\n\nconst id = computed(() => Number(route.params.id))\nconst list = computed(() => props.base)\n\nconst recipe = ref<RecipeRow | null>(null)\nconst values = ref<ScreenModel>({})\nconst revision = ref('')\nconst prefix = ref<string | null>(null)\nconst previewUrl = ref<string | null>(null)\n\nconst loading = ref(true)\nconst saving = ref(false)\nconst working = ref(false)\nconst errors = ref<Record<string, string[]>>({})\nconst conflict = ref<RecipeConflict | null>(null)\n\nconst snapshot = ref('')\n\nconst canManage = computed(() => context.can('recipes.manage'))\n\n/** Closed for writing: no permission, or a publication in flight. */\nconst locked = computed(() => !canManage.value || working.value)\n\nconst current = computed(() => JSON.stringify(values.value))\nconst dirty = computed(() => snapshot.value !== '' && current.value !== snapshot.value)\n\nconst state = computed<'saving' | 'unsaved' | 'saved'>(() => {\n if (saving.value) return 'saving'\n\n return dirty.value ? 'unsaved' : 'saved'\n})\n\n/** The name follows the field rather than the answer: a rename shows at the top as it is typed. */\nconst title = computed(() => {\n const written = values.value.title as LocalizedValue | string | null | undefined\n\n return localizedValue(written, locales.active.value, recipe.value?.title ?? '')\n})\n\n/** Since when it is on the site, under the name — the one fact the badge beside it cannot carry. */\nconst publication = computed(() => {\n const row = recipe.value\n\n if (!row || (row.status !== 'published' && row.status !== 'modified')) return ''\n\n return t('recipe.live-since', { date: dates.short(row.published_at) })\n})\n\n/* \"Similar recipes\" pick from recipes, and never offer the one they are on. */\nprovideRelationOwner({ type: 'recipe', id: computed(() => recipe.value?.id ?? null) })\n\n/* The address field is the panel's shared one (`wx-slug`), and this is what it prints. */\nprovideRecordAddress({\n values,\n prefix,\n path: computed(() => recipe.value?.path),\n moving: () => t('recipe.address-moving'),\n})\n\nprovideRecipeEditor({ recipe, canManage: canManage.value, reload: () => load(true) })\n\nfunction take(detail: RecipeDetail): void {\n recipe.value = detail.recipe\n values.value = detail.values\n revision.value = detail.revision\n prefix.value = detail.prefix\n previewUrl.value = detail.preview_url\n snapshot.value = JSON.stringify(detail.values)\n conflict.value = null\n}\n\n/**\n * Read the recipe again — quietly unless this is the first time: the skeleton replaces the whole\n * screen, and a publication answered with it would throw the editor back onto the first tab.\n */\nasync function load(silent = false): Promise<void> {\n if (!silent) loading.value = true\n\n try {\n take(await api.get(id.value))\n } catch (error) {\n toast.danger(message(error))\n void router.push(list.value)\n } finally {\n loading.value = false\n }\n}\n\n/* One pending save at a time, and one pending pause. */\nlet timer: ReturnType<typeof setTimeout> | undefined\n\nfunction schedule(): void {\n if (!canManage.value || conflict.value || !dirty.value) return\n\n clearTimeout(timer)\n timer = setTimeout(() => void save(), PAUSE)\n}\n\n/** A field was left: write now rather than at the end of a pause nobody is waiting through. */\nfunction onFocusOut(): void {\n if (!canManage.value || conflict.value || !dirty.value || saving.value) return\n\n clearTimeout(timer)\n void save()\n}\n\nasync function save(): Promise<void> {\n if (!recipe.value || saving.value || !canManage.value) return\n\n clearTimeout(timer)\n\n // What is being sent, so that whatever is typed while the request is in flight stays dirty and\n // gets its own save rather than being marked as written.\n const sending = current.value\n const sent = values.value\n\n saving.value = true\n errors.value = {}\n\n try {\n const detail = await api.save(recipe.value.id, { values: sent, revision: revision.value })\n\n recipe.value = detail.recipe\n revision.value = detail.revision\n prefix.value = detail.prefix\n previewUrl.value = detail.preview_url\n conflict.value = null\n\n if (current.value === sending) snapshot.value = sending\n } catch (error) {\n const failure = error as {\n status?: number\n body?: RecipeConflict & { errors?: Record<string, string[]> }\n }\n\n if (failure.status === 409 && failure.body?.data) {\n conflict.value = failure.body\n } else if (failure.body?.errors) {\n errors.value = failure.body.errors\n toast.danger(t('recipe.save-failed'))\n } else {\n toast.danger(message(error))\n }\n } finally {\n saving.value = false\n }\n}\n\n/** Give up what was typed and take the recipe as it now is. */\nasync function takeTheirs(): Promise<void> {\n const agreed = await confirm({\n title: t('recipe.conflict-theirs-title'),\n message: t('recipe.conflict-theirs-text'),\n confirmText: t('recipe.conflict-theirs'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n await load(true)\n}\n\n/** Keep what was typed and write it over the other version — which is still in the history. */\nasync function keepMine(): Promise<void> {\n const theirs = conflict.value\n\n if (!theirs) return\n\n revision.value = theirs.data.revision\n conflict.value = null\n\n await save()\n}\n\n/** Throw away what is waiting — asked about, because it is the one button here that loses writing. */\nasync function discard(): Promise<void> {\n if (!recipe.value) return\n\n const agreed = await confirm({\n title: t('recipe.discard-title'),\n message: t('recipe.discard-text'),\n confirmText: t('recipe.discard'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n working.value = true\n\n try {\n take(await api.discard(recipe.value.id))\n toast.success(t('recipe.discarded'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\n/** Publishing is asked about, because it is the one action here that visitors see. */\nasync function publish(): Promise<void> {\n const row = recipe.value\n\n if (!row) return\n\n const agreed = await confirm({\n title: t('recipe.publish-title', { title: title.value || row.title }),\n message:\n row.path === null\n ? t('recipe.publish-nowhere')\n : t('recipe.publish-text', { address: `/${row.path}` }),\n confirmText: t('panel.publish'),\n cancelText: t('panel.cancel'),\n })\n\n if (!agreed) return\n\n if (dirty.value) await save()\n if (conflict.value) return\n\n working.value = true\n\n try {\n await api.publish(row.id)\n await load(true)\n toast.success(t('panel.published'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\nfunction badge(): 'default' | 'success' {\n const status = recipe.value?.status\n\n return status === 'published' || status === 'modified' ? 'success' : 'default'\n}\n\n/* The browser's own guard: it cannot wait for a request, so all it can do is ask. */\nfunction leaveGuard(event: BeforeUnloadEvent): void {\n if (dirty.value) event.preventDefault()\n}\n\nwatch(current, schedule)\nwatch(id, () => void load())\n\nonMounted(() => {\n void load()\n window.addEventListener('beforeunload', leaveGuard)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(timer)\n window.removeEventListener('beforeunload', leaveGuard)\n})\n\n/**\n * Leaving flushes the pause rather than asking about it; the question is asked only when the save\n * did not go through — a conflict, a refused field, a server that is not there.\n */\nonBeforeRouteLeave(async () => {\n if (!dirty.value || !canManage.value) return true\n\n await save()\n\n if (!dirty.value) return true\n\n return await confirm({\n title: t('recipe.leave-title'),\n message: t('recipe.leave-text'),\n confirmText: t('recipe.leave'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n})\n\n/* What leads away from the recipe. On a phone the head folds them into the ···. */\nconst actions = computed<ScreenAction[]>(() => {\n const leads: ScreenAction[] = []\n\n if (previewUrl.value) {\n leads.push({ key: 'preview', label: t('recipe.preview'), icon: 'eye', href: previewUrl.value })\n }\n\n if (recipe.value?.url && recipe.value.status !== 'draft') {\n leads.push({\n key: 'site',\n label: t('panel.open-on-site'),\n icon: 'link',\n href: recipe.value.url,\n })\n }\n\n // Throwing away writing lives in the ···, never beside \"publish\", and only while there is a\n // difference between what is written and what is on the site.\n if (recipe.value?.status === 'modified') {\n leads.push({\n key: 'discard',\n label: t('recipe.discard'),\n icon: 'refresh',\n danger: true,\n menu: true,\n run: () => void discard(),\n })\n }\n\n return leads\n})\n</script>\n\n<template>\n <div class=\"wx-recipe-editor\" @focusout=\"onFocusOut\">\n <template v-if=\"loading || !recipe\">\n <wx-skeleton class=\"wx-recipe-editor__ghost\" title :rows=\"1\" />\n <wx-card><wx-skeleton :rows=\"8\" /></wx-card>\n </template>\n\n <template v-else>\n <wx-screen-head\n divider\n :back=\"list\"\n :back-label=\"t('module.recipes')\"\n :title=\"title || t('recipe.untitled')\"\n :subtitle=\"publication\"\n :actions=\"actions\"\n >\n <template #trail>\n <wx-breadcrumb size=\"sm\" :label=\"t('recipe.trail')\">\n <wx-breadcrumb-item :as=\"'router-link'\" :to=\"list\">\n {{ t('module.recipes') }}\n </wx-breadcrumb-item>\n <wx-breadcrumb-item current>{{ title || t('recipe.untitled') }}</wx-breadcrumb-item>\n </wx-breadcrumb>\n </template>\n\n <template #title-after>\n <wx-badge :type=\"badge()\" dot>{{ t(`panel.status-${recipe.status}`) }}</wx-badge>\n <wx-badge v-if=\"recipe.status === 'modified'\" type=\"primary\" round>\n {{ t('panel.edits') }}\n </wx-badge>\n </template>\n </wx-screen-head>\n\n <!-- Somebody else wrote while this editor was typing. Both versions still exist, so the\n question is which one the site gets — a question, not a toast that disappears. -->\n <wx-alert\n v-if=\"conflict\"\n type=\"warning\"\n :title=\"t('recipe.conflict-title')\"\n :description=\"conflict.message\"\n >\n <template #actions>\n <wx-button size=\"sm\" variant=\"outline\" @click=\"takeTheirs\">\n {{ t('recipe.conflict-theirs') }}\n </wx-button>\n <wx-button size=\"sm\" type=\"primary\" @click=\"keepMine\">\n {{ t('recipe.conflict-mine') }}\n </wx-button>\n </template>\n </wx-alert>\n\n <wx-screen v-model=\"values\" name=\"recipes.form\" :errors=\"errors\" :disabled=\"locked\" />\n\n <wx-action-bar v-if=\"canManage\">\n <template #state>\n <wx-save-state :state=\"state\" />\n </template>\n\n <wx-button variant=\"outline\" :loading=\"saving\" :disabled=\"!dirty\" @click=\"save\">\n {{ t('recipe.save') }}\n </wx-button>\n\n <wx-button\n type=\"primary\"\n :loading=\"working\"\n :disabled=\"recipe.status === 'published' && !dirty\"\n @click=\"publish\"\n >\n {{ t('panel.publish') }}\n </wx-button>\n </wx-action-bar>\n </template>\n </div>\n</template>\n\n<style scoped>\n/*\n * No constructor here, so nothing needs the height of the window: the page scrolls, and the\n * panel gives a screen with an action bar a floor tall enough for the bar to sit at the bottom\n * (`WxMain`, CLAUDE.md §4 on `data-wx-fill`).\n */\n.wx-recipe-editor {\n display: flex;\n flex-direction: column;\n gap: var(--wx-gap, var(--wx-space-16));\n min-height: 0;\n container-type: inline-size;\n}\n\n.wx-recipe-editor__ghost {\n max-width: 420px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useAdmin, useErrorText, useTranslate, WxDate } from '@webx-ui/module-admin'\nimport { confirm, toast, WxBadge, WxButton, WxEmpty, WxSkeleton, WxText } from '@webx-ui/core'\nimport { createRecipesApi } from './api'\nimport { useRecipeEditor } from './editor'\nimport { useRecipesMessages } from './i18n'\nimport type { RecipeVersion } from './types'\n\n/**\n * What was published, when, by whom and from where — publications only; the autosaves are\n * insurance, not history.\n *\n * Restoring makes the old version the draft. Publishing it is the same separate step it always\n * is, so nothing here changes the site by itself.\n */\ndefineOptions({ name: 'WxRecipeHistory' })\n\nconst context = useAdmin()\nconst api = createRecipesApi(context)\nconst editor = useRecipeEditor()\nuseRecipesMessages()\n\nconst t = useTranslate('webx-recipes')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst versions = ref<RecipeVersion[] | null>(null)\nconst working = ref(false)\n\nconst recipe = computed(() => editor?.recipe.value ?? null)\nconst canManage = computed(() => editor?.canManage ?? false)\n\n/** The newest publication is the site — while the recipe is on it at all. */\nconst live = computed(() =>\n recipe.value?.status === 'published' || recipe.value?.status === 'modified'\n ? (versions.value?.[0]?.number ?? null)\n : null,\n)\n\nasync function load(): Promise<void> {\n const current = recipe.value\n\n if (!current) return\n\n try {\n versions.value = await api.versions(current.id)\n } catch (error) {\n versions.value = []\n toast.danger(message(error))\n }\n}\n\nasync function restore(version: RecipeVersion): Promise<void> {\n const current = recipe.value\n\n if (!current) return\n\n const agreed = await confirm({\n title: t('recipe.restore-title', { number: version.number }),\n message: t('recipe.restore-text'),\n confirmText: t('recipe.restore-version'),\n cancelText: t('panel.cancel'),\n })\n\n if (!agreed) return\n\n working.value = true\n\n try {\n await api.restoreVersion(current.id, version.number)\n // Through the editor rather than with the answer: the values it holds are what the form is\n // bound to, and a version restored underneath it has to reach the fields.\n await editor?.reload()\n toast.success(t('recipe.restored-version', { number: version.number }))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\n// The recipe is read again after every publication and every restore, so its stamp is the\n// signal that there is something new to list.\nwatch(\n () => [recipe.value?.id, recipe.value?.published_at, recipe.value?.updated_at],\n () => void load(),\n { immediate: true },\n)\n</script>\n\n<template>\n <div class=\"wx-recipe-history\">\n <wx-skeleton v-if=\"versions === null\" class=\"wx-recipe-history__ghost\" :rows=\"3\" />\n <wx-empty v-else-if=\"versions.length === 0\" :description=\"t('recipe.history-empty')\" />\n <div\n v-for=\"version in versions\"\n v-else\n :key=\"version.number\"\n class=\"wx-recipe-history__row\"\n :class=\"{ 'is-live': version.number === live }\"\n >\n <code class=\"wx-recipe-history__number\">{{\n t('recipe.version', { number: version.number })\n }}</code>\n <div class=\"wx-recipe-history__who\">\n <wx-date v-if=\"version.created_at\" :value=\"version.created_at\" size=\"md\" tone=\"default\" />\n <wx-text size=\"sm\" tone=\"muted\">\n {{ version.author ?? t(`recipe.source-${version.source}`) }}\n <template v-if=\"version.comment\"> · {{ version.comment }}</template>\n </wx-text>\n </div>\n <wx-badge v-if=\"version.number === live\" type=\"success\" dot>{{\n t('recipe.version-live')\n }}</wx-badge>\n <wx-button\n v-if=\"canManage && version.number !== live\"\n size=\"sm\"\n variant=\"outline\"\n :loading=\"working\"\n @click=\"restore(version)\"\n >\n {{ t('recipe.restore-version') }}\n </wx-button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.wx-recipe-history {\n display: flex;\n flex-direction: column;\n border: 1px solid var(--wx-border-default);\n border-radius: var(--wx-radius-md);\n background: var(--wx-bg-surface);\n overflow: hidden;\n}\n\n.wx-recipe-history__ghost {\n padding: var(--wx-space-10) var(--wx-space-12);\n}\n\n.wx-recipe-history__row {\n display: flex;\n align-items: center;\n gap: var(--wx-space-12);\n padding: var(--wx-space-10) var(--wx-space-12);\n border-block-end: 1px solid var(--wx-border-default);\n flex-wrap: wrap;\n}\n\n.wx-recipe-history__row:last-child {\n border-block-end: 0;\n}\n\n.wx-recipe-history__number {\n width: 40px;\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-sm);\n color: var(--wx-text-muted);\n}\n\n.wx-recipe-history__who {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 160px;\n}\n</style>\n","type Translate = (key: string, replace?: Record<string, string | number>) => string\n\n/**\n * `total_minutes` the way the list says it: `45 min`, `1 h 15 min`, `2 h`. The number is at the\n * end of nothing and the start of nothing a plural would bend (CLAUDE.md §4 on `:count`), so one\n * line per shape is enough in every language.\n */\nexport function formatMinutes(minutes: number | null | undefined, t: Translate): string {\n if (minutes == null || minutes <= 0) return ''\n\n const hours = Math.floor(minutes / 60)\n const rest = minutes % 60\n\n if (hours === 0) return t('panel.minutes', { count: rest })\n if (rest === 0) return t('panel.hours', { hours })\n\n return t('panel.hours-minutes', { hours, minutes: rest })\n}\n","<script setup lang=\"ts\">\nimport { nextTick, onMounted, ref, useTemplateRef } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxButton, WxDialog, WxFormItem, WxInput, WxSpace } from '@webx-ui/core'\nimport { createRecipesApi } from './api'\nimport { useRecipesMessages } from './i18n'\nimport type { RecipeRow } from './types'\n\n/**\n * A new recipe: what it is called, and nothing else. The server makes the address out of the\n * title; the address field on the settings tab is where there is room to say what changing it\n * costs.\n */\nconst { open, resolve, dismiss } = useModal<RecipeRow>()\n\nconst context = useAdmin()\nconst api = createRecipesApi(context)\nuseRecipesMessages()\n\nconst t = useTranslate('webx-recipes')\n\nconst title = ref('')\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\nconst field = useTemplateRef<HTMLElement>('field')\n\nonMounted(async () => {\n await nextTick()\n field.value?.querySelector('input')?.focus()\n})\n\nasync function submit(): Promise<void> {\n if (title.value.trim() === '' || saving.value) return\n\n saving.value = true\n errors.value = {}\n\n try {\n resolve((await api.create({ title: title.value.trim() })).recipe)\n } catch (error) {\n // The address is what can be refused here, and it is refused under the title that made it:\n // the registry sees the categories and the pages too, so \"taken\" can mean taken by either.\n errors.value = (error as { body?: { errors?: Record<string, string[]> } }).body?.errors ?? {}\n } finally {\n saving.value = false\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('panel.new-title')\" :width=\"440\">\n <wx-form-item\n :label=\"t('panel.field-title')\"\n :error=\"errors.title?.[0] ?? errors.slug?.[0]\"\n required\n >\n <div ref=\"field\">\n <wx-input v-model=\"title\" :aria-label=\"t('panel.field-title')\" @keyup.enter=\"submit\" />\n </div>\n </wx-form-item>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('panel.cancel') }}</wx-button>\n <wx-button type=\"primary\" :loading=\"saving\" :disabled=\"title.trim() === ''\" @click=\"submit\">\n {{ t('panel.create') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport {\n useAdmin,\n useErrorText,\n useTranslate,\n WxListScreen,\n WxRowMenu,\n type RowAction,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n toast,\n WxAlert,\n WxBadge,\n WxEmpty,\n WxIcon,\n WxInput,\n WxSelect,\n WxSkeleton,\n WxSortableList,\n WxTooltip,\n type TabItem,\n type TabValue,\n} from '@webx-ui/core'\nimport { createRecipesApi } from './api'\nimport { useRecipesMessages } from './i18n'\nimport { formatMinutes } from './minutes'\nimport RecipeCreateDialog from './RecipeCreateDialog.vue'\nimport type { RecipeRow, RecipeStatus, RecipesList } from './types'\n\n/**\n * Every recipe at once, in the order the site shows them (§5.9).\n *\n * No paginator: this list is where recipes are put in order, and a drag cannot cross a page\n * boundary. Recipes have one order and only one (decision 4) — no order inside a category, none\n * inside a service — so the grips are there only while the list is the whole of it. Any filter\n * makes it a selection with invisible gaps in it, the grips go away and the line under the list\n * says why: that is the difference from services, where a category has an order of its own.\n *\n * A row is a card rather than a line of cells, which is what makes the same list work on a\n * phone: the picture, the name and the address on one side, the categories and the state beside\n * them on a wide screen and under them on a narrow one.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/recipes' })\n\nconst context = useAdmin()\nconst api = createRecipesApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseRecipesMessages()\n\nconst t = useTranslate('webx-recipes')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<RecipeRow[]>([])\nconst filters = ref<RecipesList['filters']>({ categories: [], nutrients: [], services: null })\nconst loading = ref(true)\n\nconst create = createModal<RecipeRow, Record<string, never>>(RecipeCreateDialog)\n\nconst canManage = computed(() => context.can('recipes.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'recipes')?.title ??\n t('module.recipes'),\n)\n\n/*\n * Everything the list is looking at lives in the address, so coming back from a recipe lands on\n * the same view, the same filters and the same search.\n */\nconst view = computed<TabValue>({\n get: () => (typeof route.query.view === 'string' ? route.query.view : ''),\n set: (value) => {\n void router.replace({\n query: { ...route.query, view: value === '' ? undefined : String(value) },\n })\n },\n})\n\nconst inBin = computed(() => view.value === 'trashed')\n\nfunction idIn(name: string): number | null {\n const raw = route.query[name]\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n}\n\nconst category = computed(() => idIn('category'))\nconst nutrient = computed(() => idIn('nutrient'))\nconst service = computed(() => idIn('service'))\n\nconst q = computed(() => (typeof route.query.q === 'string' ? route.query.q : ''))\n\n/* What is typed, ahead of the address: the address follows it after a pause. */\nconst typed = ref(q.value)\n\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('panel.view-all') },\n { value: 'published', label: t('panel.view-published') },\n { value: 'draft', label: t('panel.view-draft') },\n { value: 'unpublished', label: t('panel.view-unpublished') },\n { value: 'trashed', label: t('panel.bin'), icon: 'trash' },\n])\n\n/** The whole list, with nothing narrowing it — the only list whose order can be dragged. */\nconst whole = computed(\n () =>\n view.value === '' &&\n q.value.trim() === '' &&\n category.value === null &&\n nutrient.value === null &&\n service.value === null,\n)\n\n/* Dragged only by somebody who may write, and only in a list that is the whole order. */\nconst sortable = computed(() => canManage.value && whole.value)\n\nconst hint = computed(() => t(whole.value ? 'panel.order-all' : 'panel.order-locked'))\n\nconst emptyText = computed(() => {\n if (q.value !== '') return t('panel.empty-search')\n\n return inBin.value ? t('panel.empty-bin') : t('panel.empty')\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n const answer = await api.list({\n q: q.value,\n category: category.value,\n nutrient: nutrient.value,\n service: service.value,\n status: inBin.value ? '' : (view.value as RecipeStatus | ''),\n trashed: inBin.value,\n })\n\n rows.value = answer.data\n filters.value = answer.filters\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(\n () => [view.value, category.value, nutrient.value, service.value, q.value] as const,\n () => void load(),\n)\nvoid load()\n\nlet pause: ReturnType<typeof setTimeout> | undefined\n\nwatch(typed, (value) => {\n clearTimeout(pause)\n pause = setTimeout(() => {\n void router.replace({ query: { ...route.query, q: value.trim() === '' ? undefined : value } })\n }, 300)\n})\n\nonBeforeUnmount(() => clearTimeout(pause))\n\nfunction narrow(name: 'category' | 'nutrient' | 'service', value: unknown): void {\n void router.replace({\n query: { ...route.query, [name]: typeof value === 'number' ? String(value) : undefined },\n })\n}\n\nconst options = (list: { id: number; title: string }[] | null) =>\n (list ?? []).map((item) => ({ value: item.id, label: item.title }))\n\nfunction open(recipe: RecipeRow): void {\n void router.push({ path: `${props.base}/${recipe.id}`, query: { ...route.query } })\n}\n\n/** A new recipe: the dialog asks for a title, the editor opens. */\nasync function add(): Promise<void> {\n const made = await create({})\n\n if (made) open(made)\n}\n\n/**\n * The order on screen, written as a whole. The list is already in its new order\n * (`WxSortableList` reorders the model before it says so), so a failure has to put it back\n * rather than leave the screen disagreeing with the database.\n */\nasync function reorder(): Promise<void> {\n try {\n await api.reorder(rows.value.map((row) => row.id))\n } catch (error) {\n toast.danger(message(error, t('panel.reorder-failed')))\n await load()\n }\n}\n\nfunction actionsFor(recipe: RecipeRow): RowAction[] {\n if (!canManage.value) return []\n\n if (inBin.value) {\n return [\n {\n key: 'restore',\n icon: 'refresh',\n label: t('panel.restore'),\n run: () => void restore(recipe),\n },\n ]\n }\n\n const actions: RowAction[] = [\n { key: 'open', icon: 'edit', label: t('panel.open'), run: () => open(recipe) },\n ]\n\n if (recipe.url && recipe.status !== 'draft') {\n actions.push({\n key: 'site',\n icon: 'external-link',\n label: t('panel.open-on-site'),\n href: recipe.url,\n })\n }\n\n if (recipe.url) {\n actions.push({\n key: 'copy',\n icon: 'copy',\n label: t('panel.copy-address'),\n run: () => void copyAddress(recipe),\n })\n }\n\n actions.push(\n recipe.status === 'published' || recipe.status === 'modified'\n ? {\n key: 'unpublish',\n icon: 'eye-off',\n label: t('panel.unpublish'),\n run: () => void run(() => api.unpublish(recipe.id), t('panel.unpublished-done')),\n }\n : {\n key: 'publish',\n icon: 'upload',\n label: t('panel.publish'),\n run: () => void run(() => api.publish(recipe.id), t('panel.published')),\n },\n )\n\n actions.push({\n key: 'delete',\n icon: 'trash',\n label: t('panel.delete'),\n danger: true,\n run: () => void remove(recipe),\n })\n\n return actions\n}\n\nasync function remove(recipe: RecipeRow): Promise<void> {\n const agreed = await confirm({\n title: t('panel.delete-title', { title: recipe.title }),\n message: t('panel.delete-text'),\n confirmText: t('panel.delete'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n await run(() => api.remove(recipe.id), t('panel.deleted'))\n}\n\nasync function restore(recipe: RecipeRow): Promise<void> {\n await run(() => api.restore(recipe.id), t('panel.restored'))\n}\n\n/** One request, one sentence about it, and the list as the server now sees it. */\nasync function run(request: () => Promise<unknown>, said: string): Promise<void> {\n try {\n await request()\n toast.success(said)\n } catch (error) {\n toast.danger(message(error))\n } finally {\n await load()\n }\n}\n\nasync function copyAddress(recipe: RecipeRow): Promise<void> {\n if (!recipe.url) return\n\n try {\n await navigator.clipboard.writeText(recipe.url)\n toast.success(t('panel.address-copied'))\n } catch {\n // A browser that refuses the clipboard without a gesture it trusts; the address is on screen.\n }\n}\n\n/* Live and live-with-edits are the same green; what is waiting has a chip of its own. */\nfunction badge(status: RecipeStatus): 'default' | 'success' {\n return status === 'published' || status === 'modified' ? 'success' : 'default'\n}\n\nfunction address(recipe: RecipeRow): string {\n if (inBin.value) return '—'\n\n return recipe.path === null ? t('panel.no-address') : `/${recipe.path}`\n}\n\n/* What the section offers. Declared, because on a phone the head folds it into the ···. */\nconst actions = computed<ScreenAction[]>(() =>\n canManage.value\n ? [{ key: 'new', label: t('panel.new'), icon: 'plus', primary: true, run: () => void add() }]\n : [],\n)\n</script>\n\n<template>\n <div class=\"wx-recipes\">\n <wx-list-screen\n v-model:view=\"view\"\n :title=\"title\"\n :views=\"views\"\n :actions=\"actions\"\n padding=\"sm\"\n >\n <div class=\"wx-recipes__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-recipes__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('panel.search')\"\n :aria-label=\"t('panel.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n\n <!-- In sight rather than behind a funnel: each of them decides whether the list can be\n dragged, and the line under the list says so. -->\n <wx-select\n :model-value=\"category\"\n :options=\"options(filters.categories)\"\n :placeholder=\"t('panel.any-category')\"\n :aria-label=\"t('panel.filter-category')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow('category', $event)\"\n />\n <wx-select\n :model-value=\"nutrient\"\n :options=\"options(filters.nutrients)\"\n :placeholder=\"t('panel.any-nutrient')\"\n :aria-label=\"t('panel.filter-nutrient')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow('nutrient', $event)\"\n />\n <!-- `null` from the server: the services module is not installed, and there is nothing\n a recipe could be related to. -->\n <wx-select\n v-if=\"filters.services !== null\"\n :model-value=\"service\"\n :options=\"options(filters.services)\"\n :placeholder=\"t('panel.any-service')\"\n :aria-label=\"t('panel.filter-service')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow('service', $event)\"\n />\n </div>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-recipes__loading\" :rows=\"5\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"emptyText\"\n :description=\"whole ? t('panel.empty-help') : undefined\"\n />\n\n <template v-else>\n <!--\n A grip and not the whole row: the row opens a recipe, and a row that both opens and\n drags is one where one of the two happens by accident. No grips at all where the list\n is a selection — a handle that moves rows past invisible ones would be lying.\n -->\n <wx-sortable-list\n v-model=\"rows\"\n class=\"wx-recipes__list\"\n :class=\"{ 'is-loading': loading }\"\n plain\n item-key=\"id\"\n :handle=\"sortable ? 'grip' : 'row'\"\n :item-label=\"(item: RecipeRow) => item.title\"\n :disabled=\"!sortable\"\n @move=\"reorder\"\n >\n <template #default=\"{ item }\">\n <component\n :is=\"inBin ? 'div' : 'router-link'\"\n class=\"wx-recipe-row\"\n v-bind=\"inBin ? {} : { to: { path: `${props.base}/${item.id}`, query: route.query } }\"\n >\n <!-- The picture's box, never the picture: `<img>` has `min-width: auto`, and a\n photograph would take the row (CLAUDE.md §4). -->\n <span class=\"wx-recipe-row__cover\" :class=\"{ 'is-empty': !item.cover?.thumb }\">\n <img v-if=\"item.cover?.thumb\" :src=\"item.cover.thumb\" alt=\"\" loading=\"lazy\" />\n <wx-icon v-else name=\"image\" size=\"sm\" />\n </span>\n\n <span class=\"wx-recipe-row__name\">\n <span class=\"wx-recipe-row__title\">{{ item.title }}</span>\n <span class=\"wx-recipe-row__address\">{{ address(item) }}</span>\n </span>\n\n <span class=\"wx-recipe-row__facts\">\n <span class=\"wx-recipe-row__chips\">\n <template v-for=\"(chip, index) in item.categories\" :key=\"chip.id\">\n <wx-tooltip v-if=\"index === 0\" :content=\"t('panel.main-category')\">\n <wx-badge size=\"sm\" round type=\"primary\">{{ chip.title }}</wx-badge>\n </wx-tooltip>\n <wx-badge v-else size=\"sm\" round>{{ chip.title }}</wx-badge>\n </template>\n </span>\n\n <span class=\"wx-recipe-row__state\">\n <span v-if=\"item.minutes\" class=\"wx-recipe-row__time\">\n <wx-icon name=\"clock\" size=\"sm\" />{{ formatMinutes(item.minutes, t) }}\n </span>\n <wx-badge :type=\"badge(item.status)\" dot size=\"sm\">\n {{ t(`panel.status-${item.status}`) }}\n </wx-badge>\n <wx-badge v-if=\"item.status === 'modified'\" type=\"primary\" size=\"sm\" round>\n {{ t('panel.edits') }}\n </wx-badge>\n </span>\n </span>\n </component>\n </template>\n\n <template #actions=\"{ item }\">\n <wx-row-menu :actions=\"actionsFor(item)\" :label=\"item.title\" />\n </template>\n </wx-sortable-list>\n\n <!-- What a drag does here, or why there is nothing to drag — under the rows it is about. -->\n <wx-alert\n v-if=\"canManage && !inBin\"\n class=\"wx-recipes__note\"\n type=\"info\"\n :description=\"hint\"\n />\n </template>\n </wx-list-screen>\n </div>\n</template>\n\n<style scoped>\n.wx-recipes {\n min-width: 0;\n container-type: inline-size;\n}\n\n.wx-recipes__bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-8);\n padding: var(--wx-space-4) var(--wx-space-8) var(--wx-space-8);\n}\n\n.wx-recipes__search {\n flex: 1 1 240px;\n min-width: 0;\n}\n\n/* The select is a fragment (it carries its dropdown beside it), so our scope attribute never\n reaches it: styled from our own wrapper (CLAUDE.md §4). */\n.wx-recipes__bar > :deep(.wx-select) {\n flex: 1 1 160px;\n max-width: 220px;\n}\n\n.wx-recipes__loading {\n padding: var(--wx-space-8);\n}\n\n.wx-recipes__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-recipes__list :deep(.wx-sortable-list__row) {\n padding-inline: var(--wx-space-8);\n}\n\n.wx-recipes__list :deep(.wx-sortable-list__row:hover) {\n background: var(--wx-bg-subtle);\n border-radius: var(--wx-radius-sm);\n}\n\n.wx-recipes__note {\n margin-block-start: var(--wx-space-8);\n}\n\n.wx-recipe-row {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) auto;\n grid-template-areas: 'cover name facts';\n align-items: center;\n gap: var(--wx-space-4) var(--wx-space-12);\n flex: 1 1 auto;\n min-width: 0;\n padding-block: var(--wx-space-4);\n color: inherit;\n text-decoration: none;\n}\n\n.wx-recipe-row__cover {\n grid-area: cover;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 32px;\n overflow: hidden;\n border-radius: var(--wx-radius-xs);\n background: var(--wx-bg-subtle);\n color: var(--wx-text-muted);\n}\n\n.wx-recipe-row__cover.is-empty {\n border: 1px dashed var(--wx-border-default);\n background: var(--wx-bg-surface);\n}\n\n.wx-recipe-row__cover img {\n width: 100%;\n height: 100%;\n min-width: 0;\n object-fit: cover;\n}\n\n.wx-recipe-row__name {\n grid-area: name;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.wx-recipe-row__title {\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.wx-recipe-row__address {\n overflow: hidden;\n color: var(--wx-text-muted);\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-xs);\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.wx-recipe-row__facts {\n grid-area: facts;\n display: flex;\n align-items: center;\n gap: var(--wx-space-12);\n min-width: 0;\n}\n\n.wx-recipe-row__chips,\n.wx-recipe-row__state {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-4);\n min-width: 0;\n}\n\n.wx-recipe-row__chips {\n justify-content: flex-end;\n max-width: 320px;\n}\n\n.wx-recipe-row__time {\n display: inline-flex;\n align-items: center;\n gap: var(--wx-space-4);\n color: var(--wx-text-muted);\n font-size: var(--wx-font-size-xs);\n white-space: nowrap;\n}\n\n/*\n * Narrow: the facts go under the name rather than beside it, so the name keeps the width. By the\n * width of the list, not of the window — the panel's sidebar decides how much the list has.\n */\n@container (max-width: 640px) {\n .wx-recipe-row {\n grid-template-columns: auto minmax(0, 1fr);\n grid-template-areas:\n 'cover name'\n 'cover facts';\n align-items: start;\n column-gap: var(--wx-space-8);\n }\n\n .wx-recipe-row__facts {\n flex-wrap: wrap;\n gap: var(--wx-space-4);\n }\n\n /* One wrapping line of badges rather than two boxes side by side (see the services list). */\n .wx-recipe-row__chips,\n .wx-recipe-row__state {\n display: contents;\n }\n\n .wx-recipes__list :deep(.wx-sortable-list__row) {\n gap: var(--wx-space-8);\n padding-inline: var(--wx-space-4);\n }\n\n .wx-recipes__bar {\n padding-inline: var(--wx-space-4);\n }\n\n .wx-recipes__bar > :deep(.wx-select) {\n max-width: none;\n }\n}\n</style>\n","import { categoryRoutes, type AdminModule, type CategoriesOptions } from '@webx-ui/module-admin'\nimport RecipeEditorPage from './RecipeEditorPage.vue'\nimport RecipeHistory from './RecipeHistory.vue'\nimport RecipesPage from './RecipesPage.vue'\n\nexport interface RecipesOptions {\n /** Where the recipes live inside the panel. The categories and the nutrients sit under it. */\n path?: string\n}\n\n/**\n * The categories of recipes as the panel's shared category screens see them: where they answer,\n * what they are edited on and what this module calls them. Exported so that a panel mounting the\n * screens somewhere of its own does not have to repeat the words.\n */\nexport function recipeCategoriesOptions(path = '/recipes'): CategoriesOptions {\n return {\n api: 'recipes/categories',\n path: `${path}/categories`,\n name: 'webx.recipes.categories',\n module: 'recipe-categories',\n screen: 'recipes.category-form',\n manage: 'recipes.categories.manage',\n count: 'recipes_count',\n // A category has no order of its own inside it (decision 4): the link shows what is in it.\n items: (id) => ({ path, query: { category: String(id) } }),\n words: {\n new: 'webx-recipes::category.new',\n empty: 'webx-recipes::category.empty',\n 'empty-help': 'webx-recipes::category.empty-help',\n order: 'webx-recipes::category.order',\n hidden: 'webx-recipes::category.hidden',\n 'no-address': 'webx-recipes::category.no-address',\n count: 'webx-recipes::category.recipes',\n 'show-items': 'webx-recipes::category.show-recipes',\n 'delete-blocked': 'webx-recipes::category.delete-blocked',\n 'delete-text': 'webx-recipes::category.delete-text',\n deleted: 'webx-recipes::category.deleted',\n saved: 'webx-recipes::category.saved',\n 'field-title': 'webx-recipes::category.field-title',\n 'field-slug': 'webx-recipes::category.field-slug',\n 'address-moving': 'webx-recipes::category.address-moving',\n },\n }\n}\n\n/**\n * What a recipe is rich in — the second kind of category a recipe has (decision 8), with no\n * address: the shared list leaves the address line out when the server says there is none, as it\n * does for the categories of the FAQ.\n */\nexport function recipeNutrientsOptions(path = '/recipes'): CategoriesOptions {\n return {\n api: 'recipes/nutrients',\n path: `${path}/nutrients`,\n name: 'webx.recipes.nutrients',\n module: 'recipe-nutrients',\n screen: 'recipes.nutrient-form',\n manage: 'recipes.categories.manage',\n count: 'recipes_count',\n items: (id) => ({ path, query: { nutrient: String(id) } }),\n words: {\n new: 'webx-recipes::nutrient.new',\n empty: 'webx-recipes::nutrient.empty',\n 'empty-help': 'webx-recipes::nutrient.empty-help',\n order: 'webx-recipes::nutrient.order',\n hidden: 'webx-recipes::nutrient.hidden',\n count: 'webx-recipes::nutrient.recipes',\n 'show-items': 'webx-recipes::nutrient.show-recipes',\n 'delete-blocked': 'webx-recipes::nutrient.delete-blocked',\n 'delete-text': 'webx-recipes::nutrient.delete-text',\n deleted: 'webx-recipes::nutrient.deleted',\n saved: 'webx-recipes::nutrient.saved',\n 'field-title': 'webx-recipes::nutrient.field-title',\n },\n }\n}\n\n/**\n * The recipes as sections of the panel: recipes, their categories and what they are rich in\n * (§5.9).\n *\n * Three modules rather than one, because the navigation is one entry per module; the server puts\n * them in the `recipes` group, which is what draws them under one heading. A section whose server\n * half is not installed never appears — the entry is built from the manifest.\n */\nexport function recipes(options: RecipesOptions = {}): AdminModule[] {\n const path = options.path ?? '/recipes'\n\n return [\n {\n id: 'recipes',\n path,\n routes: [\n { path, name: 'webx.recipes', component: RecipesPage, props: { base: path } },\n {\n path: `${path}/:id(\\\\d+)`,\n name: 'webx.recipes.edit',\n component: RecipeEditorPage,\n props: { base: path },\n },\n ],\n /*\n * The one part of `recipes.form` only this module can draw. Everything else on it is the\n * panel's (`wx-slug`, `wx-categories`, `wx-relations`, `wx-gallery`, `wx-rich-text`).\n */\n types: {\n 'wx-recipe-history': { component: RecipeHistory, kind: 'display' },\n },\n },\n {\n id: 'recipe-categories',\n path: `${path}/categories`,\n routes: categoryRoutes(recipeCategoriesOptions(path)),\n },\n {\n id: 'recipe-nutrients',\n path: `${path}/nutrients`,\n routes: categoryRoutes(recipeNutrientsOptions(path)),\n },\n ]\n}\n"],"names":["RECIPES_API","createRecipesApi","admin","base","data","body","query","search","id","input","number","ids","reorderItems","recipeEditorKey","provideRecipeEditor","editor","provide","useRecipeEditor","inject","recipesMessages","seeded","useRecipesMessages","i18n","useAdmin","PAUSE","props","__props","context","api","route","useRoute","router","useRouter","locales","useLocales","dates","useDates","t","useTranslate","message","useErrorText","computed","list","recipe","ref","values","revision","prefix","previewUrl","loading","saving","working","errors","conflict","snapshot","canManage","locked","current","dirty","state","title","written","localizedValue","publication","row","provideRelationOwner","provideRecordAddress","load","take","detail","silent","error","toast","timer","schedule","save","onFocusOut","sending","sent","failure","takeTheirs","confirm","keepMine","theirs","discard","publish","badge","status","leaveGuard","event","watch","onMounted","onBeforeUnmount","onBeforeRouteLeave","actions","leads","_createElementBlock","_Fragment","_createVNode","_unref","WxSkeleton","WxCard","WxScreenHead","WxBreadcrumb","WxBreadcrumbItem","_createTextVNode","_toDisplayString","WxBadge","_createBlock","WxAlert","WxButton","WxScreen","$event","WxActionBar","WxSaveState","versions","live","restore","version","_openBlock","_hoisted_1","WxEmpty","_renderList","_createElementVNode","_hoisted_2","_hoisted_3","WxDate","WxText","formatMinutes","minutes","hours","rest","open","resolve","dismiss","useModal","field","useTemplateRef","nextTick","submit","WxDialog","WxSpace","WxFormItem","WxInput","rows","filters","create","createModal","RecipeCreateDialog","module","view","value","inBin","idIn","name","raw","category","nutrient","service","q","typed","views","whole","sortable","hint","emptyText","answer","pause","narrow","options","item","add","made","reorder","actionsFor","copyAddress","run","remove","request","said","address","WxListScreen","WxIcon","WxSelect","_cache","WxSortableList","_normalizeClass","_withCtx","_resolveDynamicComponent","_mergeProps","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","chip","index","WxTooltip","_hoisted_9","_hoisted_10","WxRowMenu","recipeCategoriesOptions","path","recipeNutrientsOptions","recipes","RecipesPage","RecipeEditorPage","RecipeHistory","categoryRoutes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/api.ts","../src/editor.ts","../src/messages.ts","../src/i18n.ts","../src/RecipeEditorPage.vue","../src/RecipeHistory.vue","../src/minutes.ts","../src/RecipeCreateDialog.vue","../src/RecipesPage.vue","../src/module.ts"],"sourcesContent":["import { reorderItems, type AdminContext } from '@webx-ui/module-admin'\nimport type {\n RecipeDetail,\n RecipeInput,\n RecipeQuery,\n RecipeRow,\n RecipeSave,\n RecipesList,\n RecipeVersion,\n} from './types'\n\nexport interface RecipesApi {\n /** Every recipe, with what the list can be narrowed by. */\n list(query?: RecipeQuery): Promise<RecipesList>\n /** One recipe as its editor opens it: the record, the screen's values and the revision. */\n get(id: number): Promise<RecipeDetail>\n /** A new recipe, made in one transaction: a refused address leaves no bare row behind. */\n create(input: RecipeInput): Promise<Pick<RecipeDetail, 'recipe' | 'values'>>\n /**\n * Save the draft. Refused with a 409 when the revision is stale — the body is a\n * {@link RecipeConflict} with the recipe as it now is.\n */\n save(id: number, input: RecipeSave): Promise<RecipeDetail>\n /** Throw away what is waiting and keep what the site is showing. */\n discard(id: number): Promise<RecipeDetail>\n publish(id: number): Promise<RecipeRow>\n unpublish(id: number): Promise<RecipeRow>\n remove(id: number): Promise<void>\n restore(id: number): Promise<RecipeRow>\n /** The publications, newest first. */\n versions(id: number): Promise<RecipeVersion[]>\n /** An old publication becomes the draft; putting it on the site is a separate step. */\n restoreVersion(id: number, number: number): Promise<RecipeDetail>\n /** The one order recipes have — never a category's (decision 4). */\n reorder(ids: number[]): Promise<void>\n}\n\n/** The path of the list under the panel's API. */\nexport const RECIPES_API = 'recipes'\n\n/**\n * Everything under `/recipes`, below the panel's API path — except the categories and the\n * nutrients, which are the panel's shared category API under `recipes/categories` and\n * `recipes/nutrients`.\n */\nexport function createRecipesApi(admin: AdminContext): RecipesApi {\n const base = `${admin.apiPath}/${RECIPES_API}`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n list: (query = {}) => {\n const search = new URLSearchParams()\n\n // Only what was asked for: `category=` would ask for the recipes of no category.\n if (query.q) search.set('q', query.q)\n if (query.category != null) search.set('category', String(query.category))\n if (query.nutrient != null) search.set('nutrient', String(query.nutrient))\n if (query.service != null) search.set('service', String(query.service))\n if (query.status) search.set('status', query.status)\n if (query.trashed) search.set('trashed', '1')\n\n return admin.http.get<RecipesList>(search.size > 0 ? `${base}?${search}` : base)\n },\n get: (id) => admin.http.get<{ data: RecipeDetail }>(`${base}/${id}`).then(data),\n create: (input) =>\n admin.http.post<{ data: Pick<RecipeDetail, 'recipe' | 'values'> }>(base, input).then(data),\n save: (id, input) => admin.http.put<{ data: RecipeDetail }>(`${base}/${id}`, input).then(data),\n discard: (id) =>\n admin.http.post<{ data: RecipeDetail }>(`${base}/${id}/discard`, {}).then(data),\n publish: (id) => admin.http.post<{ data: RecipeRow }>(`${base}/${id}/publish`, {}).then(data),\n unpublish: (id) =>\n admin.http.post<{ data: RecipeRow }>(`${base}/${id}/unpublish`, {}).then(data),\n remove: (id) => admin.http.delete<void>(`${base}/${id}`).then(() => undefined),\n restore: (id) => admin.http.post<{ data: RecipeRow }>(`${base}/${id}/restore`, {}).then(data),\n versions: (id) =>\n admin.http.get<{ data: RecipeVersion[] }>(`${base}/${id}/versions`).then(data),\n restoreVersion: (id, number) =>\n admin.http\n .post<{ data: RecipeDetail }>(`${base}/${id}/versions/${number}/restore`, {})\n .then(data),\n reorder: (ids) => reorderItems(admin, RECIPES_API, ids, null),\n }\n}\n","import { inject, provide, type InjectionKey, type Ref } from 'vue'\nimport type { RecipeRow } from './types'\n\n/**\n * What the editor knows and the nodes of `recipes.form` do not: the recipe being edited, and a\n * way to read it again. The history needs both — its list is keyed by the recipe, and a restored\n * version has to reach the fields the form is bound to.\n */\nexport interface RecipeEditorContext {\n /** The recipe as the server last answered it, or `null` while the first request is out. */\n recipe: Ref<RecipeRow | null>\n /** Whether this administrator may write at all. */\n canManage: boolean\n /** Ask the server for the recipe again — after a restore, a publication, a discard. */\n reload(): Promise<void>\n}\n\nexport const recipeEditorKey: InjectionKey<RecipeEditorContext> = Symbol('wx-recipe-editor')\n\nexport function provideRecipeEditor(editor: RecipeEditorContext): void {\n provide(recipeEditorKey, editor)\n}\n\n/** The editor above this node, or `null` outside one — a demo or a test draws nothing. */\nexport function useRecipeEditor(): RecipeEditorContext | null {\n return inject(recipeEditorKey, null)\n}\n","import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * The English this package speaks on its own, before the server's dictionary arrives, or without\n * one. The server ships the same lines in ten languages under `webx-recipes::` and overrides\n * these; `messages.test.ts` keeps the two sets of keys equal.\n */\nexport const recipesMessages: Record<string, Messages> = {\n module: {\n group: 'Recipes',\n recipes: 'Recipes',\n categories: 'Categories',\n nutrients: 'Rich in',\n },\n panel: {\n new: 'New recipe',\n 'new-title': 'New recipe',\n 'field-title': 'Title',\n create: 'Create',\n cancel: 'Cancel',\n search: 'Search by title or address',\n empty: 'No recipes yet.',\n 'empty-help':\n 'A recipe is a page of the site with a gallery, ingredients, a method and nutrition, filed under categories.',\n 'empty-search': 'Nothing matches that.',\n 'empty-bin': 'The bin is empty.',\n\n 'status-draft': 'Draft',\n 'status-published': 'Live',\n 'status-modified': 'Live',\n 'status-unpublished': 'Off the site',\n edits: 'edits',\n 'no-address': 'No address in this language',\n 'main-category': 'The main category: it goes in the breadcrumbs',\n minutes: ':count min',\n 'hours-minutes': ':hours h :minutes min',\n hours: ':hours h',\n\n 'view-all': 'All',\n 'view-published': 'Live',\n 'view-draft': 'Drafts',\n 'view-unpublished': 'Off the site',\n bin: 'Bin',\n\n 'filter-category': 'Category',\n 'any-category': 'Any category',\n 'filter-nutrient': 'Rich in',\n 'any-nutrient': 'Rich in anything',\n 'filter-service': 'Service',\n 'any-service': 'Any service',\n\n 'order-all': 'Drag to change the order on the site. It is the one order recipes have.',\n 'order-locked':\n 'Recipes have one order, the same in every category and on every page of the site. Clear the search and the filters to drag it.',\n\n open: 'Open',\n 'open-on-site': 'Open on the site',\n 'copy-address': 'Copy the address',\n 'address-copied': 'The address is on the clipboard.',\n publish: 'Publish',\n unpublish: 'Take off the site',\n delete: 'Delete',\n restore: 'Restore',\n 'delete-title': 'Delete “:title”?',\n 'delete-text': 'It goes to the bin and comes off the site, and its address is free again.',\n deleted: 'The recipe is in the bin.',\n restored: 'The recipe is back.',\n published: 'The recipe is on the site.',\n 'unpublished-done': 'The recipe is off the site.',\n 'reorder-failed': 'The new order was not saved.',\n },\n recipe: {\n trail: 'Where this recipe sits',\n untitled: 'Untitled',\n save: 'Save',\n 'save-failed': 'The recipe was not saved.',\n 'publish-title': 'Put “:title” on the site?',\n 'publish-text':\n 'It answers at :address from the moment you do, for everyone — with its categories, services and similar recipes as they are chosen now.',\n 'publish-nowhere': 'It has no address in this language yet, so nothing will answer.',\n preview: 'Preview',\n discard: 'Discard changes',\n 'discard-title': 'Discard what is waiting?',\n 'discard-text':\n 'The recipe goes back to what the site is showing. What was written since is not listed anywhere.',\n discarded: 'The recipe is back to what is published.',\n 'conflict-title': 'The recipe changed while you were editing',\n 'conflict-mine': 'Keep mine',\n 'conflict-theirs': 'Take the newer version',\n 'conflict-theirs-title': 'Give up what you wrote?',\n 'conflict-theirs-text':\n 'The recipe is read again as it now is, and what you have typed since goes.',\n leave: 'Leave',\n 'leave-title': 'Leave without saving?',\n 'leave-text': 'The recipe could not be saved, and what you wrote is not on the server.',\n 'live-since': 'On the site since :date',\n 'address-moving':\n 'The address is changing. The old one keeps working and leads to the new one.',\n 'history-empty': 'This recipe has never been published.',\n version: '#:number',\n 'version-live': 'On the site',\n 'source-panel': 'From the panel',\n 'source-mcp': 'By an agent',\n 'source-import': 'Imported',\n 'restore-title': 'Restore version :number?',\n 'restore-text':\n 'It becomes the draft. The site keeps showing what is published until you publish this.',\n 'restore-version': 'Restore',\n 'restored-version': 'Version :number is now the draft.',\n },\n category: {\n new: 'New category',\n empty: 'No categories yet.',\n 'empty-help': 'A category is a section of the recipes with a page of its own.',\n order: 'Drag to change the order of the categories on the site.',\n hidden: 'Hidden from the site',\n 'no-address': 'No address in this language',\n recipes: 'Recipes: :count',\n 'show-recipes': 'Show its recipes',\n 'delete-blocked': 'This category still holds recipes. Move them first.',\n 'delete-text': 'It goes to the bin and its page comes off the site. Its recipes stay.',\n deleted: 'The category is in the bin.',\n saved: 'The category is saved.',\n 'field-title': 'Title',\n 'field-slug': 'Address',\n 'address-moving':\n 'The address is changing. The old one keeps working and leads to the new one.',\n },\n nutrient: {\n new: 'New nutrient',\n empty: 'Nothing here yet.',\n 'empty-help':\n 'What a recipe is rich in — iron, fibre, protein. A chip on the recipe and a filter of the catalogue, with no page of its own.',\n order: 'Drag to change the order of the chips and of the filter on the site.',\n hidden: 'Hidden from the site',\n recipes: 'Recipes: :count',\n 'show-recipes': 'Show its recipes',\n 'delete-blocked': 'Recipes are still marked with this. Take it off them first.',\n 'delete-text': 'It goes to the bin and drops out of the filter. The recipes stay.',\n deleted: 'It is in the bin.',\n saved: 'Saved.',\n 'field-title': 'Title',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { recipesMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/** Puts this package's English under the panel's dictionary, once per panel. */\nexport function useRecipesMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-recipes', recipesMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'\nimport {\n provideRecordAddress,\n provideRelationOwner,\n useAdmin,\n useDates,\n useErrorText,\n useTranslate,\n WxSaveState,\n WxScreen,\n WxScreenHead,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport {\n confirm,\n localizedValue,\n toast,\n useLocales,\n WxActionBar,\n WxAlert,\n WxBadge,\n WxBreadcrumb,\n WxBreadcrumbItem,\n WxButton,\n WxCard,\n WxSkeleton,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport { createRecipesApi } from './api'\nimport { provideRecipeEditor } from './editor'\nimport { useRecipesMessages } from './i18n'\nimport type { RecipeConflict, RecipeDetail, RecipeRow } from './types'\n\n/**\n * The editor of one recipe: a head that stays put, the described screen under it, and the bar\n * along the bottom where it is saved and published (§5.9).\n *\n * Everything between the head and the bar is `recipes.form` — the recipe, its settings, the SEO\n * card `module-seo` patches on and the history — so a project adds its own field (an author's\n * note) with a patch rather than a fork of this file. Saving is by autosave into the draft, and\n * the draft carries the categories, the services and the similar recipes too: the site changes,\n * all of it at once, only on \"Publish\". Edits are guarded by a revision: a save over somebody\n * else's is refused with a 409 and the question of which version the site gets.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/recipes' })\n\nconst context = useAdmin()\nconst api = createRecipesApi(context)\nconst route = useRoute()\nconst router = useRouter()\nconst locales = useLocales()\nconst dates = useDates()\nuseRecipesMessages()\n\nconst t = useTranslate('webx-recipes')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\n/** How long after the last keystroke the draft goes to the server. */\nconst PAUSE = 1200\n\nconst id = computed(() => Number(route.params.id))\nconst list = computed(() => props.base)\n\nconst recipe = ref<RecipeRow | null>(null)\nconst values = ref<ScreenModel>({})\nconst revision = ref('')\nconst prefix = ref<string | null>(null)\nconst previewUrl = ref<string | null>(null)\n\nconst loading = ref(true)\nconst saving = ref(false)\nconst working = ref(false)\nconst errors = ref<Record<string, string[]>>({})\nconst conflict = ref<RecipeConflict | null>(null)\n\nconst snapshot = ref('')\n\nconst canManage = computed(() => context.can('recipes.manage'))\n\n/** Closed for writing: no permission, or a publication in flight. */\nconst locked = computed(() => !canManage.value || working.value)\n\nconst current = computed(() => JSON.stringify(values.value))\nconst dirty = computed(() => snapshot.value !== '' && current.value !== snapshot.value)\n\nconst state = computed<'saving' | 'unsaved' | 'saved'>(() => {\n if (saving.value) return 'saving'\n\n return dirty.value ? 'unsaved' : 'saved'\n})\n\n/** The name follows the field rather than the answer: a rename shows at the top as it is typed. */\nconst title = computed(() => {\n const written = values.value.title as LocalizedValue | string | null | undefined\n\n return localizedValue(written, locales.active.value, recipe.value?.title ?? '')\n})\n\n/** Since when it is on the site, under the name — the one fact the badge beside it cannot carry. */\nconst publication = computed(() => {\n const row = recipe.value\n\n if (!row || (row.status !== 'published' && row.status !== 'modified')) return ''\n\n return t('recipe.live-since', { date: dates.short(row.published_at) })\n})\n\n/* \"Similar recipes\" pick from recipes, and never offer the one they are on. */\nprovideRelationOwner({ type: 'recipe', id: computed(() => recipe.value?.id ?? null) })\n\n/* The address field is the panel's shared one (`wx-slug`), and this is what it prints. */\nprovideRecordAddress({\n values,\n prefix,\n path: computed(() => recipe.value?.path),\n moving: () => t('recipe.address-moving'),\n})\n\nprovideRecipeEditor({ recipe, canManage: canManage.value, reload: () => load(true) })\n\nfunction take(detail: RecipeDetail): void {\n recipe.value = detail.recipe\n values.value = detail.values\n revision.value = detail.revision\n prefix.value = detail.prefix\n previewUrl.value = detail.preview_url\n snapshot.value = JSON.stringify(detail.values)\n conflict.value = null\n}\n\n/**\n * Read the recipe again — quietly unless this is the first time: the skeleton replaces the whole\n * screen, and a publication answered with it would throw the editor back onto the first tab.\n */\nasync function load(silent = false): Promise<void> {\n if (!silent) loading.value = true\n\n try {\n take(await api.get(id.value))\n } catch (error) {\n toast.danger(message(error))\n void router.push(list.value)\n } finally {\n loading.value = false\n }\n}\n\n/* One pending save at a time, and one pending pause. */\nlet timer: ReturnType<typeof setTimeout> | undefined\n\nfunction schedule(): void {\n if (!canManage.value || conflict.value || !dirty.value) return\n\n clearTimeout(timer)\n timer = setTimeout(() => void save(), PAUSE)\n}\n\n/** A field was left: write now rather than at the end of a pause nobody is waiting through. */\nfunction onFocusOut(): void {\n if (!canManage.value || conflict.value || !dirty.value || saving.value) return\n\n clearTimeout(timer)\n void save()\n}\n\n/* The save on its way, and what it carries. */\nlet flight: { carried: string; done: Promise<void> } | undefined\n\n/**\n * A save asked for while another is on its way waits for it instead of being dropped: leaving and\n * publishing read `dirty` right after, and a save that was skipped reads as one that failed.\n */\nasync function save(): Promise<void> {\n while (flight) {\n const { carried, done } = flight\n\n await done\n\n // That request wrote what it carried; go again only for what was typed while it was out.\n if (!dirty.value || conflict.value || current.value === carried) return\n }\n\n const carried = current.value\n const done = write()\n\n flight = { carried, done }\n\n try {\n await done\n } finally {\n if (flight?.done === done) flight = undefined\n }\n}\n\nasync function write(): Promise<void> {\n if (!recipe.value || !canManage.value) return\n\n clearTimeout(timer)\n\n // What is being sent, so that whatever is typed while the request is in flight stays dirty and\n // gets its own save rather than being marked as written.\n const sending = current.value\n const sent = values.value\n\n saving.value = true\n errors.value = {}\n\n try {\n const detail = await api.save(recipe.value.id, { values: sent, revision: revision.value })\n\n recipe.value = detail.recipe\n revision.value = detail.revision\n prefix.value = detail.prefix\n previewUrl.value = detail.preview_url\n conflict.value = null\n\n if (current.value === sending) snapshot.value = sending\n } catch (error) {\n const failure = error as {\n status?: number\n body?: RecipeConflict & { errors?: Record<string, string[]> }\n }\n\n if (failure.status === 409 && failure.body?.data) {\n conflict.value = failure.body\n } else if (failure.body?.errors) {\n errors.value = failure.body.errors\n toast.danger(t('recipe.save-failed'))\n } else {\n toast.danger(message(error))\n }\n } finally {\n saving.value = false\n }\n}\n\n/** Give up what was typed and take the recipe as it now is. */\nasync function takeTheirs(): Promise<void> {\n const agreed = await confirm({\n title: t('recipe.conflict-theirs-title'),\n message: t('recipe.conflict-theirs-text'),\n confirmText: t('recipe.conflict-theirs'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n await load(true)\n}\n\n/** Keep what was typed and write it over the other version — which is still in the history. */\nasync function keepMine(): Promise<void> {\n const theirs = conflict.value\n\n if (!theirs) return\n\n revision.value = theirs.data.revision\n conflict.value = null\n\n await save()\n}\n\n/** Throw away what is waiting — asked about, because it is the one button here that loses writing. */\nasync function discard(): Promise<void> {\n if (!recipe.value) return\n\n const agreed = await confirm({\n title: t('recipe.discard-title'),\n message: t('recipe.discard-text'),\n confirmText: t('recipe.discard'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n working.value = true\n\n try {\n take(await api.discard(recipe.value.id))\n toast.success(t('recipe.discarded'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\n/** Publishing is asked about, because it is the one action here that visitors see. */\nasync function publish(): Promise<void> {\n const row = recipe.value\n\n if (!row) return\n\n const agreed = await confirm({\n title: t('recipe.publish-title', { title: title.value || row.title }),\n message:\n row.path === null\n ? t('recipe.publish-nowhere')\n : t('recipe.publish-text', { address: `/${row.path}` }),\n confirmText: t('panel.publish'),\n cancelText: t('panel.cancel'),\n })\n\n if (!agreed) return\n\n if (dirty.value) await save()\n if (conflict.value) return\n\n working.value = true\n\n try {\n await api.publish(row.id)\n await load(true)\n toast.success(t('panel.published'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\nfunction badge(): 'default' | 'success' {\n const status = recipe.value?.status\n\n return status === 'published' || status === 'modified' ? 'success' : 'default'\n}\n\n/* The browser's own guard: it cannot wait for a request, so all it can do is ask. */\nfunction leaveGuard(event: BeforeUnloadEvent): void {\n if (dirty.value) event.preventDefault()\n}\n\nwatch(current, schedule)\nwatch(id, () => void load())\n\nonMounted(() => {\n void load()\n window.addEventListener('beforeunload', leaveGuard)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(timer)\n window.removeEventListener('beforeunload', leaveGuard)\n})\n\n/**\n * Leaving flushes the pause rather than asking about it; the question is asked only when the save\n * did not go through — a conflict, a refused field, a server that is not there.\n */\nonBeforeRouteLeave(async () => {\n if (!dirty.value || !canManage.value) return true\n\n await save()\n\n if (!dirty.value) return true\n\n return await confirm({\n title: t('recipe.leave-title'),\n message: t('recipe.leave-text'),\n confirmText: t('recipe.leave'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n})\n\n/* What leads away from the recipe. On a phone the head folds them into the ···. */\nconst actions = computed<ScreenAction[]>(() => {\n const leads: ScreenAction[] = []\n\n if (previewUrl.value) {\n leads.push({ key: 'preview', label: t('recipe.preview'), icon: 'eye', href: previewUrl.value })\n }\n\n if (recipe.value?.url && recipe.value.status !== 'draft') {\n leads.push({\n key: 'site',\n label: t('panel.open-on-site'),\n icon: 'link',\n href: recipe.value.url,\n })\n }\n\n // Throwing away writing lives in the ···, never beside \"publish\", and only while there is a\n // difference between what is written and what is on the site.\n if (recipe.value?.status === 'modified') {\n leads.push({\n key: 'discard',\n label: t('recipe.discard'),\n icon: 'refresh',\n danger: true,\n menu: true,\n run: () => void discard(),\n })\n }\n\n return leads\n})\n</script>\n\n<template>\n <div class=\"wx-recipe-editor\" @focusout=\"onFocusOut\">\n <template v-if=\"loading || !recipe\">\n <wx-skeleton class=\"wx-recipe-editor__ghost\" title :rows=\"1\" />\n <wx-card><wx-skeleton :rows=\"8\" /></wx-card>\n </template>\n\n <template v-else>\n <wx-screen-head\n divider\n :back=\"list\"\n :back-label=\"t('module.recipes')\"\n :title=\"title || t('recipe.untitled')\"\n :subtitle=\"publication\"\n :actions=\"actions\"\n >\n <template #trail>\n <wx-breadcrumb size=\"sm\" :label=\"t('recipe.trail')\">\n <wx-breadcrumb-item :as=\"'router-link'\" :to=\"list\">\n {{ t('module.recipes') }}\n </wx-breadcrumb-item>\n <wx-breadcrumb-item current>{{ title || t('recipe.untitled') }}</wx-breadcrumb-item>\n </wx-breadcrumb>\n </template>\n\n <template #title-after>\n <wx-badge :type=\"badge()\" dot>{{ t(`panel.status-${recipe.status}`) }}</wx-badge>\n <wx-badge v-if=\"recipe.status === 'modified'\" type=\"primary\" round>\n {{ t('panel.edits') }}\n </wx-badge>\n </template>\n </wx-screen-head>\n\n <!-- Somebody else wrote while this editor was typing. Both versions still exist, so the\n question is which one the site gets — a question, not a toast that disappears. -->\n <wx-alert\n v-if=\"conflict\"\n type=\"warning\"\n :title=\"t('recipe.conflict-title')\"\n :description=\"conflict.message\"\n >\n <template #actions>\n <wx-button size=\"sm\" variant=\"outline\" @click=\"takeTheirs\">\n {{ t('recipe.conflict-theirs') }}\n </wx-button>\n <wx-button size=\"sm\" type=\"primary\" @click=\"keepMine\">\n {{ t('recipe.conflict-mine') }}\n </wx-button>\n </template>\n </wx-alert>\n\n <wx-screen v-model=\"values\" name=\"recipes.form\" :errors=\"errors\" :disabled=\"locked\" />\n\n <wx-action-bar v-if=\"canManage\">\n <template #state>\n <wx-save-state :state=\"state\" />\n </template>\n\n <wx-button variant=\"outline\" :loading=\"saving\" :disabled=\"!dirty\" @click=\"save\">\n {{ t('recipe.save') }}\n </wx-button>\n\n <wx-button\n type=\"primary\"\n :loading=\"working\"\n :disabled=\"recipe.status === 'published' && !dirty\"\n @click=\"publish\"\n >\n {{ t('panel.publish') }}\n </wx-button>\n </wx-action-bar>\n </template>\n </div>\n</template>\n\n<style scoped>\n/*\n * No constructor here, so nothing needs the height of the window: the page scrolls, and the\n * panel gives a screen with an action bar a floor tall enough for the bar to sit at the bottom\n * (`WxMain`, CLAUDE.md §4 on `data-wx-fill`).\n */\n.wx-recipe-editor {\n display: flex;\n flex-direction: column;\n gap: var(--wx-gap, var(--wx-space-16));\n min-height: 0;\n container-type: inline-size;\n}\n\n.wx-recipe-editor__ghost {\n max-width: 420px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useAdmin, useErrorText, useTranslate, WxDate } from '@webx-ui/module-admin'\nimport { confirm, toast, WxBadge, WxButton, WxEmpty, WxSkeleton, WxText } from '@webx-ui/core'\nimport { createRecipesApi } from './api'\nimport { useRecipeEditor } from './editor'\nimport { useRecipesMessages } from './i18n'\nimport type { RecipeVersion } from './types'\n\n/**\n * What was published, when, by whom and from where — publications only; the autosaves are\n * insurance, not history.\n *\n * Restoring makes the old version the draft. Publishing it is the same separate step it always\n * is, so nothing here changes the site by itself.\n */\ndefineOptions({ name: 'WxRecipeHistory' })\n\nconst context = useAdmin()\nconst api = createRecipesApi(context)\nconst editor = useRecipeEditor()\nuseRecipesMessages()\n\nconst t = useTranslate('webx-recipes')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst versions = ref<RecipeVersion[] | null>(null)\nconst working = ref(false)\n\nconst recipe = computed(() => editor?.recipe.value ?? null)\nconst canManage = computed(() => editor?.canManage ?? false)\n\n/** The newest publication is the site — while the recipe is on it at all. */\nconst live = computed(() =>\n recipe.value?.status === 'published' || recipe.value?.status === 'modified'\n ? (versions.value?.[0]?.number ?? null)\n : null,\n)\n\nasync function load(): Promise<void> {\n const current = recipe.value\n\n if (!current) return\n\n try {\n versions.value = await api.versions(current.id)\n } catch (error) {\n versions.value = []\n toast.danger(message(error))\n }\n}\n\nasync function restore(version: RecipeVersion): Promise<void> {\n const current = recipe.value\n\n if (!current) return\n\n const agreed = await confirm({\n title: t('recipe.restore-title', { number: version.number }),\n message: t('recipe.restore-text'),\n confirmText: t('recipe.restore-version'),\n cancelText: t('panel.cancel'),\n })\n\n if (!agreed) return\n\n working.value = true\n\n try {\n await api.restoreVersion(current.id, version.number)\n // Through the editor rather than with the answer: the values it holds are what the form is\n // bound to, and a version restored underneath it has to reach the fields.\n await editor?.reload()\n toast.success(t('recipe.restored-version', { number: version.number }))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\n// The recipe is read again after every publication and every restore, so its stamp is the\n// signal that there is something new to list.\nwatch(\n () => [recipe.value?.id, recipe.value?.published_at, recipe.value?.updated_at],\n () => void load(),\n { immediate: true },\n)\n</script>\n\n<template>\n <div class=\"wx-recipe-history\">\n <wx-skeleton v-if=\"versions === null\" class=\"wx-recipe-history__ghost\" :rows=\"3\" />\n <wx-empty v-else-if=\"versions.length === 0\" :description=\"t('recipe.history-empty')\" />\n <div\n v-for=\"version in versions\"\n v-else\n :key=\"version.number\"\n class=\"wx-recipe-history__row\"\n :class=\"{ 'is-live': version.number === live }\"\n >\n <code class=\"wx-recipe-history__number\">{{\n t('recipe.version', { number: version.number })\n }}</code>\n <div class=\"wx-recipe-history__who\">\n <wx-date v-if=\"version.created_at\" :value=\"version.created_at\" size=\"md\" tone=\"default\" />\n <wx-text size=\"sm\" tone=\"muted\">\n {{ version.author ?? t(`recipe.source-${version.source}`) }}\n <template v-if=\"version.comment\"> · {{ version.comment }}</template>\n </wx-text>\n </div>\n <wx-badge v-if=\"version.number === live\" type=\"success\" dot>{{\n t('recipe.version-live')\n }}</wx-badge>\n <wx-button\n v-if=\"canManage && version.number !== live\"\n size=\"sm\"\n variant=\"outline\"\n :loading=\"working\"\n @click=\"restore(version)\"\n >\n {{ t('recipe.restore-version') }}\n </wx-button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.wx-recipe-history {\n display: flex;\n flex-direction: column;\n border: 1px solid var(--wx-border-default);\n border-radius: var(--wx-radius-md);\n background: var(--wx-bg-surface);\n overflow: hidden;\n}\n\n.wx-recipe-history__ghost {\n padding: var(--wx-space-10) var(--wx-space-12);\n}\n\n.wx-recipe-history__row {\n display: flex;\n align-items: center;\n gap: var(--wx-space-12);\n padding: var(--wx-space-10) var(--wx-space-12);\n border-block-end: 1px solid var(--wx-border-default);\n flex-wrap: wrap;\n}\n\n.wx-recipe-history__row:last-child {\n border-block-end: 0;\n}\n\n.wx-recipe-history__number {\n width: 40px;\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-sm);\n color: var(--wx-text-muted);\n}\n\n.wx-recipe-history__who {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 160px;\n}\n</style>\n","type Translate = (key: string, replace?: Record<string, string | number>) => string\n\n/**\n * `total_minutes` the way the list says it: `45 min`, `1 h 15 min`, `2 h`. The number is at the\n * end of nothing and the start of nothing a plural would bend (CLAUDE.md §4 on `:count`), so one\n * line per shape is enough in every language.\n */\nexport function formatMinutes(minutes: number | null | undefined, t: Translate): string {\n if (minutes == null || minutes <= 0) return ''\n\n const hours = Math.floor(minutes / 60)\n const rest = minutes % 60\n\n if (hours === 0) return t('panel.minutes', { count: rest })\n if (rest === 0) return t('panel.hours', { hours })\n\n return t('panel.hours-minutes', { hours, minutes: rest })\n}\n","<script setup lang=\"ts\">\nimport { nextTick, onMounted, ref, useTemplateRef } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxButton, WxDialog, WxFormItem, WxInput, WxSpace } from '@webx-ui/core'\nimport { createRecipesApi } from './api'\nimport { useRecipesMessages } from './i18n'\nimport type { RecipeRow } from './types'\n\n/**\n * A new recipe: what it is called, and nothing else. The server makes the address out of the\n * title; the address field on the settings tab is where there is room to say what changing it\n * costs.\n */\nconst { open, resolve, dismiss } = useModal<RecipeRow>()\n\nconst context = useAdmin()\nconst api = createRecipesApi(context)\nuseRecipesMessages()\n\nconst t = useTranslate('webx-recipes')\n\nconst title = ref('')\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\nconst field = useTemplateRef<HTMLElement>('field')\n\nonMounted(async () => {\n await nextTick()\n field.value?.querySelector('input')?.focus()\n})\n\nasync function submit(): Promise<void> {\n if (title.value.trim() === '' || saving.value) return\n\n saving.value = true\n errors.value = {}\n\n try {\n resolve((await api.create({ title: title.value.trim() })).recipe)\n } catch (error) {\n // The address is what can be refused here, and it is refused under the title that made it:\n // the registry sees the categories and the pages too, so \"taken\" can mean taken by either.\n errors.value = (error as { body?: { errors?: Record<string, string[]> } }).body?.errors ?? {}\n } finally {\n saving.value = false\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('panel.new-title')\" :width=\"440\">\n <wx-form-item\n :label=\"t('panel.field-title')\"\n :error=\"errors.title?.[0] ?? errors.slug?.[0]\"\n required\n >\n <div ref=\"field\">\n <wx-input v-model=\"title\" :aria-label=\"t('panel.field-title')\" @keyup.enter=\"submit\" />\n </div>\n </wx-form-item>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('panel.cancel') }}</wx-button>\n <wx-button type=\"primary\" :loading=\"saving\" :disabled=\"title.trim() === ''\" @click=\"submit\">\n {{ t('panel.create') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport {\n useAdmin,\n useErrorText,\n useTranslate,\n WxListScreen,\n WxRowMenu,\n type RowAction,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n toast,\n WxAlert,\n WxBadge,\n WxEmpty,\n WxIcon,\n WxInput,\n WxSelect,\n WxSkeleton,\n WxSortableList,\n WxTooltip,\n type TabItem,\n type TabValue,\n} from '@webx-ui/core'\nimport { createRecipesApi } from './api'\nimport { useRecipesMessages } from './i18n'\nimport { formatMinutes } from './minutes'\nimport RecipeCreateDialog from './RecipeCreateDialog.vue'\nimport type { RecipeRow, RecipeStatus, RecipesList } from './types'\n\n/**\n * Every recipe at once, in the order the site shows them (§5.9).\n *\n * No paginator: this list is where recipes are put in order, and a drag cannot cross a page\n * boundary. Recipes have one order and only one (decision 4) — no order inside a category, none\n * inside a service — so the grips are there only while the list is the whole of it. Any filter\n * makes it a selection with invisible gaps in it, the grips go away and the line under the list\n * says why: that is the difference from services, where a category has an order of its own.\n *\n * A row is a card rather than a line of cells, which is what makes the same list work on a\n * phone: the picture, the name and the address on one side, the categories and the state beside\n * them on a wide screen and under them on a narrow one.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/recipes' })\n\nconst context = useAdmin()\nconst api = createRecipesApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseRecipesMessages()\n\nconst t = useTranslate('webx-recipes')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<RecipeRow[]>([])\nconst filters = ref<RecipesList['filters']>({ categories: [], nutrients: [], services: null })\nconst loading = ref(true)\n\nconst create = createModal<RecipeRow, Record<string, never>>(RecipeCreateDialog)\n\nconst canManage = computed(() => context.can('recipes.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'recipes')?.title ??\n t('module.recipes'),\n)\n\n/*\n * Everything the list is looking at lives in the address, so coming back from a recipe lands on\n * the same view, the same filters and the same search.\n */\nconst view = computed<TabValue>({\n get: () => (typeof route.query.view === 'string' ? route.query.view : ''),\n set: (value) => {\n void router.replace({\n query: { ...route.query, view: value === '' ? undefined : String(value) },\n })\n },\n})\n\nconst inBin = computed(() => view.value === 'trashed')\n\nfunction idIn(name: string): number | null {\n const raw = route.query[name]\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n}\n\nconst category = computed(() => idIn('category'))\nconst nutrient = computed(() => idIn('nutrient'))\nconst service = computed(() => idIn('service'))\n\nconst q = computed(() => (typeof route.query.q === 'string' ? route.query.q : ''))\n\n/* What is typed, ahead of the address: the address follows it after a pause. */\nconst typed = ref(q.value)\n\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('panel.view-all') },\n { value: 'published', label: t('panel.view-published') },\n { value: 'draft', label: t('panel.view-draft') },\n { value: 'unpublished', label: t('panel.view-unpublished') },\n { value: 'trashed', label: t('panel.bin'), icon: 'trash' },\n])\n\n/** The whole list, with nothing narrowing it — the only list whose order can be dragged. */\nconst whole = computed(\n () =>\n view.value === '' &&\n q.value.trim() === '' &&\n category.value === null &&\n nutrient.value === null &&\n service.value === null,\n)\n\n/* Dragged only by somebody who may write, and only in a list that is the whole order. */\nconst sortable = computed(() => canManage.value && whole.value)\n\nconst hint = computed(() => t(whole.value ? 'panel.order-all' : 'panel.order-locked'))\n\nconst emptyText = computed(() => {\n if (q.value !== '') return t('panel.empty-search')\n\n return inBin.value ? t('panel.empty-bin') : t('panel.empty')\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n const answer = await api.list({\n q: q.value,\n category: category.value,\n nutrient: nutrient.value,\n service: service.value,\n status: inBin.value ? '' : (view.value as RecipeStatus | ''),\n trashed: inBin.value,\n })\n\n rows.value = answer.data\n filters.value = answer.filters\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(\n () => [view.value, category.value, nutrient.value, service.value, q.value] as const,\n () => void load(),\n)\nvoid load()\n\nlet pause: ReturnType<typeof setTimeout> | undefined\n\nwatch(typed, (value) => {\n clearTimeout(pause)\n pause = setTimeout(() => {\n void router.replace({ query: { ...route.query, q: value.trim() === '' ? undefined : value } })\n }, 300)\n})\n\nonBeforeUnmount(() => clearTimeout(pause))\n\nfunction narrow(name: 'category' | 'nutrient' | 'service', value: unknown): void {\n void router.replace({\n query: { ...route.query, [name]: typeof value === 'number' ? String(value) : undefined },\n })\n}\n\nconst options = (list: { id: number; title: string }[] | null) =>\n (list ?? []).map((item) => ({ value: item.id, label: item.title }))\n\nfunction open(recipe: RecipeRow): void {\n void router.push({ path: `${props.base}/${recipe.id}`, query: { ...route.query } })\n}\n\n/** A new recipe: the dialog asks for a title, the editor opens. */\nasync function add(): Promise<void> {\n const made = await create({})\n\n if (made) open(made)\n}\n\n/**\n * The order on screen, written as a whole. The list is already in its new order\n * (`WxSortableList` reorders the model before it says so), so a failure has to put it back\n * rather than leave the screen disagreeing with the database.\n */\nasync function reorder(): Promise<void> {\n try {\n await api.reorder(rows.value.map((row) => row.id))\n } catch (error) {\n toast.danger(message(error, t('panel.reorder-failed')))\n await load()\n }\n}\n\nfunction actionsFor(recipe: RecipeRow): RowAction[] {\n if (!canManage.value) return []\n\n if (inBin.value) {\n return [\n {\n key: 'restore',\n icon: 'refresh',\n label: t('panel.restore'),\n run: () => void restore(recipe),\n },\n ]\n }\n\n const actions: RowAction[] = [\n { key: 'open', icon: 'edit', label: t('panel.open'), run: () => open(recipe) },\n ]\n\n if (recipe.url && recipe.status !== 'draft') {\n actions.push({\n key: 'site',\n icon: 'external-link',\n label: t('panel.open-on-site'),\n href: recipe.url,\n })\n }\n\n if (recipe.url) {\n actions.push({\n key: 'copy',\n icon: 'copy',\n label: t('panel.copy-address'),\n run: () => void copyAddress(recipe),\n })\n }\n\n actions.push(\n recipe.status === 'published' || recipe.status === 'modified'\n ? {\n key: 'unpublish',\n icon: 'eye-off',\n label: t('panel.unpublish'),\n run: () => void run(() => api.unpublish(recipe.id), t('panel.unpublished-done')),\n }\n : {\n key: 'publish',\n icon: 'upload',\n label: t('panel.publish'),\n run: () => void run(() => api.publish(recipe.id), t('panel.published')),\n },\n )\n\n actions.push({\n key: 'delete',\n icon: 'trash',\n label: t('panel.delete'),\n danger: true,\n run: () => void remove(recipe),\n })\n\n return actions\n}\n\nasync function remove(recipe: RecipeRow): Promise<void> {\n const agreed = await confirm({\n title: t('panel.delete-title', { title: recipe.title }),\n message: t('panel.delete-text'),\n confirmText: t('panel.delete'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n await run(() => api.remove(recipe.id), t('panel.deleted'))\n}\n\nasync function restore(recipe: RecipeRow): Promise<void> {\n await run(() => api.restore(recipe.id), t('panel.restored'))\n}\n\n/** One request, one sentence about it, and the list as the server now sees it. */\nasync function run(request: () => Promise<unknown>, said: string): Promise<void> {\n try {\n await request()\n toast.success(said)\n } catch (error) {\n toast.danger(message(error))\n } finally {\n await load()\n }\n}\n\nasync function copyAddress(recipe: RecipeRow): Promise<void> {\n if (!recipe.url) return\n\n try {\n await navigator.clipboard.writeText(recipe.url)\n toast.success(t('panel.address-copied'))\n } catch {\n // A browser that refuses the clipboard without a gesture it trusts; the address is on screen.\n }\n}\n\n/* Live and live-with-edits are the same green; what is waiting has a chip of its own. */\nfunction badge(status: RecipeStatus): 'default' | 'success' {\n return status === 'published' || status === 'modified' ? 'success' : 'default'\n}\n\nfunction address(recipe: RecipeRow): string {\n if (inBin.value) return '—'\n\n return recipe.path === null ? t('panel.no-address') : `/${recipe.path}`\n}\n\n/* What the section offers. Declared, because on a phone the head folds it into the ···. */\nconst actions = computed<ScreenAction[]>(() =>\n canManage.value\n ? [{ key: 'new', label: t('panel.new'), icon: 'plus', primary: true, run: () => void add() }]\n : [],\n)\n</script>\n\n<template>\n <div class=\"wx-recipes\">\n <wx-list-screen\n v-model:view=\"view\"\n :title=\"title\"\n :views=\"views\"\n :actions=\"actions\"\n padding=\"sm\"\n >\n <div class=\"wx-recipes__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-recipes__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('panel.search')\"\n :aria-label=\"t('panel.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n\n <!-- In sight rather than behind a funnel: each of them decides whether the list can be\n dragged, and the line under the list says so. -->\n <wx-select\n :model-value=\"category\"\n :options=\"options(filters.categories)\"\n :placeholder=\"t('panel.any-category')\"\n :aria-label=\"t('panel.filter-category')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow('category', $event)\"\n />\n <wx-select\n :model-value=\"nutrient\"\n :options=\"options(filters.nutrients)\"\n :placeholder=\"t('panel.any-nutrient')\"\n :aria-label=\"t('panel.filter-nutrient')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow('nutrient', $event)\"\n />\n <!-- `null` from the server: the services module is not installed, and there is nothing\n a recipe could be related to. -->\n <wx-select\n v-if=\"filters.services !== null\"\n :model-value=\"service\"\n :options=\"options(filters.services)\"\n :placeholder=\"t('panel.any-service')\"\n :aria-label=\"t('panel.filter-service')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow('service', $event)\"\n />\n </div>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-recipes__loading\" :rows=\"5\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"emptyText\"\n :description=\"whole ? t('panel.empty-help') : undefined\"\n />\n\n <template v-else>\n <!--\n A grip and not the whole row: the row opens a recipe, and a row that both opens and\n drags is one where one of the two happens by accident. No grips at all where the list\n is a selection — a handle that moves rows past invisible ones would be lying.\n -->\n <wx-sortable-list\n v-model=\"rows\"\n class=\"wx-recipes__list\"\n :class=\"{ 'is-loading': loading }\"\n plain\n item-key=\"id\"\n :handle=\"sortable ? 'grip' : 'row'\"\n :item-label=\"(item: RecipeRow) => item.title\"\n :disabled=\"!sortable\"\n @move=\"reorder\"\n >\n <template #default=\"{ item }\">\n <component\n :is=\"inBin ? 'div' : 'router-link'\"\n class=\"wx-recipe-row\"\n v-bind=\"inBin ? {} : { to: { path: `${props.base}/${item.id}`, query: route.query } }\"\n >\n <!-- The picture's box, never the picture: `<img>` has `min-width: auto`, and a\n photograph would take the row (CLAUDE.md §4). -->\n <span class=\"wx-recipe-row__cover\" :class=\"{ 'is-empty': !item.cover?.thumb }\">\n <img v-if=\"item.cover?.thumb\" :src=\"item.cover.thumb\" alt=\"\" loading=\"lazy\" />\n <wx-icon v-else name=\"image\" size=\"sm\" />\n </span>\n\n <span class=\"wx-recipe-row__name\">\n <span class=\"wx-recipe-row__title\">{{ item.title }}</span>\n <span class=\"wx-recipe-row__address\">{{ address(item) }}</span>\n </span>\n\n <span class=\"wx-recipe-row__facts\">\n <span class=\"wx-recipe-row__chips\">\n <template v-for=\"(chip, index) in item.categories\" :key=\"chip.id\">\n <wx-tooltip v-if=\"index === 0\" :content=\"t('panel.main-category')\">\n <wx-badge size=\"sm\" round type=\"primary\">{{ chip.title }}</wx-badge>\n </wx-tooltip>\n <wx-badge v-else size=\"sm\" round>{{ chip.title }}</wx-badge>\n </template>\n </span>\n\n <span class=\"wx-recipe-row__state\">\n <span v-if=\"item.minutes\" class=\"wx-recipe-row__time\">\n <wx-icon name=\"clock\" size=\"sm\" />{{ formatMinutes(item.minutes, t) }}\n </span>\n <wx-badge :type=\"badge(item.status)\" dot size=\"sm\">\n {{ t(`panel.status-${item.status}`) }}\n </wx-badge>\n <wx-badge v-if=\"item.status === 'modified'\" type=\"primary\" size=\"sm\" round>\n {{ t('panel.edits') }}\n </wx-badge>\n </span>\n </span>\n </component>\n </template>\n\n <template #actions=\"{ item }\">\n <wx-row-menu :actions=\"actionsFor(item)\" :label=\"item.title\" />\n </template>\n </wx-sortable-list>\n\n <!-- What a drag does here, or why there is nothing to drag — under the rows it is about. -->\n <wx-alert\n v-if=\"canManage && !inBin\"\n class=\"wx-recipes__note\"\n type=\"info\"\n :description=\"hint\"\n />\n </template>\n </wx-list-screen>\n </div>\n</template>\n\n<style scoped>\n.wx-recipes {\n min-width: 0;\n container-type: inline-size;\n}\n\n.wx-recipes__bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-8);\n padding: var(--wx-space-4) var(--wx-space-8) var(--wx-space-8);\n}\n\n.wx-recipes__search {\n flex: 1 1 240px;\n min-width: 0;\n}\n\n/* The select is a fragment (it carries its dropdown beside it), so our scope attribute never\n reaches it: styled from our own wrapper (CLAUDE.md §4). */\n.wx-recipes__bar > :deep(.wx-select) {\n flex: 1 1 160px;\n max-width: 220px;\n}\n\n.wx-recipes__loading {\n padding: var(--wx-space-8);\n}\n\n.wx-recipes__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-recipes__list :deep(.wx-sortable-list__row) {\n padding-inline: var(--wx-space-8);\n}\n\n.wx-recipes__list :deep(.wx-sortable-list__row:hover) {\n background: var(--wx-bg-subtle);\n border-radius: var(--wx-radius-sm);\n}\n\n.wx-recipes__note {\n margin-block-start: var(--wx-space-8);\n}\n\n.wx-recipe-row {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) auto;\n grid-template-areas: 'cover name facts';\n align-items: center;\n gap: var(--wx-space-4) var(--wx-space-12);\n flex: 1 1 auto;\n min-width: 0;\n padding-block: var(--wx-space-4);\n color: inherit;\n text-decoration: none;\n}\n\n.wx-recipe-row__cover {\n grid-area: cover;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 32px;\n overflow: hidden;\n border-radius: var(--wx-radius-xs);\n background: var(--wx-bg-subtle);\n color: var(--wx-text-muted);\n}\n\n.wx-recipe-row__cover.is-empty {\n border: 1px dashed var(--wx-border-default);\n background: var(--wx-bg-surface);\n}\n\n.wx-recipe-row__cover img {\n width: 100%;\n height: 100%;\n min-width: 0;\n object-fit: cover;\n}\n\n.wx-recipe-row__name {\n grid-area: name;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.wx-recipe-row__title {\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.wx-recipe-row__address {\n overflow: hidden;\n color: var(--wx-text-muted);\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-xs);\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.wx-recipe-row__facts {\n grid-area: facts;\n display: flex;\n align-items: center;\n gap: var(--wx-space-12);\n min-width: 0;\n}\n\n.wx-recipe-row__chips,\n.wx-recipe-row__state {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-4);\n min-width: 0;\n}\n\n.wx-recipe-row__chips {\n justify-content: flex-end;\n max-width: 320px;\n}\n\n.wx-recipe-row__time {\n display: inline-flex;\n align-items: center;\n gap: var(--wx-space-4);\n color: var(--wx-text-muted);\n font-size: var(--wx-font-size-xs);\n white-space: nowrap;\n}\n\n/*\n * Narrow: the facts go under the name rather than beside it, so the name keeps the width. By the\n * width of the list, not of the window — the panel's sidebar decides how much the list has.\n */\n@container (max-width: 640px) {\n .wx-recipe-row {\n grid-template-columns: auto minmax(0, 1fr);\n grid-template-areas:\n 'cover name'\n 'cover facts';\n align-items: start;\n column-gap: var(--wx-space-8);\n }\n\n .wx-recipe-row__facts {\n flex-wrap: wrap;\n gap: var(--wx-space-4);\n }\n\n /* One wrapping line of badges rather than two boxes side by side (see the services list). */\n .wx-recipe-row__chips,\n .wx-recipe-row__state {\n display: contents;\n }\n\n .wx-recipes__list :deep(.wx-sortable-list__row) {\n gap: var(--wx-space-8);\n padding-inline: var(--wx-space-4);\n }\n\n .wx-recipes__bar {\n padding-inline: var(--wx-space-4);\n }\n\n .wx-recipes__bar > :deep(.wx-select) {\n max-width: none;\n }\n}\n</style>\n","import { categoryRoutes, type AdminModule, type CategoriesOptions } from '@webx-ui/module-admin'\nimport RecipeEditorPage from './RecipeEditorPage.vue'\nimport RecipeHistory from './RecipeHistory.vue'\nimport RecipesPage from './RecipesPage.vue'\n\nexport interface RecipesOptions {\n /** Where the recipes live inside the panel. The categories and the nutrients sit under it. */\n path?: string\n}\n\n/**\n * The categories of recipes as the panel's shared category screens see them: where they answer,\n * what they are edited on and what this module calls them. Exported so that a panel mounting the\n * screens somewhere of its own does not have to repeat the words.\n */\nexport function recipeCategoriesOptions(path = '/recipes'): CategoriesOptions {\n return {\n api: 'recipes/categories',\n path: `${path}/categories`,\n name: 'webx.recipes.categories',\n module: 'recipe-categories',\n screen: 'recipes.category-form',\n manage: 'recipes.categories.manage',\n count: 'recipes_count',\n // A category has no order of its own inside it (decision 4): the link shows what is in it.\n items: (id) => ({ path, query: { category: String(id) } }),\n words: {\n new: 'webx-recipes::category.new',\n empty: 'webx-recipes::category.empty',\n 'empty-help': 'webx-recipes::category.empty-help',\n order: 'webx-recipes::category.order',\n hidden: 'webx-recipes::category.hidden',\n 'no-address': 'webx-recipes::category.no-address',\n count: 'webx-recipes::category.recipes',\n 'show-items': 'webx-recipes::category.show-recipes',\n 'delete-blocked': 'webx-recipes::category.delete-blocked',\n 'delete-text': 'webx-recipes::category.delete-text',\n deleted: 'webx-recipes::category.deleted',\n saved: 'webx-recipes::category.saved',\n 'field-title': 'webx-recipes::category.field-title',\n 'field-slug': 'webx-recipes::category.field-slug',\n 'address-moving': 'webx-recipes::category.address-moving',\n },\n }\n}\n\n/**\n * What a recipe is rich in — the second kind of category a recipe has (decision 8), with no\n * address: the shared list leaves the address line out when the server says there is none, as it\n * does for the categories of the FAQ.\n */\nexport function recipeNutrientsOptions(path = '/recipes'): CategoriesOptions {\n return {\n api: 'recipes/nutrients',\n path: `${path}/nutrients`,\n name: 'webx.recipes.nutrients',\n module: 'recipe-nutrients',\n screen: 'recipes.nutrient-form',\n manage: 'recipes.categories.manage',\n count: 'recipes_count',\n items: (id) => ({ path, query: { nutrient: String(id) } }),\n words: {\n new: 'webx-recipes::nutrient.new',\n empty: 'webx-recipes::nutrient.empty',\n 'empty-help': 'webx-recipes::nutrient.empty-help',\n order: 'webx-recipes::nutrient.order',\n hidden: 'webx-recipes::nutrient.hidden',\n count: 'webx-recipes::nutrient.recipes',\n 'show-items': 'webx-recipes::nutrient.show-recipes',\n 'delete-blocked': 'webx-recipes::nutrient.delete-blocked',\n 'delete-text': 'webx-recipes::nutrient.delete-text',\n deleted: 'webx-recipes::nutrient.deleted',\n saved: 'webx-recipes::nutrient.saved',\n 'field-title': 'webx-recipes::nutrient.field-title',\n },\n }\n}\n\n/**\n * The recipes as sections of the panel: recipes, their categories and what they are rich in\n * (§5.9).\n *\n * Three modules rather than one, because the navigation is one entry per module; the server puts\n * them in the `recipes` group, which is what draws them under one heading. A section whose server\n * half is not installed never appears — the entry is built from the manifest.\n */\nexport function recipes(options: RecipesOptions = {}): AdminModule[] {\n const path = options.path ?? '/recipes'\n\n return [\n {\n id: 'recipes',\n path,\n routes: [\n { path, name: 'webx.recipes', component: RecipesPage, props: { base: path } },\n {\n path: `${path}/:id(\\\\d+)`,\n name: 'webx.recipes.edit',\n component: RecipeEditorPage,\n props: { base: path },\n },\n ],\n /*\n * The one part of `recipes.form` only this module can draw. Everything else on it is the\n * panel's (`wx-slug`, `wx-categories`, `wx-relations`, `wx-gallery`, `wx-rich-text`).\n */\n types: {\n 'wx-recipe-history': { component: RecipeHistory, kind: 'display' },\n },\n },\n {\n id: 'recipe-categories',\n path: `${path}/categories`,\n routes: categoryRoutes(recipeCategoriesOptions(path)),\n },\n {\n id: 'recipe-nutrients',\n path: `${path}/nutrients`,\n routes: categoryRoutes(recipeNutrientsOptions(path)),\n },\n ]\n}\n"],"names":["RECIPES_API","createRecipesApi","admin","base","data","body","query","search","id","input","number","ids","reorderItems","recipeEditorKey","provideRecipeEditor","editor","provide","useRecipeEditor","inject","recipesMessages","seeded","useRecipesMessages","i18n","useAdmin","PAUSE","props","__props","context","api","route","useRoute","router","useRouter","locales","useLocales","dates","useDates","t","useTranslate","message","useErrorText","computed","list","recipe","ref","values","revision","prefix","previewUrl","loading","saving","working","errors","conflict","snapshot","canManage","locked","current","dirty","state","title","written","localizedValue","publication","row","provideRelationOwner","provideRecordAddress","load","take","detail","silent","error","toast","timer","schedule","save","onFocusOut","flight","carried","done","write","sending","sent","failure","takeTheirs","confirm","keepMine","theirs","discard","publish","badge","status","leaveGuard","event","watch","onMounted","onBeforeUnmount","onBeforeRouteLeave","actions","leads","_createElementBlock","_Fragment","_createVNode","_unref","WxSkeleton","WxCard","WxScreenHead","WxBreadcrumb","WxBreadcrumbItem","_createTextVNode","_toDisplayString","WxBadge","_createBlock","WxAlert","WxButton","WxScreen","$event","WxActionBar","WxSaveState","versions","live","restore","version","_openBlock","_hoisted_1","WxEmpty","_renderList","_createElementVNode","_hoisted_2","_hoisted_3","WxDate","WxText","formatMinutes","minutes","hours","rest","open","resolve","dismiss","useModal","field","useTemplateRef","nextTick","submit","WxDialog","WxSpace","WxFormItem","WxInput","rows","filters","create","createModal","RecipeCreateDialog","module","view","value","inBin","idIn","name","raw","category","nutrient","service","q","typed","views","whole","sortable","hint","emptyText","answer","pause","narrow","options","item","add","made","reorder","actionsFor","copyAddress","run","remove","request","said","address","WxListScreen","WxIcon","WxSelect","_cache","WxSortableList","_normalizeClass","_withCtx","_resolveDynamicComponent","_mergeProps","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","chip","index","WxTooltip","_hoisted_9","_hoisted_10","WxRowMenu","recipeCategoriesOptions","path","recipeNutrientsOptions","recipes","RecipesPage","RecipeEditorPage","RecipeHistory","categoryRoutes"],"mappings":"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"}
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.wx-recipe-editor[data-v-
|
|
1
|
+
.wx-recipe-editor[data-v-a35c0e0f]{display:flex;flex-direction:column;gap:var(--wx-gap, var(--wx-space-16));min-height:0;container-type:inline-size}.wx-recipe-editor__ghost[data-v-a35c0e0f]{max-width:420px}.wx-recipe-history[data-v-dcd71040]{display:flex;flex-direction:column;border:1px solid var(--wx-border-default);border-radius:var(--wx-radius-md);background:var(--wx-bg-surface);overflow:hidden}.wx-recipe-history__ghost[data-v-dcd71040]{padding:var(--wx-space-10) var(--wx-space-12)}.wx-recipe-history__row[data-v-dcd71040]{display:flex;align-items:center;gap:var(--wx-space-12);padding:var(--wx-space-10) var(--wx-space-12);border-block-end:1px solid var(--wx-border-default);flex-wrap:wrap}.wx-recipe-history__row[data-v-dcd71040]:last-child{border-block-end:0}.wx-recipe-history__number[data-v-dcd71040]{width:40px;font-family:var(--wx-font-family-mono);font-size:var(--wx-font-size-sm);color:var(--wx-text-muted)}.wx-recipe-history__who[data-v-dcd71040]{display:flex;flex-direction:column;flex:1;min-width:160px}.wx-recipes[data-v-aa51858d]{min-width:0;container-type:inline-size}.wx-recipes__bar[data-v-aa51858d]{display:flex;flex-wrap:wrap;gap:var(--wx-space-8);padding:var(--wx-space-4) var(--wx-space-8) var(--wx-space-8)}.wx-recipes__search[data-v-aa51858d]{flex:1 1 240px;min-width:0}.wx-recipes__bar[data-v-aa51858d]>.wx-select{flex:1 1 160px;max-width:220px}.wx-recipes__loading[data-v-aa51858d]{padding:var(--wx-space-8)}.wx-recipes__list.is-loading[data-v-aa51858d]{opacity:.6}.wx-recipes__list[data-v-aa51858d] .wx-sortable-list__row{padding-inline:var(--wx-space-8)}.wx-recipes__list[data-v-aa51858d] .wx-sortable-list__row:hover{background:var(--wx-bg-subtle);border-radius:var(--wx-radius-sm)}.wx-recipes__note[data-v-aa51858d]{margin-block-start:var(--wx-space-8)}.wx-recipe-row[data-v-aa51858d]{display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-areas:"cover name facts";align-items:center;gap:var(--wx-space-4) var(--wx-space-12);flex:1 1 auto;min-width:0;padding-block:var(--wx-space-4);color:inherit;text-decoration:none}.wx-recipe-row__cover[data-v-aa51858d]{grid-area:cover;display:flex;align-items:center;justify-content:center;width:44px;height:32px;overflow:hidden;border-radius:var(--wx-radius-xs);background:var(--wx-bg-subtle);color:var(--wx-text-muted)}.wx-recipe-row__cover.is-empty[data-v-aa51858d]{border:1px dashed var(--wx-border-default);background:var(--wx-bg-surface)}.wx-recipe-row__cover img[data-v-aa51858d]{width:100%;height:100%;min-width:0;object-fit:cover}.wx-recipe-row__name[data-v-aa51858d]{grid-area:name;display:flex;flex-direction:column;min-width:0}.wx-recipe-row__title[data-v-aa51858d]{overflow:hidden;font-weight:var(--wx-font-weight-medium);white-space:nowrap;text-overflow:ellipsis}.wx-recipe-row__address[data-v-aa51858d]{overflow:hidden;color:var(--wx-text-muted);font-family:var(--wx-font-family-mono);font-size:var(--wx-font-size-xs);white-space:nowrap;text-overflow:ellipsis}.wx-recipe-row__facts[data-v-aa51858d]{grid-area:facts;display:flex;align-items:center;gap:var(--wx-space-12);min-width:0}.wx-recipe-row__chips[data-v-aa51858d],.wx-recipe-row__state[data-v-aa51858d]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-4);min-width:0}.wx-recipe-row__chips[data-v-aa51858d]{justify-content:flex-end;max-width:320px}.wx-recipe-row__time[data-v-aa51858d]{display:inline-flex;align-items:center;gap:var(--wx-space-4);color:var(--wx-text-muted);font-size:var(--wx-font-size-xs);white-space:nowrap}@container (max-width: 640px){.wx-recipe-row[data-v-aa51858d]{grid-template-columns:auto minmax(0,1fr);grid-template-areas:"cover name" "cover facts";align-items:start;column-gap:var(--wx-space-8)}.wx-recipe-row__facts[data-v-aa51858d]{flex-wrap:wrap;gap:var(--wx-space-4)}.wx-recipe-row__chips[data-v-aa51858d],.wx-recipe-row__state[data-v-aa51858d]{display:contents}.wx-recipes__list[data-v-aa51858d] .wx-sortable-list__row{gap:var(--wx-space-8);padding-inline:var(--wx-space-4)}.wx-recipes__bar[data-v-aa51858d]{padding-inline:var(--wx-space-4)}.wx-recipes__bar[data-v-aa51858d]>.wx-select{max-width:none}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webx-ui/module-recipes",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Recipes for the WebX UI admin panel: a gallery, ingredients, method and nutrition, flat categories with pages of their own, and the nutrients a recipe is rich in.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"vue-router": "^4.5.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@webx-ui/core": "^0.34.
|
|
47
|
-
"@webx-ui/module-admin": "^0.
|
|
48
|
-
"@webx-ui/schema": "^0.6.
|
|
46
|
+
"@webx-ui/core": "^0.34.1",
|
|
47
|
+
"@webx-ui/module-admin": "^0.19.0",
|
|
48
|
+
"@webx-ui/schema": "^0.6.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^24.10.1",
|