@monimesl/dot-cli 1.0.0-beta.7 → 1.0.0-beta.9

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 (26) hide show
  1. package/dist/templates/showcase/dot.ts +19 -1
  2. package/dist/templates/showcase/pages/basics/theme/controller.ts +28 -0
  3. package/dist/templates/showcase/pages/basics/theme/state.ts +4 -0
  4. package/dist/templates/showcase/pages/basics/theme/view.dml +50 -0
  5. package/dist/templates/showcase/pages/components/capture/controller.ts +25 -0
  6. package/dist/templates/showcase/pages/components/capture/state.ts +7 -0
  7. package/dist/templates/showcase/pages/components/capture/view.dml +32 -0
  8. package/dist/templates/showcase/pages/components/forms/view.dml +1 -1
  9. package/dist/templates/showcase/pages/components/layout/view.dml +13 -0
  10. package/dist/templates/showcase/pages/components/pinned/controller.ts +10 -0
  11. package/dist/templates/showcase/pages/components/pinned/state.ts +25 -0
  12. package/dist/templates/showcase/pages/components/pinned/view.dml +42 -0
  13. package/dist/templates/showcase/pages/components/styles/view.dml +37 -2
  14. package/dist/templates/showcase/pages/components/tabs/view.dml +4 -4
  15. package/dist/templates/showcase/pages/dash/cache/controller.ts +43 -17
  16. package/dist/templates/showcase/pages/dash/cache/state.ts +1 -0
  17. package/dist/templates/showcase/pages/dash/cache/view.dml +4 -0
  18. package/dist/templates/showcase/pages/dash/files/controller.ts +62 -32
  19. package/dist/templates/showcase/pages/dash/files/state.ts +1 -0
  20. package/dist/templates/showcase/pages/dash/files/view.dml +4 -0
  21. package/dist/templates/showcase/pages/dash/secrets/controller.ts +35 -15
  22. package/dist/templates/showcase/pages/dash/secrets/state.ts +1 -0
  23. package/dist/templates/showcase/pages/dash/secrets/view.dml +4 -0
  24. package/dist/templates/showcase/pages/index/state.ts +20 -2
  25. package/dist/templates/showcase/statics/icons/ticket-stub.svg +1 -0
  26. package/package.json +6 -6
