@open-mercato/ui 0.6.7-develop.6828.1.ab1620a63e → 0.6.7-develop.6842.1.8cd8a8883c
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/backend/JsonBuilder.js +3 -3
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/ProfileDropdown.js +2 -1
- package/dist/backend/ProfileDropdown.js.map +2 -2
- package/dist/backend/RowActions.js +2 -2
- package/dist/backend/RowActions.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +1 -1
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
- package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
- package/dist/backend/detail/AttachmentsSection.js +1 -1
- package/dist/backend/detail/AttachmentsSection.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +1 -1
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/detail/TagsSection.js +1 -1
- package/dist/backend/detail/TagsSection.js.map +2 -2
- package/dist/backend/forms/FormHeader.js +1 -2
- package/dist/backend/forms/FormHeader.js.map +2 -2
- package/dist/backend/schedule/ScheduleAgenda.js +3 -3
- package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
- package/dist/backend/schedule/ScheduleGrid.js +3 -3
- package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
- package/dist/backend/utils/apiCall.js +21 -1
- package/dist/backend/utils/apiCall.js.map +2 -2
- package/dist/frontend/LanguageSwitcher.js +2 -1
- package/dist/frontend/LanguageSwitcher.js.map +2 -2
- package/dist/primitives/button.js +9 -4
- package/dist/primitives/button.js.map +2 -2
- package/package.json +4 -4
- package/src/backend/JsonBuilder.tsx +3 -3
- package/src/backend/ProfileDropdown.tsx +1 -0
- package/src/backend/RowActions.tsx +2 -2
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +1 -1
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +35 -0
- package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
- package/src/backend/detail/AttachmentsSection.tsx +1 -1
- package/src/backend/detail/NotesSection.tsx +1 -1
- package/src/backend/detail/TagsSection.tsx +1 -1
- package/src/backend/forms/FormHeader.tsx +1 -2
- package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
- package/src/backend/schedule/ScheduleGrid.tsx +3 -3
- package/src/backend/utils/__tests__/apiCall.test.ts +61 -0
- package/src/backend/utils/apiCall.ts +29 -1
- package/src/frontend/LanguageSwitcher.tsx +1 -0
- package/src/primitives/__tests__/button.test.tsx +70 -0
- package/src/primitives/button.tsx +10 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/detail/TagsSection.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Pencil, X } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'\nimport { DataLoader } from '@open-mercato/ui/primitives/DataLoader'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '../injection/useRegisteredComponent'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\nexport type TagOption = {\n id: string\n label: string\n color?: string | null\n}\n\nexport type TagsSectionLabels = {\n loading: string\n placeholder: string\n empty: string\n loadError: string\n createError: string\n updateError: string\n labelRequired: string\n saveShortcut: string\n cancelShortcut: string\n edit?: string\n cancel?: string\n success?: string\n saving?: string\n autoSaveHint?: string\n}\n\nexport type TagsSectionController = {\n flush: () => Promise<void>\n}\n\nexport type TagsSectionProps = {\n title: string\n tags: TagOption[]\n onChange?: (next: TagOption[]) => void\n isSubmitting?: boolean\n canEdit?: boolean\n autoSave?: boolean\n controllerRef?: React.MutableRefObject<TagsSectionController | null>\n loadOptions: (query?: string) => Promise<TagOption[]>\n createTag: (label: string) => Promise<TagOption>\n onSave: (params: {\n next: TagOption[]\n added: TagOption[]\n removed: TagOption[]\n }) => Promise<void>\n labels: TagsSectionLabels\n}\n\nfunction normalizeTagLabels(labels: string[]): string[] {\n return Array.from(\n new Set(\n labels\n .map((label) => label.trim().toLowerCase())\n .filter((label) => label.length > 0),\n ),\n ).sort((a, b) => a.localeCompare(b))\n}\n\nfunction buildTagLabelKey(labels: string[]): string {\n return normalizeTagLabels(labels).join('\\0')\n}\n\nfunction TagsSectionImpl({\n title,\n tags,\n onChange,\n isSubmitting = false,\n canEdit = true,\n autoSave = false,\n loadOptions,\n createTag,\n onSave,\n labels,\n controllerRef,\n}: TagsSectionProps) {\n const [editing, setEditing] = React.useState(autoSave)\n const [draft, setDraft] = React.useState<string[]>([])\n const [saving, setSaving] = React.useState(false)\n const [, setOptions] = React.useState<Map<string, TagOption>>(() => new Map())\n const [loadingOptions, setLoadingOptions] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const draftRef = React.useRef<string[]>([])\n const savedTagsRef = React.useRef<TagOption[]>(tags)\n const optionsRef = React.useRef<Map<string, TagOption>>(new Map())\n const saveTaskRef = React.useRef<Promise<void> | null>(null)\n React.useEffect(() => {\n savedTagsRef.current = tags\n setOptions((prev) => {\n const next = new Map(prev)\n for (const tag of tags) {\n next.set(tag.label.toLowerCase(), tag)\n }\n optionsRef.current = next\n return next\n })\n }, [tags])\n\n React.useEffect(() => {\n draftRef.current = draft\n }, [draft])\n\n const syncFetchedOptions = React.useCallback((fetched: TagOption[]) => {\n if (!fetched.length) return\n setOptions((prev) => {\n const next = new Map(prev)\n for (const tag of fetched) {\n next.set(tag.label.toLowerCase(), tag)\n }\n optionsRef.current = next\n return next\n })\n }, [])\n\n const hasPendingDraftChanges = React.useCallback(() => {\n if (!autoSave || !autoSaveUserEditedRef.current) return false\n const draftLabels = normalizeTagLabels(draftRef.current)\n const savedLabels = normalizeTagLabels(savedTagsRef.current.map((tag) => tag.label))\n if (draftLabels.length !== savedLabels.length) return true\n return draftLabels.some((label, index) => label !== savedLabels[index])\n }, [autoSave])\n\n const loadSuggestions = React.useCallback(\n async (query?: string) => {\n try {\n const fetched = await loadOptions(query)\n syncFetchedOptions(fetched)\n return fetched.map((tag) => tag.label)\n } catch (err) {\n logger.error('Failed to load tag suggestions', { err })\n return []\n }\n },\n [loadOptions, syncFetchedOptions],\n )\n\n const startEditing = React.useCallback(async () => {\n if (editing || isSubmitting || !canEdit) return\n setError(null)\n setDraft(tags.map((tag) => tag.label))\n setEditing(true)\n setLoadingOptions(true)\n try {\n const fetched = await loadOptions()\n syncFetchedOptions(fetched)\n } catch (err) {\n const message = err instanceof Error ? err.message : labels.loadError\n setError(message)\n flash(message, 'error')\n } finally {\n setLoadingOptions(false)\n }\n }, [canEdit, editing, isSubmitting, labels.loadError, loadOptions, syncFetchedOptions, tags])\n\n const cancelEditing = React.useCallback(() => {\n setEditing(false)\n setDraft([])\n setError(null)\n }, [])\n\n const ensureTagOption = React.useCallback(\n async (label: string): Promise<TagOption> => {\n const normalized = label.trim()\n if (!normalized.length) {\n throw new Error(labels.labelRequired)\n }\n const existing = optionsRef.current.get(normalized.toLowerCase())\n if (existing) return existing\n try {\n const created = await createTag(normalized)\n setOptions((prev) => {\n const next = new Map(prev)\n next.set(created.label.toLowerCase(), created)\n optionsRef.current = next\n return next\n })\n return created\n } catch (err) {\n const message = err instanceof Error ? err.message : labels.createError\n throw new Error(message)\n }\n },\n [createTag, labels.createError, labels.labelRequired],\n )\n\n const handleSave = React.useCallback(async () => {\n if (saveTaskRef.current) {\n await saveTaskRef.current\n if (!autoSave || !hasPendingDraftChanges()) return\n }\n if (autoSave && !hasPendingDraftChanges()) {\n autoSaveUserEditedRef.current = false\n return\n }\n\n const trimmed = draftRef.current.map((label) => label.trim()).filter((label) => label.length > 0)\n const uniqueLabels = Array.from(new Set(trimmed.map((label) => label.toLowerCase())))\n\n let shouldContinueAutoSave = false\n\n const task = (async () => {\n const currentTags = savedTagsRef.current\n const currentIds = new Set(currentTags.map((tag) => tag.id))\n const submittedDraftKey = buildTagLabelKey(trimmed)\n\n setSaving(true)\n setError(null)\n try {\n const finalTagOptions = await Promise.all(\n uniqueLabels.map((normalized) => {\n const existing = optionsRef.current.get(normalized)\n if (existing) return existing\n const matchingLabel = trimmed.find((label) => label.toLowerCase() === normalized) ?? normalized\n return ensureTagOption(matchingLabel)\n }),\n )\n\n const finalIds = new Set(finalTagOptions.map((tag) => tag.id))\n const added = finalTagOptions.filter((tag) => !currentIds.has(tag.id))\n const removed = currentTags.filter((tag) => !finalIds.has(tag.id))\n\n if (added.length > 0 || removed.length > 0) {\n await onSave({ next: finalTagOptions, added, removed })\n }\n\n savedTagsRef.current = finalTagOptions\n onChange?.(finalTagOptions)\n if (autoSave) {\n const latestDraftKey = buildTagLabelKey(draftRef.current)\n if (latestDraftKey === submittedDraftKey) {\n autoSaveUserEditedRef.current = false\n setDraft(finalTagOptions.map((tag) => tag.label))\n } else {\n shouldContinueAutoSave = true\n }\n } else {\n setEditing(false)\n setDraft([])\n }\n if (labels.success && (added.length > 0 || removed.length > 0)) flash(labels.success, 'success')\n } catch (err) {\n const message = err instanceof Error ? err.message : labels.updateError\n setError(message)\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n })()\n\n saveTaskRef.current = task\n try {\n await task\n } finally {\n saveTaskRef.current = null\n }\n if (shouldContinueAutoSave) {\n void handleSave()\n }\n }, [autoSave, ensureTagOption, hasPendingDraftChanges, labels.success, labels.updateError, onChange, onSave])\n\n const activeTags = editing ? draft : tags.map((tag) => tag.label)\n\n const handleEditingKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (!editing) return\n if (event.key === 'Escape') {\n event.preventDefault()\n cancelEditing()\n return\n }\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n if (saving || isSubmitting) return\n void handleSave()\n }\n },\n [cancelEditing, editing, handleSave, isSubmitting, saving],\n )\n\n const autoSaveInitializedRef = React.useRef(false)\n\n React.useEffect(() => {\n if (!autoSave || autoSaveInitializedRef.current) return\n autoSaveInitializedRef.current = true\n setEditing(true)\n setDraft(tags.map((tag) => tag.label))\n let cancelled = false\n loadOptions().then((fetched) => {\n if (cancelled) return\n syncFetchedOptions(fetched)\n }).catch(() => {})\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoSave])\n\n const autoSaveUserEditedRef = React.useRef(false)\n const originalOnChange = React.useCallback(\n (values: string[]) => {\n autoSaveUserEditedRef.current = true\n setDraft(values)\n },\n [],\n )\n\n React.useEffect(() => {\n if (!autoSave || !editing || !autoSaveUserEditedRef.current) return\n if (!hasPendingDraftChanges()) return\n void handleSave()\n }, [autoSave, draft, editing, handleSave, hasPendingDraftChanges])\n\n const flush = React.useCallback(async () => {\n if (saveTaskRef.current) {\n await saveTaskRef.current\n if (!autoSave || !editing || !hasPendingDraftChanges()) return\n }\n if (!autoSave || !editing || !hasPendingDraftChanges()) return\n await handleSave()\n }, [autoSave, editing, handleSave, hasPendingDraftChanges])\n\n React.useEffect(() => {\n if (!controllerRef) return\n controllerRef.current = { flush }\n return () => {\n if (controllerRef.current?.flush === flush) {\n controllerRef.current = null\n }\n }\n }, [controllerRef, flush])\n\n const disableInteraction = isSubmitting || !canEdit\n const autoSaveStatusLabel = saving\n ? (labels.saving ?? 'Saving\u2026')\n : (labels.autoSaveHint ?? null)\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between group\">\n <h2 className=\"text-sm font-semibold\">\n {title}\n </h2>\n {autoSave ? (\n autoSaveStatusLabel ? <p className=\"text-xs text-muted-foreground\">{autoSaveStatusLabel}</p> : null\n ) : (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={editing ? cancelEditing : startEditing}\n disabled={disableInteraction || saving}\n className={\n editing\n ? 'opacity-100 transition-opacity duration-150'\n : 'opacity-100 md:opacity-0 transition-opacity duration-150 md:group-hover:opacity-100 focus-visible:opacity-100'\n }\n >\n {editing ? <X className=\"h-4 w-4\" /> : <Pencil className=\"h-4 w-4\" />}\n <span className=\"sr-only\">\n {editing ? labels.cancel ?? 'Cancel' : labels.edit ?? 'Edit'}\n </span>\n </Button>\n )}\n </div>\n\n {editing ? (\n <div className=\"rounded-lg border bg-card p-4 space-y-3\">\n <DataLoader\n isLoading={loadingOptions}\n loadingMessage={labels.loading}\n spinnerSize=\"sm\"\n >\n <div className=\"space-y-3\" onKeyDown={handleEditingKeyDown}>\n <TagsInput\n value={activeTags}\n onChange={autoSave ? originalOnChange : (values) => setDraft(values)}\n placeholder={labels.placeholder}\n loadSuggestions={loadSuggestions}\n autoFocus={!autoSave}\n />\n {error ? <p className=\"text-xs text-red-600\">{error}</p> : null}\n {autoSave ? null : (\n <div className=\"flex items-center gap-2 mt-3 mb-2\">\n <Button type=\"button\" size=\"sm\" onClick={handleSave} disabled={saving || isSubmitting}>\n {saving ? (\n <span className=\"mr-2 h-4 w-4 animate-spin rounded-full border border-background border-t-primary\" />\n ) : null}\n {labels.saveShortcut}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n onClick={cancelEditing}\n disabled={saving || isSubmitting}\n >\n {labels.cancelShortcut}\n </Button>\n </div>\n )}\n </div>\n </DataLoader>\n </div>\n ) : (\n <div\n className=\"group/tags relative rounded-lg border bg-muted/30 p-4 transition-colors hover:border-primary/40 focus-visible:border-primary focus-visible:outline-none\"\n role={disableInteraction ? undefined : 'button'}\n tabIndex={disableInteraction ? -1 : 0}\n onClick={disableInteraction ? undefined : startEditing}\n onKeyDown={(event) => {\n if (disableInteraction) return\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n void startEditing()\n }\n }}\n >\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-3 text-muted-foreground opacity-0 transition-opacity duration-150 group-hover/tags:opacity-100 group-focus-within/tags:opacity-100\"\n >\n <Pencil className=\"h-4 w-4\" />\n </span>\n {tags.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {labels.empty}\n </p>\n ) : (\n <div className=\"flex flex-wrap gap-2\">\n {tags.map((tag) => (\n <span\n key={tag.id}\n className=\"inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium\"\n style={tag.color ? { borderColor: tag.color, color: tag.color } : undefined}\n >\n {tag.label}\n </span>\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nexport function TagsSection(props: TagsSectionProps) {\n const handle = ComponentReplacementHandles.section('ui.detail', 'TagsSection')\n const Resolved = useRegisteredComponent<TagsSectionProps>(\n handle,\n TagsSectionImpl as React.ComponentType<TagsSectionProps>,\n )\n\n return (\n <div data-component-handle={handle}>\n <Resolved {...props} />\n </div>\n )\n}\n\nexport default TagsSection\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Pencil, X } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'\nimport { DataLoader } from '@open-mercato/ui/primitives/DataLoader'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '../injection/useRegisteredComponent'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\nexport type TagOption = {\n id: string\n label: string\n color?: string | null\n}\n\nexport type TagsSectionLabels = {\n loading: string\n placeholder: string\n empty: string\n loadError: string\n createError: string\n updateError: string\n labelRequired: string\n saveShortcut: string\n cancelShortcut: string\n edit?: string\n cancel?: string\n success?: string\n saving?: string\n autoSaveHint?: string\n}\n\nexport type TagsSectionController = {\n flush: () => Promise<void>\n}\n\nexport type TagsSectionProps = {\n title: string\n tags: TagOption[]\n onChange?: (next: TagOption[]) => void\n isSubmitting?: boolean\n canEdit?: boolean\n autoSave?: boolean\n controllerRef?: React.MutableRefObject<TagsSectionController | null>\n loadOptions: (query?: string) => Promise<TagOption[]>\n createTag: (label: string) => Promise<TagOption>\n onSave: (params: {\n next: TagOption[]\n added: TagOption[]\n removed: TagOption[]\n }) => Promise<void>\n labels: TagsSectionLabels\n}\n\nfunction normalizeTagLabels(labels: string[]): string[] {\n return Array.from(\n new Set(\n labels\n .map((label) => label.trim().toLowerCase())\n .filter((label) => label.length > 0),\n ),\n ).sort((a, b) => a.localeCompare(b))\n}\n\nfunction buildTagLabelKey(labels: string[]): string {\n return normalizeTagLabels(labels).join('\\0')\n}\n\nfunction TagsSectionImpl({\n title,\n tags,\n onChange,\n isSubmitting = false,\n canEdit = true,\n autoSave = false,\n loadOptions,\n createTag,\n onSave,\n labels,\n controllerRef,\n}: TagsSectionProps) {\n const [editing, setEditing] = React.useState(autoSave)\n const [draft, setDraft] = React.useState<string[]>([])\n const [saving, setSaving] = React.useState(false)\n const [, setOptions] = React.useState<Map<string, TagOption>>(() => new Map())\n const [loadingOptions, setLoadingOptions] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const draftRef = React.useRef<string[]>([])\n const savedTagsRef = React.useRef<TagOption[]>(tags)\n const optionsRef = React.useRef<Map<string, TagOption>>(new Map())\n const saveTaskRef = React.useRef<Promise<void> | null>(null)\n React.useEffect(() => {\n savedTagsRef.current = tags\n setOptions((prev) => {\n const next = new Map(prev)\n for (const tag of tags) {\n next.set(tag.label.toLowerCase(), tag)\n }\n optionsRef.current = next\n return next\n })\n }, [tags])\n\n React.useEffect(() => {\n draftRef.current = draft\n }, [draft])\n\n const syncFetchedOptions = React.useCallback((fetched: TagOption[]) => {\n if (!fetched.length) return\n setOptions((prev) => {\n const next = new Map(prev)\n for (const tag of fetched) {\n next.set(tag.label.toLowerCase(), tag)\n }\n optionsRef.current = next\n return next\n })\n }, [])\n\n const hasPendingDraftChanges = React.useCallback(() => {\n if (!autoSave || !autoSaveUserEditedRef.current) return false\n const draftLabels = normalizeTagLabels(draftRef.current)\n const savedLabels = normalizeTagLabels(savedTagsRef.current.map((tag) => tag.label))\n if (draftLabels.length !== savedLabels.length) return true\n return draftLabels.some((label, index) => label !== savedLabels[index])\n }, [autoSave])\n\n const loadSuggestions = React.useCallback(\n async (query?: string) => {\n try {\n const fetched = await loadOptions(query)\n syncFetchedOptions(fetched)\n return fetched.map((tag) => tag.label)\n } catch (err) {\n logger.error('Failed to load tag suggestions', { err })\n return []\n }\n },\n [loadOptions, syncFetchedOptions],\n )\n\n const startEditing = React.useCallback(async () => {\n if (editing || isSubmitting || !canEdit) return\n setError(null)\n setDraft(tags.map((tag) => tag.label))\n setEditing(true)\n setLoadingOptions(true)\n try {\n const fetched = await loadOptions()\n syncFetchedOptions(fetched)\n } catch (err) {\n const message = err instanceof Error ? err.message : labels.loadError\n setError(message)\n flash(message, 'error')\n } finally {\n setLoadingOptions(false)\n }\n }, [canEdit, editing, isSubmitting, labels.loadError, loadOptions, syncFetchedOptions, tags])\n\n const cancelEditing = React.useCallback(() => {\n setEditing(false)\n setDraft([])\n setError(null)\n }, [])\n\n const ensureTagOption = React.useCallback(\n async (label: string): Promise<TagOption> => {\n const normalized = label.trim()\n if (!normalized.length) {\n throw new Error(labels.labelRequired)\n }\n const existing = optionsRef.current.get(normalized.toLowerCase())\n if (existing) return existing\n try {\n const created = await createTag(normalized)\n setOptions((prev) => {\n const next = new Map(prev)\n next.set(created.label.toLowerCase(), created)\n optionsRef.current = next\n return next\n })\n return created\n } catch (err) {\n const message = err instanceof Error ? err.message : labels.createError\n throw new Error(message)\n }\n },\n [createTag, labels.createError, labels.labelRequired],\n )\n\n const handleSave = React.useCallback(async () => {\n if (saveTaskRef.current) {\n await saveTaskRef.current\n if (!autoSave || !hasPendingDraftChanges()) return\n }\n if (autoSave && !hasPendingDraftChanges()) {\n autoSaveUserEditedRef.current = false\n return\n }\n\n const trimmed = draftRef.current.map((label) => label.trim()).filter((label) => label.length > 0)\n const uniqueLabels = Array.from(new Set(trimmed.map((label) => label.toLowerCase())))\n\n let shouldContinueAutoSave = false\n\n const task = (async () => {\n const currentTags = savedTagsRef.current\n const currentIds = new Set(currentTags.map((tag) => tag.id))\n const submittedDraftKey = buildTagLabelKey(trimmed)\n\n setSaving(true)\n setError(null)\n try {\n const finalTagOptions = await Promise.all(\n uniqueLabels.map((normalized) => {\n const existing = optionsRef.current.get(normalized)\n if (existing) return existing\n const matchingLabel = trimmed.find((label) => label.toLowerCase() === normalized) ?? normalized\n return ensureTagOption(matchingLabel)\n }),\n )\n\n const finalIds = new Set(finalTagOptions.map((tag) => tag.id))\n const added = finalTagOptions.filter((tag) => !currentIds.has(tag.id))\n const removed = currentTags.filter((tag) => !finalIds.has(tag.id))\n\n if (added.length > 0 || removed.length > 0) {\n await onSave({ next: finalTagOptions, added, removed })\n }\n\n savedTagsRef.current = finalTagOptions\n onChange?.(finalTagOptions)\n if (autoSave) {\n const latestDraftKey = buildTagLabelKey(draftRef.current)\n if (latestDraftKey === submittedDraftKey) {\n autoSaveUserEditedRef.current = false\n setDraft(finalTagOptions.map((tag) => tag.label))\n } else {\n shouldContinueAutoSave = true\n }\n } else {\n setEditing(false)\n setDraft([])\n }\n if (labels.success && (added.length > 0 || removed.length > 0)) flash(labels.success, 'success')\n } catch (err) {\n const message = err instanceof Error ? err.message : labels.updateError\n setError(message)\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n })()\n\n saveTaskRef.current = task\n try {\n await task\n } finally {\n saveTaskRef.current = null\n }\n if (shouldContinueAutoSave) {\n void handleSave()\n }\n }, [autoSave, ensureTagOption, hasPendingDraftChanges, labels.success, labels.updateError, onChange, onSave])\n\n const activeTags = editing ? draft : tags.map((tag) => tag.label)\n\n const handleEditingKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (!editing) return\n if (event.key === 'Escape') {\n event.preventDefault()\n cancelEditing()\n return\n }\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n if (saving || isSubmitting) return\n void handleSave()\n }\n },\n [cancelEditing, editing, handleSave, isSubmitting, saving],\n )\n\n const autoSaveInitializedRef = React.useRef(false)\n\n React.useEffect(() => {\n if (!autoSave || autoSaveInitializedRef.current) return\n autoSaveInitializedRef.current = true\n setEditing(true)\n setDraft(tags.map((tag) => tag.label))\n let cancelled = false\n loadOptions().then((fetched) => {\n if (cancelled) return\n syncFetchedOptions(fetched)\n }).catch(() => {})\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoSave])\n\n const autoSaveUserEditedRef = React.useRef(false)\n const originalOnChange = React.useCallback(\n (values: string[]) => {\n autoSaveUserEditedRef.current = true\n setDraft(values)\n },\n [],\n )\n\n React.useEffect(() => {\n if (!autoSave || !editing || !autoSaveUserEditedRef.current) return\n if (!hasPendingDraftChanges()) return\n void handleSave()\n }, [autoSave, draft, editing, handleSave, hasPendingDraftChanges])\n\n const flush = React.useCallback(async () => {\n if (saveTaskRef.current) {\n await saveTaskRef.current\n if (!autoSave || !editing || !hasPendingDraftChanges()) return\n }\n if (!autoSave || !editing || !hasPendingDraftChanges()) return\n await handleSave()\n }, [autoSave, editing, handleSave, hasPendingDraftChanges])\n\n React.useEffect(() => {\n if (!controllerRef) return\n controllerRef.current = { flush }\n return () => {\n if (controllerRef.current?.flush === flush) {\n controllerRef.current = null\n }\n }\n }, [controllerRef, flush])\n\n const disableInteraction = isSubmitting || !canEdit\n const autoSaveStatusLabel = saving\n ? (labels.saving ?? 'Saving\u2026')\n : (labels.autoSaveHint ?? null)\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between group\">\n <h2 className=\"text-sm font-semibold\">\n {title}\n </h2>\n {autoSave ? (\n autoSaveStatusLabel ? <p className=\"text-xs text-muted-foreground\">{autoSaveStatusLabel}</p> : null\n ) : (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={editing ? cancelEditing : startEditing}\n disabled={disableInteraction || saving}\n className={\n editing\n ? 'opacity-100 transition-opacity duration-150'\n : 'opacity-100 md:opacity-0 transition-opacity duration-150 md:group-hover:opacity-100 focus-visible:opacity-100'\n }\n >\n {editing ? <X className=\"h-4 w-4\" /> : <Pencil className=\"h-4 w-4\" />}\n <span className=\"sr-only\">\n {editing ? labels.cancel ?? 'Cancel' : labels.edit ?? 'Edit'}\n </span>\n </Button>\n )}\n </div>\n\n {editing ? (\n <div className=\"rounded-lg border bg-card p-4 space-y-3\">\n <DataLoader\n isLoading={loadingOptions}\n loadingMessage={labels.loading}\n spinnerSize=\"sm\"\n >\n <div className=\"space-y-3\" onKeyDown={handleEditingKeyDown}>\n <TagsInput\n value={activeTags}\n onChange={autoSave ? originalOnChange : (values) => setDraft(values)}\n placeholder={labels.placeholder}\n loadSuggestions={loadSuggestions}\n autoFocus={!autoSave}\n />\n {error ? <p className=\"text-xs text-status-error-text\">{error}</p> : null}\n {autoSave ? null : (\n <div className=\"flex items-center gap-2 mt-3 mb-2\">\n <Button type=\"button\" size=\"sm\" onClick={handleSave} disabled={saving || isSubmitting}>\n {saving ? (\n <span className=\"mr-2 h-4 w-4 animate-spin rounded-full border border-background border-t-primary\" />\n ) : null}\n {labels.saveShortcut}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n onClick={cancelEditing}\n disabled={saving || isSubmitting}\n >\n {labels.cancelShortcut}\n </Button>\n </div>\n )}\n </div>\n </DataLoader>\n </div>\n ) : (\n <div\n className=\"group/tags relative rounded-lg border bg-muted/30 p-4 transition-colors hover:border-primary/40 focus-visible:border-primary focus-visible:outline-none\"\n role={disableInteraction ? undefined : 'button'}\n tabIndex={disableInteraction ? -1 : 0}\n onClick={disableInteraction ? undefined : startEditing}\n onKeyDown={(event) => {\n if (disableInteraction) return\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n void startEditing()\n }\n }}\n >\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-3 text-muted-foreground opacity-0 transition-opacity duration-150 group-hover/tags:opacity-100 group-focus-within/tags:opacity-100\"\n >\n <Pencil className=\"h-4 w-4\" />\n </span>\n {tags.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {labels.empty}\n </p>\n ) : (\n <div className=\"flex flex-wrap gap-2\">\n {tags.map((tag) => (\n <span\n key={tag.id}\n className=\"inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium\"\n style={tag.color ? { borderColor: tag.color, color: tag.color } : undefined}\n >\n {tag.label}\n </span>\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nexport function TagsSection(props: TagsSectionProps) {\n const handle = ComponentReplacementHandles.section('ui.detail', 'TagsSection')\n const Resolved = useRegisteredComponent<TagsSectionProps>(\n handle,\n TagsSectionImpl as React.ComponentType<TagsSectionProps>,\n )\n\n return (\n <div data-component-handle={handle}>\n <Resolved {...props} />\n </div>\n )\n}\n\nexport default TagsSection\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -105,11 +105,10 @@ function DetailHeader({
|
|
|
105
105
|
Button,
|
|
106
106
|
{
|
|
107
107
|
type: "button",
|
|
108
|
-
variant: "
|
|
108
|
+
variant: "destructive",
|
|
109
109
|
size: "sm",
|
|
110
110
|
onClick: onDelete,
|
|
111
111
|
disabled: isDeleting,
|
|
112
|
-
className: "text-red-600 border-red-200 hover:bg-red-50 rounded",
|
|
113
112
|
children: [
|
|
114
113
|
isDeleting ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 mr-2 animate-spin" }) : /* @__PURE__ */ jsx(Trash2, { className: "size-4 mr-2" }),
|
|
115
114
|
resolvedDeleteLabel
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/forms/FormHeader.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname, useSearchParams } from 'next/navigation'\nimport { Trash2, Loader2 } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { FormActionButtons, type FormActionButtonsProps } from './FormActionButtons'\nimport { ActionsDropdown, type ActionItem } from './ActionsDropdown'\nimport { InjectionSpot } from '../injection/InjectionSpot'\n\n/** Base props shared by both modes */\ntype FormHeaderBaseProps = {\n /** Back link URL */\n backHref?: string\n /** Back link label */\n backLabel?: string\n}\n\n/** Edit mode: compact header for CrudForm pages */\nexport type FormHeaderEditProps = FormHeaderBaseProps & {\n mode?: 'edit'\n /** Small title next to the back link */\n title?: string\n /** Structured action buttons (Delete/Cancel/Save) */\n actions?: FormActionButtonsProps\n /** Custom right-side content (overrides `actions`) */\n actionsContent?: React.ReactNode\n}\n\n/** Detail mode: large header for view/detail pages */\nexport type FormHeaderDetailProps = FormHeaderBaseProps & {\n mode: 'detail'\n /** Large title -- string renders as h1; ReactNode for InlineTextEditor */\n title?: React.ReactNode\n /** Small uppercase entity type label above the title */\n entityTypeLabel?: string\n /** Subtitle text below the title */\n subtitle?: string\n /** Status badge or similar element below the title */\n statusBadge?: React.ReactNode\n /** Context actions grouped into an \"Actions\" dropdown (preferred) */\n menuActions?: ActionItem[]\n /** Optional label for actions dropdown trigger */\n menuLabel?: string\n /** Trigger style for actions dropdown */\n menuTriggerMode?: 'label' | 'icon'\n /** Accessible label used when trigger is icon-only */\n menuAriaLabel?: string\n /** Optional utility actions (icon-only) displayed before menu actions */\n utilityActions?: React.ReactNode\n /** Delete action -- rendered as a standalone destructive button next to the dropdown */\n onDelete?: () => void\n /** Delete button label */\n deleteLabel?: string\n /** Whether delete is in progress */\n isDeleting?: boolean\n /** Fallback: fully custom right-side content (overrides menuActions + onDelete) */\n actionsContent?: React.ReactNode\n}\n\nexport type FormHeaderProps = FormHeaderEditProps | FormHeaderDetailProps\n\nexport function FormHeader(props: FormHeaderProps) {\n const t = useT()\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const resolvedBackLabel = props.backLabel ?? t('ui.navigation.back')\n const injectionContext = React.useMemo(\n () => ({\n path: pathname ?? '',\n query: searchParams?.toString() ?? '',\n }),\n [pathname, searchParams],\n )\n\n if (props.mode === 'detail') {\n return (\n <>\n <DetailHeader {...props} resolvedBackLabel={resolvedBackLabel} />\n <InjectionSpot spotId=\"form-header:detail\" context={injectionContext} />\n </>\n )\n }\n\n return (\n <>\n <EditHeader {...props} resolvedBackLabel={resolvedBackLabel} />\n <InjectionSpot spotId=\"form-header:edit\" context={injectionContext} />\n </>\n )\n}\n\nfunction EditHeader({\n backHref,\n resolvedBackLabel,\n title,\n actions,\n actionsContent,\n}: FormHeaderEditProps & { resolvedBackLabel: string }) {\n return (\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3\">\n <div className=\"flex items-center gap-3\">\n {backHref ? (\n <Link href={backHref} className=\"text-sm text-muted-foreground hover:text-foreground\">\n ← {resolvedBackLabel}\n </Link>\n ) : null}\n {title ? <div className=\"text-base font-medium\">{title}</div> : null}\n </div>\n {actionsContent ?? (actions ? <FormActionButtons {...actions} /> : null)}\n </div>\n )\n}\n\nfunction DetailHeader({\n backHref,\n resolvedBackLabel,\n title,\n entityTypeLabel,\n subtitle,\n statusBadge,\n menuActions,\n menuLabel,\n menuTriggerMode,\n menuAriaLabel,\n utilityActions,\n onDelete,\n deleteLabel,\n isDeleting,\n actionsContent,\n}: FormHeaderDetailProps & { resolvedBackLabel: string }) {\n const t = useT()\n const resolvedDeleteLabel = deleteLabel ?? t('ui.forms.actions.delete')\n\n const hasActions = actionsContent || utilityActions || menuActions?.length || onDelete\n\n return (\n <div className=\"flex flex-col gap-2 md:gap-3 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex flex-wrap items-center gap-2 md:gap-3 min-w-0\">\n {backHref ? (\n <Link\n href={backHref}\n className=\"inline-flex items-center text-sm text-muted-foreground hover:text-foreground shrink-0\"\n >\n <span aria-hidden className=\"mr-1 text-base\">←</span>\n <span className=\"sr-only\">{resolvedBackLabel}</span>\n </Link>\n ) : null}\n <div className=\"space-y-0.5 md:space-y-1 min-w-0\">\n {entityTypeLabel ? (\n <p className=\"text-xs uppercase text-muted-foreground\">{entityTypeLabel}</p>\n ) : null}\n {title ? (\n typeof title === 'string' ? (\n <h1 className=\"text-lg md:text-2xl font-semibold leading-tight truncate\">{title}</h1>\n ) : (\n <div className=\"text-lg md:text-2xl font-semibold leading-tight\">{title}</div>\n )\n ) : null}\n {statusBadge}\n {subtitle ? (\n <p className=\"text-xs md:text-sm text-muted-foreground\">{subtitle}</p>\n ) : null}\n </div>\n </div>\n {hasActions ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n {actionsContent ? actionsContent : (\n <>\n {utilityActions}\n {menuActions?.length ? (\n <ActionsDropdown\n items={menuActions}\n label={menuLabel}\n triggerMode={menuTriggerMode}\n ariaLabel={menuAriaLabel}\n />\n ) : null}\n {onDelete ? (\n <Button\n type=\"button\"\n variant=\"
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname, useSearchParams } from 'next/navigation'\nimport { Trash2, Loader2 } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { FormActionButtons, type FormActionButtonsProps } from './FormActionButtons'\nimport { ActionsDropdown, type ActionItem } from './ActionsDropdown'\nimport { InjectionSpot } from '../injection/InjectionSpot'\n\n/** Base props shared by both modes */\ntype FormHeaderBaseProps = {\n /** Back link URL */\n backHref?: string\n /** Back link label */\n backLabel?: string\n}\n\n/** Edit mode: compact header for CrudForm pages */\nexport type FormHeaderEditProps = FormHeaderBaseProps & {\n mode?: 'edit'\n /** Small title next to the back link */\n title?: string\n /** Structured action buttons (Delete/Cancel/Save) */\n actions?: FormActionButtonsProps\n /** Custom right-side content (overrides `actions`) */\n actionsContent?: React.ReactNode\n}\n\n/** Detail mode: large header for view/detail pages */\nexport type FormHeaderDetailProps = FormHeaderBaseProps & {\n mode: 'detail'\n /** Large title -- string renders as h1; ReactNode for InlineTextEditor */\n title?: React.ReactNode\n /** Small uppercase entity type label above the title */\n entityTypeLabel?: string\n /** Subtitle text below the title */\n subtitle?: string\n /** Status badge or similar element below the title */\n statusBadge?: React.ReactNode\n /** Context actions grouped into an \"Actions\" dropdown (preferred) */\n menuActions?: ActionItem[]\n /** Optional label for actions dropdown trigger */\n menuLabel?: string\n /** Trigger style for actions dropdown */\n menuTriggerMode?: 'label' | 'icon'\n /** Accessible label used when trigger is icon-only */\n menuAriaLabel?: string\n /** Optional utility actions (icon-only) displayed before menu actions */\n utilityActions?: React.ReactNode\n /** Delete action -- rendered as a standalone destructive button next to the dropdown */\n onDelete?: () => void\n /** Delete button label */\n deleteLabel?: string\n /** Whether delete is in progress */\n isDeleting?: boolean\n /** Fallback: fully custom right-side content (overrides menuActions + onDelete) */\n actionsContent?: React.ReactNode\n}\n\nexport type FormHeaderProps = FormHeaderEditProps | FormHeaderDetailProps\n\nexport function FormHeader(props: FormHeaderProps) {\n const t = useT()\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const resolvedBackLabel = props.backLabel ?? t('ui.navigation.back')\n const injectionContext = React.useMemo(\n () => ({\n path: pathname ?? '',\n query: searchParams?.toString() ?? '',\n }),\n [pathname, searchParams],\n )\n\n if (props.mode === 'detail') {\n return (\n <>\n <DetailHeader {...props} resolvedBackLabel={resolvedBackLabel} />\n <InjectionSpot spotId=\"form-header:detail\" context={injectionContext} />\n </>\n )\n }\n\n return (\n <>\n <EditHeader {...props} resolvedBackLabel={resolvedBackLabel} />\n <InjectionSpot spotId=\"form-header:edit\" context={injectionContext} />\n </>\n )\n}\n\nfunction EditHeader({\n backHref,\n resolvedBackLabel,\n title,\n actions,\n actionsContent,\n}: FormHeaderEditProps & { resolvedBackLabel: string }) {\n return (\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3\">\n <div className=\"flex items-center gap-3\">\n {backHref ? (\n <Link href={backHref} className=\"text-sm text-muted-foreground hover:text-foreground\">\n ← {resolvedBackLabel}\n </Link>\n ) : null}\n {title ? <div className=\"text-base font-medium\">{title}</div> : null}\n </div>\n {actionsContent ?? (actions ? <FormActionButtons {...actions} /> : null)}\n </div>\n )\n}\n\nfunction DetailHeader({\n backHref,\n resolvedBackLabel,\n title,\n entityTypeLabel,\n subtitle,\n statusBadge,\n menuActions,\n menuLabel,\n menuTriggerMode,\n menuAriaLabel,\n utilityActions,\n onDelete,\n deleteLabel,\n isDeleting,\n actionsContent,\n}: FormHeaderDetailProps & { resolvedBackLabel: string }) {\n const t = useT()\n const resolvedDeleteLabel = deleteLabel ?? t('ui.forms.actions.delete')\n\n const hasActions = actionsContent || utilityActions || menuActions?.length || onDelete\n\n return (\n <div className=\"flex flex-col gap-2 md:gap-3 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex flex-wrap items-center gap-2 md:gap-3 min-w-0\">\n {backHref ? (\n <Link\n href={backHref}\n className=\"inline-flex items-center text-sm text-muted-foreground hover:text-foreground shrink-0\"\n >\n <span aria-hidden className=\"mr-1 text-base\">←</span>\n <span className=\"sr-only\">{resolvedBackLabel}</span>\n </Link>\n ) : null}\n <div className=\"space-y-0.5 md:space-y-1 min-w-0\">\n {entityTypeLabel ? (\n <p className=\"text-xs uppercase text-muted-foreground\">{entityTypeLabel}</p>\n ) : null}\n {title ? (\n typeof title === 'string' ? (\n <h1 className=\"text-lg md:text-2xl font-semibold leading-tight truncate\">{title}</h1>\n ) : (\n <div className=\"text-lg md:text-2xl font-semibold leading-tight\">{title}</div>\n )\n ) : null}\n {statusBadge}\n {subtitle ? (\n <p className=\"text-xs md:text-sm text-muted-foreground\">{subtitle}</p>\n ) : null}\n </div>\n </div>\n {hasActions ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n {actionsContent ? actionsContent : (\n <>\n {utilityActions}\n {menuActions?.length ? (\n <ActionsDropdown\n items={menuActions}\n label={menuLabel}\n triggerMode={menuTriggerMode}\n ariaLabel={menuAriaLabel}\n />\n ) : null}\n {onDelete ? (\n <Button\n type=\"button\"\n variant=\"destructive\"\n size=\"sm\"\n onClick={onDelete}\n disabled={isDeleting}\n >\n {isDeleting ? (\n <Loader2 className=\"size-4 mr-2 animate-spin\" />\n ) : (\n <Trash2 className=\"size-4 mr-2\" />\n )}\n {resolvedDeleteLabel}\n </Button>\n ) : null}\n </>\n )}\n </div>\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -47,9 +47,9 @@ function getStatusLabel(status, t) {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
function getKindStyles(kind) {
|
|
50
|
-
if (kind === "event") return "border-
|
|
51
|
-
if (kind === "exception") return "border-
|
|
52
|
-
return "border-
|
|
50
|
+
if (kind === "event") return "border-status-info-border bg-status-info-bg text-status-info-text";
|
|
51
|
+
if (kind === "exception") return "border-status-warning-border bg-status-warning-bg text-status-warning-text";
|
|
52
|
+
return "border-status-success-border bg-status-success-bg text-status-success-text";
|
|
53
53
|
}
|
|
54
54
|
function ScheduleAgenda({ items, range, timezone, onItemClick, onSlotClick, className }) {
|
|
55
55
|
const t = useT();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/schedule/ScheduleAgenda.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ScheduleItem, ScheduleRange, ScheduleSlot } from './types'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Badge } from '../../primitives/badge'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { expandRecurringItems } from './recurrence'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction startOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate())\n}\n\nfunction endOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate(), 23, 59, 59, 999)\n}\n\nfunction eachDay(start: Date, end: Date): Date[] {\n const days: Date[] = []\n let cursor = startOfDay(start)\n const last = startOfDay(end)\n while (cursor <= last) {\n days.push(new Date(cursor))\n cursor = new Date(cursor.getTime() + DAY_MS)\n }\n return days\n}\n\nfunction overlapsDay(item: ScheduleItem, day: Date): boolean {\n const dayStart = startOfDay(day)\n const dayEnd = endOfDay(day)\n return item.startsAt <= dayEnd && item.endsAt >= dayStart\n}\n\nfunction formatDayLabel(day: Date): string {\n return day.toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric' })\n}\n\nfunction formatTimeRange(item: ScheduleItem, timezone?: string): string {\n const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' }\n if (timezone) options.timeZone = timezone\n const startLabel = item.startsAt.toLocaleTimeString(undefined, options)\n const endLabel = item.endsAt.toLocaleTimeString(undefined, options)\n return `${startLabel}-${endLabel}`\n}\n\nfunction getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallback?: string) => string): string | null {\n if (!status) return null\n if (status === 'draft') return t('schedule.item.status.draft', 'Draft')\n if (status === 'negotiation') return t('schedule.item.status.negotiation', 'Negotiation')\n if (status === 'confirmed') return t('schedule.item.status.confirmed', 'Confirmed')\n if (status === 'cancelled') return t('schedule.item.status.cancelled', 'Cancelled')\n return null\n}\n\nfunction getKindStyles(kind: ScheduleItem['kind']): string {\n if (kind === 'event') return 'border-
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ScheduleItem, ScheduleRange, ScheduleSlot } from './types'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Badge } from '../../primitives/badge'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { expandRecurringItems } from './recurrence'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction startOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate())\n}\n\nfunction endOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate(), 23, 59, 59, 999)\n}\n\nfunction eachDay(start: Date, end: Date): Date[] {\n const days: Date[] = []\n let cursor = startOfDay(start)\n const last = startOfDay(end)\n while (cursor <= last) {\n days.push(new Date(cursor))\n cursor = new Date(cursor.getTime() + DAY_MS)\n }\n return days\n}\n\nfunction overlapsDay(item: ScheduleItem, day: Date): boolean {\n const dayStart = startOfDay(day)\n const dayEnd = endOfDay(day)\n return item.startsAt <= dayEnd && item.endsAt >= dayStart\n}\n\nfunction formatDayLabel(day: Date): string {\n return day.toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric' })\n}\n\nfunction formatTimeRange(item: ScheduleItem, timezone?: string): string {\n const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' }\n if (timezone) options.timeZone = timezone\n const startLabel = item.startsAt.toLocaleTimeString(undefined, options)\n const endLabel = item.endsAt.toLocaleTimeString(undefined, options)\n return `${startLabel}-${endLabel}`\n}\n\nfunction getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallback?: string) => string): string | null {\n if (!status) return null\n if (status === 'draft') return t('schedule.item.status.draft', 'Draft')\n if (status === 'negotiation') return t('schedule.item.status.negotiation', 'Negotiation')\n if (status === 'confirmed') return t('schedule.item.status.confirmed', 'Confirmed')\n if (status === 'cancelled') return t('schedule.item.status.cancelled', 'Cancelled')\n return null\n}\n\nfunction getKindStyles(kind: ScheduleItem['kind']): string {\n if (kind === 'event') return 'border-status-info-border bg-status-info-bg text-status-info-text'\n if (kind === 'exception') return 'border-status-warning-border bg-status-warning-bg text-status-warning-text'\n return 'border-status-success-border bg-status-success-bg text-status-success-text'\n}\n\nexport type ScheduleAgendaProps = {\n items: ScheduleItem[]\n range: ScheduleRange\n timezone?: string\n onItemClick?: (item: ScheduleItem) => void\n onSlotClick?: (slot: ScheduleSlot) => void\n className?: string\n}\n\nexport function ScheduleAgenda({ items, range, timezone, onItemClick, onSlotClick, className }: ScheduleAgendaProps) {\n const t = useT()\n const days = React.useMemo(() => eachDay(range.start, range.end), [range])\n const expandedItems = React.useMemo(() => expandRecurringItems(items, range), [items, range])\n\n return (\n <div className={cn('space-y-4', className)}>\n {days.map((day) => {\n const dayItems = expandedItems.filter((item) => overlapsDay(item, day))\n const slotStart = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9, 0, 0)\n const slotEnd = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 10, 0, 0)\n return (\n <div key={day.toISOString()} className=\"rounded-xl border bg-card p-4\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm font-semibold text-foreground\">{formatDayLabel(day)}</div>\n {onSlotClick ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onSlotClick({ start: slotStart, end: slotEnd })}\n >\n {t('schedule.actions.add', 'Add')}\n </Button>\n ) : null}\n </div>\n <div className=\"mt-3 space-y-2\">\n {dayItems.length === 0 ? (\n <div className=\"rounded-lg border border-dashed p-3 text-xs text-muted-foreground\">\n {t('schedule.emptyDay', 'No scheduled items')}\n </div>\n ) : (\n dayItems.map((item) => {\n const statusLabel = getStatusLabel(item.status, t)\n return (\n <button\n key={item.id}\n type=\"button\"\n className={cn(\n 'flex w-full cursor-pointer flex-col gap-2 rounded-lg border px-3 py-2 text-left text-xs transition hover:shadow-sm',\n getKindStyles(item.kind)\n )}\n onClick={() => onItemClick?.(item)}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"font-semibold\">{item.title}</span>\n {statusLabel ? <Badge variant=\"secondary\">{statusLabel}</Badge> : null}\n </div>\n <div className=\"flex items-center justify-between text-overline text-muted-foreground\">\n <span>{formatTimeRange(item, timezone)}</span>\n <span className=\"capitalize\">{item.kind}</span>\n </div>\n </button>\n )\n })\n )}\n </div>\n </div>\n )\n })}\n </div>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -47,9 +47,9 @@ function getStatusLabel(status, t) {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
function getKindStyles(kind) {
|
|
50
|
-
if (kind === "event") return "border-
|
|
51
|
-
if (kind === "exception") return "border-
|
|
52
|
-
return "border-
|
|
50
|
+
if (kind === "event") return "border-status-info-border bg-status-info-bg text-status-info-text";
|
|
51
|
+
if (kind === "exception") return "border-status-warning-border bg-status-warning-bg text-status-warning-text";
|
|
52
|
+
return "border-status-success-border bg-status-success-bg text-status-success-text";
|
|
53
53
|
}
|
|
54
54
|
function ScheduleGrid({ items, range, timezone, onItemClick, onSlotClick, className }) {
|
|
55
55
|
const t = useT();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/schedule/ScheduleGrid.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ScheduleItem, ScheduleRange, ScheduleSlot } from './types'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Badge } from '../../primitives/badge'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { expandRecurringItems } from './recurrence'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction startOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate())\n}\n\nfunction endOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate(), 23, 59, 59, 999)\n}\n\nfunction eachDay(start: Date, end: Date): Date[] {\n const days: Date[] = []\n let cursor = startOfDay(start)\n const last = startOfDay(end)\n while (cursor <= last) {\n days.push(new Date(cursor))\n cursor = new Date(cursor.getTime() + DAY_MS)\n }\n return days\n}\n\nfunction overlapsDay(item: ScheduleItem, day: Date): boolean {\n const dayStart = startOfDay(day)\n const dayEnd = endOfDay(day)\n return item.startsAt <= dayEnd && item.endsAt >= dayStart\n}\n\nfunction formatDayLabel(day: Date): string {\n return day.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })\n}\n\nfunction formatTimeRange(item: ScheduleItem, timezone?: string): string {\n const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' }\n if (timezone) options.timeZone = timezone\n const startLabel = item.startsAt.toLocaleTimeString(undefined, options)\n const endLabel = item.endsAt.toLocaleTimeString(undefined, options)\n return `${startLabel}-${endLabel}`\n}\n\nfunction getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallback?: string) => string): string | null {\n if (!status) return null\n if (status === 'draft') return t('schedule.item.status.draft', 'Draft')\n if (status === 'negotiation') return t('schedule.item.status.negotiation', 'Negotiation')\n if (status === 'confirmed') return t('schedule.item.status.confirmed', 'Confirmed')\n if (status === 'cancelled') return t('schedule.item.status.cancelled', 'Cancelled')\n return null\n}\n\nfunction getKindStyles(kind: ScheduleItem['kind']): string {\n if (kind === 'event') return 'border-
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ScheduleItem, ScheduleRange, ScheduleSlot } from './types'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Badge } from '../../primitives/badge'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { expandRecurringItems } from './recurrence'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction startOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate())\n}\n\nfunction endOfDay(value: Date): Date {\n return new Date(value.getFullYear(), value.getMonth(), value.getDate(), 23, 59, 59, 999)\n}\n\nfunction eachDay(start: Date, end: Date): Date[] {\n const days: Date[] = []\n let cursor = startOfDay(start)\n const last = startOfDay(end)\n while (cursor <= last) {\n days.push(new Date(cursor))\n cursor = new Date(cursor.getTime() + DAY_MS)\n }\n return days\n}\n\nfunction overlapsDay(item: ScheduleItem, day: Date): boolean {\n const dayStart = startOfDay(day)\n const dayEnd = endOfDay(day)\n return item.startsAt <= dayEnd && item.endsAt >= dayStart\n}\n\nfunction formatDayLabel(day: Date): string {\n return day.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })\n}\n\nfunction formatTimeRange(item: ScheduleItem, timezone?: string): string {\n const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' }\n if (timezone) options.timeZone = timezone\n const startLabel = item.startsAt.toLocaleTimeString(undefined, options)\n const endLabel = item.endsAt.toLocaleTimeString(undefined, options)\n return `${startLabel}-${endLabel}`\n}\n\nfunction getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallback?: string) => string): string | null {\n if (!status) return null\n if (status === 'draft') return t('schedule.item.status.draft', 'Draft')\n if (status === 'negotiation') return t('schedule.item.status.negotiation', 'Negotiation')\n if (status === 'confirmed') return t('schedule.item.status.confirmed', 'Confirmed')\n if (status === 'cancelled') return t('schedule.item.status.cancelled', 'Cancelled')\n return null\n}\n\nfunction getKindStyles(kind: ScheduleItem['kind']): string {\n if (kind === 'event') return 'border-status-info-border bg-status-info-bg text-status-info-text'\n if (kind === 'exception') return 'border-status-warning-border bg-status-warning-bg text-status-warning-text'\n return 'border-status-success-border bg-status-success-bg text-status-success-text'\n}\n\nexport type ScheduleGridProps = {\n items: ScheduleItem[]\n range: ScheduleRange\n timezone?: string\n onItemClick?: (item: ScheduleItem) => void\n onSlotClick?: (slot: ScheduleSlot) => void\n className?: string\n}\n\nexport function ScheduleGrid({ items, range, timezone, onItemClick, onSlotClick, className }: ScheduleGridProps) {\n const t = useT()\n const days = React.useMemo(() => eachDay(range.start, range.end), [range])\n const expandedItems = React.useMemo(() => expandRecurringItems(items, range), [items, range])\n\n return (\n <div className={cn('grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3', className)}>\n {days.map((day) => {\n const dayItems = expandedItems.filter((item) => overlapsDay(item, day))\n const slotStart = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9, 0, 0)\n const slotEnd = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 10, 0, 0)\n return (\n <div key={day.toISOString()} className=\"rounded-xl border bg-card p-4 shadow-sm\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm font-semibold text-foreground\">{formatDayLabel(day)}</div>\n {onSlotClick ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onSlotClick({ start: slotStart, end: slotEnd })}\n >\n {t('schedule.actions.add', 'Add')}\n </Button>\n ) : null}\n </div>\n <div className=\"mt-3 space-y-2\">\n {dayItems.length === 0 ? (\n <div className=\"rounded-lg border border-dashed p-3 text-xs text-muted-foreground\">\n {t('schedule.emptyDay', 'No scheduled items')}\n </div>\n ) : (\n dayItems.map((item) => {\n const statusLabel = getStatusLabel(item.status, t)\n return (\n <button\n key={item.id}\n type=\"button\"\n className={cn(\n 'flex w-full cursor-pointer flex-col gap-2 rounded-lg border px-3 py-2 text-left text-xs transition hover:shadow-sm',\n getKindStyles(item.kind)\n )}\n onClick={() => onItemClick?.(item)}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"font-semibold\">{item.title}</span>\n {statusLabel ? <Badge variant=\"secondary\">{statusLabel}</Badge> : null}\n </div>\n <div className=\"flex items-center justify-between text-overline text-muted-foreground\">\n <span>{formatTimeRange(item, timezone)}</span>\n <span className=\"capitalize\">{item.kind}</span>\n </div>\n </button>\n )\n })\n )}\n </div>\n </div>\n )\n })}\n </div>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,22 @@ import { apiFetch } from "./api.js";
|
|
|
3
3
|
import { raiseCrudError, readJsonSafe } from "./serverErrors.js";
|
|
4
4
|
import { createScopedHeaderStack } from "./scopedHeaderStack.js";
|
|
5
5
|
const scopedRequestHeaders = createScopedHeaderStack();
|
|
6
|
+
function isAbortError(error) {
|
|
7
|
+
return typeof error === "object" && error !== null && error.name === "AbortError";
|
|
8
|
+
}
|
|
9
|
+
function createAbortError() {
|
|
10
|
+
if (typeof DOMException === "function") {
|
|
11
|
+
return new DOMException("[internal] The operation was aborted.", "AbortError");
|
|
12
|
+
}
|
|
13
|
+
const error = new Error("[internal] The operation was aborted.");
|
|
14
|
+
error.name = "AbortError";
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
function resolveAbortSignal(input, init) {
|
|
18
|
+
if (init?.signal) return init.signal;
|
|
19
|
+
if (typeof Request !== "undefined" && input instanceof Request) return input.signal ?? null;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
6
22
|
function mergeHeaders(base, extra) {
|
|
7
23
|
if (!base) return extra;
|
|
8
24
|
if (typeof Headers !== "undefined" && base instanceof Headers) {
|
|
@@ -31,9 +47,13 @@ async function apiCall(input, init, options) {
|
|
|
31
47
|
const source = typeof response.clone === "function" ? response.clone() : response;
|
|
32
48
|
if (parser) result = await parser(source);
|
|
33
49
|
else result = await readJsonSafe(source, fallback);
|
|
34
|
-
} catch {
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (isAbortError(err)) throw err;
|
|
35
52
|
result = fallback;
|
|
36
53
|
}
|
|
54
|
+
if (result == null && resolveAbortSignal(input, init)?.aborted) {
|
|
55
|
+
throw createAbortError();
|
|
56
|
+
}
|
|
37
57
|
return {
|
|
38
58
|
ok: response.ok,
|
|
39
59
|
status: response.status,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/apiCall.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport { apiFetch } from './api'\nimport { raiseCrudError, readJsonSafe } from './serverErrors'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nexport type ApiCallOptions<TReturn> = {\n parse?: (res: Response) => Promise<TReturn | null>\n fallback?: TReturn | null\n}\n\nexport type ApiCallResult<TReturn> = {\n ok: boolean\n status: number\n result: TReturn | null\n response: Response\n cacheStatus: 'hit' | 'miss' | null\n}\n\nconst scopedRequestHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): HeadersInit {\n if (!base) return extra\n if (typeof Headers !== 'undefined' && base instanceof Headers) {\n const merged = new Headers(base)\n Object.entries(extra).forEach(([key, value]) => merged.set(key, value))\n return merged\n }\n if (Array.isArray(base)) {\n return [...base, ...Object.entries(extra)]\n }\n return { ...(base as Record<string, string>), ...extra }\n}\n\nexport async function withScopedApiRequestHeaders<T>(\n headers: Record<string, string>,\n run: () => Promise<T>,\n): Promise<T> {\n return scopedRequestHeaders.withScopedHeaders(headers, run)\n}\n\nexport async function apiCall<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ApiCallOptions<TReturn>,\n): Promise<ApiCallResult<TReturn>> {\n const scopedHeaders = scopedRequestHeaders.resolveScopedHeaders()\n const requestInit = Object.keys(scopedHeaders).length > 0\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scopedHeaders) }\n : init\n const response = await apiFetch(input, requestInit)\n const parser = options?.parse\n const fallback = options?.fallback ?? null\n let result: TReturn | null = null\n const rawCacheStatus =\n response.headers?.get?.('x-om-cache') ??\n response.headers?.get?.('x-cache-status') ??\n null\n const cacheStatus = rawCacheStatus === 'hit' || rawCacheStatus === 'miss' ? rawCacheStatus : null\n try {\n const source = typeof (response as Response & { clone?: () => Response }).clone === 'function'\n ? response.clone()\n : response\n if (parser) result = await parser(source)\n else result = await readJsonSafe<TReturn>(source, fallback)\n } catch {\n result = fallback\n }\n return {\n ok: response.ok,\n status: response.status,\n result,\n response,\n cacheStatus,\n }\n}\n\nexport type ApiCallOrThrowOptions<TReturn> = ApiCallOptions<TReturn> & {\n errorMessage?: string\n}\n\nexport async function apiCallOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ApiCallOrThrowOptions<TReturn>,\n): Promise<ApiCallResult<TReturn>> {\n const { errorMessage, ...callOptions } = options ?? {}\n const call = await apiCall<TReturn>(input, init, callOptions)\n if (!call.ok) {\n await raiseCrudError(call.response, errorMessage)\n }\n return call\n}\n\nexport type ReadApiResultOrThrowOptions<TReturn> = ApiCallOrThrowOptions<TReturn> & {\n allowNullResult?: boolean\n emptyResultMessage?: string\n}\n\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ReadApiResultOrThrowOptions<TReturn> & { allowNullResult?: false },\n): Promise<TReturn>\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init: RequestInit | undefined,\n options: ReadApiResultOrThrowOptions<TReturn> & { allowNullResult: true },\n): Promise<TReturn | null>\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ReadApiResultOrThrowOptions<TReturn>,\n): Promise<TReturn | null> {\n const { allowNullResult = false, emptyResultMessage, ...callOptions } = options ?? {}\n const call = await apiCallOrThrow<TReturn>(input, init, callOptions)\n if (call.result == null && !allowNullResult) {\n const fallback =\n emptyResultMessage ?? callOptions.errorMessage ?? `Missing response payload (${call.status})`\n throw new Error(fallback)\n }\n return call.result\n}\n"],
|
|
5
|
-
"mappings": ";AAEA,SAAS,gBAAgB;AACzB,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,+BAA+B;AAexC,MAAM,uBAAuB,wBAAwB;AAErD,SAAS,aAAa,MAA+B,OAA4C;AAC/F,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,OAAO,YAAY,eAAe,gBAAgB,SAAS;AAC7D,UAAM,SAAS,IAAI,QAAQ,IAAI;AAC/B,WAAO,QAAQ,KAAK,EAAE,QAAQ,CAAC,CAAC,KAAK,KAAK,MAAM,OAAO,IAAI,KAAK,KAAK,CAAC;AACtE,WAAO;AAAA,EACT;AACA,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,WAAO,CAAC,GAAG,MAAM,GAAG,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC3C;AACA,SAAO,EAAE,GAAI,MAAiC,GAAG,MAAM;AACzD;AAEA,eAAsB,4BACpB,SACA,KACY;AACZ,SAAO,qBAAqB,kBAAkB,SAAS,GAAG;AAC5D;AAEA,eAAsB,QACpB,OACA,MACA,SACiC;AACjC,QAAM,gBAAgB,qBAAqB,qBAAqB;AAChE,QAAM,cAAc,OAAO,KAAK,aAAa,EAAE,SAAS,IACpD,EAAE,GAAI,QAAQ,CAAC,GAAI,SAAS,aAAa,MAAM,SAAS,aAAa,EAAE,IACvE;AACJ,QAAM,WAAW,MAAM,SAAS,OAAO,WAAW;AAClD,QAAM,SAAS,SAAS;AACxB,QAAM,WAAW,SAAS,YAAY;AACtC,MAAI,SAAyB;AAC7B,QAAM,iBACJ,SAAS,SAAS,MAAM,YAAY,KACpC,SAAS,SAAS,MAAM,gBAAgB,KACxC;AACF,QAAM,cAAc,mBAAmB,SAAS,mBAAmB,SAAS,iBAAiB;AAC7F,MAAI;AACF,UAAM,SAAS,OAAQ,SAAmD,UAAU,aAChF,SAAS,MAAM,IACf;AACJ,QAAI,OAAQ,UAAS,MAAM,OAAO,MAAM;AAAA,QACnC,UAAS,MAAM,aAAsB,QAAQ,QAAQ;AAAA,EAC5D,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { apiFetch } from './api'\nimport { raiseCrudError, readJsonSafe } from './serverErrors'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nexport type ApiCallOptions<TReturn> = {\n parse?: (res: Response) => Promise<TReturn | null>\n fallback?: TReturn | null\n}\n\nexport type ApiCallResult<TReturn> = {\n ok: boolean\n status: number\n result: TReturn | null\n response: Response\n cacheStatus: 'hit' | 'miss' | null\n}\n\nconst scopedRequestHeaders = createScopedHeaderStack()\n\nfunction isAbortError(error: unknown): boolean {\n return typeof error === 'object'\n && error !== null\n && (error as { name?: unknown }).name === 'AbortError'\n}\n\nfunction createAbortError(): Error {\n if (typeof DOMException === 'function') {\n return new DOMException('[internal] The operation was aborted.', 'AbortError')\n }\n const error = new Error('[internal] The operation was aborted.')\n error.name = 'AbortError'\n return error\n}\n\nfunction resolveAbortSignal(input: RequestInfo | URL, init?: RequestInit): AbortSignal | null {\n if (init?.signal) return init.signal\n if (typeof Request !== 'undefined' && input instanceof Request) return input.signal ?? null\n return null\n}\n\nfunction mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): HeadersInit {\n if (!base) return extra\n if (typeof Headers !== 'undefined' && base instanceof Headers) {\n const merged = new Headers(base)\n Object.entries(extra).forEach(([key, value]) => merged.set(key, value))\n return merged\n }\n if (Array.isArray(base)) {\n return [...base, ...Object.entries(extra)]\n }\n return { ...(base as Record<string, string>), ...extra }\n}\n\nexport async function withScopedApiRequestHeaders<T>(\n headers: Record<string, string>,\n run: () => Promise<T>,\n): Promise<T> {\n return scopedRequestHeaders.withScopedHeaders(headers, run)\n}\n\nexport async function apiCall<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ApiCallOptions<TReturn>,\n): Promise<ApiCallResult<TReturn>> {\n const scopedHeaders = scopedRequestHeaders.resolveScopedHeaders()\n const requestInit = Object.keys(scopedHeaders).length > 0\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scopedHeaders) }\n : init\n const response = await apiFetch(input, requestInit)\n const parser = options?.parse\n const fallback = options?.fallback ?? null\n let result: TReturn | null = null\n const rawCacheStatus =\n response.headers?.get?.('x-om-cache') ??\n response.headers?.get?.('x-cache-status') ??\n null\n const cacheStatus = rawCacheStatus === 'hit' || rawCacheStatus === 'miss' ? rawCacheStatus : null\n try {\n const source = typeof (response as Response & { clone?: () => Response }).clone === 'function'\n ? response.clone()\n : response\n if (parser) result = await parser(source)\n else result = await readJsonSafe<TReturn>(source, fallback)\n } catch (err) {\n if (isAbortError(err)) throw err\n result = fallback\n }\n // `readJsonSafe` swallows the abort that cancels an in-flight body read, so a\n // request aborted after its response headers arrived would otherwise look like\n // a successful call that returned an empty payload.\n if (result == null && resolveAbortSignal(input, init)?.aborted) {\n throw createAbortError()\n }\n return {\n ok: response.ok,\n status: response.status,\n result,\n response,\n cacheStatus,\n }\n}\n\nexport type ApiCallOrThrowOptions<TReturn> = ApiCallOptions<TReturn> & {\n errorMessage?: string\n}\n\nexport async function apiCallOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ApiCallOrThrowOptions<TReturn>,\n): Promise<ApiCallResult<TReturn>> {\n const { errorMessage, ...callOptions } = options ?? {}\n const call = await apiCall<TReturn>(input, init, callOptions)\n if (!call.ok) {\n await raiseCrudError(call.response, errorMessage)\n }\n return call\n}\n\nexport type ReadApiResultOrThrowOptions<TReturn> = ApiCallOrThrowOptions<TReturn> & {\n allowNullResult?: boolean\n emptyResultMessage?: string\n}\n\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ReadApiResultOrThrowOptions<TReturn> & { allowNullResult?: false },\n): Promise<TReturn>\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init: RequestInit | undefined,\n options: ReadApiResultOrThrowOptions<TReturn> & { allowNullResult: true },\n): Promise<TReturn | null>\nexport async function readApiResultOrThrow<TReturn = Record<string, unknown>>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options?: ReadApiResultOrThrowOptions<TReturn>,\n): Promise<TReturn | null> {\n const { allowNullResult = false, emptyResultMessage, ...callOptions } = options ?? {}\n const call = await apiCallOrThrow<TReturn>(input, init, callOptions)\n if (call.result == null && !allowNullResult) {\n const fallback =\n emptyResultMessage ?? callOptions.errorMessage ?? `Missing response payload (${call.status})`\n throw new Error(fallback)\n }\n return call.result\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -22,7 +22,8 @@ function LanguageSwitcher() {
|
|
|
22
22
|
en: t("common.languages.english", "English"),
|
|
23
23
|
pl: t("common.languages.polish", "Polski"),
|
|
24
24
|
es: t("common.languages.spanish", "Espa\xF1ol"),
|
|
25
|
-
de: t("common.languages.german", "Deutsch")
|
|
25
|
+
de: t("common.languages.german", "Deutsch"),
|
|
26
|
+
ko: t("common.languages.korean", "\uD55C\uAD6D\uC5B4")
|
|
26
27
|
};
|
|
27
28
|
async function setLocale(locale) {
|
|
28
29
|
if (locale === current) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/frontend/LanguageSwitcher.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useId, useTransition } from 'react'\nimport { useLocale, useLocaleLocked, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport { locales, type Locale } from '@open-mercato/shared/lib/i18n/config'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\n\nexport function LanguageSwitcher() {\n const current = useLocale()\n const localeLocked = useLocaleLocked()\n const t = useT()\n const router = useRouter()\n const [pending, startTransition] = useTransition()\n const selectId = useId()\n\n const languageLabels: Record<Locale, string> = {\n en: t('common.languages.english', 'English'),\n pl: t('common.languages.polish', 'Polski'),\n es: t('common.languages.spanish', 'Espa\u00F1ol'),\n de: t('common.languages.german', 'Deutsch'),\n }\n\n async function setLocale(locale: Locale) {\n if (locale === current) return\n try {\n const res = await fetch('/api/auth/locale', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ locale }),\n })\n if (!res.ok) return\n startTransition(() => router.refresh())\n try {\n window.dispatchEvent(new Event('om:refresh-sidebar'))\n } catch {\n // Ignore if window is unavailable\n }\n } catch {\n // Ignore network errors; UX fallback keeps previous locale\n }\n }\n\n // Locale is pinned via OM_FORCE_LOCALE \u2014 switching is a no-op, so hide the control.\n if (localeLocked) return null\n\n return (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <label htmlFor={selectId}>{t('common.language')}</label>\n <Select\n value={current}\n onValueChange={(value) => setLocale(value as Locale)}\n disabled={pending}\n >\n <SelectTrigger id={selectId} size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {locales.map((locale) => (\n <SelectItem key={locale} value={locale}>\n {languageLabels[locale]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useId, useTransition } from 'react'\nimport { useLocale, useLocaleLocked, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport { locales, type Locale } from '@open-mercato/shared/lib/i18n/config'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\n\nexport function LanguageSwitcher() {\n const current = useLocale()\n const localeLocked = useLocaleLocked()\n const t = useT()\n const router = useRouter()\n const [pending, startTransition] = useTransition()\n const selectId = useId()\n\n const languageLabels: Record<Locale, string> = {\n en: t('common.languages.english', 'English'),\n pl: t('common.languages.polish', 'Polski'),\n es: t('common.languages.spanish', 'Espa\u00F1ol'),\n de: t('common.languages.german', 'Deutsch'),\n ko: t('common.languages.korean', '\uD55C\uAD6D\uC5B4'),\n }\n\n async function setLocale(locale: Locale) {\n if (locale === current) return\n try {\n const res = await fetch('/api/auth/locale', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ locale }),\n })\n if (!res.ok) return\n startTransition(() => router.refresh())\n try {\n window.dispatchEvent(new Event('om:refresh-sidebar'))\n } catch {\n // Ignore if window is unavailable\n }\n } catch {\n // Ignore network errors; UX fallback keeps previous locale\n }\n }\n\n // Locale is pinned via OM_FORCE_LOCALE \u2014 switching is a no-op, so hide the control.\n if (localeLocked) return null\n\n return (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <label htmlFor={selectId}>{t('common.language')}</label>\n <Select\n value={current}\n onValueChange={(value) => setLocale(value as Locale)}\n disabled={pending}\n >\n <SelectTrigger id={selectId} size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {locales.map((locale) => (\n <SelectItem key={locale} value={locale}>\n {languageLabels[locale]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAsDM,cACA,YADA;AArDN,SAAS,OAAO,qBAAqB;AACrC,SAAS,WAAW,iBAAiB,YAAY;AACjD,SAAS,iBAAiB;AAC1B,SAAS,eAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,SAAS,mBAAmB;AACjC,QAAM,UAAU,UAAU;AAC1B,QAAM,eAAe,gBAAgB;AACrC,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,SAAS,eAAe,IAAI,cAAc;AACjD,QAAM,WAAW,MAAM;AAEvB,QAAM,iBAAyC;AAAA,IAC7C,IAAI,EAAE,4BAA4B,SAAS;AAAA,IAC3C,IAAI,EAAE,2BAA2B,QAAQ;AAAA,IACzC,IAAI,EAAE,4BAA4B,YAAS;AAAA,IAC3C,IAAI,EAAE,2BAA2B,SAAS;AAAA,IAC1C,IAAI,EAAE,2BAA2B,oBAAK;AAAA,EACxC;AAEA,iBAAe,UAAU,QAAgB;AACvC,QAAI,WAAW,QAAS;AACxB,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,oBAAoB;AAAA,QAC1C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;AAAA,MACjC,CAAC;AACD,UAAI,CAAC,IAAI,GAAI;AACb,sBAAgB,MAAM,OAAO,QAAQ,CAAC;AACtC,UAAI;AACF,eAAO,cAAc,IAAI,MAAM,oBAAoB,CAAC;AAAA,MACtD,QAAQ;AAAA,MAER;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,MAAI,aAAc,QAAO;AAEzB,SACE,qBAAC,SAAI,WAAU,yDACb;AAAA,wBAAC,WAAM,SAAS,UAAW,YAAE,iBAAiB,GAAE;AAAA,IAChD;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,eAAe,CAAC,UAAU,UAAU,KAAe;AAAA,QACnD,UAAU;AAAA,QAEV;AAAA,8BAAC,iBAAc,IAAI,UAAU,MAAK,MAChC,8BAAC,eAAY,GACf;AAAA,UACA,oBAAC,iBACE,kBAAQ,IAAI,CAAC,WACZ,oBAAC,cAAwB,OAAO,QAC7B,yBAAe,MAAM,KADP,MAEjB,CACD,GACH;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,10 +8,15 @@ const buttonVariants = cva(
|
|
|
8
8
|
variants: {
|
|
9
9
|
variant: {
|
|
10
10
|
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary-hover",
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/* Destructive is quiet by design: red text on a calm surface. A wall
|
|
12
|
+
of solid red buttons trains users to ignore red. The filled form is
|
|
13
|
+
`destructive-solid`, reserved for the single point-of-no-return
|
|
14
|
+
confirmation inside a dialog — never for the button that opens it. */
|
|
15
|
+
destructive: "border border-destructive bg-background text-destructive shadow-xs hover:bg-destructive/10 aria-invalid:ring-destructive dark:aria-invalid:ring-destructive",
|
|
16
|
+
"destructive-solid": "bg-destructive text-white shadow-xs hover:bg-destructive/90 aria-invalid:ring-destructive dark:aria-invalid:ring-destructive",
|
|
17
|
+
"destructive-outline": "border border-destructive bg-background text-destructive shadow-xs hover:bg-destructive/10",
|
|
18
|
+
"destructive-soft": "bg-destructive/10 text-destructive hover:bg-destructive/15",
|
|
19
|
+
"destructive-ghost": "text-destructive hover:bg-destructive/10",
|
|
15
20
|
outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
16
21
|
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
17
22
|
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/button.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer transition-all disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-disabled disabled:shadow-none disabled:[background-image:none] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:outline-none focus-visible:shadow-focus aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary-hover',\n destructive:\n '
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer transition-all disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-disabled disabled:shadow-none disabled:[background-image:none] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:outline-none focus-visible:shadow-focus aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary-hover',\n /* Destructive is quiet by design: red text on a calm surface. A wall\n of solid red buttons trains users to ignore red. The filled form is\n `destructive-solid`, reserved for the single point-of-no-return\n confirmation inside a dialog \u2014 never for the button that opens it. */\n destructive:\n 'border border-destructive bg-background text-destructive shadow-xs hover:bg-destructive/10 aria-invalid:ring-destructive dark:aria-invalid:ring-destructive',\n 'destructive-solid':\n 'bg-destructive text-white shadow-xs hover:bg-destructive/90 aria-invalid:ring-destructive dark:aria-invalid:ring-destructive',\n 'destructive-outline':\n 'border border-destructive bg-background text-destructive shadow-xs hover:bg-destructive/10',\n 'destructive-soft':\n 'bg-destructive/10 text-destructive hover:bg-destructive/15',\n 'destructive-ghost':\n 'text-destructive hover:bg-destructive/10',\n outline:\n 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',\n secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',\n ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',\n muted: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',\n link: 'text-primary underline-offset-4 hover:underline',\n },\n size: {\n default: 'h-9 px-4 py-2 has-[>svg]:px-3',\n sm: 'h-8 gap-1.5 px-3 has-[>svg]:px-2.5',\n lg: 'h-10 px-6 has-[>svg]:px-4',\n '2xs': 'h-7 gap-1 px-2 py-0.5 has-[>svg]:px-1.5 text-xs',\n icon: 'size-9',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n }\n)\n\nexport function Button({\n className,\n variant,\n size,\n asChild = false,\n ...props\n}: React.ComponentProps<'button'> &\n VariantProps<typeof buttonVariants> & { asChild?: boolean }) {\n const Comp = asChild ? Slot : 'button'\n return <Comp data-slot=\"button\" className={cn(buttonVariants({ variant, size, className }))} {...props} />\n}\n\nexport { buttonVariants }\n\n"],
|
|
5
|
+
"mappings": "AAwDS;AAvDT,SAAS,YAAY;AACrB,SAAS,WAA8B;AACvC,SAAS,UAAU;AAEnB,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,QAKT,aACE;AAAA,QACF,qBACE;AAAA,QACF,uBACE;AAAA,QACF,oBACE;AAAA,QACF,qBACE;AAAA,QACF,SACE;AAAA,QACF,WAAW;AAAA,QACX,OAAO;AAAA,QACP,OAAO;AAAA,QACP,MAAM;AAAA,MACR;AAAA,MACA,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEO,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,GAAG;AACL,GAC+D;AAC7D,QAAM,OAAO,UAAU,OAAO;AAC9B,SAAO,oBAAC,QAAK,aAAU,UAAS,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,CAAC,GAAI,GAAG,OAAO;AAC1G;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6842.1.8cd8a8883c",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,16 +155,16 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
158
|
+
"@open-mercato/shared": "0.6.7-develop.6842.1.8cd8a8883c",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
165
|
+
"@open-mercato/shared": "0.6.7-develop.6842.1.8cd8a8883c",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
|
-
"@testing-library/jest-dom": "^
|
|
167
|
+
"@testing-library/jest-dom": "^7.0.0",
|
|
168
168
|
"@testing-library/react": "^16.3.1",
|
|
169
169
|
"@types/jest": "^30.0.0",
|
|
170
170
|
"@types/react": "^19.2.17",
|
|
@@ -164,7 +164,7 @@ export function JsonBuilder({
|
|
|
164
164
|
disabled={disabled}
|
|
165
165
|
/>
|
|
166
166
|
{parseError && (
|
|
167
|
-
<div className="text-xs text-
|
|
167
|
+
<div className="text-xs text-status-error-text">Invalid JSON format</div>
|
|
168
168
|
)}
|
|
169
169
|
</div>
|
|
170
170
|
) : (
|
|
@@ -185,7 +185,7 @@ export function JsonBuilder({
|
|
|
185
185
|
</div>
|
|
186
186
|
)}
|
|
187
187
|
|
|
188
|
-
{error && <div className="text-xs text-
|
|
188
|
+
{error && <div className="text-xs text-status-error-text">{error}</div>}
|
|
189
189
|
{ConfirmDialogElement}
|
|
190
190
|
</div>
|
|
191
191
|
)
|
|
@@ -363,7 +363,7 @@ function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot, confirm }
|
|
|
363
363
|
type="button"
|
|
364
364
|
variant="ghost"
|
|
365
365
|
size="xs"
|
|
366
|
-
className="text-muted-foreground hover:text-
|
|
366
|
+
className="text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
|
|
367
367
|
title="Remove item"
|
|
368
368
|
onClick={onDelete}
|
|
369
369
|
>
|
|
@@ -123,7 +123,7 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
123
123
|
<a
|
|
124
124
|
key={idx}
|
|
125
125
|
href={it.href}
|
|
126
|
-
className={`block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? 'text-
|
|
126
|
+
className={`block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? 'text-destructive' : ''}`}
|
|
127
127
|
role="menuitem"
|
|
128
128
|
onClick={(event) => {
|
|
129
129
|
event.stopPropagation()
|
|
@@ -138,7 +138,7 @@ export function RowActions({ items = [] }: { items?: RowActionItem[] }) {
|
|
|
138
138
|
type="button"
|
|
139
139
|
variant="ghost"
|
|
140
140
|
size="sm"
|
|
141
|
-
className={`w-full justify-start rounded-none font-normal ${it.destructive ? 'text-
|
|
141
|
+
className={`w-full justify-start rounded-none font-normal ${it.destructive ? 'text-destructive' : ''}`}
|
|
142
142
|
role="menuitem"
|
|
143
143
|
onClick={(event) => {
|
|
144
144
|
event.stopPropagation()
|
|
@@ -277,7 +277,7 @@ export function ConfirmDialog({
|
|
|
277
277
|
{resolvedConfirmText !== false && (
|
|
278
278
|
<Button
|
|
279
279
|
ref={confirmButtonRef}
|
|
280
|
-
variant={variant === "destructive" ? "destructive" : "default"}
|
|
280
|
+
variant={variant === "destructive" ? "destructive-solid" : "default"}
|
|
281
281
|
onClick={handleConfirm}
|
|
282
282
|
disabled={loading}
|
|
283
283
|
className="w-full sm:w-auto"
|