quasar-ui-danx 0.5.1 → 0.5.2

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.
Files changed (33) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/dist/danx.es.js +11359 -10497
  3. package/dist/danx.es.js.map +1 -1
  4. package/dist/danx.umd.js +138 -131
  5. package/dist/danx.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +1 -1
  8. package/src/components/Utility/Buttons/ActionButton.vue +15 -5
  9. package/src/components/Utility/Code/CodeViewer.vue +10 -2
  10. package/src/components/Utility/Code/CodeViewerFooter.vue +2 -0
  11. package/src/components/Utility/Code/MarkdownContent.vue +31 -163
  12. package/src/components/Utility/Markdown/MarkdownEditor.vue +7 -2
  13. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +69 -8
  14. package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
  15. package/src/composables/markdown/features/useCodeBlocks.spec.ts +59 -33
  16. package/src/composables/markdown/features/useLinks.spec.ts +29 -10
  17. package/src/composables/markdown/useMarkdownEditor.ts +16 -7
  18. package/src/composables/useCodeFormat.ts +17 -10
  19. package/src/helpers/formats/highlightCSS.ts +236 -0
  20. package/src/helpers/formats/highlightHTML.ts +483 -0
  21. package/src/helpers/formats/highlightJavaScript.ts +346 -0
  22. package/src/helpers/formats/highlightSyntax.ts +15 -4
  23. package/src/helpers/formats/index.ts +3 -0
  24. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +15 -2
  25. package/src/helpers/formats/markdown/linePatterns.spec.ts +7 -4
  26. package/src/styles/danx.scss +3 -3
  27. package/src/styles/index.scss +5 -5
  28. package/src/styles/themes/danx/code.scss +257 -1
  29. package/src/styles/themes/danx/index.scss +10 -10
  30. package/src/styles/themes/danx/markdown.scss +59 -0
  31. package/src/test/highlighters.test.ts +153 -0
  32. package/src/types/widgets.d.ts +2 -2
  33. package/vite.config.js +5 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -8,14 +8,14 @@
8
8
  >
9
9
  <div class="flex items-center flex-nowrap">
10
10
  <component
11
+ v-if="icon || typeOptions.icon"
11
12
  :is="icon || typeOptions.icon"
12
13
  class="transition-all"
13
14
  :class="resolvedIconClass"
14
15
  />
15
16
  <div
16
17
  v-if="label || label === 0"
17
- class="ml-2"
18
- :class="labelClass"
18
+ :class="[labelClass, { 'ml-2': icon || typeOptions.icon }]"
19
19
  >
20
20
  {{ label }}
21
21
  </div>
@@ -87,7 +87,7 @@ import { ActionTarget, ResourceAction } from "../../../types";
87
87
 
