fron-ui 1.0.0rc2

Sign up to get free protection for your applications and to get access to all the features.
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