@abuzareal/contour-ui 0.4.0 → 0.6.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/CHANGELOG.md +24 -0
- package/COMBOBOX.md +87 -0
- package/CONTRACTS.md +23 -17
- package/DOCS.md +80 -0
- package/FOUNDATION.md +35 -0
- package/GETTING-STARTED.md +63 -0
- package/NEXTJS.md +5 -1
- package/OVERLAYS.md +52 -0
- package/PUBLIC-API.md +189 -162
- package/README.md +44 -28
- package/RECIPES.md +43 -0
- package/RELEASING.md +2 -2
- package/SIZE.md +51 -0
- package/STYLING.md +68 -0
- package/dist/actions.css +7 -0
- package/dist/components/hero/Sculpture.js +2 -1
- package/dist/components/three/ObjectStudy.js +2 -1
- package/dist/components/ui/Alert.d.ts +2 -1
- package/dist/components/ui/Alert.js +3 -3
- package/dist/components/ui/Avatar.d.ts +3 -1
- package/dist/components/ui/Avatar.js +3 -3
- package/dist/components/ui/Badge.js +2 -2
- package/dist/components/ui/BrandMark.js +2 -1
- package/dist/components/ui/Breadcrumbs.d.ts +2 -1
- package/dist/components/ui/Breadcrumbs.js +3 -2
- package/dist/components/ui/Button.d.ts +2 -0
- package/dist/components/ui/Button.js +3 -2
- package/dist/components/ui/Card.js +2 -2
- package/dist/components/ui/Checkbox.js +3 -3
- package/dist/components/ui/Combobox.d.ts +325 -0
- package/dist/components/ui/Combobox.js +120 -0
- package/dist/components/ui/Combobox.types.d.ts +46 -0
- package/dist/components/ui/Combobox.types.js +1 -0
- package/dist/components/ui/CountUp.d.ts +2 -1
- package/dist/components/ui/CountUp.js +6 -3
- package/dist/components/ui/DataTable.d.ts +1 -0
- package/dist/components/ui/DataTable.js +15 -10
- package/dist/components/ui/DepthButton.js +2 -2
- package/dist/components/ui/Dialog.d.ts +5 -1
- package/dist/components/ui/Dialog.js +49 -18
- package/dist/components/ui/Disclosure.js +2 -1
- package/dist/components/ui/Divider.js +3 -2
- package/dist/components/ui/DropdownMenu.d.ts +11 -4
- package/dist/components/ui/DropdownMenu.js +109 -66
- package/dist/components/ui/EmptyState.js +2 -1
- package/dist/components/ui/Field.js +2 -2
- package/dist/components/ui/FlipCard.js +2 -2
- package/dist/components/ui/FloatingPortal.d.ts +10 -0
- package/dist/components/ui/FloatingPortal.js +13 -0
- package/dist/components/ui/FloatingScope.d.ts +9 -0
- package/dist/components/ui/FloatingScope.js +43 -0
- package/dist/components/ui/IconButton.js +2 -2
- package/dist/components/ui/Kbd.js +2 -1
- package/dist/components/ui/Magnetic.js +2 -1
- package/dist/components/ui/Marquee.js +3 -2
- package/dist/components/ui/OverlayTrigger.d.ts +11 -0
- package/dist/components/ui/OverlayTrigger.js +5 -0
- package/dist/components/ui/Pagination.d.ts +5 -1
- package/dist/components/ui/Pagination.js +3 -2
- package/dist/components/ui/Popover.d.ts +18 -8
- package/dist/components/ui/Popover.js +58 -17
- package/dist/components/ui/ProgressBar.js +2 -2
- package/dist/components/ui/RadioGroup.js +5 -5
- package/dist/components/ui/Reveal.d.ts +0 -1
- package/dist/components/ui/Reveal.js +4 -3
- package/dist/components/ui/RevealHeading.js +2 -1
- package/dist/components/ui/ScrollProgress.js +2 -1
- package/dist/components/ui/SectionLabel.js +2 -1
- package/dist/components/ui/SegmentedControl.js +6 -3
- package/dist/components/ui/SelectField.js +3 -2
- package/dist/components/ui/Skeleton.js +2 -2
- package/dist/components/ui/Slider.js +3 -3
- package/dist/components/ui/Spinner.js +2 -2
- package/dist/components/ui/Stat.js +2 -1
- package/dist/components/ui/Stepper.js +3 -3
- package/dist/components/ui/Switch.d.ts +2 -0
- package/dist/components/ui/Switch.js +6 -7
- package/dist/components/ui/Table.d.ts +20 -0
- package/dist/components/ui/Table.js +9 -0
- package/dist/components/ui/Tabs.js +7 -5
- package/dist/components/ui/Tag.d.ts +2 -1
- package/dist/components/ui/Tag.js +4 -3
- package/dist/components/ui/TextArea.js +2 -1
- package/dist/components/ui/TextField.js +3 -2
- package/dist/components/ui/ThemeScope.d.ts +13 -0
- package/dist/components/ui/ThemeScope.js +10 -0
- package/dist/components/ui/ThemeToggle.d.ts +3 -0
- package/dist/components/ui/ThemeToggle.js +4 -3
- package/dist/components/ui/TiltCard.js +3 -2
- package/dist/components/ui/Timeline.js +2 -2
- package/dist/components/ui/ToastProvider.d.ts +5 -2
- package/dist/components/ui/ToastProvider.js +5 -5
- package/dist/components/ui/Tooltip.d.ts +4 -4
- package/dist/components/ui/Tooltip.js +70 -10
- package/dist/data-display.css +5 -0
- package/dist/effects.css +6 -0
- package/dist/feedback.css +5 -0
- package/dist/fonts-latin.css +10 -4
- package/dist/fonts.css +8 -0
- package/dist/forms.css +5 -0
- package/dist/hooks/useComboboxAnchor.d.ts +17 -0
- package/dist/hooks/useComboboxAnchor.js +61 -0
- package/dist/hooks/useDismiss.d.ts +12 -3
- package/dist/hooks/useDismiss.js +5 -21
- package/dist/hooks/useFloatingPanel.d.ts +14 -0
- package/dist/hooks/useFloatingPanel.js +157 -0
- package/dist/hooks/useLayerDismiss.d.ts +4 -0
- package/dist/hooks/useLayerDismiss.js +48 -0
- package/dist/hooks/useMobileMenu.d.ts +0 -1
- package/dist/hooks/useMobileMenu.js +11 -16
- package/dist/hooks/usePanelFocus.d.ts +10 -0
- package/dist/hooks/usePanelFocus.js +60 -0
- package/dist/hooks/useTheme.d.ts +1 -0
- package/dist/hooks/useTheme.js +18 -5
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -0
- package/dist/kit/data-display.css +6 -0
- package/dist/kit/forms.css +45 -2
- package/dist/kit/navigation.css +4 -0
- package/dist/kit/overlays.css +8 -4
- package/dist/lib/classNames.d.ts +2 -0
- package/dist/lib/classNames.js +16 -1
- package/dist/lib/componentClasses.d.ts +1 -0
- package/dist/lib/componentClasses.js +2 -0
- package/dist/lib/keyboard.d.ts +1 -1
- package/dist/lib/keyboard.js +5 -2
- package/dist/lib/layers.d.ts +18 -0
- package/dist/lib/layers.js +128 -0
- package/dist/lib/themeScope.d.ts +5 -0
- package/dist/lib/themeScope.js +3 -0
- package/dist/minimal-reset.css +6 -0
- package/dist/navigation.css +5 -0
- package/dist/overlays.css +5 -0
- package/dist/scoped/base.css +183 -0
- package/dist/scoped/kit/actions.css +209 -0
- package/dist/scoped/kit/data-display.css +498 -0
- package/dist/scoped/kit/effects.css +140 -0
- package/dist/scoped/kit/feedback.css +309 -0
- package/dist/scoped/kit/forms.css +404 -0
- package/dist/scoped/kit/navigation.css +266 -0
- package/dist/scoped/kit/overlays.css +299 -0
- package/dist/scoped/marquee.css +58 -0
- package/dist/scoped/spinner.css +20 -0
- package/dist/scoped/theme-toggle.css +38 -0
- package/dist/scoped/tokens.css +119 -0
- package/dist/scoped/ui.css +127 -0
- package/dist/scoped/utilities.css +17 -0
- package/dist/scoped-tokens.css +1 -0
- package/dist/scoped.css +12 -0
- package/dist/theme-toggle.css +2 -2
- package/dist/tokens.json +710 -0
- package/examples/optional/LazyScene.tsx +78 -0
- package/examples/optional/OptionalSmoothScroll.tsx +25 -0
- package/examples/optional/SmoothScrollOwner.tsx +7 -0
- package/examples/optional/optional.css +25 -0
- package/examples/recipes/AsyncProjectPicker.tsx +82 -0
- package/examples/recipes/DeleteProjectConfirmation.tsx +97 -0
- package/examples/recipes/InputCompositions.tsx +49 -0
- package/examples/recipes/RemoteRecordsTable.tsx +256 -0
- package/examples/recipes/useRemoteResource.ts +81 -0
- package/examples/reproduction/README.md +9 -0
- package/examples/reproduction/index.html +14 -0
- package/examples/reproduction/package.json +27 -0
- package/examples/reproduction/src/main.tsx +33 -0
- package/examples/reproduction/src/style.css +19 -0
- package/examples/reproduction/tsconfig.json +18 -0
- package/examples/reproduction/vite.config.ts +19 -0
- package/examples/starters/next/README.md +18 -0
- package/examples/starters/next/app/Demo.tsx +65 -0
- package/examples/starters/next/app/layout.tsx +25 -0
- package/examples/starters/next/app/next-page/page.tsx +9 -0
- package/examples/starters/next/app/page.tsx +11 -0
- package/examples/starters/next/app/style.css +19 -0
- package/examples/starters/next/next-env.d.ts +2 -0
- package/examples/starters/next/next.config.mjs +1 -0
- package/examples/starters/next/package.json +27 -0
- package/examples/starters/next/proxy.ts +16 -0
- package/examples/starters/next/tsconfig.json +30 -0
- package/examples/starters/vite/README.md +16 -0
- package/examples/starters/vite/index.html +14 -0
- package/examples/starters/vite/package.json +27 -0
- package/examples/starters/vite/src/main.tsx +81 -0
- package/examples/starters/vite/src/style.css +19 -0
- package/examples/starters/vite/tsconfig.json +18 -0
- package/examples/starters/vite/vite.config.ts +19 -0
- package/package.json +41 -5
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
/** The application maps its service failures; never display raw server exceptions. */
|
|
5
|
+
export class RecipeServiceError extends Error {
|
|
6
|
+
constructor(public readonly kind: "permission" | "service") {
|
|
7
|
+
super(kind);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export type Loader<Query, Data> = (
|
|
11
|
+
query: Query,
|
|
12
|
+
signal: AbortSignal,
|
|
13
|
+
) => Promise<Data>;
|
|
14
|
+
|
|
15
|
+
/** Application-owned fetching. The old snapshot stays labelled; obsolete work cannot commit. */
|
|
16
|
+
export function useRemoteResource<Query, Data>(
|
|
17
|
+
query: Query,
|
|
18
|
+
load: Loader<Query, Data>,
|
|
19
|
+
) {
|
|
20
|
+
const [attempt, setAttempt] = useState(0);
|
|
21
|
+
const sequence = useRef(0);
|
|
22
|
+
const [state, setState] = useState<{
|
|
23
|
+
requested: Query | null;
|
|
24
|
+
loader: Loader<Query, Data> | null;
|
|
25
|
+
attempt: number;
|
|
26
|
+
applied: Query | null;
|
|
27
|
+
data: Data | null;
|
|
28
|
+
error: "permission" | "service" | null;
|
|
29
|
+
}>({
|
|
30
|
+
requested: null,
|
|
31
|
+
loader: null,
|
|
32
|
+
attempt: -1,
|
|
33
|
+
applied: null,
|
|
34
|
+
data: null,
|
|
35
|
+
error: null,
|
|
36
|
+
});
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const revision = ++sequence.current;
|
|
40
|
+
void (async () => {
|
|
41
|
+
try {
|
|
42
|
+
const data = await load(query, controller.signal);
|
|
43
|
+
if (controller.signal.aborted || sequence.current !== revision) return;
|
|
44
|
+
setState({
|
|
45
|
+
requested: query,
|
|
46
|
+
loader: load,
|
|
47
|
+
attempt,
|
|
48
|
+
applied: query,
|
|
49
|
+
data,
|
|
50
|
+
error: null,
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (controller.signal.aborted || sequence.current !== revision) return;
|
|
54
|
+
const kind =
|
|
55
|
+
error instanceof RecipeServiceError ? error.kind : "service";
|
|
56
|
+
setState((previous) => ({
|
|
57
|
+
...previous,
|
|
58
|
+
requested: query,
|
|
59
|
+
loader: load,
|
|
60
|
+
attempt,
|
|
61
|
+
error: kind,
|
|
62
|
+
// A denied snapshot must not reappear while a later query is pending.
|
|
63
|
+
data: kind === "permission" ? null : previous.data,
|
|
64
|
+
applied: kind === "permission" ? null : previous.applied,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
return () => controller.abort();
|
|
69
|
+
}, [query, load, attempt]);
|
|
70
|
+
const current =
|
|
71
|
+
state.requested === query &&
|
|
72
|
+
state.loader === load &&
|
|
73
|
+
state.attempt === attempt;
|
|
74
|
+
return {
|
|
75
|
+
data: state.data,
|
|
76
|
+
appliedQuery: state.applied,
|
|
77
|
+
loading: !current,
|
|
78
|
+
error: current ? state.error : null,
|
|
79
|
+
retry: () => setAttempt((n) => n + 1),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Minimal component / overlay / form reproduction
|
|
2
|
+
|
|
3
|
+
Copy this folder alone into an empty directory. Run npm install --strict-peer-deps, npm run build and npm run preview. Versions are pinned in package.json; commit the generated lockfile. There are no workspace aliases or private catalogue dependencies. Use npm ci once your application lockfile exists.
|
|
4
|
+
|
|
5
|
+
Keep a failing interaction in src/main.tsx and remove unrelated UI. Include installed Contour/React/browser versions, expected/actual behavior, keyboard steps, theme, viewport and the smallest input data. Replace names and service URLs with synthetic data; never include credentials, customer records or private repository links. A public repository or sandbox can use this standalone folder.
|
|
6
|
+
|
|
7
|
+
[Report a Contour 0.6.0 Combobox issue](https://github.com/abuzareal/contour-ui/issues/new?template=bug_report.yml&title=Combobox%200.6.0%3A%20&component=Combobox&version=0.6.0)
|
|
8
|
+
|
|
9
|
+
Candidate versions need the package to be published before registry installation; the maintainer tests the same source against the packed candidate first.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<meta name="theme-color" content="#f2f3ee" />
|
|
7
|
+
<!-- theme-bootstrap -->
|
|
8
|
+
<title>Contour starter</title>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": true,
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.12.0"
|
|
7
|
+
},
|
|
8
|
+
"name": "contour-bug-reproduction",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "vite",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"build": "tsc --noEmit && vite build",
|
|
13
|
+
"preview": "vite preview"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@abuzareal/contour-ui": "0.6.0",
|
|
17
|
+
"react": "18.3.1",
|
|
18
|
+
"react-dom": "18.3.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/react": "18.3.31",
|
|
22
|
+
"@types/react-dom": "18.3.7",
|
|
23
|
+
"@types/node": "26.6.5",
|
|
24
|
+
"typescript": "5.9.3",
|
|
25
|
+
"vite": "8.3.2"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { Button, Combobox, Dialog, Popover } from "@abuzareal/contour-ui";
|
|
4
|
+
import "@abuzareal/contour-ui/scoped.css";
|
|
5
|
+
import "./style.css";
|
|
6
|
+
const options = [
|
|
7
|
+
{ value: "one", label: "One" },
|
|
8
|
+
{ value: "two", label: "Two", disabled: true },
|
|
9
|
+
];
|
|
10
|
+
function Reproduction() {
|
|
11
|
+
const [open, setOpen] = useState(false);
|
|
12
|
+
return (
|
|
13
|
+
<main className="contour-root starter">
|
|
14
|
+
<h1>Contour reproduction</h1>
|
|
15
|
+
<p>
|
|
16
|
+
Contour 0.6.0, React/React DOM 18.3.1. Replace only the smallest
|
|
17
|
+
relevant case. No real user data.
|
|
18
|
+
</p>
|
|
19
|
+
<Button onClick={() => setOpen(true)}>Open dialog</Button>
|
|
20
|
+
<Dialog open={open} onClose={() => setOpen(false)} title="Reproduction">
|
|
21
|
+
<Popover trigger="Open popover" title="Nested layer">
|
|
22
|
+
<form onSubmit={(event) => event.preventDefault()}>
|
|
23
|
+
<Combobox label="Choice" name="choice" options={options} required />
|
|
24
|
+
<Button type="reset">Reset</Button>
|
|
25
|
+
</form>
|
|
26
|
+
</Popover>
|
|
27
|
+
</Dialog>
|
|
28
|
+
</main>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const element = document.getElementById("root");
|
|
32
|
+
if (!element) throw new Error("Missing root");
|
|
33
|
+
createRoot(element).render(<Reproduction />);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
background: var(--contour-bg);
|
|
4
|
+
color: var(--contour-fg);
|
|
5
|
+
}
|
|
6
|
+
.starter {
|
|
7
|
+
max-width: 42rem;
|
|
8
|
+
margin: 0 auto;
|
|
9
|
+
padding: 2rem 1rem;
|
|
10
|
+
display: grid;
|
|
11
|
+
gap: 1.25rem;
|
|
12
|
+
}
|
|
13
|
+
.starter form {
|
|
14
|
+
display: grid;
|
|
15
|
+
gap: 1rem;
|
|
16
|
+
}
|
|
17
|
+
.starter output {
|
|
18
|
+
overflow-wrap: anywhere;
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
5
|
+
"strict": true,
|
|
6
|
+
"skipLibCheck": false,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "Bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"isolatedModules": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
17
|
+
"exclude": ["node_modules"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
import { themeBootstrapScript } from "@abuzareal/contour-ui/theme-bootstrap";
|
|
4
|
+
const hash = createHash("sha256").update(themeBootstrapScript).digest("base64");
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [
|
|
7
|
+
{
|
|
8
|
+
name: "contour-theme-bootstrap",
|
|
9
|
+
transformIndexHtml(html) {
|
|
10
|
+
// Hash the exact bytes inserted here; no inline handler or unsafe-eval allowance.
|
|
11
|
+
const policy = `default-src 'self'; script-src 'self' 'sha256-${hash}'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'`;
|
|
12
|
+
return html.replace(
|
|
13
|
+
"<!-- theme-bootstrap -->",
|
|
14
|
+
`<meta http-equiv="Content-Security-Policy" content="${policy}"><script>${themeBootstrapScript}</script>`,
|
|
15
|
+
);
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Contour + Next App Router
|
|
2
|
+
|
|
3
|
+
Copy this directory. Use Node 22.12 or newer, then:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install --strict-peer-deps
|
|
7
|
+
npm run dev
|
|
8
|
+
npm run build
|
|
9
|
+
npm start
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Direct versions: Contour 0.6.0, Next 16.4.0, React/React DOM 19.3.0, TypeScript 7.0.2; webpack explicitly selected. Commit the generated application lockfile and use npm ci afterward. There are no workspace aliases, optional Three/Lenis peers, fonts or required providers. To opt into branded fonts, import `@abuzareal/contour-ui/fonts-latin.css` in app/layout.tsx. Root CSS and route composition belong to the application.
|
|
13
|
+
|
|
14
|
+
layout.tsx and the page are server components. Demo.tsx owns client state, refs and handlers. The pure theme-bootstrap subpath is server-safe; before-paint script changes only the root theme attribute and theme-color. The root's suppressHydrationWarning is narrowly for this attribute, not a workaround for arbitrary mismatches.
|
|
15
|
+
|
|
16
|
+
proxy.ts generates a new nonce on every document request, forwards the CSP and nonce to Next and returns the CSP header. The dynamic layout reads that nonce so both bootstrap and framework scripts are authorized. This deliberately requires server rendering; static export/cached shared nonces are not supported. Inline styles remain allowed for library geometry. Production script policy has no unsafe-eval; development adds it for the bundler. Configure additional service origins explicitly. See [Next's CSP guide](https://nextjs.org/docs/app/guides/content-security-policy).
|
|
17
|
+
|
|
18
|
+
This folder establishes the named App Router/webpack pair only. Turbopack, Pages Router and Next Lenis are unverified. Optional lazy Three is covered separately by the library's optional fixture, not installed here. A release candidate uses an archive only in disposable validation copies; the exact registry installation requires the published release.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Combobox,
|
|
7
|
+
Dialog,
|
|
8
|
+
TextField,
|
|
9
|
+
ThemeToggle,
|
|
10
|
+
buttonClassName,
|
|
11
|
+
} from "@abuzareal/contour-ui";
|
|
12
|
+
const projects = [
|
|
13
|
+
{ value: "atlas", label: "Atlas" },
|
|
14
|
+
{ value: "field", label: "Field notes" },
|
|
15
|
+
];
|
|
16
|
+
export default function Demo() {
|
|
17
|
+
const [open, setOpen] = useState(false),
|
|
18
|
+
[payload, setPayload] = useState("");
|
|
19
|
+
const name = useRef<HTMLInputElement>(null);
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<p>This form inspects local values. It does not save data.</p>
|
|
23
|
+
<ThemeToggle animate={false} />
|
|
24
|
+
<form
|
|
25
|
+
onSubmit={(event) => {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
setPayload(
|
|
28
|
+
JSON.stringify(
|
|
29
|
+
Object.fromEntries(new FormData(event.currentTarget)),
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<TextField
|
|
35
|
+
ref={name}
|
|
36
|
+
label="Name"
|
|
37
|
+
name="name"
|
|
38
|
+
defaultValue="Ada"
|
|
39
|
+
required
|
|
40
|
+
/>
|
|
41
|
+
<Combobox
|
|
42
|
+
label="Project"
|
|
43
|
+
name="project"
|
|
44
|
+
options={projects}
|
|
45
|
+
defaultValue="atlas"
|
|
46
|
+
required
|
|
47
|
+
/>
|
|
48
|
+
<Button type="submit">Inspect values</Button>
|
|
49
|
+
<Button type="reset" variant="secondary">
|
|
50
|
+
Reset form
|
|
51
|
+
</Button>
|
|
52
|
+
</form>
|
|
53
|
+
<output aria-label="Form values">{payload}</output>
|
|
54
|
+
<Button onClick={() => name.current?.focus()}>Focus name</Button>
|
|
55
|
+
<Button onClick={() => setOpen(true)}>Open dialog</Button>
|
|
56
|
+
<Dialog open={open} onClose={() => setOpen(false)} title="Starter dialog">
|
|
57
|
+
<p>Try Tab, Escape and focus return.</p>
|
|
58
|
+
<Button onClick={() => setOpen(false)}>Done</Button>
|
|
59
|
+
</Dialog>
|
|
60
|
+
<Link href="/next-page" className={buttonClassName("secondary")}>
|
|
61
|
+
Next page
|
|
62
|
+
</Link>
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { headers } from "next/headers";
|
|
2
|
+
import { themeBootstrapScript } from "@abuzareal/contour-ui/theme-bootstrap";
|
|
3
|
+
import "@abuzareal/contour-ui/scoped.css";
|
|
4
|
+
import "./style.css";
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
export const metadata = { title: "Contour Next starter" };
|
|
7
|
+
export default async function Layout({
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}) {
|
|
12
|
+
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
|
13
|
+
return (
|
|
14
|
+
<html lang="en" suppressHydrationWarning>
|
|
15
|
+
<head>
|
|
16
|
+
<meta name="theme-color" content="#f2f3ee" />
|
|
17
|
+
<script
|
|
18
|
+
nonce={nonce}
|
|
19
|
+
dangerouslySetInnerHTML={{ __html: themeBootstrapScript }}
|
|
20
|
+
/>
|
|
21
|
+
</head>
|
|
22
|
+
<body>{children}</body>
|
|
23
|
+
</html>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { safeHref } from "@abuzareal/contour-ui";
|
|
2
|
+
import Demo from "./Demo";
|
|
3
|
+
export default function Page() {
|
|
4
|
+
return (
|
|
5
|
+
<main className="contour-root starter">
|
|
6
|
+
<h1>Contour Next starter</h1>
|
|
7
|
+
<p>Server helper result: {safeHref("/next-page")}</p>
|
|
8
|
+
<Demo />
|
|
9
|
+
</main>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
background: var(--contour-bg);
|
|
4
|
+
color: var(--contour-fg);
|
|
5
|
+
}
|
|
6
|
+
.starter {
|
|
7
|
+
max-width: 42rem;
|
|
8
|
+
margin: 0 auto;
|
|
9
|
+
padding: 2rem 1rem;
|
|
10
|
+
display: grid;
|
|
11
|
+
gap: 1.25rem;
|
|
12
|
+
}
|
|
13
|
+
.starter form {
|
|
14
|
+
display: grid;
|
|
15
|
+
gap: 1rem;
|
|
16
|
+
}
|
|
17
|
+
.starter output {
|
|
18
|
+
overflow-wrap: anywhere;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { experimental: { cpus: 2 } };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": true,
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.12.0"
|
|
7
|
+
},
|
|
8
|
+
"name": "contour-next-starter",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "next dev --webpack",
|
|
11
|
+
"build": "next build --webpack",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"start": "next start"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@abuzareal/contour-ui": "0.6.0",
|
|
17
|
+
"react": "19.3.0",
|
|
18
|
+
"react-dom": "19.3.0",
|
|
19
|
+
"next": "16.4.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/react": "19.3.0",
|
|
23
|
+
"@types/react-dom": "19.3.0",
|
|
24
|
+
"@types/node": "26.6.5",
|
|
25
|
+
"typescript": "7.0.2"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
2
|
+
export function proxy(request: NextRequest) {
|
|
3
|
+
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
|
|
4
|
+
const development =
|
|
5
|
+
process.env.NODE_ENV === "development" ? " 'unsafe-eval'" : "";
|
|
6
|
+
const policy = `default-src 'self'; script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${development}; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'`;
|
|
7
|
+
const headers = new Headers(request.headers);
|
|
8
|
+
headers.set("x-nonce", nonce);
|
|
9
|
+
headers.set("Content-Security-Policy", policy);
|
|
10
|
+
const response = NextResponse.next({ request: { headers } });
|
|
11
|
+
response.headers.set("Content-Security-Policy", policy);
|
|
12
|
+
return response;
|
|
13
|
+
}
|
|
14
|
+
export const config = {
|
|
15
|
+
matcher: ["/((?!api|_next/static|_next/image|favicon.svg).*)"],
|
|
16
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["DOM", "DOM.Iterable", "ES2024"],
|
|
5
|
+
"strict": true,
|
|
6
|
+
"skipLibCheck": false,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "Bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"plugins": [
|
|
16
|
+
{
|
|
17
|
+
"name": "next"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"allowJs": true,
|
|
21
|
+
"incremental": true
|
|
22
|
+
},
|
|
23
|
+
"include": [
|
|
24
|
+
"**/*.ts",
|
|
25
|
+
"**/*.tsx",
|
|
26
|
+
".next/types/**/*.ts",
|
|
27
|
+
".next/dev/types/**/*.ts"
|
|
28
|
+
],
|
|
29
|
+
"exclude": ["node_modules"]
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Contour + Vite
|
|
2
|
+
|
|
3
|
+
Copy this folder into a new directory. Node 22.12 or newer is required. Direct dependencies are pinned; `npm install` creates the application's own lockfile. Commit that lockfile and use `npm ci` for later installs.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install --strict-peer-deps
|
|
7
|
+
npm run dev
|
|
8
|
+
npm run build
|
|
9
|
+
npm run preview
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Uses Contour 0.6.0, React/React DOM 18.3.1, TypeScript 5.9.3 and Vite 8.3.2. No aliases, workspace links, optional peers, font download, private catalogue or provider are required. Scoped CSS includes tokens and component families without the legacy global reset. The application owns the small body/layout stylesheet. Optional branded fonts: import `@abuzareal/contour-ui/fonts-latin.css` once.
|
|
13
|
+
|
|
14
|
+
The Vite config inserts the exact released theme bootstrap before the application, with a calculated CSP script hash. Keep the placeholder in index.html; recalculate through the build after upgrades. Script execution is restricted; style attributes are allowed because popup positioning and some components use inline styles. Extend connect-src intentionally for your service/development socket. An HTTP CSP header may replace this meta policy if it contains the same hash. See the package GETTING-STARTED.md for embedded themes and optional features.
|
|
15
|
+
|
|
16
|
+
Run against the registry after the release is published. Release candidates are tested with the verified npm archive in a disposable copy; an unpublished candidate version will not install from npm yet.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<meta name="theme-color" content="#f2f3ee" />
|
|
7
|
+
<!-- theme-bootstrap -->
|
|
8
|
+
<title>Contour starter</title>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": true,
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.12.0"
|
|
7
|
+
},
|
|
8
|
+
"name": "contour-vite-starter",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "vite",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"build": "tsc --noEmit && vite build",
|
|
13
|
+
"preview": "vite preview"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@abuzareal/contour-ui": "0.6.0",
|
|
17
|
+
"react": "18.3.1",
|
|
18
|
+
"react-dom": "18.3.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/react": "18.3.31",
|
|
22
|
+
"@types/react-dom": "18.3.7",
|
|
23
|
+
"@types/node": "26.6.5",
|
|
24
|
+
"typescript": "5.9.3",
|
|
25
|
+
"vite": "8.3.2"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { StrictMode, useRef, useState } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Combobox,
|
|
6
|
+
Dialog,
|
|
7
|
+
TextField,
|
|
8
|
+
ThemeToggle,
|
|
9
|
+
buttonClassName,
|
|
10
|
+
} from "@abuzareal/contour-ui";
|
|
11
|
+
import "@abuzareal/contour-ui/scoped.css";
|
|
12
|
+
import "./style.css";
|
|
13
|
+
const projects = [
|
|
14
|
+
{ value: "atlas", label: "Atlas" },
|
|
15
|
+
{ value: "field", label: "Field notes" },
|
|
16
|
+
];
|
|
17
|
+
function App() {
|
|
18
|
+
const [open, setOpen] = useState(false),
|
|
19
|
+
[payload, setPayload] = useState("");
|
|
20
|
+
const name = useRef<HTMLInputElement>(null);
|
|
21
|
+
return (
|
|
22
|
+
<main className="contour-root starter">
|
|
23
|
+
<h1>Contour starter</h1>
|
|
24
|
+
<p>This form inspects local values. It does not save data.</p>
|
|
25
|
+
<ThemeToggle animate={false} />
|
|
26
|
+
<form
|
|
27
|
+
onSubmit={(event) => {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
setPayload(
|
|
30
|
+
JSON.stringify(
|
|
31
|
+
Object.fromEntries(new FormData(event.currentTarget)),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<TextField
|
|
37
|
+
ref={name}
|
|
38
|
+
label="Name"
|
|
39
|
+
name="name"
|
|
40
|
+
defaultValue="Ada"
|
|
41
|
+
required
|
|
42
|
+
/>
|
|
43
|
+
<Combobox
|
|
44
|
+
label="Project"
|
|
45
|
+
name="project"
|
|
46
|
+
options={projects}
|
|
47
|
+
defaultValue="atlas"
|
|
48
|
+
required
|
|
49
|
+
/>
|
|
50
|
+
<Button type="submit">Inspect values</Button>
|
|
51
|
+
<Button type="reset" variant="secondary">
|
|
52
|
+
Reset form
|
|
53
|
+
</Button>
|
|
54
|
+
</form>
|
|
55
|
+
<output aria-label="Form values">{payload}</output>
|
|
56
|
+
<Button onClick={() => name.current?.focus()}>Focus name</Button>
|
|
57
|
+
<Button onClick={() => setOpen(true)}>Open dialog</Button>
|
|
58
|
+
<Dialog open={open} onClose={() => setOpen(false)} title="Starter dialog">
|
|
59
|
+
<p>Try Tab, Escape and focus return.</p>
|
|
60
|
+
<Button onClick={() => setOpen(false)}>Done</Button>
|
|
61
|
+
</Dialog>
|
|
62
|
+
<a className={buttonClassName("secondary")} href="#integration">
|
|
63
|
+
Integration notes
|
|
64
|
+
</a>
|
|
65
|
+
<section id="integration">
|
|
66
|
+
<h2>Application ownership</h2>
|
|
67
|
+
<p>
|
|
68
|
+
Add your own router, validation and persistence. No provider is
|
|
69
|
+
required by the controls above.
|
|
70
|
+
</p>
|
|
71
|
+
</section>
|
|
72
|
+
</main>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const root = document.getElementById("root");
|
|
76
|
+
if (!root) throw new Error("Missing root element");
|
|
77
|
+
createRoot(root).render(
|
|
78
|
+
<StrictMode>
|
|
79
|
+
<App />
|
|
80
|
+
</StrictMode>,
|
|
81
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
background: var(--contour-bg);
|
|
4
|
+
color: var(--contour-fg);
|
|
5
|
+
}
|
|
6
|
+
.starter {
|
|
7
|
+
max-width: 42rem;
|
|
8
|
+
margin: 0 auto;
|
|
9
|
+
padding: 2rem 1rem;
|
|
10
|
+
display: grid;
|
|
11
|
+
gap: 1.25rem;
|
|
12
|
+
}
|
|
13
|
+
.starter form {
|
|
14
|
+
display: grid;
|
|
15
|
+
gap: 1rem;
|
|
16
|
+
}
|
|
17
|
+
.starter output {
|
|
18
|
+
overflow-wrap: anywhere;
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
5
|
+
"strict": true,
|
|
6
|
+
"skipLibCheck": false,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "Bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"isolatedModules": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
17
|
+
"exclude": ["node_modules"]
|
|
18
|
+
}
|