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.
@@ -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