affora 0.1.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 +16 -0
- package/CHECKS.md +40 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/agent.md +70 -0
- package/checks/cli.mjs +44 -0
- package/checks/substrate.mjs +269 -0
- package/cli/affora.mjs +131 -0
- package/design.md +113 -0
- package/package.json +71 -0
- package/registry.json +83 -0
- package/rules/antd.js +173 -0
- package/rules/generic.js +299 -0
- package/rules/hidden-radio.js +94 -0
- package/rules/magento.js +325 -0
- package/rules/mui.js +230 -0
- package/rules/shadcn-baseui.js +169 -0
- package/rules/shadcn-radix.js +289 -0
- package/src/components/combobox.tsx +279 -0
- package/src/components/datatable.tsx +454 -0
- package/src/components/dialog.tsx +548 -0
- package/src/components/productcard.tsx +551 -0
- package/src/components/settingsform.tsx +583 -0
- package/src/patterns/confirmundo.tsx +134 -0
- package/src/patterns/errremedy.tsx +155 -0
- package/src/patterns/flowform.tsx +179 -0
- package/src/patterns/gatedaction.tsx +157 -0
- package/src/patterns/persistentfeedback.tsx +106 -0
- package/src/primitives/actionbutton.tsx +64 -0
- package/src/primitives/textfield.tsx +57 -0
- package/src/tokens/themes.css +1068 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type React from "react"
|
|
2
|
+
import { useId, useState } from "react"
|
|
3
|
+
|
|
4
|
+
export type PersistentFeedbackProps = {
|
|
5
|
+
confirmationCode?: string
|
|
6
|
+
onCommit?: (code: string) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Action feedback that remains in the document until it is superseded. */
|
|
10
|
+
export const PersistentFeedback: React.FC<PersistentFeedbackProps> = ({
|
|
11
|
+
confirmationCode = "ORD-7788",
|
|
12
|
+
onCommit,
|
|
13
|
+
}) => {
|
|
14
|
+
const [placed, setPlaced] = useState(false)
|
|
15
|
+
const [value, setValue] = useState("")
|
|
16
|
+
const [error, setError] = useState("")
|
|
17
|
+
const [confirmed, setConfirmed] = useState(false)
|
|
18
|
+
const statusId = useId()
|
|
19
|
+
const inputId = useId()
|
|
20
|
+
const errorId = useId()
|
|
21
|
+
|
|
22
|
+
const place = () => {
|
|
23
|
+
setPlaced(true)
|
|
24
|
+
setConfirmed(false)
|
|
25
|
+
setError("")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const confirm = (event: React.FormEvent) => {
|
|
29
|
+
event.preventDefault()
|
|
30
|
+
const entered = value.trim()
|
|
31
|
+
if (!placed) {
|
|
32
|
+
setError("Place the order first. The confirmation code will remain in the status below.")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
if (entered !== confirmationCode) {
|
|
36
|
+
setError(`The code does not match. Enter ${confirmationCode}, shown in the persistent order status.`)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
setError("")
|
|
40
|
+
setConfirmed(true)
|
|
41
|
+
onCommit?.(entered)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<section className="apf-root" aria-labelledby={`${statusId}-title`}>
|
|
46
|
+
<style>{css}</style>
|
|
47
|
+
<header>
|
|
48
|
+
<h3 id={`${statusId}-title`}>Place order</h3>
|
|
49
|
+
<p>The result and confirmation code remain available after the action.</p>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
<button type="button" className="apf-place" onClick={place}>Place order</button>
|
|
53
|
+
|
|
54
|
+
<output id={statusId} className="apf-result" aria-live="polite">
|
|
55
|
+
<strong>Order status</strong>
|
|
56
|
+
{placed ? (
|
|
57
|
+
<span>Order placed. Confirmation code: <code>{confirmationCode}</code>.</span>
|
|
58
|
+
) : (
|
|
59
|
+
<span>No order placed yet.</span>
|
|
60
|
+
)}
|
|
61
|
+
</output>
|
|
62
|
+
|
|
63
|
+
<form className="apf-confirm" onSubmit={confirm} noValidate>
|
|
64
|
+
<label htmlFor={inputId}>Confirmation code</label>
|
|
65
|
+
<div>
|
|
66
|
+
<input
|
|
67
|
+
id={inputId}
|
|
68
|
+
value={value}
|
|
69
|
+
onChange={(event) => { setValue(event.target.value); setError(""); setConfirmed(false) }}
|
|
70
|
+
aria-invalid={error ? "true" : undefined}
|
|
71
|
+
aria-describedby={`${statusId}${error ? ` ${errorId}` : ""}`}
|
|
72
|
+
autoComplete="off"
|
|
73
|
+
/>
|
|
74
|
+
<button type="submit">Confirm order</button>
|
|
75
|
+
</div>
|
|
76
|
+
{error && <p id={errorId} className="apf-error" role="alert">{error}</p>}
|
|
77
|
+
</form>
|
|
78
|
+
|
|
79
|
+
<output className="apf-status" aria-live="polite">
|
|
80
|
+
{confirmed ? `Confirmed order with code ${confirmationCode}.` : "Order not confirmed yet."}
|
|
81
|
+
</output>
|
|
82
|
+
</section>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const css = `
|
|
87
|
+
.apf-root { box-sizing: border-box; display: grid; gap: var(--gap); width: 100%; max-width: 34rem; padding: var(--pad-loose); color: var(--fg); background: var(--glass-bg, var(--surface)); border: 1px solid var(--glass-border, var(--border)); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); font-family: var(--font); font-size: var(--text-base); line-height: var(--leading); }
|
|
88
|
+
.apf-root * { box-sizing: border-box; }
|
|
89
|
+
.apf-root h3 { margin: 0; font-family: var(--font-display); font-size: calc(var(--text-base) * 1.15); font-weight: var(--weight-display); letter-spacing: var(--tracking-tight); }
|
|
90
|
+
.apf-root header p, .apf-status { margin: 0; color: var(--fg-muted); font-size: calc(var(--text-base) * 0.9); }
|
|
91
|
+
.apf-root button { min-height: calc(var(--text-base) * 2.75); padding: var(--pad-tight) var(--pad); border-radius: var(--radius); font: inherit; font-weight: var(--weight-medium); cursor: pointer; }
|
|
92
|
+
.apf-place { color: var(--accent-fg); background: var(--accent); border: 1px solid var(--accent); }
|
|
93
|
+
.apf-result { display: grid; gap: calc(var(--gap) * 0.5); padding: var(--pad); color: var(--fg); background: var(--accent-weak); border: 1px solid var(--accent); border-radius: var(--radius); }
|
|
94
|
+
.apf-result code { padding: calc(var(--pad-tight) * 0.25) calc(var(--pad-tight) * 0.5); color: var(--fg); background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font-mono); font-weight: var(--weight-medium); }
|
|
95
|
+
.apf-confirm { display: grid; gap: calc(var(--gap) * 0.5); }
|
|
96
|
+
.apf-confirm > label { font-weight: var(--weight-medium); }
|
|
97
|
+
.apf-confirm > div { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--gap); }
|
|
98
|
+
.apf-confirm input { min-width: 0; min-height: calc(var(--text-base) * 2.75); padding: var(--pad-tight) var(--pad); color: var(--fg); background: var(--bg); border: 1px solid var(--border-strong); border-radius: var(--radius); font: inherit; }
|
|
99
|
+
.apf-confirm button { color: var(--accent); background: var(--bg); border: 1px solid var(--accent); }
|
|
100
|
+
.apf-confirm input[aria-invalid='true'] { border-color: var(--danger); }
|
|
101
|
+
.apf-error { margin: 0; padding: var(--pad-tight) var(--pad); color: var(--danger); background: var(--surface-2); border-left: 3px solid var(--danger); border-radius: var(--radius-sm); font-size: calc(var(--text-base) * 0.9); }
|
|
102
|
+
.apf-root input:focus-visible, .apf-root button:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
|
|
103
|
+
@media (max-width: 28rem) { .apf-confirm > div { grid-template-columns: 1fr; } }
|
|
104
|
+
`
|
|
105
|
+
|
|
106
|
+
export default PersistentFeedback
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type React from "react"
|
|
2
|
+
import { useId } from "react"
|
|
3
|
+
|
|
4
|
+
type CommonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
|
|
5
|
+
children: React.ReactNode
|
|
6
|
+
loading?: boolean
|
|
7
|
+
loadingLabel?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ActionButtonProps = CommonProps & (
|
|
11
|
+
| { tone?: "primary" | "secondary"; consequence?: never }
|
|
12
|
+
| { tone: "destructive"; consequence: string }
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
/** A named native action that keeps loading state and destructive consequences in text. */
|
|
16
|
+
export const ActionButton: React.FC<ActionButtonProps> = ({
|
|
17
|
+
children,
|
|
18
|
+
tone = "primary",
|
|
19
|
+
consequence,
|
|
20
|
+
loading = false,
|
|
21
|
+
loadingLabel = "Working",
|
|
22
|
+
disabled,
|
|
23
|
+
type = "button",
|
|
24
|
+
className = "",
|
|
25
|
+
...props
|
|
26
|
+
}) => {
|
|
27
|
+
const consequenceId = useId()
|
|
28
|
+
const statusId = useId()
|
|
29
|
+
const describedBy = [props["aria-describedby"], consequence ? consequenceId : null, statusId].filter(Boolean).join(" ")
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<span className={`aab-root ${className}`}>
|
|
33
|
+
<style>{css}</style>
|
|
34
|
+
<button
|
|
35
|
+
{...props}
|
|
36
|
+
type={type}
|
|
37
|
+
className="aab-button"
|
|
38
|
+
data-tone={tone}
|
|
39
|
+
disabled={disabled || loading}
|
|
40
|
+
aria-busy={loading || undefined}
|
|
41
|
+
aria-describedby={describedBy}
|
|
42
|
+
>
|
|
43
|
+
<span>{children}</span>
|
|
44
|
+
<span id={statusId} className="aab-progress" role="status" aria-live="polite">
|
|
45
|
+
{loading ? `${loadingLabel}…` : ""}
|
|
46
|
+
</span>
|
|
47
|
+
</button>
|
|
48
|
+
{consequence && <small id={consequenceId} className="aab-consequence">{consequence}</small>}
|
|
49
|
+
</span>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const css = `
|
|
54
|
+
.aab-root { display: inline-grid; gap: calc(var(--gap) * 0.5); color: var(--fg); font-family: var(--font); font-size: var(--text-base); line-height: var(--leading); }
|
|
55
|
+
.aab-button { display: inline-flex; align-items: center; justify-content: center; gap: var(--pad-tight); min-height: calc(var(--text-base) * 2.75); padding: var(--pad-tight) var(--pad); color: var(--accent-fg); background: var(--accent); border: 1px solid var(--accent); border-radius: var(--radius); font: inherit; font-weight: var(--weight-medium); cursor: pointer; transition: opacity var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease); }
|
|
56
|
+
.aab-button[data-tone='secondary'] { color: var(--fg); background: var(--surface); border-color: var(--border-strong); }
|
|
57
|
+
.aab-button[data-tone='destructive'] { color: var(--danger); background: var(--surface); border-color: var(--danger); }
|
|
58
|
+
.aab-button:disabled { cursor: not-allowed; opacity: 0.68; }
|
|
59
|
+
.aab-button:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
|
|
60
|
+
.aab-progress:empty { display: none; }
|
|
61
|
+
.aab-consequence { max-width: 32rem; color: var(--fg-muted); font-size: calc(var(--text-base) * 0.82); }
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
export default ActionButton
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type React from "react"
|
|
2
|
+
import { useId } from "react"
|
|
3
|
+
|
|
4
|
+
export type TextFieldProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "id"> & {
|
|
5
|
+
label: string
|
|
6
|
+
description?: string
|
|
7
|
+
constraint?: string
|
|
8
|
+
error?: string
|
|
9
|
+
id?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A native text field whose name, constraints, and remedy stay in the document. */
|
|
13
|
+
export const TextField: React.FC<TextFieldProps> = ({
|
|
14
|
+
label,
|
|
15
|
+
description,
|
|
16
|
+
constraint,
|
|
17
|
+
error,
|
|
18
|
+
id,
|
|
19
|
+
className = "",
|
|
20
|
+
...inputProps
|
|
21
|
+
}) => {
|
|
22
|
+
const generatedId = useId()
|
|
23
|
+
const inputId = id || generatedId
|
|
24
|
+
const descriptionId = `${inputId}-description`
|
|
25
|
+
const constraintId = `${inputId}-constraint`
|
|
26
|
+
const errorId = `${inputId}-error`
|
|
27
|
+
const describedBy = [description && descriptionId, constraint && constraintId, error && errorId, inputProps["aria-describedby"]].filter(Boolean).join(" ")
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<span className={`atf-root ${className}`}>
|
|
31
|
+
<style>{css}</style>
|
|
32
|
+
<label htmlFor={inputId}>{label}</label>
|
|
33
|
+
{description && <small id={descriptionId} className="atf-description">{description}</small>}
|
|
34
|
+
{constraint && <small id={constraintId} className="atf-constraint">Requirement: {constraint}</small>}
|
|
35
|
+
<input
|
|
36
|
+
{...inputProps}
|
|
37
|
+
id={inputId}
|
|
38
|
+
className="atf-input"
|
|
39
|
+
aria-invalid={error ? "true" : inputProps["aria-invalid"]}
|
|
40
|
+
aria-describedby={describedBy || undefined}
|
|
41
|
+
/>
|
|
42
|
+
{error && <small id={errorId} className="atf-error" role="alert">{error}</small>}
|
|
43
|
+
</span>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const css = `
|
|
48
|
+
.atf-root { display: grid; gap: calc(var(--gap) * 0.5); color: var(--fg); font-family: var(--font); font-size: var(--text-base); line-height: var(--leading); }
|
|
49
|
+
.atf-root > label { font-weight: var(--weight-medium); }
|
|
50
|
+
.atf-description, .atf-constraint { color: var(--fg-muted); font-size: calc(var(--text-base) * 0.86); }
|
|
51
|
+
.atf-input { width: 100%; min-height: calc(var(--text-base) * 2.75); padding: var(--pad-tight) var(--pad); color: var(--fg); background: var(--bg); border: 1px solid var(--border-strong); border-radius: var(--radius); font: inherit; }
|
|
52
|
+
.atf-input[aria-invalid='true'] { border-color: var(--danger); }
|
|
53
|
+
.atf-input:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
|
|
54
|
+
.atf-error { padding: var(--pad-tight) var(--pad); color: var(--danger); background: var(--surface-2); border-left: 3px solid var(--danger); border-radius: var(--radius-sm); font-size: calc(var(--text-base) * 0.86); }
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
export default TextField
|