diff --git a/src/components/ActivityForm.css b/src/components/ActivityForm.css index 4d38e37..4cacf91 100644 --- a/src/components/ActivityForm.css +++ b/src/components/ActivityForm.css @@ -245,11 +245,14 @@ display: none; } -.ff__btn { +.ff__drop { + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + width: 100%; font: inherit; - font-size: var(--fs-xs); - font-weight: 500; - padding: 14px 14px; + padding: 16px 12px; border-radius: var(--r-md); cursor: pointer; border: 1px dashed var(--zk-blue-light); @@ -258,28 +261,56 @@ text-align: center; transition: background var(--t-fast), border-color var(--t-fast), box-shadow var(--t-fast); } +.ff__drop svg { width: 20px; height: 20px; } +.ff__drop strong { font-size: var(--fs-2xs); font-weight: 500; } +.ff__drop span { font-size: var(--fs-3xs); color: var(--zk-grey); } -.ff__btn:hover:not(:disabled) { +.ff__drop:hover:not(:disabled) { background: var(--zk-white); border-style: solid; border-color: var(--zk-blue); - box-shadow: var(--ring); } +.ff__drop:focus-visible { outline: none; box-shadow: var(--ring); } +.ff__drop:disabled { opacity: 0.65; cursor: default; border-style: solid; } -.ff__btn:disabled { - opacity: 0.6; - cursor: default; - border-style: solid; +/* Uploaded: the file as a fact, not a call to action. */ +.ff__file { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 9px 9px 11px; + border: 1px solid var(--zk-line); + border-radius: var(--r-md); + background: var(--zk-white); } +.ff__file > svg { width: 15px; height: 15px; flex: none; color: var(--zk-blue-dark); } -.ff__got { +.ff__name { + flex: 1; + min-width: 0; font-size: var(--fs-2xs); - color: var(--zk-muted); + color: var(--zk-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ff__replace { + flex: none; + font: inherit; + font-size: var(--fs-3xs); + font-weight: 500; + padding: 4px 10px; + border: 1px solid var(--zk-line); + border-radius: var(--r-pill); + background: var(--zk-white); + color: var(--zk-blue-dark); + cursor: pointer; + transition: background var(--t-fast), border-color var(--t-fast); +} +.ff__replace:hover { background: var(--zk-tint-blue); border-color: var(--zk-blue-mid); } +.ff__replace:focus-visible { outline: none; box-shadow: var(--ring); } + .ff__read { border: 1px solid var(--zk-blue-light); border-left-width: 3px; @@ -349,3 +380,19 @@ } .af__err--soft ul { margin: 6px 0 0; padding-left: 18px; } .af__err--soft li { margin: 2px 0; } + +/* ---- form sections ---- */ +.af__sec + .af__sec { margin-top: 22px; } + +.af__sech { + margin: 0 0 10px; + font-size: var(--fs-3xs); + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--zk-grey); +} + +/* Uploads size to their content and never stretch to the tallest card in the + row, so a document that has been read does not pull its neighbours down. */ +.af__grid--docs { align-items: start; } diff --git a/src/components/ActivityForm.jsx b/src/components/ActivityForm.jsx index 8e564b3..8ee1ab3 100644 --- a/src/components/ActivityForm.jsx +++ b/src/components/ActivityForm.jsx @@ -267,21 +267,26 @@ export default function ActivityForm({ activityUid, instanceId, lead, onDone, on if (error && !schema) return
Could not load the form — {describeError(error).title}
if (!schema) return

Loading the form…

- return ( -
-
- {/* Stamped fields are in `fields` — they validate and they submit — - but they are not drawn. See STAMPED_FIELDS. + // Stamped fields are in `fields` — they validate and they submit — but they + // are not drawn. See STAMPED_FIELDS. A stamped field that came back EMPTY is + // drawn anyway: source_channel is mandatory, so a prefill that did not + // resolve would otherwise fail validation against a box that is not on the + // screen and cannot be filled. + const visible = fields.filter((f) => !fieldIsStamped(f.id) || isEmpty(values[f.id])) - A stamped field that came back EMPTY is drawn anyway. source_channel - is mandatory, so a prefill that did not resolve would otherwise fail - validation against a box that is not on the screen and cannot be - filled — the exact failure the field filter elsewhere in this file - guards against. Hidden when it is answered; asked when it is not. */} - {fields.filter((f) => !fieldIsStamped(f.id) || isEmpty(values[f.id])).map((f) => { - const opts = f.properties?.options || [] - const v = values[f.id] ?? (f.data_type === 'multiselect' ? [] : '') - return ( + /* DOCUMENTS GET THEIR OWN ROW. In one flat auto-fit grid the uploads sat + beside plain inputs, and an upload that has read a document is several + times taller than a text box — so a row carried three tall OCR cards and + three short fields, and the form went ragged with the inputs stranded up + beside the buttons. Split in two, each grid is internally one height. */ + const isDoc = (f) => f.data_type === 'file' || f.data_type === 'ocr' + const docFields = visible.filter(isDoc) + const dataFields = visible.filter((f) => !isDoc(f)) + + const renderField = (f) => { + const opts = f.properties?.options || [] + const v = values[f.id] ?? (f.data_type === 'multiselect' ? [] : '') + return ( - ) - })} -
+ ) + } + + return ( + + {docFields.length ? ( +
+

Documents

+
{docFields.map(renderField)}
+
+ ) : null} + + {dataFields.length ? ( +
+ {docFields.length ?

Details

: null} +
{dataFields.map(renderField)}
+
+ ) : null} {/* The form's own complaint, before anything is sent. */} {missing.length ? ( diff --git a/src/components/FileField.jsx b/src/components/FileField.jsx index a640da9..94e54e6 100644 --- a/src/components/FileField.jsx +++ b/src/components/FileField.jsx @@ -61,20 +61,40 @@ export default function FileField({ field, value, instanceId, activityUid, onCha } } + const name = files.length ? (files[0].original_name || files[0].uuid) : '' + const open = () => inputRef.current?.click() + return (
- - {files.length ? ( -
{files[0].original_name || files[0].uuid}
- ) : null} + {/* Two states, not one button that changes its words. Nothing uploaded is + an invitation — a dashed target that says what it takes. Something + uploaded is a fact — the file, named, with a quiet way to swap it. The + old single pink slab stayed the same size and weight either way, which + made a finished upload shout as loudly as an empty one. */} + {files.length && !busy ? ( +
+ + {name} + +
+ ) : ( + + )} {extracted ? (