diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 439d801..e265012 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -24,4 +24,7 @@ jobs: if: always() with: name: browser-screenshots - path: docs/*.png + path: | + docs/*.png + docs/create-request/*.png + docs/create-request/*results.json diff --git a/README.md b/README.md index 3291719..74b0d80 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Support Desk Workbench -A support queue for triaging requests, recording internal notes and keeping work between visits. Built with React, TypeScript and Vite; an optional local Node.js server adds SQLite snapshots. +A support queue for creating and triaging requests, recording internal notes and keeping work between visits. Built with React, TypeScript and Vite; an optional local Node.js server adds SQLite snapshots. The create-request flow uses scoped Tailwind CSS utilities without Preflight. [Live demo](https://flankero-dev.github.io/support-desk-workbench/) · [Checks](https://github.com/flankero-dev/support-desk-workbench/actions/workflows/checks.yml) · [Engineering decisions](docs/decisions.md) @@ -10,6 +10,8 @@ The demo uses fictional customers and browser storage. It opens directly into a ## Review it in two minutes +Choose **New request** in the queue. Submit an empty form to see field errors, then enter a title, customer, category and description. New requests start as Open, with Normal priority and no assignee. Success appears only after browser storage accepts the write. Current filters and sorting stay intact; the confirmation explains if they hide the new request and offers **View request**. + 1. Search for **Maya** and open the checkout request. Change its status, add a note, then close it with **Escape**. 2. Reload. The URL restores the search; the edited request and note remain saved. 3. Clear the search, select **Open** and **Urgent**, name the filters and choose **Save view**. Clear the filters and reapply that view. @@ -67,6 +69,8 @@ npm run test:browser Tests use Node's built-in runner. Browser checks use the production build and cover filtering, pagination, saved views, editing, import/export, reset, keyboard focus, storage conflicts and server snapshots. CI runs these checks; the Pages deployment also checks the build before publishing. Browser screenshots are available as CI artifacts. +Create-request design and review: [design specification](docs/create-request-design.md), [QA evidence and limitations](docs/create-request-qa.md). No Figma file or Figma handoff was produced. Additional browser checks cover field validation, ID collisions, record/UTF-8 limits, failed persistence, multi-tab conflicts, responsive layout and axe accessibility scans. To reproduce the optional pre-change CSS comparison, build the base revision separately and set `BASELINE_DIST=/path/to/base/dist` when running `node tests/create-request-review.mjs`. + ## Boundaries - The public demo stores requests in this browser and profile. Export before clearing site data or switching browsers. diff --git a/docs/create-request-design.md b/docs/create-request-design.md new file mode 100644 index 0000000..fc01c58 --- /dev/null +++ b/docs/create-request-design.md @@ -0,0 +1,64 @@ +# Create request — design specification + +## Design source and scope + +This is a specification of the implemented scenario, derived from the existing Support Desk Workbench interface. **No Figma document was created or opened, and no Figma handoff is claimed.** No connected Figma editing/workspace capability was available. The screenshots below are actual browser captures of the production build, not a substitute presented as a Figma file. + +One action is added to the queue heading: **New request**. It opens a native modal dialog. The existing queue, detail view, design language and storage model remain in use. + +## Geometry and tokens + +| Element | Specification | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Dialog | Existing white surface, 1 px `#d3ded4` border, 10 px radius; width `calc(100% - 32px)`, maximum 600 px, maximum height 90 vh; vertical scrolling when needed | +| Backdrop | Existing `#142a2355` with 2 px blur | +| Inner padding | 20 px below 640 px viewport width; 24 px at/above 640 px | +| Grid | One column below 640 px; two equal columns at/above 640 px; 16 px gap; title/description span both columns | +| Heading | Existing font stack; 23 px, weight 600, line-height 1.3, tracking -0.4 px | +| Text | 14 px / 21 px, `#23332d`; secondary `#58695e`; labels 13 px, `#637369` | +| Controls | Minimum 44 px high; existing font, 6 px radius, 8 px vertical / 10 px horizontal padding; textarea minimum 112 px, vertically resizable | +| Control boundary | `#82948a`, approximately 3.21:1 against white; scoped to this dialog | +| Invalid | `#9b4335` border and 13 px error text; persistent label, `aria-invalid`, linked error description | +| Primary action | Existing `.primary`: `#174f42` background with white text; existing hover/focus treatment | +| Focus | 3 px outline, 3 px offset: `#276752` normally, `#9b4335` on invalid fields; focused elements scroll into view | +| Actions | 12 px gap; 44 px minimum buttons. Mobile actions remain within the dialog's scrollable content | +| Footer | 24 px top margin, 16 px top padding, existing divider color; browser-only persistence notice | + +Tailwind's spacing scale uses rem, while this application's root is 14 px. Intentional pixel values avoid accidentally turning a 44 px target into 38.5 px. The responsive boundary is verified immediately below/at 640 px. + +## Fields + +| Label | Data field | Default | Validation | +| ------------- | ------------- | --------------------------------------- | -------------------------------------------------------------------------- | +| Title * | `title` | Empty | Non-whitespace; trimmed, 1–200 characters | +| Customer * | `customer` | Empty | Non-whitespace; trimmed, 1–200 characters | +| Category * | `category` | Empty; suggestions from current tickets | Non-whitespace; trimmed, 1–200 characters; new categories allowed | +| Priority | `priority` | Normal | Existing priority enum | +| Assignee | `assignee` | Unassigned | Existing workspace options, including imported names | +| Description * | `description` | Empty | Non-whitespace; trimmed, 1–4000 characters; internal line breaks preserved | + +ID, status Open, timestamps and the initial history entry are generated, rather than additional user input. The whole candidate snapshot must still pass the existing importer, 500-record limit and 2 million UTF-8-byte limit. + +## States and behavior + +- **Default:** title receives initial focus; labels and required marks stay visible. Cancel, Close or Escape discard this unsubmitted draft. Reopening starts clean. +- **Invalid:** submit shows specific messages and focuses the first invalid field. Errors update as that field is corrected. Enter in a text field submits; Enter in description inserts a newline. +- **Persistence error:** the form stays open and retains all values. Failed storage, duplicate ID, capacity and another-tab conflicts produce actionable text. No success message or in-memory insert is committed for a rejected creation. +- **Success:** focus moves to the confirmation heading. The title is the primary 23 px semibold heading in the summary card; the confirmation label is 16 px. The full, selectable ID follows customer/priority as labelled 12 px secondary text. Long titles and IDs wrap without truncation. It offers View request or Back to queue. Existing filters and sort are not silently cleared; a hidden result is explained. A matching result's page is selected using its actual filtered/sorted index. +- **Loading/disabled:** no artificial loading delay or disabled-submit state is introduced. Creation is a synchronous browser-storage operation; there is no creation request to a server. Native controls and existing unrelated disabled buttons retain their behavior. +- Tab/Shift+Tab wrap within the modal; native modality keeps the background inert. Closing restores the New request trigger; View request opens the existing detail dialog. + +## CSS integration + +The Vite Tailwind plugin compiles `tw:`-prefixed utilities used only by App and CreateRequestDialog. Theme and utilities are imported separately; **Preflight is omitted**. Utilities are unlayered so existing unlayered element rules do not automatically outrank them. One scoped control-border rule matches the legacy `input:not([type=checkbox])` specificity. The invalid attribute utility is more specific. No global font, heading, input or button reset is added. + +## Browser comparison + +![Desktop default](create-request/desktop-default.png) +![Mobile default at 390 px](create-request/mobile-default.png) +![Desktop invalid](create-request/desktop-invalid.png) +![Mobile invalid, scrolled to the focused field](create-request/mobile-invalid.png) +![Desktop success](create-request/desktop-success.png) +![Mobile success](create-request/mobile-success.png) + +Measured widths, column counts, target heights and overflow checks are recorded in [browser results](create-request/browser-results.json). On small screens, the 90 vh dialog intentionally scrolls vertically; screenshots of invalid fields can therefore show the focused field instead of the header. This is not a claim that the full form fits without scrolling. diff --git a/docs/create-request-qa.md b/docs/create-request-qa.md new file mode 100644 index 0000000..04e369e --- /dev/null +++ b/docs/create-request-qa.md @@ -0,0 +1,100 @@ +# Create request — QA and review + +Date: 16 October 2026. Base: main `82614209f1d50933fe938ee871b41e488f1f1f8d` (tree `4c92e2835805b48adcc12f3868528015c0bcbd40`). + +## Recommendation + +**READY for owner review, conditional on green GitHub Checks for the final PR head.** No merge or public deployment was performed. This is a bounded feature, not an architecture migration. + +## Verified evidence + +| Check | Status | Evidence | +| ---------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| TypeScript strict, browser and server configurations | VERIFIED | `npm run build` | +| ESLint and formatting | VERIFIED | `npm run lint`, `npm run format:check` | +| Existing and new unit/integration tests | VERIFIED | 48 tests, including 7 creation-domain tests and a creation/SQLite/revision round trip | +| Existing browser regression suite | VERIFIED | `tests/browser-smoke.mjs` against the actual production build | +| Creation browser suite | VERIFIED | 8 scenario groups in `tests/create-request.browser.mjs`; [results](create-request/browser-results.json) | +| Independent production review pass | VERIFIED | Separate black-box assertions in `tests/create-request-review.mjs`; [results](create-request/review-results.json) | +| Empty/whitespace fields, enum and length boundaries | VERIFIED | Domain tests and browser invalid/first-error focus checks | +| ID collision | VERIFIED | Domain collision and browser stubbed UUID; original record/copy unchanged | +| 499 → 500 and rejection of request 501 | VERIFIED | Domain boundary and browser capacity error | +| 2 MB UTF-8 limit | VERIFIED | Domain multibyte growth and browser rejection with all description text retained | +| Persistence, quota failure and retry | VERIFIED | Reload retains creation/history; failed write changes neither saved copy nor queue; retry inserts once | +| Two tabs and storage-event timing | VERIFIED | Peer copy protected, stale draft retained; immediate read-check also catches a change without a delivered event | +| API revision conflict and SQLite restart | VERIFIED | Existing PUT snapshot route accepts creation; old ETag returns 412, duplicate document returns 400; revision/data survive restart | +| Public/static-host behavior | VERIFIED | Production assets served under a non-loopback test origin; creation + reload make zero API requests | +| Desktop/mobile layout | VERIFIED | 320, 390, 639, 640, 768, 1024, 1440 px; dialog/page horizontal overflow absent; new controls at least 44 px high | +| Keyboard and focus | VERIFIED | Initial title focus, Tab/Shift+Tab containment, first invalid focus, Enter/newline, Escape, cancel/reopen, success heading and return focus | +| Accessibility scan | VERIFIED within scan scope | Five default/invalid/success desktop/mobile axe scans, no violations. Some native-control contrast checks marked incomplete by axe; separate computed-color checks verify text >=4.5:1 and control boundaries >=3:1 | +| Existing CSS cascade | VERIFIED | Computed typography/colors/padding/borders for 8 unchanged interface selectors equal the separately built base revision | +| Literal hostile text | VERIFIED | HTML-like title remains text, no injected image or handler execution | +| Corrupt-data recovery | VERIFIED | Creation works from the existing sample fallback; resulting saved snapshot is valid and reloadable | + +The additional review pass uses the interface and production assets rather than asserting React state or Tailwind class strings. It was performed as a separate adversarial review by the implementation agent; it is **not an external human review**. Automated checks are supplemented by visual inspection of actual desktop/mobile captures. + +## Regressions exercised + +The unchanged browser smoke suite covers URL filters and sort, pagination, cross-page selection/bulk updates, ticket editing, notes/history, focus when an edited ticket leaves the filter, saved-view creation/apply/reload/duplicates/removal/two-tab and corrupt-storage behavior, JSON export/import/file restore, reset, long imported names, an assignee named All, large histories, size-rejected edits, local server snapshot refresh/save/load, stale revisions, unavailable Vite API and static-host controls. Creation tests additionally round-trip a created record, preserve a saved view and explain a filtered-out new request. + +## Limited UX polish review (16 October 2026) + +Only success-screen hierarchy and invalid-field focus color changed. The request title is now 23 px/600, ahead of the full labelled 12 px identifier. UUID generation, persistence, schema, import/export and request navigation are unchanged. Invalid keyboard focus uses the same red as the error border while retaining the existing solid 3 px outline and 3 px offset; valid controls retain green focus. + +- **VERIFIED:** all 48 unit/integration tests, strict TypeScript, ESLint, formatting, production build and all three production-browser scripts rerun successfully. +- **VERIFIED:** separate black-box polish assertions at 320, 390, 639, 640 and 1440 px. A 200-character unbroken title and a long Ukrainian title wrap without horizontal overflow; the entire ID remains rendered, the saved record survives reload unchanged, and keyboard Tab/Enter opens the same existing detail record. Escape/reopen starts a clean form. +- **VERIFIED:** computed invalid border/outline colors match, outline remains 3 px with 3 px offset, valid focus remains green. Seven axe scans have zero violations; minimum measured text contrast is 4.86:1. Eight existing interface selectors still match the main-branch baseline. +- **VERIFIED:** desktop/mobile screenshots inspected, including long titles and focused errors. Existing regression coverage listed below passed again, including multi-tab, limits, import/export and API revision conflicts. +- **UNVERIFIED:** external human review, human screen-reader sessions, Safari/Firefox and physical mobile devices. This separate review pass was performed by the implementation agent, not an independent person. + +[Desktop success](create-request/desktop-success.png) · [Mobile success](create-request/mobile-success.png) · [Desktop keyboard error](create-request/polish-1440-invalid.png) · [Mobile keyboard error](create-request/polish-390-invalid.png) · [Long title desktop](create-request/polish-1440-success.png) · [Long title mobile](create-request/polish-390-success.png). Machine-readable measurements are in `review-results.json` under `polish`. + +GitHub Actions must also pass for the new remote HEAD; the PR description records that exact SHA and run links after completion. No merge or deployment is authorized by this review. + +## Findings corrected during review + +1. Initial browser focus selected Close instead of Title; title is now focused after showModal. +2. Inline errors were changing the input's accessible name; each field now has a stable labelled-by span and a separately described error. +3. Native Tab boundary behavior was inconsistent with the intended contained sequence; explicit boundary wrapping covers both form and success states. +4. Tailwind rem sizing would shrink controls in this 14 px-root application; intentional pixel values retain 44 px targets and existing spacing. +5. Existing low-contrast input borders were unsuitable for the new controls; a scoped >=3:1 boundary was added without resetting the rest of the app. + +A failing draft test compared JSON string key order after canonical import, and a focus test selected a hidden saved-views dialog instead of the open dialog. These test defects were corrected; they are not represented as product fixes. + +## Limitations and remaining verification + +| Item | Status | Practical implication | +| ------------------------------------------------------------ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Real Figma file/handoff | UNVERIFIED / unavailable | [Exact design specification](create-request-design.md) and screenshots are provided; no Figma experience is claimed | +| Safari, Firefox, physical mobile browsers, virtual keyboard | UNVERIFIED | Local browser verification uses Chromium 153; CI uses Playwright's installed Chromium | +| NVDA/VoiceOver or another human screen-reader session | UNVERIFIED | Semantics, linked errors, focus and axe checked; no certified accessibility claim | +| Browser quota/private mode/power loss | ENVIRONMENT DEPENDENT | Quota failures are simulated; storage durability and limits remain browser-specific | +| Truly simultaneous multi-tab writes between read and setItem | ENVIRONMENT DEPENDENT | Existing localStorage optimistic guard is retained; it is not a transactional cross-tab compare-and-swap. Local API ETags/SQLite provide transactional revision checks | +| GitHub Actions on final head | ENVIRONMENT DEPENDENT until completed | Consult the PR Checks tab; local results do not substitute for a successful remote run | +| Next.js example | NOT STARTED | Requires a separate scenario and approval | + +No unresolved FAILED product check is being hidden. UNVERIFIED items above remain outside the demonstrated scope; do not describe this as “ALL PASSED.” + +## Architecture decisions + +- `appendRequest` is a pure typed domain operation. It trims user input, appends a history event, checks collisions and validates the candidate using the existing import/export schema. The original records are not mutated. +- Existing `setTickets` gains an opt-in persistence requirement. Creation checks the latest saved string and writes before committing React state, so failure keeps the form and queue unchanged. Existing edit/import in-tab fallback behavior is preserved. +- UUID-based IDs avoid depending on numeric seed IDs or imported ID formats. A detected collision rejects the operation rather than overwriting data. +- A synchronous local write needs neither an invented network request nor a spinner. The optional server still receives the full snapshot only when the user explicitly saves it through Server snapshot, with the existing ETag contract. +- Native dialog and small local form state keep this feature independent of authentication, global state libraries or new routing. Tailwind is confined to the new flow. + +## Technologies honestly demonstrated + +React component/state/event handling; strict TypeScript domain types; Tailwind/Vite integration and cascade control; responsive forms; native dialog focus behavior; accessible labels/validation; localStorage failure handling; Playwright and axe browser checks; Node/SQLite snapshot integration and optimistic revisions; GitHub CI configuration. **Not demonstrated by this change:** Figma handoff, Next.js, Redux, commercial experience, production authentication or cloud server persistence. + +## Interview questions + +1. Why does creation require persistence before updating React state, while existing edits allow an in-tab fallback? +2. How do validation, duplicate IDs and UTF-8 snapshot limits differ, and why validate the entire candidate snapshot? +3. Why omit Tailwind Preflight and use prefixed utilities? How do CSS layers and selector specificity affect legacy styles? +4. What should happen when the created ticket is hidden by current filters or appears on another page? +5. What protection does localStorage's optimistic guard provide, and how is it weaker than ETag/SQLite revision checking? + +![Before desktop](create-request/before-desktop.png) +![After desktop](create-request/desktop-default.png) +![After mobile](create-request/mobile-default.png) diff --git a/docs/create-request/before-desktop.png b/docs/create-request/before-desktop.png new file mode 100644 index 0000000..4e20dfd Binary files /dev/null and b/docs/create-request/before-desktop.png differ diff --git a/docs/create-request/before-mobile.png b/docs/create-request/before-mobile.png new file mode 100644 index 0000000..de13789 Binary files /dev/null and b/docs/create-request/before-mobile.png differ diff --git a/docs/create-request/browser-results.json b/docs/create-request/browser-results.json new file mode 100644 index 0000000..a73eba0 --- /dev/null +++ b/docs/create-request/browser-results.json @@ -0,0 +1,159 @@ +{ + "results": [ + { + "name": "keyboard, negative validation, successful save, Enter and focus restoration", + "status": "VERIFIED" + }, + { + "name": "current filters, saved view and sort survive creation; reset and JSON round-trip", + "status": "VERIFIED" + }, + { + "name": "quota error preserves all form fields and memory, then retry creates only once", + "status": "VERIFIED" + }, + { + "name": "another tab's creation is protected and unsaved draft survives conflict", + "status": "VERIFIED" + }, + { + "name": "immediate storage recheck works even without a delivered storage event", + "status": "VERIFIED" + }, + { + "name": "UUID collision never overwrites the existing record", + "status": "VERIFIED" + }, + { + "name": "500-record cap preserves the existing workspace and draft", + "status": "VERIFIED" + }, + { + "name": "desktop/mobile layout, minimum targets and modal keyboard containment", + "status": "VERIFIED" + } + ], + "measurements": [ + { + "viewport": 320, + "width": 288, + "overflow": false, + "pageOverflow": false, + "fieldHeights": [ + 44, + 44, + 44, + 44, + 44, + 44, + 44, + 44 + ], + "columns": "246px" + }, + { + "viewport": 390, + "width": 358, + "overflow": false, + "pageOverflow": false, + "fieldHeights": [ + 44, + 44, + 44, + 44, + 44, + 44, + 44, + 44 + ], + "columns": "316px" + }, + { + "viewport": 639, + "width": 600, + "overflow": false, + "pageOverflow": false, + "fieldHeights": [ + 44, + 44, + 44, + 44, + 44, + 44, + 44, + 44 + ], + "columns": "558px" + }, + { + "viewport": 640, + "width": 600, + "overflow": false, + "pageOverflow": false, + "fieldHeights": [ + 44, + 44, + 44, + 44, + 44, + 44, + 44, + 44 + ], + "columns": "267px 267px" + }, + { + "viewport": 768, + "width": 600, + "overflow": false, + "pageOverflow": false, + "fieldHeights": [ + 44, + 44, + 44, + 44, + 44, + 44, + 44, + 44 + ], + "columns": "267px 267px" + }, + { + "viewport": 1024, + "width": 600, + "overflow": false, + "pageOverflow": false, + "fieldHeights": [ + 44, + 44, + 44, + 44, + 44, + 44, + 44, + 44 + ], + "columns": "267px 267px" + }, + { + "viewport": 1440, + "width": 600, + "overflow": false, + "pageOverflow": false, + "fieldHeights": [ + 44, + 44, + 44, + 44, + 44, + 44, + 44, + 44 + ], + "columns": "267px 267px" + } + ], + "errors": [], + "browser": "153.0.8010.0" +} \ No newline at end of file diff --git a/docs/create-request/desktop-default.png b/docs/create-request/desktop-default.png new file mode 100644 index 0000000..0d58238 Binary files /dev/null and b/docs/create-request/desktop-default.png differ diff --git a/docs/create-request/desktop-invalid.png b/docs/create-request/desktop-invalid.png new file mode 100644 index 0000000..49bddb3 Binary files /dev/null and b/docs/create-request/desktop-invalid.png differ diff --git a/docs/create-request/desktop-success.png b/docs/create-request/desktop-success.png new file mode 100644 index 0000000..ba6d94f Binary files /dev/null and b/docs/create-request/desktop-success.png differ diff --git a/docs/create-request/mobile-default.png b/docs/create-request/mobile-default.png new file mode 100644 index 0000000..184a010 Binary files /dev/null and b/docs/create-request/mobile-default.png differ diff --git a/docs/create-request/mobile-invalid.png b/docs/create-request/mobile-invalid.png new file mode 100644 index 0000000..095ef54 Binary files /dev/null and b/docs/create-request/mobile-invalid.png differ diff --git a/docs/create-request/mobile-success.png b/docs/create-request/mobile-success.png new file mode 100644 index 0000000..f28df47 Binary files /dev/null and b/docs/create-request/mobile-success.png differ diff --git a/docs/create-request/polish-1440-invalid.png b/docs/create-request/polish-1440-invalid.png new file mode 100644 index 0000000..eb930f7 Binary files /dev/null and b/docs/create-request/polish-1440-invalid.png differ diff --git a/docs/create-request/polish-1440-success.png b/docs/create-request/polish-1440-success.png new file mode 100644 index 0000000..11ec933 Binary files /dev/null and b/docs/create-request/polish-1440-success.png differ diff --git a/docs/create-request/polish-390-invalid.png b/docs/create-request/polish-390-invalid.png new file mode 100644 index 0000000..29a6b90 Binary files /dev/null and b/docs/create-request/polish-390-invalid.png differ diff --git a/docs/create-request/polish-390-success.png b/docs/create-request/polish-390-success.png new file mode 100644 index 0000000..86bb740 Binary files /dev/null and b/docs/create-request/polish-390-success.png differ diff --git a/docs/create-request/queue-desktop.png b/docs/create-request/queue-desktop.png new file mode 100644 index 0000000..9d1c6be Binary files /dev/null and b/docs/create-request/queue-desktop.png differ diff --git a/docs/create-request/review-results.json b/docs/create-request/review-results.json new file mode 100644 index 0000000..937d502 --- /dev/null +++ b/docs/create-request/review-results.json @@ -0,0 +1,1025 @@ +{ + "browser": "153.0.8010.0", + "accessibility": [ + { + "state": "desktop default", + "violations": [], + "incomplete": [ + "color-contrast" + ] + }, + { + "state": "desktop invalid", + "violations": [], + "incomplete": [ + "color-contrast" + ] + }, + { + "state": "desktop success", + "violations": [], + "incomplete": [ + "color-contrast" + ] + }, + { + "state": "mobile default", + "violations": [], + "incomplete": [] + }, + { + "state": "mobile invalid", + "violations": [], + "incomplete": [ + "color-contrast" + ] + }, + { + "state": "polish 390 long-title success", + "violations": [], + "incomplete": [ + "color-contrast" + ] + }, + { + "state": "polish 1440 long-title success", + "violations": [], + "incomplete": [ + "color-contrast" + ] + } + ], + "contrast": [ + { + "state": "desktop default", + "minimumText": 5.015754885495116, + "minimumControlBorder": 3.205068171626354, + "measured": [ + { + "tag": "H2", + "text": "New request", + "textRatio": 13.266890495436922 + }, + { + "tag": "P", + "text": "Add a request to this browser’s workspace. Fields marked * are require", + "textRatio": 5.837870749755676 + }, + { + "tag": "BUTTON", + "text": "Close ×", + "textRatio": 10.56097848234087 + }, + { + "tag": "SPAN", + "text": "×", + "textRatio": 10.56097848234087 + }, + { + "tag": "SPAN", + "text": "Title *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-title-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Customer *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-customer-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Category *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-category-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Priority", + "textRatio": 5.015754885495116 + }, + { + "tag": "SELECT", + "text": "create-priority-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Assignee", + "textRatio": 5.015754885495116 + }, + { + "tag": "SELECT", + "text": "create-assignee-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Description *", + "textRatio": 5.015754885495116 + }, + { + "tag": "TEXTAREA", + "text": "create-description-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "0/4000 characters", + "textRatio": 5.837870749755676 + }, + { + "tag": "P", + "text": "Status: Open · Browser only", + "textRatio": 5.837870749755676 + }, + { + "tag": "BUTTON", + "text": "Cancel", + "textRatio": 10.56097848234087 + }, + { + "tag": "BUTTON", + "text": "Create request", + "textRatio": 9.40230837163695 + } + ] + }, + { + "state": "desktop invalid", + "minimumText": 5.015754885495116, + "minimumControlBorder": 3.205068171626354, + "measured": [ + { + "tag": "H2", + "text": "New request", + "textRatio": 13.266890495436922 + }, + { + "tag": "P", + "text": "Add a request to this browser’s workspace. Fields marked * are require", + "textRatio": 5.837870749755676 + }, + { + "tag": "BUTTON", + "text": "Close ×", + "textRatio": 10.56097848234087 + }, + { + "tag": "SPAN", + "text": "×", + "textRatio": 10.56097848234087 + }, + { + "tag": "SPAN", + "text": "Title *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-title-label", + "textRatio": 13.266890495436922, + "borderRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Enter a title.", + "textRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Customer *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-customer-label", + "textRatio": 13.266890495436922, + "borderRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Enter a customer.", + "textRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Category *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-category-label", + "textRatio": 13.266890495436922, + "borderRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Enter a category.", + "textRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Priority", + "textRatio": 5.015754885495116 + }, + { + "tag": "SELECT", + "text": "create-priority-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Assignee", + "textRatio": 5.015754885495116 + }, + { + "tag": "SELECT", + "text": "create-assignee-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Description *", + "textRatio": 5.015754885495116 + }, + { + "tag": "TEXTAREA", + "text": "create-description-label", + "textRatio": 13.266890495436922, + "borderRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Enter a description.", + "textRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "0/4000 characters", + "textRatio": 5.837870749755676 + }, + { + "tag": "P", + "text": "Status: Open · Browser only", + "textRatio": 5.837870749755676 + }, + { + "tag": "BUTTON", + "text": "Cancel", + "textRatio": 10.56097848234087 + }, + { + "tag": "BUTTON", + "text": "Create request", + "textRatio": 9.40230837163695 + } + ] + }, + { + "state": "desktop success", + "minimumText": 4.859231016510143, + "minimumControlBorder": null, + "measured": [ + { + "tag": "H2", + "text": "Request created", + "textRatio": 13.266890495436922 + }, + { + "tag": "P", + "text": "Saved in this browser. New requests start as Open.", + "textRatio": 13.266890495436922 + }, + { + "tag": "H3", + "text": "", + "textRatio": 12.322613650434448 + }, + { + "tag": "P", + "text": "Customer · Normal", + "textRatio": 5.422357696790662 + }, + { + "tag": "P", + "text": "Request ID: REQ-8fd88776-3978-4ca8-a184-4d496c7c0ce6", + "textRatio": 5.422357696790662 + }, + { + "tag": "SPAN", + "text": "REQ-8fd88776-3978-4ca8-a184-4d496c7c0ce6", + "textRatio": 4.859231016510143 + }, + { + "tag": "P", + "text": "Your queue is on the page containing this request, with the current so", + "textRatio": 13.266890495436922 + }, + { + "tag": "BUTTON", + "text": "Back to queue", + "textRatio": 10.56097848234087 + }, + { + "tag": "BUTTON", + "text": "View request", + "textRatio": 9.40230837163695 + } + ] + }, + { + "state": "mobile default", + "minimumText": 5.015754885495116, + "minimumControlBorder": 3.205068171626354, + "measured": [ + { + "tag": "H2", + "text": "New request", + "textRatio": 13.266890495436922 + }, + { + "tag": "P", + "text": "Add a request to this browser’s workspace. Fields marked * are require", + "textRatio": 5.837870749755676 + }, + { + "tag": "BUTTON", + "text": "Close ×", + "textRatio": 10.56097848234087 + }, + { + "tag": "SPAN", + "text": "×", + "textRatio": 10.56097848234087 + }, + { + "tag": "SPAN", + "text": "Title *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-title-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Customer *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-customer-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Category *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-category-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Priority", + "textRatio": 5.015754885495116 + }, + { + "tag": "SELECT", + "text": "create-priority-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Assignee", + "textRatio": 5.015754885495116 + }, + { + "tag": "SELECT", + "text": "create-assignee-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Description *", + "textRatio": 5.015754885495116 + }, + { + "tag": "TEXTAREA", + "text": "create-description-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "0/4000 characters", + "textRatio": 5.837870749755676 + }, + { + "tag": "P", + "text": "Status: Open · Browser only", + "textRatio": 5.837870749755676 + }, + { + "tag": "BUTTON", + "text": "Cancel", + "textRatio": 10.56097848234087 + }, + { + "tag": "BUTTON", + "text": "Create request", + "textRatio": 9.40230837163695 + } + ] + }, + { + "state": "mobile invalid", + "minimumText": 5.015754885495116, + "minimumControlBorder": 3.205068171626354, + "measured": [ + { + "tag": "H2", + "text": "New request", + "textRatio": 13.266890495436922 + }, + { + "tag": "P", + "text": "Add a request to this browser’s workspace. Fields marked * are require", + "textRatio": 5.837870749755676 + }, + { + "tag": "BUTTON", + "text": "Close ×", + "textRatio": 10.56097848234087 + }, + { + "tag": "SPAN", + "text": "×", + "textRatio": 10.56097848234087 + }, + { + "tag": "SPAN", + "text": "Title *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-title-label", + "textRatio": 13.266890495436922, + "borderRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Enter a title.", + "textRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Customer *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-customer-label", + "textRatio": 13.266890495436922, + "borderRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Enter a customer.", + "textRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Category *", + "textRatio": 5.015754885495116 + }, + { + "tag": "INPUT", + "text": "create-category-label", + "textRatio": 13.266890495436922, + "borderRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Enter a category.", + "textRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Priority", + "textRatio": 5.015754885495116 + }, + { + "tag": "SELECT", + "text": "create-priority-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Assignee", + "textRatio": 5.015754885495116 + }, + { + "tag": "SELECT", + "text": "create-assignee-label", + "textRatio": 13.266890495436922, + "borderRatio": 3.205068171626354 + }, + { + "tag": "SPAN", + "text": "Description *", + "textRatio": 5.015754885495116 + }, + { + "tag": "TEXTAREA", + "text": "create-description-label", + "textRatio": 13.266890495436922, + "borderRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "Enter a description.", + "textRatio": 6.465551692567027 + }, + { + "tag": "SPAN", + "text": "0/4000 characters", + "textRatio": 5.837870749755676 + }, + { + "tag": "P", + "text": "Status: Open · Browser only", + "textRatio": 5.837870749755676 + }, + { + "tag": "BUTTON", + "text": "Cancel", + "textRatio": 10.56097848234087 + }, + { + "tag": "BUTTON", + "text": "Create request", + "textRatio": 9.40230837163695 + } + ] + }, + { + "state": "polish 390 long-title success", + "minimumText": 4.859231016510143, + "minimumControlBorder": null, + "measured": [ + { + "tag": "H2", + "text": "Request created", + "textRatio": 13.266890495436922 + }, + { + "tag": "P", + "text": "Saved in this browser. New requests start as Open.", + "textRatio": 13.266890495436922 + }, + { + "tag": "H3", + "text": "Довга назва заявки Довга назва заявки Довга назва заявки Довга назва з", + "textRatio": 12.322613650434448 + }, + { + "tag": "P", + "text": "Customer · Normal", + "textRatio": 5.422357696790662 + }, + { + "tag": "P", + "text": "Request ID: REQ-c1653e91-da2c-485d-8a76-5ac4e9fce56d", + "textRatio": 5.422357696790662 + }, + { + "tag": "SPAN", + "text": "REQ-c1653e91-da2c-485d-8a76-5ac4e9fce56d", + "textRatio": 4.859231016510143 + }, + { + "tag": "P", + "text": "Your queue is on the page containing this request, with the current so", + "textRatio": 13.266890495436922 + }, + { + "tag": "BUTTON", + "text": "Back to queue", + "textRatio": 9.573371520139666 + }, + { + "tag": "BUTTON", + "text": "View request", + "textRatio": 9.40230837163695 + } + ] + }, + { + "state": "polish 1440 long-title success", + "minimumText": 4.859231016510143, + "minimumControlBorder": null, + "measured": [ + { + "tag": "H2", + "text": "Request created", + "textRatio": 13.266890495436922 + }, + { + "tag": "P", + "text": "Saved in this browser. New requests start as Open.", + "textRatio": 13.266890495436922 + }, + { + "tag": "H3", + "text": "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW", + "textRatio": 12.322613650434448 + }, + { + "tag": "P", + "text": "Customer · Normal", + "textRatio": 5.422357696790662 + }, + { + "tag": "P", + "text": "Request ID: REQ-a8cda6b3-4620-47a5-9095-7696c438860e", + "textRatio": 5.422357696790662 + }, + { + "tag": "SPAN", + "text": "REQ-a8cda6b3-4620-47a5-9095-7696c438860e", + "textRatio": 4.859231016510143 + }, + { + "tag": "P", + "text": "Your queue is on the page containing this request, with the current so", + "textRatio": 13.266890495436922 + }, + { + "tag": "BUTTON", + "text": "Back to queue", + "textRatio": 10.56097848234087 + }, + { + "tag": "BUTTON", + "text": "View request", + "textRatio": 9.40230837163695 + } + ] + } + ], + "baselineCSS": { + "status": "VERIFIED", + "before": { + "h1": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "32px", + "fontWeight": "600", + "lineHeight": "38.4px", + "color": "rgb(35, 51, 45)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderRadius": "0px", + "borderColor": "rgb(35, 51, 45)", + "padding": "0px" + }, + ".topbar": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "13px", + "fontWeight": "400", + "lineHeight": "19.5px", + "color": "rgb(83, 104, 90)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderRadius": "0px", + "borderColor": "rgb(83, 104, 90) rgb(83, 104, 90) rgb(226, 231, 225)", + "padding": "0px" + }, + ".metrics strong": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "29px", + "fontWeight": "500", + "lineHeight": "40.6px", + "color": "rgb(35, 51, 45)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderRadius": "0px", + "borderColor": "rgb(35, 51, 45)", + "padding": "0px" + }, + ".request-title": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "14px", + "fontWeight": "500", + "lineHeight": "21px", + "color": "rgb(43, 66, 53)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderRadius": "0px", + "borderColor": "rgb(43, 66, 53)", + "padding": "0px" + }, + "thead": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "12px", + "fontWeight": "400", + "lineHeight": "18px", + "color": "rgb(100, 112, 92)", + "backgroundColor": "rgb(248, 250, 246)", + "borderRadius": "0px", + "borderColor": "rgb(100, 112, 92)", + "padding": "0px" + }, + ".status": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "13px", + "fontWeight": "400", + "lineHeight": "19.5px", + "color": "rgb(78, 102, 50)", + "backgroundColor": "rgb(244, 247, 237)", + "borderRadius": "4px", + "borderColor": "rgb(227, 233, 213)", + "padding": "3px 7px" + }, + ".sort-label select": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "13px", + "fontWeight": "400", + "lineHeight": "normal", + "color": "rgb(35, 51, 45)", + "backgroundColor": "rgb(255, 255, 255)", + "borderRadius": "6px", + "borderColor": "rgb(229, 233, 227)", + "padding": "5px 23px 5px 7px" + }, + ".pagination button": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "13px", + "fontWeight": "500", + "lineHeight": "19.5px", + "color": "rgb(52, 66, 59)", + "backgroundColor": "rgb(255, 255, 255)", + "borderRadius": "6px", + "borderColor": "rgb(213, 221, 215)", + "padding": "5px 10px" + } + }, + "after": { + "h1": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "32px", + "fontWeight": "600", + "lineHeight": "38.4px", + "color": "rgb(35, 51, 45)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderRadius": "0px", + "borderColor": "rgb(35, 51, 45)", + "padding": "0px" + }, + ".topbar": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "13px", + "fontWeight": "400", + "lineHeight": "19.5px", + "color": "rgb(83, 104, 90)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderRadius": "0px", + "borderColor": "rgb(83, 104, 90) rgb(83, 104, 90) rgb(226, 231, 225)", + "padding": "0px" + }, + ".metrics strong": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "29px", + "fontWeight": "500", + "lineHeight": "40.6px", + "color": "rgb(35, 51, 45)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderRadius": "0px", + "borderColor": "rgb(35, 51, 45)", + "padding": "0px" + }, + ".request-title": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "14px", + "fontWeight": "500", + "lineHeight": "21px", + "color": "rgb(43, 66, 53)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderRadius": "0px", + "borderColor": "rgb(43, 66, 53)", + "padding": "0px" + }, + "thead": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "12px", + "fontWeight": "400", + "lineHeight": "18px", + "color": "rgb(100, 112, 92)", + "backgroundColor": "rgb(248, 250, 246)", + "borderRadius": "0px", + "borderColor": "rgb(100, 112, 92)", + "padding": "0px" + }, + ".status": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "13px", + "fontWeight": "400", + "lineHeight": "19.5px", + "color": "rgb(78, 102, 50)", + "backgroundColor": "rgb(244, 247, 237)", + "borderRadius": "4px", + "borderColor": "rgb(227, 233, 213)", + "padding": "3px 7px" + }, + ".sort-label select": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "13px", + "fontWeight": "400", + "lineHeight": "normal", + "color": "rgb(35, 51, 45)", + "backgroundColor": "rgb(255, 255, 255)", + "borderRadius": "6px", + "borderColor": "rgb(229, 233, 227)", + "padding": "5px 23px 5px 7px" + }, + ".pagination button": { + "fontFamily": "\"DM Sans\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", + "fontSize": "13px", + "fontWeight": "500", + "lineHeight": "19.5px", + "color": "rgb(52, 66, 59)", + "backgroundColor": "rgb(255, 255, 255)", + "borderRadius": "6px", + "borderColor": "rgb(213, 221, 215)", + "padding": "5px 10px" + } + } + }, + "checks": [ + { + "name": "Enter first error and literal hostile text", + "status": "VERIFIED" + }, + { + "name": "browser UTF-8 2MB rejection preserves draft and saved copy", + "status": "VERIFIED" + }, + { + "name": "creation from existing corrupt-data recovery fallback", + "status": "VERIFIED" + }, + { + "name": "static-host creation and reload with zero API requests", + "status": "VERIFIED" + } + ], + "polish": [ + { + "width": 320, + "status": "VERIFIED", + "focusStyle": { + "active": true, + "visible": true, + "border": "rgb(155, 67, 53)", + "outline": "rgb(155, 67, 53)", + "width": "3px", + "offset": "3px", + "style": "solid" + }, + "hierarchy": { + "titleSize": 23, + "idSize": 12, + "weight": 600, + "titleBeforeId": true, + "overflow": false, + "titleOverflow": false + }, + "fullIdPreserved": true, + "reload": true, + "keyboardViewRequest": true + }, + { + "width": 390, + "status": "VERIFIED", + "focusStyle": { + "active": true, + "visible": true, + "border": "rgb(155, 67, 53)", + "outline": "rgb(155, 67, 53)", + "width": "3px", + "offset": "3px", + "style": "solid" + }, + "hierarchy": { + "titleSize": 23, + "idSize": 12, + "weight": 600, + "titleBeforeId": true, + "overflow": false, + "titleOverflow": false + }, + "fullIdPreserved": true, + "reload": true, + "keyboardViewRequest": true + }, + { + "width": 639, + "status": "VERIFIED", + "focusStyle": { + "active": true, + "visible": true, + "border": "rgb(155, 67, 53)", + "outline": "rgb(155, 67, 53)", + "width": "3px", + "offset": "3px", + "style": "solid" + }, + "hierarchy": { + "titleSize": 23, + "idSize": 12, + "weight": 600, + "titleBeforeId": true, + "overflow": false, + "titleOverflow": false + }, + "fullIdPreserved": true, + "reload": true, + "keyboardViewRequest": true + }, + { + "width": 640, + "status": "VERIFIED", + "focusStyle": { + "active": true, + "visible": true, + "border": "rgb(155, 67, 53)", + "outline": "rgb(155, 67, 53)", + "width": "3px", + "offset": "3px", + "style": "solid" + }, + "hierarchy": { + "titleSize": 23, + "idSize": 12, + "weight": 600, + "titleBeforeId": true, + "overflow": false, + "titleOverflow": false + }, + "fullIdPreserved": true, + "reload": true, + "keyboardViewRequest": true + }, + { + "width": 1440, + "status": "VERIFIED", + "focusStyle": { + "active": true, + "visible": true, + "border": "rgb(155, 67, 53)", + "outline": "rgb(155, 67, 53)", + "width": "3px", + "offset": "3px", + "style": "solid" + }, + "hierarchy": { + "titleSize": 23, + "idSize": 12, + "weight": 600, + "titleBeforeId": true, + "overflow": false, + "titleOverflow": false + }, + "fullIdPreserved": true, + "reload": true, + "keyboardViewRequest": true + } + ] +} \ No newline at end of file diff --git a/docs/create-request/save-error.png b/docs/create-request/save-error.png new file mode 100644 index 0000000..efdecc8 Binary files /dev/null and b/docs/create-request/save-error.png differ diff --git a/docs/create-request/tab-conflict.png b/docs/create-request/tab-conflict.png new file mode 100644 index 0000000..70eb825 Binary files /dev/null and b/docs/create-request/tab-conflict.png differ diff --git a/package-lock.json b/package-lock.json index 03c825c..96ef66b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "react-dom": "^19.2.6" }, "devDependencies": { + "@axe-core/playwright": "^4.11.1", "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.0", "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -23,11 +25,25 @@ "globals": "^17.6.0", "playwright": "^1.58.2", "prettier": "3.9.10", + "tailwindcss": "^4.3.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12" } }, + "node_modules/@axe-core/playwright": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.1.tgz", + "integrity": "sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.1" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -839,6 +855,278 @@ "dev": true, "license": "MIT" }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1210,6 +1498,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1372,6 +1670,20 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1718,6 +2030,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -1785,6 +2104,16 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2150,6 +2479,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -2515,6 +2854,27 @@ "node": ">=0.10.0" } }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", diff --git a/package.json b/package.json index dfa31a8..6cfc93c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "eslint .", "preview": "vite preview", "test": "node --test tests/*.test.mjs", - "test:browser": "node tests/browser-smoke.mjs", + "test:browser": "node tests/browser-smoke.mjs && node tests/create-request.browser.mjs && node tests/create-request-review.mjs", "start": "node server/start.ts", "format": "prettier --write src tests index.html README.md \"*.json\" \"*.js\" server \"*.ts\" \"docs/*.md\" .github/workflows", "format:check": "prettier --check src tests index.html README.md \"*.json\" \"*.js\" server \"*.ts\" \"docs/*.md\" .github/workflows" @@ -19,7 +19,9 @@ "react-dom": "^19.2.6" }, "devDependencies": { + "@axe-core/playwright": "^4.11.1", "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.0", "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -30,6 +32,7 @@ "globals": "^17.6.0", "playwright": "^1.58.2", "prettier": "3.9.10", + "tailwindcss": "^4.3.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12" diff --git a/src/App.tsx b/src/App.tsx index b9641d8..17e40a0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,10 @@ import { useWorkspace } from "./useWorkspace.ts"; import { TicketDialog } from "./components/TicketDialog.tsx"; import { ImportDialog } from "./components/ImportDialog.tsx"; import { ServerSnapshotDialog } from "./components/ServerSnapshotDialog.tsx"; +import { CreateRequestDialog } from "./components/CreateRequestDialog.tsx"; +import { appendRequest } from "./domain/createRequest.ts"; import "./styles.css"; +import "./create-request.css"; const pageSize = 8; export default function App() { @@ -31,6 +34,8 @@ export default function App() { const [selected, setSelected] = useState([]); const [activeId, setActiveId] = useState(null); const [importing, setImporting] = useState(false); + const [creating, setCreating] = useState(false); + const newRequestRef = useRef(null); const [serverSnapshotOpen, setServerSnapshotOpen] = useState(false); const [bulkStatus, setBulkStatus] = useState("In progress"); const [message, setMessage] = useState(""); @@ -269,16 +274,25 @@ export default function App() {

Requests {results.length}

- + +

+ {creating && ( + t.category)), + ).sort()} + onClose={(openId) => { + setCreating(false); + newRequestRef.current?.focus(); + if (openId) setActiveId(openId); + }} + onCreate={(draft) => { + const id = `REQ-${crypto.randomUUID()}`; + const at = new Date().toISOString(); + const updated = setTickets( + (current) => appendRequest(current, draft, at, id).tickets, + { requirePersistence: true }, + ); + const ticket = updated[updated.length - 1]; + const matches = filterTickets(updated, filters); + const index = matches.findIndex((match) => match.id === ticket.id); + if (index >= 0) setPage(Math.floor(index / pageSize) + 1); + setMessage( + `Created ${ticket.id}. Saved in this browser.${index < 0 ? " Hidden by current search or filters." : ""}`, + ); + return { ticket, matches: index >= 0 }; + }} + /> + )} {active && ( Created; + onClose: (openId?: string) => void; +}) { + const ref = useRef(null); + const resultRef = useRef(null); + const errorRef = useRef(null); + const viewAfterClose = useRef(undefined); + const committed = useRef(false); + const [draft, setDraft] = useState({ ...emptyRequest }); + const [errors, setErrors] = useState({}); + const [failure, setFailure] = useState(""); + const [created, setCreated] = useState(null); + useEffect(() => { + ref.current?.showModal(); + ref.current?.querySelector("#create-title")?.focus(); + }, []); + useEffect(() => { + if (created) resultRef.current?.focus(); + }, [created]); + useEffect(() => { + if (failure) errorRef.current?.focus(); + }, [failure]); + function change( + field: K, + value: RequestDraft[K], + ) { + const next = { ...draft, [field]: value }; + setDraft(next); + if (errors[field]) + setErrors((current) => ({ + ...current, + [field]: validateRequest(next)[field], + })); + setFailure(""); + } + const fieldClass = + "tw:w-full tw:min-h-[44px] tw:aria-invalid:border-[#9b4335]"; + function field(field: keyof RequestDraft) { + return { + id: `create-${field}`, + name: field, + "aria-labelledby": `create-${field}-label`, + "aria-invalid": Boolean(errors[field]), + "aria-describedby": errors[field] ? `create-${field}-error` : undefined, + }; + } + function error(field: keyof RequestDraft) { + return errors[field] ? ( + + {errors[field]} + + ) : null; + } + return ( + onClose(viewAfterClose.current)} + onKeyDown={(event) => { + if (event.key !== "Tab") return; + const controls = Array.from( + ref.current?.querySelectorAll( + "button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled)", + ) ?? [], + ); + const first = controls[0]; + const last = controls[controls.length - 1]; + const active = document.activeElement; + if ( + !controls.includes(active as HTMLElement) || + (event.shiftKey && active === first) || + (!event.shiftKey && active === last) + ) { + event.preventDefault(); + (event.shiftKey ? last : first)?.focus(); + } + }} + > +
+ {created ? ( +
+

+ Request created +

+

+ Saved in this browser. New requests start as Open. +

+
+

+ {created.ticket.title} +

+

+ {created.ticket.customer} · {created.ticket.priority} +

+

+ Request ID: {created.ticket.id} +

+
+

+ {created.matches + ? "Your queue is on the page containing this request, with the current sort order." + : "This request is hidden by your current search or filters. View it directly or change the filters in the queue."} +

+
+ + +
+
+ ) : ( +
{ + event.preventDefault(); + if (committed.current) return; + const invalid = validateRequest(draft); + setErrors(invalid); + setFailure(""); + const first = Object.keys(invalid)[0]; + if (first) { + ref.current + ?.querySelector(`#create-${first}`) + ?.focus(); + return; + } + try { + const result = onCreate(draft); + committed.current = true; + setCreated(result); + } catch (cause) { + setFailure( + cause instanceof Error + ? cause.message + : "The request could not be created. Your form is unchanged.", + ); + } + }} + > +
+
+

+ New request +

+

+ Add a request to this browser’s workspace. Fields marked * are + required. +

+
+ +
+
+ + + + + +