@ogygie/ui 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/DisclosureList.test.tsx +44 -0
- package/src/DisclosureList.tsx +57 -0
- package/src/extensions.ts +18 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogygie/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Ogygie front socle: the five prescribed patterns (DataTable, EmptyState, ErrorBanner, ContractForm, VarianceChart), one screen-state component (ScreenState), fetchContract and EditableText. Ships TypeScript sources — Ogygie products run Vite/tsx/vitest by construction (D1); a product imports them, it no longer copies them (D39).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"./variance-chart": "./src/VarianceChart.tsx"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@ogygie/core": "0.
|
|
27
|
+
"@ogygie/core": "0.23.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@tanstack/react-table": "^9.0.0",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"react-hook-form": "^7.90.0",
|
|
59
59
|
"recharts": "^3.10.1",
|
|
60
60
|
"typescript": "^5.9.3",
|
|
61
|
-
"vitest": "^
|
|
61
|
+
"vitest": "^4.1.12",
|
|
62
62
|
"zod": "^4.4.3"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* @intent Proves the disclosure list (spec ogygie-ui B9 bis): one closed
|
|
4
|
+
* `<details>` per entry, anchored by the prefix and the id, the title in the
|
|
5
|
+
* summary, the body and the optional link with the caller's label; the empty
|
|
6
|
+
* state alone when there is nothing to show.
|
|
7
|
+
*/
|
|
8
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
9
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
10
|
+
import { DisclosureList } from "./DisclosureList.js";
|
|
11
|
+
|
|
12
|
+
const ENTRIES = [
|
|
13
|
+
{ id: "what-is-the-scorecard", title: "What is the scorecard?", body: "A grid of criteria, one level each." },
|
|
14
|
+
{ id: "who-can-see-my-data", title: "Who can see my data?", body: "Your company and the fund.", href: "?tab=scorecard" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
describe("DisclosureList", () => {
|
|
18
|
+
afterEach(cleanup);
|
|
19
|
+
|
|
20
|
+
it("ÉTANT DONNÉ deux entrées ALORS deux details fermés, ancrés, le titre en summary, le corps et le lien libellé par l'appelant", () => {
|
|
21
|
+
const { container } = render(<DisclosureList entries={ENTRIES} anchorPrefix="faq" linkLabel="Open the tab" />);
|
|
22
|
+
const details = container.querySelectorAll("details");
|
|
23
|
+
expect(details).toHaveLength(2);
|
|
24
|
+
expect(container.querySelectorAll("details[open]")).toHaveLength(0);
|
|
25
|
+
expect(container.querySelector("#faq-what-is-the-scorecard")).not.toBeNull();
|
|
26
|
+
expect(container.querySelector("#faq-who-can-see-my-data")).not.toBeNull();
|
|
27
|
+
expect(screen.getByText("What is the scorecard?").closest("summary")).not.toBeNull();
|
|
28
|
+
expect(screen.getByText(/A grid of criteria/)).toBeDefined();
|
|
29
|
+
const link = screen.getByRole("link", { name: "Open the tab" });
|
|
30
|
+
expect(link.getAttribute("href")).toBe("?tab=scorecard");
|
|
31
|
+
expect(container.querySelectorAll("a")).toHaveLength(1);
|
|
32
|
+
expect(container.querySelector("[data-pattern='DisclosureList']")).not.toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("ÉTANT DONNÉ aucune entrée et un état vide ALORS lui seul se rend ; sans état vide, une liste vide", () => {
|
|
36
|
+
const { container, unmount } = render(<DisclosureList entries={[]} anchorPrefix="faq" emptyState={<p>Nothing to read yet.</p>} />);
|
|
37
|
+
expect(screen.getByText("Nothing to read yet.")).toBeDefined();
|
|
38
|
+
expect(container.querySelector("[data-pattern='DisclosureList']")).toBeNull();
|
|
39
|
+
unmount();
|
|
40
|
+
const bare = render(<DisclosureList entries={[]} anchorPrefix="faq" />);
|
|
41
|
+
expect(bare.container.querySelector("[data-pattern='DisclosureList']")).not.toBeNull();
|
|
42
|
+
expect(bare.container.querySelectorAll("details")).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @intent Content folded under its title (spec ogygie-ui B9 bis, D56): the
|
|
3
|
+
* pattern a generated CONTENT view renders — a help page, a FAQ, release
|
|
4
|
+
* notes — as a list of native `<details>` elements, every entry closed at
|
|
5
|
+
* render, anchored by its id so a link can open the page on one answer, with
|
|
6
|
+
* one optional link per entry. The entries are the product's MATERIAL, written
|
|
7
|
+
* in its spec; the pattern lays them out and names nothing: the link label is
|
|
8
|
+
* a prop, translated by the caller. No colour outside the tokens, no script.
|
|
9
|
+
*/
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
|
|
12
|
+
export interface DisclosureEntry {
|
|
13
|
+
/** Unique within the list; the anchor is `<anchorPrefix>-<id>`. */
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
body: string;
|
|
17
|
+
/** An in-app path or a query (`?tab=scorecard`), rendered as a link under the body. */
|
|
18
|
+
href?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DisclosureListProps {
|
|
22
|
+
entries: readonly DisclosureEntry[];
|
|
23
|
+
/** Prefix of every anchor, the view's id in a generated screen. */
|
|
24
|
+
anchorPrefix: string;
|
|
25
|
+
/** The label of an entry's link, when it carries one — the product's words. */
|
|
26
|
+
linkLabel?: ReactNode;
|
|
27
|
+
/** Rendered alone when the list is empty. */
|
|
28
|
+
emptyState?: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function DisclosureList({ entries, anchorPrefix, linkLabel, emptyState }: DisclosureListProps) {
|
|
32
|
+
if (entries.length === 0 && emptyState !== undefined) return <>{emptyState}</>;
|
|
33
|
+
return (
|
|
34
|
+
<dl data-pattern="DisclosureList" className="divide-y divide-border rounded-md border border-border bg-surface-raised">
|
|
35
|
+
{entries.map((entry) => (
|
|
36
|
+
<details key={entry.id} id={`${anchorPrefix}-${entry.id}`} data-entry={entry.id} className="group px-4 py-3">
|
|
37
|
+
<summary className="cursor-pointer list-none text-sm font-medium text-text transition-colors hover:text-accent">
|
|
38
|
+
<span aria-hidden="true" className="mr-2 inline-block font-mono text-xs text-text-faint transition-transform group-open:rotate-90">
|
|
39
|
+
›
|
|
40
|
+
</span>
|
|
41
|
+
{entry.title}
|
|
42
|
+
</summary>
|
|
43
|
+
<dd className="mt-2 ml-5 text-sm leading-relaxed whitespace-pre-line text-text-muted">
|
|
44
|
+
{entry.body}
|
|
45
|
+
{entry.href !== undefined && (
|
|
46
|
+
<p className="mt-2">
|
|
47
|
+
<a href={entry.href} className="text-accent underline-offset-2 hover:underline">
|
|
48
|
+
{linkLabel ?? entry.href}
|
|
49
|
+
</a>
|
|
50
|
+
</p>
|
|
51
|
+
)}
|
|
52
|
+
</dd>
|
|
53
|
+
</details>
|
|
54
|
+
))}
|
|
55
|
+
</dl>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @intent What a screen EXTENSION POINT exchanges (spec ogygie-ui B9 bis,
|
|
3
|
+
* D56): a host screen of the product declares a point at the manifest — a
|
|
4
|
+
* place where other screens may mount, a tab bar in v1 — and the generator
|
|
5
|
+
* maintains in the host's folder a registry of `TabContribution`: the id of
|
|
6
|
+
* the contribution, its label (a translated node the contributing screen's
|
|
7
|
+
* written body owns), and what to render. The host reads the registry once
|
|
8
|
+
* and keeps its own tab bar: no component is imposed here, only the shape.
|
|
9
|
+
*/
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
|
|
12
|
+
export interface TabContribution {
|
|
13
|
+
/** Unique on the point; the id the host uses to select the tab. */
|
|
14
|
+
id: string;
|
|
15
|
+
label: ReactNode;
|
|
16
|
+
/** The contributing screen's container, rendered when the tab is active. */
|
|
17
|
+
render: () => ReactNode;
|
|
18
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* resolves them.
|
|
10
10
|
*/
|
|
11
11
|
export { EmptyState, type EmptyStateProps } from "./EmptyState.js";
|
|
12
|
+
export { DisclosureList, type DisclosureEntry, type DisclosureListProps } from "./DisclosureList.js";
|
|
13
|
+
export type { TabContribution } from "./extensions.js";
|
|
12
14
|
export { ErrorBanner, type ErrorBannerProps } from "./ErrorBanner.js";
|
|
13
15
|
export { ScreenState, Skeleton, type ScreenStateProps, type StateCopy } from "./ScreenState.js";
|
|
14
16
|
export { EditableText, type EditableTextProps } from "./EditableText.js";
|