accomadesc 0.3.42 → 0.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 (39) hide show
  1. package/README.md +130 -34
  2. package/dist/AccoCard.svelte +1 -1
  3. package/dist/CalendarAvailable.svelte +1 -1
  4. package/dist/MainNav.svelte +1 -2
  5. package/dist/PageComponent.svelte +1 -2
  6. package/dist/PhotoGallery.svelte +5 -1
  7. package/dist/Pricing.svelte +81 -168
  8. package/dist/SiteState.svelte.d.ts +1 -1
  9. package/dist/SiteState.svelte.js +21 -13
  10. package/dist/basic/TextInput.svelte +69 -116
  11. package/dist/basic/TextInput.svelte.d.ts +2 -0
  12. package/dist/basic/icons/actions.d.ts +13 -0
  13. package/dist/basic/icons/actions.js +44 -0
  14. package/dist/basic/icons/navigation.d.ts +6 -0
  15. package/dist/basic/icons/navigation.js +16 -0
  16. package/dist/basic/icons/ui.d.ts +13 -0
  17. package/dist/basic/icons/ui.js +44 -0
  18. package/dist/basic/icons.d.ts +4 -0
  19. package/dist/basic/icons.js +51 -413
  20. package/dist/helpers/debounce.js +8 -2
  21. package/dist/helpers/format.js +2 -1
  22. package/dist/helpers/normalizeDate.d.ts +1 -1
  23. package/dist/helpers/normalizeDate.js +25 -16
  24. package/dist/helpers/readICS.js +21 -4
  25. package/dist/index.d.ts +1 -1
  26. package/dist/names/README.md +1 -1
  27. package/dist/names/gen.js +10 -1
  28. package/dist/occuplan/state.svelte.js +7 -3
  29. package/dist/occusplan-link/OccuPlanAvailableInfo.svelte +38 -0
  30. package/dist/occusplan-link/OccuPlanGrid.svelte +375 -0
  31. package/dist/occusplan-link/OccuPlanPicker.svelte +575 -0
  32. package/dist/occusplan-link/OccuPlanRows.svelte +368 -0
  33. package/dist/occusplan-link/OccuPlanWrapper.svelte +108 -0
  34. package/dist/occusplan-link/defaultTranslations.js +157 -0
  35. package/dist/occusplan-link/state.svelte.d.ts +92 -0
  36. package/dist/occusplan-link/state.svelte.js +424 -0
  37. package/dist/svg/LogoSVG.svelte +0 -1
  38. package/dist/types.d.ts +1 -1
  39. package/package.json +10 -4
package/README.md CHANGED
@@ -1,64 +1,160 @@
1
- # asc (Accomade Svelte Components)
1
+ # accomadesc (Accomade Svelte Components)
2
2
 
3
- This project is a collection of Svelte 5 components used throughout the different products and websites that are made with https://accoma.de.
3
+ A collection of Svelte 5 components used throughout Accomade products and websites. Provides reusable UI components for accommodation listings including calendars, pricing displays, photo galleries, booking forms, and more.
4
4
 
5
+ ## Installation
5
6
 
7
+ ```bash
8
+ npm install accomadesc
9
+ ```
6
10
 
7
- # INITIAL README - create-svelte
11
+ Peer dependencies (must be installed separately):
8
12
 
9
- Everything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
13
+ ```bash
14
+ npm install svelte @sveltejs/kit
15
+ ```
10
16
 
11
- Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
17
+ ## Quick Start
18
+
19
+ ```svelte
20
+ <script>
21
+ import { Button, AccoCard } from 'accomadesc';
22
+
23
+ const card = {
24
+ id: '1',
25
+ kind: 'acco-card',
26
+ content: {
27
+ title: 'Beach House',
28
+ photos: [{ url: '/house.jpg', alt: 'Beach house' }],
29
+ prices: { from: 150, to: 300, currency: 'EUR' },
30
+ amenities: ['wifi', 'parking'],
31
+ url: '/beach-house',
32
+ },
33
+ };
34
+ </script>
35
+
36
+ <AccoCard {...card} />
37
+ <Button variant="primary">Book Now</Button>
38
+ ```
12
39
 
