@perspective-dev/viewer 4.3.0 → 4.4.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 (227) hide show
  1. package/dist/cdn/perspective-viewer.js +2 -2
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/pro-dark.css +1 -1
  17. package/dist/css/pro.css +1 -1
  18. package/dist/css/solarized-dark.css +1 -1
  19. package/dist/css/solarized.css +1 -1
  20. package/dist/css/themes.css +1 -1
  21. package/dist/css/vaporwave.css +1 -1
  22. package/dist/esm/perspective-viewer.inline.js +2 -2
  23. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  24. package/dist/esm/perspective-viewer.js +2 -2
  25. package/dist/esm/perspective-viewer.js.map +4 -4
  26. package/dist/wasm/perspective-viewer.d.ts +57 -53
  27. package/dist/wasm/perspective-viewer.js +190 -165
  28. package/dist/wasm/perspective-viewer.wasm +0 -0
  29. package/dist/wasm/perspective-viewer.wasm.d.ts +17 -18
  30. package/package.json +7 -5
  31. package/src/{less/aggregate-selector.less → css/aggregate-selector.css} +23 -20
  32. package/src/css/column-dropdown.css +109 -0
  33. package/src/{less/column-selector.less → css/column-selector.css} +160 -158
  34. package/src/{less/column-settings-panel.less → css/column-settings-panel.css} +69 -59
  35. package/src/{less/column-style.less → css/column-style.css} +52 -66
  36. package/src/{less/column-symbol-attributes.less → css/column-symbol-attributes.css} +15 -14
  37. package/src/{less/config-selector.less → css/config-selector.css} +151 -135
  38. package/src/{less/containers/dropdown-menu.less → css/containers/dropdown-menu.css} +20 -19
  39. package/src/{less/containers/pairs-list.less → css/containers/pairs-list.css} +13 -12
  40. package/src/{themes/variables.less → css/containers/scroll-panel.css} +25 -22
  41. package/src/{less/containers/split-panel.less → css/containers/split-panel.css} +15 -14
  42. package/src/{less/containers/tabs.less → css/containers/tabs.css} +17 -19
  43. package/src/css/dom/checkbox.css +102 -0
  44. package/src/css/dom/scrollbar.css +35 -0
  45. package/src/{less/dom/select.less → css/dom/select.css} +17 -18
  46. package/src/{less/empty-column.less → css/empty-column.css} +19 -18
  47. package/src/{less/expression-editor.less → css/expression-editor.css} +19 -18
  48. package/src/{less/filter-dropdown.less → css/filter-dropdown.css} +12 -11
  49. package/src/{less/filter-item.less → css/filter-item.css} +16 -15
  50. package/src/{less/form/code-editor.less → css/form/code-editor.css} +26 -30
  51. package/src/{less/form/debug.less → css/form/debug.css} +19 -18
  52. package/src/{less/function-dropdown.less → css/function-dropdown.css} +12 -11
  53. package/src/css/plugin-selector.css +261 -0
  54. package/src/{less/render-warning.less → css/render-warning.css} +18 -17
  55. package/src/{less/status-bar.less → css/status-bar.css} +156 -144
  56. package/src/css/type-icon.css +116 -0
  57. package/src/{less/viewer.less → css/viewer.css} +112 -146
  58. package/src/rust/components/column_dropdown.rs +229 -119
  59. package/src/rust/components/column_selector/active_column.rs +81 -62
  60. package/src/rust/components/column_selector/add_expression_button.rs +1 -0
  61. package/src/rust/components/column_selector/aggregate_selector.rs +25 -15
  62. package/src/rust/components/column_selector/config_selector.rs +315 -199
  63. package/src/rust/components/column_selector/empty_column.rs +2 -2
  64. package/src/rust/components/column_selector/expr_edit_button.rs +8 -2
  65. package/src/rust/components/column_selector/filter_column.rs +37 -26
  66. package/src/rust/components/column_selector/inactive_column.rs +41 -29
  67. package/src/rust/components/column_selector/invalid_column.rs +7 -18
  68. package/src/rust/components/column_selector/pivot_column.rs +11 -5
  69. package/src/rust/components/column_selector/sort_column.rs +23 -13
  70. package/src/rust/components/column_selector.rs +163 -84
  71. package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +1 -1
  72. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +3 -2
  73. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +3 -2
  74. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +2 -3
  75. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +7 -1
  76. package/src/rust/components/column_settings_sidebar/style_tab.rs +153 -112
  77. package/src/rust/components/column_settings_sidebar.rs +91 -53
  78. package/src/rust/components/containers/dragdrop_list.rs +2 -1
  79. package/src/rust/components/containers/sidebar_close_button.rs +1 -1
  80. package/src/rust/components/containers/split_panel.rs +1 -0
  81. package/src/rust/components/containers/tab_list.rs +1 -1
  82. package/src/rust/components/copy_dropdown.rs +7 -28
  83. package/src/rust/components/datetime_column_style/custom.rs +2 -2
  84. package/src/rust/components/datetime_column_style/simple.rs +2 -2
  85. package/src/rust/components/datetime_column_style.rs +4 -2
  86. package/src/rust/components/editable_header.rs +7 -4
  87. package/src/rust/components/empty_row.rs +1 -1
  88. package/src/rust/components/export_dropdown.rs +4 -30
  89. package/src/rust/components/expression_editor.rs +19 -10
  90. package/src/rust/components/filter_dropdown.rs +246 -102
  91. package/src/rust/components/font_loader.rs +11 -28
  92. package/src/rust/components/form/code_editor.rs +17 -2
  93. package/src/rust/components/form/color_range_selector.rs +19 -6
  94. package/src/rust/components/form/debug.rs +30 -13
  95. package/src/rust/components/function_dropdown.rs +186 -113
  96. package/src/rust/components/main_panel.rs +71 -89
  97. package/src/rust/components/mod.rs +1 -1
  98. package/src/rust/components/modal.rs +7 -1
  99. package/src/rust/components/number_column_style.rs +22 -7
  100. package/src/rust/components/plugin_selector.rs +34 -102
  101. package/src/rust/components/portal.rs +274 -0
  102. package/src/rust/components/render_warning.rs +72 -123
  103. package/src/rust/components/settings_panel.rs +115 -11
  104. package/src/rust/components/status_bar.rs +222 -98
  105. package/src/rust/components/status_bar_counter.rs +8 -20
  106. package/src/rust/components/status_indicator.rs +64 -114
  107. package/src/rust/components/string_column_style.rs +2 -2
  108. package/src/rust/components/style/style_cache.rs +5 -1
  109. package/src/rust/components/viewer.rs +391 -39
  110. package/src/rust/custom_elements/copy_dropdown.rs +102 -21
  111. package/src/rust/custom_elements/export_dropdown.rs +102 -20
  112. package/src/rust/custom_elements/mod.rs +0 -7
  113. package/src/rust/custom_elements/modal.rs +7 -103
  114. package/src/rust/custom_elements/viewer.rs +99 -35
  115. package/src/rust/custom_events.rs +23 -2
  116. package/src/rust/dragdrop.rs +149 -10
  117. package/src/{less/containers/scroll-panel.less → rust/engines.rs} +15 -13
  118. package/src/rust/js/plugin.rs +1 -1
  119. package/src/rust/lib.rs +5 -4
  120. package/src/rust/presentation/props.rs +39 -0
  121. package/src/rust/presentation/sheets.rs +3 -3
  122. package/src/rust/presentation.rs +44 -8
  123. package/src/rust/renderer/limits.rs +32 -3
  124. package/src/{less/dom/scrollbar.less → rust/renderer/props.rs} +18 -19
  125. package/src/rust/renderer.rs +83 -9
  126. package/src/rust/session/column_defaults_update.rs +1 -1
  127. package/src/rust/session/metadata.rs +23 -2
  128. package/src/rust/session/props.rs +178 -0
  129. package/src/rust/session.rs +124 -117
  130. package/src/rust/tasks/column_locator.rs +133 -0
  131. package/src/rust/{model → tasks}/columns_iter_set.rs +14 -23
  132. package/src/rust/{model → tasks}/edit_expression.rs +34 -10
  133. package/src/rust/{model → tasks}/eject.rs +2 -2
  134. package/src/rust/{model → tasks}/get_viewer_config.rs +0 -11
  135. package/src/rust/{model → tasks}/intersection_observer.rs +19 -3
  136. package/src/{less/containers/radio-list.less → rust/tasks/is_invalid_drop.rs} +21 -14
  137. package/src/rust/tasks/mod.rs +52 -0
  138. package/src/rust/{model → tasks}/plugin_column_styles.rs +69 -46
  139. package/src/rust/{model → tasks}/resize_observer.rs +39 -6
  140. package/src/rust/{model → tasks}/send_plugin_config.rs +1 -1
  141. package/src/rust/tasks/structural.rs +53 -0
  142. package/src/rust/utils/mod.rs +4 -0
  143. package/src/rust/utils/modal_position.rs +110 -0
  144. package/src/rust/utils/ptr_eq_rc.rs +74 -0
  145. package/src/rust/utils/pubsub.rs +11 -1
  146. package/src/svg/bg-pattern.png +0 -0
  147. package/src/svg/close-icon.svg +1 -1
  148. package/src/svg/expression.svg +1 -1
  149. package/src/svg/mega-menu-icons-candlestick.svg +1 -1
  150. package/src/svg/mega-menu-icons-datagrid.svg +1 -2
  151. package/src/svg/mega-menu-icons-heatmap.svg +1 -1
  152. package/src/svg/mega-menu-icons-map-scatter.svg +1 -1
  153. package/src/svg/mega-menu-icons-ohlc.svg +1 -1
  154. package/src/svg/mega-menu-icons-sunburst.svg +1 -1
  155. package/src/svg/mega-menu-icons-treemap.svg +1 -1
  156. package/src/svg/mega-menu-icons-x-bar.svg +1 -1
  157. package/src/svg/mega-menu-icons-x-y-line.svg +1 -1
  158. package/src/svg/mega-menu-icons-x-y-scatter.svg +1 -1
  159. package/src/svg/mega-menu-icons-y-area.svg +1 -1
  160. package/src/svg/mega-menu-icons-y-bar.svg +1 -1
  161. package/src/svg/mega-menu-icons-y-line.svg +1 -1
  162. package/src/svg/mega-menu-icons-y-scatter.svg +1 -1
  163. package/src/svg/radio-hover.svg +1 -1
  164. package/src/svg/radio-off.svg +1 -1
  165. package/src/svg/radio-on.svg +1 -1
  166. package/src/themes/botanical.css +157 -0
  167. package/src/themes/defaults.css +139 -0
  168. package/src/themes/dracula.css +233 -0
  169. package/src/themes/gruvbox-dark.css +255 -0
  170. package/src/themes/gruvbox.css +134 -0
  171. package/src/themes/icons.css +124 -0
  172. package/src/themes/intl/de.css +102 -0
  173. package/src/themes/intl/es.css +102 -0
  174. package/src/themes/intl/fr.css +102 -0
  175. package/src/themes/intl/ja.css +102 -0
  176. package/src/themes/intl/pt.css +102 -0
  177. package/src/themes/intl/zh.css +102 -0
  178. package/src/themes/intl.css +102 -0
  179. package/src/themes/monokai.css +233 -0
  180. package/src/themes/pro-dark.css +158 -0
  181. package/src/themes/{themes.less → pro.css} +17 -21
  182. package/src/themes/solarized-dark.css +135 -0
  183. package/src/themes/solarized.css +95 -0
  184. package/src/themes/themes.css +22 -0
  185. package/src/themes/vaporwave.css +256 -0
  186. package/dist/css/variables.css +0 -0
  187. package/src/less/column-dropdown.less +0 -95
  188. package/src/less/dom/checkbox.less +0 -100
  189. package/src/less/plugin-selector.less +0 -183
  190. package/src/less/type-icon.less +0 -68
  191. package/src/rust/components/error_message.rs +0 -56
  192. package/src/rust/custom_elements/column_dropdown.rs +0 -123
  193. package/src/rust/custom_elements/filter_dropdown.rs +0 -179
  194. package/src/rust/custom_elements/function_dropdown.rs +0 -115
  195. package/src/rust/model/column_locator.rs +0 -82
  196. package/src/rust/model/is_invalid_drop.rs +0 -36
  197. package/src/rust/model/mod.rs +0 -100
  198. package/src/rust/model/reset_all.rs +0 -38
  199. package/src/rust/model/structural.rs +0 -244
  200. package/src/themes/botanical.less +0 -142
  201. package/src/themes/dracula.less +0 -101
  202. package/src/themes/gruvbox-dark.less +0 -116
  203. package/src/themes/gruvbox.less +0 -152
  204. package/src/themes/icons.less +0 -130
  205. package/src/themes/intl/de.less +0 -102
  206. package/src/themes/intl/es.less +0 -102
  207. package/src/themes/intl/fr.less +0 -102
  208. package/src/themes/intl/ja.less +0 -102
  209. package/src/themes/intl/pt.less +0 -102
  210. package/src/themes/intl/zh.less +0 -102
  211. package/src/themes/intl.less +0 -102
  212. package/src/themes/monokai.less +0 -107
  213. package/src/themes/pro-dark.less +0 -147
  214. package/src/themes/pro.less +0 -186
  215. package/src/themes/solarized-dark.less +0 -78
  216. package/src/themes/solarized.less +0 -102
  217. package/src/themes/vaporwave.less +0 -145
  218. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline0.js +0 -0
  219. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline1.js +0 -0
  220. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline2.js +0 -0
  221. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline3.js +0 -0
  222. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline4.js +0 -0
  223. /package/src/rust/{model → tasks}/copy_export.rs +0 -0
  224. /package/src/rust/{model → tasks}/export_app.rs +0 -0
  225. /package/src/rust/{model → tasks}/export_method.rs +0 -0
  226. /package/src/rust/{model → tasks}/restore_and_render.rs +0 -0
  227. /package/src/rust/{model → tasks}/update_and_render.rs +0 -0
