pull_request_ai 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +125 -0
  4. data/Rakefile +10 -0
  5. data/app/assets/config/pull_request_ai_manifest.js +1 -0
  6. data/app/assets/javascripts/application.js +248 -0
  7. data/app/assets/javascripts/notifications.js +76 -0
  8. data/app/assets/stylesheets/pull_request_ai/application.css +15 -0
  9. data/app/assets/stylesheets/pull_request_ai/blocs.css +4 -0
  10. data/app/assets/stylesheets/pull_request_ai/helpers.css +26 -0
  11. data/app/assets/stylesheets/pull_request_ai/inputs.css +64 -0
  12. data/app/assets/stylesheets/pull_request_ai/notification.css +71 -0
  13. data/app/assets/stylesheets/pull_request_ai/spinner.css +24 -0
  14. data/app/controllers/pull_request_ai/application_controller.rb +6 -0
  15. data/app/controllers/pull_request_ai/pull_request_ai_controller.rb +85 -0
  16. data/app/helpers/pull_request_ai/application_helper.rb +6 -0
  17. data/app/models/pull_request_ai/application_record.rb +7 -0
  18. data/app/views/layouts/pull_request_ai/application.html.erb +29 -0
  19. data/app/views/pull_request_ai/pull_request_ai/new.html.erb +70 -0
  20. data/config/initializers/rack_attack.rb +30 -0
  21. data/config/routes.rb +13 -0
  22. data/lib/pull_request_ai/bitbucket/client.rb +148 -0
  23. data/lib/pull_request_ai/client.rb +80 -0
  24. data/lib/pull_request_ai/engine.rb +10 -0
  25. data/lib/pull_request_ai/github/client.rb +124 -0
  26. data/lib/pull_request_ai/openAi/client.rb +81 -0
  27. data/lib/pull_request_ai/openAi/interpreter.rb +19 -0
  28. data/lib/pull_request_ai/repo/client.rb +43 -0
  29. data/lib/pull_request_ai/repo/file.rb +20 -0
  30. data/lib/pull_request_ai/repo/prompt.rb +33 -0
  31. data/lib/pull_request_ai/repo/reader.rb +118 -0
  32. data/lib/pull_request_ai/util/configuration.rb +49 -0
  33. data/lib/pull_request_ai/util/error.rb +28 -0
  34. data/lib/pull_request_ai/util/symbol_details.rb +14 -0
  35. data/lib/pull_request_ai/version.rb +5 -0
  36. data/lib/pull_request_ai.rb +52 -0
  37. data/lib/tasks/pull_request_ai_tasks.rake +5 -0
  38. metadata +153 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 747534e2b7a4327fb3a3eb986df97000e737072500f1e70839b2d61c5edeaf57