88
88
  export interface ActionButtonProps {
89
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";
90
+ color?: "red" | "blue" | "blue-invert" | "sky" | "sky-invert" | "sky-soft" | "green" | "green-invert" | "green-soft" | "lime" | "white" | "gray" | "slate" | "slate-invert" | "slate-soft" | "yellow" | "orange" | "amber" | "purple" | "blue-soft" | "red-soft" | "teal" | "teal-invert";
91
91
  size?: "xxs" | "xs" | "sm" | "md" | "lg";
92
92
  icon?: object | string;
93
93
  iconClass?: string;
@@ -126,7 +126,7 @@ const props = withDefaults(defineProps<ActionButtonProps>(), {
126
126
  const mappedSizeClass = {
127
127
  xxs: {
128
128
  icon: "w-2",
129
- button: "px-.5 h-5"
129
+ button: "px-1 h-4 text-[10px]"
130
130
  },
131
131
  xs: {
132
132
  icon: "w-3",
@@ -157,16 +157,22 @@ const colorClass = computed(() => {
157
157
  switch (props.color) {
158
158
  case "red":
159
159
  return "text-red-900 bg-red-300 hover:bg-red-400";
160
+ case "red-soft":
161
+ return "text-red-700 bg-red-100 hover:bg-red-200";
160
162
  case "lime":
161
163
  return "text-lime-900 bg-lime-300 hover:bg-lime-400";
162
164
  case "green":
163
165
  return "text-green-900 bg-green-300 hover:bg-green-400";
164
166
  case "green-invert":
165
167
  return "text-green-300 bg-green-900 hover:bg-green-800";
168
+ case "green-soft":
169
+ return "text-green-700 bg-green-100 hover:bg-green-200";
166
170
  case "blue":
167
171
  return "text-blue-900 bg-blue-300 hover:bg-blue-400";
168
172
  case "blue-invert":
169
173
  return "text-blue-300 bg-blue-900 hover:bg-blue-800";
174
+ case "blue-soft":
175
+ return "text-blue-700 bg-blue-100 hover:bg-blue-200";
170
176
  case "teal":
171
177
  return "text-teal-800 bg-teal-200 hover:bg-teal-400";
172
178
  case "teal-invert":
@@ -175,6 +181,8 @@ const colorClass = computed(() => {
175
181
  return "text-sky-900 bg-sky-300 hover:bg-sky-400";
176
182
  case "sky-invert":
177
183
  return "text-sky-400 bg-sky-800 hover:bg-sky-900";
184
+ case "sky-soft":
185
+ return "text-sky-700 bg-sky-100 hover:bg-sky-200";
178
186
  case "white":
179
187
  return "text-white bg-gray-800 hover:bg-gray-200";
180
188
  case "yellow":
@@ -189,6 +197,8 @@ const colorClass = computed(() => {
189
197
  return "text-slate-900 bg-slate-300 hover:bg-slate-400";
190
198
  case "slate-invert":
191
199
  return "text-slate-300 bg-slate-900 hover:bg-slate-800";
200
+ case "slate-soft":
201
+ return "text-slate-600 bg-slate-100 hover:bg-slate-200";
192
202
  case "purple":
193
203
  return "text-purple-300 bg-purple-900 hover:bg-purple-800";
194
204
  default:
@@ -251,7 +261,7 @@ const typeOptions = computed(() => {
251
261
  case "close":
252
262
  return { icon: CloseIcon };
253
263
  default:
254
- return { icon: EditIcon };
264
+ return { icon: null };
255
265
  }
256
266
  });
257
267
 
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div
3
3
  class="dx-code-viewer group flex flex-col"
4
- :class="{ 'is-collapsed': isCollapsed }"
4
+ :class="[{ 'is-collapsed': isCollapsed }, props.theme === 'light' ? 'theme-light' : '']"
5
5
  >
6
6
  <FieldLabel
7
7
  v-if="label"
@@ -84,10 +84,12 @@
84
84
 
85
85
  <!-- Footer with char count and edit toggle -->
86
86
  <CodeViewerFooter
87
+ v-if="!hideFooter"
87
88
  :char-count="editor.charCount.value"
88
89
  :validation-error="editor.validationError.value"
89
90
  :can-edit="canEdit && currentFormat !== 'markdown'"
90
91
  :is-editing="editor.isEditing.value"
92
+ :show-version="showVersion"
91
93
  @toggle-edit="editor.toggleEdit"
92
94
  />
93
95
  </div>
@@ -118,6 +120,9 @@ export interface CodeViewerProps {
118
120
  defaultCollapsed?: boolean;
119
121
  defaultCodeFormat?: "json" | "yaml";
120
122
  allowAnyLanguage?: boolean;
123
+ theme?: "dark" | "light";
124
+ showVersion?: boolean;
125
+ hideFooter?: boolean;
121
126
  }
122
127
 
123
128
  const props = withDefaults(defineProps<CodeViewerProps>(), {
@@ -128,7 +133,10 @@ const props = withDefaults(defineProps<CodeViewerProps>(), {
128
133
  canEdit: false,
129
134
  editable: false,
130
135
  collapsible: false,
131
- defaultCollapsed: true
136
+ defaultCollapsed: true,
137
+ theme: "dark",
138
+ showVersion: false,
139
+ hideFooter: false
132
140
  });
133
141
 
134
142
  const emit = defineEmits<{
@@ -12,6 +12,7 @@
12
12
  </template>
13
13
  <template v-else>
14
14
  {{ charCount.toLocaleString() }} chars
15
+ <span v-if="showVersion" class="ml-2 text-sky-400">[v1.0.5]</span>
15
16
  </template>
16
17
  </div>
17
18
  <!-- Edit toggle button -->
@@ -41,6 +42,7 @@ export interface CodeViewerFooterProps {
41
42
  validationError: ValidationError | null;
42
43
  canEdit: boolean;
43
44
  isEditing: boolean;
45
+ showVersion?: boolean;
44
46
  }
45
47
 
46
48
  const props = defineProps<CodeViewerFooterProps>();
@@ -9,29 +9,17 @@
9
9
  />
10
10
 
11
11
  <!-- Code blocks with syntax highlighting -->
12
- <div
12
+ <CodeViewer
13
13
  v-else-if="token.type === 'code_block'"
14
- class="dx-markdown-code-block"
15
- >
16
- <!-- Language toggle badge (only for json/yaml) -->
17
- <LanguageBadge
18
- v-if="isToggleableLanguage(token.language)"
19
- :format="getCodeBlockFormat(index, token.language)"
20
- :available-formats="['json', 'yaml']"
21
- :toggleable="true"
22
- @change="(fmt) => setCodeBlockFormat(index, fmt)"
23
- />
24
- <LanguageBadge
25
- v-else-if="token.language"
26
- :format="token.language"
27
- :available-formats="[]"
28
- :toggleable="false"
29
- />
30
- <pre><code
31
- :class="'language-' + getCodeBlockFormat(index, token.language)"
32
- v-html="highlightCodeBlock(index, token.content, token.language)"
33
- ></code></pre>
34
- </div>
14
+ :model-value="token.content"
15
+ :format="normalizeLanguage(token.language)"
16
+ :default-code-format="defaultCodeFormat"
17
+ :can-edit="false"
18
+ :collapsible="false"
19
+ hide-footer
20
+ allow-any-language
21
+ class="markdown-code-block"
22
+ />
35
23
 
36
24
  <!-- Blockquotes (recursive) -->
37
25
  <blockquote
@@ -186,12 +174,10 @@
186
174
  </template>
187
175
 
188
176
  <script setup lang="ts">
189
- import { computed, reactive } from "vue";
190
- import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
177
+ import { computed } from "vue";
191
178
  import { tokenizeBlocks, parseInline, renderMarkdown, getFootnotes, resetParserState } from "../../../helpers/formats/markdown";
192
179
  import type { BlockToken, ListItem } from "../../../helpers/formats/markdown";
193
- import { highlightJSON, highlightYAML } from "../../../helpers/formats/highlightSyntax";
194
- import LanguageBadge from "./LanguageBadge.vue";
180
+ import CodeViewer from "./CodeViewer.vue";
195
181
 
196
182
  export interface MarkdownContentProps {
197
183
  content: string;
@@ -202,10 +188,21 @@ const props = withDefaults(defineProps<MarkdownContentProps>(), {
202
188
  content: ""
203
189
  });
204
190
 
205
- // Track format overrides for each code block (for toggling json<->yaml)
206
- const codeBlockFormats = reactive<Record<number, string>>({});
207
- // Cache converted content for each code block
208
- const convertedContent = reactive<Record<number, string>>({});
191
+ // Normalize language aliases to standard names
192
+ function normalizeLanguage(lang?: string): string {
193
+ if (!lang) return "text";
194
+ const aliases: Record<string, string> = {
195
+ js: "javascript",
196
+ ts: "typescript",
197
+ py: "python",
198
+ rb: "ruby",
199
+ yml: "yaml",
200
+ md: "markdown",
201
+ sh: "bash",
202
+ shell: "bash"
203
+ };
204
+ return aliases[lang.toLowerCase()] || lang.toLowerCase();
205
+ }
209
206
 
210
207
  // Tokenize the markdown content
211
208
  const tokens = computed<BlockToken[]>(() => {
@@ -234,124 +231,6 @@ const sortedFootnotes = computed(() => {
234
231
  .map(([id, fn]) => ({ id, content: fn.content, index: fn.index }));
235
232
  });
236
233
 
237
- // Check if a language is toggleable (json or yaml)
238
- function isToggleableLanguage(language: string): boolean {
239
- if (!language) return false;
240
- const lang = language.toLowerCase();
241
- return lang === "json" || lang === "yaml";
242
- }
243
-
244
- // Get the current format for a code block (respecting user toggle, then default override, then original)
245
- function getCodeBlockFormat(index: number, originalLanguage: string): string {
246
- // If user has toggled this block, use their choice
247
- if (codeBlockFormats[index]) {
248
- return codeBlockFormats[index];
249
- }
250
-
251
- // If a default is set and this is a toggleable language, use the default
252
- const lang = originalLanguage?.toLowerCase();
253
- if (props.defaultCodeFormat && (lang === "json" || lang === "yaml")) {
254
- return props.defaultCodeFormat;
255
- }
256
-
257
- // Otherwise use the original language
258
- return lang || "text";
259
- }
260
-
261
- // Get converted content for a code block (handles initial conversion for defaultCodeFormat)
262
- function getConvertedContent(index: number, originalContent: string, originalLang: string): string {
263
- const format = getCodeBlockFormat(index, originalLang);
264
-
265
- // If format matches original, no conversion needed
266
- if (format === originalLang || !isToggleableLanguage(originalLang)) {
267
- return originalContent;
268
- }
269
-
270
- // Convert from original to target format
271
- try {
272
- let parsed: unknown;
273
-
274
- if (originalLang === "json") {
275
- parsed = JSON.parse(originalContent);
276
- } else if (originalLang === "yaml") {
277
- parsed = parseYAML(originalContent);
278
- }
279
-
280
- if (parsed !== undefined) {
281
- if (format === "json") {
282
- return JSON.stringify(parsed, null, 2);
283
- } else if (format === "yaml") {
284
- return stringifyYAML(parsed as object);
285
- }
286
- }
287
- } catch {
288
- // Conversion failed, return original
289
- }
290
-
291
- return originalContent;
292
- }
293
-
294
- // Set format for a code block (converts content to the new format)
295
- function setCodeBlockFormat(index: number, newFormat: string) {
296
- const token = tokens.value[index];
297
- if (token?.type !== "code_block") return;
298
-
299
- const originalLang = token.language?.toLowerCase() || "json";
300
- const current = getCodeBlockFormat(index, originalLang);
301
-
302
- // No change needed if already in target format
303
- if (current === newFormat) return;
304
-
305
- // Convert the content
306
- try {
307
- // Use the currently displayed content (which may already be converted due to defaultCodeFormat)
308
- const sourceContent = convertedContent[index] || getConvertedContent(index, token.content, originalLang);
309
- let parsed: unknown;
310
-
311
- // Parse from current format
312
- if (current === "json") {
313
- parsed = JSON.parse(sourceContent);
314
- } else {
315
- parsed = parseYAML(sourceContent);
316
- }
317
-
318
- // Convert to new format
319
- if (newFormat === "json") {
320
- convertedContent[index] = JSON.stringify(parsed, null, 2);
321
- } else {
322
- convertedContent[index] = stringifyYAML(parsed as object);
323
- }
324
-
325
- codeBlockFormats[index] = newFormat;
326
- } catch {
327
- // If conversion fails, just set the format without converting
328
- codeBlockFormats[index] = newFormat;
329
- }
330
- }
331
-
332
- // Highlight code block content based on format
333
- function highlightCodeBlock(index: number, originalContent: string, originalLanguage: string): string {
334
- const format = getCodeBlockFormat(index, originalLanguage);
335
- const originalLang = originalLanguage?.toLowerCase() || "text";
336
-
337
- // Get the content (converted if needed, or from cache if user toggled)
338
- const content = convertedContent[index] || getConvertedContent(index, originalContent, originalLang);
339
-
340
- // Apply syntax highlighting
341
- switch (format) {
342
- case "json":
343
- return highlightJSON(content);
344
- case "yaml":
345
- return highlightYAML(content);
346
- default:
347
- // For other languages, just escape HTML
348
- return content
349
- .replace(/&/g, "&amp;")
350
- .replace(/</g, "&lt;")
351
- .replace(/>/g, "&gt;");
352
- }
353
- }
354
-
355
234
  // Parse inline markdown (bold, italic, links, etc.)
356
235
  function parseInlineContent(text: string): string {
357
236
  return parseInline(text, true);
@@ -383,23 +262,12 @@ function renderBlockquote(content: string): string {
383
262
  </script>
384
263
 
385
264
  <style lang="scss">
386
- .dx-markdown-code-block {
387
- position: relative;
265
+ .markdown-code-block {
388
266
  margin: 1em 0;
389
267
 
390
- pre {
391
- margin: 0;
392
- background: rgba(0, 0, 0, 0.3);
393
- padding: 1em;
394
- border-radius: 6px;
395
- overflow-x: auto;
396
-
397
- code {
398
- background: transparent;
399
- padding: 0;
400
- font-size: 0.875em;
401
- font-family: 'Fira Code', 'Monaco', monospace;
402
- }
268
+ // Ensure auto-height instead of 100%
269
+ &.dx-code-viewer {
270
+ height: auto;
403
271
  }
404
272
  }
405
273
  </style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="dx-markdown-editor" :class="{ 'is-readonly': readonly }">
2
+ <div class="dx-markdown-editor" :class="[{ 'is-readonly': readonly }, props.theme === 'light' ? 'theme-light' : '']">
3
3
  <div class="dx-markdown-editor-body" @contextmenu="contextMenu.show">
4
4
  <!-- Floating line type menu positioned next to current block -->
5
5
  <div
@@ -25,6 +25,7 @@
25
25
  </div>
26
26
 
27
27
  <MarkdownEditorFooter
28
+ v-if="!hideFooter"
28
29
  :char-count="editor.charCount.value"
29
30
  @show-hotkeys="editor.showHotkeyHelp"
30
31
  />
@@ -81,6 +82,8 @@ export interface MarkdownEditorProps {
81
82
  readonly?: boolean;
82
83
  minHeight?: string;
83
84
  maxHeight?: string;
85
+ theme?: "dark" | "light";
86
+ hideFooter?: boolean;
84
87
  }
85
88
 
86
89
  const props = withDefaults(defineProps<MarkdownEditorProps>(), {
@@ -88,7 +91,9 @@ const props = withDefaults(defineProps<MarkdownEditorProps>(), {
88
91
  placeholder: "Start typing...",
89
92
  readonly: false,
90
93
  minHeight: "100px",
91
- maxHeight: "none"
94
+ maxHeight: "none",
95
+ theme: "dark",
96
+ hideFooter: false
92
97
  });
93
98
 
94
99
  const emit = defineEmits<{
@@ -2,10 +2,10 @@
2
2
  <div
3
3
  ref="containerRef"
4
4
  class="dx-markdown-editor-content dx-markdown-content"
5
- :class="{ 'is-readonly': readonly, 'is-empty': isEmpty }"
5
+ :class="{ 'is-readonly': readonly, 'is-empty': isContentEmpty }"
6
6
  :contenteditable="!readonly"
7
7
  :data-placeholder="placeholder"
8
- @input="$emit('input')"
8
+ @input="onInput"
9
9
  @keydown="$emit('keydown', $event)"
10
10
  @blur="$emit('blur')"
11
11
  @click="handleClick"
@@ -14,7 +14,7 @@
14
14
  </template>
15
15
 
16
16
  <script setup lang="ts">
17
- import { computed, ref } from "vue";
17
+ import { nextTick, ref, watch } from "vue";
18
18
 
19
19
  export interface MarkdownEditorContentProps {
20
20
  html: string;
@@ -27,13 +27,38 @@ const props = withDefaults(defineProps<MarkdownEditorContentProps>(), {
27
27
  placeholder: "Start typing..."
28
28
  });
29
29
 
30
- defineEmits<{
30
+ const emit = defineEmits<{
31
31
  input: [];
32
32
  keydown: [event: KeyboardEvent];
33
33
  blur: [];
34
34
  }>();
35
35
 
36
36
  const containerRef = ref<HTMLElement | null>(null);
37
+ const isContentEmpty = ref(true);
38
+
39
+ /**
40
+ * Check if the editor content is empty by examining the actual DOM text content.
41
+ * This is needed because contenteditable changes the DOM directly without updating props.
42
+ */
43
+ function checkIfEmpty(): void {
44
+ if (containerRef.value) {
45
+ const textContent = containerRef.value.textContent?.trim() || "";
46
+ isContentEmpty.value = textContent.length === 0;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Handle input events - check if content is empty and emit the input event.
52
+ */
53
+ function onInput(): void {
54
+ checkIfEmpty();
55
+ emit("input");
56
+ }
57
+
58
+ // Watch for external HTML changes (e.g., from parent component)
59
+ watch(() => props.html, () => {
60
+ nextTick(() => checkIfEmpty());
61
+ }, { immediate: true });
37
62
 
38
63
  /**
39
64
  * Find the anchor element if the click target is inside one
@@ -74,10 +99,6 @@ function handleClick(event: MouseEvent): void {
74
99
  window.open(href, "_blank", "noopener,noreferrer");
75
100
  }
76
101
 
77
- const isEmpty = computed(() => {
78
- return !props.html || props.html === "<p></p>" || props.html === "<p><br></p>";
79
- });
80
-
81
102
  // Expose containerRef for parent component
82
103
  defineExpose({ containerRef });
83
104
  </script>
@@ -231,5 +252,45 @@ defineExpose({ containerRef });
231
252
  }
232
253
  }
233
254
  }
255
+
256
+ // ==========================================
257
+ // LIGHT THEME VARIANT
258
+ // ==========================================
259
+ .dx-markdown-editor.theme-light & {
260
+ background-color: #f8fafc;
261
+ color: #1e293b;
262
+ caret-color: #1e293b;
263
+
264
+ &:focus {
265
+ border-color: rgba(14, 165, 233, 0.6);
266
+ }
267
+
268
+ &:hover:not(:focus):not(.is-readonly) {
269
+ border-color: rgba(14, 165, 233, 0.3);
270
+ }
271
+
272
+ // Placeholder styling - light theme
273
+ &.is-empty::before {
274
+ color: #94a3b8;
275
+ }
276
+
277
+ // Link tooltip - light theme
278
+ a:hover::after {
279
+ background: #e2e8f0;
280
+ color: #1e293b;
281
+ }
282
+
283
+ // Code block wrapper - light theme
284
+ .code-block-wrapper {
285
+ background: #f1f5f9;
286
+ border-color: #e2e8f0;
287
+
288
+ .dx-code-viewer {
289
+ .code-footer {
290
+ background: #e2e8f0;
291
+ }
292
+ }
293
+ }
294
+ }
234
295
  }
235
296
  </style>
@@ -14,6 +14,7 @@ const props = withDefaults(defineProps<LabelPillWidgetProps>(), {
14
14
  });
15
15
 
16
16
  const colorClasses = {
17
+ // Dark theme colors (dark backgrounds with bright text)
17
18
  sky: "bg-sky-950 text-sky-400",
18
19
  green: "bg-green-950 text-green-400",
19
20
  red: "bg-red-950 text-red-400",
@@ -33,6 +34,25 @@ const colorClasses = {
33
34
  indigo: "bg-indigo-950 text-indigo-400",
34
35
  violet: "bg-violet-950 text-violet-400",
35
36
  fuchsia: "bg-fuchsia-950 text-fuchsia-400",
37
+ // Soft/light theme colors (light backgrounds with darker text)
38
+ "sky-soft": "bg-sky-100 text-sky-700",
39
+ "green-soft": "bg-green-100 text-green-700",
40
+ "red-soft": "bg-red-100 text-red-700",
41
+ "amber-soft": "bg-amber-100 text-amber-700",
42
+ "yellow-soft": "bg-yellow-100 text-yellow-700",
43
+ "blue-soft": "bg-blue-100 text-blue-700",
44
+ "purple-soft": "bg-purple-100 text-purple-700",
45
+ "slate-soft": "bg-slate-100 text-slate-600",
46
+ "gray-soft": "bg-gray-100 text-gray-600",
47
+ "emerald-soft": "bg-emerald-100 text-emerald-700",
48
+ "orange-soft": "bg-orange-100 text-orange-700",
49
+ "lime-soft": "bg-lime-100 text-lime-700",
50
+ "teal-soft": "bg-teal-100 text-teal-700",
51
+ "cyan-soft": "bg-cyan-100 text-cyan-700",
52
+ "rose-soft": "bg-rose-100 text-rose-700",
53
+ "indigo-soft": "bg-indigo-100 text-indigo-700",
54
+ "violet-soft": "bg-violet-100 text-violet-700",
55
+ "fuchsia-soft": "bg-fuchsia-100 text-fuchsia-700",
36
56
  none: ""
37
57
  };
38
58