@alliance-droid/status-feedback-system 2.3.2 → 3.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.
@@ -1,123 +1,123 @@
1
- <script lang="ts">
2
- import { Card, Button, Input, Textarea, Select } from '@alliance-droid/svelte-component-library';
3
- import type { FeedbackBoardState } from './state.svelte.js';
4
- import PasteImageInput from '../shared/PasteImageInput.svelte';
5
-
6
- interface Props {
7
- board: FeedbackBoardState;
8
- /** Available category options for the dropdown */
9
- categories?: string[];
10
- }
11
-
12
- let { board, categories = [] }: Props = $props();
13
-
14
- let title = $state('');
15
- let message = $state('');
16
- let category = $state('');
17
- let submitting = $state(false);
18
- let success = $state(false);
19
- let error = $state('');
20
- let pastedImage = $state<string | null>(null);
21
- let pastedFilename = $state('');
22
- let textareaWrapperEl: HTMLDivElement | undefined = $state();
23
- let textareaEl: HTMLTextAreaElement | undefined = $state();
24
-
25
- $effect(() => {
26
- if (textareaWrapperEl) {
27
- textareaEl = textareaWrapperEl.querySelector('textarea') ?? undefined;
28
- }
29
- });
30
-
31
- let isAuthenticated = $derived(!!board.userEmail);
32
-
33
- let categoryOptions = $derived(
34
- categories.map((c) => ({ value: c, label: board.getCategoryLabel(c) }))
35
- );
36
-
37
- async function handleSubmit() {
38
- if (!message.trim() || !isAuthenticated) return;
39
- submitting = true;
40
- error = '';
41
- try {
42
- await board.submitFeedback({
43
- title: title.trim() || undefined,
44
- message: message.trim(),
45
- category: category || undefined,
46
- pastedImage,
47
- });
48
- success = true;
49
- title = '';
50
- message = '';
51
- category = '';
52
- pastedImage = null;
53
- pastedFilename = '';
54
- setTimeout(() => { success = false; }, 2000);
55
- } catch (err) {
56
- error = err instanceof Error ? err.message : 'Failed to submit';
57
- } finally {
58
- submitting = false;
59
- }
60
- }
61
- </script>
62
-
63
- <Card variant="outlined" padding="md">
64
- {#if !isAuthenticated}
65
- <div class="text-center py-3 text-foreground-tertiary">
66
- <i class="fa-solid fa-lock mr-1" aria-hidden="true"></i>
67
- <span class="text-sm">Sign in to submit feedback</span>
68
- </div>
69
- {:else if success}
70
- <div class="text-center py-2 text-green-400 font-semibold">
71
- <i class="fa-solid fa-check-circle mr-1"></i> Submitted!
72
- </div>
73
- {:else}
74
- <div class="space-y-3">
75
- <div class="flex items-center gap-1.5 text-xs text-foreground-tertiary">
76
- <i class="fa-solid fa-user" aria-hidden="true"></i>
77
- <span>Submitting as <span class="font-medium text-foreground-secondary">{board.userEmail}</span></span>
78
- </div>
79
- <Input
80
- bind:value={title}
81
- placeholder="Title (optional)"
82
- />
83
- <div bind:this={textareaWrapperEl}>
84
- <Textarea
85
- value={message}
86
- oninput={(e) => (message = (e.currentTarget as HTMLTextAreaElement).value)}
87
- placeholder="Describe your feedback (paste an image to attach it)\u2026"
88
- rows={3}
89
- />
90
- </div>
91
- <PasteImageInput
92
- {textareaEl}
93
- bind:image={pastedImage}
94
- bind:filename={pastedFilename}
95
- />
96
- {#if error}
97
- <p class="text-sm text-red-400">
98
- <i class="fa-solid fa-exclamation-circle mr-1"></i>
99
- {error}
100
- </p>
101
- {/if}
102
- <div class="flex gap-2 items-center flex-wrap">
103
- {#if categoryOptions.length > 0}
104
- <div class="min-w-[140px]">
105
- <Select
106
- options={[{ value: '', label: 'Category...' }, ...categoryOptions]}
107
- bind:value={category}
108
- />
109
- </div>
110
- {/if}
111
- <div class="flex-1"></div>
112
- <Button
113
- variant="primary"
114
- disabled={submitting || !message.trim()}
115
- loading={submitting}
116
- onclick={handleSubmit}
117
- >
118
- Submit
119
- </Button>
120
- </div>
121
- </div>
122
- {/if}
123
- </Card>
1
+ <script lang="ts">
2
+ import { Card, Button, Input, Textarea, Select } from '@alliance-droid/svelte-component-library';
3
+ import type { FeedbackBoardState } from './state.svelte.js';
4
+ import PasteImageInput from '../shared/PasteImageInput.svelte';
5
+
6
+ interface Props {
7
+ board: FeedbackBoardState;
8
+ /** Available category options for the dropdown */
9
+ categories?: string[];
10
+ }
11
+
12
+ let { board, categories = [] }: Props = $props();
13
+
14
+ let title = $state('');
15
+ let message = $state('');
16
+ let category = $state('');
17
+ let submitting = $state(false);
18
+ let success = $state(false);
19
+ let error = $state('');
20
+ let pastedImage = $state<string | null>(null);
21
+ let pastedFilename = $state('');
22
+ let textareaWrapperEl: HTMLDivElement | undefined = $state();
23
+ let textareaEl: HTMLTextAreaElement | undefined = $state();
24
+
25
+ $effect(() => {
26
+ if (textareaWrapperEl) {
27
+ textareaEl = textareaWrapperEl.querySelector('textarea') ?? undefined;
28
+ }
29
+ });
30
+
31
+ let isAuthenticated = $derived(!!board.userEmail);
32
+
33
+ let categoryOptions = $derived(
34
+ categories.map((c) => ({ value: c, label: board.getCategoryLabel(c) }))
35
+ );
36
+
37
+ async function handleSubmit() {
38
+ if (!message.trim() || !isAuthenticated) return;
39
+ submitting = true;
40
+ error = '';
41
+ try {
42
+ await board.submitFeedback({
43
+ title: title.trim() || undefined,
44
+ message: message.trim(),
45
+ category: category || undefined,
46
+ pastedImage,
47
+ });
48
+ success = true;
49
+ title = '';
50
+ message = '';
51
+ category = '';
52
+ pastedImage = null;
53
+ pastedFilename = '';
54
+ setTimeout(() => { success = false; }, 2000);
55
+ } catch (err) {
56
+ error = err instanceof Error ? err.message : 'Failed to submit';
57
+ } finally {
58
+ submitting = false;
59
+ }
60
+ }
61
+ </script>
62
+
63
+ <Card variant="outlined" padding="md">
64
+ {#if !isAuthenticated}
65
+ <div class="text-center py-3 text-foreground-tertiary">
66
+ <i class="fa-solid fa-lock mr-1" aria-hidden="true"></i>
67
+ <span class="text-sm">Sign in to submit feedback</span>
68
+ </div>
69
+ {:else if success}
70
+ <div class="text-center py-2 text-green-400 font-semibold">
71
+ <i class="fa-solid fa-check-circle mr-1"></i> Submitted!
72
+ </div>
73
+ {:else}
74
+ <div class="space-y-3">
75
+ <div class="flex items-center gap-1.5 text-xs text-foreground-tertiary">
76
+ <i class="fa-solid fa-user" aria-hidden="true"></i>
77
+ <span>Submitting as <span class="font-medium text-foreground-secondary">{board.userEmail}</span></span>
78
+ </div>
79
+ <Input
80
+ bind:value={title}
81
+ placeholder="Title (optional)"
82
+ />
83
+ <div bind:this={textareaWrapperEl}>
84
+ <Textarea
85
+ value={message}
86
+ oninput={(e) => (message = (e.currentTarget as HTMLTextAreaElement).value)}
87
+ placeholder="Describe your feedback (paste an image to attach it)\u2026"
88
+ rows={3}
89
+ />
90
+ </div>
91
+ <PasteImageInput
92
+ {textareaEl}
93
+ bind:image={pastedImage}
94
+ bind:filename={pastedFilename}
95
+ />
96
+ {#if error}
97
+ <p class="text-sm text-red-400">
98
+ <i class="fa-solid fa-exclamation-circle mr-1"></i>
99
+ {error}
100
+ </p>
101
+ {/if}
102
+ <div class="flex gap-2 items-center flex-wrap">
103
+ {#if categoryOptions.length > 0}
104
+ <div class="min-w-[140px]">
105
+ <Select
106
+ options={[{ value: '', label: 'Category...' }, ...categoryOptions]}
107
+ bind:value={category}
108
+ />
109
+ </div>
110
+ {/if}
111
+ <div class="flex-1"></div>
112
+ <Button
113
+ variant="primary"
114
+ disabled={submitting || !message.trim()}
115
+ loading={submitting}
116
+ onclick={handleSubmit}
117
+ >
118
+ Submit
119
+ </Button>
120
+ </div>
121
+ </div>
122
+ {/if}
123
+ </Card>
@@ -1,4 +1,7 @@
1
1
  /** Relative time formatting for feedback timestamps */
2
2
  export declare function timeAgo(timestamp: number): string;
3
- /** Map feedback status to badge variant */
4
- export declare function getStatusBadgeVariant(status: string): 'success' | 'neutral' | 'info';
3
+ import type { PublicStatus } from '../../types.js';
4
+ /** Human label for a public board status. */
5
+ export declare function publicStatusLabel(status: PublicStatus): string;
6
+ /** Map a public board status to a badge variant. */
7
+ export declare function getStatusBadgeVariant(status: PublicStatus): 'success' | 'neutral' | 'info' | 'warning';
@@ -12,11 +12,25 @@ export function timeAgo(timestamp) {
12
12
  return `${days}d ago`;
13
13
  return new Date(timestamp).toLocaleDateString();
14
14
  }
15
- /** Map feedback status to badge variant */
15
+ const PUBLIC_STATUS_LABELS = {
16
+ open: 'Open',
17
+ planned: 'Planned',
18
+ in_progress: 'In progress',
19
+ done: 'Done',
20
+ declined: 'Declined'
21
+ };
22
+ const PUBLIC_STATUS_VARIANTS = {
23
+ open: 'info',
24
+ planned: 'info',
25
+ in_progress: 'warning',
26
+ done: 'success',
27
+ declined: 'neutral'
28
+ };
29
+ /** Human label for a public board status. */
30
+ export function publicStatusLabel(status) {
31
+ return PUBLIC_STATUS_LABELS[status] ?? status;
32
+ }
33
+ /** Map a public board status to a badge variant. */
16
34
  export function getStatusBadgeVariant(status) {
17
- if (status === 'new' || status === 'reviewed')
18
- return 'success';
19
- if (status === 'resolved')
20
- return 'neutral';
21
- return 'info';
35
+ return PUBLIC_STATUS_VARIANTS[status] ?? 'neutral';
22
36
  }
@@ -1,178 +1,178 @@
1
- <script lang="ts">
2
- interface Props {
3
- x: number;
4
- y: number;
5
- boardUrl?: string;
6
- onReportBug: () => void;
7
- onRequestFeature: () => void;
8
- onViewBoard?: () => void;
9
- }
10
-
11
- import { onMount, onDestroy } from 'svelte';
12
-
13
- let { x, y, boardUrl, onReportBug, onRequestFeature, onViewBoard }: Props = $props();
14
-
15
- let popoverEl: HTMLDivElement | undefined = $state();
16
- let originalParent: HTMLElement | null = null;
17
- let portalTarget: HTMLElement | null = null;
18
-
19
- let adjustedX = $derived(Math.min(x, (typeof window !== 'undefined' ? window.innerWidth : 800) - 200));
20
- let adjustedY = $derived(Math.min(y, (typeof window !== 'undefined' ? window.innerHeight : 600) - 160));
21
-
22
- /**
23
- * Find the topmost open modal dialog.
24
- * showModal() makes everything outside the dialog inert — including
25
- * top-layer popovers that aren't descendants of the dialog.
26
- * By portalling into the dialog, our menu becomes a descendant and
27
- * escapes the inertness.
28
- */
29
- function findOpenModalDialog(): HTMLDialogElement | null {
30
- const dialogs = document.querySelectorAll<HTMLDialogElement>('dialog[open]');
31
- // showModal() sets the `modal` property — filter for true modals
32
- for (const d of dialogs) {
33
- // A dialog opened with showModal() will have a non-null ::backdrop
34
- // and will be in the top layer. We can detect this via the "open"
35
- // attribute + checking if it has modal behavior (no direct API,
36
- // but modal dialogs block interaction with the rest of the page).
37
- // The simplest reliable check: modal dialogs have returnValue capability
38
- // and are in the top layer. In practice, all open dialogs we care about
39
- // are modal since our Modal component always uses showModal().
40
- if (d.open) return d;
41
- }
42
- return null;
43
- }
44
-
45
- onMount(() => {
46
- if (!popoverEl) return;
47
-
48
- // If a modal dialog is open, portal into it to escape inertness
49
- const modalDialog = findOpenModalDialog();
50
- if (modalDialog) {
51
- originalParent = popoverEl.parentElement;
52
- portalTarget = modalDialog;
53
- modalDialog.appendChild(popoverEl);
54
- }
55
-
56
- // Show the popover to promote it to the top layer
57
- if (typeof popoverEl.showPopover === 'function') {
58
- try {
59
- popoverEl.showPopover();
60
- } catch {
61
- // Fallback: popover API not supported, z-index inline style still applies
62
- }
63
- }
64
- });
65
-
66
- onDestroy(() => {
67
- // Move the element back before Svelte tries to clean up
68
- if (popoverEl && portalTarget && originalParent) {
69
- try {
70
- if (typeof popoverEl.hidePopover === 'function') {
71
- popoverEl.hidePopover();
72
- }
73
- originalParent.appendChild(popoverEl);
74
- } catch {
75
- // Element may already be removed
76
- }
77
- }
78
- });
79
-
80
- function handleKeydown(e: KeyboardEvent) {
81
- const menu = (e.currentTarget as HTMLElement);
82
- const items = menu.querySelectorAll<HTMLElement>('[role="menuitem"]');
83
- const currentIndex = Array.from(items).indexOf(e.target as HTMLElement);
84
-
85
- if (e.key === 'ArrowDown') {
86
- e.preventDefault();
87
- const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
88
- items[next]?.focus();
89
- } else if (e.key === 'ArrowUp') {
90
- e.preventDefault();
91
- const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
92
- items[prev]?.focus();
93
- }
94
- }
95
- </script>
96
-
97
- <!-- popover="manual" promotes to browser top layer (above <dialog> modals) -->
98
- <div
99
- bind:this={popoverEl}
100
- role="presentation"
101
- popover="manual"
102
- class="sf-ctx-popover"
103
- style="left: {adjustedX}px; top: {adjustedY}px;"
104
- onmousedown={(e) => e.stopPropagation()}
105
- onclick={(e) => e.stopPropagation()}
106
- onkeydown={(e) => e.stopPropagation()}
107
- >
108
- <div class="sf-ctx-menu" role="menu" tabindex="-1" aria-label="Feedback actions" onkeydown={handleKeydown}>
109
- <button class="sf-ctx-item" role="menuitem" onclick={onReportBug}>
110
- <i class="fa-solid fa-bug text-red-400" aria-hidden="true"></i>
111
- Report Bug
112
- </button>
113
- <button class="sf-ctx-item" role="menuitem" onclick={onRequestFeature}>
114
- <i class="fa-solid fa-lightbulb text-amber-400" aria-hidden="true"></i>
115
- Request Feature
116
- </button>
117
- {#if boardUrl && onViewBoard}
118
- <div class="sf-ctx-divider" role="separator">
119
- <button class="sf-ctx-item sf-ctx-item-secondary" role="menuitem" onclick={onViewBoard}>
120
- <i class="fa-solid fa-clipboard-list" aria-hidden="true"></i>
121
- View Open Issues
122
- </button>
123
- </div>
124
- {/if}
125
- </div>
126
- </div>
127
-
128
- <style>
129
- /* Override popover defaults — remove browser default styles */
130
- .sf-ctx-popover {
131
- position: fixed;
132
- margin: 0;
133
- padding: 0;
134
- border: none;
135
- background: transparent;
136
- overflow: visible;
137
- /* Fallback for browsers without popover support */
138
- z-index: 99999;
139
- }
140
-
141
- .sf-ctx-menu {
142
- background: var(--sf-surface, var(--color-surface, #1e293b));
143
- border: 1px solid var(--sf-border, var(--color-border, #334155));
144
- border-radius: 0.5rem;
145
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
146
- overflow: hidden;
147
- min-width: 180px;
148
- }
149
-
150
- .sf-ctx-item {
151
- display: flex;
152
- align-items: center;
153
- gap: 0.625rem;
154
- width: 100%;
155
- padding: 0.625rem 0.75rem;
156
- font-size: 0.875rem;
157
- color: var(--sf-foreground, var(--color-foreground, #e2e8f0));
158
- background: transparent;
159
- border: none;
160
- cursor: pointer;
161
- text-align: left;
162
- transition: background 0.15s;
163
- }
164
-
165
- .sf-ctx-item:hover,
166
- .sf-ctx-item:focus-visible {
167
- background: var(--sf-surface-hover, var(--color-surface-hover, #334155));
168
- outline: none;
169
- }
170
-
171
- .sf-ctx-item-secondary {
172
- color: var(--sf-foreground-secondary, var(--color-foreground-secondary, #94a3b8));
173
- }
174
-
175
- .sf-ctx-divider {
176
- border-top: 1px solid var(--sf-border, var(--color-border, #334155));
177
- }
178
- </style>
1
+ <script lang="ts">
2
+ interface Props {
3
+ x: number;
4
+ y: number;
5
+ boardUrl?: string;
6
+ onReportBug: () => void;
7
+ onRequestFeature: () => void;
8
+ onViewBoard?: () => void;
9
+ }
10
+
11
+ import { onMount, onDestroy } from 'svelte';
12
+
13
+ let { x, y, boardUrl, onReportBug, onRequestFeature, onViewBoard }: Props = $props();
14
+
15
+ let popoverEl: HTMLDivElement | undefined = $state();
16
+ let originalParent: HTMLElement | null = null;
17
+ let portalTarget: HTMLElement | null = null;
18
+
19
+ let adjustedX = $derived(Math.min(x, (typeof window !== 'undefined' ? window.innerWidth : 800) - 200));
20
+ let adjustedY = $derived(Math.min(y, (typeof window !== 'undefined' ? window.innerHeight : 600) - 160));
21
+
22
+ /**
23
+ * Find the topmost open modal dialog.
24
+ * showModal() makes everything outside the dialog inert — including
25
+ * top-layer popovers that aren't descendants of the dialog.
26
+ * By portalling into the dialog, our menu becomes a descendant and
27
+ * escapes the inertness.
28
+ */
29
+ function findOpenModalDialog(): HTMLDialogElement | null {
30
+ const dialogs = document.querySelectorAll<HTMLDialogElement>('dialog[open]');
31
+ // showModal() sets the `modal` property — filter for true modals
32
+ for (const d of dialogs) {
33
+ // A dialog opened with showModal() will have a non-null ::backdrop
34
+ // and will be in the top layer. We can detect this via the "open"
35
+ // attribute + checking if it has modal behavior (no direct API,
36
+ // but modal dialogs block interaction with the rest of the page).
37
+ // The simplest reliable check: modal dialogs have returnValue capability
38
+ // and are in the top layer. In practice, all open dialogs we care about
39
+ // are modal since our Modal component always uses showModal().
40
+ if (d.open) return d;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ onMount(() => {
46
+ if (!popoverEl) return;
47
+
48
+ // If a modal dialog is open, portal into it to escape inertness
49
+ const modalDialog = findOpenModalDialog();
50
+ if (modalDialog) {
51
+ originalParent = popoverEl.parentElement;
52
+ portalTarget = modalDialog;
53
+ modalDialog.appendChild(popoverEl);
54
+ }
55
+
56
+ // Show the popover to promote it to the top layer
57
+ if (typeof popoverEl.showPopover === 'function') {
58
+ try {
59
+ popoverEl.showPopover();
60
+ } catch {
61
+ // Fallback: popover API not supported, z-index inline style still applies
62
+ }
63
+ }
64
+ });
65
+
66
+ onDestroy(() => {
67
+ // Move the element back before Svelte tries to clean up
68
+ if (popoverEl && portalTarget && originalParent) {
69
+ try {
70
+ if (typeof popoverEl.hidePopover === 'function') {
71
+ popoverEl.hidePopover();
72
+ }
73
+ originalParent.appendChild(popoverEl);
74
+ } catch {
75
+ // Element may already be removed
76
+ }
77
+ }
78
+ });
79
+
80
+ function handleKeydown(e: KeyboardEvent) {
81
+ const menu = (e.currentTarget as HTMLElement);
82
+ const items = menu.querySelectorAll<HTMLElement>('[role="menuitem"]');
83
+ const currentIndex = Array.from(items).indexOf(e.target as HTMLElement);
84
+
85
+ if (e.key === 'ArrowDown') {
86
+ e.preventDefault();
87
+ const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
88
+ items[next]?.focus();
89
+ } else if (e.key === 'ArrowUp') {
90
+ e.preventDefault();
91
+ const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
92
+ items[prev]?.focus();
93
+ }
94
+ }
95
+ </script>
96
+
97
+ <!-- popover="manual" promotes to browser top layer (above <dialog> modals) -->
98
+ <div
99
+ bind:this={popoverEl}
100
+ role="presentation"
101
+ popover="manual"
102
+ class="sf-ctx-popover"
103
+ style="left: {adjustedX}px; top: {adjustedY}px;"
104
+ onmousedown={(e) => e.stopPropagation()}
105
+ onclick={(e) => e.stopPropagation()}
106
+ onkeydown={(e) => e.stopPropagation()}
107
+ >
108
+ <div class="sf-ctx-menu" role="menu" tabindex="-1" aria-label="Feedback actions" onkeydown={handleKeydown}>
109
+ <button class="sf-ctx-item" role="menuitem" onclick={onReportBug}>
110
+ <i class="fa-solid fa-bug text-red-400" aria-hidden="true"></i>
111
+ Report Bug
112
+ </button>
113
+ <button class="sf-ctx-item" role="menuitem" onclick={onRequestFeature}>
114
+ <i class="fa-solid fa-lightbulb text-amber-400" aria-hidden="true"></i>
115
+ Request Feature
116
+ </button>
117
+ {#if boardUrl && onViewBoard}
118
+ <div class="sf-ctx-divider" role="separator">
119
+ <button class="sf-ctx-item sf-ctx-item-secondary" role="menuitem" onclick={onViewBoard}>
120
+ <i class="fa-solid fa-clipboard-list" aria-hidden="true"></i>
121
+ View Open Issues
122
+ </button>
123
+ </div>
124
+ {/if}
125
+ </div>
126
+ </div>
127
+
128
+ <style>
129
+ /* Override popover defaults — remove browser default styles */
130
+ .sf-ctx-popover {
131
+ position: fixed;
132
+ margin: 0;
133
+ padding: 0;
134
+ border: none;
135
+ background: transparent;
136
+ overflow: visible;
137
+ /* Fallback for browsers without popover support */
138
+ z-index: 99999;
139
+ }
140
+
141
+ .sf-ctx-menu {
142
+ background: var(--sf-surface, var(--color-surface, #1e293b));
143
+ border: 1px solid var(--sf-border, var(--color-border, #334155));
144
+ border-radius: 0.5rem;
145
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
146
+ overflow: hidden;
147
+ min-width: 180px;
148
+ }
149
+
150
+ .sf-ctx-item {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 0.625rem;
154
+ width: 100%;
155
+ padding: 0.625rem 0.75rem;
156
+ font-size: 0.875rem;
157
+ color: var(--sf-foreground, var(--color-foreground, #e2e8f0));
158
+ background: transparent;
159
+ border: none;
160
+ cursor: pointer;
161
+ text-align: left;
162
+ transition: background 0.15s;
163
+ }
164
+
165
+ .sf-ctx-item:hover,
166
+ .sf-ctx-item:focus-visible {
167
+ background: var(--sf-surface-hover, var(--color-surface-hover, #334155));
168
+ outline: none;
169
+ }
170
+
171
+ .sf-ctx-item-secondary {
172
+ color: var(--sf-foreground-secondary, var(--color-foreground-secondary, #94a3b8));
173
+ }
174
+
175
+ .sf-ctx-divider {
176
+ border-top: 1px solid var(--sf-border, var(--color-border, #334155));
177
+ }
178
+ </style>