4
+ data.tar.gz: 2647f3525b95e48a268a05ded206e5a644e6a6e10494680af11f398dd9f4173d
5
+ SHA512:
6
+ metadata.gz: c918aa851b69ba5b5f079bcc75ad308132083e3a83638786063917022dc1d6cc919c69524422d64b173f4e378b923d771a3a79d6c8caa7e392f6139325f95bf8
7
+ data.tar.gz: 6afa49e24926c1895088fb60704b6e5008239b1e2214e4900cbc3e313f8a4490561a5925915c357cd173196c3f09e4650286f5bfd504573f2af43abd8966a667
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 André Franco
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # PullRequestAi
2
+ This Rails Engine offers a way to request Pull Request descriptions from OpenAI chatGPT and optionally allows direct creation or updating of Pull Requests on GitHub or Bitbucket.
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem "pull_request_ai"
9
+ ```
10
+
11
+ And then execute:
12
+ ```bash
13
+ $ bundle
14
+ ```
15
+
16
+ OR, install it yourself as:
17
+ ```bash
18
+ $ gem install pull_request_ai
19
+ ```
20
+
21
+ ## Contributing
22
+ Contribution directions go here.
23
+
24
+ ## License
25
+ This Rails Engine is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
26
+
27
+ ## Configuration
28
+
29
+ To configure this Rails Engine you can **just** set some specific Environment Variables or you can use the Rails Engine initializer class.
30
+
31
+ The minimum requirement that allows this Rails Engine to ask chatGPT Pull Request descriptions based on Git respository changes is the [OpenAI Key](https://platform.openai.com/account/usage).
32
+
33
+ Using **only** Environment Variable you need to set:
34
+ - [OPENAI_API_KEY](https://platform.openai.com/account/usage)
35
+
36
+ OR, if you choose to use the initializer:
37
+ ```ruby
38
+ PullRequestAi.configure do |config|
39
+ config.openai_api_key = 'YOUR_OPENAI_API_KEY'
40
+ end
41
+ ```
42
+
43
+ ## Integrations
44
+
45
+ To enable direct creation or updating of Pull Requests this Rails Engine can integrate with GitHub or Bitbucket.
46
+
47
+ For GitHub you need to provide a [GitHub Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token).
48
+ And for Bitbucket you need to provide a [Bitbucket App Password](https://bitbucket.org/account/settings/app-passwords/) and your Bitbucket Username.
49
+
50
+ ### GitHub Configuration
51
+
52
+ Using **only** Environment Variable you need to set:
53
+ - [GITHUB_ACCESS_TOKEN](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)
54
+
55
+ OR, if you choose to use the initializer:
56
+ ```ruby
57
+ PullRequestAi.configure do |config|
58
+ config.github_access_token = 'YOUR_GITHUB_ACCESS_TOKEN'
59
+ end
60
+ ```
61
+
62
+ ### Bitbucket Configuration
63
+
64
+ Using **only** Environment Variable you need to set:
65
+ - [BITBUCKET_APP_PASSWORD](https://bitbucket.org/account/settings/app-passwords/)
66
+ - [BITBUCKET_USERNAME](https://bitbucket.org/account/settings/)
67
+
68
+ OR, if you choose to use the initializer:
69
+ ```ruby
70
+ PullRequestAi.configure do |config|
71
+ config.bitbucket_app_password = 'YOUR_BITBUCKET_APP_PASSWORD'
72
+ config.bitbucket_username = 'YOUR_BITBUCKET_USERNAME'
73
+ end
74
+ ```
75
+
76
+ ## Usage
77
+
78
+ To use the Rails Engine interface on the browser you need to mount the engine route into your project routes. To do that include on your `routes.rb` file the following:
79
+
80
+ ```ruby
81
+ mount PullRequestAi::Engine => ''
82
+ ```
83
+
84
+ Then you navigate to:
85
+
86
+ ```
87
+ http://127.0.0.1:3000/rrtools/pull_request_ai
88
+ ```
89
+
90
+ Another way to use this Rails Engine is through code, to do that create an instance of the main client object.
91
+
92
+ ```ruby
93
+ client = PullRequestAi::Client.new
94
+ ```
95
+
96
+ This object offers the following actions:
97
+ - current_opened_pull_requests(base) - method that receives a base branch and based on the current branch will return any existing open pull request from GitHub.
98
+ - destination_branches - method without arguments that will return a list of all available remote branches.
99
+ - open_pull_request(base, title, description) - method that will communicate with the GitHub API to open a new pull request with the given parameters.
100
+ - update_pull_request(number, base, title, description) - method to update an existing pull request base, title, and description.
101
+ - flatten_current_changes(branch) - method that returns changes between the current branch and the given branch in a single string.
102
+
103
+ Notes about the return of these methods, all methods take advantage of [dry-monads](https://dry-rb.org/gems/dry-monads/1.3/).
104
+
105
+ ## Extra Configurations
106
+
107
+ If you need you have access to some aditional configurations which are:
108
+ - openai_api_endpoint - The OpenAI API endpoint.
109
+ - github_api_endpoint - The GitHub API endpoint.
110
+ - bitbucket_api_endpoint - The Bitbucket API endpoint.
111
+ - model - The [`model`](https://platform.openai.com/docs/models/model-endpoint-compatibility) parameter allows the user to select which OpenAI model to use for Pull Request suggestions. The default model used by this Gem is `gpt-3.5-turbo`, which is the most accessible. However, if you have access to version 4, we recommend using the `gpt-4` model.
112
+ - temperature - The [`temperature`](https://platform.openai.com/docs/api-reference/completions/create#completions/create-temperature) parameter is an OpenAI API configuration that affects the randomness of the output. Higher values produce more random results, while lower values like 0.2 produce more focused and deterministic output.
113
+
114
+ The only way to configure these parameters is using the initializer, above it is listing as well their default values:
115
+
116
+ ```ruby
117
+ PullRequestAi.configure do |config|
118
+ ...
119
+ config.openai_api_endpoint = 'https://api.openai.com'
120
+ config.github_api_endpoint = 'https://api.github.com'
121
+ config.bitbucket_api_endpoint = 'https://api.bitbucket.org'
122
+ config.model = 'gpt-3.5-turbo'
123
+ config.temperature = 0.6
124
+ end
125
+ ```
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
6
+ load 'rails/tasks/engine.rake'
7
+
8
+ load 'rails/tasks/statistics.rake'
9
+
10
+ require 'bundler/gem_tasks'
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/pull_request_ai .css
@@ -0,0 +1,248 @@
1
+ //= require_tree .
2
+
3
+ const reloadButton = document.getElementById('reload-btn');
4
+ const requestDescriptionButton = document.getElementById('request-description-btn');
5
+ const copyChatToClipboardButton = document.getElementById('copy-chat-to-clipboard-btn');
6
+ const copyChatToDescriptionButton = document.getElementById('copy-chat-to-description-btn');
7
+ const createPullRequestButton = document.getElementById('create-pull-request-btn');
8
+ const updatePullRequestButton = document.getElementById('update-pull-request-btn');
9
+ const copyDescriptionToClipboardButton = document.getElementById('copy-description-to-clipboard-btn');
10
+ const pullRequestWebsiteButton = document.getElementById('pull-request-website-btn');
11
+
12
+ const loadingContainer = document.getElementById('loading-container');
13
+ const prepareContainer = document.getElementById('prepare-container');
14
+ const chatDescriptionContainer = document.getElementById('chat-description-container');
15
+ const pullRequestContainer = document.getElementById('pull-request-container');
16
+
17
+ const branchField = document.getElementById('branch-field');
18
+ const typeField = document.getElementById('type-field');
19
+ const summaryField = document.getElementById('summary-field');
20
+
21
+ const chatDescriptionField = document.getElementById('chat-description-field');
22
+
23
+ const pullRequestNumberField = document.getElementById('pull-request-number-field');
24
+ const pullRequestTitleField = document.getElementById('pull-request-title-field');
25
+ const pullRequestDescriptionField = document.getElementById('pull-request-description-field');
26
+
27
+ async function jsonPost(path, data) {
28
+ showSpinner();
29
+
30
+ const response = await fetch(path, {
31
+ method: 'post',
32
+ body: JSON.stringify(data),
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
36
+ },
37
+ credentials: 'same-origin'
38
+ });
39
+
40
+ if (!response.ok && response.status != 422) {
41
+ hideSpinner();
42
+ unlockSelectors();
43
+ disableSubmission();
44
+ throw new Error(`An error has occured: ${response.statusText}`);
45
+ }
46
+ return await response.json();
47
+ }
48
+
49
+ reloadButton.onclick = () => {
50
+ window.location.reload();
51
+ }
52
+
53
+ copyChatToClipboardButton.onclick = () => {
54
+ copyToClipboardValueFrom(
55
+ chatDescriptionField,
56
+ "Chat description copied to the Clipboard."
57
+ );
58
+ }
59
+
60
+ copyDescriptionToClipboardButton.onclick = () => {
61
+ copyToClipboardValueFrom(
62
+ pullRequestDescriptionField,
63
+ "Description copied to the Clipboard."
64
+ );
65
+ }
66
+
67
+ requestDescriptionButton.onclick = () => {
68
+ const data = { branch: branchField.value, type: typeField.value, summary: summaryField.value };
69
+
70
+ lockSelectors();
71
+
72
+ jsonPost('/rrtools/pull_request_ai/prepare', data).then(data => {
73
+ hideSpinner();
74
+ unlockSelectors();
75
+ if ('errors' in data) {
76
+ window.showNotification({ message: data.errors, theme: "error" });
77
+ }
78
+ else if ('notice' in data) {
79
+ window.showNotification({ message: data.notice, theme: "warning" });
80
+ }
81
+ else {
82
+ enableSubmission(data);
83
+ }
84
+ }).catch(errorMsg => {
85
+ window.showNotification({ message: errorMsg, theme: "error" });
86
+ });
87
+ }
88
+
89
+ copyChatToDescriptionButton.onclick = () => {
90
+ const chat = chatDescriptionField.value;
91
+ const current = pullRequestDescriptionField.value;
92
+ pullRequestDescriptionField.value = current + "\n\n" + chat;
93
+ window.showNotification({
94
+ message: "Chat description copied to the Pull Request description."
95
+ });
96
+ }
97
+
98
+ createPullRequestButton.onclick = () => {
99
+ const data = {
100
+ branch: branchField.value, title: pullRequestTitleField.value, description: pullRequestDescriptionField.value
101
+ };
102
+ jsonPost('/rrtools/pull_request_ai/create', data).then(data => {
103
+ hideSpinner();
104
+ processData(data, "Pull Request created successfully.");
105
+ }).catch(errorMsg => {
106
+ window.showNotification({ message: errorMsg, theme: "error" });
107
+ });
108
+ }
109
+
110
+ updatePullRequestButton.onclick = () => {
111
+ const data = {
112
+ number: pullRequestNumberField.value, branch: branchField.value, title: pullRequestTitleField.value, description: pullRequestDescriptionField.value
113
+ };
114
+ jsonPost('/rrtools/pull_request_ai/update', data).then(data => {
115
+ hideSpinner();
116
+ processData(data, "Pull Request updated successfully.");
117
+ }).catch(errorMsg => {
118
+ window.showNotification({ message: errorMsg, theme: "error" });
119
+ });
120
+ }
121
+
122
+ function showSpinner() {
123
+ loadingContainer.style.display = 'block';
124
+ }
125
+
126
+ function hideSpinner() {
127
+ loadingContainer.style.display = 'none';
128
+ }
129
+
130
+ function lockSelectors() {
131
+ branchField.setAttribute('disabled', '');
132
+ typeField.setAttribute('disabled', '');
133
+ summaryField.setAttribute('disabled', '');
134
+ requestDescriptionButton.setAttribute('disabled', '');
135
+ }
136
+
137
+ function unlockSelectors() {
138
+ branchField.removeAttribute('disabled');
139
+ typeField.removeAttribute('disabled');
140
+ summaryField.removeAttribute('disabled');
141
+ requestDescriptionButton.removeAttribute('disabled');
142
+ }
143
+
144
+ function copyToClipboardValueFrom(field, successMessage) {
145
+ if (navigator.clipboard) {
146
+ const text = field.value;
147
+ navigator.clipboard.writeText(text);
148
+ } else {
149
+ field.select();
150
+ document.execCommand("copy");
151
+ }
152
+ window.showNotification({ message: successMessage });
153
+ }
154
+
155
+ function processData(data, successMessage) {
156
+ if ('errors' in data) {
157
+ window.showNotification({ message: data.errors, theme: "error" });
158
+ }
159
+ else if ('notice' in data) {
160
+ window.showNotification({ message: data.notice, theme: "warning" });
161
+ }
162
+ else {
163
+ window.showNotification({ message: successMessage });
164
+ updateForm(data)
165
+ }
166
+ }
167
+
168
+ function enableSubmission(data) {
169
+ chatDescriptionField.value = data.description;
170
+ pullRequestWebsiteButton.classList.add('hide');
171
+
172
+ if (data.remote_enabled) {
173
+ // With Remote configured we always show the Pull Request form.
174
+ pullRequestContainer.classList.remove('hide');
175
+ if (data.open_pr) {
176
+ // With a Pull Request open we show the chat description on top with the button to copy to the form.
177
+ chatDescriptionContainer.classList.remove('hide');
178
+ copyChatToDescriptionButton.classList.remove('hide');
179
+
180
+ // Update the Pull Request form buttons accordingly.
181
+ createPullRequestButton.classList.add('hide');
182
+ updatePullRequestButton.classList.remove('hide');
183
+
184
+ // Fill the form with the existing values.
185
+ pullRequestNumberField.value = data.open_pr.number;
186
+ pullRequestTitleField.value = data.open_pr.title;
187
+ pullRequestDescriptionField.value = data.open_pr.description;
188
+
189
+ checkPullRequestVisibility(data.open_pr.link);
190
+ }
191
+ else {
192
+ // Without a Pull Request open we don't need to show the chat suggestion text area
193
+ // because we will use the form already filled with the suggestion.
194
+ chatDescriptionContainer.classList.add('hide');
195
+
196
+ // Update the Pull Request form buttons accordingly.
197
+ createPullRequestButton.classList.remove('hide');
198
+ updatePullRequestButton.classList.add('hide');
199
+
200
+ // Clear the form and fill the description with the chat suggestion.
201
+ pullRequestNumberField.value = '';
202
+ pullRequestTitleField.value = '';
203
+ pullRequestDescriptionField.value = data.description;
204
+ }
205
+ }
206
+ else {
207
+ // Without Remote configured we show the chat description with the copy button.
208
+ pullRequestContainer.classList.add('hide');
209
+ chatDescriptionContainer.classList.remove('hide');
210
+ copyChatToDescriptionButton.classList.add('hide');
211
+ }
212
+ }
213
+
214
+ function updateForm(data) {
215
+ if (data.number && data.title) {
216
+ chatDescriptionField.value = pullRequestDescriptionField.value;
217
+
218
+ // With Remote configured we always show the Pull Request form.
219
+ pullRequestContainer.classList.remove('hide');
220
+
221
+ // Update the Pull Request form buttons accordingly.
222
+ createPullRequestButton.classList.add('hide');
223
+ updatePullRequestButton.classList.remove('hide');
224
+
225
+ // Fill the form with the existing values.
226
+ pullRequestNumberField.value = data.number;
227
+ pullRequestTitleField.value = data.title;
228
+ pullRequestDescriptionField.value = data.description;
229
+
230
+ checkPullRequestVisibility(data.link);
231
+ }
232
+ }
233
+
234
+ function checkPullRequestVisibility(link) {
235
+ if (link && link.length > 0) {
236
+ pullRequestWebsiteButton.classList.remove('hide');
237
+ pullRequestWebsiteButton.onclick = function() {
238
+ window.open(link, "_blank");
239
+ };
240
+ } else {
241
+ pullRequestWebsiteButton.classList.add('hide');
242
+ }
243
+ }
244
+
245
+ function disableSubmission() {
246
+ chatDescriptionContainer.classList.add('hide');
247
+ pullRequestContainer.classList.add('hide');
248
+ }
@@ -0,0 +1,76 @@
1
+ (function Notifications(window) {
2
+
3
+ const partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs);
4
+
5
+ const append = (el, ...children) => children.forEach(child => el.appendChild(child));
6
+
7
+ const isString = input => typeof input === 'string';
8
+
9
+ const createElement = (elementType, ...classNames) => {
10
+ const element = document.createElement(elementType);
11
+ if(classNames.length) {
12
+ classNames.forEach(currentClass => element.classList.add(currentClass));
13
+ }
14
+ return element;
15
+ };
16
+
17
+ const setInnerText = (element, text) => {
18
+ element.innerText = text;
19
+ return element;
20
+ };
21
+
22
+ const createTextElement = (elementType, ...classNames) => partial(setInnerText, createElement(elementType, ...classNames));
23
+
24
+ const createParagraph = (...classNames) => createTextElement('p', ...classNames);
25
+
26
+ const defaultOptions = {
27
+ showDuration: 3000,
28
+ theme: 'success'
29
+ };
30
+
31
+ function showNotification(options) {
32
+ if(typeof options.showDuration !== 'number') {
33
+ options.showDuration = defaultOptions.showDuration;
34
+ }
35
+ if(!isString(options.theme) || options.theme.length === 0) {
36
+ options.theme = defaultOptions.theme;
37
+ }
38
+ if(!isString(options.message) || options.message.length === 0) {
39
+ options.message = defaultOptions.theme;
40
+ }
41
+
42
+ const container = createNotificationContainer();
43
+ const notificationEl = createElement('div', 'notification', options.theme);
44
+ notificationEl.addEventListener('click', () => container.removeChild(notificationEl));
45
+
46
+ if(isString(options.title) && options.title.length > 0) {
47
+ append(notificationEl, createParagraph('notification-title')(options.title));
48
+ }
49
+ append(notificationEl, createParagraph('notification-message')(options.message));
50
+ append(container, notificationEl);
51
+
52
+ if(options.showDuration && options.showDuration > 0) {
53
+ const timeout = setTimeout(() => {
54
+ container.removeChild(notificationEl);
55
+ if(container.querySelectorAll('.notification').length === 0) {
56
+ document.body.removeChild(container);
57
+ }
58
+ }, options.showDuration);
59
+
60
+ notificationEl.addEventListener('click', () => clearTimeout(timeout));
61
+ }
62
+ }
63
+
64
+ function createNotificationContainer() {
65
+ let container = document.querySelector(`notification-container`);
66
+ if(!container) {
67
+ container = createElement('div', 'notification-container');
68
+ append(document.body, container);
69
+ }
70
+ return container;
71
+ }
72
+
73
+ if (!window.showNotification) {
74
+ window.showNotification = showNotification;
75
+ }
76
+ })(window);
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,4 @@
1
+ main {
2
+ display: flex;
3
+ justify-content: center;
4
+ }
@@ -0,0 +1,26 @@
1
+ .hide { display: none; }
2
+ .block { display: block; }
3
+ .clear:after { content: ''; clear: both; display: block; }
4
+
5
+ .mt-5 { margin-top: 5px; }
6
+
7
+ .mt-10 { margin-top: 10px; }
8
+ .mt-20 { margin-top: 20px; }
9
+ .mt-40 { margin-top: 40px; }
10
+ .mr-10 { margin-right: 10px; }
11
+ .mr-20 { margin-right: 20px; }
12
+ .ml-10 { margin-left: 10px; }
13
+
14
+ .fw { width: 100% }
15
+
16
+ .w-760 { width: 760px; }
17
+
18
+ .mh-40 { min-height: 40px; }
19
+ .h-100 { height: 100px; }
20
+ .h-220 { height: 220px; }
21
+
22
+ .flex {
23
+ display: flex;
24
+ }
25
+
26
+ .red { color: #BD362F; }
@@ -0,0 +1,64 @@
1
+
2
+ input, select {
3
+ background: white;
4
+ border: 1px solid gray;
5
+ border-radius: 4px;
6
+ box-shadow: none;
7
+ color: black;
8
+ padding: 10px 12px;
9
+ }
10
+
11
+ button {
12
+ appearance: none;
13
+ background-color: #3d898b;
14
+ border: 1px solid rgba(27, 31, 35, .15);
15
+ border-radius: 6px;
16
+ box-shadow: rgba(27, 31, 35, .1) 0 1px 0;
17
+ box-sizing: border-box;
18
+ color: #fff;
19
+ cursor: pointer;
20
+ display: inline-block;
21
+ font-size: 14px;
22
+ font-weight: 500;
23
+ line-height: 20px;
24
+ padding: 6px 16px;
25
+ position: relative;
26
+ text-align: center;
27
+ text-decoration: none;
28
+ user-select: none;
29
+ -webkit-user-select: none;
30
+ touch-action: manipulation;
31
+ vertical-align: middle;
32
+ white-space: nowrap;
33
+ }
34
+
35
+ button:focus:not(:focus-visible):not(.focus-visible) {
36
+ box-shadow: none;
37
+ outline: none;
38
+ }
39
+
40
+ button:hover {
41
+ background-color: #2D7072;
42
+ }
43
+
44
+ button:focus {
45
+ box-shadow: rgba(46, 164, 79, .4) 0 0 0 3px;
46
+ outline: none;
47
+ }
48
+
49
+ button:disabled {
50
+ background-color: #a0a0a0;
51
+ border-color: rgba(27, 31, 35, .1);
52
+ color: rgba(255, 255, 255, 0.594);
53
+ cursor: default;
54
+ }
55
+
56
+ button:active {
57
+ background-color: #2D7072;
58
+ box-shadow: rgba(20, 70, 32, .2) 0 1px 0 inset;
59
+ }
60
+
61
+ textarea {
62
+ width: 760px;
63
+ resize: none;
64
+ }
@@ -0,0 +1,71 @@
1
+ .notification-container {
2
+ font-size: 14px;
3
+ box-sizing: border-box;
4
+ position: fixed;
5
+ z-index: 999999;
6
+ top: 12px;
7
+ right: 12px;
8
+ }
9
+
10
+ .notification-container .notification {
11
+ background: #ffffff;
12
+ transition: .3s ease;
13
+ position: relative;
14
+ pointer-events: auto;
15
+ overflow: hidden;
16
+ padding: 15px 15px 15px 50px;
17
+ margin: 0 0 6px;
18
+ width: 300px;
19
+ border-radius: 3px 3px 3px 3px;
20
+ box-shadow: 0 0 12px #999999;
21
+ color: #000000;
22
+ opacity: 0.9;
23
+ -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=90);
24
+ filter: alpha(opacity=90);
25
+ background-position: 15px center !important;
26
+ background-repeat: no-repeat !important;
27
+ -webkit-user-select: none; /* Chrome all / Safari all */
28
+ -moz-user-select: none; /* Firefox all */
29
+ -ms-user-select: none; /* IE 10+ */
30
+ user-select: none; /* Likely future */
31
+ }
32
+
33
+ .notification:hover {
34
+ box-shadow: 0 0 12px #000000;
35
+ opacity: 1;
36
+ cursor: pointer;
37
+ }
38
+
39
+ .notification .notification-title {
40
+ font-weight: bold;
41
+ font-size: 16px;
42
+ text-align: left;
43
+ margin-top: 0;
44
+ margin-bottom: 6px;
45
+ word-wrap: break-word;
46
+ }
47
+
48
+ .notification .notification-message {
49
+ margin: 0;
50
+ text-align: left;
51
+ word-wrap: break-word;
52
+ }
53
+
54
+ .notification p {
55
+ color: #ffffff;
56
+ }
57
+
58
+ .notification-container .success {
59
+ background: #51A351;
60
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==");
61
+ }
62
+
63
+ .notification-container .warning {
64
+ background: #f87400;
65
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=");
66
+ }
67
+
68
+ .notification-container .error {
69
+ background: #BD362F;
70
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=");
71
+ }