@inglorious/ui 6.1.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,3 +43,35 @@ The public CSS exports are:
43
43
  - `@inglorious/ui/themes/material.css`
44
44
  - `@inglorious/ui/themes/bootstrap.css`
45
45
  - `@inglorious/ui/all.css`
46
+
47
+ ## Safe-area utilities
48
+
49
+ The base styles also expose CSS variables and utility classes for iPhone notch and home-indicator safe areas.
50
+
51
+ ```css
52
+ :root {
53
+ --safe-area-top: env(safe-area-inset-top, 0px);
54
+ --safe-area-bottom: env(safe-area-inset-bottom, 0px);
55
+ --safe-area-left: env(safe-area-inset-left, 0px);
56
+ --safe-area-right: env(safe-area-inset-right, 0px);
57
+ }
58
+ ```
59
+
60
+ The following utility classes are available:
61
+
62
+ ```css
63
+ .pt-safe {
64
+ padding-top: var(--safe-area-top);
65
+ }
66
+ .pb-safe {
67
+ padding-bottom: var(--safe-area-bottom);
68
+ }
69
+ .pl-safe {
70
+ padding-left: var(--safe-area-left);
71
+ }
72
+ .pr-safe {
73
+ padding-right: var(--safe-area-right);
74
+ }
75
+ ```
76
+
77
+ These are useful for mobile layouts that need to respect notches, rounded corners, and the home indicator. They are intentionally generic utilities so apps can opt into them without having to define custom safe-area values repeatedly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ui",
3
- "version": "6.1.0",
3
+ "version": "7.1.0",
4
4
  "description": "A design system for Inglorious Web with render-function components and customizable themes",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -113,6 +113,16 @@
113
113
  "import": "./src/controls/switch/index.js"
114
114
  },
115
115
  "./switch.css": "./src/controls/switch/style.css",
116
+ "./textarea": {
117
+ "types": "./types/controls/textarea.d.ts",
118
+ "import": "./src/controls/textarea/index.js"
119
+ },
120
+ "./textarea.css": "./src/controls/textarea/style.css",
121
+ "./file-upload": {
122
+ "types": "./types/controls/file-upload.d.ts",
123
+ "import": "./src/controls/file-upload/index.js"
124
+ },
125
+ "./file-upload.css": "./src/controls/file-upload/style.css",
116
126
  "./card": {
117
127
  "types": "./types/surfaces/card.d.ts",
118
128
  "import": "./src/surfaces/card/index.js"
@@ -290,10 +300,10 @@
290
300
  "**/*.css"
291
301
  ],
292
302
  "peerDependencies": {
303
+ "@inglorious/charts": "10.0.0",
293
304
  "@inglorious/store": "11.0.0",
294
305
  "@inglorious/utils": "3.8.0",
295
- "@inglorious/charts": "9.0.0",
296
- "@inglorious/web": "6.3.0"
306
+ "@inglorious/web": "6.4.0"
297
307
  },
298
308
  "devDependencies": {
299
309
  "@fontsource/material-symbols-outlined": "^5.2.36",
@@ -308,10 +318,10 @@
308
318
  "vite": "^7.3.1",
309
319
  "vitest": "^4.0.15",
310
320
  "@inglorious/eslint-config": "1.1.2",
311
- "@inglorious/vite-plugin-vue": "1.0.1",
312
321
  "@inglorious/vite-plugin-jsx": "2.0.0",
313
322
  "@inglorious/store": "11.0.0",
314
- "@inglorious/web": "6.3.0"
323
+ "@inglorious/vite-plugin-vue": "1.0.1",
324
+ "@inglorious/web": "6.4.0"
315
325
  },
