pict 1.0.366 → 1.0.368

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict",
3
- "version": "1.0.366",
3
+ "version": "1.0.368",
4
4
  "description": "Pict browser library.",
5
5
  "main": "source/Pict.js",
6
6
  "scripts": {
@@ -56,7 +56,7 @@
56
56
  "@types/sinon": "^17.0.4",
57
57
  "eslint": "^9.39.1",
58
58
  "globals": "^16.5.0",
59
- "pict-docuserve": "^0.1.5",
59
+ "pict-docuserve": "^1.0.0",
60
60
  "quackage": "^1.2.3",
61
61
  "sinon": "^20.0.0",
62
62
  "typescript": "^5.9.3"
@@ -64,8 +64,8 @@
64
64
  "dependencies": {
65
65
  "cachetrax": "^1.0.6",
66
66
  "fable": "^3.1.72",
67
- "pict-application": "^1.0.33",
68
- "pict-provider": "^1.0.12",
67
+ "pict-application": "^1.0.34",
68
+ "pict-provider": "^1.0.13",
69
69
  "pict-template": "^1.0.15",
70
70
  "pict-view": "^1.0.68"
71
71
  }
package/source/Pict.js CHANGED
@@ -30,6 +30,7 @@ const libFable = require('fable');
30
30
  const libPackage = require('../package.json');
31
31
  const libProviderFilterManager = require('./providers/Provider-Filter-Manager.js');
32
32
  const libProviderDataBroker = require('./providers/Provider-DataBroker.js');
33
+ const libProviderIcon = require('./providers/Provider-Icon.js');
33
34
 
34
35
  const PictTemplateProvider = require('./Pict-Template-Provider.js');
35
36
  const PictTemplateAudit = require('./Pict-Template-Audit.js');
@@ -185,6 +186,26 @@ class Pict extends libFable {
185
186
 
186
187
  this.addProvider('FilterManager', {}, libProviderFilterManager);
187
188
  this.addProvider('DataBroker', {}, libProviderDataBroker);
189
+ this.addProvider('Icon', {}, libProviderIcon);
190
+ }
191
+
192
+ /**
193
+ * Convenience accessor for the built-in icon registry — equivalent to
194
+ * `pict.providers.Icon.get(name, opts)`. Always returns a string
195
+ * (never throws, never undefined); a missing icon emits a visible
196
+ * question-mark glyph so the gap is obvious in the UI.
197
+ *
198
+ * @param {string} pName PascalCase icon name, or an alias.
199
+ * @param {object} [pOpts] { variant, size, class, ariaLabel }
200
+ * @returns {string}
201
+ */
202
+ icon(pName, pOpts)
203
+ {
204
+ if (!this.providers || !this.providers.Icon)
205
+ {
206
+ return '';
207
+ }
208
+ return this.providers.Icon.get(pName, pOpts);
188
209
  }
189
210
 
190
211
  /**
@@ -533,6 +554,15 @@ class Pict extends libFable {
533
554
  // Know the in-browser reference for Pict when appropriate
534
555
  this.addTemplate(require(`./templates/Pict-Template-Self.js`));
535
556
 
557
+ // Built-in icon glyph by PascalCase name.
558
+ // {~Icon:Home~} default variant
559
+ // {~Icon:Folder:Filled~} explicit variant
560
+ // {~I:Spreadsheet~} short alias
561
+ // {~I:File:Filled~} short + variant
562
+ // Backed by pict.providers.Icon — host modules register
563
+ // additional glyphs via pict.providers.Icon.registerSet().
564
+ this.addTemplate(require(`./templates/Pict-Template-Icon.js`));
565
+
536
566
  // Look up an entity template expression
537
567
  // {~Entity:Book^AppData.Some.Address.IDBook^Render-Book-Template~}
538
568
  // {~E:Book^AppData.Some.Address.IDBook^Render-Book-Template~}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Built-in icon set for Pict-Provider-Icon.
3
+ *
4
+ * Every glyph is a full <svg> string with `viewBox="0 0 24 24"` and uses
5
+ * `currentColor` for paint so it picks up surrounding text color (and
6
+ * therefore the active theme). Width/height are intentionally NOT set
7
+ * on the <svg> — the provider's CSS sizes them via `font-size` so
8
+ * consumers can scale with the natural font cascade or via an `em`-aware
9
+ * `style="font-size:20px"` from a one-off `pict.icon('X', { size: 20 })`.
10
+ *
11
+ * Variants:
12
+ * - Outline (default) — single-stroke, hollow. Lucide-style.
13
+ * - Filled — solid currentColor fill, no stroke. Used when
14
+ * the filled visual is meaningfully distinct
15
+ * (closed-shape icons mostly — see _DEFAULTS).
16
+ *
17
+ * Adding more variants (TwoTone, Duotone, custom illustrative sets) is
18
+ * just another key on the exported map plus a corresponding `registerSet`
19
+ * call from a host or section-module. No core change required.
20
+ *
21
+ * Naming convention: PascalCase canonical names. Aliases live in
22
+ * Provider-Icon.js (Gear -> Settings, House -> Home, X -> Close, etc.)
23
+ * so consumers don't fight over naming.
24
+ */
25
+
26
+ const _OutlineDefaults =
27
+ {
28
+ // File system
29
+ 'Folder': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5V18a1.5 1.5 0 0 0 1.5 1.5h15A1.5 1.5 0 0 0 21 18V9a1.5 1.5 0 0 0-1.5-1.5h-7L10.5 6H4.5A1.5 1.5 0 0 0 3 7.5z"/></svg>',
30
+ 'FolderOpen': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5V18a1.5 1.5 0 0 0 1.5 1.5h15A1.5 1.5 0 0 0 21 18v-7H3"/><path d="M3 7.5h7L11.5 9h8A1.5 1.5 0 0 1 21 10.5"/></svg>',
31
+ 'File': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
32
+ 'FileText': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="14" y2="17"/></svg>',
33
+ 'Spreadsheet': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="1.5"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="9" y1="4" x2="9" y2="20"/></svg>',
34
+ 'Image': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="1.5"/><path d="m21 16-5-5L7 20"/></svg>',
35
+ 'Code': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
36
+
37
+ // Chevrons (open shapes — Outline only makes sense)
38
+ 'ChevronUp': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 15 12 9 18 15"/></svg>',
39
+ 'ChevronDown': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>',
40
+ 'ChevronLeft': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 6 9 12 15 18"/></svg>',
41
+ 'ChevronRight': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>',
42
+
43
+ // Arrows (open shapes)
44
+ 'ArrowUp': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>',
45
+ 'ArrowDown': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>',
46
+ 'ArrowLeft': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>',
47
+ 'ArrowRight': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>',
48
+
49
+ // Actions
50
+ 'Save': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>',
51
+ 'Close': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
52
+ 'Check': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>',
53
+ 'Plus': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
54
+ 'Minus': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>',
55
+ 'Edit': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
56
+ 'Trash': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>',
57
+ 'Copy': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
58
+ 'Share': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>',
59
+ 'Search': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
60
+ 'Refresh':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>',
61
+ 'Download':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
62
+ 'Upload': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>',
63
+ 'Link': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>',
64
+ 'ExternalLink':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>',
65
+
66
+ // Status
67
+ 'Info': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
68
+ 'Warning': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
69
+ 'Error': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
70
+ 'Success': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
71
+
72
+ // UI chrome
73
+ 'Home': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-7h-6v7H4a1 1 0 0 1-1-1v-9.5z"/></svg>',
74
+ 'Settings': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
75
+ 'Menu': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>',
76
+ 'More': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>',
77
+ 'Eye': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
78
+ 'EyeOff': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>',
79
+ 'Lock': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>',
80
+ 'User': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
81
+ 'Help': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
82
+ };
83
+
84
+ // Filled variants — only where the filled visual is meaningfully distinct
85
+ // from the outline. Open-shape icons (Chevrons, Arrows, Close, Plus) have
86
+ // no Filled variant; lookup falls back to Outline.
87
+ const _FilledVariants =
88
+ {
89
+ 'Folder': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M3 7.5V18a1.5 1.5 0 0 0 1.5 1.5h15A1.5 1.5 0 0 0 21 18V9a1.5 1.5 0 0 0-1.5-1.5h-7L10.5 6H4.5A1.5 1.5 0 0 0 3 7.5z"/></svg>',
90
+ 'FolderOpen': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M3 8a1 1 0 0 1 1-1h6.5l2 2H20a1 1 0 0 1 1 1v8.5A1.5 1.5 0 0 1 19.5 20h-15A1.5 1.5 0 0 1 3 18.5V8z" fill-opacity="0.92"/></svg>',
91
+ 'File': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-.5 7V3.5L19.5 9.5H14a.5.5 0 0 1-.5-.5z"/></svg>',
92
+ 'FileText': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-.5 7V3.5L19.5 9.5H14a.5.5 0 0 1-.5-.5zM8 12.4a.6.6 0 0 1 .6-.6h6.8a.6.6 0 0 1 0 1.2H8.6a.6.6 0 0 1-.6-.6zm0 4a.6.6 0 0 1 .6-.6h4.8a.6.6 0 0 1 0 1.2H8.6a.6.6 0 0 1-.6-.6z"/></svg>',
93
+ 'Spreadsheet':'<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M4 4h16a1.5 1.5 0 0 1 1.5 1.5v13A1.5 1.5 0 0 1 20 20H4a1.5 1.5 0 0 1-1.5-1.5v-13A1.5 1.5 0 0 1 4 4zm5 1.2H4.2v3.6H9V5.2zm10.8 0H10.2v3.6h9.6V5.2zM9 9.7H4.2v3.6H9V9.7zm10.8 0H10.2v3.6h9.6V9.7zM9 14.2H4.2v4.6H9v-4.6zm10.8 0H10.2v4.6h9.6v-4.6z"/></svg>',
94
+ 'Home': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M11.3 2.2a1 1 0 0 1 1.4 0l9 7.5a1 1 0 0 1 .3.7V20a1 1 0 0 1-1 1h-5a.5.5 0 0 1-.5-.5V14a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0-.5.5v6.5a.5.5 0 0 1-.5.5H4a1 1 0 0 1-1-1v-9.5a1 1 0 0 1 .3-.7l8-7.6z"/></svg>',
95
+ 'Settings': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M19.43 12.98c.04-.32.07-.65.07-.98s-.03-.66-.07-.98l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5 0 0 0-.61-.22l-2.49 1a7.3 7.3 0 0 0-1.69-.98l-.38-2.65A.49.49 0 0 0 14 2h-4a.49.49 0 0 0-.49.42l-.38 2.65a7.6 7.6 0 0 0-1.69.98l-2.49-1a.51.51 0 0 0-.61.22l-2 3.46a.493.493 0 0 0 .12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65a.5.5 0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65A.49.49 0 0 0 10 22h4a.49.49 0 0 0 .49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65zM12 15.5A3.5 3.5 0 1 1 12 8.5a3.5 3.5 0 0 1 0 7z"/></svg>',
96
+ 'Trash': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M9 3a1 1 0 0 0-1 1v1H4.5a.5.5 0 0 0 0 1H5v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h.5a.5.5 0 0 0 0-1H16V4a1 1 0 0 0-1-1H9zm1 5a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-1 0v-9a.5.5 0 0 1 .5-.5zm4 0a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-1 0v-9a.5.5 0 0 1 .5-.5z"/></svg>',
97
+ 'User': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="12" cy="7" r="4"/><path d="M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1H4z"/></svg>',
98
+ 'Lock': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M12 2a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V7a5 5 0 0 0-5-5zm-3 5a3 3 0 0 1 6 0v3H9V7z"/></svg>',
99
+ 'Info': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm-.99 5.99a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM10.5 11h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-1 0V12h-1.5a.5.5 0 0 1 0-1z"/></svg>',
100
+ 'Warning': '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM11.5 9a.5.5 0 0 1 1 0v4a.5.5 0 0 1-1 0V9zM12 17.5a.9.9 0 1 1 0-1.8.9.9 0 0 1 0 1.8z"/></svg>'
101
+ };
102
+
103
+ module.exports =
104
+ {
105
+ Outline: _OutlineDefaults,
106
+ Filled: _FilledVariants
107
+ };
@@ -0,0 +1,323 @@
1
+ const libPictProvider = require('pict-provider');
2
+ const _BuiltInIcons = require('./Pict-Icons-Base.js');
3
+
4
+ /**
5
+ * Pict-Provider-Icon — the central icon registry.
6
+ *
7
+ * Every glyph is an SVG string with `currentColor`-driven paint, so icons
8
+ * inherit text color from the surrounding context and re-skin automatically
9
+ * when the active theme changes. The provider also injects a small base
10
+ * stylesheet so `font-size` on a parent scales icons natively — no SVG
11
+ * width/height juggling.
12
+ *
13
+ * Public surface:
14
+ * pict.icon(name [, opts]) convenience shortcut
15
+ * pict.providers.Icon.get(name [, opts]) same, via the provider
16
+ * pict.providers.Icon.has(name [, variant])
17
+ * pict.providers.Icon.register(name, svg [, opts])
18
+ * pict.providers.Icon.registerSet(byVariant)
19
+ *
20
+ * Template tag (registered by Pict-Template-Icon.js):
21
+ * {~Icon:Home~} default variant
22
+ * {~Icon:Folder:Filled~} explicit variant
23
+ * {~I:Spreadsheet~} short alias
24
+ * {~I:File:Filled~} short + variant
25
+ *
26
+ * Extension pattern — pict-section-* modules call `registerSet()` at boot
27
+ * to plug in section-specific icons (file-type glyphs, language marks,
28
+ * etc.) without forking core.
29
+ *
30
+ * Lookup order on miss:
31
+ * 1. exact (name, variant)
32
+ * 2. (name, defaultVariant) for that name
33
+ * 3. (name, Outline) — the global default variant
34
+ * 4. inline question-mark glyph + log warning; never throws
35
+ */
36
+
37
+ const _DefaultProviderConfiguration =
38
+ {
39
+ ProviderIdentifier: 'Pict-Provider-Icon',
40
+
41
+ AutoInitialize: true,
42
+ AutoInitializeOrdinal: 0,
43
+
44
+ // Variant used when {~Icon:Name~} is asked for with no explicit
45
+ // variant *and* no per-name default exists.
46
+ DefaultVariant: 'Outline',
47
+
48
+ // Default extra class applied to every emitted icon wrapper. Apps
49
+ // override per-call via opts.class. Always also gets 'pict-icon'.
50
+ DefaultIconClass: 'pict-icon',
51
+
52
+ // Default font-size when caller passes { size: N }. Icons sized via
53
+ // `em` (1em x 1em on the inner <svg>) so font-size drives dimensions.
54
+ DefaultSize: null,
55
+
56
+ // Auto-register the bundled base icon set during construction. Apps
57
+ // that want a completely custom set can set this false and seed via
58
+ // registerSet() after construction.
59
+ RegisterBaseIcons: true
60
+ };
61
+
62
+ // PascalCase aliases — let consumers reach for the name they remember
63
+ // first. Resolved at lookup time, so registering 'Settings' makes 'Gear'
64
+ // resolve to it automatically. Kept short on purpose; expansive aliasing
65
+ // is the host's job, not core's.
66
+ const _Aliases =
67
+ {
68
+ Gear: 'Settings',
69
+ Cog: 'Settings',
70
+ House: 'Home',
71
+ X: 'Close',
72
+ Cross: 'Close',
73
+ Hamburger: 'Menu',
74
+ Pencil: 'Edit',
75
+ Bin: 'Trash',
76
+ Magnifier: 'Search',
77
+ Reload: 'Refresh',
78
+ Person: 'User',
79
+ Padlock: 'Lock'
80
+ };
81
+
82
+ // Minimal CSS — sizes the icon via the parent's font cascade, no SVG
83
+ // width/height baked in. Themes recolor through currentColor; consumers
84
+ // can layer additional class rules on top.
85
+ const _ProviderCSS = (
86
+ '.pict-icon { display: inline-flex; align-items: center; justify-content: center; vertical-align: -0.125em; flex-shrink: 0; line-height: 0; color: inherit; }' +
87
+ '.pict-icon > svg { width: 1em; height: 1em; display: block; }'
88
+ );
89
+
90
+ // Inline fallback emitted when a requested icon is missing entirely.
91
+ // Visible (not invisible) so the misconfiguration is obvious in the UI
92
+ // the first time it surfaces.
93
+ const _MissingGlyph = (
94
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
95
+ 'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">' +
96
+ '<circle cx="12" cy="12" r="10"/>' +
97
+ '<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>' +
98
+ '<line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
99
+ );
100
+
101
+ class PictProviderIcon extends libPictProvider
102
+ {
103
+ constructor(pFable, pOptions, pServiceHash)
104
+ {
105
+ let tmpOptions = Object.assign({}, _DefaultProviderConfiguration, pOptions);
106
+ super(pFable, tmpOptions, pServiceHash);
107
+
108
+ /** @type {Record<string, any>} */
109
+ this.options;
110
+ /** @type {import('../Pict.js')} */
111
+ this.pict;
112
+
113
+ // Registry shape: { [variant]: { [name]: svgString } }
114
+ this._registry = {};
115
+
116
+ // Per-name default variant. Set by the first register() call for
117
+ // that name unless a later call passes { default: true }.
118
+ this._defaultsByName = {};
119
+
120
+ // Register base icons (Outline + Filled) before anyone can ask
121
+ // for them.
122
+ if (this.options.RegisterBaseIcons)
123
+ {
124
+ this.registerSet(_BuiltInIcons);
125
+ }
126
+
127
+ // Inject the base stylesheet. CSSMap dedupes by hash so this is
128
+ // safe even if multiple Pict instances register their own Icon
129
+ // providers (single-tenant in browsers, but worth honoring).
130
+ if (this.pict && this.pict.CSSMap && typeof (this.pict.CSSMap.addCSS) === 'function')
131
+ {
132
+ this.pict.CSSMap.addCSS('Pict-Provider-Icon-CSS', _ProviderCSS, 100);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Register a single glyph. First call for a (name) sets the default
138
+ * variant for that name unless an explicit variant is passed; subsequent
139
+ * calls add more variants without changing the default.
140
+ *
141
+ * @param {string} pName PascalCase icon name (e.g. 'FileFolder')
142
+ * @param {string} pSvg Full <svg>...</svg> string (no width/height)
143
+ * @param {object} [pOpts] { variant: 'Outline'|'Filled'|... , default: bool, force: bool }
144
+ * @returns {boolean} true on success, false if a (name, variant)
145
+ * already exists and { force: true } wasn't set.
146
+ */
147
+ register(pName, pSvg, pOpts)
148
+ {
149
+ if (typeof (pName) !== 'string' || pName.length < 1)
150
+ {
151
+ this.log.warn('Pict-Provider-Icon.register requires a non-empty name.');
152
+ return false;
153
+ }
154
+ if (typeof (pSvg) !== 'string' || pSvg.indexOf('<svg') < 0)
155
+ {
156
+ this.log.warn('Pict-Provider-Icon.register requires an SVG string for [' + pName + '].');
157
+ return false;
158
+ }
159
+
160
+ let tmpOpts = pOpts || {};
161
+ let tmpVariant = tmpOpts.variant || this.options.DefaultVariant;
162
+ let tmpForce = !!tmpOpts.force;
163
+ let tmpDefault = !!tmpOpts.default;
164
+
165
+ if (!this._registry[tmpVariant])
166
+ {
167
+ this._registry[tmpVariant] = {};
168
+ }
169
+ if (this._registry[tmpVariant][pName] && !tmpForce)
170
+ {
171
+ this.log.warn('Pict-Provider-Icon: refusing to overwrite [' + pName + ':' + tmpVariant
172
+ + '] (pass { force: true } to replace).');
173
+ return false;
174
+ }
175
+ this._registry[tmpVariant][pName] = pSvg;
176
+
177
+ // First registration for this name → it becomes the default,
178
+ // regardless of which variant it was registered under. This is
179
+ // what "default is first registered" means in practice.
180
+ if (!this._defaultsByName[pName] || tmpDefault)
181
+ {
182
+ this._defaultsByName[pName] = tmpVariant;
183
+ }
184
+ return true;
185
+ }
186
+
187
+ /**
188
+ * Bulk register a whole icon set, keyed by variant.
189
+ *
190
+ * registerSet({ Outline: { Home: '<svg/>', Folder: '<svg/>' },
191
+ * Filled: { Home: '<svg/>' } });
192
+ *
193
+ * Per-name defaults follow registration order — the first variant
194
+ * containing a given name becomes that name's default.
195
+ *
196
+ * @param {Record<string, Record<string, string>>} pByVariant
197
+ * @param {object} [pOpts] { force: bool }
198
+ */
199
+ registerSet(pByVariant, pOpts)
200
+ {
201
+ if (!pByVariant || typeof (pByVariant) !== 'object')
202
+ {
203
+ return;
204
+ }
205
+ let tmpForce = !!(pOpts && pOpts.force);
206
+ let tmpVariants = Object.keys(pByVariant);
207
+ for (let v = 0; v < tmpVariants.length; v++)
208
+ {
209
+ let tmpVariant = tmpVariants[v];
210
+ let tmpIcons = pByVariant[tmpVariant];
211
+ if (!tmpIcons) continue;
212
+ let tmpNames = Object.keys(tmpIcons);
213
+ for (let n = 0; n < tmpNames.length; n++)
214
+ {
215
+ let tmpName = tmpNames[n];
216
+ this.register(tmpName, tmpIcons[tmpName], { variant: tmpVariant, force: tmpForce });
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * @param {string} pName
223
+ * @param {string} [pVariant]
224
+ * @returns {boolean}
225
+ */
226
+ has(pName, pVariant)
227
+ {
228
+ let tmpResolved = _Aliases[pName] || pName;
229
+ if (pVariant)
230
+ {
231
+ return !!(this._registry[pVariant] && this._registry[pVariant][tmpResolved]);
232
+ }
233
+ return !!this._defaultsByName[tmpResolved];
234
+ }
235
+
236
+ /**
237
+ * Return the wrapped icon HTML for `pName`. Always returns a string
238
+ * — never throws and never returns undefined, so it's safe to embed
239
+ * in template output unchecked.
240
+ *
241
+ * @param {string} pName PascalCase icon name, or an alias
242
+ * @param {object} [pOpts] { variant, size, class, ariaLabel }
243
+ * @returns {string}
244
+ */
245
+ get(pName, pOpts)
246
+ {
247
+ let tmpName = (typeof (pName) === 'string') ? pName.trim() : '';
248
+ if (tmpName.length < 1)
249
+ {
250
+ this.log.warn('Pict-Provider-Icon.get called with empty name.');
251
+ return this._wrap(_MissingGlyph, pOpts);
252
+ }
253
+ // Alias resolution — Gear → Settings, etc.
254
+ let tmpResolved = _Aliases[tmpName] || tmpName;
255
+ let tmpOpts = pOpts || {};
256
+ let tmpRequestedVariant = tmpOpts.variant;
257
+
258
+ // 1. exact match for explicitly-requested variant
259
+ if (tmpRequestedVariant && this._registry[tmpRequestedVariant] && this._registry[tmpRequestedVariant][tmpResolved])
260
+ {
261
+ return this._wrap(this._registry[tmpRequestedVariant][tmpResolved], tmpOpts);
262
+ }
263
+ // 2. per-name default variant
264
+ let tmpNameDefault = this._defaultsByName[tmpResolved];
265
+ if (tmpNameDefault && this._registry[tmpNameDefault] && this._registry[tmpNameDefault][tmpResolved])
266
+ {
267
+ if (tmpRequestedVariant)
268
+ {
269
+ // Fell back from a requested-but-missing variant — log so
270
+ // the gap is visible in dev, but still emit a glyph.
271
+ this.log.trace('Pict-Provider-Icon: variant [' + tmpRequestedVariant + '] missing for ['
272
+ + tmpResolved + '], using [' + tmpNameDefault + '].');
273
+ }
274
+ return this._wrap(this._registry[tmpNameDefault][tmpResolved], tmpOpts);
275
+ }
276
+ // 3. global default variant
277
+ let tmpGlobal = this.options.DefaultVariant;
278
+ if (this._registry[tmpGlobal] && this._registry[tmpGlobal][tmpResolved])
279
+ {
280
+ return this._wrap(this._registry[tmpGlobal][tmpResolved], tmpOpts);
281
+ }
282
+ // 4. missing — show a question-mark glyph so the gap surfaces
283
+ this.log.warn('Pict-Provider-Icon: unknown icon [' + tmpName + ']'
284
+ + (tmpRequestedVariant ? ' (variant ' + tmpRequestedVariant + ')' : '') + '.');
285
+ return this._wrap(_MissingGlyph, tmpOpts);
286
+ }
287
+
288
+ /**
289
+ * @private
290
+ * @param {string} pSvg
291
+ * @param {object} [pOpts] { size, class, ariaLabel }
292
+ * @returns {string}
293
+ */
294
+ _wrap(pSvg, pOpts)
295
+ {
296
+ let tmpOpts = pOpts || {};
297
+ let tmpClass = this.options.DefaultIconClass;
298
+ if (typeof (tmpOpts.class) === 'string' && tmpOpts.class.length > 0)
299
+ {
300
+ tmpClass += ' ' + tmpOpts.class;
301
+ }
302
+ let tmpStyle = '';
303
+ if (typeof (tmpOpts.size) === 'number' && tmpOpts.size > 0)
304
+ {
305
+ tmpStyle = ' style="font-size:' + tmpOpts.size + 'px"';
306
+ }
307
+ else if (typeof (tmpOpts.size) === 'string' && tmpOpts.size.length > 0)
308
+ {
309
+ tmpStyle = ' style="font-size:' + tmpOpts.size + '"';
310
+ }
311
+ // aria-hidden default true — most icons are decorative. Caller
312
+ // passes ariaLabel to attach a label and flip aria-hidden off.
313
+ let tmpAria = ' aria-hidden="true"';
314
+ if (typeof (tmpOpts.ariaLabel) === 'string' && tmpOpts.ariaLabel.length > 0)
315
+ {
316
+ tmpAria = ' role="img" aria-label="' + tmpOpts.ariaLabel.replace(/"/g, '&quot;') + '"';
317
+ }
318
+ return '<span class="' + tmpClass + '"' + tmpStyle + tmpAria + '>' + pSvg + '</span>';
319
+ }
320
+ }
321
+
322
+ module.exports = PictProviderIcon;
323
+ module.exports.default_configuration = _DefaultProviderConfiguration;
@@ -0,0 +1,72 @@
1
+ const libPictTemplate = require('pict-template');
2
+
3
+ /**
4
+ * Icon template tag — emits a themable SVG icon by PascalCase name.
5
+ *
6
+ * {~Icon:Home~} → default variant for 'Home'
7
+ * {~Icon:Folder:Filled~} → explicit variant
8
+ * {~I:Spreadsheet~} → short alias of {~Icon:~}
9
+ * {~I:File:Filled~} → short + variant
10
+ *
11
+ * The plugin is a thin shim over the `Icon` provider — all the registry
12
+ * + lookup + variant-fallback logic lives there. See Provider-Icon.js
13
+ * for registration, alias resolution, and per-name defaults.
14
+ *
15
+ * Sizing happens at the CSS layer: icons are wrapped in `<span class="pict-icon">`
16
+ * whose inner svg is `1em × 1em`. To resize, set `font-size` on the
17
+ * parent (e.g. a button) — no template-side sizing needed for the common
18
+ * case. One-off sizing still works through the JS API:
19
+ * `pict.icon('Save', { size: 20 })`.
20
+ */
21
+ class PictTemplateProviderIcon extends libPictTemplate
22
+ {
23
+ /**
24
+ * @param {Object} pFable - The Fable Framework instance
25
+ * @param {Object} pOptions - The options for the service
26
+ * @param {String} pServiceHash - The hash of the service
27
+ */
28
+ constructor(pFable, pOptions, pServiceHash)
29
+ {
30
+ super(pFable, pOptions, pServiceHash);
31
+
32
+ this.addPattern('{~Icon:', '~}');
33
+ this.addPattern('{~I:', '~}');
34
+ }
35
+
36
+ /**
37
+ * @param {string} pTemplateHash - Body of the tag (the bit between the
38
+ * opening pattern and the closing `~}`).
39
+ * Either "Name" or "Name:Variant".
40
+ * @returns {string}
41
+ */
42
+ render(pTemplateHash)
43
+ {
44
+ let tmpBody = (pTemplateHash || '').trim();
45
+ if (tmpBody.length < 1)
46
+ {
47
+ return '';
48
+ }
49
+ let tmpName = tmpBody;
50
+ let tmpVariant = null;
51
+ let tmpColon = tmpBody.indexOf(':');
52
+ if (tmpColon > -1)
53
+ {
54
+ tmpName = tmpBody.slice(0, tmpColon).trim();
55
+ tmpVariant = tmpBody.slice(tmpColon + 1).trim();
56
+ }
57
+
58
+ let tmpProvider = this.pict && this.pict.providers && this.pict.providers.Icon;
59
+ if (!tmpProvider || typeof (tmpProvider.get) !== 'function')
60
+ {
61
+ // Provider missing — should never happen in a normally-booted
62
+ // Pict app, but degrade silently rather than emit something
63
+ // that looks like a template tag.
64
+ return '';
65
+ }
66
+
67
+ let tmpOpts = tmpVariant ? { variant: tmpVariant } : undefined;
68
+ return tmpProvider.get(tmpName, tmpOpts);
69
+ }
70
+ }
71
+
72
+ module.exports = PictTemplateProviderIcon;