@inglorious/ui 6.0.0 → 7.0.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.
Files changed (39) hide show
  1. package/package.json +32 -12
  2. package/src/controls/controls.css +3 -1
  3. package/src/controls/file-upload/file-upload.stories.js +93 -0
  4. package/src/controls/file-upload/index.js +3 -0
  5. package/src/controls/file-upload/style.css +99 -0
  6. package/src/controls/file-upload/template.js +100 -0
  7. package/src/controls/input/input.stories.js +110 -3
  8. package/src/controls/textarea/index.js +3 -0
  9. package/src/controls/textarea/style.css +98 -0
  10. package/src/controls/textarea/template.js +94 -0
  11. package/src/controls/textarea/textarea.stories.js +158 -0
  12. package/src/data-display/before-after/before-after.stories.js +74 -0
  13. package/src/data-display/before-after/handlers.js +44 -0
  14. package/src/data-display/before-after/helpers.js +4 -0
  15. package/src/data-display/before-after/index.js +17 -0
  16. package/src/data-display/before-after/style.css +128 -0
  17. package/src/data-display/before-after/template.js +225 -0
  18. package/src/data-display/carousel/carousel.stories.js +166 -0
  19. package/src/data-display/carousel/handlers.js +78 -0
  20. package/src/data-display/carousel/helpers.js +54 -0
  21. package/src/data-display/carousel/index.js +17 -0
  22. package/src/data-display/carousel/style.css +233 -0
  23. package/src/data-display/carousel/template.js +655 -0
  24. package/src/data-display/data-display.css +2 -0
  25. package/src/examples/dashboard/app-drawer.js +12 -0
  26. package/src/examples/dashboard/before-after-data.js +29 -0
  27. package/src/examples/dashboard/primitive-content.js +220 -33
  28. package/src/index.js +4 -0
  29. package/src/layout/flex/flex.stories.js +49 -0
  30. package/src/layout/flex/style.css +59 -0
  31. package/src/layout/flex/template.js +2 -0
  32. package/types/controls/file-upload.d.ts +27 -0
  33. package/types/controls/input.d.ts +6 -0
  34. package/types/controls/textarea.d.ts +29 -0
  35. package/types/controls/upload-field.d.ts +27 -0
  36. package/types/data-display/before-after.d.ts +39 -0
  37. package/types/data-display/carousel.d.ts +72 -0
  38. package/types/index.d.ts +2 -0
  39. package/types/layout/flex.d.ts +1 -0