13
- ## Creating a project
40
+ ## Block System
14
41
 
15
- If you're seeing this, you've probably already done this step. Congrats!
42
+ The library uses a block-based content system. Each block has:
16
43
 
17
- ```bash
18
- # create a new project in the current directory
19
- npx sv create
44
+ - `id: string` - Unique identifier
45
+ - `kind: string` - Block type discriminator
46
+ - `content: T` - Block-specific content
47
+
48
+ Use type guards for type narrowing:
20
49
 
21
- # create a new project in my-app
22
- npx sv create my-app
50
+ ```typescript
51
+ import { isBookingRequest, type AccoBlock } from 'accomadesc';
52
+
53
+ function processBlock(block: AccoBlock) {
54
+ if (isBookingRequest(block)) {
55
+ // TypeScript knows block is BookingRequest
56
+ }
57
+ }
23
58
  ```
24
59
 
25
- ## Developing
60
+ ## Core Components
26
61
 
27
- Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
62
+ ### Basic UI
28
63
 
29
- ```bash
30
- npm run dev
64
+ - `Button` - Multi-variant button component
65
+ - `TextInput` - Form text input with validation
66
+ - `Icon` - SVG icon renderer
67
+ - `Avatar` - User avatar display
68
+ - `Spinner` - Loading indicator
69
+ - `Notes` - Annotations and notes
31
70
 
32
- # or start the server and open the app in a new browser tab
33
- npm run dev -- --open
34
- ```
71
+ ### Content Display
35
72
 
36
- Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
73
+ - `AccoCard` - Accommodation summary card
74
+ - `AccoDescription` - Property description
75
+ - `AmenitiesCore` - Amenities display
76
+ - `Photo` / `PhotoGallery` - Image handling
77
+ - `Section` / `Text` - Content sections
78
+ - `LeafletMap` - Location map
79
+ - `Weather` - Weather information
37
80
 
38
- ## Building
81
+ ### Booking & Calendar
39
82
 
40
- To build your library:
83
+ - `Calendar` - Availability calendar
84
+ - `CalendarGrid` - Grid-based calendar view
85
+ - `CalendarRows` - Row-based calendar view
86
+ - `CalendarAvailable` - Availability summary
87
+ - `BookingRequest` - Booking request form
88
+ - `ContactForm` - Contact form
41
89
 
42
- ```bash
43
- npm run package
44
- ```
90
+ ### Pricing
45
91
 
46
- To create a production version of your showcase app:
92
+ - `Pricing` - Detailed pricing table
93
+ - `PricingShort` - Compact pricing display
47
94
 
48
- ```bash
49
- npm run build
95
+ ### Layout
96
+
97
+ - `PageComponent` - Full page wrapper
98
+ - `PageHeader` / `PageFooter` - Page sections
99
+ - `MainNav` / `NavItem` - Navigation
100
+
101
+ ## Internationalization
102
+
103
+ Components accept i18n functions as props:
104
+
105
+ ```svelte
106
+ <script>
107
+ import { BookingRequest } from 'accomadesc';
108
+
109
+ let props = {
110
+ translateFunc: (key: string) => translations[key],
111
+ formatMoneyFunc: (amount: number, currency: string) =>
112
+ new Intl.NumberFormat().format(amount) + ' ' + currency,
113
+ formatDateFunc: (date: Date) => date.toLocaleDateString(),
114
+ };
115
+ </script>
116
+
117
+ <BookingRequest {...props} />
50
118
  ```
51
119
 
52
- You can preview the production build with `npm run preview`.
120
+ ## State Management
53
121
 
54
- > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
122
+ Use `SiteState` for application state:
55
123
 
56
- ## Publishing
124
+ ```typescript
125
+ import { SiteState } from 'accomadesc';
57
126
 
58
- Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
127
+ const site = new SiteState({
128
+ lang: 'en',
129
+ siteConfig: { ... },
130
+ translateFunc: (key) => ...,
131
+ formatMoneyFunc: (amount, currency) => ...,
132
+ formatDateFunc: (date) => ...,
133
+ });
134
+ ```
59
135
 
