@c2n/attachment 0.0.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nguyen Thai Vinh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @c2n/attachment
2
+
3
+ File and image attachments with metadata, upload progress, failure states, actions, and responsive grouping.
4
+
5
+ ```html
6
+ <script type="module">
7
+ import '@c2n/attachment'
8
+ </script>
9
+
10
+ <c2-attachment-group>
11
+ <c2-attachment name="project-brief.pdf" type="PDF" size="2.4 MB" status="complete" removable></c2-attachment>
12
+ <c2-attachment name="video.mp4" type="MP4" size="18 MB" status="uploading" progress="64" removable></c2-attachment>
13
+ </c2-attachment-group>
14
+ ```
15
+
16
+ Importing the package root registers both `c2-attachment` and `c2-attachment-group`.
@@ -0,0 +1,53 @@
1
+ import { LitElement, html, unsafeCSS } from "lit";
2
+ import { property } from "lit/decorators.js";
3
+ import { customElement } from "@c2n/core/element-helper.js";
4
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
5
+ function __decorate(decorators, target, key, desc) {
6
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
7
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
8
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
9
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
10
+ }
11
+ //#endregion
12
+ //#region src/attachment-group.scss?inline
13
+ var attachment_group_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: block;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.group {\n display: flex;\n flex-direction: column;\n gap: var(--c2-attachment-group--gap,8px);\n}\n\n:host([layout=grid]) .group,\n:host([layout=mixed]) .group {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--c2-attachment-group--min-column-width,240px)), 1fr));\n}\n\n:host([layout=mixed]) ::slotted(c2-attachment:not([src]):not([layout=tile])),\n:host([layout=mixed]) ::slotted(c2-attachment[layout=row]) {\n grid-column: 1/-1;\n}\n\n::slotted(c2-attachment) {\n min-width: 0;\n}";
14
+ //#endregion
15
+ //#region src/attachment-group.ts
16
+ var AttachmentGroup = class AttachmentGroup extends LitElement {
17
+ constructor(..._args) {
18
+ super(..._args);
19
+ this.managedItems = /* @__PURE__ */ new Set();
20
+ this.layout = "list";
21
+ this.ariaLabel = "Attachments";
22
+ }
23
+ static {
24
+ this.styles = unsafeCSS(attachment_group_default);
25
+ }
26
+ disconnectedCallback() {
27
+ this.clearManagedItems();
28
+ super.disconnectedCallback();
29
+ }
30
+ clearManagedItems() {
31
+ for (const item of this.managedItems) if (item.getAttribute("role") === "listitem") item.removeAttribute("role");
32
+ this.managedItems.clear();
33
+ }
34
+ handleSlotChange(event) {
35
+ const slot = event.currentTarget;
36
+ const attachments = new Set(slot.assignedElements({ flatten: true }).filter((element) => element.localName === "c2-attachment"));
37
+ for (const item of this.managedItems) if (!attachments.has(item) && item.getAttribute("role") === "listitem") item.removeAttribute("role");
38
+ const managedItems = /* @__PURE__ */ new Set();
39
+ for (const attachment of attachments) if (!attachment.hasAttribute("role")) {
40
+ attachment.setAttribute("role", "listitem");
41
+ managedItems.add(attachment);
42
+ } else if (this.managedItems.has(attachment) && attachment.getAttribute("role") === "listitem") managedItems.add(attachment);
43
+ this.managedItems = managedItems;
44
+ }
45
+ render() {
46
+ return html`<div class="group" role="list" aria-label=${this.ariaLabel ?? "Attachments"}><slot @slotchange=${this.handleSlotChange}></slot></div>`;
47
+ }
48
+ };
49
+ __decorate([property({ reflect: true })], AttachmentGroup.prototype, "layout", void 0);
50
+ __decorate([property({ attribute: "aria-label" })], AttachmentGroup.prototype, "ariaLabel", void 0);
51
+ AttachmentGroup = __decorate([customElement("c2-attachment-group")], AttachmentGroup);
52
+ //#endregion
53
+ export { __decorate as n, AttachmentGroup as t };
@@ -0,0 +1,2 @@
1
+ import { t as AttachmentGroup } from "./attachment-group-BtIl-saM.js";
2
+ export { AttachmentGroup };
@@ -0,0 +1,225 @@
1
+ import { n as __decorate, t as AttachmentGroup } from "./attachment-group-BtIl-saM.js";
2
+ import { LitElement, html, nothing, unsafeCSS } from "lit";
3
+ import { property, state } from "lit/decorators.js";
4
+ import { customElement } from "@c2n/core/element-helper.js";
5
+ import { classMap } from "lit/directives/class-map.js";
6
+ import "@c2n/phosphor-icons/icons/file.js";
7
+ import "@c2n/phosphor-icons/icons/file-archive.js";
8
+ import "@c2n/phosphor-icons/icons/file-audio.js";
9
+ import "@c2n/phosphor-icons/icons/file-code.js";
10
+ import "@c2n/phosphor-icons/icons/file-css.js";
11
+ import "@c2n/phosphor-icons/icons/file-csv.js";
12
+ import "@c2n/phosphor-icons/icons/file-doc.js";
13
+ import "@c2n/phosphor-icons/icons/file-html.js";
14
+ import "@c2n/phosphor-icons/icons/file-image.js";
15
+ import "@c2n/phosphor-icons/icons/file-jpg.js";
16
+ import "@c2n/phosphor-icons/icons/file-js.js";
17
+ import "@c2n/phosphor-icons/icons/file-jsx.js";
18
+ import "@c2n/phosphor-icons/icons/file-md.js";
19
+ import "@c2n/phosphor-icons/icons/file-pdf.js";
20
+ import "@c2n/phosphor-icons/icons/file-png.js";
21
+ import "@c2n/phosphor-icons/icons/file-ppt.js";
22
+ import "@c2n/phosphor-icons/icons/file-py.js";
23
+ import "@c2n/phosphor-icons/icons/file-sql.js";
24
+ import "@c2n/phosphor-icons/icons/file-svg.js";
25
+ import "@c2n/phosphor-icons/icons/file-ts.js";
26
+ import "@c2n/phosphor-icons/icons/file-tsx.js";
27
+ import "@c2n/phosphor-icons/icons/file-txt.js";
28
+ import "@c2n/phosphor-icons/icons/file-video.js";
29
+ import "@c2n/phosphor-icons/icons/file-vue.js";
30
+ import "@c2n/phosphor-icons/icons/file-xls.js";
31
+ import "@c2n/phosphor-icons/icons/file-zip.js";
32
+ //#region src/attachment.scss?inline
33
+ var attachment_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: block;\n min-width: 0;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.attachment {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n min-width: 0;\n min-height: var(--c2-attachment--min-height,76px);\n gap: var(--c2-attachment--gap,12px);\n padding: var(--c2-attachment--padding,10px 12px);\n color: var(--c2-attachment--color,#18181b);\n background: var(--c2-attachment--background,#ffffff);\n border: var(--c2-attachment--border,1px solid #e4e4e7);\n border-radius: var(--c2-attachment--border-radius,14px);\n box-shadow: var(--c2-attachment--box-shadow);\n position: relative;\n}\n\n.media {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: center;\n position: relative;\n box-sizing: border-box;\n overflow: hidden;\n width: var(--c2-attachment__media--size,48px);\n height: var(--c2-attachment__media--size,48px);\n color: var(--c2-attachment__media--color,#71717a);\n background: var(--c2-attachment__media--background,#f4f4f5);\n border-radius: var(--c2-attachment__media--border-radius,10px);\n}\n.media img,\n.media ::slotted(img),\n.media ::slotted(video),\n.media ::slotted(svg) {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: var(--c2-attachment__media--object-fit,cover);\n}\n.media ::slotted([slot=media]) {\n width: 100%;\n height: 100%;\n}\n.media .file-icon {\n --c2-phosphor-icon--size: 26px;\n --c2-phosphor-icon--color: currentColor;\n}\n\n.has-image .media {\n background: transparent;\n}\n\n.tile {\n align-items: stretch;\n flex-direction: column;\n width: min(100%, var(--c2-attachment__tile--width,220px));\n padding: 16px;\n gap: 14px;\n}\n.tile .media {\n width: 100%;\n height: auto;\n aspect-ratio: var(--c2-attachment__tile--media-aspect-ratio,1);\n}\n.tile .actions {\n position: absolute;\n top: 22px;\n right: 22px;\n padding: 2px;\n border-radius: 8px;\n background: rgba(24, 24, 27, 0.72);\n opacity: 0;\n transition: opacity 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.tile:hover .actions,\n.tile .actions:focus-within {\n opacity: 1;\n}\n.tile button {\n color: #ffffff;\n}\n\n.content {\n flex: 1;\n min-width: 0;\n}\n\n.name {\n overflow: hidden;\n color: var(--c2-attachment--color,#18181b);\n font-size: var(--c2-attachment__name--font-size,14px);\n font-weight: var(--c2-attachment__name--font-weight,500);\n line-height: 1.35;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.metadata,\n.status,\n.uploading {\n margin-top: 3px;\n color: var(--c2-attachment__metadata--color,#71717a);\n font-size: var(--c2-attachment__metadata--font-size,12px);\n line-height: 1.35;\n}\n\n.metadata {\n display: flex;\n gap: 4px;\n}\n\n.status {\n color: var(--c2-attachment__status--color,#71717a);\n}\n.status.complete {\n color: var(--c2-attachment__status__complete--color,rgb(0, 122, 77));\n}\n.status.error {\n color: var(--c2-attachment__status__error--color,#dc2626);\n}\n\n.progress-ring {\n width: 26px;\n height: 26px;\n fill: none;\n stroke-width: 2;\n transform: rotate(-90deg);\n}\n.progress-ring .progress-track {\n stroke: var(--c2-attachment__progress--background,#e4e4e7);\n}\n.progress-ring .progress-value {\n stroke: var(--c2-attachment__progress--color,rgb(2, 101, 220));\n stroke-linecap: round;\n}\n\n.visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.actions,\n.default-actions {\n display: flex;\n flex: none;\n align-items: center;\n gap: 4px;\n}\n\nbutton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--c2-attachment__action--size,28px);\n height: var(--c2-attachment__action--size,28px);\n padding: 0;\n color: var(--c2-attachment__action--color,#71717a);\n background: transparent;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n transition: background 250ms cubic-bezier(0.2, 0, 0, 1), color 250ms cubic-bezier(0.2, 0, 0, 1);\n}\nbutton:hover:not(:disabled) {\n background: var(--c2-attachment__action__hover--background,#f4f4f5);\n}\nbutton:focus-visible {\n outline: var(--c2-attachment__action__focus--outline,2px solid rgba(2, 101, 220, 0.4));\n outline-offset: 1px;\n}\nbutton:disabled {\n cursor: default;\n}\nbutton svg {\n width: 16px;\n height: 16px;\n fill: none;\n stroke: currentColor;\n stroke-width: 1.8;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n:host([disabled]) .attachment {\n opacity: var(--c2-attachment__disabled--opacity,0.38);\n}\n\n::slotted([slot=actions]) {\n flex: none;\n}";
34
+ //#endregion
35
+ //#region src/attachment.ts
36
+ var Attachment = class Attachment extends LitElement {
37
+ constructor(..._args) {
38
+ super(..._args);
39
+ this.name = "";
40
+ this.src = void 0;
41
+ this.alt = void 0;
42
+ this.type = void 0;
43
+ this.size = void 0;
44
+ this.status = "ready";
45
+ this.layout = "auto";
46
+ this.progress = 0;
47
+ this.removable = false;
48
+ this.disabled = false;
49
+ this.imageFailed = false;
50
+ }
51
+ static {
52
+ this.styles = unsafeCSS(attachment_default);
53
+ }
54
+ willUpdate(changed) {
55
+ if (changed.has("src")) this.imageFailed = false;
56
+ }
57
+ get safeProgress() {
58
+ return Math.min(100, Math.max(0, Number.isFinite(this.progress) ? this.progress : 0));
59
+ }
60
+ emitRemove() {
61
+ this.dispatchEvent(new CustomEvent("attachment-remove", {
62
+ detail: { name: this.name },
63
+ bubbles: true,
64
+ composed: true
65
+ }));
66
+ }
67
+ emitRetry() {
68
+ this.dispatchEvent(new CustomEvent("attachment-retry", {
69
+ detail: { name: this.name },
70
+ bubbles: true,
71
+ composed: true
72
+ }));
73
+ }
74
+ handleImageError() {
75
+ this.imageFailed = true;
76
+ this.dispatchEvent(new Event("media-error"));
77
+ }
78
+ get fileIcon() {
79
+ const extension = this.name.split(".").pop()?.toLowerCase() ?? "";
80
+ const kind = `${this.type ?? ""} ${extension}`.toLowerCase();
81
+ if (/\bzip\b/.test(kind)) return "zip";
82
+ if (/\b(7z|rar|tar|gz|gzip|archive)\b/.test(kind)) return "archive";
83
+ if (/\btsx\b/.test(kind)) return "tsx";
84
+ if (/\b(ts|typescript)\b/.test(kind)) return "ts";
85
+ if (/\bjsx\b/.test(kind)) return "jsx";
86
+ if (/\b(js|javascript)\b/.test(kind)) return "js";
87
+ if (/\b(html|htm)\b/.test(kind)) return "html";
88
+ if (/\bcss\b/.test(kind)) return "css";
89
+ if (/\bvue\b/.test(kind)) return "vue";
90
+ if (/\b(py|python)\b/.test(kind)) return "py";
91
+ if (/\bsql\b/.test(kind)) return "sql";
92
+ if (/\bpdf\b/.test(kind)) return "pdf";
93
+ if (/\bcsv\b/.test(kind)) return "csv";
94
+ if (/\b(doc|docx|word)\b/.test(kind)) return "doc";
95
+ if (/\b(xls|xlsx|spreadsheet|excel)\b/.test(kind)) return "xls";
96
+ if (/\b(ppt|pptx|presentation|powerpoint)\b/.test(kind)) return "ppt";
97
+ if (/\b(md|markdown)\b/.test(kind)) return "md";
98
+ if (/\b(txt|text)\b/.test(kind)) return "txt";
99
+ if (/\bpng\b/.test(kind)) return "png";
100
+ if (/\b(jpg|jpeg)\b/.test(kind)) return "jpg";
101
+ if (/\bsvg\b/.test(kind)) return "svg";
102
+ if (/\b(image|gif|webp|avif|bmp|tiff)\b/.test(kind)) return "image";
103
+ if (/\b(audio|mp3|wav|ogg|flac|m4a)\b/.test(kind)) return "audio";
104
+ if (/\b(video|mp4|mov|webm|avi|mkv)\b/.test(kind)) return "video";
105
+ if (/\b(code|json|xml|yaml|yml|toml)\b/.test(kind)) return "code";
106
+ return "file";
107
+ }
108
+ renderFileIcon() {
109
+ switch (this.fileIcon) {
110
+ case "zip": return html`<c2-phosphor-file-zip class="file-icon" aria-hidden="true"></c2-phosphor-file-zip>`;
111
+ case "archive": return html`<c2-phosphor-file-archive class="file-icon" aria-hidden="true"></c2-phosphor-file-archive>`;
112
+ case "audio": return html`<c2-phosphor-file-audio class="file-icon" aria-hidden="true"></c2-phosphor-file-audio>`;
113
+ case "code": return html`<c2-phosphor-file-code class="file-icon" aria-hidden="true"></c2-phosphor-file-code>`;
114
+ case "css": return html`<c2-phosphor-file-css class="file-icon" aria-hidden="true"></c2-phosphor-file-css>`;
115
+ case "csv": return html`<c2-phosphor-file-csv class="file-icon" aria-hidden="true"></c2-phosphor-file-csv>`;
116
+ case "doc": return html`<c2-phosphor-file-doc class="file-icon" aria-hidden="true"></c2-phosphor-file-doc>`;
117
+ case "html": return html`<c2-phosphor-file-html class="file-icon" aria-hidden="true"></c2-phosphor-file-html>`;
118
+ case "image": return html`<c2-phosphor-file-image class="file-icon" aria-hidden="true"></c2-phosphor-file-image>`;
119
+ case "jpg": return html`<c2-phosphor-file-jpg class="file-icon" aria-hidden="true"></c2-phosphor-file-jpg>`;
120
+ case "js": return html`<c2-phosphor-file-js class="file-icon" aria-hidden="true"></c2-phosphor-file-js>`;
121
+ case "jsx": return html`<c2-phosphor-file-jsx class="file-icon" aria-hidden="true"></c2-phosphor-file-jsx>`;
122
+ case "md": return html`<c2-phosphor-file-md class="file-icon" aria-hidden="true"></c2-phosphor-file-md>`;
123
+ case "pdf": return html`<c2-phosphor-file-pdf class="file-icon" aria-hidden="true"></c2-phosphor-file-pdf>`;
124
+ case "png": return html`<c2-phosphor-file-png class="file-icon" aria-hidden="true"></c2-phosphor-file-png>`;
125
+ case "ppt": return html`<c2-phosphor-file-ppt class="file-icon" aria-hidden="true"></c2-phosphor-file-ppt>`;
126
+ case "py": return html`<c2-phosphor-file-py class="file-icon" aria-hidden="true"></c2-phosphor-file-py>`;
127
+ case "sql": return html`<c2-phosphor-file-sql class="file-icon" aria-hidden="true"></c2-phosphor-file-sql>`;
128
+ case "svg": return html`<c2-phosphor-file-svg class="file-icon" aria-hidden="true"></c2-phosphor-file-svg>`;
129
+ case "ts": return html`<c2-phosphor-file-ts class="file-icon" aria-hidden="true"></c2-phosphor-file-ts>`;
130
+ case "tsx": return html`<c2-phosphor-file-tsx class="file-icon" aria-hidden="true"></c2-phosphor-file-tsx>`;
131
+ case "txt": return html`<c2-phosphor-file-txt class="file-icon" aria-hidden="true"></c2-phosphor-file-txt>`;
132
+ case "video": return html`<c2-phosphor-file-video class="file-icon" aria-hidden="true"></c2-phosphor-file-video>`;
133
+ case "vue": return html`<c2-phosphor-file-vue class="file-icon" aria-hidden="true"></c2-phosphor-file-vue>`;
134
+ case "xls": return html`<c2-phosphor-file-xls class="file-icon" aria-hidden="true"></c2-phosphor-file-xls>`;
135
+ default: return html`<c2-phosphor-file class="file-icon" aria-hidden="true"></c2-phosphor-file>`;
136
+ }
137
+ }
138
+ renderMediaFallback() {
139
+ if (this.status === "uploading") return html`
140
+ <svg class="progress-ring" viewBox="0 0 24 24" aria-hidden="true">
141
+ <circle class="progress-track" cx="12" cy="12" r="9" pathLength="100" />
142
+ <circle class="progress-value" cx="12" cy="12" r="9" pathLength="100" stroke-dasharray=${`${this.safeProgress} 100`} />
143
+ </svg>
144
+ `;
145
+ return this.renderFileIcon();
146
+ }
147
+ renderStatus() {
148
+ if (this.status === "uploading") return html`
149
+ <div class="status uploading">
150
+ <progress class="visually-hidden" max="100" .value=${this.safeProgress} aria-label=${`Uploading ${this.name || "attachment"}`}></progress>
151
+ <span>Uploading · ${Math.round(this.safeProgress)}%</span>
152
+ </div>
153
+ `;
154
+ if (this.status === "complete") return html`<div class="status complete">Uploaded</div>`;
155
+ if (this.status === "error") return html`<div class="status error" role="status">Upload failed</div>`;
156
+ return nothing;
157
+ }
158
+ renderActions() {
159
+ if (this.status !== "error" && !this.removable) return nothing;
160
+ return html`
161
+ <div class="default-actions">
162
+ ${this.status === "error" ? html`<button type="button" aria-label=${`Retry ${this.name || "attachment"}`} ?disabled=${this.disabled} @click=${this.emitRetry}>
163
+ <svg viewBox="0 0 24 24" aria-hidden="true">
164
+ <path d="M20 6v5h-5M4 18v-5h5M6.1 9a7 7 0 0 1 11.8-2.6L20 11M4 13l2.1 4.6A7 7 0 0 0 17.9 15" />
165
+ </svg>
166
+ </button>` : nothing}
167
+ ${this.removable ? html`<button
168
+ type="button"
169
+ aria-label=${`${this.status === "uploading" ? "Cancel upload" : "Remove"} ${this.name || "attachment"}`}
170
+ ?disabled=${this.disabled}
171
+ @click=${this.emitRemove}
172
+ >
173
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
174
+ </button>` : nothing}
175
+ </div>
176
+ `;
177
+ }
178
+ render() {
179
+ const showImage = !!this.src && !this.imageFailed;
180
+ const tile = this.layout === "tile" || this.layout === "auto" && !!this.src;
181
+ return html`
182
+ <article class=${classMap({
183
+ attachment: true,
184
+ "has-image": showImage,
185
+ tile
186
+ })} aria-label=${this.name || nothing}>
187
+ <div class="media">
188
+ <slot name="media">
189
+ ${showImage ? html`<img src=${this.src} alt=${this.alt ?? this.name} loading="lazy" @error=${this.handleImageError} />` : this.renderMediaFallback()}
190
+ </slot>
191
+ </div>
192
+ <div class="content">
193
+ <div class="name"><slot name="name">${this.name || "Untitled attachment"}</slot></div>
194
+ ${this.status !== "uploading" && (this.type || this.size) ? html`<div class="metadata">
195
+ <slot name="metadata"
196
+ >${this.type ? html`<span>${this.type}</span>` : nothing}${this.type && this.size ? html`<span aria-hidden="true">·</span>` : nothing}${this.size ? html`<span>${this.size}</span>` : nothing}</slot
197
+ >
198
+ </div>` : html`<slot name="metadata"></slot>`}
199
+ ${this.renderStatus()}
200
+ </div>
201
+ <div class="actions"><slot name="actions">${this.renderActions()}</slot></div>
202
+ </article>
203
+ `;
204
+ }
205
+ };
206
+ __decorate([property()], Attachment.prototype, "name", void 0);
207
+ __decorate([property({ reflect: true })], Attachment.prototype, "src", void 0);
208
+ __decorate([property()], Attachment.prototype, "alt", void 0);
209
+ __decorate([property()], Attachment.prototype, "type", void 0);
210
+ __decorate([property()], Attachment.prototype, "size", void 0);
211
+ __decorate([property({ reflect: true })], Attachment.prototype, "status", void 0);
212
+ __decorate([property({ reflect: true })], Attachment.prototype, "layout", void 0);
213
+ __decorate([property({ type: Number })], Attachment.prototype, "progress", void 0);
214
+ __decorate([property({
215
+ type: Boolean,
216
+ reflect: true
217
+ })], Attachment.prototype, "removable", void 0);
218
+ __decorate([property({
219
+ type: Boolean,
220
+ reflect: true
221
+ })], Attachment.prototype, "disabled", void 0);
222
+ __decorate([state()], Attachment.prototype, "imageFailed", void 0);
223
+ Attachment = __decorate([customElement("c2-attachment")], Attachment);
224
+ //#endregion
225
+ export { Attachment, AttachmentGroup };
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@c2n/attachment",
3
+ "version": "0.0.7",
4
+ "type": "module",
5
+ "main": "dist/attachment.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./types/src/attachment.d.ts",
9
+ "default": "./dist/attachment.js"
10
+ },
11
+ "./attachment-group.js": {
12
+ "types": "./types/src/attachment-group.d.ts",
13
+ "default": "./dist/attachment-group.js"
14
+ },
15
+ "./react": {
16
+ "types": "./react.d.ts",
17
+ "default": "./react.js"
18
+ },
19
+ "./vue": {
20
+ "types": "./vue.d.ts",
21
+ "default": "./vue.js"
22
+ },
23
+ "./custom-elements.json": "./custom-elements.json"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "types",
28
+ "react.d.ts",
29
+ "react.js",
30
+ "vue.d.ts",
31
+ "vue.js"
32
+ ],
33
+ "keywords": [
34
+ "attachment",
35
+ "web component",
36
+ "lit"
37
+ ],
38
+ "license": "MIT",
39
+ "author": "code2nguyen@gmail.com",
40
+ "publishConfig": {
41
+ "registry": "https://registry.npmjs.org",
42
+ "access": "public"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/code2nguyen/web-components.git"
47
+ },
48
+ "scripts": {
49
+ "dev": "vite",
50
+ "build": "wireit",
51
+ "build:only": "vite build",
52
+ "type-check": "wireit"
53
+ },
54
+ "wireit": {
55
+ "type-check": {
56
+ "dependencies": [
57
+ "../../core:build",
58
+ "../../icons/phosphor-icons:build"
59
+ ],
60
+ "command": "tsc -p tsconfig.lib.json --composite false"
61
+ },
62
+ "build": {
63
+ "dependencies": [
64
+ "type-check"
65
+ ],
66
+ "command": "vite build"
67
+ }
68
+ },
69
+ "dependencies": {
70
+ "@c2n/core": "0.0.7",
71
+ "@c2n/phosphor-icons": "0.0.7",
72
+ "lit": "3.3.3"
73
+ },
74
+ "devDependencies": {
75
+ "@c2n/config": "*"
76
+ },
77
+ "customElements": "custom-elements.json",
78
+ "gitHead": "c8db398a27f7cd417d665fdf5da455fee2d4e910"
79
+ }
package/react.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ //
3
+ // JSX types for the custom elements of @c2n/attachment. Import once, anywhere in the program:
4
+ //
5
+ // import '@c2n/attachment/react'
6
+ //
7
+ // Register the elements at module scope before React renders, or React writes an object prop as an
8
+ // attribute and it stringifies.
9
+
10
+ import type { DetailedHTMLProps, HTMLAttributes } from 'react'
11
+ import type { Attachment } from '@c2n/attachment'
12
+ import type { AttachmentGroup } from '@c2n/attachment/attachment-group.js'
13
+
14
+ /** Standard React host-element attributes plus the element's own public properties. */
15
+ type C2Props<T> = DetailedHTMLProps<HTMLAttributes<T>, T> & Partial<Omit<T, keyof HTMLElement>>
16
+
17
+ declare module 'react' {
18
+ namespace JSX {
19
+ interface IntrinsicElements {
20
+ 'c2-attachment': C2Props<Attachment>
21
+ 'c2-attachment-group': C2Props<AttachmentGroup>
22
+ }
23
+ }
24
+ }
25
+
26
+ export {}
package/react.js ADDED
@@ -0,0 +1,3 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ // Types only — see the matching .d.ts.
3
+ export {}
@@ -0,0 +1,30 @@
1
+ import { LitElement } from 'lit';
2
+ export type AttachmentGroupLayout = 'list' | 'grid' | 'mixed';
3
+ /**
4
+ * Arranges related attachments as a responsive list or grid.
5
+ *
6
+ * @tag c2-attachment-group
7
+ * @slot - `c2-attachment` elements.
8
+ * @slotcomponent c2-attachment
9
+ *
10
+ * @cssproperty {pixel} [--c2-attachment-group--gap=8px]
11
+ * @cssproperty {pixel} [--c2-attachment-group--min-column-width=240px]
12
+ */
13
+ export declare class AttachmentGroup extends LitElement {
14
+ static styles: import("lit").CSSResult;
15
+ private managedItems;
16
+ /** List stacks rows, grid fills equal columns, and mixed gives image tiles columns while file rows span the group. */
17
+ layout: 'list' | 'grid' | 'mixed';
18
+ /** Accessible name for the attachment collection. */
19
+ ariaLabel: string | null;
20
+ disconnectedCallback(): void;
21
+ private clearManagedItems;
22
+ private handleSlotChange;
23
+ render(): import("lit-html").TemplateResult<1>;
24
+ }
25
+ declare global {
26
+ interface HTMLElementTagNameMap {
27
+ 'c2-attachment-group': AttachmentGroup;
28
+ }
29
+ }
30
+ //# sourceMappingURL=attachment-group.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-group.d.ts","sourceRoot":"","sources":["../../src/attachment-group.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAmB,MAAM,KAAK,CAAA;AAKjD,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAE7D;;;;;;;;;GASG;AACH,qBACa,eAAgB,SAAQ,UAAU;IAC7C,OAAgB,MAAM,0BAAoB;IAE1C,OAAO,CAAC,YAAY,CAAyB;IAE7C,sHAAsH;IACzF,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAS;IAEvE,qDAAqD;IACL,SAAS,EAAE,MAAM,GAAG,IAAI,CAAgB;IAE/E,oBAAoB;IAK7B,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,gBAAgB;IAoBf,MAAM;CAGhB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,qBAAqB,EAAE,eAAe,CAAA;KACvC;CACF"}
@@ -0,0 +1,130 @@
1
+ import { LitElement, type PropertyValues } from 'lit';
2
+ import type { TypedAddEventListener, TypedRemoveEventListener } from '@c2n/core/event-helper.js';
3
+ import '@c2n/phosphor-icons/icons/file.js';
4
+ import '@c2n/phosphor-icons/icons/file-archive.js';
5
+ import '@c2n/phosphor-icons/icons/file-audio.js';
6
+ import '@c2n/phosphor-icons/icons/file-code.js';
7
+ import '@c2n/phosphor-icons/icons/file-css.js';
8
+ import '@c2n/phosphor-icons/icons/file-csv.js';
9
+ import '@c2n/phosphor-icons/icons/file-doc.js';
10
+ import '@c2n/phosphor-icons/icons/file-html.js';
11
+ import '@c2n/phosphor-icons/icons/file-image.js';
12
+ import '@c2n/phosphor-icons/icons/file-jpg.js';
13
+ import '@c2n/phosphor-icons/icons/file-js.js';
14
+ import '@c2n/phosphor-icons/icons/file-jsx.js';
15
+ import '@c2n/phosphor-icons/icons/file-md.js';
16
+ import '@c2n/phosphor-icons/icons/file-pdf.js';
17
+ import '@c2n/phosphor-icons/icons/file-png.js';
18
+ import '@c2n/phosphor-icons/icons/file-ppt.js';
19
+ import '@c2n/phosphor-icons/icons/file-py.js';
20
+ import '@c2n/phosphor-icons/icons/file-sql.js';
21
+ import '@c2n/phosphor-icons/icons/file-svg.js';
22
+ import '@c2n/phosphor-icons/icons/file-ts.js';
23
+ import '@c2n/phosphor-icons/icons/file-tsx.js';
24
+ import '@c2n/phosphor-icons/icons/file-txt.js';
25
+ import '@c2n/phosphor-icons/icons/file-video.js';
26
+ import '@c2n/phosphor-icons/icons/file-vue.js';
27
+ import '@c2n/phosphor-icons/icons/file-xls.js';
28
+ import '@c2n/phosphor-icons/icons/file-zip.js';
29
+ export { AttachmentGroup, type AttachmentGroupLayout } from './attachment-group.js';
30
+ export type AttachmentStatus = 'ready' | 'uploading' | 'complete' | 'error';
31
+ export type AttachmentLayout = 'auto' | 'row' | 'tile';
32
+ /** Events fired by {@link Attachment}, keyed for `addEventListener`. */
33
+ export interface AttachmentEventMap {
34
+ 'attachment-remove': CustomEvent<{
35
+ name: string;
36
+ }>;
37
+ 'attachment-retry': CustomEvent<{
38
+ name: string;
39
+ }>;
40
+ 'media-error': Event;
41
+ }
42
+ export interface Attachment {
43
+ addEventListener: TypedAddEventListener<Attachment, AttachmentEventMap>;
44
+ removeEventListener: TypedRemoveEventListener<Attachment, AttachmentEventMap>;
45
+ }
46
+ /**
47
+ * Displays a file or image attachment with a preview, metadata, upload state and optional actions.
48
+ *
49
+ * @tag c2-attachment
50
+ *
51
+ * @slot media - Custom thumbnail or file icon. Replaces the image supplied through `src`.
52
+ * @slot name - Custom file name. Falls back to the `name` attribute.
53
+ * @slot metadata - Custom metadata. Falls back to the `type` and `size` attributes.
54
+ * @slot actions - Custom controls shown at the end. Replaces the built-in retry and remove buttons.
55
+ *
56
+ * @event {CustomEvent<{ name: string }>} attachment-remove - Fired when the remove or cancel action is pressed.
57
+ * @event {CustomEvent<{ name: string }>} attachment-retry - Fired when retry is pressed for a failed upload.
58
+ * @event {Event} media-error - Fired when the image preview cannot be loaded; the file fallback is shown.
59
+ *
60
+ * @cssproperty {pixel} [--c2-attachment--min-height=76px]
61
+ * @cssproperty {pixel} [--c2-attachment--gap=12px]
62
+ * @cssproperty {padding} [--c2-attachment--padding=10px 12px]
63
+ * @cssproperty {color} [--c2-attachment--background=#ffffff]
64
+ * @cssproperty {border} [--c2-attachment--border=1px solid #e4e4e7]
65
+ * @cssproperty {border-radius} [--c2-attachment--border-radius=14px]
66
+ * @cssproperty {box-shadow} --c2-attachment--box-shadow
67
+ * @cssproperty {color} [--c2-attachment--color=#18181b]
68
+ * @cssproperty {pixel} [--c2-attachment__media--size=48px]
69
+ * @cssproperty {color} [--c2-attachment__media--background=#f4f4f5]
70
+ * @cssproperty {color} [--c2-attachment__media--color=#71717a]
71
+ * @cssproperty {border-radius} [--c2-attachment__media--border-radius=10px]
72
+ * @cssproperty {object-fit} [--c2-attachment__media--object-fit=cover]
73
+ * @cssproperty {pixel} [--c2-attachment__tile--width=220px]
74
+ * @cssproperty {ratio} [--c2-attachment__tile--media-aspect-ratio=1]
75
+ * @cssproperty {font-size} [--c2-attachment__name--font-size=14px]
76
+ * @cssproperty {font-weight} [--c2-attachment__name--font-weight=500]
77
+ * @cssproperty {color} [--c2-attachment__metadata--color=#71717a]
78
+ * @cssproperty {font-size} [--c2-attachment__metadata--font-size=12px]
79
+ * @cssproperty {color} [--c2-attachment__status--color=#71717a]
80
+ * @cssproperty {color} [--c2-attachment__status__complete--color=rgb(0, 122, 77)]
81
+ * @cssproperty {color} [--c2-attachment__status__error--color=#dc2626]
82
+ * @cssproperty {color} [--c2-attachment__progress--background=#e4e4e7]
83
+ * @cssproperty {color} [--c2-attachment__progress--color=rgb(2, 101, 220)]
84
+ * @cssproperty {pixel} [--c2-attachment__action--size=28px]
85
+ * @cssproperty {color} [--c2-attachment__action--color=#71717a]
86
+ * @cssproperty {color} [--c2-attachment__action__hover--background=#f4f4f5]
87
+ * @cssproperty {outline} [--c2-attachment__action__focus--outline=2px solid rgba(2, 101, 220, 0.4)]
88
+ * @cssproperty {opacity} [--c2-attachment__disabled--opacity=0.38]
89
+ */
90
+ export declare class Attachment extends LitElement {
91
+ static styles: import("lit").CSSResult;
92
+ /** File name shown as the primary label. */
93
+ name: string;
94
+ /** Image preview URL. Omit it to show the generic file preview. */
95
+ src: string | undefined;
96
+ /** Accessible description for the image preview. Defaults to the file name. */
97
+ alt: string | undefined;
98
+ /** File type or extension shown in the metadata row. */
99
+ type: string | undefined;
100
+ /** Human-readable file size, for example `2.4 MB`. */
101
+ size: string | undefined;
102
+ /** Current upload state. */
103
+ status: 'ready' | 'uploading' | 'complete' | 'error';
104
+ /** Auto uses a tile for image previews and a row for files. */
105
+ layout: 'auto' | 'row' | 'tile';
106
+ /** Upload completion percentage. Values are clamped between 0 and 100. */
107
+ progress: number;
108
+ /** Show the built-in remove action. During upload it is announced as cancel. */
109
+ removable: boolean;
110
+ /** Disable built-in actions and dim the attachment. */
111
+ disabled: boolean;
112
+ private imageFailed;
113
+ willUpdate(changed: PropertyValues<this>): void;
114
+ private get safeProgress();
115
+ private emitRemove;
116
+ private emitRetry;
117
+ private handleImageError;
118
+ private get fileIcon();
119
+ private renderFileIcon;
120
+ private renderMediaFallback;
121
+ private renderStatus;
122
+ private renderActions;
123
+ render(): import("lit-html").TemplateResult<1>;
124
+ }
125
+ declare global {
126
+ interface HTMLElementTagNameMap {
127
+ 'c2-attachment': Attachment;
128
+ }
129
+ }
130
+ //# sourceMappingURL=attachment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment.d.ts","sourceRoot":"","sources":["../../src/attachment.ts"],"names":[],"mappings":"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"}
package/vue.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ //
3
+ // Vue template types for the custom elements of @c2n/attachment. Import once, anywhere in the program:
4
+ //
5
+ // import '@c2n/attachment/vue'
6
+ //
7
+ // Tell the compiler about the tags as well, or every c2-* tag is resolved as a Vue component and renders
8
+ // nothing: template.compilerOptions.isCustomElement = (tag) => tag.startsWith('c2-') in vite.config.ts.
9
+
10
+ import type { DefineComponent, HTMLAttributes } from 'vue'
11
+ import type { Attachment, AttachmentEventMap } from '@c2n/attachment'
12
+ import type { AttachmentGroup } from '@c2n/attachment/attachment-group.js'
13
+
14
+ /** The element's own public properties, plus every attribute Vue understands on a host element. */
15
+ type C2Props<T> = Partial<Omit<T, keyof HTMLElement>> & HTMLAttributes
16
+
17
+ declare module 'vue' {
18
+ interface GlobalComponents {
19
+ 'c2-attachment': DefineComponent<
20
+ C2Props<Attachment> & {
21
+ onAttachmentRemove?: (event: AttachmentEventMap['attachment-remove']) => void
22
+ onAttachmentRetry?: (event: AttachmentEventMap['attachment-retry']) => void
23
+ onMediaError?: (event: AttachmentEventMap['media-error']) => void
24
+ }
25
+ >
26
+ 'c2-attachment-group': DefineComponent<
27
+ C2Props<AttachmentGroup> & {
28
+ 'aria-label'?: unknown
29
+ }
30
+ >
31
+ }
32
+ }
33
+
34
+ declare module '@vue/runtime-dom' {
35
+ interface HTMLAttributes {
36
+ /** Vue's own definition omits it, and slotting a plain element into a component needs it. */
37
+ slot?: string
38
+ }
39
+ }
40
+
41
+ export {}
package/vue.js ADDED
@@ -0,0 +1,3 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ // Types only — see the matching .d.ts.
3
+ export {}