@stacksjs/defaults 0.75.136 → 0.75.138
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/ai/skills/stacks-dashboard/SKILL.md +2 -2
- package/ai/skills/stacks-native/CATALOG.md +4 -4
- package/ai/skills/stacks-notifications/SKILL.md +38 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/NativeBarButton.stx +169 -0
- package/resources/components/NativeComponents.test.ts +13 -0
- package/resources/emails/notification.stx +11 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-dashboard
|
|
3
|
-
description: Use when building or customizing the Stacks admin dashboard, including dashboard pages, model management views, analytics widgets, commerce dashboards, content management, settings panels, deployment monitoring, job/queue management, or the
|
|
3
|
+
description: Use when building or customizing the Stacks admin dashboard, including dashboard pages, model management views, analytics widgets, commerce dashboards, content management, settings panels, deployment monitoring, job/queue management, or the 414 built-in dashboard components. Covers the dashboard system at storage/framework/defaults/.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -8,7 +8,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
|
|
|
8
8
|
|
|
9
9
|
# Stacks Dashboard
|
|
10
10
|
|
|
11
|
-
The Stacks admin dashboard provides a full-featured admin panel with 100+ route views,
|
|
11
|
+
The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 414 components, and a multi-section layout.
|
|
12
12
|
|
|
13
13
|
## Key Paths
|
|
14
14
|
- Dashboard components: `storage/framework/defaults/resources/components/Dashboard/`
|
|
@@ -63,7 +63,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
|
|
|
63
63
|
| `@stacksjs/mobile` | [`stacks-mobile`](../stacks-mobile/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/mobile/src/index.ts) | 12 |
|
|
64
64
|
| `@stacksjs/model-meta` | [`stacks-models`](../stacks-models/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/model-meta/src/index.ts) | 1 |
|
|
65
65
|
| `@stacksjs/newsletter` | [`stacks-newsletter`](../stacks-newsletter/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/newsletter/src/index.ts) | 7 |
|
|
66
|
-
| `@stacksjs/notifications` | [`stacks-notifications`](../stacks-notifications/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts) |
|
|
66
|
+
| `@stacksjs/notifications` | [`stacks-notifications`](../stacks-notifications/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts) | 12 |
|
|
67
67
|
| `@stacksjs/objects` | [`stacks-objects`](../stacks-objects/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/objects/src/index.ts) | 4 |
|
|
68
68
|
| `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 60 |
|
|
69
69
|
| `@stacksjs/pagination` | [`stacks-pagination`](../stacks-pagination/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/pagination/src/index.ts) | 0 |
|
|
@@ -607,11 +607,11 @@ Evidence directory: [7 retained tests](https://github.com/stacksjs/stacks/blob/m
|
|
|
607
607
|
|
|
608
608
|
Guidance: [`stacks-notifications`](../stacks-notifications/SKILL.md). Source: [storage/framework/core/notifications/src/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts).
|
|
609
609
|
|
|
610
|
-
Declared runtime exports: `BroadcastNotificationDriver`, `DatabaseNotificationDriver`, `bulkSetPreferences`, `emailParagraphs`, `ensureSuccessfulEmailResult`, `ensureSuccessfulNotificationResults`, `filterChannelsByPreferences`, `getNotificationPreferences`, `makeDeliveryRecord`, `notification`, `notificationChannel`, `notificationEmailHtml`, `notificationEmailText`, `notify`, `recordNotificationDelivery`, `resolveDeliveryRecipient`, `setNotificationPreference`, `useBroadcast`, `useChat`, `useDatabase`, `useEmail`, `useNotification`, `usePush`, `useSMS`, `wherePreferenceCategory`.
|
|
610
|
+
Declared runtime exports: `BroadcastNotificationDriver`, `DEFAULT_SMS_OPT_OUT`, `DEFAULT_UNSUBSCRIBE_NOTICE`, `DatabaseNotificationDriver`, `bulkSetPreferences`, `emailParagraphs`, `ensureSuccessfulEmailResult`, `ensureSuccessfulNotificationResults`, `filterChannelsByPreferences`, `getNotificationPreferences`, `listUnsubscribeHeaders`, `makeDeliveryRecord`, `notification`, `notificationChannel`, `notificationEmailHtml`, `notificationEmailMessage`, `notificationEmailText`, `notificationSmsBody`, `notificationUnsubscribeLinks`, `notificationUnsubscribeToken`, `notify`, `parseNotificationUnsubscribeToken`, `recordNotificationDelivery`, `resolveDeliveryRecipient`, `setNotificationPreference`, `unsubscribeTokenFromRequest`, `unsubscribeWithToken`, `useBroadcast`, `useChat`, `useDatabase`, `useEmail`, `useNotification`, `usePush`, `useSMS`, `wherePreferenceCategory`.
|
|
611
611
|
|
|
612
|
-
Local source modules: [src/delivery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/delivery.ts), [src/drivers/broadcast.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/broadcast.ts), [src/drivers/database.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/database.ts), [src/drivers/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/index.ts), [src/email-body.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/email-body.ts), [src/preferences.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/preferences.ts).
|
|
612
|
+
Local source modules: [src/delivery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/delivery.ts), [src/drivers/broadcast.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/broadcast.ts), [src/drivers/database.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/database.ts), [src/drivers/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/drivers/index.ts), [src/email-body.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/email-body.ts), [src/preferences.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/preferences.ts), [src/unsubscribe.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/unsubscribe.ts).
|
|
613
613
|
|
|
614
|
-
Evidence directory: [
|
|
614
|
+
Evidence directory: [12 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
|
|
615
615
|
|
|
616
616
|
### @stacksjs/objects
|
|
617
617
|
|
|
@@ -78,6 +78,44 @@ missing optional schema compatibility from real persistence failures. Apply
|
|
|
78
78
|
model-driven migrations before relying on persistence. Inspect channel results
|
|
79
79
|
and transport results rather than assuming a delivery log is provider proof.
|
|
80
80
|
|
|
81
|
+
## Marketing: unsubscribe links and opt-out lines
|
|
82
|
+
|
|
83
|
+
Give a marketing notification `payload.unsubscribe`, built per person with
|
|
84
|
+
`notificationUnsubscribeLinks(userId, { notice })`:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
await notify(
|
|
88
|
+
{ userId, email },
|
|
89
|
+
{ subject, body, unsubscribe: notificationUnsubscribeLinks(userId, { notice: 'You get these because marketing email is on.' }) },
|
|
90
|
+
['email'],
|
|
91
|
+
{ category: 'marketing' },
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The email's footer then shows the notice, **Unsubscribe** (`url`, default
|
|
96
|
+
`{APP_URL}/unsubscribe?token=`) and **Email preferences** (`/settings`) instead
|
|
97
|
+
of "do not reply"; the text part spells the link out; and the message carries
|
|
98
|
+
`List-Unsubscribe: <oneClickUrl>` plus `List-Unsubscribe-Post:
|
|
99
|
+
List-Unsubscribe=One-Click` (RFC 8058, default
|
|
100
|
+
`{APP_URL}/api/notifications/unsubscribe?token=`). `payload.headers` adds
|
|
101
|
+
other headers. The pure builders are `notificationEmailMessage`,
|
|
102
|
+
`notificationEmailHtml`, `notificationEmailText` and `listUnsubscribeHeaders`.
|
|
103
|
+
|
|
104
|
+
The token (`notificationUnsubscribeToken`, `parseNotificationUnsubscribeToken`)
|
|
105
|
+
is `base64url("{userId}:{channel}:{category}:{hmac}")` signed with `APP_KEY`,
|
|
106
|
+
which must be set in production. It does not expire. It turns off one
|
|
107
|
+
preference (channel + category), unlike `@stacksjs/email`'s tokens, which
|
|
108
|
+
suppress an address for good. The app owns the page and the route; the route's
|
|
109
|
+
handler is `unsubscribeWithToken(await unsubscribeTokenFromRequest(request))`,
|
|
110
|
+
which reads the query string (one-click form POSTs) before a JSON body, and
|
|
111
|
+
answers 422 when it returns null. Ask before unsubscribing on the page, so a
|
|
112
|
+
link scanner opening the URL changes nothing.
|
|
113
|
+
|
|
114
|
+
A `marketing` text ends with "Reply STOP to opt out." (`notificationSmsBody`).
|
|
115
|
+
`payload.smsOptOut` replaces the line (and adds it to other categories), and
|
|
116
|
+
`false` leaves it out. STOP only becomes an opt-out the app sees when inbound
|
|
117
|
+
SMS is wired up; see `stacks-sms`.
|
|
118
|
+
|
|
81
119
|
## Channel factories
|
|
82
120
|
|
|
83
121
|
- `useEmail(driver?)` returns a sendable transport. Without a driver it uses
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.138",
|
|
6
6
|
"description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"funding": "https://github.com/sponsors/chrisbbreuer",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.138",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@iconify-json/f7": "^1.2.2",
|
|
57
57
|
"@iconify-json/hugeicons": "^1.2.35",
|
|
58
|
-
"@stacksjs/mobile": "0.75.
|
|
58
|
+
"@stacksjs/mobile": "0.75.138",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.422",
|
|
60
60
|
"@xterm/addon-fit": "^0.11.0",
|
|
61
61
|
"@xterm/xterm": "^6.0.0",
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
interface NativeBarButtonProps {
|
|
3
|
+
/** Where the button goes, such as `/m/messages`. */
|
|
4
|
+
href: string
|
|
5
|
+
/** What a screen reader calls the button. */
|
|
6
|
+
label: string
|
|
7
|
+
/** The SF Symbol the native bar draws for it (`bubble.left`), drawn here with its Iconify stand-in. */
|
|
8
|
+
symbol?: string
|
|
9
|
+
/** An Iconify class for a symbol the stand-ins below don't cover. */
|
|
10
|
+
icon?: string
|
|
11
|
+
/** A picture (the account's own photo), drawn round inside the button. */
|
|
12
|
+
image?: string
|
|
13
|
+
/** A short title instead of the symbol: an account's initials. */
|
|
14
|
+
title?: string
|
|
15
|
+
/** A count on the button. Zero and empty draw nothing. */
|
|
16
|
+
badge?: string | number
|
|
17
|
+
/** `count` draws the number, `dot` a red dot (unread notifications). */
|
|
18
|
+
badgeStyle?: 'count' | 'dot'
|
|
19
|
+
/** `accent` sits a shade stronger in dark mode (chat); `account` is the default. */
|
|
20
|
+
variant?: 'accent' | 'account'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { symbol = '', icon = '' } = defineProps<NativeBarButtonProps>()
|
|
24
|
+
|
|
25
|
+
// The same names the native bar takes (mobileHeaderOptions-style rightButtons),
|
|
26
|
+
// so one list of buttons describes both. Written out so the stylesheet
|
|
27
|
+
// generator sees every class.
|
|
28
|
+
const standIns: Record<string, string> = {
|
|
29
|
+
'bubble.left': 'i-lucide-message-square',
|
|
30
|
+
'bubble.left.and.bubble.right': 'i-lucide-messages-square',
|
|
31
|
+
'person.crop.circle': 'i-lucide-circle-user-round',
|
|
32
|
+
'bell': 'i-lucide-bell',
|
|
33
|
+
'gearshape': 'i-lucide-settings',
|
|
34
|
+
'plus': 'i-lucide-plus',
|
|
35
|
+
'magnifyingglass': 'i-lucide-search',
|
|
36
|
+
'square.and.arrow.up': 'i-lucide-share',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const glyphClass = `${icon || standIns[symbol] || 'i-lucide-circle-user-round'} native-bar-glyph`
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<script client>
|
|
43
|
+
import { haptics } from '@stacksjs/mobile'
|
|
44
|
+
|
|
45
|
+
// A round button at the top right of a phone screen, drawn exactly as the
|
|
46
|
+
// app's native bar draws its buttons (Craft's accountNavigationButton in
|
|
47
|
+
// CraftNativeScreen.swift): a 44pt circle tinted with the accent, a 20pt
|
|
48
|
+
// glyph, a 36pt photo inset in the circle, a count badge or a red dot at its
|
|
49
|
+
// top right. The native screens (Today, Calendar) and the web screens of the
|
|
50
|
+
// same app then show one set of buttons, not two that almost match.
|
|
51
|
+
//
|
|
52
|
+
// Every prop through useReactiveProp, under names of its own: read as plain
|
|
53
|
+
// {{ label }}, a host's client signal of the same name would be shown instead.
|
|
54
|
+
// What shows (picture, title or glyph) is written in the template itself: a
|
|
55
|
+
// derived only the template reads is dropped by the bundler's tree-shake.
|
|
56
|
+
const barHref = useReactiveProp<string>('href', '')
|
|
57
|
+
const barLabel = useReactiveProp<string>('label', '')
|
|
58
|
+
const barImage = useReactiveProp<string>('image', '')
|
|
59
|
+
const barTitle = useReactiveProp<string>('title', '')
|
|
60
|
+
const barBadge = useReactiveProp<string | number>('badge', '')
|
|
61
|
+
const barBadgeStyle = useReactiveProp<string>('badgeStyle', 'count')
|
|
62
|
+
const barVariant = useReactiveProp<string>('variant', 'account')
|
|
63
|
+
|
|
64
|
+
const barCount = derived(() => {
|
|
65
|
+
const value = barBadge()
|
|
66
|
+
return value === '' || value === 0 || value === '0' || value == null ? '' : Number(value) > 99 ? '99+' : String(value)
|
|
67
|
+
})
|
|
68
|
+
const barDot = derived(() => barBadgeStyle() === 'dot')
|
|
69
|
+
const barPhoto = derived(() => (barImage() ? `background-image: url("${String(barImage()).replace(/"/g, '%22')}")` : ''))
|
|
70
|
+
|
|
71
|
+
function tapBarButton(): void {
|
|
72
|
+
void haptics.selection()
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<a :href="barHref()" class="native-bar-button" :class="barVariant() === 'accent' ? 'native-bar-accent' : ''" draggable="false" data-stx-link data-stx-prefetch="eager" @click="tapBarButton()">
|
|
77
|
+
<span class="native-bar-label">{{ barLabel() }}<span :if="barCount()">, {{ barCount() }} unread</span></span>
|
|
78
|
+
<span :if="barImage()" aria-hidden="true" class="native-bar-photo" :style="barPhoto()"></span>
|
|
79
|
+
<span :if="!barImage() && barTitle()" aria-hidden="true" class="native-bar-title">{{ barTitle() }}</span>
|
|
80
|
+
<!-- :show, not :if: an :if branch is re-rendered in the browser, where the
|
|
81
|
+
server's glyphClass doesn't exist, and the glyph silently never drew. -->
|
|
82
|
+
<i :show="!barImage() && !barTitle()" aria-hidden="true" class="{{ glyphClass }}"></i>
|
|
83
|
+
<span :if="barCount() && !barDot()" aria-hidden="true" class="native-bar-count">{{ barCount() }}</span>
|
|
84
|
+
<span :if="barCount() && barDot()" aria-hidden="true" class="native-bar-dot"></span>
|
|
85
|
+
</a>
|
|
86
|
+
|
|
87
|
+
<style>
|
|
88
|
+
/* The values are Craft's (CraftNativeScreen.swift, accountNavigationButton):
|
|
89
|
+
keep them in step, or the web and native screens drift apart again. */
|
|
90
|
+
.native-bar-button {
|
|
91
|
+
position: relative;
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
flex: none;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
width: 2.75rem;
|
|
97
|
+
height: 2.75rem;
|
|
98
|
+
border-radius: 999px;
|
|
99
|
+
color: rgb(28 77 217);
|
|
100
|
+
background: rgb(38 99 235 / 0.08);
|
|
101
|
+
text-decoration: none;
|
|
102
|
+
-webkit-tap-highlight-color: transparent;
|
|
103
|
+
-webkit-touch-callout: none;
|
|
104
|
+
transition: transform 120ms ease, opacity 120ms ease;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.native-bar-button:active { transform: scale(0.94); opacity: 0.85; }
|
|
108
|
+
|
|
109
|
+
.native-bar-label {
|
|
110
|
+
position: absolute;
|
|
111
|
+
width: 1px;
|
|
112
|
+
height: 1px;
|
|
113
|
+
overflow: hidden;
|
|
114
|
+
clip: rect(0 0 0 0);
|
|
115
|
+
white-space: nowrap;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.native-bar-glyph { width: 1.25rem; height: 1.25rem; }
|
|
119
|
+
|
|
120
|
+
.native-bar-title {
|
|
121
|
+
font-size: 0.8125rem;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
letter-spacing: 0.01em;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.native-bar-photo {
|
|
127
|
+
width: 2.25rem;
|
|
128
|
+
height: 2.25rem;
|
|
129
|
+
border-radius: 999px;
|
|
130
|
+
background: center / cover no-repeat;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.native-bar-count {
|
|
134
|
+
position: absolute;
|
|
135
|
+
top: 0;
|
|
136
|
+
right: 0;
|
|
137
|
+
min-width: 1.125rem;
|
|
138
|
+
height: 1.125rem;
|
|
139
|
+
padding: 0 0.25rem;
|
|
140
|
+
border-radius: 999px;
|
|
141
|
+
color: white;
|
|
142
|
+
background: rgb(0 122 255);
|
|
143
|
+
font-size: 0.625rem;
|
|
144
|
+
font-weight: 700;
|
|
145
|
+
line-height: 1.125rem;
|
|
146
|
+
text-align: center;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.native-bar-dot {
|
|
150
|
+
position: absolute;
|
|
151
|
+
top: 0;
|
|
152
|
+
right: 0;
|
|
153
|
+
width: 0.5625rem;
|
|
154
|
+
height: 0.5625rem;
|
|
155
|
+
border-radius: 999px;
|
|
156
|
+
background: rgb(255 59 48);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.dark .native-bar-button {
|
|
160
|
+
color: rgb(148 196 252);
|
|
161
|
+
background: rgb(38 99 235 / 0.12);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.dark .native-bar-accent { background: rgb(38 99 235 / 0.24); }
|
|
165
|
+
|
|
166
|
+
@media (prefers-reduced-motion: reduce) {
|
|
167
|
+
.native-bar-button { transition: none; }
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
@@ -39,6 +39,19 @@ describe('Native components', () => {
|
|
|
39
39
|
expect(button).not.toContain('<img')
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
it('draws bar buttons as the native bar does, from the same SF Symbol names', () => {
|
|
43
|
+
const button = read('NativeBarButton')
|
|
44
|
+
// Craft's accountNavigationButton: a 44pt tinted circle, a 36pt photo inset.
|
|
45
|
+
expect(button).toContain('width: 2.75rem;')
|
|
46
|
+
expect(button).toContain('background: rgb(38 99 235 / 0.08);')
|
|
47
|
+
expect(button).toContain('width: 2.25rem;')
|
|
48
|
+
expect(button).toContain("'bubble.left': 'i-lucide-message-square'")
|
|
49
|
+
expect(button).toContain("useReactiveProp<string | number>('badge', '')")
|
|
50
|
+
expect(button).toContain('data-stx-link data-stx-prefetch="eager"')
|
|
51
|
+
// A photo is a background, not an <img>: apps check that every image goes through their delivery.
|
|
52
|
+
expect(button).not.toContain('<img')
|
|
53
|
+
})
|
|
54
|
+
|
|
42
55
|
it('lifts the launch splash once the first screen has painted', () => {
|
|
43
56
|
expect(read('NativeAppShell')).toContain('requestAnimationFrame(() => requestAnimationFrame(() => splash.hide()))')
|
|
44
57
|
})
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// breaks kept, addresses as links) and, when the notification names one, a
|
|
5
5
|
// button to where it happened. Same <EmailLayout> as the other prebaked
|
|
6
6
|
// mailers (stacksjs/stacks#1901); an app's own resources/emails/notification
|
|
7
|
-
// wins over this one.
|
|
7
|
+
// wins over this one. A notification with `unsubscribe` (marketing) says why
|
|
8
|
+
// it came and links to Unsubscribe in the footer instead of "do not reply".
|
|
8
9
|
const appName = props.appName || 'Stacks'
|
|
9
10
|
const primaryColor = props.primaryColor || '#3451b2'
|
|
10
11
|
const subject = props.subject || appName
|
|
@@ -12,6 +13,9 @@ const paragraphs = Array.isArray(props.paragraphs) ? props.paragraphs : []
|
|
|
12
13
|
const actionUrl = props.actionUrl || ''
|
|
13
14
|
const actionLabel = props.actionLabel || 'Open'
|
|
14
15
|
const preheader = props.preheader || ''
|
|
16
|
+
const unsubscribeUrl = props.unsubscribeUrl || ''
|
|
17
|
+
const unsubscribeNotice = props.unsubscribeNotice || 'You are receiving this because these emails are turned on in your settings.'
|
|
18
|
+
const preferencesUrl = props.preferencesUrl || ''
|
|
15
19
|
const year = props.year || new Date().getFullYear()
|
|
16
20
|
</script>
|
|
17
21
|
|
|
@@ -36,7 +40,12 @@ const year = props.year || new Date().getFullYear()
|
|
|
36
40
|
<template #footer>
|
|
37
41
|
<tr>
|
|
38
42
|
<td style="padding:20px 40px;background-color:#f9fafb;border-top:1px solid #e5e7eb;">
|
|
39
|
-
|
|
43
|
+
@if(unsubscribeUrl)
|
|
44
|
+
<p style="margin:0;color:#6b7280;font-size:12px;text-align:center;">{{ unsubscribeNotice }}</p>
|
|
45
|
+
<p style="margin:8px 0 0 0;color:#6b7280;font-size:12px;text-align:center;"><a href="{{ unsubscribeUrl }}" style="color:#6b7280;text-decoration:underline;">Unsubscribe</a>@if(preferencesUrl) · <a href="{{ preferencesUrl }}" style="color:#6b7280;text-decoration:underline;">Email preferences</a>@endif</p>
|
|
46
|
+
@else
|
|
47
|
+
<p style="margin:0;color:#6b7280;font-size:12px;text-align:center;">This is an automated message from {{ appName }}. Please do not reply to this email.</p>
|
|
48
|
+
@endif
|
|
40
49
|
<p style="margin:10px 0 0 0;color:#9ca3af;font-size:11px;text-align:center;">© {{ year }} {{ appName }}. All rights reserved.</p>
|
|
41
50
|
</td>
|
|
42
51
|
</tr>
|