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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +38 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/.yardopts +8 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +105 -0
- data/Rakefile +37 -0
- data/Readme.md +4 -0
- data/db.json +192 -0
- data/fron-ui.gemspec +21 -0
- data/lib/fron-ui.rb +1 -0
- data/lib/fron_ui.rb +5 -0
- data/lib/fron_ui/version.rb +7 -0
- data/opal/fron-ui/base.rb +49 -0
- data/opal/fron-ui/behaviors/action.rb +40 -0
- data/opal/fron-ui/behaviors/actions.rb +40 -0
- data/opal/fron-ui/behaviors/confirmation.rb +23 -0
- data/opal/fron-ui/behaviors/dropdown.rb +27 -0
- data/opal/fron-ui/behaviors/file.rb +48 -0
- data/opal/fron-ui/behaviors/intendable_children.rb +76 -0
- data/opal/fron-ui/behaviors/keydown.rb +31 -0
- data/opal/fron-ui/behaviors/loop.rb +41 -0
- data/opal/fron-ui/behaviors/render.rb +30 -0
- data/opal/fron-ui/behaviors/rest.rb +121 -0
- data/opal/fron-ui/behaviors/selectable_children.rb +67 -0
- data/opal/fron-ui/behaviors/serialize.rb +32 -0
- data/opal/fron-ui/behaviors/shortcuts.rb +35 -0
- data/opal/fron-ui/behaviors/state.rb +56 -0
- data/opal/fron-ui/behaviors/transition.rb +63 -0
- data/opal/fron-ui/components/action.rb +18 -0
- data/opal/fron-ui/components/box.rb +17 -0
- data/opal/fron-ui/components/button.rb +61 -0
- data/opal/fron-ui/components/calendar.rb +129 -0
- data/opal/fron-ui/components/checkbox.rb +57 -0
- data/opal/fron-ui/components/chooser.rb +246 -0
- data/opal/fron-ui/components/color_panel.rb +235 -0
- data/opal/fron-ui/components/color_picker.rb +111 -0
- data/opal/fron-ui/components/container.rb +61 -0
- data/opal/fron-ui/components/date_picker.rb +141 -0
- data/opal/fron-ui/components/drag.rb +76 -0
- data/opal/fron-ui/components/dropdown.rb +72 -0
- data/opal/fron-ui/components/icon.rb +29 -0
- data/opal/fron-ui/components/image.rb +77 -0
- data/opal/fron-ui/components/input.rb +30 -0
- data/opal/fron-ui/components/label.rb +9 -0
- data/opal/fron-ui/components/list.rb +34 -0
- data/opal/fron-ui/components/loader.rb +63 -0
- data/opal/fron-ui/components/modal.rb +0 -0
- data/opal/fron-ui/components/notifications.rb +73 -0
- data/opal/fron-ui/components/number.rb +202 -0
- data/opal/fron-ui/components/progress.rb +52 -0
- data/opal/fron-ui/components/slider.rb +47 -0
- data/opal/fron-ui/components/tabs.rb +149 -0
- data/opal/fron-ui/components/textarea.rb +13 -0
- data/opal/fron-ui/components/time.rb +65 -0
- data/opal/fron-ui/components/title.rb +34 -0
- data/opal/fron-ui/examples/blog/index.rb +289 -0
- data/opal/fron-ui/examples/comments/components/comment.rb +75 -0
- data/opal/fron-ui/examples/comments/components/comments.rb +93 -0
- data/opal/fron-ui/examples/comments/components/footer.rb +36 -0
- data/opal/fron-ui/examples/comments/components/header.rb +35 -0
- data/opal/fron-ui/examples/comments/components/list.rb +12 -0
- data/opal/fron-ui/examples/comments/index.rb +6 -0
- data/opal/fron-ui/examples/contacts/components/contacts.rb +100 -0
- data/opal/fron-ui/examples/contacts/components/details.rb +92 -0
- data/opal/fron-ui/examples/contacts/components/item.rb +46 -0
- data/opal/fron-ui/examples/contacts/components/list.rb +10 -0
- data/opal/fron-ui/examples/contacts/components/sidebar.rb +30 -0
- data/opal/fron-ui/examples/contacts/index.rb +6 -0
- data/opal/fron-ui/examples/editor/index.rb +164 -0
- data/opal/fron-ui/examples/kitchensink/index.rb +193 -0
- data/opal/fron-ui/examples/todos/components/item.rb +84 -0
- data/opal/fron-ui/examples/todos/components/options.rb +26 -0
- data/opal/fron-ui/examples/todos/components/todos.rb +145 -0
- data/opal/fron-ui/examples/todos/index.rb +6 -0
- data/opal/fron-ui/examples/webshop/index.rb +0 -0
- data/opal/fron-ui/fonts/ionicons.rb +2954 -0
- data/opal/fron-ui/fonts/open_sans.rb +19 -0
- data/opal/fron-ui/lib/collection.rb +138 -0
- data/opal/fron-ui/lib/date.rb +23 -0
- data/opal/fron-ui/lib/debounce.rb +14 -0
- data/opal/fron-ui/lib/image_loader.rb +13 -0
- data/opal/fron-ui/lib/lorem.rb +93 -0
- data/opal/fron-ui/lib/nil.rb +29 -0
- data/opal/fron-ui/lib/record.rb +23 -0
- data/opal/fron-ui/lib/state_serializer.rb +129 -0
- data/opal/fron-ui/lib/storage.rb +57 -0
- data/opal/fron-ui/spec/setup.rb +40 -0
- data/opal/fron-ui/ui.rb +40 -0
- data/opal/fron-ui/utils/theme_roller.rb +63 -0
- data/opal/fron-ui/vendor/autoprefixer.js +21114 -0
- data/opal/fron-ui/vendor/marked.js +1291 -0
- data/opal/fron-ui/vendor/md5.js +274 -0
- data/opal/fron-ui/vendor/moment.js +3083 -0
- data/opal/fron-ui/vendor/uuid.js +92 -0
- data/opal/fron_ui.rb +13 -0
- data/spec/behaviors/action_spec.rb +34 -0
- data/spec/behaviors/actions_spec.rb +38 -0
- data/spec/behaviors/confirmation_spec.rb +23 -0
- data/spec/behaviors/dropdown_spec.rb +32 -0
- data/spec/behaviors/render_spec.rb +20 -0
- data/spec/behaviors/rest_spec.rb +70 -0
- data/spec/behaviors/selectable_children_spec.rb +40 -0
- data/spec/behaviors/serialize_spec.rb +34 -0
- data/spec/components/action_spec.rb +7 -0
- data/spec/components/base_spec.rb +19 -0
- data/spec/components/box_spec.rb +7 -0
- data/spec/components/button_spec.rb +9 -0
- data/spec/components/calendar_spec.rb +58 -0
- data/spec/components/checkbox_spec.rb +20 -0
- data/spec/components/chooser_spec.rb +75 -0
- data/spec/components/color_panel_spec.rb +49 -0
- data/spec/components/color_picker_spec.rb +41 -0
- data/spec/components/container_spec.rb +23 -0
- data/spec/components/date_picker_spec.rb +71 -0
- data/spec/components/drag_spec.rb +20 -0
- data/spec/components/dropdown_spec.rb +33 -0
- data/spec/components/image_spec.rb +33 -0
- data/spec/components/input_spec.rb +8 -0
- data/spec/components/list_spec.rb +10 -0
- data/spec/components/loader_spec.rb +9 -0
- data/spec/components/notifications_spec.rb +17 -0
- data/spec/components/number_spec.rb +64 -0
- data/spec/components/progress_spec.rb +23 -0
- data/spec/components/slider_spec.rb +25 -0
- data/spec/components/tabs_spec.rb +50 -0
- data/spec/components/textarea_spec.rb +7 -0
- data/spec/components/time_spec.rb +37 -0
- data/spec/components/title_spec.rb +11 -0
- data/spec/examples/comments_spec.rb +72 -0
- data/spec/examples/todos_spec.rb +39 -0
- data/spec/lib/collection_spec.rb +38 -0
- data/spec/lib/lorem_spec.rb +55 -0
- data/spec/lib/state_serializer_spec.rb +58 -0
- data/spec/lib/storage_spec.rb +39 -0
- data/spec/spec_helper.rb +1 -0
- 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,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
|