particle-calendar 0.1.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.
@@ -0,0 +1,453 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex">
7
+ <meta name="referrer" content="no-referrer">
8
+ <meta name="color-scheme" content="light dark">
9
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
10
+ <title>Particle — Availability calendar</title>
11
+ <style>
12
+ :root {
13
+ color-scheme: light dark;
14
+ --background: #f3f6fb;
15
+ --surface: #ffffff;
16
+ --text: #0f172a;
17
+ --muted: #64748b;
18
+ --border: #dbe2ea;
19
+ --accent: #0284c7;
20
+ --accent-start: #7dd3fc;
21
+ --accent-end: #fef08a;
22
+ --accent-gradient: linear-gradient(135deg, var(--accent-start), var(--accent-end));
23
+ --accent-glow: rgb(14 165 233 / 0.13);
24
+ --slot-background: #eff6ff;
25
+ --slot-border: #bfdbfe;
26
+ --slot-text: #075985;
27
+ --today-label-background: #e0f2fe;
28
+ --today-label-text: #075985;
29
+ --empty: #526175;
30
+ --shadow: 0 1px 2px rgb(15 23 42 / 0.04), 0 8px 24px rgb(15 23 42 / 0.04);
31
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
32
+ }
33
+
34
+ * { box-sizing: border-box; }
35
+
36
+ body {
37
+ margin: 0;
38
+ min-height: 100vh;
39
+ background: radial-gradient(circle at 8% 0%, var(--accent-glow), transparent 28rem), var(--background);
40
+ color: var(--text);
41
+ line-height: 1.45;
42
+ }
43
+
44
+ main {
45
+ width: min(100% - 2rem, 68rem);
46
+ margin: 0 auto;
47
+ padding: 3.5rem 0 2rem;
48
+ }
49
+
50
+ header { margin-bottom: 2rem; }
51
+
52
+ .brand {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: clamp(0.65rem, 2vw, 0.95rem);
56
+ }
57
+
58
+ .brand-mark {
59
+ width: clamp(2.7rem, 8vw, 4rem);
60
+ height: clamp(2.7rem, 8vw, 4rem);
61
+ flex: 0 0 auto;
62
+ overflow: visible;
63
+ }
64
+
65
+ .brand-mark-shell {
66
+ fill: var(--surface);
67
+ stroke: var(--text);
68
+ stroke-width: 2.5;
69
+ }
70
+
71
+ .brand-mark-divider,
72
+ .brand-mark-binding {
73
+ fill: none;
74
+ stroke: var(--text);
75
+ stroke-linecap: round;
76
+ stroke-width: 2.5;
77
+ }
78
+
79
+ .brand-mark-cell,
80
+ .brand-mark-particle { fill: var(--accent); }
81
+
82
+ h1 {
83
+ margin: 0;
84
+ font-size: clamp(2rem, 7vw, 3.6rem);
85
+ font-weight: 720;
86
+ letter-spacing: -0.045em;
87
+ line-height: 1;
88
+ }
89
+
90
+ h1::after {
91
+ content: ".";
92
+ color: var(--accent);
93
+ }
94
+
95
+ .sr-only {
96
+ position: absolute;
97
+ width: 1px;
98
+ height: 1px;
99
+ padding: 0;
100
+ margin: -1px;
101
+ overflow: hidden;
102
+ clip: rect(0, 0, 0, 0);
103
+ white-space: nowrap;
104
+ border: 0;
105
+ }
106
+
107
+ .intro {
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: flex-start;
111
+ gap: 0.12rem;
112
+ max-width: 38rem;
113
+ margin: 0.85rem 0 0;
114
+ }
115
+
116
+ .subtitle-name {
117
+ color: var(--text);
118
+ font-size: 1.12rem;
119
+ font-weight: 760;
120
+ letter-spacing: -0.02em;
121
+ }
122
+
123
+ .calendar-meta {
124
+ display: flex;
125
+ flex-wrap: wrap;
126
+ gap: 0.25rem 0.45rem;
127
+ margin: 0.55rem 0 0;
128
+ color: var(--muted);
129
+ font-size: 0.86rem;
130
+ font-variant-numeric: tabular-nums;
131
+ }
132
+
133
+ .week + .week { margin-top: 0.65rem; }
134
+
135
+ .weekday-header {
136
+ display: grid;
137
+ grid-template-columns: repeat(7, minmax(0, 1fr));
138
+ gap: 0.65rem;
139
+ margin-bottom: 0.55rem;
140
+ }
141
+
142
+ .weekday-header span {
143
+ padding: 0 1rem;
144
+ color: var(--muted);
145
+ font-size: 0.7rem;
146
+ font-weight: 650;
147
+ letter-spacing: 0.08em;
148
+ text-transform: uppercase;
149
+ }
150
+
151
+ .days {
152
+ display: grid;
153
+ grid-template-columns: repeat(7, minmax(0, 1fr));
154
+ gap: 0.65rem;
155
+ }
156
+
157
+ .day {
158
+ min-height: 10.5rem;
159
+ padding: 1rem;
160
+ border: 1px solid var(--border);
161
+ border-radius: 0.85rem;
162
+ background: var(--surface);
163
+ box-shadow: var(--shadow);
164
+ }
165
+
166
+ .day.placeholder { visibility: hidden; }
167
+ .day.empty { color: var(--empty); }
168
+
169
+ .day.today {
170
+ border-color: transparent;
171
+ background: linear-gradient(var(--surface), var(--surface)) padding-box,
172
+ var(--accent-gradient) border-box;
173
+ box-shadow: 0 10px 24px var(--accent-glow);
174
+ }
175
+
176
+ .date {
177
+ display: flex;
178
+ flex-direction: column;
179
+ align-items: flex-start;
180
+ margin: 0 0 0.85rem;
181
+ font-weight: 690;
182
+ line-height: 1.25;
183
+ }
184
+
185
+ .date-desktop {
186
+ display: flex;
187
+ align-items: baseline;
188
+ gap: 0.32rem;
189
+ }
190
+
191
+ .date-number {
192
+ font-size: 1.35rem;
193
+ letter-spacing: -0.03em;
194
+ }
195
+
196
+ .date-month {
197
+ color: var(--muted);
198
+ font-size: 0.72rem;
199
+ font-weight: 720;
200
+ letter-spacing: 0.06em;
201
+ text-transform: uppercase;
202
+ }
203
+
204
+ .date-mobile { display: none; }
205
+
206
+ .today-label {
207
+ display: inline-flex;
208
+ margin-top: 0.38rem;
209
+ padding: 0.18rem 0.4rem;
210
+ border-radius: 999px;
211
+ background: var(--today-label-background);
212
+ color: var(--today-label-text);
213
+ font-size: 0.64rem;
214
+ font-weight: 760;
215
+ letter-spacing: 0.06em;
216
+ text-transform: uppercase;
217
+ }
218
+
219
+ .slots {
220
+ display: flex;
221
+ flex-wrap: wrap;
222
+ gap: 0.42rem;
223
+ margin: 0;
224
+ padding: 0;
225
+ list-style: none;
226
+ }
227
+
228
+ .slot {
229
+ padding: 0.42rem 0.55rem;
230
+ border: 1px solid var(--slot-border);
231
+ border-radius: 0.45rem;
232
+ background: var(--slot-background);
233
+ color: var(--slot-text);
234
+ font-size: 0.84rem;
235
+ font-variant-numeric: tabular-nums;
236
+ font-weight: 700;
237
+ white-space: nowrap;
238
+ }
239
+
240
+ .none {
241
+ margin: 0;
242
+ font-size: 0.84rem;
243
+ }
244
+
245
+ .disabled {
246
+ max-width: 40rem;
247
+ padding: 1.4rem;
248
+ border: 1px solid var(--border);
249
+ border-radius: 0.85rem;
250
+ background: var(--surface);
251
+ box-shadow: var(--shadow);
252
+ }
253
+
254
+ .disabled h2 { margin: 0 0 0.35rem; font-size: 1.1rem; }
255
+ .disabled p { margin: 0; color: var(--muted); }
256
+
257
+ footer {
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 0.35rem;
261
+ margin-top: 2.25rem;
262
+ padding-top: 1rem;
263
+ border-top: 1px solid var(--border);
264
+ color: var(--muted);
265
+ font-size: 0.8rem;
266
+ }
267
+
268
+ .repository-link {
269
+ display: inline-flex;
270
+ align-items: center;
271
+ gap: 0.3rem;
272
+ color: inherit;
273
+ text-decoration: none;
274
+ }
275
+
276
+ .repository-link:hover,
277
+ .repository-link:focus-visible {
278
+ color: var(--accent);
279
+ text-decoration: underline;
280
+ }
281
+
282
+ .github-icon {
283
+ width: 1em;
284
+ height: 1em;
285
+ fill: currentColor;
286
+ }
287
+
288
+ @media (max-width: 68rem) {
289
+ main { padding-top: 2.25rem; }
290
+ .weekday-header,
291
+ .days { gap: 0.35rem; }
292
+ .weekday-header span {
293
+ padding: 0 0.45rem;
294
+ font-size: 0.62rem;
295
+ }
296
+ .week + .week { margin-top: 0.45rem; }
297
+ .day {
298
+ min-height: 8rem;
299
+ padding: 0.45rem;
300
+ }
301
+ .date { margin-bottom: 0.55rem; }
302
+ .date-number { font-size: 1rem; }
303
+ .date-month { font-size: 0.6rem; }
304
+ .slots { gap: 0.3rem; }
305
+ .slot {
306
+ padding: 0.3rem;
307
+ font-size: 0.67rem;
308
+ }
309
+ .none { font-size: 0.67rem; }
310
+ }
311
+
312
+ @media (max-width: 46rem) {
313
+ .weekday-header { display: none; }
314
+ .week + .week { margin-top: 0.5rem; }
315
+ .days { grid-template-columns: 1fr; gap: 0.5rem; }
316
+ .day.placeholder { display: none; }
317
+ .day {
318
+ display: grid;
319
+ grid-template-columns: max-content minmax(0, 1fr);
320
+ column-gap: 0.5rem;
321
+ align-items: start;
322
+ min-height: 0;
323
+ padding: 0.9rem;
324
+ }
325
+ .date { margin: 0; white-space: nowrap; }
326
+ .date-desktop { display: none; }
327
+ .date-mobile { display: inline; font-size: 0.88rem; }
328
+ .slots { justify-content: flex-end; }
329
+ .slot {
330
+ padding: 0.42rem 0.55rem;
331
+ font-size: 0.84rem;
332
+ }
333
+ .none { font-size: 0.84rem; }
334
+ }
335
+
336
+ @media (max-width: 26rem) {
337
+ .day {
338
+ grid-template-columns: 1fr;
339
+ row-gap: 0.65rem;
340
+ }
341
+ .slots { justify-content: flex-start; }
342
+ }
343
+
344
+ @media (prefers-color-scheme: dark) {
345
+ :root {
346
+ --background: #0f172a;
347
+ --surface: #111827;
348
+ --text: #f8fafc;
349
+ --muted: #94a3b8;
350
+ --border: #334155;
351
+ --accent: #7dd3fc;
352
+ --accent-start: #7dd3fc;
353
+ --accent-end: #fef08a;
354
+ --accent-glow: rgb(125 211 252 / 0.14);
355
+ --slot-background: #082f49;
356
+ --slot-border: #0e7490;
357
+ --slot-text: #bae6fd;
358
+ --today-label-background: #0c4a6e;
359
+ --today-label-text: #bae6fd;
360
+ --empty: #a3aec0;
361
+ --shadow: 0 1px 2px rgb(0 0 0 / 0.2), 0 10px 28px rgb(0 0 0 / 0.16);
362
+ }
363
+ }
364
+
365
+ </style>
366
+ </head>
367
+ <body>
368
+ <main>
369
+ <header>
370
+ <div class="brand">
371
+ <svg class="brand-mark" viewBox="0 0 64 64" aria-hidden="true" focusable="false">
372
+ <rect class="brand-mark-shell" x="7.5" y="10.5" width="47" height="45" rx="9"/>
373
+ <path class="brand-mark-divider" d="M8 24.5h46"/>
374
+ <path class="brand-mark-binding" d="M19.5 6.5v8M42.5 6.5v8"/>
375
+ <circle class="brand-mark-cell" cx="18" cy="34" r="3"/>
376
+ <circle class="brand-mark-cell" cx="31" cy="34" r="3"/>
377
+ <circle class="brand-mark-cell" cx="44" cy="34" r="3"/>
378
+ <circle class="brand-mark-cell" cx="18" cy="46" r="3"/>
379
+ <circle class="brand-mark-cell" cx="31" cy="46" r="3"/>
380
+ <rect class="brand-mark-particle" x="40.5" y="42.5" width="7" height="7" rx="1.5"/>
381
+ </svg>
382
+ <h1>Particle</h1>
383
+ </div>
384
+ <p class="intro">
385
+ <span class="subtitle-name">Availability calendar</span>
386
+ </p>
387
+ <% if enabled -%>
388
+ <p class="calendar-meta">
389
+ <span>Times shown in <%= h timezone_name %></span>
390
+ </p>
391
+ <% end -%>
392
+ </header>
393
+
394
+ <% if enabled -%>
395
+ <div class="calendar">
396
+ <div class="weekday-header" aria-hidden="true">
397
+ <% weekdays.each do |weekday| -%><span><%= h weekday[0, 3] %></span><% end -%>
398
+ </div>
399
+ <% weeks.each_with_index do |week, index| -%>
400
+ <section class="week" aria-labelledby="week-<%= index + 1 %>-heading">
401
+ <h2 class="sr-only" id="week-<%= index + 1 %>-heading"><%= h format_week_label(week) %></h2>
402
+ <div class="days">
403
+ <% week.each_with_index do |day, weekday_index| -%>
404
+ <% if day -%>
405
+ <article class="day<%= " empty" if day.slots.empty? %><%= " today" if day.date == today %>"
406
+ data-weekday="<%= h weekdays[weekday_index] %>">
407
+ <h3 class="date">
408
+ <span class="date-desktop" aria-hidden="true">
409
+ <span class="date-number"><%= h day.date.day %></span>
410
+ <span class="date-month"><%= h day.date.strftime('%b') %></span>
411
+ </span>
412
+ <span class="date-mobile" aria-hidden="true"><%= h format_compact_date(day.date) %></span>
413
+ <span class="sr-only"><%= h format_date(day.date) %></span>
414
+ <% if day.date == today -%><span class="today-label">Today</span><% end -%>
415
+ </h3>
416
+ <% if day.slots.empty? -%>
417
+ <p class="none">No availability</p>
418
+ <% else -%>
419
+ <ul class="slots" aria-label="Available times">
420
+ <% day.slots.each do |slot| -%>
421
+ <li class="slot"><%= h format_time(slot.starts_at) %>–<%= h format_time(slot.ends_at) %></li>
422
+ <% end -%>
423
+ </ul>
424
+ <% end -%>
425
+ </article>
426
+ <% else -%>
427
+ <div class="day placeholder" data-weekday="<%= h weekdays[weekday_index] %>" aria-hidden="true"></div>
428
+ <% end -%>
429
+ <% end -%>
430
+ </div>
431
+ </section>
432
+ <% end -%>
433
+ </div>
434
+ <% else -%>
435
+ <section class="disabled">
436
+ <h2>Calendar not available</h2>
437
+ <p>Particle is not being shared at the moment.</p>
438
+ </section>
439
+ <% end -%>
440
+
441
+ <footer>
442
+ <a class="repository-link" href="https://github.com/mendab1e/particle" aria-label="Particle on GitHub">
443
+ <svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
444
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/>
445
+ </svg>
446
+ <span>Particle</span>
447
+ </a>
448
+ <span aria-hidden="true">·</span>
449
+ <span>last updated: <%= h updated_at %></span>
450
+ </footer>
451
+ </main>
452
+ </body>
453
+ </html>
@@ -0,0 +1,34 @@
1
+ # Generated by `particle setup`. Review before installing.
2
+ # Configure ssl_certificate and ssl_certificate_key for this server separately.
3
+ server {
4
+ listen 443 ssl http2;
5
+ listen [::]:443 ssl http2;
6
+ server_name <%= @server_name %>;
7
+
8
+ add_header X-Robots-Tag "noindex, nofollow, noarchive, nosnippet, noimageindex" always;
9
+ add_header Cache-Control "private, no-store, max-age=0" always;
10
+ add_header Referrer-Policy "no-referrer" always;
11
+
12
+ location = /robots.txt {
13
+ alias "<%= escaped_output_dir %>/robots.txt";
14
+ default_type text/plain;
15
+ }
16
+
17
+ location = <%= @url_path.chomp('/') %> {
18
+ return 301 <%= @url_path %>;
19
+ }
20
+
21
+ location = <%= @url_path %> {
22
+ alias "<%= escaped_output_dir %>/index.html";
23
+ default_type text/html;
24
+
25
+ # For enforced privacy, create this file with `htpasswd` and uncomment:
26
+ # auth_basic "Private availability";
27
+ # auth_basic_user_file /etc/nginx/particle.htpasswd;
28
+ }
29
+
30
+ location = <%= @url_path %>favicon.svg {
31
+ alias "<%= escaped_output_dir %>/favicon.svg";
32
+ default_type image/svg+xml;
33
+ }
34
+ }
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: particle-calendar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Timur
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 6.1.7
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 6.1.7
26
+ - !ruby/object:Gem::Dependency
27
+ name: icalendar
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.10'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.10'
40
+ - !ruby/object:Gem::Dependency
41
+ name: icalendar-recurrence
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: tzinfo
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ description: A command-line static-site generator that subtracts private iCalendar
69
+ busy time from configured availability.
70
+ email:
71
+ - yanberdint@gmail.com
72
+ executables:
73
+ - particle
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - README.md
78
+ - assets/favicon.svg
79
+ - config/availability.example.yml
80
+ - exe/particle
81
+ - lib/availability.rb
82
+ - lib/availability/application.rb
83
+ - lib/availability/assets.rb
84
+ - lib/availability/atomic_writer.rb
85
+ - lib/availability/availability_calculator.rb
86
+ - lib/availability/busy_period.rb
87
+ - lib/availability/calendar_fetcher.rb
88
+ - lib/availability/calendar_parser.rb
89
+ - lib/availability/calendar_url.rb
90
+ - lib/availability/cli.rb
91
+ - lib/availability/config.rb
92
+ - lib/availability/config_value_parser.rb
93
+ - lib/availability/errors.rb
94
+ - lib/availability/event_timezone_validator.rb
95
+ - lib/availability/recurrence_expander.rb
96
+ - lib/availability/renderer.rb
97
+ - lib/availability/setup.rb
98
+ - lib/availability/tolerant_icalendar_parser.rb
99
+ - lib/availability/version.rb
100
+ - templates/index.html.erb
101
+ - templates/nginx.conf.erb
102
+ homepage: https://github.com/mendab1e/particle
103
+ licenses:
104
+ -
105
+ metadata:
106
+ source_code_uri: https://github.com/mendab1e/particle
107
+ rubygems_mfa_required: 'true'
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: 3.4.0
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubygems_version: 3.6.9
123
+ specification_version: 4
124
+ summary: Publish private calendar availability as static HTML
125
+ test_files: []