@scattered-light/loomiere 0.5.3 → 0.7.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.
- package/build/build-tokens.mjs +33 -6
- package/exporters/chakra.mjs +74 -0
- package/exporters/component-styles.mjs +64 -0
- package/exporters/design-system.mjs +23 -7
- package/exporters/mui.mjs +60 -0
- package/figma/library/pages/18-custom/01-custom.js +57 -0
- package/figma/library/pages/18-custom/02-labels.js +7 -0
- package/figma/library/pages/18-custom/03-doc.js +28 -0
- package/figma/library/pages/18-custom/04-date-picker.js +327 -0
- package/figma/library/pages/18-custom/05-date-picker-labels.js +9 -0
- package/figma/library/pages/18-custom/06-date-picker-doc.js +25 -0
- package/figma/library/pages/18-custom/07-money-field.js +107 -0
- package/figma/library/pages/18-custom/08-money-field-labels.js +8 -0
- package/figma/library/pages/18-custom/09-money-field-doc.js +101 -0
- package/figma/library/pages/18-custom/10-splitter.js +100 -0
- package/figma/library/pages/18-custom/11-splitter-labels.js +8 -0
- package/figma/library/pages/18-custom/12-splitter-doc.js +92 -0
- package/figma/library/pages/18-custom/13-empty-state.js +114 -0
- package/figma/library/pages/18-custom/14-empty-state-labels.js +8 -0
- package/figma/library/pages/18-custom/15-empty-state-doc.js +58 -0
- package/figma/library/pages/18-custom/16-collection.js +255 -0
- package/figma/library/pages/18-custom/17-collection-doc.js +43 -0
- package/figma/library/run-order.json +1 -0
- package/lib/contrast.mjs +17 -0
- package/package.json +1 -1
- package/tokens/README.md +10 -3
- package/tokens/component/badge.json +53 -0
- package/tokens/component/collection.json +49 -0
- package/tokens/component/list-detail.json +29 -0
- package/tokens/component/money-field.json +9 -0
- package/tokens/component/splitter.json +25 -0
- package/tokens/semantic/accessibility.json +4 -0
- package/tokens/semantic/calendar.json +75 -0
- package/tokens/semantic/empty.json +79 -0
- package/tokens/semantic/field.json +4 -0
- package/tokens/semantic/popup.json +2 -2
- package/tokens/semantic/table.json +8 -0
- package/tools/brand-builder/builder.js +45 -0
- package/tools/brand-builder/index.html +6 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// Custom: Date picker. Day, Month, Preset, and two examples. Colors are Theme variables; text is a shared style.
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
|
|
4
|
+
var fill = C.fill, stroke = C.stroke, bind = C.bind, radius = C.radius, add = C.add, T = C.T, mode = C.mode, page = C.page;
|
|
5
|
+
var comp = C.comp, textProp = C.textProp, place = C.place, removeOld = C.removeOld, icon = C.icon;
|
|
6
|
+
await page('Custom');
|
|
7
|
+
var ES = {};
|
|
8
|
+
(await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
9
|
+
|
|
10
|
+
var STATES = ['Default', 'Hover', 'Focus', 'Today', 'Selected', 'Range start', 'Range middle', 'Range end', 'Preview', 'Outside', 'Disabled'];
|
|
11
|
+
|
|
12
|
+
function dayPaint(state) {
|
|
13
|
+
if (state === 'Selected' || state === 'Range start' || state === 'Range end') return { fill: 'Emphasis:emphasis/bg', text: 'Emphasis:emphasis/fg', emphasis: 'Bold' };
|
|
14
|
+
if (state === 'Range middle' || state === 'Preview') return { fill: 'Theme:calendar/range-background', text: 'Theme:calendar/range-color' };
|
|
15
|
+
if (state === 'Hover') return { fill: 'Theme:calendar/day-background-hover', text: 'Theme:calendar/day-color' };
|
|
16
|
+
if (state === 'Outside') return { fill: null, text: 'Theme:calendar/day-color-outside' };
|
|
17
|
+
if (state === 'Disabled') return { fill: null, text: 'Theme:calendar/day-color-disabled' };
|
|
18
|
+
return { fill: null, text: 'Theme:calendar/day-color' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
removeOld('Day');
|
|
22
|
+
var days = [];
|
|
23
|
+
for (var i = 0; i < STATES.length; i++) {
|
|
24
|
+
var state = STATES[i];
|
|
25
|
+
var paint = dayPaint(state);
|
|
26
|
+
var c = comp('State=' + state);
|
|
27
|
+
c.primaryAxisAlignItems = 'CENTER';
|
|
28
|
+
c.counterAxisAlignItems = 'CENTER';
|
|
29
|
+
c.resize(40, 40);
|
|
30
|
+
bind(c, ['width', 'height'], 'Size:calendar/day-size');
|
|
31
|
+
if (paint.emphasis) mode(c, 'Emphasis', paint.emphasis);
|
|
32
|
+
var endcap = state === 'Range start' || state === 'Range end';
|
|
33
|
+
if (endcap) {
|
|
34
|
+
// A circle on a half-width band, the same shape as the CSS.
|
|
35
|
+
c.itemSpacing = 0;
|
|
36
|
+
c.clipsContent = true;
|
|
37
|
+
var band = figma.createRectangle();
|
|
38
|
+
band.name = 'Band';
|
|
39
|
+
fill(band, 'Theme:calendar/range-background');
|
|
40
|
+
band.layoutPositioning = 'ABSOLUTE';
|
|
41
|
+
band.resize(20, 40);
|
|
42
|
+
band.x = state === 'Range start' ? 20 : 0;
|
|
43
|
+
band.y = 0;
|
|
44
|
+
band.constraints = { horizontal: 'SCALE', vertical: 'STRETCH' };
|
|
45
|
+
c.appendChild(band);
|
|
46
|
+
var circle = figma.createFrame();
|
|
47
|
+
circle.name = 'Circle';
|
|
48
|
+
circle.layoutMode = 'HORIZONTAL';
|
|
49
|
+
circle.primaryAxisAlignItems = 'CENTER';
|
|
50
|
+
circle.counterAxisAlignItems = 'CENTER';
|
|
51
|
+
fill(circle, paint.fill);
|
|
52
|
+
mode(circle, 'Emphasis', 'Bold');
|
|
53
|
+
radius(circle, 'Size:calendar/day-radius');
|
|
54
|
+
circle.layoutSizingHorizontal = 'FILL';
|
|
55
|
+
circle.layoutSizingVertical = 'FILL';
|
|
56
|
+
var cap = await T('24', 'selected-condense', paint.text);
|
|
57
|
+
cap.name = 'Number';
|
|
58
|
+
add(circle, [cap]);
|
|
59
|
+
add(c, [circle]);
|
|
60
|
+
} else {
|
|
61
|
+
if (paint.fill) fill(c, paint.fill);
|
|
62
|
+
if (state === 'Range middle' || state === 'Preview') c.cornerRadius = 0;
|
|
63
|
+
else radius(c, 'Size:calendar/day-radius');
|
|
64
|
+
if (state === 'Today') {
|
|
65
|
+
stroke(c, 'Theme:calendar/today-ring', 'Size:stroke/default');
|
|
66
|
+
c.strokeAlign = 'INSIDE';
|
|
67
|
+
}
|
|
68
|
+
if (state === 'Focus' && ES['focus/ring']) await c.setEffectStyleIdAsync(ES['focus/ring'].id);
|
|
69
|
+
var n = await T('24', state === 'Selected' ? 'selected-condense' : 'trigger-condense', paint.text);
|
|
70
|
+
n.name = 'Number';
|
|
71
|
+
add(c, [n]);
|
|
72
|
+
}
|
|
73
|
+
days.push(c);
|
|
74
|
+
}
|
|
75
|
+
var daySet = figma.combineAsVariants(days, figma.currentPage);
|
|
76
|
+
daySet.name = 'Day';
|
|
77
|
+
daySet.description = 'One day in a month grid. State covers rest, hover, focus, today, the selected circle, the range band, a preview, a day outside the month, and a disabled day. Selected uses the Bold emphasis. The range band is calendar/range-background.';
|
|
78
|
+
place(daySet);
|
|
79
|
+
textProp(daySet, 'Number', 'Number', '24');
|
|
80
|
+
|
|
81
|
+
function setP(node, props) {
|
|
82
|
+
var next = {};
|
|
83
|
+
Object.keys(node.componentProperties).forEach(function (key) {
|
|
84
|
+
var name = key.split('#')[0];
|
|
85
|
+
if (props[name] !== undefined) next[key] = props[name];
|
|
86
|
+
});
|
|
87
|
+
node.setProperties(next);
|
|
88
|
+
return node;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function dayInst(state, number) {
|
|
92
|
+
var node = daySet.defaultVariant.createInstance();
|
|
93
|
+
setP(node, { State: state, Number: String(number) });
|
|
94
|
+
return node;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Sunday-first. marks is a day-of-month to a Day state. */
|
|
98
|
+
function monthCells(year, month, marks) {
|
|
99
|
+
var lead = new Date(Date.UTC(year, month - 1, 1)).getUTCDay();
|
|
100
|
+
var count = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
101
|
+
var prevCount = new Date(Date.UTC(year, month - 1, 0)).getUTCDate();
|
|
102
|
+
var cells = [];
|
|
103
|
+
for (var i = 0; i < lead; i++) cells.push({ n: prevCount - lead + 1 + i, state: 'Outside' });
|
|
104
|
+
for (var d = 1; d <= count; d++) cells.push({ n: d, state: (marks && marks[d]) || 'Default' });
|
|
105
|
+
var next = 1;
|
|
106
|
+
while (cells.length < 42) cells.push({ n: next++, state: 'Outside' });
|
|
107
|
+
return cells;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
var WEEKDAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
111
|
+
|
|
112
|
+
async function monthFrame(name, title, cells, withTitle) {
|
|
113
|
+
var frame = figma.createFrame();
|
|
114
|
+
frame.name = name;
|
|
115
|
+
frame.layoutMode = 'VERTICAL';
|
|
116
|
+
frame.fills = [];
|
|
117
|
+
bind(frame, 'itemSpacing', 'Size:calendar/header-gap');
|
|
118
|
+
if (withTitle) {
|
|
119
|
+
var heading = await T(title, 'subtitle-base', 'Theme:text/primary');
|
|
120
|
+
heading.name = 'Title';
|
|
121
|
+
add(frame, [heading]);
|
|
122
|
+
}
|
|
123
|
+
var grid = figma.createFrame();
|
|
124
|
+
grid.name = 'Grid';
|
|
125
|
+
grid.layoutMode = 'VERTICAL';
|
|
126
|
+
grid.fills = [];
|
|
127
|
+
bind(grid, 'itemSpacing', 'Size:calendar/grid-gap');
|
|
128
|
+
var head = figma.createFrame();
|
|
129
|
+
head.name = 'Weekdays';
|
|
130
|
+
head.layoutMode = 'HORIZONTAL';
|
|
131
|
+
head.itemSpacing = 0;
|
|
132
|
+
head.fills = [];
|
|
133
|
+
for (var w = 0; w < 7; w++) {
|
|
134
|
+
var slot = figma.createFrame();
|
|
135
|
+
slot.name = 'Weekday';
|
|
136
|
+
slot.layoutMode = 'HORIZONTAL';
|
|
137
|
+
slot.primaryAxisAlignItems = 'CENTER';
|
|
138
|
+
slot.counterAxisAlignItems = 'CENTER';
|
|
139
|
+
slot.fills = [];
|
|
140
|
+
slot.resize(40, 24);
|
|
141
|
+
bind(slot, 'width', 'Size:calendar/day-size');
|
|
142
|
+
add(slot, [await T(WEEKDAYS[w], 'eyebrow-minimize', 'Theme:calendar/weekday-color')]);
|
|
143
|
+
add(head, [slot]);
|
|
144
|
+
}
|
|
145
|
+
add(grid, [head]);
|
|
146
|
+
for (var row = 0; row < 6; row++) {
|
|
147
|
+
var line = figma.createFrame();
|
|
148
|
+
line.name = 'Week';
|
|
149
|
+
line.layoutMode = 'HORIZONTAL';
|
|
150
|
+
line.itemSpacing = 0;
|
|
151
|
+
line.fills = [];
|
|
152
|
+
for (var col = 0; col < 7; col++) {
|
|
153
|
+
var cell = cells[row * 7 + col];
|
|
154
|
+
add(line, [dayInst(cell.state, cell.n)]);
|
|
155
|
+
}
|
|
156
|
+
add(grid, [line]);
|
|
157
|
+
}
|
|
158
|
+
add(frame, [grid]);
|
|
159
|
+
return frame;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var octoberMarks = { 5: 'Range start', 12: 'Range end', 20: 'Disabled', 24: 'Today' };
|
|
163
|
+
for (var mid = 6; mid <= 11; mid++) octoberMarks[mid] = 'Range middle';
|
|
164
|
+
|
|
165
|
+
removeOld('Month');
|
|
166
|
+
var month = comp('Month');
|
|
167
|
+
month.layoutMode = 'VERTICAL';
|
|
168
|
+
bind(month, 'itemSpacing', 'Size:calendar/header-gap');
|
|
169
|
+
var built = await monthFrame('October', 'October 2026', monthCells(2026, 10, octoberMarks), true);
|
|
170
|
+
built.children.slice().forEach(function (child) { month.appendChild(child); });
|
|
171
|
+
built.remove();
|
|
172
|
+
month.description = 'October 2026, Sunday first. The 5th through the 12th is a range, the 24th is today, the 20th is disabled, and days from September and November are outside.';
|
|
173
|
+
place(month);
|
|
174
|
+
|
|
175
|
+
var PRESET_STATES = ['Rest', 'Hover', 'Selected'];
|
|
176
|
+
removeOld('Preset');
|
|
177
|
+
var presets = [];
|
|
178
|
+
for (var p = 0; p < PRESET_STATES.length; p++) for (var layout = 0; layout < 2; layout++) {
|
|
179
|
+
var pst = PRESET_STATES[p];
|
|
180
|
+
var chip = layout === 1;
|
|
181
|
+
var rowC = comp('Layout=' + (chip ? 'Chip' : 'List') + ', State=' + pst);
|
|
182
|
+
rowC.layoutMode = chip ? 'HORIZONTAL' : 'VERTICAL';
|
|
183
|
+
bind(rowC, 'itemSpacing', 'Size:space/0-5');
|
|
184
|
+
bind(rowC, ['paddingTop', 'paddingBottom'], 'Size:item/padding-block');
|
|
185
|
+
bind(rowC, ['paddingLeft', 'paddingRight'], 'Size:item/padding-inline');
|
|
186
|
+
radius(rowC, 'Size:item/radius');
|
|
187
|
+
if (pst === 'Hover') fill(rowC, 'Theme:item/background-hover');
|
|
188
|
+
if (pst === 'Selected') fill(rowC, 'Theme:item/background-selected');
|
|
189
|
+
var labelColor = pst === 'Selected' ? 'Theme:item/color-selected' : 'Theme:item/color';
|
|
190
|
+
var lab = await T('Last 7 days', 'trigger-condense', labelColor);
|
|
191
|
+
lab.name = 'Label';
|
|
192
|
+
var hint = await T('Oct 18 – Oct 24', 'body-minimize', pst === 'Selected' ? 'Theme:item/color-selected' : 'Theme:text/tertiary');
|
|
193
|
+
hint.name = 'Hint';
|
|
194
|
+
add(rowC, [lab, hint]);
|
|
195
|
+
presets.push(rowC);
|
|
196
|
+
}
|
|
197
|
+
var presetSet = figma.combineAsVariants(presets, figma.currentPage);
|
|
198
|
+
presetSet.name = 'Preset';
|
|
199
|
+
presetSet.description = 'A quick selection. List is the side column; Chip is the scrolling row. Selected uses the item selected tint.';
|
|
200
|
+
place(presetSet);
|
|
201
|
+
textProp(presetSet, 'Label', 'Label', 'Last 7 days');
|
|
202
|
+
|
|
203
|
+
function presetInst(layout, state) {
|
|
204
|
+
var node = presetSet.defaultVariant.createInstance();
|
|
205
|
+
node.setProperties({ Layout: layout, State: state });
|
|
206
|
+
return node;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
var BTN = await C.setOn('Buttons', 'Button');
|
|
210
|
+
function textButton(emphasis, label) {
|
|
211
|
+
var node = BTN.children.find(function (child) { return child.name === 'Emphasis=' + emphasis + ', Size=sm, State=Default, Content=Label'; });
|
|
212
|
+
if (!node) throw new Error('Button ' + emphasis);
|
|
213
|
+
var inst = node.createInstance();
|
|
214
|
+
setP(inst, { Label: label });
|
|
215
|
+
mode(inst, 'Emphasis', emphasis);
|
|
216
|
+
return inst;
|
|
217
|
+
}
|
|
218
|
+
var LEFT = 'M 10 4 L 6 8 L 10 12', RIGHT = 'M 6 4 L 10 8 L 6 12';
|
|
219
|
+
function navButton(name, path) {
|
|
220
|
+
var button = figma.createFrame();
|
|
221
|
+
button.name = name;
|
|
222
|
+
button.layoutMode = 'HORIZONTAL';
|
|
223
|
+
button.primaryAxisAlignItems = 'CENTER';
|
|
224
|
+
button.counterAxisAlignItems = 'CENTER';
|
|
225
|
+
mode(button, 'Emphasis', 'Quiet');
|
|
226
|
+
fill(button, 'Emphasis:emphasis/bg');
|
|
227
|
+
radius(button, 'Size:button/radius');
|
|
228
|
+
button.resize(32, 32);
|
|
229
|
+
bind(button, ['width', 'height'], 'Size:control/height-sm');
|
|
230
|
+
var glyph = icon('Icon', path, 16, 'Emphasis:emphasis/fg');
|
|
231
|
+
bind(glyph, ['width', 'height'], 'Size:icon-size/base');
|
|
232
|
+
add(button, [glyph]);
|
|
233
|
+
return button;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
removeOld('Date picker examples');
|
|
237
|
+
var desktop = figma.createFrame();
|
|
238
|
+
desktop.name = 'Date picker, desktop';
|
|
239
|
+
desktop.layoutMode = 'HORIZONTAL';
|
|
240
|
+
desktop.primaryAxisAlignItems = 'MIN';
|
|
241
|
+
bind(desktop, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Size:calendar/panel-padding');
|
|
242
|
+
bind(desktop, 'itemSpacing', 'Size:calendar/month-gap');
|
|
243
|
+
fill(desktop, 'Theme:popup/background');
|
|
244
|
+
radius(desktop, 'Size:popup/radius');
|
|
245
|
+
var col = figma.createFrame();
|
|
246
|
+
col.name = 'Presets';
|
|
247
|
+
col.layoutMode = 'VERTICAL';
|
|
248
|
+
col.fills = [];
|
|
249
|
+
bind(col, 'itemSpacing', 'Size:space/1');
|
|
250
|
+
add(col, [presetInst('List', 'Selected'), presetInst('List', 'Rest'), presetInst('List', 'Hover'), presetInst('List', 'Rest')]);
|
|
251
|
+
var main = figma.createFrame();
|
|
252
|
+
main.name = 'Calendar';
|
|
253
|
+
main.layoutMode = 'VERTICAL';
|
|
254
|
+
main.fills = [];
|
|
255
|
+
bind(main, 'itemSpacing', 'Size:space/4');
|
|
256
|
+
var nav = figma.createFrame();
|
|
257
|
+
nav.name = 'Nav';
|
|
258
|
+
nav.layoutMode = 'HORIZONTAL';
|
|
259
|
+
nav.primaryAxisAlignItems = 'CENTER';
|
|
260
|
+
nav.fills = [];
|
|
261
|
+
nav.layoutSizingHorizontal = 'FILL';
|
|
262
|
+
bind(nav, 'itemSpacing', 'Size:space/2');
|
|
263
|
+
var octoberTitle = await T('October 2026', 'subtitle-base', 'Theme:text/primary');
|
|
264
|
+
var novemberTitle = await T('November 2026', 'subtitle-base', 'Theme:text/primary');
|
|
265
|
+
octoberTitle.layoutSizingHorizontal = 'FILL';
|
|
266
|
+
novemberTitle.layoutSizingHorizontal = 'FILL';
|
|
267
|
+
octoberTitle.textAlignHorizontal = 'CENTER';
|
|
268
|
+
novemberTitle.textAlignHorizontal = 'CENTER';
|
|
269
|
+
add(nav, [navButton('Previous month', LEFT), octoberTitle, novemberTitle, navButton('Next month', RIGHT)]);
|
|
270
|
+
var months = figma.createFrame();
|
|
271
|
+
months.name = 'Months';
|
|
272
|
+
months.layoutMode = 'HORIZONTAL';
|
|
273
|
+
months.fills = [];
|
|
274
|
+
bind(months, 'itemSpacing', 'Size:calendar/month-gap');
|
|
275
|
+
add(months, [
|
|
276
|
+
await monthFrame('October', 'October 2026', monthCells(2026, 10, octoberMarks), false),
|
|
277
|
+
await monthFrame('November', 'November 2026', monthCells(2026, 11, null), false),
|
|
278
|
+
]);
|
|
279
|
+
var footer = figma.createFrame();
|
|
280
|
+
footer.name = 'Footer';
|
|
281
|
+
footer.layoutMode = 'HORIZONTAL';
|
|
282
|
+
footer.primaryAxisAlignItems = 'CENTER';
|
|
283
|
+
footer.fills = [];
|
|
284
|
+
footer.layoutSizingHorizontal = 'FILL';
|
|
285
|
+
bind(footer, 'itemSpacing', 'Size:space/3');
|
|
286
|
+
var summary = await T('Oct 5 – Oct 12 · 8 days', 'body-condense', 'Theme:text/secondary');
|
|
287
|
+
summary.layoutSizingHorizontal = 'FILL';
|
|
288
|
+
var actions = figma.createFrame();
|
|
289
|
+
actions.name = 'Actions';
|
|
290
|
+
actions.layoutMode = 'HORIZONTAL';
|
|
291
|
+
actions.primaryAxisAlignItems = 'CENTER';
|
|
292
|
+
actions.fills = [];
|
|
293
|
+
bind(actions, 'itemSpacing', 'Size:space/2');
|
|
294
|
+
add(actions, [textButton('Quiet', 'Clear'), textButton('Standard', 'Cancel'), textButton('Bold', 'Apply')]);
|
|
295
|
+
add(footer, [summary, actions]);
|
|
296
|
+
add(main, [nav, months, footer]);
|
|
297
|
+
add(desktop, [col, main]);
|
|
298
|
+
place(desktop);
|
|
299
|
+
|
|
300
|
+
var mobile = figma.createFrame();
|
|
301
|
+
mobile.name = 'Date picker, mobile';
|
|
302
|
+
mobile.layoutMode = 'VERTICAL';
|
|
303
|
+
bind(mobile, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Size:calendar/panel-padding');
|
|
304
|
+
bind(mobile, 'itemSpacing', 'Size:space/3');
|
|
305
|
+
fill(mobile, 'Theme:surface/modal');
|
|
306
|
+
var chips = figma.createFrame();
|
|
307
|
+
chips.name = 'Presets';
|
|
308
|
+
chips.layoutMode = 'HORIZONTAL';
|
|
309
|
+
chips.fills = [];
|
|
310
|
+
bind(chips, 'itemSpacing', 'Size:space/2');
|
|
311
|
+
add(chips, [presetInst('Chip', 'Selected'), presetInst('Chip', 'Rest'), presetInst('Chip', 'Rest')]);
|
|
312
|
+
var apply = textButton('Bold', 'Apply');
|
|
313
|
+
apply.layoutSizingHorizontal = 'FILL';
|
|
314
|
+
var mobileFooter = figma.createFrame();
|
|
315
|
+
mobileFooter.name = 'Footer';
|
|
316
|
+
mobileFooter.layoutMode = 'VERTICAL';
|
|
317
|
+
mobileFooter.fills = [];
|
|
318
|
+
mobileFooter.layoutSizingHorizontal = 'FILL';
|
|
319
|
+
bind(mobileFooter, 'itemSpacing', 'Size:space/2');
|
|
320
|
+
var mobileSummary = await T('Oct 5 – Oct 12 · 8 days', 'body-condense', 'Theme:text/secondary');
|
|
321
|
+
add(mobileFooter, [mobileSummary, apply]);
|
|
322
|
+
add(mobile, [
|
|
323
|
+
chips,
|
|
324
|
+
await monthFrame('October', 'October 2026', monthCells(2026, 10, octoberMarks), true),
|
|
325
|
+
mobileFooter,
|
|
326
|
+
]);
|
|
327
|
+
place(mobile);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Custom: headers for the date picker sets. Run after 04.
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
|
|
4
|
+
await C.page('Custom');
|
|
5
|
+
return await C.labelSets({
|
|
6
|
+
Day: 'One day. Selected is Bold emphasis. The range band is calendar/range-background. Today keeps an accent ring, including when it is selected.',
|
|
7
|
+
Month: 'October 2026. Sunday first, a range from the 5th to the 12th, today on the 24th, the 20th disabled, and outside days muted.',
|
|
8
|
+
Preset: 'A quick selection. List sits beside the months; Chip is the scrolling row on a narrow panel.',
|
|
9
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Date picker doc frame. Run after 04 and 05.
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var K = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
|
|
4
|
+
await K.page('Custom');
|
|
5
|
+
var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docKit'))(figma);
|
|
6
|
+
var AL = L.AL, add = L.add, T = L.T, docFrame = L.docFrame, section = L.section, hrow = L.hrow, divider = L.divider;
|
|
7
|
+
var inst = L.inst, notes = L.notes, captioned = L.captioned, usage = L.usage, stage = L.stage;
|
|
8
|
+
|
|
9
|
+
function day(state) { return inst('Day', 'State=' + state); }
|
|
10
|
+
|
|
11
|
+
var doc = await docFrame('Custom', 'COMPONENTS', 'Date picker', 'Pick one date or a period. The developer chooses which; the person using it cannot switch. Quick selections sit beside the months on a wide panel and in a scrolling row when the panel is narrow. Input type=date is only for a page that cannot load React. Components sit to the right.');
|
|
12
|
+
doc.x = 0; doc.y = 1400;
|
|
13
|
+
|
|
14
|
+
var pick = await section(doc, 'Choosing', 'Start from what is being picked.');
|
|
15
|
+
var picks = [['One date or a period', 'DatePicker', 'A field that opens the calendar.'], ['A calendar that stays on the page', 'Calendar', 'The month grid, no popup.'], ['Quick selections on their own', 'DatePresets', 'The same list the picker uses.'], ['A date the browser must provide', 'Input', 'type=date, when React is not available.']];
|
|
16
|
+
var pt = AL('VERTICAL', { name: 'Picker' }); add(pick, [pt]); pt.layoutSizingHorizontal = 'FILL';
|
|
17
|
+
for (var i = 0; i < picks.length; i++) { var r = hrow(pt); divider(r); add(r, [await T(picks[i][0], 'body-base', 'Theme:text/primary', 380), await T(picks[i][1], 'subtitle-base', 'Theme:text/accent', 200), await T(picks[i][2], 'body-condense', 'Theme:text/secondary', 520)]); }
|
|
18
|
+
|
|
19
|
+
var look = await section(doc, 'Day', 'Every state of a day. Selected is the bold emphasis, so a data-color around the picker recolors it.');
|
|
20
|
+
var s1 = stage(look, 'States');
|
|
21
|
+
var shown = ['Default', 'Hover', 'Focus', 'Today', 'Selected', 'Range start', 'Range middle', 'Range end', 'Preview', 'Outside', 'Disabled'];
|
|
22
|
+
for (var s = 0; s < shown.length; s++) add(s1, [await captioned(day(shown[s]), shown[s])]);
|
|
23
|
+
await usage(look, ['A single date, or the start and end of a period.', 'A range that runs across more than one month.', 'Quick selections for the common periods.'], ['Free text the person types. The field is a button; typed entry is not part of this component.', 'Switching between one date and a period. The developer sets mode.', 'A list of choices that is not dates. Use Radio or an item row.']);
|
|
24
|
+
await notes(look, [['Day', 'calendar/day-size · day-radius', 'Seven columns fit a phone. Touch layouts use the minimum touch target.'], ['Selected', 'emphasis/bg · fg', 'Bold, so it follows data-color.'], ['Range', 'calendar/range-background · range-color', 'The band between the circles. High contrast adds an accent edge.'], ['Today', 'calendar/today-ring', 'An inset ring, still there when today is selected.'], ['Presets', 'item/*', 'The same row as a menu item. A chip is that row in one line.'], ['Text', 'trigger-condense · selected-condense · eyebrow-minimize · subtitle-base · body-minimize · body-condense', 'One whole style each.']]);
|
|
25
|
+
return { id: doc.id, h: Math.round(doc.height), overflow: L.overflow(doc) };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Custom: Money field and Currency item. Built from the Input field look. Not run from here.
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
|
|
4
|
+
var v = C.v, fill = C.fill, stroke = C.stroke, bind = C.bind, radius = C.radius, pad = C.pad, add = C.add, T = C.T, page = C.page;
|
|
5
|
+
var icon = C.icon, comp = C.comp, textProp = C.textProp, place = C.place, removeOld = C.removeOld, arrange = C.arrange;
|
|
6
|
+
var ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
7
|
+
await page('Custom');
|
|
8
|
+
var SIZES = { xs: ['body-minimize', 'xs'], sm: ['body-condense', 'sm'], md: ['body-base', ''], lg: ['body-magnify', 'lg'] };
|
|
9
|
+
var CHEV = 'M 4 6 L 8 10 L 12 6';
|
|
10
|
+
var AFFIXES = ['Start', 'End', 'Picker'];
|
|
11
|
+
var STATES = ['Default', 'Hover', 'Focus', 'Invalid', 'Disabled', 'Read-only'];
|
|
12
|
+
var SIZE_NAMES = ['xs', 'sm', 'md', 'lg'];
|
|
13
|
+
|
|
14
|
+
async function money(affix, size, state) {
|
|
15
|
+
var c = comp('Affix=' + affix + ', State=' + state + ', Size=' + size, 'HORIZONTAL');
|
|
16
|
+
c.counterAxisAlignItems = 'CENTER';
|
|
17
|
+
var sfx = SIZES[size][1] ? '-' + SIZES[size][1] : '';
|
|
18
|
+
var disabled = state === 'Disabled';
|
|
19
|
+
fill(c, disabled ? 'Theme:surface/overlay' : 'Theme:surface/canvas');
|
|
20
|
+
var edge = state === 'Invalid' ? 'Theme:status/error' : state === 'Focus' ? 'Theme:focus/ring' : state === 'Hover' ? 'Theme:border/control-hover' : 'Theme:border/control';
|
|
21
|
+
stroke(c, edge, 'Size:field/border-width');
|
|
22
|
+
radius(c, 'Size:field/radius');
|
|
23
|
+
if (state === 'Focus') await c.setEffectStyleIdAsync(ES['focus/field'].id);
|
|
24
|
+
var textColor = disabled ? 'Theme:text/disabled' : 'Theme:text/primary';
|
|
25
|
+
var affixColor = disabled ? 'Theme:text/disabled' : 'Theme:text/secondary';
|
|
26
|
+
var amount = await T(size === 'sm' ? '12.00' : '1,234.50', SIZES[size][0], textColor);
|
|
27
|
+
amount.name = 'Amount';
|
|
28
|
+
var mark = await T(affix === 'Picker' ? 'USD' : '$', SIZES[size][0], affixColor);
|
|
29
|
+
mark.name = 'Currency';
|
|
30
|
+
function affixRow() {
|
|
31
|
+
var row = figma.createFrame();
|
|
32
|
+
row.name = 'Affix';
|
|
33
|
+
row.layoutMode = 'HORIZONTAL';
|
|
34
|
+
row.counterAxisAlignItems = 'CENTER';
|
|
35
|
+
row.itemSpacing = 4;
|
|
36
|
+
bind(row, 'itemSpacing', 'Size:space/1');
|
|
37
|
+
row.fills = [];
|
|
38
|
+
add(row, [mark]);
|
|
39
|
+
if (affix === 'Picker') {
|
|
40
|
+
var ic = icon('Chevron', CHEV, 16, affixColor);
|
|
41
|
+
add(row, [ic]);
|
|
42
|
+
var iconSize = size === 'lg' ? 'magnify' : size === 'sm' ? 'condense' : size === 'xs' ? 'minimize' : 'base';
|
|
43
|
+
bind(ic, ['width', 'height'], 'Size:icon-size/' + iconSize);
|
|
44
|
+
}
|
|
45
|
+
return row;
|
|
46
|
+
}
|
|
47
|
+
function hairline() {
|
|
48
|
+
var line = figma.createRectangle();
|
|
49
|
+
line.name = 'Divider';
|
|
50
|
+
fill(line, 'Theme:border/control');
|
|
51
|
+
bind(line, 'width', 'Size:field/border-width');
|
|
52
|
+
line.layoutAlign = 'STRETCH';
|
|
53
|
+
line.layoutSizingHorizontal = 'FIXED';
|
|
54
|
+
return line;
|
|
55
|
+
}
|
|
56
|
+
var row = affixRow();
|
|
57
|
+
var line = affix === 'Picker' ? hairline() : null;
|
|
58
|
+
if (affix === 'End') add(c, line ? [amount, line, row] : [amount, row]);
|
|
59
|
+
else add(c, line ? [row, line, amount] : [row, amount]);
|
|
60
|
+
amount.layoutSizingHorizontal = 'FILL';
|
|
61
|
+
pad(c, 'Size:control/padding-block' + sfx, 'Size:control/padding-inline' + sfx);
|
|
62
|
+
c.resize(280, 44);
|
|
63
|
+
// resize() flips the axis modes. Put them back on purpose, then bind the height.
|
|
64
|
+
c.primaryAxisSizingMode = 'FIXED';
|
|
65
|
+
c.counterAxisSizingMode = 'AUTO';
|
|
66
|
+
bind(c, 'height', 'Size:control/height-' + size);
|
|
67
|
+
return c;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
removeOld('Money field');
|
|
71
|
+
var comps = [];
|
|
72
|
+
for (var a = 0; a < AFFIXES.length; a++) {
|
|
73
|
+
for (var s = 0; s < STATES.length; s++) {
|
|
74
|
+
for (var z = 0; z < SIZE_NAMES.length; z++) comps.push(await money(AFFIXES[a], SIZE_NAMES[z], STATES[s]));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
var set = figma.combineAsVariants(comps, figma.currentPage);
|
|
78
|
+
set.name = 'Money field';
|
|
79
|
+
arrange(set, 980);
|
|
80
|
+
set.clipsContent = false;
|
|
81
|
+
textProp(set, 'Amount', '1,234.50', 'Amount');
|
|
82
|
+
textProp(set, 'Currency', '$', 'Currency');
|
|
83
|
+
set.description = 'An amount of money. Affix is the currency symbol at the start, at the end, or a picker that shows the code with a divider. State matches the field. Size is xs, sm, md or lg. The amount is minor units in code; this text is the formatted major amount.';
|
|
84
|
+
place(set);
|
|
85
|
+
|
|
86
|
+
removeOld('Currency item');
|
|
87
|
+
figma.currentPage.findAll(function (n) { return n.name === 'Currency item' && n.type === 'COMPONENT'; }).forEach(function (n) { if (!n.removed) n.remove(); });
|
|
88
|
+
var item = comp('Currency item', 'HORIZONTAL');
|
|
89
|
+
item.counterAxisAlignItems = 'CENTER';
|
|
90
|
+
item.itemSpacing = 12;
|
|
91
|
+
bind(item, 'itemSpacing', 'Size:space/3');
|
|
92
|
+
item.resize(240, 36);
|
|
93
|
+
item.primaryAxisSizingMode = 'FIXED';
|
|
94
|
+
item.counterAxisSizingMode = 'AUTO';
|
|
95
|
+
bind(item, 'height', 'Size:control/height-sm');
|
|
96
|
+
fill(item, 'Theme:surface/canvas');
|
|
97
|
+
pad(item, 'Size:item/padding-block', 'Size:item/padding-inline');
|
|
98
|
+
var code = await T('USD · US dollar', 'body-base', 'Theme:text/primary');
|
|
99
|
+
code.name = 'Label';
|
|
100
|
+
code.layoutSizingHorizontal = 'FILL';
|
|
101
|
+
var sym = await T('$', 'body-base', 'Theme:text/secondary');
|
|
102
|
+
sym.name = 'Symbol';
|
|
103
|
+
add(item, [code, sym]);
|
|
104
|
+
textProp(item, 'Label', 'USD · US dollar', 'Label');
|
|
105
|
+
textProp(item, 'Symbol', '$', 'Symbol');
|
|
106
|
+
item.description = 'One row in the currency picker: the code, the currency name, and the symbol.';
|
|
107
|
+
place(item);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Custom: a header for the Money field set. Run after 07.
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
|
|
4
|
+
await C.page('Custom');
|
|
5
|
+
return await C.labelSets({
|
|
6
|
+
'Money field': 'An amount of money. The symbol sits at the start or the end, following the locale, or the affix is a picker that shows the code with a divider. Sizes are xs, sm, md and lg. Invalid, disabled and read-only match the field. The amount in code is minor units.',
|
|
7
|
+
'Currency item': 'One currency in the picker: code, name, and a muted symbol.',
|
|
8
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Custom doc frame for Money field. Run after 07 and 08. Do not run this file from the repo.
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var K = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
|
|
4
|
+
await K.page('Custom');
|
|
5
|
+
var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docKit'))(figma);
|
|
6
|
+
var AL = L.AL, add = L.add, T = L.T, docFrame = L.docFrame, section = L.section, hrow = L.hrow, divider = L.divider;
|
|
7
|
+
var inst = L.inst, notes = L.notes, captioned = L.captioned, usage = L.usage, stage = L.stage;
|
|
8
|
+
|
|
9
|
+
function M(affix, state, size) {
|
|
10
|
+
return inst('Money field', 'Affix=' + affix + ', State=' + (state || 'Default') + ', Size=' + (size || 'md'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function setP(node, props) {
|
|
14
|
+
var p = {};
|
|
15
|
+
Object.keys(node.componentProperties).forEach(function (k) {
|
|
16
|
+
var base = k.split('#')[0];
|
|
17
|
+
if (props[base] !== undefined) p[k] = props[base];
|
|
18
|
+
});
|
|
19
|
+
node.setProperties(p);
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var doc = await docFrame('Custom', 'COMPONENTS', 'Money field', 'Type an amount of money in one currency, or pick the currency. The value in code is an integer in minor units (cents, yen, fils). Number field is for a plain quantity. Components sit to the right.');
|
|
24
|
+
doc.x = 1600;
|
|
25
|
+
doc.y = 400;
|
|
26
|
+
|
|
27
|
+
var pick = await section(doc, 'Choosing', 'Start from what the person is entering.');
|
|
28
|
+
var picks = [
|
|
29
|
+
['An amount of money', 'Money field', 'One currency, or a picker when there are several.'],
|
|
30
|
+
['A plain quantity', 'Number field', 'Counts, percentages, anything that is not money.'],
|
|
31
|
+
['A line of text', 'Input', 'Names, email, search.'],
|
|
32
|
+
];
|
|
33
|
+
var pt = AL('VERTICAL', { name: 'Picker' });
|
|
34
|
+
add(pick, [pt]);
|
|
35
|
+
pt.layoutSizingHorizontal = 'FILL';
|
|
36
|
+
for (var i = 0; i < picks.length; i++) {
|
|
37
|
+
var r = hrow(pt);
|
|
38
|
+
divider(r);
|
|
39
|
+
add(r, [
|
|
40
|
+
await T(picks[i][0], 'body-base', 'Theme:text/primary', 380),
|
|
41
|
+
await T(picks[i][1], 'subtitle-base', 'Theme:text/accent', 200),
|
|
42
|
+
await T(picks[i][2], 'body-condense', 'Theme:text/secondary', 520),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var look = await section(doc, 'Amounts', 'The symbol follows the locale. USD and JPY put it first. German euro and Kuwaiti dinar put it last. JPY has no decimals. KWD has three.');
|
|
47
|
+
var row = stage(look, 'Currencies');
|
|
48
|
+
var usd = M('Start', 'Default', 'md');
|
|
49
|
+
var jpy = M('Start', 'Default', 'md');
|
|
50
|
+
var kwd = M('End', 'Default', 'md');
|
|
51
|
+
var eur = M('End', 'Default', 'md');
|
|
52
|
+
add(row, [
|
|
53
|
+
await captioned(usd, 'USD'),
|
|
54
|
+
await captioned(jpy, 'JPY'),
|
|
55
|
+
await captioned(kwd, 'KWD'),
|
|
56
|
+
await captioned(eur, 'EUR, German'),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
var signed = await section(doc, 'Negative', 'A negative amount takes the text color of the palette set on the field. data-color="error" makes it error-colored. A palette on a parent does not.');
|
|
60
|
+
var signedRow = stage(signed, 'Signed');
|
|
61
|
+
var loss = setP(M('Start', 'Default', 'md'), { Amount: '-1,250.00' });
|
|
62
|
+
add(signedRow, [await captioned(loss, 'data-color="error"')]);
|
|
63
|
+
|
|
64
|
+
var open = await section(doc, 'Picker', 'The open list is Currency item rows. The trigger shows the code, and a divider separates it from the amount.');
|
|
65
|
+
var list = AL('VERTICAL', { name: 'Open list' });
|
|
66
|
+
add(open, [list]);
|
|
67
|
+
list.layoutSizingHorizontal = 'HUG';
|
|
68
|
+
[
|
|
69
|
+
['USD · US dollar', '$'],
|
|
70
|
+
['EUR · Euro', '€'],
|
|
71
|
+
['JPY · Japanese yen', '¥'],
|
|
72
|
+
].forEach(function (pair) {
|
|
73
|
+
add(list, [setP(inst('Currency item'), { Label: pair[0], Symbol: pair[1] })]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
var err = await section(doc, 'Errors', 'Minimum, maximum and a missing amount, written as money.');
|
|
77
|
+
var er = stage(err, 'Messages');
|
|
78
|
+
add(er, [
|
|
79
|
+
await captioned(M('Start', 'Invalid', 'md'), 'Enter an amount of at least $5.00'),
|
|
80
|
+
await captioned(M('Start', 'Invalid', 'md'), 'Enter an amount of $10,000.00 or less'),
|
|
81
|
+
await captioned(M('Start', 'Invalid', 'md'), 'Enter an amount'),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
await usage(look, [
|
|
85
|
+
'A price, a balance, a refund, or a budget.',
|
|
86
|
+
'A picker when the person chooses the currency. The trigger shows the code.',
|
|
87
|
+
'The symbol on the side the locale uses, so a German amount ends with the symbol.',
|
|
88
|
+
], [
|
|
89
|
+
'A quantity that is not money. Use Number field.',
|
|
90
|
+
'An exchange rate. Changing currency re-rounds the typed number. It does not convert.',
|
|
91
|
+
'The symbol inside the typed text. It lives in the affix.',
|
|
92
|
+
]);
|
|
93
|
+
await notes(look, [
|
|
94
|
+
['Value', 'Minor units', 'Cents, yen, fils. Empty is null, not zero.'],
|
|
95
|
+
['Digits', 'The currency, or fractionDigits', 'USD 2, JPY 0, KWD 3.'],
|
|
96
|
+
['Affix', 'field/affix-color', 'Muted, inside the field border.'],
|
|
97
|
+
['Divider', 'field border', 'Between the picker and the amount.'],
|
|
98
|
+
['Text', 'body-minimize · body-condense · body-base · body-magnify', 'The same style as Input at that size, including xs.'],
|
|
99
|
+
['Popup', 'money-field/picker-min-inline-size', 'The list is wider than the code trigger.'],
|
|
100
|
+
]);
|
|
101
|
+
return { id: doc.id, h: Math.round(doc.height), overflow: L.overflow(doc) };
|