@@ -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
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ createEntityRender,
3
+ notifyActionArgType,
4
+ } from "../../stories/notifyStory.js"
5
+ import { BeforeAfter } from "./index.js"
6
+
7
+ const before = {
8
+ src: "https://picsum.photos/id/1015/800/500?grayscale",
9
+ alt: "Before",
10
+ }
11
+ const after = {
12
+ src: "https://picsum.photos/id/1015/800/500",
13
+ alt: "After",
14
+ }
15
+
16
+ export default {
17
+ title: "Data Display/BeforeAfter",
18
+ tags: ["autodocs"],
19
+ render: createEntityRender({ BeforeAfter }),
20
+ argTypes: {
21
+ ...notifyActionArgType,
22
+ label: { control: "text", description: "Accessible label for the slider." },
23
+ position: {
24
+ control: { type: "range", min: 0, max: 100, step: 1 },
25
+ description: "Divider position as a percentage (0-100).",
26
+ },
27
+ step: {
28
+ control: "number",
29
+ description: "Keyboard increment (percent) for arrow keys.",
30
+ },
31
+ isDisabled: { control: "boolean", description: "Disable interaction." },
32
+ isFullWidth: {
33
+ control: "boolean",
34
+ description: "Expand to container width.",
35
+ },
36
+ before: { control: "object", description: "Before image { src, alt }." },
37
+ after: { control: "object", description: "After image { src, alt }." },
38
+ },
39
+ parameters: {
40
+ docs: {
41
+ description: {
42
+ component:
43
+ "Before/after image comparison slider. The reveal is pure CSS (clip-path); the divider position is the only piece of entity state. Drag anywhere, or focus and use arrow keys / Home / End.",
44
+ },
45
+ },
46
+ },
47
+ }
48
+
49
+ export const Default = {}
50
+ Default.args = {
51
+ id: "before-after",
52
+ type: "BeforeAfter",
53
+ label: "Before and after comparison",
54
+ position: 50,
55
+ step: 1,
56
+ isDisabled: false,
57
+ isFullWidth: true,
58
+ before,
59
+ after,
60
+ }
61
+
62
+ export const StartLeft = {}
63
+ StartLeft.args = {
64
+ ...Default.args,
65
+ id: "before-after-left",
66
+ position: 15,
67
+ }
68
+
69
+ export const Disabled = {}
70
+ Disabled.args = {
71
+ ...Default.args,
72
+ id: "before-after-disabled",
73
+ isDisabled: true,
74
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @typedef {import('../../../types/data-display/before-after.js').BeforeAfterProps} BeforeAfterEntity
3
+ */
4
+
5
+ import { clamp } from "@inglorious/utils/math/numbers.js"
6
+
7
+ import {
8
+ DEFAULT_POSITION,
9
+ DEFAULT_STEP,
10
+ MAX_POSITION,
11
+ MIN_POSITION,
12
+ } from "./helpers.js"
13
+
14
+ /**
15
+ * Initialize missing before/after state defaults.
16
+ * The only persistent piece of state is `position`: the divider location as a
17
+ * percentage (0-100). Everything else is configuration.
18
+ * @param {BeforeAfterEntity} entity
19
+ */
20
+ export function create(entity) {
21
+ entity.position ??= DEFAULT_POSITION
22
+ entity.step ??= DEFAULT_STEP
23
+
24
+ entity.before ??= null
25
+ entity.after ??= null
26
+
27
+ entity.label ??= ""
28
+ entity.isDisabled ??= false
29
+ entity.isFullWidth ??= false
30
+ }
31
+
32
+ /**
33
+ * Move the divider to a new position, clamped to the 0-100 range.
34
+ * @param {BeforeAfterEntity} entity
35
+ * @param {number} position
36
+ */
37
+ export function positionChange(entity, position) {
38
+ if (entity.isDisabled) return
39
+
40
+ const value = Number(position)
41
+ if (Number.isNaN(value)) return
42
+
43
+ entity.position = clamp(value, MIN_POSITION, MAX_POSITION)
44
+ }
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_POSITION = 50
2
+ export const DEFAULT_STEP = 1
3
+ export const MIN_POSITION = 0
4
+ export const MAX_POSITION = 100
@@ -0,0 +1,17 @@
1
+ import * as handlers from "./handlers.js"
2
+ import { BeforeAfter as renderers } from "./template.js"
3
+
4
+ export const BeforeAfter = {
5
+ ...handlers,
6
+ ...renderers,
7
+ render(entity, api) {
8
+ const id = entity.id
9
+ const props = {
10
+ ...entity,
11
+ onPositionChange: (position) =>
12
+ api.notify(`#${id}:positionChange`, position),
13
+ }
14
+
15
+ return this.renderBeforeAfter(props)
16
+ },
17
+ }
@@ -0,0 +1,128 @@
1
+ .iw-before-after {
2
+ --iw-before-after-position: 50%;
3
+
4
+ /* Neutral in every theme on purpose: the divider should separate the two
5
+ pictures without competing with them for attention. */
6
+ --iw-before-after-divider-width: 2px;
7
+ --iw-before-after-divider-color: rgb(255 255 255 / 90%);
8
+ --iw-before-after-handle-size: 40px;
9
+ --iw-before-after-handle-bg: rgb(255 255 255 / 90%);
10
+ --iw-before-after-handle-color: rgb(17 24 39 / 65%);
11
+ --iw-before-after-handle-arrow: 5px;
12
+
13
+ position: relative;
14
+ display: inline-block;
15
+ max-width: 100%;
16
+ overflow: hidden;
17
+ line-height: 0;
18
+ cursor: ew-resize;
19
+ touch-action: none;
20
+ user-select: none;
21
+ -webkit-user-select: none;
22
+ outline: none;
23
+
24
+ &:focus-visible {
25
+ outline: 2px solid var(--iw-color-primary);
26
+ outline-offset: 2px;
27
+ }
28
+
29
+ &.iw-before-after-full-width {
30
+ display: block;
31
+ width: 100%;
32
+ }
33
+
34
+ /* Greyed out and dimmed, so it reads as "you cannot move this". */
35
+ &.iw-before-after-disabled {
36
+ --iw-before-after-divider-color: rgb(156 163 175 / 60%);
37
+ --iw-before-after-handle-bg: rgb(156 163 175 / 60%);
38
+ --iw-before-after-handle-color: rgb(255 255 255 / 60%);
39
+
40
+ cursor: default;
41
+
42
+ .iw-before-after-image {
43
+ filter: grayscale(70%);
44
+ opacity: 0.75;
45
+ }
46
+
47
+ .iw-before-after-handle {
48
+ box-shadow: none;
49
+ }
50
+ }
51
+
52
+ .iw-before-after-before {
53
+ display: block;
54
+ }
55
+
56
+ .iw-before-after-before .iw-before-after-image {
57
+ width: 100%;
58
+ height: auto;
59
+ }
60
+
61
+ .iw-before-after-after {
62
+ position: absolute;
63
+ inset: 0;
64
+ clip-path: inset(0 0 0 var(--iw-before-after-position));
65
+ }
66
+
67
+ .iw-before-after-after .iw-before-after-image {
68
+ width: 100%;
69
+ height: 100%;
70
+ object-fit: cover;
71
+ }
72
+
73
+ .iw-before-after-image {
74
+ display: block;
75
+ pointer-events: none;
76
+ user-select: none;
77
+ -webkit-user-drag: none;
78
+ }
79
+
80
+ .iw-before-after-divider {
81
+ position: absolute;
82
+ inset-block: 0;
83
+ left: var(--iw-before-after-position);
84
+ width: var(--iw-before-after-divider-width);
85
+ transform: translateX(-50%);
86
+ background: var(--iw-before-after-divider-color);
87
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 12%);
88
+ pointer-events: none;
89
+ }
90
+
91
+ .iw-before-after-handle {
92
+ position: absolute;
93
+ top: 50%;
94
+ left: 50%;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ box-sizing: border-box;
99
+ width: var(--iw-before-after-handle-size);
100
+ height: var(--iw-before-after-handle-size);
101
+ border-radius: var(--iw-radius-full);
102
+ background: var(--iw-before-after-handle-bg);
103
+ transform: translate(-50%, -50%);
104
+ box-shadow: var(--iw-shadow-md);
105
+ }
106
+
107
+ /* Two chevron arrows drawn with borders, no icon font needed. */
108
+ .iw-before-after-handle::before,
109
+ .iw-before-after-handle::after {
110
+ content: "";
111
+ width: 0;
112
+ height: 0;
113
+ border-top: var(--iw-before-after-handle-arrow) solid transparent;
114
+ border-bottom: var(--iw-before-after-handle-arrow) solid transparent;
115
+ }
116
+
117
+ .iw-before-after-handle::before {
118
+ margin-right: var(--iw-before-after-handle-arrow);
119
+ border-right: var(--iw-before-after-handle-arrow) solid
120
+ var(--iw-before-after-handle-color);
121
+ }
122
+
123
+ .iw-before-after-handle::after {
124
+ margin-left: var(--iw-before-after-handle-arrow);
125
+ border-left: var(--iw-before-after-handle-arrow) solid
126
+ var(--iw-before-after-handle-color);
127
+ }
128
+ }