fron-ui 1.0.0rc2

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.
Files changed (140) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +38 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +7 -0
  7. data/.yardopts +8 -0
  8. data/Gemfile +8 -0
  9. data/Gemfile.lock +105 -0
  10. data/Rakefile +37 -0
  11. data/Readme.md +4 -0
  12. data/db.json +192 -0
  13. data/fron-ui.gemspec +21 -0
  14. data/lib/fron-ui.rb +1 -0
  15. data/lib/fron_ui.rb +5 -0
  16. data/lib/fron_ui/version.rb +7 -0
  17. data/opal/fron-ui/base.rb +49 -0
  18. data/opal/fron-ui/behaviors/action.rb +40 -0
  19. data/opal/fron-ui/behaviors/actions.rb +40 -0
  20. data/opal/fron-ui/behaviors/confirmation.rb +23 -0
  21. data/opal/fron-ui/behaviors/dropdown.rb +27 -0
  22. data/opal/fron-ui/behaviors/file.rb +48 -0
  23. data/opal/fron-ui/behaviors/intendable_children.rb +76 -0
  24. data/opal/fron-ui/behaviors/keydown.rb +31 -0
  25. data/opal/fron-ui/behaviors/loop.rb +41 -0
  26. data/opal/fron-ui/behaviors/render.rb +30 -0
  27. data/opal/fron-ui/behaviors/rest.rb +121 -0
  28. data/opal/fron-ui/behaviors/selectable_children.rb +67 -0
  29. data/opal/fron-ui/behaviors/serialize.rb +32 -0
  30. data/opal/fron-ui/behaviors/shortcuts.rb +35 -0
  31. data/opal/fron-ui/behaviors/state.rb +56 -0
  32. data/opal/fron-ui/behaviors/transition.rb +63 -0
  33. data/opal/fron-ui/components/action.rb +18 -0
  34. data/opal/fron-ui/components/box.rb +17 -0
  35. data/opal/fron-ui/components/button.rb +61 -0
  36. data/opal/fron-ui/components/calendar.rb +129 -0
  37. data/opal/fron-ui/components/checkbox.rb +57 -0
  38. data/opal/fron-ui/components/chooser.rb +246 -0
  39. data/opal/fron-ui/components/color_panel.rb +235 -0
  40. data/opal/fron-ui/components/color_picker.rb +111 -0
  41. data/opal/fron-ui/components/container.rb +61 -0
  42. data/opal/fron-ui/components/date_picker.rb +141 -0
  43. data/opal/fron-ui/components/drag.rb +76 -0
  44. data/opal/fron-ui/components/dropdown.rb +72 -0
  45. data/opal/fron-ui/components/icon.rb +29 -0
  46. data/opal/fron-ui/components/image.rb +77 -0
  47. data/opal/fron-ui/components/input.rb +30 -0
  48. data/opal/fron-ui/components/label.rb +9 -0
  49. data/opal/fron-ui/components/list.rb +34 -0
  50. data/opal/fron-ui/components/loader.rb +63 -0
  51. data/opal/fron-ui/components/modal.rb +0 -0
  52. data/opal/fron-ui/components/notifications.rb +73 -0
  53. data/opal/fron-ui/components/number.rb +202 -0
  54. data/opal/fron-ui/components/progress.rb +52 -0
  55. data/opal/fron-ui/components/slider.rb +47 -0
  56. data/opal/fron-ui/components/tabs.rb +149 -0
  57. data/opal/fron-ui/components/textarea.rb +13 -0
  58. data/opal/fron-ui/components/time.rb +65 -0
  59. data/opal/fron-ui/components/title.rb +34 -0
  60. data/opal/fron-ui/examples/blog/index.rb +289 -0
  61. data/opal/fron-ui/examples/comments/components/comment.rb +75 -0
  62. data/opal/fron-ui/examples/comments/components/comments.rb +93 -0
  63. data/opal/fron-ui/examples/comments/components/footer.rb +36 -0
  64. data/opal/fron-ui/examples/comments/components/header.rb +35 -0
  65. data/opal/fron-ui/examples/comments/components/list.rb +12 -0
  66. data/opal/fron-ui/examples/comments/index.rb +6 -0
  67. data/opal/fron-ui/examples/contacts/components/contacts.rb +100 -0
  68. data/opal/fron-ui/examples/contacts/components/details.rb +92 -0
  69. data/opal/fron-ui/examples/contacts/components/item.rb +46 -0
  70. data/opal/fron-ui/examples/contacts/components/list.rb +10 -0
  71. data/opal/fron-ui/examples/contacts/components/sidebar.rb +30 -0
  72. data/opal/fron-ui/examples/contacts/index.rb +6 -0
  73. data/opal/fron-ui/examples/editor/index.rb +164 -0
  74. data/opal/fron-ui/examples/kitchensink/index.rb +193 -0
  75. data/opal/fron-ui/examples/todos/components/item.rb +84 -0
  76. data/opal/fron-ui/examples/todos/components/options.rb +26 -0
  77. data/opal/fron-ui/examples/todos/components/todos.rb +145 -0
  78. data/opal/fron-ui/examples/todos/index.rb +6 -0
  79. data/opal/fron-ui/examples/webshop/index.rb +0 -0
  80. data/opal/fron-ui/fonts/ionicons.rb +2954 -0
  81. data/opal/fron-ui/fonts/open_sans.rb +19 -0
  82. data/opal/fron-ui/lib/collection.rb +138 -0
  83. data/opal/fron-ui/lib/date.rb +23 -0
  84. data/opal/fron-ui/lib/debounce.rb +14 -0
  85. data/opal/fron-ui/lib/image_loader.rb +13 -0
  86. data/opal/fron-ui/lib/lorem.rb +93 -0
  87. data/opal/fron-ui/lib/nil.rb +29 -0
  88. data/opal/fron-ui/lib/record.rb +23 -0
  89. data/opal/fron-ui/lib/state_serializer.rb +129 -0
  90. data/opal/fron-ui/lib/storage.rb +57 -0
  91. data/opal/fron-ui/spec/setup.rb +40 -0
  92. data/opal/fron-ui/ui.rb +40 -0
  93. data/opal/fron-ui/utils/theme_roller.rb +63 -0
  94. data/opal/fron-ui/vendor/autoprefixer.js +21114 -0
  95. data/opal/fron-ui/vendor/marked.js +1291 -0
  96. data/opal/fron-ui/vendor/md5.js +274 -0
  97. data/opal/fron-ui/vendor/moment.js +3083 -0
  98. data/opal/fron-ui/vendor/uuid.js +92 -0
  99. data/opal/fron_ui.rb +13 -0
  100. data/spec/behaviors/action_spec.rb +34 -0
  101. data/spec/behaviors/actions_spec.rb +38 -0
  102. data/spec/behaviors/confirmation_spec.rb +23 -0
  103. data/spec/behaviors/dropdown_spec.rb +32 -0
  104. data/spec/behaviors/render_spec.rb +20 -0
  105. data/spec/behaviors/rest_spec.rb +70 -0
  106. data/spec/behaviors/selectable_children_spec.rb +40 -0
  107. data/spec/behaviors/serialize_spec.rb +34 -0
  108. data/spec/components/action_spec.rb +7 -0
  109. data/spec/components/base_spec.rb +19 -0
  110. data/spec/components/box_spec.rb +7 -0
  111. data/spec/components/button_spec.rb +9 -0
  112. data/spec/components/calendar_spec.rb +58 -0
  113. data/spec/components/checkbox_spec.rb +20 -0
  114. data/spec/components/chooser_spec.rb +75 -0
  115. data/spec/components/color_panel_spec.rb +49 -0
  116. data/spec/components/color_picker_spec.rb +41 -0
  117. data/spec/components/container_spec.rb +23 -0
  118. data/spec/components/date_picker_spec.rb +71 -0
  119. data/spec/components/drag_spec.rb +20 -0
  120. data/spec/components/dropdown_spec.rb +33 -0
  121. data/spec/components/image_spec.rb +33 -0
  122. data/spec/components/input_spec.rb +8 -0
  123. data/spec/components/list_spec.rb +10 -0
  124. data/spec/components/loader_spec.rb +9 -0
  125. data/spec/components/notifications_spec.rb +17 -0
  126. data/spec/components/number_spec.rb +64 -0
  127. data/spec/components/progress_spec.rb +23 -0
  128. data/spec/components/slider_spec.rb +25 -0
  129. data/spec/components/tabs_spec.rb +50 -0
  130. data/spec/components/textarea_spec.rb +7 -0
  131. data/spec/components/time_spec.rb +37 -0
  132. data/spec/components/title_spec.rb +11 -0
  133. data/spec/examples/comments_spec.rb +72 -0
  134. data/spec/examples/todos_spec.rb +39 -0
  135. data/spec/lib/collection_spec.rb +38 -0
  136. data/spec/lib/lorem_spec.rb +55 -0
  137. data/spec/lib/state_serializer_spec.rb +58 -0
  138. data/spec/lib/storage_spec.rb +39 -0
  139. data/spec/spec_helper.rb +1 -0
  140. metadata +223 -0