@@ -16,6 +16,7 @@ export default defineDot({
16
16
  'basics/hooks',
17
17
  'basics/bars',
18
18
  'basics/colors',
19
+ 'basics/theme',
19
20
  'basics/share',
20
21
  'basics/concert',
21
22
  // What a view can show.
@@ -28,6 +29,8 @@ export default defineDot({
28
29
  'components/carousel',
29
30
  'components/lists',
30
31
  'components/tabs',
32
+ 'components/pinned',
33
+ 'components/capture',
31
34
  'components/forms',
32
35
  'components/money',
33
36
  'components/feedback',
@@ -47,16 +50,31 @@ export default defineDot({
47
50
  'dash/secrets',
48
51
  'dash/calls',
49
52
  ],
50
- // The showcase's own colours, used by name on the Styles page, and the Page colours swatches.
53
+ // How every page looks. The Theme page explains each setting.
51
54
  theme: {
55
+ // Turn one of these on to see it on every page:
56
+ // font: 'rounded',
57
+ // headingFont: 'serif',
58
+ // headingWeight: 'medium',
59
+ // textSize: 'large',
60
+ // shape: 'rounded',
61
+ // density: 'compact',
62
+ // elevation: 'raised',
63
+ // components: { Button: { size: 'large' }, View: { variants: { card: { border: 'none' } } } },
64
+ // The showcase's own colours, used by name on the Styles page, and the Page colours swatches.
52
65
  colors: {
53
66
  custom: {
54
67
  gold: { light: '#f5c542', dark: '#d4a72c' },
55
68
  // Dark text that reads on gold in both light and dark.
56
69
  ink: { light: '#1c1c1e', dark: '#1c1c1e' },
70
+ amber: { light: '#e8952a', dark: '#f0a63a' },
57
71
  ...swatchColors(),
58
72
  },
59
73
  },
74
+ // The showcase's own icon, an SVG in statics/, used by name like Dot's icons.
75
+ icons: { 'ticket-stub': 'statics/icons/ticket-stub.svg' },
76
+ // A gradient between two of the theme's colours, for a View's background.
77
+ gradients: { goldenHour: { from: 'gold', to: 'amber', direction: 'diagonal' } },
60
78
  },
61
79
  // logger() writes each page load, event, navigation and state change to the console.
62
80
  plugins: [logger()],
@@ -0,0 +1,28 @@
1
+ import state from './state';
2
+
3
+ // Sierra Leone's green and blue, darker in light mode so text in them reads on white.
4
+ const FLAG_TONES = {
5
+ success: { light: '#178a33', dark: '#4cd471' },
6
+ info: { light: '#0060a8', dark: '#5aa9ff' },
7
+ } as const;
8
+
9
+ export default definePage({
10
+ initialState: state,
11
+ configs: { navigationBar: { title: 'Theme' } },
12
+ actions: {
13
+ // A page can't change the theme, but setColors changes its colours, tones included. Undefined
14
+ // puts the theme's own back.
15
+ setFlagTones(_state, event: CheckedEvent) {
16
+ const flagTones = event.detail.checked;
17
+ this.setState({ flagTones });
18
+ if (flagTones) {
19
+ this.setColors(FLAG_TONES);
20
+ } else {
21
+ this.setColors({
22
+ success: { light: undefined, dark: undefined },
23
+ info: { light: undefined, dark: undefined },
24
+ });
25
+ }
26
+ },
27
+ },
28
+ });
@@ -0,0 +1,4 @@
1
+ export default {
2
+ // Whether the page uses the flag's green and blue for its tones, in place of the theme's.
3
+ flagTones: false,
4
+ };
@@ -0,0 +1,50 @@
1
+ <View>
2
+ <Text variant="title">Theme</Text>
3
+ <Text>A dot has no CSS. The theme in dot.ts sets how every page looks, and a page can't change it while it runs. To see a setting below, turn it on in dot.ts, where a commented line has each one, then reload.</Text>
4
+
5
+ <View gap="small">
6
+ <Text variant="subtitle">colors</Text>
7
+ <Text variant="caption" tone="muted">primary, background, surface, text, muted, border and subtle, and the tones info, success, warning and danger, each for light and dark. A colour left out is Dash's. A page can change its own with setColors, as this switch does for two tones.</Text>
8
+ <View variant="card" gap="small">
9
+ <View direction="row" gap="small" wrap>
10
+ <Button tone="info">Details</Button>
11
+ <Button tone="success">Pay SLE 120</Button>
12
+ <Button variant="secondary" tone="danger">Refund</Button>
13
+ </View>
14
+ <View direction="row" gap="small" wrap>
15
+ <Badge tone="info">New</Badge>
16
+ <Badge tone="success">Paid</Badge>
17
+ <Badge tone="warning">Due Friday</Badge>
18
+ <Badge tone="danger">Failed</Badge>
19
+ </View>
20
+ <Switch label="Use the flag's green and blue" checked="{{ flagTones }}" onChange="setFlagTones" />
21
+ </View>
22
+ </View>
23
+
24
+ <View gap="small">
25
+ <Text variant="subtitle">shape, density and elevation</Text>
26
+ <Text variant="caption" tone="muted">shape is how round corners are: square, subtle, soft (the default) or rounded. density is how much room gaps and padding take: compact, regular (the default) or comfortable. elevation: "raised" gives every card and list a soft shadow in place of its hairline.</Text>
27
+ </View>
28
+
29
+ <View gap="small">
30
+ <Text variant="subtitle">font, headingFont, headingWeight and textSize</Text>
31
+ <Text variant="caption" tone="muted">font is one of the phone's own fonts, so nothing is downloaded: system (the default), rounded, serif or mono. headingFont and headingWeight give titles their own. textSize, small, regular or large, makes text a tenth smaller or larger, except what's typed into fields, amounts and codes.</Text>
32
+ </View>
33
+
34
+ <View gap="small">
35
+ <Text variant="subtitle">components</Text>
36
+ <Text variant="caption" tone="muted">Default props for every use of a component, or for one of its variants, so a dot sets its look once. Button: { size: "large" } makes every Button large. View: { variants: { card: { elevation: "low" } } } gives only cards a shadow. A prop in the markup still wins.</Text>
37
+ </View>
38
+
39
+ <View gap="small">
40
+ <Text variant="subtitle">icons and gradients</Text>
41
+ <Text variant="caption" tone="muted">icons names SVGs in statics/, such as ticket-stub here. They work wherever Dot's icons do, and take the icon's tone. The build refuses an SVG with scripts or links, so export it from your design tool as plain shapes. gradients names a gradient between two of the theme's colours, for a View's background, such as goldenHour.</Text>
42
+ <View background="goldenHour" radius="large" padding="large" gap="small">
43
+ <View direction="row" align="center" gap="small">
44
+ <Icon name="ticket-stub" tone="ink" />
45
+ <Text weight="bold" tone="ink">Freetown Jazz Night</Text>
46
+ </View>
47
+ <Text variant="title" tone="ink">2 tickets</Text>
48
+ </View>
49
+ </View>
50
+ </View>
@@ -0,0 +1,25 @@
1
+ import state from './state';
2
+
3
+ export default definePage({
4
+ initialState: state,
5
+ configs: { navigationBar: { title: 'Pictures of a View' } },
6
+ actions: {
7
+ async capture() {
8
+ this.setState({ capturing: true, note: '' });
9
+ try {
10
+ const picture = await this.captureView('ticket');
11
+ this.setState({ picture, note: `${Math.round(picture.size / 1024)} KB` });
12
+ } catch (error) {
13
+ this.setState({ note: (error as Error).message });
14
+ } finally {
15
+ this.setState({ capturing: false });
16
+ }
17
+ },
18
+ async share() {
19
+ const { picture } = this.state;
20
+ if (picture) {
21
+ await this.dash.system.share({ file: picture });
22
+ }
23
+ },
24
+ },
25
+ });
@@ -0,0 +1,7 @@
1
+ import type { DotFile } from '@monimesl/dot-runtime';
2
+
3
+ export default {
4
+ capturing: false,
5
+ picture: null as DotFile | null,
6
+ note: '',
7
+ };
@@ -0,0 +1,32 @@
1
+ <View>
2
+ <Text variant="title">Pictures of a View</Text>
3
+ <Text>Give a View an id, and this.captureView makes a PNG of it, such as a ticket to share. It draws the whole View, even past the bottom of the screen.</Text>
4
+
5
+ <View id="ticket" variant="card" gap="medium">
6
+ <View direction="row" justify="between" align="center">
7
+ <Text variant="subtitle">Lumley Beach Sundown</Text>
8
+ <Icon name="ticket-stub" tone="gold" />
9
+ </View>
10
+ <Divider />
11
+ <DetailRow label="Name" value="Mariama Kamara" />
12
+ <DetailRow label="Where" value="Lumley Beach, Freetown" />
13
+ <DetailRow label="When" value="Friday, 6 pm" />
14
+ <DetailRow label="Paid" value="Le 300" />
15
+ <View align="center">
16
+ <QRCode value="SALONE-SOUNDS-4471" />
17
+ </View>
18
+ </View>
19
+
20
+ <Button loading="{{ capturing }}" onClick="capture">Make a picture</Button>
21
+ <If condition="{{ note }}">
22
+ <Text variant="caption" tone="muted">{{ note }}</Text>
23
+ </If>
24
+
25
+ <If condition="{{ picture }}">
26
+ <View gap="medium">
27
+ <Text variant="caption" tone="muted">captureView gives back a file, as a download does. Show it with an Image, or pass it to dash.system.share. A new picture of a View replaces the last one. A picture from another site shows only if that site allows CORS.</Text>
28
+ <Image src="{{ picture.url }}" alt="The ticket as a picture" />
29
+ <Button variant="secondary" onClick="share">Share the picture</Button>
30
+ </View>
31
+ </If>
32
+ </View>
@@ -90,7 +90,7 @@
90
90
  <Notice tone="danger" title="Not booked">{{ failure }}</Notice>
91
91
  </If>
92
92
 
93
- <Text variant="caption" tone="muted">A Footer keeps its buttons at the bottom of the screen. Put it last in the page's View.</Text>
93
+ <Text variant="caption" tone="muted">A Footer keeps its buttons at the bottom of the screen, on a bar from side to side. Put it last in the page's View.</Text>
94
94
  <Footer>
95
95
  <Button onClick="send" loading="{{ sending }}" disabled="{{ !agreed }}">Buy tickets</Button>
96
96
  </Footer>
@@ -111,6 +111,19 @@
111
111
  </View>
112
112
  </View>
113
113
 
114
+ <View gap="small">
115
+ <Text variant="subtitle">Cards in a Scroll</Text>
116
+ <Text variant="caption" tone="muted">A Scroll leaves room above and below for a low shadow, so a card's shadow isn't cut. A high shadow needs more than that, so use low here. paddingStart and paddingEnd give the first and last card room at the sides.</Text>
117
+ <Scroll gap="medium" paddingStart="medium" paddingEnd="medium">
118
+ <For each="{{ releases }}" item="release" key="id">
119
+ <View variant="card" elevation="low" border="none" width="144" gap="none">
120
+ <Text weight="medium" maxLines="1">{{ release.title }}</Text>
121
+ <Text variant="caption" tone="muted">{{ release.year }}</Text>
122
+ </View>
123
+ </For>
124
+ </Scroll>
125
+ </View>
126
+
114
127
  <View gap="small">
115
128
  <Text variant="subtitle">Grid</Text>
116
129
  <Text variant="caption" tone="muted">A Grid keeps its columns the same width, 4 unless columns says. A GridItem shows an icon or a picture, with a label and an optional badge.</Text>
@@ -0,0 +1,10 @@
1
+ import state from './state';
2
+
3
+ export default definePage({
4
+ initialState: state,
5
+ // This page has its own tabs at the bottom, so it hides Dash's. Dash brings them back on leaving.
6
+ configs: { navigationBar: { title: 'Pinned bars' }, tabBar: { hidden: true } },
7
+ mutations: {
8
+ chooseTab: (_state, event: ValueEvent) => ({ tab: event.detail.value }),
9
+ },
10
+ });
@@ -0,0 +1,25 @@
1
+ export default {
2
+ tab: 'shows',
3
+ shows: [
4
+ { id: 'lumley', title: 'Lumley Beach Sundown', place: 'Lumley Beach, Freetown', day: 'Fri' },
5
+ { id: 'aberdeen', title: 'Aberdeen Live', place: 'Aberdeen, Freetown', day: 'Fri' },
6
+ { id: 'wilberforce', title: 'Hilltop Sessions', place: 'Wilberforce, Freetown', day: 'Sat' },
7
+ { id: 'bo', title: 'Bo Town Jam', place: 'Coronation Field, Bo', day: 'Sat' },
8
+ { id: 'kenema', title: 'Eastern Beats', place: 'Kenema', day: 'Sat' },
9
+ { id: 'makeni', title: 'Makeni Nights', place: 'Wusum Stadium, Makeni', day: 'Sun' },
10
+ { id: 'tokeh', title: 'Tokeh Sands', place: 'Tokeh Beach', day: 'Sun' },
11
+ { id: 'kono', title: 'Koidu Block Party', place: 'Koidu, Kono', day: 'Sun' },
12
+ { id: 'bonthe', title: 'Bonthe Island Drums', place: 'Bonthe, Sherbro Island', day: 'Sun' },
13
+ { id: 'monrovia', title: 'Mano River Night', place: 'Sinkor, Monrovia', day: 'Mon' },
14
+ ],
15
+ saved: [
16
+ { id: 'lumley', title: 'Lumley Beach Sundown', place: 'Lumley Beach, Freetown' },
17
+ { id: 'bo', title: 'Bo Town Jam', place: 'Coronation Field, Bo' },
18
+ ],
19
+ venues: [
20
+ { id: 'national', name: 'National Stadium', town: 'Freetown' },
21
+ { id: 'bintumani', name: 'Bintumani Hotel', town: 'Freetown' },
22
+ { id: 'coronation', name: 'Coronation Field', town: 'Bo' },
23
+ { id: 'wusum', name: 'Wusum Stadium', town: 'Makeni' },
24
+ ],
25
+ };
@@ -0,0 +1,42 @@
1
+ <View>
2
+ <Pinned edge="top" background="surface" border="subtle" paddingTop="medium" paddingBottom="medium">
3
+ <Text weight="bold">Salone gig guide</Text>
4
+ </Pinned>
5
+
6
+ <Text variant="title">Pinned bars</Text>
7
+ <Text>A Pinned keeps its children on an edge of the screen while the page scrolls. Scroll this page: the title above and the tabs below stay put. Put edge="top" first in the page's View, and edge="bottom" last. At the bottom, use a Pinned or a Footer, not both.</Text>
8
+ <Text variant="caption" tone="muted">The bar reaches past the page's padding to both sides of the screen, and its children line up with the page. background fills the bar, and border draws a line on the side facing the page.</Text>
9
+ <Text variant="caption" tone="muted">Tabs in a Pinned make the page's own navigation. variant="bar" shares the width equally, with each Tab's icon over its label and its badge on the icon. The page shows the content for the chosen tab, so the state of each tab stays. This page sets tabBar: { hidden: true } in its configs, so Dash's tabs don't show under its own.</Text>
10
+
11
+ <If condition="{{ tab === 'shows' }}">
12
+ <List>
13
+ <For each="{{ shows }}" item="show" key="id">
14
+ <ListItem icon="music" title="{{ show.title }}" subtitle="{{ show.place }}">
15
+ <Badge>{{ show.day }}</Badge>
16
+ </ListItem>
17
+ </For>
18
+ </List>
19
+ </If>
20
+ <ElseIf condition="{{ tab === 'saved' }}">
21
+ <List>
22
+ <For each="{{ saved }}" item="show" key="id">
23
+ <ListItem icon="heart" title="{{ show.title }}" subtitle="{{ show.place }}" />
24
+ </For>
25
+ </List>
26
+ </ElseIf>
27
+ <Else>
28
+ <List>
29
+ <For each="{{ venues }}" item="venue" key="id">
30
+ <ListItem icon="home" title="{{ venue.name }}" subtitle="{{ venue.town }}" />
31
+ </For>
32
+ </List>
33
+ </Else>
34
+
35
+ <Pinned edge="bottom" background="surface" border="subtle" paddingBottom="medium">
36
+ <Tabs variant="bar" value="{{ tab }}" onChange="chooseTab">
37
+ <Tab value="shows" label="Shows" icon="music" />
38
+ <Tab value="saved" label="Saved" icon="heart" badge="{{ saved.length }}" />
39
+ <Tab value="venues" label="Venues" icon="location" />
40
+ </Tabs>
41
+ </Pinned>
42
+ </View>
@@ -39,8 +39,8 @@
39
39
  </View>
40
40
 
41
41
  <View gap="small">
42
- <Text variant="subtitle">Theme colours</Text>
43
- <Text variant="caption" tone="muted">The theme in dot.ts can set primary, background, surface, text, muted, border and subtle, for light and dark. Left out, they're Dash's. The theme can also name its own colours, such as gold and ink here. Use them as a background or a text tone.</Text>
42
+ <Text variant="subtitle">The dot's own colours</Text>
43
+ <Text variant="caption" tone="muted">The theme in dot.ts can name its own colours, such as gold and ink here. Use them as a background or a text tone. The Theme page has the rest of the theme.</Text>
44
44
  <View direction="row" gap="small" align="center">
45
45
  <View background="gold" radius="full" paddingTop="small" paddingBottom="small" paddingStart="medium" paddingEnd="medium">
46
46
  <Text weight="medium" tone="ink">Gold member</Text>
@@ -63,6 +63,41 @@
63
63
  </View>
64
64
  </View>
65
65
 
66
+ <View gap="small">
67
+ <Text variant="subtitle">elevation</Text>
68
+ <Text variant="caption" tone="muted">A shadow lifts a box off the page. low suits a card, and high something that floats over others.</Text>
69
+ <View direction="row" gap="large" padding="medium">
70
+ <View grow variant="card" elevation="low" border="none" align="center">
71
+ <Text>low</Text>
72
+ </View>
73
+ <View grow variant="card" elevation="high" border="none" align="center">
74
+ <Text>high</Text>
75
+ </View>
76
+ </View>
77
+ </View>
78
+
79
+ <View gap="small">
80
+ <Text variant="subtitle">Buttons</Text>
81
+ <Text variant="caption" tone="muted">variant is primary, secondary, outline or ghost. tone colours a button, such as danger for one that deletes. size is small, medium or large.</Text>
82
+ <View direction="row" gap="small" wrap>
83
+ <Button>Primary</Button>
84
+ <Button variant="secondary">Secondary</Button>
85
+ <Button variant="outline">Outline</Button>
86
+ <Button variant="ghost">Ghost</Button>
87
+ </View>
88
+ <View direction="row" gap="small" wrap>
89
+ <Button tone="danger">Delete</Button>
90
+ <Button variant="secondary" tone="success">Mark paid</Button>
91
+ <Button variant="outline" tone="warning">Remind me</Button>
92
+ <Button variant="ghost" tone="danger">Cancel</Button>
93
+ </View>
94
+ <View direction="row" gap="small" align="center" wrap>
95
+ <Button size="small" variant="secondary">Small</Button>
96
+ <Button size="medium" variant="secondary">Medium</Button>
97
+ <Button size="large" variant="secondary">Large</Button>
98
+ </View>
99
+ </View>
100
+
66
101
  <View gap="small">
67
102
  <Text variant="subtitle">width and height</Text>
68
103
  <Text variant="caption" tone="muted">In points, or fill for all the room there is.</Text>
@@ -4,11 +4,11 @@
4
4
 
5
5
  <View gap="small">
6
6
  <Text variant="subtitle">Tabs and Chip</Text>
7
- <Text variant="caption" tone="muted">Tabs sends onChange with the chosen tab's value in event.detail.value. A Tab's badge shows a count. A Chip is selected only when the page says so. An Input with type="search" and clearable makes a search box.</Text>
7
+ <Text variant="caption" tone="muted">Tabs sends onChange with the chosen tab's value in event.detail.value. A Tab's icon goes before its label, and its badge shows a count. A Chip is selected only when the page says so. An Input with type="search" and clearable makes a search box.</Text>
8
8
  <Tabs variant="underline" value="{{ tab }}" onChange="chooseTab">
9
9
  <Tab value="all" label="All" />
10
- <Tab value="album" label="Albums" badge="{{ albums }}" />
11
- <Tab value="song" label="Songs" badge="{{ songs }}" />
10
+ <Tab value="album" label="Albums" icon="playlist" badge="{{ albums }}" />
11
+ <Tab value="song" label="Songs" icon="music" badge="{{ songs }}" />
12
12
  </Tabs>
13
13
  <Input type="search" placeholder="Search by title" clearable value="{{ query }}" onChange="search" />
14
14
  <View direction="row" gap="small" wrap>
@@ -43,7 +43,7 @@
43
43
 
44
44
  <View gap="small">
45
45
  <Text variant="subtitle">variant="segmented"</Text>
46
- <Text variant="caption" tone="muted">For a few short choices. Shows at home and in the Mano River region.</Text>
46
+ <Text variant="caption" tone="muted">For a few short choices. Shows at home and in the Mano River region. For a page's own navigation, variant="bar" is on the Pinned bars page.</Text>
47
47
  <Tabs variant="segmented" value="{{ where }}" onChange="chooseWhere">
48
48
  <Tab value="home" label="Sierra Leone" />
49
49
  <Tab value="abroad" label="Abroad" />
@@ -1,3 +1,5 @@
1
+ import { errorMessage } from '@monimesl/dot-runtime';
2
+
1
3
  import state from './state';
2
4
 
3
5
  const RECENT = 'recent/';
@@ -16,37 +18,61 @@ export default definePage({
16
18
  // and the cache up to 64 MB. A full cache rejects with 'failed'.
17
19
  actions: {
18
20
  async refresh() {
19
- const keys = await this.dash.storage.cache.keys();
20
- const recent: string[] = [];
21
- for (const key of keys.filter((each) => each.startsWith(RECENT))) {
22
- // get gives null for a key that's missing or expired.
23
- recent.push((await this.dash.storage.cache.get<string>(key)) ?? '');
21
+ try {
22
+ const keys = await this.dash.storage.cache.keys();
23
+ const recent: string[] = [];
24
+ for (const key of keys.filter((each) => each.startsWith(RECENT))) {
25
+ // get gives null for a key that's missing or expired.
26
+ recent.push((await this.dash.storage.cache.get<string>(key)) ?? '');
27
+ }
28
+ const hasGenre = await this.dash.storage.cache.exists(GENRE);
29
+ this.setState({
30
+ recent,
31
+ genre: hasGenre ? ((await this.dash.storage.cache.get<string>(GENRE)) ?? '') : '',
32
+ keys: keys.join(', '),
33
+ });
34
+ } catch (error) {
35
+ this.setState({ note: errorMessage(error) });
24
36
  }
25
- const hasGenre = await this.dash.storage.cache.exists(GENRE);
26
- this.setState({
27
- recent,
28
- genre: hasGenre ? ((await this.dash.storage.cache.get<string>(GENRE)) ?? '') : '',
29
- keys: keys.join(', '),
30
- });
31
37
  },
32
38
  // Each view lasts 30 seconds, then the cache forgets it.
33
39
  async view(_state, event: DataEvent<{ id: string; name: string }>) {
34
- await this.dash.storage.cache.set(`${RECENT}${event.data.id}`, event.data.name, {
35
- ttl: RECENT_MILLISECONDS,
36
- });
40
+ try {
41
+ await this.dash.storage.cache.set(`${RECENT}${event.data.id}`, event.data.name, {
42
+ ttl: RECENT_MILLISECONDS,
43
+ });
44
+ this.setState({ note: '' });
45
+ } catch (error) {
46
+ this.setState({ note: errorMessage(error) });
47
+ }
37
48
  await this.actions.refresh();
38
49
  },
39
50
  // With no ttl, the genre lasts until it's removed.
40
51
  async setGenre(_state, event: ValueEvent) {
41
- await this.dash.storage.cache.set(GENRE, event.detail.value);
52
+ try {
53
+ await this.dash.storage.cache.set(GENRE, event.detail.value);
54
+ this.setState({ note: '' });
55
+ } catch (error) {
56
+ this.setState({ note: errorMessage(error) });
57
+ }
42
58
  await this.actions.refresh();
43
59
  },
44
60
  async forgetGenre() {
45
- await this.dash.storage.cache.remove(GENRE);
61
+ try {
62
+ await this.dash.storage.cache.remove(GENRE);
63
+ this.setState({ note: '' });
64
+ } catch (error) {
65
+ this.setState({ note: errorMessage(error) });
66
+ }
46
67
  await this.actions.refresh();
47
68
  },
48
69
  async clear() {
49
- await this.dash.storage.cache.clear();
70
+ try {
71
+ await this.dash.storage.cache.clear();
72
+ this.setState({ note: '' });
73
+ } catch (error) {
74
+ this.setState({ note: errorMessage(error) });
75
+ }
50
76
  await this.actions.refresh();
51
77
  },
52
78
  },
@@ -14,4 +14,5 @@ export default {
14
14
  recent: [] as string[],
15
15
  genre: '',
16
16
  keys: '',
17
+ note: '',
17
18
  };
@@ -45,4 +45,8 @@
45
45
  </View>
46
46
  </View>
47
47
  </View>
48
+
49
+ <If condition="{{ note }}">
50
+ <Notice tone="danger">{{ note }}</Notice>
51
+ </If>
48
52
  </View>
@@ -1,3 +1,5 @@
1
+ import { errorMessage } from '@monimesl/dot-runtime';
2
+
1
3
  import type { Archive, Song } from './state';
2
4
  import state from './state';
3
5
 
@@ -18,58 +20,86 @@ export default definePage({
18
20
  // there rejects with 'missing', while stat gives null.
19
21
  actions: {
20
22
  async refresh() {
21
- const entries = await this.dash.storage.file.list('', { recursive: true });
22
- const songs: Song[] = [];
23
- for (const entry of entries) {
24
- if (entry.kind === 'file' && entry.path.startsWith('setlist/')) {
25
- songs.push({ path: entry.path, text: await this.dash.storage.file.read(entry.path) });
23
+ try {
24
+ const entries = await this.dash.storage.file.list('', { recursive: true });
25
+ const songs: Song[] = [];
26
+ for (const entry of entries) {
27
+ if (entry.kind === 'file' && entry.path.startsWith('setlist/')) {
28
+ songs.push({ path: entry.path, text: await this.dash.storage.file.read(entry.path) });
29
+ }
26
30
  }
31
+ const archives: Archive[] = entries
32
+ .filter((entry) => entry.kind === 'folder' && /^archive\/[^/]+$/.test(entry.path))
33
+ .map((entry) => ({
34
+ path: entry.path,
35
+ time: new Date(Number(entry.name)).toLocaleTimeString('en-GB'),
36
+ }))
37
+ .reverse();
38
+ const documents = entries.filter(
39
+ (entry) => entry.kind === 'file' && entry.path.startsWith('documents/'),
40
+ );
41
+ // In bytes: what the dot's files take, and the most they may take.
42
+ const { used, quota } = await this.dash.storage.file.usage();
43
+ const kept = await this.dash.storage.file.stat(PICTURE);
44
+ this.setState({
45
+ songs,
46
+ archives,
47
+ documents,
48
+ usage: `${used} bytes used of ${quota / 1024 / 1024} MB`,
49
+ picture: kept ? (await this.dash.storage.file.open(PICTURE)).url : '',
50
+ });
51
+ } catch (error) {
52
+ this.setState({ note: errorMessage(error) });
27
53
  }
28
- const archives: Archive[] = entries
29
- .filter((entry) => entry.kind === 'folder' && /^archive\/[^/]+$/.test(entry.path))
30
- .map((entry) => ({
31
- path: entry.path,
32
- time: new Date(Number(entry.name)).toLocaleTimeString('en-GB'),
33
- }))
34
- .reverse();
35
- const documents = entries.filter(
36
- (entry) => entry.kind === 'file' && entry.path.startsWith('documents/'),
37
- );
38
- // In bytes: what the dot's files take, and the most they may take.
39
- const { used, quota } = await this.dash.storage.file.usage();
40
- const kept = await this.dash.storage.file.stat(PICTURE);
41
- this.setState({
42
- songs,
43
- archives,
44
- documents,
45
- usage: `${used} bytes used of ${quota / 1024 / 1024} MB`,
46
- picture: kept ? (await this.dash.storage.file.open(PICTURE)).url : '',
47
- });
48
54
  },
49
55
  async save(state) {
50
- await this.dash.storage.file.write(`setlist/${Date.now()}.txt`, state.draft);
51
- this.setState({ draft: '' });
56
+ try {
57
+ await this.dash.storage.file.write(`setlist/${Date.now()}.txt`, state.draft);
58
+ this.setState({ draft: '', note: '' });
59
+ } catch (error) {
60
+ this.setState({ note: errorMessage(error) });
61
+ }
52
62
  await this.actions.refresh();
53
63
  },
54
64
  // Moves the whole setlist folder into the archive, under the time it was archived.
55
65
  async archive() {
56
- await this.dash.storage.file.createFolder('archive');
57
- await this.dash.storage.file.move('setlist', `archive/${Date.now()}`);
66
+ try {
67
+ await this.dash.storage.file.createFolder('archive');
68
+ await this.dash.storage.file.move('setlist', `archive/${Date.now()}`);
69
+ this.setState({ note: '' });
70
+ } catch (error) {
71
+ this.setState({ note: errorMessage(error) });
72
+ }
58
73
  await this.actions.refresh();
59
74
  },
60
75
  // Copies an archived setlist back, in place of the current one. Without overwrite, a path
61
76
  // that's taken rejects with 'failed'.
62
77
  async copyBack(_state, event: DataEvent<{ path: string }>) {
63
- await this.dash.storage.file.copy(event.data.path, 'setlist', { overwrite: true });
78
+ try {
79
+ await this.dash.storage.file.copy(event.data.path, 'setlist', { overwrite: true });
80
+ this.setState({ note: '' });
81
+ } catch (error) {
82
+ this.setState({ note: errorMessage(error) });
83
+ }
64
84
  await this.actions.refresh();
65
85
  },
66
86
  // A folder with files in it needs recursive.
67
87
  async clearSetlist() {
68
- await this.dash.storage.file.remove('setlist', { recursive: true });
88
+ try {
89
+ await this.dash.storage.file.remove('setlist', { recursive: true });
90
+ this.setState({ note: '' });
91
+ } catch (error) {
92
+ this.setState({ note: errorMessage(error) });
93
+ }
69
94
  await this.actions.refresh();
70
95
  },
71
96
  async forgetPicture() {
72
- await this.dash.storage.file.remove(PICTURE);
97
+ try {
98
+ await this.dash.storage.file.remove(PICTURE);
99
+ this.setState({ note: '' });
100
+ } catch (error) {
101
+ this.setState({ note: errorMessage(error) });
102
+ }
73
103
  await this.actions.refresh();
74
104
  },
75
105
  },
@@ -20,4 +20,5 @@ export default {
20
20
  picture: '',
21
21
  // Files saved into the dot's documents folder, such as a ticket from the dash.media page.
22
22
  documents: [] as FileEntry[],
23
+ note: '',
23
24
  };
@@ -75,4 +75,8 @@
75
75
  <Text variant="subtitle">usage</Text>
76
76
  <Text>{{ usage }}</Text>
77
77
  </View>
78
+
79
+ <If condition="{{ note }}">
80
+ <Notice tone="danger">{{ note }}</Notice>
81
+ </If>
78
82
  </View>
@@ -1,3 +1,5 @@
1
+ import { errorMessage } from '@monimesl/dot-runtime';
2
+
1
3
  import state from './state';
2
4
 
3
5
  const SESSION = 'session';
@@ -16,31 +18,49 @@ export default definePage({
16
18
  },
17
19
  actions: {
18
20
  async refresh() {
19
- // null if there's no secret by that name.
20
- const token = await this.dash.storage.secret.get(SESSION);
21
- const signedInAs = await this.dash.storage.cache.get<string>(NAME);
22
- this.setState({
23
- token: token ? `${token.slice(0, 12)}…` : '',
24
- signedInAs: signedInAs ?? '',
25
- });
21
+ try {
22
+ // null if there's no secret by that name.
23
+ const token = await this.dash.storage.secret.get(SESSION);
24
+ const signedInAs = await this.dash.storage.cache.get<string>(NAME);
25
+ this.setState({
26
+ token: token ? `${token.slice(0, 12)}…` : '',
27
+ signedInAs: signedInAs ?? '',
28
+ });
29
+ } catch (error) {
30
+ this.setState({ note: errorMessage(error) });
31
+ }
26
32
  },
27
33
  // A value over 4 KB rejects with 'invalid'. A 33rd secret rejects with 'failed', so remove
28
34
  // one first.
29
35
  async signIn(state) {
30
- // A real dot gets its token from its own API, once the person signs in there.
31
- const token = `demo-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
32
- await this.dash.storage.secret.set(SESSION, token);
33
- await this.dash.storage.cache.set(NAME, state.name);
34
- this.setState({ name: '' });
36
+ try {
37
+ // A real dot gets its token from its own API, once the person signs in there.
38
+ const token = `demo-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
39
+ await this.dash.storage.secret.set(SESSION, token);
40
+ await this.dash.storage.cache.set(NAME, state.name);
41
+ this.setState({ name: '', note: '' });
42
+ } catch (error) {
43
+ this.setState({ note: errorMessage(error) });
44
+ }
35
45
  await this.actions.refresh();
36
46
  },
37
47
  async signOut() {
38
- await this.dash.storage.secret.remove(SESSION);
39
- await this.dash.storage.cache.remove(NAME);
48
+ try {
49
+ await this.dash.storage.secret.remove(SESSION);
50
+ await this.dash.storage.cache.remove(NAME);
51
+ this.setState({ note: '' });
52
+ } catch (error) {
53
+ this.setState({ note: errorMessage(error) });
54
+ }
40
55
  await this.actions.refresh();
41
56
  },
42
57
  async forgetAll() {
43
- await this.dash.storage.secret.clear();
58
+ try {
59
+ await this.dash.storage.secret.clear();
60
+ this.setState({ note: '' });
61
+ } catch (error) {
62
+ this.setState({ note: errorMessage(error) });
63
+ }
44
64
  await this.actions.refresh();
45
65
  },
46
66
  },
@@ -2,4 +2,5 @@ export default {
2
2
  name: '',
3
3
  signedInAs: '',
4
4
  token: '',
5
+ note: '',
5
6
  };
@@ -23,4 +23,8 @@
23
23
  </View>
24
24
 
25
25
  <Notice tone="warning" title="Keep keys off the phone">Only the token is a secret here: your name is in the cache. Never put API keys in a dot's code or .env, since anyone with the dot can read them.</Notice>
26
+
27
+ <If condition="{{ note }}">
28
+ <Notice tone="danger">{{ note }}</Notice>
29
+ </If>
26
30
  </View>
@@ -67,6 +67,12 @@ export default {
67
67
  covers: 'configs.colors, setColors, background, primary, surfaces',
68
68
  icon: 'water',
69
69
  },
70
+ {
71
+ page: 'basics/theme',
72
+ title: 'Theme',
73
+ covers: 'colors, shape, density, elevation, fonts, components, icons, gradients',
74
+ icon: 'eye',
75
+ },
70
76
  {
71
77
  page: 'basics/share',
72
78
  title: 'Share links',
@@ -88,7 +94,7 @@ export default {
88
94
  {
89
95
  page: 'components/styles',
90
96
  title: 'Styles',
91
- covers: 'background, border, radius, sizes, theme colours',
97
+ covers: 'background, border, radius, sizes, elevation, buttons, own colours',
92
98
  icon: 'star',
93
99
  },
94
100
  {
@@ -130,9 +136,21 @@ export default {
130
136
  {
131
137
  page: 'components/tabs',
132
138
  title: 'Tabs and chips',
133
- covers: 'Tabs, Tab, Chip',
139
+ covers: 'Tabs, Tab, Tab icons, Chip',
134
140
  icon: 'search',
135
141
  },
142
+ {
143
+ page: 'components/pinned',
144
+ title: 'Pinned bars',
145
+ covers: 'Pinned, Tabs variant="bar"',
146
+ icon: 'location',
147
+ },
148
+ {
149
+ page: 'components/capture',
150
+ title: 'Pictures of a View',
151
+ covers: 'View id, captureView',
152
+ icon: 'image',
153
+ },
136
154
  {
137
155
  page: 'components/forms',
138
156
  title: 'Forms',
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5 4h14a2 2 0 0 1 2 2v3a3 3 0 0 0 0 6v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3a3 3 0 0 0 0-6V6a2 2 0 0 1 2-2zm4 3v2h1.5V7zm0 4v2h1.5v-2zm0 4v2h1.5v-2z"/></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monimesl/dot-cli",
3
- "version": "1.0.0-beta.7",
3
+ "version": "1.0.0-beta.9",
4
4
  "description": "The dotctl command: build mini apps and run them in the simulator",
5
5
  "type": "module",
6
6
  "repository": {
@@ -24,11 +24,11 @@
24
24
  "dependencies": {
25
25
  "typescript": "^5.9.3",
26
26
  "uqr": "^0.1.3",
27
- "@monimesl/dot-compiler": "1.0.0-beta.7",
28
- "@monimesl/dot-core": "1.0.0-beta.7",
29
- "@monimesl/dot-dml-parser": "1.0.0-beta.7",
30
- "@monimesl/dot-script-parser": "1.0.0-beta.7",
31
- "@monimesl/dot-simulator": "1.0.0-beta.7"
27
+ "@monimesl/dot-compiler": "1.0.0-beta.9",
28
+ "@monimesl/dot-core": "1.0.0-beta.9",
29
+ "@monimesl/dot-dml-parser": "1.0.0-beta.9",
30
+ "@monimesl/dot-script-parser": "1.0.0-beta.9",
31
+ "@monimesl/dot-simulator": "1.0.0-beta.9"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^22.20.4",