60
- To publish your library to [npm](https://www.npmjs.com):
136
+ ## Development
61
137
 
62
138
  ```bash
63
- npm publish
139
+ # Install dependencies
140
+ pnpm install
141
+
142
+ # Start dev server
143
+ pnpm run dev
144
+
145
+ # Run tests
146
+ pnpm run test
147
+
148
+ # Type check
149
+ pnpm run check
150
+
151
+ # Format code
152
+ pnpm run format
153
+
154
+ # Build library
155
+ pnpm run package
64
156
  ```
157
+
158
+ ## License
159
+
160
+ MIT
@@ -21,7 +21,7 @@
21
21
  let translatedSlug = $derived(translateFunc && slug ? translateFunc(slug) : '');
22
22
  </script>
23
23
 
24
- <div class="accocard-wrapper">
24
+ <div class="accocard-wrapper" data-testid="accocard-wrapper">
25
25
  <div class="title-with-slug">
26
26
  <h2>{displayName}</h2>
27
27
  {#if slug}
@@ -20,7 +20,7 @@
20
20
  DateTime.utc().set({ day: 31, month: 12 }).plus({ years: maxFutureYears }),
21
21
  );
22
22
 
23
- const formatAvailability = (from: DateTime | null, forDays: number): string => {
23
+ const formatAvailability = (from: DateTime | null | undefined, forDays: number): string => {
24
24
  if (!formatFunc || !formatDateFunc) return '';
25
25
  if (from == null) {
26
26
  const formattedFutureDate = formatDateFunc(maxFutureDate);
@@ -21,11 +21,10 @@
21
21
 
22
22
  const pathForLang = (lang: string) => {
23
23
  const pathElements = currentPath.split('/');
24
- //initial slash results in empty string real first element
25
24
  if (pathElements.length == 1) return `/${lang}`;
26
25
 
27
26
  const firstElement = pathElements[1];
28
- if (supportedLangs?.includes(firstElement)) {
27
+ if (firstElement && supportedLangs?.includes(firstElement)) {
29
28
  return ['', lang, ...pathElements.slice(2)].join('/');
30
29
  } else {
31
30
  return ['', lang, ...pathElements.slice(1)].join('/');
@@ -53,11 +53,10 @@
53
53
  let currentPath = $derived(page.url.pathname);
54
54
  const pathForLang = (lang: string) => {
55
55
  const pathElements = currentPath.split('/');
56
- //initial slash results in empty string real first element
57
56
  if (pathElements.length == 1) return `/${lang}`;
58
57
 
59
58
  const firstElement = pathElements[1];
60
- if (supportedLangs?.includes(firstElement)) {
59
+ if (firstElement && supportedLangs?.includes(firstElement)) {
61
60
  return ['', lang, ...pathElements.slice(2)].join('/');
62
61
  } else {
63
62
  return ['', lang, ...pathElements.slice(1)].join('/');
@@ -23,7 +23,11 @@
23
23
  );
24
24
 
25
25
  let zoomed: number | null = $state(0);
26
- let zoomedPhoto: Photo | null = $derived(zoomed != null ? photos[zoomed] : null);
26
+ let zoomedPhoto: Photo | null = $derived.by(() => {
27
+ if (zoomed === null) return null;
28
+ const photo = photos[zoomed];
29
+ return photo ?? null;
30
+ });
27
31
 
28
32
  const zoom = (i: number) => {
29
33
  zoomed = i;
@@ -155,42 +155,20 @@
155
155
  return result;
156
156
  };
157
157
 
158
- const colOutputRange = (range: PricingRange, col: PricingColumn): string => {
159
- let result = '';
160
- if (!formatDateFunc || !translateFunc || !formatMoneyFunc || !formatFunc) return result;
161
-
162
- const entry = range.entry;
163
- switch (col) {
164
- case 'timeRange':
165
- result = formatRangeCol(range);
166
- break;
167
- case 'firstNight':
168
- result = formatFirstNightPriceCol(entry);
169
- break;
170
- case 'eachNight':
171
- result = formatEachNightCol(entry);
172
- break;
173
- case 'extraPerson':
174
- result = formatExtraPersonCol(entry);
175
- break;
176
- case 'minNumNights':
177
- result = formatMinNightsCol(entry);
178
- break;
179
- case 'peopleNum':
180
- result = formatPeopleNum(entry);
181
- break;
182
- }
183
- return result;
158
+ const isStaticRange = (range: PricingRange | StaticPricingRange): range is StaticPricingRange => {
159
+ return (
160
+ 'from' in range && 'to' in range && typeof range.from === 'object' && 'month' in range.from
161
+ );
184
162
  };
185
163
 
186
- const colOutputStaticRange = (range: StaticPricingRange, col: PricingColumn): string => {
164
+ const colOutput = (range: PricingRange | StaticPricingRange, col: PricingColumn): string => {
187
165
  let result = '';
188
166
  if (!formatDateFunc || !translateFunc || !formatMoneyFunc || !formatFunc) return result;
189
167
 
190
168
  const entry = range.entry;
191
169
  switch (col) {
192
170
  case 'timeRange':
193
- result = formatStaticRangeCol(range);
171
+ result = isStaticRange(range) ? formatStaticRangeCol(range) : formatRangeCol(range);
194
172
  break;
195
173
  case 'firstNight':
196
174
  result = formatFirstNightPriceCol(entry);
@@ -235,6 +213,79 @@
235
213
  </thead>
236
214
  {/snippet}
237
215
 
216
+ {#snippet pricingTable(data: PricingRange[] | StaticPricingRange[])}
217
+ {#if w > 799}
218
+ <table class="pricing-table">
219
+ {@render wideTableHead()}
220
+ <tbody>
221
+ {#each data as e}
222
+ <tr>
223
+ {#each columns as h}
224
+ <td style={colCellStyle[h]}>
225
+ {@html colOutput(e, h)}
226
+ </td>
227
+ {/each}
228
+ </tr>
229
+ {/each}
230
+ </tbody>
231
+ </table>
232
+ {:else if w > 400 && w < 800}
233
+ <table class="pricing-table">
234
+ {#each data as e}
235
+ <thead>
236
+ <tr>
237
+ <th colspan="2" scope="col">
238
+ {@html colOutput(e, 'timeRange')}
239
+ </th>
240
+ </tr>
241
+ </thead>
242
+ <tbody>
243
+ {#each columns as h}
244
+ {#if h !== 'timeRange'}
245
+ <tr>
246
+ <th scope="row">
247
+ {@html translateFunc ? translateFunc(h) : ''}:
248
+ </th>
249
+ <td>
250
+ {@html colOutput(e, h)}
251
+ </td></tr
252
+ >
253
+ {/if}
254
+ {/each}
255
+ </tbody>
256
+ {/each}
257
+ </table>
258
+ {:else}
259
+ <table class="pricing-table">
260
+ {#each data as e}
261
+ <thead>
262
+ <tr>
263
+ <th scope="col" class="main-header">
264
+ {@html colOutput(e, 'timeRange')}
265
+ </th>
266
+ </tr>
267
+ </thead>
268
+ <tbody>
269
+ {#each columns as h}
270
+ {#if h !== 'timeRange'}
271
+ <tr>
272
+ <th scope="row">
273
+ {@html translateFunc ? translateFunc(h) : h}:
274
+ </th>
275
+ </tr>
276
+ <tr>
277
+ <td>
278
+ {@html colOutput(e, h)}
279
+ </td>
280
+ </tr>
281
+ {/if}
282
+ {/each}
283
+ </tbody>
284
+ {/each}
285
+ </table>
286
+ {/if}
287
+ {/snippet}
288
+
238
289
  {#key currentLang}
239
290
  <figure bind:clientWidth={w} class="pricing-wrapper">
240
291
  {#if global}
@@ -281,148 +332,10 @@
281
332
  </table>
282
333
  {/if}
283
334
  {#if staticRanges.length > 0}
284
- {#if w > 799}
285
- <table class="pricing-table">
286
- {@render wideTableHead()}
287
- <tbody>
288
- {#each staticRanges as e}
289
- <tr>
290
- {#each columns as h}
291
- <td style={colCellStyle[h]}>
292
- {@html colOutputStaticRange(e, h)}
293
- </td>
294
- {/each}
295
- </tr>
296
- {/each}
297
- </tbody>
298
- </table>
299
- {:else if w > 400 && w < 800}
300
- <table class="pricing-table">
301
- {#each staticRanges as e}
302
- <thead>
303
- <tr>
304
- <th colspan="2" scope="col">
305
- {@html colOutputStaticRange(e, 'timeRange')}
306
- </th>
307
- </tr>
308
- </thead>
309
- <tbody>
310
- {#each columns as h}
311
- {#if h !== 'timeRange'}
312
- <tr>
313
- <th scope="row">
314
- {@html translateFunc ? translateFunc(h) : ''}:
315
- </th>
316
- <td>
317
- {@html colOutputStaticRange(e, h)}
318
- </td></tr
319
- >
320
- {/if}
321
- {/each}
322
- </tbody>
323
- {/each}
324
- </table>
325
- {:else}
326
- <table class="pricing-table">
327
- {#each staticRanges as e}
328
- <thead>
329
- <tr>
330
- <th scope="col" class="main-header">
331
- {@html colOutputStaticRange(e, 'timeRange')}
332
- </th>
333
- </tr>
334
- </thead>
335
- <tbody>
336
- {#each columns as h}
337
- {#if h !== 'timeRange'}
338
- <tr>
339
- <th scope="row">
340
- {@html translateFunc ? translateFunc(h) : h}:
341
- </th>
342
- </tr>
343
- <tr>
344
- <td>
345
- {@html colOutputStaticRange(e, h)}
346
- </td>
347
- </tr>
348
- {/if}
349
- {/each}
350
- </tbody>
351
- {/each}
352
- </table>
353
- {/if}
335
+ {@render pricingTable(staticRanges)}
354
336
  {/if}
355
337
  {#if filteredRanges.length > 0}
356
- {#if w > 799}
357
- <table class="pricing-table">
358
- {@render wideTableHead()}
359
- <tbody>
360
- {#each filteredRanges as e}
361
- <tr>
362
- {#each columns as h}
363
- <td style={colCellStyle[h]}>
364
- {@html colOutputRange(e, h)}
365
- </td>
366
- {/each}
367
- </tr>
368
- {/each}
369
- </tbody>
370
- </table>
371
- {:else if w > 400 && w < 800}
372
- <table class="pricing-table">
373
- {#each filteredRanges as e}
374
- <thead>
375
- <tr>
376
- <th colspan="2" scope="col">
377
- {@html colOutputRange(e, 'timeRange')}
378
- </th>
379
- </tr>
380
- </thead>
381
- <tbody>
382
- {#each columns as h}
383
- {#if h !== 'timeRange'}
384
- <tr>
385
- <th scope="row">
386
- {@html translateFunc ? translateFunc(h) : ''}:
387
- </th>
388
- <td>
389
- {@html colOutputRange(e, h)}
390
- </td></tr
391
- >
392
- {/if}
393
- {/each}
394
- </tbody>
395
- {/each}
396
- </table>
397
- {:else}
398
- <table class="pricing-table">
399
- {#each filteredRanges as e}
400
- <thead>
401
- <tr>
402
- <th scope="col" class="main-header">
403
- {@html colOutputRange(e, 'timeRange')}
404
- </th>
405
- </tr>
406
- </thead>
407
- <tbody>
408
- {#each columns as h}
409
- {#if h !== 'timeRange'}
410
- <tr>
411
- <th scope="row">
412
- {@html translateFunc ? translateFunc(h) : h}:
413
- </th>
414
- </tr>
415
- <tr>
416
- <td>
417
- {@html colOutputRange(e, h)}
418
- </td>
419
- </tr>
420
- {/if}
421
- {/each}
422
- </tbody>
423
- {/each}
424
- </table>
425
- {/if}
338
+ {@render pricingTable(filteredRanges)}
426
339
  {/if}
427
340
 
428
341
  {#if footnote}
@@ -25,7 +25,7 @@ export declare class SiteState implements I18nFacade {
25
25
  constructor(getSiteConfig: () => SiteConfig, lang: string | undefined);
26
26
  updateCurrentLang: (lang: string) => string;
27
27
  translateFunc: (ref: string) => string;
28
- formatFunc: (ref: string, props: Record<string, any>) => string;
28
+ formatFunc: (ref: string, props: Record<string, unknown>) => string;
29
29
  formatDateFunc: (d: DateTime | string) => string;
30
30
  translateWithLangFunc: (ref: string, lang: string) => string;
31
31
  }
@@ -12,7 +12,6 @@ export class SiteState {
12
12
  return s;
13
13
  }, {});
14
14
  }
15
- ;
16
15
  get cookieTranslations() {
17
16
  return Object.entries(this.fullTranslations).reduce((s, e) => {
18
17
  s[e[0]] = e[1].cookies;
@@ -39,10 +38,10 @@ export class SiteState {
39
38
  return this.siteTranslations;
40
39
  }
41
40
  get calendarTranslation() {
42
- return this.calendarTranslations[this.currentLang];
41
+ return this.calendarTranslations[this.currentLang] ?? {};
43
42
  }
44
43
  get cookieTranslation() {
45
- return this.cookieTranslations[this.currentLang];
44
+ return this.cookieTranslations[this.currentLang] ?? {};
46
45
  }
47
46
  get formats() {
48
47
  return this._getSiteConfigFn().lang.formats;
@@ -63,12 +62,20 @@ export class SiteState {
63
62
  if (!ref)
64
63
  return '[UNDEF]';
65
64
  const current = this.translations[this.currentLang];
66
- if (!current[ref])
65
+ if (!current)
67
66
  return '';
68
- return this.translations[this.currentLang][ref];
67
+ const value = current[ref];
68
+ if (!value)
69
+ return '';
70
+ return value;
69
71
  };
70
72
  formatFunc = (ref, props) => {
71
- const fString = this.formats[this.currentLang][ref];
73
+ const langFormats = this.formats[this.currentLang];
74
+ if (!langFormats) {
75
+ console.warn(`[Missing format language: ${this.currentLang}]`);
76
+ return '[UNDEF]';
77
+ }
78
+ const fString = langFormats[ref];
72
79
  if (!fString) {
73
80
  console.warn(`[Missing formatFunc: ${ref}]`);
74
81
  return '[UNDEF]';
@@ -78,10 +85,13 @@ export class SiteState {
78
85
  };
79
86
  formatDateFunc = (d) => {
80
87
  if (!d)
81
- return this.translateFunc("invalid");
88
+ return this.translateFunc('invalid');
82
89
  const formatSpecs = this.formats[this.currentLang];
90
+ if (!formatSpecs) {
91
+ return this.translateFunc('invalid');
92
+ }
83
93
  let f = 'yyyy-MM-dd';
84
- if (formatSpecs?.dateFormat) {
94
+ if (formatSpecs.dateFormat) {
85
95
  f = formatSpecs.dateFormat;
86
96
  }
87
97
  let date;
@@ -89,17 +99,15 @@ export class SiteState {
89
99
  date = DateTime.fromISO(d);
90
100
  else
91
101
  date = d;
92
- // if d was invalid to begin with or
93
- // translformation from ISO didn't yield a valid DateTime object
94
- if (date.isValid)
95
- return this.translateFunc("invalid");
102
+ if (!date.isValid)
103
+ return this.translateFunc('invalid');
96
104
  return date.setLocale(formatSpecs.locale).toFormat(f);
97
105
  };
98
106
  translateWithLangFunc = (ref, lang) => {
99
107
  const translation = this.translations[lang];
100
108
  if (!translation) {
101
109
  console.error(`[Tried to access unknown translation: ${lang}]`);
102
- return "[UNDEF]";
110
+ return '[UNDEF]';
103
111
  }
104
112
  const res = translation[ref];
105
113
  if (res === undefined) {