@aiaiai-pt/design-system 0.28.0 → 0.30.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.
|
@@ -130,10 +130,22 @@
|
|
|
130
130
|
let fileUploads = $state<Record<string, Array<{ key: string; name: string }>>>({});
|
|
131
131
|
let fileBusy = $state<Record<string, boolean>>({});
|
|
132
132
|
let fileError = $state<Record<string, string>>({});
|
|
133
|
-
// Platform caps mirror the public upload surface
|
|
133
|
+
// Platform caps mirror the public upload surface. The per-action tier
|
|
134
|
+
// (allowed content types + max bytes) is DATA on the action's
|
|
135
|
+
// `submission_contract.attachments`; the render projects it onto the `file`
|
|
136
|
+
// parameter (`accept` / `max_bytes`). When a parameter carries no tier we
|
|
137
|
+
// fall back to the conservative web-image defaults (3 × 5MB, image types).
|
|
134
138
|
const FILE_MAX_COUNT = 3;
|
|
135
|
-
const
|
|
136
|
-
const
|
|
139
|
+
const DEFAULT_FILE_MAX_BYTES = 5 * 1024 * 1024;
|
|
140
|
+
const DEFAULT_FILE_ACCEPT = "image/jpeg,image/png,image/webp";
|
|
141
|
+
function fileAccept(parameter: Entity): string {
|
|
142
|
+
const accept = (parameter as Record<string, unknown>).accept;
|
|
143
|
+
return typeof accept === "string" && accept.trim() ? accept : DEFAULT_FILE_ACCEPT;
|
|
144
|
+
}
|
|
145
|
+
function fileMaxBytes(parameter: Entity): number {
|
|
146
|
+
const max = Number((parameter as Record<string, unknown>).max_bytes);
|
|
147
|
+
return Number.isFinite(max) && max > 0 ? max : DEFAULT_FILE_MAX_BYTES;
|
|
148
|
+
}
|
|
137
149
|
|
|
138
150
|
// Submit state (apply seam). Only meaningful in submit modes with an onApply.
|
|
139
151
|
let submitting = $state(false);
|
|
@@ -491,13 +503,16 @@
|
|
|
491
503
|
{/each}
|
|
492
504
|
{#if (fileUploads[key] ?? []).length < FILE_MAX_COUNT}
|
|
493
505
|
<FileUpload
|
|
494
|
-
accept={
|
|
495
|
-
maxSize={
|
|
506
|
+
accept={fileAccept(parameter)}
|
|
507
|
+
maxSize={fileMaxBytes(parameter)}
|
|
496
508
|
multiple
|
|
497
509
|
disabled={!uploadFile || fileBusy[key]}
|
|
498
510
|
onfiles={(files: File[]) => handleFiles(key, files)}
|
|
499
511
|
onreject={() => {
|
|
500
|
-
fileError = {
|
|
512
|
+
fileError = {
|
|
513
|
+
...fileError,
|
|
514
|
+
[key]: `File type not allowed or too large (max ${Math.round(fileMaxBytes(parameter) / (1024 * 1024))}MB).`,
|
|
515
|
+
};
|
|
501
516
|
}}
|
|
502
517
|
/>
|
|
503
518
|
{/if}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
-->
|
|
31
31
|
<script module>
|
|
32
32
|
/**
|
|
33
|
-
* @typedef {{ id: string, label: string, description?: string, active: boolean }} PrefItem
|
|
33
|
+
* @typedef {{ id: string, label: string, description?: string, active: boolean, group?: string }} PrefItem
|
|
34
34
|
*/
|
|
35
35
|
</script>
|
|
36
36
|
|
|
@@ -54,30 +54,56 @@
|
|
|
54
54
|
class: className = "",
|
|
55
55
|
...rest
|
|
56
56
|
} = $props();
|
|
57
|
+
|
|
58
|
+
// Group rows by their optional `group` (order preserved). Items with no
|
|
59
|
+
// `group` fall in the leading unnamed group, which renders WITHOUT a header —
|
|
60
|
+
// so a flat `items` list (no group set) renders exactly as before
|
|
61
|
+
// (backward-compatible). A named group renders an <h3> section header above
|
|
62
|
+
// its bordered list (e.g. "Edição" / "Categoria" / "Freguesia").
|
|
63
|
+
const groups = $derived.by(() => {
|
|
64
|
+
/** @type {string[]} */
|
|
65
|
+
const order = [];
|
|
66
|
+
/** @type {Map<string, PrefItem[]>} */
|
|
67
|
+
const byGroup = new Map();
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
const g = item.group ?? "";
|
|
70
|
+
if (!byGroup.has(g)) {
|
|
71
|
+
byGroup.set(g, []);
|
|
72
|
+
order.push(g);
|
|
73
|
+
}
|
|
74
|
+
byGroup.get(g)?.push(item);
|
|
75
|
+
}
|
|
76
|
+
return order.map((g) => ({ group: g, items: byGroup.get(g) ?? [] }));
|
|
77
|
+
});
|
|
57
78
|
</script>
|
|
58
79
|
|
|
59
80
|
<section class="notification-prefs {className}" aria-label={label} {...rest}>
|
|
60
81
|
{#if items.length > 0}
|
|
61
|
-
|
|
62
|
-
{#
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
{#each groups as grp (grp.group)}
|
|
83
|
+
{#if grp.group}
|
|
84
|
+
<h3 class="notification-prefs-group">{grp.group}</h3>
|
|
85
|
+
{/if}
|
|
86
|
+
<List variant="bordered">
|
|
87
|
+
{#each grp.items as item (item.id)}
|
|
88
|
+
<ListItem>
|
|
89
|
+
{#snippet leading()}
|
|
90
|
+
<span class="notification-prefs-label">{item.label}</span>
|
|
91
|
+
{#if item.description}
|
|
92
|
+
<span class="notification-prefs-desc">{item.description}</span>
|
|
93
|
+
{/if}
|
|
94
|
+
{/snippet}
|
|
95
|
+
{#snippet trailing()}
|
|
96
|
+
<Toggle
|
|
97
|
+
checked={item.active}
|
|
98
|
+
disabled={busyId === item.id}
|
|
99
|
+
aria-label={item.label}
|
|
100
|
+
onchange={(next) => onToggle?.(item.id, next)}
|
|
101
|
+
/>
|
|
102
|
+
{/snippet}
|
|
103
|
+
</ListItem>
|
|
104
|
+
{/each}
|
|
105
|
+
</List>
|
|
106
|
+
{/each}
|
|
81
107
|
{:else}
|
|
82
108
|
<p class="notification-prefs-empty">{emptyText}</p>
|
|
83
109
|
{/if}
|
|
@@ -90,6 +116,18 @@
|
|
|
90
116
|
gap: var(--space-md);
|
|
91
117
|
}
|
|
92
118
|
|
|
119
|
+
.notification-prefs-group {
|
|
120
|
+
font-family: var(--type-heading-font, var(--type-body-font));
|
|
121
|
+
font-size: var(--type-body-size);
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
color: var(--color-text);
|
|
124
|
+
margin: var(--space-sm) 0 calc(-1 * var(--space-xs)) 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.notification-prefs-group:first-child {
|
|
128
|
+
margin-top: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
93
131
|
.notification-prefs-label {
|
|
94
132
|
font-family: var(--type-body-font);
|
|
95
133
|
font-size: var(--type-body-size);
|