@dilipod/ui 0.4.33 → 0.4.35

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.
@@ -61,6 +61,104 @@ export declare function slackActions(buttons: {
61
61
  actionId?: string;
62
62
  style?: 'primary' | 'danger';
63
63
  }[]): SlackBlock;
64
+ export interface SlackModalView {
65
+ type: 'modal';
66
+ callback_id: string;
67
+ title: {
68
+ type: 'plain_text';
69
+ text: string;
70
+ };
71
+ submit?: {
72
+ type: 'plain_text';
73
+ text: string;
74
+ };
75
+ close?: {
76
+ type: 'plain_text';
77
+ text: string;
78
+ };
79
+ blocks: SlackBlock[];
80
+ private_metadata?: string;
81
+ }
82
+ export interface SlackOptionGroup {
83
+ label: {
84
+ type: 'plain_text';
85
+ text: string;
86
+ };
87
+ options: Array<{
88
+ text: {
89
+ type: 'plain_text';
90
+ text: string;
91
+ };
92
+ value: string;
93
+ }>;
94
+ }
95
+ /**
96
+ * Build a modal view for Slack.
97
+ *
98
+ * slackModal({
99
+ * callbackId: 'run_process',
100
+ * title: 'Run a Process',
101
+ * submitText: 'Run',
102
+ * blocks: [...],
103
+ * })
104
+ */
105
+ export declare function slackModal(options: {
106
+ callbackId: string;
107
+ title: string;
108
+ submitText?: string;
109
+ closeText?: string;
110
+ blocks: SlackBlock[];
111
+ privateMetadata?: string;
112
+ }): SlackModalView;
113
+ /**
114
+ * Build a static select input block (for modals or messages).
115
+ *
116
+ * slackStaticSelect({
117
+ * blockId: 'process_select',
118
+ * actionId: 'process_id',
119
+ * label: 'Process',
120
+ * placeholder: 'Select a process',
121
+ * optionGroups: [
122
+ * { label: 'Invoice Bot', options: [{ text: 'Send Invoices', value: 'uuid' }] },
123
+ * ],
124
+ * })
125
+ */
126
+ export declare function slackStaticSelect(options: {
127
+ blockId: string;
128
+ actionId: string;
129
+ label: string;
130
+ placeholder?: string;
131
+ options?: Array<{
132
+ text: string;
133
+ value: string;
134
+ }>;
135
+ optionGroups?: Array<{
136
+ label: string;
137
+ options: Array<{
138
+ text: string;
139
+ value: string;
140
+ }>;
141
+ }>;
142
+ }): SlackBlock;
143
+ /**
144
+ * Build a plain text input block (for modals).
145
+ */
146
+ export declare function slackTextInput(options: {
147
+ blockId: string;
148
+ actionId: string;
149
+ label: string;
150
+ placeholder?: string;
151
+ optional?: boolean;
152
+ multiline?: boolean;
153
+ }): SlackBlock;
154
+ /**
155
+ * Build a divider block.
156
+ */
157
+ export declare function slackDivider(): SlackBlock;
158
+ /**
159
+ * Build a context block (small muted text).
160
+ */
161
+ export declare function slackContext(text: string): SlackBlock;
64
162
  /**
65
163
  * Build a complete Slack message with optional details, note, and button.
66
164
  * Covers 90% of notification use cases.
@@ -1 +1 @@
1
- {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../src/lib/slack.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACrC,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAA;IACzB,MAAM,CAAC,EAAE,UAAU,EAAE,CAAA;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,UAAU,EAAE,CAAA;CACrB;AAMD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAKvD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,UAAU,CAQ/E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE;IACP,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;CAC7B,EAAE,GACF,UAAU,CAYZ;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;IACzC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,YAAY,CAmBf"}
1
+ {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../src/lib/slack.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  * server actions, and other server-side code.
7
7
  */
8
8
  export { emailTemplate, buttonHtml, infoBoxHtml, noteBoxHtml } from './lib/email';
9
- export { slackMessage, slackSection, slackFields, slackActions } from './lib/slack';
10
- export type { SlackBlock, SlackElement, SlackField, SlackMessage } from './lib/slack';
9
+ export { slackMessage, slackSection, slackFields, slackActions, slackModal, slackStaticSelect, slackTextInput, slackDivider, slackContext, } from './lib/slack';
10
+ export type { SlackBlock, SlackElement, SlackField, SlackMessage, SlackModalView, SlackOptionGroup, } from './lib/slack';
11
11
  export { formatCentsToEuros, formatEuros, formatDuration, formatRelativeTime } from './lib/formatting';
12
12
  export { cn } from './lib/utils';
13
13
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGjF,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACnF,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAGrF,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAGtG,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGjF,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,YAAY,EACV,UAAU,EACV,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,cAAc,EACd,gBAAgB,GACjB,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAGtG,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA"}
package/dist/server.js CHANGED
@@ -135,6 +135,67 @@ function slackActions(buttons) {
135
135
  }))
136
136
  };
137
137
  }
