@aiaiai-pt/design-system 0.19.0 → 0.20.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,111 @@
1
+ <!--
2
+ @component ActionPanel
3
+
4
+ A config-driven vertical stack of detail-page call-to-action buttons —
5
+ vote / support / comment / acknowledge / reserve … Vertical-agnostic: every
6
+ CTA is data ({ label, href, icon?, variant?, disabled?, reason? }).
7
+
8
+ Presentational only — the consumer decides each CTA's enabled/disabled state
9
+ and the reason it's closed (grant or row-state). An ENABLED CTA renders as a
10
+ link (it navigates to the action form); a CLOSED CTA renders as a disabled
11
+ control with its reason as VISIBLE, programmatically-associated helper text
12
+ (`aria-describedby`) — never a dead button. Decorative icons are aria-hidden.
13
+
14
+ Consumes semantic tokens (`--space-*`, `--color-*`) so dark / high-contrast
15
+ schemes (#244) ride through without per-component token entries.
16
+
17
+ @example
18
+ <ActionPanel actions={[
19
+ { key: 'vote', label: 'Vote for this proposal', href: '/submit/vote', icon: ThumbsUpIcon },
20
+ { key: 'support', label: 'Support', disabled: true, reason: 'Voting has closed.' },
21
+ ]} />
22
+ -->
23
+ <script module>
24
+ let _actionPanelUid = 0;
25
+ /**
26
+ * @typedef {{
27
+ * key?: string,
28
+ * label: string,
29
+ * href?: string,
30
+ * icon?: import('svelte').Component,
31
+ * variant?: 'primary' | 'secondary' | 'ghost' | 'destructive',
32
+ * disabled?: boolean,
33
+ * reason?: string,
34
+ * }} ActionPanelItem
35
+ */
36
+ </script>
37
+
38
+ <script>
39
+ import Button from './Button.svelte';
40
+
41
+ let {
42
+ /** @type {ActionPanelItem[]} The CTAs to render, in order. */
43
+ actions = [],
44
+ /** @type {string} */
45
+ class: className = '',
46
+ ...rest
47
+ } = $props();
48
+
49
+ const uid = `action-panel-${_actionPanelUid++}`;
50
+ </script>
51
+
52
+ {#if actions.length > 0}
53
+ <div class="action-panel {className}" {...rest}>
54
+ {#each actions as action, i (action.key ?? i)}
55
+ {@const Icon = action.icon}
56
+ {@const isClosed = action.disabled === true || !action.href}
57
+ {@const reasonId =
58
+ action.disabled === true && action.reason ? `${uid}-reason-${i}` : undefined}
59
+ <div class="action-panel-item">
60
+ <Button
61
+ variant={action.variant ?? 'primary'}
62
+ href={isClosed ? undefined : action.href}
63
+ disabled={isClosed}
64
+ class="action-panel-btn"
65
+ aria-describedby={reasonId}
66
+ >
67
+ {#if Icon}<span class="action-panel-icon" aria-hidden="true"><Icon /></span>{/if}
68
+ {action.label}
69
+ </Button>
70
+ {#if action.disabled === true && action.reason}
71
+ <p class="action-panel-reason" id={reasonId}>{action.reason}</p>
72
+ {/if}
73
+ </div>
74
+ {/each}
75
+ </div>
76
+ {/if}
77
+
78
+ <style>
79
+ .action-panel {
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: var(--space-sm);
83
+ }
84
+
85
+ .action-panel-item {
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: var(--space-2xs);
89
+ }
90
+
91
+ /* Ficha CTAs read as a stacked column — each button spans the panel. */
92
+ .action-panel :global(.action-panel-btn) {
93
+ width: 100%;
94
+ }
95
+
96
+ .action-panel-icon {
97
+ display: inline-flex;
98
+ align-items: center;
99
+ }
100
+
101
+ .action-panel-icon :global(svg) {
102
+ width: var(--icon-size-sm);
103
+ height: var(--icon-size-sm);
104
+ }
105
+
106
+ .action-panel-reason {
107
+ margin: 0;
108
+ font-size: var(--type-body-sm-size);
109
+ color: var(--color-text-secondary);
110
+ }
111
+ </style>
@@ -0,0 +1,44 @@
1
+ export default ActionPanel;
2
+ export type ActionPanelItem = {
3
+ key?: string;
4
+ label: string;
5
+ href?: string;
6
+ icon?: import("svelte").Component;
7
+ variant?: "primary" | "secondary" | "ghost" | "destructive";
8
+ disabled?: boolean;
9
+ reason?: string;
10
+ };
11
+ type ActionPanel = {
12
+ $on?(type: string, callback: (e: any) => void): () => void;
13
+ $set?(props: Partial<$$ComponentProps>): void;
14
+ };
15
+ /**
16
+ * ActionPanel
17
+ *
18
+ * A config-driven vertical stack of detail-page call-to-action buttons —
19
+ * vote / support / comment / acknowledge / reserve … Vertical-agnostic: every
20
+ * CTA is data ({ label, href, icon?, variant?, disabled?, reason? }).
21
+ *
22
+ * Presentational only — the consumer decides each CTA's enabled/disabled state
23
+ * and the reason it's closed (grant or row-state). An ENABLED CTA renders as a
24
+ * link (it navigates to the action form); a CLOSED CTA renders as a disabled
25
+ * control with its reason as VISIBLE, programmatically-associated helper text
26
+ * (`aria-describedby`) — never a dead button. Decorative icons are aria-hidden.
27
+ *
28
+ * Consumes semantic tokens (`--space-*`, `--color-*`) so dark / high-contrast
29
+ * schemes (#244) ride through without per-component token entries.
30
+ *
31
+ * @example
32
+ * <ActionPanel actions={[
33
+ * { key: 'vote', label: 'Vote for this proposal', href: '/submit/vote', icon: ThumbsUpIcon },
34
+ * { key: 'support', label: 'Support', disabled: true, reason: 'Voting has closed.' },
35
+ * ]} />
36
+ */
37
+ declare const ActionPanel: import("svelte").Component<{
38
+ actions?: any[];
39
+ class?: string;
40
+ } & Record<string, any>, {}, "">;
41
+ type $$ComponentProps = {
42
+ actions?: any[];
43
+ class?: string;
44
+ } & Record<string, any>;
@@ -0,0 +1,131 @@
1
+ <!--
2
+ @component RecordList
3
+
4
+ A typed list of child records rendered from FIELD CONFIG — the generalisation
5
+ of a comment list into any detail-page child collection (reviews, pareceres,
6
+ occurrence updates, maintenance logs). Vertical-agnostic: `items` is the child
7
+ rows, `fields` declares which keys to show and how.
8
+
9
+ Layout convention (so one component serves every collection): the FIRST field
10
+ is the record's primary line; the rest render as a muted meta line, separated
11
+ by middots. A field may `format` as a date (locale-formatted) or `badge`
12
+ (rendered as a Badge). Soft-empty: no items → nothing; an absent field value
13
+ is skipped (no dangling separators).
14
+
15
+ Consumes semantic tokens so dark / high-contrast schemes (#244) ride through.
16
+
17
+ @example
18
+ <RecordList
19
+ items={updates}
20
+ fields={[
21
+ { key: 'note', label: 'Update' },
22
+ { key: 'author' },
23
+ { key: 'created_at', format: 'date' },
24
+ { key: 'status', format: 'badge' },
25
+ ]}
26
+ locale="pt"
27
+ />
28
+ -->
29
+ <script module>
30
+ /**
31
+ * @typedef {{ key: string, label?: string, format?: 'text' | 'date' | 'badge' }} RecordField
32
+ */
33
+ </script>
34
+
35
+ <script>
36
+ import List from './List.svelte';
37
+ import ListItem from './ListItem.svelte';
38
+ import Badge from './Badge.svelte';
39
+
40
+ let {
41
+ /** @type {Record<string, unknown>[]} The child records to render. */
42
+ items = [],
43
+ /** @type {RecordField[]} The columns; fields[0] is the primary line. */
44
+ fields = [],
45
+ /** @type {string} BCP-47 locale for date formatting. */
46
+ locale = 'en',
47
+ /** @type {string} */
48
+ class: className = '',
49
+ ...rest
50
+ } = $props();
51
+
52
+ /**
53
+ * @param {unknown} value
54
+ * @param {RecordField} field
55
+ * @returns {string}
56
+ */
57
+ function display(value, field) {
58
+ if (value === null || value === undefined || value === '') return '';
59
+ if (field.format === 'date') {
60
+ const d = new Date(String(value));
61
+ return Number.isNaN(d.getTime()) ? String(value) : d.toLocaleDateString(locale);
62
+ }
63
+ return String(value);
64
+ }
65
+
66
+ /**
67
+ * The non-empty meta fields (everything after the primary) for one record —
68
+ * precomputed so the template never emits a leading/dangling separator.
69
+ * @param {Record<string, unknown>} item
70
+ * @returns {{ field: RecordField, value: unknown }[]}
71
+ */
72
+ function metaParts(item) {
73
+ return fields
74
+ .slice(1)
75
+ .map((field) => ({ field, value: item[field.key] }))
76
+ .filter((p) => p.value !== null && p.value !== undefined && p.value !== '');
77
+ }
78
+ </script>
79
+
80
+ {#if items.length > 0 && fields.length > 0}
81
+ <List class={className} {...rest}>
82
+ {#each items as item, i (item.id ?? i)}
83
+ {@const primary = fields[0]}
84
+ {@const primaryValue = item[primary.key]}
85
+ {@const meta = metaParts(item)}
86
+ <ListItem>
87
+ {#snippet leading()}
88
+ {#if primaryValue !== null && primaryValue !== undefined && primaryValue !== ''}
89
+ <span class="record-primary">
90
+ {#if primary.format === 'badge'}
91
+ <Badge>{String(primaryValue)}</Badge>
92
+ {:else}
93
+ {display(primaryValue, primary)}
94
+ {/if}
95
+ </span>
96
+ {/if}
97
+ {#if meta.length > 0}
98
+ <span class="record-meta">
99
+ {#each meta as part, mi (part.field.key)}
100
+ {#if mi > 0}<span class="record-sep" aria-hidden="true">·</span>{/if}
101
+ {#if part.field.format === 'badge'}
102
+ <Badge>{String(part.value)}</Badge>
103
+ {:else}<span>{display(part.value, part.field)}</span>{/if}
104
+ {/each}
105
+ </span>
106
+ {/if}
107
+ {/snippet}
108
+ </ListItem>
109
+ {/each}
110
+ </List>
111
+ {/if}
112
+
113
+ <style>
114
+ .record-primary {
115
+ display: block;
116
+ }
117
+
118
+ .record-meta {
119
+ display: flex;
120
+ flex-wrap: wrap;
121
+ align-items: center;
122
+ gap: var(--space-2xs);
123
+ margin-top: var(--space-2xs);
124
+ font-size: var(--type-body-sm-size);
125
+ color: var(--color-text-secondary);
126
+ }
127
+
128
+ .record-sep {
129
+ color: var(--color-text-secondary);
130
+ }
131
+ </style>
@@ -0,0 +1,50 @@
1
+ export default RecordList;
2
+ export type RecordField = {
3
+ key: string;
4
+ label?: string;
5
+ format?: "text" | "date" | "badge";
6
+ };
7
+ type RecordList = {
8
+ $on?(type: string, callback: (e: any) => void): () => void;
9
+ $set?(props: Partial<$$ComponentProps>): void;
10
+ };
11
+ /**
12
+ * RecordList
13
+ *
14
+ * A typed list of child records rendered from FIELD CONFIG — the generalisation
15
+ * of a comment list into any detail-page child collection (reviews, pareceres,
16
+ * occurrence updates, maintenance logs). Vertical-agnostic: `items` is the child
17
+ * rows, `fields` declares which keys to show and how.
18
+ *
19
+ * Layout convention (so one component serves every collection): the FIRST field
20
+ * is the record's primary line; the rest render as a muted meta line, separated
21
+ * by middots. A field may `format` as a date (locale-formatted) or `badge`
22
+ * (rendered as a Badge). Soft-empty: no items → nothing; an absent field value
23
+ * is skipped (no dangling separators).
24
+ *
25
+ * Consumes semantic tokens so dark / high-contrast schemes (#244) ride through.
26
+ *
27
+ * @example
28
+ * <RecordList
29
+ * items={updates}
30
+ * fields={[
31
+ * { key: 'note', label: 'Update' },
32
+ * { key: 'author' },
33
+ * { key: 'created_at', format: 'date' },
34
+ * { key: 'status', format: 'badge' },
35
+ * ]}
36
+ * locale="pt"
37
+ * />
38
+ */
39
+ declare const RecordList: import("svelte").Component<{
40
+ items?: any[];
41
+ fields?: any[];
42
+ locale?: string;
43
+ class?: string;
44
+ } & Record<string, any>, {}, "">;
45
+ type $$ComponentProps = {
46
+ items?: any[];
47
+ fields?: any[];
48
+ locale?: string;
49
+ class?: string;
50
+ } & Record<string, any>;
@@ -51,6 +51,8 @@ export { default as TabList } from "./TabList.svelte";
51
51
  export { default as Tab } from "./Tab.svelte";
52
52
  export { default as TabPanel } from "./TabPanel.svelte";
53
53
  export { default as FeedView } from "./FeedView.svelte";
54
+ export { default as ActionPanel } from "./ActionPanel.svelte";
55
+ export { default as RecordList } from "./RecordList.svelte";
54
56
  export { default as Alert } from "./Alert.svelte";
55
57
  export { default as Toast } from "./Toast.svelte";
56
58
  export { default as ToastManager } from "./ToastManager.svelte";
@@ -77,6 +77,10 @@ export { default as Tab } from "./Tab.svelte";
77
77
  export { default as TabPanel } from "./TabPanel.svelte";
78
78
  export { default as FeedView } from "./FeedView.svelte";
79
79
 
80
+ // Entity ficha (#105 Phase 2)
81
+ export { default as ActionPanel } from "./ActionPanel.svelte";
82
+ export { default as RecordList } from "./RecordList.svelte";
83
+
80
84
  // Feedback
81
85
  export { default as Alert } from "./Alert.svelte";
82
86
  export { default as Toast } from "./Toast.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",