@elytracms/studio 0.0.97 → 0.0.99
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/convex/assets.ts +16 -2
- package/convex/ingest-attempt.test.ts +161 -0
- package/convex/ingest.test.ts +269 -16
- package/convex/ingest.ts +238 -47
- package/convex/path-segment-refusal.test.ts +201 -0
- package/convex/prototype-key.test.ts +92 -0
- package/convex/records-column-transform.convex.test.ts +147 -0
- package/convex/records-zod-refinements.convex.test.ts +94 -0
- package/convex/schema.ts +1 -1
- package/dist/addon.d.ts +5 -3
- package/dist/addon.js +4 -3
- package/dist/addon.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +10 -3
- package/dist/components/app-views/default-modules.js +5 -1
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +15 -8
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/builder/reuse-context.d.ts +29 -1
- package/dist/components/builder/reuse-context.js +58 -12
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +5 -6
- package/dist/components/cms/cms-store.js +12 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/field-control.d.ts +12 -1
- package/dist/components/cms/field-control.js +31 -6
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/import-batch.d.ts +23 -0
- package/dist/components/cms/import-batch.js +43 -0
- package/dist/components/cms/import-batch.js.map +1 -0
- package/dist/components/cms/rich-text/editor.js +13 -10
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/value.d.ts +9 -0
- package/dist/components/cms/rich-text/value.js +13 -0
- package/dist/components/cms/rich-text/value.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +39 -3
- package/dist/components/cms/use-document-draft.js +46 -16
- package/dist/components/cms/use-document-draft.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +7 -0
- package/dist/components/cms/use-publish-controller.js +17 -3
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/versions-pane.js +8 -5
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/delivery-health-banner.d.ts +34 -16
- package/dist/components/delivery-health-banner.js +59 -27
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/issue-severity.d.ts +3 -2
- package/dist/components/issue-severity.js.map +1 -1
- package/dist/components/nav-issues-registry.d.ts +39 -24
- package/dist/components/nav-issues-registry.js +50 -21
- package/dist/components/nav-issues-registry.js.map +1 -1
- package/dist/components/nav-issues.d.ts +23 -50
- package/dist/components/nav-issues.js +31 -59
- package/dist/components/nav-issues.js.map +1 -1
- package/dist/components/notifications/notice-center.d.ts +61 -0
- package/dist/components/notifications/notice-center.js +119 -0
- package/dist/components/notifications/notice-center.js.map +1 -0
- package/dist/components/notifications/notice-model.d.ts +137 -0
- package/dist/components/notifications/notice-model.js +107 -0
- package/dist/components/notifications/notice-model.js.map +1 -0
- package/dist/components/notifications/outcome-notice.d.ts +16 -0
- package/dist/components/notifications/outcome-notice.js +34 -0
- package/dist/components/notifications/outcome-notice.js.map +1 -0
- package/dist/components/notifications/shell-findings.d.ts +36 -0
- package/dist/components/notifications/shell-findings.js +140 -0
- package/dist/components/notifications/shell-findings.js.map +1 -0
- package/dist/components/rail-view-source.js +5 -0
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/records/record-lens.js.map +1 -1
- package/dist/components/records/record-rail.d.ts +10 -2
- package/dist/components/records/record-rail.js +74 -4
- package/dist/components/records/record-rail.js.map +1 -1
- package/dist/components/section-nav.d.ts +15 -3
- package/dist/components/section-nav.js +39 -147
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +8 -3
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/use-narrow-viewport.d.ts +11 -0
- package/dist/components/use-narrow-viewport.js +25 -0
- package/dist/components/use-narrow-viewport.js.map +1 -0
- package/dist/components/users/role-policy-drift-issues.d.ts +14 -7
- package/dist/components/users/role-policy-drift-issues.js +17 -8
- package/dist/components/users/role-policy-drift-issues.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +4 -2
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +24 -3
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +22 -2
- package/dist/lib/i18n/messages.en.js +27 -4
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/notices/toast-store.d.ts +61 -0
- package/dist/lib/notices/toast-store.js +116 -0
- package/dist/lib/notices/toast-store.js.map +1 -0
- package/dist/lib/operations/cms-persistence.d.ts +32 -7
- package/dist/lib/operations/cms-persistence.js +511 -71
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +61 -30
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.js +1 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/ui/toast.d.ts +24 -16
- package/dist/ui/toast.js +23 -32
- package/dist/ui/toast.js.map +1 -1
- package/package.json +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav-issues-registry.js","sourceRoot":"","sources":["../../src/components/nav-issues-registry.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"nav-issues-registry.js","sourceRoot":"","sources":["../../src/components/nav-issues-registry.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useContext, useEffect, useId, useMemo, useSyncExternalStore } from 'react'\nimport type { ReactNode } from 'react'\nimport { translate } from '../lib/i18n'\nimport type { IssueSeverity } from './issue-severity'\nimport { isReservedFindingSource } from './notifications/notice-model'\nimport type { Finding, NoticeLocation } from './notifications/notice-model'\n\n/**\n * EC-616 — the studio's CONTRIBUTION seam for findings (`useNavIssues`, ruling § 6b item 1);\n * since EC-690 the registry of the ONE notification-and-issue surface: everything it holds\n * is listed in the shell header's issues tray (the badge), beside the engine's own findings.\n *\n * Any component under the studio shell publishes findings with {@link useNavIssues}. While\n * the publishing component is mounted its findings are listed; on unmount they go. A rail\n * item that must ALWAYS report (not only while its view is open) declares `useIssues` on its\n * view (`nav.view({ useIssues })` / `nav.rail(kind, { useIssues })`) — the shell mounts that\n * hook for it.\n *\n * A finding is plain data: a stable `key` (unique within its source), the localized\n * `message`, a `severity` (`'error' | 'warning'`, default `'warning'`), and since EC-690 an\n * optional short `title` and a `location` (where the tray's \"Go to\" leads, or the file it names).\n *\n * EC-690 (critic point 1): every publisher has its OWN identity, assigned by the registry\n * (`useId`), so two mounted publishers that name the same source both stay listed — one can\n * never overwrite or retract the other's findings. The engine's sources (`config`, `routes`,\n * …, and the `engine:` prefix) are reserved: a contributor using one is still listed, and the\n * misuse is reported as a finding of its own.\n */\n\n/** One finding a view or addon publishes. */\nexport interface NavIssue {\n /** Stable within its source (re-publishing the same key replaces, never duplicates). */\n key: string\n /** The localized message, shown as written. */\n message: string\n /** Default `'warning'`. */\n severity?: IssueSeverity\n /** EC-690: a short localized title the tray leads the row with (e.g. \"Invalid redirect\"). */\n title?: string\n /** EC-690: where the finding is — the tray's \"Go to\" (or the file it names). */\n location?: NoticeLocation\n}\n\ntype Listener = () => void\n\ninterface Publication {\n source: string\n issues: readonly NavIssue[]\n}\n\n/** The shell-scoped store of contributed findings, keyed by PUBLISHER. */\nexport class NavIssuesRegistry {\n private readonly byPublisher = new Map<string, Publication>()\n private readonly listeners = new Set<Listener>()\n private snapshot: readonly Finding[] = []\n\n subscribe = (listener: Listener): (() => void) => {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n getSnapshot = (): readonly Finding[] => this.snapshot\n\n /**\n * Publish (or replace) ONE publisher's findings under `source`; an empty list removes the\n * publisher. `publisher` is the registry-assigned identity ({@link useNavIssues} passes its\n * `useId`), so another publisher naming the same source is untouched.\n */\n publish(publisher: string, source: string, issues: readonly NavIssue[]): void {\n if (issues.length === 0) {\n if (!this.byPublisher.delete(publisher)) return\n } else {\n this.byPublisher.set(publisher, { source, issues })\n }\n this.recompute()\n }\n\n retract(publisher: string): void {\n if (this.byPublisher.delete(publisher)) this.recompute()\n }\n\n private recompute(): void {\n const items: Finding[] = []\n for (const [publisher, { source, issues }] of this.byPublisher) {\n for (const issue of issues) {\n items.push({\n id: `${publisher}:${issue.key}`,\n source,\n key: issue.key,\n message: issue.message,\n severity: issue.severity ?? 'warning',\n ...(issue.title !== undefined ? { title: issue.title } : {}),\n ...(issue.location !== undefined ? { location: issue.location } : {}),\n })\n }\n if (isReservedFindingSource(source)) {\n items.push({\n id: `${publisher}:reserved-source`,\n source: 'engine:registry',\n key: `reserved-source:${source}`,\n message: translate('shell.notices.reservedSource', { source }),\n severity: 'error',\n location: { kind: 'config', declaredIn: 'studio' },\n })\n }\n }\n this.snapshot = items\n for (const listener of [...this.listeners]) listener()\n }\n}\n\nconst NavIssuesContext = createContext<NavIssuesRegistry | null>(null)\n\n/** Mounted once by the studio project screen, around the shell (header, rail + content). */\nexport function NavIssuesProvider({ children }: { children: ReactNode }) {\n const registry = useMemo(() => new NavIssuesRegistry(), [])\n return <NavIssuesContext.Provider value={registry}>{children}</NavIssuesContext.Provider>\n}\n\nfunction useRegistry(caller: string): NavIssuesRegistry {\n const registry = useContext(NavIssuesContext)\n if (!registry) {\n throw new Error(`${caller} must be used inside the studio shell (no NavIssuesProvider above this component).`)\n }\n return registry\n}\n\n/**\n * Publish findings to the shell's issues tray while this component is mounted. `source`\n * names the publisher's kind (`'forms'`, `'analytics:beacon'`) — it groups and orders, it is\n * not an address: each mounted call is its own publisher. Pass the same `issues` array\n * identity between renders (memoize it) or the tray re-renders on every render.\n */\nexport function useNavIssues(source: string, issues: readonly NavIssue[]): void {\n const registry = useRegistry('useNavIssues')\n const publisher = useId()\n useEffect(() => {\n registry.publish(publisher, source, issues)\n }, [registry, publisher, source, issues])\n useEffect(() => () => registry.retract(publisher), [registry, publisher])\n}\n\n/** The contributed findings, for the issues tray. */\nexport function useContributedNavIssues(): readonly Finding[] {\n const registry = useRegistry('useContributedNavIssues')\n return useSyncExternalStore(registry.subscribe, registry.getSnapshot, registry.getSnapshot)\n}\n"]}
|
|
@@ -1,62 +1,55 @@
|
|
|
1
1
|
import type { CmsValidationIssue, OrphanedCollection } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { CustomizationIssue } from '@elytracms/core/studio-core';
|
|
3
3
|
import type { IssueSeverity } from './issue-severity.js';
|
|
4
|
+
import type { NavIssue } from './nav-issues-registry.js';
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* customization issues (`CustomizationIssue`) and URL-map validation
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The engine's own project-level findings, mapped to the one shape the issues tray lists
|
|
7
|
+
* (EC-690; before it, the rail foot's cards). The least common shape across the sources —
|
|
8
|
+
* customization issues (`CustomizationIssue`) and URL-map validation (`CmsValidationIssue`,
|
|
9
|
+
* EC-469) — so a new source joins by mapping to this, not by pretending to be a
|
|
10
|
+
* customization issue.
|
|
10
11
|
*
|
|
11
12
|
* `severity` was added by EC-493: both mappers used to drop it and the widget painted
|
|
12
|
-
* every finding amber
|
|
13
|
-
* `
|
|
14
|
-
* different things, and EC-482 shipped one producer (`hierarchyMountConflicts`)
|
|
15
|
-
* emitting both into this very list.
|
|
13
|
+
* every finding amber. EC-690 added `title` (the short label a row leads with) and
|
|
14
|
+
* `location` (where "Go to" leads, or the file the row names).
|
|
16
15
|
*/
|
|
17
|
-
export
|
|
18
|
-
key: string;
|
|
19
|
-
message: string;
|
|
16
|
+
export type NavIssueItem = Required<Pick<NavIssue, 'key' | 'message'>> & {
|
|
20
17
|
severity: IssueSeverity;
|
|
21
|
-
}
|
|
18
|
+
} & Pick<NavIssue, 'title' | 'location'>;
|
|
22
19
|
/**
|
|
23
|
-
* Customization issues, as
|
|
20
|
+
* Customization issues, as findings — localized (EC-478). The producers
|
|
24
21
|
* (`studio-core/customization.ts`, `default-modules.tsx`, `builder/reuse.ts`) emit a
|
|
25
22
|
* catalog key + identifier values rather than a sentence, because the EC-236 seam
|
|
26
23
|
* lives in `packages/core` and may not import the studio catalog;
|
|
27
|
-
* {@link customizationIssueMessage} resolves them here
|
|
28
|
-
* the active locale instead of half of it.
|
|
24
|
+
* {@link customizationIssueMessage} resolves them here.
|
|
29
25
|
*
|
|
30
|
-
* EC-
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* EC-690: located where the developer fixes them — the project config's own issues
|
|
27
|
+
* (EC-479, point `project`) in `elytra.config.ts`, every other point (the rail, field
|
|
28
|
+
* renderers, app views, reuse, the AI endpoint) in the studio config (`defineStudio`).
|
|
33
29
|
*/
|
|
34
30
|
export declare function customizationItems(issues: readonly CustomizationIssue[]): NavIssueItem[];
|
|
35
31
|
/**
|
|
36
|
-
* URL-map validation issues (EC-469), as
|
|
32
|
+
* URL-map validation issues (EC-469), as findings.
|
|
37
33
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
34
|
+
* EC-690: the localized plain-language label (EC-192) is the row's `title`, the detail its
|
|
35
|
+
* `message` — the two used to be joined into one sentence. A redirect finding is located at
|
|
36
|
+
* its row in the redirects rail (by record kind), a route finding at `elytra.config.ts`.
|
|
41
37
|
*
|
|
42
|
-
* EC-493: `severity` rides along
|
|
43
|
-
*
|
|
44
|
-
* `warning` for a nested pair FROM ONE FUNCTION, and EC-488 re-derived
|
|
45
|
-
* `shadowedMountSpaces` to `warning` precisely because the fall-through made it
|
|
46
|
-
* conditional. Both land in this list.
|
|
38
|
+
* EC-493: `severity` rides along — EC-482's `hierarchyMountConflicts` emits `error` and
|
|
39
|
+
* `warning` from one function, and both land here.
|
|
47
40
|
*/
|
|
48
41
|
export declare function routeIssueItems(issues: readonly CmsValidationIssue[]): NavIssueItem[];
|
|
49
42
|
/** The stable key of the one orphaned-collections finding (EC-606). */
|
|
50
43
|
export declare const ORPHANED_COLLECTIONS_ISSUE_KEY = "orphaned-collections";
|
|
51
44
|
/**
|
|
52
45
|
* EC-606 — stored documents whose collection the project does not declare, as ONE
|
|
53
|
-
*
|
|
46
|
+
* finding (or none).
|
|
54
47
|
*
|
|
55
48
|
* Session hydration keeps only the declared collections' documents and counts the
|
|
56
49
|
* rest (`HydratedCms.orphans`, via core's `orphanedCollections`). Before EC-606 those
|
|
57
50
|
* rows were dropped with no trace, so renaming a collection id in config made its
|
|
58
51
|
* documents vanish from the studio. One item for the whole list: it is one condition
|
|
59
|
-
* with one remedy, and N
|
|
52
|
+
* with one remedy, and N rows would crowd out the rest.
|
|
60
53
|
*
|
|
61
54
|
* `warning`, not `error`: nothing is lost (the rows are stored, untouched) and nothing
|
|
62
55
|
* the editor does is blocked. The message names only one remedy, declaring the
|
|
@@ -66,23 +59,3 @@ export declare const ORPHANED_COLLECTIONS_ISSUE_KEY = "orphaned-collections";
|
|
|
66
59
|
* that has one: those rows are still on the public delivery snapshot.
|
|
67
60
|
*/
|
|
68
61
|
export declare function orphanedCollectionItems(orphans: readonly OrphanedCollection[]): NavIssueItem[];
|
|
69
|
-
/**
|
|
70
|
-
* Visible project-level issues at the rail foot — the seam's "never silent" (EC-236,
|
|
71
|
-
* extended by EC-467's reuse issues, EC-469's URL-map issues, EC-479's config
|
|
72
|
-
* issues and EC-606's orphaned collections).
|
|
73
|
-
*
|
|
74
|
-
* EC-493: an `error` and a `warning` render distinguishably here, in the treatment the
|
|
75
|
-
* document Issues pane already uses. Each finding is its own tinted card (as in the
|
|
76
|
-
* pane) rather than one shared amber box, because a shared box cannot express a mixed
|
|
77
|
-
* list; collapsed, the single glyph follows {@link highestIssueSeverity}. EC-507 moved
|
|
78
|
-
* that treatment to `components/issue-severity.ts`, the one home all three surfaces
|
|
79
|
-
* import from.
|
|
80
|
-
*
|
|
81
|
-
* `data-nav-issue-severity` is on each card (and on the collapsed glyph) as the
|
|
82
|
-
* machine-readable form of the same distinction — what the smoke test pins, and what an
|
|
83
|
-
* agent inspecting the DOM reads without decoding Tailwind classes.
|
|
84
|
-
*/
|
|
85
|
-
export declare function NavIssues({ issues, collapsed }: {
|
|
86
|
-
issues: NavIssueItem[];
|
|
87
|
-
collapsed: boolean;
|
|
88
|
-
}): import("react").JSX.Element;
|
|
@@ -1,60 +1,63 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { cn } from '../lib/utils.js';
|
|
3
1
|
import { translate } from '../lib/i18n/index.js';
|
|
4
|
-
import { PROJECT_CONFIG_FILE } from '../lib/workspace/index.js';
|
|
2
|
+
import { PROJECT_CONFIG_FILE, PROJECT_CONFIG_POINT } from '../lib/workspace/index.js';
|
|
5
3
|
import { cmsIssueLabel, cmsValidationIssueMessage, customizationIssueMessage, } from '../lib/persona/labels.js';
|
|
6
|
-
import {
|
|
7
|
-
import { highestIssueSeverity, issueSeverityIcon, issueSeverityStyle } from './issue-severity.js';
|
|
4
|
+
import { routeIssueLocation } from './notifications/notice-model.js';
|
|
8
5
|
/**
|
|
9
|
-
* Customization issues, as
|
|
6
|
+
* Customization issues, as findings — localized (EC-478). The producers
|
|
10
7
|
* (`studio-core/customization.ts`, `default-modules.tsx`, `builder/reuse.ts`) emit a
|
|
11
8
|
* catalog key + identifier values rather than a sentence, because the EC-236 seam
|
|
12
9
|
* lives in `packages/core` and may not import the studio catalog;
|
|
13
|
-
* {@link customizationIssueMessage} resolves them here
|
|
14
|
-
* the active locale instead of half of it.
|
|
10
|
+
* {@link customizationIssueMessage} resolves them here.
|
|
15
11
|
*
|
|
16
|
-
* EC-
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* EC-690: located where the developer fixes them — the project config's own issues
|
|
13
|
+
* (EC-479, point `project`) in `elytra.config.ts`, every other point (the rail, field
|
|
14
|
+
* renderers, app views, reuse, the AI endpoint) in the studio config (`defineStudio`).
|
|
19
15
|
*/
|
|
20
16
|
export function customizationItems(issues) {
|
|
21
17
|
return issues.map((issue, index) => ({
|
|
22
18
|
key: `${issue.point}:${issue.id ?? index}`,
|
|
23
19
|
message: customizationIssueMessage(issue),
|
|
24
20
|
severity: issue.severity,
|
|
21
|
+
location: {
|
|
22
|
+
kind: 'config',
|
|
23
|
+
declaredIn: issue.point === PROJECT_CONFIG_POINT ? 'project' : 'studio',
|
|
24
|
+
...(issue.id !== undefined ? { entry: issue.id } : {}),
|
|
25
|
+
},
|
|
25
26
|
}));
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
|
-
* URL-map validation issues (EC-469), as
|
|
29
|
+
* URL-map validation issues (EC-469), as findings.
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* EC-690: the localized plain-language label (EC-192) is the row's `title`, the detail its
|
|
32
|
+
* `message` — the two used to be joined into one sentence. A redirect finding is located at
|
|
33
|
+
* its row in the redirects rail (by record kind), a route finding at `elytra.config.ts`.
|
|
33
34
|
*
|
|
34
|
-
* EC-493: `severity` rides along
|
|
35
|
-
*
|
|
36
|
-
* `warning` for a nested pair FROM ONE FUNCTION, and EC-488 re-derived
|
|
37
|
-
* `shadowedMountSpaces` to `warning` precisely because the fall-through made it
|
|
38
|
-
* conditional. Both land in this list.
|
|
35
|
+
* EC-493: `severity` rides along — EC-482's `hierarchyMountConflicts` emits `error` and
|
|
36
|
+
* `warning` from one function, and both land here.
|
|
39
37
|
*/
|
|
40
38
|
export function routeIssueItems(issues) {
|
|
41
|
-
return issues.map((issue, index) =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
return issues.map((issue, index) => {
|
|
40
|
+
const location = routeIssueLocation(issue);
|
|
41
|
+
return {
|
|
42
|
+
key: `route-issue:${issue.code}:${issue.path.join('/') || index}`,
|
|
43
|
+
title: cmsIssueLabel(issue.code, issue.severity),
|
|
44
|
+
message: cmsValidationIssueMessage(issue),
|
|
45
|
+
severity: issue.severity,
|
|
46
|
+
...(location !== undefined ? { location } : {}),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
46
49
|
}
|
|
47
50
|
/** The stable key of the one orphaned-collections finding (EC-606). */
|
|
48
51
|
export const ORPHANED_COLLECTIONS_ISSUE_KEY = 'orphaned-collections';
|
|
49
52
|
/**
|
|
50
53
|
* EC-606 — stored documents whose collection the project does not declare, as ONE
|
|
51
|
-
*
|
|
54
|
+
* finding (or none).
|
|
52
55
|
*
|
|
53
56
|
* Session hydration keeps only the declared collections' documents and counts the
|
|
54
57
|
* rest (`HydratedCms.orphans`, via core's `orphanedCollections`). Before EC-606 those
|
|
55
58
|
* rows were dropped with no trace, so renaming a collection id in config made its
|
|
56
59
|
* documents vanish from the studio. One item for the whole list: it is one condition
|
|
57
|
-
* with one remedy, and N
|
|
60
|
+
* with one remedy, and N rows would crowd out the rest.
|
|
58
61
|
*
|
|
59
62
|
* `warning`, not `error`: nothing is lost (the rows are stored, untouched) and nothing
|
|
60
63
|
* the editor does is blocked. The message names only one remedy, declaring the
|
|
@@ -76,39 +79,8 @@ export function orphanedCollectionItems(orphans) {
|
|
|
76
79
|
key: ORPHANED_COLLECTIONS_ISSUE_KEY,
|
|
77
80
|
message: translate('shell.nav.orphanedCollections', { collections, file: PROJECT_CONFIG_FILE }),
|
|
78
81
|
severity: 'warning',
|
|
82
|
+
location: { kind: 'config', declaredIn: 'project' },
|
|
79
83
|
},
|
|
80
84
|
];
|
|
81
85
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Visible project-level issues at the rail foot — the seam's "never silent" (EC-236,
|
|
84
|
-
* extended by EC-467's reuse issues, EC-469's URL-map issues, EC-479's config
|
|
85
|
-
* issues and EC-606's orphaned collections).
|
|
86
|
-
*
|
|
87
|
-
* EC-493: an `error` and a `warning` render distinguishably here, in the treatment the
|
|
88
|
-
* document Issues pane already uses. Each finding is its own tinted card (as in the
|
|
89
|
-
* pane) rather than one shared amber box, because a shared box cannot express a mixed
|
|
90
|
-
* list; collapsed, the single glyph follows {@link highestIssueSeverity}. EC-507 moved
|
|
91
|
-
* that treatment to `components/issue-severity.ts`, the one home all three surfaces
|
|
92
|
-
* import from.
|
|
93
|
-
*
|
|
94
|
-
* `data-nav-issue-severity` is on each card (and on the collapsed glyph) as the
|
|
95
|
-
* machine-readable form of the same distinction — what the smoke test pins, and what an
|
|
96
|
-
* agent inspecting the DOM reads without decoding Tailwind classes.
|
|
97
|
-
*/
|
|
98
|
-
export function NavIssues({ issues, collapsed }) {
|
|
99
|
-
if (collapsed) {
|
|
100
|
-
const severity = highestIssueSeverity(issues);
|
|
101
|
-
const Icon = issueSeverityIcon(severity);
|
|
102
|
-
return (_jsx("div", { className: cn('grid h-9 place-items-center', issueSeverityStyle(severity).text), "data-nav-issue-severity": severity, title: issues.map((issue) => issue.message).join('\n'), children: _jsx(Icon, { className: "size-4", "aria-label": translate('shell.nav.issuesCount', { count: issues.length }) }) }));
|
|
103
|
-
}
|
|
104
|
-
return (_jsx(Stack, { gap: "1", className: "mb-1", children: issues.map((issue) => (_jsx(NavIssueCard, { issue: issue }, issue.key))) }));
|
|
105
|
-
}
|
|
106
|
-
/** One finding at the rail foot, tinted + glyphed by its severity. */
|
|
107
|
-
function NavIssueCard({ issue }) {
|
|
108
|
-
const style = issueSeverityStyle(issue.severity);
|
|
109
|
-
const Icon = issueSeverityIcon(issue.severity);
|
|
110
|
-
return (_jsxs(Row, { gap: "1", align: "start",
|
|
111
|
-
// `gap-1.5` (the pre-EC-493 icon/text spacing) merged over the primitive's `gap-1`.
|
|
112
|
-
className: cn('gap-1.5 rounded-md border px-2 py-1.5', style.card, style.text), "data-nav-issue-severity": issue.severity, title: issue.message, children: [_jsx(Icon, { className: "mt-0.5 size-3.5 shrink-0", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "min-w-0 leading-snug", children: issue.message })] }));
|
|
113
|
-
}
|
|
114
86
|
//# sourceMappingURL=nav-issues.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav-issues.js","sourceRoot":"","sources":["../../src/components/nav-issues.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { CmsValidationIssue, OrphanedCollection } from '@elytracms/core/cms-core'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\nimport { cn } from '../lib/utils'\nimport { translate } from '../lib/i18n'\nimport { PROJECT_CONFIG_FILE } from '../lib/workspace'\nimport {\n cmsIssueLabel,\n cmsValidationIssueMessage,\n customizationIssueMessage,\n} from '../lib/persona/labels'\nimport { Row, Stack, Text } from '@elytracms/studio/ui'\nimport { highestIssueSeverity, issueSeverityIcon, issueSeverityStyle } from './issue-severity'\nimport type { IssueSeverity } from './issue-severity'\n\n/**\n * What the rail foot needs to render one finding: a stable key, a message, and the\n * finding's severity. The least common shape across its contributing sources —\n * customization issues (`CustomizationIssue`) and URL-map validation\n * (`CmsValidationIssue`, EC-469) — so a new source joins by mapping to this, not by\n * pretending to be a customization issue.\n *\n * `severity` was added by EC-493: both mappers used to drop it and the widget painted\n * every finding amber, which stopped being a style nit once EC-488 re-derived\n * `shadowedMountSpaces` from `error` to `warning` ON THE REASONING that the two mean\n * different things, and EC-482 shipped one producer (`hierarchyMountConflicts`)\n * emitting both into this very list.\n */\nexport interface NavIssueItem {\n key: string\n message: string\n severity: IssueSeverity\n}\n\n/**\n * Customization issues, as rail-foot items — localized (EC-478). The producers\n * (`studio-core/customization.ts`, `default-modules.tsx`, `builder/reuse.ts`) emit a\n * catalog key + identifier values rather than a sentence, because the EC-236 seam\n * lives in `packages/core` and may not import the studio catalog;\n * {@link customizationIssueMessage} resolves them here, so this list renders wholly in\n * the active locale instead of half of it.\n *\n * EC-493: `severity` rides along. It used to be dropped here, which is half of why an\n * error and a warning were indistinguishable at the foot — `CustomizationIssue` has\n * carried the field since EC-236, the mapper simply did not read it.\n */\nexport function customizationItems(issues: readonly CustomizationIssue[]): NavIssueItem[] {\n return issues.map((issue, index) => ({\n key: `${issue.point}:${issue.id ?? index}`,\n message: customizationIssueMessage(issue),\n severity: issue.severity,\n }))\n}\n\n/**\n * URL-map validation issues (EC-469), as rail-foot items.\n *\n * Copy follows the issues-pane convention (EC-192): the localized plain-language label,\n * then the detail — both localized since EC-487, which gave `CmsValidationIssue` the\n * same catalog-reference carriage EC-478 gave `CustomizationIssue`.\n *\n * EC-493: `severity` rides along, the other half of the fix. It matters most here:\n * EC-482's `hierarchyMountConflicts` emits `error` for two mounts on the same path and\n * `warning` for a nested pair FROM ONE FUNCTION, and EC-488 re-derived\n * `shadowedMountSpaces` to `warning` precisely because the fall-through made it\n * conditional. Both land in this list.\n */\nexport function routeIssueItems(issues: readonly CmsValidationIssue[]): NavIssueItem[] {\n return issues.map((issue, index) => ({\n key: `route-issue:${issue.code}:${issue.path.join('/') || index}`,\n message: `${cmsIssueLabel(issue.code, issue.severity)} — ${cmsValidationIssueMessage(issue)}`,\n severity: issue.severity,\n }))\n}\n\n/** The stable key of the one orphaned-collections finding (EC-606). */\nexport const ORPHANED_COLLECTIONS_ISSUE_KEY = 'orphaned-collections'\n\n/**\n * EC-606 — stored documents whose collection the project does not declare, as ONE\n * rail-foot item (or none).\n *\n * Session hydration keeps only the declared collections' documents and counts the\n * rest (`HydratedCms.orphans`, via core's `orphanedCollections`). Before EC-606 those\n * rows were dropped with no trace, so renaming a collection id in config made its\n * documents vanish from the studio. One item for the whole list: it is one condition\n * with one remedy, and N cards would crowd out the rail.\n *\n * `warning`, not `error`: nothing is lost (the rows are stored, untouched) and nothing\n * the editor does is blocked. The message names only one remedy, declaring the\n * collection again or restoring its old id (B6). `documents.clear` plus a re-seed\n * would also remove the warning, but for a renamed collection on a live site that is\n * data loss, so the message does not suggest it. A published count rides each entry\n * that has one: those rows are still on the public delivery snapshot.\n */\nexport function orphanedCollectionItems(orphans: readonly OrphanedCollection[]): NavIssueItem[] {\n if (orphans.length === 0) return []\n const collections = orphans\n .map(({ collection, count, published }) =>\n published > 0\n ? translate('shell.nav.orphanedCollections.itemPublished', { collection, count, published })\n : translate('shell.nav.orphanedCollections.item', { collection, count }),\n )\n .join(', ')\n return [\n {\n key: ORPHANED_COLLECTIONS_ISSUE_KEY,\n message: translate('shell.nav.orphanedCollections', { collections, file: PROJECT_CONFIG_FILE }),\n severity: 'warning',\n },\n ]\n}\n\n/**\n * Visible project-level issues at the rail foot — the seam's \"never silent\" (EC-236,\n * extended by EC-467's reuse issues, EC-469's URL-map issues, EC-479's config\n * issues and EC-606's orphaned collections).\n *\n * EC-493: an `error` and a `warning` render distinguishably here, in the treatment the\n * document Issues pane already uses. Each finding is its own tinted card (as in the\n * pane) rather than one shared amber box, because a shared box cannot express a mixed\n * list; collapsed, the single glyph follows {@link highestIssueSeverity}. EC-507 moved\n * that treatment to `components/issue-severity.ts`, the one home all three surfaces\n * import from.\n *\n * `data-nav-issue-severity` is on each card (and on the collapsed glyph) as the\n * machine-readable form of the same distinction — what the smoke test pins, and what an\n * agent inspecting the DOM reads without decoding Tailwind classes.\n */\nexport function NavIssues({ issues, collapsed }: { issues: NavIssueItem[]; collapsed: boolean }) {\n if (collapsed) {\n const severity = highestIssueSeverity(issues)\n const Icon = issueSeverityIcon(severity)\n return (\n <div\n className={cn('grid h-9 place-items-center', issueSeverityStyle(severity).text)}\n data-nav-issue-severity={severity}\n title={issues.map((issue) => issue.message).join('\\n')}\n >\n <Icon\n className=\"size-4\"\n aria-label={translate('shell.nav.issuesCount', { count: issues.length })}\n />\n </div>\n )\n }\n return (\n <Stack gap=\"1\" className=\"mb-1\">\n {issues.map((issue) => (\n <NavIssueCard key={issue.key} issue={issue} />\n ))}\n </Stack>\n )\n}\n\n/** One finding at the rail foot, tinted + glyphed by its severity. */\nfunction NavIssueCard({ issue }: { issue: NavIssueItem }) {\n const style = issueSeverityStyle(issue.severity)\n const Icon = issueSeverityIcon(issue.severity)\n return (\n <Row\n gap=\"1\"\n align=\"start\"\n // `gap-1.5` (the pre-EC-493 icon/text spacing) merged over the primitive's `gap-1`.\n className={cn('gap-1.5 rounded-md border px-2 py-1.5', style.card, style.text)}\n data-nav-issue-severity={issue.severity}\n title={issue.message}\n >\n <Icon className=\"mt-0.5 size-3.5 shrink-0\" aria-hidden />\n <Text size=\"xs\" className=\"min-w-0 leading-snug\">\n {issue.message}\n </Text>\n </Row>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"nav-issues.js","sourceRoot":"","sources":["../../src/components/nav-issues.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAC5E,OAAO,EACL,aAAa,EACb,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAA;AAG9B,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AAiBjE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAqC;IACtE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACnC,GAAG,EAAE,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,EAAE,IAAI,KAAK,EAAE;QAC1C,OAAO,EAAE,yBAAyB,CAAC,KAAK,CAAC;QACzC,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,QAAQ,EAAE;YACR,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE,KAAK,CAAC,KAAK,KAAK,oBAAoB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ;YACvE,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvD;KACF,CAAC,CAAC,CAAA;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,MAAqC;IACnE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACjC,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAA;QAC1C,OAAO;YACL,GAAG,EAAE,eAAe,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,EAAE;YACjE,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC;YAChD,OAAO,EAAE,yBAAyB,CAAC,KAAK,CAAC;YACzC,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAChD,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,MAAM,8BAA8B,GAAG,sBAAsB,CAAA;AAEpE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAsC;IAC5E,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACnC,MAAM,WAAW,GAAG,OAAO;SACxB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,CACxC,SAAS,GAAG,CAAC;QACX,CAAC,CAAC,SAAS,CAAC,6CAA6C,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;QAC5F,CAAC,CAAC,SAAS,CAAC,oCAAoC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAC3E;SACA,IAAI,CAAC,IAAI,CAAC,CAAA;IACb,OAAO;QACL;YACE,GAAG,EAAE,8BAA8B;YACnC,OAAO,EAAE,SAAS,CAAC,+BAA+B,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,mBAAmB,EAAE,CAAC;YAC/F,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE;SACpD;KACF,CAAA;AACH,CAAC","sourcesContent":["import type { CmsValidationIssue, OrphanedCollection } from '@elytracms/core/cms-core'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\nimport { translate } from '../lib/i18n'\nimport { PROJECT_CONFIG_FILE, PROJECT_CONFIG_POINT } from '../lib/workspace'\nimport {\n cmsIssueLabel,\n cmsValidationIssueMessage,\n customizationIssueMessage,\n} from '../lib/persona/labels'\nimport type { IssueSeverity } from './issue-severity'\nimport type { NavIssue } from './nav-issues-registry'\nimport { routeIssueLocation } from './notifications/notice-model'\n\n/**\n * The engine's own project-level findings, mapped to the one shape the issues tray lists\n * (EC-690; before it, the rail foot's cards). The least common shape across the sources —\n * customization issues (`CustomizationIssue`) and URL-map validation (`CmsValidationIssue`,\n * EC-469) — so a new source joins by mapping to this, not by pretending to be a\n * customization issue.\n *\n * `severity` was added by EC-493: both mappers used to drop it and the widget painted\n * every finding amber. EC-690 added `title` (the short label a row leads with) and\n * `location` (where \"Go to\" leads, or the file the row names).\n */\nexport type NavIssueItem = Required<Pick<NavIssue, 'key' | 'message'>> & {\n severity: IssueSeverity\n} & Pick<NavIssue, 'title' | 'location'>\n\n/**\n * Customization issues, as findings — localized (EC-478). The producers\n * (`studio-core/customization.ts`, `default-modules.tsx`, `builder/reuse.ts`) emit a\n * catalog key + identifier values rather than a sentence, because the EC-236 seam\n * lives in `packages/core` and may not import the studio catalog;\n * {@link customizationIssueMessage} resolves them here.\n *\n * EC-690: located where the developer fixes them — the project config's own issues\n * (EC-479, point `project`) in `elytra.config.ts`, every other point (the rail, field\n * renderers, app views, reuse, the AI endpoint) in the studio config (`defineStudio`).\n */\nexport function customizationItems(issues: readonly CustomizationIssue[]): NavIssueItem[] {\n return issues.map((issue, index) => ({\n key: `${issue.point}:${issue.id ?? index}`,\n message: customizationIssueMessage(issue),\n severity: issue.severity,\n location: {\n kind: 'config',\n declaredIn: issue.point === PROJECT_CONFIG_POINT ? 'project' : 'studio',\n ...(issue.id !== undefined ? { entry: issue.id } : {}),\n },\n }))\n}\n\n/**\n * URL-map validation issues (EC-469), as findings.\n *\n * EC-690: the localized plain-language label (EC-192) is the row's `title`, the detail its\n * `message` — the two used to be joined into one sentence. A redirect finding is located at\n * its row in the redirects rail (by record kind), a route finding at `elytra.config.ts`.\n *\n * EC-493: `severity` rides along — EC-482's `hierarchyMountConflicts` emits `error` and\n * `warning` from one function, and both land here.\n */\nexport function routeIssueItems(issues: readonly CmsValidationIssue[]): NavIssueItem[] {\n return issues.map((issue, index) => {\n const location = routeIssueLocation(issue)\n return {\n key: `route-issue:${issue.code}:${issue.path.join('/') || index}`,\n title: cmsIssueLabel(issue.code, issue.severity),\n message: cmsValidationIssueMessage(issue),\n severity: issue.severity,\n ...(location !== undefined ? { location } : {}),\n }\n })\n}\n\n/** The stable key of the one orphaned-collections finding (EC-606). */\nexport const ORPHANED_COLLECTIONS_ISSUE_KEY = 'orphaned-collections'\n\n/**\n * EC-606 — stored documents whose collection the project does not declare, as ONE\n * finding (or none).\n *\n * Session hydration keeps only the declared collections' documents and counts the\n * rest (`HydratedCms.orphans`, via core's `orphanedCollections`). Before EC-606 those\n * rows were dropped with no trace, so renaming a collection id in config made its\n * documents vanish from the studio. One item for the whole list: it is one condition\n * with one remedy, and N rows would crowd out the rest.\n *\n * `warning`, not `error`: nothing is lost (the rows are stored, untouched) and nothing\n * the editor does is blocked. The message names only one remedy, declaring the\n * collection again or restoring its old id (B6). `documents.clear` plus a re-seed\n * would also remove the warning, but for a renamed collection on a live site that is\n * data loss, so the message does not suggest it. A published count rides each entry\n * that has one: those rows are still on the public delivery snapshot.\n */\nexport function orphanedCollectionItems(orphans: readonly OrphanedCollection[]): NavIssueItem[] {\n if (orphans.length === 0) return []\n const collections = orphans\n .map(({ collection, count, published }) =>\n published > 0\n ? translate('shell.nav.orphanedCollections.itemPublished', { collection, count, published })\n : translate('shell.nav.orphanedCollections.item', { collection, count }),\n )\n .join(', ')\n return [\n {\n key: ORPHANED_COLLECTIONS_ISSUE_KEY,\n message: translate('shell.nav.orphanedCollections', { collections, file: PROJECT_CONFIG_FILE }),\n severity: 'warning',\n location: { kind: 'config', declaredIn: 'project' },\n },\n ]\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Project } from '@elytracms/core/studio-core';
|
|
3
|
+
import type { ToastItem } from '../../lib/notices/toast-store.js';
|
|
4
|
+
import type { IssueSeverity } from '../issue-severity.js';
|
|
5
|
+
import type { Finding, GoToTarget } from './notice-model.js';
|
|
6
|
+
/**
|
|
7
|
+
* EC-690 — the studio's ONE notification-and-issue surface, in the shell header: a badge
|
|
8
|
+
* counting what needs attention by severity (EC-507's treatment), opening a tray that lists
|
|
9
|
+
* every live notice and every persistent finding with where it is ("Go to" a rail row or a
|
|
10
|
+
* document; the file or setting for code). Owner ruling (a), 2026-10-28: ONE badge, in the
|
|
11
|
+
* header — the rail foot shows none (and no finding text at all any more).
|
|
12
|
+
*
|
|
13
|
+
* The tray is a Radix popover bounded to the content area (`boundary` = `<main>`), so it can
|
|
14
|
+
* never cover the section rail at any width. Notices also PEEK in the content area's
|
|
15
|
+
* bottom-right ({@link NoticePeek}) when they are raised.
|
|
16
|
+
*/
|
|
17
|
+
export declare function NoticeCenter({ project, boundary }: {
|
|
18
|
+
project: Project;
|
|
19
|
+
boundary: Element | null;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
/** What the badge counts: every finding, plus every live error notice (an error stays until dismissed). */
|
|
22
|
+
export declare function noticeCounts(findings: readonly Finding[], notices: readonly ToastItem[]): Record<IssueSeverity, number>;
|
|
23
|
+
/** "2 errors, 1 warning" — or "No issues". */
|
|
24
|
+
export declare function noticeSummary(counts: Record<IssueSeverity, number>): string;
|
|
25
|
+
/** The badge's face: one count per severity present, or a muted "all clear". */
|
|
26
|
+
export declare function NoticeBadgeContent({ counts }: {
|
|
27
|
+
counts: Record<IssueSeverity, number>;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
29
|
+
/**
|
|
30
|
+
* The tray's body: live notices first (newest first), then the findings in the one order.
|
|
31
|
+
* Pure over its inputs — `target` resolves a finding's location, so a test renders it without
|
|
32
|
+
* a router (a finding with no link target renders no `Link`).
|
|
33
|
+
*/
|
|
34
|
+
export declare function NoticeTray({ projectId, findings, notices, target, onNavigate, }: {
|
|
35
|
+
projectId: string;
|
|
36
|
+
findings: readonly Finding[];
|
|
37
|
+
notices: readonly ToastItem[];
|
|
38
|
+
target: (finding: Finding) => GoToTarget | null;
|
|
39
|
+
onNavigate?: () => void;
|
|
40
|
+
}): import("react").JSX.Element;
|
|
41
|
+
/**
|
|
42
|
+
* One persistent finding: EC-507's card + glyph for its severity, a bold title, the message
|
|
43
|
+
* (clamped, full text on hover; long unbreakable paths wrap — `overflow-wrap:anywhere`), and
|
|
44
|
+
* its location action. No dismiss and no collapse (owner ruling (e)): it leaves when it stops
|
|
45
|
+
* being true.
|
|
46
|
+
*/
|
|
47
|
+
export declare function FindingRow({ finding, action }: {
|
|
48
|
+
finding: Finding;
|
|
49
|
+
action?: ReactNode;
|
|
50
|
+
}): import("react").JSX.Element;
|
|
51
|
+
/** One transient notice: EC-507's palette for an error, the success tone otherwise; always dismissable. */
|
|
52
|
+
export declare function NoticeRow({ notice }: {
|
|
53
|
+
notice: ToastItem;
|
|
54
|
+
}): import("react").JSX.Element;
|
|
55
|
+
/**
|
|
56
|
+
* New notices, peeking for 5 s in the content area's bottom-right — inside `<main>`, so they
|
|
57
|
+
* never cover the section rail (the old `Toaster` was fixed to the viewport and did, at
|
|
58
|
+
* narrow widths), and not at its top-right, where the editor's Publish button and save status
|
|
59
|
+
* sit (critic point 8). An error leaves the peek but stays in the tray until dismissed.
|
|
60
|
+
*/
|
|
61
|
+
export declare function NoticePeek(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { BellDot, CircleCheck, X } from 'lucide-react';
|
|
4
|
+
import { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router';
|
|
5
|
+
import { Button, Popover, PopoverContent, PopoverTrigger, Stack, Text } from '@elytracms/studio/ui';
|
|
6
|
+
import { cn } from '../../lib/utils.js';
|
|
7
|
+
import { translate } from '../../lib/i18n/index.js';
|
|
8
|
+
import { PROJECT_CONFIG_FILE } from '../../lib/workspace/index.js';
|
|
9
|
+
import { studioToasts } from '../../lib/notices/toast-store.js';
|
|
10
|
+
import { issueSeverityIcon, issueSeverityStyle } from '../issue-severity.js';
|
|
11
|
+
import { useStudioNav } from '../app-views/sidebar.js';
|
|
12
|
+
import { countBySeverity, goToTarget } from './notice-model.js';
|
|
13
|
+
import { useShellFindings, ViewIssuesProbes } from './shell-findings.js';
|
|
14
|
+
/**
|
|
15
|
+
* EC-690 — the studio's ONE notification-and-issue surface, in the shell header: a badge
|
|
16
|
+
* counting what needs attention by severity (EC-507's treatment), opening a tray that lists
|
|
17
|
+
* every live notice and every persistent finding with where it is ("Go to" a rail row or a
|
|
18
|
+
* document; the file or setting for code). Owner ruling (a), 2026-10-28: ONE badge, in the
|
|
19
|
+
* header — the rail foot shows none (and no finding text at all any more).
|
|
20
|
+
*
|
|
21
|
+
* The tray is a Radix popover bounded to the content area (`boundary` = `<main>`), so it can
|
|
22
|
+
* never cover the section rail at any width. Notices also PEEK in the content area's
|
|
23
|
+
* bottom-right ({@link NoticePeek}) when they are raised.
|
|
24
|
+
*/
|
|
25
|
+
export function NoticeCenter({ project, boundary }) {
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
const findings = useShellFindings(project);
|
|
28
|
+
const notices = useNotices();
|
|
29
|
+
const nav = useStudioNav(project);
|
|
30
|
+
const views = nav.registry.views;
|
|
31
|
+
const counts = noticeCounts(findings, notices);
|
|
32
|
+
const summary = noticeSummary(counts);
|
|
33
|
+
return (_jsxs(_Fragment, { children: [_jsx(ViewIssuesProbes, { project: project }), _jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-haspopup": "dialog", "aria-label": translate('shell.notices.open', { summary }), title: summary, "data-testid": "notice-badge", "data-notice-errors": counts.error, "data-notice-warnings": counts.warning, className: "flex h-7 shrink-0 items-center gap-1.5 rounded-md px-1.5 text-xs font-medium transition-colors hover:bg-muted", children: _jsx(NoticeBadgeContent, { counts: counts }) }) }), _jsx(PopoverContent, { align: "end", sideOffset: 8, collisionBoundary: boundary ?? undefined, collisionPadding: 8, "aria-label": translate('shell.notices.title'), "data-testid": "notice-tray", className: "flex max-h-[min(32rem,var(--radix-popover-content-available-height))] w-[min(26rem,var(--radix-popover-content-available-width))] flex-col gap-0 overflow-hidden p-0", children: _jsx(NoticeTray, { projectId: project.id, findings: findings, notices: notices, target: (finding) => (finding.location ? goToTarget(finding.location, views) : null), onNavigate: () => setOpen(false) }) })] })] }));
|
|
34
|
+
}
|
|
35
|
+
/** The live transient notices (the toast channel). */
|
|
36
|
+
function useNotices() {
|
|
37
|
+
return useSyncExternalStore(studioToasts.subscribe, studioToasts.getSnapshot, studioToasts.getSnapshot);
|
|
38
|
+
}
|
|
39
|
+
/** What the badge counts: every finding, plus every live error notice (an error stays until dismissed). */
|
|
40
|
+
export function noticeCounts(findings, notices) {
|
|
41
|
+
return countBySeverity([
|
|
42
|
+
...findings,
|
|
43
|
+
...notices.filter((notice) => notice.variant === 'error').map(() => ({ severity: 'error' })),
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
/** "2 errors, 1 warning" — or "No issues". */
|
|
47
|
+
export function noticeSummary(counts) {
|
|
48
|
+
const parts = [];
|
|
49
|
+
if (counts.error > 0)
|
|
50
|
+
parts.push(translate(counts.error === 1 ? 'shell.notices.errors.one' : 'shell.notices.errors.other', { count: counts.error }));
|
|
51
|
+
if (counts.warning > 0) {
|
|
52
|
+
parts.push(translate(counts.warning === 1 ? 'shell.notices.warnings.one' : 'shell.notices.warnings.other', { count: counts.warning }));
|
|
53
|
+
}
|
|
54
|
+
return parts.length > 0 ? parts.join(', ') : translate('shell.notices.none');
|
|
55
|
+
}
|
|
56
|
+
/** The badge's face: one count per severity present, or a muted "all clear". */
|
|
57
|
+
export function NoticeBadgeContent({ counts }) {
|
|
58
|
+
if (counts.error === 0 && counts.warning === 0) {
|
|
59
|
+
return _jsx(BellDot, { className: "size-4 text-muted-foreground/60", "aria-hidden": true });
|
|
60
|
+
}
|
|
61
|
+
return (_jsx(_Fragment, { children: ['error', 'warning'].map((severity) => {
|
|
62
|
+
if (counts[severity] === 0)
|
|
63
|
+
return null;
|
|
64
|
+
const Icon = issueSeverityIcon(severity);
|
|
65
|
+
return (_jsxs("span", { className: cn('flex items-center gap-0.5', issueSeverityStyle(severity).text), "data-notice-count-severity": severity, children: [_jsx(Icon, { className: "size-4", "aria-hidden": true }), counts[severity]] }, severity));
|
|
66
|
+
}) }));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The tray's body: live notices first (newest first), then the findings in the one order.
|
|
70
|
+
* Pure over its inputs — `target` resolves a finding's location, so a test renders it without
|
|
71
|
+
* a router (a finding with no link target renders no `Link`).
|
|
72
|
+
*/
|
|
73
|
+
export function NoticeTray({ projectId, findings, notices, target, onNavigate, }) {
|
|
74
|
+
const liveNotices = [...notices].reverse();
|
|
75
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "border-b border-border px-3 py-2", children: _jsx(Text, { size: "sm", weight: "semibold", children: translate('shell.notices.title') }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-2", children: liveNotices.length === 0 && findings.length === 0 ? (_jsx(Text, { size: "sm", tone: "muted", className: "px-1 py-3", "data-testid": "notice-tray-empty", children: translate('shell.notices.empty') })) : (_jsxs("ul", { "aria-label": translate('shell.notices.title'), className: "flex flex-col gap-1", children: [liveNotices.map((notice) => (_jsx("li", { children: _jsx(NoticeRow, { notice: notice }) }, `notice:${notice.id}`))), findings.map((finding) => {
|
|
76
|
+
const goTo = target(finding);
|
|
77
|
+
return (_jsx("li", { children: _jsx(FindingRow, { finding: finding, action: goTo?.kind === 'link' ? (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: goTo.splat }, onClick: onNavigate, className: "text-xs font-medium text-primary underline-offset-2 hover:underline", "data-testid": "finding-go-to", children: translate('shell.notices.goTo') })) : goTo?.kind === 'named' ? (_jsx(NamedLocation, { target: goTo })) : null }) }, finding.id));
|
|
78
|
+
})] })) })] }));
|
|
79
|
+
}
|
|
80
|
+
/** Where a code finding is: the file, or the deployment settings — named, never linked. */
|
|
81
|
+
function NamedLocation({ target }) {
|
|
82
|
+
const label = target.settings
|
|
83
|
+
? translate('shell.notices.deploymentSettings', { settings: target.settings.join(', ') })
|
|
84
|
+
: target.declaredIn === 'studio'
|
|
85
|
+
? translate('shell.notices.declaredInStudio')
|
|
86
|
+
: translate('shell.notices.declaredInProject', { file: PROJECT_CONFIG_FILE });
|
|
87
|
+
return (_jsx("code", { className: "max-w-full rounded bg-muted px-1 py-0.5 font-mono text-[11px] text-muted-foreground [overflow-wrap:anywhere]", "data-testid": "finding-named-location", children: target.entry ? `${label} · ${target.entry}` : label }));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* One persistent finding: EC-507's card + glyph for its severity, a bold title, the message
|
|
91
|
+
* (clamped, full text on hover; long unbreakable paths wrap — `overflow-wrap:anywhere`), and
|
|
92
|
+
* its location action. No dismiss and no collapse (owner ruling (e)): it leaves when it stops
|
|
93
|
+
* being true.
|
|
94
|
+
*/
|
|
95
|
+
export function FindingRow({ finding, action }) {
|
|
96
|
+
const style = issueSeverityStyle(finding.severity);
|
|
97
|
+
const Icon = issueSeverityIcon(finding.severity);
|
|
98
|
+
return (_jsxs("div", { className: cn('flex items-start gap-2 rounded-md border px-2.5 py-2', style.card), "data-notice-severity": finding.severity, "data-finding-source": finding.source, children: [_jsx(Icon, { className: cn('mt-0.5 size-3.5 shrink-0', style.text), "aria-hidden": true }), _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1", children: [finding.title ? (_jsx(Text, { size: "xs", weight: "semibold", className: style.text, children: finding.title })) : null, _jsx(Text, { size: "xs", className: "line-clamp-4 leading-snug [overflow-wrap:anywhere]", title: finding.message, children: finding.message }), action || finding.action ? (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [action, finding.action ? (_jsx(Button, { size: "sm", variant: "outline", className: "h-6 px-2 text-xs", onClick: finding.action.run, children: finding.action.label })) : null] })) : null] })] }));
|
|
99
|
+
}
|
|
100
|
+
/** One transient notice: EC-507's palette for an error, the success tone otherwise; always dismissable. */
|
|
101
|
+
export function NoticeRow({ notice }) {
|
|
102
|
+
const error = notice.variant === 'error';
|
|
103
|
+
const style = issueSeverityStyle('error');
|
|
104
|
+
const Icon = error ? issueSeverityIcon('error') : CircleCheck;
|
|
105
|
+
return (_jsxs("div", { role: error ? 'alert' : 'status', className: cn('flex items-start gap-2 rounded-md border px-2.5 py-2', error ? style.card : 'border-success/30 bg-success/10'), "data-notice-variant": notice.variant, children: [_jsx(Icon, { className: cn('mt-0.5 size-3.5 shrink-0', error ? style.text : 'text-success'), "aria-hidden": true }), _jsx(Text, { size: "xs", className: "min-w-0 flex-1 leading-snug [overflow-wrap:anywhere]", children: notice.message }), _jsx("button", { type: "button", onClick: () => studioToasts.dismiss(notice.id), "aria-label": translate('shell.notices.dismiss'), className: "shrink-0 rounded text-muted-foreground transition-colors hover:text-foreground", "data-testid": "notice-dismiss", children: _jsx(X, { className: "size-3.5", "aria-hidden": true }) })] }));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* New notices, peeking for 5 s in the content area's bottom-right — inside `<main>`, so they
|
|
109
|
+
* never cover the section rail (the old `Toaster` was fixed to the viewport and did, at
|
|
110
|
+
* narrow widths), and not at its top-right, where the editor's Publish button and save status
|
|
111
|
+
* sit (critic point 8). An error leaves the peek but stays in the tray until dismissed.
|
|
112
|
+
*/
|
|
113
|
+
export function NoticePeek() {
|
|
114
|
+
const notices = useNotices().filter((notice) => notice.peeking);
|
|
115
|
+
if (notices.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
return (_jsx("div", { className: "pointer-events-none absolute bottom-3 right-3 z-40 flex w-80 max-w-[calc(100%-1.5rem)] flex-col gap-2", "data-testid": "notice-peek", children: notices.map((notice) => (_jsx("div", { className: "pointer-events-auto rounded-md bg-background shadow-md", children: _jsx(NoticeRow, { notice: notice }) }, notice.id))) }));
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=notice-center.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notice-center.js","sourceRoot":"","sources":["../../../src/components/notifications/notice-center.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState, useSyncExternalStore } from 'react'\nimport type { ReactNode } from 'react'\nimport { BellDot, CircleCheck, X } from 'lucide-react'\nimport { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router'\nimport type { Project } from '@elytracms/core/studio-core'\nimport { Button, Popover, PopoverContent, PopoverTrigger, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '../../lib/utils'\nimport { translate } from '../../lib/i18n'\nimport { PROJECT_CONFIG_FILE } from '../../lib/workspace'\nimport { studioToasts } from '../../lib/notices/toast-store'\nimport type { ToastItem } from '../../lib/notices/toast-store'\nimport { issueSeverityIcon, issueSeverityStyle } from '../issue-severity'\nimport type { IssueSeverity } from '../issue-severity'\nimport { useStudioNav } from '../app-views/sidebar'\nimport { countBySeverity, goToTarget } from './notice-model'\nimport type { Finding, GoToTarget } from './notice-model'\nimport { useShellFindings, ViewIssuesProbes } from './shell-findings'\n\n/**\n * EC-690 — the studio's ONE notification-and-issue surface, in the shell header: a badge\n * counting what needs attention by severity (EC-507's treatment), opening a tray that lists\n * every live notice and every persistent finding with where it is (\"Go to\" a rail row or a\n * document; the file or setting for code). Owner ruling (a), 2026-10-28: ONE badge, in the\n * header — the rail foot shows none (and no finding text at all any more).\n *\n * The tray is a Radix popover bounded to the content area (`boundary` = `<main>`), so it can\n * never cover the section rail at any width. Notices also PEEK in the content area's\n * bottom-right ({@link NoticePeek}) when they are raised.\n */\nexport function NoticeCenter({ project, boundary }: { project: Project; boundary: Element | null }) {\n const [open, setOpen] = useState(false)\n const findings = useShellFindings(project)\n const notices = useNotices()\n const nav = useStudioNav(project)\n const views = nav.registry.views\n const counts = noticeCounts(findings, notices)\n const summary = noticeSummary(counts)\n\n return (\n <>\n <ViewIssuesProbes project={project} />\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-haspopup=\"dialog\"\n aria-label={translate('shell.notices.open', { summary })}\n title={summary}\n data-testid=\"notice-badge\"\n data-notice-errors={counts.error}\n data-notice-warnings={counts.warning}\n className=\"flex h-7 shrink-0 items-center gap-1.5 rounded-md px-1.5 text-xs font-medium transition-colors hover:bg-muted\"\n >\n <NoticeBadgeContent counts={counts} />\n </button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n sideOffset={8}\n collisionBoundary={boundary ?? undefined}\n collisionPadding={8}\n aria-label={translate('shell.notices.title')}\n data-testid=\"notice-tray\"\n className=\"flex max-h-[min(32rem,var(--radix-popover-content-available-height))] w-[min(26rem,var(--radix-popover-content-available-width))] flex-col gap-0 overflow-hidden p-0\"\n >\n <NoticeTray\n projectId={project.id}\n findings={findings}\n notices={notices}\n target={(finding) => (finding.location ? goToTarget(finding.location, views) : null)}\n onNavigate={() => setOpen(false)}\n />\n </PopoverContent>\n </Popover>\n </>\n )\n}\n\n/** The live transient notices (the toast channel). */\nfunction useNotices(): ToastItem[] {\n return useSyncExternalStore(studioToasts.subscribe, studioToasts.getSnapshot, studioToasts.getSnapshot)\n}\n\n/** What the badge counts: every finding, plus every live error notice (an error stays until dismissed). */\nexport function noticeCounts(findings: readonly Finding[], notices: readonly ToastItem[]): Record<IssueSeverity, number> {\n return countBySeverity([\n ...findings,\n ...notices.filter((notice) => notice.variant === 'error').map(() => ({ severity: 'error' as const })),\n ])\n}\n\n/** \"2 errors, 1 warning\" — or \"No issues\". */\nexport function noticeSummary(counts: Record<IssueSeverity, number>): string {\n const parts: string[] = []\n if (counts.error > 0) parts.push(translate(counts.error === 1 ? 'shell.notices.errors.one' : 'shell.notices.errors.other', { count: counts.error }))\n if (counts.warning > 0) {\n parts.push(translate(counts.warning === 1 ? 'shell.notices.warnings.one' : 'shell.notices.warnings.other', { count: counts.warning }))\n }\n return parts.length > 0 ? parts.join(', ') : translate('shell.notices.none')\n}\n\n/** The badge's face: one count per severity present, or a muted \"all clear\". */\nexport function NoticeBadgeContent({ counts }: { counts: Record<IssueSeverity, number> }) {\n if (counts.error === 0 && counts.warning === 0) {\n return <BellDot className=\"size-4 text-muted-foreground/60\" aria-hidden />\n }\n return (\n <>\n {(['error', 'warning'] as const).map((severity) => {\n if (counts[severity] === 0) return null\n const Icon = issueSeverityIcon(severity)\n return (\n <span\n key={severity}\n className={cn('flex items-center gap-0.5', issueSeverityStyle(severity).text)}\n data-notice-count-severity={severity}\n >\n <Icon className=\"size-4\" aria-hidden />\n {counts[severity]}\n </span>\n )\n })}\n </>\n )\n}\n\n/**\n * The tray's body: live notices first (newest first), then the findings in the one order.\n * Pure over its inputs — `target` resolves a finding's location, so a test renders it without\n * a router (a finding with no link target renders no `Link`).\n */\nexport function NoticeTray({\n projectId,\n findings,\n notices,\n target,\n onNavigate,\n}: {\n projectId: string\n findings: readonly Finding[]\n notices: readonly ToastItem[]\n target: (finding: Finding) => GoToTarget | null\n onNavigate?: () => void\n}) {\n const liveNotices = [...notices].reverse()\n return (\n <>\n <div className=\"border-b border-border px-3 py-2\">\n <Text size=\"sm\" weight=\"semibold\">\n {translate('shell.notices.title')}\n </Text>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto p-2\">\n {liveNotices.length === 0 && findings.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"px-1 py-3\" data-testid=\"notice-tray-empty\">\n {translate('shell.notices.empty')}\n </Text>\n ) : (\n <ul aria-label={translate('shell.notices.title')} className=\"flex flex-col gap-1\">\n {liveNotices.map((notice) => (\n <li key={`notice:${notice.id}`}>\n <NoticeRow notice={notice} />\n </li>\n ))}\n {findings.map((finding) => {\n const goTo = target(finding)\n return (\n <li key={finding.id}>\n <FindingRow\n finding={finding}\n action={\n goTo?.kind === 'link' ? (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: goTo.splat }}\n onClick={onNavigate}\n className=\"text-xs font-medium text-primary underline-offset-2 hover:underline\"\n data-testid=\"finding-go-to\"\n >\n {translate('shell.notices.goTo')}\n </Link>\n ) : goTo?.kind === 'named' ? (\n <NamedLocation target={goTo} />\n ) : null\n }\n />\n </li>\n )\n })}\n </ul>\n )}\n </div>\n </>\n )\n}\n\n/** Where a code finding is: the file, or the deployment settings — named, never linked. */\nfunction NamedLocation({ target }: { target: Extract<GoToTarget, { kind: 'named' }> }) {\n const label = target.settings\n ? translate('shell.notices.deploymentSettings', { settings: target.settings.join(', ') })\n : target.declaredIn === 'studio'\n ? translate('shell.notices.declaredInStudio')\n : translate('shell.notices.declaredInProject', { file: PROJECT_CONFIG_FILE })\n return (\n <code\n className=\"max-w-full rounded bg-muted px-1 py-0.5 font-mono text-[11px] text-muted-foreground [overflow-wrap:anywhere]\"\n data-testid=\"finding-named-location\"\n >\n {target.entry ? `${label} · ${target.entry}` : label}\n </code>\n )\n}\n\n/**\n * One persistent finding: EC-507's card + glyph for its severity, a bold title, the message\n * (clamped, full text on hover; long unbreakable paths wrap — `overflow-wrap:anywhere`), and\n * its location action. No dismiss and no collapse (owner ruling (e)): it leaves when it stops\n * being true.\n */\nexport function FindingRow({ finding, action }: { finding: Finding; action?: ReactNode }) {\n const style = issueSeverityStyle(finding.severity)\n const Icon = issueSeverityIcon(finding.severity)\n return (\n <div\n className={cn('flex items-start gap-2 rounded-md border px-2.5 py-2', style.card)}\n data-notice-severity={finding.severity}\n data-finding-source={finding.source}\n >\n <Icon className={cn('mt-0.5 size-3.5 shrink-0', style.text)} aria-hidden />\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n {finding.title ? (\n <Text size=\"xs\" weight=\"semibold\" className={style.text}>\n {finding.title}\n </Text>\n ) : null}\n <Text size=\"xs\" className=\"line-clamp-4 leading-snug [overflow-wrap:anywhere]\" title={finding.message}>\n {finding.message}\n </Text>\n {action || finding.action ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n {action}\n {finding.action ? (\n <Button size=\"sm\" variant=\"outline\" className=\"h-6 px-2 text-xs\" onClick={finding.action.run}>\n {finding.action.label}\n </Button>\n ) : null}\n </div>\n ) : null}\n </Stack>\n </div>\n )\n}\n\n/** One transient notice: EC-507's palette for an error, the success tone otherwise; always dismissable. */\nexport function NoticeRow({ notice }: { notice: ToastItem }) {\n const error = notice.variant === 'error'\n const style = issueSeverityStyle('error')\n const Icon = error ? issueSeverityIcon('error') : CircleCheck\n return (\n <div\n role={error ? 'alert' : 'status'}\n className={cn(\n 'flex items-start gap-2 rounded-md border px-2.5 py-2',\n error ? style.card : 'border-success/30 bg-success/10',\n )}\n data-notice-variant={notice.variant}\n >\n <Icon className={cn('mt-0.5 size-3.5 shrink-0', error ? style.text : 'text-success')} aria-hidden />\n <Text size=\"xs\" className=\"min-w-0 flex-1 leading-snug [overflow-wrap:anywhere]\">\n {notice.message}\n </Text>\n <button\n type=\"button\"\n onClick={() => studioToasts.dismiss(notice.id)}\n aria-label={translate('shell.notices.dismiss')}\n className=\"shrink-0 rounded text-muted-foreground transition-colors hover:text-foreground\"\n data-testid=\"notice-dismiss\"\n >\n <X className=\"size-3.5\" aria-hidden />\n </button>\n </div>\n )\n}\n\n/**\n * New notices, peeking for 5 s in the content area's bottom-right — inside `<main>`, so they\n * never cover the section rail (the old `Toaster` was fixed to the viewport and did, at\n * narrow widths), and not at its top-right, where the editor's Publish button and save status\n * sit (critic point 8). An error leaves the peek but stays in the tray until dismissed.\n */\nexport function NoticePeek() {\n const notices = useNotices().filter((notice) => notice.peeking)\n if (notices.length === 0) return null\n return (\n <div\n className=\"pointer-events-none absolute bottom-3 right-3 z-40 flex w-80 max-w-[calc(100%-1.5rem)] flex-col gap-2\"\n data-testid=\"notice-peek\"\n >\n {notices.map((notice) => (\n <div key={notice.id} className=\"pointer-events-auto rounded-md bg-background shadow-md\">\n <NoticeRow notice={notice} />\n </div>\n ))}\n </div>\n )\n}\n"]}
|