316
326
  "engines": {
317
327
  "node": ">= 22"
@@ -10,3 +10,5 @@
10
10
  @import "./select/style.css";
11
11
  @import "./slider/style.css";
12
12
  @import "./switch/style.css";
13
+ @import "./textarea/style.css";
14
+ @import "./file-upload/style.css";
@@ -0,0 +1,93 @@
1
+ import { createRender } from "../../stories/notifyStory.js"
2
+ import { FileUpload } from "./index.js"
3
+
4
+ export default {
5
+ title: "Controls/FileUpload",
6
+ tags: ["autodocs"],
7
+ render: createRender(FileUpload),
8
+ argTypes: {
9
+ label: {
10
+ control: "text",
11
+ description: "Field label rendered above the upload area.",
12
+ },
13
+ name: {
14
+ control: "text",
15
+ description: "Native HTML file input name attribute.",
16
+ },
17
+ accept: {
18
+ control: "text",
19
+ description: "Accepted file types, such as .pdf,.png,image/*.",
20
+ },
21
+ isMultiple: {
22
+ control: "boolean",
23
+ description: "Allows selecting multiple files.",
24
+ },
25
+ hint: {
26
+ control: "text",
27
+ description: "Helper text shown below the field when there is no error.",
28
+ },
29
+ error: {
30
+ control: "text",
31
+ description: "Validation message shown below the field.",
32
+ },
33
+ isDisabled: {
34
+ control: "boolean",
35
+ description: "Disables user input and interaction.",
36
+ },
37
+ isRequired: {
38
+ control: "boolean",
39
+ description: "Marks the field as required.",
40
+ },
41
+ isFullWidth: {
42
+ control: "boolean",
43
+ description: "Expands the upload area width to 100% of its container.",
44
+ },
45
+ selectedFiles: {
46
+ control: "object",
47
+ description: "Current list of selected file names for display.",
48
+ },
49
+ onChange: { action: "onChange" },
50
+ onBlur: { action: "onBlur" },
51
+ onFocus: { action: "onFocus" },
52
+ },
53
+ parameters: {
54
+ docs: {
55
+ description: {
56
+ component:
57
+ "Upload area for files with native input semantics, validation, and a clean drag-and-drop style.",
58
+ },
59
+ },
60
+ },
61
+ }
62
+
63
+ export const Default = {}
64
+ Default.args = {
65
+ label: "Resume",
66
+ name: "resume",
67
+ accept: ".pdf,.doc,.docx",
68
+ isMultiple: false,
69
+ hint: "Upload your resume in PDF or DOC format.",
70
+ error: "",
71
+ isDisabled: false,
72
+ isRequired: false,
73
+ isFullWidth: false,
74
+ selectedFiles: [],
75
+ title: "Choose file",
76
+ description: "or drag and drop here",
77
+ }
78
+
79
+ export const Selected = {}
80
+ Selected.args = {
81
+ ...Default.args,
82
+ label: "Attachments",
83
+ isMultiple: true,
84
+ selectedFiles: ["brief.pdf", "notes.md"],
85
+ }
86
+
87
+ export const Error = {}
88
+ Error.args = {
89
+ ...Default.args,
90
+ label: "Invoice",
91
+ error: "Only PDF files under 5MB are allowed.",
92
+ selectedFiles: ["invoice.png"],
93
+ }
@@ -0,0 +1,3 @@
1
+ import * as renderers from "./template.js"
2
+
3
+ export const FileUpload = { ...renderers }
@@ -0,0 +1,99 @@
1
+ .iw-upload {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--iw-space-1);
5
+ font-family: var(--iw-font-family-sans);
6
+
7
+ &.iw-upload-full-width .iw-upload-field-box {
8
+ width: 100%;
9
+ }
10
+
11
+ .iw-upload-label {
12
+ font-size: var(--iw-font-size-sm);
13
+ font-weight: var(--iw-font-weight-normal);
14
+ color: var(--iw-color-text);
15
+ }
16
+
17
+ .iw-upload-required {
18
+ color: var(--iw-color-error);
19
+ margin-left: var(--iw-space-1);
20
+ }
21
+
22
+ .iw-upload-field-box {
23
+ position: relative;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ min-height: 7rem;
28
+ padding: var(--iw-space-4);
29
+ border: 1px dashed var(--iw-input-border);
30
+ border-radius: var(--iw-input-radius);
31
+ background: var(--iw-input-bg);
32
+ color: var(--iw-color-text);
33
+ transition: all var(--iw-transition-fast);
34
+ cursor: pointer;
35
+
36
+ &:hover:not(:disabled) {
37
+ background: var(--iw-input-bg-hover);
38
+ border-color: var(--iw-input-border-hover);
39
+ }
40
+
41
+ &:focus-within {
42
+ border-color: var(--iw-input-border-focus);
43
+ box-shadow: var(--iw-input-shadow-focus);
44
+ }
45
+
46
+ &.iw-upload-field-error {
47
+ border-color: var(--iw-color-error);
48
+
49
+ &:focus-within {
50
+ border-color: var(--iw-color-error);
51
+ box-shadow: 0 0 0 1px var(--iw-color-error);
52
+ }
53
+ }
54
+ }
55
+
56
+ .iw-upload-input {
57
+ position: absolute;
58
+ inset: 0;
59
+ opacity: 0;
60
+ cursor: pointer;
61
+ }
62
+
63
+ .iw-upload-content {
64
+ display: flex;
65
+ flex-direction: column;
66
+ align-items: center;
67
+ gap: var(--iw-space-1);
68
+ pointer-events: none;
69
+ text-align: center;
70
+ }
71
+
72
+ .iw-upload-title {
73
+ font-weight: var(--iw-font-weight-medium);
74
+ }
75
+
76
+ .iw-upload-description {
77
+ color: var(--iw-color-text-muted);
78
+ font-size: var(--iw-font-size-sm);
79
+ }
80
+
81
+ .iw-upload-field-summary {
82
+ font-size: var(--iw-font-size-sm);
83
+ color: var(--iw-color-text-muted);
84
+ }
85
+
86
+ .iw-upload-hint,
87
+ .iw-upload-error-message {
88
+ font-size: var(--iw-font-size-sm);
89
+ line-height: var(--iw-line-height-normal);
90
+ }
91
+
92
+ .iw-upload-hint {
93
+ color: var(--iw-color-text-muted);
94
+ }
95
+
96
+ .iw-upload-error-message {
97
+ color: var(--iw-color-error);
98
+ }
99
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @typedef {import('../../../types/controls/file-upload').FileUploadProps} FileUploadProps
3
+ * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
4
+ */
5
+
6
+ import { html } from "@inglorious/web"
7
+ import { classMap } from "@inglorious/web/directives/class-map"
8
+ import { ref } from "@inglorious/web/directives/ref"
9
+
10
+ import { applyElementProps } from "../../shared/applyElementProps.js"
11
+
12
+ /**
13
+ * Renders a labeled file upload area with optional summary, hint, and error text.
14
+ * Supports drag-and-drop handling through file input semantics.
15
+ * @param {FileUploadProps} props
16
+ * @returns {TemplateResult}
17
+ */
18
+ export function render(props) {
19
+ const {
20
+ id,
21
+ name = "",
22
+ label,
23
+ hint,
24
+ error,
25
+ accept,
26
+ isMultiple = false,
27
+ isDisabled = false,
28
+ isRequired = false,
29
+ isFullWidth = false,
30
+ selectedFiles = [],
31
+ title = "Choose files",
32
+ description = "or drag and drop here",
33
+ onChange,
34
+ onBlur,
35
+ onFocus,
36
+ ...rest
37
+ } = props
38
+
39
+ const uploadId = id || name || "file-upload"
40
+
41
+ const wrapperClasses = {
42
+ "iw-upload": true,
43
+ "iw-upload-full-width": isFullWidth,
44
+ "iw-upload-disabled": isDisabled,
45
+ "iw-upload-has-error": !!error,
46
+ }
47
+
48
+ const fieldClasses = {
49
+ "iw-upload-field-box": true,
50
+ "iw-upload-field-error": !!error,
51
+ }
52
+
53
+ const summaryText =
54
+ selectedFiles.length > 0 ? selectedFiles.join(", ") : "No file selected"
55
+
56
+ return html`
57
+ <div class=${classMap(wrapperClasses)}>
58
+ ${label
59
+ ? html`
60
+ <label for=${uploadId} class="iw-upload-label">
61
+ ${label}
62
+ ${isRequired
63
+ ? html`<span class="iw-upload-required">*</span>`
64
+ : null}
65
+ </label>
66
+ `
67
+ : null}
68
+
69
+ <div class=${classMap(fieldClasses)}>
70
+ <input
71
+ id=${uploadId}
72
+ name=${name}
73
+ type="file"
74
+ accept=${accept ?? ""}
75
+ ?multiple=${isMultiple}
76
+ ?disabled=${isDisabled}
77
+ ?required=${isRequired}
78
+ class="iw-upload-input"
79
+ @change=${(event) => onChange?.(event.target.files)}
80
+ @blur=${onBlur}
81
+ @focus=${onFocus}
82
+ ${ref((element) => applyElementProps(element, rest))}
83
+ />
84
+
85
+ <div class="iw-upload-content">
86
+ <span class="iw-upload-title">${title}</span>
87
+ <span class="iw-upload-description">${description}</span>
88
+ </div>
89
+ </div>
90
+
91
+ <div class="iw-upload-field-summary">${summaryText}</div>
92
+
93
+ ${error
94
+ ? html`<span class="iw-upload-error-message">${error}</span>`
95
+ : hint
96
+ ? html`<span class="iw-upload-hint">${hint}</span>`
97
+ : null}
98
+ </div>
99
+ `
100
+ }
@@ -1,12 +1,45 @@
1
+ import { html } from "@inglorious/web"
2
+
1
3
  import { createRender } from "../../stories/notifyStory.js"
2
4
  import { Input } from "."
3
5
 
6
+ const nativeInputTypes = [
7
+ "text",
8
+ "password",
9
+ "email",
10
+ "number",
11
+ "tel",
12
+ "url",
13
+ "search",
14
+ "date",
15
+ "time",
16
+ "datetime-local",
17
+ "month",
18
+ "week",
19
+ "color",
20
+ ]
21
+
22
+ const nativeInputDefaults = {
23
+ text: { label: "Text", value: "Hello world" },
24
+ password: { label: "Password", value: "secret123" },
25
+ email: { label: "Email", value: "you@example.com" },
26
+ number: { label: "Number", value: "42" },
27
+ tel: { label: "Phone", value: "+1 555 123 4567" },
28
+ url: { label: "Website", value: "https://example.com" },
29
+ search: { label: "Search", value: "inglorious" },
30
+ date: { label: "Date", value: "2026-08-10" },
31
+ time: { label: "Time", value: "09:30" },
32
+ "datetime-local": { label: "Date and time", value: "2026-08-10T09:30" },
33
+ month: { label: "Month", value: "2026-08" },
34
+ week: { label: "Week", value: "2026-W32" },
35
+ color: { label: "Color", value: "#7c3aed" },
36
+ }
37
+
4
38
  export default {
5
39
  title: "Controls/Input",
6
40
  tags: ["autodocs"],
7
41
  render: createRender(Input),
8
42
  argTypes: {
9
- // ...notifyActionArgType,
10
43
  label: {
11
44
  control: "text",
12
45
  description: "Field label rendered above the input.",
@@ -19,9 +52,9 @@ export default {
19
52
  control: "text",
20
53
  description: "Native HTML input name attribute.",
21
54
  },
22
- type: {
55
+ inputType: {
23
56
  control: "select",
24
- options: ["text", "email", "password", "number", "tel", "url", "search"],
57
+ options: nativeInputTypes,
25
58
  description: "Native HTML input type.",
26
59
  },
27
60
  inputmode: {
@@ -101,6 +134,28 @@ Default.args = {
101
134
  isFullWidth: false,
102
135
  }
103
136
 
137
+ export const ManagedNativeTypes = {
138
+ render: (args) => html`
139
+ <div style="display: grid; gap: 1rem; max-width: 40rem;">
140
+ ${nativeInputTypes.map((inputType) =>
141
+ Input.render({
142
+ ...args,
143
+ ...nativeInputDefaults[inputType],
144
+ name: `${inputType}-field`,
145
+ label: nativeInputDefaults[inputType].label,
146
+ inputType,
147
+ value: nativeInputDefaults[inputType].value,
148
+ }),
149
+ )}
150
+ </div>
151
+ `,
152
+ }
153
+
154
+ ManagedNativeTypes.args = {
155
+ ...Default.args,
156
+ label: "Managed native types",
157
+ }
158
+
104
159
  export const WithHint = {}
105
160
  WithHint.args = {
106
161
  ...Default.args,
@@ -179,3 +234,55 @@ Number.args = {
179
234
  inputmode: "decimal",
180
235
  "data-testid": "amount-input",
181
236
  }
237
+
238
+ export const Date = {}
239
+ Date.args = {
240
+ ...Default.args,
241
+ label: "Date",
242
+ inputType: "date",
243
+ value: "2026-08-10",
244
+ min: "2024-01-01",
245
+ max: "2027-12-31",
246
+ }
247
+
248
+ export const Time = {}
249
+ Time.args = {
250
+ ...Default.args,
251
+ label: "Time",
252
+ inputType: "time",
253
+ value: "09:30",
254
+ min: "08:00",
255
+ max: "18:00",
256
+ }
257
+
258
+ export const DateTimeLocal = {}
259
+ DateTimeLocal.args = {
260
+ ...Default.args,
261
+ label: "Date and time",
262
+ inputType: "datetime-local",
263
+ value: "2026-08-10T09:30",
264
+ }
265
+
266
+ export const Month = {}
267
+ Month.args = {
268
+ ...Default.args,
269
+ label: "Month",
270
+ inputType: "month",
271
+ value: "2026-08",
272
+ }
273
+
274
+ export const Week = {}
275
+ Week.args = {
276
+ ...Default.args,
277
+ label: "Week",
278
+ inputType: "week",
279
+ value: "2026-W32",
280
+ }
281
+
282
+ export const Color = {}
283
+ Color.args = {
284
+ ...Default.args,
285
+ label: "Color",
286
+ inputType: "color",
287
+ value: "#7c3aed",
288
+ }
@@ -0,0 +1,3 @@
1
+ import * as renderers from "./template.js"
2
+
3
+ export const Textarea = { ...renderers }
@@ -0,0 +1,98 @@
1
+ .iw-textarea-field {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--iw-space-1);
5
+ font-family: var(--iw-font-family-sans);
6
+
7
+ &.iw-textarea-full-width .iw-textarea {
8
+ width: 100%;
9
+ }
10
+
11
+ .iw-textarea-label {
12
+ font-size: var(--iw-font-size-sm);
13
+ font-weight: var(--iw-font-weight-normal);
14
+ color: var(--iw-color-text);
15
+ }
16
+
17
+ .iw-textarea-required {
18
+ color: var(--iw-color-error);
19
+ margin-left: var(--iw-space-1);
20
+ }
21
+
22
+ .iw-textarea {
23
+ min-height: 3rem;
24
+ width: min(100%, 100%);
25
+ resize: none;
26
+ padding: var(--iw-space-2) var(--iw-space-3);
27
+ font-family: inherit;
28
+ font-size: var(--iw-font-size-base);
29
+ line-height: 1.5;
30
+ color: var(--iw-input-text);
31
+ background-color: var(--iw-input-bg);
32
+ border: 1px solid var(--iw-input-border);
33
+ border-radius: var(--iw-input-radius);
34
+ outline: none;
35
+ transition: all var(--iw-transition-fast);
36
+
37
+ &::placeholder {
38
+ color: var(--iw-input-text-placeholder);
39
+ }
40
+
41
+ &:hover:not(:disabled) {
42
+ background-color: var(--iw-input-bg-hover);
43
+ border-color: var(--iw-input-border-hover);
44
+ }
45
+
46
+ &:focus:not(:disabled) {
47
+ background-color: var(--iw-input-bg-focus);
48
+ border-color: var(--iw-input-border-focus);
49
+ box-shadow: var(--iw-input-shadow-focus);
50
+ }
51
+
52
+ &:disabled {
53
+ opacity: 0.5;
54
+ cursor: not-allowed;
55
+ }
56
+
57
+ &.iw-textarea-auto-size {
58
+ field-sizing: content;
59
+ }
60
+
61
+ &.iw-textarea-resizable {
62
+ resize: vertical;
63
+ }
64
+
65
+ &.iw-textarea-error {
66
+ border-color: var(--iw-color-error);
67
+
68
+ &:focus {
69
+ border-color: var(--iw-color-error);
70
+ box-shadow: 0 0 0 1px var(--iw-color-error);
71
+ }
72
+ }
73
+
74
+ &.iw-textarea-sm {
75
+ padding: var(--iw-space-1) var(--iw-space-2);
76
+ font-size: var(--iw-font-size-sm);
77
+ }
78
+
79
+ &.iw-textarea-lg {
80
+ padding: var(--iw-space-3) var(--iw-space-4);
81
+ font-size: var(--iw-font-size-lg);
82
+ }
83
+ }
84
+
85
+ .iw-textarea-hint,
86
+ .iw-textarea-error-message {
87
+ font-size: var(--iw-font-size-sm);
88
+ line-height: var(--iw-line-height-normal);
89
+ }
90
+
91
+ .iw-textarea-hint {
92
+ color: var(--iw-color-text-muted);
93
+ }
94
+
95
+ .iw-textarea-error-message {
96
+ color: var(--iw-color-error);
97
+ }
98
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @typedef {import('../../../types/controls/textarea').TextareaProps} TextareaProps
3
+ * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
4
+ */
5
+
6
+ import { html } from "@inglorious/web"
7
+ import { classMap } from "@inglorious/web/directives/class-map"
8
+ import { ref } from "@inglorious/web/directives/ref"
9
+
10
+ import { applyElementProps } from "../../shared/applyElementProps.js"
11
+
12
+ /**
13
+ * Renders a labeled textarea field with optional hint and error messaging.
14
+ * Supports disabled/readonly/required states and emits `onChange` on input.
15
+ * @param {TextareaProps} props
16
+ * @returns {TemplateResult}
17
+ */
18
+ export function render(props) {
19
+ const {
20
+ id,
21
+ name = "",
22
+ value = "",
23
+ placeholder = "",
24
+ label,
25
+ hint,
26
+ error,
27
+ size = "md",
28
+ rows = 4,
29
+ isDisabled = false,
30
+ isReadOnly = false,
31
+ isRequired = false,
32
+ isFullWidth = false,
33
+ isAutoSize = false,
34
+ isResizable = false,
35
+ onChange,
36
+ onBlur,
37
+ onFocus,
38
+ ...rest
39
+ } = props
40
+
41
+ const textareaId = id || name
42
+
43
+ const wrapperClasses = {
44
+ "iw-textarea-field": true,
45
+ "iw-textarea-full-width": isFullWidth,
46
+ "iw-textarea-disabled": isDisabled,
47
+ "iw-textarea-has-error": !!error,
48
+ }
49
+
50
+ const textareaClasses = {
51
+ "iw-textarea": true,
52
+ [`iw-textarea-${size}`]: size !== "md",
53
+ "iw-textarea-error": !!error,
54
+ "iw-textarea-auto-size": isAutoSize,
55
+ "iw-textarea-resizable": isResizable,
56
+ }
57
+
58
+ return html`
59
+ <div class=${classMap(wrapperClasses)}>
60
+ ${label
61
+ ? html`
62
+ <label for=${textareaId} class="iw-textarea-label">
63
+ ${label}
64
+ ${isRequired
65
+ ? html`<span class="iw-textarea-required">*</span>`
66
+ : null}
67
+ </label>
68
+ `
69
+ : null}
70
+
71
+ <textarea
72
+ id=${textareaId}
73
+ name=${name}
74
+ rows=${rows}
75
+ .value=${value}
76
+ placeholder=${placeholder}
77
+ ?disabled=${isDisabled}
78
+ ?readonly=${isReadOnly}
79
+ ?required=${isRequired}
80
+ class=${classMap(textareaClasses)}
81
+ @input=${(event) => onChange?.(event.target.value)}
82
+ @blur=${onBlur}
83
+ @focus=${onFocus}
84
+ ${ref((element) => applyElementProps(element, rest))}
85
+ ></textarea>
86
+
87
+ ${error
88
+ ? html`<span class="iw-textarea-error-message">${error}</span>`
89
+ : hint
90
+ ? html`<span class="iw-textarea-hint">${hint}</span>`
91
+ : null}
92
+ </div>
93
+ `
94
+ }
@@ -0,0 +1,158 @@
1
+ import { createRender } from "../../stories/notifyStory.js"
2
+ import { Textarea } from "."
3
+
4
+ export default {
5
+ title: "Controls/Textarea",
6
+ tags: ["autodocs"],
7
+ render: createRender(Textarea),
8
+ argTypes: {
9
+ label: {
10
+ control: "text",
11
+ description: "Field label rendered above the textarea.",
12
+ },
13
+ placeholder: {
14
+ control: "text",
15
+ description: "Placeholder text shown when value is empty.",
16
+ },
17
+ name: {
18
+ control: "text",
19
+ description: "Native HTML textarea name attribute.",
20
+ },
21
+ value: {
22
+ control: "text",
23
+ description: "Current textarea value.",
24
+ },
25
+ hint: {
26
+ control: "text",
27
+ description: "Helper text shown below the field when there is no error.",
28
+ },
29
+ error: {
30
+ control: "text",
31
+ description: "Validation message shown below the field.",
32
+ },
33
+ size: {
34
+ control: "select",
35
+ options: ["sm", "md", "lg"],
36
+ description: "Size scale for internal spacing and typography.",
37
+ },
38
+ rows: {
39
+ control: "number",
40
+ description: "Initial number of visible text rows.",
41
+ },
42
+ isDisabled: {
43
+ control: "boolean",
44
+ description: "Disables user input and interaction.",
45
+ },
46
+ isReadOnly: {
47
+ control: "boolean",
48
+ description: "Prevents editing while keeping the control focusable.",
49
+ },
50
+ isRequired: {
51
+ control: "boolean",
52
+ description: "Marks the field as required.",
53
+ },
54
+ isFullWidth: {
55
+ control: "boolean",
56
+ description: "Expands the textarea width to 100% of its container.",
57
+ },
58
+ isAutoSize: {
59
+ control: "boolean",
60
+ description:
61
+ "Applies field-sizing: content so the textarea grows with its content.",
62
+ },
63
+ isResizable: {
64
+ control: "boolean",
65
+ description: "Enables the browser resize handle for vertical resizing.",
66
+ },
67
+ onChange: { action: "onChange" },
68
+ onBlur: { action: "onBlur" },
69
+ onFocus: { action: "onFocus" },
70
+ },
71
+ parameters: {
72
+ docs: {
73
+ description: {
74
+ component:
75
+ "Multi-line text input with labels, hints, errors, and automatic content sizing support.",
76
+ },
77
+ },
78
+ },
79
+ }
80
+
81
+ export const Default = {}
82
+ Default.args = {
83
+ label: "Bio",
84
+ placeholder: "Tell us a little about yourself",
85
+ name: "bio",
86
+ value: "",
87
+ hint: "",
88
+ error: "",
89
+ size: "md",
90
+ rows: 4,
91
+ isDisabled: false,
92
+ isReadOnly: false,
93
+ isRequired: false,
94
+ isFullWidth: false,
95
+ isAutoSize: false,
96
+ isResizable: false,
97
+ }
98
+
99
+ export const WithHint = {}
100
+ WithHint.args = {
101
+ ...Default.args,
102
+ label: "Project notes",
103
+ hint: "Include any implementation details or blockers.",
104
+ }
105
+
106
+ export const WithError = {}
107
+ WithError.args = {
108
+ ...Default.args,
109
+ label: "Description",
110
+ value: "This is too long",
111
+ error: "Please keep the description under 280 characters.",
112
+ }
113
+
114
+ export const Required = {}
115
+ Required.args = {
116
+ ...Default.args,
117
+ label: "Message",
118
+ placeholder: "Write a quick message",
119
+ isRequired: true,
120
+ }
121
+
122
+ export const Disabled = {}
123
+ Disabled.args = {
124
+ ...Default.args,
125
+ label: "Disabled textarea",
126
+ isDisabled: true,
127
+ value: "Cannot edit this value",
128
+ }
129
+
130
+ export const Readonly = {}
131
+ Readonly.args = {
132
+ ...Default.args,
133
+ label: "Read-only textarea",
134
+ isReadOnly: true,
135
+ value: "This field is read-only and not editable.",
136
+ }
137
+
138
+ export const Small = {}
139
+ Small.args = {
140
+ ...Default.args,
141
+ label: "Small textarea",
142
+ size: "sm",
143
+ }
144
+
145
+ export const Large = {}
146
+ Large.args = {
147
+ ...Default.args,
148
+ label: "Large textarea",
149
+ size: "lg",
150
+ }
151
+
152
+ export const FullWidth = {}
153
+ FullWidth.args = {
154
+ ...Default.args,
155
+ label: "Full width textarea",
156
+ placeholder: "This textarea takes the full width of its container",
157
+ isFullWidth: true,
158
+ }
@@ -147,6 +147,12 @@ function getDefaultItems() {
147
147
  href: "/controls/icon-button",
148
148
  },
149
149
  { id: "input", primary: "Input", href: "/controls/input" },
150
+ {
151
+ id: "file-upload",
152
+ primary: "File Upload",
153
+ href: "/controls/file-upload",
154
+ },
155
+ { id: "textarea", primary: "Textarea", href: "/controls/textarea" },
150
156
  {
151
157
  id: "radio-group",
152
158
  primary: "Radio Group",
@@ -6,6 +6,7 @@ import { ButtonGroup } from "../../controls/button-group/index.js"
6
6
  import { Checkbox } from "../../controls/checkbox/index.js"
7
7
  import { Combobox } from "../../controls/combobox/index.js"
8
8
  import { Fab } from "../../controls/fab/index.js"
9
+ import { FileUpload } from "../../controls/file-upload/index.js"
9
10
  import { IconButton } from "../../controls/icon-button/index.js"
10
11
  import { Input } from "../../controls/input/index.js"
11
12
  import { RadioGroup } from "../../controls/radio-group/index.js"
@@ -13,6 +14,7 @@ import { Rating } from "../../controls/rating/index.js"
13
14
  import { Select } from "../../controls/select/index.js"
14
15
  import { Slider } from "../../controls/slider/index.js"
15
16
  import { Switch } from "../../controls/switch/index.js"
17
+ import { Textarea } from "../../controls/textarea/index.js"
16
18
  import { Avatar } from "../../data-display/avatar/index.js"
17
19
  import { Badge } from "../../data-display/badge/index.js"
18
20
  import { BeforeAfter } from "../../data-display/before-after/index.js"
@@ -514,6 +516,73 @@ Input.render({
514
516
  ]),
515
517
  },
516
518
  },
519
+ "/controls/file-upload": {
520
+ summary:
521
+ "File selection area for uploads, attachments, and drag-and-drop flows.",
522
+ description:
523
+ "FileUpload wraps the native file input in a clearer interaction area while preserving validation, hints, and selected file summaries.",
524
+ useCases: ["Attachments", "Bulk upload", "Resume and invoice submissions"],
525
+ example: {
526
+ code: `import { FileUpload } from "@inglorious/ui/file-upload"
527
+
528
+ FileUpload.render({
529
+ label: "Resume",
530
+ accept: ".pdf,.doc,.docx",
531
+ selectedFiles: ["resume.pdf"],
532
+ hint: "PDF or DOCX up to 5MB"
533
+ })`,
534
+ preview: () =>
535
+ previewColumn([
536
+ FileUpload.render({
537
+ label: "Resume",
538
+ accept: ".pdf,.doc,.docx",
539
+ selectedFiles: ["resume.pdf"],
540
+ hint: "PDF or DOCX up to 5MB",
541
+ }),
542
+ FileUpload.render({
543
+ label: "Attachments",
544
+ isMultiple: true,
545
+ selectedFiles: ["brief.pdf", "notes.md"],
546
+ }),
547
+ ]),
548
+ },
549
+ },
550
+ "/controls/textarea": {
551
+ summary: "Multi-line text entry with validation and auto-sizing support.",
552
+ description:
553
+ "Textarea is built for longer responses and keeps its height aligned with the content while supporting helper text and validation states.",
554
+ useCases: [
555
+ "Message bodies",
556
+ "Long-form notes",
557
+ "Descriptions and feedback",
558
+ ],
559
+ example: {
560
+ code: `import { Textarea } from "@inglorious/ui/textarea"
561
+
562
+ Textarea.render({
563
+ label: "Project details",
564
+ placeholder: "Describe the scope and constraints",
565
+ rows: 4,
566
+ hint: "A few sentences are enough"
567
+ })`,
568
+ preview: () =>
569
+ previewColumn([
570
+ Textarea.render({
571
+ label: "Project details",
572
+ placeholder: "Describe the scope and constraints",
573
+ rows: 4,
574
+ hint: "A few sentences are enough",
575
+ value: "",
576
+ }),
577
+ Textarea.render({
578
+ label: "Notes",
579
+ value:
580
+ "The migration is nearly complete and we're validating the edge cases.",
581
+ error: "Please keep it under 280 characters.",
582
+ }),
583
+ ]),
584
+ },
585
+ },
517
586
  "/controls/radio-group": {
518
587
  summary: "Single-choice selection among visible options.",
519
588
  description:
package/src/index.js CHANGED
@@ -9,6 +9,8 @@ export { Rating } from "./controls/rating/index.js"
9
9
  export { Select } from "./controls/select/index.js"
10
10
  export { Slider } from "./controls/slider/index.js"
11
11
  export { Switch } from "./controls/switch/index.js"
12
+ export { Textarea } from "./controls/textarea/index.js"
13
+ export { FileUpload } from "./controls/file-upload/index.js"
12
14
  export { Avatar } from "./data-display/avatar/index.js"
13
15
  export { Badge } from "./data-display/badge/index.js"
14
16
  export { BeforeAfter } from "./data-display/before-after/index.js"
@@ -1,2 +1,3 @@
1
1
  @import "./reset.css";
2
+ @import "./safe-area.css";
2
3
  @import "./tokens.css";
@@ -0,0 +1,22 @@
1
+ :root {
2
+ --safe-area-top: env(safe-area-inset-top, 0px);
3
+ --safe-area-bottom: env(safe-area-inset-bottom, 0px);
4
+ --safe-area-left: env(safe-area-inset-left, 0px);
5
+ --safe-area-right: env(safe-area-inset-right, 0px);
6
+ }
7
+
8
+ .pt-safe {
9
+ padding-top: var(--safe-area-top);
10
+ }
11
+
12
+ .pb-safe {
13
+ padding-bottom: var(--safe-area-bottom);
14
+ }
15
+
16
+ .pl-safe {
17
+ padding-left: var(--safe-area-left);
18
+ }
19
+
20
+ .pr-safe {
21
+ padding-right: var(--safe-area-right);
22
+ }
@@ -0,0 +1,27 @@
1
+ import type { TemplateResult } from "@inglorious/web"
2
+
3
+ export interface FileUploadProps {
4
+ id?: string
5
+ name?: string
6
+ label?: string
7
+ hint?: string
8
+ error?: string
9
+ accept?: string
10
+ isMultiple?: boolean
11
+ isDisabled?: boolean
12
+ isRequired?: boolean
13
+ isFullWidth?: boolean
14
+ selectedFiles?: string[]
15
+ title?: string
16
+ description?: string
17
+ onChange?: (files: FileList | null) => void
18
+ onBlur?: () => void
19
+ onFocus?: () => void
20
+ [key: string]: unknown
21
+ }
22
+
23
+ export interface FileUploadType {
24
+ render(props: FileUploadProps): TemplateResult
25
+ }
26
+
27
+ export declare const FileUpload: FileUploadType
@@ -11,6 +11,12 @@ export interface InputProps {
11
11
  | "tel"
12
12
  | "url"
13
13
  | "search"
14
+ | "date"
15
+ | "time"
16
+ | "datetime-local"
17
+ | "month"
18
+ | "week"
19
+ | "color"
14
20
  value?: string
15
21
  placeholder?: string
16
22
  label?: string
@@ -0,0 +1,29 @@
1
+ import type { TemplateResult } from "@inglorious/web"
2
+
3
+ export interface TextareaProps {
4
+ id?: string
5
+ name?: string
6
+ value?: string
7
+ placeholder?: string
8
+ label?: string
9
+ hint?: string
10
+ error?: string
11
+ size?: "sm" | "md" | "lg"
12
+ rows?: number
13
+ isDisabled?: boolean
14
+ isReadOnly?: boolean
15
+ isRequired?: boolean
16
+ isFullWidth?: boolean
17
+ isAutoSize?: boolean
18
+ isResizable?: boolean
19
+ onChange?: (value: string) => void
20
+ onBlur?: () => void
21
+ onFocus?: () => void
22
+ [key: string]: unknown
23
+ }
24
+
25
+ export interface TextareaType {
26
+ render(props: TextareaProps): TemplateResult
27
+ }
28
+
29
+ export declare const Textarea: TextareaType
@@ -0,0 +1,27 @@
1
+ import type { TemplateResult } from "@inglorious/web"
2
+
3
+ export interface FileUploadProps {
4
+ id?: string
5
+ name?: string
6
+ label?: string
7
+ hint?: string
8
+ error?: string
9
+ accept?: string
10
+ multiple?: boolean
11
+ isDisabled?: boolean
12
+ isRequired?: boolean
13
+ isFullWidth?: boolean
14
+ selectedFiles?: string[]
15
+ title?: string
16
+ description?: string
17
+ onChange?: (files: FileList | null) => void
18
+ onBlur?: () => void
19
+ onFocus?: () => void
20
+ [key: string]: unknown
21
+ }
22
+
23
+ export interface FileUploadType {
24
+ render(props: FileUploadProps): TemplateResult
25
+ }
26
+
27
+ export declare const FileUpload: FileUploadType
package/types/index.d.ts CHANGED
@@ -18,6 +18,8 @@ export type { RatingProps, RatingType } from "./controls/rating"
18
18
  export type { SelectProps, SelectType, SelectOption } from "./controls/select"
19
19
  export type { SliderProps, SliderType } from "./controls/slider"
20
20
  export type { SwitchProps, SwitchType } from "./controls/switch"
21
+ export type { TextareaProps, TextareaType } from "./controls/textarea"
22
+ export type { FileUploadProps, FileUploadType } from "./controls/file-upload"
21
23
  export type { ContainerProps, ContainerType } from "./layout/container"
22
24
  export type { FlexProps, FlexType } from "./layout/flex"
23
25
  export type { GridProps, GridType } from "./layout/grid"