@workbench-kit/react 0.0.2-prototype.0.2.10 → 0.0.2-prototype.0.2.13
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/package.json +10 -10
- package/src/index.ts +16 -2
- package/src/layout/index.ts +9 -0
- package/src/layout/sidebar/SideBarTree.tsx +301 -0
- package/src/layout/sidebar/index.ts +13 -0
- package/src/overlay/ContextMenu.tsx +112 -5
- package/src/overlay/context-menu.css +4 -2
- package/src/primitives/index.ts +5 -0
- package/src/primitives/library-detail-layout/LibraryDetailLayout.tsx +118 -4
- package/src/primitives/library-detail-layout/index.ts +5 -0
- package/src/primitives/library-detail-layout/library-detail-layout.css +57 -0
- package/src/primitives/library-detail-layout/resolve-hero-cover-media.ts +80 -0
- package/src/primitives/record-media-hero/RecordMediaHero.tsx +4 -0
- package/src/primitives/workbench-media-slot/WorkbenchMediaSlot.tsx +7 -1
- package/src/workbench/management/createWorkbenchNotify.tsx +88 -0
- package/src/workbench/management/index.ts +7 -0
- package/src/workbench/management/management.css +28 -0
- package/src/workbench/management/useWorkbenchNotify.ts +10 -0
- package/src/workbench/theme/themePresets.ts +1 -0
- package/src/workbench/workspace/WorkspaceExplorer.tsx +104 -3
- package/src/workbench/workspace/workspaceExplorerKeyboard.ts +109 -0
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import './library-detail-layout.css';
|
|
2
|
-
import type
|
|
2
|
+
import { useEffect, useState, type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
3
3
|
|
|
4
4
|
import { cx } from '../../utils/cx';
|
|
5
5
|
import { WorkbenchMediaSlot } from '../workbench-media-slot';
|
|
6
6
|
import { RecordMediaHero } from '../record-media-hero';
|
|
7
7
|
import { ScrollArea } from '../scroll-area';
|
|
8
|
+
import { resolveLibraryDetailHeroCoverMedia } from './resolve-hero-cover-media.js';
|
|
8
9
|
|
|
9
|
-
export type LibraryDetailLayoutMode = 'background' | 'banner' | 'compact';
|
|
10
|
+
export type LibraryDetailLayoutMode = 'background' | 'banner' | 'compact' | 'hero-cover';
|
|
10
11
|
|
|
11
12
|
export interface LibraryDetailLayoutProps extends Omit<
|
|
12
13
|
ComponentPropsWithRef<'div'>,
|
|
13
14
|
'children' | 'title'
|
|
14
15
|
> {
|
|
15
16
|
readonly actions?: ReactNode;
|
|
17
|
+
/** Optional attribution / footer below the identity row (`hero-cover` mode). */
|
|
18
|
+
readonly attribution?: ReactNode;
|
|
16
19
|
readonly backgroundImageUrl?: string | null;
|
|
17
20
|
readonly children?: ReactNode;
|
|
18
21
|
readonly coverAlt?: string;
|
|
19
22
|
readonly coverImageUrl?: string | null;
|
|
20
23
|
readonly description?: ReactNode;
|
|
24
|
+
/**
|
|
25
|
+
* Wide hero band URL for `hero-cover` mode. When omitted or identical to
|
|
26
|
+
* `coverImageUrl`, the cover is used as soft atmosphere behind the portrait.
|
|
27
|
+
*/
|
|
28
|
+
readonly heroImageUrl?: string | null;
|
|
21
29
|
readonly logoImageUrl?: string | null;
|
|
22
30
|
readonly mode?: LibraryDetailLayoutMode;
|
|
23
31
|
readonly summary?: ReactNode;
|
|
@@ -27,12 +35,14 @@ export interface LibraryDetailLayoutProps extends Omit<
|
|
|
27
35
|
|
|
28
36
|
export function LibraryDetailLayout({
|
|
29
37
|
actions,
|
|
38
|
+
attribution,
|
|
30
39
|
backgroundImageUrl = null,
|
|
31
40
|
children,
|
|
32
41
|
className,
|
|
33
42
|
coverAlt,
|
|
34
43
|
coverImageUrl = null,
|
|
35
44
|
description,
|
|
45
|
+
heroImageUrl = null,
|
|
36
46
|
logoImageUrl = null,
|
|
37
47
|
mode = 'banner',
|
|
38
48
|
summary,
|
|
@@ -41,7 +51,13 @@ export function LibraryDetailLayout({
|
|
|
41
51
|
...props
|
|
42
52
|
}: LibraryDetailLayoutProps): ReactNode {
|
|
43
53
|
const resolvedMode =
|
|
44
|
-
mode === 'background'
|
|
54
|
+
mode === 'background'
|
|
55
|
+
? 'background'
|
|
56
|
+
: mode === 'compact'
|
|
57
|
+
? 'compact'
|
|
58
|
+
: mode === 'hero-cover'
|
|
59
|
+
? 'hero-cover'
|
|
60
|
+
: 'banner';
|
|
45
61
|
|
|
46
62
|
return (
|
|
47
63
|
<div
|
|
@@ -54,7 +70,18 @@ export function LibraryDetailLayout({
|
|
|
54
70
|
{...props}
|
|
55
71
|
>
|
|
56
72
|
{toolbar}
|
|
57
|
-
{resolvedMode === '
|
|
73
|
+
{resolvedMode === 'hero-cover' ? (
|
|
74
|
+
<LibraryDetailHeroCover
|
|
75
|
+
actions={actions}
|
|
76
|
+
attribution={attribution}
|
|
77
|
+
coverAlt={coverAlt}
|
|
78
|
+
coverImageUrl={coverImageUrl}
|
|
79
|
+
description={description}
|
|
80
|
+
heroImageUrl={heroImageUrl ?? backgroundImageUrl}
|
|
81
|
+
summary={summary}
|
|
82
|
+
title={title}
|
|
83
|
+
/>
|
|
84
|
+
) : resolvedMode === 'background' ? (
|
|
58
85
|
<div className="ui-library-detail-layout__band" data-ui-library-detail-band="true">
|
|
59
86
|
<RecordMediaHero
|
|
60
87
|
alt={coverAlt}
|
|
@@ -105,6 +132,93 @@ export function LibraryDetailLayout({
|
|
|
105
132
|
);
|
|
106
133
|
}
|
|
107
134
|
|
|
135
|
+
function LibraryDetailHeroCover({
|
|
136
|
+
actions,
|
|
137
|
+
attribution,
|
|
138
|
+
coverAlt,
|
|
139
|
+
coverImageUrl,
|
|
140
|
+
description,
|
|
141
|
+
heroImageUrl,
|
|
142
|
+
summary,
|
|
143
|
+
title,
|
|
144
|
+
}: {
|
|
145
|
+
actions?: ReactNode;
|
|
146
|
+
attribution?: ReactNode;
|
|
147
|
+
coverAlt?: string;
|
|
148
|
+
coverImageUrl: string | null;
|
|
149
|
+
description?: ReactNode;
|
|
150
|
+
heroImageUrl: string | null;
|
|
151
|
+
summary?: ReactNode;
|
|
152
|
+
title: ReactNode;
|
|
153
|
+
}): ReactNode {
|
|
154
|
+
const [heroFailed, setHeroFailed] = useState(false);
|
|
155
|
+
const [coverFailed, setCoverFailed] = useState(false);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
setHeroFailed(false);
|
|
159
|
+
}, [heroImageUrl]);
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
setCoverFailed(false);
|
|
163
|
+
}, [coverImageUrl]);
|
|
164
|
+
|
|
165
|
+
const media = resolveLibraryDetailHeroCoverMedia({
|
|
166
|
+
heroImageUrl,
|
|
167
|
+
coverImageUrl,
|
|
168
|
+
heroFailed,
|
|
169
|
+
coverFailed,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className="ui-library-detail-layout__hero-cover" data-ui-library-detail-hero-cover="true">
|
|
174
|
+
<div className="ui-library-detail-layout__band" data-ui-library-detail-band="true">
|
|
175
|
+
<RecordMediaHero
|
|
176
|
+
alt={coverAlt}
|
|
177
|
+
className={cx(
|
|
178
|
+
'ui-library-detail-layout__band-media',
|
|
179
|
+
media.bandKind === 'atmosphere' && 'ui-library-detail-layout__band-media--atmosphere',
|
|
180
|
+
)}
|
|
181
|
+
data-atmosphere={media.bandKind === 'atmosphere' ? 'true' : undefined}
|
|
182
|
+
fallbackIcon="library"
|
|
183
|
+
imageUrl={media.bandImageUrl}
|
|
184
|
+
layout="background"
|
|
185
|
+
onImageError={() => {
|
|
186
|
+
if (media.bandKind === 'hero') {
|
|
187
|
+
setHeroFailed(true);
|
|
188
|
+
} else if (media.bandKind === 'atmosphere') {
|
|
189
|
+
setCoverFailed(true);
|
|
190
|
+
}
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
<div aria-hidden className="ui-library-detail-layout__band-overlay" />
|
|
194
|
+
</div>
|
|
195
|
+
<div className="ui-library-detail-layout__hero-cover-body">
|
|
196
|
+
{media.showPortraitCover ? (
|
|
197
|
+
<RecordMediaHero
|
|
198
|
+
alt={coverAlt}
|
|
199
|
+
className="ui-library-detail-layout__portrait-cover"
|
|
200
|
+
fallbackIcon="library"
|
|
201
|
+
imageUrl={media.resolvedCover}
|
|
202
|
+
layout="compact"
|
|
203
|
+
onImageError={() => setCoverFailed(true)}
|
|
204
|
+
/>
|
|
205
|
+
) : null}
|
|
206
|
+
<div className="ui-library-detail-layout__hero-content">
|
|
207
|
+
<div className="ui-library-detail-layout__title">{title}</div>
|
|
208
|
+
{summary ? <div className="ui-library-detail-layout__summary">{summary}</div> : null}
|
|
209
|
+
{actions ? <div className="ui-library-detail-layout__actions">{actions}</div> : null}
|
|
210
|
+
{description ? (
|
|
211
|
+
<div className="ui-library-detail-layout__description">{description}</div>
|
|
212
|
+
) : null}
|
|
213
|
+
{attribution ? (
|
|
214
|
+
<div className="ui-library-detail-layout__attribution">{attribution}</div>
|
|
215
|
+
) : null}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
108
222
|
function LibraryDetailIdentity({
|
|
109
223
|
logoImageUrl,
|
|
110
224
|
summary,
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
--shell-library-capsule-list-height: 43px;
|
|
5
5
|
--shell-detail-hero-cover-width: 231px;
|
|
6
6
|
--shell-detail-hero-cover-height: calc(var(--shell-detail-hero-cover-width, 231px) * 43 / 116);
|
|
7
|
+
--shell-detail-portrait-cover-width: 120px;
|
|
8
|
+
--shell-detail-portrait-cover-height: 160px;
|
|
9
|
+
--shell-detail-atmosphere-blur: 18px;
|
|
10
|
+
--shell-detail-atmosphere-scale: 1.12;
|
|
11
|
+
--shell-detail-atmosphere-opacity: 0.55;
|
|
7
12
|
--shell-library-detail-logo-size: 56px;
|
|
8
13
|
--shell-library-detail-thumb-width: var(--shell-library-capsule-list-width, 116px);
|
|
9
14
|
--shell-library-detail-thumb-height: var(--shell-library-capsule-list-height, 43px);
|
|
@@ -167,3 +172,55 @@
|
|
|
167
172
|
aspect-ratio: var(--shell-library-capsule-aspect, 116 / 43);
|
|
168
173
|
flex: 0 0 auto;
|
|
169
174
|
}
|
|
175
|
+
|
|
176
|
+
.ui-library-detail-layout__hero-cover {
|
|
177
|
+
display: grid;
|
|
178
|
+
gap: 0;
|
|
179
|
+
min-width: 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.ui-library-detail-layout__hero-cover .ui-library-detail-layout__band {
|
|
183
|
+
border-bottom-left-radius: 0;
|
|
184
|
+
border-bottom-right-radius: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.ui-library-detail-layout__band-media--atmosphere .ui-record-media-hero__image,
|
|
188
|
+
.ui-library-detail-layout__band-media[data-atmosphere='true'] .ui-record-media-hero__image {
|
|
189
|
+
opacity: var(--shell-detail-atmosphere-opacity, 0.55);
|
|
190
|
+
filter: blur(var(--shell-detail-atmosphere-blur, 18px));
|
|
191
|
+
transform: scale(var(--shell-detail-atmosphere-scale, 1.12));
|
|
192
|
+
object-fit: cover;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.ui-library-detail-layout__hero-cover-body {
|
|
196
|
+
display: flex;
|
|
197
|
+
gap: 16px;
|
|
198
|
+
align-items: flex-end;
|
|
199
|
+
margin-top: -48px;
|
|
200
|
+
padding: 0 16px 8px;
|
|
201
|
+
position: relative;
|
|
202
|
+
z-index: 2;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.ui-library-detail-layout__portrait-cover.ui-record-media-hero--compact {
|
|
206
|
+
box-sizing: border-box;
|
|
207
|
+
width: var(--shell-detail-portrait-cover-width, 120px);
|
|
208
|
+
min-width: var(--shell-detail-portrait-cover-width, 120px);
|
|
209
|
+
max-width: var(--shell-detail-portrait-cover-width, 120px);
|
|
210
|
+
height: var(--shell-detail-portrait-cover-height, 160px);
|
|
211
|
+
min-height: var(--shell-detail-portrait-cover-height, 160px);
|
|
212
|
+
max-height: var(--shell-detail-portrait-cover-height, 160px);
|
|
213
|
+
aspect-ratio: auto;
|
|
214
|
+
flex: 0 0 auto;
|
|
215
|
+
border: 1px solid
|
|
216
|
+
var(--shell-color-border-subtle, var(--color-border-subtle, rgba(255, 255, 255, 0.12)));
|
|
217
|
+
border-radius: var(--shell-radius-sm, 4px);
|
|
218
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.ui-library-detail-layout__attribution {
|
|
223
|
+
color: var(--shell-color-text-subtle, var(--color-text-subtle, var(--color-text-muted)));
|
|
224
|
+
font-size: var(--shell-font-size-caption, 0.75rem);
|
|
225
|
+
line-height: 1.4;
|
|
226
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure media resolution for LibraryDetailLayout `hero-cover` mode.
|
|
3
|
+
* Implements atmosphere-from-cover when no distinct wide hero URL succeeds.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ResolveLibraryDetailHeroCoverMediaInput {
|
|
7
|
+
readonly heroImageUrl?: string | null;
|
|
8
|
+
readonly coverImageUrl?: string | null;
|
|
9
|
+
readonly heroFailed?: boolean;
|
|
10
|
+
readonly coverFailed?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ResolvedLibraryDetailHeroCoverMedia {
|
|
14
|
+
/** Trimmed cover URL when load has not failed. */
|
|
15
|
+
readonly resolvedCover: string | null;
|
|
16
|
+
/** Distinct wide hero URL (differs from cover) when load has not failed. */
|
|
17
|
+
readonly resolvedHero: string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Soft backdrop URL when there is no successful distinct hero but cover is
|
|
20
|
+
* available. Must not be stretched as the sole full-bleed identity cover.
|
|
21
|
+
*/
|
|
22
|
+
readonly atmosphereUrl: string | null;
|
|
23
|
+
/** Band media to render: distinct hero, else atmosphere, else null (fallback). */
|
|
24
|
+
readonly bandImageUrl: string | null;
|
|
25
|
+
readonly bandKind: 'hero' | 'atmosphere' | 'fallback';
|
|
26
|
+
readonly showPortraitCover: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function trimUrl(value: string | null | undefined): string | null {
|
|
30
|
+
if (value === null || value === undefined) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
return trimmed === '' ? null : trimmed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveLibraryDetailHeroCoverMedia(
|
|
38
|
+
input: ResolveLibraryDetailHeroCoverMediaInput,
|
|
39
|
+
): ResolvedLibraryDetailHeroCoverMedia {
|
|
40
|
+
const coverSource = trimUrl(input.coverImageUrl);
|
|
41
|
+
const heroSource = trimUrl(input.heroImageUrl);
|
|
42
|
+
const heroFailed = input.heroFailed ?? false;
|
|
43
|
+
const coverFailed = input.coverFailed ?? false;
|
|
44
|
+
|
|
45
|
+
const resolvedCover = coverSource && !coverFailed ? coverSource : null;
|
|
46
|
+
const distinctHero = heroSource && coverSource && heroSource === coverSource ? null : heroSource;
|
|
47
|
+
const resolvedHero = distinctHero && !heroFailed ? distinctHero : null;
|
|
48
|
+
const atmosphereUrl = !resolvedHero && resolvedCover ? resolvedCover : null;
|
|
49
|
+
|
|
50
|
+
if (resolvedHero) {
|
|
51
|
+
return {
|
|
52
|
+
resolvedCover,
|
|
53
|
+
resolvedHero,
|
|
54
|
+
atmosphereUrl: null,
|
|
55
|
+
bandImageUrl: resolvedHero,
|
|
56
|
+
bandKind: 'hero',
|
|
57
|
+
showPortraitCover: resolvedCover !== null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (atmosphereUrl) {
|
|
62
|
+
return {
|
|
63
|
+
resolvedCover,
|
|
64
|
+
resolvedHero: null,
|
|
65
|
+
atmosphereUrl,
|
|
66
|
+
bandImageUrl: atmosphereUrl,
|
|
67
|
+
bandKind: 'atmosphere',
|
|
68
|
+
showPortraitCover: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
resolvedCover,
|
|
74
|
+
resolvedHero: null,
|
|
75
|
+
atmosphereUrl: null,
|
|
76
|
+
bandImageUrl: null,
|
|
77
|
+
bandKind: 'fallback',
|
|
78
|
+
showPortraitCover: false,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -15,6 +15,8 @@ export interface RecordMediaHeroProps extends ComponentPropsWithRef<'div'> {
|
|
|
15
15
|
layout?: RecordMediaHeroLayout;
|
|
16
16
|
logoUrl?: string | null;
|
|
17
17
|
maxWidth?: number | string;
|
|
18
|
+
/** Forwarded when the primary hero/cover image fails to load. */
|
|
19
|
+
onImageError?: () => void;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function resolveMaxWidth(maxWidth: number | string | undefined): CSSProperties['maxWidth'] {
|
|
@@ -33,6 +35,7 @@ export function RecordMediaHero({
|
|
|
33
35
|
layout = 'banner',
|
|
34
36
|
logoUrl = null,
|
|
35
37
|
maxWidth,
|
|
38
|
+
onImageError,
|
|
36
39
|
style,
|
|
37
40
|
...props
|
|
38
41
|
}: RecordMediaHeroProps) {
|
|
@@ -66,6 +69,7 @@ export function RecordMediaHero({
|
|
|
66
69
|
imageClassName="ui-record-media-hero__image"
|
|
67
70
|
imageUrl={imageUrl}
|
|
68
71
|
loading="eager"
|
|
72
|
+
onImageError={onImageError}
|
|
69
73
|
/>
|
|
70
74
|
{logoMedia.shouldShowImage ? (
|
|
71
75
|
<img
|
|
@@ -20,6 +20,8 @@ export interface WorkbenchMediaSlotProps extends Omit<ComponentPropsWithRef<'spa
|
|
|
20
20
|
* Pass `eager` for above-the-fold heroes and detail identity art.
|
|
21
21
|
*/
|
|
22
22
|
loading?: 'eager' | 'lazy';
|
|
23
|
+
/** Optional host callback after the slot marks the current URL as failed. */
|
|
24
|
+
onImageError?: () => void;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function WorkbenchMediaSlot({
|
|
@@ -33,6 +35,7 @@ export function WorkbenchMediaSlot({
|
|
|
33
35
|
imageClassName,
|
|
34
36
|
imageUrl = null,
|
|
35
37
|
loading = 'lazy',
|
|
38
|
+
onImageError,
|
|
36
39
|
...props
|
|
37
40
|
}: WorkbenchMediaSlotProps) {
|
|
38
41
|
const media = useWorkbenchMediaImage(imageUrl);
|
|
@@ -47,7 +50,10 @@ export function WorkbenchMediaSlot({
|
|
|
47
50
|
alt={alt ?? ''}
|
|
48
51
|
className={cx('ui-workbench-media-slot__image', imageClassName)}
|
|
49
52
|
loading={loading}
|
|
50
|
-
onError={
|
|
53
|
+
onError={() => {
|
|
54
|
+
media.onImageError();
|
|
55
|
+
onImageError?.();
|
|
56
|
+
}}
|
|
51
57
|
src={media.imageSrc}
|
|
52
58
|
/>
|
|
53
59
|
) : (
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ShowWorkbenchNoticeInput,
|
|
5
|
+
WorkbenchNoticeController,
|
|
6
|
+
WorkbenchNoticeTone,
|
|
7
|
+
} from './WorkbenchNotice.js';
|
|
8
|
+
|
|
9
|
+
export interface WorkbenchNotificationAction {
|
|
10
|
+
readonly label: string;
|
|
11
|
+
readonly onAction: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WorkbenchNotifyOptions {
|
|
15
|
+
readonly actions?: readonly WorkbenchNotificationAction[];
|
|
16
|
+
readonly dataAttributes?: ShowWorkbenchNoticeInput['dataAttributes'];
|
|
17
|
+
readonly durationMs?: number;
|
|
18
|
+
readonly id?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* NotificationService-shaped facade over {@link WorkbenchNoticeController}.
|
|
23
|
+
* Prefer this for command/install/save feedback; keep modal confirms separate.
|
|
24
|
+
*/
|
|
25
|
+
export interface WorkbenchNotify {
|
|
26
|
+
info(message: ReactNode, options?: WorkbenchNotifyOptions): string;
|
|
27
|
+
error(message: ReactNode, options?: WorkbenchNotifyOptions): string;
|
|
28
|
+
success(message: ReactNode, options?: WorkbenchNotifyOptions): string;
|
|
29
|
+
warning(message: ReactNode, options?: WorkbenchNotifyOptions): string;
|
|
30
|
+
dismiss(id: string): void;
|
|
31
|
+
clear(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function showTone(
|
|
35
|
+
controller: WorkbenchNoticeController,
|
|
36
|
+
tone: WorkbenchNoticeTone,
|
|
37
|
+
message: ReactNode,
|
|
38
|
+
options: WorkbenchNotifyOptions = {},
|
|
39
|
+
): string {
|
|
40
|
+
const { actions, dataAttributes, durationMs, id } = options;
|
|
41
|
+
const noticeId = id ?? `workbench-notify-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
42
|
+
|
|
43
|
+
const noticeMessage =
|
|
44
|
+
actions && actions.length > 0 ? (
|
|
45
|
+
<span className="ui-workbench-notify-message">
|
|
46
|
+
<span className="ui-workbench-notify-message__text">{message}</span>
|
|
47
|
+
<span className="ui-workbench-notify-message__actions">
|
|
48
|
+
{actions.map((action) => (
|
|
49
|
+
<button
|
|
50
|
+
className="ui-workbench-notify-message__action"
|
|
51
|
+
key={action.label}
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => {
|
|
54
|
+
action.onAction();
|
|
55
|
+
controller.dismissNotice(noticeId);
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{action.label}
|
|
59
|
+
</button>
|
|
60
|
+
))}
|
|
61
|
+
</span>
|
|
62
|
+
</span>
|
|
63
|
+
) : (
|
|
64
|
+
message
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return controller.showNotice({
|
|
68
|
+
...(dataAttributes === undefined ? {} : { dataAttributes }),
|
|
69
|
+
...(durationMs === undefined ? {} : { durationMs }),
|
|
70
|
+
id: noticeId,
|
|
71
|
+
message: noticeMessage,
|
|
72
|
+
tone,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a `notify.info` / `notify.error` facade over an existing notice controller.
|
|
78
|
+
*/
|
|
79
|
+
export function createWorkbenchNotify(controller: WorkbenchNoticeController): WorkbenchNotify {
|
|
80
|
+
return {
|
|
81
|
+
info: (message, options) => showTone(controller, 'info', message, options),
|
|
82
|
+
error: (message, options) => showTone(controller, 'error', message, options),
|
|
83
|
+
success: (message, options) => showTone(controller, 'success', message, options),
|
|
84
|
+
warning: (message, options) => showTone(controller, 'warning', message, options),
|
|
85
|
+
dismiss: (noticeId) => controller.dismissNotice(noticeId),
|
|
86
|
+
clear: () => controller.clearNotices(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -75,6 +75,13 @@ export type {
|
|
|
75
75
|
WorkbenchNoticePosition,
|
|
76
76
|
WorkbenchNoticeTone,
|
|
77
77
|
} from './WorkbenchNotice.js';
|
|
78
|
+
export { createWorkbenchNotify } from './createWorkbenchNotify.js';
|
|
79
|
+
export type {
|
|
80
|
+
WorkbenchNotificationAction,
|
|
81
|
+
WorkbenchNotify,
|
|
82
|
+
WorkbenchNotifyOptions,
|
|
83
|
+
} from './createWorkbenchNotify.js';
|
|
84
|
+
export { useWorkbenchNotify } from './useWorkbenchNotify.js';
|
|
78
85
|
export { KeybindingCaptureField } from './KeybindingCaptureField.js';
|
|
79
86
|
export type { KeybindingCaptureFieldProps } from './KeybindingCaptureField.js';
|
|
80
87
|
export { KeybindingManagementPanel } from './KeybindingManagementPanel.js';
|
|
@@ -2,3 +2,31 @@
|
|
|
2
2
|
@import './library-catalog-picker-dialog.css';
|
|
3
3
|
@import './library-facet-filter-dialog.css';
|
|
4
4
|
@import './management-panel.css';
|
|
5
|
+
|
|
6
|
+
.ui-workbench-notify-message {
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
flex-wrap: wrap;
|
|
9
|
+
gap: 8px;
|
|
10
|
+
align-items: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.ui-workbench-notify-message__actions {
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
flex-wrap: wrap;
|
|
16
|
+
gap: 6px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ui-workbench-notify-message__action {
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 2px 8px;
|
|
22
|
+
border: 1px solid var(--color-border, currentColor);
|
|
23
|
+
border-radius: var(--radius-sm, 4px);
|
|
24
|
+
background: transparent;
|
|
25
|
+
color: inherit;
|
|
26
|
+
font: inherit;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ui-workbench-notify-message__action:hover {
|
|
31
|
+
background: var(--color-surface-hover, rgba(255, 255, 255, 0.08));
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { createWorkbenchNotify, type WorkbenchNotify } from './createWorkbenchNotify.js';
|
|
4
|
+
import { useWorkbenchNotice } from './WorkbenchNotice.js';
|
|
5
|
+
|
|
6
|
+
/** Convenience hook: NotificationService-shaped API inside a notice provider. */
|
|
7
|
+
export function useWorkbenchNotify(): WorkbenchNotify {
|
|
8
|
+
const controller = useWorkbenchNotice();
|
|
9
|
+
return useMemo(() => createWorkbenchNotify(controller), [controller]);
|
|
10
|
+
}
|
|
@@ -20,6 +20,7 @@ export const DARK_THEME_PRESET_MANIFEST = [
|
|
|
20
20
|
{ id: 'modern', label: 'Modern Dark' },
|
|
21
21
|
{ id: 'dark-plus', label: 'Dark+' },
|
|
22
22
|
{ id: 'hc-black', label: 'High Contrast Black' },
|
|
23
|
+
{ id: 'slate', label: 'Slate (alias pack)' },
|
|
23
24
|
] as const satisfies readonly WorkbenchThemePresetManifestEntry[];
|
|
24
25
|
|
|
25
26
|
export type LightThemePresetId = (typeof LIGHT_THEME_PRESET_MANIFEST)[number]['id'];
|