138
+ function slackModal(options) {
139
+ return {
140
+ type: "modal",
141
+ callback_id: options.callbackId,
142
+ title: { type: "plain_text", text: options.title },
143
+ ...options.submitText ? { submit: { type: "plain_text", text: options.submitText } } : {},
144
+ ...options.closeText ? { close: { type: "plain_text", text: options.closeText } } : {},
145
+ blocks: options.blocks,
146
+ ...options.privateMetadata ? { private_metadata: options.privateMetadata } : {}
147
+ };
148
+ }
149
+ function slackStaticSelect(options) {
150
+ const element = {
151
+ type: "static_select",
152
+ action_id: options.actionId,
153
+ placeholder: { type: "plain_text", text: options.placeholder || "Select..." }
154
+ };
155
+ if (options.optionGroups) {
156
+ element.option_groups = options.optionGroups.map((group) => ({
157
+ label: { type: "plain_text", text: group.label },
158
+ options: group.options.map((opt) => ({
159
+ text: { type: "plain_text", text: opt.text },
160
+ value: opt.value
161
+ }))
162
+ }));
163
+ } else if (options.options) {
164
+ element.options = options.options.map((opt) => ({
165
+ text: { type: "plain_text", text: opt.text },
166
+ value: opt.value
167
+ }));
168
+ }
169
+ return {
170
+ type: "input",
171
+ block_id: options.blockId,
172
+ label: { type: "plain_text", text: options.label },
173
+ element
174
+ };
175
+ }
176
+ function slackTextInput(options) {
177
+ return {
178
+ type: "input",
179
+ block_id: options.blockId,
180
+ optional: options.optional ?? false,
181
+ label: { type: "plain_text", text: options.label },
182
+ element: {
183
+ type: "plain_text_input",
184
+ action_id: options.actionId,
185
+ ...options.placeholder ? { placeholder: { type: "plain_text", text: options.placeholder } } : {},
186
+ ...options.multiline ? { multiline: true } : {}
187
+ }
188
+ };
189
+ }
190
+ function slackDivider() {
191
+ return { type: "divider" };
192
+ }
193
+ function slackContext(text) {
194
+ return {
195
+ type: "context",
196
+ elements: [{ type: "mrkdwn", text }]
197
+ };
198
+ }
138
199
  function slackMessage(options) {
139
200
  const blocks = [slackSection(`*${options.title}*`)];
140
201
  if (options.details && Object.keys(options.details).length > 0) {
@@ -188,8 +249,13 @@ exports.formatRelativeTime = formatRelativeTime;
188
249
  exports.infoBoxHtml = infoBoxHtml;
189
250
  exports.noteBoxHtml = noteBoxHtml;
190
251
  exports.slackActions = slackActions;
252
+ exports.slackContext = slackContext;
253
+ exports.slackDivider = slackDivider;
191
254
  exports.slackFields = slackFields;
192
255
  exports.slackMessage = slackMessage;
256
+ exports.slackModal = slackModal;
193
257
  exports.slackSection = slackSection;
258
+ exports.slackStaticSelect = slackStaticSelect;
259
+ exports.slackTextInput = slackTextInput;
194
260
  //# sourceMappingURL=server.js.map
195
261
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/email.ts","../src/lib/slack.ts","../src/lib/formatting.ts","../src/lib/utils.ts"],"names":["differenceInHours","differenceInMinutes","formatDistanceToNow","twMerge","clsx"],"mappings":"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","file":"server.js","sourcesContent":["/**\n * Email Template Helpers\n *\n * Shared branded email template and HTML building blocks.\n * Used by both platform and admin apps via their local `lib/email.ts` re-exports.\n */\n\n/**\n * Wrap email body content in the standard Dilipod branded template.\n *\n * Usage:\n * emailTemplate({\n * body: '<h2>Hello</h2><p>Content here</p>',\n * })\n *\n * Or with a preheader (hidden preview text in email clients):\n * emailTemplate({\n * preheader: 'Quick summary shown in inbox',\n * body: '...',\n * })\n */\nexport function emailTemplate({ body, preheader }: { body: string; preheader?: string }): string {\n const preheaderHtml = preheader\n ? `<div style=\"display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;\">${preheader}</div>`\n : ''\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"color-scheme\" content=\"light\" />\n <meta name=\"supported-color-schemes\" content=\"light\" />\n <title>Dilipod</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f0f0f0; -webkit-font-smoothing: antialiased;\">\n ${preheaderHtml}\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color: #f0f0f0;\">\n <tr>\n <td align=\"center\" style=\"padding: 40px 16px;\">\n <!-- Main card -->\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width: 560px;\">\n <!-- Logo header -->\n <tr>\n <td style=\"padding: 0 0 24px 0;\">\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td style=\"width: 32px; height: 32px; background-color: #00e5cc; border-radius: 6px; text-align: center; vertical-align: middle; font-size: 16px; font-weight: 700; color: #111111; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\">\n D\n </td>\n <td style=\"padding-left: 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 17px; font-weight: 600; color: #111111; letter-spacing: -0.2px;\">\n Dilipod\n </td>\n </tr>\n </table>\n </td>\n </tr>\n <!-- Content card -->\n <tr>\n <td>\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color: #ffffff; border-radius: 6px; overflow: hidden; border: 1px solid #e2e2e2;\">\n <!-- Accent bar -->\n <tr>\n <td style=\"height: 3px; background: linear-gradient(90deg, #00e5cc 0%, #00c8b5 100%); font-size: 0; line-height: 0;\">&nbsp;</td>\n </tr>\n <!-- Body content -->\n <tr>\n <td style=\"padding: 36px 40px 40px 40px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; line-height: 1.7; color: #374151;\">\n ${body}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n <!-- Footer -->\n <tr>\n <td style=\"padding: 24px 4px 0 4px;\">\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;\">\n &copy; ${new Date().getFullYear()} Dilipod &mdash; Your Digital Workforce\n </td>\n <td align=\"right\" style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px;\">\n <a href=\"https://dilipod.com\" style=\"color: #9ca3af; text-decoration: none;\">dilipod.com</a>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n</body>\n</html>`\n}\n\n/**\n * Render a CTA button for use inside emailTemplate body.\n *\n * buttonHtml({ text: 'Reset Password', href: 'https://...' })\n * buttonHtml({ text: 'View Incident', href: '...', variant: 'danger' })\n */\nexport function buttonHtml({\n text,\n href,\n variant = 'primary',\n}: {\n text: string\n href: string\n variant?: 'primary' | 'danger'\n}): string {\n const bg = variant === 'danger' ? '#dc2626' : '#111111'\n const color = '#ffffff'\n return `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin-top: 24px;\">\n <tr>\n <td style=\"background-color: ${bg}; border-radius: 6px;\">\n <a href=\"${href}\" style=\"display: inline-block; padding: 12px 32px; color: ${color}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 600; text-decoration: none;\">\n ${text} &rarr;\n </a>\n </td>\n </tr>\n</table>`\n}\n\n/**\n * Render an info box (grey background) for key details.\n *\n * infoBoxHtml('<strong>Status:</strong> Deployed')\n */\nexport function infoBoxHtml(innerHtml: string): string {\n return `<div style=\"background-color: #f8fafb; padding: 16px 20px; border-radius: 6px; border: 1px solid #e5e7eb; margin: 20px 0; border-left: 3px solid #00e5cc;\">\n ${innerHtml}\n</div>`\n}\n\n/**\n * Render a warning/note box (yellow background).\n *\n * noteBoxHtml('This link expires in 1 hour.')\n */\nexport function noteBoxHtml(text: string): string {\n return `<div style=\"background-color: #fffbeb; padding: 14px 18px; border-radius: 6px; border: 1px solid #fde68a; margin: 20px 0; font-size: 13px; color: #92400e; border-left: 3px solid #f59e0b;\">\n ${text}\n</div>`\n}\n","/**\n * Slack Block Kit Helpers\n *\n * Shared Slack message and block builders used by both platform and admin apps.\n * Pure functions with zero dependencies — same pattern as email.ts.\n *\n * Transport layers (sendSlackMessage, sendSlackWebhook) stay local in each app.\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport interface SlackBlock {\n type: string\n text?: { type: string; text: string }\n elements?: SlackElement[]\n fields?: SlackField[]\n}\n\nexport interface SlackElement {\n type: string\n text?: { type: string; text: string }\n style?: string\n value?: string\n url?: string\n action_id?: string\n}\n\nexport interface SlackField {\n type: string\n text: string\n}\n\nexport interface SlackMessage {\n text: string\n blocks: SlackBlock[]\n}\n\n// ============================================\n// Block Builders\n// ============================================\n\n/**\n * Build a single mrkdwn section block.\n *\n * slackSection('*Hello* world')\n */\nexport function slackSection(mrkdwn: string): SlackBlock {\n return {\n type: 'section',\n text: { type: 'mrkdwn', text: mrkdwn },\n }\n}\n\n/**\n * Build a key-value fields block.\n *\n * slackFields({ Process: 'Invoice Bot', Status: 'Live' })\n */\nexport function slackFields(fields: Record<string, string | number>): SlackBlock {\n return {\n type: 'section',\n fields: Object.entries(fields).map(([key, value]) => ({\n type: 'mrkdwn',\n text: `*${key}:*\\n${value}`,\n })),\n }\n}\n\n/**\n * Build an actions block with buttons.\n *\n * slackActions([\n * { text: 'View', url: 'https://...' },\n * { text: 'Approve', actionId: 'approve', style: 'primary', value: '123' },\n * ])\n */\nexport function slackActions(\n buttons: {\n text: string\n url?: string\n value?: string\n actionId?: string\n style?: 'primary' | 'danger'\n }[]\n): SlackBlock {\n return {\n type: 'actions',\n elements: buttons.map((btn) => ({\n type: 'button',\n text: { type: 'plain_text', text: btn.text },\n ...(btn.url ? { url: btn.url } : {}),\n ...(btn.value ? { value: btn.value } : {}),\n ...(btn.actionId ? { action_id: btn.actionId } : {}),\n ...(btn.style ? { style: btn.style } : {}),\n })),\n }\n}\n\n// ============================================\n// Message Builder\n// ============================================\n\n/**\n * Build a complete Slack message with optional details, note, and button.\n * Covers 90% of notification use cases.\n *\n * slackMessage({\n * title: '🚀 New signup',\n * details: { Name: 'John', Email: 'john@acme.com' },\n * note: 'First user from this company',\n * buttonText: 'View Customer',\n * buttonUrl: 'https://admin.dilipod.com/customers/123',\n * })\n */\nexport function slackMessage(options: {\n title: string\n details?: Record<string, string | number>\n note?: string\n buttonText?: string\n buttonUrl?: string\n}): SlackMessage {\n const blocks: SlackBlock[] = [slackSection(`*${options.title}*`)]\n\n if (options.details && Object.keys(options.details).length > 0) {\n blocks.push(slackFields(options.details))\n }\n\n if (options.note) {\n blocks.push(slackSection(options.note))\n }\n\n if (options.buttonUrl && options.buttonText) {\n blocks.push(slackActions([{ text: options.buttonText, url: options.buttonUrl }]))\n }\n\n return {\n text: options.title,\n blocks,\n }\n}\n","/**\n * Formatting Utilities\n *\n * Shared text, currency, duration, and date formatting used across apps.\n */\n\nimport { differenceInHours, differenceInMinutes, formatDistanceToNow } from 'date-fns'\n\n/**\n * Convert a cent value to euros (numeric).\n * e.g. 2900 → 29\n */\nexport function formatCentsToEuros(cents: number): string {\n return `€${(cents / 100).toLocaleString()}`\n}\n\n/**\n * Format a euro value as a display string.\n * e.g. 299 → \"€299\" or 299.5 with decimals=2 → \"€299.50\"\n */\nexport function formatEuros(euros: number, decimals?: number): string {\n if (decimals !== undefined) {\n return `€${euros.toFixed(decimals)}`\n }\n return `€${euros.toLocaleString()}`\n}\n\n/**\n * Format milliseconds as a human-readable duration.\n * e.g. 1500 → \"1.5s\"\n */\nexport function formatDuration(ms: number): string {\n return `${(ms / 1000).toFixed(1)}s`\n}\n\n/**\n * Format a date into a compact relative-time string.\n * Returns \"—\" for null/undefined, \"5m\" for < 1 hour, \"2h\" for < 48 hours,\n * or a relative distance like \"3 days\" for older dates.\n */\nexport function formatRelativeTime(date: Date | string | null): string {\n if (!date) return '—'\n const d = typeof date === 'string' ? new Date(date) : date\n const hours = differenceInHours(new Date(), d)\n if (hours < 1) {\n const mins = differenceInMinutes(new Date(), d)\n return `${mins}m`\n }\n if (hours < 48) return `${hours}h`\n return formatDistanceToNow(d, { addSuffix: false })\n}\n","import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n"]}
1
+ {"version":3,"sources":["../src/lib/email.ts","../src/lib/slack.ts","../src/lib/formatting.ts","../src/lib/utils.ts"],"names":["differenceInHours","differenceInMinutes","formatDistanceToNow","twMerge","clsx"],"mappings":"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","file":"server.js","sourcesContent":["/**\n * Email Template Helpers\n *\n * Shared branded email template and HTML building blocks.\n * Used by both platform and admin apps via their local `lib/email.ts` re-exports.\n */\n\n/**\n * Wrap email body content in the standard Dilipod branded template.\n *\n * Usage:\n * emailTemplate({\n * body: '<h2>Hello</h2><p>Content here</p>',\n * })\n *\n * Or with a preheader (hidden preview text in email clients):\n * emailTemplate({\n * preheader: 'Quick summary shown in inbox',\n * body: '...',\n * })\n */\nexport function emailTemplate({ body, preheader }: { body: string; preheader?: string }): string {\n const preheaderHtml = preheader\n ? `<div style=\"display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;\">${preheader}</div>`\n : ''\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"color-scheme\" content=\"light\" />\n <meta name=\"supported-color-schemes\" content=\"light\" />\n <title>Dilipod</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f0f0f0; -webkit-font-smoothing: antialiased;\">\n ${preheaderHtml}\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color: #f0f0f0;\">\n <tr>\n <td align=\"center\" style=\"padding: 40px 16px;\">\n <!-- Main card -->\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width: 560px;\">\n <!-- Logo header -->\n <tr>\n <td style=\"padding: 0 0 24px 0;\">\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td style=\"width: 32px; height: 32px; background-color: #00e5cc; border-radius: 6px; text-align: center; vertical-align: middle; font-size: 16px; font-weight: 700; color: #111111; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\">\n D\n </td>\n <td style=\"padding-left: 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 17px; font-weight: 600; color: #111111; letter-spacing: -0.2px;\">\n Dilipod\n </td>\n </tr>\n </table>\n </td>\n </tr>\n <!-- Content card -->\n <tr>\n <td>\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color: #ffffff; border-radius: 6px; overflow: hidden; border: 1px solid #e2e2e2;\">\n <!-- Accent bar -->\n <tr>\n <td style=\"height: 3px; background: linear-gradient(90deg, #00e5cc 0%, #00c8b5 100%); font-size: 0; line-height: 0;\">&nbsp;</td>\n </tr>\n <!-- Body content -->\n <tr>\n <td style=\"padding: 36px 40px 40px 40px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; line-height: 1.7; color: #374151;\">\n ${body}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n <!-- Footer -->\n <tr>\n <td style=\"padding: 24px 4px 0 4px;\">\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;\">\n &copy; ${new Date().getFullYear()} Dilipod &mdash; Your Digital Workforce\n </td>\n <td align=\"right\" style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px;\">\n <a href=\"https://dilipod.com\" style=\"color: #9ca3af; text-decoration: none;\">dilipod.com</a>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n</body>\n</html>`\n}\n\n/**\n * Render a CTA button for use inside emailTemplate body.\n *\n * buttonHtml({ text: 'Reset Password', href: 'https://...' })\n * buttonHtml({ text: 'View Incident', href: '...', variant: 'danger' })\n */\nexport function buttonHtml({\n text,\n href,\n variant = 'primary',\n}: {\n text: string\n href: string\n variant?: 'primary' | 'danger'\n}): string {\n const bg = variant === 'danger' ? '#dc2626' : '#111111'\n const color = '#ffffff'\n return `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin-top: 24px;\">\n <tr>\n <td style=\"background-color: ${bg}; border-radius: 6px;\">\n <a href=\"${href}\" style=\"display: inline-block; padding: 12px 32px; color: ${color}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 600; text-decoration: none;\">\n ${text} &rarr;\n </a>\n </td>\n </tr>\n</table>`\n}\n\n/**\n * Render an info box (grey background) for key details.\n *\n * infoBoxHtml('<strong>Status:</strong> Deployed')\n */\nexport function infoBoxHtml(innerHtml: string): string {\n return `<div style=\"background-color: #f8fafb; padding: 16px 20px; border-radius: 6px; border: 1px solid #e5e7eb; margin: 20px 0; border-left: 3px solid #00e5cc;\">\n ${innerHtml}\n</div>`\n}\n\n/**\n * Render a warning/note box (yellow background).\n *\n * noteBoxHtml('This link expires in 1 hour.')\n */\nexport function noteBoxHtml(text: string): string {\n return `<div style=\"background-color: #fffbeb; padding: 14px 18px; border-radius: 6px; border: 1px solid #fde68a; margin: 20px 0; font-size: 13px; color: #92400e; border-left: 3px solid #f59e0b;\">\n ${text}\n</div>`\n}\n","/**\n * Slack Block Kit Helpers\n *\n * Shared Slack message and block builders used by both platform and admin apps.\n * Pure functions with zero dependencies — same pattern as email.ts.\n *\n * Transport layers (sendSlackMessage, sendSlackWebhook) stay local in each app.\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport interface SlackBlock {\n type: string\n text?: { type: string; text: string }\n elements?: SlackElement[]\n fields?: SlackField[]\n}\n\nexport interface SlackElement {\n type: string\n text?: { type: string; text: string }\n style?: string\n value?: string\n url?: string\n action_id?: string\n}\n\nexport interface SlackField {\n type: string\n text: string\n}\n\nexport interface SlackMessage {\n text: string\n blocks: SlackBlock[]\n}\n\n// ============================================\n// Block Builders\n// ============================================\n\n/**\n * Build a single mrkdwn section block.\n *\n * slackSection('*Hello* world')\n */\nexport function slackSection(mrkdwn: string): SlackBlock {\n return {\n type: 'section',\n text: { type: 'mrkdwn', text: mrkdwn },\n }\n}\n\n/**\n * Build a key-value fields block.\n *\n * slackFields({ Process: 'Invoice Bot', Status: 'Live' })\n */\nexport function slackFields(fields: Record<string, string | number>): SlackBlock {\n return {\n type: 'section',\n fields: Object.entries(fields).map(([key, value]) => ({\n type: 'mrkdwn',\n text: `*${key}:*\\n${value}`,\n })),\n }\n}\n\n/**\n * Build an actions block with buttons.\n *\n * slackActions([\n * { text: 'View', url: 'https://...' },\n * { text: 'Approve', actionId: 'approve', style: 'primary', value: '123' },\n * ])\n */\nexport function slackActions(\n buttons: {\n text: string\n url?: string\n value?: string\n actionId?: string\n style?: 'primary' | 'danger'\n }[]\n): SlackBlock {\n return {\n type: 'actions',\n elements: buttons.map((btn) => ({\n type: 'button',\n text: { type: 'plain_text', text: btn.text },\n ...(btn.url ? { url: btn.url } : {}),\n ...(btn.value ? { value: btn.value } : {}),\n ...(btn.actionId ? { action_id: btn.actionId } : {}),\n ...(btn.style ? { style: btn.style } : {}),\n })),\n }\n}\n\n// ============================================\n// Modal & Select Builders\n// ============================================\n\nexport interface SlackModalView {\n type: 'modal'\n callback_id: string\n title: { type: 'plain_text'; text: string }\n submit?: { type: 'plain_text'; text: string }\n close?: { type: 'plain_text'; text: string }\n blocks: SlackBlock[]\n private_metadata?: string\n}\n\nexport interface SlackOptionGroup {\n label: { type: 'plain_text'; text: string }\n options: Array<{\n text: { type: 'plain_text'; text: string }\n value: string\n }>\n}\n\n/**\n * Build a modal view for Slack.\n *\n * slackModal({\n * callbackId: 'run_process',\n * title: 'Run a Process',\n * submitText: 'Run',\n * blocks: [...],\n * })\n */\nexport function slackModal(options: {\n callbackId: string\n title: string\n submitText?: string\n closeText?: string\n blocks: SlackBlock[]\n privateMetadata?: string\n}): SlackModalView {\n return {\n type: 'modal',\n callback_id: options.callbackId,\n title: { type: 'plain_text', text: options.title },\n ...(options.submitText\n ? { submit: { type: 'plain_text', text: options.submitText } }\n : {}),\n ...(options.closeText\n ? { close: { type: 'plain_text', text: options.closeText } }\n : {}),\n blocks: options.blocks,\n ...(options.privateMetadata\n ? { private_metadata: options.privateMetadata }\n : {}),\n }\n}\n\n/**\n * Build a static select input block (for modals or messages).\n *\n * slackStaticSelect({\n * blockId: 'process_select',\n * actionId: 'process_id',\n * label: 'Process',\n * placeholder: 'Select a process',\n * optionGroups: [\n * { label: 'Invoice Bot', options: [{ text: 'Send Invoices', value: 'uuid' }] },\n * ],\n * })\n */\nexport function slackStaticSelect(options: {\n blockId: string\n actionId: string\n label: string\n placeholder?: string\n options?: Array<{ text: string; value: string }>\n optionGroups?: Array<{ label: string; options: Array<{ text: string; value: string }> }>\n}): SlackBlock {\n const element: Record<string, unknown> = {\n type: 'static_select',\n action_id: options.actionId,\n placeholder: { type: 'plain_text', text: options.placeholder || 'Select...' },\n }\n\n if (options.optionGroups) {\n element.option_groups = options.optionGroups.map((group) => ({\n label: { type: 'plain_text', text: group.label },\n options: group.options.map((opt) => ({\n text: { type: 'plain_text', text: opt.text },\n value: opt.value,\n })),\n }))\n } else if (options.options) {\n element.options = options.options.map((opt) => ({\n text: { type: 'plain_text', text: opt.text },\n value: opt.value,\n }))\n }\n\n return {\n type: 'input',\n block_id: options.blockId,\n label: { type: 'plain_text', text: options.label },\n element,\n } as unknown as SlackBlock\n}\n\n/**\n * Build a plain text input block (for modals).\n */\nexport function slackTextInput(options: {\n blockId: string\n actionId: string\n label: string\n placeholder?: string\n optional?: boolean\n multiline?: boolean\n}): SlackBlock {\n return {\n type: 'input',\n block_id: options.blockId,\n optional: options.optional ?? false,\n label: { type: 'plain_text', text: options.label },\n element: {\n type: 'plain_text_input',\n action_id: options.actionId,\n ...(options.placeholder\n ? { placeholder: { type: 'plain_text', text: options.placeholder } }\n : {}),\n ...(options.multiline ? { multiline: true } : {}),\n },\n } as unknown as SlackBlock\n}\n\n/**\n * Build a divider block.\n */\nexport function slackDivider(): SlackBlock {\n return { type: 'divider' } as SlackBlock\n}\n\n/**\n * Build a context block (small muted text).\n */\nexport function slackContext(text: string): SlackBlock {\n return {\n type: 'context',\n elements: [{ type: 'mrkdwn', text }],\n } as unknown as SlackBlock\n}\n\n// ============================================\n// Message Builder\n// ============================================\n\n/**\n * Build a complete Slack message with optional details, note, and button.\n * Covers 90% of notification use cases.\n *\n * slackMessage({\n * title: '🚀 New signup',\n * details: { Name: 'John', Email: 'john@acme.com' },\n * note: 'First user from this company',\n * buttonText: 'View Customer',\n * buttonUrl: 'https://admin.dilipod.com/customers/123',\n * })\n */\nexport function slackMessage(options: {\n title: string\n details?: Record<string, string | number>\n note?: string\n buttonText?: string\n buttonUrl?: string\n}): SlackMessage {\n const blocks: SlackBlock[] = [slackSection(`*${options.title}*`)]\n\n if (options.details && Object.keys(options.details).length > 0) {\n blocks.push(slackFields(options.details))\n }\n\n if (options.note) {\n blocks.push(slackSection(options.note))\n }\n\n if (options.buttonUrl && options.buttonText) {\n blocks.push(slackActions([{ text: options.buttonText, url: options.buttonUrl }]))\n }\n\n return {\n text: options.title,\n blocks,\n }\n}\n","/**\n * Formatting Utilities\n *\n * Shared text, currency, duration, and date formatting used across apps.\n */\n\nimport { differenceInHours, differenceInMinutes, formatDistanceToNow } from 'date-fns'\n\n/**\n * Convert a cent value to euros (numeric).\n * e.g. 2900 → 29\n */\nexport function formatCentsToEuros(cents: number): string {\n return `€${(cents / 100).toLocaleString()}`\n}\n\n/**\n * Format a euro value as a display string.\n * e.g. 299 → \"€299\" or 299.5 with decimals=2 → \"€299.50\"\n */\nexport function formatEuros(euros: number, decimals?: number): string {\n if (decimals !== undefined) {\n return `€${euros.toFixed(decimals)}`\n }\n return `€${euros.toLocaleString()}`\n}\n\n/**\n * Format milliseconds as a human-readable duration.\n * e.g. 1500 → \"1.5s\"\n */\nexport function formatDuration(ms: number): string {\n return `${(ms / 1000).toFixed(1)}s`\n}\n\n/**\n * Format a date into a compact relative-time string.\n * Returns \"—\" for null/undefined, \"5m\" for < 1 hour, \"2h\" for < 48 hours,\n * or a relative distance like \"3 days\" for older dates.\n */\nexport function formatRelativeTime(date: Date | string | null): string {\n if (!date) return '—'\n const d = typeof date === 'string' ? new Date(date) : date\n const hours = differenceInHours(new Date(), d)\n if (hours < 1) {\n const mins = differenceInMinutes(new Date(), d)\n return `${mins}m`\n }\n if (hours < 48) return `${hours}h`\n return formatDistanceToNow(d, { addSuffix: false })\n}\n","import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n"]}
package/dist/server.mjs CHANGED
@@ -133,6 +133,67 @@ function slackActions(buttons) {
133
133
  }))
