froggy-docs 1.0.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,377 @@
1
+ import 'package:jaspr/dom.dart';
2
+ import 'package:jaspr/jaspr.dart';
3
+ import 'package:http/http.dart' as http;
4
+ import 'dart:convert';
5
+ import 'dart:async';
6
+ import 'dart:html' as html;
7
+
8
+ class App extends StatefulComponent {
9
+ const App({super.key});
10
+
11
+ @override
12
+ State<App> createState() => _AppState();
13
+ }
14
+
15
+ class _AppState extends State<App> {
16
+ Map<String, dynamic>? apiData;
17
+ Timer? timer;
18
+ bool isDark = false;
19
+ String searchQuery = '';
20
+ String authHeader = '';
21
+ final Map<String, String> _responses = {};
22
+ final Map<String, bool> _loading = {};
23
+ final Map<String, Map<String, String>> _requestBodyValues = {};
24
+ final Map<String, bool> _expandedEndpoints = {};
25
+
26
+ @override
27
+ void initState() {
28
+ super.initState();
29
+ _loadData();
30
+ timer = Timer.periodic(const Duration(seconds: 3), (_) => _loadData());
31
+
32
+ try {
33
+ if (html.window.matchMedia('(prefers-color-scheme: dark)').matches) {
34
+ isDark = true;
35
+ _applyTheme();
36
+ }
37
+ } catch (_) {}
38
+ }
39
+
40
+ @override
41
+ void dispose() {
42
+ timer?.cancel();
43
+ super.dispose();
44
+ }
45
+
46
+ void _applyTheme() {
47
+ try {
48
+ html.document.documentElement?.setAttribute('data-theme', isDark ? 'dark' : 'light');
49
+ } catch (_) {}
50
+ }
51
+
52
+ Future<void> _loadData() async {
53
+ try {
54
+ final resp = await http.get(Uri.parse('/froggy_docs.json'));
55
+ if (resp.statusCode == 200) {
56
+ final data = jsonDecode(resp.body);
57
+ setState(() => apiData = data as Map<String, dynamic>);
58
+ }
59
+ } catch (_) {}
60
+ }
61
+
62
+ Future<void> _executeRequest(String method, String path, bool hasAuth, Map<String, dynamic>? props) async {
63
+ final key = '$method-$path';
64
+ setState(() => _loading[key] = true);
65
+
66
+ try {
67
+ final baseUrl = html.window.location.origin;
68
+ final url = '$baseUrl$path';
69
+
70
+ final headers = <String, String>{
71
+ 'Content-Type': 'application/json',
72
+ };
73
+
74
+ if (authHeader.isNotEmpty) {
75
+ headers['Authorization'] = authHeader;
76
+ }
77
+
78
+ String body = '{}';
79
+ if (props != null && props.isNotEmpty) {
80
+ final Map<String, dynamic> bodyData = {};
81
+ final savedValues = _requestBodyValues[key] ?? {};
82
+ for (var prop in props.entries) {
83
+ final value = savedValues[prop.key] ?? _getDefaultValue(prop.value['type']);
84
+ if (value.isNotEmpty) {
85
+ bodyData[prop.key] = _parseValue(value, prop.value['type']);
86
+ }
87
+ }
88
+ body = jsonEncode(bodyData);
89
+ }
90
+
91
+ http.Response resp;
92
+ switch (method.toUpperCase()) {
93
+ case 'GET':
94
+ resp = await http.get(Uri.parse(url), headers: headers);
95
+ break;
96
+ case 'POST':
97
+ resp = await http.post(Uri.parse(url), headers: headers, body: body);
98
+ break;
99
+ case 'PUT':
100
+ resp = await http.put(Uri.parse(url), headers: headers, body: body);
101
+ break;
102
+ case 'DELETE':
103
+ resp = await http.delete(Uri.parse(url), headers: headers);
104
+ break;
105
+ case 'PATCH':
106
+ resp = await http.patch(Uri.parse(url), headers: headers, body: body);
107
+ break;
108
+ default:
109
+ resp = await http.get(Uri.parse(url), headers: headers);
110
+ }
111
+
112
+ setState(() {
113
+ if (resp.statusCode >= 200 && resp.statusCode < 300) {
114
+ try {
115
+ _responses[key] = JsonEncoder.withIndent(' ').convert(jsonDecode(resp.body));
116
+ } catch (_) {
117
+ _responses[key] = resp.body;
118
+ }
119
+ } else {
120
+ _responses[key] = 'Error ${resp.statusCode}: ${resp.body}';
121
+ }
122
+ });
123
+ } catch (e) {
124
+ setState(() {
125
+ _responses[key] = 'Error: $e';
126
+ });
127
+ } finally {
128
+ setState(() => _loading[key] = false);
129
+ }
130
+ }
131
+
132
+ String _getDefaultValue(String? type) {
133
+ switch (type?.toString().toLowerCase()) {
134
+ case 'number':
135
+ return '0';
136
+ case 'boolean':
137
+ return 'true';
138
+ case 'array':
139
+ return '[]';
140
+ case 'object':
141
+ return '{}';
142
+ default:
143
+ return '';
144
+ }
145
+ }
146
+
147
+ dynamic _parseValue(String value, String? type) {
148
+ switch (type?.toString().toLowerCase()) {
149
+ case 'number':
150
+ return num.tryParse(value) ?? 0;
151
+ case 'boolean':
152
+ return value.toLowerCase() == 'true';
153
+ case 'array':
154
+ return [];
155
+ case 'object':
156
+ return {};
157
+ default:
158
+ return value;
159
+ }
160
+ }
161
+
162
+ Map<String, List<MapEntry<String, dynamic>>> _getEndpointsByTag() {
163
+ final Map<String, List<MapEntry<String, dynamic>>> byTag = {};
164
+ final paths = apiData?['paths'] as Map<String, dynamic>? ?? {};
165
+
166
+ for (var path in paths.entries) {
167
+ for (var method in (path.value as Map<String, dynamic>).entries) {
168
+ final tags = (method.value['tags'] as List?) ?? ['Untagged'];
169
+ for (var tag in tags) {
170
+ byTag.putIfAbsent(tag.toString(), () => []);
171
+ byTag[tag.toString()]!.add(MapEntry(path.key, method));
172
+ }
173
+ }
174
+ }
175
+ return byTag;
176
+ }
177
+
178
+ @override
179
+ Component build(BuildContext context) {
180
+ return div(classes: 'app-container', [
181
+ _buildSidebar(),
182
+ _buildMainContent(),
183
+ ]);
184
+ }
185
+
186
+ Component _buildSidebar() {
187
+ final byTag = _getEndpointsByTag();
188
+
189
+ final filtered = byTag.entries.where((e) {
190
+ if (searchQuery.isEmpty) return true;
191
+ if (e.key.toLowerCase().contains(searchQuery.toLowerCase())) return true;
192
+ return e.value.any((ep) => ep.key.toLowerCase().contains(searchQuery.toLowerCase()));
193
+ }).toList();
194
+
195
+ return aside(classes: 'sidebar', [
196
+ div(classes: 'sidebar-header', [
197
+ h2([
198
+ span([text('🐸')], attributes: {'style': 'font-size: 24px;'}),
199
+ text(' FroggyDocs'),
200
+ ]),
201
+ ]),
202
+ div(classes: 'search-container', [
203
+ input(
204
+ type: InputType.text,
205
+ classes: 'search-box',
206
+ attributes: {'placeholder': 'Search tags or endpoints...'},
207
+ onInput: (val) => setState(() => searchQuery = val.toString()),
208
+ ),
209
+ ]),
210
+ nav(classes: 'nav-list', [
211
+ for (var tagGroup in filtered) _buildTagGroup(tagGroup.key, tagGroup.value),
212
+ ]),
213
+ ]);
214
+ }
215
+
216
+ Component _buildTagGroup(String tag, List<MapEntry<String, dynamic>> endpoints) {
217
+ final matchesSearch = searchQuery.isEmpty || tag.toLowerCase().contains(searchQuery.toLowerCase());
218
+
219
+ // When searching by tag name, show ALL endpoints in that tag
220
+ // When searching by endpoint path, filter endpoints
221
+ final filtered = (searchQuery.isEmpty || matchesSearch)
222
+ ? endpoints
223
+ : endpoints.where((ep) => ep.key.toLowerCase().contains(searchQuery.toLowerCase())).toList();
224
+
225
+ if (searchQuery.isNotEmpty && filtered.isEmpty) return div([]);
226
+
227
+ return div(classes: 'tag-group', [
228
+ div(classes: 'tag-header', [
229
+ span([text('▶ $tag')]),
230
+ ]),
231
+ div(classes: 'tag-endpoints', [
232
+ for (var ep in filtered)
233
+ a(classes: 'nav-item', href: '#${ep.value.key.toUpperCase()}-${ep.key}', [
234
+ span(classes: 'method-tag ${ep.value.key.toUpperCase()}', [text(ep.value.key.toUpperCase())]),
235
+ span([text(ep.key)]),
236
+ ]),
237
+ ]),
238
+ ]);
239
+ }
240
+
241
+ Component _buildMainContent() {
242
+ return div(classes: 'main-content', [
243
+ header(classes: 'header', [
244
+ h1([text((apiData?['info'] as Map<String, dynamic>?)?['title'] as String? ?? 'API Documentation')]),
245
+ button(
246
+ classes: 'theme-toggle',
247
+ [text(isDark ? '☀️' : '🌙')],
248
+ events: {
249
+ 'click': (_) => setState(() {
250
+ isDark = !isDark;
251
+ _applyTheme();
252
+ }),
253
+ },
254
+ ),
255
+ ]),
256
+ div(classes: 'settings-section', [
257
+ div([text('Authorization: ')]),
258
+ input(
259
+ type: InputType.text,
260
+ classes: 'auth-input',
261
+ attributes: {'placeholder': 'Bearer token'},
262
+ onInput: (val) => setState(() => authHeader = val.toString()),
263
+ ),
264
+ ]),
265
+ if (apiData == null)
266
+ div(classes: 'api-section', [
267
+ p([text('Loading...')]),
268
+ ])
269
+ else
270
+ _buildApiList(),
271
+ ]);
272
+ }
273
+
274
+ Component _buildApiList() {
275
+ final paths = apiData!['paths'] as Map<String, dynamic>? ?? {};
276
+ return div([
277
+ for (var path in paths.entries)
278
+ for (var method in (path.value as Map<String, dynamic>).entries)
279
+ _buildEndpoint(path.key, method.key, method.value),
280
+ ]);
281
+ }
282
+
283
+ Component _buildEndpoint(String path, String method, dynamic spec) {
284
+ final props =
285
+ spec['requestBody']?['content']?['application/json']?['schema']?['properties'] as Map<String, dynamic>?;
286
+ final hasAuth = spec['security'] != null;
287
+ final key = '$method-$path';
288
+ final response = _responses[key];
289
+ final isLoading = _loading[key] ?? false;
290
+ final isExpanded = _expandedEndpoints[key] ?? false;
291
+ final savedValues = _requestBodyValues[key] ?? {};
292
+
293
+ return section(
294
+ classes: 'api-section',
295
+ attributes: {'id': '${method.toUpperCase()}-$path'},
296
+ [
297
+ div(classes: 'endpoint-header', [
298
+ span(classes: 'method-tag ${method.toUpperCase()}', [text(method.toUpperCase())]),
299
+ span(classes: 'path-text', [text(path)]),
300
+ if (hasAuth) span(classes: 'auth-badge', [text('🔒 Auth')]),
301
+ ]),
302
+ p(classes: 'api-description', [text(spec['summary'] ?? 'No description')]),
303
+
304
+ if (props != null && props.isNotEmpty) ...[
305
+ button(
306
+ classes: 'try-it-out-btn',
307
+ [text(isExpanded ? 'Hide Request Body' : 'Show Request Body')],
308
+ events: {'click': (_) => setState(() => _expandedEndpoints[key] = !isExpanded)},
309
+ ),
310
+ if (isExpanded) ...[
311
+ h3(classes: 'section-title', [text('Request Body')]),
312
+ div(classes: 'request-body-form', [
313
+ for (var prop in props.entries)
314
+ div(classes: 'form-field', [
315
+ label([text(prop.key), text(' (${prop.value['type'] ?? 'string'})')]),
316
+ if (prop.value['description'] != null && (prop.value['description'] as String).isNotEmpty)
317
+ span(classes: 'field-desc', [text(prop.value['description'])]),
318
+ input(
319
+ type: InputType.text,
320
+ classes: 'form-input',
321
+ attributes: {
322
+ 'placeholder': _getDefaultValue(prop.value['type']),
323
+ 'value': savedValues[prop.key] ?? '',
324
+ },
325
+ onInput: (val) {
326
+ setState(() {
327
+ _requestBodyValues[key] ??= {};
328
+ _requestBodyValues[key]![prop.key] = val.toString();
329
+ });
330
+ },
331
+ ),
332
+ ]),
333
+ ]),
334
+ ],
335
+ ],
336
+
337
+ button(
338
+ classes: 'try-it-out-btn ${props != null && props.isNotEmpty ? 'secondary' : ''}',
339
+ [text(isLoading ? 'Loading...' : 'Try It Out')],
340
+ events: {'click': (_) => _executeRequest(method, path, hasAuth, props)},
341
+ ),
342
+
343
+ if (response != null) ...[
344
+ div(classes: 'response-section', [
345
+ h4([text('Response:')]),
346
+ pre(classes: 'response-body', [text(response)]),
347
+ ]),
348
+ ],
349
+
350
+ if (props != null && props.isNotEmpty) ...[
351
+ h3(classes: 'section-title', [text('Parameters')]),
352
+ table(classes: 'params-table', [
353
+ thead([
354
+ tr([
355
+ th([text('Field')]),
356
+ th([text('Type')]),
357
+ th([text('Description')]),
358
+ ]),
359
+ ]),
360
+ tbody([
361
+ for (var prop in props.entries)
362
+ tr([
363
+ td([
364
+ b([text(prop.key)]),
365
+ ]),
366
+ td([
367
+ span(classes: 'type-label', [text((prop.value['type'] ?? 'string').toString())]),
368
+ ]),
369
+ td([text((prop.value['description'] ?? '-').toString())]),
370
+ ]),
371
+ ]),
372
+ ]),
373
+ ],
374
+ ],
375
+ );
376
+ }
377
+ }
@@ -0,0 +1,27 @@
1
+ import 'package:jaspr/dom.dart';
2
+
3
+ // As your CSS styles are defined using just Dart, you can simply
4
+ // use global variables or methods for common things like colors.
5
+ const primaryColor = Color('#01589B');
6
+
7
+ // Defines the global CSS styles for this project.
8
+ //
9
+ // By using the @css annotation, these will be rendered automatically to CSS and included in your page.
10
+ @css
11
+ List<StyleRule> get styles => [
12
+ // Special import rule to include to another css file.
13
+ css.import('https://fonts.googleapis.com/css?family=Roboto'),
14
+ // Each style rule takes a valid css selector and a set of styles.
15
+ // Styles are defined using type-safe css bindings and can be freely chained and nested.
16
+ css('html, body').styles(
17
+ width: 100.percent,
18
+ minHeight: 100.vh,
19
+ padding: .zero,
20
+ margin: .zero,
21
+ fontFamily: const .list([FontFamily('Roboto'), FontFamilies.sansSerif]),
22
+ ),
23
+ css('h1').styles(
24
+ margin: .unset,
25
+ fontSize: 4.rem,
26
+ ),
27
+ ];
@@ -0,0 +1,12 @@
1
+ import 'package:jaspr/client.dart';
2
+
3
+ import 'app.dart';
4
+ import 'main.client.options.dart';
5
+
6
+ void main() {
7
+ Jaspr.initializeApp(
8
+ options: defaultClientOptions,
9
+ );
10
+
11
+ runApp(const App());
12
+ }
@@ -0,0 +1,25 @@
1
+ // dart format off
2
+ // ignore_for_file: type=lint
3
+
4
+ // GENERATED FILE, DO NOT MODIFY
5
+ // Generated with jaspr_builder
6
+
7
+ import 'package:jaspr/client.dart';
8
+
9
+ /// Default [ClientOptions] for use with your Jaspr project.
10
+ ///
11
+ /// Use this to initialize Jaspr **before** calling [runApp].
12
+ ///
13
+ /// Example:
14
+ /// ```dart
15
+ /// import 'main.client.options.dart';
16
+ ///
17
+ /// void main() {
18
+ /// Jaspr.initializeApp(
19
+ /// options: defaultClientOptions,
20
+ /// );
21
+ ///
22
+ /// runApp(...);
23
+ /// }
24
+ /// ```
25
+ ClientOptions get defaultClientOptions => ClientOptions();
@@ -0,0 +1,20 @@
1
+ name: frontend
2
+ description: A new Jaspr project
3
+ version: 0.0.1
4
+
5
+ environment:
6
+ sdk: ^3.10.0
7
+
8
+ dependencies:
9
+ jaspr: ^0.23.0
10
+ http: ^1.6.0
11
+ web: ^1.0.0
12
+
13
+ dev_dependencies:
14
+ build_runner: ^2.10.0
15
+ build_web_compilers: ^4.4.18
16
+ jaspr_builder: ^0.23.0
17
+ lints: ^5.0.0
18
+
19
+ jaspr:
20
+ mode: client
Binary file
@@ -0,0 +1,134 @@
1
+ {
2
+ "openapi": "3.0.0",
3
+ "info": {
4
+ "title": "FroggyDocs Generated API",
5
+ "version": "1.0.0",
6
+ "description": "Auto-generated by FroggyDocs"
7
+ },
8
+ "paths": {
9
+ "/api/simple": {
10
+ "get": {
11
+ "summary": "Simple setup using method 1",
12
+ "responses": {
13
+ "200": {
14
+ "description": "Successful response"
15
+ }
16
+ },
17
+ "tags": [
18
+ "Users"
19
+ ],
20
+ "requestBody": {
21
+ "content": {
22
+ "application/json": {
23
+ "schema": {
24
+ "type": "object",
25
+ "properties": {
26
+ "title": {
27
+ "type": "string",
28
+ "description": "Post title"
29
+ },
30
+ "views": {
31
+ "type": "number",
32
+ "description": "Total views count"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ },
41
+ "/api/me": {
42
+ "get": {
43
+ "summary": "Extracting from external file called mock.json using method 2",
44
+ "responses": {
45
+ "200": {
46
+ "description": "Successful response"
47
+ }
48
+ },
49
+ "tags": [
50
+ "Users"
51
+ ],
52
+ "requestBody": {
53
+ "content": {
54
+ "application/json": {
55
+ "schema": {
56
+ "type": "object",
57
+ "properties": {
58
+ "email": {
59
+ "type": "string",
60
+ "description": "Inferred from file"
61
+ },
62
+ "age": {
63
+ "type": "number",
64
+ "description": "Inferred from file"
65
+ },
66
+ "isAdmin": {
67
+ "type": "boolean",
68
+ "description": "Inferred from file"
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ },
77
+ "/api/inline-json": {
78
+ "put": {
79
+ "summary": "Extracting from inline JSON block using method 3",
80
+ "responses": {
81
+ "200": {
82
+ "description": "Successful response"
83
+ }
84
+ },
85
+ "tags": [
86
+ "Users"
87
+ ]
88
+ }
89
+ },
90
+ "/api/login": {
91
+ "post": {
92
+ "summary": "User login endpoint with inline JSON body",
93
+ "responses": {
94
+ "200": {
95
+ "description": "Successful response"
96
+ }
97
+ },
98
+ "tags": [
99
+ "Auth"
100
+ ]
101
+ }
102
+ },
103
+ "/api/register": {
104
+ "post": {
105
+ "summary": "User registration endpoint with inline JSON body",
106
+ "responses": {
107
+ "200": {
108
+ "description": "Successful response"
109
+ }
110
+ },
111
+ "tags": [
112
+ "Auth"
113
+ ]
114
+ }
115
+ }
116
+ },
117
+ "components": {
118
+ "securitySchemes": {
119
+ "bearerAuth": {
120
+ "type": "http",
121
+ "scheme": "bearer",
122
+ "bearerFormat": "JWT"
123
+ }
124
+ }
125
+ },
126
+ "tags": [
127
+ {
128
+ "name": "Users"
129
+ },
130
+ {
131
+ "name": "Auth"
132
+ }
133
+ ]
134
+ }
@@ -0,0 +1,16 @@
1
+ <svg width="210" height="274" viewBox="0 0 210 274" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M63.9999 231L122.5 273.5C122.5 273.5 169 220.5 185 209L181.5 192L198.5 201C197 186.5 188.5 169.5 188.5 169.5C197 162 204.5 153.5 204.5 125.5C204.5 114 196.5 119.5 189.5 92.5L198 93.5C198 81.5 189 66.5 189 66.5L199.5 55.5L207.5 64C207.5 64 213 43 207.5 31C202 19 163 0 150.5 0C138 0 124 22 124 22H98.4999C98.4999 22 89.5 2.5 71 2.5C50.5001 2.5 10.5 25 4.49999 33.5C-1.50001 42 1.99992 65.5 1.99992 65.5L12.9999 56L25.4999 72.5C8.99992 92.5 -7.58171e-05 123.5 3.99992 158L17.4999 153C11.9999 164.5 10.4999 186.5 14.4999 204L29.4999 198C33.9999 214 47.4999 235 59.9999 244.5C60.4999 240 63.9999 231 63.9999 231Z" fill="#09387E"/>
3
+ <path d="M81 26.5C65 39 59.5 64.5 45 77L23.5 47.5C35 35.5 62 22.5 77 22L81 26.5Z" fill="#0066B4"/>
4
+ <path d="M152.5 20.5C175 28 187.5 39 191.5 45L183.5 55C176.5 44 166.5 34.5 146 26.5L152.5 20.5Z" fill="#0066B4"/>
5
+ <path d="M57.5 212.5C69 224.5 107 241 122.5 254C140.5 237 169 204 169 204C170.5 198.5 169 184.5 167.5 177.5L182.5 182C181 176.5 180 174 175.5 170C171 166 150.542 164.097 138 163.5C127.5 163 114.5 159 112.5 146C124 147 122 153.5 132.5 154.5L167.5 157C175.5 158 191.5 151 194.5 128.5L192.5 119C171 98 188.5 83 175.5 61.5C167.632 48.487 149 35.5 131.5 35.5H100C91 35.5 77 45.5 76 59.5C71 84 54.5 91 37.5 100.5C24 109 16.5 119.5 16.5 135.5L27.5 132C38.4097 112.908 53.7479 109.544 74 106C47 119 25.5 143 26.5 181L36 177.5C37.5 189 44 211.5 54.5 223L57.5 212.5Z" fill="#0066B4"/>
6
+ <path d="M83 194.5C95.5 235 110 240.5 122.5 253.5C150.5 232 163 192 154.5 173.5C143.5 178 134 185.5 132 194.5C112.5 184.5 106 166.5 112.5 146C125.452 156.462 145.091 156.952 166.761 157.494L167 157.5C187 158 191.5 143 193 137.5C194.5 132 196.5 124 193 119C179.622 103.295 143.065 107.948 145 81.5C146.5 61 138.5 52.5 134.5 46.5C130.5 40.5 124.5 36.5 121 37.5C117.5 38.5 119.2 46 121 50.5C125.135 60.8386 138.333 70.8257 139.5 82.5C141.95 107 115.073 117.463 97 126.5C75 140 66 163 65 181.5C64 205 73 219.5 81 228C79 219.5 77 204.5 83 194.5Z" fill="#40C4FF"/>
7
+ <path d="M163 125.5C163 133.5 168.5 139 180 139C187.5 139 194 135 193.5 126C193.5 122.5 191.5 118 186 118H171C166 118 163 121.5 163 125.5Z" fill="#09387E"/>
8
+ <path d="M23.5 47.5C20.3 49.9 15.1667 54.1667 13 56L15 58.5C24 53 28.5 50.2 42.5 41C56.5 31.8 65.5 28 78.5 23.5L77 22C53.5 24 28.5 42 23.5 47.5Z" fill="black" fill-opacity="0.12"/>
9
+ <path d="M191.5 45L199.5 55.5L199 56L191 46.5C184 38.5 170.5 28.2035 151.5 21.5L152.5 20.5C160.5 23 182.5 32 191.5 45Z" fill="black" fill-opacity="0.12"/>
10
+ <path d="M109.281 80.015C101.528 79.6946 96 84.5557 96 88.961C96 93.3663 100.954 101 109.281 101C117.609 101 123.886 97.7715 123.999 93.3663C124.112 88.961 117.035 80.3354 109.281 80.015Z" fill="#09387E"/>
11
+ <path d="M109.281 80.015C101.528 79.6946 96 84.5557 96 88.961C96 93.3663 100.954 101 109.281 101C117.609 101 123.886 97.7715 123.999 93.3663C124.112 88.961 117.035 80.3354 109.281 80.015Z" stroke="#09387E"/>
12
+ <path d="M162.957 80.645C158.105 83.1798 155.508 89.44 156.906 92.2618C158.304 95.0835 165.851 97.0356 171.288 94.1601C176.121 91.6042 177.861 82.4891 175.904 80.5952C173.946 78.7013 167.182 78.4376 162.957 80.645Z" fill="#09387E"/>
13
+ <path d="M162.957 80.645C158.105 83.1798 155.508 89.44 156.906 92.2618C158.304 95.0835 165.851 97.0356 171.288 94.1601C176.121 91.6042 177.861 82.4891 175.904 80.5952C173.946 78.7013 167.182 78.4376 162.957 80.645Z" stroke="#09387E"/>
14
+ <circle cx="112" cy="92" r="6" fill="#0066B4"/>
15
+ <path d="M172 88C172 90.7614 169.761 93 167 93C164.239 93 162 90.7614 162 88C162 85.2386 164.239 83 167 83C169.761 83 172 85.2386 172 88Z" fill="#0066B4"/>
16
+ </svg>
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>FroggyDocs</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ </head>
9
+ <body>
10
+ <script defer src="main.client.dart.js"></script>
11
+ </body>
12
+ </html>