@@ -16,13 +16,13 @@ use wasm_bindgen::prelude::*;
16
16
  use web_sys::*;
17
17
  use yew::prelude::*;
18
18
 
19
- use crate::PerspectiveProperties;
20
19
  use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg};
21
20
  use crate::js::*;
22
- use crate::model::*;
21
+ use crate::presentation::Presentation;
23
22
  use crate::renderer::*;
24
23
  use crate::root::Root;
25
- use crate::session::Session;
24
+ use crate::session::{Session, TableLoadState};
25
+ use crate::tasks::*;
26
26
  use crate::utils::*;
27
27
 
28
28
  pub struct ResizeObserverHandle {
@@ -36,6 +36,7 @@ impl ResizeObserverHandle {
36
36
  elem: &HtmlElement,
37
37
  renderer: &Renderer,
38
38
  session: &Session,
39
+ presentation: &Presentation,
39
40
  root: &Root<PerspectiveViewer>,
40
41
  ) -> Self {
41
42
  let on_resize = root
@@ -48,6 +49,7 @@ impl ResizeObserverHandle {
48
49
  elem: elem.clone(),
49
50
  renderer: renderer.clone(),
50
51
  session: session.clone(),
52
+ presentation: presentation.clone(),
51
53
  width: elem.offset_width(),
52
54
  height: elem.offset_height(),
53
55
  on_resize,
@@ -70,16 +72,43 @@ impl Drop for ResizeObserverHandle {
70
72
  }
71
73
  }
72
74
 
73
- #[derive(PerspectiveProperties!)]
75
+ #[derive(Clone)]
74
76
  struct ResizeObserverState {
75
77
  elem: HtmlElement,
76
78
  renderer: Renderer,
77
79
  session: Session,
80
+ presentation: Presentation,
78
81
  width: i32,
79
82
  height: i32,
80
83
  on_resize: Callback<()>,
81
84
  }
82
85
 
86
+ impl HasRenderer for ResizeObserverState {
87
+ fn renderer(&self) -> &Renderer {
88
+ &self.renderer
89
+ }
90
+ }
91
+
92
+ impl HasSession for ResizeObserverState {
93
+ fn session(&self) -> &Session {
94
+ &self.session
95
+ }
96
+ }
97
+
98
+ impl HasPresentation for ResizeObserverState {
99
+ fn presentation(&self) -> &'_ crate::presentation::Presentation {
100
+ &self.presentation
101
+ }
102
+ }
103
+
104
+ impl StateProvider for ResizeObserverState {
105
+ type State = ResizeObserverState;
106
+
107
+ fn clone_state(&self) -> Self::State {
108
+ self.clone()
109
+ }
110
+ }
111
+
83
112
  impl ResizeObserverState {
84
113
  fn on_resize(&mut self, entries: &js_sys::Array) {
85
114
  let is_visible = self
@@ -97,17 +126,21 @@ impl ResizeObserverState {
97
126
  if resized && is_visible {
98
127
  let state = self.clone_state();
99
128
  clone!(self.on_resize);
100
- ApiFuture::spawn(async move {
129
+ ApiFuture::spawn_throttled(async move {
101
130
  let needs_render = state
102
131
  .renderer()
103
132
  .clone()
104
133
  .with_lock(async {
105
134
  Ok(!state.renderer().is_plugin_activated()?
106
- && state.session().has_table())
135
+ && matches!(
136
+ state.session().has_table(),
137
+ Some(TableLoadState::Loaded)
138
+ ))
107
139
  })
108
140
  .await?;
109
141
 
110
142
  if needs_render {
143
+ state.presentation.reset_attached();
111
144
  state.update_and_render(Default::default())?.await?;
112
145
  } else {
113
146
  state.renderer().resize().await?;
@@ -13,7 +13,7 @@
13
13
  use perspective_js::utils::*;
14
14
 
15
15
  use crate::config::ColumnConfigValueUpdate;
16
- use crate::model::*;
16
+ use crate::tasks::*;
17
17
 
18
18
  pub trait SendPluginConfig {
19
19
  /// Update te urrent plugin with a [`ColumnonfigValueUpdate`]
@@ -0,0 +1,53 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ //! Structural typing traits that allow task methods to be automatically defined
14
+ //! for any struct that holds the necessary state handles.
15
+
16
+ use crate::custom_events::*;
17
+ use crate::dragdrop::*;
18
+ use crate::presentation::*;
19
+ use crate::renderer::*;
20
+ use crate::session::*;
21
+
22
+ pub trait HasCustomEvents {
23
+ fn custom_events(&self) -> &'_ CustomEvents;
24
+ }
25
+
26
+ pub trait HasDragDrop {
27
+ fn dragdrop(&self) -> &'_ DragDrop;
28
+ }
29
+
30
+ pub trait HasPresentation {
31
+ fn presentation(&self) -> &'_ Presentation;
32
+ }
33
+
34
+ pub trait HasRenderer {
35
+ fn renderer(&self) -> &'_ Renderer;
36
+ }
37
+
38
+ pub trait HasSession {
39
+ fn session(&self) -> &'_ Session;
40
+ }
41
+
42
+ impl HasSession for Session {
43
+ fn session(&self) -> &'_ Session {
44
+ self
45
+ }
46
+ }
47
+
48
+ pub trait StateProvider {
49
+ type State: Clone + 'static;
50
+
51
+ /// Clones just the state object fields into a new dedicated state struct.
52
+ fn clone_state(&self) -> Self::State;
53
+ }
@@ -21,7 +21,9 @@ mod custom_element;
21
21
  mod datetime;
22
22
  mod debounce;
23
23
  mod hooks;
24
+ mod modal_position;
24
25
  mod number_format;
26
+ mod ptr_eq_rc;
25
27
  mod pubsub;
26
28
  mod weak_scope;
27
29
 
@@ -33,8 +35,10 @@ pub use custom_element::*;
33
35
  pub use datetime::*;
34
36
  pub use debounce::*;
35
37
  pub use hooks::*;
38
+ pub use modal_position::*;
36
39
  pub use number_format::*;
37
40
  pub use perspective_client::clone;
41
+ pub use ptr_eq_rc::*;
38
42
  pub use pubsub::*;
39
43
  pub use weak_scope::*;
40
44
 
@@ -0,0 +1,110 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ use perspective_js::utils::global;
14
+ use web_sys::*;
15
+
16
+ /// Anchor point enum, `ModalCornerTargetCorner`
17
+ #[derive(Clone, Copy, Debug, Default)]
18
+ pub enum ModalAnchor {
19
+ BottomRightTopLeft,
20
+ BottomRightBottomLeft,
21
+ BottomRightTopRight,
22
+ BottomLeftTopLeft,
23
+ TopRightTopLeft,
24
+ TopRightBottomRight,
25
+
26
+ #[default]
27
+ TopLeftBottomLeft,
28
+ }
29
+
30
+ impl ModalAnchor {
31
+ pub const fn is_rev_vert(&self) -> bool {
32
+ matches!(
33
+ self,
34
+ Self::BottomLeftTopLeft
35
+ | Self::BottomRightBottomLeft
36
+ | Self::BottomRightTopLeft
37
+ | Self::BottomRightTopRight
38
+ )
39
+ }
40
+ }
41
+
42
+ /// Given the bounds of the target element as previously computed, as well as
43
+ /// the browser's viewport and the bounds of the already-connected modal element
44
+ /// itself, determine the best anchor point to keep the element on-screen.
45
+ pub fn calc_relative_position(
46
+ elem: &HtmlElement,
47
+ _top: f64,
48
+ left: f64,
49
+ height: f64,
50
+ width: f64,
51
+ ) -> ModalAnchor {
52
+ let window = global::window();
53
+ let rect = elem.get_bounding_client_rect();
54
+ let inner_width = window.inner_width().unwrap().as_f64().unwrap();
55
+ let inner_height = window.inner_height().unwrap().as_f64().unwrap();
56
+ let rect_top = rect.top();
57
+ let rect_height = rect.height();
58
+ let rect_width = rect.width();
59
+ let rect_left = rect.left();
60
+
61
+ let elem_over_y = inner_height < rect_top + rect_height;
62
+ let elem_over_x = inner_width < rect_left + rect_width;
63
+ let target_over_x = inner_width < rect_left + width;
64
+ let target_over_y = inner_height < rect_top + height;
65
+
66
+ // modal/target
67
+ match (elem_over_y, elem_over_x, target_over_x, target_over_y) {
68
+ (true, _, true, true) => ModalAnchor::BottomRightTopLeft,
69
+ (true, _, true, false) => ModalAnchor::BottomRightBottomLeft,
70
+ (true, true, false, _) => {
71
+ if left + width - rect_width > 0.0 {
72
+ ModalAnchor::BottomRightTopRight
73
+ } else {
74
+ ModalAnchor::BottomLeftTopLeft
75
+ }
76
+ },
77
+ (true, false, false, _) => ModalAnchor::BottomLeftTopLeft,
78
+ (false, true, true, _) => ModalAnchor::TopRightTopLeft,
79
+ (false, true, false, _) => {
80
+ if left + width - rect_width > 0.0 {
81
+ ModalAnchor::TopRightBottomRight
82
+ } else {
83
+ ModalAnchor::TopLeftBottomLeft
84
+ }
85
+ },
86
+ _ => ModalAnchor::TopLeftBottomLeft,
87
+ }
88
+ }
89
+
90
+ /// Calculate the (top, left) position for a modal element given an anchor
91
+ /// point, target element bounding rect, and the modal element's own bounding
92
+ /// rect.
93
+ pub fn calc_anchor_position(anchor: ModalAnchor, target: &DomRect, modal: &DomRect) -> (f64, f64) {
94
+ let height = target.height();
95
+ let width = target.width();
96
+ let top = target.top();
97
+ let left = target.left();
98
+ let rect_height = modal.height();
99
+ let rect_width = modal.width();
100
+
101
+ match anchor {
102
+ ModalAnchor::BottomRightTopLeft => (top - rect_height, left - rect_width + 1.0),
103
+ ModalAnchor::BottomRightBottomLeft => (top - rect_height + height, left - rect_width + 1.0),
104
+ ModalAnchor::BottomRightTopRight => (top - rect_height + 1.0, left + width - rect_width),
105
+ ModalAnchor::BottomLeftTopLeft => (top - rect_height + 1.0, left),
106
+ ModalAnchor::TopRightTopLeft => (top, left - rect_width + 1.0),
107
+ ModalAnchor::TopRightBottomRight => (top + height - 1.0, left + width - rect_width),
108
+ ModalAnchor::TopLeftBottomLeft => (top + height - 1.0, left),
109
+ }
110
+ }
@@ -0,0 +1,74 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ use std::ops::Deref;
14
+ use std::rc::Rc;
15
+
16
+ use yew::html::{ImplicitClone, IntoPropValue};
17
+
18
+ /// A thin wrapper around `Rc<T>` whose `PartialEq` uses pointer identity
19
+ /// (`Rc::ptr_eq`) instead of deep structural comparison. This makes it
20
+ /// suitable for Yew `Properties` fields that hold large, cheaply-shared
21
+ /// snapshots (e.g. `ViewConfig`, `SessionMetadata`, `Vec<String>`).
22
+ pub struct PtrEqRc<T>(Rc<T>);
23
+
24
+ impl<T> PtrEqRc<T> {
25
+ pub fn new(val: T) -> Self {
26
+ Self(Rc::new(val))
27
+ }
28
+ }
29
+
30
+ impl<T> Clone for PtrEqRc<T> {
31
+ fn clone(&self) -> Self {
32
+ Self(Rc::clone(&self.0))
33
+ }
34
+ }
35
+
36
+ impl<T> PartialEq for PtrEqRc<T> {
37
+ fn eq(&self, other: &Self) -> bool {
38
+ Rc::ptr_eq(&self.0, &other.0)
39
+ }
40
+ }
41
+
42
+ impl<T> Deref for PtrEqRc<T> {
43
+ type Target = T;
44
+
45
+ fn deref(&self) -> &T {
46
+ &self.0
47
+ }
48
+ }
49
+
50
+ impl<T> From<T> for PtrEqRc<T> {
51
+ fn from(rc: T) -> Self {
52
+ Self(Rc::new(rc))
53
+ }
54
+ }
55
+
56
+ impl<T: Default> Default for PtrEqRc<T> {
57
+ fn default() -> Self {
58
+ Self(Rc::default())
59
+ }
60
+ }
61
+
62
+ impl<T: std::fmt::Debug> std::fmt::Debug for PtrEqRc<T> {
63
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64
+ self.0.fmt(f)
65
+ }
66
+ }
67
+
68
+ impl<T> ImplicitClone for PtrEqRc<T> {}
69
+
70
+ impl<T> IntoPropValue<PtrEqRc<T>> for Rc<T> {
71
+ fn into_prop_value(self) -> PtrEqRc<T> {
72
+ PtrEqRc(self)
73
+ }
74
+ }
@@ -84,7 +84,9 @@ impl<T: Clone> PubSubInternal<T> {
84
84
  /// publishers, without leaking callbacks as listeners are dropped.
85
85
  ///
86
86
  /// Unlike `mpsc` etc., `PubSub` has no internal queue and is completely
87
- /// synchronous.
87
+ /// synchronous. Explicitly does not implement clone, as this is intended as
88
+ /// RAII, even though the internal data structures are `Clone` because they
89
+ /// need to be sent to listeners.
88
90
  #[derive(Derivative)]
89
91
  #[derivative(Default(bound = ""))]
90
92
  pub struct PubSub<T: Clone>(Rc<PubSubInternal<T>>);
@@ -116,6 +118,14 @@ impl<T: Clone + 'static> PubSub<T> {
116
118
  Callback::from(move |val: T| internal.emit(val))
117
119
  }
118
120
 
121
+ /// Subscribe a `Callback<()>` that fires whenever this PubSub emits,
122
+ /// discarding the emitted value. Useful when the subscriber only
123
+ /// needs a "something changed" notification.
124
+ pub fn add_notify_listener(&self, cb: &Callback<()>) -> Subscription {
125
+ let cb = cb.clone();
126
+ self.add_listener(move |_: T| cb.emit(()))
127
+ }
128
+
119
129
  /// Convert [`PubSub::emit`] to a `Box<dyn Fn(T)>`.
120
130
  #[must_use]
121
131
  pub fn as_boxfn(&self) -> Box<dyn Fn(T) + 'static> {
Binary file
@@ -1,4 +1,4 @@
1
- <svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+ <svg width="20" height="20" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
2
2
  <line y1="-0.5" x2="13.4536" y2="-0.5" transform="matrix(0.668965 0.743294 -0.668965 0.743294 6 6)"
3
3
  stroke="#042121" />
4
4
  <line y1="-0.5" x2="13.4536" y2="-0.5" transform="matrix(-0.668965 0.743294 0.668965 0.743294 16 6)"
@@ -1,4 +1,4 @@
1
- <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+ <svg width="14" height="14" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
2
  <defs></defs>
3
3
  <path d="M 15.915 8.555 L 10.213 14.257" stroke-width="1.00491" stroke-linecap="round" style="stroke: #042121">
4
4
  </path>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,6 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
- <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
2
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
3
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
4
  <style type="text/css">
6
5
  .st0{fill:#042121;}
7
6
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
3
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
- viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve">
4
+ viewBox="0 0 80 70" style="enable-background:new 0 0 80 70;" xml:space="preserve" width="40" height="35">
5
5
  <style type="text/css">
6
6
  .st0{fill:#042121;}
7
7
  </style>
@@ -1,4 +1,4 @@
1
- <svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+ <svg width="14" height="14" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
2
2
  <circle cx="5.5" cy="5.5" r="2.5" fill="#3F4343"/>
3
3
  <rect x="0.5" y="0.5" width="10" height="10" rx="5" stroke="#3F4343"/>
4
4
  </svg>
@@ -1,3 +1,3 @@
1
- <svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+ <svg width="14" height="14" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
2
2
  <circle cx="5.5" cy="5.5" r="3" stroke="#3F4343" />
3
3
  </svg>
@@ -1,4 +1,4 @@
1
- <svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+ <svg width="14" height="14" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
2
2
  <circle cx="5.5" cy="5.5" r="3.5" fill="#3F4343"/>
3
3
  <rect x="0.5" y="0.5" width="10" height="10" rx="5" stroke="#3F4343"/>
4
4
  </svg>