lookbook 0.4.8 → 0.5.0

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 (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
  };