Skip to content

Commit 252b3f4

Browse files
Mihai-MunteanuadrianthedevPaul-Bob
authored
UI4: inputs (#4234)
* Adjust input design * adjust the danger file to check the full history * Enhance date and time picker helpers in test suite - Added `set_picker_dates` method to set multiple dates in the picker. - Introduced `set_picker_time` method for setting specific hour, minute, and second values in the picker. - Updated tests to utilize new helper methods * add wait_for_loaded in CodeField spec to ensure proper loading before editing --------- Co-authored-by: Adrian Marin <adrian@adrianthedev.com> Co-authored-by: Paul Bob <paul.ionut.bob@gmail.com>
1 parent 0270c01 commit 252b3f4

File tree

83 files changed

+2223
-215
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+2223
-215
lines changed

.github/workflows/danger.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ jobs:
88
runs-on: ubuntu-latest
99
steps:
1010
- uses: actions/checkout@v4
11+
with:
12+
fetch-depth: 0
1113

1214
- uses: ruby/setup-ruby@v1
1315
with:

app/assets/stylesheets/application.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
--text-tiny: 0.625rem;
5252
}
5353

54-
@plugin '@tailwindcss/forms';
5554
@plugin '@tailwindcss/typography';
5655

5756
/* Add dark mode variant */
@@ -103,6 +102,7 @@
103102
@import "./css/fields/status.css";
104103
@import "./css/fields/code.css";
105104
@import "./css/fields/progress.css";
105+
@import "./css/fields/key_value.css";
106106
@import "./css/fields/trix.css";
107107
@import "./css/fields/tags.css";
108108
@import "./css/fields/tiptap.css";
@@ -116,8 +116,11 @@
116116
@import "./css/components/ui/description_list.css";
117117
@import "./css/components/ui/tabs.css";
118118
@import "./css/components/ui/badge.css";
119+
@import "./css/components/ui/file_upload_input.css";
120+
@import "./css/components/ui/file_upload_item.css";
119121
@import "./css/components/ui/dropdown.css";
120122
@import "./css/components/ui/radio_button.css";
123+
@import "./css/components/input.css";
121124

122125
@import "./css/components/field-wrapper.css";
123126
@import "./css/components/avatar.css";

app/assets/stylesheets/css/components/field-wrapper.css

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,65 @@
77
}
88

99
.field-wrapper__help {
10-
@apply text-content-secondary mt-2 text-sm;
10+
@apply text-content-secondary mt-2 text-xs leading-4;
1111
}
1212

1313
.field-wrapper__label-help {
14-
/* pb-3 offsets the pt of the label */
1514
@apply text-content-secondary text-xs leading-none font-normal pb-3;
1615
}
1716

1817
.field-wrapper__error {
19-
@apply text-danger mt-2 text-sm;
18+
@apply text-danger-content mt-2 text-xs leading-4 inline-flex items-center gap-0.5;
19+
}
20+
21+
.field-wrapper__error::before {
22+
content: "";
23+
@apply inline-block size-3 shrink-0;
24+
background-color: currentColor;
25+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke-width='2' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M10 8l4 8'/%3E%3Cpath d='M10 16l4 -8'/%3E%3C/svg%3E") no-repeat center / contain;
26+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke-width='2' stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M10 8l4 8'/%3E%3Cpath d='M10 16l4 -8'/%3E%3C/svg%3E") no-repeat center / contain;
2027
}
2128

2229
.field-wrapper__content {
2330
@apply flex-1 flex flex-row px-4;
2431
}
2532

2633
.field-wrapper__content-wrapper {
27-
@apply w-full md:w-8/12;
34+
@apply relative w-full md:w-8/12;
35+
}
36+
37+
.field-wrapper__input {
38+
@apply relative w-full;
39+
}
40+
41+
/* ==========================================================================
42+
Loading Spinner
43+
========================================================================== */
44+
45+
.input-wrapper--loading {
46+
@apply relative w-full;
47+
}
48+
49+
.input-wrapper--loading::after {
50+
@apply absolute top-1/2 rounded-full border-2 border-content border-e-tertiary pointer-events-none;
51+
52+
content: '';
53+
inset-inline-end: var(--input-icon-offset);
54+
width: var(--input-icon-size);
55+
height: var(--input-icon-size);
56+
transform: translateY(-50%);
57+
transform-origin: center;
58+
animation: input-spinner-rotation 0.9s linear infinite;
59+
}
60+
61+
@keyframes input-spinner-rotation {
62+
from { transform: translateY(-50%) rotate(0deg); }
63+
to { transform: translateY(-50%) rotate(360deg); }
64+
}
65+
66+
/* disabled & loading state */
67+
.input-wrapper--loading:has(input:disabled)::after {
68+
@apply border-content-secondary border-e-tertiary cursor-not-allowed;
2869
}
2970

