lookbook 0.4.8 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -21
  3. data/app/assets/lookbook/css/app.css +24 -13
  4. data/app/assets/lookbook/css/tooltip_theme.css +28 -0
  5. data/app/assets/lookbook/js/app.js +4 -0
  6. data/app/assets/lookbook/js/components/code.js +5 -0
  7. data/app/assets/lookbook/js/components/copy.js +4 -2
  8. data/app/assets/lookbook/js/components/filter.js +1 -1
  9. data/app/assets/lookbook/js/components/inspector.js +54 -9
  10. data/app/assets/lookbook/js/components/nav-group.js +1 -1
  11. data/app/assets/lookbook/js/components/nav-item.js +1 -0
  12. data/app/assets/lookbook/js/components/nav.js +1 -1
  13. data/app/assets/lookbook/js/components/page.js +17 -5
  14. data/app/assets/lookbook/js/components/param.js +23 -7
  15. data/app/assets/lookbook/js/components/preview-window.js +95 -26
  16. data/app/assets/lookbook/js/components/tabs.js +50 -0
  17. data/app/assets/lookbook/js/config.js +11 -4
  18. data/app/assets/lookbook/js/lib/socket.js +1 -1
  19. data/app/assets/lookbook/js/stores/inspector.js +13 -5
  20. data/app/controllers/lookbook/app_controller.rb +23 -9
  21. data/app/views/layouts/lookbook/app.html.erb +9 -3
  22. data/app/views/lookbook/components/_code.html.erb +6 -1
  23. data/app/views/lookbook/components/_drawer.html.erb +124 -0
  24. data/app/views/lookbook/components/_filter.html.erb +1 -1
  25. data/app/views/lookbook/components/_header.html.erb +2 -2
  26. data/app/views/lookbook/components/_nav.html.erb +1 -1
  27. data/app/views/lookbook/components/_nav_group.html.erb +11 -14
  28. data/app/views/lookbook/components/_nav_item.html.erb +17 -15
  29. data/app/views/lookbook/components/_nav_preview.html.erb +4 -2
  30. data/app/views/lookbook/components/_param.html.erb +6 -5
  31. data/app/views/lookbook/components/_preview.html.erb +67 -20
  32. data/app/views/lookbook/inputs/_select.html.erb +2 -3
  33. data/app/views/lookbook/inputs/_text.html.erb +3 -3
  34. data/app/views/lookbook/inputs/_textarea.html.erb +3 -3
  35. data/app/views/lookbook/inputs/_toggle.html.erb +5 -5
  36. data/app/views/lookbook/panels/_notes.html.erb +1 -1
  37. data/app/views/lookbook/panels/_output.html.erb +2 -2
  38. data/app/views/lookbook/panels/_params.html.erb +1 -1
  39. data/app/views/lookbook/panels/_preview.html.erb +52 -0
  40. data/app/views/lookbook/panels/_source.html.erb +2 -2
  41. data/app/views/lookbook/show.html.erb +22 -88
  42. data/lib/lookbook/code_formatter.rb +3 -3
  43. data/lib/lookbook/features.rb +1 -1
  44. data/lib/lookbook/preview.rb +1 -1
  45. data/lib/lookbook/version.rb +1 -1
  46. data/public/lookbook-assets/css/app.css +3 -1
  47. data/public/lookbook-assets/css/app.css.map +1 -1
  48. data/public/lookbook-assets/js/app.js +1 -1
  49. data/public/lookbook-assets/js/app.js.map +1 -1
  50. metadata +6 -3
  51. data/app/views/lookbook/components/_copy.html.erb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a4c7c01727ccbd4cc232198f9bcc33236f0fa90618d10944b78b1858d750fe2
4
- data.tar.gz: a7615d2ca70fad9bb2b05670f0bc120245b14137b18e9673b5d5fb73bd6c9bce
3
+ metadata.gz: 0bf7750f26cce0e0b6517443da61e75a2d954b50c4882637a21fdac8b7d9c520
4
+ data.tar.gz: bb81e6a5cbb6db75b62591d6c233c1f1831db92e04045eafd372ae60264cc9fc
5
5
  SHA512:
