quasar-ui-danx 0.4.95 → 0.5.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/dist/danx.es.js +25284 -23176
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +133 -120
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -2
- package/scripts/publish.sh +76 -0
- package/src/components/Utility/Buttons/ActionButton.vue +11 -3
- package/src/components/Utility/Code/CodeViewer.vue +219 -0
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +34 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +53 -0
- package/src/components/Utility/Code/LanguageBadge.vue +122 -0
- package/src/components/Utility/Code/MarkdownContent.vue +405 -0
- package/src/components/Utility/Code/index.ts +5 -0
- package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +134 -38
- package/src/components/Utility/Files/CarouselHeader.vue +24 -0
- package/src/components/Utility/Files/FileMetadataDialog.vue +69 -0
- package/src/components/Utility/Files/FilePreview.vue +118 -166
- package/src/components/Utility/Files/index.ts +1 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/useCodeFormat.ts +199 -0
- package/src/composables/useCodeViewerCollapse.ts +125 -0
- package/src/composables/useCodeViewerEditor.ts +420 -0
- package/src/composables/useFilePreview.ts +119 -0
- package/src/composables/useTranscodeLoader.ts +68 -0
- package/src/helpers/formats/highlightSyntax.ts +327 -0
- package/src/helpers/formats/index.ts +3 -1
- package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
- package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
- package/src/helpers/formats/markdown/index.ts +85 -0
- package/src/helpers/formats/markdown/parseInline.ts +124 -0
- package/src/helpers/formats/markdown/render/index.ts +92 -0
- package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
- package/src/helpers/formats/markdown/render/renderList.ts +69 -0
- package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
- package/src/helpers/formats/markdown/state.ts +58 -0
- package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
- package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
- package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
- package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
- package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
- package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
- package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
- package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
- package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
- package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
- package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
- package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
- package/src/helpers/formats/markdown/types.ts +63 -0
- package/src/styles/danx.scss +4 -0
- package/src/styles/themes/danx/code.scss +158 -0
- package/src/styles/themes/danx/index.scss +2 -0
- package/src/styles/themes/danx/markdown.scss +241 -0
- package/src/styles/themes/danx/scrollbar.scss +125 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quasar-ui-danx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"author": "Dan <dan@flytedesk.com>",
|
|
5
5
|
"description": "DanX Vue / Quasar component library",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"build:bundle": "vite build",
|
|
16
16
|
"build": "yarn clean && yarn build:types && yarn build:bundle",
|
|
17
17
|
"preview": "vite preview",
|
|
18
|
-
"
|
|
18
|
+
"publish:patch": "./scripts/publish.sh patch",
|
|
19
|
+
"publish:minor": "./scripts/publish.sh minor",
|
|
20
|
+
"publish:major": "./scripts/publish.sh major"
|
|
19
21
|
},
|
|
20
22
|
"repository": {
|
|
21
23
|
"type": "git",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# NOTE: npm now requires granular tokens with 2FA, max 90-day lifetime
|
|
5
|
+
# Classic tokens have been revoked. If auth fails, you'll need to:
|
|
6
|
+
# 1. Run `npm login` to create a new granular token
|
|
7
|
+
# 2. Complete 2FA verification
|
|
8
|
+
# See: https://gh.io/all-npm-classic-tokens-revoked
|
|
9
|
+
|
|
10
|
+
# Colors for output
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
YELLOW='\033[1;33m'
|
|
14
|
+
NC='\033[0m' # No Color
|
|
15
|
+
|
|
16
|
+
echo -e "${YELLOW}Checking npm authentication...${NC}"
|
|
17
|
+
|
|
18
|
+
# Check if logged in by trying to get current user
|
|
19
|
+
if ! npm whoami &>/dev/null; then
|
|
20
|
+
echo -e "${RED}Not logged in to npm or token expired.${NC}"
|
|
21
|
+
echo -e "${YELLOW}Please log in to npm:${NC}"
|
|
22
|
+
npm login
|
|
23
|
+
|
|
24
|
+
# Verify login succeeded
|
|
25
|
+
if ! npm whoami &>/dev/null; then
|
|
26
|
+
echo -e "${RED}Login failed. Aborting.${NC}"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
CURRENT_USER=$(npm whoami)
|
|
32
|
+
echo -e "${GREEN}Logged in as: ${CURRENT_USER}${NC}"
|
|
33
|
+
|
|
34
|
+
# Get the version bump type (default to patch)
|
|
35
|
+
BUMP_TYPE=${1:-patch}
|
|
36
|
+
|
|
37
|
+
if [[ ! "$BUMP_TYPE" =~ ^(patch|minor|major)$ ]]; then
|
|
38
|
+
echo -e "${RED}Invalid version bump type: ${BUMP_TYPE}${NC}"
|
|
39
|
+
echo "Usage: ./scripts/publish.sh [patch|minor|major]"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
echo -e "${YELLOW}Bumping version (${BUMP_TYPE})...${NC}"
|
|
44
|
+
|
|
45
|
+
# Build first to catch any errors before version bump
|
|
46
|
+
echo -e "${YELLOW}Building...${NC}"
|
|
47
|
+
yarn build
|
|
48
|
+
|
|
49
|
+
# Bump version (this updates package.json)
|
|
50
|
+
npm version $BUMP_TYPE --no-git-tag-version
|
|
51
|
+
|
|
52
|
+
# Get the new version
|
|
53
|
+
NEW_VERSION=$(node -p "require('./package.json').version")
|
|
54
|
+
echo -e "${GREEN}New version: ${NEW_VERSION}${NC}"
|
|
55
|
+
|
|
56
|
+
# Publish to npm
|
|
57
|
+
echo -e "${YELLOW}Publishing to npm...${NC}"
|
|
58
|
+
if npm publish; then
|
|
59
|
+
echo -e "${GREEN}Published successfully!${NC}"
|
|
60
|
+
|
|
61
|
+
# Git operations
|
|
62
|
+
echo -e "${YELLOW}Committing and tagging...${NC}"
|
|
63
|
+
cd ..
|
|
64
|
+
git add ui
|
|
65
|
+
git commit -m "v${NEW_VERSION}"
|
|
66
|
+
git tag "v${NEW_VERSION}"
|
|
67
|
+
git push
|
|
68
|
+
git push --tags
|
|
69
|
+
|
|
70
|
+
echo -e "${GREEN}Done! Published v${NEW_VERSION}${NC}"
|
|
71
|
+
else
|
|
72
|
+
echo -e "${RED}Publish failed!${NC}"
|
|
73
|
+
echo -e "${YELLOW}Rolling back version in package.json...${NC}"
|
|
74
|
+
git checkout package.json
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
@@ -78,14 +78,16 @@ import {
|
|
|
78
78
|
FaSolidPlay as PlayIcon,
|
|
79
79
|
FaSolidPlus as CreateIcon,
|
|
80
80
|
FaSolidStop as StopIcon,
|
|
81
|
-
FaSolidTrash as TrashIcon
|
|
81
|
+
FaSolidTrash as TrashIcon,
|
|
82
|
+
FaSolidUsers as UsersIcon,
|
|
83
|
+
FaSolidXmark as CloseIcon
|
|
82
84
|
} from "danx-icon";
|
|
83
85
|
import { computed, ref } from "vue";
|
|
84
86
|
import { ActionTarget, ResourceAction } from "../../../types";
|
|
85
87
|
|
|
86
88
|
export interface ActionButtonProps {
|
|
87
|
-
type?: "save" | "trash" | "back" | "create" | "edit" | "copy" | "folder" | "document" | "play" | "stop" | "pause" | "refresh" | "restart" | "confirm" | "cancel" | "export" | "import" | "minus" | "merge" | "check" | "clock" | "view" | "database";
|
|
88
|
-
color?: "red" | "blue" | "blue-invert" | "sky" | "sky-invert" | "green" | "green-invert" | "lime" | "white" | "gray" | "slate" | "slate-invert" | "yellow" | "orange" | "purple" | "teal" | "teal-invert";
|
|
89
|
+
type?: "save" | "trash" | "back" | "create" | "edit" | "copy" | "folder" | "document" | "play" | "stop" | "pause" | "refresh" | "restart" | "confirm" | "cancel" | "export" | "import" | "minus" | "merge" | "check" | "clock" | "view" | "database" | "users" | "close";
|
|
90
|
+
color?: "red" | "blue" | "blue-invert" | "sky" | "sky-invert" | "green" | "green-invert" | "lime" | "white" | "gray" | "slate" | "slate-invert" | "yellow" | "orange" | "amber" | "purple" | "teal" | "teal-invert";
|
|
89
91
|
size?: "xxs" | "xs" | "sm" | "md" | "lg";
|
|
90
92
|
icon?: object | string;
|
|
91
93
|
iconClass?: string;
|
|
@@ -179,6 +181,8 @@ const colorClass = computed(() => {
|
|
|
179
181
|
return "text-yellow-300 bg-yellow-800 hover:bg-yellow-700";
|
|
180
182
|
case "orange":
|
|
181
183
|
return "text-orange-400 bg-orange-900 hover:bg-orange-800";
|
|
184
|
+
case "amber":
|
|
185
|
+
return "text-amber-900 bg-amber-300 hover:bg-amber-400";
|
|
182
186
|
case "gray":
|
|
183
187
|
return "text-slate-200 bg-slate-800 hover:bg-slate-900";
|
|
184
188
|
case "slate":
|
|
@@ -242,6 +246,10 @@ const typeOptions = computed(() => {
|
|
|
242
246
|
return { icon: ViewIcon };
|
|
243
247
|
case "database":
|
|
244
248
|
return { icon: DatabaseIcon };
|
|
249
|
+
case "users":
|
|
250
|
+
return { icon: UsersIcon };
|
|
251
|
+
case "close":
|
|
252
|
+
return { icon: CloseIcon };
|
|
245
253
|
default:
|
|
246
254
|
return { icon: EditIcon };
|
|
247
255
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="dx-code-viewer group flex flex-col"
|
|
4
|
+
:class="{ 'is-collapsed': isCollapsed }"
|
|
5
|
+
>
|
|
6
|
+
<FieldLabel
|
|
7
|
+
v-if="label"
|
|
8
|
+
class="mb-2 text-sm flex-shrink-0"
|
|
9
|
+
:label="label"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
<!-- Collapsed view - inline preview -->
|
|
13
|
+
<CodeViewerCollapsed
|
|
14
|
+
v-if="collapsible && isCollapsed"
|
|
15
|
+
:preview="collapsedPreview"
|
|
16
|
+
:format="currentFormat"
|
|
17
|
+
:available-formats="currentFormat === 'json' || currentFormat === 'yaml' ? ['json', 'yaml'] : currentFormat === 'text' || currentFormat === 'markdown' ? ['text', 'markdown'] : []"
|
|
18
|
+
@expand="toggleCollapse"
|
|
19
|
+
@format-change="onFormatChange"
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
<!-- Expanded view - full code viewer -->
|
|
23
|
+
<template v-else>
|
|
24
|
+
<div class="code-wrapper relative flex flex-col flex-1 min-h-0">
|
|
25
|
+
<!-- Language badge - shows popout format options on hover -->
|
|
26
|
+
<LanguageBadge
|
|
27
|
+
:format="currentFormat"
|
|
28
|
+
:available-formats="currentFormat === 'json' || currentFormat === 'yaml' ? ['json', 'yaml'] : currentFormat === 'text' || currentFormat === 'markdown' ? ['text', 'markdown'] : []"
|
|
29
|
+
:toggleable="true"
|
|
30
|
+
@click.stop
|
|
31
|
+
@change="onFormatChange"
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<!-- Collapse button (when collapsible and expanded) -->
|
|
35
|
+
<div
|
|
36
|
+
v-if="collapsible"
|
|
37
|
+
class="collapse-toggle absolute top-0 left-0 p-1 cursor-pointer z-10 text-gray-500 hover:text-gray-300"
|
|
38
|
+
@click="toggleCollapse"
|
|
39
|
+
>
|
|
40
|
+
<CollapseIcon class="w-3 h-3" />
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Clickable header to collapse when expanded -->
|
|
44
|
+
<div
|
|
45
|
+
v-if="collapsible"
|
|
46
|
+
class="collapse-header"
|
|
47
|
+
@click="toggleCollapse"
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
<!-- Code display - readonly with syntax highlighting (non-markdown formats) -->
|
|
51
|
+
<pre
|
|
52
|
+
v-if="!editor.isEditing.value && currentFormat !== 'markdown'"
|
|
53
|
+
class="code-content dx-scrollbar flex-1 min-h-0"
|
|
54
|
+
:class="[editorClass, { 'is-collapsible': collapsible }]"
|
|
55
|
+
><code :class="'language-' + currentFormat" v-html="editor.highlightedContent.value"></code></pre>
|
|
56
|
+
|
|
57
|
+
<!-- Markdown display - rendered HTML -->
|
|
58
|
+
<MarkdownContent
|
|
59
|
+
v-else-if="currentFormat === 'markdown' && !editor.isEditing.value"
|
|
60
|
+
:content="markdownSource"
|
|
61
|
+
:default-code-format="defaultCodeFormat"
|
|
62
|
+
class="code-content dx-scrollbar flex-1 min-h-0"
|
|
63
|
+
:class="[editorClass, { 'is-collapsible': collapsible }]"
|
|
64
|
+
/>
|
|
65
|
+
|
|
66
|
+
<!-- Code editor - contenteditable, content set imperatively to avoid cursor reset -->
|
|
67
|
+
<pre
|
|
68
|
+
v-else
|
|
69
|
+
ref="codeRef"
|
|
70
|
+
class="code-content dx-scrollbar flex-1 min-h-0 is-editable"
|
|
71
|
+
:class="[editorClass, 'language-' + currentFormat, { 'is-collapsible': collapsible }]"
|
|
72
|
+
contenteditable="true"
|
|
73
|
+
@input="editor.onContentEditableInput"
|
|
74
|
+
@blur="editor.onContentEditableBlur"
|
|
75
|
+
@keydown="editor.onKeyDown"
|
|
76
|
+
></pre>
|
|
77
|
+
|
|
78
|
+
<!-- Footer with char count and edit toggle -->
|
|
79
|
+
<CodeViewerFooter
|
|
80
|
+
:char-count="editor.charCount.value"
|
|
81
|
+
:validation-error="editor.validationError.value"
|
|
82
|
+
:can-edit="canEdit && currentFormat !== 'markdown'"
|
|
83
|
+
:is-editing="editor.isEditing.value"
|
|
84
|
+
@toggle-edit="editor.toggleEdit"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import { FaSolidChevronDown as CollapseIcon } from "danx-icon";
|
|
93
|
+
import { computed, ref, toRef, watch } from "vue";
|
|
94
|
+
import { useCodeFormat, CodeFormat } from "../../../composables/useCodeFormat";
|
|
95
|
+
import { useCodeViewerCollapse } from "../../../composables/useCodeViewerCollapse";
|
|
96
|
+
import { useCodeViewerEditor } from "../../../composables/useCodeViewerEditor";
|
|
97
|
+
import FieldLabel from "../../ActionTable/Form/Fields/FieldLabel.vue";
|
|
98
|
+
import CodeViewerCollapsed from "./CodeViewerCollapsed.vue";
|
|
99
|
+
import CodeViewerFooter from "./CodeViewerFooter.vue";
|
|
100
|
+
import LanguageBadge from "./LanguageBadge.vue";
|
|
101
|
+
import MarkdownContent from "./MarkdownContent.vue";
|
|
102
|
+
|
|
103
|
+
export interface CodeViewerProps {
|
|
104
|
+
modelValue?: object | string | null;
|
|
105
|
+
format?: CodeFormat;
|
|
106
|
+
label?: string;
|
|
107
|
+
editorClass?: string;
|
|
108
|
+
canEdit?: boolean;
|
|
109
|
+
editable?: boolean;
|
|
110
|
+
collapsible?: boolean;
|
|
111
|
+
defaultCollapsed?: boolean;
|
|
112
|
+
defaultCodeFormat?: "json" | "yaml";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const props = withDefaults(defineProps<CodeViewerProps>(), {
|
|
116
|
+
modelValue: null,
|
|
117
|
+
format: "yaml",
|
|
118
|
+
label: "",
|
|
119
|
+
editorClass: "",
|
|
120
|
+
canEdit: false,
|
|
121
|
+
editable: false,
|
|
122
|
+
collapsible: false,
|
|
123
|
+
defaultCollapsed: true
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const emit = defineEmits<{
|
|
127
|
+
"update:modelValue": [value: object | string | null];
|
|
128
|
+
"update:format": [format: CodeFormat];
|
|
129
|
+
"update:editable": [editable: boolean];
|
|
130
|
+
}>();
|
|
131
|
+
|
|
132
|
+
// Initialize composable with current props
|
|
133
|
+
const codeFormat = useCodeFormat({
|
|
134
|
+
initialFormat: props.format,
|
|
135
|
+
initialValue: props.modelValue
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Local state
|
|
139
|
+
const currentFormat = ref<CodeFormat>(props.format);
|
|
140
|
+
const codeRef = ref<HTMLPreElement | null>(null);
|
|
141
|
+
|
|
142
|
+
// Collapsed state (for collapsible mode)
|
|
143
|
+
const isCollapsed = ref(props.collapsible && props.defaultCollapsed);
|
|
144
|
+
|
|
145
|
+
// Watch for changes to defaultCollapsed prop
|
|
146
|
+
watch(() => props.defaultCollapsed, (newValue) => {
|
|
147
|
+
if (props.collapsible) {
|
|
148
|
+
isCollapsed.value = newValue;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Toggle collapsed state
|
|
153
|
+
function toggleCollapse() {
|
|
154
|
+
isCollapsed.value = !isCollapsed.value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Sync composable format with current format
|
|
158
|
+
watch(currentFormat, (newFormat) => {
|
|
159
|
+
codeFormat.setFormat(newFormat);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Watch for external format changes
|
|
163
|
+
watch(() => props.format, (newFormat) => {
|
|
164
|
+
currentFormat.value = newFormat;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Watch for external value changes
|
|
168
|
+
watch(() => props.modelValue, () => {
|
|
169
|
+
codeFormat.setValue(props.modelValue);
|
|
170
|
+
editor.syncEditingContentFromValue();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Initialize editor composable
|
|
174
|
+
const editor = useCodeViewerEditor({
|
|
175
|
+
codeRef,
|
|
176
|
+
codeFormat,
|
|
177
|
+
currentFormat,
|
|
178
|
+
canEdit: toRef(props, "canEdit"),
|
|
179
|
+
editable: toRef(props, "editable"),
|
|
180
|
+
onEmitModelValue: (value) => emit("update:modelValue", value),
|
|
181
|
+
onEmitEditable: (editable) => emit("update:editable", editable)
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Sync internal editable state with prop
|
|
185
|
+
watch(() => props.editable, (newValue) => {
|
|
186
|
+
editor.syncEditableFromProp(newValue);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Initialize collapse composable
|
|
190
|
+
const { collapsedPreview } = useCodeViewerCollapse({
|
|
191
|
+
modelValue: toRef(props, "modelValue"),
|
|
192
|
+
format: currentFormat,
|
|
193
|
+
displayContent: editor.displayContent,
|
|
194
|
+
codeFormat
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Switch between JSON and YAML formats
|
|
198
|
+
function onFormatChange(newFormat: CodeFormat) {
|
|
199
|
+
currentFormat.value = newFormat;
|
|
200
|
+
emit("update:format", newFormat);
|
|
201
|
+
editor.updateEditingContentOnFormatChange();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Get the raw markdown content for MarkdownContent component
|
|
205
|
+
const markdownSource = computed(() => {
|
|
206
|
+
if (typeof props.modelValue === "string") {
|
|
207
|
+
return props.modelValue;
|
|
208
|
+
}
|
|
209
|
+
return editor.displayContent.value;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Expose isValid for external consumers
|
|
213
|
+
const isValid = computed(() => editor.isValid.value);
|
|
214
|
+
|
|
215
|
+
// Expose for parent components that may need to check validity
|
|
216
|
+
defineExpose({ isValid });
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<!-- Styles moved to global theme: src/styles/themes/danx/code.scss -->
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="code-collapsed relative flex items-center cursor-pointer"
|
|
4
|
+
@click="$emit('expand')"
|
|
5
|
+
>
|
|
6
|
+
<CollapseExpandIcon class="w-3 h-3 mr-2 flex-shrink-0 text-gray-500" />
|
|
7
|
+
<code class="code-collapsed-preview flex-1 min-w-0 truncate" v-html="preview" />
|
|
8
|
+
|
|
9
|
+
<!-- Language badge - stop propagation to prevent expand when clicking -->
|
|
10
|
+
<LanguageBadge
|
|
11
|
+
:format="format"
|
|
12
|
+
:available-formats="availableFormats"
|
|
13
|
+
:toggleable="availableFormats.length > 1"
|
|
14
|
+
@click.stop
|
|
15
|
+
@change="(fmt) => $emit('format-change', fmt)"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import { FaSolidChevronRight as CollapseExpandIcon } from "danx-icon";
|
|
22
|
+
import LanguageBadge from "./LanguageBadge.vue";
|
|
23
|
+
|
|
24
|
+
defineProps<{
|
|
25
|
+
preview: string;
|
|
26
|
+
format: string;
|
|
27
|
+
availableFormats?: string[];
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
defineEmits<{
|
|
31
|
+
expand: [];
|
|
32
|
+
"format-change": [format: string];
|
|
33
|
+
}>();
|
|
34
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="code-footer flex items-center justify-between px-2 py-1 flex-shrink-0"
|
|
4
|
+
:class="{ 'has-error': hasError }"
|
|
5
|
+
>
|
|
6
|
+
<div class="text-xs flex-1 min-w-0" :class="hasError ? 'text-red-400' : 'text-gray-500'">
|
|
7
|
+
<template v-if="validationError">
|
|
8
|
+
<span class="font-medium">
|
|
9
|
+
Error<template v-if="validationError.line"> (line {{ validationError.line }})</template>:
|
|
10
|
+
</span>
|
|
11
|
+
<span class="truncate">{{ validationError.message }}</span>
|
|
12
|
+
</template>
|
|
13
|
+
<template v-else>
|
|
14
|
+
{{ charCount.toLocaleString() }} chars
|
|
15
|
+
</template>
|
|
16
|
+
</div>
|
|
17
|
+
<!-- Edit toggle button -->
|
|
18
|
+
<QBtn
|
|
19
|
+
v-if="canEdit"
|
|
20
|
+
flat
|
|
21
|
+
dense
|
|
22
|
+
round
|
|
23
|
+
size="sm"
|
|
24
|
+
class="text-gray-500 hover:text-gray-700"
|
|
25
|
+
:class="{ 'text-sky-500 hover:text-sky-600': isEditing }"
|
|
26
|
+
@click="$emit('toggle-edit')"
|
|
27
|
+
>
|
|
28
|
+
<EditIcon class="w-3.5 h-3.5" />
|
|
29
|
+
<QTooltip>{{ isEditing ? 'Exit edit mode' : 'Edit content' }}</QTooltip>
|
|
30
|
+
</QBtn>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { FaSolidPencil as EditIcon } from "danx-icon";
|
|
36
|
+
import { computed } from "vue";
|
|
37
|
+
import { ValidationError } from "../../../composables/useCodeFormat";
|
|
38
|
+
|
|
39
|
+
export interface CodeViewerFooterProps {
|
|
40
|
+
charCount: number;
|
|
41
|
+
validationError: ValidationError | null;
|
|
42
|
+
canEdit: boolean;
|
|
43
|
+
isEditing: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const props = defineProps<CodeViewerFooterProps>();
|
|
47
|
+
|
|
48
|
+
defineEmits<{
|
|
49
|
+
"toggle-edit": [];
|
|
50
|
+
}>();
|
|
51
|
+
|
|
52
|
+
const hasError = computed(() => props.validationError !== null);
|
|
53
|
+
</script>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="dx-language-badge-container"
|
|
4
|
+
:class="{ 'is-toggleable': toggleable && availableFormats.length > 1 }"
|
|
5
|
+
@mouseenter="showOptions = true"
|
|
6
|
+
@mouseleave="showOptions = false"
|
|
7
|
+
>
|
|
8
|
+
<!-- Other format options (slide out to the left) -->
|
|
9
|
+
<transition name="slide-left">
|
|
10
|
+
<div
|
|
11
|
+
v-if="showOptions && toggleable && otherFormats.length > 0"
|
|
12
|
+
class="dx-language-options"
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
v-for="fmt in otherFormats"
|
|
16
|
+
:key="fmt"
|
|
17
|
+
class="dx-language-option"
|
|
18
|
+
@click.stop="$emit('change', fmt)"
|
|
19
|
+
>
|
|
20
|
+
{{ fmt.toUpperCase() }}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</transition>
|
|
24
|
+
|
|
25
|
+
<!-- Current format badge (stays in place) -->
|
|
26
|
+
<div class="dx-language-badge" :class="{ 'is-active': showOptions && otherFormats.length > 0 }">
|
|
27
|
+
{{ format.toUpperCase() }}
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
import { computed, ref } from "vue";
|
|
34
|
+
|
|
35
|
+
export interface LanguageBadgeProps {
|
|
36
|
+
format: string;
|
|
37
|
+
availableFormats?: string[];
|
|
38
|
+
toggleable?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const props = withDefaults(defineProps<LanguageBadgeProps>(), {
|
|
42
|
+
availableFormats: () => [],
|
|
43
|
+
toggleable: true
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
defineEmits<{
|
|
47
|
+
change: [format: string];
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
const showOptions = ref(false);
|
|
51
|
+
|
|
52
|
+
// Get formats other than the current one
|
|
53
|
+
const otherFormats = computed(() => {
|
|
54
|
+
return props.availableFormats.filter(f => f !== props.format);
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<style lang="scss">
|
|
59
|
+
.dx-language-badge-container {
|
|
60
|
+
position: absolute;
|
|
61
|
+
top: 0;
|
|
62
|
+
right: 0;
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
z-index: 10;
|
|
66
|
+
|
|
67
|
+
&.is-toggleable {
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.dx-language-options {
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.dx-language-option {
|
|
78
|
+
padding: 2px 8px;
|
|
79
|
+
font-size: 0.7em;
|
|
80
|
+
background: rgba(0, 0, 0, 0.5);
|
|
81
|
+
color: rgba(255, 255, 255, 0.7);
|
|
82
|
+
text-transform: uppercase;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
transition: all 0.2s;
|
|
85
|
+
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
|
86
|
+
|
|
87
|
+
&:hover {
|
|
88
|
+
background: rgba(0, 0, 0, 0.7);
|
|
89
|
+
color: rgba(255, 255, 255, 0.95);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&:first-child {
|
|
93
|
+
border-radius: 6px 0 0 6px;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.dx-language-badge {
|
|
98
|
+
padding: 2px 8px;
|
|
99
|
+
font-size: 0.7em;
|
|
100
|
+
border-radius: 0 6px 0 6px;
|
|
101
|
+
background: rgba(255, 255, 255, 0.1);
|
|
102
|
+
color: rgba(255, 255, 255, 0.6);
|
|
103
|
+
text-transform: uppercase;
|
|
104
|
+
transition: all 0.2s;
|
|
105
|
+
|
|
106
|
+
&.is-active {
|
|
107
|
+
border-radius: 0 6px 0 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Slide animation for options
|
|
112
|
+
.slide-left-enter-active,
|
|
113
|
+
.slide-left-leave-active {
|
|
114
|
+
transition: all 0.2s ease;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.slide-left-enter-from,
|
|
118
|
+
.slide-left-leave-to {
|
|
119
|
+
opacity: 0;
|
|
120
|
+
transform: translateX(10px);
|
|
121
|
+
}
|
|
122
|
+
</style>
|