@atelic-action/ui 0.13.0 → 0.15.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/README.md +62 -4
- package/dist/artifact/artifactConfig.d.ts +138 -0
- package/dist/artifact/artifactConfig.js +10 -0
- package/dist/artifact/readers.d.ts +33 -0
- package/dist/artifact/readers.js +117 -0
- package/dist/vite/private-chunks.d.ts +24 -0
- package/dist/vite/private-chunks.js +42 -0
- package/package.json +21 -3
- package/src/artifact/AppErrorBoundary.tsx +36 -0
- package/src/artifact/ArtifactShell.tsx +50 -0
- package/src/artifact/ErrorFallback.tsx +70 -0
- package/src/artifact/Gate.tsx +328 -0
- package/src/artifact/GradeChip.tsx +29 -0
- package/src/artifact/Report.tsx +131 -0
- package/src/artifact/SignOff.tsx +37 -0
- package/src/artifact/Writeup.tsx +791 -0
- package/src/artifact/artifactConfig.ts +134 -0
- package/src/artifact/index.ts +25 -0
- package/src/artifact/pageHead.ts +58 -0
- package/src/artifact/readers.ts +146 -0
- package/src/primitives/Button.tsx +87 -0
- package/src/primitives/Chip.tsx +30 -0
- package/src/primitives/Eyebrow.tsx +25 -0
- package/src/primitives/Lead.tsx +17 -0
- package/src/primitives/SectionHeading.tsx +42 -0
- package/src/primitives/index.ts +5 -0
- package/src/styles/artifact.css +1862 -0
- package/src/styles/primitives.css +153 -0
- package/src/vite/private-chunks.ts +65 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/* The primitives under src/primitives: the button, the chip, the eyebrow,
|
|
2
|
+
the lead paragraph, and the section heading. Every value is a theme token.
|
|
3
|
+
They sit in the package's layer like every sheet here, so any rule a site
|
|
4
|
+
writes outside a layer still wins over them whatever its specificity. */
|
|
5
|
+
@layer atelic-ui {
|
|
6
|
+
/* CTA buttons: primary (brand), dark (ink), ghost (outline). */
|
|
7
|
+
:is(.mkt, .atelic-ui) .btn {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
gap: 0.6em;
|
|
12
|
+
padding: 0.95em 1.6em;
|
|
13
|
+
font-weight: 700;
|
|
14
|
+
font-size: 1rem;
|
|
15
|
+
letter-spacing: -0.01em;
|
|
16
|
+
border-radius: var(--radius-pill);
|
|
17
|
+
transition:
|
|
18
|
+
transform 0.18s ease,
|
|
19
|
+
background 0.2s ease,
|
|
20
|
+
box-shadow 0.2s ease,
|
|
21
|
+
color 0.2s ease;
|
|
22
|
+
text-align: center;
|
|
23
|
+
line-height: 1;
|
|
24
|
+
}
|
|
25
|
+
:is(.mkt, .atelic-ui) .btn:active {
|
|
26
|
+
transform: translateY(1px) scale(0.99);
|
|
27
|
+
}
|
|
28
|
+
:is(.mkt, .atelic-ui) .btn-primary {
|
|
29
|
+
background: var(--primary);
|
|
30
|
+
color: #fff;
|
|
31
|
+
box-shadow: var(--primary-shadow);
|
|
32
|
+
}
|
|
33
|
+
:is(.mkt, .atelic-ui) .btn-primary:hover {
|
|
34
|
+
background: var(--primary-deep);
|
|
35
|
+
box-shadow: var(--primary-shadow-hover);
|
|
36
|
+
transform: translateY(-1px);
|
|
37
|
+
}
|
|
38
|
+
:is(.mkt, .atelic-ui) .btn-dark {
|
|
39
|
+
background: var(--ink);
|
|
40
|
+
color: var(--surface);
|
|
41
|
+
}
|
|
42
|
+
:is(.mkt, .atelic-ui) .btn-dark:hover {
|
|
43
|
+
background: #000;
|
|
44
|
+
transform: translateY(-1px);
|
|
45
|
+
}
|
|
46
|
+
:is(.mkt, .atelic-ui) .btn-ghost {
|
|
47
|
+
background: transparent;
|
|
48
|
+
color: var(--ink);
|
|
49
|
+
box-shadow: inset 0 0 0 1.5px var(--neutral-300);
|
|
50
|
+
}
|
|
51
|
+
:is(.mkt, .atelic-ui) .btn-ghost:hover {
|
|
52
|
+
box-shadow: inset 0 0 0 1.5px var(--ink);
|
|
53
|
+
}
|
|
54
|
+
:is(.mkt, .atelic-ui) .btn-ghost.on-dark {
|
|
55
|
+
color: #fff;
|
|
56
|
+
box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.45);
|
|
57
|
+
}
|
|
58
|
+
:is(.mkt, .atelic-ui) .btn-ghost.on-dark:hover {
|
|
59
|
+
box-shadow: inset 0 0 0 1.5px #fff;
|
|
60
|
+
background: rgba(255, 255, 255, 0.08);
|
|
61
|
+
}
|
|
62
|
+
:is(.mkt, .atelic-ui) .btn-lg {
|
|
63
|
+
padding: 1.1em 2em;
|
|
64
|
+
font-size: 1.08rem;
|
|
65
|
+
}
|
|
66
|
+
:is(.mkt, .atelic-ui) .btn .arrow {
|
|
67
|
+
transition: transform 0.2s ease;
|
|
68
|
+
}
|
|
69
|
+
:is(.mkt, .atelic-ui) .btn:hover .arrow {
|
|
70
|
+
transform: translateX(3px);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Chip: small mono tag for credentials, feature tags, service areas. */
|
|
74
|
+
:is(.mkt, .atelic-ui) .chip {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 0.5em;
|
|
78
|
+
font-family: var(--font-mono);
|
|
79
|
+
font-size: 0.72rem;
|
|
80
|
+
letter-spacing: 0.06em;
|
|
81
|
+
text-transform: uppercase;
|
|
82
|
+
padding: 0.5em 0.85em;
|
|
83
|
+
border-radius: var(--radius-pill);
|
|
84
|
+
background: var(--card);
|
|
85
|
+
color: var(--ink-soft);
|
|
86
|
+
box-shadow: inset 0 0 0 1px var(--neutral-200);
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
}
|
|
89
|
+
:is(.mkt, .atelic-ui) .chip .dot {
|
|
90
|
+
width: 6px;
|
|
91
|
+
height: 6px;
|
|
92
|
+
border-radius: 50%;
|
|
93
|
+
background: var(--primary);
|
|
94
|
+
}
|
|
95
|
+
:is(.mkt, .atelic-ui) .chip svg {
|
|
96
|
+
flex: none;
|
|
97
|
+
}
|
|
98
|
+
:is(.mkt, .atelic-ui) .chip.on-dark {
|
|
99
|
+
background: rgba(255, 255, 255, 0.08);
|
|
100
|
+
color: #efe7da;
|
|
101
|
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Eyebrow: mono uppercase kicker trailed by a bar that fades off to the
|
|
105
|
+
right — the wordmark's signature, in the brand's primary. This fade is
|
|
106
|
+
the only rendering; the old leading-dash mode is retired. */
|
|
107
|
+
:is(.mkt, .atelic-ui) .eyebrow {
|
|
108
|
+
font-family: var(--font-mono);
|
|
109
|
+
font-size: 0.74rem;
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
letter-spacing: 0.18em;
|
|
112
|
+
text-transform: uppercase;
|
|
113
|
+
color: var(--primary);
|
|
114
|
+
display: inline-flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: 0.6em;
|
|
117
|
+
}
|
|
118
|
+
:is(.mkt, .atelic-ui) .eyebrow.on-dark {
|
|
119
|
+
color: var(--primary-bright);
|
|
120
|
+
}
|
|
121
|
+
:is(.mkt, .atelic-ui) .eyebrow.center {
|
|
122
|
+
justify-content: center;
|
|
123
|
+
}
|
|
124
|
+
:is(.mkt, .atelic-ui) .eyebrow::after {
|
|
125
|
+
content: "";
|
|
126
|
+
width: 40px;
|
|
127
|
+
height: 2px;
|
|
128
|
+
display: inline-block;
|
|
129
|
+
/* Themes with a dedicated ruling accent (a brand color reserved for
|
|
130
|
+
rules, never text) set --rule-accent; default stays the text color. */
|
|
131
|
+
background: linear-gradient(90deg, var(--rule-accent, currentColor), transparent);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
:is(.mkt, .atelic-ui) .lead {
|
|
135
|
+
font-size: clamp(1.05rem, 2vw, 1.3rem);
|
|
136
|
+
color: var(--ink-soft);
|
|
137
|
+
line-height: 1.55;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
:is(.mkt, .atelic-ui) .section-head {
|
|
141
|
+
/* Width is constrained on the lead only: titles size to their section so
|
|
142
|
+
they never wrap early into dead space (the 60ch cap here was measured
|
|
143
|
+
at the body font size, boxing every title into ~510px). */
|
|
144
|
+
max-width: none;
|
|
145
|
+
}
|
|
146
|
+
:is(.mkt, .atelic-ui) .section-head.center {
|
|
147
|
+
margin-inline: auto;
|
|
148
|
+
}
|
|
149
|
+
:is(.mkt, .atelic-ui) .section-head .section-head-lead {
|
|
150
|
+
margin-top: 16px;
|
|
151
|
+
max-width: 60ch;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a document's content out of every file the login page can load.
|
|
3
|
+
*
|
|
4
|
+
* With the wall armed, /assets/ stays open because the login page needs its
|
|
5
|
+
* own hashed CSS and JS to render (needsSession in src/gate/wall.ts). A content module that
|
|
6
|
+
* lands in a chunk there would be readable by anyone who opens /login, which
|
|
7
|
+
* is exactly how the example writeup and report first shipped: their route
|
|
8
|
+
* heads imported the content, so all of it rode in the entry bundle. So the
|
|
9
|
+
* client build writes every chunk carrying a private content module into
|
|
10
|
+
* /assets/doc/, which the wall holds back, and fails outright when one turns
|
|
11
|
+
* up anywhere else.
|
|
12
|
+
*
|
|
13
|
+
* Public content is the short list that is safe to ship to every page: the
|
|
14
|
+
* 404's copy, the sealed proposal (ciphertext by design, GATE.md), and the
|
|
15
|
+
* heads in `*.meta.ts`, which carry a title and nothing a snapshot produced.
|
|
16
|
+
* Every other module under src/content/ is treated as private, so a new
|
|
17
|
+
* artifact starts behind the wall.
|
|
18
|
+
*/
|
|
19
|
+
import type { Plugin } from "vite";
|
|
20
|
+
|
|
21
|
+
/** Where the client build writes the chunks the wall holds back. */
|
|
22
|
+
export const DOC_DIR = "assets/doc";
|
|
23
|
+
|
|
24
|
+
const PUBLIC_CONTENT = /\/src\/content\/(not-found\.tsx|proposal[^/]*|[^/]+\.meta\.ts)$/;
|
|
25
|
+
|
|
26
|
+
/** Whether a module id is content that must never ship to the login page. */
|
|
27
|
+
export function isPrivateContent(id: string): boolean {
|
|
28
|
+
const path = id.split("?")[0].replaceAll("\\", "/");
|
|
29
|
+
return path.includes("/src/content/") && !PUBLIC_CONTENT.test(path);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function privateChunks(): Plugin {
|
|
33
|
+
return {
|
|
34
|
+
name: "atelic:private-chunks",
|
|
35
|
+
configEnvironment(name) {
|
|
36
|
+
if (name !== "client") return;
|
|
37
|
+
return {
|
|
38
|
+
build: {
|
|
39
|
+
rollupOptions: {
|
|
40
|
+
output: {
|
|
41
|
+
chunkFileNames: (chunk) =>
|
|
42
|
+
chunk.moduleIds.some(isPrivateContent)
|
|
43
|
+
? `${DOC_DIR}/[name]-[hash].js`
|
|
44
|
+
: "assets/[name]-[hash].js",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
generateBundle(_options, bundle) {
|
|
51
|
+
if (this.environment?.name !== "client") return;
|
|
52
|
+
for (const file of Object.values(bundle)) {
|
|
53
|
+
if (file.type !== "chunk" || file.fileName.startsWith(`${DOC_DIR}/`)) continue;
|
|
54
|
+
const leaked = Object.entries(file.modules)
|
|
55
|
+
.filter(([id, module]) => module.renderedLength > 0 && isPrivateContent(id))
|
|
56
|
+
.map(([id]) => id.slice(id.indexOf("/src/") + 1));
|
|
57
|
+
if (leaked.length > 0) {
|
|
58
|
+
this.error(
|
|
59
|
+
`${file.fileName} would ship ${leaked.join(", ")} outside ${DOC_DIR}/, where the login page can load it with no session. A route's head is the usual cause: give it a *.meta.ts module and keep the content to the component.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|