@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.
@@ -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", tabindex: "-1"
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, tabindex: "0"
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", tabindex: "-1"
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", tabindex: "-1", id: "#{_listId}-#{idx}"
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", tabindex: "-1"
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", tabindex: "-1"
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", tabindex: "0",
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
- tabindex: "-1"
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", tabindex: "0"
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", tabindex: "-1"
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", tabindex: "-1", id: "#{_listId}-#{idx}"
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 '#', tabindex: "0"
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, tabindex: "0"
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 '#', tabindex: "0"
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", tabindex: "-1"
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", tabindex: "-1"
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
- tabindex: "-1"
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
- @click: (=> @selectIndex(idx))
203
+ @pointerup: @onOptionPointerup
204
+ @click: @onOptionClick
186
205
  @mouseenter: (=> highlightedIndex = idx)
187
206
  = opt.textContent
@@ -93,7 +93,7 @@ export ToggleGroup = component
93
93
  slot
94
94
 
95
95
  for item, idx in _items
96
- button tabindex: "-1"
96
+ button tabIndex: -1
97
97
  aria-pressed: !!@_isPressed(item)
98
98
  $pressed: @_isPressed(item)?!
99
99
  $disabled: @disabled?!
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.
@@ -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: '#{@fontFamily}';
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 @fontFamily
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 { Email, Head, Body, Preview, Font, Container, Section, Row, Column, Heading, Text, Link, Image, Divider, Button, Markdown, CodeBlock, CodeInline, Tailwind } from './components.rip'
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>&#8202;&#8202;</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>&#8202;&#8202;&#8203;</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>&#8202;&#8202;</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>&#8202;&#8202;&#8203;</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.2",
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.122",
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.116",
31
+ "@rip-lang/server": "1.3.118",
32
32
  "playwright": "1.58.2"
33
33
  },
34
34
  "scripts": {