6
- metadata.gz: 89ae1a5899bfd6bd848ea8bebc895d85b0d88a9b4d66914f52ddf04de24a7d566b0deeb0239e51d857afac2f4e1d2706aab77098191419d2b6e0c52df946b4b1
7
- data.tar.gz: 48729571a609a5505095f11d69b4ddb53735eeb4a6f26b83ec326a8f86ad346b17c7b66087efbd87a0002737ec000efbeefbba1acf56560d87be1a667b3d24d3
6
+ metadata.gz: a569c06a48d6b30b8f328f25ad1eecfa474427d61dbf5a91395c780593329bb4424fdec52cb71d332dd229edc3d2031d9a9af84e864a4e906dcfcd7d387ea953
7
+ data.tar.gz: 1316810b7e32c5fda3ebd96b0d23566654f0a274fdaa730ae0ac50d5eaa03d0ed093ab412f504b1b0176cb37ecb26258d27f4aac081b6bce13bc754b6555c62d
data/README.md CHANGED
@@ -28,7 +28,7 @@ Lookbook uses [RDoc/Yard-style comment tags](#annotating-preview-files) to exten
28
28
  - Auto-updating UI when component or preview files are updated _(Rails v6.0+ only)_
29
29
  - Use comment tag annotations for granular customisation of the preview experience
30
30
  - Fully compatible with standard the ViewComponent preview system
31
- - [**Experimental**] In-browser live editable preview parameters (similar to Storybook Controls/Knobs)
31
+ - In-browser live-editable preview parameters (similar to basic Storybook Controls/Knobs)
32
32
 
33
33
  ## Lookbook demo
34
34
 
@@ -63,10 +63,16 @@ end
63
63
 
64
64
  The `at` property determines the root URL that the Lookbook UI will be served at.
65
65
 
66
- > If you would like to expose the Lookbook UI in production as well as in development, just remove the `if Rails.env.development?` condition from around the mount statement.
67
-
68
66
  Then you can start your app as normal and navigate to `http://localhost:3000/lookbook` (or whatever mount path you specified) to view your component previews in the Lookbook UI.
69
67
 
68
+ #### Mounting in Production
69
+
70
+ If you would like to expose the Lookbook UI in production as well as in development
71
+
72
+ 1. Remove the `if Rails.env.development?` condition from around the mount statement in `routes.rb`
73
+ 2. Add `config.view_component.show_previews = true` to `config/environments/production.rb`
74
+
75
+
70
76
  ## Usage
71
77
 
72
78
  You don't need to do anything special to see your ViewComponent previews and examples in Lookbook - just create them as normal and they'll automatically appear in the Lookbook UI. Preview templates, custom layouts and even bespoke [preview controllers](https://viewcomponent.org/guide/previews.html#configuring-preview-controller) should all work as you would expect.
@@ -79,7 +85,7 @@ Lookbook parses [Yard-style comment tags](https://rubydoc.info/gems/yard/file/do
79
85
 
80
86
  ```ruby
81
87
  # @label Basic Button
82
- # @display bg_color #fff
88
+ # @display bg_color "#fff"
83
89
  class ButtonComponentPreview < ViewComponent::Preview
84
90
 
85
91
  # Primary button
@@ -96,7 +102,7 @@ class ButtonComponentPreview < ViewComponent::Preview
96
102
  # Button with icon
97
103
  # ----------------
98
104
  # This example uses dynamic preview parameters
99
- # which can be edited live in the Lookbook UI
105
+ # which can be edited live in the Lookbook UI
100
106
  #
101
107
  # @param text
102
108
  # @param icon select [heart, cog, alert]
@@ -110,7 +116,7 @@ class ButtonComponentPreview < ViewComponent::Preview
110
116
  # ---------------
111
117
  # For light-on-dark screens
112
118
  #
113
- # @display bg_color #000
119
+ # @display bg_color "#000"
114
120
  def secondary
115
121
  render ButtonComponent.new(style: :inverted) do
116
122
  "Click me"
@@ -161,7 +167,7 @@ The following Lookbook-specific tags are available for use:
161
167
  * [`@display`](#display-tag)
162
168
  * [`@!group ... @!endgroup`](#group-tag)
163
169
  * [`@hidden`](#hidden-tag)
164
- * [`@param`](#param-tag) [⚠️ **experimental!** - requires [feature opt-in](#experimental-features) ⚠️]
170
+ * [`@param`](#param-tag)
165
171
 
166
172
  <h3 id="label-tag">🏷 @label</h3>
167
173
 
@@ -188,7 +194,7 @@ end
188
194
  The `@display` tag lets you pass custom parameters to your preview layout so that the component preview can be customised on a per-example basis.
189
195
 
190
196
  ```ruby
191
- # @display bg_color #eee
197
+ # @display bg_color "#eee"
192
198
  class FooComponentPreview < ViewComponent::Preview
193
199
 
194
200
  # @display max_width 500px
@@ -207,6 +213,8 @@ The `@display` tag can be applied at the preview (class) or at the example (meth
207
213
  - `<key>` must be a valid Ruby hash key name, without quotes or spaces
208
214
  - `<value>` will be parsed using the [Ruby YAML parser](https://yaml.org/YAML_for_ruby.html) to resolve the value
209
215
 
216
+ > Note: Ruby YAML does not (generally) require quoting of string values. However in some cases it _is_ required due to the presence of [indicator characters](https://yaml.org/YAML_for_ruby.html#indicators_in_strings) (such as `#`, `:` etc) - hence why the hex color code in the example above is surrounded by quotes. It's perfectly ok to quote all string values if you prefer.
217
+
210
218
  These display parameters can then be accessed via the `params` hash in your preview layout using `params[:lookbook][:display][<key>]`:
211
219
 
212
220
  ```html
@@ -309,9 +317,7 @@ class FooComponentPreview < ViewComponent::Preview
309
317
  end
310
318
  ```
311
319
 
312
- <h3 id="param-tag"> 🚧 @param (experimental)</h3>
313
-
314
- > ⚠️ This feature is currently flagged as an **experimental** feature which requires [feature opt-in](#experimental-features) to use. Its API and implementation may change in the future.
320
+ <h3 id="param-tag">@param</h3>
315
321
 
316
322
  The `@param` tag provides the ability to specify **editable preview parameters** which can be changed in the Lookbook UI in order to customise the rendered output on the fly, much like the [Controls (knobs) addon](https://storybook.js.org/addons/@storybook/addon-controls) for Storybook.
317
323
 
@@ -324,7 +330,7 @@ The `@param` tag takes the following format:
324
330
  ```
325
331
 
326
332
  - `<name>` - name of the dynamic preview param
327
- - `<input_type>` - input field type to generate in the UI
333
+ - `<input_type>` - input field type to generate in the UI
328
334
  - `<opts?>` - YAML-encoded field options, used for some field types
329
335
 
330
336
  #### Input types
@@ -355,7 +361,7 @@ The following **input field types** are available for use:
355
361
  @param <name> select <options>
356
362
  ```
357
363
 
358
- `<options>` should be a [YAML array](https://yaml.org/YAML_for_ruby.html#simple_inline_array) of options which must be formatted in the same style as the input for Rails' [`options_for_select`](https://apidock.com/rails/v6.0.0/ActionView/Helpers/FormOptionsHelper/options_for_select) helper:
364
+ `<options>` should be a [YAML array](https://yaml.org/YAML_for_ruby.html#simple_inline_array) of options which must be formatted in the same style as the input for Rails' [`options_for_select`](https://apidock.com/rails/v6.0.0/ActionView/Helpers/FormOptionsHelper/options_for_select) helper:
359
365
 
360
366
  ```ruby
361
367
  # Basic options:
@@ -435,7 +441,7 @@ The following structured types are also available but should be considered **exp
435
441
 
436
442
  ```ruby
437
443
  class ButtonComponentPreview < ViewComponent::Preview
438
-
444
+
439
445
  # The params defined below will be editable in the UI:
440
446
  #
441
447
  # @param content text
@@ -494,6 +500,17 @@ If you wish to add additional paths to listen for changes in, you can use the `l
494
500
  config.lookbook.listen_paths << Rails.root.join('app/other/directory')
495
501
  ```
496
502
 
503
+ ### Custom favicon
504
+
505
+ If you want to change the favicon used by the Lookbook UI, you can provide a path to your own (or a data-uri string) using the `ui_favicon` option:
506
+
507
+ ```ruby
508
+ config.lookbook.ui_favicon = "/path/to/my/favicon.png"
509
+ ```
510
+
511
+ > To disable the favicon entirely, set the value to `false`.
512
+
513
+
497
514
  <h3 id="experimental-features">Experimental features opt-in</h3>
498
515
 
499
516
  Some features may occasionally be released behind a 'experimental' feature flag while they are being tested and refined, to allow people to try them out and provide feedback.
@@ -508,10 +525,6 @@ To opt into individual experimental features, include the name of the feature in
508
525
  config.lookbook.experimental_features = ["feature_name"]
509
526
  ```
510
527
 
511
- The current experimental features that can be opted into are:
512
-
513
- - `params`: Live-editable, dynamic preview parameters ([read more](#param-tag)). Include `"params"` in the `experimental_features` config option to opt in.
514
-
515
528
  #### Opting into all experimental features (not recommended!)
516
529
 
517
530
  If you want to live life on the bleeding-edge you can opt-in to all current **and future** experimental features (usual caveats apply):
@@ -527,9 +540,10 @@ Lookbook provides a few keyboard shortcuts to help you quickly move around the U
527
540
 
528
541
  - `f` - move focus to the nav filter box
529
542
  - `Esc` [when focus is in nav filter box] - Clear contents if text is present, or return focus to the UI if the box is already empty
530
- - `s` - Switch to Source tab in the inspector
531
- - `o` - Switch to Output tab in the inspector
532
- - `n` - Switch to Notes tab in the inspector
543
+ - `s` - Switch to Source tab in the drawer
544
+ - `n` - Switch to Notes tab in the drawer
545
+ - `v` - Switch to the rendered preview
546
+ - `o` - Switch to the code preview
533
547
  - `r` - Refresh the preview (useful if using something like Faker to generate randomised data for the preview)
534
548
  - `w` - Open the standalone rendered preview in a new window
535
549
 
@@ -2,6 +2,7 @@
2
2
  @import "tailwindcss/components";
3
3
  @import "tailwindcss/utilities";
4
4
  @import "tippy.js/dist/tippy";
5
+ @import "tippy.js/dist/border";
5
6
  @import "code_theme";
6
7
  @import "tooltip_theme";
7
8
 
@@ -57,8 +58,8 @@
57
58
  }
58
59
 
59
60
  @layer components {
60
- #nav > ul > li {
61
- @apply py-1;
61
+ #nav > ul > li > div {
62
+ @apply py-1 border-b border-gray-300;
62
63
  }
63
64
 
64
65
  .nav-toggle {
@@ -77,8 +78,12 @@
77
78
  @apply block;
78
79
  }
79
80
 
81
+ .code.wrapped pre {
82
+ @apply whitespace-pre-wrap;
83
+ }
84
+
80
85
  .code .line {
81
- @apply flex items-center leading-relaxed;
86
+ @apply leading-relaxed;
82
87
  }
83
88
 
84
89
  .code.numbered {
@@ -87,35 +92,41 @@
87
92
 
88
93
  .code.numbered:before {
89
94
  content: "";
90
- left: calc(2.7em + 8px);
95
+ left: 2.7em;
91
96
  @apply absolute top-0 bottom-0 border-r border-gray-200;
92
97
  }
93
98
 
99
+ .code.numbered .line {
100
+ padding-left: calc(2.7em + 8px);
101
+ @apply relative;
102
+ }
103
+
94
104
  .code .line-number {
105
+ display: inline-block;
95
106
  width: calc(2.7em + 8px);
96
107
  padding-top: 3px;
97
108
  padding-bottom: 3px;
98
109
  padding-right: 8px;
99
110
  margin-right: 16px;
100
- @apply font-mono text-right text-gray-400 flex-none text-xs;
111
+ @apply font-mono text-right text-gray-400 flex-none text-xs absolute left-0;
101
112
  }
102
113
 
103
114
  .code .line-content {
104
115
  @apply flex-none pr-4;
105
116
  }
106
117
 
107
- /* .code .line:before {
108
- content: counter(line);
109
- width: calc(3em + 8px);
110
- padding-top: 2px;
111
- padding-bottom: 2px;
112
- padding-right: 8px;
113
- @apply font-mono inline-block text-right mr-4 text-gray-400 border-r border-gray-200;
114
- } */
118
+ .resize-handle {
119
+ @apply flex items-center justify-center h-full w-full border-gray-300 bg-white hover:bg-indigo-100 hover:bg-opacity-20 text-gray-400 hover:text-gray-700 transition select-none touch-none;
120
+ }
115
121
  }
116
122
 
117
123
  @layer utilities {
118
124
  .form-input {
119
125
  @apply border-gray-300 text-gray-700 focus:ring-indigo-300 focus:border-indigo-300 rounded-sm text-sm w-full;
120
126
  }
127
+
128
+ .checked-bg {
129
+ background-color: #ffffff;
130
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cg fill='%23f3f3f3' fill-opacity='1'%3E%3Cpath fill-rule='evenodd' d='M0 0h4v4H0V0zm4 4h4v4H4V4z'/%3E%3C/g%3E%3C/svg%3E");
131
+ }
121
132
  }
@@ -4,13 +4,41 @@
4
4
  &[data-placement^="top"] > .tippy-arrow::before {
5
5
  border-top-color: theme("colors.indigo.500");
6
6
  }
7
+
7
8
  &[data-placement^="bottom"] > .tippy-arrow::before {
8
9
  border-bottom-color: theme("colors.indigo.500");
9
10
  }
11
+
10
12
  &[data-placement^="left"] > .tippy-arrow::before {
11
13
  border-left-color: theme("colors.indigo.500");
12
14
  }
15
+
13
16
  &[data-placement^="right"] > .tippy-arrow::before {
14
17
  border-right-color: theme("colors.indigo.500");
15
18
  }
16
19
  }
20
+
21
+ .tippy-box[data-theme~="menu"] {
22
+ border: 1px solid theme("colors.gray.300");
23
+ @apply bg-white text-gray-600 shadow-md rounded;
24
+
25
+ & > .tippy-content {
26
+ padding: 0;
27
+ }
28
+
29
+ &[data-placement^="top"] > .tippy-arrow::before {
30
+ border-top-color: white;
31
+ }
32
+
33
+ &[data-placement^="bottom"] > .tippy-arrow::before {
34
+ border-bottom-color: white;
35
+ }
36
+
37
+ &[data-placement^="left"] > .tippy-arrow::before {
38
+ border-left-color: white;
39
+ }
40
+
41
+ &[data-placement^="right"] > .tippy-arrow::before {
42
+ border-right-color: white;
43
+ }
44
+ }
@@ -13,7 +13,9 @@ import nav from "./components/nav";
13
13
  import navItem from "./components/nav-item";
14
14
  import navGroup from "./components/nav-group";
15
15
  import splitter from "./components/splitter";
16
+ import tabs from "./components/tabs";
16
17
  import copy from "./components/copy";
18
+ import code from "./components/code";
17
19
  import sizes from "./components/sizes";
18
20
 
19
21
  import initFilterStore from "./stores/filter";
@@ -42,11 +44,13 @@ Alpine.data("page", page);
42
44
  Alpine.data("splitter", splitter);
43
45
  Alpine.data("previewWindow", previewWindow);
44
46
  Alpine.data("copy", copy);
47
+ Alpine.data("code", code);
45
48
  Alpine.data("inspector", inspector);
46
49
  Alpine.data("filter", filter);
47
50
  Alpine.data("param", param);
48
51
  Alpine.data("sizes", sizes);
49
52
  Alpine.data("nav", nav);
53
+ Alpine.data("tabs", tabs);
50
54
  Alpine.data("navItem", navItem);
51
55
  Alpine.data("navGroup", navGroup);
52
56
 
@@ -0,0 +1,5 @@
1
+ export default function code() {
2
+ return {
3
+ wrap: false,
4
+ };
5
+ }
@@ -1,7 +1,9 @@
1
- export default function copy(id) {
1
+ export default function copy() {
2
2
  return {
3
3
  get content() {
4
- const target = document.getElementById(id);
4
+ const target = document.getElementById(
5
+ this.$root.getAttribute("data-target")
6
+ );
5
7
  return (target ? target.innerHTML : "").trim();
6
8
  },
7
9
  done: false,
@@ -12,7 +12,7 @@ export default function filter() {
12
12
  this.$store.filter.raw = "";
13
13
  },
14
14
  focus($event) {
15
- if ($event.target.tagName === "INPUT") {
15
+ if ($event && $event.target.tagName === "INPUT") {
16
16
  return;
17
17
  }
18
18
  setTimeout(() => this.$refs.input.focus(), 0);
@@ -1,17 +1,62 @@
1
+ import sizeObserver from "./sizes";
2
+
1
3
  export default function inspector() {
2
4
  return {
3
- isActivePanel(panel) {
4
- return this.$store.inspector.panels.active == panel;
5
+ width: 0,
6
+ height: 0,
7
+ init() {
8
+ const ro = new ResizeObserver((entries) => {
9
+ const rect = entries[0].contentRect;
10
+ this.width = Math.round(rect.width);
11
+ this.height = Math.round(rect.height);
12
+ });
13
+ ro.observe(this.$el);
14
+ this.width = Math.round(this.$el.clientWidth);
15
+ this.height = Math.round(this.$el.clientHeight);
5
16
  },
6
- switchPanel(panel) {
7
- this.$store.inspector.panels.active = panel;
17
+ get orientation() {
18
+ return this.$store.inspector.drawer.orientation;
8
19
  },
9
- get showSource() {
10
- return this.$store.inspector.preview.source;
20
+ get view() {
21
+ return this.$store.inspector.preview.view;
11
22
  },
12
- toggleSource() {
13
- this.$store.inspector.preview.source =
14
- !this.$store.inspector.preview.source;
23
+ get horizontal() {
24
+ return this.canBeVertical ? this.orientation === "horizontal" : true;
25
+ },
26
+ get vertical() {
27
+ return !this.horizontal;
28
+ },
29
+ get canBeVertical() {
30
+ return this.width > 800;
31
+ },
32
+ get drawerHidden() {
33
+ return this.$store.inspector.drawer.hidden;
34
+ },
35
+ get maxDrawerHeight() {
36
+ return Math.round(this.height * 0.7);
37
+ },
38
+ get maxDrawerWidth() {
39
+ return Math.round(this.width * 0.7);
40
+ },
41
+ isActiveDrawerPanel(panel) {
42
+ return this.$store.inspector.drawer.panel === panel;
43
+ },
44
+ switchDrawerPanel(panel) {
45
+ this.$store.inspector.drawer.panel = panel;
46
+ },
47
+ isActivePreviewPanel(panel) {
48
+ return this.$store.inspector.preview.panel === panel;
49
+ },
50
+ switchPreviewPanel(panel) {
51
+ this.$store.inspector.preview.panel = panel;
52
+ },
53
+ toggleOrientation() {
54
+ this.$store.inspector.drawer.orientation =
55
+ this.orientation === "horizontal" ? "vertical" : "horizontal";
56
+ },
57
+ toggleDrawer() {
58
+ this.$store.inspector.drawer.hidden =
59
+ !this.$store.inspector.drawer.hidden;
15
60
  },
16
61
  preview: {
17
62
  width: null,
@@ -15,7 +15,7 @@ export default function navGroup() {
15
15
  },
16
16
  getChildren() {
17
17
  return this.$refs.items
18
- ? Array.from(this.$refs.items.querySelectorAll(":scope > li"))
18
+ ? Array.from(this.$refs.items.querySelectorAll(":scope > li > div"))
19
19
  : [];
20
20
  },
21
21
  navigateToFirstChild() {
@@ -15,6 +15,7 @@ export default function navItem(matchers) {
15
15
  this.$store.sidebar.open = false;
16
16
  },
17
17
  filter(text) {
18
+ this.hidden = false;
18
19
  if (text.length) {
19
20
  const matched = matchers.map((m) => m.includes(text));
20
21
  this.hidden = !matched.filter((m) => m).length;
@@ -22,7 +22,7 @@ export default function nav() {
22
22
  },
23
23
  getChildren() {
24
24
  return this.$refs.items
25
- ? Array.from(this.$refs.items.querySelectorAll(":scope > li"))
25
+ ? Array.from(this.$refs.items.querySelectorAll(":scope > li > div"))
26
26
  : [];
27
27
  },
28
28
  setActive() {
@@ -1,5 +1,21 @@
1
1
  import createSocket from "../lib/socket";
2
2
 
3
+ const morphOpts = {
4
+ key(el) {
5
+ return el.getAttribute("key") ? el.getAttribute("key") : el.id;
6
+ },
7
+ updating(el, toEl, childrenOnly, skip) {
8
+ if (
9
+ el.getAttribute &&
10
+ el.getAttribute("data-morph-strategy") === "replace"
11
+ ) {
12
+ el.innerHTML = toEl.innerHTML;
13
+ return skip();
14
+ }
15
+ },
16
+ lookahead: true,
17
+ };
18
+
3
19
  export default function page() {
4
20
  return {
5
21
  init() {
@@ -22,11 +38,7 @@ export default function page() {
22
38
  },
23
39
  morph(dom) {
24
40
  const pageHtml = dom.getElementById(this.$root.id).outerHTML;
25
- Alpine.morph(this.$root, pageHtml, {
26
- key(el) {
27
- return el.getAttribute("key") ? el.getAttribute("key") : el.id;
28
- },
29
- });
41
+ Alpine.morph(this.$root, pageHtml, morphOpts);
30
42
  this.$dispatch("page:morphed");
31
43
  },
32
44
  };
@@ -1,18 +1,34 @@
1
- export default function param() {
1
+ import debounce from "debounce";
2
+
3
+ export default function param(name, value, opts = {}) {
2
4
  return {
3
- setFocus() {
4
- if (this.$refs.input) {
5
- setTimeout(() => this.$refs.input.focus(), 0);
5
+ name,
6
+ value,
7
+ updating: false,
8
+ init() {
9
+ if (opts.debounce) {
10
+ this.$watch(
11
+ "value",
12
+ debounce(() => this.updateIfValid(), opts.debounce)
13
+ );
14
+ } else {
15
+ this.$watch("value", () => this.updateIfValid());
6
16
  }
7
17
  },
8
- update(name, value) {
18
+ setFocus() {
19
+ setTimeout(() => this.$root.focus(), 0);
20
+ },
21
+ updateIfValid() {
22
+ if (this.validate()) this.update();
23
+ },
24
+ update() {
9
25
  const searchParams = new URLSearchParams(window.location.search);
10
- searchParams.set(name, value);
26
+ searchParams.set(this.name, this.value);
11
27
  const path = location.href.replace(location.search, "");
12
28
  this.setLocation(`${path}?${searchParams.toString()}`);
13
29
  },
14
30
  validate() {
15
- return this.$el.reportValidity ? this.$el.reportValidity() : true;
31
+ return this.$root.reportValidity ? this.$root.reportValidity() : true;
16
32
  },
17
33
  };
18
34
  }
@@ -1,37 +1,106 @@
1
1
  export default function preview() {
2
2
  return {
3
- onResize(e) {
4
- const size =
5
- this.resizeStartSize - (this.resizeStartPosition - e.pageX) * 2;
6
- const parentSize = this.$root.parentElement.clientWidth;
7
- const percentSize = (Math.round(size) / parentSize) * 100;
8
- const minWidth = (300 / parentSize) * 100;
9
- this.$store.inspector.preview.width = `${Math.min(
10
- Math.max(percentSize, minWidth),
11
- 100
12
- )}%`;
3
+ get store() {
4
+ return this.$store.inspector.preview;
13
5
  },
14
- onResizeStart(e) {
6
+ get maxWidth() {
7
+ return this.store.width === "100%" ? "100%" : `${this.store.width}px`;
8
+ },
9
+ get maxHeight() {
10
+ return this.store.height === "100%" ? "100%" : `${this.store.height}px`;
11
+ },
12
+ get parentWidth() {
13
+ return Math.round(this.$root.parentElement.clientWidth);
14
+ },
15
+ get parentHeight() {
16
+ return Math.round(this.$root.parentElement.clientHeight);
17
+ },
18
+ start() {
15
19
  this.$store.layout.reflowing = true;
16
- this.onResize = this.onResize.bind(this);
17
- this.onResizeEnd = this.onResizeEnd.bind(this);
18
- this.resizeStartPosition = e.pageX;
19
- this.resizeStartSize = this.$root.clientWidth;
20
- window.addEventListener("pointermove", this.onResize);
21
- window.addEventListener("pointerup", this.onResizeEnd);
22
- },
23
- onResizeEnd() {
24
- window.removeEventListener("pointermove", this.onResize);
25
- window.removeEventListener("pointerup", this.onResizeEnd);
20
+ this.store.resizing = true;
21
+ },
22
+ end() {
26
23
  this.$store.layout.reflowing = false;
24
+ this.store.resizing = false;
25
+ },
26
+ onResizeStart(e) {
27
+ this.onResizeWidthStart(e);
28
+ this.onResizeHeightStart(e);
29
+ },
30
+ toggleFullSize() {
31
+ const { height, width } = this.store;
32
+ if (height === "100%" && width === "100%") {
33
+ this.toggleFullHeight();
34
+ this.toggleFullWidth();
35
+ } else {
36
+ if (height !== "100%") this.toggleFullHeight();
37
+ if (width !== "100%") this.toggleFullWidth();
38
+ }
39
+ },
40
+ onResizeWidth(e) {
41
+ const width =
42
+ this.resizeStartWidth - (this.resizeStartPositionX - e.pageX) * 2;
43
+ const boundedWidth = Math.min(
44
+ Math.max(Math.round(width), 200),
45
+ this.parentWidth
46
+ );
47
+ this.store.width =
48
+ boundedWidth === this.parentWidth ? "100%" : boundedWidth;
49
+ },
50
+ onResizeWidthStart(e) {
51
+ this.start();
52
+ this.onResizeWidth = this.onResizeWidth.bind(this);
53
+ this.onResizeWidthEnd = this.onResizeWidthEnd.bind(this);
54
+ this.resizeStartPositionX = e.pageX;
55
+ this.resizeStartWidth = this.$root.clientWidth;
56
+ window.addEventListener("pointermove", this.onResizeWidth);
57
+ window.addEventListener("pointerup", this.onResizeWidthEnd);
58
+ },
59
+ onResizeWidthEnd() {
60
+ window.removeEventListener("pointermove", this.onResizeWidth);
61
+ window.removeEventListener("pointerup", this.onResizeWidthEnd);
62
+ this.end();
27
63
  },
28
64
  toggleFullWidth() {
29
- const preview = this.$store.inspector.preview;
30
- if (preview.width === "100%" && preview.lastWidth) {
31
- preview.width = preview.lastWidth;
65
+ const { width, lastWidth } = this.store;
66
+ if (width === "100%" && lastWidth) {
67
+ this.store.width = lastWidth;
68
+ } else {
69
+ this.store.lastWidth = width;
70
+ this.store.width = "100%";
71
+ }
72
+ },
73
+ onResizeHeight(e) {
74
+ const height =
75
+ this.resizeStartHeight - (this.resizeStartPositionY - e.pageY);
76
+ const boundedHeight = Math.min(
77
+ Math.max(Math.round(height), 200),
78
+ this.parentHeight
79
+ );
80
+ this.$store.inspector.preview.height =
81
+ boundedHeight === this.parentHeight ? "100%" : boundedHeight;
82
+ },
83
+ onResizeHeightStart(e) {
84
+ this.start();
85
+ this.onResizeHeight = this.onResizeHeight.bind(this);
86
+ this.onResizeHeightEnd = this.onResizeHeightEnd.bind(this);
87
+ this.resizeStartPositionY = e.pageY;
88
+ this.resizeStartHeight = this.$root.clientHeight;
89
+ window.addEventListener("pointermove", this.onResizeHeight);
90
+ window.addEventListener("pointerup", this.onResizeHeightEnd);
91
+ },
92
+ onResizeHeightEnd() {
93
+ window.removeEventListener("pointermove", this.onResizeHeight);
94
+ window.removeEventListener("pointerup", this.onResizeHeightEnd);
95
+ this.end();
96
+ },
97
+ toggleFullHeight() {
98
+ const { height, lastHeight } = this.store;
99
+ if (height === "100%" && lastHeight) {
100
+ this.store.height = lastHeight;
32
101
  } else {
33
- preview.lastWidth = preview.width;
34
- preview.width = "100%";
102
+ this.store.lastHeight = height;
103
+ this.store.height = "100%";
35
104
  }
36
105
  },
37
106
  };