@getdom/studio 0.1.0
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/LICENSE.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from 'vue';
|
|
3
|
+
import { createInlineDiff, createSplitDiff, createUnifiedDiff } from './textDiff.js';
|
|
4
|
+
|
|
5
|
+
defineOptions({
|
|
6
|
+
__doc: {
|
|
7
|
+
name: 'Text diff',
|
|
8
|
+
tag: '<DomTextDiff>',
|
|
9
|
+
description: 'A standalone text comparison for AI rewrites, code patches, editorial review, and before-and-after copy changes.',
|
|
10
|
+
icon: 'M8 7H4m4 0L6 5m2 2L6 9m10 8h4m-2-2v4M11 5h3a3 3 0 0 1 3 3v1m-6 10H8a3 3 0 0 1-3-3v-1',
|
|
11
|
+
playground: {
|
|
12
|
+
initial: {
|
|
13
|
+
original: 'Write better pages with a flexible visual editor.',
|
|
14
|
+
proposed: 'Create clearer pages with an editor built for focused teams.',
|
|
15
|
+
mode: 'diff',
|
|
16
|
+
view: 'inline',
|
|
17
|
+
format: 'text',
|
|
18
|
+
originalLabel: 'Current',
|
|
19
|
+
proposedLabel: 'Suggested',
|
|
20
|
+
showHeader: true,
|
|
21
|
+
showModeSwitcher: true,
|
|
22
|
+
framed: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
slots: [
|
|
26
|
+
{ name: 'header', payload: '{ additions, removals, mode, setMode, originalLabel, proposedLabel }', description: 'Replace the comparison header and optionally control the active content mode.' },
|
|
27
|
+
],
|
|
28
|
+
events: [
|
|
29
|
+
{ name: 'update:mode', payload: "'diff' | 'original' | 'replacement'", description: 'Fired when the active content mode changes.' },
|
|
30
|
+
{ name: 'mode-change', payload: "'diff' | 'original' | 'replacement'", description: 'Fired after the viewer switches between the diff, current text, and replacement text.' },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const props = defineProps({
|
|
36
|
+
original: {
|
|
37
|
+
type: String,
|
|
38
|
+
default: '',
|
|
39
|
+
_edit: { description: 'Current or original text.' },
|
|
40
|
+
},
|
|
41
|
+
proposed: {
|
|
42
|
+
type: String,
|
|
43
|
+
default: '',
|
|
44
|
+
_edit: { description: 'Suggested replacement text.' },
|
|
45
|
+
},
|
|
46
|
+
mode: {
|
|
47
|
+
type: String,
|
|
48
|
+
default: 'diff',
|
|
49
|
+
_edit: { options: ['diff', 'original', 'replacement'], description: 'Show the complete diff, current text, or finished replacement.' },
|
|
50
|
+
},
|
|
51
|
+
view: {
|
|
52
|
+
type: String,
|
|
53
|
+
default: 'inline',
|
|
54
|
+
_edit: { options: ['inline', 'unified', 'split'], description: 'Inline prose, unified patch, or side-by-side comparison.' },
|
|
55
|
+
},
|
|
56
|
+
format: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: 'text',
|
|
59
|
+
_edit: { options: ['text', 'code'], description: 'Use proportional text or code-oriented monospace styling.' },
|
|
60
|
+
},
|
|
61
|
+
originalLabel: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: 'Original',
|
|
64
|
+
_edit: { description: 'Label for the original content.' },
|
|
65
|
+
},
|
|
66
|
+
proposedLabel: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: 'Suggested',
|
|
69
|
+
_edit: { description: 'Label for the proposed content.' },
|
|
70
|
+
},
|
|
71
|
+
showHeader: {
|
|
72
|
+
type: Boolean,
|
|
73
|
+
default: true,
|
|
74
|
+
_edit: { description: 'Show comparison labels and change counts.' },
|
|
75
|
+
},
|
|
76
|
+
showModeSwitcher: {
|
|
77
|
+
type: Boolean,
|
|
78
|
+
default: true,
|
|
79
|
+
_edit: { description: 'Show the Diff, Current, and Replacement controls inside the standard header.' },
|
|
80
|
+
},
|
|
81
|
+
framed: {
|
|
82
|
+
type: Boolean,
|
|
83
|
+
default: true,
|
|
84
|
+
_edit: { description: 'Render the standard border, radius, and canvas background.' },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const emit = defineEmits(['update:mode', 'mode-change']);
|
|
89
|
+
const modeOptions = [
|
|
90
|
+
{ value: 'diff', label: 'Diff' },
|
|
91
|
+
{ value: 'original', label: 'Current' },
|
|
92
|
+
{ value: 'replacement', label: 'Replacement' },
|
|
93
|
+
];
|
|
94
|
+
const activeMode = ref(normalizeMode(props.mode));
|
|
95
|
+
|
|
96
|
+
const resolvedView = computed(resolveView);
|
|
97
|
+
const resolvedFormat = computed(resolveFormat);
|
|
98
|
+
const inlineSegments = computed(resolveInlineSegments);
|
|
99
|
+
const unifiedRows = computed(resolveUnifiedRows);
|
|
100
|
+
const splitRows = computed(resolveSplitRows);
|
|
101
|
+
const changeStats = computed(calculateChangeStats);
|
|
102
|
+
const figureLabel = computed(resolveFigureLabel);
|
|
103
|
+
const completeText = computed(resolveCompleteText);
|
|
104
|
+
const completeTextLabel = computed(resolveCompleteTextLabel);
|
|
105
|
+
|
|
106
|
+
watch(readModeProp, syncModeFromProp);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Checks whether a content mode is supported.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} value Candidate content mode.
|
|
112
|
+
* @returns {boolean} Whether the mode is supported.
|
|
113
|
+
*/
|
|
114
|
+
function isSupportedMode(value) {
|
|
115
|
+
for (const option of modeOptions) {
|
|
116
|
+
if (option.value === value) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Normalizes a content mode to the complete diff view.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} value Candidate content mode.
|
|
125
|
+
* @returns {'diff'|'original'|'replacement'} Supported content mode.
|
|
126
|
+
*/
|
|
127
|
+
function normalizeMode(value) {
|
|
128
|
+
return isSupportedMode(value) ? value : 'diff';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Reads the externally supplied mode for the prop watcher.
|
|
133
|
+
*
|
|
134
|
+
* @returns {string} Current mode prop.
|
|
135
|
+
*/
|
|
136
|
+
function readModeProp() {
|
|
137
|
+
return props.mode;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Synchronizes externally controlled mode changes with the local viewer.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} value Updated mode prop.
|
|
144
|
+
* @returns {void}
|
|
145
|
+
*/
|
|
146
|
+
function syncModeFromProp(value) {
|
|
147
|
+
activeMode.value = normalizeMode(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Switches the visible content and notifies controlled consumers.
|
|
152
|
+
*
|
|
153
|
+
* @param {'diff'|'original'|'replacement'} value Requested content mode.
|
|
154
|
+
* @returns {void}
|
|
155
|
+
*/
|
|
156
|
+
function setMode(value) {
|
|
157
|
+
const nextMode = normalizeMode(value);
|
|
158
|
+
if (nextMode === activeMode.value) return;
|
|
159
|
+
activeMode.value = nextMode;
|
|
160
|
+
emit('update:mode', nextMode);
|
|
161
|
+
emit('mode-change', nextMode);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Checks whether a diff view is supported.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} value Candidate view.
|
|
168
|
+
* @returns {boolean} Whether the view is supported.
|
|
169
|
+
*/
|
|
170
|
+
function isSupportedView(value) {
|
|
171
|
+
return ['inline', 'unified', 'split'].includes(value);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Checks whether a text format is supported.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} value Candidate format.
|
|
178
|
+
* @returns {boolean} Whether the format is supported.
|
|
179
|
+
*/
|
|
180
|
+
function isSupportedFormat(value) {
|
|
181
|
+
return ['text', 'code'].includes(value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Resolves unknown view values to the inline presentation.
|
|
186
|
+
*
|
|
187
|
+
* @returns {'inline'|'unified'|'split'} Supported view.
|
|
188
|
+
*/
|
|
189
|
+
function resolveView() {
|
|
190
|
+
return isSupportedView(props.view) ? props.view : 'inline';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Resolves unknown format values to proportional text.
|
|
195
|
+
*
|
|
196
|
+
* @returns {'text'|'code'} Supported format.
|
|
197
|
+
*/
|
|
198
|
+
function resolveFormat() {
|
|
199
|
+
return isSupportedFormat(props.format) ? props.format : 'text';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Creates inline segments for the current component values.
|
|
204
|
+
*
|
|
205
|
+
* @returns {Array<Record<string, unknown>>} Inline diff segments.
|
|
206
|
+
*/
|
|
207
|
+
function resolveInlineSegments() {
|
|
208
|
+
return createInlineDiff(props.original, props.proposed);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Creates unified rows for the current component values.
|
|
213
|
+
*
|
|
214
|
+
* @returns {Array<Record<string, unknown>>} Unified diff rows.
|
|
215
|
+
*/
|
|
216
|
+
function resolveUnifiedRows() {
|
|
217
|
+
return createUnifiedDiff(props.original, props.proposed);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Creates split rows for the current component values.
|
|
222
|
+
*
|
|
223
|
+
* @returns {Array<Record<string, unknown>>} Split diff rows.
|
|
224
|
+
*/
|
|
225
|
+
function resolveSplitRows() {
|
|
226
|
+
return createSplitDiff(props.original, props.proposed);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Describes the currently visible comparison or complete text.
|
|
231
|
+
*
|
|
232
|
+
* @returns {string} Accessible figure label.
|
|
233
|
+
*/
|
|
234
|
+
function resolveFigureLabel() {
|
|
235
|
+
if (activeMode.value === 'original') return `${props.originalLabel} text`;
|
|
236
|
+
if (activeMode.value === 'replacement') return `${props.proposedLabel} replacement text`;
|
|
237
|
+
return `${props.originalLabel} compared with ${props.proposedLabel}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Returns the complete text for the active non-diff mode.
|
|
242
|
+
*
|
|
243
|
+
* @returns {string} Original or proposed text.
|
|
244
|
+
*/
|
|
245
|
+
function resolveCompleteText() {
|
|
246
|
+
return activeMode.value === 'original' ? props.original : props.proposed;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Returns the accessible label for the active complete text.
|
|
251
|
+
*
|
|
252
|
+
* @returns {string} Complete text label.
|
|
253
|
+
*/
|
|
254
|
+
function resolveCompleteTextLabel() {
|
|
255
|
+
return activeMode.value === 'original' ? props.originalLabel : props.proposedLabel;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Calculates visible addition and removal counts for the active view.
|
|
260
|
+
*
|
|
261
|
+
* @returns {{ additions: number, removals: number }} Change counts.
|
|
262
|
+
*/
|
|
263
|
+
function calculateChangeStats() {
|
|
264
|
+
const operations = resolvedView.value === 'inline' ? inlineSegments.value : unifiedRows.value;
|
|
265
|
+
return {
|
|
266
|
+
additions: operations.filter(isAddedOperation).length,
|
|
267
|
+
removals: operations.filter(isRemovedOperation).length,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Checks whether an operation represents added content.
|
|
273
|
+
*
|
|
274
|
+
* @param {Record<string, unknown>} operation Diff operation.
|
|
275
|
+
* @returns {boolean} Whether content was added.
|
|
276
|
+
*/
|
|
277
|
+
function isAddedOperation(operation) {
|
|
278
|
+
return operation.type === 'added';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Checks whether an operation represents removed content.
|
|
283
|
+
*
|
|
284
|
+
* @param {Record<string, unknown>} operation Diff operation.
|
|
285
|
+
* @returns {boolean} Whether content was removed.
|
|
286
|
+
*/
|
|
287
|
+
function isRemovedOperation(operation) {
|
|
288
|
+
return operation.type === 'removed';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Returns the visible sign for a unified diff row.
|
|
293
|
+
*
|
|
294
|
+
* @param {Record<string, unknown>} row Diff row.
|
|
295
|
+
* @returns {string} Addition, removal, or unchanged sign.
|
|
296
|
+
*/
|
|
297
|
+
function rowSign(row) {
|
|
298
|
+
if (row.type === 'added') return '+';
|
|
299
|
+
if (row.type === 'removed') return '−';
|
|
300
|
+
return '';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Returns the visible line number pair for a unified row.
|
|
305
|
+
*
|
|
306
|
+
* @param {Record<string, unknown>} row Diff row.
|
|
307
|
+
* @returns {string} Original and proposed line numbers.
|
|
308
|
+
*/
|
|
309
|
+
function rowNumbers(row) {
|
|
310
|
+
return `${row.originalNumber ?? ''} ${row.proposedNumber ?? ''}`.trim();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Returns the semantic background utility for a diff line.
|
|
315
|
+
*
|
|
316
|
+
* @param {string} type Diff line state.
|
|
317
|
+
* @returns {string} Tailwind background utility.
|
|
318
|
+
*/
|
|
319
|
+
function lineBackgroundClass(type) {
|
|
320
|
+
if (type === 'removed') return 'bg-destructive/10';
|
|
321
|
+
if (type === 'added') return 'bg-success/10';
|
|
322
|
+
if (type === 'blank') return 'bg-secondary/35';
|
|
323
|
+
return '';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Returns the semantic text utility for a diff sign.
|
|
328
|
+
*
|
|
329
|
+
* @param {string} type Diff line state.
|
|
330
|
+
* @returns {string} Tailwind text utility.
|
|
331
|
+
*/
|
|
332
|
+
function lineSignClass(type) {
|
|
333
|
+
if (type === 'removed') return 'text-destructive';
|
|
334
|
+
if (type === 'added') return 'text-success';
|
|
335
|
+
return 'text-muted-fg';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Returns the semantic highlight utility for changed text.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} type Diff line state.
|
|
342
|
+
* @returns {string} Tailwind background utility.
|
|
343
|
+
*/
|
|
344
|
+
function markBackgroundClass(type) {
|
|
345
|
+
return type === 'removed' ? 'bg-destructive/20' : 'bg-success/25';
|
|
346
|
+
}
|
|
347
|
+
</script>
|
|
348
|
+
|
|
349
|
+
<template>
|
|
350
|
+
<figure
|
|
351
|
+
class="w-full min-w-0 overflow-hidden bg-canvas text-sm leading-[1.65] text-canvas-fg [container-type:inline-size]"
|
|
352
|
+
:class="framed ? 'rounded-xl border border-border' : ''"
|
|
353
|
+
:aria-label="figureLabel"
|
|
354
|
+
>
|
|
355
|
+
<header
|
|
356
|
+
v-if="showHeader"
|
|
357
|
+
data-diff-header
|
|
358
|
+
class="grid min-h-[2.65rem] grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-4 border-b border-border bg-secondary/45 px-3 py-[0.55rem]"
|
|
359
|
+
:class="framed ? '' : 'rounded-t-lg border border-border'"
|
|
360
|
+
>
|
|
361
|
+
<slot
|
|
362
|
+
name="header"
|
|
363
|
+
:additions="changeStats.additions"
|
|
364
|
+
:removals="changeStats.removals"
|
|
365
|
+
:mode="activeMode"
|
|
366
|
+
:set-mode="setMode"
|
|
367
|
+
:original-label="originalLabel"
|
|
368
|
+
:proposed-label="proposedLabel"
|
|
369
|
+
>
|
|
370
|
+
<div class="flex min-w-0 items-center gap-2 text-xs font-[650]">
|
|
371
|
+
<span>{{ originalLabel }}</span>
|
|
372
|
+
<svg
|
|
373
|
+
v-if="resolvedView !== 'split'"
|
|
374
|
+
class="size-4 shrink-0 text-muted-fg"
|
|
375
|
+
viewBox="0 0 20 20"
|
|
376
|
+
fill="none"
|
|
377
|
+
stroke="currentColor"
|
|
378
|
+
stroke-linecap="round"
|
|
379
|
+
stroke-linejoin="round"
|
|
380
|
+
stroke-width="1.5"
|
|
381
|
+
aria-hidden="true"
|
|
382
|
+
>
|
|
383
|
+
<path d="M4 10h12m-4-4 4 4-4 4" />
|
|
384
|
+
</svg>
|
|
385
|
+
<span>{{ proposedLabel }}</span>
|
|
386
|
+
</div>
|
|
387
|
+
<div
|
|
388
|
+
v-if="showModeSwitcher"
|
|
389
|
+
data-mode-switcher
|
|
390
|
+
class="col-start-3 row-start-1 flex items-center rounded-lg border border-border bg-secondary/70 p-0.5"
|
|
391
|
+
role="group"
|
|
392
|
+
aria-label="Displayed text"
|
|
393
|
+
>
|
|
394
|
+
<button
|
|
395
|
+
v-for="option in modeOptions"
|
|
396
|
+
:key="option.value"
|
|
397
|
+
type="button"
|
|
398
|
+
class="cursor-pointer rounded-md border-0 px-2 py-1.5 font-[inherit] text-[0.6875rem] font-[650] leading-none transition-[background-color,color,box-shadow] duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring motion-reduce:transition-none"
|
|
399
|
+
:class="activeMode === option.value
|
|
400
|
+
? 'bg-canvas text-canvas-fg shadow-sm'
|
|
401
|
+
: 'bg-transparent text-muted-fg hover:text-canvas-fg'"
|
|
402
|
+
:aria-pressed="activeMode === option.value"
|
|
403
|
+
@click="setMode(option.value)"
|
|
404
|
+
>
|
|
405
|
+
{{ option.label }}
|
|
406
|
+
</button>
|
|
407
|
+
</div>
|
|
408
|
+
<Transition
|
|
409
|
+
enter-active-class="transition-opacity duration-150 ease-out motion-reduce:transition-none"
|
|
410
|
+
enter-from-class="opacity-0"
|
|
411
|
+
enter-to-class="opacity-100"
|
|
412
|
+
leave-active-class="transition-opacity duration-100 ease-in motion-reduce:transition-none"
|
|
413
|
+
leave-from-class="opacity-100"
|
|
414
|
+
leave-to-class="opacity-0"
|
|
415
|
+
>
|
|
416
|
+
<div
|
|
417
|
+
v-if="activeMode === 'diff'"
|
|
418
|
+
class="col-start-2 row-start-1 flex items-center gap-2"
|
|
419
|
+
aria-label="Change summary"
|
|
420
|
+
>
|
|
421
|
+
<span class="rounded-md bg-destructive/10 px-1.5 py-1 font-mono text-[0.6875rem] font-bold leading-none text-destructive">−{{ changeStats.removals }}</span>
|
|
422
|
+
<span class="rounded-md bg-success/15 px-1.5 py-1 font-mono text-[0.6875rem] font-bold leading-none text-success">+{{ changeStats.additions }}</span>
|
|
423
|
+
</div>
|
|
424
|
+
</Transition>
|
|
425
|
+
</slot>
|
|
426
|
+
</header>
|
|
427
|
+
|
|
428
|
+
<div class="grid min-w-0">
|
|
429
|
+
<Transition
|
|
430
|
+
enter-active-class="transition-opacity duration-150 ease-out motion-reduce:transition-none"
|
|
431
|
+
enter-from-class="opacity-0"
|
|
432
|
+
enter-to-class="opacity-100"
|
|
433
|
+
leave-active-class="transition-opacity duration-100 ease-in motion-reduce:transition-none"
|
|
434
|
+
leave-from-class="opacity-100"
|
|
435
|
+
leave-to-class="opacity-0"
|
|
436
|
+
>
|
|
437
|
+
<div
|
|
438
|
+
v-if="activeMode !== 'diff'"
|
|
439
|
+
:key="activeMode"
|
|
440
|
+
class="col-start-1 row-start-1 min-h-14 overflow-auto whitespace-pre-wrap break-words bg-secondary/20 px-4 py-3.5"
|
|
441
|
+
:aria-label="completeTextLabel"
|
|
442
|
+
>
|
|
443
|
+
<pre v-if="resolvedFormat === 'code'" class="m-0 min-w-max whitespace-pre font-mono text-[0.8125rem] leading-[1.65]"><code>{{ completeText }}</code></pre>
|
|
444
|
+
<div v-else role="document">{{ completeText }}</div>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div
|
|
448
|
+
v-else-if="resolvedView === 'inline'"
|
|
449
|
+
key="diff-inline"
|
|
450
|
+
class="col-start-1 row-start-1 min-h-14 whitespace-pre-wrap break-words bg-secondary/20 px-4 py-3.5"
|
|
451
|
+
:class="resolvedFormat === 'code' ? 'font-mono text-[0.8125rem]' : ''"
|
|
452
|
+
>
|
|
453
|
+
<template v-for="segment in inlineSegments" :key="segment.id">
|
|
454
|
+
<ins
|
|
455
|
+
v-if="segment.type === 'added'"
|
|
456
|
+
class="rounded-[0.2rem] bg-success/15 px-[0.14em] py-[0.08em] text-success no-underline [box-decoration-break:clone] [-webkit-box-decoration-break:clone]"
|
|
457
|
+
>{{ segment.value }}</ins>
|
|
458
|
+
<del
|
|
459
|
+
v-else-if="segment.type === 'removed'"
|
|
460
|
+
class="rounded-[0.2rem] bg-destructive/15 px-[0.14em] py-[0.08em] text-destructive decoration-[1px] [box-decoration-break:clone] [-webkit-box-decoration-break:clone]"
|
|
461
|
+
>{{ segment.value }}</del>
|
|
462
|
+
<span v-else>{{ segment.value }}</span>
|
|
463
|
+
</template>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<div
|
|
467
|
+
v-else-if="resolvedView === 'unified'"
|
|
468
|
+
key="diff-unified"
|
|
469
|
+
class="col-start-1 row-start-1 min-w-0 overflow-auto"
|
|
470
|
+
>
|
|
471
|
+
<div
|
|
472
|
+
class="w-max min-w-full text-xs leading-[1.65]"
|
|
473
|
+
:class="resolvedFormat === 'code' ? 'font-mono' : ''"
|
|
474
|
+
role="table"
|
|
475
|
+
aria-label="Unified text changes"
|
|
476
|
+
>
|
|
477
|
+
<div
|
|
478
|
+
v-for="row in unifiedRows"
|
|
479
|
+
:key="row.id"
|
|
480
|
+
class="grid min-h-7 grid-cols-[1.75rem_3.75rem_minmax(20rem,1fr)] border-t border-border/50 first:border-t-0"
|
|
481
|
+
:class="lineBackgroundClass(row.type)"
|
|
482
|
+
role="row"
|
|
483
|
+
>
|
|
484
|
+
<span
|
|
485
|
+
class="select-none px-[0.45rem] py-[0.14rem] text-center font-extrabold tabular-nums"
|
|
486
|
+
:class="lineSignClass(row.type)"
|
|
487
|
+
aria-hidden="true"
|
|
488
|
+
>{{ rowSign(row) }}</span>
|
|
489
|
+
<span
|
|
490
|
+
class="select-none px-[0.45rem] py-[0.14rem] text-right text-muted-fg tabular-nums"
|
|
491
|
+
:aria-label="`Lines ${rowNumbers(row)}`"
|
|
492
|
+
>{{ rowNumbers(row) }}</span>
|
|
493
|
+
<code class="block whitespace-pre border-l border-border/60 px-3 py-[0.14rem]" role="cell">
|
|
494
|
+
<template v-for="segment in row.segments" :key="segment.id">
|
|
495
|
+
<mark
|
|
496
|
+
v-if="segment.type === row.type && row.type !== 'same'"
|
|
497
|
+
class="rounded-[0.18rem] px-[0.1em] py-[0.05em] font-[inherit] text-inherit"
|
|
498
|
+
:class="markBackgroundClass(row.type)"
|
|
499
|
+
>{{ segment.value }}</mark>
|
|
500
|
+
<span v-else>{{ segment.value }}</span>
|
|
501
|
+
</template>
|
|
502
|
+
</code>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
<div v-else key="diff-split" class="col-start-1 row-start-1 min-w-0 overflow-auto">
|
|
508
|
+
<div
|
|
509
|
+
class="w-max min-w-max text-xs leading-[1.65]"
|
|
510
|
+
:class="resolvedFormat === 'code' ? 'font-mono' : ''"
|
|
511
|
+
role="table"
|
|
512
|
+
aria-label="Side-by-side text changes"
|
|
513
|
+
>
|
|
514
|
+
<div
|
|
515
|
+
v-for="row in splitRows"
|
|
516
|
+
:key="row.id"
|
|
517
|
+
class="grid grid-cols-[minmax(20rem,1fr)_minmax(20rem,1fr)] border-t border-border/50 first:border-t-0"
|
|
518
|
+
role="row"
|
|
519
|
+
>
|
|
520
|
+
<div
|
|
521
|
+
class="grid min-h-7 grid-cols-[2.5rem_minmax(17.5rem,1fr)]"
|
|
522
|
+
:class="lineBackgroundClass(row.original?.type || 'blank')"
|
|
523
|
+
role="cell"
|
|
524
|
+
>
|
|
525
|
+
<span class="select-none px-[0.45rem] py-[0.14rem] text-right text-muted-fg tabular-nums">{{ row.original?.originalNumber || '' }}</span>
|
|
526
|
+
<code class="block whitespace-pre border-l border-border/60 px-2.5 py-[0.14rem]">
|
|
527
|
+
<template v-for="segment in row.original?.segments || []" :key="segment.id">
|
|
528
|
+
<mark
|
|
529
|
+
v-if="segment.type === 'removed'"
|
|
530
|
+
class="rounded-[0.18rem] bg-destructive/20 px-[0.1em] py-[0.05em] font-[inherit] text-inherit"
|
|
531
|
+
>{{ segment.value }}</mark>
|
|
532
|
+
<span v-else>{{ segment.value }}</span>
|
|
533
|
+
</template>
|
|
534
|
+
</code>
|
|
535
|
+
</div>
|
|
536
|
+
<div
|
|
537
|
+
class="grid min-h-7 grid-cols-[2.5rem_minmax(17.5rem,1fr)] border-l border-border"
|
|
538
|
+
:class="lineBackgroundClass(row.proposed?.type || 'blank')"
|
|
539
|
+
role="cell"
|
|
540
|
+
>
|
|
541
|
+
<span class="select-none px-[0.45rem] py-[0.14rem] text-right text-muted-fg tabular-nums">{{ row.proposed?.proposedNumber || '' }}</span>
|
|
542
|
+
<code class="block whitespace-pre border-l border-border/60 px-2.5 py-[0.14rem]">
|
|
543
|
+
<template v-for="segment in row.proposed?.segments || []" :key="segment.id">
|
|
544
|
+
<mark
|
|
545
|
+
v-if="segment.type === 'added'"
|
|
546
|
+
class="rounded-[0.18rem] bg-success/25 px-[0.1em] py-[0.05em] font-[inherit] text-inherit"
|
|
547
|
+
>{{ segment.value }}</mark>
|
|
548
|
+
<span v-else>{{ segment.value }}</span>
|
|
549
|
+
</template>
|
|
550
|
+
</code>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
</Transition>
|
|
556
|
+
</div>
|
|
557
|
+
</figure>
|
|
558
|
+
</template>
|
|
559
|
+
|
|
560
|
+
<style scoped>
|
|
561
|
+
@container (max-width: 32rem) {
|
|
562
|
+
[data-diff-header] {
|
|
563
|
+
gap: 0.55rem;
|
|
564
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
[data-mode-switcher] {
|
|
568
|
+
grid-column: 1 / -1;
|
|
569
|
+
grid-row: 2;
|
|
570
|
+
width: 100%;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
[data-mode-switcher] button {
|
|
574
|
+
flex: 1;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
</style>
|