134
134
  };
135
135
  }
136
+ function slackModal(options) {
137
+ return {
138
+ type: "modal",
139
+ callback_id: options.callbackId,
140
+ title: { type: "plain_text", text: options.title },
141
+ ...options.submitText ? { submit: { type: "plain_text", text: options.submitText } } : {},
142
+ ...options.closeText ? { close: { type: "plain_text", text: options.closeText } } : {},
143
+ blocks: options.blocks,
144
+ ...options.privateMetadata ? { private_metadata: options.privateMetadata } : {}
145
+ };
146
+ }
147
+ function slackStaticSelect(options) {
148
+ const element = {
149
+ type: "static_select",
150
+ action_id: options.actionId,
151
+ placeholder: { type: "plain_text", text: options.placeholder || "Select..." }
152
+ };
153
+ if (options.optionGroups) {
154
+ element.option_groups = options.optionGroups.map((group) => ({
155
+ label: { type: "plain_text", text: group.label },
156
+ options: group.options.map((opt) => ({
157
+ text: { type: "plain_text", text: opt.text },
158
+ value: opt.value
159
+ }))
160
+ }));
161
+ } else if (options.options) {
162
+ element.options = options.options.map((opt) => ({
163
+ text: { type: "plain_text", text: opt.text },
164
+ value: opt.value
165
+ }));
166
+ }
167
+ return {
168
+ type: "input",
169
+ block_id: options.blockId,
170
+ label: { type: "plain_text", text: options.label },
171
+ element
172
+ };
173
+ }
174
+ function slackTextInput(options) {
175
+ return {
176
+ type: "input",
177
+ block_id: options.blockId,
178
+ optional: options.optional ?? false,
179
+ label: { type: "plain_text", text: options.label },
180
+ element: {
181
+ type: "plain_text_input",
182
+ action_id: options.actionId,
183
+ ...options.placeholder ? { placeholder: { type: "plain_text", text: options.placeholder } } : {},
184
+ ...options.multiline ? { multiline: true } : {}
185
+ }
186
+ };
187
+ }
188
+ function slackDivider() {
189
+ return { type: "divider" };
190
+ }
191
+ function slackContext(text) {
192
+ return {
193
+ type: "context",
194
+ elements: [{ type: "mrkdwn", text }]
195
+ };
196
+ }
136
197
  function slackMessage(options) {
137
198
  const blocks = [slackSection(`*${options.title}*`)];
138
199
  if (options.details && Object.keys(options.details).length > 0) {
@@ -176,6 +237,6 @@ function cn(...inputs) {
176
237
  return twMerge(clsx(inputs));
177
238
  }
178
239
 
179
- export { buttonHtml, cn, emailTemplate, formatCentsToEuros, formatDuration, formatEuros, formatRelativeTime, infoBoxHtml, noteBoxHtml, slackActions, slackFields, slackMessage, slackSection };
240
+ export { buttonHtml, cn, emailTemplate, formatCentsToEuros, formatDuration, formatEuros, formatRelativeTime, infoBoxHtml, noteBoxHtml, slackActions, slackContext, slackDivider, slackFields, slackMessage, slackModal, slackSection, slackStaticSelect, slackTextInput };
180
241
  //# sourceMappingURL=server.mjs.map
181
242
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/email.ts","../src/lib/slack.ts","../src/lib/formatting.ts","../src/lib/utils.ts"],"names":[],"mappings":"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","file":"server.mjs","sourcesContent":["/**\n * Email Template Helpers\n *\n * Shared branded email template and HTML building blocks.\n * Used by both platform and admin apps via their local `lib/email.ts` re-exports.\n */\n\n/**\n * Wrap email body content in the standard Dilipod branded template.\n *\n * Usage:\n * emailTemplate({\n * body: '<h2>Hello</h2><p>Content here</p>',\n * })\n *\n * Or with a preheader (hidden preview text in email clients):\n * emailTemplate({\n * preheader: 'Quick summary shown in inbox',\n * body: '...',\n * })\n */\nexport function emailTemplate({ body, preheader }: { body: string; preheader?: string }): string {\n const preheaderHtml = preheader\n ? `<div style=\"display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;\">${preheader}</div>`\n : ''\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"color-scheme\" content=\"light\" />\n <meta name=\"supported-color-schemes\" content=\"light\" />\n <title>Dilipod</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f0f0f0; -webkit-font-smoothing: antialiased;\">\n ${preheaderHtml}\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color: #f0f0f0;\">\n <tr>\n <td align=\"center\" style=\"padding: 40px 16px;\">\n <!-- Main card -->\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width: 560px;\">\n <!-- Logo header -->\n <tr>\n <td style=\"padding: 0 0 24px 0;\">\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td style=\"width: 32px; height: 32px; background-color: #00e5cc; border-radius: 6px; text-align: center; vertical-align: middle; font-size: 16px; font-weight: 700; color: #111111; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\">\n D\n </td>\n <td style=\"padding-left: 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 17px; font-weight: 600; color: #111111; letter-spacing: -0.2px;\">\n Dilipod\n </td>\n </tr>\n </table>\n </td>\n </tr>\n <!-- Content card -->\n <tr>\n <td>\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color: #ffffff; border-radius: 6px; overflow: hidden; border: 1px solid #e2e2e2;\">\n <!-- Accent bar -->\n <tr>\n <td style=\"height: 3px; background: linear-gradient(90deg, #00e5cc 0%, #00c8b5 100%); font-size: 0; line-height: 0;\">&nbsp;</td>\n </tr>\n <!-- Body content -->\n <tr>\n <td style=\"padding: 36px 40px 40px 40px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; line-height: 1.7; color: #374151;\">\n ${body}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n <!-- Footer -->\n <tr>\n <td style=\"padding: 24px 4px 0 4px;\">\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;\">\n &copy; ${new Date().getFullYear()} Dilipod &mdash; Your Digital Workforce\n </td>\n <td align=\"right\" style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px;\">\n <a href=\"https://dilipod.com\" style=\"color: #9ca3af; text-decoration: none;\">dilipod.com</a>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n</body>\n</html>`\n}\n\n/**\n * Render a CTA button for use inside emailTemplate body.\n *\n * buttonHtml({ text: 'Reset Password', href: 'https://...' })\n * buttonHtml({ text: 'View Incident', href: '...', variant: 'danger' })\n */\nexport function buttonHtml({\n text,\n href,\n variant = 'primary',\n}: {\n text: string\n href: string\n variant?: 'primary' | 'danger'\n}): string {\n const bg = variant === 'danger' ? '#dc2626' : '#111111'\n const color = '#ffffff'\n return `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin-top: 24px;\">\n <tr>\n <td style=\"background-color: ${bg}; border-radius: 6px;\">\n <a href=\"${href}\" style=\"display: inline-block; padding: 12px 32px; color: ${color}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 600; text-decoration: none;\">\n ${text} &rarr;\n </a>\n </td>\n </tr>\n</table>`\n}\n\n/**\n * Render an info box (grey background) for key details.\n *\n * infoBoxHtml('<strong>Status:</strong> Deployed')\n */\nexport function infoBoxHtml(innerHtml: string): string {\n return `<div style=\"background-color: #f8fafb; padding: 16px 20px; border-radius: 6px; border: 1px solid #e5e7eb; margin: 20px 0; border-left: 3px solid #00e5cc;\">\n ${innerHtml}\n</div>`\n}\n\n/**\n * Render a warning/note box (yellow background).\n *\n * noteBoxHtml('This link expires in 1 hour.')\n */\nexport function noteBoxHtml(text: string): string {\n return `<div style=\"background-color: #fffbeb; padding: 14px 18px; border-radius: 6px; border: 1px solid #fde68a; margin: 20px 0; font-size: 13px; color: #92400e; border-left: 3px solid #f59e0b;\">\n ${text}\n</div>`\n}\n","/**\n * Slack Block Kit Helpers\n *\n * Shared Slack message and block builders used by both platform and admin apps.\n * Pure functions with zero dependencies — same pattern as email.ts.\n *\n * Transport layers (sendSlackMessage, sendSlackWebhook) stay local in each app.\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport interface SlackBlock {\n type: string\n text?: { type: string; text: string }\n elements?: SlackElement[]\n fields?: SlackField[]\n}\n\nexport interface SlackElement {\n type: string\n text?: { type: string; text: string }\n style?: string\n value?: string\n url?: string\n action_id?: string\n}\n\nexport interface SlackField {\n type: string\n text: string\n}\n\nexport interface SlackMessage {\n text: string\n blocks: SlackBlock[]\n}\n\n// ============================================\n// Block Builders\n// ============================================\n\n/**\n * Build a single mrkdwn section block.\n *\n * slackSection('*Hello* world')\n */\nexport function slackSection(mrkdwn: string): SlackBlock {\n return {\n type: 'section',\n text: { type: 'mrkdwn', text: mrkdwn },\n }\n}\n\n/**\n * Build a key-value fields block.\n *\n * slackFields({ Process: 'Invoice Bot', Status: 'Live' })\n */\nexport function slackFields(fields: Record<string, string | number>): SlackBlock {\n return {\n type: 'section',\n fields: Object.entries(fields).map(([key, value]) => ({\n type: 'mrkdwn',\n text: `*${key}:*\\n${value}`,\n })),\n }\n}\n\n/**\n * Build an actions block with buttons.\n *\n * slackActions([\n * { text: 'View', url: 'https://...' },\n * { text: 'Approve', actionId: 'approve', style: 'primary', value: '123' },\n * ])\n */\nexport function slackActions(\n buttons: {\n text: string\n url?: string\n value?: string\n actionId?: string\n style?: 'primary' | 'danger'\n }[]\n): SlackBlock {\n return {\n type: 'actions',\n elements: buttons.map((btn) => ({\n type: 'button',\n text: { type: 'plain_text', text: btn.text },\n ...(btn.url ? { url: btn.url } : {}),\n ...(btn.value ? { value: btn.value } : {}),\n ...(btn.actionId ? { action_id: btn.actionId } : {}),\n ...(btn.style ? { style: btn.style } : {}),\n })),\n }\n}\n\n// ============================================\n// Message Builder\n// ============================================\n\n/**\n * Build a complete Slack message with optional details, note, and button.\n * Covers 90% of notification use cases.\n *\n * slackMessage({\n * title: '🚀 New signup',\n * details: { Name: 'John', Email: 'john@acme.com' },\n * note: 'First user from this company',\n * buttonText: 'View Customer',\n * buttonUrl: 'https://admin.dilipod.com/customers/123',\n * })\n */\nexport function slackMessage(options: {\n title: string\n details?: Record<string, string | number>\n note?: string\n buttonText?: string\n buttonUrl?: string\n}): SlackMessage {\n const blocks: SlackBlock[] = [slackSection(`*${options.title}*`)]\n\n if (options.details && Object.keys(options.details).length > 0) {\n blocks.push(slackFields(options.details))\n }\n\n if (options.note) {\n blocks.push(slackSection(options.note))\n }\n\n if (options.buttonUrl && options.buttonText) {\n blocks.push(slackActions([{ text: options.buttonText, url: options.buttonUrl }]))\n }\n\n return {\n text: options.title,\n blocks,\n }\n}\n","/**\n * Formatting Utilities\n *\n * Shared text, currency, duration, and date formatting used across apps.\n */\n\nimport { differenceInHours, differenceInMinutes, formatDistanceToNow } from 'date-fns'\n\n/**\n * Convert a cent value to euros (numeric).\n * e.g. 2900 → 29\n */\nexport function formatCentsToEuros(cents: number): string {\n return `€${(cents / 100).toLocaleString()}`\n}\n\n/**\n * Format a euro value as a display string.\n * e.g. 299 → \"€299\" or 299.5 with decimals=2 → \"€299.50\"\n */\nexport function formatEuros(euros: number, decimals?: number): string {\n if (decimals !== undefined) {\n return `€${euros.toFixed(decimals)}`\n }\n return `€${euros.toLocaleString()}`\n}\n\n/**\n * Format milliseconds as a human-readable duration.\n * e.g. 1500 → \"1.5s\"\n */\nexport function formatDuration(ms: number): string {\n return `${(ms / 1000).toFixed(1)}s`\n}\n\n/**\n * Format a date into a compact relative-time string.\n * Returns \"—\" for null/undefined, \"5m\" for < 1 hour, \"2h\" for < 48 hours,\n * or a relative distance like \"3 days\" for older dates.\n */\nexport function formatRelativeTime(date: Date | string | null): string {\n if (!date) return '—'\n const d = typeof date === 'string' ? new Date(date) : date\n const hours = differenceInHours(new Date(), d)\n if (hours < 1) {\n const mins = differenceInMinutes(new Date(), d)\n return `${mins}m`\n }\n if (hours < 48) return `${hours}h`\n return formatDistanceToNow(d, { addSuffix: false })\n}\n","import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n"]}
1
+ {"version":3,"sources":["../src/lib/email.ts","../src/lib/slack.ts","../src/lib/formatting.ts","../src/lib/utils.ts"],"names":[],"mappings":"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","file":"server.mjs","sourcesContent":["/**\n * Email Template Helpers\n *\n * Shared branded email template and HTML building blocks.\n * Used by both platform and admin apps via their local `lib/email.ts` re-exports.\n */\n\n/**\n * Wrap email body content in the standard Dilipod branded template.\n *\n * Usage:\n * emailTemplate({\n * body: '<h2>Hello</h2><p>Content here</p>',\n * })\n *\n * Or with a preheader (hidden preview text in email clients):\n * emailTemplate({\n * preheader: 'Quick summary shown in inbox',\n * body: '...',\n * })\n */\nexport function emailTemplate({ body, preheader }: { body: string; preheader?: string }): string {\n const preheaderHtml = preheader\n ? `<div style=\"display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;\">${preheader}</div>`\n : ''\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"color-scheme\" content=\"light\" />\n <meta name=\"supported-color-schemes\" content=\"light\" />\n <title>Dilipod</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f0f0f0; -webkit-font-smoothing: antialiased;\">\n ${preheaderHtml}\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color: #f0f0f0;\">\n <tr>\n <td align=\"center\" style=\"padding: 40px 16px;\">\n <!-- Main card -->\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width: 560px;\">\n <!-- Logo header -->\n <tr>\n <td style=\"padding: 0 0 24px 0;\">\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td style=\"width: 32px; height: 32px; background-color: #00e5cc; border-radius: 6px; text-align: center; vertical-align: middle; font-size: 16px; font-weight: 700; color: #111111; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\">\n D\n </td>\n <td style=\"padding-left: 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 17px; font-weight: 600; color: #111111; letter-spacing: -0.2px;\">\n Dilipod\n </td>\n </tr>\n </table>\n </td>\n </tr>\n <!-- Content card -->\n <tr>\n <td>\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color: #ffffff; border-radius: 6px; overflow: hidden; border: 1px solid #e2e2e2;\">\n <!-- Accent bar -->\n <tr>\n <td style=\"height: 3px; background: linear-gradient(90deg, #00e5cc 0%, #00c8b5 100%); font-size: 0; line-height: 0;\">&nbsp;</td>\n </tr>\n <!-- Body content -->\n <tr>\n <td style=\"padding: 36px 40px 40px 40px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; line-height: 1.7; color: #374151;\">\n ${body}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n <!-- Footer -->\n <tr>\n <td style=\"padding: 24px 4px 0 4px;\">\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;\">\n &copy; ${new Date().getFullYear()} Dilipod &mdash; Your Digital Workforce\n </td>\n <td align=\"right\" style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px;\">\n <a href=\"https://dilipod.com\" style=\"color: #9ca3af; text-decoration: none;\">dilipod.com</a>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n</body>\n</html>`\n}\n\n/**\n * Render a CTA button for use inside emailTemplate body.\n *\n * buttonHtml({ text: 'Reset Password', href: 'https://...' })\n * buttonHtml({ text: 'View Incident', href: '...', variant: 'danger' })\n */\nexport function buttonHtml({\n text,\n href,\n variant = 'primary',\n}: {\n text: string\n href: string\n variant?: 'primary' | 'danger'\n}): string {\n const bg = variant === 'danger' ? '#dc2626' : '#111111'\n const color = '#ffffff'\n return `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin-top: 24px;\">\n <tr>\n <td style=\"background-color: ${bg}; border-radius: 6px;\">\n <a href=\"${href}\" style=\"display: inline-block; padding: 12px 32px; color: ${color}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 600; text-decoration: none;\">\n ${text} &rarr;\n </a>\n </td>\n </tr>\n</table>`\n}\n\n/**\n * Render an info box (grey background) for key details.\n *\n * infoBoxHtml('<strong>Status:</strong> Deployed')\n */\nexport function infoBoxHtml(innerHtml: string): string {\n return `<div style=\"background-color: #f8fafb; padding: 16px 20px; border-radius: 6px; border: 1px solid #e5e7eb; margin: 20px 0; border-left: 3px solid #00e5cc;\">\n ${innerHtml}\n</div>`\n}\n\n/**\n * Render a warning/note box (yellow background).\n *\n * noteBoxHtml('This link expires in 1 hour.')\n */\nexport function noteBoxHtml(text: string): string {\n return `<div style=\"background-color: #fffbeb; padding: 14px 18px; border-radius: 6px; border: 1px solid #fde68a; margin: 20px 0; font-size: 13px; color: #92400e; border-left: 3px solid #f59e0b;\">\n ${text}\n</div>`\n}\n","/**\n * Slack Block Kit Helpers\n *\n * Shared Slack message and block builders used by both platform and admin apps.\n * Pure functions with zero dependencies — same pattern as email.ts.\n *\n * Transport layers (sendSlackMessage, sendSlackWebhook) stay local in each app.\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport interface SlackBlock {\n type: string\n text?: { type: string; text: string }\n elements?: SlackElement[]\n fields?: SlackField[]\n}\n\nexport interface SlackElement {\n type: string\n text?: { type: string; text: string }\n style?: string\n value?: string\n url?: string\n action_id?: string\n}\n\nexport interface SlackField {\n type: string\n text: string\n}\n\nexport interface SlackMessage {\n text: string\n blocks: SlackBlock[]\n}\n\n// ============================================\n// Block Builders\n// ============================================\n\n/**\n * Build a single mrkdwn section block.\n *\n * slackSection('*Hello* world')\n */\nexport function slackSection(mrkdwn: string): SlackBlock {\n return {\n type: 'section',\n text: { type: 'mrkdwn', text: mrkdwn },\n }\n}\n\n/**\n * Build a key-value fields block.\n *\n * slackFields({ Process: 'Invoice Bot', Status: 'Live' })\n */\nexport function slackFields(fields: Record<string, string | number>): SlackBlock {\n return {\n type: 'section',\n fields: Object.entries(fields).map(([key, value]) => ({\n type: 'mrkdwn',\n text: `*${key}:*\\n${value}`,\n })),\n }\n}\n\n/**\n * Build an actions block with buttons.\n *\n * slackActions([\n * { text: 'View', url: 'https://...' },\n * { text: 'Approve', actionId: 'approve', style: 'primary', value: '123' },\n * ])\n */\nexport function slackActions(\n buttons: {\n text: string\n url?: string\n value?: string\n actionId?: string\n style?: 'primary' | 'danger'\n }[]\n): SlackBlock {\n return {\n type: 'actions',\n elements: buttons.map((btn) => ({\n type: 'button',\n text: { type: 'plain_text', text: btn.text },\n ...(btn.url ? { url: btn.url } : {}),\n ...(btn.value ? { value: btn.value } : {}),\n ...(btn.actionId ? { action_id: btn.actionId } : {}),\n ...(btn.style ? { style: btn.style } : {}),\n })),\n }\n}\n\n// ============================================\n// Modal & Select Builders\n// ============================================\n\nexport interface SlackModalView {\n type: 'modal'\n callback_id: string\n title: { type: 'plain_text'; text: string }\n submit?: { type: 'plain_text'; text: string }\n close?: { type: 'plain_text'; text: string }\n blocks: SlackBlock[]\n private_metadata?: string\n}\n\nexport interface SlackOptionGroup {\n label: { type: 'plain_text'; text: string }\n options: Array<{\n text: { type: 'plain_text'; text: string }\n value: string\n }>\n}\n\n/**\n * Build a modal view for Slack.\n *\n * slackModal({\n * callbackId: 'run_process',\n * title: 'Run a Process',\n * submitText: 'Run',\n * blocks: [...],\n * })\n */\nexport function slackModal(options: {\n callbackId: string\n title: string\n submitText?: string\n closeText?: string\n blocks: SlackBlock[]\n privateMetadata?: string\n}): SlackModalView {\n return {\n type: 'modal',\n callback_id: options.callbackId,\n title: { type: 'plain_text', text: options.title },\n ...(options.submitText\n ? { submit: { type: 'plain_text', text: options.submitText } }\n : {}),\n ...(options.closeText\n ? { close: { type: 'plain_text', text: options.closeText } }\n : {}),\n blocks: options.blocks,\n ...(options.privateMetadata\n ? { private_metadata: options.privateMetadata }\n : {}),\n }\n}\n\n/**\n * Build a static select input block (for modals or messages).\n *\n * slackStaticSelect({\n * blockId: 'process_select',\n * actionId: 'process_id',\n * label: 'Process',\n * placeholder: 'Select a process',\n * optionGroups: [\n * { label: 'Invoice Bot', options: [{ text: 'Send Invoices', value: 'uuid' }] },\n * ],\n * })\n */\nexport function slackStaticSelect(options: {\n blockId: string\n actionId: string\n label: string\n placeholder?: string\n options?: Array<{ text: string; value: string }>\n optionGroups?: Array<{ label: string; options: Array<{ text: string; value: string }> }>\n}): SlackBlock {\n const element: Record<string, unknown> = {\n type: 'static_select',\n action_id: options.actionId,\n placeholder: { type: 'plain_text', text: options.placeholder || 'Select...' },\n }\n\n if (options.optionGroups) {\n element.option_groups = options.optionGroups.map((group) => ({\n label: { type: 'plain_text', text: group.label },\n options: group.options.map((opt) => ({\n text: { type: 'plain_text', text: opt.text },\n value: opt.value,\n })),\n }))\n } else if (options.options) {\n element.options = options.options.map((opt) => ({\n text: { type: 'plain_text', text: opt.text },\n value: opt.value,\n }))\n }\n\n return {\n type: 'input',\n block_id: options.blockId,\n label: { type: 'plain_text', text: options.label },\n element,\n } as unknown as SlackBlock\n}\n\n/**\n * Build a plain text input block (for modals).\n */\nexport function slackTextInput(options: {\n blockId: string\n actionId: string\n label: string\n placeholder?: string\n optional?: boolean\n multiline?: boolean\n}): SlackBlock {\n return {\n type: 'input',\n block_id: options.blockId,\n optional: options.optional ?? false,\n label: { type: 'plain_text', text: options.label },\n element: {\n type: 'plain_text_input',\n action_id: options.actionId,\n ...(options.placeholder\n ? { placeholder: { type: 'plain_text', text: options.placeholder } }\n : {}),\n ...(options.multiline ? { multiline: true } : {}),\n },\n } as unknown as SlackBlock\n}\n\n/**\n * Build a divider block.\n */\nexport function slackDivider(): SlackBlock {\n return { type: 'divider' } as SlackBlock\n}\n\n/**\n * Build a context block (small muted text).\n */\nexport function slackContext(text: string): SlackBlock {\n return {\n type: 'context',\n elements: [{ type: 'mrkdwn', text }],\n } as unknown as SlackBlock\n}\n\n// ============================================\n// Message Builder\n// ============================================\n\n/**\n * Build a complete Slack message with optional details, note, and button.\n * Covers 90% of notification use cases.\n *\n * slackMessage({\n * title: '🚀 New signup',\n * details: { Name: 'John', Email: 'john@acme.com' },\n * note: 'First user from this company',\n * buttonText: 'View Customer',\n * buttonUrl: 'https://admin.dilipod.com/customers/123',\n * })\n */\nexport function slackMessage(options: {\n title: string\n details?: Record<string, string | number>\n note?: string\n buttonText?: string\n buttonUrl?: string\n}): SlackMessage {\n const blocks: SlackBlock[] = [slackSection(`*${options.title}*`)]\n\n if (options.details && Object.keys(options.details).length > 0) {\n blocks.push(slackFields(options.details))\n }\n\n if (options.note) {\n blocks.push(slackSection(options.note))\n }\n\n if (options.buttonUrl && options.buttonText) {\n blocks.push(slackActions([{ text: options.buttonText, url: options.buttonUrl }]))\n }\n\n return {\n text: options.title,\n blocks,\n }\n}\n","/**\n * Formatting Utilities\n *\n * Shared text, currency, duration, and date formatting used across apps.\n */\n\nimport { differenceInHours, differenceInMinutes, formatDistanceToNow } from 'date-fns'\n\n/**\n * Convert a cent value to euros (numeric).\n * e.g. 2900 → 29\n */\nexport function formatCentsToEuros(cents: number): string {\n return `€${(cents / 100).toLocaleString()}`\n}\n\n/**\n * Format a euro value as a display string.\n * e.g. 299 → \"€299\" or 299.5 with decimals=2 → \"€299.50\"\n */\nexport function formatEuros(euros: number, decimals?: number): string {\n if (decimals !== undefined) {\n return `€${euros.toFixed(decimals)}`\n }\n return `€${euros.toLocaleString()}`\n}\n\n/**\n * Format milliseconds as a human-readable duration.\n * e.g. 1500 → \"1.5s\"\n */\nexport function formatDuration(ms: number): string {\n return `${(ms / 1000).toFixed(1)}s`\n}\n\n/**\n * Format a date into a compact relative-time string.\n * Returns \"—\" for null/undefined, \"5m\" for < 1 hour, \"2h\" for < 48 hours,\n * or a relative distance like \"3 days\" for older dates.\n */\nexport function formatRelativeTime(date: Date | string | null): string {\n if (!date) return '—'\n const d = typeof date === 'string' ? new Date(date) : date\n const hours = differenceInHours(new Date(), d)\n if (hours < 1) {\n const mins = differenceInMinutes(new Date(), d)\n return `${mins}m`\n }\n if (hours < 48) return `${hours}h`\n return formatDistanceToNow(d, { addSuffix: false })\n}\n","import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dilipod/ui",
3
- "version": "0.4.33",
3
+ "version": "0.4.35",
4
4
  "description": "Dilipod Design System - Shared UI components and styles",
5
5
  "author": "Dilipod <hello@dilipod.com>",
6
6
  "license": "MIT",
@@ -22,6 +22,7 @@ export interface UploadedFile {
22
22
  type: string
23
23
  size: number
24
24
  url?: string
25
+ processing?: boolean
25
26
  }
26
27
 
27
28
  export interface FilePreviewProps {
@@ -157,11 +158,12 @@ export function FilePreview({
157
158
  const FileIcon = getFileIcon(file.type)
158
159
  const typeLabel = getTypeLabel(file.type)
159
160
  const sizeLabel = formatSize(file.size)
160
- const isPreviewable = canPreview(file)
161
+ const isProcessing = file.processing === true
162
+ const isPreviewable = !isProcessing && canPreview(file)
161
163
  const previewType = getPreviewType(file)
162
-
164
+
163
165
  return (
164
- <div
166
+ <div
165
167
  key={i}
166
168
  className={`flex items-center justify-between p-3 rounded-md bg-gray-50 transition-colors ${
167
169
  isPreviewable ? 'hover:bg-gray-100 cursor-pointer' : ''
@@ -171,29 +173,41 @@ export function FilePreview({
171
173
  <div className="flex items-center gap-3 min-w-0">
172
174
  <div className="relative shrink-0">
173
175
  <div className="w-10 h-10 rounded-sm bg-white border border-gray-200 flex items-center justify-center">
174
- <FileIcon className="w-5 h-5 text-[var(--cyan)]" weight="fill" />
176
+ {isProcessing ? (
177
+ <CircleNotch className="w-5 h-5 text-[var(--cyan)] animate-spin" />
178
+ ) : (
179
+ <FileIcon className="w-5 h-5 text-[var(--cyan)]" weight="fill" />
180
+ )}
175
181
  </div>
176
- {previewType === 'video' && (
182
+ {!isProcessing && previewType === 'video' && (
177
183
  <Play className="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 text-[var(--cyan)] bg-white rounded-full" weight="fill" />
178
184
  )}
179
185
  </div>
180
186
  <div className="min-w-0">
181
187
  <p className="text-sm font-medium text-[var(--black)] truncate">{file.filename}</p>
182
188
  <p className="text-xs text-muted-foreground">
183
- {typeLabel} · {sizeLabel}
184
- {isPreviewable && (
185
- <span className="text-[var(--cyan)] ml-1">· Click to preview</span>
189
+ {isProcessing ? (
190
+ <span className="text-amber-600">Processing video — this takes about a minute</span>
191
+ ) : (
192
+ <>
193
+ {typeLabel} · {sizeLabel}
194
+ {isPreviewable && (
195
+ <span className="text-[var(--cyan)] ml-1">· Click to preview</span>
196
+ )}
197
+ </>
186
198
  )}
187
199
  </p>
188
200
  </div>
189
201
  </div>
190
- <button
191
- onClick={(e) => handleDownload(e, file)}
192
- className="p-2 rounded-sm hover:bg-gray-200 transition-colors shrink-0"
193
- title="Download"
194
- >
195
- <Download className="w-4 h-4 text-muted-foreground" />
196
- </button>
202
+ {!isProcessing && (
203
+ <button
204
+ onClick={(e) => handleDownload(e, file)}
205
+ className="p-2 rounded-sm hover:bg-gray-200 transition-colors shrink-0"
206
+ title="Download"
207
+ >
208
+ <Download className="w-4 h-4 text-muted-foreground" />
209
+ </button>
210
+ )}
197
211
  </div>
198
212
  )
199
213
  })}