@@ -0,0 +1,52 @@
1
+ module UI
2
+ # Simple progressbar
3
+ #
4
+ # @author Gusztáv Szikszai
5
+ # @since 0.1.0
6
+ class Progress < Base
7
+ tag 'ui-progress'
8
+
9
+ component :bar, 'ui-progress-bar'
10
+
11
+ style borderRadius: -> { theme.border_radius.em },
12
+ minWidth: -> { (theme.size * 6).em },
13
+ background: -> { colors.input },
14
+ height: -> { 0.8.em },
15
+ 'ui-progress-bar' => {
16
+ borderRadius: -> { theme.border_radius.em },
17
+ background: -> { colors.primary },
18
+ transition: 'width 320ms',
19
+ height: :inherit,
20
+ display: :block
21
+ }
22
+
23
+ # Initializes the component and sets default value
24
+ def initialize
25
+ super
26
+ self.value = 0
27
+ end
28
+
29
+ # Sets the color of the bar
30
+ #
31
+ # @param color [String, Symbol] The color
32
+ def color=(color)
33
+ @bar.style.backgroundColor = color
34
+ end
35
+
36
+ # Sets the value
37
+ #
38
+ # @param value [Float] The value (0..1)
39
+ def value=(value)
40
+ value = value.clamp(0, 1) * 100
41
+ @bar.style.width = "#{value}%"
42
+ self[:percent] = "#{value}%"
43
+ end
44
+
45
+ # Returns the value
46
+ #
47
+ # @return [Float] The value
48
+ def value
49
+ @bar.style.width.to_i / 100
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,47 @@
1
+ module UI
2
+ # Horizontal range / slider component
3
+ #
4
+ # @attr value [Float] The value
5
+ #
6
+ # @author Gusztáv Szikszai
7
+ # @since 0.1.0
8
+ class Slider < Drag
9
+ tag 'ui-slider'
10
+
11
+ style minWidth: -> { (theme.size * 6).em },
12
+ height: -> { theme.size.em },
13
+ background: :transparent,
14
+ cursor: :pointer,
15
+ 'ui-drag-handle' => { borderRadius: -> { theme.border_radius.em },
16
+ background: -> { colors.primary },
17
+ pointerEvents: :auto,
18
+ height: 1.5.em,
19
+ width: 1.5.em,
20
+ top: '50%' },
21
+ '&:focus' => { outline: :none,
22
+ '&:before' => { boxShadow: -> { theme.focus_box_shadow.call } } },
23
+ '&:before' => { borderRadius: -> { theme.border_radius.em },
24
+ background: -> { colors.input },
25
+ position: :absolute,
26
+ marginTop: -0.4.em,
27
+ height: 0.8.em,
28
+ content: "''",
29
+ top: '50%',
30
+ right: 0,
31
+ left: 0 }
32
+
33
+ on :mousedown, :focus
34
+
35
+ # Initailizes the component by
36
+ # seting tabindex and restricting
37
+ # it to be horizontal
38
+ def initialize
39
+ super
40
+ @vertical = false
41
+ self[:tabindex] ||= 0
42
+ end
43
+
44
+ alias value value_x
45
+ alias value= value_x=
46
+ end
47
+ end
@@ -0,0 +1,149 @@
1
+ module UI
2
+ # Component for tabular UI.
3
+ #
4
+ # @author Gusztáv Szikszai
5
+ # @since 0.1.0
6
+ class Tabs < UI::Container
7
+ # Component for the handle.
8
+ #
9
+ # @author Gusztáv Szikszai
10
+ # @since 0.1.0
11
+ class Handle < Action
12
+ include ::Record
13
+
14
+ tag 'ui-tab-handle'
15
+
16
+ attribute_accessor :tab_id
17
+
18
+ style padding: -> { theme.spacing.em },
19
+ top: -> { theme.border_size.em },
20
+ display: 'inline-block',
21
+ position: :relative,
22
+ fontWeight: 600,
23
+ opacity: 0.5,
24
+ '&.selected' => { borderBottom: -> { "#{theme.border_size.em} solid #{colors.primary}" },
25
+ color: -> { colors.primary } },
26
+ '&:focus' => { borderBottom: -> { "#{theme.border_size.em} solid #{colors.focus}" },
27
+ color: -> { colors.focus } },
28
+ '&:hover, &.selected, &:focus' => { opacity: 1 }
29
+
30
+ # Renders the component
31
+ def render
32
+ self.tab_id = @data[:id]
33
+ self.text = @data[:title]
34
+ end
35
+ end
36
+
37
+ # Tab handle container.
38
+ #
39
+ # @author Gusztáv Szikszai
40
+ # @since 0.1.0
41
+ class Handles < Collection
42
+ include UI::Behaviors::SelectableChildren
43
+
44
+ tag 'ui-tab-handles'
45
+
46
+ style display: :block,
47
+ flex: '0 0 auto',
48
+ minHeight: 3.em,
49
+ borderBottom: -> { "#{theme.border_size.em} solid #{dampen colors.background, 0.05}" }
50
+ end
51
+
52
+ # Basic tab container.
53
+ #
54
+ # @author Gusztáv Szikszai
55
+ # @since 0.1.0
56
+ class Tab < Base
57
+ tag 'ui-tab'
58
+
59
+ style padding: -> { theme.spacing.em }
60
+ end
61
+
62
+ extend Forwardable
63
+
64
+ tag 'ui-tabs'
65
+
66
+ component :handles, Handles, base: Handle
67
+
68
+ def_delegators :handles, :base, :base=
69
+
70
+ style '> *:not(ui-tab-handles):not(.active)' => { visibility: :hidden,
71
+ overflow: :hidden,
72
+ display: :block,
73
+ padding: 0,
74
+ height: 0 },
75
+ '> .active' => { visibility: :visible,
76
+ overflow: :auto,
77
+ display: :block,
78
+ height: :auto,
79
+ flex: 1 }
80
+
81
+ on :selected_change, :select_tab
82
+
83
+ # Monkeypatch to update tabs
84
+ def <<
85
+ super
86
+ update_tabs
87
+ end
88
+
89
+ # Monkeypatch to update tabs
90
+ def insert_before
91
+ super
92
+ update_tabs
93
+ end
94
+
95
+ # Monkeypatch to update tabs
96
+ def remove
97
+ super
98
+ update_tabs
99
+ end
100
+
101
+ # Activates the currently selected tab
102
+ def select_tab
103
+ select find_by_id(handles.selected.tab_id)
104
+ end
105
+
106
+ # Selects the given tab.
107
+ #
108
+ # @param tab [UI::Tabs::Tab, Fron::Component] The tab
109
+ def select(tab)
110
+ return unless tab
111
+ return if active_tab == tab
112
+ active_tab.remove_class(:active) if active_tab
113
+ handles.select handles.find("[tab_id='#{tab[:tab_id]}']")
114
+ tab.add_class(:active)
115
+ end
116
+
117
+ # Returns the active tab
118
+ #
119
+ # @return [UI::Tabs::Tab, Fron::Component] The tab
120
+ def active_tab
121
+ children.find { |child| child.has_class(:active) }
122
+ end
123
+
124
+ # Returns the tab by the given id
125
+ #
126
+ # @param id [String] The id
127
+ #
128
+ # @return [UI::Tabs::Tab, Fron::Component] The tab
129
+ def find_by_id(id)
130
+ children.find { |child| child[:tab_id] == id }
131
+ end
132
+
133
+ # Updates tab handles to reflect the content.
134
+ def update_tabs
135
+ return unless @handles
136
+ handles.items = tabs.map { |item| { id: item[:tab_id], title: item[:tab_title] } }.uniq
137
+ return if handles.selected || handles.children.empty?
138
+ handles.select_first
139
+ select_tab
140
+ end
141
+
142
+ # Returns all tabs
143
+ #
144
+ # @return [Array<UI::Tabs::Tab>] The tabs
145
+ def tabs
146
+ find_all('[tab_id][tab_title]')
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,13 @@
1
+ module UI
2
+ # Textarea component.
3
+ #
4
+ # @author Gusztáv Szikszai
5
+ # @since 0.1.0
6
+ class Textarea < Input
7
+ tag 'textarea'
8
+
9
+ style padding: -> { (theme.spacing / 2).em },
10
+ lineHeight: -> { 1.5.em },
11
+ boxSizing: 'border-box'
12
+ end
13
+ end
@@ -0,0 +1,65 @@
1
+ module UI
2
+ # Time component to display static times or
3
+ # times that counts upwards (fromNow), using [Moment.js](http://momentjs.com/).
4
+ #
5
+ # @attr from_now [Bolean] The use the fromNow or not
6
+ # @attr format [String] The format to display
7
+ #
8
+ # @author Gusztáv Szikszai
9
+ # @since 0.1.0
10
+ class Time < Fron::Component
11
+ tag 'ui-time'
12
+
13
+ attr_accessor :from_now, :format
14
+
15
+ # Initializes the component
16
+ def initialize
17
+ super
18
+ render
19
+ end
20
+
21
+ # Sets the value (time) of the component
22
+ #
23
+ # @param value [Date, String] The value
24
+ def value=(value)
25
+ @value = if value.is_a? Date
26
+ Native(`moment(#{value}.date)`)
27
+ else
28
+ Native(`moment(#{value})`)
29
+ end
30
+ rescue
31
+ @value = nil
32
+ ensure
33
+ render
34
+ end
35
+
36
+ # Returns the value
37
+ #
38
+ # @return [Native] The moment wrapped value
39
+ def value
40
+ (@value || Native(`moment()`))
41
+ end
42
+
43
+ # Renders the component
44
+ def render
45
+ clear_timeout @id
46
+ request_animation_frame do
47
+ self.text = if @from_now
48
+ value.fromNow
49
+ else
50
+ value.format(@format || 'YYYY-MM-DD')
51
+ end
52
+ end
53
+ recall
54
+ end
55
+
56
+ private
57
+
58
+ # Periodically calls render
59
+ def recall
60
+ @id = timeout 1000 do
61
+ render
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,34 @@
1
+ module UI
2
+ # Title component
3
+ #
4
+ # Features:
5
+ # * Align text (left / center / right )
6
+ # * Bottom border
7
+ #
8
+ # @author Gusztáv Szikszai
9
+ # @since 0.1.0
10
+ class Title < UI::Container
11
+ extend Forwardable
12
+
13
+ tag 'ui-title'
14
+
15
+ style borderBottom: -> { "#{theme.border_size.em} solid #{dampen colors.background, 0.05}" },
16
+ paddingBottom: -> { theme.spacing.em },
17
+ fontFamily: -> { theme.font_family },
18
+ height: 2.em,
19
+ '> span' => { fontSize: '1.6em',
20
+ fontWeight: 600,
21
+ flex: 1 }
22
+
23
+ component :span, :span
24
+
25
+ def_delegators :span, :text, :text=
26
+
27
+ # Sets the text align of the component
28
+ #
29
+ # @param value [Symbol] The alignment
30
+ def align=(value)
31
+ @style.textAlign = value
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,289 @@
1
+ require 'fron_ui'
2
+
3
+ class Item < UI::Container
4
+ include UI::Behaviors::Rest
5
+ include ::Record
6
+
7
+ style padding: -> { theme.spacing.em },
8
+ '> span' => { fontWeight: 600 },
9
+ small: { opacity: 0.5 },
10
+ '&.selected' => {
11
+ color: -> { colors.focus }
12
+ }
13
+
14
+ component :span, :span
15
+ component :small, :small do
16
+ component :span, :span, text: 'Created '
17
+ component :time, UI::Time, from_now: true
18
+ end
19
+
20
+ def render
21
+ @span.text = @data[:title]
22
+ end
23
+ end
24
+
25
+ class List < UI::List
26
+ include UI::Behaviors::SelectableChildren
27
+ include UI::Behaviors::Rest
28
+
29
+ rest url: 'http://localhost:3000/posts'
30
+
31
+ def refresh
32
+ request :get, '' do |items|
33
+ self.items = items
34
+ yield
35
+ end
36
+ end
37
+ end
38
+
39
+ class Header < UI::Box
40
+ tag 'ui-header[direction=row]'
41
+
42
+ style '> *' => { padding: -> { "0 #{theme.spacing.em}" } }
43
+
44
+ component :brand, 'ui-brand', text: 'Blog'
45
+ component :posts, UI::Action, text: 'Content', action: :content
46
+ component :posts, UI::Action, text: 'New Post', action: :create
47
+ end
48
+
49
+ class Posts < UI::Container
50
+ extend Forwardable
51
+
52
+ tag 'ui-posts'
53
+
54
+ style 'ui-box:last-of-type ui-icon' => { marginRight: -> { theme.spacing.em } },
55
+ 'ui-body' => { overflow: :auto },
56
+ 'ui-title' => {
57
+ flex: '0 0 2em',
58
+ '> span' => {
59
+ overflow: :hidden,
60
+ textOverflow: :ellipsis,
61
+ whiteSpace: :nowrap
62
+ }
63
+ }
64
+
65
+ component :box, UI::Box, flex: '0 0 15em' do
66
+ component :title, UI::Title, text: 'Posts', direction: :row do
67
+ component :button, UI::Button, type: :success, shape: :square, action: :create do
68
+ component :icon, UI::Icon, glyph: :plus
69
+ end
70
+ end
71
+ component :list, List, base: Item, flex: 1
72
+ end
73
+
74
+ component :preview, UI::Box, flex: 1 do
75
+ component :title, UI::Title, direction: :row do
76
+ component :button, UI::Button, action: :edit do
77
+ component :icon, UI::Icon, glyph: :edit
78
+ component :span, :span, text: 'Edit'
79
+ end
80
+ component :button, UI::Button, action: :confirm_destroy!, type: :danger do
81
+ component :icon, UI::Icon, glyph: 'trash-b'
82
+ component :span, :span, text: 'Delete'
83
+ end
84
+ end
85
+ component :body, 'ui-body'
86
+ end
87
+
88
+ on :selected_change, :select
89
+
90
+ def_delegators :box, :list
91
+ def_delegators :list, :selected, :select_first
92
+
93
+ def refresh
94
+ list.refresh do
95
+ break if selected
96
+ select_first
97
+ end
98
+ end
99
+
100
+ def select
101
+ self.selected = selected.data
102
+ end
103
+
104
+ def selected=(data)
105
+ @preview.title.text = data[:title]
106
+ @preview.body.html = `marked(#{data[:body].to_s}, { gfm: true, breaks: true })`
107
+ end
108
+ end
109
+
110
+ class Form < UI::Container
111
+ include UI::Behaviors::Serialize
112
+ include UI::Behaviors::Actions
113
+ include UI::Behaviors::Rest
114
+
115
+ extend Forwardable
116
+
117
+ tag 'ui-form'
118
+
119
+ rest url: 'http://localhost:3000/posts'
120
+
121
+ style 'input[type=text]' => { fontSize: 2.em,
122
+ flex: '0 0 auto',
123
+ borderRadius: 0,
124
+ borderBottom: -> { "#{theme.border_size.em} solid #{dampen colors.background, 0.05}" } },
125
+ 'ui-container:first-of-type' => {
126
+ position: :relative,
127
+ paddingLeft: '50%',
128
+ marginTop: '0 !important'
129
+ },
130
+ 'ui-base, textarea' => {
131
+ boxSizing: 'border-box',
132
+ fontSize: 16.px,
133
+ padding: 20.px
134
+ },
135
+ 'ui-base' => {
136
+ borderLeft: -> { "#{theme.border_size.em} solid #{dampen colors.background, 0.05}" },
137
+ overflow: :auto,
138
+ div: {
139
+ maxWidth: 800.px,
140
+ margin: '0 auto',
141
+ '> *' => {
142
+ margin: 0,
143
+ marginBottom: 0.6.em
144
+ }
145
+ }
146
+ },
147
+ 'input, textarea' => {
148
+ '&:focus' => {
149
+ boxShadow: :none
150
+ }
151
+ },
152
+ textarea: {
153
+ position: :absolute,
154
+ resize: :none,
155
+ top: 0,
156
+ left: 0,
157
+ width: '50%',
158
+ height: '100%'
159
+ }
160
+
161
+ component :title, UI::Input, name: :title, placeholder: 'Post title...'
162
+ component :container, UI::Container, flex: 1, direction: :row, compact: true do
163
+ component :textarea, UI::Textarea, name: :body, placeholder: 'Post body...', spellcheck: false
164
+ component :preview, UI::Base, flex: 1 do
165
+ component :div, :div
166
+ end
167
+ end
168
+ component :statusbar, UI::Container, direction: :row, align: :end, flex: '0 0 auto' do
169
+ component :button, UI::Button, text: 'Save', action: :save
170
+ end
171
+
172
+ def_delegators :container, :preview, :textarea
173
+
174
+ on :keyup, :render
175
+
176
+ def initialize
177
+ super
178
+ textarea.on :scroll do
179
+ sync
180
+ end
181
+ end
182
+
183
+ def sync
184
+ preview.scroll_top = preview.scroll_height * (textarea.scroll_top / textarea.scroll_height)
185
+ end
186
+
187
+ def load(id, defaults = { title: '', body: '' })
188
+ if id
189
+ request :get, id do |data|
190
+ super data
191
+ render
192
+ yield if block_given?
193
+ end
194
+ else
195
+ super defaults
196
+ render
197
+ yield if block_given?
198
+ end
199
+ end
200
+
201
+ def save
202
+ if data[:id]
203
+ update data
204
+ else
205
+ create data do |item|
206
+ trigger :created, id: item[:id]
207
+ end
208
+ end
209
+ end
210
+
211
+ def render
212
+ preview.div.html = `marked(#{data[:body].to_s}, { gfm: true, breaks: true })`
213
+ end
214
+ end
215
+
216
+ class Main < UI::Container
217
+ extend Forwardable
218
+ include UI::Behaviors::Actions
219
+ include UI::Behaviors::Rest
220
+ include UI::Behaviors::State
221
+ include UI::Behaviors::Confirmation
222
+
223
+ rest url: 'http://localhost:3000/posts'
224
+
225
+ tag 'main'
226
+
227
+ style height: '100vh',
228
+ boxSizing: 'border-box',
229
+ fontSize: 14.px,
230
+ padding: -> { theme.spacing.em }
231
+
232
+ component :header, Header
233
+ component :content, UI::Container, direction: :row, flex: 1, compact: true do
234
+ component :posts, Posts, flex: 1, direction: :row
235
+ component :form, Form, flex: 1
236
+ end
237
+
238
+ state_changed :state_changed
239
+
240
+ confirmation :destroy!, 'Are you sure you want to remove this post?'
241
+
242
+ on :created, :created
243
+
244
+ def state_changed
245
+ if state.key?(:id)
246
+ load(state[:id])
247
+ else
248
+ @content.posts.refresh
249
+ @content.posts.show
250
+ @content.form.hide
251
+ end
252
+ end
253
+
254
+ def state
255
+ super.to_h
256
+ end
257
+
258
+ def content
259
+ self.state = {}
260
+ end
261
+
262
+ def destroy!
263
+ request :delete, @content.posts.selected.data[:id] do
264
+ @content.posts.refresh
265
+ end
266
+ end
267
+
268
+ def edit
269
+ self.state = state.merge!(id: @content.posts.selected.data[:id])
270
+ end
271
+
272
+ def load(id)
273
+ @content.form.load id do
274
+ @content.posts.hide
275
+ @content.form.show
276
+ end
277
+ end
278
+
279
+ def create
280
+ self.state = { id: nil }
281
+ end
282
+
283
+ def created(event)
284
+ self.state = { id: event.id }
285
+ end
286
+ end
287
+
288
+ Fron::Sheet.render_style_tag
289
+ DOM::Document.body << Main.new