@stacksjs/defaults 0.75.27 → 0.75.28

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.
@@ -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 406 built-in dashboard components. Covers the dashboard system at storage/framework/defaults/.
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 407 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, 406 components, and a multi-section layout.
11
+ The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 407 components, and a multi-section layout.
12
12
 
13
13
  ## Key Paths
14
14
  - Dashboard components: `storage/framework/defaults/resources/components/Dashboard/`
@@ -106,6 +106,42 @@ export default {
106
106
  }
107
107
  ```
108
108
 
109
+ ## The base design: email components
110
+
111
+ STX templates build on a bundled component library (`defaults/resources/components/Email`),
112
+ which every bundled template uses and which gives an app a finished look with a few props:
113
+
114
+ ```html
115
+ <EmailLayout
116
+ title="Your license key"
117
+ preheader="Shown after the subject in the inbox"
118
+ brand="Uplink" brandUrl="https://uplink.example" accent="#f59e0b"
119
+ footer="You are getting this because you bought Uplink."
120
+ >
121
+ <EmailText size="heading">You have Uplink Monthly.</EmailText>
122
+ <EmailText>Here is your license key.</EmailText>
123
+ <EmailCode>UPLK-7RKR-Z2BJ-V6QV-4PUH</EmailCode>
124
+ <EmailButton href="https://uplink.example/activate" bg="#f59e0b" color="#18181b">Activate</EmailButton>
125
+ <EmailDivider />
126
+ <EmailText size="sm">Small print.</EmailText>
127
+ </EmailLayout>
128
+ ```
129
+
130
+ - `<EmailLayout>`: a muted page with one bordered card (a border, not a shadow: Gmail strips
131
+ shadows), an optional brand header and footer, a hidden `preheader`, and `@media` rules for
132
+ dark mode and phones on the class hooks the other components carry. Clients that ignore
133
+ `@media` keep the inline light design.
134
+ - `<EmailText size="heading|lg|md|sm">`, `<EmailButton>`, `<EmailDivider>`, `<EmailSection>`,
135
+ `<EmailImage>`, `<EmailLink>`.
136
+ - `<EmailCode>`: a key or one-time code, large and monospace, selectable in one tap, wrapping at
137
+ hyphens on a phone.
138
+
139
+ A colour passed as a prop is the template's own choice: that element gets no dark-mode hook.
140
+
141
+ Two stx traps when writing a component or template: never put the literal text of a style
142
+ tag inside a `<script server>` comment (the parser takes it for one), and write head CSS as a
143
+ raw string (`{!! css !!}`) rather than a style element, which stx lifts out as component CSS.
144
+
109
145
  ## Template Locations
110
146
  - STX templates: `.stx` files processed by STX engine
111
147
  - HTML templates: `.html` files with `{{ }}` variable interpolation
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.27",
5
+ "version": "0.75.28",
6
6
  "description": "A modern Stacks development environment.",
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.27",
5
+ "version": "0.75.28",
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.27",
58
- "@stacksjs/mobile": "^0.75.27",
58
+ "@stacksjs/mobile": "^0.75.28",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }
@@ -23,8 +23,8 @@ const padY = props.padY || '14px'
23
23
  </script>
24
24
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;border-collapse:separate;">
25
25
  <tr>
26
- <td align="center" style="border-radius:6px;background-color:{{ bg }};mso-padding-alt:0;">
27
- <a href="{{ href }}" style="display:inline-block;padding:{{ padY }} {{ padX }};color:{{ color }};text-decoration:none;border-radius:6px;font-weight:600;font-size:16px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;text-align:center;mso-padding-alt:{{ padY }} {{ padX }};">
26
+ <td align="center" style="border-radius:8px;background-color:{{ bg }};mso-padding-alt:0;">
27
+ <a href="{{ href }}" style="display:inline-block;padding:{{ padY }} {{ padX }};color:{{ color }};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;text-align:center;mso-padding-alt:{{ padY }} {{ padX }};">
28
28
  <slot />
29
29
  </a>
30
30
  </td>
@@ -0,0 +1,28 @@
1
+ <script server>
2
+ // A code the reader copies: a license key, a one-time passcode, a recovery
3
+ // code. Large monospace on a tinted block, spaced so O and 0, l and 1 are
4
+ // told apart, and selectable as one unit where the client allows it
5
+ // (`user-select: all`), so a tap or double-click takes the whole code. On a
6
+ // narrow screen it wraps at a hyphen rather than mid-group, and only breaks
7
+ // inside a group when a code has no hyphens to wrap at.
8
+ //
9
+ // <EmailCode>UPLK-7RKR-Z2BJ-V6QV-4PUH</EmailCode>
10
+ // <EmailCode size="32px" align="center">482 913</EmailCode>
11
+ interface EmailCodeProps {
12
+ size?: string
13
+ align?: 'left' | 'center' | 'right'
14
+ spacing?: string
15
+ }
16
+
17
+ const props = defineProps<EmailCodeProps>()
18
+ const size = props.size || '22px'
19
+ const align = props.align || 'center'
20
+ const spacing = props.spacing || '24px'
21
+ </script>
22
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:separate;margin:0 0 {{ spacing }} 0;">
23
+ <tr>
24
+ <td class="email-code" align="{{ align }}" style="padding:18px 20px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:10px;color:#18181b;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace;font-size:{{ size }};font-weight:600;letter-spacing:0.08em;line-height:1.4;text-align:{{ align }};-webkit-user-select:all;user-select:all;overflow-wrap:anywhere;">
25
+ <slot />
26
+ </td>
27
+ </tr>
28
+ </table>
@@ -10,11 +10,11 @@ interface EmailDividerProps {
10
10
  }
11
11
 
12
12
  const props = defineProps<EmailDividerProps>()
13
- const color = props.color || '#e5e7eb'
13
+ const color = props.color || '#e4e4e7'
14
14
  const spacing = props.spacing || '24px'
15
15
  </script>
16
16
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;margin:{{ spacing }} 0;">
17
17
  <tr>
18
- <td style="height:1px;font-size:1px;line-height:1px;background-color:{{ color }};">&nbsp;</td>
18
+ <td class="email-divider" style="height:1px;font-size:1px;line-height:1px;background-color:{{ color }};">&nbsp;</td>
19
19
  </tr>
20
20
  </table>
@@ -3,10 +3,25 @@
3
3
  //
4
4
  // Wraps content in the table-based body that Outlook + Gmail (Android)
5
5
  // require, plus the standard meta block (charset / viewport /
6
- // X-UA-Compatible / colour-scheme). Two-tone bodyBg / contentBg
7
- // mirrors the bundled templates: muted page colour outside, white
8
- // card inside. Named slots: head / header / footer. See README for
9
- // usage examples.
6
+ // X-UA-Compatible / colour-scheme). Named slots: head / header / footer.
7
+ //
8
+ // The base design, which every bundled template inherits:
9
+ // - a muted page with one white card, a hairline border and a 12px radius.
10
+ // A border rather than a shadow: Gmail strips box-shadow, so a shadow-only
11
+ // card lost its edge in the most-used client.
12
+ // - `preheader`: the line an inbox shows after the subject. Without it the
13
+ // inbox shows whatever text comes first - usually the brand name again.
14
+ // - `brand` / `accent` / `brandUrl`: a small brand header (an accent dot and
15
+ // the name) when the template does not fill the `header` slot itself.
16
+ // - `footer`: a muted, centred line under the card when the template does
17
+ // not fill the `footer` slot.
18
+ // - dark mode and a tighter phone layout, as `@media` rules on the class
19
+ // hooks below. The CSS inliner leaves `@media` rules in the head
20
+ // (stacksjs/stacks#1902), so clients that honour them (Apple Mail, iOS,
21
+ // Outlook for Mac) get them and the rest keep the inline light design.
22
+ // - those rules are written out raw from here, not as a style element in
23
+ // the template: stx treats a component's own style element as component
24
+ // CSS and lifts it out of the head, which lost them.
10
25
  //
11
26
  // Notes on `defineProps()`: stx fills missing props with an empty
12
27
  // string (not `undefined`), so destructure defaults silently no-op
@@ -17,6 +32,11 @@ interface EmailLayoutProps {
17
32
  width?: number | string
18
33
  bodyBg?: string
19
34
  contentBg?: string
35
+ preheader?: string
36
+ brand?: string
37
+ brandUrl?: string
38
+ accent?: string
39
+ footer?: string
20
40
  }
21
41
 
22
42
  const props = defineProps<EmailLayoutProps>()
@@ -24,6 +44,32 @@ const title = props.title || ''
24
44
  const width = props.width || 600
25
45
  const bodyBg = props.bodyBg || '#f4f4f5'
26
46
  const contentBg = props.contentBg || '#ffffff'
47
+ const preheader = props.preheader || ''
48
+ const brand = props.brand || ''
49
+ const brandUrl = props.brandUrl || ''
50
+ const accent = props.accent || '#3451b2'
51
+ const footer = props.footer || ''
52
+ const fontStack = '-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,\'Helvetica Neue\',Arial,sans-serif'
53
+
54
+ const responsiveCss = [
55
+ '<style>',
56
+ '@media only screen and (max-width: 620px) {',
57
+ ' .email-outer { padding: 16px 8px !important; }',
58
+ ' .email-pad { padding: 28px 24px !important; }',
59
+ ' .email-brand { padding: 24px 24px 0 !important; }',
60
+ ' .email-code { font-size: 18px !important; letter-spacing: 0.04em !important; }',
61
+ '}',
62
+ '@media (prefers-color-scheme: dark) {',
63
+ ' .email-body, .email-outer { background-color: #0c0c0e !important; }',
64
+ ' .email-card { background-color: #141417 !important; border-color: #27272a !important; }',
65
+ ' .email-heading, .email-brand-name { color: #f4f4f5 !important; }',
66
+ ' .email-text { color: #d4d4d8 !important; }',
67
+ ' .email-muted { color: #a1a1aa !important; }',
68
+ ' .email-code { background-color: #1c1c20 !important; border-color: #27272a !important; color: #f4f4f5 !important; }',
69
+ ' .email-divider { background-color: #27272a !important; }',
70
+ '}',
71
+ '</style>',
72
+ ].join('\n')
27
73
  </script>
28
74
 
29
75
  <!DOCTYPE html>
@@ -35,19 +81,42 @@ const contentBg = props.contentBg || '#ffffff'
35
81
  <meta name="color-scheme" content="light dark">
36
82
  <meta name="supported-color-schemes" content="light dark">
37
83
  <title>{{ title }}</title>
84
+ {!! responsiveCss !!}
38
85
  <slot name="head" />
39
86
  </head>
40
- <body style="margin:0;padding:0;background-color:{{ bodyBg }};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;line-height:1.6;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
87
+ <body class="email-body" style="margin:0;padding:0;background-color:{{ bodyBg }};font-family:{{ fontStack }};line-height:1.6;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
88
+ @if(preheader)
89
+ <div style="display:none;max-height:0;max-width:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:{{ bodyBg }};">{{ preheader }}&#8199;&#847;&#8199;&#847;&#8199;&#847;&#8199;&#847;&#8199;&#847;&#8199;&#847;&#8199;&#847;&#8199;&#847;</div>
90
+ @endif
41
91
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;background-color:{{ bodyBg }};">
42
92
  <tr>
43
- <td align="center" style="padding:40px 20px;">
44
- <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="max-width:{{ width }}px;width:100%;background-color:{{ contentBg }};border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.1);">
93
+ <td class="email-outer" align="center" style="padding:40px 20px;background-color:{{ bodyBg }};">
94
+ <table class="email-card" role="presentation" cellpadding="0" cellspacing="0" border="0" style="max-width:{{ width }}px;width:100%;background-color:{{ contentBg }};border:1px solid #e4e4e7;border-radius:12px;overflow:hidden;border-collapse:separate;">
95
+ @if(brand)
96
+ <tr>
97
+ <td class="email-brand" style="padding:32px 40px 0;">
98
+ <span style="display:inline-block;width:10px;height:10px;border-radius:5px;background-color:{{ accent }};vertical-align:middle;margin-right:10px;"></span>
99
+ @if(brandUrl)
100
+ <a class="email-brand-name" href="{{ brandUrl }}" style="color:#18181b;text-decoration:none;font-size:15px;font-weight:600;letter-spacing:-0.01em;vertical-align:middle;font-family:{{ fontStack }};">{{ brand }}</a>
101
+ @else
102
+ <span class="email-brand-name" style="color:#18181b;font-size:15px;font-weight:600;letter-spacing:-0.01em;vertical-align:middle;font-family:{{ fontStack }};">{{ brand }}</span>
103
+ @endif
104
+ </td>
105
+ </tr>
106
+ @endif
45
107
  <slot name="header" />
46
- <tr><td style="padding:40px;">
108
+ <tr><td class="email-pad" style="padding:32px 40px 40px;">
47
109
  <slot />
48
110
  </td></tr>
49
111
  <slot name="footer" />
50
112
  </table>
113
+ @if(footer)
114
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="max-width:{{ width }}px;width:100%;border-collapse:collapse;">
115
+ <tr>
116
+ <td class="email-muted" style="padding:20px 24px 0;text-align:center;color:#71717a;font-size:12px;line-height:1.6;font-family:{{ fontStack }};">{{ footer }}</td>
117
+ </tr>
118
+ </table>
119
+ @endif
51
120
  </td>
52
121
  </tr>
53
122
  </table>
@@ -14,17 +14,20 @@ interface EmailTextProps {
14
14
  const props = defineProps<EmailTextProps>()
15
15
  const align = props.align || 'left'
16
16
  const size = props.size || 'md'
17
- const color = props.color || (size === 'heading' ? '#111827' : '#4b5563')
17
+ const color = props.color || (size === 'heading' ? '#18181b' : size === 'sm' ? '#71717a' : '#3f3f46')
18
+ // Hooks for the dark-mode rules in <EmailLayout>. A colour passed as a prop is
19
+ // the template's own choice, so it gets no hook and dark mode leaves it alone.
20
+ const role = props.color ? '' : size === 'heading' ? 'email-heading' : size === 'sm' ? 'email-muted' : 'email-text'
18
21
  const spacing = props.spacing || '16px'
19
22
 
20
23
  const sizeStyles: Record<string, string> = {
21
24
  sm: 'font-size:14px;line-height:1.5;',
22
25
  md: 'font-size:16px;line-height:1.6;',
23
26
  lg: 'font-size:18px;line-height:1.6;',
24
- heading: 'font-size:20px;line-height:1.4;font-weight:600;',
27
+ heading: 'font-size:22px;line-height:1.3;font-weight:600;letter-spacing:-0.01em;',
25
28
  }
26
29
  const sizeStyle = sizeStyles[size] || sizeStyles.md
27
30
  </script>
28
- <p style="margin:0 0 {{ spacing }} 0;text-align:{{ align }};color:{{ color }};{{ sizeStyle }}">
31
+ <p class="{{ role }}" style="margin:0 0 {{ spacing }} 0;text-align:{{ align }};color:{{ color }};{{ sizeStyle }}">
29
32
  <slot />
30
33
  </p>