@rip-lang/ui 0.4.2 → 0.4.4
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/browser/components/autocomplete.rip +1 -1
- package/browser/components/carousel.rip +1 -1
- package/browser/components/checkbox-group.rip +1 -1
- package/browser/components/combobox.rip +1 -1
- package/browser/components/context-menu.rip +1 -1
- package/browser/components/date-picker.rip +1 -1
- package/browser/components/grid.rip +1 -1
- package/browser/components/menu.rip +1 -1
- package/browser/components/menubar.rip +2 -2
- package/browser/components/multi-select.rip +1 -1
- package/browser/components/nav-menu.rip +3 -3
- package/browser/components/number-field.rip +2 -2
- package/browser/components/select.rip +21 -2
- package/browser/components/toggle-group.rip +1 -1
- package/email/README.md +15 -0
- package/email/components.rip +17 -6
- package/email/email.rip +5 -1
- package/email/example-1.html +19 -0
- package/email/example-1.rip +41 -0
- package/email/example-2.html +3 -0
- package/email/example-2.rip +68 -0
- package/package.json +3 -3
|
@@ -124,7 +124,7 @@ export Autocomplete = component
|
|
|
124
124
|
$open: open?!
|
|
125
125
|
style: "position:fixed;margin:0;inset:auto"
|
|
126
126
|
for item, idx in filteredItems
|
|
127
|
-
div role: "option",
|
|
127
|
+
div role: "option", tabIndex: -1
|
|
128
128
|
@click: (=> @selectIndex(idx))
|
|
129
129
|
@mouseenter: (=> @_hlIdx = idx; @_updateHighlight())
|
|
130
130
|
"#{if typeof item is 'string' then item else (item.label or item.name or String(item))}"
|
|
@@ -93,7 +93,7 @@ export Carousel = component
|
|
|
93
93
|
onMouseleave: -> @_startAutoplay() if @autoplay
|
|
94
94
|
|
|
95
95
|
render
|
|
96
|
-
div role: "region", aria-roledescription: "carousel", aria-label: @label,
|
|
96
|
+
div role: "region", aria-roledescription: "carousel", aria-label: @label, tabIndex: 0
|
|
97
97
|
$orientation: @orientation
|
|
98
98
|
@keydown: @onKeydown
|
|
99
99
|
@mouseenter: @onMouseenter
|
|
@@ -81,7 +81,7 @@ export CheckboxGroup = component
|
|
|
81
81
|
slot
|
|
82
82
|
|
|
83
83
|
for opt, idx in _options
|
|
84
|
-
button role: "checkbox",
|
|
84
|
+
button role: "checkbox", tabIndex: -1
|
|
85
85
|
aria-checked: opt.dataset.value in @value
|
|
86
86
|
$checked: (opt.dataset.value in @value)?!
|
|
87
87
|
$disabled: @disabled?!
|
|
@@ -140,7 +140,7 @@ export Combobox = component
|
|
|
140
140
|
style: "position:fixed;margin:0;inset:auto"
|
|
141
141
|
$open: open?!
|
|
142
142
|
for item, idx in @items
|
|
143
|
-
div role: "option",
|
|
143
|
+
div role: "option", tabIndex: -1, id: "#{_listId}-#{idx}"
|
|
144
144
|
$value: item
|
|
145
145
|
$highlighted: (idx is highlightedIndex)?!
|
|
146
146
|
@click: (=> @selectIndex(idx))
|
|
@@ -79,7 +79,7 @@ export ContextMenu = component
|
|
|
79
79
|
div ref: "_list", role: "menu", $open: true, @keydown: @_onKeydown
|
|
80
80
|
style: "position:fixed;left:#{posX}px;top:#{posY}px;z-index:50"
|
|
81
81
|
for item, idx in _menuItems
|
|
82
|
-
div role: "menuitem",
|
|
82
|
+
div role: "menuitem", tabIndex: -1
|
|
83
83
|
$highlighted: (idx is highlightedIndex)?!
|
|
84
84
|
$disabled: item.dataset.disabled?!
|
|
85
85
|
$value: item.dataset.item
|
|
@@ -195,7 +195,7 @@ export DatePicker = component
|
|
|
195
195
|
# Day grid
|
|
196
196
|
. $days: true, role: "grid"
|
|
197
197
|
for entry, dIdx in _daysInView
|
|
198
|
-
button role: "gridcell",
|
|
198
|
+
button role: "gridcell", tabIndex: -1
|
|
199
199
|
$outside: entry.outside?!
|
|
200
200
|
$today: dpSameDay(entry.date, _today)?!
|
|
201
201
|
$selected: (if @range then (Array.isArray(@value) and (dpSameDay(entry.date, @value[0]) or dpSameDay(entry.date, @value[1]))) else dpSameDay(entry.date, @value))?!
|
|
@@ -879,7 +879,7 @@ export Grid = component
|
|
|
879
879
|
@_resizeObs.disconnect()
|
|
880
880
|
|
|
881
881
|
render
|
|
882
|
-
div.grid-container ref: "_container", role: "grid",
|
|
882
|
+
div.grid-container ref: "_container", role: "grid", tabIndex: 0,
|
|
883
883
|
$editing: editing?!,
|
|
884
884
|
$selecting: selecting?!
|
|
885
885
|
table.rip-grid
|
|
@@ -139,7 +139,7 @@ export Menu = component
|
|
|
139
139
|
@keydown: @onMenuKeydown
|
|
140
140
|
for item, idx in items
|
|
141
141
|
div role: item.getAttribute('role') or 'menuitem'
|
|
142
|
-
|
|
142
|
+
tabIndex: -1
|
|
143
143
|
aria-checked: item.getAttribute('aria-checked')?!
|
|
144
144
|
$highlighted: (idx is highlightedIndex)?!
|
|
145
145
|
$disabled: item.dataset.disabled?!
|
|
@@ -129,7 +129,7 @@ export Menubar = component
|
|
|
129
129
|
slot
|
|
130
130
|
|
|
131
131
|
for menu in _menus
|
|
132
|
-
button role: "menuitem",
|
|
132
|
+
button role: "menuitem", tabIndex: 0
|
|
133
133
|
"data-menu-trigger": menu.dataset.menu
|
|
134
134
|
aria-haspopup: "menu"
|
|
135
135
|
aria-expanded: activeMenu is menu.dataset.menu
|
|
@@ -143,7 +143,7 @@ export Menubar = component
|
|
|
143
143
|
"data-menu-list": menu.dataset.menu
|
|
144
144
|
@keydown: (e) => @_onMenuKeydown(e, menu.dataset.menu, @_menuItemsFor(menu))
|
|
145
145
|
for item, idx in @_menuItemsFor(menu)
|
|
146
|
-
div role: "menuitem",
|
|
146
|
+
div role: "menuitem", tabIndex: -1
|
|
147
147
|
$highlighted: (idx is highlightedIndex)?!
|
|
148
148
|
$value: item.dataset.item
|
|
149
149
|
@click: (=> @selectItem(menu.dataset.menu, item.dataset.item))
|
|
@@ -191,7 +191,7 @@ export MultiSelect = component
|
|
|
191
191
|
aria-multiselectable: "true"
|
|
192
192
|
style: "position:fixed;margin:0;inset:auto"
|
|
193
193
|
for item, idx in filtered
|
|
194
|
-
div role: "option",
|
|
194
|
+
div role: "option", tabIndex: -1, id: "#{_listId}-#{idx}"
|
|
195
195
|
$value: @_val(item)
|
|
196
196
|
$selected: @_isSelected(item)?!
|
|
197
197
|
$highlighted: (idx is highlightedIndex)?!
|
|
@@ -98,12 +98,12 @@ export NavigationMenu = component
|
|
|
98
98
|
|
|
99
99
|
for navItem, nIdx in _navItems
|
|
100
100
|
if navItem.dataset.link?
|
|
101
|
-
a $nav-link: true, href: navItem.getAttribute('href') or '#',
|
|
101
|
+
a $nav-link: true, href: navItem.getAttribute('href') or '#', tabIndex: 0
|
|
102
102
|
@keydown: @_onKeydown
|
|
103
103
|
= navItem.textContent
|
|
104
104
|
else if navItem.dataset.trigger?
|
|
105
105
|
. style: "display:inline-block;position:relative"
|
|
106
|
-
button $nav-trigger: navItem.dataset.trigger,
|
|
106
|
+
button $nav-trigger: navItem.dataset.trigger, tabIndex: 0
|
|
107
107
|
aria-expanded: activePanel is navItem.dataset.trigger
|
|
108
108
|
$open: (activePanel is navItem.dataset.trigger)?!
|
|
109
109
|
@click: (=> if activePanel is navItem.dataset.trigger then @_closePanel() else @_openPanel(navItem.dataset.trigger))
|
|
@@ -118,7 +118,7 @@ export NavigationMenu = component
|
|
|
118
118
|
@mouseenter: (=> @_cancelClose())
|
|
119
119
|
@mouseleave: (=> @_scheduleClose())
|
|
120
120
|
for link, lIdx in Array.from(navItem.querySelectorAll('a, [data-link]'))
|
|
121
|
-
a href: link.getAttribute('href') or '#',
|
|
121
|
+
a href: link.getAttribute('href') or '#', tabIndex: 0
|
|
122
122
|
@keydown: (e) =>
|
|
123
123
|
if e.key is 'Escape'
|
|
124
124
|
@_closePanel()
|
|
@@ -136,7 +136,7 @@ export NumberField = component
|
|
|
136
136
|
|
|
137
137
|
render
|
|
138
138
|
div role: "group", $disabled: @disabled?!, $readonly: @readOnly?!
|
|
139
|
-
button aria-label: "Decrease",
|
|
139
|
+
button aria-label: "Decrease", tabIndex: -1
|
|
140
140
|
$decrement: true
|
|
141
141
|
aria-controls: _id
|
|
142
142
|
disabled: @disabled or (@min? and @value <= @min)
|
|
@@ -155,7 +155,7 @@ export NumberField = component
|
|
|
155
155
|
@keydown: @_onKeydown
|
|
156
156
|
@blur: @_onBlur
|
|
157
157
|
|
|
158
|
-
button aria-label: "Increase",
|
|
158
|
+
button aria-label: "Increase", tabIndex: -1
|
|
159
159
|
$increment: true
|
|
160
160
|
aria-controls: _id
|
|
161
161
|
disabled: @disabled or (@max? and @value >= @max)
|
|
@@ -26,6 +26,7 @@ export Select = component
|
|
|
26
26
|
typeaheadBuffer := ''
|
|
27
27
|
typeaheadTimer := null
|
|
28
28
|
suppressTriggerClick := false
|
|
29
|
+
suppressOptionClick := false
|
|
29
30
|
_ready := false
|
|
30
31
|
_popupGuard =! ARIA.popupGuard()
|
|
31
32
|
_listId =! "sel-#{Math.random().toString(36).slice(2, 8)}"
|
|
@@ -94,6 +95,22 @@ export Select = component
|
|
|
94
95
|
e.preventDefault()
|
|
95
96
|
if open then @close(false, true) else @openMenu()
|
|
96
97
|
|
|
98
|
+
onOptionPointerup: (e:: any) ->
|
|
99
|
+
return unless open
|
|
100
|
+
return unless e.button is 0
|
|
101
|
+
return unless e.pointerType in ['mouse', 'pen']
|
|
102
|
+
idx = Number(e.currentTarget.dataset.idx)
|
|
103
|
+
suppressOptionClick = true
|
|
104
|
+
@selectIndex(idx)
|
|
105
|
+
|
|
106
|
+
onOptionClick: (e:: any) ->
|
|
107
|
+
idx = Number(e.currentTarget.dataset.idx)
|
|
108
|
+
if suppressOptionClick
|
|
109
|
+
suppressOptionClick = false
|
|
110
|
+
e.preventDefault()
|
|
111
|
+
return
|
|
112
|
+
@selectIndex(idx)
|
|
113
|
+
|
|
97
114
|
_nextEnabled: (from, dir) ->
|
|
98
115
|
len = options.length
|
|
99
116
|
i = from
|
|
@@ -175,13 +192,15 @@ export Select = component
|
|
|
175
192
|
@keydown: @onListKeydown
|
|
176
193
|
for opt, idx in options
|
|
177
194
|
div role: "option"
|
|
178
|
-
|
|
195
|
+
tabIndex: -1
|
|
196
|
+
data-idx: idx
|
|
179
197
|
$value: @getOpt(opt)
|
|
180
198
|
$highlighted: (idx is highlightedIndex)?!
|
|
181
199
|
$selected: (@getOpt(opt) is String(@value))?!
|
|
182
200
|
$disabled: @isDisabled(opt)?!
|
|
183
201
|
aria-selected: @getOpt(opt) is String(@value)
|
|
184
202
|
aria-disabled: @isDisabled(opt)?!
|
|
185
|
-
@
|
|
203
|
+
@pointerup: @onOptionPointerup
|
|
204
|
+
@click: @onOptionClick
|
|
186
205
|
@mouseenter: (=> highlightedIndex = idx)
|
|
187
206
|
= opt.textContent
|
package/email/README.md
CHANGED
|
@@ -53,3 +53,18 @@ Tailwind config: theme
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Use one shared Tailwind config per rendered email tree.
|
|
56
|
+
|
|
57
|
+
## Runnable examples
|
|
58
|
+
|
|
59
|
+
From `packages/ui/email`:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
../../../bin/rip example-1.rip > example-1.html
|
|
63
|
+
../../../bin/rip example-2.rip > example-2.html
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- `example-1.rip` shows the plain curated-component workflow.
|
|
67
|
+
- `example-2.rip` shows a richer newsletter layout using the most email-safe current path: curated components plus inline styles.
|
|
68
|
+
- Both runnable examples load Inter from Google Fonts through the curated `Font` component's `google: 'Inter'` mode.
|
|
69
|
+
|
|
70
|
+
For now, the curated component path is the best choice for emails you want to send broadly across clients. The Tailwind path is promising, but it is still better treated as an advanced or experimental workflow until more email-client hardening is done.
|
package/email/components.rip
CHANGED
|
@@ -55,29 +55,40 @@ export Font = component
|
|
|
55
55
|
@fallbackFontFamily:: string | string[] := "Arial"
|
|
56
56
|
@fontStyle:: string := "normal"
|
|
57
57
|
@fontWeight:: number := 400
|
|
58
|
+
@google:: string := ""
|
|
59
|
+
@display:: string := "swap"
|
|
58
60
|
@webFontUrl:: string := ""
|
|
59
61
|
@webFontFormat:: string := "woff2"
|
|
60
62
|
|
|
63
|
+
_family ~= @google or @fontFamily
|
|
64
|
+
_googleFamily ~= encodeURIComponent(_family).replace(/%20/g, '+')
|
|
65
|
+
_googleSpec ~= if @fontStyle is 'italic'
|
|
66
|
+
"#{_googleFamily}:ital,wght@1,#{@fontWeight}"
|
|
67
|
+
else
|
|
68
|
+
"#{_googleFamily}:wght@#{@fontWeight}"
|
|
69
|
+
_googleUrl ~= if @google then "https://fonts.googleapis.com/css2?family=#{_googleSpec}&display=#{@display}" else ''
|
|
61
70
|
_src ~= if @webFontUrl then "src: url(#{@webFontUrl}) format('#{@webFontFormat}');" else ''
|
|
62
71
|
_fallback:: string[] ~= if kind(@fallbackFontFamily) is 'array' then Array.from(@fallbackFontFamily) else [@fallbackFontFamily]
|
|
63
72
|
_msoAlt ~= _fallback[0]
|
|
64
73
|
_fallbackStr:: string ~= _fallback.join(', ')
|
|
74
|
+
_fontRule ~= "* { font-family: '#{_family}', #{_fallbackStr}; }"
|
|
65
75
|
|
|
66
|
-
_css ~=
|
|
76
|
+
_css ~= if @webFontUrl
|
|
77
|
+
"""
|
|
67
78
|
@font-face {
|
|
68
|
-
font-family: '#{
|
|
79
|
+
font-family: '#{_family}';
|
|
69
80
|
font-style: #{@fontStyle};
|
|
70
81
|
font-weight: #{@fontWeight};
|
|
71
82
|
mso-font-alt: '#{_msoAlt}';
|
|
72
83
|
#{_src}
|
|
73
84
|
}
|
|
74
|
-
|
|
75
|
-
font-family: '#{@fontFamily}', #{_fallbackStr};
|
|
76
|
-
}
|
|
85
|
+
#{_fontRule}
|
|
77
86
|
"""
|
|
87
|
+
else
|
|
88
|
+
("@import url('#{_googleUrl}');\n" if _googleUrl else '') + _fontRule
|
|
78
89
|
|
|
79
90
|
render
|
|
80
|
-
if
|
|
91
|
+
if _family
|
|
81
92
|
style innerHTML: _css
|
|
82
93
|
|
|
83
94
|
# ==============================================================================
|
package/email/email.rip
CHANGED
|
@@ -7,4 +7,8 @@
|
|
|
7
7
|
# ==============================================================================
|
|
8
8
|
|
|
9
9
|
export { toHTML, toText, renderEmail } from './render.rip'
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
Email, Head, Body, Preview, Font,
|
|
12
|
+
Container, Section, Row, Column, Heading, Text, Link, Image, Divider, Button,
|
|
13
|
+
Markdown, CodeBlock, CodeInline, Tailwind
|
|
14
|
+
} from './components.rip'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html data-part="Email" lang="en" dir="ltr"><head data-part="Head"><meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /><meta name="x-apple-disable-message-reformatting" /><style>@font-face {
|
|
2
|
+
font-family: 'Inter';
|
|
3
|
+
font-style: normal;
|
|
4
|
+
font-weight: 400;
|
|
5
|
+
mso-font-alt: 'Arial, sans-serif';
|
|
6
|
+
src: url(https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
|
|
7
|
+
}
|
|
8
|
+
* {
|
|
9
|
+
font-family: 'Inter', Arial, sans-serif;
|
|
10
|
+
}</style><style>@font-face {
|
|
11
|
+
font-family: 'Inter';
|
|
12
|
+
font-style: normal;
|
|
13
|
+
font-weight: 700;
|
|
14
|
+
mso-font-alt: 'Arial, sans-serif';
|
|
15
|
+
src: url(https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
|
|
16
|
+
}
|
|
17
|
+
* {
|
|
18
|
+
font-family: 'Inter', Arial, sans-serif;
|
|
19
|
+
}</style></head><body data-part="Body"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td style="background:#f6f9fc;padding:24px;font-family:'Inter',Arial,sans-serif"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0" data-part="Preview" data-skip-in-text="true">Welcome to Rip, Alice!<div> </div></div><table style="max-width:37.5em;background:#ffffff;padding:32px;border:1px solid #e5e7eb" data-part="Container" align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr style="width:100%"><td><h1 style="font-size:28px;line-height:32px;margin:0 0 16px;color:#111827">Welcome, Alice</h1><p style="font-size:16px;line-height:24px;margin:16px 0;color:#4b5563" data-part="Text">Your account is ready. Click below to sign in.</p><a style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background:#111827;color:#ffffff;padding:12px 18px;border-radius:8px" data-part="Button" href="https://example.com/login" target="_blank"><span><!--[if mso]><i style="mso-font-width:450%;mso-text-raise:18" hidden>  </i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px">Sign in</span><span><!--[if mso]><i style="mso-font-width:450%" hidden>  ​</i><![endif]--></span></a><hr style="width:100%;border:none;border-top:1px solid #e5e7eb;margin:24px 0" data-part="Divider" /><p style="font-size:14px;line-height:22px;margin:16px 0;color:#6b7280" data-part="Text">If the button does not work, copy and paste this link: <a style="color:#0f766e;text-decoration:none" data-part="Link" href="https://example.com/login" target="_blank">https://example.com/login</a></p></td></tr></tbody></table></td></tr></tbody></table></body></html>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Example 1 — minimal curated email rendered to real HTML.
|
|
3
|
+
#
|
|
4
|
+
# Run from packages/ui/email:
|
|
5
|
+
# ../../../bin/rip example-1.rip > example-1.html
|
|
6
|
+
#
|
|
7
|
+
import { renderEmail, Email, Head, Body, Preview, Font, Container, Heading, Text, Button, Divider, Link } from './email.rip'
|
|
8
|
+
|
|
9
|
+
export WelcomeEmail = component
|
|
10
|
+
@name:: string := "friend"
|
|
11
|
+
@loginUrl:: string := "https://example.com/login"
|
|
12
|
+
|
|
13
|
+
render
|
|
14
|
+
Email lang: 'en'
|
|
15
|
+
Head
|
|
16
|
+
Font google: 'Inter', fallbackFontFamily: 'Arial, sans-serif', fontWeight: 400
|
|
17
|
+
Font google: 'Inter', fallbackFontFamily: 'Arial, sans-serif', fontWeight: 700
|
|
18
|
+
Body style: "background:#f6f9fc;padding:24px;font-family:'Inter',Arial,sans-serif"
|
|
19
|
+
Preview text: "Welcome to Rip, #{@name}!"
|
|
20
|
+
Container style: 'background:#ffffff;padding:32px;border:1px solid #e5e7eb'
|
|
21
|
+
Heading as: 'h1', style: 'font-size:28px;line-height:32px;margin:0 0 16px;color:#111827'
|
|
22
|
+
"Welcome, #{@name}"
|
|
23
|
+
|
|
24
|
+
Text style: 'font-size:16px;line-height:24px;color:#4b5563'
|
|
25
|
+
"Your account is ready. Click below to sign in."
|
|
26
|
+
|
|
27
|
+
Button href: @loginUrl, style: 'background:#111827;color:#ffffff;padding:12px 18px;border-radius:8px'
|
|
28
|
+
'Sign in'
|
|
29
|
+
|
|
30
|
+
Divider style: 'margin:24px 0;border-top:1px solid #e5e7eb'
|
|
31
|
+
|
|
32
|
+
Text style: 'font-size:14px;line-height:22px;color:#6b7280'
|
|
33
|
+
= 'If the button does not work, copy and paste this link: '
|
|
34
|
+
Link href: @loginUrl, style: 'color:#0f766e'
|
|
35
|
+
= @loginUrl
|
|
36
|
+
|
|
37
|
+
result = renderEmail WelcomeEmail,
|
|
38
|
+
name: 'Alice'
|
|
39
|
+
loginUrl: 'https://example.com/login'
|
|
40
|
+
|
|
41
|
+
p result.html
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html data-part="Email" lang="en" dir="ltr"><head data-part="Head"><meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /><meta name="x-apple-disable-message-reformatting" /><style>@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400&display=swap');
|
|
2
|
+
* { font-family: 'Inter', Arial, sans-serif; }</style><style>@import url('https://fonts.googleapis.com/css2?family=Inter:wght@700&display=swap');
|
|
3
|
+
* { font-family: 'Inter', Arial, sans-serif; }</style></head><body data-part="Body"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td style="background:#f8fafc;padding:24px;font-family:'Inter',Arial,sans-serif"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0" data-part="Preview" data-skip-in-text="true">Weekly product digest for Alice<div> </div></div><table style="max-width:37.5em;background:#ffffff;padding:32px;border:1px solid #e2e8f0" data-part="Container" align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr style="width:100%"><td><h1 style="font-size:30px;line-height:36px;margin:0 0 12px;color:#0f172a">Weekly digest</h1><p style="font-size:16px;line-height:24px;margin:16px 0;color:#475569" data-part="Text">Hi Alice, here are the product and customer updates worth reading this week.</p><hr style="width:100%;border:none;border-top:1px solid #e2e8f0;margin:24px 0" data-part="Divider" /><table style="padding:16px 0" data-part="Section" align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><h2 style="font-size:20px;line-height:28px;margin:0 0 8px;color:#111827">Activation is up 8%</h2><p style="font-size:15px;line-height:24px;margin:0;color:#475569" data-part="Text">Trial users are reaching their first successful workflow faster after the onboarding simplification.</p></td></tr></tbody></table><table style="padding:16px 0" data-part="Section" align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><h2 style="font-size:20px;line-height:28px;margin:0 0 8px;color:#111827">Search is much faster</h2><p style="font-size:15px;line-height:24px;margin:0;color:#475569" data-part="Text">The new query path reduced median lookup time and removed several long-tail slow requests.</p></td></tr></tbody></table><hr style="width:100%;border:none;border-top:1px solid #e2e8f0;margin:24px 0" data-part="Divider" /><table style="width:100%" data-part="Row" align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr style="width:100%"><td style="width:70%;vertical-align:top" data-part="Column"><p style="font-size:14px;line-height:22px;margin:0;color:#64748b" data-part="Text">Open the dashboard for the full breakdown and action items.</p></td><td style="width:30%;text-align:right;vertical-align:top" data-part="Column"><a style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background:#0f172a;color:#ffffff;padding:12px 16px;border-radius:8px" data-part="Button" href="https://example.com/dashboard" target="_blank"><span><!--[if mso]><i style="mso-font-width:400%;mso-text-raise:18" hidden>  </i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px">Open dashboard</span><span><!--[if mso]><i style="mso-font-width:400%" hidden>  ​</i><![endif]--></span></a></td></tr></tbody></table><p style="font-size:13px;line-height:20px;margin:24px 0 0;color:#64748b" data-part="Text">Need help? Visit <a style="color:#0f766e;text-decoration:none" data-part="Link" href="https://example.com/help" target="_blank">the support center</a>.</p></td></tr></tbody></table></td></tr></tbody></table></body></html>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Example 2 — richer newsletter-style email using the safest current path:
|
|
3
|
+
# curated email components + inline styles.
|
|
4
|
+
#
|
|
5
|
+
# Run from packages/ui/email:
|
|
6
|
+
# ../../../bin/rip example-2.rip > example-2.html
|
|
7
|
+
#
|
|
8
|
+
import { renderEmail, Email, Head, Body, Preview, Font, Container, Section, Row, Column, Heading, Text, Button, Divider, Link } from './email.rip'
|
|
9
|
+
|
|
10
|
+
updates = [
|
|
11
|
+
{
|
|
12
|
+
title: 'Activation is up 8%'
|
|
13
|
+
body: 'Trial users are reaching their first successful workflow faster after the onboarding simplification.'
|
|
14
|
+
}
|
|
15
|
+
{
|
|
16
|
+
title: 'Search is much faster'
|
|
17
|
+
body: 'The new query path reduced median lookup time and removed several long-tail slow requests.'
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
export WeeklyDigestEmail = component
|
|
22
|
+
@dashboardUrl:: string := "https://example.com/dashboard"
|
|
23
|
+
@name:: string := "Alice"
|
|
24
|
+
|
|
25
|
+
render
|
|
26
|
+
Email lang: 'en'
|
|
27
|
+
Head
|
|
28
|
+
Font google: 'Inter', fallbackFontFamily: 'Arial, sans-serif', fontWeight: 400
|
|
29
|
+
Font google: 'Inter', fallbackFontFamily: 'Arial, sans-serif', fontWeight: 700
|
|
30
|
+
Body style: "background:#f8fafc;padding:24px;font-family:'Inter',Arial,sans-serif"
|
|
31
|
+
Preview text: "Weekly product digest for #{@name}"
|
|
32
|
+
Container style: 'background:#ffffff;padding:32px;border:1px solid #e2e8f0'
|
|
33
|
+
Heading as: 'h1', style: 'font-size:30px;line-height:36px;margin:0 0 12px;color:#0f172a'
|
|
34
|
+
"Weekly digest"
|
|
35
|
+
|
|
36
|
+
Text style: 'font-size:16px;line-height:24px;color:#475569'
|
|
37
|
+
"Hi #{@name}, here are the product and customer updates worth reading this week."
|
|
38
|
+
|
|
39
|
+
Divider style: 'margin:24px 0;border-top:1px solid #e2e8f0'
|
|
40
|
+
|
|
41
|
+
for update in updates
|
|
42
|
+
Section style: 'padding:16px 0'
|
|
43
|
+
Heading as: 'h2', style: 'font-size:20px;line-height:28px;margin:0 0 8px;color:#111827'
|
|
44
|
+
= update.title
|
|
45
|
+
Text style: 'font-size:15px;line-height:24px;color:#475569;margin:0'
|
|
46
|
+
= update.body
|
|
47
|
+
|
|
48
|
+
Divider style: 'margin:24px 0;border-top:1px solid #e2e8f0'
|
|
49
|
+
|
|
50
|
+
Row style: 'width:100%'
|
|
51
|
+
Column style: 'width:70%;vertical-align:top'
|
|
52
|
+
Text style: 'font-size:14px;line-height:22px;color:#64748b;margin:0'
|
|
53
|
+
'Open the dashboard for the full breakdown and action items.'
|
|
54
|
+
Column style: 'width:30%;text-align:right;vertical-align:top'
|
|
55
|
+
Button href: @dashboardUrl, style: 'background:#0f172a;color:#ffffff;padding:12px 16px;border-radius:8px'
|
|
56
|
+
'Open dashboard'
|
|
57
|
+
|
|
58
|
+
Text style: 'font-size:13px;line-height:20px;color:#64748b;margin:24px 0 0'
|
|
59
|
+
= 'Need help? Visit '
|
|
60
|
+
Link href: 'https://example.com/help', style: 'color:#0f766e'
|
|
61
|
+
'the support center'
|
|
62
|
+
= '.'
|
|
63
|
+
|
|
64
|
+
result = renderEmail WeeklyDigestEmail,
|
|
65
|
+
dashboardUrl: 'https://example.com/dashboard'
|
|
66
|
+
name: 'Alice'
|
|
67
|
+
|
|
68
|
+
p result.html
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rip-lang/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Unified UI system for Rip — browser widgets, email components, and Tailwind integration",
|
|
6
6
|
"exports": {
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
"ui.rip"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"rip-lang": ">=3.13.
|
|
25
|
+
"rip-lang": ">=3.13.124",
|
|
26
26
|
"tailwindcss": "^4",
|
|
27
27
|
"css-tree": "^3"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@axe-core/playwright": "4.11.1",
|
|
31
|
-
"@rip-lang/server": "1.3.
|
|
31
|
+
"@rip-lang/server": "1.3.118",
|
|
32
32
|
"playwright": "1.58.2"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|