testio-tailwind 3.26.2 → 3.27.1

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/.eleventy.js CHANGED
@@ -45,7 +45,7 @@ module.exports = function (eleventyConfig) {
45
45
  includes: "_includes",
46
46
  layouts: "_layouts"
47
47
  },
48
- templateFormats: ["html", "md", "njk", "pug", "haml"],
48
+ templateFormats: ["html", "md", "njk", "pug", "haml", "11ty.js"],
49
49
  htmlTemplateEngine: "njk",
50
50
 
51
51
  // 1.1 Enable eleventy to pass dirs specified above
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "testio-tailwind",
3
- "version": "3.26.2",
3
+ "version": "3.27.1",
4
4
  "description": "Tailwind based design system for Test IO",
5
5
  "scripts": {
6
- "clean": "del dist --force",
6
+ "clean": "npx del-cli dist --force",
7
7
  "create-dist": "mkdirp dist",
8
8
  "create-static": "mkdirp dist/static",
9
9
  "dev:11ty": "eleventy --serve --watch",
@@ -61,6 +61,20 @@
61
61
  </details>
62
62
 
63
63
  <div class="navlinks right">
64
+ <details class="popover-wrapper dropright component-search">
65
+ <summary class="btn btn-square navlink">
66
+ <span class="icon icon-search" aria-hidden="true"></span>
67
+ <span class="sr-only">Search components</span>
68
+ </summary>
69
+ <div class="popover-menu search">
70
+ <form class="form-search inverted" role="search">
71
+ <input type="search" class="form-control" id="component-search-input" placeholder="Search components..." autocomplete="off" aria-label="Search components">
72
+ <button type="button" class="btn btn-clear" aria-label="Clear search"></button>
73
+ <button type="button" class="btn btn-submit" aria-label="Search"></button>
74
+ </form>
75
+ <div id="component-search-results" class="list-searchresults" role="listbox" aria-label="Search results" hidden></div>
76
+ </div>
77
+ </details>
64
78
  <button onclick="(() => document.body.classList.toggle('dark'))()" class="navlink">
65
79
  <svg class="w-icon h-icon fill-purple hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
66
80
  <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
@@ -1,3 +1,4 @@
1
+ import "./modules/component_search.js";
1
2
  import autosize from "autosize";
2
3
  import Dropzone from "dropzone";
3
4
  import $ from "jquery";
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Component search - fetches search-index.json and filters results on input.
3
+ * Uses the search popover pattern from /forms/#Search.
4
+ * Shows popover with max 8 results, navigates on click.
5
+ */
6
+ const MAX_RESULTS = 8;
7
+
8
+ let searchIndex = [];
9
+
10
+ async function loadSearchIndex() {
11
+ try {
12
+ const res = await fetch("/search-index.json");
13
+ if (res.ok) {
14
+ searchIndex = await res.json();
15
+ }
16
+ } catch (e) {
17
+ console.warn("Component search: could not load index", e);
18
+ }
19
+ }
20
+
21
+ function slugifySection(name) {
22
+ return name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, " $1").trim();
23
+ }
24
+
25
+ function search(term) {
26
+ if (!term || term.length < 2) return [];
27
+ const lower = term.toLowerCase();
28
+ return searchIndex
29
+ .filter(
30
+ (item) =>
31
+ item.title.toLowerCase().includes(lower) ||
32
+ (item.section && slugifySection(item.section).toLowerCase().includes(lower))
33
+ )
34
+ .slice(0, MAX_RESULTS);
35
+ }
36
+
37
+ function showResults(results) {
38
+ const container = document.getElementById("component-search-results");
39
+ if (!container) return;
40
+
41
+ if (results.length === 0) {
42
+ container.innerHTML = '<div class="component-search-empty">No components found</div>';
43
+ container.hidden = false;
44
+ return;
45
+ }
46
+
47
+ container.innerHTML = results
48
+ .map(
49
+ (item) =>
50
+ `<a href="${escapeHtml(item.url)}" class="issue-item">
51
+ <span class="issue-title">${escapeHtml(item.title)}</span>
52
+ <span class="issue-info">${escapeHtml(slugifySection(item.section))}</span>
53
+ </a>`
54
+ )
55
+ .join("");
56
+ container.hidden = false;
57
+ }
58
+
59
+ function hideResults() {
60
+ const container = document.getElementById("component-search-results");
61
+ if (container) {
62
+ container.hidden = true;
63
+ }
64
+ }
65
+
66
+ function escapeHtml(str) {
67
+ const div = document.createElement("div");
68
+ div.textContent = str;
69
+ return div.innerHTML;
70
+ }
71
+
72
+ function initComponentSearch() {
73
+ const componentSearch = document.querySelector(".component-search");
74
+ const input = document.getElementById("component-search-input");
75
+ const container = document.getElementById("component-search-results");
76
+ const details = componentSearch?.closest("details");
77
+ if (!input || !container) return;
78
+
79
+ let debounceTimer;
80
+
81
+ // Focus input when popover opens
82
+ details?.addEventListener("toggle", () => {
83
+ if (details.open) {
84
+ setTimeout(() => input.focus(), 0);
85
+ hideResults();
86
+ }
87
+ });
88
+
89
+ input.addEventListener("input", () => {
90
+ clearTimeout(debounceTimer);
91
+ const term = input.value.trim();
92
+ if (term.length < 2) {
93
+ hideResults();
94
+ return;
95
+ }
96
+ debounceTimer = setTimeout(() => {
97
+ showResults(search(term));
98
+ }, 150);
99
+ });
100
+
101
+ input.addEventListener("focus", () => {
102
+ const term = input.value.trim();
103
+ if (term.length >= 2) {
104
+ showResults(search(term));
105
+ }
106
+ });
107
+
108
+ input.addEventListener("keydown", (e) => {
109
+ if (e.key === "Escape") {
110
+ hideResults();
111
+ if (details) {
112
+ details.removeAttribute("open");
113
+ }
114
+ }
115
+ });
116
+
117
+ // Clear button
118
+ const clearBtn = componentSearch?.querySelector(".btn-clear");
119
+ clearBtn?.addEventListener("click", (e) => {
120
+ e.preventDefault();
121
+ input.value = "";
122
+ hideResults();
123
+ input.focus();
124
+ });
125
+
126
+ // Prevent form submit
127
+ const form = componentSearch?.querySelector("form");
128
+ form?.addEventListener("submit", (e) => {
129
+ e.preventDefault();
130
+ const term = input.value.trim();
131
+ if (term.length >= 2) {
132
+ showResults(search(term));
133
+ }
134
+ });
135
+ }
136
+
137
+ // Initialize when DOM ready
138
+ if (document.readyState === "loading") {
139
+ document.addEventListener("DOMContentLoaded", () => {
140
+ loadSearchIndex().then(initComponentSearch);
141
+ });
142
+ } else {
143
+ loadSearchIndex().then(initComponentSearch);
144
+ }
@@ -60,6 +60,7 @@
60
60
  @import './components/scrollbars.css' layer(components);