3071
.field-wrapper--full-width {
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
:root {
2+
--input-py: --spacing(1);
3+
--input-font-size: --spacing(3.5);
4+
--input-leading: --spacing(5.5);
5+
--input-border-w: 1px;
6+
7+
--input-icon-offset: --spacing(2);
8+
--input-icon-size: --spacing(4.5);
9+
--input-icon-gap: --spacing(1);
10+
}
11+
12+
/* ==========================================================================
13+
Base
14+
========================================================================== */
15+
@layer base {
16+
select,
17+
textarea,
18+
.textarea-field,
19+
input:not([type="range"]):not([type="checkbox"]):not([type="radio"]),
20+
.input-field,
21+
tags.tagify {
22+
@apply appearance-none inline-flex bg-primary text-content
23+
rounded-lg border border-tertiary ps-2 pe-2 font-normal overflow-hidden
24+
text-ellipsis whitespace-nowrap relative w-full;
25+
26+
/* box-shadow: var(--box-shadow); */
27+
padding-block: var(--input-py);
28+
font-size: var(--input-font-size);
29+
line-height: var(--input-leading);
30+
}
31+
32+
tags.tagify {
33+
@apply overflow-visible whitespace-normal p-2 text-clip;
34+
}
35+
36+
textarea,
37+
.textarea-field {
38+
@apply block text-ellipsis whitespace-normal wrap-break-word;
39+
}
40+
41+
/* ==========================================================================
42+
Size Variants — Tailwind utilities + token overrides
43+
========================================================================== */
44+
45+
.input--size-sm {
46+
--input-py: --spacing(1);
47+
--input-font-size: --spacing(3);
48+
--input-leading: --spacing(4);
49+
50+
--input-icon-size: --spacing(4);
51+
}
52+
53+
.input--size-md {
54+
--input-py: --spacing(1);
55+
--input-font-size: --spacing(3.5);
56+
--input-leading: --spacing(5.5);
57+
58+
--input-icon-size: --spacing(4.5);
59+
}
60+
61+
.input--size-lg {
62+
--input-py: --spacing(2);
63+
--input-font-size: --spacing(3.5);
64+
--input-leading: --spacing(5.5);
65+
66+
--input-icon-size: --spacing(5);
67+
}
68+
69+
/* For icons and with shortcut search command + k where the size depends on the parent size*/
70+
:has(> .input--size-sm) {
71+
--input-icon-size: --spacing(4);
72+
--input-leading: --spacing(3.5);
73+
}
74+
:has(> .input--size-md) {
75+
--input-icon-size: --spacing(4.5);
76+
--input-leading: --spacing(4);
77+
}
78+
:has(> .input--size-lg) {
79+
--input-icon-size: --spacing(5);
80+
--input-leading: --spacing(4.5);
81+
}
82+
83+
/* ==========================================================================
84+
Disabled State
85+
========================================================================== */
86+
87+
/* Disabled input — cursor on parent (input has pointer-events-none) */
88+
.field-wrapper__content-wrapper:has(input:disabled),
89+
.field-wrapper__content-wrapper:has(select:disabled),
90+
.field-wrapper__content-wrapper:has(textarea:disabled),
91+
.field-wrapper__content-wrapper:has(.input-field:disabled) {
92+
@apply cursor-not-allowed;
93+
}
94+
95+
select:disabled,
96+
textarea:disabled,
97+
.textarea-field:disabled,
98+
input:not([type="range"]):not([type="checkbox"]):not([type="radio"]):disabled,
99+
.input-field:disabled {
100+
@apply pointer-events-none text-content-secondary;
101+
}
102+
/* ==========================================================================
103+
Placeholder
104+
========================================================================== */
105+
106+
*::placeholder {
107+
@apply text-content-secondary;
108+
}
109+
110+
/* ==========================================================================
111+
States: Focus
112+
========================================================================== */
113+
114+
select:focus-visible,
115+
textarea:focus-visible,
116+
.textarea-field:focus-visible,
117+
input:not([type="range"]):not([type="checkbox"]):not([type="radio"]):focus-visible,
118+
.input-field:focus-visible
119+
{
120+
@apply outline-none;
121+
box-shadow: var(--box-shadow-focus);
122+
}
123+
}
124+
125+
@layer component {
126+
/* ==========================================================================
127+
States: Error, Success
128+
========================================================================== */
129+
select.input-field--error,
130+
textarea.input-field--error,
131+
.textarea-field.input-field--error,
132+
input:not([type="range"]):not([type="checkbox"]):not([type="radio"]).input-field--error,
133+
.input-field.input-field--error
134+
{
135+
box-shadow: var(--box-shadow-error);
136+
}
137+
138+
select.input-field--success,
139+
textarea.input-field--success,
140+
.textarea-field.input-field--success,
141+
input:not([type="range"]):not([type="checkbox"]):not([type="radio"]).input-field--success,
142+
.input-field.input-field--success {
143+
box-shadow: var(--box-shadow-success);
144+
}
145+
146+
/* ==========================================================================
147+
Color Input
148+
========================================================================== */
149+
150+
input[type="color"] {
151+
@apply cursor-pointer;
152+
153+
height: calc(var(--input-leading) + var(--input-py) * 2 + var(--input-border-w) * 2);
154+
}
155+
156+
/* ==========================================================================
157+
Password Visibility
158+
========================================================================== */
159+
160+
.input-field__password-toggle {
161+
@apply absolute text-content-secondary inset-y-0 end-0 flex items-center cursor-pointer;
162+
163+
padding-inline-end: var(--input-icon-offset);
164+
}
165+
166+
/* Wrapper-based selector: reliably targets password inputs with visibility toggle
167+
(User create resource, password confirmation, etc.) */
168+
.field-wrapper__input:has(.input-field__password-toggle) input {
169+
padding-inline-end: calc(
170+
var(--input-icon-size) +
171+
var(--input-icon-offset) +
172+
var(--input-icon-gap)
173+
);
174+
}
175+
176+
/* Unified icon sizing — reads from size token */
177+
.input-field__password-toggle svg,
178+
.input-wrapper--loading::after,
179+
.search-input__prefix svg {
180+
@apply shrink-0;
181+
width: var(--input-icon-size);
182+
height: var(--input-icon-size);
183+
}
184+
185+
/* Loading: show spinner only — hide visibility toggle */
186+
.input-wrapper--loading .input-field__password-toggle {
187+
@apply hidden;
188+
}
189+
190+
/* ==========================================================================
191+
Date Input
192+
========================================================================== */
193+
194+
input[type="date"]::-webkit-calendar-picker-indicator,
195+
.input-field--date::-webkit-calendar-picker-indicator {
196+
@apply opacity-0 absolute end-0 top-0 h-full cursor-pointer;
197+
}
198+
199+
input[type="date"],
200+
.input-field--date {
201+
@apply bg-no-repeat relative;
202+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12'/%3E%3Cpath d='M16 3v4'/%3E%3Cpath d='M8 3v4'/%3E%3Cpath d='M4 11h16'/%3E%3Cpath d='M11 15h1'/%3E%3Cpath d='M12 15v3'/%3E%3C/svg%3E");
203+
background-position: right var(--input-icon-offset) center;
204+
background-size: var(--input-icon-size);
205+
}
206+
207+
input[type="date"]:disabled,
208+
.input-field--date:disabled {
209+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12'/%3E%3Cpath d='M16 3v4'/%3E%3Cpath d='M8 3v4'/%3E%3Cpath d='M4 11h16'/%3E%3Cpath d='M11 15h1'/%3E%3Cpath d='M12 15v3'/%3E%3C/svg%3E");
210+
}
211+
212+
/* ==========================================================================
213+
Search Input
214+
========================================================================== */
215+
216+
.search-input {
217+
@apply relative w-full;
218+
}
219+
220+
.search-input__prefix {
221+
@apply absolute inset-y-0 start-0 flex items-center pointer-events-none text-content-secondary z-10 ps-[var(--input-icon-offset)];
222+
}
223+
224+
/* Input padding — prefix side, suffix side (no shortcut / with shortcut) */
225+
input[type=search],
226+
.search-input__input {
227+
padding-inline-start: calc(
228+
var(--input-icon-offset) +
229+
var(--input-icon-size) +
230+
var(--input-icon-gap)
231+
);
232+
padding-inline-end: calc(
233+
var(--input-icon-offset) +
234+
var(--input-icon-gap)
235+
);
236+
}
237+
238+
/* Mac: ⌘ + K — narrower */
239+
.search-input:has(.mac_class) .search-input__input--with-shortcut {
240+
padding-inline-end: calc(
241+
var(--input-icon-offset) +
242+
var(--input-icon-size) * 2 +
243+
var(--input-icon-gap) * 3
244+
);
245+
}
246+
247+
/* PC: CTRL + K — wider (only one abbr in DOM, so :has() matches exclusively) */
248+
.search-input:has(.pc_class) .search-input__input--with-shortcut {
249+
padding-inline-end: calc(
250+
var(--input-icon-offset) +
251+
var(--input-icon-size) * 3 +
252+
var(--input-icon-gap) * 4
253+
);
254+
}
255+
/* Suffix (shortcut) */
256+
.search-input__suffix {
257+
@apply absolute inset-y-0 end-0 flex items-center text-content-secondary pointer-events-none z-10 gap-1;
258+
padding-inline-end: var(--input-icon-offset);
259+
}
260+
261+
.search-input__shortcut {
262+
@apply text-xs font-semibold py-px px-1 border border-secondary rounded-sm bg-secondary min-w-5 flex items-center justify-center;
263+
264+
line-height: var(--input-leading);
265+
box-shadow: var(--box-shadow-search-input-shortcut);
266+
}
267+
268+
/* ==========================================================================
269+
Range Input
270+
========================================================================== */
271+
272+
input[type="range"] {
273+
@apply accent-accent ;
274+
}
275+
276+
/* Thumb (WebKit + Firefox) */
277+
input[type="range"]::-webkit-slider-thumb {
278+
@apply accent-accent-content bg-accent-content;
279+
}
280+
}

0 commit comments

Comments
 (0)