61
61
  @import './components/search.css' layer(components);
62
62
  @import './components/sections.css' layer(components);
63
+ @import './components/stepper.css' layer(components);
63
64
  @import './components/select.css' layer(components);
64
65
  @import './components/selectable_token.css' layer(components);
65
66
  @import './components/sidebar.css' layer(components);
@@ -13,11 +13,6 @@
13
13
  @apply text-black;
14
14
  }
15
15
 
16
- .sidebar.manager summary:after,
17
- .header.manager summary:after {
18
- @apply bg-black;
19
- }
20
-
21
16
  .sidebar.manager .navlink.active,
22
17
  .header.manager .navlink.active {
23
18
  @apply text-black bg-navlink-manager-active-bg;
@@ -116,3 +116,8 @@
116
116
  .dropdown-menu .issue-info {
117
117
  @apply text-label-inverted-color;
118
118
  }
119
+
120
+ .popover-menu .task-item .task-title, .popover-menu .issue-item .issue-title,
121
+ .dropdown-menu .issue-title {
122
+ @apply text-white;
123
+ }
@@ -0,0 +1,116 @@
1
+ /* Stepper - Multi-step wizard component (converted from testio-designsystem) */
2
+
3
+ .stepper {
4
+ @apply flex flex-row items-center flex-wrap gap-0;
5
+ }
6
+
7
+ .stepper-step {
8
+ @apply flex flex-row items-center shrink-0;
9
+ }
10
+
11
+ .stepper-step .stepper-icon {
12
+ @apply flex items-center justify-center shrink-0 w-8 h-8 text-base font-semibold rounded-full border-2 border-gray-300 bg-white text-label-color;
13
+ }
14
+
15
+ .stepper-step .stepper-label {
16
+ @apply ml-xs font-semibold text-sm text-appbody-textcolor;
17
+ }
18
+
19
+ /* Connector line between steps */
20
+ .stepper-step:not(:last-child)::after {
21
+ content: "";
22
+ @apply w-8 md:w-12 mx-xs h-px bg-bordercolor self-center;
23
+ }
24
+
25
+ /* Completed state */
26
+ .stepper-step.completed .stepper-icon {
27
+ @apply border-success bg-success text-white;
28
+ }
29
+
30
+ .stepper-step.completed .stepper-icon .icon {
31
+ @apply text-white;
32
+ }
33
+
34
+ .stepper-step.completed .stepper-label {
35
+ @apply text-success;
36
+ }
37
+
38
+ /* Active state */
39
+ .stepper-step.active .stepper-icon {
40
+ @apply border-none bg-primary text-white border-2;
41
+ }
42
+
43
+ .stepper-step.active .stepper-icon .icon {
44
+ @apply text-white;
45
+ }
46
+
47
+ .stepper-step.active .stepper-label {
48
+ @apply text-primary;
49
+ }
50
+
51
+ /* Pending state (default) */
52
+ .stepper-step:not(.completed):not(.active) .stepper-icon {
53
+ @apply border-0 bg-gray-lighter text-label-color;
54
+ }
55
+
56
+ .stepper-step:not(.completed):not(.active) .stepper-label {
57
+ @apply text-label-color;
58
+ }
59
+
60
+ /* Dark mode */
61
+ .dark .stepper-step .stepper-icon {
62
+ @apply border-bordercolor bg-gray-800 text-label-color;
63
+ }
64
+
65
+ .dark .stepper-step.completed .stepper-icon {
66
+ @apply border-success bg-success;
67
+ }
68
+
69
+ .dark .stepper-step.active .stepper-icon {
70
+ @apply border-primary bg-primary;
71
+ }
72
+
73
+ .dark .stepper-step:not(.completed):not(.active) .stepper-icon {
74
+ @apply bg-gray-800;
75
+ }
76
+
77
+ /* ===== Vertical stepper ===== */
78
+
79
+ .stepper-vertical {
80
+ @apply flex flex-col items-stretch gap-0;
81
+ }
82
+
83
+ .stepper-vertical .stepper-label {
84
+ @apply ml-0 pt-2;
85
+ }
86
+
87
+ .stepper-vertical .stepper-step {
88
+ @apply flex flex-row items-start gap-0 pb-md last:pb-0;
89
+ }
90
+
91
+ .stepper-vertical .stepper-step .stepper-icon {
92
+ @apply shrink-0 relative z-10;
93
+ }
94
+
95
+ .stepper-vertical .stepper-step .stepper-container {
96
+ @apply flex flex-col flex-1 min-w-0 ml-xs gap-0;
97
+ }
98
+
99
+ .stepper-vertical .stepper-step .stepper-container .stepper-label {
100
+ @apply mb-xxs;
101
+ }
102
+
103
+ /* Hide horizontal connector for vertical layout */
104
+ .stepper-vertical .stepper-step:not(:last-child)::after {
105
+ content: none;
106
+ }
107
+
108
+ /* Vertical connector line - runs from bottom of icon down to next step */
109
+ .stepper-vertical .stepper-step:not(:last-child) {
110
+ @apply relative;
111
+ }
112
+
113
+ .stepper-vertical .stepper-step:not(:last-child)::before {
114
+ content: "";
115
+ @apply block absolute left-4 top-8 bottom-0 w-px bg-bordercolor -translate-x-1/2 z-0;
116
+ }
@@ -0,0 +1,67 @@
1
+ ---
2
+ tags: components
3
+ title: Stepper
4
+ ---
5
+
6
+ %p.mb-heading Stepper component for multi-step wizard flows. Use for checkout, onboarding, or any sequential process.
7
+ %p.mb-sm
8
+ Completed steps show a check icon. The active step is highlighted. Pending steps are muted.
9
+ .mb-md
10
+ .stepper
11
+ .stepper-step.completed
12
+ .stepper-icon
13
+ %span.icon.icon-check
14
+ .stepper-label Step 1
15
+ .stepper-step.active
16
+ .stepper-icon 2
17
+ .stepper-label Step 2
18
+ .stepper-step
19
+ .stepper-icon 3
20
+ .stepper-label Step 3
21
+ .stepper-step
22
+ .stepper-icon 4
23
+ .stepper-label Step 4
24
+
25
+ %p.mb-heading.mt-lg Four-step wizard example with all completed except the last.
26
+ .mb-md
27
+ .stepper
28
+ .stepper-step.completed
29
+ .stepper-icon
30
+ %span.icon.icon-check
31
+ .stepper-label Details
32
+ .stepper-step.completed
33
+ .stepper-icon
34
+ %span.icon.icon-check
35
+ .stepper-label Payment
36
+ .stepper-step.completed
37
+ .stepper-icon
38
+ %span.icon.icon-check
39
+ .stepper-label Review
40
+ .stepper-step.active
41
+ .stepper-icon 4
42
+ .stepper-label Confirm
43
+
44
+ %p.mb-heading.mt-lg Vertical stepper variant. Add the .stepper-vertical class to the container. Use .stepper-container to wrap the label and content for each step.
45
+ .mb-md
46
+ .stepper.stepper-vertical
47
+ .stepper-step.completed
48
+ .stepper-icon
49
+ %span.icon.icon-check
50
+ .stepper-container
51
+ .stepper-label Enter your details
52
+ %p.text-sm.text-label-color.mt-xxs Name, email, and shipping address.
53
+ .stepper-step.active
54
+ .stepper-icon 2
55
+ .stepper-container
56
+ .stepper-label Choose payment method
57
+ %p.text-sm.text-label-color.mt-xxs Credit card or PayPal.
58
+ .stepper-step
59
+ .stepper-icon 3
60
+ .stepper-container
61
+ .stepper-label Review order
62
+ %p.text-sm.text-label-color.mt-xxs Verify your order before confirming.
63
+ .stepper-step
64
+ .stepper-icon 4
65
+ .stepper-container
66
+ .stepper-label Confirmation
67
+ %p.text-sm.text-label-color.mt-xxs Order complete.
@@ -0,0 +1,57 @@
1
+ module.exports = class {
2
+ data() {
3
+ return {
4
+ permalink: "/search-index.json",
5
+ eleventyExcludeFromCollections: true,
6
+ };
7
+ }
8
+
9
+ render(data) {
10
+ const collectionMap = {
11
+ components: "/components/",
12
+ button: "/buttons/",
13
+ forms: "/forms/",
14
+ tables: "/tables/",
15
+ charts: "/charts/",
16
+ layout: "/layout/",
17
+ navigation: "/navigation/",
18
+ typography: "/typography/",
19
+ agenticqa: "/agenticqa/",
20
+ icons: "/icons/",
21
+ issuing: "/issuing/",
22
+ layouts: null, // layouts have their own URLs
23
+ };
24
+
25
+ const items = [];
26
+
27
+ for (const [collName, baseUrl] of Object.entries(collectionMap)) {
28
+ if (!baseUrl || !data.collections[collName]) continue;
29
+ const collection = data.collections[collName];
30
+ for (const item of collection) {
31
+ if (item.data && item.data.title) {
32
+ const anchor = encodeURIComponent(item.data.title);
33
+ items.push({
34
+ title: item.data.title,
35
+ url: baseUrl + "#" + anchor,
36
+ section: collName,
37
+ });
38
+ }
39
+ }
40
+ }
41
+
42
+ // Add layout pages (standalone pages with their own URL)
43
+ if (data.collections.layouts) {
44
+ for (const item of data.collections.layouts) {
45
+ if (item.data && item.data.title && item.url) {
46
+ items.push({
47
+ title: item.data.title,
48
+ url: item.url.startsWith("/") ? item.url : "/" + item.url,
49
+ section: "layouts",
50
+ });
51
+ }
52
+ }
53
+ }
54
+
55
+ return JSON.stringify(items);
56
+ }
57
+ };